《剑指 Offer》专项突破版 - 面试题 59、60 和 61 : 详解堆的应用(C++ 实现)

文章介绍了如何利用堆(最小堆和最大堆)解决数据流中的三个问题:1.在数据流中找到第k大的数字;2.找出数组中出现频率最高的k个数字;3.从两个递增排序数组中找到和最小的k个数对。通过优化数据结构和算法,保证了较高的效率。
摘要由CSDN通过智能技术生成

目录

前言

面试题 59 : 数据流的第 k 大数字

面试题 60 : 出现频率最高的 k 个数字

面试题 61 : 和最小的 k 个数对


 


前言

堆最大的特点是最大值或最小值位于堆的顶部,只需要 O(1) 的时间就可以求出一个数据集合中的最大值或最小值,同时在堆中添加或删除元素的时间复杂度都是 O(logn),因此综合来看堆是一个比较高效的数据结构。如果面试题需要求出一个动态数据集合中的最大值或最小值,那么可以考虑使用堆来解决问题。

堆经常用来求取一个数据集合中值最大或最小的 k 个元素。通常,最小堆用来求取数据集合中 k 个值最大的元素,最大堆用来求取数据集合中 k 个值最小的元素

接下来使用最小堆或最大堆解决几道典型的算法面试题。


面试题 59 : 数据流的第 k 大数字

题目

请设计一个类型 KthLargest,它每次从一个数据流中读取一个数字,并得出数据流已经读取的数字中第 k(k >= 1)大的数字。该类型的构造函数有两个参数:一个是整数 k,另一个是包含数据流中最开始数字的整数数组 nums。该类型还有一个函数 add,用来添加数据流中的新数字并返回数据流中已经读取的数字的第 k 大数字。

例如,当 k = 3 且 nums 为数组 [4, 5, 8, 2] 时,调用构造函数创建类型 KthLargest 的实例之后,第 1 次调用 add 函数添加数字 3,此时已经从数据流中读取了数字 4、5、8、2 和 3,第 3 大的数字是 4;第 2 次调用 add 函数添加数字 5 时,则返回第 3 大的数字 5。

分析

与数据流相关的题目的特点是输入的数据是动态添加的,也就是,可以不断地从数据流中读取新的数据,数据流的数据量是无限的。在这个题目中,类型 KthLargest 的函数 add 用来添加从数据流中读取的新数据。

解决这个题目的关键在于选择合适的数据结构。如果数据存储在排序的数组中,那么只需要 O(1) 的时间就能找出第 k 大的数字。但这个直观的方法有两个缺点。首先,需要把从数据流中读取的所有数据都存到排序数组中,如果从数据流中读取 n 个数字,那么动态数组的大小为 O(n)。随着不断地从数据流中读取新的数据,O(n) 的空间复杂度可能会耗尽所有的内存。其次,在排序数组中添加新的数字的时间复杂度也是 O(n)

下面换一个角度看待第 k 大的数字。如果能够找出 k 个最大的数字,那么第 k 大的数字就是这 k 个最大数字中最小的一个。例如,从数据流中已经读出了 4、5、8、2、3 这 5 个数字,其中最大的 3 个数字是 4、5、8。这 3 个数字的最小值 4 就是 4、5、8、2、3 这 5 个数字中第 3 大的数字。

由于每次都需要找出 k 个数字中的最小值,因此可以把这 k 个数字保存到最小堆中。每当从数据流中读出一个数字,就先判断这个新的数字是不是有必要添加到最小堆中

  1. 如果最小堆中元素的数目还小于 k,那么直接将它添加到最小堆中

  2. 如果最小堆中已经有 k 个元素,那么将其和位于堆顶的最小值进行比较。如果新读出的数字小于或等于堆中的最小值,那么堆中的 k 个数字都比它大,因此它不可能是 k 个最大的数字中的一个。由于只需要保存最大的 k 个数字,因此新读出的数字可以忽略。如果新的数字大于堆顶的数字,那么堆顶的数字就是第 k + 1 大的数字,可以将它从堆中删除,并将新的数字添加到堆中,这样堆中保存的仍然是到目前为止从数据流中读出的最大的 k 个数字,此时第 k 大的数字正好位于最小堆的堆顶

代码实现

class KthLargest {
public:
    KthLargest(int k, vector<int>& nums) : capacity(k) {
        for (int num : nums)
        {
            add(num);
        }
    }
    
    int add(int val) {
        if (minHeap.size() < capacity)
        {
            minHeap.push(val);
        }
        else if (val > minHeap.top())
        {
            minHeap.pop();
            minHeap.push(val);
        }
        return minHeap.top();
    }
private:
    priority_queue<int, vector<int>, greater<int>> minHeap;
    size_t capacity;
};

假设数据流中总共有 n 个数字。这种解法特别适合 n 远大于 k 的场景。当 n 非常大时,内存可能不能容纳数据流中的所有数字。但使用最小堆之后,内存中只需要保存 k 个数字,空间效率非常高


面试题 60 : 出现频率最高的 k 个数字

题目

请找出数组中出现频率最高的 k 个数字。例如,当 k 等于 2 时,输入数组 [1, 2, 2, 1, 3, 1],由于数字 1 出现了 3 次,数字 2 出现了 2 次,数字 3 出现了 1 次,因此出现频率最高的 2 个数字是 1 和 2。

分析

如果在面试过程中遇到这个题目,首先要想到的是解决这个题目需要用到哈希表。这个题目的输入是一个数组,哈希表可以用来统计数组中数字出现的频率,哈希表的键是数组中出现的数字,而值是数字出现的频率

接下来找出出现频率最高的 k 个数字。可以用一个最小堆存储出现频率最高的 k 个数字,堆中的每个元素是数组中的数字及其在数组中出现的次数(即哈希表中数字到频率的映射)。由于比较的是数字的频率,因此设置最小堆比较元素的规则,以便让频率最低的数字位于堆的顶部

代码实现

struct GreaterCmpByCnt
{
    bool operator()(const pair<int, int>& lhs, const pair<int, int>& rhs) const
    {
        return lhs.second > rhs.second;
    }
};
​
class Solution {
public:
    vector<int> topKFrequent(vector<int>& nums, int k) {
        unordered_map<int, int> numToCount;
        for (int num : nums)
        {
            ++numToCount[num];
        }
​
        priority_queue<pair<int, int>, vector<pair<int, int>>, GreaterCmpByCnt> minHeap;
        for (const pair<int, int>& kv : numToCount)
        {
            if (minHeap.size() < k)
            {
                minHeap.push(kv);
            }
            else if (minHeap.top().second < kv.second)
            {
                minHeap.pop();
                minHeap.push(kv);
            }
        }
​
        vector<int> result(k);
        for (int i = 0; i < k; ++i)
        {
            result[i] = minHeap.top().first;
            minHeap.pop();
        }
        return result;
    }
};


面试题 61 : 和最小的 k 个数对

题目

给定两个递增排序的整数数组,从两个数组中各取一个数字 u 和 v 组成一个数对 (u, v),请找出和最小的 k 个数对。例如,输入两个数组 [1, 5, 13, 21] 和 [2, 4, 9, 15],和最小的 3 个数对为 (1, 2)、(1, 4) 和 (2, 5)。

分析

假设第 1 个数组 nums1 的长度为 m,第 2 个数组 nums2 的长度为 n,那么从两个数组中各取一个数字能组成 m x n 个数对。

这个题目要求找出和最小的 k 个数对。可以用最大堆来存储这个 k 个和最小的数对。逐一将 m x n 个数对添加到最大堆中

题目给出的条件是输入的两个数组都是递增排序的,这个特性我们还没有用到。如果从第 1 个数组中选出第 k + 1 个数字和第 2 个数组中的某个数字组成数对 p,那么该数对之和一定不是和最小的 k 个数对中的一个,这是因为第 1 个数组中的前 k 个数字和第 2 个数组中的同一个数字组成的 k 个数对之和都要小于数对 p 之和。因此,不管输入的数组 nums1 有多长,最多只考虑前 k 个数字。同理,不管输入的数组 nums2 有多长,最多也只考虑前 k 个数字。

代码实现

struct LessCmpBySum {
    bool operator()(const vector<int>& lhs, const vector<int>& rhs) 
    {
        return lhs[0] + lhs[1] < rhs[0] + rhs[1];
    }
};
​
class Solution {
public:
    vector<vector<int>> kSmallestPairs(vector<int>& nums1, vector<int>& nums2, int k) {
        priority_queue<vector<int>, vector<vector<int>>, LessCmpBySum> maxHeap;
        int m = min((int)nums1.size(), k), n = min((int)nums2.size(), k);
        for (int i = 0; i < m; ++i)
        {
            for (int j = 0; j < n; ++j)
            {
                if (maxHeap.size() < k)
                {
                    maxHeap.push({ nums1[i], nums2[j] });
                }
                else if (nums1[i] + nums2[j] < maxHeap.top()[0] + maxHeap.top()[1])
                {
                    maxHeap.pop();
                    maxHeap.push({ nums1[i], nums2[j] });
                }
            }
        }
​
        vector<vector<int>> result;
        while (!maxHeap.empty())
        {
            result.push_back(maxHeap.top());
            maxHeap.pop();
        }
        return result;
    }
};

上述代码有两个相互嵌套的 for 循环,每个循环最多执行 k 次(假设数组 num1 和 num2 的长度都大于或等于 k)。在循环体内可能在最大堆中进行添加或删除操作,由于最大堆中最多包含 k 个元素,因此添加、删除操作的时间复杂度都是 O(logk)。这两个 for 循环的时间复杂度是 O(k^2 * logk)。另外,上述代码还有一个 while 循环,它逐一从最大堆中删除元素并将对应的数对添加到 result 数组中,这个 while 循环的时间复杂度是 O(klogk)。因此,上述代码总的时间复杂度是 O(k^2 * logk)

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值