哈希表是以查找为主要目的的数据结构,数据之间并没有逻辑关系。

哈希表通过建立键值对以方便查找目标数据,且时间复杂度为O(1)

现实中,每个人的身份证号是唯一的,则可以通过哈希表来存储每个人的信息并高效查找。

示例

有三个人,通过它们唯一的Id查找信息,使用列表:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
var group = new List<Person> {
new Person("001", "AAA"),
new Person("002", "BBB"),
new Person("003", "CCC")};

Person target;
foreach(var person in group)
{
if(person.Id == "003")
{
target = person;
Console.WriteLine($"Person {target.Name} is at {target.Id}");
break;
}
}

public class Person(string id, string name)
{
public string Id {get; set;} = id;
public string Name {get; set;} = name;
}

虽然可以得到Id003的个人信息,但当数据量更大时,显然需要耗费更多的时间遍历。

如果采用哈希表(C#里的System.Collections.Generic.Dictionary):

1
2
3
4
5
6
7
8
9
10
var group = new Dictionary<string, Person>();
group.Add("001", new Person("AAA"));
group.Add("002", new Person("BBB"));
group.Add("003", new Person("CCC"));
Console.WriteLine(group["003"]);

public class Person(string name)
{
public string Name {get; set;} = name;
}

使用Dictionary后为每个人建立键值对,查找时无需遍历就能得到对应的信息。

常用操作

哈希表的各种操作都十分高效,时间复杂度为O(1)

1
2
3
4
5
6
7
8
9
10
11
12
13
// 初始化
var group = new Dictionary<string, Person>();

// 向哈希表添加键值对
group.Add("001", new Person("AAA"));
group.Add("002", new Person("BBB"));
group.Add("003", new Person("CCC"));

// 删除一个键为 002 的键值对
group.Remove("002");

// 获取键为 003 的值
var personC = group["003"];

如果需要遍历哈希表的每一个键/值,也是可以做到的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 通过 foreach 遍历哈希表的每一个键值对
foreach(var pair in group){
var id = pair.Key; // 获取键
var person = pair.Value; // 获取值
}

// 遍历哈希表的每一个键,在循环体中获取值
foreach(var key in group.Keys){
var id = key; // 获取键
var person = group[key]; // 获取值
}

// 遍历哈希表的每一个值
foreach(var value in group.Values){
var person = value;
}

实现

通常可以使用数组来简单地实现一个哈希表,在每一个位置存放一个键值对。

为了让键可以找到对应的值,我们需要实现一个哈希函数来映射。

哈希函数需要处理如下操作:

  • 将输入的键传入某个函数得到哈希值
  • 将哈希值对数组长度取模,得到值在数组里的索引
  • 通过索引返回值

由于实现简单,此处将某个函数忽略,即就是哈希值

然后设一个长度为1000的数组,故哈希值会对1000取模。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
/*  int: string 键值对 */
class Pair(int key, string value)
{
public int Key { get; init; } = key;
public string Value { get; init; } = value;
}

/* 基于列表实现 */
class ListHashTable {
const int Capacity = 1000;
List<Pair> units = new(Capacity); // 初始化长度为 1000 的列表

public ListHashTable()
{
for(int i=0; i<Capacity; i++){
units.Add(null);
}
}

// 哈希函数
int HashFunction(int key) => key % Capacity;

// 添加
public void Add(int key, string value) => units[HashFunction(key)] = new Pair(key, value);

// 删除
public void Remove(int key) => units[HashFunction(key)] = null;

// 查询
public string GetValue(int key) => units[HashFunction(key)].Value;
}

哈希冲突

由于处理过程被简化了,所以很难保证计算后的索引是唯一的,这就导致哈希冲突。

比如对于一个对1000取模来存储的哈希表,遇到100114200114的键时,存储的位置一样(都是114),数据会被覆盖。

处理这种冲突最简单的方法就是扩容哈希表,这样哈希函数在进行取模运算后,冲突的概率就会降低。

但显而易见的是,除了扩容之外,也可以通过优化哈希函数或改变数据结构来让元素更紧凑但不覆盖地存储。