1.合并两个有序链表
2.移除元素
3.路径总和
4.实现strStr()
5.对称二叉树
6.二叉树的中序遍历
7.相交链表
8.有效的字母异位词
9.只出现一次的数字
10.搜索插入位置
11.两数相加
12.搜索旋转排序数组
总结
链表、二叉树、迭代、递归、双指针、字符串单模匹配、二分查找
难度:★ 链接:力扣
将两个升序链表合并为一个新的 升序 链表并返回。新链表是通过拼接给定的两个链表的所有节点组成的。
示例1:
输入:l1 = [1,2,4], l2 = [1,3,4] 输出:[1,1,2,3,4,4]
示例2:
输入:l1 = [], l2 = [] 输出:[]
示例3:
输入:l1 = [], l2 = [0] 输出:[0]
提示:
- 两个链表的节点数目范围是
[0, 50]
-100 <= Node.val <= 100
l1
和l2
均按 非递减顺序 排列
解题思路:
方法一:迭代法
设置一个哨兵节点prehead用于返回最后合并的链表,再设置一个移动指针prev来指向两个链表中每次遍历时值较小的那个节点。因为两个链表是有序的,当循环结束时,至少有一个链表已经全部遍历完成了,所以只需将不为空的那个链表全部加到已经合并链表的末尾即可。
方法二:递归法
如果 l1 或者 l2 一开始就是空链表 ,那么没有任何操作需要合并,所以我们只需要返回非空链表。否则,我们要判断 l1 和 l2 哪一个链表的头节点的值更小,然后递归地决定下一个添加到结果里的节点。如果两个链表有一个为空,递归结束。
解题代码:
方法一:
/**
* 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 prehead=new ListNode(-1);
ListNode prev=prehead;
// 开始循环遍历,将移动指针prev的指针域指向两个链表中较小的一个结点
while(list1!=null&&list2!=null){
if(list1.val
方法二:
/**
* 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 l1, ListNode l2) {
if (l1 == null) {
return l2;
} else if (l2 == null) {
return l1;
}
//将较小的节点递归地添加到结果链表中
else if (l1.val < l2.val) {
l1.next = mergeTwoLists(l1.next, l2);
return l1;
} else {
l2.next = mergeTwoLists(l1, l2.next);
return l2;
}
}
}
难度:★ 链接:力扣
给你一个数组 nums 和一个值 val,你需要 原地 移除所有数值等于 val 的元素,并返回移除后数组的新长度。不要使用额外的数组空间,你必须仅使用 O(1) 额外空间并 原地 修改输入数组。元素的顺序可以改变。你不需要考虑数组中超出新长度后面的元素。
说明:
为什么返回数值是整数,但输出的答案是数组呢?请注意,输入数组是以「引用」方式传递的,这意味着在函数里修改输入数组对于调用者是可见的。你可以想象内部操作如下:
// nums 是以“引用”方式传递的。也就是说,不对实参作任何拷贝
int len = removeElement(nums, val);// 在函数里修改输入数组对于调用者是可见的。
// 根据你的函数返回的长度, 它会打印出数组中 该长度范围内 的所有元素。
for (int i = 0; i < len; i++) {
print(nums[i]);
}
示例1:
输入:nums = [3,2,2,3], val = 3
输出:2, nums = [2,2]
解释:函数应该返回新的长度 2, 并且 nums 中的前两个元素均为 2。你不需要考虑数组中超出新长度后面的元素。例如,函数返回的新长度为 2 ,而 nums = [2,2,3,3] 或 nums = [2,2,0,0],也会被视作正确答案。
示例2:
输入:nums = [0,1,2,2,3,0,4,2], val = 2
输出:5, nums = [0,1,4,0,3]
解释:函数应该返回新的长度 5, 并且 nums 中的前五个元素为 0, 1, 3, 0, 4。注意这五个元素可为任意顺序。你不需要考虑数组中超出新长度后面的元素。
提示:
0 <= nums.length <= 100
0 <= nums[i] <= 50
0 <= val <= 100
解题思路:
采用双指针法,设置left,right两个指针变量,left指向下一个赋值的位置,right指向当前正在处理的元素,判断是否等于需要移出的元素值,若不是则将其指向的值赋值到left位置,实现原地修改
解题代码:
class Solution {
public int removeElement(int[] nums, int val) {
// 采用双指针的方法
// left 指向下一个赋值的位置,right指向当前正在处理的元素
int left=0;
int n=nums.length;
for(int right=0;right
难度:★ 链接:力扣
给你二叉树的根节点 root 和一个表示目标和的整数 targetSum 。判断该树中是否存在 根节点到叶子节点 的路径,这条路径上所有节点值相加等于目标和 targetSum 。如果存在,返回 true ;否则,返回 false 。叶子节点 是指没有子节点的节点。
示例1:
输入:root = [5,4,8,11,null,13,4,7,2,null,null,null,1], targetSum = 22
输出:true
解释:等于目标和的根节点到叶节点路径如上图所示。
示例2:
输入:root = [1,2,3], targetSum = 5
输出:false
解释:树中存在两条根节点到叶子节点的路径:
(1 --> 2): 和为 3
(1 --> 3): 和为 4
不存在 sum = 5 的根节点到叶子节点的路径。
示例3:
输入:root = [], targetSum = 0 输出:false 解释:由于树是空的,所以不存在根节点到叶子节点的路径。
提示:
- 树中节点的数目在范围
[0, 5000]
内-1000 <= Node.val <= 1000
-1000 <= targetSum <= 1000
解题思路:
遇到二叉树类型的题目,自然会联想到使用递归算法来解题。
本题本质就是询问从根节点出发到其叶子节点,是否存在路径总和等于所给目标和targetSum.那么可以将这个大问题化为小问题,就是询问是否存在从当前节点到叶子节点的路径和等于targetSum-val (val为当前节点的值),这样就满足了递归的性质,,如果当前节点就是叶子节点,那么判断路径和是否等于val,若是非叶子节点,只需递归地询问根节点的每一个子节点即可。
解题代码:
/**
* 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 {
public boolean hasPathSum(TreeNode root, int targetSum) {
if(root==null){
return false;
}
//如果是叶子节点,直接判断当前节点值是否等于目标路径和
if(root.left==null&&root.right==null){
return root.val==targetSum;
}
//递归判断其左右子树
return hasPathSum(root.left,targetSum-root.val)||hasPathSum(root.right,targetSum-root.val);
}
}
难度:★ ★ 链接:力扣
实现 strStr() 函数。给你两个字符串 haystack 和 needle ,请你在 haystack 字符串中找出 needle 字符串出现的第一个位置(下标从 0 开始)。如果不存在,则返回 -1 。
说明:
当 needle 是空字符串时,我们应当返回什么值呢?这是一个在面试中很好的问题。对于本题而言,当 needle 是空字符串时我们应当返回 0 。这与 C 语言的 strstr() 以及 Java 的 indexOf() 定义相符。
示例1:
输入:haystack = "hello", needle = "ll" 输出:2
示例2:
输入:haystack = "aaaaa", needle = "bba" 输出:-1
提示:
1 <= haystack.length, needle.length <= 10^4
haystack
和needle
仅由小写英文字符组成
解题思路:
方法一:使用Java字符串中已经封装好的方法,虽说能通过,但其实这背离了本题出题者的意图
方法二:暴力匹配法,将haystack 中所有长度等于needle长度的字串一一与needle进行逐个字符比较,全部相同则返回第一个字符的下标,否则进行移动到 haystack 下一个位置,进行下一轮遍历比较。简洁点就是类似于一个滑块在haystack 中滑动,将其中的每个子串与needle进行比较。
方法三:KMP算法,该算法比较难理解,先插个眼,后续继续深入学习一下
解题代码:
方法一:Java封装好的方法(能AC,不建议)
class Solution {
public int strStr(String haystack, String needle) {
return haystack.indexOf(needle);
}
}
方法二:暴力匹配算法
class Solution {
public int strStr(String haystack, String needle) {
int n=haystack.length();
int m=needle.length();
//下标i为子串的第一个下标i+m<=n防止数组下标越界
for(int i=0;i+m<=n;i++){
//布尔值存储是否存在这样的子串,默认值为true
boolean flag=true;
for(int j=0;j
方法三:
class Solution {
public int strStr(String haystack, String needle) {
int n = haystack.length(), m = needle.length();
if (m == 0) {
return 0;
}
int[] pi = new int[m];
for (int i = 1, j = 0; i < m; i++) {
while (j > 0 && needle.charAt(i) != needle.charAt(j)) {
j = pi[j - 1];
}
if (needle.charAt(i) == needle.charAt(j)) {
j++;
}
pi[i] = j;
}
for (int i = 0, j = 0; i < n; i++) {
while (j > 0 && haystack.charAt(i) != needle.charAt(j)) {
j = pi[j - 1];
}
if (haystack.charAt(i) == needle.charAt(j)) {
j++;
}
if (j == m) {
return i - m + 1;
}
}
return -1;
}
}
难度:★ 链接:力扣
给你一个二叉树的根节点
root
, 检查它是否轴对称。
示例1:
输入:root = [1,2,2,3,4,4,3] 输出:true
示例2:
输入:root = [1,2,2,null,3,null,3] 输出:false
提示:
- 树中节点数目在范围
[1, 1000]
内-100 <= Node.val <= 100
解题思路:
二叉树结构,往递归方向思考。不难发现,就是询问二叉树的左右子树是否满足对称的性质,只需递归询问在同一深度下,左子树的值是否等于对应右子树的值,和左子树的左孩子是否等于右子树的右孩子,如此反复递归询问。
解题代码:
/**
* 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 {
public boolean isSymmetric(TreeNode root) {
return isMirro(root,root);
}
public boolean isMirro(TreeNode root1,TreeNode root2){
if(root1==null&&root2==null){
return true;
}
if(root1==null||root2==null){
return false;
}
return (root1.val==root2.val)&&isMirro(root1.left,root2.right)&&isMirro(root1.right,root2.left);
}
}
难度:★ 链接:力扣
给定一个二叉树的根节点
root
,返回 它的 中序 遍历 。
示例1:
输入:root = [1,null,2,3] 输出:[1,3,2]
示例2:
输入:root = [] 输出:[]
示例3:
输入:root = [1] 输出:[1]
提示:
- 树中节点数目在范围
[0, 100]
内-100 <= Node.val <= 100
解题思路:
典型的递归,只需根据二叉树的中序遍历的方法:左中右,即先遍历左子树,再遍历当前节点,最后遍历右节点,按照这个顺序去访问二叉树中的每一个节点。本题采用一个动态数组ArrayList来动态存储中序遍历的结果。
解题代码:
/**
* 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 {
public List inorderTraversal(TreeNode root) {
Listarr=new ArrayList();
if(root==null){
return arr;
}
arr=func(arr,root);
return arr;
}
public List func(Listarr,TreeNode root){
if(root==null){
return arr;
}
func(arr,root.left);
arr.add(root.val);
func(arr,root.right);
return arr;
}
}
难度:★ 链接:力扣
给你两个单链表的头节点 headA 和 headB ,请你找出并返回两个单链表相交的起始节点。如果两个链表不存在相交节点,返回 null 。图示两个链表在节点 c1 开始相交:
题目数据 保证 整个链式结构中不存在环。
注意,函数返回结果后,链表必须 保持其原始结构 。
自定义评测:
评测系统 的输入如下(你设计的程序 不适用 此输入):
intersectVal - 相交的起始节点的值。如果不存在相交节点,这一值为 0
listA - 第一个链表
listB - 第二个链表
skipA - 在 listA 中(从头节点开始)跳到交叉节点的节点数
skipB - 在 listB 中(从头节点开始)跳到交叉节点的节点数
评测系统将根据这些输入创建链式数据结构,并将两个头节点 headA 和 headB 传递给你的程序。如果程序能够正确返回相交节点,那么你的解决方案将被 视作正确答案 。
示例1:
输入:intersectVal = 8, listA = [4,1,8,4,5], listB = [5,6,1,8,4,5], skipA = 2, skipB = 3
输出:Intersected at '8'
解释:相交节点的值为 8 (注意,如果两个链表相交则不能为 0)。
从各自的表头开始算起,链表 A 为 [4,1,8,4,5],链表 B 为 [5,6,1,8,4,5]。
在 A 中,相交节点前有 2 个节点;在 B 中,相交节点前有 3 个节点。
— 请注意相交节点的值不为 1,因为在链表 A 和链表 B 之中值为 1 的节点 (A 中第二个节点和 B 中第三个节点) 是不同的节点。换句话说,它们在内存中指向两个不同的位置,而链表 A 和链表 B 中值为 8 的节点 (A 中第三个节点,B 中第四个节点) 在内存中指向相同的位置。
示例2:
输入:intersectVal = 2, listA = [1,9,1,2,4], listB = [3,2,4], skipA = 3, skipB = 1
输出:Intersected at '2'
解释:相交节点的值为 2 (注意,如果两个链表相交则不能为 0)。
从各自的表头开始算起,链表 A 为 [1,9,1,2,4],链表 B 为 [3,2,4]。
在 A 中,相交节点前有 3 个节点;在 B 中,相交节点前有 1 个节点。
示例3:
输入:intersectVal = 0, listA = [2,6,4], listB = [1,5], skipA = 3, skipB = 2
输出:null
解释:从各自的表头开始算起,链表 A 为 [2,6,4],链表 B 为 [1,5]。
由于这两个链表不相交,所以 intersectVal 必须为 0,而 skipA 和 skipB 可以是任意值。
这两个链表不相交,因此返回 null 。
提示:
listA 中节点数目为 m
listB 中节点数目为 n
1 <= m, n <= 3 * 104
1 <= Node.val <= 105
0 <= skipA <= m
0 <= skipB <= n
如果 listA 和 listB 没有交点,intersectVal 为 0
如果 listA 和 listB 有交点,intersectVal == listA[skipA] == listB[skipB]
解题思路:
双指针法,设置ptr1,ptr2两个指针分别指向A,B两个链表的头部,当两个链表相同则其头节点即为相交的首节点。若不相同,让两个指针先遍历本链表,若长度相同则会在某个点相遇,若长度不同,则长度短的链表A(假设长度A
解题代码:
/**
* 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 ptr1=headA;
ListNode ptr2=headB;
//当两个链表完全相同时,首个相交节点就是它们的首节点
if(ptr1==ptr2){
return ptr1;
}else{
while(ptr1!=ptr2){
ptr1=ptr1!=null?ptr1.next:headB;
ptr2=ptr2!=null?ptr2.next:headA;
}
return ptr1;
}
}
}
难度:★ 链接:力扣
给定两个字符串
s
和t
,编写一个函数来判断t
是否是s
的字母异位词。注意:若
s
和t
中每个字符出现的次数都相同,则称s
和t
互为字母异位词。
示例1:
输入: s = "anagram", t = "nagaram" 输出: true
示例2:
输入: s = "rat", t = "car" 输出: false
提示:
1 <= s.length, t.length <= 5 * 104
s
和t
仅包含小写字母
解题思路:
方法一:统计两个字符串中每个字母出现的频次,然后进行比较。这是最常规的思路
方法二:将两个字符串转换成字符数组,然后将其进行排序,再判断这两个字符数组是否相同
解题代码:
方法一:
class Solution {
public boolean isAnagram(String s, String t) {
//统计两个字符串中字母出现的频次,比较是否相同
int nums[]=new int[26];//存储各个字母的频次
//长度不同,肯定不合题意
if(s.length()!=t.length()){
return false;
}
for(int i=0;i
方法二:
class Solution {
public boolean isAnagram(String s, String t) {
//将字符串排序,如果相同则返回true 否则返回false
if(s.length()!=t.length()){
return false;
}
//将字符串转换成对应的字符数组
char ch1[]=s.toCharArray();
char ch2[]=t.toCharArray();
//排序
Arrays.sort(ch1);
Arrays.sort(ch2);
return Arrays.equals(ch1,ch2);
}
}
难度:★ 链接:力扣
给定一个非空整数数组,除了某个元素只出现一次以外,其余每个元素均出现两次。找出那个只出现了一次的元素。你的算法应该具有线性时间复杂度。 你可以不使用额外空间来实现吗?
示例1:
输入: [2,2,1] 输出: 1
示例2:
输入: [4,1,2,1,2] 输出: 4
解题思路:
先将数组进行排序,如果是出现两次的元素则会相邻,即和后一个相邻元素值相等;只出现一次的元素何其后一个相邻元素的值肯定不同,以此来判断找出只出现一次的元素。循环变量的步长为2
解题代码:
class Solution {
public int singleNumber(int[] nums) {
//先将数组升序排序
Arrays.sort(nums);
int i=0;
while(i+1
难度:★ 链接:力扣
给定一个排序数组和一个目标值,在数组中找到目标值,并返回其索引。如果目标值不存在于数组中,返回它将会被按顺序插入的位置。请必须使用时间复杂度为 O(log n) 的算法。
示例1:
输入: nums = [1,3,5,6], target = 5 输出: 2
示例2:
输入: nums = [1,3,5,6], target = 2 输出: 1
示例3:
输入: nums = [1,3,5,6], target = 7 输出: 4
提示:
- 1 <= nums.length <= 10^4
- -10^4 <= nums[i] <= 10^4
- nums为无重复元素的升序排列数组
- -10^4<= target <= 10^4
解题思路:
二分查找法,若数组中不存在该元素,则循环结束时,区间左端点left就是最后mid+1,即该元素应当插入的位置。
解题代码:
class Solution {
public int searchInsert(int[] nums, int target) {
int n=nums.length;
int left=0,right=n-1;
while(left<=right){
int mid=(right-left)/2+left;
if(nums[mid]==target){
return mid;
}else if(target
难度:★★ 链接:力扣
给你两个 非空 的链表,表示两个非负的整数。它们每位数字都是按照 逆序 的方式存储的,并且每个节点只能存储 一位 数字。请你将两个数相加,并以相同形式返回一个表示和的链表。你可以假设除了数字 0 之外,这两个数都不会以 0 开头。
示例1:
输入:l1 = [2,4,3], l2 = [5,6,4] 输出:[7,0,8] 解释:342 + 465 = 807.
示例2:
输入:l1 = [0], l2 = [0] 输出:[0]
示例3:
输入:l1 = [9,9,9,9,9,9,9], l2 = [9,9,9,9] 输出:[8,9,9,9,0,0,0,1]
提示:
- 每个链表中的节点数在范围
[1, 100]
内0 <= Node.val <= 9
- 题目数据保证列表表示的数字不含前导零
解题思路:
模拟加法的过程,注意最后进位大于0的情况
解题代码:
/**
* 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 addTwoNumbers(ListNode l1, ListNode l2) {
ListNode head=null;
ListNode tail=null;
int carry=0;//进位
while(l1!=null||l2!=null){
int a=l1!=null?l1.val:0;
int b=l2!=null?l2.val:0;
int sum=a+b+carry;
if(head==null){
head=tail=new ListNode(sum%10);
}else{
tail.next=new ListNode(sum%10);
tail=tail.next;
}
carry=sum/10;
if(l1!=null){
l1=l1.next;
}
if(l2!=null){
l2=l2.next;
}
}
//处理最后一个进位,如果大与0则需要再次开辟一个新节点来存储
if(carry>0){
tail.next=new ListNode(carry);
}
return head;
}
}
难度:★★ 链接: 力扣
整数数组 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 。
注意:你必须设计一个时间复杂度为 O(log n) 的算法解决此问题。
示例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(n),而题目要求是O(logn)显然不行。
其次,对于有序序列或者部分有序序列,我们可以联想到使用二分查找法,其复杂度为O(logn)。
本题是有序数组经过在某点处旋转从而形成了一部分有序,另一部分可能有序可能部分有序的情况,我们通过判断局部区间端点值的大小关系可以判断该区间是否是有序的,再有序区间内使用二分查找去搜索目标值。无序的区间再次一分为二,其中一部分有序,另一部分可能有序也可能部分有序,重复上述过程进行对目标值的搜索。由于使用的是二分查找法,所以其复杂度为O(logn)
解题代码:
class Solution {
public int search(int[] nums, int target) {
int n=nums.length;
if(n==0){
return -1;
}
if(n==1){
return nums[0]==target?0:-1;
}
int left=0;
int right=n-1;
while(left<=right){
int mid=(right-left)/2+left;
if(nums[mid]==target){
return mid;
}
//通过区间端点值的比较来判断左边是否是有序序列
if(nums[0]<=nums[mid]){
//注意目标值可能为端点值的情况
if(nums[0]<=target&&target
其实算法题做多了,会发现它其实是有章可循的。而这个“章”是靠长期的刷题、不断总结所得来的。可以是一些常见的算法模板,可以是拿到题目时的“条件反射”,可以是一题多解。本周和一位大佬朋友一起刷题,互相监督、交流刷题才有动力,否则可能会中途懈怠或者放弃等等。
我的刷题思路是由简单题入手,培养自己对题目的感知能力,即所谓的手感。其次,积累基础的算法,和一些常用的经典算法。因为中等题或者困难题其实可以拆解出来变成一道道简单题,等简单题有自信可以手撕的时候,中等题便可以有能力尝试AC,不再是“面向题解编程”了!当然,刷完了一题还是要看题解的,借鉴其它码友优秀的思路与代码,学习转化总结,最终变成自己的!这是一个内化的过程,像食草动物那样不断嚼烂、反刍、吸收!
《每日一题,一周一结》专栏正式开始,希望能一直坚持下去,欢迎感兴趣的朋友一起交流学习。