[LeetCode] 99. Recover Binary Search Tree

恢复二叉搜索树的两种解法
博客围绕恢复二叉搜索树展开,树中两个元素交换位置,需在不改变结构的情况下恢复。介绍了两种解法,解法1通过中序遍历保存节点和值,排序后赋值,空间复杂度O(n);解法2利用Morris遍历实现中序遍历,空间复杂度O(1)。

Recover Binary Search Tree

Two elements of a binary search tree (BST) are swapped by mistake.
Recover the tree without changing its structure.
Example:

Input: [1,3,null,null,2]
1
/
3
\
2
Output: [3,1,null,null,2]
3
/
1
\
2

解析

直接交换两位置的节点值,重点在找到节点位置。
最简单的就是直接中序遍历整棵树,保存节点和节点值,对节点值进行排序,将排好序的节点值赋给节点。空间复杂度为O(n)。

解法1:空间复杂度O(n)

利用三个指针,分别为pre指向当前节点(即根节点)的前驱节点,first指针指向第一个交换的地方,second指针指向第二个交换的地方,利用中序遍历得到两个指针,并交换节点值。
例子:
假设中序遍历为163452,需要交换的为2和6,首先pre=1,root从1往后移:
当root=3时,pre=6,此时pre大于root,违反中序遍历的顺序,由于first初始为NULL,则令first=pre=6为第一个交换点,second=root(如果后续没有顺序错误这里second就是第二个交换点);
当root=2时,pre=5,违反同上,此时first不为NULL,second=root=2,中序遍历结束;
交换first=6和second=2的节点值。

class Solution {
public:
    TreeNode* pre, *first, *second;
    void recoverTree(TreeNode* root) {
        pre = first = second = NULL;
        inorder(root);
        if(first && second)
            swap(first->val, second->val);
    }
    void inorder(TreeNode* root){
        if(!root) return;
        inorder(root->left);
        if(!pre) pre = root;
        else{
            if(pre->val > root->val){
                if(!first) first = pre;
                second = root;
            }
            pre = root;
        }
        inorder(root->right);
    }
};

解法2:空间复杂度O(1)

利用Morris遍历来实现中序遍历的O(1)空间复杂度,Morris遍历算法具体步骤查看Binary Tree Inorder Traversal
在解法1和Morris遍历的基础上增加一个指针记录上一个访问的节点。即总共有cur, pre, first, second, precur五个指针。

class Solution {
public:
    void recoverTree(TreeNode* root) {
        TreeNode *cur, *pre, *first, *second, *parent;
        first = second = parent = NULL;
        cur = root;
        while(cur){
            if(!cur->left){
                if(parent && parent->val > cur->val){
                    if(!first) first = parent;
                    second = cur;
                }
                parent = cur;
                cur = cur->right;
            }
            else{
                pre = cur->left;
                while(pre->right && pre->right != cur)
                    pre = pre->right;
                if(!pre->right){
                    pre->right = cur;
                    cur = cur->left;
                }
                else{
                    if(parent->val > cur->val){
                        if(!first) first = parent;
                        second = cur;
                    }
                    parent = cur;
                    pre->right = NULL;
                    cur = cur->right;
                }
            }
        }
        if(first && second) swap(first->val, second->val);
    }
};

参考

http://www.cnblogs.com/grandyang/p/4298069.html
Morris遍历,Binary Tree Inorder Traversal

<think>好的,我现在需要解决用户的问题:如何在不使用栈或递归的情况下,以线性时间完成二叉树的欧拉巡游遍历。首先,我得回顾一下欧拉巡游的基本概念。欧拉巡游是一种遍历二叉树的方法,它会访问每个节点三次:第一次进入节点时,第二次从左子树返回时,第三次从右子树返回时。这种遍历方式可以用来生成树的某些特定序列,比如前序、中序和后序遍历的组合。 用户要求不使用栈或递归,并且在线性时间内完成。通常,递归或栈是实现树遍历的常规方法,但这里需要替代方案。已知的线性时间遍历方法中,Morris遍历算法可以在不使用栈或递归的情况下实现中序遍历,这或许可以借鉴。Morris遍历利用线索二叉树的概念,通过修改树的指针来临时建立前驱节点的链接,从而允许回溯而无需额外存储。 那么,是否可以将类似的思路应用到欧拉巡游上?欧拉巡游需要三次访问节点,而Morris遍历通常处理单次或两次访问。需要设计一种方法,在每个节点上插入线索,以指示何时应该转向左子树或右子树,并在访问后恢复树的结构,确保线性时间。 考虑每个节点的结构。在二叉树中,每个节点有左右两个指针。在欧拉巡游中,每个节点被访问三次,所以需要跟踪当前处于哪个访问阶段。例如,第一次访问时处理前序,第二次处理中序,第三次处理后序。如何在不使用栈的情况下记录这些状态? 或许可以为每个节点添加标记,但这会占用额外空间。用户可能希望保持原树结构不变,因此需要利用已有的指针来存储信息。线索二叉树的做法是,将空指针改为指向遍历中的前驱或后继节点。在Morris遍历中,找到当前节点的前驱,并将前驱的右指针指向当前节点,从而允许回溯。 对于三次访问的情况,可能需要更复杂的线索设置。例如,当第一次访问节点时,处理前序部分,然后建立线索以便返回时处理中序。类似地,处理右子树前建立线索。但三次访问可能需要多个线索,或者多次修改指针,这可能增加时间复杂度。 另一个思路是利用父指针。如果树节点包含指向父节点的指针,那么可以在遍历时通过父指针回溯,而不需要栈。但用户的问题中没有提到树是否带有父指针,通常默认的二叉树节点结构可能不包含父指针,所以这可能不符合要求。 还有一种方法是使用Threaded Binary Tree的扩展,允许每个节点有多个线索,指示不同的访问阶段。例如,在第一次访问后,设置线索指向下一个应该访问的节点,第二次访问后调整线索,等等。这可能在线性时间内完成,但实现起来较为复杂。 或者,可以利用每个节点的空指针来存储临时信息。例如,当遍历左子树时,将当前节点的右指针暂时指向父节点,以便在返回时能够找到路径。这种方法需要在遍历过程中动态修改指针,并在完成后恢复原状,类似于Morris遍历的做法。 具体步骤可能如下: 1. 初始化当前节点为根节点。 2. 当当前节点不为空时: a. 如果当前节点的左子节点为空,处理前序访问,转向右子节点。 b. 否则,找到左子树的最右节点(前驱)。 c. 如果前驱的右指针为空,将其指向当前节点,处理前序访问,然后当前节点移至左子节点。 d. 如果前驱的右指针指向当前节点,处理中序访问,恢复前驱的右指针为空,然后处理右子节点。 但这只覆盖了前序和中序的情况,欧拉巡游需要三次访问。可能需要扩展这个算法,使得在处理右子树之前再次访问当前节点,然后处理右子树,并在最后处理后序访问。例如,当第二次返回到当前节点时,处理中序访问,然后处理右子树,最后第三次访问处理后续。 但如何区分这三次访问呢?Morris遍历通常处理两次访问(前驱建立和恢复时),而这里需要三次。可能需要更细致地调整指针,记录访问状态。例如,在第一次访问时处理前序,第二次访问时处理中序,第三次访问时处理后序。为了实现这一点,可能需要多次设置和重置线索,同时确保每个节点被正确访问三次。 另一个挑战是,如何在遍历过程中不遗漏任何访问阶段。例如,当通过线索返回时,需要知道当前是处于第一次、第二次还是第三次访问。可能需要利用节点的某些标志位,但用户可能不允许额外空间,因此需要利用指针中的某些位或者通过指针的值来判断。 或者,可以分阶段处理:首先进行前序遍历,然后中序,最后后序,但这样会导致三次遍历,时间复杂度为O(n) * 3,这不符合线性时间的要求。 综上,可能需要在Morris遍历的基础上进行扩展,允许每个节点被访问三次。具体来说,当第一次访问节点时处理前序部分,并设置线索;当通过线索返回时处理中序部分,并再次设置线索以便处理右子树后的第三次访问。最后,当第三次访问时处理后序部分,并恢复指针。 例如,步骤可能如下: 1. 当前节点为根。 2. 当前节点不为空时: a. 处理前序访问(第一次访问)。 b. 如果左子节点存在: i. 找到左子树的最右节点(前驱)。 ii. 如果前驱的右指针为空,设置其指向当前节点,当前节点移至左子。 iii. 否则(前驱的右指针指向当前节点),恢复前驱的右指针为空,处理中序访问(第二次访问),当前节点移至右子。 c. 否则: i. 处理中序访问(第二次访问)。 ii. 处理后序访问(第三次访问)。 iii. 当前节点移至右子。 3. 当无法继续时,处理后序访问(第三次访问)并回溯。 这可能需要更复杂的逻辑,确保每次访问都被正确处理,并且指针被正确恢复。此外,可能需要处理右子树的线索,类似左子树的方式,以覆盖所有三次访问。 不过,这样的算法实现起来非常复杂,且容易出错。需要验证是否每个节点都被正确访问三次,并且所有指针在遍历结束后恢复原状。此外,时间复杂度是否严格保持O(n)是关键。每个节点的前驱查找操作的总时间应为O(n),因为在Morris遍历中,每个边被访问常数次。 可能存在的困难包括处理第三次访问时的回溯路径,以及确保在右子树处理完成后能够正确返回到父节点并进行后续访问。这可能需要额外的线索或状态标记,但用户要求不使用额外空间,因此必须仅利用树的结构。 最终,可能确实存在这样的算法,结合线索二叉树和状态跟踪,通过修改指针来记录访问阶段,从而实现三次访问的欧拉巡游,同时保持线性时间和常数空间。这可能需要将每个节点的左和右指针在遍历过程中临时用于线索,以指示不同的访问阶段,并在访问后恢复指针。 例如,当第一次访问节点时,处理前序,建立左子树的前驱线索。当第二次返回时,处理中序,建立右子树的前驱线索。当第三次返回时,处理后序,并恢复所有线索。这样,每个节点被处理三次,而指针的修改和恢复在常数时间内完成,总时间保持线性。 总之,解决方案可能类似于扩展的Morris遍历,处理三次访问,通过临时修改指针来跟踪遍历状态,从而在不使用栈或递归的情况下实现欧拉巡游。具体实现需要仔细处理每个阶段的指针操作,确保正确性和效率。</think>为了在不使用栈或递归的情况下实现二叉树的欧拉巡游遍历,可以采用**线索二叉树(Threaded Binary Tree)**的改进方法,结合**Morris遍历**的思想,通过临时修改指针实现线性时间遍历。以下是具体步骤: --- ### **算法步骤** 1. **初始化当前节点为根节点**,并定义三个访问阶段的标记(通过指针状态隐式记录)。 2. **遍历过程**: - **第一次访问(前序)**:当首次到达节点时,记录前序访问,并尝试向左子树建立线索。 - **第二次访问(中序)**:通过线索回溯到节点时,记录中序访问,并转向右子树。 - **第三次访问(后序)**:在右子树遍历完成后,通过父指针或线索回溯到节点,记录后序访问。 3. **指针恢复**:在遍历过程中临时修改的指针需恢复原状,以保持树的结构不变。 --- ### **具体实现** ```python class TreeNode: def __init__(self, val=0, left=None, right=None): self.val = val self.left = left self.right = right def euler_tour(root): result = [] current = root while current: # 第一次访问(前序) result.append(current.val) if current.left: # 寻找左子树的最右节点(前驱) predecessor = current.left while predecessor.right and predecessor.right != current: predecessor = predecessor.right if not predecessor.right: # 建立线索并处理左子树 predecessor.right = current current = current.left else: # 第二次访问(中序) result.append(current.val) # 恢复指针并转向右子树 predecessor.right = None current = current.right else: # 无左子树时,直接转向右子树 current = current.right # 处理后序访问(需额外处理右子树的最右节点) # 此处需扩展逻辑以涵盖第三次访问,但需更复杂的指针操作 return result ``` --- ### **关键点** 1. **线索建立与恢复**:通过左子树的最右节点建立临时指针,实现回溯。 2. **三次访问处理**: - 前序访问在首次到达节点时记录。 - 中序访问在通过线索回溯时记录。 - 后序访问需在右子树遍历完成后记录(需进一步扩展逻辑)。 3. **时间复杂度**:每个节点被访问常数次,总时间复杂度为$O(n)$,空间复杂度为$O(1)$[^1]。 --- ### **局限性** - 上述代码仅实现了前序和中序访问,完整的欧拉巡游需扩展逻辑以处理后序
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值