Set和Map及哈希表介绍

    • 搜索方式介绍
    • TreeMap
      • Map使用
    • TreeSet
      • Set使用
    • Set和Map常用方法练习(后面补充)
    • 练习之Set/Map
    • oj练习(后面补充)
    • 哈希表
      • 哈希冲突
        • 避免冲突-哈希函数设计
        • 避免冲突-负载因子调节
        • 避免冲突-闭散列
        • 避免冲突-开散列
    • 模拟实现哈希表
    • 哈希Map源码分析

搜索方式介绍

哈希集合(Hash Set)是一种数据结构,集合(Set)的一种实现方式。哈希集合使用哈希表(Hash Table)来实现这一特性。Set和Map底下有四种实现:Map和Set适合动态查找的集合容器;TreeSet和TreeMap背后都是一棵搜索树(红黑树)

原始的搜索方式:效率低;不适对区间经常进行插入和删除操作的对象查找
1.直接遍历,时间复杂度为O(N),元素如果比较多效率会非常慢
2.二分查找,时间复杂度为,但搜索前必须要求序列是有序的
Set和Map及哈希表介绍_第1张图片
为什么Set和Map适合动态查找呢?这就要取决于它们的模型(Key-value的键值对)
1:纯key模型;Set只存key
比如:我要查通讯录的某个名字在不在

2:key-Value模型;Map存key-Value键值对
比如:统计单词出现的个数;key:value

TreeMap

TreeMap底层是一个搜索树(红黑树);存放的键一定是按照有序的顺序存储的,因为二叉搜索树也是有序的。
TreeMap和TreeSet的key都是要可比较的;堆的也是需要可比较的;但是堆的第一次offer是不进行比较的;所以它第一次放进去不报错;但是这里的TreeMap和TreeSet如果是不能比较的就必报错。

Map使用

Map是一个接口类,该类没有继承自Collection,该类中存储的是结构的键值:。Map没有实现Iterable所以Map不能使用迭代器去遍历;我们后面遍历得使用特殊手段
Set和Map及哈希表介绍_第2张图片
1:Map是一个接口,不能直接实例化对象,如果要实例化对象只能实例化其实现类TreeMap或者HashMap
2:Map中存放键值对的Key是唯一的,value是可以重复的
3:Map中的Key可以全部分离出来,存储到Set中来进行访问(因为Key不能重复)。
4:Map中的value可以全部分离出来,存储在Collection的任何一个子集合中(value可能有重复)。
5:Map中键值对的Key不能直接修改,value可以修改,如果要修改key,只能先将该key删除掉,然后再来进行重新插入。

Map.Entry是什么?
Map.Entry是Map内部实现的用来存放键值对映射关系的内部类,该内部类中主要提供了的获取,value的设置以及Key的比较方式。(注意:这里并没有提供设置Key的方法)
Set和Map及哈希表介绍_第3张图片

TreeSet

底层也是一棵搜索树,存储数据的特点为有序的,不可以重复的,所有存放的元素都是可以比较,不可重复的。
TreeSet底层使用TreeMap的实现(HashSet底层使用HashMap)。TreeSet和TreeMap存元素时的key一定得可比较;不然会出现ClassCastException的异常
Set和Map及哈希表介绍_第4张图片
Map的遍历
1:
把Key放入set里;然后遍历获取全部value

public static void main(String[] args) {
        Map<String,Integer> map = new HashMap<>();
        map.put("aaa",1);
        map.put("bbb",2);
        map.put("ccc",3);
        Set<String> set = map.keySet();
        for (String s : set) {
            System.out.println(s+" = "+map.get(s));
        }
    }

2:
使用Enrty然后获取里面的Key

public static void main(String[] args) {
        Map<String,Integer> map = new HashMap<>();
        map.put("aaa",1);
        map.put("bbb",2);
        map.put("ccc",3);
        Set<Map.Entry<String, Integer>> entries = map.entrySet();
        for (Map.Entry<String, Integer> entry : entries) {
            System.out.println(entry.getKey()+" = "+entry.getValue());
        }
    }

Set使用

Set与Map主要的不同有两点:Set是继承自Collection的接口类,Set中只存储了Key;重复的key后面会覆盖前面
Set和Map及哈希表介绍_第5张图片

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

Set的三种遍历方法:
1:迭代器

public static void main(String[] args) {
        Set<Integer> set = new HashSet<>();
        set.add(1);
        set.add(2);
        set.add(3);
        Iterator<Integer> it = set.iterator();
        while(it.hasNext()) {
            System.out.print(it.next()+" ");
        }
    }

2:for each循环

public static void main(String[] args) {
        Set<Integer> set = new HashSet<>();
        set.add(1);
        set.add(2);
        set.add(3);
        for (Integer integer : set) {
            System.out.print(integer+" ");
        }
    }

Set和Map常用方法练习(后面补充)

总结:
Set和Map及哈希表介绍_第6张图片

练习之Set/Map

10万个为什么:如何生成10W个随机数

1:统计10W个数据中;不重复的数据 [去重]
Treeset和HashSet都能做到

    //十万个为什么之Set、Map运用。生成范围1-50000
    public static void main(String[] args) {
        int []array=new int[10_0000];
        Random random=new Random();
        for (int i = 0; i <10_0000 ; i++) {
            array[i]=random.nextInt(5_0000);
        }
        func1(array);

    }
    //先生成10w个为什么
    public static void func1(int []array){
        HashSet<Integer> set=new HashSet<>();
//        Set set=new TreeSet<>();
        set.add(9);
        set.add(1);
        for (int i = 0; i < array.length; i++) {
            set.add(array[i]);

        }
        System.out.println(set);
    }

注意:Integer类型;没超过这int类型表示的最大数字范围;它的哈希值计算还是它本身。所以我们打印的结果就看似有序的。其实并非真正意义上的有序。

2:统计10W个数据中,第一个重复的数据库

    //统计10W个数据中,第一个重复的数据库
    public static void first(int []array){
        HashSet<Integer> set=new HashSet<>();
//        Set set=new TreeSet<>();
        for (int i = 0; i < array.length; i++) {
            if(!set.contains(array[i])) {
                set.add(array[i]);
            }else {
                System.out.println(array[i]);
                return;

            }

        }

    }

3:统计10W个数据中每个数据出现的次数
最终结果是一个key value。key是值不重复;value是出现的次数。直接存Map里

   //统计10W个数据中每个数据出现的次数
    public static void count(int[] array){
        HashMap<Integer,Integer> map=new HashMap<>();
        for (int i = 0; i <array.length ; i++) {
            int key=array[i];
            //我获取一下看看有没有这个元素;如果没有;我得放进去(key,1)
            if(map.get(key)==null){
                map.put(key,1);
            }else {
             //里面已经有了;我们直接给value加1。那我们得先获取到value
                int val=map.get(key);
                map.put(key,val+1);

            }
            for (Map.Entry<Integer,Integer> entry:map.entrySet() ) {
                System.out.println(entry.getKey()+"出现"+entry.getValue()+"次");

            }
        }


    }

oj练习(后面补充)

Set和Map及哈希表介绍_第7张图片

哈希表

理想的搜索方法;不经过任何比较,一次直接从表中得到要搜索的元素。如果构造一种存储结构,通过某种函数hashFunc。使元素的存储位置与它的关键码之间能够建立一一映射的关系,那么在查找时通过该函数可以很快找到该元素。(数组遍历是O(N);二分查找是O(log N);搜索树是O(log N);而哈希表能达到O(1))
插入元素:根据待插入元素的关键码,以此函数计算出该元素的存储位置并按此位置进行存放
搜索元素:对元素的关键码进行同样的计算,把求得的函数值当做元素的存储位置,在结构中按此位置取元素比较,若关键码相等,则搜索成功

哈希表是一种数据结构:上述方式为哈希(散列)方法,哈希方法中使用的转换函数称为哈希(散列)函数,构造出来的结构称为哈希表(Hash Table)
例如:数据集合{1,7,6,4,5,9};
哈希函数设置为:hash(key) = key % capacity;capacity为存储元素底层空间总的大小。
Set和Map及哈希表介绍_第8张图片

哈希冲突

哈希冲突:不同关键字通过相同哈希函数计算出相同的哈希地址,该种现象称为哈希冲突或哈希碰撞。
这些具有不同关键码而具有相同哈希地址的数据元素称为“同义词”。
例如:上面的例子2和12的位置相同(2%10=2;12%10=2)

避免冲突-哈希函数设计

当我们试图将大量的数据映射到有限数量的哈希桶时;冲突是必然的;冲突没法解决;只能尽量降低冲突率
降低冲突:哈希函数设计要合理设计;需要以下设计原则
1:哈希函数的定义域必须包括需要存储的全部关键码,而如果散列表允许有m个地址时,其值域必须在0到m-1之间
2:哈希函数计算出来的地址能均匀分布在整个空间中
3:哈希函数应该比较简单
常见的哈希函数:
1.直接定制法–(常用)
取关键字的某个线性函数为散列地址:Hash(Key)= A*Key + B优点:简单、均匀
缺点:需要事先知道关键字的分布情况
使用场景:适合查找比较小且连续的情况面试题:字符串中第一个只出现一次字符
2.除留余数法–(常用)
设散列表中允许的地址数为m,取一个不大于m,但最接近或者等于m的质数p作为除数,按照哈希函数:Hash(key) = key% p(p<=m),将关键码转换成哈希地址

避免冲突-负载因子调节

Set和Map及哈希表介绍_第9张图片
Set和Map及哈希表介绍_第10张图片
现在知道负载因子的重要性:如何调节负载因子;存的数越来越多,前面的填入表的个数是无法改变的(你不能说不给人家存)只能改变后面的表的长度

避免冲突-闭散列

闭散列也叫开放定址法的基本思想是:当发生哈希冲突时,如果哈希表未被装满,说明在哈希表中必然还有空位置,那么可以把key存放到冲突位置中的“下一个”空位置中去。那如何寻找下一个空位置呢?
例如:上面举的例子;如果现在需要插入元素44,先通过哈希函数计算哈希地址,下标为4,因此44理论上应该插在该位置,但是该位置已经放了值为4的元素,即发生哈希冲突。我们怎么找下一个空位置?
Set和Map及哈希表介绍_第11张图片
1.线性探测法;
从发生冲突的位置开始,依次向后探测,直到寻找到下一个空位置为止。找到8的位置为空;插入进去。
当我们使用闭散列处理哈希冲突时;不能随便删掉哈希表的已有元素;哪怕不用了也不能随便删除;因为直接删除掉,44查找起来可能会受影响。所以线性探测采用标记的伪删除法来删除一个元素。

2.二次探测法;
线性探测的缺陷是产生冲突的数据堆积在一块,这与其找下一个空位置有关系,因为找空位置的方式就是挨着往后逐个去找,因此二次探测为了避免该问题,找下一个空位置的方法为:Hi=(H0+i^2)%m或者Hi=(H0- i ^2)%m;i为1,2,3……(i是第一次冲突,或者第二次冲突等等)
H0是通过散列函数Hash(x)对元素的关键码key进行计算得到的位置;m是表的大小。对于刚才要插入44,产生冲突,使用解决后的情况为:(4+1^2)%10

表的长度为质数(质数能比较好降低冲突;比如公倍数)且表负载因子a不超过0.5时,新的表项才一定能够插入,而且任何一个位置都不会被探查两次。因此只要表中有一半的空位置,就不会存在表满的问题。在搜索时可以不考虑表装满的情况,但在插入时必须确保表的负载因子a不超过0.5,如果超出必须考虑增容。
闭散列缺点:为了高效;空间利用率比较低,这也是哈希的缺陷。

避免冲突-开散列

开散列法又叫链地址法(开链法),首先对关键码集合用散列函数计算散列地址,具有相同地址的关键码归于同一子集合,每一个子集合称为一个桶,各个桶中的元素通过一个单链表链接起来,各链表的头结点存储在哈希表中。
Set和Map及哈希表介绍_第12张图片
总结:采用开放地址法处理哈希冲突的时候,其平均查找长度应当大于链地址法
冲突的地方;这里是一个链表;全部堆积在这里;jdk1.7之前采用头插法;jdk1.8使用尾插法。在大集合中的搜索问题转化为在小集合中做搜索。
这里使用单链表:优点是它相对简单且占用的额外内存较少。每个节点只需存储键、值和下一个节点的引用即可。然而,单链表的缺点是在查找特定键值对时需要遍历整个链表,这可能会导致查找操作的时间复杂度为 O(n),其中 n 是链表的长度。在极端情况下,如果哈希函数选择不当,链表可能会变得非常长,影响性能。

哈希表的冲突率是不高的,冲突个数是可控的,也就是每个桶中的链表的长度是一个常数,所以,通常意义下,我们认为哈希表的插入/删除/查找时间复杂度是O(1)

模拟实现哈希表

针对int类型

import java.util.Arrays;

//int类型
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 usedSize;

    public HashBuck() {
        array = new Node[8];
    }
//我们使用头插比较简单
    public void put(int key,int val) {
        int index = key % array.length;
        //遍历Index下标的数组,如果有相同的key那么替换
        Node cur = array[index];
        while (cur != null) {
            if(cur.key == key) {
                cur.val = val;
                return;
            }
            cur = cur.next;
        }
        //进行头插法
        Node node = new Node(key, val);
        node.next = array[index];
        array[index] = node;
        usedSize++;
        if( loadFactor() >= 0.75f) {
            //扩容
            //array = Arrays.copyOf(array,2*array.length);
            resize();
        }
    }

    private void resize() {//哈希函数值变了;所以我们不能简简单单的扩容;需要根据哈希函数重新把原来的元素放到相应的位置;然后才能找得到
        Node[] newArray = new Node[2*array.length];
        for (int i = 0; i < array.length; i++) {
            Node cur = array[i];
            while (cur != null) {
                Node curNext = cur.next;
                int newIndex = cur.key % newArray.length;
                //拿着cur节点 进行插入到新的位置
                cur.next = newArray[newIndex];
                newArray[newIndex] = cur;
                cur =  curNext;
            }
        }
        array = newArray;
    }

    private float loadFactor() {
        return usedSize*1.0f / array.length;
    }

    public int get(int key) {
        int index = key % array.length;
        Node head = array[index];
        while (head != null) {
            if(head.key == key) {
                return head.val;
            }
            head = head.next;
        }
        return -1;
    }
}

针对泛型:


//泛型
//泛型需要注意;使用的是哈希code; int index = hash % array.length;。不是我们上一个代码的int类型直接key%array.length;
//
public class HashBuck2<K,V> {

    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 = (Node<K,V>[])new Node[10];
    public int usedSize;

    public void put(K key,V val) {
        int hash = key.hashCode();
        int index = hash % array.length;
        Node<K,V> cur = array[index];
        while (cur != null) {
            if(cur.key.equals(key)) {
                cur.val = val;
                return;
            }
            cur = cur.next;
        }
        Node<K,V> node = new Node<>(key, val);
        node.next = array[index];
        array[index] = node;
        usedSize++;
    }

    public V get(K key) {//先找桶再找值
        int hash = key.hashCode();
        int index = hash % array.length;
        Node<K,V> cur = array[index];
        while (cur != null) {
            if(cur.key.equals(key)) {//如果两个id相同的对象;存的key是不同;但是他们存在同一个桶里;通过equals比较取哪个
                return cur.val;
            }
            cur = cur.next;
        }
        return null;
    }

}

Set和Map及哈希表介绍_第13张图片

假设我要储存person这个对象:我得重写哈希code;这样子id值相同的对象才能存到一个桶里;因为是根据哈希值来计算的。最后通过key比较我们就可以取出想要的。

import java.util.*;

class Person {
    public String id;

    public Person(String id) {
        this.id = id;
    }

    @Override
    public String toString() {
        return "Person{" +
                "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);
    }

    //重写了HashCode方法,根据对象的id属性来计算哈希值
    //如果两个对象的 id 属性值相同,那么它们的哈希码也会相同,这符合哈希表中的要求。
    //这两个对象被视为在哈希表中的相同位置,即它们会被放置在哈希表的同一个桶中。
    @Override
    public int hashCode() {
        return Objects.hash(id);
    }
}

hashcode和equals区别:
hashCode 是一个方法,用于计算对象的哈希码,通常是一个整数。
hashCode 方法的主要作用是提供一种快速定位对象的机制,以加速数据结构的存储和检索操作。
哈希码的计算通常基于对象的内容(属性值),但不是唯一标识对象的内容,因此可能存在哈希冲突(不同对象具有相同哈希码)。

equals 是一个方法,用于比较两个对象的内容(属性值),以确定它们是否在语义上相等。
equals 方法的主要作用是定义对象之间的相等性比较规则,通常用于业务逻辑中的对象比较。

HashCode一定要再哈希表当中才会发挥出它的意义的;一般来说我们重写equals都要重写一下HashCode;但是我们通常在其它情况都是使用不到HashCode;所以前面的学习也就一直没有重写。
在哈希表设计里:
hashcode相同equals一定相同吗
不一定;但哈希码并不完全唯一。不同的对象可以具有相同的哈希码,这种情况称为哈希冲突。而我们使用equals区分这个相同的哈希码是不是同一个对象。

equals相同hashcode一定相同吗
一定;equals一样,代表是两个对象一样,那么它的对应的HashCode的值也一定是相同的

哈希Map源码分析

哈希映射(HashMap)的底层通常使用数组和链表(或红黑树)来实现。在 Java 8 及以后的版本中,如果同一个桶中的键值对数量达到一定阈值(通常是8个),则链表会被转换为红黑树,以提高检索性能。

Set和Map及哈希表介绍_第14张图片
构造方法:
Set和Map及哈希表介绍_第15张图片

Set和Map及哈希表介绍_第16张图片
无参构造:
Set和Map及哈希表介绍_第17张图片
默认容量是0;但是我们去put却能成功。在putVal(树化的代码也在在里面)完成的(大串代码,调用无参构造方法的时候,第一次pu’t才会开辟内存)
Set和Map及哈希表介绍_第18张图片

分配的内存是按2的次幂分配:例如你new的是19;实际分配的是32;往大的分;往小16放不下19个
Set和Map及哈希表介绍_第19张图片

哈希Map如何解决哈希冲突:链地址法通常用于大多数哈希映射实现;可能还会使用其它的方法应对不同的情况

java中的:哈希map和哈希table有点区别;HashMap的key和value可以为null;为null时给你赋值为0。hashtable的key、value不能为null

你可能感兴趣的:(数据结构,哈希算法,散列表,算法)