前言
本章介绍二叉搜索树的实现,Map和Set的区别,TreeMap,TreeSet,HashMap.HashSet的区别
一.二叉搜索树
1.概念
二叉搜索树(Binary Search Tree, BST)是一种常见的数据结构,它或是一棵空树,或是具有以下性质的二叉树:
- 若它的左子树不为空,则左子树上所有结点的值都小于根节点的值
- 若它的右子树不为空,则右子树上所有结点的值都大于根节点的值
- 它的左右子树也分别为二叉搜索树
下图就是一个二叉搜索树
⬇️⬇️⬇️
2.查找
因为二叉搜索树独特的特性,让我们查找元素变得很容易,按照其值与节点的值所比较,如果cur.val < val
就说明在比此节点的值大,进而推断出所查找的节点在其右子树,相反若cur.val > val
就说明比此节点的值小,进而推断出所查找的节点在其左子树,直到找到达成cur.val == val
条件为止返回true
,否则返回false
public boolean search(int val) {
TreeNode cur = root;
while (cur != null) {
if (cur.val < val) {
cur = cur.right;
} else if (cur.val > val) {
cur = cur.left;
} else {
return true;
}
}
return false;
}
3.插入
如果是空树的话,直接把新插入的节点赋给root
,否则就按照查找的逻辑,找到该节点所应该插入的位置,要是树中原本就有该值,则插入失败,返回false
public boolean insert(int val) {
TreeNode node = new TreeNode(val);
if (root == null) {
root = node;
return true;
}
TreeNode cur = root;
TreeNode parent = null;
while (cur != null) {
parent = cur;
if (cur.val < val) {
cur = cur.right;
} else if (cur.val > val) {
cur = cur.left;
} else {
return false;
}
}
if (parent.val < val) {
parent.right = node;
} else {
parent.left = node;
}
return true;
}
4.删除
删除操作相对复杂一些,因为要考虑被删除节点子树的情况,一般分为三种情况:
(1)如果被删除节点是叶子节点,直接删除它即可。
(2)如果被删除节点有一个叶子节点,那么将被删除节点替换成其子节点即可。
(3)如果被删除节点有两个子节点,那么需要使用替换法,即找到该节点的中序后继节点(即比删除节点大的最小节点),将中序后继节点的值替换到待删除结点的值,然后再删除中序后继节点。
public void remove(int val) {
TreeNode cur = root;
TreeNode parent = null;
while (cur != null) {
parent = cur;
if (cur.val < val) {
cur = cur.right;
} else if (cur.val > val) {
cur = cur.left;
} else {
//删除的逻辑
removeNode(parent, cur);
return;
}
}
}
private void removeNode(TreeNode parent, TreeNode cur) {
if (cur.left == null) {
if (cur == root) {
root = cur.right;
}
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 = root.left;
}
if (cur == parent.left) {
parent.left = cur.left;
} else if (cur == parent.right) {
parent.right = cur.left;
}
} else {
TreeNode targetParent = cur;
TreeNode target = cur.right;
//找到中序后继节点
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;
}
}
}
二.Map和Set介绍
1.概念和场景
Map和Set是一种专门用来进行搜索的容器或数据结构,也可以用于存储和操作数据集合,其搜索的效率与其具体的实例化子类有关.
静态查找:一般不会对区间进行插入和删除操作,像以前的搜索方式为:直接遍历(O(n)),二分查找(O(log2n)).
动态查找:那就与静态查找相反,在查找时会涉及到插入和删除的操作.即Map和Set所适用的查找.
2.模型
我们一般把搜索的数据称为关键字(Key),和关键字对应的称为值(Value),将其称之为Key-value的键值对,所以对应的模型会有两种:
-
纯Key模型,比如:
快速查找一个单词是否在词典中
-
Key-Value模型,比如:
统计文件中的每个单词出现的次数,统计结果为每个单词与之对应的次数:<单词,出现的次数>
Map中储存的是Key-Value的键值对模型,Set中储存的是纯Key模型
三.Map的使用
1.Map.Entry<K,V>的说明
Map.Entry<K,V>是Map内部用来存放<key,value>键值对映射关系的内部类,该内部类主要提供了<key,value>的获取,value的设置及key的比较方法.
注意:Map.Entry<K,V>并没有提供设置Key的方法
2.Map的说明
- Map是一个接口,不能直接实例化对象,如果要实例化对象只能实例化其实现类TreeMap或者HashMap
- Map中存放的键值对Key是唯一的,value是可以重复的
- 在TreeMap中插入键值对时,key不能为空,否则就会抛NullPointerException异常,value可以为空.但是HashMap的key和value都可以为空
- Map中的Key可以全部分离出来,存储到Set中来进行访问(Key不能重复)
- Map中的value可以全部分离出来,储存在Collection的任何一个子类集合中(Value可以有重复)
- Map中键值对的Key不能直接修改,value可以修改,如果要修改Key,只能将key先删除掉,然后再重新插入
Map底层结构 | TreeMap | HashMap |
---|---|---|
底层结构 | 红黑树 | 哈希桶 |
插入/删除/查找时间复杂度 | O(log2n) | O(1) |
是否有序 | 关于Key有序 | 无序 |
线程安全 | 不安全 | 不安全 |
插入/删除查找区别 | 需要进行元素比较 | 通过哈希函数计算哈希地址 |
比较与覆写 | key必须能够比较,否则会抛出ClassCastException异常 | 自定义类型需要覆写equls和hashCode方法 |
应用场景 | 需要key有序场景下 | key是否有序不关心,需要更高的时间性能 |
3.Set的说明
- Set是继承自Collection的一个接口类
- Set中只存储了key,并且要求key唯一
- TreeSet的底层是使用Map来实现的,其实用key与Object的一个默认对象作为键值对插入到Map中的
- Set会对集合中的元素进行去重
- 实现Set接口的常用类有TreeSet和HashSet,还有一个LinkedHashSet,LinkedHashSet是在HashSet的基础上维护了一个双向链表来记录元素的插入次序
- Set中的Key同样不能修改,如果要修改,现将原来的删除掉,然后再重新插入
- TreeSet在插入时,key不能为空,HashSet可以
Set底层结构 | TreeSet | HashSet |
---|---|---|
底层结构 | 红黑树 | 哈希桶 |
插入/删除/查找时间复杂度 | O(log2n) | O(1) |
是否有序 | 关于Key有序 | 不一定有序 |
线程安全 | 不安全 | 不安全 |
插入/删除查找区别 | 按照红黑树的特性来进行插入和删除 | 通过哈希函数计算哈希地址 |
比较与覆写 | key必须能够比较,否则会抛出ClassCastException异常 | 自定义类型需要覆写equls和hashCode方法 |
应用场景 | 需要key有序场景下 | key是否有序不关心,需要更高的时间性能 |