目录
- 搜索树
- 1.概念
- 2.操作--查找
- 代码实现:
- 3.操作--插入
- 代码实现:
- 4.操作--删除
- 代码实现:
- 实现
- 搜索
- 概念
- 模型
- Set的说明
- 常见方法
- Map的使用
- 概念
- Entry<K,V>
- 常用方法
搜索树
1.概念
二叉搜索树(Binary Search Tree)–>TreeMap
二叉树:树的度为 2(度就是子树的个数)
二叉搜索树:在二叉树的基础上做出了额外要求:
如果左子树不为空,左子树的所有key都应该小于根节点的key
如果右子树不为空,右子树的所有key都应该大于等于根节点的key
注意区分 二叉搜索树 和 堆 的区别
二叉搜索树是存在“左右关系”的,要求任意节点 左<根<右
堆是存在“上下关系”的,要求 父节点<子节点
二叉搜索树示例:
二叉搜索树特性:
二叉搜索树最左侧的节点一定是最小的
二叉搜索树最右侧的节点一定是最大的
中序遍历二叉搜索树会打印一个有序序列
2.操作–查找
如果要进行查找,就可以用类似于 “ 二分 ” 的方式来进行
思路:
如果查找的值比该节点的值大,往右子树找,如果比该节点值小,往左子树找,想通就找到了
理想情况下,如果二叉搜索树左右子树的个数都差不多
此时时间复杂度为O(logN)
二叉搜索树和树的高度相关
最坏情况下为O(N)
代码实现:
//查找key是否存在
public TreeNode search(int key) {
TreeNode cur=root;
while (cur!=null){
if(key<cur.key){
cur=cur.left;
}else if(key>cur.key){
cur=cur.right;
}else {//找到了
return cur;
}
}
return null;//找到cur等于空了也没找到
}
3.操作–插入
对于二叉搜索树来说,插入的本质还是查找
通过查找的方式,找到新元素所在的位置
思路:
1.先按照查找的思路,直到记录位置的节点cur指向null,此时 cur 的父节点parent 就指向一个叶子结点
2.从parent的子节点插入元素(注意判断该元素比父节点大还是小)
注意对根节点为空的情况特殊讨论
代码实现:
public boolean insert(int key) {
if(root==null){
root=new TreeNode(key);
return true;
}
TreeNode parent=null;//记录插入的父节点的位置
TreeNode cur=root;//记录查找的位置
while (cur!=null){
if(key<cur.key){//要插入的元素比当前元素小
parent=cur;
cur=cur.left;
}else if(key>cur.key){//要插入的元素比当前元素大
parent=cur;
cur=cur.right;
}else{//如果两值相等,只保留一个元素,此时插入失败
return false;
}
}
//此时cur指向空,parent是叶子节点
if(key< parent.key){
parent.left=new TreeNode(key);
}else {
parent.right=new TreeNode(key);
}
return true;
}
4.操作–删除
二叉树一般使用 “ 孩子表示法 ” ,没法通过子节点找到父节点(有计算下标位置的公式只适用于完全二叉树)
所以,在删除操作时,不光要记录要删除节点(设为cur)的位置,也需要记录该节点的父节点(设为parent)的位置
1.如果 cur 没有子树,直接删除 cur 即可,然后再修改parent的引用,此时**要注意 cur 是 parent 的左子树还是右子树**
如果是左子树:parent.left=null,如果是右子树:parent.right=null
2.如果 cur 只有左子树,没有右子树,可以把 cur 的左子树接到 parent 上(相当于架空cur)
if(parent.left == cur){
parent.left=cur.left;
}
if(parent.right ==cur){
parent.right=cur.left;
}
3.如果 cur 只有右子树,没有左子树,可以把 cur 的右子树接到 parent 上(与第二点类似)
if(parent.left == cur){
parent.left=cur.right;
}
if(parent.right ==cur){
parent.right=cur.right;
}
4.如果 cur 同时有右左子树和右子树
有一种巧妙的方法
找到 cur 待删除元素 的右子树的最左侧元素 /左子树的最右侧元素,然后把这个元素赋给cur 位置的值
这时就相当于要删除的元素没了
最后把 右子树的最左侧元素 /左子树的最右侧元素 删除掉(这个值就被当成“替罪羊”了)
5.除了上述情况,还要考虑一个特殊情况
如果待删除节点是根节点,要修改root 引用的指向
代码实现:
//删除key的值
public boolean remove(int key) {
if(root==null){
return false;
}
TreeNode cur=root;
TreeNode parent=null;
while (cur!=null){
if(key<cur.key){
parent=cur;
cur=cur.left;
} else if (key>cur.key) {
parent=cur;
cur=cur.right;
}else{
break;
}
}
if(cur==null) return false;//没找到
//1.cur左右子树都没
if(cur.left==null && cur.right==null){
if(cur==root){
root=null;
return true;
}
if(parent.left==cur) parent.left=null;
if(parent.right==cur) parent.right=null;
}
//2.cur只有左子树
if(cur.left!=null && cur.right==null){
if(cur==root){
root=cur.left;
}else if(parent.left==cur) parent.left=cur.left;//架空cur
else if(parent.right==cur) parent.right=cur.left;
return true;
}
//3.cur只有右子树
if(cur.left==null && cur.right!=null){
if(cur==root) root=cur.right;
else if(parent.left==cur) parent.left=cur.right;
else if (parent.right==cur) parent.right=cur.right;
return true;
}
//4.cur既有左子树又有右子树
if(cur.left!=null&&cur.right!=null){
removeHelper(cur);
return true;
}
return false;
}
private void removeHelper(TreeNode cur){
TreeNode goat=cur.right;//找右子树的最左侧
TreeNode goatParent=cur;
while (goat.left!=null){
goatParent=goat;
goat=goat.left;
}//goat的左子树为空
cur.key=goat.key;//”替罪“
//两种情况,一个没有进入while循环,一个进入了循环
if(goatParent.right==goat){
goatParent.right=goat.right;
} else if (goatParent.left==goat) {
goatParent.left=goat.right;
}
}
实现
结合上述操作的方法实现,再添加打印操作
static class TreeNode {
public int key;
public TreeNode left;
public TreeNode right;
TreeNode(int key) {
this.key = key;
}
@Override
public String toString() {
return "TreeNode{" +
"key=" + key +
", left=" + left +
", right=" + right +
'}';
}
}
public TreeNode root;
public void inOrder(TreeNode root){//中序遍历
if(root==null) return;
inOrder(root.left);
System.out.print(root.key+" ");
inOrder(root.right);
}
public void print(){
inOrder(root);
System.out.println();
}
public static void main(String[] args) {//测试实现
int[] arr= {1,3,2,6,5,7,8,9,10,0};
BinarySearchTree bst=new BinarySearchTree();
for (int key:arr){
bst.insert(key);
}
bst.print();
System.out.println(bst.search(3));
bst.remove(1);
bst.print();
bst.remove(3);
bst.print();
System.out.println(bst.remove(1));
bst.print();
}
搜索
概念
Map和set是⼀种专⻔⽤来进⾏搜索的容器或者数据结构,其搜索的效率与其具体的实例化⼦类有关。
应用场景:
- 根据姓名查询考试成绩
- 通讯录,即根据姓名查询联系⽅式
如果先搜索关键字是否已经在集合中可能在查找时进⾏⼀些插⼊和删除的操作,即动态查找,那上述二叉搜索树就不太适合了,Map和Set就是⼀种适合动态查找的集合容器。
模型
⼀般把搜索的数据称为关键字(Key),和关键字对应的称为值(Value),将其称之为Key-value的键
值对,所以模型会有两种:
- 纯 key 模型,⽐如:
◦有⼀个英⽂词典,快速查找⼀个单词是否在词典中
◦快速查找某个名字在不在通讯录中 - Key-Value 模型,⽐如:
◦统计⽂件中每个单词出现的次数,统计结果是每个单词都有与其对应的次数:<单词,单词出现的次数>
Set的说明
Set的核心特点:
1.保存的元素有序
2.保存的元素不会重复
3. 不能重复,不能修改,顺序无关
与List不同,List(有序的)中的{1,2,3}和{1,3,2}是不同的,而Set(无序的)中这两个组合是相同的
Set中是没有“下标”概念的
Set 最主要的应用场景就是看元素是否在 Set 中存在
常见方法
核心操作:add ,remove,contains
Set<String> set1=new TreeSet<>();//树形结构,与二叉树有很大联系
Set<String> set2=new HashSet<>();//哈希表
//添加元素
set1.add("a");
set1.add("c");
set1.add("b");
set2.add("a");
set2.add("b");
set2.add("c");
System.out.println(set1);//[a, b, c]
System.out.println(set1.equals(set2));//true
//判定元素是否存在
System.out.println(set1.contains("a"));//true
//删除元素
set1.remove("c");
System.out.println(set1);//[a, b]
//获取元素个数
System.out.println(set1.size());//2
//判定是否为空
System.out.println(set1.isEmpty());//false
//清空整个集合
set1.clear();
System.out.println(set1);//[]
//遍历集合中的元素
for (String s:set2){
System.out.print(s);//abc
}
System.out.println();
//基于迭代器遍历
Iterator<String> it=set2.iterator();
while (it.hasNext()){
System.out.print(it.next());//abc
}
Map的使用
和Set类似,Map更进一步,Set上面保存的只是“键”(key),Map上面保存的是“键值对”(key-value)
键值对key-value表示的是映射关系(Map最主要的用途)
即拿到一个key 就能对应到一个唯一的 value
在Map 中,值可以重复,但是键不能重复
即 key 是唯一的
概念
Entry<K,V>
说明:
Map.Entry<K, V> 是Map内部实现的⽤来存放<key, value>键值对映射关系的内部类,该内部类中主要提供了<key, value>的获取,value的设置以及Key的⽐较⽅式
keySet 是把所有的 key,收集出来,放到一个 Set 中
entrySet 是把所有的 key-value 收集出来,放到一个 Set 中
常用方法
核心操作:get , put,remove
//Map<key,value>
Map<String,String> map=new TreeMap<>();
Map<String,String> map1=new HashMap<>();
//用 put 插入键值对;用 get 根据 key 获取 value
map.put("张三","语文老师");
map.put("李四","数学老师");
map.put("王五","英语老师");
//和Set一样,Map顺序“无序”,无论插入顺序如何,最终保存的顺序一定
System.out.println(map);//{张三=语文老师, 李四=数学老师, 王五=英语老师}
map.put("赵六","语文老师");//value可重复,但key不能
System.out.println(map);//{张三=语文老师, 李四=数学老师, 王五=英语老师, 赵六=语文老师}
//使用 get 根据 key 获取到 value
System.out.println(map.get("张三"));//语文老师
System.out.println(map.get("Mike"));//null
//使用 getOrDefault ,在查找的时候,指定默认值
System.out.println(map.getOrDefault("Mike","老师"));//老师
//得到键值对个数
System.out.println(map.size());//4
//判定 key 是否存在(高效)
System.out.println(map.containsKey("张三"));//true
//判定 value 是否存在(低效)[遍历]
System.out.println(map.containsValue("语文老师"));//true
//拿到所有的key [慎用](低效)
Set<String> keySet=map.keySet();
System.out.println(keySet);//[张三, 李四, 王五, 赵六]
//拿到所有的 value [慎用](低效)
Collection<String> values=map.values();
System.out.println(values);//[语文老师, 数学老师, 英语老师, 语文老师]
//遍历 Map 的所有键值对(高开销操作,慎用)
for (Map.Entry<String,String> entry:map.entrySet()){
System.out.println(entry.getKey()+":"+entry.getValue());
//张三:语文老师
//李四:数学老师
//王五:英语老师
//赵六:语文老师
}
System.out.println(map.isEmpty());//false
map.clear();