静态查找:数据集合稳定,不需要添加删除元素的查找操作。
动态查找:查找过程中需要同时添加或删除元素。
查找结构
静态:线性表
动态:二叉排序树、哈希
1. 顺序查找:遍历待查数组,逐个与关键字比较,匹配则查找成功。O(n)
# 设置一个哨兵
def SeqSearch(Array, key):
if Array is None or len(Array) == 0:
return None
i = 0
Array.append(key)
while Array[i] != key:
i += 1
return None if i == len(Array) else i
2. 二分查找
数组必须要有序;数组存在明确的上下界;能够通过索引访问(所以链表就很不适合二分查找)。
# 最普通的情况,规定有序数组不重复
class Solution:
def search(self, nums: List[int], target: int) -> int:
if nums == None or len(nums) == 0:
return -1
low = 0
high = len(nums) - 1
while low <= high: # 双端闭区间[low, high]查找
mid = (low + high) // 2
if nums[mid] == target:
return mid
elif nums[mid] > target:
high = mid - 1
elif nums[mid] < target:
low = mid + 1
return -1
# 寻找左侧边界的二分搜索。初始化 right = nums.length,决定了「搜索区间」是 [left, right),所以决定了 while (left < right),同时也决定了 left = mid + 1 和 right = mid
# 因为需找到 target 的最左侧索引,所以当 nums[mid] == target 时不要立即返回,而要收紧右侧边界以锁定左侧边界。
def search(nums, target):
if nums == None or len(nums) == 0:
return -1
low = 0
high = len(nums)
while low < high: # [low, high) 上搜索
mid = (low + high) // 2
if nums[mid] == target:
high = mid # 找到target之后不要立即返回,缩小搜索区间上界,在[low, mid)中继续搜索,锁定左侧边界low
elif nums[mid] > target:
high = mid
elif nums[mid] < target:
low = mid + 1
if low == len(nums): # target 比所有数都大
return -1
return low if nums[low] == target else -1 # 如果找到,low应该指向左侧边界
# 寻找右侧边界的二分搜索
def search(nums, target):
if nums == None or len(nums) == 0:
return -1
low = 0
high = len(nums)
while low < high: # [low, high) 上搜索
mid = (low + high) // 2
if nums[mid] == target:
low = mid + 1 # 找到target之后不要立即返回,缩小搜索区间下界,在[mid+1, high)中继续搜索,锁定右侧边界high-1
elif nums[mid] > target:
high = mid
elif nums[mid] < target:
low = mid + 1
if low-1 == len(nums): # target 比所有数都大
return -1
return low-1 if nums[low-1] == target else -1 # 若找到,最后low == high,右侧边界在 high-1
# 递归实现二分搜索
class Solution:
def search(self, nums: List[int], target: int) -> int:
if nums == None or len(nums) == 0:
return -1
return self.recursiveSearch(nums, 0, len(nums)-1, target)
def recursiveSearch(self, nums, low, high, target):
if low > high: # 双端闭区间搜索
return -1
mid = (low+high)//2
if nums[mid] == target:
return mid
elif nums[mid] > target:
return self.recursiveSearch(nums, low, mid-1, target)
elif nums[mid] < target:
return self.recursiveSearch(nums, mid+1, high, target)
return -1
3. 插值查找(按比例查找)
数据变化均匀的情况下效率比二分要高。和二分相比唯一的不同在于mid = low + (key - a[low])/(a[high]-a[low])*(high-low)
4. 斐波那契查找(黄金比例查找)
根据斐波那契数列F[k]来放置mid。 F[k] = 1, 1, 2, 3, 5, 8, 13, ...
待查数组元素个数为F[k] - 1
mid = low + F[k-1] -1
def FibonacciSearch(data, key):
F = [0,1]
count = 1;
length = len(data)
low = 0
high = length - 1 # 双端闭区间查找
if(key < data[low] or key > data[high]):
return -1
while F[count] < length: # 生成斐波那契数列
F.append(F[count-1] + F[count])
count = count + 1
low = F[0]
high = F[count] # F[count] 大于或等于length
while len(data)-1 < F[count-1]: # 如果F[count-1]大于数组最大下标,第一次mid=0+F[count-1]就会越界,要将数据个数补全
data.append(data[-1])
while(low <= high): # 双端闭区间
mid = low + F[count-1] # 计算当前分割下标
if(data[mid] > key): # 若查找记录小于当前分割记录
high = mid-1
count = count-1 # 左边数组长度为F[count-1]
elif(data[mid] < key): # 若查找记录大于当前分割记录
low = mid+1
count = count-2 # 右边数组长度为F[count-2]
else: # 若查找记录等于当前分割记录
return mid
return -1
data = [0,1,16,24,35,48,59,62,73,88,99]
key = 35
idx = FibonacciSearch(data, key)
print(data)
print('index for given key:', idx)
5. 线性索引
稠密索引:关键码对目标数据进行一定的提取。索引表对关键码排序形成。只需要对索引表的关键码二分查找即可。应用于数据量不是特别大的时候,因为会导致索引表也非常大。
分块索引:不要建立一个数据量等大小的索引表。各个块内部没有排序,各个块之间排序。
倒排索引:反过来由属性来确定记录。
二叉排序树(二叉搜索树)
插入和删除的效率不错,同时查找的效率也很高的算法。
满足:
左子树不为空时,左子树上所有节点的值小于它的根节点的值;
右子树不为空时,右子树上所有节点的值大于它的根节点的值;
左右子树也分别为二叉排序树。
中序遍历二叉排序树,就能得到一个有序序列。二叉树结构有利于插入删除操作。
查找、插入、删除
class Node:
def __init__(self, data):
self.data = data
self.lchild = None
self.rchild = None
class BST:
def __init__(self, node_list):
self.root = Node(node_list[0])
for data in node_list[1:]:
self.insert(data) # 插入元素创建二叉排序树
# 搜索
def search(self, node, parent, key): # 开始搜索的节点node,其父节点parent,关键字key
if node is None:
return False, node, parent
if node.data == key:
return True, node, parent # 如果当前节点的val等于key,返回搜索结果
if node.data > key:
return self.search(node.lchild, node, data) # 如果当前节点的val大于key,去左子树搜索
else:
return self.search(node.rchild, node, data) # 如果当前节点的val大于key,去右子树搜索
# 插入
def insert(self, data):
flag, n, p = self.search(self.root, self.root, data)
if not flag: # 如果二叉排序中不存在待插入节点,找到新节点的父节点
new_node = Node(data) # 创建新节点
if data > p.data: # 判断新节点是父节点的左孩子还是右孩子,然后插入即可
p.rchild = new_node
else:
p.lchild = new_node
# 删除
def delete(self, root, data):
flag, n, p = self.search(root, root, data)
if flag is False:
print("无该关键字,删除失败")
else:
if n.lchild is None: # 若待删节点n的左子树为空
if n == p.lchild: # 若n是其父节点p的左子树,则n的右子树变为p的左子树
p.lchild = n.rchild
else:
p.rchild = n.rchild # 若n是p的右子树,则n的右子树变为p的右子树
elif n.rchild is None: # 若n的右子树为空
if n == p.lchild:
p.lchild = n.lchild
else:
p.rchild = n.lchild
else: # 若n的左右子树均不为空
pre = n.rchild
if pre.lchild is None: # 若n的右子树的左子树为空
n.data = pre.data # n右子树的数据赋给n
n.rchild = pre.rchild # n的右子树变为n的右子树的右子树
else: # 若n的右子树的左子树不为空
next = pre.lchild
while next.lchild is not None: # 一直向左遍历到左子树为空的节点
pre = next
next = next.lchild
n.data = next.data # 把左子树为空的节点的数据赋给n
pre.lchild = next.rchild # 该节点的右子树链到该节点的父节点的左子树
# 先序遍历
def preOrderTraverse(self, node):
if node is not None:
print(node.data)
self.preOrderTraverse(node.lchild)
self.preOrderTraverse(node.rchild)
# 中序遍历
def inOrderTraverse(self, node):
if node is not None:
self.inOrderTraverse(node.lchild)
print(node.data)
self.inOrderTraverse(node.rchild)
# 后序遍历
def postOrderTraverse(self, node):
if node is not None:
self.postOrderTraverse(node.lchild)
self.postOrderTraverse(node.rchild)
print(node.data)
a = [49, 38, 65, 97, 60, 76, 13, 27, 5, 1]
bst = BST(a) # 创建二叉查找树
print('遍历')
bst.inOrderTraverse(bst.root) # 中序遍历
print('删除元素')
bst.delete(bst.root, 49)
bst.inOrderTraverse(bst.root)
print('搜索')
res, node, parent = bst.search(bst.root, None, 97)
print(res, node.data, parent.data)
平衡二叉排序树 AVL
避免以下二叉排序树的情况,防止二叉排序树退化成链。
平衡二叉排序树:要么是一颗空树,要么是一颗二叉排序树,且左右子树深度之差的绝对值不超过1,且左右子树都是平衡二叉排序树。
构建方法:在构建二叉排序树时,每插入一个节点,就检查树的平衡性是否被破坏(平衡因子大于1,左子树深度-右子树深度)。 如果被破坏,就旋转最小不平衡子树。遍历与查找和普通二叉排序树一样,但插入和删除需要考虑平衡性问题。
平衡被破坏的四种情况:
- 插入点位于X的左子节点的左子树——左左
- 插入点位于X的左子节点的右子树——左右
- 插入点位于X的右子节点的左子树——右左
- 插入点位于X的右子节点的右子树——右右
对于上面四中情况,可以分为两类:
1、外侧插入:左左、右右,都是又往边上发展了。
2、内侧插入:左右、右左,都是往里面来了些。
恢复平衡的方法:外侧插入单旋转、内侧插入双旋转。
先看图中外侧插入11,使得k2点平衡因子为3-1=2>1,为了恢复平衡,想象把k1点提起,k2自然下滑,把k1的右子树B挂到k2的左侧。
为什么这么做了:
1. 根据二叉排序树的性质,k2 > k1,所以k2必须成为新树形中的k1节点的右子节点。(k1向上提起,使k2自然下滑)
2. 同样根据性质,B子树的所有节点的键值都在k1和k2之间,也就是大于k1,小于k2,那就是在k1的右子树上,k2的左子树上,因此将B子树挂到k2的左侧(将B子树挂到k2的左侧)。最终调整后的图如上右图,这是左左,右右的情况一样。
注意调整后k1和k2的深度
def LL_Rotate(self, node):
k1 = node.left # 不平衡点的左子树k1
node.left = k1.right # k1的右子树的所有值都介于kl和node的值之间,所以将其设置为node的左子树
k1.right = node # 提起n点,node自然下滑。即node变为n的右子树
node.height = max(self.height(node.left), self.height(node.right)) + 1
k1.height = max(self.height(k1.left), self.height(node)) + 1
return k1
def RR_Rotate(self, node):
k1 = node.right
node.right = k1.left
k1.left = node
node.height = max(self.height(node.right), self.height(node.left)) + 1
k1.height = max(self.height(k1.right), self.height(node)) + 1
return k1
再看内侧插入的情况,图中插入15后k3点不平衡,k1上提k3自然落下k1右子树挂到k3左端,这样单旋转后发现还是不平衡的。
所以就需要先对k1、k2进行单旋转,然后再对k2、k3单旋转。即先对k1做RR_rotate,再对k3做LL_rotate
def LR_rotate(self, node):
node.left = self.RR_rotate(node.left)
return self.LL_rotate(node)
右左的情况类似
def RL_rotate(self, node):
node.right = self.LL_rotate(node.right)
return self.RR_rotate(node)
插入:
def height(self, node):
if node is None:
return -1
return node.height
def insert(self, key):
self.root = self._insert(key, self.root)
def _insert(self, key, node):
if node is None: # 要插入的树空
node = Node(key)
return node
if key == node.data: # 要插入的树中存在重复元素
print('重复元素,插入失败')
return node
if key < node.data: # 要插入的值小于根节点
node.left = self._insert(key, node.left) # 插入左子树中
# 判断平衡性
if self.height(node.left) - self.height(node.right) > 1:
if key < node.left.data: # 插入节点位于node左孩子的左子树中
node = self.LL_rotate(node) # 左左单旋转
else:
node = self.LR_rotate(node) # 插入的节点位于node左孩子的右子树中,左右双旋转
elif key > node.data: # 插入值大于根节点
node.right = self._insert(key, node.right) # 插入右子树中
if self.height(node.right) - self.height(node.left) > 1:
if key > node.right.data:
node = self.RR_rotate(node)
else:
node = self.RL_rotate(node)
node.height = max(self.height(node.left), self.height(node.right) + 1
return node
删除:
1.当前节点为要删除的节点且是叶子(无子树),直接删除,当前节点(为None)的平衡不受影响。
2.当前节点为要删除的节点且只有一个左儿子或右儿子,用左儿子或右儿子代替当前节点,当前节点的平衡不受影响。
3.当前节点为要删除的节点且有左子树右子树:如果右子树高度较高,则从右子树选取最小节点,将其值赋予当前节点,然后删除右子树的最小节点。如果左子树高度较高,则从左子树选取最大节点,将其值赋予当前节点,然后删除左子树的最大节点。这样操作当前节点的平衡不会被破坏。
4.当前节点不是要删除的节点,则对其左子树或者右子树进行递归操作。当前节点的平衡条件可能会被破坏,需要进行平衡操作。
def search(self, node, key):
if node is None:
return False, node
if node.data == key:
return True, node
if node.data > key:
return self.search(node.lchild, key)
else:
return self.search(node.rchild, key)
def findMin(self, node):
if node.left: # 如果存在左子树,就一直去左子树里找最小
return self.findMin(node.left)
return node # 没有左子树的话,当前根就是最小
def findMax(self, node):
if node.right: # 如果存在右子树,就去右子树找最大
return self.findMax(node.right)
return node # 没有右子树的话,当前根就是最大
def delete(self, key):
self.root = self.remove(key, self.root)
def remove(self, key, node):
if node is None: # node是None,两种情况:上来就是空树;递归到最后没有找到待删元素
print("没找到关键字,删除失败")
return node
if key == node.data: # 要删除的就是当前的根节点
if node.left and node.right: # 左右子树都不空
if self.height(node.left) > self.height(node.right): # 在高度更大的子树上进行操作
# 左子树高度大,删除左子树中元素最大的的节点,同时将其值赋给当前根节点
# 这样新的根节点值还是能保证比其左子树任意节点都大,node的平衡性不会被破坏
maxNode = self.findMax(node.left)
node.data = maxNode.data # 因为当前根节点一定比其左子树任意节点都大
node.left = self.remove(node.data, node.left) # 去node的左子树把原来最大的元素节点删除
else:
# 右子树高度大
minNode = self.findMin(node.right)
node.data = minNode.data
node.right = self.remove(node.data, node.right)
node.height = max(self.height(node.left), self.height(node.right)) + 1
else: # 左右子树中有一个为空,或者全为空
if node.right:
node = node.right # 左空右不空,直接用右子树代替node
elif node.left:
node = node.left # 右空左不空,左子树代替node
node.height = max(self.height(node.left), self.height(node.right)) + 1
else:
node = None # 都空,直接用None(也即左子树)代替node
elif key < node.data: # 要删除的不是当前子树的根节点,去其左子树或右子树递归删除,当前节点平衡性可能被破坏
node.left = self.remove(key, node.left) # 去node左子树删除,可能导致左低右高
if self.height(node.right) - self.hight(node.left) > 1: # 当前节点node不平衡,左低右高
if self.height(node.right.left) > self.height(node.right.right): # node右子树的左子树更高,双旋转
node = self.RL_rotate(node)
else: # node右子树的右子树更高,右右单旋转
node = self.RR_rotate(node)
node.height = max(self.height(node.left), self.height(node.right)) + 1
else: # key > node.data,去node的右子树递归删除
node.right = self.remove(key, node.right)
if self.height(node.left) - self.height(node.right) > 1: # node失去平衡,左高右低
if self.height(node.left.right) > self.height(node.left.left): # node左子树的右子树更高,双旋转
node = self.LR_rotate(node)
else:
node = self.LL_rotate(node) # 左左单旋转
node.height = max(self.height(node.left), self.height(node.right)) + 1
return node
多路查找树
降低对外部存储结构的访问次数。每一个节点的孩子可以多于两个,每个节点可以存储多个元素。
2-3树,每一个节点都具有两个孩子或三个孩子。左子树元素小于节点元素小于右子树元素。但不同于二叉排序树的是,2节点要么没孩子要么有两个孩子,不能只有一个孩子。3节点要么没孩子要么有三个孩子,且从左到右变大。2-3树所有叶子都要在同一层次上。
2-3树的插入:
1. 空树,建立一个2节点作为根节点即可。
2.插入进一个2节点,例如上图中插入3,把2节点变为3节点插入即可。
3.插入进一个3节点,三种情况。第一种,插入的叶子是3节点,上面的父节点是2节点,通过扩展父节点。例如上图中插入5,不能再从6向下扩展,则拆分3节点后向上扩展,把父节点扩展为3节点,再调整位置。
第二种情况,插入的叶子是3节点,父节点也是3节点,一直向上找父节点为2节点为止,扩展2节点。例如插入11,第三层的3节点满了,向上一层的3节点也满了,再向上找到2节点进行扩展,调整后还是要保持中序遍历结果是有序的。
第三种情况,如果一直向上找到根节点都是3节点,就需要增加高度来插入。例如再插入2,增加层以后,上面的3节点都需要拆成2节点来保证叶子都在同一层。
2-3树的删除:
1. 要删除的元素在一个3节点叶子上。直接删除即可,不会影响树结构。
2. 所删除的元素位于2节点的叶子上
分四种情况:此节点双亲也是2节点,且拥有一个3节点的右孩子,直接删除不行,拆分3节点后左旋转
此节点的双亲是2节点,且拥有一个2节点的右孩子,再拆一个3节点才行。注意根节点的左子树的最右节点元素是根节点元素直接前驱,根节点右子树的最左节点元素是根节点元素直接后继,所以补位后左旋转:
此节点的双亲是3节点。把双亲变2节点即可:
当前树是一个满二叉树,降低树高:
3. 所删元素位于非叶子的分支节点。此时按树中序遍历得到此元素的前驱或后续元素,补位:
分支节点是2节点
分支节点是3节点
2-3-4树,基于2-3树的扩展,多了一种4节点的情况。要么没有孩子,要么有4个孩子。其他性质和2-3树一致。
2-3-4树的插入:
2-3-4树的删除:
B树
2-3树是3阶B树,2-3-4树是4阶B树。
哈希表查找
不通过比较,直接得到关键字的位置。记录的存储位置 = f(关键字),每个关键字key对应一个存储位置f(key)。存储和查找的时候使用的哈希函数相同即可。但可能出现一个哈希地址对应多个记录的情况,即哈希冲突。在设计哈希函数时要令哈希冲突尽可能少。