樹和圖是面試當中最常考的內容之一。
幾個要點:
1. 樹,二叉樹Binary Tree,二叉搜尋樹Binary Seach Tree,定義。
2. 二叉樹的幾種遍曆:前序preOrder,中序inOrder,和後序postOrder,層次遍曆。其中層次遍曆非常的重要,很多公司都喜歡考,其實就是圖的廣度優先搜尋Breath First Search。
3. 圖的表示:臨接表和臨接矩陣。
4. 圖的遍曆:深度優先DFS,廣度優先BFS,這兩個遍曆演算法的時間複雜度都是O(|V|+|E|),V是節點數,E是邊數,但是兩種遍曆有不同的適用情境,這個要根據題目來。
Java描述的一棵Binary Search Tree:包括建立,插入節點,尋找節點,中序遍曆,前序走訪,後序遍曆。
注意二叉搜尋樹的平均查詢時間複雜度是O(logN),和二分尋找的時間複雜度一樣。
public class BinarySearchTree {private TreeNode root;private class TreeNode {int data;TreeNode leftChild;TreeNode rightChild;TreeNode(int v) {data = v;leftChild = null;rightChild = null;}}/** * create an empty binary tree */public BinarySearchTree() {root = null;}/** * search for a node with given data will use a helper function: TreeNode * lookup(TreeNode node, int data). */public TreeNode lookup(int data) {return lookup(root, data);}private TreeNode lookup(TreeNode node, int data) {if (node == null) {return null;} else {if (data == node.data) {return node;} else if (data < node.data) {return lookup(node.leftChild, data);} else {return lookup(node.rightChild, data);}}}/** * insert a new node to the this binary tree with the help of another * function: TreeNode insert(TreeNode node, int data) */public void insert(int data) {root = insert(root, data);}/** * recursively insert a new node */private TreeNode insert(TreeNode node, int v) {if (node == null) {return new TreeNode(v);} else {if (v <= node.data) {node.leftChild = insert(node.leftChild, v);} else {node.rightChild = insert(node.rightChild, v);}return node;}}/** * inOrder traverse */public void printInOrder(TreeNode node) {if (node.leftChild != null) {printInOrder(node.leftChild);}System.out.print(node.data + " ");if (node.rightChild != null) {printInOrder(node.rightChild);}}/** * preOrder traverse */public void printPreOrder(TreeNode node) {System.out.print(node.data + " ");if (node.leftChild != null) {printPreOrder(node.leftChild);}if (node.rightChild != null) {printPreOrder(node.rightChild);}}/** * postOrder traverse */public void printPostOrder(TreeNode node) {if (node.leftChild != null) {printPostOrder(node.leftChild);}if (node.rightChild != null) {printPostOrder(node.rightChild);}System.out.print(node.data + " ");}}
好了,開始做題吧。
1. Implement a function to check if a binary tree is balanced. For the purpose of this question, a balanced tree is defined to be a tree such that the heights of the two subtrees of any node never differ by more than one. 檢查一顆二叉樹是否平衡。這裡平衡二叉樹指任何節點的左右子樹高度差不超過1。
來源:Cracking the Coding Interview 5th Edition,Charpter4,Question1.
分析解答:首先,如何獲得一顆二叉樹的高度?利用遞迴,二叉樹的演算法大部分都是應用遞迴演算法。二叉樹的高度:就是根節點左右子樹的高度中大的那個再加上1。這個演算法的時間複雜度是O(N),因為它訪問了每個節點一次。
public int getHeight(TreeNode node){if(node == null){return 0;}int leftHeight = getHeight(node.leftChild);int rightHeight = getHeight(node.rightChild);return Math.max(leftHeight, rightHeight)+1;}
方法一:自頂而下,檢查根節點的左右子樹高度差,檢查左子樹子樹的高度差,右子樹子樹的高度差…以此直到檢查完所有的節點。
虛擬碼:
boolean isBalanced(root){leftHeight = getHeight(root.leftChild);rightHeight = getHeight(root,rightChild);if(abs(leftHeight-rightHeight) > 1){return false;}else{return isBalanced(root.leftChild)&&(root.rightChild);}}
這裡getHeight在每個節點上都被重複調用了,所以時間複雜度是O(N^2),面試的時候,一般來說如果你做出了一個O(N^2)的演算法,你就要想想看很有可能還存在更好的演算法。尤其是樹,如果每個節點只碰了一遍就可以解決問題,那就可以做到O(N)。
方法二:自下而上:分別檢查左、右子樹是否平衡,如果不平衡直接結束,然後再檢查左右子樹的高度差。這樣,每個節點就只訪問了一次,因此時間複雜度是O(N)。即,在每次計算左右子樹的高度時,就完成是否平衡的檢查操作。如果平衡則返回子樹的高度,不然返回某個flag,如-1,直接結束檢查。
Java代碼:http://pastebin.com/VNn7D85q 這裡的checkHeight函數完成了如上的功能。
最後還要插一句,關於如何二叉樹的刪除,還有平衡。一般來說,刪除多個節點以後,有可能導致二叉樹進入不平衡的狀態,這裡的平衡並不是說所有左右子樹的高度都一樣,而是差值在一定的範圍內,如該題規定的差值範圍是一。有時候會採用marked as deleted的方式來進行刪除操作,實際上並沒有真正的刪除。而如果真正的刪除了元素,並且要時刻保持一顆樹處於平衡狀態,那麼就需要進行節點的旋轉rotation操作來重構一棵樹。旋轉操作的情況很多,左旋、右旋等等。實際比較複雜,所以一般來說不會在面試題裡出現。詳細的請參看
Mark Allen Weiss 的Data Structures and Algorithm Analysis in C 2nd Edition ,其中樹的那章,AVL樹和紅/黑樹狀結構有詳細的旋轉操作講解和實現。
2. Given a sorted (increasing order) array, write an algorithm to create a binary search tree with minimal height. 給定一個排序(升序排列)數組,產生一個具有最小高度的二叉搜尋樹。
來源:Cracking the Coding Interview 5th Edition,Charpter4,Question3.
分析解答:如果對BST二叉排序樹(以下稱為BST)進行中序遍曆,那麼得到的結果就是一個升序排列的數組。這個性質非常的重要,可以用來檢測一個二叉樹是否是BST。而這裡要求高度最小,那麼就是要求做到盡量的平衡。很顯然,就是將數組不斷的從中間分割,形成左右子樹。
Java代碼:http://pastebin.com/4BxK03hS 這裡用到了一個helper function。這個解答的時間複雜度是O(N),因為數組的每個元素只訪問了一遍。
3. Implement a function to check if a tree is a binary search tree. 檢查一顆二叉樹是否是二叉排序樹。
來源:Cracking the Coding Interview 5th Edition,Charpter4,Question5.
分析解答: 方法一 這裡可以直接利用上一題的性質,即如果一棵二叉樹是BST,那麼它的中序遍曆是一個遞增的數組。所以可以對中序遍曆演算法稍加修改,每次遍曆,記錄上一個元素,並跟當前節點元素值比較。
static int lastVisit = Integer.MIN_VALUE;public static boolean isBST(TreeNode n){if(n == null){return true;}if(!isBST(n.leftChild)){return false;}if(n.data < lastVisit){return false;}lastVisit = n.data;if(!isBST(n.rightChild)){return false;}return true;}
方法二:利用BST的性質:即任何一個節點的左孩子的值,小於該節點的值,小於該節點右孩子的值,注意這裡需要的是任意節點。並且,左子樹的所有節點的的值,都小於其父節點的值,小於所有的右子樹的值。
例如: 10
4 12
2 11
這就不是一顆BST,因為節點11的位置錯了,它應該在10的右邊,11的左邊。
這裡採用自頂向下的方法:每往下一層,就得到了該層節點取值的範圍,如果不在這個範圍內,則不是BST。例如上述的一顆BST,第一層 10的取值範圍是[MIN, MAX],第二層左子樹的範圍是[MIN, 10],右子樹是[10, MAX],到了第三層,4的左子樹的範圍是[MIN, 4],右子樹範圍是[4,10],依次迭代下去。
Java代碼:http://pastebin.com/9XEDhF2D 這個演算法的時間複雜度是O(N),因為訪問了每個節點一次。
4. Mirror a tree. 將一棵樹鏡像。
分析解答:如果上面幾個題都看懂了,這個問題就十分簡單了,就用最直觀的遞迴演算法。這個題目沒什麼難度,稍微好點的公司可能一上來就會問這個題。
一棵樹的鏡像就是將這個樹做抽對稱,也就是把所有節點的左子樹和右子樹對掉。
例如: 10 的鏡像就是 10
4 12 12 4
2 11 2 11
中序遍曆的結果會變成遞減排序的數組,
記住,遞迴演算法一定不能忘記base case,一般來說是輸入為NULL值這種情況,有時候還有一些特殊取值。
Java 代碼:
public void mirror(TreeNode n){if(n == null){return;}mirror(n.leftChild);mirror(n.rightChild);TreeNode temp = n.leftChild;n.leftChild = n .rightChild;n.rightChild = temp;}
5. Given a binary tree and a number, check if there exists a path from root to leaf, that the total sum of every node's value is equal to the number. 給定一個二叉樹以及一個數,判斷是否存在這樣的一條從根節點要葉子節點的路徑,該路徑上每個節點的值的和等於輸入整數。
分析解答: 很簡單的一道題了,如果你對遞迴已經比較熟練的話。給出代碼:這裡的base case就是輸入根為null,此時判斷輸入的number是否是0。
public boolean hasPathSum(TreeNode n, int sum){if(n == null){return (sum == 0);}int sum_left = sum - n.data;return hasPathSum(n.leftChild,sum_left)||hasPathSum(n.rightChild, sum_left);}
6. Print all paths from root to every leaf node. 列印從根節點到分葉節點的所有路徑。
分析解答:其實就是一個樹的DFS,DFS是採用遞迴方式進行的。具體的說,就是先訪問根節點,再訪問完左子樹,再訪問完右子數,每次訪問到最深處的葉子節點,就列印這一條路徑,因此在遞迴調用的時還要有一個變數來儲存目前為止訪問過的這條路徑的所有節點。
Java代碼:http://pastebin.com/zgXAxuBa 演算法的時間複雜度是O(logN),因為一共有logN層的節點。
7. Given a binary tree in which each node contains a value. Design an algorithm to print all paths which sum to a given value. Note that a path can start or end anywhere in the tree. 給出一個二叉樹,這個樹的每個節點包含一個值,給出一個值,列印所有的路徑,使得該路徑上所有節點值的和等於該值。這條路徑可以從樹的任何一個節點開始或者結束。
題目來源:Cracking the Coding Interview 5th Edition. Chapter 4, Q 9.
分析解答:看起來和上面兩個題很像:求和,列印路徑,但這裡有一個條件,這條路徑可以從任意節點開始並且結束。
很容易想到的brute force解法,DFS遍曆每個節點,每訪問一下節點,從該節點開始往下再DFS遍曆求和,如果等於給定值,就輸出路徑;小於則繼續向下;大於則返回。
Java 代碼:http://pastebin.com/F7WVeSW6
But wait!!! 這裡忽視了一個情況,就是節點的值可能為負!那麼可能存在如有一種路徑{2,5, 6 -2, -4},和{2, 5}這條路徑的和一樣都是7!!
其實只要把代碼稍加修改,在helper function裡面,去掉相等之外的兩種情況就可以了。
那麼這個演算法的時間複雜度又是多少?O(NlogN),因為迭代了N個節點,而每次被迭代的節點,向上累加了一共logN層。
8. Given two binary trees: T1 which has millions of nodes, and T2 which has hundreds of nodes. Design an algorithm to decide if T2 is a subtree of T1. 兩棵二叉樹,T1和T2,T1有百萬個節點,T2有幾百個節點,判斷T2是否是T1的子樹。
題目來源:Cracking the Coding Interview 5th Edition. Chapter 4, Q 8.
分析解答:如何判斷一棵樹和另一顆樹相等/子樹,最簡單的方法是同時用中序,和先序、後序遍曆中的一種,得到遍曆後的串,然後判斷這兩個串是否相等/是另一個串的字串。注意這裡中序遍曆是必要的,然後在先序和後序裡二選一,這樣才可以唯一確定一棵樹。至於為什麼,以及如何有兩種遍曆的結果得出第三個,你可以看這篇文章。http://www.cnblogs.com/zhangchaoyang/articles/2008434.html
如何判斷一個串是另一串的子串,可以用suffix tree。這個我會在接下來的文章裡補充。記得面amazon的時候就是死在了這題上…當時都不知到trie是個啥,現在面試又狂喜歡問,衰!
但是這裡題目已經說了T1很大很大很大,你固然可以用上面這個方法做,但是絕對不是面試官希望看到的做法,可儘管如此,你需要提出來上面這個解法。行不行是一回事,會不會,想沒想到又是另外一回事。因為T1特別大,所以你需要很大的記憶體來存所有節點的值,當然你可以辯解沒說這個資料結構再大也不會有那個T1大…好吧,拿你沒辦法。
怎麼做呢?
遍曆T1的每一個節點,當遇到一個節點,它的值和T2的根節點的值相同時,判斷這個T1的子樹和T2是不是同一棵樹。判斷是否是同一子樹,就是利用遞迴的方法判斷了。所以這裡需要兩個函數,一個isSubTree(T1,T2),另一個是isSameTree(T1, T2)。
給出java的實現:http://pastebin.com/MF1rFPEF
參考:
【1】.http://cslibrary.stanford.edu/110/BinaryTrees.html
【2】.
Cracking the Coding Interview 5th Edition.