11. 盛最多水的容器
分析
用两个指针, 一个指针指向最开头, 一个指针指向最结尾,
如果第2个指针的高度比较低的话, 那就把第2个指针, 往前移动1位
如果第1个指针的高度比较低的话, 那就把低1个指针, 往后移动1位
每移动完一次, 求下当前两指针所构成的面积, 更新下最大值
模拟样例:
第1次:高度: [1, 8, 6, 2, 5, 4, 8, 3, 7]
下标: [0, 1, 2, 3, 4, 5, 6, 7, 8]
距离是8, 高度是1, 面积8
第2次, 高度: [1, 8, 6, 2, 5, 4, 8, 3, 7] 因为1比较矮, 前指针往后移动1格
距离是7, 高度是7, 7 x 7 = 49
做法的正确性
证明: 两指针一开始在最优解的两侧, 每次其中一个指针往中间靠拢, 必然会有其中一个指针到达最优解的一边, 要么左边先到, 要么右边先到, 一定会有一个先到, 不妨设左边先到
假设左边先到的话
然后我们证明后指针所指向的高度, 严格小于 前指针所指向的高度
反证法: 如果后指针所指向的高度 >= 前指针所指向的高度, 那么会有新的面积(图中红色框的面积)
原最优解的面积最大也是 蓝色虚线的面积, 显然小于红色面积, 与假设蓝色指针所指向的位置是最优解, 矛盾
因此, 如果左边先到达边界, 后指针的高度必然是 < 前指针的高度, 因此后指针一直可以往前走, 那么一定可以遍历到最优解, 取到的最大值就是解
code
class Solution {
public:
int maxArea(vector<int>& height) {
int res = 0;
for (int i = 0, j = height.size() - 1; i < j;){
res = max(res, min(height[i], height[j]) * (j - i));
if (height[i] < height[j]) i ++;
else j --;
}
return res;
}
};
12. 整数转罗马数字
分析
罗马数字一般左边大, 右边小, II, 表示2, XII, 10 + 2; 如果左边小, 右边大, 表示减法, IV, 表示5 - 1 = 4
先看
个位 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 |
---|---|---|---|---|---|---|---|---|---|
I | II | III | IV | V | VI | VII | VIII | IX | |
十位 | 10 | 20 | 30 | 40 | 50 | 60 | 70 | 80 | 90 |
X | XX | XXX | XL | L | LX | LXX | LXXX | XC | |
百位 | 100 | 200 | 300 | 400 | 500 | 600 | 700 | 800 | 900 |
C | CC | CCC | CD | D | DC | DCC | DCCC | CM | |
千位 | 1000 | 2000 | 3000 | 4000 | 5000 | 6000 | 7000 | 8000 | 9000 |
M | MM | MMM |
有了这个表示后, 可以直接查表做
比方说1234, 千位是1-M, 百位是2-CC, 十位是3-XXX, 个位是4-IV
MCCXXXIV
再来个3999
千位是3-MMM, 百位是9-CM, 十位是9-XC, 个位是9-IX
MMMCMXCIX
因此直接模拟即可, 实现的时候一些技巧
技巧
千位的话, 有几个1千, 值就-几个1千, 答案就+几个M
百位的话, 有规律, 100, 200, 300之间的话, 有几个1百, 就+几个C(这里指数字->罗马), 600, 700, 800, 有几个C就加几个百(这里指罗马->数字)
100, 400 500 900没有规律, 单独需要记住
从大到考虑这几项, 如果>= 900, 就对应成CM, -900; >= 500, 就对应成D, -500; >= 400, 对应成CD, - 400; >= 100, 对应成C, - 100
模拟下, 如果数是942, 那么会对应一个CM, 然后-900
如果是8xx, 那么会对应D, -500, 变成3xx, 3xx >= 100, 会给一个C, 然后变成2xx, 会再给一个C, 变成1xx, 再给个C, 变成xx, 所以合起来是DCCC, 正好对应800的DCCC
对应十位, 个位也是类似, 单独扣出, 90, 50, 40, 10, 9, 5, 4, 1
模拟下2649
>= 1000, 给个M, 变成1649, 仍然>= 1000, 再给M, 变成649, 当前:MM
649 >= 500, 给个D, 变成149, >= 100, 给个C,变成49 当前:MMDC
49 >= 40, 给个XL, 变成9 当前:MMDCXL
9>=9, 给个IX, 变成0, 结束, 当前MMDCXLIX
code
class Solution {
public:
string intToRoman(int num) {
int value[] = {
1000,
900, 500, 400, 100,
90, 50, 40, 10,
9, 5, 4, 1
};
string reps[] = {
"M",
"CM", "D", "CD", "C",
"XC", "L", "XL", "X",
"IX", "V", "IV", "I",
};
string res;
for (int i = 0; i < 13; i ++ ){
while (num >= value[i]){
res += reps[i];
num -= value[i];
}
}
return res;
}
};
13. 罗马数字转整数
分析
找规律, 除了4, 40, 400, 9, 90, 900, 其他罗马数字相加就是结果
比方说, 80 = 50 + 10 + 10 + 10, L+X+X+X; 700 = 500 + 100 + 100, D+C+C
但是4, 9这些怎么处理, 可以发现前面字母 < 后面字母, 就用减法, 其他情况就加
2649 MMDCXLIX
M: 1000, M: 1000, 2000
DC: D= 500, C = 100, D>C, 那么 500 + 100 = 600
XL: X= 10, L = 50, X<L, 那么 -10 + 50
IX: I= 1, X = 10, I < X, 那么 -1 + 10
code
因为是从千位往个位算, 因此遇到MD这个, 必然会先加上千位, 然后进行DC判断
class Solution {
public:
int romanToInt(string s) {
unordered_map<char, int> hash;
hash['I'] = 1, hash['V'] = 5;
hash['X'] = 10, hash['L'] = 50;
hash['C'] = 100, hash['D'] = 500;
hash['M'] = 1000;
int res = 0;
for (int i = 0; i < s.size(); i ++ )
if (i + 1 < s.size() && hash[s[i]] < hash[s[i + 1]])
res -= hash[s[i]];
else res += hash[s[i]];
return res;
}
};
14. 最长公共前缀
分析
首先考虑下时间复杂度, 最起码是所有字符串的长度之和
因为只要能做到时间复杂度 < 所有字符串的长度之和, 就可以了
首先看下每个字符串的第1个字母, 是否一样? 如果不一样, 公共前缀是空的
一样的话, 最长公共前缀起码是1, 再来看下第2个字母是否都一样, 还一样的话, 看第3个
直到找到不一样的, 比如第4个字母, 不全一样, 那么最长的公共前缀就是3
时间复杂度 <= 所有字符串长度之和
两重循环, 第1重循环枚举当前枚举的是第几个字符, 第二重循环枚举下所有字符串, 看下所有字符串的当前位置是否都一样
code
class Solution {
public:
string longestCommonPrefix(vector<string>& strs) {
string res;
if (strs.empty()) return res;
for (int i = 0;; i ++ ){ // 循环没有结束条件, 因为循环里必然会退出
if (i >= strs[0].size()) return res; // 如果当前循环位置 >= 第1个字符串长度, 直接返回
else {
char c = strs[0][i]; // 取出第1个字符串当前位置的字母
for (auto& str : strs) // 遍历所有字符串
if (i >= str.size() || str[i] != c) return res; // 如果遍历到的字符串长度不够 或者 字母不相同
res += c;
}
}
return res;
}
};
code(代码优化)
class Solution {
public:
string longestCommonPrefix(vector<string>& strs) {
string res;
if (strs.empty()) return res;
for (int i = 0; i < strs[0].size(); i ++ ){
char c = strs[0][i];
for (auto& str : strs){
if (str[i] != c) return res;
}
res += c;
}
return res;
}
};
15. 三数之和
分析
注意是三个元素a, b, c. a, b, c不能是同一个元素, 但是他们的数值可以是一样的.
不能重复, 1, 2, 3和2, 3, 1算同一个
双指针算法:
双指针算法一定要保证序列有序, 有序才能用双指针
所以先将整个数组排序, 然后先枚举第一个数, 因为双指针, 但是有3个数, 不可能有3指针算法. 即: 先枚举第1个数的位置
为了避免重复, 要求
i
<
j
<
k
i < j < k
i<j<k, 这样就会减少重复情况
当
i
i
i固定以后,
j
,
k
j, k
j,k可以用双指针了, 假设
j
j
j固定了, 找到一个最小的k使得nums[j] + nums[k] + nums[i] >= 0
, 那么对于每一个j
, 都有一个固定的k
(可以求出来这个k
)
当然可以先枚举j
, 再枚举k
两维循环, 找最小的k
暴力枚举的话O(n^2), 可以找到每一个j
对应的k
双指针的话, 可以到O(n)
为什么可以用双指针
因为整个序列是有序的,nums[i]
是固定的, 当j
往后移动的时候,nums[j]
⬆️, 那么nums[k]
⬇️(因为需要保证nums[j] + nums[k] + nums[i] >= 0
)
j->j’的话, k一定往左走, 走到k’
yxc:因此j只会递增, k只会递减, j最多只会走n, k最多也只会走n, 加到一块O(2n)(但是, 我认为j + k最多走n步, 应该上课讲错了, 虽然答案也是O(n))
重复答案处理
比方说 1 1 1 1 1 -2
1 1 1 1 1 -2
1 1 1 1 1 -2
其实是同一种, 都是 1 1 -2
在枚举完第1个i的值=1的时候, 走到下一个位置, 发现nums[i]仍然是1, (其实这种情况在上一个位置都枚举完了), 因此下一个nums[i] = 1的情况都要略过, 因此i必须往后, 走到非1的位置. 即: 下一个nums[i]与上一个nums[i]相同的话, 就跳过
联动题
- 最接近的三数之和
四数之和
code
class Solution {
public:
vector<vector<int>> threeSum(vector<int>& nums) {
vector<vector<int>> res;
sort(nums.begin(), nums.end());
for (int i = 0; i < nums.size(); i ++ ){
if (i && nums[i] == nums[i - 1]) continue;
for (int j = i + 1, k = nums.size() - 1; j < k; j ++ ){
if (j > i + 1 && nums[j] == nums[j - 1]) continue;
while (j < k - 1 && nums[i] + nums[j] + nums[k - 1] >= 0) k --;
// k是最小的数 使得>= 0, 成立, 才保证单调
// 如果下一个位置仍然可以保证>= 0, 那么k往前移动一格
if (nums[i] + nums[j] + nums[k] == 0) res.push_back({nums[i], nums[j], nums[k]});
}
}
return res;
}
};
16. 最接近的三数之和
分析
最接近有两重含义, 左边最接近, 和右边最接近, 两种情况都要考虑到
保证答案只有1个, 可以不用考虑判重问题
最坏情况下, 三重循环, O(n^3)
考虑优化:
先排序, 双指针, 因为有3个变量, 不好用双指针, 所以先枚举一个变量, 枚举i
, nums[i]
固定了,
对于j, k, 再枚举j, 对于每一个j找到一个最小的k使得nums[i] + nums[j] + nums[k] >= target
这样就求出了>= target
的最小值
另外的情况, 那么nums[i] + nums[j] + nums[k - 1] < target
是必然的, 也是最接近的
用上题的算法, 求出来k, 然后k再 -1, 就是左边的最接近的情况, 取min
code
因为此题计算过程要记录差值的最小值, 和总和, 所以用pair来存, 1.存差值, 2.存总和
class Solution {
public:
int threeSumClosest(vector<int>& nums, int target) {
pair<int, int> res(INT_MAX, INT_MAX);// 1.保存差值, 用于计算的时候取最小 2.保存总和, 来输出答案
sort(nums.begin(), nums.end());
for (int i = 0; i < nums.size(); i ++ ){
for (int j = i + 1, k = nums.size() - 1; j < k; j ++ ){
while (j < k - 1 && nums[i] + nums[j] + nums[k - 1] >= target) k --;
int s = nums[i] + nums[j] + nums[k];
res = min(res, make_pair(abs(s - target), s)); // s不一定>= target, 因为可能给的数据就是<= target的
if (j < k - 1){ // 一定要写, 否则j循环会和k - 1重合
int s = nums[i] + nums[j] + nums[k - 1];
res = min(res, make_pair(target - s, s));
}
}
}
return res.second;
}
};
17. 电话号码的字母组合
分析
爆搜顺序, 考虑当前位置填哪些字母, 需要path
, 当前位置u
, 两个参数
递归树
时间复杂度: 每个字母最多4种选择, n个字母, 4^n * push_back需要O(n)复杂度, O(4^n * n)
code
class Solution {
public:
string strs[10] = {
"",
"", "abc", "def",
"ghi", "jkl", "mno",
"pqrs", "tuv", "wxyz",
};
vector<string> res;
vector<string> letterCombinations(string digits) {
if (digits.empty()) return res;
dfs(digits, 0, "");
return res;
}
void dfs(string& digits, int u, string path){
if (u == digits.size()){
res.push_back(path);
return;
}else {
for (auto& c : strs[digits[u] - '0'])
dfs(digits, u + 1, path + c);
}
}
};
18. 四数之和
分析
思路同15, 16题, 先枚举两个变量, 后两个变量用双指针优化掉, 变成O(n)
去重的方法和前面一样, 如果当前数和前面一样, 就跳过
推广:
如果求5数之和的话, 先枚举其中3个数
如果求n个数之和的话, 那么dp(背包问题)
code
class Solution {
public:
vector<vector<int>> fourSum(vector<int>& nums, int target) {
vector<vector<int>> res;
sort(nums.begin(), nums.end());
for (int i = 0; i < nums.size(); i ++ ){
if (i && nums[i] == nums[i - 1]) continue;
for (int j = i + 1; j < nums.size(); j ++ ){
if (j > i + 1 && nums[j] == nums[j - 1]) continue;
for (int k = j + 1, u = nums.size() - 1; k < u; k ++ ){
if (k > j + 1 && nums[k] == nums[k - 1]) continue;
while (u - 1 > k && nums[i] + nums[j] + nums[k] + nums[u - 1] >= target) u --;
if (nums[i] + nums[j] + nums[k] + nums[u] == target) res.push_back({nums[i], nums[j], nums[k], nums[u]});
}
}
}
return res;
}
};
19. 删除链表的倒数第N个节点
分析
删的话, 可能会删掉头节点, 因此需要加1个虚拟头节点, 虚拟头节点一定不会被删
所以这题要删除倒数第n个点, 核心是找到倒数第n个点的前一个点, 然后指针改一下
怎么找倒数第k个点, 先求下链表的总长度n, 要找的倒数第k个点的前一个点, 也就是倒数第k+1个点
然后从头往后遍历, 遍历到倒数第 k + 1个点
跳1步 能跳到第2个点
倒数第1个点, 就是当前的第n - 1 + 1个点
倒数第k + 1个点, 就是当前的 n - (k + 1) + 1 = n - k个点,
因此需要跳n - k - 1步
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* removeNthFromEnd(ListNode* head, int k) {
auto dummy = new ListNode(-1);
dummy->next = head;
int n = 0;
for (auto p = dummy; p; p = p->next) n ++;
auto p = dummy;
for (int i = 0; i < n - k - 1; i ++ ) p = p->next;
p->next = p->next->next;
return dummy->next;
}
};
code(一次扫描)
先first
指针(图中红色的)移动到k + 1的位置, 即:先循环k + 1次. 然后first
指针只有(n - k - 1)步可以走了, 此时再让first
和second
指针一起走
那么结束后, 绿色指针就会走到n - k - 1的位置, 也就是倒数第k个点的前一个点
然后如上做法, 调整指针位置即可
/**
* 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* removeNthFromEnd(ListNode* head, int k) {
ListNode* dummy = new ListNode(-1);
dummy-> next = head;
ListNode* first = dummy;
ListNode* second = dummy;
for (int i = 0; i <= k; i ++ ) first = first->next;
while (first){
first = first->next;
second = second->next;
}
second->next = second->next->next;
return dummy->next;
}
};
20. 有效的括号
分析
把左括号看成妹子, 如果是左括号, push到栈里, 然后往后找, 然后发现右括号(男生), 男生会找最近的左括号且能匹配的, 如果能匹配的话, 就弹出栈顶的左括号(妹子), 并且继续往后走;不匹配的话, 返回false
最后如果栈里有元素, 表示不合法, 栈为空则合法
code
class Solution {
public:
bool isValid(string s) {
stack<char> stk;
for (int i = 0; i < s.size(); i ++ ){
if (s[i] == '(' || s[i] == '[' || s[i] == '{'){
stk.push(s[i]);
}else if (s[i] == ')'){
if (stk.empty() || stk.top() != '(') return false; // 如果当前栈为空, 没有妹子可以领取了, 但当前来了一个男生, 或者 栈顶女生和当前男生匹配不上
stk.pop();
}else if (s[i] == ']'){
if (stk.empty() || stk.top() != '[') return false;
stk.pop();
}else {
if (stk.empty() || stk.top() != '{') return false;
stk.pop();
}
}
return stk.empty();
}
};
code(简便写法)
可以观察ASCII码发现左括号和右括号差值<= 2, 所以在判断匹配的时候, 直接看差值就行了
class Solution {
public:
bool isValid(string s) {
stack<char> stk;
for (int i = 0; i < s.size(); i ++ ){
if (s[i] == '(' || s[i] == '[' || s[i] == '{'){
stk.push(s[i]);
}else {
if (stk.size() && abs(stk.top() - s[i]) <= 2) stk.pop();
else return false;
}
}
return stk.empty();
}
};