二叉树
树¶
Part I | Part II |
---|---|
二叉树 | 哈夫曼树 |
二叉树¶
二叉树的定义
二叉树的先序遍历 DLR
bool PreOrderTraverse(BiTree &T)
{
if(T == nullptr)
{
return true;
}
//访问根结点
visit(T);
//递归遍历左孩子
PreOrderTraverse(T->lchild);
//递归遍历右孩子
PreOrderTraverse(T->rchild);
return true;
}
如何理解这个算法?通过画图,描述函数的调用时机,其实还是比较简单
以这个三结点的二叉树为例
三结点二叉树
算法执行过程
二叉树的中序遍历 LDR
bool InOrderTraverse(BiTree &T)
{
if (T == nullptr)
{
return true;
}
//第一步:访问左孩子
InOrderTraverse(T->lchild);
//第二步:访问根结点
visit(T);
//第三步:访问右孩子
InOrderTraverse(T->rchild);
return true;
}
二叉树的后序遍历 LRD
bool PostOrderTraverse(BiTree &T)
{
if (T == nullptr)
{
return true;
}
//第一步:访问左孩子
PostOrderTraverse(T->lchild);
//第二步:访问右孩子
PostOrderTraverse(T->rchild);
//第三步:访问根结点
visit(T);
return true;
}
三种算法的时间复杂度:O(n),空间复杂度:O(n)
三种算法的思想都是DFS(深度优先算法)
二叉树的中序遍历——非递归算法
void LDR(BiTree &T)
{
//第一步:创建一个栈,用于保存二叉树的结点
SqStack S;
InitSqStack(S);
BiTree p = T;
while( p || !IsEmpty(S))
{
if(p)
{
//根结点进栈,遍历左子树
Push(S, p);
p = p->lchild;
}
else
{
//根结点出栈,输出根结点,遍历右子树
Pop(S, p);
visit(p);
p = p->rchild;
}
}
}
二叉树的层次遍历算法
void LevelOrder(BiTree &S)
{
/*
算法设计思路:
1.将根结点入队
2.队列不为空时循环,从队列中出列一个元素,访问它,并作以下步骤:
2.1 如果该元素的左孩子不为空,让该元素的左孩子入队
2.2 如果该元素的右孩子不为空,让该元素的右孩子入队
*/
SqQueue Q;
InitSqQueue(Q); //初始化队列
BiTree p;
PushQueue(Q, S); //根结点指针进入队列
while (!QueueEmpty(Q)) //队不为空,则循环
{
//将根结点出队
DeQueue(Q, p);
//访问根结点
cout << p->data << endl;
//if判断,是否能将根结点的左右孩子进队
if (p->lchild != nullptr)
{
PushQueue(Q, p->lchild); //有左孩子时将其进队
}
if (p->rchild != nullptr)
{
PushQueue(Q, p->rchild); //有右孩子时将其进队
}
}
}
二叉树的建立(DLR先序遍历,递归算法)
bool CreatBiTree(BiTree &T)
{
ElemType input;
cin >> input;
if(input == -1)//建立空结点的标志为 -1(这个自己设定一个就好)
return false;
T = new BiNode;
//D
T->data = input;
//L
CreatBiTree(T->lchild);
//R
CreatBiTree(T->rchild);
return true;
}
二叉树的复制
bool CopyBiTree(const BiTree& T, BiTree& NewT)
{
if (T == nullptr) { //如果是空树返回0
return false;
}
NewT = new BiNode; //申请新结点空间
//D
NewT->data = T->data; //复制根结点
//L
CopyBiTree(T->lchild, NewT->lchild); //递归复制左子树
//R
CopyBiTree(T->rchild, NewT->rchild); //递归复制右子树
return true;
}
求二叉树的深度
int Depth(BiTree &T)
{
if(T == nullptr)
{
return 0;
}
int m = Depth(T->lchild);
int n = Depth(T->rchild);
if(m>n) //二叉树的深度为m与n的较大者加1
return m+1;
else
return n+1;
}
如何理解这个算法?
在我看来,二叉树的深度计算算法,是把二叉树的递归调用运用的淋漓尽致
首先,二叉树 = 根结点+左子树+右子树;右子树=根结点+左子树+右子树;左子树=根结点+左子树+右子树。。。如此循环反复,因此,我们对二叉树模型进行运算,只需要像剥洋葱一样,一层一层把二叉树剖开,把一个复杂二叉树的运算求解问题,分解成一个个的单结点二叉树(叶子结点)问题的累加,就会比较方便
这个深度计算算法,就是这个思想。用递归函数一层层分解二叉树,递归的到叶子结点。例如,当我们第一个叶子结点的的左孩子和右孩子都是空时,叶子结点的m和n都等于0,再通过一个if判断语句,让m和n中的较大值加1。也即叶子结点返回1,这个返回值就放在叶子结点的双亲的m中,再去看这个双亲的右孩子,循环反复,最后就能得到二叉树的深度。
求二叉树的结点数
int CountNode(BiTree &T)
{
if (T == nullptr)
{
return 0;
}
// //L
// int m = CountNode(T->lchild);
// //R
// int n = CountNode(T->rchild);
// //
// return m + n + 1;
//更加简单的语句
//结点个数为:左子树的结点个数+右子树的结点个数+1
return CountNode(T->lchild) + CountNode(T->rchild) + 1;
}
求二叉树的叶子结点数
//王卓老师的视频范例
int Count0Node(BiTree &T)
{
//③:递归函数将上一个结点剖分成左右子树,如果结点的孩子为空,那么返回0
//这里不会出现结点的两个孩子都是空的,因为上一个结点执行这个递归函数的时候就已经判断了这种情况
//这个语句只是为了以下两种情况而存在的:
//1. 空树
//2. 某个分支结点只有一个孩子
if (T == nullptr)
{
return 0;
}
//①:其实呢,我们还是可以把这个问题拆分成左子树和右子树的统计问题
if (T->lchild == nullptr && T->rchild == nullptr)
{
//以T为根结点的这棵树,左右孩子都没有,那他就是叶子结点
return 1;
}
//②:如果不是这个情况,就说明这个根结点至少有一个孩子,还要继续剖分这个结点
return Count0Node(T->lchild) + Count0Node(T->rchild);
}
//我自己写的程序,确实有一点拉胯
/*
参数n:用于统计叶子结点数
参数flag:用于判断某个结点的两个孩子是否都为空
*/
bool Count0Node(BiTree &T, int &n, int &flag)
{
if (T == nullptr)
{
flag = 1; //标志
return false;
}
if (flag == 1)
{
++n;
flag = 0;
}
//L
Count0Node(T->lchild, n, flag);
//R
Count0Node(T->rchild, n, flag);
return true;
}
哈夫曼树¶
哈夫曼树的定义
typedef struct HNode
{
int weight; //权重
int parent, lchild, rchild; //每个结点的双亲、左右孩子的数组下标
} * HuffmanTree;
哈夫曼树的初始化
void InitHTree(HuffmanTree& H, const int n)
{
//哈夫曼树的存储结构为顺序存储
//由哈夫曼树的构造过程得知,n个权重结点构造出的哈夫曼树具有2*n-1个结点
//通常哈夫曼树的顺序存储结构下标从1开始计数,因此,如果我们使用数组实现的话
//那么数组的长度应该是2*n
H = new HNode[2 * n];
for (int i = 1; i < 2 * n; ++i) //将2n-1个元素的lchild,rchild,parent置为0
{
H[i].parent = H[i].lchild = H[i].rchild = 0;//右结合律
}
int input;
for (int i = 1; i <= n; ++i)
{
cin >> input;
H[i].weight = input; //输入前n个元素的weight值
}
}
哈夫曼树的构造算法
void CreatHuffman(HuffmanTree &H, const int length)
{
//第一步:对哈夫曼树进行初始化
InitHTree(H, length);
//第二步:找出当前森林中最小的两棵树,创建新树,并让原来的两个树作为新树的孩子
for (int i = length + 1; i < 2 * length; ++i) //合并产生n-1个结点——构造Huffman树
{
int i1 = 0, i2 = 0;
Select(H, i - 1, i1, i2);//重点是这个Select算法
H[i].weight = H[i1].weight + H[i2].weight;//i的权值为左右孩子权值之和
H[i1].parent = H[i2].parent = i; //表示从F中删除s1,s2
H[i].lchild = i1; //s1,s2分别作为i的左右孩子
H[i].rchild = i2;
}
}
Select算法
void Select(HuffmanTree &H, const int n, int &i1, int &i2)
{
vector<int> vec;
for (int i = 1; i <= n; ++i)
{
if (H[i].parent == 0)
{
vec.push_back(i);
}
}
//找出最小的一个
auto flag1 = vec.begin();
for (auto it = vec.begin() + 1; it != vec.end(); ++it)
{
if (H[*it].weight < H[*flag1].weight)
{
flag1 = it;
}
}
i1 = *flag1; //最小的元素下标
vec.erase(flag1);
auto flag2 = vec.begin();
for (auto it = vec.begin() + 1; it != vec.end(); ++it)
{
if (H[*it].weight < H[*flag2].weight)
{
flag2 = it;
}
}
i2 = *flag2; //第二小的元素的下标
}
void HuffmanCode(HuffmanTree &H, const int n)
{
//第一步:调用函数创建一个顺序存储结构的哈夫曼树,同上的函数一样
CreatHuffman(H, n);
//第二步:遍历哈夫曼树中每一个叶子结点,也即哈夫曼数组中的前n个元素
for (int i = 1; i <= n; ++i)
{
int chd = i;
int par = H[chd].parent;
//自下而上得到哈夫曼编码,用栈来保存再合适不过了
SqStack S;
InitStack(S);
while (par != 0) //从叶子结点开始向上回溯,直到根结点
{
H[par].lchild == chd ? /*左孩子,0进栈*/ Push(S, 0) : /*右孩子,1进栈*/ Push(S, 1);
//继续向上回溯
chd = par;
par = H[chd].parent;
}
//出栈//黑框中打印编码
while (!IsEmpty(S))
{
int out;
Pop(S, out);
cout << out;
}
cout << endl;
}
}
有注意到一个问题,在严蔚敏版《数据结构》C语言一书中,在哈夫曼树的构造算法中,两个拥有相同双亲的叶子结点,左右位置是可以相互交换的。这会让人产生困惑:由相同的权重结点构造出来的哈夫曼树是不唯一的,那么我通过哈夫曼树的得到编码也是不唯一的,这怎么搞?
本篇完~