總結
- 想清楚再編碼
- 分析方法:舉例子、畫圖
第1節:畫圖分析方法
對于二叉樹、二維數組、鏈表等問題,都可以采用畫圖的方法來分析,例如:
- 面試題19二叉樹的鏡像:通過畫圖發現,實質就是在遍歷樹的同時交換非葉節點的左右子節點。
- 面試題20順時針打印矩陣:通過畫圖發現,實質就是一圈一圈的打印數組。
- 面試題26復雜鏈表的復制:畫圖,發現復制鏈表的過程,分三個步驟:復制節點,設置random指針,拆分兩個鏈表。
面試題 19:二叉樹的鏡像(反轉二叉樹)
題目:請完成一個函數,輸入一個二叉樹,該函數輸出它的鏡像。
題目分析
LeetCode 226. Invert Binary Tree
何為鏡像:即照鏡子得到的像,與原像是左右顛倒的。
求二叉樹鏡像:即反轉二叉樹。
求解思路:對于每個非葉子節點,反轉其左右孩子節點。既可以用遞歸也可以用迭代。
題目典故:著名的Homebrew
的作者Max Howell
在面試Google時被問到這題,并且沒有做出來,原推文
- Google: 90% of our engineers use the software you wrote (Homebrew), but you can’t invert a binary tree on a whiteboard so fuck off.
題目考點及相關題目
問題本質:樹的DFS或BFS遍歷。
擴展:掌握非遞歸的實現。
我的代碼如下:
1.遞歸方法:
public class Solution {
public TreeNode invertTree(TreeNode root) {
if(root == null) return root;
TreeNode temp = root.left;
root.left = invertTree(root.right);
root.right = invertTree(temp);
return root;
}
}
2.非遞歸DFS(棧):
public TreeNode invertTree(TreeNode root) {
if (root == null) {
return null;
}
final Deque<TreeNode> stack = new LinkedList<>();
stack.push(root);
while(!stack.isEmpty()) {
final TreeNode node = stack.pop();
final TreeNode left = node.left;
node.left = node.right;
node.right = left;
if(node.left != null) {
stack.push(node.left);
}
if(node.right != null) {
stack.push(node.right);
}
}
return root;
}
3.非遞歸BFS(隊列):
public TreeNode invertTree(TreeNode root) {
if (root == null) {
return null;
}
final Queue<TreeNode> queue = new LinkedList<>();
queue.offer(root);
while(!queue.isEmpty()) {
final TreeNode node = queue.poll();
final TreeNode left = node.left;
node.left = node.right;
node.right = left;
if(node.left != null) {
queue.offer(node.left);
}
if(node.right != null) {
queue.offer(node.right);
}
}
return root;
}
面試題 20:順時針打印矩陣
題目:輸入一個矩陣,按照從外向里以順時針的順序依次打印出每一個數字。
題目分析
LeetCode 54. Spiral Matrix
順時針打印矩陣:即螺旋矩陣輸出。
規律:一圈一圈的輸出數組里的數據,注意邊界條件不好確定時,多畫幾個圖就很清楚了。
邊界: “上”肯定要打印,打印“右”的條件是至少有兩行,打印“下”至少兩行兩列;打印“左”至少要有三行兩列。
題目考點及相關題目
多畫圖,幫助理解。
相關題目有:LeetCode 59. Spiral Matrix II
我的代碼如下:
解法一:
public class Solution {
public List<Integer> spiralOrder(int[][] matrix) {
List<Integer> res = new ArrayList<Integer>();
if(matrix.length <= 0) return res;
int m = matrix.length, n = matrix[0].length;
int min = Math.min(m, n);
int k = min%2 == 0 ? (min/2 - 1) : min/2;
for(int i = 0; i <= k; i ++)
spiral(matrix, i, res, m, n);
return res;
}
public void spiral(int[][] matrix, int k, List<Integer> res, int m, int n){
// 上
for(int i = k; i < n - k; i ++)
res.add(matrix[k][i]);
// 右
for(int i = k + 1; i < m - k; i ++)
res.add(matrix[i][n-k-1]);
// 下(加判斷條件,排除兩種情況:只有一列時,只有一行時)
if(k < n - k - 1 && k < m - k - 1){
for(int i = n - k - 2; i >= k; i --)
res.add(matrix[m-k-1][i]);
}
// 左(加判斷條件,排除兩種情況:只有一列時,只有不超過2行時)
if(k < n - k - 1 && k < m - k - 2){
for(int i = m - k - 2; i > k; i --)
res.add(matrix[i][k]);
}
}
}
解法二:
public class Solution {
public List<Integer> spiralOrder(int[][] matrix) {
List<Integer> res = new ArrayList<Integer>();
if (matrix.length == 0) {
return res;
}
int rowBegin = 0;
int rowEnd = matrix.length-1;
int colBegin = 0;
int colEnd = matrix[0].length - 1;
while (rowBegin <= rowEnd && colBegin <= colEnd) {
// Traverse Right
for (int j = colBegin; j <= colEnd; j ++) {
res.add(matrix[rowBegin][j]);
}
rowBegin++;
// Traverse Down
for (int j = rowBegin; j <= rowEnd; j ++) {
res.add(matrix[j][colEnd]);
}
colEnd--;
if (rowBegin <= rowEnd) {
// Traverse Left
for (int j = colEnd; j >= colBegin; j --) {
res.add(matrix[rowEnd][j]);
}
}
rowEnd--;
if (colBegin <= colEnd) {
// Traver Up
for (int j = rowEnd; j >= rowBegin; j --) {
res.add(matrix[j][colBegin]);
}
}
colBegin ++;
}
return res;
}
}
第2節:舉例分析方法
通過舉例子,理解題目意思并找到規律;最后也以用例子來測試程序是否完善。
- 面試題22棧的壓入、彈出序列:通過舉實際棧的例子,來模擬棧的壓入和彈出操作,就能發現隱藏的規律。
- 面試題24二叉搜索樹的后序遍歷序列:理解后續遍歷的特點,并找到遞歸方法的思路。
- 面試題21包含min函數的棧:用一個棧來專門來存儲當前棧中的最小值。
面試題 21:包含min函數的棧
題目:定義棧的數據結構,請在該類型中實現一個能夠得到棧的最小元素的min函數。在該函數中,調用min、push、及pop的時間復雜度都是$O(1)$。
題目分析
- 使用兩個棧,一個數據棧,一個最小數棧
- 每次存放數據時,若存放的數據比此時最小棧中的棧頂值要大,那么將最小數棧棧頂元素再存一次(即增加一個棧頂元素),如果要存的數據比棧頂元素小,那么就將此值也存入最小值棧。
- 出棧時,兩棧都要同時出數據,使得最小數棧的棧頂元素總是目前數據棧中的最小值。兩個棧中的元素個數總是保持相等。
題目考點及相關題目
用一個輔助棧來存儲最小值元素。
相關題目:LeetCode 239. Sliding Window Maximum
我的代碼如下:
class MinStack {
Stack<Integer> data;
Stack<Integer> min;
public MinStack() {
// do initialize if necessary
data = new Stack<Integer>();
min = new Stack<Integer>();
}
public void push(int x) {
if (!min.isEmpty() && min.peek() < x) min.push(min.peek());
else min.push(x);
data.push(x);
}
public void pop() {
min.pop();
data.pop();
}
public int top() {
return data.peek();
}
public int getMin() {
return min.peek();
}
}
面試題 22:棧的壓入、彈出序列
題目:輸入兩個整數序列,第一個序列表示 棧的壓入順序,請判斷第二個序列是否為該棧的彈出順序。假設壓入棧的所有數字均不相等。例如序列
1、2、3、4、5
是某棧的壓棧序列,序列4、5、3、2、1
是該壓棧序列對應的一個彈出序列,但4、3、5、1、2
就不可能是該壓棧序列的彈出序列。
題目分析
用一個輔助棧stack來存儲壓棧序列,如果下一個彈出的數字剛好是輔助棧頂數字,那么直接彈出,并將輔助棧內棧頂數字也彈出;如果下一個彈出的數字不在輔助棧頂,我們把壓棧序列中還沒有入棧的數字壓入輔助棧,直到下一個需要彈出的數字壓入棧頂為止。如果 所有的數字都 壓入棧了仍然沒有找到下一個彈出的數字,那么該序列不可能是一個彈出序列。
題目考點及相關題目
棧的壓入、彈出操作。
我的代碼如下:
public boolean isPopOrder(List<Integer> push, List<Integer> pop){
if(pop.size() != push.size()) return false;
Stack<Integer> stack = new Stack<Integer>();
while(!pop.isEmpty()){
if(stack.isEmpty()) stack.push(push.remove(0));
while(stack.peek() != pop.get(0) && !push.isEmpty()) stack.push(push.remove(0));
if(push.isEmpty() && stack.peek() != pop.get(0)) return false;
else{
stack.pop();
pop.remove(0);
}
}
return true;
}
面試題 23:從上往下打印二叉樹
題目:從上往下拓印出二叉樹的每個結點,同層的結點 按照從左到右的順序 打印。
題目分析
LeetCode 102. Binary Tree Level Order Traversal
即樹的層次遍歷,用到了隊列。
題目考點及相關題目
層次遍歷,隊列。
相關題目:LeetCode 103. Binary Tree Zigzag Level Order Traversal、LeetCode 107. Binary Tree Level Order Traversal II、LeetCode 111. Minimum Depth of Binary Tree
我的代碼如下:
public class Solution {
public List<List<Integer>> levelOrder(TreeNode root) {
List<List<Integer>> rs = new ArrayList<List<Integer>>();
LinkedList<TreeNode> level = new LinkedList<TreeNode>();
if(root != null) level.push(root);
while(!level.isEmpty()){
int levelNum = level.size();
List<Integer> tmp = new ArrayList<Integer>();
for(int i=0; i<levelNum; i++){
TreeNode temp = level.removeFirst();
tmp.add(temp.val);
if(temp.left != null) level.add(temp.left);
if(temp.right != null) level.add(temp.right);
}
rs.add(tmp);
}
return rs;
}
}
面試題 24:二叉搜索樹的后序遍歷序列
題目:輸入一個整數數組,判斷該數組是不是某二叉搜索樹的后序遍歷的結果。如果是則返回
true
,否則返回false
。假設輸入的數組的任意兩個數字都互不相同。
題目分析
經過分析可知,后序遍歷得到的序列的特點:
- 最后一個數字是該二叉搜索樹的根節點,前面的序列又可以分成兩部分;
- 前一部分是根節點的左子樹,它們的值都比根節點值小;
- 后一部分是根節點的右子樹,它們的值都比根節點值大。
因此,需要再次判斷該二叉樹的左子樹序列和右子樹序列是否滿足以上特點。很明顯地,這是一個遞歸操作的過程。
題目考點及相關題目
二叉搜索樹的概念以及后序遍歷的特點
相關題目:輸入一個整數數組,判斷該數組是不是某二叉搜索樹的前序遍歷的結果。
我的代碼如下:
1.我的遞歸程序,沒有經過完整的校驗
public class Solution{
public boolean VerifySequenceOfBST(int[] sequence){
if(sequence.length <= 0) return false;
return verify(sequence, 0, sequence.length - 1);
}
public boolean verify(int[] sequence, int start, int end){
if(start >= end) return true;
int root = sequence[end];
int i = start, j = end - 1;
// 找到左右子序列的分界處,經驗證,無論是只有左子樹還是只有右子樹,或者左右子樹均有的情況,都會滿足i == j + 1;否則說明該序列不滿足二叉搜索樹的性質。
while(i < end && sequence[i] < root) i ++;
while(j >= start && sequence[j] > root) j --;
if(i != j + 1) return false;
return verify(sequence, start, j) && verify(sequence, i, end - 1);
}
}
2.標準答案請見P158
。
面試題 25:二叉樹中和為某一值的路徑
題目:輸入一棵二叉樹和一個整數,打印出二叉樹中結點值的和為輸入整數的所有路徑。從樹的根節點開始往下一直到葉子節點所經過的節點形成一條路徑。
題目分析
- 首先這種題肯定是對樹遍歷,而樹的遍歷要么是深搜,要么是廣搜;
- 對于深搜,那就是遞歸了,關鍵問題在:怎么計算并存儲遍歷到當前節點時的和,可以用我的方法,用一個變量來記錄到達當前節點的和,或者方法二中比較巧妙的思想。
- 深搜遞歸總會遇到的問題是:變量(中間結果)在傳遞的過程中會發生改變,比如方法一中的:list和cur,一定要注意還原,這是非常重要的一步,否則會導致先前的結果仍然在list當中。這也是形參和實參的問題。
- 對于廣搜,那么就要用額外的變量存儲到達每個節點的和,這種方法的空間復雜度比較高,這里就不仔細介紹了。
題目考點及相關題目
樹的前序遍歷,也就是DFS
相關題目:Path Sum、Binary Tree Paths
我的代碼如下:
1.我的深搜代碼
public class Solution {
List<List<Integer>> res = new ArrayList<List<Integer>>();
public List<List<Integer>> pathSum(TreeNode root, int sum) {
if(root == null) return res;
pathSumWithDFS(root, root.val, new ArrayList<Integer>(Arrays.asList(root.val)), sum);
return res;
}
public void pathSumWithDFS(TreeNode root, int cur, List<Integer> list, int sum){
if(root.left == null && root.right == null){
if(cur == sum) {
// 這是十分推薦的寫法
List<Integer> temp = new ArrayList<Integer>(list);
res.add(temp);
}
return;
}
//if(cur >= sum) return; 注意這一句不能加,因為可能節點值有負值。
if(root.left != null) {
list.add(root.left.val);
pathSumWithDFS(root.left, cur + root.left.val, list, sum);
// 這一步非常重要
list.remove(list.size()-1);
}
if(root.right != null) {
list.add(root.right.val);
pathSumWithDFS(root.right, cur + root.right.val, list, sum);
// 這一步非常重要
list.remove(list.size()-1);
}
}
}
2.他人更加簡潔的代碼(用棧更好,而且它這個函數不用記錄到目前節點的和,而是用sum-當前節點的值,方法更加巧妙)
public class Solution {
private List<List<Integer>> resultList = new ArrayList<List<Integer>>();
public void pathSumInner(TreeNode root, int sum, Stack<Integer>path) {
path.push(root.val);
if(root.left == null && root.right == null)
if(sum == root.val) resultList.add(new ArrayList<Integer>(path));
if(root.left!=null) pathSumInner(root.left, sum-root.val, path);
if(root.right!=null)pathSumInner(root.right, sum-root.val, path);
// 這一步非常重要
path.pop();
}
public List<List<Integer>> pathSum(TreeNode root, int sum) {
if(root==null) return resultList;
Stack<Integer> path = new Stack<Integer>();
pathSumInner(root, sum, path);
return resultList;
}
}
第3節:分解讓復雜問題簡單化
分治法:先把大問題分解成若干個小問題,然后再逐個解決的思想。
- 面試題27二叉搜索樹與雙向鏈表:把大問題分解成左子樹和右子樹兩個小問題,然后再把轉換左右子樹得到的鏈表和根結點鏈接起來就解決了整個問題。
- 面試題28字符串的排列:整個字符串的排列是一個大問題,而第一個字符之后的字符串的排列就是一個小問題,因此分解問題,并采用遞歸思想解決。
面試題 26:復雜鏈表的復制
題目:請實現函數
ComplexListNode *Clone(ComplexListNode *pHead)
,復制一個復雜鏈表。在復雜鏈表中,每個結點除了有一個m_pNext指針指向下一個結點,還有一個m_pSibling指向的任意結點或者NULL。
題目分析
LeetCode 138. Copy List with Random Pointer
- 首先,要理解什么是深拷貝。
-深拷貝是指在拷貝對象時,同時會對引用指向的對象進行拷貝。
-相對應的,淺拷貝是指在拷貝對象時,對于基本數據類型的變量會重新復制一份,而對于引用類型的變量只是對引用進行拷貝。- 此題深拷貝鏈表,主要分為如下兩步:
-第一步復制原始鏈表中的結點,并用next指針連接起來。
-第二步設置每個結點的random指針。- 第一步很容易就可以完成,主要在于第二步,很容易就會犯淺拷貝的錯。只是復制了對象的引用,導致結果出錯。
- 第二步中復制random指針,應該是根據原鏈表中節點N的random指針指向的節點S,找到新鏈表中所對應的S',而不是簡單地將新鏈表中的N'的random指針指向S。
- 至于如何找到對應的S',有兩種方法:
-要么都從頭指針開始遍歷經過指針N,N'且經過相同步分別找到S, S'。這樣的話,時間復雜度會到O(n^2)。
-要么就是用空間換時間,使用Map存儲新舊鏈表中的對應的節點對<N, N'>- 還有另外一種方法,既不用開辟新的空間來存儲節點,也不用從頭遍歷查找。方法比較巧妙:
>- 在第一步復制原始鏈表結點時,新結點鏈接在原結點后面,然后再將新結點的next指針指向原結點的next指針,具體如下:
- 第二步鏈接新結點的random指針就很容易了,它對應在原結點的random指針的后面一個結點,具體如下:
題目考點及相關題目
把復雜鏈表的復制過程分解成三個步驟:復制結點、設置random指針、拆分兩個鏈表。
相關題目:Clone Graph
我的代碼如下:
1.使用HashMap,空間換時間,時間復雜度$O(n)$,空間復雜度$O(n)$。
public class Solution {
public RandomListNode copyRandomList(RandomListNode head) {
if(head == null) return head;
RandomListNode newHead = new RandomListNode(0);
RandomListNode p = newHead;
RandomListNode s = newHead;
RandomListNode q = head;
RandomListNode r = head;
Map<RandomListNode, RandomListNode> nodes = new HashMap<RandomListNode, RandomListNode>();
while(q != null){
RandomListNode tmp = new RandomListNode(q.label);
p.next = tmp;
p = p.next;
nodes.put(q, p);
q = q.next;
}
//p.next = null;
while(r != null){
s.next.random = nodes.get(r.random);
s = s.next;
r = r.next;
}
return newHead.next;
}
}
2.使用鏈接方式,方法巧妙,時間復雜度$O(n)$,空間復雜度$O(1)$。
public class Solution {
public RandomListNode copyRandomList(RandomListNode head) {
if(head == null) return null;
RandomListNode p = head, q = p;
while(p != null){
RandomListNode tmp = new RandomListNode(p.label);
RandomListNode next = p.next;
p.next = tmp;
tmp.next = next;
p = next;
}
while(q != null){
RandomListNode t = q.next;
t.random = q.random == null ? null : q.random.next;
q = t.next;
}
RandomListNode newHead = head.next, s = head;
while(s != null){
RandomListNode n = s.next;
s.next = n.next;
s = s.next;
n.next = s == null ? null : s.next;
}
return newHead;
}
}
面試題 27:二叉搜索樹與雙向鏈表
題目:輸入一棵二叉搜索樹,將該二叉搜索樹轉換成一個排序的雙向鏈表。要求不能創建任何新的結點,只能調整樹中結點指針的指向。
題目分析
- 注意題目要求:不能創建任何新的結點,只能調整指針的指向
- 因為在二叉樹中,每個結點都有兩個指向子結點的指針,在雙向鏈表中每個結點也有兩個指針,它們分別指向前、后兩個結點。因此,在理論上有可能實現二叉搜索樹和排序的雙向鏈表的轉換。
- 由于要求轉換之后雙向鏈表是排好序的,而二叉搜索樹的中序遍歷剛好是一個有序數組,因此此題的遍歷方法肯定是中序遍歷二叉搜索樹。
- 具體思想:如下圖,當遍歷到根節點
10
時,我們將樹看成三部分,根節點10
、根節點為6
的左子樹、根節點為14
的右子樹,可以想像一下,如果左子樹已經排好序成為了雙向鏈表,那么它的最后一個節點(也就是左子樹里的最大值8
)的右指針將指向10
,同理,10
的右指針將指向右子樹里的最小值12
,如下圖所示:很明顯,上述就是一個遞歸過程。
中序.png中序2.png
題目考點及相關題目
分治法的本質:將復雜問題分解成小問題,然后遞歸調用函數功能,即可完成對復雜問題的求解。
相關題目:LeetCode 114. Flatten Binary Tree to Linked List
我的代碼如下:
1.未經驗證的Java代碼,時間復雜度較高,全耗在了遍歷左子樹,找到最后一個節點上面, $O(n^2)$。
public TreeNode Convert(TreeNode root){
if(root == null) return null;
TreeNode head = Convert(root.left);
if(head == null) head = root;
else{
TreeNode p = head;
while(p.right != null) p = p.right;
p.right = root;
root.left = p;
}
TreeNode right = Convert(root.right);
root.right = right;
if(right != null)
right.left = root;
return head;
}
2.標準答案見P170
面試題 28:字符串的排列
題目:輸入一個字符串,打印出該字符串中字符的所有排列。例如輸入字符
a、b、c
所能排列出來的所有字符串abc、acb、bac、bca、cab
和cba
。
題目分析
- 先不談解題思路,先看題目要求:打印出該字符串中字符的所有排列,如果字符串存在相同字符,那么打印出的結果也肯定會包含相同的字符串,但是只從題目字面上理解,是沒有讓我們去重的,因此可以不用管打印出的字符串中是否存在相同的情況;但是如果讓我們去重的話,先使用list.contains(o)判斷一下是否存在,再決定是否添加。
- 求解思路:
-將一個字符串看成由兩部分組成:第一部分為它的第一個字符,第二部分是后面的所有字符;
-我們求整個字符串的排列,可以看成兩步:首先求所有可能出現在第一個位置的字符,即把第一個字符和后面所有的字符交換;然后固定第一個字符,求后面所有字符的排列。
-很明顯,這是典型的遞歸思路。
題目考點及相關題目
本質:按照一定要求擺放若干數字,可以先求出這些數字的所有排列,然后再一一判斷每個排列是否滿足題目所給定的要求。
本題擴展:求字符的所有組合(如對于字符串
abc
的所有組合a, b, c, ab, ac, bc, abc
),求解思路仍然差不多,將輸入的n
個字符看成兩部分:第一個字符和后面所有字符,在構成長度m的組合時,分兩種情況考慮:1)如果包含第一個字符,則需要從后面的所有字符中選取m-1
個字符,2)如果不包含第一個字符,則從后面的所有字符中選取m
個字符。分析到這里,就感覺可以用動態規劃了,遞推公式:$f(n,m) = f(n-1, m-1) + f(n-1, m)$,具體實現就不細說了。
相關題目:八皇后問題(回溯);8個數字放在正方體8個頂點上,問是否有可能使得正方體三組相對的面上的4個頂點的和都相等。
我的代碼如下:
public List<String> Permutation(String string){
List<String> res = new ArrayList<String>();
Permutation(new StringBuffer(string), 0, res);
return res;
}
public static void Permutation(StringBuffer sb, int start, List<String> list){
if(start == sb.length()) {
// 這是去重后的結果
/* if(!list.contains(sb.toString())){
list.add(sb.toString());
System.out.println(sb);
}*/
// 不去重的結果
list.add(sb.toString());
}else{
for(int i = start; i < sb.length(); i ++){
char temp = sb.charAt(i);
sb.setCharAt(i, sb.charAt(start));
sb.setCharAt(start, temp);
Permutation(sb, start + 1, list);
// 對更改的內容進行還原
temp = sb.charAt(i);
sb.setCharAt(i, sb.charAt(start));
sb.setCharAt(start, temp);
}
}
}
第4節:本章小結
當遇到難題,沒有任何思路時,一般的求解辦法:畫圖、舉例、分解。
具體算法就會涉及到分治法、動態規劃法,都是通過分解問題而來。