63、HashMap(哈希表)
HashMap
(哈希表)
我们对Index
/IndexMut
的实现并不理想:我们需要按照id
遍历整个Vec
来检索想要的ticket;时间复杂度为O(n)
,其中n
就是TicketStore
里面ticket
的数量。
我们可以通过使用不同的数据结构来存储ticket
:HashMap<K,V>
(哈希表)
use std::collections::HashMap;
// Type inference lets us omit an explicit type signature (which
// would be `HashMap<String, String>` in this example).
// 类型推断允许我们省略显式的类型签名(在此例中,该类型应为 HashMap<String, String>)。
let mut book_reviews = HashMap::new();
book_reviews.insert(
"Adventures of Huckleberry Finn".to_string(),
"My favorite book.".to_string(),
);
HashMap
适用于键值对,它对两者都是泛型:K
是键类型的有效参数,V
是值类型的有效参数。
插入、查找和删除的预期成本是常数,即 O(1)。这对我们的用例来说听起来完美,对吧?
关键要求
HashMap
的结构体定义中没有trait bound
,但是在其方法里可以找到一些。让我们来看看insert
操作,例如:
// Slightly simplified
impl<K, V> HashMap<K, V>
where
K: Eq + Hash,
{
pub fn insert(&mut self, k: K, v: V) -> Option<V> {
// [...]
}
}
Key
类型必须实现Eq
和Hash
trait。
让我们深入了解这两个trait。
Hash
散列函数(散列器)可以将潜在的无限值得集合(如所有可能的字符串)映射到有界范围(例如u64
这么大)。
目前有许多不同的散列函数,每种函数都有不同的特性(速度,碰撞风险,可逆性等等)。
哈希表,顾名思义,其背后原理就是哈希函数。它对键进行散列,然后使用散列来存储/检索相关的值。这种策略要求Key
类型必须是可以散列的,因此K
上绑定了Hash
trait。
我们可以在std::hash
模块找到Hash
trait:
pub trait Hash {
// Required method
fn hash<H>(&self, state: &mut H)
where H: Hasher;
}
我们很少手动实现Hash
,大多数情况下,可以直接用派生宏:
#[derive(Hash)]
struct Person {
id: u32,
name: String,
}
Eq
HashMap
必须能够比较键的相等性。这在处理哈希冲突的时候尤为重要——当两个不同的键散列为相同的值时。
这大概就是PartialEq
trait的用途吧,差不多。PartialEq
对于HashMap
来说是不够的,因为它不能保证reflexivity
(自反性),即确保a==a
始终为true
。
例如,浮点数(f32
和f64
)实现了PartialEq
,但是,但是我们不能保证他们的自反性:f32::NAN==f32::NAN
是false
。
自反性对于HashMap
正常工作非常重要:没有它,我们将不能用与插入时相同的键再次散列计算以从map中检索值。
Eq
trait通过自反性属性拓展了PartialEq
:
pub trait Eq: PartialEq {
// No additional methods
}
这是一个标记trait:它不添加任何新方法,只是让编译器知道PartialEq
实现的相等的逻辑是自反的。
派生PartialEq
时,可以自动派生Eq
:
#[derive(PartialEq, Eq)]
struct Person {
id: u32,
name: String,
}
Eq
和 Hash
是关联的
Eq
和 Hash
之间存在一个隐式约定:如果两个键相等,它们的哈希值也必须相同。这对于HashMap
的工作至关重要。如果我们违反了这个约定,那么我们在使用HashMap
时会得到无意义的结果。
练习
这次的练习就是让我们完善之前的代码,为它添加HashMap
的功能,直接看题目吧:
// TODO: Replace `todo!()`s with the correct implementation.
// Implement additional traits on `TicketId` if needed.
use std::collections::HashMap;
use std::ops::{Index, IndexMut};
use ticket_fields::{TicketDescription, TicketTitle};
#[derive(Clone)]
pub struct TicketStore {
tickets: HashMap<TicketId, Ticket>,
counter: u64,
}
#[derive(Clone, Copy, Debug, PartialEq/* TODO */)]
pub struct TicketId(u64);
#[derive(Clone, Debug, PartialEq)]
pub struct Ticket {
pub id: TicketId,
pub title: TicketTitle,
pub description: TicketDescription,
pub status: Status,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct TicketDraft {
pub title: TicketTitle,
pub description: TicketDescription,
}
#[derive(Clone, Debug, Copy, PartialEq, Eq)]
pub enum Status {
ToDo,
InProgress,
Done,
}
impl TicketStore {
pub fn new() -> Self {
Self {
tickets: todo!()
counter: 0,
}
}
pub fn add_ticket(&mut self, ticket: TicketDraft) -> TicketId {
let id = TicketId(self.counter);
self.counter += 1;
let ticket = Ticket {
id,
title: ticket.title,
description: ticket.description,
status: Status::ToDo,
};
todo!()
id
}
pub fn get(&self, id: TicketId) -> Option<&Ticket> {
todo!()
}
pub fn get_mut(&mut self, id: TicketId) -> Option<&mut Ticket> {
todo!()
}
}
impl Index<TicketId> for TicketStore {
type Output = Ticket;
fn index(&self, index: TicketId) -> &Self::Output {
self.get(index).unwrap()
}
}
impl Index<&TicketId> for TicketStore {
type Output = Ticket;
fn index(&self, index: &TicketId) -> &Self::Output {
&self[*index]
}
}
impl IndexMut<TicketId> for TicketStore {
fn index_mut(&mut self, index: TicketId) -> &mut Self::Output {
self.get_mut(index).unwrap()
}
}
impl IndexMut<&TicketId> for TicketStore {
fn index_mut(&mut self, index: &TicketId) -> &mut Self::Output {
&mut self[*index]
}
}
有多个地方的代码需要修改,首先是:
#[derive(Clone, Copy, Debug, PartialEq/* TODO */)]
pub struct TicketId(u64);
我们需要完善一些派生宏,根据前面学到的内容,我们在这里填写Eq和Hash就对了。
第二,是这里的new方法:
pub fn new() -> Self {
Self {
tickets: todo!()
counter: 0,
}
}
之前的代码是在这里初始化Vec
,很显然我们要用的是HashMap来替换掉它,所以我们就可以在这里编写:
pub fn new() -> Self {
Self {
tickets: HashMap::new(),
counter: 0,
}
}
然后就是add_ticket
这个方法,直接猜也能猜出来,我们要实现的是HashMap
添加数据的功能:
pub fn add_ticket(&mut self, ticket: TicketDraft) -> TicketId {
let id = TicketId(self.counter);
self.counter += 1;
let ticket = Ticket {
id,
title: ticket.title,
description: ticket.description,
status: Status::ToDo,
};
self.tickets.insert(id, ticket);
id
}
就是插入这样一行代码就可以了self.tickets.insert(id, ticket);
,和常规的C++,Java等语言的操作方式都很像。
最后就是这两个方法:
pub fn get(&self, id: TicketId) -> Option<&Ticket> {
todo!()
}
pub fn get_mut(&mut self, id: TicketId) -> Option<&mut Ticket> {
todo!()
}
也是一样的,我们要做的是根据ID进行查询,还是查找HashMap
提供的get
方法,于是这样编写代码:
pub fn get(&self, id: TicketId) -> Option<&Ticket> {
self.tickets.get(&id)
}
pub fn get_mut(&mut self, id: TicketId) -> Option<&mut Ticket> {
self.tickets.get_mut(&id)
}
最终代码也是成功运行了,并顺利通过了测试。
以上就是这一节的内容了。