数据结构与算法基础-第6章 搜索算法

第6章 搜索算法

搜索是计算机科学中最基本的操作之一,它的目标是在数据集合中找到特定的元素或判断元素是否存在。搜索算法的性能直接影响程序的整体效率。本章将介绍多种搜索算法,从简单的线性搜索到高效的哈希表。

6.1 线性搜索

线性搜索是最直观的搜索方法,它从数据集合的第一个元素开始,依次检查每个元素,直到找到目标元素或遍历完整个集合。线性搜索适用于任何类型的数据集合,不需要数据有任何特殊的组织形式。

def linear_search(arr, target):  
    for i in range(len(arr)):  
        if arr[i] == target:  
            return i  
    return -1  

def linear_search_with_count(arr, target):  
    comparisons = 0  
    for i in range(len(arr)):  
        comparisons += 1  
        if arr[i] == target:  
            return i, comparisons  
    return -1, comparisons  

data = [3, 7, 2, 9, 5, 1, 8, 4, 6]  
target = 5  
index, count = linear_search_with_count(data, target)  
print(f"在位置{index}找到{target}, 比较次数: {count}")

线性搜索的时间复杂度为O(n),在最坏情况下需要遍历所有n个元素。线性搜索的空间复杂度为O(1),不需要额外的存储空间。线性搜索适用于小规模数据、无序数据以及只需要偶尔搜索的场景。

Linear Search Animation

虽然线性搜索效率不高,但它在某些场景下仍然有用。当数据规模很小,或者数据经常变化导致无法维护有序结构时,线性搜索是最简单可行的方案。线性搜索也可以用于查找满足特定条件的元素,而不只是精确匹配。

6.2 二分搜索

Binary Search Animation

二分搜索是一种高效的搜索算法,它利用数据的有序性,每次将搜索范围缩小一半。二分搜索要求数据集合必须有序,可以是升序或降序排列。算法从中间元素开始比较,如果目标元素小于中间元素,则在左半部分继续搜索;如果大于中间元素,则在右半部分继续搜索。

def binary_search(arr, target):  
    left, right = 0, len(arr) - 1  
      
    while left <= right:  
        mid = (left + right) // 2  
          
        if arr[mid] == target:  
            return mid  
        elif arr[mid] < target:  
            left = mid + 1  
        else:  
            right = mid - 1  
      
    return -1  

def binary_search_recursive(arr, target, left=0, right=None):  
    if right is None:  
        right = len(arr) - 1  
      
    if left > right:  
        return -1  
      
    mid = (left + right) // 2  
      
    if arr[mid] == target:  
        return mid  
    elif arr[mid] < target:  
        return binary_search_recursive(arr, target, mid + 1, right)  
    else:  
        return binary_search_recursive(arr, target, left, mid - 1)  

sorted_data = [1, 2, 3, 4, 5, 6, 7, 8, 9]  
target = 6  
index = binary_search(sorted_data, target)  
print(f"在有序数组的位置{index}找到{target}")

二分搜索的时间复杂度为O(log n),因为每次搜索范围缩小一半,最多需要log n次比较。空间复杂度在迭代版本中为O(1),在递归版本中为O(log n),因为需要递归栈空间。

二分搜索相比线性搜索有巨大的性能优势。在100万元素的数组中,线性搜索平均需要50万次比较,二分搜索最多只需要20次比较。二分搜索广泛应用于有序数组、字典查找、数据库索引等场景。

二分搜索还可以用于查找满足特定条件的位置。比如查找第一个大于目标元素的位置,或查找最后一个小于目标元素的位置。这些变种在实际应用中经常遇到。

def find_first_greater(arr, target):  
    left, right = 0, len(arr)  
    result = -1  
      
    while left < right:  
        mid = (left + right) // 2  
          
        if arr[mid] > target:  
            result = mid  
            right = mid  
        else:  
            left = mid + 1  
      
    return result  

def find_last_less(arr, target):  
    left, right = 0, len(arr)  
    result = -1  
      
    while left < right:  
        mid = (left + right) // 2  
          
        if arr[mid] < target:  
            result = mid  
            left = mid + 1  
        else:  
            right = mid  
      
    return result

6.3 哈希表基础

Hash Table Animation

哈希表是一种极其高效的搜索数据结构,它可以在O(1)平均时间复杂度内完成搜索、插入和删除操作。哈希表使用哈希函数将键映射到数组索引,通过索引直接访问数据。

哈希函数是哈希表的核心,它将任意大小的输入转换为固定范围的输出。好的哈希函数应该满足几个性质。确定性是指相同的输入总是产生相同的输出。均匀性是指哈希值在输出范围内均匀分布,避免集中。高效性是指计算哈希值的时间复杂度应该是O(1)。

def simple_hash(key, size):  
    return hash(key) % size  

def string_hash(s, size):  
    hash_value = 0  
    for char in s:  
        hash_value = (hash_value * 31 + ord(char)) % size  
    return hash_value  

class SimpleHashTable:  
    def __init__(self, size=10):  
        self.size = size  
        self.table = [None] * size  
        self.count = 0  
      
    def hash(self, key):  
        return hash(key) % self.size  
      
    def insert(self, key, value):  
        if self.count >= self.size * 0.7:  
            self.resize()  
          
        index = self.hash(key)  
        while self.table[index] is not None:  
            if self.table[index][0] == key:  
                self.table[index] = (key, value)  
                return  
            index = (index + 1) % self.size  
          
        self.table[index] = (key, value)  
        self.count += 1  
      
    def get(self, key):  
        index = self.hash(key)  
        start_index = index  
          
        while self.table[index] is not None:  
            if self.table[index][0] == key:  
                return self.table[index][1]  
            index = (index + 1) % self.size  
            if index == start_index:  
                break  
          
        return None  
      
    def resize(self):  
        old_table = self.table  
        self.size *= 2  
        self.table = [None] * self.size  
        self.count = 0  
          
        for item in old_table:  
            if item is not None:  
                self.insert(item[0], item[1])  

hash_table = SimpleHashTable()  
hash_table.insert('apple', 10)  
hash_table.insert('banana', 20)  
hash_table.insert('cherry', 30)  
print(f"apple的值: {hash_table.get('apple')}")  
print(f"banana的值: {hash_table.get('banana')}")

哈希表的性能在很大程度上取决于哈希函数的质量。Python内置的hash函数已经经过精心设计,可以满足大多数应用场景。对于自定义哈希函数,需要特别注意均匀性和避免哈希冲突。

6.4 哈希冲突处理

哈希冲突是指不同的键被哈希函数映射到相同的索引位置。因为哈希函数的输出范围有限,而输入范围无限,哈希冲突不可避免。处理哈希冲突的方法主要有两种,链地址法和开放地址法。

链地址法将哈希表的每个位置变成一个链表,所有哈希到同一位置的键都存储在同一个链表中。这种方法简单直观,处理冲突不需要重新计算位置。但链表可能很长,导致搜索效率降低。

class ChainingHashTable:  
    def __init__(self, size=10):  
        self.size = size  
        self.table = [[] for _ in range(size)]  
      
    def hash(self, key):  
        return hash(key) % self.size  
      
    def insert(self, key, value):  
        index = self.hash(key)  
        for i, (k, v) in enumerate(self.table[index]):  
            if k == key:  
                self.table[index][i] = (key, value)  
                return  
        self.table[index].append((key, value))  
      
    def get(self, key):  
        index = self.hash(key)  
        for k, v in self.table[index]:  
            if k == key:  
                return v  
        return None  
      
    def delete(self, key):  
        index = self.hash(key)  
        for i, (k, v) in enumerate(self.table[index]):  
            if k == key:  
                self.table[index].pop(i)  
                return True  
        return False

开放地址法在发生冲突时,按照一定规则寻找下一个空闲位置。线性探测是最简单的开放地址法,它依次检查下一个位置,直到找到空闲位置。线性探测容易产生聚集现象,导致连续区域被占用。

class LinearProbingHashTable:  
    def __init__(self, size=10):  
        self.size = size  
        self.keys = [None] * size  
        self.values = [None] * size  
        self.deleted = [False] * size  
      
    def hash(self, key):  
        return hash(key) % self.size  
      
    def find_slot(self, key):  
        index = self.hash(key)  
        start_index = index  
          
        while self.keys[index] is not None and not self.deleted[index]:  
            if self.keys[index] == key:  
                return index, True  
            index = (index + 1) % self.size  
            if index == start_index:  
                return -1, False  
          
        return index, False  
      
    def insert(self, key, value):  
        index, found = self.find_slot(key)  
          
        if index == -1:  
            raise Exception("哈希表已满")  
          
        if found:  
            self.values[index] = value  
        else:  
            self.keys[index] = key  
            self.values[index] = value  
            self.deleted[index] = False  
      
    def get(self, key):  
        index, found = self.find_slot(key)  
        if found:  
            return self.values[index]  
        return None  
      
    def delete(self, key):  
        index, found = self.find_slot(key)  
        if found:  
            self.deleted[index] = True  
            return True  
        return False

二次探测和双重哈希是开放地址法的改进版本,它们可以减少聚集现象。二次探测使用二次函数计算下一个位置,双重哈希使用第二个哈希函数计算步长。这些方法可以更均匀地分布元素,减少冲突链的长度。

6.5 二叉搜索树的搜索

二叉搜索树是一种支持高效搜索的树形数据结构。在二叉搜索树中,每个节点的左子树所有节点值都小于该节点,右子树所有节点值都大于该节点。这种性质使得搜索过程可以快速缩小范围。

class BSTNode:  
    def __init__(self, key, value=None):  
        self.key = key  
        self.value = value  
        self.leftNone  
        self.rightNone  

class BinarySearchTree:  
    def __init__(self):  
        self.rootNone  
      
    def search(self, key):  
        return self._search(self.root, key)  
      
    def _search(self, node, key):  
        if node is None:  
            return None  
          
        if key == node.key:  
            return node.value  
        elif key < node.key:  
            return self._search(node.left, key)  
        else:  
            return self._search(node.right, key)  
      
    def insert(self, key, value=None):  
        self.root = self._insert(self.root, key, value)  
      
    def _insert(self, node, key, value):  
        if node is None:  
            return BSTNode(key, value)  
          
        if key < node.key:  
            node.left = self._insert(node.left, key, value)  
        elif key > node.key:  
            node.right = self._insert(node.right, key, value)  
        else:  
            node.value = value  
          
        return node  
      
    def find_min(self):  
        return self._find_min(self.root)  
      
    def _find_min(self, node):  
        if node is None:  
            return None  
        while node.left:  
            node = node.left  
        return node.key  
      
    def find_max(self):  
        return self._find_max(self.root)  
      
    def _find_max(self, node):  
        if node is None:  
            return None  
        while node.right:  
            node = node.right  
        return node.key  

bst = BinarySearchTree()  
bst.insert(50'A')  
bst.insert(30'B')  
bst.insert(70'C')  
bst.insert(20'D')  
bst.insert(40'E')  

print(f"搜索30: {bst.search(30)}")  
print(f"最小值: {bst.find_min()}")  
print(f"最大值: {bst.find_max()}")

BST Search Animation

二叉搜索树的搜索时间复杂度取决于树的形状。在平衡的二叉搜索树中,搜索时间复杂度为O(log n)。在最坏情况下,树退化为链表,搜索时间复杂度为O(n)。为了保证性能,可以使用自平衡二叉搜索树如AVL树或红黑树。

二叉搜索树不仅支持搜索,还支持范围查询。可以查找所有在某个范围内的元素,通过递归遍历树的适当部分实现。这种操作在数据库索引和文件系统中经常使用。

6.6 高级搜索技术

除了基本的搜索算法,还有一些高级技术可以提高搜索效率或处理特殊场景。

跳表是一种支持快速搜索的数据结构,它通过多层索引加速搜索过程。跳表在链表基础上建立多层跳跃指针,高层指针跳过多个节点,低层指针逐步细化。跳表的搜索时间复杂度为O(log n),比链表的O(n)快得多。

import random  

class SkipListNode:  
    def __init__(self, key, level):  
        self.key = key  
        self.forward = [None] * level  

class SkipList:  
    def __init__(self, max_level=4, p=0.5):  
        self.max_level = max_level  
        self.p = p  
        self.level = 1  
        self.head = SkipListNode(-1, max_level)  
      
    def random_level(self):  
        level = 1  
        while random.random() < self.p and level < self.max_level:  
            level += 1  
        return level  
      
    def insert(self, key):  
        update = [None] * self.max_level  
        current = self.head  
          
        for i in range(self.level - 1, -1, -1):  
            while current.forward[i] and current.forward[i].key < key:  
                current = current.forward[i]  
            update[i] = current  
          
        lvl = self.random_level()  
          
        if lvl > self.level:  
            for i in range(self.level, lvl):  
                update[i] = self.head  
            self.level = lvl  
          
        new_node = SkipListNode(key, lvl)  
          
        for i in range(lvl):  
            new_node.forward[i] = update[i].forward[i]  
            update[i].forward[i] = new_node  
      
    def search(self, key):  
        current = self.head  
          
        for i in range(self.level - 1, -1, -1):  
            while current.forward[i] and current.forward[i].key < key:  
                current = current.forward[i]  
          
        current = current.forward[0]  
          
        if current and current.key == key:  
            return True  
        return False  

skip_list = SkipList()  
for num in [3, 6, 7, 9, 12, 19, 17, 26, 21, 25]:  
    skip_list.insert(num)  

print(f"搜索19: {skip_list.search(19)}")  
print(f"搜索20: {skip_list.search(20)}")

Skip List Animation

布隆过滤器是一种空间效率极高的概率型数据结构,用于判断元素是否在集合中。布隆过滤器可能产生误报,但不会产生漏报。当布隆过滤器判断元素存在时,元素可能实际不存在;当判断元素不存在时,元素一定不存在。布隆过滤器适用于可以容忍误报的场景,如缓存过滤、垃圾邮件检测等。

import hashlib  

class BloomFilter:  
    def __init__(self, size=1000, hash_count=3):  
        self.size = size  
        self.hash_count = hash_count  
        self.bit_array = [False] * size  
      
    def add(self, item):  
        for i in range(self.hash_count):  
            index = self._hash(item, i) % self.size  
            self.bit_array[index] = True  
      
    def might_contain(self, item):  
        for i in range(self.hash_count):  
            index = self._hash(item, i) % self.size  
            if not self.bit_array[index]:  
                return False  
        return True  
      
    def _hash(self, item, i):  
        return int(hashlib.md5(f'{item}{i}'.encode()).hexdigest(), 16)  

bloom = BloomFilter()  
bloom.add('apple')  
bloom.add('banana')  

print(f"'apple' 可能存在: {bloom.might_contain('apple')}")  
print(f"'orange' 可能存在: {bloom.might_contain('orange')}")

Bloom Filter Animation


本章简单提了多种搜索算法。线性搜索简单但效率低,适用于小规模数据。二分搜索效率高但要求有序数据。哈希表提供接近O(1)的搜索效率,是实际应用中最重要的搜索结构。二叉搜索树结合了数组的灵活性和搜索的效率。跳表和布隆过滤器等高级技术在特定场景下有重要应用。下一章我们将学习动态规划,一种强大的算法设计技术。

预览时标签不可点