二叉树根序遍历

二叉树

树,是一种常用到的数据结构,可以用来模拟具有树状结构性质的数据集合.在树的结构中,每个节点包含一个节点的值和所有节点的列表.从图的观点来看,树也可以看作是一个由N个节点和N-1条边组成的有向无环图。

在树中,应用最广泛的是二叉树.正如名字中所描述的一样,二叉树的每个节点最多由两个节点(子树结构),习惯上成为左子树和右子树.

二叉树的数据结构一般使用结构体来进行描述

struct TreeNode {
    ValueType value; // 当前节点的值
    struct TreeNode * left; // 左节点
    struct TreeNode *right; // 右节点
};

这样在初始化一个节点时,就需要使用:

struct TreeNode *node = malloc(sizeof(struct TreeNode *));
node->value = ...;
node->left= ...;
node->right = ...;

更多的时候为了书写方便,会使用关键字typedef进行简化:

typedef struct TreeNode TreeNode;

这样就可以将初始化节点的操作简化为:

TreeNode *node = malloc(sizeof(TreeNode *));
node->value = ...;
node->left= ...;
node->right = ...;

二叉树的遍历

二叉树的遍历是使用最广泛的的操作之一,最常使用到的遍历顺序有四种:

  • 前(根)序遍历:按照先访问根节点,然后左子树,最后右子树的顺序,递归遍历所有节点.
  • 中(根)序遍历:   按照先访问左子树,然后根节点,最后右子树的顺序,递归访问所有节点.
  • 后(根)序遍历:   按照先访问左节点,然后右节点,最后根节点的顺序,递归访问所有节点.
  • 层序遍历:  按照节点的深度,依次从左到右遍历所有的节点(暂时不做讨论).

对于下图中的二叉树,

先序遍历的过程:

  • 树的根节点为F,所以先访问跟节点F,左子树的根节点为B,右子树的根节点为G,所以遍历的结果应该为: F(B子树的遍历序列)(G子树的遍历序列);
  • F节点左子树的根节点为B,B的左子树根节点为A,右节点为D,D的左子树根基点为C,右子树根节点为E,所以F的左子树的遍历顺序为: BADCE
  • F节点的右子树根节点为G,G没有左子树,右子树根节点为G,G没有左子树,右子树的跟节点为I,I的左子树根节点为H。所以F的右子树的遍历序列为:GIH
  • 所以二叉树的先序遍历序列为:FBADCEGIH

中序遍历过程:

  • 根节点F的左子树的根节点为B,右子树的根节点的G,所以遍历的结果序列应该为:(B子树的遍历序列)F(G子树的遍历序列)
  • B左子树的根节点是A,以A为根的左子树的中序遍历结果序列为A;右子树的根节点D,以D为根节点的右子树的中序遍历的结果序列为:CDE.所以以B子树的遍历序列(B为根节点的左子树)的中序遍历结果序列为:ABCDE
  • G节点没有左子树,只有一个右子树I,I只有一个左子树H,所以G子树的中序遍历结果序列为:GHI
  • 所以中序遍历的结果序列为:ABCDEFGHI

后序遍历的过程:

  • 树的根节点为F,左子树的根节点为B,右子树的根节点为G,所以遍历的结果应该是:(B子树的遍历结果)(G子树的遍历结果)F;
  • B的左子树的根节点为A,A没有子节点;B的右子树的根节点为D,D子树的左子树只有一个C节点,右子树只有一个E节点,所以D子树的编列结果序列为:CED,B子树的遍历结果序列为:ACEDB;
  • G只有一个以I为根节点的右子树,I只有一个以H为根节点的左子树,所以I子树的遍历结果序列为:HI,G子树的遍历结果序列为:HIG
  • 所以后续遍历的结果序列为:ACEDBHIGF

中序和后序遍历的顺序大致相同,只是遍历根节点的顺序不一样,由此也可发现其实所谓的先序,中序,后序只是在遍历过程中根节点的访问顺序不同而已,习惯上称这三种方式为根序遍历.

树的节点结构具有相似性,因此在遍历的过程中经常采用循环和递归的方式来实现遍历.

根序遍历

先序遍历是按照根节点,左子树和右子树的顺序访问所有节点。由于二叉树遍历的定义所具有的递归特性,在遍历过程中可常使用递归和循环来进行树的遍历.

递归实现先序遍历实现大概这个样子:

void preorderTraversal(struct TreeNode *root) {
    if (!root) {
    // 递归结束条件
        return;
    }
    // 访问当前根节点
    printf("%d", root->value);
    // 访问左子树    
    preorderTraversal(root->left);
    // 访问右子树
    preorderTraversal(root->right);
}

同理可以使用递归实现中序遍历和后续遍历:

// 中序遍历
void inorderTraversal(struct TreeNode *root) {
    if (!root) {
    // 递归结束条件
        return;
    }

    // 访问左子树    
    preorderTraversal(root->left);
    // 访问当前根节点
    printf("%d", root->value);
    // 访问右子树
    preorderTraversal(root->right);
}

// 后序遍历
void postorderTraversal(struct TreeNode *root) {
    if (!root) {
    // 递归结束条件
        return;
    }

    // 访问左子树    
    preorderTraversal(root->left);
    // 访问右子树
    preorderTraversal(root->right);
    // 访问当前根节点
    printf("%d", root->value);
}

除了递归之外,还可以使用循环的方式来实现二叉树的先序遍历:

void preorderTraversal(TreeNode *root) {
    // 可以使用数组,栈,双向链表或者其他自定义结构来存储尚未遍历右子树的节点
    TreeNode *nodes[10000] = {0};
    int index = -1;
    TreeNode *current = root;
// 只要数组不为空或者当前节点不为空则循环
    while (index != -1 || current) {
        if (current) {
            // 先访问根节点
            printf("%d", current->value);
            nodes[++index] = current;
            // 然后左节点
            current = current->left;
        } else {
            // 最后是右节点
            current = nodes[index--]->right;
        }
    }
}

中序遍历:

// 中序遍历
void inorderTraversal(TreeNode *root) {
    TreeNode *nodes[1000] = {0};
    int top = -1;
    TreeNode *treeNode = root;
    while (treeNode || top != -1) {
        // 获取所有的左子树节点
        while (treeNode) {
            nodes[++top] = treeNode;
            treeNode = treeNode->left;
        }
        // 每次获取栈顶元素
        treeNode = nodes[top--];
        printf("%d", treeNode->value);
        if (treeNode->right) {
            treeNode = treeNode->right;
        } else {
            // 防止遍历根节点时循环
            treeNode = NULL;
        }
    }
}

对于后序遍历会比较麻烦一点:

void postorderTraversal(TreeNode *root) {
    TreeNode *treeNode = root;
    TreeNode *lastVisit = NULL;
    TreeNode *nodes[1000] = {0};
    int top = -1;
    while (treeNode || top != -1) {
        while (treeNode) {
            nodes[++top] = treeNode;
            treeNode = treeNode->left;
        }
        // 获取栈顶元素
        treeNode = nodes[top--];
        if (treeNode->right && treeNode->right != lastVisit) {
            nodes[++top] = treeNode;
            treeNode = treeNode->right;
        } else {
            printf("%d", treeNode->value);
            lastVisit = treeNode;
            treeNode = NULL;
        }
    }
    
}

看起来这样是实现了节点的先序访问,但是却没有对访问的节点序列进行保存,就没有办法继续访问遍历结果或者对遍历结果做进一步的操作.

为了解决这一问题,常用的操作是添加一个数组来保存遍历的结果序列.由于事先不太可能提前准确预判节点的数量,所以对于这个需要保存结果序列的数组,有两种常用的初始化方式:

  • 可以通过预判来初步判定来预估节点的数量级,初始化一个足够大的数组来保存遍历的结果序列:
void subpreorderTraversal (struct TreeNode* root, int *result, int *returnSize) {
     if (!root) {
         return;
     }
     result[*returnSize] = root->value;
     (*returnSize) += 1;
     subpreorderTraversal(root->left, result, returnSize);
     subpreorderTraversal(root->right, result, returnSize);
 }
int* preorderTraversal(struct TreeNode* root, int* returnSize){
    *returnSize = 0;
    int *result = malloc(sizeof(int) * 10000); // 这里初始化了一个可以保存10000个整数的数组
    subpreorderTraversal(root, result, returnSize);
    return result;

}
  • 通过扩容来满足节点存取的需要,既然无法准确知道节点的个数,那就在数组容量不足时动态进行数组扩容增大数组的容量:
// 定义一个数组结构
struct TreeNodeArray {
    int size; // 当前数组的最大容量
    int current; // 下一个将要保存的元素的索引
    int *values; // 数组的指针
};
// 初始化数组结构
struct TreeNodeArray *init() {
    struct TreeNodeArray *arr = malloc(sizeof(struct TreeNodeArray *));
    arr->current = 0;
    arr->size = 4;
    arr->values = malloc(sizeof(int) *  arr->size);
    return arr;
}

// 使用该函数来保存节点
void save(struct TreeNodeArray *array, int value) {
    if (array->current >= array->size) {
        // 当数组容量不足时进行扩容
        array->size *= 2;
         // 注意这里使用的都是字节,所以要使用 size(数据类型) * 数据个数 的形式
        array->values = realloc(array->values, sizeof(int) * array->size);
    }
    array->values[array->current] = value;
    array->current += 1;
}

这样就可以使用save方法进行遍历结果的保存:

void preorderTraversal(TreeNode *root, struct TreeNodeArray *arr) {
    if (!root) {
        return;
    }
    save(arr, root->value);
    preOrder(root->left, arr);
    preOrder(root->right, arr);
}

对比以上两种方式就会发现:

第一种方式简单粗暴的增大存储空间,这样就可以直接直接进行数据的存储节约了存取的时间,而第二种方式需要在存取数据时频繁地进行扩容,通过精细化的扩容算法可以适当地减少内存的使用. 不过一般在对于内存要求不高的情况下,更加倾向于使用暴力开辟内存的方法来进行数据存取.

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值