目录

  • 一、堆的介绍
  • 1. 用数组表示堆,如何区分父节点和子节点?
  • 2. 堆的种类?
  • 二、堆的插入、删除操作
  • 1. 几个基本操作
  • 2. 堆的插入
  • 3. 堆的删除
  • 三、堆的建立
  • 1. 建堆的原理
  • 2. 代码(参考LeetCode第215题——数组中地第K个最大元素)

一、堆的介绍

堆是一个有固定顺序的完全二叉树,通常用数组来表示。

1. 用数组表示堆,如何区分父节点和子节点?

下图是一个堆常用的编号方式示意图:根节点编号为0,根节点的左子节点编号为1、右子节点编号为2,再往下是3、4、5、6……即按照从上往下、从左往右的顺序编号。



java recursive 父节点 父节点数组表示法图示_父节点


在数组中,就按照如上所述的下标进行保存,如上图保存为数组就是{10, 20, 15, 25, 50, 30, 40, 35, 45}。

在这样一个数组中,下标为i的节点的父节点的下标就是 (i - 1) / 2(除法向下取整);而下标为i的节点的左子节点的下标2 * i + 1右子节点的下标是2 * i + 2。

示例:

第3个元素(25)的左子节点是第**2 * 3 + 1 = 7**个元素(35),
而它的右子节点是第**2 * 3 + 2 = 8**个元素(45)。

2. 堆的种类?

堆一般分为两种,即最大堆和最小堆。

最大(小)堆中每个节点的父节点一定比当前节点大(小);

或者说,每个节点的左右子树中的所有节点,一定比这个节点小(大)。

如下图所示则是一个最大堆:



java recursive 父节点 父节点数组表示法图示_java recursive 父节点_02


二、堆的插入、删除操作

为了保证我们在插入、删除任意节点后,最大(小)堆仍然能保证它的最大(小)特性,需要研究一套既定的、有规律可循的操作来完成插入和删除。

1. 几个基本操作

在讲堆的插入与删除之前,要明确堆的几个基本操作,而插入和删除全都是由这些基本操作组成的:

shiftUp():如果一个节点比它的父节点大(最大堆)或者小(最小堆),那么需要将它同父节点交换位置。这样是这个节点在数组的位置上升。

shiftDown():如果一个节点比它的子节点小(最大堆)或者大(最小堆),那么需要将它向下移动。这个操作也称作“堆化(heapify)”。

shiftUp 或者 shiftDown 是一个递归的过程,所以它的时间复杂度是 O(log n)。

2. 堆的插入

首先,在堆的第一个空白位置处插入这个新元素,然后递归调用上述的shiftUp()操作,直到它的父节点比它大(最大堆)或小(最小堆)。

示例:

我们通过一个插入例子来看看插入操作的细节。我们将数字16插入到这个堆中:

java recursive 父节点 父节点数组表示法图示_数据结构_03


第一步是将新的元素插入到堆的第一个空白位置处。则堆变成:

java recursive 父节点 父节点数组表示法图示_数据结构_04


不幸运的是,现在堆不满足堆的属性,因为 2 在 16 的上面,我们需要将大的数字在上面(这是一个最大堆),为了恢复堆属性,我们需要交换162。现在还没有完成,因为 10 也比 16 小。我们继续交换我们的插入元素和它的父节点,直到它的父节点比它大或者我们到达树的顶部。这就是所谓的 shiftUp(),每一次插入操作后都需要进行。它将一个太大或者太小的数字“浮起”到树的顶部。

最后我们得到的堆:

java recursive 父节点 父节点数组表示法图示_父节点_05


现在每一个父节点都比它的子节点大,满足了最大堆的属性。

3. 堆的删除

为了将这个节点删除后的空位填补上,首先要将本堆中最后一个元素的值(假设为value)移动到此位置,然后在被删位置处,用此位置当前的值value和此处的父节点、子节点去比较,如果它与父节点的关系破坏了最大(小)堆,则递归调用shiftUp()来修复;如果它与子节点的关系破坏了最大(小)堆,则递归调用shiftDown()来修复。

示例:

我们将这个树中的 (10) 删除:

java recursive 父节点 父节点数组表示法图示_数据结构_06


现在顶部有一个空的节点,怎么处理?

java recursive 父节点 父节点数组表示法图示_数据结构_07


当插入节点的时候,我们将新的值返给数组的尾部。现在我们来做相反的事情:我们取出数组中的最后一个元素,将它放到树的顶部,然后再修复堆属性。现在被删除位置处的元素值为1,由于它没有父节点,所以我们只把它与子节点进行比较,看是否违反了最大堆的属性:现在有两个数字( 7 和 2)可用于交换。我们选择这两者中的较大者称为最大值放在树的顶部,所以交换 7 和 1,现在树变成了:

java recursive 父节点 父节点数组表示法图示_java recursive 父节点_08


继续堆化直到该节点没有任何子节点或者它比两个子节点都要大为止。对于我们的堆,我们只需要再有一次交换就恢复了堆属性:

java recursive 父节点 父节点数组表示法图示_子节点_09


如上,就完成了堆的删除操作。

三、堆的建立

堆的插入和删除操作都不是最难的,最难的是堆的建立。这里我们以最大堆为例,讲讲如何由一个无序数组,建立一个最大堆。

1. 建堆的原理

建立最大(小)堆的原理是,只要保证了某节点左右子树最大(小)堆,那么只要对此节点不断做shifDown()操作,那么最终就能把以此节点为根节点的树变成最大(小)堆。也就是说,把以某节点为根节点的子树,调整成一个最大(小)堆。

那么,只要在一棵二叉树中,从最后一个非叶子节点开始,从下往上地对每个节点都做此操作,就能把整棵树调整成一个最大(小)堆。

2. 代码(参考LeetCode第215题——数组中地第K个最大元素)

为了方便计算,最大堆并不用链表的方式来存储,而是用数组的方式,这样,我们就可以知道某节点n,它的父节点是**(n - 1) / 2**,左孩子2 x n + 1右孩子2 x n + 2

这样一来,我们就可以用如下程序构建一个基于数组实现的最大堆了:

void shiftDown(vector<int>& nums, int k)
{
	int leftChild = 2 * k + 1, rightChild = 2 * k + 2;
	int max = k; //假设在当前节点,及其左、右子节点,共三个节点中,最大的是当前这个节点。后序我们就要更新max,看到底哪个才是最大的,把最大的那个和当前节点交换
	if(leftChild < nums.size() && nums[leftChild] > nums[max])
		max = leftChild;
	if(rightChild < nums.size() && nums[rightChild] > nums[max])
		max = rightChild;
	if(max != k)
	{
		swap(nums[max], nums[k]);
		shiftDown(nums, max); // 如果原k节点调整了位置(上一步swap调整),那么就要将k继续做shiftDown操作,直到它比它的左、右孩子都大
	}
}

void buildMaxHeap(vector<int>& nums)  // 用nums数组表示二叉树
{
	for(int i = nums.size() / 2; i >= 0; i--)  // 从第一个非叶子节点开始,从下往上,将每棵子树调整成最大堆
	{
		shiftDown(nums, i);
	}
}