4. 选择排序
4.1 简单选择排序
4.1.1 算法分析和思路
-
基本思想:
在待排序的数据中选出最大(小)的元素放在其最终的位置。 -
基本操作:
- 首先通过n-1次关键字比较,从n个记录中找出关键字最小的记录,将它与第一个记录交换;
- 再通过n-2次关键字比较,从n-1个记录中找出关键字次小的记录,将它与第二个记录交换;
- 重复上述操作,共进行n-1趟排序后,排序结束。
4.1.2 算法代码
//简单选择排序算法
void SelectSort(SqList &L){
for(int i = 1; i < L.length; i++){//共进行n-1趟排序
int x = i;//x用于记录最小值的位置
for(int j = i + 1; j <= L.length; j++){
if(L.nums[j].key < L.nums[x].key){
x = j;//记录最小值位置
}
//若第i个不是最小值,将关键字最小的记录(第x个记录)与第i个记录交换
if(x != i){
Swap(L, i, x);
}
}
}
}
4.1.3 算法性能分析
- 时间复杂度: O ( n 2 ) O(n^2) O(n2)
- 空间复杂度: O ( 1 ) O(1) O(1)
- 是一种不稳定的排序方法(在此算法上稍作修改可以改为稳定排序)
4.2 堆排序
4.2.1 基本概念
-
堆(Heap)的定义
从堆的定义可以看出,堆实质是满足如下性质的完全二叉树:二叉树中任一非叶子结点均小于(大于)它的孩子结点 -
完全二叉树
树中的结点按从上到下,一层一层,每层从左到右来编号。 -
堆排序
若在输出堆顶的最小值(最大值)后,使得剩余n-1个元素的序列重新又建成一个堆,则得到n个元素的次小值(次大值)…如此反复,便能得到一个有序序列,这个过程称之为堆排序。
4.2.2 算法分析和思路
实现堆排序需解决两个问题:
- 堆的建立:如何由一个无序序列建成一个堆?
-
对一个无序序列反复筛选就可以得到一个堆;
即:从一个无序序列建堆的过程就是一个反复筛选的过程。- 单节点的二叉树是堆;
- 在完全二叉树中所有以叶子结点(序号 i>n/2)为根的子树是堆。
换句话说,所有叶子结点都不需要调整了,已经是堆了,就从完全二叉树最后一个非叶子结点开始调整。- 完全二叉树性质:最后一个非叶子节点是第n/2个元素,即编号为n/2
- 这样我们只需依次将以序号为n/2、n/2-1、… 、1的结点为根的子树均调整为堆即可。
即:对应由n个元素组成的无序序列,“筛选”只需要从第n/2个元素开始。
-
堆的实质是一个线性表,可以顺序存储一个堆
-
以小根堆建立为例:
- 初始将顺序表中元素按照序号依次排在二叉树中;(实际就是根据初始顺序表的存储顺序人为按照序号构建/“想象”的二叉树)
- 从最后一个非叶子结点开始,依次向前调整
- 调整从第n/2个元素开始,将以该元素为根的二叉树调整为堆,对应的数组中的元素对应进行交换。
- 将以序号为(n/2-1)的结点为根的二叉树调整为堆,对应的数组中的元素对应进行交换。
… - 最后将以序号为1的结点为根的二叉树调整为堆,对应的数组中的元素对应进行交换。
//建立初始堆
for(int i = n/2; i >= 1; i--){
HeapAdjust(L, i, n);
}
- 堆的调整:如何在输出堆顶元素后,调整剩余元素为一个新的堆?
-
以小根堆调整为例:
- 输出堆顶元素之后,以堆中最后一个元素代替之;
- 然后将根节点值与左、右子树的根节点值进行比较,并与其中小者进行交换;
- 重复上述操作,直至叶子结点,将得到新的堆,称这个从堆顶至叶子的调整过程为筛选。
-
输出堆顶元素之后,剩下的元素还是符合堆的定义。
当把最后一个元素调上来时,就不符合堆的定义了,就需要把调到堆顶的元素调下去。这就是“筛选”的过程。
//堆调整(小根堆)
/*
参数:
i:将序号为i的结点为根的二叉树调整为堆;
m:当前堆的最大序号;
*/
void HeapAdjust(SqList &L, int i, int m){
//沿key较小的孩子结点不断向下筛选
for(int j = 2*i; j <= m; j *= 2){
if((j < m) && (L.nums[j].key > L.nums[j+1].key)){
j++;//j为key较小的记录的索引
}
if(L.nums[i].key <= L.nums[j].key){
break;
}
Swap(L, i, j);
i = j;
}
}
4.2.3 算法示例
4.2.4 算法代码
//堆排序算法
//小根堆,最后排序结果由大到小依次排序;大根堆,由小到大
void HeapSort(SqList &L){
int n = L.length;
//建立初始堆
for(int i = n/2; i >= 1; i--){
HeapAdjust(L, i, n);
}
//进行n-1趟排序
for(int i = n; i > 1; i--){
//根与最后一个元素交换
Swap(L, 1, i);
//对L.nums[1]到L.nums[i-1]重新调整建堆
HeapAdjust(L, 1, i-1);
}
}
//堆调整(小根堆)
/*
参数:
i:将序号为i的结点为根的二叉树调整为堆;
m:当前堆的最大序号;
*/
void HeapAdjust(SqList &L, int i, int m){
//沿key较小的孩子结点不断向下筛选
for(int j = 2*i; j <= m; j *= 2){
if((j < m) && (L.nums[j].key > L.nums[j+1].key)){
j++;//j为key较小的记录的索引
}
if(L.nums[i].key <= L.nums[j].key){
break;
}
Swap(L, i, j);
i = j;
}
}
4.2.5 算法性能分析
- 时间复杂度:
O
(
n
l
o
g
n
)
O(nlogn)
O(nlogn)
- 最大优点:堆排序在最坏情况下,其时间复杂度都是 O ( n l o g n ) O(nlogn) O(nlogn)。
- 无论待排序序列中的记录是正序还是逆序排列,都不会使堆排序处于“最好”或“最坏”的状态。
- 空间复杂度: O ( 1 ) O(1) O(1)
- 是一种不稳定的排序方法
- 不适用于待排序记录个数n较少的情况,但对于n较大的文件还是很有效的。
4.2.6 堆排序应用——优先队列(PriorityQueue)
小根堆实现最小优先队列
大根堆实现最大优先队列
- 普通队列特点:FIFO 先进先出
- 优先队列:
- 最小优先队列,不管入队列顺序如何,当前最小的元素优先出队列;
- 最大优先队列,不管入队列顺序如何,当前最大的元素优先出队列;
- 可以用堆来实现优先队列
每一次入队列就是一次堆的插入操作:在二叉树最后插入入队结点;进行结点调整,使其“上浮”至合适位置。
每一次出队列就是一次堆的删除堆顶结点操作,其实就是上文提到的堆的调整:堆顶元素“出队”,以堆中最后一个元素代替之;不断“筛选”至的到新的堆。 - 小根堆的堆顶是整个堆中的最小元素,即可用小根堆实现最小优先队列
大根堆的堆顶是整个堆中的最大元素,即可用大根堆实现最大优先队列