[ CSDN 转载 ] 数据结构——堆

文章转载自 数据结构——堆(带图详解)_大根堆-CSDN博客 ,有删改

目录

堆的概念

堆的性质

堆的创建

1、堆向下调整

2、堆的创建

3、建堆的时间复杂度

堆的插入和删除

1、堆的插入

2、堆的删除

堆的应用

1、优先级队列的实现

2、堆排序

3、Top-k问题


堆 (Heap)

堆的概念

前面介绍的优先级队列在JDK1.8中其底层使用了堆的数据结构,而堆实际就是在完全二叉树的基础之上进行了一些元素的调整。

如果有一个 关键码的集合 K = {k0 k1 k2 kn-1} ,把它的所有元素 按完全二叉树的顺序存储方式存储 在一 个一维数组中 ,并满足: Ki <= K2i+1 Ki<= K2i+2 (Ki >= K2i+1 且 Ki >=K2i+2) i = 0 , 1 , 2… ,则 称为小堆 ( 或大堆) 。(即双亲比孩子的数值小(大)——小(大)堆)将根节点最大的堆叫做最大堆或大根堆,根节点最小的堆叫做最小堆或小根堆。

堆的性质

  • 堆中某个节点的值总是不大于或不小于其父节点的值;
  • 堆总是一棵完全二叉树。

下面来看一下堆的可视化操作

堆的创建

1、堆向下调整

对于集合 { 27,15,19,18,28,34,65,49,25,37 } 中的数据,如果将其创建成堆呢?

仔细观察上图后发现: 根节点的左右子树已经完全满足堆的性质,因此只需将根节点向下调整好即可

向下过程 ( 以小堆为例 )

1. 让 parent 标记需要调整的节点, child 标记 parent 的左孩子 ( 注意: parent 如果有孩子一定先是有左孩子 )

2. 如果 parent 的左孩子存在,即 :child < size , 进行以下操作,直到 parent 的左孩子不存在

  • parent右孩子是否存在,存在找到左右孩子中最小的孩子,让child进行标记
  • 将parent与较小的孩子child比较,如果:parent小于较小的孩子child,调整结束。否则:交换parent与较小的孩子child,交换完成之后,parent中大的元素向下移动,可能导致子树不满足对的性质,因此需要继续向下调整,即parent = child;child = parent*2+1; 然后继续2。

1.  public void shiftDown(int[] array, int parent) {
    
2.      // child先标记parent的左孩子,因为parent可能右左没有右
    
3.      int child = 2 * parent + 1;
    
4.      int size = array.length;
    
5.      while (child < size) {
    
6.             // 如果右孩子存在,找到左右孩子中较小的孩子,用child进行标记
    
7.             if(child+1 < size && array[child+1] < array[child]){
    
8.                   child += 1;
    
9.             }
    

11.             // 如果双亲比其最小的孩子还小,说明该结构已经满足堆的特性了
    
12.             if (array[parent] <= array[child]) {
    
13.                   break;
    
14.             }else{
    
15.                   // 将双亲与较小的孩子交换
    
16.                   int t = array[parent];
    
17.                   array[parent] = array[child];
    
18.                   array[child] = t;
    

20.                   // parent中大的元素往下移动,可能会造成子树不满足堆的性质,因此需要继续向下调整
    
21.                   parent = child;
    
22.                   child = parent * 2 + 1;
    
23.             }
    
24.      }
    
25.  }

注意:在调整以 parent 为根的二叉树时,必须要满足 parent 的左子树和右子树已经是堆了才可以向下调整。

时间复杂度分析:

最坏的情况即图示的情况,从根一路比较到叶子,比较的次数为完全二叉树的高度,即时间复杂度为O(log₂N)

2、堆的创建

那对于普通的序列 { 1,5,3,8,7,6 } ,即根节点的左右子树不满足堆的特性,又该如何调整呢?

1.  public static void createHeap(int[] array) {
    
2.      // 找倒数第一个非叶子节点,从该节点位置开始往前一直到根节点,遇到一个节点,应用向下调整
    
3.      for(int root = (array.length-2)/2; root >= 0; root--){
    
4.              shiftDown(array, array.length, root);
    
5.      }
    
6.  }

3、建堆的时间复杂度

因为堆是完全二叉树,而满二叉树也是完全二叉树,此处为了简化使用满二叉树来证明 ( 时间复杂度本来看的就是近似值,多几个节点不影响最终结果) :

因此:**建堆的时间复杂度为O(N) **

堆的插入和删除

1、堆的插入

堆的插入总共需要两个步骤:

  1. 先将元素放入到底层空间中(注意:空间不够时需要扩容)
  2. 将最后新插入的节点向上调整,直到满足堆的性质

1.  public void shiftUp(int child) {
    
2.      // 找到child的双亲
    
3.      int parent = (child - 1) / 2;
    

5.      while (child > 0) {
    
6.      // 如果双亲比孩子大,parent满足堆的性质,调整结束
    
7.            if (array[parent] > array[child]) {
    
8.                break;
    
9.            }else{
    
10.            // 将双亲与孩子节点进行交换
    
11.            int t = array[parent];
    
12.            array[parent] = array[child];
    
13.            array[child] = t;
    

15.            // 小的元素向下移动,可能到值子树不满足对的性质,因此需要继续向上调增
    
16.            child = parent;
    
17.            parent = (child - 1) / 2;
    
18.            }
    
19.      }
    
20.  }

2、堆的删除

堆的删除一定删除的是堆顶元素。

堆的删除步骤如下:

  1. 将堆顶元素对堆中最后一个元素交换
  2. 将堆中有效数据个数减少一个
  3. 对堆顶元素进行向下调整
1.  public static void shiftDown(int[] array, int size, int parent){
    
2.          int child = parent*2+1;
    

4.          while(child < size){
    
5.              // 找左右孩子中较大的孩子
    
6.              if(child+1 < size && array[child+1] > array[child]){
    
7.                  child += 1;
    
8.              }
    

10.              // 双亲小于交大的孩子
    
11.              if(array[parent] < array[child]){
    
12.                  swap(array, parent, child);
    
13.                  parent = child;
    
14.                  child = parent*2+1;
    
15.              }else{
    
16.                  return;
    
17.              }
    
18.          }
    
19.  }

堆的应用

1、优先级队列的实现

用堆作为底层结构 封装优先级队列

1.  public class MyPriorityQueue {
    
2.      Integer[] array;
    
3.      int size;   // 有效元素的个数
    

5.      public MyPriorityQueue(){
    
6.          array = new Integer[11];
    
7.          size = 0;
    
8.      }
    

10.      public MyPriorityQueue(int initCapacity){
    
11.          if(initCapacity < 1){
    
12.              throw new IllegalArgumentException("初始容量小于1");
    
13.          }
    

15.          array = new Integer[initCapacity];
    
16.          size = 0;
    
17.      }
    

19.      public MyPriorityQueue(Integer[] arr){
    
20.          // 1. 将arr中的元素拷贝到数组中
    
21.          array = new Integer[arr.length];
    
22.          for(int i = 0; i < arr.length; ++i){
    
23.              array[i] = arr[i];
    
24.          }
    
25.          size = arr.length;
    

27.          // 2. 找当前完全二叉树中倒数第一个叶子节点
    
28.          //    注意:倒数第一个叶子节点刚好是最后一个节点的双亲
    
29.          //    最后一个节点的编号size-1  倒数第一个非叶子节点的下标为(size-1-1)/2
    
30.          int lastLeafParent = (size-2)/2;
    

32.          // 3. 从倒数第一个叶子节点位置开始,一直到根节点的位置,使用向下调整
    
33.          for(int root = lastLeafParent; root >= 0; root--){
    
34.              shiftDown(root);
    
35.          }
    
36.      }
    

38.      boolean offer(Integer e){
    
39.          if(e == null){
    
40.              throw new NullPointerException("插入时候元素为null");
    
41.          }
    

43.          ensureCapacity();
    

45.          array[size++] = e;
    

47.          // 注意:当新元素插入之后,可能会破坏对的性质---需要向上调整
    
48.          shiftUp(size-1);
    
49.          return true;
    
50.      }
    

52.      // 将堆顶的元素删除掉
    
53.      public Integer poll(){
    
54.          if(isEmpty()){
    
55.              return null;
    
56.          }
    

58.          Integer ret = array[0];
    

60.          // 1. 将堆顶元素与堆中最后一个元素交换
    
61.          swap(0, size-1);
    

63.          // 2. 将堆中有效元素个数减少一个
    
64.          size--;  // size -= 1;
    

66.          // 3. 将堆顶元素往下调整到合适位置
    
67.          shiftDown(0);
    
68.          return ret;
    
69.      }
    

71.      public int size(){
    
72.          return size;
    
73.      }
    

75.      public boolean isEmpty(){
    
76.          return size == 0;
    
77.      }
    

79.      public void clear(){
    
80.          size = 0;
    
81.      }
    

83.      // 功能:调整以parent为根的二叉树
    
84.      //    前提:必须要保证parent的左右子树已经满足堆的特性
    
85.      // 时间复杂度:O(logN)
    
86.      private void shiftDown(int parent){
    
87.          // 默认让child先标记左孩子---因为:parent可能有左没有右
    
88.          int child = parent*2 + 1;
    

90.          // while循环条件可以保证:parent的左孩子一定存在
    
91.          //       但是不能保证parent的右孩子是否存在
    
92.          while(child < size){
    
93.              // 1. 找到左右孩子中较小的孩子
    
94.              if(child+1 < size && array[child+1] < array[child]){
    
95.                  child += 1;
    
96.              }
    

98.              // 2. 较小的孩子已经找到了
    
99.              //    检测双亲和孩子间是否满足堆的特性
    
100.              if(array[parent] > array[child]){
    
101.                  swap(parent, child);
    

103.                  // 大的双亲往下走了,可能会导致子树又不满足堆的特性
    
104.                  // 因此需要继续往下调整
    
105.                  parent = child;
    
106.                  child = parent*2 + 1;
    
107.              }else{
    
108.                  // 以parent为根的二叉树已经是堆了
    
109.                  return;
    
110.              }
    
111.          }
    
112.      }
    

114.      private void shiftUp(int child){
    
115.          int parent = (child-1)/2;
    

117.          while(child != 0){
    
118.              if(array[child] < array[parent]){
    
119.                  swap(child, parent);
    
120.                  child = parent;
    
121.                  parent = (child-1)/2;
    
122.              }else{
    
123.                  return;
    
124.              }
    
125.          }
    
126.      }
    

128.      private void ensureCapacity(){
    
129.          if(array.length == size){
    
130.              int newCapacity = array.length*2;
    
131.              array = Arrays.copyOf(array, newCapacity);
    
132.          }
    
133.      }
    

135.      // 注意:left和right是数组的下标
    
136.      private void swap(int left, int right){
    
137.          int temp = array[left];
    
138.          array[left] = array[right];
    
139.          array[right] = temp;
    
140.      }

2、堆排序

堆排序即利用堆的思想来进行排序,总共分为两个步骤:

1. 建堆

  • 升序:建大堆
  • 降序:建小堆

2. 利用堆删除思想来进行排序

建堆和堆删除中都用到了向下调整,因此掌握了向下调整,就可以完成堆排序

1.  public static void swap(int[] array, int left, int right){
    
2.          int temp = array[left];
    
3.          array[left] = array[right];
    
4.          array[right] = temp;
    
5.      }
    

7.      public static void shiftDown(int[] array, int size, int parent){
    
8.          int child = parent*2+1;
    

10.          while(child < size){
    
11.              // 找左右孩子中较大的孩子
    
12.              if(child+1 < size && array[child+1] > array[child]){
    
13.                  child += 1;
    
14.              }
    

16.              // 双亲小于交大的孩子
    
17.              if(array[parent] < array[child]){
    
18.                  swap(array, parent, child);
    
19.                  parent = child;
    
20.                  child = parent*2+1;
    
21.              }else{
    
22.                  return;
    
23.              }
    
24.          }
    
25.      }
    

27.      // 假设:升序
    
28.      public static void heapSort(int[] array){
    
29.          // 1. 建堆----升序:大堆    降序:小堆---向下调整
    
30.          for(int root = (array.length-2)/2; root >= 0; root--){
    
31.              shiftDown(array, array.length, root);
    
32.          }
    

35.          // 2. 利用堆删除的思想来排序---向下调整
    
36.          int end = array.length-1;   // end标记最后一个元素
    
37.          while(end != 0){
    
38.              swap(array,0,end);
    
39.              shiftDown(array, end,0);
    
40.              end--;
    
41.          }
    
42.      }

3、Top-k问题

TOP-K 问题:即求数据结合中前 K 个最大的元素或者最小的元素,一般情况下数据量都比较大

对于 Top-K 问题,能想到的最简单直接的方式就是排序,但是:如果数据量非常大,排序就不太可取了 ( 可能数据都不能一下子全部加载到内存中) 。最佳的方式就是用堆来解决,基本思路如下:

1. 用数据集合中前 K 个元素来建堆

  • 前k个最大的元素,则建小堆
  • 前k个最小的元素,则建大堆

2. 用剩余的 N-K 个元素依次与堆顶元素来比较,不满足则替换堆顶元素

将剩余 N-K 个元素依次与堆顶元素比完之后,堆中剩余的 K 个元素就是所求的前 K 个最小或者最大的元素。

Top-k问题

1.  class Solution {
    
2.      public int[] smallestK(int[] arr, int k) {
    
3.          int[] vec = new int[k];
    
4.          if (k == 0) { // 排除 0 的情况
    
5.              return vec;
    
6.          }
    
7.          PriorityQueue<Integer> queue = new PriorityQueue<Integer>(new Comparator<Integer>() {
    
8.              public int compare(Integer num1, Integer num2) {
    
9.                  return num2 - num1;
    
10.              }
    
11.          });
    
12.          for (int i = 0; i < k; ++i) {
    
13.              queue.offer(arr[i]);
    
14.          }
    
15.          for (int i = k; i < arr.length; ++i) {
    
16.              if (queue.peek() > arr[i]) {
    
17.                  queue.poll();
    
18.                  queue.offer(arr[i]);
    
19.              }
    
20.          }
    
21.          for (int i = 0; i < k; ++i) {
    
22.              vec[i] = queue.poll();
    
23.          }
    
24.          return vec;
    
25.      }
    
26.  }

复杂度分析

时间复杂度O(nlog k),其中 n 是数组 arr 的长度。由于大根堆实时维护前 k 小值,所以插入删除都是O(logk) 的时间复杂度,最坏情况下数组里 n 个数都会插入,所以一共需要 O(nlogk) 的时间复杂度。

空间复杂度O(k),因为大根堆里最多 k 个数