仅需一篇文章让你深入理解 优先级队列(堆)

优先级队列(堆)

1. 二叉树的顺序存储

1.1 存储方式

使用数组保存二叉树结构,方式即将二叉树用层序遍历方式放入数组中。

一般只适合表示完全二叉树,因为非完全二叉树会有空间的浪费。

这种方式的主要用法就是堆的表示

在这里插入图片描述

1.2 下标关系

已知双亲(parent)的下标,则:

  • 左孩子(left)下标 = 2 * parent + 1;
  • 右孩子(right)下标 = 2 * parent + 2;

已知孩子(不区分左右)(child)下标,则:

  • 双亲(parent)下标 = (child - 1) / 2;

2. 堆(heap)

2.1. 概念

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

2.2. 将完全二叉树转换为大根堆

我们用到一种思路,叫作向下调整

  1. 找到下标最大的父亲节点parent,并将parent向前遍历
  2. 每次遍历都需要进行向下调整

向下调整过程:

  1. 声明孩子节点为2*parent+1
  2. 循环条件,孩子节点下标不能大于堆的长度。
  3. 找出左右孩子节点的最大值,和父亲节点的值进行比较
    o:如果比父亲节点的值大,则交换最大孩子节点值和父亲节点值,并将父亲节点往下走到孩子节点处,孩子节点为当前父亲节点的孩子。
    o:如果比父亲节点的值下,则退出循坏

具体实现代码

public class HeapDemo  {

    public int[] elem;
    public int usedSize;

    public HeapDemo() {
        this.elem = new int[10];
    }

    /**
     * 在这里为什么可以传len
     * 因为每棵树的结束位置实际上都是一样的
     * @param parent
     * @param len
     */
    public void adjustDown(int parent,int len) {
        while(parent*2+1 < len) {
            int left = parent*2+1;
            int right;
            if(left + 1 < len) {
                right = left+1;
            }else {
                right = left;
            }
            int max = elem[left] >= elem[right] ? left : right;
            if(elem[max] > elem[parent]) {
                int tmp = elem[parent];
                elem[parent] = elem[max];
                elem[max] = tmp;
                parent = max;
            }else {
                break;
            }
        }
    }

    public void adjustDown2(int parent,int len) {
        int child = 2*parent+1;

        //child < len 说明没有左孩子
        while (child < len) {
            //child+1 < len 判断是否有有孩子
            if(child+1 < len && this.elem[child] < this.elem[child+1]) {
                child++;
            }
            //child 下标一定是左右孩子的最大值下标
            if(this.elem[child] > this.elem[parent]) {
                int tmp = elem[parent];
                elem[parent] = elem[child];
                elem[child] = tmp;
                parent = child;
                child = parent*2+1;
            }else {
                break;
            }
        }
    }

    public void createBigHeap(int[] array) {
        for(int i = 0; i < array.length; i++ ){
            this.elem[i] = array[i];
            this.usedSize++;
        }
        //elem已经存放了元素
        //开始每个父亲节点向下调整
        //已知孩子节点n,父亲节点为(n-1)/2
        for(int i = (this.usedSize-2)/2; i >= 0 ; i--) {
            adjustDown(i,this.usedSize);
        }
    }

    public void show() {
        for(int i = 0; i < this.usedSize; i++) {
            System.out.print(this.elem[i] + " ");
        }
        System.out.println();
    }

}

2.3. 优先级队列–新增元素

  1. 判断堆是否为满,若满则二倍扩容
  2. 新增元素,并进行向上调整
    循环条件,child > 0
    和父亲节点比较大小
    若比他大,则交换,并继续向上
    若比他笑,则跳出循环
    /**
     * 逻辑:放到数组的最后一个位置
     * 然后向上调整
     * @param child
     */
    public void adjustUp(int child) {
        int parent = (child-1)/2;
        while(child > 0) {
            if(elem[child] > elem[parent]) {
                int tmp = elem[child];
                elem[child] = elem[parent];
                elem[parent] = tmp;
                child = parent;
                parent = (child-1)/2;
            }else {
                break;
            }
        }
    }

    public void push(int val) {
        //若堆为满,则进行二倍扩容
        if(isFull()) {
            this.elem = Arrays.copyOf(this.elem,2*this.elem.length);
        }
        this.elem[usedSize] = val;
        usedSize++;
        //向上调整
        adjustUp(usedSize-1);
    }

    public boolean isFull() {
        return this.usedSize == this.elem.length;
    }

2.4. 优先级队列–弹出元素

逻辑:

  1. 交换首尾元素
  2. 进行向下调整
    public void adjustDown2(int parent,int len) {
        int child = 2*parent+1;

        //child < len 说明没有左孩子
        while (child < len) {
            //child+1 < len 判断是否有有孩子
            if(child+1 < len && this.elem[child] < this.elem[child+1]) {
                child++;
            }
            //child 下标一定是左右孩子的最大值下标
            if(this.elem[child] > this.elem[parent]) {
                int tmp = elem[parent];
                elem[parent] = elem[child];
                elem[child] = tmp;
                parent = child;
                child = parent*2+1;
            }else {
                break;
            }
        }
    }

    public int poll() {
        if(isEmpty()) {
            throw new RuntimeException("队列为空!");
        }
        //删除
        //交换首尾位置元素
        int ret = this.elem[0];
        this.elem[0] = this.elem[usedSize-1];
        this.elem[usedSize-1] = ret;
        this.usedSize--;
        adjustDown2(0,usedSize);
        return ret;
    }
    
    public int peek() {
        if(isEmpty()) {
            throw new RuntimeException("队列为空!");
        }
        return this.elem[0];
    }

    public boolean isEmpty() {
        return this.usedSize == 0;
    }

2.5. 完整手撕优先级队列

import java.util.Arrays;

/**
 * Created with IntelliJ IDEA.
 * Description:优先级队列---堆
 * User: starry
 * Date: 2021 -04 -20
 * Time: 15:22
 */
public class HeapDemo  {

    public int[] elem;
    public int usedSize;

    public HeapDemo() {
        this.elem = new int[10];
    }

    /**
     * 在这里为什么可以传len
     * 因为每棵树的结束位置实际上都是一样的
     * @param parent
     * @param len
     */
    public void adjustDown(int parent,int len) {
        while(parent*2+1 < len) {
            int left = parent*2+1;
            int right;
            if(left + 1 < len) {
                right = left+1;
            }else {
                right = left;
            }
            int max = elem[left] >= elem[right] ? left : right;
            if(elem[max] > elem[parent]) {
                int tmp = elem[parent];
                elem[parent] = elem[max];
                elem[max] = tmp;
                parent = max;
            }else {
                break;
            }
        }
    }

    public void adjustDown2(int parent,int len) {
        int child = 2*parent+1;

        //child < len 说明没有左孩子
        while (child < len) {
            //child+1 < len 判断是否有有孩子
            if(child+1 < len && this.elem[child] < this.elem[child+1]) {
                child++;
            }
            //child 下标一定是左右孩子的最大值下标
            if(this.elem[child] > this.elem[parent]) {
                int tmp = elem[parent];
                elem[parent] = elem[child];
                elem[child] = tmp;
                parent = child;
                child = parent*2+1;
            }else {
                break;
            }
        }
    }

    public void createBigHeap(int[] array) {
        for(int i = 0; i < array.length; i++ ){
            this.elem[i] = array[i];
            this.usedSize++;
        }
        //elem已经存放了元素
        //开始每个父亲节点向下调整
        //已知孩子节点n,父亲节点为(n-1)/2
        for(int i = (this.usedSize-2)/2; i >= 0 ; i--) {
            adjustDown(i,this.usedSize);
        }
    }

    /**
     * 逻辑:放到数组的最后一个位置
     * 然后向上调整
     * @param child
     */
    public void adjustUp(int child) {
        int parent = (child-1)/2;
        while(child > 0) {
            if(elem[child] > elem[parent]) {
                int tmp = elem[child];
                elem[child] = elem[parent];
                elem[parent] = tmp;
                child = parent;
                parent = (child-1)/2;
            }else {
                break;
            }
        }
    }

    public void push(int val) {
        //若堆为满,则进行二倍扩容
        if(isFull()) {
            this.elem = Arrays.copyOf(this.elem,2*this.elem.length);
        }
        this.elem[usedSize] = val;
        usedSize++;
        //向上调整
        adjustUp(usedSize-1);
    }

    public int poll() {
        if(isEmpty()) {
            throw new RuntimeException("队列为空!");
        }
        //删除
        //交换首尾位置元素
        int ret = this.elem[0];
        this.elem[0] = this.elem[usedSize-1];
        this.elem[usedSize-1] = ret;
        this.usedSize--;
        adjustDown2(0,usedSize);
        return ret;
    }

    public int peek() {
        if(isEmpty()) {
            throw new RuntimeException("队列为空!");
        }
        return this.elem[0];
    }

    public boolean isEmpty() {
        return this.usedSize == 0;
    }

    public boolean isFull() {
        return this.usedSize == this.elem.length;
    }

    public void show() {
        for(int i = 0; i < this.usedSize; i++) {
            System.out.print(this.elem[i] + " ");
        }
        System.out.println();
    }

}

3. 优先级队列

PriorityQueue 实现 Queue 接口

		/**
         * PriorityQueue 堆  优先级队列
         * PriorityQueue 底层 默认是一个小根堆
         * 每次存元素的时候,一定要保证数据进去堆后,依然可以维持为一个小堆/大堆
         * 每次取出一个元素的时候,一定要保证剩下的元素,也要调整为一个小堆/大堆
         */
        PriorityQueue<Integer> qu = new PriorityQueue<>();
        qu.offer(21);
        qu.offer(23);
        qu.offer(2);
        qu.offer(43);
        qu.offer(5);
        qu.offer(8);
        System.out.println(qu.peek());  //2
        qu.poll();
        System.out.println(qu.peek());  //5

3.1 比较器构造方法

之前我们有说过PriorityQueue 默认是一个小根堆,那么我们想要一个大根堆,需要怎么办呢?

在底层,PriorityQueue 不仅有普通的构造方法,还有参数为比较器的构造方法,我们采取匿名内部类的方式来传入比较器的参数构成函数式接口,如下:

	PriorityQueue<Integer> qu = new PriorityQueue<>(
		new Comparator<Integer>() {
            @Override
            public int compare(Integer o1, Integer o2) {
                return o1-o2;
            }
        });

返回o1-o2时是小根堆
返回o2-o1时是大根堆

3.2 底层扩容方式

在这里插入图片描述
可以看到他是在

  • 小于64个元素前每次变为原来长度的两倍+2
  • 大于64个元素后每次右移一位,相当于1.5倍扩容

4. TopK问题

  • 求前k个最小的元素------建大堆
  • 求前k个最大的元素------建小堆

那么建多大的堆呢?
建一个大小为k的堆,这样只用维护大小为k的堆,调整的时间复杂度也会很低,达到Olog2的k

思路流程

以求前k个最大元素举例

  1. 先把数组前三个放入小根堆中
  2. 然后一直循环遍历数组
  3. 如果遇到比堆顶元素大的元素,出队(向下调整)
  4. 再将该元素入队(向上调整)
  5. 遍历结束后,堆中就是前k个最大的元素了

核心小堆堆顶元素必然是前k大元素中最小的那个,如果遇到比堆顶元素还大的元素,肯定要放入舍弃最小的,放入比最小还大的元素。

代码实现

	public static void topK(int[] arr,int k) {
        PriorityQueue<Integer> queue = new PriorityQueue<>(k, new Comparator<Integer>() {
            @Override
            public int compare(Integer o1, Integer o2) {
                return o1-o2;
            }
        });
        for(int i = 0; i < arr.length; i++) {
            if(i < k) {
                queue.offer(arr[i]);
            }else {
                int top = queue.peek();
                if(arr[i] > top) {
                    queue.poll();
                    queue.offer(arr[i]);
                }
            }
        }
        for(int i = 0; i < k; i++) {
            System.out.println(queue.poll());
        }
    }

如果要实现前k个最小的元素

  • 把修改为大根堆:return o2-o1;
  • 修改比较条件:arr[i] < top

ok,现在就变成求前k个最小的元素了!

如果求第k小的元素,思路还是一样的,最后堆顶元素就是第k小的元素

4.1. TopK问题应用练习

查找和最小的K对数字

给定两个以升序排列的整形数组 nums1 和 nums2, 以及一个整数 k。
定义一对值 (u,v),其中第一个元素来自 nums1,第二个元素来自 nums2。
找到和最小的 k 对数字 (u1,v1), (u2,v2) … (uk,vk)。
示例 1:
输入: nums1 = [1,7,11], nums2 = [2,4,6], k = 3
输出: [1,2],[1,4],[1,6]
解释: 返回序列中的前 3 对数:
[1,2],[1,4],[1,6],[7,2],[7,4],[11,2],[7,6],[11,4],[11,6]
示例 2:
输入: nums1 = [1,1,2], nums2 = [1,2,3], k = 2
输出: [1,1],[1,1]
解释: 返回序列中的前 2 对数:
[1,1],[1,1],[1,2],[2,1],[1,2],[2,2],[1,3],[1,3],[2,3]
示例 3:
输入: nums1 = [1,2], nums2 = [3], k = 3
输出: [1,3],[2,3]
解释: 也可能序列中所有的数对都被返回:[1,3],[2,3]

来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/find-k-pairs-with-smallest-sums

思路

和topK问题类似
我们之前比较的是数值大小,现在变成每次比较的是list中前两个和的大小
思路还是一样,需要灵活变通以下

代码如下

class Solution {
    public List<List<Integer>> kSmallestPairs(int[] nums1, int[] nums2, int k) {
        List<List<Integer>> out = new ArrayList<>();
        for(int i = 0; i < nums1.length; i++) {
            for (int j = 0; j < nums2.length; j++) {
                List<Integer> in = new ArrayList<>();
                in.add(nums1[i]);
                in.add(nums2[j]);
                out.add(in);
            }
        }
        PriorityQueue<List<Integer>> queue = new PriorityQueue<>(k, new Comparator<List<Integer>>() {
            @Override
            public int compare(List<Integer> o1, List<Integer> o2) {
                return (o2.get(0)+o2.get(1)) - (o1.get(0)+o1.get(1));
            }
        });
        for(int i = 0; i < out.size(); i++) {
            if(i < k) {
                queue.offer(out.get(i));
            }else {
                List<Integer> top = queue.peek();
                if(top.get(0)+top.get(1) > out.get(i).get(0)+out.get(i).get(1)) {
                    queue.poll();
                    queue.offer(out.get(i));
                }
            }
        }
        List<List<Integer>> res = new ArrayList<>();
        for(int i = 0; i < k; i++) {
            if(queue.peek() != null) {
                res.add(queue.poll());
            }
        }
        return res;
    }
}

我这个代码还可以优化,比如插入的时候就可以比较了,没必要再循环一遍和创建一个结果数组

5. 堆排序

思路:

  • 我们如果想要从小往大排序,则建立大根堆
  • 我们如果想要从大往小排序,则建立小根堆

逻辑:

  1. 我们交换首位的位置
  2. 然后向下调整尾位置以前的堆
  3. 循环到尾指针 == 0 时,排序结束

这样我们就保证了每次最大的元素放到了最后的位置,全部循环完毕后,则时从小到大排序的

    public void heapSort() {
        int end = usedSize-1;
        while (end > 0) {
            int top = elem[0];
            elem[0] = elem[end];
            elem[end] = top;
            adjustDown2(0,end);
            end--;
        }
    }

把它写成函数的完整形式为

public class SmallToBig{

    public void adjustDown(int[] array,int parent, int len) {
        int child = parent*2+1;
        while (child < len) {
            if(child+1 < len && array[child] < array[child+1]) {
                child = child+1;
            }
            if(array[parent] < array[child]) {
                int tmp = array[parent];
                array[parent] = array[child];
                array[child] = tmp;
                parent = child;
                child = 2*child+1;
            }else {
                break;
            }
        }
    }

	public void createBigHeap(int[] array) {
        int len = array.length;
        int parent = (len-2)/2;
        for (int i = parent; i >= 0; i--) {
            adjustDown(array,i,len);
        }
    }

    public void heapSort(int[] array) {
        int end = array.length-1;
        while (end > 0) {
            int top = array[0];
            array[0] = array[end];
            array[end] = top;
            adjustDown(array,0,end);
            end--;
        }
    }

    public static void main(String[] args) {
        int[] array = {27,15,19,18,28,34,65,49,25,37};
        Work3 a = new Work3();
        a.createBigHeap(array);
        a.heapSort(array);
        System.out.println(Arrays.toString(array));
    }

}

时间复杂度:O (n*log2n)
额外空间复杂度:O (1)

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值