【JAVA数据结构】哈希表-HashSet and HashMap

简介: JAVA数据结构 & 哈希表 -HashSet and HashMap

JAVA数据结构 & 哈希表 -HashSet and HashMap

引例

在讲这个部分之前,请试着去做一下下面这道题。


题:给定一串序列(char[] (小写字母)),要求你将其排序并且不能出现重复也不能有一个缺席。


没错,我们可以这么做:

char[] arr = new char[26];
//假设ch 为某个小写字母;
arr[ch - 'a']++;

我们可以讲这个字母减掉’ ‘ a ’(即 97),这样a对应0,b对应1…


这样我们就只需要遍历一遍数组,只要不是’\u0000’(默认值),就打印or其他。


并且也可以快速知道对应字母的个数。

这样的思想就是“哈希思想”,把一个元素以特定的方法放进数组里,只要知道这个方法,就可以快速查找并且快速发现重复…


Set和Map的最大用处就是查找!


哈希的时间复杂度甚至达到了O(1)!

因为根据下标来找,太快了!

1. 哈希方法导致的冲突

细心的同学可能发现了,用同一个哈希方法的话,避免不了两个元素放在了同一个位置去了,那么我们就需要避免和解决。

1.1 冲突的避免(从整体减少冲突的次数)

这里要用到一个 负载因子常量的概念(其他地方可能有其他的说法)

这个负载因子就是哈希表中【已经放入的元素的个数 / 表最大容量】的最大值

一些实验数据事实表明:


哈希表越满越容易冲突


呈现出S型趋势

设计:


只需要让实际的负载因子时刻小于这个最大值,一旦大于等于这个最大值的时候,就让数组扩容。

也面临了另一个大问题,就是扩容后,需要重新将每个元素按照原本的哈希方法放进哈希表。

public static final double Load_FACTOR = 0.75;//一直处于七五分满

1

1.2 冲突的解决

1.2.1 闭散列

散列这个说法一些书籍仍在使用【哈希表的意思】

那么闭散列就是在顺序表内部解决这个问题

至于用顺序表是因为下标访问快

线性探测法

发现用哈希方法解析后的下标,已经被占用,那么从左往右探测,第一个空的就可以放上去

缺点很大:放的效率很低,取的时候效率也低,要找的那个位置不是我们要的数据,那么就要“线性探测”从左往右遍历找我们的数据。

并且我们很难去区分这个位置是否有元素,比如int[] 可能值就是0,引用类型也可能值就是null,怎么就能说明那个位置空了呢?

很容易达到负载值而需要重新扩容

这个方法很少用,最多出道题恶心人。

例如问,按照线性探测法找,需要多少步?


二次探测法

本质上跟【线性探测法】的思路是一致的,放在顺序表的其他空的位置。


但是这个方法是按照一个规律去实现的


Hi = (H0 + i2) % m【m为表的大小,H代表哈希值(后面会讲)】

Hi = (H0 - i2) % m【或者是这种】

【i】就是“跳跃的次数”,即第一次发现该位置有人,跳一次,此时 i为1


跳一次后发现位置还是有元素,再次跳一次,此时 i为2

注意:放置的时候发现没有元素,即 i为0

如下图所示(e代表元素element):





这种方法是线性探测法的优化,加快了放置查找的速度


其他缺点依旧一个不落的继承

1.2.2 开散列(哈希桶)

也就是在顺序表“外”



如图所示,让数组的类型为“链表的节点”,遇到该位置已经被占用,就头插进去就好


用带头+尾插的技巧也是完全可以的,因为插入的时候都是要检测是否重复的,自然到达链尾很容易

有些地方用二叉搜索树,甚至高度平衡的二叉树【即红黑树】


如下图所示:




是否是就很像一个一个的“桶”

所以才叫哈希桶

2. 基础简单的HashSet的模拟

用简单的 int(基本数据类型)

如下图的大概模板

属性:

节点数组

已存放元素个数

负载因子最大值

方法:

构造方法

放置方法(重复则覆盖)

扩容重塑方法

计算负载因子方法

获取key键方法(这里可以认为是判断是否存在)

这里如果是Map,则对应返回的应该是键值对key对应的value值

public class HashBuck {//哈希桶法
    static class Node {
    }
    public Node[] array;
    public int useSize;
    public static final double Load_FACTOR = 0.75;//"全局"常数
    public HashBuck() {
        array = new  Node[10];
    }
    public void put(int key) {
    }
    private void resize() {
    }
    private double calculateLoadFactor() {
    }
    public boolean get(int key) {
    }
}



2.1 属性

2.1.1 节点

//静态内部类
static class Node {
    public int key;
    public Node next;
    public Node(int key) {
        this.key = key;
    }
}
public Node[] array;


2.1.2 已存放元素个数以及实际负载因子


public int useSize;
public static final double Load_FACTOR = 0.75;//"全局"常数

2.2 方法

2.2.1 构造方法

public HashBuck() {
    array = new  Node[10];//默认大小为10
}


2.2.2 放置方法

哈希方法我并没有直接做一个方法出来,一般就是一条表达式而已

这里是 int index = key % array.length;【取模法是很常见的】

public void put(int key) {
    int index = key % array.length;
    Node cur = array[index];
    //检测
    while(cur != null) {
        if(cur.key == key) {
            return;
        }else {
            cur = cur.next;
        }
    }
    //头插
    Node newOne = new Node(key);
    newOne.next = array[index];
    array[index] = newOne;
    //已存放元素+1,并且计算是否超过负载因子最大值,若超过,则扩容重塑
    useSize++;
    if(calculateLoadFactor() > Load_FACTOR) {
        resize();
    }
}



2.2.3 计算负载因子方法

private double calculateLoadFactor() {
    return (double) useSize / array.length;
}


2.2.4 扩容重塑方法

只需要遍历节点数组【哈希表】,null一定代表没有数据

不是null的就将该链表遍历一遍,每一个键值重新放置

这里拿一个新的数组放的原因是,重新放置可能会放在前面也可能放置到后面,那么这个值等一下可能又会被遍历到。

private void resize() {
        //扩容两倍,当然可以其他倍数(系统为1.5倍,随后细说)
        Node[] newArr = new Node[array.length * 2];
        //重塑
        for (int i = 0; i < array.length; i++) {
            while(array[i] != null) {
                int index = array[i].key % newArr.length;
                Node cur = newArr[index];
                Node newOne = new Node(array[i].key);
                if(cur == null) {
                    newArr[index] = newOne;
                }else {
//  尾插法              while(cur.next != null) {
//                        cur = cur.next;
//                    }
//                    cur.next = new Node(array[i].key, array[i].val);
                    newOne.next = cur;
                    newArr[index] = newOne;
                }
                array[i] = array[i].next;
            }
        }
        //指向新的节点数组
        array = newArr;

2.2.5 获取key键方法

public boolean get(int key) {
    int index = key % array.length;
    Node cur = array[index];
    while(cur != null) {
        if(cur.key == key) {
            return true;
        }
        cur = cur.next;
    }
    return false;
}


我在这里就不测试了

3. HashMap的模拟

在【2】的基础下,只需要让节点多一个成员,则多出一层“映射关系”,这就是Map映射

如下代码,大致与上面相似

key为自变量,不能重复,重复则覆盖(value值更改)

public class HashBuck {
    static class Node {
        public int key;
        public int val;
        public Node next;
        public Node(int key, int val) {
            this.key = key;
            this.val = val;
        }
    }
    public Node[] array;
    public int useSize;
    public static final double Load_FACTOR = 0.75;//"全局"常数




public HashBuck() {
        array = new  Node[10];
    }
    public void put(int key, int val) {
        int index = key % array.length;
        Node cur = array[index];
        while(cur != null) {
            if(cur.key == key) {
                cur.val = val;
                return;
            }else {
                cur = cur.next;
            }
        }
        Node newOne = new Node(key, val);
        newOne.next = array[index];
        array[index] = newOne;
        useSize++;
        if(calculateLoadFactor() > Load_FACTOR) {
            resize();
        }
    }
    private void resize() {
        Node[] newArr = new Node[array.length * 2];
        for (int i = 0; i < array.length; i++) {
            while(array[i] != null) {
                int index = array[i].key % newArr.length;
                Node cur = newArr[index];
                Node newOne = new Node(array[i].key, array[i].val);
                if(cur == null) {
                    newArr[index] = newOne;
                }else {
//  尾插               while(cur.next != null) {
//                        cur = cur.next;
//                    }
//                    cur.next = new Node(array[i].key, array[i].val);
                    newOne.next = cur;
                    newArr[index] = newOne;
                }
                array[i] = array[i].next;
            }
        }
        array = newArr;
    }
    private double calculateLoadFactor() {
        return (double) useSize / array.length;
    }
    public int get(int key) {
        int index = key % array.length;
        Node cur = array[index];
        while(cur != null) {
            if(cur.key == key) {
                return cur.val;
            }
            cur = cur.next;
        }
        return -1;
    }
}


4. 数据类型为引用类型

我们刚才用的是int[ ] ,如果是String类型,自定义类型呢?

那么这里就需要用到一个方法hashcode() ,这个方法可以获取到哈希值(引用类型的身份证),这样就可以进行取模运算了

如果是自定义类型,我们得自己重写hashcode()方法,String系统已经重写了

一定一定要重写equals()方法,否则我们认为相同的两个引用,哈希值也会不同

下面是泛型版本:

public class MyHashMap <K, V> {
    //如果用到泛型的话,记住要重写一些方法,如equals 和 hashCode
    //以及在特定位置补上自己的哈希方法
    static class Node<K, V> {
        public K key;
        public V val;
        public Node<K, V> next;
        public Node(K key, V val) {
            this.key = key;
            this.val = val;
        }
    }
    public Node<K, V>[] array;
    public int useSize;
    public static final double Load_FACTOR = 0.75;//"全局"常数
    public MyHashMap() {
        array = (Node<K,V>[])(new Node[10]);
    }
    public void put(K key, V val) {
        int index = key.hashCode() % array.length;
        Node<K, V> cur = array[index];
        while(cur != null) {
            if(cur.key.equals(key)) {
                cur.val = val;
                return;
            }else {
                cur = cur.next;
            }
        }
        Node<K, V> newOne = new Node<>(key, val);
        newOne.next = array[index];
        array[index] = newOne;
        useSize++;
        if(calculateLoadFactor() > Load_FACTOR) {
            resize();
        }
    }
    private void resize() {
        Node<K, V>[] newArr = (Node<K,V>[]) (new Node[array.length * 2]);
        for (int i = 0; i < array.length; i++) {
            while(array[i] != null) {
                int index = array[i].key.hashCode() % newArr.length;
                Node<K, V> cur = newArr[index];
                Node<K, V> newOne = new Node<>(array[i].key, array[i].val);
                if(cur == null) {
                    newArr[index] = newOne;
                }else {
//  尾插               while(cur.next != null) {
//                        cur = cur.next;
//                    }
//                    cur.next = new Node(array[i].key, array[i].val);
                    newOne.next = cur;
                    newArr[index] = newOne;
                }
                array[i] = array[i].next;
            }
        }
        array = newArr;
    }
    private double calculateLoadFactor() {
        return (double) useSize / array.length;
    }
    public V get(int key) {
        int index = key % array.length;
        Node<K, V> cur = array[index];
        while(cur != null) {
            if(cur.key.equals(key)) {
                return cur.val;
            }
            cur = cur.next;
        }
        return null;
    }
}




数组的构建参考的是hashMap源码

下面是随便一个自定义类:


class student {
    char[] name;
    int id;
    int score;
}


【Alt + insert】后点击这个东西,一路next就好了


我们也可以自己去决定“怎么样才算相等”,自己重写equals()


目录
相关文章
|
11月前
|
前端开发 Java
java实现队列数据结构代码详解
本文详细解析了Java中队列数据结构的实现,包括队列的基本概念、应用场景及代码实现。队列是一种遵循“先进先出”原则的线性结构,支持在队尾插入和队头删除操作。文章介绍了顺序队列与链式队列,并重点分析了循环队列的实现方式以解决溢出问题。通过具体代码示例(如`enqueue`入队和`dequeue`出队),展示了队列的操作逻辑,帮助读者深入理解其工作机制。
457 1
|
存储 算法 安全
探究‘公司禁用 U 盘’背后的哈希表算法与 Java 实现
在数字化办公时代,信息安全至关重要。许多公司采取“禁用U盘”策略,利用哈希表算法高效管理外接设备的接入权限。哈希表通过哈希函数将设备标识映射到数组索引,快速判断U盘是否授权。例如,公司预先将允许的U盘标识存入哈希表,新设备接入时迅速验证,未授权则禁止传输并报警。这有效防止恶意软件和数据泄露,保障企业信息安全。 代码示例展示了如何用Java实现简单的哈希表,模拟公司U盘管控场景。哈希表不仅用于设备管理,还在文件索引、用户权限等多方面助力信息安全防线的构建,为企业数字化进程保驾护航。
|
9月前
|
存储 安全 Java
Java 集合面试题从数据结构到 HashMap 源码剖析详解及长尾考点梳理
本文深入解析Java集合框架,涵盖基础概念、常见集合类型及HashMap的底层数据结构与源码实现。从Collection、Map到Iterator接口,逐一剖析其特性与应用场景。重点解读HashMap在JDK1.7与1.8中的数据结构演变,包括数组+链表+红黑树优化,以及put方法和扩容机制的实现细节。结合订单管理与用户权限管理等实际案例,展示集合框架的应用价值,助你全面掌握相关知识,轻松应对面试与开发需求。
434 3
|
11月前
|
存储 Java 编译器
Java 中 .length 的使用方法:深入理解 Java 数据结构中的长度获取机制
本文深入解析了 Java 中 `.length` 的使用方法及其在不同数据结构中的应用。对于数组,通过 `.length` 属性获取元素数量;字符串则使用 `.length()` 方法计算字符数;集合类如 `ArrayList` 采用 `.size()` 方法统计元素个数。此外,基本数据类型和包装类不支持长度属性。掌握这些区别,有助于开发者避免常见错误,提升代码质量。
988 1
|
12月前
|
存储 NoSQL Java
【数据结构进阶】哈希表
哈希表是一种高效的数据结构,通过哈希函数实现数据映射,支持平均O(1)时间复杂度的查找、插入和删除操作。本文详细介绍了哈希表的基本概念、哈希函数的设计(如直接定址法和除留余数法)以及哈希冲突的解决方法(如开放定址法和链地址法)。同时,文章通过代码实例展示了线性探测和链地址法两种哈希表的实现过程,并分析了各自的优缺点。最后总结指出,合理选择哈希函数和冲突解决策略是优化哈希表性能的关键。
1223 2
|
数据库
数据结构中二叉树,哈希表,顺序表,链表的比较补充
二叉搜索树,哈希表,顺序表,链表的特点的比较
数据结构中二叉树,哈希表,顺序表,链表的比较补充
|
存储 缓存 安全
Java 集合江湖:底层数据结构的大揭秘!
小米是一位热爱技术分享的程序员,本文详细解析了Java面试中常见的List、Set、Map的区别。不仅介绍了它们的基本特性和实现类,还深入探讨了各自的使用场景和面试技巧,帮助读者更好地理解和应对相关问题。
253 5
|
Java
让星星⭐月亮告诉你,HashMap中保证红黑树根节点一定是对应链表头节点moveRootToFront()方法源码解读
当红黑树的根节点不是其对应链表的头节点时,通过调整指针的方式将其移动至链表头部。具体步骤包括:从链表中移除根节点,更新根节点及其前后节点的指针,确保根节点成为新的头节点,并保持链表结构的完整性。此过程在Java的`HashMap$TreeNode.moveRootToFront()`方法中实现,确保了高效的数据访问与管理。
159 2
|
Java 索引
让星星⭐月亮告诉你,HashMap之往红黑树添加元素-putTreeVal方法源码解读
本文详细解析了Java `HashMap` 中 `putTreeVal` 方法的源码,该方法用于在红黑树中添加元素。当数组索引位置已存在红黑树类型的元素时,会调用此方法。具体步骤包括:从根节点开始遍历红黑树,找到合适位置插入新元素,调整节点指针,保持红黑树平衡,并确保根节点是链表头节点。通过源码解析,帮助读者深入理解 `HashMap` 的内部实现机制。
248 2
|
算法 Java 容器
Map - HashSet & HashMap 源码解析
Map - HashSet & HashMap 源码解析
244 0