【数据结构与算法 | 哈希表篇】哈希表

1. 前言

给每个数据分配一个编号,放入表格(数组)。

建立编号和表格索引的关系,这样就可以通过编号快速查找数据。

  • 理想情况编号当唯一时,表格能容纳所有的数据。
  • 现实是不能说为了容纳所有的数据造一个超大的表格,编号也可能重复。

解决:

  • 有限长度的数组,以拉链方式存储数据。
  • 允许编号适当重复,通过数据自身来进行区分。查找时,顺着单向链表查找即可。

2. 哈希表相关代码

哈希表是数组+链表/数组+红黑树的结合。

如果链表过长,会导致查询速率下降。

JDK8开始,HashMap和HashSet内部实现做了改进,引入了树化机制。具体来说,在HashMap种,当一个桶中的链表长度超过阈值(默认是8)且哈希表的容量大于64时,这个链表会被转换成一颗红黑树,这样可以显著减少查询时间,因为红黑树的查找时间复杂度为O(logn)。需要注意的是,当桶的元素数量减少到一定阈值(通常是6或者更小)以下时,红黑树又会退化成链表。

下面的代码包含了Entry内部类,实现了get,put,remove,resize等方法。

public class HashTable {
    // 键值指针和哈希值
    public class Entry {
        Object key;
        Object value;
        Entry next;
        int hash;

        public Entry(Object key, Object value) {
            this.key = key;
            this.value = value;
        }

        public Entry(Object key, Object value, Entry next) {
            this.key = key;
            this.value = value;
            this.next = next;
        }
    }
    // 保证数组的长度是2的倍数,比如:2^1, 2^2, 2^3...
    // 是为了方便计算哈希值得到数组索引
    // 以及方便将原索引位置的一个链表分为两个链表
    Entry[] table = new Entry[16];
    // 底层装填元素的个数
    int size;
    // 装填因子
    float loadfactor = 0.75f;
    // 阈值,超过该值数组将会扩容
    int threshold = (int)(loadfactor*table.length);
    private Object get(int hash, Object key) {
        // 通过哈希值计算得到数组的索引位置
        // 等同于hash % (table.length - 1),但下者的性能更好
        int index = hash & (table.length - 1);
        Entry p = table[index];
        if (p == null) {
            return null;
        }
        while (p != null) {
            if(p.key == key) {
                return p.value;
            }
            p = p.next;
        }
        // 此处表明并没有找到
        return null;
    }
    public Object get(Object key) {
        int hash = key.hashCode();
        hash = hash ^ (hash >> 16);
        return get(hash, key);
    }
    private void put(int hash, Object key, Object value) {
        int index = hash & (table.length - 1);
        // 先判断有没有找到,如果有,则更新;没有则添加
        Entry p = table[index];
        Entry q = null;
        if (p == null) {
            table[index] = new Entry(key, value);
        } else {
            while (p != null) {
                // 进入该if判断说明已经找到,更新该值即可
                if (p.key == key) {
                    p.value = value;
                    return;
                }
                q = p;
                p = p.next;
            }
            // 进行到这里说明还没找到,在链表末尾添加节点
            q.next = new Entry(key, value);
        }
        size++;
        // 此时可能会超过阈值,判断需不需要扩容
        resize(size);
    }
    public void put(Object key, Object value) {
        int hash = key.hashCode();
        hash = hash ^ (hash >> 16);
        put(hash, key, value);
    }
    private Object remove(int hash, Object key) {
        int index = hash & (table.length - 1);
        Entry p = table[index];
        Entry q = null;
        if (p == null) {
            return null;
        }
        while (p != null) {
            // 找到了
            if (p.key == key) {
                // 如果要删除的是链表的头节点
                if (q == null) {
                    table[index] = p.next;
                } else {
                    q.next = p.next;
                }
                size--;
                return p.value;
            }
            q = p;
            p = p.next;
        }
        return null;
    }
    public Object remove(Object key) {
        int hash = key.hashCode();
        hash = hash ^ (hash >> 16);
        return remove(hash, key);
    }
    public void resize(int size) {
        if (size > threshold) {
            // 扩容扩大到原来数组大小的一倍
            Entry[] newTable = new Entry[table.length << 1];
            threshold = newTable.length;
            // 此时需要将原来数组的内容搬到另一个数组上
            // 可以遵循规律:每条链表最多可以分为两条链表
            // 一条是接到原来的索引位置,另一条接到原来索引位置+table.length的索引位置
            for (int i = 0; i < table.length; i++) {
                Entry a = null;
                Entry b = null;
                Entry p = table[i];
                while (p != null) {
                    int index = (p.key.hashCode() & (newTable.length - 1));
                    // 分配给a链表
                    if (index == i) {
                        // 第一次分配给a链表
                        if (a == null) {
                            a = p;
                        } else {
                            a.next = p;
                        }
                    }
                    // 分配给b链表
                    else {
                        // 第一次分配给b链表
                        if (b == null) {
                            b = p;
                        } else {
                            b.next = p;
                        }
                    }
                    p = p.next;
                }
                // 把a链表接到新数组的原来的索引位置
                // 把b链表接到新数组的原来索引+table.length的位置(此时table还没有更新)
                newTable[i] = a;
                newTable[i + table.length] = b;
            }
            table = newTable;
        }
    }
    public void traversal() {
        for (int i = 0; i < table.length; i++) {
            Entry p = table[i];
            while (p != null) {
                System.out.print(p.key + "    ");
                p = p.next;
            }
        }
    }
}

3. 单元测试

import org.junit.Test;

/**
 * ClassName : HashTabletest
 * Package : PACKAGE_NAME
 * Description
 *
 * @Author HeXua
 * @Create 2024/8/10 0:19
 * Version 1.0
 */
public class HashTabletest {
    @Test
    public void test() {
        HashTable hashtable = new HashTable();
        hashtable.put(1, "AA");
        hashtable.put(2, "BB");
        hashtable.put(3, "CC");
        hashtable.put(4, "DD");
//        hashtable.traversal();
        // 1    2    3    4
//        System.out.println(hashtable.get(1));
        // AA
        hashtable.remove(1);
        hashtable.traversal();
        // 2    3    4
    }
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值