目录
一、选择排序
(1)定义
简单选择排序又称为直接选择排序,是一种最简单的选择排序算法,每次从待排序序列中选择一个最小的放在最前面。
(2)算法步骤
① 设待排序的记录存储在数组r[1...n]中,首先从r[1..n]中选择一个关键字最小的记录r[k],r[k]与r[1]交换。
②第二趟排序,从r[2..n]中选择一个关键字最小的记录r[k],r[k]与r[2]交换。
③重复上述过程,经过n-1趟排序,得到有序序列。
利用简单选择排序算法对序列{12,2,16,30,28,20,16*,6,10,18}进行非递减排序
选择排序和冒泡排序相似,一个个冒出来,不过冒泡排序通过两两交换将最大的记录交换到最后面(逆序),选择排序每次选择一个最小的记录和最前面记录交换,其他元素没动
(3)代码
void SimpleSelectSort(int r[],int n){
int k;
for(int i=0;i<n-1;i++){//n-1趟排序
k=i;
for(int j=i+1;j<n;j++){//找最小值
if(r[k]>r[j]){
k=j;//记录最小值下标
}
}
if(i!=k){
swap(r[k],r[i]);
}
}
}
(4)算法分析
①时间复杂度
简单选择排序需要n-1趟排序,每趟排序n-i次比较,总的比较次数为
简单选择排序的时间复杂度为O(n^2)
②空间复杂度
简单选择排序在交换时使用了一个辅助空间temo,空间复杂度也为O(1)
③稳定性
从上面实例中也看出,16和10交换后,排在了16*后面,最后排好序后16和16*排序前后的位置是相反的,因此简单选择排序是不稳定的。
二、 堆排序
(1)定义
堆排序是一种树形选择排序算法,简单选择排序算法每次选择一个关键字最小的记录需要O(n)的时间,而堆排序选择一个关键字最小的记录只需要O(logn)的时间
堆可以看作一棵完全二叉树的顺序存储结构,在这棵完全二叉树中,如果每一个结点的值都大于等于左右孩子的值,称为最大堆。如果每一个结点的值都小于等于左右孩子的值,称为最小堆。
如下图所示,使用顺序存储一棵最大堆(不是链式存储)
因为完全二叉树有一个性质,如果一个结点的下标为i,其左孩子下标为2i,其右孩子下标为2i+1,其双亲的下标为i/2。所以使用顺序存储【注意下标从1开始】。
堆排序充分利用堆顶记录最大(最小)的性质进行排序,每次将堆顶记录交换到最后,剩余记录调整为堆即可。
完全二叉树还是一棵平衡二叉树(左右子树的高度差≤1),且具有n个结点的完全二叉树的深度为
(2)算法步骤
算法步骤:
- 构建初始堆
- 堆顶和最后一个记录交换,即r[1]和r[n]交换,将r[1..n-1]重新调整为堆【r[1]和r[n]交换是将根(最大值)调到最后】
- 堆顶和最后一个记录交换,即r[1]和r[n-1]交换,将r[1..n-2]重新调整为堆【r[1]和r[n]交换是将根(次大值)调到后面】
- 循环n-1次,得到一个有序序列
因为构建初始堆需要反复调整为堆,所以先说明如何调整堆,然后再讲解如何构建初始堆,进行堆排序
(3)调整堆(下沉)
堆排序时寻找第二大值,首先将堆顶30和最后一个记录12交换,交换后除了堆顶之外,其他结点都满足最大堆的定义,只需要将堆顶执行“下沉”操作,即可调整为堆。
“下沉”操作:堆顶与左右孩子比较,如果比孩子大,则已调整为堆;如果比孩子小,则与较大的孩子交换,交换到新的位置后,继续向下比较,从根节点一直比较到叶子。【不满足堆定义一直向下直到叶子】
如下图所示,下沉寻找第二大的值
调整堆的过程就是堆顶从根到叶子“下沉”的过程
下沉的代码如下
void Sank(int k,int n){
while(2*k<=n){//如果有左孩子,k的左孩子为2k,右孩子为2k+1
int j=2*k//j指向左孩子
if(j<n&&r[j]<r[j+1])//如果有右孩子,且左孩子比右孩子小
j++;//j指向右孩子
if(r[k]>r[j])//比“较大孩子”大
break;//已满足堆
else
swap(r[k],r[j]);//与较大孩子交换
k=j;//k指向交换后的新位置,继续向下比较,一直下沉到叶子
}
}
(4)构建初始堆
构建初始堆过程:首先按照完全二叉树的顺序构建一棵完全二叉树,然后从最后一个分支结点n/2开始调整堆,依次将序号为n/2-1,n/2-2,……,1的结点依次执行下沉操作调整为堆。
构建初始堆要从最后一个分支结点开始到1号结点逆序调整堆,因为如果从1号结点开始,下沉操作需要其他结点都满足最大堆的定义,那么这将无法下沉,从最后一个分支结点开始能够保证每个结点以下的部分是满足最大堆的定义,然后慢慢调整
对无序序列{12,16,2,30,28,20,16*,6,10,18}构建初始堆(最大堆)
void CreateHeap(int n){
for(int i=n/2;i>=1;i--){//从最后一个分支节点n/2开始下沉调整为堆,直到第一个结点
Sank(i,n);
}
}
(5)堆排序
构建初始堆之后,开始进行堆排序。因为最大堆的堆顶是最大的记录,可以将堆顶交换到最后一个元素的位置,然后堆顶执行下沉操作,调整r[1..n-1]为堆即可。重复此过程,直到生于一个结点,得到有序序列
void HeapSort(int n){
CreateHeap(n);
for(int i=n;i>1;i--){
swap(r[i],r[1]);//堆顶与最后一个记录交换
Sank(1,n);//堆顶下沉
}
}
对上面那个初始堆进行堆排序,寻找第二大值已在下沉部分完成,接下来展示后面的堆排序过程
将n-1的元素放顶堆下沉就行,所以只用排序n-1趟,并且16*本来在后面的,现在排在了16前面,所以不稳定
(6)算法分析
堆排序的运行时间主要耗费在构建初始堆和反复调整堆上。
构建初始堆需要从最后一个分支节点n/2到第一个结点进行下沉操作,下沉操作最多达到树的深度logn,因此构建初始堆的时间复杂度上界是O(nlogn),而实际上这是一个比较大的上届,大多数分支结点的下沉操作少于logn。
堆排序的过程中,每一趟排序需要从堆顶下沉到叶子,下沉操作为树的深度logn,一共n-1趟排序,总的时间复杂度为O(nlogn)
交换记录时需要一个辅助空间,使用辅助空间为常数,空间复杂度为O(1)
堆排序时多次交换关键字,可能会发生相等关键字排序前后位置不一致的情况,因此堆排序是不稳定的排序方法
三、分配排序
分配排序不需要比较关键字的大小,根据关键字各位上的值,进行若干趟“分配”和“收集”实现排序
分配排序包括桶排序和基数排序
(1)桶排序
桶排序将待排序序列划分成若干个区间,每个区间可形象地看作一个桶,如果桶中的记录多于一个则使用较快的排序方法进行排序,把每个桶中的记录依次收集起来,得到有序序列。
注意:
- 桶排序的数据最好是均匀分布的【不能都在一个桶里】
- 桶排序针对不同的数据选择的划分方法是不同的
- 桶内排序时使用的比较排序算法也有可能不同
例如,有10个学生的成绩(68,75,54,70,83,48,80,12,75*,92),对改成绩序列进行桶排序。学生成绩在0~100,可以划分为10个桶,即0~9,10~19,20~39,……90~100,将学生成绩依次放入桶中,然后对桶中数据排序
(2)基数排序
基数排序可以看作桶排序的扩展,它是一种多关键字排序算法。如果记录按照多个关键字排序,则依次按照这些关键字进行排序。例如扑克牌排序,扑克牌由数字面值和花色两个关键字组成,可以先按照面值(2,3,……10,J,Q,K,A)排序,再按照花色排序
如果记录按照一个数值型的关键字排序,可以把该关键字看作由d位组成的多关键字排序,每一位的值取值范围为[0,r),其中r称为基数。
例如,十进制数268由3位数组成,每一位的取值范围为[0,r),十进制数的基数r为10
算法步骤:
①求出待排序序列中最大关键字的位数d,然后从低位到高位进行基数排序
②按个位i二将关键字依次分配到桶中,将每个桶中的数据依次收集起来
③按十位将关键字依次分配到桶中,将每个桶中的数据依次收集起来
④依次下去,直到d位处理完毕,得到一个有序序列
先从低位开始排序,低位已经从小到大排好序了,那么高位相等时也能知道谁大谁小
例如,有10个学生的成绩(68,75,54,70,83,48,80,12,75*,92) ,对该成绩序列进行基数排序。
算法分析
基数排序需要进行d趟排序,每一趟排序包含分配和收集,分配需要O(n)时间,收集如果使用顺序队列也需要O(n)时间,如果使用链式队列则只需要将r个链队首尾相连即可,需要O(r)时间,总的时间复杂度位O(d(n+r))。
如果使用顺序队列,需要r个大小为n的队列(因为不知道某一位相同的有几个) ,空间复杂度位O(rn)。如果使用链式队列,则需要n个结点+r个结点(基数也要有个指针),空间复杂度为O(n+r)
基础排序是按照关键字出现的顺序依次进行的,是稳定的