内容目录
- 1. 选择类排序
- 1.1 直接选择排序
- 1.2 堆排序
- 2. 交换类排序
- 2.1 冒泡排序
- 2.2 快速排序
- 3. 插入类排序
- 3.1 直接插入排序
- 3.2 希尔排序
- 4. 其它排序
- 4.1 归并排序
- 4.2 基数排序/桶排序
排序
1. 选择类排序
选择类排序的特征是每次从待排序集合中选择出一个最大值或者最小值。
1.1 直接选择排序
原理:两层循环,外层循环标记从哪开始遍历待排序数组,内层循环标记当前正在比较的元素。每次内层循环选出一个最小值/最大值。
ADL代码:
复杂度:
1.2 堆排序
原理:堆排序在逻辑上使用完全二叉树的结构,分为大顶堆和小顶堆两类,即根结点为最大值的二叉树和根节点为最小值的二叉树。在物理结构上一般使用数组来实现完全二叉树。以大顶堆为例,对待排序数组进行排序主要需要完成两个操作:
- 在待排序数组上构造大顶堆。
- 从堆中取出最大值,并调整堆使其根节点仍然存储最大值。
可见,使用堆排序也是每次从待排序集合中选择一个最大或者最小值,因此其属于选择排序。
那么该如何实现堆排序的这两个操作呢?
答案是:需要一个Restore操作负责:在其左右子树均已经为合法的堆的情况下,将以R为根的二叉树重建为堆。
有了Restore操作之后,就可以完成初始化整个堆和从堆中选择最大值或者最小值的操作了:
- 初始化整个堆: 从结点floor(n/2)开始直到结点1依次遍历,对以每个结点为根的子树执行Restore操作。
- 从堆中选出一个最大值:将结点1与最后一个结点交换,堆的大小减一,对新的结点1执行Restore操作。
ADL代码:
算法Restore(R, f, e. R)
/*R是存储完全二叉树的数组,索引从1开始;f是子树根节点索引;e是R中元素数量*/
R1.[初始化当前需要对比的子树的根]
i <- f.
R2.[与左右子结点对比]
// R[i]的右子结点就是R[2 * i + 1]
// 左子节点就是R[2 * i]
WHILE i <= e / 2 DO
(
IF 2 * i + 1 <= e AND R[2 * i + 1] > R[2 * i]
child <- 2 * i + 1.
ELSE child <- 2 * i.
IF R[child] > R[i] THEN
(
R[child] <-> R[i].
i <- child.
)
ELSE
(
BREAK.
)
)|
算法HeapSort(R, n. R)
/**/
H1.[初始化堆]
FOR i = n/2 TO 1 STEP -1 DO
(
Restore(R, i, n. R).
)
H2.[堆排序]
e <- n.
FOR j = n TO 2 STEP -1 DO
(
R[1] <-> R[j].
e <- e - 1.
Restore(R, 1, e. R).
)|
复杂度:时间复杂度O(nlogn)。空间复杂度O(1)。不稳定。
2. 交换类排序
2.1 冒泡排序
原理:两层循环,外层循环标记待排序数组的最后一个元素下标e,内层循环负责比较当前元素i与其下一个元素j:
- 如果i > j,交换i,j。
- 如果i <= j,什么都不做。
这样内层循环遍历到e,此时e一定存储的是从1到e中的最大元素。
ADL代码:
扩展:看一下对冒泡排序的改进p237。改变了最好情况下的时间复杂度。
复杂度:O(n2)。O(1)。稳定。
2.2 快速排序
原理:快速排序基于一种自顶向下分治的思想,步骤如下:
- 如果待排序数组为空或者只剩下一个元素,直接返回
- 从待排序数组中任意选取一个元素,将比这个元素小的元素交换到数组的左边,比这个元素大的元素交换到数组的右边,这样就得到了两个子数组。
- 分别对两个子数组进行以上操作。
可以看到,这个算法适合使用递归来实现。
算法的关键问题和步骤在于:如何高效的把小的元素交换到右边,把大的元素交换到左边?
决定算法时间复杂度的步骤是:如何从待排序数组中选择一个元素,使其尽量把原数组划分为等长的子数组?
扩展:看一下对快速排序的改进p244,使用随机数算法选取基准元素。
ADL代码:
算法Partition(R, m, n. j)
/*负责从子数组[Rm, Rn]中选择一个基准元素,并把比它小的元素交换到数组的右边,比它大的元素交换到数组的左边。
*返回值j是基准元素的下标。
*/
P1.[选取一个基准元素]
base <- R[m].
P2.[交换]
left <- m + 1. right <- n.
WHILE left < right DO
(
IF (R[left] > base && R[right] < base) THEN
(
R[left] <-> R[right].
left <- left + 1.
right <- right - 1.
)
ELSE IF R[left] <= base THEN
left <- left + 1.
ELSE
right <- right - 1.
)
P3. [返回基准元素]
IF R[left] > base
(
R[left - 1] <-> R[m].
j <- left - 1.
)
ELSE
(
R[left] <-> R[m].
j <- left.
)|
算法QSort(R, m, n. R)
Q1. [递归出口]
IF (m == n || m > n) RETURN.
Q2. [划分数组]
Partition(R, m, n. j).
Q3. [递归]
QSort(R, m, j - 1. R).
Qsort(R, j + 1, n. R).|
复杂度:
3. 插入类排序
3.1 直接插入排序
原理:两层循环,第一层循环标记前k个已经排好序的元素的下一个元素i,第二层循环将i插入到前k个元素中,使前k+1个元素有序。
ADL代码:
复杂度:复杂O(n2),最好情况下下O(n)。空间O(1)
3.2 希尔排序
原理:直接插入排序的性能与待排序数组本身的**”有序度“高度相关,原数组越有序,时间复杂度就越接近O(n),原数组越无序,时间复杂度越接近O(n2)。特别是远距离的逆序对比近距离的逆序对会显著增加比较次数**。
例如,下面这个数组(2, 1)这个逆序对需要比较101次才能调整有序
[..., 0, 2, ...100个元素..., 1, ...]
而,下面这个数组(2, 1)这个逆序对只需要比较11次。
[..., 0, 2, ...10个元素..., 1, ...]
那么为了尽量减少原数组中远距离的逆序对,需要直接在大的步长的子数组中进行排序,
例如,假设原数组有1000个元素,那么可以先选取步长为100,将如下子数组单独进行直接插入排序:
// 这里 子数组中的数字是下标而非元素的值
[1, 101, 201, ..., 901],
[2, 102, 202, ..., 902],
[3, 103, 203, ..., 903],
...
[100, 200, 300, ..., 1000]
之后逐步减少步长,然后进行排序,直到最后步长为1:
[1, 2, 3, ..., 1000] // 退化为原始的直接插入排序
这样,最后原数组会完全有序。
ADL代码:
4. 其它排序
4.1 归并排序
原理:类似快速排序,归并排序也利用了分而治之的思想,但是它是自底向上分治。步骤如下:
- 如果待排序的数组为空或者大小为1,直接返回
- 将数组从中间分开,对两个子数组分别进行以上操作
- 这时两个子数组已经有序,对两个子数组进行归并,然后返回
ADL代码:
算法Merge(R, t, m, n. X)
/**/
建立X。
算法MSort(R, m, n. R)
/**/
M1.[递归出口]
IF m == n OR m > n THEN
RETURN.
M2.[划分数组]
mid <- (m + n) / 2.
MSort(R, m, mid. R).
Msort(R, mid + 1, n. R).
M3.[归并]
Merge(R, m, mid, n. R).|
时间复杂度:O(nlogn)
4.2 基数排序/桶排序
不常考,了解即可。可能考简答题。