被迫重操舊業(?)
再不刷題面試就真要翻車了。。。。
好在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;
}
};