堆是一种特殊的基于树的数据结构,其中树是一个完整的二叉树。一般来说,堆可以有两种类型:
堆满足如下性质:参见heap-data-structure
因为堆作为一颗完全二叉树
根据二叉树的性质,完全二叉树能够完美的映射到数组结构中去:如果节点从0开始编号,并把节点映射到数组中之后,则节点之间满足如下关系:
n为数组长度,n/2 -1实际上表示数组从头到尾最后一个非叶子结点的索引位置。
因此常常使用数组来实现堆结构,比如Java中的PriorityQueue,就是采用数组实现的二叉堆。
由于堆算作一个偏序(只有父节点和子节点的大小关系,没有两个子节点之间的大小关系),因此同一批元素采用不同算法构建成的堆在数组中的实际存储顺序是不一定相同的,并且堆排序也是一种不稳定的排序算法。
import java.util.Arrays; import java.util.Collection; import java.util.Comparator; public class MaxBinaryTreeHeap<E> { private Object[] heap; private int size; /** * 如果未指定初始容量,默认为16 */ private static int capacity = 16; /** * 如果元素使用自然排序,那么比较器为null;否则使用比较器比较 */ private final Comparator<? super E> cmp; /** * 对元素进行比较大小的方法,如果传递了自定义比较器,则使用自定义比较器,否则则需要数据类型实现Comparable接口 * * @param e1 被比较的第一个对象 * @param e2 被比较的第二个对象 * @return 0 相等 ;小于0 e1 < e2 ;大于0 e1 > e2 */ private int compare(E e1, E e2) { if (cmp != null) { return cmp.compare(e1, e2); } else { return ((Comparable<E>) e1).compareTo(e2); } } /** * 初始化空的大顶堆,使用默认容量 */ public MaxBinaryTreeHeap() { this(capacity, null); } /** * 初始化空的大顶堆,指定容量 * * @param initCapacity 指定容量数组 */ public MaxBinaryTreeHeap(int initCapacity) { this(initCapacity, null); } /** * 初始化空的大顶堆,指定比较器 * * @param comparator 指定比较器 */ public MaxBinaryTreeHeap(Comparator<? super E> comparator) { this(capacity, comparator); } /** * 初始化空的大顶堆,指定容量和比较器 * * @param initCapacity 指定数组容量 * @param comparator 指定比较器 */ public MaxBinaryTreeHeap(int initCapacity, Comparator<? super E> comparator) { if (initCapacity < 1) { throw new IllegalArgumentException(); } capacity = initCapacity; this.heap = new Object[initCapacity]; cmp = comparator; } /** * 同通过一批数据初始化大顶堆 * * @param heap 数组 */ public MaxBinaryTreeHeap(Collection<? extends E> heap) { this(heap, null); } /** * 同通过一批数据和指定比较器初始化大顶堆 * * @param heap 数组 * @param comparator 自定义的比较器 */ public MaxBinaryTreeHeap(Collection<? extends E> heap, Comparator<? super E> comparator) { Object[] array = heap.toArray(); this.cmp = comparator; if (array.getClass() != Object[].class) { array = Arrays.copyOf(array, array.length, Object[].class); } for (Object o : array) { if (o == null) { throw new NullPointerException(); } } this.heap = array; this.size = array.length; buildHeap(this.heap); } /** * 同通过一批数据初始化大顶堆 * * @param heap 数据数组 */ private void buildHeap(Object[] heap) { /*i从最后一个非叶子节点的索引开始,递减构建,直到i=-1结束循环 这里元素的索引是从0开始的,所以最后一个非叶子节点array.length/2 - 1,这是利用了完全二叉树的性质*/ for (int i = heap.length / 2 - 1; i >= 0; i--) { buildHeap(heap, i, heap.length); } } /** * 初始化大顶堆 * * @param arr 数据数组 * @param i 非叶子节点的索引 * @param length 堆长度 */ private void buildHeap(Object[] arr, int i, int length) { //先把当前非叶子节点元素取出来,因为当前元素可能要一直移动 Object temp; //节点的子节点的索引 int childIndex; /*循环判断父节点是否大于两个子节点,如果左子节点索引大于等于堆长度 或者父节点大于两个子节点 则结束循环*/ for (temp = arr[i]; (childIndex = 2 * i + 1) < length; i = childIndex) { //childIndex + 1 < length 说明该节点具有右子节点,并且如果右子节点的值大于左子节点,那么childIndex自增1,即childIndex指向右子节点索引 if (childIndex + 1 < length && compare((E) arr[childIndex], (E) arr[childIndex + 1]) < 0) { childIndex++; } //如果发现最大子节点(左、右子节点)大于根节点,为了满足大顶堆根节点的值大于子节点,需要进行值的交换 //如果子节点更换了,那么,以子节点为根的子树会受到影响,所以,交换之后继续循环对子节点所在的树进行判断 if (compare((E) arr[childIndex], (E) temp) > 0) { swap(arr, i, childIndex); } else { //走到这里,说明父节点大于最大的子节点,满足大顶堆的条件,直接终止循环 break; } } } /** * 大顶堆排序(顺序) * 实际上就是不断循环将堆顶元素与堆尾元素互换,然后移除堆尾元素,之后重构大顶堆的过程 */ public Object[] heapSort() { //使用大顶堆的副本进行排序输出 Object[] arr = Arrays.copyOf(heap, size); /*开始堆排序,i = arr.length - 1,即从大顶堆尾部的数开始,直到i=0结束循环*/ for (int i = size - 1; i > 0; i--) { //交换堆顶与堆尾元素顺序 swap(arr, 0, i); //重新构建大顶堆 buildHeap(arr, 0, i); } return arr; } /** * 添加节点,构建大顶堆 * * @param e 需要添加的节点 */ public void add(E e) { /*判空*/ if (e == null) { throw new NullPointerException(); } /*检查容量*/ if (heap.length == size) { resize(); } /*添加节点*/ addNode(e, size++); } /** * 添加节点,并向上重构大顶堆,最终找到一个位置加入新结点e,该位置的结点小于等于其父结点 * * @param e 要添加的节点 */ private void addNode(E e, int start) { //获取size处节点的父节点索引 int parent = (start - 1) / 2; /*如果size>0 寻找合适的位置:在某个插入的位置的新节点小于等于对应的父节点的值*/ while (start > 0) { //判断父节点和新子节点的大小,如果父节点小于等于新子节点,那么符合小顶堆的要求,重构结束,该start就是子节点插入的位置 if (compare((E) heap[parent], e) >= 0) { break; } else { //否则,将父节点的值移动到子节点的位置处 heap[start] = heap[parent]; //将start的索引值变成父节点的索引值 start = parent; //重新计算父节点的索引,不断循环,直到找到父节点值小于等于新子节点值的索引 parent = (start - 1) / 2; } } //在合适的位置插入新节点值 heap[start] = e; } /** * 底层数组扩容 */ private void resize() { heap = Arrays.copyOf(heap, heap.length * 2, Object[].class); } /** * 交换元素 * * @param arr 数组 * @param a 元素的下标 * @param b 元素的下标 */ private static void swap(Object[] arr, int a, int b) { Object temp = arr[a]; arr[a] = arr[b]; arr[b] = temp; } /** * 删除找到的第一个堆节点,并且重构大顶堆 * * @param e 需要删除的节点 * @return false 删除失败 true 删除成功 */ public boolean remove(E e) { int eIndex = -1; for (int i = 0; i < size; i++) { //这里是通过compare来查找元素是否相同的 if (compare((E) heap[i], e) == 0) { eIndex = i; } } /*没找到需要删除的元素*/ if (eIndex == -1) { return false; } /*找到了*/ //原尾部元素x E x = (E) heap[size - 1]; //交换查找到的元素与堆尾元素的位置 swap(heap, eIndex, size - 1); //移除堆尾元素 heap[size--] = null; //从eIndex开始向下重新构建大顶堆 buildHeap(heap, eIndex, size); //构建之后如果eIndex位置的元素就是x,说明没有调整堆结构,那么将该位置的元素看成新插入的元素,需要向上构建大顶堆 if (heap[eIndex] == x) { //调用addNode从eIndex开始向上重构大顶堆 addNode(x, eIndex); } return true; } public int size() { return size; } @Override public String toString() { StringBuilder stringBuilder = new StringBuilder(); stringBuilder.append("["); for (int i = 0; i < size; i++) { stringBuilder.append(heap[i]); if (i != size - 1) { stringBuilder.append(","); } } stringBuilder.append("]"); return stringBuilder.toString(); } }
请参考:PriorityQueue类的实现
public static void main(String[] args) { Integer[] arr = new Integer[]{9, 8, 5, 4, 5, 2, 1, 3, 7}; //构建大顶堆 MaxBinaryTreeHeap<Integer> maxBinaryHeap = new MaxBinaryTreeHeap<>(Arrays.asList(arr)); //输出大顶堆 System.out.println(maxBinaryHeap); //添加节点,并且重构大顶堆 maxBinaryHeap.add(11); maxBinaryHeap.add(77); //输出大顶堆 System.out.println(maxBinaryHeap); //删除节点,并且重构大顶堆 //删除失败 System.out.println(maxBinaryHeap.remove(79)); //删除成功 System.out.println(maxBinaryHeap.remove(7)); //输出大顶堆 System.out.println(maxBinaryHeap); //大顶堆排序(顺序排序) System.out.println(Arrays.toString(maxBinaryHeap.heapSort())); //输出大顶堆 System.out.println(maxBinaryHeap); }
输出结果如下:
[9,8,5,7,5,2,1,3,4]
[77,11,5,7,9,2,1,3,4,5,8]
false
true
[77,11,5,8,9,2,1,3,4,5]
[1, 2, 3, 4, 5, 5, 8, 9, 11, 77]
[77,11,5,8,9,2,1,3,4,5]
10种常见排序算法原理详解以及Java代码的完全实现
heap-data-structure