C语言实现常见八大排序万字详解

Posted SimplexXx0

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了C语言实现常见八大排序万字详解相关的知识,希望对你有一定的参考价值。


八种排序的动图展示讲解

插入排序

插入排序是指在待排序的元素中,假设前面n-1(其中n>=2)个数已经是排好顺序的,现将第n个数插到前面已经排好的序列中,然后找到合适自己的位置,使得插入第n个数的这个序列也是排好顺序的。 按照此法对所有元素进行插入,直到整个序列排为有序的过程,称为插入排序 。类似打扑克牌插入
1.思想:

假设一组数有n个元素,前n-1个已有序,记第n-1个位置为end,
用一个临时变量x记录第n个元素的值,x依次从end开始依次往前比,比x大的往后挪,找到比x小的停下,把x插入到它的后面。类似于打扑克牌。

2.图解:

单趟排序

整组排序

按照单趟排序方法对整组进行多趟排序

3.代码实现

void InsertSort(int*a, int n)

	for(int i = 0; i < n-1;i++)
	
		int end = i;
		int tem = a[end+1];
		while(end > 0)
		
			if(a[end] > tem)//比tem大往后挪
			
				a[end+1] = a[end];
				end--;
			
			else//比tem小停止
			
				break;
							
		
		a[end+1] = tem;//在后面把tem插入
	

4.性能分析

  • 时间复杂度:O(N^2)
  • 空间复杂度: O(1)
  • 稳定性:稳定

希尔排序

希尔排序也是一种插入排序,是直接插入排序算法的一种更高效的改进版本,又称“缩小增量排序”。希尔排序是非稳定排序算法。

1.思想:

把一个数组分成gap组,gap也是每组相邻两个元素之间的间距。对每一组进行直接插入排序(又称预排序),使整个数组接近有序。完成预排序后,使gap=1,也就是在最后对整个数组进行一次单趟排序,数组有序,完成排序。
可以进行多次预排序,使数组更加接近有序(具体做法:完成一趟预排序后,改变gap再次进行预排序)
gap越小,排的越慢,数组越接近有序。

2.图解

3.代码实现

void ShellSort(int* a, int n)


	for (int gap = n / 2; gap > 0; gap /= 2)//改变gap,进行多次预排序
	
		for (int i = 0; i < n - gap; i++)//每次对一个数在其本组上进行单趟插入排序,
		//i++相当于全部的组轮序着排,并不是一组先排完后再排另一组
		
			int end = i;//这里开始和插入排序一模一样,只是把间距变成了gap
			int tmp = a[end + gap];
			while (end >= 0)
			
				if (a[end] > tmp)
				
					a[end + gap] = a[end];
						end -= gap;
				
				else
				
					break;
				
			
			a[end + gap] = tmp;
		
	


4.性能分析

  • 时间复杂度:O(N*logN)
  • 空间复杂度: O(1)
  • 稳定性:不稳定

选择排序

选择排序是一种简单直观的排序算法。它的工作原理是:第一次从待排序的数据元素中选出最小(或最大)的一个元素,存放在序列的起始位置,然后再从剩余的未排序元素中寻找到最小(大)元素,然后放到已排序的序列的末尾。

1.思想:

  • (一次找一个数)遍历找到最小的数放到最边上

  • (一次找两个数)遍历数组,找出最大的数和最小的数,分别放到最两边,再对中间的无序数组再选最大最小,放到中间无序数列的两头

2.图解
一次选一个数


一次选两个数

选出小的放左边,大的放右边,图像应该比较好想象。

3.代码实现

void SelectSort(int* a, int n)

	int begin = 0; int end = n - 1;
	while (begin < end)
	
		int min = begin; int max = end;
		for (int i = begin; i <= end; i++)
		
			if (a[i] < a[min])
				min = i;         //mini记录查找过程中最小值的位置下标
			if (a[i] > a[max])
				max = i;         //maxi记录查找过程中最大值的位置下标
		
		
		Swap(&a[min], &a[begin]);  //把找到的最小值放到数组最左边
		if (max = begin)
			min = max;
		Swap(&a[max], &a[end]);     //把找到的最大值放到数组最右边
	
	//控制范围,对剩下中间的数再进行选择、排序
	++begin;
	--end;	

代码注解

代码中两个Swap函数之间的if条件判断,是为了防止出现一次选两个数的特殊情况而做的修正。如果max的位置和begin位置相同,那min和begin交换后,max位置的原始值已经变了,如果不修正就进行max和end交换会发生错误。

Swap(&a[min], &a[begin]); 
	if (max = begin)
		min = max;
Swap(&a[max], &a[end]);

堆排序

1.思想:

需要用到二叉树大小堆的概念。堆的逻辑结构是一棵完全二叉树,物理结构是数组。给我们一个数组,要排升序,我们就要把数组建成大堆;排降序,就建小堆。这里我们用向下调整AdjustDown()来建一个大堆(父节点比任何一个子节点大叫大堆)

完成一棵树(或子树)的堆排序操作图解

向下调整的代码实现

void Swap(int* x,int* y)//交换函数

	int tem = *x;
	*x = *y;
	*y = tem;


void AdjustDown(int*a,int n,int root)

	int child = root*2 + 1;
	int parent = root;
	for(child < n)
		//找到两个孩子中大的那一个
		if(a[child+1]<n && a[child+1]>a[child])
			child++;
		if(a[child]>a[parent])
		
			Swap(&a[child],&a[parent]);
			parent = child;//交换完后再往下对比
			child = parent*2 - 1;
		
			

对于要调一棵树,需要先保证把它的子树全部调成堆。所以我们可以从最后一棵子树的父亲开始调整,调完一棵子树再从此父节点依次往前一个结点调,最后完成整棵树的堆排序。

调完堆后就可以对堆里的数据进行排序了
思路:把堆顶数据和最后一棵子树调换位置,对除了尾结点的前n-1棵树进行调堆

void HeapSort(int* a, int n)//堆排序
	//排升序,先把数组建成大堆
	int parent = (n - 1 - 1) / 2;  //建大堆时,从最后一个孩子的父亲开始调
	for (int i = parent; i >= 0; i--)
	
		AdjustDown(a, n, i);
	
	for (int end=n-1;end>=0;end--)
	
		Swap(&a[0], &a[end]);//堆顶和最后一个孩子交换,前n-1个再调整成大堆
		AdjustDown(a, end, 0);
	


冒泡排序

这个算法的名字由来是因为越小的元素会经由交换慢慢“浮”到数列的顶端(升序或降序排列),就如同碳酸饮料中二氧化碳的气泡最终会上浮到顶端一样,故名“冒泡排序”。

1.思想

依次比较两个相邻的元素,前一个比后一个大则交换,否则继续比较下一对(升序),一趟完之后最大的数就被排在了最后面。再对前面的数继续排序,完成整组排序。

2.图解

单趟冒泡排序


3.代码实现

void BbubleSort(int* a, int n)

	assert(a);
	int end = n;
	while (end > 0)
	
		int exchange = 0;
		for (int i = 0; i < end - 1; i++)
		
			if (a[i] > a[i + 1]) //当判断条件为if(a[i-1]>a[i]),循环条件为for(int i=1;i<end;i++)
			
				exchange = 1;//判断是否交换过,如果一趟内未发生交换,则数组已有序,不用再排
				Swap(&a[i], &a[i + 1]);
			
		
		end--;
		if (exchange == 0)//如果一趟内没有发生交换,则表明数组已有序,跳出循环
		
			break;
		
	

快速排序

快速排序是Hoare于1962年提出的一种二叉树结构的交换排序方法

1.思想

这里是引用快速排序算法通过多次比较和交换来实现排序,其排序流程如下:

  • 任取待排序元素序列中的某元素作为基准值,按照该排序码将待排序序列分割成两子序列,左子序列中所有元素均小于基准值,右子序列中所有元素均大于基准值,然后左右子序列重复该过程划分各自的子序列,直到所有元素都排列在相应位置上为止。
  • 重复上述过程,可以看出,这是一个递归定义,类似二叉树的前序遍历。通过递归将左侧部分排好序后,再递归排好右侧部分的顺序。当左、右两个部分各数据排序完成后,整个数组的排序也就完成了。采用了分而治之的思想(分成小区间,让各自区间各自排序)

三数取中法:如何有效地选择基准值做key?

一般我们选最左/右边或随机选择的值来做key,不免会发生选到的就是最大值或最小值的情况,这样排序的效率就会很低。我们拿最左/右边的值和中间的值,三个值比较,取中间的不大不小的值来做key就可以解决这个问题。

int MidIndex(int* a,int left, int right)

	//int mid = left + right/2;  
	int mid = left + ((right - left) >> 1);//二进制往右移一位相当于除二,移两位相当于除四
	if (a[left] < a[mid])
	
		if (a[mid] < a[right])
		
			return mid;
		
		else
		
			return a[left] < a[right] ?  right : left;
		
	
	else //(a[left] > a[mid])
	
		if (a[mid] > a[right])
		
			return mid;
		
		else
		
			return a[right] > a[left] ? left : right;
		
	

通过基准值划分为左右子区间的常见方法

1.Hoare版本(左右指针法)

思想:

一般选最左/右边的值作为基准值key,

  • 左指针left从头开始找比key小的值,找到停下;
  • 然后右指针right从尾开始往前走,找到比key大的值,找到停下;
  • 交换此时left 和 right 的值。交换完重复步骤继续找、交换。
  • 当最后left 和 right 相遇时,把相遇位置的值和key交换。此时key的左边全部为比key小的,右边全部为比key大的。完成区间划分

使用原则

(因为最后一步要把相遇位和key交换,这样能保证key的左边比key小,key的右边比key大)

  • 选最左边的做基准值,右指针先走→(右边先走找小于key的值,一直找如果找不到,走到 left 相遇停下,left位是比key小的,保证了相遇位比key值小)

  • 选最右边的做基准值,左指针先走→(同上理,能保证相遇位比key大)

代码实现

int partition1(int* a, int left, int right)//单趟排序

	int mid = MidIndex(a, left, right);//三数取中
	Swap(&a[left], &a[mid]);//把中间数位置换为left

	int keyi = left;
	while (left < right)
	
		while (left < right && a[right] >= a[keyi])
			right--;
		while (left < right && a[left] <= a[keyi])
			left++;
		Swap(&a[left], &a[right]);
	
	Swap(&a[left], &a[keyi]);
	return left;

特殊场景

像这两种情况left 和 right 找不到相应的值就会一直走,导致越界,为了防止越界,要在while循环里加判断条件 left < right

递归程序缺陷:递归深度太深会导致栈溢出

2.挖坑法

挖坑法是左右指针法的一种变形
思想:

  • 将第一个值作为基准值保存到临时变量key里,形成一个坑位pivot
  • 假设选左边做坑,那右边先走,找到小,放进坑里,右就变成了新的坑
  • 然后左边走,找到大,放到坑里,左变成新的坑。然后右边走,重复步骤。
  • 相遇停下的时候把key值填到pivot坑中

图解


代码实现

int partition2(int* a, int left,int right)

	int mid = MidIndex(a, left, right);
	Swap(&a[left], &a[mid]);

	int key = a[left];
	int pivot = left;
	while (left < right)
	
		while(left<right && a[right] > key)
			right--;
		a[pivot] = a[right];
		pivot = right;

		while (left < right && a[left] < key)
			left++;
		a[pivot] = a[left];
		pivot = left;
	
	a[pivot] = key;
	return pivot;

3.前后指针法

思想:

  • 初始时,prev指针指向序列开头,cur指针指向prev的后一个位置,取基准值key
  • cur找小,找到小后,++prev往后一步,把 cur 和 prev 交换(相当于找到小的往前放)
  • cur 走完整个序列,把 prev 和 key 交换。

图解:

代码实现

int partition3(int* a, int left, int right)

	int mid = MidIndex(a, left, right);
	Swap(&a[left], &a[mid]);

	int key = left;
	int prev = left;
	int cur = left + 1;
	while (cur <= right)
	
		if (a[cur] < a[key] && ++prev != cur)
		                      //如果一开始cur就比key小,prev往后走就赶上了cur,交换后相当于没交换,所以如果prev的下一个是cur的让它们两个一起往后走一步就行了
			Swap(&a[cur], &a[prev]);
		
		cur++;
	
	Swap(&a[prev], &a[key]);
	return prev;

递归法小区间优化

递归调用层次越深,区间划分越多,递归调用的次数就越多,效率就会降低。我们可以考虑后面的几层用其他排序方法排序,可以大大减少调用递归的次数,防止栈溢出,提升排序效率。

代码汇总

//三数取中
int MidIndex(int* a,int left, int right)

	//int mid = left + right/2;  
	int mid = left + ((right - left) >> 1);//二进制往右移一位相当于除二,移两位相当于除四
	if (a[left] < a[mid])
	
		if (a[mid] < a[right])
		
			return mid;
		
		else
		
			return a[left] < a[right] ?  right : left;
		
	
	else //(a[left] > a[mid])
	
		if (a[mid] > a[right])
		
			return mid;
		
		else
		
			return a[right] > a[left] ? left : right;
		
	


//区间划分1.hoare左右指针
int partition1(int* a, int left, int right)//单趟排序

	int mid = MidIndex(a, left, right);//三数取中
	Swap(&a[left], &a[mid]);//把中间数位置换为left

	int keyi = left;
	while (left < right)
	
		while (left < right && a[right] >= a[keyi])
			right--;
		while (left < right && a[left] <= a[keyi])
			left++;
		Swap(&a[left], &a[right]);
	
	Swap(&a[left], &a[keyi]);
	return left;




//区间划分2:挖坑法。
// 思想:选left或right做坑,这里选left做坑pivot,key记录当前值,right先走,right找到比key小的数停下,
//       把right填到坑里,然后right做坑,left走,找到大于key的数,填进坑,left做坑,right走
int partition2(int* a, int left,int right)

	int mid = MidIndex(a, left, right);
	Swap(&a[left], &a[mid]);

	int key = a[left];
	int pivot = left;
	while (left < right)
	
		while(left<right && a[right] > key)
			right--;
		a八大排序算法C语言过程图解+代码实现(插入,希尔,选择,堆排,冒泡,快排,归并,计数)

[八大排序]0基础C语言实现八大排序,详解快排,归并,希尔

数据结构之八大排序算法(C语言实现)

八大排序 (万字总结)(详细解析,建议收藏!!!)

万字总结画解八大排序算法

详解数据结构八大排序(源码实现)(动图分析)