数据结构笔记:二叉树的遍历与技巧

引言

本篇是最近有遇到过的一个题目,关于二叉树的遍历,正好有一些经验与技巧,所以开一篇帖子记录一下。

二叉树遍历介绍

遍历是数据结构中常见的操作,主要是将所有元素都访问一遍。对于线性结构来说,遍历分为两种:正序遍历和逆序遍历。而对于树形结构来说,根据节点访问顺序不同,二叉树的遍历分为如下4种:

  • 前序遍历(Preorder Traversal)
  • 中序遍历(Inorder Traversal)
  • 后序遍历(Postorder Traversal)
  • 层序遍历(Levelorder Traversal)

二叉树的数据结构为:

struct Node
{
    Node* left;
    Node* right;
    int data;
};

二叉树前序遍历

一般先序遍历的顺序为,根、左子节点、右子节点→根(左子节点)、左叶子节点、右叶子结点→ 根(右子节点)、左叶子结点、右叶子结点(PS:这里顺序是三层,下面的都是,但图是4层)。即先根再左最后右,简称根左右,具体如下图所示:

在这里插入图片描述

那么我们可以写出它的递归形式代码:

void preorderTraversal(Node* p)
{
    if (!p) return;
    cout << p->data << " ";
    preorderTraversal(p->left);
    preorderTraversal(p->right);
}

二叉树中序遍历

中序遍历的顺序为左叶子结点、根(左子节点)、右叶子结点 → 左子节点、根、左叶子结点 → 左叶子节点、根(右子节点)、右叶子结点,即先左再根最后右,简称左根右,示意图为:
在这里插入图片描述
同样写出递归形式代码:

void inorderTraversal(Node* p)
{
    if (!p) return;
    inorderTraversal(p->left);
    cout << p->data << " ";
    inorderTraversal(p->right);
}

二叉树后序遍历

中序遍历的顺序为左叶子结点、右叶子结点、根(左子节点) → 左叶子节点、右叶子结点、根(右子节点) → 左子节点、左子结点、根,即先左再右最后根,简称左右根,示意图为:
在这里插入图片描述
那么它的递归形式代码为:

void postorderTraversal(Node* p)
{
    if (!p) return;
    postorderTraversal(p->left);
    postorderTraversal(p->right);
    cout << p->data << " ";
}

二叉树层序遍历

还有一种层序遍历,这种就字面意思,从上到下,从左至右,直接给出示意图为:
在这里插入图片描述

那么代码为:

void levelOrderTraversal(Node* root)
{
    queue<Node*> q;
    q.push(root);
    while (!q.empty())
    {
        Node* p = q.front(); q.pop();
        cout << p->data << " ";
        if (p->left)  q.push(p->left);
        if (p->right) q.push(p->right);
    }
}

该函数使用队列来跟踪需要访问的节点,它首先将根节点推送到队列中,然后,它进入一个循环,该循环一直持续到队列为空。在循环的每次迭代中,它都会将前面的节点p从队列中移出,打印其数据,并将其左右子节点推到队列中,这样就达到了层级的效果。

二叉树遍历全代码

这里因为上面定义的结构体没有分成两部分,仅仅只定义了单节点,没有考虑多关系问题,不过反而代码思路会更加清晰,除了定义测试用例会复杂些,这里我们建立二元树结构:

Node* root = new Node{ 
    new Node{ new Node{ nullptr, nullptr, 4 }, new Node{ nullptr, nullptr, 5 }, 2 },
    new Node{ new Node{ nullptr, nullptr, 6 }, new Node{ nullptr, nullptr, 7 }, 3 },
    1
};

我最近正好在学cpp,nullptr在c语言里就是NULL,空指针。将其展开为如下形式:

        1
       / \
      /   \
     /     \
    2       3
   / \     / \
  4   5   6   7

该种形式当然可以更改结构体构造方式,比如二叉树的四种遍历方式中提到的:

  struct treenode {
      int val;
     treenode *left;
      treenode *right;
     treenode() : val(0), left(nullptr), right(nullptr) {}
      treenode(int x) : val(x), left(nullptr), right(nullptr) {}
      treenode(int x, treenode *left, treenode *right) : val(x), left(left), right(right) {}
 };

不过这里还是以自定义二元树结构来产生初值,那么将上述代码汇总为:

#include <iostream>
#include <queue>
#include <stack>

using namespace std;

struct Node
{
    Node* left;
    Node* right;
    int data;
};

// preorder traversal
void preorderTraversal(Node* p)
{
    if (!p) return;
    cout << p->data << " ";		// 先输出根
    preorderTraversal(p->left);	// 次输出左子树
    preorderTraversal(p->right);// 最后右子树
}

// inorder traversal
void inorderTraversal(Node* p)
{
    if (!p) return;
    inorderTraversal(p->left);
    cout << p->data << " ";		
    inorderTraversal(p->right);
}

// postorder traversal
void postorderTraversal(Node* p)
{
    if (!p) return;
    postorderTraversal(p->left);
    postorderTraversal(p->right);
    cout << p->data << " ";
}

// level-order traversal
void levelOrderTraversal(Node* root)
{
    queue<Node*> q;
    q.push(root);
    while (!q.empty())
    {
        Node* p = q.front(); q.pop();
        cout << p->data << " ";
        if (p->left)  q.push(p->left);
        if (p->right) q.push(p->right);
    }
}


int main()
{
    // create binary tree
    Node* root = new Node{ 
        new Node{ new Node{ nullptr, nullptr, 4 }, new Node{ nullptr, nullptr, 5 }, 2 },
        new Node{ new Node{ nullptr, nullptr, 6 }, new Node{ nullptr, nullptr, 7 }, 3 },
        1
    };

    // perform traversals
    cout << "Preorder traversal: ";
    preorderTraversal(root);
    cout << endl;

    cout << "Inorder traversal: ";
    inorderTraversal(root);
    cout << endl;

    cout << "Postorder traversal: ";
    postorderTraversal(root);
    cout << endl;

    cout << "Level-order traversal: ";
    levelOrderTraversal(root);
    cout << endl;

    return 0;
}

进行编译后,打印输出:

Preorder traversal: 1 2 4 5 3 6 7 
Inorder traversal: 4 2 5 1 6 3 7 
Postorder traversal: 4 5 2 6 7 3 1 
Level-order traversal: 1 2 3 4 5 6 7 

手算遍历的一些技巧

一般如果从选择题的角度,需要给出前中,或者中后,来确定二叉树形态,再推中序遍历,即:

前序、中序——> 后序
后序、中序——> 前序

如果给了前后序的话,那么问题就变为了求多少种二叉树结构,比如说2011年的统考题:

【2011统考真题】一棵二叉树的前序遍历序列和后序遍历序列分别为1,2,3,4和4,3,2,1,该二叉树的中序遍历序列不会是( ).

A . A. A.:1、2、3、4

B . B. B.:2、3、4、1

C . C. C.:3、2、4、1

D . D. D.:4、3、2、1

该题就是因为前序NLR与后序LRN序列刚好相反,所以无法确定一颗完整的二叉树,但可以确定二叉树中结点的祖先关系。根据这就能大致推断出中序结点顺序,比如说这里的仅考虑以1为孩子的结点2为根结点的子树,它也只能有左孩子。

所以这里仅考虑在有中序的前提下,推另一种的情况下,整个过程可以表示成如下图:

在这里插入图片描述

这是比较官方的做法,根节点给的也很明显,能够快速定位并进一步分治,另一种就是依据超级简单-投影法遍历树,引用其中的一种遍历方式如下:

后序遍历就像在右边大风的情况下,将二叉树树枝刮向左方,且顺序为左子树、右子树、根,太阳直射,将所有的结点投影到地上。图中的二叉树,其后序序列投影如图所示。后序遍历序列为:DEBGFCA。

在这里插入图片描述

但该种方式其实只考虑了一维向量,或者说把横线看成是一根坐标轴,仅仅考虑了x的方向变化,而下面这种方法,是通过横轴与竖轴一起确立的唯一二叉树,这里先略过规则,直接看我画的如下图:
在这里插入图片描述
(PS:画了坐标轴,反而比不画的时候丑了很多,emmm,得学学怎么作图)

该方案,源自B站的无脑秒解!已知先/后序遍历与中序遍历,求后/先序遍历。我们发现将中序遍历的输出放y轴上,前序和后序放x轴上,然后点对点将它连起来后,结果与正常做法没有什么两样,这里还可以用我之前在数据结构设计题大题总结(非代码) 该篇中引用的一张百度百科图同样适用:

当然,我算的是另一个,这个20个序列太多了,我在iPad上画的稍微简单点,能一页表达完整的题:
在这里插入图片描述

关于这种求解的证明也很简单,因为先序是根左右,中序是左根右,所以:

在这里插入图片描述
而已知后序(左右根),颠倒后变为(根右左),则同样是上图。

二叉树遍历coding

这里其实也是两题:

从前序与中序遍历序列构造二叉树

从中序与后序遍历序列构造二叉树

这里以前序和中序题为例,题目为:

在这里插入图片描述
输入: preorder = [3,9,20,15,7], inorder = [9,3,15,20,7]
输出: [3,9,20,null,null,15,7]

输入: preorder = [3,9,20,15,7], inorder = [9,3,15,20,7]
输出: [3,9,20,null,null,15,7]

这里找了一个动态图演示:
在这里插入图片描述

解题思路就是我前面所提到的加上一点小细节:

在中序遍历中对根节点进行定位时,一种简单的方法是直接扫描整个中序遍历的结果并找出根节点,但这样做的时间复杂度较高。我们可以考虑使用哈希表来帮助我们快速地定位根节点。对于哈希映射中的每个键值对,键表示一个元素(节点的值),值表示其在中序遍历中的出现位置。在构造二叉树的过程之前,我们可以对中序遍历的列表进行一遍扫描,就可以构造出这个哈希映射。在此后构造二叉树的过程中,我们就只需要 O(1)O(1) 的时间对根节点进行定位了。

所以代码为:

class Solution {
private:
    unordered_map<int, int> index;

public:
    TreeNode* myBuildTree(const vector<int>& preorder, const vector<int>& inorder, int preorder_left, int preorder_right, int inorder_left, int inorder_right) {
        if (preorder_left > preorder_right) {
            return nullptr;
        }
        
        // 前序遍历中的第一个节点就是根节点
        int preorder_root = preorder_left;
        // 在中序遍历中定位根节点
        int inorder_root = index[preorder[preorder_root]];
        
        // 先把根节点建立出来
        TreeNode* root = new TreeNode(preorder[preorder_root]);
        // 得到左子树中的节点数目
        int size_left_subtree = inorder_root - inorder_left;
        // 递归地构造左子树,并连接到根节点
        // 先序遍历中「从 左边界+1 开始的 size_left_subtree」个元素就对应了中序遍历中「从 左边界 开始到 根节点定位-1」的元素
        root->left = myBuildTree(preorder, inorder, preorder_left + 1, preorder_left + size_left_subtree, inorder_left, inorder_root - 1);
        // 递归地构造右子树,并连接到根节点
        // 先序遍历中「从 左边界+1+左子树节点数目 开始到 右边界」的元素就对应了中序遍历中「从 根节点定位+1 到 右边界」的元素
        root->right = myBuildTree(preorder, inorder, preorder_left + size_left_subtree + 1, preorder_right, inorder_root + 1, inorder_right);
        return root;
    }

    TreeNode* buildTree(vector<int>& preorder, vector<int>& inorder) {
        int n = preorder.size();
        // 构造哈希映射,帮助我们快速定位根节点
        for (int i = 0; i < n; ++i) {
            index[inorder[i]] = i;
        }
        return myBuildTree(preorder, inorder, 0, n - 1, 0, n - 1);
    }
};

后序和中序定位也一样,后序的尾结点为根节点,另外,同样可以构造哈希映射,帮助我们快速定位根节点:

class Solution {
    int post_idx;
    unordered_map<int, int> idx_map;
public:
    TreeNode* helper(int in_left, int in_right, vector<int>& inorder, vector<int>& postorder){
        // 如果这里没有节点构造二叉树了,就结束
        if (in_left > in_right) {
            return nullptr;
        }

        // 选择 post_idx 位置的元素作为当前子树根节点
        int root_val = postorder[post_idx];
        TreeNode* root = new TreeNode(root_val);

        // 根据 root 所在位置分成左右两棵子树
        int index = idx_map[root_val];

        // 下标减一
        post_idx--;
        // 构造右子树
        root->right = helper(index + 1, in_right, inorder, postorder);
        // 构造左子树
        root->left = helper(in_left, index - 1, inorder, postorder);
        return root;
    }
    TreeNode* buildTree(vector<int>& inorder, vector<int>& postorder) {
        // 从后序遍历的最后一个元素开始
        post_idx = (int)postorder.size() - 1;

        // 建立(元素,下标)键值对的哈希表
        int idx = 0;
        for (auto& val : inorder) {
            idx_map[val] = idx++;
        }
        return helper(0, (int)inorder.size() - 1, inorder, postorder);
    }
};
  • 3
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

submarineas

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值