目录
导言:
二叉搜索树又称二叉排序树,它非常适合用来实现搜索和插入操作。类似于二分查找一样,在搜索操作中,可以根据节点的值和目标值的大小关系,选择左子树或右子树进行进一步搜索。本文主要对二叉搜索树的操作和其对应的TreeSet与TreeMap集合做一个介绍。
正文:
一.搜索树
1.概念
二叉搜索树又称二叉排序树,它或者是一棵空树,或者是具有以下性质的二叉树:
- 若它的左子树不为空,则左子树上所有节点的值都小于根节点的值
- 若它的右子树不为空,则右子树上所有节点的值都大于根节点的值
- 它的左右子树也分别为二叉搜索树
对于二叉排序树进行中序遍历,可以得到一个升序的有序序列。想要实现这棵树,需要先给出节点的定义。
代码如下:
class TreeNode {
int val;
TreeNode left;
TreeNode right;
public TreeNode(int val) {
this.val = val;
this.left = null;
this.right = null;
}
}
2.查找
二叉排序树的查找操作是通过比较节点的值和目标值的大小关系来进行的。有点类似二分查找,每次都能缩小一半的范围,效率比较高。
具体步骤如下:
- 从根节点开始,将目标值与当前节点的值进行比较。
- 如果目标值等于当前节点的值,则找到了目标节点,返回该节点。
- 如果目标值小于当前节点的值,则在左子树中继续查找。
- 如果目标值大于当前节点的值,则在右子树中继续查找。
- 重复上述步骤,直到找到目标节点或者遍历到叶子节点为止。
代码如下:
class BinarySearchTree {
private TreeNode root;
public TreeNode search(int target) {
TreeNode current = root;
while (current != null) {
if (target == current.val) {
return current;
} else if (target < current.val) {
current = current.left;
} else {
current = current.right;
}
}
return null;
}
}
二叉排序树的查找操作的平均时间复杂度为O(log n),其中n为树中节点的个数。这是因为在一个平衡的二叉排序树中,每次查找都可以将搜索范围减少一半,因此查找的时间复杂度是对数级别的。然而,如果二叉排序树不平衡,例如退化成链表的情况,最坏情况下查找的时间复杂度可能会达到O(n),即线性级别。因此,为了保持二叉排序树的平衡,人们提出了平衡二叉排序树(如AVL树、红黑树等),通过旋转和调整节点来保持树的平衡,从而保证查找操作的时间复杂度始终在O(log n)的水平上,有兴趣的可以自行研究。
3.插入
二叉排序树的插入操作是将一个新的节点插入到树中,保持树的有序性。具体步骤如下:
- 从根节点开始,将要插入的节点与当前节点的值进行比较。
- 如果要插入的节点值小于当前节点的值,并且当前节点的左子节点为空,则将新节点插入为当前节点的左子节点。
- 如果要插入的节点值大于当前节点的值,并且当前节点的右子节点为空,则将新节点插入为当前节点的右子节点。
- 如果要插入的节点值小于当前节点的值,但是当前节点的左子节点不为空,则继续在左子树中递归插入。
- 如果要插入的节点值大于当前节点的值,但是当前节点的右子节点不为空,则继续在右子树中递归插入。
代码如下:
class BinarySearchTree {
private TreeNode root;
public void insert(int val) {
root = insertNode(root, val);
}
private TreeNode insertNode(TreeNode root, int val) {
if (root == null) {
return new TreeNode(val);
}
if (val < root.val) {
root.left = insertNode(root.left, val);
} else if (val > root.val) {
root.right = insertNode(root.right, val);
}
return root;
}
}
二叉排序树的插入操作的平均时间复杂度为O(log n),最坏情况下查找的时间复杂度可能会达到O(n),分析过程与二叉排序树的查找一致。
4.删除
二叉排序树的删除情况比较复杂,要分多种情况讨论。思路如下:
- 如果要删除的节点是叶子节点(没有子节点),则直接删除该节点。
- 如果要删除的节点只有一个子节点,则将其子节点替换该节点的位置。
- 如果要删除的节点有两个子节点,则需要找到其右子树中的最小节点(或者左子树中的最大节点)来替换该节点,并删除最小节点(或最大节点)。
具体操作如下:
设待删除结点为 cur, 待删除结点的双亲结点为 parent
1. 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
2. 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
3.cur.left != null && cur.right != null
1. 需要使用替换法进行删除,即在它的右子树中寻找中序下的第一个结点(关键码最小),用它的值填补到被删除节点中,再来处理该结点的删除问题
代码如下:
public boolean remove(int key) {
TreeNode cur = root;
TreeNode parent = null;
while (cur != null) {
if(cur.key < key) {
parent = cur;
cur = cur.right;
}else if(cur.key > key) {
parent = cur;
cur = cur.left;
}else {
removeNode(parent,cur);
return true;
}
}
return false;
}
public void removeNode(TreeNode parent,TreeNode cur){
if(cur.left == null) {
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) {
if(cur == root) {
root = cur.left;
}else if(cur == parent.left) {
parent.left = cur.left;
}else {
parent.right = cur.left;
}
}else {
TreeNode t = cur.right;
TreeNode tp = cur;
while (t.left != null) {
tp = t;
t = t.left;
}
cur.key = t.key;
if(tp.left == t) {
tp.left = t.right;
}else {
tp.right = t.right;
}
}
}
与上面情况一致,在平均情况下,删除操作的时间复杂度也是 O(log n),最坏情况下查找的时间复杂度可能会达到O(n)。
二.Map与Set
1.Set
1.概念
Set是一种集合,它用于存储一组唯一的、无序的元素。Set接口继承自Collection接口,因此它具有Collection接口中定义的大部分方法,同时也提供了一些特有的方法。
Set集合的特点包括:
- 不允许重复元素:Set中不能包含重复的元素,即同一个元素只能出现一次。
- 无序性:Set中的元素没有特定的顺序,即元素的存储顺序和添加顺序无关。
- 提供了添加、删除、查找等操作:Set接口提供了添加元素、删除元素、查找元素等操作的方法。
2.常见方法
方法 | 解释 |
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中,可以达到去重的效果 |
3.Set的实现类
Java中常用的Set实现类包括HashSet、LinkedHashSet和TreeSet:
- HashSet:基于哈希表实现,具有良好的查找性能,不保证元素的顺序。
- LinkedHashSet:继承自HashSet,内部使用双向链表维护元素的顺序,可以保持元素的插入顺序。
- TreeSet:基于红黑树实现,可以对元素进行排序,保证元素的有序性。
下面进行简单的使用:
public class test {
public static void main(String[] args) {
// 创建一个HashSet对象
Set<String> hashSet = new HashSet<>();
// 添加元素
hashSet.add("apple");
hashSet.add("banana");
hashSet.add("orange");
// 遍历HashSet
for (String fruit : hashSet) {
System.out.println(fruit);
}
// 创建一个LinkedHashSet对象
Set<String> linkedHashSet = new LinkedHashSet<>();
// 添加元素
linkedHashSet.add("apple");
linkedHashSet.add("banana");
linkedHashSet.add("orange");
// 遍历LinkedHashSet
for (String fruit : linkedHashSet) {
System.out.println(fruit);
}
Set<String> treeSet = new TreeSet<>();
// 添加元素
treeSet.add("apple");
treeSet.add("banana");
treeSet.add("orange");
// 遍历TreeSet
for (String fruit : treeSet) {
System.out.println(fruit);
}
}
}
注意:
- Set是继承自Collection的一个接口类
- Set中只存储了key,并且要求key一定要唯一
- TreeSet的底层是使用Map来实现的,其使用key与Object的一个默认对象作为键值对插入到Map中的
- Set最大的功能就是对集合中的元素进行去重
- 实现Set接口的常用类有TreeSet和HashSet,还有一个LinkedHashSet,LinkedHashSet是在HashSet的基础上维护了一个双向链表来记录元素的插入次序。
- Set中的Key不能修改,如果要修改,先将原来的删除掉,然后再重新插入
- TreeSet中不能插入null的key,HashSet可以。
2.Map
1.概念
Map是一种键值对映射的集合,它存储了一组键值对(key-value pairs),并且允许通过键来查找对应的值。Map接口定义了一系列方法来操作键值对,包括添加、删除、查找等操作。
Map集合的特点包括:
- 键值对映射:Map中的元素是以键值对的形式存储的,每个键对应一个值。
- 不允许重复的键:Map中的键是唯一的,不允许重复的键,但值可以重复。
- 键和值可以为null:Map中的键和值都可以为null。
- 提供了添加、删除、查找等操作:Map接口提供了添加键值对、删除键值对、根据键查找值等操作的方法。
2.常用方法
关于Map.Entry<K, V>的说明:
Map.Entry<K, V> 是Map内部实现的用来存放<key, value>键值对映射关系的内部类,该内部类中主要提供了<key, value>的获取,value的设置以及Key的比较方式。
方法 | 解释 |
K getKey() | 返回 entry 中的 key |
V getValue() | 返回 entry 中的 value |
V setValue(V value) | 将键值对中的value替换为指定value |
常用的方法:
方法 | 解释 |
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<K> keySet() | 返回所有 key 的不重复集合 |
Collection<V> values() | 返回所有 value 的可重复集合 |
Set<Map.Entry<K, V>> entrySet() | 返回所有的 key-value 映射关系 |
boolean containsKey(Object key) | 判断是否包含 key |
boolean containsValue(Object value) | 判断是否包含 value |
3.Map的实现类
Map接口的常用实现类包括HashMap、LinkedHashMap、TreeMap和Hashtable。
HashMap:
- HashMap是最常用的Map实现类之一,它基于哈希表实现,具有快速的查找和插入性能。
- HashMap不保证元素的顺序,输出结果是无序的。
- 允许使用null作为键和值。
- 在多线程环境下不是线程安全的,如果需要在多线程环境下使用,可以通过Collections.synchronizedMap方法创建一个同步的HashMap。
LinkedHashMap:
- LinkedHashMap继承自HashMap,并且通过双向链表维护插入顺序或者访问顺序。
- 保持了元素的插入顺序或者访问顺序,输出结果与插入或访问顺序有关。
- 允许使用null作为键和值。
- 在多线程环境下不是线程安全的,可以通过Collections.synchronizedMap方法创建一个同步的LinkedHashMap。
TreeMap:
- TreeMap是基于红黑树实现的,可以对键进行排序。
- TreeMap会根据键的自然顺序或者自定义的Comparator对键进行排序,输出结果是有序的。
- 不允许使用null作为键,但允许使用null作为值。
- 在多线程环境下不是线程安全的,可以通过Collections.synchronizedMap方法创建一个同步的TreeMap。
Hashtable:
- Hashtable是较早的Map实现类,它是线程安全的,所有的方法都是同步的。
- 不允许使用null作为键或值。
- 在多线程环境下可以安全地使用,但在单线程环境下性能通常不如HashMap。
简单的使用代码如下:
public class test {
public static void main(String[] args) {
// 创建一个HashMap对象
Map<String, Integer> hashMap = new HashMap<>();
// 添加键值对
hashMap.put("apple", 10);
hashMap.put("banana", 20);
hashMap.put("orange", 15);
// 遍历HashMap
for (Map.Entry<String, Integer> entry : hashMap.entrySet()) {
System.out.println("Key: " + entry.getKey() + ", Value: " + entry.getValue());
}
// 创建一个LinkedHashMap对象
Map<String, Integer> linkedHashMap = new LinkedHashMap<>();
// 添加键值对
linkedHashMap.put("apple", 10);
linkedHashMap.put("banana", 20);
linkedHashMap.put("orange", 15);
// 遍历LinkedHashMap
for (Map.Entry<String, Integer> entry : linkedHashMap.entrySet()) {
System.out.println("Key: " + entry.getKey() + ", Value: " + entry.getValue());
}
// 创建一个TreeMap对象
Map<String, Integer> treeMap = new TreeMap<>();
// 添加键值对
treeMap.put("apple", 10);
treeMap.put("banana", 20);
treeMap.put("orange", 15);
// 遍历TreeMap
for (Map.Entry<String, Integer> entry : treeMap.entrySet()) {
System.out.println("Key: " + entry.getKey() + ", Value: " + entry.getValue());
}
// 创建一个Hashtable对象
Map<String, Integer> hashtable = new Hashtable<>();
// 添加键值对
hashtable.put("apple", 10);
hashtable.put("banana", 20);
hashtable.put("orange", 15);
// 遍历Hashtable
for (Map.Entry<String, Integer> entry : hashtable.entrySet()) {
System.out.println("Key: " + entry.getKey() + ", Value: " + entry.getValue());
}
}
}
注意:
- 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实现类。如果需要快速的查找和插入,并且不需要保持顺序,可以使用HashMap;如果需要保持插入顺序或访问顺序,可以使用LinkedHashMap;如果需要对键进行排序,可以使用TreeMap;如果需要在多线程环境下使用,并且不需要对键进行排序,可以使用Hashtable。
总结:
Set和Map在底层实现上都使用了类似的数据结构,如哈希表和红黑树,但是在存储内容和数据结构的使用上有一些区别。 Set主要关注元素的唯一性,而Map需要同时存储键值对,并且需要保证键的唯一性。总的来说,Set集合用于存储不重复的元素,而Map集合用于存储键值对。它们在Java集合框架中都扮演着重要的角色,可以根据具体的需求选择合适的实现类来使用。