目录
一、刷题
记录LeetCode力扣刷题,持续更新中…
LeetCode官网:https://leetcode.cn/
题号1:两数之和
给定一个整数数组 nums 和一个整数目标值 target,请你在该数组中找出 和为目标值 target 的那 两个 整数,并返回它们的数组下标。
你可以假设每种输入只会对应一个答案。但是,数组中同一个元素在答案里不能重复出现。
你可以按任意顺序返回答案。
1. 暴力解法(双循环)
class Solution {
public int[] twoSum(int[] nums, int target) {
int[] list=new int[2];
for(int i=0; i<nums.length;i++){
for(int j=i+1; j<nums.length;j++){
if(nums[i]+nums[j]==target){
list[0]=i;
list[1]=j;
}
}
}
return list;
}
}
时间复杂度O(n²)
j的初始值如果为0的话,循环次数会更多且需判断 i!=j
2. HashMap 唯一key
class Solution {
public int[] twoSum(int[] nums, int target) {
int[] list=new int[2];
Map<Integer, Integer> hashMap = new HashMap<Integer, Integer>();
for (int i = 0; i < nums.length; ++i) {
if (hashMap.containsKey(target - nums[i])) {
list[0]=hashMap.get(target - nums[i]);
list[1]=i;
}
hashMap.put(nums[i], i);
}
return list;
}
}
时间复杂度O(n)
我拿到题目时也想过先确定第一个数,再判断第二数是否在数组中(避免嵌套循环),但是第一时间没找到合适方法,后面看官方解法才想到。官方解法没有提前生成list数组,能提高一点执行用时。
3. 双指针(未通过版)
排序后下标已经乱了,不适合该题但是思路可以
class Solution {
public int[] twoSum(int[] nums, int target) {
int[] list=new int[2];
int start=0,end=nums.length-1;
QuickSort(nums,start,end);
// Arrays.sort(nums);
while(start<end){
if(nums[start]+nums[end]>target) end--;
if(nums[start]+nums[end]<target) start++;
if(nums[start]+nums[end]==target){
list[0]=start;
list[1]=end;
break;
}
}
return list;
}
public void QuickSort(int[] nums,int start,int end){
if(start<end){
// 获取分区后的枢纽位置
int pivotIndex=Partition(nums,start,end);
// 分别对枢纽左右两边的子数组进行递归排序
QuickSort(nums, start, pivotIndex - 1);
QuickSort(nums, pivotIndex + 1, end);
}
}
public int Partition(int[] nums,int start,int end){
// 单边循环
int pivot=nums[start]; // 基准元素
int mask=start; // 标记指针
for(int i=start+1;i<=end;i++){
if(nums[i]<nums[mask]){
mask++;
// 交换
int temp=nums[i];
nums[i]=nums[mask];
nums[mask]=temp;
}
}
// 交换基准
nums[start]=nums[mask];
nums[mask]=pivot;
return mask;
}
}
时间复杂度O(nlog₂n)(取决于排序算法O(n)+O(nlog₂n)=O(nlog₂n))
先使用快速排序(或者Arrays.sort())排序好,再用首尾指针去判断
题号2:两数相加
给你两个 非空 的链表,表示两个非负的整数。它们每位数字都是按照 逆序 的方式存储的,并且每个节点只能存储 一位 数字。
请你将两个数相加,并以相同形式返回一个表示和的链表。
你可以假设除了数字 0 之外,这两个数都不会以 0 开头。
1. 同一下标加减
/**
* 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,tail=null;
// 进位
int carry=0;
while(l1!=null || l2!=null){
int a=l1==null?0:l1.val;
int b=l2==null?0:l2.val;
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;
}
// 首位可能还要进位
if(carry>0) tail.next=new ListNode(carry);
return head;
}
}
时间复杂度O(n)
两个链表中同一下标位置的数字可以直接相加(的确没想到这点),最后用尾插法构建链表,注意一下进位即可。
2. 递归
/**
* 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) {
return add(l1,l2,0);
}
public ListNode add(ListNode l1, ListNode l2, int carry){
if(l1==null && l2==null && carry==0) return null;
int sum=carry;
if(l1!=null){
sum+=l1.val;
l1=l1.next;
}
if(l2!=null){
sum+=l2.val;
l2=l2.next;
}
return new ListNode(sum%10,add(l1,l2,sum/10));
}
}
递归执行循环代码,不用去处理节点的连接。
3. 转数字再求和(未通过版)
求和后,用尾插法重组链表,但超过int、long的取值范围,不支持这种解法
/**
* 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 m1=l1.next,m2=l2.next;
int a=l1.val,b=l2.val,speed=10;
while(m1!=null){
a+=m1.val*speed;
m1=m1.next;
speed*=10;
}
speed=10;
while(m2!=null){
b+=m2.val*speed;
m2=m2.next;
speed*=10;
}
int c=a+b;
ListNode head=new ListNode();
ListNode tail=head;
head.val=c%10;
c/=10;
// 尾插法
while(c!=0){
ListNode r=new ListNode();
r.val=c%10;
tail.next=r;
tail=r;
c/=10;
}
return head;
}
}
头插法:一个指针
尾插法:双指针
题号3:无重复字符的最长子串
给定一个字符串 s ,请你找出其中不含有重复字符的 最长子串 的长度。
1. 暴力解法
class Solution {
public int lengthOfLongestSubstring(String s) {
if (s == null)
return 0;
int start=0, end = 0,max = 0;
Set<Character> set = new HashSet<>();
while(end<s.length()){
while(set.contains(s.charAt(end))){
set.remove(s.charAt(start));
start++;
}
set.add(s.charAt(end));
max=Math.max(max,end-start+1);
end++;
}
return max;
}
}
时间复杂度O(n²)
依次从每一个字符开始的,枚举所有子串,依次筛选出最长的子串。例如abcbc,子串有[a、ab、abc、abcb、abcbc]、[b、bc、bcb、bcbc]、[c、cb、cbc],共12个。暴力解法也是基于双指针,两指针不是一起移动,而是分开移动的,start固定,end向右移动,移动完再回退到start位置重新遍历。
2. 滑动窗口
滑动窗口算法的基本思想是维护一个窗口(可能限制窗口大小),通过移动窗口的两个边界来处理问题。它是一种常用的双指针算法,可以优化暴力枚举的时间复杂度,使得算法的执行效率更高。
class Solution {
public int lengthOfLongestSubstring(String s) {
if (s.length()<2)
return s.length();
int start=0, end = 0,max = 0;
// Set<Character> set = new HashSet<>();
// for(;end<s.length();end++){
// while(set.contains(s.charAt(end))){
// set.remove(s.charAt(start)); //存在就从左边开始删,一直删到不存在为止
// start++;
// }
// set.add(s.charAt(end)); //不存在保存
// max=Math.max(max,end-start+1);
// }
Map<Character,Integer> map = new HashMap<>();
while(end<s.length()){
//存在start不用再慢慢加加,直接一步到位
if(map.containsKey(s.charAt(end))) start=Math.max(start,map.get(s.charAt(end))+1); //不能回退
map.put(s.charAt(end),end);
max=Math.max(max,end-start+1);
end++;
}
return max;
}
}
时间复杂度O(n)
start和end只会往一个方向移动,避免重复的循环。为了更好理解滑动窗口,接下来我将一步步优化算法:
以abcbc为例,它有12个子串。end依次判断a、ab、abc子串,当end移动到第二个b时,end需要回退start的位置,再去判断子串b、bc。
第一次优化:这时候已经重复判断了,abc子串不重复,也意味着b、bc不重复,能减少判断了几个子串。这时候何不如end不后退去重复判断,只需要把a移除,start前进即可。
第二次优化:到这里已经算是滑动窗口算法了,但在第一步我们已经判断过一次b重复了,第二步时又判断了一次b重复,这有点冗余了,所以可以让start直接跳到重复字符的下一个字符。
这里就解释为什么用HashMap不用HashSet,HashSet需要while去一直移动到重复字符的下一个,而HashMap有记录字符串下标,所以可以直接跳过去。
特别注意HashMap中保存的字符可能是以前出现的,不在窗口中,所以start要用max选取最大的。例如"abba",start移动到第二个b,end移动到第二个a时,map里面就存在第一个a。
题号4:寻找两个正序数组的中位数
给定两个大小分别为 m 和 n 的正序(从小到大)数组 nums1 和 nums2。请你找出并返回这两个正序数组的 中位数 。
算法的时间复杂度应该为 O(log (m+n)) 。
1. 双指针
不满足题设的时间复杂度
class Solution {
public double findMedianSortedArrays(int[] nums1, int[] nums2) {
if (nums1.length == 0) return findMedian(nums2);
if (nums2.length == 0) return findMedian(nums1);
int p1 = 0, p2 = 0, i = 0;
int[] merge = new int[nums1.length + nums2.length];
// 两个数组都还有值没比较
while (p1 < nums1.length && p2 < nums2.length) merge[i++] = nums1[p1] <= nums2[p2] ? nums1[p1++] : nums2[p2++];
// 只有第一个数组还有值
while (p1 < nums1.length) merge[i++] = nums1[p1++];
// 只有第一个数组还有值
while (p2 < nums2.length) merge[i++] = nums2[p2++];
return findMedian(merge);
}
public double findMedian(int[] nums) {
int length = nums.length;
if (length % 2 == 0) {
return (nums[(length - 1) / 2] + nums[length / 2]) / 2.0;
} else {
return nums[length / 2];
}
}
}
时间复杂度O(m+n),空间复杂度O(m+n)
利用两个指针依次比较元素,将两个数组合并到一个新数组中,然后根据奇数,还是偶数,返回中位数。
优化:
这里其实我们可以不用将两个数组真的合并,只需要找到中位数在哪,从而优化空间复杂度。总长度是奇数的话,只要找到一个中间数【length/2】,偶数的话,只要找到两个数【length/2的数和它前一个数(length-1)/2】,即可得到中位数。
class Solution {
public double findMedianSortedArrays(int[] nums1, int[] nums2) {
int p1 = 0, p2 = 0, pre_median = 0, median = 0;
int length = nums1.length + nums2.length;
for (int i = 0; i <= length / 2; i++) {
pre_median = median; // 记录排序的前一个结果
if (p1 < nums1.length && (p2 >= nums2.length || nums1[p1] < nums2[p2])) {
median = nums1[p1++];
} else {
median = nums2[p2++];
}
}
if (length % 2 == 0) {
return (pre_median + median) / 2.0;
} else {
return median;
}
}
}
时间复杂度O(m+n),空间复杂度O(1)
2. 二分查找(双指针+递归)
class Solution {
public double findMedianSortedArrays(int[] nums1, int[] nums2) {
int length = nums1.length + nums2.length;
if (length % 2 == 0) {
return (getKth(nums1,0,nums2,0,length/2) + getKth(nums1,0,nums2,0,length/2+1)) / 2.0;
} else {
return getKth(nums1,0,nums2,0,length/2+1);
}
}
//找到第k小的元素(递归)
//s1是第一个数组的起点下标,s2是第二个数组的起点下标
private int getKth(int[] nums1,int s1,int[] nums2,int s2,int k){
//注意跳出条件
if(s1==nums1.length) return nums2[s2+k-1];
if(s2==nums2.length) return nums1[s1+k-1];
if(k==1) return Math.min(nums1[s1],nums2[s2]);
//移动起始下标
int new_s1=Math.min(nums1.length,s1+k/2)-1;
int new_s2=Math.min(nums2.length,s2+k/2)-1;
//移动距离new_s-s+1
if (nums1[new_s1] > nums2[new_s2]){
return getKth(nums1,s1,nums2,new_s2+1,k-(new_s2-s2+1));
}else{
return getKth(nums1,new_s1+1,nums2,s2,k-(new_s1-s1+1));
}
}
}
时间复杂度 O(log(m+n)),空间复杂度 O(1)
如果对时间复杂度的要求有 log,通常都需要用到二分查找。第一个解法是一个一个地查找,但由于数组是有序的就可以一半一半地排除,减少次数。
二分查找是在有序数组中查找某一特定元素,将数组一分为二,查找元素与中间元素相比较,小于就在左边找,大于就在右边找,重复上述过程,不断缩小范围,每次剔除一半,效率更高。
原问题可以转换成求数组第k小的数,官方解法说明有点难理解,在我看来,只在中位数的前半部分(总共k个数,length/2)使用二分查找,例如上图的27之前的数,因为与后半部分无关,每次查找剔除一半元素(k/2个数),k逐渐变小直至为1。那这k/2个数怎么去确定呢?这就是比较难理解的点了。
由于两个数组没有一个数组那么直观,每次剔除元素,每一个数组都剔除一点太混乱,最好就是只在单个数组里面剔除这k/2个数。以上图为例,A和B个数组,总共14个数,需要找到第7小的数和第8小的数,以找第7小的数来说,需要先剔除k/2=3个数,即将两个数组的指针指向下标k/2-1,判断这个数的大小,3<=4,这就说明A数组的前2个数,可能有<=3的数,也可能有>3的数,就算全部<=3,加上B的前三个数,总共也才k/2-1+k/2=k-1(k为偶数时)或者k-2个数,不可能是第k个数,所以可以将B前3个剔除。
这里自己理一遍就会清楚一点。
橙色是剔除元素,k减去3(k不是每次都剔除k/2),指针下标往前(k/2-1),继续重复上一步。
这里有几个注意点:
当其中一个数组不够k/2时:它的指针只指向最后一个元素,此时剔除个数就不足k/2
当其中一个数组为空时:直接返回另外一个数组的第k元素
当 k=1时:直接返回两个数组首元素的最小值
4. 中位数特性
这里是一个特殊方法,依据的是中位数的特性,它可以将数组平分成相等的两个部分,按照之前的思路,先以一个数组来平分,一共有m+1种切法:
再以两个数组【数组A,长度m,数组B,长度n】来说,分别在 i 和 j 处【i∈[0,m]、j ∈[0,n] 】切:
i 的左边加上 j 的左边就相对于一个大数组的左半部分,i 的右边加上 j 的右边就相对于一个大数组的右半部分,这两部分具有一些特性:
总长度偶数时:左半部分元素个数=右半部分元素个数、左半部分的最大值max<=右半部分的最小值min(中位数=(max+min)/2)
总长度奇数时:左半部分元素个数+1=右半部分元素个数、左半部分的最大值max<=右半部分的最小值min(中位数=max)
此时难点就在于 i 、j 怎么划分?
由第一个特性可知:i+j=m−i+n−j(当 m+n 为偶数)或 i+j=m−i+n−j+1(当 m+n 为奇数),可将这两个合并得到 i+j= (m+n+1)/2,因为m+n不管是偶数还是奇数加不加1都不影响 i+j 的结果(int类型),则 j = (m+n+1) / 2 - i,这里m必须小于n 【如果 i = m,j= (n-m+1)/2,j 可能为负数】。流程上就可以先确定 i 再确定 j 。
有第二个特性可知:重点在 i 和 j 左右两边的四个数,为了保证 max ( A [ i - 1 ] , B [ j - 1 ]) <= min ( A [ i ] , B [ j ]),因为 A [ i - 1 ] <= A [ i ],B [ i - 1 ] <= B [ i ] 这是必然的,所以只需要保证 B [ j - 1 ] < = A [ i ] 和 A [ i - 1 ] <= B [ j ]
还需注意切到边界的情况,这种可以单独去处理也可以规定A[−1]=B[−1]=负无穷,A[m]=B[n]=正无穷(不会对左半部分的最大值产生影响;不会对右半部分的最小值产生影响)而不用单独处理。
这里还可以再简化一下条件,因为 i 递增,j 就会递减,所以在数组A中,一定存在一个最大的 i 满足 A [ i - 1 ] <= B [ j ],而i+1(它的下一个数)就一定满足 A [ i ] > B[ j+1 ],与原条件一致,第二条大小关系就不需要单独去判断了。(就算A里面所有数都大于或小于B里面的数,i=0/m 就可以用无穷去判断,不影响结果)
结论:i 用二分查找去取,而 j 由 i 的值确定,遵从 :
m<n
j = (m+n+1) / 2 - i
存在最大的 i 满足A [ i−1 ] ≤ B [ j ]
边界值为无穷
class Solution {
public double findMedianSortedArrays(int[] nums1, int[] nums2) {
if(nums1.length>nums2.length){
return findMedianSortedArrays(nums2,nums1); //保证m<n
}
int m=nums1.length,n=nums2.length;
int start=0,end=m; //二分法确定i
// median1:左半部分的最大值, median2:右半部分的最小值
int median1 = 0, median2 = 0;
while(start<=end){
int i = (start+end)/2;
int j = (m+n+1) /2-i;
int nums_i_1= i==0 ? Integer.MIN_VALUE : nums1[i-1];
int nums_i = i==m? Integer.MAX_VALUE : nums1[i];
int nums_j_1= j==0 ? Integer.MIN_VALUE : nums2[j-1];
int nums_j = j==n? Integer.MAX_VALUE : nums2[j];
if(nums_i_1<=nums_j){
median1=Math.max(nums_i_1,nums_j_1);//奇数时中位数在左边
median2=Math.min(nums_i,nums_j);
start =i+1; //i往前走
}else{
end= i-1; //i退后
}
}
return (m+n)%2==0 ? (median1+median2) / 2.0 : median1;
}
}
时间复杂度是O( log min(m,n))),空间复杂度 O(1)
二、解法总结
1. 嵌套循环
暴力解法最常用,不考虑时间复杂度。
2. 双指针
首尾指针,快慢指针,一个指针一个方向,减少循环次数,例如滑动窗口、二分查找算法。
3. 递归
例如遍历,线性结构用迭代,非线性结构用递归。递归执行循环代码,注意递归跳出条件。
4. 原地置换
用数组代替哈希表的作用,例题题号:LCR 120 :寻找文件副本
三、常用API(Java)
//引用类型
Integer
Integer.MIN_VALUE/MAX_VALUE //正负无穷
Character
//长度
list.length
str.length()
arrList.size()
set.size()
map.size()
//集合
int[] list=new int[length]
int[] list={
1,2,3}
char[] charList={
'a'}
ArrayList<T> arrList= new ArrayList<>()
Set<T> set = new HashSet<>()
Map<T,T> map = new HashMap<>()
//添加元素
arrList.add(element)
arrList.add(index,element)
set.add(element))
map.put(key,value)
//判断元素是否存在或相等
str.equals("")
set.contains(element)
map.containsKey(key)
map.containsValue(value)
//获取元素
list[index]
str.charAt(index)
arrList.get(index)
set.get(index)
map.get(key)
map.values() //所有value的集合
//修改元素
arrList.set(index,element)
//删除元素
arrList.remove(index/element)
arrList.clear()
set.remove(element)
set.clear()
map.remove(key)
map.clear()
//查找元素
arrList.indexOf(element) //第一次出现的下标
str.indexOf("") //第一次出现的下标
//字符串方法
str1.concat(str2) //拼接
str.startsWith/endsWith("") //判断是否以这个字符串开头或结束
str.substring(start, end) //截取,不含end
str.split("") //分割成字符串数组
str.replace("old","new") //替换
String.join("分隔符", str1, str2) //合并
str.toUpperCase/toLowerCase() //大小写
str.trim() //去空白
str.getBytes() //转字节数组
//遍历
for (T element: arr) {
} //增强for
//建立一个迭代器,并且将List中的元素放进迭代器中
Iterator it = lists.listIterator();
//迭代器的头指针是空的
while (it.hasNext()){
//每次next,指针都会指向下一个元素
T element = it.next();
}
//Math
Math.max/min(a,b)
Math.abs(a)
Math.round(a) //四舍五入为整数
//排序
Arrays.sort()