快速排序和堆排序

为什么在平均情况下快速排序比堆排序要优秀?
对于这个问题,我们主要可以从工作机理、原理分析以及复杂度比较三个角度去理解。

1、工作机理

设有任意一数组, Python 快速排序和堆排序_数组,

目标:将其从大到小进行排序;就快排和堆排两个算法的过程作出如下分析:

快速排序过程:
S1. 在数组中定义两个指针 , p1, p2; 指针p1是从左向右,p2是从右向左。

S2. 取数组的第一个元素作为基准(理论上可以取任意一个),利用上述两个指针找出比基准小的放在左边,比基准大的放在右边。

S3. 在第二步中调整完后,基准值会将原数组分成三部分,[比基准小的数],[基准], [比基准大的数]

S4. 把S3中的 [比基准小的数], [比基准大的数] 代入S2 ;

堆排序过程:
S1. 初始化大顶堆;(该结构上是一颗完全二叉树,该过程又叫构造堆过程。)

S2. 将最后一个元素与第一个元素互换;

S3. 重新调整至大顶堆,代入S1;

2、原理分析

(1) 快速排序原理

其实就是通过不断产生新的基准值,将原数组不断地划分成两部分,然后进行大小调整。这样做的原因是因为它在迭代过程中会产生一个不可控的序列,就是[比基准小的数], [比基准大的数]。我们只知道这两个子数组里面的元素比基准大或小,实质上该数组内部元素之间的大小关系我们不清楚,但是该算法告诉我们可以通过不断选取新的基准将数组划分,可以使得这些“不可控的序列”逐渐变小直至消失,此时数组已经排好序,这就是快速排序要做的事情。

(2) 堆排序原理

其实就是利用了大顶堆的性质,而实质上它又同时是一颗完全二叉树。大顶堆的层级大小性质可以帮助我们控制部分数据的大小关系,完全二叉树的顺序储存性质可以帮助我们把数据按一定大小关系排列起来。大顶堆告诉我们上一层的数据比下一层的数据大,但我们并不清楚每一层内部元素之间的大小关系,我们只能通过不断调整堆,让它自身不断生成大顶堆,这样会使每一层内部元素的大小产生调整,调整范围也会越来越小,直至排序完毕,这就是堆排序要做的事情。

3、时间复杂度比较分析

上面大多都是陈铺序述,熟悉算法的朋友可以跳过。下面我们聚焦于两个排序算法之间的迭代过程中的一些小细节分析:

(1) 在快排的迭代过程中,我们所处理的 [比基准大的数],[比基准小的数] 序列中,在进行两个数之间大小比较时,在该局部范围内,产生“大于”或者“小于”的可能性是一样的。这意味着每比较一次必然会产生一次有意义的比较结果,会缩减接下来迭代的扫描工作量。

(2) 我们再来看看堆排序。在每一次进行重新堆调整的时候,我们在迭代时其实就已经知道,上一层的结点值一定是比下面大的。为了打乱堆结构把最后一个元素与顶堆互换时,此时我们也已经知道,互换后的元素是一定比下一层的数要小的。而在迭代时为了调整堆我们还是要进行一次已经知道结果的比较,这无疑是没有什么价值的,也就是产生了一次没有意义的比较,对接下来的迭代工作量并没有任何进展。

由上述不难看出,两种排序方式都是采用了分治的思想,注意到该两种算法在迭代实现时,大体上都分成了两条扫描路线:

A. 快速排序:

一是基于基准值调整大小时,对整个数组各项元素的扫描,该部分时间复杂度分N.

二是不断产生新的基准值对数组进行划分所产生的迭代次数,该部分时间复杂度为 Python 快速排序和堆排序_数组_02.

(Python 快速排序和堆排序_数组_02 的由来:原数组不断二分会产生的序列为 Python 快速排序和堆排序_快速排序_04, 其中 k 的值就是需要迭代的次数,而Python 快速排序和堆排序_数组_05最终会收敛于 1,亦即 Python 快速排序和堆排序_迭代_06 时, Python 快速排序和堆排序_数组_07 转换: Python 快速排序和堆排序_数组_08)

B. 堆排序:

一是在产生顶堆时对各个结点数之间的比较,会涉及到整个数组元素的扫描,该部分时间复杂度为n.

二是在调整堆的时候,对非叶子节点的扫描,该部分时间复杂度为log(n);

两者平均时间复杂度均为Python 快速排序和堆排序_快速排序_09

对于快速排序,当数组的各项元素均相等,又或者是每一次迭代时选到的基准值恰恰是数组里的最值,此时的快排时间复杂度就为o(n^2). 因为此时每一次迭代划分数组所发生的比较都是无效比较。而这样的情况发生的概率为 Python 快速排序和堆排序_数组_10, 这是非常小的概率事件了。

而对于堆排序,碍于堆的特性,在迭代时所产生的无效比较概率是相对较大甚至为100%。 因此,在实际使用中,快速排序的使用常优于堆排序。

912. 排序数组

方法一:快速排序

快速排序 将序列分成前后两部分,前一部分的数据都比后一部分的数据要小,然后再递归调用函数对两部分的序列分别进行快速排序,以此使整个序列达到有序。

定义函数 randomized_quicksort(nums, l, r) 为对 Python 快速排序和堆排序_迭代_11 数组里 Python 快速排序和堆排序_快速排序_12 的部分进行排序,每次先调用 randomized_partition 函数对 Python 快速排序和堆排序_迭代_11 数组里 Python 快速排序和堆排序_快速排序_12 的部分进行划分,并返回分界值的下标 Python 快速排序和堆排序_数组_15,然后递归调用 randomized_quicksort(nums, l, pos - 1) 和 randomized_quicksort(nums, pos + 1, r) 。

核心是划分函数,先确定一个分界值(主元 pivot),然后再进行划分。这里采用随机的方式选择主元,对当前划分区间 Python 快速排序和堆排序_快速排序_16

划分函数 Python 快速排序和堆排序_快速排序_17 维护两个指针 Python 快速排序和堆排序_快速排序_18Python 快速排序和堆排序_迭代_19,开始 Python 快速排序和堆排序_数组_20
对于任意数组下标 Python 快速排序和堆排序_数组_21

Python 快速排序和堆排序_数组_22 时,Python 快速排序和堆排序_快速排序_23
Python 快速排序和堆排序_数组_24 时,Python 快速排序和堆排序_数组_25
Python 快速排序和堆排序_迭代_26 时,Python 快速排序和堆排序_快速排序_27

每次移动指针 Python 快速排序和堆排序_迭代_19 ,如果 Python 快速排序和堆排序_快速排序_29Python 快速排序和堆排序_快速排序_18 加一,交换 Python 快速排序和堆排序_数组_31Python 快速排序和堆排序_快速排序_32,否则,继续移动指针 Python 快速排序和堆排序_迭代_19

Python 快速排序和堆排序_迭代_19 移动到 Python 快速排序和堆排序_数组_35 时结束循环,Python 快速排序和堆排序_数组_36 的数都小于等于 Python 快速排序和堆排序_快速排序_37Python 快速排序和堆排序_迭代_38 的数大于 Python 快速排序和堆排序_快速排序_37,那么交换 Python 快速排序和堆排序_快速排序_40Python 快速排序和堆排序_迭代_41 ,即能使得 Python 快速排序和堆排序_迭代_42 区间的数都小于 Python 快速排序和堆排序_快速排序_43 区间的数,完成一次划分,且分界值下标为 Python 快速排序和堆排序_快速排序_44,返回即可。

class Solution:
    def randomized_partition(self, nums, l, r):
        pivot = random.randint(l, r)
        nums[pivot], nums[r] = nums[r], nums[pivot]
        i = l - 1
        for j in range(l, r):
            if nums[j] < nums[r]:
                i += 1
                nums[j], nums[i] = nums[i], nums[j]
        i += 1
        nums[i], nums[r] = nums[r], nums[i]
        return i

    def randomized_quicksort(self, nums, l, r):
        if r <= l:  return
        mid = self.randomized_partition(nums, l, r)
        self.randomized_quicksort(nums, l, mid - 1)
        self.randomized_quicksort(nums, mid + 1, r)

    def sortArray(self, nums: List[int]) -> List[int]:
        self.randomized_quicksort(nums, 0, len(nums) - 1)
        return nums

方法二:堆排序

堆排序 是先将序列建成大根堆,使得每个父节点的元素大于等于它的子节点。此时整个序列最大值即为堆顶元素,将其与末尾元素交换,使末尾元素为最大值,然后再调整堆顶元素使得剩下的 Python 快速排序和堆排序_迭代_45

class Solution:
    def max_heapify(self, heap, root, heap_len):
        p = root
        while p * 2 + 1 < heap_len:
            l, r = p * 2 + 1, p * 2 + 2
            if heap_len <= r or heap[r] < heap[l]:
                nex = l
            else:
                nex = r
            if heap[p] < heap[nex]:
                heap[p], heap[nex] = heap[nex], heap[p]
                p = nex
            else:
                break
        
    def build_heap(self, heap):
        for i in range(len(heap) - 1, -1, -1):
            self.max_heapify(heap, i, len(heap))

    def heap_sort(self, nums):
        self.build_heap(nums)
        for i in range(len(nums) - 1, -1, -1):
            nums[i], nums[0] = nums[0], nums[i]
            self.max_heapify(nums, 0, i)
            
    def sortArray(self, nums: List[int]) -> List[int]:
        self.heap_sort(nums)
        return nums

方法三:归并排序

归并排序利用了分治的思想来对序列进行排序。对一个长为 n 的待排序的序列,将其分解成两个长度为 Python 快速排序和堆排序_数组_46

定义 mergeSort(nums, l, r) 函数表示对 nums 数组里 [l,r] 的部分进行排序,整个函数流程如下:

递归调用函数 mergeSort(nums, l, mid) 对 nums 数组里 [l,mid] 部分进行排序。

递归调用函数 mergeSort(nums, mid + 1, r) 对 nums 数组里 [mid+1,r] 部分进行排序。

此时 nums 数组里 [l,mid] 和 [mid+1,r] 两个区间已经有序,我们对两个有序区间线性归并即可使 nums 数组里 [l,r] 的部分有序。

线性归并的过程并不难理解,由于两个区间均有序,所以我们维护两个指针 i 和 j 表示当前考虑到 [l,mid] 里的第 ii 个位置和 [mid+1,r] 的第 jj 个位置。

如果 nums[i] <= nums[j] ,那么我们就将 nums[i] 放入临时数组 tmp 中并让 i += 1 ,即指针往后移。否则我们就将 nums[j] 放入临时数组 tmp 中并让 j += 1 。如果有一个指针已经移到了区间的末尾,那么就把另一个区间里的数按顺序加入 tmp 数组中即可。

这样能保证我们每次都是让两个区间中较小的数加入临时数组里,那么整个归并过程结束后 [l,r] 即为有序的。

函数递归调用的入口为 mergeSort(nums, 0, nums.length - 1),递归结束当且仅当 l >= r。

class Solution:
    def merge_sort(self, nums, l, r):
        if l == r:
            return
        mid = (l + r) // 2
        self.merge_sort(nums, l, mid)
        self.merge_sort(nums, mid + 1, r)
        tmp = []
        i, j = l, mid + 1
        while i <= mid or j <= r:
            if i > mid or (j <= r and nums[j] < nums[i]):
                tmp.append(nums[j])
                j += 1
            else:
                tmp.append(nums[i])
                i += 1
        nums[l: r + 1] = tmp

    def sortArray(self, nums: List[int]) -> List[int]:
        self.merge_sort(nums, 0, len(nums) - 1)
        return nums