hash表是一种数据结构,它使用hash函数组织数据,以支持快速插入和搜索。
其关键思想是使用hash函数将键映射到存储桶。
hash函数是hash表中最重要的组件,该hash表的用于将键映射到特定的桶。简单举例,我们使用 y= x % 5 作为散列函数,其中x是键值,y是分配的桶的索引。
散列函数将取决与键值的范围和桶的数量。
如何设计hash函数是一个开放的问题,思想时尽可能地将键分配到桶中,理想情况瞎,完美的hash函数是键和桶之间是一对一映射,然而大多数情况瞎,hash函数并不完美,需要在桶地数量和桶的容量之间进行权衡。
冲突解决算法应该解决以下几个问题:
这些问题与桶的容量和可能映射到同一个桶的键的数目有关。
假设存储最大键数的桶有N个键,如果N是常数且很小,我们可以简单地使用一个数组将键存在同一个桶中。如果N是可变的或者很大,我们可能需要使用高度平衡的二叉树来代替。
插入和搜索是hash表中的两个基本操作,此外还有基于这两个操作的操作,当我们删除元素时,要先搜索元素,然后在元素存在的情况下从相应位置移除元素。
这里使用LinkedList数组来实现HashSet,并记录一个size属性。index是key%size后的索引,在单个LinkedList中,将key作为值存入,实现多个键存在一个桶里。相同的key当然是相同的值,不同的key在index一样的时候可以存进同一个桶,并且根据key区分,以实现一个桶多个键的效果。
class MyHashSet {
private LinkedList[] lists;
private final int size = 10000;
/**
* Initialize your data structure here.
*/
public MyHashSet() {
lists = new LinkedList[size];
}
public void add(int key) {
int index = key % size;
if (lists[index] == null) {
lists[index] = new LinkedList();
}
if (!contains(key)) {
lists[index].addFirst(key);
}
}
public void remove(int key) {
int index = key % size;
if (lists[index] != null) {
lists[index].remove((Integer) key);
}
}
/**
* Returns true if this set contains the specified element
*/
public boolean contains(int key) {
int index = key % size;
return lists[index] != null && lists[index].contains(key);
}
}
/**
* Your MyHashSet object will be instantiated and called as such:
* MyHashSet obj = new MyHashSet();
* obj.add(key);
* obj.remove(key);
* boolean param_3 = obj.contains(key);
*/
记录了Node数组、容量、当前大小以及负载因子。当size>=capacity * THERESHOD时扩容为原来的两倍。
为了方便理解代码,这里hash函数只是简单返回了自身,要了解更多可以查看HashMap源码的Hash方法。
这里的桶都是为了存储键,值是和键是一一对应的,只要考虑键和桶的关系就行。
class MyHashMap {
Node[] arr;
int capacity;
int size;
private static final double THERESHOD = 0.75;
/**
* Initialize your data structure here.
*/
public MyHashMap() {
capacity = 200000;
arr = new Node[capacity];
size = 0;
}
/**
* value will always be non-negative.
*/
public void put(int key, int value) {
put(arr, key, value);
}
private void put(Node[] arr, int key, int value) {
if (size > capacity * THERESHOD) {
// 二倍扩容
growCapacity();
}
int idx = hash(key) % capacity;
// 使用二次hash 解决碰撞
while (arr[idx] != null && arr[idx].key != key) {
if (arr[idx].value == -1) {
// 说明这个元素已经被remove了
break;
}
idx = hash(idx) % capacity;
}
arr[idx] = new Node(key, value);
size++;
}
private void growCapacity() {
// 倍增后reHash放入即可
capacity *= 2;
Node[] newArr = new Node[capacity];
reHash(newArr, arr);
arr = newArr;
}
private void reHash(Node[] newArr, Node[] arr) {
for (Node node : arr) {
// 被删掉的应该被清除
if (node != null && node.value != -1) {
put(newArr, node.key, node.value);
}
}
}
/**
* Returns the value to which the specified key is mapped, or -1 if this map contains no mapping for the key
*/
public int get(int key) {
int idx = getIdxByKey(key);
return idx == -1 ? -1 : arr[idx].value;
}
private int getIdxByKey(int key) {
int idx = hash(key) % capacity;
while (arr[idx] != null && arr[idx].key != key) {
idx = hash(idx) % capacity;
}
if (arr[idx] == null || arr[idx].value == -1) {
return -1;
}
return idx;
}
private int hash(int key) {
return Integer.hashCode(key);
}
/**
* Removes the mapping of the specified value key if this map contains a mapping for the key
*/
public void remove(int key) {
int idx = getIdxByKey(key);
if (idx != -1) {
arr[idx].value = -1;
size--;
}
}
}
class Node {
int key;
int value;
public Node(int key, int value) {
this.key = key;
this.value = value;
}
}
如果有M个键,那么在使用Hash表时,很同意就达到O(M)的空间复杂度。
但是,Hash表的时间复杂度和设计有很强的联系。我没可能使用数组来将值存在同一个桶中,理想情况下,桶的大小足够小时,可以看作是一个常数。插入和搜索的时间复杂度都是O(1)。
但在最坏的情况瞎,桶大小的最大值将为N。插入时间复杂度为O(1),搜索时为O(N)。
内置hash表的原理
内置hash表的典型设计是:
插入和搜索的平均时间复杂度仍为O(1)。最坏情况下的插入和搜索的时间复杂度是O(logN),使用高度平衡的BST。这是在插入和搜索之间的一种平衡。
简单地迭代每个值并将值插入集合中。 如果值已经在哈希集中,则存在重复。
boolean findDuplicates(List<Type>& keys) {
// Replace Type with actual type of your key
Set<Type> hashset = new HashSet<>();
for (Type key : keys) {
if (hashset.contains(key)) {
return true;
}
hashset.insert(key);
}
return false;
}
目标元素作为键,出现次数作为值,每遍历一次更新值
在这个例子中,如果我们只想在有解决方案时返回 true,我们可以使用哈希集合来存储迭代数组时的所有值,并检查 target - current_value 是否在哈希集合中。但是,我们被要求返回更多信息,这意味着我们不仅关心值,还关心索引。我们不仅需要存储数字作为键,还需要存储索引作为值。因此,我们应该使用哈希映射而不是哈希集合。
ReturnType aggregateByKey_hashmap(List<Type>& keys) {
// Replace Type and InfoType with actual type of your key and value
Map<Type, InfoType> hashmap = new HashMap<>();
for (Type key : keys) {
if (hashmap.containsKey(key)) {
if (hashmap.get(key) satisfies the requirement) {
return needed_information;
}
}
// Value can be any information you needed (e.g. index)
hashmap.put(key, value);
}
return needed_information;
}
示例 :给定一个字符串,找到它重的第一个非重复字符并返回它的索引。如果它不存在,则返回-1
解决此问题的一种简单方法是首先计算每个字符的出现次数。然后通过结果找出第一个与众不同的角色。因此,我们可以维护一个哈希映射,其键是字符,而值是相应字符的计数器。每次迭代一个字符时,我们只需将相应的值加 1。
解决此类问题的关键是在遇到现有键时确定策略。在上面的示例中,我们的策略是计算事件的数量。有时,我们可能会将所有值加起来。有时,我们可能会用最新的值替换原始值。策略取决于问题,实践将帮助您做出正确的决定。
ReturnType aggregateByKey_hashmap(List<Type>& keys) {
// Replace Type and InfoType with actual type of your key and value
Map<Type, InfoType> hashmap = new HashMap<>();
for (Type key : keys) {
if (hashmap.containsKey(key)) {
hashmap.put(key, updated_information);
}
// Value can be any information you needed (e.g. index)
hashmap.put(key, value);
}
return needed_information;
}
致谢 —— leecode