T1是一棵含有几百万个节点的树,T2含有几百个节点。判断T2是否是T1 的子树。
首先考虑小数据量的情况,可以根据树的前序和中序遍历所得的字符串,来通过判断T2生成的字符串是否是T1字符串的子串,来判断T2是否是T1的子树。假设T1的节点数为N,T2的节点数为M。遍历两棵树算法时间复杂性是O(N + M), 判断字符串是否为另一个字符串的子串的复杂性也是O( N + M)(比如使用KMP算法)。所需要的空间也是O(N + M)。
这里有一个问题需要注意:对于左节点或者右节点为null的情况,需要在字符串中插入特殊字符表示。否则对于下面这种情形将会判断错误:
因此如果插入特殊字符,上述两棵树的中序和前序遍历的结果是相同的。
由于本例有几百万的节点,需要占用O(N + M)的内存。
如果换一种思路,就是遍历T1,每当T1的某个节点与T2的根节点值相同时,就判断两棵子树是否相同。这个算法的复杂度是O(N*M)。我们再仔细 思考一下。因为只有在节点值与T2的根节点值相同才会调用O(M)。假设有K次这种情况,那么算法的复杂度就是O(N + K*M)。
- package Tree_Graph;
- import CtCILibrary.AssortedMethods;
- import CtCILibrary.TreeNode;
- public class S4_8 {
- // 判断t2是否是t1的子树
- public static boolean isSubTree(TreeNode t1, TreeNode t2) {
- if(t2 == null) { // 空树始终是另一个树的子树
- return true;
- }
- if(t1 == null) { // 此时r2非空,非空树不可能是一个空树的子树
- return false;
- }
- if(t1.data == t2.data) { // 找到两树匹配
- if(isSameTree(t1, t2)){
- return true;
- }
- }
- // 继续在r1的左子树和右子树里找匹配
- return isSubTree(t1.left, t2) || isSubTree(t1.right, t2);
- }
- // 判断两棵树是否相同
- public static boolean isSameTree(TreeNode t1, TreeNode t2) {
- if(t1==null && t2==null) {
- return true;
- }
- if(t1==null || t2==null) {
- return false;
- }
- if(t1.data != t2.data) {
- return false;
- }
- return isSameTree(t1.left, t2.left) && isSameTree(t1.right, t2.right);
- }
- public static void main(String[] args) {
- // t2 is a subtree of t1
- int[] array1 = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13};
- int[] array2 = {2, 4, 5, 8, 9, 10, 11};
- TreeNode t1 = AssortedMethods.createTreeFromArray(array1);
- TreeNode t2 = AssortedMethods.createTreeFromArray(array2);
- if (isSubTree(t1, t2))
- System.out.println("t2 is a subtree of t1");
- else
- System.out.println("t2 is not a subtree of t1");
- // t4 is not a subtree of t3
- int[] array3 = {1, 2, 3};
- TreeNode t3 = AssortedMethods.createTreeFromArray(array1);
- TreeNode t4 = AssortedMethods.createTreeFromArray(array3);
- if (isSubTree(t3, t4))
- System.out.println("t4 is a subtree of t3");
- else
- System.out.println("t4 is not a subtree of t3");
- }
- }
对于上面讨论的2种解法,哪种解法比较好呢?其实有必要好好讨论一番:
1)方法一会占用O(N + M)的内存,而另外一种解法只会占用O(logN + logM)的内存(递归的栈内存)。当考虑scalability扩展性时,内存使用的多寡是个很重要的因素。
2)方法一的时间复杂度为O(N + M),方法二最差的时间复杂度是O(N*M)。所以要通过工程实践或者是历史数据看一下哪种方法更优。当然了,方法二也可能会很早发现两棵树的不同,早早的退出了checkSubTree。
总的来说,在空间使用上,方法二更好。在时间上,需要通过实际数据来验证。