Leetcode 大部分是medium难度不怎么按顺序题解(上)
被迫重操旧业(?)
再不刷题面试就真要翻车了。。。。
好在medium题难度还比较水,幸亏它不考什么神DP或数据结构或blabla不然我还面个锤子(x)
但是现场写代码还不准出错ATP顶8住啊所以还是练练手感叭。。。。
就,按顺序随便做几个。点中等难度然后按题号排序这样。
UPD:2020.3.24 改了改标题,为了让其更契合这篇题解原本的内容(x)
原标题:LeetCode medium难度顺序题解
UPD:2020.4.11 忘了说了 我
终于面完那一拨了啊啊啊啊啊嗷嗷嗷哦啊啊哦哦啊
每次都好紧张啊有没有 而且准备得越多就越紧张(因为准备得越多就发现自己不会的越多,我fo了)
(其实也就面了两家)
不管了爱咋咋8
UPD:2021.1.19
万万没想到我这破题解竟然更新了一年
总觉得这篇文档里内容一多博客园编辑起来就变得特别特别卡。所以后面的题解就另开一个文档了。
2. 两数相加
高精度但是用单向链表。
一开始ATP写的很麻烦,基本思路是先把两个数字重叠的部分相加,然后再单独处理较长的数字多出来的那部分,然后再处理进位这样。一共三个while循环。
但是后来发现好像直接改一下判断条件,如果其中一个数字链表到头了就不要管它,这样就只用一个while就能解决。
(果然年纪大了哭哭)
class Solution {
public:
ListNode* addTwoNumbers(ListNode* l1, ListNode* l2) {
ListNode *Answer, *now, *newnode;
int tmp, carrybit = 0;
Answer = new ListNode(0);
now = Answer;
while (l1 != NULL || l2 != NULL){
tmp = carrybit;
if (l1 != NULL) tmp += l1->val;
if (l2 != NULL) tmp += l2->val;
carrybit = tmp / 10; tmp = tmp % 10;
now->val = tmp;
if (l1 != NULL) l1 = l1->next;
if (l2 != NULL) l2 = l2->next;
if (l1 != NULL || l2 != NULL || carrybit != 0) {
newnode = new ListNode(0);
now->next = newnode; now = newnode;
}
}
if (carrybit != 0) {
now->val = carrybit;
}
return Answer;
}
};
3.无重复字符的最长子串
\(O(n^2)\)的很好想嘛就直接暴力枚举了。
但是其实显然应该还有更优的做法。。。ATP一开始想对每个字符处理它能延伸的最长区间,但是这样没法很好地处理包含的问题。。。
其实two pointers扫一遍就可以了。每次加入一个新字符的时候移动左端点即可。
class Solution {
public:
int lengthOfLongestSubstring(string s) {
int Answer = 0, len = s.size(), ext[200], L = 0;
memset(ext, -1, sizeof(ext));
for (int i = 0; i < len; i ++) {
L = max(L, ext[s[i]] + 1);
ext[s[i]] = i;
Answer = max(Answer, i - L + 1);
}
return Answer;
}
};
5. 最长回文子串
manacher板子题。。。(但是我忘了manacher怎么写所以抄的zyf2000的板子1551)
细节问题就是两个字符中间要加分隔符来处理回文中心不在字符上的情况,然后最开头的位置要放一个特殊字符这样就可以避免while循环的时候越界。
之前都是用&#@这样的字符来着但是考虑到LeetCode的尿性,它没说全是小写字母还是不用这些字符了。。于是ATP就把它很麻烦地倒腾到数组里用数字来做了。
最后那个输出结果按理说是可以算一下然后直接用string的substr函数搞的,但是ATP懒得动脑子所以就使用脑死亡做法——一个一个把字符加进去。。。
边界卡了半天。我好菜(
class Solution {
public:
string longestPalindrome(string s) {
int len = s.size(), *p, *arr, id = 1, mx = 1;
string Answer = "";
arr = new int[2 * len + 10];
p = new int[2 * len + 10];
arr[0] = -2;
for (int i = 0; i < len; i ++) {
arr[i * 2 + 1] = s[i];
arr[i * 2 + 2] = -1;
}
for (int i = 1; i < 2 * len; i ++) {
if (mx > i) p[i] = min(p[2 * id - i], mx - i);
else p[i] = 1;
while (arr[i - p[i]] == arr[i + p[i]])
++ p[i];
if (i + p[i] > mx) {
mx = i + p[i]; id = i;
}
}
mx = 0; id = 1;
for (int i = 1; i < 2 * len; i ++) {
int val = p[i];
if (arr[i - p[i]] == -1) val = p[i] - 1;
if (val > mx){mx = val; id = i;}
}
for (int i = id - mx + 1; i <= id + mx - 1; i ++)
if (arr[i] > 0) Answer.push_back((char)(arr[i]));
return Answer;
}
};
6. Z字形变换
我的做法是先算出要用几行几列的数组,然后再一个个填字(究极脑死做法
但是实际上应该可以直接构造答案字符串。只要知道现在正在读第几个Z字形的第几个字符,就可以算出它对应原字符串的哪个位置。这样应该会更快一点。。。
class Solution {
public:
string convert(string s, int numRows) {
int cycle = numRows * 2 - 2, len = s.size();
int numCols, p1, p2;
bool flag = false;
string Answer = "";
if (numRows == 1) return s;
numCols = len / cycle;
numCols *= numRows - 1;
if (len % cycle > 0) numCols += 1;
if (len % cycle > numRows) numCols += len % cycle - numRows;
char arr[numRows][numCols];
p1 = p2 = 0;
for (int i = 0; i < numRows; i ++)
for (int j = 0; j < numCols; j ++)
arr[i][j] = 0;
for (int i = 0; i < len; i ++) {
arr[p2][p1] = s[i];
if (flag == false) {
if (p2 + 1 < numRows) p2 ++;
else {p1 ++; p2 --; flag = true;}
}else {
if (p2 - 1 >= 0) {p1 ++; p2 --;}
else {p2 ++; flag = false;}
}
}
for (int i = 0; i < numRows; i ++)
for (int j = 0; j < numCols; j ++)
if (arr[i][j] > 0) Answer.push_back(arr[i][j]);
return Answer;
}
};
8. 字符串转换整数(atoi)
注意判一下开头的正负号就行了。。越界问题ATP直接用long long搞的(又是脑死做法)
class Solution {
public:
int myAtoi(string str) {
int ptr = 0;
long long Answer = 0, sign = 1;
const long long MAX = 2147483648LL;
while (str[ptr] == ' ') ptr ++;
if (str[ptr] != '+' && str[ptr] != '-' && (str[ptr] < '0' || str[ptr] > '9'))
return 0;
if (str[ptr] == '+') {sign = 1; ptr ++;}
else if (str[ptr] == '-') {sign = -1; ptr ++;}
while (str[ptr] <= '9' && str[ptr] >= '0') {
Answer = Answer * 10 + str[ptr] - '0';
ptr ++;
if (Answer > MAX) Answer = MAX;
}
Answer = Answer * sign;
if (Answer == MAX) Answer = MAX - 1;
return (int)Answer;
}
};
11. 盛水最多的容器
two pointers扫一遍即可。第一个指针在开头,第二个指针在结尾。因为如果缩短x轴长度能增大总容量就一定得排除短板,所以两个指针指向的位置哪个短就先移动哪个。
但是严格证明我不会。。。看了一下别人的题解,大致思路是说排除短板以后不会被扫到的那些解一定不可能是最优解。
class Solution {
public:
#include <algorithm>
int maxArea(vector<int>& height) {
int p1 = 0, p2 = height.size() - 1;
int Answer = 0;
while (p1 != p2) {
Answer = max(Answer, (p2 - p1) * min(height[p2], height[p1]));
if (height[p2] < height[p1]) p2 --;
else p1 ++;
}
return Answer;
}
};
12. 整数转罗马数字
这题的关键是总结出整数转罗马数字的规则(x
LeetCode上有人说实际上目标是使用的字母最少,听起来好像有道理。
规则是能用大的就用大的,包括40、90这些特殊数字。但是10,100这些数字可以连续使用多次,40,90这种的就不可以。
class Solution {
public:
string intToRoman(int num) {
string Answer = "";
int ww[13] = {1, 4, 5, 9, 10, 40, 50, 90, 100, 400, 500, 900, 1000};
string st[13] = {"I", "IV", "V", "IX", "X", "XL", "L", "XC", "C", "CD", "D", "CM", "M"};
for (int i = 12; i >= 0; i --) {
if (i % 2 == 1){
if (num - ww[i] >= 0) {
num -= ww[i]; Answer += st[i];
}
}else {
while (num - ww[i] >= 0) {
num -= ww[i]; Answer += st[i];
}
}
}
return Answer;
}
};
15.三数之和
\(O(n^2)\)的思路很显然,就是枚举其中一个数然后two pointers找另外两个数就可以了。
细节问题就是怎么避免重复的三元组,以及怎么让它能够选择相同的数字(比如序列里有3个0,那么[0,0,0]也是一个解)
第一个问题我的方法是固定枚举较小的数字,然后two pointers的时候一定在它后面做。并且一定让第一个指针指向较小的数字,第二个指针指向较大的数字。
对于第二个问题,我的方法是在枚举的时候一定是枚举相同数字中的第一个数,然后处理后面的数,这样如果有重复出现的数字,就可能在two pointers的时候被包括进去。
还有一个细节就是在双指针枚举的时候,第二个指针碰到第一个符合条件的数就要及时停下,不要再枚举后面相等的数字了(具体表现在while循环的控制条件)。
这样就能够允许出现p1指在一串相等数字的开头,而p2指在一串相等数字的结尾这样的情况。就能满足题目要求了。
class Solution {
private:
void GetAns(vector< vector<int> > &Answer, int aim, int L, int n, vector<int> &nums) {
int p1 = L, p2 = n - 1;
while (p1 < p2) {
while (p1 < p2 && nums[p1] + nums[p2] > aim) p2 --;
if (p1 == p2) break;
if (nums[p1] + nums[p2] == aim) {
vector<int> tmp;
tmp.clear();
tmp.push_back(-aim);
tmp.push_back(nums[p1]);
tmp.push_back(nums[p2]);
Answer.push_back(tmp);
}
while (p1 < p2 && nums[p1 + 1] == nums[p1]) p1 ++;
p1 ++;
}
}
public:
#include <algorithm>
vector<vector<int>> threeSum(vector<int>& nums) {
int n = nums.size();
sort(nums.begin(), nums.end());
vector< vector<int> > Answer;
Answer.clear();
for (int i = n - 1; i >= 0; i --) {
while (i - 1 >= 0 && nums[i - 1] == nums[i])
--i;
GetAns(Answer, -nums[i], i + 1, n, nums);
}
return Answer;
}
};
16. 最接近的三数之和
跟上一题大同小异就是把双指针那个地方判断条件改一下就行了。。。因为是找最接近的而不是恰好相等的,所以恰好小于它的和恰好大于它的都要判断一下。
class Solution {
#define abs(x) ((x)>0?(x):-(x))
private:
void checkans(int &ans, int tmp, int target) {
if (abs(tmp - target) < abs(ans - target))
ans = tmp;
}
int GetAns(vector<int> &nums, int target, int L, int n) {
int ans, p1 = L, p2 = n - 1;
ans = nums[p1] + nums[p2];
while (p1 < p2) {
while (p1 < p2 && nums[p1] + nums[p2] > target) p2 --;
if (p1 != p2)
checkans(ans, nums[p1] + nums[p2], target);
if (p2 + 1 < n)
checkans(ans, nums[p1] + nums[p2 + 1], target);
while (p1 < p2 && nums[p1 + 1] == nums[p1]) p1 ++;
p1 ++;
}
return ans;
}
public:
int threeSumClosest(vector<int>& nums, int target) {
int n = nums.size(), Answer;
sort(nums.begin(), nums.end());
Answer = nums[0] + nums[1] + nums[2];
for (int i = 0; i < n - 2; i ++) {
int tmp = GetAns(nums, target - nums[i], i + 1, n);
tmp += nums[i];
checkans(Answer, tmp, target);
while (i + 1 < n && nums[i + 1] == nums[i])
i ++;
}
return Answer;
}
};
17. 电话号码的字母组合
我的方法是无脑dfs。。。
class Solution {
private:
int c[10] = {0, 0, 3, 6, 9, 12, 15, 19, 22, 26};
void dfs(int u, string now, int len, string digits, vector<string> &Answer) {
if (u == len) {
if (now != "")
Answer.push_back(now);
return;
}
int idx = digits[u] - '0';
for (int i = c[idx - 1]; i < c[idx]; i ++) {
string tmp = now;
tmp.push_back('a' + i);
dfs(u + 1, tmp, len, digits, Answer);
}
}
public:
vector<string> letterCombinations(string digits) {
int len = digits.size();
vector<string> Answer;
Answer.clear();
dfs(0, "", len, digits, Answer);
return Answer;
}
};
18.四数之和
它咋这么喜欢N数之和。。。不做了qwq
19.删除链表的倒数第N个节点
我搞了个队列保持n个元素(
实际上和开个数组把节点全存下来没有本质区别(!
之所以队列开了n+1是因为删除节点还需要知道它前面的那个节点是什么
不过好像有种更好的做法,开个计数器,再开两个变量,只记录倒数第n个和倒数第n+1个就行了。这样空间是O(1)的。
/**
* Definition for singly-linked list.
* struct ListNode {
* int val;
* ListNode *next;
* ListNode(int x) : val(x), next(NULL) {}
* };
*/
class Solution {
public:
ListNode* removeNthFromEnd(ListNode* head, int n) {
ListNode *ptr = head, *last;
queue<ListNode*> q;
while (!q.empty()) q.pop();
while (ptr != NULL) {
if (q.size() == n + 1) q.pop();
q.push(ptr);
ptr = ptr->next;
}
if (q.size() < n + 1) {
head = head->next;
return head;
}
last = q.front(); q.pop();
ptr = q.front();
last->next = ptr->next;
return head;
}
};
/**
* Definition for singly-linked list.
* struct ListNode {
* int val;
* ListNode *next;
* ListNode(int x) : val(x), next(NULL) {}
* };
*/
class Solution {
public:
ListNode* removeNthFromEnd(ListNode* head, int n) {
ListNode *ptr, *last, *now = head;
int cnt = 0;
ptr = last = NULL;
while (now != NULL) {
if (ptr != NULL) {
last = ptr; ptr = ptr->next;
}
cnt ++;
if (cnt == n) ptr = head;
now = now->next;
}
if (cnt == n)
return head->next;
last->next = ptr->next;
return head;
}
};
22.括号生成
一开始觉得它是不是可以用卡特兰数或者树结构之类的来搞一搞。。。但是后来发现直接爆搜的复杂度已经差不多可以了。。。
就 如果左括号比右括号多,就可以填一个右括号;如果左括号还没到n个,就可以填一个左括号。
这样的话并不存在搜出重复解的情况,所以渐进复杂度应该已经是最优了。
class Solution {
private:
void dfs(int l, int r, int n, string now, vector<string> &Answer) {
if (l == n && l == r) {
Answer.push_back(now);
return;
}
if (l > r)
dfs(l, r + 1, n, now + ")", Answer);
if (l < n)
dfs(l + 1, r, n, now + "(", Answer);
}
public:
vector<string> generateParenthesis(int n) {
vector<string> Answer;
Answer.clear();
dfs(0, 0, n, "", Answer);
return Answer;
}
};
24. 两两交换链表中的节点
画画图就明白了(
注意这题的数据有可能给你一个空链表or只有一个节点的链表
然后注意奇数个节点的情况也需要能正确处理
/**
* Definition for singly-linked list.
* struct ListNode {
* int val;
* ListNode *next;
* ListNode(int x) : val(x), next(NULL) {}
* };
*/
class Solution {
public:
ListNode* swapPairs(ListNode* head) {
ListNode *lst, *A, *B, *nxt;
if (head == NULL || head->next == NULL) return head;
lst = NULL; A = head; B = head->next; nxt = B->next;
if (lst == NULL && nxt == NULL) {
B->next = A; A->next = NULL;
return B;
}
while (A != NULL && B != NULL) {
if (lst == NULL) {
A->next = B->next; B->next = A;
head = B;
}else {
A->next = B->next; B->next = A;
lst->next = B;
}
lst = A; A = A->next;
if (A != NULL) B = A->next;
}
return head;
}
};
29. 两数相除
好麻烦这玩意儿。。。
题目的意思是只让用int
一开始想用long long脑死亡做法然后看了一眼题还是算了。。。
以及还有一个问题是 谁说的负数不能左移。。。。
因为它不让我负数左移所以我偏偏就要用unsigned int强转一下
真要较真的话自己写个左移函数倒也行(
思路是用位运算,每次减掉divisor*2^n。再用一个单独的变量来统计结果。整个流程是用负数做的,符号最后再统计。因为负数的域比较大。。。虽然也不知道有没有用。
然后里面那些乱七八糟的特判全都是关于INT_MIN的。
因为如果divisor是1,dividend是INT_MIN的话,统计结果用的变量t也被迫左移了31位变成了INT_MIN
然后t就永远没有办法通过右移变成0了,就会无限循环
所以就直接把这种情况判掉了。。。别的情况都没啥毛病
class Solution {
public:
int divide(int dividend, int divisor) {
int s1 = 0, s2 = 0, ans = 0, t = 1;
if (dividend == -2147483647 - 1 && divisor == -1) {
return 2147483647;
}
if (dividend > 0) {s1 = 1; dividend = -dividend;}
if (divisor > 0) {s2 = 1; divisor = -divisor;}
while ((int)((unsigned int)divisor << 1) < 0) {
divisor = (int)((unsigned int)divisor << 1);
if (divisor < dividend) {
divisor >>= 1; break;
}
t = t << 1;
}
while (t != 0) {
if (dividend - divisor <= 0) {
dividend -= divisor;
ans += t;
if (t == -2147483647 - 1)
s1 ^= 1;
}
if (t == -2147483647 - 1)
t = 1073741824;
else t >>= 1;
divisor >>= 1;
}
if ((s1 ^ s2) == 1) ans = -ans;
return ans;
}
};
31. 下一个排列
找到排列变化的规律然后直接模拟就行。
我是先找最长不上升后缀(举个栗子,4 2 5 3 1 的最长不上升后缀就是 5 3 1,1 5 5 1 的最长不上升后缀就是 5 5 1)。
如果这个不上升后缀已经覆盖了整个字符串,即整个字符串都是不上升的,说明现在已经是最大排列了,直接排个序输出就行。
否则这个后缀前面紧挨着它的那个字符肯定是小于它的。把这个字符设为c。
这也说明后缀里面一定存在至少一个字符大于c。把大于c的最小字符找出来,和c交换。然后再把修改后的后缀排个序就行了。
道理也好懂。首先要把字典序恰好扩大一位,那肯定是改动的范围越小越好,并且肯定是从后往前改。
所以就要找最小能变动什么范围内的字符。
刚才找到最长的不上升后缀,这个后缀本身已经是字典序最大的排列了,在这个后缀内部改动是没用的。
所以要改这个后缀前面紧挨着它的那个字符c。
而要让改动后字典序变化尽量小,那就要找大于c的最小字符。
一个细节问题是,一定要找最长不上升后缀而不是最长下降后缀。
原因考虑下 1 5 5 1 这个例子就明白了。
class Solution {
public:
void nextPermutation(vector<int>& nums) {
int n = nums.size();
int decpos = n - 1, minpos = n;
for (int i = n - 1; i > 0; i --)
if (nums[i - 1] >= nums[i])
decpos = i - 1;
else break;
if (decpos == 0) {
sort(nums.begin(), nums.end());
return;
}
for (int i = decpos; i < n; i ++)
if (nums[i] > nums[decpos - 1]) {
if (minpos == n) minpos = i;
else if (nums[i] < nums[minpos]) minpos = i;
}
swap(nums[decpos - 1], nums[minpos]);
sort(nums.begin() + decpos, nums.end());
return;
}
};
33. 搜索旋转排序数组
这题ATP以前面试的时候人家问过诶
但是当时ATP没想到还有log的做法,只说了线性的
实际上两次二分就能解决。一次二分找到旋转的位置也就是数组里最小的元素,第二次二分就直接做了。
第一次二分实际上就是找小于nums[0]的最靠左的元素。如果找不到,那说明最小的元素就是nums[0]。
class Solution {
private:
int FindBegin(vector<int> &nums) {
int n = nums.size();
int tar = nums[0], l = 1, r = n - 1, mid;
if (l > r) return 0;
while (l != r) {
mid = (l + r) >> 1;
if (nums[mid] > nums[0]) l = mid + 1;
else r = mid;
}
if (nums[l] < nums[0]) return l;
else return 0;
}
int FindNumber(vector<int> &nums, int begin, int tar) {
int n = nums.size();
int l = 0, r = n - 1, mid, pos;
while (l != r) {
mid = (l + r) >> 1;
pos = (begin + mid) % n;
if (nums[pos] < tar) l = mid + 1;
else r = mid;
}
pos = (l + begin) % n;
if (nums[pos] == tar) return pos;
else return -1;
}
public:
int search(vector<int>& nums, int target) {
if (nums.size() == 0) return -1;
int pos = FindBegin(nums);
int Answer = FindNumber(nums, pos, target);
return Answer;
}
};
34. 在排序数组中查找元素的第一个和最后一个位置
两次二分即可。我也不知道我写的为啥这么慢
class Solution {
private:
int searchLower(vector<int> &nums, int tar) {
int n = nums.size();
int l = 0, r = n - 1, mid;
if (n == 0) return -1;
while (l != r) {
mid = (l + r) >> 1;
if (nums[mid] < tar) l = mid + 1;
else r = mid;
}
if (nums[l] == tar) return l;
else return -1;
}
int searchUpper(vector<int> &nums, int tar) {
int n = nums.size();
int l = 0, r = n - 1, mid, ans = -1;
if (n == 0) return -1;
while (l <= r) {
mid = (l + r) >> 1;
if (nums[mid] == tar)
ans = max(ans, mid);
if (nums[mid] <= tar) l = mid + 1;
else r = mid - 1;
}
return ans;
}
public:
vector<int> searchRange(vector<int>& nums, int target) {
vector<int> Answer;
Answer.clear();
Answer.push_back(searchLower(nums, target));
Answer.push_back(searchUpper(nums, target));
return Answer;
}
};
36. 有效的数独
究极脑死做法它又来了
行和列好说,九宫格的话。。。我是模拟了两个变量让它们一格格跳的。。。
举个栗子,先让列变量一格格跳,跳三步以后行变量换行。如果行变量也跳了三步就说明走完了一个九宫格,就初始化到下一个九宫格。
不过leetcode这个系统不让函数强制中间return,所以只好加了一个flag变量。一开始是想判到一个错就return false来着。。
class Solution {
public:
bool isValidSudoku(vector<vector<char>> &board) {
int N = 9;
bool flag = true;
for (int i = 0; i < N ;i ++) {
int state = 0;
for (int j = 0; j < N; j ++)
if (board[i][j] != '.') {
int x = board[i][j] - '0';
if ((state >> x) & 1)
flag = false;
state |= (1 << x);
}
}
for (int i = 0; i < N; i ++) {
int state = 0;
for (int j = 0; j < N; j ++)
if (board[j][i] != '.') {
int x = board[j][i] - '0';
if ((state >> x) & 1)
flag = false;
state |= (1 << x);
}
}
for (int i = 0, j = 0, c1 = 0; ; ) {
int state = 0, c2 = 0;
for ( ; ; ) {
if (board[i][j] != '.') {
int x = board[i][j] - '0';
if ((state >> x) & 1)
flag = false;
state |= (1 << x);
}
c2 ++; j ++;
if (c2 % 3 == 0) {
j -= 3; i ++;
}
if (c2 == 9) break;
}
i -= 3; j += 3; c1 ++;
if (c1 % 3 == 0) {
i += 3; j -= 9;
}
if (c1 == 9) break;
}
return flag;
}
};
39. 组合总和
一开始在思考一些比如双指针或者二分或者之类之类的东西,但是后来发现它要所有方案并且选的数字个数还不限。。。那基本上就爆搜会经济一点了(
随便加了几个剪枝,比如提前判定如果没有可行解就退出之类的
class Solution {
private:
void search(int now, int lim, vector<int> &candidates, int tar, vector< vector<int> > &Answer, vector<int> &tmp) {
if (tar == 0) {
Answer.push_back(tmp);
return;
}
if (now == lim) return;
int num = candidates[now], _t = tar;
if (tar < num) return;
if (now == lim - 1 && tar % num != 0)
return;
search(now + 1, lim, candidates, tar, Answer, tmp);
vector<int> rec = tmp;
for (int i = 1; i <= _t / num; i ++) {
tmp.push_back(num); tar -= num;
search(now + 1, lim, candidates, tar, Answer, tmp);
}
tmp = rec;
}
public:
vector<vector<int>> combinationSum(vector<int>& candidates, int target) {
int N = candidates.size();
vector< vector<int> > Answer;
vector<int> tmp;
Answer.clear(); tmp.clear();
sort(candidates.begin(), candidates.end());
search(0, N, candidates, target, Answer, tmp);
return Answer;
}
};
43. 字符串相乘
高精度乘高精度。不用压位。
class Solution {
public:
string multiply(string num1, string num2) {
int a[120], b[120], c[300], l1, l2, len, carry;
memset(c, 0, sizeof(c));
l1 = num1.size(); l2 = num2.size();
for (int i = 0; i < l1; i ++)
a[i] = num1[l1 - i - 1] - '0';
for (int i = 0; i < l2; i ++)
b[i] = num2[l2 - i - 1] - '0';
len = l1 + l2 - 1;
for (int i = 0; i < l1; i ++)
for (int j = 0; j < l2; j ++)
c[i + j] += a[i] * b[j];
carry = 0;
for (int i = 0; i < len; i ++) {
c[i] += carry;
carry = c[i] / 10;
c[i] %= 10;
}
while (carry != 0) {
c[len] = carry % 10;
carry /= 10;
len ++;
}
while (len > 1 && c[len - 1] == 0) len --;
string Answer = "";
for (int i = len - 1; i >= 0; i --)
Answer.push_back(c[i] + '0');
return Answer;
}
};
46. 全排列
next_permutation无脑过。。。但是注意数据可能不是初始有序的。
class Solution {
public:
vector<vector<int>> permute(vector<int>& nums) {
vector< vector<int> > Answer;
Answer.clear();
sort(nums.begin(), nums.end());
Answer.push_back(nums);
while (next_permutation(nums.begin(), nums.end()))
Answer.push_back(nums);
return Answer;
}
};
47. 全排列II
跟上一个题一模一样的代码就能过。。。
但是要非得较真的话,我记得前面有一个题让你在O(n)时间内生成下一个排列。而且那个题是可以处理重复数字的。把那个函数调用n次也行8(
48. 旋转图像
先按照主对角线转置,再水平翻转即可
class Solution {
public:
void rotate(vector<vector<int>>& matrix) {
int n = matrix.size();
for (int i = 0; i < n; i ++)
for (int j = i; j < n; j ++)
swap(matrix[i][j], matrix[j][i]);
for (int i = 0; i < n; i ++)
for (int j = 0; j <= (n - 1)/2; j ++)
swap(matrix[i][j], matrix[i][n - j - 1]);
}
};
49. 字母异位词分组
两个字符串如果有相同的字母,那么它们两个内部排序后得到的字符串肯定是相同的。比如nat和tan排序以后都得到ant。
所以就先把字符串内部排序,再按照内部排序后的结果为关键字排序,同一组内的字符串就被放到一起了。
具体实现用了pair,第一关键字是内部排序后的字符串,第二关键字是原字符串。
class Solution {
public:
vector<vector<string>> groupAnagrams(vector<string>& strs) {
vector<string> tmp = strs;
vector< vector<string> > Answer;
int n = strs.size();
Answer.clear();
for (int i = 0; i < n; i ++)
sort(tmp[i].begin(), tmp[i].end());
vector< pair<string, string> > a;
a.clear();
for (int i = 0; i < n; i ++)
a.push_back(make_pair(tmp[i], strs[i]));
sort(a.begin(), a.end());
for (int i = 0; i < n; i ++) {
tmp.clear();
tmp.push_back(a[i].second);
while (i < n - 1 && a[i].first == a[i + 1].first) {
i ++; tmp.push_back(a[i].second);
}
Answer.push_back(tmp);
}
return Answer;
}
};
50. Pow(x, n)
快速幂。注意判一下n是负数的情况,尤其是n为INT_MIN的情况。
class Solution {
public:
double myPow(double x, int n) {
double Answer = 1.0;
bool sign = false, intmin = false;
if (n == -2147483647 - 1) {
intmin = true; n = n / 2;
}
if (n < 0) {
sign = true; n = -n;
}
for (; n != 0; n >>= 1) {
if (n & 1) Answer *= x;
x *= x;
}
if (intmin == true) Answer = Answer * x;
if (sign == true)
Answer = 1 / Answer;
return Answer;
}
};
54. 螺旋矩阵
先往右走再往下走再往左走再往右走。
注意有可能给一个完全空的vector,要特判。
class Solution {
public:
vector<int> spiralOrder(vector<vector<int>>& matrix) {
int d[4][2] = {{0, 1}, {1, 0}, {0, -1}, {-1, 0}};
int p1 = 0, p2 = 0, M, N;
vector<int> Answer;
Answer.clear();
M = matrix.size();
if (M == 0) return Answer;
N = matrix[0].size();
while (M > 0 && N > 0) {
for (int i = 0; i < N; i ++)
Answer.push_back(matrix[p1][p2++]);
p2 --; p1 ++; M --; N --;
for (int i = 0; i < M; i ++)
Answer.push_back(matrix[p1++][p2]);
p1 --; p2 --;
if (M <= 0 || N <= 0) break;
for (int i = 0; i < N; i ++)
Answer.push_back(matrix[p1][p2--]);
p2 ++; p1 --; M --; N --;
for (int i = 0; i < M; i ++)
Answer.push_back(matrix[p1--][p2]);
p1 ++; p2 ++;
}
return Answer;
}
};
55.跳跃游戏
把每个点能跳的范围看成一个区间。从0号位置开始,如果这些区间能不间断地覆盖到最后一个点,那就说明能跳过去。否则就跳不过去。
维护目前能达到的最远的右端点R,则所有左端点小于R的区间都可以跳。迭代更新R直到枚举完最后一个区间。
class Solution {
public:
bool canJump(vector<int>& nums) {
int n = nums.size(), R = 0;
vector< pair<int, int> > intervals;
for (int i = 0; i < n; i ++)
intervals.push_back(make_pair(i, i + nums[i]));
sort(intervals.begin(), intervals.end());
for (int i = 0; i < n; i ++)
if (intervals[i].first <= R)
R = max(R, intervals[i].second);
if (R >= n - 1) return true;
return false;
}
};
56. 合并区间
跟上一个题合并的原理是差不多的,只不过要维护左右两个端点。每次遇到一个合并不了的区间就把维护的端点L和R当做一个答案放入Answer,然后重置L和R为当前区间的左右端点即可。
class Solution {
public:
vector<vector<int>> merge(vector<vector<int>>& intervals) {
int n = intervals.size(), L, R;
vector< pair<int, int> > re;
vector< vector<int> > Answer;
vector<int> tmp;
re.clear(); Answer.clear();
if (n == 0) return Answer;
for (int i = 0; i < n; i ++)
re.push_back(make_pair(intervals[i][0], intervals[i][1]));
sort(re.begin(), re.end());
L = re[0].first; R = re[0].second;
for (int i = 1; i < n; i ++) {
if (re[i].first <= R)
R = max(R, re[i].second);
else {
tmp.clear();
tmp.push_back(L); tmp.push_back(R);
Answer.push_back(tmp);
L = re[i].first; R = re[i].second;
}
}
tmp.clear(); tmp.push_back(L); tmp.push_back(R);
Answer.push_back(tmp);
return Answer;
}
};
59. 螺旋矩阵II
仍然采用无脑移动指针法
class Solution {
public:
vector<vector<int>> generateMatrix(int n) {
int d[4][2] = {{0, 1}, {1, 0}, {0, -1}, {-1, 0}};
vector< vector<int> > Answer;
Answer.clear();
vector<int> tmp;
for (int i = 0; i < n; i ++) {
tmp.push_back(0);
}
for (int i = 0; i < n; i ++)
Answer.push_back(tmp);
int idx = 0, p1 = 0, p2 = 0, now = 0;
while (now < n * n) {
while (p1 < n && p1 >= 0 && p2 < n && p2 >= 0 && Answer[p1][p2] == 0) {
now ++; Answer[p1][p2] = now;
p1 += d[idx][0]; p2 += d[idx][1];
}
p1 -= d[idx][0]; p2 -= d[idx][1];
idx = (idx + 1) % 4;
p1 += d[idx][0]; p2 += d[idx][1];
}
return Answer;
}
};
60. 第k个排列
无脑next_permutation。因为C++里的string是继承的vector,所以直接像vector那么搞就行了
class Solution {
public:
string getPermutation(int n, int k) {
string perm;
perm.clear();
for (int i = 1; i <= n; i ++)
perm.push_back(i + '0');
for (int i = 1; i < k; i ++)
next_permutation(perm.begin(), perm.end());
return perm;
}
};
61. 旋转链表
直观上来看,旋转右移k次就是把链表分成n-k和k两节,然后交换。
而右移len(链表长度)次和右移0次没有区别,所以把k对n取个模
然后从头往后跳n-k步把链表断成两节,最后把这两节交换一下位置即可。
注意在计算n-k的时候,k有可能是0,n-k就会变成n。这会带来一些细节上的问题。我的方法是把n-k再对n取一次模。
/**
* Definition for singly-linked list.
* struct ListNode {
* int val;
* ListNode *next;
* ListNode(int x) : val(x), next(NULL) {}
* };
*/
class Solution {
public:
ListNode* rotateRight(ListNode* head, int k) {
int n = 1;
ListNode *tail = NULL, *ptr = head, *last = NULL;
if (head == NULL) return head;
for (tail = head; tail -> next != NULL; tail = tail->next, n ++);
k = (n - (k % n)) % n;
for (int i = 0; i < k; i ++) {
last = ptr;
ptr = ptr->next;
}
if (last == NULL) return head;
last->next = NULL; tail->next = head;
return ptr;
}
};
62. 不同路径
简单的DP。从上面的位置和左边的位置转移过来就可以了。
数组开大一圈并且从(1, 1)开始递推可以省去边界的特判,非常好写。
以及这次终于记得new完了要delete了。
class Solution {
public:
int uniquePaths(int m, int n) {
int **f = new int*[m + 1];
int Answer;
for (int i = 0; i <= m; i ++)
f[i] = new int[n + 1];
for (int i = 0; i <= m; i ++)
for (int j = 0; j <= n; j ++)
f[i][j] = 0;
f[1][1] = 1;
for (int i = 1; i <= m; i ++)
for(int j = 1; j <= n; j ++)
if (f[i][j] == 0)
f[i][j] = f[i - 1][j] + f[i][j - 1];
Answer = f[m][n];
for (int i = 0; i <= m; i ++)
delete[] f[i];
delete[] f;
return Answer;
}
};
63. 不同路径
做到这个题才发现一个很重要的问题
因为它要返回的值是int所以就直接用int做了,但是实际上稍微大点儿的数据好像就会爆int。
而leetcode的编译器是只要溢出就报错,因为这个问题卡了好久(
我觉得要么是它的数据有问题(感觉可能性不大?),更可能的是这个地方虽然爆了int但是对最后结果没有影响,因为有障碍的存在,这个爆了的位置不会更新最后答案。
所以就在中间直接强转long long做了。最后反正过了_(:з」∠)_
class Solution {
public:
int uniquePathsWithObstacles(vector<vector<int>>& obstacleGrid) {
int m = obstacleGrid.size();
if (m == 0) return 0;
int n = obstacleGrid[0].size();
int **f = new int*[m + 1];
int Answer;
for (int i = 0; i <= m; i ++) {
f[i] = new int[n + 1];
}
for (int i = 0; i <= m; i ++)
for (int j = 0; j <= n; j ++)
f[i][j] = 0;
if (obstacleGrid[0][0] == 0) f[1][1] = 1;
for (int i = 1; i <= m; i ++)
for (int j = 1; j <= n; j ++)
if (f[i][j] == 0 && obstacleGrid[i - 1][j - 1] == 0){
long long A = f[i - 1][j], B = f[i][j - 1];
A += B;
f[i][j] = (int)A;
}
Answer = f[m][n];
for (int i = 0; i <= m; i ++)
delete[] f[i];
delete[] f;
return Answer;
}
};
64. 最小路径和
简单DP
就 无脑long long
class Solution {
public:
int minPathSum(vector<vector<int>>& grid) {
int m = grid.size();
if (m == 0) return 0;
int n = grid[0].size();
const long long INF = (1LL << 60);
long long **f = new long long*[m + 1];
for (int i = 0; i <= m; i ++)
f[i] = new long long[n + 1];
for (int i = 0; i <= m; i ++)
for (int j = 0; j <= n; j ++)
f[i][j] = INF;
f[1][1] = (int)grid[0][0];
for (int i = 1; i <= m; i ++)
for (int j = 1; j <= n; j ++)
if (f[i][j] == INF)
f[i][j] = min(f[i - 1][j], f[i][j - 1]) + grid[i - 1][j - 1];
int Answer = (int)f[m][n];
for (int i = 0; i <= m; i ++)
delete[] f[i];
delete[] f;
return Answer;
}
};
73.矩阵置零
这题想了半天!
O(mn)的显然,O(m+n)的也好做,就是为行和列单独维护0标记就可以了。
O(1)的。。。一开始想搏一搏(划掉)用long long来压位维护0标记(伪O(1)算法)但是因为数据有规模超过64的所以失败了(
但是实际上可以把0标记维护到矩阵里面。我的做法是用第一行和第一列来维护0标记,然后特判第一行和第一列。用了两个额外的bool变量。
class Solution {
public:
void setZeroes(vector<vector<int>>& matrix) {
int m = matrix.size();
if (m == 0) return;
int n = matrix[0].size();
bool r1, c1;
r1 = c1 = false;
for (int i = 0; i < m; i ++)
if (matrix[i][0] == 0) c1 = true;
for (int i = 0; i < n; i ++)
if (matrix[0][i] == 0) r1 = true;
for (int i = 1; i < m; i ++)
for (int j = 1; j < n; j ++)
if (matrix[i][j] == 0)
matrix[i][0] = matrix[0][j] = 0;
for (int i = 1; i < m; i ++)
if (matrix[i][0] == 0)
for (int j = 1; j < n; j ++)
matrix[i][j] = 0;
for (int j = 1; j < n; j ++)
if (matrix[0][j] == 0)
for (int i = 1; i < m; i ++)
matrix[i][j] = 0;
if (r1 == true)
for (int i = 0; i < n; i ++)
matrix[0][i] = 0;
if (c1 == true)
for (int i = 0; i < m; i ++)
matrix[i][0] = 0;
return;
}
};
74. 搜索二维矩阵
可以发现这个矩阵从左到右一行行读就是一个有序的序列。按照序列的方式二分,然后算出二分到的mid在矩阵中的对应位置就可以了。
class Solution {
public:
bool searchMatrix(vector<vector<int>>& matrix, int target) {
int m = matrix.size();
if (m == 0) return false;
int n = matrix[0].size();
if (n == 0) return false;
int l = 0, r = m * n - 1, mid, x, y;
while (l != r) {
mid = (l + r) >> 1;
x = mid / n; y = mid % n;
if (matrix[x][y] >= target) r = mid;
else l = mid + 1;
}
x = l / n; y = l % n;
if (matrix[x][y] == target) return true;
return false;
}
};
75.颜色分类
第一眼冒泡,第二眼桶排
只用O(1)的空间还要只扫描一遍,那肯定必须只能边扫边排了。
初步的想法是如果遇到一个0就把它交换到前面,那肯定要有一个指针记录当前前缀0的位置。
那么就相当于把序列分成三段来扩展。0段和1段是挨在一起的,2段是从尾部开始扩展的。
0段的结尾和2段的开头分别用一个变量来记,然后保证目前的循环变量i就是1段的结尾。
维护的时候,如果遇到一个0,就把它和当前0段结尾的那个位置交换,这样0段向后扩展了一位,1段也向后扩展了一位。
如果遇到一个1,只需要把1段的指针后移。因为循环语句每次自动i++,所以循环体内什么也不用做
如果遇到一个2,要把它放到结尾,把后面的某个数字交换过来。但是如果交换过来的还是一个2的话相当于什么也没做,所以要找到从后往前第一个不是2的位置,然后交换过来。
当循环变量i和记录2段开头的指针p2碰头的时候说明扫描已经结束了,因为i之前都是0和1,而p2之后都是2。已经排好了。
class Solution {
public:
void sortColors(vector<int>& nums) {
int n = nums.size();
int p0 = 0, p2 = n;
for (int i = 0; i < p2; i ++)
if (nums[i] == 0) {
swap(nums[i], nums[p0]); p0 ++;
}else if (nums[i] == 1) {
continue;
}else {
while (p2 - 1 > i && nums[p2 - 1] == 2)
p2 --;
if (p2 - 1 == i) break;
swap(nums[i], nums[p2 - 1]); p2 --;
if (nums[i] == 0) {
swap(nums[i], nums[p0]); p0 ++;
}
}
return;
}
};
77.组合
直接爆搜,渐进复杂度就已经说得过去了。我的做法复杂度是O(C(n,k)*k)的,其实如果把vector
不过查了两天的错竟然是因为带参忘了传引用。太卑微了
class Solution {
private:
void search(vector< vector<int> > &Answer, int now, int cnt, int n, int k, int state) {
if (now == n + 1) {
if (cnt != k) return;
vector<int> tmp;
tmp.clear();
for (int i = 1; i <= n; i ++)
if ((state >> i) & 1)
tmp.push_back(i);
Answer.push_back(tmp);
return;
}
if (cnt + (n - now + 1) > k)
search(Answer, now + 1, cnt, n, k, state);
if (cnt < k)
search(Answer, now + 1, cnt + 1, n, k, state | (1 << now));
}
public:
vector<vector<int>> combine(int n, int k) {
vector< vector<int> > Answer;
Answer.clear();
search(Answer, 1, 0, n, k, 0);
return Answer;
}
};
78. 子集
无脑枚举法。O(n*2^n)
这题如果不是必须用vector来输出的话倒是应该还有渐进复杂度更低的做法。比如用格雷码来枚举二进制位之类的(?),是不是有希望省掉那个n。。。但是肯定更复杂的做法常数会更大。如果n不是很大的话这样搞也没什么意义。n很大的话网站也跑不出来(x)
class Solution {
public:
vector<vector<int>> subsets(vector<int>& nums) {
int n = nums.size();
vector< vector<int> > Answer;
Answer.clear();
for (int i = 0; i < (1 << n); i ++) {
vector<int> tmp;
tmp.clear();
for (int j = 0; j < n; j ++)
if ((i >> j) & 1)
tmp.push_back(nums[j]);
Answer.push_back(tmp);
}
return Answer;
}
};
79. 单词搜索
直接搜。意外地跑得还挺快??
class Solution {
private:
#define CVEC vector< vector<char> >
int d[4][2] = {{1, 0}, {0, 1}, {-1, 0}, {0, -1}};
int m, n;
bool Search(int now, int x, int y, int len, CVEC &board, string &word, CVEC &ext) {
if (now >= len - 1) return true;
bool tmp;
for (int i = 0; i < 4; i ++) {
int u = x + d[i][0];
int v = y + d[i][1];
if (u < m && u >= 0 && v < n && v >= 0 && ext[u][v] != 0 && board[u][v] == word[now + 1]) {
ext[u][v] = 0;
tmp = Search(now + 1, u, v, len, board, word, ext);
ext[u][v] = 1;
if (tmp == true) return true;
}
}
return false;
}
public:
bool exist(vector<vector<char>>& board, string word) {
int len = word.size();
CVEC ext = board;
bool flag = false;
m = board.size();
if (m == 0 || len == 0) return false;
n = board[0].size();
for (int i = 0; i < m; i ++)
for (int j = 0; j < n; j ++)
if (word[0] == board[i][j]){
ext[i][j] = 0;
flag = flag || Search(0, i, j, len, board, word, ext);
if (flag == true) return true;
ext[i][j] = 1;
}
return false;
}
};
80.删除排序数组中的重复项II
把符合要求的元素都往前交换就可以了。维护一个指针表示当前合法的前缀有多长,然后循环扫描每个元素。如果是合法的就往前交换,不合法就继续扫描。
class Solution {
public:
int removeDuplicates(vector<int>& nums) {
int ptr = 1, n = nums.size();
int lastnum, cnt;
if (n == 0) return 0;
lastnum = nums[0]; cnt = 1;
for (int i = 1; i < n; i ++) {
if (nums[i] == lastnum) {
if (cnt < 2) cnt++;
else
continue;
}else {
lastnum = nums[i]; cnt = 1;
}
swap(nums[ptr], nums[i]); ptr ++;
}
return ptr;
}
};
81. 搜索旋转排序数组
关键的一个区别就是在二分找断开的位置的时候不满足单调性了。如果每个数都不相同的话,找小于nums[0]且最靠左的数字就很好找;但是如果允许相同的数字,比如说2 2 2 0 2 2这种情况就不满足单调性,没法直接二分了。实际上这种时候最坏情况复杂度就是O(n)的了,没有改进余地了。不过也可以在二分找断开位置之前先暴力地把那一段相等的前缀判掉,比如2 2 2 0 2 2变成0 2 2,这样就满足单调性,可以二分解决了。
这样写,大概,也许,貌似,可能会比暴力好一点点8。。。
class Solution {
private:
int Findpos(vector<int> &nums, int n) {
int l = 1, r = n - 1, mid, ans = n;
while (l != r && nums[l] == nums[l - 1]) l ++;
while (l != r) {
mid = (l + r) >> 1;
if (nums[mid] <= nums[0]) {
ans = min(ans ,mid);
r = mid;
}else l = mid + 1;
}
if (nums[l] <= nums[0])
ans = min(ans, l);
if (ans == n) return 0;
if (ans == l && nums[l] == nums[l - 1]) return 0;
return ans;
}
bool Findnum(vector<int> &nums, int n, int pos, int target) {
int l = 0, r = n - 1, mid, tmp;
while (l <= r) {
mid = (l + r) >> 1;
tmp = (pos + mid) % n;
if (nums[tmp] == target) return true;
if (nums[tmp] > target) r = mid - 1;
else l = mid + 1;
}
return false;
}
public:
bool search(vector<int>& nums, int target) {
int pos, n = nums.size();
if (n == 0) return false;
if (n == 1) return (target == nums[0]);
pos = Findpos(nums, n);
return Findnum(nums, n, pos, target);
}
};
82. 删除排序链表中的重复元素
我的做法是另外开了一个链表,把所有不重复的节点都摘进去。
/**
* Definition for singly-linked list.
* struct ListNode {
* int val;
* ListNode *next;
* ListNode(int x) : val(x), next(NULL) {}
* };
*/
class Solution {
public:
ListNode* deleteDuplicates(ListNode* head) {
ListNode *Answer = NULL, *tail = NULL;
for (ListNode *ptr = head; ; ) {
int cnt = 0;
if (ptr == NULL) break;
while (ptr->next != NULL && ptr->val == ptr->next->val) {
cnt ++; ptr = ptr->next;
}
if (cnt == 0) {
if (Answer == NULL){
Answer = tail = ptr;
ptr = ptr->next; tail->next = NULL;
}else {
tail->next = ptr; tail = ptr;
ptr = ptr->next; tail->next = NULL;
}
}else ptr = ptr->next;
}
return Answer;
}
};
86. 分隔链表
还是两个链表,判断一下往哪边放就可以了。
/**
* Definition for singly-linked list.
* struct ListNode {
* int val;
* ListNode *next;
* ListNode(int x) : val(x), next(NULL) {}
* };
*/
class Solution {
public:
ListNode* partition(ListNode* head, int x) {
ListNode *h1, *h2, *t1, *t2;
h1 = h2 = t1 = t2 = NULL;
for (ListNode *ptr = head; ptr != NULL; ) {
if (ptr->val < x) {
if (h1 == NULL) h1 = t1 = ptr;
else {t1->next = ptr; t1 = t1->next;}
ptr = ptr->next; t1->next = NULL;
}
else {
if (h2 == NULL) h2 = t2 = ptr;
else {t2->next = ptr; t2 = t2->next;}
ptr = ptr->next; t2->next = NULL;
}
}
if (t1 == NULL) return h2;
t1->next = h2;
return h1;
}
};
89. 格雷编码
n位格雷码的生成过程是:先把n-1位格雷码逆序接在后面,然后前一半高位接一个0,后一半高位接一个1即可
class Solution {
public:
vector<int> grayCode(int n) {
vector<int> Answer;
int len = 2;
Answer.clear();
Answer.push_back(0);
if (n == 0) return Answer;
Answer.push_back(1);
for (int i = 1; i < n; i ++) {
for (int j = len - 1; j >= 0; j --)
Answer.push_back(Answer[j]);
for (int j = len; j < 2*len; j ++)
Answer[j] += (1 << i);
len = len * 2;
}
return Answer;
}
};
91. 解码方法
DP一下就可以了。因为每个位置的结果最多只跟前一个和前两个位置有关。注意0是不能解码的,所以0不能单独作为一位,也不能作为2位数的开头。比如“01”这种是不行的。
class Solution {
public:
int numDecodings(string s) {
int n = s.size(), Answer;
int *f = new int[n + 1];
for (int i = 0; i <= n; i ++) f[i] = 0;
f[0] = 1;
for (int i = 1; i <= n; i ++) {
int x = s[i - 1] - '0';
if (x != 0)
f[i] += f[i - 1];
if (i != 1 && s[i - 2] != '0') {
x += (s[i - 2] - '0') * 10;
if (x >= 1 && x <= 26)
f[i] += f[i - 2];
}
}
Answer = f[n];
delete[] f;
return Answer;
}
};
92. 反转链表
先把m+1~n的next指针反转,再把m-1连到n,把n+1连到m就可以了。
/**
* Definition for singly-linked list.
* struct ListNode {
* int val;
* ListNode *next;
* ListNode(int x) : val(x), next(NULL) {}
* };
*/
class Solution {
public:
ListNode* reverseBetween(ListNode* head, int m, int n) {
ListNode *beforeM, *afterN, *M, *N;
int cnt = 0;
beforeM = afterN = M = N = NULL;
for (ListNode *ptr = head, *last = NULL; ptr != NULL; ) {
cnt ++;
if (cnt == m - 1) beforeM = ptr;
if (cnt == m) M = ptr;
if (cnt == n) N = ptr;
if (cnt == n + 1) afterN = ptr;
if (cnt > m && cnt <= n) {
ListNode *tmp = ptr;
ptr = ptr->next;
tmp->next = last;
last = tmp;
}else {
last = ptr; ptr = ptr -> next;
}
}
if (beforeM != NULL)
beforeM->next = N;
else head = N;
M->next = afterN;
return head;
}
};
105.从前序与中序遍历序列构造二叉树
标配写法
/**
* Definition for a binary tree node.
* struct TreeNode {
* int val;
* TreeNode *left;
* TreeNode *right;
* TreeNode(int x) : val(x), left(NULL), right(NULL) {}
* };
*/
class Solution {
private:
TreeNode* build(vector<int> &preorder, vector<int> &inorder, int lp, int rp, int li, int ri) {
if (lp > rp) return NULL;
TreeNode *head;
int pos = li;
head = new TreeNode(preorder[lp]);
if (lp == rp) return head;
while (pos <= ri && inorder[pos] != head->val)
pos ++;
head->left = build(preorder, inorder, lp + 1, lp + (pos - li), li, pos - 1);
head->right = build(preorder, inorder, lp + (pos - li) + 1, rp, pos + 1, ri);
return head;
}
public:
TreeNode* buildTree(vector<int>& preorder, vector<int>& inorder) {
int n = preorder.size();
if (n == 0) return NULL;
TreeNode *head;
head = build(preorder, inorder, 0, n - 1, 0, n - 1);
return head;
}
};
113. 路径总和
注意是到叶节点的路径。
/**
* Definition for a binary tree node.
* struct TreeNode {
* int val;
* TreeNode *left;
* TreeNode *right;
* TreeNode(int x) : val(x), left(NULL), right(NULL) {}
* };
*/
class Solution {
private:
void dfs(TreeNode *now, int nowsum, int target, vector<int> &tmp, vector< vector<int> > &Answer) {
if (nowsum == target && now->left == NULL && now->right == NULL)
Answer.push_back(tmp);
if (now == NULL) return;
if (now->left != NULL) {
tmp.push_back(now->left->val);
dfs(now->left, nowsum + now->left->val, target, tmp, Answer);
vector<int>::iterator it = tmp.end();
it --; tmp.erase(it);
}
if (now->right != NULL) {
tmp.push_back(now->right->val);
dfs(now->right, nowsum + now->right->val, target, tmp, Answer);
vector<int>::iterator it = tmp.end();
it --; tmp.erase(it);
}
}
public:
vector<vector<int>> pathSum(TreeNode* root, int sum) {
vector<int> tmp;
tmp.clear();
vector< vector<int> > Answer;
Answer.clear();
if (root == NULL) return Answer;
tmp.push_back(root->val);
dfs(root, root->val, sum, tmp, Answer);
return Answer;
}
};
123. 买卖股票的最佳时机
递推一下。f表示在第i天卖出能得到的最大价值,g表示在第i天买入能够得到的最大价值。做一个前缀和,就把f变成在第i天之前卖出得到的最大价值,g变成在第i天之后买入得到的最大价值。然后f和g加一下就得到了买卖两次的最大价值。注意处理只买一次的情况(如样例2)
class Solution {
public:
int maxProfit(vector<int>& prices) {
int n = prices.size();
if (n == 0) return 0;
int *f = new int[n];
int *g = new int[n];
int Answer = 0;
int Min = 2147483647, Max = -Min - 1;
for (int i = 0; i < n; i ++) {
Min = min(Min, prices[i]);
f[i] = max(0, prices[i] - Min);
}
for (int i = n - 1; i >= 0; i --) {
Max = max(Max, prices[i]);
g[i] = max(0, Max - prices[i]);
}
for (int i = 1; i < n; i ++)
f[i] = max(f[i], f[i - 1]);
for (int i = n - 2; i >= 0; i --)
g[i] = max(g[i], g[i + 1]);
for (int i = 1; i < n; i ++)
Answer = max(Answer, f[i - 1] + g[i]);
Answer = max(Answer, f[n - 1]);
Answer = max(Answer, g[0]);
delete[] f;
delete[] g;
return Answer;
}
};
128. 最长连续序列
这个题很有意思。如果把这个序列划分成一段段极长的连续序列,这些序列一定是不相交的。那么我们只要找到每个序列的开头,然后顺次枚举直到不连续,就可以枚举到每一个极长的连续序列统计答案。并且因为极长连续序列不相交,这样枚举复杂度均摊O(n)。序列开头的特点就是比它恰好小1的那个数字不在序列里。用一个哈希表判断一下就可以了。
class Solution {
public:
int longestConsecutive(vector<int>& nums) {
int n = nums.size(), Answer = 0;
if (n == 0) return 0;
unordered_map<int, int> hashSet;
hashSet.clear();
for (int i = 0; i < n; i ++)
hashSet[nums[i]] = 1;
for (int i = 0; i < n; i ++)
if (hashSet[nums[i] - 1] == 0) {
int cnt = 0, ptr = nums[i];
while (hashSet[ptr] == 1) {
cnt ++; ptr ++;
}
Answer = max(Answer, cnt);
}
return Answer;
}
};
129. 求根到叶子结点数字之和
dfs做就可以了。注意特判输入是空树的情况
/**
* Definition for a binary tree node.
* struct TreeNode {
* int val;
* TreeNode *left;
* TreeNode *right;
* TreeNode(int x) : val(x), left(NULL), right(NULL) {}
* };
*/
class Solution {
private:
void dfs(TreeNode *root, int &Answer, int number) {
if (root->left == NULL && root->right == NULL) {
Answer += number * 10 + root->val;
return;
}
if (root->left != NULL)
dfs(root->left, Answer, number * 10 + root->val);
if (root->right != NULL)
dfs(root->right, Answer, number * 10 + root->val);
}
public:
int sumNumbers(TreeNode* root) {
int Answer = 0;
if (root == NULL) return Answer;
dfs(root, Answer, 0);
return Answer;
}
};
130.被围绕的区域
嗨呀。。。边界条件判不好RE了半天
题面里那个“解释”基本上就是题解了
class Solution {
private:
void Fill(vector<vector<char>> &board, int x, int y, bool **ext, int n, int m) {
if (ext[x][y] == true) return;
ext[x][y] = true;
if (x - 1 >= 0 && board[x - 1][y] == 'O')
Fill(board, x - 1, y, ext, n, m);
if (y + 1 < m && board[x][y + 1] == 'O')
Fill(board, x, y + 1, ext, n, m);
if (y - 1 >= 0 && board[x][y - 1] == 'O')
Fill(board, x, y - 1, ext, n, m);
if (x + 1 < n && board[x + 1][y] == 'O')
Fill(board, x + 1, y, ext, n, m);
}
public:
void solve(vector<vector<char>>& board) {
int n = board.size();
if (n == 0) return;
int m = board[0].size();
bool **ext = new bool* [n];
for (int i = 0; i < n; i ++) {
ext[i] = new bool[m];
}
for (int i = 0; i < n; i ++)
for (int j = 0; j < m; j ++)
ext[i][j] = false;
for (int i = 0; i < n; i ++) {
if (board[i][0] == 'O')
Fill(board, i, 0, ext, n, m);
if (board[i][m - 1] == 'O')
Fill(board, i, m - 1, ext, n, m);
}
for (int j = 0; j < m; j ++) {
if (board[0][j] == 'O')
Fill(board, 0, j, ext, n, m);
if (board[n - 1][j] == 'O')
Fill(board, n - 1, j, ext, n, m);
}
for (int i = 0; i < n; i ++)
for (int j = 0; j < m; j ++)
if (ext[i][j] == false)
board[i][j] = 'X';
for (int i = 0; i < n; i ++)
delete[] ext[i];
delete[] ext;
return;
}
};
133. 克隆图
dfs一下即可。需要维护一个标记表示当前节点是否已经被新建过,以及如果被新建过它对应的地址是多少。
/*
// Definition for a Node.
class Node {
public:
int val;
vector<Node*> neighbors;
Node() {
val = 0;
neighbors = vector<Node*>();
}
Node(int _val) {
val = _val;
neighbors = vector<Node*>();
}
Node(int _val, vector<Node*> _neighbors) {
val = _val;
neighbors = _neighbors;
}
};
*/
class Solution {
private:
void cloneVertical(Node* oldnode, Node* newnode, Node* *ext) {
ext[newnode->val] = newnode;
int n = oldnode->neighbors.size();
for (int i = 0; i < n; i ++) {
Node* nextnode = oldnode->neighbors[i];
if (ext[nextnode->val] == NULL) {
Node *newnextnode;
newnextnode = new Node(nextnode->val);
cloneVertical(nextnode, newnextnode, ext);
}
newnode->neighbors.push_back(ext[nextnode->val]);
}
}
public:
Node* cloneGraph(Node* node) {
Node *newnode;
if (node == NULL) return NULL;
newnode = new Node(node->val);
Node* *ext = new Node*[200];
for (int i = 0; i < 200; i ++)
ext[i] = NULL;
cloneVertical(node, newnode, ext);
return newnode;
}
};
134. 加油站
这个题也挺有意思。条件实际上就是选定一个起点后,任意一个时刻gas的前缀和不能少于cost的前缀和。
O(n^2)的做法很好想,直接暴力就可以。考虑O(n)的做法怎么实现。
观察可以发现这样一个性质:如果从点i开始走,走到点j发现走不动了,那么如果从i+1,i+2,....,j-1开始走,走到点j也还是会走不动。
因为从i开始走,走到点i+1的时候,由于gas>=cost,油箱里最坏情况就是已经空了,较好的情况下还能剩下一些油。也就是说此时在点i+1开始走的时候是有额外的初始汽油的。但是如果直接选择点i+1作为起点,油箱初始一定是空的,所以情况肯定不会好于刚才从点i开始的时候。其它的点i+2,...,点j-1也同理可以分析。
那么也就是说如果在点j卡住了,那么可能的起点一定是从点j+1开始。
这样的话枚举起点和验证起点的可行性这两者的复杂度就可以均摊了。
做法是首先找到一个能够作为起点的点,然后从这个点开始验证。如果能够走完一圈,那么这个点就是答案;否则如果在点j停住了,那么就从点j+1开始寻找新的能当做起点的点。在代码里体现为凡是枚举过的起点或是已经排除掉的起点都打了ext标记。如果所有可能的起点都被枚举过还没有找到解,那就是无解。
class Solution {
public:
int canCompleteCircuit(vector<int>& gas, vector<int>& cost) {
int n = gas.size(), Answer = -1;
if (n == 0) return Answer;
bool *ext = new bool[n];
for (int i = 0; i < n; i ++)
ext[i] = false;
int beginPosition = 0, ptr, gasSum, costSum;
for ( ; beginPosition < n; beginPosition ++)
if (gas[beginPosition] >= cost[beginPosition]) break;
else ext[beginPosition] = true;
if (beginPosition == n) return -1;
while (true) {
if (ext[beginPosition] == true) break;
ptr = beginPosition;
gasSum = gas[beginPosition];
costSum = cost[beginPosition];
while (gasSum >= costSum) {
ptr = (ptr + 1) % n;
if (ptr == beginPosition) {
Answer = beginPosition; break;
}
gasSum += gas[ptr];
costSum += cost[ptr];
}
if (Answer != -1) break;
while (beginPosition != ptr) {
ext[beginPosition] = true;
beginPosition = (beginPosition + 1) % n;
}
ext[ptr] = true;
beginPosition = (beginPosition + 1) % n;
}
delete[] ext;
return Answer;
}
};
135. 分发糖果
思路很简单,如果i的糖果必须比j多,就从j到i连一条边。这样建立拓扑图以后对每个节点求拓扑序就可以了。
不过y1s1这题标hard难度可能是因为步骤确实有点多(先初始化再建图再拓扑排序再统计答案blabla
class Solution {
private:
void add(int x, int y, int *p, int *a, int *nxt, int &tot) {
tot ++; a[tot] = y; nxt[tot] = p[x]; p[x] = tot;
}
public:
int candy(vector<int>& ratings) {
int n = ratings.size(), tot = 0;
if (n == 0) return 0;
if (n == 1) return 1;
int *p = new int[n];
int *a = new int[2 * n];
int *nxt = new int[2 * n];
int *d = new int[n];
int *ans = new int[n];
queue<int> q;
for (int i = 0; i < n; i ++)
d[i] = ans[i] = p[i] = 0;
if (ratings[0] < ratings[1])
add(0, 1, p, a, nxt, tot);
for (int i = 1; i < n - 1; i ++) {
if (ratings[i] < ratings[i + 1])
add(i, i + 1, p, a, nxt, tot);
if (ratings[i] < ratings[i - 1])
add(i, i - 1, p, a, nxt, tot);
}
if (ratings[n - 1] < ratings[n - 2])
add(n - 1, n - 2, p, a, nxt, tot);
for (int i = 0; i < n; i ++)
for (int j = p[i]; j != 0; j = nxt[j])
d[a[j]] ++;
for (int i = 0; i < n; i ++)
if (d[i] == 0) {
q.push(i); ans[i] = 1;
}
while (!q.empty()) {
int u = q.front(); q.pop();
for (int i = p[u]; i != 0; i = nxt[i]) {
ans[a[i]] = max(ans[a[i]], ans[u] + 1);
d[a[i]] --;
if (d[a[i]] == 0) q.push(a[i]);
}
}
int Answer = 0;
for (int i = 0; i < n; i ++)
Answer += ans[i];
delete[] p;
delete[] a;
delete[] nxt;
delete[] d;
delete[] ans;
return Answer;
}
};
139. 单词拆分
先考虑暴力怎么做。\(f[i]\)表示[0..i]这段前缀是否能被正确拆分,那么递推的时候只需要枚举最后一次切分的地方j,检查[j+1..i]这一段是不是字典里的单词,如果是的话就用f[j]递推f[i]就可以了。
因为要从字典里比较,于是把字典里所有词都倒着建成trie树。这样在递推位置i的时候就可以依次向前枚举然后在trie树上走动,如果trie树上不能走了就说明再往前枚举也不可能出现字典里的单词了,就可以break。每次走到一个有结束标记的节点时就说明遇到了一段在字典里出现过的词,这时候就可以更新f[i]。时间复杂度是\(O(n^2)\)的。
class Solution {
private:
struct trieNode {
bool is_end;
unordered_map<char, trieNode*> ch;
trieNode(){is_end = false;ch.clear();}
}*root;
void trieInsert(trieNode* root, string &str) {
int len = str.size();
trieNode *nownode = root;
for (int i = len - 1; i >= 0; i --) {
char nowch = str[i];
if (nownode->ch[nowch] == NULL) {
trieNode *tmp = new trieNode;
nownode->ch[nowch] = tmp;
}
nownode = nownode->ch[nowch];
}
nownode->is_end = true;
}
void check(trieNode *root, bool *f, int pos, string s) {
trieNode *nownode = root;
for (int i = pos; i >= 0; i --) {
char nowch = s[i];
if (nownode->ch[nowch] == NULL) break;
nownode = nownode->ch[nowch];
if (nownode->is_end == true) {
f[pos + 1] = f[pos + 1] || f[i];
if (f[pos + 1] == true) break;
}
}
}
public:
bool wordBreak(string s, vector<string>& wordDict) {
int n = wordDict.size();
int len = s.size();
bool *canBreak = new bool[len + 1];
root = new trieNode;
for (int i = 0; i < n; i ++)
trieInsert(root, wordDict[i]);
for (int i = 0; i <= len; i ++)
canBreak[i] = false;
canBreak[0] = true;
for (int i = 0; i < len; i ++)
check(root, canBreak, i, s);
return canBreak[len];
}
};
208. 实现Trie(前缀树)
打个板子练练手.
感觉那些insert之类的函数应该也可以递归地写。用string的substr处理一下参数之类的。
class Trie {
private:
map<char, Trie*> child;
bool is_end;
public:
/** Initialize your data structure here. */
Trie() {
child.clear();
is_end = false;
}
/** Inserts a word into the trie. */
void insert(string word) {
int len = word.size();
Trie* now = this;
for (int i = 0; i < len; i ++) {
if (now->child[word[i]] == NULL) {
Trie* ch = new Trie();
now->child[word[i]] = ch;
}
now = now->child[word[i]];
}
now->is_end = true;
}
/** Returns if the word is in the trie. */
bool search(string word) {
int len = word.size();
Trie* now = this;
for (int i = 0; i < len; i ++) {
now = now->child[word[i]];
if (now == NULL) return false;
}
return now->is_end;
}
/** Returns if there is any word in the trie that starts with the given prefix. */
bool startsWith(string prefix) {
int len = prefix.size();
Trie* now = this;
for (int i = 0; i < len; i ++) {
now = now->child[prefix[i]];
if (now == NULL) return false;
}
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. 长度最小的子数组
把数组做前缀和转化,问题就变为找到最接近的两个数字它们的差大于等于s。
这个问题与“两数之和”类似,第一个数字从左向右移动的时候,对应的第二个数字也是从左向右单调移动的(由于整个数组都是正数,前缀和数组是单增的)
two pointers就可以O(n)。如果二分第二个数字就是O(nlogn)。
class Solution {
public:
int minSubArrayLen(int s, vector<int>& nums) {
int n = nums.size();
int Answer = n + 1;
int p1 = 0, p2 = 1;
int *sum = new int[n + 1];
sum[0] = 0;
for (int i = 1; i <= n; i ++)
sum[i] = sum[i - 1] + nums[i - 1];
while (p1 < n) {
while (p2 <= n && sum[p2] - sum[p1] < s)
p2 ++;
if (p2 > n) break;
if (sum[p2] - sum[p1] >= s)
Answer = min(Answer, p2 - p1);
p1 ++;
}
delete[] sum;
if (Answer == n + 1) return 0;
else return Answer;
}
};
215. 数组中的第K个最大元素
直接用c++自带的nth_element就可
这个函数接受三个参数,第一个和第三个元素是指定操作区间的,左闭右开(与sort一样);第二个参数指向操作区间中的一个位置,操作后若这个位置上的数为x,那么它左边的数都比x小,右边的数都比x大。nth_element函数的平均复杂度是O(n)的。
当然也可以自己实现。类似于快排的写法,但不过每次不是两边都递归,而是有选择性的递归左边或右边。因为快排的时候每次都是把比基准元素小的放在左边,比基准元素大的放在右边。这样比如左边的元素大于k个,说明答案就在左半边;否则答案就在右半边,递归下去做,平均也是O(n)的。
class Solution {
public:
int findKthLargest(vector<int>& nums, int k) {
int p = nums.size() - k + 1;
nth_element(nums.begin(), nums.begin() + p - 1, nums.end());
return nums[p - 1];
}
};
216. 组合总和III
因为每个数字只能用一次且只有9个数字,所以最多只有\(2^{10}=1024\)种组合,用二进制枚举方案就可以了。
class Solution {
private:
int countOnes(int number) {
int cnt = 0;
while (number != 0) {
cnt += (number & 1);
number >>= 1;
}
return cnt;
}
int calcSum(int number) {
int sum = 0;
for (int i = 0; i < 9; i ++) {
if (number & 1) sum += (i + 1);
number >>= 1;
}
return sum;
}
vector<int> toVector(int number) {
vector<int> res;
for (int i = 0; i < 9; i ++) {
if (number & 1) res.push_back(i + 1);
number >>= 1;
}
return res;
}
public:
vector<vector<int>> combinationSum3(int k, int n) {
vector< vector<int> > Answer;
for (int i = 0; i < (1 << 9); i ++) {
if (countOnes(i) == k && calcSum(i) == n)
Answer.push_back(toVector(i));
}
return Answer;
}
};
32. 最长有效括号
类似于判断单个字符串的方法,依次扫描每个字符,如果当前字符是左括号,就直接入栈并且记录它的位置;如果当前字符是右括号,分以下几种情况讨论:
- 栈顶有一个左括号。此时这个左括号可以和新来的右括号配对。设新来的右括号是第i位,那么当前的栈顶元素指出了以i结尾的极长合法括号串的开头(实际上是开头之前的那个元素)。
- 栈为空或者栈顶是右括号。此时说明新来的右括号是不合法的,直接把它入栈且记录位置。
举个栗子,对于(()())这个串,操作过程如下:
0. 入栈一个-1作为边界
- 先入栈两个左括号
- 2号位置的右括号与1号位置的左括号配对。此时栈顶元素的下标是0,更新答案为2-0=2。
- 入栈一个左括号
- 4号位置的右括号与3号位置的左括号配对。此时栈顶元素的下标是0,更新答案为4-0=4。
- 5号位置的右括号与0号位置的左括号配对。此时栈顶元素的下标是-1(见第0步),更新答案为5-(-1)=6。
从第4步可以看出,这个做法正确的关键是更新答案时使用的是“弹出左括号后新的栈顶元素的下标”而不是“刚出栈的元素下标”。
如果是后者,就不能处理若干个合法括号串拼在一起的情况。在刚刚的例子里,如果用“刚出栈的元素下标”来更新答案,第4步得到的答案就会变成4-2=2,而不是4-0=4。
class Solution {
public:
int longestValidParentheses(string s) {
int n = s.size();
int Answer = 0;
if (n == 0) return Answer;
stack<int> st;
while (!st.empty()) st.pop();
st.push(-1);
for (int i = 0; i < n; i ++) {
if (s[i] == '(') {
st.push(i);
}else {
if (st.top() != -1 && s[st.top()] == '(') {
st.pop();
Answer = max(Answer, i - st.top());
}else {
st.push(i);
}
}
}
return Answer;
}
};
142. 环形链表
空间O(n)的很好做。O(1)的这个做法之前一直不会,这次总算弄明白了。。
就是设置两个指针fast和slow,fast每次走两步,slow每次走一步。如果fast和slow能相遇,就说明链表有环。
值得注意的是,相遇的时候slow一定没有重复走已经走过的节点。也就是说如果链表总节点数为N,则slow和fast相遇的时候slow走的步数一定小于等于N步。
因为slow和fast每前进一次,它们之间的距离都会拉开1(因为fast每次多走一步)。显然在一个长度为N的链表上,两个节点之间的距离不可能是N+1。所以它们一定会在slow走完一圈之前相遇。
当fast和slow相遇的时候,说明有环,但是这个相遇的点不一定是环的开头节点。
设环的长度为b(目前未知),链表中不属于环的部分长度为a(目前也未知),显然a+b=N。
当fast和slow相遇的时候,fast比slow多走了若干圈。即:\(fast = slow + k \cdot b\)。
又因为fast每次走两步而slow走一步,有:\(fast = 2 \cdot slow\)。
可得\(slow = k \cdot b\)。又可以知道,环的开头结点对应的步数是\(a+nb\),所以slow只要再往后走a步就可以到达环的开头了。
虽然a是未知的,但可以发现,如果一个指针ptr从链表开头开始走,每次走一步,那么它往后走a步后也会到达环的开头,即它会跟a相遇。
由于a一定小于N,这次相遇一定是ptr和slow的第一次相遇。
于是算法就有了。首先用fast和slow指针找到第一次相遇的位置。然后让一个指针ptr指到链表开头,和slow一起往前走。当它和slow相遇,就找到了环的开始节点。
这里的实现把fast和ptr这两个指针合并了。
注意判断没有环的情况。没有环的话fast一定会先走到NULL节点,特判一下就可以了。
/**
* Definition for singly-linked list.
* struct ListNode {
* int val;
* ListNode *next;
* ListNode(int x) : val(x), next(NULL) {}
* };
*/
class Solution {
public:
ListNode *detectCycle(ListNode *head) {
ListNode *fast, *slow;
fast = slow = head;
do {
if (fast == NULL || fast->next == NULL)
return NULL;
slow = slow->next;
fast = fast->next->next;
} while (fast != slow);
fast = head;
while (fast != slow) {
fast = fast->next;
slow = slow->next;
}
return fast;
}
};
143. 重排链表
写的略有点麻烦了。
观察可以发现,题目要的操作就是把链表分成长度尽量相等的A和B两部分,然后把后一部分反转,最后再ABAB这样子穿插着连起来。
例如[1,2,3,4,5]就是分成[1,2,3]和[4,5]。然后把后一部分反转变成[5,4],然后穿插起来就变成了[1,5,2,4,3]。
于是只要先统计节点总数,然后找到两部分的分界点,把第二部分反转,然后穿插连接。
注意特判链表为空或只有一个节点的情况。
/**
* 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* reverse(ListNode* head) {
ListNode *now, *last, *tmp;
now = head->next; last = head;
last->next = NULL;
while (now != NULL) {
tmp = now->next;
now->next = last;
last = now; now = tmp;
}
return last;
}
void reorderList(ListNode* head) {
int n = 0, half;
ListNode *now = head, *last, *p1, *p2;
while (now != NULL) {
n++; now = now->next;
}
if (n <= 1) return;
half = n / 2 + n % 2;
now = head;
for (int i = 1; i <= half; i ++) {
last = now;
now = now->next;
}
last->next = NULL;
now = reverse(now);
p1 = head; p2 = now;
while (p1 != NULL && p2 != NULL) {
now = p1; p1 = p1->next;
last = p2; p2 = p2->next;
now->next = last;
last->next = p1;
}
}
};
144. 二叉树的前序遍历
按定义做即可。注意特判空树。
/**
* Definition for a binary tree node.
* struct TreeNode {
* int val;
* TreeNode *left;
* TreeNode *right;
* TreeNode() : val(0), left(nullptr), right(nullptr) {}
* TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
* TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {}
* };
*/
class Solution {
public:
void dfs(TreeNode *root, vector<int> &ans) {
ans.push_back(root->val);
if (root->left != NULL)
dfs(root->left, ans);
if (root->right != NULL)
dfs(root->right, ans);
}
vector<int> preorderTraversal(TreeNode* root) {
vector<int> ans;
ans.clear();
if (root == NULL) return ans;
dfs(root, ans);
return ans;
}
};
145. 二叉树的后序遍历
手工栈写法。栈中不仅需要记录当前节点,还需要记录当前节点处理到了哪一步。在这道题中,一个节点的处理分三步:左节点入栈、右节点入栈、加入ans数组。所以设置了三个状态,保证只有在左右子树全部处理完以后才把当前节点的值加入ans。
/**
* Definition for a binary tree node.
* struct TreeNode {
* int val;
* TreeNode *left;
* TreeNode *right;
* TreeNode() : val(0), left(nullptr), right(nullptr) {}
* TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
* TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {}
* };
*/
class Solution {
public:
vector<int> postorderTraversal(TreeNode* root) {
vector<int> ans, state;
vector<TreeNode*> st;
ans.clear(); st.clear(); state.clear();
if (root == NULL) return ans;
st.push_back(root);
state.push_back(1);
while (!st.empty()) {
TreeNode *u = st.back();
int nowstate = state.back();
if (nowstate == 1) {
state.pop_back();
state.push_back(2);
if (u->left != NULL) {
st.push_back(u->left);
state.push_back(1);
}
}else if (nowstate == 2) {
state.pop_back();
state.push_back(3);
if (u->right != NULL) {
st.push_back(u->right);
state.push_back(1);
}
}else {
ans.push_back(u->val);
state.pop_back();
st.pop_back();
}
}
return ans;
}
};
147. 对链表进行插入排序
简单的链表操作。
/**
* Definition for singly-linked list.
* struct ListNode {
* int val;
* ListNode *next;
* ListNode(int x) : val(x), next(NULL) {}
* };
*/
class Solution {
private:
ListNode* insert(ListNode *head, ListNode *tar) {
ListNode *now = head, *last = NULL;
while (now != NULL && tar->val >= now->val) {
last = now; now = now->next;
}
if (last != NULL)
last->next = tar;
else head = tar;
tar->next = now;
return head;
}
public:
ListNode* insertionSortList(ListNode* head) {
ListNode *p1, *p2, *tmp;
if (head == NULL) return NULL;
p1 = head; p2 = head->next;
p1->next = NULL;
while (p2 != NULL) {
tmp = p2; p2 = p2->next;
p1 = insert(p1, tmp);
}
head = p1;
return head;
}
};
152. 乘积最大子数组
显然可以用f[i]表示前i个数,第i个数一定选的最大乘积子串。
直接递推的话问题在于这个数组里可能有负数,所以子串的一部分最大不能保证整体乘积最大。
但考虑负数带来的影响。就是有可能第i个数是负数,然后前面一个负数乘积子串乘了第i个数以后立刻变成了一个大正数。
这种情况下,跟第i个数配对的肯定是最小的那个负数(绝对值最大),这样才有可能得到最大的答案。
所以只需要再维护一个数组g,表示前i个数,第i个数一定选的最小乘积子串就可以了。
每次递推的时候,f[i-1]和g[i-1]都有可能用来更新f[i]。当然,也要考虑只选f[i]不选前面的数字的情况。
class Solution {
public:
int maxProduct(vector<int>& nums) {
int n = nums.size(), ans;
if (n == 0) return 0;
int *f = new int[n+1];
int *g = new int[n+1];
f[0] = g[0] = ans = nums[0];
for (int i = 1; i < n; i ++) {
f[i] = max(nums[i]*f[i-1], nums[i]*g[i-1]);
f[i] = max(f[i], nums[i]);
g[i] = min(nums[i]*f[i-1], nums[i]*g[i-1]);
g[i] = min(g[i], nums[i]);
ans = max(ans, f[i]);
ans = max(ans, g[i]);
}
return ans;
}
};
189. 旋转数组
最显然的方法是每次向右移动一位,移动k次。这样做的时间复杂度是\(O(kn)\)的,空间复杂度是\(O(1)\)的。
不难发现第i位的数移动k次以后就位于\((i+k)\%n\)这个位置。所以每次可以算出当前数字应该放在什么地方。开一个\(O(n)\)的空数组,一个一个填数就可以了。时间复杂度\(O(n)\),空间复杂度\(O(n)\)。
考虑如何优化空间。显然,每次用第i位的数覆盖\((i+k)\%n\)以后,必须把\((i+k)\%n\)位置原来的数字存着,不能丢掉。这个时候最好立刻再把\((i+k)\%n\)位置的数放到它正确的位置,即\((i+k+k)\%n\)。
这样以此类推下去就构成了一条链。可以发现,有时候一条这样的链就可以覆盖所有数字(例如n=8,k=3,则这条链是0->3->6->1->4->7->2->5),但另外一些时候不行(例如n=8,k=6时)。
观察发现,如果k与n的最大公因数是g,那么就需要g条链来覆盖所有数字。每条链上的位置编号模g同余。
例如,当n=8,k=6时,g=2。第一条链是0->6->4->2,第二条链是1->7->5->3。第一条链上的位置编号模g等于0,第二条链上的位置编号模g等于1。
这样我们只需要枚举前g个点就可以访问所有的g条不同的链,只需要在每一条链上遍历即可。在同一条链上交换的空间复杂度是O(1),因为只需要存着刚被覆盖掉的数字即可。
由于每个数字只会被交换一次,时间复杂度\(O(n)\),空间复杂度\(O(1)\)
class Solution {
public:
void rotate(vector<int>& nums, int k) {
int n = nums.size(), fac;
if (n <= 1) return;
k = k % n;
fac = gcd(n, k);
for (int i = 0; i < fac; i ++) {
int last, now, ptr;
last = nums[i]; ptr = i;
do {
ptr = (ptr + k) % n;
now = nums[ptr];
nums[ptr] = last;
last = now;
} while (ptr != i);
}
}
};
199. 二叉树的右视图
这道题有意思在于看起来是只要顺着右边走就可以,但有可能左子树比右子树长,所以左子树下部的一些节点也需要统计进答案。
所以要把整棵树都dfs一边,但先遍历右子树再遍历左子树,并且记录目前已经达到的最大深度。如果左子树某些节点的深度超过了右子树能达到的最大深度,那么也要把它加入答案。
/**
* Definition for a binary tree node.
* struct TreeNode {
* int val;
* TreeNode *left;
* TreeNode *right;
* TreeNode(int x) : val(x), left(NULL), right(NULL) {}
* };
*/
class Solution {
private:
void dfs(int dep, TreeNode* now, vector<int> &ans) {
if (dep >= ans.size())
ans.push_back(now->val);
if (now->right != NULL)
dfs(dep + 1, now->right, ans);
if (now->left != NULL)
dfs(dep + 1, now->left, ans);
}
public:
vector<int> rightSideView(TreeNode* root) {
vector<int> ans;
ans.clear();
if (root != NULL)
dfs(0, root, ans);
return ans;
}
};
200. 岛屿数量
简单的bfs。上下左右四个方向搜索,搜过的点标个1即可
class Solution {
private:
int n, m;
int d[4][2] = {{0, 1}, {1, 0}, {-1, 0}, {0, -1}};
bool ext[500][500];
void bfs(int sx, int sy, vector<vector<char>> &grid) {
queue<pair<int, int>> q;
q.push(make_pair(sx, sy));
ext[sx][sy] = true;
while (!q.empty()) {
pair<int, int> u;
u = q.front(); q.pop();
for (int i = 0; i < 4; i ++) {
int x = u.first + d[i][0];
int y = u.second + d[i][1];
if (x >= 0 && x < n && y >= 0 && y < m && grid[x][y] == '1' && ext[x][y] == false) {
q.push(make_pair(x, y));
ext[x][y] = true;
}
}
}
}
public:
int numIslands(vector<vector<char>>& grid) {
int ans = 0;
n = grid.size();
m = grid[0].size();
memset(ext, 0, sizeof(ext));
for (int i = 0; i < n; i ++)
for (int j = 0; j < m; j ++)
if (grid[i][j] == '1' && ext[i][j] == false) {
bfs(i, j, grid); ++ans;
}
return ans;
}
};
201. 数字范围按位与
因为是位运算,所以每一位分开考虑。
在数字连续变化的过程中,每一位都会经历由0变1再变0再变1这样循环的过程,不同的位循环的周期长度不一样。
显然,从右往左第i位(从1开始)循环的周期就是1<<i这个样子。
因为是按位与,所以只要有一个是0那全都是0。可以发现,如果数字连续变化超过了第i位半个周期的长度,那么第i位肯定至少会出现一个0。
例如,在样例[5,7]中,第2位的周期是4,半周期是2,数字范围长度是3,所以第二位一定出现了一个0。事实也是如此:101/110/111,第一个数字的第二位是0。
那么只需要考虑数字变化范围不超过半周期的情况。显然,此时第i位要么保持不变,要么只变化了一次(如果变化两次以上,那么范围就超过一个周期了)
所以只需要判断第一个数字和最后一个数字的第i位是否都为1。如果都为1,那么第i位按位与的结果是1;否则是0。
class Solution {
public:
int rangeBitwiseAnd(int m, int n) {
int x = 0;
for (int i = 31; i >= 0; i --) {
long long lim = 1LL << i;
if (n - m >= lim)
x = (x << 1) | 0;
else {
int u = (m >> i) & 1;
int v = (n >> i) & 1;
if (u == 1 && v == 1)
x = (x << 1) | 1;
else x = (x << 1) | 0;
}
}
return x;
}
};
187. 重复的DNA序列
哈希一下,用unordered_map
class Solution {
private:
unordered_map<string, int> hashTable;
public:
vector<string> findRepeatedDnaSequences(string s) {
int len = s.size();
vector<string> ans;
ans.clear(); hashTable.clear();
if (len < 10) return ans;
for (int i = 0; i < len - 10 + 1; i ++) {
string tmp = s.substr(i, 10);
if (hashTable[tmp] == 1)
ans.push_back(tmp);
hashTable[tmp] ++;
}
return ans;
}
};
207. 课程表
简单的拓扑排序。
class Solution {
public:
bool canFinish(int numCourses, vector<vector<int>>& prerequisites) {
if (numCourses == 0) return true;
int indeg[numCourses];
int m = prerequisites.size();
queue<int> q;
unordered_map<int, vector<int>> outlink;
memset(indeg, 0, sizeof(indeg));
for (int i = 0; i < m; i ++) {
vector<int> e = prerequisites[i];
indeg[e[0]] ++;
outlink[e[1]].push_back(e[0]);
}
while (!q.empty()) q.pop();
for (int i = 0; i < numCourses; i ++)
if (indeg[i] == 0) q.push(i);
while (!q.empty()) {
int u = q.front(); q.pop();
int k = outlink[u].size();
for (int i = 0; i < k; i ++) {
int v = outlink[u][i];
indeg[v] --;
if (indeg[v] == 0) q.push(v);
}
}
for (int i = 0; i < numCourses; i ++)
if (indeg[i] != 0) return false;
return true;
}
};
210. 课程表II
把上一个题的代码魔改一下就可以了。
主要就是增加一个记录答案的数组ans,拓扑排序的时候每从队列里弹出一个节点就把它加到ans里。
最后统计答案的时候如果发现有度数没有减到0的节点,说明有环,就直接把ans清掉让它返回一个空数组
class Solution {
public:
vector<int> findOrder(int numCourses, vector<vector<int>>& prerequisites) {
vector<int> ans;
ans.clear();
if (numCourses == 0) return ans;
int indeg[numCourses];
int m = prerequisites.size();
queue<int> q;
unordered_map<int, vector<int>> outlink;
memset(indeg, 0, sizeof(indeg));
for (int i = 0; i < m; i ++) {
vector<int> e = prerequisites[i];
indeg[e[0]] ++;
outlink[e[1]].push_back(e[0]);
}
while (!q.empty()) q.pop();
for (int i = 0; i < numCourses; i ++)
if (indeg[i] == 0) q.push(i);
while (!q.empty()) {
int u = q.front(); q.pop();
ans.push_back(u);
int k = outlink[u].size();
for (int i = 0; i < k; i ++) {
int v = outlink[u][i];
indeg[v] --;
if (indeg[v] == 0) q.push(v);
}
}
for (int i = 0; i < numCourses; i ++)
if (indeg[i] != 0) ans.clear();
return ans;
}
};
211. 添加与搜索单词
一开始纠结了半天,因为不知道怎么动态维护AC自动机的fail指针(它好像就没有办法动态维护)。
后来仔细一看题,哦原来是必须全部匹配,那直接一个trie树就行了
每次遇到通配符的时候采用了暴力搜索的方式。
class WordDictionary {
public:
/** Initialize your data structure here. */
struct trieNode {
unordered_map<char, trieNode*> ch;
bool is_end;
};
trieNode *root;
WordDictionary() {
root = new trieNode;
root->is_end = false;
}
void addWord(string word) {
int len = word.size();
trieNode *now = root;
for (int i = 0; i < len; i ++) {
char x = word[i];
if (now->ch[x] == NULL) {
now->ch[x] = new trieNode;
now->ch[x]->is_end = false;
}
now = now->ch[x];
}
now->is_end = true;
}
bool dfs(string &word, int i, int len, trieNode *now) {
if (now == NULL) return false;
if (i == len) return now->is_end;
if (word[i] == '.') {
for (char x = 'a'; x <= 'z'; x ++)
if (dfs(word, i+1, len, now->ch[x]))
return true;
return false;
}
char x = word[i];
return dfs(word, i+1, len, now->ch[x]);
}
bool search(string word) {
int len = word.size();
trieNode *now = root;
return dfs(word, 0, len, now);
}
};
/**
* Your WordDictionary object will be instantiated and called as such:
* WordDictionary* obj = new WordDictionary();
* obj->addWord(word);
* bool param_2 = obj->search(word);
*/
213. 打家劫舍II
DP。因为是一个环,直接DP可能有后效性,所以分成第一个点选or第一个点不选考虑
每次递推的过程是一样的,用f[i]表示前i个点的最大收益,要么就在前i-1个点里选,不选当前点;要么就在前i-2个点里选,并且选上当前点
边界条件是f[1]和f[2]。n=1的情况要特判
class Solution {
public:
int rob(vector<int>& nums) {
int n = nums.size();
int f[n+1], ans = 0;
if (n == 1) return nums[0];
memset(f, 0, sizeof(f));
f[1] = nums[0];
f[2] = max(nums[0], nums[1]);
for (int i = 3; i <= n; i ++)
f[i] = max(f[i-1], f[i-2]+nums[i-1]);
ans = f[n-1];
memset(f, 0, sizeof(f));
f[2] = nums[1];
for (int i = 3; i <= n; i ++)
f[i] = max(f[i-1], f[i-2]+nums[i-1]);
ans = max(ans, f[n]);
return ans;
}
};