08-内部排序
插入排序
直接插入排序
**思想:**把n个待排序的元素看成为一个有序表和一个无序表。开始时有序表中只包含1个元素,无序表中包含有n-1个元素,排序过程中每次从无序表中取出第一个元素,将它插入到有序表中的适当位置,使之成为新的有序表,重复n-1次可完成排序过程。
//直接插入排序
void insertSort(int arr[],int n){ //待排关键字存储在数组arr中,个数为n
int temp,i,j;
for(i=1;i<n;i++){
temp=arr[i]; //将待插入的关键字暂存于temp中
j=i-1;
/*下面这个循环完成了从待排关键字开始扫描,如果大于待排关键字,则后移一位*/
while(j>=0&&temp<arr[j]){
arr[j+1]=arr[j];
--j;
}
arr[j+1]=temp; //找到插入位置,将temp中暂存的待排关键字插入
}
}
折半插入排序
**思想:**和直接插入排序类似,只是查找插入的位置的方法不同,折半插入排序是采用折半查找法来查找插入位置的。折半查找法的一个基本条件就是序列已经有序。
执行流程:
原来序列:13 38 49 65 76 97 27 49
已经排序 | 未排序 | |||||||
关键字 | 13 | 38 | 49 | 65 | 76 | 97 | 27 | 49 |
数组下标 | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 |
1)low=1,high=5,m=(0+5)/2=2,下标为2的关键字49,27<49,所以27插入到49的低半区,改变high=m-1,low=0;
2)low=0,high=1,m=(0+1)/2=0,下标为0的关键字13,27>13,所以27应该插入到13的高半区,改变low=m+1=1,high=1;
3)low=1,high=1,m=(1+1)/2=1,下标为1的关键字38,27<38,所以27应该插入到38的低半区,改变high=m-1=0,low=1,此时low>high,折半查找结束,27插入位置在下标为high的关键字之后,即13之后;
4)一次向后移动关键字97,76,65,49,38,然后将27插入,这一趟折半插入排序结束。执行完这一趟排序结果为:13 27 38 49 65 76 97 49
希尔排序
**思想:**希尔排序是把元素按下标的一定增量进行分组,对每组使用直接插入排序算法排序;
随着增量逐渐减少,当增量减至 1 时*,*整个文件恰被分成一组,算法便终止
执行流程:
原始序列:{49 38 65 97 76 13 27 49 55 04}
1)以增量5分割序列,得到以下几个子序列
子序列1 | 49 | 13 | ||||||||
---|---|---|---|---|---|---|---|---|---|---|
子序列2 | 38 | 27 | ||||||||
子序列3 | 65 | 49 | ||||||||
子序列4 | 97 | 55 | ||||||||
子序列5 | 76 | 04 |
分别对这5个子序列进行直接插入排序,得到:
子序列1 | 13 | 49 | ||||||||
---|---|---|---|---|---|---|---|---|---|---|
子序列2 | 27 | 38 | ||||||||
子序列3 | 49 | 65 | ||||||||
子序列4 | 55 | 97 | ||||||||
子序列5 | 04 | 76 |
一趟希尔排序结果为:13 27 49 55 04 49 38 65 97 76;
2)以增量3(上一步增量的二分之一,向上取整或向下取整,此处向上取整)分割序列,得到以下几个子序列
子序列1 | 13 | 55 | 38 | 76 | ||||||
---|---|---|---|---|---|---|---|---|---|---|
子序列2 | 27 | 04 | 65 | |||||||
子序列3 | 49 | 49 | 97 |
分别对这3个子序列进行直接插入排序,得到:
子序列1 | 13 | 38 | 55 | 76 | ||||||
---|---|---|---|---|---|---|---|---|---|---|
子序列2 | 04 | 27 | 65 | |||||||
子序列3 | 49 | 49 | 97 |
又一趟希尔排序结果为:13 04 49 38 27 49 55 65 97 76;
3)以增量1分割序列,即对上面结果全体关键字进行一次直接插入排序
结果为:04 13 27 38 49 49 55 65 76 97
void shellSort(int arr[],int n){
int temp;
for(int gap=n/2;gap>0;gap/=2){
for(int i=gap;i<n;++i){
temp=arr[i];
int j;
for(j=i;j>=gap&&arr[j-gap]>temp;j-=gap){
arr[j]=arr[j-gap];
}
arr[j]=temp;
}
}
}
选择排序
简单选择排序
**思想:**每趟从待排序的记录中选出关键字最小的记录,顺序放在已排序的记录序列末尾,直到全部排序结束为止。

//简单选择排序
void selectSort(int arr[],int n){
int i,j,k;
int temp;
for(i=0;i<n;++i){
k=i;
for(j=i+1;j<n;++j){ //从无序序列中跳出一个最小的关键字
if(arr[k]>arr[j])
k=j;
}
temp=arr[i]; //以下三句完成了最小关键字和无序序列第一个关键字交换
arr[i]=arr[k];
arr[k]=temp;
}
}
堆排序
堆的相关概念
**堆的概念:**任何一个非叶结点的值都不大于(或不小于)其左右孩子结点的值。父亲大孩子小,大顶堆;父亲小孩子大,小顶堆;
完全二叉树的最后一个非叶节点的位置:└n/2┘-1;
建堆:

**大顶堆插入:**将要插入的结点x放在最底层最右边,插入后满足完全二叉树的特点;之后依次向上将x调整到合适的位置上以满足父大子小的性质;

**大顶堆删除:**当删除堆中一个结点时,原来位置会出现一个空,填充这个空的方法就是,将最底层最右边的叶子的值赋给该空位并且下调到合适位置,最后把该叶子删除;

堆排序算法
对结点的调整方法:
将当前结点(假设值为a)的值与孩子结点进行比较,如果存在大于a值得孩子结点,则从中选出最大的一个与a交换。当a来到下一层的时候重复上述过程,直到a的孩子结点的值都小于a的值为止;
思想:
1)从无序序列所确定的完全二叉树的最后一个非叶子节点开始,从右至左,从下至上,对每个结点进行调整,最终将得到一个大顶堆。
2)将当前无序序列中的第一个关键字,反映在树中是根结点(假设为a)与无序序列的最后一个关键字交换(假设为b)。a进入有序序列;到达最终位置。无序序列中关键字减少1个,有序序列中关键字增加1个。此时只有结点b可能不满足堆的定义,对其进行调整。
3)重复第2)步,直到无序序列中的关键字剩下1个时排序结束;
/*结点调整函数*/
void sift(int arr[],int low,int high){
int i=low,j=2*i+1;
int temp=arr[i];
while(j<=high){
if(j<high&&arr[j]<arr[j+1]) ++j;
if(temp<arr[j]){
arr[i]=arr[j];
i=j;
j=2*i+1;
}else{
break;
}
}
arr[i]=temp;
}
/*堆排序主函数*/
void heapSort(int arr[],int n){
int i,temp;
for(i=n/2-1;i>=0;--i)
sift(arr,i,n-1);
for(i=n-1;i>0;--i){
temp=arr[0];
arr[0]=arr[i];
arr[i]=temp;
sift(arr,0,i-1);
}
}
交换排序
冒泡排序
**思想:**依次比较相邻的两个数,将比较小的数放在前面,比较大的数放在后面。
(1)第一次比较:首先比较第一和第二个数,将小数放在前面,将大数放在后面。
(2)比较第2和第3个数,将小数 放在前面,大数放在后面。
(3)如此继续,直到比较到最后的两个数,将小数放在前面,大数放在后面,重复步骤,直至全部排序完成
(4)在上面一趟比较完成后,最后一个数一定是数组中最大的一个数,所以在比较第二趟的时候,最后一个数是不参加比较的。
(5)在第二趟比较完成后,倒数第二个数也一定是数组中倒数第二大数,所以在第三趟的比较中,最后两个数是不参与比较的。
(6)依次类推,每一趟比较次数依次减少

//冒泡排序,最大的在最右边
void BubbleSort(int arr[],int n){
int i,j,flag,temp;
for(i=n-1;i>=1;--i){
flag=0; //flag=0来标记此趟排序是否发生了交换
for(j=1;j<=i;++j){
if(arr[j-1]>arr[j]){
temp=arr[j];
arr[j]=arr[j-1];
arr[j-1]=temp;
flag=1; //发生了交换则flag=1
}
}
if(flag==0) //一趟排序过程中没有发生关键字交换,则证明序列有序,排序结束
return;
}
}
快速排序
**思想:**每一趟选择当前所有子序列中的一个关键字作为枢轴,从右往左扫描将子序列中比枢轴小的移动到枢轴前边,从左往右扫描比枢轴大的移动到枢轴后边;当本趟所欲子序列都被枢轴以上述规则划分完毕后会得到新的一组更短的子序列,它们将成为下一趟划分的初始序列集。
void quickSort(int arr[],int low,int high){
/*对从arr[low]到arr[high]的关键字进行排序*/
int temp;
int i=low,j=high;
if(low<high){
temp=arr[low];
/*下面这个循环完成了一趟排序,并且左边的全是小于temp的关键字,右边全是大于temp的关键字*/
while(i<j){
while(j>i&&arr[j]>=temp) --j; //从右往左扫描,找到一个小于temp的关键字
if(i<j){
arr[i]=arr[j]; //放在temp左边
++i; //i右移一位
}
while(j>i&&arr[j]<temp) ++i; //从左往右扫描,找到一个大于temp的关键字
if(i<j){
arr[j]=arr[i]; //放在temp右边
--j; //j左移一位
}
}
arr[i]=temp; //将temp放在最终位置
quickSort(arr,low,i-1); //递归的对temp左边的关键字进行排序
quickSort(arr,i+1,high); //递归的对temp右边的关键字进行排序
}
}
归并排序
二路归并排序
思想:
原始序列:49 38 65 97 76 13 27
1)将原始序列看做只含有一个关键字的子序列
{49} {38} {65} {97} {76} {13} {27}
2)两两归并
{38,49},{65,97},{13,76},{27}
3)继续归并
{38,49,65,97},{13,27,76}
4)继续归并,结果如下
13 27 38 49 65 76 97
//实现将arr[]中low~mid和mid+1~high范围内的两段有序序列归并成一段有序序列
void merge(int arr[],int low,int mid,int high){
int i,j,k;
int n1=mid-low+1; //从low到mid的关键字个数
int n2=high-mid; //从mid到high的关键字个数
int L[n1],R[n2];
for(i=0;i<n1;i++) //将待归并的子表赋值给L[]
L[i]=arr[low+i];
for(j=0;j<n2;j++) //将待归并的子表赋值给R[]
R[j]=arr[mid+1+j];
i=0;j=0;k=low;
while(i<n1&&j<n2){
if(L[i]<=R[j]){
arr[k]=L[i];
i++;
}else{
arr[k]=R[j];
j++;
}
k++;
}
while(i<n1){
k++;
arr[k]=L[i];
i++;
}
while(j<n2){
k++;
arr[k]=R[j];
j++;
}
}
//二路归并排序主算法
void mergeSort(int arr[],int low,int high){
if(low<high){
int mid=(low+high)/2;
mergeSort(arr,low,high);
mergeSort(arr,mid+1,high);
metge(arr,low,mid,high);
}
}
基数排序
1)按关键字最低位分桶

2)重新收集,按桶的顺序从左到右,从下往上收集

3)第二趟分配,将关键字按位扫描,按照第二位分配到桶内

4)重新收集,按桶的顺序从左到右,从下往上收集

5)第三趟分配,将关键字按位扫描,按照第一位分配到桶内

6)重新收集,按桶的顺序从左到右,从下往上收集

稳定性分析
**稳定性:**序列中存在相同关键字但位置不同例如:49 49排序完成后(49 49)相同的关键字还维持着排序前的顺序,则稳定。
**稳定:**冒泡排序、直接插入排序、归并排序、基数排序、(关键字插入,适用链表存储)简单选择排序有可能稳定。
**不稳定:**快速排序、希尔排序、堆排序、(关键字交换,适用顺序表存储)简单选择排序。

08-外部排序
置换-选择排序
原始序列:15 19 04 83 12 27 11 25 11 34 26 07 10 90 06
假设内存中可以存放最多4个记录,生成初始归并段
步数 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | ... |
缓冲区内容 | 15 | 15 | 11 | 11 | 11 | 11 | 11 | 11 | 11 | 11 | 11 | 06 | ... | ... |
19 | 19 | 19 | 19 | 25 | 16 | 16 | 16 | 16 | 16 | 16 | 16 | ... | ... | |
04 | 12 | 27 | 27 | 27 | 27 | 34 | 26 | 26 | 26 | 26 | 26 | ... | ... | |
83 | 83 | 83 | 83 | 83 | 83 | 83 | 83 | 07 | 10 | 90 | 90 | ... | ... | |
输出结果 | 04 | 12 | 15 | 19 | 25 | 27 | 34 | 83 | 07 | 10 | 11 | 16 | ... | ... |
第一初始归并段 | 第二初始归并段 | ... | ... |
通过置换-选择排序算法得到的m个初始归并段长度可能不同。不同的归并策略可能导致归并次数不同,即意味着需要I/O操作次数不同,因此需要找出一种归并次数最少的归并策略来减少I/O的操作次数,以提高排序效率。引出了下一个知识点——最佳归并树。
最佳归并树
不同的归并方法的总I/O的次数不同,我们要找到总I/O的次数最小的归并方法,即最佳归并树。
I/O次数其实就是带权路径的长度,即利用哈夫曼树的构造方法。


原始序列:9 2 3 6 12 30 17 18 24(数值代表每个归并段所含关键字个数)
采用3路归并:


败者树(是一种数据结构不是方法)
