程序員進階之算法練習(三十二)LeetCode專場

前言

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的題目更適合面試,這次的題目準備既是爲自己練習,也是爲了方便後續面試。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章