Map和Set

Map和Set_第1张图片


目录

Map

Map常用方法的使用

Set

Set常用方法的使用

例题

二叉搜索树

哈希表

 哈希冲突

避免冲突

小结


Map

Map 是一个接口类,该类没有继承自 Collection ,该类中存储的是 结构的键值对,并且 K 一定是唯一的,不能重复

Map和Set_第2张图片

Map常用方法的使用

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

 map.put(1,"hello");
 map.put(2,"world");

//结果:{1=hello, 2=world}

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

map.get(1);

//结果:hello

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

String str=map.getOrDefault(2,"hi");
String str1=map.getOrDefault(3,"hi");

//结果:
//str  world  
//str1 hi

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

map.remove(1);
System.out.println(map);

//结果:{2=world}

5、Set keySet() 返回所有 key 的不重复集合

 map.put(1,"hi");
 map.put(2,"thanks");
 map.put(2,"what");
 map.put(3,"how");
 System.out.println(map.keySet());

//结果:[1, 2, 3]

6、Set> entrySet()   //返回所有的 key-value 映射关系

map.put(1,"hi");
map.put(2,"thanks");
map.put(3,"how");
for(Map.Entry entry : map.entrySet()){
        System.out.println(entry.getKey() + "--->" + entry.getValue());
   }
   System.out.println();

//结果:
1--->hi
2--->thanks
3--->how

7、boolean containsKey(Object key)   //判断是否包含 key

     boolean containsValue (Object value)   //判断是否包含 value
 map.put(1,"hi");
 map.put(2,"thanks");
 map.put(3,"how");
 map.containsKey(1);//true
 map.containsKey(5);//false
 map.containsValue("hi");//true
 map.containsValue("hello");//false
补充说明:Map.Entry Map 内部实现的用来存放 键值对映射关系的内部类 ,该内部类中主要提供了 的获取, value 的设置以及Key的比较方式。使用Map.Entry类,你可以得到在同一时间得到所有的信息 (注意:Map.Entry 并没有提供设置 Key 的方法 )
K getKey()          //返回 entry 中的 key
V getValue()        //返回 entry 中的 value
V setValue(V value) //将键值对中的value替换为指定value

注意:

1. Map 是一个接口,不能直接实例化对象 如果 要实例化对象只能实例化其实现类 TreeMap 或者 HashMap 。
Map和Set_第3张图片

2. Map中存放键值对的Key是唯一的,value是可以重复的 。

Map和Set_第4张图片

3. 在 Map 中插入键值对时, key 不能为空,否则就会抛 NullPointerException 异常 ,但是 value可以为空 。
Map和Set_第5张图片

Map和Set_第6张图片

4. Map中的Key可以全部分离出来,存储到Set来进行访问(因为Key不能重复)。

5. Map中的value可以全部分离出来,存储在Collection的任何一个子集合中(value可能有重复)。

6. Map中键值对的 Key 不能直接修改, value 可以修改,如果要修改 key ,只能先将该 key删除掉,然后再来进行 重新插入。

Set

Set Map 主要的不同有两点: Set 是继承自 Collection 的接口类, Set 中只存储了 Key

Map和Set_第7张图片

Set常用方法的使用

1、boolean add(E e)   //添加元素,但重复元素不会被添加成功

      Set set=new HashSet<>();
      set.add(1);
      set.add(2);
      set.add(3);
      boolean flg=set.add(1);
      System.out.println(flg);
      System.out.println(set);

//结果
//false
//[1, 2, 3]

2、boolean contains(Object o)   //判断 o 是否在集合中

      boolean flg=set.contains(1);//true
      boolean flg1=set.contains(6);//false

3、Iterator iterator()   //返回迭代器

      set.add(1);
      set.add(2);
      set.add(3);
      Iterator it= set.iterator();
      while(it.hasNext()){
        System.out.print(it.next()+" ");
      }

//结果:1  2  3

4、boolean remove(Object o)  //删除集合中的 o

      set.add(1);
      set.add(2);
      set.add(3);
      boolean flg2=set.remove(1);
      System.out.println(flg2);//true
      System.out.println(set);//[2, 3]

5、int size()  //返回set中元素的个数

      set.add(1);
      set.add(2);
      set.add(3);
      System.out.println(set.size());//3

6、boolean isEmpty()   //检测set是否为空,空返回true,否则返回false

7、Object[] toArray()   //将set中的元素转换为数组返回

      Set set1=new HashSet<>();
      Object[] array1=set1.toArray();

      Set set2=new HashSet<>();//指定类型
      Integer[] array=set2.toArray(new Integer[0]);

     
   
注意:
1. Set 是继承自 Collection 的一个接口类
2. Set 中只存储了 key ,并且要求 key 一定要唯一
3. Set 的底层是使用 Map 来实现的,其使用 key Object 的一个默认对象作为键值对插入到 Map 中的
4. Set 最大的功能就是对集合中的元素进行去重
Map和Set_第8张图片

5. 实现Set接口的常用类有TreeSetHashSet,还有一个LinkedHashSetLinkedHashSet是在HashSet的基础 上维护了一个双向链表来记录元素的插入次序。

6. Set 中的 Key 不能修改,如果要修改,先将原来的删除掉,然后再重新插入 。
7. Set 中不能插入 null key

例题

1、在1_0000个数据里面找重复的数据

public  static Map func(int[] array){
        //判断array中的元素是否在map中,如果不在就是1,如果在就在原来的基础上加1
        Map map=new HashMap<>();
        for(int x:array){
            if(map.get(x)==null){
                map.put(x,1);
            }else{
                int val=map.get(x);
                map.put(x,val+1);
            }
        }
        return map;
    }

    public static void main(String[] args) {
        int[] array=new int[10000];
        Random random=new Random();
        for(int i=0;i< array.length;i++){
            array[i]=random.nextInt(1000);//生成的随机数在0-1000之间,一定有重复的元素
        }
        Map map=func(array);
        System.out.println(map);
        String str="array";
        System.out.println(str.toCharArray());
    }

2、将10000个数据去重

 public static Set func(int[] array){
        Set set=new HashSet<>();
        for (int x:array) {
            set.add(x);
        }
        return set;
    }

 public static void main(String[] args) {
        int[] array=new int[10000];
        Random random=new Random();
        for (int i = 0; i < array.length; i++) {
            array[i]=random.nextInt(1000);
        }
        Set set=func(array);
        System.out.println(set);
       int ret=func1(array);
        System.out.println(ret);
    }

二叉搜索树

二叉搜索树又称二叉排序树,它或者是一棵空树 ,或者是具有以下性质的二叉树
若它的左子树不为空,则左子树上所有节点的值都小于根节点的值
若它的右子树不为空,则右子树上所有节点的值都大于根节点的值
它的左右子树也分别为二叉搜索树

Map和Set_第9张图片

 查找

 在二叉搜索树中查找某个值key,就用key和根节点比较,key更大,在右子树查找,key更小,在左子树查找。

Map和Set_第10张图片

代码示例:

    public Node search(int key){
      Node cur=root;
      while(cur!=null){
          if(key>cur.val){
              cur=cur.right;
          }else if(key

插入

在二叉搜索树中插入某个结点,要不破坏它的性质,所以要先找到合适插入的位置,然后再插入

   public boolean insert(int val){
       //空树情况下
        if(root==null){
            root=new Node(val);
            return true;
        }

       //找到要插入的位置      
        Node cur=root;
        Node parent=null;//记录父节点
        while(cur!=null) {
            if (val > cur.val) {
                parent = cur;
                cur = cur.right;
            } else if (val < cur.val) {
                parent = cur;
                cur = cur.left;
            } else {
                return false;//不能有相同数据
            }
        }

        //插入
            Node tmp=new Node(val);
            if(val

删除

删除二叉搜索树中某个结点,先找到要删除的位置,在进行删除

Map和Set_第11张图片

 代码示例:

  public void remove(int key){
      Node cur=root;
      Node parent=null;
      //找到要删除结点位置
        while(cur!=null){
          if(cur.val==key){
              removeNode(cur,parent);
              break;
          }else if(cur.val

哈希表

顺序结构以及平衡树 中,元素关键码与其存储位置之间没有对应的关系,因此在 查找一个元素时,必须要经过关键码的多次比较 顺序查找时间复杂度为 O(N) ,平衡树中为树的高度,即 O( log2^N
) ,搜索的效率取决于搜索过程中 元素的比较次数。
理想的搜索方法:可以 不经过任何比较,一次直接从表中得到要搜索的元素。  如果构造一种存储结构,通过某种函数(hashFunc)使元素的存储位置与它的关键码之间能够建立一一映射的关系,那么在查找时通过该函数可以很快找到该元素。
当向该结构中:
插入元素
根据待插入元素的关键码,以此函数计算出该元素的存储位置并按此位置进行存放 。
搜索元素
对元素的关键码进行同样的计算,把求得的函数值当做元素的存储位置,在结构中按此位置取元素比较,若关键码相等,则搜索成功。
该方式即为哈希( 散列 ) 方法, 哈希方法中使用的转换函数称为哈希 ( 散列 ) 函数,构造出来的结构称为哈希表(Hash Table)(或者称散列表)。

 哈希冲突

当一个哈希函数为: hash(key) = key % capacity ;( capacity 为存储元素底层空间总的大小)
Map和Set_第12张图片

我们可以看到4和14通过相同哈希函数找到了相同的位置,发生了冲突。

此时不同的关键字通过相同的哈希函数有可能找到相同的位置。此时的情况就叫: 哈希冲突/哈希碰撞。

避免冲突

首先,我们需要明确一点,由于我们哈希表底层数组的容量往往是小于实际要存储的关键字的数量的,这就导致一个问题, 冲突的发生是必然的 ,但我们能做的应该是尽量的 降低冲突率。
哈希函数设计
引起哈希冲突的一个原因可能是: 哈希函数设计不够合理 哈希函数设计原则
1.哈希函数的定义域必须包括需要存储的全部关键码,而如果散列表允许有 m 个地址时,其值域必须在 0 m-1 之间
2.哈希函数计算出来的地址能均匀分布在整个空间中
3.哈希函数应该比较简单
常见哈希函数
1. 直接定制法 --( 常用 )
取关键字的某个线性函数为散列地址: Hash Key = A*Key + B 优点:简单、均匀。缺点:需要事先知道关键字的分布情况。使用场景:适合查找比较小且连续的情况。
2. 除留余数法 --( 常用 )
设散列表中允许的 地址数为 m ,取一个不大于 m ,但最接近或者等于 m 的质数 p 作为除数,按照哈希函数:Hash(key) = key% p(p<=m), 将关键码转换成哈希地址 。
负载因子调节

散列表的载荷因子定义为: a =填入表中的元素个数/散列表的长度。
a是散列表装满程度的标志因子。由于表长是定值,a与“填入表中的元素个数”成正比,所以,a越大,表明填入表中的元素越多,产生冲突的可能性就越大。反之,a越小,标明填入表中的元素越少,产生冲突的可能性就越小。实际上,散列表的平均查找长度是载荷因子a的函数,只是不同处理冲突的方法有不同的函数。对于开放定址法,荷载因子是特别重要因素,应严格限制在0. 7-0.8以下。超过0.8,查表时的CPU缓存不命中( cachemissing)按照指数曲线上升。因此,一些采用开放定址法的hash库,如Java的系统库限制了荷载因子为0.75,超过此值将resize散列表。

随着负载因子的增大,冲突率也在增大,所以当冲突率达到一个无法忍受的程度时,我们需要通过降低负载因子来变相的降低冲突率。已知哈希表中已有的关键字个数是不可变的,那我们能调整的就只有哈希表中的数组的大小。

解决哈希冲突 两种常见的方法是: 闭散列 开散列

闭散列

也叫开放定址法,当发生哈希冲突时,如果哈希表未被装满,说明在哈希表中必然还有空位置,那么可以把 key 存放到冲突位置中的 下一个 空位置中去。 那如何寻找下一个空位置呢?
比如上面的场景,现在需要插入元素 14 ,先通过哈希函数计算哈希地址,下标为 4 ,因此 14 理论应该插在该 位置,但是该位置已经放了值为 4 的元素,即发生哈希冲突。这时我们可以进行线性探测找到下一个位置。
线性探测:从发生冲突的位置开始,依次向后探测,直到寻找到下一个空位置为止。
Map和Set_第13张图片

 但是线性探测有一个不好的地方,它依次向后探测,直到寻找到下一个空位置为止,如果空位置是连续的那么产生冲突的元素都放在一起了。

开散列

开散列法(哈希桶)又叫链地址法 ( 开链法 ),首先对关键码集合用散列函数计算散列地址,具有相同地址的关键码归于同一子集合,每一个子集合称为一个桶,各个桶中的元素通过一个单链表链接起来,各链表的头结点存储在哈希表中。开散列,可以认为是把一个在大集合中的搜索问题转化为在小集合中做搜索了。
Map和Set_第14张图片

 实现

public class HashBuck {

    static class Node{
        public int key;
        public int val;
        Node next;

        public Node(int key,int val){
            this.key=key;
            this.val=val;
        }
    }
    public Node[] array;
    public int usedSize;
    public static final double DEFAULT_LOAD_FACTOR = 0.75;//负载因子
    public HashBuck(){
        this.array=new Node[10];
    }
    /**
     * put函数
     * @param key
     * @param val
     */
    public void put(int key,int val){
        //1、确认key所在位置
        int index=key% array.length;
        Node cur=array[index];
        //2、遍历这个下标的链表,看是不是有相同的key,有的话要更新val值
        while(cur!=null){
            if(cur.key==key){
                cur.val=val;
                return;
            }
            cur=cur.next;
        }
        //3、没有key这个元素,用头插法插入
        Node node=new Node(key,val);
        node.next=array[index];
        array[index]=node;
        this.usedSize++;
        //4、插入元素成功之后,检查当前散列表的负载因子
        if(loadFactor()>=DEFAULT_LOAD_FACTOR){
            resize();//扩容
        }
    }
    private void resize(){
        Node[] newArray=new Node[array.length*2];
        for(int i=0;i< newArray.length;i++){
            Node cur=newArray[i];
            while(cur!=null){
                int index= cur.key% newArray.length;//获取新下标
                //就是把cur这个节点,以头插的形式 插入到新的数组对应下标的链表当中
                Node curNext=cur.next;
                cur.next=newArray[index];
                newArray[index]=cur;
                cur=curNext;
            }
        }
        array=newArray;
    }
    private double loadFactor() {
        return 1.0*usedSize/ array.length;
    }

    /**
     * 根据key获得val值
     * @param key
     * @return
     */
    public int get(int key){
        //1、确认key所在位置
        int index=key% array.length;
        Node cur=array[index];
        //2、遍历这个下标的链表,找到key,返回当前val值
        while(cur!=null){
            if(cur.key==key){
               return cur.val;
            }
            cur=cur.next;
        }
        return -1;
    }
}
java 中计算哈希值实际上是调用的类的 hashCode 方法,进行 key 的相等性比较是调用 key equals 法。所以如果要用自定义类作为 HashMap key 或者 HashSet 的值, 必须重写 hashCode equals ,而且要做到 equals 相等的对象,hashCode 一定是一致的,但是hashCode相等的两个对象,equals不一定相等。
代码示例:
class Person{
    public String ID;
    public Person(String ID){
        this.ID=ID;
    }
    //重写
    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        Person person = (Person) o;
        return Objects.equals(ID, person.ID);
    }

    @Override
    public int hashCode() {
        return Objects.hash(ID);
    }

    @Override
    public String toString() {
        return "Person{" +
                "ID='" + ID + '\'' +
                '}';
    }
}
public class HashBuck2 {

    static class Node{
        public K key;
        public V val;
        public Node next;
        public Node(K key,V val){
            this.key=key;
            this.val=val;
        }
    }
    public Node[] array=( Node[])new Node[10];
    int usedSize;

    /**
     * put函数
     * @param key
     * @param val
     */
    public void put(K key,V val){
        int hash=key.hashCode();
        int index=hash% array.length;
        Node cur=array[index];
        while(cur!=null){
        //利用equals进行相等性比较
            if(cur.key.equals(key)){
                cur.val=val;
                return;
            }
            cur=cur.next;
        }
        Node node=new Node<>(key,val);
        node.next=array[index];
        array[index]=node;
        this.usedSize++;
    }

    /**
     * 根据key获得val值
     * @param key
     * @return
     */
    public V get(K key){
        int hash=key.hashCode();
        int index=hash% array.length;
        Node cur=array[index];
        while(cur!=null){
            if(cur.key.equals(key)){
                return cur.val;
            }
            cur=cur.next;
        }
        return null;
    }

常见问题:(看源码)

1.如果new HashMap(19), bucket数组多大?
      >=19 &&最接近19的一个2次幂--32
2. HashMap什么时候开辟bucket数组占用内存?
      第一次put的时候--16
3. hashMap何时扩容?
      超过负载因子的时候扩容,且是2倍扩容的
4.当两个对象的hashcode相同会发生什么?
      冲突
5.如果两个键的hashcode相同,你如何获取值对象?
      遍历与hashCode值相等时相连的链表,直到相等(equals)或者null(没有找到)
6.你了解重新调整HashMap大小存在什么问题吗?
      重新哈希原来的元素到新的链表当中

小结

以上就是今天的内容了,有什么问题都可以在评论区留言哦✌✌✌

Map和Set_第15张图片

你可能感兴趣的:(数据结构)