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

Posted IT莫扎特

tags:

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


1.排序的概念及其运用

排序:

所谓排序,就是使一串记录,按照其中的某个或某些关键字的小,递增或递减的排列起来的操作。

稳定性:

假定在待排序的记录序列中,存在多个具有相同的关键字的记录,若经过排序,这些记录的相对次序保持不变,即在原序列中,r[i]=r[j],且r[i]在r[j]之前,而在排序后的序列中,r[i]仍在r[j]之前,则称这种排序算法是稳定的;否则称为不稳定的。

内部排序:

数据元素全部放在内存中的排序。

外部排序:

数据元素太多不能同时放在内存中,根据排序过程的要求不能在内外存之间移动数据的排序

2.常见排序算法的实现

插入排序

1、从图中观察的现象是如果后一个数不比前一个数小,那就不需要插入,不插入的动作就是break出循环
2、如果前面的数都比pos值大,那么就将前n个数都往后挪动,直到比pos值小或者相等就停止,可以用循环控制,这里防止越界需要再加判断

插入排序的基本思想:其基本思想是:

把待排序的记录按其关键码值的大小逐个插入到一个已经排好序的有序序列中,直到所有的记录插入完为止,得到一个新的有序序列 。

代码

//插入排序,升序
void InsertSort(int* arr, int n)
{
	int i = 0;
	while (i < n - 1)
	{
		int end = i;
		int tmp = arr[end + 1];
		while (end >= 0) 
		{
			if (arr[end] > tmp) 
			{
				arr[end + 1] = arr[end];
				end--;
			}
			else 
			{
				break;
			}
		}
		//由于end是后置--所以当到达合适的位置时,需要+1
		arr[end + 1] = tmp;
		i++;
	}
}

直接插入排序的特性总结:

  1. 元素集合越接近有序,直接插入排序算法的时间效率越高,反之越低
  2. 时间复杂度:O(N^2)
  3. 空间复杂度:O(1),它是一种稳定的排序算法
  4. 稳定性:稳定

希尔排序

希尔排序法又称缩小增量法。

希尔排序法的基本思想是:
先选定一个整数,把待排序数组中所有元素分成个组,所有距离为gap的元素分在同一组内,并对每一组内的元素进行排序。然后,重复上面分组和排序的工作。当到达=1时,所有元素在统一组内排好序。


从图中观察到的现象:
1、gap越大越不接近有序,但是挪动的更快
2、gap越小越接近有序,挪动的越慢
3、gap为1时,已经很接近有序了,直接插入排序,gap不为1时就是预排序的过程,让数组接近于有序,接近有序后直接插入排序的效率会更高

代码

//希尔排序
void ShellSort(int* arr, int n) 
{
	int gap = n;
	while (gap > 1) 
	{
	//控制gap值的变化,让数组接近有序,gap == 1就可以直接插入排序
		gap = (gap / 3 + 1);
		for (int i = 0; i < n - gap; i++) 
		{
			int end = i;
			int tmp = arr[end + gap];
			while (end >= 0) 
			{
				if (arr[end] > tmp) 
				{
					arr[end + gap] = arr[end];
					end -= gap;
				}
				else 
				{
					break;
				}
			}
			arr[end + gap] = tmp;
		}
	}
}

时间复杂度分析:
最坏的情况,逆序,gap很大的时候–》 O(N),当gap很小时本来应该是O(N * N),但是经过前面的预排序,数组已经已经很接近有序的,所以间隔为gap的插入排序可以理解为很接近O(N),而再看外循环影响循环次数的语句,gap = (gap / 3 + 1);,
当 gap / 3 / 3 / 3 … == 1,展开之后是 3 ^ x = gap,所以外层while循环执行的次数就是x次,
那么算法的整体时间复杂度就是O(log 3 (N) * N),
log 3 (N),以3为底N的对数

选择排序

选择排序的基本思想:
在每次遍历数组的过程中一次循环选两个下标找出最大和最小值的下,将大的交换到右边,小的交换到左边

//选择排序
void selectSort(int* arr, int n)
{
	int left = 0;
	int right = n - 1;
	
	while (left < right) 
	{
		int MaxIndex = left, MinIndex = left;
		for (int i = left; i <= right; i++) 
		{
			if (arr[MaxIndex] < arr[i])
				MaxIndex = i;
			else
				MinIndex = i;
		}
		//将大的值交换到右边,小的值交换到左边
		Swap(&arr[left],&arr[MinIndex]);
		//防止max被换走
		if (MaxIndex == left) 
		{
			MaxIndex = MinIndex;
		}
		Swap(&arr[right], &arr[MaxIndex]);

		left++;
		right--;
	}
}
  1. 时间复杂度:O(N^2)
  2. 空间复杂度:O(1)
  3. 稳定性:不稳定

堆排

博主之前已经在堆实现这一章讲解过了,如果需要细致学习的请点击链接,这里就不再叙说了,需要注意的是排升序要建大堆,排降序建小堆

堆排教程.

代码:

//向下调整
void AdjustDown(int* arr, int n, int parent)
{
	int child = parent * 2 + 1;
	while (child < n) 
	{
		if(child + 1 < n && arr[child + 1] > arr[child])
		{
			child++;
		}
		else if (arr[child] > arr[parent]) 
		{
			Swap(&arr[child],&arr[parent]);
			parent = child;
			child = parent * 2 + 1;
		}
		else 
		{
			break;
		}
	}
}
//堆排
void HeapSort(int* arr, int n) 
{
	for(int i = (n - 1 - 1) / 2 ; i >= 0; i--)
	{
		AdjustDown(arr, n, i);
	}
	
	int end = n - 1;
	while (end >= 0) 
	{
		Swap(&arr[end--],&arr[0]);
		AdjustDown(arr,end,0);
	}
}

冒泡排序


冒泡排序的基本思想:
就是根据序列中两个记录键值的比较结果来对换这两个记录在序列中位置,交换排序的特点是:将键值较大的记录向序列的尾部移动,键值较小的记录向序列的前部移动。

两两相比较,将小的交换到前面大的交换到后面,排序n个元素只需要比较n - 1趟,每冒泡一趟少比较一个元素

//冒泡排序
void bubblesort(int* arr, int n) 
{
	int end = 0;
	for (end = n; end > 0; end--) 
	{
		int flag = 0;
		int j = 0;
		for (j = 1; j < end; j++) 
		{
			if (arr[j - 1] > arr[j]) 
			{
				Swap(&arr[j - 1] ,&arr[j]);
				flag = 1;
			}
			else 
			{
				break;
			}
		}
		if (!flag)
			break;
	}
}
  1. 时间复杂度:O(N^2)
  2. 空间复杂度:O(1)
  3. 稳定性:稳定

快排

快速排序是Hoare于1962年提出的一种二叉树结构的交换排序方法,其基本思想为:任取待排序元素序列中的某元素作为基准值,按照该排序码将待排序集合分割成两子序列,左子序列中所有元素均小于基准值,右子序列中所有元素均大于基准值,然后最左右子序列重复该过程,直到所有元素都排列在相应位置上为止。

hoare版本

过程:
1、单趟排序选出key,通常情况下key的位置都是选择在数组下标为0的位置,最左边,最右边都可以
2、将小的值交换到左边,将大的值交换到右边,最终把key放到正确的位置,保证左边的值要比key小,右边的值要比key大

左右指针法:
左边的哨兵找比key大的值,右边的哨兵找比key小的值


观察到的现象是一次单趟排序后,key的左边值都比key要小,key右边的值都要比key大,这样就已经达成了初步有序的目的了

	if (begin >= end) 
	{
		return;
	}
	
	int left = begin, right = end;
	int key = left;
	while (left < right)
	{
		//右找小,left < right防止升序的情况下出现越界
		while (left < right && arr[right] >= arr[key])
		{
			right--;
		}
		//左找大
		while (left < right && arr[left] <= arr[key])
		{
			left++;
		}
		//交换,将比key小的值换到左边,比key大的值换到右边
		Swap(&arr[left], &arr[right]);
	}
	int meet = left;
	//确定key的位置
	Swap(&arr[left], &arr[key]);

	QuickSort(arr,begin, meet - 1);
	QuickSort(arr, meet + 1, end);

挖坑法

基本思想:
将第一个位置选做key,这样就形成了天然的坑位,右边去找比key小的值,找到后将值填充到坑位中,自己成为新坑,左去找大,找到后将值放到右边的坑中,自己成为新的坑,反复直到相遇,相遇点也是一个坑位,将key的值放到坑中,这样单趟排就已经确定key值的位置了

初步单趟排就已经确定了,并且key也放到它正确的位置了,key的左边比他小,右边比他大

//挖坑法
void QuickSort(int* arr, int begin, int end) 
{
	if (begin >= end) 
	{
		return;
	}

	int left = begin, right = end;
	//将第一个数据存放在临时变量中,成为一个天然坑位	
	int key = arr[left];
	while (left < right) 
	{
		//找小
		while (left < right && arr[right] >= key)
			right--;
	   //找到小的将小的放到左边坑,右边成为新的坑位
		arr[left] = arr[right];
		//找大
		while (left < right && arr[left] <= key)
			left++;
	    //找到大的将大的放到右边坑,左边成为新的坑位
		arr[right] = arr[left];
	}
	//选key,将key值放到相遇点的位置
	arr[left] = key;
	int meet = left;
	//左区间单趟排
	QuickSort(arr, begin,meet - 1);
	//右区间单趟排
	QuickSort(arr, meet + 1, end);
}

前后指针法

基本思想:
双指针,定义prev和cur开始一前一后,cur去找比key小的值,找到后++prev,再交换cur和prev它们的值,直到数组遍历完,最后一下交换key位置的值和prev位置的值,这样就确定了key值的位置

//前后指针法
void __QuickSort(int* arr, int begin, int end)
{
	if (begin >= end)
	{
		return;
	}
	int prev = begin - 1;
	int cur = begin;
	int key = begin;
	
	while (cur <= end)
	{
		while (arr[cur] < arr[key] && ++prev != cur)
		{
			Swap(&arr[cur], &arr[prev]);
		}
		cur++;
	}
	Swap(&arr[prev],&arr[key]);
	
	_QuickSort(arr, begin, prev - 1);
	_QuickSort(arr, prev + 1, end);
}

快排时间复杂度分析:

先考虑理想情况:

从图中我们可以看出快排的单趟排不管是右遇左,还是左遇右,合计起来也只能走数组长度N次,每选一个key值划分左右区间,单趟排确定key值的位置,不断递归划分左右区间,递归的深度是以2^N递增的,所以它的时间复杂度是O(N * log N)

考虑最坏情况:
如果数组已经是有序的了,那么不管是右遇左,还是左遇右每次都得走n-1步才能选出key的位置,那么它的执行次数就是一个等差数列了,所以时间复杂度就是O(N^2),怎么优化呢?

快排的优化:

针对快排得优化:
思考:对快排影响最大的是选的key,如果key越接近中位数越接近二分,效率越高

1、三数取中

找出这个区间中的中位数,使每次选key都为中位数,那么不用考虑有序这种恶劣的情况出现了

//三数取中
int GetMidIndex(int* arr, int left, int right)
{
	int mid = (left + right) >> 1;
	if (arr[left] < arr[mid])
	{
		if (arr[mid] < arr[right])
		{
			return mid;
		}
		else if (arr[left] > arr[right])
		{
			return left;
		}
		else
		{
			return right;
		}
	}
	else //arr[left] > arr[mid]
	{
		if (arr[mid] > arr[right])
		{
			return mid;
		}
		else if (arr[left] < arr[right])
		{
			return left;
		}
		else
		{
			return right;
		}
	}
}

2、小区间优化

当每一个区间递归下去的时候只剩20个数了(官方参考),就可以考虑不再递归换用插入排序,接近于有序插入排序的效果会更好一些,而且递归 也是有消耗的,能节省就节省一些

	if (end - begin > 10) 
	{
		QuickSort(arr, begin, meet - 1);
		QuickSort(arr, meet + 1, end);
	}
	else 
	{
		InsertSort(arr + begin, end - begin + 1);
	}

完整的代码:

//快速排序
void QuickSort(int* arr, int begin, int end)
{
	if (begin >= end) 
	{
		return;
	}
	
	int MidIndex = GetMidIndex(arr, begin, end);
	int left = begin, right = end;
	Swap(&arr[MidIndex], &arr[left]);
	int key = left;
	
	while (left < right)
	{
		
		while (left < right && arr[right] >= arr[key])
		{
			right--;
		}
		
		while (left < right && arr[left] <= arr[key])
		{
			left++;
		}
		
		Swap(&arr[left], &arr[right]);
	}
	int meet = left;
	Swap(&arr[left], &arr[key]);

	if (end - begin > 20) 
	{
		QuickSort(arr, begin, meet - 1);
		QuickSort(arr, meet + 1, end);
	}
	else 
	{
		InsertSort(arr + begin, end - begin + 1);
	}
	
}

快排非递归实现

为什么会有非递归的版本呢,有些场景递归解决不了的问题于是就需要非递归登场了
非递归实现思想:由于C语言库并没有栈,所以需要自己动手实现一个栈,如果需要相关代码的可以点击这个链接: 栈实现.要想实现非递归并不难,只需要理解好快排的递归原理,快排的递归思想是,对一段区间单趟排,选key,确定好key的位置,再对key的左区间递归递归单趟排,key的右区间递归单趟排,不断地分裂,确定key的位置,只剩一个值了,这样一来数组就有序了,读者有没有发现,其中描述的两个步骤无非就是选key和对一段区间进行单趟排序,直到只剩一个值了,就可以是有序的,所以只需要用栈模拟递归的过程,将一段区间用栈保存起来,取区间出来单趟排,再选key的位置,不断划分左右区间,最终只剩一个值了,数组就是有序的了

//单趟排序,返回key
int parsort(int* arr, int begin, int end)
{
	//三数取中
	int MidIndex = GetMidIndex(arr, begin, end);
	int left = begin, right = end;
	Swap(&arr[MidIndex], &arr[left]);
	int key = left;
	while (left < right)
	{
		//右找小,left < right防止升序的情况下出现越界
		while (left < right && arr[right] >= arr[key])
		{
			right--;
		}
		//左找大
		while (left < right && arr[left] <= arr[key])
		{
			left++;
		}
		//交换
		Swap(&arr[left], &arr[right]);
	}
	

以上是关于万字总结画解八大排序算法的主要内容,如果未能解决你的问题,请参考以下文章

八大排序算法C语言过程图解+代码实现(插入,希尔,选择,堆排,冒泡,快排,归并,计数)

❤️五万字《十大排序算法》动图讲解❤️(建议收藏)

❤️《画解数据结构》之 顺序表八大算法总结❤️(建议收藏)

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

八大排序算法总结

苏州程序大白用2万字解析数据结构和八大排序算法☀️《❤️记得收藏❤️》