zoukankan      html  css  js  c++  java
  • 查找

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

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

    查找结构

    静态:线性表

    动态:二叉排序树、哈希

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

  • 相关阅读:
    第八章 多线程编程
    Linked List Cycle II
    Swap Nodes in Pairs
    Container With Most Water
    Best Time to Buy and Sell Stock III
    Best Time to Buy and Sell Stock II
    Linked List Cycle
    4Sum
    3Sum
    Integer to Roman
  • 原文地址:https://www.cnblogs.com/chaojunwang-ml/p/11285823.html
Copyright © 2011-2022 走看看