堆排序 (大根堆)

大根堆的概念

首先 我们来引入大根堆的概念 来让大家方便理解。

  来介绍 父节点 和左右孩子。

啥意思呢    可以这么理解

所谓的父节点和左右孩子 有点类似于高中生物的遗传图谱。

 

嘿嘿 遗传图谱中 需要父亲和妈妈才能生出俩孩子

但是堆这个概念里 只用 爹就能生俩孩子(bushi

就是这样的效果捏。

这是堆的效果  那么什么是大根堆呢?

就是   最上面的元素最大,也就是(划重点)对于任何一个父节点来说 ,父节点都要比他的左右孩子大,只有这样的堆,才叫做大根堆。

1.1 堆排序中 父节点和左右孩子的下标

堆是怎么来的?我们要如何处理堆?

其实 堆是从数组中来的

由这个图,我们可以得知 堆的最上方 是 数组的下标 0 

那么大家可以把堆暂时看作一个2维数组vector

第0行是 0 第1行下标 1,2,第2行下标 3,4,5,6;

由此你发现 如果按照上述的描述把 堆一个个排成一维的数据后 是不是就是下方的数组了呢

1.2父节点和左右孩子的位置关系

这里 父节点 和左右孩子的位置关系 由上面的思想 可知

我们 假设 父节点 在一维数组中的下标是 i  那么左孩子就是2*i+1 ,右孩子和左孩子挨着 ,而且比左孩子大一位 于是 右孩子下标 2*i+2.

1.3局部大根堆的构建思路

那么我们在了解大根堆后,就该尝试着构建一个大根堆了。

这里又产生了两种思路,

1.从下标0开始构建大根堆

2.从最后一个父节点开始构造大根堆

这里其实应该都可以,只不过第一种是让问题变得越来越复杂

举个例子, 假设  正好 arr[0]  大于arr[1]  也大于arr[2]

那么继续向下找 ,此时 你就需要同时处理两个大根堆

(其实不管 上面三个数关系(大小)如何,都要处理两个父节点)

随着层数的递进 需要处理的父节点数 呈指数级上升。

就很麻烦。

于是,我们选择 从最后一个父节点下手 。

1.4构建局部大根堆

书接上文,我们既然要从最后一个父节点下手,首先 我得知道 最后一个父节点在那吧。

直接给出结论  最后一个父节点的下标index=arr.size()/2-1;

那你可能会问了,为啥是这个下标呢,原因和 每次生俩孩子有关系,这里我不过多赘述。

继续我们的话题,如何构建局部大根堆。

首先 我们找到 父节点的下标, 和他两个孩子的下标。然后看看父亲会不会比孩子小

如果父亲比孩子小就交换一下(交换最大的那个孩子,不然不还得换嘛)

注意喽,因为我们是从下往上遍历的,所以上面可能会有很小的数,

这个时候 如果 发生了交换 我们现在的父节点大根堆(局部) 是成立了

但是孩子的数字变了 ,那孩子的孩子 万一比孩子大了呢 哈哈哈哈哈哈哈哈哈

有点绕哈哈哈哈,就是 爹和孩子换数字了  万一爹比孙子还小呢?(哈哈哈

所以 我们在交换 完后 需要判断一下 孩子的大根堆还成不成立,不成立就换,然后再判断孩子的孩子的大根堆成不成立。

void make_big(vector<int>& arr,int start ,int end)
{
	int now = start;//传入一个父节点。
	int l = 2 * now + 1;//找到左孩子 
	for (; l <= end;now = l ,l=2*now+1)
	{
		if (l < end && arr[l] < arr[l + 1])l++;//如果这个节点 有右孩子 并且 右孩子大的话
		if (arr[now] < arr[l])
		{
			swap(arr[now], arr[l]);
		}
		//如果父亲小于孩子 就交换 //注意 孩子也可能是一个父亲 所以对孩子也要判断一下。
		else { break; }//局部大根堆构建完成 }
	}
}

看到这段代码,你可能会有几个问题

1.start是干嘛用的 ?

首先我们呢回看小标题 内容是构建局部大根堆

所以 start 是我们需要传入的父节点坐标  我们目前 的这段代码

就是来构建局部的大根堆的。

2.end是干嘛用的?

end 的出现 是因为 代码中不加end的话可能会造成下标越界。

哎,为啥可能会越界呢?  这就得回到我们上面所讲的一个功能了

就是  我们在构建一个局部大根堆的时候  有可能会对孩子的数值产生影响 从而导致孩子的大根堆就错了,所以我们在交换完后 令 start 等于孩子的下标  ,看看孩子的大根堆需不需要改动。

这样的代码有个缺陷 就是说 我怎么知道我的孩子有没有孩子?

正如最后一个父节点  假设 在构建大根堆的时候 和孩子发生了交换,然后你想去访问孩子的孩子,这个时候 ,孩子的孩子,这个下标是不是就越界了。

而无论你是孩子还是爹 是不是都是数组里的元素 所以数组里的元素 下标一定是小于arr.size()        的。这样就避免的下标溢出的问题

1.5局部大根堆代码思路讲解

首先 具体的思路,上面已经讲过了 ,所以这里来讲一下代码的实现思路

首先 要接受 我需要构造哪个位置的局部大根堆。 也就是父节点下标start 以及 数组中一共有多少数别越界了 end

然后 我们开始构建大根堆   

第一个问题

找到 孩子中最大的孩子。

在解决这个问题之前 ,我在提出一个问题,每个爹都有俩孩子吗?,很明显 除了最后一个爹都有俩孩子。那最后一个爹,可能就只有一个孩子,

于是我们唯一可以确定的是   左孩子的下标一定是小于等于arr.size()-1,

所以 我们先判断一下

if (l < end && arr[l] < arr[l + 1])l++;//如果这个节点 有右孩子 并且 右孩子大的话

这里的思想是 如果有右孩子,并且右孩子比左孩子大,那就让左孩子下标++变成右孩子下标。

于是 这里下标L代表的就是两个孩子中最大的孩子的下标

if (arr[now] < arr[l])
{
	swap(arr[now], arr[l]);
}
//如果父亲小于孩子 就交换 //注意 孩子也可能是一个父亲 所以对孩子也要判断一下。
else { break; }//局部大根堆构建完成 }

然后 如果爹比儿子小就交换 ,如果不小就直接break退出循环,哎这时候你可能问了,

你也没看 交换后孩子 的大根堆还成不成立啊

其实条件藏在for里面,你会发现,如果发生了交换 for循环还要继续

for (; l <= end;now = l ,l=2*now+1)

如果发生了交换 ,就让  now 也就是父节点 变成 孩子的节点 ,换一句话来说

这里for中语句3的作用就是 把孩子当成爹。

ok,局部大根堆讲解完毕,下面来讲整体起来怎么操作。

2.大根堆的实现和堆的排序思维

int x = arr.size() / 2 - 1;//找到最后一个父节点。
for (int i = x; i >= 0; i--)
{
	make_big(arr, i, arr.size() - 1);
}
int end = arr.size() - 1;
//构造大根堆。
//之后进行交换。

如图,这串代码的左右明显  就是从最后一个父节点开始  遍历所有父节点,然后构成一个大根堆,

这里不是重点,所以不多说。

for (int i = 0; i < arr.size()-1; i++)
{
	swap(arr[0], arr[end]);
	end--;
	make_big(arr, 0, end);
}

重点在这里 , 我们大根堆 大根堆 最主要的特点是什么?

当然是 最上面 的元素 也就是arr[0]是最大的啊 ,所以我们每一次构建大根堆 就是在找到目前堆里面的最大值 

首先第一次 我们把最大的放在数组的最后面,然后 我们不希望最后面已经排序好的数据发生变化,于是我们令end--,使得 下一次大根堆排序的时候默认不会 把已经排序好的元素 再放队里面。

然后我们分析一下   我们交换了 0号和最后一个 这两个元素 ,而且 我们不把最后已经排序好的元素放在堆里面 ,这就意味着 目前的堆   除了 第一个父节点,其他的父节点都是排序好的。

于是我们只用把第一个父节点排序好 就又构建出一个大根堆,然后继续交换,继续构建大根堆。

不断的循环,最后 我们就把数组中的数据 按照升序排列好了。

#include<iostream>
#include<vector>
using namespace std;
//欧克啊 ,还是来写 堆排序
//写堆排序的时候 我们分成两个思路来做
//第一 先 构造一个大根堆
//然后 交换 0 和end 的元素 下标--
//这是大根堆明显 乱顺序了 于是需要从新排列
void make_big(vector<int>& arr,int start ,int end)
{
	int now = start;//传入一个父节点。
	int l = 2 * now + 1;//找到左孩子 
	for (; l <= end;now = l ,l=2*now+1)
	{
		if (l < end && arr[l] < arr[l + 1])l++;//如果这个节点 有右孩子 并且 右孩子大的话
		if (arr[now] < arr[l])
		{
			swap(arr[now], arr[l]);
		}
		//如果父亲小于孩子 就交换 //注意 孩子也可能是一个父亲 所以对孩子也要判断一下。
		else { break; }//局部大根堆构建完成 }
	}
}

void  foster(vector<int>&arr)
{
	int x = arr.size() / 2 - 1;//找到最后一个父节点。
	for (int i = x; i >= 0; i--)
	{
		make_big(arr, i, arr.size() - 1);
	}
	int end = arr.size() - 1;
	//构造大根堆。
	//之后进行交换。
	for (int i = 0; i < arr.size()-1; i++)
	{
		swap(arr[0], arr[end]);
		end--;
		make_big(arr, 0, end);
	}

	for (int i = 0; i < arr.size(); i++)
	{
		cout << arr[i] << ' ';
	}
}
int main()
{
	vector<int>arr = { 1,4,3,7,9,8,5 };//一共有7个数字。
	foster(arr);
}

这是 我的案例代码 如果你感兴趣 可以 复印,修改一下数组中的数据来 体验一下

!!!如果我上述的内容有问题,可以评论一下,哈哈 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值