1.实现一个函数,检查二叉树是否平衡。在这个问题中,平衡树的定义如下:任意一个结点,其两棵子树的高度差不超过1。
class TreeNode { int data; TreeNode left; TreeNode right; public TreeNode(int value) { this.data = value; this.left = null; this.right = null; } } public class BalanceTree { public static void main(String[] args) { // TODO Auto-generated method stub } public static int getHeight(TreeNode root) { if (root == null) { return 0; } return Math.max(getHeight(root.left), getHeight(root.right)) + 1; } public static boolean isBalanced(TreeNode root) { if (root == null) { return true; } int heightDiff = getHeight(root.left) - getHeight(root.right); if (Math.abs(heightDiff) > 1) { return false; } else { return isBalanced(root.left) && isBalanced(root.right); } } }
思路:上述代码直接递归访问整棵树,计算每个结点两棵子树的高度。虽然可行,但是效率不高,getHeight会被反复调用计算同一个结点的高度。这个算法的时间复杂度为o(NlogN)。
class TreeNode { int data; TreeNode left; TreeNode right; public TreeNode(int value) { this.data = value; this.left = null; this.right = null; } } public class BalanceTree { public static void main(String[] args) { // TODO Auto-generated method stub } public static int checkHeight(TreeNode root) { if (root == null) { return 0;//高度为0 } //检查左子树是否平衡 int leftHeight = checkHeight(root.left); if (leftHeight == -1) { return -1;//不平衡 } //检查右子树是否平衡 int rightHeight = checkHeight(root.right); if (rightHeight == -1) { return -1;//不平衡 } //检查当前结点是否平衡 int heightDiff = leftHeight - rightHeight; if (Math.abs(heightDiff) > 1) { return -1; } else { return Math.max(leftHeight, rightHeight) + 1; } } public static boolean isBalanced(TreeNode root) { if (checkHeight(root) == -1) { return false; } else { return true; } } }
改进的算法会从根节点递归向下检查每棵子树的高度。通过checkHeight方法,以递归方式获取每个结点左右子树的高度。若子树是平衡的,则checkHeight返回该子树的实际高度。 若子树不平衡,则checkHeight会立即中断执行,并返回-1。该算法的时间复杂度为o(N),空间复杂度为o(H),其中H为树的高度。
2.给定有向图,设计一个算法,找出两个结点之间是否存在一条路径。
思路:只需通过图的遍历,BFS或DFS就能解决问题。从两个结点的其中一个出发,在遍历过程中,检查是否找到另一个结点。
访问过的结点都应标记为“已访问”,以免循环和重复访问结点。
public enum State { Unvisited, Visited, Visiting; } public static boolean search(Graph g,Node start,Node end) { LinkedList<Node> q = new LinkedList<Node>(); for (Node u : g.getNodes()) { u.state = State.Unvisited; } start.state = State.Visiting; q.add(start); Node u; while(!q.isEmpty()) { u = q.removeFirst(); if (u != null) { for (Node v : u.getAdjacent()) { if (v.state == State.Unvisited) { if (v == end) { return true; } else { v.state = State.Visiting; q.add(v); } } } u.state = State.Visited; } } return false; }
3.给定一个有序整数数组,元素各不相同且按升序排列,编写一个算法,创建一棵高度最小的二叉查找树。
思路:要创建一棵高度最小的二叉树,必须让左右子树的结点数量越接近越好。即让数组的中间值成为根节点,数组左边一半成为左子树,右边一半成为右子树。然后以类似的方式构造整棵树。数组每一区段的中间元素成为子树的根节点,左半部分成为左子树,右半部分成为右子树。以递归方式调用createMinimalBST方法,该方法会传入数组的一个区段,并返回最小树的根节点。该算法简述如下:(1)将数组中间位置的元素插入树中。(2)将数组左半边元素插入左子树。(3)将数组右半边元素插入右子树。(4)递归处理。
public TreeNode createMinimalBST(int arr[], int start, int end){ if (end < start) { return null; } int mid = (start + end) / 2; TreeNode n = new TreeNode(arr[mid]); n.left = createMinimalBST(arr, start, mid - 1); n.right = createMinimalBST(arr, mid + 1, end); return n; } public TreeNode createMinimalBST(int array[]) { return createMinimalBST(array, 0, array.length - 1); }
4.给定一棵二叉树,设计一个算法,创建含有某一深度上所有结点的链表(比如,若一棵树的深度为D,则会创建出D个链表)。
思路:可以用任意方法遍历整棵树,只要记住结点位于哪一层即可。
解法一:将前序遍历算法(根->左->右)稍作修改,将level+1传入下一个递归调用。使用深度优先搜索实现。
public static void createLevelLinkedList(TreeNode root, ArrayList<LinkedList<TreeNode>> lists, int level) { if (root == null) { return;//终止条件 } LinkedList<TreeNode> list = null; //得到list if (lists.size() == level) {//该层不在链表中 list = new LinkedList<TreeNode>(); lists.add(list);//将该层添加到链表末端 } else { list = lists.get(level); } list.add(root); createLevelLinkedList(root.left, lists, level + 1); createLevelLinkedList(root.right, lists, level + 1); } public static ArrayList<LinkedList<TreeNode>> createLevelLinkedList(TreeNode root) { ArrayList<LinkedList<TreeNode>> lists = new ArrayList<LinkedList<TreeNode>>(); createLevelLinkedList(root, lists, 0); return lists; }
解法二:对广度优先搜索稍加修改,即从根节点开始迭代,然后第2层,第3层等等。处于第i层时,表明已访问过第i-1层的所有结点。也就是说,要得到i层的结点,只需直接查看i-1层结点的所有子结点即可。
public static ArrayList<LinkedList<TreeNode>> createLevelLinkedList(TreeNode root) { ArrayList<LinkedList<TreeNode>> result = new ArrayList<LinkedList<TreeNode>>(); LinkedList<TreeNode> current = new LinkedList<TreeNode>(); if (root != null) { current.add(root); } while (current.size() > 0) {//当前层不为空 result.add(current);//加入当前层 LinkedList<TreeNode> parents = current;//转到下一层 current = new LinkedList<TreeNode>();//清空,装下一层 for (TreeNode parent : parents) { if (parent.left != null) { current.add(parent.left); } if (parent.right != null) { current.add(parent.right); } } } return result; }
两者的时间复杂度都是o(N)。在空间复杂度方面,两者都要返回o(N)数据,因此第一种解法递归实现所需的额外o(logN)空间,跟必须传回的o(N)数据相比,并不算多。从大O记法的角度看,两者效率是一样的。
5.实现一个函数,检查一棵二叉树是否为二叉查找树。
解法一:中序遍历(左->根->右),将所有元素复制到数组中,然后检查该数组是否有序。问题在于,无法正确处理树中的重复值。代码实现基于树中不包含重复值的假设。
public static int last_printed = Integer.MIN_VALUE;//记录与当前元素比较的最后元素 public static boolean checkBST(TreeNode root) { if (root == null) { return true; } //递归检查左子树 if (!checkBST(root.left)) { return false; } //检查当前根结点 if (root.data <= last_printed) { return false; } last_printed = root.data; //递归检查右子树 if (!checkBST(root.right)) { return false; } return true;//全部检查完毕 }
解法二:满足二叉查找树的条件:所有左边的结点必须小于或等于当前结点,而当前结点必须小于所有右边的结点。
利用这一点,可以通过自上而下传递最小和最大值来解决问题。在迭代遍历整个树的过程中,用逐渐变窄的范围来检查各个结点。首先,从(min=INT_MIN,max=INT_MAX)这个范围开始检查根结点,处理左子树时,更新max。处理右子树时,更新min。只要有任一结点不能通过检查,则停止并返回false。
时间复杂度为O(N),N为整棵树的结点数,已是最佳做法。对于平衡树,空间复杂度为O(logN)。
public boolean checkBST(TreeNode root) { return checkBST(root, Integer.MIN_VALUE, Integer.MAX_VALUE); } public boolean checkBST(TreeNode root, int min, int max) { if (root == null) { return true; } if (root.data < min || root.data >= max) { return false; } if (!checkBST(root.left, min, root.data) || !checkBST(root.right, root.data, max)) { return false; } return true; //return (min < root.data && root.data< max) && checkBST(root.left, min, root.data) && checkBST(root.right, root.data, max); }
记住,在递归算法中,一定要确定终止条件以及结点为空的情况得到妥善处理。
6.设计一个算法,找出二叉查找树中指定结点的“下一个”结点(也即中序后继)。可以假定每个结点都含有指向父节点的连接。
思路:假定我们有一个假想的结点,访问顺序是左子树、当前结点、右子树。显然,下一个结点应该位于右边。
到底是右子树的哪个结点呢?如果中序遍历右子树,它就是接下来第一个被访问的结点,也就应该是右子树最左边的结点。
但是若这个结点没有右子树呢?若结点n没有右子树,就表示已遍访n的子树。必须回到n的父节点,记作q。
若n在q的左边,下一个我们应该访问的结点就是q(中序遍历(左->根->右))。若n在q的右边,则表示已遍访q的子树。需要从q往上访问,直至找到还未完全遍访过的结点x。
怎么才能知道还未完全遍历结点x呢?左结点已完全遍历,但其父结点尚未完全遍历。
如果一路往上遍访这棵树都没发现左结点呢?说明已位于树的最右边,不会再有中序后继,返回NULL。
public static TreeNode inorderSucc(TreeNode node) { //处理结点为空的情况 if (node == null) { return null; } if (node.right != null) {//node有右子树 return leftMostChild(node.right);//返回右子树的最左边结点 } else { TreeNode q = node; TreeNode x = q.parent;//node的父节点 while (x != null && x.left != q) {//直至q是其父节点的左子树 q = x; x = x.parent; } return x;//返回当前根结点x } } public static TreeNode leftMostChild(TreeNode node) { if (node == null) { return null; } while (node.left != null) { node = node.left; } return node; }
7.设计并实现一个算法,找出二叉树中某两个结点的第一个共同祖先。不得将额外的结点储存在另外的数据结构中。注意:这不一定是二叉查找树。
解法:《九章算法》chapter three LCA最近公共祖先I II III
8.你有两棵非常大的二叉树:T1,有几百万个结点;T2,有几百个结点。设计一个算法,判断T2是否为T1的子树。如果T1有这么一个结点n,其子树与T2一模一样,则T2为T1的子树。也就是说,从结点n处把树砍断,得到的树与T2完全相同。
思路:在规模较小且较简单的问题中,可以创建一个字符串,表示中序和前序遍历(中序+前序遍历确定一棵树)。若T2前序遍历是T1前序遍历的子串,并且T2中序遍历是T1中序遍历的子串,则T2为T1的子树,利用后缀树可以在线性时间内检查是否为子串。
解法:鉴于该问题指定的约束条件,方法是搜遍较大的那棵树T1。每当T1的某个结点与T2的根节点匹配时,就调用treeMatch。treeMatch方法会比较两棵子树,检查两者是否相同。
boolean containsTree(TreeNode t1, TreeNode t2) { if (t2 == null) {//空树一定是子树 return true; } return subTree(t1, t2); } boolean subTree(TreeNode r1, TreeNode r2) { if (r1 == null) { return false;//大的树已经空了,还未找到子树 } if (r1.data == r2.data) { if (matchTree(r1, r2)) { return true; } } return (subTree(r1.left, r2) || subTree(r1.right, r2)); } boolean matchTree(TreeNode r1, TreeNode r2) { if (r1 == null && r2 == null) {//若两者都为空 return true;//子树中已无结点 } if (r1 == null || r2 == null) {//若其中之一为空,但并不同时为空 return false; } if (r1.data != r2.data) {//结点数据不匹配 return false; } return (matchTree(r1.left, r2.left) && matchTree(r1.right, r2.right)); }
运行时间粗略一看是O(NM),N为T1结点数,M为T2结点数。但实际上不必对T2的每个结点调用treeMatch,而是会调用k次,其中k为T2根节点在T1中出现的次数。因此,运行时间接近于O(n+km)。即使这样运行时间也有所夸大。即使根结点相同,一旦发现T1和T2有结点不同,就会退出treeMatch。因此每次调用treeMatch,也不见得都会查看m个结点。
9.给定一棵二叉树,其中每个结点都含有一个数值。设计一个算法,打印结点数值总和等于某个给定值的所有路径。注意,路径不一定非得从二叉树的根结点或叶结点开始或结束。
【可能有个隐含条件:必须是向下的路径,即结点的层级逐个递增。】
《九章算法》chapter three 二叉树的路径和II
思路:运用简化推广法来解题。
部分1:简化——假设路径必须从根节点开始,但可以在任意结点结束,怎么解决?
可以从根结点开始,向左向右访问子结点,计算每条路径上到当前结点为止的数值总和,若与给定值相同则打印当前路径。注意,就算找到总和,仍要继续访问这条路径。因为这条路径可能继续往下经过a+1结点和a-1结点(或其他数值总和为0的结点序列),完整路径的总和仍然等于sum。
部分2:推广——路径可从任意结点开始。在这种情况下,对于每个结点,我们都会向“上”检查是否得到相符的总和。也就是说不再要求“从这个结点开始是否会有总和为给定值的路径”,而是关注“这个结点是否为 总和为给定值的 某条路径的末端”。
递归访问每个结点n时,会将root到n的完整路径传入该函数。随后,这个函数会以相反的顺序,从n到root,将路径上的结点值加起来。当每条子路径的总和等于sum时,就打印这条路径。
public static void findSum(TreeNode node, int sum) { int depth = depth(node); int[] path = new int[depth]; findSum(node, sum, path, 0); } //求树的深度 public static int depth(TreeNode node) { if (node == null) { return 0; } else { return Math.max(depth(node.left), depth(node.right)) + 1; } } public static void findSum(TreeNode node, int sum, int[] path, int level) { if (node == null) { return; } path[level] = node.data;//将当前结点插入路径 //从当前结点到root查找以此为终点的且总和为sum的路径 int t = 0; for (int i = level; i >= 0; i--) { t += path[i]; if (t == sum) { print(path, i, level); } } //查找当前结点之下的结点 findSum(node.left, sum, path, level + 1); findSum(node.right, sum, path, level + 1); path[level] = Integer.MIN_VALUE; } public static void print(int[] path, int start, int end) { for (int i = start; i <= end; i++) { System.out.print(path[i] + " "); } System.out.println(); }
时间复杂度为O(nlogn),空间复杂度为O(logn)。