排序又可以分为内部排序和外部排序,内部排序主要指数据元素全部放在内存中的排序,外部排序指数据元素太多不能同时放在内存中,内外存之间不能移动数据。
排序包括四个方面
插入排序:主要包括直接插入排序与希尔排序
选择排序:包括直接选择排序,树型选择排序与堆排序
交换排序:包括冒泡排序与快速排序
归并排序:归并排序
交换类排序
分配类排序
直接插入排序
直接插入排序就是将待排序的数据按照数据比较的大小有序插入到排好序的记录子集中,直到所有的数据插入完成,得到一个新的有序序列。 首先从第二个元素开始遍历,用k保存取出的元素,每个与已排好序的序列中的最后一个元素比较,如果大于最后一个元素,直接放入最后一个元素的后面,否则,继续向有序序列最后一个元素的前面遍历,继续比较,如果找到比取出的元素小的元素,就放入它的后面,有序序列向后搬移(因为已经用k保存了取出的元素,所以不用担心覆盖问题)
1 监视哨r[0]保存待插入的记录
2 从后往前查找应插入的位置
3 查找与移动用同一循环完成
它的最坏时间复杂度为O(n^2), 因为存在逆序情况,总比较次数∑(i=2->n)i=(n+2)(n-1)/2,记录次数∑(i=2->n)(i+1)=(n+4)(n-1)/2,故直接插入排序;最好情况,是个有序序列,比较次数n-1,移动次数2(n-1)最好的时间复杂度时O(n),因为需要遍历一遍,平均时间复杂度时O(n^2),由于没有开辟额外的空间,因此空间复杂度时O(1),稳定,因为相等的两个数据顺序没有发生变化,它适用于数据量比较小,接近有序的场景。
void InSort(RecordType r[],int length) // 对记录数组r做直接插入排序,length为数组长度 { for(i=2;i<length;i++) { r[0]=r[i]; j=i-1; // 将带插入记录放到监视哨r【0】中 while(r[0].key<r[j].key) { // 寻找插入位置 r[j+1]=r[j];// 移动比较在同一个循环进行 j=j-1; } r[j+1]=r[0]; // 将待插入记录插入已经排序的序列中 } // insort }算法稳定性:
证明一种排序算法是稳定的,要从算法本身的步骤中加以证明。
问题:直接插入排序的稳定性·?
答:排序比较从后往前进行
因为算法while(r[0].key<r[j].key所以相同关键字相对位置不会改变,相等情况下只能放在j+1的位置,不能放在j之前
折半插入排序
算法思想:将折半查找用于在有序记录集r[1……i-1]中确定插入位置,将对应的排序法称为折半插入排序法。
void BinSort(RecordType r[],int length) // 对数组r进行折半插入排序,length为数组的长度 { for(i=2;i<=length;++i) { x=r[i]; low=1;high=i-1; while(low<=high)// 确定插入位置 // 确定插入位置 { mid=(low+high)/2; if(x.key<r[mid].key) high=mid-1; else low=mid+1; } for(j=i-1;j>=low;--j) r[j+1]=r[j]; // 记录依次向后移动 r[low]=x; // 插入记录 } }// BinSort算法分析:
折半插入排序可减少关键字的比较次数,每插入一个元素,需要比较的次数最大为折半判定树的深度。
插入第i个元素时,设i=2^i,则需要进行log2i次比较,插入n-1个元素的平均关键字的比较次数O(nlog2n),总的时间复杂度依然史O(n^2)
希尔排序
希尔排序是通过分组,所有距离为gap的记录分在同一组内,对每一组的记录进行排序,然后减小gap,重复上述工作,它是对直接插入排序的优化,但它的时间复杂度不好计算,平均时间复杂度为O(n^1.3–n ^2),z在希尔排序过程中,相同关键字记录的领先关系发生变化,说明该排序方法是不稳定的。
算法步骤:
1 对整个文件,按照隔d1分组,组内排序
2 取d2<d1(缩小增量),继续以d为距离排序,直到dt=1(同直接插入排序)为止
当dt=1时,同直接插入排序,是否比直接插入快?
直接插入排序:一次比较移动只减少一个逆转数
希尔排序:一次移动减少逆转数可能不止一个
排序算法稳定性证明直接插入排序稳定从本身出发,排序比较从后往前进行算法while(r[0].key<r[j].key希尔排序不稳定给一反例{2,4,1,2}d1=2,得到一趟排序结果为{1,2,2,4}
冒泡排序(相邻比序法)
冒泡排序每次扫描顺次比较相邻两个元素的大小,若逆序就交换位置,反复扫描,每一趟将最大的就放在了最后,每一对相邻元素都做这样的操作,直到待排序记录到没有逆序为止,,最坏的待排序序列记录按关键字的逆序进行排序,每趟冒泡排序需进行i次比较,3i次移动,经过n-1趟冒泡排序后,比较次数n(n-1)/2;移动次数3n(n-1)/2;时间复杂度是O(n^2),因为数组可能逆序,最好是O(n),空间复杂度是O(1),稳定。
void BubbSort(Record r[],int length) { n= length; change =TRUE; for(i=1;i<=n-1 && change;++i) { change=FALSE; for(j=1;j<=n-i;++j) if(r[j].key>r[j+1].key) { x=r[j]; r[j]=r[j+1]; r[j+1]=x; change=TRUE; } } }改进方法:快速排序
由于冒泡排序扫描过程中只对相邻两个元素进行比较,因此在交换两个相邻元素时只能消除一个逆序。
如果能通过两个不相邻元素的交换,消除多个逆序,则会大大加快排序速度。
快速排序方法中的一次交换可能消除多个逆序。思想是在待排序区间中,选择一个基准值,然后扫描整个待排序区间,将比基准值小的放在基准值左边,将比基准值打的放在基准值右面,这样,整个待排序区间就被分为三部分,比基准值小的数据、基准值、比基准值大的数据,在快速排序中,将区间按照基准值划分的方式有3种 ,接下来一一介绍;
hoare方法:此方法是定义两个指针,begin与end,一开始begin在头,即在left位置,end在最后,即在right位置,以最后一个元素为基准值,begin从头往后扫描,如果a[begin]小于或等于基准值,begin往后走,否则则不动 ,end从后往前扫描,如果a[end]大于或等于基准值,end向前走,否则不动,然后交换a[begin]与a[end],第二次继续进行循环,直到begin>=end,循环结束,交换a[begin]与a[right].
挖坑法:此方法与hoare方法类似,只是不再发生交换,而是一旦a[begin]大于基准值,将a[begin]赋给a[end],一旦a[end]小于基准值,将a[end]赋给a[begin],最后将基准值赋给a[begin]
前后下标法:定义一个下表div,一开始在left位置,i也在left位置,i向后搜索,以最后一个数为基准,如果a[i]大于基准值,交换a[i]与a[div],div向后走一步,否则不交换,只让div向后走一步,最后交换a[div]与a[right]。 在选择基准值时,也有三种方法:分别为选最边上,随机选,还有三数取中法,即取数组第一个值、中间位置的值,最后位置的值中大小为中的值为基准值
快速排序非递归法:定义一个栈,先让第一个数的下标入栈,再让最后一个数的下标入栈,只要栈不为空,取出第一个栈顶元素,定义为high,再取出第二个栈顶元素,定义为low,如果low大于等于high,继续以上过程,否则,取出基准元素下标,让基准元素下标+1入栈,再让high入栈,再让low入栈,再让基准元素下标-1入栈,最终如果栈为空,结束。时间复杂度是O(n*logn),因为递归每一层的时间复杂度是O(n),有二叉树的深度层,因此为logn,空间复杂度是O(logn),因为递归调用,最多耗费层数个空间,它不稳定。
一趟排序的结果
int QKPass(RecordType r[],int left,int right) { x=r[left]; low=left;high=right; while(low<high) { while(low<high && r[high].key>=x.key) high--; if(low<high) {r[low]=r[high];low++;} while(low<high && r[low].key<x.key) low++; if(low<high) { r[high]=r[low]; high--; } } r[low]=x;return low; }// QKPass最坏情况:
每趟将序列一分两半,正好在表中间,讲表分成两个大小相等的子表,类似折半查找,骑士剑复杂度分析如下:
算法思想:
第i趟简单选择排序是指通过n-i次关键字的比较,从n-i+1个记录中选出关键字最小的记录,并和第i个记录进行交换。共需进行n-1趟简单选择排序,直到所有的记录排序完成为止。
void SelectSort(RecordType r[],int length) // 对记录数组r做简单选择排序,length为数组的长度 { n=length; for(i=1;i<=n-1;++i) { k=i; for(j=i+1;j<=n;++j) if(r[i].key<r[k].key) k=j; if(k!=i) { x=r[i];r[i]=r[k]; r[k]=x; } } }// SelectSor简单选择排序时间复杂度:
简单选择排序过程中需要进行的比较次数与初始状态下待排序的记录序列的排列情况无关。
当i=1时,需进行n-1次比较
当i=2时,需进行n-2次比较
以此类推,需要比较次数为n(n-1)/2,即进行比较操作的时间复杂度为
O(n^2)
待排序记录初始状态是按照逆序排列的,则需要移动记录的次数最多为3(n-1)
最好情况:待排序记录初始状态就已经时=是正常排列了,则不需要移动记录。
主要花在比较次数上
解决方法1 空间换时间--树形选择排序,降低比较次数,把比较过程的大小关系保留下来
附加单元来保存中间结果S(O(N))+O(nlogn)
算法思想:
1 把待排序的n个记录的关键字两两进行比较,取出较小值
2 在【n/2】个较小者中,采用同样的方法进行比较选出每两个中较小者
如此反复,直至选出最小关键字记录为止
依然是数组形式存储
空间时间同时压缩
改进要点:
堆排序是在简单排序的基础上,将向量中存储的数据看成是一颗完全二叉树,利用完全二叉树中双亲结点和孩子之间的内在关系来选择关键字最小的记录。O(nlogn)+S(O(1))
待排序的记录的关键字存放在数组r[1……n]之中,将r看成是一棵完全二叉树的顺序表示,每个结点表示一个记录,任意结点r[i]的左孩子是r[2i],右孩子是r[2i+1],双亲是r[i/2]。
对于这个完全二叉树进行调整,使各结点的关键字值满足下列条件
r[i].key>=r[2i].key并且r[i].key>=r[2i+1].key(i=1,2……),满足此条件的二叉树为堆。
void crt_heap(RecordType r[],int length) // 记录数组r建堆,length为数组长度 { n=length; for(i=n/2;i>=1;--i)// 建立初堆 n/2->1 // 自第 个记录开始进行筛选建堆 sift(r,i,n); } void sift(RecordType r[],int} k,int m) { t=r[k];// 暂存根记录r[k] x=r[k].key;i=k;j=2*i; finished=FALSE; while(j<=m && !finished)// *** { if(j<m && r[j].key < r[j+1].key) j=j+1; if(x>=r[j].key) finished=TRUE; else { r[i]=r[j]; i=j; j=2*i;// 继续筛选 } } r[i]=t;// r[k]填入到恰当的位置 }// sift如何利用堆完成排序?
1 将待排序记录按照堆的定义建立堆,并输出堆顶元素;
2 调整剩余的记录序列,利用筛选法将前n-i个元素重新筛选建成为一个新堆,在输出堆顶元素;
3 重复之前以上步骤,进行筛选,新筛选成的堆会越来越小,而新堆后面的有序关键词会越来越多,最后使待排序记录序列成为一个有序的序列,这个过程称之为堆排序。
sift重建堆1->n-1
void sift(RecordType r[],int k,int m) // 假设r[k……m]是以r[k]为根的完全二叉树,且分别以r[2k]和r[2k+1]为根的左右子树为大根堆,调整r[k],使整个序列r[k……m满足堆的性质 { t=r[k];//暂存根记录r[k] x=r[k].key;i=k;j=2*i; finished=FALSE; while(j<=m} void sift(RecordType r[],int k,int m) // 假设 { j=j+1; // 若存在右子树且根的关键字大则沿右分支筛选 if(x>=r[j].key) finished=TRUE;// 筛选完毕 else {r[i] = r[j];i=j; j=2*i;}// 继续筛选}调用一趟求出最小值,选几个最小值就掉用几趟,而其他排序方法都要全部排好才能达到要求
归并排序
采用分治法,最终将有序的子序列合并,即先使每个子序列有序,再使子序列段间有序,最终合并,具体思想是:假设初始序列含有n个记录,首先将这n个记录看成是n个有序的子序列,每个子序列的长度为1,然后两两归并,得到一个长度为2(n为奇数时,最后一个序列的长度为1)的有序子序列。
在此基础上再对长度为2的有序子序列进行两两规并,得到若干个长度为4的有序子序列,如此重复直到得到一个长度为n的有序序列为止,这种方法称作2-路归并排序。
先将整个待排序序列平均分成两份,分别对左右两个小区间按照分治算法处理,直到小区间内数据个数小于等于1,最终合并两个有序区间。它适合于外部排序,时间复杂度是O(n*logn),当然,当数据元素比较少的话,可以采用插入排序进行优化
void Merge(RecordType r1[],int low,int mid,int high,RecordType r[]) { i=low;j=mid+1;k=low; while((i<=mid)&& (j<=high))// 类似于合并两个有序表 { if (r1[i].key<=r1[j].key) {r[k]=r1[j];++j;} else {r[k]=r1[j];++j;} ++k; } while(i<=mid) {r[k]=r1[i];k++,i++;} while(j<=high); {r[k]=r1[j];k++;j++;} }// Merge归并算法
分配类排序
多关键字排序
链式基数排序
有关数据结构定义(采用静态链表)
最低位先排,最低位的值对记录进行初步排序,在此基础上再按次低位的值进行进一步排序。依次来推,由低位到高位,根据关键字的某一位对所有记录进行排序,支持最高位。
数组next域连接下一个单元,形成n个待排序链表on
d关键字位数分趟进行分配收拢
i是比较进行趟数红色分配和收集
分配需要基数个队列r待排序序列
队列收拢,首尾相接
对比小结
简单排序一般只用于n比较小的情况。
当序列中记录“基本有序”时,直接插入排序是最佳的排序方法,常与快速排序归并排序的其他排序方法结合使用。
快速排序,堆排序和归并排序平均时间复杂度为O(nlog2n)
就平均时间性能而言,快速排序是所有排序方法当中最好的。
快速排序在最坏的情况下的时间性能为O(n2)
堆排序和归并排序的最坏时间复杂仍为O(nlog2n),当n较大时,归并排序的时间性能优于堆排序,但他需要的辅助空间较大。
基数排序是稳定的,除了简单选择排序,其他几种前的排序方法也是稳定的。
快速排序,堆排序,希尔排序等时间性能较好的排序方法,以及简单选择排序都是不稳定的。
可以将简单排序法与性能较好的排序方法结合使用,比如在快速排序当中,划分子区间的长度小于某值时,可以调用直接插入排序法;或者先将待排序的序列划分成若干子序列,分别进行直接插入排序。然后再利用归并排序法将有序子序列合并成一个完整的有序序列。
基数排序的时间复杂度可以写成O(d*n)因此,它适用于n值很大而关键字的位数d较小的序列。
类别排序bestnormalworst辅助空间稳定?比较次数交换次数插入插入O(n)O(n2)O(n2)O(1)稳n-1,n(n-1)/20,n(n-1)/2插入希尔O(n)O(n1.3)O(n2)O(1)不稳??选择选择O(n2)O(n2)O(n2)O(1)不稳n(n-1)/20,n-1选择堆O(nlog2n)O(nlog2n)O(nlog2n)O(1)不稳nlog2n?交换冒泡O(n)O(n2)O(n2)O(1)稳n-1,n(n-1)/20,n(n-1)/2交换快速O(nlog2n)O(nlog2n)O(n2)O(nlog2n)不稳nlog2n,n(n-1)/2? 归并O(nlog2n)O(nlog2n)O(nlog2n)O(n)稳nlog2n/2? 基数O(d(rd+n))O(d(n+r))O(d(n+r))O(rd+n)稳??
注:基数排序中d代表长度,r代表关键字基数,n代表关键字个数
快速排序是在冒泡的基础上改进,原来冒泡是相邻两个两两交换,而快速是两个指针从前到后和从后到前,交换之后改变逆转数的个数
堆是在选择类,比较次数多,保留下来