Map & Set

Set & Map

二叉搜索树

二叉搜索树是左子树的值小于根的值,右子树的值大于根的值

其中序遍历一定是从小到大的

插入和插入操作
public class BinarySearchTree {
    //写一个内部类节点
    static class TreeNode {
        public int val;
        public TreeNode left;
        public TreeNode right;

        public TreeNode(int val) {
            this.val = val;
        }
    }

    public TreeNode root = null;

    //查找操作
    public TreeNode find(int val) {
        TreeNode cur = root;
        while(cur != null) {
            if(cur.val < val) {
                cur = cur.right;
            }else if(cur.val > val) {
                cur = cur.left;
            }else {
                return cur;
            }
        }
        return null;
    }

    //插入操作
    public void insert(int val) {
        if(root == null) {
            root = new TreeNode(val);
            return;
        }
        TreeNode cur = root;
        TreeNode curParent = null;
        while(cur != null) {
            if(cur.val < val) {
                curParent = cur;
                cur = cur.right;
            }else if(cur.val > val){
                curParent = cur;
                cur = cur.left;
            }else {
                return;
            }
        }
        TreeNode treeNode = new TreeNode(val);
        if(curParent.val < val) {
            curParent.right = treeNode;
        }else {
            curParent.left = treeNode;
        }
    }
}
删除操作(难点)

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

  1. cur.left == null

    1.cur 是 root , cur = cur.right;

    2.cur 不是 root , cur 是 parent.left , parent.left = cur.right;

    3.cur 不是 root , cur 是 parent.right , parent.right = cur.right;

  2. cur.right == null

    1.cur 是 root , cur = cur.left;

    2.cur 不是 root , cur 是 parent.left , parent.left = cur.left;

    3.cur 不是 root , cur 是 parent.right , parent.right = cur.left;

  3. cur.left != null && cur.right != null

    需要用到替换法进行删除,即先找到cur右子树中的最小值,然后与cur的值替换,然后删除这个最小值,即targetP.left = target.right 注意,这个最小值如果是cur的右子树的第一个节点的话,那么就是targetP.right = target.right;

我的理解:想要删除一个节点,那么这个节点就会有上面的3种大的特点,要么左子树是null,要么右子树是null,要么都不为空,然后按照这三种大的特点来分类下面小的特点

//删除操作
    public void remove(int val) {
        TreeNode cur = root;
        TreeNode parent = null;
        while(cur != null) {
            if(cur.val == val) {
                removeNode(parent, cur);
                return;
            }else if(cur.val < val) {
                parent = cur;
                cur = cur.right;
            }else {
                parent = cur;
                cur = cur.left;
            }
        }
    }

    public void removeNode(TreeNode parent, TreeNode cur) {
        if(cur.left == null) {
            if(cur == root) {
                root = cur.right;
            }else {
                if(parent.left == cur) {
                    parent.left = cur.right;
                }else if(parent.right == cur) {
                    parent.right = cur.right;
                }
            }
        }else if(cur.right == null) {
            if(cur == root) {
                cur = cur.left;
            }else {
                if(parent.left == cur) {
                    parent.left = cur.left;
                }else if(parent.right == cur) {
                    parent.right = cur.left;
                }
            }
        }else {
            TreeNode target = cur.right;
            TreeNode targetP = cur;
            while(target.left != null) {
                targetP = target;
                target = target.left;
            }
            cur.val = target.val;
            if(targetP.left == target) {
                targetP.left = target.right;
            }else if(targetP.right == target) {
                targetP.right = target.right;
            }
        }
    }

性能分析:插入和删除都必须经过查找,那么查找一颗二叉搜索树,最优的情况下就是树的高度,即logN, 但是如果这棵树是单分支的树,那么其平均比较次数就是N/2,所以二叉搜索树的时间复杂度是O(N)

Map和Set模型

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

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

  1. 纯K模型:Set中只存储了Key
  2. Key-Value模型:Map中存储的就是Key-Value的键值对,K一定是唯一的
  3. Set继承了迭代器Iterable和集合类Collection
  4. Map没有继承迭代器,所以要用迭代器需要把Map转化为可以用迭代器的集合,如TreeSet
  5. Set和Map的底层实现了TreeSet,HashSet,TreeMap,HashMap,即红黑树和哈希桶
Map
  • Map的常用方法
    在这里插入图片描述
public static void main(String[] args) {
        Map<String,Integer> treeMap = new TreeMap<>();
        treeMap.put("hello", 3);
        treeMap.put("happy", 6);
        treeMap.put("the", 8);
        treeMap.put(null, 2);//put中的Key必须要是可比较的
        System.out.println(treeMap);//重写了toString方法,输出{happy=6, hello=3, the=8}

        Integer val1 = treeMap.get("hello");
        //返回hello的value -> 3
        Integer val2 = treeMap.getOrDefault("hello2",100);
        //查找hello2,如果存在,返回value,如果不存在,返回100

        Set<String> keySet = treeMap.keySet();//这里是把treeMap中所有的Key都放入到Set中,
                                              //Set相当于一个麻袋
        System.out.println(keySet);//[happy, hello, the]

        Set<Map.Entry<String,Integer>> set = treeMap.entrySet();
        for(Map.Entry<String,Integer> entry : set) {
            System.out.println("key = " + entry.getKey() + 
                               ", value = " + entry.getValue());
        }
    }

public static void main2(String[] args) {
        Map<Student,Integer> treeMap = new TreeMap<>();
        //Key一定是可以比较的,这里的student需要传比较器
        treeMap.put(new Student(),1);
        treeMap.put(new Student(),4);
        System.out.println(treeMap);
    }

上面所说的Map.Entry<String, Integer> 是Map内部实现的用来存放<key, value>键值对映射关系的内部类
在这里插入图片描述
Entry<K,V>是个域是K和V的节点,Entry是Map中的内部接口 ,由于Map接口没有实现getKey和getVal等方法,所以引入Entry接口来实现这些方法

由于Key的值是唯一的,所以并没有setKey方法

  • 注意
  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删除掉,然后再来进行重新插入
    在这里插入图片描述
  • HashMap部分源码
    在这里插入图片描述
    有个table数组
    在这里插入图片描述
    在这里插入图片描述
    当我们调用一个不给初始容量值的HashMap函数时,并没有对table数组初始化,数组大小是0

但是给一个初始值的就一定会把tabel数组初始化为对应大小的数组吗?
在这里插入图片描述
看一下tableSizeFor方法里面对initialCapacity干了什么
在这里插入图片描述
最后面会把初始值转化为大于初始值并且离2^n最近的值,就是你给个10,会返回16

再看一下put方法中
在这里插入图片描述在这里插入图片描述
key.hashCode方法如果重写了hashCode方法就调用自己的,如果没有重写就调用Object的,至于为什么要右移16位:让结果更均匀的分布
在这里插入图片描述

在这里插入图片描述
(n-1)&hash就是n%len,位运算肯定会更快,所以一定要保证n是2的次幂
转换为2的n次幂的数这里体现了计算的优势

Set

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

  • 常见方法
    在这里插入图片描述
public static void main(String[] args) {
        Set<String> treeSet = new TreeSet<>();
        treeSet.add("hello");
        treeSet.add("happy");
        treeSet.add("the");
        System.out.println(treeSet);//[happy, hello, the]
    }
    
//迭代器的使用
Iterator<String> it = s.iterator();
while(it.hasNext()){
	System.out.print(it.next() + " ")}

在这里插入图片描述

TreeSet的底层是TreeMap,利用了TreeMap<K,V>中K不可重复的特点

  • 注意
  1. Set是继承自Collection的一个接口类
  2. Set中只存储了key,并且要求key一定要唯一
  3. TreeSet的底层是使用Map来实现的,其使用key与Object的一个默认对象作为键值对插入到Map中的
  4. Set最大的功能就是对集合中的元素进行去重
  5. 实现Set接口的常用类有TreeSet和HashSet,还有一个LinkedHashSet,LinkedHashSet是在HashSet的基础上维护了一个双向链表来记录元素的插入次序,Map中也有。
  6. Set中的Key不能修改,如果要修改,先将原来的删除掉,然后再重新插入
  7. TreeSet中不能插入null的key,HashSet可以。
哈希表

常见哈希函数

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

散列表的载荷因子定义为:a = 填入表中的元素个数(不可调整) / 散列表的长度(可调整)

  • 冲突-解决

闭散列和开散列

闭散列:1.线性探测 :hash(key) = key % capacity

​ 2.二次探测:Hi = (H0 + i ^2) % m

开散列:链地址法(数组+链表+红黑树),hashMap就是用的这种方法

  • 冲突严重时的解决办法

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

  1. 每个桶的背后是另一个哈希表
  2. 每个桶的背后是一棵搜索树
  • 链地址法实现

当链表长度 >= 8 && 数组长度 >= 64 此时会变成红黑树

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;

    //其实这个方法可以改为直接在创建array时就直接new
    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;
            }
            cur = cur.next;
        }
        //头插
        Node node = new Node(key,val);
        node.next = array[index];
        array[index] = node;
        useSize++;
        if(calculateLoadFactor() >= LOAD_FACTOR) {
            resize();
        }
    }

    private void resize() {
        //因为这里扩容之后hash位置可能会发生变化,所以需要对所有的值重新哈希,故不能用库方法来扩容
        Node[] newArray = new Node[array.length * 2];
        for(int i = 0; i < array.length; i++) {
            Node cur = array[i];
            while(cur != null) {
                Node curNext = cur.next;
                int index = cur.key % newArray.length;
                cur.next = newArray[index];
                newArray[index] = cur;
                cur = curNext;
            }
        }
        array = newArray;
    }

    private double calculateLoadFactor() {
        return useSize * 1.0/ 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;
    }
}
  • 泛型方法实现
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 static final double LOAD_FACTOR = 0.75;
    public Node<K,V>[] array = (Node<K,V>[])new Node[10];
    public int useSize;

    public void put(K key, V val) {
        int hash = key.hashCode();//key是student需要转化为哈希值
        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;
        useSize++;
        if(calculateLoadFactor() >= LOAD_FACTOR) {
            resize();
        }
    }

    private void resize() {
        Node<K,V>[] newArray = (Node<K,V>[])new Node[array.length * 2];
        for(int i = 0; i < array.length; i++) {
            Node<K,V> cur = array[i];
            while(cur != null) {
                Node<K,V> curNext = cur.next;
                int hash = cur.key.hashCode();
                int index = hash % newArray.length;
                cur.next = newArray[index];
                newArray[index] = cur;
                cur = curNext;
            }
        }
        array = newArray;
    }

    private double calculateLoadFactor() {
        return useSize * 1.0/ array.length;
    }

    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)) {
                return  cur.val;
            }
            cur = cur.next;
        }
        return null;
    }
}

public static void main(String[] args) {
        Student student1 = new Student("1234");
        System.out.println(student1.hashCode());
        //重写了hashCode方法后,哈希值就一样
        Student student2 = new Student("1234");
        System.out.println(student2.hashCode());
        HashBuck2<Student,String> hashBuck2 = new HashBuck2<>();
        hashBuck2.put(student1,"zhangsan");
        String val = hashBuck2.get(student2);
        System.out.println(val);//zhangsan
    }

Student自己是没有hashcode的,它是继承了Objiect的

  • 面试问题:

hashcode一样,equals一定一样吗?不一定,hashcode一样,只能证明位置一样,但是val不一定一样

equals一样,hashcode一定一样吗?一定

  1. HashMap 和 HashSet 即 java 中利用哈希表实现的 Map 和 Set
  2. java 中使用的是哈希桶方式解决冲突的
  3. java 会在冲突链表长度大于一定阈值后,将链表转变为搜索树(红黑树)
  4. java 中计算哈希值实际上是调用的类的 hashCode 方法,进行 key 的相等性比较是调用 key 的 equals 方法。所以如果要用自定义类作为 HashMap 的 key 或者 HashSet 的值,必须覆写 hashCode 和 equals 方法,而且要做到 equals 相等的对象,hashCode 一定是一致的。
  5. 哈希表的插入/删除/查找时间复杂度是O(1)
  • 相关OJ练习

136. 只出现一次的数字 - 力扣(LeetCode)

138. 复制带随机指针的链表 - 力扣(LeetCode)

771. 宝石与石头 - 力扣(LeetCode)

旧键盘 (20)__牛客网 (nowcoder.com)

692. 前K个高频单词 - 力扣(LeetCode)

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值