07.01 第 051 ~ 072 题(第 01 ~ 04 天)
分析
使用哈希表+滑动窗口。如果当前字符没有在哈希表中出现过,就将其加入哈希表,右指针继续向右,right-left+1是当前长度,不断更新最大长度;否则不断向右滑动左窗口,直至遇到哈希表内没有当前元素,再把当前元素加入哈希表。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30
| class Solution { public: int lengthOfLongestSubstring(string s) { int n = s.length(); if(n == 0 || n == 1){ return n; } unordered_map<char, int> hash; int left = 0, right = 0; int ans = 0; while(right < n){ if(hash.find(s[right]) == hash.end()){ hash[s[right]] = 1; }else{ hash[s[right]]++; } while(hash[s[right]] > 1){ hash[s[left]]--; left++; } ans = max(ans, right - left + 1); right++; } return ans; } };
|
中心扩散
可以用从中心向两边扩展的方法,对每一个位置都进行这样的操作。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38
| class Solution { public: string longestPalindrome(string s) { int n = s.length(); int len = 1, maxLen = 1; int maxStart = 0; for(int i = 0; i < n; i++){ int left = i - 1, right = i + 1; while(left >= 0 && s[left] == s[i]){ len++; left--; } while(right < n && s[right] == s[i]){ len++; right++; } while(left >= 0 &&right < n && s[left] == s[right]){ len += 2; left--; right++; } if(len > maxLen){ maxLen = len; maxStart = left + 1; } len = 1; } return s.substr(maxStart, maxLen); } };
|
- 时间复杂度:$O(n^2)$
- 空间复杂度:O(1)
动态规划
划分阶段:按照区间长度进行阶段划分。
定义状态:dp[i][j]表示s[i:j]是否是回文的。
状态转移方程:
$s[i] = s[j], dp[i][j] = \begin{cases} true & j-i \leq 2 \cr dp[i+1][j - 1] & else \end{cases}$
初始条件:dp[i][j]=false.
返回结果:s[maxStart:maxStart+maxLen]
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30
| class Solution { public: string longestPalindrome(string s) { int n = s.length(); int maxLen = 1; int maxStart = 0; vector<vector<bool>> dp(n, vector<bool>(n)); for(int j = 1; j < n; j++){ for(int i = 0; i < j; i++){ if(s[i] == s[j]){ if(j - i <= 2){ dp[i][j] = true; }else{ dp[i][j] = dp[i+1][j-1]; } } if(dp[i][j] && j - i + 1 > maxLen){ maxLen = j - i + 1; maxStart = i; } } } return s.substr(maxStart, maxLen); } };
|
Manacher 算法
分析
- 空格:读入字符串并丢弃无用的前导空格(” “)
- 符号:检查下一个字符(假设还未到字符末尾)为 ‘-‘ 还是 ‘+’。如果两者都不存在,则假定结果为正。
- 转换:通过跳过前置零来读取该整数,直到遇到非数字字符或到达字符串的结尾。如果没有读取数字,则结果为0。
- 舍入:如果整数数超过 32 位有符号整数范围 [−231, 231 − 1] ,需要截断这个整数,使其保持在这个范围内。具体来说,小于 −231 的整数应该被舍入为 −231 ,大于 231 − 1 的整数应该被舍入为 231 − 1 。
- 读取在第一个非数字字符处停止。
自己写的:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
| class Solution { public: int myAtoi(string s) { int n = s.length(); int flag = 1; int i = 0; while(i < n && s[i] == ' ') i++; if(s[i] == '-' || s[i] == '+'){ flag = s[i] == '-' ? -1 : 1; i++; } long long num = 0; while(i < n && isdigit(s[i])){ num = num * 10 + (long long)(s[i] - '0'); i++; if(flag * num > INT_MAX) return INT_MAX; if(flag * num < INT_MIN) return INT_MIN; } num *= flag; return num; } };
|
自动机
分析
用一个额外的字符串处理。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30
| class Solution { public: string reverseWords(string s) { int n = s.length(); string ans = ""; reverse(s.begin(), s.end()); int i = 0; while(1){ if(i == n){ break; } while(i < n && s[i] == ' ') i++; string tmp; while(i < n && s[i] != ' '){ tmp += s[i]; i++; } reverse(tmp.begin(), tmp.end()); if(tmp != ""){ ans += tmp + " "; } } ans = ans.substr(0, ans.size()-1); return ans; } };
|
分析
模拟。可以先将字符串反转,从前向后就是从低位到高位的运算。可以在高位的计算结果后补零,这样方便结果相加。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30
| class Solution { public: string multiply(string num1, string num2) { if (num1 == "0" || num2 == "0") return "0";
int n1 = num1.length(); int n2 = num2.length(); vector<int> result(n1 + n2, 0);
for (int i = n1 - 1; i >= 0; --i) { for (int j = n2 - 1; j >= 0; --j) { int mul = (num1[i] - '0') * (num2[j] - '0'); int sum = mul + result[i + j + 1]; result[i + j + 1] = sum % 10; result[i + j] += sum / 10; } }
string res = ""; for (int num : result) { if (!(res.empty() && num == 0)) { res += to_string(num); } }
return res.empty() ? "0" : res; } };
|
分析
纵向遍历,从第一个字符开始,比较每一个字符串的第一个字符是否相等。若相等,则比较第二个字符,若不相等,则结束循环。
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| class Solution { public: string longestCommonPrefix(vector<string>& strs) { string s0 = strs[0]; for(int j = 0; j < s0.size(); j++){ for(string& s : strs){ if(j == s.size() || s[j] != s0[j]){ return s0.substr(0, j); } } } return s0; } };
|
迭代
- 判断二叉树是否为空,为空则直接返回。
- 初始化维护一个栈,将根节点入栈。
- 当栈不为空时:
- 弹出栈顶元素 node,并访问该元素。
- 如果 node 的右子树不为空,则将 node 的右子树入栈。
- 如果 node 的左子树不为空,则将 node 的左子树入栈。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35
|
class Solution { public: vector<int> preorderTraversal(TreeNode* root) { vector<int> ans; if(!root){ return ans; } stack<TreeNode*> stk; stk.push(root); while(!stk.empty()){ TreeNode* node = stk.top(); stk.pop(); ans.push_back(node->val); if(node->right){ stk.push(node->right); } if(node->left){ stk.push(node->left); } } return ans; } };
|
Morris遍历
在二叉树遍历的深度优先遍历中,分析了如何用递归和非递归方式实现遍历二叉树,但是那些方法都无法做到空间复杂度为 O(1),对于递归方法,遍历时用到了函数栈,而对于非递归方法,则是直接申请了栈,这两种方法的空间复杂度均与树的高度相关,设树的高度为 h,则空间复杂度为 O(h)。
二叉树上的很多节点都有大量的空闲指针(如叶节点就有两个空闲指针),比如某些节点没有右孩子节点,那么这个节点的 right 指针就指向 null,我们将之称为空闲状态,而Morris遍历就是使用了这些空闲指针。
- 若 cur == null,则过程停止,否则继续下面的过程;
- 若 cur 无左子树,则令 cur = cur.right;
- 若 cur 有左子树,则找到 cur 左子树上最右的节点,记作 mostRight:
- 若 mostRight.right == null,则令 mostRight.right = cur,即让 mostRight 的 right 指针指向当前节点 cur,然后令 cur = cur.left;
- 若 mostRight.right == cur, 则令 mostRight.right = null,即让 mostRight 的 right 指针指向空,然后令 cur = cur.right。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41
|
class Solution { public: vector<int> preorderTraversal(TreeNode* root) { vector<int> ans; TreeNode* cur = root; while(cur){ if(!cur->left){ ans.push_back(cur->val); cur = cur->right; }else{ TreeNode* leftNode = cur->left; while(leftNode->right && leftNode->right != cur){ leftNode = leftNode->right; } if(leftNode->right == NULL){ ans.push_back(cur->val); leftNode->right = cur; cur = cur->left; }else if(leftNode->right == cur){ leftNode->right = NULL; cur = cur->right; } } } return ans; } };
|
迭代
- 判断二叉树是否为空,为空则直接返回。
- 初始化维护一个栈.
- 当栈不为空或当前结点不为空:
- 当当前节点不为空,将当前节点指向它的左子树,并入栈。
- 弹出栈顶元素 node,并访问该元素。
- 尝试访问右子树。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36
|
class Solution { public: vector<int> inorderTraversal(TreeNode* root) { vector<int> ans; if(!root){ return ans; } stack<TreeNode*> stk; while(root || !stk.empty()){ while(root){ stk.push(root); root = root->left; } root = stk.top(); stk.pop(); ans.push_back(root->val); root = root->right; } return ans; } };
|
Morris遍历
- 若 cur == null,则过程停止,否则继续下面的过程;
- 若 cur 无左子树,则访问当前元素,并令 cur = cur.right;
- 若 cur 有左子树,则找到 cur 左子树上最右的节点,记作 mostRight:
- 若 mostRight.right == null,则令 mostRight.right = cur,即让 mostRight 的 right 指针指向当前节点 cur,然后令 cur = cur.left;
- 若 mostRight.right == cur, 说明我们已经遍历完的左子树,则访问当前元素,并令 mostRight.right = null,即让 mostRight 的 right 指针指向空,然后令 cur = cur.right。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41
|
class Solution { public: vector<int> inorderTraversal(TreeNode* root) { vector<int> ans; while(root){ if(root->left == NULL){ ans.push_back(root->val); root = root->right; }else{ TreeNode* leftNode = root->left; while(leftNode->right && leftNode->right != root){ leftNode = leftNode->right; } if(leftNode->right == NULL){ leftNode->right = root; root = root->left; }else{ ans.push_back(root->val); leftNode->right = NULL; root = root->right; } } } return ans; } };
|
分析
使用一个队列辅助。当队列不为空,弹出节点,将其左右子树加入队列。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41
|
class Solution { public: vector<vector<int>> levelOrder(TreeNode* root) { queue<TreeNode*> que; vector<vector<int>> ans; if(root == nullptr){ return ans; } que.push(root); while(!que.empty()){ int curSize = que.size(); vector<int> cur; for(int i = 0; i < curSize; i++){ TreeNode* node = que.front(); cur.push_back(node->val); que.pop();
if(node->left){ que.push(node->left); } if(node->right){ que.push(node->right); } } ans.push_back(cur); } return ans; } };
|
分析
反转偶数层的数组。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40
|
class Solution { public: vector<vector<int>> zigzagLevelOrder(TreeNode* root) { vector<vector<int>> ans; if(root == nullptr){ return ans; } queue<TreeNode*> que; que.push(root); while(!que.empty()){ int n = que.size(); vector<int> cur; for(int i = 0; i < n; i++){ TreeNode* node = que.front(); que.pop(); cur.push_back(node->val); if(node->left) que.push(node->left); if(node->right) que.push(node->right); } if(ans.size() % 2 == 1){ reverse(cur.begin(), cur.end()); } ans.push_back(cur); } return ans; } };
|
分析

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31
|
class Solution { public: TreeNode* lowestCommonAncestor(TreeNode* root, TreeNode* p, TreeNode* q) { if(root == NULL || root == p || root == q){ return root; } TreeNode* left = lowestCommonAncestor(root->left, p, q); TreeNode* right = lowestCommonAncestor(root->right, p, q); if(left && right){ return root; } if(left){ return left; }else{ return right; } } };
|
- 时间复杂度:O(n)
- 空间复杂度:O(n),最坏情况下,二叉树是一条链,因此递归需要 O(n) 的栈空间。
分析
- 如果p和q都小于当前节点值,则递归左子树。
- 如果p和q都大于当前节点值,则递归右子树。
- 如果分别位于不同子树上,则返回当前节点值。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
|
class Solution { public: TreeNode* lowestCommonAncestor(TreeNode* root, TreeNode* p, TreeNode* q) { if(p->val < root->val && q->val < root->val){ return lowestCommonAncestor(root->left, p, q); } if(p->val > root->val && q->val >root->val){ return lowestCommonAncestor(root->right, p, q); } return root; } };
|
- 时间复杂度:O(n)
- 空间复杂度:O(n),最坏情况下,二叉搜索树退化成一条链(注意题目没有保证它是平衡树),因此递归需要 O(n) 的栈空间。
分析
如果当前节点为空,返回0.否则递归左右子树,取较大值,再+1.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
|
class Solution { public: int maxDepth(TreeNode* root) { if(root == nullptr){ return 0; } int left = maxDepth(root->left); int right = maxDepth(root->right); return max(left, right) + 1; } };
|
- 时间复杂度:O(n)
- 空间复杂度:O(n),最差情况下二叉树的高度为n,递归函数需要栈空间,而栈空间取决于递归的深度,因此空间复杂度等价于二叉树的高度。
这道题也可以用层序遍历的思路去做,在每遍历完一层的时候,ans+1.
把上面的动态规划、马拉车、自动机方法学习一下。