Skip to content

Commit e1325aa

Browse files
committed
8.21 ds modify
1 parent 17b7a22 commit e1325aa

1 file changed

Lines changed: 368 additions & 10 deletions

File tree

docs/CS/data_structure/data_structure.md

Lines changed: 368 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -500,7 +500,7 @@ typedef struct {
500500
$$ h = \lceil \log_2(n) + 1 \rceil $$
501501
其中n为结点数。
502502

503-
### 5.3 二叉树的存储结构
503+
**二叉树的存储结构**
504504
!!! abstract "二叉树存储结构"
505505
二叉树的存储结构主要有两种:顺序存储和链式存储。
506506

@@ -515,20 +515,378 @@ typedef struct BiTNode {
515515
} BiTNode, *BiTree;
516516
````
517517

518+
!!! warning
519+
在含有n个结点的二叉链表中,含有n+1个空链域。
518520

521+
### 5.3 二叉树的遍历
522+
!!! abstract "二叉树遍历"
523+
二叉树的遍历是指按照一定的顺序访问二叉树的所有结点。
519524

520-
## ch6. 图
525+
!!! note "遍历方式"
526+
- **前序遍历**: 根结点 -> 左子树 -> 右
527+
- **中序遍历**: 左子树 -> 根结点 -> 右子树
528+
- **后序遍历**: 左子树 -> 右子树 -> 根
529+
- **层序遍历**: 按层次从上到下、从左到右访问结点
530+
> 前中后可以理解为根被遍历到的顺序
521531
522-
!!! todo "待完善内容"
523-
图的基本概念、存储结构、遍历算法、最小生成树、最短路径、拓扑排序、关键路径等
532+
> 对于手写遍历,想象从根节点从上到下、从左到右环绕树,然后前序中序后序分别在左中右三个位置有标记,按照碰到标记的顺序作为顺序。
524533
525-
## ch7. 查找
534+
前三种对应的递归算法如下:
535+
```c
536+
void PreOrder(BiTree T) {
537+
if (T != NULL) {
538+
visit(T); // 访问根结点
539+
PreOrder(T->lchild); // 递归访问左子树
540+
PreOrder(T->rchild); // 递归访问右子树
541+
}
542+
}
526543

527-
!!! todo "待完善内容"
528-
线性查找、二分查找、分块查找、B树、B+树、散列表等
544+
void InOrder(BiTree T) {
545+
if (T != NULL) {
546+
InOrder(T->lchild); // 递归访问左子树
547+
visit(T); // 访问根结点
548+
InOrder(T->rchild); // 递归访问右子树
549+
}
550+
}
529551

530-
## ch8. 排序
552+
void PostOrder(BiTree T) {
553+
if (T != NULL) {
554+
PostOrder(T->lchild); // 递归访问左子树
555+
PostOrder(T->rchild); // 递归访问右子树
556+
visit(T); // 访问根结点
557+
}
558+
}
559+
````
560+
561+
前三种对应的非递归算法如下:
562+
```c
563+
todo "非递归遍历"
564+
```
565+
566+
**层序遍历**:
567+
```c
568+
void LevelOrder(BiTree T) {
569+
InitQueue(Q); // 初始化队列
570+
BiTree p;
571+
EnQueue(Q, T); // 将根结点入队
572+
while (!QueueEmpty(Q)) {
573+
DeQueue(Q, &p); // 出队一个结点
574+
visit(p); // 访问该结点
575+
if (p->lchild != NULL) EnQueue(Q, p->lchild);
576+
if (p->rchild != NULL) EnQueue(Q, p->rchild);
577+
}
578+
}
579+
```
580+
581+
**由遍历序列构造二叉树**
582+
若已知中序序列,再给出其他三种遍历序列的其中一种,就可以唯一确定一棵二叉树。
583+
584+
585+
* 前序 + 中序
586+
每次从前序找到根结点,然后在中序中找到根结点的位置,将中序分为左子树和右子树,递归构造。
587+
588+
589+
```c
590+
BiTree CreateTree_PreIn(SString pre, SString in) {
591+
if (pre.length == 0 || in.length == 0) return NULL;
592+
BiTree T = (BiTree)malloc(sizeof(BiTNode));
593+
T->data = pre.ch[0]; // 前序的第一个字符是根结点
594+
int pos = LocateElem(in, T->data); // 在中序中找到根结点位置
595+
// 构造左子树:
596+
// 前序序列中,根结点后面的 pos 个元素对应左子树的前序序列(即 pre[1..pos]),
597+
// 中序序列中,根结点左侧的 pos 个元素对应左子树的中序序列(即 in[0..pos-1])
598+
T->lchild = CreateTree_PreIn(SubString(pre, 1, pos), SubString(in, 0, pos - 1));
599+
600+
// 构造右子树:
601+
// 前序序列中,左子树之后的剩余元素对应右子树的前序序列(即 pre[pos+1..end]),
602+
// 中序序列中,根结点右侧的剩余元素对应右子树的中序序列(即 in[pos+1..end])
603+
T->rchild = CreateTree_PreIn(SubString(pre, pos + 1), SubString(in, pos + 1));
604+
605+
return T;
606+
}
607+
```
608+
609+
610+
* 后序 + 中序
611+
每次从后序找到根结点,然后在中序中找到根结点的位置,将中序分为左子树和右子树,递归构造。
612+
613+
```c
614+
BiTree CreateTree_PostIn(SString post, SString in) {
615+
if (post.length == 0 || in.length == 0) return NULL;
616+
BiTree T = (BiTree)malloc(sizeof(BiTNode));
617+
T->data = post.ch[post.length - 1]; // 后序的
618+
int pos = LocateElem(in, T->data); // 在中序中找到根结点位置
619+
// 构造左子树:
620+
// 后序序列中,根结点前面的 pos 个元素对应左子树的后序序列(即 post[0..pos-1]),
621+
// 中序序列中,根结点左侧的 pos 个元素对应左子树的中序序列(即 in[0..pos-1])
622+
T->lchild = CreateTree_PostIn(SubString(post, 0, pos - 1), SubString(in, 0, pos - 1));
623+
// 构造右子树:
624+
// 后序序列中,左子树之后的剩余元素对应右子树的后序序列(即 post[pos..end-1]),
625+
// 中序列中,根结点右侧的剩余元素对应右子树的中序序列(即 in[pos+1..end])
626+
T->rchild = CreateTree_PostIn(SubString(post, pos, post.length - 2), SubString(in, pos + 1));
627+
return T;
628+
}
629+
```
630+
631+
632+
* 层序 + 中序
633+
每次从层序找到根结点,然后在中序中找到根结点的位置,将中序分为左子树和右子树,递归构造。
634+
```c
635+
BiTree CreateTree_LevelIn(SString level, SString in) {
636+
if (level.length == 0 || in.length == 0) return NULL;
637+
BiTree T = (BiTree)malloc(sizeof(BiTNode));
638+
T->data = level.ch[0]; // 层序的第一个字符
639+
int pos = LocateElem(in, T->data); // 在中序中找到根结点位置
640+
// 构造左子树:
641+
// 中序序列中,根结点左侧的 pos 个元素对应左子树的中序序列(即 in[0..pos-1])
642+
SString leftIn = SubString(in, 0, pos - 1);
643+
// 从层序中筛选出左子树的结点
644+
SString leftLevel = FilterLevel(level, leftIn);
645+
T->lchild = CreateTree_LevelIn(leftLevel, leftIn);
646+
// 构造右子树:
647+
// 中序序列中,根结点右侧的剩余元素对应右子树的中序序列(即 in[pos+1..end])
648+
SString rightIn = SubString(in, pos + 1);
649+
// 从层序中筛选出右子树的结点
650+
SString rightLevel = FilterLevel(level, rightIn);
651+
T->rchild = CreateTree_LevelIn(rightLevel, rightIn);
652+
return T;
653+
}
654+
```
655+
656+
657+
#### 线索二叉树
658+
659+
!!! abstract "线索二叉树的概念"
660+
利用二叉树中的空指针域,存放指向该结点在某种遍历次序下的前驱和后继结点的指针。
661+
662+
**基本思想:** 在含有n个结点的二叉树中,有n+1个空指针域,可以利用这些空指针存放前驱或后继的线索。
663+
664+
**结点结构:**
665+
```c
666+
typedef struct ThreadNode {
667+
ElemType data; // 数据域
668+
struct ThreadNode *lchild, *rchild; // 左右孩子指针
669+
int ltag, rtag; // 左右线索标志
670+
} ThreadNode, *ThreadTree;
671+
```
672+
673+
**标志位说明:**
674+
- `ltag = 0`:lchild指向左孩子;`ltag = 1`:lchild指向前驱
675+
- `rtag = 0`:rchild指向右孩子;`rtag = 1`:rchild指向后继
676+
677+
##### 中序线索二叉树
678+
679+
**构造过程:**
680+
```c
681+
// 中序线索化递归函数
682+
void InThread(ThreadTree &p, ThreadTree &pre) {
683+
if (p != NULL) {
684+
InThread(p->lchild, pre); // 递归线索化左子树
685+
686+
if (p->lchild == NULL) { // 左子树为空,建立前驱线索
687+
p->ltag = 1;
688+
p->lchild = pre;
689+
}
690+
if (pre != NULL && pre->rchild == NULL) { // 建立后继线索
691+
pre->rtag = 1;
692+
pre->rchild = p;
693+
}
694+
pre = p; // 更新前驱结点
695+
696+
InThread(p->rchild, pre); // 递归线索化右子树
697+
}
698+
}
699+
700+
// 主函数
701+
void CreateInThread(ThreadTree &T) {
702+
ThreadTree pre = NULL;
703+
if (T != NULL) {
704+
InThread(T, pre);
705+
if (pre->rchild == NULL) { // 处理最后一个结点
706+
pre->rtag = 1;
707+
}
708+
}
709+
}
710+
```
711+
712+
**遍历操作:**
713+
```c
714+
// 找到中序遍历的第一个结点
715+
ThreadNode *Firstnode(ThreadNode *p) {
716+
while (p->ltag == 0) // 沿左孩子走到底
717+
p = p->lchild;
718+
return p;
719+
}
720+
721+
// 找到中序遍历的后继结点
722+
ThreadNode *Nextnode(ThreadNode *p) {
723+
if (p->rtag == 0) // 有右子树
724+
return Firstnode(p->rchild); // 右子树的最左结点
725+
else
726+
return p->rchild; // 直接后继
727+
}
728+
729+
// 中序遍历线索二叉树
730+
void InOrder_Thread(ThreadTree T) {
731+
for (ThreadNode *p = Firstnode(T); p != NULL; p = Nextnode(p))
732+
visit(p);
733+
}
734+
```
531735
532-
!!! todo "待完善内容"
533-
插入排序、选择排序、交换排序、归并排序、基数排序、外部排序等
736+
##### 其他线索二叉树
534737
738+
!!! note "先序和后序线索化特点"
739+
740+
**先序线索二叉树:**
741+
- 后继查找:有左孩子则后继为左孩子,否则为右孩子,叶结点的后继由线索指出
742+
- 前驱查找:较复杂,通常在线索化过程中处理
743+
744+
**后序线索二叉树:**
745+
- 后继查找规则:
746+
1. 若为根结点,后继为空
747+
2. 若为双亲的右孩子,或为双亲的左孩子且双亲无右子树,后继为双亲
748+
3. 若为双亲的左孩子且双亲有右子树,后继为双亲右子树中后序遍历的第一个结点
749+
750+
### 5.4 树和森林
751+
752+
#### 树的存储结构
753+
754+
**双亲表示法:**
755+
```c
756+
typedef struct {
757+
ElemType data;
758+
int parent; // 双亲位置
759+
} PTNode;
760+
761+
typedef struct {
762+
PTNode nodes[MAXSIZE];
763+
int n; // 结点数
764+
} PTree;
765+
```
766+
767+
**孩子表示法:**
768+
```c
769+
typedef struct CNode {
770+
int child; // 孩子结点在数组中的位置
771+
struct CNode *next;
772+
} CNode;
773+
774+
typedef struct {
775+
ElemType data;
776+
CNode *firstchild; // 第一个孩子
777+
} CTBox;
778+
779+
typedef struct {
780+
CTBox nodes[MAXSIZE];
781+
int n, r; // 结点数和根的位置
782+
} CTree;
783+
```
784+
785+
**孩子兄弟表示法(二叉树表示法):**
786+
```c
787+
typedef struct CSNode {
788+
ElemType data;
789+
struct CSNode *firstchild, *nextsibling; // 第一个孩子和右兄弟
790+
} CSNode, *CSTree;
791+
```
792+
793+
#### 树、森林与二叉树的转换
794+
795+
!!! tip "转换规则"
796+
797+
**树转二叉树:**
798+
1. 加线:在所有兄弟结点之间加一条连线
799+
2. 去线:树中每个结点,只保留与第一个孩子结点的连线,删除与其他孩子的连线
800+
3. 层次调整:以树的根结点为轴心,将整棵树顺时针旋转一定角度
801+
802+
**森林转二叉树:**
803+
1. 把每棵树转换为二叉树
804+
2. 第一棵二叉树不动,其余二叉树依次作为前一棵二叉树根结点的右子树
805+
806+
**二叉树转树或森林:**
807+
1. 若二叉树非空,则根结点的右子树为森林,左子树为第一棵树
808+
2. 按照树转二叉树的逆过程进行
809+
810+
#### 树和森林的遍历
811+
812+
**树的遍历:**
813+
- **先根遍历**:先访问根结点,再依次遍历各子树
814+
- **后根遍历**:先依次遍历各子树,再访问根结点
815+
816+
**森林的遍历:**
817+
- **先序遍历**:依次对森林中每棵树进行先根遍历
818+
- **中序遍历**:依次对森林中每棵树进行后根遍历
819+
820+
!!! important "遍历等价性"
821+
- 树的先根遍历 ≡ 对应二叉树的先序遍历
822+
- 树的后根遍历 ≡ 对应二叉树的中序遍历
823+
- 森林的先序遍历 ≡ 对应二叉树的先序遍历
824+
- 森林的中序遍历 ≡ 对应二叉树的中序遍历
825+
826+
### 5.5 哈夫曼树和哈夫曼编码
827+
828+
!!! abstract "基本概念"
829+
- **路径长度**:从树中一个结点到另一个结点之间的分支数目
830+
- **结点的权**:给每个结点赋予的一个有意义的数值
831+
- **结点的带权路径长度**:从根结点到该结点之间的路径长度与该结点权的乘积
832+
- **树的带权路径长度(WPL)**:树中所有叶结点的带权路径长度之和
833+
834+
**哈夫曼树(最优二叉树):** 在含有n个带权叶结点的二叉树中,带权路径长度最小的二叉树。
835+
836+
#### 哈夫曼树的构造
837+
838+
!!! example "哈夫曼算法"
839+
1. 初始化:由给定的n个权值构成n棵只有根结点的二叉树,得到森林F
840+
2. 选取和合并:在F中选取两棵根结点权值最小的树作为左右子树,构造一棵新的二叉树,新树根结点权值为左右子树权值之和
841+
3. 删除和加入:从F中删除被选取的两棵树,把新构造的树加入F中
842+
4. 重复步骤2-3,直到F中只剩一棵树为止
843+
844+
**算法实现:**
845+
```c
846+
typedef struct {
847+
int weight; // 权值
848+
int parent, lch, rch; // 双亲、左孩子、右孩子下标
849+
} HTNode, *HuffmanTree;
850+
851+
void CreateHuffmanTree(HuffmanTree &HT, int *w, int n) {
852+
if (n <= 1) return;
853+
int m = 2 * n - 1; // 哈夫曼树总结点数
854+
HT = new HTNode[m + 1]; // 0号单元未用
855+
856+
// 初始化
857+
for (int i = 1; i <= m; ++i) {
858+
HT[i].parent = HT[i].lch = HT[i].rch = 0;
859+
}
860+
for (int i = 1; i <= n; ++i) {
861+
HT[i].weight = w[i-1];
862+
}
863+
864+
// 构造哈夫曼树
865+
for (int i = n + 1; i <= m; ++i) {
866+
int s1, s2;
867+
Select(HT, i - 1, &s1, &s2); // 选择权值最小的两个结点
868+
HT[s1].parent = HT[s2].parent = i;
869+
HT[i].lch = s1; HT[i].rch = s2;
870+
HT[i].weight = HT[s1].weight + HT[s2].weight;
871+
}
872+
}
873+
```
874+
875+
#### 哈夫曼编码
876+
877+
**前缀编码:** 任何一个字符的编码都不是另一个字符编码的前缀。
878+
879+
!!! tip "哈夫曼编码特点"
880+
- 哈夫曼编码是前缀编码
881+
- 哈夫曼编码是最优前缀编码,即平均编码长度最短
882+
- 编码过程:从叶结点到根结点逆向求编码
883+
- 译码过程:从根结点开始,逐位读入编码,到达叶结点输出字符
884+
885+
## ch6. 图
886+
todo "待补充内容"
887+
888+
## ch7. 查找
889+
todo "待补充内容"
890+
891+
## ch8. 排序
892+
todo "待补充内容"

0 commit comments

Comments
 (0)