学生时代找工作前就刷过算法题,不过现在回看当初,单纯根据LeetCode上的分类:数组、字符串、动态规划这样的分类去刷,确实略显盲目。如何才能更高效、更聚焦地刷出最大价值呢?这次换工作,我前后刷了不到200题,面试多家公司,依旧遇到近半未做过的题,但基本都AC,特将心得记录在此,与君共勉。
分享一个以结果为导向的刷题网站:CodeTop 面试题目总结 。根据你想去的公司,去刷他们最高频的题目吧。亲测50%有效。
刷题核心主要3点:数据结构+算法+模板代码,对应到10余种常见题目类型,每种类型刷10题左右,深度理解一些经典题解,基本这一类的题面试中都不会再感棘手。下面就核心要点和题目类型简要梳理,随时使用。
数据结构
常见数据结构诸如数组Array、链表Node、列表List、树Tree、字典Map、集合Set等,在对应语言中的写法及常用方法必须熟记。延伸一点包括栈Stack 、堆Heap、队列Queue等也必须熟悉。
比如Java中将一个数组准递增排序,初始化一个优先级队列(按照大顶堆的形式),栈的出入等,都需要熟练掌握。
特别对于字符串String的一些操作,包括翻转reverse、子串substring、前缀startsWith、分片split等等也会常考,需要熟练。
以前这些知识都需要大家去各种地方搜集,如今随着大模型的兴起,对于数据结构的快速熟悉,大模型能起到事半功倍的效果。例如上面这些数据结构的基础逻辑、常用方法、模板代码等大模型都能快速回答,效果极佳。
算法
- 排序:快排、归并排序、堆排序,必须完全掌握,其他如冒泡排序、桶排序、插入排序等尽可能掌握。练手:. - 力扣(LeetCode)
- 二分查找:. - 力扣(LeetCode),注意有单调性就可以用二分,而不一定是明显排序过的数组,练习:. - 力扣(LeetCode)
- 二叉树:前序、中序、后序、层序遍历,递归&非递归,必须掌握。练手:. - 力扣(LeetCode) 及相似题目,. - 力扣(LeetCode), 大部分树相关的题目都可以使用DFS解决,练手:. - 力扣(LeetCode)
- 链表翻转:. - 力扣(LeetCode)
- DFS、BFS:DFS的递归和stack方式,BFS的队列queue方式, . - 力扣(LeetCode)
- 递归+剪枝:. - 力扣(LeetCode)
- 双指针:. - 力扣(LeetCode)
- 单调栈:. - 力扣(LeetCode),困难:. - 力扣(LeetCode)
- 拓扑排序:. - 力扣(LeetCode)
- 动态规划:. - 力扣(LeetCode)
其他例如并查集、贪心、滑动窗口、排列组合、背包问题等都可以使用上面的基础算法来解决,可以不学,防止越学越乱。
除了算法以外,还有一些思维上的优秀解法
- 逆向思维:. - 力扣(LeetCode),最好的扔鸡蛋方式 . - 力扣(LeetCode)
- 预处理:. - 力扣(LeetCode),. - 力扣(LeetCode)
模板代码
对应上述的算法,每种都可以抽象出模板代码,大家也可以借用大模型给出模板代码。下面仅针对两类给出我的模板代码
- 动态规划
// 子问题: 确定是一维DP还是二维DP,一般就是题目问的直接问题,部分难度较高的为中间流转态的DP,最终根据中间态算出结果
// 转移: 分条件确定dp[i]和dp[i-1]或dp[i+1]的关系,有时候因为下标从0开始容易乱,可以先用f(i)写清楚转移方程,再转换为真实下标的转移方程
// 边界:例如二维dp,通常是i或j其中一个动态,另一个固定为0或l-1之类的固定值,如 dp[i][0]=xx, dp[0][j]=yy。从常量开始遍历,计算出所有非常量的值
- 排序
class Solution {
public int[] sortArray(int[] nums) {
// 参数校验
if(null == nums || 1 >= nums.length) {
return nums;
}
int l = nums.length, n = l;
// 堆排序
// heapSort(nums);
// 快排:
// quickSort(nums);
// 归并
// mergeSort(nums);
// 桶排序
bucketSort(nums);
return nums;
}
public int[] mergeSort(int[] nums) {
// 参数校验
if(null == nums || 1 >= nums.length) {
return nums;
}
int l = nums.length;
// 归并排序:将当前区间一分为二,分别递归归并,然后使用插入排序,将子区间2的值逐个插入到子区间1中
this.mergeSort(nums, 0, l-1);
return nums;
}
private void mergeSort(int[] nums, int left, int right) {
if(left >= right) {
return;
}
int mid = left + (right - left)/2;
// System.out.println("mergeSort: left: " + left + " right: " + right + " mid: " + mid);
mergeSort(nums, left, mid);
mergeSort(nums, mid+1, right);
int i1 = left, i2 = mid+1, i = 0;
int[] temp = new int[right - left + 1];
while(i1 <= mid && i2 <= right) {
if(nums[i1] < nums[i2]) {
temp[i++] = nums[i1++];
} else {
temp[i++] = nums[i2++];
}
}
while(i1 <= mid) {
temp[i++] = nums[i1++];
}
while(i2 <= right) {
temp[i++] = nums[i2++];
}
// System.out.println("temp i:" + i);
// for(int k = 0; k < temp.length; k++) {
// System.out.print(temp[k] + ", ");
// }
// System.out.println();
for(int k = 0; k < i; k++) {
nums[left + k] = temp[k];
}
}
public int[] quickSort(int[] nums) {
// 参数校验
if(null == nums || 1 >= nums.length) {
return nums;
}
int l = nums.length;
// 快排:选取一个比较支点 pivot ,当前区间排成 <=pivot pivot >pivot 的三段,再对这三段递归快排
this.quick(nums, 0, l-1);
return nums;
}
private void quick(int[] nums, int left, int right) {
if(left >= right) {
return;
}
int pivot = findPivot(nums, left, right);
quick(nums, left, pivot - 1);
quick(nums, pivot + 1, right);
}
private int findPivot(int[] nums, int left, int right) {
int k = left;
for(int i = left+1; i <= right; i++) {
if(nums[i] > nums[left]) {
continue;
}
swap(nums, ++k, i);
}
if(k != left) {
swap(nums, k, left);
}
return k;
}
public int[] heapSort(int[] nums) {
// 参数校验
if(null == nums || 1 >= nums.length) {
return nums;
}
int l = nums.length, n = l;
// 堆排序:初始化堆
initHeap(nums, n);
// 从右往左遍历数组,每次将 i 和 0 位置的下标交换,直到 i== 0 。
// 每次交换后,i--, n-- . 此时仅堆顶元素不符合大顶堆,针对堆顶元素进行大顶堆化即可
for(int i = l-1; i >= 0; i--) {
swap(nums, 0, i);
heaplify(nums, 0, i);
}
return nums;
}
// 初始化大顶堆
private void initHeap(int[] nums, int n) {
// 参数校验
if(null == nums || n <= 1) {
return;
}
// 从 i = n/2-1 开始到 i == 0,对于每个元素,进行大顶堆化
for(int i = n/2-1; i >= 0; i--) {
heaplify(nums, i, n);
}
}
// 大顶堆化
private void heaplify(int[] nums, int i, int n) {
// 将 i 2i 2i+1 中最大的放在父节点,发生变化的子节点递归大顶堆化
int leftChild = 2*i+1, rightChild = 2*i+2, last = i;
if(leftChild < n && nums[leftChild] > nums[last]) {
last = leftChild;
}
if(rightChild < n && nums[rightChild] > nums[last]) {
last = rightChild;
}
if(last != i) {
swap(nums, last, i);
heaplify(nums, last, n);
}
}
public void bucketSort(int[] nums) {
// 参数校验
if(null == nums || 1 >= nums.length) {
return;
}
int l = nums.length, n = l;
// 确定分桶数量 bucketNum ,
int bucketNum = l;
// 当前数的桶索引计算 calBucketIndex() , 数组中的数最好能均匀的分到每个桶中
List<List<Integer>> bucketList = new ArrayList<>(bucketNum);
for(int i = 0; i < bucketNum; i++) {
bucketList.add(new ArrayList<Integer>());
}
for(int num : nums) {
int index = calBucketIndex(num, -5 *10*10*10*10, 5 *10*10*10*10, bucketNum);
bucketList.get(index).add(num);
}
// 对于每个桶采取某种排序算法,例如快排
int k = 0;
for(List<Integer> list : bucketList) {
int size = list.size();
int[] arr = new int[size];
for(int i = 0; i < size; i++) {
arr[i] = list.get(i);
}
Arrays.sort(arr);
for(int i = 0; i < size; i++) {
nums[k++] = arr[i];
}
}
// 分桶按序合并
}
private int calBucketIndex(int cur, int low, int high, int bucketNum) {
int capacity = (high - low + 1) / bucketNum;
capacity = Math.max(capacity, 1);
return Math.min((cur - low + 1) / capacity, bucketNum - 1);
}
private void swap(int[] nums, int a, int b) {
int temp = nums[a];
nums[a] = nums[b];
nums[b] = temp;
}
}