二叉树
二叉树是每个节点最多有两个子树的树结构。通常子树被称作“左子树”(left subtree)和“右子树”(right subtree)。
相关术语:
树的结点:包含一个数据元素及若干指向子树的分支;
孩子结点:结点的子树的根称为该结点的孩子;
双亲结点:B 结点是A 结点的孩子,则A结点是B 结点的双亲;
兄弟结点:同一双亲的孩子结点;
堂兄弟结点:同一层上不同双亲的结点;
祖先结点: 从根到该结点的所经分支上的所有结点
子孙结点:以某结点为根的子树中任一结点都称为该结点的子孙
层:根结点的层定义为1,根的孩子为第二层结点,依此类推;
树的深度:树中最大的层
结点的度:结点子树的个数
树的度: 树中最大的结点度。
叶子结点:也叫终端结点,是度为 0 的结点;
分枝结点:度不为0的结点;
有序树:子树有序的树,如:二叉树;
无序树:不考虑子树的顺序;
公式
(1) 在二叉树中,第i层的结点总数不超过2^(i-1);
(2) 深度为h的二叉树最多有2^h -1个结点(h>=1),最少有h个结点;
(3) 对于任意一棵二叉树,如果其叶结点数为N0,而度数为2的结点总数为N2,
则N0=N2+1;
(4) 具有n个结点的完全二叉树的深度为 log2n下取整 +1
(5)有N个结点的完全二叉树各结点如果用顺序方式存储,则结点之间有如下关系:
若I为结点编号则 如果I>1,则其父结点的编号为I/2下取整;
如果2I<=N,则其左儿子的编号为2I;若2*I>N,则无左儿子;
如果2I+1<=N,则其右儿子的结点编号为2I+1;若2*I+1>N,则无右儿子。
完全二叉树
叶节点只能出现在最下层和次下层,并且最下面一层的结点都集中在该层最左边的若干位置的二叉树。
若设二叉树的深度为h,除第 h 层外,其它各层 (1~h-1) 的结点数都达到最大个数,第 h 层所有的结点都连续集中在最左边。
叶子结点只可能在最大的两层上出现,对任意结点,若其右分支下的子孙最大层次为L,则其左分支下的子孙的最大层次必为L 或 L+1, 即度为1的点只有1个或0个。
满二叉树是特殊的完全二叉树,完全二叉树不一定是满二叉树。
满二叉树:
除了叶结点外每一个结点都有左右子叶且叶子结点都处在最底层的二叉树。
如果一个二叉树的层数为K,且结点总数是(2^k) -1 ,则它就是满二叉树。
平衡二叉树:
平衡二叉树又被称为AVL树,它是一棵二叉排序树,且具有以下性质:它是一棵空树或它的左右两个子树的高度差的绝对值不超过1,并且左右两个子树都是一棵平衡二叉树。
遍历顺序:
设L、D、R分别表示遍历左子树、访问根结点和遍历右子树, 则对一棵二叉树的遍历有三种情况:DLR(称为先根次序遍历),LDR(称为中根次序遍历),LRD (称为后根次序遍历)。
- 先序遍历
首先访问根,再先序遍历左(右)子树,最后先序遍历右(左)子树,C语言代码如下:
void XXBL( tree* root ){
输出节点;
if( root->lchild!=NULL )
XXBL( root->lchild );
if( root->rchild!=NULL )
XXBL( root->rchild );
}
- 中序遍历
递归实现
首先中序遍历左(右)子树,再访问根,最后中序遍历右(左)子树,C语言代码如下
void ZXBL( tree* root )
{
if( root->lchild!=NULL )
ZXBL( root->lchild );
输出节点;
if( root->rchild!=NULL )
ZXBL( root->rchild );
}
非递归实现
根据中序遍历的顺序,对于任一结点,优先访问其左孩子,而左孩子结点又可以看做一根结点,然后继续访问其左孩子结点,直到遇到左孩子结点为空的结点才进行访问,然后按相同的规则访问其右子树。因此其处理过程如下:
对于任一结点P,
1)若其左孩子不为空,则将P入栈并将P的左孩子置为当前的P,然后对当前结点P再进行相同的处理
2)若其左孩子为空,则取栈顶元素并进行出栈操作,访问该栈顶结点,然后将当前的P置为栈顶结点的右孩子
3)直到P为NULL并且栈为空则遍历结束
void inOrder(BinTree *root) //非递归中序遍历 {
stack s;
BinTree *p=root; while(p!=NULL||!s.empty())
{
while(p!=NULL)
{
s.push(p);
p=p->lchild;
}
if(!s.empty())
{
p=s.top();
cout<data<<" ";
s.pop();
p=p->rchild;
}
}
}
- 后序遍历
首先后序遍历左(右)子树,再后序遍历右(左)子树,最后访问根,C语言代码如下
void HXBL( tree* root ){
if( root->lchild!=NULL )
HXBL( root->lchild );
if( root->rchild!=NULL )
HXBL(root->rchild);
输出节点;
}
- 层次遍历
即按照层次访问,通常用队列来做。访问根,访问子女,再访问子女的子女(越往后的层次越低)(两个子女的级别相同)
void BiTree::LevelOrder(BiTreeNode *t)
{//用队列实现
queuetq;//创建队列tq,队列的每个元素都是结点指针
BiTreeNode* p=t;
if(!p==NULL)
{
tq.push(p);
}
while(!tq.empty())
{
p = tq.front();
cout << p->data;
tq.pop();
if(p->LeftChild)
{tq.push(p->LeftChild);}
if(p->RightChild)
{tq.push(p->RightChild);}
}
}
统计叶子结点数
int getLeafNode(BiTree T)
{
if(NULL == T)
return 0;
if(NULL == T->leftChild && NULL == T->rightChild)
return 1;
return getLeafNode(T->leftChild) + getLeafNode(T->rightChild);
}
求结点个数
int GetNodeCount(BTree* pRoot)
{
if (pRoot == NULL)
return 0;
int LeftNum = GetNodeCount(pRoot->m_nLeft);
int RightNum = GetNodeCount(pRoot->m_nRight);
int ret = LeftNum+RightNum+1;
return ret;
}
求二叉树深度
int GetTreeDepth(BTree* pRoot)
{
if (pRoot == NULL)
return 0;
int LeftDepth = GetTreeDepth(pRoot->m_nLeft);
int RightDepth = GetTreeDepth(pRoot->m_nRight);
int ret = max(LeftDepth,RightDepth)+1;
return ret;
}
线索二叉树
在结点结构中增加两个标志域LTag和RTag。LTag=0时,lchild域指示结点的左孩子,LTag=1时,lchild域指示结点的前驱;RTag=0时,rchild域指示结点的右孩子,RTag=1时,rchild域指示结点的后继。
以这种结点结构构成的二叉线索链表,链表作为二叉树的存储结构,叫做其中指向结点前驱和后继的指针叫做线索。对二叉树以某种次序遍历使其变为线索二叉树的过程叫做线索化。
- 线索二叉树的存储结构
在中序线索树找结点后继的规律是:
若其右标志为1,则右链为线索,指示其后继,否则遍历其右子树时访问的第一个结点(右子树最左下的结点)为其后继;找结点前驱的规律是:若其左标志为1,则左链为线索,指示其前驱,否则遍历左子树时最后访问的一个结点(左子树中最右下的结点)为其前驱。
在后序线索树中找到结点的后继分三种情况:
若结点是二叉树的根,则其后继为空;若结点是其双亲的右孩子,或是其双亲的左孩子且其双亲没有右子树,则其后继即为双亲结点;若结点是其双亲的左孩子,且其双亲有右子树,则其后继为双亲右子树上按后序遍历列出的第一个结点(最左下角的叶子结点)。
二叉搜索树
- 查找从根结点开始,如果树为空,返回NULL
- 若搜索树非空,则根结点关键字和X进行比较,并进行不同处理:
- 若X小于根结点键值,只需在左子树中继续搜索
- 若X大于根结点的键值,在右子树中继续进行搜索
- 若两者比较结果相等,搜索完成,返回指向此结点的指针。
查找
Positon Find(ElementType X, BinTree BST)
{
if(!BST)
return NULL; // 查找失败
if(X > BST->Data)
return Find(X, BST->Right); // 在右子树中继续查找
else if (X < BST->Data)
return Find(X, BST->Left); // 在左子树中继续查找
else // X == BST->Data
return BST; // 查找成功,返回结点的地址
}
插入
BinTree Insert(ElementType X, BinTree BST)
{
if( !BST )
{
// 若原树为空,生成并返回一个结点的二叉搜索树
BST = malloc( sizeof( struct TreeNode ) );
BST->Data = X;
BST->Left = BST->Right = NULL;
}
else // 开始找要插入元素的位置
{
if(X < BST->Data)
BST->Left = Insert(X, BST->Left); // 递归插入左子树
else if(X > BST->Data)
BST->Right = Insert(X, BST->Right); // 递归插入右子树
// else X已经存在,什么都不做
}
return BST;
}
删除
要考虑三种情况
要删除的是叶节点:直接删除,并再修改其父结点指针,置为NULL
要删除的结点只有一个孩子结点:将其父结点的指针指向要删除结点的孩子结点
要删除的结点有左、右两颗子树:用另一结点替代被删除结点:右子树的最小元素或者左子树的最大元素
BinTree Delete(ElementType X, BinTree BST)
{
Position Tmp;
if(!BST)
printf("要删除的元素未找到");
else if(X < BST->Data)
BST->Left = Delete(X, BST->Left); // 左子树递归删除
else if(X > BST->Data)
BST->Right = Delete(X, BST->Right); // 右子树递归删除
else // 找到要删除的结点
{
if(BST->Left && BST->Right) // 被删除结点有左右两个子结点
{
Tmp = FindMin(BST->Right); // 在右子树中找最小的元素填充删除结点
BST->Data = Tmp->Data;
BST->Right = Delete(BST->Data, BST->Right); // 在删除结点的右子树中删除最小元素
}
else // 被删除结点有一个或无子结点
{
Tmp = BST;
if(!BST->Left) // 有右孩子或无子结点
BST = BST->Right;
else if(!BST->Right) // 有左孩子或无子结点
BST = BST->Left;
free(Tmp);
}
}
return BST;
}
哈夫曼树(Huffman)
哈夫曼树树又称最优二叉树,是指对于一组带有确定权值的叶子结点所构造的具有带权路径长度最短的二叉树。
从树中一个结点到另一个结点之间的分支构成了两结点之间的路径,路径上的分支个数称为路径长度。
二叉树的路径长度是指由根结点到所有叶子结点的路径长度之和。如果二叉树中的叶子结点都有一定的权值,则可将这一概念拓展:设二叉树具有n个带权值的叶子结点,则从根结点到每一个叶子结点的路径长度与该叶子结点权值的乘积之和称为二叉树路径长度,记做:
WPL=W1L1+W2L2+......+WnLn;(n为二叉树中叶子结点的个数;Wk为第k个叶子的权值;Lk为第k个叶子结点的路径长度)
哈夫曼算法:
(1)根据给定n个权值{w1,w2,....,wn}构成n棵二叉树的集合F={T1,T2,.....,Tn};其中,每棵二叉树Ti(1<=i<=n)只有一个带权值wi的根结点,其左、右子树均为空。
(2)在F中选取两棵根结点权值最小的二叉树作为左、右子树来构造一棵新的二叉树,且置新的二叉树根结点权值为其左右子树根结点的权值之和。
(3)在F中删除这两棵树,同时将生成新的二叉树加入到F中。
(4)重复(2)(3),直到F中只剩下一棵二叉树加入到F中。
要进行n-1次合并才能使初始化的n棵二叉树最终合并为一棵二叉树,因此n-1次合并共产生了n-1个新结点,即最终生成的哈夫曼树共有2n-1个结点。
由于每次都是将两棵权值最小的二叉树合并生成一棵新二叉树,所以生成的哈夫曼树中没有度为1的结点。
两棵权值最小的二叉树那棵作为作为左子树、那棵作为右子树,哈夫曼算法并没有要求,故最终构造出来的哈夫曼树并不唯一,但是最小的WPL值是唯一的。
所以,哈夫曼具有如下几个特点:
(1)对给定的权值,所构造的二叉树具有的最小WPL;
(2)权值大的结点离根近,权值小的结点离根远;
(3)所生成的二叉树不唯一
(4)没有度为1的结点