zhqherm

导航

常用排序算法(2) - 堆排序,归并排序

常用排序算法(2) - 堆排序,归并排序

堆排序

算法描述

堆排序的实现就是利用优先队列的原理达到O(n log n)的复杂度进行排序。那么对于一个无序数组,首先我们要做的是建立一个二叉堆,这个阶段需要O(n)的复杂度。然后对这个已经建立好的堆进行deleteMax操作,就可以把最大元素移除放到另外一个数组里,那么只要循环n次就可以得到一个排好序的新队列。每次deleteMax操作的复杂度是O(log n),所以n次循环就是O(n log n)的复杂度。

同时可以看出来,如果每次删除一个元素,那么数组就会空出一个位置,如果这个删除不是真的删除,而是放到最后一个空间,那么就不需要再用额外的数组,所以可以把建堆的操作和deleteMax操作都当做一个下滤的操作来处理。

  • 建堆流程:

    • 找出第一个非叶子节点,并依次往前遍历并把比子节点小的数字下滤
    • 直到根节点,此时,所有节点都将比其两个子节点要大
    • 完成一个最大堆的建立
  • 下滤流程:

    • 找出x节点的子节点
    • 找出子节点中较大的节点,如果该节点比当前节点大,交换
    • 经过交换后,可以保证该节点比两个子节点都要大,而更小的节点是子节点之一
    • 然后再把更小的节点作为当前节点,循环上面的步骤
    • 每次操作都可以保证更小的节点会被过滤向后,所以当所有操作完成后,可以保证从x到队列尾部的最小节点是叶子节点
  • 排序流程:

    • 先建立好堆,保证最大的元素在队列头部,最小的元素在队列尾部(n-1位置)
    • 交换队列头尾,此时可以保证最大元素在队列末尾(n-1位置)
    • 然后n减小,重复以上步骤,直到n为1时,可以把整个数组排序完毕

实现

//堆排序
template <typename Comparable>
void HeapSort(vector<Comparable>& a)
{
    //先通过下滤操作创建一个堆
    for (int i = a.size() / 2 - 1; i >= 0; --i)
        percDown(a, i, a.size());

    //将当前最大元素摆放到队列末尾,并通过一次下滤操作重新构建堆
    for (int j = a.size() - 1; j > 0; --j)
    {
        std::swap(a[0], a[j]);
        percDown(a, 0, j);
    }

}

int leftChile(int x)
{
    return 2 * x + 1;
}

template <typename Comparable>
void percDown(vector<Comparable>& a, int i, int n)
{
    int child = 0;
    Comparable tmp = std::move(a[i]);

    for (; leftChild(i) < n; i = child)
    {
        child = leftChild(i);
        if (child < n - 1 && a[child] < a[child + 1]) ++child;
        if (tmp < a[child]) a[i] = std::move(a[child]);
        else break;
    }
    a[i] = std::move(tmp);
}

归并排序

算法描述

归并排序也是一个O(n log n)复杂度的排序算法,而且使用的比较次数几乎是最优的。算法的基本操作是合并两个已经有序的数组,可以只通过一趟遍历,每次取两个数组中最前的元素中较小的一个,并放入第三个数组中,当两个数组都为空时,第三个数组就完成了合并。这样,单独两个有序数组的合并可以看得出来时间是线性的。

  • 如果数组长度n = 1, 此时只有一个元素,那么该数组必然是有序的。
  • 如果长度大于1,递归的将前半部分和后半部分各自归并排序,得到排序后的两部分数据
  • 对这两部分数据进行合并,使用上述遍历完成的方法即可。

算法是经典的分治算法实现,将大问题分为更小部分的问题,最后再将结果结合起来,最终得到大问题的解法。

//归并排序
template <typename Comparable>
void mergeSort(vector<Comparable>& a)
{
    vector<Comparable> tmp(a.size());
    mergeSort(a, 0, a.size() - 1);
}

//内部方法
template <typename Comparable>
void mergeSort(vector<Comparable>& a, vector<Comparable>& tmp, int left, int right)
{
    if (left >= right) return;

    int mid = (left + right) / 2;
    mergeSort(a, tmp, left, mid);
    mergeSort(a, tmp, mid + 1, right);
    merge(a, tmp, left, mid + 1, right);
}

template <typename Comparable>
void merge(vector<Comparable>& a, vector<Comparable>& tmp, int left, int mid, int right)
{
    int rightPos = mid;
    int tmpPos = left;
    int num = right - left + 1;

    //挑选前后部分中数值较小的那个
    while (left < mid && rightPos <= right)
    {
        if (a[left] <= a[rightPos]) tmp[tmpPos++] = std::move(a[left++]);
        else tmp[tmpPos++] = std::move(a[rightPos++]);
    }

    //把剩余数字放入到输出数组
    while (left < mid) tmp[tmpPos++] = std::move(a[left++]);
    while (rightPos <= right) tmp[tmpPos++] = std::move(a[rightPos++]);

    //复制回原数组
    for (int i = 0; i < num; ++i, --right)
    {
        a[right] = std::move(tmp[right]);
    }
}

后记

在JAVA中,泛型排序的比较操作代价(比较不容易内联)比移动操作要大(因为元素是引用的赋值,而不是对象拷贝),由于归并排序使用的比较次数是最少的,所以标准JAVA库中默认使用归并排序。而对于C++来说,默认情况下拷贝对象的代价是比较大的,而对象比较相对代价没有这么大(编译器会主动内联),所以通常来说尽量使用拷贝少的排序算法(例如STL中的快速排序),而C++11的移动语义可能会改变这种现状。

--摘自《数据结构与算法分析——C++语言描述》

posted on 2019-12-08 23:47  zhqherm  阅读(207)  评论(0编辑  收藏  举报