C++ : 力扣_Top(347-454)

C++ : 力扣_Top(347-454)

文章目录

  • C++ : 力扣_Top(347-454)
      • 347、前 K 个高频元素(中等)
      • 350、两个数组的交集II(简单)
      • 371、两整数之和(简单)
      • 378、有序矩阵中第K小的元素(中等)
      • 380、常数时间插入、删除和获取随机元素(中等)
      • 384、打乱数组(中等)
      • 387、字符串中的第一个唯一字符(简单)
      • 395、至少有K个重复字符的最长子串(中等)
      • 412、Fizz Buzz(简单)
      • 454、四数相加II(中等)


347、前 K 个高频元素(中等)

给定一个非空的整数数组,返回其中出现频率前 k 高的元素。

输入: nums = [1,1,1,2,2,3], k = 2
输出: [1,2]

输入: nums = [1], k = 1
输出: [1]

提示:

你可以假设给定的 k 总是合理的,且 1 ≤ k ≤ 数组中不相同的元素的个数。
你的算法的时间复杂度必须优于 O(n log n) , n 是数组的大小。
题目数据保证答案唯一,换句话说,数组中前 k 个高频元素的集合是唯一的。
你可以按任意顺序返回答案。

class Solution {
public:
    vector<int> topKFrequent(vector<int>& nums, int k) {
        int size = nums.size();
        map<int, int> m;
        for(int i=0; i<size; ++i){
            ++m[nums[i]]; // 统计每个数出现的次数,放入哈希表,类型为pair
        } // 建立优先队列,按照每个数出现的次数从大到小排列
        // 注意比较函数部分要用小于,这样才是大顶堆,与排序中的比较方式不同
        auto lambda = [](pair<int,int>& a, pair<int,int>& b){return a.second<b.second;}; 
        priority_queue<pair<int,int>, vector<pair<int,int>>, decltype(lambda)> q(lambda);
        // priority_queue, vector>, comp> q;
        for(auto it=m.begin(); it!=m.end(); ++it){
            q.push(*it);
        }
        vector<int> res;
        for(int i=0; i<k; ++i){
            res.push_back(q.top().first);
            q.pop();
        }
        return res;
    }
    // struct comp{
    //     bool operator()(const pair& p1, const pair& p2){
    //         return p1.second < p2.second;
    //     }
    // };
};

思路:首先复杂度需要优于nlogn,故不能进行排序,可以用空间换时间;首先需要遍历一次统计<每个数字,出现频率>,存放在map>中,然后利用优先队列priority_queue对其出现次数进行排序,将其前k大的值放到vector中返回;这里需要注意自定义priority_queue中排序方法的写法;更好的办法是只建立k大小的优先队列,然后遍历map并逐个push,最后将优先队列转化为vector;或使用桶排序的方法,需要建立更大的额外空间,但计算复杂度更低:首先还是利用map统计词频,然后建立一个n个vector的二维桶数组,遍历map,将出现次数为y的数字push_back到第y行,然后对二维数组从下往上每行遍历,将每行的数值放到vector返回即可,共返回k个;


350、两个数组的交集II(简单)

给定两个数组,编写一个函数来计算它们的交集。

输入: nums1 = [1,2,2,1], nums2 = [2,2]
输出: [2,2]

输入: nums1 = [4,9,5], nums2 = [9,4,9,8,4]
输出: [4,9]

说明:
输出结果中每个元素出现的次数,应与元素在两个数组中出现的次数一致。
我们可以不考虑输出结果的顺序。

进阶:
如果给定的数组已经排好序呢?你将如何优化你的算法?
如果 nums1 的大小比 nums2 小很多,哪种方法更优?
如果 nums2 的元素存储在磁盘上,磁盘内存是有限的,并且你不能一次加载所有的元素到内存中,你该怎么办?

class Solution {
public:
    vector<int> intersect(vector<int>& nums1, vector<int>& nums2) {
        vector<int> res;
        int size1 = nums1.size();
        int size2 = nums2.size();
        if(size1<size2){ // 将长度较小的数组存储为multiset
            multiset<int> s(nums1.begin(), nums1.end());
            findIntersection(nums2, s, res); // 遍历另一个数组,如果找到就是交集元素
        }
        else{
            multiset<int> s(nums2.begin(), nums2.end());
            findIntersection(nums1, s, res);
        }
        return res;
    }
    void findIntersection(vector<int>& nums, multiset<int>& s, vector<int>& res){
        for(int i=0; i<nums.size(); ++i){
            auto it=s.find(nums[i]);
            if(it!=s.end()){
                res.push_back(nums[i]);
                s.erase(it); // 每找到一个,就在multiset中删除对应的元素
            }
        }
    }
};

思路:求交集,典型的关联容器求映射问题,先将其中一个数组转换为multiset,然后从另一个数组中查找其值在不在set中;也可以建立一个map,先存储数组1的元素值和出现的次数,然后再遍历数组2,遍历到的对应元素进行—m[val]操作,如果减后还大于等于0,则说明是交集元素,如果小于0,说明map中原本没有这个元素,不是交集;


371、两整数之和(简单)

不使用运算符 + 和 -,计算两整数 a 、b 之和。

输入: a = 1, b = 2
输出: 3

输入: a = -2, b = 3
输出: 1

class Solution {
public:
    int getSum(int a, int b) {
        while(b != 0){ // 循环相加,直到没有进位为止
            int temp = a ^ b; // 异或,进行不进位相加
            b = ((unsigned int)(a & b)) << 1; // 相与再左移一位,计算当前需要进位的位数(注意负数情况下同样适用)
            a = temp; // 保存当前相加结果
        }
        return a;
    }
};

思路:不能使用加减号,就利用二进制进行常规相加操作;或者巧用异或运算,进行不进位的相加运算,然后利用与或左移操作,记录当前不进位相加时需要进位的位置;然后再将上一次的不进位相加结果与进位位置进行二次异或,再次记录需要进位的位置,直到不需要再进位为止;需要注意,由于负数采用补码形式,但在加减法的本质上式统一的,故都可以利用这个方式进行计算。其次,较大的负数在左移操作时可能会出现警告,这时可以对与的结果转化为unsigned int 再左移,不影响结果;


378、有序矩阵中第K小的元素(中等)

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

matrix =
[ 1, 5, 9],
[10, 11, 13],
[12, 13, 15]
k = 8,

返回 13。

提示:你可以假设 k 的值永远是有效的,1 ≤ k ≤ n2 。

class Solution {
public:
    int kthSmallest(vector<vector<int>>& matrix, int k) {
        if(matrix.empty() || matrix.size()!=matrix[0].size()) return -1;
        int size = matrix.size();
        int left = matrix[0][0];
        int right = matrix[size-1][size-1]; // 统计当前矩阵中有多少小于等于mid的元素
        while(left < right){ // 从左上角到右下角(最小值和最大值)区间中进行二分搜索
            int mid = (left + right) / 2; // 注意mid的值不一定存在于矩阵中
            int count = findLessNum(matrix, mid, size);
            if(count < k){ // 小于等于当前中间值的元素个数不足k个,则第k个在mid到right之间
                left = mid + 1;
            }
            else{ // 小于等于mid的元素有k个或以上,则第k个在left到mid之间;
                right = mid;
            }
        }
        return right;
    }
    // 统计当前矩阵中有多少小于等于mid的元素
    int findLessNum(vector<vector<int>>& matrix, int mid, int size){
        int i = size-1, j = 0, count = 0; // 从左下角开始查找,对每列进行累积
        while(i>=0 && j <size){
            if(matrix[i][j] <= mid){ // 当前列小于mid,说明该列0-i行都小于mid
                count += i + 1;
                ++j; // 换到下一列判断
            }
            else{ // 当前值大
                --i; // 往上缩一行判断
            }
        }
        return count;
    }
};

思路:一道比较巧妙的题;不能像在矩阵中查找一个数值一样按行或列进行排除查找,一种比较方便的做法是利用优先队列(最大堆),遍历矩阵,并将每个元素push进队列中;然后pop掉头部的n^2-k个大的元素,之后队列的头部就是第k小的元素了;还有一种方式是在矩阵中使用二分法,稍微有点绕,但总体比较好理解,需要对二分进行熟练运用;首先根据左上角和右下角两个最小值最大值的区间作为范围进行二分,找到中间值,然后计算矩阵中有多少小于等于中间值的元素,和k进行比较,直到二分到最后左右边界相同时,返回最后的值即可;需要注意边界条件的判断有些难度;


380、常数时间插入、删除和获取随机元素(中等)

设计一个支持在平均 时间复杂度 O(1) 下,执行以下操作的数据结构。

insert(val):当元素 val 不存在时,向集合中插入该项。
remove(val):元素 val 存在时,从集合中移除该项。
getRandom:随机返回现有集合中的一项。每个元素应该有相同的概率被返回。

输入:
[“RandomizedSet”,“insert”,“remove”,“insert”,“getRandom”,“remove”,“insert”,“getRandom”]
[[],[1],[2],[2],[],[1],[2],[]]

输出:
[null,true,false,true,2,true,false,2]

class RandomizedSet {
private:
    vector<int> v; // 数组,保存数据
    unordered_map<int, int> m; // 哈希表,保存数据与下标的映射
public:
    RandomizedSet(){}
    bool insert(int val) {
        if(m.find(val)!=m.end()) return false;
        v.push_back(val);
        m.insert(pair<int,int>(val, v.size()-1));
        return true;
    }
    bool remove(int val) { // 删除的时候,在数组中将要删除的数据与数组最后的元素交换,然后删除最后的元素
        if(m.find(val)==m.end()) return false;
        m[v.back()] = m[val]; // 更新对应的哈希表下标映射
        swap( v[m[val]], v.back() ); 
        v.pop_back();
        m.erase(val);
        return true; 
    }
    int getRandom() {
        if(v.empty()) return -1;
        return v[rand() % v.size()]; // 利用rand函数随机返回
    }
};

思路:主要问题在于随机读取某一个元素,随机过程可以利用rand()函数实现,但是关联容器的迭代器无法通过对迭代器直接加减某个常数进行访问,所以可以建立vector数组存放数据,再建立map存放数据与数据下标的映射,这样随机返回的时候直接在数组中放回;但删除操作时需要进行额外处理:从数组中间删除非常费时,可以在删除时将数组末尾元素与要删除元素进行交换,然后直接push_back,同时也需要更新哈希表中的下标映射就可以了;注意,在无法使用迭代器直接加减的容器中,可以使用next或prev处理迭代器,如next(m.begin(),5);来返回一个5个位置之后的迭代器,但该方法的计算复杂度貌似不是o(1),其内部也需要遍历;


384、打乱数组(中等)

打乱一个没有重复元素的数组。

// 以数字集合 1, 2 和 3 初始化数组。
int[] nums = {1,2,3};
Solution solution = new Solution(nums);

// 打乱数组 [1,2,3] 并返回结果。任何 [1,2,3]的排列返回的概率应该相同。
solution.shuffle();

// 重设数组到它的初始状态[1,2,3]。
solution.reset();

// 随机返回数组[1,2,3]打乱后的结果。
solution.shuffle();

class Solution {
    vector<int> origin;
    vector<int> shuff;
public:
    Solution(vector<int>& nums) {
        origin.assign(nums.begin(), nums.end());
        shuff.assign(nums.begin(), nums.end());
    }
    vector<int> reset() {
        shuff.assign(origin.begin(), origin.end());
        return shuff;
    }
    vector<int> shuffle() {
        int size = shuff.size();
        for(int i=0; i<size; ++i){
            int j = i + rand() % (size-i); // 返回一个[i,size]区间内的随机数
            swap(shuff[i], shuff[j]);
        }
        return shuff;
    }
};

思路:经典的洗牌算法,使用 rand()%val 生成值域在 [0, val-1] 内的随机数下标 j,遍历数组,对每个下标i交换 num[i] 和 num[j] 即可; 注意下标 j 的取值必须在下标 i 之后,这样才能实现均匀输出 n! 的洗牌版本; 如5个数字洗牌,该算法可以生成的情况为 54321 共 5! 种;


387、字符串中的第一个唯一字符(简单)

给定一个字符串,找到它的第一个不重复的字符,并返回它的索引。如果不存在,则返回 -1。

s = “leetcode”
返回 0

s = “loveleetcode”
返回 2

class Solution {
public:
    int firstUniqChar(string s) {
        int dict[26] = {0}; // 字符哈希表
        for(auto c : s){
            dict[c-'a']++; // 对应的字符位置统计计数
        }
        for(int i=0; i<s.size(); ++i){
            if(dict[s[i]-'a']==1){
                return i;
            }
        }
        return -1;
    }
};

思路:先利用26长度的数组当做哈希表统计每个字符出现的次数,然后再次遍历字符串,返回第一个计数为1的字符下标;


395、至少有K个重复字符的最长子串(中等)

找到给定字符串(由小写字符组成)中的最长子串 T , 要求 T 中的每一字符出现次数都不少于 k 。输出 T 的长度。

输入:
s = “aaabb”, k = 3

输出:
3

最长子串为 “aaa” ,其中 ‘a’ 重复了 3 次。

输入:
s = “ababbc”, k = 2

输出:
5

最长子串为 “ababb” ,其中 ‘a’ 重复了 2 次, ‘b’ 重复了 3 次。

class Solution {
public:
    int longestSubstring(string s, int k) {
        if(s.size() < k) return 0;
        unordered_map<char,int> m;
        for(auto c : s){
            ++m[c]; // 统计当前字符串中每个字符出现的频次
        }        
        vector<int> split; // 存放出现次数不足k的分割点下标
        for(int i=0; i<s.size(); ++i){
            if(m[s[i]]<k){
                split.push_back(i); 
            }
        }
        if(split.empty()){ // 当前字符串满足条件,直接返回字符串长度
            return s.size();
        }
        split.push_back(s.size()); // 最后也要有一个分割点
        int left = 0, ans = 0;
        for(int i=0; i<split.size(); ++i){ // 按照每个分割点进行分治处理
            int len = split[i] - left; // 每段分治字符串的长度
            if(len>=k){
                ans = max(ans, longestSubstring(s.substr(left, len), k)); // 递归分治每一段分割出的字符串
            }
            left = split[i] + 1; // 更新每一段分割串的起始点
        }
        return ans;
    }
};

思路:这道题很有参考价值,第一眼看上去是一个求解最优的问题,想着会不会是用动态规划,然后发现动态规划很难解决这道题;换一个思路:要求得子串中每个字符次数都不小于k次,即字符串中不能含有出现次数小于k次的字符,则这种字符可以把数组分割开来,分割开之后,形成了一模一样的子问题,这就是一个分治思想的体现,代码可以用递归来实现;具体的代码思路不太好想,在统计每个字字符串时,利用map统计每个字符出现的次数,然后收集出分割点的下标,如果当前子串没有分割点了,则说明子串符合要求,当前长度就是子串长度;如果有分割点,则对每个分割出的子串进行递归处理,分割到最后的子串没有分割点,返回的是0或者子串的长度,在递归语句处利用max求取最后的最大长度;具体见代码,重点思考的题目;


412、Fizz Buzz(简单)

写一个程序,输出从 1 到 n 数字的字符串表示。

  1. 如果 n 是3的倍数,输出“Fizz”;
  2. 如果 n 是5的倍数,输出“Buzz”;
    3.如果 n 同时是3和5的倍数,输出 “FizzBuzz”。

n = 15,
返回:
“1”,
“2”,
“Fizz”,
“4”,
“Buzz”,
“Fizz”,
“7”,
“8”,
“Fizz”,
“Buzz”,
“11”,
“Fizz”,
“13”,
“14”,
“FizzBuzz”

class Solution {
public:
    vector<string> fizzBuzz(int n) {
        vector<string> result;
        if(n<1) return result;
        for(int i=1; i<=n; ++i){
            if(i%3==0&&i%5==0){
                result.push_back("FizzBuzz");
                continue;
            }
            if(i%3==0){
                result.push_back("Fizz");
                continue;
            }
            if(i%5==0){
                result.push_back("Buzz");
                continue;
            }
            result.push_back(to_string(i));
        }
        return result;
    }
};

思路:简单的条件判断就可以了;


454、四数相加II(中等)

给定四个包含整数的数组列表 A , B , C , D ,计算有多少个元组 (i, j, k, l) ,使得 A[i] + B[j] + C[k] + D[l] = 0。

为了使问题简单化,所有的 A, B, C, D 具有相同的长度 N,且 0 ≤ N ≤ 500 。所有整数的范围在 -228 到 228 - 1 之间,最终结果不会超过 231 - 1 。

输入:
A = [ 1, 2]
B = [-2,-1]
C = [-1, 2]
D = [ 0, 2]

输出:
2

解释:
两个元组如下:
1. (0, 0, 0, 1) -> A[0] + B[0] + C[0] + D[1] = 1 + (-2) + (-1) + 2 = 0
2. (1, 1, 0, 0) -> A[1] + B[1] + C[0] + D[0] = 2 + (-1) + (-1) + 0 = 0
class Solution {
public:
    int fourSumCount(vector<int>& A, vector<int>& B, vector<int>& C, vector<int>& D) {
        int sum, res = 0;
        unordered_map<int,int> m1, m2;
        int size = A.size();
        for(int i=0; i<size; ++i){
            for(int j=0; j<size; ++j){ // 统计A和B元素可以组成的和值
                sum = A[i] + B[j];
                ++m1[sum];
            }
        }
        for(int i=0; i<size; ++i){ // 统计C和D元素可以组成的和值
            for(int j=0; j<size; ++j){
                sum = C[i] + D[j];
                ++m2[sum];
            }
        }
        for(auto pair : m1){ // 对两组和值进行求解,等价于求解两数之和
            auto it = m2.find(-pair.first);
            if(it!=m2.end()){ // 找到可以组合的四元组
                res += (pair.second * (*it).second);
            }
        }
        return res;
    }
};

思路:比较好想的题,如果之前做过两数之和,就会想到将该题目转换为两数之和来求解;首先两两数组求出其元素能够组成的和值,然后再对两组和值进行匹配。使用map方便记录每个和值的组合对的个数;最后是利用相乘计算四元组的个数;注意这里使用unordered_map比map要快,因为省去了红黑树排序的问题;


你可能感兴趣的:(C++ : 力扣_Top(347-454))