树和图是面试当中最常考的内容之一。
几个要点:
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 == ) {
return node;
} else if (data < ) {
return lookup(, data);
} else {
return lookup(, 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 <= ) {
= insert(, v);
} else {
= insert(, v);
}
return node;
}
}
/**
* inOrder traverse
*/
public void printInOrder(TreeNode node) {
if ( != null) {
printInOrder();
}
( + " ");
if ( != null) {
printInOrder();
}
}
/**
* preOrder traverse
*/
public void printPreOrder(TreeNode node) {
( + " ");
if ( != null) {
printPreOrder();
}
if ( != null) {
printPreOrder();
}
}
/**
* postOrder traverse
*/
public void printPostOrder(TreeNode node) {
if ( != null) {
printPostOrder();
}
if ( != null) {
printPostOrder();
}
( + " ");
}
}
好了,开始做题吧。
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();
int rightHeight = getHeight();
return (leftHeight, rightHeight)+1;
}
方法一:自顶而下,检查根节点的左右子树高度差,检查左子树子树的高度差,右子树子树的高度差…以此直到检查完所有的节点。
伪代码:
boolean isBalanced(root){
leftHeight = getHeight();
rightHeight = getHeight(root,rightChild);
if(abs(leftHeight-rightHeight) > 1){
return false;
}else{
return isBalanced()&&();
}
}
这里getHeight在每个节点上都被重复调用了,所以时间复杂度是O(N^2),面试的时候,一般来说如果你做出了一个O(N^2)的算法,你就要想想看很有可能还存在更好的算法。尤其是树,如果每个节点只碰了一遍就可以解决问题,那就可以做到O(N)。
方法二:自下而上:分别检查左、右子树是否平衡,如果不平衡直接结束,然后再检查左右子树的高度差。这样,每个节点就只访问了一次,因此时间复杂度是O(N)。即,在每次计算左右子树的高度时,就完成是否平衡的检查操作。如果平衡则返回子树的高度,不然返回某个flag,如-1,直接结束检查。
Java代码:/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代码:/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()){
return false;
}
if( < lastVisit){
return false;
}
lastVisit = ;
if(!isBST()){
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代码:/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();
mirror();
TreeNode temp = ;
= 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 - ;
return hasPathSum(,sum_left)||hasPathSum(, sum_left);
}
6. Print all paths from root to every leaf node. 打印从根节点到叶节点的所有路径。
分析解答:其实就是一个树的DFS,DFS是采用递归方式进行的。具体的说,就是先访问根节点,再访问完左子树,再访问完右子数,每次访问到最深处的叶子节点,就打印这一条路径,因此在递归调用的时还要有一个变量来保存目前为止访问过的这条路径的所有节点。
Java代码:/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 代码:/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.
分析解答:如何判断一棵树和另一颗树相等/子树,最简单的方法是同时用中序,和先序、后序遍历中的一种,得到遍历后的串,然后判断这两个串是否相等/是另一个串的字串。注意这里中序遍历是必要的,然后在先序和后序里二选一,这样才可以唯一确定一棵树。至于为什么,以及如何有两种遍历的结果得出第三个,你可以看这篇文章。/zhangchaoyang/articles/
如何判断一个串是另一串的子串,可以用suffix tree。这个我会在接下来的文章里补充。记得面amazon的时候就是死在了这题上…当时都不知到trie是个啥,现在面试又狂喜欢问,衰!
但是这里题目已经说了T1很大很大很大,你固然可以用上面这个方法做,但是绝对不是面试官希望看到的做法,可尽管如此,你需要提出来上面这个解法。行不行是一回事,会不会,想没想到又是另外一回事。因为T1特别大,所以你需要很大的内存来存所有节点的值,当然你可以辩解没说这个数据结构再大也不会有那个T1大…好吧,拿你没办法。
怎么做呢?
遍历T1的每一个节点,当遇到一个节点,它的值和T2的根节点的值相同时,判断这个T1的子树和T2是不是同一棵树。判断是否是同一子树,就是利用递归的方法判断了。所以这里需要两个函数,一个isSubTree(T1,T2),另一个是isSameTree(T1, T2)。
给出java的实现:/MF1rFPEF
参考:
【1】./110/
【2】. Cracking the Coding Interview 5th Edition.