初等排序
初等排序
这篇随笔写于阅读《挑战程序设计竞赛2》初等排序一章后,记录一下心得以便日后的复习与回顾。
插入排序
思路
插入排序的想法很简单,就像模拟我们打牌时把牌从小到大排序一样,我们将元素依次放到适当的位置,就完成了排序。
具体的过程如下:
我们将数组分为两部分,已经排好序的和未排好序的。初始时所有元素都属于未排序的。
-
拿出未排好序部分的第一个元素 x.
-
从已排好序的最后一个元素开始,把所有比 x 大的元素都往后移动一位,直到找到小于或等于 x 的第一个元素,把 x 插入到它后面。
-
依次执行以上操作直到整个数组都是已排序的。
伪代码
我们也可以用伪代码来描述:
for i = 0 to n-1:
v = a[i]
j = i-1
while (j >= 0 && a[j] > v)
a[j+1] = a[j]
j--
a[j+1] = v
模拟
我们以一个例子看一下整个过程:
Sample input:
6
5 2 4 6 1 3
其中第一行为 n, 第二行为待排序数组。
排序过程中的情况如下:
5 2 4 6 1 3
2 5 4 6 1 3
2 4 5 6 1 3
2 4 5 6 1 3
1 2 4 5 6 3
1 2 3 4 5 6
时间复杂度分析
考虑最坏的情况,初始时序列是全逆序的,那么循环的次数是 \(1 + 2 + 3 + ... + n-1 = n*(n-1)/2\) , 因此最坏情况下复杂度是 \(O(n^2)\) 的,而最好的情况下,即全正序的排列,只需要 \(O(n)\) 。这说明了插入排序擅长处理相对顺序整齐的排列,记住这一点,在后面的希尔排序还会提到。
冒泡排序
思路
冒泡排序,顾名思义,就是每次把最大的元素像冒泡一样,送到最后一位,该过程通过相邻元素比较大小和交换来实现。
具体过程如下:
跟插入排序类似地,将数组分为已排序部分和未排序部分,初始时刻元素都属于未排序部分。
-
从第一个元素开始,相邻元素比较大小, 若前一个元素大于后一个元素,那么交换他们。
-
i 次冒泡过程会向已排序的区域加入 i 个数,因此每次冒泡的终点都会回退一位。
-
冒泡进行到所有元素都属于已排序部分。
可以做的一个简单的优化是设置一个 flag, 每轮冒泡初始时把它置为 false, 如果发生了元素交换就把它置为 true, 若某一轮冒泡之后 flag 没有被置为 true, 说明元素已经都排好了,可以结束排序了。
伪代码
根据上述思路我们可以写出如下伪代码:
for i = n-2 to 0:
for j = 0 to i:
if a[j] > a[j+1]:
swap(a[j],a[j+1])
模拟
Sample input:
6
5 2 4 6 1 3
过程:
2 4 5 1 3 6
2 4 1 3 5 6
2 1 3 4 5 6
1 2 3 4 5 6
1 2 3 4 5 6
时间复杂度分析
循环次数大概是 \(n-1 + n-2 + ... + 1 = n*(n-1)/2\) ,也是 \(O(n^2)\) 的,效率不高。
补充
冒泡排序过程中交换的次数就是这个排列的逆序数。
选择排序
思路
选择排序的思路非常直接简单,每次选择一个最大的数放在未排好序部分的第一位, n - 1 次操作之后所有数就都被排好了。
与前面类似,排序过程中也是把数组分成了已排好序和未排好序的部分,我们遍历每一个数,每次都将其作为未排好序区的起点,每一轮内层循环都选出一个合适的数加入已排序区。
伪代码
for i = 0 to n-1:
min = i;
for j = i to n-1:
if a[j]<a[min]:
min = j
swap(a[min],a[i])
时间复杂度分析
与前两个排序算法类似,从循环结构容易看出这也是 \(O(n^2)\) 的。
希尔排序
思路
前面提过,插入排序擅长处理相对有序的数据,于是我们可能有这个想法,先大概将数据排个序,再使用插入排序,是不是会更快一些?希尔排序大概就利用了这个思路。假设 \(g_n\) 是一个以 1 结尾的递减数列,我们依次以 \(g_i\) 为间隔来进行插入排序,在前几轮排序中序列已经相对有序,最后进行间隔为 1 的插入排序就会快一些。
伪代码
for i = 0 to g.length - 1:
insertsort(a,g[i])
时间复杂度分析
最坏情况下将仍然接近 \(O(n^2)\) ,但是数学上已经证明若取 \(g_n = 3 * g_{n-1} + 1\) 这样的序列平均复杂度能达到 \(O(n^{1.25})\) 。
总结
纵观这几个排序算法,不难看出其主要思路都是将数组分成排序好的和未排序好的两个部分,通过将未排序区的元素一个一个加入已排序区,来最终实现排序。

浙公网安备 33010602011771号