<数据结构>关于堆、优先队列以及对应笔试题、面试题的解析

144 阅读4分钟

知识点总结

如果已经了解以下知识点,则没有必要继续阅读该文章

  • 堆排序的原理与复杂度:建堆O(n)+堆排序O(nlogn),空间复杂度O(1)
  • 堆排序的特点:永远保持堆顶的最大(小)值特性
  • 优先队列基于堆排序实现.
  • 堆排序对应的笔试题:已知关键字序列5,8,12,19,28,20,15,22是小根堆(最小堆),插入关键字3,调整后得到的小根堆是()?
    • 3,5,12,8,28,20,15,22,19
  • 优先队列对应的面试编程题
    • 从两个有序数组中分别选择一个元素并求和,求第K大的数组和.
    • 滑动窗口最大值

堆排序原理

1258817-20190420150936225-1441021270.gif

如图所示,分别是建堆和堆排序两步操作.

建堆

  1. 将数组以层序遍历的顺序写入满二叉树
  2. 从最后一个非叶子节点从右到左,从下到上开始调整顺序,每次比较左右子树,选最值作为父节点.

堆排序

  1. 输出根节点
  2. 最后一个叶子节点移动到根节点
  3. 从上到下递归调整堆,选最值作为父节点
  4. 重复123,直到输出所有节点

优先队列原理和使用

原理

每个元素有优先级(有序)

  • 底层用堆实现,因此始终保持的是根节点的最值性质,子节点无全局序.
  • push就是进堆并调整堆,poll就是出堆并调整堆.

基本使用(java)

PriorityQueue<Integer> smallPQ = new PriorityQueue<>();//最小堆
pq.add(3);  
pq.add(1);  
pq.add(2);
int head = pq.peek(); // head = 1, 取根节点但不取出
int head = pq.poll(); // head = 1, 取根节点并取出
PriorityQueue<Integer> bigPQ = new PriorityQueue<>(Comparator.reverseOrder());//最大堆

//结构体优先队列
class Node{
    int i;
    int j;
    long val;
    public Node(int _i,int _j,long _val){
        i=_i;
        j=_j;
        val=_val;
    }
}
PriorityQueue<Node> pq = new PriorityQueue<>(new Comparator<Node>() {
    @Override
    public int compare(Node o1, Node o2) {
        return (int)(o1.val - o2.val); //从小到大
    }
});

笔面试题例子

笔试题

www.nowcoder.com/questionTer…

面试题

第K小的数组和

有两个大小都是k的数组A,B,它们元素的按非递减有序排列,找出这样的第k个最小的(ai + bj) ,其中 0<= i,j < k,要求算法的时间复杂度和空间复杂度尽量低。 给出 A = [1,7,11], B = [2,4,6]
当 k = 3, 返回 7.
当 k = 4, 返回 9.
当 k = 8, 返回 15.

解析

以两个数组作为行和列,可以获得数组对应位置求和形成的矩阵,该矩阵特性: 每行小到大排序,每列小到大排序

1711
23913
451115
671317
  1. 维护一个优先队列(堆),先将arr[0][0]入堆,并用二维数组标志哪些元素已经入过堆
  2. 循环k次,每次将根节点抛出,并将节点的右元素arr[i][j+1]和下元素arr[i+1][j]进堆,此时就能保证下一个比根节点大的节点一定在堆里,因此局部最值转换成了全局最值.

代码(java)

import java.util.Comparator;
import java.util.PriorityQueue;

class Solution {
    class Node{
        int i;
        int j;
        long val;
        public Node(int _i,int _j,long _val){
            i=_i;
            j=_j;
            val=_val;
        }
    }
    public long kthSmallestProduct(int[] nums1, int[] nums2, long k) {
        PriorityQueue<Node> queue = new PriorityQueue<>(new Comparator<Node>() {
            @Override
            public int compare(Node o1, Node o2) {
                return (int)(o1.val - o2.val);
            }
        });
        int[][] visit = new int[nums1.length][nums2.length];
        queue.add(new Node(0,0,nums1[0]*nums2[0]));
        visit[0][0] = 1;
        int cnt=0;
        long ans = 0;
        while(cnt<k){
            Node cur = queue.poll();
            ans = cur.val;
            if(cur.j+1<nums2.length&&visit[cur.i][cur.j+1]!=1){
                queue.add(new Node(cur.i,cur.j+1, nums1[cur.i]+nums2[cur.j+1]));
                visit[cur.i][cur.j+1]=1;
            }
            if(cur.i+1<nums1.length&&visit[cur.i+1][cur.j]!=1){

                queue.add(new Node(cur.i+1,cur.j, nums1[cur.i+1]+nums2[cur.j]));
                visit[cur.i+1][cur.j]=1;
            }
            cnt++;
        }
        return ans;

    }
}

滑动窗口最大值

题目:leetcode.cn/problems/sl…

解析

“获取滑动窗口中的最大值”,对应堆排序的局部最大值特性,因此可以用优先队列解决,由于某一时刻的滑动窗口最大值也可能是下一个滑动窗口的最大值,因此将下标也存储进去,以下标来判断是否将根节点抛出。

代码实现

class Solution {
    public int[] maxSlidingWindow(int[] nums, int k) {
        if(k==1){
            return nums;
        }
        PriorityQueue<int[]> queue = new PriorityQueue<>(new Comparator<int[]>() {
            @Override
            public int compare(int[] o1, int[] o2) {
                return o2[0] - o1[0];
            }
        });
        int[] ans = new int[nums.length-k+1];
        for (int i = 0; i < k; i++) {
            queue.add(new int[]{nums[i],i});
        }
        ans[0] = queue.peek()[0];
        for (int i = k; i < nums.length; i++) {
            while(queue.peek()[1]<=i-k){
                queue.poll();
            }
            queue.add(new int[]{nums[i],i});
            ans[i-k+1] = queue.peek()[0];
        }
        return ans;
    }
}