Map和Set(上)

1.建堆的向上调整算法

topk问题

方法一:1.把所有的数据放到大根堆里,时间复杂度:O(NlogN)。2.弹出k个元素,就是最大的k个元素。时间复杂度:O(KlogN)

建大根堆的过程中,是每插入一个数据就调整一次,调成大根堆。时间复杂度:O(NlogN)

方法二:1.建立大小为k的小根堆,时间复杂度:O(KlogK)。2.然后把数组剩下的n-k个元素每个都和堆顶元素进行比较,如果小于堆顶元素,就弹出堆顶元素,然后插入,并保证小堆的性质,时间复杂度:O((N-K)*logK)

建立大小为k的小根堆,时间复杂度:O(KlogK)

下面解释一下向上调整建堆时间复杂度的计算

image-20221019093550788

2.搜索树

image-20221019094741111

TreeMap和TreeSet背后是一棵搜索树,这棵搜索树是红黑树。

2.1概念

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

image-20221019095310930

2.2操作-查找

class BinarySearchTree{
    
    
    static class TreeNode{
    
    
        public int val;
        public TreeNode left;
        public TreeNode right;

        public TreeNode(int val) {
    
    
            this.val = val;
        }
    }
    //查找一个val 是不是在当前的搜索树当中
    //有就返回,没有返回null
    public TreeNode root=null;
    public TreeNode search(int val){
    
    
        TreeNode cur=root;
        while(cur!=null){
    
    
            if(val<cur.val){
    
    
                cur=cur.left;
            }else if(val>cur.val){
    
    
                cur=cur.right;
            }else{
    
    
                return cur;
            }
        }
        return null;
    }
}

image-20221019140900315

最优情况下,二叉搜索树为完全二叉树,其平均比较次数为:log2N
最差情况下,二叉搜索树退化为单支树,其平均比较次数为:N/2

注意:二叉搜索树中没有重复的数据。

2.3操作-插入

  1. 如果树为空树,即根 == null,直接插入
  2. 如果树不是空树,按照查找逻辑确定插入位置,插入新结点,插入的都是叶子节点的位置(这种情况需要一个有一个前指针记录cur前一个位置)
//插入元素key
    public Boolean insert(int key){
    
    
        if(root==null){
    
    
            root=new TreeNode(key);
            return true;
        }
        TreeNode cur=root;
        TreeNode parent=null;
        while(cur!=null){
    
    
            if(key>cur.val){
    
    
                parent=cur;
                cur=cur.right;
            }else if(key<cur.val){
    
    
                parent=cur;
                cur=cur.left;
            }else{
    
    
                return false;//相同的Key不能进行插入
            }
        }
        //cur走到null了
        TreeNode node=new TreeNode(key);
        if(key>parent.val){
    
    
            parent.right=node;
        }else{
    
    
            parent.left=node;
        }
        return true;
    }

2.4操作-删除(难点)

设待删除结点为 cur, 待删除结点的双亲结点为 parent

image-20221019150942917

image-20221019151811178

image-20221019155457911

public void remove(int key){
    
    
        if(root==null){
    
    
            return;
        }
        TreeNode cur=root;
        TreeNode parent=null;
        while(cur!=null){
    
    
            if(cur.val==key){
    
    
                removeNode(parent,cur);
            }else if(cur.val<key){
    
    
                parent=cur;
                cur=cur.right;
            }else{
    
    
                parent=cur;
                cur=cur.left;
            }
        }
    }
    private void removeNode(TreeNode parent,TreeNode cur){
    
    
        if(cur.left==null){
    
    
            if(cur==root){
    
    
                root=cur.right;
            }else if(cur==parent.left){
    
    
                parent.left=cur.right;
            }else{
    
    
                parent.right=cur.right;
            }
        }else if(cur.right==null){
    
    
            if(cur==root){
    
    
                root=cur.left;
            }else if(cur==parent.left){
    
    
                parent.left=cur.left;
            }else{
    
    
                parent.right=cur.left;
            }
        }else{
    
    
            TreeNode targetParent=cur;
            TreeNode target=cur.right;
            while(target.left!=null){
    
    
                targetParent=target;
                target=target.left;
            }
            cur.val=target.val;
            if(target==targetParent.left){
    
    
                targetParent.left=target.right;
            }else{
    
    
                targetParent.right=target.right;
            }
        }
    }

3. Map和Set

3.1 Map的说明

Map和set是一种专门用来进行搜索的容器或者数据结构,其搜索的效率与其具体的实例化子类有关。

一般把搜索的数据称为关键字(Key),和关键字对应的称为值(Value),将其称之为Key-value的键值对,所以
模型会有两种:

  1. 纯 key 模型,比如:
    有一个英文词典,快速查找一个单词是否在词典中
    快速查找某个名字在不在通讯录中
  2. Key-Value 模型,比如:
    统计文件中每个单词出现的次数,统计结果是每个单词都有与其对应的次数:<单词,单词出现的次数>

而Map中存储的就是key-value的键值对,Set中只存储了Key。

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

image-20221019161742622

image-20221019161620936

Map的常用方法说明

方法 解释
V get(Object key) 返回 key 对应的 value
V getOrDefault(Object key, V defaultValue) 返回 key 对应的 value,key 不存在,返回默认值
V put(K key, V value) 设置 key 对应的 value
V remove(Object key) 删除 key 对应的映射关系
Set keySet() 返回所有 key 的不重复集合
Collection values() 返回所有 value 的可重复集合
Set<Map.Entry<K, V>> entrySet() 返回所有的 key-value 映射关系
boolean containsKey(Object key) 判断是否包含 key
boolean containsValue(Object value) 判断是否包含 value

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

image-20221019164520164

Map.Entry<K, V> 是Map内部实现的用来存放<key, value>键值对映射关系的内部类,该内部类中主要提供了
<key, value>的获取,value的设置以及Key的比较方式。

方法 解释
V setValue(V value) 将键值对中的value替换为指定value

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

问题:既然Map有获取key和val的方法,那么为什么要把<key,val>封装到Set<>当中?

答:因为如果使用foreach遍历集合,必须实现Iterable接口,而Map没有实现这个接口,而Set实现了。

image-20221019165545119

3.1.1 map遍历的4种方式

1、使用for循环遍历map;

2、使用迭代器遍历map;

3、使用keySet迭代遍历map;

4、使用entrySet遍历map。

Map<String,String> map=new HashMap<String,String>();  
    map.put("username", "qq");  
    map.put("passWord", "123");  
    map.put("userID", "1");  
    map.put("email", "[email protected]");

方法一、for循环
for(Map.Entry<String, String> entry:map.entrySet()){
    
      
        System.out.println(entry.getKey()+"--->"+entry.getValue());  
    }

方法二、迭代器
        System.out.println("通过iterator遍历所有的value,但是不能遍历key");
        Iterator<Map.Entry<String, String>> iterator = map.entrySet().iterator();
        while (iterator.hasNext()){
    
    
            Map.Entry<String, String> next = iterator.next();
            System.out.println("key="+next.getKey()+"value="+next.getValue());
        }

方法三、keySet()迭代
System.out.println("通过map.keyset进行遍历key和value");
        for (String key:map.keySet()){
    
    
            System.out.println("key=  "+key+"   and value=  "+map.get(key));
        }

方法四、entrySet()迭代
        System.out.println("通过Map.entrySet;")
        Set<Map.Entry<String, String>> entries = map.entrySet();
        for (Map.Entry<String, String>entry:entries){
    
    
            String value = entry.getValue();
            String key = entry.getKey();
            System.out.println("key="+key+"value="+value);
        }

Map中存放键值对的Key是唯一的,value是可以重复的。再存一个相同的key,val覆盖以前的val值

image-20221019170516973

3.2 Set的说明

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

image-20221019190701629

TreeSet实现了SortedSet接口,所以它里面存放的元素一定是可比较的。

Set最大的功能就是对集合中的元素进行去重

Set是继承自Collection的一个接口类。

Set中只存储了key,并且要求key一定要唯一。

Set的底层是使用Map来实现的,其使用key与Object的一个默认对象作为键值对插入到Map中的。

image-20221019193314689

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

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

Set中不能插入null的key。

Set常用方法说明:

方法 解释
boolean add(E e) 添加元素,但重复元素不会被添加成功
void clear() 清空集合
boolean contains(Object o) 判断 o 是否在集合中
Iterator iterator() 返回迭代器
boolean remove(Object o) 删除集合中的 o
int size() 返回set中元素的个数
boolean isEmpty() 检测set是否为空,空返回true,否则返回false
Object[] toArray() 将set中的元素转换为数组返回
boolean containsAll(Collection<?> c) 集合c中的元素是否在set中全部存在,是返回true,否则返回false
boolean addAll(Collection<? extends E> c) 将集合c中的元素添加到set中,可以达到去重的效果

4.哈希表

4.1概念

顺序结构以及平衡树中,元素关键码与其存储位置之间没有对应的关系,因此在查找一个元素时,必须要经过关键
码的多次比较。顺序查找时间复杂度为O(N),平衡树中为树的高度,即O(logN),搜索的效率取决于搜索过程中
元素的比较次数。
理想的搜索方法:可以不经过任何比较,一次直接从表中得到要搜索的元素。 如果构造一种存储结构,通过某种函数(hashFunc)使元素的存储位置与它的关键码之间能够建立一一映射的关系,那么在查找时通过该函数可以很快找到该元素。

当向该结构中:
插入元素
根据待插入元素的关键码,以此函数计算出该元素的存储位置并按此位置进行存放。
搜索元素
对元素的关键码进行同样的计算,把求得的函数值当做元素的存储位置,在结构中按此位置取元素比较,若
关键码相等,则搜索成功,该方式即为哈希(散列)方法,哈希方法中使用的转换函数称为哈希(散列)函数,构造出来的结构称为哈希表(HashTable)(或者称散列表)

例如:数据集合{1,7,6,4,5,9};
哈希函数设置为:hash(key) = key % capacity; capacity为存储元素底层空间总的大小。

image-20221019195048460

用该方法进行搜索不必进行多次关键码的比较,因此搜索的速度比较快 问题:按照上述哈希方式,向集合中插入元素44,会出现什么问题?-》哈希冲突

4.2冲突

不同关键字通过相同哈希函数计算出相同的哈希地址,该种现象称为哈希冲突哈希碰撞

4.3冲突-避免

由于我们哈希表底层数组的容量往往是小于实际要存储的关键字的数量的,这就导致一个问题,冲突的发生是必然的,但我们能做的应该是尽量的降低冲突率

引起哈希冲突的一个原因可能是:哈希函数设计不够合理。 哈希函数设计原则:
哈希函数的定义域必须包括需要存储的全部关键码,而如果散列表允许有m个地址时,其值域必须在0到m-1
之间。哈希函数计算出来的地址能均匀分布在整个空间中。哈希函数应该比较简单。

常见哈希函数

  1. 直接定制法–(常用)
    取关键字的某个线性函数为散列地址:Hash(Key)= A*Key + B 优点:简单、均匀 缺点:需要事先知道关键字的分布情况 使用场景:适合查找比较小且连续的情况
  2. 除留余数法–(常用)
    设散列表中允许的地址数为m,取一个不大于m,但最接近或者等于m的质数p作为除数,按照哈希函数:Hash(key) = key% p(p<=m),将关键码转换成哈希地址

4.4 冲突-避免-负载因子调节

image-20221019202243240

已知哈希表中已有的关键字个数是不可变的,那我们能调整的就只有哈希表中的数组的大小。

4.5 冲突-解决

4.5.1 冲突-解决-闭散列

闭散列:也叫开放定址法,当发生哈希冲突时,如果哈希表未被装满,说明在哈希表中必然还有空位置,那么可以
把key存放到冲突位置中的“下一个” 空位置中去。那如何寻找下一个空位置呢?

  1. 线性探测
    比如上面的场景,现在需要插入元素44,先通过哈希函数计算哈希地址,下标为4,因此44理论上应该插在该
    位置,但是该位置已经放了值为4的元素,即发生哈希冲突。
    线性探测:从发生冲突的位置开始,依次向后探测,直到寻找到下一个空位置为止。
    插入
    通过哈希函数获取待插入元素在哈希表中的位置。如果该位置中没有元素则直接插入新元素,如果该位置中有元素发生哈希冲突,使用线性探测找到下一个空位置,插入新元素。

采用闭散列处理哈希冲突时,不能随便物理删除哈希表中已有的元素,若直接删除元素会影响其他
元素的搜索。比如删除元素4,如果直接删除掉,44查找起来可能会受影响。因此线性探测采用标
记的伪删除法来删除一个元素。

image-20221019202847735

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

因此:比散列最大的缺陷就是空间利用率比较低,这也是哈希的缺陷

4.5.2冲突-解决-开散列/哈希桶

开散列法又叫链地址法(开链法),首先对关键码集合用散列函数计算散列地址,具有相同地址的关键码归于同一子
集合,每一个子集合称为一个桶,各个桶中的元素通过一个单链表链接起来,各链表的头结点存储在哈希表中。

image-20221019203308669

从上图可以看出,开散列中每个桶中放的都是发生哈希冲突的元素。
开散列,可以认为是把一个在大集合中的搜索问题转化为在小集合中做搜索了。

这样的数组+链表的组织,当数组长度超过64且链表长度超过8时链表会变成红黑树。

JDK1.7及以前采用的是头插法,JDK1.8开始采用的是尾插法。

哈希桶其实可以看作将大集合的搜索问题转化为小集合的搜索问题了,那如果冲突严重,就意味着小集合的搜索性能其实也时不佳的,这个时候我们就可以将这个所谓的小集合搜索问题继续进行转化,例如:

  1. 每个桶的背后是另一个哈希表
  2. 每个桶的背后是一棵搜索树

4.6性能分析

虽然哈希表一直在和冲突做斗争,但在实际使用过程中,我们认为哈希表的冲突率是不高的,冲突个数是可控的,
也就是每个桶中的链表的长度是一个常数,所以,通常意义下,我们认为哈希表的插入/删除/查找时间复杂度是
O(1) 。

HashMap的元素保存顺序和元素的插入顺序无关,HashMap没有实现SortedMap接口,所以插入key不涉及key的比较。

image-20221019205535489

key不能重复,如果有重复的,就会覆盖原来的val。key和val都可以为null。

5.练习题

//1.统计10w个数据当中 不重复的数据
TreeSet<Integer> set=new TreeSet<>();
        for (int i = 0; i < array.length; i++) {
    
    
            set.add(array[i]);
        }
        System.out.println(set);
public static void main5(String[] args) {
    
    
        int[] array=new int[10];
        Random random=new Random();
        for (int i = 0; i < 10; i++) {
    
    
            array[i]=random.nextInt(5);
        }
        System.out.println(Arrays.toString(array));
        func1(array);
    }

//2.统计10w个数据当中,第一个重复的数据?
public static void func2(int[] array){
    
    
        HashSet<Integer> set=new HashSet<>();
        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个数据中,每个数据出现的次数?
public static void func3(int[] array){
    
    
        HashMap<Integer,Integer> map=new HashMap<>();
        for (int i = 0; i < array.length; i++) {
    
    
            int key=array[i];
            if(map.get(key)==null){
    
    
                map.put(key,1);
            }else{
    
    
                int val=map.get(key);
                map.put(key,val+1);
            }
        }
        for (Map.Entry<Integer,Integer> entry:
             map.entrySet()) {
    
    
            System.out.println("key: "+entry.getKey()+" val: "+entry.getValue()+"次!");
        }
        System.out.println("abcdef");
    }

image-20221019214930315

class Solution {
    
    
    public int singleNumber(int[] nums) {
    
    
        HashSet<Integer> set=new HashSet<>();
        for(int i=0;i<nums.length;i++){
    
    
            if(set.contains(nums[i])){
    
    
                set.remove(nums[i]);
            }else{
    
    
                set.add(nums[i]);
            }
        }
        for(int i=0;i<nums.length;i++){
    
    
            if(set.contains(nums[i])){
    
    
                return nums[i];
            }
        }
        return -1;
    }
}

image-20221019215106266

class Solution {
    
    
    public Node copyRandomList(Node head) {
    
    
        Node cur=head;
        HashMap<Node,Node> map=new HashMap<>();
        //第一次遍历链表
        while(cur!=null){
    
    
            Node newnode=new Node(cur.val);
            map.put(cur,newnode);
            cur=cur.next;
        }
        cur=head;
        //第二次遍历链表
        while(cur!=null){
    
    
            map.get(cur).next=map.get(cur.next);
            map.get(cur).random=map.get(cur.random);
            cur=cur.next;
        }
        return map.get(head);
    }
}
//不能用TreeMap,这样要求node必须是可比较的

image-20221019221802752

猜你喜欢

转载自blog.csdn.net/qq_63983125/article/details/127421975