数据结构中常用排序算法总结

常用排序算法总结

本文包括:选择排序、简单插入排序、折半插入排序、希尔排序、冒泡排序、快速排序、归并排序、基数排序、堆排序


一、选择排序

选择排序是最简单的一种排序算法,它的思想很简单,即:第一趟遍历整个无序的序列,然后将最小的放首位;第二趟继续遍历后面无序的序列,将第二小的放在第二位…故而时间复杂度是O(n^2)

所以代码直接模拟即可

//简单选择
void select_sort()
{
	for(int i = 0;i < n;i++){
		int min = arr[i], min_index = i;
		for(int j = i + 1;j < n;j++){
			if(arr[j] < min){
				min = arr[j];
				min_index = j;
			}
		}
		int temp = arr[i];
		arr[i] = min;
		arr[min_index] = temp;
		show(); 
	}
}

通过运行结果看一看排序过程:
在这里插入图片描述


二、简单插入

也是一个非常简单的排序算法。它实现的前提是要保证第i个元素前面的所有元素都要保持有序,因此思路就是,从数组第一个元素开始遍历,先排好第一个元素;再遍历第二个元素,然后排好前两个元素的顺序;再遍历第三个元素,排好前三个元素的顺序…一直到第n个,就排好了。

代码:

//直接插入排序
void insert_sort(){
	for(int i = 1;i < n;i++){
		int val = arr[i], j;
		for(j = i - 1;j >= 0;j--){
			if(val < arr[j])
				arr[j + 1] = arr[j];
			else
				break;
		}
		arr[j + 1] = val;
	}
}

运行结果:
在这里插入图片描述
第一趟,将4和6排好了
第二趟,将1、4、6,前三个元素排好顺序了。1和4、6比较,只要发现6比1大,6就往后面移动;4也比1大,那么4也往后面移动一格;最后剩下的位置就是1了。

一直到最后


三、折半插入排序

这是在简单插入排序算法上进行一点小小的修改。
因为插入排序每一趟,前i个元素肯定都是有序的,所以我们没有必要一个一个地比较大小,可以运用《查找》学到的知识,有序序列下,用折半查找效率更高。

因此,折半插入没有什么新鲜东西,就是折半查找和插入排序的结合,数据量大的时候还是能大大提升查找效率的。

代码:

//折半插入排序
void half_insert_sort(){
	for(int i = 1;i < n;i++){
		int val = arr[i];
		//折半查找 
		int low = 0, high = i - 1;
		while(low <= high){
			int mid = (low + high) / 2;
			if(arr[mid] < arr[i])
				low = mid + 1;
			else
				high = mid - 1;
		}
		//位置移动
		for(int j = i - 1;j >= high + 1;j--){
			arr[j + 1] = arr[j];
		} 
		arr[high + 1] = val;		//注意,这里的下标一定是high+1
	}
} 

折半查找最重要的是搞清楚边界条件,最后元素要放到high + 1的下标上去


四、希尔排序

希尔排序是一种改良的插入排序。
希尔排序开始需要设定一个步长d,根据这个步长我们可以把整个数组分成几个小组,然后每一个小组内部按照直接插入排序来排就可以了。步长要求递减,等到步长小于1时,整个排序算法就完成了(步长 = 1时就是直接插入排序)

以上文字可能不太好理解,给个图:
第一趟排序时,步长为5,故针对这个长度为10的数组,我们可以分为以下五个小组。每个小组内部按照插入排序来进行
在这里插入图片描述
第一趟排序后的结果:
在这里插入图片描述
和你的想法想法是否一样呢?

总体排序流程:
在这里插入图片描述
大家可以手动模拟验证一下

代码:

void shell_sort(int d)
{
	//分组 
	while(d > 0){
		//每一组内部还是用插入排序的思想来做 
		for(int i = d;i < n;i++){
			int val = arr[i], j;
			//和前面组内的元素对比
			for(j = i - d;j >= 0;j -= d){
				if(arr[j] > val)
					arr[j + d] = arr[j]; 
				else
					break;
			} 
			arr[j + d] = val;
		}
		d -= 2;
		show();
	}
}

五、冒泡排序

这可能是知名度最高的一种排序算法,同时也很简单。它属于交换排序的一种。

核心思想是:从头开始遍历,每次遍历前后相邻的两个元素,如果前面的元素大于后面的元素,那么将两者交换…所以第一轮下来,可以把最大的元素放最后,第二轮可以把第二大的元素放到倒数第二个位置…最后完成

代码:

//冒泡
void bubble_sort(){
	for(int i = 0;i < n - 1;i++){
		for(int j = 0;j < n - 1 - i;j++){
			if(arr[j] > arr[j + 1]){
				int temp = arr[j + 1];
				arr[j + 1] = arr[j];
				arr[j] = temp;
			}
		}	
	}
}

排序流程:
在这里插入图片描述


六、快速排序

被誉为 “效率最高” 的排序算法。虽然快速排序在理论上比堆排序、归并排序慢,但是实际情况下,尤其是处理大量随机无序的数据时,快速排序的效率往往是最高的!因此,快速排序的使用非常广泛。注:快速排序是一种不稳定的排序算法。关于稳定性的问题,我会在文末进行总结。

//快排
void quick_sort(int low, int high){
	if(low >= high)	return ;
	int i = low, j = high;
	int srd = arr[low];	//每次取第一个数作为快排的基准数 
	while(i < j){
		while(arr[j] > srd && i < j){	//先从右边开始 
			j--;
		}
		while(arr[i] <= srd && i < j){	//因为i一开始是从low出发的,所以是小于等于 
			i++;
		}
		//i, j交换 
		int temp = arr[i];
		arr[i] = arr[j];
		arr[j] = temp;
	}
	show();
	//基准数归位。此时srd这个值的位置已经确定下来了。
	arr[low] = arr[i];		//此时i==j,填i或者填j都可以
	arr[i] = srd;
	quick_sort(low, i - 1);
	quick_sort(i + 1, high);
}

过程:
在这里插入图片描述
快排是递归的,所以这个过程图可能不那么直观


七、归并排序

归并排序我觉得是排序算法中相对难一点的算法。它和快排一样,也有分治递归的思想,但实现起来却没有那么容易。

在这里插入图片描述
将整个序列拆分成若干个(分治,对应代码中的merge_sort)
在这里插入图片描述
在这里插入图片描述
将两个有序的序列合成一个有序的序列(归并:对应代码中的merge)

代码:

//归并排序
//左右有序的时候就能排了 
void merge(int arr1[], int L, int M, int R){
	int LEFT_SIZE = M - L;
	int RIGHT_SIZE = R - M + 1;
	int left[LEFT_SIZE];
	int right[RIGHT_SIZE];
	
	
	//fill left
	for(int i = L;i < M;i++){
		left[i - L] = arr1[i];
	}
	
	//fill right
	for(int i = M;i <= R;i++){
		right[i - M] = arr1[i]; 
	}
	
	//merge into orignal array
	int i = 0;int j = 0;int k = L;
	while(i < LEFT_SIZE && j < RIGHT_SIZE){
		if(left[i] < right[j]){
			arr1[k] = left[i];
			i++;k++;
		}else{
			arr1[k] = right[j];
			k++;j++;
		}
	}
	while(i < LEFT_SIZE){
		arr1[k] = left[i];
		i++;k++;
	}
	while(j < RIGHT_SIZE){
		arr1[k] = right[j];
		j++;k++; 
	}
	
} 

//分治 
void merge_sort(int arr1[], int L, int R)
{
	if(L == R)	return ;
	//将长数组看成两段 
	int M = (L + R) / 2;
	merge_sort(arr1, L, M);
	merge_sort(arr1, M + 1, R);
	merge(arr1, L, M + 1, R);
}

大体上的思路就是,先把左边弄成有序的,右边再弄成有序的,最后通过merge方法,将左右两边merge起来,就是一个有序的整体了。


八、基数排序

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述
第三趟做完之后,我们惊讶的发现,数据已经排好了。

基数排序就是这么简单,第一趟按个位分配,收集回来后再按照十位分配到十个捅里面,第三趟再按照百位分配到十个桶里面,因此也叫做桶排序

因为基数排序的这个特点,很容易让人想到用邻接链表去实现。

这就意味着我们要先手动实现以下邻接表这个数据结构,所以代码会比较长一点。

代码:

#include<stdio.h>
#include<stdlib.h>

#define MAX 10005

typedef struct Node{
	int val;
	int len;			//链接链表的长度 
	Node *next = NULL;
} Node;

int nums[10] = {102,31,24,55,13,201,307,901,100,2};

//对邻接表进行头插 
/*void insert(int key, int val, Node* arr){
	Node *p = (Node*)malloc(sizeof(Node));
	p->val = val;
	p->next = arr[key].next;
	arr[key].next = p;
}*/

//基数排序需要用尾插法 
void insert_tail(int key, int val, Node* arr){
	Node *p = (Node*)malloc(sizeof(Node));
	p->val = val;
	//拿到当前链表的长度
	Node* temp = &arr[key]; 
	int len = arr[key].len;
	for(int i = 1;i <= len;i++){
		temp = temp->next;
	}
	temp->next = p;	
	p->next = NULL;
	//有一插入了,len++ 
	arr[key].len++;
}

//销毁邻接表
void destroy(int n, Node* arr){
	for(int i = 0;i < n;i++){
		Node *p = arr[i].next;
		while(p != NULL){
			Node *temp = p->next;
			printf("%d的节点已经删除...\n", p->val);
			free(p);
			p = temp;
		}
	}
} 

//打印一行 
void showLine(int i, Node* p){
	printf("当前是第%d行:", i);
	while(p!=NULL){
		printf("%d ", p->val);
		p = p->next;
	}
	printf("\n");
}

//展示邻接表 
void show(Node* arr, int len){
	for(int i = 0;i < len;i++){
		showLine(i, arr[i].next);
	}
} 

//show数组
void showNums(){
	printf("===当前轮次下的nums数组为:");
	for(int i = 0;i < 10;i++){
		printf("%d ", nums[i]);
	}
	printf("\n");
} 



//radix = 1:表示按个位建邻接表;radix = 2--->十位 
void sort(Node* arr, int radix){
	//十个数 
	for(int i = 0;i < 10;i++){
		//取它的关键值 
		int val = nums[i];
		for(int j = 1;j < radix;j++){
			val /= 10; 
		}
		int key = val % 10;
		//将这个节点数据插在key上面的邻接表
		insert_tail(key, nums[i], arr);
	}
	//将链表上的数据收回来
	int idx = 0;
	for(int i = 0;i < 10;i++){
		Node *p = arr[i].next;
		while(p != NULL){
			int val = p->val;
			//填回到nums数组里去 
			nums[idx] = val;
			idx++;
			p = p->next;
		}
	}
	//showNums
	showNums();
	//show表 
	show(arr, 10);
}

/***
基数排序 
***/
void radix_sort(){
	//初始化
	Node *arr = (Node*)malloc(10 * sizeof(Node));
	int radix = 3;
	//个位、十位、百位。循环放桶 
	for(int i = 1;i <= radix;i++){
		//初始化表 
		for(int j = 0;j <= 9;j++){
			arr[j].next = NULL;
			arr[j].val = 0;
			arr[j].len = 0; 
		} 
		//建表,填数据 
		sort(arr, i);
		//销毁 
		destroy(10, arr);
	}
	free(arr);
}

int main()
{
	radix_sort();
	return 0;
 } 

排序结果:
在这里插入图片描述
下面是按照百位排序的结果:
在这里插入图片描述
可以看到,按照百位排好之后,nums数组里面的数据就已经有序了,相当神奇吧!


九、堆排序

构造一个堆的过程:
在这里插入图片描述
因为每一趟下来,会把最大值放到完全二叉树的最后节点,然后将其无情删去,这样破坏了堆的性质。因此,每一趟之后要经历一个筛选调整的过程。而每一次筛选调整就是用前面的二叉树再建一个新堆的过程。
在这里插入图片描述

可以看到,堆排序的特点是,每一趟都可以找到一个最大值或最小值,这一点上和选择排序、冒泡排序有相似之处。

堆排序是不稳定的。


各种排序算法总结:

在这里插入图片描述
希尔排序的时间复杂度还没有证明出来
注意四种不稳定的排序算法。简单选择和希尔也是不稳定的。

  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值