排序----内部排序
插入排序
三种插入排序的时间复杂度均为 O ( n 2 ) O(n^2) O(n2)
直接插入排序
无哨兵
//直接插入排序 没有哨兵,数组0的位置也存放的是待排元素
void InsertSort(int A[], int n){
int i,j,temp;//因为A[0]也可以存放数据元素,所以i从1开始
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;//复制到插入位置
}
}
}
无哨兵的示意图如下:
有哨兵
//直接插入排序 带哨兵,A[0]为哨兵
void InsertSort0(int A[], int n){
int i,j;
for(i=2;i<=n;i++){
if(A[i]<A[i-1]){
A[0]=A[i];
for(j=i-1; A[0]<A[j];--j){
A[j+1]=A[j];
}//for
A[j+1]=A[0];
}//if
}//for
}
折半插入排序
即先折半查找出元素的待插入位置,然后统一地移动待插入位置之后的所有元素。
折半插入仅较少了比较元素的次数,约为 O ( n l o g 2 n ) O(nlog_2n) O(nlog2n), 而元素移动的次数并未改变,仍为 O ( n 2 ) O(n^2) O(n2)
//折半插入排序, A[0]空出来当中转站
void InsertSort2(int A[],int n){
int i,j,low,high,mid;
for(i=2;i<=n;i++){//依次将A[2]~A[n]插入到前面的已排序序列
A[0]=A[i];//把要插入的元素存放到中转站
low=1;
high=i-1;//设置折半查找的范围
while(low <= high){
mid=(low+high)/2;
if(A[mid] > A[0])//查找左半边
high=mid-1;
else
low=mid+1;//查找右半边
}//当high<low时退出循环,此时low=high-1;
//且low的位置即为要插入的位置
for(j=i-1;j>=low;--j){//统一后移元素,空出插入位置A[low]
A[j+1]=A[j];
}
A[low]=A[0];
}
}
希尔排序
也叫缩小增量排序
先将待排序表分割为若干形如L[i, i+d, i+2d,…, i+kd]的特殊子表,即把相隔某个增量的记录组成一个子表,对各个子表分别进行直接插入排序。当整个表中的元素已呈“基本有序”时,再对全体记录进行一次直接插入排序。
希尔提出的方法是 d 1 = n / 2 , d i + 1 = ⌊ d i / 2 ⌋ d_1=n/2, d_{i+1}=\lfloor d_i/2 \rfloor d1=n/2,di+1=⌊di/2⌋, 并且最后一个增量等于1
稳定性:当相同的关键字的记录被划分到不同的子表中时,可能会改变它们之间的相对次序,因此希尔排序是一种不稳定的排序方法。且希尔排序仅适用于线性表为顺序存储的情况。
//希尔排序,A[0]为中转站,暂存元素
void ShellSort(int A[], int n){
//A[0]只是暂存单元,不是哨兵,当j<=0时,插入位置已到
int i,j,dk;
for(dk=n/2;dk>=1;dk=dk/2){//步长变换
for(i=1+dk;i<=n;++i){//i=1+dk即指第1个元素+步长dk
//++i指每次轮流地切换着来处理不同的子表
if(A[i] < A[i-dk]){
A[0]=A[i];
for(j=i-dk;j>=1 && A[j]>A[0];j=j-dk){
A[j+dk]=A[j];//记录后移,查找插入的位置
}
A[j+dk]=A[0];//插入
}
}
}
}
交换排序
冒泡排序
从下往上冒泡,即从数组尾部开始比较,这里每次将最小的往上冒.
是一种稳定的排序
void Swap(int &a,int &b){
int t;
t=a;
a=b;
b=t;
}
void BubbleSort(int A[], int n){
bool flag;
for(int i=0; i<n-1; i++){//n个数最多要比较n-1趟
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){
return;//本趟遍历后没有发生交换,说明表已经有序
}
}
}
快速排序
快排的思想基于分治法
**空间效率:**由于快排是递归的,需要借助一个递归工作栈来保存每层递归调用的必要信息,其容量应与递归调用的最大深度一致。最好情况下为 O ( l o g 2 n ) O(log_2n) O(log2n); 最坏情况下,因为要进行n-1次递归调用,所以栈的深度为 O ( n ) O(n) O(n); 平均情况下,栈的深度为 O ( l o g 2 n ) O(log_2n) O(log2n)
时间效率:快排的运行时间与划分是否对称有关,快排的最快情况发生在两个区域分别包含n-1个元素和0个元素时,这种最大限度的不对称性若发生在每层递归上,即对应于初始排序表基本有序或基本逆序时,就得到最坏情况下的时间复杂度为 O ( n 2 ) O(n^2) O(n2);
快排是所有内部排序算法中平均性能最右的排序算法,平均情况下时间复杂度为 O ( n l o g 2 n ) O(nlog_2n) O(nlog2n)
//快速排序
/*i,j指针轮流搜索,j先搜索,因为枢轴定在A[low]处,
即A[low]的位置先给了pivot,空了出来,所以j先搜索,
搜索到的符合条件的值放到A[low]的位置,然后i再搜索......
*/
int Partition(int A[], int low,int high){
int pivot = A[low];//将当前表中第一个元素设为pivot,对表进行划分
while(low < high){//当low>=high时跳出循环
while(low<high && A[high]>=pivot)
high--;
A[low]=A[high];//将比pivot小的元素移动到左端
//上下两个while不能颠倒,因为在最开始是将A[low]的值给了pivot,
//所以这时A[low]空出来了
while(low<high && A[low]<=pivot)
low++;
A[high] = A[low];//将比pivot大的元素移动到右端
}
//最后这里low必定等于high,low==high时跳出循环,
//并且此位置即为pivot元素要放的位置,即枢轴的位置
A[low]=pivot;//pivot元素存放到最终位置
return low;//返回存放枢轴的最终位置
}
void QuickSort(int A[], int low,int high){
if(low < high){//递归跳出的条件
//Partition就是划分操作,将表A[low...high]
//划分为满足上述条件的两个子表
int pivotpos=Partition(A,low,high);
QuickSort(A,low,pivotpos-1);
QuickSort(A,pivotpos+1,high);
}
}
选择排序
简单选择排序
一视同仁,无论数的初始状态如何,均要n-1趟处理
void Swap(int &a,int &b){
int t;
t=a;
a=b;
b=t;
}
//简单选择排序
void SelectSort(int A[], int n){
for(int i=0;i<n-1;i++){//一共进行n-1趟
int minpos=i;//记录最小元素的位置
for(int j=i+1;j<n;j++){//在A[i...n-1]中选择最小的元素
if(A[j]<A[minpos])
minpos=j;//更新最小元素的位置
}
if(minpos != i)
Swap(A[i],A[minpos]);//封装的swap()函数
}
}
堆排序
可以将该一维数组视为一棵完全二叉树:满足条件 (根>=左,右) 的称为大根堆; 满足条件**(根<=左右)**的称为小根堆;
从堆的定义中可知,堆的根节点一定是堆中所有结点的最大值或最小值
构造大根堆的思路: 把所有非终端结点(即 i < ⌊ n / 2 ⌋ i<\lfloor n/2\rfloor i<⌊n/2⌋则为非终端结点)都检查一遍,看是否满足大根堆的要求(根>=左,右),如果不满足则进行调整:将当前结点与更大的一个孩子互换(非叶子结点i的左右孩子为2i , 2i+1)
注意: 非终端叶子结点的检查顺序为i,i-1,i-2…即从编号最大的非终端叶子结点开始依次检查并调整。也即从后往前调整所有非终端结点
空间效率:O(n)
在最好,最坏和平均情况下,堆排序的时间复杂度为: O ( n l o g 2 n ) O(nlog_2n) O(nlog2n)
稳定性:不稳定
#include<stdio.h>
// 建立大根堆
//函数HeadAdjust将元素k为根的子树进行调整
void HeadAdjust(int A[],int k,int len){
A[0]=A[k];//A[0]暂存子树的根节点
for(int i=2*k;i<=len;i=i*2){//沿key较大的子节点向下筛选
if(i<len && A[i]<A[i+1])//i<len是为了保证i有右兄弟
i++;//取key较大的子节点的下标
if(A[0] >= A[i])
break;//筛选结束
else{
A[k]=A[i];//将A[i]调整到双亲节点上
k=i;//修改k的值,以便继续向下筛选
}
}
A[k]=A[0];//被筛选结点的值放入最终位置
}
void BuildMaxHeap(int A[],int len){
for(int i=len/2;i>=0;i--)//从i=n/2 ~ 1,从后往前依次调整所有非终端结点
HeadAdjust(A,i,len);
}
//堆排序算法
void HeapSort(int A[], int len){
BuildMaxHeap(A,len);//初始建堆 O(n)
for(int i=len;i>1;i--){//n-1趟的交换和建堆过程 O(nlog2^n)
Swap(A[i],A[1]);//输出堆顶元素(和堆底元素交换,即和完全二叉树的最后一个结点交换)
printf("%d ",A[i]);//输出堆顶元素
HeadAdjust(A,1,i-1);//调整,把剩余的i-1个元素整理成堆
}
}
int main(){
int a[9]={0,53,17,78,9,45,65,87,32};//a[1]~a[8]
int n=8;
printf("HeapSort 堆排: ");
HeapSort(a,n);
return 0;
}
一个结点每下坠一层,最多只需对比关键字2次
若树高为h,而结点在第i层,则将这个结点向下调整,最多只需要下坠h-i层,关键字对比次数不超过2(h-i)次
n个结点的完全二叉树的树高为 h = ⌊ l o g 2 n ⌋ + 1 h=\lfloor log_2n\rfloor+1 h=⌊log2n⌋+1
第i层最多有 2 i − 1 2^{i-1} 2i−1个结点,而只有第1~(h-1)层的结点才有可能下坠调整
将整棵树调整为大根堆,关键字对比次数不超过 ∑ i = h − 1 1 2 i − 1 2 ( h − i ) = ∑ i = h − 1 1 2 i ( h − i ) = ∑ j = 1 h − 1 2 h − j j < = 2 n ∑ j = 1 h − 1 j 2 j < = 4 n \sum_{i=h-1}^12^{i-1}2(h-i)=\sum_{i=h-1}^12^i(h-i)=\sum_{j=1}^{h-1}2^{h-j}j<=2n\sum_{j=1}^{h-1}\frac{j}{2^j}<=4n i=h−1∑12i−12(h−i)=i=h−1∑12i(h−i)=j=1∑h−12h−jj<=2nj=1∑h−12jj<=4n
部分计算过程:
∑ j = 1 h − 1 2 h − j j = ∑ j = 1 h − 1 2 ⌊ l o g 2 n ⌋ + 1 . 2 − j . j \sum_{j=1}^{h-1}2^{h-j}j=\sum_{j=1}^{h-1}2^{\lfloor log_2n\rfloor+1}.2^{-j}.j j=1∑h−12h−jj=j=1∑h−12⌊log2n⌋+1.2−j.j
而 2 ⌊ l o g 2 n ⌋ < = 2 l o g 2 n = 2^{\lfloor log_2n\rfloor}<=2^{log_2n} = 2⌊log2n⌋<=2log2n=
建立小根堆的练习
归并排序
空间复杂度为O(n)
时间复杂度为 O ( n l o g 2 n ) O(nlog_2^n) O(nlog2n)
稳定的
//2路归并排序
int n=10;
int *B=(int *)malloc(sizeof(int)*(n+1));//辅助数组B
//Merge函数的功能是将前后相邻的两个有序表归并为一个有序表
void Merge(int A[],int low,int mid,int high){
//表A的两段A[low...mid]和A[mid...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])//比较B的左右两段中的元素
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,mid+1,high);//对右侧子序列进行递归排序
Merge(A,low,mid,high);//归并
}
}
基数排序
基数排序不基于比较和移动进行排序,而基于关键字各位的大小进行排序。
基数排序的应用
外存,内存之间的数据交换
外部排序的原理
增加归并的路数,减少归并的趟数,这样就可以减少读写磁盘的趟数
多路平衡归并
败者树
正确的结点记录内容如下,即灰色结点中记录的是竞争失败者是来自哪个归并段,而根节点记录冠军(竞争最终胜利者)是来自哪个归并段
第一次归并,对比了7次找到了最小的元素来自哪个归并段。接下来按照归并排序的规则,我们还需要在归并段1~8中选出下一个最小的元素。接下来我们会让归并段3的下一个元素替代1这个元素的原有的位置
接下来要从余下的叶子结点中选出新的最小的元素,我们只需要让6和第四个归并段中最小的元素进行对比
只要构造好了败者树,接下来每次选最小的元素时,只需要对比3次,即败者树的灰色结点的层数
假设现在构造好了一棵败者树,树高为h(h不包含蓝色结点,而是灰色和绿色结点构成的树)。则第h层一共有 2 h − 1 2^{h-1} 2h−1个结点
k路归并的败者树会有k个叶子结点。所以应有 k < = 2 h − 1 k <= 2^{h-1} k<=2h−1
解得 h − 1 = ⌈ log 2 k ⌉ h -1=\lceil\log_2k\rceil h−1=⌈log2k⌉ .而 h-1 刚好代表分支结点有多少层。而之前说分支结点有多少层就需要对比多少次,所以有了败者树后,选出最小元素,只要对比关键字 ⌈ log 2 k ⌉ \lceil\log_2k\rceil ⌈log2k⌉次
如果是1024路归并,则传统方法,每次都要进行1023次比较,而败者树只要对比 ⌈ log 2 1024 ⌉ \lceil\log_21024\rceil ⌈log21024⌉ 即10次对比
数组下标为1的元素对应传统意义上的根节点,数组下标为0的对应新增加的小头头ls[0]。而叶子结点在实际的数组当中是不对应任何一个数据的。在逻辑上,每个绿色的叶子结点对应一个归并段,而实际上这些叶子结点是我们脑补上去的
置换选择排序
此时发现内存工作区中最小的是10,但是记录的MINMAX=13,13>10,所以此时不能将10放入归并段1后面(归并段1是要保证内部递增的)
除了10之外,最小的是14,14>MINMAX=13,所以此时可把14放入归并段1后面,紧接着读入下一个元素22
若WA内的所有元素都比MINMAX更小,则归并段应在此截止,并开启下一个归并段的构造
解冻红色的,找出最小的元素为2,输出,并将MINMAX置为2。每当内存工作区有空位时就读入下一个记录
输出文件FO是存放在磁盘中的,演示中为每次输出一个,实际上是先将要输出的元素放到输出缓冲区中,凑成一整块后再写入磁盘。对磁盘的读写是以块为单位的。同理于输入文件FI
最佳归并树
绿色结点上的权值代表归并段的长度,即归并段所占磁盘的块数
进行3路归并时,会在内存中开辟3个缓冲区(缓冲区的大小和磁盘的一块的大小相等),而对于2,3 和虚节点0来说,2和3分别放在一个缓冲区中,而第三缓冲区中什么也不放