不知不觉,数据结构已经学完了,博主也马上大二了时间过得太快了,但是数据结构学得确实不太好,楼主花了将近一天的时间整理了一下排序的有关算法。希望对大家也有所帮助。
每步将一个待排序记录,按其关键码大小,插入到前面已经排好序的一组记录的适当位置上,直到记录全部插入为止。
void InsertSort ( SqList &L ) { //对顺序表L作直接插入排序
for ( i = 2; i <=L.length; ++ i )
//直接在原始无序表L中排序
if (L.r[i].key < L.r[i-1].key) //若L.r[i]较小则插入有序子表内
{ L.r[0]= L.r[i]; //先将待插入的元素放入“哨兵”位置
L.r[i]= L.r[i-1]; //子表元素开始后移
for ( j=i-2; L.r[0].key < L.r[j].key; --j ) L.r[j+1]= L.r[j];
//只要子表元素比哨兵大就不断后移
L.r[j+1]= L.r[0]; //直到子表元素小于哨兵,将哨兵值送入
//当前要插入的位置(包括插入到表首)
} //if
}// InsertSort
从第二个记录开始逐趟开始插入
这个相对于直接插入排序,减少了比较的次数,但是没有减少移动的次数: n 2 n^2 n2 / 4次
只是我们查找插入的位置时,使用了折半查找的方法。
相比之下,减少了移动的次数,约为 n 2 n^2 n2 / 8次
二路排序
#include
#include
void insert(int arr[], int temp[], int n)
{
int i,first,final,k;
first = final = 0;//分别记录temp数组中最大值和最小值的位置
temp[0] = arr[0];
for (i = 1; i < n; i ++){
// 待插入元素比最小的元素小
if (arr[i] < temp[first]){
first = (first - 1 + n) % n;
temp[first] = arr[i];
}
// 待插入元素比最大元素大
else if (arr[i] > temp[final]){
final = (final + 1 + n) % n;
temp[final] = arr[i];
}
// 插入元素比最小大,比最大小
else {
k = (final + 1 + n) % n;
//当插入值比当前值小时,需要移动当前值的位置
while (temp[((k - 1) + n) % n] > arr[i]) {
temp[(k + n) % n] =temp[(k - 1 + n) % n];
k = (k - 1 + n) % n;
}
//插入该值
temp[(k + n) % n] = arr[i];
//因为最大值的位置改变,所以需要实时更新final的位置
final = (final + 1 + n) % n;
}
}
// 将排序记录复制到原来的顺序表里
for (k = 0; k < n; k ++) {
arr[k] = temp[(first + k) % n];
}
}
int main()
{
int a[8] = {3,1,7,5,2,4,9,6};
int temp[8];
insert(a,temp,8);
for (int i = 0; i < 8; i ++){
printf("%d ", a[i]);
}
return 0;
}
希尔排序就是指取固定的一些增量序列来分别进行直接插入排序,比如去dk = 5, 3, 1这种,到最后对基本有序的全体序列进行一次直接插入排序注意增量序列的之中没有除1之外的公因子,且最后一个值必定为1.
算法思想:
先将整个待排记录序列分割成若干子序列分别进行直接插入排序,待整个序列中的记录基本有序时,再对全体记录进行一次直接插入排序。分为多个子序列的方法:设置一系列增量值(dk值),将相隔某个增量的记录组成一个子序列,如第一趟dk=5,则r1,r6.为一组,r2,r7为一组,然后每一组进行直接插入排序。以此类推。不断减小dk的值,直到为1,再进行直接插入排序就能实现对整体记录的插入排序了。
dk值较大,子序列中对象较少,速度较快;
dk值逐渐减小,子序列中对象变多,但大多数对象已基本有序,所以排序速度仍然很快。
输入:增量的数目k,各个增量dk, 顺序表的长度,顺序表中各个元素。
输出:排好序的顺序表中各个元素。
算法图解(这是从网上看到的图,侵删):
void ShellSort(SqList &L, int dlta[], int t){
//按增量序列dlta[0..t-1]对顺序表L做希尔排序。
for(k = 0;k<t;++k)
ShellInsert(L, dlta[k]); //一趟增量为dlta[k]的插入排序
}
void ShellInsert(SqList &L,int dk)
{
//对顺序表进行一趟增量为dk的Shell排序,dk为步长因子
//相对于一趟插入排序相比,做了如下修改:
// 1. 前后记录位置的增量是dk,而不是1
// 2. r[0]只是暂存单元,不是哨兵。当j<=0时,插入位置已找到。
int i,j;
for(i=1+dk;i<=L.length;i++) //开始将r[i]插入有序增量子表
{
if(L.r[i].key<L.r[i-dk].key)
{
RedCopy(L.r[0],L.r[i]); //暂存在r[0]
for(j=i-dk;j>0&&L.r[0].key<L.r[j].key;j-=dk)
RedCopy(L.r[j+dk],L.r[j]); //关键字较大的记录在字表中后移
RedCopy(L.r[j+dk],L.r[0]); //在本趟结束时将r[i]插入到正确位置
}
}
}
就是先选用一个数(可以随机选也可以直接用第一个),以他为基准然后比它大的放后面,比他小的放前面,这样形成一个大致左边必定比右边小的序列后,再次对于两边的这些数分段进行递归相同的操作,不稳定
优点:平均性能好,O(nlog2n),2为下标
缺点:不稳定,初始序列有序或基本有序时,时间复杂度降为O(n^2)。
算法图解:
快排的算法详解:
一趟快速排序我们最初实现的思想就是设置两个指针low、high,一个指向基准的元素,一个指向末尾,然后我们从末尾开始往前遍历,如果我们的high小于基准了开始,那么就和low指向的基准交换。然后开始从low开始往后遍历,直到找到一个low比基准大的元素和此时的high(指向的还是基准)交换,然后再从high开始……如此重复直到我们的low=high.
但是这么算的话每次交换一对记录需要进行3次移动赋值操作。实际上我们对于基准的移动是多余的,基准就在我们最后循环结束的low=high位置。所以我们可以选择将基准提前放好,然后我们选择该交换的时候直接赋值high小于基准后直接赋值给low……直到我们low = high的时候再将放好的基准放到low(high)的位置。
int Partition(SqList &L, int low, int high)
{
L.r[0] = L.r[low]; //用子表的第一个记录作基准记录
pivotkey = L.r[low].key; //记录关键字
while(low < high) //从表的两端交替地向中间扫描
{
while(low < high && L.r[high].key >= pivotkey)--high;
L.r[low] = L.r[high]; //将比基准小的记录移到低端
while(low < high && L.r[low].key <= pivotkey)++low;
L.r[high] = L.r[low]; //将比基准大的记录移到高端
}
L.r[low] = L.r[0];
return low; //返回基准位置
}
void QSort(SqList &L, int low, int high){
//对顺序表L中的子序列L.r[low..high]做快速排序
if(low >= high)
return;
pivotkey = Partition(L, low, high);
QSort(L, low, pivotkey-1);
QSort(L, pivotkey+1, high);
return;
}
时间效率:O(nlog2n) —因为每趟确定的元素呈指数增加
空间效率:O(log2n)—递归栈(存每层low,high和pivot)
稳 定 性: 不 稳 定 —因为跳跃式交换。
每一趟(第 i 趟)在后面 n-i+1 个待排记录中选取关键字最小的记录作为有序序列中的第 i 个记录。
也就是先从所有中选出最小的作为第一个,再从剩下的选出最小的作为第二个,…
分类主要有简单选择排序,锦标赛排序,堆排序。
基本思想:每经过一趟比较就找出一个最小值,与待排序列
最前面的位置互换即可。
——首先,在n个记录中选择最小者放到r[1]位置;然后,从剩余的n-1个
记录中选择最小者放到r[2]位置;…如此进行下去,直到全部有序为止。
优点:实现简单
缺点:每趟只能确定一个元素,表长为n时需要n-1趟
前提:顺序存储结构
Void SelectSort(SqList &L ) {
for (i=1; i<L.length; ++i){
j = SelectMinKey(L,i);
if( i!=j ) r[i] «r[j];
} //for
} //SelectSort
显然简单选择排序的缺点是非常明显的,第一次比较n-1次,第二次比较n-2次,以此类推,如果我们要优化的时候,我们要考虑是否第一趟比较n-1次后第二趟还需要比较n-2次,这个是很重要的。所以我们应该从减少比较个数这个地方开始着手优化
我们可以参考我们体育比赛中的锦标赛排序,在8个运动员中选择前3名最多只需要11场比赛,而不是7+6+5场比赛。
而这就是我们的锦标赛排序:
基本思想:与体育比赛时的淘汰赛类似。
首先对 n 个记录的关键字进行两两比较,得到 [n/2] 个 优胜者(关键字小者),作为第一步比较的结果保留下来。然后在这 [n/2] 个较小者之间再进行两两比较,…,如此重复,直到选出最小关键字的记录为止。
优点:减少比较次数,加快排序速度
缺点:空间效率低
算法图解:
以关键字序列T= (21,25,49,25*,16,08,63)为例
锦标赛排序构成的树是完全(满)二叉树,其深度[ l o g 2 n log_{2}{n} log2n] +1,其中 n 为待排序元素(叶子结点)个数。
• 时间复杂度:O( n l o g 2 n nlog_{2}{n} nlog2n) —n个记录各自比较约 l o g 2 n log_{2}{n} log2n次
• 空间效率: O(n) —胜者树的附加内结点共有n0-1个!
• 稳定性:稳定 —可事先约定左结点“小”
这种排序方法的劣势就是消耗空间较大,“最大值”进行多余的比较等问题。为了弥补,J.willioms在1964年提出了另一种形式的选择排序——堆排序。
这里有一个有关堆排序讲的很好的博客
我们需要解决这几个问题:什么是堆?怎么建堆?怎么堆排序?
分为大根堆和小根堆,这个其实如果根节点是最大值,那就是大根堆,反之就是小根堆。例子如下:
那如何建堆呢?
基本方式:从最后一个非终端结点开始往前逐步调整,让每个双亲大于(或小于)子女,直到根结点为止。
完全二叉树的第一个非终端结点编号必为[n/2].
终端结点:叶子结点(所以不需要我们单独调整)
这里给出图解例子:
注意以上建堆的过程中是从最后一个非叶子结点开始每次比较的都是结点的左右孩子,不必和父节点比较,然后我们比完后不符合的就交换(这里如果两个子女都比父节点大那么就选大的上浮),注意交换过之后要再比一次左右节点(即将父母结点下移之后如果还有子节点还要继续下移判断!)。然后再往前找到倒数第二个非叶子结点…以此类推直到根结点。
我们知道,建成一个堆后,堆的根节点就是最大值(最小值),所以我们可以直接输出根节点,问题在于,我们输出根节点后还需要将剩余的缺乏根节点的堆再次维护成一个完整的堆然后再次输出根节点以达到堆排序的目的。
方法:将当前顶点与堆尾记录交换,然后仿建堆动作重新调整,如此反复直至排序结束。将任务转化为—>
H.r[i…m]中除r[i]外,其他都具有堆特征。现调整r[i]的值 ,使H.r[i…m]为堆。
这就相当于我们把根节点和最末尾的那个结点交换了一下位置然后数组长度length–直接删除了原来根节点,然后目前就是一个根节点不满足堆其他点都满足堆的一个“堆”,我们接下来只需要维护这一个点就可以了。以此类推,达到堆排序目的。即:
基于初始堆进行堆排序的算法步骤:
堆的第一个对象r[1]具有最大的关键码,将r[1]与r[n]对调,把具有最大关键码的对象交换到最后;
再对前面的n-1个对象,使用堆的调整算法,重新建立堆。
结果具有次最大关键码的对象又上浮到堆顶,即r[1] 位置;
再对调r[1]和r[n-1],然后对前n-2个对象重新调整,…
如此反复,最后得到全部排序好的对象序列。
图解:
首先我们直到由于我们建堆过程中需要比较调整,所以需要用到堆调整函数HeapAdjust。
HeapAdjust(HeapType &H , int i, int m ){
/*从结点i开始到当前堆尾m为止,自上向下比较,如果子女的
值大于双亲结点的值,则互相交换,即把局部调整为大根堆。*/
current=i; temp=H.r[i]; child=2*i; //temp暂存 r[i]值,child是其左孩子
while(child<=m){ //检查是否到达当前堆尾,未到尾则整理
if ( child<m && H.r[child].key<H.r[child+1].key )
child= child+1; //让child指向两子女中的大者位置
if ( temp.key>=H.r[child].key ) breack; //根大则不必调整,函数结束
else {
H.r[current]=H.r[child]; //否则子女中的大者上移
current= child; child=2* child; } //将根下移到孩子位置并继续向下整理!(这一点很关键)
}// while
H.r[current]=temp; //直到自下而上都满足堆定义,再安置入口结点
} // HeapAdjust
void HeapSort (HeapType &H ) {
//H是顺序表,含有H.r[ ]和H.length两个分量
for (i = H.length / 2; i >0; - - i ) //把r[1…length]建成大根堆
HeapAdjust(H.r, i, H.length ); //使r[i…length]成为大根堆
} // HeapSort
堆排序
重建时,2至i-1号结点已符合堆的要求,故只需从 1号结点开始调整。因每次从堆顶开始调整,故每次调用耗时
O(log2n).
• 时间效率: T(n) = O( n l o g 2 n nlog_{2}{n} nlog2n)。因为整个排序过程中需要调
用n-1次HeapAdjust( )算法,而此算法耗时为O( l o g 2 n log_{2}{n} log2n);
• 注意:初始建堆的关键字比较次数≤4n,T(n)=Θ(n)。
• 空间效率:O(1)。在for循环中交换记录时用到临时变量temp。
• 稳定性: 不稳定。
• 优点:对小文件效果不明显,但对大文件有效。
void HeapSort (HeapType &H ) {
//对顺序表H进行堆排序
for ( i = H.length / 2; i >0; - - i )
HeapAdjust(H,i, H.length ); //for,建立初始堆
for ( i = H.length; i > 1; - -i) {
H.r[1] ←→ H.r[i]; //交换,要借用temp
HeapAdjust( H, 1,i-1 ); //重建最大堆, m=i-1
}
}
基本思想:归并含义就是将两个或两个以上的有序表合成一个新的有序表。利用该思想可以假设刚开始的无序表是一个有n个长度为1的有序表,然后两两归并,得到[n/2]个长度为2的有序表,再次归并得到[n/4]个长度为4的有序表……以此类推最终得到长度为n的有序表。
算法图解:
归并排序的算法还是很简单的:
void Merge (RcdType SR[ ], RcdType &TR[ ],int i, m, n) {
// 将有序的SR[i…m]和SR[m+1…n]归并为有序的TR[i…n]
for(k=i , j=m+1; i<=m && j<=n; ++k ) {
if ( SR[i].key<= SR[j].key )TR[k]=SR[i++];
else TR[k]=SR[j++]; // 将两个SR记录由小到大并入TR
} // for
if (i<=m) TR[k…n]=SR[i…m]; // 将剩余的SR[i…m]复制到TR
if (j<=n) TR[k…n]=SR[j…n]; // 将剩余的SR[j…n]复制到TR
} // Merge
void MSort (RcdType SR[ ], RcdType &TR1[ ],int s, int t) {
// 将无序的SR[s…t]归并排序为TR1[s…t]
if ( s==t )TR1[s]=SR[s]; // 当len=1时返回
else {
m=(s+t)/2; // 将SR [s…t]平分为SR [s…m]和SR [m+1…t]
MSort (SR,&TR2,s, m); // 将SR 一分为二, 2分为4…
// 递归地将SR [s…m]归并为有序的TR2[s…m]
MSort (SR,&TR2,m+1, t );
// 递归地将SR [m+1…t]归并为有序的TR2[m+1…t]
Merge(TR2, TR1, s, m, t );
// 将TR2 [s…m]和TR2 [m+1…t]归并到TR1 [s…t]
} //if
} // MSort
//TR2只是一个辅助数组
第一个函数用来合并有序的两个序列,而第二个函数用来递归进行归并,这里用到了TR2这个辅助数组。首先不断递归至n个长度为1的有序数组到TR2中,然后将他们两两Merge归并起来到TR2中(注意除了第一层递归外其余的所有递归过程中虽然Merge调用的看着好像是TR1,但那是形参,实际上那是TR2!!也就是那个Merge函数实际上是在调用归并TR2的s…m and m+1…t项到自己的数组中!!),接着直到我们递归回溯至最后一层时,也就是开始的函数,我们调用Merge把就差一步就有序的TR2归并排好放到TR1中。
基数排序是与前面的排序完全不同,前面的排序主要是通过关键字间的比较和移动记录这两种操作,而实现基数排序不需要记录关键字之间的比较。它是一种借助多关键字排序的思想对单逻辑关键字进行排序的方法。
多关键字排序:n个元素的序列{R1,R2,…, Rn},每个元素Ri有d个关键字(K0i, K1i,…, Kd-1i),则序列对关键字(K0i, K1i,…, Kd-1i)有序是指:
对于序列中任意两个记录Ri和Rj(i
主要分为两类:
最高位优先(MSD):
先对最主位关键字K0进行排序,将序列分成若干个子序列,每个子序列中的元素具有相同的K0值,然后分别就每个子序列对关键字K1进行排序,按K1值的不同再分成更小的子序列,依次重复,直至对Kd-2进行排序之后得到的每个子序列中的元素都具有相同的(K0, K1,…, Kd-2),而后分别为每个子序列对Kd-1 进行排序,最后将所有子序列依次联接成为一个有序序列。
最低位优先(LSD) : 先对最次位关键字Kd-1进行排序,然后对Kd-2进行排序,依次重复,直至对K0进行排序后便成为一个有序序列
链式基数排序:
对于整型或字符型的单关键字,可以看成是由多个数位或多个字符
构成的多关键字。仅分析关键字自身每位的值,通过分配、收集进行处理。
基本有序时可选用直接插入、简单选择、堆排序、锦标赛排序、冒泡排序、归并排序、(希尔排序)等方法,其中插入排序和冒泡应该是最快的。因主要是比较操作,移动元素很少。此时平均时间复杂度为O(n)。
无序的情况下最好选用快速排序、希尔排序、简单选择排序等,这些算法的共同特点是,通过“振荡”让数值相差不大但位置差异很大的元素尽快到位。
终于写完了QAQ,真是累死我了。感谢大家看到这里,看到这里的是真,也欢迎大家一键三连哦~
以上。