从零开始写算法——二叉树篇6:二叉树的右视图 + 二叉树展开为链表

二叉树的问题往往千变万化,但归根结底是对遍历顺序指针操作的掌控。

今天我们要探讨两道非常有代表性的题目:

  1. 二叉树的右视图:如何通过巧妙的 DFS 遍历顺序,捕捉特定视角的节点?

  2. 二叉树展开为链表:如何在不使用额外空间的情况下,对树的结构进行"大手术"?

本文将基于 C++ 代码,深度解析这两类问题的解题模版与核心思维。


一、 视角的艺术:二叉树的右视图

1. 题目与思路解析

题目要求我们返回从二叉树右侧能看到的节点值。直观上,这似乎需要层序遍历(BFS)取每一层的最后一个元素。但其实,利用 DFS(深度优先搜索) 可以写出更简洁的代码。

核心策略:

  • 反向中序遍历 :传统的先序遍历是"中 -> 左 -> 右",如果我们改为"中 -> 右 -> 左",那么对于每一层来说,我们最先访问到的一定是这一层最右边的节点。

  • 深度标记:利用 depth 变量记录当前深度。如果 ans 数组的大小等于当前深度,说明我们第一次到达这一层,也就是找到了这一层的最右节点。

2. 代码实现

C++代码实现:

cpp 复制代码
class Solution {
    vector<int> ans;
    void dfs(TreeNode* root, int depth) {
        if (root == nullptr) return;
        
        // 核心逻辑:ans.size() == depth 说明这是第一次到达该深度
        // 因为我们优先走了右边,所以这就是最右边的点
        if (ans.size() == depth) {
            ans.push_back(root->val);
        }
        
        // 优先递归右子树
        dfs(root->right, depth + 1);
        
        // 注意这里是值传递递归,不是引用&,所以是不需要恢复现场的
        dfs(root->left, depth + 1);
    }
public:
    vector<int> rightSideView(TreeNode* root) {
        // 思路: 其实就是看右边的树的高度能否覆盖掉左边的树,换句话说如果答案的长度等于树的高度也就是加入ans
        dfs(root, 0);
        return ans;
    }
};

3. 深度分析:为什么不需要"恢复现场"?

代码中有一句非常有价值的注释:注意这里是值传递递归,不是&,所以是不需要恢复现场的

这是一个非常关键的知识点。

  • 不需要回溯 :我们在调用 dfs(root->right, depth + 1) 时,depth + 1 是作为一个临时值传给下一层的。当前层手中的 depth 变量并没有被改变。因此,当右子树遍历结束,去遍历左子树时,我们依然使用原始的 depth 加 1 传给左边。

  • 需要回溯的情况 :如果你使用的是全局变量 currentDepth 或者引用传递 int& depth,那么在从右子树出来、进入左子树之前,就必须手动 depth--

4. 复杂度分析

  • 时间复杂度:O(N),每个节点访问一次。

  • 空间复杂度:O(H),递归栈深度,最坏情况 O(N)。


二、 结构的重组:二叉树展开为链表

1. 题目与思路解析

这道题要求将二叉树原地展开为一个单链表(利用 right 指针)。顺序符合前序遍历。

难点挑战:你能否使用 O(1) 的额外空间来实现?(即不使用递归栈或额外的数组)。

核心策略:寻找前驱节点(Morris 遍历思想) 我们要把"左子树"塞到"右子树"的前面。

  1. 找到当前节点 cur 的左子树中最右边的那个节点(这是 cur 在中序遍历下的前驱,也是左子树中最晚被访问的节点)。

  2. 把 cur 原本的右子树,接到这个前驱节点的右边。

  3. 把整个左子树移到 cur 的右边。

  4. cur 向右移动,重复上述过程。

2. 代码实现

C++代码实现:

cpp 复制代码
class Solution {
public:
    void flatten(TreeNode* root) {
        // 思路: 要自顶向下处理才可以满足O(1)空间复杂度的要求
        // 因此可以先把234看作一个整体连在15中间,然后继续处理左子树
        TreeNode* cur = root;
        while (cur) {
            // 只有左边有东西才需要搬运
            if(cur->left) {
                // 1. 找左子树的最右节点 (接盘侠)
                TreeNode* p = cur->left;
                while (p->right) p = p->right;
                
                // 2. 把当前节点的右子树,嫁接到左子树最右节点的后面
                p->right = cur->right;
                
                // 3. 把左子树整体挪到右边
                cur->right = cur->left;
                
                // 4. 左边置空,防止双重指向
                cur->left = nullptr;
            }
            // 继续处理链表的下一个节点
            cur = cur->right;
        }
    }
};

3. 深度分析:指针的乾坤大挪移

这段代码最精彩的地方在于**"整体搬运"**。

我们看一个局部:

Plaintext:

复制代码
    1
   / \
  2   5
 / \   \
3   4   6

当 cur 指向 1 时:

  1. 找到左子树的最右节点:4

  2. 把 1 的右子树(5-6)接到 4 的后面。

  3. 把 1 的左子树(2-3-4)挪到 1 的右边。

  4. 结构变更为:1 -> 2 -> 3 -> 4 -> 5 -> 6

这一过程不需要递归,仅通过修改指针就完成了树的"拉直"。

4. 复杂度分析

  • 时间复杂度:O(N)。虽然有嵌套循环,但每个节点只会被作为"前驱节点"寻找一次,整体是线性的。

  • 空间复杂度:O(1)。我们只用了 cur 和 p 两个指针,没有使用递归栈,完美符合进阶要求。


三、 总结

  • RightSideView 教会我们:DFS 的顺序(先右后左)可以帮我们"抢占"每一层的特定位置,配合 depth 值传递,逻辑清晰且无需回溯。

  • Flatten 教会我们:指针的重组可以将树降维成链表。利用前驱节点(Predecessor)的概念,我们可以实现 O(1) 空间的算法,这是对数据结构最底层的操控能力。

相关推荐
重庆小透明20 分钟前
【面试问题】java字节八股部分
java·面试·职场和发展
小王不爱笑13221 分钟前
Java 对象拷贝(浅拷贝 / 深拷贝)
java·开发语言·python
架构师沉默24 分钟前
程序员真的要失业了吗?
java·后端·架构
AI-Ming26 分钟前
注意力机制
算法·ai·ai编程
小王不爱笑13231 分钟前
SpringBoot 自动装配深度解析:从底层原理到自定义 starter 实战(含源码断点调试)
java·spring boot·mybatis
森林里的程序猿猿34 分钟前
Spring Aop底层源码实现(一)
java·后端·spring
ℳ๓₯㎕.空城旧梦40 分钟前
C++中的解释器模式
开发语言·c++·算法
x_xbx1 小时前
LeetCode:2. 两数相加
算法·leetcode·职场和发展
兔子7731 小时前
RNN 终于讲明白了:从“模型为什么需要记忆”到 Elman 1990 全文吃透
算法
兔子7731 小时前
LSTM 终于讲明白了:从“RNN 为什么会忘”到 Hochreiter & Schmidhuber 1997 全文吃透
算法