数据结构-堆
1. 堆的概念
1.1 堆介绍
(1)堆
-
堆是用完全二叉树的结构来维护的一组数据。
-
完全二叉树:设二叉树的深度为h,除第 h 层外,其它各层 (1~h-1) 的结点数都达到最大个数,第 h 层所有的结点都连续集中在最左边的树
(2)完全二叉树的特性
- 倘若按完全二叉树来排布的数据结构,按照从根层到叶子层,从左及右的顺序,给结点编号,其顺序恰好和一维数组的顺序号相对应,即完全利用了数组的空间,不会在最后一个叶子结点之前有空置数组单元。
- 对于完全二叉树来说:
- 左子结点: l e f t n = f a t h e r n ∗ 2 + 1 left_n = father_n * 2+1 leftn=fathern∗2+1;对于从1号开始的有 l e f t n = f a t h e r n ∗ 2 left_n = father_n * 2 leftn=fathern∗2
- 右子结点: r i g h t n = f a t h e r n ∗ 2 + 2 right_n=father_n*2+2 rightn=fathern∗2+2;对于从1号开始的有 r i g h t n = f a t h e r n ∗ 2 right_n=father_n*2 rightn=fathern∗2
- 父结点: f a t h e r = l e f t n / 2 = r i g h t n / 2 father=left_n/2=right_n/2 father=leftn/2=rightn/2,(向下取整)
1.2 堆的分类
(1)小根堆
- 对于堆中的每个结点的左右子结点的值都要小于该结点的堆称为最小堆(很多时候也可以看为小于等于);
heap[i]<heap[2*i+1] && heap[i]<head[2*i+2]
(2)大根堆
- 对于堆中的每个结点的左右子结点的值都要大于该结点的堆称为最小堆(很多时候也可以看为大于等于);
heap[i]>heap[2*i+1] && heap[i]> head[2*i+2]
1.3 堆的实现–小根堆
(1)插入(也是建立过程)
-
思想:
- 插入元素时,将其放到数组末尾(最后结点位置+1);
- 调整结点位置:插入结点的值不断地与父结点的值进行比较,如果小于就和父结点进行值的交换;父结点作为当前结点,递归直至该当前结点大于等于父结点或者称为根结点;
(2)取出堆顶元素
-
删除堆顶元素后,将末尾元素置换上来,然后进行下沉;
1.4 堆的实现–大根堆
(1)插入
- 大根堆与小根堆插入过程相同,只不过是比较时将大元素浮上去;
(2)删除堆顶元素
- 大根堆与小根堆过程删除类似,不再赘述;
1.5 堆的应用–堆排序
(1)堆排序
-
利用小根堆每次取到的元素都是最小值的特性,我们可以将元素全部存储到最小堆中,不断取堆顶元素,输出的即是顺序序列。同理,也可以利用大根堆进行逆序排序;
-
堆排序是一种选择排序,整体主要由构建初始堆+交换堆顶元素和末尾元素并重建堆两部分组成。其中构建初始堆经推导复杂度为O(n),在交换并重建堆的过程中,需交换n-1次,而重建堆的过程中,根据完全二叉树的性质,
[log2(n-1),log2(n-2)...1]
逐步递减,近似为nlogn
。所以堆排序时间复杂度一般认为就是O(nlogn)
级。- 最好情况:
O(nlogn)
- 最坏情况:
O(nlogn)
- 平均情况:
O(nlogn)
- 空间复杂度:
O(1)
,但是如果之前使用其他容器,之后使用堆再次构建集合,则该次空间复杂度为O(n)
(视情况而定) - 稳定性:
不稳定
,即相同值的元素经过操作后相对位置可能会发生改变。
@Test public void testSort(){ MinHeap<Integer> heap = MinHeap.toMinHeap(new Integer[]{ 1,91,61,15,13,61,21,-1,-13,-13,-13,-13,-131,123,103 }); while(!heap.isEmpty()){ System.out.print(heap.pop() + " " ); } } -131 -13 -13 -13 -13 -1 1 13 15 21 61 61 91 103 123
- 最好情况:
2 实现代码
2.1 Java实现
(1)小根堆
-
小根堆代码:
package com.niss.datastructrue.haep; /** * @author Ni187 */ public class MinHeap<T extends Comparable<T>> { private int size; private T[] heap; public MinHeap(int capacity) { if (capacity <= 0) { throw new RuntimeException("???你想干什么???"); } this.heap = (T[]) new Comparable[capacity + 1]; size = 0; } public int getSize() { return size; } // TODO 扩容操作,待完成 private void resize() { throw new RuntimeException("超出容量"); } public boolean isEmpty() { return size == 0; } public static <E extends Comparable<E>> MinHeap toMinHeap(E[] arr) { if (arr == null) { throw new RuntimeException("!!!"); } MinHeap<E> heap = new MinHeap(arr.length + 1); for (E e : arr) { if (e != null) { heap.push(e); } } return heap; } public void push(T value) { if (size >= heap.length) { resize(); } int curIndex = size; int parentIdx = curIndex / 2; heap[size] = value; while (curIndex > 0 && value.compareTo(heap[parentIdx]) <= 0) { swap(parentIdx, curIndex); curIndex = parentIdx; parentIdx /= 2; } ++size; } public T peek() { if (size == 0) { throw new RuntimeException("空堆"); } return heap[size - 1]; } public T pop() { if (size == 0) { throw new RuntimeException("空堆"); } T value = heap[0]; heap[0] = heap[size - 1]; heap[size] = null; sink(0); size--; return value; } /** * 堆元素下沉 * * @param index 当前元素 */ private void sink(int index) { int leftIdx = index * 2 + 1; int rightIdx = index * 2 + 2; if (rightIdx >= size) { // 只有左子节点,如果左子节点比父结点小,则父结点与左子节点交换(下沉) if (leftIdx < size && heap[index].compareTo(heap[leftIdx]) > 0) { swap(leftIdx, index); return; } return; } /* 0 : 当前结点 <= 两个子节点 1 : 当前结点 > 左子节点 2 : 当前结点 > 右子节点 */ boolean ltRight = heap[index].compareTo(heap[rightIdx]) < 0; boolean ltLeft = heap[index].compareTo(heap[leftIdx]) < 0; int condition = ltLeft && ltRight ? 0 : !ltLeft ? 1 : 2; switch (condition) { case 0: return; case 1: { swap(index, leftIdx); sink(leftIdx); } // 因为我首先比较的是左子树, 左沉底之后必须再次与右子树比较,不然会出现堆顶元素 > 右子树的情况 case 2: { if (heap[index].compareTo(heap[rightIdx]) > 0) { swap(index, rightIdx); sink(rightIdx); } } default: break; } } private void swap(int left, int right) { T temp = heap[left]; heap[left] = heap[right]; heap[right] = temp; } @Override public String toString() { StringBuilder sb = new StringBuilder(); for (T v : heap) { if (v == null) { break; } sb.append(v).append(' '); } return sb.toString(); } }
(2)大根堆
-
大根堆代码
package com.niss.datastructrue.haep; /** * @author Ni187 */ public class MaxHeap<T extends Comparable<T>> { private T[] heap; private int size; public MaxHeap(int capacity) { if (capacity <= 0) { throw new RuntimeException("??想挨打吗??"); } this.heap = (T[]) new Comparable[capacity + 1]; size = 0; } public static <T extends Comparable<T>> MaxHeap<T> toMaxHeap(T[] arr) { MaxHeap<T> heap = new MaxHeap<>(arr.length); for (T v : arr) { heap.push(v); } return heap; } public boolean isEmpty() { return size == 0; } public int size() { return size; } public void resize() { throw new RuntimeException("懒得弄扩容。。。"); } public void push(T value) { if (size >= heap.length) { resize(); } this.heap[size] = value; int curIdx = size; int parentIdx = curIdx / 2; while (curIdx > 0 && value.compareTo(heap[parentIdx]) > 0) { swap(curIdx, parentIdx); curIdx = parentIdx; parentIdx /= 2; } ++size; } public T peek() { if (size <= 0) { throw new RuntimeException("真没了..."); } return heap[size - 1]; } public T pop() { if (size <= 0) { throw new RuntimeException("真没了..."); } T top = heap[0]; swap(0, size - 1); heap[size - 1] = null; --size; sink(0); return top; } private void sink(int index) { int left = index * 2 + 1; int right = index * 2 + 2; if (right >= size) { if (left < size && heap[left].compareTo(heap[index]) > 0) { swap(left, index); return; } return; } boolean btLeft = heap[index].compareTo(heap[left]) >= 0; boolean btRight = heap[index].compareTo(heap[right]) >= 0; int condition = btLeft && btRight ? 0 : !btLeft ? 1 : 2; switch (condition) { case 0: return; case 1: { swap(index, left); sink(left); } case 2: { if (heap[index].compareTo(heap[right]) < 0) { swap(right, index); sink(right); } return; } default: break; } } public void swap(int left, int right) { T temp = heap[left]; heap[left] = heap[right]; heap[right] = temp; } @Override public String toString() { StringBuilder sb = new StringBuilder(); for (T v : heap) { if (v == null) { break; } sb.append(v).append(' '); } return sb.toString(); } }
- 其实大根堆代码与小根堆几乎相同,只是大于小于的问题,可以写出小根堆之后,写一个比较函数,如果是想要构造大根堆,那么两个元素的比较结果就取相反的值;
- 或者再构造一个元素对象,compareTo结果取反,每次插入先构造该对象,自此堆中存储的是该对象;将所有的方法委托给小根堆即可;