八大排序(详细分析+动图演示)

直接插入排序

动图演示:
在这里插入图片描述
基本思想:

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

在这里插入图片描述
代码实现:

void InsertSort(int* a, int n)
{
	for (int i = 0; i < n - 1; i++)
	{
		int end = i;// 记录有序数列的最后一个元素的下标
		int tmp = a[end + 1];// 记录要插入的数据
		while (end >= 0)
		{
			// 寻找比tmp小的数据
			if (a[end]>tmp)
			{
				a[end + 1] = a[end];
				end--;
			}
			else
			{
				break;
			}		
		}
		// 将tmp插入比tmp小的数据后或首位
		a[end + 1] = tmp;
	}
}

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

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

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

希尔排序

动图演示:
在这里插入图片描述
基本思想:

先选定一个小于N的数gap,把待排序文件中所有记录分成个组,所有距离为gap的元素分在同一组内,并对每一组内的记录进行排序。然后将gap/3+1的值赋给gap,重复上述分组和排序的工作。当到达=1时,所有记录在统一组内排好序。

在这里插入图片描述

先令gap等于5,此时距离为5的元素分为一组,分别对每一组进行直接插入排序
在这里插入图片描述
再令gap=gap/3+1=2,此时距离为2的元素分为一组,对每一组进行插入排序
在这里插入图片描述
再进行上述操作,此时gap为1,所有数据被分为一组,对该组数据进行插入排序
在这里插入图片描述
代码实现:

void ShellSort(int* a, int n)
{
	int gap = n;
	while (gap > 1)
	{
		gap = gap / 3 + 1;// 减少排序组数
		// 一趟直接排序
		for (int i = 0; i < n - gap; i++)
		{
			int end = i;
			int tmp = a[end + gap];
			while (end >= 0)
			{
				if (tmp < a[end])
				{
					a[end + gap] = a[end];
					end -= gap;
				}
				else
				{
					break;
				}
			}
			a[end + gap] = tmp;
		}
	}
}

希尔排序的特性总结:

  1. 希尔排序是对直接插入排序的优化
  2. 当gap > 1时都是预排序,目的是让数组更接近于有序。当gap == 1时,数组已经接近有序的了,这样就会很快.
  3. 通过大量的试验统计,时间复杂度大约为O(N^1.3)
  4. 稳定性:不稳定

直接选择排序

动图演示:
在这里插入图片描述
基本思想:

  1. 在元素集合中选出最大(小)的数据元素
  2. 若它不是这组元素中的最后一个(第一个)元素,则将它与这组元素中的最后一个(第一个)元素交换
  3. 在剩余的集合中,重复上述步骤,直到集合剩余1个元素

代码实现:

void SelectSort2(int* a, int n)
{
	int i = 0,j=0;	
	for (i = 0; i < n-1; i++)
	{
		int min = i;// 取出第一个元素下标
		// 找到最小值的下标
		for (j = i + 1; j < n; j++)
		{
			if (a[j]<a[min])
			{
				min = j;
			}
		}
		Swap(&a[min], &a[i]);
	}
}

为了提高效率,我们可以一次遍历找到最大值和最小值

代码:

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

void SelectSort(int* a, int n)
{
	int begin = 0;
	int end = n - 1;
	while (begin < end)
	{
		// 记录最大值和最小值坐标
		int min = begin, max = end;
		// 找出最大和最小值的坐标
		for (int i = begin; i <= end; i++)
		{
			if (a[min]>a[i])
				min = i;

			if (a[max] < a[i])
				max = i;
		}
		Swap(&a[begin], &a[min]);
		// 防止最大值位于序列开头,被最小值交换
		if (max == begin)
		{
			max = min;
		}
		Swap(&a[end], &a[max]);
		begin++;
		end--;
	}
}

直接选择排序的特性总结:

  1. 直接选择排序思考非常好理解,但是效率不是很好。实际中很少使用
  2. 时间复杂度:O(N^2)
  3. 空间复杂度:O(1)
  4. 稳定性:不稳定

堆排序

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

基本思想:

要想使用堆排序首先要建堆,注意降序需要建大堆,升序要建小堆

如何建堆呢?
找到最后一个非叶结点,将该结点从后往前依次进行下述操作:(这里以建大堆为例)将传入的结点当做父节点,比较其两个子节点,将子节点与父节点比较,如果比父结点大就交换,并将原先子节点的位置当成父节点,重复上述操作。如果满足父结点比子结点大就结束操作。

代码如下:

// 向下调整
void AdjustDown(int* a, int n, int parent)
{
	int child = parent * 2 + 1;
	while (child < n)
	{
		if (child + 1 < n&&a[child] < a[child + 1])
		{
			child++;
		}

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

void test()
{
	// 从最后一个非叶子结点先前调整
	for (int i = (n - 1 - 1) / 2; i >= 0; i--)
	{
		AdjustDown(a,n,i);
	}
}

建好堆,下面就要进行排序(以升序为例),这里以图解加文字的方式帮助大家理解

在这里插入图片描述

代码实现:

void AdjustDown(int* a, int n, int parent)
{
	int child = parent * 2 + 1;// 找子结点
	while (child < n)
	{
		// 找出子结点中较大值
		if (child + 1 < n&&a[child] < a[child + 1])
		{
			child++;
		}

		if (a[child]>a[parent])
		{
			Swap(&a[child], &a[parent]);
			parent = child;
			child = parent * 2 + 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 > 0)
	{
		Swap(&a[0], &a[end]);
		AdjustDown(a, end, 0);
		end--;
	}
}

堆排序的特性总结:

  1. 堆排序使用堆来选数,效率就高了很多
  2. 时间复杂度:O(N*logN)
  3. 空间复杂度:O(1)
  4. 稳定性:不稳定

冒泡排序

动图演示:

在这里插入图片描述

基本思想:

相邻两个元素比较,不满足条件就交换,直到最后一个元素

代码实现:

void BubbleSort(int* a, int n)
{
	for (int end = n; end > 0; --end)
	{
		int exchange = 1;
		//一趟冒泡排序,把最大值放在结尾
		for (int i = 1; i < end; i++)
		{
			if (a[i - 1]>a[i])
			{
				Swap(&a[i - 1], &a[i]);
				exchange = 0;
			}
		}
		//一趟冒泡排序中没有交换就退出
		if (exchange)
			break;
	}
}

冒泡排序的特性总结:

  1. 冒泡排序是一种非常容易理解的排序
  2. 时间复杂度:O(N^2)
  3. 空间复杂度:O(1)
  4. 稳定性:稳定

计数排序

基本思路:

计数排序不是通过比较数据的大小来排序的,而是通过统计数组中相同元素个数,然后再将元素按顺序返回给原数组中。

在这里插入图片描述

当arr数组中元素很大时再从零开始会造成很大的空间浪费,如数组元素为101,103 ,104,106,我们总不能开辟106个空间吧。为了摆脱上述情况我们需要用到映射:先遍历数组找到最小值min和最大值max,把[min,max]范围映射到范围[0,max-min+1]上。

代码实现:

void CountSort(int* a, int n)
{
	int min = a[0];
	int max = a[0];
	//选出数组中最大,最小值
	for (int i = 1; i < n; i++)
	{
		if (a[i] < min)
			min = a[i];

		if (a[i]>max)
			max = a[i];
	}

	int range = max - min + 1;
	int* count = (int*)calloc(range, sizeof(int));

	//统计次数
	for (int i = 0; i < n; i++)
	{
		count[a[i] - min]++;
	}

	//排序
	int i = 0;
	for (int j = 0; j < range; j++)
	{
		while (count[j]--)
		{
			a[i++] = j + min;
		}
	}
}

计数排序的特性总结:

  1. 计数排序在数据范围集中时,效率很高,但是适用范围及场景有限
  2. 时间复杂度:O(MAX(N,范围))
  3. 空间复杂度:O(范围)
  4. 稳定性:稳定

快排

hoare版本

一趟操作的动图演示:
在这里插入图片描述

基本思路:

  1. 先选出一个key(一般是最左边)
  2. 再创建left和right分别代表数组第一个数据和最后一个数据
  3. 让right先往前走,当right找到比key小的数据时,left开始往后走,left找到比key大的数据时停下,交换right和left的数据,再重复之前操作直到left和right相遇,这时再交换key和left。
  4. 通过前几步操作后key的前面都是比它小的元素,后面都是比它大的元素,这时再将key左右两边分别进行该操作,递归下去直到左右序列只有一个数据,或是左右序列不存在完成排序

代码实现:

int PartSort(int* a, int left, int right)
{
	int keyi = left;
	while (left < right)
	{
		//左边找比keyi小
		while (left<right&&a[right]>a[keyi])
		{
			right--;
		}
		//右边找keyi大
		while (left < right&&a[left] < a[keyi])
		{
			left++;
		}
		//交换找到的大小
		Swap(&a[right], &a[left]);
	}
	Swap(&a[left], &a[keyi]);

	return left;
}
void QuickSort(int* a, int left, int right)
{
	if (left >= right)
		return;

	int keyi = PartSort(a, left, right);
	QuickSort(a, left, keyi - 1);
	QuickSort(a, keyi + 1, right);
}

挖坑法

一趟操作的动图演示:
在这里插入图片描述
基本思路:

  1. 取出一个数据key(一般是第一个)作为坑位
  2. 创建left和right分别代表数组第一个数据和最后一个数据
  3. 让right先往前走,当right找到比key小的数据时,取出该数据放入原坑位中,并让其形成新的坑位,接着让left往后走,当left找到比key大的数据时,取出该数据放入right形成的坑位中,自己形成新坑。直到left和right相遇,将数据key填入最后形成的坑位中
  4. key左右两边分别进行该操作,递归下去直到左右序列只有一个数据,或是左右序列不存在完成排序
int PartSort(int* a, int left, int right)
{
	int key = a[left];
	int hole = left;
	while (left < right)
	{
		//右边找小,填坑,自己形成新坑
		while (left<right&&a[right]>key)
		{
			right--;
		}
		a[hole] = a[right];
		hole = right;
		//左边找大,填坑,自己形成新坑
		while (left < right&&a[left] < key)
		{
			left++;
		}
		a[hole] = a[left];
		hole = left;
	}
	a[hole] = key;
	return hole;
}
void QuickSort(int* a, int left, int right)
{
	if (left >= right)
		return;

	int keyi = PartSort(a, left, right);
	QuickSort(a, left, keyi - 1);
	QuickSort(a, keyi + 1, right);
}

前后指针法

一趟操作的动图演示:
在这里插入图片描述
基本思路:

  1. 选出一个元素key(一般是第一个),再创建两个指针prev,cur
  2. 起始时,prev指针指向序列开头,cur指针指向prev+1.
  3. cur开始移动,直到cur指向的元素比prev小,prev往后移动一位,如果prev指向的位置不同于cur指向的,交换两个位置上的元素,直到cur到数组末尾,最后交换key和prev指向的数据
  4. key左右两边分别进行该操作,递归下去直到左右序列只有一个数据,或是左右序列不存在完成排序

代码实现:

int PartSort(int* a, int left, int right)
{
	int keyi = left;
	int prev = left;
	int cur = prev + 1;

	while (cur <= right)
	{
		// 当cur指向数据小于keyi指向数据且prev
		// 不同于cur时交换prev和cur指向的数据
		if (a[cur] < a[keyi] && ++prev != cur)
			Swap(&a[prev], &a[cur]);

		++cur;
	}
	Swap(&a[keyi], &a[prev]);

	return prev;
}
void QuickSort(int* a, int left, int right)
{
	if (left >= right)
		return;

	int keyi = PartSort(a, left, right);
	QuickSort(a, left, keyi - 1);
	QuickSort(a, keyi + 1, right);
}

非递归版

基本思路:

将递归改为非递归一般有两种方法,一是循环,二是利用数据结构,这里利用了数据结构——栈

先将数组左右下标入栈,接下来每趟排序从栈中取出下标,排序完成后将key左半部分和右半部分下标入栈,重复上述操作直到左右序列只有一个数据,或是左右序列不存在取消入栈。当栈为空时结束排序。

代码实现:

void QuickSortNonR(int* a, int left, int right)
{
	ST st;
	StackInit(&st);
	//将左右下标入堆
	StackPush(&st, right);
	StackPush(&st, left);

	while (!StackEmpty(&st))
	{
		//取出堆顶数据作为左,右下标
		int begin = StackTop(&st);
		StackPop(&st);

		int end = StackTop(&st);
		StackPop(&st);

		int keyi = PartSort3(a, begin, end);
		//将右半部分入堆
		if (keyi + 1 < end)
		{
			StackPush(&st, end);
			StackPush(&st, keyi + 1);
		}
		//将左半部分入堆
		if (begin < keyi - 1)
		{
			StackPush(&st, keyi - 1);
			StackPush(&st, begin);
		}
	}

	StackDestroy(&st);
}

快排优化

当key的值为最大值或最小值时,一趟排序下来只能确定最大或最小值,快排的优势得不到体现,所以我们确定的key最好为序列较为中间值,这里就引入了三数取中法。

三数取中法就是比较最左边,中间和最右边,取出三数中间数与最左边的数交换

代码如下:

int GetMidIndex(int* a,int left,int right)
{
	//取中间数
	int mid = left + (right - left) / 2;
	//比较左,中,右,返回中间值
	if (a[left] < a[mid])
	{
		if (a[mid] < a[right])
		{
			return mid;
		}
		else if (a[left] < a[right])
		{
			return right;
		}
		else
		{
			return left;
		}
	}
	else
	{
		if (a[mid] > a[right])
		{
			return mid;
		}
		else if (a[right] < a[left])
		{
			return right;
		}
		else
		{
			return left;
		}
	}
}

快速排序的特性总结:

  1. 快速排序整体的综合性能和使用场景都是比较好的,所以才敢叫快速排序
  2. 时间复杂度:O(N*logN)
  3. 空间复杂度:O(logN)
  4. 稳定性:不稳定

归并排序

递归版

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

归并排序是建立在归并操作上的一种有效的排序算法,该算法是采用分治法的一个非常典型的应用。将已有序的子序列合并,得到完全有序的序列;即先使每个子序列有序,再使子序列段间有序。

在这里插入图片描述
代码实现:

归并排序思路不难,开辟和原数组相同大小的空间,将原数组中的数据按顺序拷贝到开辟的空间,再将数据拷贝回来

void _MergeSort(int* a, int left, int right, int* tmp)
{
	if (left >= right)
		return;

	int mid = (right + left) / 2;
	_MergeSort(a, left, mid, tmp);
	_MergeSort(a, mid + 1, right, tmp);
	// 归并
	int begin1 = left, end1 = mid;
	int begin2 = mid + 1, end2 = right;
	int index = left;
	// 将a数组中数据按顺序拷贝到tmp数组中
	while (begin1 <= end1&&begin2 <= end2)
	{
		if (a[begin1] < a[begin2])
		{
			tmp[index++] = a[begin1++];
		}
		else
		{
			tmp[index++] = a[begin2++];
		}
	}

	while (begin1 <= end1)
	{
		tmp[index++] = a[begin1++];
	}

	while (begin2 <= end2)
	{
		tmp[index++] = a[begin2++];
	}

	// 将归并后的数据拷贝回原数组
	for (int i = left; i <= right; ++i)
	{
		a[i] = tmp[i];
	}
}

非递归版

归并排序的非递归版不需要借助栈来完成,通过自身循环就能达到排序的目的,但是要注意边界的特殊情况
在这里插入图片描述

特殊情况一:
在这里插入图片描述

当最后一个小组的第二个区间元素不足gap个时,需要控制该区间的边界,让其右边界为n-1

特殊情况二:

在这里插入图片描述

当最后一个小组的第二个区间不存在时,不需要对该区间进行合并

特殊情况三:

在这里插入图片描述
当最后一个小组的第一个区间元素不足gap个时,需要控制该区间的边界,让其右边界为n-1,和特殊情况一相似

代码实现:

void MergeSortNonR(int* a, int n)
{
	int* tmp = (int*)malloc(sizeof(int)*n);
	int grounpNum = 1;
	while (grounpNum < n)
	{
		for (int i = 0; i < n; i += 2 * grounpNum)
		{
			//归并
			int begin1 = i, end1 = i + grounpNum - 1;
			int begin2 = i + grounpNum, end2 = i + grounpNum * 2 - 1;
			int index = begin1;

			//[begin2,end2]不存在修正为一个不存在的区间
			if (begin2 >= n)
			{
				begin2 = n + 1;
				end2 = n;
			}

			//end1越界,修正一下
			if (end1 >= n)
			{
				end1 = n - 1;
			}

			//end2越界,修正一下
			if (end2 >= n)
			{
				end2 = n - 1;
			}
			// 将a数组中数据按顺序拷贝到tmp数组中
			while (begin1 <= end1&&begin2 <= end2)
			{
				if (a[begin1] < a[begin2])
				{
					tmp[index++] = a[begin1++];
				}
				else
				{
					tmp[index++] = a[begin2++];
				}
			}

			while (begin1 <= end1)
			{
				tmp[index++] = a[begin1++];
			}

			while (begin2 <= end2)
			{
				tmp[index++] = a[begin2++];
			}
		}

		// 将归并后的数据拷贝回原数组
		for (int i = 0; i < n; ++i)
		{
			a[i] = tmp[i];
		}
		grounpNum *= 2;
	}
	free(tmp);
}

归并排序的特性总结:

  1. 归并的缺点在于需要O(N)的空间复杂度
  2. 时间复杂度:O(N*logN)
  3. 空间复杂度:O(N)
  4. 稳定性:稳定

排序的时间复杂度、空间复杂度、稳定性总结

在这里插入图片描述

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值