09-堆 Heap(最大堆)

学习资源:慕课网liyubobobo老师的《玩儿转数据结构》


1、堆

1.1、二叉堆

  • 是一棵完全二叉树。完全二叉树:简单理解,就是把元素按从左到右的顺序,一层一层地排列成二叉树的形状

  • 有两种二叉堆:

    • 最大堆

    • 最小堆

1.2、最大堆

满足:堆中某个结点的值总是不大于其父结点的值

image-20200503231333255

1.3、最小堆

满足:堆中某个结点的值总是不大于其孩子结点的值

2、最大堆的实现

可以使用数组表示一棵完全二叉树。完全二叉树逐层从左到右使用数字标记每个结点,数字对应数组中的索引。

image-20200721170620569

最大堆的内部使用数组实现,但对外展示还是使用完全二叉树的形式。

2.1、实现方式

2.1.1、方式一

索引从 1 开始,0 空出,结点的索引有以下规律:

  • 结点 i 的父结点:parent(i) = i/2(向下取整)
  • 结点 i 的左孩子结点:leftChild(i) = 2*i
  • 结点 i 的右孩子结点:rightChild(i) = 2*i + 1
image-20200503235120462

2.1.2、方式二

索引从 0 开始,结点的索引有以下规律:

  • 结点 i 的父结点:parent(i) = (i - 1)/2(向下取整)
  • 结点 i 的左孩子结点:leftChild(i) = 2*i + 1
  • 结点 i 的右孩子结点:rightChild(i) = 2*i + 2
image-20200504000409859

2.2、代码实现

2.2.1、基础部分

package heap;

import array.Array;

// 根结点索引从 0 开始
public class MaxHeap<E extends Comparable<E>> {

	//基于动态数组
    private Array<E> data;

    public MaxHeap(int capacity) {
        data = new Array<>(capacity);
    }

    public MaxHeap() {
        data = new Array<>();
    }

    public int size(){
        return data.getSize();
    }

    public boolean isEmpty(){
        return data.isEmpty();
    }

    private int parent(int index){
        
        if(index == 0){
            throw new IllegalArgumentException("根结点没有父结点");
        }
        return (index - 1)/2;
    }

    private int leftChild(int index){
        return index*2 + 1;
    }

    private int rightChild(int index){
        return index*2 + 2;
    }
}

在之前实现的动态数组中添加一个API

// 交换两个索引处的值
public void swap(int i, int j){

    if(i<0 || i>=size || j<0 || j>=size)
    	throw new IllegalArgumentException("索引越界");

    E e = data[i];
    data[i] = data[j];
    data[j] = e;
}

2.2.2、添加结点 和 Sift Up

在堆的内部,元素是存储在数组中的,所以很容易实现添加操作;但是添加后操作完成之后,还要根据最大堆的性质进行调整。

实现思路:

  1. 将新添加的结点与其父结点进行比较,如果不满足最大堆的性质,交换新添加结点和父结点的位置即可(在数组中是比较容易实现的)
  2. 重复上述比较交换的过程,直到新添加的结点小于其当前的父结点

sift

// 添加元素
public void add(E e){
    
    data.addToLast(e);
    siftUp(data.getSize() - 1);
}

private void siftUp(int k) {
    
    while (k > 0 && data.get(k).compareTo(data.get(parent(k))) > 0){
        data.swap(k, parent(k));
        //继续判断交换后的新结点是否大于现在的父结点
        k = parent(k);
    }
}

2.2.3、取出元素 和 Sift Down

对于最大堆,取出元素时只取出最大元素,即堆顶得结点,也即数组的第一个元素。

实现思路:

  1. 取出最大元素62后,将数组中的最后一个元素16放置到堆顶
  2. 然后进行调整:新的堆顶结点16和其左右孩子结点比较,并与其中最大的孩子结点52交换位置
  3. 然后再向下不断进行,直到16这个结点到达合适的位置

siftdown

// 查看堆中的最大元素
public E findMax(){
    
    if(data.getSize() == 0){
        throw new IllegalArgumentException("堆为空,没有最大结点");
    }
    return data.get(0);
}
// 取出堆中的最大元素
public E exactMax(){
    
    E ret = findMax();
    data.swap(0, data.getSize() - 1);
    data.removeLast();
    siftDown(0);
    return ret;
}
private void siftDown(int k) {
    
    while(leftChild(k) < data.getSize()){
        
        // 找到结点k 的左右孩子中最大的结点
        int maxChild = leftChild(k);
        //右孩子 > 左孩子
        if(maxChild+1 < data.getSize() && data.get(maxChild+1).compareTo(data.get(maxChild)) > 0){
            maxChild = rightChild(k);
        }
        // 如果结点k已经满足最大堆,退出循环即可
        if(data.get(k).compareTo(data.get(maxChild)) >=0){
            break;
        }
        data.swap(k, maxChild);
        k = maxChild;
    }
}

2.2.4、replace

定义:取出最大元素后,放入一个新元素;也相当于将最大元素替换为新元素

实现思路(这里选择思路2):

  1. 可以先extractMax,再add, 两次O(logn)的操作
  2. 也可以直接将堆顶元素替换为新元素,再对堆顶元素Sift Down,一次O(logn)的操作
public E replace(E e){
    
    E ret = findMax();
    data.set(0, e);
    siftDown(0);
    return ret;
}

2.2.5、heapify

定义:将任意数组整理成堆的形状

实现思路:

  1. 通过循环将数组中的元素逐个添加进堆对象中。复杂度为O(nlogn)

  2. 从最后一个非叶子结点(最后一个结点的父结点),从下之上、从右至左(对应到数组中也就是从后向前),逐个地对每一个元素元素进行Sift Down。复杂度为O(n)

  3. 直到对根结点完成 Sift Down

    heapify

在Array类中添加新的构造器函数,传入一个静态数组转换为动态数组

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 MaxHeap(T[] arr){
    
    data = new Array<>(arr);
    for (int i = parent(arr.length-1); i >=0 ; i--) {
        siftDown(i);
    }
}

3、全部代码

package heap;

import array.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<>();
    }

    public MaxHeap(E[] arr){

        data = new Array<>(arr);
        for (int i = parent(arr.length-1); i >=0 ; i--) {
            siftDown(i);
        }
    }

    public int size(){
        return data.getSize();
    }

    public boolean isEmpty(){
        return data.isEmpty();
    }

    private int parent(int index){
        if(index == 0){
            throw new IllegalArgumentException("根结点没有父结点");
        }
        return (index - 1)/2;
    }

    private int leftChild(int index){
        return index*2 + 1;
    }

    private int rightChild(int index){
        return index*2 + 2;
    }

    // 添加元素
    public void add(E e){

        data.addToLast(e);
        siftUp(data.getSize() - 1);
    }
    private void siftUp(int k) {

        while (k > 0 && data.get(k).compareTo(data.get(parent(k))) > 0){

            data.swap(k, parent(k));
            //继续判断交换后的新结点是否大于现在的父结点
            k = parent(k);
        }
    }

    // 查看堆中的最大元素
    public E findMax(){

        if(data.getSize() == 0){
            throw new IllegalArgumentException("堆为空,没有最大结点");
        }
        return data.get(0);
    }
    // 取出堆中的最大元素
    public E exactMax(){

        E ret = findMax();
        data.swap(0, data.getSize() - 1);
        data.removeLast();
        siftDown(0);

        return ret;
    }
    private void siftDown(int k) {

        while(leftChild(k) < data.getSize()){

            // 找到结点k 的左右孩子中最大的结点
            int maxChild = leftChild(k);
            //右孩子 > 左孩子
            if(maxChild+1 < data.getSize() && data.get(maxChild+1).compareTo(data.get(maxChild)) > 0){
                maxChild = rightChild(k);
            }

            // 如果结点k已经满足最大堆,退出循环即可
            if(data.get(k).compareTo(data.get(maxChild)) >=0){
                break;
            }

            data.swap(k, maxChild);
            k = maxChild;
        }
    }

    public E replace(E e){

        E ret = findMax();
        data.set(0, e);
        siftDown(0);
        return ret;
    }
}

4、测试

测试Sift Down,从最大堆中取出元素,其输出必然保证有序

@Test
public void test(){
    
    int n = 100;
    MaxHeap<Integer> heap = new MaxHeap<>();
    Random random = new Random();
    for(int i = 0; i<n; i++){
        heap.add(random.nextInt(Integer.MAX_VALUE));
    }
    int[] arr = new int[n];
    for (int i = 0; i <n; i++) {
        arr[i] = heap.exactMax();
    }
    for (int i = 1; i < n; i++) {
//        System.out.println(arr[i-1]);
        if(arr[i-1] < arr[i]){
            throw new RuntimeException("堆错误");
        }
    }
    System.out.println("堆正确");
}

5、其他类型的堆

  • d叉堆
image-20200613103125202
  • 索引堆
  • 二项堆
  • 斐波那契堆

6、基于最大堆的优先队列

优先队列:在优先队列中,元素被赋予优先级;当访问元素时,具有最高优先级的元素最先出队。

底层直接复用最大堆的方法实现队列的接口即可

package heap;

import queue.arrayQueue.Queue;

public class PriorityQueue<E extends Comparable<E>> implements Queue<E> {

    private MaxHeap<E> maxHeap;

    public PriorityQueue() {
        this.maxHeap = new MaxHeap<>();
    }

    @Override
    public void enqueue(E e) {
        maxHeap.add(e);
    }

    @Override
    public E dequeue() {
        return maxHeap.exactMax();
    }

    @Override
    public E getFront() {
        return maxHeap.getMax();
    }

    @Override
    public int getSize() {
        return maxHeap.size();
    }

    @Override
    public boolean isEmpty() {
        return maxHeap.isEmpty();
    }
}

7、Java中的优先队列

PriorityQueue,底层是最小堆

特点:构造器可以传入一个比较器,规定优先级

public class PriorityQueue<E> extends AbstractQueue<E>
    implements java.io.Serializable {
    
}

API接口

image-20200613101805246
posted @ 2020-06-13 10:37  卡文迪雨  阅读(209)  评论(0编辑  收藏  举报