快排与堆排

本文复习一下快速排序和堆排序 2 种排序算法(为了多快好省地刷 leetcode )。

快排

主要思想:

通过一趟排序将要排序的数据分割成独立的两部分,其中一部分的所有数据都比另外一部分的所有数据都要小,然后再按此方法对这两部分数据分别进行快速排序,整个排序过程可以递归进行,以此达到整个数据变成有序序列。

时间复杂度 \(O(n \log n)\) ,空间复杂度取决于是否使用递归实现。

代码实现:

int partition(vector<int> &v, int p, int r)
{
    int x = v[r];
    int i = p - 1;
    for (int j = p; j < r; j++)
    {
        if (v[j] < x)
            i = i + 1, swap(v[i], v[j]);
    }
    swap(v[i + 1], v[r]);
    return i + 1;
}
void quickSort(vector<int> &v, int p, int r)
{
    if (p < r)
    {
        int q = partition(v, p, r);
        quickSort(v, p, q - 1);
        quickSort(v, q + 1, r);
    }
}

partition 函数的图解(图源为《算法导论》):

在这里的实现,我们默认区间的最右侧 v[r] = 4 为主元,将上面所示的数组分为 2 部分,左侧小于等于 4,右侧大于 4 。下面是函数中几个临时变量所表示的含义:

堆排

堆的性质:

子结点的键值总是小于(或者大于)它的父节点。

三个步骤:

  • 建立大顶堆,这样 nums[0] 总是为最大的元素
  • 把最大的的元素放在堆的最末尾 ,即 swap(nums[0], nums[j]), j = n-1, ..., 1.
  • 调整堆,使其满足大顶堆的性质,即 heapify(nums, j, 0).

代码实现:

class Solution {
public:
    vector<int> sortArray(vector<int>& nums)
    {
        heapSort(nums);
        return nums;
    }
    
    void heapSort(vector<int> &nums)
    {
        int n = nums.size();
        buildHeap(nums);
        for (int i = n - 1; i >= 0; --i)
        {
            swap(nums[i], nums[0]);  /* nums[0] is the max element. */
            heapify(nums, i, 0);
        }
    }
    
    /* O(n) time. */
    void buildHeap(vector<int> &nums)
    {
        int n = nums.size();
        for (int i = n / 2; i >= 0; --i)
            heapify(nums, n, i);
    }
    
    /* @nums - a max-heap
     * @n - make [0, n) be a heap
     * @idx - heapify nums[idx]
     * O(logn) time.
     */
    void heapify(vector<int> &nums, int n, int idx)
    {
        int l = (idx << 1) + 1;
        int r = l + 1;
        int largest = idx;
        if (l < n && nums[largest] < nums[l]) largest = l;
        if (r < n && nums[largest] < nums[r]) largest = r;
        if (largest != idx)
        {
            swap(nums[largest], nums[idx]);
            heapify(nums, n, largest);
        }
    }
};

heapify

heapify 函数图解如下图所示。需要注意的是,图中数组下标是从 1 开始的,而上面的代码实现是从 0 开始的。

时间复杂度 \(O(\log n)\) .

buildHeap

在表示堆的数组中,范围 $\lfloor n/2 \rfloor $ 到 \(n-1\) 是叶子节点(下标从 0 开始),对于叶子节点,自然而然会满足堆的性质,对叶子节点调用 heapify 丝毫没有影响,因此不需要调整。这就是为什么 for 循环的范围是 size/2 -> 0

时间复杂度为 \(O(n)\) .

heapSort

调用 buildHeap 后的数组,是一个大顶堆,所以 v[0] 是最大的数字,我们把它交换到数组的最末尾处。然后对 [0, heapSize) 范围内的数字进行 heapify ,因为影响的只有位置 0 ,所以只需要调用一次 heapify(v, 0) 就能使得数组满足堆的性质。

时间复杂度 \(O(n\log n)\) .

heapSort 的图解如下:

posted @ 2020-10-21 19:39  sinkinben  阅读(405)  评论(1编辑  收藏  举报