• 数据结构:Map和Set(1)


    搜索树

    概念

    若它的左子树不为空,则左子树上所有节点的值都小于根节点的值
    若它的右子树不为空,则右子树上所有节点的值都大于根节点的值
    它的左右子树也分别为二叉搜索树

    这棵树的中序遍历结果是有序的

    接下来我们来模拟一棵二叉搜索树,和二叉树一样,定义左右结点,结点值和根结点

    1. public class BinarySearchTree {
    2. static class TreeNode{
    3. public int val;
    4. public TreeNode left;
    5. public TreeNode right;
    6. public TreeNode(int val){
    7. this.val = val;
    8. }
    9. }
    10. public TreeNode root;
    11. }

    查找

    拿目标值key与root进行比较,比root大的往右边搜索,比root小的往左边搜索;接着继续与左/右子树根结点比较,重复上面步骤

    搜索代码

    1. public boolean search(int key){
    2. TreeNode cur = root;
    3. while(cur != null){
    4. if(cur.val < key){
    5. cur = cur.right;
    6. } else if (cur.val>key) {
    7. cur = cur.left;
    8. }else{
    9. return true;
    10. }
    11. }
    12. return false;
    13. }

    时间复杂度:

    最好情况:完全二叉树,那时间复杂度为O(logN)

    最坏情况:单分支二叉树,时间复杂度O(N)

    插入

    对于下方的二叉树,我们需要插入15

    我们可以定义一个cur,负责遍历二叉树;定义一个parent记录子树的根结点位置

    如果当前cur结点的值比目标插入的值小,就把parent定位到当前结点,把cur往右子树移动

    如果cur值较大就向左子树移。cur负责帮助parent定位到目标值附近

    定义一个node承载目标值,如果parent的值小于目标值就把node右插,反之则左插

    1. public boolean search(int key){
    2. TreeNode cur = root;
    3. while(cur != null){
    4. if(cur.val < key){
    5. cur = cur.right;
    6. } else if (cur.val>key) {
    7. cur = cur.left;
    8. }else{
    9. return true;
    10. }
    11. }
    12. return false;
    13. }
    14. public static boolean insert(TreeNode root, int val){
    15. if(root == null){
    16. root = new TreeNode(val);
    17. return true;
    18. }
    19. TreeNode cur = root;
    20. TreeNode parent = null;
    21. while(cur!=null){
    22. if(cur.val < val){
    23. parent = cur;
    24. cur = cur.right;
    25. }else if(cur.val > val){
    26. parent = cur;
    27. cur = cur.left;
    28. }else{
    29. return false;
    30. }
    31. }
    32. TreeNode node = new TreeNode(val);
    33. if(parent.val>val){
    34. parent.left = node;
    35. }else{
    36. parent.right = node;
    37. }
    38. return true;
    39. }

    拿这么一个列表来测试一下

    array = {5,12,3,2,11,15}

    正常画出来的图是这样的

    测试debug后画出来的图一模一样😊

     删除

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

    第一种情况:cur.left == null

    1. cur是root时,让root = cur.right

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

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

    1. if(cur.left == null){
    2. if(cur == root){
    3. root = parent.right;
    4. }else if(cur == parent.left){
    5. parent.right = cur.left;
    6. }else{
    7. parent.right = cur.right;
    8. }
    9. }

    第二种情况:cur.right == null

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

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

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

    1. else if(cur.right == null) {
    2. if(cur == root){
    3. root = parent.left;
    4. }else if(cur == parent.left){
    5. parent.left = cur.left;
    6. }else{
    7. parent.right = cur.left;
    8. }
    9. }

    第三种情况:cur.left != null && cur.right != null

    我们逍遥删除cur位置这个40元素

    首先确定的一点是,40的左子树比40都小,右子树比40都大

    替换法:找一个数据来替换cur

    1.确定cur这里将来要放的数据一定比左边都大,比右边都小

    2.要么在左树里面找到最大的数值(左树最右边的数据);

    要么就在右树里面找到最小的数值来替换(右树最左边的数据)

    3.找到合适的数据之后,直接替换cur的值,然后删除那个合适的数据结点

    1. else{
    2. //找到合适的值(从右子树找最小值)
    3. //target负责找到合适值,targetParent负责记录target的双亲结点
    4. TreeNode targetParent = cur;
    5. TreeNode target = cur;
    6. while(target.left != null){
    7. targetParent = target;
    8. target = target.left;
    9. }
    10. cur.val = target.val;
    11. //删除target,因为已经到最左边了,所以直接让parent的左边 = target的右边
    12. //就算右边为空也没关系
    13. targetParent.left = target.right;
    14. }

    其实这个代码还存在一个问题,如下图,我们找到50为最小值进行替换,替换完删除50的时候执行targetParent.left = target.right; 但是变成20改为55了,这明显不对劲

    也就是说,上面的代码只适合targetParent.left = target的情况

    遇到这种targetParent.right = target的情况,需要让targetParent的右边 = target的右边

    代码修改(只修改删除target的部分)

    1. if(targetParent.left == target){
    2. targetParent.left = target.right;
    3. }else{
    4. targetParent.right = target.right;
    5. }

    不想看分析可以直接看这

    整个的代码:

    1. public void remove(int val){
    2. TreeNode cur = root;
    3. TreeNode parent = null;
    4. while(cur!=null){
    5. if(cur.val < val){
    6. parent = cur;
    7. cur = cur.right;
    8. }else if(cur.val > val){
    9. parent = cur;
    10. cur = cur.left;
    11. }else{
    12. //开始删除
    13. removeNode(cur,parent);
    14. }
    15. }
    16. }
    17. private void removeNode(TreeNode cur, TreeNode parent) {
    18. if(cur.left == null){
    19. if(cur == root){
    20. root = parent.right;
    21. }else if(cur == parent.left){
    22. parent.right = cur.left;
    23. }else{
    24. parent.right = cur.right;
    25. }
    26. }else if(cur.right == null) {
    27. if(cur == root){
    28. root = parent.left;
    29. }else if(cur == parent.left){
    30. parent.left = cur.left;
    31. }else{
    32. parent.right = cur.left;
    33. }
    34. }else{
    35. //找到合适的值(从右子树找最小值)
    36. //target负责找到合适值,targetParent负责记录target的双亲结点
    37. TreeNode targetParent = cur;
    38. TreeNode target = cur;
    39. while(target.left != null){
    40. targetParent = target;
    41. target = target.left;
    42. }
    43. cur.val = target.val;
    44. //删除target
    45. if(targetParent.left == target){
    46. targetParent.left = target.right;
    47. }else{
    48. targetParent.right = target.right;
    49. }
    50. }
    51. }

    Map的使用

    搜索

    我们学过的搜索

    1.直接遍历: --> O(N),速度较慢

    2.二分查找:--> O(logN),但要求搜索的序列有序

    上面的搜索都属于静态搜索

    而现实中我们需要在查找时进行一些插入和删除操作,就需要用到Map和Set这两个适合动态查找的容器了

    Map属于Key-Value 模型 Key-Value 模型比如:
    统计文件中每个单词出现的次数,统计结果是每个单词都有与其对应的次数: < 单词,单词出现的次数 >
    梁山好汉的江湖绰号:每个好汉都有自己的江湖绰号

     Map有两种形式,二叉搜索树和哈希表

    1. Map map = new TreeMap<>();//二叉搜索树, 查找复杂度O(logN)
    2. Map map1 = new HashMap<>();//哈希表, 查找复杂度O(1) 哈希表-->数组+列表+红黑树

    方法

    put设置key和对应的value 

    get通过key来找到对应的值 

    如果找不到就返回null

    getOrDefault方法和get差不多,只不过找不到就默认返回自己设置的返回值

    keySet获取所有的key

    entrySet返回key-value的所有关系

    Set> entrySet = map.entrySet();

    而Set是所有Entry的集合

    注意:

    1.Map是一个接口,不能直接实例化对象,只能实例化其实现类TreeMap或HashMap

    2.Map里的键值对中key是唯一的,但value是可以重复的,以最后一个value为准

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


    Set的使用

    Set属于 key 模型 key 模型比如:
    有一个英文词典,快速查找一个单词是否在词典中
    快速查找某个名字在不在通讯录中

    方法

    iterator方法,输出每个key,每行1个 

    注意:

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

    2.TreeSet的底层是TreeMap

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


    哈希表

    概念

    顺序结构以及平衡树中,元素关键码与其存储位置之间没有对应的关系,因此在查找一个元素时,
    必须要经过关键码的多次比较。搜索的效率取决于搜索过程中元素的比较次数
    理想搜索方法:可以不经过比较,一次直接从表中得到要搜索的元素,通过该元素的存储位置和它的关键码之间建立一一映射的关系

    这种函数叫做哈希函数:hash(key) = key % capacity

    capacity是存储元素底层空间的大小

     哈希冲突:不同的关键字key,通过相同的哈希函数,得到相同的值

    哈希冲突无法解决,只能降低冲突率

    哈希函数的设计

    合理的哈希函数可以降低冲突率

    原则:

    1.哈希函数定义域包括需要存储的全部关键码,如果散列表允许有m个地址时,值域必须在0到m-1之间

    2.哈希函数计算出来的地址能均匀分布在整个空间中

    3.哈希函数比较简单

    常见的哈希函数

    直接订制法:取关键字的某个线性函数为散列地址

    优点:简单、均匀
    缺点:需要事先知道关 键字的分布情况
    使用场景:适合查找比较小且连续的情况
    1. public int firstUniqChar(String s) {
    2. int[] count = new int[26];
    3. for(int i = 0; i < s.length(); i++){
    4. char ch = s.charAt(i);
    5. count[ch-'a']++;
    6. }
    7. for(int i = 0; i< s.length();i++){
    8. char ch = s.charAt(i);
    9. if(count[ch-'a'] == 1){
    10. return i;
    11. }
    12. }
    13. return -1;
    14. }

    除留余数法

    设散列表中允许的 地址数为 m ,取一个不大于 m ,但最接近或者等于 m 的质数 p 作为除数,按照哈希函数: Hash(key) = key% p(p<=m),  将关键码转换成哈希地址

    负载因子调节 

    由图片我们可以知道负载因子越高,冲突率逐渐增加

    填入表里面的元素已经是固定的情况下,为了使负载因子降低,我们只能让散列表扩容


    冲突避免方法

    闭散列

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

    那么怎么找到这个“下一个”位置呢?

    1.线性探测法:

    比如我们要在这个线性表中放入44,14,24,34,54

    44与4冲突了,探测到4下一个的位置时空的,就把44放进去,后面的14,24,34挨个放进去空位置,到了54,后面没有位置可以放了,返回到前面继续探测,找到0位置 

    但是线性探测有个缺点,产生冲突的数据会堆在一块 

    2.二次探测

    或者

    H0时通过散列函数对关键码key计算出来的位置,m是表的大小,i = 0,1,2,3....代表的是要插入的数据排在第几位

     这样明显不会堆在一起,而是均匀散开来了


    开散列/哈希桶

    又叫链地址法(开链法),采用数组+链表(+红黑树,当数组长度>64 && 链表长度 >=8 之后才会把链表变成红黑树)的方法,把冲突的元素采用尾插法插入被冲突元素的后面(JDK 1.7之前采用头插法,JDK 1.8之后采用尾插法)

    数组的每个元素是链表的头节点


    手搓一个哈希桶

    初始化链表

    每个结点需要有三个域,key,value和next

    1. static class Node{
    2. public int key;
    3. public int val;
    4. public Node next;
    5. public Node(int key, int val) {
    6. this.key = key;
    7. this.val = val;
    8. }
    9. }
    10. public Node[] array;
    11. public int usedSize;//记录存放的有效数据

    插入元素 

    第一步:首先用cur进行遍历,遍历index下标的链表是否存在key,如果存在就更新value,遍历完还不存在就插入元素

    1. public void put(int key, int val){
    2. int index = key % array.length;
    3. //遍历index下标的链表是否存在key,如果存在就更新value,不存在就插入数据
    4. Node cur = array[index];
    5. while(cur!=null){
    6. if(cur.key == key){
    7. //更新value
    8. cur.val = val;
    9. }
    10. cur = cur.next;
    11. }
    12. //cur==null-->遍历完没有找到key
    13. // 头插法
    14. Node node = new Node(key,val);
    15. node.next = array[index];
    16. array[index] = node;
    17. usedSize++;
    18. }

    第二步:计算负载因子

    如果负载因子仍然大于默认的最低负载因子,则散列表需要进行扩容

    1. public static final float DEFAULT_LOAD_FACTOR = 0.75f;
    2. private float doLoadFactor(){
    3. return usedSize * 1.0f / array.length;
    4. }

    面试题:可以这样进行扩容吗?

    1. if(doLoadFactor()>DEFAULT_LOAD_FACTOR){
    2. //扩容
    3. array = Arrays.copyOf(array, 2*array.length);
    4. }

    都这么问了,那很明显是不行的😊

    假设我们的11元素经过哈希之后放在插入到1下标这里

    经过扩容之后,capacity发生了改变,变成了20

    根据hash(key) = key % capacity,此时11%20 = 11

    因为长度改变,原来冲突的元素放到了其他位置中去,所以这样扩容是不行滴

    设置cur遍历原来的数组,每遍历到一个元素就纵向遍历链表的元素并进行头插法

    代码: 

    注意:这里为什么要用一个tmp保存cur.next呢?

    所以我们用一个tmp来保存原来的纵向遍历时11元素的地址


    获取元素

    1. public int get(int key){
    2. int index = key % array.length;
    3. Node cur = array[index];
    4. while(cur != null){
    5. if(cur.key == key){
    6. return cur.val;
    7. }
    8. cur = cur.next;
    9. }
    10. return -1;
    11. }

    Map和Set其他一些注意事项

    hashCode 

    创建一个student类,放入学生身份id

    1. class Student {
    2. public String id;
    3. public Student(String id) {
    4. this.id = id;
    5. }
    6. }
    7. public class Test {
    8. public static void main(String[] args) {
    9. Student student1 = new Student("61012345");
    10. Student student2 = new Student("61012345");
    11. System.out.println(student1.hashCode());
    12. System.out.println(student2.hashCode());
    13. }
    14. }

    我们发现打印出两种结果

    在没有重写hashCode方法的时候,系统默认调取Object类里面的hashCode方法

    虽然student1student2id属性值相同,但它们是不同的对象实例,因此它们在内存中有不同的地址。而Object里的hashCode使用对象的地址来生成哈希码,才有上面两种不同的结果

    我们在student类里面重写一下方法(tips:可以点击generate-->equals() and hashCode()-->一路点下去创建方法)

    1. @Override
    2. public boolean equals(Object o) {
    3. if (this == o) return true;
    4. if (!(o instanceof Student)) return false;
    5. Student student = (Student) o;
    6. return Objects.equals(id, student.id);
    7. }
    8. @Override
    9. public int hashCode() {
    10. return Objects.hash(id);
    11. }

    重写完打印结果

    这个结果才满足x%len=hashcode这个算式,x相同,len确定,得到的结果就是一样的

    🆗接下来我们想要重新写一下上面的哈希桶代码,跟之前手搓的不一样的是,重写的方法允许key传入一个类实例化对象而不是单纯能进行比较大小的数字

    泛型类哈希桶代码

    1. public class HashBuck2 {
    2. static class Node{
    3. public K key;
    4. public V val;
    5. public Node next;
    6. public Node(K key, V val){
    7. this.key = key;
    8. this.val = val;
    9. }
    10. }
    11. public Node[] array;
    12. public int usedSize;
    13. public static final float DEFAULT_LOAD_FACTOR = 0.75f;
    14. public HashBuck2(){
    15. array = (Node[]) new Node[10];//强转
    16. }
    17. public void put(K key, V val){
    18. int hash = key.hashCode();
    19. int index = hash % array.length;
    20. Node cur = array[index];
    21. while(cur!=null){
    22. //引用类型不能用等号
    23. //if(cur.key == key){
    24. if(cur.key.equals(key)){
    25. //更新value
    26. cur.val = val;
    27. }
    28. cur = cur.next;
    29. }
    30. Node node = new Node(key,val);
    31. node.next = array[index];
    32. array[index] = node;
    33. usedSize++;
    34. }
    35. public V getValue(K key){
    36. int hash = key.hashCode();
    37. int index = hash % array.length;
    38. Node cur = array[index];
    39. while(cur != null){
    40. if(cur.key.equals(key)){
    41. return cur.val;
    42. }
    43. cur = cur.next;
    44. }
    45. return null;
    46. }
    47. }

    测试:

    问题:hashCode和equals区别

    一个例子带你看明白:

    ⚠以后在写自定义对象的时候建议把equals和hashCode重写一下

  • 相关阅读:
    LeetCode994.腐烂的橘子
    Java中的StringTable常量池
    R语言【数据集的导入导出】
    电脑系统还原怎么操作?
    5.3-5.4二分搜索算法实例
    Linux安装OpenCV——利用包管理器apt从源仓库安装(绝对是最简单的安装方法)
    WPF实现一个表格数据从cs获取动态渲染
    STC51单片机学习笔记6——串口发送&中断接收
    【深入浅出C#】章节10: 最佳实践和性能优化:内存管理和资源释放
    AP8100 DC-DC 升压恒压电源管理芯片
  • 原文地址:https://blog.csdn.net/hellg/article/details/134240783