首页 > 编程语言 >手撕排序算法之选择排序

手撕排序算法之选择排序

时间:2023-04-18 20:37:30浏览次数:30  
标签:结点 下标 int 孩子 元素 选择 算法 排序

前言

选择排序算法有两种,一种是直接选择排序,另一种是堆排序。他们的算法思想都离不开选择二字,其中堆排序比选择排序的更加快捷。

一、直接选择排序

1.排序思想

以升序为例

遍历数组,找到数组中最小的元素放到最前面,在从剩下的数组元素中找到最小的,依次放入,当只剩下一个元素时,已经排好。

2.直接选择排序代码(每次只选一个)

#define _CRT_SECURE_NO_WARNINGS
#include<stdio.h>
void Swap(int* p1, int* p2)
{
	int tem = *p1;
	*p1 = *p2;
	*p2 = tem;
}
void SelectSort(int* a, int  n)
{
	int i = 0, min = 0, j = 0;
	for (i = 0; i < n - 1; i++)
	{
  min = i;//假设该位置的元素是最小的
  for (j = i + 1; j < n; j++)
  {
  	if (a[min] > a[j])//选出最小元素的下标
  	{
    min = j;
  	}
  }
  if (min != i)
  //如果原本位置的数据不是最小,则根据选出最小元素的下标与该位置元素交换
  {
  	int tem = a[i];
  	a[i] = a[min];
  	a[min] = tem;
  }
	}
}
int main()
{
	int arr[5] = { 2,3,1,4,5 };
	SelectSort(arr, 5);
	for (int i = 0; i < 5; i++)
	{
  printf("%d ", arr[i]);
	}
}

调试结果:

手撕排序算法之选择排序_直接选择排序

对于直接选择排序,我们可以略加优化,使其每次能选出两个值,分别是最小和最大,放入数组首和数组尾。

3.直接选择排序代码(每次选两个)

#define _CRT_SECURE_NO_WARNINGS
#include<stdio.h>
void Swap(int* p1, int* p2)
{
	int tem = *p1;
	*p1 = *p2;
	*p2 = tem;
}
void SelectSort(int* a, int n)
{
	int begin = 0;//未排序区间起始下标
	int end = n - 1;//未排序区间终点下标
	while (begin < end)//排序区间左下标小于右下标
	{
  int max = begin, min = begin;
  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)
  //如果数组中的最大值下标恰好是第一个(即与起始位置下标重合,需要进行修正)
  //这是因为在确定最大值与最小值下标后,先把最小值放到了
  //数组首的位置,此时最大值和最小值发生了交换,但是最大值对应的下标却没有改变,
  //此时最大值对应的下标应该是min
  {
  max=min;
  }
  Swap(&a[max], &a[end]);
  //根据最大元素的下标,将最大元素放在未排序区间的终点
  begin++;//选出当前区间最小值并放于起点后,区间起点位置后移
  end--;//选出当前区间最大值并放于终点后,区间起点位置前移
	}
}
int main()
{
	int arr[5] = { 2,3,1,4,5 };
	SelectSort(arr, 5);
	for (int i = 0; i < 5; i++)
	{
  printf("%d ", arr[i]);
	}
}

调试结果:

手撕排序算法之选择排序_堆排序_02


二、堆排序

1.什么是堆

在讲堆排序之前,我们需要明白所谓的堆是什么。

我们知道土堆,谷堆,它们都是近似于正金字塔的形状,那么堆是不是也是这样呢?

如下图是一颗完全二叉树

手撕排序算法之选择排序_结点_03

它的层序遍历顺序是:100,82,74,69,73,56,65,33,47,53

堆:将这样一颗按照层序遍历顺序的完全二叉树依次放入数组中,并且该完全二叉树的每个结点的值都大于等于该结点左右孩子的值(或者每个结点的值都小于等于该结点左右孩子的值)就叫做堆。

手撕排序算法之选择排序_数组_04

2.堆的两种类型

堆有两种类型,最大堆和最小堆(或者叫大顶堆,小顶堆,以下统称为最大堆和最小堆)。

最大堆:所有结点的值都大于等于该结点左右孩子的值

最小堆:所有结点的值都小于等于该结点左右孩子的值

3.堆的结构

堆的逻辑结构是一颗二叉树,是我们想象构造出来的,堆的物理结构是一个数组,是实际存储数据用的结构。

堆有两种特性:

1.结构性:该特性指的是堆是用数组表示的完全二叉树

2.有序性:即每个结点的值都不小于(或者不大于)其孩子的值

4.堆与完全二叉树的联系

在上文我们解释了堆的本质就是数组,存放的是按层序遍历依次放入的完全二叉树,数组下标依次对应着二叉树结点的数值

对于完全二叉树,父节点与孩子结点有如下关系

lchild=parent*2+1,即左孩子结点的下标等于该结点的父节点的下标乘以2再加1

rchild=parent*2+2,即右孩子结点的下标等于该结点的父节点的下标乘以2再加2

parent=(child-1)/2,即父节点的下标等于他的左孩子或者右孩子结点的下标减去1后再除以2

举例说明:

手撕排序算法之选择排序_数组_05

手撕排序算法之选择排序_结点_03

我们把下标为0的结点当作父结点,0*2+1=1,0*2+2=2,得到的分别是他的左右孩子结点的下标,再例如,我们将下标为4的结点当作父节点,4*2+1=9,得到的恰好是它的左孩子结点的下标。接着,下标为7和8的两个结点是下标为3结点的的两个孩子结点,(7-1)/2=3,(7-2)/2=3,得到的也正是这两个结点的父结点的下标。

5.堆排序算法思想

以建最大堆排升序为例

第一步:建堆

什么是建堆呢?将父结点位置的元素与左右孩子中较大元素作比较,如果父结点较小,则交换两者的值,并且使父结点指向较大值所在的下标,孩子结点指向新父结点的左孩子。一直到整棵树都满足堆的要求。这个比较的过程也被称为向下调整法,向下调整有个很重要的前提,那就是父结点的左右子树必须都是最大堆

那么比较过程会在什么时候停止呢?有两种情况:一是在比较的过程中,父节点的值更大,此时无需继续比较,二是遇到叶子结点,因为叶子结点没有孩子结点,所以也无需比较

我们会发现,利用堆排序进行排序时,建堆的过程只能从最后一个有孩子结点的父结点开始依次向前开始向下调整的过程,因为只有这样才能满足向下调整法的要求(初始数据元素是乱序的,建堆的过程是将最大或者最小的值放到根结点的过程,父结点指向的位置当作根结点,该根结点的左右子树必须满足堆的要求,从后向前其实就是从最底层开始调整,底层满足要求了,才能从上层开始,向下进行调整)

我们来举例说明:

手撕排序算法之选择排序_堆排序_07

我们要把上述10个数据按照堆排序进行升序,我们把这10个数据当成一颗完全二叉树按层序遍历顺序浏览的数据,去还原出这颗树,第一个数是根节点,接下来两个数据是根节点的左右孩子,再4个数据依次是左右孩子结点的左右孩子,最后绘制结果如下

手撕排序算法之选择排序_结点_08

在建堆过程中,把最后一个有孩子的父结点当作根结点,使用向下调整法使子树满足堆的要求(此时该根结点的左子树或者右子树至多有一个元素,一定满足向下调整法的要求),调整完成后,父结点向前移动,将新的父结点指向的位置当作根结点,利用向下调整法使新的子树也满足堆的要求,直到整棵树逐渐变为最大堆。

我们可以通过元素个数,确定最后一个元素的下标,求得该元素的父结点的下标。例如,上述有10个数据,(10-1-1)/2=4,此时的父结点是下标为4的数据,它的孩子结点是下标为9的数据,如下图

手撕排序算法之选择排序_直接选择排序_09

父结点的值小于它的孩子结点的值,交换两者的数值,并使夫结点指向与他交换数值数据的下标,如下图

手撕排序算法之选择排序_数组_10

此时已经来到叶子结点,因此调整(比较)停止

此时,从后向前调整,新的父结点下标变为了3,孩子结点指向下标7所在位置

如下图,

手撕排序算法之选择排序_结点_11

以下标为3,7,8构成的堆状结构满足最大堆的要求,因此无需比较,新的父节点指向下标3的位置,孩子结点指向新的父结点的左孩子,如下图

手撕排序算法之选择排序_直接选择排序_12

以下标为2的结点为根节点的子树同样满足最大堆的要求,因此也无需调整,新的父结点下标再次--,指向下标1,如下图

手撕排序算法之选择排序_结点_13

将下标为1的位置当作新的根节点,它小于它的孩子结点中的较大值,因此发生交换,父结点指向新的位置

手撕排序算法之选择排序_结点_14

以新的父结点为根结点的子树满足最大堆的要求,因此无需交换,父结点此时指向下标为0的位置,它的左右子树也满足最大堆的要求,开始最后一次的向下调整,如下图

手撕排序算法之选择排序_数组_15

53小于100,发生交换,同时父结点指向下标为1的位置,53小于82,再次发生交换,父结点指向下标为4的位置,53小于73,再次发生交换,此时已经到达叶子结点,交换停止,依次如下图所示

手撕排序算法之选择排序_数组_16

手撕排序算法之选择排序_堆排序_17

手撕排序算法之选择排序_堆排序_18

最终建堆完成后如下:

手撕排序算法之选择排序_直接选择排序_19

手撕排序算法之选择排序_数组_20

第二步:利用堆进行选择排序

当我们建好一个最大堆时,会发现根结点的值是最大的,因此我们将跟结点(下标为0)的值与最后一个数据交换,让最大的值放到最后,然后不用搭理它,再次建堆,使根结点的值再次调整为最大时,再次与倒数第二个数据交换,以此类推,直到整个数组只剩下一个元素,堆排序已经完成。也因此排升序建最大堆,排降序建的是最小堆


6.堆排序代码

#define _CRT_SECURE_NO_WARNINGS
#include<stdio.h>
void Swap(int* p1, int* p2)//交换两个变量的值
{
	int tem = *p1;
	*p1 = *p2;
	*p2 = tem;
}
void AdustDown(int* a, int n, int root)//向下调整
{
	int parent = root;
  //将父结点指向的位置当作根结点,以它为根结点的树当作子树
	int child = parent * 2 + 1;
  //始终指向左孩子
	while (child < n)
  //当孩子结点的下标小于元素个数时,表面还没到叶子结点,调整继续
	{
  if (a[child] < a[child + 1] && (child + 1) < n)
  //因为是建最大堆,因此选出左右孩子中的较大值,并且要保证右孩子存在
  {
  	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 - 2) / 2; i >= 0; i--)
  //i是父结点的下标,从后向前,一直到整棵树的根结点
	{
  AdustDown(a, n,i);
	}
  //循环结束后,数据已经满足最大堆的要求,根结点的元素值最大
	//利用堆选择排序,升序建大堆
	int end = n - 1;//最后一个元素的下标
	while (end > 0)//当只剩下一个元素时,循环才能结束
	{
  Swap(&a[0], &a[end]);
  //将最大值交换到最后一个位置
  AdustDown(a, end, 0);
  //只需要从根结点开始,调整一次,这里end表示当前元素个数
  end--;//每次交换后,最后一个元素的下标减去1
	}
}
int main()
{
	//堆排序,大堆
	int arr[10] = { 2,6,0,9,7,14,18,27,98,5118};
	HeapSort(arr, 10);
	for (int i = 0; i < 10; i++)
	{
		printf("%d ", arr[i]);
	}
}

调试结果:

手撕排序算法之选择排序_直接选择排序_21

标签:结点,下标,int,孩子,元素,选择,算法,排序
From: https://blog.51cto.com/u_15466618/6204023

相关文章

  • 排序算法-基数排序
    基数排序RadixSort1.RadixSort介绍RadixSort属于“分配式排序”(DistributionSort),又称“桶子法”(BucketSort),其是通过比较待排序序列的所有元素的各个位的值,将元素分配至“桶”中,以达到排序的目的。RadixSort是一种效率较高且稳定的排序算法,其是桶排序的扩展。RadixSor......
  • 初次排序算法学习
    直接选择排序:思路:从数组中挑出最小(最大)的数,与数组第一位(最后一位)交换位置,然后依次进行,直到最后两个元素比较完毕为止。实现:声明一个中间变量max,用于存放最大值;声明一个变量m,用于存放最大值对应的序号。外侧循环次数是n-1,n是数组元素个数,意思是挑出n-1个最大值,剩下的自然是最......
  • 基于GOA蚱蜢优化算法的KNN分类器最优特征选择matlab仿真
    1.算法仿真效果matlab2022a仿真结果如下:2.算法涉及理论知识概要蝗虫优化算法(GrasshopperOptimizationAlgorithm,GOA)是一种新型的元启发式算法,由Mirjalili等人于2017年提出。该算法受幼虫和成年蝗虫大范围移动与寻找食物源的聚集行为启发,具有操作参数少,公式简单......
  • 基于决策树算法的病例类型诊断matlab仿真
    1.算法仿真效果matlab2022a仿真结果如下:2.算法涉及理论知识概要ID3算法是一种贪心算法,用来构造决策树。ID3算法起源于概念学习系统(CLS),以信息熵的下降速度为选取测试属性的标准,即在每个节点选取还尚未被用来划分的具有最高信息增益的属性作为划分标准,然后继续这个过程,直到生成......
  • 串的模式匹配(BF算法)
    【问题描述】串的模式匹配算法BF的实现与应用。【输入形式】第一行输入主串s;第二行输入模式串t;输入串中均不包含空格字符。【输出形式】模式串在主串s中的出现的每一个位置序号。若一次都未匹配到,则输出0。【样例输入1】ababcabcacbabab【样例输出1】13612【样例输入2】11111345......
  • 基于GOA蚱蜢优化算法的KNN分类器最优特征选择matlab仿真
    1.算法仿真效果matlab2022a仿真结果如下:     2.算法涉及理论知识概要       蝗虫优化算法(GrasshopperOptimizationAlgorithm,GOA)是一种新型的元启发式算法,由Mirjalili等人于2017年提出。该算法受幼虫和成年蝗虫大范围移动与寻找食物源的聚......
  • 实际问题中用到的算法——递归算法确定插帧顺序
    问题:现在需要给一个视频序列插帧,插帧算法要求每次只能由两帧输入插值得到其中间帧。如果现在需要给一个视频做4倍(或者更高的8,16倍等类似)的插帧,则一个插帧的思路是当前视频每相邻帧之间插入3帧,即:假设插帧前视频帧序号是0,4,8,12…,则插帧时补充相邻帧跨过的3个序号,得到插......
  • 选择私有云解决方案,需要考虑哪些要素
    也许你觉得会花费比较贵,其实这点企业勿需担心,企业目前拥有很多选择很多,相信可以找到一个既适合企业自身业务、又能满足成本效益需求的私有云解决方案。不过,在选择私有云解决方案时,不要忘了迁移至云端的初衷:虚拟化、安全性和开放性。·虚拟化:一个真正的自服务私有云应将所有存储系统......
  • 大数据的快速崛起,离不开数据、区块链和算法的支持
    事实上,自2001年来,大数据已然呈现出爆炸式增长。经过多年发展,大数据的应用已经帮助我们开发了更多、更高端的技术在我们的工作、生活中。与此同时,大数据也衍生出了其他技术,比如人工智能、机器学习等。那么,放眼未来,大数据又将如何开启新征程?1.通过数据,更好赋权这个意思就是我们应该给......
  • 离散型制造企业如何选择MES系统
    随着MES系统越来越被企业所重视,并并被运用到很多不同行业的制造业中。MES对于制造企业来说,其所需要的要求是各不相同的,比如离散型制造企业,该如何去选择MES系统呢?什么是离散型制造企业?离散型制造企业的产品往往是由多个零件经过一系列并不连续的工序的加工最终装配而成。离散型MES......