二叉树由于非线性,是一种稍稍复杂的数据结构。在本科学习数据结构时基本上时一头雾水,对于二叉树的很多知识都没有理解,考完试以后很快就忘掉了。在后面写代码做题时意识到了数据结构的重要性,于是重新温习,将二叉树的一些常见算法敲一遍。

二叉树基础算法

本次实验环境为Clion+WSL+GCC;用于测试的树如图

二叉树

1. 二叉树链式存储结构

首先定义一个二叉树结点的结构体

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
//定义节点
struct TreeNode
{
    int data;
    struct TreeNode *left;
    struct TreeNode *right;
    //结构体构造函数
    TreeNode(int data)
    {
        this->data=data;
        this->left=this->right=nullptr;
    }
};

2. 二叉树递归遍历(先序、中序、后序)

二叉树的递归遍历是比较容易实现的,主要是要理解层层递归的过程,递归的方法在之后解决问题会经常用到。

  • 先序遍历
1
2
3
4
5
6
7
8
9
void preOrder(TreeNode *root)
{
    if(root ==nullptr)
        return;
    cout<<root->data<<"  ";
    //对左子树和右子树进行递归
    preOrder(root->left);
    preOrder(root->right);
}
  • 中序遍历
1
2
3
4
5
6
7
8
void midOrder(TreeNode *root)
{
    if(root == nullptr)
        return;
    midOrder(root->left);
    cout<<root->data<<"  ";
    midOrder(root->right);
}

-后序遍历

1
2
3
4
5
6
7
8
void postOrder(TreeNode* root)
{
    if(root == nullptr)
        return;
    postOrder(root->left);
    postOrder((root->right));
    cout<<root->data<<"  ";
}

递归的代码都比较简洁,主要是弄懂递归的方法。主函数中构造了一个简单的树来进行测试。完整的代码如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
#include <iostream>

using std::endl;
using std::cout;

//定义节点
struct TreeNode
{
    int data;
    struct TreeNode *left;
    struct TreeNode *right;
    //结构体构造函数
    TreeNode(int data)
    {
        this->data=data;
        this->left=this->right=nullptr;
    }
};

void preOrder(TreeNode *root)
{
    if(root ==nullptr)
        return;
    cout<<root->data<<"  ";
    //对左子树和右子树进行递归
    preOrder(root->left);
    preOrder(root->right);
}

void midOrder(TreeNode *root)
{
    if(root == nullptr)
        return;
    midOrder(root->left);
    cout<<root->data<<"  ";
    midOrder(root->right);
}

void postOrder(TreeNode* root)
{
    if(root == nullptr)
        return;
    postOrder(root->left);
    postOrder((root->right));
    cout<<root->data<<"  ";
}

int main() {
    //测试数据
    struct TreeNode *root = new TreeNode(1);
    root->left = new TreeNode(2);
    root->right = new TreeNode(3);
    root->left->left = new TreeNode(4);
    root->left->right = new TreeNode(5);
    preOrder(root);
    cout<<endl;
    midOrder(root);
    cout<<endl;
    postOrder(root);
    return 0;
}

二叉树遍历非递归实现

虽然递归版本代码简洁容易实现,但是当树的深度比较深时会比较容易发生栈溢出。因此可以自己定义栈或者队列来实现二叉树的遍历。

  • 先序遍历非递归实现 主要的算法流程
  • 建立一个空栈,将根节点压栈
  • 进行以下循环直至栈空
    1. 弹出栈顶元素
    1. 将弹出元素的右子树入栈
    1. 弹出元素左子树入栈
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
void preOrderWithStack(TreeNode *root)
{
    std::stack<TreeNode* > s;
    if(root == nullptr)
        return;
    s.push(root);
    while(!s.empty())
    {
        TreeNode *current=s.top();
        s.pop();
        cout<<current->data<<"  ";
        //root节点右孩子先入栈
        if(current->right)
            s.push(current->right);
        if(current->left)
            s.push(current->left);
    }
}
  • 中序遍历是二叉树中使用比较多的遍历方法。下面是非递归形式的中序遍历方法。
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
void midOrderWithStack(TreeNode *root)
{
    std::stack<TreeNode *> s;
    TreeNode *current=root;
    while(current != nullptr || s.empty() == false)
    {
        while(current != nullptr)
        {   //左子树节点入栈
            s.push(current);
            current = current->left;
        }
        current = s.top();
        s.pop();
        cout<<current->data<<"  ";
        current=current->right;
    }
}

可以看到相对于递归形式是稍微复杂一些。下面来分析一下整个过程。

从整体上看,首先是定义了一个栈,然后有一个大的 while 循环,在这个大循环里还套有一个小循环。整棵树的遍历在这个大循环中完成。在第一次进行小循环时,树的所有左侧结点都会入栈,在这个例 子中,也就是 1 2 4,由于 4 节点没有左右孩子,因此打印该节点的值,依次向上去查找。

  • 后序遍历 后续遍历相对来说会麻烦一点。需要用到两个栈来实现。完整的算法流程如下:
  1. 将root节点压入第一个栈
  2. 进行循环直到第一个栈空
  • 从第一个栈中弹出栈顶节点,压入第二个栈。
  • 将上一步弹出的那个节点的左孩子和右孩子压入第一个栈
  1. 从第二个栈中打印节点

整体算法不算复杂。第一个栈中的存储树节点的顺序称为树的逆后续遍历,通过栈的特性将逆后续遍历在逆一次,就是后序遍历了。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
void postOrderWithStack(TreeNode *root)
{
    if(root == nullptr)
        return;
    std::stack<TreeNode* > s1,s2;
    s1.push(root);
    TreeNode *current=root;
    while(!s1.empty())
    {
        current=s1.top();
        s1.pop();
        s2.push(current);
        //将原来的栈顶节点的左右孩子入栈,要先判断是否为空,否则会访问越界,引发段错误。
        if(current->left!= nullptr)
            s1.push(current->left);
        if(current->right!= nullptr)
            s1.push(current->right);
    }
    while(!s2.empty())
    {
        current=s2.top();
        s2.pop();
        cout<<current->data<<"  ";
    }
}

总结

二叉树的遍历是比较基础的算法,在后面做算法题和解决实际问题时会经常用到。所以需要熟练掌握递归和非递归地遍历二叉树,之后才能学习更加复杂地数据结构。