Kopoo的刷题日记
2022.4.6-前缀和数组
学习的是前缀数组
303. 区域和检索 - 数组不可变
class NumArray {
private int[]res;
public NumArray(int[] nums) {
res=new int[nums.length+1];
for(int i=1;i<res.length;i++){
//res[i]代表i个数组元素的和
res[i]=nums[i-1]+res[i-1];
}
}
public int sumRange(int left, int right) {
return res[right+1]-res[left];
}
}
/**
* Your NumArray object will be instantiated and called as such:
* NumArray obj = new NumArray(nums);
* int param_1 = obj.sumRange(left,right);
*/
前缀数组空间换时间,我觉得最主要的点是res数组代表了什么
res[i]记录nums[0...i-1]的累加和,因此最终取得[left,right]的数组和应该为res[right+1]-res[left]
304. 二维区域和检索 - 矩阵不可变
class NumMatrix {
private int[][] res;
public NumMatrix(int[][] matrix) {
int m=matrix.length;
int n=matrix[0].length;
if(m==0||n==0)return;
res=new int[m+1][n+1];
for(int i=1;i<=m;i++){
for(int j=1;j<=n;j++){
res[i][j]=res[i-1][j]+res[i][j-1]-res[i-1][j-1]+matrix[i-1][j-1];
}
}
}
public int sumRegion(int row1, int col1, int row2, int col2) {
int result=res[row2+1][col2+1]-res[row2+1][col1]-res[row1][col2+1]+res[row1][col1];
return result;
}
}
/**
* Your NumMatrix object will be instantiated and called as such:
* NumMatrix obj = new NumMatrix(matrix);
* int param_1 = obj.sumRegion(row1,col1,row2,col2);
*/
这题目其实不难,矩阵的面积是(A-B)(C-D) = AC - AD - BC + BD
但是要注意题目的测试用例,是从0,0 开始因此sumRegin方法取值最好不要用-1这样的,会越界异常
还有就是数组的定义
定义:res[i] [j]记录 matrix 中⼦矩阵 [0, 0, i-1, j-1] 的元素和
560. 和为 K 的子数组
class Solution {
public int subarraySum(int[] nums, int k) {
HashMap<Integer,Integer> map=new HashMap<>();
int result=0;
int sum=0;
map.put(0,1);
for(int num:nums){
sum+=num;
int temp=sum-k;
if(map.containsKey(temp)){
result+=map.get(temp);
}
map.put(sum,map.getOrDefault(sum,0)+1);
}
return result;
}
}
这道题目感觉最难的是hashmap如何使用,实现放的key是累加和,value是累加和出现的次数
map 记录的是前缀和到该前缀和出现的次数的映射。
总结
总的来说该算法还是很巧妙的,就是需要用空间换时间的方式记录需要的数的和,然后需要计算时以O(1)的复杂度相减直接计算
2022.4.7-差分数组
复习一下前缀和数组
/**
* @Author: Kopoo
* @Date: 2022/4/7 10:24
*/
public class PrefixSum {
int[] preSum;
public PrefixSum(int[] nums) {
preSum = new int[nums.length + 1];
for (int i = 1; i < preSum.length; i++) {
preSum[i] = preSum[i - 1] + nums[i - 1];
}
}
public int pre_Sum(int i, int j) {
return preSum[j + 1] - preSum[i];
}
}
类似前缀和技巧构造的 prefix 数组,我们先对nums数组构造⼀个 diff 差分数组,diff[i] 就是 nums[i] 和 nums[i-1] 之差
int[] diff=new int[arr.length];
// 构造差分数组
diff[0]=arr[0];
for(int i=1;i<arr.length;i++){
diff[i]=arr[i]-arr[i-1];
}
diff反推arr
设计成工具类
/**
* @Author: Kopoo
* @Date: 2022/4/7 11:03
*/
// 差分数组工具类
class Difference {
// 差分数组
private int[] diff;
/* 输入一个初始数组,区间操作将在这个数组上进行 */
public Difference(int[] nums) {
assert nums.length > 0;
diff = new int[nums.length];
// 根据初始数组构造差分数组
diff[0] = nums[0];
for (int i = 1; i < nums.length; i++) {
diff[i] = nums[i] - nums[i - 1];
}
}
/* 给闭区间 [i,j] 增加 val(可以是负数)*/
public void increment(int i, int j, int val) {
diff[i] += val;
if (j + 1 < diff.length) {
diff[j + 1] -= val;
}
}
/* 返回结果数组 */
public int[] result() {
int[] res = new int[diff.length];
// 根据差分数组构造结果数组
res[0] = diff[0];
for (int i = 1; i < diff.length; i++) {
res[i] = res[i - 1] + diff[i];
}
return res;
}
}
这样构造差分数组 diff
,就可以快速进行区间增减的操作,如果你想对区间 nums[i..j]
的元素全部加 3,那么只需要让 diff[i] += 3
,然后再让 diff[j+1] -= 3
即可:
原理很简单,回想 diff
数组反推 nums
数组的过程,diff[i] += 3
意味着给 nums[i..]
所有的元素都加了 3,然后 diff[j+1] -= 3
又意味着对于 nums[j+1..]
所有元素再减 3,那综合起来,是不是就是对 nums[i..j]
中的所有元素都加 3 了?
只要花费 O(1) 的时间修改 diff
数组,就相当于给 nums
的整个区间做了修改。多次修改 diff
,然后通过 diff
数组反推,即可得到 nums
修改后的结果。
370.区间加法
题目描述
假设你有一个长度为 n 的数组,初始情况下所有的数字均为 0,你将会被给出 k 个更新的操作。
其中,每个操作会被表示为一个三元组:[startIndex, endIndex, inc],你需要将子数组 A[startIndex ... endIndex](包括 startIndex 和 endIndex)增加 inc。
请你返回 k 次操作后的数组。
输入: length = 5, updates = [[1,3,2],[2,4,3],[0,2,-2]
输出: [-2,0,3,5,3]
初始状态:
[0,0,0,0,0]
进行了操作 [1,3,2] 后的状态:
[0,2,2,2,0]
进行了操作 [2,4,3] 后的状态:
[0,2,5,5,3]
进行了操作 [0,2,-2] 后的状态:
[-2,0,3,5,3]
import java.util.Arrays;
import java.util.Scanner;
public class Main {
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
int length = sc.nextInt();
int[] arr = new int[length];
while (sc.hasNext()) {
int startIndex = sc.nextInt();
int endIndex = sc.nextInt();
int inc = sc.nextInt();
help(startIndex, endIndex, inc, arr);
System.out.println(Arrays.toString(arr));
}
System.out.println(Arrays.stream(arr).toArray());
}
private static void help(int startIndex, int endIndex, int inc, int[] arr) {
//diff[i]代表arr[i]-arr[i-1]
int[] diff = new int[arr.length];
diff[0] = arr[0];
for (int i = 1; i < arr.length; i++) {
diff[i] = arr[i] - arr[i - 1];
}
diff[startIndex] += inc;
if (endIndex + 1 < arr.length) {
diff[endIndex + 1] -= inc;
}
recover_arr(diff, arr);
}
private static void recover_arr(int[] diff, int[] res) {
res[0] = diff[0];
for (int i = 1; i < diff.length; i++) {
res[i] = res[i - 1] + diff[i];
}
}
}
没啥问题,经典的差分数组
1109. 航班预订统计
class Solution {
public int[] corpFlightBookings(int[][] bookings, int n) {
int[] res=new int[n+1];
int[] diff=new int[n+1];
int[] result=new int[n];
diff[1]=res[1];
//diff[i]=res[i]-res[i-1]
for(int i=0;i<bookings.length;i++){
help(res,bookings[i][0],bookings[i][1],bookings[i][2],diff);
}
result_arr(diff,res);
for(int i=0;i<n;i++){
result[i]=res[i+1];
}
return result;
}
public void help(int[]res,int start,int end,int index,int[] diff){
diff[start]+=index;
if(end+1<res.length){
diff[end+1]-=index;
}
}
public void result_arr(int[] diff,int[] res){
res[1]=diff[1];
for(int i=2;i<diff.length;i++){
res[i]=res[i-1]+diff[i];
}
}
}
容易,要记住差分数组的实现代码
1094. 拼车
1 2 3 4 5 6 7
2 2 2 2 2
3 3 3
class Solution {
public boolean carPooling(int[][] trips, int capacity) {
int[] res=new int[1001];
int[] diff=new int[1001];
for(int[] trip:trips){
int numPassenger=trip[0];
int from=trip[1];
int to=trip[2]-1;
help(numPassenger,from,to,diff);
}
result(res,diff);
for(int num:res){
if(num>capacity){
return false;
}
}
return true;
}
public void help(int numPassenger,int from,int to,int[] diff){
diff[from]+=numPassenger;
if(to+1<diff.length){
diff[to+1]-=numPassenger;
}
}
public void result(int[] res,int[] diff){
res[0]=diff[0];
for(int i=1;i<res.length;i++){
res[i]=diff[i]+res[i-1];
}
}
}
写错了一点,题目有点没说清楚,就是 比如下面这个测试用例
[[2,1,5],[3,5,7]]
3
应该是true,也就是一群人下和一群人上车可以同步进行的
所以范围要改为[trip[1], trip[2] - 1]
总结
差分数组不难,需要掌握好的是范围取值,
i到j
是diff[i]+index,然后在diff[j+1]-index
2022.4.8-双指针技巧秒杀七道链表题目
21. 合并两个有序链表
/**
* Definition for singly-linked list.
* public class ListNode {
* int val;
* ListNode next;
* ListNode() {}
* ListNode(int val) { this.val = val; }
* ListNode(int val, ListNode next) { this.val = val; this.next = next; }
* }
*/
class Solution {
public ListNode mergeTwoLists(ListNode list1, ListNode list2) {
ListNode dummy=new ListNode(-1);
ListNode p1=list1;
ListNode p2=list2;
ListNode p=dummy;
while(p1!=null&&p2!=null){
if(p1.val<p2.val){
p.next=p1;
p1=p1.next;
}
else{
p.next=p2;
p2=p2.next;
}
p=p.next;
}
if(p1!=null){
p.next=p1;
}
if(p2!=null){
p.next=p2;
}
return dummy.next;
}
}
dummy虚拟头节点还是很有用的,不然代码会复杂很多
23. 合并K个升序链表
/**
* Definition for singly-linked list.
* public class ListNode {
* int val;
* ListNode next;
* ListNode() {}
* ListNode(int val) { this.val = val; }
* ListNode(int val, ListNode next) { this.val = val; this.next = next; }
* }
*/
class Solution {
public ListNode mergeKLists(ListNode[] lists) {
if(lists.length==0)return null;
ListNode dummy=new ListNode(-1);
ListNode p=dummy;
PriorityQueue<ListNode>pq=new PriorityQueue<>((a,b)->(a.val-b.val));
for(ListNode list: lists){
if(list!=null){
pq.add(list);
}
}
while(!pq.isEmpty()){
ListNode node=pq.poll();
p.next=node;
if(node.next!=null){
pq.add(node.next);
}
p=p.next;
}
return dummy.next;
}
}
PriorityQueue<ListNode>pq=new PriorityQueue<>((a,b)->(a.val-b.val));
上面这个表达式很核心,要学会用lambda表达式来进行使用=
剑指 Offer 22. 链表中倒数第k个节点
快慢指针就可以
/**
* Definition for singly-linked list.
* public class ListNode {
* int val;
* ListNode next;
* ListNode(int x) { val = x; }
* }
*/
class Solution {
public ListNode getKthFromEnd(ListNode head, int k) {
ListNode fast=head, slow=head;
for(int i=0;i<k;i++){
fast=fast.next;
}
while(fast!=null){
fast=fast.next;
slow=slow.next;
}
return slow;
}
}
19. 删除链表的倒数第 N 个结点
/**
* Definition for singly-linked list.
* public class ListNode {
* int val;
* ListNode next;
* ListNode() {}
* ListNode(int val) { this.val = val; }
* ListNode(int val, ListNode next) { this.val = val; this.next = next; }
* }
*/
class Solution {
public ListNode removeNthFromEnd(ListNode head, int n) {
ListNode dummy=new ListNode(-1);
dummy.next=head;
ListNode fast=dummy;
ListNode slow=dummy;
for(int i=0;i<n;i++){
fast=fast.next;
}
while(fast.next!=null){
slow=slow.next;
fast=fast.next;
}
slow.next=slow.next.next;
return dummy.next;
}
}
需要注意的是出现只有一个数的链表情况,只要将快慢指针标在虚拟节点上就可以解决
这个虚拟节点还挺好用的
876. 链表的中间结点
/**
* Definition for singly-linked list.
* public class ListNode {
* int val;
* ListNode next;
* ListNode() {}
* ListNode(int val) { this.val = val; }
* ListNode(int val, ListNode next) { this.val = val; this.next = next; }
* }
*/
class Solution {
public ListNode middleNode(ListNode head) {
ListNode fast=head;
ListNode slow=head;
while(fast!=null&&fast.next!=null){
slow=slow.next;
fast=fast.next.next;
}
return slow;
}
}
141. 环形链表
/**
* Definition for singly-linked list.
* class ListNode {
* int val;
* ListNode next;
* ListNode(int x) {
* val = x;
* next = null;
* }
* }
*/
public class Solution {
public boolean hasCycle(ListNode head) {
ListNode fast=head;
ListNode slow=head;
while(fast!=null&&fast.next!=null){
slow=slow.next;
fast=fast.next.next;
if(fast==slow)return true;
}
return false;
}
}
看看就好,都是比较经典的方法类似
4月16日-142. 环形链表 II
/**
* Definition for singly-linked list.
* class ListNode {
* int val;
* ListNode next;
* ListNode(int x) {
* val = x;
* next = null;
* }
* }
*/
public class Solution {
public ListNode detectCycle(ListNode head) {
ListNode fast=head;
ListNode slow=head;
while(fast!=null&&fast.next!=null){
fast=fast.next.next;
slow=slow.next;
if(fast==slow)break;
}
if(fast==null||fast.next==null)return null;
slow=head;
while(fast!=slow){
fast=fast.next;
slow=slow.next;
}
return slow;
}
}
把图记下来
如果从相遇点继续前进 k - m 步,也恰好到达环起点。因为结合上图的 fast 指针,从相遇点开始⾛k步可以转回到相遇点,那⾛ k - m 步肯定就⾛到环起点了
4月17日-160. 相交链表
/**
* Definition for singly-linked list.
* public class ListNode {
* int val;
* ListNode next;
* ListNode(int x) {
* val = x;
* next = null;
* }
* }
*/
public class Solution {
public ListNode getIntersectionNode(ListNode headA, ListNode headB) {
ListNode p1=headA, p2=headB;
while(p1!=p2){
if(p1==null)p1=headB;
else p1=p1.next;
if(p2==null)p2=headA;
else p2=p2.next;
}
return p1;
}
}
这个刷不出来就很奇怪,真的要好好努力了,加油啊!你一定可以的
2022.5.1-双指针技巧秒杀七道数组题目
26. 删除有序数组中的重复项
class Solution {
public int removeDuplicates(int[] nums) {
int confirm_idx=0, compare_idx=0;
while(compare_idx<nums.length){
if(nums[confirm_idx]==nums[compare_idx]){
compare_idx++;
}else{
nums[++confirm_idx]=nums[compare_idx];
}
}
return confirm_idx+1;
}
}
秒了,思路很清晰,双指针思想,实际就是快慢指针
83. 删除排序链表中的重复元素
/**
* Definition for singly-linked list.
* public class ListNode {
* int val;
* ListNode next;
* ListNode() {}
* ListNode(int val) { this.val = val; }
* ListNode(int val, ListNode next) { this.val = val; this.next = next; }
* }
*/
class Solution {
public ListNode deleteDuplicates(ListNode head) {
ListNode slow=head;
ListNode fast=head;
if(head==null)return null;
while(fast!=null){
if(fast.val!=slow.val){
slow.next=fast;
slow=slow.next;
}
fast=fast.next;
}
slow.next=null;
return head;
}
}
同上题一样双指针,用在链表上
5月2日-27. 移除元素
class Solution {
public int removeElement(int[] nums, int val) {
if(nums==null||nums.length==0)return 0;
int fast=0,slow=0;
while(fast!=nums.length){
if(nums[fast]!=val){
nums[slow]=nums[fast];
slow++;
}
fast++;
}
return slow++;
}
}
秒了,还是双指针
5月3日-283. 移动零
class Solution {
public void moveZeroes(int[] nums) {
if(nums==null||nums.length==0)return;
int fast=0,slow=0;
while(fast<nums.length){
if(nums[fast]==0){
fast++;
}
else{
nums[slow++]=nums[fast++];
}
}
while(slow<nums.length){
nums[slow++]=0;
}
}
秒了,还是快慢指针
5月4日-167. 两数之和 II
class Solution {
public int[] twoSum(int[] numbers, int target) {
int left=0,right=numbers.length-1;
while(left<right){
int sum=numbers[left]+numbers[right];
if(sum==target){
return new int[]{left+1,right+1};
}
else if(sum<target){
left++;
}
else if(sum>target){
right--;
}
}
return new int[]{-1,-1};
}
}
思路要清晰,实际就是双指针类似于二分查找
5月5日-344. 反转字符串
class Solution {
public void reverseString(char[] s) {
if(s==null||s.length==0)return;
int left=0,right=s.length-1;
while(left<right){
char temp=s[left];
s[left]=s[right];
s[right]=temp;
left++;
right--;
}
}
}
刷简单题目还是很容易的了,就是双指针来反转数组
5. 最长回文子串
class Solution {
public String longestPalindrome(String s) {
String temp="";
for(int i=0;i<s.length();i++){
String s1=help(s,i,i);
String s2=help(s,i,i+1);
temp=temp.length()>s1.length()?temp:s1;
temp=temp.length()>s2.length()?temp:s2;
}
return temp;
}
public String help(String s,int i,int j){
while(i>=0&&j<s.length()&&s.charAt(i)==s.charAt(j)){
i--;
j++;
}
return s.substring(i+1,j);
}
}
回文串的判断注意是中心扩散法,让左右指针从中心向两端扩展。
2022.6.5-我写了首诗,把滑动窗口算法算法变成了默写题
76. 最小覆盖子串
class Solution {
public String minWindow(String s, String t) {
HashMap<Character,Integer> need=new HashMap<>();
HashMap<Character,Integer> window=new HashMap<>();
for(Character c:t.toCharArray()){
need.put(c,need.getOrDefault(c,0)+1);
}
int left=0;
int right=0;
int valid=0;
int start=0;
int len=Integer.MAX_VALUE;
while(right<s.length()){
char c=s.charAt(right);
right++;
if(need.containsKey(c)){
window.put(c,window.getOrDefault(c,0)+1);
if(window.get(c).equals(need.get(c))){
valid++;
}
}
while(valid==need.size()){
if(right-left<len){
start=left;
len=right-left;
}
char d=s.charAt(left);
left++;
if(need.containsKey(d)){
if(window.get(d).equals(need.get(d))){
valid--;
}
window.put(d,window.getOrDefault(d,0)-1);
}
}
}
return len==Integer.MAX_VALUE?"":s.substring(start,start+len);
}
}
背诵,思路要清晰,这个题目逻辑性很强非常典型的滑动窗口题
6月16号-567. 字符串的排列
class Solution {
public boolean checkInclusion(String s1, String s2) {
HashMap<Character,Integer> need=new HashMap<>();
HashMap<Character,Integer> window=new HashMap<>();
for(Character ch:s1.toCharArray()){
need.put(ch,need.getOrDefault(ch,0)+1);
}
int left=0;
int right=0;
int valid=0;
int len=s1.length();
while(right<s2.length()){
char c=s2.charAt(right);
right++;
if(need.containsKey(c)){
window.put(c,window.getOrDefault(c,0)+1);
if(window.get(c).equals(need.get(c))){
valid++;
}
}
while(right-left>=len){
if(valid==need.size()){
return true;
}
char d=s2.charAt(left);
left++;
if(need.containsKey(d)){
if(window.get(d).equals(need.get(d))){
valid--;
}
window.put(d,window.getOrDefault(d,0)-1);
}
}
}
return false;
}
}
和上面一题的思路一样,不同点在于何时进行窗口左移,缩小的时机应该为窗口大于或者等于要求字符串大小
第二个不同点是直接返回true,如果长度相同并且valid也相同