这里讲的是最简单的搜索树。
搜索树的概念很简单,就是根节点的左边节点的值比根节点小,右边节点比根节点大,同时每一棵子树都是如此。
示意图:
思路:
跟链表的查找有点像,首先先设置一个节点cur,判断查找的值是否大于当前cur的值,大于cur的值,那么cur = cur.right,否则如果小于cur的值,cur = cur.left ,不然就是返回当前节点的值。
代码:
public TreeNode search(int key) {
if(root == null) {
return null;
}
TreeNode cur = root;
while(cur != null) {
if(cur.key > key) {
cur = cur.left;
}
else if(cur.key < key) {
cur = cur.right;
}
else {
return cur;
}
}
return null;
}
思路:
插入节点就相当于在找到一个合适的节点后,插入到这个节点的左边或者右边。也就是说插入节点,这个节点其实也就是叶子节点。创建一个节点cur来查找合适的位置,当cur为空的时候,那么cur的上一个节点就是合适的节点。所以我们还要创建一个parent来保存cur的上一个节点,来确保cur为空的时候,还能找到上一个节点。
public boolean insert(int key) {
TreeNode node = new TreeNode(key);
if(root == null) {
root = node;
}
TreeNode cur = root;
TreeNode parent = null;
while (cur != null) {
if(cur.key > key) {
parent = cur;
cur = cur.left;
}
else if (cur.key < key) {
parent = cur;
cur = cur.right;
}
else {
return false;
}
}
if(parent.key > key) {
parent.left = node;
}
else{
parent.right = node;
}
return true;
}
删除比较复杂,具体分7种情况。
现在假设我们通过查找,cur就是我们要删除的节点。
一、cur.left为空的时候
1.cur.left为空的时候,而cur 又为 root 的时候,直接root = root.right即可。
示意图:
2.cur.left为空的时候,创建一个节点来保存cur的上一个节点,假设这个保存的节点是parent。如果cur.right 不为空,那么下一步操作就是parent .right = cur.right即可。
3.cur.left为空的时候,创建一个节点来保存cur的上一个节点,假设这个保存的节点是parent。如果cur.right 为空,那么下一步操作就是parent .right = null即可。
二、cur.right 为空的时候
1.cur.right 为空的时候,并且cur == root 的时候,那么直接root = root.left 即可。
2.cur.right为空的时候,创建一个节点来保存cur的上一个节点,假设这个保存的节点是parent。如果cur.right 为空,那么下一步操作就是parent .right = cur.right即可。
3.2.cur.right为空的时候,创建一个节点来保存cur的上一个节点,假设这个保存的节点是parent。如果cur.left 为空,那么下一步操作就是parent .right = null即可。
三、cur.right 不为空和cur.left 不为空的情况
假如我要删除85这个节点,思路如下:
要么在85这个节点的左边找到最大值,要么在85这个节点的右边找到最小值,然后找到的这个值替换掉85这个节点的值。
技巧:利用了最大或者最小值的节点只能是叶子节点的特性。
我以在右边找到最小值为例画图。
85的右边的最小值就是87,然后85的节点的值替换成87,然后87节点置空即可。
具体代码:
public void remove(int key) {
TreeNode cur = root;
TreeNode parent = null;
while (cur != null) {
if(cur.val < key) {
parent = cur;
cur = cur.right;
}else if(cur.val > key) {
parent = cur;
cur = cur.left;
}else {
//找到了 就要开始删除了
removeNode(cur,parent);
return;
}
}
}
private void removeNode(TreeNode cur, TreeNode parent) {
if(cur.left == null) {
if(cur == root) {
root = root.right;
}else if(cur == parent.left) {
parent.left = cur.right;
}else {
parent.right = cur.right;
}
}else if(cur.right == null) {
if(cur == root) {
root = root.left;
}else if(cur == parent.left){
parent.left = cur.left;
}else {
parent.right = cur.left;
}
}else {
TreeNode targetParent = cur;
TreeNode target = cur.right;
while (target.left != null) {
targetParent = target;
target = target.left;
}
cur.val = target.val;
//分两种情况
if(targetParent.left == target) {
targetParent.left = target.right;
}else {
targetParent.right = target.right;
}
}
}
import java.util.HashMap;
import java.util.Map;
import java.util.TreeMap;
public class test {
public static void main(String[] args) {
Map<String,Integer> map = new TreeMap<>();
Map<String,Integer> map2 = new HashMap<>();
}
}
Map是一个接口,尖括号里面是数据的引用类型。上面的代码就是Map的具体使用。
Map想要实例化,只能实例化TreeMap或者HashMap.
如果实例化TreeMap,底层是一棵红黑树。
如果实例化HashMap,底层是哈希表。
按照我自身的理解,我认为Map就是一个容器,存储的模式是键值对的模式。就好比一个人总会有外号的,当有人叫这个外号的时候,你总会有回应。例如,悟空是斗战胜佛,猪八戒是净坛使者。。。。
1.增(添加元素)
public static void main(String[] args) {
Map<String,Integer> map = new TreeMap<>();
map.put("hello",8);
map.put("strange",10);
map.put("apple",3);
map.put("banana",4);
System.out.println(map);
//Map map2 = new HashMap<>();
}
注意观察,我添加元素的时候,不是按照我代码的顺序打印出来的,是排列过的顺序打印出来的。
原因是TreeMap 里面的元素必须是课比较的,否则就不能添加,另外null也不能添加。所以添加自定义类型要实现Comparable接口或者传一个比较器。
public static void main(String[] args) {
Map<students,Integer> map2 = new TreeMap<>();
map2.put(new students(),1);
}
public static void main(String[] args) {
Map<students,Integer> map2 = new TreeMap<>();
map2.put(null,1);
}
2.删(删除元素)
public static void main(String[] args) {
Map<String,Integer> map = new TreeMap<>();
map.put("hello",8);
map.put("strange",10);
map.put("apple",3);
map.put("banana",4);
System.out.println(map);
map.remove("hello");
map.remove("apple",3);
System.out.println(map);
}
利用remove方法可以删除Map中的元素,有两种参数形式,一种只传key值,第二种是传key值和value值。
3.查(查找元素)
public static void main(String[] args) {
Map<String,Integer> map = new TreeMap<>();
map.put("hello",8);
map.put("strange",10);
map.put("apple",3);
map.put("banana",4);
System.out.println(map);
System.out.println(map.get("apple"));
System.out.println(map.get("banana"));
}
通过查找key值可以得出key所对应的的value值。
4.改(修改元素)
Map中的元素的key是不可以重复,但是value可以,因此你新增一个相同的元素,当key相同时,会直接覆盖,value会变成新的value。
public static void main(String[] args) {
Map<String,Integer> map = new TreeMap<>();
map.put("hello",8);
map.put("strange",10);
map.put("apple",3);
map.put("banana",4);
System.out.println(map);
map.put("apple",13);
map.put("banana",14);
System.out.println(map);
}
public static void main(String[] args) {
Map<String,Integer> map = new TreeMap<>();
map.put("hello",8);
map.put("strange",10);
map.put("apple",3);
map.put("banana",4);
System.out.println(map);
Set<String> set = map.keySet();
System.out.println(set);
}
这个方法就是把Map中的元素的key值都放到一个集合中去。
public static void main(String[] args) {
Map<String,Integer> map = new TreeMap<>();
map.put("hello",8);
map.put("strange",10);
map.put("apple",3);
map.put("banana",4);
System.out.println(map);
Set<String> set = map.keySet();
System.out.println(set);
Set<Map.Entry<String,Integer>> set2= map.entrySet();
for (Map.Entry<String,Integer> stringIntegerEntry: set2) {
System.out.println(stringIntegerEntry);
}
}
Set
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 |
返回所有的 key-value 映射关系 |
boolean containsKey(Object key) | 判断是否包含 key |
boolean containsValue(Object value) | 判断是否包含 value |
HashMap 和 TreeMap的各种方法都是一样的,只是底层不一样。
Map底层结构 | TreeMap | HashMap |
---|---|---|
底层结构 | 红黑树 | 哈希桶 |
插入/删除/查找时间 复杂度 | O(1) | |
是否有序 | 关于Key有序 | 无序 |
线程安全 | 不安全 | 不安全 |
插入/删除/查找区别 | 需要进行元素比较 | 通过哈希函数计算哈希地址 |
比较与覆写 | key必须能够比较,否则会抛出 ClassCastException异常 | 自定义类型需要覆写equals和 hashCode方法 |
应用场景 | 需要Key有序场景下 | Key是否有序不关心,需要更高的 时间性能 |
set的使用同Map差不太多,这里就不再演示Set方法的使用了。需要注意的是Set的模式是纯key模型,也就是里面只有key值,没有value值。运用的场景一般是在去重的情况下。
方法 | 解释 |
---|---|
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 extends E> c) | 将集合c中的元素添加到set中,可以达到去重的效果 |
至于为什么Set能去重?是因为TreeSet 和 HashSet 的底层分别是 TreeMap 和 HashMap.
哈希表也是一种数据结构,它的增删查改的时间复杂度很快,都是O(1)。
为什么会这么快呢?
原因是它插入元素时,是通过哈希函数来插入的,那么查找时也是通过哈希函数来查找的,这就是它快的原因。
假设现在有一组数据,{1,4,7,2,3,8};设有一个长度为10的数组,那么根据数据和数组长度的关系,我们把哈希函数设为:data % 数组的长度。那么就有:
如果后面还有数据例如 42 , 52 , 这些等等数据要存放到哈希表中怎么办呢?
假如后面还有数据要存入,那么哈希表必然是放不下的。这种情况就称作哈希冲突。
更为专业的解释:不同关键字通过相同哈希哈数计算出相同的哈希地址,该种现象称为哈希冲突或哈希碰撞。
想要实现O(1)的时间复杂度,哈希冲突是无法避免的,哈希冲突总会存在。唯一能做的就是降低哈希冲突率。
想要降低哈希冲突率,那必然要设计好哈希函数。
关于哈希函数的学习,可以不用太了解,毕竟不可能自己写哈希函数。
常见的哈希函数法:
\1. 直接定制法–(常用)
取关键字的某个线性函数为散列地址:Hash(Key)= A*Key + B 优点:简单、均匀 缺点:需要事先知道关 键字的分布情况 使用场景:适合查找比较小且连续的情况
\2. 除留余数法–(常用)
设散列表中允许的地址数为m,取一个不大于m,但最接近或者等于m的质数p作为除数,按照哈希函数:
Hash(key) = key% p(p<=m),将关键码转换成哈希地址。
哈希表的负载因子的定义这样的:元素的个数 / 散列表的长度
负载因子一般是在0.75左右是最好的。
负载因子和冲突率的图表关系:
可以看出负载因子越高,冲突的发生率就越高。
那么如果降低负载因子呢?
由于你的元素肯定是一直放进来的,那么就不可能通过减少元素个数来降低,只能通过改变散列表的大小来降低。
1.线性探测
从发生冲突的位置开始,依次向后探测,直到寻找到下一个空位置为止。
引用上面的例子,假如我要放入32,42等数据,那么就在发生哈希冲突的位置,开始寻找下一个空位置,并且把这些数据放进去。
但这样方法的缺点就是不敢随意删除元素,假如我第一个元素22,如果我删除了,那么后面的32,42等就很难再找到。并且这样会把发生冲突的数据都堆积在一起,这一点很不好。
2.二次探测
二次探测为了解决这个问题,引入了一个函数来寻找下一个位置:Hi = (H0 + i^2)% length。H0就是你第一次发生冲突的位置,i就是你发生的次数,通过这个函数来查找下一个位置,会很好的把这些堆积的数据都分隔开。
以上的两种方法称为闭散列,共同的缺点就是空间的利用率不高。
下面就介绍另一种解决方法
开散列又称为哈希桶,链地址法,开链法等。
他的结构就是一个数组+链表。什么意思呢?