假 设 树 中 度 为 i ( i = 0 , 1 , ⋅ ⋅ ⋅ , m ) 的 结 点 数 为 n i 假设树中度为i(i=0,1,···,m)的结点数为n_i 假设树中度为i(i=0,1,⋅⋅⋅,m)的结点数为ni
(1) 总 结 点 数 = n 0 + n 1 + n 2 + ⋅ ⋅ ⋅ + n m 总结点数=n_0+n_1+n_2+···+n_m 总结点数=n0+n1+n2+⋅⋅⋅+nm
(2) 总 分 支 数 = 0 × n 0 + 1 × n 1 + 2 × n 2 + ⋅ ⋅ ⋅ + m × n m 总分支数=0×n_0+1×n_1+2×n_2+···+m×n_m 总分支数=0×n0+1×n1+2×n2+⋅⋅⋅+m×nm
(3) 总 结 点 数 = 总 分 支 数 + 1 总结点数=总分支数+1 总结点数=总分支数+1
由 m h − 1 + m h − 2 + m h − 3 + ⋅ ⋅ ⋅ + m − 1 m^{h-1}+m^{h-2}+m^{h-3}+···+m-1 mh−1+mh−2+mh−3+⋅⋅⋅+m−1推导,此时高度为 n n n个结点组成的树中的最小高度(等比数列求和)
此时,除了最大层,其余层的结点的双亲结点的度数应该都为 m m m,其实就是高度为 h h h的 m m m叉树至多含有结点树的逆推导
第 i i i个结点之前,有1 ∼ i − 1 \sim i-1 ∼i−1共 i − 1 i-1 i−1个结点,每一个结点都有 m m m个孩子,所以到结点 i i i的第1个子女结点时,编号已经到了 ( i − 1 ) × m + 1 (i-1)×m+1 (i−1)×m+1( + 1 +1 +1是因为要算上根结点)
当结点 i i i不是其双亲的第 m m m个子女( ( i − 1 ) × m + m + 1 (i-1)×m+m+1 (i−1)×m+m+1)时才有右兄弟
j m + 1 = ( j − 1 ) m + m + 1 jm+1=(j-1)m+m+1 jm+1=(j−1)m+m+1
j m = i − 1 jm=i-1 jm=i−1是 m m m的整数倍,此时结点 i i i是其双亲结点 j j j的第 m m m个子女结点
#define MAX_TREE_SIZE 100 //树中最多结点数
/*树的结点定义*/
typedef struct {
ElemType data; //数据元素值
int parent; //双亲位置域
}PTNode;
/*树的类型定义*/
typedef struct {
PTNode nodes[MAX_TREE_SIZE]; //双亲表示
int n; //结点数
}PTree;
/*孩子链表结点定义*/
typedef struct ChildNode {
int nodeID; //孩子在数组中的序号
struct ChildNode* next; //指向兄弟
}ChildNode, * ChildList;
/*树的结点定义*/
typedef struct {
ElemType data; //数据元素值
ChildList child; //孩子单链表
}CTNode;
/*树的类型定义*/
typedef struct {
CTNode nodes[MAX_TREE_SIZE]; //孩子表示
int n; //结点数
}CTree;
typedef struct CSNode {
ElemType data; //数据域
struct CSNode* firstchild, * nextsibling; //第一个孩子和右兄弟指针
}CSNode, * CSTree;
树 | 森林 | 二叉树 |
---|---|---|
先根遍历 | 先序遍历 | 先序遍历 |
后根遍历 | 中序遍历 | 中序遍历 |
对满二叉树按层序编号,对于编号为 i i i的结点:
(1)若有双亲,则其双亲为 ⌊ i / 2 ⌋ \lfloor i/2 \rfloor ⌊i/2⌋
(2)若有左孩子,则左孩子为 2 i 2i 2i
(3)若有右孩子,则右孩子为 2 i + 1 2i+1 2i+1
(1)若 i ≤ ⌊ n / 2 ⌋ i≤\lfloor n/2 \rfloor i≤⌊n/2⌋,则结点 i i i为分支结点,否则为叶子结点
(2)叶子结点只可能出现在层次最大的两层
(3)若有度为1的结点,则只可能有一个,且该结点只有左孩子而无右孩子
(4)按层序编号后,一旦出现某结点(编号为 i i i)为叶子结点或只有左孩子则编号大于 i i i的结点均为叶子结点
(5)若 n n n为偶数,则编号最大的分支结点(编号为 n / 2 n/2 n/2)只有左孩子,没有右孩子,其余分支结点左、右孩子都有。
若 n n n为奇数,则每个分支结点都有左孩子和右孩子
假 设 树 中 度 为 i ( i = 0 , 1 , ⋅ ⋅ ⋅ , 2 ) 的 结 点 数 为 n i 假设树中度为i(i=0,1,···,2)的结点数为n_i 假设树中度为i(i=0,1,⋅⋅⋅,2)的结点数为ni,总结点数为 n n n
总 结 点 数 = n 0 + n 1 + n 2 总结点数=n_0+n_1+n_2 总结点数=n0+n1+n2
总 分 支 数 = 0 × n 0 + 1 × n 1 + 2 × n 2 总分支数=0×n_0+1×n_1+2×n_2 总分支数=0×n0+1×n1+2×n2
总 结 点 数 = 总 分 支 数 + 1 总结点数=总分支数+1 总结点数=总分支数+1
i i i为偶数时,其双亲的编号为 i / 2 i/2 i/2,结点 i i i是双亲的左孩子
i i i为奇数时,其双亲的编号为 ( i − 1 ) / 2 (i-1)/2 (i−1)/2,结点 i i i是双亲的右孩子
2 h − 1 − 1 < n ≤ 2 h − 1 2^{h-1}-12h−1−1<n≤2h−1 或 2 h − 1 ≤ n < 2 h 2^{h-1}≤n<2^h 2h−1≤n<2h
n n n个结点共有 2 n 2n 2n个指针域,用了 n − 1 n-1 n−1个,还剩 2 n − ( n − 1 ) 2n-(n-1) 2n−(n−1)个空链域
typedef struct BitNode {
ElemType data; //数据域
struct BitNode* lchild, * rchild; //左、右孩子指针
}BitNode,*BiTree;
根结点 | 左子树 | 右子树 |
---|---|---|
N | L | R |
因为访问左右子树的先后顺序不变
不可能存在一个结点同时拥有左右孩子
void PreOrder(BiTree T) {
if (!T)
return; //如果二叉树为空,则什么也不做
visit(T); //访问根结点
PreOrder(T->lchild); //访问左子树
PreOrder(T->rchild); //访问右子树
}
void InOrder(BiTree T) {
if (!T)
return; //如果二叉树为空,则什么也不做
PreOrder(T->lchild); //访问左子树
visit(T); //访问根结点
PreOrder(T->rchild); //访问右子树
}
void PostOrder(BiTree T) {
if (!T)
return; //如果二叉树为空,则什么也不做
PreOrder(T->lchild); //访问左子树
PreOrder(T->rchild); //访问右子树
visit(T); //访问根结点
}
借助栈
void PreOrder(BiTree T) {
InitStack(S); //初始化栈
BitNode* p = T; //设置遍历指针p
if (!T)
return; //如果二叉树为空,则什么也不做
while (p || !IsEmpty(S)) { //栈不空或p不空时循环
if (p) {
visit(p); //栈顶元素出栈,访问出栈结点
/*一路向左*/
Push(S, p); //当前结点入栈
p = p->lchild; //左子树
}
else {
/*左孩子空,出栈*/
Pop(S, p); //转向出栈结点的右子树
p = p->rchild; //右子树
}
}
}
借助栈
void InOrder(BiTree T) {
InitStack(S); //初始化栈
BitNode* p = T; //设置遍历指针p
if (!T)
return; //如果二叉树为空,则什么也不做
while (p || !IsEmpty(S)) { //栈不空或p不空时循环
if (p) {
/*一路向左*/
Push(S, p); //当前结点入栈
p = p->lchild; //左子树
}
else {
/*左孩子空,出栈*/
Pop(S, p); //转向出栈结点的右子树
visit(p); //栈顶元素出栈,访问出栈结点
p = p->rchild; //右子树
}
}
}
借助栈
1° 沿着根的左孩子,依次入栈,直到左孩子为空;
2° 读栈顶元素:
若其右孩子不空且未被访问过,将右子树转执行1°
否则,转执行3°
3° 栈顶元素出栈并访问
void PostOrder(BiTree T) {
InitStack(S); //初始化栈
BitNode* p = T; //设置遍历指针p
BitNode* r = NULL; //辅助指针r,指向最近访问过的结点;或者在结点中增加一个标志域,记录是否已被访问
if (!T)
return; //如果二叉树为空,则什么也不做
while (p || !IsEmpty(S)) { //栈不空或p不空时循环
if (p) {
/*一路向左*/
Push(S, p); //当前结点入栈
p = p->lchild; //左子树
}
else {
GetTop(S, p); //读栈顶结点
if (p->rchild && p->rchild != r) { //若栈顶结点i的右子树存在,且未被访问过
p = p->rchild; //转向i的右孩子j
push(S, p); //将结点j压入栈
p = p->lchild; //转向结点j的左子树
}
else { //否则,弹出结点并访问
Pop(S, p); //此时栈顶结点i的左右子树都已分析完,将结点i出栈
visit(p); //访问结点i
r = p; //记录被访问的结点i
p = NULL; //结点访问完后,重置p指针
}
}
}
}
借助队列
void LevelOrder(BiTree T) {
InitQueue(Q); //初始化辅助队列
BitNode* p; //设置遍历指针p
if (!T)
return; //如果二叉树为空,则什么也不做
EnQueue(Q, T); //将根结点入队
while (!IsEmpty(Q)) { //队列不空则循环
DeQueue(Q, p); //队头结点出队
visit(p); //访问出队结点
if (p->lchild)
EnQueue(Q, p->lchild); //左子树不空,则左子树树根结点入队
if (p->rchild)
EnQueue(Q, p->rchild); //右子树不空,则右子树树根结点入队
}
}
可用于求二叉树的高度的【非递归算法】
层次遍历+队列+聪明的小脑瓜
1° 设置变量 l e v e l level level记录当前结点所在层数
2° 设置变量 l a s t last last指向当前层的最右结点(即保存队尾rear)
3° 每次层次遍历时与 l a s t last last指针比较,若两者相等,则 l e v e l + 1 level+1 level+1,并让 l a s t last last指向下一层的最右结点,直到遍历完成
----------------------------------------------------------------------------------------------------------------
求二叉树的高度的【递归算法】
1° 求左子树高度 l l e v e l llevel llevel
2° 求右子树高度 r l e v e l rlevel rlevel
3° m a x { l l e v e l , r l e v e l } + 1 max\{llevel,rlevel\}+1 max{llevel,rlevel}+1
+ 1 +1 +1是因为要算上根结点的高度
可用于判定完全二叉树
层次遍历+队列+聪明的小脑瓜
1° 将所有结点入队,包括空结点
2° 若遇到空结点入队,则出队查看其后是否有非空结点,若有则不是完全二叉树
(这个部分蛮有意思的,当时学的时候很喜欢做这部分的题,嘻嘻)
【注意】以下提到的根结点是相对于当前分析的树(子树)而言的
先序——确定根结点值(序列第1个)
中序——确定根结点位置
先序——确定根结点值(序列最后1个)
中序——确定根结点位置
层序——确定根结点值(序列第1个)
中序——确定根结点位置
引入线索二叉树的目的:
加快查找结点的前驱或后继的速度
线索二叉树特点:
n n n个结点共有链域指针 2 n 2n 2n个
除根结点外,每个结点都被一个指针指向(即树中有多少条边),共 n − 1 n-1 n−1个结点
剩余的链域建立线索,共 2 n − ( n − 1 ) 2n-(n-1) 2n−(n−1)个线索
/*线索链表*/
typedef struct ThreadNode {
ElemType data; //数据域
struct ThreadNode* lchild, * rchild; //左、右线索
int ltag, rtag; //左、右线索标志
}ThreadNode,* ThreadTree;
p指向当前正在访问的结点
pre指向刚刚访问过的结点,即pre指向p的前驱
/*p指向当前正在访问的结点*/
/*pre指向刚刚访问过的结点,即pre指向p的前驱*/
void PreTread(ThreadTree& p, ThreadTree& pre) {
if (!p)
return; //当前访问结点为空,无操作
if (!p->lchild) { //左子树为空
/*建立前驱线索*/
p->lchild = pre;
p->ltag = 1;
}
if (!pre && !pre->rchild) { //当前正在访问结点的前驱结点非空,且前驱结点没有右子树
/*建立前驱结点的后继线索*/
pre->rchild = p;
pre->rtag = 1;
}
pre = p; //标记当前结点成为刚刚访问过的结点
PreTread(p->lchild, pre); //递归,线索化左子树
PreTread(p->rchild, pre); //递归,线索化右子树
}
void CreatePreThread(ThreadTree T) {
ThreadTree pre = NULL;
if (!T)
return; //空二叉树,无操作
PreTread(T, pre); //线索化二叉树
/*处理遍历的最后一个结点*/
pre->rchild = NULL;
pre->rtag = 1;
}
/*求先序线索二叉树中结点p在先序序列下的后继*/
ThreadNode* NextNode(ThreadNode* p) {
if (p->ltag == 0)
return p->lchild; //ltag==0,直接返回左孩子
return p->rchild; //rtag==0,此时左孩子空,返回右孩子
//rtag==1,此时左孩子空,右孩子空,返回后继结点
}
/*不含头结点的先序线索二叉树的先序遍历*/
void Preorder(ThreadNode* T) {
for (ThreadNode* p = T; p; p = NextNode(p))
visit(p);
}
p指向当前正在访问的结点
pre指向刚刚访问过的结点,即pre指向p的前驱
/*中序遍历对二叉树线索化*/
void InTread(ThreadTree &p, ThreadTree &pre) {
if (!p)
return; //当前访问结点为空,无操作
InTread(p->lchild, pre); //递归,线索化左子树
if (!p->lchild) { //左子树为空
/*建立前驱线索*/
p->lchild = pre;
p->ltag = 1;
}
if (!pre && !pre->rchild) { //当前正在访问结点的前驱结点非空,且前驱结点没有右子树
/*建立前驱结点的后继线索*/
pre->rchild = p;
pre->rtag = 1;
}
pre = p; //标记当前结点成为刚刚访问过的结点
InTread(p->rchild, pre); //递归,线索化右子树
}
/*中序遍历建立中序线索二叉树*/
void CreateInThread(ThreadTree T) {
ThreadTree pre = NULL;
if (!T)
return; //空二叉树,无操作
InThread(T, pre); //线索化二叉树
/*处理遍历的最后一个结点*/
pre->rchild = NULL;
pre->rtag = 1;
}
可增加头结点 H e a d Head Head
/*类似于双向线索链表*/
Head -> lchild = 根结点;
Head -> rchild = 最后一个结点;
第一个结点 -> lchild = Head;
最后一个结点 -> rchild = Head;
/*求中序线索二叉树中中序序列下的第一个结点*/
ThreadNode* Firstnode(ThreadNode* p) {
while (p->ltag == 0)
p = p->lchild; //最左下结点(不一定是叶结点)
return p;
}
/*求中序线索二叉树中结点p在中序序列下的后继*/
ThreadNode* NextNode(ThreadNode* p) {
if (p->rtag == 0)
return Firstnode(p->rchild);
else
return p->rchild; //rtag==1,直接返回后继线索
}
/*不含头结点的中序线索二叉树的中序遍历*/
void Inorder(ThreadNode* T) {
for (ThreadNode* p = Firstnode(T); p; p = NextNode(p))
visit(p);
}
/*求中序线索二叉树中中序序列下的最后一个结点*/
ThreadNode* Finalnode(ThreadNode* p) {
while (p->rtag == 0)
p = p->rchild; //最右下结点(不一定是叶结点)
return p;
}
/*求中序线索二叉树中结点p在中序序列下的前驱*/
ThreadNode* FrontNode(ThreadNode* p) {
if (p->ltag == 0)
return Firstnode(p->lchild);
else
return p->lchild; //ltag==1,直接返回前驱线索
}
p指向当前正在访问的结点
pre指向刚刚访问过的结点,即pre指向p的前驱
/*p指向当前正在访问的结点*/
/*pre指向刚刚访问过的结点,即pre指向p的前驱*/
void PostTread(ThreadTree& p, ThreadTree& pre) {
if (!p)
return; //当前访问结点为空,无操作
PostTread(p->lchild, pre); //递归,线索化左子树
PoatTread(p->rchild, pre); //递归,线索化右子树
if (!p->lchild) { //左子树为空
/*建立前驱线索*/
p->lchild = pre;
p->ltag = 1;
}
if (!pre && !pre->rchild) { //当前正在访问结点的前驱结点非空,且前驱结点没有右子树
/*建立前驱结点的后继线索*/
pre->rchild = p;
pre->rtag = 1;
}
pre = p; //标记当前结点成为刚刚访问过的结点
}
void CreatePostThread(ThreadTree T) {
ThreadTree pre = NULL;
if (!T)
return; //空二叉树,无操作
PostThread(T, pre); //线索化二叉树
/*处理遍历的最后一个结点*/
pre->rchild = NULL;
pre->rtag = 1;
}
注意:需要栈的支持
因为结点 x x x的右孩子不一定为空
为了解决求后序后继的问题,需要采用带标志域的三叉链表
(1)双亲 *parent
(2)左孩子 *lchild
(3)右孩子 *rchild
由于根结点无兄弟,所以转换后的二叉树无右子树
Union(S, Root1, Root2); //将集合S中的子集Root2并入子集Root1
//要求:Root1与Root2不相交
Find(S, x); //查找集合S中单元素x所在的子集,并返回该子集的名字
Initial(S); //将集合S中的每个元素都初始化为只有一个单元素的子集
void Union(int S[], int Root1, int Root2) {
/*要求Root1和Root2是不同的,且表示子集合的名字*/
S[Root2] = Root1; //将根Root2连接到另一根Root1上面
}
int Find(int S[], int x) {
while (S[x] >= 0) //循环寻找x的根
x = S[x];
return x; //根的S[]小于0
}
void Initial(int S[]) { //S为并查集
for (int i = 0; i < size; i++)
S[i] = -1; //每个自成单元素集合
}
左 子 树 结 点 值 < 根 结 点 值 < 右 子 树 结 点 值 左子树结点值<根结点值<右子树结点值 左子树结点值<根结点值<右子树结点值
BSTNode* BST_Search(BiTree T, ElemType key) {
while (T != NULL && key != T->data) {
/*若树空或等于根节结点值,则结束循环*/
if (key < T->data)
T = T->lchild; //小于,则在左子树上查找
else
T = T->rchild; //大于,则在右子树上查找
}
}
查找效率主要取决于树的高度。
- 平衡二叉树 —— O ( l o g 2 n ) O({log}_2n) O(log2n)
- 单支树 —— O ( n ) O(n) O(n)
- 平均查找长度 A S L T = 层 数 × 该 层 结 点 数 总 结 点 数 {ASL}_T=\frac{层数×该层结点数}{总结点数} ASLT=总结点数层数×该层结点数
二叉排序树、二叉查找的关系
BSTNode* BST_Search(BiTree T, ElemType key) {
if (!T)
return NULL;
if (key == T->data)
return T;
if (key < T->data)
return BST_Search(T->lchild, key);
if (key > T->data)
return BST_Search(T->rchild, key);
}
int BST_Insert(BiTree& T, KeyType k) {
if (T == NULL) {
/*原树为空,新插入的记录为根结点*/
T = (BiTree)malloc(sizeof(BSTNode));
T->key = k;
T->lchild = NULL;
T->rchild = NULL;
return 1; //返回1,插入成功
}
else if (k == T->key)
return 0; //树中存在相同关键字的结点,插入失败
else if (k < T->key)
return BST_Insert(T->lchild, k); //插入到T的左子树
else
return BST_Insert(T->rchild, k); //插入到T的右子树
}
void Creat_BST(BITree& T, KeyType str[], int n) {
T = NULL; //初始化时T为空树
int i = 0;
while (i < n) {
/*依次将每个关键字插入到二叉排序树中*/
BST_Insert(T, str[i]);
i++;
}
}
在二叉树中删除并插入某结点,得到的二叉树不一定和原来的相同
假设以 n h n_h nh表示深度为 h h h的平衡树中含有的最少结点数
递推公式: n 0 = 0 , n 1 = 1 , n 2 = 2 , n h = n h − 1 + n h − 2 + 1 n_0=0,n_1=1,n_2=2,n_h=n_{h-1}+n_{h-2}+1 n0=0,n1=1,n2=2,nh=nh−1+nh−2+1
含有 n n n个结点的平衡二叉树的最大深度为 O ( l o g 2 n ) O({log}_2n) O(log2n),平均查找长度为 O ( l o g 2 n ) O({log}_2n) O(log2n)
w i w_i wi :第 i i i个叶结点所带的权值
l i l_i li :该叶结点到根结点的路径长度
给定 n n n个权值为 w 1 w_1 w1, w 2 w_2 w2, ⋅ ⋅ ⋅ ··· ⋅⋅⋅, w n w_n wn的结点
1° 将这 n n n个结点分别作为 n n n棵仅含一个结点的二叉树,构成森林 F F F;
2° 构造一个新结点,从 F F F中选取两棵根结点权值最小的树作为新结点的左、右子树,并且将新结点的权值置为左、右子树上根结点的权值之和;
3° 从 F F F中删除刚才选出的两棵树,同时将新得到的树加入 F F F中;
4° 重复步骤2°和3°,直到 F F F中只剩下一棵树为止。
n n n个叶结点 + + + n − 1 n-1 n−1个双分支结点
没有明确规定0、1对应左、右子树
左、右孩子结点的顺序是任意的
王道