前言
BAT常見的算法面試題解析:
程序員算法基礎——動態規劃
程序員算法基礎——貪心算法
工作閒暇也會有在線分享,算法基礎教程----騰訊課堂地址。
今天是LeetCode專場練習。
正文
Copy List with Random Pointer
題目鏈接
題目大意:
給出一個鏈表RandomListNode *next, *random;
每個節點有int值,有兩個指針,一個指向下一個節點,一個指向鏈表的任意節點;
現在實現一個深度複製,複製節點的next、random、還有int值;
題目解析:
要求的是複製所有的值,其中的next、int是常規值,遍歷一遍賦值即可;
較爲複雜的是random指針的複製,random指針有可能指向上一個節點,也可能指向下一個節點,在賦值的時候要保持對應的關係;
這裏可以用hash解決,我們把舊鏈表和新鏈表的節點一一對應,比如說oldList[i]=>newList[i];
那麼如果random指針指向oldList[i],相當於新鏈表指向newList[i];
class Solution {
public:
RandomListNode *copyRandomList(RandomListNode *head) {
RandomListNode *ret = NULL;
RandomListNode *p = head;
unordered_map<RandomListNode *, RandomListNode *> hashMap;
while (p) {
RandomListNode *node = new RandomListNode(p->label);
hashMap[p] = node;
if (ret) {
ret->next = node;
}
ret = node;
p = p->next;
}
p = head;
ret = hashMap[head];
while (p) {
if (p->random) {
ret->random = hashMap[p->random];
}
p = p->next;
ret = ret->next;
}
return hashMap[head];;
}
};
複雜度解析:
時間複雜度是O(N)
空間複雜度是O(N)
Insert Interval
題目鏈接
題目大意:
給出n個不重疊的區間[x, y],並且按照起始座標x進行從小到大的排序;
現在新增一個區間[a, b],爲了保持區間不重疊,對區間進行merge,問剩下的區間有哪些;
Example:
**Input: **intervals = [[1,2],[3,5],[6,7],[8,10],[12,16]], newInterval = [4,8]
Output: [[1,2],[3,10],[12,16]]
Explanation: Because the new interval [4,8] overlaps with [3,5],[6,7],[8,10].
題目解析:
最直接的做法是對所有區間進行處理,分情況討論:
1、區間[x, y]與[a, b] 無重疊,則不變換;
2、區間[x, y]與[a, b] 有部分重疊,則拿出來特殊處理;
最後從情況2的所有區間和[a, b]中找到一個區間的起始最小值、結束最大值,作爲新的區間。
但是這樣的代碼複雜度比較高,更簡潔的做法可以是:
1、把區間[a, b]放入n個區間中,按起始和結束位置從小到大排序;
2、如果區間i的起始位置<=區間i-1的結束位置,則認爲是一個區間;
bool cmp(Interval a, Interval b) {
if (a.start != b.start) {
return a.start < b.start;
}
else {
return b.end < a.end;
}
}
class Solution {
public:
vector<Interval> insert(vector<Interval>& intervals, Interval newInterval) {
intervals.push_back(newInterval);
if (intervals.empty()) return vector<Interval>{};
vector<Interval> ret;
sort(intervals.begin(), intervals.end(), cmp);
ret.push_back(intervals[0]);
for (int i = 1; i < intervals.size(); ++i) {
if (ret.back().end < intervals[i].start) { // 新的段
ret.push_back(intervals[i]);
}
else {
ret.back().end = max(ret.back().end, intervals[i].end);
}
}
return ret;
}
}leetcode;
複雜度解析:
方法1
時間複雜度是O(N)
空間複雜度是O(N)
方法2
時間複雜度是O(NLogN)
空間複雜度是O(N)
Word Break
題目鏈接
題目大意:
給出原串s,字符串數組dict,要求:
1、把s分成多個連續的子串;
2、每個子串都在dict裏面;
問,是否有解。
s = "leetcode",
dict = ["leet", "code"].
Return true because "leetcode" can be segmented as "leet code".
題目解析:
把一個串分成2個串的可能性有n種可能,n是字符串長度。
那麼對於串[l, r] 如果[l, k] 和 [k+1, r]是合法的,那麼[l, r]也是合法的。
故而用動態規劃:
dp[i][j] 表示字符串[i, j]是否爲合法的子串;
枚舉k∈[i, j] 來判斷分割字符串的位置;
轉移轉移是O(N),因爲需要判斷區間[i, k]和[k+1, j]是否合法(用字典數配合);
最後判斷dp[1, n]是否合法。
class Solution {
public:
bool dp[N];
bool wordBreak(string s, vector<string>& wordDict) {
unordered_set<string> wordSet;
for (int i = 0; i < wordDict.size(); ++i) {
wordSet.insert(wordDict[i]);
}
memset(dp, 0, sizeof(dp));
dp[0] = true;
for (int i = 1; i <= s.length(); ++i) {
for (int j = i - 1; j >= 0; --j) {
if (dp[j]) {
string substr = string(s.begin() + j, s.begin() + i);
if (wordSet.find(substr) != wordSet.end()) {
dp[i] = true;
break;
}
}
}
}
return dp[s.size()];
}
}leetcode;
複雜度解析:
時間複雜度
O(N^3) N^2的狀態* N的字典數判斷。
空間複雜度
O(N^2+M) N^2是狀態數量,M是字典數;
優化方案:
1、dp用1維表示;dp[i] 表示前i個是否合理,轉移的時候dp[i]=dp[k] && substr(k+1, i)
2、判斷substr是否存在時,可以用字典數;
Word Break II
題目鏈接
在前文Word Break的基礎上,輸出所有的解。
Input:
s = "catsanddog"
wordDict = ["cat", "cats", "and", "sand", "dog"]
Output:
[
"cats and dog",
"cat sand dog"
]
題目解析:
用vector來存可能的解,然後用dfs來輸出即可。
class Solution {
public:
vector<int> g[N];
vector<string> wordBreak(string s, vector<string>& wordDict) {
vector<string> ret;
unordered_set<string> wordSet;
for (int i = 0; i < wordDict.size(); ++i) {
wordSet.insert(wordDict[i]);
}
vector<bool> dp(s.length() + 1, false);
dp[0] = true;
for (int i = 1; i <= s.length(); ++i) {
for (int j = i - 1; j >= 0; --j) {
if (dp[j]) {
string substr = string(s.begin() + j, s.begin() + i);
if (wordSet.find(substr) != wordSet.end()) {
// cout << i << " " << j << " " << substr << endl;
dp[i] = true;
g[i].push_back(j);
}
}
}
}
vector<string> cur;
if (dp[s.length()]) {
dfs(s, ret, cur, s.length());
}
return ret;
}
void dfs(string &s, vector<string> &ret, vector<string> &cur, long n) {
for (int i = 0; i < g[n].size(); ++i) {
string str = string(s.begin() + g[n][i], s.begin() + n);
cur.push_back(str);
dfs(s, ret, cur, g[n][i]);
cur.pop_back();
}
if (n == 0) {
string str = cur.back();
for (int i = cur.size() - 2; i >= 0; --i) {
str += string(" ");
str += cur[i];
}
// cout << str << endl;
ret.push_back(str);
}
}
}leetcode;
LRU Cache
題目鏈接
題目大意:
實現一個最近最少使用的緩存算法,要求:
get(key) - 返回緩存中key對應的值,如果沒有存在緩存,返回-1;
set(key, value) - 設置緩存中的key對應的value;
緩存有固定大小。
題目解析:
緩存需要維護兩個信息,
1是key和value的對應;
2是value的有效時間;
時間是從小到大,每次會把一個大的值插入(新值),同時可能刪掉舊值;(命中)
那麼維護一個value的有效時間,優先隊列;
這種做法,單次操作的時間複雜度是O(LogN),和題目要求的O(1)有較大的差距;
O(1)表示存儲的數據結構只能用數組或者hash加鏈表的方式。
數組的讀取是O(1),但是增刪是O(N)的操作;
hash+鏈表的方式較爲符合題目的要求,可以實現大致O(1)的查找,也可以實現O(1)的增刪操作;
基於此數據結構,我們可以延伸出以下的解法:
1、使用雙向鏈表存儲每個key和value;
2、每次get、set已有節點時,把節點放到鏈表的最前面;
3、每次set的時候如果size已經達到限制,則去掉尾部節點,然後在頭部增加節點;
接下來的問題是如何實現O(1)的讀取,O(1)的大小判斷,以及O(1)的鏈表移動;
O(1)的讀取,我們引入unordered_map,然後每次根據key去獲取當前節點;(unordered_map 比 map 更快)
O(1)的增刪操作,我們通過list.splice函數實現;(因爲是雙向鏈表,O(1)的增刪並不難實現)
O(1)的大小判斷,list獲取size是O(N)的複雜度,所以我們引入一個變量curSize來記錄當前節點數量;
總結
從簡單的指針複製和區間重疊處理,再到分詞、LRU實現,LeetCode的題目更適合面試,這次的題目準備既是爲自己練習,也是爲了方便後續面試。