目录
[1.1 二叉搜索树的概念与特性](#1.1 二叉搜索树的概念与特性)
[1.2 二叉搜索树的基本操作](#1.2 二叉搜索树的基本操作)
[1.3 二叉搜索树的性能分析](#1.3 二叉搜索树的性能分析)
[1.4 红黑树:自平衡的二叉搜索树](#1.4 红黑树:自平衡的二叉搜索树)
[2.1 搜索的两种模型](#2.1 搜索的两种模型)
[2.2 搜索算法对比](#2.2 搜索算法对比)
[3.1 Map接口概述](#3.1 Map接口概述)
[3.2 Map的核心实现类](#3.2 Map的核心实现类)
[3.3 HashMap vs TreeMap 详细对比](#3.3 HashMap vs TreeMap 详细对比)
[3.4 Map的常用方法详解](#3.4 Map的常用方法详解)
[3.5 Map.Entry内部类](#3.5 Map.Entry内部类)
[4.1 Set接口概述](#4.1 Set接口概述)
[4.2 Set的核心实现类](#4.2 Set的核心实现类)
[4.3 HashSet vs TreeSet 详细对比](#4.3 HashSet vs TreeSet 详细对比)
[4.4 Set的常用方法](#4.4 Set的常用方法)
[4.5 Set的实际应用场景](#4.5 Set的实际应用场景)
[5.1 哈希表的基本原理](#5.1 哈希表的基本原理)
[5.2 哈希函数的设计](#5.2 哈希函数的设计)
[5.3 哈希冲突的解决方案](#5.3 哈希冲突的解决方案)
[方法1:链地址法(Java HashMap采用)](#方法1:链地址法(Java HashMap采用))
[5.4 负载因子与扩容](#5.4 负载因子与扩容)
[5.5 Java 8+的优化:链表转红黑树](#5.5 Java 8+的优化:链表转红黑树)
[6.1 时间复杂度对比](#6.1 时间复杂度对比)
[6.2 内存使用分析](#6.2 内存使用分析)
[6.3 选择合适的数据结构](#6.3 选择合适的数据结构)
[7.1 自定义对象作为键](#7.1 自定义对象作为键)
[7.2 并发场景下的Map](#7.2 并发场景下的Map)
[7.3 内存敏感场景的优化](#7.3 内存敏感场景的优化)
[8.1 HashMap的工作原理](#8.1 HashMap的工作原理)
[8.2 HashMap和Hashtable的区别](#8.2 HashMap和Hashtable的区别)
[8.3 ConcurrentHashMap的实现原理](#8.3 ConcurrentHashMap的实现原理)
[8.4 如何设计一个好的hashCode()方法](#8.4 如何设计一个好的hashCode()方法)
前言
在日常编程中,我们经常需要处理各种数据查找问题。比如:
-
根据学生姓名查找成绩
-
检查一个单词是否在词典中存在
-
统计一篇文章中每个单词出现的次数
-
去重操作,确保集合中没有重复元素
传统的数据结构如数组和链表在查找时需要遍历整个集合,时间复杂度为O(n),效率低下。而Map和Set提供了高效的数据存储和查找方案,是现代编程中不可或缺的数据结构。
一、搜索树:有序存储的基石
1.1 二叉搜索树的概念与特性
二叉搜索树(Binary Search Tree,BST)是一种特殊的二叉树,具有以下性质:
若左子树不为空,则左子树上所有节点的值都小于根节点的值
若右子树不为空,则右子树上所有节点的值都大于根节点的值
左右子树也分别为二叉搜索树
java
// 二叉搜索树节点定义
class TreeNode {
int val;
TreeNode left;
TreeNode right;
TreeNode(int val) {
this.val = val;
}
}
1.2 二叉搜索树的基本操作
查找操作
java
public class BinarySearchTree {
private TreeNode root;
// 查找元素
public TreeNode search(int key) {
TreeNode cur = root;
while (cur != null) {
if (key == cur.val) {
return cur; // 找到
} else if (key < cur.val) {
cur = cur.left; // 在左子树中查找
} else {
cur = cur.right; // 在右子树中查找
}
}
return null; // 未找到
}
}
插入操作
java
public boolean insert(int key) {
if (root == null) {
root = new TreeNode(key);
return true;
}
TreeNode cur = root;
TreeNode parent = null;
// 寻找插入位置
while (cur != null) {
parent = cur;
if (key == cur.val) {
return false; // 元素已存在
} else if (key < cur.val) {
cur = cur.left;
} else {
cur = cur.right;
}
}
// 插入新节点
TreeNode newNode = new TreeNode(key);
if (key < parent.val) {
parent.left = newNode;
} else {
parent.right = newNode;
}
return true;
}
删除操作(最复杂)
删除操作需要考虑三种情况:
待删除节点为叶子节点
待删除节点只有一个子节点
待删除节点有两个子节点
java
public boolean remove(int key) {
TreeNode cur = root;
TreeNode parent = null;
// 查找待删除节点
while (cur != null) {
if (key == cur.val) {
break;
}
parent = cur;
if (key < cur.val) {
cur = cur.left;
} else {
cur = cur.right;
}
}
if (cur == null) {
return false; // 未找到
}
// 情况1:有两个子节点
if (cur.left != null && cur.right != null) {
// 找到右子树中的最小节点
TreeNode minParent = cur;
TreeNode minNode = cur.right;
while (minNode.left != null) {
minParent = minNode;
minNode = minNode.left;
}
// 用最小节点的值替换当前节点值
cur.val = minNode.val;
// 转换为删除minNode(此时minNode最多有一个右子节点)
cur = minNode;
parent = minParent;
}
// 情况2和3:有一个子节点或没有子节点
TreeNode child = null;
if (cur.left != null) {
child = cur.left;
} else if (cur.right != null) {
child = cur.right;
}
if (parent == null) {
root = child; // 删除根节点
} else if (parent.left == cur) {
parent.left = child;
} else {
parent.right = child;
}
return true;
}
1.3 二叉搜索树的性能分析
二叉搜索树的性能高度依赖于树的结构:
-
最佳情况:完全平衡的二叉树,查找时间复杂度为O(log n)
-
最坏情况:退化为链表,查找时间复杂度为O(n)
java
最佳情况(平衡树):
8
/ \
4 12
/ \ / \
2 6 10 14
查找路径长度:O(log n)
最坏情况(单支树):
2
\
4
\
6
\
8
查找路径长度:O(n)
1.4 红黑树:自平衡的二叉搜索树
为了解决二叉搜索树可能退化为链表的问题,Java的TreeMap和TreeSet使用了红黑树(Red-Black Tree)。红黑树是一种自平衡的二叉搜索树,它通过以下规则确保树的近似平衡:
每个节点要么是红色,要么是黑色
根节点是黑色
每个叶子节点(NIL节点)是黑色
红色节点的两个子节点都是黑色
从任一节点到其每个叶子节点的所有路径都包含相同数目的黑色节点
这些规则保证了红黑树的关键特性:从根到叶子的最长路径不会超过最短路径的两倍。
二、搜索的数学模型
2.1 搜索的两种模型
在实际应用中,搜索问题通常分为两种模型:
Key-Value模型
存储键值对,根据键查找值。典型应用:
-
字典:根据单词查找释义
-
电话簿:根据姓名查找电话
-
缓存系统:根据键查找缓存值
java
// Key-Value模型示例
Map<String, String> dictionary = new HashMap<>();
dictionary.put("apple", "苹果");
dictionary.put("banana", "香蕉");
String meaning = dictionary.get("apple"); // 返回"苹果"
纯Key模型
只存储键,检查键是否存在。典型应用:
-
屏蔽词过滤:检查某个词是否在屏蔽列表中
-
用户ID集合:检查用户ID是否已注册
-
黑名单系统:检查IP是否在黑名单中
java
// 纯Key模型示例
Set<String> stopWords = new HashSet<>();
stopWords.add("的");
stopWords.add("了");
stopWords.add("在");
boolean contains = stopWords.contains("的"); // 返回true
2.2 搜索算法对比
| 搜索方法 | 时间复杂度 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|---|
| 线性查找 | O(n) | 实现简单 | 效率低 | 数据量小,无序数据 |
| 二分查找 | O(log n) | 效率高 | 需要有序数组 | 静态数据,很少插入删除 |
| 哈希查找 | O(1) | 查找最快 | 需要额外空间,可能冲突 | 动态数据,频繁查找 |
| 树查找 | O(log n) | 有序,平衡 | 实现复杂 | 需要有序数据,频繁插入删除 |
三、Map:键值对映射
3.1 Map接口概述
Map是Java中用于存储键值对的接口,主要特点:
-
键必须是唯一的,值可以重复
-
一个键最多映射到一个值
-
提供了丰富的集合视图方法
java
// Map的基本使用
Map<String, Integer> studentScores = new HashMap<>();
studentScores.put("Alice", 95);
studentScores.put("Bob", 87);
studentScores.put("Charlie", 92);
// 获取值
int score = studentScores.get("Alice"); // 95
// 检查键是否存在
boolean hasBob = studentScores.containsKey("Bob"); // true
// 遍历所有键值对
for (Map.Entry<String, Integer> entry : studentScores.entrySet()) {
System.out.println(entry.getKey() + ": " + entry.getValue());
}
3.2 Map的核心实现类
HashMap
-
基于哈希表实现
-
允许null键和null值
-
不保证顺序
-
查找、插入、删除的时间复杂度为O(1)
TreeMap
-
基于红黑树实现
-
键不允许为null
-
按键的自然顺序或自定义顺序排序
-
查找、插入、删除的时间复杂度为O(log n)
LinkedHashMap
-
继承自HashMap
-
维护插入顺序或访问顺序
-
适合需要保持顺序的场景
3.3 HashMap vs TreeMap 详细对比
| 特性 | HashMap | TreeMap |
|---|---|---|
| 底层结构 | 数组+链表/红黑树 | 红黑树 |
| 排序 | 无序 | 按键自然顺序或Comparator排序 |
| null键 | 允许一个null键 | 不允许null键 |
| null值 | 允许多个null值 | 允许多个null值 |
| 时间复杂度 | O(1)(平均) | O(log n) |
| 空间复杂度 | 较高(有负载因子) | 较低(只有节点) |
| 线程安全 | 不安全 | 不安全 |
| 使用场景 | 快速查找,不关心顺序 | 需要有序遍历,范围查找 |
3.4 Map的常用方法详解
java
public class MapExamples {
public static void main(String[] args) {
Map<String, Integer> map = new HashMap<>();
// 1. 添加元素
map.put("apple", 1);
map.put("banana", 2);
map.put("orange", 3);
// 2. 获取元素
Integer value = map.get("apple"); // 1
Integer defaultValue = map.getOrDefault("grape", 0); // 0
// 3. 检查包含
boolean hasKey = map.containsKey("banana"); // true
boolean hasValue = map.containsValue(2); // true
// 4. 遍历
// 遍历键
for (String key : map.keySet()) {
System.out.println("Key: " + key);
}
// 遍历值
for (Integer val : map.values()) {
System.out.println("Value: " + val);
}
// 遍历键值对
for (Map.Entry<String, Integer> entry : map.entrySet()) {
System.out.println(entry.getKey() + ": " + entry.getValue());
}
// 5. 删除
map.remove("orange"); // 删除键为"orange"的映射
map.remove("apple", 1); // 仅当键为"apple"且值为1时才删除
// 6. 大小和清空
int size = map.size(); // 当前元素个数
map.clear(); // 清空所有元素
}
}
3.5 Map.Entry内部类
Map.Entry是Map接口的内部接口,表示一个键值对:
java
public interface Map<K,V> {
interface Entry<K,V> {
K getKey();
V getValue();
V setValue(V value);
// 比较两个Entry是否相等
boolean equals(Object o);
// 计算哈希码
int hashCode();
}
}
使用示例:
java
Map<String, Integer> map = new HashMap<>();
map.put("A", 1);
map.put("B", 2);
for (Map.Entry<String, Integer> entry : map.entrySet()) {
String key = entry.getKey();
Integer value = entry.getValue();
entry.setValue(value * 2); // 修改值
}
四、Set:不重复元素的集合
4.1 Set接口概述
Set是Java中用于存储不重复元素的集合,主要特点:
-
不允许重复元素(基于equals()方法判断)
-
最多允许一个null元素(取决于具体实现)
-
没有get()方法,因为Set不是键值对结构
4.2 Set的核心实现类
HashSet
-
基于HashMap实现
-
使用对象的hashCode()确定存储位置
-
不保证顺序
-
允许null元素
TreeSet
-
基于TreeMap实现
-
元素按自然顺序或Comparator排序
-
不允许null元素
-
提供了一系列有序操作方法
LinkedHashSet
-
继承自HashSet
-
维护插入顺序
-
性能略低于HashSet
4.3 HashSet vs TreeSet 详细对比
| 特性 | HashSet | TreeSet |
|---|---|---|
| 底层结构 | HashMap | TreeMap |
| 排序 | 无序 | 自然顺序或Comparator排序 |
| null元素 | 允许一个null | 不允许null |
| 时间复杂度 | O(1)(平均) | O(log n) |
| 元素要求 | 需要正确实现hashCode()和equals() | 需要实现Comparable或提供Comparator |
| 使用场景 | 快速去重,不关心顺序 | 需要有序集合,范围查询 |
4.4 Set的常用方法
java
public class SetExamples {
public static void main(String[] args) {
// 1. 创建Set
Set<String> hashSet = new HashSet<>();
Set<String> treeSet = new TreeSet<>();
Set<String> linkedHashSet = new LinkedHashSet<>();
// 2. 添加元素
hashSet.add("apple");
hashSet.add("banana");
hashSet.add("orange");
hashSet.add("apple"); // 重复,不会被添加
// 3. 检查包含
boolean hasApple = hashSet.contains("apple"); // true
// 4. 删除元素
hashSet.remove("banana");
// 5. 集合运算
Set<String> set1 = new HashSet<>(Arrays.asList("A", "B", "C"));
Set<String> set2 = new HashSet<>(Arrays.asList("B", "C", "D"));
// 并集
Set<String> union = new HashSet<>(set1);
union.addAll(set2); // [A, B, C, D]
// 交集
Set<String> intersection = new HashSet<>(set1);
intersection.retainAll(set2); // [B, C]
// 差集
Set<String> difference = new HashSet<>(set1);
difference.removeAll(set2); // [A]
// 6. 遍历
for (String element : hashSet) {
System.out.println(element);
}
// 7. 转换为数组
String[] array = hashSet.toArray(new String[0]);
// 8. 清空和大小
int size = hashSet.size();
hashSet.clear();
boolean isEmpty = hashSet.isEmpty(); // true
}
}
4.5 Set的实际应用场景
场景1:去重
java
// 去除列表中的重复元素
List<Integer> numbers = Arrays.asList(1, 2, 2, 3, 3, 3, 4, 5, 5);
Set<Integer> uniqueNumbers = new HashSet<>(numbers);
// uniqueNumbers: [1, 2, 3, 4, 5]
场景2:集合运算
java
// 找出两个列表的共同元素
List<String> list1 = Arrays.asList("A", "B", "C", "D");
List<String> list2 = Arrays.asList("C", "D", "E", "F");
Set<String> set1 = new HashSet<>(list1);
Set<String> set2 = new HashSet<>(list2);
// 交集
set1.retainAll(set2); // [C, D]
场景3:检查元素存在性
java
// 快速检查黑名单
Set<String> blacklist = new HashSet<>();
blacklist.add("spam@example.com");
blacklist.add("fraud@example.com");
String email = "user@example.com";
if (blacklist.contains(email)) {
System.out.println("拒绝访问");
} else {
System.out.println("允许访问");
}
五、哈希表:高效查找的核心
5.1 哈希表的基本原理
哈希表通过哈希函数将键映射到数组的特定位置,实现快速查找:
哈希表工作原理:
计算键的哈希码:hashCode = key.hashCode()
计算数组索引:index = hashCode % array.length
在数组的index位置存储值
5.2 哈希函数的设计
好的哈希函数应该具备以下特性:
-
确定性:相同的键总是产生相同的哈希值
-
高效性:计算速度快
-
均匀性:哈希值均匀分布,减少冲突
Java中Object类的hashCode()方法:
java
public class Object {
public native int hashCode();
public boolean equals(Object obj);
}
5.3 哈希冲突的解决方案
方法1:链地址法(Java HashMap采用)
将哈希到同一位置的元素用链表连接起来:
java
// 简化版HashMap实现
public class SimpleHashMap<K, V> {
private static class Node<K, V> {
K key;
V value;
Node<K, V> next;
Node(K key, V value) {
this.key = key;
this.value = value;
}
}
private Node<K, V>[] table;
private int size;
private static final float LOAD_FACTOR = 0.75f;
// 获取值
public V get(K key) {
int index = hash(key) % table.length;
Node<K, V> node = table[index];
while (node != null) {
if (node.key.equals(key)) {
return node.value;
}
node = node.next;
}
return null;
}
// 插入值
public V put(K key, V value) {
int index = hash(key) % table.length;
// 检查是否已存在
Node<K, V> node = table[index];
while (node != null) {
if (node.key.equals(key)) {
V oldValue = node.value;
node.value = value;
return oldValue;
}
node = node.next;
}
// 插入新节点
Node<K, V> newNode = new Node<>(key, value);
newNode.next = table[index];
table[index] = newNode;
size++;
// 检查是否需要扩容
if ((float)size / table.length > LOAD_FACTOR) {
resize();
}
return null;
}
private int hash(K key) {
return key == null ? 0 : key.hashCode();
}
private void resize() {
// 扩容并重新哈希所有元素
}
}
方法2:开放地址法
当发生冲突时,寻找下一个空位置:
线性探测:index = (hash + i) % size, i=1,2,3,...
二次探测:index = (hash + i²) % size, i=1,2,3,...
双重哈希:使用第二个哈希函数
5.4 负载因子与扩容
负载因子 = 元素数量 / 哈希表容量
Java HashMap的默认参数:
-
初始容量:16
-
负载因子:0.75
-
扩容阈值:容量 × 负载因子
java
// HashMap扩容机制
void addEntry(int hash, K key, V value, int bucketIndex) {
if ((size >= threshold) && (null != table[bucketIndex])) {
resize(2 * table.length); // 扩容为原来的2倍
hash = (null != key) ? hash(key) : 0;
bucketIndex = indexFor(hash, table.length);
}
createEntry(hash, key, value, bucketIndex);
}
5.5 Java 8+的优化:链表转红黑树
当链表长度过长时,查找效率会降低。Java 8引入优化:
-
链表长度 > 8:将链表转换为红黑树
-
红黑树节点数 < 6:将红黑树转换回链表
java
// HashMap.TreeNode内部类
static final class TreeNode<K,V> extends LinkedHashMap.Entry<K,V> {
TreeNode<K,V> parent; // 红黑树节点
TreeNode<K,V> left;
TreeNode<K,V> right;
TreeNode<K,V> prev; // 双向链表
boolean red;
// 红黑树操作方法
final TreeNode<K,V> putTreeVal(HashMap<K,V> map, Node<K,V>[] tab,
int h, K k, V v) {
// 实现红黑树的插入
}
}
六、性能分析与优化
6.1 时间复杂度对比
| 操作 | HashMap | TreeMap | HashSet | TreeSet |
|---|---|---|---|---|
| 插入 | O(1)平均,O(n)最坏 | O(log n) | O(1)平均,O(n)最坏 | O(log n) |
| 删除 | O(1)平均,O(n)最坏 | O(log n) | O(1)平均,O(n)最坏 | O(log n) |
| 查找 | O(1)平均,O(n)最坏 | O(log n) | O(1)平均,O(n)最坏 | O(log n) |
| 遍历 | O(n) | O(n) | O(n) | O(n) |
6.2 内存使用分析
-
HashMap:需要数组存储桶,每个节点需要额外空间存储哈希值和引用
-
TreeMap:只需要节点存储键值对和颜色信息,但需要平衡操作的开销
-
HashSet:基于HashMap,有类似的内存开销
-
TreeSet:基于TreeMap,内存开销较小
6.3 选择合适的数据结构
场景1:需要快速查找,不关心顺序
java
// 使用HashMap/HashSet
Map<String, User> userCache = new HashMap<>(); // 用户缓存
Set<Long> activeUserIds = new HashSet<>(); // 活跃用户ID
场景2:需要有序遍历
java
// 使用TreeMap/TreeSet
Map<String, Integer> sortedScores = new TreeMap<>(); // 按姓名排序的成绩
Set<Integer> sortedNumbers = new TreeSet<>(); // 排序的数字集合
场景3:需要保持插入顺序
java
// 使用LinkedHashMap/LinkedHashSet
Map<Integer, String> accessOrder = new LinkedHashMap<>(16, 0.75f, true);
// 第三个参数true表示按访问顺序排序
七、实际应用与最佳实践
7.1 自定义对象作为键
当自定义对象作为HashMap的键或HashSet的元素时,必须正确重写hashCode()和equals()方法:
java
public class Student {
private String id;
private String name;
public Student(String id, String name) {
this.id = id;
this.name = name;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Student student = (Student) o;
return Objects.equals(id, student.id) &&
Objects.equals(name, student.name);
}
@Override
public int hashCode() {
return Objects.hash(id, name);
}
}
// 使用自定义对象作为键
Map<Student, Integer> scores = new HashMap<>();
Student alice = new Student("001", "Alice");
scores.put(alice, 95);
// 正确查找
Student searchKey = new Student("001", "Alice");
int score = scores.get(searchKey); // 正确返回95
7.2 并发场景下的Map
HashMap不是线程安全的,多线程环境下需要使用:
Hashtable:古老的线程安全实现,性能较差
Collections.synchronizedMap:包装器模式
ConcurrentHashMap:现代高效的并发实现
java
// ConcurrentHashMap的使用
ConcurrentHashMap<String, Integer> concurrentMap = new ConcurrentHashMap<>();
// 线程安全的操作方法
concurrentMap.putIfAbsent("key", 1); // 仅当键不存在时插入
concurrentMap.compute("key", (k, v) -> v == null ? 1 : v + 1); // 原子更新
7.3 内存敏感场景的优化
对于内存敏感的应用,可以考虑:
-
设置合适的初始容量,减少扩容次数
-
使用基本类型集合库(如Eclipse Collections, fastutil)
-
对于小型集合,使用数组或简单数据结构
java
// 优化HashMap初始容量
int expectedSize = 1000;
float loadFactor = 0.75f;
int initialCapacity = (int)(expectedSize / loadFactor) + 1;
Map<String, Integer> optimizedMap = new HashMap<>(initialCapacity, loadFactor);
八、常见面试题解析
8.1 HashMap的工作原理
-
基于数组+链表/红黑树实现
-
使用hashCode()计算索引,equals()比较键
-
默认负载因子0.75,达到阈值时扩容2倍
-
Java 8+中,链表长度>8时转为红黑树
8.2 HashMap和Hashtable的区别
| 特性 | HashMap | Hashtable |
|---|---|---|
| 线程安全 | 不安全 | 安全 |
| null键值 | 允许 | 不允许 |
| 性能 | 高 | 较低 |
| 迭代器 | fail-fast | 安全 |
8.3 ConcurrentHashMap的实现原理
-
分段锁(Java 7):将数据分段,每段独立加锁
-
CAS+synchronized(Java 8):更细粒度的锁,性能更好
-
支持高并发读写操作
8.4 如何设计一个好的hashCode()方法
java
@Override
public int hashCode() {
// 使用Objects.hash()自动生成
return Objects.hash(field1, field2, field3);
// 或者手动实现
int result = 17;
result = 31 * result + field1.hashCode();
result = 31 * result + field2.hashCode();
return result;
}
总结
-
搜索树:提供有序存储,TreeMap/TreeSet基于红黑树实现
-
Map接口:存储键值对,键唯一,主要实现有HashMap、TreeMap
-
Set接口:存储不重复元素,主要实现有HashSet、TreeSet
-
哈希表:高效查找的核心,通过哈希函数和冲突解决实现O(1)查找
-
性能优化:合理选择数据结构,正确实现hashCode()和equals()