散列表原理学习

1.散列表的基本原理与实现[Hash Table]:
对于基于散列表实现的符号表,若要查找一个键,需要进行以下步骤:
a.使用散列函数将给定键转化为一个"数组的索引",理想情况下,不同的key会被转为不同的索引,但在实际应用中会遇到不同的键转为相同的索引的情况,这种情况叫做碰撞。
b.得到索引后,就可以像访问数组一样,通过这个索引访问到相应的键值对。

散列表是时空权衡的经典例子。

2.散列函数
在散列内部,我们使用桶(bucket)来保存键值对,前面所说数组的索引即为桶号,决定了给定的键存在于散列表的哪个同种。
散列表所拥有的桶数被称为散列表容量(capacity)

均匀散列假设: 我们使用的散列函数能够均匀并独立地将所有的键散布于0到M-1之间

Java中许多常用的类都重写了hashCode方法(Object类的hashCode方法默认返回对象的内存地址),用于为该类型对象返回一个hashCode,通常我们用这个hashCode除以桶数M的余数就可以获取一个桶号。
code example:
String 类的hashCode方法:

  public int hashCode(){
    int  h =hash;
    if(h==0 && value.length>0){
     char val[] = value;
     
     for(int i=0;i<value.length;i++){
      h = 31 * h + val[i];
     }
   hash =h;   
    }
    return h;
 } 

hashCode方法中的value是一个char[]数组,存储中字符串的的每字符。我们可以看到在方法的最开始我们会把hash赋给h,这个hash就表示之前计算的hashCode,这样以来若之前已经计算过这个字符串对象的hashCode,这次我们就无需再计算了,直接返回之前计算过得即可。这种把hashCode缓存的策略只对不可变对象有效,因为不可变对象的hashCode是不会变的。

数值类型的hashCode方法:

public int hashCode(){
  return Integer.hashCode(value);
 }
 public static int hashCode(int value){
  return value;
 }

Double类的hashCode方法:

public int hashCode(){
  return Double.hashCode(value);
 }
 public static int hashCode(double value){
  long bits = doubleToLongBits(value);
  return (int)(bits^(bits>>>32));
 }

Date类的hashCode方法:

public int hashCode(){
  long ht = this.getTime();
  return (int) ht^(int)(ht>>32);
 }

由hashCode取桶号

private int hash(K key){
  return (x.hashCode() & 0x7ffffffff) % M;
 }

一个直接的办法就是直接拿得到的hashCode除以capacity(桶的数量),然后用所得的余数作为桶号。不过在Java中,hashCode是int型的,而Java中的int型均为有符号,所以我们要是直接使用返回的hashCode的话可能会得到一个负数,显然桶号是不能为负的。所以我们先将返回的hashCode转变为一个非负整数,再用它除以capacity取余数,作为key的对应桶号。

解决碰撞:
1.使用拉链法处理碰撞

public class ChainingHashMap<K,V>{
   private int num;
   private int capacity;
   private SeqSearchST<K,V> st;
   
   public ChainingHashMap(int initalCapacity){
    capacity = initalCapacity;
    st = (SeqSearchST<K,V>) new Object[capacity];
    for(int i=0;i<capacity;i++){
     st[i] = new SeqSearchST<>();
    }
   }
   
   private int hash(K key){
    return (key.hashCode() & 0x7ffffffff) % capacity;
   }
   
   private V get(K key){
    return st[hash(hash)].get(key);
   }
   
   public void put(K key,V value){
    st[hash(key)].put(key,value);
   }
  }
public class SeqSearchST<K, V>{
  private Node first;
  
  private class Node{
   K key;
   V val;
   Node next;
   public Node(K key,V val,Node next){
    this.key = key;
    this.val = val;
    this.next = next;
   }
  }
  
  public V get(K key){
   for(Node node = first; node !=null; node = node.next){
    if(key.equals(node.key)){
     return node.val;
    }
   }
   return null;
  }
  
  public void put(K key,V val){
   Node node;
   for(node = first;node !=null; node = node.next){
    if(key.equals(node.key)){
     node.val = val;
     return ;
    }
   }
   first = new Node(key,val,first);
  }
 }

2.线性探测法处理碰撞
线性探测法是另一种散列表的实现策略的具体方法,这种策略叫做开放定址法。开放定址法的主要思想是:用大小为M的数组保存N个键值对,其中M > N,数组中的空位用于解决碰撞问题。
线性探测法的主要思想是:当发生碰撞时(一个键被散列到一个已经有键值对的数组位置),我们会检查数组的下一个位置,这个过程被称作线性探测。线性探测可能会产生三种结果:

  • 命中:该位置的键与要查找的键相同;

  • 未命中:该位置为空;

  • 该位置的键和被查找的键不同。

动态调整数组大小:
数组的大小为桶数的2倍,不支持动态调整数组大小。而在实际应用中,当负载因子(键值对数与数组大小的比值)接近1时,查找操作的时间复杂度会接近O(n),而当负载因子为1时,根据我们上面的实现,while循环会变为一个无限循环。显然我们不想让查找操作的复杂度退化至O(n),更不想陷入无限循环。所以有必要实现动态增长数组来保持查找操作的常数时间复杂度。当键值对总数很小时,若空间比较紧张,可以动态缩小数组,这取决于实际情况。

要实现动态改变数组大小:

if(num == capacity/2)
   resize(2*capacity);
  
private void resize(int newCapacity){
   LinearProbingHashMap<K,V> hashmap = new LinearProbingHashMap<>(newCapacity);
   for(int i=0;i<capacity;i++){
    if(keys[i] !=null ){
     hashmap.put(keys[i],values[i]);
    }
   }
   keys = hashmap.keys;
   values = hashmap.values;
   capacity = hashmap.capacity;
  }

关于负载因子与查找操作的性能的关系,这里贴出《算法》(Sedgewick等)中的一个结论:

在一张大小为M并含有N = a*M(a为负载因子)个键的基于线性探测的散列表中,若散列函数满足均匀散列假设,命中和未命中的查找所需的探测次数分别为:
1/2 * (1 + 1/(1-a))和~1/2*(1 + 1/(1-a)^2)

关于以上结论,我们只需要知道当a约为1/2时,查找命中和未命中所需的探测次数分别为1.5次和2.5次。还有一点就是当a趋近于1时,以上结论中的估计值的精度会下降,不过我们在实际应用中不会让负载因子接近1,为了保持良好的性能,在上面的实现中我们应保持a不超过1/2。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值