目录
前言
对于树来说,网上教程很多。但是要么就是不完整(仅写树的操作,而没有其它知识),要么就是只怼代码,没有讲解,尤其是很多刚入门的,对递归遍历和迭代遍历根本理
解不透彻,所以准备出篇详细的二叉树的教程。
树的概念
树--是n(n>=0)个结点的有限集合,满足以下特点:
1).当n=0时,称为空树
2).当n>0时,有且仅有一个特定的称为根的结点其余的结点可分为m(m>=0)个互
不相交的子集T1,T2,T3····Tm,其中每个子集Ti又是一棵树,并称其为子树.
结点:由一个数据元素及若干指向其他结点的分支所组成
度:
结点的度:所拥有的的子树的数目
树的度 :树中所有结点的度的最大值
叶子(终端结点):度为0的结点
非终端结点:度不为0的结点
孩子(子结点):结点的子树的根称为该结点的孩子
双亲(父节点):一个结点称为该结点所有子树根的双亲
祖先:结点祖先指根到此结点的一条路径上的所有结点
子孙:从某结点到叶结点的分支上的所有结点称为该结点的子孙
兄弟:同一双亲的孩子之间互称兄弟
结点的层次---从根开始算,根的层次为1,其余结点的层次为其双亲的层次加1
堂兄弟---其双亲在同一层的结点
树的深度(高度)---一棵树所有结点层次数的最大值
无序树---若树干各结点的子树是无次序的,可以互换,则称为无序树
有序树---若树干各结点的子树从左到右是有次序的,不能互换,称为有序树
森林---是m(m>=0)棵树的集合
二叉树的基本概念
1.定义:
二叉树是由n(n>=0)个结点的有限集合,它或许为空(n=0),或是有一个根即两棵互不相交的左子树和右子树组成,其中左子树和右子树也均为二叉树。
这是一个递归的定义,二叉树可以是空集合,根可以有空的左子树或空的右子树。
2.特点:
1(、二叉树可以是空的,称空二叉树
2(、每个结点最多只能有两个孩子
3(、子树有左、右之分且次序不能颠倒
3.二叉树与树的比较:
二叉树结点的子树要区分左子树和右子树,即使只有一棵树也要进行区分,说
明它是左子树还是右子树。
这是二叉树与树的最主要的差别
二叉树的特性
1).在二叉树的第i(i>=1)层上至多有2^(i-1) 个结点
2).深度为k(k>=1)的二叉树至多有(2^k) -1 个结点
3).对任何一棵二叉树,如果其终端结点树为 n0,度为2的结点数为n2,则n0 = n2+1 。 即:叶结点数n0=度为2的结点数n2+1
4).满二叉树,深度为k(k>=1)且有2^k -1 个结点的二叉树。
满二叉树编号:自上而下、自左而右进行编号
5).完全二叉树,深度为k的二叉树中,k-1 层结点数是满的(2^(k-2)),k层结点是左连续的(即结点编号是连续的)
满二叉树是完全二叉树,完全二叉树不是满二叉树(满二叉树最右边开始剪枝得到完全二叉树)
6).具有n个结点的完全二叉树的深度为(LOG(2)n)+1 个。
如 结点数是8,则深度= (LOG(2)8)+1 = 4
结点数是6,则深度 = (LOG(2)6)+1 = 2.xx+1 = 3(向下取整)
7).对有n个结点的完全二叉树的结点按层编号(自上而下,从左到右编号)
对其中任意一结点i(1<=i<=n),有:
如果i=1,则结点i无双亲,是二叉树的根,如果i>1,则i的双亲是结点[i/2](向下取整);
如果2*i<=n,则其左孩子结点编号是2*i;
如果2*i+1<=n,则其右孩子结点编号是2*i+1;
二叉树的存储结构
二叉树的存储结构分两种,一种顺序表存储,一种是链表存储,下面介绍这两种存储结构
一、顺序存储
顺序表就是利用一位数组将树的结点按照自上而下自左而右的顺序依次放入数组中。
比如我们现在有一个完全二叉树,那么它的顺序表存储结构就如图二这样
但是顺序存储有个缺陷,我们看下这张非完全二叉树的图,它的顺序存储如图三所示,由表可知,4和5下标的位置是空的,原因是因为结点2的右子结点和结点3的左子节
点是空的。对于顺序存储来说,我们不能因为某结点不存在就不给其分配存储空间。那么为什么一定要保留这个空间呢?不保留究竟行不行?
答案是不行,其实二叉树的结点位置有个关系-->
令某二叉树有n个结点,i是其中第i个结点,当2i+1<=n是,i的左子节点是:2i;i的右子节点是2i+1。可以参考下上面的完全二叉树的图,结点2的左子结点是2*2=4,右子
结点是2*2+1=5(5是位置,不是值)。
所以对于普通二叉树不保留存储空间的话,这个关系就不成立。就没法根据某结点找到其子结点或者父结点的位置了。
除了这个关系外,这种顺序存储还会造成空间的浪费,所以一般我们更倾向于使用链式存储。
二、链式存储
连式存储,常用的是二叉链表和三叉链表
二叉链表的构造和表结果如下图所示
typedef struct btnode{
DataType data; // data域
struct btnode *lchild,*rchild; // 左右指针域
}*BinTree;
lchild | data | rchild |
以此二叉树为例,其二叉链表的表现就是如右图所示,lchild指向其左子结点,rchild指向其右子结点。若无子节点,则以^代替。
三叉链表结点结构如下图所示
typedef struct ttnode{
DataType data; // data域
struct ttnode *lchild,*rchild,*parent; // 左右指针域+双亲指针
}*TBinTree;
lchild | data | parent | rchild |
三叉链表如下图所示
二叉树的递归及迭代遍历
为了更细致的介绍遍历的细节,前半部分我会有伪代码的形式进行介绍,后面会放上真实代码供参考。
结构体选用二叉链表结构体 BinTree 、遍历对象选用这个满二叉树(图9909)。
一、先序遍历-递归
老生常谈的规则:先序遍历(根-左-右),从根节点开始执行,先输出当前结点的值,再将当前结点的左子树进行先序遍历,最后将当前结点的右子树进行先序遍历。
void preOrder(BinTree *Bt){
if(Bt!=NULL){
printf(Bt->data); // 输出当前结点的值
preOrder(Bt->lchild); // 访问左子树
preOrder(Bt->rchild); // 访问右子树
};
};
具体执行过程:
二、中序遍历-递归(做-中-右)
先遍历左子树、再遍历根,最后遍历右子树
void InOrder(BinTree *Bt){
if(Bt!=NULL){
InOrder(Bt->lchild); // 先访问左子树
printf(Bt->data); // 输出当前结点的值
InOrder(Bt->rchild); // 访问右子树
};
};
执行步骤:
1.Bt指向根结点1(不为空)
2.执行InOrder(Bt->lchild)->将以结点2为根结点的树进行中序遍历(注意:此时结点1的操作还没有执行完)
感觉这个地方有点操作系统中断概念的意思,执行A时发B更重要,就改为执行B,B执行完再返回继续执行A
3.此时Bt指向结点2且2不为空
4.执行InOrder(Bt->lchild)->将以结点4为根结点的树进行中序遍历(注意:此时结点1和2的操作还没有执行完)
5.此时Bt指向4且4不为空
继续执行InOrder(Bt->lchild)->(注意:此时结点1&2&4的操作还没有执行完)
6.因为4的 lchilf 是NULL,所以不能继续向下执行InOrder(Bt->lchild)
7.返回结点4上次停止的位置继续执行printf(Bt->data)--> 输出4
8.继续向下执行InOrder(Bt->rchild)-->因为结点4的 rchild 也是NULL,所以结束当前操作,回到结点2的停止位置
9.执行结点2的 printf(Bt->data)-->输出2
10.继续执行结点2的 InOrder(Bt->rchild) -->输出5
11.结点2执行完后,回到结点1的停止位置继续执行
12.执行结点1的 printf(Bt->data)-->输出1
13.继续执行结点1的 InOrder(Bt->rchild),将以结点3为根节点的树进行中序遍历
14.重复以上操作
三、后序遍历-递归(左-右-中)
先遍历左子树,再遍历右子树,最后遍历根
void PostOrder(BinTree *Bt){
if(Bt!=NULL){
PostOrder(Bt->lchild); // 先访问左子树
PostOrder(Bt->rchild); // 访问右子树
printf(Bt->data); // 输出当前结点的值
};
};
将整棵树分为三个部分(这张图应该放在开头比较合适)
后序遍历--先遍历左子树得到{4,5,2},再遍历右子树得到{7,6,3},最后遍历根得到{1}。
所以结果就是{4,5,2,7,6,1,1},整个递归过程和上面的差不多,只是顺序变了而已。具体步骤就细说了。
总结:
对于树的遍历来说,先序、中序和后序中的【先、中、后】是针对根节点来说的,先--先访问根节点;中--中间访问根节点;后--最后访问根节点。而左右结点呢永远是左
在前右在后。
迭代:
对于迭代(待会就不用伪代码了),个人觉得它比递归更容易让人理解,递归这个东西理解的人觉得它很简单,不理解的人就觉得很蒙。对于不理解上面实现的同学,还是先
看迭代的实现过程吧。
先序、中序和后序的迭代可以用栈实现,而层次遍历可以用队列实现。
四、先序遍历-迭代
🤔迭代的过程:对于二叉树而言,当根节点的左子结点不是叶子结点时,那么其必然是它所在树的根节点。所以对于先序遍历就是不断找到左结点(找左子结点就是找根节
点)并将其入栈,等到左子节点全部找完,再找右子节点。这样既可实现迭代版本的先序遍历。
def PreOrder(root): // root:待遍历的树
stack,result = [],[] // result:返回结果
curl = root
while stack or curl:
while curl:
reault.append(curl.data) // 访问根节点数据域 值
stack.append(curl) // 将curl 入栈
curl = curl.lchild // 遍历curl的左子树,让左子树的结点继续入栈
top = stack.pop() // 左子树遍历完后,栈顶元素出栈
curl = top.rchild // 根据栈顶元素遍历右子树
已上图为例模拟一遍入栈过程(声明栈的位置跳过,直接看while循环中的内容):
1、执行 reault.append(curl.data),所以此时result=[1]
2、执行stack.append(curl),所以此时stack的结果是[1]
3、执行curl = curl.lchild,所以根结点为2的这棵树入栈,因为curl不为空,所以继续执行
3.1、执行 reault.append(curl.data),所以此时result=[1,2]
3.2、执行stack.append(curl),所以此时stack的结果是[1,2]
3.3、执行curl = curl.lchild,所以根结点为4的这棵树入栈,因为curl不为空,所以继续执行
3.3.1、执行 reault.append(curl.data),所以此时result=[1,2,4]
3.3.2、执行stack.append(curl),所以此时stack的结果是[1,2,4]
3.3.3、执行curl = curl.lchild,因为curl为空,所以跳出循环体
3.4、执行top = stack.pop(),取出stack栈顶元素4 -->此时stack的结果是[1,2]
3.5、执行 curl = top.rchild ,因为4的右子树是NULL,所以不会执行while curl
3.6、继续执行top = stack.pop(),取出stack栈顶元素2 -->此时stack的结果是[1]
3.7、执行 curl = top.rchild ,因为2的右子树是5不为NULL,所以执行 while curl
3.7.1、执行reault.append(curl.data),所以此时result=[1,2,4,5]
3.7.2、执行stack.append(curl),所以此时stack的结果是[1,5]
3.7.3、执行curl = curl.lchild,因为curl为空,所以跳出循环体
3.8、执行top = stack.pop(),取出stack栈顶元素5 -->此时stack的结果是[1]
3.9、执行 curl = top.rchild ,因为5的右子树是NULL,所以不会执行while curl
3.10、继续执行top = stack.pop(),取出stack栈顶元素1 -->此时stack的结果是[ ]
3.11、执行 curl = top.rchild ,因为1的右子树是3不为NULL,所以执行 while curl
~~~~~~~~~右子树的步骤和 左子树的顺序一致~~~~~~~~~参考上述过程就好
五、后序遍历-迭代
后序遍历--先找右子节点,找完后再找左子节点,最后倒叙排列下即可(Ps:原因:因为按这个顺序,result中的顺序是 根-右-左,反过来正好是后序遍历)
def PostOrder(root):
result,stack = [],[]
curl = root
while stack or curl:
while curl:
result.append(curl.data)
stack.append(curl)
curl = curl.rchild
top = stack.pop()
curl =top.lchild
print result[::-1]
六、中序遍历-迭代
中序遍历--再于先将所有的左结点全部入栈(在某些情况下左结点就是根节点),然后再遍历右子树
def InOrder(root):
result,stack = [],[]
curl = root
while stack or curl:
while curl:
stack.append(curl)
curl = curl.lchild
top = stack.pop()
result.append(top.data)
curl = top.rchild
print result
七、层序遍历-迭代
对于层序遍历,我准备以自上而下,自左而右的顺序进行遍历
执行顺序:
1、树入栈
2、当栈或树不为空时,
2.1、栈顶元素出栈-->访问根节点数据域
2.2、若当前结点的左子树不为空,则左子树入栈
2.3、若当前结点的右子树不为空,则右子树入栈
3.重复2的操作,即可得到层序遍历的结果
def btravel(root):
# 层序
queue = [] # 创建队列
result1 = [] # 遍历结果
queue.append(root) # 入队列
while queue:
top = queue.pop() # 取对首元素
result1.append(top.data) # 访问结点数据域
if top.lchild:
queue.insert(0,top.lchild) # 队尾插入左子树
if top.rchild:
queue.insert(0,top.rchild) # 队尾插入右子树
print result1
完整版
class Node:
def __init__(self, value=None, left=None, right=None):
self.value = value
self.left = left # 左子树
self.right = right # 右子树
def preTraverse(root):
'''
递归-前序遍历
'''
if root == None:
return
print(root.value)
preTraverse(root.left)
preTraverse(root.right)
def midTraverse(root):
'''
递归-中序遍历
'''
if root == None:
return
midTraverse(root.left)
print(root.value)
midTraverse(root.right)
def afterTraverse(root):
'''
递归-后序遍历
'''
if root == None:
return
afterTraverse(root.left)
afterTraverse(root.right)
print(root.value)
def preTraverse1(root):
# 迭代-前序
# 父-左-右
stack,list1 = [],[] # stack:栈 list1:排序结果
curl = root
while stack or curl: # 当栈不为空或者 二叉树不为空时
while curl:
list1.append(curl.value)
stack.append(curl)
curl = curl.left
top =stack.pop()
curl = top.right
print list1
def midTraverse1(root):
# 迭代-中序
# 左-父-右
stack, list1 = [], [] # stack:栈 list1:排序结果
curl = root
while stack or curl:
while curl:
stack.append(curl)
curl = curl.left
top = stack.pop()
list1.append(top.value)
curl = top.right
print list1
def afterTraverse1(root):
# 迭代-后序
stack, list1 = [], [] # stack:栈 list1:排序结果
curl = root
while stack or curl:
while curl:
list1.append(curl.value)
stack.append(curl)
curl = curl.right
top = stack.pop()
curl = top.left
print list1[::-1]
def btravel(root):
# 层序
queue = []
result1 = []
queue.append(root)
while queue:
top = queue.pop()
result1.append(top.value)
if top.left:
queue.insert(0,top.left)
if top.right:
queue.insert(0,top.right)
print result1