哈希表

1. 什么是哈希表

 

我们先来做个题(leetCode上387题)

public class Solution_387 {
    public int firstUniqChar(String s) {
        int[] freq = new int[26];

        for(int i=0 ; i<s.length() ; i++)
            freq[s.charAt(i)-'a'] ++;

        for(int i=0; i<s.length() ; i++)
            if(freq[s.charAt(i)-'a'] == 1)
                return i;
        return -1;
    }
}

这个题背后就隐藏着哈希表这种数据结构的思想

 

       它的本质就是把我们真正关心的那个内容,即字符(ch)转换成一个索引(index),然后直接用一个数组来存储相应的内容,由于我们数组本身是支持随机访问的,所以我们可以使用O(1)的时间复杂度来完成操作。

       在哈希表中,我们是可以存储各种类型的数据,对于每种数据类型,我们都需要一个方法把它转换成索引,相应的我们关注的这个类型转换成索引的这个函数就称为哈希函数。很多时候哈希函数并不是像上一题那样容易.

 

 

  

 

 

2. 哈希函数的设计

       上一节我们说到“键”通过哈希函数得到的“索引”分布越均匀越好,但这个是很难做到的。对于一些特殊领域,有特殊哈希函数设计方法,甚至有专门的论文,所以我们主要关注一般的哈希函数的设计原则。

我们先来看下整型:

 

我们知道身份证倒数56位代表着这个人生日的日期,如上就是16号生日,而一个月最多也就31天,所以如果对于省份证只取后六位,会导致索引全部分布在320000以内,导致分布不均匀,并且只取六位也没有利用所有信息,这两点都会导致哈希冲突

 

再来看下浮点型:

 

我们看下字符串型:

如果我们字符太多,上面的计算公式就很复杂。我们使用多项式的化简

上面大括号里面的值可能会很大,大到产生整型溢出,所以我们可以把取模的过程挪到括号里面

 

最后来看下符合类型,其实它也可以套用上面的公式:

 

可以看到上面,我们都是转成整型处理,但要记住,这并不是唯一办法,因为哈希函数的设计是一个很深奥的问题,每种不同的情况应该做不同的设计

 

 

2. java中hashCode()

 

       在们具体使用java编程的时候,对于基本的数据类型如字符串String等,我们根本不需要自己去实现hash函数,在java语言中对于这些类型都可以调用其中的hashCode这样的一个方法直接获得我们当前的这个数据所对应的那个hash函数的值。

public class Main {
    public static void main(String[] args) {
        int a = 42;
        System.out.println( ((Integer)a).hashCode() );

        int b = -42;
        System.out.println( ((Integer)b).hashCode() );

        double c = 3.1415926;
        System.out.println( ((Double)c).hashCode() );

        String d = "yy";
        System.out.println( d.hashCode() );

    }
}


结果:
42
-42
219937201
3872

       我们java中的hashCode与我们开始说的哈希函数有些不同。对于java的hashCode,它的返回值是一个int值,由于int是有符号的,所以hashCode()返回的值有可能是正,有可能是负具体我们要将我们的hashCode转成一个索引,这个工作需要在哈希表的类中来完成

      其实这个也合理,我们在上一小节所讲的,整型转换成索引的方式,是对素数进行一个取模的运算,这个素数的值其实也是哈希表的大小,如果我们没有这个哈希表的话,我们也取不出我们的素数。所以我们定义这个类的时候就不能把它直接转换成我们的索引,这是因为我们不知道这个索引的最大值是多少。所以在java的设计中的hashCode的接口只是将每一个数据类型和整型对应起来,这个整型可正可负,而这个整型如何和你的数组中的索引对应,这一点由我们的哈希表内部的逻辑来完成
     

    如果我们需要自定义一个类型,比如自定义学生类,对于这个类,我们可以覆盖Object类中的hashCode()函数来定义自己类相应的哈希值。

    

package com.javaweb.demo;

public class Student {

    int grade;
    int cls;
    String firstName;
    String lastName;

    Student(int grade, int cls , String firstName , String lastName){
        this.grade = grade;
        this.cls = cls;
        this.firstName = firstName;
        this.lastName = lastName;
    }

    @Override
    public int hashCode(){

        int B = 31;

        int hash = 0;
        hash = hash * B + grade;
        hash = hash * B + cls;
        hash = hash * B + firstName.toLowerCase().hashCode();
        hash = hash * B + lastName.toLowerCase().hashCode();

        return hash;
    }
}
package com.javaweb.demo;

public class Main {
    public static void main(String[] args) {

        Student student = new Student(3,2,"young","young");
        System.out.println(student.hashCode());

        Student student1 = new Student(3,2,"YOUNG","YOUNG");
        System.out.println(student1.hashCode());


    }
}


结果:
-609474657
-609474657

 

我们类有hashCode之后,从道理上我们就可以使用java为我们提供的和哈希表有关的数据结构了。java中为我们提供了两个和哈希表相关的结构,一个是HashSet(以哈希表为底层结构的集合),一个HashMap。

 

将student传进去,在具体存储上,他就会自动调用我们Student类中的hashCode,然后计算出一个索引值,存储到一个相应的位置中。

需要注意的是:

1)对于我们Student类如果没有重写HashCode方法的话,我们调用的就是Object类中的hashCode方法,它是将我们创建的每个类的地址相应的把它转换成一个整型。

2)当我们使用HashSet和HashMap的时候,hashCode方法只是用于帮我们就算哈希函数的值,但是在产生哈希冲突的时候,我们同样是要比较两个不同的对象他们之间是否是相等的,也就是他们对应的哈希函数的值虽然相等,此时我们要辨别两个对象的不同,我们就要真正的看两个类是否是相等的,正因为如此,我们要将这个类作为哈希表的键的话,我们只复写了hashCode方法是不够的,我们还要对这个类复写equals方法(判断两个对象是否相等)

这样,当我们覆盖了自定义类里面equals方法以后,我们再来把我们的类作为HashSet或者HashMap的键来使用才是真正的放心了,此时在产生哈希冲突时,虽然两个对象对应同样的哈希值,我们也可以使用equals这样的方法来区分这两个类对象的不同。

 

3.哈希冲突处理—链地址法(Seperate Chaining)

其中M就是一个整数求哈希值的时候对一个素数取模的模

 

4. 自己写HashTable

import java.util.TreeMap;

public class HashTable<K, V> {

    private TreeMap<K, V>[] hashtable;
    private int size;
    private int M;

    public HashTable(int M){
        this.M = M;
        size = 0;
        hashtable = new TreeMap[M];
        for(int i = 0 ; i < M ; i ++)
            hashtable[i] = new TreeMap<>();
    }

    public HashTable(){
        this(97);
    }

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

    public int getSize(){
        return size;
    }

    public void add(K key, V value){
        TreeMap<K, V> map = hashtable[hash(key)];
        if(map.containsKey(key))
            map.put(key, value);
        else{
            map.put(key, value);
            size ++;
        }
    }

    public V remove(K key){
        V ret = null;
        TreeMap<K, V> map = hashtable[hash(key)];
        if(map.containsKey(key)){
            ret = map.remove(key);
            size --;
        }
        return ret;
    }

    public void set(K key, V value){
        TreeMap<K, V> map = hashtable[hash(key)];
        if(!map.containsKey(key))
            throw new IllegalArgumentException(key + " doesn't exist!");

        map.put(key, value);
    }

    public boolean contains(K key){
        return hashtable[hash(key)].containsKey(key);
    }

    public V get(K key){
        return hashtable[hash(key)].get(key);
    }
}

  因为M是固定的,如果N无线大的话,那么N/M也是无限大的。所以我们的数组如果是基于一个静态数组的话,显然是不合理的(N增加M不能改变)

 

import java.util.TreeMap;

public class HashTable<K , V> {
    private static final int upperTol = 10;
    private static final int lowerTol = 2;
    private static final int initCapacity = 7;

    private TreeMap<K, V>[] hashtable;
    private int size;
    private int M;

    public HashTable(int M){
        this.M = M;
        size = 0;
        hashtable = new TreeMap[M];
        for(int i = 0 ; i < M ; i ++)
            hashtable[i] = new TreeMap<>();
    }

    public HashTable(){
        this(initCapacity);
    }

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

    public int getSize(){
        return size;
    }

    public void add(K key, V value){
        TreeMap<K, V> map = hashtable[hash(key)];
        if(map.containsKey(key))
            map.put(key, value);
        else{
            map.put(key, value);
            size ++;

            if(size >= upperTol * M)
                resize(2 * M);
        }
    }

    public V remove(K key){
        V ret = null;
        TreeMap<K, V> map = hashtable[hash(key)];
        if(map.containsKey(key)){
            ret = map.remove(key);
            size --;

            if(size < lowerTol * M && M / 2 >= initCapacity)
                resize(M / 2);
        }
        return ret;
    }

    public void set(K key, V value){
        TreeMap<K, V> map = hashtable[hash(key)];
        if(!map.containsKey(key))
            throw new IllegalArgumentException(key + " doesn't exist!");

        map.put(key, value);
    }

    public boolean contains(K key){
        return hashtable[hash(key)].containsKey(key);
    }

    public V get(K key){
        return hashtable[hash(key)].get(key);
    }

    private void resize(int newM){
        TreeMap<K, V>[] newHashTable = new TreeMap[newM];
        for(int i = 0 ; i < newM ; i ++)
            newHashTable[i] = new TreeMap<>();

        int oldM = M;
        this.M = newM;
        for(int i = 0 ; i < oldM ; i ++){
            TreeMap<K, V> map = hashtable[i];
            for(K key: map.keySet())
                newHashTable[hash(key)].put(key, map.get(key));
        }

        this.hashtable = newHashTable;
    }

}

 

  • 4
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
在JavaScript中,哈希表可以通过对象(Object)来实现。对象的属性名就是哈希表中的键,属性值就是哈希表中的值。以下是一个简单的哈希表的实现: 1. 定义一个HashTable类,包含以下方法: ```javascript class HashTable { constructor() { this.table = {}; } // 向哈希表中添加键值对 put(key, value) { this.table[key] = value; } // 从哈希表中获取指定键的值 get(key) { return this.table[key]; } // 从哈希表中移除指定键的值 remove(key) { delete this.table[key]; } // 判断哈希表中是否包含指定键 contains(key) { return this.table.hasOwnProperty(key); } // 获取哈希表中所有的键 getKeys() { return Object.keys(this.table); } // 获取哈希表中所有的值 getValues() { return Object.values(this.table); } // 获取哈希表中键值对的数量 size() { return Object.keys(this.table).length; } // 清空哈希表 clear() { this.table = {}; } } ``` 2. 创建一个HashTable对象,并向其中添加键值对: ```javascript const hashTable = new HashTable(); hashTable.put('name', 'Tom'); hashTable.put('age', 18); hashTable.put('gender', 'male'); ``` 3. 从哈希表中获取指定键的值: ```javascript const name = hashTable.get('name'); // 'Tom' ``` 4. 从哈希表中移除指定键的值: ```javascript hashTable.remove('gender'); ``` 5. 判断哈希表中是否包含指定键: ```javascript const hasAge = hashTable.contains('age'); // true const hasGender = hashTable.contains('gender'); // false ``` 6. 获取哈希表中所有的键: ```javascript const keys = hashTable.getKeys(); // ['name', 'age'] ``` 7. 获取哈希表中所有的值: ```javascript const values = hashTable.getValues(); // ['Tom', 18] ``` 8. 获取哈希表中键值对的数量: ```javascript const size = hashTable.size(); // 2 ``` 9. 清空哈希表: ```javascript hashTable.clear(); ```

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值