Map和Set

简介: Map和Set

搜索树

概念

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

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

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

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

操作-查找

操作-插入

1.如果树为空树,即根 == null,直接插入

2.如果树不是空树,按照查找逻辑确定插入位置,插入新节点

操作-删除

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

1.cur.left == null

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

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

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

2.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

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

       1.确定要删除结点这里将来的数据一定是比左边都大,比右边都小的数据

       2.如何保证第一条?

       (1)在左树里面找到最大的数据(左树最右边的数据)

       (2)在右树里面找到最小的数据(右树最左边的数据)

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

实现

代码如下:

public class BinarySearchTree {
    public static class Node {
        int key;
        Node left;
        Node right;
 
        public Node(int key) {
            this.key = key;
        }
    }
 
    private Node root = null;
 
    /**
     * 在搜索树中查找key,如果找到,返回key所在结点,否则返回null
     *
     * @param key
     * @return
     */
    public Node search(int key) {
        Node cur = root;
        while (cur != null) {
            //找到,并直接返回结点
            if (key == cur.key) {
                return cur;
            } else if (key < cur.key) {
                //比当前结点更小,所以到左边遍历以寻找
                cur = cur.left;
            } else {
                //比当前节点更大,所以到右边遍历以寻找
                cur = cur.right;
            }
        }
 
        //没有查找到,直接返回null
        return null;
    }
 
    /**
     * 插入操作
     *
     * @param key
     * @return true表示插入成功,false表示插入失败
     */
    public boolean insert(int key) {
        //如果root是空的,则直接创建新节点
        if (root == null) {
            root = new Node(key);
            return true;
        }
        //如果树不是空树,按照查找逻辑确定插入位置,插入新节点
        Node cur = root;
        Node parent = null;
        while (cur != null) {
            if (key == cur.key) {
                //相同表明原搜索二叉树就含有key,插入失败
                return false;
            } else if (key < cur.key) {
                //根据二叉树的性质,应该向左寻找插入位置
                parent = cur;
                cur = cur.left;
            } else {
                //向右寻找插入的位置
                parent = cur;
                cur = cur.right;
            }
        }
 
        //找到了,这里创建新的结点
        Node node = new Node(key);
        //判断新的结点应该插入右边还是左边
        if (key < parent.key) {
            parent.left = node;
        } else {
            parent.right = node;
        }
        //返回true以表明插入成功
        return true;
    }
 
    /**
     * 删除成功返回true,删除失败返回false
     *
     * @param key
     * @return
     */
    public boolean remove(int key) {
        Node cur = root;
        Node parent = null;
        while (cur != null) {
            if (key == cur.key) {
                //找到了,跳出准备进行删除操作
                break;
            } else if (key < cur.key) {
                parent = cur;
                cur = cur.right;
            } else {
                parent = cur;
                cur = cur.right;
            }
        }
 
        //跳出循环后准备删除操作
        //先判断要删除的元素是否存在,为null则删除失败
        if (cur == null) {
            return false;
        }
        /*、
        根据cur的孩子是否存在分四种情况
        1.cur左右孩子均不存在
        2.cur只有左孩子
        3.cur只有右孩子
        4.cur的左右孩子均存在
        看起来只有四种情况,实际情况1可以与情况2或者情况三进行合并,只需要处理那种情况即可
        除了情况四之外,其他情况可以直接删除
        情况四不能直接删除,需要在子树中找到一个替代结点进行删除
         */
        if (cur.left == null) {
            if (cur == root) {
                root = cur.right;
            } else {
                if (cur == parent.left) {
                    parent.left = cur.right;
                } else if (cur == parent.right) {
                    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 if (cur == parent.right) {
                    parent.right = cur.left;
                }
            }
        } else {
            //找到右树中最小的,让其与删除结点替换,然后删除这个结点
            Node targetParent = cur;
            Node target = cur.right;
            while(target.left != null) {
                targetParent = target;
                target = targetParent.left;
            }
            cur.key = target.key;
            //再删除target这个结点
            if(target == targetParent.right) {
                targetParent.right = target.right;
            } else if(target == targetParent.left) {
                targetParent.left = target.right;
            }
        }
        return true;
    }
}

性能分析

插入和删除操作都必须先查找,查找效率代表了二叉搜索树中各个操作的性能

对有n个结点的二叉搜索树,若每个元素查找的概率相等,则二叉搜索树平均查找长度是结点在二叉搜索树的深度的函数,即节点越深,则比较次数越多

但对于同一个关键码的集合,如果各关键码插入的次序不同,可能得到不同结构的二叉搜索树: 比如一个单支搜索树就是比较特殊的情况

最优情况下,二叉树为完全二叉树,其平均比较次数为log2 N

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

问题提出:如果退化为单支树,二叉树的搜索性能就失去了。那能否进行改进,不论按照什么次序插入关键码,都可以使二叉树的性能最佳?

和java类集的关系

TreeMap和TreeSet即java中利用的搜索树实现的Map和Set;实际上用的是红黑树,而红黑树是一棵近似平衡的二叉搜索树,即在二叉搜索树的基础之上+颜色以及红黑树性质验证。

搜索

概念及场景

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

1.直接遍历,时间复杂度为O(N),元素如果比较多效率会非常慢

2.二分查找,时间复杂度为O(log2 N),但在搜索之前会要求序列是有序的

上述排序比较适合静态类型的查找,即一般不会对区间进行插入和删除操作了,而现实中的方法比如:

1.根据姓名查询考试成绩

2.通讯录,即根据姓名查询联系方式

3.不重复集合,即需要先搜索关键字是否已经在集合中

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

模型

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

1.纯Key模型,比如:

有一个英文词典,快速查找一个单词是否在词典中

快速查找某个名字在不在通讯录中

2.Key-Value型,比如:

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

梁山好汉的江湖绰号:每个好汉都有自己的江湖绰号

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

Map的使用

关于Map的说明

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

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

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

方法 解释
K getKey() 返回entry中的key
V getValue() 返回entry中的value
V setValue(V value) 将键值对中的value替换为指定value

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

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<K,V>> entrySet() 返回所有的key-value映射关系
boolean containsKey(Object key) 判断是否包含key
boolean containsValue(Object value) 判断是否包含value

注意:

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

2. Map中存放键值对时,key不能为空,否则会抛NullPointerException异常,value可以为空。但是HashMap的key和value都可以为空。

3.Map中存放键值对的key是唯一的,value是可以重复的

4.Map中的key可以全部分离出来,存储到Set中来进行访问(因为key不能重复)

5.Map中的value可以全部分离出来,存储在Collection的任何一个子集合中(value可能有重复)

6.Map中的键值对的key不能修改,value可以修改,如果要修改key,只能先将key删除掉,再重新插入新的key

TreeMap和HashMap的区别

Map底层结构 TreeMap HashMap
底层结构 红黑树 哈希桶
插入/删除/查找时间复杂度 O(log2 N) O(1)
是否有序 关于key有序 无序
线程安全 不安全 不安全
插入/删除/查找的区别 需要进行元素比较 通过哈希函数计算哈希地址
比较和覆写 key必须能够比较,否则会抛出ClassCastException异常 自定义类型需要覆写equals和hashCode方法
应用场景

需要在key有序的场景下

不关心key是否有序,需要高的时间性能

Set的说明

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

常见方法的说明

方法 解释
boolean add(E e) 添加元素,但重复的元素不会添加成功
void clear() 清空集合
boolean contains(Object o) 判断集合o是否在集合中
Iterator<E> 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中,可以达到去重的效果

注意:

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

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

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

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

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

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

7.TreeSet中不能插入null的key,HashSet可以

TreeSet使用举例

public class SetTest {
    public static void TestSet() {
        Set<String> s = new TreeSet<>();
 
        //add(key):如果key不存在,则插入,返回true
        //如果key存在,返回false
        boolean isIn = s.add("apple");
        s.add("orange");
        s.add("peach");
        s.add("banana");
        System.out.println(s.size());
        System.out.println(s);
 
        isIn = s.add("apple");
        //add(key):key如果是空,抛出空指针异常
        //s.add(null);
 
        //contains(key):如果key存在,返回true,否则返回false
        System.out.println(s.contains("apple"));
        System.out.println(s.contains("watermelen"));
 
        //remove(key):key存在,删除成功,返回true
        //  key不存在,删除失败返回false
        //  key为空,抛出空指针异常
        s.remove("apple");
        System.out.println(s);
 
        s.remove("watermelen");
        System.out.println(s);
 
        //利用迭代器遍历
        Iterator<String> it = s.iterator();
        while(it.hasNext()) {
            System.out.print(it.next() + " ");
        }
        System.out.println();
    }
 
    public static void main(String[] args) {
        TestSet();
    }
}

 

 

相关文章
|
6天前
|
存储 JavaScript 索引
js开发:请解释什么是ES6的Map和Set,以及它们与普通对象和数组的区别。
ES6引入了Map和Set数据结构。Map的键可以是任意类型且有序,与对象的字符串或符号键不同;Set存储唯一值,无重复。两者皆可迭代,支持for...of循环。Map有get、set、has、delete等方法,Set有add、delete、has方法。示例展示了Map和Set的基本操作。
17 3
|
1月前
|
存储 自然语言处理 C++
map和set的简单介绍
map和set的简单介绍
20 1
|
1月前
|
存储 安全 Java
java集合框架及其特点(List、Set、Queue、Map)
java集合框架及其特点(List、Set、Queue、Map)
|
3月前
|
JavaScript 前端开发 定位技术
JavaScript 中如何代理 Set(集合) 和 Map(映射)
JavaScript 中如何代理 Set(集合) 和 Map(映射)
50 0
|
3月前
|
存储 安全 Java
Map和Set(JAVA)
Map和Set(JAVA)
50 1
|
3月前
|
编译器 C++ 容器
【C++学习手札】基于红黑树封装模拟实现map和set
【C++学习手札】基于红黑树封装模拟实现map和set
|
7天前
|
存储 搜索推荐 C++
【C++高阶(二)】熟悉STL中的map和set --了解KV模型和pair结构
【C++高阶(二)】熟悉STL中的map和set --了解KV模型和pair结构
|
26天前
|
存储 JavaScript 前端开发
set和map的区别
set和map的区别
38 4
|
1月前
|
存储 编译器 容器
用红黑树封装实现map和set
用红黑树封装实现map和set
14 0
|
1月前
|
存储 JSON C++
【C++】容器篇(五)—— map和set的基本介绍
【C++】容器篇(五)—— map和set的基本介绍