树、哈夫曼树以及二叉树的各种操作
文章目录
- 树
- 树的表达方式
- 树的定义
- 树的概念
- 树的存储结构
- 双亲表示法
- 代码实现
- 孩子表示法
- 代码实现
- 孩子兄弟表示法
- 代码实现(无序树)找不了父亲,只能找孩子
- 二叉树
- 二叉树定义
- 满二叉树
- 完全二叉树
- 二叉树的性质
- 二叉树的顺序存储
- 代码实现
- 二叉树的链式存储
- 代码实现
- 二叉树的遍历
- 遍历思想
- 广度遍历
- 代码实现
- 深度遍历(递归版)
- 先序遍历
- 中序遍历
- 后序遍历
- 深度遍历综合代码
- 深度遍历(非递归版)
- 前序
- 中序
- 后序
- 根据遍历结果重构二叉树
- 线索二叉树
- 背景介绍
- 线索化
- 线索化的改进
- 线索化的优势
- 在中序线索化的二叉树上找前驱和后继
- 代码实现(1和2)
- 高级搜索树
- 二叉搜索树(BST)
- 定义和性质
- 构建二叉排序树
- 二叉搜索树的优势
- 二叉搜索树的实现
- 二叉搜索树的插入操作
- 代码实现(包括递归与非递归版)
- 查找某一节点下的最⼤或最⼩值
- 二叉搜索树的删除操作
- 代码示例(递归版本)
- 二叉搜索树缺点
- 二叉平衡树(AVL)
- 平衡因⼦
- 平衡树失衡情况分析
- 代码实现(插入)
- 代码实现(删除)
- 并查集
- 树的表现形式
- 查找
- 合并
- 路径压缩
- 代码实现
- 哈夫曼树
- 概念⼀:什么是结点的路径⻓度
- 概念⼆:什么是树的路径⻓度
- 概念三:什么是结点的带权路径⻓度?
- **概念四:什么是树的带权路径⻓度** WPL
- 应⽤场景
- 哈夫曼树的构造过程
- 定⻓编码的缺陷
- 变⻓编码
- 前缀属性
- 前缀码
- 问题
- 代码示例
树
树的表达方式
集合中的元素关系呈现出一对多的情况
树的定义
- 树(Tree)是n(n≥0)个节点的有限集合T,它满足两个条件 :
-
有且仅有一个特定的称为根(Root)的节点
-
其余的节点可以分为m(m≥0)个互不相交的有限集合T1、T2、……、Tm,其中每一个集合又是一棵树,并称为其根的子树(Subtree)。
- 树的定义具有递归性,即“树中还有树”。
树的概念
-
结点:使⽤树结构存储的每一个数据元素都被称为“结点”。例如图中的A就是一个结点。
-
根结点:有一个特殊的结点,这个结点没有前驱,我们将这种结点称之为根结点。
-
⽗结点(双亲结点)、子结点和兄弟结点:对于ABCD四个结点来说,A就是BCD的⽗结点,也称之为双亲结点。⽽BCD都是A的子结点,也称之为孩子结点。对于BCD来说,因为他们都有同一个爹,所以它们互相称之为兄弟结点。
-
叶子结点:如果一个结点没有任何子结点,那么此结点就称之为叶子结点。
-
结点的度:结点拥有的子树的个数,就称之为结点的度。
-
树的度:在各个结点当中,度的最⼤值。为树的度。
-
n叉树:度为n的树,在n叉树中,每个节点最多有n个(直接孩子)
-
树的深度或者⾼度:结点的层次从根结点开始定义起,根为第一层,根的孩子为第二层。依次类推。
-
结点A的度:3 结点B的度:2 结点M的度:0
-
结点A的孩子:B C D 结点B的孩子:E F
-
树的度:3 树的深度:4
-
叶子结点:K L F G M I J
-
结点A是结点F的祖先
-
结点F是结点K的叔叔结点
树的存储结构
双亲表示法
树中的数据,数据之间的父子关系,指明某节点的父亲是谁
双亲表示法采⽤顺序表(也就是数组)存储普通树,其实现的核心思想是:顺序存储各个节点的同时,给各节点附加一个记录其⽗节点位置的变量。
根节点没有⽗节点(⽗节点又称为双亲节点),因此根节点记录⽗节点位置的变量通常置为 -1。
-
利⽤顺序表存储,表元素由数据和⽗结点构成
-
特点分析:
- 根结点没有双亲,所以位置域设置为-1
- 知道一个结点,找他的⽗结点,非常容易,O(1)级
- 找孩子节点,必须遍历整个表
代码实现
#include <iostream>
using namespace std;
struct {char data; //存储数据int father_i; //存储父亲下标
}tree[100];
int s; //全局变量,表示树节点个数
//初始化
void initial(char root){tree[0].data = root;tree[0].father_i = -1;s++;
}
//查找给定的节点数据的下标
int find_index(char x){for(int i = 0; i< s;i++){if(tree[i].data == x){return i;}}return -1; //没找到返回-1
}
//增加
void add(char x, char father){tree[s].data = x;tree[s].father_i = find_index(father);s++;
}
int main(){int n;cout<<"请输入树的节点总个数"<<endl;cin>>n;char root; //根节点cout<<"请输入根节点的数据"<<endl;cin>>root;initial(root);char x; //代表要增加的节点的数据char father; //代表要增加节点的父节点,注意输入的父节点数据要已经添加到树中cout<<"请输入"<<n-1<<"组节点数据和该节点的父节点数据"<<endl;for(int i=1;i<10;i++){//TODOcin>>x;cin>>father;add(x,father);}char xx;cout<<"请输入要查找的节点数据:"<<endl;cin>>xx;int xx_index = find_index(xx);cout<<"该节点的父节点为:"<<tree[tree[xx_index].father_i].data<<endl;int count = 0; //子节点的个数cout<<"该节点的子节点为:";for(int i=0;i<=s;i++){//TODOif(xx_index == tree[i].father_i){//TODOcout<<tree[i].data<<" ";count++;}}
}
孩子表示法
树中的数据,数据之间的父子关系,指明某节点的孩子是谁
孩子表示法存储普通树采⽤的是 “顺序表+链表” 的组合结构。
其存储过程是:从树的根节点开始,使⽤顺序表依次存储树中各个节点。需要注意,与双亲表示法不同的是,孩子
表示法会给各个节点配备一个链表,⽤于存储各节点的孩子节点位于顺序表中的位置。
如果节点没有孩子节点(叶子节点),则该节点的链表为空链表。
使⽤孩子表示法存储的树结构,正好和双亲表示法相反,查找孩子结点的效率很⾼,⽽不擅长做查找⽗结点的操作。我们还可以将双亲表示法和孩子表示法合二为一:
代码实现
#include <iostream>
using namespace std;
typedef struct treeNode{int data; //存放孩子节点数据struct treeNode* next; //存放下一个孩子节点
}treeNode,*treeList;
struct {char data; //存放节点数据treeList L; //孩子链表的首元指针
}tree[100];
int s=0; //目前树中的节点个数
//初始化
void initial(char root){tree[0].data = root;tree[0].L = NULL;s++;
}
//找出节点下标位置
int find_index(char x){for(int i=0;i<s;i++){//TODOif(x == tree[i].data){return i;}}return -1;
}
//插入 头插法
void insert(char x, char fx){//先找到父节点fx的下标int fx_index = find_index(fx);if(fx_index==-1){//TODOcout<<"父节点不存在,插入失败!"<<endl;return ;}tree[s].data = x;tree[s].L = NULL;//建立一个孩子节点treeNode* node = (treeNode*)malloc(sizeof(treeNode));node->data = s;node->next = tree[fx_index].L;tree[fx_index].L = node;s++;
}
int main(){int n;cout<<"请输入树的节点总个数:"<<endl;cin>>n;char root;cout<<"请输入根节点的数据:"<<endl;cin>>root;initial(root);char x, fx; //节点以及该父亲节点的数据cout<<"请输入该孩子节点数据以及该孩子节点的父亲节点数据:";for(int i=1;i<n;i++){//TODOcin>>x>>fx;insert(x,fx);}//找孩子节点char xx;cout<<"请输入要操作的节点:"<<endl;cin>>xx;int xx_index = find_index(xx);if(xx_index==-1){//TODOcout<<"该节点不存在,退出程序!"<<endl;exit(0); //无异常退出}treeNode* p = tree[xx_index].L;cout<<"该节点的孩子节点为:";while(p!=NULL){//TODOcout<<tree[p->data].data<<" ";p = p->next;}cout<<endl;//找父亲节点cout<<"该节点的父亲节点为:";for(int i = 0; i < s; i++){treeNode* q = tree[i].L;while(q!=NULL){if(q->data==xx_index){cout<<tree[i].data;break;}q=q->next;}}
}
孩子兄弟表示法
左边后继节点的指针域:指向第一个孩子(最左边),存第一个孩子的地址
右边后继节点的指针域指向该节点右边相邻的一个兄弟,存右边相邻兄弟的地址
左孩子,右兄弟
用递归:有一个二叉链表root,在以root为根节点的二叉链表中找fx©所在的节点
在树结构中,同一层的节点互为兄弟节点。例如普通树中,节点 A、B 和 C 互为兄弟节点,⽽节点 D、E 和 F 也
互为兄弟节点。
所谓孩子兄弟表示法,指的是⽤将整棵树⽤二叉链表存储起来,具体实现方案是:从树的根节点开始,依次存储各
个结点的孩子结点和兄弟结点。
在二叉链表中,各个结点包含三部分内容:
孩子兄弟表示法示例图:
在以孩子兄弟表示法构建的二叉链表中,如果要查找结点 x 的所有孩子,则只要根据该结点的 firstchild 指针找到
它的第一个孩子,然后沿着孩子结点的 nextsibling 指针不断地找它的兄弟结点,就可以找到结点 x 的所有孩子。
代码实现(无序树)找不了父亲,只能找孩子
#include <iostream>
using namespace std;
typedef struct Node{char data; //存放节点数据域struct Node* left; //左边存放孩子节点struct Node* right; //右边存放兄弟节点
}Node,*treeNode;
//初始化
treeNode initial(char root){Node* s = (Node*)malloc(sizeof(Node)); //假设不存在内存分配失败的情况s->data = root;s->left = s->right = NULL;
}
//递归查找元素所在的节点
Node* find_Node(treeNode tree,char x){if(tree==NULL||x==tree->data){ //如果是空树,或者根节点就是要找的就直接返回tree就行return tree;}if(tree->left!=NULL){Node* p = find_Node(tree->left,x);if(p!=NULL&&p->data==x){return p;}}if(tree->right!=NULL){//TODONode* q = find_Node(tree->right,x);if(q!=NULL&&q->data==x){return q;}}return NULL; //如果找不到,就返回空
}
//插入
void insert(treeNode tree, char x, char fx){Node* fx_Node = find_Node(tree,fx);Node* s = (Node*)malloc(sizeof(Node));s->data = x;s->left = NULL; //该节点的孩子节点为NULLs->right = fx_Node->left; fx_Node->left = s;
}
int main(){int n;cout<<"请输入树的节点总个数:"<<endl;cin>>n;char root;cout<<"请输入树的根节点数据:"<<endl;cin>>root;treeNode tree = initial(root); //该指针指向整颗树cout<<"请输入节点数据和该节点的父节点数据:"<<endl;char x,fx;for(int i=1;i<n;i++){//TODOcin>>x>>fx;insert(tree,x,fx);}//找孩子节点,找不了父亲节点cout<<"请输入要操作的节点"<<endl;char xx;cin>>xx;Node* xx_Node = find_Node(tree,xx);cout<<"该节点的孩子节点为:";Node* p = xx_Node->left; //先找左孩子,再找左孩子的兄弟节点if(p!=NULL){while(p!=NULL){//TODOcout<<p->data<<" ";p=p->right;}}
}
二叉树
二叉树定义
-
二叉树是每个结点最多有两个子树的树结构。二叉树不允许存在度⼤于2的树。
-
它有五种最基本的形态:
- 二叉树可以是空集
- 根可以有空的左子树或者右子树;
- 左右子树都是空。只有左子树或者右子树的叫做斜树。
满二叉树
在一棵二叉树中,如果所有分支结点都存在左子树和右子树,并且所有叶子结点都
在同一层上,这样的二叉树称为满二叉树。
一棵深度为k且有2^k-1个结点的二叉树称为满二叉树。( k ≥ 1)
完全二叉树
如果一棵深度为k,有n个结点的二叉树中各结点能够与深度为k的顺序编号的满二叉树从1到n标号的结点相对应的
二叉树称为完全二叉树。
特点:
-
所有的叶结点都出现在第k层或k-1层
-
若任一结点,如果其右子树的最⼤层次为i,则其左子树的最⼤层次为i或i+1
二叉树的性质
-
性质1:在二叉树的第i层上的结点最多为2^(i-1)个。(i ≥ 1)
-
性质2:深度为k的二叉树至多有2^k-1个结点。(i ≥ 1)
-
性质3:在一棵二叉树中,叶结点的数目比度为2的结点数目多一个。
-
性质4:具有N个结点的完全二叉树的深度为logN+1。(向下取整)
-
性质5:如果有一棵n个结点的完全二叉树,其结点编号按照层次序(从上到下,从左到右),则除根结点外,满足[i/2 , i, 2i, 2i+1]的规则
二叉树的顺序存储
依靠性质5,可以将任意棵二叉树构造成满二叉树结构或完全二叉树结构,依据下标规则,就可以找到⽗结点,子结点。
代码实现
#include <iostream>
using namespace std;
char tree[100]; //定义一个数组用来存放二叉树节点数据
int flag; //flag为0表示左孩子,flag为1表示右孩子
int find_index(char x){for(int i=0;i<100;i++){//TODOif(x==tree[i]){//TODOreturn i;}}return -1; //找不到数据返回-1}
int main(){int n; //表示树中节点总个数cout<<"请输入树的节点总个数:"<<endl;cin>>n;for(int i=0;i<100;i++){//TODOtree[i] = ' ';}char root;cout<<"请输入树的根节点数据:"<<endl;cin>>root;tree[0] = root;char x,fx; //节点和该节点的父节点数据for(int i=1;i<n;i++){//TODOcin>>x>>fx>>flag;int fx_index = find_index(fx); //找到父节点的下标//TODOif(flag==0){//左孩子tree[2*fx_index] = x; }else if(flag==1){//右孩子tree[2*fx_index+1] = x;}}//找节点的孩子节点char xx;cout<<"请输入要操作的节点数据:"<<endl;cin>>xx;int xx_index = find_index(xx);if(xx_index==1){//TODOcout<<"该节点为根节点"<<endl;}else{cout<<"该节点的孩子为:"<<tree[2*xx_index]<<" "<<tree[2*xx_index+1]<<endl;cout<<"该节点的父亲为:"<<tree[xx_index/2];}
}
二叉树的链式存储
由于二叉树的每个结点最多只能有两个子树,每个结点定义两个指针域和一个数据域即可。
代码实现
#include <iostream>
using namespace std;
typedef struct Node{char data; //数据域struct Node* right; //右孩子指针struct Node* left; //左孩子指针
}Node,*treeList;
//初始化
treeList initial(char root){Node* s = (Node*)malloc(sizeof(Node));s->data = root;s->left = s->right = NULL;return s;
}
//查找数据为x的节点指针
Node* find_Node(treeList tree, char x){if(tree==NULL||x == tree->data){//TODOreturn tree;}if(tree->left!=NULL){Node* p = find_Node(tree->left,x);if(p!=NULL&&p->data==x){return p;}}if(tree->right!=NULL){//TODONode* q =find_Node(tree->right,x);if(q!=NULL&&q->data==x){return q;}}return NULL;
}
//插入
void insert(treeList tree, char x, char father_x,int flag){Node* father_x_node = find_Node(tree,father_x);if(father_x_node==NULL){cout<<"该节点不存在"<<endl; return;}Node* s =(Node*)malloc(sizeof(Node));s->data = x;s->left=s->right = NULL;if(flag==0){//TODO//左孩子father_x_node->left = s;}if(flag==1){//TODO//右孩子father_x_node->right = s;}
}
int main(){int flag; //0代表左孩子 1代表右孩子int n;cout<<"请输入节点总个数:"<<endl;cin>>n;char root;cout<<"请输入根节点数据:"<<endl;cin>>root;treeList tree = initial(root);char x,fx;cout<<"请输入节点数据、该节点的父节点数据、flag的值(0表示右孩子,1表示左孩子)"<<endl;for(int i=1;i<n;i++){//TODOcin>>x>>fx>>flag;insert(tree,x,fx,flag);}//找孩子cout<<"请输入要操作节点数据:"<<endl;char xx;cin>>xx;Node* xx_node = find_Node(tree,xx);if(xx_node!=NULL){//TODOif(xx_node->left!=NULL){//TODOcout<<"该节点的左孩子为:"<<xx_node->left->data<<endl;}if(xx_node->right!=NULL){//TODOcout<<"该节点的右孩子为:"<<xx_node->right->data<<endl;}}
}
二叉树的遍历
遍历思想
-
遍历 :沿某条搜索路径周游二叉树,对树中的每一个节点访问一次且仅访问一次。
-
对线性结构⽽言,只有一条搜索路径(因为每个结点均只有一个后继),故不需要另加讨论。
-
二叉树是非线性结构,每个结点有两个后继,则存在如何遍历即按什么样的搜索路径进行遍历的问题。
-
按层次,⽗子关系,知道了⽗,那么就把其所有的子结点都看一遍
-
按深度,一条道走到黑,然后再返回走另一条道
-
遍历结果
先序遍历:A B C D E F G H K
中序遍历:B D C A E H G K F
后序遍历:D C B H K G F E A
广度遍历
-
算法思想
-
引入队列,将根结点入队
-
从队列中取出队头元素,访问该结点,将该结点的所有孩子节点入队
-
再次从队列中取出队头元素,并访问,以此重复
-
直到队列为空,说明所有元素都遍历完成
-
代码实现
#include <iostream>
using namespace std;
//二叉树定义
typedef struct BNode{char data; //数据域struct BNode* left; //左孩子struct BNode* right; //右孩子
}BNode,*treeList;
//队列定义
typedef struct Node{char data; //数据域struct Node* next; //下个节点的指针域
}Node,*QueueList;
QueueList f; //队首指针
QueueList r; //队尾指针//初始化队列
void initial_queue(){Node* s = (Node*)malloc(sizeof(Node));s->next = NULL;f=r=s;
}
//判空
int isEmpty(){if(f->next==NULL){return 1; //为空}return 0; //不为空
}
//入队
void enter_queue(char x){Node* s = (Node*)malloc(sizeof(Node));s->data = x;s->next = r->next;r->next = s;r=s;
}
//出队
char leave_queue(){Node* q = f->next;char data = q->data;f->next = q->next;if(q->next==NULL){//TODOr=f;}free(q);return data;
}
//初始化二叉树
treeList initial(char root){BNode* s = (BNode*) malloc(sizeof(BNode));s->data = root;s->left = s->right = NULL;return s;
}
//查找节点
BNode* find_node(treeList tree, char x){if(tree==NULL||tree->data == x){//TODOreturn tree;}if(tree->left!=NULL){//TODOBNode* p = find_node(tree->left,x);if(p!=NULL&&p->data == x){//TODOreturn p;}}if(tree->right!=NULL){//TODOBNode* q = find_node(tree->right,x);if(q!=NULL&&q->data==x){//TODOreturn q;}}return NULL;
}
//插入节点
void insert(treeList tree,char x, char fx,int flag){BNode* fx_node = find_node(tree,fx);if(fx_node==NULL){cout<<"节点不存在,插入失败!"<<endl;return;}BNode* x_node = (BNode*)malloc(sizeof(BNode));x_node->data = x;x_node->left = x_node->right = NULL;if(flag==0){//左孩子fx_node->left = x_node;}if(flag==1){//右孩子fx_node->right = x_node;}
}
//访问输出数据
void visit(char x){cout<<x<<" ";
}//广度遍历
void wide_search(treeList tree){initial_queue(); //初始化队列//将根节点数据入队enter_queue(tree->data);while(isEmpty()==0){ //队列不空char data = leave_queue();BNode* bnode = find_node(tree,data);if(bnode->left!=NULL){enter_queue(bnode->left->data);}if(bnode->right!=NULL){enter_queue(bnode->right->data);}visit(data);}
}
int main(){int flag; //0表示左孩子,1表示右孩子int n;cout<<"请输入树中节点总个数:"<<endl;cin>>n;char root; cout<<"请输入根节点数据:"<<endl;cin>>root;treeList tree = initial(root);char x,fx;cout<<"请输入节点以及其父节点数据还有flag(0,1)"<<endl;for(int i=1;i<n;i++){//TODOcin>>x>>fx>>flag;insert(tree,x,fx,flag);}//广度遍历cout<<"广度遍历结果:"<<endl;wide_search(tree);
}
深度遍历(递归版)
先序遍历
中序遍历
后序遍历
深度遍历综合代码
#include <iostream>
using namespace std;
//二叉树定义
typedef struct BNode{char data; //数据域struct BNode* left; //左孩子struct BNode* right; //右孩子
}BNode,*treeList;
//队列定义
typedef struct Node{char data; //数据域struct Node* next; //下个节点的指针域
}Node,*QueueList;
QueueList f; //队首指针
QueueList r; //队尾指针//初始化队列
void initial_queue(){Node* s = (Node*)malloc(sizeof(Node));s->next = NULL;f=r=s;
}
//判空
int isEmpty(){if(f->next==NULL){return 1; //为空}return 0; //不为空
}
//入队
void enter_queue(char x){Node* s = (Node*)malloc(sizeof(Node));s->data = x;s->next = r->next;r->next = s;r=s;
}
//出队
char leave_queue(){Node* q = f->next;char data = q->data;f->next = q->next;if(q->next==NULL){//TODOr=f;}free(q);return data;
}
//初始化二叉树
treeList initial(char root){BNode* s = (BNode*) malloc(sizeof(BNode));s->data = root;s->left = s->right = NULL;return s;
}
//查找节点
BNode* find_node(treeList tree, char x){if(tree==NULL||tree->data == x){//TODOreturn tree;}if(tree->left!=NULL){//TODOBNode* p = find_node(tree->left,x);if(p!=NULL&&p->data == x){//TODOreturn p;}}if(tree->right!=NULL){//TODOBNode* q = find_node(tree->right,x);if(q!=NULL&&q->data==x){//TODOreturn q;}}return NULL;
}
//插入节点
void insert(treeList tree,char x, char fx,int flag){BNode* fx_node = find_node(tree,fx);if(fx_node==NULL){cout<<"节点不存在,插入失败!"<<endl;return;}BNode* x_node = (BNode*)malloc(sizeof(BNode));x_node->data = x;x_node->left = x_node->right = NULL;if(flag==0){//左孩子fx_node->left = x_node;}if(flag==1){//右孩子fx_node->right = x_node;}
}
//访问输出数据
void visit(char x){cout<<x<<" ";
}//广度遍历
void wide_search(treeList tree){initial_queue(); //初始化队列//将根节点数据入队enter_queue(tree->data);while(isEmpty()==0){ //队列不空char data = leave_queue();BNode* bnode = find_node(tree,data);if(bnode->left!=NULL){enter_queue(bnode->left->data);}if(bnode->right!=NULL){enter_queue(bnode->right->data);}visit(data);}
}//前序遍历(根节点-》左子树-》右子树)
void front_search(treeList tree){if(tree==NULL){return;}visit(tree->data);front_search(tree->left);front_search(tree->right);
}
//中序遍历(左子树-》根节点-》右子树)
void middle_search(treeList tree){if(tree==NULL){return;}middle_search(tree->left);visit(tree->data);middle_search(tree->right);
}
//后序遍历(左子树-》右子树-》根节点)
void back_search(treeList tree){if(tree==NULL){return;}back_search(tree->left);back_search(tree->right);visit(tree->data);
}
int main(){int flag; //0表示左孩子,1表示右孩子int n;cout<<"请输入树中节点总个数:"<<endl;cin>>n;char root; cout<<"请输入根节点数据:"<<endl;cin>>root;treeList tree = initial(root);char x,fx;cout<<"请输入节点以及其父节点数据还有flag(0,1)"<<endl;for(int i=1;i<n;i++){//TODOcin>>x>>fx>>flag;insert(tree,x,fx,flag);}//广度遍历cout<<"广度遍历结果:"<<endl;wide_search(tree);cout<<endl;//深度遍历//前序遍历结果cout<<"前序遍历结果"<<endl;front_search(tree);cout<<endl;cout<<"中序遍历结果"<<endl;middle_search(tree);cout<<endl;cout<<"后序遍历结果"<<endl;back_search(tree);
}
深度遍历(非递归版)
前序
中序
后序
#include <iostream>
using namespace std;
//二叉树定义
typedef struct BNode{char data; //数据域struct BNode* left; //左孩子struct BNode* right; //右孩子
}BNode,*treeList;//定义链栈
typedef struct Node{struct BNode* data; //把树的节点当作数据struct Node* next;
}Node,*stackList;//初始化链栈
stackList initial_stack(){Node* s = (Node*)malloc(sizeof(Node));s->next = NULL;return s;
}//入栈
void enter_stack(stackList sstack, BNode* x){//头插法Node* s = (Node*)malloc(sizeof(Node));s->data = x;s->next = sstack->next;sstack->next = s;
}//判空
int isEmpty(stackList sstack){if(sstack->next == NULL){return 1; //空}return 0; //非空
}//出栈
BNode* pop(stackList sstack){if(isEmpty(sstack)==0){BNode* x = sstack->next->data; //先保存数据Node* p = sstack->next;sstack->next = p->next;free(p);return x;}else{cout<<"出栈失败!"<<endl;}
}//得到栈顶元素
BNode* get_top(stackList sstack){if(sstack->next!=NULL){return sstack->next->data;}
}//初始化二叉树
treeList initial(char root){BNode* s = (BNode*) malloc(sizeof(BNode));s->data = root;s->left = s->right = NULL;return s;
}
//查找节点
BNode* find_node(treeList tree, char x){if(tree==NULL||tree->data == x){//TODOreturn tree;}if(tree->left!=NULL){//TODOBNode* p = find_node(tree->left,x);if(p!=NULL&&p->data == x){//TODOreturn p;}}if(tree->right!=NULL){//TODOBNode* q = find_node(tree->right,x);if(q!=NULL&&q->data==x){//TODOreturn q;}}return NULL;
}
//插入节点
void insert(treeList tree,char x, char fx,int flag){BNode* fx_node = find_node(tree,fx);if(fx_node==NULL){cout<<"节点不存在,插入失败!"<<endl;return;}BNode* x_node = (BNode*)malloc(sizeof(BNode));x_node->data = x;x_node->left = x_node->right = NULL;if(flag==0){//左孩子fx_node->left = x_node;}if(flag==1){//右孩子fx_node->right = x_node;}
}
//访问输出数据
void visit(char x){cout<<x<<" ";
}//前序遍历(根节点-》左子树-》右子树)
void front_search(treeList tree){BNode* p = tree;BNode* k = NULL;stackList sstack = initial_stack();while(isEmpty(sstack)==0||p!=NULL){if(p!=NULL){visit(p->data);enter_stack(sstack,p);p=p->left;}if(p==NULL){k = pop(sstack);p=k->right;}}
}
//中序遍历(左子树-》根节点-》右子树)
void middle_search(treeList tree){BNode* p = tree;BNode* k = NULL;stackList sstack = initial_stack();while(isEmpty(sstack)==0||p!=NULL){if(p!=NULL){enter_stack(sstack,p);p=p->left;}if(p==NULL){k = pop(sstack);visit(k->data);p=k->right;}}
}
//后序遍历(左子树-》右子树-》根节点)
void back_search(treeList tree){BNode* p = tree;BNode* k = NULL;BNode* pre = NULL;stackList sstack = initial_stack();while(isEmpty(sstack)==0||p!=NULL){if(p!=NULL){enter_stack(sstack,p);p=p->left;}if(p==NULL){k = get_top(sstack);if(k->right!=NULL&&pre!=k->right){//说明还未访问右子树p=k->right;}else{visit(k->data);pop(sstack);pre = k;}}}
}
int main(){int flag; //0表示左孩子,1表示右孩子int n;cout<<"请输入树中节点总个数:"<<endl;cin>>n;char root; cout<<"请输入根节点数据:"<<endl;cin>>root;treeList tree = initial(root);char x,fx;cout<<"请输入节点以及其父节点数据还有flag(0,1)"<<endl;for(int i=1;i<n;i++){//TODOcin>>x>>fx>>flag;insert(tree,x,fx,flag);}cout<<endl;//深度遍历cout<<"前序遍历结果"<<endl;front_search(tree);cout<<endl;cout<<"中序遍历结果"<<endl;middle_search(tree);cout<<endl;cout<<"后序遍历结果"<<endl;back_search(tree);
}
根据遍历结果重构二叉树
-
前序+中序
-
后序+中序
-
层次遍历+中序1
先找根节点,再找左子树,再找右子树
-
若某二叉树的前遍历访问顺序是序abdgcefh,中序遍历顺序是dgbaechf,则后序遍历的访问顺序是什么。
-
已知一棵二叉树的中序序列和后序序列分别是BDCEAFHG 和 DECBHGFA,请画出这棵二叉树。
线索二叉树
背景介绍
现有一棵结点数目为n的二叉树,采⽤二叉链表的形式存储,对于每个结点均有指向左右孩子的两个指针域。
结点为n的二叉树一共有n-1条有效分支路径。那么,则二叉链表中存在:2n - ( n-1 ) = n + 1个 空指针域
那么,这些空指针造成了空间浪费。
当对二叉树进行中序遍历时可以得到二叉树的中序序列。
如图所示二叉树的中序遍历结果为DBEAC,可以得知A的前驱结点为E,后继结点为C。
这种关系的获得是建⽴在完成遍历后得到 的,那么可不可以在建⽴二叉树时就记录下前驱后继的关系呢,那么在
后续寻找前驱结点和后继结点时将⼤⼤提升效率。
线索化
-
将某结点的空指针域指向该结点的前驱后继,定义规则如下:
- 若结点的左子树为空,则该结点的左孩子指针指向其前驱结点。
- 若结点的右子树为空,则该结点的右孩子指针指向其后继结点。
-
这种指向前驱和后继的指针称为线索。将一棵普通二叉树以某种次序遍历,并添加线索的过程称为线索化。
线索化的改进
-
可以将一棵二叉树线索化为一棵线索二叉树,那么新的问题产⽣了。我们如何区分一个结点的 lchild指针是指向左孩子还是前驱结点呢?
-
为了解决这一问题,现需要添加标志位ltag,rtag。并定义规则如下:
- ltag为0时,指向左孩子,为1时指向前驱
- rtag为0时,指向右孩子,为1时指向后继
-
在遍历过程中,如果当前结点没有左孩子,需要将该结点的 lchild 指针指向遍历过程中的前一个结点,所以
在遍历过程中,设置一个指针(名为 pre ),时刻指向当前访问结点的前一个结点。
线索化的优势
-
递归遍历需要使⽤系统栈,非递归遍历需要使⽤内存中的空间来帮助遍历,⽽线索化之后就不需 要这些辅助了,直接可以像遍历数组一样遍历。
-
线索二叉树核心目的在于加快查找结点的前驱和后继的速度。如果不使⽤线索的话,当查找一个 结点的前驱与后继需要从根节点开始遍历,当然,如果二叉树数据量较⼩时,可能线索化之后作⽤不 ⼤,但是当数据量很⼤时,线索化所带来的性能提升就会比较明显。
在中序线索化的二叉树上找前驱和后继
代码实现(1和2)
记住中序遍历:BDCAEHGKF
(1)
#include <iostream>
using namespace std;
typedef struct Node{char data; //数据域struct Node* right; //左孩子指针int rflag; //0为线索 1为孩子标志struct Node* left; //右孩子指针int lflag;
}Node,*Tree;
Node* pre = NULL; //遍历的前一个指针
//初始化树
Tree initial_tree(char root){Node* s = (Node*)malloc(sizeof(Node));if(s==NULL){cout<<"内存分配失败!"<<endl;return NULL;}s->data = root;s->right=s->left=NULL;s->lflag=s->rflag=1; //都是孩子return s;
}
//递归找出数据为x的节点
Node* find_node(Tree tree, char x){if(tree!=NULL&&tree->data ==x){return tree;}if(tree->left!=NULL&&tree->lflag==1){Node* p = find_node(tree->left,x);if(p!=NULL&&p->data==x){return p;}}if(tree->right!=NULL&&tree->rflag==1){Node* p = find_node(tree->right,x);if(p!=NULL&&p->data==x){return p;}}return NULL;
}
//插入
void insert_node(Tree tree,char x, char fx, int flag){Node* fx_node = find_node(tree,fx);if(fx_node==NULL){cout<<"插入失败!"<<endl;return;}Node* s = (Node*)malloc(sizeof(Node));s->data = x;s->right=s->left=NULL;s->lflag=s->rflag=1;if(flag==0){//左孩子fx_node->left = s;fx_node->lflag=1;}else if(flag==1){//右孩子fx_node->right = s;fx_node->rflag = 1;}
}
//边遍历边添加线索
void visit(Tree tree){if(tree->left==NULL){tree->left=pre;tree->lflag=0; //添加线索}if(pre!=NULL&&pre->right==NULL){pre->right=tree;pre->rflag=0; //添加线索}pre=tree;
}
void middle_order(Tree tree){if(tree==NULL){return;}if(tree->left!=NULL&&tree->left->lflag==1){middle_order(tree->left);}visit(tree);if(tree->right!=NULL&&tree->right->rflag==1){middle_order(tree->right);}
}
int main(){int n;cout<<"请输入节点总个数:"<<endl;cin>>n;char root;cout<<"请输入根节点的数据:"<<endl;cin>>root;int flag; //0代表左孩子,1代表右孩子Tree tree = initial_tree(root);char x,fx;cout<<"请输入孩子节点的数据和该孩子的父亲节点的数据以及是否是左右孩子(flag为0则左孩子,flag为1则右孩子):"<<endl;for(int i=1;i<n;i++){cin>>x>>fx>>flag;insert_node(tree,x,fx,flag);}middle_order( tree);char xx;cout<<"请输入要操作的节点数据"<<endl;cin>>xx;Node* xx_node = find_node(tree,xx);if(xx_node->lflag==0){cout<<"前驱为:"<<xx_node->left->data;}else{//如果左孩子不是线索则找出该节点左子树中最靠右的节点即是前驱Node* p = xx_node->left;if(p==NULL){cout<<"该节点无前驱"<<endl;}else{while(p->right!=NULL&&p->rflag==1){p=p->right;}cout<<"前驱为:"<<p->data;}}if(xx_node->rflag==0){cout<<"后继为:"<<xx_node->right->data;}else{//如果该节点右孩子不是后继则找出该节点右子树中最靠左的节点Node* p = xx_node->right;if(p==NULL){cout<<"该节点无后继"<<endl;}else{while(p->left!=NULL&&p->lflag==1){p=p->left;}cout<<"后继为:"<<p->data;}}
}
(2)
#include <iostream>
using namespace std;//若二叉树中一个节点左为空,则指向该节点的前驱,若右为空,前驱节点的右,的后继指向该节点
//二叉树定义
typedef struct BNode{char data; //数据域struct BNode* left; //左孩子struct BNode* right; //右孩子int lflag ; //0表示孩子,1表示线索int rflag ;
}BNode,*treeList;
//若二叉树中一个节点左为空,则指向该节点的前驱,若右为空,前驱节点的右,的后继指向该节点
BNode* pre = NULL;
//初始化二叉树
treeList initial(char root){BNode* s = (BNode*) malloc(sizeof(BNode));s->data = root;s->left = s->right = NULL;s->lflag = s->rflag = 0;return s;
}//查找节点
BNode* find_node(treeList tree, char x){if(tree==NULL||tree->data == x){//TODOreturn tree;}if(tree->left!=NULL&&tree->lflag==0){//TODOBNode* p = find_node(tree->left,x);if(p!=NULL&&p->data == x){//TODOreturn p;}}if(tree->right!=NULL&&tree->rflag==0){//TODOBNode* q = find_node(tree->right,x);if(q!=NULL&&q->data==x){//TODOreturn q;}}return NULL;
}//插入节点
void insert(treeList tree,char x, char fx,int flag){BNode* fx_node = find_node(tree,fx);if(fx_node==NULL){cout<<"节点不存在,插入失败!"<<endl;return;}BNode* x_node = (BNode*)malloc(sizeof(BNode));x_node->data = x;x_node->left = x_node->right = NULL;x_node->lflag = x_node->rflag =0;if(flag==0){//左子fx_node->left = x_node;}if(flag==1){//右孩子fx_node->right = x_node;}
}
//添加线索
void visit(BNode* x){//pre是x的前驱if(x->left==NULL){//TODOx->left = pre;x->lflag = 1; //表示为线索}//x是pre的后继if(pre!=NULL&&pre->right==NULL){pre->right = x;pre->rflag = 1; //添加线索}//更新prepre = x;
}//先序遍历要添加:判断if(tree->lflag==0)来判断是否是左孩子,还是已经添加过的线索
//因为visit(会改变目前节点x的lflag)//中序遍历(左子树-》根节点-》右子树)
void middle_search(treeList tree){if(tree==NULL){return;}middle_search(tree->left);visit(tree);middle_search(tree->right);
}int main(){int flag; //0表示左孩子,1表示右孩子int n;cout<<"请输入树中节点总个数:"<<endl;cin>>n;char root; cout<<"请输入根节点数据:"<<endl;cin>>root;treeList tree = initial(root);char x,fx;cout<<"请输入节点以及其父节点数据还有flag(0,1)"<<endl;for(int i=1;i<n;i++){//TODOcin>>x>>fx>>flag;insert(tree,x,fx,flag);}cout<<endl;cout<<"中序遍历结果"<<endl;middle_search(tree);cout<<endl;cout<<"请输入要操作的节点:"<<endl;char xx;cin>>xx;BNode* xx_node = find_node(tree,xx);cout<<"该节点的前驱为:"<<endl;//找前驱if(xx_node->lflag==1){cout<<xx_node->left->data<<endl;}else{ //即如果为0则找x左子树中最靠右边的孩子BNode* p = xx_node->left;while(p!=NULL&&p->rflag==0){//TODOp=p->right;}if(p==NULL){cout<<"前驱不存在!";}else{ cout<<p->data;}}cout<<endl;cout<<"后继节点为:";//找后继if(xx_node->rflag==1){cout<<xx_node->right->data<<endl;}else{ //即如果为0则找x左子树中最靠右边的孩子BNode* p = xx_node->right;while(p!=NULL&&p->lflag==0){//TODOp=p->left;}if(p==NULL){cout<<"后继不存在!";}else{ cout<<p->data;}}
}
高级搜索树
用于数据的存储和查找的树
BST AVL树 红黑树 B树 B+树
二叉搜索树(BST)
二叉搜索树(Binary Search Tree),也有称之为二叉排序树、二叉查找树
左子树<根<右子树的值:即二叉平衡树的中序遍历序列有序
定义和性质
在二叉树的基础上,增加了几个规则约束:
-
如果他的左⼦树不空,则左⼦树上所有结点的值均⼩于它的根结点的值。
-
若它的右⼦树不空,则右⼦树上所有结点的值均⼤于它的根结点的值。
-
它的左、右树⼜分为二叉排序树。
构建二叉排序树
假设有无序序列如下:8 3 10 1 6 14 4 7 13
二叉搜索树的优势
-
二叉排序树的中序遍历,就是一个从⼩到⼤排好序的序列,但是查找时,完全没有必要先进行中序遍历生成一个有序的数组,再二分查找,直接根据二叉搜索树的约束直接操作。
-
查找时间最坏情况就是树的深度
-
二叉搜索树的查找逻辑,可以写成递归的思路和非递归思路。
BiTree SearchBST(BiTree T,KeyType key){//如果递归过程中 T 为空,则查找结果,返回NULL;或者查找成功,返回指向该关键字的指针 if (!T || key==T->data) {return T;}else if(key<T->data){//递归遍历其左孩⼦return SearchBST(T->lchild, key);}else{//递归遍历其右孩⼦return SearchBST(T->rchild, key); }
}
二叉搜索树的实现
二叉搜索树的插入操作
代码实现(包括递归与非递归版)
#include <iostream>
using namespace std;
typedef struct Node{int data; //节点数据struct Node* left; //左孩子指针struct Node* right; //右孩子指针
}Node,*treeList;
//递归版
treeList insert1(treeList tree,int x){if(tree==NULL){//空树,此时该位置就是插入x的位置Node* s = (Node*)malloc(sizeof(Node));s->data = x;s->left = s->right = NULL;return s;}if(x<tree->data){//x应该插入到r的左子树上tree->left=insert1(tree->left,x);//此时插入的节点是tree->left}if(x>tree->data){//x应该插入到r的右子树上tree->right = insert1(tree->right,x);//此时插入的节点是tree->right}return tree; //把改变后的新树返回
}
//非递归版
treeList insert2(treeList tree, int x){Node* s = (Node*)malloc(sizeof(Node));s->data = x;s->left = s->right = NULL;if(tree == NULL){//空树,此时该位置就是插入x的位置return s;}Node* p = tree; //p是用来遍历的指针Node* pre = NULL; //pre指向p的父亲while(p!=NULL){pre = p;if(x>p->data){p=p->right;}else{ //x<p->datap=p->left;}}if(x>pre->data){pre->right = s;}else{ //假设用户没有输入重复的数据pre->left = s;}return tree;
}
//中序遍历
void middle_search(treeList tree){if(tree==NULL){return; //树为空,则无需遍历,直接退出}middle_search(tree->left);cout<<tree->data<<" ";middle_search(tree->right);
}//查找节点是否存在
void find(treeList tree, int num){Node* p = tree; //遍历指针while(p!=NULL){if(num>p->data){p=p->right;}else if(num<p->data){p = p->left;}else{cout<<"找到了,存在!"<<endl;return;}}cout<<"不存在"<<endl;
}
int main(){int n;cout<<"请输入节点个数:"<<endl;cin>>n;//定义一颗空树treeList tree = NULL;int a[10];cout<<"请输入数据:"<<endl;for(int i=0;i<n;i++){//TODOcin>>a[i];tree = insert2(tree,a[i]);}//中序遍历输出结果,从小到大middle_search(tree);//查找树上是否存在该节点数据int num;cout<<"请输入要查找的数据:"<<endl;cin>>num;find(tree,num);
}
查找某一节点下的最⼤或最⼩值
-
算法思想:
-
比当前节点⼩的值,一定放在他的左⼦树上,那么一直往左查找最左边的节点,就是最⼩值
-
比当前节点⼤的值,一定放在他的右⼦树上,那么一直往右查找最左边的节点,就是最⼤值
-
二叉搜索树的删除操作
代码示例(递归版本)
#include <iostream>
using namespace std;
typedef struct Node{int data; //节点数据struct Node* left; //左孩子指针struct Node* right; //右孩子指针
}Node,*treeList;//递归版
treeList insert(treeList tree,int x){if(tree==NULL){//空树,此时该位置就是插入x的位置Node* s = (Node*)malloc(sizeof(Node));s->data = x;s->left = s->right = NULL;return s;}if(x<tree->data){//x应该插入到r的左子树上tree->left=insert(tree->left,x);//此时插入的节点是tree->left}if(x>tree->data){//x应该插入到r的右子树上tree->right = insert(tree->right,x);//此时插入的节点是tree->right}return tree; //把改变后的新树返回
}//中序遍历
void middle_search(treeList tree){if(tree==NULL){return; //树为空,则无需遍历,直接退出}middle_search(tree->left);cout<<tree->data<<" ";middle_search(tree->right);
}//找到节点左子树中最靠右的节点
Node* find_left_tree_right(Node* x){while(x->right!=NULL){x=x->right;}return x;
}//删除
treeList delete_node(treeList tree,int num){if(num<tree->data){tree->left = delete_node(tree->left,num);}if(num>tree->data){tree->right = delete_node(tree->right,num);}if(num==tree->data){//两种情况 如果要删除的节点度为2,或者删除的节点度为0/1//此时tree就是要删除的节点if(tree->left!=NULL&&tree->right!=NULL){//此时节点度为2//然后找到该节点左子树中最靠右的节点qNode* q = find_left_tree_right(tree->left);int result = q->data;tree->data = result;tree->left = delete_node(tree->left,result);}else{Node* pp = tree;//此时节点的度为0/1if(tree->left!=NULL){//该节点节点只有左孩子tree= tree->left;}else{tree=tree->right; //此时包含该节点节点只有右孩子和无孩子的情况}free(pp);pp=NULL; //防止pp为野指针}}return tree; //返回删除后的树(此时树会发生变化)
}int main(){int n;cout<<"请输入节点个数:"<<endl;cin>>n;//定义一颗空树treeList tree = NULL;int a[10];cout<<"请输入数据:"<<endl;for(int i=0;i<n;i++){//TODOcin>>a[i];tree = insert(tree,a[i]);}//中序遍历输出结果,从小到大cout<<"建二叉搜索树之后的结果"<<endl;middle_search(tree);//删除树中节点数据int num;cout<<"请输入要删除的数据:"<<endl;cin>>num;cout<<endl;tree = delete_node(tree,num);//中序遍历输出结果,从小到大cout<<"删除之后的二叉搜索树:"<<endl;middle_search(tree);
}
二叉搜索树缺点
二叉平衡树(AVL)
AVL树是最早被发明的⾃平衡二叉查找树。在AVL树中,任一节点对应的两棵⼦树的最⼤⾼度差的绝对值为1,因此它也被称为⾼度平衡树。AVL树是根据它的发明者G.M. Adelson-Velsky和E.M. Landis命名的。
查找、插入和删除在平均和最坏情况下的时间复杂度都是 O(logn)。 增加和删除元素的操作则可能需要借由一次或多次树旋转,以实现树的重新平衡。
平衡因⼦
左⼦树与右⼦树的⾼度差即为该节点的平衡因⼦(BF,Balance Factor)
平衡二叉树中不存在平衡因⼦⼤于 1 的节点。
在一棵平衡二叉树中,节点的平衡因⼦只能取 0 、1 或者 -1 ,分别对应着左右⼦树等⾼,左⼦树比较⾼,右⼦树比较⾼。
平衡树失衡情况分析
当加入一个新节点后,右边或者左边的⾼度增加,导致失衡
代码实现(插入)
#include <iostream>
using namespace std;
typedef struct Node{int data;struct Node* left; //左孩子struct Node* right; //右孩子int h; //树中节点的高度
}Node, *treeList;//获取树的高度
int getHeight(Node* s){if(s==NULL){return 0; //为空,高度为0}else{return s->h;}
}
//求节点高度最大值
int maxx(int a,int b){return a>b?a:b;
}
//右旋
Node* right_rotation(Node* x){Node* y = x->left;x->left = y->right;y->right = x;//右旋后节点高度变化(只有x,y节点高度发生变化)x->h = maxx(getHeight(x->left),getHeight(x->right))+1;y->h = maxx(getHeight(y->left),getHeight(y->right))+1;return y;
}//左旋
Node* left_rotation(Node* x){Node* y = x->right;x->right = y->left;y->left = x;x->h = maxx(getHeight(x->left),getHeight(x->right))+1;y->h = maxx(getHeight(y->left),getHeight(y->right))+1;return y;
}
//调整函数
//以x为根的子树LL的失衡情况
treeList LL(Node* x){return right_rotation(x);
}
//以x为根的子树RR的失衡情况
treeList RR(Node* x){return left_rotation(x);
}
//以x为根的子树LR的失衡情况
treeList LR(Node* x){x->left = left_rotation(x->left);return right_rotation(x);
}
//以x为根的子树RL的失衡情况
treeList RL(Node* x){x->right = right_rotation(x->right);return left_rotation(x);
}//插入操作
treeList insert_into_tree(treeList tree,int data){if(tree==NULL){Node* s = (Node*)malloc(sizeof(Node));s->data = data;s->left = s->right = NULL;s->h = 1; //若空树则第一个节点高度赋值为1return s;}if(data<tree->data){tree->left = insert_into_tree(tree->left,data);//此时在tree的左子树上插入了一个节点if(getHeight(tree->left)-getHeight(tree->right)>1){//此时如果失衡左子树一定比右子树高//失衡 LL LRif(data<tree->left->data){//LL 是往tree的左孩子的左子树中插入节点导致失衡的//对tree进行右旋一次即可tree = LL(tree);}if(data>tree->left->data){//LR 是往tree的左孩子的右子树中插入节点导致失衡的//先对tree->left进行左旋,再对tree进行右旋tree = LR(tree);}}}if(data>tree->data){tree->right = insert_into_tree(tree->right,data);//此时在tree的右子树上插入了一个节点if(getHeight(tree->right)-getHeight(tree->left)>1){//此时如果失衡右子树一定比左子树高//失衡 RR RLif(data<tree->right->data){//RL 是往tree的右孩子的左子树中插入节点导致失衡的//先对tree->right进行右旋一次,再对tree进行左旋一次即可tree = RL(tree);}if(data>tree->right->data){//RR 是往tree的右孩子的右子树中插入节点导致失衡的//对tree进行左旋一次即可tree = RR(tree);}}}//tree的高度会变化tree->h = maxx(getHeight(tree->left),getHeight(tree->right))+1;return tree;
}
//中序遍历
void middle_search(treeList tree){if(tree==NULL){return;}middle_search(tree->left);cout<<tree->data<<"高度:"<<tree->h<<endl;middle_search(tree->right);
}
int main(){int n;cout<<"请输入节点总个数:"<<endl;cin>>n;treeList tree = NULL; //定义一颗空树int a[100];cout<<"请输入数据:"<<endl;for(int i=0;i<n;i++){cin>>a[i];tree = insert_into_tree(tree,a[i]);}middle_search(tree);
}
代码实现(删除)
删除节点80,相当于插入55
#include<stdio.h>
#include<stdlib.h>
//二叉链表存树
typedef struct AVLnode{int data;struct AVLnode* left;struct AVLnode* right;int h;//该节点的高度
}AVLNode,*AVLTree;
int geth(AVLNode* x)
{if(x==NULL){return 0;}else{return x->h; }}
int maxx(int a,int b)
{return a>b?a:b;
}
//调整函数
//LL--->以x为根的子树LL失衡情况,x的左孩子的左子树插入了节点 x失衡
AVLTree LL_rotation(AVLTree x)
{//右旋 AVLNode* y=x->left;x->left=y->right;y->right=x;x->h =maxx(geth(x->left),geth(x->right))+1;y->h =maxx(geth(y->left),geth(y->right))+1;return y;
}
//RR--->以x为根的子树RR失衡情况,x的右孩子的右子树插入了节点 x失衡
AVLTree RR_rotation(AVLTree x)
{//左旋 AVLNode* y=x->right ;x->right =y->left ;y->left =x;x->h =maxx(geth(x->left),geth(x->right))+1;y->h =maxx(geth(y->left),geth(y->right))+1;return y;
}//LR--->以x为根的子树LR失衡情况,x的左孩子的右子树插入了节点 x失衡
AVLTree LR_rotation(AVLTree x)
{//左旋 --->右旋 x->left=RR_rotation(x->left); x=LL_rotation(x);return x;
}
//RR--->以x为根的子树RL失衡情况,x的右孩子的左子树插入了节点 x失衡
AVLTree RL_rotation(AVLTree x)
{//右旋 --->左旋 x->right =LL_rotation(x->right ); x=RR_rotation(x);return x;
}AVLTree insert(AVLTree r,int k)
{if(r==NULL){r=(AVLNode*)malloc(sizeof(AVLNode));//if(r==NULL)r->data=k;r->left=r->right=NULL;r->h =1;}if(k<r->data){r->left=insert(r->left,k);/*//r的左子树插入了一个节点,r的高度可能变化,重新求一下 r->h=maxx(geth(r->left),geth(r->right))+1;*///此时 在r的左子树插入了一个节点 //r有可能失衡 if(geth(r->left)-geth(r->right)>1) {//失衡 LL LR if(k<r->left->data){//LLr=LL_rotation(r); } if(k>r->left->data){//LRr=LR_rotation(r);} } }if(k>r->data){r->right =insert(r->right ,k);// r->h=maxx(geth(r->left),geth(r->right))+1;//此时 在r的右边子树插入了一个节点 //r有可能失衡 RR RLif(geth(r->right)-geth(r->left)>1) {//失衡 RR RR if(k>r->right->data){//RR r=RR_rotation(r);} if(k<r->right->data){//RLr=RL_rotation(r);} } }//r的左子树或者右子树插入了一个节点,r的高度可能变化,重新求一下r->h=maxx(geth(r->left),geth(r->right))+1;//注意 return r;
}
//找前驱
AVLNode* find(AVLNode* p)
{while(p->right!=NULL){p=p->right;}return p;
}
//删除
AVLTree delet(AVLTree r,int k)
{if(k<r->data){r->left=delet(r->left,k);//r的左子树删除了一个节点,r有可能失衡if(geth(r->right)-geth(r->left)>1) {//"相当于"r的右子树中插入导致的失衡情况//RR RLAVLNode* rr=r->right;if(geth(rr->right)>=geth(rr->left)){//RRr=RR_rotation(r);}else{//RLr=RL_rotation(r);}}}else if(k>r->data){r->right =delet(r->right ,k);//r的右子树删除了一个节点,r有可能失衡if(geth(r->left )-geth(r->right )>1) {//"相当于"r的左子树中插入导致的失衡情况//LL LRAVLNode* rl=r->left ;if(geth(rl->left )>=geth(rl->right )){//LLr=LL_rotation(r);}else{//LRr=LR_rotation(r);}}}else{if(r->left!=NULL&&r->right!=NULL){AVLNode* q=find(r->left);r->data=q->data;r->left=delet(r->left,q->data);//r的左子树删除了一个节点,r有可能失衡if(geth(r->right)-geth(r->left)>1) {//"相当于"r的右子树中插入导致的失衡情况//RR RLAVLNode* rr=r->right;if(geth(rr->right)>=geth(rr->left)){//RRr=RR_rotation(r);}else{//RLr=RL_rotation(r);}}}else{AVLNode* q=r;if(r->left!=NULL){r=r->left;}else{r=r->right; }free(q);q=NULL;}}if(r!=NULL){r->h=maxx(geth(r->left),geth(r->right))+1;//注意 } return r;
}
void InOrder(AVLTree r)
{if(r!=NULL){InOrder(r->left);printf("%d %d\n",r->data,r->h);InOrder(r->right);}
}
int main()
{int n,x;int a[105];scanf("%d",&n);for(int i=1;i<=n;i++){scanf("%d",&a[i]);} AVLTree r=NULL; for(int i=1;i<=n;i++){r=insert(r,a[i]);}while(scanf("%d",&x)!=EOF){r=delet(r,x);InOrder(r); printf("\n");}return 0;} /*
9
8 3 10 1 6 14 4 7 13
*/
并查集
树的表现形式
并查集的实现原理也⽐较简单,就是⽤树来表示⼀个集合,树的每个结点都是集合的⼀个元素,树根对应的那个结点就是该集合的代表。
⽐如上图中的两棵树,就分别对应两个集合,其中第⼀个集合为a,b,c,d,代表元素是a,第⼆个集合是e,f,g,代表结合是e。
树的节点表示集合中的元素,指针表示指向⽗节点的指针,根节点的指针指向⾃⼰,表示其没有⽗节点。沿着每个节点的⽗节点不断向上查找,最终就可以找到该树的根节点,即该集合的代表元素。
现在,假设使⽤⼀个⾜够⻓的数组来存储树节点,那么 makeSet 要做的就是构造出如下图的森林,其中每个元素都是⼀个单元素集合,即⽗节点是其⾃身:
接下来就是find操作了
查找
查找操作就是找到i的祖先直接返回
合并
⽐如我们合并(4,3),(3,2),(2,1)。
如果此时我们合并了⼀万个结点,这个时候,其实从时间复杂度来说,查找的次数就⾮常⾮常多
了。
这个时候,我们就想出来了⼀个策略,路径压缩。
路径压缩
就是在每次查找时,令查找路径上的每个节点都直接指向根节点。
代码实现
#include <iostream>
using namespace std;
//双亲表示法存树
int f[10];
//非递归版查找根节点
//int find(int x){
// //找到x所在树的根节点
// while(f[x]!=x){
// x= f[x];
// }
// return x;
//}//递归版查找根节点
int find(int x){if(x==f[x]){ //如果是根节点直接返回return x;}return f[x] = find(f[x]);
}
int main(){cout<<"请输入集合总数:"<<endl;int n;cin>>n;cout<<"输入合并次数和查询次数"<<endl;int m1,m2;cin>>m1; cin>>m2;for(int i = 1; i <= n; i++){f[i] = i; //初始化,让每棵树的根节点的父亲是自己}cout<<"请输入要合并的两个集合中的元素:"<<endl;int x,y; //表示树中的元素for(int i = 0; i <m1; i++){//合并x和y所在的集合cin>>x;cin>>y;int fx = find(x); //找到x所在树的根节点int fy = find(y); //找到y所在树的根节点f[fy] =fx ; //fx作fy的根节点}cout<<"请输入要查询的元素:"<<endl;int xx,yy;for(int i = 0; i<m2; i++){//查询cin>>xx;cin>>yy;int fxx = find(xx);int fyy = find(yy);if(fxx==fyy){cout<<"两元素属于同一颗树"<<endl;}else{cout<<"两元素不属于同一颗树"<<endl;}}
}
哈夫曼树
概念⼀:什么是结点的路径⻓度
从根结点到该结点的路径上的连接数
结点k的路径⻓度就是从根结点A到结点k的路径上的连接数,也就是我们看到红⾊连边,也就是3。
概念⼆:什么是树的路径⻓度
就是树的每个叶⼦结点的路径⻓度之和
那么图中树的路径⻓度之和就是13.
概念三:什么是结点的带权路径⻓度?
结点的路径⻓度与结点权值的乘积。
⽐如规定结点K的权重/权值为4,结点K的路径⻓度为3,那么带权路径⻓度就是3 * 4 = 12。
概念四:什么是树的带权路径⻓度 WPL
就是树的所有叶⼦结点的带权路径⻓度之和。
应⽤场景
在数据膨胀、信息爆炸的今天,数据压缩的意义不⾔⽽喻。谈到数据压缩,就不能不提赫夫曼(Huffman)编码,赫夫曼编码是⾸个实⽤的压缩编码⽅案,即使在今天的许多知名压缩算法⾥,依然可以⻅到赫夫曼编码的影⼦。
另外,在数据通信中,⽤⼆进制给每个字符进⾏编码时不得不⾯对的⼀个问题是如何使电⽂总⻓最短且不产⽣⼆义性。根据字符出现频率,利⽤赫夫曼编码可以构造出⼀种不等⻓的⼆进制,使编码后的电⽂⻓度最短,且保证不产⽣⼆义性。
当有了以上概念以后,我们就可以⽤WPL去判断⼀颗⼆叉树的性能。WPL的值越⼩,⼆叉树的性能越优。
哈夫曼树的构造过程
⾸先,出给⼀个初始森林:
第⼀步,森林中选择出两颗结点的权值最⼩的⼆叉树
第⼆步:合并两颗⼆叉树,增加⼀个新的结点作为新的⼆叉树的根,权值为左右孩⼦的权值之和。
第三步:不断的将新的结点跟权值最⼩的结点相结合
所以说,其实哈夫曼树其实很简单。我们说,哈夫曼编码可以有效的压缩数据(压缩率依赖于数据的特性在20%~~90%不等)。我们要讲哈夫曼编码,继续来看⼏个名词解释。
## 哈夫曼编码
### 定⻓编码
像ASCII编码、Unicode编码。ASCII编码每⼀个字符使⽤8个bit,能够编码256个字符;Unicode 编码每个字符占16个bit,能够编码65536个字符,包含所有ASCII编码的字符。
假设我们要使⽤定⻓编码对由符号A,B,C,D和E构造的消息进⾏编码,对每个字符编码需要多少位呢?
⾄少需要3位,2个bit位不够表示五个字符,只能表示4个字符。
如果对DEAACAAAAABA进⾏编码呢?总共12个字符,每个字符需要3bit,总共需要36位。
定⻓编码的缺陷
浪费空间!对于仅包含ASCII字符的纯⽂本消息,Unicode使⽤的空间是ASCII的两倍,两种⽅式都会造成空间浪费;字符“ a”和“ e”的出现频率⽐“ q”和“ z”的出现频率⾼,但是他们都占⽤了相同位数的空间。要解决定⻓编码的缺陷,便有了下⾯的变⻓编码。
变⻓编码
单个编码的⻓度不⼀样,可以根据整体出现的频率来调节,出现的频率越⾼,编码⻓度越短。
变⻓编码优于定⻓编码的是,变⻓编码可以将短编码赋予平均出现频率较⾼的字符,同⼀消息的编码⻓度⼩于定⻓编码。
这个时候⼜有⼀个问题,字符有⻓有短,我们怎么知道⼀个字符从哪⾥开始,⼜从哪⾥结束呢?如果位数固定,就没这个问题了。
前缀属性
字符集当中的⼀个字符编码不是其他字符编码的前缀,则这个字符编码具有前缀属性。所谓前缀,⼀个编码可以被解码为多个字符,表示不唯⼀。⽐如,abcd需要编码表示,设a = 0,b = 10,c =110,d = 11。那么110可以表示c,也可以表示da。不理解没关系,来看个例⼦。
000不是11、01、001、10的前缀,000具有前缀属性。11不是000,01,001,10的前缀。⽐如01001101100010 可以翻译为 RSTQPT。
⼜⽐如
编码1110可以解码为QQQP、QTP,QQS和TS,对于任意⼀个编码,解码的结果不⼀样。
前缀码
所谓的前缀码,就是没有任何码字是其他码字的前缀。
问题
请设计变⻓的前缀编码,使⽤22位对消息DEAACAAAAABA进⾏编码。消息中字符A出现了8次,字符B、C、D、E均只出现了⼀次,字符A⽤⼀个bit表示,A = 0.其他字符的编码均不能以0开头,B =10.C = 110 D = 1110 E = 11110.刚刚好22位。当然也可以制定其他的⼏种。
所以哈夫曼编码就是⼀种前缀码。在构造这种编码的时候,就是⽤⼀颗哈夫曼树来进⾏构造的。
代码示例
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
//左 '1' 右 '0'
typedef struct
{int weight;//节点的权值int l;//左孩子位置(下标) int r;//右孩子位置(下标) int p; //父亲位置(下标) }HuffmanNode,*HuffmanTree;
void findNode(HuffmanTree tree,int l,int* s1,int* s2)
{int minn=0;//权值最小的两个根节点的下标//先找到一个根节点for(int i=1;i<=l;i++){if(tree[i].p==0){minn=i;break;}} for(int i=1;i<=l;i++){if(tree[i].p==0&&tree[i].weight<tree[minn].weight){minn=i;}}(*s1)=minn;//再找第二小的//先找到一个根节点(不能是*s1) for(int i=1;i<=l;i++){if(tree[i].p==0&&i!=(*s1)){minn=i;break;}} for(int i=1;i<=l;i++){if(i!=(*s1)&&tree[i].p==0&&tree[i].weight<tree[minn].weight){minn=i;}}(*s2)=minn;}
HuffmanTree CreateHuffmanTree(int w[],int n)
{int m=2*n-1;HuffmanTree tree=(HuffmanNode*)malloc(sizeof(HuffmanNode)*(m+1));if(tree==NULL){printf("空间分配失败");return NULL; }for(int i=1;i<=n;i++){tree[i].weight=w[i];tree[i].l=0;tree[i].r=0;tree[i].p=0; }int s1,s2;//添加n-1个节点 for(int i=n+1;i<=m;i++){findNode(tree,i-1,&s1,&s2);tree[i].weight=tree[s1].weight+tree[s2].weight;tree[i].p=0;tree[i].l=s1;tree[i].r=s2;tree[s1].p=tree[s2].p=i;//记得改父亲 }return tree;
}
char** CreateHuffmanCode(HuffmanTree tree,int n)
{char* t=(char*)malloc(sizeof(char)*n); //用于存储哈夫曼编码,编码长度小于n-1,但是最后yi'geif(t==NULL){printf("空间分配失败");return NULL; }char** codes=(char**)malloc(sizeof(char*)*(n+1)); //和s[i]下标对应,方便输出字符+哈夫曼编码if(codes==NULL){printf("空间分配失败");return NULL; }for(int i=0;i<n+1;i++){codes[i]=NULL; //防止其成为野指针}int start;int x,xp;for(int i=1;i<=n;i++){start=n-1;//倒着放编码 ( //哈夫曼编码从后往前放,因为要从下往上找出父节点)t[start]='\0'; ////t数组的最后一个位置放字符串结束标志x=i;xp=tree[i].p;while(xp!=0){start--;if(x==tree[xp].l){t[start]='1';}else{t[start]='0';}x=xp;xp=tree[x].p;}codes[i]=(char*)malloc(sizeof(char)*(n-start));strcpy(codes[i],&t[start]); } free(t);t=NULL;return codes;
}
int main()
{ int n;char s[105];int w[105];scanf("%d",&n);for(int i=1;i<=n;i++){getchar();scanf("%c %d",&s[i],&w[i]);}//构造哈夫曼树HuffmanTree tree=NULL;tree=CreateHuffmanTree(w,n);for(int i=1;i<2*n;i++){printf("%d ",tree[i].weight);}printf("\n");char** codes=CreateHuffmanCode(tree,n);for(int i=1;i<=n;i++) {printf("%c:%s\n",s[i],codes[i]);}return 0;
}
/*
9
a 1
g 1
m 1
t 1
e 2
h 23
i 3
s 5
*/