文章目录
金三银四面试季,必然少不了关于数据结构与算法的面试,数据结构最重要的恐怕就是树与链表了,而关于树的定义、用途、操作等,必然在面试中要问到。以前也零散的看过一些这方面的知识,终于决定做一下系统的总结,以备后用。
树的基本概念
- 树(Tree)是由一个或多个结点组成的有限集合T,其中有一个特定的称为根的结点;其余结点可分为m(m≥0)个互不相交的有限集T1,T2,T3 ,…,Tm,每一个集合本身又是一棵树,且称为根的子树。
- 结点(Node):树中的元素,包含数据项及若干指向其子树的分支。
- 结点的度(Degree):所有结点当中,子树分支最最多的就是树的度
- 结点的层次:从根结点开始算起,根为第一层,
- 叶子(Leaf):度为零的结点,也称端结点。
- 孩子(Child):结点子树的根称为该结点的孩子结点。
- 双亲(Parent):孩子结点的上层结点,称为这些结点的双亲。
- 兄弟(Sibling):同一双亲的孩子。
- 深度(Depth): 树中结点的最大层次数
- 森林(Forest):互不相交的树的集合
一般树与二叉树的区别
- 树的结点个数至少为1,而二叉树的结点个数可以为0;
- 树的结点最大度数没有限制,而二叉树结点的最大度数为2;
- 树的结点无左、右之分,而二叉树的结点有左、右之分。
二叉树
二叉树是一种重要的树形结构,其结构定义为:二叉树是n(n≥0)个结点的有限集,它或为空树(n=0),或由一个根结点和两棵分别称为根的左子树和右子树的、互不相交的二叉树组成。
既不是满二叉树也不是完全二叉树,则为一般二叉树
二叉树的每个结点至多有二棵子树(不存在度大于2的结点),二叉树的子树有左右之分,次序不能颠倒。
- 1.在二叉树的第K层上,最多有 2k-1 (K >= 1)个结点
- 2.深度为K的二叉树,最多有 2k - 1 个结点(K>=1)
- 3.对于任何一棵二叉树,如果其叶子结点的个数为K,度为2的结点数为M,则K=M+1
- 4.对于一棵有 n 个结点的完全二叉树的结点按层次进行编号(如上图,从第一层到第 (log 2n 向下取整),每层从左到右),对任意结点 i (1<i<n),有:
如果i=1,则结点i无父结点,是二叉树的根,如果i>1,则父结点为 i/2 向下取整
如果2i>n,则结点i为叶子结点,无左子结点,否则,其左子结点为2i
如果2i+1>n,则结点i无右子结点,否则,其右子结点是结点2i+1
二叉树存储方式一般分为两种,顺序存储与链表式存储
顺序存储:
链式存储:
顺序存储可能造成存储空间的浪费。
二叉树存储结构
- 孩子表示法:用指针指出每个节点的孩子节点
class Node:
def __init__(self,item,l_child=None, r_child=None):
self.item = item
self.l_child = l_child
self.r_child = r_child
#优点:寻找一个节点的孩子节点比较方便。
#缺点:寻找一个节点得双亲节点很不方便。
- 双亲表示法:用指针表示出每个节点的双亲节点
class Node:
def __init__(self,item,parent=None):
self.item = item
self.parent = parent
#优点:寻找一个节点得双亲节点操作实现很方便
#缺点:寻找一个节点的孩子节点很不方便
- 孩子双亲表示法:用指针既表示出每个节点得双亲节点,也表示出每个节点的孩子节点
class Node:
def __init__(self,item,parent=None,l_child=None, r_child=None):
self.item = item
self.parent = parent
self.l_child = l_child
self.r_child = r_child
#优点:找某个节点的双亲节点和孩子节点非常方便
- 孩子兄弟表示法:即表示出每个节点的第一个孩子节点,也表示出每个节点的下一个兄弟节点
class Node:
def __init__(self,item,NextBrother=None,l_child=None, r_child=None):
self.item = item
self.NextBrother = NextBrother #指向其下一个兄弟节点
self.l_child = l_child
self.r_child = r_child
#优点:找某个节点的兄弟结点节点和孩子节点非常方便
满二叉树
一棵深度为k,且有2k - 1个节点的树是满二叉树。
通俗解释:除叶结点外,每一个结点都有左右子树,且叶结点都处在最底层的二叉树。
- 如果一颗树深度为h,最大层数为k,且深度与最大层数相同,即k=h;
- 它的叶子数是: 2(h-1)
- 第k层的结点数是: 2(k-1)
- 总结点数是: 2k - 1
- 树高:h=log2(n+1)
如上图a
所示,就是一个满二叉树,从图形上看,满二叉树是一个三角形。
完全二叉树
完全二叉树是由满二叉树而引出来的。对于深度为K的,有n个结点的二叉树,当且仅当其每一个结点都与深度为K的满二叉树中编号从1至n的结点一一对应时称之为完全二叉树。
若设二叉树的深度为h,除第 h 层外,其它各层 (1~h-1) 的结点数都达到最大个数,第h 层所有的结点都连续集中在最左边,这就是完全二叉树。
满二叉树一定是完全二叉树,完全二叉树不一定是满二叉树。
- 深度为k的完全二叉树,至少有2(k-1)个节点,至多有2k-1个节点。
- 树高h=log2n + 1。
二叉排序树
二叉排序树又叫二叉查找树或者二叉搜索树,它首先是一个二叉树,而且必须满足下面的条件:
- 若左子树不空,则左子树上所有结点的值均小于它的根节点的值;
- 若右子树不空,则右子树上所有结点的值均大于它的根结点的值
- 左、右子树也分别为二叉排序树
- 没有键值相等的节点
二叉树的遍历
先序遍历
- 访问根结点
- 访问左子树的左子树,再访问左子树中的右子树
- 访问右子树的左子树,再访问右子树中的右子树
- 任意子树输出顺序为:父结点——左子结点——右子结点
- 如上图先序遍历顺序为:
A-B-D-H-I-E-J-C-F-K-G
中序遍历
- 先访问左子树中的左子树,再访问左子树中的右子树
- 访问根结点。
- 后访问右子树中的左子树,再访问右子树中的右子树
- 任意子树输出顺序为:左子结点——父结点——右子结点
- 如上图中序遍历顺序为:
H-D-I-B-E-J-A-F-K-C-G
后序遍历
- 先访问左子树中的左子树,再访问左子树中的右子树
- 再访问右子树中的左子树,再访问右子树中的右子树
- 访问根结点
- 任意子树输出顺序为:左子结点——右子结点——父结点
- 如上图后序遍历顺序为:
H-I-D-J-E-B-K-F-G-C-A
层次遍历
- 访问根结点,即第1层,设为 i 层
- 访问i+1层的结点,从左到右顺序访问
- 层次遍历输出顺序为:根结点—— i 层结点从左到右—— i +1层结点从左到右
- 如上图层次遍历顺序为:
A-B-C-D-E-F-G-H-I-J-K
Python代码实现
class Node(object):
def __init__(self, item):
self.item = item
self.lchild = None
self.rchild = None
class Tree(object):
NodeList = []
def __init__(self):
self.root = None
def add(self, item):
node = Node(item)
if self.root == None:
self.root = node
Tree.NodeList.append(self.root)
else:
while True:
node = Tree.NodeList[0]
if node.lchild == None:
node.lchild = node
Tree.NodeList.append(node.lchild)
return
elif node.rchild == None:
node.rchild=node
Tree.NodeList.append(node.rchild)
Tree.NodeList.pop(0)
return
def traverse(self): # 层次遍历
if not self.root:
return None
NodeList = [self.root]
res = [self.root.item]
while NodeList != []:
pop_node = NodeList.pop(0)
if pop_node.lchild is not None:
NodeList.append(pop_node.lchild)
res.append(pop_node.lchild.item)
if pop_node.rchild is not None:
NodeList.append(pop_node.rchild)
res.append(pop_node.rchild.item)
return res
def preorder(self,root): # 先序遍历
if root is None:
return []
result = [root.item]
left_item = self.preorder(root.lchild)
right_item = self.preorder(root.rchild)
return result + left_item + right_item
def inorder(self,root): # 中序序遍历
if root is None:
return []
result = [root.item]
left_item = self.inorder(root.lchild)
right_item = self.inorder(root.rchild)
return left_item + result + right_item
def postorder(self,root): # 后序遍历
if root is None:
return []
result = [root.item]
left_item = self.postorder(root.lchild)
right_item = self.postorder(root.rchild)
return left_item + right_item + result
t = Tree()
for i in range(10):
t.add(i)
print('层序遍历:',t.traverse())
print('先序遍历:',t.preorder(t.root))
print('中序遍历:',t.inorder(t.root))
print('后序遍历:',t.postorder(t.root))
根据遍历结果推出树形结构
-
已知先序与中序遍历结果,可以推导出树形结构
-
已知中序与后序遍历结果,可以推导出树形结构
-
已知先序与后序遍历结果,无法推导出树形结构,因为无法判断根结点的之前或之后的结点是属于左子树还是右子树
红黑树
红黑树(Red Black Tree) 是一种自平衡二叉查找树。红黑树是每个节点都带有颜色属性的二叉查找树,颜色或红色或黑色。红黑树的性质有以下5点
- 1、结点是红色或黑色。
- 2、根结点是黑色。
- 3、每个叶结点(NIL节点,空节点)是黑色的。
- 4、每个红色结点的两个子结点都是黑色。(从每个叶子到根的所有路径上不能有两个连续的红色结点)
- 5、从任一节点到其每个叶子的所有路径都包含相同数目的黑色结点。
平衡二叉树:又称AVL树,它或者是颗空树,或者是具有下列性质的二叉树:它的左子树和右子树都是平衡二叉树,且左子树和右子树的深度之差的绝对值不超过1。若将二叉树节点的平衡因子BF定义为该节点的左子树的深度减去它的右子树的深度,则平衡二叉树上所有节点的平衡因子只可能为-1,0,1。只要二叉树上有一个节点的平衡因子的绝对值大于1,那么这颗平衡二叉树就失去了平衡。
- 非叶子节点最多拥有两个子节点;
- 非叶子节值大于左边子节点、小于右边子节点;
- 树的左右两边的层级数相差不会大于1;
- 没有值相等重复的节点
左旋
所谓以X为中心左旋,就是调整X与其右子树的关系,原 X 的右子节点 Y 变为父节点,将 X 父节点变为左子节点;那原先的子节点在旋转之后如何分布呢?其实不必要计较哪些规则语句,因为可以推导出来
推导步骤如下:
- 根据二叉搜索树的规则,左子结点比父结点小,父结点比右子结点小,因此左旋第一步就是将X的右子结点Y提升为父结点
- 由于X比Y小,因此X只能作为Y的左子结点
- 原X的左子树,仍然比X小,所以只能作为调整后X的左子树
- 原Y的右子树,仍然比Y大,所以只能作为调整后Y的右子树
- 原Y的左子树因为是Y的子树,所以其比X要大,因此只能作为X的右子树
右旋
所谓以X为中心右旋,就是调整X与其左子树的关系,原 X 的左子节点 Y 变为父节点,将 X 父节点变为右子节点;那原先的子节点在旋转之后如何分布呢?
推导步骤如下:
- 根据二叉搜索树的规则,左子结点比父结点小,父结点比右子结点小,因此右旋第一步就是将X的左子结点Y提升为父结点
- 由于X比Y大,因此X只能作为Y的右子结点
- 原X的右子树,仍然比X大,所以只能作为调整后X的右子树
- 原Y的左子树,仍然比Y小,所以只能作为调整后Y的左子树
- 原Y的右子树因为是Y的子树,所以其比X要小,因此只能作为X的左子树
红黑树的插入
红黑树首先是一棵二叉排序树,所以它的插入操作要遵循二叉排序树的插入原则;先把要插入的节点的key与根进行比较,小则和根的左孩子做比较,大则跟右孩子作比较,直到找到叶子节点。
现在按照二叉查找树的插入方式插入了节点21,那么这个21该是什么颜色呢?
1、如果是黑色,那么不管原来的红黑树是什么样的,这样一定会破坏平衡,因为原来的树是平衡的,现在在这一条路径上多了一个黑色,必然违反了性质5
2、如果是红色,那么也有可能会破坏平衡,主要可能是违反了性质4,新插入的点21的父节点22为红色。但是却有一种特殊情况,比如上图中,如果我插入一个key=0的节点。把0这个节点置为红色,并不会影响原来树的平衡,因为0的父节点是黑色。如下图:
所以选择插入的21节点为红色
21的颜色置为红色,他的父亲也是红色,违反性质4,需要调整,怎么调整呢?现在就要看新插入的点的叔叔节点了。
因为21的叔叔27是红色的。所以要保持平衡,可以把25设置成红色,22,27分别设置成黑色,
新的问题:17是红色的,孩子25也是红色的,违反了性质4,需要继续调整,将17、18调整为黑色,此时根结点根据变换规则会变成红色,则不符合性质2,因此继续调整根结点的颜色为黑色,至此所有规则都符合,插入完成。
这只是插入操作一个示例,总的来说红黑树的插入操作需要变色、左旋、右旋来完成,更详细的说明可以参考我下面列出的博客。
红黑树python代码实现
class RBNode:
def __init__(self, val, color="black"):
self.key = x
self.left = None
self.right = None
self.parent = None
self.color = 'black'
self.size=None
def is_black_node(self):
return self.color == "black"
def is_red_node(self):
return self.color == "red"
def set_black_node(self):
self.color = "black"
def set_red_node(self):
self.color = "red"
#定义红黑树
class RBTree(object):
def __init__(self):
self.nil = RBNode(0)
self.root = self.nil
#左旋转
def LeftRotate( T, x):
'''
* 左旋示意图:对节点x进行左旋,左旋选择x的右结点
* parent parent
* / /
* x y
* / \ / \
* lx y ----------> x ry
* / \ / \
* ly ry lx ly
'''
y = x.right
x.right = y.left
if y.left != T.nil:
y.left.parent = x
y.parent = x.parent
if x.parent == T.nil:
T.root = y
elif x == x.parent.left:
x.parent.left = y
else:
x.parent.right = y
y.left = x
x.parent = y
#右旋转
def RightRotate( T, x):
'''
* 右旋示意图:对节点x进行右旋,选择x的左结点
* parent parent
* / /
* x y
* / \ / \
* y rx -----> ly x
* / \ / \
* ly ry ry rx
'''
y = x.left
x.left = y.right
if y.right != T.nil:
y.right.parent = x
y.parent = x.parent
if x.parent == T.nil:
T.root = y
elif x == x.parent.right:
x.parent.right = y
else:
x.parent.left = y
y.right = x
x.parent = y
#红黑树的插入
def RBInsert( T, z):
y = T.nil
x = T.root
while x != T.nil:
y = x
if z.key < x.key:
x = x.left
else:
x = x.right
z.parent = y
if y == T.nil:
T.root = z
elif z.key < y.key:
y.left = z
else:
y.right = z
z.left = T.nil
z.right = T.nil
z.color = 'red'
RBInsertFixup(T, z)
return z.key, '颜色为', z.color
#红黑树的上色
def RBInsertFixup( T, z):
while z.parent.color == 'red':
if z.parent == z.parent.parent.left:
y = z.parent.parent.right
if y.color == 'red':
z.parent.color = 'black'
y.color = 'black'
z.parent.parent.color = 'red'
z = z.parent.parent
else:
if z == z.parent.right:
z = z.parent
LeftRotate(T, z)
z.parent.color = 'black'
z.parent.parent.color = 'red'
RightRotate(T,z.parent.parent)
else:
y = z.parent.parent.left
if y.color == 'red':
z.parent.color = 'black'
y.color = 'black'
z.parent.parent.color = 'red'
z = z.parent.parent
else:
if z == z.parent.left:
z = z.parent
RightRotate(T, z)
z.parent.color = 'black'
z.parent.parent.color = 'red'
LeftRotate(T, z.parent.parent)
T.root.color = 'black'
def RBTransplant( T, u, v):
if u.parent == T.nil:
T.root = v
elif u == u.parent.left:
u.parent.left = v
else:
u.parent.right = v
v.parent = u.parent
def RBDelete(T, z):
y = z
y_original_color = y.color
if z.left == T.nil:
x = z.right
RBTransplant(T, z, z.right)
elif z.right == T.nil:
x = z.left
RBTransplant(T, z, z.left)
else:
y = TreeMinimum(z.right)
y_original_color = y.color
x = y.right
if y.parent == z:
x.parent = y
else:
RBTransplant(T, y, y.right)
y.right = z.right
y.right.parent = y
RBTransplant(T, z, y)
y.left = z.left
y.left.parent = y
y.color = z.color
if y_original_color == 'black':
RBDeleteFixup(T, x)
#红黑树的删除
def RBDeleteFixup( T, x):
while x != T.root and x.color == 'black':
if x == x.parent.left:
w = x.parent.right
if w.color == 'red':
w.color = 'black'
x.parent.color = 'red'
LeftRotate(T, x.parent)
w = x.parent.right
if w.left.color == 'black' and w.right.color == 'black':
w.color = 'red'
x = x.parent
else:
if w.right.color == 'black':
w.left.color = 'black'
w.color = 'red'
RightRotate(T, w)
w = x.parent.right
w.color = x.parent.color
x.parent.color = 'black'
w.right.color = 'black'
LeftRotate(T, x.parent)
x = T.root
else:
w = x.parent.left
if w.color == 'red':
w.color = 'black'
x.parent.color = 'red'
RightRotate(T, x.parent)
w = x.parent.left
if w.right.color == 'black' and w.left.color == 'black':
w.color = 'red'
x = x.parent
else:
if w.left.color == 'black':
w.right.color = 'black'
w.color = 'red'
LeftRotate(T, w)
w = x.parent.left
w.color = x.parent.color
x.parent.color = 'black'
w.left.color = 'black'
RightRotate(T, x.parent)
x = T.root
x.color = 'black'
def TreeMinimum( x):
while x.left != T.nil:
x = x.left
return x
#中序遍历
def Midsort(x):
if x!= None:
Midsort(x.left)
if x.key!=0:
print('key:', x.key,'x.parent',x.parent.key)
Midsort(x.right)
代码参考博客:https://blog.youkuaiyun.com/z649431508/article/details/78034751
红黑树推荐博客文章:https://www.cnblogs.com/yangecnu/p/Introduce-Red-Black-Tree.html
AVL树LL、LR、RR、RL类型判断和调整:http://www.cnblogs.com/qingergege/p/7294892.html
B树、B-树、B+树、B*树
这里的不同的博客有很多不同的说法,有的说B树是二叉搜索树,有的说是多路搜索树,有的说是平衡二叉搜索树,这里我更倾向于是平衡二叉搜索树。下面是按照多路搜索树的定义
B树:是一种多路搜索树(并不是二叉的)
B树与红黑树最大的不同在于,B树的结点可以有许多子女,从几个到几千个。那为什么又说B树与红黑树很相似呢?因为与红黑树一样,一棵含n个结点的B树的高度也为O(lgn),但可能比一棵红黑树的高度小许多,应为它的分支因子比较大。所以,B树可以在O(logn)时间内,实现各种如插入(insert),删除(delete)等动态集合操作。
- 定义任意非叶子结点最多只有M个儿子;且M>2;
- 根结点的儿子数为[2, M];
- 除根结点以外的非叶子结点的儿子数为[M/2, M];
- 每个结点存放至少M/2-1(取上整)和至多M-1个关键字;(至少2个关键字)
- 非叶子结点的关键字个数=指向儿子的指针个数-1;
- 非叶子结点的关键字:K[1], K[2], …, K[M-1];且K[i] < K[i+1];
- 非叶子结点的指针:P[1], P[2], …, P[M];其中P[1]指向关键字小于K[1]的子树,P[M]指向关键字大于K[M-1]的子树,其它P[i]指向关键字属于(K[i-1], K[i])的子树;
- 所有叶子结点位于同一层;
B-树
B-树的特性:
- 关键字集合分布在整颗树中;
- 任何一个关键字出现且只出现在一个结点中;
- 搜索有可能在非叶子结点结束;
- 其搜索性能等价于在关键字全集内做一次二分查找;
- 自动层次控制;
B-树的搜索,从根结点开始,对结点内的关键字(有序)序列进行二分查找,如果命中则结束,否则进入查询关键字所属范围的儿子结点;重复,直到所对应的儿子指针为空,或已经是叶子结点;
B+树
B+树是B-树的变体,也是一种多路搜索树, 其定义基本与B-树同,其特点是:
- 非叶子结点的子树指针与关键字个数相同;
- 非叶子结点的子树指针P[i],指向关键字值属于[K[i], K[i+1])的子树(B-树是开区间);
- 为所有叶子结点增加一个链指针;
- 所有关键字都在叶子结点出现,搜索命中总在叶子节点;
B+的特性:
- 所有关键字都出现在叶子结点的链表中(稠密索引),且链表中的关键字恰好是有序的;
- 不可能在非叶子结点命中;
- 非叶子结点相当于是叶子结点的索引(稀疏索引),叶子结点相当于是存储(关键字)数据的数据层;
- 更适合文件索引系统;-增 删文件(节点)时,效率更高,因为B+树的叶子节点包含所有关键字,并以有序的链表结构存储,这样可很好提高增删效率。
B*树
B*树 是B+树的变体,在B+树的非根和非叶子结点再增加指向兄弟的指针;
- B*树定义了非叶子结点关键字个数至少为(2/3)*M,即块的最低使用率为2/3(代替B+树的1/2);
- B+树的分裂:当一个结点满时,分配一个新的结点,并将原结点中1/2的数据复制到新结点,最后在父结点中增加新结点的指针;
- B+树的分裂只影响原结点和父结点,而不会影响兄弟结点,所以它不需要指向兄弟的指针;
- B树的分裂:当一个结点满时,如果它的下一个兄弟结点未满,那么将一部分数据移到兄弟结点中,再在原结点插入关键字,最后修改父结点中兄弟结点的关键字(因为兄弟结点的关键字范围改变了);如果兄弟也满了,则在原结点与兄弟结点之间增加新结点,并各复制1/3的数据到新结点,最后在父结点增加新结点的指针;所以,B树分配新结点的概率比B+树要低,空间使用率更高;
参考博客:https://www.cnblogs.com/qlqwjy/p/7965491.html
后面的关于红黑树、B树的代码实现我暂写不出来,只能写一写基本的定义以及说明,敬请谅解,经过这些总结,虽然写不出代码来,但至少理解了一些思想,还是有点收获的。