二叉树遍历算法详解--附源码

前言:

知道了树的遍历的类型以及概念,接下来我们就要设计算法将其用代码优雅的表述出来。(还不清楚概念的可以看另一篇博客:树的遍历基本概念)

开发语言:C++
编辑器:CLion
项目源码:树的遍历

我们以这棵树为例:二叉树遍历算法详解--附源码_第1张图片
在写代码之前首先定义树的结构体,并且初始化创建一棵树:

//定义树节点的结构体
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) {}
     };
//初始化树
TreeNode* generateTree()
{
    int a;
    cin>>a;
    auto *root = new TreeNode();
    if(a == -1)       //输入-1代表空节点,返回空值
        return nullptr;
    else{
        root->val = a;
        root->left = generateTree();  //构建左子树
        root->right = generateTree();   //构建右子树
    }
    return root;
}
对于前序,后序和中序遍历,我们可以使用递归和非递归的方法来解决
  1. 递归法
    1.前序遍历:对于前序遍历我们从根节点开始遍历,遇到一个节点我们就打印出它的值并且递归遍历左右节点。
//前序遍历 --递归版
void pre(TreeNode* root)
{
    if(root == nullptr)  //如果节点不存在则返回
        return;
    else
    {
        cout<<root->val;  //输出当前根节点的值
        pre(root->left);  //递归遍历左子树
        pre(root->right);  //递归遍历右子树
    }
}

          2. 中序遍历:对于中序遍历,我们在遍历完所有的左节点后再遍历根节点,打印根节点信息,然后右节点。

//中序遍历 --递归版
void medium(TreeNode* root)
{
    if(root == nullptr)  //如果为空值就返回
        return;
    else
    {
        medium(root->left);  //递归遍历左子树
        cout<<root->val;    //打印当前节点值
        medium(root->right);   //递归遍历右子树
    }
}

          3. 后序遍历:后序遍历先递归遍历左子树,再递归遍历右子树,然后打印根节点信息。

//后序遍历  --递归版
void last(TreeNode* root)
{
    if(root == nullptr)  //如果为空值就返回
        return;
    else
    {
        last(root->left);   //遍历左子树
        last(root->right);  //遍历右子树
        cout<<root->val;   //打印当前节点值
    }
}
  1. 非递归版,由于递归就是隐式的使用栈,我们使用非递归也就是迭代的方法就要显示的将栈的使用表示出来。
    1. 前序遍历:从根节点开始遍历,不断遍历左节点直至为空,打印其信息并且入栈,当遍历完左边之后弹出栈顶,遍历栈顶元素的右子树。
//前序遍历 --非递归
void pre1(TreeNode* root)
{
    stack<TreeNode*>ans;  //保存需要遍历的节点信息
    while(root != nullptr || !ans.empty())
    {
        while(root != nullptr)
        {
            cout<<root->val; //打印根节点信息
            ans.push(root);
            root = root->left;  //左节点入栈
        }
        if(!ans.empty())
        {
            root = ans.top();  
            ans.pop();     //弹出栈顶
            root = root->right;   //遍历右节点
        }
    }
}

          2. 中序遍历

//中序遍历  --非递归
/*
 * 主要思想:从根节点开始入栈,一直向左遍历,直到左节点为空,此时弹出最左边的节点,同时将当前节点指向最左边节点
 * 的右节点,重新刚刚的操作,这样就能保证 中序。例如这个例子:
 * 1、 0入栈,1入栈,3入栈,此时当前节点左节点为空,栈内元素为【310】;
 * 2、 节点指向3,打印3,弹出3,节点指向3的右节点,因为右节点为空,所以继续弹出元素1;
 * 3、 节点指向1的右节点4,对于4.重复1操作。
 * 4、 重复以上操作直到当前节点为空并且栈也为空,结束循环。
 */
void medium1(TreeNode* root)
{
    stack<TreeNode*>ans;  //用栈来保存所遍历过的节点信息
    while(root!= nullptr || !ans.empty())  //根节点和栈不为空就进入循环
    {
        while (root != nullptr)
        {
            ans.push(root);
            root = root->left;   //如果左节点不为空就一直向左遍历,直到为空
        }
        if(!ans.empty())
        {
            root = ans.top();  //当前节点设置为栈顶
            cout<<root->val;  //打印
            ans.pop();   //弹出栈顶
            root = root->right;   //当前节点指向右节点
         }
    }
}

          3. 后序遍历

//后序遍历非递归版
/*
 * 后序遍历一定要保证根节点在左右节点之后遍历,所以遍历根节点时,我们要判断它的左右节点是否为空,或者左右节点是否被遍历过。同时我们压栈时为了保证
 * 先遍历的是左节点,我们要先将右节点入栈,然后将左节点入栈。
 */
void last1(TreeNode* root)
{
    stack<TreeNode*>ans;  //用栈来保存所遍历过的节点信息
    ans.push(root);
    TreeNode* pre = nullptr;  //保存前一个遍历的节点的值
    while(!ans.empty())
    {
        TreeNode* temp = ans.top();
        if((temp->right == nullptr && temp->left == nullptr) || (pre != nullptr && (pre == temp->left || pre == temp->right)))
        {
            ans.pop();
            cout<<temp->val;
            pre = temp;
        }
        else
        {
            if(temp->right != nullptr)  //先将右节点压栈,再将左节点压栈
                ans.push(temp->right);
            if(temp->left != nullptr)
                ans.push(temp->left);
        }
    }
}

*以上所有的方法由于使用了额外的栈开销(递归方法也隐式的调用了,因为要保存你处理过但是还没有处理完的函数的信息),同时需要遍历每一个元素,所以时间复杂度和空间复杂度都是O(N) 。还有另一种使用常数空间遍历的方法–Morris遍历,由于比较复杂,我会用另一篇文章来详细写。

  1. 广度优先遍历(BFS):广度优先遍历树,需要用到队列(Queue)来存储节点对象,队列的特点就是先进先出。先往队列中插入左节点,再插右节点,这样出队就是先左节点后右节点了。依次弹出就行并打印就行。
//广度优先遍历
void bfs(TreeNode* root)
{
    queue<TreeNode*>ans; //队列保存每一层的遍历信息
    ans.push(root);  //初始状态将根节点入队列
    while (!ans.empty())
    {
        TreeNode* front = ans.front();  //依次遍历每一层的队列
        cout<<front->val;
        ans.pop();
        if(front->left != nullptr)  //依次入栈每一层对应的下一层的队列,先左后右
            ans.push(front->left);
        if(front->right != nullptr)
            ans.push(front->right);
    }
}
  1. 深度优先遍历(DFS):深度优先遍历各个节点,需要使用到栈(Stack)这种数据结构。stack的特点是是先进后出。整个遍历过程如下:首先打印当前节点,然后先往栈中压入右节点,再压左节点,这样出栈就是先左节点后右节点了。
//深度优先遍历
void dfs(TreeNode* root)
{
    stack<TreeNode*>ans;
    ans.push(root);   //根节点入队列
    while (!ans.empty())
    {
        TreeNode* temp = ans.top();
        cout<<temp->val;    //打印根节点信息
        ans.pop();     //弹出根节点
        if(temp->right != nullptr)   //由于栈后进先出的特点,我们需要先保存右节点,再保存左节点
            ans.push(temp->right);
        if(temp->left != nullptr)
            ans.push(temp->left);
    }
}
  1. 代码运行结果:
    二叉树遍历算法详解--附源码_第2张图片
  2. 源代码:
/*
 * 树的遍历操作
 * 2020/10/27
 * CSU-XZY
 * */
#include
using namespace std;
//定义树节点的结构体
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) {}
     };

//初始化树
TreeNode* generateTree()
{
    int a;
    cin>>a;
    auto *root = new TreeNode();
    if(a == -1)       //输入-1代表空节点,返回空值
        return nullptr;
    else{
        root->val = a;
        root->left = generateTree();  //构建左子树
        root->right = generateTree();   //构建右子树
    }
    return root;
}
//前序遍历 --递归版
void pre(TreeNode* root)
{
    if(root == nullptr)  //如果节点不存在则返回
        return;
    else
    {
        cout<<root->val;  //输出当前根节点的值
        pre(root->left);  //递归遍历左子树
        pre(root->right);  //递归遍历右子树
    }
}
//中序遍历 --递归版
void medium(TreeNode* root)
{
    if(root == nullptr)  //如果为空值就返回
        return;
    else
    {
        medium(root->left);  //递归遍历左子树
        cout<<root->val;    //打印当前节点值
        medium(root->right);   //递归遍历右子树
    }
}
//后序遍历  --递归版
void last(TreeNode* root)
{
    if(root == nullptr)  //如果为空值就返回
        return;
    else
    {
        last(root->left);   //遍历左子树
        last(root->right);  //遍历右子树
        cout<<root->val;   //打印当前节点值
    }
}
//前序遍历 --非递归
void pre1(TreeNode* root)
{
    stack<TreeNode*>ans;  //保存需要遍历的节点信息
    while(root != nullptr || !ans.empty())
    {
        while(root != nullptr)
        {
            cout<<root->val; //打印根节点信息
            ans.push(root);
            root = root->left;  //左节点入栈
        }
        if(!ans.empty())
        {
            root = ans.top();
            ans.pop();     //弹出栈顶
            root = root->right;   //遍历右节点
        }
    }
}
//中序遍历  --非递归
/*
 * 主要思想:从根节点开始入栈,一直向左遍历,直到左节点为空,此时弹出最左边的节点,同时将当前节点指向最左边节点
 * 的右节点,重新刚刚的操作,这样就能保证 中序。例如这个例子:
 * 1、 0入栈,1入栈,3入栈,此时当前节点左节点为空,栈内元素为【310】;
 * 2、 节点指向3,打印3,弹出3,节点指向3的右节点,因为右节点为空,所以继续弹出元素1;
 * 3、 节点指向1的右节点4,对于4.重复1操作。
 * 4、 重复以上操作直到当前节点为空并且栈也为空,结束循环。
 */
void medium1(TreeNode* root)
{
    stack<TreeNode*>ans;  //用栈来保存所遍历过的节点信息
    while(root!= nullptr || !ans.empty())  //根节点和栈不为空就进入循环
    {
        while (root != nullptr)
        {
            ans.push(root);
            root = root->left;   //如果左节点不为空就一直向左遍历,直到为空
        }
        if(!ans.empty())
        {
            root = ans.top();  //当前节点设置为栈顶
            cout<<root->val;  //打印
            ans.pop();   //弹出栈顶
            root = root->right;   //当前节点指向右节点
         }
    }
}
//后序遍历非递归版
/*
 * 后序遍历一定要保证根节点在左右节点之后遍历,所以遍历根节点时,我们要判断它的左右节点是否为空,或者左右节点是否被遍历过。同时我们压栈时为了保证
 * 先遍历的是左节点,我们要先将右节点入栈,然后将左节点入栈。
 */
void last1(TreeNode* root)
{
    stack<TreeNode*>ans;  //用栈来保存所遍历过的节点信息
    ans.push(root);
    TreeNode* pre = nullptr;  //保存前一个遍历的节点的值
    while(!ans.empty())
    {
        TreeNode* temp = ans.top();
        if((temp->right == nullptr && temp->left == nullptr) || (pre != nullptr && (pre == temp->left || pre == temp->right)))
        {
            ans.pop();
            cout<<temp->val;
            pre = temp;
        }
        else
        {
            if(temp->right != nullptr)  //先将右节点压栈,再将左节点压栈
                ans.push(temp->right);
            if(temp->left != nullptr)
                ans.push(temp->left);
        }
    }
}
//广度优先遍历
void bfs(TreeNode* root)
{
    queue<TreeNode*>ans; //队列保存每一层的遍历信息
    ans.push(root);  //初始状态将根节点入栈
    while (!ans.empty())
    {
        TreeNode* front = ans.front();  //依次遍历每一层的队列
        cout<<front->val;
        ans.pop();
        if(front->left != nullptr)  //依次入栈每一层对应的下一层的队列,先左后右
            ans.push(front->left);
        if(front->right != nullptr)
            ans.push(front->right);
    }
}
//深度优先遍历
void dfs(TreeNode* root)
{
    stack<TreeNode*>ans;
    ans.push(root);   //根节点入栈
    while (!ans.empty())
    {
        TreeNode* temp = ans.top();
        cout<<temp->val;    //打印根节点信息
        ans.pop();     //弹出根节点
        if(temp->right != nullptr)   //由于栈后进先出的特点,我们需要先保存右节点,再保存左节点
            ans.push(temp->right);
        if(temp->left != nullptr)
            ans.push(temp->left);
    }
}
int main()
{
    TreeNode* root = generateTree(); //初始化树
    cout<<"-----------------递归版本------------------"<<endl;
    cout<<"前序遍历为:    ";
    pre(root);
    cout<<endl<<"中序遍历为:    ";
    medium(root);
    cout<<endl<<"后序遍历为:    ";
    last(root);
    cout<<endl<<"----------------非递归版本------------------"<<endl;
    cout<<"前序遍历为:    ";
    pre1(root);
    cout<<endl<<"中序遍历为:    ";
    medium1(root);
    cout<<endl<<"后序遍历为:    ";
    last1(root);
    cout<<endl<<"----------------BFS和DFS------------------"<<endl;
    cout<<"BFS为:       ";
    bfs(root);
    cout<<endl<<"DFS为:       ";
    dfs(root);
    return 0;
}

你可能感兴趣的:(算法,C++,c++,数据结构,树结构,c语言,树堆)