徒手挖地球七周目
NO.16 最接近的三数之和 中等
思路一:暴力破解法 用list保存所有的三数之和的情况,然后找出最接近target的数:
public int threeSumClosest(int[] nums, int target) {
int len=nums.length;
if (nums==null||len<3)throw new IllegalArgumentException("error of argument!");
// 用list保存所有的三数之和
List<Integer> list=new ArrayList<>();
for (int i=0;i<len;i++){
for (int j=i+1;j<len;j++){
for (int k=j+1;k<len;k++){
list.add(nums[i]+nums[j]+nums[k]);
}
}
}
// 遍历list找出最接近target的数
int ans=list.get(0);
for (Integer i : list) {
if (Math.abs(target-ans)>Math.abs(target-i))
ans=i;
}
return ans;
}
时间复杂度:O(n^3)
思路二:双指针法 和第15题的双指针法思路类似,本题免去了去重的操作。1. 先对数组排序,时间复杂度O(nlogn)。2. 依次遍历每个元素nums[i]。3. 然后用前后指针L=i+1和R=nums.length-1分别指向nums[i]后面部分的开头nums[L]和结尾nums[R]。4. 得到sum=nums[i]+nums[L]+nums[R],如果sum更接近target,就更新ans。5. 如果sum==target,就已经找到最接近target的三数之和,返回sum;如果sum>target,说明需要小一点的数来组合,即R–;如果sum<target,说明需要大一点的数来组合,即L++。6. 如果没有得到sum==target,那么每次双指针都需要遍历所有"后面所有元素",即while(L<R)。
public int threeSumClosest(int[] nums, int target) {
int len= nums.length,ans=nums[0]+nums[1]+nums[2];
if (nums==null||len<3)throw new IllegalArgumentException("error of argument!");
// 对数组排序
Arrays.sort(nums);
// 依次遍历每个元素nums[i]
for (int i=0;i<len;i++){
// 然后用前后指针L和R分别指向nums[i]后面部分的开头和结尾
int L=i+1;
int R=len-1;
while (L<R){
int sum=nums[i]+nums[L]+nums[R];
if (Math.abs(target-ans)>Math.abs(target-sum)){
ans=sum;
}
if (sum==target){//如果sum==target,那就已经找到最接近target的三数之和
return ans;
}else if (sum<target){//如果sum<target,说明需要大一点的数来组合,即L++
L++;
}else if (sum>target){//如果sum<target,说明需要小一点的数来组合,即R--;
R--;
}
}
}
return ans;
}
时间复杂度:O(n^2) 整个遍历过程,固定值为 n 次,双指针为 n 次。
NO.17 电话号码的字母组合 中等
思路一:回溯法 如果这道题加一个条件:“每次输入3位字符的字符串”。那么这道题就非常简单了,直接三层for循环就解决了。这道题棘手的地方就是如何确定循环的层数,这时候递归就派上用场了(模仿大佬的语气)!
例如输入"2345"这样的字符串:第一次递归处理2,然后处理完第一个字符2之后,将输入的字符改变成"345"并调用第二个递归函数;第二次递归处理3,将字符串改变成"45"后再次递归;第三次递归处理4,将字符串改变成 “5"后继续递归;第四次递归处理5,将字符串改变成”"后继续递归;最后发现字符串为空了,将结果放到列表中并返回。
上面是从函数调用的角度去看的,而每次调用下一层递归时,都需要将本层的一些处理结果放到一个临时变量中,再传递给下一层,从这个变量层层传递的变化看,就像一棵树一样,这个算法的时间复杂度很高,是O(3^n)这个级别的。
// 用数组或hashmap存储数字及其对应的字符表
String[] letters={"","","abc","def","ghi","jkl","mno","pqrs","tuv","wxyz"};
// 结果集
List<String> result=new ArrayList<>();
public List<String> letterCombinations(String digits) {
// 边界处理
if (digits.length()==0||digits==null)return result;
backTrack(digits,"",0);
return result;
}
// 递归函数
public void backTrack(String str,String combination,int index){
// 递归终止条件,当index==str.length()时说明str==""
if (index==str.length()){
result.add(combination);
return;
}
// 获取当前index位置的字符,此处和前文的思路中有所不同:没有采用每次将字符串切割的方法
// subString()每次都会生成新的字符串,而用index方式取当前第一个字符,效率更高一点
char c= str.charAt(index);
String letter=letters[c-'0'];
// 遍历letter字符串,例如第一次得到的是‘2’,即遍历“abc”
for (int i=0;i<letter.length();i++){
// 这里是比较值得思考的地方,递归调用
backTrack(str,combination+letter.charAt(i),index+1);
}
}
时间复杂度:O(3^n)
思路二:队列法 利用队列先进先出的特点来处理该问题。
直接用一个例子来说明思路:假设输入的还是"23",先将"2"对应的字符依次放入队列,队列res变为{“a”,“b”,“c”};将此时队列中的每个字符串依次取出的同时分别和下一个输入数字所对应的字符拼接后重新放入队列,将"a"取出和第二个输入数字"3"对应的字符"def"依次拼接后重新放入队列,队列res变为{“b”,“c”,“ad”,“ae”,“af”},将"b"取出和第二个输入数字"3"对应的字符"def"依次拼接后重新放入队列,队列res变为{“c”,“ad”,“ae”,“af”,“bd”,“be”,“bf”},将"c"取出和第二个输入数字"3"对应的字符"def"依次拼接后重新放入队列,队列res变为{“ad”,“ae”,“af”,“bd”,“be”,“bf”,“cd”,“ce”,“cf”};所有输入数字遍历结束。
public List<String> letterCombinations(String digits) {
List<String> res=new ArrayList<>();
// 边界处理
if (digits==null||digits.length()==0)return res;
// 用数组或hashmap存储数字及其对应的字符表
String[] letters={"","#","abc","def","ghi","jkl","mno","pqrs","tuv","wxyz"};
// 先往队列中加入一个空字符,防止第一次循环从队列中取出第一个元素时出现下标越界异常
res.add("");
// 遍历输入的字符串
for(int i=0;i<digits.length();i++){
// 取出当前遍历数字对应的字符串
String letter=letters[digits.charAt(i)-'0'];
// 获取当前队列的长度,不能在for循环中直接j<res.size(),因为内层循环中队列在不断增长,导致死循环
int size = res.size();
// 遍历队列中每个元素
for (int j=0;j<size;j++){
// 从队列中取出第一个元素
String temp = res.remove(0);
// 遍历当前数字对应的字符串的每个字符,依次和取出的第一个元素拼接后重新放入队列
for (int k=0;k<letter.length();k++){
res.add(temp+letter.charAt(k));
}
}
}
return res;
}
时间复杂度:O(3^n)
NO.18 四数之和 中等
思路一:双指针法 熟悉的配方,熟悉的味道,回想“2.两数之和”和“15.三数之和”分别是如何计算的。这里我的思路是将二者相结合形参这道“四数之和”的算法:1.因为需要用到双指针法,所以先将数组排序。2. 遍历数组每个元素nums[i]的时候计算其于target的差值temp(这里有点两数之和的味道)。3. 同时在nums[i]元素后面的部分寻找是否有三个数相加等于temp(这里就是进行双指针法解三数之和,具体思路参考徒手挖地球六周目中的三数之和双指针法思路),如果找到三数和等于temp就将这三个数和nums[i]加入结果集。5. 在nums[i]元素后面的部分进行双指针法全部遍历完后,对nums[i+1]进行上述操作,直至数组中所有元素都进行完毕。
和"三数之和"一样需要"去重":1. 外层for遍历每个元素nums[i]时,除了0号元素之外如果nums[i]==nums[i+1],则需要跳过。2. 内层循环除了第一个元素之外,如果nums[j]==nums[j+1],也需要跳过。
可以优化的地方:1. 固定当前nums[i]元素后,最小的四数之和已经大于target,则结束循环。2.固定当前nums[i]元素后,当前最大的四数之和依然小于target,则跳过当前元素,进行下一个元素nums[i+1]。
public List<List<Integer>> fourSum(int[] nums, int target) {
List<List<Integer>> ans=new ArrayList<>();
int len=nums.length;
if (nums==null||len<4)return ans;
// 排序
Arrays.sort(nums);
// 遍历数组每一个元素,因为是求四数之和,所以i<len-3
for (int i=0;i<len-3;i++){
// 如果当前最小的四数之和已经大于target,则结束循环
if (nums[i]+nums[i+1]+nums[i+2]+nums[i+3]> target)break;
// 如果当前最大的四数之和依然小于target,则跳过当前元素,进行下一个元素
if (nums[i]+nums[len-1]+nums[len-2]+nums[len-3]< target)continue;
// 跳过重复的元素
if (i>0&&nums[i]==nums[i-1])continue;
// 当前数组想要组成target所需要的值
int temp=target-nums[i];
// 遍历i号元素后面部分的每个元素,因为是求三数之和,所以i<len-2
for (int j=i+1;j<len-2;j++){
// 跳过重复元素
if (j>i+1&&nums[j]==nums[j-1])continue;
// 用双指针分别指向j号元素后面部分的开始元素和结尾元素
int L=j+1,R=len-1;
while (L<R){
int sum=nums[j]+nums[L]+nums[R];
if (sum==temp){
ans.add(Arrays.asList(nums[i],nums[j],nums[L],nums[R]));
while (L<R&&nums[L]==nums[L+1])L++;
while (L<R&&nums[R]==nums[R-1])R--;
L++;
R--;
}else if (sum<temp){
L++;
}else if (sum>temp){
R--;
}
}
}
}
return ans;
}
时间复杂度:O(n^3)
虽然才做了不到二十道题目,但是感觉思维上已经有一点长进了,不再是看到任何题目都是脑袋一片空白或者只是想暴力破解了。说明最近刷题有一定的成效,我的刷题方式还不是分类刷题,暂时先是按顺序刷中等和简单的题目锻炼思维。等做题量达到一定程度(或许是100~150左右),再进行系统的分类刷题。
因为还要兼顾学校的课程和一些javaee开发知识的学习,目前刷题节奏尽量保持:一天至少一道或两道新题,每道题至少做两遍或以上,每天用随机数选两道做过的题目捋清思路复盘。