常见排序算法集合(数据结构)

排序算法的衡量指标

1.时间复杂度:算法运行时间随数据量变大的增长趋势

2.空间复杂度:算法执行过程中需要占用的额外存储空间

3.稳定性:时间复杂度和空间复杂度是否恒定

ps:时间复杂度与空间复杂度均用大O符号,即O(1)、O(n)等表示


排序算法的分类

1.从存储设备分:内排序外排序 (内排序是指在排序过程中,待排序的所有记录都被存储在内存中。外排序指由于排序的记录个数太多,不能同时放置在内存中,整个排序过程需要在内外存之间多次交换数据)

2.按对关键字的操作:基于关键字比较的排序分布排序


常见排序算法

插入排序

插入排序是指将一个记录插入到已经排好序的有序表中,从而得到一个新的、记录数+1的有序表

插入排序步骤

1.将待排序序列中的第一个元素当做初始时已经排好序的有序表

2.取待排序序列中的下一个元素记为item)作为待插入元素,并用该元素比较有序表中的元素

3.若有序表中的某个元素大于item,则将该元素向后移动一位;若有序表中某一元素小于等于item,则将item插入到该元素后

4.重复步骤1->3,直到待排序序列中的所有元素都被插入到有序表中

插入排序的时间复杂度 :若待排序序列本身有序 ,则时间复杂度为O(n) ;若待排序序列为逆序 ,则时间复杂度为O(n^2)

插入排序代码实现如下(C++):

cpp 复制代码
//插入排序代码实现
#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;

//排序函数 
void insertionSort(vector<int>& nums, int n){
	
	int tmp;//暂存无序序列中的元素 
	int j;//在循环外定义,方便内层循环结束后保存位置信息 
	
	//外层循环,枚举无序序列的起始位置
	for(int i=1;i<=n;i++){
		tmp=nums[i+1];//取出无序序列中的第一个元素暂存到tmp 
		
		//内层循环,从有序序列末尾向前找插入位置,同时移动元素 
		for(j=i;j>=1;j--){
			//若有序序列的元素比tmp大,就将该元素向后挪一位 
			if(nums[j]>tmp){
				nums[j+1]=nums[j];
			}
			//找到有序序列中第一个小于等于tmp的元素,就停止查找 
			else{
				break;
			}
		}
		nums[j+1]=tmp;//将该元素插入到有序序列中的正确位置 
	} 	
} 

int main(){
	//数据输入 
	int n;
	cin>>n;
	
	vector<int> nums(n+1);
	for(int i=1;i<=n;i++){
		cin>>nums[i];
	}
	
	insertionSort(nums, n);
	
	//数据输出处理
	cout<<"排序结果如下:"<<endl;
	for(int i=1;i<=n;i++){
		cout<<nums[i]<<" ";
	} 
	cout<<endl;
	
	return 0;
}
/*
10
12 11 13 5 6 7 9 3 8 10
排序结果如下:
3 5 6 7 8 9 10 11 12 13
*/

希尔排序

希尔排序 (也称Shell排序)是插入排序的优化版本,实现思路是通过分组插入排序降低待排序序列的无序度,最后对整体执行一次普通插入排序。

希尔排序步骤

1.定义一个gap (初始 gap=n/2,n为序列中元素的个数),之后每次gap = gap /2,直到gap = 1为止

2.从头遍历待排序序列,对每个下标间隔gap的元素执行插入排序

3.重复步骤1和步骤2直到gap = 1

4.对分组插入排序后的序列进行一次插入排序

希尔排序的时间复杂度 :平均时间复杂度为O(N^1.3) ,若待排序序列为逆序 ,则时间复杂度为O(n^2)

希尔排序代码实现如下(C++):

cpp 复制代码
//希尔排序代码实现 
#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;

void shellSort(vector<int> &nums){
	int cnt = 0;//记录排序趟数 
	int n = nums.size()-1;
	
	//对待排序序列进行分组,步长逐步折半(gap表示将待排序序列分为多少组) 
	for(int gap = n/2;gap >= 1;gap = gap/2){
		cnt++;//趟数+1
		
		//遍历每个分组,对每个分组进行插入排序
		for(int i = gap+1; i<=n; i++){
			int tmp=nums[i];//取出一个分组中的第一个元素,并保存该元素
			int j;//提前定义,便于后续保存位置 
			
			//向前遍历同组元素,找到插入位置 
			for(j = i-gap;j>0;j-=gap){
				if(nums[j] > tmp){
					nums[j + gap] = nums[j];//将比tmp大的元素后移 
				}
				//找到插入位置,退出循环(同一分组内该位置前的元素都比tmp小,该位置后的元素都比tmp大) 
				else{
					break;
				} 
			}
			nums[j+gap]=tmp;//插入元素到正确位置 		
		} 
		cout<<"第"<<cnt<<"趟排序的结果:"<<endl;
		for(int i=1;i<=n;i++){
			cout<<nums[i]<<" ";
		}
		cout<<endl;
	}
}

int main(){
	//数据输入
	int n;
	cin>>n;
	
	vector<int> nums(n+1);
	for(int i=1;i<=n;i++) {
		cin>>nums[i];
	}
	
	shellSort(nums);
	
	//数据输出
	cout<<"排序结果如下:"<<endl;
	for(int i=1;i<=n;i++){
		cout<<nums[i]<<" ";
	}
	cout<<endl;
	
	return 0;
}
/*
10
12 11 13 5 6 7 9 3 8 10
第1趟排序的结果:
7 9 3 5 6 12 11 13 8 10
第2趟排序的结果:
3 5 6 9 7 10 8 12 11 13
第3趟排序的结果:
3 5 6 7 8 9 10 11 12 13
排序结果如下:
3 5 6 7 8 9 10 11 12 13
*/

选择排序

顾名思义,就是每次从待排序序列中取出一个最小值,然后放在有序表中,直到排序完成

选择排序的时间复杂度 :与待排序序列本身是否有序无关,时间复杂度恒为O(n^2)

选择排序代码实现如下(C++):

cpp 复制代码
//选择排序代码实现
#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;

void selectionSort(vector<int> &nums){
	int n = nums.size()-1;
	int min_index;//保存最小的数据的下标 
	
	//从待排序序列中找到最小的数据,放到序列的第一个位置 
	for(int i=1;i<n;i++) {
		min_index=i;//最小的数据的下标
		int tmp;//暂时保存数据 
		
		for(int j=i+1;j<=n;j++){
			//若当前数据小于nums[min_index],则更新min_index为当前下标 
			if(nums[j]<nums[min_index]){
				min_index=j;
			}
		}
		
		//把这一趟遍历的数据放在待排序序列的第一个位置 
		tmp=nums[i];
		nums[i]=nums[min_index];
		nums[min_index]=tmp;
	}
}

int main(){
	//数据输入
	int n;
	cin>>n;
	
	vector<int> nums(n+1);
	for(int i=1;i<=n;i++) {
		cin>>nums[i];
	}
	
	selectionSort(nums);
	
	//数据输出
	cout<<"排序结果如下:"<<endl;
	for(int i=1;i<=n;i++){
		cout<<nums[i]<<" ";
	}
	cout<<endl;
	
	return 0;
}
/*
10
12 11 13 5 6 7 9 3 8 10
排序结果如下:
3 5 6 7 8 9 10 11 12 13
*/

堆排序

堆排序是一种基于堆的排序算法,是选择排序的优化版本

:一棵完全二叉树,分为小顶堆和大顶堆两种类型

小顶堆 :每个父节点的值都小于等于其左右子节点的值,堆顶(根节点)是整个堆的最小值

大顶堆 :每个父节点的值都大于等于其左右子节点的值,堆顶(根节点)是整个堆的最大值

堆化 :将一棵无序的完全二叉树调整为堆结构的过程(以大顶堆为例,待排序序列转换成完全二叉树后,从该树的最下一层的最右边节点开始,向它的父亲节点遍历找到第一个不是叶子节点的节点(记为x),若x大于其左右子树,则继续向前遍历;若x小于它的任一左右子树,则从子树中选择最大的一个与节点x进行交换,然后向前遍历)

堆排序步骤(升序排序):

1.将待排序序列视为一棵完全二叉树(类似于层次遍历),从最后一个非叶子节点开始从下往上对每个节点进行堆化操作,直到得到一个大顶堆

2.交换堆顶元素(最大值)和堆的最后一个元素,将最大值放到有序表末尾,然后对新的堆顶元素进行堆化操作,使其成为大顶堆

3.重复步骤1和步骤2,直到堆的大小缩小为1,排序完成

堆排序的时间复杂度与待排序序列本身是否有序无关,时间复杂度恒为O(n logn) ,堆化操作时间复杂度为O(log n)

堆排序代码实现如下(C++):

cpp 复制代码
//堆排序代码实现 
#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;

/*
调整以i为根节点的子树为大顶堆(每个父节点的值都大于等于其左右子节点的值的完全二叉树) 
nums:待排序数组
n:堆的大小
i:当前节点的索引
*/
void heapify(vector<int> &nums,int n,int i){
	int max_data = i;//初始化最大值为根节点
	int left = 2*i+1;
	int right = 2*i+2;
	
	//如果左子节点大于根节点
	if(left < n && nums[left] > nums[max_data]){
		max_data = left;
	} 
	
	//如果右子节点大于根节点
	if(right < n && nums[right] > nums[max_data]){
		max_data = right;
	} 
	
	//如果最大值不是根节点,则交换并递归调整
	if(max_data != i){
		swap(nums[i], nums[max_data]);
		heapify(nums, n, max_data);
	} 	
} 

/*
排序函数(升序)
nums:待排序数组 
*/
void heapSort(vector<int> &nums){
	int n = nums.size();
	
	//构建大顶堆
	for(int i = n / 2 - 1;i >= 0;i--){
		heapify(nums,n,i);
	}
	
	//遍历大顶堆,将堆顶的最大值移动到末尾
	for(int i = n-1;i>0;i--){
		swap(nums[0],nums[i]);
		
		heapify(nums,i,0);
	} 
}

int main(){
	// 测试数据
    //数据输入处理 
	int n;//数组大小
	cin>>n;
	
	vector<int> nums(n+1);
	for(int i=1;i<=n;i++){
		cin>>nums[i];
	}
	
	heapSort(nums);
	
	cout<<"排序结果如下:"<<endl;
	for(int i=1;i<=n;i++){
		cout<<nums[i]<<" ";
	} 
	cout<<endl;
	
	return 0;
}
/*
10
12 11 13 5 6 7 9 3 8 10
排序结果如下:
3 5 6 7 8 9 10 11 12 13
*/

冒泡排序

从待排序序列的第一个元素开始,依次比较相邻的两个元素,若前者大于 后者,交换两者位置 ;若前者小于等于 后者,两元素位置保持不变

代码实现方式:两层循环

冒泡排序的时间复杂度 :若待排序序列本身有序 ,则时间复杂度为O(n) ;若待排序序列为逆序 ,则时间复杂度为O(n^2)

冒泡排序代码实现如下(C++):

cpp 复制代码
//冒泡排序代码实现
#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;

void bubbleSort(vector<int> &nums){
	int n = nums.size()-1;
	
	int tmp;//临时保存数据
	bool flag=0;//剪枝操作(若序列本身有序,则flag==false,提前退出循环) 
	
	for(int i=1;i<=n-1;i++){
		flag=0;//对每个元素进行比较前先恢复flag为原状态 
		for(int j=1;j<=n-i;j++){
			if(nums[j]>nums[j+1]){
				flag=true;//数组乱序,更新flag表示对其进行操作 
				
				//交换相邻的两个数据 
				tmp=nums[j];
				nums[j]=nums[j+1];
				nums[j+1]=tmp;
			}
		}
		
		//序列本身有序,直接退出循环 
		if(flag==false){
			break;
		}
	}
}

int main(){
	//数据输入
	int n;
	cin>>n;
	
	vector<int> nums(n+1);
	for(int i=1;i<=n;i++) {
		cin>>nums[i];
	}
	
	bubbleSort(nums);
	
	//数据输出
	cout<<"排序结果如下:"<<endl;
	for(int i=1;i<=n;i++){
		cout<<nums[i]<<" ";
	}
	cout<<endl;
	
	return 0;
}
/*
10
12 11 13 5 6 7 9 3 8 10
排序结果如下:
3 5 6 7 8 9 10 11 12 13
*/

快速排序

快速排序 是一种基于分治思想 的排序算法,实现思路是选择一个基准元素,将待排序序列划分为两部分,其左边 元素小于等于基准元素,右边 元素大于等于 基准元素,然后递归对左右子数组重复该过程

快速排序步骤

1.选择基准元素(常见选择策略有:选择第一个元素、选择最后一个元素、选择中间元素)

2.遍历待排序序列,将小于等于基准元素的元素放在基准元素左边,将大于等于基准元素的元素放在基准元素右边

3.递归,对基准元素左边的子序列和右边的子序列重复步骤1和步骤2直到子序列长度为1

快速排序的时间复杂度 :最好情况:O(nlogn) ;最坏情况:O(n^2) ;平均情况:O(nlogn)

快速排序代码实现如下(C++):(注:由于快速排序实现方法多样,此处仅选择挖坑法实现)

cpp 复制代码
//快速排序(挖坑法)代码实现
#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;

void Qsort(vector<int> &nums, int left, int right){
	int key;//基准元素 
	
	if(left<right){//待排序数组中至少有两个数据
		int i=left;
		int j=right;
		key=nums[left];//以最左边元素为基准元素(此时nums[left]的值被保存到key中,nums[left]的位置就变成了一个坑) 
	 
		while(i<j){
			//从后往前找比key小的数(填左坑) 
			while(i<j && nums[j]>key){
				j--;
			}
			if(i<j){
				nums[i]=nums[j];//将nums[j]填到左坑中,nums[j]的位置变成了坑 
				i++;
			}
			
			//从前往后找比key大的数(填右坑)
			while(i<j && nums[i]<key){
				i++;
			} 
			if(i<j){
				nums[j]=nums[i];//将nums[i]填到右坑中,nums[i]的位置又变成了新的坑 
				j--;
			}
		}
		
		//循环结束后i==j,即左右指针指向同一位置
		nums[i]=key;//将基准元素填到最后一个坑内
		Qsort(nums,left,i-1);//对基准元素左边的子数组进行递归排序
		Qsort(nums,i+1,right);//对基准元素右边的子数组进行递归排序 
	}
}

int main(){
	//数据输入处理 
	int n;//数组大小
	cin>>n;
	
	vector<int> nums(n+1);
	for(int i=1;i<=n;i++){
		cin>>nums[i];
	}
	
	Qsort(nums,1,n);
	
	//数据输出处理
	cout<<"排序结果如下:"<<endl;
	for(int i=1;i<=n;i++){
		cout<<nums[i]<<" ";
	} 
	cout<<endl;
	
	
	return 0;
} 
/*
10
12 11 13 5 6 7 9 3 8 10
排序结果如下:
3 5 6 7 8 9 10 11 12 13
*/

归并(合并)排序

归并排序 ,也叫合并排序(下面统一叫归并排序)。归并排序是一种基于分治思想 的排序算法,实现思路是先将待排序序列递归差分为最小单元(单个元素),再将有序的子数组逐层合并, 最终得到完整的有序数组

归并排序步骤

1.拆分:将待排序序列从中间拆分为左右两个子数组,然后递归拆分左右子数组,直到每个子数组仅含1个元素

2.合并:从最小的有序子序列开始,将两个有序子数组合并为一个新的有序序列,然后逐层向上合并,最终合并为一个完整的有序数组

归并排序的时间复杂度与待排序序列本身是否有序无关,时间复杂度恒为O(nlogn)

归并排序代码实现如下(C++):

cpp 复制代码
//归并排序代码实现
#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;

void mergeSort(vector<int> &nums,int left,int right){
	if(left >= right)return;//递归出口
	
	//分治操作 
	//将待排序序列拆分为两半,左半边为left~mid   右半边为mid+1~right
	int mid = (left + right)/2;
	//对左半边区间进行归并排序
	mergeSort(nums,left,mid);
	//对右半边区间进行归并排序
	mergeSort(nums,mid+1,right); 
	
	//合并操作
	int i=left;//左半边区间指针,初始指向本区间第一个元素 
	int j=mid+1;//右半边区间指针,初始指向本区间第一个元素 
	vector<int> tmp;//临时数组,暂存合并后的有序序列 
	//同时遍历左右区间,按顺序放入临时数组 
	while(i<=mid && j<=right){
		//左区间元素更小,放入临时数组后左指针后移 
		if(nums[i]<=nums[j]){
			tmp.push_back(nums[i++]);
		}
		//右区间元素更小,放入临时数组后右指针后移
		else{
			tmp.push_back(nums[j++]);
		} 
	}
	//处理左区间剩余元素(右区间已遍历完)
	while(i<=mid){
		tmp.push_back(nums[i++]);
	}
	//处理右区间剩余元素(左区间已遍历完)
	while(j<=right){
		tmp.push_back(nums[j++]);
	} 
	//将临时数组中的有序序列放回原数组的[left,right]区间 
	copy(tmp.begin(), tmp.end(), nums.begin() + left);
}

int main(){
	//数据输入
	int n;
	cin>>n;
	
	vector<int> nums(n+1);
	for(int i=1;i<=n;i++) {
		cin>>nums[i];
	}
	
	mergeSort(nums,1,n);
	
	//数据输出
	cout<<"排序结果如下:"<<endl;
	for(int i=1;i<=n;i++){
		cout<<nums[i]<<" ";
	}
	cout<<endl;
	
	return 0;
} 
/*
10
12 11 13 5 6 7 9 3 8 10
排序结果如下:
3 5 6 7 8 9 10 11 12 13
*/

总结

1. 上面提到的七种排序算法都属于内排序(待排序的所有数据都能一次性加载到计算机的内存中,并在内存中完成整个排序过程的排序方式)

2. 以稳定性(时间和空间复杂度是否恒定)看,在这七种排序算法中,

稳定的排序算法有:插入排序、冒泡排序、归并排序;

不稳定的排序算法有:希尔排序、选择排序、堆排序、快速排序;

相关推荐
Yzzz-F2 小时前
[模板]Nim博弈
算法
小龙报2 小时前
【数据结构与算法】栈和队列的综合应用:1.用栈实现队列 2.用队列实现栈 3.设计循环队列
c语言·数据结构·数据库·c++·redis·算法·缓存
重生之我是Java开发战士2 小时前
【广度优先搜索】队列:N叉树的层序遍历,二叉树的锯齿形层序遍历,二叉树的最大宽度,在每个树行中找最大值
数据结构·算法·leetcode·广度优先
qq_416018722 小时前
移动平台C++开发指南
开发语言·c++·算法
王璐WL2 小时前
【C++】string的经典算法题
开发语言·c++·算法
闻缺陷则喜何志丹2 小时前
【动态规划】P8591 『JROI-8』颅脑损伤 2.0|普及+
c++·算法·动态规划·洛谷
阿贵---2 小时前
C++中的工厂模式高级应用
开发语言·c++·算法
倾心琴心2 小时前
【agent辅助pcb routing coding学习】实践7 length matching 算法学习
学习·算法·agent·pcb·routing
y = xⁿ2 小时前
【LeetCodehot100】T114:二叉树展开为链表 T105:从前序与中序遍历构造二叉树
java·算法·链表