zoukankan      html  css  js  c++  java
  • 数据结构与算法复习——4、二叉堆

    4、二叉堆——前置知识复习1

    一、堆简介

      堆是一种数据结构,要求有效地完成两种基本操作:Insert(插入元素)和Pop(删除最值),其中最值可能是最大值、最小值或者其它复杂的最值。在这两种基本操作中,必然包括Top(寻找最值),但基本的堆不要求其他复杂的操作,比如合并,或者寻找任意元素。如果能够高效地寻找任意元素,则还可能做这些操作:Decrese(减小某点权值)、Increase(增加某点权值)、Delete(删除某点)。

      堆的这些操作对操作系统有重要意义:操作系统一般在调度时为每个任务都会分配一个优先级,当任务冲突时,优先级最高的将会被优先执行。可见Insert即添加新任务,Pop即任务完成,Decrease、Increase即对某个任务的优先级调整(比如降低某些占用太多资源的任务),Delete即中止某项任务。当然,对于操作系统来讲,建立堆也是必不可少的。

      堆的实现有很多种,我们今天来复习最基本(但是也非常高效)的二叉堆

    二、二叉堆

      二叉堆是一棵树,它的递归定义如下:

    1、二叉堆是一棵完全二叉树

    2、二叉堆根节点的值与它左、右子节点的值分别都满足同一个序(例如都小于、都大于)(没有子节点时默认满足);

    3、二叉堆的左右两棵子树也都是二叉堆。

    (如果不清楚完全二叉树是什么:作为二叉树,除了最后一层有可能例外,其余层都是满的)

    完全二叉树有一个很好的性质:如果经过恰当的编号(根节点是1,之后向下每层从左向右编号),那么,节点$N$的父结点就是$N/2$(向下取整),左子结点就是$2N$,右子节点则是$2N+1$。这表明,我们不需要一棵真正的树,而只需要用数组来模拟就好;只要我们事先能控制好堆的最大规模。

    二叉堆因此具有两个主体性质:结构性(是完全二叉树)、堆序性(最值在最上面)。我们下面用小根堆举例,介绍二叉堆怎么做基本操作:

    Insert:空堆是平凡的。如果不是空堆,则新入的节点应该放在某个位置。为了维持堆的结构性,我们首先把它放在最后一层的末尾(即数组的末尾),然后通过交换元素来维持堆序。具体的方法称为向上过滤(上滤):对于这个新结点$X$,如果它的值比它的父节点还小,则交换它与它的父亲。很显然这种交换是合适的,因为它的(可能的)兄弟必然大于等于它的父亲,也就大于它。不断进行,直到不需要交换为止。从二叉堆的结构我们知道,这个操作最坏是$O(log N)$的。但是有文章指出这个操作的平均复杂度实际上能够达到$O(1)$,但我目前无力证明。

    这一操作可以简化,由于交换是一个比较浪费的操作,我们用空位置来代替:首先在末尾扩展一个空位置,检查这个空位置能不能放置新的节点$X$,如果能,则放入;如果不能,则将父节点移到这个空位置里,空位置则转移到了父节点的原本位置,重复直到新结点进入了空位置为止。Insert的某种实现如下:

     1 void Insert(int V) {
     2     if (hsize == 0) {
     3         val[root] = V;
     4         hsize++;
     5     } else {
     6         int empt = ++hsize;
     7         while (empt > root) {
     8             if (V < val[empt / 2]) {
     9                 val[empt] = val[empt / 2];
    10                 empt /= 2;
    11             } else {
    12                 val[empt] = V;
    13                 break;
    14             }
    15         }
    16         val[empt] = V;
    17     }
    18     return;
    19 }
    insert

    Pop:空堆仍然是平凡的,否则这样做:将堆顶删除,然后把最后一个元素放到堆顶,之后向下过滤(下滤):首先比较该节点与左右子节点的值,如果都是满足堆序的,就停止下滤;否则,将它与左右子节点中较小的交换,然后重复下滤直到停止或到叶子节点为止。

    与上滤一样,下滤也可以简化,只要把应该被下滤的节点变成空位置,观察这个节点能不能放到空位置,若不能,将空位置的左右子节点的较小者放至空位置,空位置下移即可。Pop操作(包括下滤操作)的某种实现如下:

     1 void downAdjust(int node) {
     2     int empt = node;
     3     int tempv = val[node];
     4     int vl, vr;
     5     do {
     6         if (empt * 2 <= hsize)
     7             vl = val[empt * 2];
     8         else
     9             vl = 0x7fffffff;
    10 
    11         if (empt * 2 + 1 <= hsize)
    12             vr = val[empt * 2 + 1];
    13         else
    14             vr = 0x7fffffff;
    15 
    16         if (vl >= tempv && vr >= tempv) {
    17             val[empt] = tempv;
    18             break;
    19         } else {
    20             if (vl < vr) {
    21                 val[empt] = vl;
    22                 empt *= 2;
    23             } else {
    24                 val[empt] = vr;
    25                 empt *= 2;
    26                 empt++;
    27             }
    28         }
    29     } while (1);
    30     return;
    31 }
    32 
    33 void Pop() {
    34     if (hsize == 0)
    35         return;
    36 
    37     val[root] = val[hsize--];
    38     downAdjust(root);
    39     return;
    40 }
    pop

    BuildHeap:从$N$个元素的数组开始建立一个堆,最简单的想法就是进行$N$次插入。它的最坏可能当然是$O(N log N)$,不过鉴于有文章提到Insert的平均复杂度是$O(1)$,也可以期待对随机数组这样建堆达到$O(N)$。不过,建堆明显有一个更好的方法:

    将数组直接建为完全二叉树(实际上不需要操作),然后从第一个有子节点的元素开始(明显是编号最大的叶子节点的父节点,也就是$N/2$),每个节点都下滤,就完成建堆。

    看上去这个操作也会是$O(N log N)$的,但是我们有如下定理告诉我们它实际上是$O(N)$:

    定理:一棵有$2^{h+1} - 1$个节点的满二叉树,每个节点的高度和是$S = 2^{h+1} - 1 - (h + 1)$。

    证明:高度是这样定义的:叶子节点的高度是$0$;每个节点的高度是它的子节点的高度$+1$。这样可以知道,定理描述的二叉树中,根节点的高度是$h$,第二层的两个节点的高度是$h-1$,以此类推。这样,我们应该有如下和式:

    $S = sum_{i=0}^{h-1} 2^i (h-i)$

    为了求这个和,两边同乘$2$

    $2S = sum_{i=1}^{h} 2^i (h-i+1)$

    上下相减就有了

    $S = -h + sum_{i=1}^{h-1} 2^i + 2^h$

    $=2^{h+1}-2-h$

    $=2^{h+1}-1 - (h+1)$

    由于刚刚的下滤建堆操作将不会进行超过$O(S)$次比较和赋值,而二叉堆是一个完全二叉树,如果$N=2^h+m$,显然有$2^h-1-h leq S leq 2^{h+1}-1-(h+1)$,因此定理告诉我们,这种建堆的操作是$O(N)$的,可见十分优越。它的某种实现如下:

     1 void BuildHeap(int A[], int size) {
     2     if (size == 0)
     3         return;
     4 
     5     hsize = size;
     6     for (int i = 0; i <= size; i++)
     7         val[i + 1] = A[i];
     8 
     9     for (int i = hsize / 2; i; i--) {
    10         downAdjust(i);
    11     }
    12     return;
    13 }
    buildheap

    这就是最简单的堆——二叉堆的简单介绍。

  • 相关阅读:
    CentOS7下搭建hadoop2.7.3完全分布式
    在linux命令行利用SecureCRT上传下载文件
    SPDY和HTTP
    哈希表的工作原理
    LVS(Linux Virtual Server)
    Discuz x3 UCenter实现同步登陆原理
    Goroutine(协程)为何能处理大并发?
    缓存与DB数据一致性问题解决的几个思路
    固态硬盘SSD与闪存(Flash Memory)
    堆和栈的区别(转过无数次的文章)
  • 原文地址:https://www.cnblogs.com/halifuda/p/14357941.html
Copyright © 2011-2022 走看看