二叉堆与优先队列

二叉堆

什么是二叉堆?
二叉堆本质上是一种完全二叉树,它分为两个类型。

  1. 最大堆。
    最大堆的任何一个父节点的值,都大于或等于它左、右孩子节点的值。
  2. 最小堆。
    最大堆的任何一个父节点的值,都小于或等于它左、右孩子节点的值。

二叉堆的根节点叫作堆顶。
最大堆和最小堆的特点决定了:最大堆的堆顶是整个堆中的最大元素;最小堆的堆顶是整个堆中的最小元素。

二叉堆的自我调整
对于二叉堆,有如下几种操作。

  1. 插入节点。
  2. 删除节点。
  3. 构建二叉堆。

所谓堆的自我调整,就是把一个不符合堆性质的完全二叉树,调整成一个堆。
以下以最小堆为例,看下二叉堆如何自我调整:
1. 插入节点
当二叉堆插入节点时,插入位置是完全二叉树的最后一个位置。例如插入一个新节点,值是 0。

image.png

这时,新节点的父节点5比0大,显然不符合最小堆的性质。于是让新节点“上浮”,和父节点交换位置。
image.png

继续用节点0和父节点3做比较,因为0小于3,则让新节点继续“上浮”。
image.png

继续比较,最终新节点0“上浮”到了堆顶位置。
image.png

2. 删除节点
二叉堆删除节点的过程和插入节点的过程正好相反,所删除的是处于堆顶的节点。例如删除最小堆的堆顶节点1。

image.png

这时,为了继续维持完全二叉树的结构,我们把堆的最后一个节点10临时补到原本堆顶的位置。
image.png

接下来,让暂处堆顶位置的节点10和它的左、右孩子进行比较,如果左、右孩子节点中最小的一个(显然是节点2)比节点10小,那么让节点10“下沉”。
image.png

继续让节点10和它的左、右孩子做比较,左、右孩子中最小的是节点7,由于10大于7,让节点10继续“下沉”。
image.png

这样一来,二叉堆重新得到了调整。

3. 构建二叉堆
构建二叉堆,也就是把一个无序的完全二叉树调整为二叉堆,本质就是让所有非叶子节点依次“下沉”。
下面举一个无序完全二叉树的例子,如下图所示。

image.png

首先,从最后一个非叶子节点开始,也就是从节点10开始。如果节点10大于它左、右孩子节点中最小的一个,则节点10“下沉”。
image.png

接下来轮到节点3,如果节点3大于它左、右孩子节点中最小的一个,则节点3“下沉”。
image.png

然后轮到节点1,如果节点1大于它左、右孩子节点中最小的一个,则节点1“下沉”。事实上节点1小于它的左、右孩子,所以不用改变。
接下来轮到节点7,如果节点7大于它左、右孩子节点中最小的一个,则节点7“下沉”。
image.png

节点7继续比较,继续“下沉”。
image.png

经过上述几轮比较和“下沉”操作,最终每一节点都小于它的左、右孩子节点,一个无序的完全二叉树就被构建成了一个最小堆。
时间复杂度
插入和删除操作,,时间复杂度是O(logn)。但构建堆的时间复杂度却并不是O(nlogn),而是O(n)。

具体的代码实现:
二叉堆虽然是一个完全二叉树,但它的存储方式并不是链式存储,而是顺序存储。换句话说,二叉堆的所有节点都存储
在数组中。


image.png

假设父节点的下标是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();

输出结果:


image.png

优先队列

优先队列不再遵循先入先出的原则,而是分为两种情况。(其实还有很多情况,主要是设定出队的优先级,甚至可以用算法构建优先级,这里是最简单的情况)
最大优先队列,无论入队顺序如何,都是当前最大的元素优先出队
最小优先队列,无论入队顺序如何,都是当前最小的元素优先出队
优先队列的实现是以二叉堆为基础
每一次入队操作就是堆的插入操作,每一次出队操作就是删除堆顶节点。
入队操作

  1. 插入新节点5


    image.png
  2. 新节点5“上浮”到合适位置


    image.png

出队操作

  1. 让原堆顶节点10出队。


    image.png
  2. 把最后一个节点1替换到堆顶位置。


    image.png
  3. 节点1“下沉”,节点9成为新堆顶。
    image.png

    时间复杂度:
    二叉堆节点“上浮”和“下沉”的时间复杂度都是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());
}
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,117评论 4 360
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 66,963评论 1 290
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 107,897评论 0 240
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,805评论 0 203
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,208评论 3 286
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,535评论 1 216
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,797评论 2 311
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,493评论 0 197
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,215评论 1 241
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,477评论 2 244
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 31,988评论 1 258
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,325评论 2 252
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,971评论 3 235
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,055评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,807评论 0 194
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,544评论 2 271
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,455评论 2 266