树形结构是非常重要的非线性结构,是以分支关系定义的一对多的层次结构。
树是n(n≥0)个结点的有限集。当n=0时,称为空树。在任意一棵非空树中应满足:
a.有且仅有一个特定的称为根的结点。
b.当n>1时,其余结点可分为m(m>0)个互不相交的有限集T,T2,…,Tm,其中每个集
合本身又是一棵树,并且称为根的子树。
树结构具有以下性质:
结点:一个数据元素及其若干指向其子树的分支。
结点的度:结点所拥有的子树的棵数称为结点的度。
树的度:树中结点的度的最大值称为树的度。
叶子结点:树中度为0(向下没有分支)的结点称为叶子结点(或终端结点)。
非叶子结点:树中度不为0(问下有分支)的结点称为非叶子结点(或非终端结点或分支结点)。
除了根结点,分支结点也称为内部结点。
子结点:一个结点的子树的根称为该结点的孩子结点或子结点。
双亲结点或父节点:与子结点对应,子树的根。
兄弟结点:同一双亲结点的所有子结点互称为兄弟结点。
层次:规定树中根结点的层次为1,其众结点的层次等于其双亲结点的层次加1。
堂兄弟结点:在同一层上,但是双亲不同的所有结点互称为堂兄弟结点。
层次路径:从根结点开始,到达某结点p所经过的所有结点称为结点p的层次路径(有且只有一条)。
祖先结点:结点p的层次路径上的所有结点(p除外)称为p的祖先。
子孙结点:以某结点为根的子树中的任意结点称为该结点的子孙结点。
树的深度: 树中结点的最大层次值,又称为树的高度。
分支数=孩子数=度
性质1:树中的结点个数等于所有结点的度之和加1。
性质2:对m度树,定义叶子结点个数为 n 0 n_0 n0,度为1的结点个数为 n 1 n_1 n1,… ,度为m的结点个数为 n m n_m nm,
于是有 n 0 = n 2 + 2 ∗ n 3 + 3 ∗ n 4 + … + ( m − 1 ) ∗ n m + 1 n_0=n_2+2*n_3+3*n_4+…+(m-1)*n_m+1 n0=n2+2∗n3+3∗n4+…+(m−1)∗nm+1
性质3:在非空m度树中,第i层上至多有 m i − 1 m^{i -1} mi−1个结点(i≥1)。
性质4:高度为h的m(m>1)度树,最多有 m h − 1 m − 1 \frac{m^{h}-1}{m-1} m−1mh−1个结点。
性质5:具有n个结点的m度树的最小高度是 log m [ n ∗ ( m − 1 ) + 1 ] \log_m[n * (m -1) +1] logm[n∗(m−1)+1]
双亲表示法
孩子链表示法
孩子兄弟表示法
二叉树的定义只需要把树的定义中m个分支改成最多两个分支就可以了。
性质1:树中的结点个数等于所有结点的度之和加1。
性质2:对任意一棵二叉树,定义叶子结点个数为 n 0 n_0 n0,度为2的结点个数为 n 2 n_2 n2, n 0 = n 2 + 1 n_0=n_2+1 n0=n2+1
性质3:在非空二叉树中,第i层上至多有 2 i − 1 2^{i -1} 2i−1个结点(i≥1)。
性质4:高度为k的二叉树,最多有 2 k − 1 {2^{k}-1} 2k−1个结点(k≥1)。
性质5:具有n个结点的二叉树的最小高度是 log 2 ( n + 1 ) \log_2(n +1) log2(n+1)
满二叉树、完全二叉树、二叉排序树、平衡二叉树、折半树
、堆。
性质1:在非空一叉树,第i层上至多有 2 k − 1 2^{k-1} 2k−1个结点(i≥1)。
性质2:深度为k的二叉树至多有 2 k − 1 2^{k}-1 2k−1个结点(k≥1).
性质3:对任何一棵二叉树,若其叶子结点数为n,度为2的结点数为 n 2 n_2 n2,则 n 0 = n 2 十 1 n_0=n_2十1 n0=n2十1
性质4:树中的结点个数等于所有结点的度之和加1
性质5:深度为k的满二叉树中编号从1到n的前n个结点构成了一棵深度为k的完全二叉树,其中 2 k − 1 ≤ n ≤ 2 k − 1 2^{k-1}\le n \le2^k-1 2k−1≤n≤2k−1
性质6:n个结点的完全二义树深度为 ( l o g 2 n ) + 1 (log_2n)+1 (log2n)+1或 l o g 2 ( n + 1 ) log_2(n+1) log2(n+1)。
性质7:若完全二叉树的深度为k,则所有的叶子结点都出现在第k层或第k-1层。
性质8:对于任一结点,若其右子树的最大层次为1,则其左子树的最大层次为1或2。
性质9:若对一棵有n个结点的完全二叉树(深度为 ( l o g 2 n ) + 1 (log_2n)+1 (log2n)+1)的结点按层(从第1层到第 ( l o g 2 n ) + 1 (log_2n)+1 (log2n)+1层)序自左至右进行编号,则对于编号为1( 1 ≤ i ≤ n 1\le i \le n 1≤i≤n)的结点,有以下情形:
①若i=1,则结点i是二叉树的根,无双亲结点;否则,若i>1,则其双亲结点编号是[i/2」
②若2i>n,则结点i为叶子结点,无左孩子:否则,其左孩子结点编号是2i.
③若2i+1>n,则结点i无右孩子;否则,其右孩子结点编号是2i+1。
性质10:对于一棵完全二叉树,度为1的结点个数是1或0。
二叉树的顺序存储就是按照每个结点的编号作为数组下标存储到数组中;如果不满足完全二叉树,我们以通过补空标志,构建一棵完全二叉树。
在最坏的情况下,一个深度为k且只有k个结点的单支树需要长度为 2 k − 1 2^{k-1} 2k−1的一维数组。此时是一棵单右支树。一般只有完全二叉树才采用顺序结构进行存储,一般的二叉树不适用于顺序结构。
#define MAX_SIZE 100
typedef int sqbitree[MAX_SIZE];
因为二叉树有左、右两个子树,所以二叉树的链式结构的每个结点有3个域:一个数据域,两个分别指向左、右子结点的指针域。
二叉树的遍历分为:先序遍历、中序遍历、后序遍历和层次遍历。
先序遍历、中序遍历、后序遍历需要使用到栈,而层次遍历需要用到队列。无论是哪种遍历,在遍历过程中,叶子结点的相对顺序是不变的。
访问是指对结点做某种处理,如输出信息、修改结点的值,我们把访问结点的操作称为遍历,遍历二叉树是指按指定的规律对二叉树中的每个结点访问一次且仅访问一次。
若以L、D、R分别表示遍历左子树、遍历根节点和遍历右子树,则会有以下6种遍历方案:LDR、LRD、DLR、DRL、RLD、RDL。如果事先约定先左后右,则只有以下三种情况:DLR——先序(根)遍历、LDR——中序(根)遍历、LRD——后序(根)遍历。
先序遍历就是从二叉树的根结点出发,当第一次到达结点时就输出结点数据,按照先向左再向右的方向访问。若二叉树为空,则遍历结束。否则有以下情形:
a.访问根结点。
b.先序遍历左子树(递归调用本算法)。
c.先序遍历子树(涕归调用本算法)。
若采用递归进行遍历,则会调用系统栈进行遍历,具体代码如下:
//二叉树定义部分:
#include
#include
typedef int ElementType;
typedef struct BTNode{
ElementType data;
struct BTNode * Lchild, *Rchild;
}BTNode;
void PreorderTraverse(BTNode * T){
//先序遍历,递归写法
//如果为空,则访问根结点
if(T == NULL) return;
printf("%d\n", T->data);
PreorderTraverse(T->Lchild);
PreorderTraverse(T->Rchild);
}
若采用用户栈,可以借助以下代码:
//定义需要用的类型及函数
#include
#include
#define MAX_SIZE 100
typedef int ElementType;
typedef struct BTNode{
ElementType data;
struct BTNode * Lchild, *Rchild;
}BTNode;
typedef struct Stack{
//定义栈
//定义一个存放二叉树指针的数组
BTNode * data[MAX_SIZE];
int top;
}SqStack;
void Init_stack(SqStack * S){
S->top = 0;
}
int isEmpty(SqStack * S){
if(S->top == 0) return 1;
else return 0;
}
bool push(SqStack * S, BTNode * node){
if (S->top == MAX_SIZE) return false;
S->data[S->top] = node;
S->top++;
return true;
}
BTNode * pop(SqStack * S){
//出栈
if (S->top == 0) return NULL;
S->top--;
return S->data[S->top];
}
void PreorderTraverse_user(BTNode * root){
//二叉树遍历,调用用户栈
//用来暂存结点的栈
//BTNode * stack[100];
SqStack * stack;
Init_stack(stack);
// 新建一个工作结点,并指向根结点
BTNode * node = root;
// 当遍历到最后一个结点,若左右子树都为空,且栈也为空,则跳出循环。
//while (node != NULL || !stack.isEmpty()){
while (node != NULL || !isEmpty(stack)){
while(node != NULL){
printf("%d\n", node->data);
//暂存该结点
//stack.push(node);
push(stack, node);
node = node->Lchild;
}
//if (!stack.isEmpty()){
if (!isEmpty(stack)){
//左子树为空,再取出该元素,并获取其右子树
//node = stack.pop();
node = pop(stack);
node = node->Rchild;
}
}
}
中序遍历就是从二叉树的根结点出发,当第二次到达结点时输出结点数据,按照先向左再向右的方向访问。算法的递归定义是:若二叉树为空,则遍历结束:否则有以下情形:
a.中序遍历左子树(递归调用本算法)。
b.访回根结点。
c.中序遍历右子树(递归调用本算法)。
调用递归方法实现代码如下:
void InorderTraverse(BTNode * T){
// 中序遍历
if (T == NULL) return;
InorderTraverse(T->Lchild);
printf("%d", T->data);
InorderTraverse(T->Rchild);
}
调用用户栈,自定义方法如下:
void midorderTraversal(BTNode * root){
SqStack * stack;
Init_stack(stack);
BTNode * node = root;
while(node != NULL || !isEmpty(stack)){
while(node != NULL){
push(stack, node);
node = node->Lchild;
}
if (!isEmpty(stack)){
node = pop(stack);
printf("%d", node->data);
node = node ->Rchild;
}
}
}
后序遍历就是从二叉树的根结点出发,当第三次到达结点时就输出结点数据,按照先向左再向右的方向访问。算法的递归定义是:若二叉树为空,则遍历结束;否则有以下情形:
a.后序遍历左子树(递归调用本算法)。
b.后序遍右子树(递归调用木算法)。
c.访回根结点。
若使用系统栈,采用递归方法如下:
void postTraverse(BTNode * T){
//后续遍历
if (T == NULL) return;
postTraverse(T->Lchild);
postTraverse(T->Rchild);
printf("%d\n", T->data);
}
若采用用户栈,自定义方法如下:
void postTraversal(BTNode * root){
SqStack * stack;
BTNode * node = root;
BTNode * last = root;
while(node != NULL || !isEmpty(stack)){
while (node != NULL){
push(stack, node);
node =node->Lchild;
}
// 查看当前栈顶元素
//如果其右子树为空,或者右子树已经访问,则输出该结点的值
if (node->Rchild == NULL || node->Rchild == last){
printf("%d\n", node->data);
pop(stack);
last = node;
node = NULL;
}else{
node = node->Rchild;
}
}
}
层次遍历二叉树,是从根结点开始遍历,按层次次序“自上而下,从左至右”访问树中的各结点。为保证是按层次遍历,必须设置一个队列,初始化时为空。设T是指向根结点的指针变量,层次遍历非递归法是:若二叉树为空,则返回;否则,令p=T,p入队。
a.队首元素出队到p。
b.访问p所指向的结点。
c.将p所指向的结点的左、右子结点依次入队,直到队空为止。
实现代码如下:
void leverTraverse(BTNode * T){
// 定义一个队列来存储结点
BTNode * Queue[MAX_SIZE];
BTNode * p = T;
int front = rear = 0;
// 当结点部位空时,开始进出队列
if(p != NULL){
Queue[rear++] = p;
while(front < rear){
//当队列不为空,进行输出和访问。
p = Queue[front++];
printf("%d", p->data);
//有左结点,就将左节点放到队列中
if (p->Lchild != NULL)
Queue[front++] = p->Lchild;
if (p->Rchild != NULL)
Queue[front++] = p->Rchild;
}
}
}
以下三种组合可以唯一确定一棵二叉树:
在二叉树的结点上加上线索的二叉树称为线索二叉树,对二叉树以某种遍历方式(如先序、中序、后序或层次等)进行遍历,使其变为线索二叉树的过程称为对二叉树进行线索化。
左孩子或直接前驱 | 是否无左孩子 | 数据 | 是否无右孩子 | 右孩子或直接后继 |
---|---|---|---|---|
Lchild | Ltag | data | Rtag | Rchild |
以Ltag为例,标记方法如下:
先写出遍历序列,然后根据有无子树添加指向左、右子树的指针或指向直接前驱、直接后继的线索。
以下为线索二叉树的构建例子:
对于一般的树,可以方便地转换成一棵唯一的二叉树与之对应。其详细步骤如下(孩子兄弟表示法):
二叉树转为树,步骤如下:
森林转换成二叉树的转换步骤如下:
二叉树转为森林的步骤如下:
①去连线。将二叉树的根结点与其右子结点,以及沿右子结点链方向的所有右子结点的连线全部去掉,得到若干棵孤立的二叉树,每棵树就是原来森林中的树依次对应的二叉树。
②二叉树的还原。将各棵孤立的二叉树按二叉树还原为树的方法还原成一般的树。
二叉树转为森林的例子如下:
树的遍历有两种:先序遍历和后序遍历。
构建要点:需要让编码长的路径长度长一些,编码短的路径长度短一些即可。
给定n个权值分别为 w 1 w_1 w1, w 2 w_2 w2,…, w n w_n wn的结点,构造哈夫曼树的算法描述如下:
例子:
如将w={8,3,4,6,5,5}构造成哈夫曼树,步骤图如下:
W P L = 6 ∗ 2 + 3 ∗ 3 + 4 ∗ 3 + 8 ∗ 2 + 5 ∗ 3 + 5 ∗ 3 = 79 WPL=6*2+3*3+4*3+8*2+5*3+5*3=79 WPL=6∗2+3∗3+4∗3+8∗2+5∗3+5∗3=79
若没有一个编码是另一个编码的前缀,则称这样的编码为前缀编码。
由哈夫曼树得到哈夫曼编码是很自然的过程。首先,将每个出现的字符当作一个独立的结点,其权值为它出现的频度(或次数),构造出对应的哈夫曼树。显然,所有字符结点都出现在叶结点中。我们可将字符的编码解释为从根至该字符的路径上边标记的序列,其中边标记为0表示“转向左孩子”,标记为1表示“转向右孩子”。
如何判断是不是哈夫曼编码?
并查集是一种非常精巧且实用的数据结构,它主要用于处理一些不相交集合的合并及查询向题。常见的用途有求连通子图、求最小生成树的Kruskal算法和求最近公共祖先。
在使用并查集时,首先会存任一组不相交的动态集合S={S1,s2,…,Sn},一般会使用一个整数表示集合中的一个元素。
并查集是一种简单的集合表示,它支持以下3种操作:
通常用树(森林)的双亲表示作为并查集的存储结构,每个子集合以一棵树表示。所有表示子集合的树,构成表示全集合的森林,存放在双亲表示数组内。通常用数组元素的下标代表元素名,用根结点的下标代表子集合名,根结点的双亲结点为负数。