leecode Hot100 学习记录
前缀和
不单调数组求任意子数组和
前缀和概念和数列前n项和的概念很像,但是不包含最后一项(左闭右开),模版来自303. 区域和检索 - 数组不可变 - 力扣(LeetCode)
主要特点是,通过定义一个数组前缀和,就可以只需要O(1)复杂度求出任意一个子数组的和结果。
//leecode303.区域和检索:计算前缀和的模版实现
public class NumArray {
private final int [] s;
public NumArray(int [] nums) {
s = new int [nums.length+1];
for (int i = 0; i <nums.length; i++) {
s[i+1] =s[i] + nums[i];//默认定义了s【0】为0
}
}
public int sumRange(int left, int right) {
return s[right+1]-s[left];
}
}应用这个定义可以做560. 和为 K 的子数组 - 力扣(LeetCode)
Q:为什么要用前缀和?
A:因为这个待求的数组是不单调的。使用前缀和定义的数据结构,两个值相减可以求出来任意一个子数组的和。滑动窗口需要满足单调性,当右端点元素进入窗口时,窗口元素和是不能减少的。本题 nums 包含负数,当负数进入窗口时,窗口左端点该往哪个方向移动?无法确定。如果没有负数的话,则可以用滑动窗口。
于是转换成 s[j]-s[i] = k 这样的问题。可以看成是两数之和的变种。
s[j] = k + s[i] => s[right]-k = s[left] , 然后使用哈希表,快速定位是否存在,存在了就说明加1,最后合并所有的值。这里注意必须要用sj-k而不是si+k。这是因为,我们需要进行遍历来填充哈希表,在遍历的过程中去寻找符合条件的i与j。由于j大于i,也就是说,我们只有先知道了j才能知道i。遍历过程是只能先知道遍历到的元素的值,是只能给遍历过的元素key赋值value的。因此,我们只能求得si。因为si表示就是在当前元素sj之前的元素。
两数之和变体:选择更大的那个值与target计算,求之前遍历过的是否存在小的那个值。
public int subarraySum(int[] nums, int k) {
int [] s = new int[nums.length+1];
for (int i=0;i<nums.length;i++){
s[i+1] = s[i]+nums[i];
}
int ans = 0;
Map<Integer, Integer> map = new HashMap<>();
for (int i = 0; i <s.length ; i++) {
ans += map.getOrDefault(s[i]-k,0);//查询已有的哈希表
map.put(s[i],map.getOrDefault(s[i],0)+1);//创建前缀和的值的哈希表元素
}
return ans;
}同样的问题可以解决:53. 最大子数组和 - 力扣(LeetCode)
实际上这个需要计算最大数组和,多考虑一步就是如何得到最大数组和,s[i] - min(s[k])。我们依旧遍历一次前缀数组,但是我们记录前缀数组最小的部分在哪里,然后用当前的和减去之前的最小的和,可以求得当前最大的值。遍历一轮不停更新最小的和与最大的值,就可以得到最终的结果了。注意:min需要从s[0]开始,而这道题目必须要先比较最大值,再更新最小和,否则会将只有一个元素的数组误求成0。
public int maxSubArray(int[] nums) {
int []sum = new int[nums.length+1];
for (int i = 0; i < nums.length; i++) {
sum[i+1] = sum[i] + nums[i];
}//求出前缀和数组
int min = 0;
int maxVal = Integer.MIN_VALUE;
for (int i = 1; i < sum.length; i++) {
if (sum[i]-min > maxVal){
maxVal = sum[i]-min;
}
if (sum[i] < min){
min = sum[i];
}
}
return maxVal;
}滑动窗口求最大值
使用方法:单调队列
“及时去除无用数据,保证双端队列有序”
模拟滑动窗口的过程,发现:如果新加入的比前面的大,那么很可能是窗口最大值。而前面已经加入的肯定不可能是最大值(要比更大的先脱离窗口);而新加入的比前面的小的时候,新加入的元素仍然有可能变成最大值(当后来的逗比新加入的小)
所以最后我们希望记录下最可能成为最大值的元素,而去掉不可能成为最大值的元素,构成了一个单调的变动的集合。最终我们需要的数据结构是左出右进/右出的结构,因此使用的是双端队列。
由于队列维护的结构都是单调的,因此这个方法也叫做单调队列。
说实在的,理解这道题突然有点破防:后来进滑动窗口的一定比窗口里面的更加**’‘年轻‘’**,新来的寿命比你长,价值还比你大,凭什么还留着你个老登。你不可能成为最大的那个了。如果新来的没你有价值,其实你还是有可能成为最大的地位的。退位也是以后的事。
具体代码如下:每个元素只会进一次队列出一次队列,虽然看着是两个循环,但是时间复杂度是O(n),空间复杂度是O(k)
class Solution {
public int[] maxSlidingWindow(int[] nums, int k) {
int[] ans = new int[nums.length-k+1];
Deque<Integer> q = new ArrayDeque<>();
for (int i = 0; i < nums.length; i++) {
// 进
while (!q.isEmpty() && nums[q.getLast()] < nums[i] ) {
//后来者更大,需要删除已有的最新的,保证只留下最可能成为最大值的,可能要去掉多个所以用的是while而不是if
q.removeLast();
}
q.addLast(i);
// 出,能出or该出就出
int left = i-k+1;
if (left >q.getFirst() ) {
q.removeFirst();
}
// 记录元素
if (left>=0){
ans[left]=nums[q.getFirst()];
}
}
return ans;
}
}其他单调队列题目:
- 绝对差不超过限制的最长连续子数组 https://leetcode.cn/problems/longest-continuous-subarray-with-absolute-diff-less-than-or-equal-to-limit/
- 预算内的最多机器人数目 https://leetcode.cn/problems/maximum-number-of-robots-within-budget/
- 和至少为 K 的最短子数组 https://leetcode.cn/problems/shortest-subarray-with-sum-at-least-k/
- 满足不等式的最大值 https://leetcode.cn/problems/max-value-of-equation/
- 跳跃游戏 VI https://leetcode.cn/problems/jump-game-vi/
- 购买水果需要的最少金币数 https://leetcode.cn/problems/minimum-number-of-coins-for-fruits/
覆盖子串类问题
使用方法:数组记录每个字母出现次数,然后比较。
今天自己写出来的第一个hard题目:smile:(虽然判断函数的逻辑有问题抄的灵神的,但是主要逻辑都是自己一气呵成的!大大的进步!)
思路:这类题目给定一个长的串s,给定一个短串t。核心在于如何判断是否s的子串包含t。这个题目要求的是只要覆盖了就行。如abvc覆盖了abc。核心在于写判断什么时候覆盖的逻辑。需要自定义一个函数进行判断。主要逻辑就是滑动窗口来收集所有的覆盖t的子串,然后记录到哈希表里面最后拿出最小的那个就行。
时间复杂度:O(52 * n) = O(n),其中n是字符串s的长度
- isCovered方法:每次调用需要遍历52个字符(26个大写字母 + 26个小写字母),时间复杂度O(52)
- minWindow方法:
- 初始化:O(|t|),统计t中字符频率
- 双指针遍历:O(n),n是s的长度
- 每次移动指针时可能调用isCovered:最坏情况下O(52*n)
具体个人代码如下:
class Solution {
private boolean isCovered(int[] cntS, int[] cntT) {
for (int i = 'A'; i <= 'Z'; i++) {
if (cntS[i] < cntT[i]) {
return false;//反着来快,有一个反例就不行
}
}
for (int i = 'a'; i <= 'z'; i++) {
if (cntS[i] < cntT[i]) {
return false;
}
}
return true;
}
public String minWindow(String s, String t) {
int [] countT = new int[128];
int [] countS = new int[128];
Map<Integer,String> map = new HashMap<>();
int min = Integer.MAX_VALUE;
for (int i=0;i<t.length();i++){
countT[t.charAt(i)]++;
}//统计出来每个字母的频率
int left = 0;
int right = 0;
while (right < s.length()){
countS[s.charAt(right)]++;
right++;
if (isCovered(countS, countT)){
while(left<right && isCovered(countS, countT)){
countS[s.charAt(left)]--;
left++;//这个left一定会加,好处就是实际上最后的left一定是新的滑动窗口起点,继续找子串,而不是停留在原地陷入死循环。
}
map.put(right-left+1,s.substring(left-1,right)) ;//这里得注意,left一定要减一,right前面也是加了之后的所以直接用就行。substring是左闭右开,这里不需要用right+1;
min = Math.min(min,right-left+1);
}
}
if (map.containsKey(min)){
return map.get(min);
}
return "";
}
}数组
区间合并问题
使用方法:排序
合并区间的关键在于,必须先都按照左端点进行排序,这样就可以仅关注右边端点。一旦出现下一个左端点是大于当前的右端的值,那么当前的区间一定就是固定的结果区间了。因为排序之后不存在后面的其他区间左端点比下一个左端点小。
两个关键代码:写排序的比较器;写比较过程的分支结构
关于比较器:
Comparator 是 Java 中的一个函数式接口,位于 java.util 包下。它定义了一个用于比较两个对象的方法 compare,该方法接受两个参数,并返回一个整数值。返回值的含义如下:
- 如果返回值小于 0,表示第一个参数小于第二个参数。
- 如果返回值等于 0,表示两个参数相等。
- 如果返回值大于 0,表示第一个参数大于第二个参数。
也就是说,如果写(a,b)->return a - b; 就肯定是正序了,因为如果a大,那么返回的就是正数,而a实际就会排在后面了。反之,如果写成return b-a,那么就是倒序,因为b大,返回正数,让a排在了b的前面。也就是说,返回值的符号实际上表示了第一个参数和第二个参数的关系,谁小谁在前,谁大谁靠后,然后才看出来是不是正序/倒序。
具体代码如下:
public int[][] merge(int[][] intervals) {
List<int []> ans = new ArrayList<>();
Arrays.sort(intervals,(a,b)->a[0]-b[0]);//正序排序左端点
for (int i = 0; i < intervals.length; i++) {
if (!ans.isEmpty() && ans.getLast()[1]>=intervals[i][0]){
ans.getLast()[1]=Math.max(ans.getLast()[1],intervals[i][1]);
}else {
ans.addLast(intervals[i]);
}
}
return ans.toArray(new int[ans.size()][]);
}轮转数组
关键在于两个注意点:
一是要将k取模,大于数组长度的k相当于先转一圈(没变)再转几个,需要防止数组越界;
二是了解java特性,java没有指针,传递的是数组的引用。在Java中,数组是对象,所以当数组作为参数传递时,传递的是引用(内存地址),而不是数组本身的副本。
class Solution {
public void ratateUtil(int [] nums,int start,int end){
int l = start;
int r = end;
while( l < r){
int temp = nums[l];
nums[l] = nums[r];
nums[r] = temp;
l++;
r--;
}
}
public void rotate(int[] nums, int k) {
k = k % nums.length;
ratateUtil(nums,0,nums.length-1);
ratateUtil(nums,0,k-1);
ratateUtil(nums,k,nums.length-1);
}
}求除自身外数组乘积
题目238. 除自身以外数组的乘积 - 力扣(LeetCode)
关键点:这道题目不能用除法,要模仿前缀和的方法,实际上可以用前缀乘积和后缀乘积来做。直接用前缀*后缀就可以了。
//普通方法
public int[] productExceptSelf(int[] nums) {
// 先计算前缀后缀
int [] pre = new int[nums.length+1];
pre[0] = 1;
int [] suf = new int[nums.length+1];
suf[nums.length-1] =1;//注意前缀和后缀的初始化都是基于源数组的起始和末尾的,乘积所以用1初始化
for (int i = 1; i <=nums.length; i++){
pre[i] = pre[i-1]*nums[i-1];
}
for (int i = nums.length-2; i >=0; i--){
suf[i] = suf[i+1]*nums[i+1];
}
int []res = new int[nums.length];
for(int i =0;i<nums.length;i++){
res[i] = pre[i]*suf[i];
}
return res;
}
//优化空间复杂度的方法
public int[] productExceptSelf(int[] nums) {
int n = nums.length;
int[] suf = new int[n];
suf[n - 1] = 1;
for (int i = n - 2; i >= 0; i--) {
suf[i] = suf[i + 1] * nums[i + 1];
}
int pre = 1;
for (int i = 0; i < n; i++) {
// 此时 pre 为 nums[0] 到 nums[i-1] 的乘积,直接乘到 suf[i] 中
suf[i] *= pre;
pre *= nums[i];
}
return suf;
}缺失的第一个正数
题目:41. 缺失的第一个正数 - 力扣(LeetCode)
注意:这道题目需要时间复杂度为O(n),空间复杂度为O(1)。所以核心在于交换,我们的排查方式就是让每个数归位到正确的位置上,然后从头开始找第一个不在位置的正数;如果找不到就是n+1。
- 缺失的第一个正数表明一定是范围在1-n,不可能更大,作为交换判断的条件。这种场景下的交换排序的时间复杂度依旧是O(n)
- 一直换到当前的位置变成正确的值。
public int firstMissingPositive(int[] nums) {
for (int i = 0; i < nums.length; i++) {
//给数组的每个数找他们应该在的位置,发现不对就交换。让原本的数组从1开始。所以这里用的是=nums[nums[i]-1]
while(nums[i]>=1 && nums[i]<=nums.length && nums[i]!=nums[nums[i]-1]){
int temp = nums[nums[i]-1];
nums[nums[i]-1] = nums[i];
nums[i] = temp;
}
}
//因此这里的判断是i+1,因为当前格子实际数组是1-n的顺序,是比索引大一。
for (int i = 0; i < nums.length; i++) {
if (nums[i] != i+1) {
return i+1;
}
}
return nums.length+1;
}矩阵
螺旋矩阵
重点在于使用一个二维数组当做下一步的方向上的增减,并提前预判下一步是否超界或者遍历过,根据条件转向。

public List<Integer> spiralOrder(int[][] matrix) {
int m = matrix.length;
int n = matrix[0].length;
List<Integer> ans = new ArrayList<>();
int[][] copy = new int[m][n];//防止更改原本的数组
for (int i = 0; i < matrix.length; i++) {
copy[i] = Arrays.copyOf(matrix[i], matrix[i].length);
}
int [][]DIRECTION = new int [][]{{0,1},{1,0},{0,-1},{-1,0}};
int i=0,j=0;
int di = 0;//表示现在是哪个方向
for (int k = 0; k < n*m; k++) {
ans.add(matrix[i][j]);
copy[i][j] = Integer.MAX_VALUE;
int x = i+DIRECTION[di][0];
int y = j+DIRECTION[di][1];
if (x<0 || x>=m || y<0 || y>=n || copy[x][y]==Integer.MAX_VALUE) {
di = (di+1) % 4;//该转向了
}
i = i+DIRECTION[di][0];
j = j+DIRECTION[di][1];
}
return ans;
}顺时针旋转矩阵
代码很简单,一次转置+一次横对称。但是原理需要理解:
顺时针旋转90°的具体实现
对于顺时针旋转90°:
- θ = 90°,所以两条对称轴夹角应为45°
- 具体操作:
- 先关于主对角线翻转(左上到右下的对角线)
- 再关于垂直中轴翻转(矩阵的垂直中线)

public void rotate(int[][] matrix) {
for (int i = 0; i < matrix.length; i++) {
for (int j = 0; j < i; j++) {//转置只对对角线下的元素操作。
int temp = matrix[i][j];
matrix[i][j] = matrix[j][i];
matrix[j][i] = temp;
}
}
for (int i = 0; i < matrix.length; i++) {
for (int j = 0; j < matrix[i].length/2; j++) {
int temp = matrix[i][j];
matrix[i][j] = matrix[i][matrix[i].length-j-1];//对称交换
matrix[i][matrix[i].length-j-1] = temp;
}
}
}二维有序矩阵查找
笨办法就是每行用个二分法。
记得用优化后的写法:int mid = left + (right-left)/2;
灵神的写法是,每次只看右上角的:大就删列,小就删行。
时间复杂度O(m+n)
public boolean searchMatrix(int[][] matrix, int target) {
int i = 0;
int j = matrix[0].length-1;
while (i < matrix.length && j >= 0){
if (matrix[i][j] == target){
return true;
}else if (matrix[i][j] > target){
j--;
}else {
i++;
}
}
return false;
}链表
相交链表
其实这个题我会,但是灵神的代码实在太牛逼了,记一下
这个代码是寻找两个链表的交点。
原理: 两个指针分别从两个链表头开始,同步前进。当一个指针到达链表末尾时,它跳到另一个链表的头继续前进。这样两个指针走过的总路程相同,如果有交点,它们最终会在交点处相遇。
class Solution {
public ListNode getIntersectionNode(ListNode headA, ListNode headB) {
ListNode p = headA;
ListNode q = headB;
while (p != q) {
p = p != null ? p.next : headB;
q = q != null ? q.next : headA;
}
return p;
}
}两数相加
主要是为了看下简便的写法:
- 更优雅的边界处理:循环条件
while (l1 != null || l2 != null || carry != 0)确保所有情况都被处理,包括最后可能的进位。 - 统一的节点创建:不再需要多个if-else分支来决定是否创建新节点,而是统一创建新节点并更新carry值。
class Solution {
public ListNode addTwoNumbers(ListNode l1, ListNode l2) {
ListNode dummy = new ListNode(0); // 虚拟头节点
ListNode curr = dummy;
int carry = 0;
while (l1 != null || l2 != null || carry != 0) {//概括三种情况
int x = (l1 != null) ? l1.val : 0;//第一条链表的值
int y = (l2 != null) ? l2.val : 0;//第二条链表的值
int sum = carry + x + y;//暂时的和
carry = sum / 10;//是否进位
curr.next = new ListNode(sum % 10);//结果
curr = curr.next;//下一个
if (l1 != null) l1 = l1.next;
if (l2 != null) l2 = l2.next;
}
return dummy.next;
}
}删除链表节点
换句话说,如果遇到需要删除头节点的题目,添加哨兵节点可以简化代码逻辑,请记住这个技巧。
加了一个头结点之后,删除时候就不需要额外讨论删除头结点的问题,如果真要讨论就看fast的状态,根据fast的位置判断是否删除头。
public ListNode removeNthFromEnd(ListNode head, int n) {
ListNode pHead = new ListNode(0);
pHead.next = head;
ListNode fast = pHead;
ListNode slow = pHead;
int count = 0;
while(fast!=null&&count<n){
fast = fast.next;
count++;
}
while(fast!=null && fast.next!=null){
fast = fast.next;
slow = slow.next;
}
slow.next = slow.next.next;
return pHead.next;
}反转链表II
//反转链表2-片段反转 需要哨兵+pre /cur/next三个指针保证当前指向前面的,最后连接原链表和反转后的尾巴和头部。返回哨兵下一个
public ListNode reverseBetween(ListNode head, int left, int right) {
ListNode dummy = new ListNode(0) ;
dummy.next = head;
ListNode p = dummy;
int count = 0;
while (count < left-1) {
p = p.next;
count++;
}//p在left前一个,从这里开始反转
ListNode pre = null;
ListNode cur = p.next;
while (count < right) {//遍历最后一次结束cur是反转的下一个,pre时反转的最后一个
ListNode next = cur.next;
cur.next = pre;
pre = cur;
cur = next;
count++;
}
p.next.next = cur;
p.next = pre;
return dummy.next;
}k个反转链表同上道理,多统计一次长度,加更新p0
class Solution {
public ListNode reverseKGroup(ListNode head, int k) {
// 统计节点个数
int n = 0;
for (ListNode cur = head; cur != null; cur = cur.next) {
n++;
}
ListNode dummy = new ListNode(0, head);
ListNode p0 = dummy;
ListNode pre = null;
ListNode cur = head;
// k 个一组处理
for (; n >= k; n -= k) {
for (int i = 0; i < k; i++) { // 同 92 题
ListNode nxt = cur.next;
cur.next = pre; // 每次循环只修改一个 next,方便大家理解
pre = cur;
cur = nxt;
}
// 见视频
ListNode nxt = p0.next;
p0.next.next = cur;
p0.next = pre;
p0 = nxt;
}
return dummy.next;
}
}
作者:灵茶山艾府
链接:https://leetcode.cn/problems/reverse-nodes-in-k-group/solutions/1992228/you-xie-cuo-liao-yi-ge-shi-pin-jiang-tou-plfs/
来源:力扣(LeetCode)
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。