【数据结构】二叉树非递归前中后序遍历详解

二叉树的遍历是二叉树操作的基础核心,递归遍历实现简单但存在栈溢出风险,在处理深度较大的二叉树时,非递归遍历凭借手动维护栈的方式更具稳定性。本文将详细讲解二叉树前序、中序、后序的非递归遍历实现思路,结合 C 语言代码完整实现,帮助大家理解非递归遍历的核心逻辑。

一、二叉树基础准备

1.1 二叉树节点结构定义

首先定义二叉树的节点结构体,包含数据域和左右孩子指针,同时定义栈结构用于非递归遍历的节点暂存,栈的最大容量通过宏定义MAXSIZE设置。

cpp 复制代码
#include <stdio.h>
#include <stdlib.h>
#define MAXSIZE 100
typedef char ElemType;
// 二叉树节点结构体
typedef struct TreeNode {
    ElemType data;        // 节点数据(字符型)
    struct TreeNode *lchild;  // 左孩子指针
    struct TreeNode *rchild;  // 右孩子指针
} TreeNode;
typedef TreeNode* BiTree;
// 栈结构体(用于非递归遍历手动维护节点)
typedef struct {
    BiTree data[MAXSIZE];
    int top;  // 栈顶指针,初始为-1表示空栈
} Stack;

1.2 二叉树的构建

本文所有遍历均基于前序遍历序列 构建二叉树,序列中用#表示空节点,示例序列为ABDH#K###E##CFI###G#J##

构建逻辑为:递归读取序列字符,非#则创建节点,依次递归构建左子树和右子树,#则置为空节点。

cpp 复制代码
char str[] = "ABDH#K###E##CFI###G#J##"; 
int idx = 0; // 全局索引,用于遍历构建序列
// 前序构建二叉树
void createTree(BiTree *T) 
{
    ElemType ch = str[idx++];
    if (ch == '#') 
    {
        *T = NULL; // 空节点
    } 
    else 
    {
        *T = (BiTree)malloc(sizeof(TreeNode));
        (*T)->data = ch;
        createTree(&(*T)->lchild); // 先构建左子树
        createTree(&(*T)->rchild); // 后构建右子树
    }
}

1.3 栈的基本操作

非递归遍历的核心是手动操作栈,实现栈的初始化、判空、入栈、出栈基础功能,为后续遍历提供支撑:

cpp 复制代码
// 初始化栈
void initStack(Stack *s) {
    s->top = -1;
}
// 判断栈是否为空
int isEmpty(Stack *s) {
    return s->top == -1;
}
// 入栈:成功返回1,栈满返回0
int push(Stack *s, BiTree p) {
    if (s->top >= MAXSIZE - 1) return 0;
    s->data[++(s->top)] = p;
    return 1;
}
// 出栈:成功返回1,栈空返回0,p接收出栈节点
int pop(Stack *s, BiTree *p) {
    if (isEmpty(s)) return 0;
    *p = s->data[(s->top)--];
    return 1;
}

二、非递归前序遍历

2.1 遍历规则

前序遍历的核心规则是:根节点 → 左子树 → 右子树,即先访问当前根节点,再依次遍历左、右子树。

2.2 非递归实现思路

  1. 若二叉树为空,直接返回;
  2. 初始化栈,将根节点压入栈中;
  3. 循环处理栈,直到栈空:
    • 弹出栈顶节点,立即访问该节点(根节点处理);
    • 先将右孩子 压入栈,再将左孩子压入栈(栈是后进先出,保证左孩子先出栈被访问);
  4. 循环结束,完成前序遍历。

2.3 完整代码实现

cpp 复制代码
// 非递归前序遍历
void preOrderNonRecursive(BiTree T) {
    if (T == NULL) return;
    Stack s;
    initStack(&s);
    push(&s, T);  // 根节点入栈
    while (!isEmpty(&s)) {
        BiTree p;
        pop(&s, &p);               // 弹出栈顶节点
        printf("%c ", p->data);    // 访问根节点
        // 先压右,后压左,保证左子树先遍历
        if (p->rchild) {
            push(&s, p->rchild);
        }
        if (p->lchild) {
            push(&s, p->lchild);
        }
    }
}

2.4 遍历结果

基于示例序列构建的二叉树,前序非递归遍历结果为:A B D H K E C F I G J

三、非递归中序遍历

3.1 遍历规则

中序遍历的核心规则是:左子树 → 根节点 → 右子树,即先遍历左子树至最底层,再访问根节点,最后遍历右子树。

3.2 非递归实现思路

中序遍历无法像前序那样直接入栈根节点,需要先遍历左子树,核心思路为:

  1. 初始化栈,定义节点指针p指向根节点;
  2. 循环处理,条件为p不为空 或 栈不为空
    • p不为空,将p压入栈,p指向其左孩子(一直向左走,遍历左子树);
    • p为空,弹出栈顶节点,访问 该节点(左子树遍历完成,处理根节点),然后p指向其右孩子(遍历右子树);
  3. 循环结束,完成中序遍历。

3.3 完整代码实现

cpp 复制代码
// 非递归中序遍历
void inOrderNonRecursive(BiTree T) {
    Stack s;
    initStack(&s);
    BiTree p = T;
    while (p != NULL || !isEmpty(&s)) {
        // 遍历左子树,依次入栈
        while (p != NULL) {
            push(&s, p);
            p = p->lchild;
        }
        // 左子树遍历完成,处理根节点和右子树
        if (!isEmpty(&s)) {
            pop(&s, &p);
            printf("%c ", p->data);  // 访问根节点
            p = p->rchild;           // 遍历右子树
        }
    }
}

3.4 遍历结果

基于示例序列构建的二叉树,中序非递归遍历结果为:H K D B E A I F C G J

四、非递归后序遍历

4.1 遍历规则

后序遍历的核心规则是:左子树 → 右子树 → 根节点 ,是三种遍历中最复杂的,因为根节点需要在左右子树都遍历完成后才能访问,本文采用双栈法实现,逻辑清晰易理解。

4.2 双栈法实现思路

利用两个栈s1(主栈)和s2(辅助栈),通过反转遍历顺序实现后序,核心思路:

  1. 若二叉树为空,直接返回;
  2. 初始化两个栈,将根节点压入s1
  3. 循环处理s1,直到s1为空:
    • 弹出s1栈顶节点,将其压入s2(辅助栈暂存);
    • 先将左孩子 压入s1,再将右孩子 压入s1(使s1弹出顺序为根→右→左s2存储顺序为左→右→根的反序);
  4. 循环处理s2,依次弹出节点并访问,即为左→右→根的后序遍历结果。

4.3 完整代码实现

cpp 复制代码
// 非递归后序遍历(双栈法)
void postOrderNonRecursive(BiTree T) {
    if (T == NULL) return;
    Stack s1, s2;
    initStack(&s1);
    initStack(&s2);
    push(&s1, T);
    BiTree p;
    // 主栈s1处理,节点按根→右→左压入辅助栈s2
    while (!isEmpty(&s1)) {
        pop(&s1, &p);
        push(&s2, p);
        // 先压左,后压右,保证s1弹出右→左
        if (p->lchild) {
            push(&s1, p->lchild);
        }
        if (p->rchild) {
            push(&s1, p->rchild);
        }
    }
    // 弹出s2,得到后序遍历结果
    while (!isEmpty(&s2)) {
        pop(&s2, &p);
        printf("%c ", p->data);
    }
}

4.4 遍历结果

基于示例序列构建的二叉树,后序非递归遍历结果为:K H D E B I F J G C A

五、主函数测试

将二叉树构建和三种非递归遍历整合到主函数,直接运行即可输出遍历结果:

cpp 复制代码
int main(int argc, char const *argv[])
{
    BiTree T;
    createTree(&T); // 构建二叉树
    printf("非递归前序遍历:");
    preOrderNonRecursive(T);
    printf("\n非递归中序遍历:");
    inOrderNonRecursive(T);
    printf("\n非递归后序遍历:");
    postOrderNonRecursive(T);
    printf("\n");
    return 0;
}

完整代码如下

cpp 复制代码
#include <stdio.h>
#include <stdlib.h>

// 栈最大容量,根据二叉树大小调整
#define MAXSIZE 100

// 定义二叉树节点的数据类型(这里用字符型,方便演示)
typedef char ElemType;

// ==================== 1. 二叉树节点结构体定义 ====================
typedef struct TreeNode {
    ElemType data;               // 节点数据域
    struct TreeNode *lchild;     // 左孩子指针
    struct TreeNode *rchild;     // 右孩子指针
} TreeNode;
// 给二叉树指针起别名,简化代码
typedef TreeNode* BiTree;

// ==================== 2. 栈结构体定义(用于非递归遍历) ====================
typedef struct {
    BiTree data[MAXSIZE];  // 栈存储二叉树节点指针
    int top;               // 栈顶指针,-1表示空栈
} Stack;

// ==================== 3. 栈的基础操作函数 ====================
// 初始化栈
void initStack(Stack *s) {
    s->top = -1;
}

// 判断栈是否为空,空返回1,非空返回0
int isEmpty(Stack *s) {
    return s->top == -1;
}

// 入栈操作:将节点p压入栈s
int push(Stack *s, BiTree p) {
    // 栈满判断
    if (s->top >= MAXSIZE - 1) return 0;
    // 栈顶指针+1,存入节点
    s->data[++(s->top)] = p;
    return 1;
}

// 出栈操作:将栈顶节点弹出,存入p
int pop(Stack *s, BiTree *p) {
    // 栈空判断
    if (isEmpty(s)) return 0;
    // 取出栈顶节点,栈顶指针-1
    *p = s->data[(s->top)--];
    return 1;
}

// ==================== 4. 二叉树创建(前序遍历序列创建,#表示空节点) ====================
// 测试用二叉树序列:ABDH#K###E##CFI###G#J##
char str[] = "ABDH#K###E##CFI###G#J##";
int idx = 0;  // 全局索引,遍历序列字符串

// 前序递归创建二叉树
void createTree(BiTree *T) {
    // 读取当前字符
    ElemType ch = str[idx++];
    // #代表空节点
    if (ch == '#') {
        *T = NULL;
    } else {
        // 分配节点内存
        *T = (BiTree)malloc(sizeof(TreeNode));
        // 赋值节点数据
        (*T)->data = ch;
        // 递归创建左子树
        createTree(&(*T)->lchild);
        // 递归创建右子树
        createTree(&(*T)->rchild);
    }
}

// ==================== 5. 非递归前序遍历 ====================
// 规则:根 -> 左 -> 右
void preOrderNonRecursive(BiTree T) {
    // 二叉树为空,直接返回
    if (T == NULL) return;

    Stack s;
    // 初始化栈
    initStack(&s);
    // 根节点先入栈
    push(&s, T);

    // 栈不为空,循环遍历
    while (!isEmpty(&s)) {
        BiTree p;
        // 弹出栈顶节点
        pop(&s, &p);
        // 【访问节点】前序:先访问根
        printf("%c ", p->data);

        // 栈是后进先出,所以先压右孩子,再压左孩子
        // 保证出栈时左孩子先被访问
        if (p->rchild) {
            push(&s, p->rchild);
        }
        if (p->lchild) {
            push(&s, p->lchild);
        }
    }
}

// ==================== 6. 非递归中序遍历 ====================
// 规则:左 -> 根 -> 右
void inOrderNonRecursive(BiTree T) {
    Stack s;
    initStack(&s);
    // 遍历指针p,初始指向根节点
    BiTree p = T;

    // 循环条件:p不为空 或 栈不为空
    while (p != NULL || !isEmpty(&s)) {
        // 1. 一直向左走,把所有左节点入栈,直到左节点为空
        while (p != NULL) {
            push(&s, p);
            p = p->lchild;
        }

        // 2. 左节点为空,弹出栈顶(根节点)并访问
        if (!isEmpty(&s)) {
            pop(&s, &p);
            // 【访问节点】中序:左走完访问根
            printf("%c ", p->data);
            // 3. 访问右子树
            p = p->rchild;
        }
    }
}

// ==================== 7. 非递归后序遍历(双栈法,最易理解) ====================
// 规则:左 -> 右 -> 根
// 双栈法思路:s1存节点,s2存逆序后序序列,最后弹出s2即为后序
void postOrderNonRecursive(BiTree T) {
    if (T == NULL) return;

    Stack s1, s2;
    initStack(&s1);
    initStack(&s2);
    // 根节点入s1
    push(&s1, T);
    BiTree p;

    // 处理s1,将节点按 根->右->左 压入s2
    while (!isEmpty(&s1)) {
        pop(&s1, &p);
        push(&s2, p);

        // 先压左,后压右,保证s1弹出顺序为 根->右->左
        if (p->lchild) {
            push(&s1, p->lchild);
        }
        if (p->rchild) {
            push(&s1, p->rchild);
        }
    }

    // 弹出s2所有节点,就是后序遍历结果
    while (!isEmpty(&s2)) {
        pop(&s2, &p);
        // 【访问节点】后序:左、右都走完访问根
        printf("%c ", p->data);
    }
}

// ==================== 8. 主函数:测试三种遍历 ====================
int main() {
    BiTree T;
    // 1. 创建二叉树
    createTree(&T);

    // 2. 测试非递归前序遍历
    printf("非递归前序遍历结果:");
    preOrderNonRecursive(T);
    printf("\n");

    // 3. 测试非递归中序遍历
    printf("非递归中序遍历结果:");
    inOrderNonRecursive(T);
    printf("\n");

    // 4. 测试非递归后序遍历
    printf("非递归后序遍历结果:");
    postOrderNonRecursive(T);
    printf("\n");

    return 0;
}

运行截图如下

六、核心总结

  1. 前序遍历 :根先出,利用栈后进先出,先压右、后压左,实现根→左→右的访问顺序;
  2. 中序遍历 :先遍历左子树至最底层,左空出根访问,再处理右子树,循环条件需同时判断节点和栈;
  3. 后序遍历(双栈法) :通过两个栈反转顺序,s1 实现根→右→左 ,s2 暂存后弹出即为左→右→根
  4. 非递归遍历的核心是手动模拟递归的栈帧过程,通过栈暂存未处理的节点,控制遍历顺序。

三种非递归遍历的时间复杂度均为O(n) (每个节点入栈、出栈、访问各一次),空间复杂度为O(n) (最坏情况,二叉树退化为链表,栈存储所有节点),在实际开发中,非递归遍历更适合处理深度较大的二叉树避免递归的栈溢出问题。

相关推荐
计算机安禾2 小时前
【数据结构与算法】第24篇:哈夫曼树与哈夫曼编码
c语言·开发语言·数据结构·c++·算法·visual studio
郝学胜-神的一滴2 小时前
[力扣 20] 栈解千愁:有效括号序列的优雅实现与深度解析
java·数据结构·c++·算法·leetcode·职场和发展
Yzzz-F2 小时前
Problem - 2148F - Codeforces[字符串后缀排序]
数据结构·算法
Kethy__2 小时前
计算机中级-数据库系统工程师-数据结构-树与二叉树(1)
数据结构·算法··数据库系统工程师·计算机中级
深邃-3 小时前
C语言内存函数
c语言·开发语言·数据结构·c++·算法
旺仔.2913 小时前
八大排序:(三)快速排序
数据结构·c++·算法
北顾笙9803 小时前
day13-数据结构力扣
数据结构·算法·leetcode
查古穆3 小时前
堆-前 K 个高频元素
数据结构·算法·leetcode
Mem0rin4 小时前
[Java/数据结构]线性表之栈与队列
java·开发语言·数据结构