数据结构_哈希表(Java)

在讲解HashMap集合之前,我们先说说一个重要的数据结构---哈希表。
哈希表是一种非常优秀数据结构,对哈希表进行数据的插入,查找(有时也包括删除)的时间复杂度都是O(1)。

从这个时间复杂度,我们就可以知道哈希表是基于数组实现的,因为只有数组才可以直接通过下标获取对应的元素,而其他的数据结构要获取某个位置元素,时间复杂度最少也是O(lg N)。

既然哈希表基于数组实现的,那么就有数组的缺陷,难以扩展。因为数组的长度在创建时就需要确定,超出了数组的长度,就要进行数组扩容。
另外我们不能按照存放顺序来遍历哈希表中元素,也就是说哈希表是无序的。

一.哈希化

怎样将数据存放到哈希表中呢?也就是说我们要将数据和哈希表中的数组下标相联系起来。

将数据转换成一个值,这个值我们就叫它哈希值hashCode,每个对象都应该有哈希值,因此在Object类中提供了hashCode()方法,我们要尽量保证数据转换成的哈希值hashCode是唯一的。
怎样保证哈希值hashCode是唯一的呢?可以用一种简单的方法,就是幂的连乘。
想一想数字101和110,为什么我们知道这两个值是不同的呢?就是因为它们使用了幂的连乘。101 = 1 * 100 + 0 * 10 + 1, 110 = 1* 100 + 1 * 10 + 0.
当然还有其他的方式,来实现hashCode值尽量唯一性。

通过幂的连乘我们保证hashCode的唯一,但是导致另一个问题,就是hashCode过大,既然hashCode值与数组下标是对应的,那么就要创建超级大的数组,这个显然是不能接受的。

怎么解决这个问题?那就要用到哈希化了,将一个很大的hashCode值转成在数组下标范围内的较小的值,这个过程叫做哈希化。

我们通过哈希化解决了hashCode值过大的问题,但是又导致了一个新的问题,就是转成之后的值,我们没有办法保证它的唯一。那么就可能导致不同数据(hashCode值)对应相同的数组下标。
这个时候就产生了冲突,而解决这个冲突,我们一般有两种方式:开放地址法和链地址法。

二. 开放地址法

如果我们要存储20个元素数据,一般创建大一倍长度的数组,这里选择创建长度为41的数组。

为什么要创建大一倍长度的数组呢?其实就是用来解决冲突问题的。而这里为什么选择41呢, 是因为使用再哈希法探测的时候,要求数组长度必须是质数,后面会有详细介绍。

开放地址法怎么解决冲突呢?就是如果发现哈希化得到的下标位置在数组中已经有值了,那么就要寻找数组中没有被占用的空白位置,来存放这个元素。
寻找数组中空白位置,有三种方法:线性探测,二次探测和再哈希探测。

2.1 线性探测

它的意思就是如果当前下标已经被占用了,那么就沿着当前位置向下查找,如果下个位置也被占用,就继续查找,直到找到一个没有被占用的空白位置,将元素存放到这个位置。

有人会疑惑,既然这个值已经不在哈希化得到那个下标位置了,甚至我们都不知道它到底在那个位置了,那么我们怎么查找它呢?

其实也简单,就是先通过哈希化得到下标,然后查看数组这个下标的元素对应的hashCode值,与我们要查找元素的hashCode值是不是相同的,如果相同,那么就是这个元素,如果不相同,那就继续按照线性探测的方式,查看下一个元素的hashCode值,直到相同hashCode值的元素查找成功,或者碰到空白位置查找失败。

注,很多时候我们不能简简单单只比较hashCode值。其实两个对象是否相等,只要equals方法返回true,那么这两个对象就是相等的,但是equals方法是一个比较复杂的方法,尤其是两个对象属性很多的时候,判断起来很耗时间。所以我们优先判断hashCode值是否相等,然后调用equals方法。
因此对象的hashCode值并不一定是唯一的,但是相同对象(即equals方法返回true)的hashCode值一定是相等的。不然哈希表就变得非常混乱。

这里还有个要注意的地方,就是开放地址法哈希表删除的问题?你不能把对应数组下标的元素真实删除了。

因为我们在查找元素的时候,结束查找的条件就是查到空白元素,所以我们随意删除元素,会导致查找元素失败。简单地方式我们将被删除元素位置替换成一个标志对象。查找时碰到这个标志对象,那就继续寻找下个位置元素。添加时,碰到这个标志对象,就直接在这个位置添加新元素。

class DataItem {
    private String data;

    // 用deleteItem这个静态变量标记已删除元素。因为在链表删除的时候,是不能将数组中的元素置位null,用这个标记
    public static final DataItem deleteItem = new DataItem();

    public DataItem() {
    }

    public DataItem(String data) {
        this.data = data;
    }

    public String getData() {
        return data;
    }

    @Override
    public boolean equals(Object obj) {
        if (!(obj instanceof DataItem)) return false;
        DataItem other = (DataItem) obj;
        return this.data == other.data;
    }

    // 用来缓存hash值,避免每次都计算hashCode值
    private int hash;

    // 注意这里计算hashCode方法,很容易就数值溢出了,即超出int类型的最大值。
    @Override
    public int hashCode() {
        if (hash != 0 ) return hash;
        char[] chars = data.toCharArray();
        int h = 0;

        for (int i = 0; i < chars.length; i++) {
            char ch = chars[i];
            h = h * 31 + ch;
        }
        hash = h;
        return h;
    }

    @Override
    public String toString() {
        final StringBuilder sb = new StringBuilder(" {");
        sb.append("data=").append(data);
        sb.append('}');
        return sb.toString();
    }
}

public class LineHash {

    private DataItem[] store;
    private int maxSize;
    private int size;

    public LineHash(int maxSize) {
        this.maxSize = maxSize;
        store = new DataItem[maxSize];
        size = 0;
    }

    // 哈希化 函数
    private int hashFunc(int hashCode) {
        return hashCode % maxSize;
    }


    public void insert(DataItem value){
        int hashCode = value.hashCode();
        // 得到哈希化之后的值
        int hashIndex = hashFunc(hashCode);
        // hashIndex对应坐标的不为null,且没有被删除,那么就查找下一个元素。
        // 直到找到hashIndex对应位置 元素 为空,或者被删除。那么就在这个位置插入新元素。
        // 注意这里没有考虑 死循环的情况
        while (store[hashIndex] != null && store[hashIndex] != DataItem.deleteItem) {
            hashIndex = hashIndex + 1;
            if (hashIndex == maxSize)
                hashIndex = 0;
        }
        store[hashIndex] = value;
        size++;
    }

    public DataItem get(int hashCode) {
        // 得到哈希化之后的值
        int hashIndex = hashFunc(hashCode);
        // 跳出循环的条件有两个,一个是找到一个空白元素,表示没有查到对应元素,二是找到相同hashCode的值,返回它。
        // 注意不能将被删除元素作为循环终止条件,被删除元素只能作为继续查找下一个元素的条件
        while (store[hashIndex] != null) {
            // 如果hashIndex对应位置元素被删除了,那么就查找下一个元素。
            // 注意这里千万不能将store[hashIndex] != DataItem.deleteItem判断条件放到while循环中。
            if (store[hashIndex] != DataItem.deleteItem) {
                DataItem temp = store[hashIndex];
                if (temp.hashCode() == hashCode) {
                    return temp;
                }
            }

            hashIndex = hashIndex + 1;
            if (hashIndex == maxSize)
                hashIndex = 0;
        }
        return null;
    }

    public DataItem remove(int hashCode) {
        int hashIndex = hashFunc(hashCode);
        // 跳出循环的条件有两个,一个是找到一个空白元素,表示没有查到对应元素,
        // 二是找到相同hashCode的值,就将hashIndex对应下标元素换成deleteItem,表示已删除
        // 注意不能将被删除元素作为循环终止条件,被删除元素只能作为继续查找下一个元素的条件
        while (store[hashIndex] != null) {
            // 如果hashIndex对应位置元素被删除了,那么就查找下一个元素。
            // 注意这里千万不能将store[hashIndex] != DataItem.deleteItem判断条件放到while循环中。
            if (store[hashIndex] != DataItem.deleteItem) {
                DataItem temp = store[hashIndex];
                if (temp.hashCode() == hashCode) {
                    size --;
                    store[hashIndex] = DataItem.deleteItem;
                    return temp;
                }
            }
            hashIndex = hashIndex + 1;
            if (hashIndex == maxSize)
                hashIndex = 0;
        }
        return null;
    }

    public int getSize() {
        return size;
    }

    public static void main(String[] args){

        LineHash lineHash = new LineHash(333);
        for (int i = 0; i < 100; i++) {

            DataItem dataItem = new DataItem(i+"_"+i);
            lineHash.insert(dataItem);
        }

        System.out.println("size === "+ lineHash.getSize());

        DataItem keyDataItem1 = new DataItem("50_50");
        DataItem keyDataItem2 = new DataItem("25_25");
        // 查询方法是否有效
        System.out.println("get() ==" +lineHash.get(keyDataItem1.hashCode()));
        System.out.println("get() ==" +lineHash.get(keyDataItem2.hashCode()));

        // 删除方法是否有效
        System.out.println("remove == "+lineHash.remove(keyDataItem1.hashCode()));
        // 重复删除不会有异常
        System.out.println("remove == "+lineHash.remove(keyDataItem1.hashCode()));

        // 删除之后,在哈希表中就查找不到了
        System.out.println("get() ==" +lineHash.get(keyDataItem1.hashCode()));
        // 删除不会影响其他元素。
        System.out.println("get() ==" +lineHash.get(keyDataItem2.hashCode()));
        System.out.println("get() ==" +lineHash.get(new DataItem("75_75").hashCode()));
        System.out.println("get() ==" +lineHash.get(new DataItem("755").hashCode()));
        System.out.println("size === "+ lineHash.getSize());
    }


}

从代码中可以看出,如果数组元素较多时,哈希化得到下标被占据的可能性越大,而且查找下一个有用位置所要耗费的时间就越多,因为数组比较满,没有空白位置。

所以当数组元素越来越多的时候,哈希表的效率就会变得非常慢,这个时候就要扩充数组,重新将原来元素放入对应位置。那么什么时候扩充数组呢?我们设置一个负载因子loadFactor(比如0.75f),当数组中元素数量超过数组长度与负载因子的乘积时,那么就扩充数组。

注意,当我们扩充数组之后,数组的长度改变了,那么哈希化之后得到坐标值也和以前不一样了。所以每次扩容都很耗时间,因为要遍历元素全部元素,重新计算哈希化后的坐标值,在放入新数组对应位置。

2.2 二次探测

二次探测是相对于线性探测来说的。当发生冲突时,线性探测是每次以1的步长去查找空白位置,那么就有一点小问题?如果数组中的元素发生了局部聚集,也就是说在数组一段区间内,都有元素没有空白位置,那么线性探测要找到空白位置,就要移动很多步。
二次探测就是每次探测的步长是步数的平方,像x+1^2, x + 2 ^2, x + 3^2等等这样,这样就可以避免局部聚集的情况了。
但是二次探测也有一个小问题,对于哈希化得到相同值的元素来说,它们每次探测的时候,所走的路径是一样,这样也会产生另外一种形式的数据聚集。

2.3 再哈希探测

想一想二次探测的问题,是因为哈希化得到相同值的元素,它们每次探测的步长是一样的,所以也会产生聚集。

怎么解决这个问题呢?在哈希化得到相同值的元素得到相同位置时,但是它们的hashCode值是不一样的,我们要利用这个条件,通过这个hashCode值得到探测的步长,那么得到的步长一般不会重复,这个过程就叫做再哈希。

这个再哈希函数应该满足两个条件,一 和哈希化函数应该不同,二 得到的结果值不能是0. 因为步长是0,就会原地踏步,陷入死循环。

一般我们使用这样的公式,step = constant - ( hashCode % constant)。constant一般为质数,hashCode % constant得到的值在constant范围内,但是有可能为0,所以用constant减它。这样就将数 0 --- constant-1 变成 constant --- 1。

还有一个注意点,再哈希探测要求数组长度是质数,为什么有这个奇怪要求呢?主要是防止一种情况,得到的步长是数组长度的因子。例如 数组长度是10,哈希化得到下标值是0或者5,得到的步长正好也是5.
这个时候就有问题了,你会发现我们的探测序列一直是 0、5、10、0、5等等,是一个死循环,没办法找其他位置。但是如果数组长度是个质数,那么它的探测序列就不一样咯,0、5、10、4、9、3、8、2、7、1、6、0 你会发现会它会遍历数组所有位置。

class DataItem {
    private String data;

    // 用deleteItem这个静态变量标记已删除元素。因为在链表删除的时候,是不能将数组中的元素置位null,用这个标记
    public static final DataItem deleteItem = new DataItem();

    public DataItem() {
    }

    public DataItem(String data) {
        this.data = data;
    }

    public String getData() {
        return data;
    }

    @Override
    public boolean equals(Object obj) {
        if (!(obj instanceof DataItem)) return false;
        DataItem other = (DataItem) obj;
        return this.data == other.data;
    }

    // 用来缓存hash值,避免每次都计算hashCode值
    private int hash;

    // 注意这里计算hashCode方法,很容易就数值溢出了,即超出int类型的最大值。
    @Override
    public int hashCode() {
        if (hash != 0 ) return hash;
        char[] chars = data.toCharArray();
        int h = 0;

        for (int i = 0; i < chars.length; i++) {
            char ch = chars[i];
            h = h * 31 + ch;
        }
        hash = h;
        return h;
    }

    @Override
    public String toString() {
        final StringBuilder sb = new StringBuilder(" {");
        sb.append("data=").append(data);
        sb.append('}');
        return sb.toString();
    }
}

public class DoubleHash {

    private DataItem[] store;
    private int maxSize;
    private int size;

    public DoubleHash(int maxSize) {
        this.maxSize = maxSize;
        store = new DataItem[maxSize];
        size = 0;
    }

    // 哈希化 函数
    private int hashFunc(int hashCode) {
        return hashCode % maxSize;
    }

    // 再哈希,得到步长
    private int getFuncStep(int hashCode) {
        return 5 - hashCode % 5;
    }


    public void insert(DataItem value){
        int hashCode = value.hashCode();

        // 得到哈希化之后的值
        int hashIndex = hashFunc(hashCode);

        // 通过哈希值,得到步长step
        int step = getFuncStep(hashCode);
        // hashIndex对应坐标的不为null,且没有被删除,那么就查找下一个元素。
        // 直到找到hashIndex对应位置 元素 为空,或者被删除。那么就在这个位置插入新元素。
        // 注意这里没有考虑 死循环的情况
        while (store[hashIndex] != null && store[hashIndex] != DataItem.deleteItem) {
            hashIndex = hashIndex + step;
            // 不能使用hashIndex == maxSize这个条件了,因为hashIndex不再是每次自增1,那么就有可能跳过maxSize这个值。
            hashIndex = hashIndex % maxSize;
        }
        store[hashIndex] = value;
        size++;
    }

    public DataItem get(int hashCode) {
        // 得到哈希化之后的值
        int hashIndex = hashFunc(hashCode);
        // 通过哈希值,得到步长step
        int step = getFuncStep(hashCode);
        // 跳出循环的条件有两个,一个是找到一个空白元素,表示没有查到对应元素,二是找到相同hashCode的值,返回它。
        // 注意不能将被删除元素作为循环终止条件,被删除元素只能作为继续查找下一个元素的条件
        while (store[hashIndex] != null) {
            // 如果hashIndex对应位置元素被删除了,那么就查找下一个元素。
            // 注意这里千万不能将store[hashIndex] != DataItem.deleteItem判断条件放到while循环中。
            if (store[hashIndex] != DataItem.deleteItem) {
                DataItem temp = store[hashIndex];
                if (temp.hashCode() == hashCode) {
                    return temp;
                }
            }

            hashIndex = hashIndex + step;
            hashIndex = hashIndex % maxSize;
        }
        return null;
    }

    public DataItem remove(int hashCode) {
        int hashIndex = hashFunc(hashCode);
        // 通过哈希值,得到步长step
        int step = getFuncStep(hashCode);
        // 跳出循环的条件有两个,一个是找到一个空白元素,表示没有查到对应元素,
        // 二是找到相同hashCode的值,就将hashIndex对应下标元素换成deleteItem,表示已删除
        // 注意不能将被删除元素作为循环终止条件,被删除元素只能作为继续查找下一个元素的条件
        while (store[hashIndex] != null) {
            // 如果hashIndex对应位置元素被删除了,那么就查找下一个元素。
            // 注意这里千万不能将store[hashIndex] != DataItem.deleteItem判断条件放到while循环中。
            if (store[hashIndex] != DataItem.deleteItem) {
                DataItem temp = store[hashIndex];
                if (temp.hashCode() == hashCode) {
                    size --;
                    store[hashIndex] = DataItem.deleteItem;
                    return temp;
                }
            }
            hashIndex = hashIndex + step;
            hashIndex = hashIndex % maxSize;
        }
        return null;
    }

    public int getSize() {
        return size;
    }

    public static void main(String[] args){

        DoubleHash lineHash = new DoubleHash(333);
        for (int i = 0; i < 100; i++) {

            DataItem dataItem = new DataItem(i+"_"+i);
            lineHash.insert(dataItem);
        }

        System.out.println("size === "+ lineHash.getSize());

        DataItem keyDataItem1 = new DataItem("50_50");
        DataItem keyDataItem2 = new DataItem("25_25");
        // 查询方法是否有效
        System.out.println("get() ==" +lineHash.get(keyDataItem1.hashCode()));
        System.out.println("get() ==" +lineHash.get(keyDataItem2.hashCode()));

        // 删除方法是否有效
        System.out.println("remove == "+lineHash.remove(keyDataItem1.hashCode()));
        // 重复删除不会有异常
        System.out.println("remove == "+lineHash.remove(keyDataItem1.hashCode()));

        // 删除之后,在哈希表中就查找不到了
        System.out.println("get() ==" +lineHash.get(keyDataItem1.hashCode()));
        // 删除不会影响其他元素。
        System.out.println("get() ==" +lineHash.get(keyDataItem2.hashCode()));
        System.out.println("get() ==" +lineHash.get(new DataItem("44_44").hashCode()));
        System.out.println("size === "+ lineHash.getSize());
    }


}

三. 链地址法
在开放地址法中,如果我们发现有冲突(即哈希化得到下标位置已经有值了),是通过再寻找一个空白位置,也解决冲突。根本原因是数组对应下标只能存放一个值。

这时我们就想到了一种数据结构---链表,即采用数组和链表结合方式。数组中不在存放真实的值,而是一个链表。查找的时候,就是先通过哈希化得到的下标,找到数组对应下标存放的链表,然后再遍历链表,找到hashCode值相等元素。

class DataItem {
    private String data;

    // 指向下一个DataItem的引用,形成一个链表
    DataItem next;

    public DataItem(String data) {
        this(data, null);
    }

    public DataItem(String data, DataItem next) {
        this.data = data;
        this.next = next;
    }

    public String getData() {
        return data;
    }

    @Override
    public boolean equals(Object obj) {
        if (!(obj instanceof DataItem)) return false;
        DataItem other = (DataItem) obj;
        return this.data == other.data;
    }

    // 用来缓存hash值,避免每次都计算hashCode值
    private int hash;

    // 注意这里计算hashCode方法,很容易就数值溢出了,即超出int类型的最大值。
    @Override
    public int hashCode() {
        if (hash != 0 ) return hash;
        char[] chars = data.toCharArray();
        int h = 0;

        for (int i = 0; i < chars.length; i++) {
            char ch = chars[i];
            h = h * 31 + ch;
        }
        hash = h;
        return h;
    }

    @Override
    public String toString() {
        final StringBuilder sb = new StringBuilder(" {");
        sb.append("data=").append(data);
        sb.append('}');
        return sb.toString();
    }
}

public class LinkedHash {

    private DataItem[] store;
    private int maxSize;
    private int size;

    public LinkedHash(int maxSize) {
        this.maxSize = maxSize;
        store = new DataItem[maxSize];
        size = 0;
    }

    // 哈希化 函数
    private int hashFunc(int hashCode) {
        return hashCode % maxSize;
    }


    public void insert(DataItem value){
        size++;
        int hashCode = value.hashCode();

        // 得到哈希化之后的值
        int hashIndex = hashFunc(hashCode);
        System.out.println("hashCode=="+hashCode+"  hashIndex=="+hashIndex+"  value=="+value);

        DataItem temp = store[hashIndex];
        // 如果对应下标对应元素为空,就将value值存到这个下标位置。value相当于一个链表
        if (temp == null) {
            store[hashIndex] = value;
            return;
        }

        // 利用循环查找 链表尾,然后将value插入链表尾
        while (true) {
            if (temp.next == null) break;
            temp = temp.next;
        }

        temp.next = value;
    }

    public DataItem get(int hashCode) {
        // 得到哈希化之后的值
        int hashIndex = hashFunc(hashCode);
        DataItem temp = store[hashIndex];
        // 链表为null,没有这个元素,返回null
        if (temp == null) {
            return null;
        }
        // 链表不为空就,遍历链表,查找与hashCode相同的元素,并返回它
        while (temp != null) {
            if (temp.hashCode() == hashCode) {
                return temp;
            }
            temp = temp.next;
        }
        // 链表遍历完成之后,还是没有找到,就返回null
        return null;
    }

    public DataItem remove(int hashCode) {
        // 得到哈希化之后的值
        int hashIndex = hashFunc(hashCode);
        DataItem temp = store[hashIndex];
        // 链表为null,没有这个元素,返回null,删除失败
        if (temp == null) {
            return null;
        }
        DataItem prev = null;
        // 链表不为空就,遍历链表,查找与hashCode相同的元素,从列表中删除它。
        while (temp != null) {
            if (temp.hashCode() == hashCode) {

                // 如果prev为空,表示temp是链表头,所以要重新给store[hashIndex]数组赋值
                if (prev == null) {
                    store[hashIndex] = temp.next;
                } else {
                    // prev不为空,就将它的next指向temp的 next,这样就将temp从链表中删除了
                    prev.next = temp.next;
                }
                size -- ;
                return temp;
            }
            temp = temp.next;
        }
        return null;
    }

    public int getSize() {
        return size;
    }

    public static void main(String[] args){

        LinkedHash lineHash = new LinkedHash(333);
        for (int i = 0; i < 100; i++) {

            DataItem dataItem = new DataItem(i+"_"+i);
            lineHash.insert(dataItem);
        }

        System.out.println("size === "+ lineHash.getSize());

        DataItem keyDataItem1 = new DataItem("50_50");
        DataItem keyDataItem2 = new DataItem("25_25");
        // 查询方法是否有效
        System.out.println("get() ==" +lineHash.get(keyDataItem1.hashCode()));
        System.out.println("get() ==" +lineHash.get(keyDataItem2.hashCode()));

        // 删除方法是否有效
        System.out.println("remove == "+lineHash.remove(keyDataItem1.hashCode()));
        // 重复删除不会有异常
        System.out.println("remove == "+lineHash.remove(keyDataItem1.hashCode()));

        // 删除之后,在哈希表中就查找不到了
        System.out.println("get() ==" +lineHash.get(keyDataItem1.hashCode()));
        // 删除不会影响其他元素。
        System.out.println("get() ==" +lineHash.get(keyDataItem2.hashCode()));
        System.out.println("get() ==" +lineHash.get(new DataItem("44_44").hashCode()));
        System.out.println("size === "+ lineHash.getSize());
    }
}

总结

哈希值与哈希化

哈希表是基于数组实现的,对数据的插入,查找(有时也包括删除)的时间复杂度都是O(1)。有两个很重要的概念哈希值与哈希化
哈希值:每个对象都有哈希值hashCode,所以在顶层父类Object中提供了hashCode()方法。

对于哈希值,我们要尽量使不同对象有不同的哈希值,但是极个别情况下发生相等情况也是可以的。
但是我们必须保证,相等对象一定有相等的哈希值,这时使用哈希表的基石。所以也解释了为什么复写Object的equals(Object obj)方法时,要求复写hashCode(),不然你使用哈希表存放对象时,就会产生意想不到的情况。

哈希化:将较大的哈希值hashCode转成一定区间范围内的较小值(数组长度范围之内)。

为什么要哈希化?因为我们在数组中存放值,就要知道对应下标,如果直接使用哈希值hashCode作为数组下标,就非常不现实。一般哈希值hashCode比较大,那要创建很大的数组。我们一般根据存放数据的数量来来创建相应大小数组。

这里我们实现哈希化,是使用取余操作,即hashCode % 数组长度,这样就能得到在数组长度范围内的值。但是取余操作是非常耗时的,不建议使用。在HashCode中我们会介绍一种更好的实现方式。

但是有一个问题,哈希化得到的下标值可能是相同的,这个时候就产生冲突,而解决这个冲突,我们一般有两种方式:开放地址法和链地址法。

开放地址法

开放地址法解决冲突的方式,它会创建一个比要存放数据数量大一倍的数组,如果发生冲突,它会沿着数组下标向下查找一个空白位置,将元素存放到这个位置。

根据向下查找步长不同,又分为三种方式线性探测,二次探测和再哈希探测

  • 线性探测:步长固定,且为1
  • 二次探测:步长固定,是步数的平方,像x+1^2, x + 2 ^2, x + 3^2等等这样。
  • 再哈希探测:步长不固定,根据哈希值得到不同步长,一般使用公式 step = constant - (key % constant),其中constant为质数。

查找元素时,先通过哈希化得到一个下标值,获取数组对应位置值,比较这它们的哈希值,如果相同就表示查找到了,如果不同,就根据探测方式不同,向下查找下一个位置元素,直到找到哈希值相等元素查找成功,或者一个空白元素查找失败。

注:一般情况下,我们不只是仅仅比较哈希值,因为可能存在不同对象哈希值相等情况,所以我们在哈希值相等情况下,继续调用equals方法,这样才能确保相等。
那为什么不直接调用equals方法,还干嘛进行哈希值的比较。那是因为equals方法有可能是非常耗时的,而比较哈希值,我们可以排除不相等情况,这个非常重要。

因为开放地址法查找元素的特性,所以删除元素时,不能真的将对应数组下标位置设置为null,这样可能会导致查找元素失败。

当哈希表元素较多时,会发现哈希表效率会变得特别低,因为发生冲突的可能性越来越大,并且查找下一个空白位置,所需要的耗时也越多。这个时候我们就要进行数组的扩容,来提高哈希表的效率。

注意数组的长度改变了,那么哈希化得到的值也发生了改变,所以我们必须重新遍历哈希表中元素,通过哈希化,将它们存放到新数组对应位置。

什么时候进行数组的扩容呢? 这里就有两个概念了 负载因子 和 阈值。
阈值等于 数组长度 * 负载因子,当哈希表元素个数超过阈值时,就扩充数组。

链地址法

链地址法哈希表采用数组和链表结合的方式,来解决冲突的。即数组不再直接存放元素,而是存放一个链表,元素存放在链表中。
哈希化得到下标值,获取对应位置的值,如果为null,那么新创建一个链表,将元素存放到链表头,然后将链表存入数组中,如果不为null,就是一个链表,将元素存入这个链表尾。
查找元素时,哈希化得到下标值,获取对应位置的值,如果为null,那就是查找失败,如果不为null,就是一个链表,遍历一个链表,查找哈希值相等的元素,如果相等就表示查找成功,如果遍历完整个链表,还是没有找到,就表示查找失败。

理论上说,链地址法哈希表是不需要扩容的,因为链表的存储几乎是无限,但是这样的话,就没有哈希表的优势了,因为链表过长,遍历链表耗时变长。
所以我们也要确定一个阈值,当哈希表元素数量超过阈值时,就要进行数组扩容,并重新分配元素位置。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,968评论 6 482
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,601评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 153,220评论 0 344
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,416评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,425评论 5 374
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,144评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,432评论 3 401
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,088评论 0 261
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,586评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,028评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,137评论 1 334
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,783评论 4 324
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,343评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,333评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,559评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,595评论 2 355
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,901评论 2 345

推荐阅读更多精彩内容