前言
在了解TreeSet和TreeMap之前,先让我们介绍一下搜索树的概念。
1. 搜索树
二叉搜索树又称二叉排序树,这颗树要么是一棵空树,要么是具有以下性质的二叉树:
- 若它的左子树不为空,则左子树上所有节点的值都小于根节点的值
- 若它的右子树不为空,则右子树上所有节点的值都大于根节点的值
- 它的左右子树也分别为二叉搜索树
那么对于一颗二叉排序树应该具有的基本操作是什么呢?我们知道作为一颗二叉排序树一定满足的条件是左子树上的所有结点都小于根节点,右子树上的所有结点都大于根节点。因此对于一颗二叉搜索树而言,我们应该存在查找、插入、删除的操作。
1.1 查找
二叉排序树根节点的左子树都小于根节点,根节点的右子树都大于右子树,基于这个特点,我们可以按照以下规律查找:
从根节点开始比较,当待查找的结点大于根节点时,我们就到右子树中去找;当待查找的结点小于根节点是,我们就到左子树中去找;当待查找的结点等于根节点时,此时直接返回根节点就可;当查找完整棵树发现该节点不存在时,返回null
//二叉排序树查找操作的实现:
public TreeNode search(int key) {
if(isEmpty()) {
throw new EmptyException("查找树为空,无法查找");
}
TreeNode cur = this.root;
while(cur != null) {
if(cur.val > key) {
cur = cur.left;
}else if(cur.val < key) {
cur = cur.right;
}else {
return cur;
}
}
return null;
}
1.2 插入
二叉排序树中不能插入相同的元素,当元素已存在时插入失败,也就说二叉排序树中所有数据都是只有一份的。对于插入操作,我们会根据二叉排序树的性质遍历整棵树找到适合的位置进行插入,使得插入结束后仍然是一颗搜索树。
public boolean insert(int key) {
TreeNode node = new TreeNode(key);
if(isEmpty()) {
this.root = node;
return true; }
TreeNode cur = this.root;
TreeNode parent = null;
while(cur != null) {
parent = cur;
if(cur.val > key) {
cur = cur.left;
}else if(cur.val < key) {
cur = cur.right;
}else {
System.out.println("插入失败,key值已存在");
return false; }
}
//找到可插入节点的父节点,判断往结点那边插入
if(parent.val > key) {
parent.left = node;
}else {
parent.right = node;
}
return true;
}
再插入过程中需要注意保存可插入结点的父节点,同时注意判断往可插入节点的父节点的那边进行插入。
1.3 删除(较难)
设待删除结点为cur
,待删除结点的父节点为parent
;对于删除结点在树中可能存在以下几种不同的情况:
cur.left == null
,待删除结点的左子树为空
cur == root
,待删除结点为根结点;则root = cur.right
,根结点等于删除结点的右子树结点。cur != root ; cur = parent.left
,待删除结点是父节点的左子树结点;则parent.left = cur.right
,待删除结点的父节点的左子树等于删除结点的右子树结点。cur != root ; cur = parent.right
,待删除结点是父节点的右子树结点;则parent.right = cur.right
,待删除结点的父节点的右子树等于删除结点的右子树结点。
cur.right == null
,待删除结点的右子树为空
cur == root
,待删除结点为根结点;则root = cur.left
,根结点等于删除结点的左子树结点。cur != root ; cur = parent.left
,待删除结点是父节点的左子树结点;则parent.left = cur.left
,待删除结点的父节点的左子树等于删除结点的左子树结点。cur != root ; cur = parent.right
,待删除结点是父节点的右子树结点;则parent.right = cur.left
,待删除结点的父节点的右子树等于删除结点的左子树结点。
cur.left != null && cur.right != null
,待删除结点的左右子树都不为空对于左右子树都不为空的情况下,我们可以使用替换删除法,即寻找一个合适的值来替代掉原来的数,让后将这个“合适的数”原来所在的位置删除掉。
#合适的数: 待删除结点的左子树中的最大值(左子树中的最右边结点)或者待删除结点的右子树中的最小值(右子树中的最左边结点)。
public boolean remove(int key) {
if(isEmpty()) {
throw new EmptyException("数为空,无法删除");
}
//找到要删除结点,同时保存他的父节点
TreeNode cur = this.root;
TreeNode parent = null;
while(cur != null) {
parent = cur ;
if(cur.val > key) {
cur = cur.left;
}else if(cur.val < key) {
cur = cur.right;
}else {
//找到删除节点了,删除
removeNode(parent,cur);
return true; }
}
System.out.println("删除节点不存在");
return false;
}
//删除结点存在的情况
//1. 左边为空
//2. 右边为空
//3. 左右为空
//4. 左右不为空
private void removeNode(TreeNode parent, TreeNode cur) {
//删除结点可能是根节点,此时parent结点==null,且记得修改根节点
if(cur.left == null) {
//同时包括1、3
if(cur == root) {
root = cur.right;
}else if(cur == parent.left) {
parent.left = cur.right;
}else {
parent.right = cur.right;
}
}else if(cur.right == null) {
//处理2
if(cur == root) {
root = cur.left;
}else if(cur == parent.left) {
parent.left = cur.left;
}else {
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(targetParent.left == target) {
targetParent.left = target.right;
}else {
targetParent.right = target.right;
}
}
}
这里的替换删除中的最合适的数为右子树中的最小值,targerParent
结点保存的是合适结点的父节点,需要特别注意的是:在右子树中寻找最小值时,最小值可能是右子树的根节点,所以我们应该判断最小值是不是根节点。
1.4 搜索树的模拟实现
/**
* 二叉搜索树
* 时间复杂度:
* 好的:O(LogN)
* 坏的:O(N),单分支
*/
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;
//插入得保存父节点
public boolean insert(int key) {
TreeNode node = new TreeNode(key);
if(isEmpty()) {
this.root = node;
return true; }
TreeNode cur = this.root;
TreeNode parent = null;
while(cur != null) {
parent = cur;
if(cur.val > key) {
cur = cur.left;
}else if(cur.val < key) {
cur = cur.right;
}else {
System.out.println("插入失败,key值已存在");
return false; }
}
//找到可插入节点的父节点,判断往结点那边插入
if(parent.val > key) {
parent.left = node;
}else {
parent.right = node;
}
return true;
}
/**
* 搜索元素
* 时间复杂度:好的情况下:O(LogN)
* 坏的情况下:O(N)
* @param key
* @return
*/
public TreeNode search(int key) {
if(isEmpty()) {
throw new EmptyException("查找树为空,无法查找");
}
TreeNode cur = this.root;
while(cur != null) {
if(cur.val > key) {
cur = cur.left;
}else if(cur.val < key) {
cur = cur.right;
}else {
return cur;
}
}
return null;
}
public boolean isEmpty() {
return this.root == null;
}
public boolean remove(int key) {
if(isEmpty()) {
throw new EmptyException("数为空,无法删除");
}
//找到要删除结点,同时保存他的父节点
TreeNode cur = this.root;
TreeNode parent = null;
while(cur != null) {
parent = cur ;
if(cur.val > key) {
cur = cur.left;
}else if(cur.val < key) {
cur = cur.right;
}else {
//找到删除节点了,删除
removeNode(parent,cur);
return true; }
}
System.out.println("删除节点不存在");
return false;
}
//删除结点存在的情况
//1. 左边为空
//2. 右边为空
//3. 左右为空
//4. 左右不为空
private void removeNode(TreeNode parent, TreeNode cur) {
//删除结点可能是根节点,此时parent结点==null,且记得修改根节点
if(cur.left == null) {
//同时包括1、3
if(cur == root) {
root = cur.right;
}else if(cur == parent.left) {
parent.left = cur.right;
}else {
parent.right = cur.right;
}
}else if(cur.right == null) {
//处理2
if(cur == root) {
root = cur.left;
}else if(cur == parent.left) {
parent.left = cur.left;
}else {
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(targetParent.left == target) {
targetParent.left = target.right;
}else {
targetParent.right = target.right;
}
}
}
}
我们知道在数据的插入和删除过程中,二叉搜索树最优的情况下能够化为一颗完全二叉树,其平局比较次数为LogN
;在最坏的情况下二叉搜索树会退化为一颗单分支的数,其平均比较此事为N/2
2. TreeSet
和TreeMap
TreeMap 和 TreeSet 中利用搜索树实现的 Map 和 Set;实际上它们底层中用的数据结构是红黑树,而红黑树是一棵近似平衡的二叉搜索树,即在二叉搜索树的基础之上 + 颜色以及红黑树性质验证。
#模型:一般把搜索的数据称为关键字(Key),和关键字对应的称为值(Value),将其称之为Key-value的键值对,所以模型通常会有两种
模型会有两种:
- 纯Key模型,如:
- 快速查找一个单词是否在词典中
- 快速查找某个名字在不在通讯录中
- Key-Value模型,如:
- 统计文件中每个单词出现的次数,统计结果是每个单词都有与其对应的次数:<单词,单词出现的次数>
- 梁山好汉的江湖绰号:每个好汉都有自己的江湖绰号
Map中存储的就是key-value的键值对,Set中只存储了Key。
对于TreeMap和TreeSet,我们将从以下方面进行切入介绍:相同点、不同点、还有常用方法。
2.1 异同点
相同点:
- TreeMap和TreeSet底层使用了红黑树,也就是说它们都是有序的集合,他们存储的值都是拍好序的。
- 运行速度都要比Hash集合慢,他们内部对元素的操作时间复杂度为O(logN),而HashMap/HashSet则为O(1)
- 由于 TreeMap和TreeSet底层使用了红黑树,所以当我们实现元素插入的时候,插入的一定是个能够比较的大小的数据(实现Comparable接口或者提供比较器)
不同点:- 最主要的区别就是TreeSet和TreeMap非别实现Set和Map接口
- TreeSet只存储一个对象(Key),而TreeMap存储两个对象(Key和Value),对于TreeMap中仅仅Key对象有序
- TreeSet中不存在重复集合(天然去重的集合),即Key的值不会重复;而TreeMap中却拥有可以重复的Value值,存放键值对的Key是唯一的,value是可以重复的
2.2 TreeMap的常见方法
方法 | 介绍 |
---|---|
V get(Object key) | 返回 key 对应的 value |
V put(K key, V value) | 将Key和Value作为键值对,添加到Map中 |
boolean containsKey(Object key) | 如果Map中存在key则返回true |
V getOrDefault(Object key, V defaultValue) | 如果Map中有关于key对应的value 则返回Map中的value, 否则返回默认设置的value |
Set<Map.Entry<K,V>> entrySet() | 返回一个set集合,里面的元素是Map.Entry<K, V> |
#Map.Entry<K,V>
:Map.Entry<K, V> 是Map内部实现的用来存放<key, value>键值对映射关系的内部类,该内部类中主要提供了<key, value>的获取,value的设置以及Key的比较方式。
方法 | 介绍 |
---|---|
K getKey() | 获得entry 中的 key |
V getValue() | 获得entry 中的 value |
import java.util.Map;
import java.util.Set;
import java.util.TreeMap;
public class Test {
public static void main(String[] args) {
Map<String, Integer> treeMap = new TreeMap<>();
//put(Key,Value)存放键值对Key-Value
//Key在这里是String类型的name
//Value在这里是Integer类型的age
System.out.println("-----put(Key,Value)....");
treeMap.put("张三", 18);
treeMap.put("李四", 22);
treeMap.put("王五", 21);
//get(Key)获取Key"王五"对应的Value年龄
System.out.println("-----get(Key)....");
Integer age = treeMap.get("王五");
System.out.println(age);
//查看treeMap里是否有"李四"
System.out.println("-----containsKey(Object key)....");
boolean ret = treeMap.containsKey("李四");
System.out.println(ret);
//getOrDefault(Object key, V defaultValue)
System.out.println("-----getOrDefault(Object key , V defaultValue)....");
System.out.println(treeMap.getOrDefault("张三", 20));
System.out.println(treeMap.getOrDefault("钱七", 20));
//将treeMap转化为一个Set集合
//获取Entry对象,然后获取key和value
System.out.println("-----Set<Map.Entry<K,V>> entrySet()....");
for (Map.Entry<String, Integer> entry : treeMap.entrySet()) {
System.out.println("Key == " + entry.getKey() + " Value == " + entry.getValue());
}
}
}
运行结果:
2.3 TreeSet的常用方法
方法 | 解释 |
---|---|
boolean add(E e) | 添加集合中不存在元素,重复元素不会被添加成功 |
boolean contains(Object o) | 判断 o 是否在集合中 |
Iterator< E > iterator() | 返回迭代器对象 |
boolean remove(Object o) | 删除集合中的 o |
boolean isEmpty() | 检测set是否为空,空返回true,否则返回false |
void clear() | 清空集合 |
int size() | 返回set中元素的个数 |
import java.util.*;
public class Test {
public static void main(String[] args) {
Set<String> treeSet = new TreeSet<>();
//add(Key)添加元素
System.out.println("-----add(Key).....");
treeSet.add("张三");
treeSet.add("李四");
treeSet.add("王五");
treeSet.add("钱七");
//remove(Key)删除元素 "张三"
System.out.println("-----remove(Key).....");
treeSet.remove("张三");
//是否包含元素 "钱七"
System.out.println("-----contains(Key).....");
boolean isContain = treeSet.contains("钱七");
System.out.println("是否包含元素钱七: " + isContain);
//集合是否为null
System.out.println("-----isEmpty().....");
boolean empty = treeSet.isEmpty();
System.out.println("集合是否为空: " + empty);
//得到集合元素个数
System.out.println("-----size().....");
int size = treeSet.size();
System.out.println("元素个数: " + size);
//迭代器遍历得到Set集合内容
System.out.println("-----iterator().....");
System.out.print("集合里面的元素:");
Iterator<String> iterator = treeSet.iterator();
while (iterator.hasNext()) {
System.out.print(iterator.next() + " ");
}
System.out.println();
//删除所有元素
System.out.println("-----clear().....");
treeSet.clear();
}
}
执行结果如下:
前面我们提到Set是天然去重的,也就是说我们可以利用Set来完成我们的去重操作,例如:当一个序列中数据为:{1 , 1 , 1, 23 ,77 , 2 , 92 ,77},对于一个TreeSet集合,把每个元素添加到Set集合就可以完成去重操作。
import java.util.*;
public class Test {
public static void main(String[] args) {
int arr[] = {1 , 1 , 1, 23 ,77 , 2 , 92 ,77};
Set<Integer> treeSet = new TreeSet<>();
//去重前打印
System.out.println("去重前:");
for (int val : arr) {
System.out.print(val + " ");
}
System.out.println();
//去重
for (int val : arr) {
treeSet.add(val);
}
//去重后, 打印
System.out.println("去重后:");
for (int val : treeSet) {
System.out.print(val + " ");
}
System.out.println();
}
}
运行结果如下:
2.4 总结
- Set是继承自Collection的一个接口类
- Set中只存储了key,并且要求key一定要唯一
- TreeSet的底层是使用Map来实现的,其使用key与Object的一个默认对象作为键值对插入到Map中的
- Map中存放键值对的Key是唯一的,value是可以重复的
- TreeMap和TreeSet的底层数据结构是红黑树,所以插入的元素都必须是能够比较大小的