网络知识 娱乐 八大排序,你都掌握了吗?

八大排序,你都掌握了吗?

八大排序

  • ①🌞直接插入排序
    • 💡复杂度分析
  • ②🌞希尔排序
    • 预排序
    • 💡复杂度分析
  • ③🌞选择排序
    • 💡复杂度分析
  • ④🌞堆排序
    • 💡复杂度分析
  • ⑤🌞冒泡排序
    • 💡复杂度分析
  • ⑥🌞快速排序
    • 递归算法
      • 1.hoare(左右指针法)
      • 2.挖坑法
      • 3.前后指针法
    • 非递归算法
    • 💡复杂度分析
    • 快排的优化❗️❗️
      • 1.三数取中
      • 2.小区间优化
  • ⑦🌞归并排序
    • 递归算法
    • 非递归算法
    • 💡复杂度分析
  • ⑧🌞计数排序
    • 💡复杂度分析

①🌞直接插入排序

动图演示:
在这里插入图片描述

思想:
可以想象为打扑克的排序,在摸牌时,假设前面的牌都是排好序的,现在摸到一张新的牌,插入原来排好的序列中。
那么我们在写代码的时候就可以认为第一个是有序的,从第二个开始,把后边的数全部插入到前面

代码实现:

//插入排序
void InSertSort(int* a, int n)
{
	int cur = 1;
	while (cur < n)
	{
		int tmp = a[cur];//排cur处的数字
		int end = cur - 1;
		while (end >= 0)
		{
			if (a[end] > tmp)//继续比较
			{
				a[end + 1] = a[end];
				end--;
			}
			else
			{
				break;
			}
		}
		//通过画图就可知道不管是break还是结束while循环
		//都要在end + 1处插入
		a[end + 1] = tmp;
		cur++;
	}
}

💡复杂度分析

最坏的情况就是当整个数组都是逆序的时候此时复杂度为O(N ^ 2)

最好的情况就是当数组接近顺序有序的时候此时复杂度为O(N)

为什么接近就是O(N)?
例如这个接近有序的数组:1 2 3 5 4 7 6 8 9
要挪动的数字只有4、6, 都只挪动(循环)一次所以复杂度为O(N)


②🌞希尔排序

从上面的分析可知,如果一个数组接近有序,那么插入排序的复杂度就为O(N)。
那么我们就可以分两个步骤排序:
1、预排序 ——> 接近有序
2、直接插入排序

那么如和预排序呢?

预排序

先分组,对分组的数据进行插入排序
间隔为gap的分成一组,对一组进行插入排序
如图:


在这里插入图片描述

用绿色为例:

在这里插入图片描述

全部排完后:

在这里插入图片描述
可以看出这个相比于最开始更接近有序,并且大的数被更快的挪到后边,小的更快的挪到前面。
并且当gap越小,越接近有序,当gap为1时,就是插入排序

那么现在就要确定gap的值:
刚开始把gap值设的较大一点,慢慢减小,最后为1时就排好了(多组预排序)

void ShellSort(int* a, int n)
{
	//一趟预排序
	int gap = n;
	while (gap > 1)
	{
		//gap不能等于0
		//gap == 1时直接插入排序
		gap = gap / 3 + 1;
		for (int i = 0; i < n - gap; i += gap)
		{
			int end = i;
			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;
		}
	}
}

💡复杂度分析

假设最坏情况(逆序)
刚开始gap很大,for循环走2/3N ->N次,但下面的while就是常数次,所以时间复杂度为O(N)
当gap很小时,本来是O(N^2),但是前面经过预排序,已经变得有序,所以还是O(N)
可以看出来while循环里复杂度为O(N)

那么while循环了多少次呢?log3(N)
所以整体的时间复杂度:O(long3(N) * N)

平均的时间复杂度为O(N^1.3)


③🌞选择排序

在这里插入图片描述

思路:
本质上就是选出最小(最大),插入到左右两端,在选出次小(大)的

为了提高效率,我们可以一次把最大和最小的选出来.

void SelectSort(int* a, int n)
{
	int left = 0, right = n - 1;
	while (left < right)
	{
		int minIndex = left, maxIndex = left;//记录下标
		for (int i = left; i <= right; i++)
		{
			if (a[minIndex] > a[i])
			{
				minIndex = i;
			}
			if (a[maxIndex] < a[i])
			{
				maxIndex = i;
			}
		}
		Swap(&a[minIndex], &a[left]);
		//当最大的在左边时候,最大的会被交换走
		if (left == maxIndex)
		{
			maxIndex = minIndex;
		}
		Swap(&a[maxIndex], &a[right]);
		left++;
		right--;
	}
}

注意把最小的放到左边时有可能会影响最大值(有可能最大值在左边被交换走了)。

💡复杂度分析

选出最小的N次,选N次
时间复杂度为O(N^2)


④🌞堆排序

堆排序在以前写过的文章有详细讲解:
一万字学会堆和二叉树
这里就不过多介绍

void Swap(int* a, int* b)
{
	int tmp = *a;
	*a = *b;
	*b = tmp;
}


//大堆
void AdjustDown(int* a, int n, int parent)
{
	int child = 2 * parent + 1;
	while (child < n)
	{
		if (child + 1 < n && a[child + 1] > a[child])
		{
			child++;
		}

		if (a[child] > a[parent])
		{
			Swap(&a[child], &a[parent]);
			parent = child;
			child = 2 * parent + 1;
		}
		else
		{
			break;
		}
	}
}



void HeapSort(int* a, int n)
{
	//建堆
	for (int i = (n - 1 - 1) / 2; i >= 0; i--)
	{
		AdjustDown(a, n, i);
	}
	//大的放后面
	int end = n - 1;
	while (end)
	{
		Swap(&a[0], &a[end]);
		AdjustDown(a, end, 0);
		end--;
	}
}

💡复杂度分析

建堆的时间复杂度为O(N),向下调整的时间复杂度为O(log^N)(高度)
O(N + N*log^N)——> O(N * log(N))


⑤🌞冒泡排序

大致思想就是两个指针遍历数字,每次都把最大的给挪到最后
要注意的是控制趟数
假如有n个元素,则走n - 1趟
每走一趟,俩指针需要走的趟数就少一次(n - 1 - i)

在这里插入图片描述

void BubbleSort(int* a, int n)
{
	for (int i = 0; i < n - 1; i++)
	{
		//用来判断是否交换过,提高效率
		int flag = 1;
		for (int cur = 0; cur < n - 1 - i; cur++)
		{
			if (a[cur + 1] < a[cur])
			{
				Swap(&a[cur], &a[cur + 1]);
				flag = 0;
			}
		}
		if (flag)
		{
			break;
		}
	}
}

💡复杂度分析

最坏情况:
第一趟:N
第二趟:N - 1
……
O(N^2)
最好情况(有序)
O(N)

可以发现冒泡和插入的时间复杂度相似
那么两种排序算法哪个更好呢?
1️⃣顺序有序时两个一样好
2️⃣接近有序时插入排序好(比如有一段有序,插入就不用挪动,而冒泡还得走完)


⑥🌞快速排序

思想:选出一个key(左),把key放入正确位置,使key的左边全是小于key的,右边全是大于key的,走一遍叫单趟排序
然后递归下去就能排好全部

这里单趟排序有三种方法:
1️⃣Hoare(左右指针法)
2️⃣挖坑法
3️⃣前后指针法

递归算法

1.hoare(左右指针法)

在这里插入图片描述
注意一定要让右边先走
这样每次相遇点一定是小的
左边只有一个就不用递归了,把右边在进行单趟排序,直到都只剩下一个元素

//hoare法
int PartSort(int* a, int begin, int end)
{
	int keyi = begin;
	int left = begin, right = end;
	while (left < right)
	{
		//右找小
		while (left < right && a[right] >= a[keyi])
		{
			right--;
		}
		//左找大
		while (left < right && a[left] <= a[keyi])
		{
			left++;
		}
		if (left < right)
		{
			Swap(&a[left], &a[right]);
		}
	}
	Swap(&a[keyi], &a[left]);
	return left;
}


void QuickSort(int* a, int begin, int end)
{
	//中间没有元素就返回
	if (begin >= end)
	{
		return;
	}
	int keyi = PartSort(a, begin, end);
	QuickSort(a, begin, keyi - 1);
	QuickSort(a, keyi + 1, end);
}

2.挖坑法

在这里插入图片描述

//挖坑法
int PartSort1(int* a, int begin, int end)
{
	int tmp = a[begin];
	int left = begin, right = end;
	while (left < right)
	{
		//右边先找小
		while (left < right && a[right] >= tmp)
		{
			right--;
		}
		a[left] = a[right];
		//左边找大的
		while (left < right && a[left] <= tmp)
		{
			left++;
		}
		a[right] = a[left];
	}
	a[left] = tmp;
	return left;
}


void QuickSort(int* a, int begin, int end)
{
	//中间没有元素就返回
	if (begin >= end)
	{
		return;
	}
	int keyi = PartSort1(a, begin, end);
	QuickSort(a, begin, keyi - 1);
	QuickSort(a, keyi + 1, end);
}

3.前后指针法

在这里插入图片描述

//前后指针法
int PartSort2(int* a, int left, int right)
{
	int key = left;
	int prev = left, cur = prev + 1;
	while (cur <= right)
	{
		if (a[cur] < a[key])
		{
			prev++;
			Swap(&a[cur], &a[prev]);
		}
		cur++;
	}
	Swap(&a[prev], &a[key]);
	return prev;
}


void QuickSort(int* a, int begin, int end)
{
	//中间没有元素就返回
	if (begin >= end)
	{
		return;
	}
	int keyi = PartSort2(a, begin, end);
	QuickSort(a, begin, keyi - 1);
	QuickSort(a, keyi + 1, end);
}

非递归算法

当改成非递归算法时要用到栈,用栈的特性达到递归的效果,用栈只是为了获取单趟排序的区间,单趟排序不变

//非递归快排
void QuickSortNonR(int* a, int begin, int end)
{
	Stack st;
	StackInit(&st);
	StackPush(&st, begin);
	StackPush(&st, end);
	while (!StackEmpty(&st))
	{
		int right = StackTop(&st);
		StackPop(&st);
		int left = StackTop(&st);
		StackPop(&st);
		int key = PartSort(a, left, right);
		//key左边有区间就入栈
		if (left < key - 1)
		{
			StackPush(&st, left);
			StackPush(&st, key - 1);
		}
		//key右边有数据就入栈
		if (right > key + 1)
		{
			StackPush(&st, key + 1);
			StackPush(&st, right);
		}
	}
	StackDestroy(&st);
}

💡复杂度分析

我们知道快排的理想情况就是每次key放在中间的位置,那么理想状态下时间复杂度为O(N*log(N))

但是这毕竟是理想状况,如果数组变成逆序有序,时间复杂度直接变成O(N^2)

快排的优化❗️❗️

1.三数取中

针对O(N^2)这类情况就有了三数取中的方法
大致意思就是比较最左、最右、中间的三个数,把大小居中的数当key

int GetMidIndex(int* a, int left, int right)
{
	int mid = (left + right) >> 1;
	if (a[left] < a[right])
	{
		if (a[mid] < a[left])
		{
			return left;
		}
		else if (a[mid] > a[right])
		{
			return right;
		}
		else
		{
			return mid;
		}
	}
	else
	{
		if (a[mid] < a[right])
		{
			return right;
		}
		else if (a[mid] > a[left])
		{
			return left;
		}
		else
		{
			return mid;
		}
	}
}

2.小区间优化

我们可以发现当我们往下递归时,随着深度增加,每一层的递归次数都以平方的速度增加,越往下就递归越多次。
为了减少最后几层的递归次数,当要递归数组长度小于某个值的时候,就采取非快排算法,小区间优化若是使用得当的话,会在一定程度上加快快速排序的效率,而且待排序列的长度越长,该效果越明显。

void QuickSort(int* a, int begin, int end)
{
	//中间没有元素就返回
	if (begin >= end)
	{
		return;
	}
	if(end - begin > 10)
	{ 
		int keyi = PartSort2(a, begin, end);
		QuickSort(a, begin, keyi - 1);
		QuickSort(a, keyi + 1, end);
	}
	else
	{
		//a + begin 是要插入排序的开始位置
		//end - begin 是长度  +1就是元素个数
		InSertSort(a + begin, end - begin + 1);
	}
}

⑦🌞归并排序

递归算法

在这里插入图片描述

思想:
将两个有序数组合并,得到一个整体有序的数组

在这里插入图片描述
但是归并的前提是有序数组,得到有序数组的方法就是当数组只有一个元素就认为是有序

在这里插入图片描述
创建一个等长的临时数组,在临时数组归并,归并好后再拷贝回原数组

void _MergeSort(int* a, int* tmp, int left, int right)
{
	if (left >= right)
	{
		return;
	}
	//注意中间下标求法
	int mid = (left + right) >> 1;
	//对左序列归并
	_MergeSort(a, tmp, left, mid);
	//对右序列归并
	_MergeSort(a, tmp, mid + 1, right);
	int begin1 = left, end1 = mid;
	int begin2 = mid + 1, end2 = right;
	//用i记录放入tmp的位置
	int i = left;
	//归并两个序列放入tmp中
	while (begin1 <= end1 && begin2 <= end2)
	{
		if (a[begin1] < a[begin2])
		{
			tmp[i++] = a[begin1++];
		}
		else
		{
			tmp[i++] = a[begin2++];
		}
	}
	while (begin2 <= end2)
	{
		tmp[i++] = a[begin2++];
	}
	while (begin1 <= end1)
	{
		tmp[i++] = a