数组复习
数组复习基本就是熟练使用数组,经常配合指针使用以及思维的使用
443. 压缩字符串 - 力扣(LeetCode)
使用双指针分别标志我们在字符串中读和写的位置,当读指针 read 位于字符串的末尾,或读指针 read 指向的字符不同于下一个字符时,我们就认为读指针 read 位于某一段连续相同子串的最右侧。该子串对应的字符即为读指针 read 指向的字符串。我们使用变量 left
class Solution {
public static int compress(char[] chars) {
int n = chars.length;
if(n == 1)return 1;
char b [] = new char[n];
for (int i = 0,j = 0; i < n;) {
b[j] = chars[i];i++;j++;
int sum = 1;
while (i < n && chars[i] == chars[i-1]){
i++;
sum++;
}
if(sum != 1){
for (int k = 0; k < Integer.toString(sum).length(); k++) {
b[j] = Integer.toString(sum).charAt(k);j++;
}
}
}
int m = 0;
for (int i = 0; i < n; i++) {
if(b[i] != 0){
chars[i] = b[i];
// System.out.println(b[i]);
m++;
}
}
return m;
}
}
双指针复习
1679. K 和数对的最大数目 - 力扣(LeetCode)
很easy,用两个指针从最左边和最右边,逐渐靠拢,符合条件就筛选。
class Solution {
public static int maxOperations(int[] nums, int k) {
Arrays.sort(nums);
int n = nums.length;
int res = 0,i = 0,j = n-1;
while (i<j){
if(nums[i] + nums[j] == k){
i++;j--;res++;
}else if (nums[i] + nums[j] > k){
j--;
}else{
i++;
}
}
return res;
}
}
滑动窗口
3. 无重复字符的最长子串 - 力扣(LeetCode)
也是类似于双指针,这个双指针中间的比作一个窗口,然后两个指针都是从0开始走,然后符合条件就扩张右指针,如果符合左边的条件同样去移动左指针来拿到最佳窗口,经常和map一并使用。
76. 最小覆盖子串 - 力扣(LeetCode)
class Solution {
public String minWindow(String s, String t) {
//维护s中滑动窗口中各个字符出现次数
Map<Character,Integer> hs = new HashMap<>();
//维护t中滑动窗口中各个字符出现次数
Map<Character,Integer> ht = new HashMap<>();
for (int i = 0; i < t.length(); i++) {
ht.put(t.charAt(i),ht.getOrDefault(t.charAt(i),0)+1);
}
String ans = "";
int len = Integer.MAX_VALUE;//这个len是为了让结果是存储的最短的字符串而存在的变量
int cnt = 0;//cnt用来计数此时hs中复合ht中的数字此时有几个了
for (int left = 0,right = 0; right < s.length(); right++) {
hs.put(s.charAt(right),hs.getOrDefault(s.charAt(right),0) + 1);
//如果ts表中也包含当前字符
if(ht.containsKey(s.charAt(right))){
//并且hs表中字符是 <= ht中字符的,说明该字符是必须的,并且还未到达字符串t所要求的数
if(hs.get(s.charAt(right)) <= ht.get(s.charAt(right))){
cnt++;
}
}
//收缩滑动窗口
//如果左边的值不在ht表中,或者他在hs表中出现次数多于ht表中的出现次数
while (left < right && (!ht.containsKey(s.charAt(left)) ||
hs.get(s.charAt(left)) > ht.get(s.charAt(left)))){
hs.put(s.charAt(left),hs.get(s.charAt(left)) - 1);
left++;
}
//此时滑动窗口包含字符串t的全部字符
if(cnt == t.length() && right - left + 1 < len){
len = right - left + 1;
ans = s.substring(left,right+1);
}
}
return ans;
}
}
哈希复习
用一个数组模拟哈希的查找,关于hash的用法标记
1、把哈希数组的长度设置为1009.模也是1009,设置一个默认变量M:意思是空,用Arrays.fill(h,M)意思就是将所有的数组位置都传入值M,
2、在 for (Map.Entry<Integer,Integer> entry : map.entrySet()) {}
中意思就是:map.ectrySet()指的是把map中的所有键值对都设为一个集合,用来遍历,Map.Entry<Integer,Integer>意思是遍历的每一个对象的类型,entry就是每一次遍历出来的对象,该对象可以调用哈希的函数,如entry.getValue()\entry.getKey();等等
3、最后一个方法find 其中是使用模运算hash存储的查找过程,具体为
-
int k = (x % N + N) % N;
: 这行代码计算x
在哈希表中的初始索引k
。先计算x % N
,再通过加上N
并对N
取模来确保k
是一个非负数(处理负数情况)。while (h[k] != M && h[k] != x)
: 这是一个循环,它的条件是当前h[k]
既不等于M
(可能表示该位置未使用或是空槽),也不等于x
(要查找的元素)。这个循环的目的是在哈希表中找到x
。k++; if(k == N) k = 0;
: 这两行代码用来处理探查。如果当前的位置不符合条件,就移动到下一个位置。如果k
达到N
,那么就循环回到数组的开头(设置为0),实现循环探查。
- 返回值:
return k;
: 最后,函数返回找到x
的位置索引。如果在哈希表中没有找到x
,它返回的是该元素可能被放置的第一个空位置的索引,或者当前位置(如果h[k]
为x
)。
public class 独一无二的出现次数 {
//https://leetcode.cn/problems/unique-number-of-occurrences/?envType=study-plan-v2&envId=leetcode-75
final int N = 1009,M = 2009;
int h[] = new int[N];
public boolean uniqueOccurrences(int[] arr){
Map<Integer,Integer> map = new HashMap<>();
Arrays.fill(h,M);
for (int i = 0; i < arr.length; i++) {
int k = find(arr[i]);
if(h[k] == M){
h[k] = arr[i];
map.put(arr[i],1);
}else{
map.put(arr[i],map.get(arr[i])+1);
}
}
Map<Integer,Integer> map2 = new HashMap();
for (Map.Entry<Integer,Integer> entry : map.entrySet()) {
int value = entry.getValue();
if(map2.containsKey(value))return false;
map2.put(value,1);
}
return true;
}
//模运算hash存储buckets,
public int find(int x){
int k = (x % N + N)% N;
while (h[k] != M && h[k] != x){
k++;if(k == N) k = 0;
}
return k;
}
}
二叉树复习
二叉树总是带着递归一起使用,所以想要学好二叉树算法就一点要理解递归的奥秘
236. 二叉树的最近公共祖先 - 力扣(LeetCode)
其实我已经给了大部分注释,看上面的多行注释意思为自定义了一个TreeNode的类,其中有左二叉树、右二叉树,和val值。
二叉树总是使用前中后序遍历,这里面用的是中序遍历,也就是中左右的顺序,拿出二叉树的所有值,然后递归返回。
它的要求就是找到p和q,但是不用区分是哪一个,而且题目说一定存在,所以中序遍历的中,就是只要找到值就return。
如果找不到然后找该节点的左子树用递归调用自己这个方法来找,左之后就是右,然后再往下就是三个判断,是用在递归返回的时候使用
如果左右子树都没找到p、q那就继续返回null,如果谁找到了就返回谁,如果两个都找到了就直接返回根节点,这样根节点就会直线return,直到
返回到栈中的最底层,然后返回函数,得出答案。
/**
* Definition for a binary tree node.
* public class TreeNode {
* int val;
* TreeNode left;
* TreeNode right;
* TreeNode(int x) { val = x; }
* }
*/
class Solution {
public TreeNode lowestCommonAncestor(TreeNode root, TreeNode p, TreeNode q) {
if (root == null || root == p || root == q){
//只要当前根节点是p和q的中任意一个,就返回(因为无法比根节点更祖了)
return root;
}
//根节点不是p和q中的任意一个,那么就继续分别去左子树和右子树找p和q
TreeNode left = lowestCommonAncestor(root.left,p,q);
TreeNode right = lowestCommonAncestor(root.right,p,q);
//p和q都没有找到,那就没有返回null
if(left == null && right == null){
return null;
}
//因为题目保证p和q均存在于给定的二叉树,所以一定是有结果不是左就是右。
//左子树没有p也没有q,就返回右子树的结果
if(left == null)return right;
if(right == null)return left;
//前面进行了判断,也就是左右都找到了值,则此时的root就是我们想要的值:二叉树的最近公共祖先。
//左右子树都找到p和q了,那就说明p和q分别在两个子树上,所以次数的最近公共祖先就是root
return root;
}
}
437. 路径总和 III - 力扣(LeetCode)
这道题是
能够顺便用到前缀和,同时也用到了递归,寻找多个节点的值为targetSum,那么如果某一个节点是preSum,所以他要找的就是preSum - targetSum (很重要) 如果此时在他上方有前缀和,map存储的是前缀和的数量,可以直接用count+= (那个数量)比如用10 5 3 这条路来看,map(10,1) map(15,1) map(18,1),当指针指到3的时候,此时preSum = 18,preSum - targetSum = 18 - 8 得10,相当于使用前缀和之差来找到自己想要的路径总合,最后还用到了回溯,回溯就是在递归之后将此次函数中将深搜之前更改的公共资源恢复原样。
/**
* Definition for a binary tree node.
* public class TreeNode {
* int val;
* TreeNode left;
* TreeNode right;
* TreeNode() {}
* TreeNode(int val) { this.val = val; }
* TreeNode(int val, TreeNode left, TreeNode right) {
* this.val = val;
* this.left = left;
* this.right = right;
* }
* }
*/
class Solution {
int targetSum,count = 0;
Map<Integer,Integer> map;//map中key存储前缀和的值,value存储在当前这条路径中此前缀和的出现的次数
public int pathSum(TreeNode root, int targetSum) {
if(root == null) return 0;
this.targetSum = targetSum;
this.map = new HashMap<>();
map.put(0,1);//表示前缀和为0的节点为空,有一个空 也就是根节点的前缀和
dfs(root,0);
return count;
}
private void dfs(TreeNode node, int preSum) {
if(node == null)return;
preSum += node.val;//preSum保存的是前缀和的值,
count += map.getOrDefault(preSum - targetSum,0);//累计满足要求的前缀和的数量
map.put(preSum,map.getOrDefault(preSum,0) + 1);//然后将这条路的前缀和的值为preSum给存储。
//进行深搜,递归
dfs(node.left,preSum);
dfs(node.right,preSum);
map.put(preSum,map.get(preSum) - 1);//路径退缩(回溯),去掉不在路径上的当前结点的前缀和,必存在无需getDefault。
}
}
前缀和/差分复习
差分就是把前缀和给反过来了,前缀和是原数组arr,前缀数组brr,其中brr中是
for (int i = 1; i < arr.length; i++) {
arr[i] = sc.nextInt();
brr[i] = brr[i-1]+arr[i];
}
而差分的话是,原数组是arr,然后差分数组是brr,通过每次在arr中的操作,比如在下标1-3的地方都进行+1,此时就在差分数组中brr[1] + 1,然后再brr[3]-1
等把所有的操作结束后,就把差分数组brr进行前缀和,此时的brr就是原数组进行多次操作后的样子,其中最重要的就是insert函数。同理的矩形差分数组也是这样。
797. 差分 - AcWing题库
import java.util.Scanner;
public class Main {
static int arr [];
static int brr [];
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
int n = sc.nextInt();
int m = sc.nextInt();
arr = new int[100010];
brr = new int[100010];
for (int i = 1; i <= n; i++) {
arr[i] = sc.nextInt();
insert(i,i,arr[i]);
}
int q [][] = new int[m][3];
for (int i = 0; i < m; i++) {
for (int j = 0; j < 3; j++) {
q[i][j] = sc.nextInt();
}
insert(q[i][0],q[i][1],q[i][2]);
}
for (int i = 1; i <= n; i++) {
brr[i] += brr[i-1];
}
for (int i = 1; i <= n; i++) {
System.out.printf("%d ",brr[i]);
}
}
private static void insert(int l,int r, int c){
brr[l] +=c;
brr[r + 1] -=c;
}
}