1. 搜索树
TreeMap和TreeSet底层是用一颗搜索树实现的,不过这颗树是一颗红黑树,树的节点有黑色也有红色,我们现在只需要先了解一下什么是搜索树。
二叉搜索树:又称二叉排序树
- 若它的左子树不为空,则左子树所有节点的值都小于根节点值。
- 若它的右子树不为空,则右子树所有节点的值都大于根节点值。
- 左右子树也是一颗二叉搜索树。
查找:
- 若根节点不为空:
-
若根节点key == 查找key 返回true;
-
若根节点key > 查找key 在其左子树查找;
-
若根节点key < 查找key 在其右子树查找;
-
否则返回 false。
public Node search(int key) { Node cur = root; while (cur != null) { //1.若根节点key == 查找key,返回 if (key == cur.key) { return cur; //2.若根的key大于查找key去左子树 } else if (cur.key > key) { cur = cur.left; //3.若根的key大小于查找key去右子树 } else { cur = cur.right; } } //4.找不到返回null return null; }
插入:
分情况: 每次插入都是从根节点往下遍历
- 若该树为空树,即:根(root) == null;直接插入元素。
- 若树不为空树,按照查找逻辑,找到合适位置,插入新节点。
- 如:插入10,此时根节点是5,10比5大,往5的右子树遍历;
- 此时根节点是8,10比8大,往8的右子树遍历;
- 此时根节点是12,10比12小,往12的左子树遍历。
- 此时根节点为null,直接插入。
public boolean insert(int key) {
//1.若根节点是null,直接插入
if (root == null) {
root = new Node(key);
return true;
}
Node cur = root;
Node parent = null;
//2.遍历找到待插入key的合适位置
//待插入key,插入的位置肯定是一个根节点的叶子节点
//当cur为null,parent就是待插入key的根节点
while (cur != null) {
if (key == cur.key) {
return false;
} else if (key < cur.key) {
parent = cur;
cur = cur.left;
} else {
parent = cur;
cur = cur.right;
}
}
//3.判断待插入key比当前节点key的大小关系
//待插入key比根key大,就插入根节点右子树
//否则插入根节点左子树
Node node = new Node(key);
if (key < parent.key) {
parent.left = node;
} else {
parent.right = node;
}
return true;
}
删除:
分情况: 假设待删除节点为cur,待删除节点的父亲节点为parent。
-
cur.left == null;
-
cur 不是根节点(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
需要使用一种替罪羊法来进行删除,即在当前要删除的节点的左子树的最右边找叶子节点(找到9),或者当前删除节点的右子树的最左边找叶子节点(找到16).
-
假设要删除的节点是15,把15和9交换,或者把15和16交换。
-
9:一定是当前删除节点左树中的最大值。(既然是最大值,9肯定是没有右子树了)
-
16:一定是当前删除节点右树中的最小值。(既然是最小值,16肯定是没有左子树了)
-
**注意:**要判断待删除节点的左子树的有无右子树/(右子树有无左子树)。
-
此时问题就转换为如何删除9或者16:
-
cur.key = t.key;
-
tp.left = t.rigth;
-
-
public boolean remove(int key) {
Node cur = root;
Node parent = null;
//1.遍历找到要删除key的位置
while (cur != null) {
if (cur.key == key) {
removeHelp(parent, cur);
return true;
} else if (cur.key > key) {
parent = cur;
cur = cur.left;
} else if (cur.key < key) {
parent = cur;
cur = cur.right;
}
}
return false;
}
private void removeHelp(Node parent,Node cur){
if(cur.left == null){
if(cur == root){
root = cur.right;
} else 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 = cur.left;
} else if (cur == parent.left) {
parent.left = cur.left;
}else if(cur == parent.right){
parent.right = cur.left;
}
}else {
//1.从待删除节点的右子树的最左边找替罪羊
Node target = cur.right;//替罪羊节点
Node targetParent = cur;
while(target != null){
targetParent = target;
target = target.left;
}
//2.交换值
cur.key = target.key;
//3.判断,
//待删除节点的右树有没有左子树
if(target == targetParent.left){
targetParent.left = target.right;
}else {
targetParent.right = target.right;
}
}
结论:
-
插入和删除操作都必须先查找,即查找就代表了二叉搜索树的各个操作的性能。
-
对有N个节点的二叉搜索树,若每个元素查找的概率相等(满二叉树),则平均查找长度是节点在二叉搜索树的深度的函数(logn),即节点越深,比较次数越多。
-
对于同一组数据,如果插入的次序不同,可能得到不同结构的二叉搜索树。
-
最优情况:二叉搜索树为完全二叉树,平均比较次数为O(logN)。
-
最差情况:二叉搜索树退化为单支树,其平均比较次数为O(N)。
-
TreeMap 和 TreeSet 即 java 中利用搜索树实现的 Map 和 Set;实际上用的是红黑树,而红黑树是一棵近似平衡的 二叉搜索树,即在二叉搜索树的基础之上 + 颜色以及红黑树性质验证,关于红黑树的内容这里不做讲解。
简单实现二叉搜索树:
-
public class BinarySearchTree {
public static class Node {
int key;
Node left;
Node right;
public Node(int key) {
this.key = key;
}
}
private Node root = null;
public Node search(int key) {
Node cur = root;
while (cur != null) {
//1.若根节点,返回
if (key == cur.key) {
return cur;
//2.若根的key大于查找key去左子树
} else if (cur.key > key) {
cur = cur.left;
//3.若根的key大小于查找key去右子树
} else {
cur = cur.right;
}
}
//4.找不到返回null
return null;
}
public boolean insert(int key) {
//1.若根节点是null,直接插入
if (root == null) {
root = new Node(key);
return true;
}
Node cur = root;
Node parent = null;
//2.遍历找到待插入key的合适位置
//待插入key,插入的位置肯定是一个根节点的叶子节点
//当cur为null,parent就是待插入key的根节点
while (cur != null) {
if (key == cur.key) {
return false;
} else if (key < cur.key) {
parent = cur;
cur = cur.left;
} else {
parent = cur;
cur = cur.right;
}
}
//3.判断待插入key比当前节点key的大小关系
//待插入key比根key大,就插入根节点右子树
//否则插入根节点左子树
Node node = new Node(key);
if (key < parent.key) {
parent.left = node;
} else {
parent.right = node;
}
return true;
}
public boolean remove(int key) {
Node cur = root;
Node parent = null;
//1.遍历找到要删除key的位置
while (cur != null) {
if (cur.key == key) {
removeHelp(parent, cur);
return true;
} else if (cur.key > key) {
parent = cur;
cur = cur.left;
} else if (cur.key < key) {
parent = cur;
cur = cur.right;
}
}
return false;
}
private void removeHelp(Node parent,Node cur){
if(cur.left == null){
if(cur == root){
root = cur.right;
} else 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 = cur.left;
} else if (cur == parent.left) {
parent.left = cur.left;
}else if(cur == parent.right){
parent.right = cur.left;
}
}else {
//1.从待删除节点的右子树的最左边找替罪羊
Node target = cur.right;//替罪羊节点
Node targetParent = cur;
while(target != null){
targetParent = target;
target = target.left;
}
//2.交换值
cur.key = target.key;
//3.判断,
//待删除节点的右树有没有左子树
if(target == targetParent.left){
targetParent.left = target.right;
}else {
targetParent.right = target.right;
}
}
}
}
2. Map
1.Map和set是一种专门用来进行搜索的容器或者数据结构,其搜索的效率与其具体的实例化子类有关。
- 直接遍历:时间复杂度O(N)
- 二分查找:时间复杂度O(logN),但搜索前必须要求数据是有序的。
- 比较适合静态类型查找,一般不会对指定区间进行插入和删除。
2.Map:是一个接口类,该类没有继承自Collection,该类中存储的是<K,V>结构的键值对,并且K一定是唯一的,不能重复,但是V可以重复(修改原本K的V值)。
-
K和V的类型可以相同,也可以不同,K的类型一定要是可以比较的。
-
键值对:表示K和V是一一对应的关系,比如K=面包,V=10(元)。
Map常用方法:
方法 | 解释 |
---|---|
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 keySet() | 返回所有 key 的不重复集合 |
Collection values() | 返回所有 value 的可重复集合 |
Set<Map.Entry<K,V>> entrySet() | 返回所有的 key-value 映射关系 |
boolean containsKey(Object key) | 判断是否包含 key,有返回true,否则false |
boolean containsValue(Object value) | 判断是否包含 value,有返回true,否则false |
-
Map是一个接口,不能直接实例化对象,如果要实例化对象只能通过实例化实现类TreeMap或HashMap。
-
HashMap<K,V> :存储数据采用哈希表结构,元素的存取顺序不能保证一致,K的类型一定要可以比较,由于要保证K的唯一,不重复,需要重写HashCode()、equals()方法。
-
使用put方法时,如传的key存在,则只会把指定key所对应的value值,替换成指定的新值,而不会再添加一个key,返回值为key对应的value值。
-
使用get方法时,获取指定key所对应的value值。
-
remove方法,根据指定的key删除元素,返回被删除元素的value值。
-
TreeMap和HashMap的区别:
Map底层结构 TreeMap HashMap 底层结构 红黑树 哈希桶 插入/删除/查找时间 复杂度 O(logN) O(1) 是否有序 关于Key有序 无序 线程安全 不安全 不安全 插入/删除/查找区别 需要进行元素比较 通过哈希函数计算哈希地址 比较与覆写 key必须能够比较,否则会抛出 ClassCastException异常 自定义类型需要覆写equals和 hashCode方法 应用场景 需要Key有序场景下 Key是否有序不关心,需要更高的 时间性能 HashMap方法演示:
public class Main { public static void main(String[] args) { //创建HashMap对象,key为String类型,value为Integer HashMap<String,Integer> map = new HashMap<>(); //给map添加元素,每一次放入都会进行比较,第一次放入也会比较 map.put("奶茶",18); map.put("面包",6); map.put("咖啡",22); map.put(null,null); //因为value是Integer类型,可以使用int也可以Integer接收 //这涉及到拆包问题 int get1 = map.get("奶茶");//返回18 Integer get2 = map.get("咖啡");//返回22 //因为HashMap重写了toString //输出:{null=null, 面包=6, 咖啡=22, 奶茶=18} System.out.println(map); Integer del = map.remove("奶茶");//返回对应value值 } }
``
Set<Map.Entry<K,V>> entrySet():集合遍历键值対
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 |
获取Map集合中,所有的Key和Value并组织起来,把相对的K和V作为一个整体,放入Set,组织起来的这个结构就叫做Map.Entry<K,V>。原先Key,Value的类型是Map,现在变成了Map.Entry类型。(泛型的参数,不参加类型的组成的:ArrayList < String> list1 和Arra< Integer > list2,此时list1和list2的类型都是ArrayList)
源码:
使用方法:
可以直接通过for each 遍历原先的Map里的内容。如果不实现Set<Map.Entry<K,V>> entrySet(),那么是无法遍历得到Map里的key和value值的。
3. Set
常见方法:
方法 | 解释 |
---|---|
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中,可以达到去重的效果 |
注意:
- Set是继承Collection的一个接口类。
- Set中只存储了key,并且要求key一定要唯一。
- Set的底层是使用Map来实现的,其使用key与Object的一个默认对象作为键值对插入到Map中的。
- Set最大的功能就是对集合中的元素进行去重。
- 实现Set接口的常用类有TreeSet和HashSet,还有一个LinkedHashSet,LinkedHashSet是在HashSet的基础 上维护了一个双向链表来记录元素的插入次序。
- Set中的Key不能修改,如果要修改,先将原来的删除掉,然后再重新插入。
- Set中不能插入null的key。
TreeSet和HashSet的区别:
Set底层结构 | TreeSet | HashSet |
---|---|---|
底层结构 | 红黑树 | 哈希桶 |
插入/删除/查找时间 复杂度 | O(logN) | O(1) |
是否有序 | 关于Key有序 | 不一定有序 |
线程安全 | 不安全 | 不安全 |
插入/删除/查找区别 | 按照红黑树的特性来进行插入和删除 | 1.先计算key哈希地址 2. 然后进行 插入和删除 |
比较与覆写 | key必须能够比较,否则会抛出 ClassCastException异常 | 自定义类型需要覆写equals和 hashCode方法 |
应用场景 | 需要Key有序场景下 | Key是否有序不关心,需要更高的 时间性能 |
public class Main {
public static void main(String[] args) {
HashSet<String> hashSet = new HashSet<>();
//add(key),如果不存在,则插入并返回true
//key为空,报空指针异常
boolean insert = hashSet.add("咖啡");
hashSet.add("拿铁");
//contains(key),如果key存在,返回true
boolean cont = hashSet.contains("咖啡");
//remove(key),如果key存在删除返回true
//key为空,报空指针异常
boolean del = hashSet.remove("咖啡");
//使用迭代器遍历
Iterator<String> it = hashSet.iterator();
while(it.hasNext()){
System.out.println(it.next()+" ");
//程序运行,输出 拿铁
//咖啡 已被删除了
}
}
}