题目与题解
15. 三数之和
题目链接:15. 三数之和
代码随想录题解:15. 三数之和
解题思路
虽然做过一次了但是再做还是两眼一抹黑。本来想参考两数之和来做,先遍历第一个数字,下标为i,从nums[0]开始遍历,然后第二层循环从j = i+1开始遍历,用两数之和的哈希表计算第三个数字和对应的下标。但是最后整烂了,又重新看了答案。
答案有两种方法,第一种仍旧用哈希表完成,但是用的是set类型,同时有一些去重的操作。首先,对数组进行排序,然后循环最外层遍历第一个数,但如果nums[i]大于0,再加后面的数只能更大,可以直接break;如果nums[i] = nums[i-1],后面数字会重复,需要continue。第二层循环根两数之和有点类似,但不完全一样,需要用hashset来去重,第二层循环每次遍历的数为nums[j],所以要查找的第三个数就是c=-nums[i]-nums[j]。如果set中不存在c,就把nums[j]加入set,否则说明找到了一个元组,把nums[i],c,nums[j]加入结果数组后,再把c去掉。这里还涉及到一个去重的问题,如果数组中存在如-2,1,1,1,1,0这样的序列,对于-2,1,1这样的元组,即使第一次把c去掉了,遍历到第三个1的时候还是会被加入set,导致结果重复,所以要设置如果nums[j] == nums[j-1] == nums[j-2],即有连续三个数相同时,必须让j++,直到不满足条件,避免重复。
class Solution {
public List<List<Integer>> threeSum(int[] nums) {
List<List<Integer>> result = new ArrayList<>();
Arrays.sort(nums);
for (int i = 0; i < nums.length; i++) {
// 如果第一个元素大于零,不可能凑成三元组
if (nums[i] > 0) {
return result;
}
// 三元组元素a去重
if (i > 0 && nums[i] == nums[i - 1]) {
continue;
}
HashSet<Integer> set = new HashSet<>();
for (int j = i + 1; j < nums.length; j++) {
// 三元组元素b去重
if (j > i + 2 && nums[j] == nums[j - 1] && nums[j - 1] == nums[j - 2]) {
continue;
}
int c = -nums[i] - nums[j];
if (set.contains(c)) {
result.add(Arrays.asList(nums[i], nums[j], c));
set.remove(c); // 三元组元素c去重
} else {
set.add(nums[j]);
}
}
}
return result;
}
}
另一种方法为双指针法,更好理解。仍旧需要先排序,第一层循环跟前面一样,剪枝条件也相同;第二层循环设置两个指针left和right,left从i+1开始,right从数组最后一个数字开始。当left < right时,如果nums[i]+nums[left]+nums[right]小于0,说明数字不够大,left要右移,反之right要左移,如果三数之和等于结果,就将其加入结果序列,然后进行去重,如果left < right且nums[left]=nums[left+1],left右移,同理right左移,left和right都移完后,再收缩窗口,left++,right--。
class Solution {
public List<List<Integer>> threeSum(int[] nums) {
List<List<Integer>> result = new ArrayList<>();
Arrays.sort(nums);
// 找出a + b + c = 0
// a = nums[i], b = nums[left], c = nums[right]
for (int i = 0; i < nums.length; i++) {
// 排序之后如果第一个元素已经大于零,那么无论如何组合都不可能凑成三元组,直接返回结果就可以了
if (nums[i] > 0) {
return result;
}
if (i > 0 && nums[i] == nums[i - 1]) { // 去重a
continue;
}
int left = i + 1;
int right = nums.length - 1;
while (right > left) {
int sum = nums[i] + nums[left] + nums[right];
if (sum > 0) {
right--;
} else if (sum < 0) {
left++;
} else {
result.add(Arrays.asList(nums[i], nums[left], nums[right]));
// 去重逻辑应该放在找到一个三元组之后,对b 和 c去重
while (right > left && nums[right] == nums[right - 1]) right--;
while (right > left && nums[left] == nums[left + 1]) left++;
right--;
left++;
}
}
}
return result;
}
}
注意点
三数之和的细节很多,去重是最麻烦的一件事。
首先为了提高效率,在对数组排序后,要对第一层循环进行剪枝,保证nums[i]<=0,否则后序计算都是无用功,其次要保证nums[i]不会重复,否则结果会有重复。
第二步是对剩下两个数进行处理,无论是用哈希法还是双指针法,都要保证后面两个数不重复。
另外,在双指针法中,要注意对left和right去重的地方是放在else里面,而不是判断sum的时候,比较清晰,就是难记。去重完了以后也不要忘记它们还需要再移动一次。
18. 四数之和
题目链接:18. 四数之和
代码随想录题解:18. 四数之和
解题思路
这题和三数之和非常类似,不同点在于:多了一层循环,以及判断跳出循环的依据有所变化。
首先是双层的for循环,去重方法仍旧是判断nums[i]=nums[i-1]时continue,第二层也是一样。但是剪枝方法就有所区别了,之前只需要nums[i]>0时剪枝即可,因为三数之和的target就是0,nums[i]>0时由于j至少是i+1,那nums[j]也必然大于0,所以不等式成立。但是如果这题仿照着直接写nums[i]>target,就会碰到一个问题,target不一定大于0,也就是说nums[i] > target时,如果target是负数,nums[i]+nums[j]其实有可能小于target,导致漏掉一些答案。所以这里还需要加上条件nums[i]>0,就没问题了。
同理,第二层剪枝时,nums[i]+nums[j] >0也得加上。
第三层循环就跟三数之和完全一样了,照搬就行。
class Solution {
public List<List<Integer>> fourSum(int[] nums, int target) {
Arrays.sort(nums);
List<List<Integer>> result = new ArrayList<>();
for (int i = 0; i < nums.length - 3; i++) {
if (nums[i] > target && nums[i] >= 0) break;
if (i > 0 && nums[i] == nums[i-1]) continue;
for (int j = i + 1; j < nums.length - 2; j++) {
if (nums[i] + nums[j] > target && nums[i] + nums[j] >= 0) break;
if (j > i + 1 && nums[j] == nums[j-1]) continue;
int left = j + 1, right = nums.length - 1;
while (left < right) {
int sum = nums[i] + nums[j] + nums[left] + nums[right];
if (sum > target) right--;
else if (sum < target) left++;
else {
result.add(Arrays.asList(nums[i],nums[j],nums[left],nums[right]));
while(left < right && nums[left] == nums[left + 1]) left++;
while(left < right && nums[right] == nums[right - 1]) right--;
left++;
right--;
}
}
}
}
return result;
}
}
注意点
剪枝的条件不一样,一定要注意。
344.反转字符串
题目链接:344.反转字符串
代码随想录题解:344.反转字符串
解题思路
最简单的题,没什么可说的,直接一遍循环用双指针不断交换两个指针指向的数字就行。
class Solution {
public void reverseString(char[] s) {
int i = 0, j = s.length - 1;
while (i < j) {
char temp = s[i];
s[i] = s[j];
s[j] = temp;
i++;
j--;
}
}
}
注意点
无
541. 反转字符串II
题目链接:541. 反转字符串II
代码随想录题解:541. 反转字符串II
解题思路
基于前一题的reverse函数,用循环来反转字符串,循环每次步长为2*k,有个需要额外处理的地方就是如果最后的一组数不足k个,也是要反转的,因此设置循环的结束条件是i < s.length() - k,保证前面存在k个数就正常反转,每次反转i到i+k-1之间的数;如果不存在k个数,等跳出循环后,判断i是否大于s.length(),如果小于它,反转i到s.length()之间的数就好。
class Solution {
public String reverseStr(String s, int k) {
char[] ss = s.toCharArray();
int i = 0;
for (; i < s.length() - k; i += 2*k) {
reverse(ss, i, i + k - 1);
}
if (i < ss.length) {
reverse(ss, i, ss.length - 1);
}
return new String(ss);
}
public char[] reverse(char[] ss, int start, int end) {
while (start < end) {
char temp = ss[start];
ss[start++] = ss[end];
ss[end--] = temp;
}
return ss;
}
}
注意点
Java中String类型不可修改,所以为了方便先用toCharArray()转换为字符数组,再进行修改,最后用new String(char[])返回String即可。、
循环体可以像答案一样写的更漂亮一点:
for (int i = 0; i< ch.length; i += 2 * k) {
// 2. 剩余字符小于 2k 但大于或等于 k 个,则反转前 k 个字符
if (i + k <= ch.length) {
reverse(ch, i, i + k -1);
continue;
}
// 3. 剩余字符少于 k 个,则将剩余字符全部反转
reverse(ch, i, ch.length - 1);
}
卡码网:54.替换数字
题目链接:卡码网:54.替换数字
代码随想录题解:卡码网:54.替换数字
解题思路
Java里String不可修改,最简单的方法就是遍历原String的同时,新建一个StringBuilder类型,如果s.charAt(i)是字母,就直接加到StringBuilder后面,否则StringBuilder就加上'number'字符串。
import java.util.*;
public class Main {
public static void main (String[] args) {
/* code */
Scanner scanner = new Scanner(System.in);
String s = scanner.nextLine();
StringBuilder result = new StringBuilder();
for (int i = 0; i < s.length(); i++) {
if (s.charAt(i) >= '0' && s.charAt(i) <= '9') {
result.append("number");
}else {
result.append(s.charAt(i));
}
}
System.out.println(result);
}
}
还有一种以c++为基准的思路,尽量不用额外空间的前提下修改数组,所以要先遍历字符串,统计数字字符的数量count,然后将数组扩容为原先的长度加上五倍的count(因为原字符还占了一个位置),然后在数组中从后往前依次更新数组,最后返回更新后的字符串即可。
import java.util.*;
public class Main {
public static void main (String[] args) {
/* code */
Scanner scanner = new Scanner(System.in);
String s = scanner.nextLine();
int count = 0;
for (int i = 0; i < s.length(); i++) {
if (s.charAt(i) >= '0' && s.charAt(i) <= '9') {
count++;
}
}
int newLen = s.length() + 5*count;
char[] chars = new char[newLen];
for (int i = s.length() - 1, j = newLen - 1; i >= 0; i--) {
if (s.charAt(i) < '0' || s.charAt(i) > '9') {
chars[j--] = s.charAt(i);
} else {
chars[j--] = 'r';
chars[j--] = 'e';
chars[j--] = 'b';
chars[j--] = 'm';
chars[j--] = 'u';
chars[j--] = 'n';
}
}
System.out.println(new String(chars));
}
}
注意点
很久不写ACM模式,已经忘了输入用Scanner,输入字符串用scanner.nextLine(),后面再复习一下。
151.翻转字符串里的单词
题目链接:151.翻转字符串里的单词
代码随想录题解:151.翻转字符串里的单词
解题思路
分为以下几步:
1. 去除头尾空格,直接设置头尾指针不断前进,直到都找到第一个不为空格的字符,记录此时头尾的位置即可。
2. 去除中间多余的空额,用类似移动元素的方法来做,用后面的字符覆盖前面多余的空格,最后记录尾部实际的位置end即可。
3. 反转单词:可以先反转从start到end的整个字符串,然后再根据空格的分割,分别再反转每个空格之间的单词。
class Solution {
public String reverseWords(String s) {
char[] ss = s.toCharArray();
int start = 0, end = s.length() - 1;
while (start < s.length() && ss[start] == ' ') {
start++;
}
while (end >= 0 && ss[end] == ' ') {
end--;
}
int i = start, j = start;
while (j <= end) {
if (j > start && ss[j - 1] == ' ' && ss[j] == ' ') j++;
else {
ss[i++] = ss[j++];
}
}
end = i - 1;
reverse(ss, start, end);
j = start;
for (i = start; i <= end; i++) {
if (ss[i] == ' ') {
reverse(ss, j, i-1);
j = i+1;
}
if (i == end) reverse(ss, j, i);
}
return new String(ss, start, i - start);
}
public void reverse(char[] ss, int start, int end) {
while (start < end) {
char temp = ss[start];
ss[start++] = ss[end];
ss[end--] = temp;
}
}
}
注意点
这道题步骤比较多,细节也很多,容易写错。移动元素的时候要判断ss[i]要被ss[j]还是ss[j-1]覆盖,不能多了或者少了,移动完后的结尾也需要记录;反转单词技巧性较强,两次反转很难想到,但是写出来是容易的。
卡码网:55.右旋转字符串
题目链接:卡码网:55.右旋转字符串
代码随想录题解:卡码网:55.右旋转字符串
解题思路
纯技巧题,一共三次反转,并不难写,但很难想。
第一次反转整个数组,第二次反转前k个数,第三次反转剩下的数,就完成了旋转的过程。
import java.util.*;
public class Main {
public static void main (String[] args) {
/* code */
Scanner scanner = new Scanner(System.in);
int k = scanner.nextInt();
scanner.nextLine();
String s = scanner.nextLine();
char[] ss = s.toCharArray();
reverseString(ss, 0, s.length()-1);
reverseString(ss, 0, k - 1);
reverseString(ss, k, s.length()-1);
System.out.println(new String(ss));
}
public static void reverseString(char[] chars, int start, int end) {
int i = start, j = end;
while (i < j) {
char temp = chars[i];
chars[i] = chars[j];
chars[j] = temp;
i++;
j--;
}
}
}
注意点
如果先输入数字,再输入字符串,用ACM模式时,中间需要多加一步scanner.nextLine(),防止输入接收不对。
另外,static方法不能调用非static方法,所以写reverse方法时也要用static才行。
今日收获
复习了一下很难的三数之和和四数之和,巩固了一下字符串的解法,包括String与char[]的转换,StringBuilder的用法,ACM模式下如何输入参数。