机器学习1002_Python基础_数据结构与算法_查找

本文介绍了三种常见的查找算法:顺序查找、二分查找和哈希表查找。通过Python代码展示了它们的实现和效率分析。顺序查找在有序和无序列表中的时间复杂度为O(N);二分查找的时间复杂度为O(log n);哈希表查找理论上时间复杂度为O(1),但在处理哈希冲突时可能会增加到O(N)。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

我们只看常用的三种算法:顺序查找(sequential search)、二分查找(binary_search)、哈希表查找(hashing)。并简单分析三种算法的效率。
1. 顺序查找:从头向尾逐次查找,直到找到为止。我们使用Python实现顺序查找功能。
import time
def sequential_search(a_list,item):
    pos = 0  # 当前指针位置
    found = False
    # 如果指针未历遍列表,且当前位置以前未找到该元素,则
    while pos < len(a_list) and not found:
        if a_list[pos] == item:  # 如果当前位置是需要查找的元素
            found = True
        else:
            pos = pos + 1  # 否则指针指向下一个元素
    return found

test_list = [1,2,32,8,17,19,42,13,0]
print(sequential_search(test_list,3))
print(sequential_search(test_list,13))
上述是通用的顺序查找算法,针对于一些特殊的列表,如顺序列表(order list),顺序查找算法做出一些微小的调整:如果当前位置的元素大于要查找的元素,则停止,不必再向后检索,因为后面的元素肯定都大于要查找的元素。下面我们来看一个例子。
import time
def ordered_sequential_search(a_list,item):
    pos = 0  # 当前指针位置
    found = False
    stop = False
    while pos < len(a_list) and not found and not stop:
        if a_list[pos] == item:
            found = True
        else:
            if a_list[pos] > item:
                stop = True  # 如果当前位置的元素大于要查找的元素,则停止
            else:
                pos = pos + 1  # 指针指向下一个元素
    return found

test_list = [0,2,3,8,13,17,19,32,42]
print(ordered_sequential_search(test_list,1))
print(ordered_sequential_search(test_list,32))

2. 二分查找:在一个顺序列表中,从中位数开始查找,并比较所查找的元素与中位数的大小,将列表二分为含查找元素的区间C1,以及不含查找元素的区间C2。然后再对C1进行二分,直到找到为止。
例如有一个列表[0,2,3,8,13,17,19,32,42],假设需要查找到32,若使用顺序查找法,要先历遍0,2,3,8,13,17,19共7个元素。若使用二分法,则首先找到列表中点(0+9)//2=4(实际上是第5个元素13)。
此时中点为13,小于需要查找的32,所以32应该在区间[17,19,32,42]中。
再次查找中点(0+3)//2 = 1 (实际上是第2个元素)。此时中点为19,小于需要查找的32,所以32应该在区间[32,42]中。
再次查找中点(0+1)//2 = 0 (实际上是第1个元素)。此时中点为32,我们找到了需要的元素。
下面是实现二分查找的Python代码:
import time
def binary_search(a_list,item):
    starttime = time.clock()
    first = 0  # 当前指针位置
    last = len(a_list)-1
    found = False

    while first <= last and not found:
        midpoint = (first + last) // 2
        if a_list[midpoint] == item:
            found = True
        else:
            if item < a_list[midpoint]:  # 如果查找元素小于中点
                last = midpoint - 1  # 那么中点作为新的上界
            else:
                first = midpoint + 1  # 否则作为新的下界
    endtime = time.clock()
    return  found,(endtime-starttime)

test_list = [0,2,3,8,13,17,19,32,42]
print(binary_search(test_list,1))
print(binary_search(test_list,32))

3. 哈希表查找
无论顺序法还是二分法,都无法事先知道我们需要查找的元素的准确位置,因此查找速度较慢。那么我们是否可以通过一些计算事先找到元素的位置,直接到该位置获取元素的值呢?试想一下,如果在列表[1,2,3,4]中,我们事先知道3在第三位,那么直接到第三个位置取值即可,无须从1,2找起,也无需进行二分。
Python里一种最常用的数据结构字典(dictionary)就使用了这种算法。字典内有键(key)和值(data),如dict= {‘1’:’li’, ‘2’:’chen’},当我们需要找学号为2的同学时,程序直接到储存’chen’的位置获取值。
下面我们来看哈希表查找更加具体的原理:
首先我们有一个空的哈希表
现在有一组值item[54,26,93,17,77,31]想要放入哈希表中
我们令h(item) = item % 11,如54%11=10,则放至编号为10的位置,则我们有了一个放入了数值的哈希表
现在我们有了放入数值的哈希表,如果我们需要找学号为54的同学,则只需计算54%11=10,即可直接到编号为10的狭槽(slot)获取值。无须从77开始找起。
哈希表具有一个严重的缺陷:哈希值冲突。例如44%11=0,77%11=0,两个数值被添加到相同狭槽。影响我们查找的结果。
为了解决哈希值冲突的问题,有一个方法便是寻找一个完美的哈希值计算公式,以及足够大的哈希表,保证每一个可能的放入哈希表的值(item),都有一个特别的slot。
实际上这种做法十分地浪费存储空间。比如一个学号可能长达9位,有一亿种可能,需要准备一亿个slot。
我们的目标是找到一个哈希计算公式,能够最小化哈希值冲突,容易计算,且使item均匀分布在哈希表之中。
1)我们使用的方法是线性探测(linear probing)。我们想要将44加入哈希表中
经过计算44%11=0,发现slot [0] 已经有值,那么我们只需要找到下一个空的slot。即将44放入slot [1] 内。
我们再往里面添加55%11=0,此时slot [0] 里面已经有值,向后位移三位到slot [3],已经有值,向后位移三位到slot [6],已经有值,向后位移三位到slot [9],已经有值,向后位移三位到slot [1],空值,则放进去。
我们继续往里面添加20%11=9,此时slot [9] 里面已经有值,向后位移三位到slot [1],已经有值,向后位移三位到slot [4],已经有值,向后位移三位到slot [7],空值,则放进去。
2)线性探测的一种变形——二次探测(quadratic probing)
直观上很容易理解,如果我们要查找55,则根据55%44=0找到slot [0] 这一列,然后再按照77-44-55的顺序找到55。
3)我们使用python创建一个HashTable类, 实现哈希表查找。这个HashTable类,实际上是简单的字典数据结构。
import time
class HashTable:
    def __init__(self):
        self.size = 11
        self.slots = [None] * self.size  # 创建一个空的哈希表(第一行)
        self.data = [None] * self.size  # 创建一个空的哈希表(第二行)

    def put(self,key,data):
        hash_value = self.hash_function(key,len(self.slots))  # 获得应该放入的slot的编号
        if self.slots[hash_value] == None:  # 如果该slot为空
            self.slots[hash_value] = key  # 则放入该slot
            self.data[hash_value] = data
        else:
            if self.slots[hash_value] == key:  # 如果该slot已有相同的key
                self.data[hash_value] =data  # 则替换其value
            else:
                next_slot = self.rehash(hash_value,len(self.slots))  # 如果该slot非空,则移至下一个slot
                while self.slots[next_slot] != None and self.slots[next_slot] != key:
                    next_slot = self.rehash(next_slot,len(self.slots))  # 知道找到空slot或有相同key的slot

                if self.slots[next_slot] == None:
                    self.slots[next_slot] = key
                    self.data[next_slot] = data
                else:
                    self.data[next_slot] = data  # 替换

    def hash_function(self,key,size):
        return key % size  # 返回slot的编号,如55%11=0

    def rehash(self,old_hash,size):
        return (old_hash + 1) % size  # 返回下一个slot的编号 (0+1)%11=1

    def get(self,key):
        start_slot = self.hash_function(key,len(self.slots))  # 55%11=0

        data = None
        stop = False
        found = False
        position = start_slot
        # 直到找到或历遍哈希表为止
        while self.slots[position] != None and not found and not stop:
            if self.slots[position] == key:
                found = True
                data = self.data[position]
            else:
                position = self.rehash(position,len(self.slots))  # 移到下一个slot
                if position == start_slot:  # 如果已经历遍哈希表,则停止
                    stop = True
        return  data

    def __getitem__(self,key):
        return self.get(key)

    def __setitem__(self,key,data):
        self.put(key,data)

h = HashTable()

h[54] = "cat"  # 自动调用类中定义的方法__setitem__
h[26] = "dog"
h[93] = "lion"
h[17] = "tiger"
h[77] = "bird"
h[31] = "cow"
h[44] = "goat"
h[55] = "pig"
h[20] = "chicken"

print(h.slots)
print(h.data)
print(h[20])

4. 三种方法的分析对比
我们使用大O表示法来体现算法时间复杂度:
O(1)表示该算法的执行时间(或执行时占用空间)总是为一个常量,不论输入的数据集是大是小。
O(N)表示一个算法的性能会随着输入数据的大小变化而线性变化。
……
1)顺序查找:
当列表的元素越多,顺序查找的次数n越大(线性),算法复杂度为O(N)。
当列表的元素越多,顺序查找的次数n越大(线性),算法复杂度为O(N)。
在一个经过排序的列表中查找时
此时算法复杂度仍然为O(N)。
2)二分查找
两分法就是将列表中的元素均分成两部分,直到剩下1个元素为止。如果列表中有n个元素,令计算次数为i,则n/2^(i)=1。例如列表中有2个元素,则2/2^(1)=1,计算1次,有4个元素,则2/2^(2)=1,计算2次。解得i=log2(n)。此时算法复杂度为O(log n)。
3)哈希表查找
如果仅仅是简单的哈希表,算法复杂度仅仅为O(1),因为我们可以经过1次计算(或者常数次)即可找到。哈希表的优点在查找快捷,在数据通信领域常常得到运用。缺点在于对储存空间的消耗。

参考资料:Problem Solving with Algorithms and Data Structures
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值