14.哈希表

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/endlessseaofcrow/article/details/81269110

哈希表

1.基本概念

wasn3.png

wa4QK.png

哈希表最重要的就是“键”转化为“索引”–哈希函数的设计,同时哈希冲突后如何解决

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

哈希表是时间和空间之间的平衡。

2.哈希函数的设计

“键”通过哈希函数得到的“索引”,索引分布的越均匀越好。

2.1整型

  • 小范围正整数直接使用

  • 小范围负整数直接偏移 -100-10(统一+100)

    大整数

身份证号:130481199304111819

通常做法:取模 比如取后四位,等同于mod 10000

取后六位?等同于 mod 1000000 这样由于倒数5、6位只能在1-31之间,所以会造成分布不均匀。同时没有利用所有的信息

具体问题就具体分析。

一个简单的解决方法:模一个素数。不同数量级的数据规模可以选择不同的素数。

w8HeY.png

2.2浮点型

w8TRi.png

2.3 字符串

当做整数处理。

w8zaX.png

注:B可以根据实际情况选择,M表示素数取模。

为了防止整型溢出,可以采用以下的方法:以下结果是一样的,(我没有验证过)

w80TJ.png

2.4复合类型

w8W66.png

2.5总结

  • 转成整型处理,并不是唯一的方法。

原则:

  1. 一致性:如果a==b,则hash(a)==hash(b)
  2. 高效性: 计算高效简便
  3. 均匀性: 哈希值均匀分布

3.Java中的hashCode

3.1

int a=42;
System.out.println(((Integer)a).hashCode());
int b=-42;
System.out.println(((Integer)b).hashCode());
double d=3.14;
System.out.println(((Double)d).hashCode());
String str="antfin";
System.out.println(str.hashCode());
//42
//-42
//300063655
//-1412795196

Java中的hashcode返回值为int,所以有正有负,具体在类中转化为索引需要开发者自己实现,因为事先不知道索引的大小。

3.2类

public class Student {
    private int ID;
    private int cls;
    private String firstName;
    private String lastName;

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

    @Override
    public int hashCode(){
        int B=31; //随机取
        int hash=0;
        hash=hash*B+ID ;
        hash=hash*B+cls;
        hash=hash*B+firstName.toLowerCase().hashCode();
        hash=hash*B+lastName.toLowerCase().hashCode();
        return hash;
    }
}
System.out.println(new Student(12,22,"antfin","alibaba").hashCode());

其实有默认的hashCode,是根据地址映射的。

System.out.println(new Student(12,22,"antfin","alibaba").hashCode());
        System.out.println(new Student(12,22,"antfin","alibaba").hashCode());
不覆盖hashcode方法。
1627674070
1360875712

但这与我们的逻辑不符合,所以我们要同时覆盖hashcode 和equals

4.链地址法—处理哈希冲突

  • 对一个素数M取模,由于Java中hashCode(k1)可能为负数,所以要取绝对值,以下是同样的效果
(hashCode(k1)&0x7fffffff)%M

0x7fffffff其实在32位中表示首位为0,剩下31位为1.

  • 链地址法,其实是一个查找表,不见得不一定是 链表。查找表也可以使用平衡树结构。数组中每一个都存储查找表。

wnIKO.png

由于Java中TreeMap的底层就是红黑树,其实就是当冲突达到一定程度就使用了TreeMap,红黑树的时间复杂度虽然比链表低,但是当数据规模较小的时候,链表是更快的。

5.实现hashMap

  • 使用用红黑树实现的TreeMap,体现了面对对象的好处。
/**
 * Alipay.com Inc. Copyright (c) 2004-2018 All Rights Reserved.
 */
package com.antfin.hashcode;

import java.util.TreeMap;

/**
 * @author alibaba
 * @version $Id: HashTable.java, v 0.1 2018年07月24日 下午11:51 alibaba Exp $
 */
public class HashTable<K, V> {
    private TreeMap<K, V>[] hashTable;
    private int             size;
    //M的取值很重要
    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 contain(K key) {
        return hashTable[hash(key)].containsKey(key);
    }

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

6.时间复杂度分析

6.1上一节实现的哈希表的时间复杂度

总共有M个地址,如果放入哈希表的元素为N,由于数组大小为M,支持随机访问的能力,故不用考虑定位数组的时间复杂度,则平均时间复杂度为:

如果每个地址是链表:O(N/M)

如果每个地址是平衡树:O(log(N/M))

说好的O(1)呢?扩容,resize

6.2哈希表的动态空间处理

平均每个地址承载的元素多过一定程度,即扩容。

N/M>=upperTol

平均每个地址承载的元素少过一定程度,即缩容

N/M<=lowerTol

/**
 * Alipay.com Inc. Copyright (c) 2004-2018 All Rights Reserved.
 */
package com.antfin.hashcode;

import java.util.TreeMap;

/**
 * @author alibaba
 * @version $Id: HashTables.java, v 0.1 2018年07月28日 下午2:42 alibaba Exp $
 */
public class HashTables<K, V> {
    private        TreeMap<K, V> hashTables[];
    private        int           size;
    private        int           M;
    private static int           initCapacity = 7;
    private        int           UPPER_LOT    = 10;
    private        int           LOWER_LOT    = 2;

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

    public HashTables() {
        this(initCapacity);
    }

    public int getSize() {
        return size;
    }

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

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

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

    public boolean contain(K key) {
        TreeMap<K, V> map = hashTables[hash(key)];
        if (map.containsKey(key)) { return true; }
        return false;
    }

    public V get(K key) {
        TreeMap<K, V> map = hashTables[hash(key)];
        if (!map.containsKey(key)) { throw new IllegalArgumentException("get failed," + key + "doesn't exist!"); }
        return map.get(key);
    }

    public V remove(K key) {
        TreeMap<K, V> map = hashTables[hash(key)];
        if (!map.containsKey(key)) { throw new IllegalArgumentException("remove failed," + key + "doesn't exist!"); }
        size--;
        if (size<LOWER_LOT*M&&M/2>=initCapacity)
            resize(M/2);
        return map.remove(key);
    }

    private void resize(int newM) {
        TreeMap<K,V> newHashtable []=new TreeMap[newM];
        for (int i=0;i<newM;i++){
            newHashtable[i]=new TreeMap<>();
        }
        //此处要注意新旧M的替换,因为for循环使用的是oldM而hash则使用的是newM
        int oldM=M;
        this.M=newM;
        for (int i=0;i<oldM;i++){
            TreeMap<K,V>map=hashTables[i];
            for (K key:map.keySet()){
                newHashtable[hash(key)].put(key,map.get(key));
            }
        }
        this.hashTables=newHashtable;
    }

}

53J9ke.png

53JBBd.png

一开始,我不理解为什么增加扩容和缩容就变成了O(1)操作,其实是因为通过扩容和缩容操作,每条链上面的元素个数成了(lowerTol-upperTol)即确定的,自然变成了常数级别的。

6.3更复杂的动态空间处理方法

53JH1r.png

保证扩容的M仍然为素数,减少哈希碰撞的概率,使元素分布的更均匀。

/**
 * Alipay.com Inc. Copyright (c) 2004-2018 All Rights Reserved.
 */
package com.antfin.hashcode;

import java.util.TreeMap;

/**
 * @author alibaba
 * @version $Id: HashTables.java, v 0.1 2018年07月28日 下午2:42 alibaba Exp $
 */
public class HashTables<K, V> {
    private final int           capacity[]    =
            {53, 97, 193, 389, 769, 1543, 3079, 6151, 12289, 24593,
                    49157, 98317, 196613, 393241, 786433, 1572869, 3145739, 6291469,
                    12582917, 25165843, 50331653, 100663319, 201326611, 402653189, 805306457, 1610612741};
    private       TreeMap<K, V> hashTables[];
    private       int           size;
    private       int           M;
    private       int           capacityIndex = 0;
    private       int           UPPER_LOT     = 10;
    private       int           LOWER_LOT     = 2;

    public HashTables() {
        this.M = capacity[capacityIndex];
        this.size = 0;
        hashTables = new TreeMap[M];
        for (int i = 0; i < M; i++) {
            hashTables[i] = new TreeMap<>();
        }
    }

    public int getSize() {
        return size;
    }

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

    public void add(K key, V value) {
        TreeMap<K, V> map = hashTables[hash(key)];
        if (map.containsKey(key)) { map.put(key, value); } else {
            map.put(key, value);
            size++;
            if (size > UPPER_LOT * M && capacityIndex + 1 < capacity.length) { resize(capacity[capacityIndex++]); }
        }
    }

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

    public boolean contain(K key) {
        TreeMap<K, V> map = hashTables[hash(key)];
        if (map.containsKey(key)) { return true; }
        return false;
    }

    public V get(K key) {
        TreeMap<K, V> map = hashTables[hash(key)];
        if (!map.containsKey(key)) { throw new IllegalArgumentException("get failed," + key + "doesn't exist!"); }
        return map.get(key);
    }

    public V remove(K key) {
        TreeMap<K, V> map = hashTables[hash(key)];
        if (!map.containsKey(key)) { throw new IllegalArgumentException("remove failed," + key + "doesn't exist!"); }
        size--;
        if (size < LOWER_LOT * M && capacityIndex-1 >= 0) { resize(capacity[capacityIndex--]); }
        return map.remove(key);
    }

    private void resize(int newM) {
        TreeMap<K, V> newHashtable[] = new TreeMap[newM];
        for (int i = 0; i < newM; i++) {
            newHashtable[i] = new TreeMap<>();
        }
        //此处要注意新旧M的替换,因为for循环使用的是oldM而hash则使用的是newM
        int oldM = M;
        this.M = newM;
        for (int i = 0; i < oldM; i++) {
            TreeMap<K, V> map = hashTables[i];
            for (K key : map.keySet()) {
                newHashtable[hash(key)].put(key, map.get(key));
            }
        }
        this.hashTables = newHashtable;
    }

}

6.4其他的碎碎念

53Jhla.png

Java标准库平衡树就是红黑树:TreeMap,TreeSet

哈希表:HashMap,HashSet

一个bug

53JyFz.md.png

在Java标准库中,链表转为红黑树的前提也是数据具有可比较性。

7.更多处理哈希冲突的方法

7.1开放地址法

  • 每个地址都对每个元素开放,每个地址直接存元素(而不是链表或者红黑树),冲突了直接放在该元素地址后面下一个为空的地方。(线性探测)

    线性探测:遇到哈希冲突+1

  • 平方探测:遇到哈希冲突 +1,+4,+9 ,+16,不会产生一整片空间全部被占据的方法。

  • 二次哈希,遇到哈希冲突,选择另外一个哈希函数,+hash2(key)

当负载率()到达一定程度的时候就扩容。

7.2

  • 再哈希法

猜你喜欢

转载自blog.csdn.net/endlessseaofcrow/article/details/81269110