算法的核心是「解决问题的步骤化逻辑」,Java 算法的学习不是 "背代码",而是「理解思想→手动实现→性能优化→场景落地」的循序渐进过程。本文将按「入门→进阶→精通」三层阶梯,覆盖基础算法思想、经典算法实现、工程化优化、实战落地,形成完整的知识闭环。
一、学习路径总览
表格
| 阶段 | 核心目标 | 关键能力 | 核心算法 / 思想 |
|---|---|---|---|
| 入门 | 理解算法本质,掌握基础思想和简单算法 | 能实现排序 / 查找,分析时间复杂度 | 枚举、递归、贪心(入门);冒泡 / 选择 / 插入排序;顺序 / 二分查找 |
| 进阶 | 掌握四大经典算法思想,实现高级算法 | 能解决中等难度算法题,融合数据结构 | 分治、动态规划、回溯、贪心(进阶);快排 / 归并 / 堆排;DFS/BFS;KMP |
| 精通 | 算法优化 + 工程化落地,适配高并发 / 高性能场景 | 能设计算法、优化性能、落地业务 | 算法剪枝 / 并行化;高并发算法;框架 / 中间件算法应用;业务场景算法选型 |
二、入门阶段:算法基础
1. 核心目标
- 理解「算法 = 输入→处理步骤→输出」的本质;
- 掌握时间复杂度 / 空间复杂度的分析方法(大 O 表示法);
- 实现最基础的排序、查找算法,理解核心逻辑。
2. 前置知识
- Java 基础:循环、分支、数组、方法、递归;
- 数据结构基础:数组、链表(线性结构);
- 核心概念:时间复杂度(O(1)、O(n)、O(n2)、O(logn))、空间复杂度。
3. 基础算法思想(入门)
(1)枚举(暴力破解):最直观的算法思想
原理 :遍历所有可能的解,验证是否满足条件,适合小规模数据场景。代码案例:枚举法找两数之和
java
运行
/**
* 枚举法:在数组中找到和为target的两个数,返回其索引
* 时间复杂度:O(n²),空间复杂度:O(1)
*/
public class EnumerationDemo {
public static int[] twoSum(int[] nums, int target) {
// 枚举所有两两组合
for (int i = 0; i < nums.length; i++) {
for (int j = i + 1; j < nums.length; j++) {
if (nums[i] + nums[j] == target) {
return new int[]{i, j};
}
}
}
throw new IllegalArgumentException("无满足条件的数");
}
public static void main(String[] args) {
int[] nums = {2, 7, 11, 15};
int target = 9;
int[] result = twoSum(nums, target);
System.out.println("索引:" + result[0] + "," + result[1]); // 输出:0,1
}
}
关键解析:
- 枚举法是 "笨办法",但逻辑简单、易实现,适合数据量小的场景;
- 时间复杂度O(n2)(两层循环),空间复杂度O(1)(仅用常量空间)。
(2)递归:自顶向下的分解思想
原理 :将大问题分解为相同逻辑的小问题,直到触达终止条件,再回溯合并结果。核心三要素:
- 递归终止条件(避免死循环);
- 递推公式(大问题→小问题);
- 回溯处理(小问题结果→大问题结果)。
代码案例:递归求阶乘
java
运行
/**
* 递归求n的阶乘(n! = n*(n-1)*...*1)
* 时间复杂度:O(n)(递归n次),空间复杂度:O(n)(递归栈深度)
*/
public class RecursionDemo {
public static int factorial(int n) {
// 1. 终止条件
if (n == 1) {
return 1;
}
// 2. 递推公式:n! = n * (n-1)!
return n * factorial(n - 1);
}
public static void main(String[] args) {
System.out.println("5! = " + factorial(5)); // 输出:120
}
}
关键解析:
- 递归的核心是 "分解 + 回溯",但递归栈过深会导致
StackOverflowError(如 n=10000); - 优化方向:尾递归(JVM 不优化尾递归,需手动改为循环)、记忆化缓存(避免重复计算)。
4. 基础查找算法
(1)顺序查找:遍历所有元素
原理 :从数组头部开始,逐个比较元素,找到目标值则返回索引,适合无序数组。代码案例:
java
运行
/**
* 顺序查找:无序数组中查找目标值
* 时间复杂度:O(n),空间复杂度:O(1)
*/
public class SequentialSearch {
public static int search(int[] nums, int target) {
for (int i = 0; i < nums.length; i++) {
if (nums[i] == target) {
return i;
}
}
return -1; // 未找到
}
public static void main(String[] args) {
int[] nums = {3, 1, 4, 1, 5, 9};
System.out.println("查找4的索引:" + search(nums, 4)); // 输出:2
System.out.println("查找6的索引:" + search(nums, 6)); // 输出:-1
}
}
(2)二分查找:有序数组的高效查找
原理 :将有序数组折半,每次排除一半元素,核心是 "缩小查找范围"。代码案例:非递归版二分查找
java
运行
/**
* 二分查找:有序数组中查找目标值(非递归版,推荐)
* 时间复杂度:O(logn),空间复杂度:O(1)
*/
public class BinarySearch {
public static int search(int[] nums, int target) {
int left = 0;
int right = nums.length - 1;
// 循环条件:left <= right(包含最后一个元素)
while (left <= right) {
// 计算中间索引(避免left+right溢出)
int mid = left + (right - left) / 2;
if (nums[mid] == target) {
return mid; // 找到目标,返回索引
} else if (nums[mid] < target) {
left = mid + 1; // 目标在右半区
} else {
right = mid - 1; // 目标在左半区
}
}
return -1; // 未找到
}
public static void main(String[] args) {
int[] nums = {1, 3, 5, 7, 9, 11};
System.out.println("查找7的索引:" + search(nums, 7)); // 输出:3
System.out.println("查找8的索引:" + search(nums, 8)); // 输出:-1
}
}
关键解析:
- 二分查找的前提是「数组有序」,时间复杂度O(logn)(每次折半,效率远高于顺序查找);
- 易错点:
mid的计算(避免溢出)、循环条件(left <= right)、边界调整(mid±1)。
5. 基础排序算法
表格
| 排序算法 | 核心思想 | 时间复杂度 | 空间复杂度 | 稳定性 |
|---|---|---|---|---|
| 冒泡排序 | 相邻元素比较,大的后移(像气泡上浮) | O(n2) | O(1) | 稳定 |
| 选择排序 | 找最小值,放到已排序区末尾 | O(n2) | O(1) | 不稳定 |
| 插入排序 | 逐个将元素插入已排序区的正确位置 | O(n2)(最坏)/O(n)(最好) | O(1) | 稳定 |
(1)冒泡排序(入门必学)
java
运行
/**
* 冒泡排序:相邻元素比较,大的后移
* 优化:添加标志位,若某轮无交换则提前结束
*/
public class BubbleSort {
public static void sort(int[] nums) {
int n = nums.length;
boolean swapped; // 标记是否发生交换
for (int i = 0; i < n - 1; i++) {
swapped = false;
// 每轮冒泡后,最后i个元素已排序,无需比较
for (int j = 0; j < n - 1 - i; j++) {
if (nums[j] > nums[j + 1]) {
// 交换相邻元素
int temp = nums[j];
nums[j] = nums[j + 1];
nums[j + 1] = temp;
swapped = true;
}
}
if (!swapped) {
break; // 无交换,数组已排序,提前退出
}
}
}
public static void main(String[] args) {
int[] nums = {6, 3, 8, 2, 9, 1};
sort(nums);
for (int num : nums) {
System.out.print(num + " "); // 输出:1 2 3 6 8 9
}
}
}
(2)插入排序(实际场景更常用)
java
运行
/**
* 插入排序:将元素逐个插入已排序区的正确位置
* 适合小规模/近乎有序的数组,效率高于冒泡/选择排序
*/
public class InsertionSort {
public static void sort(int[] nums) {
int n = nums.length;
for (int i = 1; i < n; i++) {
int temp = nums[i]; // 待插入元素
int j = i - 1; // 已排序区最后一个元素索引
// 找到插入位置(已排序区元素>temp则后移)
while (j >= 0 && nums[j] > temp) {
nums[j + 1] = nums[j];
j--;
}
nums[j + 1] = temp; // 插入元素
}
}
public static void main(String[] args) {
int[] nums = {6, 3, 8, 2, 9, 1};
sort(nums);
for (int num : nums) {
System.out.print(num + " "); // 输出:1 2 3 6 8 9
}
}
}
三、进阶阶段:经典算法思想与高级算法
1. 核心目标
- 掌握分治、动态规划、回溯、贪心四大经典算法思想;
- 实现工程中常用的高级排序(快排 / 归并 / 堆排);
- 融合数据结构(树 / 图 / 堆)实现复杂算法(DFS/BFS/KMP)。
2. 前置知识
- 入门阶段的算法基础;
- 数据结构:树、图、堆、哈希表(非线性结构);
- 核心能力:问题拆解、状态定义、边界处理。
3. 四大经典算法思想(进阶核心)
(1)分治算法:分而治之
原理 :将大问题分解为多个独立的子问题,解决子问题后合并结果,核心是「递归 + 合并」。典型应用:快速排序、归并排序、二分查找。
代码案例:归并排序(分治的经典实现)
java
运行
/**
* 归并排序:分治思想,先拆分再合并
* 时间复杂度:O(nlogn)(稳定),空间复杂度:O(n)(需要临时数组)
*/
public class MergeSort {
// 主方法:排序数组
public static void sort(int[] nums) {
if (nums == null || nums.length <= 1) {
return;
}
mergeSort(nums, 0, nums.length - 1);
}
// 递归拆分:将数组拆分为左右两部分
private static void mergeSort(int[] nums, int left, int right) {
if (left >= right) {
return; // 终止条件:子数组长度为1
}
int mid = left + (right - left) / 2;
mergeSort(nums, left, mid); // 左半区排序
mergeSort(nums, mid + 1, right); // 右半区排序
merge(nums, left, mid, right); // 合并左右有序数组
}
// 合并:将两个有序子数组合并为一个有序数组
private static void merge(int[] nums, int left, int mid, int right) {
int[] temp = new int[right - left + 1]; // 临时数组
int i = left; // 左半区指针
int j = mid + 1; // 右半区指针
int k = 0; // 临时数组指针
// 合并两个有序数组
while (i <= mid && j <= right) {
if (nums[i] <= nums[j]) {
temp[k++] = nums[i++];
} else {
temp[k++] = nums[j++];
}
}
// 处理左半区剩余元素
while (i <= mid) {
temp[k++] = nums[i++];
}
// 处理右半区剩余元素
while (j <= right) {
temp[k++] = nums[j++];
}
// 将临时数组复制回原数组
System.arraycopy(temp, 0, nums, left, temp.length);
}
public static void main(String[] args) {
int[] nums = {6, 3, 8, 2, 9, 1};
sort(nums);
for (int num : nums) {
System.out.print(num + " "); // 输出:1 2 3 6 8 9
}
}
}
关键解析:
- 归并排序是「稳定排序」(相等元素相对位置不变),适合链表排序、大数据量外排序;
- 时间复杂度O(nlogn)(拆分logn层,每层合并O(n)),空间复杂度O(n)(临时数组)。
(2)动态规划(DP):最优子结构 + 重叠子问题
核心思想 :将大问题分解为子问题,存储子问题的解(避免重复计算),逐步推导最优解。核心步骤:
- 定义状态(子问题的解);
- 确定状态转移方程(子问题间的关系);
- 初始化边界条件;
- 推导最终解。
代码案例:斐波那契数列(DP 优化递归)
java
运行
/**
* 动态规划:求斐波那契数列第n项(F(n)=F(n-1)+F(n-2),F(0)=0,F(1)=1)
* 递归版:时间O(2ⁿ)(大量重复计算),DP版:时间O(n),空间O(1)
*/
public class DynamicProgrammingDemo {
// 优化版:滚动数组,空间O(1)
public static int fibonacci(int n) {
if (n <= 1) {
return n;
}
int a = 0; // F(0)
int b = 1; // F(1)
int res = 0;
for (int i = 2; i <= n; i++) {
res = a + b; // F(i)=F(i-1)+F(i-2)
a = b; // 滚动更新
b = res;
}
return res;
}
// 经典DP版:数组存储子问题解,空间O(n)
public static int fibonacciDP(int n) {
if (n <= 1) {
return n;
}
int[] dp = new int[n + 1];
dp[0] = 0;
dp[1] = 1;
for (int i = 2; i <= n; i++) {
dp[i] = dp[i - 1] + dp[i - 2]; // 状态转移方程
}
return dp[n];
}
public static void main(String[] args) {
System.out.println("F(10) = " + fibonacci(10)); // 输出:55
}
}
代码案例:0-1 背包问题(DP 经典应用)
java
运行
/**
* 0-1背包问题:给定物品重量w、价值v,背包容量C,求最大价值
* 状态定义:dp[i][j] = 前i个物品,容量j的背包的最大价值
* 状态转移:dp[i][j] = max(dp[i-1][j], dp[i-1][j-w[i]] + v[i])(选/不选第i个物品)
*/
public class KnapsackProblem {
public static int maxValue(int[] weights, int[] values, int capacity) {
int n = weights.length;
// 优化:一维数组(逆序遍历避免重复选择)
int[] dp = new int[capacity + 1];
for (int i = 0; i < n; i++) {
// 逆序遍历:保证每个物品只选一次
for (int j = capacity; j >= weights[i]; j--) {
dp[j] = Math.max(dp[j], dp[j - weights[i]] + values[i]);
}
}
return dp[capacity];
}
public static void main(String[] args) {
int[] weights = {2, 3, 4, 5}; // 物品重量
int[] values = {3, 4, 5, 6}; // 物品价值
int capacity = 8; // 背包容量
System.out.println("最大价值:" + maxValue(weights, values, capacity)); // 输出:10
}
}
(3)回溯算法:试错 + 回溯
原理 :通过递归尝试所有可能的解,若当前解不满足条件则 "回溯"(撤销选择),适合组合、排列、子集等问题。核心模板:
java
运行
void backtrack(路径, 选择列表) {
if (终止条件) {
保存结果;
return;
}
for (选择 : 选择列表) {
做出选择;
backtrack(路径, 选择列表);
撤销选择; // 回溯
}
}
代码案例:全排列问题
java
运行
import java.util.ArrayList;
import java.util.List;
/**
* 回溯算法:求数组的全排列
* 时间复杂度:O(n!)(n个元素的全排列数),空间复杂度:O(n)(递归栈+路径)
*/
public class Permutation {
private List<List<Integer>> result = new ArrayList<>();
public List<List<Integer>> permute(int[] nums) {
List<Integer> path = new ArrayList<>();
boolean[] used = new boolean[nums.length]; // 标记元素是否已使用
backtrack(nums, path, used);
return result;
}
private void backtrack(int[] nums, List<Integer> path, boolean[] used) {
// 终止条件:路径长度等于数组长度
if (path.size() == nums.length) {
result.add(new ArrayList<>(path)); // 保存结果(需新建列表)
return;
}
// 遍历选择列表
for (int i = 0; i < nums.length; i++) {
if (used[i]) {
continue; // 跳过已使用的元素
}
// 做出选择
path.add(nums[i]);
used[i] = true;
// 递归
backtrack(nums, path, used);
// 撤销选择(回溯)
path.remove(path.size() - 1);
used[i] = false;
}
}
public static void main(String[] args) {
Permutation permutation = new Permutation();
int[] nums = {1, 2, 3};
List<List<Integer>> result = permutation.permute(nums);
System.out.println("全排列:" + result); // 输出:[[1,2,3],[1,3,2],[2,1,3],[2,3,1],[3,1,2],[3,2,1]]
}
}
(4)贪心算法:局部最优→全局最优
原理 :每一步选择当前局部最优解,最终希望得到全局最优解(并非所有问题都适用,需满足 "贪心选择性质")。典型应用:活动选择、哈夫曼编码、最短路径(Dijkstra)。
代码案例:活动选择问题(最多不重叠活动)
java
运行
import java.util.Arrays;
import java.util.Comparator;
/**
* 贪心算法:活动选择问题,选择最多的不重叠活动
* 策略:按结束时间排序,每次选结束最早的活动
*/
public class ActivitySelection {
// 活动类:开始时间、结束时间
static class Activity {
int start;
int end;
Activity(int start, int end) {
this.start = start;
this.end = end;
}
}
public static int maxActivities(Activity[] activities) {
// 按结束时间升序排序(贪心策略核心)
Arrays.sort(activities, Comparator.comparingInt(a -> a.end));
int count = 1; // 至少选第一个活动
int lastEnd = activities[0].end; // 上一个活动的结束时间
// 遍历剩余活动
for (int i = 1; i < activities.length; i++) {
if (activities[i].start >= lastEnd) { // 不重叠
count++;
lastEnd = activities[i].end;
}
}
return count;
}
public static void main(String[] args) {
Activity[] activities = {
new Activity(1, 2),
new Activity(3, 4),
new Activity(0, 6),
new Activity(5, 7),
new Activity(8, 9),
new Activity(5, 9)
};
System.out.println("最多选" + maxActivities(activities) + "个活动"); // 输出:4
}
}
4. 高级排序算法(工程核心)
表格
| 排序算法 | 核心思想 | 时间复杂度 | 空间复杂度 | 稳定性 | 适用场景 |
|---|---|---|---|---|---|
| 快速排序 | 分治,选基准值分区 | O(nlogn)(平均)/O(n2)(最坏) | O(logn)(递归栈) | 不稳定 | 通用排序(工程首选,JDK Arrays.sort 底层) |
| 归并排序 | 分治,拆分后合并 | O(nlogn)(稳定) | O(n) | 稳定 | 链表排序、外排序、需要稳定排序的场景 |
| 堆排序 | 基于堆结构,选堆顶元素 | O(nlogn) | O(1) | 不稳定 | 内存受限、需要原地排序的场景 |
代码案例:快速排序(工程首选)
java
运行
/**
* 快速排序:分治+基准值分区
* 优化:随机选基准值,避免最坏情况(有序数组)
*/
public class QuickSort {
public static void sort(int[] nums) {
if (nums == null || nums.length <= 1) {
return;
}
quickSort(nums, 0, nums.length - 1);
}
private static void quickSort(int[] nums, int left, int right) {
if (left >= right) {
return;
}
int pivotIndex = partition(nums, left, right); // 分区,返回基准值索引
quickSort(nums, left, pivotIndex - 1); // 左半区排序
quickSort(nums, pivotIndex + 1, right); // 右半区排序
}
// 分区:将数组分为<=基准值、基准值、>=基准值三部分
private static int partition(int[] nums, int left, int right) {
// 随机选基准值(避免有序数组导致的最坏情况)
int randomIndex = left + (int) (Math.random() * (right - left + 1));
swap(nums, left, randomIndex); // 基准值移到左边界
int pivot = nums[left]; // 基准值
int i = left;
int j = right;
while (i < j) {
// 从右找第一个<=基准值的元素
while (i < j && nums[j] > pivot) {
j--;
}
// 从左找第一个>基准值的元素
while (i < j && nums[i] <= pivot) {
i++;
}
swap(nums, i, j); // 交换
}
swap(nums, left, i); // 基准值归位
return i;
}
private static void swap(int[] nums, int i, int j) {
int temp = nums[i];
nums[i] = nums[j];
nums[j] = temp;
}
public static void main(String[] args) {
int[] nums = {6, 3, 8, 2, 9, 1, 5, 7};
sort(nums);
for (int num : nums) {
System.out.print(num + " "); // 输出:1 2 3 5 6 7 8 9
}
}
}
5. 图 / 树的核心算法
(1)DFS/BFS(图 / 树的遍历)
java
运行
import java.util.*;
/**
* 图的DFS(深度优先)和BFS(广度优先)遍历
* 邻接表存储图,适用于稀疏图
*/
public class GraphTraversal {
private Map<Integer, List<Integer>> adj; // 邻接表
public GraphTraversal() {
adj = new HashMap<>();
}
// 添加边(无向图)
public void addEdge(int v1, int v2) {
adj.computeIfAbsent(v1, k -> new ArrayList<>()).add(v2);
adj.computeIfAbsent(v2, k -> new ArrayList<>()).add(v1);
}
// DFS:递归实现
public void dfs(int start) {
Set<Integer> visited = new HashSet<>();
dfsRec(start, visited);
System.out.println();
}
private void dfsRec(int v, Set<Integer> visited) {
visited.add(v);
System.out.print(v + " ");
// 遍历邻接顶点
for (int neighbor : adj.getOrDefault(v, new ArrayList<>())) {
if (!visited.contains(neighbor)) {
dfsRec(neighbor, visited);
}
}
}
// BFS:队列实现
public void bfs(int start) {
Set<Integer> visited = new HashSet<>();
Queue<Integer> queue = new LinkedList<>();
queue.offer(start);
visited.add(start);
while (!queue.isEmpty()) {
int v = queue.poll();
System.out.print(v + " ");
// 遍历邻接顶点
for (int neighbor : adj.getOrDefault(v, new ArrayList<>())) {
if (!visited.contains(neighbor)) {
visited.add(neighbor);
queue.offer(neighbor);
}
}
}
System.out.println();
}
public static void main(String[] args) {
GraphTraversal graph = new GraphTraversal();
graph.addEdge(0, 1);
graph.addEdge(0, 2);
graph.addEdge(1, 3);
graph.addEdge(2, 3);
graph.addEdge(3, 4);
System.out.println("DFS遍历:");
graph.dfs(0); // 输出:0 1 3 2 4
System.out.println("BFS遍历:");
graph.bfs(0); // 输出:0 1 2 3 4
}
}
(2)Dijkstra 算法(单源最短路径)
java
运行
import java.util.*;
/**
* Dijkstra算法:带权有向图的单源最短路径(权重非负)
* 基于小顶堆优化,时间复杂度O(ElogV)
*/
public class Dijkstra {
// 存储边:<目标顶点,权重>
private Map<Integer, Map<Integer, Integer>> adj;
public Dijkstra() {
adj = new HashMap<>();
}
// 添加带权边(有向图)
public void addEdge(int from, int to, int weight) {
adj.computeIfAbsent(from, k -> new HashMap<>()).put(to, weight);
}
// 求start到所有顶点的最短路径
public Map<Integer, Integer> shortestPath(int start) {
// 存储最短路径长度:<顶点,距离>
Map<Integer, Integer> dist = new HashMap<>();
// 小顶堆:<距离,顶点>,按距离升序
PriorityQueue<int[]> heap = new PriorityQueue<>(Comparator.comparingInt(a -> a[0]));
// 初始化:start到自身距离为0,其他为无穷大
for (int v : adj.keySet()) {
dist.put(v, Integer.MAX_VALUE);
}
dist.put(start, 0);
heap.offer(new int[]{0, start});
while (!heap.isEmpty()) {
int[] curr = heap.poll();
int currDist = curr[0];
int currV = curr[1];
// 已找到更短路径,跳过
if (currDist > dist.get(currV)) {
continue;
}
// 遍历邻接顶点
for (Map.Entry<Integer, Integer> entry : adj.getOrDefault(currV, new HashMap<>()).entrySet()) {
int nextV = entry.getKey();
int weight = entry.getValue();
int newDist = currDist + weight;
// 更新最短路径
if (newDist < dist.get(nextV)) {
dist.put(nextV, newDist);
heap.offer(new int[]{newDist, nextV});
}
}
}
return dist;
}
public static void main(String[] args) {
Dijkstra graph = new Dijkstra();
graph.addEdge(0, 1, 2);
graph.addEdge(0, 2, 5);
graph.addEdge(1, 2, 1);
graph.addEdge(1, 3, 4);
graph.addEdge(2, 3, 1);
Map<Integer, Integer> dist = graph.shortestPath(0);
System.out.println("0到各顶点的最短路径:");
for (Map.Entry<Integer, Integer> entry : dist.entrySet()) {
System.out.println("0 → " + entry.getKey() + ":" + entry.getValue());
}
// 输出:
// 0 → 0:0
// 0 → 1:2
// 0 → 2:3
// 0 → 3:4
}
}
四、精通阶段:算法优化与工程化落地
1. 核心目标
- 掌握算法性能优化技巧(剪枝、缓存、并行化);
- 适配高并发 / 高性能场景的算法设计;
- 理解框架 / 中间件中的算法应用,落地业务场景。
2. 算法性能优化技巧
(1)剪枝:提前排除无效解
- 原理:在递归 / 枚举过程中,提前判断当前路径不可能得到最优解,直接回溯 / 终止;
- 应用:回溯算法(如 N 皇后、数独)、动态规划(如背包问题)。
(2)缓存 / 记忆化:避免重复计算
- 原理:存储已计算的子问题解,下次直接使用;
- 应用:递归→记忆化递归(如斐波那契、爬楼梯)、动态规划。
(3)并行化:利用多线程加速
- 原理:将算法拆分为独立子任务,多线程并行执行;
- 应用:归并排序(拆分后多线程排序子数组)、大数据量查找(分块并行查找)。
代码案例:并行归并排序
java
运行
import java.util.concurrent.ForkJoinPool;
import java.util.concurrent.RecursiveAction;
/**
* 并行归并排序:基于Fork/Join框架,利用多线程加速
* 适合大数据量排序,充分利用多核CPU
*/
public class ParallelMergeSort extends RecursiveAction {
private static final int THRESHOLD = 1000; // 阈值:小于阈值则串行排序
private int[] nums;
private int left;
private int right;
public ParallelMergeSort(int[] nums, int left, int right) {
this.nums = nums;
this.left = left;
this.right = right;
}
@Override
protected void compute() {
if (right - left <= THRESHOLD) {
// 小数据量:串行插入排序(比归并更高效)
InsertionSort.sort(nums, left, right);
return;
}
// 大数据量:拆分任务
int mid = left + (right - left) / 2;
ParallelMergeSort leftTask = new ParallelMergeSort(nums, left, mid);
ParallelMergeSort rightTask = new ParallelMergeSort(nums, mid + 1, right);
// 并行执行子任务
invokeAll(leftTask, rightTask);
// 合并结果
merge(nums, left, mid, right);
}
// 合并两个有序子数组
private void merge(int[] nums, int left, int mid, int right) {
int[] temp = new int[right - left + 1];
int i = left, j = mid + 1, k = 0;
while (i <= mid && j <= right) {
temp[k++] = nums[i] <= nums[j] ? nums[i++] : nums[j++];
}
while (i <= mid) temp[k++] = nums[i++];
while (j <= right) temp[k++] = nums[j++];
System.arraycopy(temp, 0, nums, left, temp.length);
}
// 对外暴露的排序方法
public static void sort(int[] nums) {
ForkJoinPool pool = new ForkJoinPool(); // 默认使用CPU核心数
pool.invoke(new ParallelMergeSort(nums, 0, nums.length - 1));
pool.shutdown();
}
public static void main(String[] args) {
int[] nums = new int[1000000]; // 100万数据
for (int i = 0; i < nums.length; i++) {
nums[i] = (int) (Math.random() * 1000000);
}
long start = System.currentTimeMillis();
sort(nums);
long end = System.currentTimeMillis();
System.out.println("并行排序耗时:" + (end - start) + "ms");
}
}
// 辅助:插入排序(指定区间)
class InsertionSort {
public static void sort(int[] nums, int left, int right) {
for (int i = left + 1; i <= right; i++) {
int temp = nums[i];
int j = i - 1;
while (j >= left && nums[j] > temp) {
nums[j + 1] = nums[j];
j--;
}
nums[j + 1] = temp;
}
}
}
3. 高并发场景的算法适配
(1)并发排序:避免线程安全问题
- 禁止在多线程下直接修改共享数组(如快速排序的分区操作);
- 推荐:分块排序→合并(如 ParallelMergeSort)、使用线程安全的集合(CopyOnWriteArrayList)。
(2)限流算法:高并发系统的核心算法
代码案例:令牌桶算法(限流)
java
运行
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
/**
* 令牌桶算法:高并发限流核心算法
* 原理:定时生成令牌放入桶中,请求需获取令牌才能执行
*/
public class TokenBucketLimiter {
private final BlockingQueue<Object> bucket; // 令牌桶
private final ScheduledExecutorService scheduler; // 定时生成令牌
// 构造方法:桶容量、令牌生成速率(个/秒)
public TokenBucketLimiter(int capacity, int rate) {
this.bucket = new ArrayBlockingQueue<>(capacity);
this.scheduler = Executors.newSingleThreadScheduledExecutor();
// 定时生成令牌
scheduler.scheduleAtFixedRate(() -> {
try {
bucket.put(new Object()); // 放入令牌
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}, 0, 1000 / rate, TimeUnit.MILLISECONDS);
}
// 获取令牌(阻塞)
public boolean acquire() {
try {
bucket.take(); // 无令牌则阻塞
return true;
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
return false;
}
}
// 关闭调度器
public void shutdown() {
scheduler.shutdown();
}
public static void main(String[] args) {
// 桶容量10,每秒生成5个令牌(限流QPS=5)
TokenBucketLimiter limiter = new TokenBucketLimiter(10, 5);
// 模拟10个请求
for (int i = 0; i < 10; i++) {
new Thread(() -> {
if (limiter.acquire()) {
System.out.println(Thread.currentThread().getName() + " 获取令牌,执行请求");
}
}).start();
}
// 延迟关闭
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
limiter.shutdown();
}
}
4. 框架 / 中间件中的算法应用
表格
| 框架 / 中间件 | 核心算法 | 应用场景 |
|---|---|---|
| JDK Arrays.sort | 快速排序(基本类型)/ 归并排序(对象类型) | 数组排序,兼顾性能与稳定性 |
| Redis | 跳表(ZSet)、哈希表(String/Hash)、布隆过滤器 | 有序集合、键值存储、缓存去重 |
| Tomcat | 线程池(阻塞队列 + 拒绝策略)、LRU 缓存 | 请求处理、连接池管理 |
| MyBatis | 动态 SQL 解析(正则表达式)、缓存(LRU) | SQL 生成、减少数据库查询 |
五、精通阶段补充:高频高级算法(字符串 / 高级数据结构 / 分布式)
这部分是大厂面试核心考点 和工业级开发必备 ,也是从 "单机算法" 到 "分布式算法" 的跨越,涵盖字符串处理、连通性问题、单调优化、分布式架构四大类高频场景,是算法能力从 "中级" 到 "高级" 的关键。
5.1 经典字符串算法(面试 / 工程 TOP1 高频)
字符串处理是 Java 开发中最常见的场景(如解析、匹配、脱敏),核心算法围绕匹配、查找、回文、前缀匹配 展开,以下是工业级开发中最常用的 4 类算法,配套可运行代码 + 工程化优化。
5.1.1 KMP 算法:高效字符串匹配(解决「暴力匹配」的 O (n*m) 痛点)
核心问题 :在主串S中查找模式串P的首次出现位置,暴力匹配的时间复杂度为O(n∗m),KMP 通过预处理模式串生成 next 数组 ,将时间复杂度优化至O(n+m)。核心原理 :利用模式串的最长相等前后缀 ,当匹配失败时,模式串无需回退到起点,仅回退到最长相等后缀的下一个位置,减少重复比较。代码案例:KMP 算法实现(工业级可直接复用)
java
运行
/**
* KMP算法:高效字符串匹配
* 核心:预处理模式串生成next数组(最长相等前后缀长度)
* 适用场景:大文本匹配、正则解析、关键词检索(工程高频)
*/
public class KMPAlgorithm {
// 主方法:在主串s中查找模式串p,返回首次出现的起始索引,未找到返回-1
public static int kmpSearch(String s, String p) {
if (s == null || p == null || p.length() > s.length()) {
return -1;
}
int[] next = getNextArray(p); // 预处理模式串生成next数组
int i = 0; // 主串指针
int j = 0; // 模式串指针
while (i < s.length() && j < p.length()) {
if (j == -1 || s.charAt(i) == p.charAt(j)) {
i++;
j++;
} else {
j = next[j]; // 模式串回退,主串不回退(KMP核心)
}
}
return j == p.length() ? i - j : -1;
}
// 生成next数组:next[j]表示p[0..j-1]的最长相等前后缀长度,next[0]固定为-1
private static int[] getNextArray(String p) {
int[] next = new int[p.length()];
next[0] = -1;
int i = 0; // 前缀指针
int j = -1; // 后缀指针
while (i < p.length() - 1) {
if (j == -1 || p.charAt(i) == p.charAt(j)) {
i++;
j++;
// 优化next数组:避免重复比较相同字符(工业级优化)
if (p.charAt(i) != p.charAt(j)) {
next[i] = j;
} else {
next[i] = next[j];
}
} else {
j = next[j];
}
}
return next;
}
// 拓展:查找模式串在主串中所有出现的位置
public static List<Integer> kmpSearchAll(String s, String p) {
List<Integer> result = new ArrayList<>();
if (s == null || p == null || p.length() > s.length()) {
return result;
}
int[] next = getNextArray(p);
int i = 0, j = 0;
while (i < s.length()) {
if (j == -1 || s.charAt(i) == p.charAt(j)) {
i++;
j++;
} else {
j = next[j];
}
if (j == p.length()) {
result.add(i - j);
j = next[j - 1]; // 继续查找下一个匹配(工业级细节)
}
}
return result;
}
public static void main(String[] args) {
String s = "ABABCABABABCABC";
String p = "ABC";
int firstIndex = kmpSearch(s, p);
List<Integer> allIndex = kmpSearchAll(s, p);
System.out.println("首次出现位置:" + firstIndex); // 输出:2
System.out.println("所有出现位置:" + allIndex); // 输出:[2, 8, 11]
}
}
5.1.2 前缀树(Trie 树):字符串前缀匹配 / 字典检索
核心问题 :解决字符串前缀匹配、字典查询、词频统计 等问题,时间复杂度为O(k)(k 为字符串长度),比哈希表更适合前缀相关场景。核心原理 :基于多叉树 实现,每个节点存储一个字符,从根到叶子节点的路径组成一个完整字符串,节点附带「是否为单词结尾」「词频」等属性。代码案例:Trie 树实现(支持增 / 查 / 前缀匹配 / 词频统计)
java
运行
/**
* 前缀树(Trie树):工业级实现,支持增、查、前缀匹配、词频统计
* 适用场景:搜索引擎联想、字典检索、敏感词过滤、手机号/邮箱前缀匹配
*/
public class TrieTree {
// 节点内部类:工业级设计,包含词频、子节点、是否为单词结尾
private static class TrieNode {
int count; // 该节点作为结尾的词频
TrieNode[] children; // 子节点:26个小写字母(可扩展为HashMap支持所有字符)
boolean isEnd; // 是否为单词的结尾节点
public TrieNode() {
this.count = 0;
this.children = new TrieNode[26]; // 仅支持小写字母,工程中可替换为HashMap<Character, TrieNode>
this.isEnd = false;
}
}
private final TrieNode root; // 根节点:空节点,不存储任何字符
public TrieTree() {
this.root = new TrieNode();
}
// 1. 插入字符串:支持重复插入(词频累加)
public void insert(String word) {
if (word == null || word.length() == 0) {
return;
}
TrieNode curr = root;
for (char c : word.toCharArray()) {
int index = c - 'a';
if (curr.children[index] == null) {
curr.children[index] = new TrieNode(); // 不存在则创建节点
}
curr = curr.children[index];
}
curr.isEnd = true;
curr.count++; // 词频+1
}
// 2. 查询字符串是否存在,返回词频(0表示不存在)
public int search(String word) {
if (word == null || word.length() == 0) {
return 0;
}
TrieNode curr = root;
for (char c : word.toCharArray()) {
int index = c - 'a';
if (curr.children[index] == null) {
return 0; // 路径中断,不存在
}
curr = curr.children[index];
}
return curr.isEnd ? curr.count : 0; // 必须是结尾节点才表示存在
}
// 3. 前缀匹配:判断是否存在以prefix为前缀的字符串
public boolean startsWith(String prefix) {
if (prefix == null || prefix.length() == 0) {
return true;
}
TrieNode curr = root;
for (char c : prefix.toCharArray()) {
int index = c - 'a';
if (curr.children[index] == null) {
return false;
}
curr = curr.children[index];
}
return true; // 路径存在即表示有前缀匹配
}
// 4. 拓展:删除字符串(工业级必备,支持词频递减,无词频则删除节点)
public void delete(String word) {
if (search(word) == 0) {
return; // 不存在,直接返回
}
deleteRec(root, word, 0);
}
// 递归删除:后序遍历,无子节点则删除当前节点
private boolean deleteRec(TrieNode node, String word, int depth) {
if (depth == word.length()) {
node.count--; // 词频-1
node.isEnd = node.count > 0; // 词频为0则不再是结尾节点
return node.count == 0 && isAllChildrenNull(node); // 无词频且无子节点则可删除
}
int index = word.charAt(depth) - 'a';
TrieNode child = node.children[index];
boolean needDeleteChild = deleteRec(child, word, depth + 1);
if (needDeleteChild) {
node.children[index] = null; // 删除子节点
// 当前节点无其他子节点且非结尾节点,则可继续向上删除
return isAllChildrenNull(node) && !node.isEnd;
}
return false;
}
// 辅助:判断节点的所有子节点是否为空
private boolean isAllChildrenNull(TrieNode node) {
for (TrieNode child : node.children) {
if (child != null) {
return false;
}
}
return true;
}
public static void main(String[] args) {
TrieTree trie = new TrieTree();
trie.insert("java");
trie.insert("java"); // 重复插入,词频为2
trie.insert("javascript");
trie.insert("python");
System.out.println("java词频:" + trie.search("java")); // 输出:2
System.out.println("jav是否为前缀:" + trie.startsWith("jav")); // 输出:true
System.out.println("php是否存在:" + trie.search("php")); // 输出:0
trie.delete("java");
System.out.println("删除后java词频:" + trie.search("java")); // 输出:1
trie.delete("java");
System.out.println("再次删除后java是否存在:" + (trie.search("java") > 0)); // 输出:false
System.out.println("jav是否仍为前缀:" + trie.startsWith("jav")); // 输出:true(javascript仍存在)
}
}
5.1.3 Manacher 算法:线性时间求最长回文子串(解决「中心扩展」的 O (n²) 痛点)
核心问题 :查找字符串中最长回文子串 (如 "abba""abcba"),中心扩展法的时间复杂度为O(n2),Manacher 算法通过预处理字符串 + 回文半径数组 + 中心右边界 优化至O(n)。适用场景 :密码学、字符串脱敏、回文相关业务(如车牌 / 手机号回文验证)。(注:Manacher 算法为面试高频难点,以下实现为工业级简化版,保留核心逻辑,易理解、可复用)
java
运行
/**
* Manacher算法:线性时间求最长回文子串(工业级简化版)
* 预处理:在字符串每个字符间插入#,统一奇偶回文处理(如abc→#a#b#c#)
* 核心:回文半径数组p[]、当前回文中心c、当前回文右边界r
*/
public class ManacherAlgorithm {
// 主方法:返回字符串的最长回文子串
public static String longestPalindrome(String s) {
if (s == null || s.length() < 2) {
return s;
}
// 1. 预处理字符串,插入#
String t = preProcess(s);
int n = t.length();
int[] p = new int[n]; // 回文半径数组:p[i]表示以i为中心的回文半径
int c = 0, r = 0; // 中心c,右边界r
// 2. 线性遍历计算p[]
for (int i = 1; i < n - 1; i++) {
int mirror = 2 * c - i; // i关于c的镜像节点
if (i < r) {
p[i] = Math.min(r - i, p[mirror]); // 利用镜像优化,减少计算
}
// 中心扩展:尝试扩大回文半径
while (t.charAt(i + p[i] + 1) == t.charAt(i - p[i] - 1)) {
p[i]++;
}
// 更新中心c和右边界r
if (i + p[i] > r) {
c = i;
r = i + p[i];
}
}
// 3. 找到最大回文半径和对应中心
int maxLen = 0, centerIndex = 0;
for (int i = 1; i < n - 1; i++) {
if (p[i] > maxLen) {
maxLen = p[i];
centerIndex = i;
}
}
// 4. 还原原字符串的最长回文子串
int start = (centerIndex - maxLen) / 2;
return s.substring(start, start + maxLen);
}
// 预处理:插入#,首尾添加不同字符避免越界(如^#a#b#c#$)
private static String preProcess(String s) {
StringBuilder sb = new StringBuilder();
sb.append("^");
for (char c : s.toCharArray()) {
sb.append("#").append(c);
}
sb.append("#$");
return sb.toString();
}
public static void main(String[] args) {
String s1 = "babad";
String s2 = "cbbd";
System.out.println("babad的最长回文子串:" + longestPalindrome(s1)); // 输出:bab或aba
System.out.println("cbbd的最长回文子串:" + longestPalindrome(s2)); // 输出:bb
}
}
5.2 高级数据结构融合算法(面试高频 / 工程底层)
这类算法是数据结构 + 算法思想 的融合,是大厂面试的压轴考点 ,也是 Redis、Tomcat、MyBatis 等框架的底层实现核心,掌握后可轻松应对复杂业务场景的算法设计。
5.2.1 并查集(Union-Find):解决连通性问题
核心问题 :判断多个元素是否属于同一集合 、合并两个集合 ,时间复杂度近乎O(1)(路径压缩 + 按秩合并优化)。核心原理 :基于数组 实现,每个元素指向自己的父节点,根节点表示集合的标识;通过路径压缩 (查找时将节点直接指向根节点)和按秩合并 (将小集合合并到大集合,减少树的高度)实现高效操作。适用场景:图的连通性判断、朋友圈问题、分布式节点分组、电商订单合并。
java
运行
/**
* 并查集(Union-Find):工业级实现,路径压缩+按秩合并(高度)
* 核心操作:find(查找根节点+路径压缩)、union(合并两个集合)、isConnected(判断是否连通)
*/
public class UnionFind {
private int[] parent; // 父节点数组:parent[i]表示i的父节点
private int[] rank; // 秩数组:rank[i]表示以i为根的集合的树高度
private int count; // 集合的数量
// 构造方法:初始化n个独立的集合
public UnionFind(int n) {
this.count = n;
this.parent = new int[n];
this.rank = new int[n];
for (int i = 0; i < n; i++) {
parent[i] = i; // 初始时,每个元素的父节点是自己
rank[i] = 1; // 初始时,每个树的高度为1
}
}
// 查找根节点:带路径压缩(工业级核心优化)
public int find(int x) {
if (parent[x] != x) {
parent[x] = find(parent[x]); // 路径压缩:直接指向根节点
}
return parent[x];
}
// 合并两个集合:按秩合并(高度),小高度合并到大高度下(避免树退化)
public void union(int x, int y) {
int rootX = find(x);
int rootY = find(y);
if (rootX == rootY) {
return; // 已属于同一集合,无需合并
}
// 按秩合并:小高度 → 大高度
if (rank[rootX] > rank[rootY]) {
parent[rootY] = rootX;
} else if (rank[rootX] < rank[rootY]) {
parent[rootX] = rootY;
} else {
parent[rootY] = rootX;
rank[rootX]++; // 高度相同时,合并后高度+1
}
count--; // 集合数量-1
}
// 判断两个元素是否属于同一集合
public boolean isConnected(int x, int y) {
return find(x) == find(y);
}
// 获取集合的数量
public int getCount() {
return count;
}
public static void main(String[] args) {
// 案例:朋友圈问题:5个人,0-1连通,1-2连通,3-4连通,求朋友圈数量
UnionFind uf = new UnionFind(5);
uf.union(0, 1);
uf.union(1, 2);
uf.union(3, 4);
System.out.println("朋友圈数量:" + uf.getCount()); // 输出:2
System.out.println("0和2是否连通:" + uf.isConnected(0, 2)); // 输出:true
System.out.println("0和3是否连通:" + uf.isConnected(0, 3)); // 输出:false
}
}
5.2.2 单调栈:解决「下一个更大 / 更小元素」问题
核心问题 :在线性时间内找到数组中每个元素的下一个更大元素、下一个更小元素、柱状图最大矩形、接雨水 等问题,时间复杂度O(n),解决暴力遍历的O(n2)痛点。核心原理 :维护一个单调递增 / 递减的栈 ,栈中存储数组的索引,遍历数组时,若当前元素破坏栈的单调性,则弹出栈顶元素并计算结果,直到栈恢复单调性,再将当前索引入栈。适用场景:数组极值查找、柱状图面积计算、雨水收集、表达式解析。
java
运行
import java.util.*;
/**
* 单调栈:工业级实现,解决「下一个更大元素」问题
* 拓展:可直接修改为下一个更小元素、前一个更大/更小元素
*/
public class MonotonicStack {
// 主方法:找数组中每个元素的下一个更大元素,无则返回-1
public static int[] nextGreaterElement(int[] nums) {
if (nums == null || nums.length == 0) {
return new int[0];
}
int n = nums.length;
int[] res = new int[n];
Arrays.fill(res, -1);
Deque<Integer> stack = new LinkedList<>(); // 单调栈:存储索引,保持栈内元素对应值单调递减
for (int i = 0; i < n; i++) {
// 当前元素 > 栈顶元素对应值:破坏单调性,弹出并计算
while (!stack.isEmpty() && nums[i] > nums[stack.peek()]) {
int index = stack.pop();
res[index] = nums[i]; // 栈顶元素的下一个更大元素为当前元素
}
stack.push(i); // 入栈当前索引
}
return res;
}
// 拓展:柱状图中最大的矩形(面试高频压轴题)
public static int largestRectangleArea(int[] heights) {
if (heights == null || heights.length == 0) {
return 0;
}
int n = heights.length;
int[] left = new int[n]; // left[i]:i左侧第一个更小元素的索引
int[] right = new int[n]; // right[i]:i右侧第一个更小元素的索引
Deque<Integer> stack = new LinkedList<>();
int maxArea = 0;
// 计算left数组
for (int i = 0; i < n; i++) {
while (!stack.isEmpty() && heights[stack.peek()] >= heights[i]) {
stack.pop();
}
left[i] = stack.isEmpty() ? -1 : stack.peek();
stack.push(i);
}
stack.clear();
// 计算right数组
for (int i = n - 1; i >= 0; i--) {
while (!stack.isEmpty() && heights[stack.peek()] >= heights[i]) {
stack.pop();
}
right[i] = stack.isEmpty() ? n : stack.peek();
stack.push(i);
}
// 计算每个柱子作为高度的最大矩形面积
for (int i = 0; i < n; i++) {
int width = right[i] - left[i] - 1;
maxArea = Math.max(maxArea, heights[i] * width);
}
return maxArea;
}
public static void main(String[] args) {
int[] nums = {2, 1, 2, 4, 3};
int[] nge = nextGreaterElement(nums);
System.out.println("下一个更大元素:" + Arrays.toString(nge)); // 输出:[4, 2, 4, -1, -1]
int[] heights = {2, 1, 5, 6, 2, 3};
System.out.println("柱状图最大矩形面积:" + largestRectangleArea(heights)); // 输出:10
}
}
5.2.3 跳表(SkipList):有序集合的高效实现(Redis ZSet 底层)
核心问题 :解决有序链表 的查找效率低(O(n))的问题,跳表通过多层索引 将查找效率优化至O(logn),且实现比红黑树更简单。核心原理 :在有序链表的基础上,构建多层稀疏索引(上层索引是下层索引的子集),查找时从顶层索引开始,快速定位到目标元素的大致范围,再逐层向下查找。适用场景 :有序集合存储、Redis ZSet 底层、排行榜系统(按分数排序)。(注:以下为Redis 简化版跳表实现,保留核心的增 / 查 / 删逻辑,和工业级实现一致)
java
运行
import java.util.Random;
/**
* 跳表(SkipList):Redis ZSet底层简化版,支持增、查、删、按序遍历
* 核心:多层索引、随机层高、头节点/尾节点、前驱/后继指针
* 时间复杂度:增/查/删 O(logn),比红黑树实现更简单
*/
public class SkipList {
// 跳表节点类
private static class Node {
int value; // 存储值(Redis中为score+member)
Node[] forward; // 前进指针:forward[level]表示当前节点在level层的下一个节点
public Node(int value, int level) {
this.value = value;
this.forward = new Node[level];
}
}
private static final int MAX_LEVEL = 16; // 最大层数(Redis默认32)
private static final double P = 0.5; // 随机层高的概率(Redis默认0.25)
private final Random random;
private int level; // 跳表现有最大层数
private final Node head; // 头节点,所有层的起始节点
public SkipList() {
this.random = new Random();
this.level = 1;
this.head = new Node(Integer.MIN_VALUE, MAX_LEVEL);
}
// 随机生成节点的层数(工业级随机策略)
private int randomLevel() {
int l = 1;
while (random.nextDouble() < P && l < MAX_LEVEL) {
l++;
}
return l;
}
// 查找元素:返回节点,未找到返回null
public Node find(int value) {
Node curr = head;
// 从顶层到底层逐层查找
for (int i = level - 1; i >= 0; i--) {
// 找到当前层小于value的最后一个节点
while (curr.forward[i] != null && curr.forward[i].value < value) {
curr = curr.forward[i];
}
}
// 最后检查下一层是否为目标值
if (curr.forward[0] != null && curr.forward[0].value == value) {
return curr.forward[0];
}
return null;
}
// 插入元素
public void insert(int value) {
Node[] update = new Node[MAX_LEVEL]; // 存储每层需要更新的前驱节点
Node curr = head;
// 1. 找到每层的前驱节点,存入update
for (int i = level - 1; i >= 0; i--) {
while (curr.forward[i] != null && curr.forward[i].value < value) {
curr = curr.forward[i];
}
update[i] = curr;
}
// 2. 随机生成新节点层数
int newLevel = randomLevel();
// 3. 若新层数超过现有层数,更新update和跳表层数
if (newLevel > level) {
for (int i = level; i < newLevel; i++) {
update[i] = head;
}
level = newLevel;
}
// 4. 创建新节点并插入
Node newNode = new Node(value, newLevel);
for (int i = 0; i < newLevel; i++) {
newNode.forward[i] = update[i].forward[i];
update[i].forward[i] = newNode;
}
}
// 删除元素
public void delete(int value) {
Node[] update = new Node[MAX_LEVEL]; // 存储每层需要更新的前驱节点
Node curr = head;
// 1. 找到每层的前驱节点,存入update
for (int i = level - 1; i >= 0; i--) {
while (curr.forward[i] != null && curr.forward[i].value < value) {
curr = curr.forward[i];
}
update[i] = curr;
}
// 2. 检查是否存在目标节点
curr = curr.forward[0];
if (curr == null || curr.value != value) {
return; // 不存在,直接返回
}
// 3. 逐层删除节点
for (int i = 0; i < level; i++) {
if (update[i].forward[i] != curr) {
break; // 该层无目标节点,无需继续
}
update[i].forward[i] = curr.forward[i];
}
// 4. 更新跳表现有最大层数(删除空层)
while (level > 1 && head.forward[level - 1] == null) {
level--;
}
}
// 按序遍历跳表(底层链表)
public void traverse() {
Node curr = head.forward[0];
while (curr != null) {
System.out.print(curr.value + " ");
curr = curr.forward[0];
}
System.out.println();
}
public static void main(String[] args) {
SkipList skipList = new SkipList();
skipList.insert(3);
skipList.insert(1);
skipList.insert(5);
skipList.insert(2);
skipList.insert(4);
System.out.println("按序遍历:");
skipList.traverse(); // 输出:1 2 3 4 5
System.out.println("查找5:" + (skipList.find(5) != null)); // 输出:true
skipList.delete(3);
System.out.println("删除3后遍历:");
skipList.traverse(); // 输出:1 2 4 5
}
}
5.3 分布式场景核心算法(微服务 / 高并发必备)
现在的 Java 开发均基于分布式 / 微服务架构 ,单机算法已无法满足业务需求,这部分是高级 Java 工程师的核心能力 ,涵盖分布式架构中最常用的 3 类算法,配套Java 简易工业级实现,可直接融入项目开发。
5.3.1 一致性哈希算法:分布式缓存核心(解决「缓存雪崩」「数据倾斜」)
核心问题 :解决分布式缓存中节点扩容 / 缩容时的数据迁移 问题,普通哈希算法(hash (key)% n)在节点数 n 变化时,会导致所有缓存失效(缓存雪崩),一致性哈希算法将数据迁移量降至O(1/n)。核心原理:
- 将节点 和缓存 key 映射到一个0~2^32-1的环形哈希空间;
- 数据 key 按顺时针方向映射到最近的节点;
- 节点扩容 / 缩容时,仅影响该节点在环上的相邻节点,数据迁移量极小;
- 引入虚拟节点 解决「数据倾斜」问题(物理节点对应多个虚拟节点,均匀分布在环上)。适用场景:Redis 集群、分布式缓存、负载均衡、分布式存储。
java
运行
import java.util.*;
/**
* 一致性哈希算法:工业级实现,带虚拟节点(解决数据倾斜)
* 核心:哈希环、虚拟节点、节点/key映射、节点增删
* 适用场景:Redis集群、分布式缓存、负载均衡
*/
public class ConsistentHashing {
private final SortedMap<Long, String> hashRing = new TreeMap<>(); // 哈希环:key=哈希值,value=节点名
private final int virtualNodeNum; // 每个物理节点的虚拟节点数(推荐32/64)
private static final String VIRTUAL_NODE_SUFFIX = "#"; // 虚拟节点后缀,区分物理节点
// 构造方法:初始化物理节点和虚拟节点数
public ConsistentHashing(List<String> physicalNodes, int virtualNodeNum) {
this.virtualNodeNum = virtualNodeNum;
// 初始化物理节点(添加虚拟节点到哈希环)
for (String node : physicalNodes) {
addNode(node);
}
}
// 哈希函数:工业级常用MD5+Long(避免哈希冲突)
private long hash(String key) {
return Objects.hash(key); // 工程中可替换为MD5/SHA1哈希,提升分布均匀性
}
// 添加节点(物理节点+虚拟节点)
public void addNode(String physicalNode) {
for (int i = 0; i < virtualNodeNum; i++) {
String virtualNode = physicalNode + VIRTUAL_NODE_SUFFIX + i;
long hash = hash(virtualNode);
hashRing.put(hash, physicalNode); // 虚拟节点映射到物理节点
System.out.println("添加虚拟节点:" + virtualNode + ",哈希值:" + hash);
}
}
// 删除节点(物理节点+虚拟节点)
public void removeNode(String physicalNode) {
for (int i = 0; i < virtualNodeNum; i++) {
String virtualNode = physicalNode + VIRTUAL_NODE_SUFFIX + i;
long hash = hash(virtualNode);
hashRing.remove(hash);
}
System.out.println("删除物理节点:" + physicalNode + "及其所有虚拟节点");
}
// 核心:找到key对应的物理节点
public String getNode(String key) {
if (hashRing.isEmpty()) {
throw new RuntimeException("哈希环为空,无可用节点");
}
long hash = hash(key);
// 找到顺时针第一个大于等于key哈希值的节点
SortedMap<Long, String> subMap = hashRing.tailMap(hash);
Long nodeHash = subMap.isEmpty() ? hashRing.firstKey() : subMap.firstKey();
return hashRing.get(nodeHash);
}
public static void main(String[] args) {
// 初始化物理节点:Redis1、Redis2、Redis3
List<String> physicalNodes = Arrays.asList("192.168.1.100:6379", "192.168.1.101:6379", "192.168.1.102:6379");
// 每个物理节点对应10个虚拟节点
ConsistentHashing ch = new ConsistentHashing(physicalNodes, 10);
// 测试key映射
String key1 = "user:100";
String key2 = "order:200";
String key3 = "product:300";
System.out.println("key1:" + key1 + " → 节点:" + ch.getNode(key1));
System.out.println("key2:" + key2 + " → 节点:" + ch.getNode(key2));
System.out.println("key3:" + key3 + " → 节点:" + ch.getNode(key3));
// 测试节点扩容:添加Redis4
ch.addNode("192.168.1.103:6379");
System.out.println("扩容后key1:" + key1 + " → 节点:" + ch.getNode(key1)); // 大概率仍映射到原节点,数据迁移量小
// 测试节点缩容:删除Redis2
ch.removeNode("192.168.1.101:6379");
System.out.println("缩容后key2:" + key2 + " → 节点:" + ch.getNode(key2)); // 仅映射到相邻节点
}
}
5.3.2 雪花算法(SnowFlake):分布式唯一 ID 生成(大厂标配)
核心问题 :解决分布式系统中全局唯一 ID 的生成问题,要求 ID唯一、有序、高性能、可溯源 ,雪花算法是大厂最常用的实现方案,纯内存生成,性能达 100w+/s。核心原理:将 64 位 Long 型 ID 分为 4 个部分:
- 1 位符号位:固定为 0,保证 ID 为正数;
- 41 位时间戳:精确到毫秒,可使用约 69 年(2^41-1 毫秒);
- 10 位机器位:可部署 1024 个节点(5 位数据中心 + 5 位机器 ID);
- 12 位序列号 :同一毫秒内,每个节点可生成 4096 个唯一 ID(2^12)。适用场景:分布式订单 ID、用户 ID、商品 ID、日志 ID 等全局唯一标识。
java
运行
/**
* 雪花算法(SnowFlake):工业级实现,线程安全(CAS+volatile)
* 64位ID结构:0(1) + 时间戳(41) + 数据中心ID(5) + 机器ID(5) + 序列号(12)
* 特性:唯一、有序、高性能、可溯源、无依赖(纯内存)
*/
public class SnowFlake {
// 各部分的位数
private static final int TIMESTAMP_BIT = 41;
private static final int DATA_CENTER_BIT = 5;
private static final int MACHINE_BIT = 5;
private static final int SEQUENCE_BIT = 12;
// 各部分的最大值(位运算计算)
private static final long MAX_DATA_CENTER = (1L << DATA_CENTER_BIT) - 1;
private static final long MAX_MACHINE = (1L << MACHINE_BIT) - 1;
private static final long MAX_SEQUENCE = (1L << SEQUENCE_BIT) - 1;
// 各部分的偏移量
private static final long MACHINE_OFFSET = SEQUENCE_BIT;
private static final long DATA_CENTER_OFFSET = SEQUENCE_BIT + MACHINE_BIT;
private static final long TIMESTAMP_OFFSET = SEQUENCE_BIT + MACHINE_BIT + DATA_CENTER_BIT;
// 起始时间戳:2024-01-01 00:00:00(可自定义,建议项目启动时间)
private static final long START_TIMESTAMP = 1704067200000L;
// 全局变量(volatile保证可见性,CAS保证原子性)
private final long dataCenterId; // 数据中心ID
private final long machineId; // 机器ID
private volatile long lastTimestamp = -1L; // 上一次生成ID的时间戳
private volatile long sequence = 0L; // 序列号
// 构造方法:初始化数据中心ID和机器ID(需保证分布式环境下唯一)
public SnowFlake(long dataCenterId, long machineId) {
if (dataCenterId < 0 || dataCenterId > MAX_DATA_CENTER) {
throw new IllegalArgumentException("数据中心ID超出范围:0~" + MAX_DATA_CENTER);
}
if (machineId < 0 || machineId > MAX_MACHINE) {
throw new IllegalArgumentException("机器ID超出范围:0~" + MAX_MACHINE);
}
this.dataCenterId = dataCenterId;
this.machineId = machineId;
}
// 核心方法:生成分布式唯一ID(线程安全)
public long nextId() {
long currTimestamp = getCurrentTimestamp();
// 1. 处理时钟回拨(工业级关键:避免时间戳倒退导致ID重复)
if (currTimestamp < lastTimestamp) {
throw new RuntimeException("时钟回拨,拒绝生成ID:" + (lastTimestamp - currTimestamp) + "ms");
}
// 2. 同一毫秒内,序列号自增
if (currTimestamp == lastTimestamp) {
sequence = (sequence + 1) & MAX_SEQUENCE;
// 同一毫秒内序列号耗尽,等待下一个毫秒
if (sequence == 0) {
currTimestamp = waitNextMillis(lastTimestamp);
}
} else {
// 不同毫秒,序列号重置为0
sequence = 0L;
}
// 3. 更新上一次时间戳
lastTimestamp = currTimestamp;
// 4. 拼接64位ID并返回
return (currTimestamp - START_TIMESTAMP) << TIMESTAMP_OFFSET // 时间戳部分
| dataCenterId << DATA_CENTER_OFFSET // 数据中心部分
| machineId << MACHINE_OFFSET // 机器部分
| sequence; // 序列号部分
}
// 辅助:获取当前毫秒级时间戳
private long getCurrentTimestamp() {
return System.currentTimeMillis();
}
// 辅助:等待下一个毫秒,直到获取到新的时间戳
private long waitNextMillis(long lastTimestamp) {
long curr = getCurrentTimestamp();
while (curr <= lastTimestamp) {
curr = getCurrentTimestamp();
}
return curr;
}
// 测试:多线程生成ID(验证线程安全)
public static void main(String[] args) {
SnowFlake snowFlake = new SnowFlake(1, 1);
// 10个线程,每个线程生成1000个ID
for (int i = 0; i < 10; i++) {
new Thread(() -> {
for (int j = 0; j < 1000; j++) {
System.out.println(Thread.currentThread().getName() + " → " + snowFlake.nextId());
}
}, "ID-Generator-" + i).start();
}
}
}
5.3.3 令牌桶算法:分布式限流核心(高并发系统必备)
核心问题 :解决分布式系统中接口限流、流量削峰 问题,令牌桶算法相比漏桶算法更灵活,支持突发流量 (桶内有令牌时可一次性处理)。核心原理:
- 系统以固定速率生成令牌,放入令牌桶(桶有最大容量,满则丢弃);
- 每个请求必须获取一个令牌才能执行,无令牌则拒绝 / 阻塞;
- 支持突发流量:桶内预存的令牌可应对短时间的高并发请求。适用场景 :接口限流、网关限流(如 Spring Cloud Gateway)、秒杀系统、消息队列削峰。(以下为分布式令牌桶的 Java 实现,基于 Redis 实现令牌共享,解决单机令牌桶的分布式问题)
java
运行
import redis.clients.jedis.Jedis;
import redis.clients.jedis.params.SetParams;
import java.util.concurrent.TimeUnit;
/**
* 分布式令牌桶算法:基于Redis实现(解决单机令牌桶的分布式问题)
* 核心:Redis自增+过期时间实现令牌生成,SET NX实现原子性获取令牌
* 适用场景:分布式接口限流、网关限流、秒杀系统
* 依赖:Redis、Jedis客户端
*/
public class DistributedTokenBucket {
private final Jedis jedis; // Redis客户端(工程中建议使用JedisPool/Redisson)
private final String bucketKey; // Redis中令牌桶的key
private final int capacity; // 令牌桶最大容量
private final int rate; // 令牌生成速率(个/秒)
private final long interval = 1000; // 令牌生成间隔(毫秒)
// 构造方法:初始化Redis、桶容量、生成速率
public DistributedTokenBucket(Jedis jedis, String bucketKey, int capacity, int rate) {
this.jedis = jedis;
this.bucketKey = bucketKey;
this.capacity = capacity;
this.rate = rate;
// 初始化令牌桶(后台线程持续生成令牌)
startTokenGenerator();
}
// 后台线程:持续生成令牌(固定速率)
private void startTokenGenerator() {
new Thread(() -> {
while (!Thread.currentThread().isInterrupted()) {
try {
// 1. 获取当前令牌数
String currentStr = jedis.get(bucketKey);
int current = currentStr == null ? 0 : Integer.parseInt(currentStr);
// 2. 计算新的令牌数(不超过最大容量)
int newToken = Math.min(current + rate, capacity);
// 3. 更新令牌数到Redis(原子性)
jedis.set(bucketKey, String.valueOf(newToken));
// 4. 按间隔休眠
TimeUnit.MILLISECONDS.sleep(interval);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
break;
}
}
}, "Token-Generator").start();
}
// 核心方法:获取令牌(非阻塞,返回是否成功)
public boolean acquire() {
return acquire(1); // 默认获取1个令牌
}
// 重载:获取n个令牌(原子性,Redis DECRBY实现)
public boolean acquire(int n) {
if (n <= 0 || n > capacity) {
return false;
}
// 1. 令牌数自减n(原子性)
long current = jedis.decrBy(bucketKey, n);
// 2. 自减后令牌数>=0:获取成功;否则回滚,获取失败
if (current >= 0) {
return true;
} else {
jedis.incrBy(bucketKey, n); // 回滚
return false;
}
}
// 拓展:获取令牌(阻塞,直到获取成功)
public void acquireBlocking() throws InterruptedException {
while (!acquire()) {
TimeUnit.MILLISECONDS.sleep(10);
}
}
// 测试
public static void main(String[] args) {
// 初始化Redis客户端(工程中建议使用连接池)
Jedis jedis = new Jedis("127.0.0.1", 6379);
jedis.auth("your_redis_password"); // 若Redis有密码需添加
// 令牌桶:容量100,速率10个/秒
DistributedTokenBucket tokenBucket = new DistributedTokenBucket(jedis, "limit:api:user", 100, 10);
// 模拟100个请求获取令牌
for (int i = 0; i < 100; i++) {
new Thread(() -> {
if (tokenBucket.acquire()) {
System.out.println(Thread.currentThread().getName() + " 获取令牌成功,执行请求");
} else {
System.out.println(Thread.currentThread().getName() + " 获取令牌失败,限流");
}
}, "Request-" + i).start();
}
}
}
六、算法与 Java 生态深度融合(新特性 + 主流框架)
Java 算法的学习不能脱离Java 生态 ,实际开发中算法并非单独实现,而是与Java 新特性(Stream / 虚拟线程)、主流框架(Spring/Netty/Redis) 深度结合。本部分聚焦算法在 Java 日常开发中的实际应用,让算法从 "刷题代码" 变为 "业务代码"。
6.1 算法与 Java8 + 新特性融合
Java8 + 的 Stream、Lambda、CompletableFuture,Java19 + 的虚拟线程,为算法实现提供了更简洁的语法、更高效的并行能力,无需手动编写多线程代码,即可实现算法的并行化。
6.1.1 Stream 流:简化线性算法实现
Stream 的过滤、映射、排序、归约 等操作,底层封装了排序、查找、枚举等基础算法,可大幅简化代码,同时支持并行流 实现算法的多核并行执行。代码案例:Stream 实现数组 / 集合的算法操作(排序 / 去重 / TopK / 统计)
java
运行
import java.util.*;
import java.util.stream.Collectors;
/**
* Stream+Lambda简化算法实现:排序、去重、TopK、数值统计、过滤
* 底层:并行流基于Fork/Join框架,自动实现算法的并行化,无需手动创建线程
*/
public class StreamAlgorithm {
public static void main(String[] args) {
List<Integer> list = Arrays.asList(5, 2, 8, 2, 9, 1, 5, 3, 7);
// 1. 基础排序:升序/降序(底层基于Arrays.sort,快排/归并)
List<Integer> sortedAsc = list.stream().sorted().collect(Collectors.toList());
List<Integer> sortedDesc = list.stream().sorted(Comparator.reverseOrder()).collect(Collectors.toList());
System.out.println("升序:" + sortedAsc); // [1,2,2,3,5,5,7,8,9]
System.out.println("降序:" + sortedDesc); // [9,8,7,5,5,3,2,2,1]
// 2. 去重+排序:一行实现(替代HashSet+手动排序)
List<Integer> distinctSorted = list.stream().distinct().sorted().collect(Collectors.toList());
System.out.println("去重升序:" + distinctSorted); // [1,2,3,5,7,8,9]
// 3. TopK问题:取前3大元素(底层基于堆,效率O(nlogk))
List<Integer> top3 = list.stream().sorted(Comparator.reverseOrder()).limit(3).collect(Collectors.toList());
System.out.println("前3大元素:" + top3); // [9,8,7]
// 4. 数值统计:求和、平均值、最大值、最小值(替代循环统计)
int sum = list.stream().mapToInt(Integer::intValue).sum();
double avg = list.stream().mapToInt(Integer::intValue).average().orElse(0);
int max = list.stream().mapToInt(Integer::intValue).max().orElse(0);
System.out.println("和:" + sum + ",平均值:" + avg + ",最大值:" + max); // 和:42,平均值:4.666...,最大值:9
// 5. 并行流:大数据量下的并行过滤(利用多核CPU,效率远高于单线程)
List<Integer> bigList = new ArrayList<>();
for (int i = 0; i < 10000000; i++) {
bigList.add((int) (Math.random() * 1000000));
}
long start = System.currentTimeMillis();
// 并行流:parallelStream(),底层Fork/Join框架实现并行过滤
List<Integer> filterParallel = bigList.parallelStream()
.filter(num -> num > 500000)
.collect(Collectors.toList());
long end = System.currentTimeMillis();
System.out.println("并行过滤1000万数据耗时:" + (end - start) + "ms");
}
}
6.1.2 CompletableFuture:实现异步并行算法
Java8 的CompletableFuture支持异步任务编排 ,可将算法的独立子任务拆分为异步任务,实现并行执行 + 结果合并 ,比传统Thread/ExecutorService更简洁,适合分治算法的并行化。代码案例:CompletableFuture 实现并行归并排序(分治算法异步化)
java
运行
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.stream.Collectors;
/**
* CompletableFuture实现异步并行归并排序
* 核心:将数组拆分为多个子数组,异步排序后,异步合并结果,充分利用多核CPU
* 适用场景:大数据量排序、分布式计算的本地子任务并行
*/
public class CompletableFutureSort {
// 线程池:根据CPU核心数设置,避免线程过多导致上下文切换
private static final ExecutorService executor = Executors.newVirtualThreadPerTaskExecutor(); // Java19+虚拟线程,更轻量
private static final int THRESHOLD = 1000; // 小数据量阈值,单线程排序
// 主方法:异步并行归并排序
public static CompletableFuture<List<Integer>> parallelMergeSort(List<Integer> list) {
// 小数据量:单线程插入排序,直接返回
if (list.size() <= THRESHOLD) {
return CompletableFuture.completedFuture(insertionSort(list));
}
// 大数据量:拆分为左右两个子数组
int mid = list.size() / 2;
List<Integer> left = list.subList(0, mid);
List<Integer> right = list.subList(mid, list.size());
// 异步排序左、右子数组(两个子任务并行执行)
CompletableFuture<List<Integer>> sortedLeft = parallelMergeSort(left).asyncSupply(executor);
CompletableFuture<List<Integer>> sortedRight = parallelMergeSort(right).asyncSupply(executor);
// 两个子数组排序完成后,异步合并结果
return sortedLeft.thenCombineAsync(sortedRight, CompletableFutureSort::merge, executor);
}
// 插入排序:小数据量高效
private static List<Integer> insertionSort(List<Integer> list) {
List<Integer> res = new ArrayList<>(list);
for (int i = 1; i < res.size(); i++) {
int temp = res.get(i);
int j = i - 1;
while (j >= 0 && res.get(j) > temp) {
res.set(j + 1, res.get(j));
j--;
}
res.set(j + 1, temp);
}
return res;
}
// 合并两个有序数组
private static List<Integer> merge(List<Integer> left, List<Integer> right) {
List<Integer> res = new ArrayList<>();
int i = 0, j = 0;
while (i < left.size() && j < right.size()) {
if (left.get(i) <= right.get(j)) {
res.add(left.get(i++));
} else {
res.add(right.get(j++));
}
}
res.addAll(left.subList(i, left.size()));
res.addAll(right.subList(j, right.size()));
return res;
}
public static void main(String[] args) throws Exception {
// 生成100万随机数
List<Integer> bigList = new java.util.Random().ints(0, 1000000)
.limit(1000000)
.boxed()
.collect(Collectors.toList());
// 异步并行排序
long start = System.currentTimeMillis();
List<Integer> sortedList = parallelMergeSort(bigList).get();
long end = System.currentTimeMillis();
System.out.println("100万数据并行排序耗时:" + (end - start) + "ms");
System.out.println("排序结果验证:" + (sortedList.get(0) <= sortedList.get(500000) && sortedList.get(500000) <= sortedList.get(999999))); // true
executor.shutdown();
}
}
6.1.3 虚拟线程(Java19+):轻量级并行算法实现
Java19 引入的虚拟线程 (Project Loom)是 JVM 级别的轻量级线程,无需内核态切换,创建成本低(可创建百万级虚拟线程),适合细粒度的算法并行化 (如分治算法的大量子任务),比传统线程池效率提升 10 倍以上。上述案例中Executors.newVirtualThreadPerTaskExecutor()即为虚拟线程池,替换传统线程池后,无需修改其他代码,即可实现算法的轻量级并行。
6.2 算法与 Java 主流框架的融合
Java 开发的核心是框架,算法是框架的底层核心 ,同时框架也为算法提供了落地载体 。以下是 3 个主流框架中算法的应用场景 + 集成方式,让算法真正融入业务开发。
6.2.1 Spring/Spring Boot:算法在业务层的落地
- 限流算法 :整合到 Spring Boot 的拦截器 / 过滤器中,实现接口限流(如令牌桶算法 +
HandlerInterceptor); - 雪花算法 :通过 Spring 的单例 Bean实现全局唯一 ID 生成,注入到 Service/DAO 层;
- 缓存算法 :结合 Spring Cache,实现 LRU/LFU 缓存(如
LinkedHashMap实现 LRU,整合@Cacheable); - 动态规划 / 贪心:在业务层实现最优解计算(如电商的优惠券组合、物流的路径规划)。
代码案例:Spring Boot 整合令牌桶算法实现接口限流
java
运行
// 1. 自定义限流注解
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface RateLimit {
int capacity() default 100; // 令牌桶容量
int rate() default 10; // 令牌生成速率(个/秒)
}
// 2. 限流拦截器:实现令牌桶算法,拦截加了@RateLimit的接口
@Component
public class RateLimitInterceptor implements HandlerInterceptor {
// 存储每个接口的令牌桶,key=接口方法名,value=令牌桶
private final Map<String, TokenBucketLimiter> bucketMap = new ConcurrentHashMap<>();
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
// 仅拦截Controller的方法
if (!(handler instanceof HandlerMethod)) {
return true;
}
HandlerMethod handlerMethod = (HandlerMethod) handler;
RateLimit rateLimit = handlerMethod.getMethodAnnotation(RateLimit.class);
if (rateLimit == null) {
return true; // 无注解,放行
}
// 获取接口方法名,作为令牌桶的key
String methodKey = handlerMethod.getMethod().getDeclaringClass().getName() + "." + handlerMethod.getMethod().getName();
// 懒加载创建令牌桶
TokenBucketLimiter bucket = bucketMap.computeIfAbsent(methodKey, k -> new TokenBucketLimiter(rateLimit.capacity(), rateLimit.rate()));
// 获取令牌,失败则返回429限流
if (!bucket.acquire()) {
response.setStatus(HttpServletResponse.SC_TOO_MANY_REQUESTS);
response.getWriter().write("请求过于频繁,请稍后再试!");
return false;
}
return true;
}
// 内部实现令牌桶算法(简化版,非分布式)
static class TokenBucketLimiter {
private final BlockingQueue<Object> bucket;
private final ScheduledExecutorService scheduler;
public TokenBucketLimiter(int capacity, int rate) {
this.bucket = new ArrayBlockingQueue<>(capacity);
this.scheduler = Executors.newSingleThreadScheduledExecutor();
scheduler.scheduleAtFixedRate(() -> {
try {
bucket.put(new Object());
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}, 0, 1000 / rate, TimeUnit.MILLISECONDS);
}
public boolean acquire() {
return bucket.poll() != null; // 非阻塞获取令牌
}
}
}
// 3. 注册拦截器
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Autowired
private RateLimitInterceptor rateLimitInterceptor;
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(rateLimitInterceptor).addPathPatterns("/**");
}
}
// 4. Controller使用限流注解
@RestController
@RequestMapping("/api")
public class UserController {
// 限流:容量50,速率5个/秒
@RateLimit(capacity = 50, rate = 5)
@GetMapping("/user")
public String getUser() {
return "success:获取用户信息";
}
}
6.2.2 Netty:算法在网络层的应用
Netty 是 Java 高性能网络框架,底层大量使用算法 + 数据结构 ,同时可在 Netty 中实现网络层算法:
- 编解码算法:Protobuf/JSON 编解码、Base64/MD5 哈希算法;
- 多路复用算法:Reactor 模式(基于 IO 多路复用,底层是 epoll/kqueue 算法);
- 心跳算法:基于定时任务的心跳检测(贪心算法实现心跳超时判断);
- 粘包 / 拆包算法:基于定长 / 分隔符 / 长度字段的拆包算法(线性遍历 + 匹配)。
6.2.3 Redis:算法在缓存层的集成
Redis 是 Java 开发的必备缓存,底层实现了跳表、哈希表、布隆过滤器 等高级数据结构 / 算法,同时可通过 Redis 实现分布式算法:
- 分布式雪花 ID :基于 Redis 的
INCR实现分布式唯一 ID(替代单机雪花算法); - 分布式令牌桶 :基于 Redis 的
DECRBY/INCR实现分布式限流(前文已实现); - 布隆过滤器:Redis 的 BloomFilter 插件,实现缓存穿透防护;
- 排序算法 :Redis 的
SORT命令、ZSet(跳表)实现有序集合排序。
十一、算法落地 3 个经典 Java 实战项目(从代码到产品)
算法的最终价值是落地业务 ,以下选取 Java 开发中最经典的 3 个项目 (电商秒杀、风控系统、推荐系统),从架构设计、算法选型、核心代码、工程化落地四个维度,完整讲解算法如何融入项目,实现从 "刷题代码" 到 "生产代码" 的转变。
7.1 项目 1:电商秒杀系统(高并发算法落地)
7.1.1 核心业务问题
秒杀系统的核心痛点是高并发请求冲击(瞬间十万 / 百万 QPS),导致数据库宕机、超卖、接口超时。
7.1.2 核心算法选型
表格
| 问题场景 | 算法 / 数据结构 | 选型原因 |
|---|---|---|
| 全局唯一订单 ID | 雪花算法 | 纯内存生成,高性能,唯一有序 |
| 接口限流 | 令牌桶算法(分布式) | 支持突发流量,分布式一致 |
| 请求削峰 | 阻塞队列(ArrayBlockingQueue) | 缓冲请求,逐步消费 |
| 库存超卖 | 乐观锁 / Redis 自增 | 保证库存操作的原子性 |
| 热点商品缓存 | 哈希表(ConcurrentHashMap) | O (1) 查找,高并发安全 |
7.1.3 核心架构设计
用户请求 → Nginx反向代理 → Spring Boot网关(分布式令牌桶限流) → 阻塞队列削峰 → 消费者线程池 → Redis缓存(热点商品/库存) → 数据库(最终一致性)
7.1.4 核心工程化代码(Spring Boot)
java
运行
// 1. 雪花算法单例Bean
@Configuration
public class SnowFlakeConfig {
@Bean
public SnowFlake snowFlake() {
return new SnowFlake(1, 1); // 数据中心ID=1,机器ID=1
}
}
// 2. 秒杀Service:整合队列+Redis+雪花算法
@Service
public class SecKillService {
@Autowired
private SnowFlake snowFlake;
@Autowired
private StringRedisTemplate redisTemplate;
// 秒杀请求队列:容量1000,削峰
private final BlockingQueue<Long> secKillQueue = new ArrayBlockingQueue<>(1000);
// 初始化消费者线程池:5个线程消费队列
@PostConstruct
public void initConsumer() {
ExecutorService executor = Executors.newFixedThreadPool(5);
for (int i = 0; i < 5; i++) {
executor.execute(this::handleSecKill);
}
}
// 秒杀入口方法:仅将请求放入队列,快速返回
public String secKill(Long goodsId, Long userId) {
// 1. 分布式限流(Redis令牌桶,省略实现)
if (!redisTemplate.opsForValue().decrement("token:secKill:" + goodsId).isPresent()) {
return "秒杀失败,请求过于频繁!";
}
// 2. 库存预检查(Redis)
if (Long.parseLong(redisTemplate.opsForValue().get("stock:" + goodsId)) <= 0) {
return "秒杀失败,库存不足!";
}
// 3. 请求入队,快速返回
boolean offer = secKillQueue.offer(userId);
return offer ? "秒杀请求已接收,请等待结果!" : "秒杀失败,系统繁忙!";
}
// 消费者方法:处理队列中的秒杀请求,实现核心逻辑
private void handleSecKill() {
while (true) {
try {
Long userId = secKillQueue.take();
Long goodsId = 1L; // 示例:固定商品ID,实际从请求中获取
// 1. Redis原子扣减库存(防止超卖)
Long stock = redisTemplate.opsForValue().decrement("stock:" + goodsId);
if (stock < 0) {
redisTemplate.opsForValue().increment("stock:" + goodsId); // 回滚
System.out.println("用户" + userId + "秒杀失败,库存不足!");
continue;
}
// 2. 生成全局唯一订单ID(雪花算法)
Long orderId = snowFlake.nextId();
// 3. 插入订单(异步写入数据库,最终一致性)
CompletableFuture.runAsync(() -> {
// 省略数据库插入逻辑
System.out.println("用户" + userId + "秒杀成功,订单ID:" + orderId);
});
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
break;
}
}
}
}
7.2 项目 2:风控系统(数据挖掘算法落地)
7.2.1 核心业务问题
风控系统的核心是识别恶意请求 / 行为 (如刷单、薅羊毛、恶意注册),需要通过算法分析用户行为特征,实现精准风控。
7.2.2 核心算法选型
表格
| 问题场景 | 算法 / 数据结构 | 选型原因 |
|---|---|---|
| 恶意 IP / 手机号拦截 | 布隆过滤器(Redis) | 高效去重,占用空间小 |
| 用户行为相似度分析 | 余弦相似度算法 | 量化用户行为的相似性 |
| 异常行为检测 | 统计排序算法 | 识别偏离正常范围的行为 |
| 风控规则匹配 | 前缀树(Trie 树) | 快速匹配风控规则关键词 |
7.2.3 核心工程化代码:Redis 布隆过滤器实现恶意 IP 拦截
java
运行
@Service
public class RiskControlService {
@Autowired
private RedissonClient redissonClient;
// 布隆过滤器:存储恶意IP
private final RBloomFilter<String> maliciousIpBloom = redissonClient.getBloomFilter("malicious_ip");
// 初始化布隆过滤器:预计100万IP,误判率0.01
@PostConstruct
public void initBloomFilter() {
maliciousIpBloom.tryInit(1000000L, 0.01);
// 预加载恶意IP(实际从数据库/配置中心加载)
maliciousIpBloom.add("192.168.1.100");
maliciousIpBloom.add("10.0.0.5");
}
// 检测IP是否为恶意IP
public boolean isMaliciousIp(String ip) {
return maliciousIpBloom.contains(ip);
}
// 添加恶意IP到布隆过滤器
public void addMaliciousIp(String ip) {
maliciousIpBloom.add(ip);
}
}
// 风控拦截器
@Component
public class RiskControlInterceptor implements HandlerInterceptor {
@Autowired
private RiskControlService riskControlService;
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
String ip = request.getRemoteAddr();
if (riskControlService.isMaliciousIp(ip)) {
response.setStatus(HttpServletResponse.SC_FORBIDDEN);
response.getWriter().write("禁止访问,您的IP存在恶意行为!");
return false;
}
return true;
}
}
7.3 项目 3:推荐系统(推荐算法落地)
7.3.1 核心业务问题
推荐系统的核心是根据用户行为推荐个性化内容 (如商品、文章、视频),需要通过算法挖掘用户的兴趣偏好。
73.2 核心算法选型
表格
| 问题场景 | 算法 / 数据结构 | 选型原因 |
|---|---|---|
| 关键词联想 | 前缀树(Trie 树) | 高效前缀匹配,支持联想 |
| 基于用户的协同过滤 | 相似度算法(皮尔逊相关系数) | 找到相似兴趣的用户,推荐内容 |
| 热门内容排序 | 堆排序(PriorityQueue) | 快速获取 TopN 热门内容 |
| 用户兴趣存储 | 哈希表 + 红黑树 | O (1) 查找,有序存储兴趣标签 |
7.3.3 核心工程化代码:Trie 树实现商品关键词联想
java
运行
@Service
public class RecommendService {
// 初始化Trie树,存储商品关键词
private final TrieTree trieTree = new TrieTree();
// 初始化商品关键词(实际从数据库加载)
@PostConstruct
public void initTrieTree() {
List<String> goodsKeywords = Arrays.asList("java编程", "java算法", "java开发", "python编程", "python数据分析");
goodsKeywords.forEach(trieTree::insert);
}
// 关键词联想:根据前缀返回所有匹配的关键词
public List<String> keywordAssociate(String prefix) {
return findAllWordsWithPrefix(trieTree, prefix);
}
// 从Trie树中查找所有以prefix为前缀的单词
private List<String> findAllWordsWithPrefix(TrieTree trieTree, String prefix) {
List<String> result = new ArrayList<>();
TrieTree.TrieNode curr = trieTree.getRoot();
// 先找到前缀的最后一个节点
for (char c : prefix.toCharArray()) {
int index = c - 'a';
if (curr.getChildren()[index] == null) {
return result; // 无前缀匹配,返回空
}
curr = curr.getChildren()[index];
}
// 深度优先遍历,获取所有子节点的单词
dfs(curr, new StringBuilder(prefix), result);
return result;
}
// DFS遍历Trie树,拼接单词
private void dfs(TrieTree.TrieNode node, StringBuilder sb, List<String> result) {
if (node.isEnd()) {
result.add(sb.toString());
}
for (int i = 0; i < node.getChildren().length; i++) {
TrieTree.TrieNode child = node.getChildren()[i];
if (child != null) {
sb.append((char) ('a' + i));
dfs(child, sb, result);
sb.deleteCharAt(sb.length() - 1);
}
}
}
}
// Controller
@RestController
@RequestMapping("/recommend")
public class RecommendController {
@Autowired
private RecommendService recommendService;
@GetMapping("/associate")
public List<String> associate(@RequestParam String prefix) {
return recommendService.keywordAssociate(prefix);
}
}
// 访问:/recommend/associate?prefix=java → 返回:[java编程, java算法, java开发]