【数据结构】Map和Set

目录

前言 

 1、搜索树

1.1、概念

 1.2、创建一个二叉搜索树的类

1.2.1、查找二叉搜索树中指定的值

1.2.2、插入一个数据

1.4、删除一个数据 

2、搜索

2.1、概念理解以及使用场景

2.2、Set和Map的模型 

3、Map的学习使用

3.1、Map中常用的方法介绍

1、put方法(TreeMap中放入数据) 

 2、get方法(返回key对应的value)

 3、 getOrDefault方法,如果没有这个Key,则返回一个默认的值(这里的默认值,有自己填入)

 4、keySet方法(将Map中所有的key都放入Set当中)【Collection value与这个方法类似】

 5、entrySet() 方法【难点】

4、Set的学习使用

4.1、常见的方法说明

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


前言 

Java中的集合包括三大类,他们是Set(类)、List(列表)和Map(映射),他们都处于java.util包中,Set、List和Map都是接口,他们有各自的实现类。Set的实现类主要有HashSet和TreeSet,List的实现类主要有ArrayList,Map的实现类主要有HashMap和TreeMap。

  • HashSet和HashMap 他们的底层是一个哈希表
  • TreeMap和TreeSet他们的底层是一棵搜索树【红黑树】

Set和Map的作用:在以后的学习中,涉及到了一些查找和搜索的时候,我们可以使用实现了Set和Map接口的某个具体的类 。


 1、搜索树

1.1、概念

二叉搜索树又称二叉排序树,它要么是一棵空树,要么是具有以下性质的二叉树:

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

如下图: 

 

 二叉搜索树上没有两个相同的值。


 1.2、创建一个二叉搜索树的类

当然这里我们并没有实现搜素二叉树的建立,我们只是了解以下,它的思维。 

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;//定义一个二叉搜索树的根结点
}

1.2.1、查找二叉搜索树中指定的值

代码示例: 

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;//定义一个二叉搜索树的根结点
    //查找二叉搜索树中指定的值val
    public TreeNode find(int val){
        TreeNode cur = root;//定义一个cur结点遍历这颗二叉树搜索树,root不动,记录整颗二叉树,别的方法在使用的时候,也可以从root结点开始,防止root在这个方法中移动之后,找不到开始root的位置。
        while(root != null){//假设这颗树还有孩子节点,继续遍历
            if(cur.val > val){//要查找的值,比cur遍历到的结点的值小,则向cur的左子树遍历
                cur = cur.left;
            }else if(cur.val < val){//要查找的值,比cur遍历到的结点的值大,则向cur的右子树遍历
                cur = cur.right;
            }else{
                return cur;//找到了,返回cur走到的结点
            }
        }
        return null;//遍历完都没有找到要查找的值,返回null.
    }
}

1.2.2、插入一个数据

代码示例 

   //插入一个数据
   public void insert(int val){
        if(root == null){//当搜索二叉树为空,则创建一个结点传入val,将其作为根节点
            root = new TreeNode(val);
            return;
        }
        //搜索二叉树不为空
        TreeNode cur = root;//创建一个cur遍历二叉树
        TreeNode parent = root;//创建一个parent变量来记录cur遍历过的结点,防止cur遍历到叶子节点后,继续向下遍历时,丢失对叶子节点的记录
        while(cur != null){//如果cur没有将搜索二叉树遍历完,进入
            if(cur.val > val){//如果cur遍历到的结点的值,比要插入的值大
                parent = cur;//parent先记录cur当前位置
                cur = cur.left;//cur去遍历当前结点的左子树
            }else if(cur.val < val){//当cur遍历到的结点的值,比要插入的值小
                parent = cur;//parent记录当前cur的位置
                cur = cur.right;//cur遍历当前结点的右子树
            }else{//当要插入的值与cur的值相等
                return;//直接返回,因为这是一个搜索二叉树,没有必要有两个相同值的结点,搜索二叉树只是让树中有这个值就行。
            }
        }
        //当cur遍历到的结点为空时,
        TreeNode newNode = new TreeNode(val);//创建一个结点,让其值域为val
        if(parent.val > val){//如果val的值比parent记录的结点的值小
            parent.left = newNode;//将这个结点,放在parent记录的结点的左子树
        }else{//如果val比parent的val大
            parent.right = newNode;//将这个节点,放在parent记录的结点的右子树
        }
   }

❗❗❗注意:当我们在插入多个数据的时候,插入的数据比根节点都大的时候,极端情况下,就会形成一个单分支的树,这样在查找的时候,它的时间复杂度会变得很高,这是为了让树变得平衡,就引进了AVL了树,当高度相差超过1的时候,就会旋转,平衡左右子树的高度。

1.4、删除一个数据 

我们定义一个待删除结点为cur,待删除结点的双亲接结点为parent,用来遍历搜索二叉树,那么以cur为参考,存在三种情况,这三种情况中还有三种情况

1、cur遍历到的结点的左子树为空(cur.left == null)

  1. cur是根节点(root),将cur所指结点删除之后,则root指向cur结点的右子树(root = cur.right)

  2. cur不是根节点,cur是parent结点的左子树(parent.left),将cur所指结点删除之后,  则parent结点的左子树,指向cur结点的右子树,即parent.left = cur.right;

  3. cur不是根节点,cur是parent结点的右子树(parent.right),将cur所指结点删除之后,则parent结点的右子树指向cur的右子树,即parent.right = cur.right;

2、cur遍历到的结点的右子树为空(cur.right == null)

  1. cur是根节点(root),将cur所指结点删除之后,则root指向cur结点的左子树(root = cur.right)

  2. cur不是根节点(root),cur是parent结点的右子树(parent.right)将cur所指结点删除之后,则parent结点的右子树指向cur结点的左子树。

  3. cur不是根节点,cur是parent结点的左子树(parent.left)将cur所指结点的结点删除之后,则parent结点的左子树指向cur结点的左子树。

3、cur遍历到的结点的左右子树都不为空(cur.left != null && cur.right != null)

需要使用替换法进行删除。

代码示例

    public void remove(int val){
        TreeNode cur = root;//定义cur,遍历要删除的数字
        TreeNode parent = null;//定义parent,记录cur当前所在位置
        while(cur != null){//没有将树遍历完,并且没有找到要删除的值,进入循环
            if(cur.val == val){//找到要删除结点
                removeNode(parent,cur);//将parent,cur结点传给删除操作的方法
            }else if(cur.val < val){//cur遍历到的结点的值,小于要删除的值
                parent = cur;//parent先将cur当前位置记录下来
                cur = cur.right;//cur向搜索二叉树的右子树遍历
            }else{//当cur的值大于要删除的值
                parent = cur;
                cur = cur.left;//cur向搜索二叉树的左子树遍历
            }
        }
}
    /*
    * 删除操作
    * */
    private void removeNode(TreeNode parent, TreeNode cur) {
        //cur的左子树为空
        if(cur.left == null){
            //cur指向root结点
            if(cur == root){
                root = root.right;//将root结点删除之后,新的root为root的右子树
            //cur是parent的左子树,但cur没有左子树
            } else if (parent.left == cur) {
                parent.left = cur.right;//将cur删掉之后,parent的左子树指向cur的右子树
            //cur是parent的右子树,但是cur没有左子树
            }else{
                parent.right = cur.right;//将cur删除之后,cur的右子树传递给parent的右子树
            }
            //cur的右子树为空
        }else if(cur.right == null){
            //cur指向根节点
            if(cur == root){
                root = cur.left;//因为cur没有右子树,所以将cur删除之后,cur的左子树作为新的根结点
            //cur是parent的左子树,但是cur没有右子树    
            }else if(parent.left == cur){
                parent.left = cur.left;//将cur删除之后,cur的左子树传给parent的左子树
            //cur是parent的右子树,但是cur没有右子树    
            }else{
                parent.right = cur.left;//将cur删除之后,cur的左子树传递给parent的右子树
            }
            //cur的左右子树都不为空
        }else{
            TreeNode target = cur.right;//定义target指向cur结点的右子树
            TreeNode targetParent = cur;//定义targetParent指向cur结点
            //当target的左子树不为空,则进入循环,直到找到的target没有左孩子,但有可能存在右孩子
            while(target.left != null){
                targetParent = target;
                target = target.left;
            }
            //当找到没有左孩子的target结点,或者在指定target等于cur的右子树的时候,target就没有左孩子结点,target为targetParent的右孩子节点
            cur.val = target.val;//将target的值赋给cur结点,这样就将要删除的值,转换为删除target结点
            //如果是通过循环找到的target结点
            if(target == targetParent.left){
                targetParent.left = target.right;//将target结点的右孩子结点传给targetParent结点的左子树
            //如果target是targetParent的右孩子结点    
            }else{
                targetParent.right = target.right;//将target结点的右孩子结点传给targetParent结点的右孩子结点
            }
        }
    }

 总结:

  • 插入和删除操作都需要先查找,查找效率代表了二叉搜索树中各个操纵的性能。
  • 在有n个结点的,以乱序的方式,插入二叉搜索树,则可能会形成一棵高度较平衡的二叉树,若以顺序的方式插入搜索二叉树,则这颗树,极有可能形成单分支的树。
  • 在最好的情况下,这棵树是高度平衡的二叉树,比较平均次数为:logN
  • 在最坏的情况下,则棵树是一个单分支的树,比较平均次数为:N/2;

2、搜索

2.1、概念理解以及使用场景

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

我们以前常见的搜索方式有:

  • 直接遍历,时间复杂度为O(N),但是在元素如果比较多的情况下效率会非常的慢。
  • 二分查找,时间复杂度为O(logN),但是搜索前必须要求序列是有序的。

这两种查找方式跟适合静态类型的查找,即这些数据不会再发生变动。

而我们再现实中查找比如:

  • 根据名字查找图书
  • 通讯录,即根据姓名查询联系方式
  • 不重复集合,即需要先搜索关键字是否已经在集合中

可能在查找时进行一些插入和删除的操作,即动态查找,那上述两种方式就不太适合了,下面我们说到的Map和Set是 一种适合动态查找的集合容器。


2.2、Set和Map的模型 

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

纯Key模型,比如:

  • 有一个字典,快速查找一个单词是否再词典中
  • 快速查找某个名字在不在通讯录中

Key-Value模型,比如:

  • 统计文件中每个单词出现的次数,统计结果是每个单词都有与其对应的次数<单词,出现次数>。

Map使用的就是Key-Value模型,Set使用的是Key模型。 


3、Map的学习使用

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

3.1、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<K>  keySet() 返回所有 key 的不重复集合
Collection<V> values()返回所有 value 的可重复集合
Set<Map,Entry> entrySet() 返回所有的 key-value 映射关系
boolean containsKey(Object key) 判断是否包含 key
boolean containsValue(Object value) 判断是否包含 value

1、put方法(TreeMap中放入数据) 

 

 2、get方法(返回key对应的value)

 3、 getOrDefault方法,如果没有这个Key,则返回一个默认的值(这里的默认值,有自己填入)

 4、keySet方法(将Map中所有的key都放入Set当中)【Collection<V> value与这个方法类似】

 5、entrySet() 方法【难点】

在说这个方法之前,我们先要了解一下Map.Entry<K,V>。

Entry<K,V>是Map内部实现的用来存放<Key,value>键值对映射关系的内部类。

 现在来了解entrySet()方法的使用

 Map.entrySet()方法返回的是一个Set<Map.Entry<K,V>>类型的值,首先该返回值是一个集合Set,集合中的元素是Map.Entry<K,V>类型的,每个Map.Entry可以看作是一个键值对对象,可以通过getKey()和getValue()方法分别获取其键和值。

 


注意事项:

1、Map是一个接口,不能直接实例化对象,如果要实例化对象只能实例化其实现类TreeMap或者HashMap

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

 

3、在TreeMap中插入键值对是,key不能为空,否则就会抛NullPointerException异常,value可以为空但是HashMap的key和value都可以为空

 

4、Map中的key可以全部分离出来,存储到Set中来进行访问(因为key不能重复)【刚刚使用的keySet实现的就是将key放在Set当中】

5、Map中的value可以全部分离出来,存储到Collection的任何一个集合中(value可能有重复)【value和key同理】

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


4、Set的学习使用

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

4.1、常见的方法说明

方法解释
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 c)将集合c中的元素添加到set中,可以达到去重的效果

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

 注意事项:

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

2、Set中只存储key,并且要求key一定要唯一

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

4、Set最大的功能就是对集合中的元素进行去重(set当中的元素是不能重复的)

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

6、Set中的key不能修改,如果要修改,健将原来的删除掉,然后再重新插入

7、TreeSet中不能插入null的key(因为需要key需要比较),HashSet可以。

 


  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值