一、树
1. 基本术语
树是一种非线性的数据结构,是由唯一的根和若干不相交的子树组成的。
- 结点的度(Degree):结点的子树个数;
- 树的度:树的所有结点中最大的度数;
- 叶结点(Leaf):度为0的结点;
- 父结点(Parent):有子树的结点是其子树的根节点的父结点;
- 子结点/孩子结点(Child):若A结点是B结点的父结点,则称B结点是A结点的子结点;
- 兄弟结点(Sibling):具有同一个父结点的各结点彼此是兄弟结点;
- 祖先结点(Ancestor):沿树根到某一结点路径上的所有结点都是这个结点的祖先结点;
- 子孙结点(Descendant):某一结点的子树中的所有结点是这个结点的子孙;
- 堂兄弟结点:父节点在同一层的结点互为堂兄弟结点;
- 路径和路径长度:从结点 n1n_1n1 到 nkn_knk 的路径为一个结点序列n1,n2,...,nkn_1,n_2,...,n_kn1,n2,...,nk。nin_ini是ni+1n_{i+1}ni+1的父结点。路径所包含边的个数为路径的长度;
- 结点的层次(Level):规定根结点在1层,其他任一结点的层数是其父结点的层数加1;
- 树的深度(Depth):树中所有结点中的最大层次是这棵树的深度
- 有序树:树中结点的子树从左往右是有次序的,不能交换;
- 无序树:树中结点的子树没有顺序,可以任意交换;
- 森林:若干不相交的树的集合;
2. 存储结构
- 顺序存储:双亲存储结构,用一个一维数组即可实现
int tree[MaxSize];
用数组下标来表示树的结点,数组中的值表示该结点的父结点
- 链式存储:孩子存储结构(邻接表)、孩子兄弟存储结构;
二、二叉树
1. 基本概念
- 二叉树的每个结点最多有两棵树,即二叉树中的结点的度只能为 0、1、20、1、20、1、2,且二叉树的子树有顺序之分,不能颠倒。
- 满二叉树:每一个层的结点数都达到最大值的二叉树;
- 完全二叉树:对于深度为 kkk,有 nnn 个结点的二叉树,其每一个结点都与深度为 kkk 的满二叉树中编号从 111 至 nnn 的结点一一对应;
2. 主要性质
- 一个二叉树第 iii 层的最大结点数为: 2i−12^{i-1}2i−1 ;
- 深度为 kkk 的二叉树的最大结点总数为:2k−12^{k}-12k−1 ;
- 对任意非空二叉树上,若叶结点个数为 n0n_0n0,度为 222 的结点个数为 n2n_2n2,则满足关系:n0=n2+1n_0=n_2+1n0=n2+1;
- 在一棵度为 nnn 的树中,若度为 111 的结点数为 n1n_1n1,度为 222 的结点数为 n2n_2n2,… ,度为 mmm 的结点数为 nmn_mnm,则叶结点的个数 n0n_0n0 有:
n0=1+n2+2n3+...+(m−1)nm n_0=1+n_2+2n_3+...+(m-1)n_mn0=1+n2+2n3+...+(m−1)nm - 给定 nnn 个节点,能构成 C2nnn+1\frac{C^n_{2n}}{n+1}n+1C2nn 种不同的二叉树;
- 【满二叉树】结点总数与深度的关系为:n=∑i=0k2i=2k+1−1n=\sum_{i=0}^k{2^i}=2^{k+1}-1n=∑i=0k2i=2k+1−1;
- 【完全二叉树】具有 nnn 个结点的完全二叉树的高度为 ⌊log2n⌋+1\lfloor{\log_2{n}}\rfloor+1⌊log2n⌋+1 或 ⌈log2(n+1)⌉\lceil{\log_2{(n+1)}}\rceil⌈log2(n+1)⌉;
- 【完全二叉树】左子树的结点数为(总结点数为 nnn):
l(n)={n−2k−1,n+1−2k−1≤2k−12k−1,n+1−2k−1>2k−1l(n)= \left \{\begin{array}{cc} n-2^{k-1}, &n+1-2^{k-1}\leq 2^{k-1}\\ 2^k-1, & n+1-2^{k-1} > 2^{k-1} \end{array}\right. l(n)={n−2k−1,2k−1,n+1−2k−1≤2k−1n+1−2k−1>2k−1 - 【完全二叉树】右子树的结点数为:r(n)=n−l(n)r(n)=n-l(n)r(n)=n−l(n);
3. 存储结构
- 顺序存储:适用于完全二叉树,对于一般二叉树则会造成大量空间浪费;
- 链式存储:详见二叉搜索树中的内容
4. 树的遍历
- 先序遍历(PreOrder):根、左、右
- 中序遍历 (InOrder):左、根、右
- 后序遍历(PostOrder):左、右、根
- 层序遍历(LevelOrder)
- 递归遍历
//先序遍历:根、左、右
void PreorderTraversal(BinTree BT)
{
if (BT) {
printf("%d ", BT->Data);
PreorderTraversal(BT->Left);
PreorderTraversal(BT->Right);
}
}
// 中序遍历:左、根、右
void InorderTraversal(BinTree BT)
{
if (BT) {
InorderTraversal(BT->Left);
/* 此处假设对BT结点的访问就是打印数据 */
printf("%d ", BT->Data); /* 假设数据为整型 */
InorderTraversal(BT->Right);
}
}
// 后序遍历:左、右、根
void PostorderTraversal(BinTree BT)
{
if (BT) {
PostorderTraversal(BT->Left);
PostorderTraversal(BT->Right);
printf("%d ", BT->Data);
}
}
- 非递归遍历:需要用一个 stack 来模拟递归时的函数调用
// 先序遍历
void PreorderTraversal(BinTree BT)
{
if (BT) {
stack<TreeNode*> S;
TreeNode* p = NULL;
S.push(BT);
while (!S.empty()){
p = S.top(); // 访问根结点
S.pop();
Visit(p);
if (p->Right) // 右子树入栈
S.push(p->Right);
if (p->Left) // 左子树入栈
S.push(p->Left);
}
}
}
// 中序遍历
void InorderTraversal(BinTree BT)
{
if (BT) {
stack<TreeNode*> S;
TreeNode* p = BT;
while (!S.empty() || p != NULL){
while (p) { // 若左孩子存在,则入栈
S.push(p);
p = p->left;
}
if (!S.empty()) {
p = S.top(); S.pop();
Visit(p);
p = p->right;
}
}
}
}
// 后序遍历:需要判断上次访问的节点是位于左子树,还是右子树
void PostorderTraversal(BinTree BT)
{
if (root == NULL) {
stack<TreeNode*> S;
TreeNode *p, *pLast; // p:当前访问节点,pLast:上次访问节点
p = root;
pLastVisit = NULL;
while (pCur){ // 先将 p 移动到左子树最下边
S.push(p);
p = p->Left;
}
while (!S.empty()){
p = S.top();
// 根节点被访问的前提:无右子树或右子树已被访问过
if (p->Right == NULL || p->Right == pLast){
Visit(p); S.pop();
pLast = p; // 修改最近被访问的节点
}
else{ // 进入右子树
p = p->Right;
while (p){
S.push(p);
p = p->Left;
}
}
}
}
}
// 后序遍历:双栈法
// 逆后序遍历序列是先序遍历过程中对左右子树遍历顺序交换所得到的结果
void PostorderTraversal(BinTree BT)
{
if (BT) {
stack<TreeNode*> S1;
stack<TreeNode*> S2;
TreeNode* p = NULL;
S1.push(BT);
while (!S.empty()){
p = S1.top(); S1.pop();
S2.push(p);
if (p->Left)
S.push(p->Left);
if (p->Right)
S.push(p->Right);
}
while (!S2.empty()) {
p = S2.top(); S2.pop();
VIsit(p);
}
}
}
- 层序遍历
// 层序遍历:队列实现
void LevelorderTraversal(BinTree BT)
{
queue<ElementType> q;
BinTree T;
if (!BT)
return; /* 若是空树则直接返回 */
q.push(BT);
while (!q.empty()) {
T = q.front(); q.pop();
VIsit(T);
if (T->Left)
q.push(T->Left);
if (T->Right)
q.push(T->Right);
}
}
- 线索二叉树,结点结构如下:
Left | Ltag | Data | Rtag | Right |
---|
struct TBTNode {
ElementType Data;
int Ltag = 0, Rtag = 0;
struct TBTNode *Left;
struct TBTNode *Right;
};
如果 Ltag = 0,则 Left 表示指针,指向左孩子,否则 Left 表示为线索,指向结点的直接前驱;
如果 Rtag = 0,则 Right 表示指针,指向右孩子,否则 Right 表示为线索,指向结点的直接后继;
(1)中序线索二叉树
// 通过中序遍历对二叉树线索化的递归算法
void InThread(TagBTree p, TBTNode *&pre) // pre 表示 p 的前驱
{
if (p) {
InThread(p->Left, pre); // 左子树线索化
if (!p->Left) {
p->Left = pre;
p->Ltag = 1;
}
if (pre && !pre->Right) {
pre->Right = p;
pre->Rtag = 1;
}
pre = p;
InThread(p->Right, pre); // 右子树线索化
}
}
// 通过中序遍历建立线索二叉树
void CreatInTread(TBTNode *root)
{
TBTNode *p = NULL;
if (root) {
InThread(root, pre);
pre->Right = NULL;
pre->Rtag = 1;
}
}
// 查找以 p 为根的线索二叉树在中序遍历下的第一个结点
TBTNode* First(TBTNode *p)
{
while (p->Ltag == 0)
p = p->Left;
return p;
}
// 查找 p 在中序遍历下的后继结点
TBTNode* Next(TBTNode *p)
{
if (p->Rtag == 0)
return First(p->Right);
else
return p->Right;
}
(2)前序线索二叉树
// 构造前序线索二叉树
void PreThread(TagBTree p, TBTNode *&pre) // pre 表示 p 的前驱
{
if (p) {
if (!p->Left) {
p->Left = pre;
p->Ltag = 1;
}
if (pre && !pre->Right) {
pre->Right = p;
pre->Rtag = 1;
}
pre = p;
if (!p->Ltag)
preThread(p->Left, pre);
if (!p->Rtag)
preThread(p->Right, pre);
}
}
// 遍历访问前序线索二叉树
void PreOrder(TBTNode *root)
{
if (root) {
TBTNode *p = root;
while (p) {
while (p->Ltag == 0) {
VIsit(p);
p->Left;
}
Visit(p);
p = p->Right;
}
}
}
(3)后序线索二叉树
void PostThread(TagBTree p, TBTNode *&pre) // pre 表示 p 的前驱
{
if (p) {
PostThread(p->Left, pre);
PostThread(p->Right, pre);
if (!p->Left) {
p->Left = pre;
p->Ltag = 1;
}
if (pre && !pre->Right) {
pre->Right = p;
pre->Rtag = 1;
}
pre = p;
}
}
对于后序线索二叉树,应注意:
- 若结点 x 是二叉树的根,则其后继为空;
- 若结点 x 是其父结点的右孩子,或是其父结点的左结点且父结点没有右孩子,则其后继为父结点;
- 若结点 x 是其父结点的左孩子,且父结点有右孩子,则其后继为父结点右子树上按后序遍历列出的第一个结点;
- 遍历顺序的转换
// 先序 + 中序 ——> 后序
// 初始调用: PostOrder(0,0,0,N);
void PostOrder(int preL, int inL, int postL, int n)
{
if (n == 0)
return;
if (n == 1) {
post[postL] = pre[preL];
return;
}
int root = pre[preL];
post[postL + n - 1] = root;
int i = 0;
for (; i < n; i++) {
if (in[inL + i] == root)
break;
}
int L = i;
int R = n - L - 1;
PostOrder(preL + 1, inL, postL, L); //左子树
PostOrder(preL + L + 1, inL + L + 1, postL + L, R); //右子树
}
// 中序 + 后序 ——> 先序 /* 方法类似 */
// 先序 + 后序 ——> 无法确定一棵唯一的二叉树
结论:对于一棵二叉树,可以根据其前序和中序 或 中序和后序 这两种遍历顺序的组合来构造一棵唯一的二叉树,而根据前序和后序则不能唯一确定这棵二叉树;
5. 树、森林、二叉树的互相转换
- 树 ——> 二叉树
- 二叉树 ——> 树
- 森林 ——> 二叉树
- 二叉树 ——> 森林:只需要不停的将根节点有右孩子的二叉树的右孩子链接断开,直到不存在根节点有右孩子即可;
6. 树和森林的遍历
- 先序遍历:先访问根结点,再访问根结点的每棵子树;
- 后序遍历:先访问根结点的子树,再访问根结点;
对于森林来说,以树为单位进行遍历;
二、二叉搜索树
1. 定义
- 二叉搜索树可以为空,如果不为空,则满足:
1)非空左子树的所有键值小于其根节点的键值;
2)非空右子树的所有键值大于其根节点的键值;
3)左、右子树都是二叉搜索树; - 存储结构
// 二叉搜索树 BST
struct TreeNode;
typedef int ElementType;
typedef struct TNode *Position;
typedef struct TNode *SearchTree;
struct TNode {
ElementType Data;
SearchTree Left;
SearchTree Right;
};
注意:二叉搜索树的中序遍历序列应当是递增的;
2. 基本操作
// 插入
SearchTree Insert(SearchTree BST, ElementType X)
{
if (!BST) { /* 若原树为空,生成并返回一个结点的二叉搜索树 */
BST = (SearchTree)malloc(sizeof(struct TNode));
BST->Data = X;
BST->Left = BST->Right = NULL;
}
else { /* 开始找要插入元素的位置 */ // 此处简单修改后可作为 查找 函数
if (X < BST->Data)
BST->Left = Insert(BST->Left, X); /*递归插入左子树*/
else if (X > BST->Data)
BST->Right = Insert(BST->Right, X); /*递归插入右子树*/
/* else X已经存在,什么都不做 */
}
return BST;
}
SearchTree Delete(SearchTree BST, ElementType X)
{
Position Tmp;
if (!BST)
printf("要删找除的元素未到");
else {
if (X < BST->Data)
BST->Left = Delete(BST->Left, X); /* 从左子树递归删除 */
else if (X > BST->Data)
BST->Right = Delete(BST->Right, X); /* 从右子树递归删除 */
else { /* BST就是要删除的结点 */
/* 如果被删除结点有左右两个子结点 */
if (BST->Left && BST->Right) {
/* 从右子树中找最小的元素填充删除结点 */
Tmp = FindMin(BST->Right);
BST->Data = Tmp->Data;
/* 从右子树中删除最小元素 */
BST->Right = Delete(BST->Right, BST->Data);
}
else { /* 被删除结点有一个或无子结点 */
Tmp = BST;
if (!BST->Left) /* 只有右孩子或无子结点 */
BST = BST->Right;
else /* 只有左孩子 */
BST = BST->Left;
free(Tmp);
}
}
}
return BST;
}
3. 平衡二叉树(AVL 树)
- 定义:一种特殊的二叉搜索树,对任一结点,其左右子树的高度之差的绝对值不超过 1;
- 平衡因子:一个结点的平衡因子即其左子树的高度减去右子树高度的差值,可能的取值有:-1、0、1;
- 平衡调整:LL型、RR型、LR型、RL型
(1)单旋转:LL、RR
(2)双旋转:LR、RL
代码实现
typedef int ElementType;
typedef struct AVLNode *Position;
typedef Position AVLTree; /* AVL树类型 */
struct AVLNode {
ElementType Data; /* 结点数据 */
AVLTree Left; /* 指向左子树 */
AVLTree Right; /* 指向右子树 */
int Height; /* 树高 */
};
int GetHeight(Position P)
{
if (P == NULL)
return -1;
else
return P->Height;
}
AVLTree SingleLeftRotation(AVLTree A)
{ /* 注意:A必须有一个左子结点B */
/* 将A与B做左单旋,更新A与B的高度,返回新的根结点B */
AVLTree B = A->Left;
A->Left = B->Right;
B->Right = A;
A->Height = max(GetHeight(A->Left), GetHeight(A->Right)) + 1;
B->Height = max(GetHeight(B->Left), A->Height) + 1;
return B;
}
AVLTree SingleRightRotation(AVLTree A)
{
AVLTree B = A->Right;
A->Right = B->Left;
B->Left = A;
A->Height = max(GetHeight(A->Left), GetHeight(A->Right)) + 1;
B->Height = max(GetHeight(B->Left), A->Height) + 1;
return B;
}
// 左-右 双旋
AVLTree DoubleLeftRightRotation(AVLTree A)
{ /* 注意:A必须有一个左子结点B,且B必须有一个右子结点C */
/* 将A、B与C做两次单旋,返回新的根结点C */
/* 将B与C做右单旋,C被返回 */
A->Left = SingleRightRotation(A->Left);
/* 将A与C做左单旋,C被返回 */
return SingleLeftRotation(A);
}
// 右-左 双旋
AVLTree DoubleRightLeftRotation(Position A)
{
A->Right = SingleLeftRotation(A->Right);
return SingleRightRotation(A);
}
AVLTree Insert(AVLTree T, ElementType X)
{ /* 将X插入AVL树T中,并且返回调整后的AVL树 */
if (!T) { /* 若插入空树,则新建包含一个结点的树 */
T = (AVLTree)malloc(sizeof(struct AVLNode));
T->Data = X;
T->Height = 0;
T->Left = T->Right = NULL;
} /* if (插入空树) 结束 */
else if (X < T->Data) {
/* 插入T的左子树 */
T->Left = Insert(T->Left, X);
/* 如果需要左旋 */
if (GetHeight(T->Left) - GetHeight(T->Right) == 2)
if (X < T->Left->Data)
T = SingleLeftRotation(T); /* 左单旋 */
else
T = DoubleLeftRightRotation(T); /* 左-右双旋 */
} /* else if (插入左子树) 结束 */
else if (X > T->Data) {
/* 插入T的右子树 */
T->Right = Insert(T->Right, X);
/* 如果需要右旋 */
if (GetHeight(T->Left) - GetHeight(T->Right) == -2)
if (X > T->Right->Data)
T = SingleRightRotation(T); /* 右单旋 */
else
T = DoubleRightLeftRotation(T); /* 右-左双旋 */
} /* else if (插入右子树) 结束 */
/* else X == T->Data,无须插入 */
/* 别忘了更新树高 */
T->Height = max(GetHeight(T->Left), GetHeight(T->Right)) + 1;
return T;
}
4. B-树
三、堆(完全二叉树)
- 特性
(1)结构性:用数组表示的完全二叉树
(2)有序性:任一结点的键值是其子树所有结点的最大值/最小值 - 存储结构
// 堆
typedef int ElementType;
typedef struct HNode *Heap; /* 堆的类型定义 */
struct HNode {
ElementType *Data; /* 存储元素的数组 */
int Size; /* 堆中当前元素个数 */
int Capacity; /* 堆的最大容量 */
};
typedef Heap MaxHeap; /* 最大堆 */
typedef Heap MinHeap; /* 最小堆 */
- 基本操作
#define MAXDATA 1000 /* 该值应根据具体情况定义为大于堆中所有可能元素的值 */
#define MINDATA -1
MaxHeap CreateHeap(int MaxSize)
{ /* 创建容量为MaxSize的空的最大堆 */
MaxHeap H = (MaxHeap)malloc(sizeof(struct HNode));
H->Data = (ElementType *)malloc((MaxSize + 1) * sizeof(ElementType));
H->Size = 0;
H->Capacity = MaxSize;
H->Data[0] = MAXDATA; /* 定义"哨兵"为大于堆中所有可能元素的值*/
// H->Data[0] = MINDATA; // 最小堆
return H;
}
bool IsFull(MaxHeap H)
{
return (H->Size == H->Capacity);
}
bool Insert(MaxHeap H, ElementType X)
{ /* 将元素X插入最大堆H,其中H->Data[0]已经定义为哨兵 */
int i;
if (IsFull(H)) {
printf("最大堆已满");
return false;
}
i = ++H->Size; /* i指向插入后堆中的最后一个元素的位置 */
for (; H->Data[i / 2] < X; i /= 2) // 若是最小堆,则 >
H->Data[i] = H->Data[i / 2]; /* 上滤X */
H->Data[i] = X; /* 将X插入 */
return true;
}
#define ERROR -1 /* 错误标识应根据具体情况定义为堆中不可能出现的元素值 */
bool IsEmpty(MaxHeap H)
{
return (H->Size == 0);
}
ElementType DeleteMax(MaxHeap H)
{ /* 从最大堆H中取出键值为最大的元素,并删除一个结点 */
int Parent, Child;
ElementType MaxItem, X;
if (IsEmpty(H)) {
printf("最大堆已为空");
return ERROR;
}
MaxItem = H->Data[1]; /* 取出根结点存放的最大值 */
/* 用最大堆中最后一个元素从根结点开始向上过滤下层结点 */
X = H->Data[H->Size--]; /* 注意当前堆的规模要减小 */
for (Parent = 1; Parent * 2 <= H->Size; Parent = Child) {
Child = Parent * 2;
if ((Child != H->Size) && (H->Data[Child] < H->Data[Child + 1]))
Child++; /* Child指向左右子结点的较大者 */
if (X >= H->Data[Child]) break; /* 找到了合适位置 */
else /* 下滤X */
H->Data[Parent] = H->Data[Child];
}
H->Data[Parent] = X;
return MaxItem;
}
/*----------- 建造最大堆 -----------*/
void PercDown(MaxHeap H, int p)
{ /* 下滤:将H中以H->Data[p]为根的子堆调整为最大堆 */
int Parent, Child;
ElementType X;
X = H->Data[p]; /* 取出根结点存放的值 */
for (Parent = p; Parent * 2 <= H->Size; Parent = Child) {
Child = Parent * 2;
if ((Child != H->Size) && (H->Data[Child] < H->Data[Child + 1]))
Child++; /* Child指向左右子结点的较大者 */
if (X >= H->Data[Child]) break; /* 找到了合适位置 */
else /* 下滤X */
H->Data[Parent] = H->Data[Child];
}
H->Data[Parent] = X;
}
void BuildHeap(MaxHeap H)
{ /* 调整H->Data[]中的元素,使满足最大堆的有序性 */
/* 这里假设所有H->Size个元素已经存在H->Data[]中 */
int i;
/* 从最后一个结点的父节点开始,到根结点1 */
for (i = H->Size / 2; i > 0; i--)
PercDown(H, i);
}
四、哈夫曼树与哈夫曼编码
1. 基本概念
- 哈夫曼树又称为最优二叉树,特点是带权路径最短:
(1)路径:指树中的一个结点到另一个结点的分支所构成的路线;
(2)路径长度:指路径上的分支数目;
(3)树的路径长度:指从根结点到每个结点的路径长度之和;
(4)带权路径长度:即从该结点到根之间的路径长度乘以结点的权值的数值;
(5)树的带权路径长度(WPL):指树中所有叶子结点的带权路径长度之和; - 特点:
(1)没有度为 1 的结点;
(2)n 个叶子结点的哈夫曼树共有 2n−12n-12n−1 个结点;
(3)哈夫曼树的任意非叶结点的左右子树交换后仍为哈夫曼树;
(4)对同一组权值,可能存在不同构的哈夫曼树;
- 构造:通过最小堆来实现,每次选择两个权值最小的结点进行合并,并将合并后的结点插入堆中(此处默认构造的哈夫曼树为二叉树)
注意:对于哈夫曼三叉树,可能需要补充一个权值为 0 的结点后才能完成构造;
2. 哈夫曼编码
- 任一字符的编码串都不是另一字符编码串的前缀,即前缀码不同;
- 哈夫曼编码产生的是最短前缀码;
补充
- 根据一个给定序列,构造一棵唯一的完全二叉搜索树
// arr 按照 非递减 排序
SearchTree CBT(int* arr, int N, SearchTree T)
{
int k = (int)(log(N) / log(2)); //计算深度
if (k == 0) {
T = Insert(arr[0], T);
return T;
}
else {
int temp = (int)pow(2, k - 1);
int lastnode = N + 1 - temp * 2; //最后一层节点数
int leftnode = 0; //左子树节点总数
if (lastnode <= temp) {
leftnode = N - temp; //底层节点全在左子树
}
else
leftnode = temp * 2 - 1; //左子树为满二叉树
int rightnode = N - leftnode - 1;
T = Insert(arr[leftnode], T);
if (leftnode != 0) { //递归实现
int* arr_left = new int[leftnode];
memcpy(arr_left, arr, leftnode * sizeof(int));
T->left = CBT(arr_left, leftnode, T->left);
delete[] arr_left;
}
if (rightnode != 0) {
int* arr_right = new int[rightnode];
memcpy(arr_right, arr + leftnode + 1, rightnode * sizeof(int));
T->right = CBT(arr_right, rightnode, T->right);
delete[] arr_right;
}
return T;
}
}