1. 二叉树(Binary Tree)
(1)定义
每个节点最多只有两个子节点,没有子节点的节点称之为叶子节点,第一个节点为根节点。
(2)二叉树的性质
① 我们将节点用数组的形式进行保存,比如是tree, tree[0]就代表根节点。假设父节点是tree[n],左节点为tree[n*2+1], 右节点为tree[n*2+2]。
② 每一层的节点都是满的树我们称为满二叉树(Full Binary Tree),比如下面图a。除了最后一层不满之外,其它层都是满的的树称为完全二叉树(Complete Binary Tree),如图b。
③ 在一棵满二叉树中,我们假设根节点是第0层,则每一层的节点个数为2^n,第n层之前所有节点个数为(2^n) - 1。
(3)二叉树的各种基本操作
二叉树的创建,先序遍历,中序遍历,后序遍历
from typing import List
class TreeNode:
def __init__(self, val: int):
self.val = val
self.left = None
self.right = None
class Tree:
def __init__(self):
self.root = None
# 给你一个数组,去创建一颗二叉树
def create(self, nums: List[int]) -> None:
tree_nodes = []
for num in nums:
if num:
temp_node = TreeNode(num)
tree_nodes.append(temp_node)
else:
tree_nodes.append(None)
for i, node in enumerate(tree_nodes):
if node:
if i * 2 + 1 < len(nums):
node.left = tree_nodes[i * 2 + 1]
if i * 2 + 2 < len(nums):
node.right = tree_nodes[i * 2 + 2]
self.root = tree_nodes[0]
return self.root
# 树的先序遍历,也就是先访问父节点,再访问左节点,再访问右节点
def pre_order(self, node: TreeNode) -> None:
if not node:
return
print(node.val)
self.pre_order(node.left)
self.pre_order(node.right)
# 树的中序遍历,先访问左节点,再访问父节点,再访问右节点
def in_order(self, node: TreeNode) -> None:
if not node:
return
self.in_order(node.left)
print(node.val)
self.in_order(node.right)
# 树的后序遍历,先访问左节点,再访问右节点,再访问父节点
def post_order(self, node: TreeNode) -> None:
if not node:
return
self.post_order(node.left)
self.post_order(node.right)
print(node.val)
if __name__ == '__main__':
test = Tree()
root = test.create([5,1,4,None,None,3,6])
test.pre_order(root)
test.in_order(root)
test.post_order(root)
2. 二叉搜索树 (Binary Search Tree)
(1)定义
一颗二叉树满足如下性质,则就是一颗二叉搜索树
① 若左子树不为空,则所有的左子树的值都小于父节点的值
② 若右子树不为空,则所有的右子树的值都大于父节点的值。
③ 左右子树也都是二叉搜索树
④ 没有相同值的节点
(2)性质
① 对二叉搜索树进行中序遍历(先遍历左节点,再遍历父节点,再遍历右节点)则得到有序数列。
(3)二叉搜索树的各种操作
① 插入操作
若当前的树为空,则插入的元素为根节点。
若插入的节点小于根节点,则插入到左子树(递归)。
如果插入的节点大于等于根节点,则插入到右子树(递归)。
所有新插入的节点成为叶子节点。
# 插入一个新的节点
def insert(self, root, val):
if not root:
self.root = TreeNode(val)
return
if val == root.val:
return
if val < root.val:
if not root.left:
root.left = TreeNode(val)
else:
self.insert(root.left, val)
else:
if not root.right:
root.right = TreeNode(val)
else:
self.insert(root.right, val)
② 查询操作
# 查找一个节点是否在这课树中,如果不在则返回False,在的话则返回True
def search(self, root, val):
if not root:
return False
if val < root.val:
return self.search(root.left, val)
elif val > root.val:
return self.search(root.right, val)
else:
return True
③ 创建二叉搜索树
def create(self, nums):
for num in nums:
new_node = TreeNode(num)
self.insert(self.root, new_node)
return self.root
④ 中序遍历,可以得到从小到大的list
def in_order(self, node, rlt):
if not node:
return rlt
self.in_order(node.left, rlt)
rlt.append(node.val)
self.in_order(node.right, rlt)
⑤ 删除操作
如果要删除的节点是叶子节点,就直接删除,修改叶子节点的left或者right为None
如果要删除的节点a只有一个子节点b,a节点的父节点是c,那么将c的对应子节点指向b,同时删掉a节点
如果要删除的节点既有左节点又有右节点,那么选择该节点的右子树中最小的那个点的值赋值到该节点,并且删除最小值的那个节点。为什么我们要选择右子树中最小的那个点呢?因为父节点的右子树中的所有节点都要大于父节点的值,为了满足搜索二叉树的性质,所以我们选择了这个点。
如果不理解的话,可以参考这篇博客,里面有详细的图解。
def delete(self, parent, curr, val):
# 如果是一颗空树
if not curr:
return None
if val < curr.val:
self.delete(curr, curr.left, val)
elif val > curr.val:
self.delete(curr, curr.right, val)
else:
if not curr.left:
if parent.left == curr:
parent.left = curr.right
else:
parent.right = curr.right
del curr
elif not curr.right:
if parent.left == curr:
parent.left = curr.left
else:
parent.right = curr.left
del curr
else:
pre_node = curr.right
if not pre_node.left:
curr.val = pre_node.val
curr.right = pre_node.right
del pre_node
else:
temp_node = curr.right
while temp_node.left:
pre_node = temp_node
temp_node = temp_node.left
curr.val = temp_node.val
pre_node.left = None
del temp_node
(4)性能分析
二叉查找树的性能与树的深度有关,当树的分布均匀的时候,也就是接近一颗完全二叉树的时候,不伦查找,插入,还是删除效率都是O(long n),最坏的情况就是O(n),如果当树只有左子树的时候,就需要从根节点一直遍历到叶子节点,才能找到要操作的元素,时间复杂度为O(n)。
(5)最后的整体代码:
class TreeNode:
def __init__(self, val: int):
self.val = val
self.left = None
self.right = None
class BST:
def __init__(self):
self.root = None
# 查找一个节点是否在这课树中,如果不在则返回False,在的话则返回True
def search(self, root, val):
if not root:
return False
if val < root.val:
return self.search(root.left, val)
elif val > root.val:
return self.search(root.right, val)
else:
return True
# 插入一个新的节点
def insert(self, root, val):
if not root:
self.root = TreeNode(val)
return
if val == root.val:
return
if val < root.val:
if not root.left:
root.left = TreeNode(val)
else:
self.insert(root.left, val)
else:
if not root.right:
root.right = TreeNode(val)
else:
self.insert(root.right, val)
def create(self, nums):
for num in nums:
self.insert(self.root, num)
return self.root
def in_order(self, node, rlt):
if not node:
return rlt
self.in_order(node.left, rlt)
rlt.append(node.val)
self.in_order(node.right, rlt)
def delete(self, parent, curr, val):
# 如果是一颗空树
if not curr:
return None
if val < curr.val:
self.delete(curr, curr.left, val)
elif val > curr.val:
self.delete(curr, curr.right, val)
else:
if not curr.left:
if parent.left == curr:
parent.left = curr.right
else:
parent.right = curr.right
del curr
elif not curr.right:
if parent.left == curr:
parent.left = curr.left
else:
parent.right = curr.left
del curr
else:
pre_node = curr.right
if not pre_node.left:
curr.val = pre_node.val
curr.right = pre_node.right
del pre_node
else:
temp_node = curr.right
while temp_node.left:
pre_node = temp_node
temp_node = temp_node.left
curr.val = temp_node.val
pre_node.left = None
del temp_node
if __name__ == '__main__':
test = BST()
root = test.create([5,3,2,1,4])
test.delete(root, root, 3)
rlt = []
test.in_order(root, rlt)
print(rlt)
(6)有关二叉搜索树的题目
[LeeCode] 98. Validate Binary Search Tree 题目对应博客
3. 平衡二叉树
上面我们提到了,二叉搜索树在接近一颗完全二叉树的时候,查找,删除,插入的最好效率都是O(log n),但是当插入的序列是有序的时候,比如插入的顺序是5,4,3,2,1,那么创建好的二叉树就成了只有左节点的二叉树,上述的操作效率全部全成了O(n)。同时还存在一个问题,就是每次我们删除节点的时候(当删除的节点左右子树都有的时候),总是会选择右子树中值最小的节点,这样会造成了树的不平衡,树的平衡性都到了破坏,效率也会变低,于是就有了下面的平衡二叉树,也叫AVL树。
AVL树定义:AVL树是最先发明的自平衡二叉查找树。AVL树得名于它的发明者 G.M. Adelson-Velsky 和 E.M. Landis,他们在 1962 年的论文 "An algorithm for the organization of information" 中发表了它。在AVL中任何节点的两个儿子子树的高度最大差别为1。这样,查找,添加和删除平均和最坏时间复杂度都控制在了O(log N)。
在上面我们实现了二叉搜索树,那么如何将创建的树编程平衡二叉树呢,也就是如何将树的高度降低?
方法是通过旋转,那么什么是旋转,下面通过图给出解释:
这里为什么叫下面的名字,我是这样理解的,第一个新插入的节点是2,是root节点的左节点的左节点,剩下的同理,右右情况,是新插入的节点为7,就是root节点的右节点的右节点,左右情况新插入的几点是4,就是root节点的左节点的右节点,右左情况新插入的几点是4,是root节点的右节点的左节点。
这里为什么是插入节点呢,因为是原来的树是平衡的,但是当插入新节点后,树才不平衡,我们这时候就需要旋转树了。
这是维基百科上面给的一幅图,将需要旋转的情况分为了上面的四种,这里我们将空节点的高度定义为-1,叶子节点为0。当一个节点的左子树与右子树(左右子树可以为空树)高度只差为2的时候,树就需要旋转。
四种旋转情况
(1)左左情况,节点5的高度为2,节点3的高度为1,节点2的高度为0,root节点的左子树高度为1,右子树高度为-1(因为是一颗空树),左右子树的高度差就变成了2,此时这棵树不满足平衡二叉树的条件,我们需要对这颗树进行旋转,将它变成一颗平衡二叉树。如何旋转呢? 我们以中间的节点3为轴,把节点5往下拉,就变成了表格下面对应的这种情况,这里需要注意图上面的这些不同颜色的小直角三角形,他们代表该节点的子节点。我们旋转后这些小直角三角形的位置发生了变化,B变成了节点5的左节点,同时节点3的右节点变成了节点5,这样就是旋转的过程。这里为什么可以这样赋值呢,即便这样旋转以后,依然不影响树的平衡,是因为3<B<5,同时B还小于A,所以这样旋转不会破坏平衡。那么具体代码是怎么实现的呢,下面先给出这个旋转函数的实现,我最后会给出整体的代码,这里一开始函数的某些语句看不懂不要紧,这里主要是想通过代码帮助大家理解旋转过程。
# 在node的左孩子top的左子树添加节点,左旋转
def rotate_LL(self, node):
# top 旋转过后树的定点
top = node.left
# 断开当前节点的左孩子
node.left = top.right
# 将当前节点作为top的右孩子
top.right = node
# 更新top 和 当前节点的高度,因为这两个点的相对位置发生了变化,叶子节点或者空节点长度为0
node.height = max(self.Hight(node.left), self.Hight(node.right)) + 1
top.height = max(self.Hight(top.left), self.Hight(top.right)) + 1
# 返回旋转过后,这课局部的树的根节点
return top
代码参数的node节点就代表了节点5,这个函数传递进去的node是不平衡的这部分树的根节点。
(2)右右情况其实跟左左情况对称的,这里详细解释了,这里给上代码,可以结合代码看
# 在node节点的右孩子top的右子树添加新的节点,右旋转
def rotate_RR(self, node):
top = node.right
node.right = top.left
top.left = node
node.height = max(self.Hight(node.left), self.Hight(node.right)) + 1
top.height = max(self.Hight(top.left), self.Hight(top.right)) + 1
return top
(3)左右情况
左右情况想要达到平衡的话就需要旋转2次,第一次旋转,变成了左左的情况,然后第二次旋转就可以使用左左情况的函数去旋转了。我们知道怎么将一个左左情况的树旋转,那么第一次旋转是怎么转的呢,我们怎么才能转成左左情况,这里在代码实现的时候,其实很巧妙。我们只是调用了右右旋转的那个函数就将树旋转成了左左情况,这么一说,其实有点懵,我们在右右旋转的时候,参数传进去的node是root,那么在这里我们传进去的就不是root了,而是root的left,也就是节点3。这里为什么会旋转成功,可以自己去操作一下试试。这里附上代码,根据代码去看效果会更好。看代码是不是被惊到了,居然就只有两行,这里注意右右旋转后的返回值一定要赋值给node.left,这样我们的树才会是一个整体的树,如果没有这个赋值操作的话,我们的树经过旋转就会别拆分成了两部分,不信的话可以自己去通过旋转去画一下图。
# 在node节点的左孩子的右子树添加新节点
def rotate_LR(self, node):
node.left = self.rotate_RR(node.left)
return self.rotate_LL(node)
(4)右左情况是左右情况的对称,理解了左右,右左情况自然就好懂了,这里不详细解释了。
# 在node节点的右孩子的左子树添加新的节点
def rotate_RL(self, node):
node.right = self.rotate_LL(node.right)
return self.rotate_RR(node)
可能会有疑问的问题:
(1)我们知道了如何旋转,那么我们怎么判断出现了不平衡的情况呢?
当一个节点的左右子树高度只差为2,则出现了不平衡。
(2)我们判断出来了出现了不平衡,那么到底是上面四种情况中的哪一种呢?
我们什么时候会需要旋转,插入数据的时候,删除数据的时候可能会出现需要旋转的情况。 假设当前的节点为node,新插入的数据值为val。
① 在插入节点的过程中,如果我们在node节点判断出现了不平衡,
当插入的节点是在node的左子树:
val < node.left.val,就是左左情况,如果val > node.left.val 就是左右情况
if self.Hight(node.left) - self.Hight(node.right) == 2:
if val < node.left.val:
node = self.rotate_LL(node)
else:
node = self.rotate_LR(node)
当插入的节点是在node的右子树:
val < node.right.val,就是右左情况,如果val > node.right.val,就是右右情况
if self.Hight(node.right) - self.Hight(node.left) == 2:
if val < node.right.val:
node = self.rotate_RL(node)
else:
node = self.rotate_RR(node)
② 在删除节点后,node节点判断出现了不平衡
如果要删除的节点在node的左子树里面:
if self.Hight(node.right) - self.Hight(node.left) == 2:
if self.Hight(node.right.right) >= self.Hight(node.right.left):
node = self.rotate_RR(node)
else:
node = self.rotate_RL(node)
如果要删除的节点在node的右子树里:
if self.Hight(node.left) - self.Hight(node.right) == 2:
if self.Hight(node.left.left) >= self.Hight(node.left.right):
node = self.rotate_LL(node)
else:
node = self.rotate_LR(node)
(3)你可能会有疑问,如果出现了下面这样的情况怎么办?a是不是既包含了左左情况,又包含了左右情况,b既包含了右右情况,又包含了右左情况。
首先,这种情况不会出现在插入里面,因为没插入一个数我们都会判断是不是不平衡,不平衡的话,我们回去调整。这种情况是可能出现在删除数据的过程中的,当我们删除一个数据的时候,可能会出现上面这种情况比如:
我们原来的树是平衡的,但是删除一个节点后,出现了不平衡,并且变成了上面这种情况。
当出现这种情况的时候,比如a能形成左左,我们就判断为左左,不能就是左右。同样的右边这种,如果可以形成右右,就判断为右右,否则就是右左。具体代码实现,可以参考上面问题(2)里面删除的代码过程。
对了,查看函数跟二叉搜索树的过程一样,没有特殊的地方。
整体代码:
from typing import List
import collections
class TreeNode:
def __init__(self, val: int):
self.val = val
self.height = 0
# 一棵树中可能会插入相同val的点,cnt代表相同val的点有都多少个
self.cnt = 0
self.left = None
self.right = None
class AVLTree:
def __init__(self):
self.root = None
def Hight(self, node):
if node:
return node.height
return -1
# 在node的左孩子top的左子树添加节点,左旋转
def rotate_LL(self, node):
# top 旋转过后树的定点
top = node.left
# 断开当前节点的左孩子
node.left = top.right
# 将当前节点作为top的右孩子
top.right = node
# 更新top 和 当前节点的高度,因为这两个点的相对位置发生了变化,叶子节点或者空节点长度为0
node.height = max(self.Hight(node.left), self.Hight(node.right)) + 1
top.height = max(self.Hight(top.left), self.Hight(top.right)) + 1
# 返回旋转过后,这课局部的树的根节点
return top
# 在node节点的右孩子top的右子树添加新的节点,右旋转
def rotate_RR(self, node):
top = node.right
node.right = top.left
top.left = node
node.height = max(self.Hight(node.left), self.Hight(node.right)) + 1
top.height = max(self.Hight(top.left), self.Hight(top.right)) + 1
return top
# 在node节点的左孩子的右子树添加新节点
def rotate_LR(self, node):
node.left = self.rotate_RR(node.left)
return self.rotate_LL(node)
# 在node节点的右孩子的左子树添加新的节点
def rotate_RL(self, node):
node.right = self.rotate_LL(node.right)
return self.rotate_RR(node)
# 查找一个节点是否在这课树中,如果不在则返回False,在的话则返回True
def search(self, root, val):
if not root:
return False
if val < root.val:
return self.search(root.left, val)
elif val > root.val:
return self.search(root.right, val)
else:
return True
# 良好的用户接口,只需要提供val值
def insert(self, val):
if not self.root:
self.root = TreeNode(val)
else:
self._insert(self.root, val)
# 插入一个新的节点
def _insert(self, node, val):
#这里空树的情况不考虑,因为空树的情况已经在insert(val)函数里面解决了
# 如果插入的新值存在树中,那么直接对应的节点的cnt加1,这是代表这棵树里面有cnt+1个这个值的节点
if val == node.val:
node.cnt += 1
return node
if val < node.val:
if not node.left:
node.left = TreeNode(val)
else:
# 这里是个关键点
node.left = self._insert(node.left, val)
# 说明这棵树需要旋转, 这里是left-right的高度,是因为,插入的到的是左子树,所以左子树的高度会大于等于右子树
# 当树发生旋转后,root节点的高度,在旋转函数里面已经调整过来
if self.Hight(node.left) - self.Hight(node.right) == 2:
if val < node.left.val:
node = self.rotate_LL(node)
else:
node = self.rotate_LR(node)
else:
if not node.right:
node.right = TreeNode(val)
else:
node.right = self._insert(node.right, val)
# 这里是right-left的高度,因为插入到右子树中,所以右子树的高度会大于等于左子树高度
if self.Hight(node.right) - self.Hight(node.left) == 2:
if val < node.right.val:
node = self.rotate_RL(node)
else:
node = self.rotate_RR(node)
# 更新节点的高度,从叶子节点向根节点回溯更新
# 这里之所以需要调整root的高度,是因为如果插入root节点后没有发生旋转,root节点的高度是不会更新的
# 所以这里需要更新一下root节点的高度,插入当前节点后发生了旋转,这里的高度更新不会发生变化
node.height = max(self.Hight(node.left), self.Hight(node.right)) + 1
return node
# 创建一颗平衡二叉树
def create(self, nums):
for num in nums:
self.insert(num)
return self.root
# 得到一个有序的list
def get_list(self):
rlt = []
self.in_order(self.root, [])
return rlt
def in_order(self, node, rlt):
if not node:
return rlt
self.in_order(node.left, rlt)
rlt.append(node.val)
self.in_order(node.right, rlt)
# 按层将节点打印出来
def layer_order(self):
self._layer_order(self.root)
def _layer_order(self, node):
if not node:
return
rlt = collections.deque()
rlt.append(node)
while rlt:
length = len(rlt)
for i in range(length):
curr = rlt.popleft()
if curr:
print(curr.val, end=", ")
rlt.append(curr.left)
rlt.append(curr.right)
print('\n')
# 删除节点
def delete(self, val):
if self.root is None:
raise KeyError('Error, the tree is empty.')
else:
# _delete(node) 函数的返回值是当前的node, 返回值的作用是将节点串连起来
self.root = self._delete(self.root, val)
def _delete(self, node, val):
if node is None:
raise KeyError('Error, the value is not in the tree.')
if val < node.val:
node.left = self._delete(node.left, val)
# 这里为什么是right的高度减去left的高度,是因为删除的节点在左子树里,右子树的高度肯定会高于左子树
if self.Hight(node.right) - self.Hight(node.left) == 2:
if self.Hight(node.right.right) >= self.Hight(node.right.left):
node = self.rotate_RR(node)
else:
node = self.rotate_RL(node)
node.height = max(self.Hight(node.left), self.Hight(node.right)) + 1
elif val > node.val:
node.right = self._delete(node.right, val)
# 这里为什么是left的高度减去right的高度,是因为删除的节点在右子树里,左子树的高度肯定会高于右子树
if self.Hight(node.left) - self.Hight(node.right) == 2:
if self.Hight(node.left.left) >= self.Hight(node.left.right):
node = self.rotate_LL(node)
else:
node = self.rotate_LR(node)
node.height = max(self.Hight(node.left), self.Hight(node.right)) + 1
else: # 此时找到了要删除的节点
# 当要删除的节点有两个子节点的时候
if node.left and node.left:
# 找值最小的那个节点
temp_node = node.right
while temp_node.left:
temp_node = temp_node.left
# 将最小节点的信息复制到当前要删除的节点
node.val = temp_node.val
node.cnt = temp_node.cnt
# 再调用删除函数,删除最小的节点,当node.right没有left的时候,最小的节点就是node.right
# node.right有left节点的时候,就一直往下找left节点,一直找到一个left叶子节点
node.right = self._delete(node.right, node.val)
node.height = max(self.Hight(node.left), self.Hight(node.right)) + 1
else:
if node.left:
temp = node
node = node.left
del temp
else:
temp = node
node = node.right
del temp
return node
if __name__ == '__main__':
test = AVLTree()
test.create([10,7,12,6,8])
test.layer_order()
print('=======')
test.delete(12)
test.layer_order()
4. 红黑树
为什么要使用红黑树?
既然已经存在了AVL树,并且它的查找,删除和插入的效率都是O(log n),为什么我们还需要红黑树?AVL树的平衡要求树的左右子树高度差不超过1,但这需要大量的旋转操作来满足树的平衡,我们需要从树的底部向上调整,调整的级别为O(log n),这会花费很多时间。AVL树比红黑树更平衡,这也就意味着这棵树在插入和删除节点后,更容易引起树的不平衡,因此AVL树需要更多次数的调整。而红黑树在满足查找,删除和插入在最好最坏情况下都是O(log n)级别的情况下,插入最多调整2次,删除最多调整3次的情况满足树的平衡,调整是O(1)级别的。
红黑树的缺点在哪?
红黑树之所以不需要调整那么多次,是因为他没有AVL树平衡,也就是说AVL树的平衡度更高,这也就决定了AVL树的查询效率比红黑树高,虽然这两种结构都是O(log n)的查找速度,但是这个n是不一样的,比如O(log 10)跟O(log 1000)是不一样的。所以当在插入和删除更多的场景下,我们使用红黑树,在查询远远多于插入和删除的场景下,我们使用AVL树。
红黑树的推荐博客
下面这些博客详解讲解了红黑树,我下面的总结是在看后下面的博客后写的,里面写了我在学习的时候遇到的一些困惑,可能会解决你在学习红黑树的时候一些困惑,因为可能会大家会遇到相同的疑惑点。
算法 - 红黑树详细分析,看了都说好 - 个人文章 - SegmentFault 思否
另外推荐一个数据结构可视化的网站,里面也有红黑树生成过程的动画展示
Data Structure Visualization (主页)
Red/Black Tree Visualization (红黑树)
红黑树插入操作总结:
红黑树新插入的节点一开始颜色都是红色(后面可能会调整颜色),切该节点在创建的时候,左右子树都指向了一个NIL的叶子节点,这个叶子节点代替了我们平常使用的空节点,这个叶子节点的颜色为黑色。
情况1:如果这棵树是一颗空树,那么新插入的节点成为根节点,并且将颜色置为黑色(调整完毕)
情况2:如果插入节点的父节点是黑色的,直接插入不需要调整操作(调整完毕)
如果插入节点的父节点是红色的,这里又分为3中情况:
情况3:如果插入节点的叔叔节点为红色,这里不管新插入的节点N是P的左节点,还是右节点,都进行如下操作。将父节点P和叔叔节点U全部置为黑色,爷爷节点G置为红色。(此时G节点有可能是根节点,如果是根节点,需要将G置为黑色,因为红黑树需要根节点必须为黑色,另外因为G节点的父节点有可能也是红色,这违反了红黑树的性质,需要进一步调整,所以当我们在这一步调整完以后,需要接着往上继续递归调整,此时进入到情况1)
情况4:如果插入的节点N是P的右节点,且叔叔节点为黑色,进行如下操作,左旋P节点(调整完以后进入情况5,如果不满足情况4,则必定满足情况5,此时直接进入情况5)
情况5:如果插入的节点N是P的左节点,且叔叔节点为黑色,进行如下操作,将P染成黑色,G染成红色,右旋G节点 (调整完毕)
这里的情况1~3,我们能够看到插入之前原来的树都是平衡的,看情况4和情况5你可能会感觉,为什么原来的树不平衡,这是我刚开始学红黑树的时候疑惑的地方,就拿情况4来说G的左子树在插入N之前,左边没哟黑色节点,而右边有一个黑色节点U,这不是本来就违反了红黑树的性质吗? 其实这里的这张图代表的情况4是其他节点经过调整后造成了这种情况,也就是说这里的N并不是新插入的节点,因为我们在情况3的时候说了,调整完以后需要继续向上调整。另外,这里需要明确的一点,那个小小的黑色节点(带三角形的那个)是叶子节点,叶子节点也算节点,同时叶子叶节点是黑色,那么如果N是新插入的节点,同时满足上述的情况4,会是怎样的呢?会变成下面这样,这个U成为了叶子节点。这样在去掉N以后,原来的树是不是也是平衡的。
同样针对情况5,上面的图也是通过其他节点调整后得到的,如果N同样是插入节点,那么图就变成了下面的这个样子。当N是插入节点的时候,U是叶子节点。
红黑树删除节点总结
删除的节点可能会有两个不是叶子节点的子节点或者一个或者没有,我们为了简化操作,将有两个非叶子节点子节点的删除转化成删除节点的叶子节点只有一个的情况。具体怎么转化,上面两个博客中都有讲。
在做完下面的所有情况的时候,我们最后一步都要将根节点置为黑色,下面就不提这一点了。
(1)如果删除的节点是红色,直接用后继节点去替代删除节点的位置,不需要调整,因为删除的节点是红色的话,并不会影响红黑树的黑高。
(2)当删除节点为黑色的时候,分为6种情况,提前做一些假设:
情况1:如果N是红的,直接将N置为黑色
情况2~6:
情况2,3是并列情况,也就是说只能有其中一种情况发生,情况2结束后去到情况四,如果满足情况4,调整结束,如果不满足去到情况5,然后情况6。情况3结束后返回到情况1,
下面是上面两个博客中下面评论中的一个,感觉总结的很好
python代码实现:
import collections
class RBNode:
def __init__(self, val, color="R"):
self.val = val
self.color = color
self.cnt = 0
self.left = None
self.right = None
self.parent = None
class RBTree:
def __init__(self):
self.NIL = RBNode(-1, "B")
self.root = self.NIL
self.size = 0
def create(self, nums):
for num in nums:
self.insert(num)
# 按层将节点打印出来
def layer_order(self):
self._layer_order(self.root)
def _layer_order(self, node):
if not node:
return
rlt = collections.deque()
rlt.append(node)
while rlt:
length = len(rlt)
for i in range(length):
curr = rlt.popleft()
if curr != self.NIL:
print(curr.val, curr.color, end=", ")
rlt.append(curr.left)
rlt.append(curr.right)
print('\n')
# 实际上就是RR
def left_rotate(self, node):
parent = node.parent
right = node.right
node.right = right.left
if node.right:
node.right.parent = node
right.left = node
node.parent = right
right.parent = parent
if parent == self.NIL:
self.root = right
else:
if parent.left ==node:
parent.left = right
else:
parent.right = right
# 实际上就是LL
def right_rotate(self, node):
parent = node.parent
left = node.left
node.left = left.right
if node.left != self.NIL:
node.left.parent = node
node.parent = left
left.right = node
left.parent = parent
if parent == self.NIL:
self.root = left
else:
if parent.left == node:
parent.left = left
else:
parent.right = left
def insert(self, val):
new_node = RBNode(val)
new_node.parent = self.NIL
new_node.left = self.NIL
new_node.right = self.NIL
self._insert(new_node)
def _insert(self, node):
temp_root = self.root
# 这个节点用来存放父节点
temp_node = self.NIL
while temp_root != self.NIL:
temp_node = temp_root
if node.val == temp_node.val:
temp_node.cnt += 1
return
elif node.val < temp_node.val:
temp_root = temp_node.left
else:
temp_root = temp_node.right
if temp_node == self.NIL:
self.root = node
node.color = "B"
elif node.val < temp_node.val:
temp_node.left = node
node.parent = temp_node
else:
temp_node.right = node
node.parent = temp_node
self.insert_fix_up(node)
def insert_fix_up(self, node):
if node.val == self.root.val:
return
# 因为我们每次插入的节点都是红色的,并且如果新插入节点的父节点是黑色节点的话,我们就直接插入,不需要任何调整操作
# 当node节点没有父节点的时候,说明我们调整到了根节点,此时只需要将根节点颜色置黑,确保根节点是黑色的就可以了
while node.parent and node.parent.color == "R":
# case 3 or case 5
if node.parent == node.parent.parent.left:
node_uncle = node.parent.parent.right
# 1. 没数叔叔节点,若此节点为父节点右节点,则先左旋再右旋,否则直接右旋
# 2. 有叔叔节点,叔叔节点颜色为黑色
# 3. 有叔叔节点,叔叔节点颜色为红色,父节点和叔叔节点颜色都设为黑,祖父节点颜色设置为红色
# 叔叔节点为空和叔叔节点为黑色可以放到一块讨论,处理情况一样
# 插入总结下来分为两大情况,(1)父节点为黑色的时候,直接插入不需要处理(2)父节点为红色,叔叔节点是否为红色是一种情况,
# 叔叔节点为空或者叔叔节点是黑色的是另外一种情况。
if node_uncle and node_uncle.color == "R":
# case 3, 这种情况调整完以后,因为
node.parent.color = "B"
node_uncle.color = "B"
node.parent.parent.color = "R"
# 更新node节点为这棵树的根节点,继续向上递归调整
node = node.parent.parent
# 这里的根节点被染成了红色,可能跟根节点的父节点(如果存在的话)都是红色,发生冲突,所以需要继续向上调整
continue
elif node == node.parent.right:
# case 4, 因为当出现情况四后,调整过后直接进入情况五
self.left_rotate(node.parent)
node = node.left
# case 5
node.parent.color = "B"
node.parent.parent.color = "R"
self.right_rotate(node.parent.parent)
# 这里为什么直接整个函数就结束了?是因为出现情况5,并且调整过后,根节点依然是黑色的,并且黑色节点的数量没有增加,所以结束
# 根节点的兄弟节点也不需要调整了
return
# 对称情况
elif node.parent == node.parent.parent.right:
node_uncle = node.parent.parent.left
# case 3
if node_uncle and node_uncle.color == "R":
node.parent.color = "B"
node_uncle.color = "B"
node.parent.parent.color = "R"
node = node.parent.parent
continue
# case 4
elif node == node.parent.left:
self.right_rotate(node.parent)
node = node.right
node.parent.color = "B"
node.parent.parent.color = "R"
self.left_rotate(node.parent.parent)
return
self.root.color = "B"
def transplant(self, node_u, node_v):
# 将node_u的父节点连接到node_v,并且删除node_u
if node_u.parent == self.NIL:
self.root = node_v
elif node_u == node_u.parent.left:
node_u.parent.left = node_v
elif node_u == node_u.parent.right:
node_u.parent.right = node_v
node_v.parent = node_u.parent
def find_min(self, node):
# 返回以node节点为根节点的树的最小节点
temp_node = node
while temp_node.left != self.NIL:
temp_node = temp_node.left
return temp_node
def find_max(self, node):
temp_node = node
while temp_node.right != self.NIL:
temp_node = temp_node.right
return temp_node
def delete(self, val):
if self.root == self.NIL:
print("There is a blank tree!")
return
self._delete(self.root, val)
def _delete(self, node, val):
if node == self.NIL:
print("There is no such node!")
return
if val < node.val:
self._delete(node.left, val)
elif val > node.val:
self._delete(node.right, val)
else:
node_color = node.color
# 左节点为空,这意味着找到要删除的节点只有右节点或者没有节点
if node.left == self.NIL:
# temp_node是删除节点的子节点
temp_node = node.right
self.transplant(node, temp_node)
elif node.right == self.NIL:
temp_node = node.left
self.transplant(node, temp_node)
else:
# node节点有两个子节点
min_node = self.find_min(node.right)
node_color = min_node.color
node.val = min_node.val
node.cnt = min_node.cnt
temp_node = min_node.right
#self._delete(min_node, val)
self.transplant(min_node, min_node.right)
if node_color == "B":
self.delete_fix_up(temp_node)
def delete_fix_up(self, node):
while node != self.root and node.color == "B":
if node == node.parent.left:
node_brother = node.parent.right
if node_brother.color == "R":
# case 2
node_brother.color = "B"
node.parent.color = "R"
self.left_rotate(node.parent)
node_brother = node.parent.right
if node_brother.left.color == "B" and node_brother.right.color == "B":
# case 3
node_brother.color = "R"
node = node.parent
if node.parent.color == "R":
# case 4
node.parent.color = "B"
return
else:
if node_brother.right.color == "B":
# case 5
node_brother.color = "R"
node_brother.left.color = "B"
self.right_rotate(node_brother)
node_brother = node.parent.right
# case 6
node_brother.color = node.parent.color
node.parent.color = "B"
node_brother.right.color = "B"
self.left_rotate(node.parent)
break
else:
# 对称
node_brother = node.parent.left
if node_brother.color == "R":
node.parent.color = "R"
node_brother.color = "B"
self.right_rotate(node.parent)
node_brother = node.parent.left
if node_brother.left.color == "B" and node_brother.right.color == "B":
node_brother.color = "R"
node = node.parent
if node.parent.color == "R":
node.parent.color = "B"
return
else:
if node_brother.left.color == "B":
node_brother.right.color = "B"
node_brother.color = "R"
self.left_rotate(node_brother)
node_brother = node.parent.left
node_brother.color = node.parent.color
node.parent.color = "B"
node_brother.left.color = "B"
self.right_rotate(node.parent)
break
# 如果删除的节点是黑色的,并且删除节点的子节点是红色的,case 1
node.color = "B"
if __name__ == "__main__":
test = RBTree()
#nums = [5, 14, 16, 3, 18, 2, 9, 15, 6, 17, 10, 19, 4, 1, 12, 8, 7, 11, 13]
nums = [5, 14, 16, 3, 18, 2, 9, 15, 6, 17, 10, 19, 4, 1, 12, 8, 7, 11, 13, 20]
test.create(nums)
test.delete(5)
test.layer_order()
5. B树(B-树)和B+树
上面的平衡二叉树和红黑树都是在内存中查找,但是如果数据量很大,我们不可能一次性的将所有数据加载到内存中,这时候就需要考虑如何在磁盘中快速查找数据,,这就用到了B树和B+树,他们的适用场景是查找磁盘中的大量数据。
B+树相对于B树来说,通常会更矮一些,这是由于B+树的特性所决定的。以下是一些原因:
-
数据存储在叶子节点:在B+树中,所有的数据都存储在叶子节点上,而内部节点只存储键值和子节点的引用。这意味着内部节点可以容纳更多的键值对,而不需要额外的空间来存储数据。相比之下,B树的内部节点需要存储数据,因此内部节点的大小会比较大。
-
更多的子节点:B+树的内部节点通常会有更多的子节点,这意味着在相同高度的情况下,B+树可以覆盖更多的数据范围。因为内部节点存储的是键值和子节点的引用,而不是数据本身,所以可以容纳更多的键值对。
-
叶子节点的链表连接:B+树的叶子节点通过指针连接成一个有序链表,这使得范围查询和顺序访问更加高效。通过遍历叶子节点的链表,可以轻松地获取一个范围内的数据。而B树的叶子节点之间没有这种链表连接。
由于上述特性,B+树相对于B树来说,可以存储更多的数据和键值对,而且在相同高度的情况下,可以覆盖更广的数据范围。这使得B+树的高度相对较低,从而提高了查询效率和索引的性能。
需要注意的是,B+树和B树在不同的应用场景下可能有不同的优势。B树适用于需要随机访问的场景,而B+树适用于范围查询和顺序访问较多的场景。选择使用哪种树结构取决于具体的需求和数据访问模式。
推荐博客:重温数据结构:理解 B 树、B+ 树特点及使用场景 - 掘金
6. Trie 树 (字典树)
根节点不包含字符
从根节点到某一节点路径上的值连起来就是对应的字符串
所有的节点存放的字符都不同
python 代码实现
class TrieNode:
def __init__(self):
self.children = {}
self.is_word = False
class Trie:
def __init__(self):
"""
Initialize your data structure here.
"""
self.root = TrieNode()
def insert(self, word: str) -> None:
"""
Inserts a word into the trie.
"""
node = self.root
for letter in word:
if not node.children.get(letter):
node.children[letter] = TrieNode()
node = node.children[letter]
node.is_word = True
def search(self, word: str) -> bool:
"""
Returns if the word is in the trie.
"""
node = self.root
for letter in word:
if node.children.get(letter):
node = node.children[letter]
else:
return False
if node.is_word:
return True
return False
def startsWith(self, prefix: str) -> bool:
"""
Returns if there is any word in the trie that starts with the given prefix.
"""
node = self.root
for letter in prefix:
if node.children.get(letter):
node = node.children[letter]
else:
return False
return True
if __name__ == '__main__':
trie = Trie()
trie.insert("apple")
print(trie.search("apple"))
print(trie.search("app"))
print(trie.startsWith("app"))
trie.insert("app")
print(trie.search("app"))