算法导论笔记——堆排序1

堆的这部分内容比较多,我会分成几个部分进行讲解。所有的讲解基本上是 基于《算法导论》

堆(二叉堆)说白了其实就是个数组 , 它可以被近似的看作完全二叉树,树上的每一个节点对应数组中的每一个元素。

这里简单的介绍一下完全二叉树 , 完全二叉树是从左往右依次填充 , 这里举一个例子方便大家理解

像这样 , 其中第六个节点不是从左向右依次填充的 , 所以这棵树不是完全二叉树

如果改成这样,就是完全二叉树

二叉树就简单介绍到这里 , 这里可以画一个二叉树和堆的关系图 , 方便大家更好的理解堆

这是上述二叉树所创建的堆

这里介绍两种堆——最大堆 , 最小堆;

从定义上来看,最大堆就是除去根节点以外,所有的父亲节点大于他的孩子节点

自然 , 我们也不难想象出,最小堆的定义是:除去根节点,所有的父亲节点都小于他的孩子节点

代码表示—— 最大堆 A[parent] >= A[i]

                      最小堆 A[parent] <= A[i] 

最小堆通常用来构造优先队列 , C++STL中的priority_queue的底层实现逻辑其实也就是最小堆

在算法中,当我们既无法适合最大堆或者是最小堆的时候,我们就会统称为堆。

堆实际上就是用数组来表示树,当有n个元素的堆时,我们可以把它看作一个高度为logn的完全二叉树。书上这里并没有详细解释堆为什么可以看作高度为O(logn)的完全二叉树 这里我来简单的说一下。

 下标从0开始 , 就比如说3的父亲是下标为0的1 , 这里我们可以用公式(2 - 1)/ 2 = 0,求得3的父亲节点是下标为0的1.这样举个例子,大家应该会让大家理解起来更加轻松。

 如果是一颗满二叉树,他的节点的个数为2^n - 1 。

推导过程如下,第一行是1个节点 ,第二行是2个节点 , 第三行是4个节点

分别为2^0 , 2^1 , 2^2 ,可是使用一点点高中知识——等比数列的求和公式 , 自然也就可以推出来一颗满二叉树的节点个数。自然一颗满二叉树的高度就为  当然,并不是所有的二叉树都是满二叉树,所以一颗二叉树的高度可以近似的看作O(logn)

这里其实还有两个性质需要我插上一嘴

当数组从0开始的时候

left child = parent * 2 + 1——左孩子的位置应当为他的父亲 * 2 + 1的,

right child = parent * 2 + 1 + 1——右孩子的位置应当为他的父亲 * 2 + 2的

根据计算机计算除法是以向下取整为基础 , parent = (child - 1) / 2 这里的孩子是不分左右的

当数组从1开始的时候

left child = parent * 2        right child = parent * 2 + 1

这里我们举个例子来方便大家理解

这里书中就开始将如何维护堆这种数据结构的性质了,这里我们放到后面去讲。

int main() {
	int a[] = {4 , 7 , 5 , 6 , 9};
	return 0;
}

 这里我们先假设以上的a作为一个堆,他的树状结构应该是这样的

 这既不是一个最大堆或是最小堆。那么我们应该如何把他变成大堆或者小堆,这里就是堆排序的第一个重点了。这里我们可以对整个堆执以下的操作

这里以最大堆为例子

我们从7 开始进行一个操作,他先和他的两个孩子进行比较 ,在左右孩子中找出较大的一个出来,如果两孩子中大的一个,如果两个孩子中大的那一个大于他的父亲,那么就进行交换。从现在的树来看,就是7应当与6  ,9中(9 >6)的9进行交换,,那么此时,树就会变成这样

 此时7没有孩子了,所以此次交换结束。再从原来的7(现在的9)的前一个节点(现在的4)进行刚才的操作。

观察4的左右孩子,找出孩子中最大的(9),如果比4大,就进行交换

再在4中找他的两个孩子节点中最大的那一个(7),7  > 4所以需要进行交换,

 这就是一颗已经调整好的树,也就是我们所想要的最大堆.不难看出,其中所有的孩子都是要小于他的父亲,此时存储堆的数组也会变成这样

 

 刚才的操作大家可能有一些问题,这里为什么不从最后一个叶子节点(数组的末端)进行刚才的操作

这样的操作我们可以刻把他简称为down操作

这里我稍作解释,这里我们需要不停的将数组中的数字向下进行调整,因为作为叶子节点,他是没有孩子的,所以我们不需要对叶子节点进行以上的操作。

这算是出了个难题了,我们应当如何找到最后一个父亲节点呢。

这里我们需要用到刚才我们所谈到的公式parent = (child - 1) / 2。这里我们可以找到最后一个节点的父亲节点,这也就是最后一个的父亲节点。这里依然用以上的例子

9的下标为4,那么他父亲的下标就是(4 - 1) / 2,也就是1。在数组中下标为1的元素值就是7,所以我们应当对7和他之前的节点进行down操作。

知道了思路,现在我们就来开始coding吧

代码如下

void AdjustDown(int* a, int n, int root) {
	int parent = root;
	int child = parent * 2 + 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 = parent * 2 + 1;
		}
		else {
			break;
		}
	}
}

当不能再进行以上的操作的时候,立马停止本次的down操作

	for (int i = (n - 1 - 1) / 2; i >= 0; i--) {
		AdjustDown(a, n, i);
	}

堆排序,堆排序,那么现在知道了这样我们就需要进行排序操作了。

假设我们要进行从小到大的依次排序,这里我们是需要选择最大堆还是最小堆呢?

,并且当我们在排序的时候又应当如何进行某些操作以保证最大堆/最小堆的性质呢?
一篇文章太长也容易让读者丧失之前的兴趣,所以留下两个问腿提供给大家,希望大家在之后进行思考。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值