目录
[1. 树 (Tree) 的基本概念](#1. 树 (Tree) 的基本概念)
[1.2 树的相关术语](#1.2 树的相关术语)
[1.3 树的表示:孩子兄弟表示法](#1.3 树的表示:孩子兄弟表示法)
[1.4 树形结构的实际运用场景](#1.4 树形结构的实际运用场景)
[2. 二叉树 (Binary Tree)](#2. 二叉树 (Binary Tree))
[2.2 特殊的二叉树](#2.2 特殊的二叉树)
[2.3 二叉树的存储结构](#2.3 二叉树的存储结构)
[3. 实现顺序结构二叉树:堆 (Heap)](#3. 实现顺序结构二叉树:堆 (Heap))
[3.2 堆的实现](#3.2 堆的实现)
[3.2.1 向上调整算法 (AdjustUp) 与堆的插入](#3.2.1 向上调整算法 (AdjustUp) 与堆的插入)
[3.2.2 向下调整算法 (AdjustDown) 与堆的删除](#3.2.2 向下调整算法 (AdjustDown) 与堆的删除)
[3.3 堆的应用](#3.3 堆的应用)
[3.3.1 堆排序 (Heap Sort)](#3.3.1 堆排序 (Heap Sort))
[3.3.2 TOP-K 问题](#3.3.2 TOP-K 问题)
[4. 实现链式结构二叉树](#4. 实现链式结构二叉树)
[4.1 前、中、后序遍历](#4.1 前、中、后序遍历)
[4.3 层序遍历](#4.3 层序遍历)
[4.4 判断是否为完全二叉树](#4.4 判断是否为完全二叉树)
[6. 二叉树选择题解析](#6. 二叉树选择题解析)
1. 树 (Tree) 的基本概念
树是一种非线性的数据结构,由 n (n≥0) 个有限结点组成一个具有层次关系的集合。它看起来像一棵倒挂的树,根朝上,叶朝下。
-
递归定义:
- 有一个特殊的结点,称为根结点 (Root),它没有前驱结点。
- 除根结点外,其余结点被分成 M (M>0) 个互不相交的集合 T1, T2, ..., Tm,其中每一个集合 Ti 又是一棵结构与树类似的子树。
- 每棵子树的根结点有且只有一个前驱,可以有0个或多个后继。
-
核心特征:
- 子树之间不能有交集(否则就是图结构)。
- 除了根结点,每个结点有且仅有一个父结点。
- 一棵有 N 个结点的树有 N-1 条边。
1.2 树的相关术语
:
- 父结点/双亲结点 (Parent):若一个结点含有子结点,则这个结点称为其子结点的父结点。
- 子结点/孩子结点 (Child):一个结点含有的子树的根结点称为该结点的子结点。
- 结点的度 (Degree of Node):一个结点有几个孩子,它的度就是多少。
- 树的度 (Degree of Tree):一棵树中,所有结点的度的最大值。
- 叶子结点/终端结点 (Leaf):度为 0 的结点。
- 分支结点/非终端结点 (Branch):度不为 0 的结点。
- 兄弟结点 (Sibling):具有相同父结点的结点。
- 结点的层次 (Level):从根开始定义,根为第 1 层,根的子结点为第 2 层,以此类推。
- 树的高度或深度 (Height/Depth):树中结点的最大层次。
- 祖先 (Ancestor):从根到该结点所经分支上的所有结点。
- 子孙 (Descendant):以某结点为根的子树中任一结点。
- 路径 (Path):从树中任意节点出发,沿父节点-子节点连接,达到任意节点的序列。
- 森林 (Forest):由 m (m>0) 棵互不相交的树的集合。
1.3 树的表示:孩子兄弟表示法
树的存储表示方式有多种,如双亲表示法、孩子表示法等。最常用的是孩子兄弟表示法。
cpp
struct TreeNode
{
struct Node* child; // 指向左边开始的第一个孩子结点
struct Node* brother; // 指向其右边的下一个兄弟结点
int data; // 结点中的数据域
};
这种方法巧妙地将一棵复杂的多叉树转化为一个二叉链表结构,便于统一处理。
1.4 树形结构的实际运用场景
- 文件系统:计算机的文件系统(如 Windows 资源管理器)是树形结构的典型应用。设备和驱动器是根,文件夹是分支结点,文件是叶子结点,清晰地表达了层级关系。
2. 二叉树 (Binary Tree)
二叉树是树形结构中最常用的一种。一棵二叉树是结点的一个有限集合,该集合或者为空,或者由一个根结点加上两棵分别称为左子树和右子树的二叉树组成。
- 特点 :
- 二叉树不存在度大于 2 的结点。
- 二叉树的子树有左右之分,次序不能颠倒,因此二叉树是有序树。
2.2 特殊的二叉树
- 满二叉树 (Full Binary Tree):一个二叉树,如果每一层的结点数都达到最大值,则这个二叉树就是满二叉树。如果层数为 K,则结点总数为 2^k - 1。
- 完全二叉树 (Complete Binary Tree) :对于深度为 K 的,有 n 个结点的二叉树,当且仅当其每一个结点都与深度为 K 的满二叉树中编号从 1 至 n 的结点一一对应时,称之为完全二叉树。满二叉树是一种特殊的完全二叉树。
二叉树的重要性质
- 若规定根结点的层数为 1,则一棵非空二叉树的第 i 层上最多有 2^(i-1) 个结点。
- 若规定根结点的层数为 1,则深度为 h 的二叉树的最大结点数是 2^h - 1。
- 若规定根结点的层数为 1,具有 n 个结点的满二叉树的深度 h = log₂(n+1)。
- 对任何一棵二叉树,如果度为 0 的叶结点个数为 n₀,度为 2 的分支结点个数为 n₂,则有 n₀ = n₂ + 1 。
- 证明 :设度为1的节点数为 n₁,总节点数 N = n₀ + n₁ + n₂。树的边数为 N-1。从度的角度看,边数也等于 0n₀ + 1n₁ + 2*n₂。联立两式:n₀ + n₁ + n₂ - 1 = n₁ + 2n₂,化简得 n₀ = n₂ + 1。
2.3 二叉树的存储结构
- 顺序结构 :使用数组存储。一般只适合表示完全二叉树 ,因为非完全二叉树会造成空间浪费。现实中,堆 (Heap) 这种特殊的二叉树通常使用顺序结构的数组来存储。
- 链式结构 :用链表来表示。每个结点由三个域组成:数据域、左指针域、右指针域。
- 二叉链 :
leftChild,data,rightChild。这是最常用的结构。 - 三叉链 :
leftChild,data,rightChild,parent。多了一个指向父节点的指针,便于某些操作。
- 二叉链 :
3. 实现顺序结构二叉树:堆 (Heap)
堆是一种特殊的完全二叉树,使用数组存储。它满足以下性质:
- 堆中某个结点的值总是不大于(小堆)或不小于(大堆)其父结点的值。
- 堆总是一棵完全二叉树。
堆的性质(数组下标从0开始) :
对于序号为 i 的结点:
- 若 i > 0,其双亲序号为 (i-1)/2;若 i=0,则为根结点,无双亲。
- 若 2i+1 < n,其左孩子序号为 2i+1;否则无左孩子。
- 若 2i+2 < n,其右孩子序号为 2i+2;否则无右孩子。
3.2 堆的实现
堆的底层结构为数组,定义如下:
cpp
typedef int HPDataType;
typedef struct Heap
{
HPDataType* a;
int size;
int capacity;
} HP;
3.2.1 向上调整算法 (AdjustUp) 与堆的插入
-
插入逻辑:将新数据插入到数组的尾部,然后进行向上调整,直到满足堆的性质。
-
代码实现 :
cppvoid AdjustUp(HPDataType* a, int child) { int parent = (child - 1) / 2; while (child > 0) { if (a[child] > a[parent]) { // 大堆逻辑 Swap(&a[child], &a[parent]); child = parent; parent = (child - 1) / 2; } else { break; } } } void HPPush(HP* php, HPDataType x) { // 1. 检查容量,不足则扩容 if (php->size == php->capacity) { // ... realloc 逻辑 ... } // 2. 插入数据到尾部 php->a[php->size] = x; php->size++; // 3. 向上调整 AdjustUp(php->a, php->size - 1); } -
向上调整建堆时间复杂度 :O(NlogN)。
- 推导 :假设是满二叉树,高度为 h。第 i 层有 2^(i-1) 个节点,最多需要向上调整 i-1 次。总移动次数 T(h) = 2¹1 + 2²2 + ... + 2^(h-1)*(h-1)。通过错位相减法,最终推导出 T(n) ≈ nlog₂n。
3.2.2 向下调整算法 (AdjustDown) 与堆的删除
-
删除逻辑:删除堆顶数据。将堆顶元素与最后一个元素交换,然后删除数组最后一个数据(即原堆顶),再对新的堆顶进行向下调整。
-
前提:左右子树必须已经是一个堆。
-
代码实现 :
cppvoid AdjustDown(HPDataType* a, int n, int parent) { int child = parent * 2 + 1; // 假设左孩子 while (child < n) { // 1. 选出左右孩子中大的那个(大堆) if (child + 1 < n && a[child + 1] > a[child]) { ++child; } // 2. 孩子比父亲大,就交换 if (a[child] > a[parent]) { Swap(&a[child], &a[parent]); parent = child; child = parent * 2 + 1; } else { break; } } } void HPPop(HP* php) { assert(php->size > 0); // 1. 交换堆顶和堆尾 Swap(&php->a[0], &php->a[php->size - 1]); // 2. 删除堆尾 php->size--; // 3. 向下调整 AdjustDown(php->a, php->size, 0); } -
向下调整建堆时间复杂度 :O(N)。
- 推导:从最后一个非叶子节点开始,从右到左、从下到上进行向下调整。假设树高为 h,第 i 层有 2^(i-1) 个节点,最多需要向下调整 h-i 次。总移动次数 T(h) = 2⁰*(h-1) + 2¹*(h-2) + ... + 2^(h-2)*1。通过错位相减法,最终推导出 T(n) ≈ n。
3.3 堆的应用
3.3.1 堆排序 (Heap Sort)
-
版本二(原地排序,推荐) :
- 建堆:对数组使用向下调整算法建堆(O(N))。升序建大堆,降序建小堆。
- 排序:将堆顶元素与堆尾元素交换,堆的有效大小减1,然后对新的堆顶进行向下调整(O(logN))。重复此过程直到堆中只剩一个元素。
- 总时间复杂度 :O(N) + O(NlogN) = O(NlogN)。
3.3.2 TOP-K 问题
- 问题:求数据集合中前 K 个最大或最小的元素。
- 最佳方案 :使用堆。
- 用数据集合中前 K 个元素来建堆。
- 求前 K 个最大 的元素,则建小堆。
- 求前 K 个最小 的元素,则建大堆。
- 用剩余的 N-K 个元素依次与堆顶元素比较。
- 如果新元素比堆顶元素更符合条件(例如,找最大时,新元素比小堆的堆顶大),则替换堆顶元素,并进行一次向下调整。
- 遍历完成后,堆中剩余的 K 个元素就是所求结果。
- 用数据集合中前 K 个元素来建堆。
- 时间复杂度:O(K + (N-K)logK)。
4. 实现链式结构二叉树
链式二叉树通过指针连接,其结构定义如下:
cpp
typedef int BTDataType;
typedef struct BinaryTreeNode
{
struct BinaryTreeNode* left;
struct BinaryTreeNode* right;
BTDataType val;
} BTNode;
二叉树的定义是递归式的,因此其操作也基本都是递归实现。
4.1 前、中、后序遍历
遍历规则基于访问根结点的时机:
- 前序遍历 (Preorder):根结点 -> 左子树 -> 右子树
- 中序遍历 (Inorder):左子树 -> 根结点 -> 右子树
- 后序遍历 (Postorder):左子树 -> 右子树 -> 根结点
代码实现:
cpp
void PreOrder(BTNode* root) {
if (root == NULL) { printf("N "); return; }
printf("%d ", root->val);
PreOrder(root->left);
PreOrder(root->right);
}
void InOrder(BTNode* root) {
if (root == NULL) { printf("N "); return; }
InOrder(root->left);
printf("%d ", root->val);
InOrder(root->right);
}
void PostOrder(BTNode* root) {
if (root == NULL) { printf("N "); return; }
PostOrder(root->left);
PostOrder(root->right);
printf("%d ", root->val);
}
4.3 层序遍历
- 定义:从根结点出发,自上而下,自左至右逐层访问树的结点。
- 实现 :需要借助队列 。
- 根节点入队。
- 当队列不为空时,取出队头节点并访问。
- 如果该节点有左孩子,左孩子入队。
- 如果该节点有右孩子,右孩子入队。
- 重复步骤2-4。
4.4 判断是否为完全二叉树
- 思路 :基于层序遍历。
- 进行层序遍历,但遇到空节点(NULL)时不跳出,而是将其子节点(也是NULL)继续入队。
- 当从队列中取出一个 NULL 节点时,停止入队操作。
- 检查队列中剩余的所有节点,如果全都是 NULL,则是完全二叉树;如果还有非 NULL 节点,则不是。
6. 二叉树选择题解析
二叉树性质题
-
某二叉树共有 399 个结点,其中有 199 个度为 2 的结点,则该二叉树中的叶子结点数为()
- 解析:根据性质 n₀ = n₂ + 1。已知 n₂ = 199,所以 n₀ = 199 + 1 = 200。
- 答案:B (200)
-
在具有 2n 个结点的完全二叉树中,叶子结点个数为 ()
- 解析:在完全二叉树中,度为1的节点 n₁ 只能是 0 或 1。总节点数 N = n₀ + n₁ + n₂ = 2n。又因为 n₀ = n₂ + 1,所以 N = 2n₀ + n₁ - 1 = 2n。即 2n₀ = 2n - n₁ + 1。因为 2n₀ 和 2n 都是偶数,所以 n₁ 必须是 1。代入得 2n₀ = 2n - 1 + 1 = 2n,所以 n₀ = n。
- 答案:A (n)
-
一棵完全二叉树的结点数位为531个,那么这棵树的高度为 ()
- 解析:深度为 h 的满二叉树节点数为 2^h - 1。2⁹ - 1 = 511,2¹⁰ - 1 = 1023。因为 511 < 531 ≤ 1023,所以高度为 10。
- 答案:B (10)
-
一个具有 767 个结点的完全二叉树,其叶子结点个数为 ()
- 解析:N = 767。根据完全二叉树性质,n₁ 只能是 0 或 1。N = n₀ + n₁ + n₂ = 2n₀ + n₁ - 1 = 767。所以 2n₀ = 768 - n₁。因为 2n₀ 是偶数,768 也是偶数,所以 n₁ 必须是 0。代入得 2n₀ = 768,n₀ = 384。
- 答案:B (384)
链式二叉树遍历选择题
-
某完全二叉树按层次输出(同一层从左到右)的序列为 ABCDEFGH。该完全二叉树的前序序列为()
- 解析 :根据层序序列
ABCDEFGH还原完全二叉树结构:A是根,B是A左孩子,C是A右孩子,D是B左孩子,E是B右孩子,F是C左孩子,G是C右孩子,H是D左孩子。前序遍历(根左右):A -> B -> D -> H -> E -> C -> F -> G。 - 答案:A (ABDHECFG)
- 解析 :根据层序序列
-
二叉树的先序遍历和中序遍历如下:先序遍历:EFHIGJK; 中序遍历:HFIEJKG. 则二叉树根结点为()
- 解析:先序遍历的第一个节点就是根节点。
- 答案:A (E)
-
设一课二叉树的中序遍历序列:badce,后序遍历序列:bdeca,则二叉树前序遍历序列为 ____。
- 解析 :
- 后序
bdeca的最后一个a是根。 - 在中序
badce中找到a,a左边b是左子树,右边dce是右子树。 - 看右子树:后序
dec的最后一个c是右子树的根。中序dce中,c左边d是左孩子,右边e是右孩子。 - 树结构为:a 是根,左孩子 b,右孩子 c;c 的左孩子 d,右孩子 e。
- 前序遍历(根左右):a -> b -> c -> d -> e。
- 后序
- 答案:D (abcde)
- 解析 :
-
某二叉树的后序遍历序列与中序遍历序列相同,均为 ABCDEF,则按层次输出(同一层从左到右)的序列为
- 解析 :
- 后序
ABCDEF的最后一个F是根。 - 中序
ABCDEF中,F在最后,说明F没有右子树,ABCDE是其左子树。 - 递归分析左子树:后序
ABCDE的E是根,中序ABCDE的E在最后,说明E没有右子树,ABCD是其左子树。 - 以此类推,这棵树是一个所有节点都只有左孩子的单支树:F -> E -> D
- 后序
- 解析 :