巴尔加瓦算法图解——第五章 散列表

❑ 学习散列表——最有用的基本数据结构之一。散列表用途广泛,本章将介绍其常见的用途。

❑ 学习散列表的内部机制:实现、冲突和散列函数。这将帮助你理解如何分析散列表的性能。

1. 散列函数

散列函数:数组和链表都被直接映射到内存,但散列表更复杂,它使用散列函数来确定元素的存储位置

要求:

❑ 它必须是一致的。例如,假设你输入apple时得到的是4,那么每次输入apple时,得到的都必须为4。如果不是这样,散列表将毫无用处。

❑ 它应将不同的输入映射到不同的数字。例如,如果一个散列函数不管输入是什么都返回1,它就不是好的散列函数。最理想的情况是,将不同的输入映射到不同的数字。

❑ 散列函数总是将同样的输入映射到相同的索引。每次你输入avocado,得到的都是同一个数字。因此,你可首先使用它来确定将鳄梨的价格存储在什么地方,并在以后使用它来确定鳄梨的价格存储在什么地方。

❑ 散列函数将不同的输入映射到不同的索引。avocado映射到索引4,milk映射到索引0。每种商品都映射到数组的不同位置,让你能够将其价格存储到这里。

❑ 散列函数知道数组有多大,只返回有效的索引。如果数组包含5个元素,散列函数就不会返回无效索引100。

你可能根本不需要自己去实现散列表,任一优秀的语言都提供了散列表实现。Python提供的散列表实现为字典,你可使用函数dict来创建散列表。

book = dict()

book['apple']=0.67

book['milk']=1.49

book['avocado']=1.6

print (book['apple'])

散列表由键和值组成。在前面的散列表book中,键为商品名,值为商品价格。散列表将键映射到值。

【练习】

当说一个散列函数是一致的时,意味着对于相同的输入,这个散列函数总是会产生相同的输出

 f(x)=len(x)  一致,每次都返回这个字符串x的长度。

f(x) = rand () 随机数

2. 应用案例

【映射:将列表用于查找】

假设你要创建一个类似这样的电话簿,将姓名映射到电话号码。该电话簿需要提供如下功能。

❑ 添加联系人及其电话号码。

❑ 通过输入联系人来获悉其电话号码。这非常适合使用散列表来实现!在下述情况下,使用散列表是很不错的选择。

❑ 创建映射。

❑ 查找。

phone_book=dict()

phone_book['Hym']=111222

phone_book['feifei']=2222222

phone_book['ashley']=111111

print(phone_book['ashley'])

散列表被用于大海捞针式的查找,将网址映射到IP地址。例如,你在访问像http://adit.io这样的网站时,计算机必须将adit.io转换为IP地址。

【防止重复,区分集合和列表】

1. 集合

voted = {'Ashley', 'Hym', 'Feifei'}



def check_voter(name):

    if name in voted:

        print("Kick them out!")

    else:

        voted.add(name)

        print("Let them vote!")

check_voter('Tony')

2. 列表

在Python中,列表使用方括号 `[]` 来表示。列表是一种有序、可变的数据类型,可以包含多种数据类型的元素,包括整数、浮点数、字符串等。

  

voted = ['Ashley', 'Hym', 'Feifei']



def check_voter(name):

    if name in voted:

        print("Kick them out!")

    else:

        voted.append(name) #.append

        print("Let them vote!")

check_voter('Tony')

【比较列表[]和集合字典{}】

操作

集合

列表

创建

my_set = {1, 2, 3}

my_list = [1, 2, 3, 'hello', 4.5]

添加

my_set.add(4)

my_list.append(6) 到末尾

删除

my_set.remove(2)  # 如果元素不存在,会抛出 KeyError

my_set.discard(2)  # 如果元素不存在,不会抛出错误

my_set.pop()  # 随机删除一个元素

 del my_list[1]

集合操作

intersection_set = set1 & set2  # 或者使用 intersection_set = set1.intersection(set2)

difference_set = set1 - set2  # 或者使用 difference_set = set1.difference(set2)

symmetric_difference_set = set1 ^ set2  # 或者使用symmetric_difference_set = set1.symmetric_difference(set2)

set1 = {1, 2, 3}

set2 = {3, 4, 5}

union_set = set1 | set2  # 或者使用 union_set = set1.union(set2)

插入

 my_list.insert(2,'new_element')

移动

my_list.remove('hello')

获取列表长度

len()

切片

 sublist = my_list[1:3]  # 获取索引1到2的子列表

列表合并

new_list = my_list + [7, 8, 9]

【将散列表用作缓存cache】

访问facebook网站

(1)你向Facebook的服务器发出请求。(2)服务器做些处理,生成一个网页并将其发送给你。(3)你获得一个网页。

例如,Facebook的服务器可能搜集你朋友的最近活动,以便向你显示这些信息,这需要几秒钟的时间。作为用户的你,可能感觉这几秒钟很久,进而可能认为Facebook怎么这么慢!另一方面,Facebook的服务器必须为数以百万的用户提供服务,每个人的几秒钟累积起来就相当多了。为服务好所有用户,Facebook的服务器实际上在很努力地工作。有没有办法让Facebook的服务器少做些工作,从而提高Facebook网站的访问速度呢?

网站将数据记住,而不再重新计算。

如果你登录了Facebook,你看到的所有内容都是为你定制的。你每次访问facebook.com,其服务器都需考虑你感兴趣的是什么内容。

这就是缓存,具有如下两个优点。

❑ 用户能够更快地看到网页。

❑ Facebook需要做的工作更少。缓存是一种常用的加速方式,所有大型网站都使用缓存,而缓存的数据则存储在散列表中!

import urllib.request

cache = {}

def get_page(url):
    if url in cache:
        return cache[url]  # 返回缓存中的数据
    else:
        data = get_data_from_server(url)
        cache[url] = data  # 将数据保存到缓存中
        return data

# 从服务器获取真实数据的函数
def get_data_from_server(url):
    try:
        response = urllib.request.urlopen(url)
        return response.read().decode('utf-8')
    except urllib.error.URLError as e:
        print(f"Error fetching data from server: {e}")
        return None

# 替换为你的网址
url_to_fetch = " "

# 调用 get_page 函数
result = get_page(url_to_fetch)
print(result)

结果是已经登陆过的内容。

注意:

get_page()函数名后圆括号

{}集合/字典中的的键-值,cache[url] 表示索引,键。通过键访问值。

【散列表的三个作用】

❑ 模拟映射关系;

❑ 防止重复;

❑ 缓存/记住数据,以免服务器再通过处理来生成它们。

3. 冲突collision

冲突(collision):给两个键分配的位置相同。

Avocado 和 apple分配了两个同样的键怎么办?

处理冲突的方式很多,最简单的办法如下:如果两个键映射到了同一个位置,就在这个位置存储一个链表(分别指向avocado, avocado下一个是 apple)

❑ 散列函数很重要。前面的散列函数将所有的键都映射到一个位置,而最理想的情况是,散列函数将键均匀地映射到散列表的不同位置。

❑ 如果散列表存储的链表很长,散列表的速度将急剧下降。然而,如果使用的散列函数很好,这些链表就不会很长!

4. 性能

线性时间(简单查找)

对数时间(二分查找)

常量时间(散列表)

O(n)

O(logn)

O(1)

  

这意味着无论散列表包含一个元素还是10亿个元素,从其中获取数据所需的时间都相同。实际上,你以前见过常量时间——从数组中获取一个元素所需的时间就是固定的:不管数组多大,从中获取一个元素所需的时间都是相同的。在平均情况下,散列表的速度确实很快。

在最糟情况下,散列表所有操作的运行时间都为O(n)——线性时间,这真的很慢。

读取(查地址)

插入+删除

(集体插入/分别)

数组

O(1)

O(n)

链表

O(n)

O(1)

数列表平均情况

O(1)

O(1)

数列表最糟情况

O(n)

O(n)

平均情况下,兼具两者优点。

最糟糕情况下,各项操作都很慢。因此要避免冲突。

需要具备:

❑ 较低的填装因子;❑ 良好的散列函数。

以下是散列表运行原理(其实不管你使用的是哪种编程语言,其中都内置了散列表实现。你可使用内置的散列表,并假定其性能良好。)

【填装因子】

公式:因子=散列表包含的元素数/位置数

散列表使用数组来存储数据,因此你需要计算数组中被占用的位置数。

填装因子大于1意味着商品数量超过了数组的位置数。一旦填装因子开始增大,你就需要在散列表中添加位置,这被称为调整长度(resizing)。填装因子越低,发生冲突的可能性越小,散列表的性能越高。

良好的散列函数

良好的散列函数均匀分布,糟糕的散列函数链表扎堆。

【练习】

5.4 散列函数的结果必须是均匀分布的,这很重要。它们的映射范围必须尽可能大。最糟糕的散列函数莫过于将所有输入都映射到散列表的同一个位置。假设你有四个处理字符串的散列函数。

A.不管输入是什么,都返回1。 所有键都指向1的位置。

B.将字符串的长度用作索引。 长度一样的字符串指向同一个位置。

C.将字符串的第一个字符用作索引。即将所有以a打头的字符串都映射到散列表的同一个位置,以此类推。

D将每个字符都映射到一个素数:a=2,b=3,c=5,d=7,e=11,等等。对于给定的字符串,这个散列函数将其中每个字符对应的素数相加,再计算结果除以散列表长度的余数。例如,如果散列表的长度为10,字符串为bag,则索引为(3+2+17)% 10=22% 10=2。在下面的每个示例中,上述哪个散列函数可实现均匀分布?假设散列表的长度为10。

使用素数相加可能减缓冲突的发生

5.5 将姓名和电话号码分别作为键和值的电话簿,其中联系人姓名为EstherBenBobDan

phone_book = {'Esther': '123-456-7890', 'Ben': '987-654-3210', 'Bob': '555-123-4567', 'Dan': '111-222-3333'}

5.6 电池尺寸到功率的映射,其中电池尺寸为AAAAAAAAAA

battery_power = {'A''Low''AA''Medium''AAA''High''AAAA''Ultra'}

5.7 书名到作者的映射,其中书名分别为MausFun HomeWatchmen

book_author = {'Maus': 'Art Spiegelman', 'Fun Home': 'Alison Bechdel', 'Watchmen': 'Alan Moore'}

5.小结

你几乎根本不用自己去实现散列表,因为你使用的编程语言提供了散列表实现。

你可使用Python提供的散列表,并假定能够获得平均情况下的性能:常量时间。

散列表是一种功能强大的数据结构,其操作速度快,还能让你以不同的方式建立数据模型。你可能很快会发现自己经常在使用它。

❑ 你可以结合散列函数和数组来创建散列表。

❑ 冲突很糟糕,你应使用可以最大限度减少冲突的散列函数。

❑ 散列表的查找、插入和删除速度都非常快。

❑ 散列表适合用于模拟映射关系。

❑ 一旦填装因子超过0.7,就该调整散列表的长度。

❑ 散列表可用于缓存数据(例如,在Web服务器上)。

❑ 散列表非常适合用于防止重复。

  • 7
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值