赞
踩
本篇章主要介绍二叉树,包括二叉树的定义、基本性质、存储结构及遍历,并用Python实现其创建及其遍历等操作。
与树相似,二叉树
(
B
i
n
a
r
y
(Binary
(Binary
T
r
e
e
)
Tree)
Tree)也是
n
(
n
≥
0
)
n(n \geq 0)
n(n≥0)个结点的有限集合,其特点是每个结点最多只有两棵子树,即二叉树中不存在度大于2的结点,而且其子树有左右之分(左子树与右子树),次序不能颠倒。所以根据定义,二叉树有以下5种基本形态:
二叉树与度为2的有序树的区别是:
(1) 度为2的树至少有3个结点,而二叉树可以为空;
(2) 度为2的有序树的孩子的左右次序是相对另一个孩子而言的,若某个结点只有一个孩子,则这个孩子无需区分是左还是右,而二叉树就不一样了,它需要区分。
一棵高度为 h h h,且含有 2 h − 1 2^h-1 2h−1个结点的二叉树称为满二叉树,即树中的每层都含有最多的结点。满二叉树的叶子结点都集中在二叉树的最下层,除叶子结点外的每个结点的度都为2。
由图可知,对于编号为
i
i
i的结点,若有双亲,则其双亲为
⌊
i
2
⌋
\lfloor \frac {i} {2}\rfloor
⌊2i⌋;若有左孩子,则左孩子为
2
i
2i
2i;若有右孩子,则右孩子为
2
i
+
1
2i+1
2i+1。
一棵高度为 h h h,含有 n n n个结点的二叉树,当且仅当其每个结点都与高度为 h h h的满二叉树中编号为 1 1 1~ n n n的结点一一对应时,称为完全二叉树。
完全二叉树就是对应相同高度的满二叉树缺失最下层最右边的一些连续叶子结点。
特点如下:
(1) 若
i
≤
⌊
n
2
⌋
i \leq \lfloor \frac {n} {2}\rfloor
i≤⌊2n⌋,则结点
i
i
i为分支结点,否则为叶子结点;
(2) 叶子结点只可能在层次最大两层上出现。对于最大层次中的叶子结点,都依次排列在该层最左边的位置上;
(3) 若有度为1的结点,则只可能有一个,且该节点只有左孩子而无右孩子;
(4) 若按层序编号后,一旦出现某结点(假设编号为
i
i
i)为叶子结点或只有左孩子,则编号大于
i
i
i的结点均为叶子结点;
(5) 若
n
n
n为奇数,则每个分支结点只有左孩子和右孩子;若
n
n
n为偶数,则编号最大分支结点(假设编号为
n
2
\frac {n} {2}
2n)只有左孩子,没有右孩子,其余分支结点左、右孩子都有。
左子树上所有结点的关键字
均小于根结点的关键字;右子树上的所有结点的关键字均大于根结点的关键字;左子树和右子树右各是一棵二叉排序树。
关键字
:数据元素中唯一标识该元素的某个数据项的值,使用基于关键字的查找,查找结果应该是唯一的。在介绍查找这部分时还会再说。
树上任一结点的左子树和右子树的深度之差不超过1。
性质1:在二叉树的第 i i i层上至多有 2 i − 1 2^{i-1} 2i−1个结点( i ≥ 1 i \geq 1 i≥1);
证明:当 i = 1 i=1 i=1时, 2 i − 1 = 2 0 = 1 2^{i-1}=2^0=1 2i−1=20=1,即至多只有一个根结点;当 i = 2 i=2 i=2时, 2 i − 1 = 2 1 = 2 2^{i-1}=2^1=2 2i−1=21=2,即至多只有两个结点;当 i = 3 i=3 i=3时, 2 i − 1 = 2 2 = 4 2^{i-1}=2^2=4 2i−1=22=4,即至多只有四个结点;以此类推,由归纳假设可知这是一个公比为2的等比数列,所以第 i i i层上至多有 2 i − 1 2^{i-1} 2i−1个结点。
性质2:深度为 k k k的二叉树至多有 2 k − 1 2^k-1 2k−1个结点( k ≥ 1 k \geq 1 k≥1);
证明:深度为
k
k
k的二叉树的最大结点数为(等比数列求和)
∑
i
=
1
k
(
第
i
层
上
的
最
大
结
点
数
)
=
∑
i
=
1
k
2
i
−
1
=
2
k
−
1
\sum_{i=1}^k(第i层上的最大结点数)=\sum_{i=1}^k2^{i-1}=2^k-1
i=1∑k(第i层上的最大结点数)=i=1∑k2i−1=2k−1
性质3:对任何一棵二叉树 T T T,如果其叶子结数为 n 0 n_0 n0,度为2的结点数为 n 2 n_2 n2,则 n 0 = n 2 + 1 n_0=n_2+1 n0=n2+1;
证明:假设度为0,1和2的结点数分别为 n 0 n_0 n0, n 1 n_1 n1和 n 2 n_2 n2,结点总数 n = n 0 + n 1 + n 2 n=n_0+n_1+n_2 n=n0+n1+n2;再看二叉树中的分支数,除根节点外,其余结点都有一个分支进入,设 B B B为分支总数,则 n = B + 1 n=B+1 n=B+1;由于这些分支是由度为1或2的结点射出的,所以又有 B = n 1 + 2 n 2 B=n_1+2n_2 B=n1+2n2,于是可得 n 0 + n 1 + n 2 = n 1 + 2 n 2 + 1 n_0+n_1+n_2=n_1+2n_2+1 n0+n1+n2=n1+2n2+1,即 n 0 = n 2 + 1 n_0=n_2+1 n0=n2+1。
性质4:具有 n n n个结点的完全二叉树的深度为 ⌈ log 2 ( n + 1 ) ⌉ \lceil \log_2(n+1) \rceil ⌈log2(n+1)⌉或 ⌊ log 2 n ⌋ + 1 \lfloor \log_2n \rfloor+1 ⌊log2n⌋+1;
证明:假设深度为
k
k
k,则根据性质2和完全二叉树的定义有
2
k
−
1
−
1
<
n
≤
2
k
−
1
或
2
k
−
1
≤
n
<
2
k
2^{k-1}-1<n\leq 2^k-1或2^{k-1}\leq n <2^k
2k−1−1<n≤2k−1或2k−1≤n<2k
可得
2
k
−
1
<
n
+
1
≤
2
k
2^{k-1}<n+1\leq 2^k
2k−1<n+1≤2k,即
k
−
1
<
log
2
(
n
+
1
)
≤
k
k-1<\log_2(n+1)\leq k
k−1<log2(n+1)≤k,因为
k
k
k为正整数,所以
k
=
⌈
log
2
(
n
+
1
)
⌉
k=\lceil \log_2(n+1) \rceil
k=⌈log2(n+1)⌉,或者也可得
k
−
1
≤
log
2
n
<
k
k-1\leq \log_2n<k
k−1≤log2n<k,即
k
=
⌊
log
2
n
⌋
+
1
k=\lfloor \log_2n \rfloor+1
k=⌊log2n⌋+1。
性质5:如果对一棵有 n n n个结点的完全二叉树的结点按层序编号,则对任一结点,有
(1) 如果
i
=
1
i=1
i=1,则结点
i
i
i是二叉树的根结点,无双亲结点;如果
i
>
1
i>1
i>1,则其双亲结点的编号为
⌊
i
2
⌋
\lfloor \frac {i} {2} \rfloor
⌊2i⌋,即当
i
i
i是偶数时,其双亲结点的编号为
i
2
\frac {i} {2}
2i,它是双亲的左孩子;当
i
i
i是奇数时,其双亲结点的编号为
i
−
1
2
\frac {i-1} {2}
2i−1,它是双亲的右孩子。
(2) 如果
2
i
>
n
2i>n
2i>n,则结点
i
i
i无左孩子,即结点
i
i
i为叶子结点;否则其左孩子的编号为
2
i
2i
2i。
(3) 如果
2
i
+
1
>
n
2i+1>n
2i+1>n,则结点
i
i
i无右孩子;否则其右孩子的编号为
2
i
+
1
2i+1
2i+1。
用一组地址连续的存储单元自上而下、自左至右存储完全二叉树上的结点元素,即将完全二叉树上的结点存储在一维数组中。
由此可见,这种顺序存储结构仅适用于完全二叉树,因为,在最坏的情况下,一个深度为
k
k
k且只有
k
k
k个结点单只树却需要长度为
2
k
−
1
2^k-1
2k−1的一维数组。
对于非完全二叉树来树,顺序存储的空间利用率较低,而且树越深,结点越多,浪费的空间更多。通常二叉树都采用链式存储结构,用链表结点来存储二叉树的每个结点,每个结点包括数据域data、左指针域lchild、右指针域rchild。结点定义如下:
class BiTreeLinkNode(object):
def __init__(self, data='#'):
self.data = data
self.lchild = None
self.rchild = None
下图为常用的二叉链表的存储结构,实际上,在不同的应用中,还可以增加某些指针域,如增加指向父结点的指针parent后,变为三叉链表的存储结构。
根据二叉树的定义,可将结点分为以下4种结点:
遍历二叉树就是以一定的规则将二叉树中的结点排列成一个线性序列,使得序列中的每个结点都有一个直接前驱和直接后继,当然,第一个和最后一个结点除外。
空结点的值这里设置为#。
根结点 − − > --> −−>左子树 − − > --> −−>右子树
上面那棵树通过先序遍历得到的先序序列为: A B D H I E C F J G ABDHIECFJG ABDHIECFJG。
对于LR结点,先访问结点本身,再递归访问其左子树,然后递归访问其右子树,最后结束当前递归调用并返回至上一层;对于NR结点,由于其无左子树,所以访问根结点后就直接访问右子树,然后结束当前递归并返回至上一层;对于LN结点,由于其无右子树,所以就正常访问根结点后,然后在递归访问其左子树,最后结束当前递归调用并返回至上一层;对于NN结点,由于其无左子树和右子树,所以访问根结点后即可结束当前递归并返回至上一层。
递归实现先序遍历:
def PreOrder(self, RootNode):
if RootNode:
self.VisitBinaryTreeNode(RootNode)
self.PreOrder(RootNode.lchild)
self.PreOrder(RootNode.rchild)
看图说话,紫色箭头就是向下遍历,直至结点为NN结点,然后返回至上一层(红色的箭头),即当前结点的双亲结点,然后再遍历双亲结点的右孩子。其实递归操作也是如此,只不过递归的代码更简洁。
非递归实现先序遍历:
def PreOrderNoRecursive(self, RootNode):
# 使用一个栈来记录访问过的二叉树的结点
stackNode = []
treeNode = RootNode
while len(stackNode) > 0 or treeNode is not None:
while treeNode is not None:
# 向下遍历
self.VisitBinaryTreeNode(treeNode)
stackNode.append(treeNode)
treeNode = treeNode.lchild
if len(stackNode) > 0:
# 返回至上一层结点
treeNode = stackNode.pop()
# 访问其右孩子
treeNode = treeNode.rchild
左子树 − − > --> −−>根结点 − − > --> −−>右子树
上面那棵树通过中序遍历得到的中序序列为: H D I B E A J F C G HDIBEAJFCG HDIBEAJFCG。
中序遍历的第一个结点是二叉树最左侧的结点,最后一个结点是二叉树最右侧的一个结点。
递归实现中序遍历:
def InOrder(self, RootNode):
if RootNode:
self.InOrder(RootNode.lchild)
self.VisitBinaryTreeNode(RootNode)
self.InOrder(RootNode.rchild)
非递归实现中序遍历:
def InOrderNoRecursive(self, RootNode):
# 使用一个栈来记录未访问过的二叉树的结点
stackNode = []
treeNode = RootNode
while len(stackNode) > 0 or treeNode is not None:
while treeNode is not None:
stackNode.append(treeNode)
treeNode = treeNode.lchild
if len(stackNode) > 0:
treeNode = stackNode.pop()
self.VisitBinaryTreeNode(treeNode)
treeNode = treeNode.rchild
左子树 − − > --> −−>右子树 − − > --> −−>根结点
上面那棵树通过后序遍历得到的后序序列为: H I D E B J F G C A HIDEBJFGCA HIDEBJFGCA。
递归实现后序遍历:
def PostOrder(self, RootNode):
if RootNode:
self.PostOrder(RootNode.lchild)
self.PostOrder(RootNode.rchild)
self.VisitBinaryTreeNode(RootNode)
后序遍历的非递归算法还有那么一丝丝的麻烦,根据上图的二叉树,这里的大致流程就是,如果当前结点有左孩子的话,我就一直遍历,所以就是 A − > B − > D − > H A->B->D->H A−>B−>D−>H,如果当前结点没有右孩子结点,就直接访问当前结点,然后将其双亲结点的 s t a t e state state标记为 T r u e True True,意味着双亲结点的左孩子结点已经被访问过。然后就返回到了结点 D D D,它有右结点 I I I,如果结点 I I I没有孩子结点的话就直接访问,有的话再来一次遍历,访问结点 I I I之后在访问结点 D D D,就这样一直遍历。
class TreeState(object):
# 标记一个结点及其右孩子结点是否已被访问
def __init__(self, Node, state):
self.Node = Node
self.state = state
非递归实现后序遍历:
def PostOrderNoRecursive(self, RootNode): # 使用一个栈来记录未访问过的二叉树的结点及其状态 # 如果当前结点有左孩子, 来标记左孩子结点是否已经被访问过 stackNode = [] treeNode = RootNode while treeNode is not None: # 先跑到结点H tempNode = TreeState(treeNode, state=False) stackNode.append(tempNode) treeNode = treeNode.lchild while len(stackNode) > 0: treeNode = stackNode.pop() if treeNode.Node.rchild is None or treeNode.state: # 当前结点没有右孩子 self.VisitBinaryTreeNode(treeNode.Node) else: # 当前结点有右孩子, 且还没有被访问 stackNode.append(treeNode) treeNode.state = True treeNode = treeNode.Node.rchild while treeNode is not None: tempNode = TreeState(treeNode, state=False) stackNode.append(tempNode) treeNode = treeNode.lchild
从根结点开始,按结点所在层次从小到大,从左到右访问树中的每个结点。
上面那棵树通过层次遍历得到的层次序列为:
A
B
C
D
E
F
G
H
I
J
ABCDEFGHIJ
ABCDEFGHIJ。
层次遍历的流程就相对简单了,这里用到了一个通过collections库里面的deque类构建的双端队列,具体用法可参考我的这篇博客,层次遍历的代码如下:
def LeverOrder(self, RootNode):
from collections import deque
queue = deque(maxlen=100)
queue.append(RootNode)
while len(queue) > 0:
treeNode = queue.popleft()
self.VisitBinaryTreeNode(treeNode)
if treeNode.lchild is not None:
queue.append(treeNode.lchild)
if treeNode.rchild is not None:
queue.append(treeNode.rchild)
en,前面的遍历都已经介绍了,这都不是最重要的,最重要的是没有一棵二叉树怎么遍历???─━ _ ─━✧,下面就通过递归来创建一棵二叉树,建造的流程可参考代码注释,代码如下:
def CreateBinaryTree(self, root=None, pos=0): """ 建立一个层次序列为ABCDEFGHI##J###的二叉树 *****************A***************** *********************************** *********B***************C********* *********************************** *****D*******E*******F*******G***** *********************************** ***H***I***#***#***J***#***#***#*** 递归结束条件: pos>=15 and data='#' 然后回退到上一个结点, 下一部就是创建右子树 0->1->3->7->15->16->8->17->18->4->9->10->2->5->11->23->24->12->6->13->14 A->B->D->H->H左子树->H右子树->I->I左子树->I右子树->E->E左子树->E右子树->C->F->J->J左子树->J右子树->F右子树->G->G左子树->G右子树 :param root: :param pos: :return: """ if pos >= len(self.data_list) or self.data_list[pos] == '#': # 递归结束条件 return None else: root = BiTreeLinkNode(self.data_list[pos]) # 递归建立左子树 root.lchild = self.CreateBinaryTree(root, 2*pos+1) # 递归建立右子树 root.rchild = self.CreateBinaryTree(root, 2*pos+2) return root
class BinaryTree(object): def __init__(self, data_list): self.data_list = data_list def CreateBinaryTree(self, root=None, pos=0): def VisitBinaryTreeNode(self, RootNode): def PreOrder(self, RootNode): def PreOrderNoRecursive(self, RootNode): def InOrder(self, RootNode): def InOrderNoRecursive(self, RootNode): def PostOrder(self, RootNode): def PostOrderNoRecursive(self, RootNode): def LeverOrder(self, RootNode): if __name__ == '__main__': tree_obj = BinaryTree('ABCDEFGHI##J###') bitree = tree_obj.CreateBinaryTree() print('先序遍历递归: ', end='') tree_obj.PreOrder(bitree) print('\n先序遍历非递归: ', end='') tree_obj.PreOrderNoRecursive(bitree) print('\n中序遍历递归: ', end='') tree_obj.InOrder(bitree) print('\n先序遍历非递归: ', end='') tree_obj.InOrderNoRecursive(bitree) print('\n后序遍历递归: ', end='') tree_obj.PostOrder(bitree) print('\n后序遍历非递归: ', end='') tree_obj.PostOrderNoRecursive(bitree) print('\n层序遍历: ', end='') tree_obj.LeverOrder(bitree)
运行结果如下:
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。