
数据结构排序算法笔记
公式显示渲染有问题,下次更新
排序
算法种类 | 最好情况 | 平均情况 | 最坏情况 | 空间复杂度 | 是否稳定 |
---|---|---|---|---|---|
直接插入排序 | O(n) | O(n^2) | O(n^2) | O(1) | 是 |
冒泡排序 | O(n) | O(n^2) | O(n^2) | O(1) | 是 |
简单选择排序 | O(n^2) | O(n^2) | O(n^2) | O(1) | 否 |
希尔排序 | 取决于间隔序列 | O(1) | 否 | ||
快速排序 | O(nlogn) | O(nlogn) | O(n^2) | O(logn) | 否 |
堆排序 | O(nlogn) | O(nlogn) | O(nlogn) | O(1) | 否 |
归并排序 | O(nlogn) | O(nlogn) | O(nlogn) | O(n) | 是 |
基数排序 | O(d(n + r)) | O(d(n + r)) | O(d(n + r)) | O(n + r) | 是 |
插入排序
算法思想
- 算法思想: 每次将一个待排序的记录按其关键字大小插入到前面已排好的子序列中,直到全部记录插入完成。
- 简单的说就是将要排序的前面的大于其的记录右移
算法实现
1 | //直接插入排序 |
1 | //直接插入排序(带哨兵) |
- 空间复杂度: $O(1)$
- 时间复杂度: 主要来自对比关键字,移动元素。
- 若有n个元素,则需要n-1趟处理
- 最好时间复杂度:
- $O(n)$
- 最坏时间复杂度:
- $O(n^{2})$
平均
时间复杂度:$O(n^2)$ (最好和最坏加和取半)- 算法稳定性:
稳定
希尔排序
思想
- 希尔排序(Shell Sort): 先追求表中元素部分有序,再逐渐逼近全局有序。
希尔排序:
先将待排序表分割成若干形如$L[i,i+d,i+2d,…,i+kd]$的特殊子表,对各个子表分别进行直接插入排序。缩小增量d,重复上述过程,直到$d=1$为止。
1 | 一般是缩小一半(但是考研五花八门) |
算法实现
1 | //希尔排序 |
算法性能分析
时间复杂度:
和增量序列$d_{1},d_{2},d_{3},…$的选择有关,目前无法用数学手段证明确切的时间复杂度,最坏时间复杂度为$O(n^{2})$,当n在某个范围内时,可达$O(n^{1.3})$
冒泡排序
原理
从后往前(或从前往后)两两比较相邻元素的值,若为逆序(即$A[i-1]>A[i]$),则交换它们,直到序列比较完。称这样过程为一趟冒泡排序。
注意:
- 如果某一趟排序过程中未发生交换,则算法可提前结束
算法实现
1 | //交换(每次交换都要移动3次) |
1 | //冒泡排序 |
算法性能分析
- 空间复杂度: $O(1)$
- 冒泡排序是稳定的
快速排序
算法思想
- 算法思想: 在待排序表$L[1 …n]$中任取一个元素pivot作为枢轴(或基准,通常取首元素),通过一趟排序将待排序表划分为独立的两部分$L[1 …k-1]$和$L[k+1 …n]$,使得$L[1 …k-1]$中的所有元素小于pivot,$l[k+1 …n]$中的所有元素大于等于pivit,则pivot放在了最终未知$L(k)$上,这个过程称为一次划分。然后分别递归地对两个子表重复上述过程,直至每部分内只有一个元素或空为止,即所有元素放在其最终位置上。
代码实现
1 | //用第一个元素将待排序序列划分成左右两个部分 |
1 | //快速排序 |
算法效率分析
- 空间复杂度 = O(递归层数)
时间复杂度:
最好
时间复杂度=$O(n\log_{2}n)$最坏
时间复杂度=$O(n^{2})$
空间复杂度:
最好
空间复杂度=$O(\log_{2}n)$最坏
空间复杂度=$O(n)$
排序情况:
- 最好: 每次选的枢轴元素都能将序列划分成均匀的两部分
- 最坏: 若序列原本就有逆序,则时间,空间复杂度最高
- 可优化:尽量选择可以把数据中分的枢纽元素
稳定性: 快速排序是不稳定的
注:
- 408原题中说,对所有尚未确定最终位置的所有元素进行一遍处理称为一趟排序,因此一次划分 $\neq$ 一趟排序。
- 一次划分可以确定一个元素的最终位置,而一趟排序也许可以确定多个元素的最终位置。
1 | 注意:快速排序是所有内部排序算法平均性能最优的排序算法 |
简单选择排序
算法思想
- 选择排序: 每一趟在待排序元素中选取关键字最小(或最大)的元素加入有序子序列。
- 注意: 两个相同元素,取先找到的
算法实现
1 | //简单选择排序 |
1 | //交换 |
算法性能
- 稳定性: 不稳定
- 适用性: 既可以用顺序表,也可用链表
堆排序
算法思想
- 若n各关键字序列$L[1…n]$满足下面某一条性质,则称为
堆(Heap)
:- 若满足:$L(i)\geq L(2i)$且$L(i)\geq L(2i+1)$,$(1\le i\le \frac{n}{2})$——大根堆(大顶堆)
- 若满足:$L(i)\le L(2i)$且$L(i)\le L(2i+1)$,$(1\le i\le \frac{n}{2})$——小根堆(小顶堆)
- 选择排序: 每一趟在待排序元素中选取关键字最小(或最大)的元素加入有序子序列。
- 建立大根堆:
- 思路:
- 把所有非终端结点都检查一遍,是否满足大根堆的要求,如果不满足,则进行调整。
- 操作:
- 检查当前结点是否满足:根$\geq$左右孩子,若不满足,将当前结点与更大的一个孩子互换。
- i的左孩子—— 2i
- i的右孩子—— 2i+1
- i的父结点—— $\lfloor \frac{i}{2}\rfloor$
- 若元素互换破坏了下一级的堆,则采用相同的方法继续往下调整(小元素不断下坠)
- 检查当前结点是否满足:根$\geq$左右孩子,若不满足,将当前结点与更大的一个孩子互换。
- 思路:
代码实现
- 过程: 先建堆,后排序
1 | //建立大根堆 |
1 | //将以k为根的子树调整为大根堆 |
- 执行过程: 看视频,讲的很好
1 | //堆排序的完整逻辑 |
算法效率分析
- 结论:
- 一个结点每下坠一层,最多需对比关键字2次
- 若树高为h,某结点在第i层,则将这个结点向下调整最多只需要下坠h-i层,关键字对比次数不超过2(h-i)
- n个结点的完全二叉树树高$h=\lfloor\log_{2}n\rfloor+1$
- 推导:
- 第i层最多有$2^{i-1}$个结点,而只有第$1\sim (h-1)$层的结点才有可能需要下坠调整
- 将整棵树调整为大根堆,关键字对比次数不超过:
1 | title:关键词对比次数 |
结论:
建堆的过程,关键字对比次数不超过4n,建堆时间复杂度$O(n)$
- 稳定性: 堆排序是不稳定的
- 特点:
- 基于大根堆的堆排序得到递增序列
- 基于小根堆的堆排序得到递减序列
堆的插入删除
- 在堆中删除元素:
归并排序
算法思想
归并: 把
两个
或多个已经有序的序列合并成一个结论: m路归并,每选出一个元素需要对比关键字m-1次
在内部排序中一般采用2路归并:
核心操作: 把数组内的两个有序序列归并为一个
代码实现
1 | int *B(int *)malloc(n*sizeof(int)); //辅助数组B |
1 | //A[low...mid]和A[mid+1...high]各自有序,将两个部分归并 |
1 | //完整代码(加上下面这一段) |
算法效率分析
特点分析:
- 2路归并的
归并树
—— 形态上就是一刻倒立的二叉树 - 二叉树的第h层最多有$2^{h-1}$个结点,若树高为$h$,则应满足$n\le 2^{h-1}$,即$h-1=\lceil log_{2}n\rceil$
- 2路归并的
结论: n个元素进行2路归并排序,归并趟数$=\lceil log_{2}n\rceil$
时间复杂度: $O(n\log_{2}n)$
- 每趟归并时间复杂度为$O(n)$
空间复杂度: $O(n)$,来自与辅助数组B
- 递归所调用的空间不会超过$\log_{2}n$
基数排序
算法思想
假设长度为n的线性表中每个结点$a_{j}$的关键字由$d$元组$(k_{j}^{d-1},k^{d-2}{j},k^{d-3}{j},…,k_{j}^{1},k_{j}^{0})$组成,其中,$0\le k_{j}^{i}\le r-1(0\le j<n,0\le i\le d-1)$,$r$称为
基数
基数排序得到递减序列的过程如下:
初始化
:设置r个空队列,$Q_{r-1},Q_{r-2},…,Q_{0}$- 按照各个关键字权重递增的次序(个,十,百),对d个关键字位分别做分配和收集
分配
:顺序扫描各个元素,若当前处理的关键字位$=x$,则将元素插入$Q_{x}$队尾收集
:把$Q_{r-1},Q_{r-2},…,Q_{0}$各个队列中的结点依次出队并链接
注意: 基数排序不是基于比较的排序算法
- 基数排序具体操作:
算法效率分析
- 基数排序稳定性:
应用
- 基数排序擅长解决的问题:
- 数据元素的关键字可以方便地拆分为d组,且d较小
- 每组关键字的取值范围不大,即r较小
- 数据元素个数n较大
本文是原创文章,采用CC BY-NC-SA 4.0协议,完整转载请注明来自星染Blog
评论