一. 堆是什么
堆树的定义如下:
(1)堆树是一颗完全二叉树;
完全二叉树:除了最后一层,其他层的节点个数都是最大值,即 2^N,N为层数,根节点为0;且最后一层自左向右的节点是连续的
(2)堆树中某个节点的值总是不大于或不小于其孩子节点的值;
(3)堆树中每个节点的子树都是堆树。
当父节点的键值总是大于或等于任何一个子节点的键值时为最大堆。
当父节点的键值总是小于或等于任何一个子节点的键值时为最小堆。
堆可以用数组来表示,因为堆是完全二叉树,而完全二叉树很容易就存储在数组中。位置 k 的节点的父节点位置为 k/2,而它的两个子节点的位置分别为 2k 和 2k+1。这里不使用数组索引为 0 的位置,是为了更清晰地描述节点的位置关系。
package com.hong.heap;
import com.hong.arrays.Array;
public class MaxHeap<E extends Comparable<E>> {
private Array<E> data;
public MaxHeap(int capacity) {
data = new Array<>(capacity);
}
public MaxHeap() {
data = new Array<>();
}
/**
* 将任意数组转化成堆的形式 heapify
* 两种方式:
* 1.向堆中一个一个的添加元素,时间复杂度O(NlongN) ,N为元素个数
* 2.从第一个非叶子节点开始,进行siftDown,时间复杂度 O(N)
*
* @param arr
*/
public MaxHeap(E[] arr) {
/**
* 由最后一个元素的索引推导出第一个非叶子节点的索引,然后开始向前一个一个的进行siftDown()
* 这样,就不需要像第一种方式一样,需要遍历每个元素
*/
data = new Array<>(arr);
for (int i = parent(arr.length - 1); i >= 0; i--) {
siftDown(i);
}
}
/**
* 返回堆中元素的个数
*
* @return
*/
public int size() {
return data.getSize();
}
public boolean isEmpty() {
return data.isEmpty();
}
/**
* 返回完全二叉树的数组表示中,index索引位置的元素的父元素在数组中的索引
*
* @param index
* @return
*/
private int parent(int index) {
if (index == 0) {
throw new IllegalArgumentException("index-0 doesn't have parent.");
}
return (index - 1) / 2;
}
/**
* 返回完全二叉树的数组表示中,index索引位置的元素的左孩子在数组中的索引
*
* @param index
* @return
*/
private int leftChild(int index) {
return index * 2 + 1;
}
/**
* 返回完全二叉树的数组表示中,index索引位置的元素的右孩子在数组中的索引
*
* @param index
* @return
*/
private int rightChild(int index) {
return index * 2 + 2;
}
/**
* 向堆中添加元素
* 在堆的最后增加一个结点,然后沿这堆树上升.
* 将新元素放到数组末尾,然后上浮到合适的位置。
* @param e
*/
public void add(E e) {
//尾插法
data.addLast(e);
siftUp(data.getSize() - 1);
}
/**
* 向上筛选,找到新插入元素的正确位置
* 在堆中,当一个节点比父节点大,那么需要交换这个两个节点。
* 交换后还可能比它新的父节点大,因此需要不断地进行比较和交换操作,把这种操作称为上浮(ShiftUp)。
*
* @param k
*/
private void siftUp(int k) {
/* while (k > 0 && data.get(parent(k)).compareTo(data.get(k)) < 0) {
data.swap(k, parent(k));
k = parent(k);
}*/
/**
* siftUp优化:
* 在上面的代码中,每次比较当前节点和其父节点的值,符合条件则交换
* 这中间的交换过程是可以优化的,我们的目的是最终将k位置的元素与沿着
* 其父节点的路径上找到第一个比k位置元素大的节点时或者到达根节点时则停止比较.
* 参考Java的PriorityQueue的实现。
*/
E cur = data.get(k);
while (k > 0){
int parentIndex = parent(k);
E parent = data.get(parentIndex);
if (cur.compareTo(parent) <= 0){
break;
}
data.set(k,parent);
k = parentIndex;
}
data.set(k,cur);
}
/**
* 查看堆中的最大元素
*
* @return
*/
public E findMax() {
if (data.getSize() == 0) {
throw new IllegalArgumentException("Can not findMax when heap is empty.");
}
return data.get(0);
}
/**
* 取出堆中的最大元素
*
* @return
*/
public E extractMax() {
E max = findMax();
//将最后一个元素先放到第一个位置,然后数据下沉筛选,找到在堆中的正确位置
data.swap(0, data.getSize() - 1);
data.removeLast();
siftDown(0);
return max;
}
/**
* 向下调整k位置的元素
* 当前k位置的元素与 k位置元素的左右孩子中较大的值比较,
* 如果 > 较大值,则说明元素已经在正确的位置了,终止循环;
* 否则互换,从较大值位置继续上面的逻辑
*
* 类似地,当一个节点比子节点来得小,也需要不断地向下进行比较和交换操作,把这种操作称为下沉(Shift Down)。
* 一个节点如果有两个子节点,应当与两个子节点中最大那么节点进行交换。
* @param k
*/
private void siftDown(int k) {
// 当前节点是非叶子节点
while (leftChild(k) < data.getSize()) {
int j = leftChild(k);
// 如果也存在右孩子,则取出左右孩子较大值索引
if ((j + 1) < data.getSize() &&
data.get(j + 1).compareTo(data.get(j)) > 0) {
j = j + 1;
}
// data[j] 是 leftChild 和 rightChild 中的最大值
if (data.get(k).compareTo(data.get(j)) >= 0) {
break;
}
data.swap(j, k);
k = j;
}
}
/**
* 从堆中取出最大元素,替换成元素e
*
* @param e
* @return
*/
public E replace(E e) {
E ret = findMax();
data.set(0, e);
siftDown(0);
return ret;
}
}
package com.hong.arrays;
/**
* @author wanghong
* @date 2019/04/06 17:04
* 封装自定义数组
**/
public class Array<T> {
private T[] data;
private int size;
public Array() {
this(10);
}
public Array(int capacity) {
data = (T[]) new Object[capacity];
size = 0;
}
public Array(T[] arr) {
data = (T[]) new Object[arr.length];
for (int i = 0; i < arr.length; i++) {
data[i] = arr[i];
}
size = arr.length;
}
public int getCapacity() {
return data.length;
}
// 获取数组中的元素个数
public int getSize() {
return size;
}
public boolean isEmpty() {
return size == 0;
}
/**
* 头插法
*
* @param t
*/
public void addFirst(T t) {
add(0, t);
}
/**
* 尾插法
*
* @param t
*/
public void addLast(T t) {
add(size, t);
}
/**
* 向index索引处插入新元素
*
* @param index
* @param t
*/
public void add(int index, T t) {
// 判断要插入的索引是否越界,即要插入的索引位置在[0,size]的区间内
if (index < 0 || index > size) {
throw new IllegalArgumentException("Add failed,Require index > 0 and index <= size");
}
// 判断当前数组容量是否已满,满了自动扩容
if (size == data.length) {
// throw new IllegalArgumentException("Add failed,Array is full");
resize(2 * data.length);
}
// 将 索引 >= index 位置的元素依次向后挪一位,腾出index的位置放入新元素,这里从最后一个元素开始挪动
for (int i = size - 1; i >= index; i--) {
data[i + 1] = data[i];
}
data[index] = t;
size++;
}
/**
* 获取index索引处的元素
*
* @param index
* @return
*/
public T get(int index) {
if (index < 0 || index >= size) {
throw new IllegalArgumentException("Get failed. Index is illegal.");
}
return data[index];
}
public T getLast() {
return get(size - 1);
}
public T getFirst() {
return get(0);
}
/**
* 修改index索引位置的元素为t
*
* @param index
* @param t
*/
public void set(int index, T t) {
if (index < 0 || index >= size) {
throw new IllegalArgumentException("Set failed. Index is illegal.");
}
data[index] = t;
}
/**
* 判断数组中是否包含元素t
*
* @param t
* @return
*/
public boolean contains(T t) {
for (int i = 0; i < size; i++) {
if (data[i].equals(t)) {
return true;
}
}
return false;
}
/**
* 查找数组中元素t的索引,无则返回-1
*
* @param t
* @return
*/
public int find(T t) {
for (int i = 0; i < size; i++) {
if (data[i].equals(t)) {
return i;
}
}
return -1;
}
/**
* 删除第一个元素
*
* @return
*/
public T removeFirst() {
return remove(0);
}
/**
* 删除最后一个元素
*
* @return
*/
public T removeLast() {
return remove(size - 1);
}
/**
* 从数组中删除指定的元素
*
* @param t
*/
public void removeElement(T t) {
int index = find(t);
if (index != -1) {
remove(index);
}
}
/**
* 从数组中删除index位置处的元素并返回被删除的元素
*
* @param index
* @return
*/
public T remove(int index) {
if (index < 0 || index >= size) {
throw new IllegalArgumentException("Remove failed. Index is illegal.");
}
T t = data[index];
for (int i = index + 1; i < size; i++) {
data[i - 1] = data[i];
}
size--;
data[size] = null; // loitering objects != memory leak
/**
* 删除元素后,如果剩下的容量到了一半则缩容
* 为了防止复杂度震荡,即出现这样的操作:
* addLast -> 超过data.length -> resize -> removeLast -> resize
* 反复几次,会造成复杂度突然上升,性能下降
* 解决方案:Lazy
*/
// if (size == data.length / 2) {
if (size == data.length / 4 && data.length / 2 != 0) {
resize(data.length / 2);
}
return t;
}
@Override
public String toString() {
StringBuilder res = new StringBuilder();
res.append(String.format("Array:size = %d,capacity = %d\n", size, data.length));
res.append("[");
for (int i = 0; i < size; i++) {
res.append(data[i]);
if (i != size - 1) {
res.append(",");
}
}
res.append("]");
return res.toString();
}
/**
* 将数组扩容为newCapacity大小
*
* @param newCapacity
*/
private void resize(int newCapacity) {
T[] newData = (T[]) new Object[newCapacity];
for (int i = 0; i < size; i++) {
newData[i] = data[i];
}
data = newData;
}
/**
* 互换i,j位置的两个元素
*
* @param i
* @param j
*/
public void swap(int i, int j) {
if (i < 0 || i >= size || j < 0 || j >= size)
throw new IllegalArgumentException("Index is illegal.");
T t = data[i];
data[i] = data[j];
data[j] = t;
}
}
二. 堆排序
由于堆可以很容易得到最大的元素并删除它,不断地进行这种操作可以得到一个递减序列。如果把最大元素和当前堆中数组的最后一个元素交换位置,并且不删除它,那么就可以得到一个从尾到头的递减序列,从正向来看就是一个递增序列。因此很容易使用堆来进行排序。并且堆排序是原地排序,不占用额外空间。
- 版本一:原地堆排序
package com.hong.heap;
import com.hong.sort.SortTestHelper;
/**
* @author wanghong
* @date 2019/11/24 22:18
* 不使用一个额外的最大堆, 直接在原数组上进行原地的堆排序
**/
public class HeapSort {
// 我们的算法类不允许产生任何实例
private HeapSort(){}
public static void sort(Integer[] arr){
int n = arr.length;
// 注意,此时我们的堆是从0开始索引的
// 从(最后一个元素的索引-1)/2开始,即第一个 non-leaf node
// 最后一个元素的索引 = n-1
for( int i = (n-1-1)/2 ; i >= 0 ; i -- ){
shiftDown2(arr, n, i);
}
for( int i = n-1; i > 0 ; i-- ){
swap( arr, 0, i);
shiftDown2(arr, i, 0);
}
}
// 交换堆中索引为i和j的两个元素
private static void swap(Object[] arr, int i, int j){
Object t = arr[i];
arr[i] = arr[j];
arr[j] = t;
}
// 原始的shiftDown过程
private static void shiftDown(Comparable[] arr, int n, int k){
while( 2*k+1 < n ){
int j = 2*k+1;
if( j+1 < n && arr[j+1].compareTo(arr[j]) > 0 )
j += 1;
if( arr[k].compareTo(arr[j]) >= 0 )break;
swap( arr, k, j);
k = j;
}
}
// 优化的shiftDown过程, 使用赋值的方式取代不断的swap,
// 该优化思想和我们之前对插入排序进行优化的思路是一致的
private static void shiftDown2(Comparable[] arr, int n, int k){
Comparable e = arr[k];
while( 2*k+1 < n ){
int j = 2*k+1;
if( j+1 < n && arr[j+1].compareTo(arr[j]) > 0 ){
j += 1;
}
if( e.compareTo(arr[j]) >= 0 ){
break;
}
arr[k] = arr[j];
k = j;
}
arr[k] = e;
}
// 测试 HeapSort
public static void main(String[] args) {
int N = 1000000;
Integer[] arr = SortTestHelper.generateRandomArray2(N, 0, 100000);
SortTestHelper.testSort2("com.hong.heap.HeapSort", arr);
}
}
- 版本二
package com.hong.heap;
import com.hong.sort.SortTestHelper;
/**
* @author wanghong
* @date 2019/11/24 21:50
* 基础堆排序和Heapify(堆化)
**/
public class HeapSort1 {
// 对整个arr数组使用HeapSort1排序
// HeapSort1, 将所有的元素依次添加到堆中, 在将所有元素从堆中依次取出来, 即完成了排序
// 无论是创建堆的过程, 还是从堆中依次取出元素的过程, 时间复杂度均为O(nlogn)
// 整个堆排序的整体时间复杂度为O(nlogn)
public static void sort(int[] arr){
int n = arr.length;
MaxHeap<Integer> maxHeap = new MaxHeap<>(n);
for (int i = 0;i < n;i++){
maxHeap.add(arr[i]);
}
for (int i = n-1;i>=0;i--){
arr[i] = maxHeap.extractMax();
}
}
// 测试 HeapSort1
public static void main(String[] args) {
int N = 1000000;
int[] arr = SortTestHelper.generateRandomArray(N, 0, 100000);
SortTestHelper.testSort("com.hong.heap.HeapSort1", arr);
}
}
- 版本三
package com.hong.heap;
import com.hong.sort.SortTestHelper;
/**
* @author wanghong
* @date 2019/11/24 21:57
* 优化的堆排序
**/
public class HeapSort2 {
// 对整个arr数组使用HeapSort2排序
// HeapSort2, 借助我们的heapify过程创建堆
// 此时, 创建堆的过程时间复杂度为O(n), 将所有元素依次从堆中取出来, 时间复杂度为O(nlogn)
// 堆排序的总体时间复杂度依然是O(nlogn), 但是比HeapSort1性能更优, 因为创建堆的性能更优
public static void sort(Integer[] arr){
MaxHeap<Integer> maxHeap = new MaxHeap<>(arr);
for (int i = arr.length-1;i >= 0;i--){
arr[i] = maxHeap.extractMax();
}
}
// 测试 HeapSort2
public static void main(String[] args) {
int N = 1000000;
Integer[] arr = SortTestHelper.generateRandomArray2(N, 0, 100000);
SortTestHelper.testSort2("com.hong.heap.HeapSort2", arr);
}
}
三. 堆排序的应用——Top K问题
在N个元素中选出前M个元素
M远小于N
排序: NlongN
优先队列 NlogM 维护当前看到的前M个元素
四. 排序算法总结
/ 可以通过⾃自定义⽐比较函数,让排序算法不不存在稳定性的问题。
boolean operator<(const Student& otherStudent){
return score != otherStudent.score ?
score > otherStudent.score :
name < otherStudent.name;
}
