没什么写, 突发奇想总结一下以前写过的排序算法, 权当复习。
以下皆以升序举例, a为默认数组名, n默认为数组长度
冒泡排序
大多数人学的第一个排序算法就算冒泡排序, 思想是每次比较相邻的两个数, 如果与升序不符合就进行交换; 从前到后一轮交换下来, 不难发现最大值会被交换到最后位, 因此下次只需要对前n-1个元素进行遍历即可。
每轮交换至少确定一个数的位置, 所以最多需要进行n - 1轮,
时间复杂度为
代码实现
1 | void Sort(int a[], int n) { |
选择排序
思想是每次都从剩余序列中选出一个最小值放到前面, 经过n-1次选择后数组即有序。
可以发现选择排序的过程其实和冒泡排序差不多,
但选择排序第i
次选择只需要交换一次,
比较n - i + 1
次, 而冒泡排序在比较次数相同的情况下,
还会有更多次交换。 因此选择排序的常数要比冒泡排序小。
代码实现
1 | void Sort(int a[], int n) { |
插入排序
插入排序是i
个元素排序,
在把第i + 1
个元素插入前面已经有序的序列中。
插入排序的优势在于, 比较次数并不是死板的,
而是取决于元素应插入的位置。 特别的, 如果对有序序列进行插入排序,
那么每次插入只需要比较一次, 时间复杂度为
代码实现
1 | void Sort(int a[], int n) { |
希尔排序
希尔排序基于对插入排序的优化, 是第一个出现的时间复杂度低于
前面提到, 当序列有序程度增大时, 插入排序的效率也会随之升高。希尔排序会先对数组的子数组进行插入排序, 直到数组有序程度较高, 再对整个数组进行插入排序。
在子数组的选择策略上,
我们选择从第一个元素开始的、下标间隔相同的所有元素,
如[3, 8, 3, 2, 4]
, 若间隔为2,
子数组应该是[3, 3, 4]
。
在实现上, 我们选择多组不同的间隔, 这里也叫做增量,
按增量从大到小的顺序依次对相应的子数组进行插入排序。
这里引入一个新的名词:增量序列,
增量序列是一个递增、初始项为1的正整数序列, 即为我们选择的间隔序列,
如[1, 2, 4, 8]
,
如果我们要对长度为7的子数组使用该增量序列进行希尔排序,
则我们应该依次对增量为4, 2, 1的子数组进行插入排序,
显然增量为1的子数组即为原数组,
所以按增量序列依次进行插入排序最后肯定是能达到排序的效果的。
显然希尔排序的效率会受到增量序列的影响, 希尔排序的时间复杂度证明非常复杂, 受限于水平不在此说明。
比较常见的增量序列有发明者希尔推荐的希尔序列:[1, ..., len / 4, len / 2]
,
以及效率更高的Sedgewick序列
1 | int Sedgewick[]={28, |
代码实现
1 | void Sort(int a[], int n) { |
归并排序
归并排序是非常经典的分治算法, 思想是非常简单的:
- 将数组分为左右两半
- 对左半边数组和右半边数组进行归并排序
- 将两段有序数组合并成一个有序数组
显然很适合用递归来实现, 而用递归最重要的就算确定递归的边界, 这里的边界就是数组的长度小于1, 此时数组已然是有序的。
在合并数组时, 若要达到线性的合并时间, 就要用到一个辅助数组,
将两边数组按大小依次填入辅助数组,
最后将辅助数组的数据写回原数组;显然辅助数组的长度最大要为n,
因此空间复杂度为
在时间复杂度上, 考虑递归的层数。 因为每次递归会将数组分成两半,
所以层数最大为
代码实现
1 | void Sort(int a[], int l, int r, int t[]) { |
快速排序
快速排序也是一个分治算法, 思想如下:
- 从当前数组选出任意一个数作为枢纽p
- 把数组中所有比p小的数放到p左边, 大的数放到右边(相等随意), 这个过程叫做划分。
- 对p左边的元素和右边的元素进行快速排序。
第一次看到时肯定是很迷茫的, 经过上面的递归过程数组怎么就有序了呢?
可以用类似数学归纳法的方法证明:
首先来看递归边界: 当数组的长度小于等于1时, 数组本身是有序的。
设数组经过划分后, 分出左右两个子数组为L/R, 假设L, R经过快速排序, 可以变成有序
此时有
L < p < R
, 显然当前数组也为有序
以上就是快速排序的思想, 但要想让快速排序‘快速’, 我们就要考虑划分操作的实现。
最容易想到的是, 使用两个辅助数组,
来记录两边的信息;这当然是可以的, 但空间复杂度将于归并排序相同为
但先人发明了一个非常巧妙的双指针算法, 能以
基本思想是: 使用两个指针i, j, 从数组两边想中间遍历, i会停止在大于p的元素上, j会停止在小于p的元素上;当i, j都停止时, 将i、j指向元素进行一次交换。 遍历结束于i,j相遇。
关于时间复杂度, 我们同样关注递归的深度。 但快排于归并排序不同的是, 每次划分后两边的元素数量不一定相等, 因此快排的时间复杂度会取决于数据, 具有随机性。
对于快排的最好情况:每次划分都能划分为两个元素数量差不多的两边,
此时时间复杂度为
代码实现
1 | void Sort(int a[], int l, int r) { |
堆排序
堆这个数据结构的原理就不多赘述了, 主要如何以数组自建堆、以
首先我们将待排序的数组看作一个完全二叉树, 然后自底向上的将二叉树调整为堆。
过程如下:
- 首先将i的两个子树调整为堆
- 此时将i下滤, i所在的二叉树即变成了堆。
- 递归的终止条件为, 当结点i没有子树时, 即可以返回。
而由于完全二叉树数组存储的特殊性, 我们只需要从后往前的遍历数组, 即可保证每个结点被遍历前其子树已被遍历。
经过n次遍历, 数组已经变成了一个堆。 接下来是堆排序的第二个关键点:从堆中取n次堆顶元素得到排序序列。
我们可以发现每次弹出堆顶元素, 堆的大小减一, 即所需的数组大小减一。
参考堆实现中的删除顶点元素操作:将堆顶与最后一个元素交换, 此时堆顶的两个子树都是堆, 只需要将堆顶再次下滤, 即可完成对堆的调整;而原来的堆顶元素显然就可以保存在原堆的最后一个元素那里。
时间复杂度
每次下滤操作的时间复杂度为2n
次下滤, 因此时间复杂度为
空间复杂度
由于我们始终是在对数组进行操作, 并没有使用到其它的额外空间。
因此空间复杂度为
代码实现
1 |
|