【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.2 冲突的解决

1.2.1 闭散列
  • 散列这个说法一些书籍仍在使用【哈希表的意思】
  • 那么闭散列就是在顺序表内部解决这个问题
    • 至于用顺序表是因为下标访问快
  1. 线性探测法
    • 发现用哈希方法解析后的下标,已经被占用,那么从左往右探测,第一个空的就可以放上去
      • 缺点很大:放的效率很低,取的时候效率也低,要找的那个位置不是我们要的数据,那么就要“线性探测”从左往右遍历找我们的数据。
      • 并且我们很难去区分这个位置是否有元素,比如int[] 可能值就是0,引用类型也可能值就是null,怎么就能说明那个位置空了呢?
      • 很容易达到负载值而需要重新扩容
    • 这个方法很少用,最多出道题恶心人。
      • 例如问,按照线性探测法找,需要多少步?

在这里插入图片描述

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

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

    • 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()

5. 集合类的基本用途与使用

5.1 实例化Map

  • 一般用普通类实例化接口的形式,这样这个引用的功能更加具有针对性。

  • 接下来来看看Map的一些基本功能(高亮即重点)

  • 方法解释
    V get(Object key)返回key对应的value值
    V getOrDefault(Object key, V defaultValue)返回对应value,不存在则返回defaultValue
    V put(K key, V value)放置键值对(key重复则覆盖)
    V remove(Object key)根据唯一的key删除键值对
    Set keySet()key不重复,将所有key值放在Set里并返回
    Collection values()将所有values放在集合中并返回
    Set<Map.Entry<K, V>> entrySet()返回所有键值对集(entry即条目)
    boolean containsKey(Object key)是否存在key值
    boolean containsValue(Object value)是否存在value值
  • 其中还有一个特别重要的静态内部接口,Entry

  • 这里我们可以理解为,将key和value打包起来了,成为一个独立的引用,其实里面

  • 方法解释
    K getKey()获取键值对的key值
    V getValue()获取键值对的value值
    V setValue(V value)设置键值对的value值
    Set<Map.Entry<Integer, Integer>> set = hashMap.entrySet();
    Map.Entry<Integer, Integer>[] entry = (Map.Entry<Integer, Integer>[]) set.toArray();//把map的 key 和 value 组装成一个整体
    entry[0].getKey();
    entry[0].getValue();
    entry[0].setValue();
    
  • 无法设置key,转化为Set有什么用,等一下将Set的时候细说!

  • 接下来解答一些疑惑,就是为什么返回类型是接口/抽象类型,但是仍然可以正常使用?

    • 原因就是,重写方法的规矩除了完全相等以外,还有一个例外
      • 就是,返回类型呈现继承/实现关系,【父亲方法的返回类型】被【子类方法的返回类型】继承或者实现,也是重写!
      • 这样,返回类型就可以是普通类啦,并且哈希实现就是哈希,树实现就是树。
  • 至于Entry的本质

    • 在这里插入图片描述

    • 这是源码,Entry被Node实现后,

      • 只要被Node实例化,【对应的引用就有了对应的Node】,也就有了对应的【key,value】
      • 这是因为,方法被重写后,访问key和value并非private
        • 这样就能够在父类访问子类成员了!
        • 这样,使用Entry的get…方法,就相当于有对应的Node引用使用了get…方法,就获取到了对应的key,value值
    • 对于entrySet()方法

    • 在这里插入图片描述

    • 看不懂不重要,重要的是解决了我们的疑问

      • 就是,Entry被一个特定的Node实例化后,Node被删除,还访问得到吗?
      • 明确的告诉你,还可以,因为这里的节点被(深)拷贝了一份,Entry是被这份拷贝实例化的!
注意
  1. key唯一,value不唯一,key不可被设置,只能删掉重新放置
  2. 哈希里是可以存放null的,而树不可以(因为null不能比较,会抛异常的)
  3. Map并不是”Collection家族“的

5.2 实例化Set

  • 下面是Set的基本功能(高亮即重点)

  • 方法解释
    boolean add(E e)增加元素,重复则会失败,返回false
    void clear()清空集合
    boolean contains(Object o)查看元素是否存在
    Iterator iterator()返回迭代器
    boolean remove(Object o)删除对应元素
    int size()返回集合大小
    boolean isEmpty()集合是否为空
    Object[] toArray()转化为数组(非基本数据类型数组!)
    boolean containsAll(Collection<?> c)查询c集合的所有元素是否个个都存在
    boolean addAll(Collection<? extends E> c)添加一整个集合的所有元素,去重作用(完全不重复则返回true)(有现成的集合类的时候适用)
    5.2.1 迭代器
    • 这个“工具”的作用就是遍历集合

    • 用法如下:

      • hasNext() 与 next() 这两个方法打配合
    • public static void main(String[] args) {
          Map<String, Integer> hashMap = new HashMap<>();
          hashMap.put("小卡拉", 3);
          hashMap.put("马拉圈", 4);
          hashMap.put("芜湖", 5);
          System.out.println(hashMap.get("小"));
          
          Set<Map.Entry<String, Integer>> set = hashMap.entrySet();
          
          //for-each 语法遍历
          for(Map.Entry<String, Integer> s : set) {
              System.out.print(s.getKey() + " " + s.getValue() + " ");
          }
          
          //迭代器遍历法
          Iterator<Map.Entry<String, Integer>> iter =  set.iterator();
          //Iterator<?> 用通配符也可以
          while(iter.hasNext()) {
              System.out.print(iter.next() +  " ");
          }
      
      }
      
5.2.2 toArray()
  • 这个方法有两个用法

  • (T[])set.toArray();
    //需要强制类型转化!由于擦除机制,数组的返回是Object[]
    
  • set.toArray(arr);
    //这个arr必须是对应的非基本数据类型数组!或者父类数组
    //set里面的元素被整合到arr里了
    //返回值可以不接收,接收的话,方式跟上面一样
    
  • 需要重点强调的一点是,基本数据类型的数组与其包装类的数组不能直接相互转化(自然也不存在自动拆箱装箱),必须遍历一遍!!!

其他方法相对简单,不细说

注意
  1. Set是Collection家族的接口类
  2. key值唯一,Set重点在于除重
  3. key要修改,必须删除再添加
  4. 树的key不能是null,因为需要比较
  5. TreeSet/HashSet底层由Map实现,value为Object类型的一个默认对象,key就是key
    • 节省功夫嘛

在这里插入图片描述

  • Set和Map 还可用 LinkedHashSet和LinkedHashMap实例化
    • 这维护了双向链表,可以记录元素的插入次序

6. 哈希部分源码

  • 刚才已经接触了一些源码了,接下来只有一些补充了

6.1 HashMap

6.1.1 一些常量
  • 在这里插入图片描述

    • 在这里插入图片描述

      • 这个是默认的表的默认容量
    • 在这里插入图片描述

      • 这个是表的最大容量
    • 在这里插入图片描述

      • 这个是负载因子默认最大值
    • 在这里插入图片描述

      • 这个哈希桶树化临界值
        • 即桶内节点达到8,就会折叠成红黑树(原本只是链表)
    • 在这里插入图片描述

      • 这个是哈希桶退化为链表的临界值
        • 即桶内节点被删除到6,就会退化成链表(原本是红黑树)
    • 在这里插入图片描述

      • 这个是哈希桶树化前提的前提
        • 就是,表的容量很少时,可能会导致桶内的东西太多了【就是冲突次数多】
        • 那么规定一个值,当表的已有元素总数(节点总个数)达到这个值的时候,才考虑折叠链表为红黑树,其他情况直接扩容即可其他情况是指,桶内元素达到界值后不选择树化,而是扩容哈希表
        • 为了避免进行扩容、树形化选择的冲突,这个值不能小于 4 * TREEIFY_THRESHOLD
    • 树化方法treeifyBin 就不带大家看了

6.1.2 哈希方法

在这里插入图片描述

  • 此方法的作用是让值放的更分散
6.1.3 调整容量
  • 这个操作是将容量调整为最接近的二进制数(较大)

    • 2, 4, 8, 16…
    • 原理就是,让二进制序列最左边的1之后的位都变成1,
      • 【10101】= =》【11111】= =》【100000】
      • 右移按位与是让这个位【1】成倍数往后扩展
        • 就是原本可能只有一个1扩展为两个1,扩展到四个1,这样最快
          在这里插入图片描述
  • 在一个构造方法中有用到

    • 用途:搭配哈希方法降低冲突率
6.1.4 构造方法

在这里插入图片描述

  1. 提供容量和负载因子
  2. 提供容量
  3. 什么都不提供
  4. 提供一个Map,将整个Map的所有元素(拷贝成新的一个引用)
6.1.5 LinkedHashMap
  • 在HashMap中并没有实例化对象LinkedHashMap的构造方法,但是有继承它的…
  • 在这里插入图片描述
  • 了解即可

6.2 HashSet

  • 底层也是Map,key就是key,value是一个Object默认值
  • 主要带大家看看构造方法
  • 在这里插入图片描述
  1. 不带参数构造方法
  2. 提供一个集合引用,将所有元素导入Set里
  3. 提供容量和负载因子
  4. 提供容量
  5. 提供容量,负载因子以及布尔类型
    • 这个类型并没有什么用,只是区分其他构造方法而已

    • 在这里插入图片描述

    • 就是构造LinkedHashMap( )

    • 本质上LinkedHashSet就是用到的就是这个方法

    • 在这里插入图片描述


文章到此结束!谢谢观看
可以叫我 小马,我可能写的不好或者有错误,但是一起加油鸭🦆

这是我的代码仓库!(在马拉圈的23.2里)代码仓库

Map&Set · 代码位置

邮箱:2040484356@qq.com

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

s:103

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值