常见的排序算法(中等篇)

在本篇,我们介绍四种排序算法,分别是:归并排序、快速排序、希尔排序和堆排序。(以下代码均用C++实现)

预知以下代码中用到的swap函数的定义头文件是#include<algorithm>

一、归并排序:

  归并排序(英语:Merge sort,或mergesort),是创建在归并操作上的一种有效的排序算法,效率为 O ( n log ⁡ n ) O(n\log n) O(nlogn)(大 O O O符号)。1945年由约翰·冯·诺伊曼首次提出。该算法是采用分治法(Divide and Conquer)的一个非常典型的应用,且各层分治递归可以同时进行:

  • 分割:递归地把当前序列平均分割成两半。
  • 集成:在保持元素顺序的同时将上一步得到的子序列集成到一起(归并)。

图解如下:
归并排序代码如下:

//1.归并排序
void mergeSort(int source[], int n){    //n代表数组大小
    int *a = source;
    int *b = new int[n];
    int seg, start;
    for(seg = 1; seg < n; seg += seg){
        for(start = 0; start < n; start += seg * 2){
            int low = start, mid = min(start + seg, n), high = min(start + seg * 2, n);
            int k = low;
            int start1 = low, end1 = mid;
            int start2 = mid, end2=high;
            while(start1 < end1 && start2 < end2)
                b[k++] = a[start1] < a[start2] ? a[start1++] : a[start2++];
            while(start1 < end1)
                b[k++] = a[start1++];
            while(start2 < end2)
                b[k++] = a[start2++];
        }
        int *temp = a;
        a = b;
        b = temp;
    }
    if(a != source){
        for(int i = 0; i < n; i++)
            b[i] = a[i];
        b = a;
    }
    delete[] b; //最后不要忘了释放掉b的空间,数组不要忘了加中括号("[]")
}
复杂度分析:

时间复杂度  最坏时间复杂度 O ( n log ⁡ n ) O(n\log n) O(nlogn)、最优时间复杂度 O ( n log ⁡ n ) O(n\log n) O(nlogn)、平均时间复杂度 O ( n log ⁡ n ) O(n\log n) O(nlogn)
空间复杂度  需要辅助空间 O ( n ) O(n) O(n)

二、快速排序:

  快速排序(英语:Quicksort),又称划分交换排序(partition-exchange sort),简称快排,一种排序算法,最早由东尼·霍尔提出。在平均状况下,排序n个项要 O ( n log ⁡ n ) O(n\log n) O(nlogn)(大O符号)次比较。在最坏状况下则需要 O ( n 2 ) O(n^2) O(n2)次比较,但这种状况并不常见。事实上,快速排序 O ( n log ⁡ n ) O(n\log n) O(nlogn)通常明显比其他算法更快,因为它的内部循环(inner loop)可以在大部分的架构上很有效率地达成。
  快速排序使用分治法(Divide and conquer)策略来把一个序列(list)分为较小和较大的2个子序列,然后递归地排序两个子序列。
步骤为:

  1. 挑选基准值:从数列中挑出一个元素,称为“基准”(pivot);
  2. 分割:重新排序数列,所有比基准值小的元素摆放在基准前面,所有比基准值大的元素摆在基准后面(与基准值相等的数可以到任何一边)。在这个分割结束之后,对基准值的排序就已经完成;
  3. 递归排序子序列:递归地将小于基准值元素的子序列和大于基准值元素的子序列排序。

  递归到最底部的判断条件是数列的大小是零或一,此时该数列显然已经有序。
  若想更清楚地了解快排,可查看该链接,链接中有清晰的图文描述。

图解如下:
快速排序
代码如下:

//2.快速排序
/*
你可以直接使用algorithm头文件中的sort(a, a + n)来进行快排
sort(a, a + n);//排序a[0]~a[n-1]的所有数
*/
void quickSort(int source[], int start, int end){
    if(start >= end) return;
    int mid = source[end];  //枢纽元素值(基准数)
    int left = start, right = end - 1;
    while(left<right){  //在整个范围内搜寻比枢纽元素值小或大的元素,然后将左侧元素与右侧元素交换
        while(source[left] < mid && left < right){  //试图在左侧找到一个比枢纽元更大的元素
            left++;
        }
        while(source[right] >= mid && left < right){    //试图在右侧找到一个比枢纽元更小的元素
            right--;
        }
        swap(source[left],source[right]);
    }
    if(source[left] >= source[end])
        swap(source[left],source[end]);
    else
        left++;
    quickSort(source, start, left - 1);
    quickSort(source, left + 1, end);
}

时间复杂度  最坏时间复杂度 O ( n 2 ) O(n^2) O(n2)、最优时间复杂度 O ( n log ⁡ n ) O(n\log n) O(nlogn)、平均时间复杂度 O ( n log ⁡ n ) O(n\log n) O(nlogn)
空间复杂度  需要辅助空间 O ( log ⁡ n ) O(\log n) O(logn)
  若想更加详细清楚地了解快排的复杂度分析,可以自行去维基百科搜索。

三、希尔排序:

  希尔排序(Shellsort),也称递减增量排序算法,是插入排序的一种更高效的改进版本。希尔排序是非稳定排序算法。
  希尔排序是基于插入排序的以下两点性质而提出改进方法的:

  1. 插入排序在对几乎已经排好序的数据操作时,效率高,即可以达到线性排序的效率;
  2. 但插入排序一般来说是低效的,因为插入排序每次只能将数据移动一位(即步长为1)。

  我们可以把希尔排序理解为步长更大的插入排序,即刚开始选择一个大的步长,慢慢缩小步长直至为1,这也就利用了上面的插入排序的第一条性质。

图解如下:
希尔排序

以23, 10, 4, 1的步长序列进行希尔排序。

代码如下:

//3.希尔排序
void shellSort(int source[], int n){    //n代表数组大小
    int h = 1;  //步长
    while(h < n / 3){
        h = 3 * h + 1;
    }
    while(h >= 1){
        for(int i = h; i < n; i++){
            for(int j = i; j >= h && source[j] < source[j - h]; j -= h)
                swap(source[j], source[j - h]);
        }
        h /= 3;
    }
}

时间复杂度  最优时间复杂度 O ( n ) O(n) O(n)、最坏时间复杂度:根据步长序列的不同而不同,好的是 O ( n log ⁡ 2 n ) O(n\log ^2 n) O(nlog2n)

空间复杂度  需要辅助空间 O ( 1 ) O(1) O(1)

  希尔排序的步长选择与时间复杂度的关系:
希尔排序步长——时间复杂度关系

四、堆排序:

  堆排序(英语:Heapsort)是指利用这种数据结构所设计的一种排序算法。堆是一个近似完全二叉树的结构,并同时满足堆积的性质:即子节点的键值或索引总是小于(或者大于)它的父节点。
  在堆的数据结构中,堆中的最大值总是位于根节点(在优先队列中使用堆的话堆中的最小值位于根节点)。堆中定义以下几种操作:

  1. 最大堆调整(Max Heapify):将堆的末端子节点作调整,使得子节点永远小于父节点;
  2. 创建最大堆(Build Max Heap):将堆中的所有数据重新排序;
  3. 堆排序(HeapSort):移除位在第一个数据的根节点,并做最大堆调整的递归运算。

  参考更详细的堆排序介绍,请点击该链接

图解如下:
堆排序
  如若感觉上图过于抽象,请点击上方的堆排序介绍,相信你看完了之后会对堆排序有一个更加清晰的认识。
代码如下:

//4.堆排序
void maxHeapify(int source[], int start, int end){
    int father = start;
    int son = father * 2 + 1;
    while(son <= end){  //子节点在指标范围内才进行比较
        if(son + 1 <= end && source[son] < source[son + 1]) //先比较两个子节点大小,选择最大的子节点
            son++;
        if(source[father] > source[son])    //若父节点已经大于子节点,则代表已经调整完毕,跳出该函数
            return;
        else{   //否则交换父子内容,再继续子节点和父节点的比较
            swap(source[father], source[son]);
            father = son;
            son = father * 2 + 1;
        }
    }
}

void heapSort(int source[], int n){     //n代表数组大小
    for(int i = n / 2 - 1; i >= 0; i--) //从最后一个非叶节点(n/2-1)开始调整
        maxHeapify(source,i,n-1);
    //上步循环运行完毕之后,构成了一个大顶堆

    for(int i = n - 1; i > 0; i--){ //调整元素,完毕后即完成了堆排序
        swap(source[0],source[i]);
        maxHeapify(source,0,i-1);
    }
}
复杂度分析:

时间复杂度  最坏时间复杂度 O ( n log ⁡ n ) O(n\log n) O(nlogn)、最优时间复杂度 O ( n log ⁡ n ) O(n\log n) O(nlogn)、平均时间复杂度 O ( n log ⁡ n ) O(n\log n) O(nlogn)
空间复杂度  需要辅助空间 O ( 1 ) O(1) O(1)

简要比较:

名称
排序比较

以上解释和图表大部分来自维基百科

如有错误欢迎来指正

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值