堆(Heap)的详解和二叉堆(Binary Heap)的代码实现

目录

一,堆的定义和介绍

二,如何用数组存储二叉堆?

三、向最大堆中插入元素 Shift up

四、从最大堆中取出元素 Shift Down

五、总结


一,堆的定义和介绍

堆(heap)是计算机科学中一类特殊的数据结构的统称。堆通常是一个可以被看做一棵树的数组对象。

堆就是用数组实现的二叉树。堆总是满足下列性质:

  • 堆中某个节点的值总是不大于或不小于其父节点的值。
  • 堆总是一棵完全二叉树。

堆是非线性数据结构,相当于一维数组,有两个直接后继。

将根节点最大的堆叫做最大堆或大根堆,根节点最小的堆叫做最小堆或小根堆。常见的堆有二叉堆、斐波那契堆等。

堆的使用场景:

  • 构建优先队列
  • 支持堆排序
  • 快速找出一个集合中的最小值(或者最大值)

下面我们主要分析一种最为经典的堆实现 二叉堆 Binary Heap,并且只实现 最大堆。

二叉堆对应的是一个二叉树,所谓的二叉树就是每一个节点最多只能有两个子节点。

二叉堆(最大堆)所对应的二叉树的特点:

  • 任何一个节点的值总是不大于其父节点的值。
  • 必须是完全二叉树。

所谓的完全二叉树就是,(1)除了最后一层节点之外,其他层所有的节点个数必须是最大值。对于一个二叉树来说,第一层最多有1个节点,第二层最多有2个节点,第三层最多有4个节点,以此列推。(2)在最后一层虽然它的节点个数可以不是最大值,但是所有的节点必须集中在左侧。

 

 

二,如何用数组存储二叉堆?

使用数组实现二叉堆,是一个非常经典的实现。‘

我们之所以可以使用数组来存储一个二叉堆,正是因为堆是一颗完全二叉树。我们可以尝试一下,给这个完全二叉树自上到下、自左到右的每一个节点标上一个序列号,如下图所示。最顶端的节点是1,下面一层2、3,再下面一层4、5、6、7,以此类推。相当于依照层序自上到下,然后在每一层自左到右,按照这个顺序标上序列号。

这样标上序列号之后,我们就可以发现,对于每一个节点来说,左子节点的序列号都是它的序列号的2倍,右子节点的序列号都是它的序列号的2倍加1。

还要注意一点,这里是将根节点的序列号声明成了1,如果我们把根节点的序列号声明成0的话,后面依次类推0、1、2、3、4、5、6。这时候也有类似的性质,这时候具体的左右子节点的计算规则会发生一些改变,但是依然有相应的规律,有兴趣的同学可以尝试自己找一下这个规律。

不过对于堆来说,一个经典的实现,就是根节点从1开始标记。这样一来我们就可以把所有的这些数据存在数组中,而相应的我们刚刚标记的这些序列号,就是数组中的索引。

所以对于上图中的二叉堆而言,就可以用如下的数组进行存储。

 

大家要注意,0索引这里是不使用的,

现在我们有了这样一个数组之后,就可以非常轻松的使用我们之前讲的公式,找到数组中每个元素,它所对应的左子节点元素和右子节点元素。同理我们也可以找到数组每个元素的父节点元素。

parent(i) = i / 2 //求i元素的父节点元素,这里采用的是计算机的除法,如果不能整除则向下取整。

left child(i) = 2 * i //求i元素的左子节点元素。

right child(i) = 2 * i + 1 //求i元素的右子节点元素。

二叉堆代码实现的整体框架如下:

public class MaxHeap<T> {
	
	private T[] data;
	private int count;
	
	/**
	 * 构造函数,构造一个空堆,可容纳capacity个元素
	 * @param capacity
	 */
	public MaxHeap(int capacity){
		data = (T[]) new Object[capacity];
	}
	
	/**
	 * 返回堆中的元素个数
	 * @return
	 */
	public int size(){
		return count;
	}
	
	/**
	 * 返回一个布尔值,表示堆中是否为空
	 * @return
	 */
	public boolean isEmpty(){
		return count == 0;
	}

	/**
	 * 测试 MaxHeap
	 * @param args
	 */
	public static void main(String[] args) {
		// TODO Auto-generated method stub
		MaxHeap<Integer> maxHeap = new MaxHeap<Integer>(100);
		System.out.println("maxHeap size: " + maxHeap.size());
	}

}

三、向最大堆中插入元素 Shift up

我们先来看一下动态演示:

现在有一个已经存在的堆,如下图。

如果有一个新的元素52,需要加入到这个最大堆。通过上面的分析知道,这个堆我们使用了一个数据来实现,在这个最大堆中添加一个新的元素,相当于在数组的末尾添加一个元素。此时52就添加到了索引为11的位置,这样相当于最大堆变成了如下样子。

 

但是大家要注意,这个样子的二叉树是不满足最大堆的定义,所以接下来我们要进行一些列的操作,来维护最大堆的定义。那具体应该怎么做呢?

其实这个过程非常简单,在我们加入新元素之前,整个二叉树就是一个最大堆,所以问题肯定出现在新加入的元素上。 我们需要做的是将新加入的元素,调整到合适的位置,使得整个二叉树依然保持最大堆的性质。那么我们怎么调整这个顺序呢?

把新加入的元素和它的父节点元素进行比较。如果父节点元素比新加入元素小,违背了最大堆的定义,所以把这两个元素交换一下位置。交换完成后二叉树变成如下样子。

 

此时新元素是根节点的子树部分,已经满足最大堆的定义。那么下一步这个新元素52可能还是和它的父节点元素不满足最大堆的定义。所以这时候新加入的元素52再和它的父节点元素42进行比较,是否父节点原始比这个新元素还要小。在这里父节点元素41比新元素52小,所以他们又要交换一下位置,现在我们的二叉树就变成了如下样子。

此时新元素是根节点的子树部分,依然保持了最大堆的性质。那么现在到了这种状态,新元素52到了新的位置,它还有可能和它的父节点元素违背最大堆的定义。所以还需要再次比较一下新元素和它父节点元素,52比62要小,此时就不需要交换这两个元素的位置了。

经过这一些列的操作,我们依旧维持了整个二叉树最大堆的定义。我们可以看到新元素52从最底层逐渐往上升的过程,就是我们要分析的Shift Up过程。通过这样的方式我们成功的向最大堆中添加了新的元素。下面我们来看一下具体的代码实现。

/**
 * 在堆的有关操作中,需要比较堆中元素的大小,所以E extends Comparable
 * @param <E>
 */
public class MaxHeap<E extends Comparable> {
	
	protected E[] data;
	protected int count;
	protected int capacity;
	
	/**
	 * 构造函数,构造一个空堆,可容纳capacity个元素
	 * @param capacity
	 */
	public MaxHeap(int capacity){
		data = (E[]) new Comparable[capacity + 1];
		count = 0;
		this.capacity = capacity;
	}
	
	/**
	 * 返回堆中的元素个数
	 * @return
	 */
	public int size(){
		return count;
	}
	
	/**
	 * 返回一个布尔值,表示堆中是否为空
	 * @return
	 */
	public boolean isEmpty(){
		return count == 0;
	}
	
	/**
	 * 向最大堆中插入一个新的元素
	 * @param t
	 */
	public void insert(E e){
		if(count < capacity){
			data[count + 1] = e;
			count++;
			shiftUp(count);
		}
	}
	
	/**
	 * 最大堆核心辅助函数,把新插入的元素,移动到合适的位置。
	 * @param k
	 */
	private void shiftUp(int k){
		while(k > 1 && data[k / 2].compareTo(data[k]) < 0){
			swap(k / 2, k);
			k = k / 2;
		}
	}
	
	private void swap(int i, int j){
		E temp = data[i];
		data[i] = data[j];
		data[j] = data[i];
	}

}

 

四、从最大堆中取出元素 Shift Down

首先我们来看一下Shift Down的操作流程。

假设如下二叉树是当前最大堆的状态。

如果我们想从堆中取出一个元素,这里我们要注意。从堆中取出一个元素,只能取出根节点的元素。对于最大堆来说,相当于取出了优先级最大(值最大)的那个元素。

 

 

那么现在我们整个堆中相当于少了一个元素,怎么填补这个元素呢?

答案非常简单,我们只需要把整个堆的最后一个元素,放到整个堆的第一个元素的位置。这时候整个堆依然是一个完全二叉树,

前面的代码实现中,有一个count的成员变量,来描述当前堆中有多少个元素。这时候count--。这个例子中索引为11的元素16可以不动,我们在之后的操作中都已count为界,所以索引为11的元素不会被访问到。

这样一来首先我们整个堆拿出了一个元素,并且依然保持这它是一个完全二叉树。但是我们可以看出来,这个完全二叉树此时不是一个最大堆,这是因为我们从最下面取出一个元素放到最上面,这时候根节点元素不是整个堆中的最大值,会比它的子节点小。所以下面我们要做的事情,就是调整这些元素的位置,使得它保持最大堆的性质。

这个调整的过程其实是将根节点的元素,一步一步的向下移动,最终找到它合适的位置。这也就是为什么这个操作加Shift Down。

 

 

接下来我们看一下整个操作的动态演示。

这里我们将元素16向下移动,但是它可以向左也可以向右,那么究竟向哪个方向呢?

先比较一下左子节点和右子节点的大小,然后把较大的子节点元素和该元素比较,如果较大的子节点元素更大,则交换两个元素的位置,反之就不交换位置,结束整个Shift Down操作。或者当该元素已经在最下面一层(没有子节点),也需要结束整个Shift Down操作。

在这个例子里左节点元素52比右节点元素30大,左节点元素52比元素16大,所以元素16跟左节点元素52换。因为这样换完之后,才能保证元素52比元素16和元素32都要大。

接下来我们继续下移,比较元素16的左右两个子节点元素28和41。右节点元素41大于左节点元素28,右节点元素41大于元素16。所以元素16和右子节点元素41交换位置。

 

这时候元素16继续和它的子节点元素比较,此时的元素16只有左子节点,所以只需要比较元素16和左子节点元素15。元素16大于左子节点元素15,所以不需要进行交换。到此为止我们的Shift Down操作就结束了。

此时我们的最大堆成功的退出了一个元素62,之后经过Shift Down的操作,继续维持了最大堆的性质。那么下一步如果我们还要往外推出元素的话,根元素52就会推出去。然后元素15就会被换上去,再经过一些列的Shift Down操作,维持最大堆的性质。

下面我们来看一下具体的代码实现。

/**
 * 在堆的有关操作中,需要比较堆中元素的大小,所以E extends Comparable
 * @param <E>
 */
public class MaxHeap<E extends Comparable> {
	
	protected E[] data;
	protected int count;
	protected int capacity;
	
	/**
	 * 构造函数,构造一个空堆,可容纳capacity个元素
	 * @param capacity
	 */
	public MaxHeap(int capacity){
		data = (E[]) new Comparable[capacity + 1];
		count = 0;
		this.capacity = capacity;
	}
	
	/**
	 * 返回堆中的元素个数
	 * @return
	 */
	public int size(){
		return count;
	}
	
	/**
	 * 返回一个布尔值,表示堆中是否为空
	 * @return
	 */
	public boolean isEmpty(){
		return count == 0;
	}
	
	/**
	 * 向最大堆中插入一个新的元素
	 * @param t
	 */
	public void insert(E e){
		if(count < capacity){
			data[count + 1] = e;
			count++;
			shiftUp(count);
		}
	}
	
	/**
	 * 最大堆核心辅助函数,把新插入的元素,移动到合适的位置。
	 * @param k
	 */
	private void shiftUp(int k){
		while(k > 1 && data[k / 2].compareTo(data[k]) < 0){
			swap(k / 2, k);
			k = k / 2;
		}
	}
	
	/**
	 * 取出最大堆中,最大的元素
	 * @return
	 */
	public E extractMax(){
		if(count <= 0){
			return null;
		}
		E e = data[1];
		
		swap(1, count);
		count --;
		shiftDown(1);
		
		return e;
	}
	
	/**
	 * 最大堆核心辅助函数,把根节点的元素,移动到合适的位置。
	 * @param k
	 */
	private void shiftDown(int k){
		while(k * 2 <= count){ //判断节点k是否有子节点,只要有右节点就一定有子节点
			int j = k * 2; //在此轮循环中,data[k]和data[j]交换位置
			if(j + 1 <= count && data[j + 1].compareTo(data[j]) > 0){
				//有右子节点,并且右子节点元素比左子节点元素更大。
				j ++;
			}
			// data[j] 是 data[2*k]和data[2*k+1]中的最大值
			if(data[k].compareTo(data[j]) >= 0){
				//节点k已经比它的所有子节点更大,不需要交换位置了。
				break;
			}
			
			swap(k, j);
			k = j;
		}
	}
	
	private void swap(int i, int j){
		E temp = data[i];
		data[i] = data[j];
		data[j] = temp;
	}

}

 

五、总结

堆是一种灵活动态的数据结构,它能够在不停的添加和取出元素之后,一直保持着根节点元素最大(或这小)。正式由于这个特性,堆非常适合用来实现优先队列,也可以用来实现排序(堆排序)。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值