Java中Map和Set详细介绍,哈希桶的实现

大家好呀,前一节我们接触了二叉搜索树,那么紧接着,我们要学习一种十分重要而且也是我们在初阶数据结构中接触的最后一种数据结构—Map和Set,本篇博客将会详细介绍两种数据结构,并且针对哈希表底层实现一个哈希桶,小伙伴们快来一起学起来吧~

一,Map和Set概念与场景

Map和Set是一种专门用来进行搜索的容器或者数据结构,其搜索的效率与其具体的实例化子类有关。我们以前学习过搜索方式比如二分查找和直接遍历比较适合静态查找,即在查找元素程中,我们不会在对数据进行插入和删除操作了,但是在某些需要需要在查找时 进行一些查找操作的情形下,上述两种数据数据结构便不再适合,因此我们便引出了Map和Set这种数据,他们之间的联系非常紧密,是一种适合进行动态查找的数据结构。

模型简介

在Java中,一般把搜索的数据称为关键字(Key),和关键字对应的称为值(Value),将其称之为Key-Value的键值对,所以
模型会有两种:
1. 纯 Key 模型,纯K模型每次枝存储的是一条数据。
2. Key-Value 模型,这种模型存储的是一组数据,这种模型会在数据之间建立联系,可以通过一个数据找到另一个数据
而Map中存储的就是Key-Value的键值对,Set中只存储了Key。

二,Map

Map简介

Map是一个接口类,该类没有继承自Collection,该类中存储的是<K,V>结构的键值对,并且K一定是唯一的,不能重复,Map内部还有一个名字为Entry的接口,也就是说,我们在实Map时,需要把Entry也一并实现,Jvm为我们提供了两种实现了这个接口的类,TreeMap和HashMap(我们可以直接实例化来使用)。首先为大家介绍先为大家介绍Map里的Entry。

3.2 关于Map.Entry<K, V>的说明

在idea中我们可以看到源码

Entry这个接口在Map内部,也就是说,实现Map这个接口的类也一定实现了Entry这个接口,Map.Entry<K, V> 是Map内部实现的用来存放<key, value>键值对映射关系的内部类,我们可以通过Map的entrySet()方法可以获取Map中所有的键值对,返回一个Set集合,其中每个元素都是一个Map.Entry对象。Map.Entry接口包含了访问和操作键值对的方法,如getKey()、getValue()和setValue(),如下

1,K  getKey() 返回 entry 中的 key
2,V  getValue() 返回 entry 中的 value
3,V  setValue(V value) 将键值对中的value替换为指定value

注意:Map.Entry<K,V>并没有提供设置Key的方法

具体使用方法在讲解Map常用方法的最后一个示范,我们先可以看看TreeMap是怎样实现这个接口的

我们知道TreeMap底层是一颗红黑树,上面这张图也表示TreeMap是以红黑树的方式组织数据的。

Map的常用方法及使用

1.V put(K key, V value) 设置 key 对应的 value

演示:

import java.util.Map;
import java.util.TreeMap;

public class Main {
    public static void main(String[] args) {
        Map<String,Integer> treeMap=new TreeMap<>();
        treeMap.put("hello",2);
        treeMap.put("world",3);

    }
}

2.V get(Object key) 返回 key 对应的 value

public class Main {
    public static void main(String[] args) {
        Map<String,Integer> treeMap=new TreeMap<>();
        treeMap.put("hello",2);
        treeMap.put("world",3);
        int val=treeMap.get("hello");
        System.out.println(val);//输出2
    }
}

需要注意的是get()这里方法返回的是一个Integer的值,用int接收自动拆箱,如果用Integer接受则输出null。如果key不存在则抛出异常


3.V getOrDefault(Object key, V defaultValue) 返回 key 对应的 value,key 不存在,返回默认值

和get()   方法相同,只是这个方法可以设置一个默认值,如果key不存在则返回默认的val

public class Main {
    public static void main(String[] args) {
        Map<String,Integer> treeMap=new TreeMap<>();
        treeMap.put("hello",2);
        treeMap.put("world",3);
        int val=treeMap.get("hello");
        System.out.println(val);//输出2
        int val1=treeMap.getOrDefault("abc",100);
        System.out.println(val1);//"abc"不存在,输出100
    }
}


4.V remove(Object key) 删除 key 对应的映射关系

简单来说就是删除Key对应的一组数据,十分简单

5.boolean containsKey(Object key) 判断是否包含 key
6.boolean containsValue(Object value) 判断是否包含 value

这两个方法就是判断是否包含某个key或者val,理解起来没有难度  这里就不作演示了


7.Set<K> keySet() 返回所有 key 的不重复集合

也就是把所有Map中的Key值组织成一个Set,Set我们后面还会继续介绍,还可以用一个Set的对象来接收,里面存放的就是所有Map里的Key值的一个集合

组织起来后我们当然就可以用Set的方法来对他进行操作


8.Collection<V> values() 返回所有 value 的可重复集合

有了可以组织Key值的Set,那么自然有组织Values的方法,不过,这个方法是把所有value值组织成一个Collection对象,这个我们从他的返回值中可以清楚的反映出来,至于Collection嘛,他是一个类,这里我再放出这张图

当然我们也可以用一个对象来接收

Collection<Integer> collection=treeMap.values();

9.Set<Map.Entry<K, V>> entrySet() 返回所有的 key-value 映射关系

这个方法和我们上面所说的的Entry接口有关,它主要是把Map的一个个节点组织成一个Set,注意是节点,这个从他的返回值可以看出,下面演示用法

import java.util.Collection;
import java.util.Map;
import java.util.Set;
import java.util.TreeMap;

public class Main {
    public static void main(String[] args) {
        Map<String,Integer> treeMap=new TreeMap<>();
        treeMap.put("hello",2);
        treeMap.put("world",3);
        Set<Map.Entry<String, Integer>> set=treeMap.entrySet();
        //试试怎么用for—each循环遍历
        for (Map.Entry<String, Integer> s : set) {//注意s的类型
            //这里就可以调用上面entry介绍的三种方法
            s.getKey();
            s.getValue();
            s.setValue(1);
        }
    }
}

迭代器遍历方式

学习了entrySet方法下面额外为大家介绍一种用迭代器遍历Map的方式,

我们可以看出Set继承了Iterator接口,但是Map没有,所以我们不能直接用迭代器的方式来遍历Map,但是通过上面的方法。我们可以先把Map转化为Set,再进行遍历

import java.util.*;

public class Main {
    public static void main(String[] args) {
        Map<String, Integer> treeMap = new TreeMap<>();
        treeMap.put("hello", 2);
        treeMap.put("world", 3);
        Set<Map.Entry<String, Integer>> set = treeMap.entrySet();
        Iterator<Map.Entry<String, Integer>> it = set.iterator();
        while (it.hasNext()) {
            System.out.println(it.next() + " ");
        }
    }
}

Map的方法就差不多说到这里了

注意事项:


1. Map是一个接口,不能直接实例化对象,如果要实例化对象只能实例化其实现类TreeMap或者HashMap
2. Map中存放键值对的Key是唯一的,value是可以重复的
3. 在TreeMap中插入键值对时,key不能为空,否则就会抛NullPointerException异常,value可以为空。但是HashMap的key和value都可以为空。
4. Map中的Key可以全部分离出来,存储到Set中来进行访问(因为Key不能重复)。
5. Map中的value可以全部分离出来,存储在Collection的任何一个子集合中(value可能有重复)。
6. Map中键值对的Key不能直接修改,value可以修改,如果要修改key,只能先将该key删除掉,然后再来进行重新插入。
7. TreeMap和HashMap的区别

三,Set

Set的常用方法

Set与Map主要的不同有两点:Set是继承自Collection的接口类,Set中只存储了Key。其余的很多使用方法很相似,这里简略概述

1.boolean add(E e) 添加元素,但重复元素不会被添加成功
2.void clear() 清空集合
3.boolean contains(Object o) 判断 o 是否在集合中
4.boolean remove(Object o) 删除集合中的 o
5.int size() 返回set中元素的个数
6.boolean isEmpty() 检测set是否为空,空返回true,否则返回false

上面六个方法很基础也很简单,这里一起演示

public class Main {
    public static void main(String[] args) {
        Set<String> treeSet=new TreeSet<>();
        treeSet.add("hello");
        treeSet.add("world");
        boolean boo=treeSet.contains("hello");//输出true,不存在则输出false
        int size=treeSet.size();//2
        treeSet.remove("hello");
        System.out.println(treeSet.isEmpty());
        treeSet.contains("hello");//false
    }
}

7.Iterator<E> iterator() 返回迭代器

和上面介绍过的相同,主要是迭代器遍历

public class Main {
    public static void main(String[] args) {
        Set<String> treeSet=new TreeSet<>();
        treeSet.add("hello");
        treeSet.add("world");
        Iterator<String> iterator=treeSet.iterator();
        while (iterator.hasNext()){
            System.out.println(iterator.next()+" ");
        }
    }
}


8,Object[] toArray() 将set中的元素转换为数组返回

需要注意返回的是Object对象需要强转

public class Main {
    public static void main(String[] args) {
        Set<String> treeSet=new TreeSet<>();
        treeSet.add("hello");
        treeSet.add("world");
        String[] strings= (String[]) treeSet.toArray();
        
    }
}


9.boolean containsAll(Collection<?> c) 集合c中的元素是否在set中全部存在,是返回true,否则返回false

public class Main {
    public static void main(String[] args) {
        Set<String> treeSet=new TreeSet<>();
        treeSet.add("hello");
        treeSet.add("world");
        Set<String> treeSet1=new TreeSet<>();
        treeSet.add("hello");
        treeSet.add("abc");
        treeSet.contains("hello");
        treeSet.containsAll(treeSet1);

    }
}


10.boolean addAll(Collection<? extendsE> c) 将集合c中的元素添加到set中,可以达到去重的效果

注意,这个集合必须继承自Collection

import java.util.*;

public class Main {
    public static void main(String[] args) {
        Set<String> treeSet=new TreeSet<>();
        treeSet.add("hello");
        treeSet.add("world");
        Set<String> treeSet1=new TreeSet<>();
        treeSet.add("hello");
        treeSet.add("abc");
        treeSet.addAll(treeSet1);//有去重效果
        for (String s:treeSet) {
            System.out.println(s);
        }
    }
}

注意事项

1. Set是继承自Collection的一个接口类
2. Set中只存储了key,并且要求key一定要唯一
3. TreeSet的底层是使用Map来实现的,其使用key与Object的一个默认对象作为键值对插入到Map中的

(源码)


4. Set最大的功能就是对集合中的元素进行去重
5. 实现Set接口的常用类有TreeSet和HashSet,还有一个LinkedHashSet,LinkedHashSet是在HashSet的基础上维护了一个双向链表来记录元素的插入次序。
6. Set中的Key不能修改,如果要修改,先将原来的删除掉,然后再重新插入
7. TreeSet中不能插入null的key,HashSet可以

四,哈希桶的实现

实现哈希表有多种方式,要点还是在于如何解决哈希冲突,这里采用哈希桶的方式,用数组-链表结合来实现

哈希冲突

定义:(百度的)

哈希冲突‌是指在哈希表中,两个或更多个不同的键被映射到了同一个哈希桶的情况这种情况可能会导致数据丢失或者检索效率下降,因为不同的键被映射到了同一个位置,需要额外的操作来处理这种冲突。

简单来说,我们再用哈希表(注:哈希表可以是数组,链表,树等数据结构)存储数据时,因为要根据要插入的数值大小来确定这个树的存储位置,就有可能会出现多个数据映射到同一位置的情况,举个例子:

我们定义一个长度为10的哈希表(这里采用数组)并以数据在数组中的下标=数据大小%数组长度的方式来确定数组位置,那么这时候,我们可能会遇到一个问题,如图

当然,为了解决这个问题也有很多方法,我们这里采用哈希桶的方法,即采用数组+链表的方式来存储数据

这样,哈希冲突便得到了有效解决,当然,如果数据过多的时候,我们就会对数组进行扩容,一般来说,当数据个数/数组长度 >0.75时,我们对数组进行扩容。这里的0.75我们称作负载因子,可以自己定义,一般在0.75左右最佳

代码实现

初始准备:

因为我们采用数组+链表模拟,所以我们需要定义链表节点的值和存放这个链表的数组

class HashBuck{
    class Node
    {
        int key;//哈希表是KEY-VAl模型
        int val;
        Node next;
        Node(int val,int key){
            this.val = val;
            this.key = key;
        }
    }
    Node[] arrays=new Node[10];//初始数组长度
    int useSize=0;//数组里面元素个数
  }
}

插入操作

假设我们在传入值为key和关键码为val的值,我们一般以key%数组长度来确定此值在数组中的位置,在插入时,还需要判断数组中是否有值为val的值,因为哈希表中的值不能重复,所以如果有值为val的值时,我们便更新它的关键码为新的key值,否则,我们在数组对应位置中的链表中头插或者尾插一个新的节点,这里选择头插或者尾插均可,不是我们关注的重点,最后别忘记了useSize++


    public void Insert(int val,int key){
        int index=key/ arrays.length;//确定数据在数组中的位置
        Node cur=arrays[index];//用于遍历数组中的链表
        while(cur!=null){
            if(cur.val==val){//有值为val的节点则更新它的key后走人~
                cur.key=key;
                return;
            }
            cur=cur.next;
        }
        Node node=new Node(val, key);
       //这里选择头插的方式
        node.next=arrays[index];
        arrays[index]=node;
        useSize++;
}

在插入操作基本完成后,这个时候就需要我们关注哈希冲突的问题,当负载因子大于0.75时,我们就对数组进行扩容,写一个计算负载因子的方法,如果超过负载因子,就对数组进行扩容。但这个时候千万不能像顺序表那样扩容,因为数组的变化会引起代码中的index的变化(index=key%数组长度),这个时候我们需要进行遍历和重新哈希,来达到调整的目的,此时我们可以重新封装一个方法

计算负载因子

private static final double LoadFactor=0.75f;
private double doLoadFactor(){
        return useSize*1.0/arrays.length;//useSize*1.0才能计算出小数
    }

然后就可以来调整数组大小了,调整过后需要重新哈希

重新哈希

 private void resize() {
        Node[] array=new Node[2*arrays.length];//新的数组
        for (int i = 0; i <array.length; i++) {//开始遍历
            Node cur=arrays[i];
            while(cur!=null){
                 Node curN=cur.next;//必须要先记录下下一个位置,否则可能会因为下面的操作打乱
                int index=cur.key/array.length;//新的index
                 cur.next=array[index];
                 array[index]=cur;
                 cur=curN;
            }
        }
          arrays=array;//新的数组给旧的数组
    }

最后提一句,在JVM中,当数组长度超过64或者链表长度超过8时,就会把哈希桶调整成树

完整代码

class HashBuck{
    class Node
    {
        int val;
        int key;
        Node next;
        Node(int val,int key){
            this.val = val;
            this.key = key;
        }
    }
    Node[] arrays=new Node[10];
    int useSize=0;
    private static final double LoadFactor=0.75f;
//插入
    public void Insert(int val,int key){
        int index=key/ arrays.length;
        Node cur=arrays[index];
        while(cur!=null){
            if(cur.val==val){
                cur.key=key;
                return;
            }
            cur=cur.next;
        }
        Node node=new Node(val, key);
        node.next=arrays[index];
        arrays[index]=node;
        useSize++;

        if(doLoadFactor()>0.75){
            resize();
        }
    }
//重新哈希
    private void resize() {
        Node[] array=new Node[2*arrays.length];
        for (int i = 0; i <array.length; i++) {
            Node cur=arrays[i];
            while(cur!=null){
                 Node curN=cur.next;
                int index=cur.key/array.length;
                 cur.next=array[index];
                 array[index]=cur;
                 cur=curN;
            }
        }
          arrays=array;
    }
//计算负载因子
    private double doLoadFactor(){
        return useSize*1.0/arrays.length;
    }
}

以上就是全部内容了,感谢大家支持。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值