【数据结构】查找算法 Python 版

我自己写了一个 Python 类,里面放了各种查找算法的代码,并且总结了所有常见的查找算法的基本思想、特点、适用情况等等。通过 __doc__ 方法可以查看代码里的说明和介绍。

写代码的时候参考了[Data Structure & Algorithm] 七大查找算法。这个作者用的是C++,但是我看他的代码里面基本没有使用什么C++的特性,也很好懂。我就照着书上的写了个Python的。引用的这个 blog 里,作者少总结了一种方法,也就是 键树,我放在了代码里。

# -*- coding=utf-8 -*-


class SearchingAlgorithms(object):
    ''' 七大查找算法 Python 版,所有待查序列里都是按主键来查找,即没有主键相同的两项。 所有的查找算法的空间复杂度都比较低,因为比较之后无需存储比较结果,也无需存储被比较的值。 时间复杂度根据被查找的值波动变化非常大,所以使用平均查找长度 ASL 来衡量算法性能。 静态查找主要包括:顺序查找,折半二分查找,斐波那契查找,插值查找 树查找主要包括:次优查找树,二叉查找树,平衡二叉树,2-3树,红黑树,b树,b+树,键树 按照分块思想的查找有:索引顺序查找,键值查找 除此之外,还有 Hash 查找,使查找时间缩减到 o(1) '''

    def __init__(self):
        pass

    def sequential_search(self, lists, key):
        ''' 顺序查找算法,最简单的一种,静态查找,等概分布,针对没有排序的序列而言。平均查找长度 ASL = (n+1)/2 设立哨兵的方法,避免每次都检测是否检测完整个表,本函数没写哨兵。 :param lists: 待查链表,顺序表等等。 :param key: 待查关键字 :return: 如果查到了,返回索引位置,如果没有查到,返回 -1 '''
        for i in xrange(0, len(lists)):
            if lists[i] == key:
                return i
        return -1

    def binary_search(self, lists, key):
        ''' 二分查找法,又叫折半查找,应用于等概分布的有序序列,利用了判定树,平均查找长度 ASL = (log2 n+1) - 1 '''
        if lists == []:
            return -1
        if len(lists) == 1:
            if key == lists[0]:
                return 0
            else:
                return -1
        lists = sorted(lists)  # 先排序
        print lists  # [-6, 2, 3, 4, 9, 10, 13, 47, 78, 90, 111, 125, 345, 908, 999]
        low, high = 0, len(lists) - 1
        while low < high:
            mid = (low + high) / 2
            if lists[mid] > key:
                high = mid - 1
            if lists[mid] < key:
                low = mid + 1
            if lists[mid] == key:
                return mid
        return -1

    def insertion_search(self, lists, key):
        ''' 插值查找,二分折半查找的改进型,应用于等概分布,其实计算 mid 值消耗的时间也比较多,因为有很多乘除法运算。 适用于有序的,均匀分布的,非常大数据量的查找。 '''
        if lists == []:
            return -1
        if len(lists) == 1:
            if key == lists[0]:
                return 0
            else:
                return -1
        lists = sorted(lists)  # 先排序
        low, high = 0, len(lists) - 1
        while low < high:
            if key - lists[low] < 0:  # 以免出现回退
                return -1
            mid = low + (key - lists[low]) / (lists[high] - lists[low]) * (high - low)
            if lists[mid] > key:
                high = mid - 1
            if lists[mid] < key:
                low = mid + 1
            if lists[mid] == key:
                return mid
        return -1

    def fibonacci_search(self, lists, key):
        ''' 斐波那契查找算法,插值查找的一种,针对有序静态查找表来做。整体性能优于折半查找,但是在最坏情况下,比折半查找要差。 相较于插值查找,不需要做乘除法运算,应用于等概分布 '''
        if lists == []:
            return -1
        if len(lists) == 1:
            if key == lists[0]:
                return 0
            else:
                return -1
        lists = sorted(lists)  # 先排序

        # [0, 1, 1, 2, 3, 5, 8, 13, 21, 34,...]
        def fibonacci(n):
            if n == 0:
                return 0
            if n == 1:
                return 1
            f = [0]
            f.append(1)
            for i in xrange(2, n + 1):
                f.append(f[i - 1] + f[i - 2])
            return f.pop()

        low, high = 0, len(lists)-1
        i = 1
        while fibonacci(i) < len(lists):
            i += 1
        if key == lists[high]+1:
            return -1
        for _ in xrange(len(lists), fibonacci(i)):
            lists.append(lists[high]+1)
        high = len(lists) - 1
        #print "fibonacci: ", i, high, fibonacci(i)
        while low <= high:
            mid = fibonacci(i-1) + low
            if lists[mid] == key:
                return mid
            if lists[mid] < key:
                low = mid + 1
                i -= 2
            if lists[mid] > key:
                high = mid - 1
                i -= 1
        return -1

    def binary_search_tree_search(self, lists, key):
        ''' 二叉查找树,又称为二叉排序树,对于每一个节点,其左子节点一定比其小,右子节点一定比其大。一般采用二叉链表作为存储结构。 由于构建 BST 的过程是个动态的过程,所以二叉查找树是一个动态查找表的查找。 二叉树如果中序遍历,其结果就是有序数组。 平均查找长度是 2(1+ 1/n)(ln n), 时间复杂度为 o(log2 n),在最坏情况下,是 o(n),此时需要平衡化处理。 一、每次添加一个结点,一定是在 BST的叶子节点上,每次插入都只需修改某个叶子节点的指针即可。 二、每次删除一个结点, 1、若是叶子节点,由于其没有子树,所以只需修改其父节点的指针即可。 2、若是只有左子树或者右子树,则分情况讨论,若该结点是其双亲结点的左子树结点,则让这颗子树成为其父节点的左子树; 若该结点是其双亲结点的右子树结点,则让这颗子树成为其父结点的右子树。 3、若是左右子树都存在,则有两种处理方法,具体需要看图和代码来理解。 '''

    def balanced_binary_tree_search(self, lists, key):
        ''' 平衡二叉树,又叫 AVL 树,左右子树都是平衡二叉树,且左右子树的深度之差不超过 1。 由于二叉查找树性能恶化的原因在于树趋近于链表,也就是左右不平衡,所以平衡二叉树能够改善 BST的不稳定性能。 其平均查找时间同样是 o(log2 n),且效果比 BST 稳定。 将二叉排序树改造成平衡二叉树的方法,也就是插入和删除的时候保持平衡,较为复杂,书本上有就不细说了。 平衡二叉树分为 平衡2-3树和平衡红黑树,效率都比较高,但是真复杂啊。红黑树是 2-3树的一种简单实现。 平衡2-3树: "2-3查找树定义":和二叉树不一样,2-3树运行每个节点保存1个或者两个的值。对于普通的2节点(2-node),他保存1个key和左右两个自己点。 对应3节点(3-node),保存两个Key,2-3查找树的定义如下: 1)要么为空,要么: 2)对于2节点,该节点保存一个key及对应value,以及两个指向左右节点的节点,左节点也是一个2-3节点,所有的值都比key要小,右节点也 是一个2-3节点,所有的值比key要大。 3)对于3节点,该节点保存两个key及对应value,以及三个指向左中右的节点。左节点也是一个2-3节点,所有的值均比两个key中的最小的 key还要小;中间节点也是一个2-3节点,中间节点的key值在两个跟节点key值之间;右节点也是一个2-3节点,节点的所有key值比两个 key中的最大的key还要大。 "2-3查找树的性质": 1)如果中序遍历2-3查找树,就可以得到排好序的序列; 2)在一个完全平衡的2-3查找树中,根节点到每一个为空节点的距离都相同。(这也是平衡树中“平衡”一词的概念,根节点到叶节点的最长距离 对应于查找算法的最坏情况,而平衡树中根节点到叶节点的距离都一样,最坏情况也具有对数复杂度。 平衡红黑树: 2-3查找树能保证在插入元素之后能保持树的平衡状态,最坏情况下即所有的子节点都是2-node,树的高度为lgn,从而保证了最坏情况下的时 间复杂度。但是2-3树实现起来比较复杂,于是就有了一种简单实现2-3树的数据结构,即红黑树(Red-Black Tree)。    基本思想:红黑树的思想就是对2-3查找树进行编码,尤其是对2-3查找树中的3-nodes节点添加额外的信息。红黑树中将节点之间的链接分为 两种不同类型,红色链接,他用来链接两个2-nodes节点来表示一个3-nodes节点。黑色链接用来链接普通的2-3节点。特别的,使用红色链接的两个 2-nodes来表示一个3-nodes节点,并且向左倾斜,即一个2-node是另一个2-node的左子节点。这种做法的好处是查找的时候不用做任何修改,和 普通的二叉查找树相同。 “红黑树的定义”:红黑树是一种具有红色和黑色链接的平衡查找树,同时满足: 1、红色节点向左倾斜 2、一个节点不可能有两个红色链接 3、整个树完全黑色平衡,即从根节点到所以叶子结点的路径上,黑色链接的个数都相同。    下图可以看到红黑树其实是2-3树的另外一种表现形式:如果我们将红色的连线水平绘制,那么他链接的两个2-node节点就是2-3树中的一个 3-node节点了。 “红黑树的性质”:整个树完全黑色平衡,即从根节点到所以叶子结点的路径上,黑色链接的个数都相同(2-3树的第2)性质,从根节点到叶子节 点的距离都相等)。 “复杂度分析”:最坏的情况就是,红黑树中除了最左侧路径全部是由3-node节点组成,即红黑相间的路径长度是全黑路径长度的2倍。 '''

    def b_tree_search(self,lists, key):
        ''' B树查找:B树(B-tree)是一种树状数据结构,它能够存储数据、对其进行排序并允许以O(log n)的时间复杂度运行进行查找、顺序读取、插入和\ 删除的数据结构。B树,概括来说是一个节点可以拥有多于2个子节点的二叉查找树。与自平衡二叉查找树不同,B树为系统最优化大块数据的读和写操作。\ B-tree算法减少定位记录时所经历的中间过程,从而加快存取速度。普遍运用在数据库和文件系统。 “B树定义”: B树可以看作是对 2-3查找树的一种扩展,即他允许每个节点有 M-1个子节点。根节点至少有两个子节点,每个节点有 M-1个key,并且以升序排\ 列,位于 M-1和 M key的子节点的值位于 M-1 和 M key对应的 Value之间,其它节点至少有 M/2个子节点。 B+树定义: B+树是对B树的一种变形树,它与B树的差异在于: 1、有k个子结点的结点必然有k个关键码; 2、非叶结点仅具有索引作用,跟记录有关的信息均存放在叶结点中。 3、树的所有叶结点构成一个有序链表,可以按照关键码排序的次序遍历全部记录。 B和 B+树的区别在于,B+树的非叶子结点只包含导航信息,不包含实际的值,所有的叶子结点和相连的节点使用链表相连,便于区间查找和遍历。 B+ 树的优点在于: 由于B+树在内部节点上不好含数据信息,因此在内存页中能够存放更多的key。 数据存放的更加紧密,具有更好的空间局部性。因此访问叶子几点上\ 关联的数据也具有更好的缓存命中率。B+树的叶子结点都是相链的,因此对整棵树的便利只需要一次线性遍历叶子结点即可。而且由于数据顺序排列并且相连,\ 所以便于区间查找和搜索。而B树则需要进行每一层的递归遍历。相邻的元素可能在内存中不相邻,所以缓存命中性没有B+树好。   但是B树也有优点,其优点在于,由于B树的每一个节点都包含key和value,因此经常访问的元素可能离根节点更近,因此访问也更迅速。 B/B+树常用于文件系统和数据库系统中,它通过对每个节点存储个数的扩展,使得对连续的数据能够进行较快的定位和访问,能够有效减少查找时\ 间,提高存储的空间局部性从而减少IO操作。它广泛用于文件系统及数据库中,如: Windows:HPFS文件系统; Mac:HFS,HFS+文件系统; Linux:ResiserFS,XFS,Ext3FS,JFS文件系统; 数据库:ORACLE,MYSQL,SQLSERVER等中。 '''

    def digital_search_tree_search(self, lists, key):
        ''' 键树,又称数字查找树,利用的基本思想类似于我们查英文字典。也就是把序列中的关键字拆分成若干部分,这些部分可作为每一层键树种的结点。\ 组合起来之后即可得到要查询的关键字,最典型的应用也是查找英文单词。事先将所有的英文单词组织成一颗键树,达到效率最高。 键树的存储结构: 1、多重链表,最容易想到的存储结构; 2、孩子兄弟链表来表示键树,左子节点是孩子,右子节点是兄弟。两种表示方法即普通树和二叉树之间的互换。 '''

    def hash_search(self, lists, key):
        ''' 什么是哈希表(Hash)? 我们使用一个下标范围比较大的数组来存储元素。可以设计一个函数(哈希函数, 也叫做散列函数),使得每个元素的关键字都与一个函数值(即数\ 组下标)相对应,于是用这个数组单元来存储这个元素;也可以简单的理解为,按照关键字为每一个元素"分类",然后将这个元素存储在相应"类"所对应的地\ 方。但是,不能够保证每个元素的关键字与函数值是一一对应的,因此极有可能出现对于不同的元素,却计算出了相同的函数值,这样就产生了"冲突",换句话\ 说,就是把不同的元素分在了相同的"类"之中。后面我们将看到一种解决"冲突"的简便做法。 总的来说,"直接定址"与"解决冲突"是哈希表的两大特点。 什么是哈希函数? 哈希函数的规则是:通过某种转换关系,使关键字适度的分散到指定大小的的顺序结构中,越分散,则以后查找的时间复杂度越小,空间复杂度越高。 算法思想:哈希的思路很简单,如果所有的键都是整数,那么就可以使用一个简单的无序数组来实现:将键作为索引,值即为其对应的值,这样就可\ 以快速访问任意键的值。这是对于简单的键的情况,我们将其扩展到可以处理更加复杂的类型的键。 算法流程:   1)用给定的哈希函数构造哈希表;   2)根据选择的冲突处理方法解决地址冲突;     常见的解决冲突的方法:拉链法和线性探测法。   3)在哈希表的基础上执行哈希查找。   哈希表是一个在时间和空间上做出权衡的经典例子。如果没有内存限制,那么可以直接将键作为数组的索引。那么所有的查找时间复杂度为O(1);如\ 果没有时间限制,那么我们可以使用无序数组并进行顺序查找,这样只需要很少的内存。哈希表使用了适度的时间和空间来在这两个极端之间找到了平衡。只需\ 要调整哈希函数算法即可在时间和空间上做出取舍。    复杂度分析:   单纯论查找复杂度:对于无冲突的Hash表而言,查找复杂度为O(1)(注意,在查找之前我们需要构建相应的Hash表)。   使用Hash,我们付出了什么?   我们在实际编程中存储一个大规模的数据,最先想到的存储结构可能就是map,也就是我们常说的KV pair,经常使用Python的博友可能更有这种体\ 会。使用map的好处就是,我们在后续处理数据处理时,可以根据数据的key快速的查找到对应的value值。map的本质就是Hash表,那我们在获取了超高查找\ 效率的基础上,我们付出了什么?   Hash是一种典型以空间换时间的算法,比如原来一个长度为100的数组,对其查找,只需要遍历且匹配相应记录即可,从空间复杂度上来看,假如数\ 组存储的是byte类型数据,那么该数组占用100byte空间。现在我们采用Hash算法,我们前面说的Hash必须有一个规则,约束键与存储位置的关系,那么就需\ 要一个固定长度的hash表,此时,仍然是100byte的数组,假设我们需要的100byte用来记录键与位置的关系,那么总的空间为200byte,而且用于记录规则\ 的表大小会根据规则,大小可能是不定的。 '''

search = SearchingAlgorithms()
l = [4, 90, 125, 2, 9, 47, 999, -6, 111, 908, 13, 78, 345, 3, 10,909]
print "Sequential search: ", search.sequential_search(l, 10)
print "Binary search: ", search.binary_search([101], 101)
print "Insertion search: ", search.insertion_search(l, 907)
print "Fibonacci search: ", search.fibonacci_search(l, 4)
print search.b_tree_search.__doc__
    原文作者:查找算法
    原文地址: https://blog.csdn.net/dongrixinyu/article/details/78639349
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞