采用栈数据结构的二叉树非递归遍历

简介:   【前言】树的遍历,根据访问自身和其子节点之间的顺序关系,分为前序,后序遍历。对于二叉树,每个节点至多有两个子节点(特别的称为左,右子节点),又有中序遍历。由于树自身具有的递归性,这些遍历函数使用递归函数很容易实现,代码也非常简洁。

  【前言】树的遍历,根据访问自身和其子节点之间的顺序关系,分为前序,后序遍历。对于二叉树,每个节点至多有两个子节点(特别的称为左,右子节点),又有中序遍历。由于树自身具有的递归性,这些遍历函数使用递归函数很容易实现,代码也非常简洁。借助于数据结构中的栈,可以把树遍历的递归函数改写为非递归函数。

 

  在这里我思考的问题是,很显然,循环可以改写为递归函数。递归函数是否借助栈这种数据结构改写为循环呢。因为函数调用中,call procedure stack 中存储了流程的 context,调用和返回相当于根据调用栈中的 context 进行跳转。而采用 stack 数据结构时,主要还是一个顺序循环结构,主要通过 continue 实现流程控制。

 

  首先,给出遍历二叉树的序的定义:

 

  (1)前序遍历:当前节点,左子节点,右子节点;

  (2)中序遍历:左子节点,当前节点,右子节点;

  (3)后序遍历:左子节点,右子节点,当前节点。

 

  对二叉查找树 BST 来说,中序遍历的输出,是排序结果。所以这里我以一个 BST 的中序遍历为主要例子说明问题。一个简单的 BST 如下图所示(为了保证美观精确,下图由我临时编写的一个 VC 窗口程序绘制为样本进行加工得到的):

 

  

 

  其中序遍历的输出为:1,2,3,4,5,6,7,8,9;

 

  首先给出中序遍历的递归函数,代码如下:

 

 1 typedef struct tagNODE
 2 {
 3     int nVal;
 4     int bVisited; //是否被访问过
 5     struct tagNODE *pLeft;
 6     struct tagNODE *pRight;
 7 } NODE, *LPNODE;
 8 
 9 //中序遍历二叉树(递归版本)
10 void Travel_Recursive(LPNODE pNode)
11 {
12     if(pNode != NULL)
13     {
14         Travel_Recursive(pNode->pLeft);
15         _tprintf(_T("%ld, "), pNode->nVal);
16         Travel_Recursive(pNode->pRight);
17     }
18 }

 

  很明显,对应于前面给出的定义,只需要调整上述代码中行号为 14,15,16 的顺序,就可以得到相应的遍历序。

 

  现在,引入栈数据结构,它是一个元素为节点指针的数组,将上面的递归函数改写为非递归函数。中序遍历的基本方法是:

 

  (1)将根节点 push 入栈;

  (2)当栈不为空时,重复(3)到(5)的操作:

  (3)偷窥栈顶部节点,如果节点的左子节点不为 NULL,且没有被访问,则将其左子节点 push 入栈,并跳到(3)。

  (4)当被偷窥的节点没有左子树,pop 该节点出栈,并访问它(同时标记该节点为已访问状态)。

  (5)当该节点的右子节点不为空,将其右子节点 push 入栈,并跳到(3)。

 

  根据以上方法,给出非递归函数的中序遍历版本代码如下:

 

 1 typedef struct tagNODE
 2 {
 3     int nVal;
 4     int bVisited; //是否被访问过
 5     struct tagNODE *pLeft;
 6     struct tagNODE *pRight;
 7 } NODE, *LPNODE;
 8 
 9 //辅助数据结构
10 LPNODE g_Stack[256];
11 int g_nTop;
12 
13 //遍历二叉树,借助于stack数据结构的非递归版本
14 void TravelTree()
15 {
16     //while the stack is not empty
17     while(g_nTop >= 0)
18     {
19         //peek the top node in stack;
20         LPNODE pNode = g_Stack[g_nTop];
21 
22         //push left child;
23         if(pNode->pLeft != NULL && !pNode->pLeft->bVisited)
24         {
25             ++g_nTop;
26             g_Stack[g_nTop] = pNode->pLeft;
27             continue;
28         }
29 
30         //pop and visit it;
31         _tprintf(_T("%ld, "), pNode->nVal);
32         pNode->bVisited = 1;
33         --g_nTop; 
34 
35         //push right child;
36         if(pNode->pRight != NULL && !pNode->pRight->bVisited)
37         {
38             ++g_nTop;
39             g_Stack[g_nTop] = pNode->pRight;
40             continue;
41         }       
42     }
43 }

 

  以前面的 BST 为例,在非递归函数中,栈状态的动态变化如下图所示(下图主要由 Excel 和 Photoshop 制作):

  

  在上面的代码的 while 循环体内,可以分为三个小的代码块:

 

  (1)pop 栈顶的节点,并访问此节点 (line 30 ~ 33);

  (2)push 左子节点 (line 22 ~ 28);

  (3)push 右子节点 (line 35 ~ 41);

 

  只要调整 while 循环体中的这三个代码块的顺序,就可以分别实现三种遍历序。例如,前序:(1)(2)(3);后序:(2)(3)(1)。

  从上面的代码中,有两点需要说明:

 

  (1)最后一个代码块中的 continue 可以不需要写,但为了可以调整代码块的顺序,两个 continue 都是需要的。

  (2)因为前序遍历的逻辑的简洁性,不借助于 bVisited 标记,也可以完成遍历,但为了通用,还是需要这个节点标记。

 

  最后,补充上其他并不重要的方法,创建树,释放树,main 函数的代码如下(把已有所有代码拼在一起即构成完整的 Demo 程序):

 

//左右 Child 定义
#define LCHILD        0
#define RCHILD        1

typedef struct tagNODE
{
    int nVal;
    int bVisited; //是否被访问过
    struct tagNODE *pLeft;
    struct tagNODE *pRight;
} NODE, *LPNODE;

LPNODE g_Stack[256];
int g_nTop;

LPNODE InsertNode(LPNODE pParent, int nWhichChild, int val)
{
    LPNODE pNode = (LPNODE)malloc(sizeof(NODE));
    memset(pNode, 0, sizeof(NODE));
    pNode->nVal = val;

    if(pParent != NULL)
    {
        if(nWhichChild == LCHILD)
            pParent->pLeft = pNode;
        else
            pParent->pRight = pNode;
    }
    return pNode;
}

//递归释放二叉树的内存
void FreeTree(LPNODE pRoot)
{
    if(pRoot != NULL)
    {
        FreeTree(pRoot->pLeft);
        FreeTree(pRoot->pRight);
        //_tprintf(_T("freeing Node (%ld) ...\n"), pRoot->nVal);
        free(pRoot);
    }
}

int _tmain(int argc, _TCHAR* argv[])
{
    //索引为 0 的元素不使用。
    LPNODE pNodes[10] = { 0 };

    pNodes[1] = InsertNode(pNodes[0], LCHILD, 7);
    pNodes[2] = InsertNode(pNodes[1], LCHILD, 4);
    pNodes[3] = InsertNode(pNodes[1], RCHILD, 9);
    pNodes[4] = InsertNode(pNodes[2], LCHILD, 2);
    pNodes[5] = InsertNode(pNodes[2], RCHILD, 6);
    pNodes[6] = InsertNode(pNodes[3], LCHILD, 8);
    pNodes[7] = InsertNode(pNodes[4], LCHILD, 1);
    pNodes[8] = InsertNode(pNodes[4], RCHILD, 3);
    pNodes[9] = InsertNode(pNodes[5], LCHILD, 5);

    //push 根节点
    g_nTop = 0;
    g_Stack[g_nTop] = pNodes[1];

    TravelTree();
    _tprintf(_T("\n"));

    Travel_Recursive(pNodes[1]);
    _tprintf(_T("\n"));

    FreeTree(pNodes[1]);
    return 0;
}
View Code

 

  可以看到,释放树(FreeTree)这个函数,就是按照后序遍历的顺序进行释放的。

 

  【补充】和本文相关的我写的其他博客文章:

 

  (1)采用路径模型实现遍历二叉树的方法。2013-5-18;

  (2)[非原创]树和图的遍历。2008-8-10;

 

  【后记】

  献给曾经向我请教“采用非递归方法遍历树”的 小玉(littlehead)学妹。

目录
相关文章
|
14天前
|
存储 C语言 C++
【C++数据结构——栈与队列】顺序栈的基本运算(头歌实践教学平台习题)【合集】
本关任务:编写一个程序实现顺序栈的基本运算。开始你的任务吧,祝你成功!​ 相关知识 初始化栈 销毁栈 判断栈是否为空 进栈 出栈 取栈顶元素 1.初始化栈 概念:初始化栈是为栈的使用做准备,包括分配内存空间(如果是动态分配)和设置栈的初始状态。栈有顺序栈和链式栈两种常见形式。对于顺序栈,通常需要定义一个数组来存储栈元素,并设置一个变量来记录栈顶位置;对于链式栈,需要定义节点结构,包含数据域和指针域,同时初始化栈顶指针。 示例(顺序栈): 以下是一个简单的顺序栈初始化示例,假设用C语言实现,栈中存储
132 77
|
14天前
|
存储 C++ 索引
【C++数据结构——栈与队列】环形队列的基本运算(头歌实践教学平台习题)【合集】
【数据结构——栈与队列】环形队列的基本运算(头歌实践教学平台习题)【合集】初始化队列、销毁队列、判断队列是否为空、进队列、出队列等。本关任务:编写一个程序实现环形队列的基本运算。(6)出队列序列:yzopq2*(5)依次进队列元素:opq2*(6)出队列序列:bcdef。(2)依次进队列元素:abc。(5)依次进队列元素:def。(2)依次进队列元素:xyz。开始你的任务吧,祝你成功!(4)出队一个元素a。(4)出队一个元素x。
36 13
【C++数据结构——栈与队列】环形队列的基本运算(头歌实践教学平台习题)【合集】
|
14天前
|
Java C++
【C++数据结构——树】二叉树的基本运算(头歌实践教学平台习题)【合集】
本关任务:编写一个程序实现二叉树的基本运算。​ 相关知识 创建二叉树 销毁二叉树 查找结点 求二叉树的高度 输出二叉树 //二叉树节点结构体定义 structTreeNode{ intval; TreeNode*left; TreeNode*right; TreeNode(intx):val(x),left(NULL),right(NULL){} }; 创建二叉树 //创建二叉树函数(简单示例,手动构建) TreeNode*create
38 12
|
14天前
|
C++
【C++数据结构——树】二叉树的性质(头歌实践教学平台习题)【合集】
本文档介绍了如何根据二叉树的括号表示串创建二叉树,并计算其结点个数、叶子结点个数、某结点的层次和二叉树的宽度。主要内容包括: 1. **定义二叉树节点结构体**:定义了包含节点值、左子节点指针和右子节点指针的结构体。 2. **实现构建二叉树的函数**:通过解析括号表示串,递归地构建二叉树的各个节点及其子树。 3. **使用示例**:展示了如何调用 `buildTree` 函数构建二叉树并进行简单验证。 4. **计算二叉树属性**: - 计算二叉树节点个数。 - 计算二叉树叶子节点个数。 - 计算某节点的层次。 - 计算二叉树的宽度。 最后,提供了测试说明及通关代
38 10
|
14天前
|
存储 C语言 C++
【C++数据结构——栈与队列】链栈的基本运算(头歌实践教学平台习题)【合集】
本关任务:编写一个程序实现链栈的基本运算。开始你的任务吧,祝你成功!​ 相关知识 初始化栈 销毁栈 判断栈是否为空 进栈 出栈 取栈顶元素 初始化栈 概念:初始化栈是为栈的使用做准备,包括分配内存空间(如果是动态分配)和设置栈的初始状态。栈有顺序栈和链式栈两种常见形式。对于顺序栈,通常需要定义一个数组来存储栈元素,并设置一个变量来记录栈顶位置;对于链式栈,需要定义节点结构,包含数据域和指针域,同时初始化栈顶指针。 示例(顺序栈): 以下是一个简单的顺序栈初始化示例,假设用C语言实现,栈中存储整数,最大
38 9
|
14天前
|
C++
【C++数据结构——栈和队列】括号配对(头歌实践教学平台习题)【合集】
【数据结构——栈和队列】括号配对(头歌实践教学平台习题)【合集】(1)遇到左括号:进栈Push()(2)遇到右括号:若栈顶元素为左括号,则出栈Pop();否则返回false。(3)当遍历表达式结束,且栈为空时,则返回true,否则返回false。本关任务:编写一个程序利用栈判断左、右圆括号是否配对。为了完成本关任务,你需要掌握:栈对括号的处理。(1)遇到左括号:进栈Push()开始你的任务吧,祝你成功!测试输入:(()))
30 7
|
14天前
|
存储 算法 测试技术
【C++数据结构——树】二叉树的遍历算法(头歌教学实验平台习题) 【合集】
本任务旨在实现二叉树的遍历,包括先序、中序、后序和层次遍历。首先介绍了二叉树的基本概念与结构定义,并通过C++代码示例展示了如何定义二叉树节点及构建二叉树。接着详细讲解了四种遍历方法的递归实现逻辑,以及层次遍历中队列的应用。最后提供了测试用例和预期输出,确保代码正确性。通过这些内容,帮助读者理解并掌握二叉树遍历的核心思想与实现技巧。
37 2
|
28天前
|
数据库
数据结构中二叉树,哈希表,顺序表,链表的比较补充
二叉搜索树,哈希表,顺序表,链表的特点的比较
数据结构中二叉树,哈希表,顺序表,链表的比较补充
|
14天前
|
数据采集 存储 算法
【C++数据结构——图】图的遍历(头歌教学实验平台习题) 【合集】
本文介绍了图的遍历算法,包括深度优先遍历(DFS)和广度优先遍历(BFS)。深度优先遍历通过递归方式从起始节点深入探索图,适用于寻找路径、拓扑排序等场景;广度优先遍历则按层次逐层访问节点,适合无权图最短路径和网络爬虫等应用。文中提供了C++代码示例,演示了如何实现这两种遍历方法,并附有测试用例及结果,帮助读者理解和实践图的遍历算法。
28 0
|
2月前
|
C语言
【数据结构】栈和队列(c语言实现)(附源码)
本文介绍了栈和队列两种数据结构。栈是一种只能在一端进行插入和删除操作的线性表,遵循“先进后出”原则;队列则在一端插入、另一端删除,遵循“先进先出”原则。文章详细讲解了栈和队列的结构定义、方法声明及实现,并提供了完整的代码示例。栈和队列在实际应用中非常广泛,如二叉树的层序遍历和快速排序的非递归实现等。
289 9

热门文章

最新文章