树形结构是一种比线性结构更复杂的结构,与线性结构一样,是一种在逻辑上是有序的结构。树形结构(如果非空)具有一个顶点,称为起始结点,起始结点下又连接着其他结点,一直往下延伸。树形结构逻辑上有序的意思就是从起始结点往下延伸的顺序。
以下用一张图来描述下树的一些基本属性:
了解了树的一些基本属性后,我们来看看树的特例之一:二叉树
二叉树
为什么说二叉树是树的特例呢?因为二叉树是一个最简单的树形结构,它的每个结点的后继结点至多有两个,所以后继结点可能为0,1,2。而且明确定义了后继结点中谁是左结点,谁是右结点。为了便于理解,我通过一张图来描述一下常见的二叉树的图形结构:
二叉树的重要性质
性质一:如果一个树中定义根结点的层数为0,以i代表其它结点的层数,那么第i层最多为2^i个结点
说明:第一层是根结点,为1个结点=2º=1,如果为满二叉树,那么第二层就最多为2个结点=2¹=2,依次类推
性质二:如果一个树的高度为h,定义根结点的高度为0,那么一个高度为h的树中最多有2(h+1)-1个结点,最少有2h-1个结点
性质三:对于任何非空二叉树T,如果其叶子结点的个数为N0,其度为2的结点的个数为N2,则N0=N2+1
说明:一个层数为1的二叉树,定义根结点的层数为0,那么它的度为2的结点为0或者1,它的叶子结点为1或者2,一个层数为2的二叉树,它的度为2的结点为0,1,2或3,它的叶子结点为1,2,3,4。依次类推。在此有两种情况可以帮助理解:
1、当往树中添加一个结点时,如果此结点不能构成它的双亲结点度为2,那么此种情况并不影响树中叶子结点的个数,比如看下图:
此种情况时,并不会改变树中叶子结点个数,比如左图中有叶子结点C,D。在C中添加了E的左子结点后,树中有叶子结点D,E。还是两个叶子结点,此种情况并不影响N0=N2+1的结果。
2、当往树中添加一个结点时,如果此结点加入后,它的双亲结点的度从1变成2,此时,度为2的结点个数+1,叶子结点也加1,也不影响N0=N2+1的结果。
综合上述两种情况,N0=N2+1的结果只需要从单点树(就是只有一个根结点的树)中来验证即可,因为单点树只要满足了此条性质,无论往单点数加什么结点,都不会影响这条性质的结果。在单点树中N0=1,N2=0,所以N0=N2+1
性质四:满二叉树的叶子结点比分支结点多一个
说明:满二叉树中的结点要么是叶子结点,要么就是度为2的分支结点,推论跟性质三类似,可以作为参考
性质五:n个结点的完全二叉树的高度h为不大于㏒₂ⁿ的最大整数。
说明:根据性质二与完全二叉树的性质可得出,2h-1<T<2(h+1)-1 ——> 2^h <T<2^(h+1) ——> h<㏒₂ⁿ<h+1 ——> h为不大于㏒₂ⁿ的最大整数
性质六:如果n个结点的完全二叉树的结点按层次并按从左到右的顺序从0开始编号,对任一结点i(0<=i<=n-1)都有:
1、序号为0的结点是根结点
2、对于i>0,其父结点的编号是(i-1)/2
3、若2i+1<n,其左子结点的序号为2i+1,否则无左子结点
4、若2i+2<n,其右子结点的序号为2i+2,否则无右子结点
说明:
(1)对于i>0,其父结点的编号是(i-1)/2:
对于一个结点查找其父结点,我们可以从父结点出发,去找子结点的规律,参考下图:
可能看出,父结点下标k=(i-1)除以2,或者等于(i-2)除以2,如果不考虑整除的话,用/取整除来代替,于是父结点下标k=(i-1)/2
(2)若2
i+1<n,其左子结点的序号为2
i+1,否则无左子结点
解释下2*i+1<n,为什么有这个限制条件,有了这个,就可以避免掉单点树,因为单点树也是一个完全二叉树,但是单点树没有左子结点与右子结点,于是3,4两条性质不适用。而
其左子结点的序号为2*i+1,否则无左子结点
这句,通过上面的父结点去理解即可。
(3)同(2)
定义一个二叉树
点击查看代码
class BinNode:
"""
二叉树结点类
"""
def __init__(self, _data, left=None, right=None):
# 初始化结点
self.data = _data
self.left = left
self.right = right
class BinTree:
"""
二叉树类
"""
def __init__(self):
self._root = None
def is_empty(self):
return self._root is None
def root(self):
return self._root
def leftchild(self):
return self._root.left
def rightchild(self):
return self._root.right
def set_root(self, rootnode):
self._root = rootnode
def set_left(self, leftnode):
self._root.left = leftnode
def set_right(self, rightchild):
self._root.right = rightchild
def preorder_elements(self, t):
"""
先序根遍历(递归方式)
:return:
"""
root = t
if root is None:
return
print(root.data)
self.preorder_elements(root.left)
self.preorder_elements(root.right)
def preorder_elem_nonrec(self):
"""
先序根遍历(非递归方式)
:return:
"""
root = self._root
s = SStack()
t = root
while t is not None and not s.is_empty():
while t is not None:
print(t.data)
t = t.left
if t.right is not None:
s.push(t.right)
t = s.pop()
def postorder_elements(self, t):
"""
后序根遍历(递归方式)
:return:
"""
root = t
if root is None:
return
self.postorder_elements(root.left)
self.postorder_elements(root.right)
print(root.data)
def postorder_elem_nonrec(self):
"""
后序根遍历,非递归方式
:return:
"""
root = self._root
s = SStack()
t = root
# 从根结点开始循环
while t is not None or not s.is_empty():
while t is not None:
# 将当前结点加入栈中,并一直往树的左边找
s.push(t)
t = t.left if t.left is not None else t.right
# 上面循环后,s栈中存的就是根结点的所有待出栈的左边结点
node = s.pop()
print(node.data)
# 判断是否找到了当前栈顶结点的左子结点, 这时候依据后序根遍历,应该去找栈顶结点的右边结点
if not s.is_empty() and node == s.top().left:
t = s.top().right
else:
t = None
def middle_elem(self, t):
"""
中序根遍历(递归方式)
:return:
"""
root = t
if root is None:
return
self.middle_elem(root.left)
print(root.data)
self.middle_elem(root.right)
def middle_elem_nonrec(self):
"""
中序根遍历(非递归方式)
:return:
"""
root = self._root
s = SStack()
t = root
while t is not None or not s.is_empty():
while t is not None:
s.push(t)
t = t.left
node = s.pop()
print(node.data)
if node.right is not None:
t = node.right
else:
t = None
def breadth_first(self):
"""
宽度优先遍历,从左到右依次遍历结点
:return:
"""
root = self._root
# 需要用到队列,以下代码省略
二叉树的遍历
还是以两种搜索方式为基础,深度优先遍历与广度优先遍历。深度优先遍历:顺着一条路径尽可能的向前搜索,必要时回溯。广度优先遍历:在所有路径上并头前进。
以深度优先遍历的方式:
按深度优先方式遍历一棵二叉树,需要做三件事,遍历左子树、遍历右子树和访问根结点。这三件事的不同顺序会产生三种遍历的顺序:
1、先序根遍历:遍历根结点 -> 遍历左子树 -> 遍历右子树
2、中序根遍历:遍历左子树 -> 遍历根结点 -> 遍历右子树
3、后序根遍历:遍历左子树 -> 遍历右子树 -> 遍历根结点
方便记忆的方式:从遍历名称解读,先序根就是先访问根结点,再访问左子树,最后访问右子树,中序根就是中间步骤访问根结点,这三种方式中,左子树一定是优先于右子树被访问的。还有就是在得到一个二叉树序列时,我们想要算出它的先序、中序与后序遍历序列的时候,容易搞混,在进行分析的时候,我们一定要将此规则应用到每一个子树上,包括叶子结点也是一个子树,这才不会混淆,以下是举例说明:
当进行中序根遍历或后序根遍历时也是一样,在一步步往下遍历时,遇到的子树都要去应用上遍历规则,才会才能找到第一个弹出的点,当往下遍历时子树为空时,这时就需要回溯,回溯到最近的一次访问子树,从它开始继续往下遍历。
以广度优先的方式:
按广度优先遍历,则是将子树按从上到下,从左到右的顺序一直遍历结束。
两种遍历方法的算法:
深度优先往下遍历时,当遇到分支时,最先遇到的分支将是最后遍历的选择,这符合栈的后进先出,先进后出的规则,所以深度优先遍历将会使用到栈这个数据结构。广度优先遍历时,当遇到分支时,将会齐头并进,是符合队列的先进先出,后进后出的规则,广度优先遍历一般会使用到队列。上面二叉树的定义代码中,就定义了多个遍历的方法,分别采用了深度遍历(包括三种遍历顺序)和广度遍历。可用于参考。
二叉树的应用
应用一:优先队列
优先队列的概念:优先队列就是一种考虑队列中元素优先级的队列,本质上还是一种队列数据结构,但是弹出的时候需要将队列中优先级最高的元素弹出,如果出现相同优先级的元素,保证弹出它们之间的一个。
实现优先队列的方式:
1、使用顺序表来实现优先队列
顺序表是基础的数据结构,对于顺序表的理解可以参加数组这个结构。用顺序表来实现优先队列,通过优先级按下标从表头到表尾进行排序元素,表尾的元素优先级最高,每次取出优先级最高的元素的时候,时间复杂度为O(1)。当插入一个新元素时,需要比较并将元素插入到合适的位置,构建新的优先级顺序序列,此时所造成的元素移动的时间复杂度开销为O(n)
2、使用链接表来实现优先队列
用链接表实现优先队列,将优先级高的元素放置链接表的表头,因为链接表头部删除的复杂度为O(1)。此种实现方式也会造成有线性时间的复杂度,在对链接表进行插入元素所遍历元素造成的开销。
顺序表与链接表相关概念可以参考:数据结构与算法之线性结构
以上两种方式都会存在有线性时间O(n)的复杂度,这个不是最好的方案,可以通过树形结构来优化方案。
3、用堆来实现优先队列
堆:从结构上看,堆就是结点里存储数据的完全二叉树。但堆中的元素满足一种顺序。如果完全二叉树的树根为最小元素,树中每个结点的数据均小于或等于其子结点的数据,这种构造出来的完全二叉树或堆叫做小顶堆,如果最大元素在树根,称为大顶堆。有了堆结构之后对于构造完全二叉树有什么好处呢?
对于堆顶元素,我们可以直接拿得到,时间复杂度是O(1),需要思考的是插入元素与拿出堆顶元素后重新构建堆序的时间复杂度,这两个操作都可以在O(logn)的时间内完成。下面来解释:
插入元素:向堆中插入一个元素,需要与堆中已有的数据进行比较,可以将元素与堆中的最后一个元素进行比较,如果此元素小于它的父结点,那么将此元素上移,不停的上移直到根结点比较结束。此时就完成了堆序的构建。因为完全二叉树的性质,进行比较与替换的次数只需要logn次就行了,参考上述的性质五
弹出元素:需要将剩下的元素构建成新的堆序。将末尾元素放置堆顶,然后将剩下的元素与堆顶元素进行比较,直至构成新的堆序。操作的时间复杂度也是O(logn)
总结:对于优先队列,通过线性结构来定义,避免不了某些操作会出现线性时间的复杂度,通过树形结构来定义,可以将时间复杂度缩短至logn。
基于堆实现的优先队列的Python代码
点击查看代码
class priorityQueueError(ValueError):
pass
class priorityQueue:
"""
用堆实现的优先队列类
"""
def __init__(self, elist=[]):
self._elems = list(elist)
if elist:
self.buildheap()
def is_empty(self):
return not self._elems
def peek(self):
if self.is_empty():
raise priorityQueueError("in peek")
return self._elems[0]
def enqueue(self, e):
"""
入队操作
:return:
:param e 添加的元素
"""
# 加入一个假元素
self._elems.append(None)
self.siftup(e, len(self._elems) - 1)
def siftup(self, e, last):
"""
往优先队列里插入数据,调整堆序
:param e: 元素
:param last: 结束
:return: None
"""
elems, i, j = self._elems, last, (last - 1) // 2
while i > 0 and e < elems[j]:
elems[i] = elems[j]
i = j
j = (j - 1) // 2
elems[i] = e
def dequeue(self):
"""
出队
:return:
"""
if self._elems is None:
raise priorityQueueError("in dequeue")
# 弹出首端元素
elems = self._elems
e0 = elems[0]
prio_elem = elems.pop()
# 重新构建堆序
if len(elems) == 0:
raise priorityQueueError("no more elements")
self.siftdown(prio_elem, 0, len(elems))
return e0
def siftdown2(self):
"""
弹出首端元素后,构建堆序
思路:每次都补前面的一个坑。直到到达堆的尾部
缺陷:到了叶子结点的时候,有三种情况需要判断,逻辑复杂
情况1:如果只有一个结点的话,直接将此结点放入坑中即可。
情况2:如果有两个结点, 需要选一个较小的结点去填坑
情况3:如果选择的结点为右结点,结束即可,如果选择的结点为左结点,则需要将右结点左移一位,用于补位
:return:
"""
elems = self._elems
i, j = 2 * parent_index + 1, 2 * parent_index + 2
parent_index = 0
while 2 * parent_index + 1 < len(elems) - 1:
if elems[i] <= elems[j]:
elems[parent_index] = elems[i]
parent_index = i
else:
elems[parent_index] = elems[j]
parent_index = j
i, j = 2 * parent_index + 1, 2 * parent_index + 2
temp = 2 * parent_index + 1
elem = elems.pop()
if 2 * parent_index + 1 < len(elems) - 1:
elem2 = elems.pop()
if elem2 < elem:
elems[parent_index] = elem2
elems[2 * parent_index + 1] = elem
else:
elems[parent_index] = elem
elems[2 * parent_index + 1] = elem
def buildheap(self):
"""
初始化构建堆
:return:
"""
end = len(self._elems)
for i in range(end // 2, -1, -1):
self.siftdown(self._elems[i], i, end)
def siftdown(self, e, begin, end):
"""
构建堆,在begin起始点添加一个堆顶,将以下的子堆关联起来共同构建一个更大的子堆
:param e: 需要添加到堆顶的元素
:param begin: 堆顶元素的下标
:param end: 堆的最大下标
:return:
"""
elems = self._elems
i, j = begin, begin * 2 + 1
while j < end:
if j + 1 < end and elems[j + 1] < elems[j]:
# 存在右边结点,并右边结点小于当前结点
j += 1
if e < elems[j]:
# 构建结束,已经找到e需要插入的点了
break
# 如果当前e不是最小的,将j位置的元素放到e位置中,e的位置继续往下找
elems[i] = elems[j]
i, j = j, 2 * j + 1
elems[i] = e
堆的应用:堆排序
如果一个顺序表中,存储的是个小顶堆,那么按照优先队列的操作方式依次弹出堆顶元素,得到的将是一个递增序列。这种方法可以帮助于排序,
应用二:哈夫曼树(最优二叉树)
哈夫曼树是哈夫曼编码所使用构建的一种二叉树结构,为了了解哈夫曼树的特性,先来了解下哈夫曼编码的由来。
哈夫曼编码:是可变字长编码(VLC)的一种,Huffman于1952年提出一种编码方法,该方法完全依据字符出现概率来构造异字头的平均长度最短的码字(百度百科摘选)。这是哈夫曼编码的定义,以下是定义中一些概念的理解。
1、可变字长编码(VLC)是什么意思?
可变字长编码的含义就是每个字符编码的长度可以动态改变,可变字长对应的就是不变字长编码,或称为定长编码,比如一串文本字符,ABCD,用定长编码来编码为:00,01,10,11,用的是8位来代表这四个字符。如果变长编码来编码ABCD的话,使用0,1,10,11来进行编码,每个字符所编码的长度不确定,得到的编码为(011011)
2、异字头是什么意思?
上述用定长编码得到的编码串为00,01,10,11,所保证ABCD编码为(00,01,10,11)与(00,01,10,11)解码为ABCD都是唯一的。使用变长的(011011),当前编码解码后并不能唯一得到ABCD的解码,可能得到的是ADAD,ABBABB等原码。所以要想使用定长编码,就需要有一种方式来保证一致性。怎么保持一致性的关键在于任何一个字符的编码都不能是另一个字符编码的前缀,而异字头的含义就是指前缀不同。所以哈夫曼编码是一种一致性编码或称为前缀编码。
3、哈夫曼编码用什么来确定编码长度?
用的是字符出现的概率。字符出现的越多,所编码的长度就越小。
4、哈夫曼编码怎么保证“异字头”?
使用的是哈夫曼树来构造,一开始并不是叫哈夫曼树,只是哈夫曼编码使用了一种最优二叉树的方式来保证任何一个字符的编码都不能是另一个字符编码的前缀,而且还能通过带权的方式(出现的频率就是权值)来获取每个字符的编码,将左子树的边定义为0,右子树的边定义为1。此种带权最优二叉树就被称为哈夫曼树。
哈夫曼树介绍
1、图解
2、定义
给定n个权值作为n个叶子结点,构造一颗二叉树,若该树的带权路径长度达到最小,则这样的二叉树称为最优二叉树。
说明:一个树带权路径长度怎么算?就是从根结点到一个叶子结点所经过的边的最短条数(或者是每个叶子结点的层数,根结点层数为0)* 叶子结点所带的权值。
3、构建算法
点击查看代码
# 哈夫曼树
from trees.tree_do import PriorityQueue
from trees.binary_tree_node import BinNode
class HTNode(BinNode):
"""
哈夫曼树扩展的结点类
"""
def __lt__(self, other):
"""
比较结点之间的权值大小
:param other:
:return:
"""
return self.data < other.data
class HuffmanPrioQ(PriorityQueue):
"""
哈夫曼树所使用的优先队列
"""
def number(self):
"""
返回当前队列中所剩元素的个数
:return:
"""
return len(self._elems)
class HuffmanTree:
"""
哈夫曼树
"""
def __init__(self):
pass
def huffmanTree(self, weights):
trees = HuffmanPrioQ()
for w in weights:
trees.enqueue(HTNode(w))
while trees.number() > 1:
t1 = trees.dequeue()
t2 = trees.dequeue()
x = t1.data + t2.data
trees.enqueue(HTNode(x, t1, t2))
return trees.dequeue()
4、哈夫曼编码的应用
哈夫曼编码可以支持对数据的解压缩。而且是无损的。
5、其它
哈夫曼编码又分为自适应的与非自适应的。适应性哈夫曼编码(Adaptive Huffman coding),又称动态哈夫曼编码(Dynamic Huffman coding),是基于哈夫曼编码的适自适应编码技术。它允许在符号正在传输时构建代码,允许一次编码并适应数据中变化的条件,即随着数据流的到达,动态地收集和更新符号的概率(频率)。一遍扫描的好处是使得源程序可以实时编码,但由于单个丢失会损坏整个代码,因此它对传输错误更加敏感(摘自百度百科)。非适应性的又称为静态哈夫曼编码,静态哈夫曼编码需要先扫码编码原文,得到文本频率,然后再次扫码编码原文进行编码,所以静态哈夫曼编码在内存使用上,效率上都劣于动态哈夫曼编码。
总结:
1、树形结构相较于线性结构,是具有层次关系。在我们生活中,比如家里的祖辈关系、公司的组织结构关系等等都是属于树形结构。
2、二叉树是树形结构中的一种,二叉树顾名思义,就是两个分叉的树,所以二叉树的每一个结点至多有两个子结点。
3、二叉树的遍历有深度优先与广度优先两种,深度优先又分为先序根、中序根、后序根三种
4、在二叉树中又存在一种特殊的二叉树,为完全二叉树。在结构上很像完全二叉树的可以更优的实现优先队列的结构为:堆
5、二叉树的另一项重要应用就是哈夫曼树,哈夫曼树是哈夫曼编码过程中所构建的一种最优二叉树,它广泛应用于解压缩领域,是一种一致性无损压缩算法。
6、二叉树只是树形结构的一种,且是最简单的一种,还有多种应用广泛的树形结构并没有介绍,比如:
基于二叉树的扩展:二叉搜索(排序)树等
平衡树类:AVL、红黑树、B树、B+树等
图论相关:最小生成树等