算法从入门到放弃——第九期 PriorityQueue大顶堆和小顶堆

我不再介绍堆排序的基本概念,我只是从实际使用过程说说,对JAVA而言,有现成实现PriorityQueue。

那么先说一下概要:

1、堆排序的实现,包括你往搜索树里插入元素,和时间复杂度考虑

2、topk问题的解决(到底小顶堆是找最大还是找最小)

3、leetcode相关题目

一、堆排序实现

堆排序的实现难点在堆元素的插入,熟悉堆元素插入那么堆排序的过程 不难写:

核心,1、建堆,2、堆顶元素处理,3、重排移除堆顶之后的数组,4、堆顶元素处理,然后34循环,虽然不难,但是也有坑,不写个十遍,也难记得住

    public static void main(String[] args) {
        int[] arr = {1, 4, 7, 8, 9, 6, 5, 2, 3};
        heapSort(arr);
        System.out.println(arr);
    }

    public static void heapSort(int[] arr) {
        buildHeap(arr, arr.length);
        for (int i = 0; i < arr.length; i++) {
            swap(arr, 0, arr.length - 1 - i);
            maxHeap(arr, 0, arr.length - 1 - i);
        }
    }

    public static void buildHeap(int[] array, int heapSize) {
        for (int i = (heapSize - 2) >> 1; i >= 0; i--) {
            maxHeap(array, i, heapSize);
        }
    }

    public static void maxHeap(int[] array, int index, int heapSize) {
        int left = 2 * index + 1;
        int right = 2 * index + 2;
        int largest = index;
        if (left < heapSize && array[largest] < array[left]) {
            largest = left;
        }
        if (right < heapSize && array[largest] < array[right]) {
            largest = right;
        }
        if (largest != index) {
            swap(array, largest, index);
            maxHeap(array, largest, heapSize);
        }
    }

    public static void swap(int[] arr, int i, int j) {
        int temp = arr[i];
        arr[i] = arr[j];
        arr[j] = temp;
    }

二、topk问题的解决

在大数据领域有一个TopK的问题,这种问题的解决两种思路,一种全部遍历做统计,那就是MapReduce或者bitMap等等,一种就是全部遍历利用堆,那么到底到底小顶堆是找最大还是找最小呢?或者反过来也一样,大顶堆是找最大还是找最小呢?

结论应该是:全局看,大顶堆就是找topk小,小顶堆就是找topk大,懂堆排序的都不难理解,但是从局部看,在堆内,堆是排序的,大顶堆堆顶就是最大,小顶堆顶部就是最小

如果我们每次取堆顶,再放入一个元素,再取堆顶,再放,循环,那么对于最小堆,我得到的可是topk小,大顶堆我们可以这样得到topk大

如果我们一直往堆里放,直到所有元素都放完,那么对于大顶堆,我们得到的是topk小,对于小顶堆我们得到的是topk大

所以怎么使用元素决定了最终我们得到结果的情况,leetcode上有很多聪明的解法,就是基于我以上所说的两种处理元素方式

三、leetcode相关题目

378. 有序矩阵中第 K 小的元素

给你一个 n x n 矩阵 matrix ,其中每行和每列元素均按升序排序,找到矩阵中第 k 小的元素。
请注意,它是 排序后 的第 k 小元素,而不是第 k 个 不同 的元素。

示例 1:

输入:matrix = [[1,5,9],[10,11,13],[12,13,15]], k = 8
输出:13
解释:矩阵中的元素为 [1,5,9,10,11,12,13,13,15],第 8 小元素是 13
示例 2:

输入:matrix = [[-5]], k = 1
输出:-5

但凡这类题目中或多或少有排序题目的,其最优解,要不就是利用数据接口做线性时间复杂度,要不就是利用快排、堆排做nlogn时间复杂度,这已经是最快的方式了。

本题最应该出现的我觉得是自己实现堆排序,当然java有PriorityQueue这题就甚至算不上中等题只能算是简单题了

    public static int kthSmallest(int[][] matrix, int k) {
        PriorityQueue<Integer> heap = new PriorityQueue<>((a, b) -> a - b);
        for (int i = 0; i < matrix[0].length; i++) {
            for (int j = 0; j < matrix[i].length; j++) {
                heap.offer(matrix[i][j]);
            }
        }
        int result = Integer.MAX_VALUE;
        while(k>0){
            Integer poll = heap.poll();
            k--;
            if(k==0){
                result = poll;
            }
        }
        return result;
    }

会遍历二位数组,有手就行。

373. 查找和最小的 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]

这题难的点在于,我们可以正常路线算结果,放map,然后取前k个没有问题,但测试用例同样大的发指,需要在时间上做优化

题目暗示的很明显,用堆排序就完事了,但是怎么遍历两个数组呢?正常情况下我们遍历两个数组的过程是 (u1,v1),  (u1,v2)  ...  (u1,vk) , | (u2,v1),  (u2,v2)  ...  (u2,vk) ... | (up,v1),  (up,v2)  ...  (up,vk) ,为了看的稍微舒服一点我用 “|” 做了区分,如果我们同样以这个过程往里放元素,那么时间复杂度还是没有能够减少,n^2logn,怎么样将n^2转为线性时间复杂度呢?虽然数组是有序的,我可以确定(u1,v1)为当前最小的,但是无法确定(u1,vk)的大小和(up,v1)的大小和他们在数组中的位置,例如【1111】和【1123】以及k=2来说,或者倒过来【1123】和【1111】而言,结果应该是 8个【11】之后是4个【12】,但是你不全遍历完n^2是无法确定在遍历顺序而言【11】遍历之前的【12】是否能够排在前k个里的。

数组是有序的,是我们唯一剩下的筹码,堆排序是我们一眼看过去应该用的手段,这两个必须要结合解决这个遍历顺序的问题,才能解决n^2复杂度的问题。

所以这里引出一种做法,我们先统一语境,【0,0】指的是第一个数组编号为0的元素和第二个数组编号为0的元素构成的,那么我在初始化堆的过程中,以【i,0】作为元素初始化,假设这里初始化之后K个元素已经满了,此时【k,0】之后的元素还没初始化进来,0维度遍历也没有初始化进来,但我们知道堆顶是最小的,我们出堆顶元素【0,0】,此时有两种情况,下个最小的是【01】或者【10】,所以我要把【01】加进来,因为【10】已经有了,此时堆大小仍然是k,随着【10】加速,堆重排,假设堆顶是【10】,那么我么应该把【10】取出来,此时下一个最小的是【20】或者【01】,注意如果弹出的是【20】且未来一直弹出【k0】那么最多也就弹出k个结束,所以不需要再加元素,因为如果【01】都不能取,说明【02】...【0k】也都不能取;如果弹出的不是【20】而是【01】,那么下一个有可能的是【20】或者【02】,以此你应该能感觉到一股规律:

我先放【i0】,再尝试放【0j】,如果【0j】被取走,说明【0j+1】有可能,那么我就持续往里放就行了,也就是说我们传统遍历是从左到右,从上往下,这块利用堆,我们可以以洪泛的方式从原点向周边遍历

代码:

        PriorityQueue<int[]> heap = new PriorityQueue<>((a, b) -> nums1[a[0]] + nums2[a[1]] - (nums1[b[0]] + nums2[b[1]]));
        List<List<Integer>> result = new ArrayList<>();
        for (int i = 0; i < Math.min(k, nums1.length); i++) {
            heap.offer(new int[]{i, 0});
        }
        int count = k;
        while (count > 0&&!heap.isEmpty()) {
            //当前最小
            int[] poll = heap.poll();
            count--;
            result.add(Arrays.asList(nums1[poll[0]], nums2[poll[1]]));
            if (poll[1] + 1 < nums2.length) {
                poll[1] = poll[1] + 1;
                heap.offer(poll);
            }
        }
        return result;

就这还是多次debug之后才能通过所有测试用例的

就这些,有新题再补

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值