二叉树中找出2个节点的最近公共祖先

给定二叉树和两个节点n1n2,编写程序以找到他们的最近公共祖先。


在做这个之前, 有些东西需要提前问清楚, 二叉树是不是二叉搜索树, 如果是二叉搜索树, 那就好处理多了. 在保证2个节点都属于此二叉树的情况下, 由于二叉搜索树是排序过的, 位于左子树的结点都比父结点小, 而位于右子树的结点都比父结点大, node.data>node.left.data && node.data>node.right.data , 我们只需要从树的根结点开始和两个输入的结点进行比较。

如果当前结点的值比两个结点的值都大,那么最低的共同父结点一定是在当前结点的左子树中,于是下一步遍历当前结点的左子结点。

如果当前结点的值比两个结点的值都小,那么最低共同父结点一定在当前结点的右子树中,于是下一步遍历当前结点的右子结点。

这样在树中从上到下找到的第一个在两个输入结点的値之间的结点,就是最低的公共祖先。

- (void)viewDidLoad {
    [super viewDidLoad];
    
    [self findCommmonTreeNode];

}

- (void)findCommmonTreeNode {
    TreeNode * tree = [TreeNode nodeWithDataArray:@[@(10),@(6),@(14),@(4),@(8),@(12),@(16),@(1),@(5),@(7),@(9)]];

    int p = 7;
    int q = 9;

    [self __findCommonTreeNode:tree withFirstData:p secondData:q];
    [self __findCommonTreeNode2:tree withFirstData:p secondData:q];
    TreeNode * node = [self __findCommonTreeNode3:tree withFirstData:p secondData:q];
    NSLog(@"方法3,找到了 %d",node.data);
    
}
/// 二叉搜索树找到公共节点, 前提是这2个节点肯定在二叉树中,  如果左右节点不存在也会有打印
- (TreeNode *)__findCommonTreeNode:(TreeNode *)tree withFirstData:(int)firstData secondData:(int)secondData {
    
    while (tree) {
        
        if (tree.data > firstData && tree.data > secondData) {
            tree = tree.leftNode;
        } else if (tree.data < firstData && tree.data < secondData) {
            tree = tree.rightNode;
        } else {
            NSLog(@"找到了%d",tree.data);
            return tree;
        }
    }
    return nil;

}

这种搜索方式的时间复杂度是 O(树的深度),  时间效率还是挺好的.


如果面试官说不是二叉搜索树, 那就还有一个需要确认的, 二叉树的每个节点上有没有指向父节点的指针, 如果有, 那么问题可以简化很多.

我们可以不考虑这个是二叉树, 把n1, n2节点的parentNode串起来组成2个链表, 就可以把问题转化成求这2个链表的第一个公共节点了.更多方法可以看这个, https://blog.csdn.net/u014600626/article/details/107296340 , 此处放了一个hash法的, 时间复杂度是O(2个链表的长度和)

把其中一个链表放到set中, 然后遍历另一个链表, 在遍历过程中找到了set中包含的元素, 那就说明是第一个公共节点了

// 哈希表/Set,O(M+N)
- (void)__findCommonNodeHash:(Node *)firstNode secondNode:(Node *)secondNode {
    if (firstNode==nil || secondNode==nil) {
        return;
    }
    
    NSMutableSet * set = [NSMutableSet set];
    while (firstNode) {
        [set addObject:firstNode];
        firstNode = firstNode.next;
    }
    
    while (secondNode) {
        
        if ([set containsObject:secondNode]) {
            NSLog(@"hash法 找到了, 是%p %d",secondNode,secondNode.data);
            return;
        }
        
        secondNode = secondNode.next;
    }
    
    NSLog(@"hash法 没找到");
    
}

如果面试官说不行, 没有指向父节点的指针, 就是一个最普通的二叉树, 那就用更通用的解法.

方案1: 保存从根节点到n1, n2 的路径, 比如找1和7的公共节点, 根节点到1的路径是10->6->4->1 , 根节点到7的路径是10->6->8->7,  最近的公共祖先就是6,   

现在问题来了, 怎么保存这个路径, 可以参考 https://blog.csdn.net/u014600626/article/details/107060519 , 大致就是采用前序遍历, 然后保存每个遍历到的节点加入到数组中, 如果这个节点 == n1那就对数组进行一次copy操作, 遍历到n2也copy一次, 经过copy之后的数组就是从根节点到n1, n2 的路径了, 在从路径中找最后一个相同的节点即可. 这个需要对树遍历一次, 时间复杂度是O(n). 需要额外的空间保存路径, 空间上也是O(n).

/// 普通二叉树找到,
- (TreeNode *)__findCommonTreeNode2:(TreeNode *)tree withFirstData:(int)firstData secondData:(int)secondData {
    
    // 找第一个节点的路径
    NSArray<TreeNode *> * firstArray = [self __findPathInTreeNode:tree withData:firstData array:[NSMutableArray array]];
    // 找第二个节点的路径
    NSArray<TreeNode *> * secondArray = [self __findPathInTreeNode:tree withData:secondData array:[NSMutableArray array]];

    // 找2个路径中最后一个相同的node
    TreeNode * result = nil;
    for (int i = 0; i<firstArray.count && i<secondArray.count; i++) {
        if (firstArray[i] == secondArray[i]) {
            result = firstArray[i] ;
        }
    }
    NSLog(@"找到了,是%d",result.data);
    return result;
}

- (NSArray *)__findPathInTreeNode:(TreeNode *)tree withData:(int)data array:(NSMutableArray *)array {
    
    if (tree == nil) {
        return array;
    }
    // 保存走过的路径
    static NSMutableArray * pathArray = nil;
    if (pathArray == nil) {
        pathArray = [NSMutableArray array];
    }
    
    
    [pathArray addObject:tree];
    
    if (tree.data == data) {
        [array addObjectsFromArray:pathArray];
    }
    
    // 前序遍历, 根->左->右
    [self __findPathInTreeNode:tree.leftNode withData:data array:array];
    [self __findPathInTreeNode:tree.rightNode withData:data array:array];
    
    
    [pathArray removeObject:tree];
    return array;
}

方案2: 如果说辅助空间都不让用了,  还可以这样,  如果在其中的一个节点 左侧找到了n1 || n2 , 右侧也找到了n1 || n2中的一个, 那这个节点就是最近公共祖先了, 

举例来说, n1 = 1, n2 = 7, 这2个节点都在10的左侧, 10的右侧没有, 所以10不是, 继续向左侧寻找, 10的左侧是6, 6的左侧找到了1, 6的右侧找到了7, 6就是最近公共祖先了.

总结一下策略, 

递归结束的标志就是,  在某个节点的左字数发现一个节点, 右子树发现一个节点, 那就是此节点了;

如果左子树发现了, 右子树没有发现, 那就继续往左子树寻找;

如果右子树发现了, 左子树没有发现, 那就继续往右子树寻找;

时间复杂度的话, 只需要一次遍历树即可 , 时间复杂度是O(N),空间复杂度是O(1).

/// 普通二叉树找到, 不使用额外空间
- (TreeNode *)__findCommonTreeNode3:(TreeNode *)tree withFirstData:(int)firstData secondData:(int)secondData {

    if (tree) {
        NSLog(@"寻找到 %d",tree.data);
    }
    
    if (tree == nil || tree.data == firstData || tree.data == secondData) {
        return tree;
    }
    
    TreeNode * left = [self __findCommonTreeNode3:tree.leftNode withFirstData:firstData secondData:secondData];
    NSLog(@"在%d左侧寻找,找到了%d",tree.data,left.data);
    
    TreeNode * right = [self __findCommonTreeNode3:tree.rightNode withFirstData:firstData secondData:secondData];
    NSLog(@"在%d右侧寻找,找到了%d",tree.data,right.data);

    // 左边找到一个值, 右边找到一个值, 说明就是结果了
    if (left && right) {
        return tree;
    }
    // 左边找到了,右边没找到, 继续在左边寻找
    if (left) {
        return left;
    }
    // 右边找到了,左边没找到, 继续在右边寻找
    if (right) {
        return right;
    }
    
    return nil;
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值