数据结构——堆和堆的应用

一、什么是堆?

堆是一种特殊的树,堆要满足下面两点。
1、堆是一个完全二叉树;
2、堆中每一个节点的值都必须大于等于(或小于等于)其子树中每个节点的值。
通过下图可以更好理解:


根据堆的要求我们可以知道,堆有两种形式;对于每个节点的值都大于等于子树中每个节点值的堆叫大顶堆,对于每个节点的值都小于等于子树中每个节点值的堆,叫作小顶堆

二、怎么实现一个堆?

要实现一个堆最重要的一个操作就是堆化,堆化有两种方法。(下面的案例我们用小顶堆来演示)

这里有个小提示: 我们用数组来实现堆时,一般都假设堆中的数据是从数组下标为1的位置开始存储,如果节点的下标是i,那左子节点的下标就是2 * i,右子节点的下标就是2 * i + 1,父节点的下标就是 i/2;(如果从0开始的话,下标会多一次运算。)

1、从下往上的堆化
让新插入的节点与父节点对比大小。如果不满足子节点大于等于父节点,就互换两个节点。一直重复这个过程,直到父子节点之间满足堆的第二个要求。
我们用代码实现如下:

public class ArrayHeap(
  // 数组,从下标1开始存储数据  
  private int[] root; 
  // 堆的大小
  private int size;
  // 堆已经存储了多少个数据
  private int count;

  public ArrayHeap(int capacity){
    root = new int[capacity +1];
    size = capacity;
    count = 0;
  }

  /**
   * 插入数据
   * 
   * @param data 要插入的数据
   * @return 返回是否成功
  */
   public boolean insert(int data){
    // 堆满了
    if(count >= size) return false;
    root[++count] = data;
    // 堆化
    heapify(root, count);
    reutrn true;
  }
  /**
   * 自下而上堆化
   *
   * @param arr 数组
   * @param i 节点下标
  */
  public void heapify(int[] arr, int i){
    while( i/2 > 0 && a[i] < arr[i/2]){ // 当节点小于父节点时
        // 交换
        swap(arr, i, i/2);
        // 继续判断
        i = i/2;
    }
  }

// 交换两个位置的值
 private void swap(int[] arr, int u, int v){
    int temp = arr[u];
    arr[u] = arr[v];
    arr[v] = temp;
 }
}

2、从上往下的堆化
我们把最后一个节点放到堆顶,然后利用同样的父子节点对比方法,对于不满足父子节点大小关系的,互换两个节点,并且重复进行这个过程,直到父子节点之间满足大小关系为止。
这里我们实现移除堆顶节点,代码如下:

/**
 * 删除堆顶元素
*/
public int removeMin(){
  // 堆中没有数据
  if(count == 0) return -1;
  int data = root[1];
  // 把最后一个节点放到堆顶
  root[1] = root[count]
  // 堆化
  heapifyMin(root, count, 1);
  return data;
}

/**
  * 自上而下的堆化操作
  *
  *@param arr 数组
  *@param n 堆中元素个数
  *@param i 当前节点
*/
public void heapifyMin(int[] arr, int n, int i){
  while(true){
    // 找出要与节点 i 交换的子节点 
    int minPos = i;
    if(i*2 <= n && arr[i*2] < arr[minPos]){
      minPos = i * 2;
    }
    if(i*2 + 1 <= n && arr[i*2 + 1] < arr[minPos]){
      minPos = i * 2 + 1;
    }
    // 如果没有要交换的,说明子节点都大于节点i
    if(i == minPos){
      return;
    }
    // 互换位置
    swap(arr, i, minPos);
    i = minPos;
 }
}

3、建堆和排序
不借助另一个数组,就在原数组上操作。有两种思路。

第一种,把元素一个个往堆中插入,利用上面的插入操作,就可以组成堆。

第二种,从后往前处理数组,并且每个数据都是从上往下堆化,跟上面删除堆顶的堆化操作一样。

第二种实现代码如下:

 public void buildHeap(int[] arr, int n){
     for(int i = n/2; i >= 1; i--){
       // 调用上面删除堆顶的堆化操作
       heapifyMin(arr, n, i);
    } 
 }

建堆的实际时间复杂度是O(n), 按道理来说每个节点堆化的时间复杂度是O(logn),叶子节点是不需要堆化的,所以需要堆化的节点是n/2 + 1个节点,那是不是堆化的总时间复杂度就是O(nlog)呢。我们来推导一下。

层数 高度 节点个数 每层的时间复杂度
1 h 20 1* h
2 h-1 21 2 * (h-1)
3 h-2 22 4 * (h-2)
4 h-3 23 8 * (h-2)
... ... ... ...
h-1 1 2h-1 2h-1 * 1

总的时间复杂度 = 每层的时间复杂度之和
f(n) = 1h + 21 * (h-1) + 22 * (h-2)+ 23 * (h-3) + ... + 2h-1 * 1
把两边都乘2
2 * f(n) = 21
h + 22 * (h-1) + 23 * (h-2) + 24 * (h-3) + ... + 2h * 1
两个相减
f(n) = -h + 2 + 22 + 2 + 23 + ...+ 2k + ... + 2h-1 +2h = 2h+1 - h -2

因为 h = log2n 代入公式f(n),就能得到 f(n) = O(n),所以,建堆的时间复杂度就是O(n)。

现在我们把建好堆的数组进行排序,这个过程有点类似上面删除堆顶元素,当堆顶的元素移除之后,把下标为n的元素放到堆顶,将移除的元素放到下标n的位置,将剩下n-1个元素重新构建成堆。一直重复到下标为1的一个元素,排序工作就完成了。代码如下:

/**
 * 排序
 * @param arr 数组
 * @param n 数据的个数,数组arr中的数据从下标1到n的位置。
*/
public static void sort(int[] arr, int n){
  buildHeap(arr, n);
  for(int i = n; i > 1; i--){
    // 交换位置
    swap(arr, 1, i);
    // 重新堆化
    headify(arr, i, 1);
  }
}

三、堆的应用

1、优先级队列
我们知道队列最大的特性就是先进先出。但在优先级队列,是按照优先级来,优先级最高的,最先出队。

一个堆就可以看作一个优先级队列 。在优先级队列中插入一个元素,就相当于往堆中插入一个元素;从优先级队列中取出优先级最高的元素,就相当于取出堆顶元素(这里的堆是大顶堆);常见案例有 合并有充小文件、高性能定时器

2、利用堆求 Top K
如何在一个包含n个数据的数组中,查找前K大数据呢?先维护一个大小为K的小顶堆,顺序遍历数组,从数组中取出数据与堆顶元素比较。如果比堆顶元素大,我们就把堆顶元素删除,并且将这个元素插入到堆中;如果比堆顶元素小,则不做处理,继续遍历数组。等遍历完之后,堆中数据就是前K大元素。

2、利用堆求中位数
借助堆这种数据结构,我们不用排序,就可以非常高效地实现求中位数的操作。具体实现代码如下:

    /**
     * 用堆求数组的中位数;维护两个堆,大顶堆中存储前半部分数据,
     * 小顶堆中存储后半部分数据。且小顶堆中的数据都大于大顶堆中的数据
     *
     * @param arr 数组
     * @param n   数据个数
     * @return 中位数
     */
    public int getMedian(int[] arr, int n) {
        // 计算两个顶堆的大小
        int minSize = n / 2;
        int maxSize = n / 2;
        // 如果数组大小是奇数,大顶堆存储 n/2+1个数据
        // 这样就不管数组大小是奇偶,中位值都是大顶堆的第一个值
        if (n % 2 == 1) {
            maxSize = n / 2 + 1;
        }
         // 大顶堆 
        ArrayHeap heapMax = new ArrayHeap(maxSize);
        // 小顶堆 
        ArrayHeap heapMin = new ArrayHeap(minSize);
        // 往两个堆中添加数据,利用小顶堆的特性把数组前minSize大的值放入heapMin中
        for (int i = 0; i < n; i++) {
            // 如果heapMin满了
            if (heapMin.isFull()) {
                int temp = arr[i];
                // 如果当前值大于heapMin第一个值,说明heapMin中的值还不是数组中最大的值
                if (arr[i] > heapMin.root[1]) {
                    // 把heapMin第一个值替换成当前值
                    temp = heapMin.root[1];
                    heapMin.root[1] = arr[i];
                    // 重新堆化heapMin
                    heapMin.heapipyMin(heapMin.root, minSize, 1);
                }
                // 把小顶堆踢出来的值或者小于小顶堆第一个值添加到hemapMax中
                heapMax.insertMax(temp);
            } else {
                // 把数据插入heapMin
                heapMin.insertMin(arr[i]);
            }
        }
        return heapMax.root[1];
    }
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 217,734评论 6 505
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,931评论 3 394
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 164,133评论 0 354
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,532评论 1 293
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,585评论 6 392
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,462评论 1 302
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,262评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,153评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,587评论 1 314
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,792评论 3 336
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,919评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,635评论 5 345
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,237评论 3 329
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,855评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,983评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,048评论 3 370
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,864评论 2 354

推荐阅读更多精彩内容