> For the complete documentation index, see [llms.txt](https://junnie.gitbook.io/nine-chapter/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://junnie.gitbook.io/nine-chapter/8.data-structure/summary/er-cha-dui-binary-heap.md).

# 二叉堆 Binary Heap

二叉堆（Binary Heap）没什么神秘，性质比二叉搜索树 BST 还简单。其主要操作就两个，`sink`（下沉）和 `swim`（上浮），用以维护二叉堆的性质。其主要应用有两个，首先是一种排序方法「堆排序」，第二是一种很有用的数据结构「优先级队列」。

本文就以实现优先级队列（Priority Queue）为例，来讲讲一下二叉堆怎么运作的。

#### 一、二叉堆概览 <a href="#yi-er-cha-dui-gai-lan" id="yi-er-cha-dui-gai-lan"></a>

首先，二叉堆和二叉树有啥关系呢，为什么人们总是把二叉堆画成一棵二叉树？

因为，二叉堆在逻辑上其实是一种特殊的二叉树（完全二叉树），只不过存储在数组里。一般的链表二叉树，我们操作节点的指针，而在数组里，我们把数组索引作为指针：

```java
// 父节点的索引
int parent(int root) {
    return root / 2;
}
// 左孩子的索引
int left(int root) {
    return root * 2;
}
// 右孩子的索引
int right(int root) {
    return root * 2 + 1;
}
```

画个图你立即就能理解了，比如 `arr` 是一个字符数组，注意数组的第一个索引 0 空着不用：

[![](https://labuladong.github.io/algo/images/heap/1.png)](https://labuladong.github.io/algo/images/heap/1.png)

你看到了，因为这棵二叉树是「完全二叉树」，所以把 `arr[1]` 作为整棵树的根的话，每个节点的父节点和左右孩子的索引都可以通过简单的运算得到，这就是二叉堆设计的一个巧妙之处。

为了方便讲解，下面都会画的图都是二叉树结构，相信你能把树和数组对应起来。

二叉堆还分为最大堆和最小堆。**最大堆的性质是：每个节点都大于等于它的两个子节点**。类似的，最小堆的性质是：每个节点都小于等于它的子节点。

两种堆核心思路都是一样的，本文以最大堆为例讲解。

对于一个最大堆，根据其性质，显然堆顶，也就是 `arr[1]` 一定是所有元素中最大的元素。

#### 二、优先级队列概览 <a href="#er-you-xian-ji-dui-lie-gai-lan" id="er-you-xian-ji-dui-lie-gai-lan"></a>

优先级队列这种数据结构有一个很有用的功能，你插入或者删除元素的时候，元素会自动排序，这底层的原理就是二叉堆的操作。

数据结构的功能无非增删查该，优先级队列有两个主要 API，分别是 `insert` 插入一个元素和 `delMax` 删除最大元素（如果底层用最小堆，那么就是 `delMin`）。

下面我们实现一个简化的优先级队列，先看下代码框架：

> PS：这里用到 Java 的泛型，`Key` 可以是任何一种可比较大小的数据类型，比如 Integer 等类型。

```java
public class MaxPQ
    <Key extends Comparable<Key>> {
    // 存储元素的数组
    private Key[] pq;
    // 当前 Priority Queue 中的元素个数
    private int N = 0;

    public MaxPQ(int cap) {
        // 索引 0 不用，所以多分配一个空间
        pq = (Key[]) new Comparable[cap + 1];
    }

    /* 返回当前队列中最大元素 */
    public Key max() {
        return pq[1];
    }

    /* 插入元素 e */
    public void insert(Key e) {...}

    /* 删除并返回当前队列中最大元素 */
    public Key delMax() {...}

    /* 上浮第 k 个元素，以维护最大堆性质 */
    private void swim(int k) {...}

    /* 下沉第 k 个元素，以维护最大堆性质 */
    private void sink(int k) {...}

    /* 交换数组的两个元素 */
    private void exch(int i, int j) {
        Key temp = pq[i];
        pq[i] = pq[j];
        pq[j] = temp;
    }

    /* pq[i] 是否比 pq[j] 小？ */
    private boolean less(int i, int j) {
        return pq[i].compareTo(pq[j]) < 0;
    }

    /* 还有 left, right, parent 三个方法 */
}
```

空出来的四个方法是二叉堆和优先级队列的奥妙所在，下面用图文来逐个理解。

#### 三、实现 swim 和 sink <a href="#san-shi-xian-swim-he-sink" id="san-shi-xian-swim-he-sink"></a>

为什么要有上浮 `swim` 和下沉 `sink` 的操作呢？为了维护堆结构。

我们要讲的是最大堆，每个节点都比它的两个子节点大，但是在插入元素和删除元素时，难免破坏堆的性质，这就需要通过这两个操作来恢复堆的性质了。

对于最大堆，会破坏堆性质的有两种情况：

1. 如果某个节点 A 比它的子节点（中的一个）小，那么 A 就不配做父节点，应该下去，下面那个更大的节点上来做父节点，这就是对 A 进行**下沉**。
2. 如果某个节点 A 比它的父节点大，那么 A 不应该做子节点，应该把父节点换下来，自己去做父节点，这就是对 A 的**上浮**。

当然，错位的节点 A 可能要上浮（或下沉）很多次，才能到达正确的位置，恢复堆的性质。所以代码中肯定有一个 `while` 循环。

细心的读者也许会问，这两个操作不是互逆吗，所以上浮的操作一定能用下沉来完成，为什么我还要费劲写两个方法？

是的，操作是互逆等价的，但是最终我们的操作只会在堆底和堆顶进行（等会讲原因），显然堆底的「错位」元素需要上浮，堆顶的「错位」元素需要下沉。

**上浮的代码实现：**

```java
private void swim(int k) {
    // 如果浮到堆顶，就不能再上浮了
    while (k > 1 && less(parent(k), k)) {
        // 如果第 k 个元素比上层大
        // 将 k 换上去
        exch(parent(k), k);
        k = parent(k);
    }
}
```

画个 GIF 看一眼就明白了：

[![](https://labuladong.github.io/algo/images/heap/swim.gif)](https://labuladong.github.io/algo/images/heap/swim.gif)

**下沉的代码实现：**

下沉比上浮略微复杂一点，因为上浮某个节点 A，只需要 A 和其父节点比较大小即可；但是下沉某个节点 A，需要 A 和其**两个子节点**比较大小，如果 A 不是最大的就需要调整位置，要把较大的那个子节点和 A 交换。

```java
private void sink(int k) {
    // 如果沉到堆底，就沉不下去了
    while (left(k) <= N) {
        // 先假设左边节点较大
        int older = left(k);
        // 如果右边节点存在，比一下大小
        if (right(k) <= N && less(older, right(k)))
            older = right(k);
        // 结点 k 比俩孩子都大，就不必下沉了
        if (less(older, k)) break;
        // 否则，不符合最大堆的结构，下沉 k 结点
        exch(k, older);
        k = older;
    }
}
```

画个 GIF 看下就明白了：

[![](https://labuladong.github.io/algo/images/heap/sink.gif)](https://labuladong.github.io/algo/images/heap/sink.gif)

至此，二叉堆的主要操作就讲完了，一点都不难吧，代码加起来也就十行。明白了 `sink` 和 `swim` 的行为，下面就可以实现优先级队列了。

#### 四、实现 delMax 和 insert <a href="#si-shi-xian-delmax-he-insert" id="si-shi-xian-delmax-he-insert"></a>

这两个方法就是建立在 `swim` 和 `sink` 上的。

**`insert` 方法先把要插入的元素添加到堆底的最后，然后让其上浮到正确位置**。

[![](https://labuladong.github.io/algo/images/heap/insert.gif)](https://labuladong.github.io/algo/images/heap/insert.gif)

```java
public void insert(Key e) {
    N++;
    // 先把新元素加到最后
    pq[N] = e;
    // 然后让它上浮到正确的位置
    swim(N);
}
```

**`delMax` 方法先把堆顶元素 `A` 和堆底最后的元素 `B` 对调，然后删除 `A`，最后让 `B` 下沉到正确位置**。

```java
public Key delMax() {
    // 最大堆的堆顶就是最大元素
    Key max = pq[1];
    // 把这个最大元素换到最后，删除之
    exch(1, N);
    pq[N] = null;
    N--;
    // 让 pq[1] 下沉到正确位置
    sink(1);
    return max;
}
```

[![](https://labuladong.github.io/algo/images/heap/delete.gif)](https://labuladong.github.io/algo/images/heap/delete.gif)

至此，一个优先级队列就实现了，插入和删除元素的时间复杂度为 `O(logK)`，`K` 为当前二叉堆（优先级队列）中的元素总数。因为我们时间复杂度主要花费在 `sink` 或者 `swim` 上，而不管上浮还是下沉，最多也就树（堆）的高度，也就是 log 级别。

#### 五、最后总结 <a href="#wu-zui-hou-zong-jie" id="wu-zui-hou-zong-jie"></a>

二叉堆就是一种完全二叉树，所以适合存储在数组中，而且二叉堆拥有一些特殊性质。

二叉堆的操作很简单，主要就是上浮和下沉，来维护堆的性质（堆有序），核心代码也就十行。

优先级队列是基于二叉堆实现的，主要操作是插入和删除。插入是先插到最后，然后上浮到正确位置；删除是调换位置后再删除，然后下沉到正确位置。核心代码也就十行。

也许这就是数据结构的威力，简单的操作就能实现巧妙的功能，真心佩服发明二叉堆算法的人！

#### **六.Heap Sort 原理**

<http://www.itdadao.com/articles/c15a1041906p0.html>

<http://bubkoo.com/2014/01/14/sort-algorithm/heap-sort/>

堆排序就是把最大堆堆顶的最大数取出，将剩余的堆继续调整为最大堆，再次将堆顶的最大数取出，这个过程持续到剩余数只有一个时结束。在堆中定义以下几种操作：

* 最大堆调整（Max-Heapify）：将堆的末端子节点作调整，使得子节点永远小于父节点
* 创建最大堆（Build-Max-Heap）：将堆所有数据重新排序，使其成为最大堆
* 堆排序（Heap-Sort）：移除位在第一个数据的根节点，并做最大堆调整的递归运算

最大堆调整（MAX‐HEAPIFY）的作用是保持最大堆的性质，是创建最大堆的核心子程序，

![](/files/-M33gyNp54U34XGT2Ywu)

```
/**
 * 从 index 开始检查并保持最大堆性质
 *
 * @array
 *
 * @index 检查的起始下标
 *
 * @heapSize 堆大小
 *
 **/
function maxHeapify(array, index, heapSize) {
  var iMax = index,
      iLeft = 2 * index + 1,
      iRight = 2 * (index + 1);
  if (iLeft < heapSize && array[index] < array[iLeft]) {
    iMax = iLeft;
  }
  if (iRight < heapSize && array[iMax] < array[iRight]) {
    iMax = iRight;
  }
  if (iMax != index) {
    swap(array, iMax, index);
    maxHeapify(array, iMax, heapSize); // 递归调整
  }
}
function swap(array, i, j) {
  var temp = array[i];
  array[i] = array[j];
  array[j] = temp;
}
```

创建最大堆（Build-Max-Heap）的作用是将一个数组改造成一个最大堆，接受数组和堆大小两个参数，Build-Max-Heap 将**自下而上**的调用 Max-Heapify 来改造数组，建立最大堆。

因为 Max-Heapify 能够保证下标 i 的结点之后结点都满足最大堆的性质，所以自下而上的调用 Max-Heapify 能够在改造过程中保持这一性质。如果最大堆的数量元素是 n，那么 Build-Max-Heap 从 Parent(n) 开始，往上依次调用 Max-Heapify。流程如下：

![](/files/-M33gyNrBIkGMgeRmldL)

```
public static void buildheap(int[] array)
    {
        size=array.length;

        for(int i=array.length/2;i>=0;i--)
        {
            maxheap(array,i);
        }
    }
```

堆排序（Heap-Sort）是堆排序的接口算法，Heap-Sort先调用Build-Max-Heap将数组改造为最大堆，然后将堆顶和堆底元素交换，之后将底部上升，最后重新调用Max-Heapify保持最大堆性质。由于堆顶元素必然是堆中最大的元素，所以一次操作之后，堆中存在的最大元素被分离出堆，重复n-1次之后，数组排列完毕。整个流程如下：

![](/files/-M33gyNtFNrIaTv_t0Yt)

```
public static void heapsort(int[] a)
    {
        array=a;
        buildheap(array);//first initial the tree


        for(int i=size-1;i>=0;i--)
        {   

            //swap(array[0],array[i]);
            int temp;
            temp=array[0];
            array[0]=array[i];
            array[i]=temp;

            size=size-1; //size is the static member of class,so change once then every value in class will change

            maxheap(array,0);//rebuild the heap of whole tree            
        }    
    }
```

```
public static void maxHeapify(int[] A,int fathernodeindex)//i is the index of father node
    {

        int left=fathernodeindex*2+1; //  
        int right=fathernodeindex*2+2;
        int largest=fathernodeindex;

        if(left<A.length && A[left]>A[largest]){
            largest=left;
        }
        if(right<A.length && A[right]>A[largest]){
            largest=right;
        }

        swap(A[fathernodeindex],A[largest]);
        maxHeapify(A,largest);

    }
public static void buildMaxHeap(int[] A){

        for(int i=A.length/2;i>=0;i--){
            maxHeapify(A,i);
        }
    }
public static void heapSort(int[] A){

        if(A==null ||A.length==0){
            return;
        }

        buildMaxHeap(A);
        int len=A.length;
        for(int i=A.length-1;i>=0;i--){

            //swap(A[0],A[i]);
            swap(A[0],A[i]);
            len--;
            maxHeapify(A,0);//rebuild the heap of whole tree

            //wrong,need a attribute size

        }

    }
```
