树与二叉树学习笔记

一、树的核心概念

  1. 定义:由 n 个节点构成的有限集,n=0 时为空树;非空树有且仅有一个根节点,除根节点外每个节点仅有一个前驱,所有节点可有 0 个或多个后继,且树中无环。
  2. 关键术语:
    • 度:节点含子树的个数为节点度,树的度为所有节点度的最大值。
    • 节点分类:度为 0 的是叶子节点(无子女),度不为 0 的是分支节点(有子女)。
    • 节点关系:子树的根节点是当前节点的孩子节点,当前节点是孩子节点的双亲(父)节点;同双亲的节点为兄弟节点,双亲在同一层的节点为堂兄弟节点。
    • 森林:m 棵互不相交的树组成的集合。
    • 层数:根节点为第一层,逐层向下递增。

二、树的三种存储结构

(一)双亲表示法

  • 存储方式:一维数组存储节点,每个元素为结构体(含 data 存储节点值、parent 存储父节点下标),根节点 parent 设为 - 1。

  • 特点:查找父节点高效,直接通过 parent 下标定位;查找孩子节点需遍历整个数组,效率较低,适用于少找孩子、多找父亲的场景。

  • 代码实现:

    #include <stdio.h>
    #include <string.h>

    // 定义双亲表示法的节点结构体
    #define MAX_TREE_SIZE 100
    typedef struct {
    // 节点数据
    char data;
    // 父节点下标,根节点为-1
    int parent;
    } PTNode;

    // 树的双亲表示结构
    typedef struct {
    // 节点数组
    PTNode nodes[MAX_TREE_SIZE];
    // 节点数和根节点下标
    int nodeNum, root;
    } PTree;

    // 初始化树
    void initPTree(PTree *tree) {
    tree->nodeNum = 0;
    tree->root = -1;
    memset(tree->nodes, 0, sizeof(tree->nodes));
    }

    // 添加节点
    void addNode(PTree *tree, char data, int parentIndex) {
    if (tree->nodeNum >= MAX_TREE_SIZE) {
    printf("树已满,无法添加节点\n");
    return;
    }
    tree->nodes[tree->nodeNum].data = data;
    tree->nodes[tree->nodeNum].parent = parentIndex;
    // 根节点
    if (parentIndex == -1) {
    tree->root = tree->nodeNum;
    }
    tree->nodeNum++;
    }

    // 查找指定节点的父节点
    int findParent(PTree *tree, int nodeIndex) {
    if (nodeIndex < 0 || nodeIndex >= tree->nodeNum) {
    return -2; // 无效节点
    }
    return tree->nodes[nodeIndex].parent;
    }

    // 测试示例
    int main() {
    PTree tree;
    initPTree(&tree);
    // 添加根节点A(父节点-1)
    addNode(&tree, 'A', -1);
    // 添加节点B(父节点0,即A)
    addNode(&tree, 'B', 0);
    // 添加节点C(父节点0,即A)
    addNode(&tree, 'C', 0);

    复制代码
      int parentIdx = findParent(&tree, 1); // 查找B的父节点
      if (parentIdx == -1) {
          printf("节点是根节点\n");
      } else if (parentIdx == -2) {
          printf("节点不存在\n");
      } else {
          printf("节点%c的父节点是%c\n", tree->nodes[1].data, tree->nodes[parentIdx].data);
      }
      return 0;

    }

(二)孩子表示法

  • 存储方式:结合顺序存储与链式存储,数组存储节点值及孩子链表头指针,链表存储孩子节点的下标。

  • 特点:查找孩子节点高效,直接遍历对应链表即可;查找父节点需遍历所有孩子链表,可通过拓展结构体增加 parent 成员优化,适用于多找孩子的场景。

  • 代码实现:

    #include <stdio.h>
    #include <stdlib.h>
    #include <string.h>

    #define MAX_TREE_SIZE 100

    // 孩子链表节点结构
    typedef struct ChildNode {
    // 孩子节点在数组中的下标
    int childIndex;
    // 指向下一个孩子
    struct ChildNode *next;
    } ChildNode;

    // 树节点结构(数组元素)
    typedef struct {
    // 节点数据
    char data;
    // 孩子链表头指针
    ChildNode *firstChild;
    } CTNode;

    // 树的孩子表示结构
    typedef struct {
    // 节点数组
    CTNode nodes[MAX_TREE_SIZE];
    // 节点数和根节点下标
    int nodeNum, root;
    } CTree;

    // 初始化树
    void initCTree(CTree *tree) {
    tree->nodeNum = 0;
    tree->root = -1;
    for (int i = 0; i < MAX_TREE_SIZE; i++) {
    tree->nodes[i].data = '\0';
    tree->nodes[i].firstChild = NULL;
    }
    }

    // 添加孩子节点到指定父节点
    void addChild(CTree *tree, int parentIndex, int childIndex) {
    if (parentIndex < 0 || parentIndex >= tree->nodeNum || childIndex < 0 || childIndex >= tree->nodeNum) {
    printf("父节点或子节点下标无效\n");
    return;
    }
    ChildNode *newNode = (ChildNode *)malloc(sizeof(ChildNode));
    newNode->childIndex = childIndex;
    newNode->next = NULL;

    复制代码
      // 找到父节点孩子链表的末尾,插入新节点
      ChildNode *p = tree->nodes[parentIndex].firstChild;
      if (p == NULL) {
          tree->nodes[parentIndex].firstChild = newNode;
      } else {
          while (p->next != NULL) {
              p = p->next;
          }
          p->next = newNode;
      }

    }

    // 遍历指定节点的所有孩子
    void traverseChildren(CTree *tree, int parentIndex) {
    if (parentIndex < 0 || parentIndex >= tree->nodeNum) {
    printf("节点不存在\n");
    return;
    }
    printf("节点%c的孩子节点:", tree->nodes[parentIndex].data);
    ChildNode *p = tree->nodes[parentIndex].firstChild;
    while (p != NULL) {
    printf("%c ", tree->nodes[p->childIndex].data);
    p = p->next;
    }
    printf("\n");
    }

    // 测试示例
    int main() {
    CTree tree;
    initCTree(&tree);

    复制代码
      // 添加根节点A
      tree->nodes[0].data = 'A';
      tree->root = 0;
      tree->nodeNum++;
      
      // 添加节点B
      tree->nodes[1].data = 'B';
      tree->nodeNum++;
      
      // 添加节点C
      tree->nodes[2].data = 'C';
      tree->nodeNum++;
      
      // 给A添加孩子B和C
      addChild(&tree, 0, 1);
      addChild(&tree, 0, 2);
      
      // 遍历A的孩子
      traverseChildren(&tree, 0);
      return 0;

    }

(三)孩子兄弟表示法(重点)

  • 存储方式:二叉链表结构,左指针指向第一个孩子,右指针指向同层下一个兄弟(核心口诀:左孩子右兄弟)。

  • 优势:可将任意树转换为二叉树,能复用二叉树的算法进行操作,是考试高频考点,查找孩子和兄弟节点均较便捷。

  • 代码实现:

    #include <stdio.h>
    #include <stdlib.h>

    // 孩子兄弟表示法的节点结构
    typedef struct CSNode {
    // 节点数据
    char data;
    // 第一个孩子节点
    struct CSNode *firstChild;
    // 下一个兄弟节点
    struct CSNode *nextSibling;
    } CSNode, *CSTree;

    // 创建节点
    CSNode* createNode(char data) {
    CSNode *node = (CSNode *)malloc(sizeof(CSNode));
    node->data = data;
    node->firstChild = NULL;
    node->nextSibling = NULL;
    return node;
    }

    // 先序遍历(按树的逻辑遍历)
    void preOrderTraverse(CSTree tree) {
    if (tree == NULL) return;
    // 访问当前节点
    printf("%c ", tree->data);
    // 遍历第一个孩子
    preOrderTraverse(tree->firstChild);
    // 遍历兄弟节点
    preOrderTraverse(tree->nextSibling);
    }

    // 测试示例
    int main() {
    // 构建树:A是根,A的孩子是B,B的兄弟是C;B的孩子是D
    CSNode *A = createNode('A');
    CSNode *B = createNode('B');
    CSNode *C = createNode('C');
    CSNode *D = createNode('D');

    复制代码
      A->firstChild = B;
      B->nextSibling = C;
      B->firstChild = D;
      
      printf("先序遍历结果:");
      preOrderTraverse(A); // 输出:A B D C
      printf("\n");
      return 0;

    }

三、二叉树的基础认知

  1. 定义:每个节点最多有两个子节点的树,是树结构的特殊形式。
  2. 特殊类型:
    • 满二叉树:每一层节点数均达到最大值(每个非叶子节点都有两个子节点)。
    • 完全二叉树:叶节点仅出现在最下层和次下层,且最下层节点集中在左侧,缺失节点仅可能在最下层右侧。

四、二叉树的四种遍历方法(核心重点,附 C 语言实现)

(一)先序遍历

  • 遍历顺序:根节点 → 左子树 → 右子树(递归执行,分支节点需重复该逻辑)。

  • 记忆技巧:"先见根",优先访问当前树的根节点,再逐层深入左子树,最后回溯访问右子树。

  • 代码实现:

    #include <stdio.h>
    #include <stdlib.h>

    // 二叉树节点结构
    typedef struct BiTNode {
    char data;
    struct BiTNode *lchild, *rchild;
    } BiTNode, *BiTree;

    // 创建二叉树(按先序序列,空节点用#表示)
    void createBiTree(BiTree *T) {
    char ch;
    // 注意:前面加空格避免读取换行符
    scanf(" %c", &ch);
    if (ch == '#') {
    *T = NULL;
    } else {
    *T = (BiTNode *)malloc(sizeof(BiTNode));
    (*T)->data = ch;
    // 构建左子树
    createBiTree(&(*T)->lchild);
    // 构建右子树
    createBiTree(&(*T)->rchild);
    }
    }

    // 先序遍历
    void preOrder(BiTree T) {
    if (T == NULL) return;
    // 访问根节点
    printf("%c ", T->data);
    // 遍历左子树
    preOrder(T->lchild);
    // 遍历右子树
    preOrder(T->rchild);
    }

    // 测试示例:输入序列 A B # D # # C # #
    int main() {
    BiTree T;
    printf("请输入二叉树先序序列(空节点用#表示):");
    createBiTree(&T);
    printf("先序遍历结果:");
    preOrder(T); // 输出:A B D C
    printf("\n");
    return 0;
    }

(二)中序遍历

  • 遍历顺序:左子树 → 根节点 → 右子树(递归执行)。

  • 记忆技巧:"根在中间",先深入左子树最底层,再回溯访问根节点,最后访问右子树。

  • 核心用途:结合先序遍历可唯一构造二叉树(先序找根,中序分左右子树)。

  • 代码实现(基于上述二叉树节点结构):

    // 中序遍历
    void inOrder(BiTree T) {
    if (T == NULL) return;
    // 遍历左子树
    inOrder(T->lchild);
    // 访问根节点
    printf("%c ", T->data);
    // 遍历右子树
    inOrder(T->rchild);
    }

    // 测试:在main函数中调用
    // printf("中序遍历结果:");
    // inOrder(T); // 输入A B # D # # C # #,输出:B D A C

(三)后序遍历

  • 遍历顺序:左子树 → 右子树 → 根节点(递归执行)。

  • 记忆技巧:"最后见根",类比 "剪葡萄",先摘完左右子树的所有节点,最后访问根节点。

  • 核心用途:后序遍历的最后一个节点为根节点,可辅助构造二叉树。

  • 代码实现(基于上述二叉树节点结构):

    // 后序遍历
    void postOrder(BiTree T) {
    if (T == NULL) return;
    // 遍历左子树
    postOrder(T->lchild);
    // 遍历右子树
    postOrder(T->rchild);
    // 访问根节点
    printf("%c ", T->data);
    }

    // 测试:在main函数中调用
    // printf("后序遍历结果:");
    // postOrder(T); // 输入A B # D # # C # #,输出:D B C A

(四)层序遍历

  • 遍历顺序:从上到下、逐层遍历,同一层内从左到右依次访问节点。

  • 记忆技巧:"按层扫描",类似阅读文章的顺序,简单直观,无需递归(需借助队列实现)。

  • 代码实现:

    // 队列节点结构(用于层序遍历)
    typedef struct QueueNode {
    BiTree data;
    struct QueueNode *next;
    } QueueNode;

    typedef struct {
    QueueNode *front, *rear;
    } LinkQueue;

    // 初始化队列
    void initQueue(LinkQueue *q) {
    q->front = q->rear = (QueueNode *)malloc(sizeof(QueueNode));
    q->front->next = NULL;
    }

    // 入队
    void enQueue(LinkQueue *q, BiTree t) {
    QueueNode *newNode = (QueueNode *)malloc(sizeof(QueueNode));
    newNode->data = t;
    newNode->next = NULL;
    q->rear->next = newNode;
    q->rear = newNode;
    }

    // 出队
    int deQueue(LinkQueue *q, BiTree *t) {
    if (q->front == q->rear) return 0; // 队空
    QueueNode *p = q->front->next;
    *t = p->data;
    q->front->next = p->next;
    if (q->rear == p) q->rear = q->front;
    free(p);
    return 1;
    }

    // 层序遍历
    void levelOrder(BiTree T) {
    LinkQueue q;
    initQueue(&q);
    if (T == NULL) return;
    // 根节点入队
    enQueue(&q, T);
    while (q.front != q.rear) {
    BiTree node;
    // 出队
    deQueue(&q, &node);
    // 访问节点
    printf("%c ", node->data);
    // 左孩子入队
    if (node->lchild != NULL) enQueue(&q, node->lchild);
    // 右孩子入队
    if (node->rchild != NULL) enQueue(&q, node->rchild);
    }
    }

    // 测试:在main函数中调用
    // printf("层序遍历结果:");
    // levelOrder(T); // 输入A B # D # # C # #,输出:A B C D

五、学习技巧以及总结

学习技巧

  • 存储结构:结合实例画图,对比三种存储方式的优缺点与适用场景。
  • 遍历方法:通过动画演示 + 手动模拟,牢记 "根左右""左根右""左右根" 的核心顺序,多做遍历真题巩固。
  • 构造二叉树:掌握 "先序找根、中序分左右""后序找根、先序分左右" 的核心逻辑,勤加练习。

2.总结

  1. 树的三种存储结构各有优劣:双亲表示法易找父节点,孩子表示法易找子节点,孩子兄弟表示法可将普通树转为二叉树处理,是最常用的存储方式。
  2. 二叉树遍历是核心考点:先序(根左右)、中序(左根右)、后序(左右根)基于递归实现,层序遍历需借助队列,不同遍历顺序可相互配合构造二叉树。
相关推荐
奥特曼_ it5 小时前
【数据分析+机器学习】基于机器学习的招聘数据分析可视化预测推荐系统(完整系统源码+数据库+开发笔记+详细部署教程)✅
笔记·数据挖掘·数据分析
A9better5 小时前
嵌入式开发学习日志50——任务调度与状态
stm32·嵌入式硬件·学习
六义义6 小时前
java基础十二
java·数据结构·算法
四维碎片6 小时前
QSettings + INI 笔记
笔记·qt·算法
非凡ghost6 小时前
ESET NupDown Tools 数据库下载工具
学习·软件需求
Tansmjs6 小时前
C++与GPU计算(CUDA)
开发语言·c++·算法
zzcufo6 小时前
多邻国第5阶段17-18学习笔记
笔记·学习
独自破碎E6 小时前
【优先级队列】主持人调度(二)
算法
BlackWolfSky7 小时前
鸿蒙中级课程笔记4—应用程序框架进阶1—Stage模型应用组成结构、UIAbility启动模式、启动应用内UIAbility
笔记·华为·harmonyos
weixin_445476687 小时前
leetCode每日一题——边反转的最小成本
算法·leetcode·职场和发展