目录
1. 基本概念
查找(Searching)就是根据给定的某个值,在查找表中确定一个其关键字等于给定值的数据元素。
查找表(Search Table):由同一类型的数据元素构成的集合
关键字(Key):数据元素中某个数据项的值,又称为键值
主键(Primary Key):可唯一的标识某个数据元素或记录的关键字
查找表按照操作方式可分为:
(1).静态查找表(Static Search Table):只做查找操作的查找表。它的主要操作是:
①查询某个“特定的”数据元素是否在表中
②检索某个“特定的”数据元素和各种属性
(2).动态查找表(Dynamic Search Table):在查找中同时进行插入或删除等操作:
①查找时插入数据
②查找时删除数据
平均查找长度(Average Search Length,ASL):需和指定key进行比较的关键字的个数的期望值,称为查找算法在查找成功时的平均查找长度。
对于含有n个数据元素的查找表,查找成功的平均查找长度为:ASL = Pi*Ci的和。
Pi:查找表中第i个数据元素的概率。
Ci:找到第i个数据元素时已经比较过的次数。
2 无序表查找
2.1 顺序查找
2.1.1 算法简介
说明:顺序查找适合于存储结构为顺序存储或链接存储的线性表。
2.1.2 算法描述
基本思想:顺序查找也称为线形查找,属于无序查找算法。从数据结构线形表的一端开始,顺序扫描,依次将扫描到的结点关键字与给定值k相比较,若相等则表示查找成功;若扫描结束仍没有找到关键字等于k的结点,表示查找失败。
2.1.3 python实现
# 最基础的遍历无序列表的查找算法
# 时间复杂度O(n)
def sequential_search(lis, key):
length = len(lis)
for i in range(length):
if lis[i] == key:
return i
else:
return False
if __name__ == '__main__':
LIST = [1, 5, 8, 123, 22, 54, 7, 99, 300, 222]
result = sequential_search(LIST, 123)
print(result)
2.1.4 算法评价
算法分析:最好情况是在第一个位置就找到了,此为O(1);最坏情况在最后一个位置才找到,此为O(n);所以平均查找次数为(n+1)/2。最终时间复杂度为O(n)
优缺点:
缺点:是当n 很大时,平均查找长度较大,效率低;
优点:是对表中数据元素的存储没有要求。另外,对于线性链表,只能进行顺序查找。
3 有序表查找
查找表中的数据必须按某个主键进行某种排序。
3.1 二分查找(Binary Search)
3.1.1 算法简介
二分查找(Binary Search),是一种在有序数组中查找某一特定元素的查找算法。查找过程从数组的中间元素开始,如果中间元素正好是要查找的元素,则查找过程结束;如果某一特定元素大于或者小于中间元素,则在数组大于或小于中间元素的那一半中查找,而且跟开始一样从中间元素开始比较。如果在某一步骤数组为空,则代表找不到。
这种查找算法每一次比较都使查找范围缩小一半。
3.1.2 算法描述
给予一个包含 n个带值元素的数组A
1、 令 L为0 , R为 n-1 ;
2、 如果L>R,则搜索以失败告终 ;
3、 令 m (中间值元素)为 ⌊(L+R)/2⌋;
4、 如果 Am<T,令 L为 m + 1 并回到步骤二 ;
5、 如果 Am>T,令 R为 m - 1 并回到步骤二;
3.1.3 python实现
# 针对有序查找表的二分查找算法
# 时间复杂度O(log(n))
def binary_search(lis, key):
low = 0
high = len(lis) - 1
time = 0
while low < high:
time += 1
mid = int((low + high) / 2)
if key < lis[mid]:
high = mid - 1
elif key > lis[mid]:
low = mid + 1
else:
# 打印折半的次数
print("times: %s" % time)
return mid
print("times: %s" % time)
return False
if __name__ == '__main__':
LIST = [1, 5, 7, 8, 22, 54, 99, 123, 200, 222, 444]
result = binary_search(LIST, 99)
print(result)
3.1.4 算法评价
时间复杂度:折半搜索每次把搜索区域减少一半,时间复杂度为 O(logn)
空间复杂度:O(1)
3.2 插值查找
3.2.1 算法简介
二分查找法虽然已经很不错了,但还有可以优化的地方。有的时候,对半过滤还不够狠,要是每次都排除十分之九的数据岂不是更好?选择这个值就是关键问题,插值的意义就是:以更快的速度进行缩减。
插值查找是根据要查找的关键字key与查找表中最大最小记录的关键字比较后的查找方法。其核心就在于:
插值的计算公式value = (key - list[low])/(list[high] - list[low])。用这个value来代替二分查找中的1/2。
时间复杂度o(logn),但对于表长较大而关键字分布比较均匀的查找表来说,效率较高。
3.2.2 算法描述
基于二分查找算法,将查找点的选择改进为自适应选择,可以提高查找效率。当然,差值查找也属于有序查找。
注:对于表长较大,而关键字分布又比较均匀的查找表来说,插值查找算法的平均性能比折半查找要好的多。反之,数组中如果分布非常不均匀,那么插值查找未必是很合适的选择。
3.2.3 python实现
# 插值查找算法
# 时间复杂度O(log(n))
def binary_search(lis, key):
low = 0
high = len(lis) - 1
time = 0
while low < high:
time += 1
# 计算mid值是插值算法的核心代码
mid = low + int((high - low) * (key - lis[low])/(lis[high] - lis[low]))
print("mid=%s, low=%s, high=%s" % (mid, low, high))
if key < lis[mid]:
high = mid - 1
elif key > lis[mid]:
low = mid + 1
else:
# 打印查找的次数
print("times: %s" % time)
return mid
print("times: %s" % time)
return False
if __name__ == '__main__':
LIST = [1, 5, 7, 8, 22, 54, 99, 123, 200, 222, 444]
result = binary_search(LIST, 54)
print(result)
3.2.4 算法评价
插值算法的总体时间复杂度仍然属于O(log(n))级别的。
其优点是,对于表内数据量较大,且关键字分布比较均匀的查找表,使用插值算法的平均性能比二分查找要好得多。反之,对于分布极端不均匀的数据,则不适合使用插值算法。
3.3 斐波那契查找
3.3.1 算法简介
斐波那契数列,又称黄金分割数列,指的是这样一个数列:1、1、2、3、5、8、13、21、····,在数学上,斐波那契被递归方法如下定义:F(1)=1,F(2)=1,F(n)=f(n-1)+F(n-2) (n>=2)。该数列越往后相邻的两个数的比值越趋向于黄金比例值(0.618)。
3.3.2 算法描述
斐波那契查找就是在二分查找的基础上根据斐波那契数列进行分割的。在斐波那契数列找一个等于略大于查找表中元素个数的数F[n],将原查找表扩展为长度为F[n](如果要补充元素,则补充重复最后一个元素,直到满足F[n]个元素),完成后进行斐波那契分割,即F[n]个元素分割为前半部分F[n-1]个元素,后半部分F[n-2]个元素,找出要查找的元素在那一部分并递归,直到找到。
3.3.3 python实现
# 斐波那契查找算法
# 时间复杂度O(log(n))
def fibonacci_search(lis, key):
# 需要一个现成的斐波那契列表。其最大元素的值必须超过查找表中元素个数的数值。
F = [1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 144,
233, 377, 610, 987, 1597, 2584, 4181, 6765,
10946, 17711, 28657, 46368]
low = 0
high = len(lis) - 1
# 为了使得查找表满足斐波那契特性,在表的最后添加几个同样的值
# 这个值是原查找表的最后那个元素的值
# 添加的个数由F[k]-1-high决定
k = 0
while high > F[k] - 1:
k += 1
print(k)
i = high
while F[k] - 1 > i:
lis.append(lis[high])
i += 1
print(lis)
# 算法主逻辑。time用于展示循环的次数。
time = 0
while low <= high:
time += 1
# 为了防止F列表下标溢出,设置if和else
if k < 2:
mid = low
else:
mid = low + F[k - 1] - 1
print("low=%s, mid=%s, high=%s" % (low, mid, high))
if key < lis[mid]:
high = mid - 1
k -= 1
elif key > lis[mid]:
low = mid + 1
k -= 2
else:
if mid <= high:
# 打印查找的次数
print("times: %s" % time)
return mid
else:
print("times: %s" % time)
return high
print("times: %s" % time)
return False
if __name__ == '__main__':
LIST = [1, 5, 7, 8, 22, 54, 99, 123, 200, 222, 444]
result = fibonacci_search(LIST, 444)
print(result)
3.3.4 算法评价
算法分析:最坏情况下,时间复杂度为O(log2n),且其期望复杂度也为O(log2n)。但就平均性能,要优于二分查找。但是在最坏情况下,比如这里如果key为1,则始终处于左侧半区查找,此时其效率要低于二分查找。
总结:二分查找的mid运算是加法与除法,插值查找则是复杂的四则运算,而斐波那契查找只是最简单的加减运算。在海量数据的查找中,这种细微的差别可能会影响最终的查找效率。因此,三种有序表的查找方法本质上是分割点的选择不同,各有优劣,应根据实际情况进行选择。
4 线性索引查找
对于海量的无序数据,为了提高查找速度,一般会为其构造索引表。
索引就是把一个关键字与它相对应的记录进行关联的过程。一个索引由若干个索引项构成,每个索引项至少包含关键字和其对应的记录在存储器中的位置等信息。
索引按照结构可以分为:线性索引、树形索引和多级索引。
线性索引:将索引项的集合通过线性结构来组织,也叫索引表。
线性索引可分为:稠密索引、分块索引和倒排索引
4.1 稠密索引
稠密索引指的是在线性索引中,为数据集合中的每个记录都建立一个索引项。
这其实就相当于给无序的集合,建立了一张有序的线性表。其索引项一定是按照关键码进行有序的排列。
这也相当于把查找过程中需要的排序工作给提前做了。
4.2 分块索引
4.2.1 算法简介
给大量的无序数据集合进行分块处理,使得块内无序,块与块之间有序。这其实是有序查找和无序查找的一种中间状态或者说妥协状态。因为数据量过大,建立完整的稠密索引耗时耗力,占用资源过多;但如果不做任何排序或者索引,那么遍历的查找也无法接受,只能折中,做一定程度的排序或索引。
要求是顺序表,分块查找又称索引顺序查找,它是顺序查找的一种改进方法。
4.2.2 算法描述
将n个数据元素"按块有序"划分为m块(m ≤ n)。
每一块中的结点不必有序,但块与块之间必须"按块有序";
即第1块中任一元素的关键字都必须小于第2块中任一元素的关键字;
而第2块中任一元素又都必须小于第3块中的任一元素,……
算法流程:
1、先选取各块中的最大关键字构成一个索引表;
2、查找分两个部分:先对索引表进行二分查找或顺序查找,以确定待查记录在哪一块中;
3、在已确定的块中用顺序法进行查找。
4.2.3 算法评价
时间复杂度:O(log(m)+N/m)
4.3 倒排索引
不是由记录来确定属性值,而是由属性值来确定记录的位置,这种被称为倒排索引。其中记录号表存储具有相同次关键字的所有记录的地址或引用(可以是指向记录的指针或该记录的主关键字)。倒排索引是最基础的搜索引擎索引技术。
https://www.cnblogs.com/zlslch/p/6440114.html
5 树表查找
5.1 二叉排序树(BST)
5.1.1 算法简介
二叉查找树是先对待查找的数据进行生成树,确保树的左分支的值小于右分支的值,然后在就行和每个节点的父节点比较大小,查找最适合的范围。 这个算法的查找效率很高,但是如果使用这种查找方法要首先创建树。
二叉查找树(BinarySearch Tree)或者是一棵空树,或者是具有下列性质的二叉树:
1)若任意节点的左子树不空,则左子树上所有结点的值均小于它的根结点的值;
2)若任意节点的右子树不空,则右子树上所有结点的值均大于它的根结点的值;
3)任意节点的左、右子树也分别为二叉查找树。
上述性质简称二叉排序树性质(BST性质)。
二叉排序树又称二叉查找树,是一种动态查找表,所谓动态查找表是指除了查询检索操作以外,还可以进行插入、删除操作的结构;与之相对应的就是静态查找表,只能进行查询检索操作。
5.1.2 算法描述
对于二叉排序树的实现就是对查找、插入、删除操作的实现,因为要进行插入删除操作,所以要用链表结构来实现,操作介绍如下:
1. 查找key:从根结点出发,若key大于根结点则向根结点的右侧走,跟这个根结点的右结点比较;若key小于根结点则向根结点的左侧走,跟这个根结点做结点比较,该根结点的左右子树分别都是二叉排序树,所以重复上面的步骤,直到找到key,或者到NULL(叶子结点的子结点为空)。
2. 插入key:跟查找key步骤相同,找到key时返回,不进行插入;未找到时,在最后一个查找叶子结点下插入key值,若key大于该叶子结点的值则插入该叶子结点的右侧,作为该叶子结点的右子树,否则作为左子树。
3. 删除key:跟查找key步骤相同,若查找最终为NULL(未找到),则返回,若在结点p处找到key,则进行一下判断:
a. 若p为叶子结点,先找到p的父节点q,若p为q的左子树,则将q的左子树指针至NULL,若p为q的右子树,则将q的右子树指针至NULL,删除p
b. 若p的左子树不为空,右子树为空,则找到p的父节点q,若p是q的左子树,则将q的左子树指针指向p的左子树;若p是q的右子树,则将q的右子树指针指向p的左子树,删除p
c. 若p的右子树不为空,左子树为空,则找到p的父节点q,若p是q的左子树,则将q的左子树指针指向p的右子树,若p是q的右子树,则将q的右子树指针指向p的右子树,删除p
d. 若p的左右子树都不为空。根据二又排序树的特点,可以从其左子树中选择关键字最大的节点或从其右子树中选择关键字最小的节点放在被删去节点的位置。假如选取左子树上关键字最大的节点,那么该节点一定是左子树的最右下节点。注意,当把左子树中最右下节点*r上移时,如果它有左子树,还需把这棵左子树改为*r节点原来双亲节点的右子树。例如:找到p的父节点q,将p的左子树作为p右子树的最左结点的子结点,然后判断p是q的右子树还是q的左子树,若p是q的左子树,则将q的左子树指针指向p的右子树,若p是q的右子树,则将q的右子树指针指向p的右子树,删除p。
5.1.3 python实现
class BSTNode:
"""
定义一个二叉树节点类。
以讨论算法为主,忽略了一些诸如对数据类型进行判断的问题。
"""
def __init__(self, data, left=None, right=None):
"""
初始化
:param data: 节点储存的数据
:param left: 节点左子树
:param right: 节点右子树
"""
self.data = data
self.left = left
self.right = right
class BinarySortTree:
"""
基于BSTNode类的二叉排序树。维护一个根节点的指针。
"""
def __init__(self):
self._root = None
def is_empty(self):
return self._root is None
def search(self, key):
"""
关键码检索
:param key: 关键码
:return: 查询节点或None
"""
bt = self._root
while bt:
entry = bt.data
if key < entry:
bt = bt.left
elif key > entry:
bt = bt.right
else:
return entry
return None
def insert(self, key):
"""
插入操作
:param key:关键码
:return: 布尔值
"""
bt = self._root
if not bt:
self._root = BSTNode(key)
return
while True:
entry = bt.data
if key < entry:
if bt.left is None:
bt.left = BSTNode(key)
return
bt = bt.left
elif key > entry:
if bt.right is None:
bt.right = BSTNode(key)
return
bt = bt.right
else:
bt.data = key
return
def delete(self, key):
"""
二叉排序树最复杂的方法
:param key: 关键码
:return: 布尔值
"""
p, q = None, self._root # 维持p为q的父节点,用于后面的链接操作
if not q:
print("空树!")
return
while q and q.data != key:
p = q
if key < q.data:
q = q.left
else:
q = q.right
if not q: # 当树中没有关键码key时,结束退出。
return
# 上面已将找到了要删除的节点,用q引用。而p则是q的父节点或者None(q为根节点时)。
if not q.left:
if p is None:
self._root = q.right
elif q is p.left:
p.left = q.right
else:
p.right = q.right
return
# 查找节点q的左子树的最右节点,将q的右子树链接为该节点的右子树
# 该方法可能会增大树的深度,效率并不算高。可以设计其它的方法。
r = q.left
while r.right:
r = r.right
r.right = q.right
if p is None:
self._root = q.left
elif p.left is q:
p.left = q.left
else:
p.right = q.left
def __iter__(self):
"""
实现二叉树的中序遍历算法,
展示我们创建的二叉排序树.
直接使用python内置的列表作为一个栈。
:return: data
"""
stack = []
node = self._root
while node or stack:
while node:
stack.append(node)
node = node.left
node = stack.pop()
yield node.data
node = node.right
if __name__ == '__main__':
lis = [62, 58, 88, 48, 73, 99, 35, 51, 93, 29, 37, 49, 56, 36, 50]
bs_tree = BinarySortTree()
for i in range(len(lis)):
bs_tree.insert(lis[i])
bs_tree.insert(100)
bs_tree.delete(100)
for i in bs_tree:
print(i, end=" ")
print("\n", bs_tree.search(4))
5.1.4 算法评价
- 二叉排序树以链式进行存储,保持了链接结构在插入和删除操作上的优点。
- 在极端情况下,查询次数为1,但最大操作次数不会超过树的深度。也就是说,二叉排序树的查找性能取决于二叉排序树的形状,也就引申出了后面的平衡二叉树。
- 给定一个元素集合,可以构造不同的二叉排序树,当它同时是一个完全二叉树的时候,查找的时间复杂度为O(log(n)),近似于二分查找。
- 当出现最极端的斜树时,其时间复杂度为O(n),等同于顺序查找,效果最差,如右图。
5.2 平衡二叉树
5.2.1 算法简介
平衡二叉树(AVL树,发明者的姓名缩写):一种高度平衡的排序二叉树,其每一个节点的左子树和右子树的高度差最多等于1。平衡二叉树首先必须是一棵二叉排序树!
平衡因子(Balance Factor):将二叉树上节点的左子树深度减去右子树深度的值。对于平衡二叉树所有包括分支节点和叶节点的平衡因子只可能是-1,0和1,只要有一个节点的因子不在这三个值之内,该二叉树就是不平衡的。
最小不平衡子树:距离插入结点最近的,且平衡因子的绝对值大于1的节点为根的子树。
平衡二叉树是对二叉搜索树(又称为二叉排序树)的一种改进。二叉搜索树有一个缺点就是,树的结构是无法预料的,随意性很大,它只与节点的值和插入的顺序有关系,往往得到的是一个不平衡的二叉树。在最坏的情况下,可能得到的是一个单支二叉树,其高度和节点数相同,相当于一个单链表,对其正常的时间复杂度有O(log2n)变成了O(n),从而丧失了二叉排序树的一些应该有的优点。
当插入一个新的节点的时候,在普通的二叉树中不用考虑树的平衡因子,只要将大于根节点的值插入到右子树,小于节点的值插入到左子树,递归即可。而在平衡二叉树则不一样,在插入节点的时候,如果插入节点之后有一个节点的平衡因子要大于2或者小于-2的时候,他需要对其进行调整,现在只考虑插入到节点的左子树部分(右子树与此相同)。主要分为以下三种情况:
(1) 若插入前一部分节点的左子树高度和右子树高度相等,即平衡因子为0,插入后平衡因子变为1,仍符合平衡的条件不用调整。
(2) 若插入前左子树高度小于右子树高度,即平衡因子为-1,则插入后将使平衡因子变为0,平衡性反倒得到调整,所以不必调整。
(3) 若插入前左子树的高度大于右子树高度,即平衡因子为1,则插入左子树之后会使得平衡因子变为2,这样的情况下就破坏了平衡二叉树的结构,所以必须对其进行调整,使其加以改善。
5.2.2 算法描述
平衡二叉树的构建思想:每当插入一个新结点时,先检查是否破坏了树的平衡性,若有,找出最小不平衡子树。在保持二叉排序树特性的前提下,调整最小不平衡子树中各结点之间的连接关系,进行相应的旋转,成为新的平衡子树。
是由[1,2,3,4,5,6,7,10,9]构建平衡二叉树:
1、平衡二叉树插入节点的调整方法
平衡二叉树最基本的4种调整操作,调整的原则是调整后他的搜索二叉树的性质不变,即树的中序遍历是不会改变的:
(1). LL型调整
在B点的左子树上插入一个节点。插入后B点的左子树的平衡因子变为1,A节点的平衡因子变为了2。这样可以看出来A节点为根节点的子树是最小不平衡子树。调整时,单向右旋平衡。将A的左孩子B向右旋转代替A成为的根节点,将A的节点右下旋转成为B的右子树的根节点,而B的原右子树变为A的左子树。详细过程如下图:
@staticmethod
def LL(a, b):
a.left = b.right # 将b的右子树接到a的左子结点上
b.right = a #将a树接到b的右子结点上
a.bf = b.bf = 0 #调整a、b的bf值。
return b
(2). RR型调整操作
在A节点的右孩子的右子树上插入节点,使得A节点的平衡因子由-1变为-2而引起的不平衡所进行的调整操作。调整操作大致一样。调整时,单向左旋平衡。将A的右孩子B向左上旋转代替A成为根节点,将A的节点左下旋转称为B的左子树的根节点,而B的原左子树变为A的右子树。
@staticmethod
def RR(a, b):
a.right = b.left
b.left = a
a.bf = b.bf = 0
return b
(3)LR型的调整操作
在A节点的左孩子的右子树上插入节点,使得A节点的平衡因子由1变为了2而引起的不平衡所进行的调整的操作。调整的方法:先坐旋转后向右旋转平衡,即先将A节点的左孩子(即B节点)的右子树的根节点(设为C节点)向左上旋转提升到B节点的位置,然后再把该C节点向右上旋转提升到A节点的位置。因调整前后对应的中序序列相同,所以调整后仍保持二叉排序树的性质不变。即调整过程如图:
@staticmethod
def LR(a,b):
c = b.right
a.left, b.right = c.right, c.left
c.left, c.right = b, a
if c.bf == 0: #c本身就是插入点
a.bf = b.bf = 0
elif c.bf == 1: #插在c的左子树
a.bf = -1
b.bf = 0
else: #插在c的右子树
a.bf = 0
b.bf = 1
c.bf = 0
return c
(4)RL型
这是因在A节点的右孩子(设为B节点)的左子树上插入节点,使得A节点的平衡因子由-1变为-2而引起的不平衡。RL型调整的一般情况如图所示。调整的方法是:先右旋转后向左旋转平衡,即先将A节点的右孩子(即B节点)的左子树的根节点(设为C节点)向右上旋转提升到B节点的位置,然后再把该C节点向左上旋转提升到A节点的位置。因调整前后对应的中序序列相同,所以调整后仍保持二叉排序树的性质不变。
@staticmethod
def RL(a, b):
c = b.left
a.right, b.left = c.left, c.right
c.left, c.right = a, b
if c.bf == 0:
a.bf = b.bf = 0
elif c.bf == 1:
a.bf = 0
b.bf = -1
else:
a.bf = 1
b.bf = 0
c.bf = 0
return c
2、平衡二叉树删除节点的调整方法
平衡二叉树的删除查点操作与插人操作有许多相似之处。
在平衡二叉树上删除节点x(假定有且仅有一个节点值等于x)的过程如下:
(1)采用二叉排序树的删除方法找到节点x并删除之。
(2)沿根节点到被删除节点的路线之逆逐层向上查找,必要时修改x祖先节点的平衡因子,因为删除节点x后,会使某些子树的高度降低。
(3)查找途中,一旦发现x的某个祖先*p失衡,就要进行调整。不妨设节点x在*p的左子树中,在节点*p失衡后,要做何种调整,要看节点*p的右孩子*pl,若*pl的平衡因子是1,说明它的左子树高,需做RL调整;若*pl的平衡因子是一1,需做RR调整;若*pl的平衡因子是0,则做RL或RR调整均可。如果节点x在*p的右子树中,调整过程类似。
(4)如果调整之后,对应的子树的高度降低了,这个过程还将继续,直到根节点为止。
也就是说,在平衡二叉树上删除一个节点有可能引起多次调整,不像插入节点那样至多调整一次。
3、平衡二叉树的查找
在平衡二叉树上进行查找的过程和在二叉排序树上进行查找的过程完全相同,因此,在平衡二叉树上进行查找时关键字的比较次数不会超过平衡二叉树的深度。
5.2.3 python实现
https://blog.csdn.net/qq_34840129/article/details/80728186
class StackUnderflow(ValueError):
pass
class SStack():
def __init__(self):
self.elems = []
def is_empty(self):
return self.elems == []
def top(self): # 取得栈里最后压入的元素,但不删除
if self.elems == []:
raise StackUnderflow('in SStack.top()')
return self.elems[-1]
def push(self, elem):
self.elems.append(elem)
def pop(self):
if self.elems == []:
raise StackUnderflow('in SStack.pop()')
return self.elems.pop()
class Assoc: # 定义一个关联类
def __init__(self, key, value):
self.key = key # 键(关键码)
self.value = value # 值
def __lt__(self, other): # Python解释器中遇到比较运算符<,会去找类里定义的__lt__方法(less than)
return self.key < other.key
def __le__(self, other): # (less than or equal to)
return self.key < other.key or self.key == other.key
def __str__(self):
return 'Assoc({0},{1})'.format(self.key, self.value) # key和value分别替换前面{0},{1}的位置。
class BinTNode:
def __init__(self, dat, left=None, right=None):
self.data = dat
self.left = left
self.right = right
class DictBinTree:
def __init__(self, root=None):
self.root = root
def is_empty(self):
return self.root is None
def search(self, key): # 检索是否存在关键码key
bt = self.root
while bt is not None:
entry = bt.data
if key < entry.key:
bt = bt.left
elif key > entry.key:
bt = bt.right
else:
return entry.value
return None
def insert(self, key, value):
bt = self.root
if bt is None:
self.root = BinTNode(Assoc(key, value))
return
while True:
entry = bt.data
if key < entry.key: # 如果小于当前关键码,转向左子树
if bt.left is None: # 如果左子树为空,就直接将数据插在这里
bt.left = BinTNode(Assoc(key, value))
return
bt = bt.left
elif key > entry.key:
if bt.right is None:
bt.right = BinTNode(Assoc(key, value))
return
bt = bt.right
else:
bt.data.value = value
return
def print_all_values(self):
bt, s = self.root, SStack()
while bt is not None or not s.is_empty(): # 最开始时栈为空,但bt不为空;bt = bt.right可能为空,栈不为空;当两者都为空时,说明已经全部遍历完成了
while bt is not None:
s.push(bt)
bt = bt.left
bt = s.pop() # 将栈顶元素弹出
yield bt.data.key, bt.data.value
bt = bt.right # 将当前结点的右子结点赋给bt,让其在while中继续压入栈内
def entries(self):
bt, s = self.root, SStack()
while bt is not None or not s.is_empty():
while bt is not None:
s.push(bt)
bt = bt.left
bt = s.pop()
yield bt.data.key, bt.data.value
bt = bt.right
def print_key_value(self):
for k, v in self.entries():
print(k, v)
def delete(self, key):
# 以下这一段用于找到待删除结点及其父结点的位置。
del_position_father, del_position = None, self.root # del_position_father是待删除结点del_position的父结点
while del_position is not None and del_position.data.key != key: # 通过不断的比较,找到待删除结点的位置
del_position_father = del_position
if key < del_position.data.key:
del_position = del_position.left
else:
del_position = del_position.right
if del_position is None:
print('There is no key')
return
if del_position.left is None: # 如果待删除结点只有右子树
if del_position_father is None: # 如果待删除结点的父结点是空,则说明待删除结点是根结点
self.root = del_position.right # 则直接将根结点置空
elif del_position is del_position_father.left: # 如果待删除结点是其父结点的左结点
del_position_father.left = del_position.right # ***改变待删除结点父结点的左子树的指向
else:
del_position_father.right = del_position.right
return
# 如果既有左子树又有右子树,或者仅有左子树时,都可以用直接前驱替换的删除结点的方式,只不过得到的二叉树与原理中说明的不一样,但是都满足要求。
pre_node_father, pre_node = del_position, del_position.left
while pre_node.right is not None: # 找到待删除结点的左子树的最右结点,即为待删除结点的直接前驱
pre_node_father = pre_node
pre_node = pre_node.right
del_position.data = pre_node.data # 将前驱结点的data赋给删除结点即可,不需要改变其原来的连接方式
if pre_node_father.left is pre_node:
pre_node_father.left = pre_node.left
if pre_node_father.right is pre_node:
pre_node_father.right = pre_node.left
def build_dictBinTree(entries):
dic = DictBinTree()
for k, v in entries:
dic.insert(k, v)
return dic
class AVLNode(BinTNode):
def __init__(self, data):
BinTNode.___init__(self, data)
self.bf = 0
class DictAVL(DictBinTree):
def __init__(self, data):
DictBinTree.___init__(self)
@staticmethod
def LL(a, b):
a.left = b.right # 将b的右子树接到a的左子结点上
b.right = a # 将a树接到b的右子结点上
a.bf = b.bf = 0 # 调整a、b的bf值。
return b
@staticmethod
def RR(a, b):
a.right = b.left
b.left = a
a.bf = b.bf = 0
return b
@staticmethod
def LR(a, b):
c = b.right
a.left, b.right = c.right, c.left
c.left, c.right = b, a
if c.bf == 0: # c本身就是插入点
a.bf = b.bf = 0
elif c.bf == 1: # 插在c的左子树
a.bf = -1
b.bf = 0
else: # 插在c的右子树
a.bf = 0
b.bf = 1
c.bf = 0
return c
@staticmethod
def RL(a, b):
c = b.left
a.right, b.left = c.left, c.right
c.left, c.right = a, b
if c.bf == 0:
a.bf = b.bf = 0
elif c.bf == 1:
a.bf = 0
b.bf = -1
else:
a.bf = 1
b.bf = 0
c.bf = 0
return c
def insert(self, key, value):
a = p = self.root
if a is None: # 如果根结点为空,则直接将值插入到根结点
self.root = AVLNode(Assoc(key, value))
return
a_father, p_father = None # a_father用于最后将调整后的子树接到其子结点上
while p is not None: # 通过不断的循环,将p下移,查找插入位置,和最小非平衡子树
if key == p.data.key: # 如果key已经存在,则直接修改其关联值
p.data.value = value
return
if p.bf != 0: # 如果当前p结点的BF=0,则有可能是最小非平衡子树的根结点
a_father, a, = p_father, p
p_father = p
if key < p.data.key:
p = p.left
else:
p = p.right
# 上述循环结束后,p_father已经是插入点的父结点,a_father和a记录着最小非平衡子树
node = AVLNode(Assoc(key, value))
if key < p_father.data.key:
p_father.left = node
else:
p_father.right = node
# 新结点已插入,a是最小非平衡子树的根结点
if key < a.data.key: # 新结点在a的左子树
p = b = a.left
d = 1 # d记录新结点被 插入到a的哪棵子树
else:
p = b = a.right # 新结点在a的右子树
d = -1
# 在新结点插入后,修改b到新结点路径上各结点的BF值。调整过程的BF值修改都在子函数中操作
while p != node:
if key < p.data.key:
p.bf = 1
p = p.left
else:
p.bf = -1
p = p.right
if a.bf == 0: # 如果a的BF原来为0,那么插入新结点后不会失衡
a.bf = d
return
if a.bf == -d: # 如果新结点插入在a较低的子树里
a.bf = 0
return
# 以上两条if语句都不符合的话,说明新结点被插入在较高的子树里,需要进行调整
if d == 1: # 如果新结点插入在a的左子树
if b.bf == 1: # b的BF原来为0,如果等于1,说明新结点插入在b的左子树
b = DictAVL.LL(a, b)
else: # 新结点插入在b的右子树
b = DictAVL.LR(a, b)
else: # 新结点插入在a的右子树
if b.bf == -1: # 新结点插入在b的右子树
b = DictAVL.RR(a, b)
else: ##新结点插入在b的左子树
b = DictAVL.RL(a, b)
# 将调整后的最小非平衡子树接到原树中,也就是接到原来a结点的父结点上
if a_father is None: # 判断a是否是根结点
self.root = b
else:
if a_father == a:
a_father.left = b
else:
a_father.right = b
if __name__ == "__main__":
# LL调整
entries = [(5, 'a'), (2.5, 'g'), (2.3, 'h'), (3, 'b'), (2, 'd'), (4, 'e'), (3.5, 'f')]
dic = build_dictBinTree(entries)
dic.print_key_value()
print('after inserting')
dic.insert(1, 'i')
dic.print_key_value()
# LR调整
entries = [(2.5, 'g'), (3, 'b'), (4, 'e'), (3.5, 'f')]
dic = build_dictBinTree(entries)
dic.print_key_value()
print('after inserting')
dic.insert(3.2, 'i') # LL
dic.print_key_value()
5.2.4 算法评价
AVL树保持每个结点的左子树与右子树的高度差至多为1,从而可以证明树的高度为O(log(n))。
Insert操作与delete操作的复杂度均为log(n),旋转操作可能会达到log(n)次
含有n个节点的平衡二叉树的平均查找长度为O(log2n)
5.3 B-树
https://www.sohu.com/a/154640931_478315
B 树可以看作是对2-3查找树的一种扩展,即他允许每个节点有M-1个子节点。
①根节点至少有两个子节点;
②每个节点有M-1个key,并且以升序排列;
③位于M-1和M key的子节点的值位于M-1 和M key对应的Value之间;
④非叶子结点的关键字个数=指向儿子的指针个数-1;
⑤非叶子结点的关键字:K[1], K[2], …, K[M-1];且K[i] ;
⑥其它节点至少有M/2个子节点;
⑦所有叶子结点位于同一层;
如:(M=3)
B树算法思想:
B-树的搜索,从根结点开始,对结点内的关键字(有序)序列进行二分查找,如果命中则结束,否则进入查询关键字所属范围的儿子结点;重复,直到所对应的儿子指针为空,或已经是叶子结点;
B树的特性:
1.关键字集合分布在整颗树中;
2.任何一个关键字出现且只出现在一个结点中;
3.搜索有可能在非叶子结点结束;
4.其搜索性能等价于在关键字全集内做一次二分查找;
5.自动层次控制;
由于限制了除根结点以外的非叶子结点,至少含有M/2个儿子,确保了结点的至少利用率,其最底搜索性能为O(LogN)
5.4 B+树
B+树是B-树的变体,也是一种多路搜索树:
1.其定义基本与B-树同,除了:
2.非叶子结点的子树指针与关键字个数相同;
3.非叶子结点的子树指针P[i],指向关键字值属于[K[i], K[i+1])的子树
4.B-树是开区间;
5.为所有叶子结点增加一个链指针;
6.所有关键字都在叶子结点出现;
如:(M=3)
https://www.sohu.com/a/156153437_186061
B+树算法思想:
B+的搜索与B-树也基本相同,区别是B+树只有达到叶子结点才命中(B-树可以在非叶子结点命中),其性能也等价于在关键字全集做一次二分查找;
B+树的特性:
1.所有关键字都出现在叶子结点的链表中(稠密索引),且链表中的关键字恰好是有序的;
2.不可能在非叶子结点命中;
3.非叶子结点相当于是叶子结点的索引(稀疏索引),叶子结点相当于是存储(关键字)数据的数据层;
4.更适合文件索引系统;
5.5 树表查找总结
二叉查找树平均查找性能不错,为O(logn),但是最坏情况会退化为O(n)。在二叉查找树的基础上进行优化,我们可以使用平衡查找树。平衡查找树中的2-3查找树,这种数据结构在插入之后能够进行自平衡操作,从而保证了树的高度在一定的范围内进而能够保证最坏情况下的时间复杂度。但是2-3查找树实现起来比较困难,红黑树是2-3树的一种简单高效的实现,他巧妙地使用颜色标记来替代2-3树中比较难处理的3-node节点问题。红黑树是一种比较高效的平衡查找树,应用非常广泛,很多编程语言的内部实现都或多或少的采用了红黑树。
除此之外,2-3查找树的另一个扩展——B/B+平衡树,在文件系统和数据库系统中有着广泛的应用。B/B+树常用于文件系统和数据库系统中,它通过对每个节点存储个数的扩展,使得对连续的数据能够进行较快的定位和访问,能够有效减少查找时间,提高存储的空间局部性从而减少IO操作。
6 哈希表
6.1 算法简介
哈希查找也称为散列查找。O(1)的查找,即所谓的秒查。所谓的哈希其实就是在记录的存储位置和记录的关键字之间建立一个确定的对应关系f,使得每个关键字key对应一个存储位置f(key)。查找时,根据这个确定的对应关系找到给定值的映射f(key),若查找集合中存在这个记录,则必定在f(key)的位置上。哈希技术既是一种存储方法,也是一种查找方法。
哈希查找的操作步骤 :
(1)用给定的哈希函数构造哈希表;
(2)根据选择的冲突处理方法解决地址冲突;
(3)在哈希表的基础上执行哈希查找。
6.2 散列函数的构造方法
构造哈希函数的目的是时得到的哈希地址尽可能均匀地分布在n个连续内存单元地址上,同时使计算过程尽可能简单以达到尽可能高的时间效率。一个好的散列函数:计算简单、散列地址分布均匀。
- 直接定址法
例如取关键字的某个线性函数为散列函数:
f(key) = a*key + b (a,b为常数) - 数字分析法
抽取关键字里的数字,根据数字的特点进行地址分配。是提取关键字中取值较均匀的数字位作为哈希地址的方法。 - 平方取中法
将关键字的数字求平方,取分布均匀的即为作为哈希地址的方法。 - 折叠法
将关键字的数字分割后分别计算,再合并计算,一种玩弄数字的手段。先把关键字中的若干段作为一小组,然后把各小组折叠相加后分布均匀的即为作为哈希地址的方法。 - 除留余数法
最为常见的方法之一。
对于表长为m的数据集合,散列公式为:
f(key) = key mod p (p<=m)
mod:取模(求余数)
该方法最关键的是p的选择,而且数据量较大的时候,冲突是必然的。一般会选择接近m的质数。 - 随机数法
选择一个随机数,取关键字的随机函数值为它的散列地址。
f(key) = random(key)
总结,实际情况下根据不同的数据特性采用不同的散列方法,考虑下面一些主要问题:
- 计算散列地址所需的时间
- 关键字的长度
- 散列表的大小
- 关键字的分布情况
- 记录查找的频率
6.3 处理散列冲突
解决哈希冲突的方法有很多,可分为开放定址法和拉链法两大类。其基本思路是当发生哈希冲突时通过哈希冲突函数(设为hl(k)(l=1,2,……,m-1))产生一个新的哈希地址使hl(ki)不等于hl(kj)。哈希冲突函数产生的哈希地址仍可能有哈希冲突问题,此时再用新的哈希冲突函数得到新的哈希地址,一直到不存在哈希冲突为止,因此有1=1,2,.…,m-1.这样就把要存绪的n个元素,通过哈希函数映射得到的哈希地址(当哈希冲突时通过哈希冲突函数映射得到的哈希地址)存绪到了m个连续内存单元中,从而完成了哈系表的建立。
(1)与装填因子α有关;
(2)与所采用的哈希函数有关;
(3)与解决冲突的哈希冲突函数有关。
6.3.1 开放定址法
(1)线性探查法
就是一旦发生冲突,就去寻找下一个空的散列地址,只要散列表足够大,空的散列地址总能找到,并将记录存入。
公式是:
这种简单的冲突解决办法被称为线性探测,无非就是自家的坑被占了,就逐个拜访后面的坑,有空的就进,也不管这个坑是不是后面有人预定了的。
线性探测带来的最大问题就是冲突的堆积,你把别人预定的坑占了,别人也就要像你一样去找坑。
改进的办法有平方探查法和随机数探测法。
(2)平方探查法
设发生冲突的地址为d,则平方探查法的探查序列为:
平方探查法的数学描述公式为:
不做特殊说明是按“+”计算。
平方探查法是一种较好的处理冲突的方法,可以避免出现堆积问题。它的缺点是不能探查到哈希表上的所有单元,但至少能探查到一半单元。
6.3.2 拉链法
碰到冲突时,不更换地址,而是将所有关键字为同义词的记录存储在一个链表里,在散列表中只存储同义词子表的头指针。
拉链法是把所看的同义词用单链表链接起来的方法,在这种方法中,哈希表每个单元中存放的不再是元素本身,而是相应同义词单链表的头指针。由于单链表中可插入任意多个节点,所以此时装填因子α根据同义词的多少既可以设定为大于1,也可以设定为小于或等于1,通常取α=1。
与开放定址法相比,拉链法有以下几个优点:
(1)拉链法处理冲突简单,且无堆积现象,即非同义词之间绝不会发生冲突,因此平均查找长度较短:
(2)由于拉链法中各链表上的元素空间是动态申请的,故它更适合于建表前无法确定表长的情况;
(3)开放定址法为减少冲突要求装填因子α较小,故当数据规模较大时会浪费很多空间,而拉链法中可取,且数据规模较大时,拉链法中增加的指针域可忽略不计,因此节省空间;
(4)在用拉链法构造的哈希表中,删除元素的操作易于实现,只需删去链表上相应的元素即可。 而对开放地址发构造的哈希表,删除元素不能简单地将被删除元素的空间置为空,否则将截断在它之后填入哈希表的同义词元素的查找路径,这是因为在开放地址法中,空地址单元(即开放地址)能查找失败的条件。因此在用开放地址法处理冲突的哈希表上执行删除操作,只能在被删除元素上做删除标记,而不能真正删除元素。
6.4 python实现
# 忽略了对数据类型,元素溢出等问题的判断。
class HashTable:
def __init__(self, size):
self.elem = [None for i in range(size)] # 使用list数据结构作为哈希表元素保存方法
self.count = size # 最大表长
def hash(self, key):
return key % self.count # 散列函数采用除留余数法
def insert_hash(self, key):
"""插入关键字到哈希表内"""
address = self.hash(key) # 求散列地址
while self.elem[address]: # 当前位置已经有数据了,发生冲突。
address = (address+1) % self.count # 线性探测下一地址是否可用
self.elem[address] = key # 没有冲突则直接保存。
def search_hash(self, key):
"""查找关键字,返回布尔值"""
star = address = self.hash(key)
while self.elem[address] != key:
address = (address + 1) % self.count
if not self.elem[address] or address == star: # 说明没找到或者循环到了开始的位置
return False
return True
if __name__ == '__main__':
list_a = [12, 67, 56, 16, 25, 37, 22, 29, 15, 47, 48, 34]
hash_table = HashTable(12)
for i in list_a:
hash_table.insert_hash(i)
for i in hash_table.elem:
if i:
print((i, hash_table.elem.index(i)), end=" ")
print("\n")
print(hash_table.search_hash(15))
print(hash_table.search_hash(33))
6.5 算法评价
如果没发生冲突,则其查找时间复杂度为O(1),属于最极端的好了。但是,现实中冲突可不可避免的,下面三个方面对查找性能影响较大:
- 散列函数是否均匀
- 处理冲突的办法
- 散列表的装填因子(表内数据装满的程度)
在一般情况下,假设哈希函数是均匀的,则可以证明:不同的解决冲突方法得到的哈希表的平均查找长度不同。表9.4列出了用几种不同的方法解决冲突时哈希表的平均查找长度。从中看到,哈希表的平均查找长度不是元素个数n的函数,而是装填因子α的函数。因此,在设计哈希表时可通过选择α来控制哈希表的平均查找长度。
参考:
二叉树的算法代码,一篇文章全搞定!http://www.sohu.com/a/254294287_478315
https://www.cnblogs.com/lsqin/p/9342929.html
https://blog.csdn.net/Kaiyuan_sjtu/article/details/80109004