b站
数据结构与算法
树性结构
树与森林
树结构介绍
一个结点下面可能连接多个结点,并不断延伸。树的分支只能向后单独延伸,b不能与其他分支上的结点相交。
- 位于最上方的结点为根结点
- 每个结点连接的子结点数目(分支的数目),称为结点的度,而各个结点度的最大值为树的度。
- 每个结点延伸下一个结点称为一颗子树。
- 每个结点的层次按从上往下顺序,树的根节点为1,每向下一层+1。整棵树中所有结点的最大层次,即这棵树的深度
规定结点之间的名称
- 与当前结点直接向下相连的结点,称为子节点。反过来,当前结点为下面的结点的父节点。
- 如果某个结点没有任何的子节点(结点度为0),则称为叶子节点
- 如果两个结点的父节点是同一个,则称这个两个结点为兄弟结点
- 从根节点开始一直到某个结点的整条路径的所有结点,都是这个结点的祖先结点
森林
森林由很多棵构成的。m棵树的集合称为森林。
二叉树
二叉树是最大只能有2个度的树。二叉树任何结点的子树是有左右之分。
五种基本形态
二叉树的所有分支结点都存在左子树和右子树,且叶子结点都在同一层,称之为满二叉树。如果最后一层有空缺,并且所有的叶子节点是按照从左往右的顺序排列,称之为完全二叉树。满二叉树一定是完全二叉树。
二叉树与树和森林的转换
二叉树与树
对树直接将所有的兄弟节点连起来。擦掉所有结点除了最左边结点以外的连线。
结论:树转化为二叉树,根节点一定没有右子树。
二叉树与森林
先将森林中的所有树转换为二叉树,再依次连接。
相比树转换为二叉树,森林转换为二叉树,根节点就存在右子树,右子树连接的是森林中其他的树。
二叉树的性质
- 二叉树,第i层的最大结点数量为 2 i − 1 2^{i-1} 2i−1
- 一棵深度为k的二叉树,可具有最大结点的数量为 n = 2 0 + 2 1 + 2 2 + . . . 2 k − 1 n=2^{0}+2^{1}+2^{2}+...2^{k-1} n=20+21+22+...2k−1。即为一个等比数列,公比q为2, S n = 2 k − 1 S_n=2^{k}-1 Sn=2k−1。且结点的边数为E=n-1。
- 假设一棵二叉树中度为0、1、2的结点数量分别为
n
0
、
n
1
、
n
2
n_0、n_1、n_2
n0、n1、n2,可直接得到结点总数
n
=
n
0
+
n
1
+
n
2
n=n_0+n_1+n_2
n=n0+n1+n2
每个结点有且仅有一条边与其父节点相连,那么边数之和可表示为 E = n 1 + 2 n 2 E=n_1+2n_2 E=n1+2n2
则有 E = n − 1 = n 1 + 2 n 2 E=n-1=n_1+2n_2 E=n−1=n1+2n2, n = n 1 + 2 n 2 + 1 n=n_1+2n_2+1 n=n1+2n2+1, n = n 1 + 2 n 2 + 1 = n 0 + n 1 + n 2 n=n_1+2n_2+1=n_0+n_1+n_2 n=n1+2n2+1=n0+n1+n2
综上,对任一棵二叉树,如果其叶子结点个数为 n 0 n_0 n0,度为2的结点个数为 n 2 n_2 n2,则满足 n 0 = n 2 + 1 n_0=n_2+1 n0=n2+1 - 假设二叉树为满二叉树,层数为k,结点数量为 n = 2 k − 1 n=2^{k}-1 n=2k−1,最后一层可满可不满。则一棵完全二叉树结点树n满足: 2 k − 1 − 1 < n < = 2 k − 1 2^{k-1}-1<n<=2^k-1 2k−1−1<n<=2k−1,则n肯定是一个整数,那么写为 2 k − 1 < = n < = 2 k − 1 2^{k-1}<=n<=2^k-1 2k−1<=n<=2k−1,得到 k − 1 < = log 2 n k-1<=\log _2n k−1<=log2n。综上一棵n个结点的完全二叉树深度为 k = [ log 2 n ] + 1 k=[\log_2n]+1 k=[log2n]+1
- 对任意一个结点i,结点顺序为从上到下,从左往右:
对一拥有左右孩子的结点,其左孩子为2i,右孩子为2i+1
如果i=1,此结点为二叉树的根节点,如果i>1,那么其父节点为[i/2],如第3个结点的父节点为第1个结点,即根节点。
如果2i>n,则结点i没有左孩子,n为5时,假设此时i=3,那么2i=6>n=5,说明第三个结点没有左子树。
如果2i+1>n,则结点i没有右孩子。
二叉树练习
- 由三个结点可构造出多少不同种的二叉树?(N个结点呢)
动态规划法
假设由三个结点,其中一个作根节点,剩下两个结点,两个都在左边或右边或一左一右 h ( 3 ) = h ( 2 ) ∗ h ( 0 ) + h ( 1 ) ∗ h ( 1 ) + h ( 0 ) ∗ h ( 2 ) h(3)=h(2)*h(0)+h(1)*h(1)+h(0)*h(2) h(3)=h(2)∗h(0)+h(1)∗h(1)+h(0)∗h(2)
// Dynamic Programming 动态规划法
_Bool DP()
{
int size;
scanf("%d", &size); // 输入二叉树的节点数
int dp[size + 1]; // 加1是包括0
dp[0] = dp[1] = 1; // 没有结点或只有一个结点得到1
for(int i = 2; i <= size ; ++i)
{
dp[i] = 0;
for(int j = 0; j < i ; ++j) // 动态规划,逆推所有的情况
{
dp[i] += dp[j] * dp[i - j - 1];
}
}
printf("%d",dp[size]); // 输出结点为size的构造二叉树数量
return 1;
}
卡特兰数公式法
// 卡特兰数,直接根据数列的规律,利用算式解决
int Catalan(int n)
{
int res = 1;
for(int i = 2; i <= n ; ++i)
{
res *= i;
}
return res;
}
int n;
scanf("%d", &n);
printf("%d", Catalan(2*n) / (Catalan(n) * Catalan(n+1)));
-
一棵完全二叉树有1001个结点,其中叶子节点的个数为?
可先求出层数, k = [ log 2 n ] + 1 = 9 + 1 = 10 k=[\log_2n]+1=9+1=10 k=[log2n]+1=9+1=10
此二叉树的层数为10,最后一层不满,可得前9层的结点数为 S n = 2 k − 1 = 511 S_n=2^{k}-1=511 Sn=2k−1=511,则剩下的结点都在第10层: 1001 − 511 = 490 1001-511=490 1001−511=490,根据完全二叉树性质,第10层按照顺序排列。
由于第10层不满,叶子节点数也包括第9层部分,先计算第9层的结点数 2 9 − 1 = 256 个 2^{9-1}=256个 29−1=256个,除去第9层中度为1和2的结点,只需要让第10层的结点数除以2(多加一个以剔除度为1的情况) ( 490 + 1 ) / 2 = 245 (490+1)/2=245 (490+1)/2=245,故第9层的叶子节点数为 256 − 245 = 11 256-245=11 256−245=11,综上所有的叶子节点数为 11 + 490 = 501 11+490=501 11+490=501 -
深度为h的满m叉树的第k层有多少个结点?
根据满二叉树的性质推出m叉树情况下,第k层的结点数为 n = m i − 1 n=m^{i-1} n=mi−1 -
一棵1025个结点的二叉树的层数k的取值范围
最小值情况为完全二叉树,最大值情况为所有结点只有一个子节点,类似单链表 -
将一棵树转换为二叉树,根节点的右边连接的是
性质,结果为空
二叉树的构建
采用数组并不方便,存在大量计算
采用链表的链式存储,一个结点需要存放一个指向左子树和一个指向右子树的指针
初始化链表
typedef char E;
typedef struct TreeNode
{
E element;
struct TreeNode * left;
struct TreeNode * right;
} * Node;
构建图中的二叉树
int main()
{
Node a = malloc(sizeof(struct TreeNode));
Node b = malloc(sizeof(struct TreeNode));
Node c = malloc(sizeof(struct TreeNode));
Node d = malloc(sizeof(struct TreeNode));
Node e = malloc(sizeof(struct TreeNode));
a->element = 'A';
b->element = 'B';
c->element = 'C';
d->element = 'D';
e->element = 'E';
a->left = b;
a->right = c;
b->left = d;
b->right = e;
d->left = d->right = NULL;
e->left = e->right = NULL;
c->left = c->right = NULL;
printf("%c",a->left->right->element);
}
可插入断点查看变量的内存,存储的结点及元素
二叉树的遍历
前序遍历
首先从根节点开始,先遍历左边再遍历右边。规律,整棵二叉树的根节点一定是出现在最前面的。
// 前序遍历二叉树
void preOrder(Node root)
{
if(root == NULL)
{
return;
}
else
{
printf("%c",root->element);
preOrder(root->left);
preOrder(root->right);
}
}
通过设置断点,函数调用栈查看递归函数是如何遍历结点。其递归本质是调用栈的特性实现。若自己写栈来实现同样效果,但更加麻烦。
- 一路向左,不断入栈,直到尽头
- 到达尽头,出栈,看有无右子树,没有则继续出栈
- 拿到右子树,从右子树开始,重复上述,直到清空
// 构建二叉树以及遍历
typedef char E;
typedef struct TreeNode
{
E element;
struct TreeNode * left;
struct TreeNode * right;
} * Node;
// 构建栈,栈内元素类型定义为Node,即二叉树结点指针
typedef Node T;
struct StackNode
{
T element;
struct StackNode * next;
}; // 链表实现栈
typedef struct StackNode * SNode;
void initStack(SNode head)
{
head->next = NULL;
}
_Bool pushStack(SNode head, T element)
{
SNode node = malloc(sizeof(struct StackNode));
if(node == NULL)
{
return 0;
}
node->next = head->next;
node->element = element;
head->next = node;
return 1;
}
_Bool isEmpty(SNode head)
{
return head->next == NULL;
}
T popStack(SNode head)
{
SNode top = head->next;
head->next = head->next->next;
T e = top->element;
free(top);
return e;
}
// 前序遍历二叉树(自构建栈实现)
void preOrder(Node root)
{
struct StackNode head;
initStack(&head);
while (root || !isEmpty(&head)) // 栈空且结点为NULL才终止循环
{
while (root) // 先遍历左子树
{
printf("%c", root->element);
pushStack(&head, root); // 每经过一个结点,将结点入栈
root = root->left; // 一直往左走, 遍历下一个孩子结点
}
Node node = popStack(&head); // 出栈
root = node->right; // 取出栈结点的右孩子数
}
}
中序遍历
先完成整个左子树的遍历后打印,然后再遍历右子树。打印的时机发生了改变。
// 中序遍历二叉树
void InOrder(Node root)
{
if(root == NULL)
{
return;
}
else
{
InOrder(root->left);
printf("%c",root->element); // 先遍历左子树,再打印
InOrder(root->right);
}
}
// 中序遍历
void InOrder(Node root)
{
struct StackNode head;
initStack(&head);
while (root || !isEmpty(&head)) // 栈空且结点为NULL才终止循环
{
while (root) // 先遍历左子树
{
pushStack(&head, root); // 每经过一个结点,将结点入栈
root = root->left; // 一直往左走, 遍历下一个孩子结点
}
Node node = popStack(&head); // 出栈
printf("%c", node->element);
root = node->right; // 取出栈结点的右孩子数
}
}
后序遍历
等待左右子树都全部遍历完成,才进行打印。发现整棵二叉树的根节点一定是在后面的。A结点再所有结之后,B在子节点D、E后面。后序遍历与前序遍历只是规律相反,不是打印相反。
// 后序遍历二叉树
void PostOrder(Node root)
{
if(root == NULL)
{
return;
}
else
{
PostOrder(root->left);
PostOrder(root->right);
printf("%c",root->element); // 遍历完左右子树再打印
}
}
非递归法需要改变之前的逻辑。在之前的前序和中序遍历中,出栈的时机是在左子树完成遍历后,而这里的后序遍历需要让左子树和右子树都完成遍历。因此可以修改在左子树完成遍历后不着急将结点出栈,而是等全部遍历完成后再出栈。
typedef char E;
typedef struct TreeNode
{
E element;
struct TreeNode * left;
struct TreeNode * right;
struct TreeNode * flag; // 假定一个标志判断左子树和右子树遍历
} *
T peekStack(SNode head) // 取一下栈顶的元素查看,不出栈
{
return head->next->element;
}
// 后序遍历
void PostOrder(Node root)
{
struct StackNode head;
initStack(&head);
while (root || !isEmpty(&head)) // 栈空且结点为NULL才终止循环
{
while (root) // 先遍历左子树
{
pushStack(&head, root); // 每经过一个结点,将结点入栈
root->flag = 0;
root = root->left; // 一直往左走, 遍历下一个孩子结点
}
root = peekStack(&head);
if(root->flag == 0) // 左子树遍历完后
{
root->flag = 1;
root = root->right; // 遍历右子树
}
else // 全部遍历完再打印
{
printf("%c", root->element);
popStack(&head);
root = NULL;
}
}
}
层序遍历
按照从上往下每一层,从左到右的顺序打印每个结点。
利用队列实现,首先将根节点存入队列中,循环执行
- 进行出队操作,得到一个结点,并打印结点的值
- 将此结点的左右孩子结点依次入队
不断重复以上,直到队列为空
// 层序遍历
void levelOrder(Node root)
{
struct Queue queue;
initQueue(&queue);
InQueue(&queue, root); // 根节点入队
while (!isEmpty(&queue)) // 不断重复直到队列为空
{
Node node = DeQueue(&queue); // 头一个结点出队
printf("%c", node->element);
if(node->left) // 如果存在左右子树
{
InQueue(&queue,node->left); // 按顺序先左后右
}
if(node->right)
{
InQueue(&queue, node->right);
}
}
}
还可实现递归的层序遍历
递归的层序遍历
前序遍历(根在前,从左往右,一棵树的根永远在左子树前面,左子树又永远在右子树前面 )
中序遍历(根在中,从左往右,一棵树的左子树永远在根前面,根永远在右子树前面)
后序遍历(根在后,从左往右,一棵树的左子树永远在右子树前面,右子树永远在根前面)
二叉树练习
-
现有一棵二叉树前序遍历结果为ABCDE,中序遍历结果为:BADCE,则后序遍历的结果为BDECA
根据遍历特性作出草图 -
对二叉树的结点从1开始编号,要求每个结点的编号大于其左右孩子的编号,采用哪种遍历方式?
后序遍历
高级树结构
线索化二叉树
二叉树的某些结点会存在NULL的情况,利用NULL可将其线索化为某一种顺序遍历的指向下一个按顺序的结点的指针。
线索化的规则为
- 结点的左指针,指向其当前遍历顺序的前驱节点。
- 结点的右指针,指向其当前遍历顺序的后继结点.
以下按照前序遍历的方式
如何分别某个结点的指针到底是指向的是其左右孩子结点,还是某种遍历顺序下的前驱或是后续结点?分别为左右添加一个标志位,表示左右指针指向是孩子还是遍历线索。
最终不需要使用栈。
案例:
先按照正常遍历进行,留意存在空指针的结点,需要修改其指针的指向
// 构建二叉树
typedef char E;
typedef struct TreeNode
{
E element;
struct TreeNode * left;
struct TreeNode * right;
int leftTag, rightTag; // 标志位,1表示指针指向的是线索,不为1表示正常的孩子结点
} * Node;
// 创建结点的函数
Node createNode(E element)
{
Node node = malloc(sizeof(struct TreeNode));
node->right = node->left = NULL;
node->element = element;
node->leftTag = node->rightTag = 0;
return node;
}
Node a = createNode('A');
Node b = createNode('B');
Node c = createNode('C');
Node d = createNode('D');
Node e = createNode('E');
a->left = b;
b->left = d;
b->right = e;
a->right = c;
进行前序遍历的线索化
// 前序遍历线索化函数
Node pre = NULL; // 需要一个pre来保存后续结点的指向
void preOrderThreaded(Node root)
{
if(root == NULL)
{
return;
}
if(root->left == NULL) // 判断如果当前结点的左边是否为空,是则指向上一个结点
{
root->left = pre;
root->leftTag = 1; // 修改标记
}
if(pre && pre->right == NULL) // 判断上一个结点的右边是否为空,是则进行线索化,指向当前结点
{
pre->right = root;
pre->rightTag = 1;
}
pre = root; // 每遍历完一个,需要更新pre,表示上一个遍历的结点
if(root->leftTag == 0) // 只有标志为0才可进行遍历,否则为线索
{
preOrderThreaded(root->left);
}
if(root->rightTag == 0)
{
preOrderThreaded(root->right);
}
}
在线索化后前序遍历
// 前序遍历
void preOrder(Node root)
{
while (root)
{
printf("%c", root->element);
if(root->leftTag == 0)
{
root = root->left;
}
else
{
root = root->right;
}
}
}
进行中序遍历的线索化
// 中序遍历线索化函数
void InOrderThreaded(Node root)
{
if(root == NULL)
{
return;
}
if(root->leftTag == 0) // 只有标志为0才可进行遍历,否则为线索
{
InOrderThreaded(root->left);
} // 左边遍历完再进行线索化
if(root->left == NULL) // 判断如果当前结点的左边是否为空,是则指向上一个结点
{
root->left = pre;
root->leftTag = 1; // 修改标记
}
if(pre && pre->right == NULL) // 判断上一个结点的右边是否为空,是则进行线索化,指向当前结点
{
pre->right = root;
pre->rightTag = 1;
}
pre = root; // 每遍历完一个,需要更新pre,表示上一个遍历的结点
if(root->rightTag == 0)
{
InOrderThreaded(root->right);
}
}
// 中序遍历
void InOrder(Node root)
{
while (root) // 先走左边
{
while (root && root->leftTag == 0) // 查找左边的是不是线索
{
root = root->left;
}
printf("%c", root->element); // 到最左边再打印
while(root && root->rightTag == 1) // 右边是线索化后的结果,表示是下一个结点
{
root = root->right;
printf("%c", root->element); // 线索往下为中序遍历结果,直接打印
}
root = root->right;
}
}
进行后序遍历的线索化
先完成左右的遍历,但对于右边遍历结束后不一定能找到对应子树的根节点。需要修改结点。
加入父节点
struct TreeNode * parent; // 指向父节点
// 后序遍历线索化函数
void PostOrderThreaded(Node root)
{
if(root == NULL)
{
return;
}
if(root->leftTag == 0) // 只有标志为0才可进行遍历,否则为线索
{
PostOrderThreaded(root->left);
if(root->left)
{
root->left->parent = root; // 当前结点不为空,将左结点设为父子结点
}
}
if(root->rightTag == 0)
{
PostOrderThreaded(root->right);
if(root->right)
{
root->right->parent = root;
}
}// 左右遍历完再进行线索化
if(root->left == NULL) // 判断如果当前结点的左边是否为空,是则指向上一个结点
{
root->left = pre;
root->leftTag = 1; // 修改标记
}
if(pre && pre->right == NULL) // 判断上一个结点的右边是否为空,是则进行线索化,指向当前结点
{
pre->right = root;
pre->rightTag = 1;
}
pre = root; // 每遍历完一个,需要更新pre,表示上一个遍历的结点
}
// 后序遍历
void postOrder(Node root)
{
Node last = NULL, node = root; // 暂存结点,一个记录上一次遍历结点,还有一个从root开始
while (node)
{
while (node->left != last && node->leftTag == 0) // 从最左边的结点开始
{
node = node->left;
}
while (node && node->rightTag == 1) // 如果右边线索,则一直进行
{
printf("%c", node->element); // 沿途打印
last = node;
node = node->right;
}
// 当前左右结点结束,需要寻找其他兄弟节点
if( node == root && node->right == last) // 如果是根节点,没有兄弟节点,需要特殊处理
{
printf("%c", node->element);
return;
}
while (node && node->right == last) // 如果当前结点的右结点为上一个结点,一直向前
{
printf("%c", node->element);
last = node;
node = node->parent;
}
if(node && node->rightTag == 0) // 当前结点的右节点不是线索,走右节点。是则等下一轮
{
node = node->right;
}
}
}
二叉查找树
类似于二分搜索思想,进行快速查找。
- 左子树中所有结点的值均小于其根节点的值
- 右子树中所有结点的值均大于其根节点的值
- 二叉搜索树的子树也是二次搜索树
二叉查找树满足左边一定比当前结点小,右边一定比当前结点大的规则
构建二叉树
typedef int E;
typedef struct TreeNode
{
E element;
struct TreeNode * left;
struct TreeNode * right;
}* Node;
创建结点
Node createNode(E element)
{
Node node = malloc(sizeof(struct TreeNode));
node->left = node->right = NULL;
node->element = element;
return node;
}
插入元素
// 插入元素
Node insert(Node root, E element)
{
if(root)
{
if(root->element > element) // 当插入结点值小于当前结点,放到左边去
{
root->left = insert(root->left, element);
}
else if(root->element < element) // 当插入结点值大于当前结点,放到右边
{
root->right = insert(root->right, element);
}
}
else
{
root = createNode(element); // 结点为空,则找到插入元素的未知
}
return root; // 返回当前结点
}
// 前序遍历二叉树(递归法)
void preOrder(Node root)
{
if(root == NULL)
{
return;
}
else
{
printf("%d ",root->element);
preOrder(root->left);
preOrder(root->right);
}
}
查找元素,方法类似二分法,根据特性
// 查找元素
Node find(Node root, E target)
{
while (root)
{
if(root->element > target) // 当前结点大于查找目标值,向左查找
{
root = root->left;
}
else if(root->element < target)
{
root = root->right;
}
else
{
return root; // 当前值就是所查找目标值
}
}
return NULL;
}
// 查找最大的元素
Node findMax(Node root)
{
while (root && root->right) // 当前右边的值大于当前结点值
{
root = root->right;
}
return root;
}
删除元素,若只有一个孩子,可直接上位;或者无孩子可不管。有两个孩子,则为保持二叉搜索树的性质,有两种选择
- 选取其左子树最大结点上位
- 选择其右子树最小结点上位
编程以第一种情况:
Node delete(Node root , E target)
{
if(root == NULL) // 到底为找到删除的结点,返回空
{
return NULL;
}
if(root->element > target) // 当前结点值大于删除的结点值时
{
root->left = delete(root->left, target); // 向左边查找,递归法
}
else if(root->element < target)
{
root->right = delete(root->right, target);
}
else // 找到时
{
if(root->left && root->right) // 处理左右孩子均存在时
{
Node max = findMax(root->left); // 寻找左子树最大的结点值
root->element = max->element; // 将值进行替换
root->left = delete(root->left, root->element); // 替换后,以同样方式删除替换上来后的结点
}
else // 删除该节点即可,将root指定的孩子返回
{
Node tmp = root;
if(root->left) // 左孩子或者右孩子
{
root = root->right;
}
else
{
root = root->left;
}
free(tmp);
}
}
return root; // 返回最终结点
}
测试
Node root = insert(NULL, 18);
insert(root, 10);
insert(root, 7);
insert(root, 15);
insert(root, 22);
insert(root, 9);
insert(root, 8);
preOrder(root);
printf("\n");
delete(root, 10);
preOrder(root);
printf("\n");
平衡二叉树(AVL Tree)
原理
对于二叉查找树,当插入的值为{20、15、13、8、6、3},递减的数列,得到的二叉树,按照二叉搜索树完全是个链表。在进行查找结点时,得不到优化,因此二叉查找树只有在理想情况下,查找效率才最高。平衡二叉树即使得二叉树左右维持平衡的。
- 平衡二叉树一定是二叉查找树
- 任意结点的左右子树也是一棵平衡二叉树
- 从根节点开始,左右子树的高度差不能超过1
二叉树上结点的左子树高度减去右子树高度,得到结果为该结点的平衡因子。其可快速得到失衡的情况,有四种不同情况。
-
LL型调整
失衡的结点为15,需要进行右旋操作。首先找到最小不平衡子树(最先到达不平衡的),右旋将三个结点的中间结点作为新的根节点,而其他两个结点现在变成左右子树。
右旋后得到所有结点都是平衡的,且仍是二叉查找树。 -
RR型调整
进行左旋,和以上LL型正好反过来。 -
RL型调整
需要先右旋,再左旋。注意右旋操作是针对的后两个结点。
右旋后变为了RR型,再进行左旋即可得到平衡状态。 -
LR型调整
和RL型相反,则先左旋,变为LL型,再右旋。
代码实现
在插入结点时注意维护整棵树的平衡因子。
初始化创建结点,需要一个变量记录子树的高度
typedef int E;
typedef struct TreeNode
{
E element;
struct TreeNode * left;
struct TreeNode * right;
int height; // 每个结点需要记录当前子树的高度,便于计算平衡因子
}* Node;
Node createNode(E element)
{
Node node = malloc(sizeof(struct TreeNode));
node->left = node->right = NULL;
node->element = element;
node->height = 1; // 高度初始化
return node;
}
获取最大值和高度
int max(int a, int b)
{
return a > b ? a : b;
}
int getHeight(Node root)
{
if(root == NULL)
{
return 0;
}
return root->height;
}
左旋
// 左旋
Node leftRotation(Node root) // 传入原本的根节点,得到新的根节点
{
Node newRoot = root->right;
root->right = newRoot->left;
newRoot->left = root;
root->height = max(getHeight(root->left), getHeight(root->right)) + 1; // 计算高度
newRoot->height = max(getHeight(newRoot->left), getHeight(newRoot->right)) + 1; // 计算高度
return newRoot;
}
右旋
// 右旋
Node rightRotation(Node root)
{
Node newRoot = root->left;
root->right = newRoot->right;
newRoot->right = root;
root->height = max(getHeight(root->right), getHeight(root->left))+1;
newRoot->height = max(getHeight(newRoot->right), getHeight(newRoot->left)) + 1; // 计算高度
return newRoot;
}
先左旋再右旋
// 左旋,再右旋
Node leftRightRotation(Node root)
{
root->left = leftRotation(root->left);
return rightRotation(root);
}
先右旋再左旋
// 右旋,再在旋
Node rightLeftRotation(Node root)
{
root->right = rightRotation(root->right);
return leftRotation(root);
}
插入,注意需要动态计算树的高度
// 插入
Node insert(Node root, E element)
{
if(root == NULL) // 结点为NULL,找到插入位置,直接创建新的
{
root = createNode(element);
}
else if(root->element > element) // 二叉搜索树类似,判断大小
{
root->left = insert(root->left, element);
if(getHeight(root->left) - getHeight(root->right) > 1) // 插入完成后计算平衡因子
{
if(root->left->element > element) // 若成立,则左边结点大于插入的结点,为LL型;否则为LR型
{
root = rightRotation(root); // LL型进行右旋
}
else
{
root = leftRightRotation(root); // LR型进行左旋再右旋
}
}
}
else if(root->element < element)
{
root->right = insert(root->right, element);
if(getHeight(root->left) - getHeight(root->right) < -1) // 计算平衡因子
{
if(root->right->element < element) // 若成立,则右边结点小于插入的结点,为RR型;否则为RL型
{
root = leftRotation(root); // RR型进行左旋
}
else
{
root = rightLeftRotation(root); // RL型进行右旋再左旋
}
}
}
// 更新树的高度
root->height = max(getHeight(root->left), getHeight(root->right))+1; //加1为自身结点的高度
return root; // 返回root到上一级
}
测试
在插入结点,可在失衡时,进行调整状态,形成平衡二叉树
Node root = NULL;
while (1)
{
E e;
scanf("%d", &e);
root = insert(root, e);
printf("");
}
红黑树
由于在平衡二叉树中,一旦平衡因子的绝对值超过1就失衡,所以引入红黑树,使得要求没那么严格。
- 每个结点可以是黑色或是红色
- 根节点一定是黑色
- 红色结点的父节点和子节点不能为红色,即不能有两个连续的红色
- 所有空结点都是黑色(空结点为NULL,红黑树中是将空结点视为叶子节点)
- 每个结点到空结点路径上出现的黑色结点的个数都相等
可通过不严格平衡和改变颜色,可一定程度上减少旋转次数。
插入结点4后,为保持红黑树规则,改变7和15颜色,即直接将父节点和其兄弟结点同时修改为黑色,然后将爷爷结点改成红色。
正常情况下,爷爷结点会变成红色,还得往上看有没有破坏红黑树的规则。如上爷爷结点为根节点,则必须是黑色。
再插入1后,连续红色不满足,需要变色。
但变色后,所有NULL结点经历的黑色结点数量不正确。
对以上父节点为红色,父节点的兄弟结点为黑色,变色无法解决问题,只能考虑旋转。规则与之前的平衡二叉树一样的。
旋转和变色操作顺序可以交换进行。
插入时关键的判断点
- 整棵树为NULL,直接作为根节点,变成黑色
- 父节点是黑色,直接插入
- 父节点是红色,且父节点的兄弟结点是红色,直接变色(注意往上看有没有破坏之前的结构)
- 父节点是红色,但父节点的兄弟结点是黑色,需要根据(LL、LR、RR、RL)情况,进行旋转,再变色即可。
其他树结构
B树和B+树
B树(Balance Tree),专门为磁盘数据读取设计的一种度为m的查找树,平衡树,但不仅限于二叉树。之前二叉树基于内存读取的优化,磁盘读取数据更慢,需要优化。
每个结点可保存多个值,每个结点可以连接多个子树。所有值有N个,就可以划分出N+1个区间,子树最多可以有N+1个。
- 树中每个结点最多含有m个孩子(m>=2)
- 除根节点和叶子节点外,其它每个结点至少有[m/2]个孩子,键值数量至少有[m/2]-1个
- 若根节点不是叶子节点,则至少有两个孩子
- 所有叶子节点都出现在同一层
- 一个结点包含多种信息(P0、K1、P1、K2…Kn、Pn),其中P为指向子树的指针,K为键值
Ki为键值,每个结点保存的值,且键值按顺序升序排序(K(i-1)<Ki)
Pi为指向子树的指针,且指针Pi指向的子树中所有结点的键值均小于Ki,但都大于K(i-1)
键值的个数n必须满足[m/2]-1<=n<=m-1
B树的插入(以度为3例子)
插入的规则
- 如果结点上的元素数未满,则将新元素插入到该结点,并保持结点中元素的顺序
- 如果该结点上的元素已满,则需要将结点平均地分裂成两个结点:
1. 首先从该结点中的所有元素和新元素中先出一个中位数作为分割值
2. 小于中位数的元素作为左子树划分出去,大于中位数的元素作为右子树划分
3. 分割值此时上升到父节点中,若没有父节点,就创建一个新的
删除的规则 - 删除的是叶子结点中元素
正常情况直接删除
删除后,键值数小于最小值,找兄弟借一个
没有借到,直接和兄弟结点、对应的分割值合并 - 删除的是某个根节点值得元素
一般情况会删除一个分割值,删掉后需要重新从左右子树中找一个新的分割值
上来之后左右子树中出现键值数小于最小值情况,则只能合并 - 以上结束后,往上看上面得结点是否满足性质
删除16
删除15
只有14不满足性质,需要向兄弟结点借一个
删除17,兄弟结点不能再借,则合并兄弟结点与分割键
父节点只有一个元素,也需要改变。兄弟结点不能借,则进行合并
删除4
找一个新的分割值,取左边最大
最左节点元素数量少,需要向兄弟借,但不能,则合并
B数高度平衡且有序,适合在磁盘上保存数据
B数和红黑树规则不同,出现等价的情况
- B树叶节点等深,实际上体现在红黑树中为任一叶节点到达根节点的路径中,黑色路径所占的长度是相等的,黑色结点是B树的结点分割值
- B树结点的键值数量不能超过N,实际上体现在红黑树约定相邻红色结点接最多两条。红黑树与4阶B树是有一定关系的。
B+数
- 有k个子树的中间结点包含有k个元素(B树中是k-1个元素),每个元素不保存数据,只用来索引,所有数据都保存在叶子节点。
- 所有的叶子节点中包含全部元素信息,及指向这些元素记录的指针,且叶子节点按从小到大顺序连接
- 所有的根节点元素都同时存在于子节点中,在子节点元素中是最大或最小元素
最后一层形成一个有序链表,可进行顺序查找。B+树查询必须最终查找到叶子节点,而B树值匹配,并不一定在到达叶子节点后成功匹配,因此B树的查找性能不高。
哈夫曼树
概念
给定N个权值作为N个叶子节点,构造一棵二叉树,若树的带权路径长度达到最小,称为最优二叉树,也称为哈夫曼树。
带权路径长度指的是树中所有的叶节点的权值乘上其到根节点的路径长度。
W
P
L
∑
i
=
1
n
(
v
a
l
u
e
(
i
)
×
d
e
p
t
h
(
i
)
)
WPL\sum_{i=1}^{n}(value(i) \times depth(i))
WPL∑i=1n(value(i)×depth(i))
根据计算得右图结果,带权路径长度最小
首先选两棵权重最小的树作为一棵新的树的左右子树,左右顺序不重要,得到树根节点权值为这两个结点之和
重复以上操作,继续选择两个最小的组成一棵新的树
继续重复
以上应用是对数据进行压缩,得到文件的压缩包
优先级队列
权值越大的元素优先排到前面,可插队机制
修改以下入队操作
// 入队
bool offerQueue(LinkedQueue queue, T element)
{
LNode node = malloc(sizeof(struct LNode));
if(node == NULL)
{
return NULL;
}
node->element = element;
node->next = NULL; // 用到判断下一个结点为空
LNode pre = queue->front; // 从头结点依次看,直到找到第一个小于当前值的结点或到达末尾
while (pre->next && pre->next->element >= element)
{
pre = pre->next;
}
if(pre == queue->rear) // 如果位置到最后,则直接插入
{
queue->rear->next = node;
queue->rear = node;
}
else // 优先级队列
{
node->next = pre->next;
pre->next = node;
}
return true;
}
构建哈夫曼树
// 二叉树构建
typedef char E;
typedef struct TreeNode
{
E element;
struct TreeNode * left;
struct TreeNode * right;
int value; // 存放权值
}* Node;
// 链表
typedef Node T; // 修改链表中的结点类型
struct LNode
{
T element;
struct LNode * next; // 下一个结点
};
typedef struct LNode * LNode;
// 队列
struct Queue
{
LNode front, rear; // 头结点和尾结点
};
typedef struct Queue * LinkedQueue;
// 初始化队列
bool initQueue(LinkedQueue queue)
{
LNode node = malloc(sizeof(struct LNode)); // 分配内存
if(node == NULL) return false;
queue->rear = queue->front = node; // 初始队首队尾
node->next = NULL; // 默认将下一个结点设为空
return true;
}
// 创建结点
Node createNode(E element, int value)
{
Node node = malloc(sizeof(struct TreeNode));
node->left = node->right = NULL;
node->element = element;
node->value = value;
return node;
}
// 打印
void printQueue(LinkedQueue queue)
{
printf("<<");
LNode node = queue->front->next; // 先拿到头结点的下一个
while (node)
{
printf("%c ", node->element->element);
node = node->next; // 指向下一个结点
}
printf("<<\n");
}
// 入队
bool offerQueue(LinkedQueue queue, T element)
{
LNode node = malloc(sizeof(struct LNode));
if(node == NULL)
{
return NULL;
}
node->element = element;
node->next = NULL; // 用到判断下一个结点为空
LNode pre = queue->front; // 从头结点依次看,直到找到第一个小于当前值的结点或到达末尾
while (pre->next && pre->next->element->value <= element->value) // 权值比较,小的值在前面
{
pre = pre->next;
}
if(pre == queue->rear) // 如果位置到最后,则直接插入
{
queue->rear->next = node;
queue->rear = node;
}
else // 优先级队列
{
node->next = pre->next;
pre->next = node;
}
return true;
}
// 出队
T pollQueue(LinkedQueue queue)
{
LNode tmp = queue->front->next; //先取出头指针下一个结点
T e = tmp->element; // 将元素取出来
queue->front->next = queue->front->next->next; // 头指针的下一个结点指向下下一个结点
if(queue->rear == tmp) // 当tmp为最队尾结点时
queue->rear = queue->front; // 则使尾结点等于头结点
free(tmp); // 释放
return e;
}
测试
offerQueue(&queue,createNode('A',19));
offerQueue(&queue,createNode('B',5));
offerQueue(&queue,createNode('C',6));
offerQueue(&queue,createNode('D',12));
offerQueue(&queue,createNode('E',8));
如将字符进行哈夫曼编码
求出A的哈夫曼编码即为根节点到A整条路径上的值拼接
取出队列中元素构建哈夫曼树
// 队列中只有一个元素
while (queue.front->next != queue.rear) // front下一个元素尾rear即队列中只有一个元素
{
Node left = pollQueue(&queue);
Node right = pollQueue(&queue);
Node node = createNode(" ", left->value + right->value); // 创建新的根节点
node->left = left;
node->right = right;
offerQueue(&queue, node); // 将重新构建的树入队
}
Node root = pollQueue(&queue);
得到哈夫曼树后,对字符进行编码
// 计算哈夫曼编码
char * encode(Node root, E e)
{
if(root == NULL)
{
return NULL;
}
if(root->element == e)
{
return ""; // 找到返回空串
}
char * str = encode(root->left, e); // 先去左边找
char * s = malloc(sizeof(char)*20); //
if(str != NULL) //
{
s[0] = '0';
str = strcat(s, str); // 如果左边找到,则把左边的已经拼接好的字符串拼接到当前的后面
}
else
{
str = encode(root->right, e);
if(str != NULL)
{
s[0] = '1';
str = strcat(s, str);
}
}
return str; // 返回拼接好的字符串到上一级
}
// 打印哈夫曼编码值
void printEncode(Node root, E e)
{
printf("%c 的编码为:%s", e, encode(root,e));
putchar('\n');
}
堆和优先级队列
必须是完全二叉树,树中的父亲都比孩子小,称为“小顶堆”;树中的父亲都比孩子大,称为“大顶堆”。注意与二叉查找树不同。一般堆适合用数组存储
0号下标位不存储任何内容
插入元素时
不满足大顶堆性质,需要将8与父节点交换。继续计算父节点,看是否满足性质,向上比较,进行交换
删除时,删除最顶上的元素,由于需要满足完全二叉树性质,将最后面的拿上来,然后反方向从上往下的堆化操作,交换。
以大顶堆为例,采用数组
初始化
// 大顶堆结构
// 构建数组
typedef int E;
typedef struct MaxHeap
{
E * arr;
int size;
int capacity;
} * Heap;
_Bool initHeap(Heap heap)
{
heap->size = 0;
heap->capacity = 10;
heap->arr = malloc(sizeof(E) * heap->capacity);
return heap->arr != NULL;
}
插入
_Bool insert(Heap heap, E element)
{
if(heap->size == heap->capacity)
{
return 0;
}
int index = ++heap->size; // 先计算出插入的位置,自增
// 向上堆化,直到符合规则
while (index > 1 && element > heap->arr[index/2])
{
heap->arr[index] = heap->arr[index/2];
index /= 2;
}
// 得到index最终位置,
heap->arr[index] = element;
return 1;
}
删除
E delete(Heap heap)
{
E max = heap->arr[1], e = heap->arr[heap->size--]; // 最大的根节点和最后面的元素
int index = 1;
while (index * 2 < heap->size) // 从上往下查找
{
int child = index * 2;
// 看右孩子和左孩子哪个大,先选大的
if(child < heap->size && heap->arr[child] < heap->arr[child+1])
{
child += 1;
}
if(e >= heap->arr[child]) // 子节点不大于新的结点,则说明是这个位置
{
break;
}
else
{
heap->arr[index] = heap->arr[child];
index = child; // 进行堆化,将子节点换上去
}
heap->arr[index] = e; // 将index处位置更新,即下移
return max;
}
}
测试
struct MaxHeap heap;
initHeap(&heap);
insert(&heap, 5);
insert(&heap, 2);
insert(&heap, 3);
insert(&heap, 7);
insert(&heap, 6);
insert(&heap, 11);
// printHeap(&heap);
for(int i = 0 ; i<5; ++i)
{
printf("%d ", delete(&heap));
}
排序算法中会用到堆
算法实战
- 给定二叉搜索树的根结点 root,返回值位于范围 [low, high] 之间的所有结点的值的和。
struct TreeNode {
int val;
struct TreeNode *left;
struct TreeNode *right;
};
// 二叉查找树的范围和
int rangeSumBST(struct TreeNode* root, int low, int high) {
if(root == NULL)
{
return 0;
}
if(root->val > high) // 根节点的值比最大值大,在在左边找
{
return rangeSumBST(root->left, low, high);
}
else if (root->val < low)
{
return rangeSumBST(root->right, low, high);
}
else // 在范围内,则计算和
{
return root->val + rangeSumBST(root->left, low, high) + rangeSumBST(root->right, low, high);
}
}
剑指offer 07. 重建二叉树
输入某二叉树的前序遍历和中序遍历的结果,请重建该二叉树。并返回根节点。假设输入的前序遍历和中序遍历的结果中都不含重复的数字。
给出:
前序遍历 preorder = [3,9,20,15,7]
中序遍历 inorder = [9,3,15,20,7]
返回如下二叉树:
3
/ \
9 20
/ \
15 7
- 前序遍历首元素为根节点值
- 中序遍历通过根节点值,寻找根节点位置
- 将左右两边序列分隔开,重构为根节点的左右子树(递归分治)
- 新的序列中,重复以上步骤,通过前序遍历再次找到当前子树的根节点,进行分割
- 直到分割到仅剩下一个结点,开始回溯,完成整个二叉树的重建
// 创建结点
struct TreeNode * createNode(int val)
{
struct TreeNode * node = malloc(sizeof(struct TreeNode));
node->left = node->right = NULL;
node->value = val;
return node;
}
// 递归分治实现
struct TreeNode* buildTreeCore(int* preOrder, int* inOrder, int start, int end, int index)
{
if(start > end)
{
return NULL;
}
if(start == end) // 到头直接返回
{
return createNode(preOrder[index]);
}
struct TreeNode* node = createNode(preOrder[index]);
int pos = 0;
while (inOrder[pos] != preOrder[index]) // 找到中序对应位置,开始左右划分
{
// 当前结点的左子树建立
node->left = buildTreeCore(preOrder,inOrder, start, pos-1, index+1);
// 当前结点的右子树
node->right = buildTreeCore(preOrder, inOrder,pos+1, end, index+(pos-start)-1);
// 最后一个index需要先跳过左子树的所有结点,才是右子树的根节点,pos-start即为从中序划分出来的
return node;
}
}
struct TreeNode* buildTree(int* preOrder, int preOrderSize, int* inOrder, int inOrderSize)
{
// 传入前序和后序序列,通过start和end指定当前中序序列的处理范围,最后一个index是前序遍历的对应头结点位置
return buildTreeCore(preOrder,0 ,inOrder, preOrderSize - 1, 0);
}
- 验证二叉搜索树
给一个二叉树的根节点 root ,判断其是否是一个有效的二叉搜索树。
有效 二叉搜索树定义如下:
节点的左子树只包含 小于 当前节点的数。
节点的右子树只包含 大于 当前节点的数。
所有左子树和右子树自身必须也是二叉搜索树。
bool isValidBST(struct TreeNode* root) {
if(root == NULL)
{
return 0;
}
// 根节点左边的值大于根节点时,不满足二叉查找树
if(root->left != NULL && root->left->val >= root->val)
{
return false;
}
if(root->right != NULL && root->right->val <= root->val)
{
return false;
}
// 以同样方式递归,左右子树的结点
return isValidBST(root->left) && isValidBST(root->right);
}
对以下情况不满足,需要引入上下界进行限定(且需要将上下界限定为long类型)
struct TreeNode {
int val;
struct TreeNode *left;
struct TreeNode *right;
};
// 加入上下界的限定,且为long型
bool isValid(struct TreeNode* root, long min, long max)
{
if(root == NULL)
{
return true;
}
// 根节点左边的值大于根节点时,不满足二叉查找树
if(root->left != NULL && (root->left->val >= root->val)|| root->left->val <= min)
{
return false;
}
if(root->right != NULL && (root->right->val <= root->val || root->right->val >= max))
{
return false;
}
// 以同样方式递归,左右子树的结点
return isValid(root->left, min, root->val) && isValid(root->right, root->val, max);
}
- 求根节点到叶结点数字之和
// 求根节点到叶节点数字之和
struct TreeNode {
int val;
struct TreeNode *left;
struct TreeNode *right;
};
int sumNumbersImpl(struct TreeNode* root, int parent) {
if(root == NULL)
{
return 0; // 到头就返回为0
}
int sum = root->val + parent * 10; // 向后拼接,之前的值*10加上当前值
if(!root->left && !root->right)
{
return sum; // 叶子节点则返回最终结果
}
// 非叶子节点则继续向左右子树进行
return sumNumbersImpl(root->left, sum) + sumNumbersImpl(root->right, sum);
}
int sumNumbers(struct TreeNode* root)
{
return sumNumbersImpl(root, 0);
}
剑指offer051. 二叉树中的最大路径和
路径 被定义为一条从树中任意节点出发,沿父节点-子节点连接,达到任意节点的序列。同一个节点在一条路径序列中 至多出现一次 。该路径 至少包含一个 节点,且不一定经过根节点。
路径和 是路径中各节点值的总和。
给定一个二叉树的根节点 root ,返回其 最大路径和,即所有路径上节点值之和的最大值。
// 二叉树中的最大路径和
struct TreeNode {
int val;
struct TreeNode *left;
struct TreeNode *right;
};
int result = -2147483648;
int max(int a, int b )
{
return a > b ? a : b;
}
int maxValue(struct TreeNode* root)
{
if(root == NULL)
{
return 0;
}
// 先把左右两边或是不走的情况计算,取出值最大的情况
int leftMax = max(maxValue(root->left),0);
int rightMax = max(maxValue(root->right),0);
// 计算最大值情况的结果
int maxTmp = leftMax + rightMax + root->val;
result = max(maxTmp, result); // 更新最大值
// 从上面往下走情况,左或者右, 返回其最大值
return max(leftMax, rightMax) + root->val; // 加上当前节点的值,每次必经过
}
int maxPathSum(struct TreeNode* root){
maxValue(root);
return result;
}