目录
前言
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接口的某个具体的类 。
二叉搜索树又称二叉排序树,它要么是一棵空树,要么是具有以下性质的二叉树:
- 若他的左子树不为空,则左子树上所有的值都小于根节点的值
- 若他的右子树不为空,则右子树上所有的值都大于根节点的值
- 它的左子树和右子树也分别为二叉搜索树
如下图:
二叉搜索树上没有两个相同的值。
当然这里我们并没有实现搜素二叉树的建立,我们只是了解以下,它的思维。
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;//定义一个二叉搜索树的根结点
}
代码示例:
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.
}
}
代码示例
//插入一个数据
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的时候,就会旋转,平衡左右子树的高度。
我们定义一个待删除结点为cur,待删除结点的双亲接结点为parent,用来遍历搜索二叉树,那么以cur为参考,存在三种情况,这三种情况中还有三种情况
1、cur遍历到的结点的左子树为空(cur.left == null)
- cur是根节点(root),将cur所指结点删除之后,则root指向cur结点的右子树(root = cur.right)
- cur不是根节点,cur是parent结点的左子树(parent.left),将cur所指结点删除之后, 则parent结点的左子树,指向cur结点的右子树,即parent.left = cur.right;
- cur不是根节点,cur是parent结点的右子树(parent.right),将cur所指结点删除之后,则parent结点的右子树指向cur的右子树,即parent.right = cur.right;
2、cur遍历到的结点的右子树为空(cur.right == null)
- cur是根节点(root),将cur所指结点删除之后,则root指向cur结点的左子树(root = cur.right)
- cur不是根节点(root),cur是parent结点的右子树(parent.right)将cur所指结点删除之后,则parent结点的右子树指向cur结点的左子树。
- 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;
Map和Set是一种专门用来进行搜索的容器或者数据结构,其搜索的效率于其具体的实例化子类有关。
我们以前常见的搜索方式有:
- 直接遍历,时间复杂度为O(N),但是在元素如果比较多的情况下效率会非常的慢。
- 二分查找,时间复杂度为O(logN),但是搜索前必须要求序列是有序的。
这两种查找方式跟适合静态类型的查找,即这些数据不会再发生变动。
而我们再现实中查找比如:
- 根据名字查找图书
- 通讯录,即根据姓名查询联系方式
- 不重复集合,即需要先搜索关键字是否已经在集合中
可能在查找时进行一些插入和删除的操作,即动态查找,那上述两种方式就不太适合了,下面我们说到的Map和Set是 一种适合动态查找的集合容器。
一般把搜索的数据称为关键字(key),和关键字对应的称为值(value)。将其称为Key-value的键值对,所以模型会有两种:
纯Key模型,比如:
- 有一个字典,快速查找一个单词是否再词典中
- 快速查找某个名字在不在通讯录中
Key-Value模型,比如:
- 统计文件中每个单词出现的次数,统计结果是每个单词都有与其对应的次数<单词,出现次数>。
Map使用的就是Key-Value模型,Set使用的是Key模型。
Map是一个接口类,该类没有继承自Collection,该类中存储的是
方法 | 解释 |
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 |
返回所有 key 的不重复集合 |
Collection |
返回所有 value 的可重复集合 |
Set | 返回所有的 key-value 映射关系 |
boolean containsKey(Object key) | 判断是否包含 key |
boolean containsValue(Object value) | 判断是否包含 value |
在说这个方法之前,我们先要了解一下Map.Entry
Entry
Map.entrySet()方法返回的是一个Set
>类型的值,首先该返回值是一个集合Set,集合中的元素是Map.Entry 类型的,每个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删除掉,然后再来进行重新插入。
set与Map主要的不同有两点:Set是继承自Collection的接口类,Set中只存储了key.
方法 | 解释 |
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、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可以。