递归与非递归寻找二叉树中2个节点的最低公共祖先节点

题目:

给定一颗二叉树的根节点,以及二叉树中的2个节点的指针,求这2个节点的最低公共父节点。二叉树的存储形式中只有左右节点的指针。

要求:

分别用递归和非递归来做。

注意事项:

思考过程中,要想到的有一些特殊情况:
如果一个节点是另一个节点的父节点或祖宗节点,则最低公共父节点为空为该祖宗节点的父节点;
若该祖宗节点是根节点,则认为没有公共父节点。
当然,这一条具体如何规定,是可以商榷的。比如说,规定祖宗节点就是最低公共父节点也不是不行,只是程序写法会略有不同。

分析:

分别找出从根节点到这2个节点的路径,路径体现为单向链表。
然后分别从头遍历这2个单向链表,找到最后一个在相同位置的相同点,但这个点不可以和这2个节点中的任何一个相同。
“找到最后一个在相同位置的相同点”,就是指的是找到最低公共父节点,“相同位置”指的就是在链表中的第几个的位置。
“这个点不可以和这2个节点中的任何一个相同”,就是当这2个节点一个是祖宗节点,一个是子孙节点是,要以祖宗节点的父节点为公共父节点。

由以上分析可见,只要有了这2个单向链表,找出公共父节点并不难。难的是,如何找到这2条路径,并将其存成一个单向链表。
找到路径的过程,可以有递归和非递归2种方式。
首先,确定数据结构:
无论递归或非递归的方式,我们都需要一种数据结构来保存这个单向链表。假设二叉树节点的数据结构如下:

struct TreeNode {
    int data;
    TreeNode *left;
    TreeNode *right;
    TreeNode(int d=0):data(d), left(NULL), right(NULL){}
};

那么这个单向链表的数据结构就是把二叉树节点的数据结构封装一下,如下:

struct LinkedNode {
    TreeNode *tnp;
    LinkedNode *next; 
    LinkedNode(TreeNode* t=NULL, LinkedNode* n=NULL):tnp(t), next(n){}
};

现在,开始考虑递归和非递归的方法.

一、递归的方法

linked list是在递归的过程中不断增长的. 一旦找到,则返回。每次递归到下一个节点时,

  1. 首先将节点添加为linked list的首节点(这就像是链接栈一样,栈顶始终是链接栈)
  2. 然后,分别递归左子树和右子树,
  3. 递归的过程中,一直保持从root到当前节点的一条list
  4. 2个节点就会找出2条list,然后对比发现第一个不一样的节点之前的节点,就是公共祖先节点

(2021/04/09 注: 原先的递归方法有缺陷,已修正)
(2021/06/02 注: 递归中的seach方法有缺陷且不够简明,已修正。应当使用二级指针较为简明且正确。)

全部代码如下:

#include <iostream>
using namespace std;

struct TreeNode {
    int data;
    TreeNode * left;
    TreeNode * right;
    TreeNode(int d) : data(d), left(nullptr), right(nullptr) {}
};

struct LinkedNode {
    TreeNode * p;
    LinkedNode * next; 
    LinkedNode(TreeNode *np) : p(np), next(nullptr) {}
};

template <typename T>
void deleteList(T* root) {
    if (root == nullptr) return;
    do {
        T* tmp = root->next; 
        delete root; 
        root = tmp; 
    } while (root != nullptr);
}

template <typename T>
void printList(T* root) {
    while (root != nullptr) {
        if (root->p != nullptr) {
            cout << root->p->data << " ";
        }
        root = root->next;
    }
    cout << endl;
}

bool searchTargetTreeNode(TreeNode * root, TreeNode * target, LinkedNode** h)
{
    if (root == nullptr || target == nullptr) return false;
    
    LinkedNode * tmp = new LinkedNode(root);
    
    if (*h == nullptr) {
        (*h) = tmp;
    }
    else {
        (*h)->next = tmp;
    }
    
    if (root == target) return true;
    if ( searchTargetTreeNode(root->left, target, &tmp) )  return true; 
    if ( searchTargetTreeNode(root->right, target, &tmp) ) return true;

    delete tmp;
    (*h)->next = nullptr;
    return false;
}

TreeNode * findSharedAncestor(TreeNode * root, TreeNode *a, TreeNode *b) 
{
    if (root == nullptr || a == nullptr || b == nullptr) return nullptr;
    
    LinkedNode * h1 = nullptr;
    bool bFoundA = searchTargetTreeNode(root, a, &h1);
    if (!bFoundA) {
        deleteList(h1);
        return nullptr; 
    }
    else {
        printList(h1);
    }
    
    LinkedNode * h2 = nullptr;
    bool bFoundB = searchTargetTreeNode(root, b, &h2);
    if (!bFoundB) {
        deleteList(h2);
        return nullptr;
    }
    else {
        printList(h2);
    }
    
    TreeNode * result = nullptr;
    
    while (true) {
        if (h1->p == h2->p) {
            result = h1->p; 
        }
        else {
            break;
        }
        
        if (h1->next == nullptr || h2->next == nullptr) break;
        
        h1 = h1->next;
        h2 = h2->next;
    }
    
    deleteList(h1);
    deleteList(h2);
    
    return result;
    
}

int main()
{
    TreeNode n1(1), n2(2), n3(3);
    TreeNode n4(4), n5(5), n6(6);
    TreeNode n7(7), n8(8), n9(9);
    
    n1.left  = &n2;
    n1.right = &n3;
    n2.left  = &n4;
    n2.right = &n5;
    n3.left  = &n6;
    n3.right = &n7;
    n4.left  = &n8;
    n6.right = &n9;
    
    TreeNode * result = findSharedAncestor(&n1, &n8, &n5);
    cout << "shared ancestor is " << result->data << endl;
    return 0;
}

(2021-08-05 注: 今天又写了一遍这个程序。发现原来的写法中对于二级指针的应用很有意思,不过还是略显复杂。下面给出一个简单些的写法,不需要借助自定义的链表结构)

#include <iostream>
#include <vector>
using namespace std;

struct TreeNode {
    int data; 
    TreeNode * left;
    TreeNode * right;
    TreeNode (int d, TreeNode* l=nullptr, TreeNode* r=nullptr) : data(d), left(l), right(r) {}
};

void print_vector(vector<TreeNode*> & vec) 
{
    for (auto v : vec) cout << v->data << " ";
    cout << endl;
}

bool find_a_path(TreeNode * head, int data, vector<TreeNode *>& myvec)
{
    if (head == nullptr) return false;
    
    myvec.push_back(head);
    if (head->data == data) return true;
    
    if (find_a_path(head->left,  data, myvec)) return true; 
    if (find_a_path(head->right, data, myvec)) return true;
    
    myvec.pop_back();
    return false;
}

TreeNode * get_shared_ancestor(TreeNode * head, int d1, int d2)
{
    vector<TreeNode *> v1;
    vector<TreeNode *> v2;
    
    if ( find_a_path(head, d1, v1) ) print_vector(v1);
    else return nullptr;
    if ( find_a_path(head, d2, v2) ) print_vector(v2);
    else return nullptr;
        
    TreeNode * result = nullptr;
    int min_s = min(v1.size(), v2.size());
    
    for (int i=0; i<min_s; ++i) {
        if (v1[i] == v2[i]) result = v1[i];
        else break;
    }
    return result;
}

int main()
{
    TreeNode n1(10), n2(20), n3(30), n4(40), n5(50), n6(60), n7(70), n8(80), n9(90);
    n1.left  = &n2;
    n1.right = &n3;
    n2.left  = &n4;
    n2.right = &n5; 
    n3.left  = &n6;
    n3.right = &n7;
    n4.left  = &n8;
    n7.right = &n9;
    
    TreeNode * shared_ancestor = nullptr;
    
    shared_ancestor = get_shared_ancestor(&n1, 80, 90);
    if ( shared_ancestor == nullptr) cout << "No shared Ancestor!\n";
    else cout << "Shared Ancestor is: " << shared_ancestor->data << endl;

    shared_ancestor = get_shared_ancestor(&n1, 40, 80);
    if ( shared_ancestor == nullptr) cout << "No shared Ancestor!\n";
    else cout << "Shared Ancestor is: " << shared_ancestor->data << endl;
    
    return 0;
}

二、非递归的方法

如果用非递归,则相当于使用一个栈了。栈的话,就要考虑什么时候入栈,什么时候出栈。
出栈的情况:

  1. 如果是叶子节点,并且没有找到目标节点,出栈;
  2. 如果左右两个子树都已经被遍历过了,也没有找到目标节点,出栈。

以上的分析,就意味着需要知道“左右2个子树是否都已经被遍历过了”这件事。
于是,可知,上述递归方法中的链接栈节点的数据结构还不足以支撑我们知道这件事,那么就给原数据结构增加一些新标记:

struct LinkedNode {
    TreeNode *tnp;
    LinkedNode *next;
    bool left_in_stack;  // tnp->left in stack 
    bool right_in_stack; // tnp->right in stack 
    LinkedNode(TreeNode* t=NULL, LinkedNode* n=NULL):tnp(t), next(n), left_in_stack(false), right_in_stack(false){}
};

用一个while(top != NULL) 的循环来判断栈是否已经为空。若是,则说明已经找遍整棵树,没有找到目标节点;若否,则继续退栈或入栈操作。
具体方法是:
每次进入while循环时,因为可能是退栈后进入,所以首先检查当前栈顶节点的左右两个节点是否都已经走过:

  1. 若左右都已经走过,则退栈, continue;
  2. 若左节点走过,而右节点不存在,则退栈,continue;
  3. 若左节点走过,而右节点没走过,则右节点入栈,标记右节点已入栈,continue;
  4. 若左节点存在,且左节点没有走过,则入栈左节点,并标记左节点已经入栈,continue;
  5. 若左节点不存在,则查看是否有右节点:
    5.1 若没有右节点,则退栈,continue;
    5.2 若存在右节点,则右节点入栈,并标记右节点已入栈,contiue;
    注意,以上的方法总结下来就是:
    每次入栈只能入栈一个节点,优先左节点;不能每次同时入栈左节点和右节点,否则一旦找到target节点,没法打印整个path

完整代码如下:

#include <iostream>
using namespace std;

struct TreeNode {
    int data;
    TreeNode *left;
    TreeNode *right;
    TreeNode(int d=0):data(d), left(NULL), right(NULL){}
};

void print_tree(TreeNode *h) 
{
    if (h->left) print_tree(h->left);
    cout << h->data << ", ";
    if (h->right) print_tree(h->right);
}


struct LinkedNode {
    TreeNode *tnp;
    LinkedNode *next;
    bool left_in_stack;  // tnp->left in stack 
    bool right_in_stack; // tnp->right in stack 
    LinkedNode(TreeNode* t=NULL, LinkedNode* n=NULL):tnp(t), next(n), left_in_stack(false), right_in_stack(false){}
};


LinkedNode* _pop_stack(LinkedNode * top)
{
    if (top == NULL) return NULL;
    LinkedNode *tmp = top;
    top = top->next;
    delete tmp;
    
    return top;
}

LinkedNode* _push_stack(LinkedNode *top, TreeNode* tn, bool mark_left)  // mark_left: true, left_in_stack =true; false, right_in_stack = true 
{
    LinkedNode *stack_node = new LinkedNode(tn);
    if (mark_left) {
        top->left_in_stack = true;
    }
    else {
        top->right_in_stack = true;
    }
    stack_node->next = top;
    top = stack_node;
    return top;
}

void _print_linked_list(LinkedNode *head) 
{
    if (head == NULL) cout << "No path found!" << endl;
    LinkedNode *top = head;
    
    while(head != NULL) {
        cout << head->tnp->data << "->";
        head = head->next;
    }
    cout << "NULL" << endl;
}

template <typename T>
T* reverse_link(T* head) 
{
    if (head == NULL || head->next == NULL) return head;
    
    T *p1 = head;
    T *p2 = head->next;
    T* new_head = reverse_link(p2);
    p2->next = p1;
    p1->next = NULL;
    return new_head;
}

/* 用栈来做: 
   每次进入while循环时,因为可能是退栈后进入,所以首先检查当前栈顶节点的左右两个节点是否都已经走过:
   1. 若左右都已经走过,则退栈, continue;
   2. 若左节点走过,而右节点不存在,则退栈,continue;
   3. 若左节点走过,而右节点没走过,则右节点入栈,标记右节点已入栈,continue;
   4. 若左节点存在,且左节点没有走过,则入栈左节点,并标记左节点已经入栈,continue;
   5. 若左节点不存在,则查看是否有右节点:
   5.1 若没有右节点,则退栈,continue; 
   5.2 若存在右节点,则右节点入栈,并标记右节点已入栈,contiue;
   注意,以上的方法总结下来就是:
   每次入栈只能入栈一个节点,优先左节点;不能每次同时入栈左节点和右节点,否则一旦找到target节点,没法打印整个path
*/

LinkedNode* get_path(TreeNode* root, TreeNode* target)
{
    if (root == NULL) return NULL;
    
    LinkedNode *top = new LinkedNode(root);
    if (root == target) return top;
    
    while(top != NULL) {
        TreeNode *tree_node = top->tnp;

        if (top->left_in_stack && top->right_in_stack) {  // both child nodes were once in stack, pop top
            top = _pop_stack(top);
            continue;
        }
        
        if (top->left_in_stack && not top->right_in_stack)  { // push right node to stack
            if(tree_node->right != NULL) {  // Push right node 
                top = _push_stack(top, tree_node->right, false); 
                if (tree_node->right == target) return top;
            }
            else { // if no right node, pop top (now left has been in the stack; so, pop top is to pop left)
                top = _pop_stack(top);
            }
            continue;
        }
        
        if (not top->left_in_stack) { 
            if (tree_node->left != NULL) { // Push left node
                top = _push_stack(top, tree_node->left, true);
                if (tree_node->left == target) return top;
                continue;
            }
            else {  // left is not in stack and left is NULL
                if (tree_node->right == NULL) {  // right is also NULL, then pop top 
                    top = _pop_stack(top);
                    continue;
                }
                else {  // left is NULL, right is not NULL, and right is not in stack, then push right
                    top = _push_stack(top, tree_node->right, false);
                    if (tree_node->right == target) return top;
                    continue;
                }
            }
        }
    }
    
    return NULL; // top == NULL
}


// 注意,如果t1是t2的祖先节点,则t1本身不能算t1和t2的公共父节点
TreeNode* find_last_parent(LinkedNode *h1, LinkedNode *h2, TreeNode *t1, TreeNode *t2)
{
    if (h1==NULL || h2==NULL) return NULL;

    LinkedNode *parent = NULL;
    while (true) {
        if (h1->tnp==h2->tnp && h1->tnp!=t1 && h1->tnp!=t2) {
            parent = h1;
            h1 = h1->next; 
            h2 = h2->next; 
        }
        else {
            break;
        }
    }
    if (parent != NULL) {
        cout << "Last parent is " << parent->tnp->data << endl;
        return parent->tnp;
    }
    else 
        return NULL;
}

int main()
{
    TreeNode *root = new TreeNode(0);
    TreeNode *p1 = new TreeNode(10);
    TreeNode *p2 = new TreeNode(20);
    TreeNode *p3 = new TreeNode(30);
    TreeNode *p4 = new TreeNode(40);
    TreeNode *p5 = new TreeNode(50);
    TreeNode *p6 = new TreeNode(60);
    TreeNode *p7 = new TreeNode(70);
    TreeNode *p8 = new TreeNode(80);
    TreeNode *p9 = new TreeNode(90);
    
    root->left = p1;
    root->right = p2;
    p1->left = p3;
    p1->right = p4;
    p2->left = p5;
    p2->right = p6;
    p3->left = p7;
    p3->right = p8;
    p4->left = p9; 
    
    print_tree(root);
    cout << endl << endl;
    
    LinkedNode *top1 = get_path(root, p3); 
    LinkedNode *top2 = get_path(root, p8);
    LinkedNode *h1 = reverse_link(top1);
    LinkedNode *h2 = reverse_link(top2);
    _print_linked_list(h1);
    _print_linked_list(h2);
    find_last_parent(h1, h2, p3, p8);
    
    return 0;
}

(END)

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值