一. Move Zeros
例题
给定一个数组 nums,编写一个函数将所有 0 移动到数组的末尾,同时保持非零元素的相对顺序。
示例:
输入: [0,1,0,3,12]
输出: [1,3,12,0,0]
说明:
必须在原数组上操作,不能拷贝额外的数组。
尽量减少操作次数。
- 解法一
#include <iostream>
#include <vector>
using namespace std;
// 283. Move Zeroes
// https://leetcode.com/problems/move-zeroes/description/
// 时间复杂度: O(n)
// 空间复杂度: O(n)
class Solution {
public:
void moveZeroes(vector<int>& nums) {
vector<int> nonZeroElements;
// 将vec中所有非0元素放入nonZeroElements中
for(int i = 0 ; i < nums.size() ; i ++)
if(nums[i])
nonZeroElements.push_back(nums[i]);
// 将nonZeroElements中的所有元素依次放入到nums开始的位置
for(int i = 0 ; i < nonZeroElements.size() ; i ++)
nums[i] = nonZeroElements[i];
// 将nums剩余的位置放置为0
for(int i = nonZeroElements.size() ; i < nums.size() ; i ++)
nums[i] = 0;
}
};
int main() {
int arr[] = {0, 1, 0, 3, 12};
vector<int> vec(arr, arr + sizeof(arr)/sizeof(int));
Solution().moveZeroes(vec);
for(int i = 0 ; i < vec.size() ; i ++)
cout << vec[i] << " ";
cout << endl;
return 0;
}
二. 对move zero进行优化
优化1: 在原数组中完成, 不去开辟新的空间
// 原地(in place)解决该问题
// 时间复杂度: O(n)
// 空间复杂度: O(1)
class Solution {
public:
void moveZeroes(vector<int>& nums) {
int k = 0; // nums中, [0...k)的元素均为非0元素
// 遍历到第i个元素后,保证[0...i]中所有非0元素
// 都按照顺序排列在[0...k)中
for(int i = 0 ; i < nums.size() ; i ++)
if(nums[i])
nums[k++] = nums[i];
// 将nums剩余的位置放置为0
for(int i = k ; i < nums.size() ; i ++)
nums[i] = 0;
}
};
优化2: 在寻找非0元素的同时, 把0元素丢到最后
// 原地(in place)解决该问题
// 时间复杂度: O(n)
// 空间复杂度: O(1)
class Solution {
public:
void moveZeroes(vector<int>& nums) {
int k = 0; // nums中, [0...k)的元素均为非0元素
// 遍历到第i个元素后,保证[0...i]中所有非0元素
// 都按照顺序排列在[0...k)中
// 同时, [k...i] 为 0
for(int i = 0 ; i < nums.size() ; i ++)
if(nums[i])
swap(nums[k++] , nums[i]);
}
};
优化3: 如果全为非0元素, 防止自己和自己交换操作
// 原地(in place)解决该问题
// 时间复杂度: O(n)
// 空间复杂度: O(1)
class Solution {
public:
void moveZeroes(vector<int>& nums) {
int k = 0; // nums中, [0...k)的元素均为非0元素
// 遍历到第i个元素后,保证[0...i]中所有非0元素
// 都按照顺序排列在[0...k)中
// 同时, [k...i] 为 0
for(int i = 0 ; i < nums.size() ; i ++)
if(nums[i])
if(k != i)
swap(nums[k++] , nums[i]);
else
k ++;
}
};
二. 三路快排partion思路的应用 Sort Color
leetcode 75号题
给定一个包含红色、白色和蓝色,一共 n 个元素的数组,
原地对它们进行排序,使得相同颜色的元素相邻,并按照红色、白色、蓝色顺序排列。
此题中,我们使用整数 0、 1 和 2 分别表示红色、白色和蓝色。
注意:
不能使用代码库中的排序函数来解决这道题。
示例:
输入: [2,0,2,1,1,0]
输出: [0,0,1,1,2,2]
进阶:
一个直观的解决方案是使用计数排序的两趟扫描算法。
首先,迭代计算出0、1 和 2 元素的个数,然后按照0、1、2的排序,重写当前数组。
你能想出一个仅使用常数空间的一趟扫描算法吗?
思路
- 观察到题中数组中的元素种类很少,只有0 1 2, 我们可以使用计数排序
// 计数排序的思路
// 对整个数组遍历了两遍
// 时间复杂度: O(n)
// 空间复杂度: O(k), k为元素的取值范围
class Solution {
public:
void sortColors(vector<int> &nums) {
int count[3] = {0}; // 存放0, 1, 2三个元素的频率
for(int i = 0 ; i < nums.size() ; i ++){
assert(nums[i] >= 0 && nums[i] <= 2);
count[nums[i]] ++;
}
int index = 0;
for(int i = 0 ; i < count[0] ; i ++)
nums[index++] = 0;
for(int i = 0 ; i < count[1] ; i ++)
nums[index++] = 1;
for(int i = 0 ; i < count[2] ; i ++)
nums[index++] = 2;
}
};
优化
- 仅使用常数空间的一趟扫描算法: 使用三路快排
// 三路快速排序的思想
// 对整个数组只遍历了一遍
// 时间复杂度: O(n)
// 空间复杂度: O(1)
class Solution {
public:
void sortColors(vector<int> &nums) {
int zero = -1; // [0...zero] == 0
int two = nums.size(); // [two...n-1] == 2
for(int i = 0 ; i < two ; ){
if(nums[i] == 1)
i ++;
else if (nums[i] == 2)
swap( nums[i] , nums[--two]);
else{ // nums[i] == 0
assert(nums[i] == 0);
swap(nums[++zero] , nums[i++]);
}
}
}
};
三. 对撞指针 Two Sum II - Input Array is Sorted
给定一个已按照升序排列 的有序数组,找到两个数使得它们相加之和等于目标数。
函数应该返回这两个下标值 index1 和 index2,其中 index1 必须小于 index2。
说明:
返回的下标值(index1 和 index2)不是从零开始的。
你可以假设每个输入只对应唯一的答案,而且你不可以重复使用相同的元素。
示例:
输入: numbers = [2, 7, 11, 15], target = 9
输出: [1,2]
解释: 2 与 7 之和等于目标数 9 。因此 index1 = 1, index2 = 2 。
解决方法
O(nlogn)的解决方法
- 数组的特点-- 有序, 所以可以使用二分搜索
// 二分搜索法
// 时间复杂度: O(nlogn)
// 空间复杂度: O(1)
class Solution {
public:
vector<int> twoSum(vector<int>& numbers, int target) {
assert(numbers.size() >= 2);
// assert(isSorted(numbers));
for(int i = 0 ; i < numbers.size() - 1 ; i ++){
int j = binarySearch(numbers, i+1, numbers.size()-1, target - numbers[i]);
if(j != -1){
int res[2] = {i+1, j+1};
return vector<int>(res, res+2);
}
}
throw invalid_argument("the input has no solution");
}
private:
int binarySearch(const vector<int> &nums, int l, int r, int target){
assert(l >= 0 && l < nums.size());
assert(r >= 0 && r < nums.size());
while(l <= r){
int mid = l + (r - l)/2;
if(nums[mid] == target)
return mid;
if(target > nums[mid])
l = mid + 1;
else
r = mid - 1;
}
return -1;
}
bool isSorted(const vector<int>& numbers){
for(int i = 1 ; i < numbers.size() ; i ++)
if(numbers[i] < numbers[i-1])
return false;
return true;
}
};
int main() {
int nums[] = {2, 7, 11, 15};
vector<int> vec(nums, nums + sizeof(nums) / sizeof(int));
int target = 9;
vector<int> res = Solution().twoSum(vec, target);
for(int i = 0 ; i < res.size() ; i ++)
cout << res[i] << " ";
cout << endl;
return 0;
}
O(n)的解决方法
1. 开始 i=0 j=numbers.size-1
2. 如果 numbers[i]+numbers[j] < target 则i++
3. 如果 numbers[i]+numbers[j] > target 则j--
4. 直到找到i和j使得number[i]+number[j]=target, 或者i>=j即不存在
// 对撞指针
// 时间复杂度: O(n)
// 空间复杂度: O(1)
class Solution {
public:
vector<int> twoSum(vector<int>& numbers, int target) {
assert(numbers.size() >= 2);
// assert(isSorted(numbers));
int l = 0, r = numbers.size() - 1;
while(l < r){
if(numbers[l] + numbers[r] == target){
int res[2] = {l+1, r+1};
return vector<int>(res, res+2);
}
else if(numbers[l] + numbers[r] < target)
l ++;
else // numbers[l] + numbers[r] > target
r --;
}
throw invalid_argument("the input has no solution");
}
private:
bool isSorted(const vector<int>& numbers){
for(int i = 1 ; i < numbers.size() ; i ++)
if(numbers[i] < numbers[i-1])
return false;
return true;
}
};
int main() {
int nums[] = {2, 7, 11, 15};
vector<int> vec(nums, nums + sizeof(nums) / sizeof(int));
int target = 9;
vector<int> res = Solution().twoSum(vec, target);
for(int i = 0 ; i < res.size() ; i ++)
cout << res[i] << " ";
cout << endl;
return 0;
}
四. 滑动窗口 Minimum Size Subarray Sum
给定一个含有 n 个正整数的数组和一个正整数 s ,找出该数组中满足其和 ≥ s 的长度最小的连续子数组。
如果不存在符合条件的连续子数组,返回 0。
示例:
输入: s = 7, nums = [2,3,1,2,4,3]
输出: 2
解释: 子数组 [4,3] 是该条件下的长度最小的连续子数组。
进阶:
如果你已经完成了O(n) 时间复杂度的解法, 请尝试 O(n log n) 时间复杂度的解法。
滑动窗口解法
1. 子数组即 两个指针 l和r 下标之间的数组, 开始时l=0 r=-1
2. l-r 元素之和 < target r++, 直到找到j, 使l-r之间的元素和>target, 得到一个符合条件的子数组
3. l-r 元素之和 > target l--, 直到找到i, 使得l-r之间的元素和< target
4. 重复步骤2和3, 直到找到所有 符合条件的子数组(l >= 数组的size即可结束)
// 滑动窗口的思路
// 时间复杂度: O(n)
// 空间复杂度: O(1)
class Solution {
public:
int minSubArrayLen(int s, vector<int>& nums) {
assert(s > 0);
int l = 0 , r = -1; // nums[l...r]为我们的滑动窗口
int sum = 0;
int res = nums.size() + 1;
while(l < nums.size()){ // 窗口的左边界在数组范围内,则循环继续
if(r + 1 < nums.size() && sum < s)
sum += nums[++r];
else // r已经到头 或者 sum >= s
sum -= nums[l++];
if(sum >= s)
res = min(res, r - l + 1);
}
if(res == nums.size() + 1)
return 0;
return res;
}
};
int main() {
int nums[] = {2, 3, 1, 2, 4, 3};
vector<int> vec(nums, nums + sizeof(nums)/sizeof(int));
int s = 7;
cout << Solution().minSubArrayLen(s, vec) << endl;
return 0;
}
五. 在滑动窗口中做记录 Longest Substring Without Repeating Character
给定一个字符串,请你找出其中不含有重复字符的 最长子串 的长度。
示例 1:
输入: "abcabcbb"
输出: 3
解释: 因为无重复字符的最长子串是 "abc",所以其长度为 3。
示例 2:
输入: "bbbbb"
输出: 1
解释: 因为无重复字符的最长子串是 "b",所以其长度为 1。
示例 3:
输入: "pwwkew"
输出: 3
解释: 因为无重复字符的最长子串是 "wke",所以其长度为 3。
请注意,你的答案必须是 子串 的长度,"pwke" 是一个子序列,不是子串。
用滑动窗口解题
// 滑动窗口
// 时间复杂度: O(len(s))
// 空间复杂度: O(len(charset))
class Solution {
public:
int lengthOfLongestSubstring(string s) {
int freq[256] = {0}; // 记录滑动窗口中元素 出现的频率 比如5 出现了 freq[5]=1
int l = 0, r = -1; //滑动窗口为s[l...r]
int res = 0;
// 整个循环从 l == 0; r == -1 这个空窗口开始
// 到l == s.size(); r == s.size()-1 这个空窗口截止
// 在每次循环里逐渐改变窗口, 维护freq, 并记录当前窗口中是否找到了一个新的最优值
while(l < s.size()){
if(r + 1 < s.size() && freq[s[r+1]] == 0)
freq[s[++r]] ++;
else //r已经到头 || freq[s[r+1]] == 1
freq[s[l++]] --; // r+1对应的元素 在滑动窗口中存在, l++ 并且移除freq[s[l]]的记录
res = max(res, r-l+1); // 每次的到的符合条件的子串, 都会和上一个符合条件的子串比长度, 最后得到最长子串的长度
}
return res;
}
};