数据结构-排序

目录

1.冒泡排序

1.1.复杂度分析

2.简单选择排序

2.1.复杂度分析

3.直接插入排序

​ 3.1.复杂度分析

4.希尔排序

5.堆排序

6.1.复杂度分析

6.归并排序

 6.1.复杂度分析

7.快速排序

 7.1.复杂度分析

8.总结

8.1.稳定性

8.2.内部排序分类


 

这里选给出排序记录的数据类型定义,本例用的是线性表

//顺序表结构
#define MAX 10
typedef struct{
	int r[MAX+1];
	int length;
}SqList;

1.冒泡排序

冒泡排序是一种交换排序,它的基本思想是:两两比较相邻记录的关键字,如果反序则交换,直到没有反序的记录为止,这里给出的是优化后的冒泡排序,加了一个标志变量,用来提前退出已经有序的冒泡排序。

代码实现:

void BubbleSort(SqList &L){
	int i,j;
	bool flag=true;//为true的时候说明有交换需要继续排序,为false则说明已经有序,不需要再排序
	for(i=1;i<L.length&&flag;i++){
		flag=false;//每次排序都将标志变量改为false
		for(j=L.length-1;j>=i;j--){
			if(L.r[j]>L.r[j+1]){//每次都与前一个数比较,只要比前一个数小就交换位置
				swap(L.r[j],L.r[j+1]);
				flag=true;//只要有一次交换,这个数组就不一定是有序的,就修改标志变量
			}
		}
	}
}

来看一组排序的过程,这里只给出i=1时的比较流程

 可能这里看不出来flag的好处,让我们看一个特殊的数组

这个数组,当我们比较一次也就是i=1的时候,比较完数组已经是有序的了,我们不需要再比较,当i=2时,没有发生任何元素的交换,这样flag=false,排序就退出啦! 

1.1.复杂度分析

时间复杂度:最好的情况下就是数组是正序的,这样只用比较n-1次,且无交换时间复杂度是O(N),最坏的情况下就是数组是逆序的,第i个元素要和前面i-1个元素比较,故比较i-1次,故一共是\sum i-1,i从1到n-1,求和就是n(n-1)/2次,并做等数量级的移动,总时间复杂度是O(N*N)

空间复杂度:因为没有借助额外的空间,故空间复杂度是O(1)

2.简单选择排序

简单选择排序是通过n-i次关键字间的比较,从n-i+1个记录中选出关键字最小的记录,并和 i 个记录交换之。

代码实现:

void SelectSort(SqList &L){
	int i,j,min;
	for(i=1;i<L.length;i++){//循环L.length-1次 
		min=i;//每次都将开始比较的位置元素当做最小值 
		for(j=i+1;j<=L.length;j++){//和剩下从i+1到L.length的元素比较,找到最小 
			if(L.r[j]<L.r[min]){//只要比目前min下标对应的元素小,就将min改为最小值对应的下标 
				min=j;
			}	
		}
		if(min!=i){//当min!=i的时候说明原来第i个位置不是从i到L.length的最小值,就交换位置 
			swap(L.r[min],L.r[i]);
		}
	}
}

来一组模拟

其实很好理解,就是每次选出一个元素和其余后面的所有元素比较,找到这些元素里面最小的,因为最后一个元素其后无元素要比较了,所以比较n-1次就行,每个元素,加入是第i个元素,其后有n-i个元素要进行比较,加上第i个,一共n-i+1个,从中选出最小值。 

2.1.复杂度分析

时间复杂度:选择排序无论最好最坏情况下,比较的次数都相同,每各元素都要和后面的元素比较第i个元素后面有n-i个元素,故每趟比较n-i次,n-1趟排序比较需要\sum i-1次,i从1到n-1,因为第n个元素不需要比较了,选择排序的好处就是交换次数很少,因为每趟比较结束才会移动,故n-1趟比较移动次数就是n-1,因此,总时间复杂度是O(N*N)

空间复杂度:因为没有借助额外的空间,故空间复杂度是O(1)

3.直接插入排序

直接插入排序的基本操作是将一个记录插入到已经排好序的有序表中,从而得到一个新的,记录数增1的有序表。

代码实现:

void InsertSort(SqList &L){
	int i,j;
	for(i=2;i<=L.length;i++){//因为每次是跟前一个元素比较,i从2开始 
		if(L.r[i]<L.r[i-1]){//只要后面的比它前一个元素小,我们就要将后面的插到前面去 
			L.r[0]=L.r[i];//设置哨兵,保存L.r[i]的值,因为后面在移动的时候会将该数覆盖 
			for(j=i-1;L.r[j]>L.r[0];j--){//直到在前面找到一个大于该数的元素就停止循环和移动 
				L.r[j+1]=L.r[j];
			}
			L.r[j+1]=L.r[0];//循环退出时位置j的元素小于哨兵,所以加工哨兵放在j后面 
		}
	}
} 

 来一组模拟

 3.1.复杂度分析

时间复杂度:最好的情况下,就是数组是正序的,这样,每个元素只会跟前一个元素比较,故比较n-1次,因为第一个元素不需要比较,移动的次数是0,故时间复杂度是O(N)。最坏的情况下,数组是逆序的,这样每个元素要跟前面的一个元素比较1次,前面i-1个元素每次要跟哨兵比较,故比较i-1次,一共就是i次,\sum i,i从2到n,求和就是(n+2)(n-1)/2次,因为该元素要移动到哨兵再将前i-1个元素后移,该元素再从哨兵出插到第一个位置,故第i个元素移动的记录是i-1+2=i+1,故移动的最大值是\sum i+1,i从2到n,求和就是(n+4)(n-1)/2次。

空间复杂度:因为没有借助额外的空间,故空间复杂度是O(1)

4.希尔排序

希尔排序又称为“缩小增量排序”,是插入排序的一种。对于直接插入排序,它的效率在某些情况下是很高的,比如,我们的记录本身就是基本有序的,这样就只需要少量的插入操作,就可以完成整个记录的排序工作,此时直接插入很高效。但是并不是所有的数据都是基本有序的,所以,希尔排序来啦!!希尔排序其实就是先将记录变成基本有序,再用一次直接插入排序使整体有序的算法。

采取跳跃分割的策略:将相距某个“增量”的记录组成一个子序列,这样才能保证在子序列内分别进行直接插入排序后得到的结果是基本有序而不是局部有序。

代码实现:

void ShellSort(SqList &L){
	int i,j;
	int increment=L.length;//初始化增量为数组元素个数 
	do{
		increment=increment/3+1;//设置增量 
		for(i=increment+1;i<=L.length;i++){1~increment为一组,increment~n为一组,比较两组对应位置的值(1对应increment+1,2对应increment+2..)
			if(L.r[i-increment]>L.r[i]){//如果后面的一组值小于前面的,就说明要插入了 
				L.r[0]=L.r[i];//先将待插入的数设置为哨兵 
				for(j=i-increment;j>0&&L.r[0]<L.r[j];j-=increment){//用循环来判断是为了看小的数插入之后是否在这个数之前还有比插入的数更小的,有则继续交换
					L.r[j+increment]=L.r[j];//将大的数移到小数原来所在的位置,就是后移 
				}
				L.r[j+increment]=L.r[0];//将小的数插入到前面,为什么是j+increment,因为退出循环的时候j-increment,但是因为这个位置不满足循环条件所以退出的,因此插入位置是上一个循环的位置 
			}
		}
	}while(increment>1);//当增量小于等于一的时候退出 
} 

每次第i个数都是跟 i-increment 位置的元素比较,因为i初始化是 increment+1,这样,其实 i-increment 就是从1-2-3-...increment ,i 就是从increment+1 - increment+2 - ...L.length,所以很明显,比较的元素之间是差increment这个增量的。

来一组模拟

这里我只模拟了increment=4的时候,接下来increment=2,步骤是一样的。 

5.堆排序

堆排序是一种树形选择结构,在排序的过程中,将待排序列的记录r[1..n]看成一个完全二叉树的顺序存储结构,利用完全二叉树和双亲结点和孩子结点的内在关系,在当前无序的序列中选择关键字最大(或最小)的记录。

堆排序的步骤如下:

1. 按堆的定义将待排序列调整为大根堆(这个过程称为建初堆),交换r[1]和r[n],则r[n]为关键字最大的记录

2.将r[1..n-1]重新调整为堆,交换r[1]和r[n-1],则r[n-1]为关键字次大的记录

3.循环n-1次,直到交换了r[1]和r[2]为止,得到了一个非递减的有序序列r[1..n]

堆排序要完成两个过程:建初堆&调整堆

建初堆:要将一个无序序列调整为堆,就必须将其所对应的二叉树以每个结点为根的子树都调整为堆。显然,只有一个结点的树必是堆,而在完全二叉树中,所有序号大于\left \lfloor n/2 \right \rfloor。这样,只需利用筛选法,从最后一个分支结点\left \lfloor n/2 \right \rfloor开始,依次将序号为\left \lfloor n/2 \right \rfloor\left \lfloor n/2 \right \rfloor-1、...、1的结点作为根的子树调整为堆即可

调整堆:筛选法调整堆:

从r[2s]和r[2s+1]中选出关键字较大者,假设r[2s]的关键字较大,比较r[s]和r[2s]的关键字

1.若L.r[s]>=L.r[2s],说明以r[s]为根的子树已经是堆,不必做任何调整

2.若L.r[s]<L.r[2s],交换L.r[s]和L.r[2s],交换后,以r[2s+1]为根的子树仍是堆,如果以r[2s]为根的子树不是堆,则重复上述过程,将以r[2s]为根的子树调整为堆,直至进行到叶子结点为止

代码实现

void HeapAdjust(SqList &L,int s,int m){//调整堆
	L.r[0]=L.r[s];//将根结点的值保存在L.r[0]处
	for(int i=2*s;i<m;i*=2){//将以s为根的子树以及以s的叶子结点为根的子树调整为大根堆
		if(i<m&&L.r[i]<L.r[i+1]) i++;//找到叶子结点里较大的节点,用i记录下标
		if(L.r[0]>=L.r[i]) break;//如果根结点比叶子结点大,就不用调整了
		L.r[s]=L.r[i]; s=i;//将叶子结点的值赋值给它的根结点,s=i表示想要将根结点的值放在它的叶子结点处,但若以它的叶子结点根结点为子树中有比s结点更大的,就需要继续交换,所以这里,只交换下标
	} 
	L.r[s]=L.r[0];//将根结点的值赋值给最后找到的替换的位置下标为s的结点
} 
void HeapSort(SqList &L){
	for(int i=L.length/2;i>0;i--){//初建堆
		HeapAdjust(L,i,L.length);//从非终端结点开始,到根节点1
	}
	for(int i=L.length;i>1;i--){//堆排序
		swap(L.r[1],L.r[i]);//每次将根结点与当前待排序的最后一个节点交换
		HeapAdjust(L,1,i-1);//将根结点到待排序的除最后一个结点之外的堆调整为大根堆
	}
} 

已知无序序列为{19,38,65,9,76,13,27,49},来模拟初建堆和调整堆的过程

首先,从第一个非终端结点出发,即n/2=4,L.r[4]=97>L.r[4*2]=49 ,故以97 为根的子树已经是大根堆,不需要调整,然后调整n/2-1=3为根的子树,首先找到左右孩子的最大值为L.r[2*3+1]=27,然后比较根结点和孩子结点最大值,L.r[3]=65>L.r[3*2+1]=27,所以以65位根的子树已经是大根堆,也不需要调整,然后调整n/2-2=2为根的子树,首先找到左右孩子的最大值为L.r[2*2]=97,然后比较根结点和孩子结点最大值,L.r[2]=38<L.r[2*2]=97,所以需要调整,执行L.r[i]=97赋值给L.r[s]=38,将根结点下标改为较大的孩子结点下标,即s=i=4,由for循环判断可知i*2=8<n,所以进入for循环继续调整,再找到以s为根节点的左右孩子最大值,注意,现在的s结点下标是4了哦,故现在的位置就是97,只有一个左孩子,然后比较根结点和左孩子谁大,左孩子跟97比较??当然不是,97移走了,应该跟打算放在这个位置的结点比较,即进行堆调整的根节点,所以我们每次调整之前,都会将根结点保存为哨兵,每次跟哨兵比较就好了。L.r[0]=38<L.r[2*s]=49,故要将L.r[i]=L.r[8]赋值给L.r[s]=L.r[4],s=i=8,i*2=16>m退出循环,然后将哨兵处的值赋值给最后得到的s下标的位置L.r[s]=L.r[8]=L.r[0]=38.

 然后调整n/2-3=1为根的子树,将根结点保存为哨兵L.r[0]=L.r[s],首先找到左右孩子的最大值为L.r[2*1]=97,然后比较根结点和孩子结点最大值,L.r[1]=49<L.r[2*1]=97,所以需要调整,执行L.r[i]=97赋值给L.r[s]=38,将根结点下标改为较大的孩子结点下标,即s=i=2,由for循环知i*=2故i=4,继续循环,找到以s=2为根的左右孩子的最大值为L.r[2*2+1]=L.[5]=76,然后比较哨兵跟孩子结点较大值,L.r[0]=49<L.e[5]=76,所以需要调整,执行L.r[i]=L.r[2*2+1]=76赋值给L.r[s]=L.r[2]=76,s=i=2*2+1,i*=2=10>m,故退出循环。

 

此时,根结点也已经调整完毕,就得到了一个大根堆。

然后开始堆排序和调整堆

每次将根结点和当前待调整的堆的最后一个结点 i 交换,然后将 1到 i-1 调整为大根堆,调整堆其实跟初建堆是一样的,但是因为除了我们交换的第一个结点,其余结点均满足堆性质,所以我们只需要自上至下进行一条路径上的结点的调整即可

将97和38交换,然后将除97以外的结点调整为大根堆

再将76与27交换,然后将除97、76以外的结点调整为大根堆

再将65与13交换,然后将除97、76、65以外的结点调整为大根堆

然后一直像这样交换和调整,循环n-1次我们的数组就是非递减的有序序列了。 

6.1.复杂度分析

时间复杂度:设有n个记录的的初始序列对应的完全二叉树的深度为h,建初堆时,每个非终端结点都要自上而下进行筛选,由于第 i 层上的结点数小于等于2^{i-1},且第 i 层结点最大下移深度为h-i,没下移一层要做两次比较,所以建初堆时关键字总的比较次数为\sum_{i=h-1}^{1}2^{i-1}*2(h-1)=\sum_{i=h-1}^{1}2^{i}*(h-1)=\sum_{j=1}^{h-1}2^{h-j}*j\leqslant 2*n\sum_{j=1}^{h-1}j/2^{j}\leqslant 4n

调整堆新建时要做n-1次“筛选”,每次“筛选”都要将根结点下移到合适的位置。n个结点的完全二叉树的深度为\left \lfloor log_{2}n \right \rfloor+1,则重新建堆时关键字总的比较次数不超过2n\left \lfloor log_{2}n \right \rfloor+1

由此,堆排序在最坏的情况下其时间复杂度也为O(nlong2n)

 空间复杂度:仅需要一个记录 大小供交换用的存储空间没所以空间复杂度为O(1)

6.归并排序

归并排序就是将两个或两个以上的有序表合并成一个有序表的过程。将两个有序表合并成一个有序表的过程称为2-路归并。

归并排序算法的思想是:假设初始序列有n个记录,则可看成是n个有序子序列,每个子序列的长度为1,然后两两归并,得到\left \lceil n/2 \right \rceil个长度为2或者1的有序子序列,再两两归并,...,如此重复,直至得到一个长度为n的有序序列为止。

代码实现

void merge(SqList &L,int l,int r){
	if(l>=r) return;
	int mid=(l+r)>>1;//将当前序列一分为二 
	merge(L,l,mid);//对子序列L.r[l..mid]递归,进行归并排序 
	merge(L,mid+1,r);//对子序列L.r[mid+1..r]递归,进行归并排序 
	int i=l,j=mid+1,k=0;
	while(i<=mid&&j<=r){//合并两个子序列到tmp 
		if(L.r[i]<=L.r[j]) tmp[k++]=L.r[i++];
		else tmp[k++]=L.r[j++];
	}
	while(i<=mid) tmp[k++]=L.r[i++];//如果子序列中还有没合并到tmp中的,就直接复制到tmp 
	while(j<=r) tmp[k++]=L.r[j++];//如果子序列中还有没合并到tmp中的,就直接复制到tmp
	for(int i=l,j=0;i<=r;j++,i++){//将合并后的子序列再复制到原来的数组中去,这样,子序列L.r[l..r]就是合并完有序的 
		L.r[i]=tmp[j];
	}
} 

来一组模拟,已知待排序的关键字序列为{49,38,65,97,76,13,27}

首先利用递归将待排序列分为子序列,一直到每个子序列的个数都为1

 

 然后开始归并,就是递归开始回溯了,我们开始将子序列一点点合并回去

这样归并结束我们就的到了一个非递减的有序序列,注意归并的时候,是从1 到 2 ,从2 到4 ... 每次都是合并两个子序列,合并到了一个临时数组里,合并的时候,因为两个子序列已经是有序的,所以用两个指针i和j分别指向两个子序列的最小值处,然后每次将小的那个数合并到临时数组,并将指针后移,最后再讲未合并完的子序列复制到临时数组里。

下面这张图就是归并的整体过程:

 

 6.1.复杂度分析

时间复杂度:当有n个记录时,要进行\left \lfloor log2n \right \rfloor趟归并排序,每一趟归并,其关键字比较次数不超过n,元素移动次数都是n,因此,归并排序的时间复杂度为O(nlong2n)

空间复杂度:因为需要一个和记录个数相等的辅助数组,所以空间复杂度为O(n)

7.快速排序

快速排序是由冒泡排序改进而得的。在冒泡排序中,只对相邻的两个记录进行比较,因此每次交换记录时只能消除一个逆序对。如果能通过两个(不相邻)记录的一次交换,消除对个逆序对,则会大大加快排序的速度。快速排序方法中的一次交换可能消除多个逆序

快速排序的算法思想:在记录中任取一个数作为枢轴,将比枢轴小的数移到枢轴的左侧,将比枢轴大的数移到枢轴的右侧,这样之后,枢轴左侧的的数一定全部小于右边,然后我们再分别对左边和右边的数用相同的方法进行排序,直到不能再分,我们就得到了一个非递减的有序记录。

代码实现:

void quickSort(SqList &L,int l,int r){
	if(l>=r) return;
	int mid=(l+r)>>1;//取中间的记录作为枢轴
	int i=l-1,j=r+1;//因为下面用的do-while结构,所以刚开始,两个指针指向记录两侧
	while(i<j){//两个指针没有重合也没有越过枢轴就继续判断
		do i++; while(L.r[i]<L.r[mid]);//移动左指针找到比枢轴大的数
		do j--; while(L.r[j]>L.r[mid]);//移动右指针找不枢轴小的数
		if(i<j) swap(L.r[i],L.r[j]);//如果找到的数在分别在枢轴左侧和右侧,就交换两个记录
	}
	quickSort(L,l,j);//再对左侧排序
	quickSort(L,j+1,r);//再对右侧排序
}

已知待排序记录的关键字序列为{22,47,66,32,76,59,15},给出快速排序的过程

 

这里只给出了一趟排序,下一趟排序就是分别 对 j 左侧包括 j 进行排序,对 j 右侧进行排序

 7.1.复杂度分析

时间复杂度:快速排序每次将待排序数组分为两个部分,在理想状况下,每一次都将待排序数组划分成等长两个部分,则需要logn次划分。
而在最坏情况下,即数组已经有序或大致有序的情况下,每次划分只能减少一个元素,快速排序将不幸退化为冒泡排序,所以快速排序时间复杂度下界为O(nlogn),最坏情况为O(n^2)。在实际应用中,快速排序的平均时间复杂度为O(nlogn)

空间复杂度:因为快速排序是递归的,执行时需要一个栈来存放相应的数据,最大递归调次数与递归树的深度一致,所以最好情况下空间复杂度为O(long2n),最坏情况下为O(N)。

8.总结

8.1.稳定性

稳定排序:插入排序,冒泡排序,选择排序,归并排序

不稳定排序:希尔排序,快速排序,堆排序

8.2.内部排序分类

  • 插入类:将无序子序列中的一个或几个记录“插入”到有序序列中,从而增加记录的有序序列的长度。主要包括直接插入排序,折半插入排序和希尔排序
  • 交换类:通过“交换”无序序列中的记录从而得到其中关键字最小或最大的记录,并将它加入到有序子序列中,以此方法增加记录的有序子序列长度。主要包括冒泡排序和快速排序
  • 选择类:从记录的无序子序列中“选择”关键字最小或最大的记录,并将它加入到有序子序列中,以此方法增加记录的有序子序列长度。主要包括简单选择排序,树形选择和堆排序
  • 归并类:同过“归并”两个或两个以上的记录有序子序列,逐步增加记录有序序列的长度。2-路归并排序是最为常见的归并排序方法。
  • 2
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值