二叉搜索树又称二叉排序树,它或者是一棵空树,或者是具有以下性质的二叉树:
若它的左子树不为空,则左子树上所有节点的值都小于根结点的值
若它的右子树不为空,则右子树上所有节点的值都大于根节点的值
它的左右子树分别也称为二叉搜索树
1.如果树为空树,即根 == null,直接插入
2.如果树不是空树,按照查找逻辑确定插入位置,插入新节点
设待删除节点为cur,待删除结点的双亲结点为parent
1.cur.left == null
1. cur 是 root ,则 root = cur.right2. cur 不是 root , cur 是 parent.left ,则 parent.left = cur.right3. 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.left3.cur 不是 root , cur 是 parent.right ,则 parent.right = cur.left
3.cur.left != null && cur.right != null1.确定要删除结点这里将来的数据一定是比左边都大,比右边都小的数据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
问题提出:如果退化为单支树,二叉树的搜索性能就失去了。那能否进行改进,不论按照什么次序插入关键码,都可以使二叉树的性能最佳?
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是一个接口类,该类没有继承自Collection,该类中存储的是
结构的键值对,并且K一定是唯一的,不能重复。
Map.Entry
是Map内部实现的用来存放 该内部类中主要提供了键值对映射关系的内部类, 的获取,value的设置以及Key的比较方式。
方法 | 解释 |
K getKey() | 返回entry中的key |
V getValue() | 返回entry中的value |
V setValue(V value) | 将键值对中的value替换为指定value |
注意:Map.Entry
方法 | 解释 |
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 |
返回所有key的不重复集合 |
Collection |
返回value所有的可重复集合 |
Set |
返回所有的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和Map主要的不同有两点:Set是继承自Collection的接口类,Set中之存储了Key.
方法 | 解释 |
boolean add(E e) | 添加元素,但重复的元素不会添加成功 |
void clear() | 清空集合 |
boolean contains(Object o) | 判断集合o是否在集合中 |
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可以
public class SetTest {
public static void TestSet() {
Set 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 it = s.iterator();
while(it.hasNext()) {
System.out.print(it.next() + " ");
}
System.out.println();
}
public static void main(String[] args) {
TestSet();
}
}