第五章、树和二叉树
一、树
1.树的定义
-
树是包括n(n0)个结点的有限集合D,R是D中元素构成的序偶的集合,若D为空,则R也为空,此时该树为空树。否则,R满足以下特性:
-
(1)有且仅有一个结点r∈D,不存在任何结点v∈D,v≠r,使得<v, r>∈R,称r为树的根。
-
(2)除根r以外的任一结点u∈D,都有且仅有一个结点v ∈D, v≠u,使得<v,u>∈R。
-
图示:
-
树的递归定义:
-
树是包括n(n>=0)个结点的有限集T。若n=0,则该树为空树。
-
在非空树T中,其中有且仅有一个特定的称为根的结点(r) ,其余结点T-{r}
-
划分成m(m≥0)个互不相交的子集T1,T2,…Tm,其中,每个子集Ti都是树,被称为树根r的子树。
-
树是递归数据结构:树的定义中引用了树概念本身。
-
1.1树的基本术语
-
结点(node):树中的元素。根结点和它的子树根(如果存在)之间形成一条边。
-
路径(path):从某个结点沿树中的边可到达另一个结点,则称这两个结点之间存在一条路径。树中任意两点之间都存在路径。
-
双亲(parent):若一个结点有子树,那么该结点称为子树根的双亲。
A、F、B的双亲是E。 C、D的双亲是F。
-
孩子(child):某结点子树的根是该结点的孩子。
E有三个孩子:A、F、B。 D有一个孩子:J。
-
兄弟(sibling):有相同双亲的结点互为兄弟。
-
A、F、B互为兄弟, C和D互为兄弟。
-
后裔(decendent):一个结点的所有子树上的任何结点都是该结点的后裔。
结点C的后裔为:L、M、N。
-
祖先(ancestor):从根结点到某个结点的路径上的所有结点都是该结点的祖先。
-
结点L的祖先为:E、F、C。
-
结点的度(degree):结点拥有的子树数。
结点E的度为3,结点F的度为2, 结点A的度为1,结点G的度为0。
-
叶子(leaf):度为零的结点。
B、G、J、M、N均为叶子结点。
-
分支结点(branch):度不为零的结点。
E、A、F、C等为分支结点。
-
树的度:树中结点的最大的度。
该树的度为3。
-
结点的层次:根结点的层次为1,其余结点的层次等于其双亲结点的层次加1。
结点E的层次为1。 结点M的层次为5。
-
树的高度:树中结点的最大层次。
∵树中结点的最大层次为5。 ∴树的高度为5。
-
无序树:如果树中结点的各子树之间的次序是不重要的,可以交换位置。
-
有序树:如果树中结点的各棵子树看成是从左到右有次序的,则称该树为有序树。
-
森林:是树的有限集合,0个或多个不相交的树组成森林。
若将树中的根去掉,则得到根的子树组成的森林。 若增加一个结点,将森林中各树的根作为新增结点的孩子,则森林即成为树。
二、二叉树的定义
1.定义:二叉树(binary tree)是结点的有限集合,该集合或者为空集,或者是由一个根和两个互不相交的、称为该根的左子树和右子树的二叉树组成。
-
二叉树的5种基本形态:
注意:补充:由n个结点组成的二叉树,其不同形态数量为:
1.1二叉树应用实例
- 示例:设有序表为(21,25,28,33,36,45),现在要在表中查找元素33。
2.二叉树的主要性质
-
性质1:二叉树的第i(i≥1)层上至多有2i-1 个结点。
-
性质2:高度为h的二叉树上至多有2h–1个结点。当h=0时,二叉树为空二叉树。
-
性质3:包含n个元素的二叉树的高度最小为 log2 (n+1),最大为n。
-
性质4:任意一棵二叉树中,若叶结点的个数为n0,度为2的结点的个数为n2,则必有n0=n2+1。
- 拓展:m叉树中度为0~m的结点数目分别是n0、n1…nm ,则有:
n0=n2 +2n3+3n4+…(m-1) nm +1
- 拓展:m叉树中度为0~m的结点数目分别是n0、n1…nm ,则有:
-
性质5:具有n个结点的完全二叉树的高度为【log2 (n+1)】
- 结论推广:具有n个结点的完全m叉树的高度为【logm (n+1)】
-
性质6 假定对一棵有n个结点的完全二叉树中的结点,按从上到下、从左到右的顺序,从0到n-1编号(见下图),设树中某个结点的序号为i,0i<n ,则有以下关系成立:
3.二叉树抽象数据类型
-
ADT BinaryTree{
Data: 二叉树是结点的有限集合,该集合或者为空集,或者是由一个根和两个互不相交的称为该根的左子树和右子树的二叉树组成。
Operations:Create(T): 创建一棵空二叉树。 Destroy(T): 撤销一棵二叉树。 IsEmpty(T):若二叉树空,则返回true,否则返回false。 Clear(T): 移去树中所有结点,成为空二叉树。 Root(T):获取根结点的数据 MakeTree(T, x, left,right):创建一棵二叉树,x为根结点,left为 左子树,right为右子树。 BreakTree(T, left, right):拆分二叉树为三部分,x为根的值, left和right分别为原树的左右子树 PreOrder(r): 先序遍历二叉树。 InOrder(r): 中序遍历二叉树。 PostOrder(r):后序遍历二叉树。
}
3.1二叉树的两种表示
- 顺序表示:结点可以按层次顺序存储在一片连续的存储单元中。根结点保存在编号为0的位置上。
- 链接表示:
-
链接表示代码演示:
typedef struct btNode//树结点 { ElemType element; struct btNode *lChild; struct btNode *rChild; }BTNode; typedef struct binaryTree{//树根 BTNode * root; }BinaryTree;
3.2二叉树基本运算
- 为了方便运算,故加上一个parent指针,指向该结点的双亲结点
typedef char ElemType;
typedef struct btnode{
ElemType element;
struct btnode *lChild;
struct btnode *rChild;
struct btnode *parent;
}BTNode;
- 创建空二叉树:
void Create(BinaryTree *bt)
{
bt->root=NULL;
}
- 创建新结点:创建新节点,该结点的数据为x,ln和rn为该结点的左右孩子
BTNode* NewNode(ElemType x, BTNode *ln, BTNode *rn)
{
BTNode *p=(BTNode *)malloc(sizeof(BTNode));
p->element=x;
p->lChild=ln;
p->rChild=rn;
return p;
}
- 二叉树判空:若二叉树非空,则x为根结点的值返回1,否则返回0
int Root(BinaryTree * bt, ElemType *x)
{
if(bt ->root) { x= &bt->root->element; return 1; }
else return 0;
}
- 构造二叉树bt:根结点的值为e,left和right为根的左右子树
//创建根结点后 左右子树的生成。
void MakeTree(BinaryTree *bt,ElemType e,
BinaryTree *left, BinaryTree *right)
{
if(bt->root||left==right)
return;
bt->root=NewNode(e,left->root,right->root);
left->root=right->root=NULL;
}
- BreakTree(tree,left,right):拆分二叉树bt,将其左孩子作为左子树left的根,将其右孩子作为右子树right的根,释放根结点空间
//重新确定左右子树的根,从左右子树分别再次生成下一个孩子结点。
void BreakTree(BinaryTree *tree, BinaryTree *left,BinaryTree*right) {
if(!tree->root||left->root||right->root)
return;
left->root = tree->root->lChild;
right->root = tree->root->rChild;
free(tree->root);
tree->root = NULL;
}
- 主函数:
void main( ) {
BinaryTree a, b, x, y, z;
Create(&a); Create(&b);
Create(&x); Create(&y);
Create(&z);
MakeTree(&y, 'E', &a, &b);
MakeTree(&z, 'F', &a, &b);
MakeTree(&x, 'C', &y, &z);
MakeTree(&y, 'D', &a, &b);
MakeTree(&z, 'B', &y, &x);
…… //见运行代码}
4.二叉树的遍历递归算法
时间复杂度均为:O(n)
-
先序遍历
-
中序遍历
-
后序遍历
技巧:先序的“先”是根结点在开头;中序的“中”是根结点在中间;后序的“后”是根结点在最后。
示例:先序遍历序列: A B D C E F (A为根结点)
4.1先序遍历
-
⑴ 先序遍历(VLR)
IF 二叉树为空,THEN 空操作返回;
ELSE
①访问根结点;
②先序遍历左子树;
③先序遍历右子树。- 先序遍历序列: A B D C E F
void PreOrder (BTNode *t) //先序遍历递归函数
{ if(t==NULL) return;
printf(“%c”,t->element);//输出结点的值
PreOrder (t->lChild); //先序遍历左子树
PreOrder (t->rChild); //先序遍历右子树
}
void PreOrderTree(BinaryTree * bt)
{ PreOrder(bt->root); //调用先序遍历递归函数
}
4.2中序遍历
-
(2) 中序遍历(LVR)
IF 二叉树为空,THEN 空操作返回;
ELSE
①中序遍历左子树;
② 访问根结点;
③ 中序遍历右子树。- 中序遍历序列:B D A E C F
void InOrder (BTNode *t ) //中序遍历递归函数
{
if(t==NULL) return;
InOrder (t->lChild); //中序遍历左子树
printf (“%c”,t->element); //输出结点的值
InOrder (t->rChild); //中序遍历右子树
}
void InOrderTree(BinaryTree * bt)
{ InOrder(bt->root); //调用中序遍历递归函数
}
4.3后序遍历
-
(3)后序遍历 (LRV)
IF 二叉树为空,THEN 空操作返回;
ELSE①后序遍历左子树; ②后序遍历右子树; ③访问根结点。
- 后序遍历序列:D B E F C A
void PostOrder (BTNode *t) //后序遍历递归函数
{
if(t==NULL) return;
PostOrder (t->lChild); //后序遍历左子树
PostOrder (t->rChild); //后序遍历右子树
printf(“%c”,t->element); //输出结点的值
}
void PostOrderTree(BinaryTree * bt)
{ PostOrder(bt->root); //调用后序遍历递归函数
}
4.4层次遍历
- 层次遍历算法思想:
1.初始化空队列;
2.二叉树根节点入队;
3.如果队列不为空,则:
3.1 取队头元素并访问;
3.2 队头元素出队;
3.3 如果该队头元素有左右孩子,则左右孩子依次入队。
代码演示:
#define QUEUESIZE 100 //队列的容量,根据实际需求调整
void LevelOrderTree(BinaryTree *tree)
{ Queue Q; // Q是用于存储BTNode结点类型指针的队列
BTNode *p;
if(!tree->root) return; //二叉树为空,则直接退出
Create(&Q, QUEUESIZE); // 初始化队列Q,假设容量需求为100
EnQueue(&Q, tree->root); // 根结点进队
while(!IsEmpty(&Q)){ //队列不空则循环
Front(&Q, &p); DeQueue(&Q); // 获取队头结点p
printf(“%c”, p->element); // 访问结点p
if(p->lChild) EnQueue(&Q, p->lChild); // 若p左孩子存在则进队
if(p->rChild) EnQueue(&Q, p->rChild); // 若p右孩子存在也进队
}
Destroy(&Q); //销毁队列,释放队列申请的存储空间
}
4.5相关题型
练习1
已知一棵二叉树结点的先序遍历序列为:F,D,E,B,C,A, 中序遍历序列为 D,B,E,F,A,C, 请画出该二叉树,并写出后序遍历的序列。
练习2
已知一棵二叉树结点的后遍历序列为:A,C,B,D,F,E, 中序遍历序列为 C,A,E,F,B,D, 请画出该二叉树,并写出先序遍历的序列。
5.二叉树遍历的实际应用
5.1计算二叉树的节点数
- 算法思想:
(1).若二叉树为空,则返回结点数为0;
(2).否则,返回“左子树结点数+右子树结点数+1”
int Size(BTNode* t) //定义递归函数
{
if(!t) return 0;
return Size(t->lChild)+Size(t->rChild)+1;
}
int TreeSize(BinaryTree *bt) //调用递归算法求解
{ return Size(bt->root);
}
5.2求二叉树的高度
- 算法思想:
(1).若二叉树为空,则返回高度为0;
(2).否则,计算左子树的高度lh、右子树的高度rh
(3).比较lh和rh,返回较大者+1为整个树的高度
int Depth(BTNode* t) //定义递归函数
{
int lh,rh;
if(!t) return 0;
lh=Depth(t->lChild);
rh=Depth(t->rChild;
return lh>rh?lh+1:rh+1;
}
int TreeDepth(BinaryTree *bt)
{ //调用递归算法求解
return Depth(bt->root);
}
5.3二叉树复制
- 算法思想:
(1).若二叉树为空,则空操作;
(2).否则
2.1 构造并复制新的根结点;
2.2 复制左子树;
2.3 复制右子树。
BTNode* Copy(BTNode *f)
{
if(!f)
return NULL;
BTNode *t = (BTNode *) malloc(sizeof(BTNode));
t-> element = f-> element;
t->lChild = Copy(f->lChild);
t->rChild = Copy(f->rChild);
return t;
}
5.4删除二叉树
- 算法思想:
(1).若二叉树为空,则空操作;
(2).否则
2.1 释放左子树;
2.2 释放右子树;
2.3 释放根结点。
void Clear(BTNode *t)
{ if(t){
Clear(t->lChild);
Clear(t->rChild);
free(t);
}
}
void TreeClear(BinaryTree* bt) // 清空二叉树bt
{ Clear(bt->root);
bt->root = NULL;
}
#include<stdio.h>
#include<stdlib.h>
#include<algorithm>
#define ElemType int
typedef struct btnode {
ElemType element;
struct btnode* lChild;
struct btnode* rChild;
}BTNode;
int GetNodeNum(BTNode* t); //求二叉树结点个数
int GetLeafNum(BTNode* t); //求二叉树叶子结点个数
int GetTreeHeight(BTNode* t); //求二叉树的高度
void SwapSubTree(BTNode* t); //交换二叉树所有子树
//先序遍历构建二叉树
BTNode* PreCreateBt(BTNode* t) {
char ch;
ch = getchar();
if (ch == '#') { //输入为#表示这里建立空二叉树,即遍历算法的空操作
t = NULL;
}
else {
t = (BTNode*)malloc(sizeof(BTNode));
t->element = ch; //构造根结点
t->lChild = PreCreateBt(t->lChild); //构造左子树
t->rChild = PreCreateBt(t->rChild); //构造右子树
}
return t;
}
//先序遍历
void PreorderTransverse(BTNode* t) {
if (t == NULL) {
return;
}
printf("%c", t->element); //打印输出根结点,此处可以定义其他操作
PreorderTransverse(t->lChild); //然后先序遍历左子树
PreorderTransverse(t->rChild); //最后先序遍历右子树
}
//中序遍历
void MediumorderTransverse(BTNode* t) {
if (t == NULL) {
return;
}
MediumorderTransverse(t->lChild); //中序遍历根结点的左子树
printf("%c", t->element); //打印输出根结点,此处可以定义其他操作
MediumorderTransverse(t->rChild); //最后中序遍历根结点的右子树
}
//后序遍历
void PostorderTransverse(BTNode* t) {
if (t == NULL) {
return;
}
PostorderTransverse(t->lChild); //后序遍历根结点的左子树
PostorderTransverse(t->rChild); //然后后序遍历根结点的右子树
printf("%c", t->element); //最后打印输出根结点,此处可以定义其他操作
}
//求二叉树结点个数
int GetNodeNum(BTNode* t) {
if (t == NULL) return 0;
return GetNodeNum(t->lChild) + GetNodeNum(t->rChild) + 1;
}
//求二叉树叶子结点个数
int GetLeafNum(BTNode* t) {
if (t == NULL) return 0;
if ((t->lChild == NULL) && (t->rChild == NULL)) return 1;
return GetLeafNum(t->lChild) + GetLeafNum(t->rChild);
}
//求二叉树的高度
int GetTreeHeight(BTNode* t) {
if (t == NULL) return 0;
else return 1 + std::max(GetTreeHeight(t->lChild), GetTreeHeight(t->rChild));
}
//交换二叉树所有子树
void SwapSubTree(BTNode* t) {
if (t) {
BTNode* temp = t->lChild;
t->lChild = t->rChild;
t->rChild = temp;
SwapSubTree(t->lChild);
SwapSubTree(t->rChild);
}
}
/*
* 测试的树形
1
2 5
3 4 # 6
## ## ##
*/
// 123##4##5#6##
int main() {
BTNode* t = NULL;
// 为二叉树添加元素
printf("请输入先序遍历的二叉树序列\n");
t = PreCreateBt(t);
printf("二叉树的节点数:%d\n", GetNodeNum(t));
printf("二叉树的叶子数:%d\n", GetLeafNum(t));
printf("二叉树的树高度:%d\n", GetTreeHeight(t));
SwapSubTree(t);
printf("\n交换所有左右子树后:\n\n");
printf("先序遍历:\n");
PreorderTransverse(t);
printf("\n\n中序遍历:\n");
MediumorderTransverse(t);
printf("\n\n后序遍历:\n");
PostorderTransverse(t);
printf("\n");
return 0;
}
/*
编写程序实现求二叉树结点个数、叶子结点个数、二叉树的高度以及交换二叉树所有左右子树的操作。
*/
/*以下未完整代码*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
// 定义二叉树结点
typedef struct TreeNode {
char data;
struct TreeNode* lChild;
struct TreeNode* rChild;
} TreeNode;
//树根定义
typedef struct binaryTree {
TreeNode* root;
}BinaryTree;
// 创建二叉树
TreeNode* createTree(char* str, int* index) {
if (str[*index] == '\0') {
return NULL;
}
TreeNode* node = (TreeNode*)malloc(sizeof(TreeNode));
node->data = str[*index];
node->lChild = NULL;
node->rChild = NULL;
if (str[++(*index)] == '(') {
node->lChild = createTree(str, index);
}
if (str[++(*index)] == '(') {
node->rChild = createTree(str, index);
}
++(*index);
return node;
}
// 先序遍历
void preOrder(TreeNode* root) {
if (root == NULL) {
return;
}
printf("%c ", root->data);
preOrder(root->lChild);
preOrder(root->rChild);
}
// 中序遍历
void inOrder(TreeNode* root) {
if (root == NULL) {
return;
}
inOrder(root->lChild);
printf("%c ", root->data);
inOrder(root->rChild);
}
// 后序遍历
void postOrder(TreeNode* root) {
if (root == NULL) {
return;
}
postOrder(root->lChild);
postOrder(root->rChild);
printf("%c ", root->data);
}
// 求结点个数
int countNodes(TreeNode* root) {
if (root == NULL) {
return 0;
}
return 1 + countNodes(root->lChild) + countNodes(root->rChild);
}
// 求树的高度
int height(TreeNode* root) {
if (root == NULL) {
return 0;
}
int leftHeight = height(root->lChild);
int rightHeight = height(root->rChild);
return 1 + (leftHeight > rightHeight ? leftHeight : rightHeight);
}
// 求叶子结点数
int countLeaves(TreeNode* root) {
if (root == NULL) {
return 0;
}
else if (root->lChild == NULL && root->rChild == NULL) {
return 1;
}
else {
return countLeaves(root->lChild) + countLeaves(root->rChild);
}
}
// 交换二叉树左右子树
void swap(TreeNode* root) {
if (root == NULL) {
return;
}
TreeNode* temp = root->lChild;
root->lChild = root->rChild;
root->rChild = temp;
swap(root->lChild);
swap(root->rChild);
}
6.二叉树非递归算法
-
算法思想:
- 创建一个空栈和一个指向根节点的指针。
- 将指向根节点的指针压入栈中。
- 当栈不为空时,执行以下操作:
- 弹出栈顶元素并访问该节点。
- 如果该节点的右子节点非空,则将右子节点压入栈中。
- 如果该节点的左子节点非空,则将左子节点压入栈中。
- 重复步骤3直到栈为空。
//先序
struct TreeNode {
int val;
struct TreeNode *left;
struct TreeNode *right;
};
void preorderTraversal(struct TreeNode* root) {
if (root == NULL) return;
struct TreeNode** stack = (struct TreeNode**)malloc(sizeof(struct TreeNode*) * 1000);
int top = -1;
stack[++top] = root;
while (top >= 0) {
struct TreeNode* node = stack[top--];
printf("%d ", node->val);
if (node->right != NULL) {
stack[++top] = node->right;
}
if (node->left != NULL) {
stack[++top] = node->left;
}
}
free(stack);
}
-
算法思想:
- 定义一个指向二叉树节点的指针cur,初始值为根节点。
- 定义一个栈,用于存储待访问的节点。
- 当cur不为空或者栈不为空时,进行如下操作:
- 如果cur不为空,则将cur入栈,并将cur指向其左子节点。
- 如果cur为空,则从栈中弹出一个节点并访问它,然后将cur指向该节点的右子节点。
//中序
#include <stdio.h>
// 二叉树结构体
struct TreeNode {
int val;
struct TreeNode *left;
struct TreeNode *right;
};
// 栈结构体
struct Stack {
struct TreeNode **data;
int top;
};
// 初始化栈
void initStack(struct Stack *stack, int size) {
stack->data = (struct TreeNode **)malloc(size * sizeof(struct TreeNode *));
stack->top = -1;
}
// 判断栈是否为空
int isEmpty(struct Stack *stack) {
return stack->top == -1;
}
// 入栈
void push(struct Stack *stack, struct TreeNode *node) {
stack->data[++stack->top] = node;
}
// 出栈
struct TreeNode *pop(struct Stack *stack) {
return stack->data[stack->top--];
}
// 二叉树的中序遍历(非递归)
int* inorderTraversal(struct TreeNode* root, int* returnSize){
// 初始化栈
struct Stack stack;
initStack(&stack, 100);
// 初始化返回数组
int *result = (int *)malloc(100 * sizeof(int));
int index = 0;
// 定义指向当前节点的指针cur
struct TreeNode *cur = root;
while (cur != NULL || !isEmpty(&stack)) {
if (cur != NULL) {
push(&stack, cur);
cur = cur->left;
} else {
cur = pop(&stack);
result[index++] = cur->val;
cur = cur->right;
}
}
// 设置返回数组的大小
*returnSize = index;
return result;
}
// 测试代码
int main() {
// 创建二叉树
struct TreeNode *root = (struct TreeNode *)malloc(sizeof(struct TreeNode));
root->val = 1;
root->left = NULL;
struct TreeNode *node1 = (struct TreeNode *)malloc(sizeof(struct TreeNode));
node1->val = 2;
node1->left = NULL;
struct TreeNode *node2 = (struct TreeNode *)malloc(sizeof(struct TreeNode));
node2->val = 3;
node2->left = NULL;
root->right = node1;
node1->left = node2;
// 中序遍历二叉树
int size;
int *result = inorderTraversal(root, &size);
// 输出结果
printf("中序遍历结果:");
for (int i = 0; i < size; i++) {
printf("%d ", result[i]);
}
printf("\n");
return 0;
}
-
算法思想:
- 定义一个栈,并将根节点入栈。
- 定义一个指针p,指向根节点。
- 定义一个变量lastvisit,用于记录上一个被访问的节点。
- 当栈不为空时,执行以下操作:
- 将栈顶元素出栈,并将其赋值给p。
- 如果p的左右子节点都为空,或者p的左右子节点中有一个是上一个被访问的节点,则访问p,并将lastvisit设置为p。
- 否则,依次将p的右子节点和左子节点入栈。
//后序
#include <stdio.h>
#define MAX_SIZE 100
typedef struct TreeNode {
int val;
struct TreeNode* left;
struct TreeNode* right;
} TreeNode;
void postorderTraversal(TreeNode* root) {
if (root == NULL) {
return;
}
TreeNode* stack[MAX_SIZE];
int top = -1;
TreeNode* p = root;
TreeNode* lastvisit = NULL;
while (p != NULL || top != -1) {
while (p != NULL) {
stack[++top] = p;
p = p->left;
}
p = stack[top];
if (p->right == NULL || p->right == lastvisit) {
printf("%d ", p->val);
top--;
lastvisit = p;
p = NULL;
} else {
p = p->right;
}
}
}
int main() {
TreeNode* root = (TreeNode*)malloc(sizeof(TreeNode));
root->val = 1;
root->left = (TreeNode*)malloc(sizeof(TreeNode));
root->left->val = 2;
root->left->left = NULL;
root->left->right = NULL;
root->right = (TreeNode*)malloc(sizeof(TreeNode));
root->right->val = 3;
root->right->left = NULL;
root->right->right = NULL;
printf("Postorder Traversal: ");
postorderTraversal(root);
return 0;
}
7.线索二叉树
- 线索二叉树:可以保存遍历过程中结点之间的前驱和后继关系信息,充分利用二叉链表的n+1个空的指针域。
lTag | lChild | element | rChild | rTag |
---|
- 通过二叉树的先、中、后序的结点排列顺序,构造出下图的线索二叉树:
三、树与森林
1.树(Tree)转换为 二叉树(BTree)算法
- 算法思想:
⑴ 连线:将树中的兄弟用线连接;
⑵ 去线:保留最左孩子的连线,去掉其余孩子的连线;
⑶ 整形:调整为习惯的二叉树形(水平右斜,垂直左斜)。
- 转换方法图示:
/*代码思想:
1. 使用任何遍历方法(先序、中序或后序)遍历给定的树。
2. 对于遍历过程中遇到的每个节点,创建一个新的二叉树节点,并使其成为当前节点的左子节点。
3. 将根植于当前节点右侧子节点的子树移动到新创建的节点的右侧子树。
4. 将当前节点的右子节点设置为 NULL。
*/
#include<stdio.h>
typedef struct TreeNode {
int data;
struct TreeNode* left;
struct TreeNode* right;
} TreeNode;
void convertToBTree(TreeNode* root) {
if (root == NULL) {
return;
}
convertToBTree(root->left);
TreeNode* temp = root->right;
root->right = malloc(sizeof(TreeNode));
root->right->data = temp->data;
root->right->left = temp->left;
root->right->right = temp->right;
temp->left = NULL;
temp->right = NULL;
convertToBTree(root->right);
}
int main() {
// Create a sample tree
TreeNode* root = malloc(sizeof(TreeNode));
root->data = 1;
root->left = malloc(sizeof(TreeNode));
root->left->data = 2;
root->right = malloc(sizeof(TreeNode));
root->right->data = 3;
root->left->left = malloc(sizeof(TreeNode));
root->left->left->data = 4;
root->left->right = malloc(sizeof(TreeNode));
root->left->right->data = 5;
// Convert to binary tree
convertToBTree(root);
return 0;
}
- 转换注意:
⑴ 左孩子右兄弟。(树→二叉树) 二叉树中有两个结点X和Y,且X是Y的双亲
二叉树中 | 树中 |
---|---|
Y是X左孩子 | Y是X孩子 |
Y是X右孩子 | Y是X兄弟 |
⑵ 树的根结点没有兄弟,所以树对应的二叉树根结点没有右子树
2.森林(Forest)转换成二叉树(BTree)
- 方法思想:
(1)若F为空,则B为空二叉树
(2)若F非空,则B的根是F中第一棵子树T1的根R1,B的左子树是T1的子树森林(T11,T12,…,T1m)所对应的二叉树,B的右子树是森林(T2,…,Tn)所对应的二叉树
最后所形成的二叉树就是森林所对应的二叉树。
- 方法图示:
/*代码思想:
1.如果树的根节点为空或没有子节点,则它已经是二叉树。因此,我们可以返回根节点本身。
2. 如果根节点有两个以上的子节点,那么我们需要删除其中一些子节点以使其成为二叉树。我们可以选择任意两个子节点,并使它们成为根节点的左子节点和右子节点。我们可以抛弃所有其他孩子。
3. 如果根节点只有一个子节点,那么我们需要通过将子节点设置为根节点的右子节点并将其左子节点设置为 null 来使其成为二叉树。
4. 递归地将左右子树转换为二叉树。
*/
#include<stdio.h>
struct TreeNode {
int val;
struct TreeNode *left;
struct TreeNode *right;
};
struct TreeNode* convertToBinaryTree(struct TreeNode* root) {
if (root == NULL || (root->left == NULL && root->right == NULL)) {
return root;
}
if (root->left != NULL && root->right != NULL) {
root->left = convertToBinaryTree(root->left);
root->right = convertToBinaryTree(root->right);
return root;
}
if (root->left != NULL) {
struct TreeNode* newRoot = root->left;
root->left = convertToBinaryTree(newRoot->right);
newRoot->right = convertToBinaryTree(root);
return newRoot;
}
if (root->right != NULL) {
struct TreeNode* newRoot = root->right;
root->right = convertToBinaryTree(newRoot->left);
newRoot->left = convertToBinaryTree(root);
return newRoot;
}
}
int main() {
// Create a forest with three trees
struct TreeNode* tree1 = (struct TreeNode*) malloc(sizeof(struct TreeNode));
tree1->val = 1;
tree1->left = (struct TreeNode*) malloc(sizeof(struct TreeNode));
tree1->left->val = 2;
tree1->left->left = NULL;
tree1->left->right = NULL;
tree1->right = (struct TreeNode*) malloc(sizeof(struct TreeNode));
tree1->right->val = 3;
tree1->right->left = NULL;
tree1->right->right = NULL;
struct TreeNode* tree2 = (struct TreeNode*) malloc(sizeof(struct TreeNode));
tree2->val = 4;
tree2->left = (struct TreeNode*) malloc(sizeof(struct TreeNode));
tree2->left->val = 5;
tree2->left->left = NULL;
tree2->left->right = NULL;
tree2->right = (struct TreeNode*) malloc(sizeof(struct TreeNode));
tree2->right->val = 6;
tree2->right->left = NULL;
tree2->right->right = NULL;
struct TreeNode* tree3 = (struct TreeNode*) malloc(sizeof(struct TreeNode));
tree3->val = 7;
tree3->left = (struct TreeNode*) malloc(sizeof(struct TreeNode));
tree3->left->val = 8;
tree3->left->left = NULL;
tree3->left->right = NULL;
tree3->right = (struct TreeNode*) malloc(sizeof(struct TreeNode));
tree3->right->val = 9;
tree3->right->left = NULL;
tree3->right->right = NULL;
// Combine the trees into a forest
struct TreeNode* forest = (struct TreeNode*) malloc(sizeof(struct TreeNode));
forest->val = 0;
forest->left = tree1;
forest->right = (struct TreeNode*) malloc(sizeof(struct TreeNode));
forest->right->val = 0;
forest->right->left = tree2;
forest->right->right = (struct TreeNode*) malloc(sizeof(struct TreeNode));
forest->right->right->val = 0;
forest->right->right->left = tree3;
forest->right->right->right = NULL;
// Convert the forest into a binary tree
struct TreeNode* binaryTree = convertToBinaryTree(forest);
return 0;
}
3.二叉树转换成森林
- 方法图示:连线、去线、整形
/*算法思想:
1. 为二叉树节点定义一个结构,其中包含数据和指向其左右子节点的指针。
2. 为林节点定义一个结构,其中包含数据和指向其子节点的指针。
3. 创建一个函数以将元素插入二叉树中。
*/
#include<stdio.h>
struct TreeNode {
int data;
struct TreeNode *left;
struct TreeNode *right;
};
struct ForestNode {
int data;
struct ForestNode *next;
struct TreeNode *child;
};
void insert(struct TreeNode **root, int data) {
if (*root == NULL) {
*root = (struct TreeNode*)malloc(sizeof(struct TreeNode));
(*root)->data = data;
(*root)->left = NULL;
(*root)->right = NULL;
} else {
if (data < (*root)->data) {
insert(&((*root)->left), data);
} else {
insert(&((*root)->right), data);
}
}
}
void convertToForest(struct TreeNode *root, struct ForestNode **forest) {
if (root == NULL) {
return;
}
// 创建一个新的森林节点
struct ForestNode *newNode = (struct ForestNode*)malloc(sizeof(struct ForestNode));
newNode->data = root->data;
newNode->next = NULL;
newNode->child = NULL;
// 把新的森林结点添加到原来的森林中
if (*forest == NULL) {
*forest = newNode;
} else {
struct ForestNode *temp = *forest;
while (temp->next != NULL) {
temp = temp->next;
}
temp->next = newNode;
}
// 将左子树转换为森林
convertToForest(root->left, forest);
// 将右子树转换为森林转换为森林
convertToForest(root->right, forest);
}
int main() {
struct TreeNode *root = NULL;
struct ForestNode *forest = NULL;
// 将元素插入二叉树
insert(&root, 5);
insert(&root, 3);
insert(&root, 7);
insert(&root, 1);
insert(&root, 4);
insert(&root, 6);
insert(&root, 8);
// 将二叉树转换为森林
convertToForest(root, &forest);
return 0;
}
4.关于树、二叉树、森林的表示方法
4.1多重链表表示法(链接结构)
4.2孩子兄弟表示法
4.3双亲表示法(顺序结构)
4.4三重链表表示法(链接结构)
4.5带右链的先序表示法(顺序结构
ITag=0,有孩子结点;ITag=1,没有孩子结点;
5.树和森林的遍历
5.1按深度方向遍历
(1)先序遍历:若森林为空,则遍历结束。否则、
a) 访问第一棵树的根;
b) 按先序遍历第一棵树的根结点的子树组成的森林;
c) 按先序遍历其余树组成的森林。
先序遍历的结果是:A B C K D E H F J G
(2)中序遍历:若森林为空,则遍历结束,否则 a) 按中序遍历第一棵树的根结点的子树组成的森林;
b) 访问第一棵树的根;
c) 按中序遍历其余树组成的森林。
中序遍历的结果是:B K C A H E J F G D
(3)后序遍历:若森林为空,则遍历结束,否则 a) 按后序遍历第一棵树的根结点的子树组成的森林;
b) 按后序遍历除了第一棵树以外的树构成的森林;
c) 访问第一棵树的根。
后序遍历的结果是:KCBHJGFEDA
四、堆和优先权队列
-
前提:使得新元素加入该数据结构的次序无关紧要,但每次取出的元素具有最高优先级。
-
解决思路:
-
新元素插在表的尾部(或头部),假设用元素值表示优先级
-
取优先级最高的元素时,扫描线性表中的每一个元素,查找优先级最高的元素,并从线性表中取出。
-
1.堆的定义:
-
从逻辑结构上看,一个大小为n的堆是一棵包含n个结点的完全二叉树,根结点称为堆顶。
-
最小堆:树中每个结点的数据都小于或等于其孩子结点。(从上到下,从左到右减小的排列)
-
最大堆:树中每个结点的数据都大于或等于其孩子结点。(从上到下,从左到右增大的排列)
-
2.建堆的运算
- 基本思想:从最后面的非叶结点开始,一直到根结点,对不满足最小堆要求的结点依次执行向下调整。
-
最后一个叶子结点的双亲的位置:【(n−2)/2】
- 从(n−2)/2到0下标不符合要求的结点依次执行向下调整
void AdjustDown(ElemType heap[], int current, int border)
{ //3个形参分别表示待调整数组、当前调整下标、调整区域最后元素的下标
int p=current; int minChild; ElemType temp;
while(2*p+1<=border) { //若p不是叶结点,则执行调整
if((2*p+2<=border)&&(heap[2*p+1]>heap[2*p+2]))
minChild=2*p+2; //右孩子存在且较小,minChild指右
else
minChild=2*p+1; //右孩子不存在或较大,minChild指左
if(heap[p]<=heap[minChild])
break; //若当前结点不大于其最小的孩子,则调整结束
else{ //否则将p和其最小孩子交换
temp=heap[p];
heap[p]=heap[minChild];
heap[minChild]=temp;
p=minChild; //设置下轮循环待考查的元素的位置,刚才例第3步
}
}
}
void CreateHeap(ElemType heap[],int n) //建堆算法
{
int i;
for(i=(n-2)/2;i>-1;i--) //从(n−2)/2到0下标
AdjustDown(heap,i,n-1); //依次执行向下调整
}
3.优先权队列
-
优先权队列:元素进入数据结构的次序不重要,每次取出元素按照最高优先级的元素取出。
-
元素按优先权的高低顺序依次出队,而不是按元素进入队列的先后顺序出队;
-
用元素进入队列时间的长短表示元素的优先权,时间越长优先权越高;
-
3.1优先权队列实现方法
4.优先权队列演示代码
4.1优先权队列ADT
ADT PriorityQueue {
数据:
n≥0个元素的最小堆或最大堆。
运算:
Create(PQ,mSize):建立一个空优先权队列;
Destroy(PQ):销毁一个优先权队列,释放其占用的内存空间;
IsEmpty(PQ):若优先权队列空,则返回TRUE;否则返回FALS;
IsFull(PQ):若优先权队列满,则返回TRUE;否则返回FALSE;
Size(PQ):获取当前优先权队列中元素的总数量;
Append(PQ,x):将新元素x加入优先权队列;//需要调用AdjustUp
Serve(PQ,x):从优先权队列中取出优先权最高的元素,并通过x返回}
4.2优先权队列存储表示
//优先权队列的顺序存储结构
typedef struct priorityQueue{
ElemType *elements; //存储元素的数组
int n; //优先权队列中包含的元素的数量
int maxSize; //优先权队列的容量
}PriorityQueue;
4.3向上调整方法AdjustUp
-
算法思想:
-
设新元素插入在最小堆的元素序列的尾部;
-
从新插入元素开始,自底向上,与父结点元素进行大小比较:若父结点元素大于孩子结点元素,则进行交换调整,直到其父节点元素不大于孩子结点元素,或者新元素已到达堆顶位置为止。
-
- 向上调整演示代码
//heap:存储元素的数组;current:待调整的数组下标
//满足最小堆条件:满足heap[i]≤heap[2i+1]和heap[i]≤heap[2i+2] ,i=0,1,…,【(current-2)/2】
void AdjustUp(ElemType heap[], int current)
{ //两个形参分别是存放堆的数组,新插入元素的下标
int p=current; //让p指向新元素位置
ElemType temp;
while(p>0){ //当p指向根结点时循环结束
if(heap[p]<heap[(p-1)/2]){//若p元素小于其父节点交换
temp=heap[p];
heap[p]=heap[(p-1)/2];
heap[(p-1)/2]=temp;
p=(p-1)/2; //将p向上移动至父节点位置
}
else break; //若p指向的元素不小于其父节点,则调整完毕
}
} //AdjustUp的时间复杂度O(log2n)
4.4.优先权队列全部代码
//相关类型的定义:
typedef int ElemType;
typedef int BOOL;
typedef struct
{
ElemType* element;
int n;
int maxSize;
}PriorityQueue;
//创建一个空的优先权队列
void CreatePQ(PriorityQueue *PQ, int mSize)
{
PQ->maxSize=mSize;
PQ->n=0;
PQ->elements=(ElemType*)malloc(mSize*sizeof(ElemType));
}
//销毁一个优先权队列,释放其占用的内存空间
void Destroy(PriorityQueue *PQ)
{
free(PQ->elements);
PQ->n=0;
PQ->maxSize=0;
}
//判断优先权队列是否为空
BOOL IsEmpty(PriorityQueue *PQ)
{
if(PQ->n==0) return TRUE;
else return FALSE;
}
//判断优先权队列是否已满
BOOL IsFull(PriorityQueue *PQ)
{
if(PQ->n==PQ->maxSize) return TRUE;
else return FALSE;
}
//获取优先权队列的元素总数量
int Size(PriorityQueue *PQ)
{
return PQ->n;
}
//在优先权队列中增加一个新元素x
void Append(PriorityQueue *PQ, ElemType x)
{
if(IsFull(PQ)) return;
PQ->elements[PQ->n]=x;
PQ->n++;
AdjustUp(PQ->elements, PQ->n-1);
}
//取出优先权最高的元素由参数x返回,并在优先权队列中删除该元素
void Serve(PriorityQueue *PQ, ElemType *x)
{
if(IsEmpty(PQ)) return;// 若优先权队列为空获取元素失败,直接返回
*x=PQ->elements[0]; // 获取堆顶元素,并赋值给*x
PQ->n--; // 元素数量计数器减1
PQ->elements[0]=PQ->elements[PQ->n];
AdjustDown(PQ->elements, 0, PQ->n-1);// 对新的堆顶元素执行向下调整
}
完整可运行代码:(可修改成用户输入优先权队列)
#include <stdio.h>
#include <stdlib.h>
#define FALSE 0
#define TRUE 1
typedef int ElemType;
typedef int BOOL;
typedef struct {
ElemType* elements;
int n;
int maxSize;
} PriorityQueue;
void AdjustDown(ElemType elements[], int root, int n) {
int parent = root;
int child = 2 * parent + 1;
ElemType temp = elements[parent];
while (child <= n) {
// 选择左右孩子中优先级较高的一个
if (child < n && elements[child] < elements[child + 1]) {
child++;
}
// 如果根节点的优先级高于或等于其最高优先级的子节点,则无需继续调整
if (temp >= elements[child]) {
break;
}
// 将子节点上移
elements[parent] = elements[child];
parent = child;
child = 2 * parent + 1;
}
// 将被调整的节点放到最终位置
elements[parent] = temp;
}
void AdjustUp(ElemType elements[], int child) {
int parent = (child - 1) / 2;
ElemType temp = elements[child];
while (child > 0 && temp > elements[parent]) {
// 将父节点下移
elements[child] = elements[parent];
child = parent;
parent = (child - 1) / 2;
}
// 将被调整的节点放到最终位置
elements[child] = temp;
}
//创建一个空的优先权队列
void CreatePQ(PriorityQueue* PQ, int mSize)
{
PQ->maxSize = mSize;
PQ->n = 0;
PQ->elements = (ElemType*)malloc(mSize * sizeof(ElemType));
}
//销毁一个优先权队列,释放其占用的内存空间
void Destroy(PriorityQueue* PQ)
{
free(PQ->elements);
PQ->n = 0;
PQ->maxSize = 0;
}
//判断优先权队列是否为空
BOOL IsEmpty(PriorityQueue* PQ)
{
if (PQ->n == 0) return TRUE;
else return FALSE;
}
//判断优先权队列是否已满
BOOL IsFull(PriorityQueue* PQ)
{
if (PQ->n == PQ->maxSize) return TRUE;
else return FALSE;
}
//获取优先权队列的元素总数量
int Size(PriorityQueue* PQ)
{
return PQ->n;
}
//在优先权队列中增加一个新元素x
void Append(PriorityQueue* PQ, ElemType x)
{
if (IsFull(PQ)) return;
PQ->elements[PQ->n] = x;
PQ->n++;
AdjustUp(PQ->elements, PQ->n - 1);
}
//取出优先权最高的元素由参数x返回,并在优先权队列中删除该元素
void Serve(PriorityQueue* PQ, ElemType* x)
{
if (IsEmpty(PQ)) return;// 若优先权队列为空获取元素失败,直接返回
*x = PQ->elements[0]; // 获取堆顶元素,并赋值给*x
PQ->n--; // 元素数量计数器减1
PQ->elements[0] = PQ->elements[PQ->n];
AdjustDown(PQ->elements, 0, PQ->n - 1);// 对新的堆顶元素执行向下调整
}
void CreatePQ(PriorityQueue* PQ, int mSize);
void Destroy(PriorityQueue* PQ);
BOOL IsEmpty(PriorityQueue* PQ);
BOOL IsFull(PriorityQueue* PQ);
int Size(PriorityQueue* PQ);
void Append(PriorityQueue* PQ, ElemType x);
void Serve(PriorityQueue* PQ, ElemType* x);
int main() {
PriorityQueue PQ;
CreatePQ(&PQ, 10); // 创建一个最大容量为10的空优先队列
Append(&PQ, 5);
Append(&PQ, 10);
Append(&PQ, 3);
Append(&PQ, 8);
Append(&PQ, 1);
int size = Size(&PQ);
printf("优先队列中的元素数量为: %d\n", size);
int x;
Serve(&PQ, &x);
printf("优先队列中的最高优先级元素为: %d\n", x);
Destroy(&PQ); // 销毁优先队列并释放内存空间
return 0;
}
示例:
五、哈夫曼树及其编码
1.前件知识
1.1扩充二叉树
- 除叶子结点外,其余结点均有两个孩子。
1.2定义
-
路径长度:指从根结点到该结点的路径上所包括的边的数目。
-
树的内路径长度:除叶子结点外,从根到树中其他所有结点的路径长度之和。
-
树的外路径长度:指从根到树中所有叶子结点的路径长度之和。
-
叶子结点的加权路径长度:从根到该叶子的路径长度与叶子的权的乘积。
-
树的带权路径长度:树中所有叶子结点的加权路径长度之和,记作:(m是叶子结点的个数,wk是第k个叶子结点的权,lk是该叶子结点的路径长度。)
WPL计算示例:
2.哈夫曼树
2.1算法描述:
⑴ 根据给定的一组权值{w1,w2 ,…,wn}生成一个森林F={T1,T2,…,Tn},其中每棵二叉树Ti只有一个权值为wi的根结点,其左、右子树均为空。
⑵ 从F中选择两棵根结点权值最小的树作为新树根的左、右子树,新树根的权值是左、右子树根结点的权值之和。(一般约定左子树根权值小)
⑶ 从F中删除这两棵树,另将新二叉树加入F中。
⑷ 重复⑵和⑶,直到F中只包含一棵树为止。该树即为最终求得的哈夫曼树。
2.2构造哈夫曼树
2.3哈夫曼树编码
3.哈夫曼树代码演示
#include<stdio.h>
#include<stdlib.h>
#define N 30 //叶子结点最大值
#define M 2*N-1 //所有结点最大值
typedef struct{
int weight;
int parent;
int Lchild;
int Rchild;
int flag;
}HTNode,HuffmanTree[M+1]; //0号单元不用
int select(HuffmanTree ht,int n);
void InitHuffmanTree(HuffmanTree ht,int n);
void crtHuffmanTree(HuffmanTree ht,int n);
void printHuffmanTree(HuffmanTree ht,int n);
void InitHuffmanTree(HuffmanTree ht,int n)//初始哈夫曼树
{
for(int i=1;i<=n;i++)//初始化叶子节点
{
ht[i].Lchild=0;
ht[i].Rchild=0;
ht[i].weight=0;
ht[i].parent=0;
ht[i].flag=0;
scanf("%d",&ht[i].weight);
}
int m=2*n-1;
for(int i=n+1;i<=m;i++)//初始化非叶子节点
{
ht[i].Lchild=0;
ht[i].Rchild=0;
ht[i].weight=0;
ht[i].parent=0;
ht[i].flag=0;
}
}
void crtHuffmanTree(HuffmanTree ht,int n)//构建哈夫曼树
{
for(int i=n+1;i<=(2*n-1);i++)
{
int s1=select(ht,i-1);
int s2=select(ht,i-1);
ht[i].weight = ht[s1].weight+ht[s2].weight;
ht[s1].parent=i;
ht[s2].parent=i;
ht[i].Lchild=s1;
ht[i].Rchild=s2;
}
}
int select(HuffmanTree ht,int n)//选择最小权值的结点下标
{
int i,temp,min;
for(i=1;i<=n;i++)//设置初始下标和权值
{
if(ht[i].flag==0)
{
temp = ht[i].weight;//初始权值
min=i;//初始下标
break;
}
}
for(i=1;i<=n;i++)
{
if(ht[i].flag==0&&temp>ht[i].weight)
{
temp=ht[i].weight;
min = i;
}
}
ht[min].flag=1;
return min;
}
void printHuffmanTree(HuffmanTree ht,int n)//打印哈夫曼树
{ printf("结点 weigh parent Lchild Rchild\n");
for(int i=1;i<=n;i++)
{
printf("%d\t%d\t%d\t%d\t%d\n",i,ht[i].weight,ht[i].parent,ht[i].Lchild,ht[i].Rchild);
}
printf("\n");
}
int main()
{
HuffmanTree ht;
int n;//n为所需的结点数
printf("输入所需创建的结点数为:");
scanf("%d",&n);
InitHuffmanTree(ht,n);//初始化
printf("初始的哈夫曼树为\n");
printHuffmanTree(ht,2*n-1);//打印初始的哈夫曼树
crtHuffmanTree(ht,n);//构建哈夫曼树
printf("构建后的哈夫曼树为\n");
printHuffmanTree(ht,2*n-1);//打印构建的哈夫曼树
return 0;
}
注意:
-
已知w[0,…,n-1]中存储n个权值,构造哈夫曼树的函数CrtHuffmanTree的执行步骤如下:
首先构造n棵哈夫曼树,每棵树只有一个权值为w[i]的根结点,将它们逐一加入优先权队列。
从优先权队列中取出两棵根节点权值最小的哈夫曼树x和y。以它们根的权值之和为根的权值,x和y为左、右子树构造一棵新哈夫曼树,并将新哈夫曼树加入优先权队列。
重复上一步骤n-1次,直到优先权队列中只剩下唯一的一棵哈夫曼树,此即为构造完成的哈夫曼树。