最近在复习刷一些算法题,做到了排序题,就顺便整理下数据结构中的八大排序算法。
首先解释下算法的稳定性定义:对于待排序列中相同项的原来次序不能被算法改变则称该算法稳定。
八大排序的稳定性,时间复杂度、空间复杂度、稳定性总结如下图:
其中STL中的sort函数原理是:在数据量大的时候采用快速排序,分段后的数据量小于某一个门槛时便采用直接插入排序。
1.直接插入排序
思路:每次将一个待排序的元素按大小插入到前面已经排过序的序列中的合适位置,直到数据全部插入为止。
时间复杂度:O(n^2)
空间复杂度:O(1)
稳定性:稳定
1 | void insert_sort (int arr[], int n) { |
2.Shell排序
思路:希尔排序是对直接插入排序算法的改进。先取一个小于n的整数h1作为第一个增量,把全部数据分成h1个组。先在各组内进行直接排序,然后取第二个增量h2 < h1重复上面的分组和排序,直至所取的增量ht = 1(ht < ht-1 < … < h2 < h1),即所有记录放在同一组中进行直接排序为止。
时间复杂度:O(n^2)
空间复杂度:O(1)
稳定性:不稳定
对Sell排序更具体的描述和图解样例可在此点击查看
1 | void shell_sort (int arr[], int n) { |
3.冒泡排序
思路:从数组开头进行遍历,元素两两比较,若位置不对,即交换位置。
时间复杂度:O(n^2)
空间复杂度:O(1)
稳定性:稳定
1 | void bubble_sort (int arr[], int n) { |
4.快速排序
思路:对冒泡排序的改进,通过一趟排序将数据分为2份,其中一部分所有数据都比另一部分的要小,然后按此方法再对两列数据进行快排,递归进行。
递归实现
具体步骤:
- 从数列中挑出一个元素,称为 “基准”(pivot)。
- 重新排序数列,所有元素比基准值小的摆放在基准前面,所有元素比基准值大的摆在基准的后面(相同的数可以到任一边)。在这个分区退出之后,该基准就处于数列的中间位置。这个称为分区(partition)操作。
- 递归地(recursive)把小于基准值元素的子数列和大于基准值元素的子数列排序。
时间复杂度:O(nlogn), 最坏情况为O(n^2)
空间复杂度:O(nlogn), 最坏情况为O(n)
稳定性:不稳定
1 | void recurive_quick_sort (int arr[], int left, int right) { |
非递归实现
具体步骤:
- 申请一个栈,存放排序数组的起始位置和终点位置。
- 将整个数组的起始位置start和终点位置end进栈
- 出栈数据,对出栈的数据进行排序,查找基准数据所在最终的位置 pivot。
- 判断起始位置start是否小于基准位置pivot-1,如果小于则将起始位置和pivot-1为终点位置进栈
- 判断基准位置pivot+1 是否小于终点位置end,如果小于则将 pivot+1作为起始位置,end作为终点位置进栈
- 判断栈是否为空,如果不为空则重复第三步,否则退出操作。
1 | int partition (int arr[], int start, int end) { |
5.直接选择排序
思路:进行n-1次排序,每次在剩下的元素中选择最小(大)的元素放到已排好序元素的后面
时间复杂度:O(n^2)
空间复杂度:O(1)
稳定性:不稳定
1 | void select_sort (int arr[], int n) { |
6.堆排序
思路:利用heap这种数据结构(可看成一个完全二叉树),使在排序中比较的次数明显减少。
具体步骤:
- 第1趟将索引0至n-1处的全部数据建大顶(或小顶)堆,就可以选出这组数据的最大值(或最小值)。将该堆的根节点与这组数据的最后一个节点交换,就使的这组数据中最大(最小)值排在了最后。
- 第2趟将索引0至n-2处的全部数据建大顶(或小顶)堆,就可以选出这组数据的最大值(或最小值)。将该堆的根节点与这组数据的倒数第二个节点交换,就使的这组数据中最大(最小)值排在了倒数第2位。
- …
- 第k趟将索引0至n-k处的全部数据建大顶(或小顶)堆,就可以选出这组数据的最大值(或最小值)。将该堆的根节点与这组数据的倒数第k个节点交换,就使的这组数据中最大(最小)值排在了倒数第k位。
其实整个堆排序过程中, 我们只需重复做两件事:
- 建堆(初始化+调整堆, 时间复杂度为O(n));
- 拿堆的根节点和最后一个节点交换.
时间复杂度:O(nlogn)
空间复杂度:O(1)
对堆排序更具体的描述和图解样例可在此点击查看
1 | void heap_sort (int arr[], int n) { |
7.归并排序
思路:即将两个已经排序的序列合并成一个序列的操作。该算法是采用分治法(Divide and Conquer)的一个非常典型的应用。
具体步骤:
- 申请两个与已经排序序列相同大小的空间,并将两个序列拷贝其中;
- 设定最初位置分别为两个已经拷贝排序序列的起始位置,比较两个序列元素的大小,依次选择相对小的元素放到原始序列;
- 重复2直到某一拷贝序列全部放入原始序列,将另一个序列剩下的所有元素直接复制到原始序列尾。
时间复杂度:O(nlogn)
空间复杂度:O(n)
对归并排序更具体的描述和图解样例可在此点击查看
1 | void merge_sort (int arr[], int low, int high) { |
8.基数排序
思路:将所有待比较数值(正整数)统一为同样的数位长度,数位较短的数前面补零。然后,从最低位开始,依次进行一次排序。这样从最低位排序一直到最高位排序完成以后, 数列就变成一个有序序列。
具体步骤:
- 将数组按照个位数进行排序
- 将1得到的数组按照十位数进行排序
- …
- 将上一步得到的数组按照数组存在数字的最高位进行排序
时间复杂度:O(d(r + n))
空间复杂度:O(rd + n)
r代表关键字基数,d代表长度,n代表关键字个数
对基数排序样例可在此点击查看
1 | int maxbit (int arr[], int n) // 求数组内的最大位数 |