1:需要考虑进位的问题,需要使用一个变量存储进位标识,每一次都去判断,而且在一个链表判断完成之后,另外一个链表,也是需要单独考虑进位问题的
2:在计算结束后,需要再次判断进位问题,如果有进位,则需要进行处理。
3:需要维护返回的指针和移动的当前指针,返回的指针确定之后就不再变化,而移动的当前指针会随着操作而变动,最好每次判断返回指针是否为null,这样防止空指针异常。
class Solution {
public ListNode addTwoNumbers(ListNode l1, ListNode l2) {
//进位标识
int flag = 0;
ListNode res = null;
ListNode next = null;
int val1 = 0;
int val2 = 0;
while(l1 != null || l2 != null){
val1 = l1 == null ? 0 : l1.val;
val2 = l2 == null ? 0 : l2.val;
int i = val1 + val2 + flag;
if(l1 != null){
l1 = l1.next;
}
if(l2 != null){
l2 = l2.next;
}
if(i > 9){
flag = i/10;
i = i%10;
}else {
flag = 0;
}
if(res == null){
res = new ListNode(i);
next = res;
}else {
next.next = new ListNode(i);
next = next.next;
}
}
if(flag == 1){
next.next = new ListNode(1);
}
return res;
}
}
1.使用双指针方进行解决,先将一个指针向前移动n位,此时再另两个指针同时移动,当快的那个指针移动到尾节点,那么慢的指针会移动到倒数第n个节点,此时再进行删除
2.编程时,需要注意的是,当快指针移动到尾部,慢指针所在的位置,要保证慢指针再倒数第n + 1个,这样才能使用slow.next = slow.next.next,这一点要特别注意。
class Solution {
public ListNode removeNthFromEnd(ListNode head, int n) {
ListNode fast = head;
ListNode slow = head;
for (int i = 0; i < n; i++) {
fast = fast.next;
}
if (fast == null){
return slow.next;
}
while(fast.next != null){
fast = fast.next;
slow = slow.next;
}
slow.next = slow.next.next;
return head;
}
}
1.需要维护返回的指针和移动的当前指针,返回的指针确定之后就不再变化,而移动的当前指针会随着操作而变动,最好每次判断返回指针是否为null,这样防止空指针异常。
2.在新链表的下一个节点确定后,他的下下一个节点要置为null
2.清楚指针的维护关系
class Solution {
public ListNode mergeTwoLists(ListNode l1, ListNode l2) {
if(l1 == null) return l2;
if(l2 == null) return l1;
ListNode res = null;
ListNode current = null;
while(l1 != null && l2 != null){
if(l1.val > l2.val){
if(res == null){
res = l2;
current = res;
}else{
current.next = l2;
current = current.next;
}
l2 = l2.next;
current.next = null;
}else {
if(res == null){
res = l1;
current = res;
}else{
current.next = l1;
current = current.next;
}
l1 = l1.next;
current.next = null;
}
}
if(l1 != null) current.next = l1;
if(l2 != null) current.next = l2;
return res;
}
}
1.需要使用3个指针进行维护,因为在反转的过程中,是有两条链的
class Solution {
public ListNode reverseList(ListNode head) {
if(head == null || head.next == null) return head;
ListNode current = head;
ListNode next = null;
ListNode before = null;
while(current != null){
next = current.next;
current.next = before;
before = current;
current = next;
}
return before;
}
}
1.需要维护返回的节点,每次判断返回的节点是否为空,为空则进行赋值
2.两两交换,这样的话,指针每次会移动两位,所以要记录 next 和 third指针
3.维护一个current节点的前指针,因为在需要在交换之后,将前指针的下一个指向进行修改
class Solution {
public ListNode swapPairs(ListNode head) {
if(head == null || head.next == null) return head;
ListNode res = null;
ListNode current = head;
ListNode before = null;
ListNode next = null;
ListNode third = null;
//c n t
//1 - 2 - 3 - 4 - 5 - 6
while(current != null && current.next != null){
next = current.next;
third = current.next.next;
current.next = third;
next.next = current;
if(res == null){
res = next;
}else {
before.next = next;
}
before = current;
current = third;
}
return res;
}
}
二刷两两交换链表
class Solution {
public ListNode swapPairs(ListNode head) {
if(head == null || head.next == null){
return head;
}
//结果返回的节点
ListNode res = null;
//前一个节点
ListNode per = null;
//当前节点
ListNode current = null;
ListNode next = null;
ListNode third = null;
// n c t
//每次移动两位 2 -> 1 3 -> 4
while(head != null && head.next != null){
current = head;
next = head.next;
third = head.next.next;
next.next = current;
current.next = null;
//确定返回的最终值
if(res == null){
res = next;
}
//连接到之前的链表中,并维护per指针
if(per != null){
per.next = next;
per = current;
}else{
per = current;
}
head = third;
}
if(head != null){
per.next = head;
}
return res;
}
}
1.使用递归来解决问题,需要记录递归的深度,用于创建数组
2.递归的结束条件为当前所查节点为null,此时需要创建数组,然后在递归返回时,进行赋值
class Solution {
public int[] reversePrint(ListNode head) {
return rec(head, 0);
}
//1 -> 3 -> 2
public int[] rec(ListNode current, int num){
if(current == null){
return new int[num];
}
int[] ints = rec(current.next, ++num);
int length = ints.length;
//num在rec(current.next, ++num)递归时,已经加1了,此时不用再加1了
ints[length - num] = current.val;
return ints;
}
}
1.使用两个链表来进行保存奇偶链表,每次循环向前移动两位指针指向
2.需要记录奇偶链表的头结点和当前访问的节点,当前节点用于拼接奇偶链表的下一个节点,会进行移动
3.在最后,奇链表的尾部链接偶链表的头部即可
class Solution {
public ListNode oddEvenList(ListNode head) {
if(head == null || head.next == null) return head;
ListNode even = null;
ListNode odd = null;
ListNode evenHead = null;
ListNode oddHead = null;
while (head != null){
if(odd == null){
odd = head;
oddHead = head;
}else {
odd.next = head;
odd = odd.next;
}
head = head.next;
//需要将下一节点置为null,防止最后一个不设置而出现循环的情况,而且
//需要在head = head.next之后,不然节点置为null时,下个节点找不到
odd.next = null;
if(head == null){
break ;
}
if(even == null){
even = head;
evenHead = head;
}else {
even.next = head;
even = even.next;
}
head = head.next;
//需要将下一节点置为null,防止最后一个不设置而出现循环的情况,而且
//需要在head = head.next之后,不然节点置为null时,下个节点找不到
even.next = null;
}
odd.next = evenHead;
return oddHead;
}
}
1.双指针法,先让快的节点移动k位,再让两个指针同时移动,当快的到尾部时,慢的就是倒数第k为
class Solution {
// 1->2->3->4->5, 和 k = 2.
public ListNode getKthFromEnd(ListNode head, int k) {
ListNode slow = head;
ListNode fast = head;
for (int i = 0; i < k; i++) {
fast = fast.next;
}
while(fast != null){
fast = fast.next;
slow = slow.next;
}
return slow;
}
}
给你一个整数数组 nums ,找到其中最长严格递增子序列的长度。
子序列是由数组派生而来的序列,删除(或不删除)数组中的元素而不改变其余元素的顺序。例如,[3,6,2,7] 是数组 [0,3,1,6,2,2,7] 的子序列。
示例 1:
输入:nums = [10,9,2,5,3,7,101,18]
输出:4
解释:最长递增子序列是 [2,3,7,101],因此长度为 4 。
题解:
使用动态规划进行处理,最长递增子序列的长度是和前面的子集有关系的,
如求第n个数的最长子序列,那么你就需要求出在 0 - n-1 之间的最长子序列,然后再 + 1,即可求出当前第n个数的最长子序列
在求第n个数的最长子序列, 依次遍历0 - n-1 之间的最长子序列,而且需要是数字小于第n位时,才能生效
public int lengthOfLIS(int[] nums) {
int length = nums.length;
//新建数组,保存第n位他的最长子序列
int[] dp = new int[length];
//存放最终返回的值
int res = 1;
for (int i = 0; i < length; i++) {
if(i == 0){
dp[i] = 1;
continue;
}
//依次从0 - n-1 找到比n小的数的最大的最长子序列
int max = 0;
for(int j = 0; j < i; j++){
if(nums[j] < nums[i] && dp[j] > max){
max = dp[j];
}
}
dp[i] = max + 1;
//进行返回值的更新
if(dp[i] > res){
res = dp[i];
}
}
return res;
}
输入一个字符串,打印出该字符串中字符的所有排列。
你可以以任意顺序返回这个字符串数组,但里面不能有重复元素。
示例:
输入:s = "abc"
输出:["abc","acb","bac","bca","cab","cba"]
题解:
把字符串分为两部分:一部分是字符串的第一个字符,另一部分是第一个字符以后的所有字符。
第一步是求所有可能出现在第一个位置的字符,即把第一个字符和后面所有字符交换。(for循环、交换操作)
第二步是固定住第一个字符,求后面所有字符的排列。(递归)如何求剩余字符的排列:将剩余字符的第一个字符依次和后面的字符进行交换,
而“求后面所有字符的排列”即可按照上面的思路递归进行。
递归出口:求剩余字符的排列时只剩下一个字符
实现借助一个char[],通过交换元素得到不同的排列,在递归返回时将其装入ArrayList。
如下图所示,有两点需要注意:
在每个分支进行完后,要将交换过的元素复位,从而不会影响其他分支。
因为有“Swap A with A”这样的操作,并且题目指出可能有字符重复,所以分支末尾可能有重复的序列,在加入ArrayList要进行去重判断,不重复再加入。
以 abc为例,图解过程如下:
class Solution {
public String[] permutation(String s) {
//1.先转换为数组,然后再使用递归思想,进行求解,每次固定第一个数
if(s == null || "".equals(s)){
return new String[]{};
}
//使用hashset进行存储,判断是否重复,如使用list,则会超时
HashSet set = new HashSet<>();
//当前已遍历的点
addString(s.toCharArray(), 0, set);
return set.toArray(new String[]{});
}
private void addString(char[] chars, int index, HashSet set) {
//如判断到最后,则添加并返回
if(index == chars.length - 1){
String value = String.valueOf(chars);
if(! set.contains(value)){
set.add(value);
}
return;
}
for (int i = index; i < chars.length; i++) {
//交换数组第i个字符和第index个字符
swap(i, index, chars);
addString(chars, index + 1, set);
//再次交换数组第i个字符和第index个字符,保证回到此次for循环前字符数组的状态,不影响字符数组进行下一次for循环
swap(index, i, chars);
}
}
private void swap(int i, int index, char[] chars) {
char temp = chars[i];
chars[i] = chars[index];
chars[index] = temp;
}
}
题目描述:
比较两个版本号 version1 和 version2。 如果 version1 > version2 返回 1,如果 version1 < version2 返回 -1, 除此之外返回 0。
你可以假设版本字符串非空,并且只包含数字和 . 字符。
. 字符不代表小数点,而是用于分隔数字序列。
例如,2.5 不是“两个半”,也不是“差一半到三”,而是第二版中的第五个小版本。
你可以假设版本号的每一级的默认修订版号为 0。例如,版本号 3.4 的第一级(大版本)和第二级(小版本)修订号分别为 3 和 4。其第三级和第四级修订号均为 0。
示例:
示例 1:
输入: version1 = "0.1", version2 = "1.1"
输出: -1
示例 2:
输入: version1 = "1.0.1", version2 = "1"
输出: 1
题解:
先使用逗号进行分割,然后将字符串转为int型进行比较,需要注意一点的是,两个字符串使用逗号分割之后的长度可能不同,这个需要进行一下特殊处理。
class Solution {
public int compareVersion(String version1, String version2) {
String[] split1 = version1.split("\\.");
String[] split2 = version2.split("\\.");
int length1 = split1.length;
int length2 = split2.length;
//找到最大的长度
int maxLength = length1 > length2 ? length1 : length2;
for (int i = 0; i < maxLength; i++) {
String splitSub1 = null;
String splitSub2 = null;
//判断是否达到最大,防止数组越界
if(i < length1){
splitSub1 = split1[i];
}
if(i < length2){
splitSub2 = split2[i];
}
//都在的情况
if(splitSub1 != null && splitSub2 != null){
//都转为int然后再进行比较
Integer integer1 = Integer.valueOf(splitSub1);
Integer integer2 = Integer.valueOf(splitSub2);
if(integer1 > integer2){
return 1;
}else if(integer1 < integer2){
return -1;
}
}else if(splitSub1 == null){
Integer integer2 = Integer.valueOf(splitSub2);
if(integer2 != 0){
return -1;
}
}else {
Integer integer1 = Integer.valueOf(splitSub1);
if(integer1 != 0){
return 1;
}
}
}
return 0;
}
}
题目描述:
给定 n 个非负整数 a1,a2,...,an,每个数代表坐标中的一个点 (i, ai) 。在坐标内画 n 条垂直线,垂直线 i 的两个端点分别为 (i, ai) 和 (i, 0)。找出其中的两条线,使得它们与 x 轴共同构成的容器可以容纳最多的水。
说明:你不能倾斜容器,且 n 的值至少为 2。
图中垂直线代表输入数组 [1,8,6,2,5,4,8,3,7]。在此情况下,容器能够容纳水(表示为蓝色部分)的最大值为 49。
示例:
输入: [1,8,6,2,5,4,8,3,7]
输出: 49
题解:
设立两个指针,一个从头一个从尾,相向而行遍历数组,每次舍弃较短边
(1)计算面积最大值的初值,该初值以数组中的第一个元素和最后一个元素构成两边。
(2)设置首尾两个指针,首指针i指向数组中的第一个元素,尾指针j指向数组中的最后一个元素。
(3)当首指针i小于尾指针j时,一直循环计算其面积。若计算所得的当前面积大于(1)步骤中所计算得的面积最大值,则更新最大值。每一次循环都舍弃索引i和索引j中较短的那一条边。
为什么每一次循环舍弃索引i和索引j中较短的那一条边,我们最终得到的结果就会是最大的面积值呢?
反证法:
假设我们现在遍历到了height数组中第i和第j个元素,且height[i] < height[j],如果我们的面积最大值中取了第i个元素,那么构成我们的面积最大值的两个元素一定是i和j,因为j继续减小的话长方形的宽肯定一直在减小,而其高最多只能是height[i],不可能比height[i]更大,因此我们在继续遍历的过程中,继续保持i不变而减小j是没有意义的。我们可以直接舍弃i,从i + 1开始去继续遍历。
由于整个过程只遍历了一次数组,因此时间复杂度为O(n),其中n为数组height的长度。而使用空间就是几个变量,故空间复杂度是O(1)。
class Solution {
public int maxArea(int[] height) {
if(height == null || height.length == 1){
return 0;
}
int length = height.length;
int right = length-1;
int left = 0;
int with = length-1;
int res = 0;
while (left < right){
int minHeight = height[left] > height[right] ? height[right] : height[left];
int temp = minHeight * with;
if(temp > res){
res = temp;
}
if(height[left] > height[right]){
right--;
}else {
left++;
}
with--;
}
return res;
}
}
给定一个排序数组,你需要在原地删除重复出现的元素,使得每个元素只出现一次,返回移除后数组的新长度。
不要使用额外的数组空间,你必须在原地修改输入数组并在使用 O(1) 额外空间的条件下完成。
示例 1:
给定数组 nums = [1,1,2],
函数应该返回新的长度 2, 并且原数组 nums 的前两个元素被修改为 1, 2。
你不需要考虑数组中超出新长度后面的元素。
示例 2:
给定 nums = [0,0,1,1,1,2,2,3,3,4],
函数应该返回新的长度 5, 并且原数组 nums 的前五个元素被修改为 0, 1, 2, 3, 4。
你不需要考虑数组中超出新长度后面的元素。
说明:
为什么返回数值是整数,但输出的答案是数组呢?
请注意,输入数组是以“引用”方式传递的,这意味着在函数里修改输入数组对于调用者是可见的。
你可以想象内部操作如下:
// nums 是以“引用”方式传递的。也就是说,不对实参做任何拷贝
int len = removeDuplicates(nums);
// 在函数里修改输入数组对于调用者是可见的。
// 根据你的函数返回的长度, 它会打印出数组中该长度范围内的所有元素。
for (int i = 0; i < len; i++) {
print(nums[i]);
}
题解:
官方题解:双指针法
数组完成排序后,我们可以放置两个指针i 和j,其中i 是慢指针,而j 是快指针。只要 nums[i] = nums[j],我们就增加 j 以跳过重复项。
当我们遇到 nums[j] != nums[i],跳过重复项的运行已经结束,因此我们必须把它(nums[j])的值复制到 nums[i + 1]。然后递增 i,接着我们将再次重复相同的过程,直到 j 到达数组的末尾为止。
复杂度分析
时间复杂度:O(n),假设数组的长度是 n,那么 i 和 j 分别最多遍历 n 步。
空间复杂度:O(1)
class Solution {
public int removeDuplicates(int[] nums) {
if(nums == null) return 0;
if(nums.length == 1) return 1;
int res = 0;
for(int i = 1; i
给定一个二叉树,找出其最大深度。
二叉树的深度为根节点到最远叶子节点的最长路径上的节点数。
说明: 叶子节点是指没有子节点的节点。
示例:
给定二叉树 [3,9,20,null,null,15,7],
3
/ \
9 20
/ \
15 7
返回它的最大深度 3 。
Related Topics 树 深度优先搜索 递归
题解:
直接使用递归判断就行,每次判断一个子树的左节点和右节点哪个深度最大,并再次基础上加1即可。
class Solution {
public int maxDepth(TreeNode root) {
if(root == null) return 0;
//进行左右节点的递归工作
int left = maxDepth(root.left, 1);
int right = maxDepth(root.right, 1);
return Math.max(left, right);
}
private int maxDepth(TreeNode root, int depth){
if(root == null){
return depth;
}
return Math.max( maxDepth(root.left, depth + 1), maxDepth(root.right, depth + 1));
}
}
输入某二叉树的前序遍历和中序遍历的结果,请重建该二叉树。假设输入的前序遍历和中序遍历的结果中都不含重复的数字。
例如,给出
前序遍历 preorder = [3,9,20,15,7]
中序遍历 inorder = [9,3,15,20,7]
返回如下的二叉树:
3
/ \
9 20
/ \
15 7
限制:
0 <= 节点个数 <= 5000
题解:
递归思想!
使用前序和中序遍历可以确认唯一的二叉树,通过前序可以得知根节点为1,然后通过中序可以得知左子树为{4, 7, 2},右子树为{5, 3, 8, 6}。接下来通过递归思想,{4, 7, 2}中4为根节点,再继续分左右子树,对{5, 3, 8, 6}也做同样操作,直至遍历结束。
在二叉树的前序遍历序列中,第一个数字总是树的根结点的值。但在中序遍历序列中,根结点的值在序列的中间,左子树的结点的值位于根结点的值的左边,而右子树的结点的值位于根结点的值的右边。因此我们需要扫描中序遍历序列,才能找到根结点的值。
前序遍历序列的第一个数字1就是根结点的值。扫描中序遍历序列,就能确定根结点的值的位置。根据中序遍历特点,在根结点的值1前面的3个数字都是左子树结点的值,位于1后面的数字都是右子树结点的值。
在二叉树的前序遍历和中序遍历的序列中确定根结点的值、左子树结点的值和右子树结点的值的步骤如下图所示:
自己的理解:
1.递归时,要注意,是先构建好子树,然后再构建好外层的树,我开始就是从外部开始构建的,也就是说是先构建好根节点,然后将跟根节点传递给子节点,并且判断当前是左字树,还是右子树,然后再进行其他操作,这样的话,违背了规则的原则,也是一个错误的思想,子树的构建,不应该牵连到父类是什么样子的。
2.在子树递归时,需要时刻注意前根遍历的开始下标和结束下标,这个一定要判断好,而对于中根遍历的的话,因为根在中间,正好可以平均分配,而前根就不一样了,这是能做出提的关键。
class Solution {
public TreeNode buildTree(int[] preorder, int[] inorder) {
if(preorder == null || inorder == null){
return null;
}
return recurseBuildTree(preorder, 0, preorder.length-1, inorder, 0, inorder.length-1);
}
public TreeNode recurseBuildTree(int[] preorder, int perLeft, int perRight, int[] inorder, int inLeft, int inRight){
if(perLeft > perRight || inLeft > inRight){
return null;
}
TreeNode root = new TreeNode(preorder[perLeft]);
for(int i=inLeft; i<=inRight; i++){
if(inorder[i] == root.val){
//(i-inLeft)为左子树的个数,在加上perLeft,那么就是左子树的结束
root.left = recurseBuildTree(preorder, perLeft+1, i-inLeft+perLeft, inorder, inLeft, i-1);
//(i-inLeft)为左子树的个数,在加上perLeft,那么就是左子树的结束,再加1,那么就是右子树的开始
root.right = recurseBuildTree(preorder, i-inLeft+perLeft+1, perRight, inorder, i+1, inRight);
}
}
return root;
}
}
假设你正在爬楼梯。需要 n 阶你才能到达楼顶。
每次你可以爬 1 或 2 个台阶。你有多少种不同的方法可以爬到楼顶呢?
注意:给定 n 是一个正整数。
示例 1:
输入: 2
输出: 2
解释: 有两种方法可以爬到楼顶。
1. 1 阶 + 1 阶
2. 2 阶
示例 2:
输入: 3
输出: 3
Related Topics 动态规划
题解:
使用动态规划,你爬第n阶楼梯时,可以从n-1上来,也可以从n-2阶楼梯上来,所以你上楼梯的方法是在n-1和n-2的总和
class Solution {
public int climbStairs(int n) {
if(n==1) return 1;
if(n==2) return 2;
int[] dp = new int[n];
dp[0] = 1;
dp[1] = 2;
for(int i=2; i
给定一个数组,将数组中的元素向右移动 k 个位置,其中 k 是非负数。
进阶:
尽可能想出更多的解决方案,至少有三种不同的方法可以解决这个问题。
你可以使用空间复杂度为 O(1) 的 原地 算法解决这个问题吗?
示例 1:
输入: nums = [1,2,3,4,5,6,7], k = 3
输出: [5,6,7,1,2,3,4]
解释:
向右旋转 1 步: [7,1,2,3,4,5,6]
向右旋转 2 步: [6,7,1,2,3,4,5]
向右旋转 3 步: [5,6,7,1,2,3,4]
示例 2:
输入:nums = [-1,-100,3,99], k = 2
输出:[3,99,-1,-100]
解释:
向右旋转 1 步: [99,-1,-100,3]
向右旋转 2 步: [3,99,-1,-100]
题解:
解法一:可以增加一个数组保存新移动的数据,这个简单
解法二:先进行整体的反转,然后根据右移数,进行两次局部的反转
class Solution {
public void rotate(int[] nums, int k) {
//先进行整体的反转,再进行局部的反转,
int length = nums.length;
k %= length;
//整体反转 如nums = [1,2,3,4,5,6,7], k = 3
recurseArray(nums, 0, length-1); //[7, 6, 5, 4, 3, 2, 1]
//局部的反转
recurseArray(nums, 0, k-1); //[5, 6, 7, 4, 3, 2, 1]
recurseArray(nums, k, length-1); //[5, 6, 7, 1, 2, 3, 4]
}
private void recurseArray(int[] nums, int i, int j){
//两两比较
if(i>j){
return;
}
while(i
给你一个包含 n 个整数的数组 nums,判断 nums 中是否存在三个元素 a,b,c ,使得 a + b + c = 0 ?请你找出所有和为 0 且不重复的三元组。
注意:答案中不可以包含重复的三元组。
示例 1:
输入:nums = [-1,0,1,2,-1,-4]
输出:[[-1,-1,2],[-1,0,1]]
示例 2:
输入:nums = []
输出:[]
示例 3:
输入:nums = [0]
输出:[]
提示:
0 <= nums.length <= 3000
-105 <= nums[i] <= 105
题解:
可以先对数组进行排序,使用Arrays.sort进行排序,然后数组是从到大进行排序的了,然后使用 a+b = -c,先固定一个数,然后使用双指针法进行数组的查找,写代码时,要注意不能出现重复的数。
需要注意的点:
1.每次都需要进行判断,当前下标值和上一个下标值是否一样,一样则跳过
2.数组只需要遍历到当前下标值>=0就行,如果都大于1,显然不满足条件
class Solution {
public List> threeSum(int[] nums) {
if(nums == null){
return null;
}
Arrays.sort(nums);
List> list = new ArrayList<>();
for(int i=0; i> list, int target, int left, int right, int[] nums){
int pre = nums[left] - 1; //保证第一次pre不能和nums[left]相同,之后会用这个判断是否发生了重复
while(left < right){
if(pre == nums[left]){
left++;
continue;
}
int temp = 0 - (nums[left] + nums[right]);
if(target == temp){
pre = nums[left]; //这个要相同之后,再进行一个判断,防止重复
List subList = new ArrayList<>();
subList.add(nums[left]);
subList.add(nums[right]);
subList.add(target);
list.add(subList);
left++;right--; //这步不能忘
}else if(target > temp){
right--;
}else{
left++;
}
}
}
}
给定一个字符串,请你找出其中不含有重复字符的 最长子串 的长度。
示例 1:
输入: s = "abcabcbb"
输出: 3
解释: 因为无重复字符的最长子串是 "abc",所以其长度为 3。
示例 2:
输入: s = "bbbbb"
输出: 1
解释: 因为无重复字符的最长子串是 "b",所以其长度为 1。
示例 3:
输入: s = "pwwkew"
输出: 3
解释: 因为无重复字符的最长子串是 "wke",所以其长度为 3。
请注意,你的答案必须是 子串 的长度,"pwke" 是一个子序列,不是子串。
示例 4:
输入: s = ""
输出: 0
提示:
0 <= s.length <= 5 * 104
s 由英文字母、数字、符号和空格组成
题解:
使用滑动窗口进行求解,使用map进行存储已经存在的元素,字符 -> 数组下标 的形势进行保存,循环时,每次都判断,当前字符是否出现过,如果出现过,那么就要更新滑动窗口的开始位置,开始位置的判断是本题的解题关键
解题注意点:
1.需要到了HashMap的containsKey方法,这个要会写
2.在map.get(c)时,可能字符串在map中,但是出现的位置在滑动窗口的左边,此时这个值是不能算是在滑动窗口出现过的,这个需要特别注意,比如: kababkc,当判断数组下标为5, 字符'k'时,此时的滑动窗口的开始下标为3, 此时k已经在map中,下标为1 ,所以对于下标开始的判断很关键
class Solution {
public int lengthOfLongestSubstring(String s) {
int n = s.length();
Map map = new HashMap<>();
int res = 0;
int start = 0;
for (int j = 0; j < n; j++) {
//取出该元素值
char c = s.charAt(j);
if(map.containsKey(c)){
//得到字符串的下一个位置,这个位置可能会成功滑动窗口的开始位置
int start2 = map.get(c)+1;
//取出一个最大的当做开始,为什么需要这么做,因为这个map是没有删除的,也就是说map中存放的元素,有可能在
//滑动窗口的左边,这时取得值都已经作废了
//比如: kababkc,当判断数组下标为5, 字符'k'时,此时的滑动窗口的开始下标为3, 此时k已经在map中,下标为1
//所以对于下标开始的判断很关键
start = Math.max(start, start2);
}
map.put(c, j);
//进行值的更新
if(res < (j-start+1)){
res = j-start+1;
}
}
return res;
}
}
给定一个未排序的整数数组 nums
,找出数字连续的最长序列(不要求序列元素在原数组中连续)的长度。
进阶:你可以设计并实现时间复杂度为 O(n)
的解决方案吗?
示例 1:
输入:nums = [100,4,200,1,3,2]
输出:4
解释:最长数字连续序列是 [1, 2, 3, 4]。它的长度为 4。
示例 2:
输入:nums = [0,3,7,2,5,8,4,6,0,1]
输出:9
提示:
0 <= nums.length <= 104
-109 <= nums[i] <= 109
题解:
使用哈希表进行解决,依次遍历整个数组进行查找,而且在查找时,我们只从开始位置进行查询,也就是说,当我们从 2查找时,1不能存在,1存在的话,就会出现重复,导致我们查找的出现重复。
注意点:
1.使用HashSet将整个数组进行保存,去重
2.依次遍历数组的每个元素,如果该元素减1,不存放于数组中,那么它就是一个开始位置,我们从这个依次+1,去HashSet中进行查找。
class Solution {
public int longestConsecutive(int[] nums) {
//先将数都放到HashSet中
HashSet set = new HashSet<>();
for(int i=0;i
给定一个仅包含数字 2-9 的字符串,返回所有它能表示的字母组合。答案可以按 任意顺序 返回。
给出数字到字母的映射如下(与电话按键相同)。注意 1 不对应任何字母。
示例 1:
输入:digits = "23"
输出:["ad","ae","af","bd","be","bf","cd","ce","cf"]
示例 2:
输入:digits = ""
输出:[]
示例 3:
输入:digits = "2"
输出:["a","b","c"]
提示:
0 <= digits.length <= 4
digits[i] 是范围 ['2', '9'] 的一个数字。
题解:
全排列的一般都是使用递归,此时需要注意的是怎么递归,结束条件是什么
注意:
1.对于每个数字对应的字符进行映射,使用Map
2.获取到数组对应的String数组,因为每个数字对应一个数组,所以是String,这里依次从String的第一个开始,然后进行递归
3.使用StringBuilder进行保存,可以进行恢复
class Solution {
public List letterCombinations(String digits) {
if(digits == null || digits.length() == 0){
return new ArrayList<>();
}
Map map = new HashMap<>();
map.put('2',"abc");
map.put('3',"def");
map.put('4',"ghi");
map.put('5',"jkl");
map.put('6',"mno");
map.put('7',"pqrs");
map.put('8',"tuv");
map.put('9',"wxyz");
String[] strArr = new String[digits.length()];
for(int i=0; i list = new ArrayList<>();
//从第0个开始,每次递归调用
recurseCombinations(strArr, 0, list, new StringBuilder());
return list;
}
private void recurseCombinations(String[] strArr, int index, List list, StringBuilder sb){
//当遍历完成后,转为String,并放到集合中
if(strArr.length == index){
list.add(sb.toString());
return;
}
char[] chars = strArr[index].toCharArray();
//依次遍历当前的每一个字符
for(int i=0; i
数字 n 代表生成括号的对数,请你设计一个函数,用于能够生成所有可能的并且 有效的 括号组合。
示例 1:
输入:n = 3
输出:["((()))","(()())","(())()","()(())","()()()"]
示例 2:
输入:n = 1
输出:["()"]
提示:
1 <= n <= 8
题解:
使用递归解决,并且关于字符串使用StringBuilder, 使用两个变量都为n, 一个代表左括号的剩余数量left,一个代表右括号的剩余数量right, 初始都为n, 当左括号、右括号剩余数量都为0
时,进行生成字符串。
递归的返回条件为left<0 || right< 0 || left > right,其中对于left > right表示的是左括号剩余的大于右括号,如())这样的就不满足条件,直接返回。
class Solution {
public List generateParenthesis(int n) {
List list = new ArrayList<>();
StringBuilder sb = new StringBuilder();
recurseGenerate(n, n, sb, list);
return list;
}
private void recurseGenerate(int left, int right, StringBuilder sb, List list){
if(left > right || left < 0 || right < 0){ //left > right,表示如果左括号剩余的多,说明本次是违规的操作,比如))()((,这样的输出不合法
return;
}
if(left == 0 && right == 0){
list.add(sb.toString());
}
recurseGenerate(left-1, right, sb.append("("), list);
sb.deleteCharAt(sb.length()-1);
recurseGenerate(left, right-1, sb.append(")"), list);
sb.deleteCharAt(sb.length()-1);
}
}
给你一个由 '1'(陆地)和 '0'(水)组成的的二维网格,请你计算网格中岛屿的数量。
岛屿总是被水包围,并且每座岛屿只能由水平方向和/或竖直方向上相邻的陆地连接形成。
此外,你可以假设该网格的四条边均被水包围。
示例 1:
输入:grid = [
["1","1","1","1","0"],
["1","1","0","1","0"],
["1","1","0","0","0"],
["0","0","0","0","0"]
]
输出:1
示例 2:
输入:grid = [
["1","1","0","0","0"],
["1","1","0","0","0"],
["0","0","1","0","0"],
["0","0","0","1","1"]
]
输出:3
提示:
m == grid.length
n == grid[i].length
1 <= m, n <= 300
grid[i][j] 的值为 '0' 或 '1'
题解:
使用DFS进行求解,如果当前遍历的坐标符合要求,则将坐标位置置为0
注意点:
1.递归的结束条件
class Solution {
public int numIslands(char[][] grid) {
int n = grid.length;
int m = grid[0].length;
int res = 0;
for(int i=0; i= grid.length || j<0 || j>=grid[0].length || grid[i][j] == '0'){
return;
}
//设置为0
grid[i][j] = '0';
//进行递归
recurseNumIslands(grid, i+1 ,j);
recurseNumIslands(grid, i-1 , j);
recurseNumIslands(grid, i , j+1);
recurseNumIslands(grid, i , j-1);
}
}
给定一棵二叉树,你需要计算它的直径长度。一棵二叉树的直径长度是任意两个结点路径长度中的最大值。这条路径可能穿过也可能不穿过根结点。
示例 :
给定二叉树
1
/ \
2 3
/ \
4 5
返回 3, 它的长度是路径 [4,2,1,3] 或者 [5,2,1,3]。
注意:两结点之间的路径长度是以它们之间边的数目表示。
Related Topics 树
题解:
使用递归 + 动态规划解决问题
递归调用每个节点,而且每个节点保存两个值,一个是当前节点的最大深度, 一个是以当前节点为一个子树的内部的最大深度,
使用数组进行保存,int[2]{maxDiameterWithCurrentRootNode, maxDeep }
当前节点的使用current[],左节点使用leftTemps[],右节点使用RightTemps[],求当前节点时,左右节点的值均已求出
current[0] = max(leftTemps[0], RightTemps[0], leftTemps[1] + RightTemps[1] + 1), 其中leftTemps[1] + RightTemps[1] + 1为已当前节点为根的最大直径
current[1] = max(leftTemps[1] , RightTemps[1]) + 1
class Solution {
public int diameterOfBinaryTree(TreeNode root) {
//使用递归解决问题,每次保存一个数组,第一个下标为以当前节点为根的最大直径,
//第二个就是以当前节点的的最大深度
//使用动态规划 + 递归运算
if(root == null){
return 0;
}
int[] temps = recurseDiameter(root);
return Math.max(temps[0], temps[1]) - 1;
}
private int[] recurseDiameter(TreeNode root){
if(root == null){
return new int[]{0,0};
}
//leftTemps[0] 代表以root.left为根的最大直径, leftTemps[1]代表以root.left中的最大深度
int[] leftTemps = recurseDiameter(root.left);
int[] rightTemps = recurseDiameter(root.right);
//当前节点的最大深度
int maxdeep = Math.max(leftTemps[1], rightTemps[1]) + 1;
//以当前节点为根的最大直径, 它的取值为Math(左节点为根的最大直径, 有节点为根的最大直径, 当前节点为根的最大直径),取最大的一个
int max = Math.max(leftTemps[0], rightTemps[0]);
max = Math.max(max, leftTemps[1] + rightTemps[1] + 1);
return new int[]{max, maxdeep};
}
}
运用你所掌握的数据结构,设计和实现一个 LRU (最近最少使用) 缓存机制 。
实现 LRUCache 类:
LRUCache(int capacity) 以正整数作为容量 capacity 初始化 LRU 缓存
int get(int key) 如果关键字 key 存在于缓存中,则返回关键字的值,否则返回 -1 。
void put(int key, int value) 如果关键字已经存在,则变更其数据值;如果关键字不存在,则插入该组「关键字-值」。当缓存容量达到上
限时,它应该在写入新数据之前删除最久未使用的数据值,从而为新的数据值留出空间。
进阶:你是否可以在 O(1) 时间复杂度内完成这两种操作?
示例:
输入
["LRUCache", "put", "put", "get", "put", "get", "put", "get", "get", "get"]
[[2], [1, 1], [2, 2], [1], [3, 3], [2], [4, 4], [1], [3], [4]]
输出
[null, null, null, 1, null, -1, null, -1, 3, 4]
解释
LRUCache lRUCache = new LRUCache(2);
lRUCache.put(1, 1); 缓存是 {1=1}
lRUCache.put(2, 2); 缓存是 {1=1, 2=2}
lRUCache.get(1); 返回 1
lRUCache.put(3, 3); 该操作会使得关键字 2 作废,缓存是 {1=1, 3=3}
lRUCache.get(2); 返回 -1 (未找到)
lRUCache.put(4, 4); 该操作会使得关键字 1 作废,缓存是 {4=4, 3=3}
lRUCache.get(1); 返回 -1 (未找到)
lRUCache.get(3); 返回 3
lRUCache.get(4); 返回 4
提示:
1 <= capacity <= 3000
0 <= key <= 3000
0 <= value <= 104
最多调用 3 * 104 次 get 和 put
题解:
使用LinkedHashMap可以很轻松的实现,但是你这样的话,没有起到自己设计的目的,只是自己调用了API,所以,这里使用HashMap进行设计。
注意点:
1.LRU需要使用链表结构进行保存,就像LinkedHashMap一样,构造虚拟节点内部类,保存到HashMap的value中
2.HashMap中value保存的是虚拟的链表节点,如果value直接存放值的话,工作量太大
3.构建出虚拟的头结点和尾节点,只是用来指明头尾的位置,这样的不用考虑空指针异常的问题,这个能想到的话,做题很快的
4.明白LUR的含义,在get时,将访问的当前节点移动到头部去,当put时,先查看是否存在,如不存在,则创建虚拟节点进行包装后,放入,再判断是否超过最大容量,超过时,则需要删除。
class LRUCache {
class CacheLinked{
int key;
int val;
CacheLinked next;
CacheLinked pre;
public CacheLinked(){}
public CacheLinked(int key, int val){
this.key = key;
this.val = val;
}
}
//size存放当前元素数, capacity存放最大元素数
private int size;
private int capacity;
private CacheLinked head;
private CacheLinked tail;
Map map = new HashMap<>();
public LRUCache(int capacity) {
this.capacity = capacity;
//构建头尾的虚拟节点,不存放任何元素,只是为了操作简单,不用进行空指针的判断,如果不构建
//虚拟节点有你受的了,光空指针的判断就很麻烦
head = new CacheLinked();
tail = new CacheLinked();
head.next = tail;
tail.pre = head;
}
public int get(int key) {
CacheLinked cache = map.get(key);
if(cache == null){
return -1;
}
//更新lru,移除元素,并添加到首部
moveCache(cache);
addHead(cache);
return cache.val;
}
public void put(int key, int value) {
CacheLinked cache = map.get(key);
//分两种情况,缓存已存在,和缓存不存在,
if(cache == null){
CacheLinked newCache = new CacheLinked(key, value);
addHead(newCache);
//判断容量大小,如果超过了,那么就将最就的那个从hashmap中删除
size++;
if(size > capacity){
CacheLinked oldTail = moveTail();
//移除元素
map.remove(oldTail.key);
}
map.put(key, newCache);
}else{
//更新值
cache.val = value;
//更新lru,移除元素,并添加到首部
moveCache(cache);
addHead(cache);
}
}
private void addHead(CacheLinked cache){
CacheLinked oldHead = head.next;
head.next = cache;
cache.next = oldHead;
oldHead.pre = cache;
cache.pre = head;
}
private void moveCache(CacheLinked cache){
//因为有虚拟节点的存在,根本不用判断空指针的问题
cache.pre.next = cache.next;
cache.next.pre = cache.pre;
//释放元素,防止内存遗漏
cache.next = null;
cache.pre = null;
}
//移除尾部元素,并将尾部元素进行返回
private CacheLinked moveTail(){
CacheLinked oldTail = tail.pre;
CacheLinked newTail = oldTail.pre;
newTail.next = tail;
tail.pre = newTail;
oldTail.next = null;
oldTail.pre = null;
return oldTail;
}
}
在上次打劫完一条街道之后和一圈房屋后,小偷又发现了一个新的可行窃的地区。这个地区只有一个入口,我们称之为“根”。 除了“根”之外,每栋房子有且只有一个“父“
房子与之相连。一番侦察之后,聪明的小偷意识到“这个地方的所有房屋的排列类似于一棵二叉树”。 如果两个直接相连的房子在同一天晚上被打劫,房屋将自动报警。
计算在不触动警报的情况下,小偷一晚能够盗取的最高金额。
示例 1:
输入: [3,2,3,null,3,null,1]
3
/ \
2 3
\ \
3 1
输出: 7
解释: 小偷一晚能够盗取的最高金额 = 3 + 3 + 1 = 7.
示例 2:
输入: [3,4,5,1,3,null,1]
3
/ \
4 5
/ \ \
1 3 1
输出: 9
解释: 小偷一晚能够盗取的最高金额 = 4 + 5 = 9.
Related Topics 树 深度优先搜索
题解:
使用递归 + 动态规划进行求解,因为每个节点都可以选也可以不选,如果当前节点选择的话,他的两个子节点都不能选择了,所以,我们可以使用数组保存当前节点的两个状态,即[0]不选当前节点的最大值, [1]选择了当前节点的最大值
注意:
1.对于选择当前节点还好,直接左右节点不选 + 当前节点的value
而对于不选当前节点,那么左节点和右节点都可以选或者是不选,所以,我们要查出最大的左节点和最大的右节点,如下面这样情况
3
/ \
4 5
/ \ \
1 10000 1
当遍历到根节点3时,我们看根据是不选4这个节点,他的整体会更大一点
class Solution {
public int rob(TreeNode root) {
if(root == null){
return 0;
}
int[] res = recurseReachMaxRob(root);
return Math.max(res[0], res[1]);
}
public int[] recurseReachMaxRob(TreeNode root) {
if(root == null){
return new int[]{0, 0};
}
//[0]不选当前节点的最大值, [1]选择了当前节点的最大值
int[] leftMaxJob = recurseReachMaxRob(root.left);
int[] rightMaxJob = recurseReachMaxRob(root.right);
//选择了当前节点,那么它的左右孩子都不能选
int selectCurrent = leftMaxJob[0] + rightMaxJob[0] + root.val;
//不选择当前节点的话,那么左右节点都可以选,也都可以不选,这样要看做大值
//这个是解题的关键,当前节点不选的,他的子节点可以选,也可以不选,这是,就要取最大的一个,
// 3
// / \
// 4 5
// / \ \
// 1 10000 1
//当遍历到根节点3时,我们看根据是不选4这个节点,他的整体会更大一点
int noSelectCurrent = Math.max(leftMaxJob[0], leftMaxJob[1]) + Math.max(rightMaxJob[0], rightMaxJob[1]);
return new int[]{noSelectCurrent, selectCurrent};
}
}
给定一个二叉树,检查它是否是镜像对称的。
例如,二叉树 [1,2,2,3,4,4,3] 是对称的。
1
/ \
2 2
/ \ / \
3 4 4 3
但是下面这个 [1,2,2,null,3,null,3] 则不是镜像对称的:
1
/ \
2 2
\ \
3 3
进阶:
你可以运用递归和迭代两种方法解决这个问题吗?
Related Topics 树 深度优先搜索 广度优先搜索
题解:
首先分析下这个对称二叉树,也就是一个二叉树中间对称。所以我们可以使用递归的思想,首先以根节点以及其左右子树,左子树的左子树和右子树的右子树相同,左子树的右子树和右子树的左子树相同。两个条件都要符合,所以我们第一个传根节点的左子树和右子树,先判断左右子树根结点的比较。然后分辨对左子树的左子树和右子树的右子树。左子树的右子树和右子树的左子树进行判断。只有两个条件都满足则返回的是true,一层一层递归进入,则可以得到结果。
注意:
1.主要递归进行判断,如果值不相同的话,那么也不是一个镜像二叉树
2.只要有一个返回为false,那么整个结果就会返回false
class Solution {
public boolean isSymmetric(TreeNode root) {
if(root == null){
return false;
}
return recurseJudgeSymmetric(root.left, root.right);
}
private boolean recurseJudgeSymmetric(TreeNode left, TreeNode right){
if(left == null){
//left为null时,判断right是否为null
//right == null,成立返回true
return right == null;
}
//此时走到这,说明left不为空了,
if(right == null){
return false;
}
//值不相等,则也返回false
if(left.val != right.val){
return false;
}
boolean symmetric1 = recurseJudgeSymmetric(left.left, right.right);
boolean symmetric2 = recurseJudgeSymmetric(left.right, right.left);
//返回他们之间的 && 运算,只要有一个为false那么就为false
return symmetric1 && symmetric2;
}
}
给定一个二叉树,判断其是否是一个有效的二叉搜索树。
假设一个二叉搜索树具有如下特征:
节点的左子树只包含小于当前节点的数。
节点的右子树只包含大于当前节点的数。
所有左子树和右子树自身必须也是二叉搜索树。
示例 1:
输入:
2
/ \
1 3
输出: true
示例 2:
输入:
5
/ \
1 4
/ \
3 6
输出: false
解释: 输入为: [5,1,4,null,null,3,6]。
根节点的值为 5 ,但是其右子节点值为 4 。
题解:
先使用中序遍历,二叉搜索树的话,对于中序遍历来说,整个数组是有序的,所以可以依次判断数组的后一个是否比前一个数组大,如果小的话,则不满足。
class Solution {
public boolean isValidBST(TreeNode root) {
if(root == null){
return false;
}
//中序遍历,肯定是递增的
List list = new ArrayList<>();
//先进行中序遍历
recurseValidBST(root, list);
//如果是二叉搜索树的话,那么数据是有序的,进行遍历就行
for(int i=1; i= list.get(i)){
return false;
}
}
return true;
}
private void recurseValidBST(TreeNode root, List list){
if(root == null){
return;
}
recurseValidBST(root.left, list);
list.add(root.val);
recurseValidBST(root.right, list);
}
}
给定一个包含非负整数的 m x n 网格 grid ,请找出一条从左上角到右下角的路径,使得路径上的数字总和为最小。
说明:每次只能向下或者向右移动一步。
示例 1:
输入:grid = [[1,3,1]
,[1,5,1],
[4,2,1]]
输出:7
解释:因为路径 1→3→1→1→1 的总和最小。
示例 2:
输入:grid = [[1,2,3],[4,5,6]]
输出:12
提示:
递归的话,结束条件时什么
到达右下角,则结束, 结束之后返回值,因为只能向左或者是向右,则进行比较,左右做小值
m == grid.length
n == grid[i].length
1 <= m, n <= 200
0 <= grid[i][j] <= 100
Related Topics 数组 动态规划
题解:
两种解法:
一种是递归,会超时.....................,每次递归判断向左向右的最小值,并进行求值
二种是动态规划,第dp[i][j]代表走到i,j所走的最小路径,那么这个最小路径和和dp[i-1][j],dp[i][j-1]有关系的,
具体的最小值为dp[i][j] = Math.max(dp[i-1][j], dp[i][j-1]) + grid[i][j];
注意:
对于临界点的判断,因为i=0的话,你再i-1就会出错了,这个要特别注意
class Solution {
public int minPathSum(int[][] grid) {
if(grid == null){
return 0;
}
int n = grid.length;
int m = grid[0].length;
int[][] dp = new int[n][m];
for(int i=0; i
设计一个支持 push ,pop ,top 操作,并能在常数时间内检索到最小元素的栈。
push(x) —— 将元素 x 推入栈中。
pop() —— 删除栈顶的元素。
top() —— 获取栈顶元素。
getMin() —— 检索栈中的最小元素。
示例:
输入:
["MinStack","push","push","push","getMin","pop","top","getMin"]
[[], [-2], [0], [-3], [], [], [], []]
输出:
[null, null, null, null, -3, null, 0, -2]
解释:
MinStack minStack = new MinStack();
minStack.push(-2);
minStack.push(0);
minStack.push(-3);
minStack.getMin(); --> 返回 -3.
minStack.pop();
minStack.top(); --> 返回 0.
minStack.getMin(); --> 返回 -2.
提示:
pop、top 和 getMin 操作总是在 非空栈 上调用。
Related Topics 栈 设计
题解:
使用额外的一个栈保存当前最小的数,每当一个元素进栈时,都会在两个栈中各存放一个元素,最小栈中存放的是和之前的进行比较,看哪个小,就将小的放入栈中,具体官方题解有动态图,这个看一下:https://leetcode-cn.com/problems/min-stack/solution/zui-xiao-zhan-by-leetcode-solution/
class MinStack {
private LinkedList stack;
private LinkedList minStack;
/** initialize your data structure here. */
public MinStack() {
stack = new LinkedList<>();
minStack = new LinkedList<>();
}
public void push(int x) {
//如果为空,则直接存放进去
if(stack.size() == 0){
minStack.addFirst(x);
}else{
//不为空,取出最小栈中的头部,并进行判断大小,而且每次都会将比较
//之后最小的,放入到最小栈中
int temp = minStack.getFirst();
if(temp > x){
minStack.addFirst(x);
}else{
minStack.addFirst(temp);
}
}
stack.addFirst(x);
}
public void pop() {
stack.removeFirst();
minStack.removeFirst();
}
public int top() {
return stack.getFirst();
}
public int getMin() {
return minStack.getFirst();
}
}
给你一个字符串 s,找到 s 中最长的回文子串。
示例 1:
输入:s = "babad"
输出:"bab"
解释:"aba" 同样是符合题意的答案。
示例 2:
输入:s = "cbbd"
输出:"bb"
示例 3:
输入:s = "a"
输出:"a"
示例 4:
输入:s = "ac"
输出:"a"
提示:
1 <= s.length <= 1000
s 仅由数字和英文字母(大写和/或小写)组成
先给出官方题解,然后写一下自己的理解,需要注意和深刻理解的点:
1.求dp[i][j]是否是回文串时,需要dp[i+1][j-1]的状态已知,而对于dp[i+1][j-1]的状态也是需要它上面的状态才行,所以,在进行求值时,我们需要先根据内部的才能求出外部的,在
求解时,需要根据长度进行求取,先求出所有的回文子串长度为1的,然后再求出长度为2的,再求为3的
2.所有循环时,不是对i和j进行循环,外层对长度进行循环,内层对开始位置i进行循环,根据i和长度来确定j的位置,这样进行求解
class Solution {
public String longestPalindrome(String s) {
String res = "";
int length = s.length();
boolean[][] dp = new boolean[length][length];
//len代表当前所遍历的长度,因为只有dp[i][j]判断的条件是,dp[i+1][j-1]的状态已知,而对于dp[i+1][j-1]的
//状态怎么得知的,所以,在进行求值时,是根据长度进行的,先求出长度为3的,上面的那个循环已经求得了长度为1和2是不是
//回文串了,那就此时就从3开始判断了
for(int len=1; len<=length; len++){
//我们判断时,是从i开始的,如果i+len如果大于length,则说明本次已不满足条件了,因为,我们遍历时,是先根据长度进行循环的,
for(int i=0; i+len<=length; i++){
//i+len-1,表示当前遍历的j在哪个位置,因为,本次判断的长度为len,我们开始位置为i,那么j的位置就是i+len了,因为下标
//从0开始,则需要-1,就找到了j的位置
//始终记住dp[i][j],表示的是从第i为到第j为是不是一个回文串,他们之间的间隔为len
int j = i + len - 1;
if(len == 1){
dp[i][j] = true;
}else if(len == 2){
dp[i][j] = (s.charAt(i) == s.charAt(j));
}else if(s.charAt(i)==s.charAt(j) && dp[i+1][j-1]){
dp[i][j] = true;
}
//满足条件,则查看是否需要更新最大值
if(dp[i][j] && res.length()
给定一个字符串,你的任务是计算这个字符串中有多少个回文子串。
具有不同开始位置或结束位置的子串,即使是由相同的字符组成,也会被视作不同的子串。
示例 1:
输入:"abc"
输出:3
解释:三个回文子串: "a", "b", "c"
示例 2:
输入:"aaa"
输出:6
解释:6个回文子串: "a", "a", "a", "aa", "aa", "aaa"
提示:
输入的字符串长度不会超过 1000 。
Related Topics 字符串 动态规划
题解:
需要使用动态规划,思路和leetcode第5题思路是一样的,主要的还是对于动态转移方式的判断,和条件
需要注意的点:
1.dp[i][j]表示从数组下标i到数组下标j是否是一个回文串,它的转移方程为dp[i][j] = (arr[i] == arr[j] && dp[i+1][j-1] == true)
2.求解时,根据长度进行求解,让回文字串不断增大,先从长度为1 到长度为length进行判断
class Solution {
public int countSubstrings(String s) {
if(s == null || s.length() == 0){
return 0;
}
int length = s.length();
int res = 0;
boolean[][] dp = new boolean[length][length];
//len表示当前的长度,先从1开始判断,也就是判断abc中的a是不是回文串
//等长度为1的判断完成后,判断2的,如ab,bc是不是,再判断长度为3的
//如abc是不是
for(int len=1; len<=length; len++){
for(int i=0; i+len<=length; i++){
//i+len-1,表示结束位置j
int j = i + len -1;
if(len == 1){
dp[i][j] = true;
}else if(len == 2){
dp[i][j] = s.charAt(i) == s.charAt(j);
}else if(s.charAt(i) == s.charAt(j) && dp[i+1][j-1]){
dp[i][j] = true;
}
if(dp[i][j]){
res++;
}
}
}
return res;
}
}
整数数组 nums 按升序排列,数组中的值 互不相同 。
在传递给函数之前,nums 在预先未知的某个下标 k(0 <= k < nums.length)上进行了 旋转,使数组变为 [nums[k], nums[
k+1], ..., nums[n-1], nums[0], nums[1], ..., nums[k-1]](下标 从 0 开始 计数)。例如, [0,1,2
,4,5,6,7] 在下标 3 处经旋转后可能变为 [4,5,6,7,0,1,2] 。
给你 旋转后 的数组 nums 和一个整数 target ,如果 nums 中存在这个目标值 target ,则返回它的索引,否则返回 -1 。
示例 1:
输入:nums = [4,5,6,7,0,1,2], target = 0
输出:4
示例 2:
输入:nums = [4,5,6,7,0,1,2], target = 3
输出:-1
示例 3:
输入:nums = [1], target = 0
输出:-1
提示:
1 <= nums.length <= 5000
-10^4 <= nums[i] <= 10^4
nums 中的每个值都 独一无二
nums 肯定会在某个点上旋转
-10^4 <= target <= 10^4
进阶:你可以设计一个时间复杂度为 O(log n) 的解决方案吗?
Related Topics 数组 二分查找
题解:
使用二分进行查询,当将数组一分为二时,肯定有一份是从小到大排序的,这个是可以肯定的,因为此题有个前提-------数组中的值互不相同,所以,我们解题的关键是找到哪一半是升序的,然后依次判断这个target是否在这个升序排列中,如果不存在,那么就在另一份了,所以此题也是二分的思想
class Solution {
public int search(int[] nums, int target) {
if(nums == null || nums.length == 0){
return -1;
}
if(nums.length == 1){
return nums[0] == target ? 0 : -1;
}
int left = 0;
int right = nums.length-1;
int mid = 0;
while(left <= right){
mid = (left + right)/2;
if(nums[mid] == target){
return mid;
}
//从有序的一方进行判断
if(nums[left] <= nums[mid] ){
//左边有序
if(nums[left] <= target && nums[mid] > target){
right = mid - 1;
}else{
left = mid + 1;
}
}else{
if(nums[mid] < target && nums[right] >= target){
left = mid + 1;
}else{
right = mid - 1;
}
}
}
return -1;
}
}
以数组 intervals 表示若干个区间的集合,其中单个区间为 intervals[i] = [starti, endi] 。请你合并所有重叠的区间,并返
回一个不重叠的区间数组,该数组需恰好覆盖输入中的所有区间。
示例 1:
输入:intervals = [[1,3],[2,6],[8,10],[15,18]]
输出:[[1,6],[8,10],[15,18]]
解释:区间 [1,3] 和 [2,6] 重叠, 将它们合并为 [1,6].
示例 2:
输入:intervals = [[1,4],[4,5]]
输出:[[1,5]]
解释:区间 [1,4] 和 [4,5] 可被视为重叠区间。
提示:
1 <= intervals.length <= 104
intervals[i].length == 2
0 <= starti <= endi <= 104
Related Topics 排序 数组
题解:
进行合并的区域,需要先进行排序,因为,可能给的数组不是从小到达排序的,排序时,按照开始节点进行排序,例如给的用例为[[2,3],[4,5],[6,7],[8,9],[1,10]],此时就需要先进行排序了,再排序之后我们就可以发现规律了,如
[[1,10],[2,3],[4,5],[6,7],[8,9]],此时如果需要进行排序,那么肯定是当前遍历元素的开始比前一个元素的结束要小,此时就要进行合并,而且,再查找一个最大值,作为结束。
class Solution {
public int[][] merge(int[][] intervals) {
if(intervals == null || intervals.length == 1){
return intervals;
}
ArrayList list = new ArrayList<>();
//从小到大进行排序,按照起始节点进行排序
Arrays.sort(intervals, (a,b) -> a[0] - b[0]);
list.add(intervals[0]);
for(int i=1; i
编写一个程序,找到两个单链表相交的起始节点。
例如,下面的两个链表:
A: a1 → a2
↘
c1 → c2 → c3
↗
B: b1 → b2 → b3
在节点 c1 开始相交。
注意:
如果两个链表没有交点,返回 null.
在返回结果后,两个链表仍须保持原有的结构。
可假定整个链表结构中没有循环。
程序尽量满足 O(n) 时间复杂度,且仅用 O(1) 内存。
思路:如果没有 O(n) 时间复杂度,且仅用 O(1) 内存的条件,这个题很容器写,但是因为对于时间复杂度和空间复杂度有要求,所以,我们应该再做思考,我们知道,如果两个链表相交,从相交点开始,以后的元素都是相同的,我们设A链表中,不重复的为listA,设B链表中,不重复的为listB,相同的那部分为AequalB。
那么就有listA+AequalB+listB=listB+AequalB+listA,当然这也是显然易见的,所以我们就可以找到交点了,
例如,对于A链表,我们a1 → a2 → c1→c2 → c3→b1 → b2 → b3→c1
对于B链表,我们b1 → b2 → b3→c1 → c2 → c3→a1 → a2→c1
可以发现,我们从A链表开始,当A链表遍历完后,再从B链表头节点找, 对于B,我们从B链表开始,当B链表遍历完后,再从A链表头节点找,这样会在相交的地方,两值相等。
注意:
分为两种情况,如果A和B链一样长,那么第一次遍历时,就直接a == b == null了,如果A和B链不一样长,那么第一次遍历时,就直接a == null时,b一定不是null,那么就需要进行a = headB; b = headA的判断
public class Solution {
public ListNode getIntersectionNode(ListNode headA, ListNode headB) {
if(headA==null || headB==null){
return null;
}
//分为两种情况: 如果A和B链一样长,那么第一次遍历时,就直接a == b == null了
// 如果A和B链不一样长,那么第一次遍历时,就直接a == null时,b一定不是null,那么就需要进行a = headB; b = headA的判断
//a -> b
//c -> d, 如果A和B链一样长,那么第一次遍历时,就直接a == b == null了,所以直接退出,如果A,B链不想等,那么的话,才需要进行二次遍历
//如a -> b
//c -> d -> e, 只有A,B链不同时为null时,才会走扫描第二次,也就是a = headB; b = headA;才有用
ListNode a = headA;
ListNode b = headB;
while(a != b){
if(a == null){
a = headB;
}else{
a = a.next;
}
if(b == null){
b = headA;
}else{
b = b.next;
}
}
return a;
}
}
实现获取 下一个排列 的函数,算法需要将给定数字序列重新排列成字典序中下一个更大的排列。
如果不存在下一个更大的排列,则将数字重新排列成最小的排列(即升序排列)。
必须 原地 修改,只允许使用额外常数空间。
示例 1:
输入:nums = [1,2,3]
输出:[1,3,2]
示例 2:
输入:nums = [3,2,1]
输出:[1,2,3]
示例 3:
输入:nums = [1,1,5]
输出:[1,5,1]
示例 4:
输入:nums = [1]
输出:[1]
提示:
1 <= nums.length <= 100
0 <= nums[i] <= 100
Related Topics 数组
题解:
首先你要知道什么是字典树,其次你要很清楚怎么根据一个字典树,找到他的下一个排列的字典树。这两点清楚的话,这题并不难,主要的话,还是要理解字典树,详细的可以参考这篇:https://blog.csdn.net/qq_33594380/article/details/82377923。
1.字典序值
字典序值就是当前序列在字典序中的排列位置。那给定一个排列,我们应该如何求出它的字典序值呢?
为了求排列对应的序号,只要该序列到第一个序列即1,2,3…n 所需要移动的次数。
移动原则是a[i] 从大到小移动,对于每一个数字a[i],若i前面比a[i] 小的个数正好是a[i] - 1 个,则这个数不需要向后移动以达到目标序列,否则i 后面必然有a[i] - t - 1 (此处t 的值为a[i] 之前比a[i] 小的数字的个数)个比a[i] 小的数,只要把a[i] 移动到比自己小的数后面才能使得移动中的序列正向目标前进。
因此只要求出每个数的移动次数,然后相加就是该序列的位置。即每个数到正确位置需要移动的次数为
—— 这一段引自 https://blog.csdn.net/hello_tomorrow_111/article/details/78696294 并略作优化
啥意思,有点模糊,举个栗子来解释一下。我们就以第一点中提到的[1, 2, 3] 的字典序为例,我现在想知道321 在序列中的位置应该怎么计算呢?
(1)找到该序列的第一个序列,即123
(2)从序列左边开始,查找每个值应该移动的位数。
首先来看3,与第一个序列相比,3 之前本来应该有两个元素12,但是现在它前面没有比它小的元素,所以它要移动3 - 1 - 0 (a[i] - t - 1),即两位到达正序位置。
接着来看2,2 之前应该是有一个元素1,但是它前面也没有比它小的元素,所以它要向后移动2 - 1 - 0,即一位来达到正序。
对于1 来说,它是排列中最小的元素,它之前不应该有元素,而事实也是如此,所以它不需要移动。
将3 和2 移动的次数相加就是321 在字典序中的字典序值。那么3 和2 分别需要移动多少次才能移动两位或者一位呢,以3 为例来看一下:
321 中的3 往后移动两位需要经历以下流程(回退):231、213、132、123。
2. 下一个序列
字典序值说完了,说说下一个序列,下一个序列求解的思想非常简单:
(1)从右向左找到第一个左临小于右临的位置,左临的位置标记为i
(2)从右向左找到第一个大于a[i] 的数值,位置为j
(3)交换a[i] 与a[j] 的值
(4)将i 右边的部分排成正序
如果看这个流程看不懂的话,建议去看Leetcode 上的动图,保证看几遍就懂:
https://leetcode-cn.com/problems/next-permutation/solution/
Say sth more (简称SM): 为什么要这么做呢,提供一个栗子结合理解,假设我们要求15499 + 1 的和,这个应该怎么算呢?
解法是:从右向左找到第一个不为9 的数值,将该数值加1,然后该数值以后所有的9 都变成0。此例可以与求解下一个序列的过程结合理解。
class Solution {
public void nextPermutation(int[] nums) {
if(nums == null || nums.length == 1){
return;
}
int length = nums.length;
int ans = -1;
//1 2 3 8 7 6 5 4
// i-1 i
//1, 2, 3
// ans
//第一步.从后向前找第一个,a[i] > a[i-1]的数
for(int i=length-1; i>0; i--){
if(nums[i] > nums[i-1]){
ans = i-1;
//break语句不能忘记,不然就错了......
break;
}
}
if(ans == -1){
//此时不存在下一个更大的排列,则将数字重新排列成最小的排列
reverseArray(nums, 0, length-1);
}else{
for(int i=length-1; i>=ans; i--){
if(nums[i] > nums[ans]){
//第二步,元素的交换
swapNum(nums, i, ans);
//第三步,指定的数据反转
reverseArray(nums, ans+1, length-1);
break;
}
}
}
}
//交换元素
private void swapNum(int[] nums, int i, int j){
int temp = nums[i];
nums[i] = nums[j];
nums[j] = temp;
}
//反转指定下标范围内的元素
private void reverseArray(int[] nums, int l, int r){
while(l