【LeetCode Cookbook(C++ 描述)】一刷数组(Array)

本系列文章仅是 GitHub 大神 @halfrost 的刷题笔记 《LeetCode Cookbook》的提纲以及示例、题集的 C++转化。原书请自行下载学习。
本篇文章涉及新手应该优先刷的几道经典数组算法题,以后会更新“二刷”“三刷”等等。

LeetCode #27: Remove Element 移除元素

LeetCode-27
给你一个数组 nums 和一个值 val,你需要原地in-place)移除所有数值等于 val 的元素。元素的顺序可能发生改变。然后返回 nums 中与 val 不同的元素的数量。

假设 nums 中不等于 val 的元素数量为 k,要通过此题,您需要执行以下操作:

  • 更改 nums 数组,使 nums 的前 k 个元素包含不等于 val 的元素。nums 的其余元素和 nums 的大小并不重要。
  • 返回 k

换言之,我们只需要保证数组索引在   [ 0 , k ) \ [0, k)  [0,k) 内的元素对应的值均不同于 val 即可。

所谓原地,就是我们不能 new 一个新的数组,必须在原有数组上进行修改。我们无法直接删除数组中的某一个元素,数组的元素在内存地址中是连续的,只能覆盖。

暴力解法

这种方法很容易想到,也是最为直观的,即使用两层嵌套循环,首先遍历数组,一旦发现需要移除的元素,就将剩余的数组整体前移一位,直接将需要移除的元素覆盖掉。

class Solution {
public:
    int removeElement(vector<int>& nums, int val) {
        int size = nums.size();
        for (int i = 0; i < size; i++) {
            if (nums[i] == val) {
                for (int j = i + 1; j < size; j++) 
                    nums[j - 1] = nums[j];
                i--; 	//下标i以后的数值都向前移动了一位,i也向前移动一位
                size--; //此时数组的大小 - 1
            }
        }
        return size;
    }
};

对暴力算法进行复杂度分析,不难得出该算法的时间复杂度为   O ( n 2 ) \ O(n^2)  O(n2),对于一般情况来说,这并不是一个理想的结果。程序提交结果如下:暴力解法
完全可以 AC,但是存在优化的空间。在这种算法中,将数组剩余元素整体前移的行为使得时间复杂度急剧增大,这一行为并不是完成这道题目所必需的,我们完全可以将需要删除的元素替换为下一个不同于 val 的元素,返回值 k 则始终指向不需要移除的元素的下一个元素,那么经过一次遍历后,可以返回一个满足题意的 k 值。

快慢指针解法(Tow-Pointer Technique

所谓快慢指针,就是两个指针 slowfast 同向而行,一快一慢。在数组上并没有严格意义上的指针,但我们可以将数组中的索引比作“指针”,快指针 fast 指向当前要和 val 对比的元素,慢指针 slow 则指向将被赋值的位置。
快慢指针我们让慢指针 slow 走在后面,快指针 fast 走在前面探路,找到一个需要移除的元素就赋值给 nums[slow] 并让 slow 前进一步。假设 val = 2:一前一后第一次替换
第二次替换
在这里插入图片描述
总体来看,思路大抵是这样的:

  • 遍历数组。如果 fast 指针指向的元素 nums[fast] ≠ \neq = val,则 nums[fast] 是输出数组的元素,将其赋值到 nums[slow] 的位置,slowfast 同时向后移动一位。
  • 如果 nums[fast] = val,证明当前 nums[fast] 是要移除的元素,fast 向后移动一位。
  • fast 遍历完整个数组,循环终止,slow 的值就是剩余数组的长度。

以此类推,就可以保证从 nums[0]nums[slow](不包含后者)均为不同于 val 的元素。在上述图中所给出例子中,最终函数返回 slow,截取出数组   [ 0 , 1 , 3 , 0 , 4 ] \ [0, 1, 3, 0, 4]  [0,1,3,0,4],符合题意。代码实现如下:

class Solution {
public:
    int removeElement(vector<int>& nums, int val) {
        int slow = 0;
        for (int fast = 0; fast < nums.size(); fast++)

            if (nums[fast] != val)
                nums[slow++] = nums[fast];
            
        return slow;
    }
};

对此种算法进行复杂度分析,该算法的时间复杂度为   O ( n ) \ O(n)  O(n)
最优解法

更多例子

让我们原地修改数组的题目,一般均可使用快慢指针技巧秒杀。
#26
给你一个非严格递增排列的数组 nums ,请你原地删除重复出现的元素,使每个元素只出现一次,返回删除后数组的新长度。元素的相对顺序应该保持一致。然后返回 nums 中唯一元素的个数。

考虑 nums 的唯一元素的数量为 k ,你需要做以下事情确保你的题解可以被通过:

  • 更改数组 nums ,使 nums 的前 k 个元素包含唯一元素,并按照它们最初在 nums 中出现的顺序排列。
  • nums 的其余元素与 nums 的大小不重要。
  • 返回 k

思路如下:

  • 遍历数组。如果 fast 指针指向的元素 nums[fast] ≠ \neq = nums[slow],则 nums[fast] 是输出数组的元素,slow 向后移动一位,将其赋值到 nums[slow] 的位置, fast 向后移动一位。
  • 如果 nums[fast] = nums[slow],证明当前 nums[fast] 是重复的元素,fast 向后移动一位。
  • fast 遍历完整个数组,循环终止,slow 的值为不重复数组的最后一个元素的索引,应返回剩余数组的长度 slow + 1

本题的思路与#27题之所以不太一样,主要是因为#27题引入了外部参数 val 进行比对,而本题是基于数组内元素进行去重。

class Solution {
public:
    int removeDuplicates(vector<int>& nums) {
        int slow = 0;
        for (int fast = 0; fast < nums.size(); fast++)

            if (nums[slow] != nums[fast])
                nums[++slow] = nums[fast];  //易错点
            
        return slow + 1;  //注意返回值应为数组长度而非最后一位索引
    }
};

在这里插入图片描述

✅ 拓展:一个有序单链表的去重,也可以使用快慢指针法解决。
#83
给定一个已排序的链表的头 head , 删除所有重复的元素,使每个元素只出现一次。返回已排序的链表。

其实和数组去重是一致的,唯一的区别是把数组赋值操作变成操作指针而已。

class Solution {
public:
    ListNode* deleteDuplicates(ListNode* head) {
        if (head == nullptr) return nullptr;
        ListNode* slow = head;
        ListNode* fast = head;
        while (fast != nullptr)
        {
            if (slow->val != fast->val)
            {
                slow->next = fast;  //当前元素的下一个元素设置为下一个不相等的元素
                slow = slow->next;  //当前元素向后移动
            }
            fast = fast->next;      //下一个元素向后移动
        }
        slow->next = nullptr;       //断开与后面重复元素的连接
        return head;
    }
};

#283
给定一个数组 nums ,编写一个函数将所有 0 移动到数组的末尾,同时保持非零元素的相对顺序。

请注意 ,必须在不复制数组的情况下原地对数组进行操作。

对于这道题,我们完全可以复用#27题的函数 removeElement ,先删除所有的 0 ,再在数组尾部补全所有的 0

class Solution {
public:
    void moveZeroes(vector<int>& nums) {
        int k = removeElement(nums, 0);

        for (; k < nums.size(); k++)
            nums[k] = 0;  
    }

    int removeElement(vector<int>& nums, int val) {  //#27题原函数
        int slow = 0;
        for (int fast = 0; fast < nums.size(); fast++)

            if (nums[fast] != val)
                nums[slow++] = nums[fast];
            
        return slow;
	}
};

LeetCode #59:Spiral Matrix II 螺旋矩阵 II

#59
给你一个正整数 n ,生成一个包含 1 到 n2 所有元素,且元素按顺时针顺序螺旋排列的 n x n 正方形矩阵 matrix

一道难度中等、面试出现概率极高的考察编程能力的题目,本身不涉及算法,但是容易出现 bug。

基本思路如下(上 ⇒ 右 ⇒ 下 ⇒ 左):

  • 上:从左到右填充。
  • 右:从上到下填充。
  • 下:从右到左填充。
  • 左:从下到上填充。

tutorials

简单解法

非常经典的模拟法,直接用编程语言直白地表现出来即可;注意随时画图,避免出现错误。以四个边界作为每行每列遍历填充的起点与终点,每完成一步就缩小边界规模。

基本思路中的 4 步走完后,需要全部遍历并填充,直到 n2 的数全部填完。

class Solution {
public:
    vector<vector<int>> generateMatrix(int n) {
        vector<vector<int>> matrix(n, vector<int>(n));  //初始化二维数组
        int cnt = 1;  //初始化指针
        int upper_bound = 0, lower_bound = n - 1;  //初始化四个矩阵边界
        int left_bound = 0, right_bound = n - 1;

        while (cnt <= n * n)  //重复基本 4 步
        {
        	//从左至右填充最上行
            for (int i = left_bound; i <= right_bound; i++) matrix[upper_bound][i] = cnt++;
            upper_bound++;    //上边界向下移动

            //从上到下填充最右列
            for (int i = upper_bound; i <= lower_bound; i++) matrix[i][right_bound] = cnt++;
            right_bound--;    //右边界向左移动

            //从右至左填充最下行
            for (int i = right_bound; i >= left_bound; i--) matrix[lower_bound][i] = cnt++;
            lower_bound--;    //下边界向上移动

            //从下到上填充最左列
            for (int i = lower_bound; i >= upper_bound; i--) matrix[i][left_bound] = cnt++;
            left_bound++;    //左边界向右移动
        }
        return matrix;
    }
};

显然,该算法的时间复杂度为   O ( n 2 ) \ O(n^2)  O(n2),由于需要维护一个大小为   n 2 \ n^2  n2 的二维数组,则空间复杂度也为   O ( n 2 ) \ O(n^2)  O(n2)

@halfrost 解法

大神 @halfrost 的解法可能会略显繁琐(可能也是因为 Go 语言 C++ 化的原因),再这里先挖个坑,以后有时间再来填上相关的笔记与思路。可以 AC 通过,最快运行时间 0 ms

  • 补充对于@halfrost 解法的笔记
  • 继续核查 C++描述版本的准确性(是否还原大神的做法)

新版本:

class Solution {
public:
    vector<vector<int>> generateMatrix(int n) {
        vector<vector<int>> res(n, vector<int>(n, 0));
    	int num = 1;
    	int row = 0, col = 0;
    	int direction = 0; // 0: 向右, 1: 向下, 2: 向左, 3: 向上

    	vector<pair<int, int>> dirs = {
	        {0, 1}, // 向右
	        {1, 0}, // 向下
	        {0, -1}, // 向左
	        {-1, 0} // 向上
    	};

    	for (int i = 0; i < n * n; ++i) {
        	res[row][col] = num++;
			// 检查下一个位置是否有效
        	int nextRow = row + dirs[direction].first;
        	int nextCol = col + dirs[direction].second;
        	// 如果下一个位置无效(越界或已访问),则改变方向
        	if (nextRow < 0 || nextRow >= n || nextCol < 0 || nextCol >= n || res[nextRow][nextCol] != 0) {
            	// 使用switch语句简化方向更新
            	switch (direction) {
                	case 0: // 当前向右,无效则尝试向下
                    	if (row + 1 < n && res[row + 1][col] == 0) direction = 1;
						else direction = 3; // 否则向上
                    	break;
                	case 1: // 当前向下,无效则尝试向左
                    	if (col - 1 >= 0 && res[row][col - 1] == 0) direction = 2;
						else direction = 0; // 否则向右
                   		break;
                	case 2: // 当前向左,无效则尝试向上
                    	if (row - 1 >= 0 && res[row - 1][col] == 0) direction = 3;
                    	else direction = 1; // 否则向下
                        break;
                	case 3: // 当前向上,无效则尝试向右
                    	if (col + 1 < n && res[row][col + 1] == 0) direction = 0;
                     	else direction = 2; // 否则向左
	                    break;
            	}	
            	// 不需要再单独更新 nextRow 和 nextCol,因为已经确定了新的方向
            	// 直接使用 dirs[direction]
	            row += dirs[direction].first;
	            col += dirs[direction].second;
        	} else {
	            // 如果下一个位置有效,则直接移动到该位置
	            row = nextRow;
	            col = nextCol;
        	}
    	}

    	return res;
	}
};

老版本(未还原 switch 语句):

class Solution {
public:
    vector<vector<int>> generateMatrix(int n) {
        vector<vector<int>> res(n, vector<int>(n, 0));
        vector<vector<bool>> visited(n, vector<bool>(n, false));

        int num = 1;
        int row = 0, col = 0;
        int direction = 0; // 0: right, 1: down, 2: left, 3: up

        vector<pair<int, int>> dirs = {
            {0, 1}, // right
            {1, 0}, // down
            {0, -1}, // left
            {-1, 0} // up
        };

        for (int i = 0; i < n * n; ++i) {
            res[row][col] = num++;
            visited[row][col] = true;

            int nextRow = row + dirs[direction].first;
            int nextCol = col + dirs[direction].second;
            // 避免越界
            if (nextRow < 0 || nextRow >= n || nextCol < 0 || nextCol >= n || visited[nextRow][nextCol]) {
                // 更改方向
                direction = (direction + 1) % 4;
                // 移动到新方向上的下一个有效位置
                nextRow = row + dirs[direction].first;
                nextCol = col + dirs[direction].second;
            }   

        row = nextRow;
        col = nextCol;
        }

        return res;
	}
};

LeetCode #209:Minimum Size Subarray Sum 长度最小的子数组

#209
给定一个含有 n 个正整数的数组和一个正整数 target 。找出该数组中满足其总和大于等于 target 的长度最小子数组   [ n u m s l , n u m s l + 1 , . . . , n u m s r − 1 , n u m s r ] \ [nums_l, nums_{l+1}, ..., nums_{r-1}, nums_r]  [numsl,numsl+1,...,numsr1,numsr],并返回其长度。如果不存在符合条件的子数组,返回 0

暴力查找

遍历数组 nums,每次将一个下标的元素作为子数组的开始,接下来从下标 i 开始向后遍历,找到最小下标 j ,使得从 nums[i]nums[j] 之和大于等于 target ,更新子数组的最小长度。

class Solution {
public:
    int minSubArrayLen(int target, vector<int>& nums) {
        int l = nums.size();  							//获取数组长度
        int min_len = l + 1;  							//初始化最小长度为数组长度 + 1
        for (int i = 0; i < l; i++) {
            int cur_sum = 0;  							//遍历过的总和,该值要与target进行比较
            for (int j = i; j < l; j++) {
                cur_sum += nums[j]; 					//累加当前元素到和中
                if (cur_sum >= target) {				// 若和大于等于目标值
                    min_len = min(min_len, j - i + 1);  // 更新最小长度
                    break;  							// 跳出内层循环
                }
            }
        }
        return min_len == l + 1 ? 0 : min_len;  	    // 若最小长度未被修改,返回0,否则返回最小长度
    }
};

该算法运用了双重嵌套循环,时间复杂度为   O ( n 2 ) \ O(n^2)  O(n2),对于题目给出最高   1 0 5 \ 10^5  105 数量级的数据,显然过于缓慢,以至于超出 AC 时间限制。

滑动窗口算法

引入:左右指针技巧

处理数组和链表相关问题时,双指针技巧是经常用到的。双指针技巧主要分为两类:一个是上述已然涉及的快慢指针,另一个则是左右指针

所谓左右指针,就是两个指针相向而行或者相背而行。实际上,二分搜索算法就是对于左右指针的经典应用:

int binarySearch(vector<int>& nums, int target) {
    // 一左一右两个指针相向而行
    int left = 0, right = nums.size() - 1;
    while(left <= right) {
        int mid = (right + left) / 2;
        if(nums[mid] == target)
            return mid; 
        else if (nums[mid] < target)
            left = mid + 1; 
        else if (nums[mid] > target)
            right = mid - 1;
    }
    return -1;
}

#167
给你一个下标从 1 开始的整数数组 numbers ,该数组已按非递减顺序排列,请你从数组中找出满足相加之和等于目标数 target 的两个数。如果设这两个数分别是 numbers[index1]numbers[index2] ,则 1 ≤ \leq index1   < \ <  < index2 ≤ \leq numbers.length

以长度为 2 的整数数组 [ index1 , index2 ] 的形式返回这两个整数的下标 index1index2

你可以假设每个输入只对应唯一的答案,而且你不可以重复使用相同的元素。

你所设计的解决方案必须只使用常量级的额外空间。

只要数组有序,就应该想到双指针技巧。这道题的解法有点类似于二分搜索,通过调节 leftright 就可以调整 sum 的大小:

class Solution {
public:
    vector<int> twoSum(vector<int>& numbers, int target) {
        // 一左一右两个指针相向而行
        int left = 0, right = numbers.size() - 1;
        while (left < right) {
            int sum = numbers[left] + numbers[right];
            if (sum == target) return {left + 1, right + 1};  // 题目要求的索引是从 1 开始的
            else if (sum < target) left++; 		// 让 sum 大一点
            else if (sum > target) right--; 	// 让 sum 小一点
        }
        return {-1, -1};
    }
};

#344
一般编程语言都会提供 reverse 函数来反转字符串,其实这个函数的原理非常简单:

class Solution {
public:
    void reverseString(vector<char>& s) {
        // 一左一右两个指针相向而行
        int left = 0, right = s.size() - 1;
        while (left < right) {
            // 交换 s[left] 和 s[right]
            char temp = s[left];
            s[left] = s[right];
            s[right] = temp;
            left++;
            right--;
        }
    }
};

在这里插入图片描述
给你一个字符串 s ,找到 s 中最长的回文子串(正反序一致)。

找回文串的难点在于,回文串的长度可能是奇数也可能是偶数。如果回文串长度为奇数,则它有一个中心字符;如果回文串长度为偶数,我们可以认为它有两个中心字符。因此,解决问题的核心在于从中心向两端扩散的左右指针技巧,这样我们就可以十分便捷地表示回文串的本质:

// 在 s 中寻找以 s[l] 和 s[r] 为中心的最长回文串
string palindrome(string s, int l, int r) {
    // 防止索引越界
    while (l >= 0 && r < s.length()
            && s[l] == s[r]) {
        // 双指针,向两边展开
        l--; r++;
    }
    // 返回以 s[l] 和 s[r] 为中心的最长回文串
    return s.substr(l + 1, r - l - 1);
}

那么,根据我们以上达成有关于奇偶数的共识,如果输入相同的 lr ,就相当于寻找长度为奇数的回文串;如果输入相邻的 lr ,就相当于寻找长度为偶数的回文串。综上所述,总体思路如下:

  • 找到以 s[i] 为中心的回文子串
  • 找到以 s[i]s[i+1] 为中心的回文子串
  • 更新答案

则整个题目的代码实现如下:

class Solution {
public:
    string longestPalindrome(string s) {
        string res = "";
        for (int i = 0; i < s.length(); i++) {
            // 以 s[i] 为中心的最长回文子串
            string s1 = palindrome(s, i, i);
            // 以 s[i] 和 s[i+1] 为中心的最长回文子串
            string s2 = palindrome(s, i, i + 1);
            res = res.length() > s1.length() ? res : s1;
            res = res.length() > s2.length() ? res : s2;
        }
        eturn res;
    }

    string palindrome(string s, int l, int r) {
        // 防止索引越界
        while (l >= 0 && r < s.length()
                && s[l] == s[r]) {
            // 双指针,向两边展开
            l--; r++;
        }
        // 返回以 s[l] 和 s[r] 为中心的最长回文串
        return s.substr(l + 1, r - l - 1);
    }
};

问题的解决

滑动窗口算法主要用于解决子数组问题,比如寻找符合某个条件的最长或最短子数组。

所谓滑动窗口,其实是维护一个可以扩大和缩小的、囊括数据的“窗口”,不断滑动,从而更新答案,获得结果。利用这一特性,对于#209题的求给定数组中满足其总和大于等于 target 的长度最小子数组的长度,我们可以得出以下解题思路:

  • 使用左右指针 leftrightleftright 之间的长度即为滑动窗口的大小(即连续数组的大小)。
  • 如果滑动窗口内的值 sum ≥ \geq target , 维护连续数组的最短长度,left 向右移动,缩小滑动窗口。
  • 如果滑动窗口内的值 sum   < \ <  < target ,则 right 向右移动,扩大滑动窗口。
class Solution {
public:
    int minSubArrayLen(int target, vector<int>& nums) {
        int left = 0, right = 0, sum = 0;
        int min_length = INT_MAX;

        for (; right < nums.size(); right++) {  //也可使用 while 循环并自增 right
            sum += nums[right];
            while (sum >= target)
            {
                //取之前窗口长度与当前窗口长度最短的
                min_length = min(min_length, right - left + 1);
                //去掉左侧的数
                sum -= nums[left];
                //缩小窗口
                left++;
            }
            
        }
        return min_length == INT_MAX ? 0 : min_length;
        
    }
};

深入“滑动窗口”:如何聪明地穷举

该算法的大致逻辑如下:

int left = 0, right = 0;

while (right < nums.size()) {
    // 增大窗口
    window.add(nums[right]);
    right++;
    
    while (/*window needs shrink*/) {
        // 缩小窗口
        window.remove(nums[left]);
        left++;
    }
}

基于此种逻辑,指针 leftright 不会回退(它们的值只增不减),字符串或数组中的每个元素都只会进入窗口一次,随后被移除窗口一次,不存在某些元素多次进出窗口,故算法的时间复杂度就和数据长度成正比,为   O ( n ) \ O(n)  O(n)。值得注意的是,滑动窗口并不能穷举出所有子串(要想如此,只能使用嵌套 for 循环),但是对于该算法所适用的情景下,不需要进行穷举就能得出答案。滑动窗口就是这样一个算法模板,对枚举过程剪枝优化,避免冗杂的计算

@labuladong 大佬提供了一份实用的、可替换的代码模板:

/* 滑动窗口算法框架 */
void slidingWindow(string s) {
    // 用合适的数据结构记录窗口中的数据,根据具体场景变通
    // 比如说,我想记录窗口中元素出现的次数,就用 map
    // 我想记录窗口中的元素和,就用 int
    unordered_map<char, int> window;
    
    int left = 0, right = 0;
    while (right < s.size()) {
        // c 是将移入窗口的字符
        char c = s[right];
        window.add(c);
        // 增大窗口
        right++;
        // 进行窗口内数据的一系列更新
        ...

        /*** debug 输出的位置 ***/
        // 注意在最终的解法代码中不要 print
        // 因为 IO 操作很耗时,可能导致超时
        printf("window: [%d, %d)\n", left, right);
        /********************/
        
        // 判断左侧窗口是否要收缩
        while (left < right && window needs shrink) {
            // d 是将移出窗口的字符
            char d = s[left];
            window.remove(d)
            // 缩小窗口
            left++;
            // 进行窗口内数据的一系列更新
            ...
        }
    }
}

框架中两处 ... 表示的是更新窗口的地方,分别是扩大和缩小窗口的更新数据操作,在具体题目中则体现为特定的代码逻辑。

#76
给你一个字符串 s 、一个字符串 t 。返回 s 中涵盖 t 所有字符的最小子串。如果 s 中不存在涵盖 t 所有字符的子串,则返回空字符串 ""

注意:

  • 对于 t 中重复字符,我们寻找的子字符串中该字符数量必须不少于 t 中该字符数量。
  • 如果 s 中存在这样的子串,我们保证它是唯一的答案。

对此题应用滑动窗口算法,思路如下:

  1. 在字符串 s 中使用左右指针技巧,初始化 left = right = 0 ,把索引左闭右开区间(便于边界处理) [left, right) 称为一个窗口。
  2. 不断增加 right 指针扩大 [left, right) 窗口,直到窗口内的字符串包含了字符串 t 的全部字符。
  3. 停止增加 right ,转而不断增加 left 以缩小窗口 [left, right) ,直到不再符合要求;同时,每次增加 left,都要更新一轮结果。
  4. 重复第 2 步和第 3 步,直到 right 指针到达字符串 s 的尽头。

简而言之,这一思路的第 2 步在寻找一个可行解,而第 3 步则在优化这一可行解,二者相辅相成,最终拟合出最优解

现在我们来运用先前介绍的代码框架:

首先,初始化两个哈希表 windowneed ,分别记录窗口中的字符以及需要凑齐的字符:

unordered_map<char, int> need, window;
for (char c : t) need[c]++;

其次,进行左右指针初始化:

int left = 0, right = 0;
int valid = 0; 
while (right < s.size()) {
    // 开始滑动
}

其中 valid 表示窗口中满足 need 条件的字符个数,一旦 valid == need.size 成立,则说明窗口已完全覆盖字符串 t

根据上述思路,如果一个字符进入或移出窗口,应该增加或减少 window 计数器;当 valid 满足 need 时应该收缩窗口,同时更新结果。对照着模板,总的代码实现如下:

class Solution {
public:
    string minWindow(string s, string t) {
        unordered_map<char, int> window, need;
        for (char c : t) need[c]++;

        int left = 0, right = 0;
        int valid = 0;
        int start = 0, len = INT_MAX;
        while (right < s.size()) {
            // c 是将移入窗口的字符
            char c = s[right];
            //扩大窗口
            right++;
            //进行窗口内数据的一系列更新
            if (need.count(c))
            {
                window[c]++;
                if (window[c] == need[c]) valid++;
            }
            // 判断左侧窗口是否要收缩
            while (valid == need.size()) {
                //更新最小覆盖子串
                if (right - left < len)
                {
                    start = left;
                    len = right - left;
                }
                // d 是将移出窗口的字符
                char d = s[left];
                // 缩小窗口
                left++;
                // 进行窗口内数据的一系列更新
                if (need.count(d))
                {
                    if (window[d] == need[d]) valid--;
                    window[d]--;               
                }
            }
        }
        return len == INT_MAX ? "" : s.substr(start, len);
    }
};

利用这个模板,许多相似的题目都可以做出来。

LeetCode #977:Squares of a Sorted Array 有序数组的平方

#977

这也是左右指针技巧的简单应用。我们可以利用原数组的有序性来减少时间复杂度,平方后的极值必然出现在数组两端,同时考虑负数的情况,绝对值可以更准确地反映平方后两端的大小。因此,可以用 2 个指针分别指向原数组的首尾,分别计算平方值,比较两者大小,较大的放在新数组尾端,随后较大的数所对应的指针移动,直至两个指针相撞,平方后的排序就完成了。

class Solution {
public:
    vector<int> sortedSquares(vector<int>& nums) {
        int left = 0, right = nums.size() - 1;
        vector<int> squared(nums.size());
        int site = nums.size() - 1;

        while (left <= right)
        {
            if (nums[left] * nums[left] < nums[right] * nums[right])
            {
                squared[site] = nums[right] * nums[right];
                right--;
            }
            else {
                squared[site] = nums[left] * nums[left];
                left++;
            }
            site--;
        }
        
        return squared;
    }
};

@halfrost 大神也提供了其他解法,利用 sort() 内置函数:

class Solution {
public:
    vector<int> sortedSquares(vector<int>& nums) {
        vector<int> squared(nums.size());
    	for (int i = 0; i < nums.size(); ++i) {
        	squared[i] = nums[i] * nums[i]; // 计算平方
    	}
    	sort(squared.begin(), squared.end()); // 对平方后的结果进行排序
    	return squared; 
    }
};

呜啊?

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值