一、二叉树
逻辑结构
1. 基本定义
二叉树是一种常见的树形数据结构,其中每个节点最多有两个子节点,分别称为左子节点和右子节点。二叉树的逻辑结构可以通过以下几个方面来描述:
- 节点(Node):二叉树中的每个元素称为节点。每个节点包含三个部分:一个数据元素、一个指向左子节点的链接和一个指向右子节点的链接。
- 根节点(Root Node):二叉树的顶点节点,唯一没有父节点的节点。
- 叶子节点(Leaf Node):没有子节点的节点。
- 内部节点(Internal Node):至少有一个子节点的节点,且度 ≤ 2 \leq2 ≤2。
- 子树(Subtree):由一个节点及其所有后代节点构成的树。每个节点都有左子树和右子树。
- 空树(Empty Tree):不包含任何节点的树。
m
m
m叉树 v.s. 度为
m
m
m的树。
2. 二叉树的类型
- 普通二叉树(Binary Tree):每个节点最多有两个子节点,没有其他特殊性质要求。
- 满二叉树(Full Binary Tree):所有非叶子节点都有两个子节点,并且所有叶子节点在同一层。高度为 h h h的满二叉树,含有 2 h − 1 2^h-1 2h−1个 结点。
- 完全二叉树(Complete Binary Tree):除了最后一层外,每层节点都是满的,并且最后一层的叶子节点尽可能左对齐。
- 平衡二叉树(Balanced Binary Tree):左右子树的高度差不超过1。
- 二叉排序树(Balanced Sorting Tree):左子树关键字 < 根结点关键字 < 右子树关键字。
3. 二叉树的性质
- n 0 = 1 + n 2 n_0=1+n_2 n0=1+n2
n = n 0 + n 1 + n 2 n=n_0+n_1+n_2 n=n0+n1+n2(二叉树的性质)且 n = n 1 + 2 n 2 + 1 n=n_1+2n_2+1 n=n1+2n2+1(树的性质),联立可得 n 0 = 1 + n 2 n_0=1+n_2 n0=1+n2。
- 具有 n n n个结点的完全二叉树的高度 h h h是 ⌈ l o g 2 ( 1 + n ) ⌉ \lceil log_2(1+n) \rceil ⌈log2(1+n)⌉或 ⌊ l o g 2 n + 1 ⌋ \lfloor log_2n+1 \rfloor ⌊log2n+1⌋
高为 h h h的二叉树最多有 2 h − 1 2^h-1 2h−1个结点(满二叉树)
高为 h h h的二叉树最少有 ( 2 h − 1 − 1 ) + 1 = 2 h − 1 (2^{h-1}-1)+1=2^{h-1} (2h−1−1)+1=2h−1个结点(高是 h − 1 h-1 h−1的满二叉树再加上一个结点)
那么 2 h − 1 ≤ n ≤ 2 h − 1 2^{h-1}\leq n\leq 2^h-1 2h−1≤n≤2h−1,可推出 h − 1 ≤ l o g 2 n h-1 \leq log_2n h−1≤log2n 和 l o g 2 ( n + 1 ) ≤ h log_2(n+1)\leq h log2(n+1)≤h
故有, h = ⌊ l o g 2 n + 1 ⌋ h=\lfloor log_2n+1 \rfloor h=⌊log2n+1⌋和 h = ⌈ l o g 2 ( 1 + n ) ⌉ h=\lceil log_2(1+n) \rceil h=⌈log2(1+n)⌉
- 对于完全二叉树,可以由结点数
n
n
n推出
n
0
、
n
1
、
n
2
n_0、n_1、n_2
n0、n1、n2,即
n
1
=
0
n_1=0
n1=0或
1
1
1。
- 若完全二叉树有 2 k 2k 2k个结点,则必有 n 1 = 1 , n 0 = k , n 2 = k − 1 n_1=1,n_0=k,n_2=k-1 n1=1,n0=k,n2=k−1。
因为 n = n 0 + n 1 + n 2 → 2 k = n 0 + n 1 + n 2 n=n_0+n_1+n_2 \rightarrow 2k=n_0+n_1+n_2 n=n0+n1+n2→2k=n0+n1+n2,又因为 n 0 = 1 + n 2 n_0=1+n_2 n0=1+n2,故有 2 k = 2 n 2 + n 1 + 1 2k=2n_2+n_1+1 2k=2n2+n1+1,所以 n 1 = 1 , n 2 = k − 1 , n 0 = k n_1=1,n_2=k-1,n_0=k n1=1,n2=k−1,n0=k。
- 若完全二叉树有 2 k − 1 2k-1 2k−1个结点,则必有 n 1 = 0 , n 0 = k , n 2 = k − 1 n_1=0,n_0=k,n_2=k-1 n1=0,n0=k,n2=k−1。
因为 n = n 0 + n 1 + n 2 → 2 k − 1 = n 0 + n 1 + n 2 n=n_0+n_1+n_2 \rightarrow 2k-1=n_0+n_1+n_2 n=n0+n1+n2→2k−1=n0+n1+n2,又因为 n 0 = 1 + n 2 n_0=1+n_2 n0=1+n2,故有 2 k − 1 = 2 n 2 + n 1 + 1 2k-1=2n_2+n_1+1 2k−1=2n2+n1+1,所以 n 1 = 0 , n 2 = k − 1 , n 0 = k n_1=0,n_2=k-1,n_0=k n1=0,n2=k−1,n0=k。
物理结构
1. 顺序存储
特别适合完全二叉树。根节点存储在数组的第一个位置,对于任意节点在数组中的位置 i i i:
- i i i的左孩子节点的位置是 2 i 2i 2i。
- i i i的右孩子节点的位置是 2 i + 1 2i + 1 2i+1。
- i i i的双亲结点的位置是 ⌊ i / 2 ⌋ \lfloor i/2\rfloor ⌊i/2⌋
若是普通的二叉树所需空间仍然是 2 h − 1 2^h-1 2h−1。极大的浪费了存储空间,此时考虑用链式存储方式。
2. 链式存储
typedef struct BiTNode {
int data; // 数据域
struct BiTNode *lchild, *rchild; // 指针域
} BiTNode, *BiTree;
注:
n
n
n个结点的二叉链表共有
n
+
1
n+1
n+1 个空链域。
数据的操作
1.先序遍历(根左右)
递归算法
void PreOrder(BiTree T)
{
if(T)
{
visit(T);
PreOrder(T->lchild);
PreOrder(T->rchild);
}
}
非递归算法
void PreOrder(BiTree T)
{
Stack s, InitStack(s); // 定义栈
BiTNode *p = T; // p是遍历指针
while (p || !isEmpty(s))
{
if (p) // 一路向左
{
visit(p);
Push(s, p);
p = p -> lchild;
}
else // 左孩子是空,则转向访问右孩子
{
Pop(s, p) // 已经访问过该结点,出栈
p = p -> rchild;
}
}
}
2.中序遍历(左根右)
递归算法
void InOrder(BiTree T)
{
if(T)
{
InOrder(T->lchild);
visit(T);
InOrder(T->rchild);
}
}
非递归算法
void InOrder(BiTree T)
{
Stack s, InitStack(s); // 栈
BiTNode *p = T; // 遍历指针
while (p || !isEmpty(s)) // 若p是NULL则证明GetTop(s)是叶子结点
{
if (p) // 一路向左
{
Push(s, p); // 入栈的是根结点
p = p -> lchild;
}
else
{
Pop(s, p), visit(p); // 因为是一路向左,先弹出来的一定是左孩子,然后是根节点,最后是右孩子
p = p -> rchild; // 左走到头了,该向右转
}
}
}
3.后序遍历(根左右)
递归算法
void PostOrder(BiTree T)
{
if(T)
{
PostOrder(T->lchild);
PostOrder(T->rchild);
visit(T);
}
}
非递归算法
void PostOrder(BiTree T)
{
Stack s, InitStack(s);
BiTNode *p = T;
BiTNode *r = NULL;
while (p || !isEmpty(s))
{
if (p) // 一路向左
{
Push(s, p);
p = p -> lchild;
}
else
{
GetTop(s, p);
if (p -> rchild && p -> rchild != r) p = p -> rchild; // 如果右子树存在且未被访问过
else // 否则访问节点并出栈
{
Pop(s, p);
visit(p);
r = p; // 标记已访问节点
p = NULL; // 因为是叶子结点或整个子树已经被处理完,还需要继续访问上一个结点,故将p设置为NULL
}
}
}
}
为什么要判断右子树是否访问过:
- 后序遍历要求在访问一个节点之前先访问其左子树和右子树,因此在每次访问一个节点时必须确保其左右子树已经访问完毕。
- 通过 r r r指针标记上次访问的节点,如果当前节点的右子树存在且没有被访问过,则先处理右子树;否则说明左右子树都处理完毕,可以访问当前节点。
- 这种机制确保了每个节点在其左右子树都处理完后才被访问,符合后序遍历的定义。
4.层次遍历(自上而下,从左到右)
void InverLevel(BiTree T)
{
Queue q, InitQueue(q); // 队列
BiTNode *p = T; // 遍历指针
EnQueue(q, p);
while (!isEmpty(q))
{
DeQueue(q, p);
visit(p);
if (p -> lchild) EnQueue(q, p -> lchild); // 若左孩子非空,则入队列
if (p -> rchild) EnQueue(q, p -> rchild); // 若右孩子非空,则入队列
}
}
5. 由遍历序列构造二叉树
先序+中序
查找中序遍历中节点值的位置
int findPosition(int* inorder, int inStart, int inEnd, int data)
{
for (int i = inStart; i <= inEnd; i++)
if (inorder[i] == data)
return i;
return -1;
}
递归构建二叉树
BiTree buildTree(int* preorder, int preStart, int preEnd, int* inorder, int inStart, int inEnd) {
if (preStart > preEnd || inStart > inEnd) // 越界
return NULL;
// 先序遍历的第一个元素是当前子树的根节点
int rootData = preorder[preStart];
BiTree root = (BiTree)malloc(sizeof(BiTNode));
root->data = rootData;
root->lchild = root->rchild = NULL;
// 在中序遍历中找到根节点的位置
int rootIndex = findPosition(inorder, inStart, inEnd, rootData);
// 计算左子树的节点数量
int leftTreeSize = rootIndex - inStart;
// 递归构建左子树
root->lchild = buildTree(preorder, preStart + 1, preStart + leftTreeSize, inorder, inStart, rootIndex - 1);
// 递归构建右子树
root->rchild = buildTree(preorder, preStart + leftTreeSize + 1, preEnd, inorder, rootIndex + 1, inEnd);
return root;
}
构建二叉树的主函数
BiTree constructTree(int* preorder, int* inorder, int length)
{
return buildTree(preorder, 0, length - 1, inorder, 0, length - 1);
}
后序+中序
递归构建二叉树
BiTree buildTree(int* postorder, int postStart, int postEnd, int* inorder, int inStart, int inEnd) {
if (postStart > postEnd || inStart > inEnd) {
return NULL;
}
// 后序遍历的最后一个元素是当前子树的根节点
int rootData = postorder[postEnd];
BiTree root = (BiTree)malloc(sizeof(BiTNode));
root->data = rootData;
root->lchild = root->rchild = NULL;
// 在中序遍历中找到根节点的位置
int rootIndex = findPosition(inorder, inStart, inEnd, rootData);
// 计算左子树的节点数量
int leftTreeSize = rootIndex - inStart;
// 递归构建左子树
root->lchild = buildTree(postorder, postStart, postStart + leftTreeSize - 1, inorder, inStart, rootIndex - 1);
// 递归构建右子树
root->rchild = buildTree(postorder, postStart + leftTreeSize, postEnd - 1, inorder, rootIndex + 1, inEnd);
return root;
}
层序+中序
从层序遍历中提取在当前中序区间内的节点
void extractSubLevelOrder(int* levelOrder, int* subLevelOrder, int levelSize, int* inorder, int inStart, int inEnd)
{
int subIndex = 0;
for (int i = 0; i < levelSize; i++)
for (int j = inStart; j <= inEnd; j++)
if (levelOrder[i] == inorder[j])
{
subLevelOrder[subIndex++] = levelOrder[i];
break;
}
}
递归构建二叉树
BiTree buildTree(int* levelOrder, int levelSize, int* inorder, int inStart, int inEnd) {
if (inStart > inEnd) {
return NULL;
}
// 层序遍历的第一个元素是当前子树的根节点
int rootData = levelOrder[0];
BiTree root = (BiTree)malloc(sizeof(BiTNode));
root->data = rootData;
root->lchild = root->rchild = NULL;
// 在中序遍历中找到根节点的位置
int rootIndex = findPosition(inorder, inStart, inEnd, rootData);
// 构建左子树的层序遍历数组
int leftSubLevelOrder[levelSize];
extractSubLevelOrder(levelOrder, leftSubLevelOrder, levelSize, inorder, inStart, rootIndex - 1);
// 构建右子树的层序遍历数组
int rightSubLevelOrder[levelSize];
extractSubLevelOrder(levelOrder, rightSubLevelOrder, levelSize, inorder, rootIndex + 1, inEnd);
// 递归构建左子树
root->lchild = buildTree(leftSubLevelOrder, rootIndex - inStart, inorder, inStart, rootIndex - 1);
// 递归构建右子树
root->rchild = buildTree(rightSubLevelOrder, inEnd - rootIndex, inorder, rootIndex + 1, inEnd);
return root;
}
二、线索二叉树
逻辑结构
线索二叉树是在普通二叉树的基础上进行改进,目的是为了提高对二叉树的遍历效率。在普通二叉树中,节点的左右指针通常指向其左右子树,而在线索二叉树中,除了指向左右子树的指针外,还增加了指向某种遍历次序下该节点的前驱节点和后继节点的线索。
- 在线索二叉树中,每个节点的左右指针可以被视为是线索化的。
- 左指针:
- 如果节点的左子树存在,则指向左子树。
- 如果节点的左子树不存在,则指向其在中序遍历下的前驱节点(称为前驱线索)。
- 右指针:
- 如果节点的右子树存在,则指向右子树。
- 如果节点的右子树不存在,则指向其在中序遍历下的后继节点(称为后继线索)。
其标志域的含义如下:
l
t
a
g
=
{
0
,
l
c
h
i
l
d
域指示结点的左孩子
1
,
l
c
h
i
l
d
域指示结点的前驱
r
t
a
g
=
{
0
,
r
c
h
i
l
d
域指示结点的右孩子
1
,
r
c
h
i
l
d
域指示结点的后继
\begin{aligned} & ltag = \begin{cases}0, & lchild\text { 域指示结点的左孩子 } \\ 1, & lchild\text { 域指示结点的前驱 }\end{cases} \\ & rtag = \begin{cases}0, & rchild\text { 域指示结点的右孩子 } \\ 1, & rchild\text { 域指示结点的后继 }\end{cases} \end{aligned}
ltag={0,1,lchild 域指示结点的左孩子 lchild 域指示结点的前驱 rtag={0,1,rchild 域指示结点的右孩子 rchild 域指示结点的后继
物理结构
采用链式存储方式:
typedef struct ThreadNode{
int data; // 数据域
struct ThreadNode *lchild, *rchild; // 左右孩子指针
int ltag, rtag; // 左右线索标志
} ThreadNode, *ThreadTree;
数据的操作
1. 线索二叉树的构造
先序线索二叉树
访问结点
void visit(ThreadNode *q)
{
if (q -> lchild == NULL) // 建立前驱线索
{
q -> lchild = pre;
q -> ltag = 1;
}
if (q -> rchild == NULL && pre) // 建立后继线索
{
pre -> rchild = q;
pre -> rtag = 1;
}
pre = q;
}
先序遍历
void PreThread(ThreadNode *T)
{
if (T)
{
visit(T);
if (T -> ltag == 0) // 若上一行处理前驱线索,若不判断是否是索引,就会使得程序陷入死循环中
PreThread(T -> lchild);
PreThread(T -> rchild);
}
}
创建前序索引二叉树
void CreatePreThread(ThreadNode *T)
{
pre = NULL;
if (T)
{
PreThread(T);
if (pre -> rchild == NULL) pre -> rtag = 1; // 检查最后一个结点,将其后继设置成线索
}
}
中序线索二叉树
访问结点
void visit(ThreadNode *q)
{
if (q -> lchild == NULL) // 建立前驱线索
{
q -> lchild = pre;
q -> ltag = 1;
}
if (q -> rchild == NULL && pre) // 建立后继线索
{
pre -> rchild = q;
pre -> rtag = 1;
}
pre = q;
}
中序遍历
void InThread(ThreadNode *T)
{
if (T)
{
InThread(T -> lchild);
visit(T);
InThread(T -> rchild);
}
}
创建中序索引二叉树
void CreateInThread(ThreadNode *T)
{
pre = NULL;
if (T)
{
InThread(T);
if (pre -> rchild == NULL) pre -> rtag = 1; // 检查最后一个结点,将其后继设置成线索
}
}
后序线索二叉树
访问结点
void visit(ThreadNode *q)
{
if (q -> lchild == NULL) // 建立前驱线索
{
q -> lchild = pre;
q -> ltag = 1;
}
if (q -> rchild == NULL && pre) // 建立后继线索
{
pre -> rchild = q;
pre -> rtag = 1;
}
pre = q;
}
中序遍历
void PostThread(ThreadNode *T)
{
if (T)
{
PostThread(T -> lchild);
PostThread(T -> rchild);
visit(T);
}
}
创建中序索引二叉树
void CreatePostThread(ThreadNode *T)
{
pre = NULL;
if (T)
{
PostThread(T);
if (pre -> rchild == NULL) pre -> rtag = 1; // 检查最后一个结点,将其后继设置成线索
}
}
2. 线索二叉树的遍历
先序线索二叉树找后继
ThreadNode * Nextnode(ThreadNode *p)
{
if (p -> rtag) p = p ->rlchild;
else
{
if (p -> ltag) p = p -> rchild;
else p = p -> lchild;
}
return p;
}
利用先序线索二叉树进行先序遍历
void PreOrder(ThreadNode *T)
{
for (ThreadNode *p = T; p; p = NextNode(p))
visit(p);
}
先序线索二叉树找前驱(改用三叉链表)
三叉链表
typedef struct ThreadNode {
int data; // 数据域
struct ThreadNode *lchild, *rchild, *parent; // 左右孩子、双亲指针
int ltag, rtag; // 左右线索域
}
寻找最右下结点
ThreadNode * Lastnode(ThreadNode *p)
{
while (p -> rtag == 0) p = p -> rchild;
return p;
}
找前驱
ThreadNode * Prenode(ThreadNode *p)
{
if (p -> parent == NULL) return NULL; // 根结点
if (p -> ltag) p = p -> lchild;
else
{
ThreadNode *father = p -> parent; // 找到p的双亲结点
if (p == father -> lchild) p = father;
else if (p == father -> rchild && father -> rtag) p = father;
else Lastnode(father -> lchild);
}
return p;
}
利用先序线索二叉树进行逆先序遍历
void RevPreorder(ThreadNode *T)
{
for (ThreadNode *p = LastNode(T); p; p = Prenode(p))
visit(p);
}
中序线索二叉树找后继
寻找最左下结点
ThreadNode * Firstnode(ThreadNode *p)
{
while (p -> ltag == 0) p = p -> lchild;
return p;
}
找后继
ThreadNode * Nextnode(ThreadNode *p)
{
if (p -> rtag) p = p -> rchild;
else p = Firstnode(p -> rchild);
return p;
}
利用中序线索二叉树进行中序遍历
void Inorder(ThreadNode *T)
{
for (ThreadNode *p = Firstnode(T); p; p = Nextnode(p))
visit(p);
}
中序二叉树找前驱
寻找最右下结点
ThreadNode * Lastnode(ThreadNode *p)
{
while (p -> rtag == 0) p = p -> rchild;
return p;
}
找前驱
ThreadNode * Prenode(ThreadNode *p)
{
if (p -> ltag) p = p -> lchild;
else p = LastNode(p -> lchild);
return p;
}
利用中序线索二叉树进行逆中序遍历
void RevInorder(ThreadNode *T)
{
for (ThreadNode *p = LastNode(T); p; p = Prenode(p))
visit(p);
}
后序线索二叉树找后继(改用三叉链表)
三叉链表
typedef struct ThreadNode {
int data; // 数据域
struct ThreadNode *lchild, *rchild, *parent; // 左右孩子、双亲指针
int ltag, rtag; // 左右线索域
}
寻找最左下结点
ThreadNode * Firstnode(ThreadNode *p)
{
while (p -> ltag == 0) p = p -> lchild;
return p;
}
找后继
ThreadNode * Nextnode(ThreadNode *p)
{
if (p -> parent == NULL) return NULL; // 根结点
if (p -> rtag) p = p -> rchild;
else
{
ThreadNdoe *father = p -> parent;
if (father -> rchild == p) p = father;
else if (father -> lchild == p && father -> rtag) p = father;
else p = Firstnode(father -> rchild);
}
return p;
}
利用后序线索二叉树进行后序遍历
void Postorder(ThreadNode *T)
{
for (ThreadNode *p = Firstnode(T); p; p = Nextnode(p))
visit(p);
}
后序线索二叉树找前驱
找前驱
ThreadNode * Prenode(ThreadNode *p)
{
if (p -> ltag) p = p -> lchild;
else
{
if (p -> rtag) p = p -> lchild;
else p = p -> rchild;
}
return p;
}
利用后序线索二叉树进行逆后序遍历
void RevPostorder(ThreadNode *T)
{
for (ThreadNode *p = T; p; p = Prenode(p))
visit(p);
}
三、树
逻辑结构
树是 n ( n ≥ 0 ) n(n≥0) n(n≥0)个结点的有限集。当 n = 0 n=0 n=0时,称为空树。在任意一棵非空树中应满足:
- 有且仅有一个特定的称为根的结点。
- 当
n
>
1
n>1
n>1时,其余结点可分为
m
(
m
>
0
)
m(m>0)
m(m>0)个互不相交的有限集
T
1
,
T
2
,
⋯
,
T
m
T_1,T_2,⋯,T_m
T1,T2,⋯,Tm,其中每个集
合本身又是一棵树,并且称为根的子树。
显然,树的定义是递归的,即在树的定义中又用到了其自身,树是一种递归的数据结构。树
作为一种逻辑结构,同时也是一种分层结构,具有以下两个特点:
- 树的根结点没有前驱,除根结点外的所有结点有且只有一个前驱。
- 树中所有结点都可以有零个或多个后继。
物理结构
1.顺序存储——下标表示法
假设一个
m
m
m 叉树存储在一个一维数组 A
中,其中下标从 1
开始:
- 根节点:存储在
A[1]
。 - 节点的第
k
k
k个子节点:
- 如果节点
A[i]
的下标为i
,那么其第 k k k个子节点的下标为 m ∗ i + k − ( m − 1 ) m*i + k - (m - 1) m∗i+k−(m−1),其中 1 ≤ k ≤ m 1 \leq k \leq m 1≤k≤m。
- 如果节点
- 节点的双亲节点:
- 如果节点
A[i]
的下标为i
,那么其双亲节点的下标为 ⌊ i + ( m − 1 ) m ⌋ \left\lfloor \frac{i + (m - 1)}{m} \right\rfloor ⌊mi+(m−1)⌋(根节点除外)。
- 如果节点
优点:对于完全
m
m
m叉树能够快速找到一个结点的孩子和双亲。
缺点:对于一颗非完全
m
m
m叉树,可能会造成存储空间的浪费,必须要
m
h
−
1
m
−
1
\frac{m^h-1}{m-1}
m−1mh−1个存储空间。
2.顺序存储——双亲表示法
typedef struct {
int data; // 数据域
int parent; // 双亲域
} PTNode;
typedef struct {
PTNode nodes[Maxsize];
int n; // 定义当前树的结点个数
}
优点:找双亲(父节点)很方便
缺点:找孩子不方便,只能从头到尾遍历整个数组
3.顺序存储+链式存储——孩子表示法
typedef struct CTNode { // 定义孩子结点
int child; // 孩子结点在数组中的位置
struct CTNode *next; // 下一个孩子
} CTNode;
typedef struct { // 定义根节点
int data;
CTNode *firstChild; // 记录该结点的第一个孩子
} CTBox;
typedef struct {
CTBox nodes[Maxsize];
int n, r; //定义当前结点的个数以及根节点的位置
} CTree;
优点:找孩子很方便
缺点:找双亲(父节点)不方便,只能遍历每个链表
4.链式存储——孩子兄弟表示法
typedef struct CSNode {
int data; // 数据域
struct CSNode *firstchild, *nextsibling; // 第一个孩子结点和右兄弟指针
} CSNode, *CSTree;
即左孩子右兄弟
优点:对于树的前序遍历可以借助二叉树的前序遍历。且插入删除操作也可以借助二叉树的操作实现。
缺点:访问不直观,对于要进行层次遍历不如顺序存储方便。
数据的操作
1. 树的先根遍历
树的先根遍历与这颗树所对应的二叉树的先序遍历相同
void PreOrder(CSTree *T)
{
if (T)
{
visit(T);
PreOrder(T -> firstchild);
PreOrder(T -> nextsibling);
}
}
2. 树的后根遍历
树的后根遍历与这颗树所对应的二叉树的中序遍历相同
void PostOrder(CSTree *T)
{
if (T)
{
PostOrder(T -> firstchild);
PostOrder(T -> nextsibling);
visit(T);
}
}
3. 树的层序遍历——孩子表示法
定义数据结构
typedef struct CTNode { // 定义孩子结点
int child; // 孩子结点在数组中的位置
struct CTNode *next; // 下一个孩子
} CTNode;
typedef struct { // 定义根节点
int data;
CTNode *firstChild; // 记录该结点的第一个孩子
} CTBox;
typedef struct {
CTBox nodes[Maxsize];
int n, r; //定义当前结点的个数以及根节点的位置
} CTree;
层次遍历
void InverLevel(CTree T)
{
Queue q, InitQueue(q);
CTBox p = T.nodes[T.r]; // 根结点
Enqueue(q, p); // 根结点入队列
while (!isEmpty(q))
{
Dequeue(q, p);
visit(p);
CTNode *c= p.firstchild; // 访问根结点的孩子结点
while (c) Enqueue(q, T.nodes[c.child]), c = c -> next; // 将孩子结点全部入队列
}
}
四、森林
逻辑结构
森林(Forest)是树的概念的延伸,是由 n ( n ≥ 0 ) n(n \geq 0) n(n≥0)棵互不相交的树的集合。当 n = 0 n = 0 n=0时,称为空森林。森林是一种递归的数据结构,可以将其定义为:
一个空森林:不包含任何树。
一个非空森林:由一棵树及其他不相交的若干棵树组成。
物理结构
顺序存储——双亲表示法
typedef struct {
int data; // 数据域
int parent; // 双亲域
} PFNode;
typedef struct {
PFNode nodes[Maxsize];
int n; // 定义当前树的结点个数
}
优点:找双亲(父节点)很方便
缺点:找孩子不方便,只能从头到尾遍历整个数组
顺序存储+链式存储——孩子表示法
typedef struct CTNode { // 定义孩子结点
int child; // 孩子结点在数组中的位置
struct CTNode *next; // 下一个孩子
} CTNode;
typedef struct { // 定义树结点
int data;
CTNode *firstChild; // 记录该结点的第一个孩子
} CTBox;
typedef struct {
CTBox nodes[Maxsize]; // 所有结点的数组
int n; // 当前结点的个数
int roots[Maxsize]; // 记录根节点的位置的数组
int rootCount; // 根节点的数量
} FCTree;
优点:找孩子很方便
缺点:找双亲(父节点)不方便,只能遍历每个链表
链式存储——孩子兄弟表示法
typedef struct CSNode {
int data; // 数据域
struct CSNode *firstchild; // 第一个孩子结点指针
struct CSNode *nextsibling; // 右兄弟结点指针
} CSNode, *CSTree;
typedef struct {
CSTree roots[Maxsize]; // 根节点的数组
int rootCount; // 根节点的数量
} FCSForest;
优点:对于森林的前序遍历可以借助二叉树的前序遍历。且插入删除操作也可以借助二叉树的操作实现。
缺点:访问不直观,对于要进行层次遍历不如顺序存储方便。
数据的操作
森林的先序遍历
森林的先序遍历与这颗树所对应的二叉树的先序遍历相同
void PreOrder(CSTree *T)
{
if (T)
{
visit(T);
PreOrder(T -> firstchild);
PreOrder(T -> nextsibling);
}
}
森林的中序遍历
森林的中序遍历与这颗树所对应的二叉树的中序遍历相同
void InOrder(CSTree *T)
{
if (T)
{
InOrder(T -> firstchild);
visit(T);
InOrder(T -> nextsibling);
}
}
五、树、森林与二叉树的转换
树转二叉树
森林转二叉树
二叉树转树
二叉树转森林
六、哈夫曼编码(应用)
带权路径长度
结点的权:有某种现实含义的数值(如:表示结点的重要性等)
结点的带权路径长度:从树的根到该结点的路径长度(经过的边数)与该结点上权值的乘积
树的带权路径长度:树中所有叶结点的带权路径长度之和(WPL, Weighted Path Length)
W
P
L
=
∑
i
=
1
n
w
i
l
i
WPL=\sum_{i=1}^n w_il_i
WPL=i=1∑nwili
例如这颗树的
W
P
L
=
3
×
(
5
+
1
+
10
+
3
)
=
57
WPL=3\times(5+1+10+3)=57
WPL=3×(5+1+10+3)=57。
哈夫曼树的定义
在含有 n n n个带权叶结点的二叉树中,其中带权路径长度(WPL)最小的二叉树称为哈夫曼树,也称最优二叉树。
哈夫曼树的构造
给定
n
n
n个权值分别为
w
1
,
w
2
,
…
,
w
n
w_1, w_2,…, w_n
w1,w2,…,wn的结点,构造哈夫曼树的算法描述如下:
1)将这
n
n
n个结点分别作为n棵仅含一个结点的二叉树,构成森林F。
2)构造一个新结点,从
F
F
F中选取两棵根结点权值最小的树作为新结点的左、右子树,并且将新结点的权值置为左、右子树上根结点的权值之和。
3)从
F
F
F中删除刚才选出的两棵树,同时将新得到的树加入
F
F
F中。
4)重复步骤2)和3),直至
F
F
F中只剩下一棵树为止。
哈夫曼树的性质:
- 每个初始结点最终都成为叶结点,且权值越小的结点到根结点的路径长度越大
- 哈夫曼树的结点总数为 2 n − 1 2n − 1 2n−1
n 0 = n , n 1 = 0 n_0=n,n_1=0 n0=n,n1=0,又因为 n 总 = n 0 + n 1 + n 2 n_{总}=n_0+n_1+n_2 n总=n0+n1+n2, n 0 = 1 + n 2 n_0=1+n_2 n0=1+n2,联立可得 n 总 = 2 n 0 − 1 = 2 n − 1 n_{总}=2n_0-1=2n-1 n总=2n0−1=2n−1
- 哈夫曼树中不存在度为1的结点。
- 哈夫曼树并不唯一,但WPL必然相同且为最优
七、并查集
逻辑结构
并查集(Disjoint Set)是一种数据结构,用于管理一组互不相交的集合。它支持两种主要操作:查找(Find)和合并(Union)。
物理结构——顺序存储
因为并查集只需要查找双亲结点,则用双亲表示法能将并查集的优势最大化。
typedef struct {
int data; // 数据域
int parent; // 双亲位置域
} PTNode;
typedef struct {
PTNode nodes[Maxsize]; // 双亲表示
int n; // 结点数
} PTree;
数据的操作
1.初始化
void Initial(int s[])
{
for (int i = 0; i < size; i ++ ) s[i] = -1;
}
2.并(Union)
void Union(int s[], int Root1, int Root2)
{
if (Root1 == Root2) return; // 若来自一个集合,不进行合并操作
else s[Root2] = Root1; // 将Root2变成Root1的孩子
}
3.查(Find)
int find(int s[], int x)
{
while (s[x] != -1) x = s[x];
return x;
}
4.并查集的优化
分析时间复杂度
-
Find操作( h h h为树高): O ( h ) O(h) O(h)
-
Union操作: O ( 1 ) O(1) O(1)
故并查集的时间复杂度主要是由树的高度决定的。
Union操作的优化(按秩合并)
优化思路:在每次Union操作构建树的时候,尽可能让树不长高。
- 用根节点的绝对值表示树的结点总数
- Union操作,让小树合并到大树
void Union(int s[], int Root1, int Root2)
{
if (Root1 == Root1) return;
if (s[Root2] > s[Root1]) // Root2的结点数比Root1少(绝对值)
{
s[Root1] += s[Root2]; // 累加结点数
s[Root2] = s[Root1]; // 小树合并到大树
}
else
{
s[Root2] += s[Root1]; // 累加结点数
s[Root1] = s[Root2]; // 小树合并到大树
}
}
用该方法构造出来的树 h ≤ ⌊ l o g 2 n ⌋ + 1 h\leq\lfloor log_2n \rfloor+1 h≤⌊log2n⌋+1。故Find操作的时间复杂度为 O ( l o g 2 n ) O(log_2n) O(log2n)。
Find操作的优化(路径压缩)
优化思路:在每次Find操作的时候,将查找路径上所有结点都挂到根结点下。
int find(int s[], int x)
{
if (s[x] != -1) s[x] = find(s[x]); //将x的父亲置为x父亲的祖先节点,实现路径的压缩
return s[x];
}
这样可使树的高度不超过 O ( α ( n ) ) O(\alpha(n)) O(α(n))。 α ( n ) \alpha(n) α(n)是一个增长很缓慢的函数,对于常见的 n n n值,通常 α ( n ) ≤ 4 \alpha(n)\leq4 α(n)≤4,故树高 h ≤ 4 h\leq4 h≤4,Find操作的时间复杂度 O ( 1 ) O(1) O(1)。