9.1 基本概念
排序(Sort):就是重新排列表中的元素,使表中的元素满⾜按关键字有序的过程。
评价排序算法的指标:时间复杂度和空间复杂度
稳定性:
排序算法分类:内部排序和外部排序;内部排序数据都在内存中,外部排序需要有读写磁盘的操作。
9.2 插入排序
9.2.1 算法思想
每次将⼀个待排序的记录按其关键字大小插入到前面已排好序的子序列中,
直到全部记录插入完成。
9.2.2 代码实现
//直接插入排序
void InsertSort(int A[],int n){
int i,j,temp;
for(i=1;i<n; i++)//将各元素插入已排好序的序列中
if(A[i]<A[i-1]){//若A[i]关键字小于前驱
temp=A[i];//用temp暂存A[i]
for(j=i-1;j>=0 && A[j]>temp;--j)//检查所有前面已排好序的元
A[j+1]=A[j];//所有大于temp的元素都向后挪位
A[j+1]=temp;//复制到插入位置
}
}
9.2.3 算法效率分析
空间复杂度: O(1)
最好时间复杂度:(全部有序): O(n)
最坏时间复杂度:(全部逆序): O(n2)
平均时间复杂度: O(n2)
算法稳定性:稳定
9.2.4 算法优化(希尔排序)
9.2.4.1 算法思想
先追求表中部分元素有序,再逐渐逼近全局有序
具体就是先将待排序表分割成若干形如L[i, i + d, i + 2.d…, i + kd]的“特殊”子表,对各个子表分别进行直接插入排序。缩小增量d,重复上述过程,直到d=1为止。
9.2.4.2 算法代码实现
//希尔排序
void Shellsort(int A[],int n){
int d, i,j;//A[0]只是暂存单元,不是哨兵,当j<=8时,插入位置已到
for(d= n/2;d>=1; d=d/2)l/步长变化
for(i=d+1; i<=n; ++i)
if(A[i]<A[i-d]){//需将A[i]插入有序增量子表
A[0]=A[i];//暂存在A[0]
for(j= i-d; j>0 && A[0]<A[jl; j-=d)
A[j+d]=A[j];//记录后移,查找插入的位置
A[j+d]=A[0];//插入
}//if
}
9.2.4.3 算法效率分析
空间复杂度:O(1)
时间复杂度:
和增量序列d, d2, d3…的选择有关,目前无法用数学手段证明确切的时间复杂度
最坏时间复杂度为O(n2),当n在某个范围内时,可达O(n1.3),优于直接插入排序
稳定性:不稳定,只能适用于顺序表,因为需要随机存取数据,而链表则不支持
9.3 交换排序
9.3.1 算法思想
交换排序:根据序列中两个元素关键字的比较结果来对换这两个记录在序列中的位置,主要的交换排序就是冒泡排序和快速排序
9.3.2 冒泡排序
9.3.2.1 算法思想
从后往前(或从前往后)两两比较相邻元素的值,若为逆序(即A[i-1]>A[i]) ,则交换它们,直到序列比较完。称这样过程为“一趟” 冒泡排序。
9.3.2.2 算法代码实现
void swap(int &a, int &b){//交换
int temp = a;
a=b;
b = temp;
}
void BubbleSort(int A[],int n){//冒泡排序
for(int i=0;i<n-1;i++){
bool flag=false; // 表示本趟冒泡是否发生交换的标志
for(int j=n-1;j>i;j--)//一趟冒泡过程
if(A[j-1]>A[j]){//若为逆序
swap(A[j-1],A[j]); //交换
flag=true;
}
if(flag==false)
return;//本趟遍历后没有发生交换,说明表已经有序
}
}
9.3.2.3 算法效率分析
空间复杂度:O(1)
时间复杂度:
最好情况下:O(n)
最坏情况下:O(n2)
平均时间复杂度:O(n2)
每次进行交换操作需要对元素进行三次移动
稳定性:稳定
冒泡排序同样适用于链表
9.3.3 快速排序
9.3.3.1 算法思想
算法思想:在待排序表L[1…n]中任取一个元素pivot作为枢轴(或基准,通常取首元素),通过一趟排序将待排序表划分为独立的两部分L[1…k-1]和L[k+1…n],使得L[1…k-1]中的所有元素小于pivot, L[k+1…n]中的所有元素大于等于pivot,则pivot放在 了其最终位置L(k)上,这个过程称为一次“划分”。然后分别递归地对两个子表重复上述过程,直至每部分内只有一个元素或空为止,即所有元素放在了其最终位置上。
9.3.3.2 实现代码
//用第一个元素将待排序序列划分成左右两个部分
int Partition(int A[],int low,int high){
int pivot=A[low] ;//第一 个元素作为枢轴
while(low<high){//用low. high搜索枢轴的最终位置
while( low<high&&A [high]>=pivot) --high;
A[low]=A[high]; // 比枢轴小的元素移动到左端
while( low<high&&A [low] <=pivot) ++low;
A[high]=A[low]; // 比枢轴大的元素移动到右端
}
A[low]=pivot; //枢轴元素存放到最终位置
return low; //返回存放枢轴的最终位置
}
void QuickSort(int A[],int low,int high){
if(low<high){ //递归跳出的条件
int pivotpos=Partition(A, low, high); //划分
QuickSort(A, low, pivotpos-1); //划分左子表
QuickSort(A, pivotpos+1,high); //划分右子表
}
}
9.3.3.3 算法效率分析
时间复杂度=O(n*递归层数)
最好时间复杂度=O(nlog2n)
最坏时间复杂度=O(n2)
空间复杂度=O(递归层数)
最好空间复杂度=O(log2n)
最坏空间复杂度=O(n)
快速排序是所有内部排序算法中平均性能最优的排序算法
平均时间复杂度为O(nlog2n)
稳定性 :不稳定
9.4 选择排序
9.4.1 简单选择排序
9.4.1.1 算法思想
选择排序:每一趟在待排序元素中选取关键字最小(或最大)的元素加入有序子序列。
n个元素的简单选择排序需要n-1趟处理
9.4.1.2 代码实现
/简单选择排序
void SelectSort(int A[],int n){
for(int i=;i<n-1;i++){//一共进行n-1趟
int min=i;//记录最小元素位置
for(int j=i+1;j<n;j++)//在A[i...n-1]中选择最小的元素
if(A[j]<A[min] ) min=j;//更新最小元素位置
if(min!=i) swap(A[i],A[min] );
//封装的swap()函数共移动元素3次
}
}
//交换
void swap(int &a, int &b){
int temp = a;
a=b;
b = temp;
}
9.4.1.3 算法效率分析
空间复杂度:O(1)
时间复杂度:O(n2)
稳定性:不稳定
既适用于顺序表,又适用于链表
9.4.2 堆排序
9.4.2.1 堆的概念
若n个关键字序列L[1…n]满足下面某一条性质, 则称为堆(Heap) :
①若满足: L(i)≥L(2i)且L(i)≥L(2i+1) (1≤i≤n/2) –大根堆(大顶堆)
②若满足: L(i)≤L(2i)且L(i)≤L(2i+1) (1≤i≤n/2) –小根堆(小顶堆)
9.4.2.2 算法思路
如果是大根堆,那么堆顶的元素是整个堆最大的元素。
根≥左、右
把所有非终端结点都检查一遍, 是否满足大根堆的要求,如果不满足,将当前结点与更大的⼀个孩子互换。
若元素互换破坏了下⼀级的堆,则采⽤相同的方法继续往下调整(小元素不断“下坠”)
9.4.2.3 算法效率分析
空间复杂度:O(1)
时间复杂度:建堆O(n),排序O(log2n),总的时间复杂度O(nlog2n)
稳定性:不稳定
特性:基于大根堆的堆排序得到"递增序列",基于小根堆的堆排序得到"递减序列".
9.4.2.4 堆元素的插入和删除
插入
对于小根堆,新元素放到表尾,与父节点对比,若新元素比父节点更小,则将二者互换。新元素就这样一路“上升”,直到无法继续上升为止。
删除
被删除的元素用堆底元素替代,然后让该元素不断“下坠”,直到无法下坠为止。
9.5 归并排序
9.5.1 算法思想
把两个或多个已经有序的序列合并为一个有序序列,在每个小序列中选择最小的放在最前面,然后依次比较
9.5.2 实现代码
int *B=(int *)malloc( n*sizeof(int) );//辅助数组B
//A[low..mid]和A[mid+...high]各自有序,将两个部分归并
void Merge(int A[],int low,int mid,int high){
int i,j,k;
for(k=low; k<=high;k++)
B[K]=A[k];//将A中所有元素复制到B中
for(i=low,j=mid+1,k=i; i<=mid&&j<=high; k++){
if(B[i]<=B[j])
A[k]=B[i++];//将较小值复制到A中
else
A[k]=B[j++];
}
while(i<=mid) A[k++]=B[i++];
while(j<=high) A[k++]=B[j++];
}
void MergeSort(int A[] ,int low,int high){
if( low<high){
int mid=( low+high)/2;//从中间划分
MergeSort(A,low,mid);/对左半部分归并排序
MergeSort(A,mid1,high);//对右半部分归并排序
Merge(A,low,mid,high);//归并
}
}
9.5.3 算法效率分析
空间复杂度:O(n)
时间复杂度:O(nlog2n)
稳定性:稳定
9.6 基数排序
9.6.1 算法思想
假设长度为n的线性表中每个结点a的关键字由d元组
(kjn,kjn-1, kjn-2,…kj0)组成,上图中的百位数字就是kjn,个位数字就是kj0,10是基数。
基数排序得到递减序列的过程如下:
初始化:设置r个空队列,Qr-1,Qr-2,…Q0.
按照各个关键字位权重递增的次序(个、十、百),对d个关键字位分别做“分配"和“收集”
分配:顺序扫描各个元素,若当前处理的关键字位=x,则将元素插入Qx队尾
收集:把Qr-1, Qr-2… Q0各个队列中的结点依次出队并链接
基数排序不是基于比较的排序算法
9.6.2 算法效率分析
空间复杂度:需要r个辅助队列,为O®
时间复杂度:一趟分配O(n),一趟收集O®,总共d趟分配,收集,总的时间复杂度为O(d(n+r).
稳定性:稳定的
9.6.3 基数排序的应用
年龄排序
某学校有10000学生,将学生信息按年龄递减排序生日可拆分为三组关键字:年(1991 ~ 2005)、 月(1 ~ 12)、 日(1~31)
基数排序擅长解决的问题:
①拆分为d组,且d较小
②每组关键字的取值范围不大,即r较小
③数据元素个数n较大
9.7 各种内部排序算法的比较及应用
9.7.1 各种排序算法的性质
9.7.2 适用性
适用于顺序存储 | 顺序存储和链式存储均适合 |
---|---|
折半插入排序 | 冒泡排序 |
希尔排序 | 简单选择排序 |
快速排序 | 归并排序 |
堆排序 | 基数排序 |