内部排序
外层循环为最多排序的次数,内层循环为每一轮比较排序的过程
插入排序
直接插入排序
每次将一个待排序的记录按照其关键字的大小插入到前面已排好序的子序列中,直到全部记录插入完成。从前面有序子表寻找插入位置k;(给插入位置腾出空间)后移k到表尾的全部元素,将这个元素插入k位置。边比较边移动【数组中下标0处存放待排结点副本,初始第一个元素组成的单个子表视为有序序列】
void InsertSort(ElemType A[],int n){
for(i=2;i<=n;i++){
if(A[i]<A[i-1]){ //升序排列,首次与前面一个结点比较
A[0]=A[i]; //保存结点副本
for(j=i-1;A[j]>A[0];--j) //如果从i-1开始有比“哨兵”值大的元素,后移,直到有序子表遍历完
A[j+1]=A[j];
A[j+1]=A[0];
}
}
}
折半插入排序
比较与查找分离;顺序存储的线性表。折半查找算法找出元素的待插入位置;统一地移动待插入位置之后的所有元素。
void InsertSort(ElemType A[],int n){
for(i=2;i<=n;i++){
A[0]=A[i]; //将1至i-1看成一个数列,保存A[i]的副本
low=1;
high=i-1;
//寻找合适的插入位置,结束时low>high
while(low<=high){
mid=(low+high)/2;
if(A[mid]>A[0])
high=mid-1;
else
low=mid+1;
}
//移动元素
for(j=i-1;j>=low;--j) //从i-1开始到low后移,腾出low位置为待插入位置
A[j+1]=A[j];
A[low]=A[0];
}
}
希尔排序/缩小增量排序
先将待排序表分成若干子表,例如L[i,i+d,i+2d,i+3d,……i+kd]的“特殊”子表,把相隔某个增量的记录组成一个子表,对每个子表进行直接插入排序,当整个表中的元素都已“基本有序”,对全体记录进行一次直接插入排序
void ShellSort(ElemType A[],int n){ //数组下标从1开始
for(dk=n/2;dk>=1;dk=dk/2) //每次增量的取值,递减
for(i=dk+1;i<=n;i++) //每次增量下需要进行几组插入排序
if(A[i]<A[i-dk]){
A[0]=A[i]; //利用移动法删除,如果后面有比前面更小的,就移动/交换
for(j=i-dk;j>0&&A[j]>A[0];j-=dk)
A[j+dk]=A[j];
A[j+dk]=A[0];
}
}
void ShellSort(ElemType A[],int n){ //数组下标从0开始
for(dk=n/2;dk>=1;dk=dk/2) //每次增量的取值,递减
for(i=dk;i<n;i++) //每次增量下需要进行几组插入排序
for(j=i;j-dk>=0&&A[j-dk]>A[j];j-=dk) //利用移动法删除
Swap(A[j-dk],A[j]);
}
交换排序
冒泡排序
第一趟冒泡:从前往后(从后往前)两两比较元素的值,若不是升序,就交换结点,直到序列比较完成。结果是将数列中最小的元素值交换到待排序的第一个位置(最大的元素值交换到待排序的最后一个位置)
下一趟冒泡:前一趟确定的元素不再参与比较。即为每趟冒泡排序的结果都是把序列中的最小元素(最大元素)放到序列的最终位置,最多n-1次就可以将所有元素排好序 【flag 标记本次冒泡是否发生交换——可有可无】
void BubbleSort(ElemType A[],int n){
for(i=0;i<n-1;i++){ //进行n-1次排序
flag=false;
for(j=n-1;j>i;j--) //从后向前冒,每次从最后一个元素开始到i+1比较,i往前已经有序
if(A[j-1]>A[j]){
Swap(A[j-1],A[j]);
flag=true;
}
if(flag==false) //没有交换,说明表已经有序
return ;
}
}
注:冒泡排序中产生的有序子序列一定是全局有序的【不同于直接插入排序】有序子序列中的所有元素的关键字一定小于或大于无序子序列中所有元素的关键字,也就是每趟排序的结果都是将一个元素放到其最终位置。
快速排序
基于分治思想,待排序表L[1……n]中任取一个元素pivot作为枢轴,通过一趟排序将待排序的表划分为独立的两部分L[1……k-1]和L[k+1……n],使pivot左边表中所有元素均小于它,右边表中所有元素都大于它,最后将pivot插入L[k]中,完成一次快速排序。分别递归对左右两个子表重复上述过程,直到每个子表内只有一个元素为止【每个元素都已经放在正确的位置上】
void QuickSort(ElemType A[],int low,int high){
if(low<high){
int pivot_pos=Partition(A,low,high); //划分满足条件的左右子表,依旧保存在A中,返回值为枢轴所在位置
QuickSort(A,low,pivot_pos-1); //依次对两个子表进行快速排序,左边从第一个元素到枢轴前一个,右边从枢轴后一个到最后一个元素
QuickSort(A,pivot_pos+1,high);
}
}
//划分操作是快速排序的核心
int Partition(ElemType A[],int low,int high){
ElemType pivot=A[low]; //选用第一个元素作为枢轴
while(low<high){
while(low<high&&A[high]>=pivot) //如果后面的元素比枢轴大,说明符合条件,就将指针前移
high--;
A[low]=A[high]; //把后面的元素移到前面,退出循环的条件是后面有比枢轴小的或者low>=high 就进行插入操作
while(low<high&&A[low]<=pivot) //如果前面的元素比枢轴小,说明符合条件,就将指针后移
low++;
A[high]=A[low]; //把前面的元素移到后面
}
A[low]=pivot; //最后把枢轴的元素插入low指向位置,退出循环时low==high
return low;
}
注:快速排序中,并不产生有序子序列,每趟排序后会将枢轴元素放到最终位置【每次划分都会确定一个位置的元素】也是前后交换只是每次交换条件不同
改编快速排序算法,使之每次选取的枢轴值都是随机从当前子表中选择的。增加步骤:直接随机求出枢轴下标,将枢轴值与A[low]交换 主要思想同快速排序
int Partition2(ElemType A[],int low,int high){
//随机取一个下标,交换到当前子表第一个位置
int rand_index=low+rand()%(high-low+1);
Swap(A[rand_index],A[low]);
ElemType pivot=A[low]; //选用第一个元素作为枢轴
while(low<high){
while(low<high&&A[high]>=pivot) //如果后面的元素比枢轴大,说明符合条件,就将指针前移
high--;
A[low]=A[high]; //把后面的元素移到前面,退出循环的条件是后面有比枢轴小的或者low>=high 就进行插入操作
while(low<high&&A[low]<=pivot) //如果前面的元素比枢轴小,说明符合条件,就将指针后移
low++;
A[high]=A[low]; //把前面的元素移到后面
}
A[low]=pivot; //最后把枢轴的元素插入low指向位置,退出循环时low==high
return low;
}
课后习题
1、双向冒泡排序,在正反两个方面交替进行扫描,即为第一趟把关键字最大的元素放在序列的最后面,第二趟把关键字最小的元素放在序列的最前面,如此反复进行,直到排序完成。
(1)奇数趟从前往后比较相邻元素的关键字大小,逆序就交换,直到把序列中的最大关键字移动到尾部
(2)偶数趟从后往前比较相邻元素的关键字大小,逆序就交换,直到把序列中的最小关键字移动到前部
void BubbleSort(ElemType A[],int n){
int low=0,high=n-1;
bool flag=true;
while(low<high&&flag==true){
flag=false;
for(i=low;i<high;i++)
if(A[i]>A[i+1]){
swap(A[i],A[i+1]);
flag=true;
}
high--; //最大元素已经移到最后,修改上界
for(i=high;i>low;i--)
if(A[i]<A[i-1]){
swap(A[i],A[i-1]);
flag=true;
}
low++; //最小元素已经移动到前面,修改下界
}
}
2、线性表按顺序存储,且每个元素都是不相同的整数型元素,把所有奇数移动到所有偶数前边(奇数前偶数后)
基于快速排序,只需遍历一次即可,时间复杂度为O(n)空间复杂度为O(1)。假设表为L(1….n),从前往后找到一个偶数元素L(i),从后往前找到一个奇数元素L(j)。两者交换,重复上述过程直到 i > j
void move(ElemType A[],int n){ //A[]共有n个元素
int i=0,j=n-1;
while(i<j){
while(i<j&&A[i]%2!=0) //从前往后找,如果是奇数就向后继续找,找到偶数就结束循环
i++;
while(i<j&&A[j]%2!=1) //从后往前找,如果是偶数就向前继续找,找到奇数就结束循环
j--;
if(i<j){ //如果符合交换条件就交换,然后继续向前/后遍历
swap(A[i],A[j]);
i++;
j--;
}
}
}
3、在数组L[1……n]中找出第k小的元素(从小到大排序后处于第k个位置的元素)
基于快速排序,从数组中选择枢轴pivot(选取第一个)进行快速排序一样的划分操作,表L(1….n)被划分为L(1…m-1) L(m+1…n) L(m)=pivot
对pivot位置的判断及递归条件:
(1)m=k,pivot就是要找的第k个位置元素,返回pivot
(2)m>k,待查找元素落在L(1…m-1)中,对L(1…m-1) 递归查找第k小的元素
(3)m<k,待查找元素落在L(m+1…n)中,对L(m+1…n) 递归查找第k-m小的元素
int K_Search(ElemType L[],int low,int high,int k){
int pivot=L[low];
int low_temp=low; //递归时修改上下界
int high_temp=high;
//快速排序基本思想
while(low<high){
while(low<high&&L[high]>=pivot) //后面元素比枢轴元素大,就将指针前移
high--;
L[low]=L[high];
while(low<high&&L[low]<=pivot)
low++;
L[high]=L[low];
}
L[low]=pivot; //low=high时,表示当前位置为枢轴最终位置
//递归判断
if(low==k) //当前pivot所在位置就是第k个位置
return L[low];
else if(low>k) //左半子表中查找
return K_Search(L,low_temp,low-1,k);
else //low<k时,右半子表中查找
return K_Search(L,low+1,high_temp,k-low);
}
4、2016年统考题目(Page 324)
仿照快速排序的思想,查找符合条件的元素,求和的过程。最小的n/2个元素放在A1中,其余的元素放在A2中,分组结果求和即可。基于枢轴将n个元素划分为两个子集,根据划分后枢轴所在的位置 pivot 进行处理:
(1)pivot=n/2,分组完成,求和即可
(2)pivot<n/2,枢轴及之前的所有元素均属于A1,继续对pivot之后的元素划分
(3)pivot>n/2,枢轴及之后的所有元素均属于A2,继续对pivot之前的元素划分
int Set_Partition(int A[],int n){
int low=0,high=n-1,k=n/2,s1=0,s2=0,flag=1;
int pivot=A[low];
while(flag==1){
while(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; //low=high时,表示当前位置为枢轴最终位置
if(low==k) //划分成功,不需要再划分
flag=0;
else if(low<k)
Set_Partition(A,n-low);
else
Set_Partition(A,n);
}
for(i=0;i<k;i++)
s1=s1+A[i];
for(i=k;i<n;i++)
s2=s2+A[i];
return s2-s1;
}
5、荷兰国旗问题:设有一个仅有红、白、蓝三种颜色的条块组成的条块序列,使得这些条块按红、白、蓝的顺序排好,排成荷兰国旗图案
顺序扫描线性表,将红色条块交换到线性表最前面,蓝色条块交换到线性表最后面。设置3个指针,j——工作指针(当前扫描元素)i——以前的元素全部为红色 k——以后的元素全部为蓝色。初始 i=0,k=n-1 根据 j 所指示的元素颜色,决定将其交换到序列的前部/尾部。
typedef enum{Red,White,Blue} color;
void Flag_Arrange(color a[],int n){
int i=0,j=0,k=n-1;
while(j<=k){
switch(a[j]){
case Red:
swap(a[i],a[j]);
i++;
j++;
break;
case White:
j++;
break;
case Blue:
swap(a[j],a[k]);
k--; //不需要j++,j=k时还会进入循环,也就是可能j==k,指向同一个位置
}
}
}
选择排序
简单选择排序
每一趟(如第 i 趟)在后面n-i+1个待排序元素中选取关键字最小的元素,作为有序子序列的第i个元素,直到第n-1趟排序完成。如果待排序序列中只剩1个元素,就不用再比较。通过下标来访问元素的值,并且比较元素的大小后保存下标,符合条件就交换
void SelectSort(ElemType A[],int n){
for(i=0;i<n-1;i++){ //进行n-1次排序
min=i; //min中保存数组中最小值下标,初始假设为i
for(j=i+1;j<n;j++) //比较i至n-1的元素,如果还有比它小的,就保存下标
if(A[min]>A[j])
min=j;
if(min!=i)
swap(A[i],A[min]);
}
}
堆排序
大根堆:根>左、右【递增序列】堆顶元素是最大值元素;小根堆:根<左、右【递减序列】堆顶元素是最小值元素
考虑问题:如何将无序序列构造成初始堆?输出堆顶元素后,如何将剩余元素调整成新的堆?
【大根堆为例】
1、从后向前依次对于各结点为根的子树进行筛选,看该结点的值是否大于其左右子结点的值;若不大于,则将左右子结点中的较大值与之交换。交换后可能破坏下一级堆,继续采用上述方法构造下一级堆,直到以该结点为根的子树构成堆为止。反复上述过程,调整堆的方法建堆,直到根结点。
2、输出堆顶元素后,将堆的最后一个元素与堆顶元素进行交换【将堆顶元素加入有序子序列】并将待排序元素再次调整为大根堆(小元素下坠)
3、最后输出的就是递增的有序序列
//建立大根堆算法
void BuildMaxHeap(ElemType A[],int len){
for(int i=len/2;i>0;i--) //从i=[n/2]到1 反复调整堆
HeadAdjust(A,i,len); //以i为根的子树进行调整
}
void HeadAdjust(ElemType A[],int k,int len){ //将元素k为根的子树进行调整
A[0]=A[k]; //A[0]暂存子树的根结点
for(i=2*k;i<=len;i*=2){
if(i<len&&A[i]<A[i+1]) //左右孩子比较,沿着key较大的子结点向下筛选,取较大子结点的下标
i++;
if(A[0]>=A[i]) //如果根结点值比较大结点值都大,则无需交换,否则子结点>根结点,与根结点进行交换
break;
else{
A[k]=A[i];
k=i;
}
}
}
课后习题
1、基于单链表的待排序关键字序列上进行简单选择排序(带有头结点的单链表)
void SelectSort(LinkList L){
p=L->next;
L->next=NULL;
while(p!=NULL){
r=L; //r为q的前驱
q=L->next;
while(q!=NULL&&q->data<=p->data){ //寻找后面元素<前面元素的位置,
r=q;
q=q->next;
}
u=p->next;
p->next=r->next; //p头插入r之后(q指向第一个大于p的元素)r为比p小的元素
r->next=p;
p=u;
}
}
2、判断一个数据序列是否构成一个小根堆
顺序表L[1…n]看成一棵完全二叉树,扫描所有结点,遇到孩子结点的关键字小于根结点的关键字返回false,遍历结束返回true
bool MinHeap(ElemType A[],int len){
if(len%2==0){ //数值长度为偶数,有一个单分支结点
if(A[len/2]>A[len]) //判断单分支结点,孩子结点的关键字小于根结点的关键字
return false;
for(i=len/2-1;i>=1;i--) //i表示所有根结点关键字
if(A[i]>A[2*i]||A[i]>A[2*i+1])
return false;
}
else{
for(i=len/2-1;i>=1;i--) //i表示所有根结点关键字
if(A[i]>A[2*i]||A[i]>A[2*i+1])
return false;
}
return true;
}
归并排序
归并:两个或两个以上的有序表合成一个新的有序表【2路归并排序】表A可分为两端A[low……mid]和A[mid+1……high],使其各自有序
ElemType *B=(ElemType *)malloc((n+1)*sizeof(ElemType)); //辅助数值B用来存放一次归并排序后的结果
void Merge(ElemType A[],int low,int mid,int high){
//将待排序所有元素复制到B中
for(int i=low;i<=high;i++)
B[i]=A[i];
//一般操作,每个子表进行比较
i=low;
j=mid+1;
for(k=i;i<=mid&&j<=high;k++){ //k指向的是A的下标,在B中比较,然后复制到A中
if(B[i]<=B[j]) //前面与后面比较,每次赋值小的到A,调整A中的数据元素
A[k]=B[i++];
else
A[k]=B[j++];
}
//若有一个表未检测完,直接复制到A中
while(i<=mid)
A[k++]=B[i++];
while(j<=high)
A[k++]=B[j++];
}
void MergeSort(ElemType A[],int low,int high){
if(low<high){
int mid=(low+high)/2; //从中间划分为两个子序列
MergeSort(A,low,high); //对左侧子序列进行递归排序
MergeSort(A,mid+1,high); //对右侧子序列进行递归排序
Merge(A,low,mid,high); //归并排序
}
}
基数排序
对于每一位的排序,从最低位开始排序,复杂度为O(k*n) n为数组长度,k为数组中数的最大位数
基数排序按照低位优先排序,然后收集;再按照高位排序,然后再收集;以此类推,直到最高位。【排序+收集的过程】链式基数排序,掌握排序的过程
补充题目
1、顺序表用数组A[]表示,表中元素存储在数组下标1~m+n的范围内,前m个元素递增有序,后n个元素递增有序,设计算法,使得整个表有序
数组A[1….m+n]看成一个已经过m趟插入排序的表,从m+1趟开始,将后n个元素(下标到m+n为止)依次插入前面的有序表中
void Insert_Sort(ElemType A[],int m,int n){
for(i=m+1;i<=m+n;i++){ //A[m+1...m+n]插入有序表
A[0]=A[i]; //A[0]存放哨兵
for(j=i-1;A[j]>A[0];j--) //从后往前插入
A[j+1]=A[j];
A[j+1]=A[0];
}
}
2、简单排序算法:计数排序(counting sorting)
对一个待排序的表(用数组表示)进行排序,并将排序结果存放到另一个新的表中;表中所有待排序的关键码互不相同,计数排序算法针对表中的每个记录,扫描待排序的表一趟,统计表中有多少个记录的关键码比该关键码小;假设针对某个记录统计出的计数值为c,则这个记录在新的有序表中的合适存放位置为c
void CountSort(ElemType A[],ElemType B[],int n){
int count=0;
for(i=0;i<n;i++){
for(j=0;j<n;j++)
if(A[j].key<A[i].key)
count++;
B[count]=A[i];
}
}
//简单选择排序
for(i=0;i<n;i++) //增加一个计数域,初始化所有值为0
a[i].count=0;
for(i=0;i<n;i++)
for(j=i+1;j<n;j++)
if(a[i].key<a[j].key) //哪一个数据大,就将其数据域加1
a[j].count++;
else
a[i].count++;
3、数组中存放无序关键序列(K1,K2,K3……Kn),要求将Kn放在将元素排序后正确的位置
基于快速排序,以Kn为枢轴元素,进行一次快速排序,将快速排序从最后一个为枢轴,先从前向后,再从后向前
int Partition(ElemType K[],int n){
int low=1,high=n;
ElemType pivot=K[high];
while(low<high){
while(low<high&&K[low]<=pivot)
low++;
K[high]=K[low];
while(low<high&&K[high]>=pivot)
high--;
K[low]=K[high];
}
K[low]=pivot;
return low;
}
a[j].count++;
else
a[i].count++;
}