大部分内部排序算法只适用于顺序存储的线性表
基本思想:每次将一个待排序的记录按其关键字大小插入前面已排好序的子序列,直到全部记录插入完成
步骤:(1)找出L[i]在L[1……i-1]中的插入位置k
(2)把L[k….i-1]依次向后挪动
(3)把L[i]复制到L[k]
初始L[1]视为一个已经排好的子序列
void Insertsort(Elemtype a[],int n ) \\序列a和a的长度
{
int i ,j;
for(i=2; i<=n; i++)
{
if(a[i]<a[i-1])
{
a[0]=a[i]; \\复制为哨兵,a[0]不存放数据
for(j=i-1; a[0]<a[j]; j--)
{
a[j+1]=a[j];
}
a[j+1]=a[0];
}
}
}
在直接插入排序的基础上,把比较和移动操作分离,先折半查找出元素的插入位置,然后统一移动带插入位置之后的所有元素
void Insertsort(Elemtype a[], int n)
{
int i ,j ,low,high,mid;
for(i=2; i<= n; i++)
{
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;
}
}
for(j =i-1; j>high+1; j--)
{
a[j+1] =a[j]; \\统一后移
}
a[high+1]=a[0];
}
}
待排序列为正序时,直接插入排序的时间复杂度会从O(n2)降低到O(n),因此更适用于基本有序的排序表和数据量不大的排序表,希尔排序正是基于这两点对直接插入排序改进来的,又称缩小增量排序
基本思想:把待排序列分割成若干个形如L[i,i+d,i+2d……i+kd]的特殊字表,即把相隔某个增量的记录组成一个子表,对各个子表分别进行直接插入排序,当整个表中的元素基本有序时,再对全体进行一次直接插入排序
过程:取一个小于n的步长d1,把表中的全部记录分成d1组,左右距离为d1的倍数的记录放在同一组,组内进行直接插入排序,然后取第二个步长d2
void Shellsort(Elemtype a[], int n )
{
int dk,i,j;
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[0]<a[j]; j-=dk)
{
a[j+dk]=a[j];
}
a[j+dk]=a[0];
}
}
}
}
void Bubblesort(Elemtype 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 ;
}
}
基本思想:基于分治法,在待排序列中任取一个作为枢轴,通过一趟排序将待排序列分为独立的两部分L[1…k-1]和L[k+1……n],则枢轴放在了L[k],称为一次划分,然后分别递归的对两个子表重复上述过程,直到每个部分内只有一个元素或为空为止
void Quicksort(Elemtype a[],int low,int high)
{
if(low <high)
{
int p= Partition(a,low ,high); \\划分
Quicksort(a,low ,p-1); \\递归排序
Quicksort(a,p+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]; \\比枢轴小的移动到左边
while(low<high && a[low]<= pivot)
{
low++;
}
a[high]=a[low]; \\比枢轴大的移动到右边
}
a[low]= pivot; \\枢轴元素的最终位置
return low; \\返回存储枢轴的最终位置
}
快排不会产生子序列,但每趟排序会有一个元素枢轴放在最终位置上
基本思想:每一趟选择待排序列中最小的元素,作为有序序列的下一个元素,直到全部到最后一个就不用排了
设排序表为L[1……n],第i趟排序从L[i……n]中选择关键字最小的元素与L[i]交换,每一趟排序可以确定一个元素的最终位置,n-1趟完成排序
void Selectsort(Elemtype a[],int n )
{
for(int i =0; i<n-1 ; i++)
{
int min =i;
for(int j=i+1;j<n; j++)
{
if(a[j]<a[min])
{
min=j;
}
}
if(min!= j)
{
swap(a[i],a[min]);
}
}
}
L[1……n]的关键字序列满足:
(1)L[i]>=L[2i]且L[i]>=L[2i+1],称之为大根堆,最大元素存放在根结点,且任意一个结点的值小于或等于父结点
(1)L[i]<=L[2i]且L[i]<=L[2i+1],称之为小根堆,最小元素存放在根结点,且任意一个结点的值大于或等于父结点
堆排序的基本思路:把序列建成一个初始堆,由于堆本身的特点,即根结点的元素是最大值或者最小值,输出堆顶元素后,通常将堆底元素送入堆顶,此时根结点不满足堆的性质,堆被破坏,将堆顶元素向下调整使其继续保持性质,再输出堆顶元素,如此重复,直到堆只剩一个元素
构造初始堆:n个结点的完全二叉树,最后一个结点是第 ⌊ \lfloor ⌊n/2 ⌋ \rfloor ⌋个结点的孩子,对第 ⌊ \lfloor ⌊n/2 ⌋ \rfloor ⌋个结点为根的子树筛选(对大根堆,若根结点的关键字小于左右孩子中关键字较大者,则交换),使该子树成为堆。之后向前依次对各结点( ⌊ \lfloor ⌊n/2 ⌋ \rfloor ⌋-1~1)为根的子树进行筛选,看结点值是否大于左右子结点的值,若不大于,左右子节点中的较大值与之交换,交换后可能会破坏下一级的堆,于是继续采用上述方法构造下一级的堆,直到以该结点为根的子树构成堆为止。然后反复调整堆建堆,直到根结点
\\建立大根堆
void Buildmaxheap(Elemtype a[], int len )
{
for(int i =len/2; i>0; i--)
{
Headadjust(a,i,len) \\从n/2~1反复调整堆
}
}
void Headadjust(Elemtype a[], int k, int len)
{
\\调整元素k为根的子树
a[0]=a[k]; \\暂存子树结点
for(int i=2*k; i<=len ;i *=2) \\沿着key较大的子结点向下筛选
{
if(i<len && a[i]<a[i+1])
{
i++; \\取关键字大的子结点的下标
}
if(a[0]>=a[i])
{
break;
}
else
{
a[k]=a[i]; \\a[i]调整到父结点
k=i; \\需修改k,继续向下筛选
}
}
a[k]=a[0];
}
\\堆排序
void Heapsort(Elemtype a[], int len)
{
Buildmaxheap(a,len);
for(int i =len; i>1; i--)
{
swap(a[i],a[1]);
Headadjust(a,i,i-1);
}
}
堆也支持插入操作
堆排序适合关键字较多的情况,如在一亿个元素中选最大的100个
基本思想:设长度为n的序列,将其视为n个有序的子表,每个子表的长度为1,然后两两归并,得到 ⌈ \lceil ⌈n/2 ⌉ \rceil ⌉个长度为2或1的有序表,就两两归并,直到合并成长度为n的有序表,这称为2路归并排序
Merge()的功能是将前后相邻的两个有序表归并为一个有序表;设==两个有序表a[low….mid],a[mid+1……high]==存放在同一顺序表中的相邻位置,先将他们复制到辅助数组B中,每次对应B中的两个段取出一个记录进行关键字的比较,将较小的放入a,当数组B中有一段的下标超出其对应的表长(即该段的所有元素都已经复制到a),将另一段的剩余部分直接复制到a
Elemtype *b=(Elemtype*)malloc((n+1)*sizeof(Elemtype));
void Merge(Elemtype a[], int low, int mid ,int high)
{
int i ,j ,k;
for(k =low; k<=high; k++)
{
B[k]=A[k];
}
for(i = low ,j=mid+1,k=i; i<=mid && j<=high; k++)
{
if(B[i]<=B[j])
{
a[k]=B[i++];
}
else
{
a[k]=B[j++];
}
}
while(i<=mid)
{
a[k++]=B[i++]; \\第一个表未检测完,复制
}
while(j<=high)
{
a[k++]=B[j++]; \\第二个表未检测完,复制,两个while只有一个会执行
}
}
一趟归并操作:调用 ⌈ \lceil ⌈n/2h ⌉ \rceil ⌉次算法merge(),将序列中前后相邻且长度为h的有序段进行两两归并,得到前后相邻,长度为2h的有序段,整个归并排序需要进行 ⌈ \lceil ⌈log2n ⌉ \rceil ⌉趟
递归的2路归并过程:
(1)分解:n个元素的表分成n/2个元素的子表,采用2路归并排序算法对两个子表递归的进行排序
(2)合并:合并两个已排序的子表得到排序结果
void Mergesort(Elemtype 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); \\归并
}
}
n个元素进行k路归并排序,排序的趟数m满足km=n,m=logkn,m为整数,故m= ⌈ \lceil ⌈log2n ⌉ \rceil ⌉
基于关键字的各位的大小进行排序,是一种借助多关键字排序的思想对单逻辑关键字进行排序的方法
最高位优先法:按关键字位权重递减依次逐层划分成若干更小的子序列,最后将所有子序列依次连接成一个有序序列
最低位优先法:按关键字位权重递增依次进行排序,最后形成一个有序序列
以r为基数的最低位优先基数排序的过程,在排序过程中,使用r个队列Q0,Q1……Qr-1
对i=0,1,2….d-1,依次做一次分配和收集
分配:开始时,把队列Q0,Q1……Qr-1置成空,然后依次考察线性表中的每个结点aj,若aj的关键字kji=k,就把aj放进Qk队列
收集:把队列Q0,Q1……Qr-1队列中的结点依次首尾相接,得到新的结点序列,从而组成新的线性表
通常采用链式基数排序,举例{278,109,063,930,589,184,505,269,008,083}
各种内部算法的性能比较
文件的记录过多无法全复制进内存,因此需要把待排序的记录存储在外存上,排序时把数据一部分一部分的调入内存进行排序,排序过程中需要多次进行内存和外存之间的交换,这种排序方法就是外部排序
文件通常按块存储在磁盘上,在外部排序过程中的世界代价主要考虑访问磁盘的次数,即I/O次数
通常采用归并排序法:
(1)根据内存缓冲区大小,将外存的文件分成若干个长度为m的子文件,一次读入内存利用内部排序方法对他们进行排序,并将排序后得到的有序文件重新写回外存,称这些有序子文件为归并段或顺串
(2)对这些归并段进行逐趟归并,使归并段逐渐由小到大,直到整个文件有序
外部排序时间=内部排序时间+外存信息读写的时间+内部归并所需的时间
增大归并路数,可以减少归并趟数,进而减少总的磁盘I/O次数
但是增加归并路数会导致内部归并的时间增加,做内部归并时,在k个元素中选择关键字最小的记录需要比较k-1次,每趟归并n个元素需要做(n-1)(k-1)次比较,S趟归并总共需要的比较次数为S(n-1)(k-1)= ⌈ \lceil ⌈log2r ⌉ \rceil ⌉(n-1)(k-1)/ ⌈ \lceil ⌈log2k ⌉ \rceil ⌉,所以不能采用普通的内部归并排序算法
为了内部归并不受k的增大的影响,引入败者树
败者树的k个叶结点分别存放k个归并段在归并过程中房钱参加比较的记录,内部结点永爱记忆左右子树中的失败者,胜者继续向上比较,一直到根结点,若大的为失败者,则根结点指向的数为最小数
k路归并的败者树深度为 ⌈ \lceil ⌈log2k ⌉ \rceil ⌉,k个记录中选择最小关键字最多需要 ⌈ \lceil ⌈log2k ⌉ \rceil ⌉次比较,总的比较次数为S(n-1) ⌈ \lceil ⌈log2k ⌉ \rceil ⌉=(n-1) ⌈ \lceil ⌈log2r ⌉ \rceil ⌉
使用败者树内部归并的比较次数就与k无关了
但是k仍然不是越大越好,因为需要增加相应的输入输出缓冲区的个数;内存空间不变,就会减少每个输入缓冲区的容量,使得内外存交换数据的次数增大;k过大,归并趟数减少,但是读写外存的次数仍会增加
减少初始归并段个数r也可以减少归并趟数S,若总的记录个数为n,每个归并段的长度为l,则归并段的个数r= ⌈ \lceil ⌈n/l ⌉ \rceil ⌉
采用内部排序方法得到的各个初始归并段长度都相同,因此找到新的方法来产生更长的初始归并段,这就是置换选择排序
设初始待排文件为FI,初始归并段除数文件为FO,内存工作区为WA,FO和WA的初始状态为空,WA可容纳w个记录
步骤:
(1)从FI输入w个记录到WA
(2)从WA中选出关键字最小值的记录,记为MINIMAX
(3)将MINIMAX记录输出到FO中
(4)若FI不空,从FI输入下一个记录到WA
(5)从WA中所有关键字比MINIMA记录的关键字大的记录中选出最小关键字记录,作为新的MINIMAX
(6)重复3-5,直到WA选不出新的MINIMAX,由此得到一个初始归并段,输出一个归并段的结束标志到FO中
(7)重复2-6,直到WA空,由此得到全部初始归并段
设WA容量为3
文件经过置换选择排序后,得到的是长度不等的初始归并段,下面讨论如何组织长度不等的初始归并段的归并顺序,使得I/O次数最少
设9个初始归并段,长度依次为9,30,12,18,3,17,2,6,64,做3路平衡归并,如图
树的带权路径长度WPL为过程过程中的总读记录数,则I/O次数=2*WPL=484
归并方案不同,所得的归并树不同,可以将哈夫曼树的思想推广到m叉树的情形,让记录数最少的初始归并段先归并,记录数最多的最后归并,可以建立总的I/O次数最好的最佳归并树
若初始归并段不足以构成一颗严格k叉树时(如9个段的3路归并却只有8个段),需要添加长度为0的虚段
怎么判断添加多少个虚段合适?设有n个度为0的结点,度为k的结点有nk个