Java算法刷题没思路 程序员面试必考的高频题目与精选资源指南
哎,说实话,每次看到”算法刷题没思路”这几个字,我就忍不住想拍拍你的肩膀说——别慌,这事儿我太懂了。
先给你吃颗定心丸:那些让你头疼的算法题,真不是天才专属。我见过太多程序员,从刷第一道题时连for循环都写不利索,到后来手撕LeetCode硬如铁,这条路走通了,你就也走通了。
今天咱们不整那些虚头巴脑的”三天速成”鸡汤,我直接把压箱底的东西全掏出来,让你明明白白知道该往哪儿使劲儿。
一、为什么你总是没思路?真相扎心但有用
很多人刷题刷到怀疑人生,不是因为笨,而是因为方向搞错了。
1.1 你的刷题姿势可能从一开始就错了
让我问你几个问题,对号入座:
- 刷题时是不是一遇到困难就看题解,看完觉得自己会了,过两天再遇到照样懵?
- 是不是一上来就刷困难题,然后崩溃弃坑?
- 是不是题目刷了不少,但换道题还是没思路?
- 是不是一看到”数组”、”链表”就脑子一片空白?
如果你中了其中两条以上,恭喜你——你的问题出在刷题方法论上,不是脑子问题。
我认识一个程序员朋友,小陈。他之前在一家互联网公司干了三年后端,一直觉得自己代码写得还行。结果去面大厂,第一关算法就挂了。面试题目是”找出数组中第三个最大的数”,他愣是想了二十分钟,最后用暴力解法写出来,面试官点点头说”思路可以优化一下”。
后来他问我:”为什么我刷题刷了那么多,一上考场就懵?”
我跟他说了一句话:你刷的是”题”,不是”思维”。
这两者差得远了。
1.2 算法思维的本质是什么?
算法思维,说白了就是把复杂问题拆成你能搞定的小事。
举个例子,你去做一顿大餐,你会先把所有菜洗好、切好、备料,然后再开火。你不会直接把生肉扔进锅里,然后手忙脚乱地找调料。
算法也是这个逻辑:
- 把问题拆解成小问题
- 确认每个小问题你能解决
- 把它们拼起来
那些让你”没思路”的题目,本质上是因为你还没建立起”识别题型→套用模式”的反射机制。
一旦你认识了足够多的题型,你会发现:80%的算法题都是换皮不换骨。
二、Java程序员面试必考的高频题型(附代码)
好,咱们进入正题。我把面试中出现频率最高、最经典的算法题型给你梳理一遍,每道题我都配上Java代码。
这些题你要是能独立写出来,面试基本就能横着走了。
2.1 数组类:两数之和 —— 哈希表的经典应用
题目:给定一个整数数组 nums 和一个目标值 target,请你在该数组中找出和为目标值的那两个整数,并返回他们的数组下标。
思路:用哈希表存储已经遍历过的数字,每次检查 target - num 是否已经在表中。
public class TwoSum {
public int[] twoSum(int[] nums, int target) {
// 用HashMap存储数字和它的索引
Map<Integer, Integer> map = new HashMap<>();
for (int i = 0; i < nums.length; i++) {
int complement = target - nums[i];
// 如果找到了,直接返回
if (map.containsKey(complement)) {
return new int[]{map.get(complement), i};
}
// 否则存入map
map.put(nums[i], i);
}
// 题目保证有解,不会走到这里
return new int[0];
}
}
关键点:这道题的精髓在于一次遍历搞定。很多人会想到用双重循环(O(n²)),但哈希表把时间复杂度降到了O(n)。
2.2 链表类:反转链表 —— 指针操作的经典
题目:反转一个单链表。
思路:用三个指针,分别记录前一个节点、当前节点、下一个节点。
class ListNode {
int val;
ListNode next;
ListNode(int x) { val = x; }
}
public class ReverseLinkedList {
// 迭代法
public ListNode reverseList(ListNode head) {
ListNode prev = null; // 前一个节点
ListNode curr = head; // 当前节点
while (curr != null) {
ListNode nextTemp = curr.next; // 暂存下一个节点
curr.next = prev; // 反转指针
prev = curr; // prev前进
curr = nextTemp; // curr前进
}
return prev; // prev就是新的头节点
}
// 递归法
public ListNode reverseListRecursive(ListNode head) {
if (head == null || head.next == null) {
return head;
}
ListNode newHead = reverseListRecursive(head.next);
head.next.next = head;
head.next = null;
return newHead;
}
}
关键点:链表反转是面试高频中的高频。递归版本更优雅,迭代版本更实用,建议两种都熟练掌握。
2.3 双指针类:盛最多水的容器 —— 贪心思维
题目:给定 n 个非负整数 a₁, a₂, …, aₙ,每个数代表坐标中的一个点 (i, aᵢ)。在坐标内画 n 条垂直线,找出其中的两条线,使得它们与 x 轴共同构成的容器可以容纳最多的水。
思路:双指针从两端向中间移动,每次移动较短的那条线。
public class MaxArea {
public int maxArea(int[] height) {
int left = 0;
int right = height.length - 1;
int maxWater = 0;
while (left < right) {
// 计算当前面积
int area = Math.min(height[left], height[right]) * (right - left);
maxWater = Math.max(maxWater, area);
// 移动较短的那条线
if (height[left] < height[right]) {
left++;
} else {
right--;
}
}
return maxWater;
}
}
关键点:这道题的精髓在于为什么移动短边不会错过最优解。因为面积由短边和宽度共同决定,移动长边只会让宽度变小,面积不可能更大。这个贪心证明是面试时经常追问的。
2.4 二分查找类:搜索旋转排序数组
题目:整数数组 nums 按升序排列,数组中的值互不相同。在传递给函数之前,nums 在预先未知的某个下标上进行了旋转。请你找出其中最小的元素。
思路:修改版二分查找,关键在于判断哪一半是有序的。
public class SearchRotatedArray {
public int findMin(int[] nums) {
int left = 0;
int right = nums.length - 1;
while (left < right) {
int mid = left + (right - left) / 2;
if (nums[mid] > nums[right]) {
// 最小值在右半部分
left = mid + 1;
} else {
// 最小值在左半部分(包括mid)
right = mid;
}
}
return nums[left];
}
// 更常见的版本:在旋转数组中搜索目标值
public int search(int[] nums, int target) {
int left = 0;
int right = nums.length - 1;
while (left <= right) {
int mid = left + (right - left) / 2;
if (nums[mid] == target) {
return mid;
}
// 判断哪一半是有序的
if (nums[left] <= nums[mid]) {
// 左半部分有序
if (target >= nums[left] && target < nums[mid]) {
right = mid - 1;
} else {
left = mid + 1;
}
} else {
// 右半部分有序
if (target > nums[mid] && target <= nums[right]) {
left = mid + 1;
} else {
right = mid - 1;
}
}
}
return -1;
}
}
关键点:二分查找的变体很多,但核心思路不变——每次排除一半。旋转数组的关键在于找到无序的那一半,目标值一定在有序的那一半里或者另一半里。
2.5 动态规划类:爬楼梯 —— DP入门必刷
题目:假设你正在爬楼梯。需要 n 阶你才能到达楼顶。每次你可以爬 1 或 2 个台阶。有多少种不同的方法可以爬到楼顶?
思路:第 n 阶只能从第 n-1 阶或第 n-2 阶来,所以 f(n) = f(n-1) + f(n-2)。
public class ClimbingStairs {
public int climbStairs(int n) {
if (n <= 2) {
return n;
}
// 只需要保存前两个状态,空间优化到O(1)
int prev2 = 1; // f(1)
int prev1 = 2; // f(2)
int current = 0;
for (int i = 3; i <= n; i++) {
current = prev1 + prev2;
prev2 = prev1;
prev1 = current;
}
return current;
}
// 带记忆化的递归版本
public int climbStairsMemo(int n) {
int[] memo = new int[n + 1];
Arrays.fill(memo, -1);
return climbStairsHelper(n, memo);
}
private int climbStairsHelper(int n, int[] memo) {
if (n <= 2) {
return n;
}
if (memo[n] != -1) {
return memo[n];
}
memo[n] = climbStairsHelper(n - 1, memo) + climbStairsHelper(n - 2, memo);
return memo[n];
}
}
关键点:这道题是动态规划的入门题,但它教会你DP的核心思想——重叠子问题和状态转移方程。面试时经常会在这道题基础上加变化,比如每次可以爬1、2或3步。
2.6 栈类:有效括号 —— 经典栈应用
题目:给定一个只包括 ‘(‘,’)‘,’{‘,’}‘,’[‘,’]’ 的字符串,判断字符串是否有效。
思路:遇到左括号入栈,遇到右括号检查栈顶是否匹配。
public class ValidParentheses {
public boolean isValid(String s) {
if (s.length() % 2 != 0) {
return false;
}
Stack<Character> stack = new Stack<>();
Map<Character, Character> mapping = new HashMap<>();
mapping.put(')', '(');
mapping.put('}', '{');
mapping.put(']', '[');
for (char c : s.toCharArray()) {
if (mapping.containsValue(c)) {
stack.push(c);
} else if (mapping.containsKey(c)) {
if (stack.isEmpty() || stack.pop() != mapping.get(c)) {
return false;
}
}
}
return stack.isEmpty();
}
}
关键点:这道题看似简单,但面试官经常追问扩展——比如最小添加括号使括号有效、括号生成等问题,核心思路都是一样的。
2.7 深度优先搜索(DFS):岛屿数量
题目:给你一个由 ‘1’(陆地)和 ‘0’(水)组成的的二维网格,请你计算网格中岛屿的数量。
思路:遍历每个格子,如果遇到陆地,就从这个点开始DFS,把所有连通的陆地都标记为已访问。
public class NumberOfIslands {
public int numIslands(char[][] grid) {
if (grid == null || grid.length == 0) {
return 0;
}
int count = 0;
int rows = grid.length;
int cols = grid[0].length;
for (int i = 0; i < rows; i++) {
for (int j = 0; j < cols; j++) {
if (grid[i][j] == '1') {
dfs(grid, i, j);
count++;
}
}
}
return count;
}
private void dfs(char[][] grid, int i, int j) {
// 边界检查和是否已访问
if (i < 0 || i >= grid.length || j < 0 || j >= grid[0].length || grid[i][j] != '1') {
return;
}
grid[i][j] = '0'; // 标记为已访问
// 向四个方向继续DFS
dfs(grid, i + 1, j);
dfs(grid, i - 1, j);
dfs(grid, i, j + 1);
dfs(grid, i, j - 1);
}
}
关键点:DFS是处理图/网格问题的神器。记住这个模板:边界检查 → 标记已访问 → 递归探索邻居。面试时经常在这个基础上加条件,比如求最大岛屿面积、岛屿周长等。
2.8 广度优先搜索(BFS):二叉树的层序遍历
题目:给你一个二叉树,请你返回其按层序遍历得到的节点值序列。
思路:用队列实现,每层的所有节点入队,然后依次出队处理。
import java.util.*;
class TreeNode {
int val;
TreeNode left;
TreeNode right;
TreeNode(int x) { val = x; }
}
public class BinaryTreeLevelOrder {
public List<List<Integer>> levelOrder(TreeNode root) {
List<List<Integer>> result = new ArrayList<>();
if (root == null) {
return result;
}
Queue<TreeNode> queue = new LinkedList<>();
queue.offer(root);
while (!queue.isEmpty()) {
int levelSize = queue.size();
List<Integer> currentLevel = new ArrayList<>();
for (int i = 0; i < levelSize; i++) {
TreeNode node = queue.poll();
currentLevel.add(node.val);
if (node.left != null) {
queue.offer(node.left);
}
if (node.right != null) {
queue.offer(node.right);
}
}
result.add(currentLevel);
}
return result;
}
}
关键点:BFS用队列,DFS用栈(或递归)。层序遍历是BFS最经典的应用,面试中经常扩展到”之字形遍历”、”每层求平均值”等变体。
2.9 堆/优先队列:前K个最大的元素
题目:给你一个整数数组 nums 和一个整数 k,请你返回其中前 k 个最大的元素。
思路:维护一个大小为 k 的最小堆,堆顶就是第 k 大的元素。
import java.util.*;
public class TopKFrequent {
public int[] topKFrequent(int[] nums, int k) {
// 统计每个数字出现的频率
Map<Integer, Integer> frequencyMap = new HashMap<>();
for (int num : nums) {
frequencyMap.put(num, frequencyMap.getOrDefault(num, 0) + 1);
}
// 维护一个大小为k的最小堆
PriorityQueue<Integer> minHeap = new PriorityQueue<>((a, b) ->
frequencyMap.get(a) - frequencyMap.get(b));
for (int num : frequencyMap.keySet()) {
minHeap.add(num);
if (minHeap.size() > k) {
minHeap.poll();
}
}
// 输出结果
int[] result = new int[k];
for (int i = 0; i < k; i++) {
result[i] = minHeap.poll();
}
return result;
}
}
关键点:堆的应用场景很多——”前K大”、”第K大”、”数据流的中位数”等等,核心思路都是用固定大小的堆来维护最优解。
2.10 回溯算法:全排列
题目:给定一个不含重复数字的数组 nums,返回其所有可能的全排列。
思路:回溯模板——选择 → 探索 → 撤销选择。
import java.util.*;
public class Permutations {
public List<List<Integer>> permute(int[] nums) {
List<List<Integer>> result = new ArrayList<>();
backtrack(nums, 0, result);
return result;
}
private void backtrack(int[] nums, int start, List<List<Integer>> result) {
// 递归终止条件
if (start == nums.length) {
List<Integer> current = new ArrayList<>();
for (int num : nums) {
current.add(num);
}
result.add(current);
return;
}
for (int i = start; i < nums.length; i++) {
// 做选择
swap(nums, start, i);
// 递归探索
backtrack(nums, start + 1, result);
// 撤销选择
swap(nums, start, i);
}
}
private void swap(int[] nums, int i, int j) {
int temp = nums[i];
nums[i] = nums[j];
nums[j] = temp;
}
}
关键点:回溯算法的本质是穷举所有可能的选择。模板是固定的,关键是找准”选择列表”、”终止条件”和”撤销操作”。
三、刷题规划:别盲目刷题,要有策略
光有题目不够,你得知道怎么刷。
我见过太多人刷了500道题,面试还是挂。为什么?因为他们的刷题路径是乱的。
3.1 刷题顺序建议
按这个顺序来,效率最高:
第一阶段:基础数据结构和简单题(1-2周)
- 数组:两数之和、三数之和、盛水容器
- 链表:反转链表、环形链表、合并两个有序链表
- 字符串:回文串、反转字符串、最长公共前缀
- 目标:熟悉Java基础API,建立信心
第二阶段:核心数据结构(2-3周)
- 栈:有效括号、最小栈、柱状图最大矩形
- 队列:用栈实现队列、滑动窗口最大值
- 树:二叉树遍历(前中后序、层序)、二叉搜索树、最大深度
- 哈希表:最长无重复子串、字母异位词分组
- 目标:掌握核心数据结构的使用场景
第三阶段:高级算法(3-4周)
- 二分查找:旋转数组搜索、搜索插入位置
- 双指针:接雨水、颜色分类
- 动态规划:爬楼梯、背包问题、最长递增子序列
- 回溯:全排列、子集、括号生成
- BFS/DFS:岛屿数量、二叉树遍历、拓扑排序
- 目标:掌握解题模式,建立题型识别能力
第四阶段:高频题专项突破(2-3周)
- 重点攻克LeetCode Hot 100和剑指Offer
- 针对薄弱环节专项练习
- 目标:形成肌肉记忆,看到题目就知道思路
3.2 正确的刷题姿势
- 每道题都要独立思考至少20分钟,不要一遇到困难就看题解
- 即使做了出来,也要看最优解,学习别人的思路
- 一周后重新做,检验是否真正掌握
- 建立错题本,记录解题思路和时间
- 模拟面试环境,限时完成
四、精选资源推荐
4.1 刷题平台
| 平台 | 特点 | 适合阶段 |
|---|---|---|
| LeetCode | 题目最多,更新最快,面试必备 | 全程 |
| 牛客网 | 国内面试真题多,有模拟面试功能 | 冲刺阶段 |
| 剑指Offer | 经典中的经典,很多题目是面试原题 | 基础阶段 |
| HihoCoder | 题目质量高,适合进阶 | 进阶阶段 |
4.2 学习资源
视频教程:
- 代码随想录(卡尔):讲解非常清晰,适合入门
- LeetCode题解视频:B站上很多优质UP主
- 左程云《程序员代码面试指南》:大厂面试官写的,角度很独特
刷题手册:
- LeetCode Hot 100:必刷,覆盖了高频考点
- 剑指Offer 66题:很多公司的面试题库来源
- Neetcode 150:国外很火的刷题清单,按题型分类
刷题工具:
- LeetCode官方插件:VS Code插件,直接在编辑器刷题
- 算法可视化网站:visualgo.net,帮助理解算法执行过程
五、面试实战技巧
刷题归刷题,最终还是要面试见真章。
5.1 面试时遇到不会的题怎么办?
不要直接说”我不会”,试试这样:
- 先确认题目理解:”我理解这道题是要求XXX,对吗?”
- 说出你的思路:”我想到一个暴力解法,先遍历所有…但时间复杂度是O(n²),可能不够好”
- 询问提示:”有没有什么约束条件我需要注意的?”
- 尝试改进:”如果用哈希表来优化,时间复杂度可以降到O(n)”
- 即使没写出来,思路清晰也能加分
5.2 代码规范要点
- 变量名要有意义,不要 a、b、c
- 关键步骤加注释
- 考虑边界条件(空输入、单个元素等)
- 写完代码自己过一遍测试用例
5.3 常见问题
Q:刷多少题才能应付面试? A:至少刷200道,其中100道要能独立快速写出。质量比数量重要。
Q:一天刷几道题合适? A:3-5道高质量题目,比10道水题更有用。
Q:怎么判断自己是否准备好了? A:能独立完成LeetCode Hot 100的80%,面试基本没问题。
六、最后说几句
算法刷题这事儿,就像练武功——没有捷径,但有方法。
我认识的大厂程序员,基本都刷过几百道题。这不是什么炫耀的资本,而是基本功。就像厨师要会切菜,工程师要会写算法。
你现在的”没思路”,只是因为你见的题型还不够多。
坚持刷30天,每天3道题,你会发现自己变了。
不是因为你变聪明了,而是因为你见过足够多的题型,知道怎么拆解问题。
最后送你一句话:算法不难,难的是你还没开始。
现在就去打开LeetCode,选一道简单题,开始刷吧。
这篇指南我会持续更新,如果你有其他想了解的算法题或者刷题问题,随时来找我聊。咱们一起把这块硬骨头啃下来。
