搜索树
二叉搜索树又称二叉排序树,它或者是一棵空树,或者是具有以下性质的二叉树
【1】若它的左子树不为空,则左子树上所有节点的值都小于根节点的值
【2】若它的右子树不为空,则右子树上所有节点的值都大于根节点的值
【3】它的左右子树也分别为二叉搜索树
如图所示,这样的就是搜索树
注意点:【1】.给定一棵二叉搜索树,根据节点值大小排序所需时间复杂度是线性的
【2】二叉搜索树最差情况下会退化为单支树,因此其查找的效率为O(N)
【3】二叉平衡搜索树是二叉树,同时也是平衡树,即:每个节点左右子树高度差的绝对值不超过1,换句话说每个节点左右子树顶多差一层;
在平衡二叉树中查找元素时,找的规则和二叉搜索树的规则一样,最差情况下比较的是树的高度,二叉平衡搜索树的高度为:O(logn) 以2为底,即最小算法复杂度
搜索树——元素插入
public class BinaryTree2 {
public class TreeNode{
public int val;
public TreeNode left;
public TreeNode right;
public TreeNode(int val) {
this.val = val;
}
}
public TreeNode root=null;
public boolean insert(int val){
TreeNode node=new TreeNode(val);
if(root==null){
root=node;
return true;
}
TreeNode cur =root;
TreeNode parent=null;
while(cur!=null){
if(cur.val<val){
parent=cur;
cur=cur.right;
}else if(cur.val>val){
parent=cur;
cur=cur.left;
}else{
return false;
}
}
//要插入元素了,所以我们要建立一个新的结点来放置元素
if(parent.val<val){
parent.right=node;
}else{
parent.left=node;
}
return true;
}
public static void main(String[] args) {
BinaryTree2 binaryTree2=new BinaryTree2();
binaryTree2.insert(1);
binaryTree2.insert(3);
binaryTree2.insert(6);
binaryTree2.insert(4);
}
}
搜索树——元素删除
在删除元素的时候,我们分为三种情况:1.cur.leftnull 2.cur.rightnull 3.cur的左右都不为空,第三种情况最复杂,需要使用“替罪羊”的方法去解决
//删除搜索树的结点
public void remove(int val){
TreeNode cur=root;
TreeNode parent=null;
while(cur!=null){
if(cur.val<val){
parent=cur;
cur=cur.right;
}else if(cur.val>val){
parent=cur;
cur=cur.left;
}else{
//说明cur已经找到要删除元素的位置了,所以现在我们开始调用删除的操作
removeNode(parent,cur);
return;
}
}
}
//此时cur就是所要删除的元素
public void removeNode(TreeNode parent,TreeNode cur) {
//情况1:所删元素左边为空
if (cur.left == null) {
if (cur == root) {
cur.right = root;
} else if (cur == parent.left) {
parent.left = cur.right;
} else {
parent.right = cur.right;
}
} else if (cur.right == null) {
//情况2:所删元素右边为空
if (cur == root) {
cur.left = root;
} else if (cur == parent.left) {
parent.left = cur.left;
} else {
parent.right = cur.left;
}
} else {
//情况3:所要删除的结点左右均存在
TreeNode targetParent = cur;
TreeNode target = cur.right;//可以按右边写,也可以按左边走,都可以
//此处全部按照右边走
while (target.left != null) {
targetParent = target;
target = target.left;
}
cur.val = target.val;
if (target == targetParent.left) {
targetParent.left = target.right;
} else {
targetParent.right = target.right;
}
}
}
AVL树和红黑树
【1】搜索树我们可能会变成一棵单分支的树,所以我们就需要使用AVL树; AVL树可以通过树的旋转降低树的高度
AVL树旋转方式:左旋、右旋、左右双旋、右左双旋
【2】AVL树通过旋转操作后我们可以得到红黑树(红黑树给结点增加了颜色,而且相对AVL树来讲,它的旋转会少很多)
**注意:**AVL树和红黑树这两个难度较高,我们会在后面高阶中讲到
搜索中的模型
【1】纯key模型
比如:TreeSet HashSet
这个模型就是比如有一个英文词典,我们在其中快速查找一个单词是否在词典中
【2】Key-Value模型
比如:TreeMap HashMap
这个模型是统计文件中每个单词出现的次数 eg:<单词,单词出现的次数>
**注意:**其中TreeSet ,TreeMap是树;HashSet,HashMap是哈希表
Map的使用
Map的方法
代码展示:
public class Test{
public static void main(String[] args) {
Map<String,Integer> map=new TreeMap<>();
//因为map是一个接口,所以不能直接实例化对象
// 所以我们在实例化第一个对象的时候用的是TreeMap
TreeMap<String,Integer> map2=new TreeMap<>();
//第二种是具体的类实例化具体的对象
map2.put("this",3);
map2.put("phe",5);
map2.put("a",7);
//我们在比较的时候比较的key
int val= map2.getOrDefault("phe8",1999);
//phe8存在就返回其对应的val,若是其不存在那就返回默认值,也就是1999
System.out.println(val);
Set<String> set=map2.keySet();//返回所有key的不重复集合
Collection<Integer> collection=map2.values();//返回所有val的可重复集合
System.out.println("========");
Set<Map.Entry<String,Integer>> entries=map2.entrySet();
//返回所有的key-value映射关系
for(Map.Entry<String,Integer> entry:entries){
System.out.println("key:"+entry.getKey()+"value:"+entry.getKey());
}
//for循环的加强型for (循环变量类型 循环变量名称 : 要被遍历的对象)
//所以该循环我们就是把entries遍历,赋值给entry,让它们的类型为Map.entry<String,Integer>
}
}
Map使用的重点:
1.Map是一个接口,不能直接实例化对象,如果要实例化对象只能实例化其实现类TreeMap或者HashMap
2.Map中存放键值对的key是唯一的,value是可以重复的
3.在TreeMap中插入键值对时,key不能为空,否则会抛NullPointerException异常,value可以为空;但是HashMap的key和value都可以为空
4.Map中的key可以全部分离出来,存储到Set中来进行访问(因为key不能重复)
5.Map中的value可以全部分离出来,存储在Collection的任何一个子集合中(value可能有重复)
6.Map中键值对的key不能直接修改,value可以修改,如果要修改key,只能先把key删除,然后再进重新插入;另外要是已经有一个key,再插入一个和这个key一样的key,那么我们就要更新value的值,变成最新的value值
TreeMap和HashMap的区别
Set的使用
Set的方法
代码展示:
//Set的使用
public class Test{
public static void main(String[] args) {
Set<String> set=new TreeSet<>();
set.add("hello");
set.add("abc");
set.add("plo");
System.out.println(set);
for(String s:set){
System.out.println(s);
}
}
}
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和HashSet的区别
Set和Map的使用场景
使用Map
Map是一个存储键值对的(k-v)的集合,每个键最多只能映射到一个值,当你需要以下功能的时候,应该使用Map
==1.关联查询:==当你需要根据键来获取值,那么Map是最合适的选择,Map能够快速的基于键来访问相应的值
==2.键值对存储:==当你需要存储和管理相关联的信息时,使用Map可以很方便的实现这一点
==3.唯一键:==如果你的数据模型要求键是唯一的,使用Map可以实现这一点,因为Map不允许重复的键
==4.快速更新:==如果你需要根据键快速更新值,Map也是一个好的选择,因为它提供了高效的查找和更新的工作
使用Set
Set是一个不包含重复元素的集合,当你需要以下功能的时,应该使用Set
==1.唯一性:==保集合中的元素不重复,使用Set是一个很好的选择
==2.去重:==如果你要从一个集合中去重重复的元素,使用Set是一个简单直接的方法
==3.存在性检查:==当你需要频繁的检查某个元素是否已经存在于集合中时,Set可以高效的完成任务
实际应用实例
1.如果你需要处理一组不重复的元素,使用Set是最合适的
2.如果你需要存储某种类型的对象及其属性(如员工ID及其详细信息),此时Map最合适,因为你可以通过员工ID(键)快速找到员工的详细信息(值)
总结:总而言之,选择Set还是Map取决于你的具体需求
【1】如果你关心的是不重复的值,使用Set
【2】如果你需要通过键来关联或者访问值,使用Map
注意:List和Set继承自Collection接口,而Map是独立接口,所以不是继承自Collection接口
哈希表
【1】哈希表就是用来查找的数据结构,用空间换取的时间,因此哈希表查找的时间复杂度平均为O(1),也就是常数范围内就可以找到
【2】哈希属于内存数据库,即集合中的数据必须在内存中通过哈希的方式组织,才能准确的查找
【3】哈希表的插入/删除/查找时间复杂度是O(1)
哈希冲突
【1】什么是哈希冲突呢?就是同时有两个key要争夺同一个位置,从而造成了哈希冲突,而引起哈希冲突的一个可能是:哈希函数设计不合理
【2】哈希是以牺牲空间为代价,提高查询的效率,采用哈希处理时,一般所需空间都会比元素个数多,否则产生冲突的概率就比较大,影响哈希的性能
【3】因为哈希表是有限大小和不同键可能映射到相同值的情况(即鸽巢原理),所以哈希冲突是不可避免的
哈希函数设计原则
1.哈希函数的定义域必须包括需要存储的全部关键码,而如果散列表允许有m个地址,其值域必须在0到m-1之间
2.哈希函数计算出来的地址能均匀分布在整个空间中
3.哈希函数比较简单
4.哈希函数的值域必须在哈希表格的范围之内;哈希函数的值域应该尽可能均匀分布,即取每个位置应该是等概率的
**所以:**散列函数有一个共同性质,即函数值应按同等概率取其值域的每一个值。
常见哈希函数
哈希函数的选择和数据整体情况有关,与单个元素没有直接关联
1.直接定制法
取关键字的某个线性函数为散列地址:Hash(Key)=A*Key+B
优点:简单,均匀
缺点:需要事先知道关键字的分布情况
使用场景:适合查找比较小且连续的情况
2.除留余数法
设散列表中允许的地址数为m,取一个不大于m,但最接近或者等于m的质数p作为除数,按照哈希函数:Hash(key)=key%p(p<=m),将关键码转换成哈希地址
3. 平方取中法了解)
4. 折叠法(了解)
5. 随机数法(了解)
6. 数学分析法(了解)
避免哈希冲突
1.合理的哈希函数
2.负载因子——0.75
散列表的负载因子定义为:负载因子=填入表中的元素个数/散列表的长度
注意: 若是负载因子=1时,哈希冲突不是一定会产生,只有待插入元素通过哈希函数计算出要插入的位置,该位置上有数据时才会发生哈希冲突
解决哈希冲突
常见哈希冲突处理:闭散列(线性探测、二次探测)、开散列(链地址法)、多次散列
【1】开放定址法区分为:线性查找法、二次查找法、双重散列法等;开放定址法是指一旦发生了冲突,就去寻找下一个空的散列地址,所以其平均查找长度高于链接法处理冲突
【2】拉链法:拉链法解决冲突的做法是将所有关键字为同义词的节点链接在同一个单链表中;拉链法处理冲突简单,且无堆积现象,即非同义词绝不会发生冲突,因此平均查找长度较短
【3】用哈希(散列)方法处理冲突(碰撞)时可能出现堆积(聚集)现象,会受堆积现象直接影响的是平均查找长度,因为哈希方法冲突会使在查找冲突关键字时,还要根据冲突处理办法多次比较关键字,则直接影响了平均查找长度
闭散列
闭散列表最大的缺陷就是空间利用率比较低,这也是哈希的缺陷
线性探测
当下标为4的位置已经有了元素4,那么剩下的计算值下标也为4的元素44、14、54、64、74就不能往下标为4的位置放了,只能放在这个元素的后面位置,这就导致了把更多冲突元素放在了一起
**注意:**采用闭散列处理哈希冲突的时,不能随便物理删除哈希表中已有的元素,若直接删除元素会影响其他元素的搜索。比如删除元素4,如果直接删除掉,44查找起来可能会受影响,因此线性探测采用标记的伪删除法来删除一个元素,简单来说就是从哈希表删除一个记录,不是将记录的位置置空,而是设置一个标记,标记这个元素是无效的了
二次探测
线性探测的缺陷是产生冲突的数据堆积在一块,这与其找下一个空位置有关系,因为找空位置的方式就是挨着往后逐个去找,因此二次探测为了避免该问题,找下一个空位置的方法为:其中:i= 1,2,3;Ho是通过散列函数Hash(x)对元素的关键码 key 进行计算得到的位置,m是表的大小。
或者
开散列/哈希桶(重点!)
画图演示:
冲突严重时的解决办法
哈希桶可以看作将大集合的搜索问题转化为小集合的搜索问题,那如果冲突严重,就意味着小集合的搜索性能其实也是不佳的,这个时候我们就可以将这个所谓的小集合搜索问题继续进行转化,例如:
1.每个桶的背后是另一个哈希表
2.每个桶的背后是一棵搜索树
哈希桶的实现
代码展示:
public class HashBuck {
static class Node{
public int key;
public int val;
public Node next;
public Node(int key, int val) {
this.key = key;
this.val = val;
}
}
public Node[] array;
public int usedSize;
public HashBuck() {
array = new Node[10];
}
//尾插法
public void put(int key,int val){
int index=key% array.length;
Node node=new Node(key,val);
Node cur=array[index];
while(cur.next!=null){
cur=cur.next;
}
cur.next=node;
usedSize++;
}
//头插法
public void put2(int key,int val){
int index=key% array.length;
Node cur=array[index];
//先遍历一遍整体的链表,看是否存在与当前相同的key
//如果有的话要更新value后return,没有的话进行头插
while(cur!=null){
if(cur.key==key){
cur.val=val;
return;
}
cur=cur.next;
}
//没有当前这个key
Node node=new Node(key,val);
node.next=array[index];
array[index]=node;
usedSize++;
}
//扩容需要注意的事项是什么?
//把所有元素都要进行重新的哈希
private void resize(){
Node[] tmpArr=new Node[array.length*2];
//遍历原来的数组,将所有元素重新哈希到新的数组中
for (int i = 0; i < array.length ; i++) {
Node cur=array[i];
while(cur!=null) {
//记录当前结点的下个结点
Node curNext = cur.next;
int newIndex = cur.key % tmpArr.length;
//头插
cur.next=tmpArr[newIndex];
tmpArr[newIndex]=cur;
cur=curNext;
}
}
array=tmpArr;
}
private double loadFactor(){
return usedSize*1.0/array.length;
}
public int get(int key){
int index=key% array.length;
Node cur=array[index];
while(cur!=null){
if(cur.key==key){
return cur.val;
}
cur=cur.next;
}
return -1;
}
}
面试题目整理
1.你知道hashCode和equals的区别吗?
hashCode:定位下标
equals:遍历下边列表,比较key
举例:在HashMap中,比如我们要查询美景这个词语
用hashCode查询下标,找到美字,然后从美开头的词语里面用equals比较key,找出美景一词