Java Map和Set

目录

一.二叉搜索树

1.概念

2.二叉搜索树功能的模拟实现

2.1 查找

2.2 插入

2.3 删除

二.Map

1. Map.Entry,v>

2.Map的常用方法

3.TreeMap和HashMap的区别

4.注意事项

三.Set

1.Set的常见方法

2.TreeSet和HashSet的区别

3.注意事项

四.哈希表

1.哈希表的介绍

 2.冲突

 3.负载因子

4.开散列(哈希桶)


一.二叉搜索树

1.概念

二叉搜索树具有以下特性:

若它的左子树不为空,则左子树上所有节点的值都小于根节点的值;

若它的右子树不为空,则右子树上所有节点的值都大于根节点的值;

它的左右子树也分别为二叉搜索树;

空树也是二叉搜索树。

2.二叉搜索树功能的模拟实现

基本信息:

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

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

2.1 查找

根据二叉搜索树的特性,我们首先比较要查的值与根节点的大小关系,如果相等,正好找到;如果大于,说明可能在右子树上;如果小于,说明可能在左子树上。

这个思路很简单,直接上代码:

    public TreeNode search(int val) {
        if(root==null){
            return null;
        }

        TreeNode cur=root;

        while(cur!=null){
            if(cur.val==val){
                return cur;
            }else if(cur.val>val){
                cur=cur.left;
            }else{
                cur=cur.right;
            }
        }

        return null;
    }

2.2 插入

首先我们要先查找这个点是否存在,因为这是一颗搜索树,是用来查某一个 val 是否存在的,存一个和存两个没有区别。如果不存在,说明我们可以插入了,所以说下面我们要找一个合适的位置插入。

第一步先判断是不是空树,如果是空树直接在root上操作。如果不是就遍历这颗树,我们要不断往下找,操作类似查找。如果一个节点比 key 的值小(说明   key 在节点的右子树上),同时这个节点的右子树还是空的,那么直接插在这个节点的右子树上;相反,如果一个节点比 key 的值大,同时左子树还是空的,那么直接插在这个节点的左子树上。

public void insert(int key) {
    if(root==null){
        root=new TreeNode(key);
        return ;
    }

    TreeNode node=new TreeNode(key);

    TreeNode cur=root;

    while(cur!=null){
        if(cur.val<key && cur.right==null){
            cur.right=node;
        }else if (cur.val>key && cur.left==null){
            cur.left=node;
        }else{
            if(cur.val<key){
                cur=cur.right;
            }else if(cur.val>key){
                cur=cur.left;
            }else{
                return ;
            }
        }
    }
}

2.3 删除

删除操作是最难的,要考虑的情况较多。

要删除的节点可以分为以下几种情况:1.左子树是空;2.右子树是空;3.左右子树都不是空;4.左右子树都是空。

我们要定义一个 parent 节点,这个节点用来存储cur的父节点。图中的例子cur是parent的右节点,cur当然也可以是parent的左节点。

cur的左子树为空,那么要删除cur的话首先要判断cur的parent的左节点还是右节点。如果和图中例子一样为右节点,那么 parent.right = cur.right;如果为左节点,那么 parent.left = cur.right。

cur的右子树为空的判断处理与左子树的相同这里就不过多赘述了。

重量级的来的,cur的左右子树都不为空。对于这个问题,我们采用的是替换,将cur的值替换掉。

那么谁来替换cur呢?这又要说到搜索树的性质了,cur的左子树上的值都比cur小,所以说,可以取出cur左子树中的最大值来替代cur,这样能继续满足搜索树的要求即左子树的上的值都比cur上的小(因为它是最大值)。取cur右子树的最小值行不行?也行。

那什么样的节点是最值节点呢?在左子树中,一个节点没有右子树,说明其是左子树最右面的节点,即为最大节点。同理在右子树中,一个节点没有左子树,说明其是右子树最左面的节点,即为最小节点。

知道了这些我们可以写代码了,代码上有注释:

public void remove(int key){
    TreeNode cur=root;
    TreeNode parent=root;

    //找到我们要删除节点在哪里
    while(cur!=null){
        if(cur.val>key){
            parent=cur;
            cur=cur.left;
        }else if(cur.val<key){
            parent=cur;
            cur=cur.right;
        }else{
            //找到了
            removeNode(parent,cur);
            return;
        }
    }
}

private 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{
            parent.right=cur.right;
        }
    }else if(cur.right==null){
        //右子树为空
        if(cur==root){
            root=cur.left;
        }else if(parent.right==cur){
            parent.right=cur.left;
        }else{
            parent.left=cur.left;
        }
    }else{
        //这里以找右子树的最小值为例
        TreeNode tem=cur.right;
        //存它的目的是为了一会删最值节点用的
        TreeNode temParent=cur;

        //找最小值所在的节点
        while(tem.left!=null){
            temParent=tem;
            tem=tem.left;
        }

        cur.val=tem.val;
        if(temParent.left==tem){
            temParent.left=tem.right;
        }else{
            temParent.right=tem.right;
        }
    }
}

二.Map

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

1. Map.Entry<K,V>

Map.Entry是Map的一个内部类,用来存放K和V的映射关系,也就是说它存的是一个集合,集合里都是映射关系。这个内部类里也提供了也写方法:

方法

功能
K getKey()返回 key 值
V getValue()返回 value 值
V setValue(V value)替换 value 值

2.Map的常用方法

注意这里总结的是常用的方法,不是全部方法,全部方法去idea上看即可。

方法功能
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
void clear()清空集合
int size()获得集合大小
boolean isEmpty()判断集合是否为空

3.TreeMap和HashMap的区别

MapTreeMapHashMap
底层结构红黑树哈希桶
基本操作的时间复杂度O(log(N))O(1)
是否有序关于key有序无序
线性安全不安全不安全
基本操作的区别需要进行元素的比较通过哈希实现
比较和覆写key必须可以比较自定义类型需要覆写
应用场景需要key有序场景下key有序无关,需要更高的时间性能

4.注意事项

1.Map是一个接口,接口不能直接实例化,要借助TreeMap或HashMap来实例化。

Map<Integer,Integer> map1=new TreeMap<>();
Map<Integer,Integer> map2=new HashMap<>();

2.key和value是一种映射关系,就是说我们可以用key来找其对应的value值。比如我们可以存姓名和年龄,姓名的key,年龄是value。我们就可以通过姓名来找到这个人的年龄。这里注意,姓名也就是key是不能重复的,但年龄(value)是可以重复的。

3. TreeMap插入关系时,value可以为空,key不为空;但HashMap中都可以为空

4.Map中的Key可以全部分离出来,存储到Set中来进行访问(因为Key不能重复);Map中的value可以全部分离出来,存储在Collection的任何一个子集合中(value可能有重复)。

三.Set

Set继承自 Collection 的类接口,Set中只存储Key。

1.Set的常见方法

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

2.TreeSet和HashSet的区别

SetTreeSetHashSet
底层结构红黑树哈希桶
基本操作的时间复杂度O(log(N))O(1)
是否有序关于key有序不一定有序
线性安全不安全不安全
基本操作的区别需要进行元素的比较通过哈希实现
比较和覆写key必须可以比较自定义类型需要覆写
应用场景需要key有序场景下key有序无关,需要更高的时间性能

3.注意事项

1.Set这个接口适用于只需要判断某一元素是否在集合里的这种情况,像判断某一班级里有没有XX这个同学。

2.TreeSet插入的key值不可以是空的,而HashSet可以插入空的

四.哈希表

在用树实现集合时,我们发现每次找数据都要比较很多次,太慢了。有没有说明方法能不用比较直接找到呢?哈希方法可以。

1.哈希表的介绍

哈希表又叫散列表,是通过哈希函数建立的一个数据结构。

哈希函数是什么呢?

举个例子:

我们定义一组数据{1,5,6,8,7,9};

此时哈希函数是:hash(key) = key % capacity(capacity为存储元素底层空间总的大小)。

 2.冲突

还是上面的例子,如果在数据中加入了15,我们发现有问题。因为hash(15)=5跟hash(5)冲突了。对于这种用不同的 key 值,但得到了相同的哈希地址的现象我们叫冲突。

遇见冲突我们要想办法解决这个问题的,这里要明白一点,由于我们给的底层存储空间是有限的,永远无法满足所有情况,因此冲突是无法避免的,我们能做的是降低冲突率。

在哈希函数设计时,我们有几种解决方案:

1.直接定制法:在数据范围已知的情况下使用,且查找的范围比较小且连续,如:存26个字母。函数:hash(key) =  A*key + B;

2.除留余数法:地址数为m,取一个不大于m,但最接近或者等于m的质数p作为除数。哈希函数:hash(key) = key%p;

3.取满法:仅适用于做算法题中,就是直接取到给定数据范围的最大值。

 3.负载因子

先看一下网上的说法:HashMap 负载因子( load factor),也叫做扩容因子和装载因子,它是 HashMap 在进行扩容时的一个阈值,当 HashMap 中的元素个数超过了容量乘以负载因子时,就会进行扩容。默认的负载因子是 0.75,也就是说当 HashMap 中的元素个数超过了容量的 75% 时,就会进行扩容。

关于为什么负载因子是0.75,现在没有什么官方解释。

4.开散列(哈希桶)

开散列法又叫拉链法,名字通俗易懂,其结构就跟拉链一样,数组内存的是链表头节点的地址。

代码的模拟实现:

public class HashBucket {
    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 = new Node[10];
    public int usedSize;

    public static final double DEFAULT_LOAD_FACTOR = 0.75f;

    public void push(int key,int val){
        int idx=key% array.length;
        //先判断key是否存在
        Node cur=array[idx];
        while(cur!=null){
            if(cur.key==key){
                cur.val=val;
                return ;
            }
            cur=cur.next;
        }
        //如果不存在
        Node node=new Node(key, val);

        node.next=array[idx];
        array[idx]=node;
        usedSize++;
        //看看要不要扩容
        if(doLoadFactor()>=DEFAULT_LOAD_FACTOR){
            resize();
        }
    }

    private double doLoadFactor(){
        //求当前负载因子的值
        return usedSize*1.0/ array.length;
    }

    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) {
                int idx = cur.key % newArray.length;
                Node curN = cur.next;
                cur.next = newArray[idx];
                newArray[idx] = cur;
                cur = curN;
            }
        }
        
        array = newArray;
    }

    public int getVal(int key){
        int idx=key% array.length;
        Node cur=array[idx];

        while(cur!=null){
            if(cur.key==key){
                return cur.val;
            }
            cur=cur.next;
        }

        return -1;
    }
}

评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值