转换题目,每次仅合并两个升序链表。
合并两个升序链表:
ListNode* mergeTwoLists(ListNode* a,ListNode* b){
if((!a)||(!b)) return a?a:b;
ListNode head,*tail=&head,*aptr=a,*bptr=b;
while(aptr&&bptr)
{
if(aptr->val<bptr->val)
{
tail->next=aptr;aptr=aptr->next;
}
else
{
tail->next=bptr;bptr=bptr->next;
}
tail=tail->next;
}
tail->next=(aptr?aptr:bptr);
return head.next;
}
合并k个升序链表:
/**
* Definition for singly-linked list.
* struct ListNode {
* int val;
* ListNode *next;
* ListNode() : val(0), next(nullptr) {}
* ListNode(int x) : val(x), next(nullptr) {}
* ListNode(int x, ListNode *next) : val(x), next(next) {}
* };
*/
class Solution {
public:
ListNode* mergeTwoLists(ListNode* a,ListNode* b)
{
if((!a)||(!b)) return a?a:b;
ListNode head;
ListNode* aptr=a,*bptr=b,*tail=&head;
while(aptr&&bptr)
{
if(aptr->val<bptr->val)
{
tail->next=aptr;aptr=aptr->next;
}
else
{
tail->next=bptr;bptr=bptr->next;
}
tail=tail->next;
}
tail->next=(aptr?aptr:bptr);
return head.next;
}
ListNode* mergeKLists(vector<ListNode*>& lists) {
ListNode* ans=NULL;
for(int i=0;i<lists.size();i++)
ans=mergeTwoLists(ans,lists[i]);
return ans;
}
};
“合并两个升序链表”每次选择两个链表中最小的节点,那么类比到“合并k个升序链表”,可以每次选择k个链表中最小的节点,这个选择可以靠优先级队列(二叉堆) 来实现。
/**
* Definition for singly-linked list.
* struct ListNode {
* int val;
* ListNode *next;
* ListNode() : val(0), next(nullptr) {}
* ListNode(int x) : val(x), next(nullptr) {}
* ListNode(int x, ListNode *next) : val(x), next(next) {}
* };
*/
class Solution {
public:
struct Status{
int val;
ListNode* ptr;
bool operator < (const Status &rhs) const{
return val > rhs.val;
}
};
priority_queue<Status> q;
ListNode* mergeKLists(vector<ListNode*>& lists) {
for(auto node:lists)
{
if(node) q.push({node->val,node});
}
ListNode head,*tail=&head;
while(!q.empty())
{
auto f=q.top();q.pop(); //取根
tail->next=f.ptr;
tail=tail->next;
if(f.ptr->next) q.push({f.ptr->next->val,f.ptr->next});
}
return head.next;
}
};
复杂度
优先队列q
中元素最多有k
个,每次pop
和push
的复杂度为O(logk),由于所有链表的每一个节点都会push
和pop
一遍,所以算法的总复杂度为O(nlogk)。
tar_i=i
。nums[tar_i]
的数,记为tar_j
。例如,452631,应当选择2与3交换。
也就是说,从右向左寻找第一个升序二元组 ,记录tar_i=i
,需要注意的是,此时,i
的右边一定是一个降序排列。然后再寻找一个比nums[i]
大却又尽可能小的数,由于i
右边是一个降序排列,所以从右往左寻找第一个大于nums[tar_i]
的数即可,记为tar_j
。最后,将tar_i
右边的数进行升序重排即可。
如果找不到这样一个升序二元组 ,说明此时的序列是一个降序序列,那么下一个排列就是这些数的升序排列,直接重新sort
即可。
class Solution {
public:
void nextPermutation(vector<int>& nums) {
//先不考虑重复数的存在
int n=nums.size();
int tar_i=-1,tar_j=-1;
for(int i=n-2;i>=0;i--)
{
if(nums[i]<nums[i+1])
{
tar_i=i;
break;
}
}
if(tar_i==-1)
{
sort(nums.begin(),nums.end());
return ;
}
for(int i=n-1;i>tar_i;i--)
{
if(nums[i]>nums[tar_i])
{
tar_j=i;
break;
}
}
swap(nums[tar_i],nums[tar_j]);
sort(nums.begin()+tar_i+1,nums.end());
return ;
}
};
dp[i]
表示以s[i]
为结尾的最长有效括号子串的长度。即,如果dp[i]
不属于有效子串的一部分,则dp[i]=0
。s[i]=='('
: dp[i]=0
。s[i]==')'
: 分类讨论
s[i-1]=='('
,那么s[i-1]与s[i]构成一对有效括号,即"xxxxxx( )"有效括号子串长度增加2,即dp[i]=dp[i-2]+2
。s[i-dp[i-1]-1]=='('
,s[i]之前,以s[i-1]结尾是一个有效括号子串,即"xxxxxx) )",这时候需要考虑s[i]之前的那个有效括号子串的前一个符号。
dp[i]=dp[i-1]+2
。dp[i]=dp[i-1]+2+dp[(i-dp[i-1]-1)-1]
。s[i]=='('
,则dp[i]=0
。i-1>=0
、i-2>=0
、i-dp[i-1]-1>=0
、(i-dp[i-1]-1)-1>=0
。最后,比较所有有效括号子串的长度,取最大值返回。
class Solution {
public:
int longestValidParentheses(string s) {
int n=s.size();
vector<int> dp(n+1,0); //vector vec(n,t); 初始化一个长度为n的数组,每个元素初始化为t。
int ans=0;
for(int i=1;i<n;i++) //ensure i-1>=0
{
//base case
if(s[i]=='(') dp[i]=0;
//状态转移方程
if(s[i]==')')
{
if(s[i-1]=='(')
{
dp[i]=2;
if(i>=2) //ensure i-2>=0
dp[i]+=dp[i-2];
}
if((i-dp[i-1]-1)>=0&&s[(i-dp[i-1]-1)]=='(')
{
dp[i]=dp[i-1]+2;
if(i-dp[i-1]-2>=0) //ensure i-dp[i-1]-2>=0
dp[i]+=dp[i-dp[i-1]-2];
}
}
ans=max(ans,dp[i]);
}
return ans;
}
};
[nums[k], nums[k+1], ..., nums[n-1], nums[0], nums[1], ..., nums[k-1]]
(下标从0开始计数)。[left,mid]
和[mid+1,right]
两部分,初始状态时left=0,right=n-1
。(本质上也可以理解为是分为了[left,mid-1]和[mid+1,right]和[mid]三部分,但是如果不加上mid,代码中会出现mid-1和mid+1的情况,在判断两个边界的时候会出现数组下标越界的情况,所以将min包括到两个部分中,效果相同,但是便于处理。)target
在不在有序部分实现上下界的更新。nums[left,right]
,每次取其中值nums[mid]
,每次都判断nums[mid]
是不是我所寻找的target
。然后判断左右两部分[left,mid]
和[mid,right]
。
[left,mid]
有序,也就是nums[left]<=nums[right]
,这里的等号表示区间里只有一个元素的情况,可以直接通过该部分有序这个特性判断target
在不在该部分中:如果target
在该部分中,舍弃[mid,right]
部分,即右界更新,right=mid-1
;如果target
不在该部分中,那么舍弃这个部分,即左界更新,left=mid+1
。[left,mid]
无序,那么[mid,right]
一定有序,同样地,判断target
在不在这个有序部分中:如果target
在这个部分中,则舍弃[left,mid]
部分,即左界更新,left=mid+1
;如果target
不在这个部分中,那么舍弃这个部分,即右界更新,right=mid-1
。class Solution {
public:
int search(vector<int>& nums, int target) {
int n=nums.size();
int left=0,right=n-1;
//[0.mid-1]和[mid+1,n-1]两个部分里一定有一个部分是有序的,根据有序部分来修改上下界(可以判断target是否在有序部分里)
//实际上将数组分成了两个部分,[0.mid],[mid],[mid,n-1],mid应当是两个部分共用的
while(left<=right)
{
int mid=(left+right)/2;
//base case
if(nums[mid]==target)
{
return mid;
}
//如果[0,mid]有序
if(nums[left]<=nums[mid]) //当只有一个元素时,会取等,这也说明是在闭区间[left,mid]中搜索。
{
//如果target在这个部分里
if(nums[left]<=target&&target<=nums[mid]) //target exists
{
//已经判断了nums[mid]!=target
right=mid-1; //将另一部分舍弃
}
else //target不在有序部分里
{
left=mid+1; //将这一部分舍弃
}
}
else //[mid+1,n-1]有序
{
if(target>=nums[mid]&&target<=nums[right]) //在有序部分里
{
left=mid+1; //舍弃左边无序的部分
}
else //在另一部分
{
right=mid-1; //舍弃这个有序的部分
}
}
}
return -1;
}
};
class Solution {
public:
vector<int> searchRange(vector<int>& nums, int target) {
vector<int> ans;
int n=nums.size();
//左界
int left=0,right=n,leftBound=-1;
while(left<right)
{
int mid=left+(right-left)/2;
if(nums[mid]==target)
{
right=mid;
}
else if(nums[mid]<target)
{
left=mid+1;
}
else if(nums[mid]>target)
{
right=mid;
}
}
if(left<n&&nums[left]==target) leftBound=left;
//右界
left=0;
right=n;
int rightBound=-1;
while(left<right)
{
int mid=left+(right-left)/2;
if(nums[mid]==target)
{
left=mid+1;
}
else if(nums[mid]<target)
{
left=mid+1;
}
else if(nums[mid]>target)
{
right=mid;
}
}
if(left>=1&&nums[left-1]==target) rightBound=left-1; //这里! left=0时,left-1会溢出!
ans.push_back(leftBound);
ans.push_back(rightBound);
return ans;
}
};
class Solution {
public:
vector<int> searchRange(vector<int>& nums, int target) {
int n=nums.size();
int leftBound=-1,rightBound=-1;
//左界
int left=0,right=n-1;
while(left<=right)
{
int mid=left+(right-left)/2;
if(nums[mid]==target)
{
right=mid-1; //向左收缩
}
else if(nums[mid]>target)
{
right=mid-1;
}
else if(nums[mid]<target)
{
left=mid+1;
}
}
//出循环条件left=right+1,此时left指向左界。
if(left==n) leftBound=-1;
else leftBound=nums[left]==target?left:-1;
//右界
left=0;
right=n-1;
while(left<=right)
{
int mid=left+(right-left)/2;
if(nums[mid]==target)
{
left=mid+1; //向右收缩
}
else if(nums[mid]>target)
{
right=mid-1;
}
else if(nums[mid]<target)
{
left=mid+1;
}
}
//出循环条件left=right+1,此时right指向右界
if(right<0) rightBound=-1;
else rightBound=nums[right]==target?right:-1;
return vector<int>{leftBound,rightBound};
}
};
Attention
对于穷举所有可能性才能达到答案的问题,一般都用递归回溯算法。
class Solution {
public:
void dfs(vector<int>& candidates,int target,vector<vector<int> >& ans,int idx,vector<int>& path)
{
if(target<0||idx==candidates.size()) return ;
if(target==0)
{
ans.emplace_back(path);
return ;
}
//不选择candicate[idx]
dfs(candidates,target,ans,idx+1,path);
//选择,选择前加入路径,选择后撤销选择(从路径中撤销)
path.emplace_back(candidates[idx]);
dfs(candidates,target-candidates[idx],ans,idx,path);
path.pop_back();
}
vector<vector<int>> combinationSum(vector<int>& candidates, int target) {
vector<vector<int> > ans;
vector<int> path;
dfs(candidates,target,ans,0,path);
return ans;
}
};
Attention
对于height[i]来说,他的雨水储量是由min(leftMax,rightMax)决定的。
一开始考虑的时候只考虑了左边最值,考虑非常不全面:(。
既然明确了这一点,就可以暴力获取height[i]两边的最大值来得到min(leftMax,rightMax),从而计算每一个柱子的储水。
应该是样例比较水,所以 O(n2) 的复杂度也可以ac。
class Solution {
public:
int trap(vector<int>& height) {
int n=height.size(),ans=0;
vector<int> leftMax(n),rightMax(n);
int tmp=0;
for(int i=0;i<n;i++)
{
leftMax[i]=tmp;
tmp=max(tmp,height[i]);
}
tmp=0;
for(int i=n-1;i>=0;i--)
{
rightMax[i]=tmp;
tmp=max(tmp,height[i]);
}
for(int i=0;i<n;i++)
ans=ans+(min(leftMax[i],rightMax[i])>height[i]?min(leftMax[i],rightMax[i])-height[i]:0);
return ans;
}
};
void dfs(vector<vector<int> >& ans,vector<int>& current,vector<int>&nums,int cnt,vector<bool>& flag)
{
if(cnt==nums.size())
{
ans.emplace_back(current);
for(int i=0;i<current.size();i++)
cout<<current[i]<<" ";
cout<<endl;
return ;
}
for(int i=0;i<nums.size();i++)
{
if(flag[i]==false)
{
current.emplace_back(nums[i]);
flag[i]=true;
dfs(ans,current,nums,cnt+1,flag);
current.pop_back();
flag[i]=false;
}
}
}
vector<vector<int>> permute(vector<int>& nums) {
vector<vector<int> > ans;
vector<int> current;
vector<bool> flag={false};
dfs(ans,current,nums,0,flag);
return ans;
}
复杂度:O(n2)。这种时候,要想到用哈希表加速,也就是unordered_map!
class Solution {
public:
vector<vector<string>> groupAnagrams(vector<string>& strs) {
int n=strs.size();
vector<vector<string> > ans;
vector<bool> outer_flag(n);
int cnt=-1;
bool flag=false;
for(int i=0;i<n;i++)
{
if(!outer_flag[i]) //这个词没有被归类
{
cnt++;
flag=false;
outer_flag[i]=true;
vector<string> vec;
ans.emplace_back(vec);
ans[cnt].emplace_back(strs[i]);
string tmp=strs[i];
sort(tmp.begin(),tmp.end());
for(int j=i+1;j<n;j++) //看后面的有没有同类的
{
if(!outer_flag[j])
{
string tmp2=strs[j];
sort(tmp2.begin(),tmp2.end());
if(tmp==tmp2)
{
ans[cnt].emplace_back(strs[j]);
flag=true;
outer_flag[j]=true;
}
}
}
}
}
return ans;
}
};
如果str1与str2是字母异位词,那么他们排序后得到的字符串str1’和str2’应当是一样的。
class Solution {
public:
vector<vector<string>> groupAnagrams(vector<string>& strs) {
map<string,vector<string> > mp;
vector<vector<string>> ans;
int n=strs.size();
for(int i=0;i<n;i++)
{
string tmp=strs[i];
sort(tmp.begin(),tmp.end());
map<string,vector<string> >::iterator it=mp.find(tmp);
if(it!=mp.end())
{
it->second.emplace_back(strs[i]);
}
else
{
vector<string> vec;
vec.emplace_back(strs[i]);
mp[tmp]=vec;
}
}
map<string,vector<string> >::iterator it;
for(it=mp.begin();it!=mp.end();it++)
{
ans.emplace_back(it->second);
}
return ans;
}
};
//官方实现
class Solution {
public:
vector<vector<string>> groupAnagrams(vector<string>& strs) {
unordered_map<string,vector<string> > mp;
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.emplace_back(it->second);
return ans;
}
};
//根据官方实现修改后
class Solution {
public:
vector<vector<string>> groupAnagrams(vector<string>& strs) {
map<string,vector<string> > mp;
vector<vector<string>> ans;
for(string& s : strs)
{
string tmp=s;
sort(tmp.begin(),tmp.end());
// auto it=mp.find(tmp);
// if(it!=mp.end())
// it->second.emplace_back(s);
// else
// mp[tmp].emplace_back(s);
mp[tmp].emplace_back(s);
}
for(auto it=mp.begin();it!=mp.end();it++)
ans.emplace_back(it->second);
return ans;
}
};
Attention
for(auto x : str)
和 for(auto& x : str)
for(auto x : str){}
:x是str中每一个对象的复制,对x的操作不会影响原容器中的对象。for(auto& x : str){}
:x是str中每个对象的引用,对x的操作会影响到原容器中对应的对象。引用会更快。auto
:自动推断变量类型,常用于容器遍历和获取函数返回值。string
类型变量的sort
:sort(str.begin(),str.end())
即可根据字典序对字符串进行排序(sort的头文件为
)。unordered_map
是依靠哈希表实现的,速度比map快,头文件是
,但是操作和map类似。这道题求的是最值(不要求获取最大子数组序列),这是典型的动态规划的适用问题。
动态规划解题步骤:
class Solution {
public:
int maxSubArray(vector<int>& nums) {
int n=nums.size();
vector<int> dp(n);
dp[0]=nums[0];
int ans=dp[0];
for(int i=1;i<n;i++)
{
dp[i]=max(nums[i],dp[i-1]+nums[i]);
ans=max(ans,dp[i]);
}
return ans;
}
};
dp[i]可以仅使用一个变量pre存储。
class Solution {
public:
int maxSubArray(vector<int>& nums) {
int pre=0;
int ans=nums[0];
for(auto& x : nums)
{
pre=max(x,pre+x);
ans=max(ans,pre);
}
return ans;
}
};
假设当前格子为pre,那么他可以到达的最大位置maxn是max(pre+i+nums[pre+i]),0<=i<=nums[pre]。
pre的更新,由于每次都在pre~(pre+nums[pre])之间寻找最远距离,所以每次更新pre为pre+pre[nums]+1。因此,如果更新后的pre>maxn,那么就说明,不会再往后走了,此时即可跳出循环。
最后判断maxn与n-1的大小即可。
class Solution {
public:
bool canJump(vector<int>& nums) {
int n=nums.size();
int pre=0;
int maxn=0;
while(pre<n-1)
{
for(int i=pre;i<n&&i<=pre+nums[pre];i++)
maxn=max(maxn,i+nums[i]);
pre+=nums[pre]+1;
if(pre>maxn) break;
}
return maxn>=n-1;
}
};
将所有区间按左端点从小到大排序后,你会发现,相邻的两个区间,只有两种情况:
那么问题也就迎刃而解了。
class Solution {
public:
//必须设置成static
static bool cmp (const vector<int>& a,const vector<int>& b){
return a[0]<b[0];
}
vector<vector<int>> merge(vector<vector<int>>& intervals) {
sort(intervals.begin(),intervals.end(),cmp); //按左端点排序
int n=intervals.size();
vector<vector<int> > ans;
int cnt=0;
bool flag=false;
while(cnt<n-1)
{
int l=intervals[cnt][0],r=intervals[cnt][1];
while(intervals[cnt+1][0]<=r) //下一个区间的左端点比当前区间的右端点小或相等,可以合并
{
r=max(r,intervals[cnt+1][1]); //这里容易错,不能想当然地觉得后面那个区间的右端点一定更大
cnt++;
if(cnt==n-1) //额外处理最后一个区间
{
flag=true;
break;
}
}
vector<int> tmp={l,r};
ans.emplace_back(tmp);
cnt++;
}
if(!flag)
ans.emplace_back(intervals[n-1]);
return ans;
}
};
Attention:
//必须设置成static
static bool cmp (const vector<int>& a,const vector<int>& b){
return a[0]<b[0];
}
...
void f(){
sort(intervals.begin(),intervals.end(),cmp); //按左端点排序
}
假设 dp[i][j]
表示 将
w
o
r
d
1
word1
word1 的前 i
个字符转换为
w
o
r
d
2
word2
word2 的前 j
个字符需要的最少操作数,那么状态转移方程为:
dp[i][j] = min(dp[i-1][j] + 1, dp[i][j-1] + 1,, dp[i-1][j-1] + 1), if word1[i] != word[j]
dp[i][j] = dp[i-1][j-1], if word1[i] == word2[j]
其中,
dp[i-1][j] + 1
表示插入操作;dp[i][j-1] + 1
表示删除操作;dp[i-1][j-1] + 1
表示替换操作;word1[i] == word2[j]
,那么不需要做额外操作,就能实现转换,所以此时 dp[i][j] = dp[i-1][j-1]
。初始条件为:
dp[0][0] = 0;
for(int i = 1; i <= n; i++) dp[i][0] = i;
for(int j = 1; j <= m; j++) dp[0][j] = j;
也就是说,假如其中一个 w o r d word word 为空,则最少进行 i i i 或 j j j 次插入操作即可实现转换。
因此,为了方便计算,dp[][]
的第
0
0
0 行和第
0
0
0 列拿来存放初始条件,初始化:
vector<vector<int> > dp(n + 1, vector<int>(m + 1, maxn));
完整代码为:
class Solution {
public:
int minDistance(string word1, string word2) {
int n = word1.size(), m = word2.size();
int maxn = 1e9;
vector<vector<int> > dp(n + 1, vector<int>(m + 1, maxn));
dp[0][0] = 0;
for(int i = 1; i <= n; i++) dp[i][0] = i;
for(int j = 1; j <= m; j++) dp[0][j] = j;
// dp[i][j]:word1 前 i 个字符转换成 word2 前 j 个字符需要的最少操作次数。
for(int i = 1; i <= n; i++)
{
for(int j = 1; j <= m; j++)
{
int top = dp[i][j-1] + 1, left = dp[i-1][j] + 1, pre = dp[i-1][j-1];
if(word1[i-1] != word2[j-1]) // dp 的第 1 行/列 对应的才是 word 的 0
{
dp[i][j] = min(dp[i][j], min(min(left, top), pre + 1));
}
else
{
dp[i][j] = pre;
}
}
}
return dp[n][m];
}
};
时间复杂度:O(
n
∗
m
n * m
n∗m),其中
n
n
n 和
m
m
m 分别为
w
o
r
d
1
word1
word1 和
w
o
r
d
2
word2
word2 的长度。
空间复杂度:O(
n
∗
m
n * m
n∗m),其中
n
n
n 和
m
m
m 分别为
w
o
r
d
1
word1
word1 和
w
o
r
d
2
word2
word2 的长度。