查找

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

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

查找结构

静态:线性表

动态:二叉排序树、哈希

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

原文地址:https://www.cnblogs.com/chaojunwang-ml/p/11285823.html