Week 10. 第205-214题

205. 同构字符串

分析

题目要求:

  1. 相同字符不能映射到相同字符
  2. 不同字符不能被映射到相同字符

解决思路:
3. 开个hash表, 存下映射, 存下当前哪个字符—>哪个字符, 每次来个新的字符的时候, 判断下字符是否被映射过了;如果被映射过的话, 判断下映射的字符是否和之前的字符一样 (存一个s—>t)
4. 两个不同的字符是否映射为同一个字符, 这里需要存一个t—>s, 每次对于一个新的字符, 需要判断下, 新的字符时候被之前的某些字符映射过; 如果映射过的话, 需要看一下, 映射过的字符是同一个字符

所以开两个hash表就够了

总结:
相当于判断下, 数学中的双射

code

class Solution {
public:
    bool isIsomorphic(string s, string t) {
        if (s.size() != t.size()) return false;
        unordered_map<char, char>  st, ts;
        for (int i = 0; i < s.size(); i ++ ){
            int a = s[i], b = t[i];
            if (st.count(a) && st[a] != b) return false; // a以前被映射过, 现在a需要匹配b, 但是以前的a并不是映射到b
            st[a] = b;
            if (ts.count(b) && ts[b] != a) return false; // b以前被映射过, 但是并不是a
            ts[b] = a;
        }
        return true;
    }
};

206. 反转链表

分析

见每日一题(春季)Week1

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* reverseList(ListNode* head) {
        if (!head || !head->next) return head;
        auto a = head, b = head->next;
        while (b){
            auto c = b->next;
            b->next = a;
            a = b, b = c;
        }
        head->next = NULL;
        return a;
    }
};

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* reverseList(ListNode* head) {
        if (!head || !head->next) return head;
        auto tail = reverseList(head->next);
        head->next->next = head;
        head->next = NULL;
        return tail;
    }
};

207. 课程表

分析

经典的有向图求拓扑排序的问题

步骤:
1.统计所有点的入度
2.d[u] == 0的入队

拓扑排序

命题: 一个图有拓扑排序的话, 等价于这个图是不存在环的
如果存在环的话, 那么环内所有点没有任何突破口, 任何时刻环内所有点入度都不可能为0, 所以我们算法就不会遍历n个点

code

class Solution {
public:
    bool canFinish(int n, vector<vector<int>>& edges) {
        vector<vector<int>> g(n); // 邻接表
        vector<int> d(n);

        for (auto &e : edges){
            int a = e[0], b = e[1];
            g[a].push_back(b); //  a --> b的边
            d[b] ++ ; // b入度 ++
        }
          

        queue<int> q;

        for (int i = 0; i < n; i ++ )
            if (d[i] == 0)
                q.push(i);

        int cnt = 0; 
        while (q.size()){
            auto t = q.front(); q.pop(); 
            cnt ++; // 统计已经遍历的点的个数
            for (auto i : g[t])
                if (-- d[i] == 0) q.push(i);
        }

        return cnt == n;

    }
};

正向图存在拓扑许 等价于 反向图存在拓扑序
按照题意, 应该这样建边

            int b = e[0], a = e[1];

208. 实现 Trie (前缀树)

分析

因为每个儿子a, b, c, …, 都是按顺序排好的, 所以不用存边a, b, c
所以想看某个点存不存在儿子的话, 直接看指针对应的位置存不存在

指针对应的位置: if (p->son[0])直接看就行了
在这里插入图片描述

时间复杂度

因为每个函数都只有一个循环, 所以时间复杂度是所有单词长度的总和

code

class Trie {
public: 
    struct Node{
        bool is_end;
        Node* son[26]; // 26个字母, 所以26个儿子
        Node() {
            is_end = false;
            for (int i = 0; i < 26; i ++ )
                son[i] = NULL; // 初始化, 先将儿子置空
        }
    }*root;

    /** Initialize your data structure here. */
    Trie() {
        root = new Node();
    }
    
    /** Inserts a word into the trie. */
    void insert(string word) {
        auto p = root;
        for (auto c : word){
            int u = c - 'a';
            if (!p->son[u]) p->son[u] = new Node();
            p = p->son[u];
        }
        p->is_end = true;
    }
    
    /** Returns if the word is in the trie. */
    bool search(string word) {
        auto p = root;
        for (auto c : word){
            int u = c - 'a';
            if (!p->son[u]) return false;
            p = p->son[u];
        }
        return p->is_end;
    }
    
    /** Returns if there is any word in the trie that starts with the given prefix. */
    bool startsWith(string word) {
        auto p = root;
        for (auto c : word){
            int u = c - 'a';
            if (!p->son[u]) return false;
            p = p->son[u];
        }
        return true;
    }
};

/**
 * Your Trie object will be instantiated and called as such:
 * Trie* obj = new Trie();
 * obj->insert(word);
 * bool param_2 = obj->search(word);
 * bool param_3 = obj->startsWith(prefix);
 */

209. 长度最小的子数组

分析

考虑下暴力怎么做

暴力枚举下两个端点, 再求下和 O(n^3)

优化的话, 凡是枚举两个端点的题目, 优化的思路基本都是考虑下单调性, 因为用单调性的话可以去掉1维

考虑单调性的话, 要考虑求的值是什么, 比如现在枚举完右边这个端点i, 找到一个最靠右的j, 使得[j, i]总和 >= s

在这里插入图片描述
所以i往后走的时候, j一定往后走, 因此具有单调性

我们可以边移动指针, 边维护总和, 这样时间复杂度是O(n)的

code

class Solution {
public:
    int minSubArrayLen(int s, vector<int>& nums) {
        int res = INT_MAX;
        for (int i = 0, j = 0, sum = 0; i < nums.size(); i ++ ) {
            sum += nums[i];
            while (sum - nums[j] >= s) sum -= nums[j ++ ]; // 探地雷, 如果当前j的位置上的数, 能够删掉且满足双指针定义, 就让j往后走
            if (sum >= s) res = min(res, i - j + 1);
        }
        if (res == INT_MAX) res = 0;
        return res;
    }
};

210. 课程表 II

分析

在207的基础上, 把遍历的结果存下来即可

code

class Solution {
public:
    vector<int> findOrder(int n, vector<vector<int>>& edges) {
        vector<vector<int>> g(n);
        vector<int> d(n);
        for (auto &e : edges){
            int b = e[0], a = e[1]; // 第2个数是第1个数的先修课程, 所以要从第2个数 指向第1个数
            g[a].push_back(b);
            d[b] ++;
        }
        queue<int> q;
        for (int i = 0; i < n; i ++ )  
            if (d[i] == 0) q.push(i);

        vector<int> res;

        while (q.size()){
            auto t = q.front(); q.pop();
            res.push_back(t);

            for (auto i : g[t])
                if (-- d[i] == 0) q.push(i);
        }
        if (res.size() < n) return {};
        return res;
    }
};

211. 添加与搜索单词 - 数据结构设计

分析

插入的话, hash和trie都是线性的
查找的话比如a.b.c....., hash表是不支持这种查找的
你需要枚举下'.'是哪个单词, 枚举的时候'.'有26种选择
比如k个点的话, 2 6 k 26^k 26k

对于插入来说, Trie树的插入
查找的话, 遇到'.'只会将整棵Trie树种的单词遍历一遍, 肯定比 2 6 k 26^k 26k要小

如图
在红色边中第1条边中遍历的时候, 不遍历到第2条红色边, 因为是一棵树, 所以不会重复遍历

最坏的情况下, 也只会遍历Trie树中所有节点数量多次, 最坏情况下是所有单词插入的总长度
在这里插入图片描述

code

class WordDictionary {
public:
    struct Node {
        bool is_end;
        Node* son[26];
        Node() {
            is_end = false;
            for (int i = 0; i < 26; i ++ )  
                son[i] = NULL;
        }
    }*root;
    /** Initialize your data structure here. */
    WordDictionary() {
        root = new Node();
    }
    
    void addWord(string word) {
        auto p = root;
        for (auto c : word){
            auto u = c - 'a';
            if (!p->son[u]) p->son[u] = new Node();
            p = p->son[u];
        }
        p->is_end = true;
    }
    
    bool search(string word) {
        return dfs(root, word, 0);
    }
    bool dfs(Node* p, string word, int i){
        if (i == word.size()) return p->is_end;
        if (word[i] != '.'){
            int u = word[i] - 'a';
            if (!p->son[u]) return false; // 当前的路不通, 直接返回
            return dfs(p->son[u], word, i + 1); // 通的话, 继续递归
        }else {
            for (int j = 0; j < 26; j ++ ) // 因为i已经用过了, 所以用j
            	// 一定要带p->son[j]条件, 否则递归会到nullptr
                if (p->son[j] && dfs(p->son[j], word, i + 1)) return true; // 当前j通并且可以递归成功, 返回true
            return false;
        }
            
    }

};

/**
 * Your WordDictionary object will be instantiated and called as such:
 * WordDictionary* obj = new WordDictionary();
 * obj->addWord(word);
 * bool param_2 = obj->search(word);
 */

212. 单词搜索 II

分析

第1个单词是a, 第2个单词有1-2个分支, 普通搜索就比较麻烦
因此需要将所有单词维护成一个Trie树, 在Trie树中走
在这里插入图片描述
然后在搜索的时候一定要在Trie树中走, 如果走到Trie树外的话, 表示当前的路径, 一定不存在对应的单词, 所以一定要在Trie树内部走

比方说下一个字母是c, 就是要判断当前点出发是否能够到达c, 如果有的话, 继续走; 没有的话, 就不能走

所以这题在搜索的时候, 将1个单词变成多个单词, 就应该把将多个单词维护成1个Trie
然后在搜的时候, 每次判断当前这一步能不能走, 在Trie树中判断当前这个点, 存不存在一条对应的边就可以了, 如果存在一条对应边的话, 才可以走, 否则的话不能走

然后最后要维护下所有遍历到的单词, 开一个hash表, 把所有遍历到的单词输出出来

这里Trie树不能存结尾了, 要存编号了, 因为你要知道遍历到的是哪个单词, 需要将遍历到的单词输出出来

时间复杂度

最坏情况下dfs会枚举n * m 个起点, 然后每个起点会搜素一条路径, 假设平均长度是k的话, 每条路径除了第1次之外, 每次搜的时候, 下一个方向有3种选择, 所以时间复杂度是 n ∗ m ∗ 4 ∗ 3 k − 2 n * m * 4 * 3^{k - 2} nm43k2, 第1次有4种选择, 后面的k - 2次只有3种选择

code

class Solution {
public:
    struct Node{
        int id;
        Node* son[26];
        Node() {
            id = -1; // 这里注意, 不小心写成 int id = -1, 报错了
            for (int i = 0; i < 26; i ++ ) son[i] = NULL;
        };
    }*root;
    unordered_set<int> ids;
    vector<vector<char>> g;
    int dx[4] = {-1, 0, 1, 0}, dy[4] = {0, 1, 0, -1};
    int n, m;
    void insert(string& word, int id){
        auto p = root;
        for (auto c : word){
            int u = c - 'a';
            if (!p->son[u]) p->son[u] = new Node();
            p = p->son[u];
        }
        p->id = id;
    }

    vector<string> findWords(vector<vector<char>>& board, vector<string>& words) {
        n = board.size(), m = board[0].size();
        g = board;
        root = new Node();

        for (int i = 0; i < words.size(); i ++ ) insert(words[i], i); // 先插入所有单词, 建立Trie

        for (int i = 0; i < n; i ++ )
            for (int j = 0; j < m; j ++ ){
                int u = board[i][j] - 'a';
                if (root->son[u]) dfs(root->son[u], i, j); // 只有起点存在, 才从这个起点开始搜
            }


        vector<string> res;
        for (auto id : ids) res.push_back(words[id]);
        return res;
    }

    void dfs(Node* p, int x, int y){ // 不太熟
        if (p->id != -1) ids.insert(p->id); // 如果已经搜到某个单词末尾, 就直接在ids里添加id
        char t = g[x][y];// 先拷贝
        g[x][y] = '.'; // 改成'.' 防止重复搜索

        for (int i = 0; i < 4; i ++ ){
            int a = x + dx[i], b = y + dy[i];
            if (a >= 0 && a < n && b >= 0 && b < m && g[a][b] != '.'){
                int u = g[a][b] - 'a';
                if (p->son[u]) dfs(p->son[u], a, b); // 只有边存在的时候, 才继续搜
            }
        }
        g[x][y] = t; // 恢复现场
    }
};

213. 打家劫舍 II

分析

一圈点
回顾上一题
f[i] : 必选i,最大收益 f[i] = g[i - 1] + w[i]
g[i]: 必不选i, 最大收益 g[i] = max(f[i - 1], g[i - 1]);

这题就多了1个限制, 起点和终点不能同时选

可以枚举下

1和n不能同时选

1.不选1, f[i]含义就变了, 变成了必选i, 且不选1; g[i]必不选i, 且不选1的最大价值,

由于1号点没选, 所以n号点选没选都可以, 所以答案是max(f[n], g[n])

主要按照上一题做法, 不知道最后一个点要不要选, 因为不知道1号点要不要选, 所以只要1号点确定了, n号点也确定了

2.选1 由于1号点确定了, 所以这种情况下的最大值是g’[n]

最后取下两种情况的max
在这里插入图片描述

code

class Solution {
public:
    int rob(vector<int>& nums) {
        int n = nums.size(); 
        vector<int> f(n + 1), g(n + 1);
        if (n == 1) return nums[0]; // 只有1个数的话, 不特判, 会对g[1]和0取max, 所以要特判
        // 不选起点
        for (int i = 2; i <= n; i ++ ){
            f[i] = g[i - 1] + nums[i - 1];
            g[i] = max(f[i - 1], g[i - 1]);
        }
        int res = max(f[n], g[n]);
        // 必选起点
        f[1] = nums[0];
        g[1] = INT_MIN; // 定义成不合法状态, 按照题目最大收益不合法, 就给一个非常小的数

        for (int i = 2; i <= n; i ++ ){
            f[i] = g[i - 1] + nums[i - 1];
            g[i] = max(f[i - 1], g[i - 1]);
        }

        res = max(res, g[n]); // 第2种情况下, 只能取到g[n], 因为选择起点的状况下,  不能选终点, 所以是g[n]
        return res;
    }
};

214. 最短回文串

分析

给了一段字符串
希望补充一段, 使得补充后的字符串为回文串, 并且补充的长度最短

加一段后, 左右对称的话, 意味着前后min那段是对应的, 那么中间也必然是回文串
目标是让加的那段最小, 等价于后边长度最小, 总长度固定的, 相当于中间部分的回文串最长

因此这个问题相当于求原串中的最长回文前缀, 使得前缀是一个回文串

原串的前缀是回文串等价于新构造的字符串前缀等于后缀
所以如果想求最长的前缀是回文串的话, 相当于是求新串的最长的长度, 在情况下, 前缀和后缀相等

next[i]的定义就是: [1 ~ i] 最大的和后缀的相等的前缀

由于中间的#没有出现过, 因此前缀和后缀不会越过分隔符, 所以一定是合法的

在这里插入图片描述
所以用KMP求一下就可以了

code

注意min那段不是回文串, 因此最后需要将min翻转后 加到前面

class Solution {
public:
    string shortestPalindrome(string s) {
        string t(s.rbegin(), s.rend());
        int n = s.size(); 
        s = ' ' + s + '#' + t;
        vector<int> ne(n * 2 + 2);
        for (int i = 2, j = 0; i <= n * 2 + 1; i ++ ){
            while (j && s[i] != s[j + 1]) j = ne[j]; // 注意是s[i] != s[j + 1]
            if (s[i] == s[j + 1]) j ++ ;
            ne[i] = j;
        }

        int len = ne[2 * n + 1];
        string left = s.substr(1, len), right = s.substr(1 + len, n - len);

        return string(right.rbegin(), right.rend()) + left + right;
    }
};
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值