文章目录
- 一、二叉搜索树
- 二、Map接口:Key-Value键值对容器
-
- [2.1 Map的特性](#2.1 Map的特性)
- [2.2 内部类:Map.Entry<K,V>](#2.2 内部类:Map.Entry<K,V>)
- [2.3 Map的常用方法](#2.3 Map的常用方法)
- [2.4 TreeMap与HashMap的区别](#2.4 TreeMap与HashMap的区别)
- 三、Set接口:无重复元素的集合
-
- [3.1 Set的特性](#3.1 Set的特性)
- [3.2 Set的常用方法](#3.2 Set的常用方法)
- [3.3 TreeSet与HashSet的区别](#3.3 TreeSet与HashSet的区别)
- 四、哈希表:HashMap与HashSet的底层
-
- [4.1 哈希表的核心思想](#4.1 哈希表的核心思想)
- [4.2 哈希冲突(Hash Collision)](#4.2 哈希冲突(Hash Collision))
- [4.3 哈希表的实现(简化版HashMap)](#4.3 哈希表的实现(简化版HashMap))
- [4.4 哈希表与Java类集的关联](#4.4 哈希表与Java类集的关联)
一、二叉搜索树
1.1 二叉搜索树的定义
二叉搜索树是一种具有排序特性的二叉树,满足以下规则:
- 若左子树不为空,左子树上所有节点的值均小于根节点的值;
- 若右子树不为空,右子树上所有节点的值均大于根节点的值;
- 左右子树也分别为二叉搜索树。
对于数组{5,3,4,1,7,8,2,6,0,9},构建的二叉搜索树如下:
5
/ \
3 7
/ \ / \
1 4 6 8
/ \ \
0 2 9
1.2 核心操作
(1)查找
查找逻辑遵循左小右大原则:
- 若根节点值等于目标值,返回当前节点
- 若目标值小于根节点值,在左子树中查找;
- 若目标值大于根节点值,在右子树中查找。
java
public TreeNode search(int key){
TreeNode cur = root;
while(cur!=null){
if(cur.val< key)
cur=cur.right;
else if(cur.val> key)
cur=cur.left;
else
return cur;
}
return null;
}
时间复杂度 :取决于树的高度,最优为完全二叉树的O(log₂N),最差为单支树的O(N)。
(2)插入
- 若树为空,直接将新节点作为根节点;
- 若树非空,按查找逻辑遍历,找到插入位置(父节点的左/右子树为空处);
- 若插入值小于父节点值,作为左子节点插入;否则作为右子节点插入。
java
public void insert(int key) {
if (root == null) {
root = new TreeNode(key);
return;
}
TreeNode cur = root;
TreeNode parent = root;
while (cur != null) {
if (cur.val > key) {
parent = cur;
cur = cur.left;
} else if (cur.val < key) {
parent = cur;
cur = cur.right;
}else {
return;
}
}
if (parent.val > key)
parent.left = new TreeNode(key);
else
parent.right = new TreeNode(key);
}
(3)删除
删除需分三种情况处理,保证删除后树的结构仍满足二叉搜索树规则:
-
情况1:待删除节点(cur)无左子树
若cur是根节点,根节点更新为cur的右子树;否则,将父节点(parent)的对应指针(左/右)指向cur的右子树。

-
情况2:待删除节点(cur)无右子树
逻辑与情况1对称,将父节点的对应指针指向cur的左子树。

-
情况3:待删除节点(cur)左右子树均存在
采用"替换法":在cur的右子树中找到中序遍历的第一个节点(即右子树中最小节点,称为后继节点),用该节点的值覆盖cur的值,再删除后继节点(后继节点必满足情况1或情况2)。

java
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(parent, cur);
break;
}
}
}
private void removeNode(TreeNode parent, TreeNode cur) {
//情况1
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;
}
//情况2
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;
}
//情况3
else{
TreeNode tmpParent = cur;
TreeNode tmp = cur.right;
//找到右树的最小节点
while(tmp.left!=null){
tmpParent = tmp;
tmp=tmp.left;
}
cur.val = tmp.val;
//删除右树最小节点
if(tmpParent.left==tmp)
tmpParent.left = tmp.right;
else if(tmpParent.right == tmp)
tmpParent.right = tmp.right;
}
}
1.3 性能瓶颈与改进方向
二叉搜索树的性能依赖于树的结构:若插入顺序有序(如1,2,3,4),会退化为单支树,此时查找/插入/删除的时间复杂度变为O(N),完全失去优势。
改进方案 :使用平衡二叉搜索树(如红黑树),通过颜色规则和旋转操作维持树的平衡,确保最坏情况下的时间复杂度仍为O(log₂N)。这也是TreeMap和TreeSet的底层实现。
二、Map接口:Key-Value键值对容器
Map是Java中存储键值对(Key-Value)的核心接口,其设计目标是支持高效的键查找、插入和删除。
2.1 Map的特性
- 不继承自
Collection接口,独立成为顶层接口; - Key唯一(不可重复),Value可重复;
- 支持Key的快速查找,底层实现决定查找效率;
- 常用实现类:
TreeMap(红黑树实现)、HashMap(哈希表实现)。 - Map没有实现
Iterable,实现的类不可以通过迭代器进行遍历。如果需要遍历,可以先调用entrySet()返回到Set中,再遍历:
java
Set<Map.Entry<String,Integer>> entrySet = treeMap.entrySet();
for(Map.Entry<String,Integer> entry : entrySet){
System.out.println("key: "+entry.getKey()+" value: "+ entry.getValue());
}
2.2 内部类:Map.Entry<K,V>
Map通过内部类Map.Entry存储单个键值对,提供了键值对的访问方法:
| 方法 | 功能 |
|---|---|
K getKey() |
返回当前Entry的Key |
V getValue() |
返回当前Entry的Value |
V setValue(V value) |
修改当前Entry的Value,返回旧值 |
注意:Map.Entry不提供修改Key的方法,若需修改Key,需先删除原键值对,再重新插入。
2.3 Map的常用方法
| 方法 | 功能描述 |
|---|---|
V get(Object key) |
根据Key获取Value,Key不存在返回null |
V getOrDefault(Object key, V defaultValue) |
根据Key获取Value,Key不存在返回默认值 |
V put(K key, V value) |
插入键值对,Key已存在则覆盖Value,返回旧Value |
V remove(Object key) |
删除Key对应的键值对,返回删除的Value |
Set<K> keySet() |
返回所有Key的集合(不可重复) |
Collection<V> values() |
返回所有Value的集合(可重复) |
Set<Map.Entry<K,V>> entrySet() |
返回所有键值对的集合 |
boolean containsKey(Object key) |
判断是否包含指定Key |
boolean containsValue(Object value) |
判断是否包含指定Value |
2.4 TreeMap与HashMap的区别
| 对比维度 | TreeMap | HashMap |
|---|---|---|
| 底层结构 | 红黑树(平衡二叉搜索树) | 哈希桶(数组+链表/红黑树) |
| 时间复杂度 | 插入/删除/查找:O(log₂N) |
插入/删除/查找:O(1)(平均情况) |
| 有序性 | 按Key自然排序(或自定义排序) | 无序 |
| Key限制 | 不可为null,需支持比较(实现Comparable或提供Comparator) |
可为null(仅允许一个null Key) |
| Value限制 | 可为null |
可为null(允许多个null Value) |
| 线程安全 | 不安全 | 不安全 |
| 适用场景 | 需要Key有序的场景(如排序统计) | 无需有序,追求高效读写的场景 |
三、Set接口:无重复元素的集合
Set接口继承自Collection,核心特性是"存储无重复的Key",底层实现依赖于Map(将Key作为Map的Key,Value使用一个默认空对象填充)。
3.1 Set的特性
- 仅存储Key,不存储Value;
- Key唯一(重复元素插入失败);
- Set实现了
Iterable,实现的类可以通过迭代器进行遍历。 TreeSet不可以插入null的key,而HashSet可以。- 常用实现类:
TreeSet(红黑树实现)、HashSet(哈希表实现)、LinkedHashSet(哈希表+双向链表,保留插入顺序)。
3.2 Set的常用方法
| 方法 | 功能描述 |
|---|---|
boolean add(E e) |
插入元素,重复元素返回false |
boolean contains(Object o) |
判断是否包含指定元素 |
boolean remove(Object o) |
删除指定元素,成功返回true |
int size() |
返回元素个数 |
Iterator<E> iterator() |
返回迭代器,用于遍历元素 |
void clear() |
清空集合 |
3.3 TreeSet与HashSet的区别
| 对比维度 | TreeSet | HashSet |
|---|---|---|
| 底层结构 | 红黑树 | 哈希桶(数组+链表/红黑树) |
| 时间复杂度 | 插入/删除/查找:O(log₂N) |
插入/删除/查找:O(1)(平均情况) |
| 有序性 | 按Key自然排序 | 无序(LinkedHashSet保留插入顺序) |
| Key限制 | 不可为null,需支持比较 |
可为null(仅允许一个null) |
| 去重逻辑 | 基于比较器(Comparable/Comparator) |
基于hashCode()和equals() |
| 适用场景 | 需要有序去重的场景 | 无需有序,追求高效去重的场景 |
四、哈希表:HashMap与HashSet的底层
哈希表(Hash Table)是一种"键值对映射"的数据结构,通过哈希函数将Key映射到存储地址,实现O(1)级别的高效读写,是HashMap和HashSet的底层实现。
4.1 哈希表的核心思想
理想的搜索场景是"无需比较,直接定位"。哈希表通过以下逻辑实现:
- 哈希函数(Hash Function):将Key转换为存储地址(如
hash(key) = key % 数组长度); - 插入元素:通过哈希函数计算地址,将键值对存入该地址;
- 查找元素:通过哈希函数计算地址,直接访问该地址获取元素。
示例:对于数据集合{1,4,5,6,7,9},哈希函数为hash(key) = key % 10(数组长度为10),存储结果如下:
| 数组索引 | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 |
|---|---|---|---|---|---|---|---|---|---|---|
| 存储元素 | - | 1 | - | - | 4 | 5 | 6 | 7 | - | 9 |
4.2 哈希冲突(Hash Collision)
(1)冲突的定义
当两个不同的Key通过哈希函数计算出相同的存储地址时,称为哈希冲突。
例如""Key=4和Key=44,4%10=4,44%10=4,会映射到同一地址)。
冲突是必然存在的------因为哈希表的数组长度有限,而Key的范围可能无限,无法避免不同Key映射到同一地址。我们能做的是"尽量降低冲突率"。
(2)冲突的避免
冲突避免的核心是"优化哈希函数"和"调节负载因子"。
① 哈希函数设计原则
- 定义域覆盖所有Key,值域在
[0, 数组长度-1]之间; - 计算结果均匀分布(减少冲突);
- 计算效率高(简单易实现)。
常用哈希函数
| 函数类型 | 实现逻辑 | 适用场景 |
|---|---|---|
| 直接定制法 | Hash(key) = A*key + B(线性函数) |
Key范围小且连续(如年龄) |
| 除留余数法 | Hash(key) = key % p(p为接近数组长度的质数) |
通用场景(HashMap采用类似逻辑) |
| 平方取中法 | 对Key平方后取中间几位 | 未知Key分布,Key位数较少 |
| 折叠法 | 将Key分割为若干部分,叠加求和后取模 | Key位数较多(如手机号) |
② 负载因子调节(重点)
负载因子(Load Factor)定义:负载因子 = 已存储元素个数 / 数组长度。
- 负载因子越大,数组越满,冲突率越高;
- 负载因子越小,数组越空,空间利用率越低。
结论:负载因子需控制在合理范围(java中HashMap默认0.75)。当负载因子超过阈值时,会触发数组扩容(通常扩容为原来的2倍),从而降低负载因子,减少冲突。
(3)冲突的解决
当冲突发生时,需通过特定方式处理,常用方案有"闭散列"和"开散列"。
① 闭散列(开放定址法)
逻辑:当地址冲突时,在数组中寻找下一个空位置存储元素。
- 线性探测:从冲突地址开始,依次向后查找空位置(如Key=44冲突后,查找索引5、6、7、8,找到空位置8存储);
- 缺陷:容易导致"数据堆积"(冲突元素集中在某一区域),降低查找效率;
- 二次探测 :改进线性探测,查找逻辑为
H i = (H0 ± i²) % 数组长度(i=1,2,3...),分散冲突元素,但空间利用率较低(负载因子需≤0.5)。
② 开散列(链地址法,重点)
逻辑:数组的每个位置(称为"桶")存储一个链表(数组长度超过64且链表长度超过8,转为红黑树),冲突的元素被加入同一个桶的链表中。
示例:Key=4和Key=44冲突后,都存入索引4的桶中,形成链表:
| 数组索引 | 0 | 1 | 2 | 3 | 4 | ... |
|---|---|---|---|---|---|---|
| 存储元素 | - | 1 | - | - | 4→44→null | ... |
开散列的优势:
- 空间利用率高(无需预留空位置);
- 冲突处理简单(仅在桶内链表操作);
- 性能稳定(HashMap采用此方案,当链表长度超过8时,转为红黑树,进一步优化查找效率)。
4.3 哈希表的实现(简化版HashMap)
以下是基于开散列(链地址法)的简化版哈希表实现,包含put(插入)、get(查找)和resize(扩容)操作:
java
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[] arr = new Node[10];
//已存节点个数
public int usedSize;
//负载因子阈值
public static final double DEFAULT_LOAD_FACTOR = 0.75f;
//插入
public void put(int key, int val){
int index = key% arr.length;
Node cur = arr[index];
while(cur!=null){
//key存在则替换val
if(cur.key==key){
cur.val=val;
return;
}
cur=cur.next;
}
//key不存在,新建节点
Node newNode = new Node(key,val);
//头插法
newNode.next = arr[index];
arr[index]=newNode;
usedSize++;
//检查负载因子
if(doLoadFactor() >= DEFAULT_LOAD_FACTOR){
resize();
}
}
//扩容
private void resize() {
Node[] newArr = new Node[arr.length*2];
for (int i = 0; i < arr.length; i++) {
Node cur = arr[i];
while(cur!=null){
int index = cur.key % newArr.length;
//记录cur的下一个节点,防止后面修改cur.next后找不到原来的节点
Node curNext = cur.next;
//头插法
cur.next = newArr[index];
newArr[index]=cur;
cur = curNext;
}
}
arr = newArr;
}
//计算负载因子
private double doLoadFactor(){
return usedSize*1.0/ arr.length;
}
//根据key获取val
public int getValue(int key){
int index = key % arr.length;
Node cur = arr[index];
while(cur!=null){
if(cur.key==key)
return cur.val;
cur=cur.next;
}
return -1;
}
}
注意: 扩容不可以直接把原来的数组复制到新数组中,因为哈希地址会随着数组长度改变,必须遍历每一个元素,重新分配地址。
对于引用类型的数据:
- 重写
hashCode()方法,保证逻辑上认为相等的对象hashcode也相等;再调用hashcode()得到这个对象的hashcode,再计算哈希地址。 - 判断key是否相等时,不可以用
==来判断,而是要调用重写的equals()方法。
例如:
java
import java.util.Objects;
public class Student {
int id;
String name;
@Override
public boolean equals(Object o) {
if (o == null || getClass() != o.getClass()) return false;
Student student = (Student) o;
return id == student.id && Objects.equals(name, student.name);
}
@Override
public int hashCode() {
//当id和name相等时,hashcode也相等
return Objects.hash(id, name);
}
}
4.4 哈希表与Java类集的关联
- HashMap/HashSet的底层实现:均基于哈希表(开散列),HashSet本质是"Key为元素、Value为默认空对象的HashMap";
- 冲突处理:JDK8中,当桶内链表长度超过8时,自动转为红黑树;当长度小于6时,转回链表(平衡时间和空间效率);
- 自定义Key的要求 :若使用自定义类作为HashMap的Key或HashSet的元素,必须覆写
hashCode()和equals()方法,且需满足:equals()返回true的两个对象,hashCode()必须相等;hashCode()相等的两个对象,equals()不一定返回true(避免误判为相同元素)。