二叉树理解
n(n>=0)个结点的有限集合,由一个根结点和两个互不相交的子结点,左子树和右子树组成的树形结构。
二叉树的性质
- 在二叉树的第i层上至多有2^(i-1)个结点(i>0) 如上图第3层有 结点数:2^(3-1) = 4
- 具有n个结点的完全二叉树的深度必为log₂n+1 如上图有结点7个 深度:log₂7+1 = 3(对数化简 log₂N = n <==> 2ⁿ = N )
- 深度为k的二叉树至多有2^(k)-1个结点(k>=0) 如上图深度为3 结点数:2^(3) -1 = 7
- 对于完全二叉树,若从上至下、从左至右编号,则编号为i 的结点,其左孩子编号必为2i,其右孩子编号必为2i+1;其双亲的编号必为i/2(i=1 时为根,除外)
满二叉树和完全二叉树
满二叉树:深度为k且有2^(k)-1个结点的二叉树。(每个根结点下都有两个子结点)
完全二叉树:深度为k的,有n个结点的二叉树。(k-1层为满二叉树 k层结点尽量靠左)
二叉树的表示
二叉链表法
在结点中包含了一个数据域和两个指针域,指针域分别对应存储左子树结点和右子树结点。
typedef struct tag_BiTreeNode
{
int data;
struct tag_BiTreeNode *leftChild, *RightChild;//左右子树结点
}TBiTree;
三叉链表法
结点中包含了一个数据域和三个指针域,三个指针域分别对应存储左子树结点、右子树结点,以及根结点。
typedef struct tag_BiTreeNode
{
int data;
struct tag_BiTreeNode* leftChild, *RightChild;//左右子树结点
struct tag_BiTreeNode* parent;//根结点
}TBiTree;
双亲链表法
提供两个结构体,分别为结点结构和树结构,前者保存自身数据、根结点位置下标、左右子树结点标志。后者保存树的所有结点到数组、结点数量(数组大小)、根结点数组下标。如下图通过回溯很容易表达出一颗树。
typedef struct tag_TreeNode
{
int data; //数据
int parentPos; //父结点的数组下标
char LRTag; //左右子结点标志
}TTreeNode;
typedef struct tag_BiTree
{
TTreeNode node[100]; //结点是分散的,需要存储到数组中管理
int node_num; //结点数目
int root; //根结点的位置,根结点数组下标
}TBiTree;
二叉树的遍历
树的遍历有三种方式:先序遍历、中序遍历、后序遍历
先序遍历(先根再左再右)
中序遍历(先左再根再右)
后序遍历(先左再右再根)
//先序遍历
void Preorder(TBiTree *root)
{
if (root == NULL)
return;
printf("%d ", root->data);
Preorder(root->leftChild);
Preorder(root->RightChild);
}
//中序遍历
void Inorder(TBiTree *root)
{
if (root == NULL)
return;
Inorder(root->leftChild);
printf("%d ", root->data);
Inorder(root->RightChild);
}
//后序遍历
void Postorder(TBiTree *root)
{
if (root == NULL)
return;
Postorder(root->leftChild);
Postorder(root->RightChild);
printf("%d ", root->data);
}
从递归遍历来看,如果不看输出语句,这三种方式是完全一致的,那么到底有什么区别呢?从下图可以看出,从虚线的起点到终点,每个结点都经过了3次,而上面三种不同的遍历说明:其结点的访问路径是相同的,只是访问的时机不同。
求叶子结点的个数
通过从根结点开始一直向下遍历,如果当前结点下还有结点重复向下遍历,如果当前结点不是树(没有左子树结点且没有右子树结点)那么子结点数累加。
void CountLeaf(TBiTree *root,int *sum)
{ //判断为非空树
if (root != NULL)
{
//如果是叶子结点,数量自加
if (root->leftChild == NULL && root->RightChild == NULL)
sum++;
//递归遍历左子树,直到叶子处
if (root->leftChild != NULL)
{
CountLeaf(root->leftChild,sum);
}
//递归遍历右子树,直到叶子处
if (root->RightChild != NULL)
{
CountLeaf(root->RightChild,sum);
}
}
}
求树的深度
求根结点左子树高度,根结点右子树高度,比较的子树最大高度,再+1。
若左子树还是树,重复步骤1;若右子树还是树,重复步骤1。
int Depth(TBiTree *root)
{
int depthL = 0;
int depthR = 0;
int depthH = 0;
if (root == NULL)
{
depthH = 0;
return depthH;
}
depthL = Depth(root->leftChild);
depthR = Depth(root->RightChild);
depthH = 1 + (depthL > depthR ? depthL : depthR);
return depthH;
}
copy二叉树
malloc新结点,
拷贝左子树,拷贝右子树,让新结点连接左子树,右子树
若左子树还是树,重复步骤1、2;若右子树还是树,重复步骤1、2。
TBiTree* CopyTree(TBiTree *root)
{
TBiTree *newNode = NULL;
TBiTree *lNode = NULL;
TBiTree *rNode = NULL;
if (root == NULL)//递归终止条件
return NULL;
if (root->leftChild != NULL)
lNode = CopyTree(root->leftChild);//当前层级递归该做什么(保存左节点)
else
lNode = NULL;
if (root->RightChild != NULL)
rNode = CopyTree(root->RightChild);//当前层级递归该做什么(保存有节点)
else
rNode = NULL;
newNode = (TBiTree *)malloc(sizeof(TBiTree));
if (newNode == NULL)
return NULL;
newNode->leftChild = lNode;
newNode->RightChild = rNode;
newNode->data = root->data;
return newNode;//当前层级应该返回给上一层一个节点
}
树的非递归遍历(中序遍历)
步骤1:
如果结点有左子树,该结点入栈;
如果结点没有左子树,访问该结点;
步骤2:
如果结点有右子树,重复步骤1;
如果结点没有右子树(结点访问完毕),根据栈顶指示回退,访问栈顶元素,并访问右子树,重复步骤1
如果栈为空,表示遍历结束。
TBiTree* GoLeft(TBiTree* root,stack<TBiTree *> &s)
{
if (root == NULL)
return NULL;
while (root->lChild != NULL)
{
s.push(root);
root = root->lChild;
}
return root;
}
void Inorder2(TBiTree *root)
{
TBiTree* node = NULL;
stack<TBiTree *> s;
node = GoLeft(root, s);
while (node != NULL)
{
printf("%d ", node->data);
if (node->rChild != NULL)
{
node = GoLeft(node->rChild, s);
}
else if(!s.empty())
{
node = s.top();
s.pop();
}
else
{
node = NULL;
}
}
}
二叉树的创建
中序和先序创建树
假设给定遍历后的树,先序、中序、后序,怎样组合可以确定一颗树呢?
结论:
- 通过中序遍历和先序遍历可以确定一个树
- 通过中序遍历和后续遍历可以确定一个树
- 通过先序遍历和后序遍历确定不了一个树
试着通过先序和中序的组合画出一棵树,
先序遍历结果:ABDHKECFIGJ
中序遍历结果:HKDBEAIFCGJ
#号法创建树
#创建树,让树的每一个节点都变成度数为2的树
关键点:要清楚的确定左子树什么结束,右子树什么时候开始。
先序遍历:ABDH#K###E##CFI###G#J##,请画出树的形状
#pragma warning(disable : 4996)
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
typedef struct tag_BiTree
{
int data;
struct tag_BiTree *lChild, *rChild;
}TBiTree;
//#法创建树
TBiTree* CreatTree()
{
TBiTree *node = NULL;
TBiTree *lNode = NULL;
TBiTree *rNode = NULL;
char c;
scanf("%c", &c);
if (c == '#')
{
return NULL;
}
else
{
node = (TBiTree *)malloc(sizeof(TBiTree));
memset(node, 0, sizeof(TBiTree));
node->data = c;
lNode = CreatTree();
if (lNode != NULL)
{
node->lChild = lNode;
}
else
{
node->lChild = NULL;
}
rNode = CreatTree();
if (rNode != NULL)
{
node->rChild = rNode;
}
else
{
node->rChild = NULL;
}
}
return node;
}
//先序遍历
void Preorder(TBiTree *root)
{
if (root == NULL)
return;
printf("%c ", root->data);
Preorder(root->lChild);
Preorder(root->rChild);
}
//释放树
void FreeTree(TBiTree *root)
{
if (root == NULL)
return;
if (root->lChild != NULL)
FreeTree(root->lChild);
if (root->rChild != NULL)
FreeTree(root->rChild);
if (root != NULL)
{
printf("\nfree data = %c", root->data);
free(root);
}
}
int main(int argc, char** argv)
{
TBiTree *node = CreatTree();
Preorder(node);
FreeTree(node);
system("pause");
return 0;
}
二叉线索树
简单理解就是通过一个结点可以遍历获取到整个树,类似于双向链表,通过一个结点可以获取到前驱结点和后继结点。有了双向链表这种数据结构我们就可以在创建树时把结点加入到链表中,就不用通过二叉线索树来遍历整个树了,但是二叉线索树思想还是要学一下的。
typedef struct BiThrNode
{
char data;//结点数据
struct BiThrNode *lchild, *rchild;//左右孩子指针
int LTag;//左标志
int RTag;//右标志
} BiThrNode;
在二叉线索树的结点中除了包含左右子树结点的指针域还要包含两个标志域
规定:
- 若结点有左子树,则LChild指向其左孩子;否则,LChild指向其直接前驱(即线索)
- 若结点有右子树,则RChild指向其右孩子;否则,RChild指向其直接后继(即线索)
非递归遍历
int InOrderTraverse_Thr(BiThrNode* T)
{
BiThrNode* p;
p = T->lchild; /* p指向根结点 */
while (p != T)
{
/* 空树或遍历结束时,p==T */
while (p->LTag == Link)
p = p->lchild;
printf("%c ", p->data);
while (p->RTag==Thread && p->rchild!=T)
{
p = p->rchild;
printf("%c ", p->data);
}
p=p->rchild;
}
return 0;
}
霍夫曼树
组建一个网络,耗费最小 WPL(树的带权路径长度)最小;这个方法是霍夫曼想出来的,称为霍夫曼树,也称最优二叉树。
理解路径、路径长度、带权路径、带权路径长度
路径:从根结点出发到达每个子结点或孙子结点经过的路线就是路径。
路径长度:路径上的分支数目称作路径长度。根结点到第n层的路径长度为n - 1。 树的路径长度是从树根到每一结点的路径长度之和。
例如上图,树的路径长度为:1+1+2+2 = 6
带权路径:上图为一个带权路径,每条路径上的数字是一个有着某种规则的权。
带权路径长度:结点的带权路径长度为从该结点到树根之间的路径长度与结点上权的乘积。树的带权路径长度为树中所有叶子结点的带权路径长度之和,称为WPL(树的带权路径长度),其中带权路径长度WPL最小的二叉树称作霍夫曼树(最优二叉树)。
例如上图的二叉树 WPL = 5*2+1*2+2*1 = 14
什么是霍夫曼树
上面讲过了,带权路径长度WPL最小的二叉树就叫作霍夫曼树(最优二叉树)
叶子结点ABCD可以构成多种形态的二叉树,相比于二叉树a,二叉树a的WPL值更小,因此二叉树b为最优二叉树。
二叉树a WPL = 5*2+3*2+1*2+2*2 = 22
二叉树b WPL = 5*1+3*2+1*3+2*3 = 20
霍夫曼树的构造
对于文本”BADCADFEED”的传输而言,因为重复出现的只有”ABCDEF”这6个字符,因此可以用下面的方式编码:
编码规则:从根节点出发,向左标记为0,向右标记为1。
上图的编码的二叉树为:
由上图看,每个子结点的路径长度都相同,如果这样做对于字符的发送和接收是很低效率且容易出错的,为了提高效率和精确度。我们得优化二叉树。假设经过统计ABCDEF在需要传输的报文中出现的概率如下
每次选择两个概率小的结点(左小右大),构成一颗新二叉树,把他们的概率加起来构成一个新的结点再去同其他结点构成新的二叉树,每次把用过的结点删除,直到形成一颗树为止,这棵树即霍夫曼树。
通过优化形成新的编码规则,这样一来 每一个字符的编码路径,都不包含另外一个字符的路径。
霍夫曼树是一种特殊的二叉树
霍夫曼树应用于信息编码和数据压缩领域
霍夫曼树是现代压缩算法的基础