两数之和
暴力枚举
cpp
class Solution {
public:
vector<int> twoSum(vector<int>& nums, int target) {
for(int i=0; i<nums.size()-1; i++){
for(int j=i+1; j<nums.size(); j++){
if(target == nums[i] + nums[j]){
return {i,j};
}
}
}
return {};
}
};
哈希表
方法一的时间复杂度较高的原因是寻找target-x的时间复杂度过高。
因此,我们需要一种更优秀的方法,能够快速寻找数组中是否存在目标元素。如果存在,我们需要找到它的索引。
使用哈希表,可以将寻找target-x的时间复杂度从O(N)降到O(1)。
cpp
class Solution{
public:
vector<int> twoSum(vector<int>& nums, int target){
unordered_map<int, int> hashtable;
for(int i=0; i<nums.size(); i++){
auto it = hashtable.find(target-nums[i]);
if(it != hashtable.end()){
return {it->second,i};
}
hashtable[nums[i]] = i;
}
return {];
}
};
字母异位词分组
给一个字符串数组,请你将字母异位词组合在一起。
排序
由于互为字母异位词的两个字符串包含的字母相同。
cpp
class Solution {
public:
vector<vector<string>> groupAnagrams(vector<string>& strs) {
unordered_map<string, vector<string>> strMap;
for(string& str:strs){
string key = str;
sort(key.begin(),key.end());
mp[key].emplace_back(str);
}
vector<vector<string>> ans;
for(auto it=mp.begin(); it!=mp.end();it++){
ans.empalce_back(it->second);
}
return ans;
}
};
最长连续序列
给定一个未排序的整数数组nums,找出数字连续的最长序列(不要求序列元素在原数组中连续)的长度。
请设计并实现时间复杂度为O(n)的算法解决此问题。
哈希表
我们考虑枚举数组中的每个数x,考虑以其为起点,不断尝试匹配x+1,x+2,...是否存在,假设最长匹配到了x+y,那么以x为起点的最长连续序列即为x,x+1,...,x+y,其长度为y+1,我们不断枚举并更新答案即可。
对于匹配的过程,暴力的方法是O(n)遍历数组去看是否存在这个数,但其实更高效的方法是使用一个哈希表存储数组中的数,这样查看一个数是否存在即能优化至O(1)的时间复杂度。
仅仅是这样,我们的算法时间复杂度最坏情况下还是会达到O(n²)(外存需要枚举O(n)个数,内存需要暴力匹配O(n)次),无法满足题目的要求。
仔细分析这个过程,会发现其中执行了很多不必要的枚举,如果已知有一个从x开始的连续序列,而我们却从x+1开始,那么得到的结果肯定不会优于枚举x为起点的答案,因此我们在外层循环的时候碰到这种情况跳过即可。
cpp
class Solution {
public:
int longestConsecutive(vector<int>& nums) {
unordered_set<int> hashTable;
for(const int&num:nums){
hashTable.insert(num);
}
int maxLength = 0;
for(const int&num:hashTable){
if(!hashTable.count(num-1)){
int currentNum = num;
int currentLen = 1;
while(hashTable.count(currentNum+1)){
currentNum++;
currentLen++;
}
maxLength = max(maxLength, currentLen);
}
}
return maxLength;
}
};
移动零
给定一个数组nums,编写一个函数将所有0移动到数组的末尾,同时保持非零元素的相对顺序。
双指针
使用双指针,左指针指向当前已处理好的序列的尾部,右指针指向待处理序列的头部。
右指针不断向右移动,每次右指针指向非零数,则将左右指针对应的数交换,同时左指针右移。
无重复字符的最长子串
cpp
class Solution {
public:
int lengthOfLongestSubstring(string s) {
int n = s.size();
if(n <= 1){
return n;
}
int l = 0;
int r = 1;
unordered_set<char> charMap;
charMap.insert(s[l]);
int maxLen = 1;
while(r < n){
if(charMap.count(s[r])){
maxLen = max(maxLen, r-l);
charMap.remove(s[l]);
l++;
}else{
charMap.insert(s[r]);
r++;
}
}
maxLen = max(maxLen,r-l);
return maxLen;
}
};
和为k的子数组
枚举
cpp
class Solution {
public:
int subarraySum(vector<int>& nums, int k) {
int count = 0;
for(int i=0; i<nums.size(); i++){
int sum = 0;
for(int j=i; j>=0; j--){
sum += nums[j];
if(sum == k){
count++;
}
}
}
return count;
}
};
前缀和+哈希表优化
方法一的瓶颈在于对每个i,我们需要枚举所有的j来判断是否符合条件。
定义pre[i]为0,...,i的和
cpp
pre[i] = pre[i-1] + nums[i]
[j,...i]子数组和为k:
cpp
pre[i] - pre[j-1] == k
pre[j-1] = pre[i] - k;
最大子数组和
找出一个具有最大和的连续子数组,返回最大和(至少包含一个元素)。
动态规划
用f(i)代表以第i个数结尾的连续子数组的最大和。
可以考虑nums[i]单独成为一段还是加入f(i-1)对应的一段。
f(i) = max{nums[i],f(i-1)+nums[i]}
不难给出一个时间复杂度、空间复杂度均为O(n)的实现,即用一个f数组来保存f(i)的值,用一个循环求出所有f(i)。
f(i)只和f(i-1)有关,所以可以用一个变量维护即可,类似于滚动数组。
cpp
class Solution {
public:
int maxSubArray(vector<int>& nums) {
int n = nums.size();
if(n == 1){
return nums[0];
}
int sum = nums[0];
int maxSum = sum;
for(int i=1; i<n; i++){
if(sum < 0){
sum = nums[i];
}else{
sum += nums[i];
}
maxSum = max(maxSum,sum);
}
return maxSum;
}
};
合并区间
合并所有重叠的区间,并返回一个不重叠的区间。
排序
如果按照区间的左端点排序,那么在排完序的列表中,可以合并的区间一定是连续的。
用数组merged存储最终的答案。
首先,将列表中的区间按照左端点升序排序。
然后我们将第一个区间加入merged数组中,并按顺序考虑之后的每个区间:
- 如果当前区间的左端点在数组merged中最后一个区间的右端点之后,那么它们不会重合,直接将区间加入数组merged的末尾。
- 否则,重合,需要将当前区间的右端点更新数组merged中最后一个区间的右端点,将其重置为两者的较大值。
cpp
class Solution {
public:
vector<vector<int>> merge(vector<vector<int>>& intervals) {
vector<vector<int>> merged;
sort(intervals.begin(), intervals.end());
for(int i=0; i<intervals.size(); i++){
int L = intervals[i][0], R = intervals[i][1];
if(!merged.size() || merged.back()[1] < L){
merged.push_back({L,R});
}else{
merged.back()[1] = max(merged.back()[1], R);
}
}
return merged;
}
};
轮转数组
cpp
class Solution {
public:
void reverseNum(vector<int>& nums,int start,int end){
while(start < end){
swap(nums[start++],nums[end--]);
}
}
void rotate(vector<int>& nums, int k) {
int n = nums.size();
k = k%n;
reverseNum(nums,0,n-1);
reverseNum(nums,0,k-1);
reverseNum(nums,k,n-1);
}
};
使用额外的数组
可以使用额外的数组来将每个元素放在正确的位置。
除自身以外数组的乘积
answer[i]等于nums中除nums[i]之外其余各元素的乘积。
左右乘积列表
利用索引左侧所有数字的乘积和右侧所有数字的乘积(即前缀与后缀)相乘得到答案。
- 初始化两个空数组L和R。对于给定索引i,L[i]代表的是i左侧所有数字的乘积,R[i]代表的是右侧所有数字的乘积。
- 我们需要用两个循环来填充L和R数组的值。对于数组L,L[0]应该是1。L[i] = L[i-1]*nums[i];
- R[Length-1] = 1。R[i] = R[i+1]*nums[i]。
cpp
class Solution {
public:
vector<int> productExceptSelf(vector<int>& nums) {
int n = nums.size();
vector<int> L(n,1);
vector<int> R(n,1);
vector<int> res(n);
for(int i=1; i<n; i++){
L[i] = L[i-1]*nums[i-1];
}
for(int i=n-2; i>=0; i--){
R[i] = R[i+1]*nums[i+1];
}
for(int i=0;i<n;i++){
res[i] = L[i]*R[i];
}
return res;
}
};
空间复杂度O(1)的方法
仅管上面的方法已经能够很好地解决这个问题,但是空间复杂度并不为常数。
由于输出数组不算在空间复杂度内,那么我们可以将L或R数组用输出数组来计算。
先把输出数组当做L数组来计算,然后再动态构造R数组得到结果。
- 初始化answer数组,对于给定索引i,answer[i]代表的是i左侧所有数字的乘积。
- 没有构造R数组。用一个遍历来跟踪右边元素的乘积。并更新数组answer[i] = answer[i] * R,R=R*nums[i]
cpp
class Solution {
public:
vector<int> productExceptSelf(vector<int>& nums) {
int n = nums.size();
vector<int> res(n,1);
for(int i=1; i<n; i++){
res[i] *= res[i-1]*nums[i-1];
}
int R = 1;
for(int i=n-1; i>=0; i--){
res[i] = res[i]*R;
R *= nums[i];
}
return res;
}
};