【JAVA-哈希表实现】

哈希表

1.哈希表也叫散列表,是一种数据结构。

2.哈希表本质上是数组。通过哈希函数将键映射到数组的特定位置,以便快速访问和查找键对应的值。

3.实现哈希表采用的两种方法:①数组+链表;②数组+二叉树。

4.哈希表提供了 O(1) 时间复杂度的查找操作。

1.哈希表用来快速判断一个元素是否出现集合里。
哈希算法:把元素直接映射为哈希表上的索引,然后就可以通过查询索引下标快速知道此元素是否在数组里面。
  • 存储时采用键值对存储方式 ,Key-Value,其中要求Key不重复
  • 先计算Key的哈希值(通常为int),将哈希值与数组(设置初始值是16length)的长度范围进行下标映射,将哈希值映射到大小为16(2的整数倍)的表中。
  • 获取一个下标: int index = hash & len;
  • 直接到数组 index位置取出数据(节点数据(K-V-Next)) 
2.解决哈希冲突的方法
(1)开放寻址法

假设当hash值为3冲突时(假设此时hash表长度为15)。

        线性探测法:顺着表查找,直到找到一个空单元或查遍全表。

        H1 = (3+1)%15 = 4,此时若4依旧冲突,则往下一个查找

        H2 = (3+2)%15 = ... 

        二次探查法:当哈希冲突时,在表的左右进行跳跃探测。1^2,-1^2,2^2,-2^2...

        H1 = (3+1^2)%15 = 4,此时若4依旧冲突,则再hash,即

        H2 = (3+(-1)^2)%15 = 2 …

        伪随机探测法:产生一些随机系列值,并给定随机数作为起点

        假设产生的随机系列为2,5,9 …,则

        H1 = (3+2)%15 = 5

        H2 = (3+5)%15 = 8...

(2)拉链法
拉链法在索引1的位置发生了冲突, 发生冲突的元素都被存储在链表中。
(3)再哈希法

对原始哈希函数重新计算哈希值,然后将冲突的元素插入到重新计算的哈希值对应的位置。

        再哈希法的函数表达式可以表示为:newValue = (hash_value + f(key)) % table_size

newHvalue 是重新计算后的哈希值,hash_value 是原始哈希值,f(key) 是一个用于计算新的偏移量的函数,table_size 是哈希表的大小。

(4)公共溢出区法

设立两个表:基础表溢出表。将所有关键字通过哈希函数计算出相应的地址。然后将未发生冲突的关键字放入相应的基础表中,将具有冲突的元素存储在溢出表中。(通常是一个链表或者其他数据结构)

在查找时,先用给定值通过哈希函数计算出相应的散列地址,与基本表的相应位置进行比较,如果不相等,再到溢出表中顺序查找。

3.开放定址法与拉链法的比较:

①拉链法处理冲突简单,且无堆积现象,即非同义词决不会发生冲突,因此平均查找长度较短;

②拉链法中各链表上的结点空间是动态申请的,更适合于造表前无法确定表长的情况;

③开放定址法为减少冲突,要求装填因子α(装填因子 = 元素数量 / 表的大小)较小,当结点规模较大时会浪费很多空间。

而拉链法中可取α≥1,且结点较大时,拉链法中增加的指针域可忽略不计,因此节省空间;

④拉链法构造的散列表中,删除结点的操作易于实现。只要简单地删去链表上相应的结点即可。

4.存在问题:
1.哈希冲突(一般 很少)。
2. 不同的 hash 值与长度 length 进行映射时,映射到同一个 index ( 频繁 )。
5.数组+链表的哈希表实现方式:
/**
 * 哈希表:
 * 节点类:Node类
 * 属性:
 * 存储结构-节点数组
 * 元素个数
 * 存储结构数组被占用的格子数量
 * 默认初始长度 16
 * 扩容阈值 0.75
 * 数组长度
 * 方法:
 * put: 存储
 * resize(); 扩容
 * get: 获取
 *
 * @param <K>
 * @param <V>
 */
class Node<K, V> {
    K key;
    V value;
    Node<K, V> next;
    int hash;// K的hash值
    public Node(K key, V value, int hash) {
        this.key = key;
        this.value = value;
        this.hash = hash;
    }
}
public class MyHashMap<K,V> {
    private Node<K, V>[] table;
    private int modCount;// 数组被占用的格子数量
    private int elmSize;// 元素个数
    private int capacity;// 数组的容量
    private static final int DEFAULT_CAPACITY = 16;
    private static final double LOAD_FACTOR = 0.75;

    /**
     * 构造方法 根据传进来的容量进行初始化哈希表
     *
     * @param initCapacity
     */
    public MyHashMap(int initCapacity) {
        if (initCapacity < DEFAULT_CAPACITY) {
            initCapacity = DEFAULT_CAPACITY;
        }
        table = new Node[initCapacity];
        capacity = initCapacity;
        elmSize = 0;
        modCount = 0;
    }

    public V get(K key) {
        int h = key.hashCode();
        int index = h & (capacity - 1);
        Node<K, V> first = table[index];
        // 判断 目标位置的节点是否为null
        if (first != null) {
            // 判断 first 是否是咱们需要的节点
            // 1: 哈希值一致 才需要比较后面的
            // 2: 接着比较key的引用地址 如果地址一致 就不需要比较内容
            // 3: 地址不一致的情况会存在内容一致的情况 使用equals比较
            if (first.hash == h && first.key == key || first.key.equals(key)) {
                return first.value;
            }
            // 第一个节点比较不成功 从这个节点开始作为头节点遍历链表
            Node temp = first;// 此处与java底层实现有区别:判断 first 是链表节点(写循环进行遍历)还是红黑树节点(写方法去遍历红黑树)
            while (temp.next != null) {
                temp = temp.next;
                if (temp.hash == h && temp.key == key || temp.key.equals(key)) {
                    return (V) temp.value;
                }
            }
        }
        return null;
    }

    /**
     * 设置元素 用键值对进行存放
     *
     * @param key
     * @param value
     */
    public void put(K key, V value) {
//        System.out.println("Put");
        int h = key.hashCode();// 计算Key 的hash
        Node<K, V> elmNode = new Node<>(key, value, h);// 新节点
        int index = h & (capacity - 1);// 根据hash值 与 数组长度 得到下标
        // System.out.println(index);
        Node<K, V> first = table[index];
        if (first == null) {
            table[index] = elmNode;
            modCount++;
            elmSize++;
        } else {
            Node<K, V> oldNode = null;
            // 如果first 与 新节点的key一致 更新 first节点的v
            if (first.hash == h && first.key == key || first.key.equals(key)) {
                first.value = value;
                oldNode = new Node<>(key, first.value, h);
            } else {
                Node<K, V> temp = first;
                while (temp.next != null) {
                    temp = temp.next;
                    if (temp.hash == h && temp.key == key || temp.key.equals(key)) {
                        temp.value = value;
                        oldNode = new Node<>(key, temp.value, h);
                        break;
                    }
                }
                if (oldNode == null) {// 新增节点
                    temp.next = elmNode;
                    elmSize++;
                }
            }
        }
// 扩容: 数组的被占用格子数 比例大于数组容量的百分之75的时候扩容
        if (modCount >= capacity * LOAD_FACTOR) {
            resize();
        }
    }

    /**
    *      扩容:
    *      扩容 2倍扩容
    *      创建一个更大的数组 将原本的所有元素存储进去
     *      (每个元素取出 重新映射位置 因为放置进去时是根据 int index = h & (capacity - 1); capacity放置的,扩容后capacity发生改变)
    *      每个元素存储的位置与当前数组的长度capacity相关
     *     写函数时,如果出现问题,自己重新写一遍
    */

    private void resize() {
        System.out.println("扩容进入:" + capacity);
        modCount = 0;
        elmSize = 0;
        int oldCapacity = capacity;
        int newCapacity = oldCapacity + oldCapacity; //加法比乘法快
        Node<K, V>[] oldTable = table;
        Node<K, V>[] newTable = new Node[newCapacity];

        //把表中的数据取出来一个一个放到新的表中去
        for (int i = 0; i < oldCapacity; i++) {
//            System.out.println("扩容循环");
            // 取出旧节点
            Node<K, V> node = oldTable[i];

            //此处与对比处的bug进行对比
            //取出旧表中的一个node,看这个node的头是否为空。如果头为空,设置该节点放入元素;如果头不为空,循环遍历到链表尾部,尾部下一个节点设置为该节点放入元素
            //如果oldTable中node为空,则不考虑
            while (node != null) {
                Node<K, V> newNode = new Node<>(node.key, node.value, node.hash);
                System.out.println("遍历旧数组中的链表");
                int h = node.hash;
                System.out.println("旧数组中元素重新进行映射");
                int index = h & (newCapacity - 1);
                Node<K, V> first = newTable[index];
                if (first == null) {
                    newTable[index] = newNode;
                    elmSize++;
                    modCount++;
                } else {
                    System.out.println("新数组目标位置是一条链表");
                    Node<K, V> temp = first;
                    while (temp.next != null) {
                    // System.out.println("temp.next !=null");
                    // System.out.println(temp.value);
                        temp = temp.next;
                    }
                    temp.next = newNode;
                    elmSize++;
                }
                node = node.next;
            }
        }
        //更新全局的 哈希表与容量
        table = newTable;
        capacity = newCapacity;
        System.out.println("扩容:" + capacity);
    }

    /**
     * 测试存储以及输出
     * @param args
     */
    public static void main(String[] args) {
        MyHashMap<String, Integer> map = new MyHashMap<>(16);
        for (int i = 0; i < 100; i++) {
            map.put("Hello" + i, i);    //存进去的字符Hello是哈希值就比较大了
        }
        System.out.println("数组长度:" + map.capacity);
        System.out.println("数组被占用格子数量:" + map.modCount);
        System.out.println("元素个数:" + map.elmSize);
    }

}

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值