【数据结构】手撕排序算法(上)—— 插入排序(直接插入、希尔) 选择排序(直接选择、堆排序)

1. 排序的概念及意义

1.1 排序的概念

排序:所谓排序,就是使一串记录按照其中的某个或某个关键字的大小,递增或递减的排列起来的操作。
稳定性:假定在待排序的记录序列中,存在多个具有相同的关键字的记录,若经过排序,这些记录的相对次序保不变。即在原序列中,r[i] = r[j],且 r[i]在 r[j]之前,而在排序后的序列中,r[i]容在r[j]之前,则称这种排序算法是稳定的;否则称为不稳定的。
内部排序:数据元素全部放在内存中的排序。
外部排序:数据元素太多不能同时放在内存中,根据排序过程的要求不能在内存之外之间移动数据的排序。

1.2 常见的排序算法

在这里插入图片描述

2. 插入排序

2.1 基本思想

直接插入排序是一种简单的插入算法。其基本思想是:把待排序的记录按其关键码值的大小逐个插入到一个已经排好的有序序列中,知道所有的记录插入完为止,得到一个新的有序序列。实际中我们玩扑克牌时,就用了插入排序的算法。

2.2 直接插入排序

在这里插入图片描述

总结:

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

在这里插入图片描述

//直接插入排序
void InsertSort(int* a, int n)
{
	//[0,end]有序,end + 1位置的值插入到[0,end],使[0,end+1]也有序
	for (int i = 0; i < n - 1; i++)
	{
		int end = i;
		int tmp = a[end + 1];//tmp是从后面那一个要排序到前面的那个数
		while (end >= 0)
		{
			if (a[end] > tmp)
			{
				a[end + 1] = a[end];
				--end;
			}
			else
			{
				break;//找到比tmp小的值的位置了
			}
		}
		a[end + 1] = tmp;//把tmp插到比它小的后一个位置
	}
}
  • 时间复杂度:O(N^2)
  • 什么情况下最坏?逆序 1+2+3+…+n-1
  • 什么情况下最好? 顺序 O(N)

2.3 希尔排序

希尔排序又称缩小增量法。希尔排序的基本思想是:先选定一整数,把待排序文件中所有记录分成个组,所有距离的记录分在同一个组内,并对每一组的记录进行排序。然后,取重复上述分组和排序的工作。当达到=1时,所有记录再统一内排序。(=1时,相当于直接插入排序)

在这里插入图片描述

总结:

  1. 希尔排序是对直接插入排序的优化
  2. 当 gap > 1时都是预排序,目的是让数组更接近有序。当 gap = = 1时,数组已经接近有序了,这时候就会很快。这样整体而言,可以达到优化的效果。我们实现后可以进行性能测试的对比。
  3. 希尔排序的时间复杂度不好计算,需要进行推导,推导出来平均的时间复杂度为:O(N^1.3 ~ N^2)
  4. 稳定性:不稳定

在这里插入图片描述

  • 多组间隔为gap的预排序,gap由大到小
  • gap越大,大的数可以越快的到后面,小的数可以越快的到前面
  • gap越大,预排越不接近有序
  • gap越小,越接近有序
  • gap == 1 就是直接插入排序
void ShellSort(int* a, int n)
{
	int gap = n;
	while (gap > 1)
	{
		//gap = gap / 2; //时间复杂度:log2N 以2为底 N为对数
		gap = gap / 3 + 1;//时间复杂度:log3N 以3为底 N为对数
		// +1 的目的是为了让最后一次gap == 1
		//把间隔为gap的多组数据同时排
		for (int i = 0; i < n - gap; i++)
		{
			int end = i;
			int tmp = a[end + gap];
			while (end >= 0)
			{
				if (a[end] > tmp)
				{
					a[end + gap] = a[end];
					end = end - gap;
				}
				else
				{
					break;
				}
			}
			a[end + gap] = tmp;
		}
	}
}
  • 平均时间复杂度:O(N^1.3)
  • 最坏:O(N^2)
  • 最好:O(N * logN)

3. 选择排序

3.1 基本思想

每一次从待排序的数据元素中选出最小(或最大)的一个元素,存放在序列的起始位置,直到全部待排序的数据元素排完。

3.2 直接选择排序

  • 在元素集合array[i] ~array[n-1] 中选择关键码最大(最小)的元素
  • 若它不是这组元素中的最后一个(第一个)元素,则将它与这组元素中的最后一个(第一个)元素交换
  • 在剩余的 array[i] ~ array[n-2] ( array[i + 1] ~ array[n - 1] ) 集合中,重复上述步骤,直到集合剩余一个元素

在这里插入图片描述
总结:

  1. 直接选择排序思想非常好理解,但是效率很不好,实际中很少使用
  2. 时间复杂度:O(N^2)
  3. 空间复杂度:O(1)
  4. 稳定性:不稳定
void SelectSort(int* a, int n)
{
	for (int i = 0; i < n - 1; i++)//进行n-1趟选择
	{
		int min = i;//存放最小值下标
		for (int j = i + 1; j <= n; j++)
		{
			if (a[min] > a[j])
			{
				min = j;
			}
		}
		Swap(&a[i], &a[min]);
	}
}

我们再来改进一下,让找最小值和最大值同时进行。

void SelectSort(int* a, int n)
{
	int begin = 0;//数组第一个元素
	int end = n - 1;//数组最后一个元素
	while (begin < end)
	{
		int mini = begin;//记录存放最小数的下标
		int maxi = begin;//记录存放最大数的下标
		for (int i = begin; i <= end; i++)
		{
			if (a[i] < a[mini])
			{
				mini = i;//此时a[i]比最小数还要小,把a[i]的下标i赋给mini,a[mini]存放的就是最小数
			}
			if (a[i] > a[maxi])
			{
				maxi = i;//此时a[i]比最大数还要大,把a[i]的下标i赋给maxi,a[maxi]存放的就是最大数
			}
		}
		Swap(&a[begin], &a[mini]);
		//如果begin根maxi重叠了,需要修正一下maxi的位置
		if (begin == maxi)
		{
			maxi = mini;
		}
		Swap(&a[maxi], &a[end]);
		++begin;
		--end;
	}
}

直接选择排序无论是最优、最差时间复杂度,还是平均复杂度,都是O(N^2)

3.3 堆排序

完全二叉树的顺序存储
在这里插入图片描述

堆的逻辑结构是一棵完全二叉树
堆的物理结构是一个数组
通过下标可以找到父子节点关系:
leftchild = parent * 2 + 1
rightchild = parent * 2 + 2
parent = (child - 1) / 2

堆有两个特性:

  1. 结构性:用数组表示的完全二叉树
  2. 有序性:①最大堆,也称为大顶堆-》树中的所有父亲节点都大于等于孩子 ②最小堆,也称为小顶堆-》树中的所有父亲节点都小于等于孩子

例子:

(101,88,46,70,34,39,45,58,66,10)就是一个堆,他的每一个父节点都大于等于孩子,是一个大堆。
在这里插入图片描述

那么,我们如何把数组构建成一个堆呢?(最小堆)

  • 向下调整算法

前提:左右子树必须都是小堆

从根节点开始,选出左右孩子中较小的那一个,跟父亲比较,如果比父亲小就交换,然后继续往下调,调到叶子节点就终止。

在这里插入图片描述

切记:升序我们是建大堆

void AdjustDown(int* a, int n, int root)
{
	int parent = root;
	int child = parent * 2 + 1;//默认是左孩子
	while (child < n)
	{
		//1.选出左右孩子中大的那一个
		if (child + 1 < n && a[child + 1] > a[child])
		{
			child += 1;
		}
		//2.父节点与左右孩子较大的那个比较
		if (a[child] > a[parent])
		{
			Swap(&a[child], &a[parent]);
			parent = child;
			child = parent * 2 + 1;
		}
		else
		{
			break;
		}
	}
}

那么,大多数的左右子树都不是小堆,就不能够直接使用向下调整算法了,怎么办?

我们可以倒着从最后一棵子树开始调,再分析倒着走,叶子不需要调,从倒数最后一个非叶子节点的子树开始调,如图:就是8

在这里插入图片描述

  • 建堆方式:自底向上的建堆方式

在这里插入图片描述

我们在排升序的时候,一定是建的大堆,为什么不建小堆呢?
如果,我们建的是小堆,最小数在堆顶,已经被选出来了。那么在剩下的数中再去选数,但是剩下的树结构已经乱了,需要重新建堆才能选出下一个数,建堆的时间复杂度是O(N),那么这样不是不可以,但是堆排序就没有效率优势了。

在这里插入图片描述
总结:

  1. 堆排序使用堆来选数,效率高了很多
  2. 时间复杂度:O(N * logN)
  3. 空间复杂度:O(1)
  4. 稳定性:不稳定
void AdjustDown(int* a, int n, int root)
{
	int parent = root;
	int child = parent * 2 + 1;//默认是左孩子
	while (child < n)
	{
		//1.选出左右孩子中大的那一个
		if (child + 1 < n && a[child + 1] > a[child])
		{
			child += 1;
		}
		//2.父节点与左右孩子较大的那个比较
		if (a[child] > a[parent])
		{
			Swap(&a[child], &a[parent]);
			parent = child;
			child = parent * 2 + 1;
		}
		else
		{
			break;
		}
	}
}

void HeapSort(int* a, int n)
{
	//建堆  时间复杂度:O(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;
	}
}
  • 2
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 3
    评论
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值