优先队列·二叉堆

一、定义

​ 一般在优先队列里面说“堆”这个词,指的都是二叉堆这种数据结构实现。堆的最大结构特定是能很顺利的找到最小值!

​ 堆是一棵完全二叉树,他总是满足,父节点的元素值小于其子节点的元素。并且每个子树都是堆!

二、结构

2.1 结构图

​ 已知堆是一棵完全二叉树,我们画一棵高度为3的堆。

2.2 结构性质

​ 容易证明,一棵高为\(h\)的完全二叉树有\(2^h\)\(2^{h+1}\)-1个节点,这意味着完全二叉树的高是\(logN\),显然他是\(O(logN)\)

证明:

1.高度为\(h\)的满二叉树,有节点\(S_{max} = 2^{h+1}\)-1。

2.完全二叉树除最底层外,其他层是满二叉树。

3.不计算最底层,有节点有共\(S_{min} = 2^{h}\)-1。

4.由于高度是h,所以最底层最少有一个节点,所以范围为\([S_{min}+1,S_{max}]\)

​ 由于完全二叉树具有很明显的规律,所以我们可以用一个数组而不需要用链表来表示。我们设计一个数组来表示上图的“二叉堆/堆”。

根据完全二叉树的性质,我们可以得到这样的数组来表示一个堆:

​ 数组第0位放弃,从第一位开始放入二叉树的元素。我们看到,在堆中描述的父子结构(颜色标记),在数组中依然得以保留,不过保留的方式变成了:\(i\)个位置上的元素,他的左儿子总是在第\(2i\)位置上,右儿子在\(2i+1\)的位置上!

2.3 结构说明:

​ 因此,一个堆结构将由一个数组和一个代表当前堆大小的整数构成。

三、堆序性质

3.1 堆序性质

​ 堆序性质是让堆操作快速执行。我们设计堆的初衷是为了快速找出最小元素。所以堆序性质应该也能满足这个目的,我们设定堆的最小元素在根节点,如果把任意子树都当作堆结构,那么可以得到:堆父节点的值总是小于(或等于)子节点。

3.2 简单图解


不满足二叉堆堆序性质,完全二叉树就不是堆!

四、堆操作

4.1 插入 (insert)

​ 根据堆的特性,我们每次插入都需要保证满足堆的堆序特性。所以很多时候我们在插入之后,不得不调整整个堆的顺序来维持堆的数据结构。

4.1.1 上滤:

​ 为将一个元素X插入到堆中,我们在下一个可用位置创建一个空穴,否则该堆将不再是完整树,如果X可以放在该空穴中而不破坏堆的序,那么可以插入完成。否则,我们把空穴的父节点 上的元素移入该空穴中,这样,空穴就朝着根的方向冒一步,继续该过程直到X能被放入空穴为止。

4.1.2 操作说明

​ 我们可以反复的交换操作,直到建立正确的堆序来实现上滤插入。这个操作我们可以通过递归来实现。如果我们要插入的新的值是最小元素,我们需要一只上滤到根节点。那么这种插入到时间是\(O(N)\)。不过在实际中早已经证明,执行一次插入平均需要2.607次比较,因此平均上移元素1.607层。

插入一次需要上移动N层,加上一次插入时间。所以插入总时间是上移时间+插入程序执行时间。

4.2 删除最小元素(deleteMin)

​ 根据堆堆特性,找到最先元素很简单,就是树的根节点,但是删除它却不容易,删除根节点,必然会破坏树的结构。

4.2.1 下滤

​ 删除一个元素,我们可以很快找到,根节点元素就是最小的元素。我们删除根节点后,根据完全二叉树的性质,我们必然要移动最后一个节点。最后一个节点成为X,如果X能被放到空穴中(此时空穴在根节点),那么deleteMin完成,不过这一般不太可能,因为我们将空穴的儿子中较小者放到空穴中,这样把空穴向下推了一层,重复该动作直到X可以被放大空穴中。


4.2.2 操作说明

​ 其实我们可以当作将最后一个节点放到根节点,然后从根节点开始下滤,进行位置调整。我们通过反复的交换空穴位置,直到将最后一个值能够插入到空穴,递归可以帮我们做到这点。同上滤一样,最坏情况依然是\(O(N)\)。平均时间是\(O(logN)\)

这里有个小说明,我们将最后一个节点放到根节点时,并不是做交换,因为根节点被舍弃,我们直接赋值就可以。会减少操作时间。

4.3 构建堆

​ 构建N个元素的二叉堆,我们直接执行N次insert方法即可, 因为在设计insert方法时,就已将考虑到将插入到值放到堆的合适位置。输入N项,即可自动生成N大小的堆。

​ 一般的算法时将N项以任意顺序放入树中,保持结构特性,然后按顺序对所有节点进行下滤。

稍微解释下我们在代码中的下滤做法。

// 代码在文章最后
	/**
	 * 构建
	 */
	private void buildHeap() {
		for (int i = currentSize / 2; i > 0; i--) {
			percolateDown(i);
		}
	}

​ 我们选取 \(i = currentSize/2\),之所以是这个值,是因为\(array[currentSize]\)为最后一个节点, \(i = currentSize/2\) 得到的是倒数第二层最后一个节点的父节点,正如上图中的节点 21,我们从倒数第二层开始下滤,而不需要从最后一层进行,因为我们只在乎父子节点的大小关系。

代码位置

https://github.com/dhcao/dataStructuresAndAlgorithm/blob/master/src/chapterSix/BinaryHeap.java

posted @ 2019-03-24 23:14  undifinedException  阅读(579)  评论(0编辑  收藏  举报