后端技术栈 2019-05-17
什么是堆
堆的基本特点有以下两项:
什么是完全二叉树
要求除了最后一层以外,其余层的节点都要是满的。
大顶堆
每个节点的值都大于其子节点的值,我们通常称之为大顶堆。
小顶堆
每个节点的值都小于其子节点的值,我们通常称之为小顶堆。
那么我们该如何来进行一个堆的构建呢?
先别急,我们来了解以下的几个概念先:
堆的存储方式
如上图所示,当我们对一个堆进行节点数据存储的时候,将每个节点的值都按照二叉树从上到下,从左到右的顺序存储到数组里面去。拿图中的6号节点来看(暂时命名数组为arr),它的存储位置是arr[6],它的父亲节点为arr[3],如果你有细心发现的话,会发现父子节点之间的编号存在有以下的关系:
这三个规律在下文中会涉及到。
向上堆化
所谓的向上堆化,是指在构建堆的过程中,当一个节点的值大于其父节点的值的时候,就会发生节点值的交换,直至当前节点的值小于或等于父节点的值。如下图所示:
图中有一个初始化的堆,现在需要往堆里面插入一个新的元素39。
由于堆里面每个节点的存储顺序都是按照二叉树从上至下,从左到右的顺序来进行存储,而且为了满足完全二叉树的要求,所以新的节点会被插入到 5 的位置。
插入之后,就要开始进行向上堆化的过程了。
首先39需要和自己的父节点进行大小比较,然后进行交换节点的值。
再次进行节点大小的比较,然后交换节点的值。
所有的比较节点完毕之后,就会如上图所示
向下堆化
所谓的向下堆化,是指在构建堆的过程中,当一个节点的值小于其父节点的值的时候,就会发生节点值的交换,直至当前节点的值大于或等于父节点的值。
同样我们结合图片的形式来进行描述:
图中有一个初始化的堆结构,然后现在需要进行节点的插入操作
新插入的节点8位于1号索引的位置,然后依次进行向下堆化处理
所有的比较节点完毕之后,就会如上图所示
在建堆(大顶堆)完成之后,数组中的堆顶元素通常都是最大的元素,这个时候,我们只需要将堆顶的元素进行移除,就能获取到当前数组的最大元素了。但是新的问题又来了,如何进行对于堆顶元素的弥补呢?
这个时候我们会将存储节点的数组的最后一个元素弥补到头部节点处,然后从上往下进行向下堆化处理,不断的重复堆顶元素移除,弥补头结点,向下堆化,不断的循环这段操作。
基于大顶堆的排序完整代码
/** * @author idea * @data 2019/2/19 */ public class TopHeap { private int[] arr; private int maxSize; private int count; public TopHeap(int size) { this.arr = new int[size]; this.maxSize = size; this.count = 0; } /** * 添加元素 * * @param data */ private void add(int data) { if (count > maxSize) { return; } count++; arr[count] = data; int i = count; //向上堆化 while (i / 2 > 0 && arr[i] > arr[i / 2]) { swap(arr, i, i / 2); i = i / 2; } } /** * 删除节点 * * @return */ private int removeNode() { if (count < 0) { return -1; } int result = arr[1]; arr[1] = arr[count]; count--; //向下堆化 heapify(arr, count, 1); return result; } /** * 排序 * * @return */ public int[] sort() { int[] result = new int[count]; int size = count; for (int j = 0; j < size; j++) { result[j] = removeNode(); } return result; } /** * 堆化 * * @param arr 数组 * @param total 数组总长度 * @param i 指堆化的开始索引值 */ private void heapify(int[] arr, int total, int i) { while (true) { int maxPos = i; if (i * 2 <= total && arr[i] < arr[i * 2]) { maxPos = i * 2; } if (i * 2 + 1 <= total && arr[maxPos] < arr[i * 2 + 1]) { maxPos = i * 2 + 1; } if (maxPos == i) { break; } swap(arr, i, maxPos); i = maxPos; } } /** * 交换数组元素值 * * @param arr * @param n * @param k */ private void swap(int[] arr, int n, int k) { int temp = arr[n]; arr[n] = arr[k]; arr[k] = temp; } public static void main(String[] args) { int[] arr=new int[]{12, 3, -45, 234, 123, 12, 55, 33}; TopHeap topHeap = new TopHeap(12); for (int i : arr) { topHeap.add(i); } int[] res = topHeap.sort(); for (int re : res) { System.out.print(re + " "); } } }
堆在java应用中的具体实践
java里面的优先级队列PriorityQueue的底层操作很多都是基于了堆进行排序的,我们知道队列是遵循先进先出(First-In-First-Out)模式的,但有些时候需要在队列中基于优先级处理对象。
例如说当在某个特殊的应用场景中,需要对居民的年龄进行排序操作,那么这个时候可以通过使用PriorityQueue来对每一个Person对象的年龄进行排序,然后进入队列进行年龄的从小到大的排序处理。
import java.util.PriorityQueue; /** * @author idea * @date 2019/5/15 */ class Person implements Comparable<Person> { public int age; Person(int age) { this.age = age; } @Override public int compareTo(Person other) { return other.age - age; } @Override public String toString() { return "Person{" + "age=" + age + '}'; } } public class PriorityQueueDemo { public static void main(String[] args) { PriorityQueue<Person> queue = new PriorityQueue<>(); Person person1 = new Person(31); Person person2 = new Person(22); Person person3 = new Person(65); Person person4 = new Person(15); Person person5 = new Person(28); /** * offer 插入新的元素 插入失败会返回false * add 插入新的元素 插入失败会抛异常 */ queue.offer(person1); queue.offer(person2); queue.offer(person3); queue.offer(person4); queue.offer(person5); int size = queue.size(); /** * element 会弹出队首元素,但是并不会删除 * peek 会弹出队首元素,但是也不会删除 */ for (int i = 0; i < size; i++) { /** * remove 移除队首元素 失败会抛异常 * poll 移除队首元素 失败会返回null */ Person person = queue.poll(); System.out.println(person.toString()); } } }
深入源码来看offer操作
public boolean offer(E e) { if (e == null) throw new NullPointerException(); modCount++; int i = size; if (i >= queue.length) grow(i + 1); size = i + 1; if (i == 0) queue[0] = e; else //插入元素之后需要进行向上堆化处理 siftUp(i, e); return true; } private void siftUp(int k, E x) { if (comparator != null) siftUpUsingComparator(k, x); else siftUpComparable(k, x); } //向上堆化的核心部分 @SuppressWarnings("unchecked") private void siftUpComparable(int k, E x) { Comparable<? super E> key = (Comparable<? super E>) x; while (k > 0) { int parent = (k - 1) >>> 1; Object e = queue[parent]; //通过比较器进行比对 if (key.compareTo((E) e) >= 0) break; queue[k] = e; k = parent; } //堆顶元素插入 queue[k] = key; }
poll操作的源码部分:
public E poll() { if (size == 0) return null; int s = --size; modCount++; E result = (E) queue[0]; E x = (E) queue[s]; queue[s] = null; if (s != 0) //向下堆化处理 siftDown(0, x); return result; } private void siftDown(int k, E x) { if (comparator != null) siftDownUsingComparator(k, x); else siftDownComparable(k, x); } //向下堆化处理 private void siftDownComparable(int k, E x) { Comparable<? super E> key = (Comparable<? super E>)x; int half = size >>> 1; // loop while a non-leaf while (k < half) { //找到左右节点里面相对较小的那个节点 int child = (k << 1) + 1; Object c = queue[child]; int right = child + 1; if (right < size && ((Comparable<? super E>) c).compareTo((E) queue[right]) > 0) c = queue[child = right]; //比较器比较元素的大小 if (key.compareTo((E) c) <= 0) break; //取代原来节点的值 queue[k] = c; k = child; } queue[k] = key; }
有几点地方我们需要注意一下:
总体小结