二叉树的顺序存储
一、为什么需要顺序存储二叉树?
在数据结构里,二叉树的存储方式有两种:链式存储和顺序存储。链式存储用指针串联节点,灵活但对初学者不太友好;顺序存储用数组存放节点,通过下标计算节点关系,实现简单,尤其适合完全二叉树 —— (比如堆排序里的堆就是完全二叉树的顺序存储)。
顺序存储的核心思路:把二叉树的节点按层序遍历的顺序放进数组,根节点放第一个位置,然后依次是左孩子、右孩子,以此类推。通过下标公式,能快速找到任意节点的父节点和子节点,不需要指针。
二、C 语言实现代码
#include <stdio.h>
#include <string.h>
// 定义最大节点数(一般给固定大小,这里设为100足够用)
#define MAX_SIZE 100
// 二叉树的顺序存储结构
typedef struct {
char data[MAX_SIZE]; // 存储节点数据(用char举例,也常考字符或整数)
int length; // 当前节点个数
} SeqBinTree;
// 1. 初始化二叉树
void InitTree(SeqBinTree *tree) {
// 把数组清空,用'\0'表示空节点
memset(tree->data, '\0', sizeof(tree->data));
tree->length = 0; // 初始节点数为0
}
// 2. 插入节点(按层序顺序插入,适合构建完全二叉树)
void InsertNode(SeqBinTree *tree, char data) {
if (tree->length >= MAX_SIZE) {
printf("树已满,无法插入\n");
return;
}
// 新节点放在数组末尾(层序顺序)
tree->data[tree->length] = data;
tree->length++;
}
// 3. 求父节点(已知子节点下标i)
char GetParent(SeqBinTree *tree, int i) {
if (i == 0) { // 根节点(下标0)没有父节点
return '\0';
}
// 父节点下标公式:(i-1)/2(整数除法)
int parent_idx = (i - 1) / 2;
return tree->data[parent_idx];
}
// 4. 求左孩子(已知父节点下标i)
char GetLeftChild(SeqBinTree *tree, int i) {
// 左孩子下标公式:2*i + 1
int left_idx = 2 * i + 1;
// 下标超出范围,说明没有左孩子
if (left_idx >= tree->length) {
return '\0';
}
return tree->data[left_idx];
}
// 5. 求右孩子(已知父节点下标i)
char GetRightChild(SeqBinTree *tree, int i) {
// 右孩子下标公式:2*i + 2
int right_idx = 2 * i + 2;
if (right_idx >= tree->length) {
return '\0';
}
return tree->data[right_idx];
}
// 6. 前序遍历(递归版,
高频考点)
// 顺序:根节点 -> 左子树 -> 右子树
void PreOrder(SeqBinTree *tree, int i) {
// 下标越界,说明是空节点,递归结束
if (i >= tree->length) {
return;
}
// 访问当前节点
printf("%c ", tree->data[i]);
// 遍历左子树(左孩子下标:2*i+1)
PreOrder(tree, 2*i + 1);
// 遍历右子树(右孩子下标:2*i+2)
PreOrder(tree, 2*i + 2);
}
// 7. 中序遍历(递归版)
// 顺序:左子树 -> 根节点 -> 右子树
void InOrder(SeqBinTree *tree, int i) {
if (i >= tree->length) {
return;
}
InOrder(tree, 2*i + 1); // 左
printf("%c ", tree->data[i]); // 根
InOrder(tree, 2*i + 2); // 右
}
// 8. 后序遍历(递归版)
// 顺序:左子树 -> 右子树 -> 根节点
void PostOrder(SeqBinTree *tree, int i) {
if (i >= tree->length) {
return;
}
PostOrder(tree, 2*i + 1); // 左
PostOrder(tree, 2*i + 2); // 右
printf("%c ", tree->data[i]); // 根
}
// 9. 层序遍历(顺序存储的天然优势,直接按数组顺序输出)
void LevelOrder(SeqBinTree *tree) {
for (int i = 0; i < tree->length; i++) {
printf("%c ", tree->data[i]);
}
}
// 主函数:测试以上功能
int main() {
SeqBinTree tree;
InitTree(&tree);
// 构建一个完全二叉树
// 结构如下:
// A
// / \
// B C
// / \ /
// D E F
InsertNode(&tree, 'A'); // 下标0(根)
InsertNode(&tree, 'B'); // 下标1(A的左孩子)
InsertNode(&tree, 'C'); // 下标2(A的右孩子)
InsertNode(&tree, 'D'); // 下标3(B的左孩子)
InsertNode(&tree, 'E'); // 下标4(B的右孩子)
InsertNode(&tree, 'F'); // 下标5(C的左孩子)
// 测试父子关系
printf("B的父节点:%c\n", GetParent(&tree, 1)); // 应该是A
printf("C的左孩子:%c\n", GetLeftChild(&tree, 2)); // 应该是F
printf("B的右孩子:%c\n", GetRightChild(&tree, 1));// 应该是E
// 测试遍历
printf("\n前序遍历:");
PreOrder(&tree, 0); // 预期:A B D E C F
printf("\n中序遍历:");
InOrder(&tree, 0); // 预期:D B E A F C
printf("\n后序遍历:");
PostOrder(&tree, 0); // 预期:D E B F C A
printf("\n层序遍历:");
LevelOrder(&tree); // 预期:A B C D E F
return 0;
}
三、代码分析(重点看这里)
1. 结构体设计
typedef struct {
char data[MAX_SIZE];
int length;
} SeqBinTree;
data
数组:按层序顺序存放节点,下标 0 是根节点,依次往后是第二层左、右孩子,第三层左左、左右、右左、右右……length
:记录实际节点数,方便判断是否越界(常考边界条件,比如空树、满树)。
2. 核心公式(必须记住)
顺序存储的灵魂是下标计算:
- 若父节点下标为
i
:- 左孩子下标 =
2*i + 1
- 右孩子下标 =
2*i + 2
- 左孩子下标 =
- 若子节点下标为
i
:- 父节点下标 =
(i-1)/2
(整数除法,比如 i=4 时,(4-1)/2=1,对应父节点下标 1)
- 父节点下标 =
3. 遍历方法(算法题核心)
四种遍历中,前、中、后序用递归实现(递归版),层序遍历直接按数组顺序输出(顺序存储的优势)。
以中序遍历为例,逻辑是 “左 - 根 - 右”:
void InOrder(SeqBinTree *tree, int i) {
if (i >= tree->length) return; // 递归终止条件
InOrder(tree, 2*i + 1); // 先遍历左子树
printf("%c ", tree->data[i]); // 再访问根节点
InOrder(tree, 2*i + 2); // 最后遍历右子树
}
递归终止条件是 “下标越界”,这是初学者容易漏掉的点,写算法必须包含。
四、扩展思考
- 适用场景:顺序存储只适合完全二叉树,否则会浪费空间(比如斜树,数组里会有很多空位)。可能问 “为什么堆要用顺序存储?”—— 因为堆是完全二叉树,顺序存储效率高。
- 与链式存储的对比:顺序存储找父子节点快(O (1)),但插入删除不方便;链式存储反之。简答题可能考这个对比。
- 常见错误:计算下标时忘记 “根节点从 0 开始”,比如误把左孩子公式写成
2*i
(这是根节点从 1 开始的情况)。代码里严格用 0 开始,符合 C 语言数组习惯。
二叉树的链式存储
一、为什么用链式存储?
二叉树的链式存储用指针将节点串联起来,每个节点记录自己的左右孩子位置,不用像顺序存储那样考虑数组下标,对各种形态的二叉树都很友好。比如一棵不规则的二叉树(有的节点只有左孩子,有的只有右孩子),用链式存储不会浪费空间,插入删除也更方便,这也是它在考试和实际应用中更常见的原因。
二、C 语言实现代码
下面是适合初学者的代码,操作都很基础,注释详细,跟着实现一遍就能明白核心逻辑:
#include <stdio.h>
#include <stdlib.h>
// 节点结构体(考试中最常用的定义方式)
typedef struct Node {
char data; // 节点存储的数据(字符为例,也可以是整数等)
struct Node *left; // 左孩子指针
struct Node *right; // 右孩子指针
} Node;
// 创建新节点(基础操作,必须掌握)
Node* createNode(char data) {
Node *newNode = (Node*)malloc(sizeof(Node));
if (newNode == NULL) {
printf("内存分配失败\n");
return NULL;
}
newNode->data = data;
newNode->left = NULL; // 初始时左右孩子都为空
newNode->right = NULL;
return newNode;
}
// 前序遍历(根→左→右,考试常考递归实现)
void preOrder(Node *root) {
if (root == NULL) { // 空节点则返回,递归终止条件
return;
}
printf("%c ", root->data); // 访问根节点
preOrder(root->left); // 遍历左子树
preOrder(root->right); // 遍历右子树
}
// 中序遍历(左→根→右)
void inOrder(Node *root) {
if (root == NULL) {
return;
}
inOrder(root->left); // 先遍历左子树
printf("%c ", root->data); // 再访问根节点
inOrder(root->right); // 最后遍历右子树
}
// 后序遍历(左→右→根)
void postOrder(Node *root) {
if (root == NULL) {
return;
}
postOrder(root->left); // 遍历左子树
postOrder(root->right); // 遍历右子树
printf("%c ", root->data); // 最后访问根节点
}
// 查找节点(根据值查找,辅助功能)
Node* findNode(Node *root, char target) {
if (root == NULL) {
return NULL; // 树空或没找到
}
if (root->data == target) {
return root; // 找到目标节点
}
// 先在左子树找,找到就返回
Node *leftResult = findNode(root->left, target);
if (leftResult != NULL) {
return leftResult;
}
// 左子树没找到,再在右子树找
return findNode(root->right, target);
}
// 给指定节点添加左孩子
void addLeftChild(Node *parent, char data) {
if (parent == NULL) {
printf("父节点不存在,无法添加左孩子\n");
return;
}
if (parent->left != NULL) {
printf("左孩子已存在,无法添加\n");
return;
}
parent->left = createNode(data);
}
// 给指定节点添加右孩子
void addRightChild(Node *parent, char data) {
if (parent == NULL) {
printf("父节点不存在,无法添加右孩子\n");
return;
}
if (parent->right != NULL) {
printf("右孩子已存在,无法添加\n");
return;
}
parent->right = createNode(data);
}
// 释放二叉树内存(避免内存泄漏,好习惯)
void freeTree(Node *root) {
if (root == NULL) {
return;
}
freeTree(root->left); // 先释放左子树
freeTree(root->right); // 再释放右子树
free(root); // 最后释放当前节点
}
int main() {
// 手动构建一棵示例二叉树:
// A
// / \
// B C
// / / \
// D E F
// 创建根节点
Node *root = createNode('A');
// 给根节点添加左右孩子
addLeftChild(root, 'B');
addRightChild(root, 'C');
// 找到B节点,给它添加左孩子D
Node *nodeB = findNode(root, 'B');
addLeftChild(nodeB, 'D');
// 找到C节点,给它添加左右孩子E和F
Node *nodeC = findNode(root, 'C');
addLeftChild(nodeC, 'E');
addRightChild(nodeC, 'F');
// 测试遍历(考试中最常考的内容)
printf("前序遍历:");
preOrder(root); // 预期:A B D C E F
printf("\n中序遍历:");
inOrder(root); // 预期:D B A E C F
printf("\n后序遍历:");
postOrder(root); // 预期:D B E F C A
// 释放内存
freeTree(root);
return 0;
}
三、代码分析
1. 核心结构体设计
typedef struct Node {
char data;
struct Node *left;
struct Node *right;
} Node;
评论区