这几天面试都碰到了二叉树,在此将问题总结下,以备今后再遇。
参考文献:
1. 数据结构与算法分析——c语言描述
2. 百度百科 http://baike.baidu.com/view/88806.htm http://baike.baidu.com/view/127820.htm
定义:二叉树是一棵树,其中每个节点都不能有多于两个儿子节点
性质:
1、二叉树的平均深度要比N小,为O(N^(1/2));二叉查找树,其深度的平均值为O(logN)
2、二叉树分为满二叉树和完全二叉树。
满二叉树:所有非叶子节点都达到最大的二叉树
完美二叉树:叶子节点仅在层次最大的两层出现,对于任意节点,左子树的高度等于右子数的高度加1
3、第i层的节点总数不超过2^(i-1)
4、深度为h的二叉树,最多有2^h-1个节点(h>=1),最少有h个节点
5、对于任意二叉树,如果其叶子节点数为N0,而度数为2的节点总数为N2,则有N0=N2+1
6、具有n个节点的完全二叉树,其深度为int(log2n)+1
7、有N个节点的完全二叉树,如果采用顺序方式存储,则节点所在顺序表中的位置有如下关系:
若节点i不为根节点,则其父节点编号为i/2;
若2*i<=N,则其左儿子的编号为i*2,若2*i>N,则无左儿子;
若2*i+1<=N,则其右儿子的编号为2*i+1;若2*i>N,则无右儿子。
8、给定N个节点,能构成h(N)中不同的二叉树。
h(N)为卡特兰树德第N项, h(N) = C(n, 2n)/(n+1)
类型:
1、二叉查找树:对于树中的每个节点x,其左子树的所有关键字的值小于关键字x的值,右子树中所有关键字的值,大于x
关于二叉查找树的删除,存在三种情况:
2、一般二叉树
参考文献:
1. 数据结构与算法分析——c语言描述
2. 百度百科 http://baike.baidu.com/view/88806.htm http://baike.baidu.com/view/127820.htm
定义:二叉树是一棵树,其中每个节点都不能有多于两个儿子节点
性质:
1、二叉树的平均深度要比N小,为O(N^(1/2));二叉查找树,其深度的平均值为O(logN)
2、二叉树分为满二叉树和完全二叉树。
满二叉树:所有非叶子节点都达到最大的二叉树
完美二叉树:叶子节点仅在层次最大的两层出现,对于任意节点,左子树的高度等于右子数的高度加1
3、第i层的节点总数不超过2^(i-1)
4、深度为h的二叉树,最多有2^h-1个节点(h>=1),最少有h个节点
5、对于任意二叉树,如果其叶子节点数为N0,而度数为2的节点总数为N2,则有N0=N2+1
6、具有n个节点的完全二叉树,其深度为int(log2n)+1
7、有N个节点的完全二叉树,如果采用顺序方式存储,则节点所在顺序表中的位置有如下关系:
若节点i不为根节点,则其父节点编号为i/2;
若2*i<=N,则其左儿子的编号为i*2,若2*i>N,则无左儿子;
若2*i+1<=N,则其右儿子的编号为2*i+1;若2*i>N,则无右儿子。
8、给定N个节点,能构成h(N)中不同的二叉树。
h(N)为卡特兰树德第N项, h(N) = C(n, 2n)/(n+1)
类型:
1、二叉查找树:对于树中的每个节点x,其左子树的所有关键字的值小于关键字x的值,右子树中所有关键字的值,大于x
下面是二叉查找树的定义及常用方法的C实现:
struct TreeNode;
typedef struct TreeNode *Position;
typedef struct TreeNode *SearchTree;
typedef int ElementType;
SearchTree MakeEmpty(SearchTree T);
Position Find(ElementType x, SearchTree T);
SearchTree Insert(ElementType X, SearchTree T);
SearchTree Delete(ElementType X, SearchTree T);
Position findMin(SearchTree T);
Position findMax(SearchTree T);
struct TreeNode
{
ElementType Element;
SearchTree left;
SearchTree Right;
};
//建立空树
SearchTree MakeEmpty(SearchTree T)
{
if(T != NULL)
{
MakeEmpty(T->Left);
MakeEmpty(T->Right);
free(T);
}
return NULL;
}
Position Find(ElementType X, SearchTree T)
{
if(T == NULL)
return NULL;
if(x < T->Element)
return Find(X, T->Left);
else if(X > T->Element)
return Find(X, T->Right);
else
return T;
}
Position FindMin(SearchTree T)
{
if(T == NULL)
return NULL;
else if(T->Left == NULL)
return T;
else return FindMin(T->Left);
}
Position FindMax(SearchTree T)
{
if(T != NULL)
{
while(T->Right != NULL)
T = T->Right;
}
return T;
}
SearchTree Insert(ElementType X, SearchTree T)
{
if(T == NULL)
{
//Create and return an one-node tree
T = (SearchTree)malloc(sizeof(struct TreeNode));
if(T == NULL)
Error("out of space!\n");
else
{
T->Element = X;
T->Left = T->Right = NULL;
}
}
else if(X < T->Element)
T->Left = Insert(X, T->Left);
else if(X > T->Element)
T->Right = Insert(X, T->Right);
return T;
}
关于二叉查找树的删除,存在三种情况:
1、叶子节点,直接删除即可;
2、如果只有一个儿子节点,将其父节点指向其儿子节点,然后free掉该节点,即可;
3、最复杂的情况,有两个节点,一般的删除策略是用其右子树的最小节点的数据,代替该节点的数据,并递归删除那个节点(即右子数最小节点)
代码如下:
SearchTree Delete(ElementType X, SearchTree T)
{
Position TmpCell;
if(T == NULL)
Error("Element not found");
else if(x < T->Element)
T->left = Delete(X, T->Left);
else if(X > T->Element)
T->Right = Delete(X, T->Right);
else if(T->Left&&T->Right)
{
TmpCell = FindMin(T->Right);
T->Element = TmpCell->Element;
T->Right = Delete(T->Element, T->Right);
}
else
{
TmpCell = T;
if(T->Left == NULL)
T = T->Right;
else if(T->Right == NULL)
T = T->Left;
free(TmpCell);
}
return T;
}
二叉树的遍历,按照当前节点的遍历顺序,分别由前序,中序和后序,此处的前中后,是根据当前节点的遍历次序来说的。比如前序,就是先处理当前的节点P,再处理其左右节点。
void PreOrderTraverse(Tree T)
{
if(T != NULL)
{
PrintElement(T->Element);
PreOrderTraverse(T->Left);
PreOrderTraverse(T->Right);
}
}
中序和后序的递归遍历和上面相似,就是在中间或最后打印当前节点。下面分析树的非递归遍历,二叉树前序非递归遍历:
void PreOrderTraverseNew(Tree T)
{
InitStack(S);
Position p = T;
while(p || !StackEmpty(S))
{
if(p)
{
PrintElement(p->Element);
Push(S, p);
p = p->lchild;
}
else
{
Pop(S, p);
p = p->rchild;
}
}
}
中序的非递归遍历为:
void InOrderTraverseNew(Tree T)
{
InitStack(S);
Position p = T;
while(p || !StackEmpty(S))
{
if(p)
{
Push(S, p);
p = p->lchild;
}
else
{
Pop(S, p);
PrintElement(p->Element);
p = p->rchild;
}
}
}
可见,前序和中序的非递归遍历很相近,采用栈来实现,当左子树不为空时,就不断往栈中压入节点,直到左儿子为空,则弹出栈顶元素,并且指向其右儿子,然后重复前面的过程。直到栈中节点全部出栈,且p指向空。唯一的区别在于前序在当前节点入栈前就处理(本例中为打印)该节点,然后再入栈;而中序则是在该元素出栈时打印处理。可见,在非递归的实现中,遍历指针p总会指向null,因此遍历的判断条件仅有一个,具体来讲,就是是否print一个节点,前序的判断为入栈前就要打印,而中旬的判断为其左子树为空或已打印处理,从堆栈的角度来看,左子树为空和左子树已经打印,对于当前节点来说,栈的状态都是一样的,即要打印的节点都是栈顶元素,而且不用考虑右子树的状态,因此出栈打印即可。
但对于后序遍历来说,上面的方法是不行的。因为,在后序遍历中,一个节点是否打印处理,有两种情况,一是该节点左右子树都为空,另一种情况则是该节点的左右子树都已经打印。在判断一个节点是否出栈处理时,首先要确定该节点的右子树是否已经处理,若无,则还要进入右子数处理,因为需要设置一个tag,随着每个节点进入栈中,来记录该节点的右子树是否已经处理。
因此,后序的非递归遍历为:
void PostOrderTraverse(Tree T)
{
InitStack(S);
Position p = T;
while(p || !StackEmpty)
{
while(p)
{
Push(S, p, 0);
p = p->lchild;
}
while(!StackEmpty(S) && GetTopTag(S) == 1)
{
Pop(S, p);
PrintElement(p->Element);
}
if(!StackEmpty(S))
{
setTop(S, 1);
p = GetTop(S);
p = p->rchild;
}
else
break;
}
}
树的深度优先遍历即为先前提到的先序、中序和后序,而树的广度优先遍历则用队列来实现。
void BreadthSearch(Tree T)
{
Queue Q;
InitQueue(Q);
Position p = T;
EnQueue(Q, p);
while(!QueueEmpty(Q))
{
DeQueue(Q, p);
PrintElement(p->Element);
if(p->lchild)
{
EnQueue(Q, p->lchild);
}
else if(p->rchild)
{
EnQueue(Q, p->rchild);
}
}
}
树的高度
采用递归的方式即可求取
int Height(Tree T)
{
if(T == NULL)
return 0;
else
{
return Max(Height(T->Left), Height(T->Right)) + 1;
}
}
给定n个权值作为n个叶子节点,构造一棵二叉树,若带权路径长度达到最小,称这样的二叉树为最优二叉树,也叫做哈夫曼树。
路径:在一棵树中,从一个节点往下可以到达的孩子或子孙节点之间的通路,成为路径;
路径长度:通路中分支的数目称为路径长度,若规定根节点的层数为1,则从根节点到第L层节点的路径长度为L-1;
节点的权:若将树中节点赋给一个有着某种含义的数值,则称该数值为该节点的权;
节点的带权路径长度:从根节点到该节点之间的路径长度与该节点的权的乘积。
树德带权路径长度:所有叶子节点的带权路径长度之和,记为WPL。
哈夫曼树的构造:
假设有n个权值,则构造出的哈夫曼树有n个叶子节点,n个权值分别设为分别设为w1、w2、...、wn,则哈夫曼树的构造规则为:
(1) 将w1、w2、...、wn看成是有n棵树的森林(每棵树仅有一个节点);
(2) 在森林中选出两个根节点的权值最小的树合并,作为一棵新树的左右子树,且新树的根节点权值为其左右子树的根节点权值之和;
(3) 从森林中删除选取的两棵树,并将新树加入森林;
(4) 重复(2) (3)步,直到森林中只剩一棵树为止,该树即为所求得的哈夫曼树。