从零开始写算法——二叉树篇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) 空间的算法,这是对数据结构最底层的操控能力。

相关推荐
Coder_Boy_1 小时前
基于SpringAI的在线考试系统-核心业务流程图
java·数据库·spring boot·软件工程
大志若愚YYZ1 小时前
ROS2学习 C++中的this指针
c++·学习·算法
AI科技星2 小时前
光子的几何起源与量子本质:一个源于时空本底运动的统一模型
服务器·人工智能·线性代数·算法·机器学习
源代码•宸2 小时前
Golang原理剖析(map面试与分析)
开发语言·后端·算法·面试·职场和发展·golang·map
Kingairy2 小时前
Python面试高频题
java·python·面试
CodeByV2 小时前
【算法题】栈
算法
黎雁·泠崖2 小时前
Java数组入门:定义+静态/动态初始化全解析(隐式转换+案例+避坑指南)
java·开发语言·python
sprintzer2 小时前
1.6-1.15力扣数学刷题
算法·leetcode·职场和发展
jiang_bluetooth2 小时前
channel sounding基于探测序列的时延和相位差算法
算法·蓝牙测距·channel sound·gfsk·蓝牙6.0