• 查找


    静态查找:数据集合稳定,不需要添加删除元素的查找操作。

    动态查找:查找过程中需要同时添加或删除元素。

    查找结构

    静态:线性表

    动态:二叉排序树、哈希

    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)。存储和查找的时候使用的哈希函数相同即可。但可能出现一个哈希地址对应多个记录的情况,即哈希冲突。在设计哈希函数时要令哈希冲突尽可能少。

  • 相关阅读:
    java扫描文件夹下面的所有文件(递归与非递归实现)
    JAVA8 十大新特性详解
    Intellij IDEA创建的Web项目配置Tomcat并启动Maven项目
    Spring官网下载各版本jar包
    史上最全Java面试题(带全部答案)
    深入JVM对象引用
    23种设计模式全解析
    git difftool和mergetool图形化
    Java技术——你真的了解String类的intern()方法吗
    动手实现一个vue中的模态对话框组件
  • 原文地址:https://www.cnblogs.com/chaojunwang-ml/p/11285823.html
Copyright © 2020-2023  润新知