java数据结构_Map和Set_9.1
1. 搜索树
1.1 概念
二叉搜索树又称二叉排序树,它或者是一棵空树,或者是具有以下性质的二叉树:
- 若它的左子树不为空,则左子树上所有的结点都小于根结点的值
- 若它的右子树不为空,则右子树上所有的结点都大于根结点的值
- 它的左右子树也都分别是二叉搜索树
如下图,就是一棵二叉搜索树,二叉搜索树的中序遍历就是升序排列的
1.2 操作-查找
因为二叉搜索树天然就带有小的在左,大的在右的顺序,所以在查找的时候更加方便,类似于有天然的二分查找
代码实现如下:
1.3 操作 - 插入
按照二叉搜索树,小的在左边,大的在右边逻辑确定插入位置,插入新结点。因为要插入的结点最终都是称为叶子结点,所以光定义一个cur来表示要插入的结点还不够,还要定义一个parent结点来表示cur的父亲结点。
代码如下:
但上面代码的逻辑性还是有问题的,插入操作,并没有考虑到,如果要插入的树是一棵空树,即root == null的情况,会出现空指针的异常。
修正:
1.4 操作 - 删除(难)
搜索二叉树的删除是会发生很尴尬的情况的
如下图:
如果要删除值为8的结点,是很方便的,可以将7结点的right连接到值为9的结点。
但如果删除的是左边的值为1的结点呢?
删除了之后,3的left是连接值为2的结点呢?还是连接值为0的结点呢?这样就会出现尴尬的情况,采用的方法是替换法(也称替罪羊法),即可以在要删除的结点的右子树中,寻找一个值最小的叶子节点,换个方式讲是,找到要删除的结点的右子树的最左边子树(有点绕,可以多多理解一下),然后用这个最左边子树的值来覆盖要删除的结点的值,然后将最左边子树的结点删除。注意是覆盖,并不是将要删除的结点真的删除了,最终删除的结点是要删除的结点的右子树的最左边子树的结点。
举例:
如果要删除的结点是值为90的结点
先找到90结点的右子树为值为120的结点,然后再找到右子树120的最左边子树,即值为95的结点,用这个结点的值95来覆盖掉要删除的结点90的值,然后删除值为95的这个结点,如何删除呢?如果值为95的这个结点仍有有右子树呢?(注意:值为95的这个结点不可能再有左子树了,因为我们要在前面的操作中找的就是 要删除的结点的右子树的最左子树)就把值为95的结点的右子树连接到值为120的结点的left上即可。
(上面只是一种方式,是找到 要删除结点 的 右子树 的最左侧子树,同理, 也可以找到 要删除的结点 的 左子树 的最右侧子树,都相反一下即可,此处以右子树的最左侧左子树来研究)
代码实现:
设待删除结点为cur,带删除结点的双亲结点为parent
在删除中,有多种情况:
1.1情况:cur为root 且 cur.left == null 如下图,此时直接将cur的left赋值为root即可
删除后的效果:
1.2情况:cur不是root,cur.left == null,cur在左子树上,即cur 是 parent.left 如下图的效果,此时要删除的结点是值为40的结点,直接parent.left = cur.right即可
1.3 情况:cur不是root,cur.left == null cur是parent.right,即cur在右子树上,如下图情况,则直接parent.right = cur.right即可
2.1情况 cur.right == null cur是root 则直接root = cur.left即可
2.2情况:cur不是root cur是parent.left 且cur.right == null 则parent .left = cur.left
2.3 cur不是root cur是parent.right 且cur.right == null 则parent.right = cur.left
即使用前面所述的替代法。
代码实现如下:
下面的代码实现是前面的删除逻辑
替代法因为要向下找到要删除结点 的 右边子树 的 最左边子树,所以还需要一个寻找的逻辑,定义一个targetParent和target来寻找找最左子树
当结束上面的while循环时候,即target指向的是最左子树结点,然后cur.val = target.val进行覆盖
如下图所示:要删除的结点是cur,t是要删除节点 的 右边结点 的 最左侧结点,将cur的值覆盖为95,然后将 t 结点删掉(targetParent.left = target.right)(注意:删除的时候不要直接targetParent.left = null 因为target可能有右节点 也可能 没有右节点)
则代码如下:
但上面代码还是存在逻辑漏洞
如果是下图的情况,cur为值为95的结点,在while循环结束后,t指向的是值为120的结点,直接执行targetParent.left = target.right 就乱套了
所以还应该加一个 if 条件 即:if(taregt == targeParent.left)的时候,才能targetParent.left = target.right;否则应该是targetParent.right = target.right;
则替代法的实现代码应该是:
完整的删除代码如下:
public void remove(int val) {
TreeNode cur = root;
TreeNode parent = null;
while(cur != null) {
if(val < cur.val) {
parent = cur;
cur = cur.left;
} else if(val > cur.val) {
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(cur == parent.right) {
parent.right = cur.right;
} else {
parent.left = cur.right;
}
} else if(cur.right == null) {
if(cur == root) {
root = cur.left;
} else if(cur == parent.right) {
parent.right = cur.left;
} else {
parent.left = cur.left;
}
} else {
//替代法:
TreeNode targetParent = cur;
TreeNode target = cur.right;
while(target.left != null) {
targetParent = target;
target = target.left;
}
cur.val = target.val;
if(targetParent.left == target) {
targetParent.left = target.right;
} else {
targetParent.right = target.right;
}
}
}
1.5 性能分析:
插入和删除的操作都必须先查找,所以查找的效率可以代表二叉搜索树中各个操作的性能。
对有n个结点的二叉搜索树,若每个元素查找的概率相等,则二叉搜索树平均查找商都是结点在二叉搜索树的函数,即节点越深,则比较的次数就越多。
但对于同一个关键码集合,如果各关键码插入的次序不同,可能得到不同结构的二叉搜索树:
最好情况下:二叉搜索树为完全二叉树,平均比较次数为logN
最坏情况下:二叉搜索树退化为单分支树,其平均比较次数为:N / 2
可以及逆行改进,使得不管按照什么次序插入关键码,都可以使得二叉搜索树的性能最佳 -- 数据结构高阶时候学习!
2. 搜索
2.1 概念及场景
Map和Set是一种专门用来进行搜索的容器或者数据结构,其搜索的效率与其具体的实例化子类有关。之前常见的搜索方式有:
- 直接遍历:时间复杂度为O(N),元素如果比较多的话,效率较低。
- 二分查找:时间复杂度为O(logN),但搜索前必须要求序列是有序的
上述方式比较适合静态类型的数据进行查找,即一般不会对区间内进行插入和删除操作了,但在现实情况中的查找:1. 根据姓名查询考试成绩 2. 通讯录,即根据姓名查询联系方式 3. 不重复集合,即需要先搜索关键字是否已经在集合中,可能在查找的过程中进行一些插入和删除的操作,即动态查找,那上述两种方式就不合适了,这里介绍的Map和Set是一种适合动态查找的集合容器。
2.2 模型
一般把搜索的数据称为关键字(Key),和关键字对应的称为值(Value),将其称之为Key-value的键值对,所以模型有两种:
- 纯Key模型,比如 有一个英文词典,快速查找一个单词是否收录在该词典中
- Key-Value模型,比如,梁山好汉的绰号
而Map中存储的就是key-value键值对,Set中只存储了Key。
3. Map的使用
Map官方文档:Map (Java Platform SE 8 )
3.1 Map的说明:
Map是一个接口类,并没有继承自Collection,该类中存储的是<K,V>结构的键值对,并且K一定是唯一的,不能重复。
3.2 关于Map.Entry<K,V>的说明
Map.Entr<K,V>是Map内部实现的用来存放<key,value>键值对映射关系的内部类,该内部类中主要提供了key和value的获取,value的设置以及key的比较方式
注意:Map.Entry<K,V>并没有提供设置Key的方法
3.3 Map的常用方法说明
注意:
- Map是一个接口,不能直接实例化对象,如果要实例化对象是能实例化其实现类TreeMap或者HashMap。
- Map中存放键值对的Key是唯一的,value是可以重复的
- 在TreeMap中插入键值对时,key不能为空,否则会抛出空指针异常,但value可以为空,在HashMap中,key和value都可以为空
- Map中的Key可以全部分离出来,存储到Set中来进行访问(因为Key不能重复)
- Map中的value可以全部分离出来,存储在Collection的任何一个子集中(value可能有重复)。
- Map中键值对的Key不能直接修改,value可以修改,如果要修改key,只能先将该key删除掉,然后再来进行重新插入。
3.4 TreeMap的使用案例
getOrDefault方法的源码:
完!