搜索树
概念
二叉搜索树又称二叉排序树,它或者是一棵空树,或者是具有以下性质的二叉树:
若它的左子树不为空,则左子树上所有节点的值都小于根结点的值
若它的右子树不为空,则右子树上所有节点的值都大于根节点的值
它的左右子树分别也称为二叉搜索树
操作-查找
操作-插入
1.如果树为空树,即根 == null,直接插入
2.如果树不是空树,按照查找逻辑确定插入位置,插入新节点
操作-删除
设待删除节点为cur,待删除结点的双亲结点为parent
1.cur.left == null
1. cur 是 root ,则 root = cur.right
2. cur 不是 root , cur 是 parent.left ,则 parent.left = cur.right
3. cur 不是 root,cur 是 parent.right,则 parent.right = cur.right
2.cur.right == null
1. cur 是 root,则 root = cur.left
2. cur 不是 root , cur 是 parent.left ,则 parent.left = cur.left
3.cur 不是 root , cur 是 parent.right ,则 parent.right = cur.left
3.cur.left != null && cur.right != null
1.确定要删除结点这里将来的数据一定是比左边都大,比右边都小的数据
2.如何保证第一条?
(1)在左树里面找到最大的数据(左树最右边的数据)
(2)在右树里面找到最小的数据(右树最左边的数据)
3.根据第二点,在找到合适的数据之后,直接替换cur的值,然后删除那个合适的数据结点即可
实现
代码如下:
public class BinarySearchTree { public static class Node { int key; Node left; Node right; public Node(int key) { this.key = key; } } private Node root = null; /** * 在搜索树中查找key,如果找到,返回key所在结点,否则返回null * * @param key * @return */ public Node search(int key) { Node cur = root; while (cur != null) { //找到,并直接返回结点 if (key == cur.key) { return cur; } else if (key < cur.key) { //比当前结点更小,所以到左边遍历以寻找 cur = cur.left; } else { //比当前节点更大,所以到右边遍历以寻找 cur = cur.right; } } //没有查找到,直接返回null return null; } /** * 插入操作 * * @param key * @return true表示插入成功,false表示插入失败 */ public boolean insert(int key) { //如果root是空的,则直接创建新节点 if (root == null) { root = new Node(key); return true; } //如果树不是空树,按照查找逻辑确定插入位置,插入新节点 Node cur = root; Node parent = null; while (cur != null) { if (key == cur.key) { //相同表明原搜索二叉树就含有key,插入失败 return false; } else if (key < cur.key) { //根据二叉树的性质,应该向左寻找插入位置 parent = cur; cur = cur.left; } else { //向右寻找插入的位置 parent = cur; cur = cur.right; } } //找到了,这里创建新的结点 Node node = new Node(key); //判断新的结点应该插入右边还是左边 if (key < parent.key) { parent.left = node; } else { parent.right = node; } //返回true以表明插入成功 return true; } /** * 删除成功返回true,删除失败返回false * * @param key * @return */ public boolean remove(int key) { Node cur = root; Node parent = null; while (cur != null) { if (key == cur.key) { //找到了,跳出准备进行删除操作 break; } else if (key < cur.key) { parent = cur; cur = cur.right; } else { parent = cur; cur = cur.right; } } //跳出循环后准备删除操作 //先判断要删除的元素是否存在,为null则删除失败 if (cur == null) { return false; } /*、 根据cur的孩子是否存在分四种情况 1.cur左右孩子均不存在 2.cur只有左孩子 3.cur只有右孩子 4.cur的左右孩子均存在 看起来只有四种情况,实际情况1可以与情况2或者情况三进行合并,只需要处理那种情况即可 除了情况四之外,其他情况可以直接删除 情况四不能直接删除,需要在子树中找到一个替代结点进行删除 */ 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 { //找到右树中最小的,让其与删除结点替换,然后删除这个结点 Node targetParent = cur; Node target = cur.right; while(target.left != null) { targetParent = target; target = targetParent.left; } cur.key = target.key; //再删除target这个结点 if(target == targetParent.right) { targetParent.right = target.right; } else if(target == targetParent.left) { targetParent.left = target.right; } } return true; } }
性能分析
插入和删除操作都必须先查找,查找效率代表了二叉搜索树中各个操作的性能
对有n个结点的二叉搜索树,若每个元素查找的概率相等,则二叉搜索树平均查找长度是结点在二叉搜索树的深度的函数,即节点越深,则比较次数越多
但对于同一个关键码的集合,如果各关键码插入的次序不同,可能得到不同结构的二叉搜索树: 比如一个单支搜索树就是比较特殊的情况
最优情况下,二叉树为完全二叉树,其平均比较次数为log2 N
最差情况下,二叉搜索树退化为单支树,其平均比较次数为:N/2
问题提出:如果退化为单支树,二叉树的搜索性能就失去了。那能否进行改进,不论按照什么次序插入关键码,都可以使二叉树的性能最佳?
和java类集的关系
TreeMap和TreeSet即java中利用的搜索树实现的Map和Set;实际上用的是红黑树,而红黑树是一棵近似平衡的二叉搜索树,即在二叉搜索树的基础之上+颜色以及红黑树性质验证。
搜索
概念及场景
Map和Set是一种专门用来进行搜索的容器或者数据结构,其搜索的效率与其具体的实例化子类有关。以前常见的搜索方式有:
1.直接遍历,时间复杂度为O(N),元素如果比较多效率会非常慢
2.二分查找,时间复杂度为O(log2 N),但在搜索之前会要求序列是有序的
上述排序比较适合静态类型的查找,即一般不会对区间进行插入和删除操作了,而现实中的方法比如:
1.根据姓名查询考试成绩
2.通讯录,即根据姓名查询联系方式
3.不重复集合,即需要先搜索关键字是否已经在集合中
可能在查找时进行一些插入和删除的操作,即动态查找,那上述两种方式就不太适合了,本节介绍的Map和Set是一种适合动态查找的集合容器。
模型
一般把搜索的数据称为关键字(Key)和关键字对应值(Value),将其称之为Key-Value的键值对,所以模型会有两种:
1.纯Key模型,比如:
有一个英文词典,快速查找一个单词是否在词典中
快速查找某个名字在不在通讯录中
2.Key-Value型,比如:
统计文件中每个单词的出现次数,结果是每个单词都有与其对应的次数:<单词,对应次数>
梁山好汉的江湖绰号:每个好汉都有自己的江湖绰号
而Map中存储的就是key-value的键值对,Set中只存储了key
Map的使用
关于Map的说明
Map是一个接口类,该类没有继承自Collection,该类中存储的是<K,V>结构的键值对,并且K一定是唯一的,不能重复。
关于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 |
注意:Map.Entry<K,V>并没有提供设置Key的方法
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<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 |
注意:
1.Map是个接口,不能直接实例化对象,如果要实例化对象只能实例化其实现类TreeMap或者HashMap
2. Map中存放键值对时,key不能为空,否则会抛NullPointerException异常,value可以为空。但是HashMap的key和value都可以为空。
3.Map中存放键值对的key是唯一的,value是可以重复的
4.Map中的key可以全部分离出来,存储到Set中来进行访问(因为key不能重复)
5.Map中的value可以全部分离出来,存储在Collection的任何一个子集合中(value可能有重复)
6.Map中的键值对的key不能修改,value可以修改,如果要修改key,只能先将key删除掉,再重新插入新的key
TreeMap和HashMap的区别
Map底层结构 | TreeMap | HashMap |
底层结构 | 红黑树 | 哈希桶 |
插入/删除/查找时间复杂度 | O(log2 N) | O(1) |
是否有序 | 关于key有序 | 无序 |
线程安全 | 不安全 | 不安全 |
插入/删除/查找的区别 | 需要进行元素比较 | 通过哈希函数计算哈希地址 |
比较和覆写 | key必须能够比较,否则会抛出ClassCastException异常 | 自定义类型需要覆写equals和hashCode方法 |
应用场景 | 需要在key有序的场景下 |
不关心key是否有序,需要高的时间性能 |
Set的说明
Set和Map主要的不同有两点:Set是继承自Collection的接口类,Set中之存储了Key.
常见方法的说明
方法 | 解释 |
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中,可以达到去重的效果 |
注意:
1.Set是继承自Collection的一个接口类
2.Set中只存储了key,并且要求key一定要唯一
3. TreeSet的底层是使用Map实现的,其使用key与Object的一个默认对象作为键值插入到Map中的
4.Set最大的功能就是对集合中的元素进行去重
5.实现Set接口的常用类有TreeSet和HashSet,还有一个叫LinkedHashSet,LinkedHashSet是在HashSet的基础上维护了一个双向链表来记录元素的插入顺序
6.Set的Key不能修改,如果要修改,现将原来的删除掉,然后再重新插入
7.TreeSet中不能插入null的key,HashSet可以
TreeSet使用举例
public class SetTest { public static void TestSet() { Set<String> s = new TreeSet<>(); //add(key):如果key不存在,则插入,返回true //如果key存在,返回false boolean isIn = s.add("apple"); s.add("orange"); s.add("peach"); s.add("banana"); System.out.println(s.size()); System.out.println(s); isIn = s.add("apple"); //add(key):key如果是空,抛出空指针异常 //s.add(null); //contains(key):如果key存在,返回true,否则返回false System.out.println(s.contains("apple")); System.out.println(s.contains("watermelen")); //remove(key):key存在,删除成功,返回true // key不存在,删除失败返回false // key为空,抛出空指针异常 s.remove("apple"); System.out.println(s); s.remove("watermelen"); System.out.println(s); //利用迭代器遍历 Iterator<String> it = s.iterator(); while(it.hasNext()) { System.out.print(it.next() + " "); } System.out.println(); } public static void main(String[] args) { TestSet(); } }