leetcode 21-30

21. 合并两个有序链表

分析

二路归并算法
比较当前两个指针的值, 哪个比较小, 就接到答案的末尾, 然后让较小的指针后移, 最后再让答案的指针指向下一个位置.

code

/**
 * Definition for singly-linked list.
 * struct ListNode {
 *     int val;
 *     ListNode *next;
 *     ListNode() : val(0), next(nullptr) {}
 *     ListNode(int x) : val(x), next(nullptr) {}
 *     ListNode(int x, ListNode *next) : val(x), next(next) {}
 * };
 */
class Solution {
public:
    ListNode* mergeTwoLists(ListNode* l1, ListNode* l2) {
        auto dummy = new ListNode(-1), tail = dummy;
        while (l1 && l2){
            if (l1->val < l2->val){
                tail = tail->next = l1;
                l1 = l1->next;
            }else {
                tail = tail->next = l2;
                l2 = l2->next;
            }
        }
        if (l1) tail->next = l1;
        if (l2) tail->next = l2;
        return dummy->next;
    }
};

22. 括号生成

分析

合法的括号序列必须满足两个条件

  1. 任何前缀中(数量 >= )数量
  2. (数量 = )数量

因此dfs中需要保证 ( >), 注意是严格大于

code

class Solution {
public:
    vector<string> res;
    vector<string> generateParenthesis(int n) {
        dfs(0, 0, "", n);
        return res;
    }
    void dfs(int l, int r, string path, int n){
        if (l == n && r == n){
            res.push_back(path);
        }else {
            if (l < n) {
                dfs(l + 1, r, path + '(', n);
            }
            if (l > r && r < n)
                dfs(l, r + 1, path + ')', n);
        }
    }
};

23. 合并K个升序链表

分析

简单的优先队列操作

code

/**
 * Definition for singly-linked list.
 * struct ListNode {
 *     int val;
 *     ListNode *next;
 *     ListNode() : val(0), next(nullptr) {}
 *     ListNode(int x) : val(x), next(nullptr) {}
 *     ListNode(int x, ListNode *next) : val(x), next(next) {}
 * };
 */
class Solution {
public:
    struct cmp{
        bool operator()(ListNode* a, ListNode* b){
            return a->val > b->val;
        }
    };
    ListNode* mergeKLists(vector<ListNode*>& lists) {
        auto dummy = new ListNode(-1), tail = dummy;
        priority_queue<ListNode*, vector<ListNode*>, cmp> heap;
        for (auto list : lists){
            if(list) heap.push(list);
        }
        while (heap.size()){
            auto cur = heap.top(); heap.pop();
            tail = tail->next = cur;
            if (cur->next) heap.push(cur->next);
        }
        return dummy->next;
    }
};

24. 两两交换链表中的节点

分析

在这里插入图片描述

code

/**
 * Definition for singly-linked list.
 * struct ListNode {
 *     int val;
 *     ListNode *next;
 *     ListNode() : val(0), next(nullptr) {}
 *     ListNode(int x) : val(x), next(nullptr) {}
 *     ListNode(int x, ListNode *next) : val(x), next(next) {}
 * };
 */
class Solution {
public:
    ListNode* swapPairs(ListNode* head) {
        auto dummy = new ListNode(-1);
        dummy->next = head;
        for (auto p = dummy; p->next && p->next->next;){
            auto a = p->next, b = a->next;
            p->next = b; // 1
            a->next = b->next; // 2
            b->next = a; // 3
            p = a; // 4
        }
        return dummy->next;
    }
};

25. K 个一组翻转链表

分析

在循环中, 需要判断链表长度够不够k个, 如果不够, 直接break
够的话, 开始翻转:

  1. 先找到后面的节点c
  2. 让b->a
  3. a = b, b = c;
    如下图(k个链表内部翻转):
    在这里插入图片描述

一段完成后的扫尾工作:
在这里插入图片描述

code

/**
 * Definition for singly-linked list.
 * struct ListNode {
 *     int val;
 *     ListNode *next;
 *     ListNode() : val(0), next(nullptr) {}
 *     ListNode(int x) : val(x), next(nullptr) {}
 *     ListNode(int x, ListNode *next) : val(x), next(next) {}
 * };
 */
class Solution {
public:
    ListNode* reverseKGroup(ListNode* head, int k) {
        ListNode* dummy = new ListNode(-1);
        dummy->next = head;
        for (auto p = dummy; p->next && p->next->next;){
            auto q = p;
            // 因为q是当前需要翻转的k个指针的前一个指针, 因此循环k次
            for (int i = 0; i < k && q; i ++ ) q = q->next;  // 注意这里要加上 &&q 条件, 否则容易循环到nullptr
            if (!q) break;
            auto a = p->next, b = a->next;
            for (int i = 0; i < k - 1; i ++ ){
                auto c = b->next; // 找到后面的节点c
                b->next = a; // 翻转
                a = b, b = c; // 找到下一次
            }
            auto c = p->next;
            p->next = a, c->next = b; // 1. 2.
            p = c;// 3.
        }
        return dummy->next;
    }
};

26. 删除排序数组中的重复项

分析

同leetcode 80
保证[0, k - 1]已经是无重复的数, 然后考虑x(nums[i])能往第k个位置上放, 因为是有序的往后搜索的, 所以x至多跟nums[k - 1]上的数相同.

  1. 如果不相同, 那么直接放到第k个位置上, k ++
  2. 如果相同, 那么跳过当前x, 往nums后面的元素遍历, for循环会自动往后走

code

class Solution {
public:
    int removeDuplicates(vector<int>& nums) {
        int k = 0;
        for (auto x : nums)
            if (!k || nums[k - 1] != x)
                nums[k ++] = x;
        return k;
    }
};

27. 移除元素

分析

遍历数组, [0, k - 1]表示没有= val的元素

code

class Solution {
public:
    int removeElement(vector<int>& nums, int val) {
        int k = 0;
        for (auto &x : nums)
            if (x != val)
                nums[k ++ ] = x;
        return k;
    }
};

28. 实现 strStr()

分析

KMP模板题
next[i] = j含义: 当前字符串以i结尾最大后缀 = 前缀的长度为j, 即p[i - j + 1 ~ i] = p[1 ~ j]
注意 next数组是针对模版串p的next,因为需要对模版串计算 最大前缀和后缀相同,在文本中移动模版串。

code

class Solution {
public:
    int strStr(string s, string p) {
        if (p.empty()) return 0;
        int n = s.size(), m = p.size();
        s = ' ' + s, p = ' ' + p;
        vector<int> ne(m + 1);
        for (int i = 2, j = 0; i <= m; i ++ ){
            while (j && p[i] != p[j + 1]) j = ne[j];
            if (p[i] == p[j + 1]) j ++;
            ne[i] = j;
        }

        for (int i = 1, j = 0; i <= n; i ++ ){
            while (j && s[i] != p[j + 1]) j = ne[j];
            if (s[i] == p[j + 1]) j ++ ;
            if (j == m){ // 此时说明已经匹配成功m个字符
                return i - m;  // 那么在i中开头的字符为 i - m
            }
        }
        return -1;
    }
};

29. 两数相除

分析

一次一次减, 是可以做
但是如果 x = 2 31 , y = 1 x = 2^{31}, y = 1 x=231,y=1, 就需要减 2 31 2^{31} 231次, 超时
需要想一些优化的做法.
倍增/ 二进制/ 快速幂思想
在这里插入图片描述
具体怎么将 2 1 , 2 4 , 2 5 2^1, 2^4, 2^5 21,24,25找出来呢
从大到小考虑
在这里插入图片描述
负数的话
在这里插入图片描述

code

class Solution {
public:
    int divide(int x, int y) {
        typedef long long LL;
        vector<LL> exp;
        bool is_minus = false;
        if (x < 0 && y > 0 || x > 0 && y < 0) is_minus = true;

        LL a = abs((LL)x), b = abs((LL)y);
        for (LL i = b; i <= a; i = i + i) exp.push_back(i); // 从小到大计算 2^0b, 2^1b, ... 2^30b

        LL res = 0;
        for (int i = exp.size() - 1; i >= 0; i -- ){ // 倒序遍历找最大的2^k*b <= a , 2^k就是商
            if (a >= exp[i]){
                a -= exp[i]; 
                res += 1ll << i; // 注意-2147483647 / -1, 移位会溢出, 所以用1ll << i
            }
        }

        if (is_minus) res = -res;
        if (res > INT_MAX || res < INT_MIN) res = INT_MAX;
        return res;
    }
};

30. 串联所有单词的子串

分析

先考虑下, 我们要枚举哪些情况
n : s.size()
m : words.size(); 单词的个数
w : word[0].size() 每个单词的长度, 题目规定所有单词长度相等
因为我们要枚举所有m * w的子串, 大概要枚举 O ( n ) O(n) O(n)个, 要枚举下起始位置, 起始位置可以是0, 1, 2, 3 …
可以把所有起始位置按照w的余数来分类.
第1类:
0, w, 2w, 3w, …
1, w + 1, 2w + 1, 3w + 1

w - 1, 2w - 1, 3w - 1, 4w - 1, …
即: 将所有起始位置分成w组, 分成w组有啥好处呢?

0 w 2w 3w 4w 5w 6w 7w
可以发现每一个单词的位置都是被限定好的, 每一个单词都会出现在区间里, 不会跨越kw
所有单词必然是会放到每个坑里, 不会横跨某个坑
在这里插入图片描述

对于第一类, 我们是要找, 连续m个窗口, 使得连续个m个子区间, 恰好可以对应到m个单词
我们可以将每个区间的单词, 看成一个整体, 问题转化为:
给了一堆连续的数, 让我们在这一堆连续的m个元素, 恰好是我们给定的m个元素
这就是经典的滑动窗口问题了

首先将m个单词存到hash表里,然后用长度是m的滑动窗口来维护所有长度是m的连续子段, 窗口里所有元素也存到hash表里, 每次窗口往后移动, 只会增加1个元素, 删除1个元素, O(1).
每次对于1个新的窗口, 当前维护的hash表的集合, 和一开始给的集合, 是不是对应的. 如果对应, 那就是一组解

怎么去判断两个集合相等?
cnt表示当前窗口中有效的单词个数(同leetcode 3)

总的时间复杂度:
对于每一类, 有n / w个小区间, 遍历字符串, 每个w长度的字符串只会进hash表1次, 出hash表1次, 计算的次数是n / w, 单词往hash表插是O(w)的, O(n / w * w) , 然后一共有w组, O(n / w * w) * w = O(nw)
如果是字符串hash的做法, 每一类的做法会变成O(n / w), 总的时间复杂度会变成O(n)

联动题

以下题目中用的cnt表示有效单词个数, 异曲同工.
LeetCode 3. 无重复字符的最长子串
LeetCode 76. 最小覆盖子串

code

class Solution {
public:
    vector<int> findSubstring(string s, vector<string>& words) {
        vector<int> res;
        if (words.empty()) return res;
        int n = s.size(), m = words.size(), w = words[0].size();
        unordered_map<string, int> tot;
        for (auto& word : words) tot[word] ++;

        for (int i = 0; i < w; i ++ ){ // 分成w组, 每组以i(w的余数)为开头
            unordered_map<string, int> wd; // 创建窗口的要放到每一组里, 相当于初始化窗口
            int cnt = 0;// 每类w, 初始化cnt
            for (int j = i; j + w <= n; j += w) { // 窗口右端点为j + w - 1 <= n - 1, 可以简写成j + w <= n, j表示当前窗口指针
                if (j >= i + m * w){// 如果当前窗口指针j, 已经超过起点i出发, m个单词的位置(m * w), 表示左边界已经不在窗口内了
                    auto word = s.substr(j - m * w, w); // 超出左边界出窗口的单词
                    wd[word] --; // 窗口内该单词数量 -1
                    if (wd[word] < tot[word]) cnt --; // 如果减掉的是有效的单词
                }
                auto word = s.substr(j, w); // 找出当前需要加入到窗口的单词
                wd[word] ++; // 窗口内单词次数 +1
                if (wd[word] <= tot[word]) cnt ++; // 如果是有效的单词
                if (cnt == m) res.push_back(j - (m - 1) * w); // 注意: 当前窗口是[..., j + w], 那么起点是 j - (m - 1)* w
            }
        }
        return res;
    }
};
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值