前言
刷题笔记系列文章旨在记录刷题过程中的一些思考和总结,对于具体的步骤不做详细阐述。目前刷题路线和思路参考了《代码随想录》中的题解及卡哥的讲解,在此特别致谢。
本文示例代码所用语言为C# (3.2是java)
1、LeetCode 704 二分查找
- 思路 :
我们使用二分法比顺序进行的优点在于可以分段筛选掉不符合条件的部分、逐步逼近target,不用遍历判断不符合的部分。
这个题目中,要找到升序数组nums 中的 target,我们的思路是,和中值比较,调整最小值、最大值的点(left和right),直到在循环中找到结果或者区间遍历完。故而在循环中确定下次循环的索引边界格外重要。 - 代码:
有两种写法,区别只在于对于区间的界定;
①左闭右闭
初始化end=nums.Length-1,
while循环的条件start<=end时,
nums[mid] > target时,end = mid-1
public int Search(int[] nums, int target) {
int start = 0;
int end = nums.Length-1;
int mid;
while(start <= end){
mid = start + (end - start)/2;
if(nums[mid] > target){
end = mid - 1;
}else if(nums[mid] < target){
start = mid +1;
}else{
return mid;
}
}
return -1;
}
②左闭右开
初始化end=nums.Length,
while循环的条件start<end时,
nums[mid] > target时,end = mid
public int Search(int[] nums, int target) {
int start = 0;
int end = nums.Length;
int mid;
while(start < end){
mid = start + (end - start)/2;
if(nums[mid] > target){
end = mid;
}else if(nums[mid] < target){
start = mid + 1;
}else{
return mid;
}
}
return -1;
}
-
注意点
- 边界问题:想清楚是以什么方式约束指针取区间的。
[0, length-1] 则 left <= right ,right = mid-1;
[0, length) 则 left < right ,right = mid; - 时间复杂度 O(logN) 。总共需要遍历 log2N次,忽略常数2。
- 边界问题:想清楚是以什么方式约束指针取区间的。
-
拓展
- 当元素可重复时,如何定位到与target相等的最小索引
public static int search(int[] nums, int target) { int l = 0; int r = nums.length - 1; while(l <= r){ int mid = l + (r-l)/2; if(nums[mid] >= target){ // 等于target的时候 右指针继续移动,继续寻找最左边的一个 // 如果已达最左的一个,再循环左指针会移动,最终会大于r,取到最左的 r = mid - 1; }else if(nums[mid] < target){ l = mid + 1; } } // 会存在找不到与target相等的情况 if(nums.length == l || nums[l] != target){ return -1; } return l; }
2、LeetCode 27 移除元素
给你一个数组 nums 和一个值 val,你需要 原地 移除所有数值等于 val 的元素,并返回移除后数组的新长度。
不要使用额外的数组空间,你必须仅使用 O(1) 额外空间并 原地 修改输入数组。
元素的顺序可以改变。你不需要考虑数组中超出新长度后面的元素。
- 思路
双指针法
public class Solution {
public int RemoveElement(int[] nums, int val) {
int low = 0;
for(int fast = 0; fast < nums.Length; fast++){
if(nums[fast] != val){
nums[low++] = nums[fast];
}
}
return low;
}
}
注意:fast是用于遍历的指针,low指针代表的是新结果的有效值,是当fast指针对应的值可以拿来赋值时,low指针才去移动;
××错误解答××
public class Solution {
public int RemoveElement(int[] nums, int val) {
int count = 0;
for(int i = 0; i < nums.Length - count; i++){
if(nums[i] == val){
count ++;
}
nums[i] = nums[i+count];
}
return nums.Length - count;
}
}
这个错误解答就是没有意识到,nums[i+count]可能也等于val,这就可能导致当前nums[i]被赋予了错误的值。
3、LeetCode 26 删除有序数组中的重复项
给你一个 升序排列 的数组 nums ,请你 原地 删除重复出现的元素,使每个元素 只出现一次 ,返回删除后数组的新长度。元素的 相对顺序 应该保持 一致 。
由于在某些语言中不能改变数组的长度,所以必须将结果放在数组nums的第一部分。更规范地说,如果在删除重复项之后有 k 个元素,那么 nums 的前 k 个元素应该保存最终结果。
将最终结果插入 nums 的前 k 个位置后返回 k 。
不要使用额外的空间,你必须在 原地 修改输入数组 并在使用 O(1) 额外空间的条件下完成。
public class Solution {
public int RemoveDuplicates(int[] nums) {
int arrIndex = 0;
for(int i = 1; i < nums.Length; i++){
if(nums[i] != nums[arrIndex]){
nums[++arrIndex] = nums[i];
}
}
return arrIndex+1;
}
××错误解法××
public class Solution {
public int RemoveDuplicates(int[] nums) {
int count = 0;
for(int i = 0; i < nums.Length-count; i++){
int t = i;
while(t+1 < nums.Length && nums[t] == nums[t+1]){
t++;
count++;
}
nums[i] = nums[i+count];
}
return nums.Length-count;
}
这个错误解答就是没有意识到,nums[i+count]可能也是重复的不需要的数,这样赋值就导致当前nums[i]被赋予了错误的值。
这题和27是类似的,只是这题的重复元素可能有多个值。
这种移动重复元素的题目,易错点在于,没有遍历到的元素是否有效 是不确定的,所以不能把没遍历到的值赋给“新数组”。
快慢指针法的巧妙之处在于:数组的下标是有规律的,所以用于存放有效数据的数组索引 用慢指针在赋值前自增就行;而快指针就承担了筛选数据的重要角色,当筛选到有效值就赋值给慢指针。
3.2、LeetCode 80 删除有序数组中的重复项II
https://leetcode.cn/problems/remove-duplicates-from-sorted-array-ii/description/
给你一个有序数组 nums ,请你 原地 删除重复出现的元素,使得出现次数超过两次的元素只出现两次 ,返回删除后数组的新长度。
不要使用额外的数组空间,你必须在 原地 修改输入数组 并在使用 O(1) 额外空间的条件下完成。
class Solution {
public int removeDuplicates(int[] nums) {
return process(nums, 2);
}
int process(int[] nums, int k) {
int u = 0;
for (int x : nums) {
if (u < k || nums[u - k] != x) nums[u++] = x;
}
return u;
}
}
作者:宫水三叶
链接:https://leetcode.cn/problems/remove-duplicates-from-sorted-array-ii/solutions/702970/gong-shui-san-xie-guan-yu-shan-chu-you-x-glnq/
class Solution {
public int removeDuplicates(int[] nums) {
if (nums.length < 3) return nums.length;
int i = 2, j = 2;
while (j < nums.length) {
if (nums[j] != nums[i - 2]) {
nums[i++] = nums[j];
}
j++;
}
return i;
}
}
4、 LeetCode 283 移动零
https://leetcode.cn/problems/move-zeroes/description/
给定一个数组 nums,编写一个函数将所有 0 移动到数组的末尾,同时保持非零元素的相对顺序。
public class Solution {
public void MoveZeroes(int[] nums) {
int arrIndex = 0;
int count = 0;
for(int i = 0; i < nums.Length; i++){
if(nums[i] != 0){
nums[arrIndex++] = nums[i];
}else{
count++;
}
}
while(arrIndex < nums.Length){
nums[arrIndex++] = 0;
}
}
}
5、LeetCode 844 比较含退格的字符串
给定 s 和 t 两个字符串,当它们分别被输入到空白的文本编辑器后,如果两者相等,返回 true 。# 代表退格字符。
public class Solution {
public bool BackspaceCompare(string s, string t) {
return Helper(s.ToCharArray()).Equals(Helper(t.ToCharArray()));
}
public String Helper(char[] s){
int slow = 0, fast = 0;
while(fast < s.Length){
if(s[fast] != '#'){
s[slow++] = s[fast++];
}else{
if(slow > 0) slow--;
fast++;
}
}
return new String(s).Substring(0,slow);
}
}
这个计算新string的双指针写法和前几题的思路类似,都是 判断快指针指引的值是否有效, 有效就赋值给慢指针。只不过这题还涉及了 “删除数据可以利用指针的回退”这一巧思。在数组中,我们删除数据一般都是新数据覆盖旧数据,这个指针回退就是运用了这点。
然而,一般用双指针就是因为不想额外使用空间、尽量一次遍历就把结果拿到,所以上面那种写法本质上和“用一个栈处理原字符串,然后将处理后的字符串进行比对”的解题思路是类似的,都是生成新字符串然后去比较。
我们看看完全双指针的解法思路是如何:
一个字符是否会被删掉,只取决于该字符后面的退格符#,而与该字符前面的退格符#无关。
所以我们定义两个指针(index),分别指向两字符串的末尾,用skipS和skipT记录前面要“删除”的字符个数(因为有可能#字符连续出现,所以需要skip来记录要跳过的字符数量)
每次我们让两指针逆序地遍历两字符串,直到两指针各自指向一个有效字符(非#且不被删除的),然后将这两个字符进行比较。重复这一过程直到找到的两个字符不相等,或遍历完字符串为止。
具体如下:
①每次外层循环都 先分别对s和t循环进行处理, 直到 s 和 t 的 index 都要指向一个合法字符
每次我们遍历到一个字符:
- 若该字符为#,则我们需要多删除一个普通字符,skip 加1;
- 若该字符为普通字符:
-
- 若 skip 为 0,则说明当前字符不需要删去;
-
- 若 skip 不为 0,则说明当前字符需要删去,我们让 skip 减 1
②比较s 和 t 的 index 指向的字符是否相等,若不相同则放回false。还有一种情况是,s 和 t 的index 修改完后,一个字符串遍历完毕了,另一个字符串还未遍历完,说明也不相同,返回false;其余情况就为true
代码如下
public class Solution {
public bool BackspaceCompare(string s, string t) {
char[] sArr = s.ToCharArray();
char[] tArr = t.ToCharArray();
int sIndex = sArr.Length-1;
int tIndex = tArr.Length-1;
int skipS = 0;
int skipT = 0;
while(sIndex >= 0 || tIndex >= 0){
while(sIndex >= 0 && (skipS > 0 || sArr[sIndex] == '#')){
if(sArr[sIndex] == '#'){
sIndex--;
skipS++;
}else if(skipS > 0){
skipS--;
sIndex--;
}
if(sIndex < 0)
break;
}
while(tIndex >= 0 && (skipT > 0 || tArr[tIndex] == '#')){
if(tArr[tIndex] == '#'){
tIndex--;
skipT++;
}else if(skipT > 0){
skipT--;
tIndex--;
}
if(tIndex < 0)
break;
}
if(sIndex >= 0 && tIndex >= 0){
if(sArr[sIndex] != tArr[tIndex]){
return false;}
}else if( sIndex >= 0 || tIndex>=0 ){
return false;
}
sIndex--;
tIndex--;
}
return true;
}
}
这个代码有冗余的地方,在寻找 s 和 t 中进行比对的有效字符时,while循环的条件可以去掉判断skip或当前字符,因为在while里还要判断。所以简化如下
public class Solution {
public bool BackspaceCompare(string s, string t) {
char[] sArr = s.ToCharArray();
char[] tArr = t.ToCharArray();
int sIndex = sArr.Length-1;
int tIndex = tArr.Length-1;
int skipS = 0;
int skipT = 0;
while(sIndex >= 0 || tIndex >= 0){
while(sIndex >= 0){
if(sArr[sIndex] == '#'){
sIndex--;
skipS++;
}else if(skipS > 0){
skipS--;
sIndex--;
}else{
break;
}
}
while(tIndex >= 0){
if(tArr[tIndex] == '#'){
tIndex--;
skipT++;
}else if(skipT > 0){
skipT--;
tIndex--;
}else{
break;
}
}
if(sIndex >= 0 && tIndex >= 0){
if(sArr[sIndex] != tArr[tIndex]){
return false;}
}else if( sIndex >= 0 || tIndex>=0 ){
return false;
}
sIndex--;
tIndex--;
}
return true;
}
}
6、 LeetCode 977 有序数组的平方
https://leetcode.cn/problems/squares-of-a-sorted-array/
给你一个按 非递减顺序 排序的整数数组 nums,返回 每个数字的平方 组成的新数组,要求也按 非递减顺序 排序。
public class Solution {
public int[] SortedSquares(int[] nums) {
int index = nums.Length-1;
int left = 0;
int right = nums.Length-1;
int leftR = 0,rightR = 0;
int[] result = new int[nums.Length];
while(left <= right){
leftR = nums[left]*nums[left];
rightR = nums[right]*nums[right];
if(leftR >= rightR){
result[index--] = leftR;
left++;
}else{
result[index--] = rightR;
right--;
}
}
return result;
}
}
错误解法
××错误解法××
public class Solution {
public int[] SortedSquares(int[] nums) {
int left = 0;
int right = nums.Length-1;
int leftR = 0, rightR = 0;
while(right >= 0){
leftR = nums[left]*nums[left];
rightR = nums[right]*nums[right];
if(leftR >= rightR){
nums[left] = nums[right];
nums[right] = leftR;
}else{
nums[right] = rightR;
}
right--;
}
return nums;
}
}
反例:
-7,-6 , 3,4
4, -6,3, 49
3,-6, 16, 49
这个错误解法就是忽略了:虽然原数组是按顺序排好的非递减数组,平方后的最值必然在两端。但是“将排好的数据放到最末尾,若不是从末端取得最值就交换首末两端的数据”这个做法使得,交换后的数据在原数组中已经不一定有序了,那结果自然也就不对了。
本质 就是因为比较依据是平方和,但是交换的数据是原值,导致交换后再平方不一定有序。
7. LeetCode 209 长度最小的子数组
给定一个含有 n 个正整数的数组和一个正整数 target 。
找出该数组中满足其和 ≥ target 的长度最小的 连续子数组 [numsl, numsl+1, …, numsr-1, numsr] ,并返回其长度。如果不存在符合条件的子数组,返回 0 。
使用双指针法解答如下
public class Solution {
public int MinSubArrayLen(int target, int[] nums) {
int start = 0;
int minLength = Int32.MaxValue;
int curResult = 0;
int curLength = 0;
for(int i = 0; i < nums.Length; i++){
curResult += nums[i];
curLength++;
while(curResult - nums[start] >= target){
curResult -= nums[start];
start++;
curLength--;
}
if(curResult >= target){
minLength = curLength < minLength ? curLength : minLength;
}
}
return minLength == Int32.MaxValue ? 0 : minLength;
}
}
但这种写法还不够简洁优雅。更优雅写法如下
public class Solution {
public int MinSubArrayLen(int target, int[] nums) {
int start = 0;
int curResult = 0;
int minLength = Int32.MaxValue;
for(int i = 0; i < nums.Length; i++){
curResult += nums[i];
while(curResult >= target){
minLength = (i-start+1) < minLength ? (i-start+1) : minLength;
curResult -= nums[start];
start++;
}
}
return minLength == Int32.MaxValue ? 0 : minLength;
}
}
优化在两个方面:中间变量length少了、循环内的处理简洁了。
不需要中间变量是因为,i - start + 1 就可以表达实际的长度(处理minLength的时候 i 指针还没有移动,所以要加1)
中间可以这样处理、不用考虑 curResult 减去 nums[start] 、start后移后“滑动窗口”是否还大于target是因为:我们求的是最小长度,能进入这个while循环已经求得一个minLength了,若start++后窗口值的总和小于target,马上就会跳出while循环,接着 i++ 进入下次for循环,这时实际窗口长度(i - start + 1)与刚刚的minLength是相等的。那如果此时curResult 还小于target,说明以该点为末端的窗口长度一定大于刚刚那个,也就没必要在意这个点了,需要向右移到下一个点。
8. LeetCode 59 螺旋矩阵II
https://leetcode.cn/problems/spiral-matrix-ii/description/
给你一个正整数 n ,生成一个包含 1 到 n2 所有元素,且元素按顺时针顺序螺旋排列的 n x n 正方形矩阵 matrix 。
public class Solution {
public int[][] GenerateMatrix(int n) {
int i = 0, j = 0;
int start = 0, end = n-1;
int number = 1;
int[][] result = new int[n][];
for(int r = 0; r < n; r++){
result[r] = new int[n];
}
while(number < n*n){
while(j <= end){
result[i][j++] = number++;
}
j--;
i++;
while(i <= end){
result[i++][j] = number++;
}
i--;
j--;
while(j >= start){
result[i][j--] = number++;
}
j++;
i--;
while(i > start){
result[i--][j] = number++;
}
j++;
i++;
start++;
end--;
}
if(n%2 == 1){
result[i][j] = number;
}
return result;
}
}
用while循环嵌套写起来很方便,循环一下回退一下就行,但是代码看起来还是有点长,里层绕圈的while可以用for循环替代。
public class Solution {
public int[][] GenerateMatrix(int n) {
int start = 0, end = n-1;
int number = 1;
int[][] result = new int[n][];
for(int r = 0; r < n; r++){
result[r] = new int[n];
}
while(number < n*n){
for(int j = start; j < end; j++) result[start][j] = number++;
for(int i = start; i < end; i++) result[i][end] = number++;
for(int j = end; j > start ; j--) result[end][j] = number++;
for(int i = end; i > start ; i--) result[i][start] = number++;
start++;
end--;
}
if(n%2 == 1){
result[n/2][n/2] = number;
}
return result;
}
}
总结
- 边界处理很重要。往往是这样的细节决定结果对错。写的时候一定要想清楚是从哪到哪。
- 移除元素的题,如果用双指针,优先考虑 处理不符合的情况,不符合就跳过,符合才给慢指针赋值。