如何判断一道算法题能不能用双指针做?
问题类型:双指针法通常用于解决数组或链表类的问题,如查找、排序、去重等。如果题目要求解决的问题属于这些类型,那么可以考虑使用双指针法。
有序性:双指针法通常适用于有序或部分有序的数组或链表。如果题目中的数据具有明显的有序性,那么可以考虑使用双指针法。
重复元素:双指针法通常适用于存在重复元素的情况。如果题目中的数据存在重复元素,那么可以考虑使用双指针法。
循环关系:在问题中寻找是否存在某种循环关系,例如两个指针分别从头部和尾部向中间移动,或者两个指针分别从某个位置向两侧移动。这种循环关系是双指针法的基础。如果题目中存在这样的循环关系,那么可以考虑使用双指针法。
边界条件:在使用双指针法时,需要考虑边界条件。例如,当两个指针相遇时,需要判断是否已经找到答案;当两个指针交叉时,需要判断是否需要继续移动指针等。如果题目中的边界条件清晰明确,那么可以考虑使用双指针法。
双指针题型主要出现在数组中,以下是一些常见的双指针题型:
- 快慢指针:这是双指针中最常用的一种形式,一般用于解决链表中的环问题。(删除链表倒数第几个节点)
- 左右指针:两个指针相向而行,直到中间相遇。此类问题的熟练使用需要一定的经验积累。(数字之和问题)
- 二分查找:这是一种在有序数组中查找某一特定元素的搜索算法。
- 盛最多水的容器:这是一个经典的双指针问题,找出由非负整数构成的坐标系中,可以盛放多少水的容器。
- 初始化左右指针:定义左右指针 i 和 j,分别表示子串的开始和结束位置。
- 确定状态:定义一个长度为 n 的数组 height,存储每个柱子的高度。
- 计算当前可行方案的最大值:利用双指针 i 和 j,分别指向左右两端,计算出以这两个柱子为底边的最大盛水量。
- 移动指针:根据当前最大盛水量更新答案,并将左右指针向中间移动一位。
- 重复步骤 3 和 4,直到左右指针相遇。
- 返回结果:返回最大盛水量。
- 初始化两个指针:定义快慢指针 slow 和 fast,分别表示子串的开始和结束位置。
- 确定状态:根据具体问题,定义一个哈希表或数组来存储需要处理的数据。
- 遍历链表:不断移动 fast 指针,直到 fast 指针指向链表的末尾。
- 判断是否满足条件:在每次移动 fast 指针之后,判断当前节点是否满足题目要求的条件。如果满足,则将该节点加入到结果集中。
- 更新指针位置:根据具体问题,更新 slow 和 fast 指针的位置。一般情况下,slow 指针向前移动一步,fast 指针向前移动两步。
- 重复步骤 3-5,直到 fast 指针指向链表的末尾。
- 返回结果:返回结果集。
如何判断一道算法题能不能用滑动窗口做?
判断一道算法题能否使用滑动窗口算法的解决,主要看以下几个关键要素:
- 问题能否用数组或字符串的形式表示。滑动窗口算法适用于处理数组或字符串的问题。
- 问题是否可以抽象为寻找连续子数组或子字符串的问题。这是因为滑动窗口算法的基本思想就是维护一个大小固定的窗口,在数组或字符串上不断移动,然后更新答案。
- 问题的最优解是否可以通过比较相邻元素得到。许多滑动窗口题目都涉及到比较窗口内元素与外界元素的关系,以确定下一步的操作。
- 是否存在重复子问题。由于滑动窗口算法利用了“以空间换时间”的策略,将嵌套循环的时间复杂度优化为了线性时间复杂度,因此如果一个问题具有大量的重复子问题,那么它就非常适合使用滑动窗口算法来解决。
滑动窗口算法是一种常见的算法思想,通常应用于数组、链表和字符串等线性结构上。这种算法的主要目标是找出一个连续的子串或子数组,满足一些特定的条件,如长度最短、最长或包含特定元素等。
以下是一些常见的滑动窗口题型:
- 寻找最长的子字符串:例如在一个字符串中找出包含所有字符的最长子串。
- 寻找最短的子字符串:例如在一个字符串中找出包含所有字符的最短子串。
- 寻找最长的全为1的子数组长度:例如在一个数组中找出全是1的最长子数组的长度。
- 寻找不包含任何元素的子数组:例如在一个数组中找出没有元素的子数组。
- 初始化两个指针:定义左右指针 i 和 j,分别表示子串的开始和结束位置。
- 确定状态:定义一个哈希表来存储已经遍历过的字符及其对应的下标,这样当遇到重复字符时,可以利用哈希表中的信息,将右指针 i 移动到重复字符的下一个位置。
- 扩展窗口:不断增大右指针 j,直到 j 指向的元素在哈希表中存在或等于字符串的末尾。
- 缩小窗口:如果右指针 j 指向的元素在哈希表中存在,那么移动左指针 i 到哈希表中该元素值加一的位置,然后删除哈希表中 j 所指向的元素。重复此步骤,直到 j 指向的元素在哈希表中不存在。
- 记录答案:每扩展一次窗口,就比较当前窗口的大小和之前记录的最大窗口大小,并更新最大窗口的大小。
- 返回结果:返回最大窗口的大小。
最小覆盖子串题解
记载s,t出现次数,如果滑动窗口内字符次数<子串内字符出现的次数就向右拉框。当满了在向左拉框,直到当前字符数不小于子串中的出现个数。记录长度并返回
class Solution {
public String minWindow(String s, String t) {
HashMap hs = new HashMap();
HashMap 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 = 0x3f3f3f3f, cnt = 0; //有多少个元素符合
for(int i = 0,j = 0;i < s.length();i ++)
{
hs.put(s.charAt(i), hs.getOrDefault(s.charAt(i), 0) + 1);
if(ht.containsKey(s.charAt(i)) && hs.get(s.charAt(i)) <=
ht.get(s.charAt(i))) cnt ++;
while(j < i && (!ht.containsKey(s.charAt(j)) ||
hs.get(s.charAt(j)) > ht.get(s.charAt(j))))
{
int count = hs.get(s.charAt(j)) - 1;
hs.put(s.charAt(j), count);
j ++;
}
if(cnt == t.length() && i - j + 1 < len){
len = i - j + 1;
ans = s.substring(j,i + 1);
}
判断问题类型:首先需要明确问题类型,判断是否适合使用单调队列。单调队列适用于一些需要维护单调性的问题,例如区间最小值、最大最小值等。
分析数据结构:如果问题类型适合使用单调队列,需要进一步分析数据结构。单调队列通常用于处理数组或链表等线性数据结构,因此需要判断题目中的数据结构是否符合要求。
判断是否有单调性:单调队列的核心思想是维护数据的单调性,因此需要判断题目中是否存在单调性。如果存在单调性,可以考虑使用单调队列来优化算法。
判断是否需要频繁查询最大值或最小值:单调队列通常用于频繁查询最大值或最小值的情况,因此需要判断题目中是否需要频繁进行这种查询操作。
区间最小值问题:这是单调队列的一种常见应用,可以通过维护一个单调递减的队列来求解。
寻找最大最小值:单调队列可以用来优化查找最大最小值的时间复杂度,时间复杂度小于O(n),但大于O(1)。
滑动窗口问题:利用单调队列,我们可以解决一些复杂的滑动窗口问题。
动态规划问题:在某些特定的动态规划问题中,单调队列可以作为优化手段,降低时间复杂度。
字符串处理问题:在处理某些涉及到字符数组或字符串的问题时,可以利用单调队列的性质来简化问题解决过程。
单调栈
常见模型:找出每个数左边离它最近的比它大/小的数
int tt = 0; for (int i = 1; i <= n; i ++ ) { while (tt && check(stk[tt], i)) tt -- ; stk[ ++ tt] = i; }
单调队列
常见模型:找出滑动窗口中的最大值/最小值
int hh = 0, tt = -1; for (int i = 0; i < n; i ++ ) { while (hh <= tt && check_out(q[hh])) hh ++ ; // 判断队头是否滑出窗口 while (hh <= tt && check(q[tt], i)) tt -- ; q[ ++ tt] = i; }
单调队列:(比队尾大或小右边界拉框,长度超了左边界拉框)
class Solution {
public int[] maxSlidingWindow(int[] nums, int k) {
int l=0,r=-1;
int[] q = new int[100005];
int [] res = new int[nums.length-k+1];
for(int i=0;i=k-1) {
res[i-k+1]=nums[q[l]];
}
}
return res;
}
}
本题用dp即可
public class Solution {
public int maxSubArray(int[] nums) {
int len = nums.length;
// dp[i] 表示:以 nums[i] 结尾的连续子数组的最大和
int[] dp = new int[len];
dp[0] = nums[0];
for (int i = 1; i < len; i++) {
if (dp[i - 1] > 0) {
dp[i] = dp[i - 1] + nums[i];
} else {
dp[i] = nums[i];
}
}
// 也可以在上面遍历的同时求出 res 的最大值,这里我们为了语义清晰分开写,大家可以自行选择
int res = dp[0];
for (int i = 1; i < len; i++) {
res = Math.max(res, dp[i]);
}
return res;
}
}
先将左端点排序,然后从第二个开始,如果后一个的左端点小于前一个的右端点,那么就可以合并,再比较右边看需不需要更新范围。
class Solution {
public int[][] merge(int[][] intervals) {
if (intervals.length == 0) {
return new int[0][2];
}
Arrays.sort(intervals, new Comparator() {
public int compare(int[] interval1, int[] interval2) {
return interval1[0] - interval2[0];
}
});
List merged = new ArrayList();
for (int i = 0; i < intervals.length; ++i) {
int L = intervals[i][0], R = intervals[i][1];
if (merged.size() == 0 || merged.get(merged.size() - 1)[1] < L) {
merged.add(new int[]{L, R});
} else {
merged.get(merged.size() - 1)[1] = Math.max(merged.get(merged.size() - 1)[1], R);
}
}
return merged.toArray(new int[merged.size()][]);
}
}
和翻转字符串一样,所有翻转都是一个方法。就是把原数组或子串翻倍截取
class Solution {
public void rotate(int[] nums, int k) {
int len =nums.length;
int[] arr=new int[2*len];
for(int i=0;i<2*len;i++){
arr[i]=nums[i%len];
}
k=k%len;
for(int i=len-k,j=0;i<2*len-k;i++,j++){
nums[j]=arr[i];
}
}
}
//两个数组,分布存该数字左边货右边的乘积,优化方案就是把右边的乘积
//在遍历时顺便做了,复杂度降低到O(1)
class Solution {
public int[] productExceptSelf(int[] nums) {
int length = nums.length;
int[] answer = new int[length];
// answer[i] 表示索引 i 左侧所有元素的乘积
// 因为索引为 '0' 的元素左侧没有元素, 所以 answer[0] = 1
answer[0] = 1;
for (int i = 1; i < length; i++) {
answer[i] = nums[i - 1] * answer[i - 1];
}
// R 为右侧所有元素的乘积
// 刚开始右边没有元素,所以 R = 1
int R = 1;
for (int i = length - 1; i >= 0; i--) {
// 对于索引 i,左边的乘积为 answer[i],右边的乘积为 R
answer[i] = answer[i] * R;
// R 需要包含右边所有的乘积,所以计算下一个结果时需要将当前值乘到 R 上
R *= nums[i];
}
return answer;
}
}
如果无时间复杂度要求直接用哈希,但是题目要求实现:求现时间复杂度为 O(n) 并且只使用常数级别额外空间的解决方案。该题使用原地哈希
原地哈希就相当于,让每个数字n都回到下标为n-1的家里。
而那些没有回到家里的就成了孤魂野鬼流浪在外,他们要么是根本就没有自己的家(数字小于等于0或者大于nums.size()),要么是自己的家被别人占领了(出现了重复)。
这些流浪汉被临时安置在下标为i的空房子里,之所以有空房子是因为房子i的主人i+1失踪了(数字i+1缺失)。
因此通过原地构建哈希让各个数字回家,我们就可以找到原始数组中重复的数字还有消失的数字。
//请你实现时间复杂度为 O(n) 并且只使用常数级别额外空间的解决方案
public class Solution {
public int firstMissingPositive(int[] nums) {
int len = nums.length;
for (int i = 0; i < len; i++) {
while (nums[i] > 0 && nums[i] <= len && nums[nums[i] - 1] != nums[i]) {
// 满足在指定范围内、并且没有放在正确的位置上,才交换
// 例如:数值 3 应该放在索引 2 的位置上
swap(nums, nums[i] - 1, i);
}
}
// [1, -1, 3, 4]
for (int i = 0; i < len; i++) {
if (nums[i] != i + 1) {
return i + 1;
}
}
// 都正确则返回数组长度 + 1
return len + 1;
}
private void swap(int[] nums, int index1, int index2) {
int temp = nums[index1];
nums[index1] = nums[index2];
nums[index2] = temp;
}
}
用两个数组分别表示行和列进行标记
class Solution {
public void setZeroes(int[][] matrix) {
int m = matrix.length, n = matrix[0].length;
boolean[] row = new boolean[m];
boolean[] col = new boolean[n];
for (int i = 0; i < m; i++) {
for (int j = 0; j < n; j++) {
if (matrix[i][j] == 0) {
row[i] = col[j] = true;
}
}
}
for (int i = 0; i < m; i++) {
for (int j = 0; j < n; j++) {
if (row[i] || col[j]) {
matrix[i][j] = 0;
}
}
}
}
}
和常见图的题目一样,标明访问的点和方向模拟即可。
class Solution {
public List spiralOrder(int[][] matrix) {
List order = new ArrayList();
if (matrix == null || matrix.length == 0 || matrix[0].length == 0) {
return order;
}
int rows = matrix.length, columns = matrix[0].length;
boolean[][] visited = new boolean[rows][columns];
int total = rows * columns;
int row = 0, column = 0;
int[][] directions = {{0, 1}, {1, 0}, {0, -1}, {-1, 0}};
int directionIndex = 0;
for (int i = 0; i < total; i++) {
order.add(matrix[row][column]);
visited[row][column] = true;
int nextRow = row + directions[directionIndex][0], nextColumn = column + directions[directionIndex][1];
if (nextRow < 0 || nextRow >= rows || nextColumn < 0 || nextColumn >= columns || visited[nextRow][nextColumn]) {
directionIndex = (directionIndex + 1) % 4;
}
row += directions[directionIndex][0];
column += directions[directionIndex][1];
}
return order;
}
}
class Solution {
public void rotate(int[][] matrix) {
int n = matrix.length;
int[][] matrix_new = new int[n][n];
for (int i = 0; i < n; ++i) {
for (int j = 0; j < n; ++j) {
matrix_new[j][n - i - 1] = matrix[i][j];
}
}
for (int i = 0; i < n; ++i) {
for (int j = 0; j < n; ++j) {
matrix[i][j] = matrix_new[i][j];
}
}
}
}
看到查找和从小到到排序,首先想到二分
class Solution {
public boolean searchMatrix(int[][] matrix, int target) {
for (int[] row : matrix) {
int index = search(row, target);
if (index >= 0) {
return true;
}
}
return false;
}
public int search(int[] nums, int target) {
int low = 0, high = nums.length - 1;
while (low <= high) {
int mid = (high - low) / 2 + low;
int num = nums[mid];
if (num == target) {
return mid;
} else if (num > target) {
high = mid - 1;
} else {
low = mid + 1;
}
}
return -1;
}
}
链表的缺点是:无法高效获取长度,无法根据偏移量快速访问元素,这也是经常考察的地方
常见方法:双指针,快慢指针
比如:找出链表倒数第几个节点。(双指针:快指针比慢指针快几步)
判断链表是否有环(快慢指针:快指针是慢指针的两倍,遇上了就证明有环)
遍历两次,长链表指向短链表,长度差也就消除了
public ListNode getIntersectionNode(ListNode headA, ListNode headB) {
if (headA == null || headB == null) return null;
ListNode pA = headA, pB = headB;
while (pA != pB) {
pA = pA == null ? headB : pA.next;
pB = pB == null ? headA : pB.next;
}
return pA;
}
用两个节点反转(pre和cur)
class Solution {
public ListNode reverseList(ListNode head) {
ListNode cur = head, pre = null;
while(cur != null) {
ListNode tmp = cur.next; // 暂存后继节点 cur.next
cur.next = pre; // 修改 next 引用指向
pre = cur; // pre 暂存 cur
cur = tmp; // cur 访问下一节点
}
return pre;
}
}
将链表转化为数组再判断是否回文即可
class Solution {
public boolean isPalindrome(ListNode head) {
List ar = new ArrayList();
ListNode p = head;
while (p != null) {
ar.add(p.val);
p = p.next;
}
for (int i = 0, j = ar.size() - 1; i
经典做法:快慢指针,如果两个指针相遇,那一定有环。
通用解法:哈希表
public class Solution {
public boolean hasCycle(ListNode head) {
Set seen = new HashSet();
while (head != null) {
if (!seen.add(head)) {
return true;
}
head = head.next;
}
return false;
}
}
哈希
public class Solution {
public ListNode detectCycle(ListNode head) {
Set set = new HashSet();
while(head!=null) {
if(set.contains(head)) {
return head;
}
set.add(head);
head=head.next;
}
return null;
}
}
双指针
class Solution {
public ListNode mergeTwoLists(ListNode list1, ListNode list2) {
ListNode ans = new ListNode(0);
ListNode cur = ans;
while(list1!=null&&list2!=null) {
if(list1.val<=list2.val){
cur.next=list1;
list1=list1.next;
}
else{
cur.next=list2;
list2=list2.next;
}
cur=cur.next;
}
if(list1==null)
cur.next=list2;
else
cur.next=list1;
return ans.next;
}
}
判断是否为空,为空置0,最后处理一下最高位
class Solution {
public ListNode addTwoNumbers(ListNode l1, ListNode l2) {
ListNode l3=new ListNode(0);
ListNode pa=l3;
int cons=0;
while(l1!=null||l2!=null){
int a=l1==null?0:l1.val;
int b=l2==null?0:l2.val;
pa.next=new ListNode((a+b+cons)%10);
cons=(a+b+cons)/10;
if(l1!=null) l1=l1.next;
if(l2!=null) l2=l2.next;
pa=pa.next;
}
if(cons!=0) pa.next=new ListNode(cons);
return l3.next;
}
}
快慢指针,开头末尾特殊处理
class Solution {
public ListNode removeNthFromEnd(ListNode head, int n) {
ListNode dummy = new ListNode(0, head);
ListNode first = head;
ListNode second = dummy;
for (int i = 0; i < n; ++i) {
first = first.next;
}
while (first != null) {
first = first.next;
second = second.next;
}
second.next = second.next.next;
ListNode ans = dummy.next;
return ans;
}
}
递归
class Solution {
public ListNode swapPairs(ListNode head) {
if(head == null || head.next == null){
return head;
}
ListNode next = head.next;
head.next = swapPairs(next.next);
next.next = head;
return next;
}
}
两两交换链表中的节点+反转链表
class Solution {
public ListNode reverseKGroup(ListNode head, int k) {
if (head == null || head.next == null){
return head;
}
//定义一个假的节点。
ListNode dummy=new ListNode(0);
//假节点的next指向head。
// dummy->1->2->3->4->5
dummy.next=head;
//初始化pre和end都指向dummy。pre指每次要翻转的链表的头结点的上一个节点。end指每次要翻转的链表的尾节点
ListNode pre=dummy;
ListNode end=dummy;
while(end.next!=null){
//循环k次,找到需要翻转的链表的结尾,这里每次循环要判断end是否等于空,因为如果为空,end.next会报空指针异常。
//dummy->1->2->3->4->5 若k为2,循环2次,end指向2
for(int i=0;i2 变成2->1。 dummy->2->1
pre.next=reverse(start);
//翻转后头节点变到最后。通过.next把断开的链表重新链接。
start.next=next;
//将pre换成下次要翻转的链表的头结点的上一个节点。即start
pre=start;
//翻转结束,将end置为下次要翻转的链表的头结点的上一个节点。即start
end=start;
}
return dummy.next;
}
//链表翻转
// 例子: head: 1->2->3->4
public ListNode reverse(ListNode head) {
//单链表为空或只有一个节点,直接返回原单链表
if (head == null || head.next == null){
return head;
}
//前一个节点指针
ListNode preNode = null;
//当前节点指针
ListNode curNode = head;
//下一个节点指针
ListNode nextNode = null;
while (curNode != null){
nextNode = curNode.next;//nextNode 指向下一个节点,保存当前节点后面的链表。
curNode.next=preNode;//将当前节点next域指向前一个节点 null<-1<-2<-3<-4
preNode = curNode;//preNode 指针向后移动。preNode指向当前节点。
curNode = nextNode;//curNode指针向后移动。下一个节点变成当前节点
}
return preNode;
}
}
用哈希表存储
class Solution {
// 创建一个哈希表用于存储原链表节点和复制后的链表节点的映射关系
Map cachedNode = new HashMap();
public Node copyRandomList(Node head) {
// 如果原链表为空,则返回空
if (head == null) {
return null;
}
// 如果哈希表中没有当前节点的映射关系,则进行复制操作
if (!cachedNode.containsKey(head)) {
// 创建一个新的节点,值为原节点的值
Node headNew = new Node(head.val);
// 将原节点和新节点的映射关系存入哈希表
cachedNode.put(head, headNew);
// 递归复制原节点的下一个节点,并将结果赋值给新节点的next指针
headNew.next = copyRandomList(head.next);
// 递归复制原节点的随机节点,并将结果赋值给新节点的random指针
headNew.random = copyRandomList(head.random);
}
// 返回哈希表中当前节点对应的复制后的节点
return cachedNode.get(head);
}
}
归并排序+合并链表
class Solution {
public ListNode sortList(ListNode head) {
return sortList(head, null);
}
public ListNode sortList(ListNode head, ListNode tail) {
if (head == null) {
return head;
}
if (head.next == tail) {
head.next = null;
return head;
}
ListNode slow = head, fast = head;
while (fast != tail) {
slow = slow.next;
fast = fast.next;
if (fast != tail) {
fast = fast.next;
}
}
ListNode mid = slow;
ListNode list1 = sortList(head, mid);
ListNode list2 = sortList(mid, tail);
ListNode sorted = merge(list1, list2);
return sorted;
}
public ListNode merge(ListNode head1, ListNode head2) {
ListNode dummyHead = new ListNode(0);
ListNode temp = dummyHead, temp1 = head1, temp2 = head2;
while (temp1 != null && temp2 != null) {
if (temp1.val <= temp2.val) {
temp.next = temp1;
temp1 = temp1.next;
} else {
temp.next = temp2;
temp2 = temp2.next;
}
temp = temp.next;
}
if (temp1 != null) {
temp.next = temp1;
} else if (temp2 != null) {
temp.next = temp2;
}
return dummyHead.next;
}
}
1.递归
先确定遍历顺序再确定操作
不要小瞧这一句话,记住这句话,在后面的题目中细细体会,你就会发现几乎所有题目都是按这个步骤思考的。
2.迭代(队列)
3.层序遍历(深搜,广搜)
1.递归:略
2.迭代遍历:
//前序遍历,后序交换一下左右即可 public class Solution { public List
preorderTraversal(TreeNode root) { Stack st = new Stack<>(); List result = new ArrayList<>(); if (root == null) return result; st.push(root); while (!st.isEmpty()) { TreeNode node = st.pop(); // 中 result.add(node.val); if (node.right != null) st.push(node.right); // 右(空节点不入栈) if (node.left != null) st.push(node.left); // 左(空节点不入栈) } return result; } } //中序 import java.util.ArrayList; import java.util.List; import java.util.Stack; class TreeNode { int val; TreeNode left; TreeNode right; TreeNode(int x) { val = x; } } public class Solution { public List
inorderTraversal(TreeNode root) { List result = new ArrayList<>(); Stack st = new Stack<>(); TreeNode cur = root; while (cur != null || !st.isEmpty()) { if (cur != null) { st.push(cur); cur = cur.left; } else { cur = st.pop(); result.add(cur.val); cur = cur.right; } } return result; } } 3.层序遍历:
class Solution { public List
> levelOrder(TreeNode root) { List
> ret = new ArrayList
>(); if (root == null) { return ret; } Queue
queue = new LinkedList (); queue.offer(root); while (!queue.isEmpty()) { List level = new ArrayList (); int currentLevelSize = queue.size(); for (int i = 1; i <= currentLevelSize; ++i) { TreeNode node = queue.poll(); level.add(node.val); if (node.left != null) { queue.offer(node.left); } if (node.right != null) { queue.offer(node.right); } } ret.add(level); } return ret; } }
class Solution {
public List inorderTraversal(TreeNode root) {
List res = new ArrayList();
inorder(root, res);
return res;
}
public void inorder(TreeNode root, List res) {
if (root == null) {
return;
}
inorder(root.left, res);
res.add(root.val);
inorder(root.right, res);
}
}
递归,每递归一次加一
class Solution {
public int maxDepth(TreeNode root) {
if(root==null) return 0;
return Math.max(maxDepth(root.right),maxDepth(root.left))+1;
}
}
翻转二叉树(LeetCode226)
递归,递归必须明确:终止条件,执行操作,返回值。至于不明白执行操作,想一想这个操作最后是什么就行。
class Solution {
public TreeNode invertTree(TreeNode root) {
if(root==null){
return null;
}
TreeNode tmp=root.right;
root.right=root.left;
root.left=tmp;
invertTree(root.left);
invertTree(root.right);
return root;
}
}
将左边与右边交换,然后子树左右再交换
class Solution {
public TreeNode invertTree(TreeNode root) {
if(root==null) {
return null;
}
TreeNode tmp = root.right;
root.right = root.left;
root.left = tmp;
invertTree(root.left);
invertTree(root.right);
return root;
}
}
递归
class Solution {
public boolean isSymmetric(TreeNode root) {
return check(root, root);
}
public boolean check(TreeNode p, TreeNode q) {
if (p == null && q == null) {
return true;
}
if (p == null || q == null) {
return false;
}
return p.val == q.val && check(p.left, q.right) && check(p.right, q.left);
}
}
直径=节点数-1,求出左右的最大深度即可。
class Solution {
int ans;
public int diameterOfBinaryTree(TreeNode root) {
ans=1;
depth(root);
return ans-1;
}
public int depth(TreeNode root){
if(root==null){
return 0;
}
int L=depth(root.left);
int R=depth(root.right);
ans=Math.max(ans,L+R+1);
return Math.max(L,R)+1;
}
}
广搜,注意要遍历完队列。
class Solution {
public List> levelOrder(TreeNode root) {
List> ret = new ArrayList>();
if (root == null) {
return ret;
}
Queue queue = new LinkedList();
queue.offer(root);
while (!queue.isEmpty()) {
List level = new ArrayList();
int currentLevelSize = queue.size();
for (int i = 1; i <= currentLevelSize; ++i) {
TreeNode node = queue.poll();
level.add(node.val);
if (node.left != null) {
queue.offer(node.left);
}
if (node.right != null) {
queue.offer(node.right);
}
}
ret.add(level);
}
return ret;
}
}
递归
class Solution {
public TreeNode sortedArrayToBST(int[] nums) {
return insert(nums,0,nums.length-1);
}
public TreeNode insert(int[] nums,int l,int r){
if(l>r) return null;
int mid=l+r>>1;
TreeNode root=new TreeNode(nums[mid]);
root.left=insert(nums,l,mid-1);
root.right=insert(nums,mid+1,r);
return root;
}
}
将根节点设置为最大或者最小值,再进行判断
class Solution {
public boolean isValidBST(TreeNode root) {
return isValidBST(root, Long.MIN_VALUE, Long.MAX_VALUE);
}
public boolean isValidBST(TreeNode node, long lower, long upper) {
if (node == null) {
return true;
}
if (node.val <= lower || node.val >= upper) {
return false;
}
return isValidBST(node.left, lower, node.val) && isValidBST(node.right, node.val, upper);
}
}
中序遍历结合队列
class Solution {
public int kthSmallest(TreeNode root, int k) {
Deque stack=new ArrayDeque();
while(root!=null||!stack.isEmpty()){
while(root!=null){
stack.push(root);
root=root.left;
}
root=stack.pop();
--k;
if(k==0){
break;
}
root=root.right;
}
return root.val;
}
}
通过层序遍历找到每一层的最后一个数字。
class Solution {
public List rightSideView(TreeNode root) {
List list = new ArrayList<>();
Deque que = new LinkedList<>();
if (root == null) {
return list;
}
que.offerLast(root);
while (!que.isEmpty()) {
int levelSize = que.size();
for (int i = 0; i < levelSize; i++) {
TreeNode poll = que.pollFirst();
if (poll.left != null) {
que.addLast(poll.left);
}
if (poll.right != null) {
que.addLast(poll.right);
}
if (i == levelSize - 1) {
list.add(poll.val);
}
}
}
return list;
}
}
将左边换到右边,右边换到左边的末尾
class Solution {
public void flatten(TreeNode root) {
while (root != null) {
//左子树为 null,直接考虑下一个节点
if (root.left == null) {
root = root.right;
} else {
// 找左子树最右边的节点
TreeNode pre = root.left;
while (pre.right != null) {
pre = pre.right;
}
//将原来的右子树接到左子树的最右边节点
pre.right = root.right;
// 将左子树插入到右子树的地方
root.right = root.left;
root.left = null;
// 考虑下一个节点
root = root.right;
}
}
}
}
class Solution {
private Map indexMap;
public TreeNode myBuildTree(int[] preorder, int[] inorder, int preorder_left, int preorder_right, int inorder_left, int inorder_right) {
if (preorder_left > preorder_right) {
return null;
}
// 前序遍历中的第一个节点就是根节点
int preorder_root = preorder_left;
// 在中序遍历中定位根节点
int inorder_root = indexMap.get(preorder[preorder_root]);
// 先把根节点建立出来
TreeNode root = new TreeNode(preorder[preorder_root]);
// 得到左子树中的节点数目
int size_left_subtree = inorder_root - inorder_left;
// 递归地构造左子树,并连接到根节点
// 先序遍历中「从 左边界+1 开始的 size_left_subtree」个元素就对应了中序遍历中「从 左边界 开始到 根节点定位-1」的元素
root.left = myBuildTree(preorder, inorder, preorder_left + 1, preorder_left + size_left_subtree, inorder_left, inorder_root - 1);
// 递归地构造右子树,并连接到根节点
// 先序遍历中「从 左边界+1+左子树节点数目 开始到 右边界」的元素就对应了中序遍历中「从 根节点定位+1 到 右边界」的元素
root.right = myBuildTree(preorder, inorder, preorder_left + size_left_subtree + 1, preorder_right, inorder_root + 1, inorder_right);
return root;
}
public TreeNode buildTree(int[] preorder, int[] inorder) {
int n = preorder.length;
// 构造哈希映射,帮助我们快速定位根节点
indexMap = new HashMap();
for (int i = 0; i < n; i++) {
indexMap.put(inorder[i], i);
}
return myBuildTree(preorder, inorder, 0, n - 1, 0, n - 1);
}
}
tagetSum必须为long类型,不然数据量变大会报错。从当前结点开始计算target,然后把下面的节点当作根节点继续开始计算。
class Solution {
public int pathSum(TreeNode root, int targetSum) {
if (root == null) {
return 0;
}
int ret = rootSum(root, targetSum);
ret += pathSum(root.left, targetSum);
ret += pathSum(root.right, targetSum);
return ret;
}
public int rootSum(TreeNode root, long targetSum) {
int ret = 0;
if (root == null) {
return 0;
}
int val = root.val;
if (val == targetSum) {
ret++;
}
ret += rootSum(root.left, targetSum - val);
ret += rootSum(root.right, targetSum - val);
return ret;
}
}
p,q必须分别存在在两个子树中;从上往下找,如果两个子树都搜到了,则返回当前root,否
class Solution {
public TreeNode lowestCommonAncestor(TreeNode root, TreeNode p, TreeNode q) {
if(root==null||root==p||root==q) return root;
TreeNode left=lowestCommonAncestor(root.left,p,q);
TreeNode right=lowestCommonAncestor(root.right,p,q);
if(left==null) return right;
if(right==null) return left;
return root;
}
}
递归比较左右子树的值,返回最大值
class Solution {
int maxSum = Integer.MIN_VALUE;
public int maxPathSum(TreeNode root) {
maxGain(root);
return maxSum;
}
public int maxGain(TreeNode node) {
if (node == null) {
return 0;
}
int leftGain = Math.max(maxGain(node.left), 0);
int rightGain = Math.max(maxGain(node.right), 0);
int priceNewpath = node.val + leftGain + rightGain;
maxSum = Math.max(maxSum, priceNewpath);
return node.val + Math.max(leftGain, rightGain);
}
}
代码模板
void backtracking(参数) {
if (终止条件:大多数是n=题目给出的最大长度) {
存放结果;
return;
}
for (选择:本层集合中元素(树中节点孩子的数量就是集合的大小)) {
处理节点;
backtracking(路径,选择列表); // 递归
回溯,撤销处理结果
}
}
class Solution {
public List> permute(int[] nums) {
List> res = new ArrayList<>();
int n =nums.length;
List output = new ArrayList<>();
for(int num:nums) {
output.add(num);
}
dfs(n,res,output,0);
return res;
}
public void dfs(int n,List> res,List output,int
first) {
if(first==n) {
res.add(new ArrayList<>(output));
return;
}
for(int i=first;i
回溯,终止条件:当前索引等于数组长度,循环条件:加入或不加入当前数字
class Solution {
List t=new ArrayList<>();
List> ans=new ArrayList>();
public List> subsets(int[] nums) {
back(0,nums);
return ans;
}
public void back(int cur,int[] nums){
if(cur==nums.length){
ans.add(new ArrayList(t));
return;
}
t.add(nums[cur]);
back(cur+1,nums);
t.remove(t.size() - 1);
back(cur + 1, nums);
}
}
class Solution {
public List letterCombinations(String digits) {
List combinations = new ArrayList();
if(digits.length()==0)
return combinations;
Map phoneMap = new HashMap(){
{
put('2',"abc");
put('3',"def");
put('4',"ghi");
put('5',"jkl");
put('6',"mno");
put('7',"pqrs");
put('8',"tuv");
put('9',"wxyz");
}
};
backtrack(combinations,phoneMap,digits,0,new StringBuffer());
return combinations;
}
public void backtrack(List combinations,Map
phoneMap,String dights,int index,StringBuffer combination){
if(index==dights.length()){
combinations.add(combination.toString());
}
else {
char digit = dights.charAt(index);
String letters = phoneMap.get(digit);
for(int i=0;i
找出所有回文串,并标记。再回溯
class Solution {
boolean[][] f;
List> ret = new ArrayList>();
List ans = new ArrayList();
int n;
public List> partition(String s) {
n = s.length();
f = new boolean[n][n];
for (int i = 0; i < n; ++i) {
Arrays.fill(f[i], true);
}
for (int i = n - 1; i >= 0; --i) {
for (int j = i + 1; j < n; ++j) {
f[i][j] = (s.charAt(i) == s.charAt(j)) && f[i + 1][j - 1];
}
}
dfs(s, 0);
return ret;
}
public void dfs(String s, int i) {
if (i == n) {
ret.add(new ArrayList(ans));
return;
}
for (int j = i; j < n; ++j) {
if (f[i][j]) {
ans.add(s.substring(i, j + 1));
dfs(s, j + 1);
ans.remove(ans.size() - 1);
}
}
}
}
先将括号放入哈希表,降低查找消耗。左括号就入队,右括号进行匹配,成功就将左括号出队,失败就返回false
class Solution {
private static final Map map = new HashMap(){{
put('{','}'); put('[',']'); put('(',')'); put('?','?');
}};
public boolean isValid(String s) {
if(s.length() > 0 && !map.containsKey(s.charAt(0))) return false;
LinkedList stack = new LinkedList() {{ add('?'); }};
for(Character c : s.toCharArray()){
if(map.containsKey(c)) stack.addLast(c);
else if(map.get(stack.removeLast()) != c) return false;
}
return stack.size() == 1;
}
}
遇到数字,左括号入栈,右括号出栈。
class Solution {
int ptr;
public String decodeString(String s) {
LinkedList stk = new LinkedList();
ptr = 0;
while (ptr < s.length()) {
char cur = s.charAt(ptr);
if (Character.isDigit(cur)) {
// 获取一个数字并进栈
String digits = getDigits(s);
stk.addLast(digits);
} else if (Character.isLetter(cur) || cur == '[') {
// 获取一个字母并进栈
stk.addLast(String.valueOf(s.charAt(ptr++)));
} else {
++ptr;
LinkedList sub = new LinkedList();
while (!"[".equals(stk.peekLast())) {
sub.addLast(stk.removeLast());
}
Collections.reverse(sub);
// 左括号出栈
stk.removeLast();
// 此时栈顶为当前 sub 对应的字符串应该出现的次数
int repTime = Integer.parseInt(stk.removeLast());
StringBuffer t = new StringBuffer();
String o = getString(sub);
// 构造字符串
while (repTime-- > 0) {
t.append(o);
}
// 将构造好的字符串入栈
stk.addLast(t.toString());
}
}
return getString(stk);
}
public String getDigits(String s) {
StringBuffer ret = new StringBuffer();
while (Character.isDigit(s.charAt(ptr))) {
ret.append(s.charAt(ptr++));
}
return ret.toString();
}
public String getString(LinkedList v) {
StringBuffer ret = new StringBuffer();
for (String s : v) {
ret.append(s);
}
return ret.toString();
}
}
用递增的单调栈,返回的是下标。
class Solution {
public int[] dailyTemperatures(int[] temperatures) {
int length = temperatures.length;
int[] ans = new int[length];
Deque stack = new LinkedList();
for (int i = 0; i < length; i++) {
int temperature = temperatures[i];
while (!stack.isEmpty() && temperature > temperatures[stack.peek()]) {
int prevIndex = stack.pop();
ans[prevIndex] = i - prevIndex;
}
stack.push(i);
}
return ans;
}
}
利用归并排序的思路,如果mid=k就返回
class Solution {
int quickselect(int[] nums, int l, int r, int k) {
if (l == r) return nums[k];
int x = nums[l], i = l - 1, j = r + 1;
while (i < j) {
do i++; while (nums[i] < x);
do j--; while (nums[j] > x);
if (i < j){
int tmp = nums[i];
nums[i] = nums[j];
nums[j] = tmp;
}
}
if (k <= j) return quickselect(nums, l, j, k);
else return quickselect(nums, j + 1, r, k);
}
public int findKthLargest(int[] _nums, int k) {
int n = _nums.length;
return quickselect(_nums, 0, n - 1, n - k);
}
}
桶排序
//基于桶排序求解「前 K 个高频元素」
class Solution {
public List topKFrequent(int[] nums, int k) {
List res = new ArrayList();
// 使用字典,统计每个元素出现的次数,元素为键,元素出现的次数为值
HashMap map = new HashMap();
for(int num : nums){
if (map.containsKey(num)) {
map.put(num, map.get(num) + 1);
} else {
map.put(num, 1);
}
}
//桶排序
//将频率作为数组下标,对于出现频率不同的数字集合,存入对应的数组下标
List[] list = new List[nums.length+1];
for(int key : map.keySet()){
// 获取出现的次数作为下标
int i = map.get(key);
if(list[i] == null){
list[i] = new ArrayList();
}
list[i].add(key);
}
// 倒序遍历数组获取出现顺序从大到小的排列
for(int i = list.length - 1;i >= 0 && res.size() < k;i--){
if(list[i] == null) continue;
res.addAll(list[i]);
}
return res;
}
}
通过局部最优,推出整体最优。手动模拟即可,你会不知不觉就用上贪心。
- 将问题分解为若干个子问题
- 找出适合的贪心策略
- 求解每一个子问题的最优解
- 将局部最优解堆叠成全局最优解
如果知道股票的最低点,问题就迎刃而解了。所以把最低价格记录下来就好了
class Solution {
public int maxProfit(int[] prices) {
int minp=Integer.MAX_VALUE;
int maxp=0;
for(int i=0;iprices[i]) minp=prices[i];
else if(prices[i]-minp>maxp) maxp=prices[i]-minp;
}
return maxp;
}
}
两道题都是拉框,框的大小: maxl=Math.max(maxl,i+nums[i]);。看框能不能到和能拉几个框
class Solution {
public boolean canJump(int[] nums) {
int maxn=0;
for(int i=0;i=nums.length-1) return true;
}
}
return false;
}
}
class Solution {
public int jump(int[] nums) {
int count=0,maxl=0;
int end=0;
for(int i=0;i
划分字母区间(LeetCode763)
一次遍历记录每个字母的最后位置,二次遍历记录当前字符串出现字母的最后位置,遍历到末尾就输出
class Solution {
public List partitionLabels(String s) {
int[] last = new int[26];
int length = s.length();
for (int i = 0; i < length; i++) {
last[s.charAt(i) - 'a'] = i;
}
List partition = new ArrayList();
int start = 0, end = 0;
for (int i = 0; i < length; i++) {
end = Math.max(end, last[s.charAt(i) - 'a']);
if (i == end) {
partition.add(end - start + 1);
start = end + 1;
}
}
return partition;
}
}
基本思路:
确定状态方程
确定初始化
确定遍历顺序
打印结果进行验证
基本思路:
for(int i=0;i
for(int j=V;)//有限的物品:看还有多少剩余空间;无限个物品:试着用一个物品装满背包(从当前物品体积开始遍历)注意下标不能小于0
详细代码请看文章:
动态规划
最长公共子序列问题 给定两个字符串str1和str2,求它们的最长公共子序列的长度。
编辑距离问题 给定两个字符串str1和str2,将str1转换为str2的最少编辑操作次数。编辑操作包括插入、删除和替换一个字符。
0-1背包问题 给定一组物品,每个物品有一定的价值和重量,现在有一个容量为W的背包,求在不超过背包容量的情况下,能够装入的物品的最大价值。
完全背包问题 给定一组物品,每个物品有一定的价值和重量,现在有一个容量为W的背包,求在不超过背包容量的情况下,能够装入的物品的最大价值。与0-1背包问题的区别在于,完全背包问题中每种物品可以无限次装入。
最长递增子序列问题 给定一个整数数组arr,求它的最长递增子序列的长度。
最长递减子序列问题 给定一个整数数组arr,求它的最长递减子序列的长度。
最长回文子串问题 给定一个字符串s,求它的最长回文子串。
最小编辑距离问题 给定两个字符串str1和str2,求将str1转换为str2所需的最少编辑操作次数。编辑操作包括插入、删除和替换一个字符。
矩阵链乘法问题 给定一个矩阵链乘法的表达式,求最优的切割方式,使得乘法运算的次数最少。
- 背包问题:给定一组物品,每种物品都有自己的重量和价格,在限定的总重量内,如何选择物品使得总价格最高。该问题可以分为01背包问题、完全背包问题和多重背包问题。
- 最大子段和问题:给定一个整数数组,求该数组中连续的子数组的最大和。
- 最长公共子序列问题:给定两个字符串,求它们的最长公共子序列的长度。
- 打家劫舍问题:假设你是一个专业的小偷,计划偷窃沿街的房屋,每家房屋都有一定数量的钱,你需要选择偷窃的房屋,使得偷窃的总金额最大。
- 爬阶梯问题:假设你正在爬楼梯,需要n阶你才能到达楼顶。每次你可以爬1或2个台阶,你有多少种不同的方法可以爬到楼顶。
- 买卖股票问题:给定一个股票价格数组,你只能选择某一天买入这只股票,并选择在未来的某一个不同的日子卖出该股票。设计一个算法来计算你所能获取的最大利润。
- 区间加权最大值问题(多约束):给定一个权值数组和一个值数组,寻找一个子数组,使得该子数组的和最大,且其权值之和最小,同时满足一些额外的约束条件。
- 接雨水问题(多约束):给定一个长度为n的直方柱子,计算在雨水下落时,可以接住多少水,同时满足一些额外的约束条件。
- 纸牌游戏问题(多约束):给定一个纸牌游戏规则,计算在最优策略下,可以赢得的最大分数,同时满足一些额外的约束条件。
class Solution {
public List> generate(int numRows) {
List> ret = new ArrayList>();
for (int i = 0; i < numRows; ++i) {
List row = new ArrayList();
for (int j = 0; j <= i; ++j) {
if (j == 0 || j == i) {
row.add(1);
} else {
row.add(ret.get(i - 1).get(j - 1) + ret.get(i - 1).get(j));
}
}
ret.add(row);
}
return ret;
}
}
是否抢第i个取决于:抢第i家豪还是抢i-1家好
class Solution {
public int rob(int[] nums) {
if (nums == null || nums.length == 0) {
return 0;
}
int length = nums.length;
if (length == 1) {
return nums[0];
}
int[] dp = new int[length];
dp[0] = nums[0];
dp[1] = Math.max(nums[0], nums[1]);
for (int i = 2; i < length; i++) {
dp[i] = Math.max(dp[i - 2] + nums[i], dp[i - 1]);
}
return dp[length - 1];
}
}
装满为止,再进行比较,最后还要进行再次判断是否有效。
class Solution {
public int coinChange(int[] coins, int amount) {
int[] dp=new int[amount+1];
Arrays.fill(dp,amount+1);
dp[0]=0;
for(int i=0;i amount ? -1 : dp[amount];
}
}
拆分问题一般都是枚举字符串的长度,再定义起点。
public class Solution {
public boolean wordBreak(String s, List wordDict) {
Set wordDictSet = new HashSet(wordDict);
boolean[] dp = new boolean[s.length() + 1];
dp[0] = true;
for (int i = 1; i <= s.length(); i++) {
for (int j = 0; j < i; j++) {
if (dp[j] && wordDictSet.contains(s.substring(j, i))) {
dp[i] = true;
break;
}
}
}
return dp[s.length()];
}
}
找到比自己小的加1,再找到最大值即可
class Solution {
public int lengthOfLIS(int[] nums) {
int res=0;
int[] f=new int[nums.length];
for(int i=0;i
乘积问题可以联系最大和的问题,注意负数乘负为正,所以结果就在最大的正数和负数之间产生。
class Solution {
public int maxProduct(int[] nums) {
int length = nums.length;
int[] maxF = new int[length];
int[] minF = new int[length];
System.arraycopy(nums, 0, maxF, 0, length);
System.arraycopy(nums, 0, minF, 0, length);
for (int i = 1; i < length; ++i) {
maxF[i] = Math.max(maxF[i - 1] * nums[i], Math.max(nums[i], minF[i - 1] * nums[i]));
minF[i] = Math.min(minF[i - 1] * nums[i], Math.min(nums[i], maxF[i - 1] * nums[i]));
}
int ans = maxF[0];
for (int i = 1; i < length; ++i) {
ans = Math.max(ans, maxF[i]);
}
return ans;
}
}
用动态规划找到一个子集使得和为数组内所有数值的一半。
class Solution {
public boolean canPartition(int[] nums) {
int n = nums.length;
if (n < 2) {
return false;
}
int sum = 0, maxNum = 0;
for (int num : nums) {
sum += num;
maxNum = Math.max(maxNum, num);
}
if (sum % 2 != 0) {
return false;
}
int target = sum / 2;
if (maxNum > target) {
return false;
}
boolean[][] dp = new boolean[n][target + 1];
for (int i = 0; i < n; i++) {
dp[i][0] = true;
}
dp[0][nums[0]] = true;
for (int i = 1; i < n; i++) {
int num = nums[i];
for (int j = 1; j <= target; j++) {
if (j >= num) {
dp[i][j] = dp[i - 1][j] | dp[i - 1][j - num];
} else {
dp[i][j] = dp[i - 1][j];
}
}
}
return dp[n - 1][target];
}
}
当前路径等于两个方块的路径加和
class Solution {
public int uniquePaths(int m, int n) {
int[][] f = new int[m][n];
for (int i = 0; i < m; ++i) {
f[i][0] = 1;
}
for (int j = 0; j < n; ++j) {
f[0][j] = 1;
}
for (int i = 1; i < m; ++i) {
for (int j = 1; j < n; ++j) {
f[i][j] = f[i - 1][j] + f[i][j - 1];
}
}
return f[m - 1][n - 1];
}
}
周围的最小值加上本身的值
class Solution {
public int minPathSum(int[][] grid) {
for(int i = 0; i < grid.length; i++) {
for(int j = 0; j < grid[0].length; j++) {
if(i == 0 && j == 0) continue;
else if(i == 0) grid[i][j] = grid[i][j - 1] + grid[i][j];
else if(j == 0) grid[i][j] = grid[i - 1][j] + grid[i][j];
else grid[i][j] = Math.min(grid[i - 1][j], grid[i][j - 1]) + grid[i][j];
}
}
return grid[grid.length - 1][grid[0].length - 1];
}
}
字串问题一般以子串长度和起点为状态开始遍历
public class Solution {
public String longestPalindrome(String s) {
int len = s.length();
if (len < 2) {
return s;
}
int maxLen = 1;
int begin = 0;
// dp[i][j] 表示 s[i..j] 是否是回文串
boolean[][] dp = new boolean[len][len];
// 初始化:所有长度为 1 的子串都是回文串
for (int i = 0; i < len; i++) {
dp[i][i] = true;
}
char[] charArray = s.toCharArray();
// 递推开始
// 先枚举子串长度
for (int L = 2; L <= len; L++) {
// 枚举左边界,左边界的上限设置可以宽松一些
for (int i = 0; i < len; i++) {
// 由 L 和 i 可以确定右边界,即 j - i + 1 = L 得
int j = L + i - 1;
// 如果右边界越界,就可以退出当前循环
if (j >= len) {
break;
}
if (charArray[i] != charArray[j]) {
dp[i][j] = false;
} else {
if (j - i < 3) {
dp[i][j] = true;
} else {
dp[i][j] = dp[i + 1][j - 1];
}
}
// 只要 dp[i][L] == true 成立,就表示子串 s[i..L] 是回文,此时记录回文长度和起始位置
if (dp[i][j] && j - i + 1 > maxLen) {
maxLen = j - i + 1;
begin = i;
}
}
}
return s.substring(begin, begin + maxLen);
}
}