堆(Heap)

1. 堆(heap)

  1. 堆逻辑上是一棵完全二叉树
  2. 堆物理上是保存在数组中 比特科技
  3. 满足任意结点的值都大于其子树中结点的值,叫做大堆,或者大根堆,或者大堆
  4. 反之,则是小堆,或者小根堆,或者小堆
  5. 堆的基本作用是,快速找集合中的最值 在这里插入图片描述
    小根堆
    在这里插入图片描述
    大根堆

2.操作-向下调整

前提:左右子树必须已经是一个堆,才能调整。
说明:

  1. array 代表存储堆的数组
  2. size 代表数组中被视为堆数据的个数
  3. index 代表要调整位置的下标
  4. left 代表 index 左孩子下标
  5. right 代表 index 右孩子下标
  6. min 代表 index 的小值孩子的下标

过程(以小堆为例):

  1. index 如果已经是叶子结点,则整个调整过程结束 1. 判断 index 位置有没有孩子 2. 因为堆是完全二叉树,没有左孩子就一定没有右孩子,所以判断是否有左孩子 3. 因为堆的存储结构是数组,所以判断是否有左孩子即判断左孩子下标是否越界,即 left >= size 越界
  2. 确定 left 或 right,谁是 index 的小孩子 min 1. 如果右孩子不存在,则 min = left 2. 否则,比较 array[left] 和 array[right] 值得大小,选择小的为 min
  3. 比较 array[index] 的值 和 array[min] 的值,如果 array[index] <= array[min],则满足堆的性质,调整结束
  4. 否则,交换 array[index] 和 array[min] 的值
  5. 然后因为 min 位置的堆的性质可能被破坏,所以把 min 视作 index,向下重复以上过程

时间复杂度分析:
坏的情况,从根一路比较到叶子,比较的次数为完全二叉树的高度,即时间复杂度为 O(log(n))

// 以大堆为例
// 借助向下调整的操作来建堆
public static void shiftDown(int[] array, int size, int index) {
    int parent = index;
    int child = 2 * parent + 1;
    while (child < size) {
        // child 本来是左子树的下标, 再 + 1 就是右子树下标
        // 在找左子树和右子树谁大
        if (child + 1 < size
                && array[child + 1] > array[child]) {
            child = child + 1;
        }
        // if 之后 child 不知道它是左还是右了, 而一定是左右中的最大值
        if (array[child] > array[parent]) {
            // 不符合大堆的特性, 交换 child 和 parent 的位置
            swap(array, child, parent);
        } else {
            // 如果发现满足堆的特性, 调整就结束了
            break;
        }
        parent = child;
        child = 2 * parent + 1;
    }
}

public static void swap(int[] array, int x, int y) {
    int tmp = array[x];
    array[x] = array[y];
    array[y] = tmp;
}

3.操作-建堆

下面一个数组,这个数组逻辑上可以看做一颗完全二叉树,但是还不是一个堆,现在我们通过算法,把它构建成一个堆。根节点左右子树不是堆,从倒数的第一个非叶子节点的子树开始调整,一直调整到根节点的树,就可以调整成堆。
图示(以大堆为例):

// 建堆前 
int[] array = { 1,5,3,8,7,6 }; 
// 建堆后 
int[] array = { 8,7,6,5,1,3 }; 

在这里插入图片描述
时间复杂度分析:
粗略估算,可以认为是在循环中执行向下调整,为 O(n * log(n)) ,实际上是 O(n)

// 要把 [0, size) 范围中的元素建成堆
public static void createHeap(int[] array, int size) {
    // 从最后一个非叶子节点出发, 从后往前走, 针对每个节点, 进行向下调整
    // 第一个 size - 1 是为了找到最后一个元素的下标
    // 再在最后一个元素下标的基础上再 - 1 再除以 2
    for (int i = (size - 1 - 1) / 2; i >= 0; i--) {
        shiftDown(array, size, i);
    }
}

4. 堆的应用-优先级队列

数据结构提供两个基本的操作,一个是返回高优先级对象,一个是添加新的对象。这种数据结构就是优先级队列(Priority Queue)

优先级队列的实现方式有很多,但常见的是使用堆来构建。

操作-入队列

过程(以大堆为例):

  1. 首先按尾插方式放入数组
  2. 比较其和其双亲的值的大小,如果双亲的值大,则满足堆的性质,插入结束
  3. 否则,交换其和双亲位置的值,重新进行 2、3 步骤
  4. 直到根结点

操作-出队列(优先级最高)

为了防止破坏堆的结构,删除时并不是直接将堆顶元素删除,而是用数组的后一个元素替换堆顶元素,然后通过向 下调整方式重新调整成堆

返回队首元素(优先级最高)

返回堆顶元素即可

public class MyPriorityQueue {
    // 这个数组就是队列本体. 基于这个数组建立堆
    private int[] array = new int[100];
    // 队列中元素的个数, 堆的大小
    private int size = 0;

    public void offer(int x) {
        if (size >= array.length) {
            return;
        }
        array[size] = x;
        size++;
        shiftUp(array, size - 1);
    }

    // 复杂度是 O(logN)
    private void shiftUp(int[] array, int index) {
        int child = index;
        int parent = (index - 1) / 2;
        while (child > 0) {
            if (array[parent] < array[child]) {
                // 交换两个元素
                swap(array, parent, child);
            } else {
                // 调整完了
                break;
            }
            child = parent;
            parent = (child - 1) / 2;
        }
    }

    private void shiftDown(int[] array, int size, int index) {
        int parent = index;
        int child = 2 * parent + 1;
        while (child < size) {
            if (child + 1 < size
                && array[child + 1] > array[child]) {
                child = child + 1;
            }
            // 条件结束, 预期 child 指向左右子树的最大值
            // 再拿 child 和 parent 进行对比
            if (array[parent] < array[child]) {
                // 不满足大堆要求, 交换
                swap(array, parent, child);
            } else {
                break;
            }
            parent = child;
            child = parent * 2 + 1;
        }
    }

    private void swap(int[] array, int x, int y) {
        int tmp = array[x];
        array[x] = array[y];
        array[y] = tmp;
    }

    // 堆顶元素就是返回值
    // 删除堆顶元素并不是直接删除.
    // 用最后一个元素来覆盖堆顶元素
    // 再从堆顶位置向下调整即可
    public Integer poll() {
        if (size == 0) {
            return null;
        }
        int ret = array[0];
        array[0] = array[size - 1];
        size--;
        shiftDown(array, size, 0);
        return ret;
    }

    public Integer peek() {
        if (size == 0) {
            return null;
        }
        return array[0];
    }
}

TopK 问题
找前 K 个大的,要建 K 个大小的小堆

import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;
import java.util.PriorityQueue;


public class TeatTopK {
    // Pair 表示数对, 方便使用 优先队列 来进行管理
    // 一个普通的类, 无法直接放到优先队列中的.
    // 此时优先级如何定义的, 还不明确.
    // 可以把当前的类实现 Comparable 接口, 并实现 compareTo 方法
    // 此时优先队列就可以借助 compareTo 决定谁是优先级高, 谁是优先级低
    static class Pair implements Comparable<Pair> {
        public int n1;
        public int n2;
        public int sum;

        public Pair(int n1, int n2) {
            this.n1 = n1;
            this.n2 = n2;
            this.sum = n1 + n2;
        }

        @Override
        public int compareTo(Pair o) {
            // this, other
            // 如果希望 this 在前 other 在后, 返回 < 0
            // 如果希望 this 在后 other 在前, 返回 > 0
            // 如果希望相等, 返回 0
            if (this.sum < o.sum) {
                return 1;
            }
            if (this.sum > o.sum) {
                return -1;
            }
            return 0;
        }
    }

    public List<List<Integer>> kSmallestPairs(int[] nums1, int[] nums2, int k) {
        List<List<Integer>> result = new ArrayList<>();
        if (k < 1) {
            return result;
        }
        // 创建一个优先队列, 通过这个优先队列来作为堆, 完成最终的 topk 求解
        PriorityQueue<Pair> queue = new PriorityQueue<>();
        for (int i = 0; i < nums1.length && i < k; i++) {
            for (int j = 0; j < nums2.length && i < k; j++) {
                queue.offer(new Pair(nums1[i], nums2[j]));
                if (queue.size() > k) {
                    // 始终保证 队列中 不超过 k 个元素
                    // 超过的话就需要淘汰掉弱者
                    queue.poll();
                }
            }
        }
        // 这两重循环结束之后, 此时 queue 就保存了需要的 k 对数字
        while (!queue.isEmpty()) {
            Pair pair = queue.poll();
            List<Integer> tmp = new ArrayList<>();
            tmp.add(pair.n1);
            tmp.add(pair.n2);
            // 每次出队列的值插入到 result 最前面
            result.add(0, tmp);
        }
        return result;
    }

    public static void main(String[] args) {

    }
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值