二叉堆
什么是二叉堆?
二叉堆本质上是一种完全二叉树,它分为两个类型。
- 最大堆。
最大堆的任何一个父节点的值,都大于或等于它左、右孩子节点的值。 - 最小堆。
最大堆的任何一个父节点的值,都小于或等于它左、右孩子节点的值。
二叉堆的根节点叫作堆顶。
最大堆和最小堆的特点决定了:最大堆的堆顶是整个堆中的最大元素;最小堆的堆顶是整个堆中的最小元素。
二叉堆的自我调整
对于二叉堆,有如下几种操作。
- 插入节点。
- 删除节点。
- 构建二叉堆。
所谓堆的自我调整,就是把一个不符合堆性质的完全二叉树,调整成一个堆。
以下以最小堆为例,看下二叉堆如何自我调整:
1. 插入节点
当二叉堆插入节点时,插入位置是完全二叉树的最后一个位置。例如插入一个新节点,值是 0。
这时,新节点的父节点5比0大,显然不符合最小堆的性质。于是让新节点“上浮”,和父节点交换位置。
继续用节点0和父节点3做比较,因为0小于3,则让新节点继续“上浮”。
继续比较,最终新节点0“上浮”到了堆顶位置。
2. 删除节点
二叉堆删除节点的过程和插入节点的过程正好相反,所删除的是处于堆顶的节点。例如删除最小堆的堆顶节点1。
这时,为了继续维持完全二叉树的结构,我们把堆的最后一个节点10临时补到原本堆顶的位置。
接下来,让暂处堆顶位置的节点10和它的左、右孩子进行比较,如果左、右孩子节点中最小的一个(显然是节点2)比节点10小,那么让节点10“下沉”。
继续让节点10和它的左、右孩子做比较,左、右孩子中最小的是节点7,由于10大于7,让节点10继续“下沉”。
这样一来,二叉堆重新得到了调整。
3. 构建二叉堆
构建二叉堆,也就是把一个无序的完全二叉树调整为二叉堆,本质就是让所有非叶子节点依次“下沉”。
下面举一个无序完全二叉树的例子,如下图所示。
首先,从最后一个非叶子节点开始,也就是从节点10开始。如果节点10大于它左、右孩子节点中最小的一个,则节点10“下沉”。
接下来轮到节点3,如果节点3大于它左、右孩子节点中最小的一个,则节点3“下沉”。
然后轮到节点1,如果节点1大于它左、右孩子节点中最小的一个,则节点1“下沉”。事实上节点1小于它的左、右孩子,所以不用改变。
接下来轮到节点7,如果节点7大于它左、右孩子节点中最小的一个,则节点7“下沉”。
节点7继续比较,继续“下沉”。
经过上述几轮比较和“下沉”操作,最终每一节点都小于它的左、右孩子节点,一个无序的完全二叉树就被构建成了一个最小堆。
时间复杂度
插入和删除操作,,时间复杂度是O(logn)。但构建堆的时间复杂度却并不是O(nlogn),而是O(n)。
具体的代码实现:
二叉堆虽然是一个完全二叉树,但它的存储方式并不是链式存储,而是顺序存储。换句话说,二叉堆的所有节点都存储
在数组中。
假设父节点的下标是parent,那么它的左孩子下标就是 2×parent+1;右孩子下标就是2×parent+2。
代码如下:
class Heap{
//插入节点的上浮操作
//插入节点使在数组的末位插入,则将此节点的值暂存,通过节点孩子节点与父节点的运算策略
//将此节点值不断的与更高小子树的父节点比较,最终得到所能达到的最大高度子树的小标,将此节点值赋予此下标
function upAdjust($arr){
//数组末位下标,即位插入节点的下标
$intChildIndex = count($arr)-1;
//插入节点下标的父节点下标
$intParentIndex = ($intChildIndex-1)/2;
// temp 保存插入的叶子节点值,用于最后的赋值
$tmp = $arr[$intChildIndex];
while ($intChildIndex > 0 && $tmp < $arr[$intParentIndex]) {
//若插入节点值比父节点值更小,则更改该插入节点下标值为父节点的值,达到父节点下移动的效果
$arr[$intChildIndex] = $arr[$intParentIndex];
//向上更高处比较,则将孩子下标指针指向更高的原父节点的下标
//以下两部的目的就是找到更高的非叶子的下标以便与插入节点值进行比较是否上浮动
$intChildIndex = $intParentIndex;
//求当前所在更高最小子树的父节点的下标
$intParentIndex = ($intChildIndex-1)/2;
}
//循环停止,则此时的子节点下标为插入的节点所有达到最高子树的下标,将插入值存储到此下标
$arr[$intChildIndex] = $tmp;
return $arr;
}
//循环下沉构建堆
//有些问题,有待改进
function buildHeap($arr){
//从最后一个叶子节点开始依次做下沉操作
$intLength = count($arr);
//这里减2的是因为每个父节点下是两个子树,减去2则会跨到另外一个最小二叉树
for ($i=($intLength-2)/2; $i >= 0 ; $i--) {
$intParentIndex = $i;
$tmp = $arr[$intParentIndex];
$intChildIndex = $intParentIndex*2 + 1;
while ($intChildIndex < $intLength) {
//如果有右孩子,且右孩子小于左孩子的值,则定位到右孩子
if($intChildIndex+1 < $intLength && $arr[$intChildIndex+1] < $arr[$intChildIndex]){
$intChildIndex++;
}
//如果父节点小于任何一个孩子的值,则直接跳出
if($tmp <= $arr[$intChildIndex]){
break;
}
//无需真正替换,直接赋值即可
$arr[$intParentIndex] = $arr[$intChildIndex];
$intParentIndex = $intChildIndex;
$intChildIndex = $intParentIndex*2 + 1;
}
$arr[$intParentIndex] = $tmp;
}
return $arr;
}
//运行
function run(){
//定义一个满足最小二叉堆规则的数组
$arr = array(1,3,2,6,5,7,8,9,10);
//插入节点0
$arr[] = 0;
//上浮自调整,使新插入节点后仍然满足最小二叉堆规则
$arrUp = $this->upAdjust($arr);
print_r($arrUp);
// $arr = array(7,1,3,10,5,2,8,9,6);
// $arrDown = $this->buildHeap($arr); //循环下沉构建堆
// print_r($arrDown);
}
}
$obj = new Heap();
$obj->run();
输出结果:
优先队列
优先队列不再遵循先入先出的原则,而是分为两种情况。(其实还有很多情况,主要是设定出队的优先级,甚至可以用算法构建优先级,这里是最简单的情况)
最大优先队列,无论入队顺序如何,都是当前最大的元素优先出队
最小优先队列,无论入队顺序如何,都是当前最小的元素优先出队
优先队列的实现是以二叉堆为基础
每一次入队操作就是堆的插入操作,每一次出队操作就是删除堆顶节点。
入队操作
-
插入新节点5
-
新节点5“上浮”到合适位置
出队操作
-
让原堆顶节点10出队。
-
把最后一个节点1替换到堆顶位置。
- 节点1“下沉”,节点9成为新堆顶。
时间复杂度:
二叉堆节点“上浮”和“下沉”的时间复杂度都是O(logn),所以优先队列入队和出队的时间复杂度也是O(logn)。
具体代码实现:
基本思想:每次入队则调整二叉堆,使数组永远满足二叉堆的规则,每次出队则取栈顶,因为二叉堆的特点,则可以保证取值为最大或者最最小,然后再调整二叉堆。也就是利用二叉堆数组的结构规则以及自我调整,保证每次出队的数组头,也就是栈顶,是整个数组队列的最大值或者最小值。
自我思考:这里只是利用了二叉堆栈顶的特殊性来满足优先级队列按照优先级出队的要求。其实可以将队列数组,每次入队和出队后,都将数组头变为最大值,这要也能满足优先队列的要求,但是时间复杂度高。至于其它算法来决定优先级,应该是利用算法规则来决定二叉堆数组的节点下标规则。
具体代码如下:
private int[] array;
private int size;
public PriorityQueue(){
//队列初始长度为32
array = new int[32];
}
/**
* 入队
* @param key 入队元素
*/
public void enQueue(int key) {
//队列长度超出范围,扩容
if(size >= array.length){
resize();
}
array[size++] = key;
upAdjust();
}
/**
* 出队
*/
public int deQueue() throws Exception {
if(size <= 0){
throw new Exception("the queue is empty !");
}
//获取堆顶元素
int head = array[0];
//让最后一个元素移动到堆顶
array[0] = array[--size];
downAdjust();
return head;
}
/**
* “上浮”调整
*/
private void upAdjust() {
int childIndex = size-1;
int parentIndex = (childIndex-1)/2;
// temp 保存插入的叶子节点值,用于最后的赋值
int temp = array[childIndex];
while (childIndex > 0 && temp > array[parentIndex])
{
//无须真正交换,单向赋值即可
array[childIndex] = array[parentIndex];
childIndex = parentIndex;
parentIndex = parentIndex / 2;
}
array[childIndex] = temp;
}
/**
* “下沉”调整
*/
private void downAdjust() {
// temp 保存父节点的值,用于最后的赋值
int parentIndex = 0;
int temp = array[parentIndex];
int childIndex = 1;
while (childIndex < size) {
// 如果有右孩子,且右孩子大于左孩子的值,则定位到右孩子
if (childIndex + 1 < size && array[childIndex + 1] >
array[childIndex]) {
childIndex++;
}
// 如果父节点大于任何一个孩子的值,直接跳出
if (temp >= array[childIndex])
break;
//无须真正交换,单向赋值即可
array[parentIndex] = array[childIndex];
parentIndex = childIndex;
childIndex = 2 * childIndex + 1;
}
array[parentIndex] = temp;
}
/**
* 队列扩容
*/
private void resize() {
//队列容量翻倍
int newSize = this.size * 2;
this.array = Arrays.copyOf(this.array, newSize);
}
public static void main(String[] args) throws Exception {
PriorityQueue priorityQueue = new PriorityQueue();
priorityQueue.enQueue(3);
priorityQueue.enQueue(5);
priorityQueue.enQueue(10);
priorityQueue.enQueue(2);
priorityQueue.enQueue(7);
System.out.println(" 出队元素:" + priorityQueue.deQueue());
System.out.println(" 出队元素:" + priorityQueue.deQueue());
}