205. 同构字符串
分析
题目要求:
- 相同字符不能映射到相同字符
- 不同字符不能被映射到相同字符
解决思路:
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} n∗m∗4∗3k−2, 第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;
}
};