二叉树寻找从m到n的路径

【问题】在二叉树中有两个节点m和n,若m是n的祖先,则使用( )可以找到从m到n的路径
A:先序遍历 B:中序遍历 C:后序遍历 D:层序遍历

本文主要参考自遇见哥:https://www.zhihu.com/people/xi-wo-wang-yi-15-46/posts

首先明确一点,什么是路径,访问一个结点 x 时,栈中结点恰好是 x 结点的所有祖先,从栈底到栈顶结点再加上 x 结点,这样就构成了从根结点到 x 结点的一条路径。

对题目换一种表达:
我们对二叉树进行遍历,我们总是把探测到的节点放入栈中,而在访问的时候才将它出栈,请问,哪一种遍历可以保证,访问到某节点时,栈中存储的永远是它的全部祖先节点,为什么?

遍历的访问顺序描述了一种左子树,根,右子树三者之间的关系. 这里提到了子树,我们在讨论树的问题的时候,最好的方式就是,永远不要把树上的节点割裂出来孤立的分析,而是把它们放在子树中,考察子树共同的性质。

我们都知道,某个节点想要访问,必先入栈,若想入栈,那么它的每一个祖先节点都必须已经入栈过。所以我们这个问题其实就是分析两个点

  1. 栈中除了祖先节点,是否还混杂了其他的节点。

  2. 祖先节点是不是完整的都保留下来了。

先序遍历:

void preorderTraversal(TreeNode* root) {
    if (root == NULL) return;

    stack<TreeNode*> stack;
    stack.push(root);
    while (!stack.empty()) {
        TreeNode* node = stack.top();
        stack.pop();
        // 访问当前节点
        cout << node->val << " ";
        // 先右后左入栈保证左子树先被访问
        if (node->right) {
            stack.push(node->right);
        }
        if (node->left) {
            stack.push(node->left);
        }
    }
}

在先序遍历中,访问的顺序是根节点-左子树-右子树。当我们访问一个节点时,我们首先将该节点放入栈中,然后立即访问它,之后才去访问它的左子树和右子树。这意味着祖先节点在其子节点被访问前就已经不在栈中了。

中序非递归遍历

// 中序遍历非递归实现
void inorderTraversal(TreeNode* root) {
    stack<TreeNode*> stack;
    TreeNode* current = root;

    while (current != NULL || !stack.empty()) {
        // 尽可能的向左走,将所有左子节点入栈
        while (current != NULL) {
            stack.push(current);
            current = current->left;
        }

        // 当左子节点走到头,开始处理栈顶节点
        current = stack.top();
        stack.pop();
        
        // 访问当前节点
        cout << current->val << " ";

        // 转向右子树
        current = current->right;
    }
}

【遍历思想】:

  1. 栈的使用:利用一个栈来模拟递归调用的行为,首先尽可能地将所有左子节点推入栈中。
  2. 节点处理:当左侧没有更多节点时,从栈中取出节点访问,然后转向该节点的右子节点。
  3. 遍历右子树:对于每个从栈中取出的节点,都尝试访问其右子树。如果右子树存在,重复前面的过程,即先将右子树的所有左子节点入栈。

在中序遍历中,当我们访问一个节点(比如节点n)时,它的左子树已经被完全访问并从栈中移除。如果n在其祖先节点的右子树中,那么在访问n之前,这个祖先节点已经被访问并从栈中移除了。因此,栈中也不会保留完整的祖先路径。只有当n位于所有祖先节点的直接左侧时(即在最左边的路径上),栈中才会包含其全部祖先。

  • 如下左图,结点c是结点n的一个祖先节点,但是结点n位于结点c的右子树,当访问到结点n时,结点c已经出栈了,因此此时栈里面未保留完整的祖先路径;
  • 而右图中,n位于其所有祖先节点的直接左侧,因此访问到n时,栈中的结点恰好是其完整的祖先节点。
  • 图3的普通情况下,中序非递归的栈中,n的祖先节点缺的就更多了,因此中序非递归遍历不能保证找到m到n的完整路径。

图1图2

图3

后序非递归遍历

vector<int> postorderTraversal(TreeNode* root) {
        vector<int> res;
        stack<TreeNode*> stk;
        TreeNode* cur = root, *pre = NULL;
        //主要思想:
        //由于在某颗子树访问完成以后,接着就要回溯到其父节点去
        //因此可以用prev来记录访问历史,在回溯到父节点时,可以由此来判断,上一个访问的节点是否为右子树;
        while(cur != NULL || !stk.empty()){
            while(cur!=NULL) {     //步骤1:沿着根的左孩子依次入栈,直到左孩子为空;
                stk.push(cur);
                cur = cur ->left;
            }
            cur = stk.top();  
            stk.pop();  //步骤2:此时从栈中弹出的元素,左子树一定是访问完了的;
            //现在需要确定的是是否有右子树,或者右子树是否访问过
            //如果没有右子树,或者右子树访问完了,也就是上一个访问的节点是右子节点时
            //说明可以访问当前节点
            if(cur->right == NULL || pre == cur->right) {
                res.push_back(cur->val);
                pre = cur;//更新历史访问记录,这样回溯的时候父节点可以由此判断右子树是否访问完成;
                cur = NULL;  //结点访问完,重置cur指针,以免跳到步骤1;
            }else{
                stk.push(cur);//如果右子树没有被访问,那么将当前节点压栈,访问右子树
                cur = cur -> right;  //将其右子树转去执行步骤2.
            }
        }
        return res;
    }
  • 左子树中的节点n:当访问n时,因为后序遍历的顺序是先左后右再根,n的右侧兄弟(即右子树)尚未开始遍历,所以n的所有祖先节点都还在栈中。
后续非递归.drawio.png
  • 右子树中的节点n:当访问的节点 n n n 在右子树,由于是后序遍历,可以肯定左子树已经全部访问完并且出栈了,而且祖先还在栈中,右子树访问到 n n n,此时 n n n的后代节点也已经全部出栈,所以到 n n n 的时候,栈中还是全部都是祖先节点。

    后续非递归2.png
  • 递归到整个树结构,后序遍历可以保证,访问到某节点时,栈中存储的永远是它的全部祖先节点。

因此后序遍历在处理祖先节点的保留上表现最佳。无论节点n位于左子树还是右子树,遍历过程保证了在访问 n n n之前,其所有祖先节点都未被访问并因此仍存储在栈中。

  • 34
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
假设二叉树的每个节点定义如下: ```c struct TreeNode { int val; struct TreeNode *left; struct TreeNode *right; }; ``` 其中 `val` 表示节点的值,`left` 和 `right` 分别表示左子树和右子树。 以下是求从根节点到给定节点的路径的 C 语言代码: ```c #include <stdio.h> #include <stdlib.h> struct TreeNode { int val; struct TreeNode *left; struct TreeNode *right; }; struct TreeNode* createNode(int val) { struct TreeNode* node = (struct TreeNode*)malloc(sizeof(struct TreeNode)); node->val = val; node->left = NULL; node->right = NULL; return node; } int getPath(struct TreeNode* root, struct TreeNode* target, int* path, int index) { if (root == NULL) { return 0; } path[index] = root->val; if (root == target) { return 1; } if (getPath(root->left, target, path, index + 1) || getPath(root->right, target, path, index + 1)) { return 1; } return 0; } int main() { // 创建二叉树 struct TreeNode* root = createNode(1); root->left = createNode(2); root->right = createNode(3); root->left->left = createNode(4); root->left->right = createNode(5); root->right->left = createNode(6); root->right->right = createNode(7); // 求从根节点到给定节点的路径 int targetVal = 5; struct TreeNode* target = root->left->right; int path[100]; int found = getPath(root, target, path, 0); if (found) { printf("Path from root to target:\n"); for (int i = 0; i < found; ++i) { printf("%d ", path[i]); } printf("\n"); } else { printf("Target node not found in the tree.\n"); } // 释放二叉树的内存 free(root->left->right); free(root->left->left); free(root->right->right); free(root->right->left); free(root->left); free(root->right); free(root); return 0; } ``` 上面的 `getPath` 函数实现了求从根节点到给定节点的路径,它的参数依次为: - `root`:当前子树的根节点; - `target`:要查找的目标节点; - `path`:保存路径的数组; - `index`:当前节点在路径中的下标。 当找到目标节点时,返回 1,否则返回 0。在递归调用左右子树之前,将当前节点的值保存到路径数组中,并且将下标加 1。如果左右子树中有一颗包含目标节点,就返回 1,表示找到了目标节点。如果左右子树都不包含目标节点,就返回 0,表示没有找到目标节点。 在主函数中,我们创建了一个二叉树,并且指定要查找值为 5 的节点。然后调用 `getPath` 函数求解路径,最后打印出路径数组。注意,在程序结束前,需要释放二叉树的内存,避免内存泄漏。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值