数据结构——哈希表

淩亂°似流年 2022-04-22 01:12 454阅读 0赞

一、从一道Leetcode题目认识哈希表

387. 字符串中的第一个唯一字符

watermark_type_ZmFuZ3poZW5naGVpdGk_shadow_10_text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L2l0Y2F0c19jbg_size_16_color_FFFFFF_t_70

因为该字符串只包含小写字母,即只存在a-z 26个小写字母,我们将其a-z对应到数组0-25索引的位置,出现一次,index+1

代码编写:

  1. class Solution {
  2. public int firstUniqChar(String s) {
  3. int[] freq = new int[26];
  4. for(int i = 0 ; i < s.length() ; i++)
  5. //统计字符串s 中,a-z的个数
  6. freq[s.charAt(i) - 'a']++;
  7. for(int i = 0 ; i < s.length() ; i++)
  8. if(freq[s.charAt(i) - 'a'] == 1 )
  9. return i;
  10. return -1;
  11. }
  12. }

这个问题内部隐藏着哈希表的结构思想,我们开辟的 int[] freq 实际上就是一个哈希表,每一个字符都和数组中的一个索引对应

watermark_type_ZmFuZ3poZW5naGVpdGk_shadow_10_text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L2l0Y2F0c19jbg_size_16_color_FFFFFF_t_70 1

此时我们简单地坐到了将字符与索引进行了一一对应,这种将”键”转化为”索引”的方式,称为哈希函数

有如一个班总共有30名学生,我们可以使用数组0-29分别表示这30名学生。

但是在某些情况下,如我们需要保存居民身份证、字符串、浮点数、日期等信息,就很难保证每一个”键”通过哈希函数的转换对应不同的”索引”。

不同的”键”通过哈希函数对应了同一个”索引”,称为哈希冲突。

哈希表充分体现了算法设计领域的经典思想:使用空间换取时间

watermark_type_ZmFuZ3poZW5naGVpdGk_shadow_10_text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L2l0Y2F0c19jbg_size_16_color_FFFFFF_t_70 2

所以我们需要①设计一个合理的哈希函数实现”键”与”索引”的对应关系,”键”通过哈希函数得到的”索引”分布越均匀越好

②解决哈希冲突

二、哈希函数的设计

“键”通过哈希函数得到的”索引”分布越均匀越好,哈希函数的设计很复杂,我们并不关注某一个特殊的领域,本文只对一般的哈希函数进行设计。

watermark_type_ZmFuZ3poZW5naGVpdGk_shadow_10_text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L2l0Y2F0c19jbg_size_16_color_FFFFFF_t_70 3

后6位的前两位”16”取值范围为01 ~ 31 表示日期,造成了分布不均的问题,可见对取模的值的选择十分重要,一般模一个素数

watermark_type_ZmFuZ3poZW5naGVpdGk_shadow_10_text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L2l0Y2F0c19jbg_size_16_color_FFFFFF_t_70 4

模以合数和素数的区别:

watermark_type_ZmFuZ3poZW5naGVpdGk_shadow_10_text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L2l0Y2F0c19jbg_size_16_color_FFFFFF_t_70 5

浮点数—->转化为整形处理

字符串—->转化为整形处理

watermark_type_ZmFuZ3poZW5naGVpdGk_shadow_10_text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L2l0Y2F0c19jbg_size_16_color_FFFFFF_t_70 6

watermark_type_ZmFuZ3poZW5naGVpdGk_shadow_10_text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L2l0Y2F0c19jbg_size_16_color_FFFFFF_t_70 7

对应的代码:

watermark_type_ZmFuZ3poZW5naGVpdGk_shadow_10_text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L2l0Y2F0c19jbg_size_16_color_FFFFFF_t_70 8

符合类型—->转化为整形处理

watermark_type_ZmFuZ3poZW5naGVpdGk_shadow_10_text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L2l0Y2F0c19jbg_size_16_color_FFFFFF_t_70 9

关于哈希函数设计的总结:

watermark_type_ZmFuZ3poZW5naGVpdGk_shadow_10_text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L2l0Y2F0c19jbg_size_16_color_FFFFFF_t_70 10

三、Java中的hashCode()

Object类中的hashCode()方法,在整形中的hashCode为数字本身,Double、Float、String等都重写了Object类中的hashCode()。

四、解决哈希冲突方法(链地址法Seperate Chaining)

watermark_type_ZmFuZ3poZW5naGVpdGk_shadow_10_text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L2l0Y2F0c19jbg_size_16_color_FFFFFF_t_70 11

五、编码实现哈希表

version1:

  1. public class HashTable<K, V> {
  2. private TreeMap<K,V>[] hashtable;
  3. private int M;
  4. private int size;
  5. public HashTable(int M){
  6. this.M = M;
  7. size = 0 ;
  8. hashtable = new TreeMap[M];
  9. for(int i = 0 ; i< M ;i++){
  10. hashtable[i] = new TreeMap<>();
  11. }
  12. }
  13. public HashTable(){
  14. this(97);
  15. }
  16. private int hash(K key){
  17. return ( key.hashCode() & 0x7fffffff ) % M;
  18. }
  19. public int getSize(){
  20. return size;
  21. }
  22. public void add(K key , V value){
  23. TreeMap<K,V> map = hashtable[hash(key)];
  24. if (map.containsKey(key))
  25. map.put(key,value);
  26. else{
  27. map.put(key,value);
  28. size ++;
  29. }
  30. }
  31. public V remove(K key){
  32. TreeMap<K,V> map = hashtable[hash(key)];
  33. V ret = null;
  34. if(map.containsKey(key)){
  35. ret = map.remove(key);
  36. size --;
  37. }
  38. return ret;
  39. }
  40. public void set(K key, V value){
  41. TreeMap<K, V> map = hashtable[hash(key)];
  42. if(!map.containsKey(key))
  43. throw new IllegalArgumentException(key + " doesn't exist!");
  44. map.put(key, value);
  45. }
  46. public boolean contains(K key){
  47. return hashtable[hash(key)].containsKey(key);
  48. }
  49. public V get(K key){
  50. return hashtable[hash(key)].get(key);
  51. }
  52. }

分析此时间复杂度:

watermark_type_ZmFuZ3poZW5naGVpdGk_shadow_10_text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L2l0Y2F0c19jbg_size_16_color_FFFFFF_t_70 12

要想尽可能达到O(1),固定地址空间是不合理的,需要resize()。

watermark_type_ZmFuZ3poZW5naGVpdGk_shadow_10_text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L2l0Y2F0c19jbg_size_16_color_FFFFFF_t_70 13

  1. //HashTable类新建成员变量
  2. private static final int upperTol = 10;
  3. private static final int lowerTol = 2;
  4. private static final int initCapacity = 7;
  5. public void add(K key, V value) {
  6. TreeMap<K, V> map = hashtable[hash(key)];
  7. if (map.containsKey(key))
  8. map.put(key, value);
  9. else {
  10. map.put(key, value);
  11. size++;
  12. if (size >= upperTol * M)
  13. resize(2 * M);
  14. }
  15. }
  16. public V remove(K key) {
  17. TreeMap<K, V> map = hashtable[hash(key)];
  18. V ret = null;
  19. if (map.containsKey(key)) {
  20. ret = map.remove(key);
  21. size--;
  22. if (size <= lowerTol * M && M > initCapacity)
  23. resize(M / 2);
  24. }
  25. return ret;
  26. }
  27. private void resize(int newM){
  28. TreeMap<K, V>[] newHashTable = new TreeMap[newM];
  29. for(int i = 0 ; i < newM ; i ++)
  30. newHashTable[i] = new TreeMap<>();
  31. for(int i = 0 ; i < M ; i ++)
  32. for(K key: hashtable[i].keySet())
  33. newHashTable[hash(key)].put(key, hashtable[i].get(key));
  34. this.M = newM;
  35. this.hashtable = newHashTable;
  36. }

再次分析此时间复杂度:

watermark_type_ZmFuZ3poZW5naGVpdGk_shadow_10_text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L2l0Y2F0c19jbg_size_16_color_FFFFFF_t_70 14

我们在add后的resize(2 * M)方法中 扩容M —-> 2*M ,发现扩容后2 * M 此时并不是一个素数。

改进:采用素数,考虑HashMap的扩容,略

哈希表的得失

watermark_type_ZmFuZ3poZW5naGVpdGk_shadow_10_text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L2l0Y2F0c19jbg_size_16_color_FFFFFF_t_70 15

发表评论

表情:
评论列表 (有 0 条评论,454人围观)

还没有评论,来说两句吧...

相关阅读

    相关 数据结构 -

    哈希表基本介绍 散列表(Hash table,也叫哈希表),是根据关键码值(Key value)而直接进行访问的数据结构。也就是说,它通过把关键码值映射到表中一个位置来访

    相关 数据结构——

    1、基础知识 1.1 引子 在实现编程中,常常面临着两个问题:存储和查询。存储和查询的效率往往决定了整个程序的效率。而我们常见存储数据的数据结构比如线性表,树等。数