数据结构-Java Map 和 Set

本文介绍了Java中Set接口和Map接口的实现类,如HashSet、HashMap、TreeSet和TreeMap。Set接口继承自Collection,而Map是独立接口。文章详细讲解了二叉搜索树的概念、插入、查找和删除操作,并探讨了Map和Set作为搜索容器的性能分析。此外,还讨论了Map和Set的使用,包括它们的存储方式、访问方法和相关API。
摘要由CSDN通过智能技术生成

前言

Set接口是继承与Collection的,而Map是独立的一个接口。

其中Set的实现类有:

  1. TreeSet

  1. HashSet

Map实现的类有:

  1. HashMap

  1. TreeMap

(HashSet和HashMap底层是一个哈希表,TreeSet和TreeMap底层是一棵搜索树(红黑树)

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

搜索树

定义树:

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. 若它的左子树不为空,则左子树上所有的节点的值,都小于根节点的值

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

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

二叉搜索树

中序遍历二叉搜索树,得到的结果是一组有序的数据。

查找

如上图,首先假设上图的二叉树已经存在,且根节点为root,要查找的值为val,如果root为空就返回null,定义一个cur节点来遍历这棵树,如果root.val > val,根据二叉搜索树的性质,就去root结点的左子树去找,否则就去右子树找,如果找到了(cur.val == val),就然后cur结点,直到cur == null,就返回空的结点。

/**
    *  查找二叉搜索树val的值
    *  val
    *
    */
    public TreeNode find(int val) {
        TreeNode cur = root;
        while (cur != null) {
            if (cur.val == val) {
                return cur;
            } else if (cur.val < val) {
                cur = cur.right;
            } else {
                cur = cur.left;
            }
        }
        return null;

    }

插入

搜索树满足概念中性质a,b,c三点,如果要插入一个val。值首先定义一个cur节点,如果root == null就 new一个 值为val的节点newNode,并将root置为这个节点(root == newNode)

如果root != null,就进行搜索,如果root.val > val,就去左边继续搜索,如果root.val < val就去右边搜索。如果当前cur结点的左和右都为null,则直接比较插入即可

例如在上图中插入6,12 > 6,就去12的左树插入,12的左树的根值为5 < 6,所以去5这个节点的右树插入,其右树跟为9 > 6,又因为val为9这个结点的左和右都为null。所以直接在这个结点的左侧插入。

// 不使用parent结点
public void insert1(int val) {
        if (root == null) {
            root = new TreeNode(val);
            return;
        }
        TreeNode newNode = new TreeNode(val);
        TreeNode cur = root;
        while (cur.left != null && cur.right != null) {
            if (newNode.val > cur.val) {
                cur =cur.right;
            }else {
                cur =cur.left;
            }
        }
        if (newNode.val > cur.val) {
            cur.right = newNode;
        }else {
            cur.left = newNode;
        }
    }

当然也可以使用parent结点来记录cur的上一个结点,如果cur == null的时候,就可以找到cur的上一个结点parent,并在parent这个结点上插入。

public void insert2(int val) {
        if (root == null) {
            root = new TreeNode(val);
            return;
        }
        TreeNode newNode = new TreeNode(val);
        TreeNode cur = root;
        TreeNode parent = null;
        while (cur != null) {
            parent = cur;
            if (cur.val < val) {
                cur =cur.right;
            }else {
                cur = cur.left;
            }
        }
        if (parent.val < val) {
            parent.right = newNode;
        }else {
            parent.left = newNode;
        }
    }

删除

二叉搜索树

如何删除

设cur节点为当前要删除的节点,parent为cur结点的父节点。

  1. 如果 cur.left == null

  1. cur == root && root.left == null ,则root = cur.right。

  1. cur != root && cur == parent.left ,则 parent.left = cur.right

  1. cur != root && cur == parent.right,则 parent.right = cur.right

  1. 如果cur.right == null,则原理同上。

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

  1. cur != root && cur == parent.left,则 parent.left = cur.left

  1. cur != root && cur == parent.right,则 prent.right = cur.left

图略

  1. cur.left != null && cur.right != null。

某个二叉搜索树

假设有这样一棵二叉搜索树。root为根节点,cur为当前将要被删除的节点。

该如何删除?

需要使用替换法进行删除,即在它的右子树中寻找中序下的第一个结点(值最小),用它的值填补到被

删除节点中,再删除这个最小值节点即可。

首先,你需要找出cur节点和cur的父节点:

实现

public void remove(int val) {
    TreeNode cur = root;
    TreeNode parent = null;
    while(cur != null){
        if (cur.val == val) {
            removeNode(parent, cur);
            break;
        }else if (cur.val > val) {
            parent = cur;
            cur = cur.left;
        }else {
             parent = cur;
            cur = cur.right;   
        }
    }
}

然后需要在cur的右树里面找到其最小值,这个最小值无非就是cur的右子树的第一个结点,或者是右子树上左树的最左的一个端点。找到这个节点之后,将其删除。

然后综上所述的cur.left == null 或者 cur.right == null的情况来完成removeNode这个方法;

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.left == cur) {
                parent.left = cur.left;
            }else {
                parent.right = cur.left;
            }
        }else/* suggests that:cur.left != null && cur.right != null */ {
            TreeNode target = cur.right;
            TreeNode targetParent = cur;
            while (target.left != null) {
                targetParent = target;
                target = target.left;
            }
            cur.val = target.val;
            if (target == targetParent.left) {
                targetParent.left = target.right;
            }else {
                targetParent.right = target.right;
            }
        }
    }

性能分析

无论是插入,还是删除操作,都需要先查找,所以查找的速度代表了二叉搜索树中各个操作的性能。查找是根据节点的值比较来的,所以树越趋向于平衡(完全二叉树或者满二叉树),查找的次数就越趋向于树的深度,否则则会趋向于节点的个数,例如单分支的二叉搜索树:他的搜索时间复杂度为O(n)

完全二叉树

单分支二叉树(右单支)

最优情况下,二叉搜索树为完全二叉树,其平均比较次数为:

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

Map和Set搜索

概念

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

以前常见的搜索方式有:

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

2. 二分查找,时间复杂度为 O(),但搜索前必须要求序列是有序的

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

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

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

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

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

存储方式

和python中的Set和Dict类是类似的,他们有直接全部存储key的模型,和存放key-value键值对的模型:

  1. key模型,例如:

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

  • 快速查找某个名字是否在通讯录中

  1. key-value模型,比如:

  • 统计某一篇英文短报中,单词的出现个数,key为单词,value为单词出现的个数,也可以称为权值

  • 统计后楼梦中,个人物名字出现的次数,其key-value模型为<名字:次数>

Map是以key-value键值对的形式来存储,而Set使用的是key存储方式。

key-value:

key:

注意Map可以看做是一种特殊的Set类型,Set中key不会出现重复元素,Map中的key也不会,但是Map中的每一个key都多了一个与之对应的value权重

Map & Set 的使用

Map

概要

在前言中我们介绍过:

其中Map是单独的一个接口,没有继承自Collection,而Set接口继承Collection

单独的接口不能直接实例化对象,如果要实例化对象只能实例化其实现类TreeMap或者HashMap

Map<String, Integer> treeMap = new TreeMap<>();

Map中传入类,以key-value的形式。其中key是唯一的,key不能有重复的,而value可以有重复的。

如果new 一个 TreeMap,那么就会以搜索树的方式插入。

  • Map的put方法,向容器中存放key-value值

  • Map重写了println和print方法,可以直接打印

public static void main(String[] args) {
        Map<String, Integer> treeMap = new TreeMap<>();
        treeMap.put("hello",3);
        treeMap.put("world",4);
        System.out.println(treeMap);
}

运行结果为:

相对应的根据传入的类,print或者println会进行重载,以适应不同的情况。

但是需要注意的是,对Map进行插入的时候key不能为null,否则会抛出空指针异常(NullPointerException):

可比较性

对于Map中传入的key类,一定是可以比较的。

假设有一个student类:

class student {
    int age;
    String name;
    public student(int age, String name) {
        this.age = age;
        this.name = name;
    }
}

在key值中传入student类:

    public static void main(String[] args) {
        Map<student, Integer> treeMap = new TreeMap<>();
        treeMap.put(new student(18,"张三"),3);
        treeMap.put(new student(19,"李四"),4);
        System.out.println(treeMap);
    }

编译会抛出异常如下:

也就是说传入的student是无法比较的。

Map元素的访问

由于Map不是数组类型,无法通过treeMap[ i ]的形式访问,编译抛出异常:

java: 需要数组, 但找到java.util.Map<java.lang.String,java.lang.Integer>

使用for-each方法 来进行访问:

Map<String,Integer> treeMap = new TreeMap<>();
  1. 访问所有key,使用Map中的keySet()方法,返回Map中所有键值对key-value的key的集合

for(String e: treeMap.keySet()) {
            System.out.println(e);
}
  1. 访问所有的value,使用Map中的values()方法,返回Map中所有键值对key-value 的value的集合

for(Integer e: treeMap.values()) {
            System.out.println(e);
}

注意:for-each迭代无法直接遍历Map这种字典类型数据:

Map的方法

方法

作用

V get(Object key)

返回 key 对应的value值,key不存在则返回null

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 的不重复集合

Set<Map.Entry<K, V>> entrySet()

返回所有的 key-value 映射关系

Collection<V> values()

返回所有 value 的可重复集合

boolean containsKey(Object key)

判断是否包含 key

boolean containsValue(Object value)

判断是否包含 value

Set

概要

Set继承与Collection接口,而Map是独立的,Set,相较于Map这种key-value的存放形式,Set只需要存放key一个关键字:key是唯一的

对于Set,我们也把他称为集合,集合是一种数学概念,集合由一个或多个确定的元素所构成的整体

它具备如下性质:

  1. 无序性,一个集合中,每个元素的地位都是相同的,元素之间是无序的。集合上可以定义序关系,定义了序关系后,元素之间就可以按照序关系排序。但就集合本身的特性而言,元素之间没有必然的序

  1. 互异性,一个集合中,任何两个元素都认为是不相同的,即每个元素只能出现一次。有时需要对同一元素出现多次的情形进行刻画,可以使用多重集,其中的元素允许出现多次。

Set中的key同Map键值对key-value中的key一样,是无法直接修改的,如果要修改可以直接删除原来的key然后再重新添加即可。

Set的实现类TreeSetHashSet,还有LinkedHashSetLinkedHashSet,他两是在HashSet的基础上维护的一个双向链表,用来记录元素的插入次序,让其在结构上有序

互异性

有如下代码:其中add(Object x);是向set中添加元素的意思。

    public static void main(String[] args) {
        Set<Integer> set1 = new TreeSet<>();
        set1.add(1);
        set1.add(2);
        set1.add(3);
        System.out.println(set1);
    }

我们在set集合中添加数字1,2,3。可以发现打印结果为:

如果往里面添加两个1呢?

    public static void main(String[] args) {
        Set<Integer> set1 = new TreeSet<>();
        set1.add(1);
        set1.add(1);
        System.out.println(set1);
    }

运行结果只有1。这就是其互异性

集合Set的一个最大的作用,就是根据它的互异性来去掉重复元素

Set元素的访问

同Map的key访问方法。使用for-each迭代器

Set的方法

方法

说明

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中,可以达到去重的效果

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值