数据结构---排序

算法复杂度

算法种类 最好 平均 最坏 空间复杂度 是否稳定
直接插入排序 O(n) O(N2) O(N2) O(1)
冒泡排序 O(n) O(N2) O(N2) O(1)
简单选择排序 O(N2) O(N2) O(N2) O(1)
希尔排序 O(n1.3) O(1)
快速排序 O(nlog2n) O(nlog2n) O(N2) O(log2n)
堆排序 O(nlog2n) O(nlog2n) O(nlog2n) O(1)
2-路归并排序 O(nlog2n) O(nlog2n) O(nlog2n) O(n)
基数排序 O(d(n+r)) O(d(n+r)) O(d(n+r)) O( r )

相关概念

  • 排序
    将数据元素的一个任意序列,重新排列成一个按关键字有序的序列。

  • 若按照记录的主关键字排序,则排序结果唯一。
    若按照记录的次关键字排序,则排序结果可以不唯一。

  • 设 Ki = Kj(1≤i≤n, 1≤j≤n, i≠j ),且在排序前的序列中 R i领先于Rj(即 i < j )。若在排序后的序列中 Ri 仍领先于 Rj,则 称所用的排序方法是稳定的;反之,则称所用的排序方法是不稳定的。

  • 若整个排序过程不需要访问外存便能完成,则称此类排序问题为内部排序,内部排序的过程是一个逐步扩大记录的有序序列的过程;反之,若参加排序的记录数量很大,整个序列的排序过程不可能在内存中完成,则称此类排序问题为外部排序。

  • 若待排序记录的关键字顺序正好和要排序顺序相同,称此表中记录为正序;反之,若待排序记录的关键字顺序正好和要排序顺序相反,称为反序。基于比较的排序算法中有些算法是与初始序列的正序或反序相关,有些算法与初始序列的正序和反序无关

插入排序

将无序子序列中的一个或几个记录“插入”到有序序列中。

直接插入排序

  • 对序列A中的元素A[1]-A[n],令i从2到n枚举,进行n-1趟操作,每趟从有序范围中寻找位置j,将待排序列中的第一个元素插入j位置,j位置之后的有序序列后移一位。
void insertSort(int a[n+1]){
	for(int i=2;i<=n;i++){
		a[0]=a[i];
		j=i;
		while(j>1&&temp<a[j-1]){
			a[j]=a[j-1];
			j--;
		}
		a[j]=[0];
	}
}
//
void insertSort(int a[n+1]){//对n个元素的排序
	for(int i=2;i<=n;i++){
		if(a[i]<a[i-1]){
			a[0]=a[i];//哨兵
		}
		for(j=i-1;a[0]<a[j];j--){
			a[j+1]=a[j];
		}
		a[j+1]=a[0];
	}
}
  • 算法评价
    时间复杂度:
    最好情况(正序) :O(N)
    最坏情况(逆序) :O(N2)
    平均情况:O(N2)

    空间复杂度:O(1)

    稳定性:稳定排序

折半插入排序

  • 将比较和移动操作分离
void InsertSort(int a[n+1]){//对n个元素排序
	int i,j,liw,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]){
				heigh=mid-1;
			}else{
				low=mid+1;
			}
		}
		for(j=i-1;j>high+1;j--){
			a[j+1]=a[j];
		}
		a[hight+1]=a[0];
	}
}
  • 算法评价
    时间复杂度:
    最好情况(正序) :
    最坏情况(逆序) :
    平均情况:O(n2)(元素的移动次数没有改变)

    空间复杂度:

    稳定性:稳定排序

希尔排序(缩小增量排序)

  • 先将排序表分割成若干形如i,i+d,i+2d…的特殊子表,分别进行直接插入排序,当整个表中的元素呈基本有序时,再对全体记录进行一次直接插入排序。
void ShellSort(int a[n+1]){//对n个元素排序
	//前后记录位置的增量是dk不是1
	//a[0]只是暂存单元,不是哨兵,当j<=0时,插入位置已到
	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];
			}
		}
	}
}
  • 时间复杂度:
    最好情况(正序) :
    最坏情况(逆序) :O(N2)
    平均情况:O(N1.3)

    空间复杂度:O(1)

    稳定性:稳定排序

交换排序

通过“交换”无序序列中的记录从而得到其中关键字最小或最大的记录,并将它加入到有序子序列中.

冒泡排序

  • 本质在于交换,每次通过交换的方式把当前剩余元素的最大值移动到一端,当剩余元素减少为0时排序结束。结果是将元素按关键字从小到大排列。
  • 每一趟排序的结果是将待排序列中最小的元素放到最终位置。
void BubbleSort(int a[n]){//对n个元素的排序
	for(int i=0;i<n-1;i++){//整个过程执行n-1趟
		for(int j=n-1;j>i;j--){
			if(a[j]<a[j-1]){
				swap(a[j],a[j+1]);
			}
		}
	}
} 
//优化
void BubbleSort(int a[n]){
	for(int i=0;i<n-1;i++){
		flag=false;
		for(int j=n-1;j>i;j--){
			if(a[j]<a[j-1]){
				swap(a[j],a[j+1]);
				flag=true;//本趟遍历后没有发生交换,说明表以有序
			}
		}
		if(flag==false)return;
	}
} 

  • 算法评价
    时间复杂度:
    最好情况(正序) :O(n)
    最坏情况(逆序) :O(n2) 。
    平均情况:O(n2)

    空间复杂度:O(1)

    稳定性:稳定排序

快速排序

  • 任选一个记录,以它的关键字作为“枢轴”,凡关键字小于枢轴的记录均移至枢轴之前,凡关键字大于枢轴的记录 均移至枢轴之后。

  • 并不产生有序子序列,但每趟排序会将一个元素放到其最终位置上。

int Partition(int a[],int left,int right){
	int temp=a[left];
	while(left<right){
		while(left<right&&a[right]>temp)right--;
		a[left]=a[right];
		while(left<right&&a[right]<=temp)left--;
		a[right]=a[left];
	}
	a[right]=temp;
	return left;
}
void quickSort(int a[],int left,int right){
	if(left<right){
		int pos=Partition(a,left,right);
		quickSort(a,left,pos-1);
		quickSort(a,pos+1,right);
	}
}
  • 算法评价
    若待排记录的初始状态为按关键字有序时,快速排序将蜕化为起泡排序,其时间复杂度为 O(n2)。 所以快速排序适用于原始记录排列杂乱无章的情况。

    时间复杂度:
    最好情况:O(nlog2n)
    最坏情况:O(n2)
    平均情况:O(nlog2n)

    空间复杂度:O(log2n)

    稳定性:不稳定排序

  • 随机快速排序
    在a[left,right]中随机选取一个主元,生成一个在[left,right]内的随机数p,然后以a[p]为主元来划分。

    具体做法:
    将a[p]与a[left]交换,其他同上。

srand((unsigned)time(NULL));
int p=(round(1.0*rand()/RAND_MAX*(right-left)+left);
swap(a[p],a[left]);

选择排序

从记录的无序子序列中“选择” 关键字最小或最大的记录,并将它加入到有序子序列中。

简单选择排序

  • 对序列A中的元素A[1]-A[n],i从1到n枚举,进行n趟操作,每趟从待排序列中选择最小的元素,令其与待排部分的第一个元素进行交换,形成新的有序区间。
void SelsetSort(int a[n+1]){//对n个元素的排序
	for(int i=1;i<=n;i++){//n趟
		min=i;
		for(int j=i;j<=n;j++){
			if(a[j]<a[min]){
				min=j;
			}
		}
		if(min!=i)swap(a[i],a[min]);
	}
}
  • 算法评价
    时间复杂度:
    最好情况:O(n2)
    最坏情况:O(n2)
    平均情况:O(n2)

    空间复杂度:O(1)

    稳定性:不稳定排序

23215->13225(不稳定)->12325->12235

堆排序

  • 用数组来存储完全二叉树。
  • 堆排序是一种树形选择排序方法,在排序过程中,将L[1,n]视为一棵完全二叉树,利用二叉树中双亲节点和孩子节点之间的内在关系,在当前无序区中选择关键字最大或最小的元素。
  • 建堆
//调整
void adjustDown (int low,int hight){
	int i=low,j=i*2;
	while(j<=height){
		if(j+1<=height&&heap[j+1]>heap[j]){
			j=j+1;
		}
		if(heap[j]>heap[i]){
			swap(heap[j],heap[i]);
			i=j;
			j=i*2;
		}else{
			break;
		}
	}
}
//建堆
void createHeap(){
	for(int i=n/2;i>=1;i++){
		downAdjust(i,n);
	}
}
  • 删除堆顶元素
void deleteTop()[
	heap[1]=heap[n--];
	downAdjust(1,n);
}
  • 添加元素
void upAdjust(int low,int height){
	int i=height,j=i/2;
	while(j>=low){
		if(heap[j]<heap[i]){
			swap(heap[j],heap[i]);
			i=j;
			j=i/2;
		}else{
			break;
		}
	}
}
void insert(int x){
	heap[++n]=x;
	upAdjust(1,n);234r
}
  • 堆排序
void heapSort(){
	creatHeap();
	for(int i=n;i>1;i--){
		swap(heap[i],heap[1]);
		downAdjust(1,i-1);
	}
}
  • 算法评价
    时间复杂度:
    最好情况:O(nlog2n)
    最坏情况:O(nlog2n)
    平均情况:O(nlog2n)

    空间复杂度:O(1)

    稳定性:不稳定排序

归并排序

通过“归并”两个或两个以上的记录有序子序列。

//递归实现
void merge(int a[n+1],int l1,int r1,int l2,int r2){
	int i=l1,j=l2;
	int temp[n+1],index=0;
	while(i<r1&&j<r2){
		if(a[i]<=a[j]{
			temp[index++]=a[i++];
		}else{
			temp[index++]=a[j++];
		}
	}
	while(i<=r1)temp[index++]=a[i++];
	while(j<=r2)temp[index++]=a[j++];
	for(i=0;i<index;i++){
		a[l1+1]=temp[i];
	}
}
void mergeSort(int a[n+1],int left,int right){
	while(left<right){
		int mid=(left+rigth)/2;
		mergeSort(a,left,mid);
		mergeSort(a,mid,right);
		merge(a,left,mid,mid,right);
	}
}
//非递归实现
void mergeSort(int a[n+1]){
	//step为组内元素个数,step/2为左子区间元素个数,等号可不取
	for(int step=2;step/2<=n;step*=2){
		//每step个元素为一组,组内前step/2和后step/2个元素进行合并
		for(int i=1;i<=n;i+=step){
			int mid=i+step/2-1;
			if(mid+1<=n){
				merge(a,i,mid,mid+1,min(i+step-1,n);
			}
		}
	}
}
//一趟归并
void mergeSort(int a[n+1]){
	//step为组内元素个数,step/2为左子区间元素个数,等号可不取
	for(int step=2;step/2<=n;step*=2){
		//每step个元素为一组,组内前step/2和后step/2个元素进行合并
		for(int i=1;i<=n;i+=step){
			merge(a,i,mid,mid+1,min(i+step-1,n);//可以用sort代替
		}
	}
}
  • 算法评价
    时间复杂度:
    最好情况:O(nlog2n)
    最坏情况:O(nlog2n)
    平均情况:O(nlog2n) ,每一趟归并的时间复杂度为 O(n),总共需进行log2n(向上取整)趟。

    空间复杂度:O(n)

    稳定性:稳定排序

基数排序

  • 基数排序是一种基于多关键字排序的思想,采用多关键字的排序思想(即基于关键字各位的大小进行排序),借助分配和收集两种单逻辑关键字进行排序。

  • 分为高位优先排序MSD和低位优先排序LSD。

  • 以r为基数的最低位优先基数排序的过程:假设线性表由结点排序a0,a1,…,an-1构成,每个节点aj的关键字由d元组(kjd-1,kjd-2…kj1,kj0)组成,其中0≤kji≤r-1(0≤j<n,0≤i≤d-1)。在排序过程中,使用r个队列Q0,Q1…Qr-1

    对i=0,1…d-1,依次做一次分配和收集。
    分配:开始时,把Q0,Q1…Qr-1各个队列置成空队列,然后依次考察线性表中的每个节点aj(j=0,1…n-1),若aj的关键字kji=k,就把aj放进Qk中。

    收集:把Q0,Q1…Qr-1各个队列中的节点依次首尾相接,得到新的节点序列从而组成新的线性表。

  • 算法评价
    时间复杂度:O(d(n+r))
    平均情况:O(nlog2n) ,每一趟归并的时间复杂度为 O(n),总共需进行log2n(向上取整)趟。

    空间复杂度:O®

    稳定性:稳定排序

外排序

  • 指排序文件较大,内存一次放不下,需要存放在外部介质的文件的排序。排序时把数据一部分一部分的调入内存进行排序。在排序过程中需要多次进行内存和外存之间的交换,对外存文件中的记录进行排序后的结果仍然被放到外存文件中。

  • 在外排序过程中的时间代价主要考虑访问磁盘的次数。

  • 首先根据内存缓冲区的大小,将外存上含n个记录的文件分成若干长度为h的子文件,依次读入内存并利用有效的内部排序方法对他们进行排序,然后将排序后得到的有序的子文件重新写回外存,通常称这些有序的子文件为归并段或顺串。然后对这些归并段进行逐趟归并,使归并段逐渐有小到大,直至得到整个有序文件为止。

    外部排序的总时间=内部排序所需的时间+外存信息读写的时间+内部归并随需的时间。

多路平衡归并与败者树

  • 败者树是树形选择排序的一种变体,可视为一棵完全二叉树,每个叶节点存放各归并段在归并过程中当前参加比较的记录,内部节点用来记忆左右子树中的失败者,而让胜者继续往上进行比较,一直到根节点。

  • 使用败者树之后,内部归并的比较次数与归并路数m无关,因此,只要内存空间允许,增大m将有效减少归并树的高度,从而减少IO次数d,提高速度。

  • 归并路数m并不是越大越好,m增大时,相应的需要增加输入缓冲区的个数。若可供使用的内存空间不变,势必要减少每个输入缓冲区的容量,使得内存,外存交换数据的次数增大,当m过大时,虽然归并趟数会减少,但读写外存的次数任然会增加。

  • 对k个有序段进行k路平衡归并的方法如下:
    1.取每个输入有序段的第一个记录作为败者树的叶子结点,建立初始败者树:两两叶子结点进行比较,在双亲结点中记录比赛的败者(关键字较大者),而让胜者去参加更高一层的比赛,如此在根结点之上胜出的“冠军”是关键字最小者。

    2.胜出的记录写至输出归并段,在对应的叶子结点处,补充其输入有序段的下一个记录,若该有序段变空,则补充一个大关键字(比所有记录关键字都大,设为kmax)的虚记录。

    3.调整败者树,选择新的关键字最小的记录:从补充记录的叶子结点向上和双亲结点的关键字比较,败者留在该双亲结点,胜者继续向上,直至树的根结点,最后将胜者放在根结点的双亲结点中。

    4.若胜出的记录关键字等于kmax,则归并结束;否则转2继续。

置换选择排序(生成初始归并段)

  • 减少初始归并段的个数也可以减少归并趟数。但若采用前面介绍的内部排序的方法,将得到长度都相同的初始归并段。

  • 设初始待排文件为FI,初始归并段文件为FO,内存工作区为WA,内存工作区可容纳w个记录。

    1.从待排文件FI中输入w个记录。

    2.从内存工作区WA中选出关键字最小的记录MINMAX。以后再选出关键字比他大的记录归入本归并段,比他小的归入下一归并段。

    3.将MINMAX记录输出到FO中。

    4.若FI不空,则从FI中读入下一个记录x放在WA中。

    5.在WA中所有关键字比MINMAX记录的关键字大的记录中选出最小的关键字记录,作为新的MINMAX。

    6.重复3-5,直到在WA中选不出新的MINMAX记录为止。由此得到一个初始归并段,输出一个归并段的结束标志到FO中。

    7.重复2-6,直到WA为空。

最佳归并树

  • 由于采用置换-选择排序的方法生成的初始归并段长度不等,在进行逐趟k路归并时对归并段的组合不同,会导致归并过程中对外存的读/写次数不同。为提高归并的时间效率,有必要对各归并段进行合理的搭配组合。按照最佳归并树的设计可以使归并过程中对外存的读/写次数最少。

  • m路归并排序可用一颗m叉树描述,因为每次做m路归并都需要有m个归并段参加,因此,归并树是一颗只有度为0和度为m的节点的严格m叉树。

  • 各个叶节点表示参加归并的一个初始归并段,叶节点上的权值表示该归并段中的记录数,根节点表示最终生成的归并段,叶节点到根节点的路径长度表示在归并过程中的归并趟数,各非叶节点代表归并成的新归并段。

  • 归并方案不同,所得的归并树不同,树的带权路径程度也不同,可以将哈夫曼树的思想推广到m叉树。让记录数少的初始归并段最先归并,记录数多的初始归并段最晚归并,就可以建立总共的IO次数最少的最佳归并树。

  • 若初始归并段不足以构成一棵严格m叉树,需要添加长度为0的虚段。

发布了30 篇原创文章 · 获赞 1 · 访问量 381

猜你喜欢

转载自blog.csdn.net/weixin_46265246/article/details/105248950
今日推荐