substring数组越界_LeetCode刷题常见问题注意点(持续更新中)

树:

二叉树这种数据结构通常可以用两种方式来处理:递归和层级遍历。

1.递归。在当前节点,分别考虑左子树和右子树如何处理。

由递归,又衍生出DFS。

由DFS,又衍生出回溯。

2.层级遍历。

遍历整棵树,可以用DFS或者BFS。DFS又可以分为前序遍历、中序遍历、后序遍历。

题目变种可能是,在遍历完后,对结果处理。

在遍历过程中,记录路径(回溯)。

树的序列化:

前序中序后序处理二叉树

典型题目:

449-二叉搜索树的序列化与反序列化。注意其中一个细节是index的初始值是high+1

297-普通二叉树的序列化与反序列化。如果二叉树的遍历考虑了null,则不管什么二叉树都可以还原。

652

二叉搜索树:

二叉搜索树的中序遍历是升序序列。

典型题目:530

以树转图

这种类型的题目,通常起点不是root,需要将树转化为图,然后再BFS或者DFS。

典型题目:

863-二叉树中所有距离为 K 的结点

栈在pop时,要注意大小是不断变化的,因此最好用while(!stack.isEmpty())来遍历。如果一定要用for循环来遍历,遍历前先取size,队列同理。

单调栈

单调增栈每次插入新数据时,会一次性将比自己小的元素全部排出。

在存储时,可以存储元素的下标,也可以存储元素本身。

常规的代码为:

for (int i = 0; i < T.length; i++) {

while (!stack.isEmpty() && T[i] > T[stack.peek()]) {

int tmp = stack.pop();

result[tmp] = i - tmp;

}

stack.push(i);

}

2.更进一步的,单调栈的应用场景

求右边第一个较小的元素,建立一个从左到右的单调递增栈,while num

求右边第一个较大的元素,建立一个从左到右的单调递减栈,while num>stack.peek()

求左边第一个较小的元素,建立一个从右往左的单调递增栈,while num

求左边第一个较大的元素,建立一个从右往左的单调递减栈,while num>stack.peek()

3.单调栈也可解决去掉N个元素后最小的问题

316、402用单调递增栈来解决,316剩下来的是最小字典序,402剩下来的是最小数。

4.解决左右边界的问题

84-柱状图中最大的矩形

前缀和:

应用常用:求区间和:前缀和常常用在一段连续区间的求和,可以用前缀和相减的方式来获得区间和。

2.前缀和常常能降低时间复杂度,降成O(n)常规前缀和

构建一个前缀和的数组,然后拿着数组再做一遍计算。

2. 前缀和+哈希表

前缀和经常和哈希表一起配套使用。可以减少时间复杂度。

可以只遍历一遍数组,在计算出sum后,判断sum-k

for (int num : nums) {

sum += num;

if (map.containsKey(sum - k)) {

count += map.get(sum - k);

}

map.put(sum, map.getOrDefault(sum, 0) + 1);

}

3.例题:325、560

525-连续数组,这题目是560的变种,题目里出现的0和1转换成-1和1,就转换成了和为0的的情况。

4.map的构造

1)一种是,用于统计次数,不在于中间具体数值。map初始为map(0,1)

2) 一种是 ,可用于计算区间长度。map初始为map(0,-1)。例如第325题。

差分:

差分类似于公交车算法,上车下车。用于多区间有重叠。注意点就是在哪个站下车,直接在当前站减或者在下一个站减。

典型题目:

1109 航班预定统计。先扫描第一遍,得出增减量。对于[i,j,k]来说,在i站上车,在i站+k;在j站下车,在j+1站-k。

得出增减量之后,遍历数组求前缀和,即得出每个点的值。

1094拼车。

253 会议室二。

循环数组

用取模来搞定循环数组,就不用创建2遍了。

例子:

503题

int n = nums.length;

Stack stack = new Stack<>();

int[] result = new int[n];

Arrays.fill(result, -1);

for (int i = 0; i < 2 * n; i++) {

int index = i % n;

while (!stack.isEmpty() && nums[index] > nums[stack.peek()]) {

result[stack.pop()] = nums[index];

}

stack.push(index);

}

滑动窗口:

子串问题,要想到能用滑动窗口。

滑动窗口的要点:

1.窗口的选择,窗口内的条件是什么?

2.窗口的大小是不是恒定的?

3.right和left分别在什么时机前进?怎么前进?有的时候是简单++,有的时候要结合其他数据结构,例如map、单调队列、单调栈等处理。不一定每次都等长前进,有的时候是跳着前进,直接赋值,这里一定要想清楚。

4.滑的时候,用for循环或者while都可以,for循环一般用于固定长度的步进,while一般用于不固定长度的步进。

5.在不固定窗口时,right 指针不停向右移动,left 指针仅仅在窗口满足条件之后才会移动,起到窗口收缩的效果。

参考资料:

例题:

1456

while (right < s.length()) {

char c = s.charAt(right);

if (isVowel(c)) {

count++;

}

if (right - left + 1 >= k) {

result = Integer.max(result, count);

if (isVowel(s.charAt(left))) {

count--;

}

left++;

}

right++;

}

经典代码:

while (right < s2.length()) {

s2Sum[s2.charAt(right) - 'a']++;

if (right >= s1.length() - 1) {

if (isEqual(s1Sum, s2Sum)) {

return true;

}

s2Sum[s2.charAt(left) - 'a']--;

left++;

}

right++;

}

1297. 子串的最大出现次数

while (left <= right && right < s.length()) {

char c = s.charAt(right);

map[c - 'a']++;

while ((right - left + 1) >= minSize && (right - left + 1) <= maxSize) {

if (cal(map) <= maxLetters) {

String sub = s.substring(left, right + 1);

result.put(sub, result.getOrDefault(sub, 0) + 1);

}

char l = s.charAt(left);

map[l - 'a']--;

left++;

}

right++;

}

区间类问题:

区间的问题几个思路供参考:1、滑动窗口(双指针) 2、前缀和的差 3、线段树(树状数组)。4、排序 5、差分(公交车算法)

滑动窗口和前缀和的使用区别:

我们可以看到,在题目里出现“连续的子数组”,就要想到用滑动窗口或者前缀和,那么,这两者的区别是什么呢?如果区间中的数有负数,则不适合用滑动窗口,滑动窗口需要单调递增,不回头。

在滑的时候,如果边界会回头,则不适合用滑动窗口,需要用前缀和。

字符串:

字母异位词:判断2个字符串是否在重新排列后相等。

思路一:分别变成char数组排序,然后再遍历一遍,时间复杂度较高。

思路二:统计字符出现的频次,每个字符的频次都一样,则相等。可以用一个26长度的char数组来替代哈希表实现,占用的内存更低。

例题LeetCode-567:力扣

这2道题的主要代码

while (right < s.length()) {

sArray[s.charAt(right) - 'a']++;

if (right >= p.length() - 1) {

if (isEqual(sArray, pArray)) {

// do something }

sArray[s.charAt(left) - 'a']--;

left++;

}

right++;

}

大小写字母转换

1.可以用Character.tolowercase

2.大小写字母间相差32

统计字符出现的次数

如果仅仅是统计字符出现的次数,建议用一个26长度的int数组来取代map。优先是:

1)运算更快

2)map如果value为0,key可能没删掉,还要单独remove key,如果不这样,计算map的size可能会有问题。

表达式

表达式是一种特殊类型的字符串处理问题,常见类型有计算器和解方程,综合运用到字符串和栈的处理方式。

一般是遍历,然后将运算符和其他字母入栈,遇到特殊运算符时做一些处理。

计算器

计算器一般有几种思路,一种是labuladong的思路,即拆解为+1-2+3的思路,数都入栈,操作符不入栈,最终把栈里的取出来相加就是结果。遇到乘除法,就先取出来计算,然后再入栈。遇到括号就递归。

另外一种用双栈,操作符一个栈,数一个栈。

还有一种用一个栈,但是要注意左右括号的处理。

还有一种是利用栈来实现中缀转后缀。

排序:

常见写法

Collections.sort(list, new Comparator() {

@Override

public int compare(String o1, String o2) {

if (o1.length() == o2.length()) {

return o1.compareTo(o2);

}

return o2.length() - o1.length();

}

});

判断字典序,哪些是默认O1-O2是升序,O2-O1是降序

compareTo >0 是降序,compareTo <0 是升序

TOP K问题

可以使用大小为K的优先队列,Java中有现成的Priorityqueue类,可以重写其中的compare方法来实现各种排序。例如直接比较,或者结合map和比较键值。

其中升序优先队列,在出队(queue.poll)时删除的是最小值,这样保留下来的都是较大值。

降序优先队列,在出队(queue.poll)时删除的是最大值,这样保留下来的都是较小值。

举个例子,[1,2,3,4,5]的队列,K为2,

如果用升序优先队列,最后保留的是[4,5],如果再poll,出队的就是4

如果用降序优先队列,最后保留的是[2,1],如果再poll,出队的就是2

BFS:

一、树上的BFS

常用于树的层序遍历

二、图上的BFS

1.两种模板。一种是只要找到就行,队列不需要取size;一种是求最短路径,有层次的概念,需要取队列的size。

2.单源起点和多源起点。

单向BFS和双向BFS,双向BFS和单向BFS一样,也是O(n),但是如果双向BFS从节点多的一端开始,可以加速。

3.有的时候可以改变二维矩阵的值,有的时候不可以改变值,对于不能改变的,要建立visited数组。

4.方向有变种,例如四个方向,八个方向,马走日等。

单源起点典型题目:

岛屿类问题:200

class Solution {

public int numIslands(char[][] grid) {

int m = grid.length;

if (m == 0) {

return 0;

}

int count = 0;

int n = grid[0].length;

for (int i = 0; i < m; i++) {

for (int j = 0; j < n; j++) {

if (grid[i][j] == '1') {

count++;

bfs(grid, i, j);

}

}

}

return count;

}

private void bfs(char[][] grid, int i, int j) {

Queue queue = new LinkedList<>();

queue.add(new int[] {i, j});

int[][] directions = {{0, 1}, {1, 0}, {-1, 0}, {0, -1}};

while (!queue.isEmpty()) {

int[] position = queue.poll();

int r = position[0];

int c = position[1];

for (int[] direction : directions) {

int x = r + direction[0];

int y = c + direction[1];

if (x >= 0 && y >= 0 && x < grid.length && y < grid[0].length && grid[x][y] == '1') {

grid[x][y] = '2';

queue.add(new int[] {x, y});

}

}

}

}

}

单词接龙类问题:单源、图

127、433、126

多源BFS:

典型题目:542

class Solution {

public int[][] updateMatrix(int[][] matrix) {

int m = matrix.length;

int n = matrix[0].length;

int[][] result = new int[m][n];

int[][] directions = new int[][] {{0, 1}, {0, -1}, {1, 0}, {-1, 0}};

boolean[][] visited = new boolean[m][n];

Queue queue = new LinkedList<>();

for (int i = 0; i < m; i++) {

for (int j = 0; j < n; j++) {

if (matrix[i][j] == 0) {

result[i][j] = 0;

visited[i][j] = true;

queue.add(new int[] {i, j});

}

}

}

while (!queue.isEmpty()) {

int[] position = queue.poll();

int r = position[0];

int c = position[1];

for (int[] dir : directions) {

int x = r + dir[0];

int y = c + dir[1];

if (x >= 0 && y >= 0 && x < m && y < n && !visited[x][y]) {

result[x][y] = result[r][c] + 1;

visited[x][y] = true;

queue.add(new int[] {x, y});

}

}

}

return result;

}

}

DFS:

DFS在递归的时候,经常有两种构造递归函数的办法,一种是直接带返回值;另一种是不带返回值(void),通常第二种用全局变量,常用于计数等。

DFS实际就是一种枚举,借助于递归来实现。

一、树上的DFS

1.基本类型

典型题目:

2.树上的DFS还有一个变种就是回溯,回溯是一种隐式的多叉决策树,通常还要剪枝。

二、图上的DFS

典型题目:200、531、130、733

三、总结

DFS比较重要的:

1.递归函数的含义。

首先要想明白递归函数的用途。想不清楚时给递归删除写下备注。

入参是什么,要带哪些参数进去?

返回值是什么?返回值是void还是指定类型?

2.递归的终止条件

如果返回值是void,考虑终止条件可能是边界条件,对于树来说,如果从根节点出发,是到了叶子节点;对于图来说,是超出了边界。

如果返回值是指定类型,终止条件可能是满足一个条件后的值就返回。

3.当前节点的处理

在二叉树中,就是当前node的处理;在图中或者小岛问题中,就是当前节点的处理。

4.下一节点的处理

在二叉树中,就是左子树和右子树的处理。在图中,根据题意,可能要处理4个方向,可能处理一个方向。

四、注意点

1.在写图中的DFS时,方向数组可以提供为全局变量,因为递归会反复调用,可以不放在dfs函数里。

2. 图上的BFS或者DFS都要注意visited的使用,普通图用hashset来存储visited,二维数组图用一个boolean数组来记录visited

五、别人的总结:

1.岛屿类问题的通用解法、DFS 遍历框架

2.

回溯

典型题目:排列、组合、子集

回溯重要的是画出决策树!

result = []

def backtrack(路径, 选择列表):

if 满足结束条件:

result.add(路径)

return

for 选择 in 选择列表:

做选择

backtrack(路径, 选择列表)

撤销选择

回溯要点:选择,约束(可以做剪枝),结束条件

回溯的剪枝是个难点:491中同层用set过滤

贪心算法:

1.数组区间类贪心。看有没有重叠,常常用数组的end来排序,然后遍历。

区间调度:435 先按照end排序,然后算不重叠的区间。最后拿n-不重叠的区间

646

二分

常规的二分就不说了,无非就是寻找区间内的某个值,寻找左边界,寻找右边界。数组有旋转数组,山脉数组。

二分的本质就是在满足单调性的前提下,分而治之解决问题。

这里提下二分的一种变种,前提是看出单调性,单调性调参。

主要题目有:875、1482、LCP12

可以查看下面题解中最后提到的变种二分题目。

int溢出

int是32位的,long是64位的,涉及到整数的乘法,防止溢出可以用long

取余

如果有负数,不能直接取余,需要(sum%K+K)%K ,防止出现负数的情况。

取模

有的时候,题目里面会说结果是个很大的整数,需要对1e9+7取余后返回。这里要注意,取余之前,先用long来装这个“很大的整数”,为什么是long呢?因为int是32位,换成十进制整数是一个2开头的10位整数,而long是64位的。既然是很大的整数,就有可能超过int的范围,所以先用long来装,再取余。

因为int是然后取余后返回int,例如(int)(count %MOD),这里括号要准确,要不然结果不对。

去重

1.可以用hashset去重,使用时先判断set.contains,然后再set.add

2.可以用stream.distinct()去重

3.可以直接把list放入set中去重。

判断是否是回文数

判断是否是奇偶数

1.可以通过对2取余来计算,取余后为0是偶数,取余后为1是奇数。

2.可以通过num&1来计算,如果是0是偶数,为1是奇数。位运算比取余运算快很多。

返回值:

求最小值时返回值不要是0或者-1,容易出bug。可以设置为Integer.MAX_VALUE;

逆向思维:

或者说是求补思维,当正向比较复杂时,反向想,会简化很多,原始问题可以通过一种很巧妙的方式进行转换。

例如:

1423. 可获得的最大点数 ---当正向首尾取时比较复杂,可以反过来想,想一下。维护一个length-k长度的滑动窗口,如果窗口内和最小,则剩下来的和就最大。

417. 太平洋大西洋水流问题 --- 从中间往边界扩散不好处于,反过来想,从边界往中间扩展。

DOUBLE运算

在计算返回值为一个double类型时,不要用int/int 直接用double/int,这样比较精准。

Map常见的坑key相同时,后面的值会覆盖前面的value。所以要考虑是否要用map.putIfAbsent

get经常会有空指针,所以建议用map.getOrDefault

value为空时,需要remove key,否则map的size会有问题。

降低时间复杂度的方法:空间换时间

比如map的引入,dp数组的引入,都是空间换时间。

2.引入二分

3.list.contains改为set.contains

设计类题目注意点:

设计类题目一般都不难,主要有这么两个注意点:

1)读懂题意,选取合适的数据结构。选择合适的数据结构,设计类题目就成功了一半。

2)考虑各种边界条件,尤其是入参的校验。和平常业务开发联系还是比较紧密的。

边界条件通常有:

1)数据结构为空时的处理

2)数组越界

!!!数据结构删除注意!!!

例如map嵌套的数据结构,map> 根据key值获取一个list后,list删除某个元素,此时并不需要重新put map,map已经是删除后的结果。

同时,如果list为空,map里需要移除。

import java.util.ArrayList;

import java.util.HashMap;

import java.util.List;

import java.util.Map;

public class Solution {

public static void main(String[] args) {

Map> map = new HashMap<>();

List list1 = new ArrayList<>();

list1.add(6);

list1.add(7);

list1.add(8);

List list2 = new ArrayList<>();

list2.add(3);

list2.add(4);

list2.add(5);

map.put(1, list1);

map.put(2, list2);

List list = map.get(1);

list.remove(2);

if (list.isEmpty()) {

map.remove(1);

}

System.out.println(map);

}

}

编程习惯:

1.不要用Ctrl+D来复制代码,例如递归左右子树以及DFS或者BFS多个方向时,容易改漏出现莫名其妙的bug,还不容易排查。

复制一个就修改检查一个,不要全部复制完再修改检查。

2. 设计大于调试。在拿到题目后,花5-10分钟想清楚,甚至用注释写下来,也比后面边写边想、或者写错了debug要好,因为调试耗时较长。

3.做好入参检查。一些corner case没过就是因为异常入参没做检查。

调试技巧:

1.在关键地方加sout日志比直接debug直观,并且快。

2.二叉树可以逐个节点构造。

薄弱点:Java8语法,stream流

并查集++

字典树+

图、邻接表、邻接矩阵。树转图。++

树(递归) 从数组还原为树

拓扑排序+

单调栈

前缀和

字符串栈++

动态规划

贪心算法++

二分的变形,非常规题。++

DFS ++

回溯+

设计类题目++

区间类问题。区间迭代。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值