《嵌入式数据结构笔记(六):二叉树》
1. 树数据结构的基本定义和属性
树是一种重要的非线性数据结构,用于表示层次关系。
- 基本定义:树是由 n(n ≥ 0)个结点组成的有限集合。当 n = 0 时,称为空树;当 n > 0 时,树必须满足两个条件:
- 有且仅有一个特定的根结点(root node)。
- 当 n > 1 时,其余结点可划分为 m(m > 0)个互不相交的子集 T1, T2, ..., Tm,每个子集本身又是一棵树,称为子树(subtree)。
- 结点属性:
- 结点的度(degree):指一个结点拥有的子树个数。例如,一个结点有 3 个子树,则其度为 3。
- 叶结点(leaf node):度为 0 的结点,即没有子树的结点。
- 分支结点(branch node):度不为 0 的结点,即至少有一个子树的结点。
- 树的整体属性:
- 树的度数:指树中所有结点的度的最大值。例如,如果所有结点度的最大值为 3,则树的度数为 3。
- 树的深度或高度:从根结点开始计算,根所在层为第 1 层,其子结点为第 2 层,依此类推。深度反映了树的层级结构。
- 树的性质强调:树的结构强调“互不相交”的子集,确保每个结点只属于一个父结点,避免循环或重复。
2. 树的存储结构
- 顺序结构:将树结点存储在连续的内存位置(如数组),通过索引表示父子关系。优点是访问快速,但插入/删除操作可能低效。
- 链式结构:使用指针或引用来链接结点(如链表),每个结点包含数据域和指向子树的指针。优点是灵活,适用于动态树结构。
3. 二叉树的定义和类型
二叉树是树的特例,具有严格的子树顺序规则。
- 基本定义:二叉树是 n 个结点的有限集合,它要么为空树,要么由一个根结点和两棵互不相交的左子树与右子树组成。关键特点包括:
- 每个结点最多有两个子树(左子树和右子树)。
- 左子树和右子树有固定顺序,不能颠倒(例如,交换左右子树会改变树结构)。
- 如果结点只有一个子树,必须明确指定是左子树或右子树。
- 特殊二叉树类型:
- 斜树(skewed tree):所有结点都只有左子树(左斜树)或只有右子树(右斜树)。结构类似线性链表。
- 满二叉树(full binary tree):所有分支结点都有左右子树,且所有叶结点都在同一层上。结点总数达到最大值(2^k - 1,k 为深度)。
- 完全二叉树(complete binary tree):对树按层序编号(根为 1),每个结点的编号与同深度的满二叉树中对应结点位置相同。不完全二叉树在编号上会有空缺。
4. 二叉树的数学特性和遍历方法
- 数学特性:
- 第 i 层(i ≥ 1)最多有 2^(i-1) 个结点。
- 深度为 k 的二叉树(k ≥ 1)最多有 2^k - 1 个结点。
- 关键公式:对于任意二叉树,叶子结点数(n0)和度为 2 的结点数(n2)满足 n0 = n2 + 1。
- 对于有 n 个结点的完全二叉树,深度为 (log₂(n)) + 1。
- 遍历方法:遍历是访问树中所有结点的过程,三种主要顺序:
- 前序遍历(preorder traversal):顺序为“根-左-右”。先访问根结点,然后递归访问左子树,最后右子树。
- 中序遍历(inorder traversal):顺序为“左-根-右”。从根开始但不先访问根;先递归访问左子树,再访问根,最后右子树。常用于二叉搜索树(BST)。
- 后序遍历(postorder traversal):顺序为“左-右-根”。从根开始但不先访问根;先递归访问左子树,然后右子树,最后根。
- 层序遍历(level order traversal):按层从上到下、从左到右访问结点。
遍历方法强调递归实现,适用于树结构的递归特性。
5. GDB调试工具的常规使用步骤
GDB(GNU Debugger)的实用指南,用于调试C/C++程序,特别是处理段错误和一般调试:
- 调试段错误(segmentation fault):
- 编译时添加调试选项:使用
gcc -g *.c
生成带调试信息的可执行文件。 - 启动GDB:运行
gdb a.out
。 - 运行程序:在GDB中输入
r
(run)命令执行程序。 - 重现错误:让程序运行到崩溃点。
- 定位错误:输入
where
或bt
命令显示调用栈,找出段错误发生的具体位置(如文件和行号)。
- 编译时添加调试选项:使用
- 一般调试流程:
- 设置断点:使用
b
命令(break),例如b fun.c:36
在文件 fun.c 的第 36 行设置断点,或b myfun
在函数 myfun 入口处设断点。 - 运行程序:输入
r
开始执行,程序会在断点处暂停。 - 单步执行:
n
(next):步过,执行下一行代码(如果遇到函数,不进入函数内部)。s
(step):步入,执行下一行代码(如果遇到函数,进入函数内部)。
- 查看变量:使用
p
(print)命令,例如p a
显示变量 a 的值,或p *ptr
显示指针 ptr 指向的数据。display a
可持续显示变量变化。 - 控制执行:
c
(continue):从当前断点继续运行,直到下一个断点或程序结束。return
:强制从当前函数返回调用处。
- 辅助命令:
set print elements 300
设置字符串显示长度(避免截断)。
- 设置断点:使用
- 关键优势:GDB 能通过 where 命令追踪函数调用栈,帮助快速定位内存错误或逻辑问题。
6. 希尔排序算法
- 算法原理:希尔排序通过分组比较和插入来优化排序。它使用一个“间隙”(gap)序列,初始 gap 为数组长度的一半,逐步减小 gap 至 1(此时等价于插入排序)。核心是减少逆序对,提升效率。
- 代码实现:
void shell_sort(int a[], int len) {for (int gap = len / 2; gap > 0; gap /= 2) { // gap 从 len/2 开始,每次减半for (int i = gap; i < len; ++i) // 从 gap 位置开始遍历{ int temp = a[i]; // 保存当前元素int j = i;while (j >= gap && a[j - gap] > temp) { // 比较并移动元素a[j] = a[j - gap]; // 将较大元素后移j -= gap; // 跳转到前一个 gap 位置}a[j] = temp; // 插入 temp 到正确位置}} }
- 关键步骤:
- 外层循环:控制 gap 值(gap = len/2, gap/2, ..., 1)。
- 内层循环:对每个 gap 组进行插入排序。从索引 gap 开始,将元素与同组前驱比较,移动元素以保持有序。
- 效率:希尔排序平均时间复杂度为 O(n log n),优于简单插入排序 O(n²),适用于中等规模数据。
- 应用场景:常用于嵌入式系统或内存受限环境,因代码简洁高效。
代码实现:
1.创建数
2.三种遍历方法
3.以队列形式遍历
4..摧毁树
头文件
#ifndef _TREE_H_
#define TREE_H_typedef char DATATYPE;
typedef struct BiTNode /* 结点结构 */
{DATATYPE data; /* 结点数据 */struct BiTNode *lchild, *rchild; /* 左右孩子指针 */
} BiTNode;//定义队列节点结构
typedef struct QueueNode
{// 指向树节点的指针BiTNode *treeNode;// 指向下一个队列节点struct QueueNode *next;
} QueueNode;//定义队列结构
typedef struct Queue
{// 队列头部QueueNode *head;// 队列尾部QueueNode *tail;
} Queue;extern void CreateTree(BiTNode **root);
extern void PreOrderTraverse(BiTNode *root);
extern void InOrderTraverse(BiTNode *root);
extern void PostOrderTraverse(BiTNode *root);
extern void DestroyTree(BiTNode *root); extern int isQueueEmpty(Queue *queue);
extern void enqueue(Queue *queue, BiTNode *treeNode);
extern BiTNode* dequeue(Queue *queue);
extern void each_of_tree(BiTNode *root);
extern void initQueue(Queue *queue);
#endif
函数
#include <stdio.h>
#include "tree.h"
#include <stdlib.h>char data[] = "Abd#g###ce#h##fi###";
int ind = 0;void CreateTree(BiTNode **root)
{char c = data[ind++];if ('#' == c){*root = NULL;return;}else{*root = malloc(sizeof(BiTNode));if (NULL == *root) /*判断申请是否成功*/{printf("malloc error\n");*root = NULL;return;} (*root)->data = c;CreateTree(&(*root)->lchild); /*左*/CreateTree(&(*root)->rchild); /*右*/}return;
}
//根左右(前序)
void PreOrderTraverse(BiTNode *root)
{if (NULL == root){return;}else{printf("%c", root->data);PreOrderTraverse(root->lchild);PreOrderTraverse(root->rchild);}return;
}
//左根右(中序)
void InOrderTraverse(BiTNode *root)
{if (NULL == root){return;}InOrderTraverse(root->lchild);printf("%c", root->data);InOrderTraverse(root->rchild);return;
}
//左右根(后序)
void PostOrderTraverse(BiTNode *root)
{if (NULL == root){return;}PostOrderTraverse(root->lchild);PostOrderTraverse(root->rchild);printf("%c", root->data);return;
}
void DestroyTree(BiTNode *root)
{if (NULL == root){return;}DestroyTree(root->lchild);DestroyTree(root->rchild);free(root);root = NULL;return;
}// 初始化队列
void initQueue(Queue *queue)
{queue->head = queue->tail = NULL;
}// 判断队列是否为空
int isQueueEmpty(Queue *queue)
{return queue->head == NULL;
}// 入队
void enqueue(Queue *queue, BiTNode *treeNode)
{QueueNode *newNode = malloc(sizeof(QueueNode));newNode->treeNode = treeNode;newNode->next = NULL;if (isQueueEmpty(queue)) {queue->head = queue->tail = newNode;} else {queue->tail->next = newNode;queue->tail = newNode;}
}// 出队
BiTNode* dequeue(Queue *queue)
{if (isQueueEmpty(queue)) {return NULL;}QueueNode *temp = queue->head;BiTNode *treeNode = temp->treeNode;queue->head = queue->head->next;if (queue->head == NULL) {queue->tail = NULL;}free(temp);return treeNode;
}// 层序遍历
void each_of_tree(BiTNode *root)
{if (root == NULL) {return;}//定义对联Queue queue;// 初始化队列initQueue(&queue);// 根节点入队enqueue(&queue, root);while (!isQueueEmpty(&queue)) {// 出队BiTNode *current = dequeue(&queue);//访问节点printf("%c \n", current->data);// 左子节点入队if (current->lchild != NULL) {enqueue(&queue, current->lchild);}// 右子节点入队if (current->rchild != NULL) {enqueue(&queue, current->rchild);}}
}
主函数
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include "tree.h"int main(int argc, char **argv)
{BiTNode *root;CreateTree(&root);PreOrderTraverse(root);printf("\n");InOrderTraverse(root);printf("\n");PostOrderTraverse(root);printf("\n");DestroyTree(root);root = NULL;each_of_tree(root);return 0;
}
7. 整体总结
- 数据结构核心:树和二叉树的定义、属性、存储及遍历是重点,强调树的结构特性(如度、深度)和二叉树的具体规则(如左右子树顺序)。数学特性(如结点数量公式)和遍历方法(前序、中序、后序)为算法实现奠定基础。
- 实用工具:GDB 调试部分提供 step-by-step 指南,解决段错误和一般调试问题,突出命令如
where
、b
、p
的重要性。 - 算法应用:希尔排序作为高效排序算法,以代码示例展示其 gap 策略和插入优化。