力扣刷题从入门到实战 Java算法学习全套资源盘点 牛客网菜鸟教程数据结构与排序算法高效学习路径推荐
说实话,我第一次接触算法题的时候,脑子里完全是懵的。刷了两天的数组反转题,连最简单的双指针法都写不利索,甚至怀疑自己是不是根本不适合干编程这条路。但说实话,算法这东西,它真的不玄乎,它就是需要你花时间去啃,去理解,去练。今天我想把所有能帮到你的东西都给你整理出来,包括我现在还在用的一些资源和学习方法。
先说说力扣吧,这个平台真的是目前国内刷题最主流的选择。它的题目质量很高,而且分类做得非常细致,从热题100到专项练习,再到公司面经,基本上你需要的它都有。我刚入门的时候,就是跟着热题100刷的,因为里面的题确实是经过大家验证过的经典题。我建议你从最简单的开始,别一上来就挑战困难题,那会直接毁掉你的自信心。
力扣的标签系统很好用,比如你想练习双指针,可以直接筛选所有带双指针标签的题。像经典的”两数之和”、”盛最多水的容器”、”三数之和”,这些题你刷过去之后,基本就能掌握双指针的核心思想了。我在刷的时候发现,有些题你看一眼题解觉得”原来如此”,但你真正自己去写的时候,可能连边界条件都处理不好。所以我的建议是,每道题你至少要自己独立写一遍,哪怕参考了思路,也要自己把代码敲出来,调试通过。
我来给你举一个具体的例子,双指针的经典题”移动零”。这个题看起来简单,但里面有很多细节值得推敲。
class Solution {
public void moveZeroes(int[] nums) {
// 用快慢指针的思路
// slow指针指向下一个非零元素应该放置的位置
// fast指针遍历整个数组
int slow = 0;
for (int fast = 0; fast < nums.length; fast++) {
// 如果当前元素不为零,就把它放到slow的位置
if (nums[fast] != 0) {
nums[slow] = nums[fast];
slow++;
}
}
// 剩下的位置全部填零
for (int i = slow; i < nums.length; i++) {
nums[i] = 0;
}
}
}
这个题的精髓其实就在于你要理解:为什么不需要真正地去”移动”零,而是先把非零元素往前放,然后再把后面补零。很多初学者会想到用交换的方式,但那样做的话,代码反而更复杂。算法题很多时候考的不是你写了多少行代码,而是你能不能找到最简洁的解法。
接下来聊聊数据结构这块。数据结构是算法的基础,你如果数据结构都没搞清楚,刷题的时候就会非常吃力。我的建议是先系统学一遍数据结构,然后再开始大量刷题。
数组和字符串是最基础的,但也是最容易被低估的。很多初学者觉得数组不就是存数据吗,有什么好学的。但你要知道,数组的很多操作直接决定了你的时间复杂度。比如你用一个数组来模拟哈希表,或者用数组来做滑动窗口,这些都是非常常见的技巧。字符串的话,重点要掌握它的不可变性,在Java中,String对象一旦创建就不能修改,所以大量拼接字符串的时候要用StringBuilder。
链表是一个非常重要的数据结构,力扣上有很多链表相关的题。比如”反转链表”、”合并两个有序链表”、”环形链表”等。学链表的时候,画图是非常有用的,你把指针怎么指、节点怎么连画出来,思路就清晰了。我刚开始学链表的时候,每次做题都会在纸上画一遍,这个习惯我一直保持到现在。
// 反转链表 - 这是链表入门必刷的题
class ListNode {
int val;
ListNode next;
ListNode(int val) {
this.val = val;
}
}
class Solution {
public ListNode reverseList(ListNode head) {
ListNode prev = null; // 前一个节点,初始为null
ListNode curr = head; // 当前节点,从头开始
while (curr != null) {
ListNode nextTemp = curr.next; // 先保存下一个节点
curr.next = prev; // 反转指针方向
prev = curr; // prev前进一步
curr = nextTemp; // curr前进一步
}
return prev; // prev现在是新的头节点
}
}
这个反转链表的题,我建议你至少手动画图理解三遍。第一遍看题,第二遍看题解,第三遍自己写。写的时候可能会卡住,但卡住之后再去想,记忆会深刻得多。
栈和队列也是面试中非常高频的考点。栈的特点是后进先出(LIFO),队列是先进先出(FIFO)。在Java中,栈可以用Stack类,但更推荐用Deque(ArrayDeque),因为Stack是线程安全的,性能略差。队列的话,用LinkedList或者ArrayDeque都可以。
栈的经典应用题是”有效的括号”,这道题你刷过之后,基本就理解栈的核心思想了。
import java.util.Deque;
import java.util.ArrayDeque;
class Solution {
public boolean isValid(String s) {
Deque<Character> stack = new ArrayDeque<>();
for (char c : s.toCharArray()) {
if (c == '(' || c == '[' || c == '{') {
// 左括号入栈
stack.push(c);
} else {
// 右括号时,栈为空说明没有配对的左括号
if (stack.isEmpty()) {
return false;
}
char top = stack.pop();
// 检查是否匹配
if (c == ')' && top != '(') return false;
if (c == ']' && top != '[') return false;
if (c == '}' && top != '{') return false;
}
}
// 最后栈必须为空才是有效的
return stack.isEmpty();
}
}
队列的经典题是”用栈实现队列”,这道题能帮你更好地理解两种数据结构的区别和联系。
树这部分是算法学习中的一个坎,很多人学到这里开始感觉吃力。二叉树、二叉搜索树、平衡二叉树,这些概念要搞清楚。递归是处理树的最自然的方式,因为树本身就是递归定义的结构。二叉树的前中后序遍历,DFS(深度优先搜索)的思路,这些你都要熟练。
我学树的时候,发现一个特别好的方法:把每道题都画成图。二叉树的题,你画了图之后,递归的边界条件和递推关系一下子就清楚了。比如”二叉树的最大深度”这道题:
class TreeNode {
int val;
TreeNode left;
TreeNode right;
TreeNode(int val) {
this.val = val;
}
}
class Solution {
public int maxDepth(TreeNode root) {
// 递归的边界条件:空节点深度为0
if (root == null) {
return 0;
}
// 递归求左子树的最大深度
int leftDepth = maxDepth(root.left);
// 递归求右子树的最大深度
int rightDepth = maxDepth(root.right);
// 当前节点的最大深度 = 左右子树最大深度的较大值 + 1
return Math.max(leftDepth, rightDepth) + 1;
}
}
图这个部分相对复杂一些,但在面试中出现的频率也很高。图的表示方式主要有两种:邻接矩阵和邻接表。遍历方式有DFS和BFS两种。Dijkstra算法、最小生成树这些算法,如果你目标是大厂的话,还是需要了解的。
排序算法是必须掌握的基础知识。常见的排序算法有冒泡排序、选择排序、插入排序、希尔排序、归并排序、快速排序、堆排序等。你要清楚每种排序的时间复杂度和空间复杂度,以及它们的适用场景。
冒泡排序是最简单的,但效率最低,时间复杂度是O(n²)。
// 冒泡排序
public static void bubbleSort(int[] arr) {
int n = arr.length;
// 外层控制需要比较的轮数
for (int i = 0; i < n - 1; i++) {
// 内层遍历,每轮把最大的元素"冒泡"到末尾
for (int j = 0; j < n - 1 - i; j++) {
if (arr[j] > arr[j + 1]) {
int temp = arr[j];
arr[j] = arr[j + 1];
arr[j + 1] = temp;
}
}
}
}
快速排序是面试中最常考的,它的平均时间复杂度是O(nlogn),而且常数因子很小,实际运行效率很高。快排的核心思想是分治:选一个基准值,把比它小的放左边,比它大的放右边,然后递归处理左右两部分。
// 快速排序
public static void quickSort(int[] arr, int left, int right) {
if (left < right) {
// 获取分区点
int pivotIndex = partition(arr, left, right);
// 递归排序左半部分
quickSort(arr, left, pivotIndex - 1);
// 递归排序右半部分
quickSort(arr, pivotIndex + 1, right);
}
}
private static int partition(int[] arr, int left, int right) {
// 选最后一个元素作为基准
int pivot = arr[right];
// i指向小于基准区域的最后一个元素的下一个位置
int i = left - 1;
for (int j = left; j < right; j++) {
// 如果当前元素小于等于基准,就把它交换到左侧区域
if (arr[j] <= pivot) {
i++;
int temp = arr[i];
arr[i] = arr[j];
arr[j] = temp;
}
}
// 把基准元素放到正确的位置
int temp = arr[i + 1];
arr[i + 1] = arr[right];
arr[right] = temp;
return i + 1;
}
归并排序也是O(nlogn)的复杂度,而且它是稳定的排序算法。面试中问到排序算法的时候,归并排序经常是加分项。
// 归并排序
public static void mergeSort(int[] arr, int left, int right) {
if (left < right) {
int mid = left + (right - left) / 2; // 防止溢出的写法
mergeSort(arr, left, mid); // 递归排序左半部分
mergeSort(arr, mid + 1, right); // 递归排序右半部分
merge(arr, left, mid, right); // 合并两个有序数组
}
}
private static void merge(int[] arr, int left, int mid, int right) {
int n1 = mid - left + 1; // 左半部分长度
int n2 = right - mid; // 右半部分长度
// 创建临时数组
int[] leftArr = new int[n1];
int[] rightArr = new int[n2];
// 复制数据到临时数组
for (int i = 0; i < n1; i++) {
leftArr[i] = arr[left + i];
}
for (int j = 0; j < n2; j++) {
rightArr[j] = arr[mid + 1 + j];
}
// 合并两个有序数组
int i = 0, j = 0, k = left;
while (i < n1 && j < n2) {
if (leftArr[i] <= rightArr[j]) {
arr[k++] = leftArr[i++];
} else {
arr[k++] = rightArr[j++];
}
}
// 复制剩余元素
while (i < n1) {
arr[k++] = leftArr[i++];
}
while (j < n2) {
arr[k++] = rightArr[j++];
}
}
资源这块我确实花了不少时间整理的。牛客网是一个很不错的平台,它的题库很全,而且有很多公司面经,你可以看到别人面试时遇到的真实题目。牛客网还有一块社区讨论,你遇到不会的题可以去看看别人的思路。它的编程练习环境也做得不错,支持多种语言,而且提交之后能告诉你哪个测试用例没过,方便你调试。
菜鸟教程的话,它更适合作为入门的参考网站。它的内容比较系统,从Java基础语法到数据结构,再到算法,都有详细的讲解。而且它的例子都很简单直观,适合零基础的同学。不过菜鸟教程的不足之处是算法部分相对基础,如果你要挑战更高难度的题,还是得去力扣这样的平台。
除了这两个,我还推荐一些其他资源。LeetCode中国官网(leetcode.cn)当然要说,它比国际版更贴近国内面试的难度和题型。B站上也有很多很好的算法教程,比如”代码随想录”的B站账号,他们讲的题解非常详细,而且有一个从简单到困难的学习路线,很适合系统学习。
还有”算法图解”这本书,它用图片的方式来讲解算法,非常通俗易懂,特别适合初学者建立直观的理解。如果你想深入一点,可以看看”算法”这本教材(俗称《算法4》),它用Java写的,代码质量很高,而且讲解非常系统。
刷题的方法也很重要。我见过很多同学刷题很努力,但效果不好,主要问题是缺乏规划。我的建议是:
第一阶段,先把基础数据结构和算法学一遍,不需要很深入,但要把核心概念和常见题型过一遍。这个阶段可以看菜鸟教程或者B站上的入门教程。
第二阶段,开始系统刷题。从简单题开始,每天保证一定的题量,但不要贪多。我当时的节奏是一天5到10道题,其中简单题占一部分,中等题占大部分。重要的是每道题都要认真思考,不要急着看题解。
第三阶段,针对薄弱点专项突破。比如你发现自己链表题总是做不出来,那就集中刷一阶段的链表题。力扣的专项练习功能在这里就很有用了。
第四阶段,模拟面试。找一些公司的面试真题,限定时间去做,模拟考试环境。这个阶段可以上牛客网找面经,或者参加LeetCode的周赛。
关于时间规划,如果你是在校生,建议你大三或者研一就开始准备。每天投入一到两个小时,坚持半年到一年,效果会非常明显。如果你在准备秋招,那时间会更紧一些,建议每天至少保持两小时的刷题时间,重点刷热题100和公司面经里的高频题。
还有一个很重要的点:总结。每刷完一个类型的题,你都要花一点时间总结一下,这个类型的题有什么规律,常见的解法是什么,容易出错的边界条件有哪些。我的做法是建立一个笔记,把每道题的关键思路记录下来,这样在面试前翻一翻,回忆会快很多。
算法学习这条路,说不难是假的。我到现在刷了差不多几百道题,还是会遇到一些让我思考很久的题。但每次解出来之后,那种成就感是非常强的。而且说实话,算法思维一旦建立起来,对你理解整个编程领域都很有帮助。你会发现,很多看似复杂的问题,用对方法之后其实是可以很简洁地解决的。
不要害怕困难题,很多困难题其实就是几个简单思路的组合。你刷的题越多,见过的题型越多,遇到新题的时候就越容易找到切入点。保持耐心,保持节奏,坚持下去,你一定会看到进步的。
