diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json new file mode 100644 index 00000000..ccd81dff --- /dev/null +++ b/.devcontainer/devcontainer.json @@ -0,0 +1,7 @@ +{ + "name": "TeX Live", + "image": "soulmachine/texlive:latest", + "extensions": [ + "James-Yu.latex-workshop" + ] +} \ No newline at end of file diff --git a/.vscode/tasks.json b/.vscode/tasks.json new file mode 100644 index 00000000..72ac5ac3 --- /dev/null +++ b/.vscode/tasks.json @@ -0,0 +1,31 @@ +{ + "version": "2.0.0", + "tasks": [ + { + "label": "lettcode-C++", + "type": "shell", + "command": "xelatex", + "args": [ + "-synctex=1", + "-interaction=nonstopmode", + "leetcode-cpp.tex" + ], + "options": { + "cwd": "${workspaceFolder}/C++/" + } + }, + { + "label": "lettcode-Java", + "type": "shell", + "command": "xelatex", + "args": [ + "-synctex=1", + "-interaction=nonstopmode", + "leetcode-java.tex" + ], + "options": { + "cwd": "${workspaceFolder}/Java/" + } + } + ] +} \ No newline at end of file diff --git a/C++/.DS_Store b/C++/.DS_Store new file mode 100644 index 00000000..cd3a2cf8 Binary files /dev/null and b/C++/.DS_Store differ diff --git a/C++/README.md b/C++/README.md index 3e67d41b..a856af82 100644 --- a/C++/README.md +++ b/C++/README.md @@ -1,5 +1,7 @@ -#C++版 ------------------ -**下载**:LeetCode题解(C++版).pdf +# C++版 -书的内容与Java版一摸一样,不过代码是用C++写的。本书的代码使用 C++ 11 标准。 +## 编译 + +```bash +docker run -it --rm -v $(pwd):/project -w /project soulmachine/texlive xelatex -interaction=nonstopmode leetcode-cpp.tex +```` diff --git a/C++/chapBFS.tex b/C++/chapBFS.tex index 9ad2297d..826d7c4a 100644 --- a/C++/chapBFS.tex +++ b/C++/chapBFS.tex @@ -36,9 +36,99 @@ \subsubsection{描述} \subsubsection{分析} +求最短路径,用广搜。 -\subsubsection{代码} +\subsubsection{单队列} +\begin{Code} +//LeetCode, Word Ladder +// 时间复杂度O(n),空间复杂度O(n) +struct state_t { + string word; + int level; + + state_t() { word = ""; level = 0; } + state_t(const string& word, int level) { + this->word = word; + this->level = level; + } + + bool operator==(const state_t &other) const { + return this->word == other.word; + } +}; + +namespace std { + template<> struct hash { + public: + size_t operator()(const state_t& s) const { + return str_hash(s.word); + } + private: + std::hash str_hash; + }; +} + + +class Solution { +public: + int ladderLength(const string& start, const string &end, + const unordered_set &dict) { + queue q; + unordered_set visited; // 判重 + + auto state_is_valid = [&](const state_t& s) { + return dict.find(s.word) != dict.end() || s.word == end; + }; + auto state_is_target = [&](const state_t &s) {return s.word == end; }; + auto state_extend = [&](const state_t &s) { + unordered_set result; + + for (size_t i = 0; i < s.word.size(); ++i) { + state_t new_state(s.word, s.level + 1); + for (char c = 'a'; c <= 'z'; c++) { + // 防止同字母替换 + if (c == new_state.word[i]) continue; + + swap(c, new_state.word[i]); + + if (state_is_valid(new_state) && + visited.find(new_state) == visited.end()) { + result.insert(new_state); + } + swap(c, new_state.word[i]); // 恢复该单词 + } + } + + return result; + }; + + state_t start_state(start, 0); + q.push(start_state); + visited.insert(start_state); + while (!q.empty()) { + // 千万不能用 const auto&,pop() 会删除元素, + // 引用就变成了悬空引用 + const auto state = q.front(); + q.pop(); + + if (state_is_target(state)) { + return state.level + 1; + } + + const auto& new_states = state_extend(state); + for (const auto& new_state : new_states) { + q.push(new_state); + visited.insert(new_state); + } + } + return 0; + } +}; +\end{Code} + + +\subsubsection{双队列} \begin{Code} //LeetCode, Word Ladder // 时间复杂度O(n),空间复杂度O(n) @@ -49,24 +139,26 @@ \subsubsection{代码} queue current, next; // 当前层,下一层 unordered_set visited; // 判重 - int level = 0; // 层次 - bool found = false; + int level = -1; // 层次 + auto state_is_valid = [&](const string& s) { + return dict.find(s) != dict.end() || s == end; + }; auto state_is_target = [&](const string &s) {return s == end;}; auto state_extend = [&](const string &s) { - vector result; + unordered_set result; for (size_t i = 0; i < s.size(); ++i) { string new_word(s); for (char c = 'a'; c <= 'z'; c++) { + // 防止同字母替换 if (c == new_word[i]) continue; swap(c, new_word[i]); - if ((dict.count(new_word) > 0 || new_word == end) && - !visited.count(new_word)) { - result.push_back(new_word); - visited.insert(new_word); + if (state_is_valid(new_word) && + visited.find(new_word) == visited.end()) { + result.insert(new_word); } swap(c, new_word[i]); // 恢复该单词 } @@ -76,25 +168,28 @@ \subsubsection{代码} }; current.push(start); - while (!current.empty() && !found) { + visited.insert(start); + while (!current.empty()) { ++level; - while (!current.empty() && !found) { - const string str = current.front(); + while (!current.empty()) { + // 千万不能用 const auto&,pop() 会删除元素, + // 引用就变成了悬空引用 + const auto state = current.front(); current.pop(); - const auto& new_states = state_extend(str); - for (const auto& state : new_states) { - next.push(state); - if (state_is_target(state)) { - found = true; //找到了 - break; - } + if (state_is_target(state)) { + return level + 1; + } + + const auto& new_states = state_extend(state); + for (const auto& new_state : new_states) { + next.push(new_state); + visited.insert(new_state); } } swap(next, current); } - if (found) return level + 1; - else return 0; + return 0; } }; \end{Code} @@ -142,24 +237,178 @@ \subsubsection{描述} \subsubsection{分析} 跟 Word Ladder比,这题是求路径本身,不是路径长度,也是BFS,略微麻烦点。 -这题跟普通的广搜有很大的不同,就是要输出所有路径,因此在记录前驱和判重地方与普通广搜略有不同。 +求一条路径和求所有路径有很大的不同,求一条路径,每个状态节点只需要记录一个前驱即可;求所有路径时,有的状态节点可能有多个父节点,即要记录多个前驱。 +如果当前路径长度已经超过当前最短路径长度,可以中止对该路径的处理,因为我们要找的是最短路径。 -\subsubsection{代码} + +\subsubsection{单队列} \begin{Code} //LeetCode, Word Ladder II // 时间复杂度O(n),空间复杂度O(n) +struct state_t { + string word; + int level; + + state_t() { word = ""; level = 0; } + state_t(const string& word, int level) { + this->word = word; + this->level = level; + } + + bool operator==(const state_t &other) const { + return this->word == other.word; + } +}; + +namespace std { + template<> struct hash { + public: + size_t operator()(const state_t& s) const { + return str_hash(s.word); + } + private: + std::hash str_hash; + }; +} + + class Solution { public: - vector > findLadders(string start, string end, - const unordered_set &dict) { - unordered_set current, next; // 当前层,下一层,用集合是为了去重 + vector > findLadders(const string& start, + const string& end, const unordered_set &dict) { + queue q; + unordered_set visited; // 判重 + unordered_map > father; // DAG + + auto state_is_valid = [&](const state_t& s) { + return dict.find(s.word) != dict.end() || s.word == end; + }; + auto state_is_target = [&](const state_t &s) {return s.word == end; }; + auto state_extend = [&](const state_t &s) { + unordered_set result; + + for (size_t i = 0; i < s.word.size(); ++i) { + state_t new_state(s.word, s.level + 1); + for (char c = 'a'; c <= 'z'; c++) { + // 防止同字母替换 + if (c == new_state.word[i]) continue; + + swap(c, new_state.word[i]); + + if (state_is_valid(new_state)) { + auto visited_iter = visited.find(new_state); + + if (visited_iter != visited.end()) { + if (visited_iter->level < new_state.level) { + // do nothing + } else if (visited_iter->level == new_state.level) { + result.insert(new_state); + } else { // not possible + throw std::logic_error("not possible to get here"); + } + } else { + result.insert(new_state); + } + } + swap(c, new_state.word[i]); // 恢复该单词 + } + } + + return result; + }; + + vector> result; + state_t start_state(start, 0); + q.push(start_state); + visited.insert(start_state); + while (!q.empty()) { + // 千万不能用 const auto&,pop() 会删除元素, + // 引用就变成了悬空引用 + const auto state = q.front(); + q.pop(); + + // 如果当前路径长度已经超过当前最短路径长度, + // 可以中止对该路径的处理,因为我们要找的是最短路径 + if (!result.empty() && state.level + 1 > result[0].size()) break; + + if (state_is_target(state)) { + vector path; + gen_path(father, start_state, state, path, result); + continue; + } + // 必须挪到下面,比如同一层A和B两个节点均指向了目标节点, + // 那么目标节点就会在q中出现两次,输出路径就会翻倍 + // visited.insert(state); + + // 扩展节点 + const auto& new_states = state_extend(state); + for (const auto& new_state : new_states) { + if (visited.find(new_state) == visited.end()) { + q.push(new_state); + } + visited.insert(new_state); + father[new_state].push_back(state); + } + } + + return result; + } +private: + void gen_path(unordered_map > &father, + const state_t &start, const state_t &state, vector &path, + vector > &result) { + path.push_back(state.word); + if (state == start) { + if (!result.empty()) { + if (path.size() < result[0].size()) { + result.clear(); + result.push_back(path); + reverse(result.back().begin(), result.back().end()); + } else if (path.size() == result[0].size()) { + result.push_back(path); + reverse(result.back().begin(), result.back().end()); + } else { // not possible + throw std::logic_error("not possible to get here "); + } + } else { + result.push_back(path); + reverse(result.back().begin(), result.back().end()); + } + + } else { + for (const auto& f : father[state]) { + gen_path(father, start, f, path, result); + } + } + path.pop_back(); + } +}; +\end{Code} + + +\subsubsection{双队列} + +\begin{Code} +//LeetCode, Word Ladder II +// 时间复杂度O(n),空间复杂度O(n) +class Solution { +public: + vector > findLadders(const string& start, + const string& end, const unordered_set &dict) { + // 当前层,下一层,用unordered_set是为了去重,例如两个父节点指向 + // 同一个子节点,如果用vector, 子节点就会在next里出现两次,其实此 + // 时 father 已经记录了两个父节点,next里重复出现两次是没必要的 + unordered_set current, next; unordered_set visited; // 判重 - unordered_map > father; // 树 + unordered_map > father; // DAG - bool found = false; + int level = -1; // 层次 + auto state_is_valid = [&](const string& s) { + return dict.find(s) != dict.end() || s == end; + }; auto state_is_target = [&](const string &s) {return s == end;}; auto state_extend = [&](const string &s) { unordered_set result; @@ -167,12 +416,13 @@ \subsubsection{代码} for (size_t i = 0; i < s.size(); ++i) { string new_word(s); for (char c = 'a'; c <= 'z'; c++) { + // 防止同字母替换 if (c == new_word[i]) continue; swap(c, new_word[i]); - if ((dict.count(new_word) > 0 || new_word == end) && - !visited.count(new_word)) { + if (state_is_valid(new_word) && + visited.find(new_word) == visited.end()) { result.insert(new_word); } swap(c, new_word[i]); // 恢复该单词 @@ -182,29 +432,37 @@ \subsubsection{代码} return result; }; + vector > result; current.insert(start); - while (!current.empty() && !found) { - // 先将本层全部置为已访问,防止同层之间互相指向 - for (const auto& word : current) - visited.insert(word); - for (const auto& word : current) { - const auto new_states = state_extend(word); - for (const auto &state : new_states) { - if (state_is_target(state)) found = true; - next.insert(state); - father[state].push_back(word); - // visited.insert(state); // 移动到最上面了 + while (!current.empty()) { + ++ level; + // 如果当前路径长度已经超过当前最短路径长度,可以中止对该路径的 + // 处理,因为我们要找的是最短路径 + if (!result.empty() && level+1 > result[0].size()) break; + + // 1. 延迟加入visited, 这样才能允许两个父节点指向同一个子节点 + // 2. 一股脑current 全部加入visited, 是防止本层前一个节点扩展 + // 节点时,指向了本层后面尚未处理的节点,这条路径必然不是最短的 + for (const auto& state : current) + visited.insert(state); + for (const auto& state : current) { + if (state_is_target(state)) { + vector path; + gen_path(father, path, start, state, result); + continue; + } + + const auto new_states = state_extend(state); + for (const auto& new_state : new_states) { + next.insert(new_state); + father[new_state].push_back(state); } } current.clear(); swap(current, next); } - vector > result; - if (found) { - vector path; - gen_path(father, path, start, end, result); - } + return result; } private: @@ -213,7 +471,19 @@ \subsubsection{代码} vector > &result) { path.push_back(word); if (word == start) { - result.push_back(path); + if (!result.empty()) { + if (path.size() < result[0].size()) { + result.clear(); + result.push_back(path); + } else if(path.size() == result[0].size()) { + result.push_back(path); + } else { + // not possible + throw std::logic_error("not possible to get here"); + } + } else { + result.push_back(path); + } reverse(result.back().begin(), result.back().end()); } else { for (const auto& f : father[word]) { @@ -226,6 +496,139 @@ \subsubsection{代码} \end{Code} +\subsubsection{图的广搜} + +本题还可以看做是图上的广搜。给定了字典 \fn{dict},可以基于它画出一个无向图,表示单词之间可以互相转换。本题的本质就是已知起点和终点,在图上找出所有最短路径。 + +\begin{Code} +//LeetCode, Word Ladder II +// 时间复杂度O(n),空间复杂度O(n) +class Solution { +public: + vector > findLadders(const string& start, + const string &end, const unordered_set &dict) { + const auto& g = build_graph(dict); + vector pool; + queue q; // 未处理的节点 + // value 是所在层次 + unordered_map visited; + + auto state_is_target = [&](const state_t *s) {return s->word == end; }; + + vector> result; + q.push(create_state(nullptr, start, 0, pool)); + while (!q.empty()) { + state_t* state = q.front(); + q.pop(); + + // 如果当前路径长度已经超过当前最短路径长度, + // 可以中止对该路径的处理,因为我们要找的是最短路径 + if (!result.empty() && state->level+1 > result[0].size()) break; + + if (state_is_target(state)) { + const auto& path = gen_path(state); + if (result.empty()) { + result.push_back(path); + } else { + if (path.size() < result[0].size()) { + result.clear(); + result.push_back(path); + } else if (path.size() == result[0].size()) { + result.push_back(path); + } else { + // not possible + throw std::logic_error("not possible to get here"); + } + } + continue; + } + visited[state->word] = state->level; + + // 扩展节点 + auto iter = g.find(state->word); + if (iter == g.end()) continue; + + for (const auto& neighbor : iter->second) { + auto visited_iter = visited.find(neighbor); + + if (visited_iter != visited.end() && + visited_iter->second < state->level + 1) { + continue; + } + + q.push(create_state(state, neighbor, state->level + 1, pool)); + } + } + + // release all states + for (auto state : pool) { + delete state; + } + return result; + } + +private: + struct state_t { + state_t* father; + string word; + int level; // 所在层次,从0开始编号 + + state_t(state_t* father_, const string& word_, int level_) : + father(father_), word(word_), level(level_) {} + }; + + state_t* create_state(state_t* parent, const string& value, + int length, vector& pool) { + state_t* node = new state_t(parent, value, length); + pool.push_back(node); + + return node; + } + vector gen_path(const state_t* node) { + vector path; + + while(node != nullptr) { + path.push_back(node->word); + node = node->father; + } + + reverse(path.begin(), path.end()); + return path; + } + + unordered_map > build_graph( + const unordered_set& dict) { + unordered_map > adjacency_list; + + for (const auto& word : dict) { + for (size_t i = 0; i < word.size(); ++i) { + string new_word(word); + for (char c = 'a'; c <= 'z'; c++) { + // 防止同字母替换 + if (c == new_word[i]) continue; + + swap(c, new_word[i]); + + if ((dict.find(new_word) != dict.end())) { + auto iter = adjacency_list.find(word); + if (iter != adjacency_list.end()) { + iter->second.insert(new_word); + } else { + adjacency_list.insert(pair>(word, unordered_set())); + adjacency_list[word].insert(new_word); + } + } + swap(c, new_word[i]); // 恢复该单词 + } + } + } + return adjacency_list; + } +}; +\end{Code} + + \subsubsection{相关题目} \begindot @@ -296,7 +699,7 @@ \subsubsection{代码} const int m = board.size(); const int n = board[0].size(); - auto is_valid = [&](const state_t &s) { + auto state_is_valid = [&](const state_t &s) { const int x = s.first; const int y = s.second; if (x < 0 || x >= m || y < 0 || y >= n || board[x][y] != 'O') @@ -312,7 +715,7 @@ \subsubsection{代码} const state_t new_states[4] = {{x-1,y}, {x+1,y}, {x,y-1}, {x,y+1}}; for (int k = 0; k < 4; ++k) { - if (is_valid(new_states[k])) { + if (state_is_valid(new_states[k])) { // 既有标记功能又有去重功能 board[new_states[k].first][new_states[k].second] = '+'; result.push_back(new_states[k]); @@ -323,7 +726,7 @@ \subsubsection{代码} }; state_t start = { i, j }; - if (is_valid(start)) { + if (state_is_valid(start)) { board[i][j] = '+'; q.push(start); } @@ -353,7 +756,7 @@ \subsection{适用场景} \textbf{输入数据}:没什么特征,不像深搜,需要有“递归”的性质。如果是树或者图,概率更大。 -\textbf{状态转换图}:树或者图。 +\textbf{状态转换图}:树或者DAG图。 \textbf{求解目标}:求最短。 @@ -374,10 +777,16 @@ \subsection{思考的步骤} \item 如何扩展状态?这一步跟第2步相关。状态里记录的数据不同,扩展方法就不同。对于固定不变的数据结构(一般题目直接给出,作为输入数据),如二叉树,图等,扩展方法很简单,直接往下一层走,对于隐式图,要先在第1步里想清楚状态所带的数据,想清楚了这点,那如何扩展就很简单了。 -\item 关于判重,状态是否存在完美哈希方案?即将状态一一映射到整数,互相之间不会冲突。 +\item 如何判断重复?如果状态转换图是一颗树,则永远不会出现回路,不需要判重;如果状态转换图是一个图(这时候是一个图上的BFS),则需要判重。 \begin{enumerate} - \item 如果不存在,则需要使用通用的哈希表(自己实现或用标准库,例如\fn{unordered_set})来判重;自己实现哈希表的话,如果能够预估状态个数的上限,则可以开两个数组,head和next,表示哈希表,参考第 \S \ref{subsec:eightDigits}节方案2。 - \item 如果存在,则可以开一个大布尔数组,作为哈希表来判重,且此时可以精确计算出状态总数,而不仅仅是预估上限。 + \item 如果是求最短路径长度或一条路径,则只需要让“点”(即状态)不重复出现,即可保证不出现回路 + \item 如果是求所有路径,注意此时,状态转换图是DAG,即允许两个父节点指向同一个子节点。具体实现时,每个节点要\textbf{“延迟”}加入到已访问集合\fn{visited},要等一层全部访问完后,再加入到\fn{visited}集合。 + \item 具体实现 + \begin{enumerate} + \item 状态是否存在完美哈希方案?即将状态一一映射到整数,互相之间不会冲突。 + \item 如果不存在,则需要使用通用的哈希表(自己实现或用标准库,例如\fn{unordered_set})来判重;自己实现哈希表的话,如果能够预估状态个数的上限,则可以开两个数组,head和next,表示哈希表,参考第 \S \ref{subsec:eightDigits}节方案2。 + \item 如果存在,则可以开一个大布尔数组,来判重,且此时可以精确计算出状态总数,而不仅仅是预估上限。 + \end{enumerate} \end{enumerate} \item 目标状态是否已知?如果题目已经给出了目标状态,可以带来很大便利,这时候可以从起始状态出发,正向广搜;也可以从目标状态出发,逆向广搜;也可以同时出发,双向广搜。 @@ -389,8 +798,8 @@ \subsection{代码模板} 对于队列,可以用\fn{queue},也可以把\fn{vector}当做队列使用。当求长度时,有两种做法: \begin{enumerate} -\item 只用一个队列,但在状态结构体\fn{state_t}里增加一个整数字段\fn{step},表示走到当前状态用了多少步,当碰到目标状态,直接输出\fn{step}即可。这个方案,可以很方便的变成A*算法,把队列换成优先队列即可。 -\item 用两个队列,\fn{current, next},分别表示当前层次和下一层,另设一个全局整数\fn{level},表示层数(也即路径长度),当碰到目标状态,输出\fn{level}即可。这个方案,状态可以少一个字段,节省内存。 +\item 只用一个队列,但在状态结构体\fn{state_t}里增加一个整数字段\fn{level},表示当前所在的层次,当碰到目标状态,直接输出\fn{level}即可。这个方案,可以很容易的变成A*算法,把\fn{queue}替换为\fn{priority_queue}即可。 +\item 用两个队列,\fn{current, next},分别表示当前层次和下一层,另设一个全局整数\fn{level},表示层数(也即路径长度),当碰到目标状态,输出\fn{level}即可。这个方案,状态里可以不存路径长度,只需全局设置一个整数\fn{level},比较节省内存; \end{enumerate} 对于hashset,如果有完美哈希方案,用布尔数组(\fn{bool visited[STATE_MAX]}或\fn{vector visited(STATE_MAX, false)})来表示;如果没有,可以用STL里的\fn{set}或\fn{unordered_set}。 @@ -398,15 +807,16 @@ \subsection{代码模板} 对于树,如果用STL,可以用\fn{unordered_map father}表示一颗树,代码非常简洁。如果能够预估状态总数的上限(设为STATE_MAX),可以用数组(\fn{state_t nodes[STATE_MAX]}),即树的双亲表示法来表示树,效率更高,当然,需要写更多代码。 -\subsubsection{双队列的写法} -\begin{Codex}[label=bfs_template1.cpp] +\subsubsection{如何表示状态} + +\begin{Codex}[label=bfs_common.h] /** 状态 */ struct state_t { int data1; /** 状态的数据,可以有多个字段. */ int data2; /** 状态的数据,可以有多个字段. */ // dataN; /** 其他字段 */ int action; /** 由父状态移动到本状态的动作,求动作序列时需要. */ - int count; /** 所花费的步骤数(也即路径长度-1),求路径长度时需要; + int level; /** 所在的层次(从0开始),也即路径长度-1,求路径长度时需要; 不过,采用双队列时不需要本字段,只需全局设一个整数 */ bool operator==(const state_t &other) const { return true; // 根据具体问题实现 @@ -435,14 +845,12 @@ \subsubsection{双队列的写法} int m; // 存放外面传入的数据 }; - /** - * @brief 反向生成路径. + * @brief 反向生成路径,求一条路径. * @param[in] father 树 * @param[in] target 目标节点 * @return 从起点到target的路径 */ -template vector gen_path(const unordered_map &father, const state_t &target) { vector path; @@ -458,132 +866,334 @@ \subsubsection{双队列的写法} } /** - * @brief 广搜. - * @param[in] state_t 状态,如整数,字符串,一维数组等 + * 反向生成路径,求所有路径. + * @param[in] father 存放了所有路径的树 + * @param[in] start 起点 + * @param[in] state 终点 + * @return 从起点到终点的所有路径 + */ +void gen_path(unordered_map > &father, + const string &start, const state_t& state, vector &path, + vector > &result) { + path.push_back(state); + if (state == start) { + if (!result.empty()) { + if (path.size() < result[0].size()) { + result.clear(); + result.push_back(path); + } else if(path.size() == result[0].size()) { + result.push_back(path); + } else { + // not possible + throw std::logic_error("not possible to get here"); + } + } else { + result.push_back(path); + } + reverse(result.back().begin(), result.back().end()); + } else { + for (const auto& f : father[state]) { + gen_path(father, start, f, path, result); + } + } + path.pop_back(); +} +\end{Codex} + + +\subsubsection{求最短路径长度或一条路径} + +\textbf{单队列的写法} + +\begin{Codex}[label=bfs_template.cpp] +#include "bfs_common.h" + +/** + * @brief 广搜,只用一个队列. * @param[in] start 起点 - * @param[in] grid 输入数据 + * @param[in] data 输入数据 * @return 从起点到目标状态的一条最短路径 */ -template -vector bfs(const state_t &start, const vector> &grid) { +vector bfs(state_t &start, const vector> &grid) { + queue q; // 队列 + unordered_set visited; // 判重 + unordered_map father; // 树,求路径本身时才需要 + + // 判断状态是否合法 + auto state_is_valid = [&](const state_t &s) { /*...*/ }; + + // 判断当前状态是否为所求目标 + auto state_is_target = [&](const state_t &s) { /*...*/ }; + + // 扩展当前状态 + auto state_extend = [&](const state_t &s) { + unordered_set result; + for (/*...*/) { + const state_t new_state = /*...*/; + if (state_is_valid(new_state) && + visited.find(new_state) != visited.end()) { + result.insert(new_state); + } + } + return result; + }; + + assert (start.level == 0); + q.push(start); + while (!q.empty()) { + // 千万不能用 const auto&,pop() 会删除元素, + // 引用就变成了悬空引用 + const state_t state = q.front(); + q.pop(); + visited.insert(state); + + // 访问节点 + if (state_is_target(state)) { + return return gen_path(father, target); // 求一条路径 + // return state.level + 1; // 求路径长度 + } + + // 扩展节点 + vector new_states = state_extend(state); + for (const auto& new_state : new_states) { + q.push(new_state); + father[new_state] = state; // 求一条路径 + // visited.insert(state); // 优化:可以提前加入 visited 集合, + // 从而缩小状态扩展。这时 q 的含义略有变化,里面存放的是处理了一半 + // 的节点:已经加入了visited,但还没有扩展。别忘记 while循环开始 + // 前,要加一行代码, visited.insert(start) + } + } + + return vector(); + //return 0; +} +\end{Codex} + + +\textbf{双队列的写法} +\begin{Codex}[label=bfs_template1.cpp] +#include "bfs_common.h" + +/** + * @brief 广搜,使用两个队列. + * @param[in] start 起点 + * @param[in] data 输入数据 + * @return 从起点到目标状态的一条最短路径 + */ +vector bfs(const state_t &start, const type& data) { queue next, current; // 当前层,下一层 unordered_set visited; // 判重 - unordered_map father; // 树 + unordered_map father; // 树,求路径本身时才需要 + + int level = -1; // 层次 - int level = 0; // 层次 - bool found = false; // 是否找到目标 - state_t target; // 符合条件的目标状态 + // 判断状态是否合法 + auto state_is_valid = [&](const state_t &s) { /*...*/ }; // 判断当前状态是否为所求目标 - auto state_is_target = [&](const state_t &s) {return true; }; + auto state_is_target = [&](const state_t &s) { /*...*/ }; + // 扩展当前状态 auto state_extend = [&](const state_t &s) { - vector result; - // ... + unordered_set result; + for (/*...*/) { + const state_t new_state = /*...*/; + if (state_is_valid(new_state) && + visited.find(new_state) != visited.end()) { + result.insert(new_state); + } + } return result; }; current.push(start); - visited.insert(start); - while (!current.empty() && !found) { + while (!current.empty()) { ++level; - while (!current.empty() && !found) { - const state_t state = current.front(); + while (!current.empty()) { + // 千万不能用 const auto&,pop() 会删除元素, + // 引用就变成了悬空引用 + const auto state = current.front(); current.pop(); - vector new_states = state_extend(state); - for (auto iter = new_states.cbegin(); - iter != new_states.cend() && ! found; ++iter) { - const state_t new_state(*iter); - - if (state_is_target(new_state)) { - found = true; //找到了 - target = new_state; - father[new_state] = state; - break; - } + visited.insert(state); + if (state_is_target(state)) { + return return gen_path(father, state); // 求一条路径 + // return state.level + 1; // 求路径长度 + } + + const auto& new_states = state_extend(state); + for (const auto& new_state : new_states) { next.push(new_state); - // visited.insert(new_state); 必须放到 state_extend()里 father[new_state] = state; + // visited.insert(state); // 优化:可以提前加入 visited 集合, + // 从而缩小状态扩展。这时 current 的含义略有变化,里面存放的是处 + // 理了一半的节点:已经加入了visited,但还没有扩展。别忘记 while + // 循环开始前,要加一行代码, visited.insert(start) } } swap(next, current); //!!! 交换两个队列 } - if (found) { - return gen_path(father, target); - //return level + 1; - } else { - return vector(); - //return 0; + return vector(); + // return 0; +} +\end{Codex} + + +\subsubsection{求所有路径} + +\textbf{单队列} + +\begin{Codex}[label=bfs_template.cpp] +/** + * @brief 广搜,使用一个队列. + * @param[in] start 起点 + * @param[in] data 输入数据 + * @return 从起点到目标状态的所有最短路径 + */ +vector > bfs(const state_t &start, const type& data) { + queue q; + unordered_set visited; // 判重 + unordered_map > father; // DAG + + auto state_is_valid = [&](const state_t& s) { /*...*/ }; + auto state_is_target = [&](const state_t &s) { /*...*/ }; + auto state_extend = [&](const state_t &s) { + unordered_set result; + for (/*...*/) { + const state_t new_state = /*...*/; + if (state_is_valid(new_state)) { + auto visited_iter = visited.find(new_state); + + if (visited_iter != visited.end()) { + if (visited_iter->level < new_state.level) { + // do nothing + } else if (visited_iter->level == new_state.level) { + result.insert(new_state); + } else { // not possible + throw std::logic_error("not possible to get here"); + } + } else { + result.insert(new_state); + } + } + } + + return result; + }; + + vector> result; + state_t start_state(start, 0); + q.push(start_state); + visited.insert(start_state); + while (!q.empty()) { + // 千万不能用 const auto&,pop() 会删除元素, + // 引用就变成了悬空引用 + const auto state = q.front(); + q.pop(); + + // 如果当前路径长度已经超过当前最短路径长度, + // 可以中止对该路径的处理,因为我们要找的是最短路径 + if (!result.empty() && state.level + 1 > result[0].size()) break; + + if (state_is_target(state)) { + vector path; + gen_path(father, start_state, state, path, result); + continue; + } + // 必须挪到下面,比如同一层A和B两个节点均指向了目标节点, + // 那么目标节点就会在q中出现两次,输出路径就会翻倍 + // visited.insert(state); + + // 扩展节点 + const auto& new_states = state_extend(state); + for (const auto& new_state : new_states) { + if (visited.find(new_state) == visited.end()) { + q.push(new_state); + } + visited.insert(new_state); + father[new_state].push_back(state); + } } + + return result; } \end{Codex} -\subsubsection{只用一个队列的写法} -双队列的写法,当求路径长度时,不需要在状态里设置一个\fn{count}字段记录路径长度,只需全局设置一个整数\fn{level},比较节省内存;只用一个队列的写法,当求路径长度时,需要在状态里设置一个\fn{count}字段,不过,这种写法有一个好处 —— 可以很容易的变为A*算法,把\fn{queue}替换为\fn{priority_queue}即可。 +\textbf{双队列的写法} -\begin{Codex}[label=bfs_template2.cpp] -// 与模板1相同的部分,不再重复 -// ... +\begin{Codex}[label=bfs_template.cpp] +#include "bfs_common.h" /** - * @brief 广搜. - * @param[in] state_t 状态,如整数,字符串,一维数组等 + * @brief 广搜,使用两个队列. * @param[in] start 起点 - * @param[in] grid 输入数据 - * @return 从起点到目标状态的一条最短路径 + * @param[in] data 输入数据 + * @return 从起点到目标状态的所有最短路径 */ -template -vector bfs(state_t &start, const vector> &grid) { - queue q; // 队列 +vector > bfs(const state_t &start, const type& data) { + // 当前层,下一层,用unordered_set是为了去重,例如两个父节点指向 + // 同一个子节点,如果用vector, 子节点就会在next里出现两次,其实此 + // 时 father 已经记录了两个父节点,next里重复出现两次是没必要的 + unordered_set current, next; unordered_set visited; // 判重 - unordered_map father; // 树 + unordered_map > father; // DAG + + int level = -1; // 层次 - int level = 0; // 层次 - bool found = false; // 是否找到目标 - state_t target; // 符合条件的目标状态 + // 判断状态是否合法 + auto state_is_valid = [&](const state_t &s) { /*...*/ }; // 判断当前状态是否为所求目标 - auto state_is_target = [&](const state_t &s) {return true; }; + auto state_is_target = [&](const state_t &s) { /*...*/ }; + // 扩展当前状态 auto state_extend = [&](const state_t &s) { - vector result; - // ... + unordered_set result; + for (/*...*/) { + const state_t new_state = /*...*/; + if (state_is_valid(new_state) && + visited.find(new_state) != visited.end()) { + result.insert(new_state); + } + } return result; }; - start.count = 0; - q.push(start); - visited.insert(start); - while (!q.empty() && !found) { - const state_t state = q.front(); - q.pop(); - vector new_states = state_extend(state); - for (auto iter = new_states.cbegin(); - iter != new_states.cend() && ! found; ++iter) { - const state_t new_state(*iter); - - if (state_is_target(new_state)) { - found = true; //找到了 - target = new_state; - father[new_state] = state; - break; + vector > result; + current.insert(start); + while (!current.empty()) { + ++ level; + // 如果当前路径长度已经超过当前最短路径长度,可以中止对该路径的 + // 处理,因为我们要找的是最短路径 + if (!result.empty() && level+1 > result[0].size()) break; + + // 1. 延迟加入visited, 这样才能允许两个父节点指向同一个子节点 + // 2. 一股脑current 全部加入visited, 是防止本层前一个节点扩展 + // 节点时,指向了本层后面尚未处理的节点,这条路径必然不是最短的 + for (const auto& state : current) + visited.insert(state); + for (const auto& state : current) { + if (state_is_target(state)) { + vector path; + gen_path(father, path, start, state, result); + continue; } - q.push(new_state); - // visited.insert(new_state); 必须放到 state_extend()里 - father[new_state] = state; + const auto new_states = state_extend(state); + for (const auto& new_state : new_states) { + next.insert(new_state); + father[new_state].push_back(state); + } } - } - if (found) { - return gen_path(father, target); - //return level + 1; - } else { - return vector(); - //return 0; + current.clear(); + swap(current, next); } + + return result; } \end{Codex} + diff --git a/C++/chapBruteforce.tex b/C++/chapBruteforce.tex index 08014864..4c9e4037 100644 --- a/C++/chapBruteforce.tex +++ b/C++/chapBruteforce.tex @@ -439,16 +439,16 @@ \subsubsection{代码} // 时间复杂度O(n!),空间复杂度O(1) class Solution { public: - vector> permute(vector& num) { + vector > permute(vector &num) { + vector > result; sort(num.begin(), num.end()); - vector> permutations; - do { - permutations.push_back(num); - } while (next_permutation(num.begin(), num.end())); // 见第2.1节 - - return permutations; + result.push_back(num); + // 调用的是 2.1.12 节的 next_permutation() + // 而不是 std::next_permutation() + } while(next_permutation(num.begin(), num.end())); + return result; } }; \end{Code} @@ -733,6 +733,7 @@ \subsection{递归} vector letterCombinations (const string &digits) { vector result; + if (digits.empty()) return result; dfs(digits, 0, "", result); return result; } @@ -761,6 +762,7 @@ \subsection{迭代} "ghi", "jkl", "mno", "pqrs", "tuv", "wxyz" }; vector letterCombinations (const string &digits) { + if (digits.empty()) return vector(); vector result(1, ""); for (auto d : digits) { const size_t n = result.size(); diff --git a/C++/chapDFS.tex b/C++/chapDFS.tex index 87c4c7bf..f6c4c107 100644 --- a/C++/chapDFS.tex +++ b/C++/chapDFS.tex @@ -39,8 +39,7 @@ \subsubsection{深搜1} return result; } - // s[0, prev-1]之间已经处理,保证是回文串 - // prev 表示s[prev-1]与s[prev]之间的空隙位置,start同理 + // prev 表示前一个隔板, start 表示当前隔板 void dfs(string &s, vector& path, vector> &result, size_t prev, size_t start) { if (start == s.size()) { // 最后一个隔板 @@ -204,24 +203,24 @@ \subsubsection{代码} class Solution { public: int uniquePaths(int m, int n) { - // 0行和0列未使用 - this->f = vector >(m + 1, vector(n + 1, 0)); - return dfs(m, n); + // f[x][y] 表示 从(0,0)到(x,y)的路径条数 + f = vector >(m, vector(n, 0)); + f[0][0] = 1; + return dfs(m - 1, n - 1); } private: vector > f; // 缓存 int dfs(int x, int y) { - if (x < 1 || y < 1) return 0; // 数据非法,终止条件 + if (x < 0 || y < 0) return 0; // 数据非法,终止条件 - if (x == 1 && y == 1) return 1; // 回到起点,收敛条件 + if (x == 0 && y == 0) return f[0][0]; // 回到起点,收敛条件 - return getOrUpdate(x - 1, y) + getOrUpdate(x, y - 1); - } - - int getOrUpdate(int x, int y) { - if (f[x][y] > 0) return f[x][y]; - else return f[x][y] = dfs(x, y); + if (f[x][y] > 0) { + return f[x][y]; + } else { + return f[x][y] = dfs(x - 1, y) + dfs(x, y - 1); + } } }; \end{Code} @@ -248,9 +247,9 @@ \subsubsection{代码} f[0] = 1; for (int i = 0; i < m; i++) { for (int j = 1; j < n; j++) { - // 左边的f[j],表示更新后的f[j],与公式中的f[i[[j]对应 + // 左边的f[j],表示更新后的f[j],与公式中的f[i][j]对应 // 右边的f[j],表示老的f[j],与公式中的f[i-1][j]对应 - f[j] = f[j - 1] + f[j]; + f[j] = f[j] + f[j - 1]; } } return f[n - 1]; @@ -338,40 +337,41 @@ \subsubsection{代码} // 深搜 + 缓存,即备忘录法 class Solution { public: - int uniquePathsWithObstacles(vector > &obstacleGrid) { + int uniquePathsWithObstacles(const vector >& obstacleGrid) { const int m = obstacleGrid.size(); const int n = obstacleGrid[0].size(); - // 0行和0列未使用 - this->f = vector >(m + 1, vector(n + 1, 0)); - return dfs(obstacleGrid, m, n); + if (obstacleGrid[0][0] || obstacleGrid[m - 1][n - 1]) return 0; + + f = vector >(m, vector(n, 0)); + f[0][0] = obstacleGrid[0][0] ? 0 : 1; + return dfs(obstacleGrid, m - 1, n - 1); } private: vector > f; // 缓存 - int dfs(const vector > &obstacleGrid, + // @return 从 (0, 0) 到 (x, y) 的路径总数 + int dfs(const vector >& obstacleGrid, int x, int y) { - if (x < 1 || y < 1) return 0; // 数据非法,终止条件 + if (x < 0 || y < 0) return 0; // 数据非法,终止条件 // (x,y)是障碍 - if (obstacleGrid[x-1][y-1]) return 0; - - if (x == 1 and y == 1) return 1; // 回到起点,收敛条件 + if (obstacleGrid[x][y]) return 0; - return getOrUpdate(obstacleGrid, x - 1, y) + - getOrUpdate(obstacleGrid, x, y - 1); - } + if (x == 0 and y == 0) return f[0][0]; // 回到起点,收敛条件 - int getOrUpdate(const vector > &obstacleGrid, - int x, int y) { - if (f[x][y] > 0) return f[x][y]; - else return f[x][y] = dfs(obstacleGrid, x, y); + if (f[x][y] > 0) { + return f[x][y]; + } else { + return f[x][y] = dfs(obstacleGrid, x - 1, y) + + dfs(obstacleGrid, x, y - 1); + } } }; \end{Code} \subsection{动规} -与上一题类似,但要特别注意第一列的障碍。在上一题中,第一列全部是1,但是在这一题中不同,第一列如果某一行有障碍物,那么后面的行应该为0。 +与上一题类似,但要特别注意第一列的障碍。在上一题中,第一列全部是1,但是在这一题中不同,第一列如果某一行有障碍物,那么后面的行全为0。 \subsubsection{代码} @@ -389,9 +389,11 @@ \subsubsection{代码} vector f(n, 0); f[0] = obstacleGrid[0][0] ? 0 : 1; - for (int i = 0; i < m; i++) - for (int j = 0; j < n; j++) - f[j] = obstacleGrid[i][j] ? 0 : (j == 0 ? 0 : f[j - 1]) + f[j]; + for (int i = 0; i < m; i++) { + f[0] = f[0] == 0 ? 0 : (obstacleGrid[i][0] ? 0 : 1); + for (int j = 1; j < n; j++) + f[j] = obstacleGrid[i][j] ? 0 : (f[j] + f[j - 1]); + } return f[n - 1]; } @@ -440,9 +442,74 @@ \subsubsection{描述} \subsubsection{分析} + 经典的深搜题。 -\subsubsection{代码} +设置一个数组 \fn{vector C(n, 0)}, \fn{C[i]} 表示第i行皇后所在的列编号,即在位置 (i, C[i]) 上放了一个皇后,这样用一个一维数组,就能记录整个棋盘。 + + +\subsubsection{代码1} +\begin{Code} +// LeetCode, N-Queens +// 深搜+剪枝 +// 时间复杂度O(n!*n),空间复杂度O(n) +class Solution { +public: + vector > solveNQueens(int n) { + vector > result; + vector C(n, -1); // C[i]表示第i行皇后所在的列编号 + dfs(C, result, 0); + return result; + } +private: + void dfs(vector &C, vector > &result, int row) { + const int N = C.size(); + if (row == N) { // 终止条件,也是收敛条件,意味着找到了一个可行解 + vector solution; + for (int i = 0; i < N; ++i) { + string s(N, '.'); + for (int j = 0; j < N; ++j) { + if (j == C[i]) s[j] = 'Q'; + } + solution.push_back(s); + } + result.push_back(solution); + return; + } + + for (int j = 0; j < N; ++j) { // 扩展状态,一列一列的试 + const bool ok = isValid(C, row, j); + if (!ok) continue; // 剪枝,如果非法,继续尝试下一列 + // 执行扩展动作 + C[row] = j; + dfs(C, result, row + 1); + // 撤销动作 + // C[row] = -1; + } + } + + /** + * 能否在 (row, col) 位置放一个皇后. + * + * @param C 棋局 + * @param row 当前正在处理的行,前面的行都已经放了皇后了 + * @param col 当前列 + * @return 能否放一个皇后 + */ + bool isValid(const vector &C, int row, int col) { + for (int i = 0; i < row; ++i) { + // 在同一列 + if (C[i] == col) return false; + // 在同一对角线上 + if (abs(i - row) == abs(C[i] - col)) return false; + } + return true; + } +}; +\end{Code} + + +\subsubsection{代码2} \begin{Code} // LeetCode, N-Queens // 深搜+剪枝 @@ -450,20 +517,20 @@ \subsubsection{代码} class Solution { public: vector > solveNQueens(int n) { - this->columns = vector(n, 0); - this->main_diag = vector(2 * n, 0); - this->anti_diag = vector(2 * n, 0); + this->columns = vector(n, false); + this->main_diag = vector(2 * n - 1, false); + this->anti_diag = vector(2 * n - 1, false); vector > result; - vector C(n, 0); // C[i]表示第i行皇后所在的列编号 + vector C(n, -1); // C[i]表示第i行皇后所在的列编号 dfs(C, result, 0); return result; } private: // 这三个变量用于剪枝 - vector columns; // 表示已经放置的皇后占据了哪些列 - vector main_diag; // 占据了哪些主对角线 - vector anti_diag; // 占据了哪些副对角线 + vector columns; // 表示已经放置的皇后占据了哪些列 + vector main_diag; // 占据了哪些主对角线 + vector anti_diag; // 占据了哪些副对角线 void dfs(vector &C, vector > &result, int row) { const int N = C.size(); @@ -481,16 +548,16 @@ \subsubsection{代码} } for (int j = 0; j < N; ++j) { // 扩展状态,一列一列的试 - const bool ok = columns[j] == 0 && main_diag[row + j] == 0 && - anti_diag[row - j + N] == 0; - if (!ok) continue; // 剪枝:如果合法,继续递归 + const bool ok = !columns[j] && !main_diag[row - j + N - 1] && + !anti_diag[row + j]; + if (!ok) continue; // 剪枝,如果非法,继续尝试下一列 // 执行扩展动作 C[row] = j; - columns[j] = main_diag[row + j] = anti_diag[row - j + N] = 1; + columns[j] = main_diag[row - j + N - 1] = anti_diag[row + j] = true; dfs(C, result, row + 1); // 撤销动作 - // C[row] = 0; - columns[j] = main_diag[row + j] = anti_diag[row - j + N] = 0; + // C[row] = -1; + columns[j] = main_diag[row - j + N - 1] = anti_diag[row + j] = false; } } }; @@ -517,7 +584,62 @@ \subsubsection{分析} 只需要输出解的个数,不需要输出所有解,代码要比上一题简化很多。设一个全局计数器,每找到一个解就增1。 -\subsubsection{代码} +\subsubsection{代码1} +\begin{Code} +// LeetCode, N-Queens II +// 深搜+剪枝 +// 时间复杂度O(n!*n),空间复杂度O(n) +class Solution { +public: + int totalNQueens(int n) { + this->count = 0; + + vector C(n, 0); // C[i]表示第i行皇后所在的列编号 + dfs(C, 0); + return this->count; + } +private: + int count; // 解的个数 + + void dfs(vector &C, int row) { + const int N = C.size(); + if (row == N) { // 终止条件,也是收敛条件,意味着找到了一个可行解 + ++this->count; + return; + } + + for (int j = 0; j < N; ++j) { // 扩展状态,一列一列的试 + const bool ok = isValid(C, row, j); + if (!ok) continue; // 剪枝:如果合法,继续递归 + // 执行扩展动作 + C[row] = j; + dfs(C, row + 1); + // 撤销动作 + // C[row] = -1; + } + } + /** + * 能否在 (row, col) 位置放一个皇后. + * + * @param C 棋局 + * @param row 当前正在处理的行,前面的行都已经放了皇后了 + * @param col 当前列 + * @return 能否放一个皇后 + */ + bool isValid(const vector &C, int row, int col) { + for (int i = 0; i < row; ++i) { + // 在同一列 + if (C[i] == col) return false; + // 在同一对角线上 + if (abs(i - row) == abs(C[i] - col)) return false; + } + return true; + } +}; +\end{Code} + + +\subsubsection{代码2} \begin{Code} // LeetCode, N-Queens II // 深搜+剪枝 @@ -526,9 +648,9 @@ \subsubsection{代码} public: int totalNQueens(int n) { this->count = 0; - this->columns = vector(n, 0); - this->main_diag = vector(2 * n, 0); - this->anti_diag = vector(2 * n, 0); + this->columns = vector(n, false); + this->main_diag = vector(2 * n - 1, false); + this->anti_diag = vector(2 * n - 1, false); vector C(n, 0); // C[i]表示第i行皇后所在的列编号 dfs(C, 0); @@ -537,9 +659,9 @@ \subsubsection{代码} private: int count; // 解的个数 // 这三个变量用于剪枝 - vector columns; // 表示已经放置的皇后占据了哪些列 - vector main_diag; // 占据了哪些主对角线 - vector anti_diag; // 占据了哪些副对角线 + vector columns; // 表示已经放置的皇后占据了哪些列 + vector main_diag; // 占据了哪些主对角线 + vector anti_diag; // 占据了哪些副对角线 void dfs(vector &C, int row) { const int N = C.size(); @@ -549,19 +671,19 @@ \subsubsection{代码} } for (int j = 0; j < N; ++j) { // 扩展状态,一列一列的试 - const bool ok = columns[j] == 0 && - main_diag[row + j] == 0 && - anti_diag[row - j + N] == 0; + const bool ok = !columns[j] && + !main_diag[row - j + N] && + !anti_diag[row + j]; if (!ok) continue; // 剪枝:如果合法,继续递归 // 执行扩展动作 C[row] = j; - columns[j] = main_diag[row + j] = - anti_diag[row - j + N] = 1; + columns[j] = main_diag[row - j + N] = + anti_diag[row + j] = true; dfs(C, row + 1); // 撤销动作 - // C[row] = 0; - columns[j] = main_diag[row + j] = - anti_diag[row - j + N] = 0; + // C[row] = -1; + columns[j] = main_diag[row - j + N] = + anti_diag[row + j] = false; } } }; @@ -597,43 +719,43 @@ \subsubsection{代码} // 时间复杂度O(n^4),空间复杂度O(n) class Solution { public: - vector restoreIpAddresses(string s) { + vector restoreIpAddresses(const string& s) { vector result; - string ip; // 存放中间结果 - dfs(s, 0, 0, ip, result); + vector ip; // 存放中间结果 + dfs(s, ip, result, 0); return result; } /** * @brief 解析字符串 * @param[in] s 字符串,输入数据 - * @param[in] startIndex 从s的哪里开始 - * @param[in] step 当前步骤编号,从0开始编号,取值为0,1,2,3,4表示结束了 - * @param[out] intermediate 当前解析出来的中间结果 + * @param[out] ip 存放中间结果 * @param[out] result 存放所有可能的IP地址 + * @param[in] start 当前正在处理的 index * @return 无 */ - void dfs(string s, size_t start, size_t step, string ip, - vector &result) { - if (start == s.size() && step == 4) { // 找到一个合法解 - ip.resize(ip.size() - 1); - result.push_back(ip); + void dfs(string s, vector& ip, vector &result, + size_t start) { + if (ip.size() == 4 && start == s.size()) { // 找到一个合法解 + result.push_back(ip[0] + '.' + ip[1] + '.' + ip[2] + '.' + ip[3]); return; } - if (s.size() - start > (4 - step) * 3) + if (s.size() - start > (4 - ip.size()) * 3) return; // 剪枝 - if (s.size() - start < (4 - step)) + if (s.size() - start < (4 - ip.size())) return; // 剪枝 int num = 0; for (size_t i = start; i < start + 3; i++) { num = num * 10 + (s[i] - '0'); - if (num <= 255) { // 当前结点合法,则继续往下递归 - ip += s[i]; - dfs(s, i + 1, step + 1, ip + '.', result); - } + if (num < 0 || num > 255) continue; // 剪枝 + + ip.push_back(s.substr(start, i - start + 1)); + dfs(s, ip, result, i + 1); + ip.pop_back(); + if (num == 0) break; // 不允许前缀0,但允许单个0 } } @@ -684,24 +806,24 @@ \subsubsection{代码} vector > combinationSum(vector &nums, int target) { sort(nums.begin(), nums.end()); vector > result; // 最终结果 - vector intermediate; // 中间结果 - dfs(nums, target, 0, intermediate, result); + vector path; // 中间结果 + dfs(nums, path, result, target, 0); return result; } private: - void dfs(vector& nums, int gap, int start, vector& intermediate, - vector > &result) { + void dfs(vector& nums, vector& path, vector > &result, + int gap, int start) { if (gap == 0) { // 找到一个合法解 - result.push_back(intermediate); + result.push_back(path); return; } for (size_t i = start; i < nums.size(); i++) { // 扩展状态 if (gap < nums[i]) return; // 剪枝 - intermediate.push_back(nums[i]); // 执行扩展动作 - dfs(nums, gap - nums[i], i, intermediate, result); - intermediate.pop_back(); // 撤销动作 + path.push_back(nums[i]); // 执行扩展动作 + dfs(nums, path, result, gap - nums[i], i); + path.pop_back(); // 撤销动作 } } }; @@ -719,9 +841,9 @@ \section{Combination Sum II} %%%%%%%%%%%%%%%%%%%%%%%%%%%%%% \subsubsection{描述} -Given a set of candidate numbers ($C$) and a target number ($T$), find all unique combinations in $C$ where the candidate numbers sums to $T$. +Given a collection of candidate numbers ($C$) and a target number ($T$), find all unique combinations in $C$ where the candidate numbers sums to $T$. -The same repeated number may be chosen from $C$ \emph{once} number of times. +Each number in $C$ may only be used \emph{once} in the combination. Note: \begindot @@ -752,24 +874,24 @@ \subsubsection{代码} public: vector > combinationSum2(vector &nums, int target) { sort(nums.begin(), nums.end()); // 跟第 50 行配合, - // 确保每个元素最多只用一次 + // 确保每个元素最多只用一次 vector > result; - vector intermediate; - dfs(nums, target, 0, intermediate, result); + vector path; + dfs(nums, path, result, target, 0); return result; } private: // 使用nums[start, nums.size())之间的元素,能找到的所有可行解 - static void dfs(vector &nums, int gap, int start, - vector &intermediate, vector > &result) { + static void dfs(const vector &nums, vector &path, + vector > &result, int gap, int start) { if (gap == 0) { // 找到一个合法解 - result.push_back(intermediate); + result.push_back(path); return; } int previous = -1; for (size_t i = start; i < nums.size(); i++) { - // 如果上一轮循环没有选nums[i],则本次循环就不能再选nums[i], + // 如果上一轮循环已经使用了nums[i],则本次循环就不能再选nums[i], // 确保nums[i]最多只用一次 if (previous == nums[i]) continue; @@ -777,9 +899,9 @@ \subsubsection{代码} previous = nums[i]; - intermediate.push_back(nums[i]); - dfs(nums, gap - nums[i], i + 1, intermediate, result); - intermediate.pop_back(); // 恢复环境 + path.push_back(nums[i]); + dfs(nums, path, result, gap - nums[i], i + 1); + path.pop_back(); // 恢复环境 } } }; @@ -818,17 +940,27 @@ \subsubsection{代码1} public: vector generateParenthesis(int n) { vector result; - if (n > 0) generate(n, "", 0, 0, result); + string path; + if (n > 0) generate(n, path, result, 0, 0); return result; } // l 表示 ( 出现的次数, r 表示 ) 出现的次数 - void generate(int n, string s, int l, int r, vector &result) { + void generate(int n, string& path, vector &result, int l, int r) { if (l == n) { + string s(path); result.push_back(s.append(n - r, ')')); return; } - generate(n, s + '(', l + 1, r, result); - if (l > r) generate(n, s + ")", l, r + 1, result); + + path.push_back('('); + generate(n, path, result, l + 1, r); + path.pop_back(); + + if (l > r) { + path.push_back(')'); + generate(n, path, result, l, r + 1); + path.pop_back(); + } } }; \end{Code} @@ -971,7 +1103,7 @@ \subsubsection{代码} // 时间复杂度O(n^2*m^2),空间复杂度O(n^2) class Solution { public: - bool exist(vector > &board, string word) { + bool exist(const vector > &board, const string& word) { const int m = board.size(); const int n = board[0].size(); vector > visited(m, vector(n, false)); @@ -1039,12 +1171,6 @@ \subsection{思考的步骤} \item 如何扩展状态?这一步跟上一步相关。状态里记录的数据不同,扩展方法就不同。对于固定不变的数据结构(一般题目直接给出,作为输入数据),如二叉树,图等,扩展方法很简单,直接往下一层走,对于隐式图,要先在第1步里想清楚状态所带的数据,想清楚了这点,那如何扩展就很简单了。 -\item 关于判重 - \begin{enumerate} - \item 是否需要判重?如果状态转换图是一棵树,则不需要判重,因为在遍历过程中不可能重复;如果状态转换图是一个DAG,则需要判重。这一点跟BFS不一样,BFS的状态转换图总是DAG,必须要判重。 - \item 怎样判重?跟广搜相同,见第 \S \ref{sec:bfs-template} 节。同时,DAG说明存在重叠子问题,此时可以用缓存加速,见第8步。 - \end{enumerate} - \item 终止条件是什么?终止条件是指到了不能扩展的末端节点。对于树,是叶子节点,对于图或隐式图,是出度为0的节点。 \item {收敛条件是什么?收敛条件是指找到了一个合法解的时刻。如果是正向深搜(父状态处理完了才进行递归,即父状态不依赖子状态,递归语句一定是在最后,尾递归),则是指是否达到目标状态;如果是逆向深搜(处理父状态时需要先知道子状态的结果,此时递归语句不在最后),则是指是否到达初始状态。 @@ -1053,6 +1179,12 @@ \subsection{思考的步骤} 为了判断是否到了收敛条件,要在函数接口里用一个参数记录当前的位置(或距离目标还有多远)。如果是求一个解,直接返回这个解;如果是求所有解,要在这里收集解,即把第一步中表示路径的数组\fn{path[]}复制到解集合里。} +\item 关于判重 + \begin{enumerate} + \item 是否需要判重?如果状态转换图是一棵树,则不需要判重,因为在遍历过程中不可能重复;如果状态转换图是一个DAG,则需要判重。这一点跟BFS不一样,BFS的状态转换图总是DAG,必须要判重。 + \item 怎样判重?跟广搜相同,见第 \S \ref{sec:bfs-template} 节。同时,DAG说明存在重叠子问题,此时可以用缓存加速,见第8步。 + \end{enumerate} + \item 如何加速? \begin{enumerate} \item 剪枝。深搜一定要好好考虑怎么剪枝,成本小收益大,加几行代码,就能大大加速。这里没有通用方法,只能具体问题具体分析,要充分观察,充分利用各种信息来剪枝,在中间节点提前返回。 @@ -1116,8 +1248,8 @@ \subsection{深搜与递归的区别} 递归有两种加速策略,一种是\textbf{剪枝(prunning)},对中间结果进行判断,提前返回;一种是\textbf{缓存},缓存中间结果,防止重复计算,用空间换时间。 -其实,递归+缓存,就是 memorization。所谓\textbf{memorization}(翻译为备忘录法,见第 \S \ref{sec:dp-vs-memorization}节),就是"top-down with cache"(自顶向下+缓存),它是Donald Michie 在1968年创造的术语,表示一种优化技术,在top-down 形式的程序中,使用缓存来避免重复计算,从而达到加速的目的。 +其实,递归+缓存,就是 memoization。所谓\textbf{memoization}(翻译为备忘录法,见第 \S \ref{sec:dp-vs-memoization}节),就是"top-down with cache"(自顶向下+缓存),它是Donald Michie 在1968年创造的术语,表示一种优化技术,在top-down 形式的程序中,使用缓存来避免重复计算,从而达到加速的目的。 -\textbf{memorization 不一定用递归},就像深搜不一定用递归一样,可以在迭代(iterative)中使用 memorization 。\textbf{递归也不一定用 memorization},可以用memorization来加速,但不是必须的。只有当递归使用了缓存,它才是 memorization 。 +\textbf{memoization 不一定用递归},就像深搜不一定用递归一样,可以在迭代(iterative)中使用 memoization 。\textbf{递归也不一定用 memoization},可以用memoization来加速,但不是必须的。只有当递归使用了缓存,它才是 memoization 。 既然递归一定是深搜,为什么很多书籍都同时使用这两个术语呢?在递归味道更浓的地方,一般用递归这个术语,在深搜更浓的场景下,用深搜这个术语,读者心里要弄清楚他俩大部分时候是一回事。在单链表、二叉树等递归数据结构上,递归的味道更浓,这时用递归这个术语;在图、隐式图等数据结构上,深搜的味道更浓,这时用深搜这个术语。 diff --git a/C++/chapDivideAndConquer.tex b/C++/chapDivideAndConquer.tex index 0876f43f..9cf03559 100644 --- a/C++/chapDivideAndConquer.tex +++ b/C++/chapDivideAndConquer.tex @@ -20,7 +20,7 @@ \subsubsection{代码} // 时间复杂度O(logn),空间复杂度O(1) class Solution { public: - double pow(double x, int n) { + double myPow(double x, int n) { if (n < 0) return 1.0 / power(x, -n); else return power(x, n); } @@ -62,7 +62,7 @@ \subsubsection{代码} // 时间复杂度O(logn),空间复杂度O(1) class Solution { public: - int sqrt(int x) { + int mySqrt(int x) { int left = 1, right = x / 2; int last_mid; // 记录最近一次mid diff --git a/C++/chapDynamicProgramming.tex b/C++/chapDynamicProgramming.tex index a6ec1d08..5327de51 100644 --- a/C++/chapDynamicProgramming.tex +++ b/C++/chapDynamicProgramming.tex @@ -25,7 +25,7 @@ \subsubsection{描述} \subsubsection{分析} 设状态为$f(i, j)$,表示从从位置$(i,j)$出发,路径的最小和,则状态转移方程为 $$ -f(i,j)=\min\left\{f(i,j+1),f(i+1,j+1)\right\}+(i,j) +f(i,j)=\min\left\{f(i+1,j),f(i+1,j+1)\right\}+(i,j) $$ @@ -101,10 +101,10 @@ \subsubsection{动规} // 时间复杂度O(n),空间复杂度O(1) class Solution { public: - int maxSubArray(int A[], int n) { + int maxSubArray(vector& nums) { int result = INT_MIN, f = 0; - for (int i = 0; i < n; ++i) { - f = max(f + A[i], A[i]); + for (int i = 0; i < nums.size(); ++i) { + f = max(f + nums[i], nums[i]); result = max(result, f); } return result; @@ -119,22 +119,24 @@ \subsubsection{思路5} // 时间复杂度O(n),空间复杂度O(n) class Solution { public: - int maxSubArray(int A[], int n) { - return mcss(A, n); + int maxSubArray(vector& A) { + return mcss(A.begin(), A.end()); } private: // 思路5,求最大连续子序列和 - static int mcss(int A[], int n) { - int i, result, cur_min; + template + static int mcss(Iter begin, Iter end) { + int result, cur_min; + const int n = distance(begin, end); int *sum = new int[n + 1]; // 前n项和 sum[0] = 0; result = INT_MIN; cur_min = sum[0]; - for (i = 1; i <= n; i++) { - sum[i] = sum[i - 1] + A[i - 1]; + for (int i = 1; i <= n; i++) { + sum[i] = sum[i - 1] + *(begin + i - 1); } - for (i = 1; i <= n; i++) { + for (int i = 1; i <= n; i++) { result = max(result, sum[i] - cur_min); cur_min = min(cur_min, sum[i]); } @@ -191,7 +193,7 @@ \subsubsection{代码} // 时间复杂度O(n^2),空间复杂度O(n^2) class Solution { public: - int minCut(string s) { + int minCut(const string& s) { const int n = s.size(); int f[n+1]; bool p[n][n]; @@ -369,7 +371,7 @@ \subsubsection{递归} // 递归,会超时,仅用来帮助理解 class Solution { public: - bool isInterleave(string s1, string s2, string s3) { + bool isInterleave(const string& s1, const string& s2, const string& s3) { if (s3.length() != s1.length() + s2.length()) return false; @@ -400,7 +402,7 @@ \subsubsection{动规} // 二维动规,时间复杂度O(n^2),空间复杂度O(n^2) class Solution { public: - bool isInterleave(string s1, string s2, string s3) { + bool isInterleave(const string& s1, const string& s2, const string& s3) { if (s3.length() != s1.length() + s2.length()) return false; @@ -430,7 +432,7 @@ \subsubsection{动规+滚动数组} // 二维动规+滚动数组,时间复杂度O(n^2),空间复杂度O(n) class Solution { public: - bool isInterleave(string s1, string s2, string s3) { + bool isInterleave(const string& s1, const string& s2, const string& s3) { if (s1.length() + s2.length() != s3.length()) return false; @@ -512,7 +514,7 @@ \subsubsection{描述} \subsubsection{分析} -首先想到的是递归(即深搜),对两个string进行分割,然后比较四对字符串。代码虽然简单,但是复杂度比较高。有两种加速策略,一种是剪枝,提前返回;一种是加缓存,缓存中间结果,即memorization(翻译为记忆化搜索)。 +首先想到的是递归(即深搜),对两个string进行分割,然后比较四对字符串。代码虽然简单,但是复杂度比较高。有两种加速策略,一种是剪枝,提前返回;一种是加缓存,缓存中间结果,即memoization(翻译为记忆化搜索)。 剪枝可以五花八门,要充分观察,充分利用信息,找到能让节点提前返回的条件。例如,判断两个字符串是否互为scamble,至少要求每个字符在两个字符串中出现的次数要相等,如果不相等则返回false。 @@ -528,12 +530,12 @@ \subsubsection{分析} \subsubsection{递归} \begin{Code} -// LeetCode, Interleaving String +// LeetCode, Scramble String // 递归,会超时,仅用来帮助理解 // 时间复杂度O(n^6),空间复杂度O(1) class Solution { public: - bool isScramble(string s1, string s2) { + bool isScramble(const string& s1, const string& s2) { return isScramble(s1.begin(), s1.end(), s2.begin()); } private: @@ -559,11 +561,11 @@ \subsubsection{递归} \subsubsection{动规} \begin{Code} -// LeetCode, Interleaving String +// LeetCode, Scramble String // 动规,时间复杂度O(n^3),空间复杂度O(n^3) class Solution { public: - bool isScramble(string s1, string s2) { + bool isScramble(const string& s1, const string& s2) { const int N = s1.size(); if (N != s2.size()) return false; @@ -597,16 +599,16 @@ \subsubsection{动规} \subsubsection{递归+剪枝} \begin{Code} -// LeetCode, Interleaving String +// LeetCode, Scramble String // 递归+剪枝 // 时间复杂度O(n^6),空间复杂度O(1) class Solution { public: - bool isScramble(string s1, string s2) { + bool isScramble(const string& s1, const string& s2) { return isScramble(s1.begin(), s1.end(), s2.begin()); } private: - typedef string::iterator Iterator; + typedef string::const_iterator Iterator; bool isScramble(Iterator first1, Iterator last1, Iterator first2) { auto length = distance(first1, last1); auto last2 = next(first2, length); @@ -634,12 +636,12 @@ \subsubsection{递归+剪枝} \subsubsection{备忘录法} \begin{Code} -// LeetCode, Interleaving String +// LeetCode, Scramble String // 递归+map做cache -// 时间复杂度O(n^3),空间复杂度O(n^3) +// 时间复杂度O(n^3),空间复杂度O(n^3), TLE class Solution { public: - bool isScramble(string s1, string s2) { + bool isScramble(const string& s1, const string& s2) { cache.clear(); return isScramble(s1.begin(), s1.end(), s2.begin()); } @@ -694,14 +696,14 @@ \subsubsection{备忘录法} }; } -// LeetCode, Interleaving String +// LeetCode, Scramble String // 递归+unordered_map做cache,比map快 // 时间复杂度O(n^3),空间复杂度O(n^3) class Solution { public: unordered_map cache; - bool isScramble(string s1, string s2) { + bool isScramble(const string& s1, const string& s2) { cache.clear(); return isScramble(s1.begin(), s1.end(), s2.begin()); } diff --git a/C++/chapGreedy.tex b/C++/chapGreedy.tex index 86b67e3a..2e098bea 100644 --- a/C++/chapGreedy.tex +++ b/C++/chapGreedy.tex @@ -38,11 +38,11 @@ \subsubsection{代码1} // 思路1,时间复杂度O(n),空间复杂度O(1) class Solution { public: - bool canJump(int A[], int n) { + bool canJump(const vector& nums) { int reach = 1; // 最右能跳到哪里 - for (int i = 0; i < reach && reach < n; ++i) - reach = max(reach, i + 1 + A[i]); - return reach >= n; + for (int i = 0; i < reach && reach < nums.size(); ++i) + reach = max(reach, i + 1 + nums[i]); + return reach >= nums.size(); } }; \end{Code} @@ -54,13 +54,13 @@ \subsubsection{代码2} // 思路2,时间复杂度O(n),空间复杂度O(1) class Solution { public: - bool canJump (int A[], int n) { - if (n == 0) return true; + bool canJump (const vector& nums) { + if (nums.empty()) return true; // 逆向下楼梯,最左能下降到第几层 - int left_most = n - 1; + int left_most = nums.size() - 1; - for (int i = n - 2; i >= 0; --i) - if (i + A[i] >= left_most) + for (int i = nums.size() - 2; i >= 0; --i) + if (i + nums[i] >= left_most) left_most = i; return left_most == 0; @@ -75,14 +75,14 @@ \subsubsection{代码3} // 思路三,动规,时间复杂度O(n),空间复杂度O(n) class Solution { public: - bool canJump(int A[], int n) { - vector f(n, 0); + bool canJump(const vector& nums) { + vector f(nums.size(), 0); f[0] = 0; - for (int i = 1; i < n; i++) { - f[i] = max(f[i - 1], A[i - 1]) - 1; + for (int i = 1; i < nums.size(); i++) { + f[i] = max(f[i - 1], nums[i - 1]) - 1; if (f[i] < 0) return false;; } - return f[n - 1] >= 0; + return f[nums.size() - 1] >= 0; } }; \end{Code} @@ -121,18 +121,18 @@ \subsubsection{代码1} // 时间复杂度O(n),空间复杂度O(1) class Solution { public: - int jump(int A[], int n) { + int jump(const vector& nums) { int step = 0; // 最小步数 int left = 0; int right = 0; // [left, right]是当前能覆盖的区间 - if (n == 1) return 0; + if (nums.size() == 1) return 0; while (left <= right) { // 尝试从每一层跳最远 ++step; const int old_right = right; for (int i = left; i <= old_right; ++i) { - int new_right = i + A[i]; - if (new_right >= n - 1) return step; + int new_right = i + nums[i]; + if (new_right >= nums.size() - 1) return step; if (new_right > right) right = new_right; } @@ -150,18 +150,18 @@ \subsubsection{代码2} // 时间复杂度O(n),空间复杂度O(1) class Solution { public: - int jump(int A[], int n) { + int jump(const vector& nums) { int result = 0; // the maximum distance that has been reached int last = 0; // the maximum distance that can be reached by using "ret+1" steps int cur = 0; - for (int i = 0; i < n; ++i) { + for (int i = 0; i < nums.size(); ++i) { if (i > last) { last = cur; ++result; } - cur = max(cur, i + A[i]); + cur = max(cur, i + nums[i]); } return result; @@ -282,21 +282,22 @@ \subsubsection{代码} \begin{Code} // LeetCode, Longest Substring Without Repeating Characters // 时间复杂度O(n),空间复杂度O(1) +// 考虑非字母的情况 class Solution { public: int lengthOfLongestSubstring(string s) { - const int ASCII_MAX = 26; + const int ASCII_MAX = 255; int last[ASCII_MAX]; // 记录字符上次出现过的位置 int start = 0; // 记录当前子串的起始位置 fill(last, last + ASCII_MAX, -1); // 0也是有效位置,因此初始化为-1 int max_len = 0; for (int i = 0; i < s.size(); i++) { - if (last[s[i] - 'a'] >= start) { + if (last[s[i]] >= start) { max_len = max(i - start, max_len); - start = last[s[i] - 'a'] + 1; + start = last[s[i]] + 1; } - last[s[i] - 'a'] = i; + last[s[i]] = i; } return max((int)s.size() - start, max_len); // 别忘了最后一次,例如"abcd" } diff --git a/C++/chapImplement.tex b/C++/chapImplement.tex index 6c0aeab8..40b88a2b 100644 --- a/C++/chapImplement.tex +++ b/C++/chapImplement.tex @@ -33,15 +33,26 @@ \subsubsection{代码} \begin{Code} //LeetCode, Reverse Integer // 时间复杂度O(logn),空间复杂度O(1) +// 考虑 1.负数的情况 2. 溢出的情况(正溢出&&负溢出,比如 x = -2147483648(即-2^31) ) class Solution { public: int reverse (int x) { - int r = 0; - - for (; x; x /= 10) - r = r * 10 + x % 10; - - return r; + long long r = 0; + long long t = x; + t = t > 0 ? t : -t; + for (; t; t /= 10) + r = r * 10 + t % 10; + + bool sign = x > 0 ? false: true; + if (r > 2147483647 || (sign && r > 2147483648)) { + return 0; + } else { + if (sign) { + return -r; + } else { + return r; + } + } } }; \end{Code} @@ -545,7 +556,7 @@ \subsubsection{相关题目} \section{Pascal's Triangle} %%%%%%%%%%%%%%%%%%%%%%%%%%%%%% -\label{sec:pascals-triangle} +\label{sec:pascal-s-triangle} \subsubsection{描述} @@ -627,7 +638,7 @@ \subsubsection{相关题目} \section{Pascal's Triangle II} %%%%%%%%%%%%%%%%%%%%%%%%%%%%%% -\label{sec:pascals-triangle-ii} +\label{sec:pascal-s-triangle-ii} \subsubsection{描述} @@ -1087,7 +1098,7 @@ \subsubsection{相关题目} \section{Max Points on a Line} %%%%%%%%%%%%%%%%%%%%%%%%%%%%%% -\label{sec:Max-Points-on-a-Line} +\label{sec:max-points-on-a-line} \subsubsection{描述} diff --git a/C++/chapLinearList.tex b/C++/chapLinearList.tex index c23c4538..bbc0ec1c 100644 --- a/C++/chapLinearList.tex +++ b/C++/chapLinearList.tex @@ -29,13 +29,13 @@ \subsubsection{代码1} // 时间复杂度O(n),空间复杂度O(1) class Solution { public: - int removeDuplicates(int A[], int n) { - if (n == 0) return 0; + int removeDuplicates(vector& nums) { + if (nums.empty()) return 0; int index = 0; - for (int i = 1; i < n; i++) { - if (A[index] != A[i]) - A[++index] = A[i]; + for (int i = 1; i < nums.size(); i++) { + if (nums[index] != nums[i]) + nums[++index] = nums[i]; } return index + 1; } @@ -49,8 +49,8 @@ \subsubsection{代码2} // 使用STL,时间复杂度O(n),空间复杂度O(1) class Solution { public: - int removeDuplicates(int A[], int n) { - return distance(A, unique(A, A + n)); + int removeDuplicates(vector& nums) { + return distance(nums.begin(), unique(nums.begin(), nums.end())); } }; \end{Code} @@ -62,8 +62,8 @@ \subsubsection{代码3} // 使用STL,时间复杂度O(n),空间复杂度O(1) class Solution { public: - int removeDuplicates(int A[], int n) { - return removeDuplicates(A, A + n, A) - A; + int removeDuplicates(vector& nums) { + return distance(nums.begin(), removeDuplicates(nums.begin(), nums.end(), nums.begin())); } template @@ -111,13 +111,13 @@ \subsubsection{代码1} // @author hex108 (https://github.com/hex108) class Solution { public: - int removeDuplicates(int A[], int n) { - if (n <= 2) return n; + int removeDuplicates(vector& nums) { + if (nums.size() <= 2) return nums.size(); int index = 2; - for (int i = 2; i < n; i++){ - if (A[i] != A[index - 2]) - A[index++] = A[i]; + for (int i = 2; i < nums.size(); i++){ + if (nums[i] != nums[index - 2]) + nums[index++] = nums[i]; } return index; @@ -134,13 +134,14 @@ \subsubsection{代码2} // 时间复杂度O(n),空间复杂度O(1) class Solution { public: - int removeDuplicates(int A[], int n) { + int removeDuplicates(vector& nums) { + const int n = nums.size(); int index = 0; for (int i = 0; i < n; ++i) { - if (i > 0 && i < n - 1 && A[i] == A[i - 1] && A[i] == A[i + 1]) + if (i > 0 && i < n - 1 && nums[i] == nums[i - 1] && nums[i] == nums[i + 1]) continue; - A[index++] = A[i]; + nums[index++] = nums[i]; } return index; } @@ -179,19 +180,19 @@ \subsubsection{代码} // 时间复杂度O(log n),空间复杂度O(1) class Solution { public: - int search(int A[], int n, int target) { - int first = 0, last = n; + int search(const vector& nums, int target) { + int first = 0, last = nums.size(); while (first != last) { const int mid = first + (last - first) / 2; - if (A[mid] == target) + if (nums[mid] == target) return mid; - if (A[first] <= A[mid]) { - if (A[first] <= target && target < A[mid]) + if (nums[first] <= nums[mid]) { + if (nums[first] <= target && target < nums[mid]) last = mid; else first = mid + 1; } else { - if (A[mid] < target && target <= A[last-1]) + if (nums[mid] < target && target <= nums[last-1]) first = mid + 1; else last = mid; @@ -237,19 +238,19 @@ \subsubsection{代码} // 时间复杂度O(n),空间复杂度O(1) class Solution { public: - bool search(int A[], int n, int target) { - int first = 0, last = n; + bool search(const vector& nums, int target) { + int first = 0, last = nums.size(); while (first != last) { const int mid = first + (last - first) / 2; - if (A[mid] == target) + if (nums[mid] == target) return true; - if (A[first] < A[mid]) { - if (A[first] <= target && target < A[mid]) + if (nums[first] < nums[mid]) { + if (nums[first] <= target && target < nums[mid]) last = mid; else first = mid + 1; - } else if (A[first] > A[mid]) { - if (A[mid] < target && target <= A[last-1]) + } else if (nums[first] > nums[mid]) { + if (nums[mid] < target && target <= nums[last-1]) first = mid + 1; else last = mid; @@ -283,7 +284,7 @@ \subsubsection{分析} $O(m+n)$的解法比较直观,直接merge两个数组,然后求第$k$大的元素。 -不过我们仅仅需要第$k$大的元素,是不需要“排序”这么复杂的操作的。可以用一个计数器,记录当前已经找到第$m$大的元素了。同时我们使用两个指针\fn{pA}和\fn{pB},分别指向A和B数组的第一个元素,使用类似于merge sort的原理,如果数组A当前元素小,那么\fn{pA++},同时\fn{m++};如果数组B当前元素小,那么\fn{pB++},同时\fn{m++}。最终当$m$等于$k$的时候,就得到了我们的答案,$O(k)$时间,$O(1)$空间。但是,当$k$很接近$m+n$的时候,这个方法还是$O(m+n)$的。 +不过我们仅仅需要第$k$大的元素,是不需要“排序”这么昂贵的操作的。可以用一个计数器,记录当前已经找到第$m$大的元素了。同时我们使用两个指针\fn{pA}和\fn{pB},分别指向A和B数组的第一个元素,使用类似于merge sort的原理,如果数组A当前元素小,那么\fn{pA++},同时\fn{m++};如果数组B当前元素小,那么\fn{pB++},同时\fn{m++}。最终当$m$等于$k$的时候,就得到了我们的答案,$O(k)$时间,$O(1)$空间。但是,当$k$很接近$m+n$的时候,这个方法还是$O(m+n)$的。 有没有更好的方案呢?我们可以考虑从$k$入手。如果我们每次都能够删除一个一定在第$k$大元素之前的元素,那么我们需要进行$k$次。但是如果每次我们都删除一半呢?由于A和B都是有序的,我们应该充分利用这里面的信息,类似于二分查找,也是充分利用了“有序”。 @@ -294,7 +295,7 @@ \subsubsection{分析} \item \fn{A[k/2-1] < B[k/2-1]} \myenddot -如果\fn{A[k/2-1] < B[k/2-1]},意味着\fn{A[0]}到\fn{A[k/2-1}的肯定在$A \cup B$的top k元素的范围内,换句话说,\fn{A[k/2-1}不可能大于$A \cup B$的第$k$大元素。留给读者证明。 +如果\fn{A[k/2-1] < B[k/2-1]},意味着\fn{A[0]}到\fn{A[k/2-1]}的肯定在$A \cup B$的top k元素的范围内,换句话说,\fn{A[k/2-1]}不可能大于$A \cup B$的第$k$大元素。留给读者证明。 因此,我们可以放心的删除A数组的这$k/2$个元素。同理,当\fn{A[k/2-1] > B[k/2-1]}时,可以删除B数组的$k/2$个元素。 @@ -314,26 +315,29 @@ \subsubsection{代码} // 时间复杂度O(log(m+n)),空间复杂度O(log(m+n)) class Solution { public: - double findMedianSortedArrays(int A[], int m, int B[], int n) { + double findMedianSortedArrays(const vector& A, const vector& B) { + const int m = A.size(); + const int n = B.size(); int total = m + n; if (total & 0x1) - return find_kth(A, m, B, n, total / 2 + 1); + return find_kth(A.begin(), m, B.begin(), n, total / 2 + 1); else - return (find_kth(A, m, B, n, total / 2) - + find_kth(A, m, B, n, total / 2 + 1)) / 2.0; + return (find_kth(A.begin(), m, B.begin(), n, total / 2) + + find_kth(A.begin(), m, B.begin(), n, total / 2 + 1)) / 2.0; } private: - static int find_kth(int A[], int m, int B[], int n, int k) { + static int find_kth(std::vector::const_iterator A, int m, + std::vector::const_iterator B, int n, int k) { //always assume that m is equal or smaller than n if (m > n) return find_kth(B, n, A, m, k); - if (m == 0) return B[k - 1]; - if (k == 1) return min(A[0], B[0]); + if (m == 0) return *(B + k - 1); + if (k == 1) return min(*A, *B); //divide k into two parts int ia = min(k / 2, m), ib = k - ia; - if (A[ia - 1] < B[ib - 1]) + if (*(A + ia - 1) < *(B + ib - 1)) return find_kth(A + ia, m - ia, B, n, k - ia); - else if (A[ia - 1] > B[ib - 1]) + else if (*(A + ia - 1) > *(B + ib - 1)) return find_kth(A, m, B + ib, n - ib, k - ib); else return A[ia - 1]; @@ -377,14 +381,14 @@ \subsubsection{代码} // 时间复杂度O(n),空间复杂度O(n) class Solution { public: - int longestConsecutive(const vector &num) { + int longestConsecutive(const vector &nums) { unordered_map used; - for (auto i : num) used[i] = false; + for (auto i : nums) used[i] = false; int longest = 0; - for (auto i : num) { + for (auto i : nums) { if (used[i]) continue; int length = 1; @@ -422,18 +426,18 @@ \subsubsection{代码} // Author: @advancedxy class Solution { public: - int longestConsecutive(vector &num) { + int longestConsecutive(vector &nums) { unordered_map map; - int size = num.size(); + int size = nums.size(); int l = 1; for (int i = 0; i < size; i++) { - if (map.find(num[i]) != map.end()) continue; - map[num[i]] = 1; - if (map.find(num[i] - 1) != map.end()) { - l = max(l, mergeCluster(map, num[i] - 1, num[i])); + if (map.find(nums[i]) != map.end()) continue; + map[nums[i]] = 1; + if (map.find(nums[i] - 1) != map.end()) { + l = max(l, mergeCluster(map, nums[i] - 1, nums[i])); } - if (map.find(num[i] + 1) != map.end()) { - l = max(l, mergeCluster(map, num[i], num[i] + 1)); + if (map.find(nums[i] + 1) != map.end()) { + l = max(l, mergeCluster(map, nums[i], nums[i] + 1)); } } return size == 0 ? 0 : l; @@ -488,14 +492,14 @@ \subsubsection{代码} // 时间复杂度O(n),空间复杂度O(n) class Solution { public: - vector twoSum(vector &num, int target) { + vector twoSum(vector &nums, int target) { unordered_map mapping; vector result; - for (int i = 0; i < num.size(); i++) { - mapping[num[i]] = i; + for (int i = 0; i < nums.size(); i++) { + mapping[nums[i]] = i; } - for (int i = 0; i < num.size(); i++) { - const int gap = target - num[i]; + for (int i = 0; i < nums.size(); i++) { + const int gap = target - nums[i]; if (mapping.find(gap) != mapping.end() && mapping[gap] > i) { result.push_back(i + 1); result.push_back(mapping[gap] + 1); @@ -550,16 +554,16 @@ \subsubsection{代码} // 先排序,然后左右夹逼,注意跳过重复的数,时间复杂度O(n^2),空间复杂度O(1) class Solution { public: - vector> threeSum(vector& num) { + vector> threeSum(vector& nums) { vector> result; - if (num.size() < 3) return result; - sort(num.begin(), num.end()); + if (nums.size() < 3) return result; + sort(nums.begin(), nums.end()); const int target = 0; - auto last = num.end(); - for (auto i = num.begin(); i < last-2; ++i) { + auto last = nums.end(); + for (auto i = nums.begin(); i < last-2; ++i) { auto j = i+1; - if (i > num.begin() && *i == *(i-1)) continue; + if (i > nums.begin() && *i == *(i-1)) continue; auto k = last-1; while (j < k) { if (*i + *j + *k < target) { @@ -611,15 +615,15 @@ \subsubsection{代码} // 先排序,然后左右夹逼,时间复杂度O(n^2),空间复杂度O(1) class Solution { public: - int threeSumClosest(vector& num, int target) { + int threeSumClosest(vector& nums, int target) { int result = 0; int min_gap = INT_MAX; - sort(num.begin(), num.end()); + sort(nums.begin(), nums.end()); - for (auto a = num.begin(); a != prev(num.end(), 2); ++a) { + for (auto a = nums.begin(); a != prev(nums.end(), 2); ++a) { auto b = next(a); - auto c = prev(num.end()); + auto c = prev(nums.end()); while (b < c) { const int sum = *a + *b + *c; @@ -684,13 +688,13 @@ \subsubsection{左右夹逼} // 先排序,然后左右夹逼,时间复杂度O(n^3),空间复杂度O(1) class Solution { public: - vector> fourSum(vector& num, int target) { + vector> fourSum(vector& nums, int target) { vector> result; - if (num.size() < 4) return result; - sort(num.begin(), num.end()); + if (nums.size() < 4) return result; + sort(nums.begin(), nums.end()); - auto last = num.end(); - for (auto a = num.begin(); a < prev(last, 3); ++a) { + auto last = nums.end(); + for (auto a = nums.begin(); a < prev(last, 3); ++a) { for (auto b = next(a); b < prev(last, 2); ++b) { auto c = next(b); auto d = prev(last); @@ -722,21 +726,21 @@ \subsubsection{map做缓存} // 时间复杂度,平均O(n^2),最坏O(n^4),空间复杂度O(n^2) class Solution { public: - vector > fourSum(vector &num, int target) { + vector > fourSum(vector &nums, int target) { vector> result; - if (num.size() < 4) return result; - sort(num.begin(), num.end()); + if (nums.size() < 4) return result; + sort(nums.begin(), nums.end()); unordered_map > > cache; - for (size_t a = 0; a < num.size(); ++a) { - for (size_t b = a + 1; b < num.size(); ++b) { - cache[num[a] + num[b]].push_back(pair(a, b)); + for (size_t a = 0; a < nums.size(); ++a) { + for (size_t b = a + 1; b < nums.size(); ++b) { + cache[nums[a] + nums[b]].push_back(pair(a, b)); } } - for (int c = 0; c < num.size(); ++c) { - for (size_t d = c + 1; d < num.size(); ++d) { - const int key = target - num[c] - num[d]; + for (int c = 0; c < nums.size(); ++c) { + for (size_t d = c + 1; d < nums.size(); ++d) { + const int key = target - nums[c] - nums[d]; if (cache.find(key) == cache.end()) continue; const auto& vec = cache[key]; @@ -744,8 +748,8 @@ \subsubsection{map做缓存} if (c <= vec[k].second) continue; // 有重叠 - result.push_back( { num[vec[k].first], - num[vec[k].second], num[c], num[d] }); + result.push_back( { nums[vec[k].first], + nums[vec[k].second], nums[c], nums[d] }); } } } @@ -765,15 +769,15 @@ \subsubsection{multimap} // @author 龚陆安(http://weibo.com/luangong) class Solution { public: - vector> fourSum(vector& num, int target) { + vector> fourSum(vector& nums, int target) { vector> result; - if (num.size() < 4) return result; - sort(num.begin(), num.end()); + if (nums.size() < 4) return result; + sort(nums.begin(), nums.end()); unordered_multimap> cache; - for (int i = 0; i + 1 < num.size(); ++i) - for (int j = i + 1; j < num.size(); ++j) - cache.insert(make_pair(num[i] + num[j], make_pair(i, j))); + for (int i = 0; i + 1 < nums.size(); ++i) + for (int j = i + 1; j < nums.size(); ++j) + cache.insert(make_pair(nums[i] + nums[j], make_pair(i, j))); for (auto i = cache.begin(); i != cache.end(); ++i) { int x = target - i->first; @@ -784,7 +788,7 @@ \subsubsection{multimap} auto c = j->second.first; auto d = j->second.second; if (a != c && a != d && b != c && b != d) { - vector vec = { num[a], num[b], num[c], num[d] }; + vector vec = { nums[a], nums[b], nums[c], nums[d] }; sort(vec.begin(), vec.end()); result.push_back(vec); } @@ -805,13 +809,13 @@ \subsubsection{方法4} // 跟方法1相比,表面上优化了,实际上更慢了,切记! class Solution { public: - vector> fourSum(vector& num, int target) { + vector> fourSum(vector& nums, int target) { vector> result; - if (num.size() < 4) return result; - sort(num.begin(), num.end()); + if (nums.size() < 4) return result; + sort(nums.begin(), nums.end()); - auto last = num.end(); - for (auto a = num.begin(); a < prev(last, 3); + auto last = nums.end(); + for (auto a = nums.begin(); a < prev(last, 3); a = upper_bound(a, prev(last, 3), *a)) { for (auto b = next(a); b < prev(last, 2); b = upper_bound(b, prev(last, 2), *b)) { @@ -864,11 +868,11 @@ \subsubsection{代码1} // 时间复杂度O(n),空间复杂度O(1) class Solution { public: - int removeElement(int A[], int n, int elem) { + int removeElement(vector& nums, int target) { int index = 0; - for (int i = 0; i < n; ++i) { - if (A[i] != elem) { - A[index++] = A[i]; + for (int i = 0; i < nums.size(); ++i) { + if (nums[i] != target) { + nums[index++] = nums[i]; } } return index; @@ -883,8 +887,8 @@ \subsubsection{代码2} // 使用remove(),时间复杂度O(n),空间复杂度O(1) class Solution { public: - int removeElement(int A[], int n, int elem) { - return distance(A, remove(A, A+n, elem)); + int removeElement(vector& nums, int target) { + return distance(nums.begin(), remove(nums.begin(), nums.end(), target)); } }; \end{Code} @@ -930,8 +934,8 @@ \subsubsection{代码} // 时间复杂度O(n),空间复杂度O(1) class Solution { public: - void nextPermutation(vector &num) { - next_permutation(num.begin(), num.end()); + void nextPermutation(vector &nums) { + next_permutation(nums.begin(), nums.end()); } template @@ -1208,7 +1212,8 @@ \subsubsection{代码1} // 思路1,时间复杂度O(n),空间复杂度O(n) class Solution { public: - int trap(int A[], int n) { + int trap(const vector& A) { + const int n = A.size(); int *max_left = new int[n](); int *max_right = new int[n](); @@ -1240,7 +1245,8 @@ \subsubsection{代码2} // 思路2,时间复杂度O(n),空间复杂度O(1) class Solution { public: - int trap(int A[], int n) { + int trap(const vector& A) { + const int n = A.size(); int max = 0; // 最高的柱子,将数组分为两半 for (int i = 0; i < n; i++) if (A[i] > A[max]) max = i; @@ -1267,7 +1273,8 @@ \subsubsection{代码3} // 时间复杂度O(n),空间复杂度O(n) class Solution { public: - int trap(int a[], int n) { + int trap(const vector& A) { + const int n = A.size(); stack> s; int water = 0; @@ -1277,17 +1284,17 @@ \subsubsection{代码3} while (!s.empty()) { // 将栈里比当前元素矮或等高的元素全部处理掉 int bar = s.top().first; int pos = s.top().second; - // bar, height, a[i] 三者夹成的凹陷 - water += (min(bar, a[i]) - height) * (i - pos - 1); + // bar, height, A[i] 三者夹成的凹陷 + water += (min(bar, A[i]) - height) * (i - pos - 1); height = bar; - if (a[i] < bar) // 碰到了比当前元素高的,跳出循环 + if (A[i] < bar) // 碰到了比当前元素高的,跳出循环 break; else s.pop(); // 弹出栈顶,因为该元素处理完了,不再需要了 } - s.push(make_pair(a[i], i)); + s.push(make_pair(A[i], i)); } return water; @@ -1879,10 +1886,11 @@ \subsubsection{代码1} // 时间复杂度O(n),空间复杂度O(1) class Solution { public: - int singleNumber(int A[], int n) { + int singleNumber(vector& nums) { int x = 0; - for (size_t i = 0; i < n; ++i) - x ^= A[i]; + for (auto i : nums) { + x ^= i; + } return x; } }; @@ -1895,8 +1903,8 @@ \subsubsection{代码2} // 时间复杂度O(n),空间复杂度O(1) class Solution { public: - int singleNumber(int A[], int n) { - return accumulate(A, A + n, 0, bit_xor()); + int singleNumber(vector& nums) { + return accumulate(nums.begin(), nums.end(), 0, bit_xor()); } }; \end{Code} @@ -1932,13 +1940,13 @@ \subsubsection{代码1} // 方法1,时间复杂度O(n),空间复杂度O(1) class Solution { public: - int singleNumber(int A[], int n) { + int singleNumber(vector& nums) { const int W = sizeof(int) * 8; // 一个整数的bit数,即整数字长 int count[W]; // count[i]表示在在i位出现的1的次数 fill_n(&count[0], W, 0); - for (int i = 0; i < n; i++) { + for (int i = 0; i < nums.size(); i++) { for (int j = 0; j < W; j++) { - count[j] += (A[i] >> j) & 1; + count[j] += (nums[i] >> j) & 1; count[j] %= 3; } } @@ -1958,11 +1966,11 @@ \subsubsection{代码2} // 方法2,时间复杂度O(n),空间复杂度O(1) class Solution { public: - int singleNumber(int A[], int n) { + int singleNumber(vector& nums) { int one = 0, two = 0, three = 0; - for (int i = 0; i < n; ++i) { - two |= (one & A[i]); - one ^= A[i]; + for (auto i : nums) { + two |= (one & i); + one ^= i; three = ~(one & two); one &= three; two &= three; @@ -2217,7 +2225,7 @@ \subsubsection{迭代版} ListNode *deleteDuplicates(ListNode *head) { if (head == nullptr) return nullptr; - for (ListNode *prev = head, *cur = head->next; cur; cur = cur->next) { + for (ListNode *prev = head, *cur = head->next; cur; cur = prev->next) { if (prev->val == cur->val) { prev->next = cur->next; delete cur; @@ -2658,7 +2666,7 @@ \subsubsection{相关题目} \subsection{Linked List Cycle} -\label{sec:Linked-List-Cycle} +\label{sec:linked-list-cycle} \subsubsection{描述} @@ -2701,7 +2709,7 @@ \subsubsection{相关题目} \subsection{Linked List Cycle II} -\label{sec:Linked-List-Cycle-II} +\label{sec:linked-list-cycle-ii} \subsubsection{描述} @@ -2761,7 +2769,7 @@ \subsubsection{相关题目} \subsection{Reorder List} -\label{sec:Reorder-List} +\label{sec:reorder-list} \subsubsection{描述} @@ -2833,7 +2841,7 @@ \subsubsection{相关题目} \subsection{LRU Cache} -\label{sec:LRU-Cachet} +\label{sec:lru-cache} \subsubsection{描述} diff --git a/C++/chapSearching.tex b/C++/chapSearching.tex index e008d75b..e94d3c38 100644 --- a/C++/chapSearching.tex +++ b/C++/chapSearching.tex @@ -28,10 +28,10 @@ \subsubsection{使用STL} // 时间复杂度O(logn),空间复杂度O(1) class Solution { public: - vector searchRange(int A[], int n, int target) { - const int l = distance(A, lower_bound(A, A + n, target)); - const int u = distance(A, prev(upper_bound(A, A + n, target))); - if (A[l] != target) // not found + vector searchRange(vector& nums, int target) { + const int l = distance(nums.begin(), lower_bound(nums.begin(), nums.end(), target)); + const int u = distance(nums.begin(), prev(upper_bound(nums.begin(), nums.end(), target))); + if (nums[l] != target) // not found return vector { -1, -1 }; else return vector { l, u }; @@ -47,14 +47,14 @@ \subsubsection{重新实现 lower_bound 和 upper_bound} // 时间复杂度O(logn),空间复杂度O(1) class Solution { public: - vector searchRange (int A[], int n, int target) { - auto lower = lower_bound(A, A + n, target); - auto uppper = upper_bound(lower, A + n, target); + vector searchRange (vector& nums, int target) { + auto lower = lower_bound(nums.begin(), nums.end(), target); + auto uppper = upper_bound(lower, nums.end(), target); - if (lower == A + n || *lower != target) + if (lower == nums.end() || *lower != target) return vector { -1, -1 }; else - return vector {distance(A, lower), distance(A, prev(uppper))}; + return vector {distance(nums.begin(), lower), distance(nums.begin(), prev(uppper))}; } template @@ -120,8 +120,8 @@ \subsubsection{代码} // 时间复杂度O(logn),空间复杂度O(1) class Solution { public: - int searchInsert(int A[], int n, int target) { - return lower_bound(A, A + n, target) - A; + int searchInsert(vector& nums, int target) { + return distance(nums.begin(), lower_bound(nums.begin(), nums.end(), target)); } template diff --git a/C++/chapSorting.tex b/C++/chapSorting.tex index a1192eb5..f92c115a 100644 --- a/C++/chapSorting.tex +++ b/C++/chapSorting.tex @@ -1,7 +1,7 @@ \chapter{排序} -\section{Merge Sorted Array} %%%%%%%%%%%%%%%%%%%%%%%%%%%%%% -\label{sec:merge-sorted-array} +\section{Merge Two Sorted Arrays} %%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +\label{sec:merge-two-sorted-arrays} \subsubsection{描述} @@ -21,7 +21,7 @@ \subsubsection{代码} // 时间复杂度O(m+n),空间复杂度O(1) class Solution { public: - void merge(int A[], int m, int B[], int n) { + void merge(vector& A, int m, vector& B, int n) { int ia = m - 1, ib = n - 1, icur = m + n - 1; while(ia >= 0 && ib >= 0) { A[icur--] = A[ia] >= B[ib] ? A[ia--] : B[ib--]; @@ -100,31 +100,36 @@ \subsubsection{代码} // 时间复杂度O(n1+n2+...),空间复杂度O(1) class Solution { public: - ListNode *mergeKLists(vector &lists) { - if (lists.size() == 0) return nullptr; - ListNode *p = lists[0]; - for (int i = 1; i < lists.size(); i++) { - p = mergeTwoLists(p, lists[i]); + ListNode * mergeTwo(ListNode * l1, ListNode * l2){ + if(!l1) return l2; + if(!l2) return l1; + ListNode dummy(-1); + ListNode * p = &dummy; + for(; l1 && l2; p = p->next){ + if(l1->val > l2->val){ + p->next = l2; l2 = l2->next; + } + else{ + p->next = l1; l1 = l1->next; + } } - return p; + p->next = l1 ? l1 : l2; + return dummy.next; } - // Merge Two Sorted Lists - ListNode *mergeTwoLists(ListNode *l1, ListNode *l2) { - ListNode head(-1); - for (ListNode* p = &head; l1 != nullptr || l2 != nullptr; p = p->next) { - int val1 = l1 == nullptr ? INT_MAX : l1->val; - int val2 = l2 == nullptr ? INT_MAX : l2->val; - if (val1 <= val2) { - p->next = l1; - l1 = l1->next; - } else { - p->next = l2; - l2 = l2->next; - } + ListNode* mergeKLists(vector& lists) { + if(lists.size() == 0) return nullptr; + + // multi pass + deque dq(lists.begin(), lists.end()); + while(dq.size() > 1){ + ListNode * first = dq.front(); dq.pop_front(); + ListNode * second = dq.front(); dq.pop_front(); + dq.push_back(mergeTwo(first,second)); } - return head.next; + + return dq.front(); } }; \end{Code} @@ -138,7 +143,7 @@ \subsubsection{相关题目} \section{Insertion Sort List} %%%%%%%%%%%%%%%%%%%%%%%%%%%%%% -\label{sec:Insertion-Sort-List} +\label{sec:insertion-sort-list} \subsubsection{描述} @@ -187,7 +192,7 @@ \subsubsection{相关题目} \section{Sort List} %%%%%%%%%%%%%%%%%%%%%%%%%%%%%% -\label{sec:Sort-List} +\label{sec:sort-list} \subsubsection{描述} @@ -273,16 +278,17 @@ \subsubsection{代码} // 时间复杂度O(n),空间复杂度O(1) class Solution { public: - int firstMissingPositive(int A[], int n) { - bucket_sort(A, n); - - for (int i = 0; i < n; ++i) - if (A[i] != (i + 1)) + int firstMissingPositive(vector& nums) { + bucket_sort(nums); + + for (int i = 0; i < nums.size(); ++i) + if (nums[i] != (i + 1)) return i + 1; - return n + 1; + return nums.size() + 1; } private: - static void bucket_sort(int A[], int n) { + static void bucket_sort(vector& A) { + const int n = A.size(); for (int i = 0; i < n; i++) { while (A[i] != i + 1) { if (A[i] <= 0 || A[i] > n || A[i] == A[A[i] - 1]) @@ -337,10 +343,10 @@ \subsubsection{代码1} // 时间复杂度O(n),空间复杂度O(1) class Solution { public: - void sortColors(int A[], int n) { + void sortColors(vector& A) { int counts[3] = { 0 }; // 记录每个颜色出现的次数 - for (int i = 0; i < n; i++) + for (int i = 0; i < A.size(); i++) counts[A[i]]++; for (int i = 0, index = 0; i < 3; i++) @@ -358,9 +364,9 @@ \subsubsection{代码2} // 双指针,时间复杂度O(n),空间复杂度O(1) class Solution { public: - void sortColors(int A[], int n) { + void sortColors(vector& A) { // 一个是red的index,一个是blue的index,两边往中间走 - int red = 0, blue = n - 1; + int red = 0, blue = A.size() - 1; for (int i = 0; i < blue + 1;) { if (A[i] == 0) @@ -382,9 +388,9 @@ \subsubsection{代码3} // 时间复杂度O(n),空间复杂度O(1) class Solution { public: - void sortColors(int A[], int n) { - partition(partition(A, A + n, bind1st(equal_to(), 0)), A + n, - bind1st(equal_to(), 1)); + void sortColors(vector& nums) { + partition(partition(nums.begin(), nums.end(), bind1st(equal_to(), 0)), + nums.end(), bind1st(equal_to(), 1)); } }; \end{Code} @@ -397,9 +403,9 @@ \subsubsection{代码4} // 时间复杂度O(n),空间复杂度O(1) class Solution { public: - void sortColors(int A[], int n) { - partition(partition(A, A + n, bind1st(equal_to(), 0)), A + n, - bind1st(equal_to(), 1)); + void sortColors(vector& nums) { + partition(partition(nums.begin(), nums.end(), bind1st(equal_to(), 0)), + nums.end(), bind1st(equal_to(), 1)); } private: template diff --git a/C++/chapStackAndQueue.tex b/C++/chapStackAndQueue.tex index db1c1a0b..9dacb601 100644 --- a/C++/chapStackAndQueue.tex +++ b/C++/chapStackAndQueue.tex @@ -74,7 +74,7 @@ \subsubsection{使用栈} // 使用栈,时间复杂度O(n),空间复杂度O(n) class Solution { public: - int longestValidParentheses(string s) { + int longestValidParentheses(const string& s) { int max_len = 0, last = -1; // the position of the last ')' stack lefts; // keep track of the positions of non-matching '('s @@ -108,7 +108,7 @@ \subsubsection{Dynamic Programming, One Pass} // @author 一只杰森(http://weibo.com/wjson) class Solution { public: - int longestValidParentheses(string s) { + int longestValidParentheses(const string& s) { vector f(s.size(), 0); int ret = 0; for (int i = s.size() - 2; i >= 0; --i) { @@ -134,7 +134,7 @@ \subsubsection{两遍扫描} // @author 曹鹏(http://weibo.com/cpcs) class Solution { public: - int longestValidParentheses(string s) { + int longestValidParentheses(const string& s) { int answer = 0, depth = 0, start = -1; for (int i = 0; i < s.size(); ++i) { if (s[i] == '(') { @@ -242,7 +242,7 @@ \subsubsection{相关题目} \subsection{Evaluate Reverse Polish Notation} %%%%%%%%%%%%%%%%%%%%%%%%%%%%%% -\label{sec:Evaluate-Reverse-Polish-Notation} +\label{sec:evaluate-reverse-polish-notation} \subsubsection{描述} diff --git a/C++/chapString.tex b/C++/chapString.tex index f6cf67f5..eefbe620 100644 --- a/C++/chapString.tex +++ b/C++/chapString.tex @@ -35,7 +35,7 @@ \subsubsection{代码} if (!::isalnum(*left)) ++left; else if (!::isalnum(*right)) --right; else if (*left != *right) return false; - else{ left++, right--; } + else { left++, right--; } } return true; } @@ -69,29 +69,20 @@ \subsubsection{暴力匹配} // 暴力解法,时间复杂度O(N*M),空间复杂度O(1) class Solution { public: - char *strStr(const char *haystack, const char *needle) { - // if needle is empty return the full string - if (!*needle) return (char*) haystack; - - const char *p1; - const char *p2; - const char *p1_advance = haystack; - for (p2 = &needle[1]; *p2; ++p2) { - p1_advance++; // advance p1_advance M-1 times - } - - for (p1 = haystack; *p1_advance; p1_advance++) { - char *p1_old = (char*) p1; - p2 = needle; - while (*p1 && *p2 && *p1 == *p2) { - p1++; - p2++; + int strStr(const string& haystack, const string& needle) { + if (needle.empty()) return 0; + + const int N = haystack.size() - needle.size() + 1; + for (int i = 0; i < N; i++) { + int j = i; + int k = 0; + while (j < haystack.size() && k < needle.size() && haystack[j] == needle[k]) { + j++; + k++; } - if (!*p2) return p1_old; - - p1 = p1_old + 1; + if (k == needle.size()) return i; } - return nullptr; + return -1; } }; \end{Code} @@ -103,10 +94,8 @@ \subsubsection{KMP} // KMP,时间复杂度O(N+M),空间复杂度O(M) class Solution { public: - char *strStr(const char *haystack, const char *needle) { - int pos = kmp(haystack, needle); - if (pos == -1) return nullptr; - else return (char*)haystack + pos; + int strStr(const string& haystack, const string& needle) { + return kmp(haystack.c_str(), needle.c_str()); } private: /* @@ -206,10 +195,10 @@ \subsubsection{代码} // 时间复杂度O(n),空间复杂度O(1) class Solution { public: - int atoi(const char *str) { + int myAtoi(const string &str) { int num = 0; int sign = 1; - const int n = strlen(str); + const int n = str.length(); int i = 0; while (str[i] == ' ' && i < n) i++; @@ -325,7 +314,7 @@ \subsubsection{分析} \end{cases} $$ -思路三:Manacher’s Algorithm, 复杂度$O(n)$。详细解释见 \myurl{http://leetcode.com/2011/11/longest-palindromic-substring-part-ii.html} 。 +思路四:Manacher’s Algorithm, 复杂度$O(n)$。详细解释见 \myurl{http://leetcode.com/2011/11/longest-palindromic-substring-part-ii.html} 。 \subsubsection{备忘录法} @@ -389,7 +378,7 @@ \subsubsection{动规} // 动规,时间复杂度O(n^2),空间复杂度O(n^2) class Solution { public: - string longestPalindrome(string s) { + string longestPalindrome(const string& s) { const int n = s.size(); bool f[n][n]; fill_n(&f[0][0], n * n, false); @@ -423,7 +412,7 @@ \subsubsection{Manacher’s Algorithm} // Transform S into T. // For example, S = "abba", T = "^#a#b#b#a#$". // ^ and $ signs are sentinels appended to each end to avoid bounds checking - string preProcess(string s) { + string preProcess(const string& s) { int n = s.length(); if (n == 0) return "^$"; @@ -520,6 +509,10 @@ \subsubsection{递归版} // 递归版,时间复杂度O(n),空间复杂度O(1) class Solution { public: + bool isMatch(const string& s, const string& p) { + return isMatch(s.c_str(), p.c_str()); + } +private: bool isMatch(const char *s, const char *p) { if (*p == '\0') return *s == '\0'; @@ -596,6 +589,10 @@ \subsubsection{递归版} // 时间复杂度O(n!*m!),空间复杂度O(n) class Solution { public: + bool isMatch(const string& s, const string& p) { + return isMatch(s.c_str(), p.c_str()); + } +private: bool isMatch(const char *s, const char *p) { if (*p == '*') { while (*p == '*') ++p; //skip continuous '*' @@ -618,6 +615,10 @@ \subsubsection{迭代版} // 迭代版,时间复杂度O(n*m),空间复杂度O(1) class Solution { public: + bool isMatch(const string& s, const string& p) { + return isMatch(s.c_str(), p.c_str()); + } +private: bool isMatch(const char *s, const char *p) { bool star = false; const char *str, *ptr; @@ -751,7 +752,7 @@ \subsubsection{有限自动机} // finite automata,时间复杂度O(n),空间复杂度O(n) class Solution { public: - bool isNumber(const char *s) { + bool isNumber(const string& s) { enum InputType { INVALID, // 0 SPACE, // 1 @@ -774,17 +775,17 @@ \subsubsection{有限自动机} }; int state = 0; - for (; *s != '\0'; ++s) { + for (auto ch : s) { InputType inputType = INVALID; - if (isspace(*s)) + if (isspace(ch)) inputType = SPACE; - else if (*s == '+' || *s == '-') + else if (ch == '+' || ch == '-') inputType = SIGN; - else if (isdigit(*s)) + else if (isdigit(ch)) inputType = DIGIT; - else if (*s == '.') + else if (ch == '.') inputType = DOT; - else if (*s == 'e' || *s == 'E') + else if (ch == 'e' || ch == 'E') inputType = EXPONENT; // Get next state from current state and input symbol @@ -809,6 +810,10 @@ \subsubsection{使用strtod()} // 偷懒,直接用 strtod(),时间复杂度O(n) class Solution { public: + bool isNumber (const string& s) { + return isNumber(s.c_str()); + } +private: bool isNumber (char const* s) { char* endptr; strtod (s, &endptr); @@ -909,7 +914,7 @@ \subsubsection{代码} } } - int romanToInt(string s) { + int romanToInt(const string& s) { int result = 0; for (size_t i = 0; i < s.size(); i++) { if (i > 0 && map(s[i]) > map(s[i - 1])) { @@ -1070,7 +1075,7 @@ \subsubsection{代码} // 时间复杂度O(n),空间复杂度O(n) class Solution { public: - string simplifyPath(string const& path) { + string simplifyPath(const string& path) { vector dirs; // 当做栈 for (auto i = path.begin(); i != path.end();) { @@ -1137,10 +1142,9 @@ \subsubsection{用 STL} // 时间复杂度O(n),空间复杂度O(1) class Solution { public: - int lengthOfLastWord(const char *s) { - const string str(s); - auto first = find_if(str.rbegin(), str.rend(), ::isalpha); - auto last = find_if_not(first, str.rend(), ::isalpha); + int lengthOfLastWord(const string& s) { + auto first = find_if(s.rbegin(), s.rend(), ::isalpha); + auto last = find_if_not(first, s.rend(), ::isalpha); return distance(first, last); } }; @@ -1154,6 +1158,10 @@ \subsubsection{顺序扫描} // 时间复杂度O(n),空间复杂度O(1) class Solution { public: + int lengthOfLastWord(const string& s) { + return lengthOfLastWord(s.c_str()); + } +private: int lengthOfLastWord(const char *s) { int len = 0; while (*s) { diff --git a/C++/chapTree.tex b/C++/chapTree.tex index 056d8e02..7e70c7b5 100644 --- a/C++/chapTree.tex +++ b/C++/chapTree.tex @@ -56,14 +56,11 @@ \subsubsection{栈} public: vector preorderTraversal(TreeNode *root) { vector result; - const TreeNode *p; stack s; - - p = root; - if (p != nullptr) s.push(p); + if (root != nullptr) s.push(root); while (!s.empty()) { - p = s.top(); + const TreeNode *p = s.top(); s.pop(); result.push_back(p->val); @@ -84,9 +81,8 @@ \subsubsection{Morris先序遍历} public: vector preorderTraversal(TreeNode *root) { vector result; - TreeNode *cur, *prev; + TreeNode *cur = root, *prev = nullptr; - cur = root; while (cur != nullptr) { if (cur->left == nullptr) { result.push_back(cur->val); @@ -157,8 +153,8 @@ \subsubsection{栈} public: vector inorderTraversal(TreeNode *root) { vector result; - const TreeNode *p = root; stack s; + const TreeNode *p = root; while (!s.empty() || p != nullptr) { if (p != nullptr) { @@ -185,9 +181,8 @@ \subsubsection{Morris中序遍历} public: vector inorderTraversal(TreeNode *root) { vector result; - TreeNode *cur, *prev; + TreeNode *cur = root, *prev = nullptr; - cur = root; while (cur != nullptr) { if (cur->left == nullptr) { result.push_back(cur->val); @@ -258,11 +253,9 @@ \subsubsection{栈} public: vector postorderTraversal(TreeNode *root) { vector result; - /* p,正在访问的结点,q,刚刚访问过的结点*/ - const TreeNode *p, *q; stack s; - - p = root; + /* p,正在访问的结点,q,刚刚访问过的结点*/ + const TreeNode *p = root, *q = nullptr; do { while (p != nullptr) { /* 往左下走*/ @@ -375,7 +368,7 @@ \subsubsection{相关题目} \subsection{Binary Tree Level Order Traversal} -\label{sec:binary-tree-tevel-order-traversal} +\label{sec:binary-tree-level-order-traversal} \subsubsection{描述} @@ -438,13 +431,16 @@ \subsubsection{迭代版} public: vector > levelOrder(TreeNode *root) { vector > result; - if(root == nullptr) return result; - queue current, next; - vector level; // elments in level level + + if(root == nullptr) { + return result; + } else { + current.push(root); + } - current.push(root); while (!current.empty()) { + vector level; // elments in one level while (!current.empty()) { TreeNode* node = current.front(); current.pop(); @@ -453,7 +449,6 @@ \subsubsection{迭代版} if (node->right != nullptr) next.push(node->right); } result.push_back(level); - level.clear(); swap(next, current); } return result; @@ -628,42 +623,36 @@ \subsubsection{递归版} \subsubsection{迭代版} \begin{Code} -//LeetCode, Binary Tree Zigzag Level Order Traversal -//广度优先遍历,用一个bool记录是从左到右还是从右到左,每一层结束就翻转一下。 +// LeetCode, Binary Tree Zigzag Level Order Traversal +// 广度优先遍历,用一个bool记录是从左到右还是从右到左,每一层结束就翻转一下。 // 迭代版,时间复杂度O(n),空间复杂度O(n) class Solution { public: vector > zigzagLevelOrder(TreeNode *root) { vector > result; - if (nullptr == root) return result; - - queue q; - bool left_to_right = true; //left to right - vector level; // one level's elements - - q.push(root); - q.push(nullptr); // level separator - while (!q.empty()) { - TreeNode *cur = q.front(); - q.pop(); - if (cur) { - level.push_back(cur->val); - if (cur->left) q.push(cur->left); - if (cur->right) q.push(cur->right); - } else { - if (left_to_right) { - result.push_back(level); - } else { - reverse(level.begin(), level.end()); - result.push_back(level); - } - level.clear(); - left_to_right = !left_to_right; + queue current, next; + bool left_to_right = true; + + if(root == nullptr) { + return result; + } else { + current.push(root); + } - if (q.size() > 0) q.push(nullptr); + while (!current.empty()) { + vector level; // elments in one level + while (!current.empty()) { + TreeNode* node = current.front(); + current.pop(); + level.push_back(node->val); + if (node->left != nullptr) next.push(node->left); + if (node->right != nullptr) next.push(node->right); } + if (!left_to_right) reverse(level.begin(), level.end()); + result.push_back(level); + left_to_right = !left_to_right; + swap(next, current); } - return result; } }; @@ -844,14 +833,15 @@ \subsubsection{递归版} class Solution { public: bool isSymmetric(TreeNode *root) { - return root ? isSymmetric(root->left, root->right) : true; + if (root == nullptr) return true; + return isSymmetric(root->left, root->right); } - bool isSymmetric(TreeNode *left, TreeNode *right) { - if (!left && !right) return true; // 终止条件 - if (!left || !right) return false; // 终止条件 - return left->val == right->val // 三方合并 - && isSymmetric(left->left, right->right) - && isSymmetric(left->right, right->left); + bool isSymmetric(TreeNode *p, TreeNode *q) { + if (p == nullptr && q == nullptr) return true; // 终止条件 + if (p == nullptr || q == nullptr) return false; // 终止条件 + return p->val == q->val // 三方合并 + && isSymmetric(p->left, q->right) + && isSymmetric(p->right, q->left); } }; \end{Code} @@ -1457,15 +1447,15 @@ \subsubsection{分析} \subsubsection{代码} \begin{Code} -// LeetCode, Validate Binary Search Tree +// Validate Binary Search Tree // 时间复杂度O(n),空间复杂度O(\logn) class Solution { public: bool isValidBST(TreeNode* root) { - return isValidBST(root, INT_MIN, INT_MAX); + return isValidBST(root, LONG_MIN, LONG_MAX); } - bool isValidBST(TreeNode* root, int lower, int upper) { + bool isValidBST(TreeNode* root, long long lower, long long upper) { if (root == nullptr) return true; return root->val > lower && root->val < upper diff --git a/C++/images/.DS_Store b/C++/images/.DS_Store new file mode 100644 index 00000000..ef69838b Binary files /dev/null and b/C++/images/.DS_Store differ diff --git a/C++/leetcode-cpp.pdf b/C++/leetcode-cpp.pdf index 40ac1520..72d3da55 100644 Binary files a/C++/leetcode-cpp.pdf and b/C++/leetcode-cpp.pdf differ diff --git a/Java/README.md b/Java/README.md index 8886a0b4..73032b56 100644 --- a/Java/README.md +++ b/Java/README.md @@ -1,3 +1,7 @@ #Java版 ----------------- -书的内容与C++版一摸一样,不过代码是用Java写的。本书的代码要求 Java 6 以上。 + +## 编译 + + docker pull soulmachine/texlive + docker run -it --rm -v $(pwd):/data -w /data soulmachine/texlive-full xelatex -synctex=1 -interaction=nonstopmode leetcode-java.tex diff --git a/README.md b/README.md index 99c9c0c7..9af9ce21 100644 --- a/README.md +++ b/README.md @@ -1,83 +1,41 @@ -#LeetCode题解 ------------------ -##PDF下载 -LeetCode题解(C++版).pdf - -C++ 文件夹下是C++版,内容一摸一样,代码是用C++写的, - -Java 文件夹下是Java版,目前正在编写中,由于拖延症,不知道猴年马月能完成。 - -##LaTeX模板 -本书使用的是陈硕开源的[模板](https://github.com/chenshuo/typeset)。这个模板制作精良,很有taste,感谢陈硕 :) - -##在Windows下编译 -1. 安装Tex Live 2013 。把bin目录例如`D:\texlive\2013\bin\win32`加入PATH环境变量。 -1. 安装字体。这个LaTex模板总共使用了9个字体,下载地址 ,有的字体Windows自带了,有的字体Ubuntu自带了,但都不全,还是一次性安装完所有字体比较方便。 -1. 安装TeXstudio -1. (可选)启动Tex Live Manager,更新所有已安装的软件包。 -1. 配置TeXstudio。 - - 启动Texstudio,选择 `Options-->Configure Texstudio-->Commands`,XeLaTex 设置为 `xelatex -synctex=1 -interaction=nonstopmode %.tex`; - - 选择 `Options-->Configure Texstudio-->Build` +# LeetCode题解 - Build & View 由默认的 PDF Chain 改为 Compile & View; +## 在线阅读 - Default Compiler 由默认的PdfLaTex 修改为 XeLaTex ; + - PDF Viewer 改为 “Internal PDF Viewer(windowed)”,这样预览时会弹出一个独立的窗口,这样比较方便。 +## PDF下载 -1. 编译。用TeXstudio打开`leetcode-cpp.tex`,点击界面上的绿色箭头就可以开始编译了。 - - 在下方的窗口可以看到TeXstudio正在使用的编译命令是`xelatex -synctex=1 -interaction=nonstopmode "leetcode-cpp".tex` +LeetCode题解(C++版).pdf -##在Ubuntu下编译 -1. 安装Tex Live 2013 - - 1.1. 下载TexLive 2013 的ISO 光盘,地址 +C++ 文件夹下是C++版,内容一模一样,代码是用C++写的。 - 1.2 mount 光盘,`sudo ./install-tl` 开始安装 +Java 文件夹下是Java版,目前正在编写中,由于拖延症,不知道猴年马月能完成。 - 1.3 加入环境变量 +## 如何编译PDF - sudo vi /etc/profile - export PATH=$PATH:/usr/local/texlive/2013/bin/x86_64-linux - export MANPATH=$MANPATH:/usr/local/texlive/2013/texmf-dist/doc/man - export INFPATH=$INFPATH:/usr/local/texlive/2013/texmf-dist/doc/info +### 命令行编译 -1. 安装字体。这个LaTex模板总共使用了9个字体,下载地址 ,有的字体Windows自带了,有的字体Ubuntu自带了,但都不全,还是一次性安装完所有字体比较方便。 -1. 安装TeXstudio -1. 配置TeXstudio。 +```bash +docker run -it --rm -v $(pwd)/C++:/project -w /project soulmachine/texlive xelatex -interaction=nonstopmode leetcode-cpp.tex +``` - 启动Texstudio,选择 `Options-->Configure Texstudio-->Commands`,XeLaTex 设置为 `xelatex -synctex=1 -interaction=nonstopmode %.tex`; +### vscode下编译 - 选择 `Options-->Configure Texstudio-->Build` +本项目已经配置好了vscode devcontainer, 可以在 Windows, Linux 和 macOS 三大平台上编译。 - Build & View 由默认的 PDF Chain 改为 Compile & View; +用 vscode 打开本项目,选择右下角弹出的 `"Reopen in Container"`,就会在容器中打开本项目,该容器安装了 Tex Live 2022 以及所需要的10个字体。 - Default Compiler 由默认的PdfLaTex 修改为 XeLaTex ; +点击vscode左下角的齿轮图标,选择 `Command Palette`,输入`tasks`, 选择 `Run Task`, 选择 `leetcode-C++`,即可启动编译。 - PDF Viewer 改为 “Internal PDF Viewer(windowed)”,这样预览时会弹出一个独立的窗口,这样比较方便。 +## LaTeX模板 -1. 编译。用TeXstudio打开`leetcode-cpp.tex`,点击界面上的绿色箭头就可以开始编译了。 +本书使用的是陈硕开源的[模板](https://github.com/chenshuo/typeset)。这个模板制作精良,感谢陈硕 :) - 在下方的窗口可以看到TeXstudio正在使用的编译命令是`xelatex -synctex=1 -interaction=nonstopmode "leetcode-cpp".tex` -1. 懒人版镜像。如果不想进行上面繁琐的安装过程,我做好了一个Ubuntu VMware虚拟机镜像,已经装好了TexLive 2013, TexStudio和字体(详细的安装日志见压缩包注释),开箱即用,下载地址 。 +这个LaTex模板总共使用了10个字体,下载地址 。有的字体Windows自带了,有的字体Ubuntu自带了,但都不全,还是一次性安装完所有字体比较方便。 -##如何贡献代码 -编译通过后,就具备了完整的LaTeX编译环境了。 +也可以参考 [Dockerfile](https://github.com/soulmachine/docker-images/blob/master/texlive/Dockerfile) 去学习如何安装所有字体。 -本书模板已经写好了,基本上不需要很多LaTeX知识就可以动手了。 +## 贡献代码 欢迎给本书添加内容或纠正错误,在自己本地编译成PDF,预览没问题后,就可以发pull request过来了。 - -##北美求职微博群 -我和我的小伙伴们在这里: - -## 【友情推荐】九章算法 - -1. 算法辅导在线视频直播课程: -1. Leetcode在线答案: -1. 新一代刷题网站,比LeetCode题型更多,质量更高 : - - **本书即将由电子工业出版社出版,书中题目将全部切换到 lintcode.com, 因此这本电子书不再更新。新的电子书在这里开源: [LintCode题解](https://github.com/soulmachine/lintcode)** diff --git "a/\345\217\202\350\200\203\350\265\204\346\226\231/silicon-job.jpeg" "b/\345\217\202\350\200\203\350\265\204\346\226\231/silicon-job.jpeg" new file mode 100644 index 00000000..dd349c0b Binary files /dev/null and "b/\345\217\202\350\200\203\350\265\204\346\226\231/silicon-job.jpeg" differ