数据结构---排序

算法复杂度

算法种类最好平均最坏空间复杂度是否稳定
直接插入排序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&&a[0]<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);
}
  • 堆排序
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+1,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( r)

    稳定性:稳定排序

外排序

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

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

  • 首先根据内存缓冲区的大小,将外存上含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的虚段。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值