参考:算法第四版
目录
1.散列表
散列表:一种符号表,又叫哈希表,java中HashMap的底层数据结构。
符号表:一般存储键值对(key-value),一般用这种数据结构通过key来找到对应的value,可以将其看作字典,key和value一一对应。
1.1什么是散列表?
想象我们要存储一个11位电话号和其对应的人名,我们申请一个大小为1000的数组map,取电话号码的后3位为数组索引,比如小明的电话号码为12345678987,其后三位为987,则将小明(value)和其电话号码(key)存入a[987]中。以后如果想要寻找12345678987的主人时,只要先对12345678987取后三位,得到987,然后去a[987]的位置去寻找有没有对应元素,有则取出value即可。
这就是散列表,将一个键值对的键映射成一个数组的索引(如果数组大小为M,则映射出的值在0-M-1之间),然后存放到对应位置的一种数据结构,将键映射成索引的方法就叫散列函数,前面的电话号码取后三位就是一种散列函数。但散列函数可能会将不同的key映射成相同的数组索引,将这两个先后插入数组中时,因为数组中本来就有元素,就会发生冲突,这时候就需要解决冲突的方法。散列函数和解决冲突的方法组成了散列表。
1.2 散列函数:
对于java库中的类,因为java中每个类都会从共同父类Object类中继承一个hashcode函数,库中的类都会重写一遍,所以hashcode()函数会返回一个32的int,可以在其基础上进行散列,一般取余即可,但需要注意hashcode()可能返回负数以及可能溢出。可以像下面一样实现:
private int hash(Key key)
{
return (key.hashCode() & 0x7fffffff) % M; //M为散列表大小,直接key.hashCode()%M可能返回负数
//直接加个绝对值可能会溢出,因为32位int的范围为-2^31~2^31-1,如果正好为-2^31时,绝对值为2^31就溢出了。
}
以上代码会将符号位屏蔽,达到了取绝对值和防止溢出的结果。
对于自己实现的类,如果没有重写hashcode函数,默认的hashcode函数是返回类的内存地址,因为程序中的内存地址可能就那一块,可能会导致数据分布不均匀,冲突增加,因此一般都需要自己重新hashcode函数,hashcode函数一般采用31法来实现,如下是自定义的一个交意类的hashcode实现
public final class Transaction implements Comparable<Transaction>
{
private final String who;//交易人
private final Date when;//交易时间
private final double amount;//交易数量
public int hashCode()
{
int hash = 17;
hash = 31*hash + who.hashCode();
hash = 31*hash + when.hashCode();
hash = 31*hash + ((Double) amount).hashCode();
return hash;
}
}
java中的String类的hashcode也是采用的类似方法
public final class String
{
private int hash = 0;
private final char[] s;
...
public int hashCode()
{
int h = hash;
if (h != 0) return h;//不等于0,说明已经计算过了,缓存
for (int i = 0; i < length(); i++)
h = s[i] + (31 * h);
hash = h;
return h;
}
}
1.3 解决冲突:
一般常用的解决冲突的方法有链表法和开放寻址法。
链表法即数组每个位置存放的不再是一个键值对,而是一个键值对的链表,如果发生冲突时,只要将键值对放到链表的末尾即可。
开放寻址法即发生冲突时,往后移一个位置放新的键值对,如果还是有元素,再往后移,直到位置为空。
下面是链表法和开放寻址法的java实现:
public class hashtable <Key extends Comparable<Key>,Value>//链表法实现散列表
{
Node<Key,Value>[] table;//散列表数组
private int capcity;//散列表大小
hashtable(int n)
{
table = new Node[n];
capcity = n;
}
private class Node<Key extends Comparable<Key>,Value>//链表节点
{
Node(Key key,Value value)
{
this.key = key;
this.value = value;
}
Key key;
Value value;
Node next;
}
void Put(Key key,Value value)
{
int index = (key.hashCode() & 0x7fffffff) % capcity;
if (table[index] == null)
{
table[index] = new Node(key,value);
return;
}
else
{
for (Node cur = table[index];; cur = cur.next)
{
if (cur.key.compareTo(key) == 0)
{
cur.value = value;
break;
}
if (cur.next == null)
{
cur.next = new Node(key,value);
break;
}
}
}
}
Value get(Key key)//取值
{
int index = key.hashCode() % capcity;
if (table[index] == null) return null;
else
{
for (Node cur = table[index];; cur = cur.next)
{
if (cur.key.compareTo(key) == 0)
{
return (Value) cur.value;
}
if (cur.next == null)
{
return null;
}
}
}
}
}
public class LinearProbingHashST<Key, Value>//开放寻址法实现
{
private int M = 30001;
private Value[] vals = (Value[]) new Object[M];
private Key[] keys = (Key[]) new Object[M];
private int hash(Key key) { /* as before */ }
public void put(Key key, Value val)
{
int i;
for (i = hash(key); keys[i] != null; i = (i+1) % M)
if (keys[i].equals(key))
break;
keys[i] = key;
vals[i] = val;
}
public Value get(Key key)
{
for (int i = hash(key); keys[i] != null; i = (i+1) % M)
if (key.equals(keys[i]))
return vals[i];
return null;
}
}
2.HashMap解析
java中的HashMap就是使用的链表法的散列表实现的
HashMap的散列函数
static final int hash(Object key)
{
int hash;
return key == null ? 0 : (hash = key.hashCode()) ^ hash >>> 16;
}
int index = hash(key) & (capacity - 1)
HashMap中的散列函数将key的hashCode()移位16位,然后和自己取与,最后取余。
冲突:
虽然使用了链表法避免冲突,但仍然可能出现同一个位置放了太多键而导致散列表退化为链表的可能,所以HashMap做了优化,当链表元素大于8时会将链表转化为红黑树,红黑树插入和取元素的时间复杂度为o(logn),虽然比不上散列表的O(1),但最低性能也有了保证。
3.leetcode中的散列表
3.1 leetcode1 twosum,两数之和
https://leetcode-cn.com/problems/two-sum/description/
这道题题目很简单,就是让你在一个数组中找出两个和为target的数,还记得第一次做这题的时候,用的是两个for循环,时间复杂度达到O(n^2),虽然也过了,但现在有了散列表,就有了更加简单的做法。利用java中的散列表HashMap,将数的值和索引分别作为键和值存入散列表,java实现如下
public int[] twoSum(int[] nums, int target)
{
HashMap<Integer,Integer> tracker = new HashMap<>();
for (int i = 0; i < nums.length; i++)
{
if (tracker.containsKey(target - nums[i]))
{
return new int[]{tracker.get(target - nums[i]) , i};
}
else
{
tracker.put(nums[i],i);
}
}
return new int[2];
}
3.2 leetcode242 valid-anagram
https://leetcode-cn.com/problems/valid-anagram/description/
思路和上面差不多,不再赘述。
public static boolean isAnagram(String s, String t)
{
if (s == null || t == null) return false;
if (s.equals(t)) return true;
if (s.length() != t.length()) return false;
int[] hashtable = new int[26];//26个英文字母
for (int i = 0; i <s.length(); i++)
{
hashtable[s.charAt(i) - 'a']++;
}
for (int i = 0; i <t.length(); i++)
{
if (hashtable[t.charAt(i) - 'a'] == 0) return false;
else hashtable[t.charAt(i) - 'a']--;
}
return true;
}
3.3 leetcode49 group-anagrams
public static List<List<String>> groupAnagrams(String[] strs)
{
Map<String,List<String>> map = new HashMap<String,List<String>>();
for (String s:strs)
{
char[] chars = s.toCharArray();
Arrays.sort(chars);
String value = String.valueOf(chars);
if (map.containsKey(value))
{
map.get(value).add(s);
}
else
{
map.put(value,new ArrayList<String>());
map.get(value).add(s);
}
}
return new ArrayList<>(map.values());
}