【二叉树】遍历(递归、迭代、Morris Traversal)

  二叉树的遍历,是程序员面试的最基本问题,对于基础分为三种遍历顺序:前序、中序、后序,这个 “前、中、后” 都是指,也就是对应先根序、中根序、后根序,左右子节点的顺序默认都是先左后右
  LeetCode 中有详细的解释: https://leetcode.com/explore/learn/card/data-structure-tree/134/traverse-a-tree/992/

一、递归形式

  二叉树的遍历,用递归的形式,也就是递归的DFS很好写,而且三种遍历方式几乎没有区别,只是遍历的位置与DFS左节点和DFS右节点的位置关系不一样,因为默认都是先左后友,所以默认的DFS中,都是先递归左节点,再递归右节点。三种遍历方式如下:

vector<int> threeOrdersTraversal(TreeNode* root) {
    vector<int> v;
    traversal(root, v);
    return v;
}

void traversal(TreeNode* root, vector<int>& v){
    if(!root) return;
    // pre-order 先根序				// in-order 中根序				// post-order 后根序
    v.push_back(root->val);			traversal(root->left, v);		traversal(root->left, v);
    traversal(root->left, v);		v.push_back(root->val);			traversal(root->right, v);
    traversal(root->right, v);		traversal(root->right, v);		v.push_back(root->val);
}

  为了看的清楚就写在一起了,对于递归形式来说,前中后的区别就是先递归还是先对根节点进行操作(如上的 v.push_back(root->val);)。递归形式简单、好写、好理解,所以一般不会有让写递归形式的,都是在树的题目中作为基础的遍历方式进行考察。

二、非递归形式(用 stack 模拟递归)

  前序遍历比较简单,直接每次 操作节点(比如push_back)、右节点入栈、左节点入栈(先右后左),就可以了。

vector<int> preorderTraversal(TreeNode* root) {
    vector<int> v;
    if(!root) return v;
    TreeNode* temp = root;
    stack<TreeNode*> s;
    s.push(root);
    while(!s.empty()){
        tmp = s.top();
        s.pop();
        v.push_back(tmp->val); 		// 对节点进行操作
        if(tmp->right) s.push(tmp->right);
        if(tmp->left)  s.push(tmp->left);
    }
    return v;
}

  中序遍历会复杂一点,每个遍历到一个,都先遍历到最左边的,然后再遍历节点自己,再是右节点。

vector<int> inorderTraversal(TreeNode* root) {
    vector<int> v;
    stack<TreeNode*> s;
    while (root || !s.empty()) {
        while (root) {
            s.push(root);
            root = root->left;
        }
        root = s.top();
        s.pop();
        v.push_back(root->val); 	// 对节点进行操作
        root = root->right;
    }
    return v;
}

  后序遍历会更复杂一点,每个节点都先遍历到最左边节点后,还要遍历最右边的,需要用一个 last 来记录遍历过哪个了,否则一个节点有右节点就push,那遍历过的还会再被 push 进stack。

vector<int> postorderTraversal(TreeNode* root) {
    vector<int> nodes;
    stack<TreeNode*> s;
    TreeNode* last = NULL;
    while (root || !s.empty()) {
        while (root) {
            s.push(root);
            root = root->left;
        }
        TreeNode* node = s.top();
        if (node->right && last != node->right)
            root = node->right;
        else {
            nodes.push_back(node->val);
            last = node;
            s.pop();
        }
    }
    return nodes;
}

三、Morris Traversal(非递归,不用栈,O(1)空间)

  Morris Traversal 的目的是,不递归,不是用辅助空间进行二叉树的遍历,这个思想来自于线索二叉树(Threaded BinaryTree)。在二叉树的结点上加上线索的二叉树称为线索二叉树,

  对二叉树以某种遍历方式(如先序、中序、后序或层次等)进行遍历,使其变为线索二叉树的过程称为对二叉树进行线索化,线索化的结果如上图所示。简单理解,就是二叉树遍历顺序变成一个链表,就是二叉树的线索化
  要使用O(1)空间进行遍历,最大的难点在于,遍历到子节点的时候怎样重新返回到父节点(假设节点中没有指向父节点的p指针)。由于不能用栈作为辅助空间,Morris 方法使用 curprev 两个指针来实现。步骤如下(以中序遍历为例):

Initialize current as root
While current is not NULL
  1. If the current does not have left child
    a) Print cur’s data
    b) Go to the right, i.e., cur = cur->right
  2. Else
    a) Make current as the right child of the rightmost node in current’s left subtree
    b) Go to this left child, i.e., cur = cur->left

  用图解的形式看会更容易理解,对于一个有 left 的节点 cur,把 cur 暂时复制到 cur 中序遍历上一个位置的 right 上,也就是 cur 的 left 中最右侧节点。同理于 “寻找二叉树中序遍历的下一个节点” 的思路,https://blog.csdn.net/lx8211667846947/article/details/81639281,不过这里找的是上一个节点

  中序遍历代码如下:

vector<int> inorderTraversal(TreeNode* root) {
    vector<int> v;
    TreeNode *cur = root, *prev = nullptr;
    while (cur != nullptr) {
        if (cur->left == nullptr) {         // 1.
            v.push_back(cur->val);  	// 对节点进行操作 
            cur = cur->right;
        }
        else {
            /* Find the inorder predecessor of cur */
            prev = cur->left;
            while (prev->right != nullptr && prev->right != cur)
                prev = prev->right;

            if (prev->right == nullptr) {   // 2.a)
                prev->right = cur;
                cur = cur->left;
            }
            /* Revert the changes made in the 'if' part to restore  
               the original tree i.e., fix the right child of predecessor */
            else {                          // 2.b)
                prev->right = nullptr;
                v.push_back(cur->val);  // 对节点进行操作 
                cur = cur->right;
            }
        }
    }
    return v;
}

  前序遍历个中序遍历的唯一区别就是,当 cur->left != nullptr 时,找到 prev 后,如果 prev->right == nullptr 那就 v.push_back(cur->val);,也就是这个对节点操作从中序的 else 中变到了 if 中:

vector<int> preorderTraversal(TreeNode* root) {
    vector<int> v;
    TreeNode *cur = root, *prev = nullptr;
    while (cur != nullptr) {
        if (cur->left == nullptr) {
            v.push_back(cur->val);
            cur = cur->right;
        }
        else {
            prev = cur->left;
            while (prev->right != nullptr && prev->right != cur)
                prev = prev->right;

            if (prev->right == nullptr) {
                v.push_back(cur->val);
                prev->right = cur;          // difference!!
                cur = cur->left;
            }
            else {
                prev->right = nullptr;
                cur = cur->right;
            }
        }
    }
    return v;
}

  后序遍历有点复杂,参考 https://www.cnblogs.com/AnnieKim/archive/2013/06/15/morristraversal.html

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值