目录
1. 搜索树
1.1 概念
1.2 查找
1.3 插入
1.4 删除节点
1.5 代码实现自建平衡二叉树
1.6 二叉搜索树和TreeMap、TreeSet的关系
2. Map和Set
2.1 搜索
2.2 模型
2.3 Map 与 Set 的区别与联系
2.3.1从接口框架的角度分析
2.3.2 从存储的模型角度分析
3.Map 的详解
3.1 Map的注意事项
3.2 Map 的常用方法说明
3.3 测试Map的方法
3.4 Map.Entry的用法 (遍历),v>
3.5 TreeMap和HashMap的比较
4.Set 的详解
4.1 Set的注意事项
4.2 Set的常用方法
4.2 Tree 和 TreeMap 的区别
5. 哈希表
5.1 概念
5.2 冲突
5.3 冲突-避免
5.4 冲突-避免-负载因子调节(重点掌握)
5.5 冲突-解决-开散列/哈希桶(重点掌握)
5.6 冲突严重时的解决办法
5.7 自定义一个哈希桶
5.8 哈希表与Java集合类的关系
二叉搜索树又称为二叉排序树,可以为一个空树,也可以满足一下性质:
1.若它的左子树不为空,则左子树上所有节点的值都小于根节点的值
2.若它的右子树不为空,则右子树上所有节点的值都大于根节点的值
3.它的左右子树也分别为二叉搜索树
下图就是一个二叉搜索树
二叉搜索树元素的查找
1.首先判断根节点不为空
2.根节点不为空,进行在搜索树种进行查找
2.1 判断key == 等于查找的 key值,返回true
2.2 根节点的key值 > 查找的key值 在左子树进行查找
2.3 根节点的key值 < 查找的key值 在右子树进行查找
1.先判断是否为空树,失控书直接创建新的节点插入为根节点
2.不是空树按照指定的位置进行插入,比较与根节点的大小。
这里需要注意的是,插入的节点一定会是叶子结点
设置删除节点为cur,待删除节点的双亲节点为 parent 。
一共有七种情况,整体可以分为三种情况
1.cur.left == null
1.1 cur 是root节点 ,则root = cur.right
1.2 cur 不是根节点,cur = parent.right 则parent.right = cur.right
1.3 cur 不是根节点,cur = parent.left 则parent.left = 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
此时需要考虑的情况比较特殊
需要使用替换法进行删除,即在它的右子树中寻找中序下的第一个结点(关键码最小),用它的值填补到被删除节点中,再来处理该结点的删除问题 。
第一种情况targetP.left = target
第二种情况targetP.right = target
package BinarySearchTree;
/**
* Created with IntelliJ IDEA.
* Description:二叉搜索树
* User: YAO
* Date: 2023-04-03
* Time: 14:50
*/
public class BinarySearchTree {
static class TreeNode{
public int val;
public TreeNode left;
public TreeNode right;
public TreeNode(int val){
this.val = val;
}
}
public TreeNode root = null;
/**
* 查找二叉搜索树的指定的val值
*/
public TreeNode find(int val){
if (root == null){
return null;
}
TreeNode cur = root;
while (cur != null){
if (val > cur.val){
cur = cur.right;
}
else if (val < cur.val){
cur = cur.left;
}else {
return cur;
}
}
return null;
}
/**
* 2.向二叉搜索树插入指定的val值
* 不管插入什么值都会插入到叶子结点
*/
public void insert(int val){
if (root == null){
root = new TreeNode(val);
return ;
}
TreeNode prev = root;
TreeNode cur = root;
// 找到插入的位置prev
while (cur != null){
if (val > cur.val){
//记录cur走过的节点
prev = cur;
cur = cur.right;
}else if (val < cur.val){
prev = cur;
cur = cur.left;
}else{
return;
}
}
// 比较插入点的值与插入的值
TreeNode node = new TreeNode(val);
if (prev.val < val){
//比插入点的位置值大插入到右边
prev.right = node;
}else {
//比插入点的位置值小插入到右边
prev.left = node;
}
}
/**
*3.中序遍历
*/
public void inOder(TreeNode root){
if (root == null){
return;
}
inOder(root.left);
System.out.print(root.val+" ");
inOder(root.right);
}
/**
* 4.删除值为val的节点
*/
public void remove(int val){
TreeNode cur = root;
TreeNode parent = root;
while (cur != null){
if (cur.val == val){
removeNode(parent,cur);
return;
}else if(cur.val > val){
parent = cur;
cur = cur.left;
}else {
parent = cur;
cur = cur.right;
}
}
}
private void removeNode(TreeNode parent, TreeNode cur) {
// parent 代表的是cur走过的节点,也是cur的父亲节点
if (cur.left == null){
// 删除节点cur左边为空
if (root == null){
root = cur.right;
}else {
if (parent.left == cur){
parent.left = cur.right;
}else {
parent.right = cur.right;
}
}
}else if(cur.right == null){
// 删除节点cur右边为空
if (root == null){
root = cur.left;
}else {
if (parent.left == cur){
parent.left = cur.left;
}else {
parent.right = cur.left;
}
}
}else {
// 删除节点左右都不为空
//找到删除节点右子树的最小值,进行覆盖cur节点,然后删除右子树最小值的节点
TreeNode targetParent = cur;
TreeNode target = cur.right;
while (target.left != null){
//1.如果删除节点的右子树的左子树不为空,那么删除节点右子树的最小值一定在这上面
targetParent = target;
target = target.left;
//找到替罪羊(删除节点右子树中最小的元素)
}
//找到删除节点右子树的最小值,将该最小值赋值给删除节点的值
//此时有两种情况,
// 1.删除节点右子树的第一个元素没有左子树,则删除节点的右子树的第一节点就是最小值
// 2.删除节点的右子树的第一个元素含有左子树,就一直找到该左子树的最末尾的左节点为最小值
cur.val = target.val;
//删除替罪羊(删除节点右子树中的最小值)
if (target == targetParent.left){
// 属于上面的第2种情况
targetParent.left = target.right;
}
if (target == targetParent.right){
// 属于上面的第1种情况
targetParent.right = target.right;//target.right 不一定是不为空的
}
}
}
}
TreeMap和TreeSet是Java种利用搜索树实现的Map和Set,实际上用的是红黑树,红黑树是一颗近似平衡的二叉搜索树,即在二叉搜索树的基础之上+颜色以及红黑树性质。
Map和set是一种专门用来进行搜索的容器或者数据结构,其搜索的效率与其具体的实例化子类有关。
搜索方式有:
1. 直接遍历,时间复杂度为O(N),元素如果比较多效率会非常慢
2. 二分查找,时间复杂度为 ,但搜索前必须要求序列是有序的
上述排序比较适合静态类型的查找,即一般不会对区间进行插入和删除操作了,而现实中的查找比如:
1. 根据姓名查询考试成绩
2. 通讯录,即根据姓名查询联系方式
3. 不重复集合,即需要先搜索关键字是否已经在集合中
可能在查找时进行一些插入和删除的操作,即动态查找,那上述两种方式就不太适合了,本节介绍的Map和Set是
一种适合动态查找的集合容器。
一般把搜索的数据称为关键字(Key),和关键字对应的称为值(Value),将其称之为Key-value的键值对,所以
模型会有两种:
1. 纯 key 模型,比如:
有一个英文词典,快速查找一个单词是否在词典中
快速查找某个名字在不在通讯录中
2. Key-Value 模型,比如:
统计文件中每个单词出现的次数,统计结果是每个单词都有与其对应的次数:<单词,单词出现的次数>
梁山好汉的江湖绰号:每个好汉都有自己的江湖绰号
而Map中存储的就是key-value的键值对,Set中只存储了Key。
Map是一个独立的接口,而Set继承自Collection接口。
Map是一个接口类,该类没有继承collection,该类中存储的是
public class mapTest {
public static void main(String[] args) {
//1.Map是一个key-value 模型
// 搜索树
Map treeMap = new TreeMap<>();
//2.向二叉搜索树里面放数据(根据k进行比较大小)
treeMap.put("d",3);
treeMap.put("a",4);
treeMap.put("c",3);
//3.打印二叉搜索树
System.out.println(treeMap);
//4.根据key返回val值
Integer val1 = treeMap.get("b");
Integer val2 = treeMap.get("f"); //没有这个值进行返回null
//5.如果没有发现值,可以指定返回的内容
Integer val3 = treeMap.getOrDefault("f",100);
//6.接收TreeMap里面所有的key值,不一定是有序的
Set keySet = treeMap.keySet();
System.out.println(keySet);
//7.接收TreeMap里面所有的val值,不一定是有序的
Collection valCollection = treeMap.values();
System.out.println(valCollection);
//8.是否包含key值,val值
System.out.println(treeMap.containsKey("a"));
System.out.println(treeMap.containsValue(3));
//9.key_value映射关系Set> entrySet()
Set> set = treeMap.entrySet();
for (Map.Entry entry : set) {
System.out.println(entry.getKey() + " " + entry.getValue());
}
}
}
Map.Entry
方法 | 解释 |
K getKey() | 返回 entry 中的 key |
V getValue() | 返回 entry 中的 value |
V setValue(V value) | 将键值对中的value替换为指定value |
注意:Map.Entry
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可以。
Set是可以使用迭代器的,但是Map不可以
顺序结构以及平衡树中,元素关键码与其存储位置之间没有对应的关系,因此在查找一个元素时,必须要经过关键码的多次比较。顺序查找时间复杂度为O(N),平衡树中为树的高度,即O(logN),搜索的效率取决于搜索过程中元素的比较次数。
理想的搜索方法:可以不经过任何比较,一次直接从表中得到要搜索的元素。 如果构造一种存储结构,通过某种函数(hashFunc)使元素的存储位置与它的关键码之间能够建立一一映射的关系,那么在查找时通过该函数可以很快找到该元素。
1. 插入元素
根据待插入元素的关键码,以此函数计算出该元素的存储位置并按此位置进行存放
2. 搜索元素
对元素的关键码进行同样的计算,把求得的函数值当做元素的存储位置,在结构中按此位置取元素比较,若关键码相等,则搜索成功
该方式即为哈希(散列)方法,哈希方法中使用的转换函数称为哈希(散列)函数,构造出来的结构称为哈希表(HashTable)(或者称散列表。
哈希函数设置为:hash(key) = key % capacity; capacity为存储元素底层空间总的大小。
引起哈希冲突的一个原因可能是:哈希函数设计不够合理。 哈希函数设计原则:
常见哈希函数
面试题:给定一个字符串 s
,找到 它的第一个不重复的字符,并返回它的索引 。如果不存在,则返回 -1
。
力扣
这就是利用的第一种哈希函数。
class Solution {
public int firstUniqChar(String s) {
int[] freq = new int[26];
char[] chars = s.toCharArray();
for (char ch : chars) {
freq[ch - 'a']++;
}
for (int i = 0; i < chars.length; i++) {
if (freq[chars[i] - 'a'] == 1) {
return i;
}
}
return -1; return -1;
}
}
所以当冲突率达到一个无法忍受的程度时,我们需要通过降低负载因子来变相的降低冲突率。已知哈希表中已有的关键字个数是不可变的,那我们能调整的就只有哈希表中的数组的大小。
开散列,可以认为是把一个在大集合中的搜索问题转化为在小集合中做搜索了。
刚才我们提到了,哈希桶其实可以看作将大集合的搜索问题转化为小集合的搜索问题了,那如果冲突严重,就意味着小集合的搜索性能其实也时不佳的,这个时候我们就可以将这个所谓的小集合搜索问题继续进行转化,例如:
1. 每个桶的背后是另一个哈希表
2. 每个桶的背后是一棵搜索树
package HashBuck;
/**
* Created with IntelliJ IDEA.
* Description:哈希桶 泛型版本
* 面试问题:
* hashCode一样,equals一样吗?
* 答案:不一定 原因:hashCode一样只能证明我在数组上的位置是一样的,而这个位置是一个列表,包含了很多元素,所以不一定一样
* equals一样,hashCode一样吗?
* 答案:一定
* User: YAO
* Date: 2023-04-04
* Time: 14:20
*/
public class hashBuck2 {
static class Node{
public K key;
public V value;
public Node next;
public Node(K key, V value) {
this.key = key;
this.value = value;
}
}
public Node[] array = (Node[]) new Node[10];
public static double LOAD_FACTOR = 0.75;
public int usedSize;
/**
* 1.往哈希桶里追加元素
*/
public void put(K key, V value){
// 1. 获取哈希编码
int hash = key.hashCode();
// 2. 根据哈希编码获取哈希地址
int index = hash % array.length;
// 3. 定义节点,传入到相应的哈希地址的列表里面
Node cur = array[index];
// 4.遍历当前当前下标的列表,如果找到key值,进行修改传入的val值
while (cur!=null){
if (cur.key.equals(key)){
cur.value = value;
return;
}
cur = cur.next;
}
// 5. 在数组里面没有找到相应的元素,即数组里面没有插入的元素,进行头插法,插入到相应的位置
Node node = new Node<>(key,value);
// 5.1 头插法进行插入
node.next = array[index];
array[index] = node;
// 5.2插入完成,usedSize进行加加
usedSize++;
// 6.判断此时的负载因子是否超过规定的负载因子,如果超过就要进行扩容
if (calculateLoadedFactor() >= LOAD_FACTOR){
//扩容
resize();//注意映射地址发生改变,要进行重新哈希
}
}
private double calculateLoadedFactor(){
return usedSize * 1.0 /array.length;
}
/**
* 扩容
* 需要注意:
* 1.地址映射不在原来的位置了,需要重新哈希
*/
private void resize(){
//1. 创建新的数组
Node[] newArray = (Node[])new Node[2*array.length];
//2. 遍历原数组的下标,对相应下标元素进行重新哈希
for (int i = 0; i < array.length; i++) {
Node cur = array[i];
while (cur != null){
// 2.1 重新哈希
// 2.2 对当前元素进行计算哈希编码
int hash = cur.key.hashCode();
// 2.3 计算新的哈希地址
int index = hash % newArray.length;
// 2.4 记录当前节点的下一个节点的地址,否则当前节点进行头插法到新的数组里面会丢失当前节点下一个节点的地址
Node curNext = cur.next;
// 2.5 头插法到扩容后的数组
cur.next = newArray[index];
newArray[index] = cur;
// 2.6 cur指向原来下标的列表的下一个节点,对此节点进行重新哈希
cur = curNext;
}
}
array = newArray;
}
/**
* 2.根据key值获取val值
*/
public V get(K key){
// 1.获取当前元素的哈希编码
int hash = key.hashCode();
int index = hash % array.length;
Node cur = array[index];
while (cur!=null){
if (cur.key.equals(key)){
return cur.value;
}
cur = cur.next;
}
return null;
}
}