【数据结构】各种排序算法&各种排序算法的比较

在这里插入图片描述

冒泡排序(Bubble Sort):
对于一串数字,如3 2 5 9 6 4 1
从3的位置开始往后进行 如果被比较数比3小,那么那个数就“浮上去”,即与3进行交换,此时变成 2 3 5 9 6 4 1
再从第二个位置开始,即3和5比,顺序正常
第三个位置,5和9比,顺序正常
第四个位置,9和6比,6比9小,所以6“浮上去”,此时变成2 3 5 6 9 4 1
第五个位置,9和4比,4比9小,所以4“浮上去”,此时变成2 3 5 6 4 9 1
第六个位置,9和1比,变成2 3 5 6 4 1 9
这样结束了第一轮冒泡,能确定的是,数组最后一位已经被调成最大的数了
第二轮冒泡,继续从头开始遍历,但是这次到“1”的位置就可以了,因为最后一位已经确定,可以不用管它。至少可以确定,冒泡排序至多需要进行N轮,即数字总数
接下来的数字变化是:
2 3 5 4 6 1 9
2 3 5 4 1 6 9
第三轮:
2 3 4 5 1 6 9
2 3 4 1 5 6 9
第四轮:
2 3 1 4 5 6 9
第五轮:
2 1 3 4 5 6 9
第六轮:
1 2 3 4 5 6 9
第七轮:
1 2 3 4 5 6 9
如果将数字顺序变更为: 2 3 5 6 9 4 1 10 11 12,那么冒泡排序理论上需要进行10轮,但实际上到第七轮的时候顺序就已经是正确顺序了,后面三轮循环没有必要,因此可以直接退出循环,这就是冒泡排序的剪枝
时间复杂度:
最好情况,全是顺序,至少要全部遍历一遍。O(N)
最坏情况,全是倒序,(n-1) + (n-2) + … + 1 = n*(n-1) / 2 = O(N²)

void Bubble_Sort(int *A,int N){
	for(int i = N-1; i >= 0; i--){
		int flag = 1;//剪枝 
		for(int j = 0; j < i; j++){
			if(A[j] > A[j+1]){
				int temp = A[j];
				A[j] = A[j+1];
				A[j+1] = temp;
				flag = 0;
			}
		}
		if(flag)	break;//全程无交换BREAK; 
	}
} 

动画演示:
在这里插入图片描述
插入排序
用抽扑克牌举例子
先抽到一张J
再抽到一张K,K比J大,所以按顺序放在J的后面
又抽到一张A,A比K大,按顺序放在K的后面
抽到一张Q,Q比A、K都要小,所以放在K的前面,Q比J大,所以在J的后面
↑这句话的操作应该是这样的:Q比A小,所以A把自己的位置挪出来,自己向右移动一个单位,Q放在A的前面(也就是腾出来的这个空位置);Q比K小,K也把位置挪出来,自己向右移动,Q放在K的位置上;Q比J大,不需要挪动了,结束插入。
最好情况:抽到的牌都是顺序的,那至少还需要遍历一次,因此是O(N)
最坏情况:抽到的牌是倒序,为O(N²)

void Insertion_Sort(int *A,int N){
	int i,j; 
	for(i = 1; i < N; i++){
		int temp = A[i];//取出数据(抽牌 
		for(j = i; j > 0 && A[j-1] > temp; j--){
			A[j] = A[j-1];//向右移出空位 
		}
		A[j] = temp;//到达指定位置,放入数据 
	}
}

动画演示:
在这里插入图片描述
时间复杂度下界:
在这里插入图片描述
在这里插入图片描述

希尔排序
在这里插入图片描述
举例:81 94 11 96 12 35 17 95 28 58 41 75 15
先取出以5为间隔(中间隔了4个数字,下标差为5)的所有数:
第一组:81_ _ _ _ 35 _ _ _ _ 41 _ _ 插入排序 → 35_ _ _ _ 41 _ _ _ _ 81 _ _
第二组:94 17 75 → 35 17 _ _ _ 41 75 _ _ _ 81 94 _
第三组:11 95 15 →35 17 11 _ _ 41 75 15 _ _ 81 94 95
第四组:96 28 → 35 17 11 28 _ 41 75 15 96 _ 81 94 95
第五组: 12 58 → 35 17 11 28 12 41 75 15 96 58 81 94 95
以3间隔、1间隔的不多赘述

大致思想:给定一个乱序数组,每次选定一个间隔数X,从每X个取出数字并进行插入排序,结果子数组放回原来的位置上
再选定一个比X小的间隔数,做同样的动作,到最后间隔数选择1,进行插入排序,此时的序列逆序对已经非常少了,所以用插入排序会非常快捷
一般地,最初希尔增量序列X为size/2,随后的为X/=2,直到1,组成一个增量序列,这样的话它的时间复杂度为Θ(N²),这表示的是最坏情况,Θ表示上界=下界,恒定为N²的时间复杂度,下面举个例子:
这表示了前三次使用间隔数进行排序都是白费功夫,完全没必要做在这里插入图片描述
因此最初希尔增量序列就不怎么起作用了,因此有人提出了更多的增量序列
在这里插入图片描述
代码实现:(普通希尔增量)

void Shell_Sort(int *A,int N){
	int D,P,i,temp;
	for(D = N/2; D > 0; D/=2){//希尔增量序列 一直到1为止 
		for(P = D; P < N; P++){//插入排序 
			temp = A[P];
			for(i = P; i >= D && A[i-D] > temp;i-=D){
				A[i] = A[i-D];//向有跨度的右边移出空位 
			}
			A[i] = temp;//到达指定位置,放入数据 
		} 
	}
}

选择排序
算法思路:从N组数据中找到最小的数,放到首项,再从除了首项的N-1组数据中找到最小的,放到第二位,以此类推。
代码实现:

void Selection_Sort(int *A,int N){
	for(int i = 0; i < N; i++){
		int MinPosition = FindMin(A,i,N);
		int temp = A[i];
		A[i] = A[MinPosition];
		A[MinPosition] = temp;
	}
}
int FindMin(int *A,int i,int N){
	int min = INT_MAX,pos = i;
	for(i; i < N; i++){
		if(A[i] <= min){
			min = A[i];
			pos = i;
		}
	}
	return pos;
}

对于FindMin(),每次执行时都需要O(N)复杂度进行查找,但是我们有更快的方法——用最小堆只需O(logN)

堆排序
堆排序就是优化后的插入排序,在插入排序中,每次都是寻找一个最小值放到数组前位,如果是使用堆的话,找到最小值放到数组前面这样对堆的结构会有一个比较大的影响,因此最好选择把最大值放到数组末尾,这样对于1~N-i 这样的堆依然能够较好的进行操作。
因此对于一个需要结果为递增数组的堆排序,选择使用最大堆的DeleteMax

思路以及疏忽点:
1.堆排序中的数组下标是从0开始,因此儿子节点的下标是2n+1和2n+2
2.结果为递增数组,选择最大堆;递减数组选择最小堆
3.基本思路(递增数组,size = N):将A[0→N-1]构建最大堆,并将A[0]放到A[N-1]处,再将A[0→N-2]构建最大堆,将A[0]放到A[N-2]处,一直到N-i=0即完成堆排序
参考博客:

图解排序算法-堆排序 by dreamcatcher-cx

伪代码:

void Heap_Sort(int *A,int N){
	for(int i = N/2 - 1; i >= 0; i--)//初始是10,父节点在4,可以画个二叉树就知道了(别忘了是从0开始 
		adjustHeap(A,i,N);
	for(int i = N-1; i > 0 ; i--){
		int temp = A[i];
		A[i] = A[0];
		A[0] = temp;
		adjustHeap(A,0,i);
	} 
}

这里再简要讲一下最大堆的调整:
(最小堆同理)最大堆的定义是左右子树依然是最大堆,因此将原堆细分为多个小的最大堆组成最大堆
在这里插入图片描述
这里就是先从43-9调节,再是72-30-49,再是79-55-66,然后一层一层回溯进行堆的调整,这样组成最大堆,具体可以回顾我的博文树-堆,以及网上参考 最大堆by凌云壮志几多愁
结合上述代码,完整的堆排序代码如下:

void Heap_Sort(int *A,int N){
	for(int i = N/2 - 1; i >= 0; i--)//初始是10,父节点在4,可以画个二叉树就知道了(别忘了是从0开始 
		adjustHeap(A,i,N);
	for(int i = N-1; i > 0 ; i--){
		int temp = A[i];
		A[i] = A[0];
		A[0] = temp;
		adjustHeap(A,0,i);
	} 
}
void adjustHeap(int *A,int pos,int N){//调整最大堆,从最后开始的每一个小堆(即非叶节点调整 
	int temp = A[pos];
	for(int i = pos*2+1; i < N; i=i*2+1){//从小的堆一步一步向上调整,并一直往下寻找比它更大的 
		if(i+1 < N && A[i] < A[i+1]){//右子节点更大就选右子节点 
			i++;
		}
		if(A[i] > temp){//够大,放到父节点 
			A[pos] = A[i];
			pos = i;//pos的位置变为子节点的位置 
		}else{
			break;
		}
	} 
	A[pos] = temp;//最后pos的位置是更新后的新子节点位置(若有) 
}

在这里插入图片描述
归并排序
归并排序的核心就是有序数列的归并(合并),就类似最开始学链表的时候,要用两个有序链表合并成一个新的有序链表,这其中的思想的是一样的
在这里插入图片描述
Aptr比Bptr小,Aptr放入,A++,C++,然后再比较,再放入以此类推。
如果一个到头了,那么把另一个数组直接嚯的一声丢进去就好了——这是“并”的精髓

接下来就是要根据“并”来做下面的操作,把一串数组进行NNN次分割,出来2个非常非常小的数组进行合并。
当然最小情况就是2个数组,每个数组里面仅有1个元素(就1个元素肯定是有序的),给你一个4和3,怎么排你懂的吧.jpg,然后这一组的2个并完了,接下来就要并隔壁另外2个元素,这样我们就有了2个数组,这两个数组都是有序的,长度为2个数组,如:1,13;2,15.再像上面一样的方法把它并进去就是一个长度为4且有序的新数组了。
一层一层由“小”的“2组有序数组”,变成“大”的“2组有序数组”,最后合并在一起,这就是归并排序。
对归并排序的具体详解我之前也有写过博客-归并排序 欢迎阅读
代码实现:

void Merge(int *A,int *TempA,int L,int R,int Rightend){//合并精华部分 
	int Leftend = R-1;//1号数组结束的位置 
	int elenum = Rightend - L + 1;//此次数字总数(因为要从TempA中放回去 
	int tempos = L;//归并的起始位置(临时存放TempA的位置,这个位置需与A严格对齐 
	int i;
	while(L<=Leftend && R<=Rightend){
		if(A[L] < A[R]){
			TempA[tempos++] = A[L++];
		}else
			TempA[tempos++] = A[R++];
	} 
	while(L<=Leftend){
		TempA[tempos++] = A[L++];
	}
	while(R<=Rightend){
		TempA[tempos++] = A[R++];
	}
	for(i = 0; i<elenum; i++,Rightend--){
		A[Rightend] = TempA[Rightend];
	}
}
void Msort(int *A,int *TempA,int L,int R){//分而治之精华部分 
	int center = (L+R)/2;
	if(L < R){
		Msort(A,TempA,L,center);//向左分割 
		Msort(A,TempA,center+1,R);
		Merge(A,TempA,L,center+1,R);//数组1的起点为L,数组2的起点为center+1。极端情况L=center就是最左边2个数 开始归并 
	}
}
void Merge_Sort(int *A,int N){
	int *TempA = new int[N];//先声明tempA,防止递归的时候重复声明数组导致浪费 
	Msort(A,TempA,0,N-1);//传进去的是数组最左和最右的下标 
}

表排序(大部分用作一种辅助工具)
待排元素不是单纯的整数,而是一个结构体
算法概述
·间接排序
定义一个指针数组作为“表”(table)
在这里插入图片描述
table数组里面最初标记的是A的下标
后来经过比较只改变table所指的位置
在这里插入图片描述
如table[0] = 3 就表示最小的是在A[3]处
因此如果仅要求按顺序输出,则输出:A[ table[0] ], A[ table[1] ], ……, A[ table[N-1] ]

如有需要,再将其进行物理排序
原理:N个数字的排列由若干个独立的环组成
在这里插入图片描述
如:table[0]的值是3,table[3]是1,table[1]是5,table[5]是0,回到table[0]组成一个环

这样就可以仅在每一个环内进行物理上的排序了,不需要换一个就要进行N次循环
用temp记录初值 ,每次换位子修改table值,用if ( table[i] == i )判断一个环的结束
然后其中的交换方式用插入排序也好其它的也好,都行

复杂度
  最好情况:初始即有序
  最坏情况:
  有[N / 2]向下取整 个环,每个环包含2个元素
  需要[N / 2]向下取整 次元素移动
  T = O( m N ) ,m 是每个A元素的复制时间。

基数排序
桶排序
桶排序:假设我们有N 个学生,他们的成绩是0到100之间的整数(于是有M = 101 个不同的成绩值)。如何在线性时间内将学生按成绩排序?
在这里插入图片描述

事实上就是开一个101的数组,数组下标作为一个指标
T(N, M) = O( M+N )
当M>>N时,桶排序不合算,需要用到基数排序

假设我们有N = 10 个整数,每个整数的值在0到999之间(于是有M = 1000 个不同的值)。

输入序列: 64, 8, 216, 512, 27, 729, 0, 1, 343, 125
用“次位优先”(Least Significant Digit)
在这里插入图片描述
模拟(对于最多是三位数的数字来讲):
1.开一个0~9的数组,然后将所有的数字,以它们的个位数为基准放入第一轮的桶中,pass 1中存储的就是一个链表,可能不够明显,见pass2
2.再开一个0~9的数组,再将所有的数字以它们的十位数为基准放入第二轮的桶中,如果前面没有那就是0.这样对于pass2,里面保存的的链表就是0→1→8→NULL
3.再开一个记录百位数,这样就保存完毕了。输出就从pass[i],然后将里面的链表悉数输出即可。
设元素个数为N,整数进制为B,LSD的趟数为P,则最坏时间复杂度是:T=O(P(N+B))

多关键字的排序
一副扑克牌是按2种关键字排序的
在这里插入图片描述
如果按照花色排序,将卡片全都按花色放置后,还需要在其内部进行面值的排序,再并起来这样是一个完整的有顺序的手牌。这样实际上也就快了那么一点点。

在这里插入图片描述
用次位优先的方法,直接按照数字大小进行保存(也就是开13个桶),再从小到大,不需要排序,将这些牌直接按照花色放到新的“花色桶”中,再合并,这样也能得到一个完整的有顺序的手牌。速度相比主谓优先要快不少。

快速排序
是在现实生活中应用最广泛的排序算法
核心:分而治之、找主元(pivot)
在这里插入图片描述
算法思想:随便找一个主元65,然后将数组分成2组:比65小的,比65大的
然后对于左边和右边再用同样的方法递归实现:找一个主元,分成比这个主元小的一组和大的一组以此类推下去,最后结束就形成了一个有序数组。
具体描述:
1.选主元
取主元不能太小,也不能太大,最好是取中间的那个数,否则给一个序列1、2、3、4、5、6、7、8,每次都取第一个的话,时间复杂度是O(N²)太慢了。不同的pivot取法对运行速度影响各不相同。
一般地,我们主元可以取数组中头、中、尾的中位数。
顺便取完中位数后,最好对它们的位置进行一个交换,方便接下来的操作:比中位数小的在中位数的左边区域,比它大的在其右边区域
代码实现:

int GetMidNum(int *A,int left,int right){
	int mid = (left+right)/2;
	if(A[left] > A[mid]){
		Swap(A[left],A[mid]);
	}
	if(A[left] > A[right]){
		Swap(A[left],A[right]);
	}
	//保证了左边的是最小的 
	if(A[mid] > A[right]){
		Swap(A[mid],A[right]);
	}
	//顺序安排结束,left<=mid<=right
	
	Swap(A[mid],A[right-1]);
	//接下来的步骤是组成 小 mid 大 这样的数组
	//把mid放到right-1的位置
	//因为right已经肯定比mid要大了,省安排一个元素 
	return A[right-1];
	
}
void Swap(int &a,int &b){
	int temp = b;
	b = a;
	a = temp;
}

2.子集划分
准备工作完毕后,可以开始进行子集划分了,最终目标是实现 小 pivot 大 的数组序列
在这里插入图片描述
给定一串数组,这里的特征是最后一位是之前选取的pivot(因此这时候的传参是A,left(=0),right(N-1)
从left和right-1位置开始设置一个i和j。
在这里插入图片描述
1.i=left ,a[i] = 8 > 6 讲道理i这边出现的应该都是比6要小的值,因此 i不动;
2.转向j, j = right -1.a[right] = 7 > 6正确,j向左移动
在这里插入图片描述
3.j = right - 2,a[j] = 2 < 6,出问题了,因此此时将a[i]和a[j]的数据进行对调,然后重新从i出发

在这里插入图片描述
4.i = left + 1 ,a[i] = 1 < 6 √;i++
在这里插入图片描述
5. i = left + 2, a[i] = 4 < 6 √ i++
在这里插入图片描述
6. i = left + 3, a[i] = 9 > 6 ×,转到j
在这里插入图片描述
7. j–, a[j] = 5 < 6 ×,此时j也是×,因此数据对调,再从i开始
在这里插入图片描述
8.a[i] = 0 < 6 √ ;–> i++
在这里插入图片描述
9. a[i] = 3 < 6√ ;–> i++
在这里插入图片描述
10.a[i] = 9 > 6 ×,转向j
在这里插入图片描述
11.j–,a[j] = 3 < 6 ×,此时i > j 那么将a[i]与a[right]换位,即完成划分。
在这里插入图片描述
这样的特点是,划分完元素之后,6所处的位置就是它在排序后的正确位置。

如果有元素正好等于pivot怎么办:
  停下来交换:会产生很多不必要的交换。(一串相等的序列)
  不理它,继续移动指针:pivot位置靠近一段,最糟糕会在最右边端,和pivot取A[0]一样,O( N^2 )
  综合考虑,选第一种,停下来交换。
小规模数据的处理
  快速排序的问题
    用递归,内存消耗大
    对小规模的数据(例如N不到100)可能还不如插入排序快
  解决方案
   当递归的数据规模充分小,则停止递归,直接调用简单排序(例如插入排序)
   在程序中定义一个Cutoff的阈值

代码实现:

void Quick_Sort(int *A,int N);
void QSort(int *A,int left,int right);
int GetMidNum(int *A,int left,int right);
void Swap(int &a,int &b);
void Quick_Sort(int *A,int N){
	QSort(A,0,N-1); 
}
void QSort(int *A,int left,int right){
//	if(cutoff < len) 
	if(left >= right)	return;
	int pivot = GetMidNum(A,left,right);
	int i = left,j = right - 1; //right - 1 : pivot的位置 
	while(i<j){//出口就是i<j,while(1)稀烂 
		while(A[++i] < pivot){}//一直移动i
		while(A[--j] > pivot){}//i出问题了移动j 
		if(i<j){
			Swap(A[i],A[j]);
		}
	}
	Swap(A[i],A[right-1]);//pivot放到i处,形成小 pivot 大 
	QSort(A,left,i-1);//pivot在i的位置上,所以不能碰i 
	QSort(A,i+1,right); 
	//数据量太小:
	//if(cutoff > right-left) Insertion_Sort(A+left,right-left+1);
}
int GetMidNum(int *A,int left,int right){
	int mid = (left+right)/2;
	if(A[left] > A[mid]){
		Swap(A[left],A[mid]);
	}
	if(A[left] > A[right]){
		Swap(A[left],A[right]);
	}
	//保证了左边的是最小的 
	if(A[mid] > A[right]){
		Swap(A[mid],A[right]);
	}
	//顺序安排结束,left<=mid<=right
	
	Swap(A[mid],A[right-1]);
	//接下来的步骤是组成 小 mid 大 这样的数组
	//把mid放到right-1的位置
	//因为right已经肯定比mid要大了,省安排一个元素 
	return A[right-1];
	
}
void Swap(int &a,int &b){
	int temp = b;
	b = a;
	a = temp;
}

各种排序算法的比较
在这里插入图片描述
一些小注解:希尔排序中的d,最坏情况d=2,好情况1<d<2
堆排序看上去很美,但实际上它的O(NlogN)里的O常数比快排和归并都要大,因此相比这几个NlogN的要慢一些
快排的O常数是这几个中最小的,因此实际应用也最广

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值