代码随想录二刷 Day06 | 15. 三数之和,18. 四数之和,344.反转字符串,541. 反转字符串II,卡码网:54.替换数字,151.翻转字符串里的单词,卡码网:55.右旋转字符串

题目与题解

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模式下如何输入参数。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值