【学习笔记】数据结构(六 ①)
树和二叉树 (一)
文章目录
- 树和二叉树 (一)
- 6.1 树(Tree)的定义和基本术语
- 6.2 二叉树
- 6.2.1 二叉树的定义
- 1、斜树
- 2、满二叉树
- 3、完全二叉树
- 4、二叉排序树
- 5、平衡二叉树(AVL树)
- 6、红黑树
- 6.2.2 二叉树的性质
- 6.2.3 二叉树的存储结构
- 6.3 遍历二叉树和线索二叉树
- 6.3.1 遍历二叉树
6.1 树(Tree)的定义和基本术语
树(Tree)是n(n≥0)个结点的有限集。
在任意一棵非空树中:
(1) 有且仅有一个特定的称为**根(Root)**的结点;
(2) 当n>1时,其余结点可分为m(m>0)个互不相交的有限集T1,T2,…,Tm,其中每一个集合本身又是一棵树,并且
称为根的子树(SubTree)。
树的特点:
- 树的根结点没有前驱,除根结点外的所有结点有且只有一个前驱。
- 树中所有结点可以有零个或多个后继。
- 树中的结点数等于所有结点的度数加1.
度为m的树中第i层上至多有mi-1个结点(i > = 1)
高度为h 的m叉树至多有( mh − 1 ) / ( m − 1 )个结点。
具有n个结点的m叉树的最小高度为[ logm ( n ( m − 1 ) + 1 ) ] 。树的其他表示形式:
( a ) 是以嵌套集合(即是一些集合的集体,对于其中任何两个集合,或者不相交,或者一个包含另一个)的形式表示的;
( b ) 是以广义表的形式表示的,根作为由子树森林组成的表的名字写在表的左边;
( c ) 用的是凹人表示法(类似书的编目)。
👉 基本术语:
-
结点的祖先是从根到该结点所经分支上的所有结点。根A到结点K的唯一路径上的任意结点,称为结点K的祖先—如结点B是结点K的祖先
-
以某结点为根的子树中的任一结点都称为该结点的子孙。结点K是结点B的子孙
-
结点的子树的根称为该 结点的孩子(Child), 相应地,该结点称为孩子的**双亲(Parent) ** 。最接近结点K的结点E称为K的双亲,而K为结点E的孩子。根A是树中唯一没有双亲的结点。
-
同一个双亲的孩子 之间互称兄弟 (Sibling) 。如结点K和结点L有相同的双亲E,即K和L为兄弟。
-
其双亲在同一层的结点互为堂兄弟。如结点G与E,F,H,I,J互为堂兄弟。
-
树的结点包含一个数据元素及若干指向其子树的分支。结点拥有的子树数(树中一个结点的孩子个数)称为结点的度 (Degree) 。如结点B的度为2,结点D的度为3
-
树的度是树内各结点的度的最大值。树的度为3。
-
度为0的结点 称为叶子 (Leaf) 或终端结点。
-
度不为0的结点称为非终端结点或分支结点。除根结点之外,分支结点也称为内部结点。
-
结点的层次 (Level) 从根开始定义起.根为第一层,根的孩子为第二层。
-
树中结点的最大层次称为树的深度 (Depth) 或高度。图中树的高度为4。
结点的深度是从根结点开始自顶向下逐层累加的。
结点的高度是从叶结点开始自底向上逐层累加的。 -
如果将树中结点的各子树看成从左至右是有次序的(即不能互换),则称该树为有序树,否则称为无序树。在有序树中最左边的子树的根称为第一个孩子,最右边的称为最后一个孩子。
-
森林 (Forest) m(m≥0) 棵互不相交的树的集合。对树中每个结点而言,其子树的集合即为森林。
任何一棵树是一个二元组 Tree= (root, F), 其中: root 是数据元素、 称做树的根结点; m(m≥0) 棵树的森林, F=(T1, T2, …, Tm), 其中 Ti = (ri, Fi) 称做 root 的第i棵子树;当m≠0时,在树根和其子树森林之间存在下列关系:RF = { < root , 78ri > I i = 1 , 2, …, m ,m>0 }
6.2 二叉树
6.2.1 二叉树的定义
二叉树 (Binary Tree) 的特点是每个结点至多只有两棵子树 (即二叉树中不存在度大于2的结点),并且二叉树的子树有左右之分,其次序不能任意颠倒。[二叉树或为空树;或是由一个根结点加上两棵分别称为左子树和右子树的、互不交的二叉树组成]
🌟特殊的二叉树
1、斜树
- 所有的结点都只有左子树的二叉树叫左斜树。所有结点都是只有右子树的二叉树叫右斜树。这两者统称为斜树。
2、满二叉树
- 一棵高度为h,且含有2h−1个结点的二叉树称为满二叉树,即树中的每层都含有最多的结点。除叶子结点之外的每个结点度数均为2。
- 可以对满二叉树按层序编号:约定编号从根结点(根结点编号为1)起,自上而下,自左向右。这样,每个结点对应一个编号,对于编号为i的结点,若有双亲,则其双亲为i / 2, 若有左孩子,则左孩子为2i ; 若有右孩子,则右孩子为2i + 1。
3、完全二叉树
- 若设二叉树的深度为h,除第 h 层外,其它各层 (1~h-1) 的结点数都达到最大个数,第 h 层所有的结点都连续集中在最左边。
4、二叉排序树
- 二叉排序树(Binary Sort Tree),又称二叉查找树(Binary Search Tree),亦称二叉搜索树。
- 若它的左子树不空,则左子树上所有结点的值均小于它根结点的值。
- 若它的右子树不空,则右子树上所有结点的值均大于它根结点的值。
- 它的左、右子树都满足为⼆叉排序树
#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>
#include <stdlib.h>
#include<stdio.h>
#include<stdlib.h>
//二叉排序树节点存储方式
typedef int DataType;
typedef struct BTnode {
DataType data; //数据域
struct BTnode* left; //左指针
struct BTnode* right; //右指针
}BTnode, * BTree;
//插入结点
void Insert_node(BTree* root, DataType data) {
if (*root == NULL) {
*root = (BTnode*)malloc(sizeof(BTnode));
if (!*root) {
printf("ERROR\n");
exit(-1);
}
(*root)->data = data;
(*root)->left = NULL;
(*root)->right = NULL;
}
else if ((*root)->data <= data)
Insert_node(&(*root)->right, data);
else if ((*root)->data > data)
Insert_node(&(*root)->left, data);
}
//创建排序二叉树
void Create_sortBtree(BTree* T, DataType* arr, int size) {
if (!arr)
return NULL;
else {
for (int i = 0; i < size; i++) {
Insert_node(T, arr[i]);
}
}
}
//中序遍历排序二叉树
void mid_travel(BTree T)
{
if (!T)
return;
mid_travel(T->left);
printf("%d ", T->data);
mid_travel(T->right);
}
//递归查找
BTnode* Btree_search(BTree root, DataType target) {
if (!root)
return NULL;
if (target == root->data) {
return root;
}
return target > root->data ? Btree_search(root->right, target) : Btree_search(root->left, target);
}
//非递归查找
BTnode* Btree_search_fa(BTree root, DataType target) {
BTnode* p = root;
while (p) {
if (p->data == target)
{
return p;
}
p = target > p->data ? p->right : p->left;
}
return NULL;
}
//获取最大值
int Btree_max(BTree T) {
BTnode* cur = T;
while (cur->right) {
cur = cur->right;
}
return cur->data;
}
//获取最小值
int Btree_min(BTree T) {
BTnode* cur = T;
while (cur->left) {
cur = cur->left;
}
return cur->data;
}
//删除结点
void Del_Node(BTree* T, int val)
{
if (T == NULL) return ;
if ((*T)->data < val)
{
Del_Node(&((*T)->right), val);
}
else if ((*T)->data > val)
{
Del_Node(&((*T)->left), val);
}
else
{
//叶子结点
if ((*T)->left == NULL && (*T)->right == NULL)
{
*T = NULL;
return;
}
//没有左孩子,只有右孩子
if ((*T)->left == NULL && (*T)->right != NULL)
{
*T = (*T)->right;
return ;
}
//没有右孩子,只有左孩子
if ((*T)->left != NULL && (*T)->right == NULL)
{
*T = (*T)->left;
return ;
}
//左右子树都有
if ((*T)->left != NULL && (*T)->right != NULL)
{
BTree cur = (*T)->left;
while (cur->right != NULL)
{
cur = cur->right;
}
cur->right = (*T)->right;
*T = (*T)->left;
return ;
}
}
}
int main()
{
BTree* root = NULL;
DataType w[9] = { 8, 3, 10, 1, 6, 14, 4, 7, 13 };
Create_sortBtree(&root, w, 9);
mid_travel(root);
printf("\n");
printf("递归查找结点:");
BTnode* node1 = Btree_search(root, 6);
printf("%d, 左子结点 %d, 右子节点 %d\n", node1->data, node1->left->data, node1->right->data);
printf("非递归查找结点:");
BTnode* node2 = Btree_search_fa(root, 6);
printf("%d, 左子结点 %d, 右子节点 %d\n", node2->data, node2->left->data, node2->right->data);
printf("该树最大值:%d\n", Btree_max(root));
printf("该树最小值:%d\n", Btree_min(root));
/*删除叶子结点 4 */
Del_Node(&root, 4);
mid_travel(root);
printf("\n");
/*删除只有右子树,没有左子树结点 10 */
Del_Node(&root, 10);
mid_travel(root);
printf("\n");
/*删除只有左子树,没有右子树结点 14 */
Del_Node(&root, 14);
mid_travel(root);
printf("\n");
/*删除有左子树和右子树结点 8 */
Del_Node(&root, 8);
mid_travel(root);
printf("\n");
return 0;
}
5、平衡二叉树(AVL树)
平衡二叉树也叫自平衡二叉搜索树(Self-Balancing Binary Search Tree),所以其本质也是一颗二叉搜索树,树上任一结点的左子树和右子树的深度/高度差不超过1。左右子树的高度差称之为平衡因子。
自平衡 - 旋转操作
触发时机:当添加一个节点之后,该树不再是一颗平衡二叉树
-
左旋
-
右旋
-
单旋转
-
LL - 左左 一次右旋
当不平衡节点的左子树的左子树有节点插入,导致二叉树不平衡
-
RR - 右右 一次左旋
当不平衡节点的右子树的右子树有节点插入,导致二叉树不平衡
-
-
双旋转
-
LR - 左右 先局部左旋,再整体右旋
当不平衡节点的左子树的右子树有节点插入,导致二叉树不平衡
-
RL - 右左 先局部右旋,再整体左旋
当不平衡节点的右子树的左子树有节点插入,导致二叉树不平衡
-
# include <stdio.h>
# include <stdbool.h>
# include <stdlib.h>
# include <math.h>
typedef struct TreeNode { // 直接定义结构体类型
int data; // 数据节点
struct TreeNode* left; // 指向左子树,现在可以使用 TreeNode
struct TreeNode* right; // 指向右子树,现在可以使用 TreeNode
} TreeNode, * PTreeNode; // 注意这里去掉了*号,因为TreeNode现在已经是结构体类型
// 记录平衡二叉树
bool BalanceTrue = false;
// 最小不平衡子树地址
TreeNode* rjt = NULL;
// 检查二叉树是否平衡,若不平衡 BalanceTrue 为 true
int checkTreeBalance(TreeNode* root) {
if (NULL == root) { return 0; }
int x = checkTreeBalance(root->left);
int y = checkTreeBalance(root->right);
// 若检测到最小二叉树后,不进行后面的检查
if (BalanceTrue) return 0;
int xx = abs(x - y);
if (xx > 1) {
// 左子树 和 右子树 相差大于1 , 二叉树不平衡
BalanceTrue = true;
rjt = root;
}
return (x > y ? x + 1 : y + 1);
}
// 返回二叉树树高
int treeHeight(TreeNode* root) {
if (NULL == root) return 0;
int ll = treeHeight(root->left);
int rr = treeHeight(root->right);
return (ll > rr ? ll + 1 : rr + 1);
}
// 父节点查询
TreeNode* queryTopData(TreeNode* root, int data) {
// 空地址异常抛出
if (NULL == root) return NULL;
// top: 父节点 ,如果为Null, 该节点为父节点
// tmp: 遍历查询节点
TreeNode* top = NULL;
TreeNode* tmp = root;
while (tmp != NULL) {
if (data == tmp->data) {
// 节点为 返回Null
if (NULL == top) return NULL;
return top;
}
top = tmp;
if (data > tmp->data) {
tmp = tmp->right;
}
else if (data < tmp->data) {
tmp = tmp->left;
}
}
return NULL;
}
// 左左旋转 - 右旋
bool turnLL(TreeNode** root, TreeNode* notBalanceRoot) {
if ((*root) != notBalanceRoot) {
printf("左左旋转,非根节点\n");
// 非根节点
TreeNode* lleft = notBalanceRoot->left;
TreeNode* lright = lleft->right;
// 查找父节点
TreeNode* fdata = queryTopData((*root), notBalanceRoot->data);
if (NULL == fdata) return false;
lleft->right = notBalanceRoot;
notBalanceRoot->left = lright;
if (notBalanceRoot == fdata->left) {
fdata->left = lleft;
}
else if (notBalanceRoot == fdata->right) {
fdata->right = lleft;
}
return true;
}
else {
// 根节点
printf("左左旋转,是根节点\n");
TreeNode* lleft = notBalanceRoot->left;
TreeNode* absroot = lleft;
TreeNode* lright = lleft->right;
lleft->right = notBalanceRoot;
notBalanceRoot->left = lright;
(*root) = absroot;
return true;
}
}
// 左右旋转
bool turnLR(TreeNode** root, TreeNode* notBalanceRoot) {
if ((*root) != notBalanceRoot) {
printf("左右旋转,非根节点");
TreeNode* lleft = notBalanceRoot->left;
TreeNode* leftRight = lleft->right;
TreeNode* leftRightLeft = leftRight->left;
// 第一次调整
leftRight->left = lleft;
lleft->right = leftRightLeft;
notBalanceRoot->left = leftRight;
// 查找父节点
TreeNode* fdata = queryTopData((*root), notBalanceRoot->data);
//if (NULL != fdata) printf("fdata: %d\n",fdata->data);
// 第二次调整
lleft = notBalanceRoot->left;
leftRight = lleft->right;
lleft->right = notBalanceRoot;
notBalanceRoot->left = leftRight;
if (notBalanceRoot == fdata->left) {
fdata->left = lleft;
}
else if (notBalanceRoot == fdata->right) {
fdata->right = lleft;
}
}
else {
printf("左右旋转,是根节点\n");
TreeNode* lleft = notBalanceRoot->left;
TreeNode* leftRight = lleft->right;
TreeNode* leftRightLeft = leftRight->left;
// 第一次调整
leftRight->left = lleft;
lleft->right = leftRightLeft;
notBalanceRoot->left = leftRight;
// 第二次调整
lleft = notBalanceRoot->left;
leftRight = lleft->right;
lleft->right = notBalanceRoot;
notBalanceRoot->left = leftRight;
(*root) = lleft;
}
}
// 右左旋转
bool turnRL(TreeNode** root, TreeNode* notBalanceRoot) {
TreeNode* rright = notBalanceRoot->right;
TreeNode* rightLeft = rright->left;
TreeNode* rightLeftRight = rightLeft->right;
// 第一次调整
rightLeft->right = rright;
rright->left = rightLeftRight;
notBalanceRoot->right = rightLeft;
// 查找父节点
TreeNode* fdata = queryTopData((*root), notBalanceRoot->data);
//if (NULL != fdata) printf("fdata: %d\n",fdata->data);
// 第二次调整
rright = notBalanceRoot->right;
rightLeft = rright->left;
rright->left = notBalanceRoot;
notBalanceRoot->right = rightLeft;
if ((*root) != notBalanceRoot) {
printf("右左旋转,非根节点\n");
if (notBalanceRoot == fdata->left) {
fdata->left = rright;
}
else if (notBalanceRoot == fdata->right) {
fdata->right = rright;
}
}
else {
printf("右左旋转,是根节点\n");
(*root) = rright;
}
}
// 右右旋转
bool turnRR(TreeNode** root, TreeNode* notBalanceRoot) {
if ((*root) != notBalanceRoot) {
printf("右右旋转,非根节点");
TreeNode* rright = notBalanceRoot->right;
TreeNode* rleft = rright->left;
// 查找父节点
TreeNode* fdata = queryTopData((*root), notBalanceRoot->data);
if (NULL != fdata) printf("fdata: %d\n", fdata->data);
rright->left = notBalanceRoot;
notBalanceRoot->right = rleft;
if (notBalanceRoot == fdata->left) {
fdata->left = rright;
}
else if (notBalanceRoot == fdata->right) {
fdata->right = rright;
}
}
else {
// 右右旋转,是根节点
printf("右右旋转,是根节点\n");
TreeNode* rright = notBalanceRoot->right;
TreeNode* absroot = rright;
TreeNode* rleft = rright->left;
rright->left = notBalanceRoot;
notBalanceRoot->right = rleft;
(*root) = absroot;
}
}
// 二叉树前序遍历
void Print1(TreeNode* root) {
if (NULL == root) return;
printf("%d\t", root->data);
Print1(root->left);
Print1(root->right);
}
// 二叉树中序遍历
void Print2(TreeNode* root) {
if (NULL == root) return;
Print2(root->left);
printf("%d\t", root->data);
Print2(root->right);
}
// 二叉树后续遍历
void Print3(TreeNode* root) {
if (NULL == root) return;
Print3(root->left);
Print3(root->right);
printf("%d\t", root->data);
}
// 插入二叉树节点
TreeNode* addNode(TreeNode* root, int data) {
if (NULL == root) {
// 头节点插入
TreeNode* Node = (TreeNode*)malloc(sizeof(TreeNode));
if (NULL == Node) {
printf("新节点申请内存失败\n");
return NULL;
}
Node->data = data;
Node->left = NULL;
Node->right = NULL;
return Node;
}
TreeNode* tmp = root;
TreeNode* top = NULL;
// 找到合适的最尾巴节点
while (NULL != tmp) {
top = tmp;
if (tmp->data == data) {
printf("已经存在该节点,节点地址: %p\n", tmp);
return root;
}
if (tmp->data < data) {
tmp = tmp->right;
}
else if (tmp->data > data) {
tmp = tmp->left;
}
}
TreeNode* Node = (TreeNode*)malloc(sizeof(TreeNode));
if (NULL == Node) {
printf("申请新节点内存失败\n");
return root;
}
Node->data = data;
Node->left = NULL;
Node->right = NULL;
// 链接节点
if (data > top->data) {
top->right = Node;
}
else if (data < top->data) {
top->left = Node;
}
return root;
}
// 删除二叉排序树节点
bool DeleteTreeNode(TreeNode** TreeRoot, int data) {
if (NULL == (*TreeRoot)) return false;
printf("删除节点: %d\n", data);
TreeNode* tmp = (*TreeRoot);
TreeNode* top = NULL;
while (tmp != NULL) {
if (tmp->data == data) {
// 叶子节点
if ((NULL == tmp->left) && (NULL == tmp->right)) {
// 叶子节点
if (NULL == top) {
// 仅有根节点的叶子节点
free(tmp);
return true;
}
else {
// 其他的叶子节点
TreeNode* lastNode = top;
if (tmp == lastNode->left) {
lastNode->left = NULL;
}
else if (tmp == lastNode->right) {
lastNode->right = NULL;
}
free(tmp);
return true;
}
}
else {
// 非叶子节点
// 算法为:
// 默认算法为: 1. 当删除该节点时,获取该树右子树最左子节点
// 2. 当右子树为空时,此时应该获取左子树最右端子节点
if (NULL != tmp->right) {
// 方案 1
TreeNode* tmp2 = tmp->right;
TreeNode* top2 = NULL;
// 找到最后一个节点
while (tmp2->left != NULL) {
top2 = tmp2;
tmp2 = tmp2->left;
}
// 删除老的节点
tmp->data = tmp2->data;
// 只有右子树节点 没有左子树节点
if (NULL == top2) {
tmp->right = NULL;
}
else {
top2->left = NULL;
}
free(tmp2);
}
else {
// 方案 2
TreeNode* tmp2 = tmp->left;
TreeNode* top2 = NULL;
// 找到最后一个节点
while (tmp2->right != NULL) {
tmp2 = tmp2->right;
}
// 删除老的节点
tmp->data = tmp2->data;
if (NULL == top2) {
tmp->left = NULL;
}
else {
top2->right = NULL;
}
free(tmp2);
}
}
}
else {
top = tmp;
if (data > tmp->data) {
tmp = tmp->right;
}
else {
tmp = tmp->left;
}
}
}
return false;
}
// 二叉树平衡调整
bool treeBalance(TreeNode** root) {
checkTreeBalance((*root));
while (BalanceTrue) {
printf("二叉树不平衡,最小不平衡子树数据结点: %d\n", rjt->data);
TreeNode* tmp;
if (1 < treeHeight(rjt->left) - treeHeight(rjt->right)) {
// 对于不平衡二叉树而言,左子树比右子树高
//
//printf("左\n");
if (rjt->left != NULL) {
tmp = rjt->left;
int ll = treeHeight(tmp->left);
int rr = treeHeight(tmp->right);
if (ll > rr) {
// 对于不平衡子树 左子树 而言, 左子树比右子树高
// 左左旋转
turnLL(root, rjt);
}
else {
// 对于不平衡子树 左子树 而言, 右子树比左子树高
// 左右旋转
//
turnLR(root, rjt);
}
}
}
else if (1 < treeHeight(rjt->right) - treeHeight(rjt->left)) {
// 对于不平衡二叉树而言,右子树比左子树高
//
//printf("右\n");
if (rjt->right != NULL) {
tmp = rjt->right;
int ll = treeHeight(tmp->left);
int rr = treeHeight(tmp->right);
if (ll > rr) {
//右左旋转
turnRL(root, rjt);
}
else {
//右右旋转
turnRR(root, rjt);
}
}
}
BalanceTrue = false;
checkTreeBalance((*root));
printf("二叉树调整平衡后数据结点:\n");
printf("前序遍历:");
Print1(*root);
printf("\n");
printf("中序遍历:");
Print2(*root);
printf("\n");
printf("\n");
}
}
int main() {
TreeNode* root = NULL;
printf("平衡二叉树插入测试\n");
int nums[] = { 65,60,70,55,40,63,69,66,68,77 };
int i;
for (i = 0; i < sizeof(nums) / sizeof(int); i++) {
printf("插入数据: %d\n", nums[i]);
root = addNode(root, nums[i]);
if (NULL == root) {
printf("首节点申请失败");
return -1;
}
treeBalance(&root);
}
printf("\n当前二叉树遍历\n");
printf("前序遍历:");
Print1(root);
printf("\n");
printf("中序遍历:");
Print2(root);
printf("\n");
//return 0;
printf("\n\n平衡二叉树删除测试\n");
for (i = 2; i < 5; i++) {
DeleteTreeNode(&root, nums[i]);
treeBalance(&root);
}
printf("\n当前二叉树遍历\n");
printf("前序遍历:");
Print1(root);
printf("\n");
printf("中序遍历:");
Print2(root);
printf("\n");
return 0;
}
删除节点
- 如果节点是叶子节点
- 如果叶子节点是根节点,则直接释放该节点的内存。
- 如果叶子节点不是根节点,则需要在父节点中删除该节点,并释放该节点的内存。
- 如果节点不是叶子节点
- 如果节点有右子树,则找到右子树中最左边的节点(即右子树的最小值节点),用这个节点的值替换当前节点的值,然后删除那个最小值节点。通过替代节点的父节点将替代节点设置为
NULL
,并释放替代节点的内存。- 如果节点没有右子树(即只有左子树),则找到左子树中最右边的节点(即左子树的最大值节点),用这个节点的值替换当前节点的值,然后删除那个最大值节点。通过替代节点的父节点将替代节点设置为
NULL
,并释放替代节点的内存。
6、红黑树
红黑树也是一种自平衡的二叉查找树,以前也叫做平衡二叉B树(Symmetric Binary B-tree)。
-
它是一种特殊的二叉查找树,红黑树的每一个节点上都有存储位表示节点的颜色每一个节点可以是红或者黑;
-
红黑树不是高度平衡的,它的平衡是通过"红黑规则"进行实现的
-
红黑树增删改查的性能都很好
平衡二叉树 | 红黑树 | |
---|---|---|
区别 | 高度平衡 当左右子树高度差超过1时,通过旋转保持平衡 | 不是高度平衡的 条件: 特有的红黑规则 |
红黑规则
- 每一个节点或是红色的,或者是黑色的 【节点是**RED** 或者**BLACK**】
- 根节点必须是黑色 【根节点必须是**BLACK**】
- 如果一个节点没有子节点或者父节点,则该节点相应的指针属性值为Nil,这些Nil视为叶子节点,每个叶子节点(Nil)是黑色的【叶子节点(外部节点,空节点)都是**BLACK**】
- 如果某一个节点是红色,那么它的父节点和子节点必须是黑色(不能出现两个红色节点相连的情况)【RED 节点的父节点和子节点都是**BLACK**】
- 对每一个节点,从该节点到其所有后代叶子节点的简单路径上,均包含相同数目的黑色节点
红黑树添加节点的规则
- 默认颜色: 添加节点默认是红色的(效率高)
#include <stdio.h>
#include <stdlib.h>
#define RED 0
#define BLACK 1
typedef struct RBNode {
int color; // 节点颜色
int data; // 数据
struct RBNode* left;
struct RBNode* right;
struct RBNode* parent;
} RBNode, * RBTree;
// 函数声明
RBNode* createNode(int data);
void insertFixup(RBTree* tree, RBNode* node);
RBNode* insertNode(RBTree* tree, int data);
void leftRotate(RBNode** root, RBNode* x);
void rightRotate(RBNode** root, RBNode* y);
void Print(RBNode* root);
// 创建新节点
RBNode* createNode(int data) {
RBNode* node = (RBNode*)malloc(sizeof(RBNode));
if (node)
{
node->data = data;
node->color = RED;
node->left = NULL;
node->right = NULL;
node->parent = NULL;
return node;
}
return;
}
// 插入新节点并修复红黑树
RBNode* insertNode(RBTree* tree, int data) {
RBNode* node = createNode(data);
if (*tree == NULL) {
*tree = node;
node->color = BLACK;
}
else {
RBNode* current = *tree;
RBNode* parent = NULL;
while (current != NULL) {
parent = current;
if (node->data < current->data) {
current = current->left;
}
else {
current = current->right;
}
}
node->parent = parent;
if (node->data < parent->data) {
parent->left = node;
}
else {
parent->right = node;
}
insertFixup(tree, node);
}
return node;
}
// 插入修复
void insertFixup(RBTree* tree, RBNode* node) {
//父亲节点
RBNode* parentOfNode = NULL;
//祖父节点
RBNode* grandparentOfNode = NULL;
//只有父亲是红色的才需要操作,父亲是黑色的无需任何操作
while (node != *tree && node->parent->color == RED) {
//父亲节点
parentOfNode = node->parent;
//祖父节点
grandparentOfNode = parentOfNode->parent;
//父亲节点是祖父的左子节点
if (parentOfNode == grandparentOfNode->left) {
//叔叔节点
RBNode* uncleOfNode = grandparentOfNode->right;
//叔叔节点存在并是红色
if (uncleOfNode && uncleOfNode->color == RED) {
//父亲黑色,叔叔黑色,祖父红色
grandparentOfNode->color = RED;
parentOfNode->color = BLACK;
uncleOfNode->color = BLACK;
//祖父作为当前节点
node = grandparentOfNode;
}
else {
//叔叔是黑色,并且当前节点是父的右孩子 - 左右LR旋转
if (node == parentOfNode->right) {
//把父为当前节点
node = parentOfNode;
//左旋
leftRotate(tree, node);
}
//叔叔是黑色,并且当前节点是父的左孩子 - 左左LL旋转
//即使叔叔是黑色,并且当前节点是父的右孩子,把父为当前节点进行左旋之后,也会变成叔叔是黑色,并且当前节点是父的左孩子的情况
//父亲黑色,祖父红色
parentOfNode->color = BLACK;
grandparentOfNode->color = RED;
//以祖父为支点右旋
rightRotate(tree, grandparentOfNode);
}
}
else {
//父亲节点是祖父的右子节点
RBNode* uncleOfNode = grandparentOfNode->left;
if (uncleOfNode && uncleOfNode->color == RED) {
grandparentOfNode->color = RED;
parentOfNode->color = BLACK;
uncleOfNode->color = BLACK;
node = grandparentOfNode;
}
else {
//叔叔是黑色,并且当前节点是父的左孩子 - 右左RL旋转
if (node == parentOfNode->left) {
node = parentOfNode;
rightRotate(tree, node);
}
//叔叔是黑色,并且当前节点是父的右孩子 - 右右LL旋转
//即使叔叔是黑色,并且当前节点是父的左孩子,把父为当前节点进行右旋之后,也会变成叔叔是黑色,并且当前节点是父的右孩子的情况
parentOfNode->color = BLACK;
grandparentOfNode->color = RED;
leftRotate(tree, grandparentOfNode);
}
}
}
//根只能是黑色的
(*tree)->color = BLACK;
}
// 左旋
void leftRotate(RBNode** root, RBNode* x) {
RBNode* y = x->right;
//建立x、y->left的关系
x->right = y->left;
if (y->left != NULL) {
y->left->parent = x;
}
//建立x->parent和y的关系
y->parent = x->parent;
if (x->parent == NULL) {
*root = y;
}
else if (x == x->parent->left) {
x->parent->left = y;
}
else {
x->parent->right = y;
}
//建立x、y的关系
y->left = x;
x->parent = y;
}
// 右旋
void rightRotate(RBNode** root, RBNode* y) {
RBNode* x = y->left;
//建立y、x->right的关系
y->left = x->right;
if (x->right != NULL) {
x->right->parent = y;
}
//建立y->parent和x的关系
x->parent = y->parent;
if (y->parent == NULL) {
*root = x;
}
else if (y == y->parent->left) {
y->parent->left = x;
}
else {
y->parent->right = x;
}
//建立y,x关系
x->right = y;
y->parent = x;
}
// 二叉树中序遍历
void Print(RBNode* root) {
if (NULL == root) return;
Print(root->left);
printf("%d - %d\t", root->data, root->color);
Print(root->right);
}
// 主函数或其他函数可以调用这些函数来操作红黑树
int main() {
RBTree tree = NULL;
int nums[9] = { 20,18,23,22,17,24,19,15,14 };
int i;
for (i = 0; i < 9; i++)
{
insertNode(&tree, nums[i]);
}
Print(tree);
return 0;
}
6.2.2 二叉树的性质
- 任意一棵树,若结点数量为n, 则边的数量为n − 1。
- 非空二叉树上第i层上至多有2i-1个结点
- 高度为h的二叉树至多有2h-1个结点
- 非空二叉树上的叶子结点数等于度为2的结点数加1,即n0= n2+ 1
- 具有 n个结点的完全二叉树的深度为 ⌊ log 2 n ⌋ + 1 \left \lfloor \log_2 n \right \rfloor + 1 ⌊log2n⌋+1
- 对完全二叉树按从上到下、从左到右的顺序依次编号1 , 2… ∗ , n,则有以下关系:
- 当 i = 1 时, 则结点 i 是二叉树的根-无双亲; 当 i > 1 时, 结点 i 的双亲的编号为 i / 2 ,即当 i 为偶数时,它是双亲的左孩子;当 i 为奇数时,它是双亲的右孩子。
- 当 2i ≤ n 时,结点 i 的左孩子编号为 2i , 否则无左孩子。
- 当 2i+1 ≤ n 时,结点 i 的右孩子编号为2i + 1,否则无右孩子。
6.2.3 二叉树的存储结构
-
顺序存储结构: 一种极端的情况,一棵深度为k的右斜树,只有k个结点,却需要分配2k-1个存储单元,造成明显的浪费,所以顺序存储结构一般只用于完全二叉树
#define MAX_TREE_SIZE 100 //二义树的最大结点数 typedef char TElementType; typedef TElementType SqBiTree[MAX_TREE_SIZE]; SqBiTree bt;
#define _CRT_SECURE_NO_WARNINGS 1 #include<stdio.h> #include<math.h> #define MAX_TREE_SIZE 100 #define CHAR 1 #define OK 1 #define ERROR 0 #define TRUE 1 #define FALSE 0 typedef int Status; /* Status是函数的类型,其值是函数结果状态代码,如OK等 */ #if CHAR typedef char TElemType; TElemType Nil = ' '; /* 设字符型以空格符为空 */ #else typedef int TElemType; TElemType Nil = 0; /* 设整型以0为空 */ #endif #define MAX_TREE_SIZE 100 /* 二叉树的最大结点数 */ typedef TElemType SqBiTree[MAX_TREE_SIZE]; /* 0号单元存储根结点 */ Status InitBiTree(SqBiTree T); /* 构造空二叉树T */ Status CreateBiTree(SqBiTree T); /* 按层序次序输入二叉树中结点的值(字符型或整型), 构造顺序存储的二叉树T */ Status BiTreeEmpty(SqBiTree T); /* 判空 */ int BiTreeDepth(SqBiTree T); /*求深度*/ Status Root(SqBiTree T, TElemType* e); /* 求根节点 */ void Print_BiTree(SqBiTree T); /* 打印 */ int main() { Status i; TElemType e; SqBiTree T; InitBiTree(T); CreateBiTree(T); printf("打印二叉树的数据\n"); Print_BiTree(T); printf("建立二叉树后,树空否?%d(1:是 0:否) 树的深度=%d\n", BiTreeEmpty(T), BiTreeDepth(T)); i = Root(T, &e); if (i) #if CHAR printf("二叉树的根为:%c\n", e); #else printf("二叉树的根为:%d\n", e); #endif else printf("树空,无根\n"); return 0; } Status InitBiTree(SqBiTree T) { /* 构造空二叉树T。因为T是固定数组,不会改变,故不需要& */ int i; for (i = 0; i < MAX_TREE_SIZE; i++) T[i] = Nil; /* 初值为空 */ return OK; } Status CreateBiTree(SqBiTree T) { /* 按层序次序输入二叉树中结点的值(字符型或整型), 构造顺序存储的二叉树T */ int i = 0; #if CHAR int l; char s[MAX_TREE_SIZE]; printf("请按层序输入结点的值(字符),空格表示空结点,结点数≤%d:\n", MAX_TREE_SIZE); gets(s); /* 输入字符串 */ l = strlen(s); /* 求字符串的长度 */ for (; i < l; i++) /* 将字符串赋值给T */ { T[i] = s[i]; if (i != 0 && T[(i + 1) / 2 - 1] == Nil && T[i] != Nil) /* 此结点(不空)无双亲且不是根 */ { printf("出现无双亲的非根结点%c\n", T[i]); exit(ERROR); } } for (i = l; i < MAX_TREE_SIZE; i++) /* 将空赋值给T的后面的结点 */ T[i] = Nil; #else printf("请按层序输入结点的值(整型),0表示空结点,输999结束。结点数≤%d:\n", MAX_TREE_SIZE); while (1) { scanf("%d", &T[i]); if (T[i] == 999) break; if (i != 0 && T[(i + 1) / 2 - 1] == Nil && T[i] != Nil) /* 此结点(不空)无双亲且不是根 */ { printf("出现无双亲的非根结点%d\n", T[i]); exit(ERROR); } i++; } while (i < MAX_TREE_SIZE) { T[i] = Nil; /* 将空赋值给T的后面的结点 */ i++; } #endif return OK; } Status BiTreeEmpty(SqBiTree T) { /* 初始条件: 二叉树T存在 */ /* 操作结果: 若T为空二叉树,则返回TRUE,否则FALSE */ if (T[0] == Nil) /* 根结点为空,则树空 */ return TRUE; else return FALSE; } int BiTreeDepth(SqBiTree T) //高度为h的二叉树至多有2^h-1个结点 - n<=pow(2,h)-1 { /* 初始条件: 二叉树T存在。操作结果: 返回T的深度 */ int i, j = -1; for (i = MAX_TREE_SIZE - 1; i >= 0; i--) /* 找到最后一个结点 */ if (T[i] != Nil) break; i++; /* 为了便于计算 */ do j++; while (i >= pow(2, j));//计算2的J次幂,返回double值。因为i<=pow(2,j)-1,因此i<pow(2,j). return j;//返回二叉树的深度 } Status Root(SqBiTree T, TElemType* e) { /* 初始条件: 二叉树T存在 */ /* 操作结果: 当T不空,用e返回T的根,返回OK;否则返回ERROR,e无定义 */ if (BiTreeEmpty(T)) /* T空 */ return ERROR; else { *e = T[0];//返回T的根,也就是第一个节点 return OK; } } void Print_BiTree(SqBiTree T) { int i; for (i = 0; i < 100; ++i) { if (T[i] != '\0') #if CHAR printf("%c", T[i]); #else printf("%d", T[i]); #endif } printf("\n"); }
-
链式存储结构
二叉树的结点 由一个数据元素和分别指向其 左、右子树的两个分支构成,则表示二叉树的链表中的结点至少包含3个域:数据域和左、右指针域。有时,为了便于找到结点的双亲,则还可在结点结构中增加一个指向其双亲结点的指针域。利用这两种结点结构所得二叉树的存储结构分别称之为二叉链表和三叉链表。
#define _CRT_SECURE_NO_WARNINGS 1 #include <stdio.h> #include <stdlib.h> #define MAX_TREE_SIZE 100 typedef char TElementType; typedef struct BiTNode { TElementType data; struct BiTNode* lchild, * rchild; //左孩子、右孩子指针 }BiTNode, * BiTree; //先序 BiTree CreateBiTree() { char ch; if (scanf(" %c", &ch) != 1) { // 检查输入是否成功 // 输入失败,可能是文件结束符或错误输入 exit(EXIT_FAILURE); } if (ch != '#') { BiTNode* node = (BiTNode*)malloc(sizeof(BiTNode)); // 分配内存 if (node == NULL) { // 内存分配失败 perror("内存分配失败"); exit(EXIT_FAILURE); } node->data = ch; node->lchild = CreateBiTree(); // 递归创建左子树 node->rchild = CreateBiTree(); // 递归创建右子树 return node; } else { return NULL; } } //中序 void Print_Tree(BiTree T) { if (T) { Print_Tree(T->lchild); printf("%c ", T->data); Print_Tree(T->rchild); } } int main() { BiTree T = NULL; T = CreateBiTree(); // abc##de#g##f### Print_Tree(T); // c b e g d f a return 0; }
6.3 遍历二叉树和线索二叉树
6.3.1 遍历二叉树
-
先(根)序遍历
(1) 访问根结点; (2) 先序遍历左子树; (3) 先序遍历右子树
-
中(根)序遍历
(1) 中序遍历左子树; (2) 访问根结点; (3) 中序遍历右子树
-
后(根)序遍历
(1) 后序遍历左子树; (2) 后序遍历右子树; (3) 访问根结点
-
层序遍历
层序遍历以树的根节点开始,按照从上到下、从左到右的顺序逐层遍历树中的节点。
过程:
- 创建一个队列,并将根节点入队。
- 当队列不为空时,执行以下步骤:
- 从队列中取出一个节点,访问该节点。
- 将该节点的所有子节点(如果存在)依次入队。
- 如果队列为空,则表示遍历结束。
/* 递归 */
#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>
#include <stdlib.h>
#define MAX_TREE_SIZE 100
#define MAXQSIZE 10
#define OK 1
#define ERROR 0
#define OVERFLOW -1
#define ERROR -2
#define TRUE 1
#define FALSE 0
typedef int Status; /* Status是函数的类型,其值是函数结果状态代码,如OK等 */
typedef char TElementType;
typedef struct BiTNode {
TElementType data;
struct BiTNode* lchild, * rchild; //左孩子、右孩子指针
}BiTNode, * BiTree;
typedef struct {
BiTNode** base; // 队列存储的是TreeNode*的指针
int front;
int rear;
} SqQueue;
Status InitQueue(SqQueue* Q) {
Q->base = (BiTNode**)malloc(MAXQSIZE * sizeof(BiTNode*));
if (!Q->base) exit(1); // 使用exit(1)表示错误退出
Q->front = Q->rear = 0;
return OK;
}
// 判断队列是否为空
int IsEmpty(SqQueue* q) {
return q->rear <= q->front;
}
Status EnQueue(SqQueue* Q, BiTNode* node) {
if ((Q->rear + 1) % MAXQSIZE == Q->front) {
return OVERFLOW; // 队列已满,返回OVERFLOW
}
Q->base[Q->rear] = node;
Q->rear = (Q->rear + 1) % MAXQSIZE;
return OK;
}
BiTNode* DeQueue(SqQueue* Q) {
if (Q->front == Q->rear) {
return ERROR; // 队列为空,返回ERROR
}
BiTNode* dequeuedNode = Q->base[Q->front]; // 使用引用来修改e的值
Q->front = (Q->front + 1) % MAXQSIZE;
return dequeuedNode;
}
void DestroyQueue(SqQueue* Q) {
free(Q->base);
Q->base = NULL;
Q->front = Q->rear = 0;
}
//先序
BiTree CreateBiTree() {
char ch;
if (scanf(" %c", &ch) != 1) { // 检查输入是否成功
// 输入失败,可能是文件结束符或错误输入
exit(EXIT_FAILURE);
}
if (ch != '#') {
BiTNode* node = (BiTNode*)malloc(sizeof(BiTNode)); // 分配内存
if (node == NULL) {
// 内存分配失败
perror("内存分配失败");
exit(EXIT_FAILURE);
}
node->data = ch;
node->lchild = CreateBiTree(); // 递归创建左子树
node->rchild = CreateBiTree(); // 递归创建右子树
return node;
}
else {
return NULL;
}
}
Status PrintElement(TElementType e)
{
printf("%c ", e);
return OK;
}
//先序遍历
Status PreOrderTraverse(BiTree T, Status(* PrintElement)(TElementType e))
{
if (T)
{
if (PrintElement(T->data))
if (PreOrderTraverse(T->lchild,PrintElement))
if (PreOrderTraverse(T->rchild,PrintElement)) return OK;
return OK;
}
else
{
return OK;
}
}
//中序遍历
Status InOrderTraverse(BiTree T, Status(*PrintElement)(TElementType e))
{
if (T)
{
if (InOrderTraverse(T->lchild, PrintElement))
if (PrintElement(T->data))
if (InOrderTraverse(T->rchild, PrintElement)) return OK;
return OK;
}
else
{
return OK;
}
}
//后序遍历
Status PostOrderTraverse(BiTree T, Status(*PrintElement)(TElementType e))
{
if (T)
{
if (PostOrderTraverse(T->lchild, PrintElement))
if (PostOrderTraverse(T->rchild, PrintElement))
if (PrintElement(T->data)) return OK;
return OK;
}
else
{
return OK;
}
}
//层序遍历
void LevelOrderTraversal(BiTree T, Status(*PrintElement)(TElementType e)) {
if (T == NULL) return;
SqQueue Q;
InitQueue(&Q); //初始化辅助队列
EnQueue(&Q, T); //将根节点入队
while (!IsEmpty(&Q)) { //队列不空则循环
BiTNode* frontNode = DeQueue(&Q); //队头结点出队
PrintElement(frontNode->data); //访问出队结点
if (frontNode->lchild != NULL) {
EnQueue(&Q, frontNode->lchild); //左子树不空,则左子树根节点入队
}
if (frontNode->rchild != NULL) {
EnQueue(&Q, frontNode->rchild); //右子树不空,则右子树根节点入队
}
}
}
int main()
{
BiTree T = NULL;
T = CreateBiTree(); // abc##de#g##f###
printf("----------先序---------------\n");
PreOrderTraverse(T, PrintElement); // a b c d e g f
printf("\n----------中序---------------\n");
InOrderTraverse(T, PrintElement); // c b e g d f a
printf("\n----------后序---------------\n");
PostOrderTraverse(T, PrintElement); // c g e f d b a
printf("\n----------层序---------------\n");
LevelOrderTraversal(T, PrintElement); // a b c d e f g
return 0;
}
非递归的思路:
先序:
中序:
后序:
/* 非递归 */
#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>
#include <stdlib.h>
#define MAX_TREE_SIZE 100
#define MAXQSIZE 10
#define OK 1
#define ERROR 0
#define OVERFLOW -1
#define ERROR -2
#define TRUE 1
#define FALSE 0
typedef int Status; /* Status是函数的类型,其值是函数结果状态代码,如OK等 */
typedef char TElementType;
typedef struct BiTNode {
TElementType data;
struct BiTNode* lchild, * rchild; //左孩子、右孩子指针
}BiTNode, * BiTree;
// 模拟栈结构
typedef struct Stack {
BiTNode* nodes[100]; // 假设最多100个节点
int top;
} Stack;
// 初始化栈
void StackInit(Stack* s) {
s->top = -1;
}
// 判断栈是否为空
int IsEmpty(Stack* s) {
return s->top == -1;
}
// 压栈
void Push(Stack* s, BiTNode* node) {
if (s->top < 99) { // 防止栈溢出
s->nodes[++(s->top)] = node;
}
}
// 出栈
BiTNode* Pop(Stack* s) {
if (!IsEmpty(s)) {
return s->nodes[(s->top)--];
}
return NULL;
}
//先序
BiTree CreateBiTree() {
char ch;
if (scanf(" %c", &ch) != 1) { // 检查输入是否成功
// 输入失败,可能是文件结束符或错误输入
exit(EXIT_FAILURE);
}
if (ch != '#') {
BiTNode* node = (BiTNode*)malloc(sizeof(BiTNode)); // 分配内存
if (node == NULL) {
// 内存分配失败
perror("内存分配失败");
exit(EXIT_FAILURE);
}
node->data = ch;
node->lchild = CreateBiTree(); // 递归创建左子树
node->rchild = CreateBiTree(); // 递归创建右子树
return node;
}
else {
return NULL;
}
}
Status PrintElement(TElementType e)
{
printf("%c ", e);
return OK;
}
// 非递归先序遍历:
void PreOrderTraverse(BiTNode* root) {
if (root == NULL) {
return;
}
Stack stack;
StackInit(&stack);
Push(&stack, root);
while (!IsEmpty(&stack)) {
root = Pop(&stack);
PrintElement(root->data);
if (root->rchild != NULL) {
Push(&stack, root->rchild);
}
if (root->lchild != NULL) {
Push(&stack, root->lchild);
}
}
}
//中序遍历
Status InOrderTraverse(BiTNode* head) {
if (head == NULL) {
return;
}
Stack stack;
StackInit(&stack);
BiTNode* current = head;
while (current != NULL || !IsEmpty(&stack)) {
// 将当前节点压入栈中,并遍历其左子树
while (current != NULL) {
Push(&stack, current);
current = current->lchild;
}
// 访问栈顶节点,并遍历其右子树
current = Pop(&stack);
PrintElement(current);
current = current->rchild;
}
}
//后序遍历
Status PostOrderTraverse(BiTNode* root) {
if (root == NULL) {
return;
}
Stack s1, s2;
StackInit(&s1);
StackInit(&s2);
Push(&s1, root);
while (!IsEmpty(&s1)) {
root = Pop(&s1);
Push(&s2, root);
if (root->lchild != NULL) {
Push(&s1, root->lchild);
}
if (root->rchild != NULL) {
Push(&s1, root->rchild);
}
}
while (!IsEmpty(&s2)) {
PrintElement(Pop(&s2)->data);
}
}
int main()
{
BiTree T = NULL;
T = CreateBiTree(); // abc##de#g##f###
printf("----------先序非递归---------------\n");
PreOrderTraverse(T, PrintElement); // a b c d e g f
printf("\n----------中序非递归---------------\n");
InOrderTraverse(T, PrintElement); // c b e g d f a
printf("\n----------后序非递归---------------\n");
PostOrderTraverse(T, PrintElement); // c g e f d b a
return 0;
}
参考:
教材:
严蔚敏《数据结构》(C语言版).pdf
离散数学及其应用(原书第8版) (Kenneth H.Rosen) .pdf
《代码随想录》回溯算法(V3.0).pdf
视频:
https://www.bilibili.com/video/BV1Zf4y1a77g/?spm_id_from=333.788&vd_source=a89593e8d33b31a56b894ca9cad33d33
https://www.bilibili.com/video/BV1b7411N798/?p=51&spm_id_from=333.880.my_history.page.click&vd_source=a89593e8d33b31a56b894ca9cad33d33
https://www.bilibili.com/video/BV1pu411c7pf/?spm_id_from=333.1007.top_right_bar_window_history.content.click&vd_source=a89593e8d33b31a56b894ca9cad33d33
https://www.bilibili.com/video/BV17F411T7Ao?p=195&vd_source=a89593e8d33b31a56b894ca9cad33d33
博客:
https://blog.csdn.net/Real_Fool_/article/details/113930623
https://blog.csdn.net/m0_73633088/article/details/133443742
https://blog.csdn.net/wangrunmin/article/details/7831318
https://blog.csdn.net/Colorful___/article/details/133603913
https://blog.51cto.com/u_13360/6732847
https://zhuanlan.zhihu.com/p/600665635
https://blog.csdn.net/crr411422/article/details/129932889
https://blog.51cto.com/u_15773967/6148952
https://blog.csdn.net/2401_82584055/article/details/138349195
https://mp.weixin.qq.com/s?__biz=Mzg5MjkxNTA5MA==&mid=2247484467&idx=2&sn=3ea1749b1c7c301289a40dac4497e87e&chksm=c03798cef74011d8ead16e27ebea8e3a1df2a9a0242385d328f0ec05973e9b12ef1fa7254e14&scene=27
工具:
https://www.cs.usfca.edu/~galles/visualization/DisjointSets.html