目录
一、2129. 将标题首字母大写(二分查找与快速排序的结构性启发)
二、83. 移动零(快速排序的启发以及基于快排的优化):双指针
三、1793. 好子数组的最大分数 双指针优化&动态规划的思想
一、438. 找到字符串中所有字母异位词 (哈希表与滑动窗口的结合)
Ⅰ:题型适用范围(未完善)
Ⅱ:二分查找&快速排序的启发
一、2129. 将标题首字母大写(二分查找与快速排序的结构性启发)
(一)题目介绍:
给你一个字符串 title
,它由单个空格连接一个或多个单词组成,每个单词都只包含英文字母。请你按以下规则将每个单词的首字母 大写 :
- 如果单词的长度为
1
或者2
,所有字母变成小写。 - 否则,将单词首字母大写,剩余字母变成小写。
请你返回 大写后 的 title
。
示例 1:
输入:title = "capiTalIze tHe titLe" 输出:"Capitalize The Title" 解释: 由于所有单词的长度都至少为 3 ,将每个单词首字母大写,剩余字母变为小写。
示例 2:
输入:title = "First leTTeR of EACH Word" 输出:"First Letter of Each Word" 解释: 单词 "of" 长度为 2 ,所以它保持完全小写。 其他单词长度都至少为 3 ,所以其他单词首字母大写,剩余字母小写。
(2)代码展示:
class Solution {
public:
string capitalizeTitle(string title) {
int n=title.length();
int l=0,r=0;
while(l<n){
while(r<n&&title[r]!=' ') r++;
if(r-l>2) title[l++]=toupper(title[l]);
while(l<r) title[l++]=tolower(title[l]);
l=++r;
}
return title;
}
};
是不是很惊讶代码的长度?在我看到题解时我也是如此的惊讶。
(3)题目要求分析:
1、给定字符串,按要求转化大小写。
2、可以认为字符串中有许多子串,用空格连接。我们可以借用分而治之的思想,将大的问题化小,逐一攻克。
3、划分思路:在这之前我们首先明白,如果用for循环进行遍历,那么时间复杂度和代码复杂度绝对不小。那么我们如何用简单且快速的方法来解决这个问题呢?我们是否还记得二分查找的简单结构:
int l=0,r=length-1;
while(l<r){
mid=(l+r)/2;
if()
...
}
在二分查找里,我们用mid限制我们想要寻找的元素。那么,在这道题中,我们就可以用同样的思想来确定子串的长度。
int n=title.length();
int l=0,r=0;
while(l<n){
while(r<n&&title[r]!=' ') r++; //这里已经非常好理解了,如此我们便可以确定子串在原串中的位置,并容易得出串的长度length=r-l
...
}
第三步就是两个要求了,题目已经十分详细,这里不再赘叙。
(四)总结归纳:
(1)
title[l++]=toupper(title[l]) 和 l=++r
这两段代码值得深思,出去应用的string的内部函数toupper()使小写字母变成大写。代码中用了l++和++r两种不同的加1方法:
i++:先赋值再加1
++i:先加1再赋值
以下是两种方法所带来的不同结果:
#include<iostream>
using namespace std;
int main(){
int i=0,j=0;
int a[2]={0};
a[i++]=1;
cout<<a[0]<<a[1];
/*输出结果为:1 0 */
a[0]=0;
a[++j]=1;
couy<<a[0]<<a[1];
/*输出结果为:0 1 */
return 0;
}
(2)还有一个隐含的点:代码中找到的是空格的位置,但求长度时是不需要减1操作的。
二、83. 移动零(快速排序的启发以及基于快排的优化):双指针
(一)题目介绍:
给定一个数组 nums
,编写一个函数将所有 0
移动到数组的末尾,同时保持非零元素的相对顺序。
请注意 ,必须在不复制数组的情况下原地对数组进行操作。
示例 1:
输入: nums =[0,1,0,3,12]
输出:[1,3,12,0,0]
(二)代码展示:
未优化版:
class Solution {
public:
void moveZeroes(vector<int>& nums) {
int n=nums.size();
if(n<2) return;
int i=0,j=1;
while(i<n&&j<n){
while(j<n&&nums[j]==0) j++;
while(i<j&&nums[i]!=0) i++;
if(j<n&&i<n){
int t=nums[i];
nums[i]=nums[j];
nums[j]=t;
i++,j++;
}
}
}
};
优化版:
class Solution {
public:
void moveZeroes(vector<int>& nums) {
int n=nums.size(),i=0,j=0;
while(j<n){
if(nums[j]!=0){
swap(nums[i],nums[j]);
i++;
}
j++;
}
}
};
(三)题目要求分析:
1、这是一个逻辑十分清晰的问题,如果可以复制函数的话那就更简单了。
2、审题知晓题意之后,我们往往会考虑要用哪一种方法可以使代码的时间复杂度和代码复杂度得到最优解。换一种理解方式,我们该如何用更有深度的思维逻辑,让计算机替人类思考解决问题,做更多的事。这是一个双指针类题,我们可能会想到快速排序。
3、现在有两个指针:
i:需要被调换的指针,即前驱。
j:查找可以与 i 指定的元素进行调换的指针,即后驱。
4、我们用指针 i 来遍历整个数组,当需要调换时,通过 j 找到合适的值进行调换。注意指针 i 一定是在指针 j 的前面,所以我们先移动指针 j 找到可以进行调换的元素,在移动 i 进行调换。类似于快速排序,我们用两个while循环代替for循环和if语句的繁复巡查。
while(i<n&&j<n){
while(j<n&&nums[j]==0) j++; //指针j用来查找元素0后边不为0的元素的位置
while(i<j&&nums[i]!=0) i++; //指针i用来查找需要替换元素0的位置,为了保证顺序,不为0的值不应该被调换,特别注意 i<j 保证前驱 i 的位置
if(j<n&&i<n){
int t=nums[i];
nums[i]=nums[j];
nums[j]=t;
i++,j++; //调换后,继续查询
}
}
5、注意,在第三个while里,必须保证 i<j ,否则会出现0在在中间的情况。
6、优化代码后,我们遍历 j ,这样,只要将后边不为0的元素放到前边就可以了。但我们会想,这样不会改变原非零元素的位置吗?在优化代码中,我们的判断条件if(nums[j]!=0),会将非零元素进行两次替换,又重现恢复了最初的位置。代码复杂度得到了极大的优化。而且,我们还省去了最初的if(n<2)判断。
(四)总结归纳:
出去上面的分析,我们还学到(或巩固)了vector迭代器中的swap()替换函数,这极大方便了我们以后写代码的复杂度。
其实,我们应该真正发自内心的去探讨动态规划那种让计算机去代替人类做一些繁杂问题的方式,用简单明了直击本质的逻辑去解决问题。这不仅仅会提高我们算法的能力,还会使我们的思维深度达到更高深的层次。
三、1793. 好子数组的最大分数 双指针优化&动态规划的思想
(一)问题描述:
给你一个整数数组 nums
(下标从 0 开始)和一个整数 k
。
一个子数组 (i, j)
的 分数 定义为 min(nums[i], nums[i+1], ..., nums[j]) * (j - i + 1)
。一个 好 子数组的两个端点下标需要满足 i <= k <= j
。
请你返回 好 子数组的最大可能 分数 。
示例 1:
输入:nums = [1,4,3,7,4,5], k = 3 输出:15 解释:最优子数组的左右端点下标是 (1, 5) ,分数为 min(4,3,7,4,5) * (5-1+1) = 3 * 5 = 15 。
(二)代码展示:
1、枚举(借鉴动态规划,自己做的,时间超限)
class Solution {
public:
int maximumScore(vector<int>& nums, int k) {
int n=nums.size();
int ans=nums[k];
for(int i=0;i<k+1;i++){
for(int j=0;j<n-k;j++){
int x=*min_element(nums.begin()+k-i,nums.begin()+k+j+1);
ans=max(ans,x*(j+i+1));
}
}
return ans;
}
};
2、双指针:
(1)优化前
class Solution {
public:
int maximumScore(vector<int>& nums, int k) {
int n=nums.size();
int l=k-1,r=k+1,ans=0;
for(int i=nums[k];;i--){
while(l>=0&&nums[l]>=i) l--;
while(r<n&&nums[r]>=i) r++;
ans=max(ans,i*(r-l-1));
if(l==-1&&r==n) break;
}
return ans;
}
};
(2)优化后
class Solution {
public:
int maximumScore(vector<int>& nums, int k) {
int n=nums.size();
int l=k-1,r=k+1,ans=0;
for(int i=nums[k];;){
while(l>=0&&nums[l]>=i) l--;
while(r<n&&nums[r]>=i) r++;
ans=max(ans,i*(r-l-1));
if(l==-1&&r==n) break;
i=max((l==-1?-1:nums[l]),(r==n?-1:nums[r]));
if(i==-1) break;
}
return ans;
}
};
(三)思路分析:
1、我简单分析以下我自己的做题思路:一开始我想到了双指针,但我没有找到循环时的条件,惯性思维的认为while(l>=0&&r<n),套11. 盛最多水的容器的模板,但令我困扰的是如此并不会降低代码的复杂度。实质上没有真正意义上掌握双指针。之后,我从l>=0&&r<n下手,尝试从数组的两端开始向k位置收缩(之前是扩张)。当时我还是在考虑什么时候移动r和 l ,我猜想用定1变1的方法是否可以解决问题。由此,我想到了动态规划。我不断尝试,想要找到递推公式,并用备忘录将每个位置进行记录。之后,研究备忘录f(i)(j)的取值范围:
我试着把子数组的长度定义为 i,把位置 k 之后的元素个数用 j 来表示,当 i=0 时,子数组的长度为 j ,当 i=1 时,相当于用 k-1 替换 k 。
写出代码后,我发现实际上是一种枚举的方法,将所有包含 k 位置的所有子数组都求了一遍。实际上时间复杂度仅为 O(kn-k) -> O(n),但仍然超时了。
2、题解就是严格的双指针,他的循环条件属实是惊艳了我,题解的方法与我想到的11. 盛最多水的容器不谋而合。具体解析可以看题解,我就不过多赘述了。
Ⅲ:巧妙地位运算
一、2917. 找出数组中的 K-or 值 (位运算)
(一)题目介绍:
给你一个整数数组 nums
和一个整数 k
。让我们通过扩展标准的按位或来介绍 K-or 操作。在 K-or 操作中,如果在 nums
中,至少存在 k
个元素的第 i
位值为 1 ,那么 K-or 中的第 i
位的值是 1 。
返回 nums
的 K-or 值。
示例 1:
输入:nums = [7,12,9,8,9,15], k = 4 输出:9 解释: 用二进制表示 numbers:
Number | Bit 3 | Bit 2 | Bit 1 | Bit 0 |
---|---|---|---|---|
7 | 0 | 1 | 1 | 1 |
12 | 1 | 1 | 0 | 0 |
9 | 1 | 0 | 0 | 1 |
8 | 1 | 0 | 0 | 0 |
9 | 1 | 0 | 0 | 1 |
15 | 1 | 1 | 1 | 1 |
Result = 9 | 1 | 0 | 0 | 1 |
位 0 在 7, 9, 9, 15 中为 1。位 3 在 12, 9, 8, 9, 15 中为 1。 只有位 0 和 3 满足。结果是 (1001)2 = 9。
(二)代码展示:
class Solution {
public:
int findKOr(vector<int>& nums, int k) {
int max=*max_element(nums.begin(),nums.end());
if(max==0) return 0;
int a=1,i,sum=0,s=0,t=0;
i=log2(max)+1;
while(i--){
for(auto e:nums)
if((a&e)!=0) sum++;
a=a<<1;
if(sum>=k) s+=1<<t;
t++;
sum=0;
}
return s;
}
};
(三)题目要求与分析:
1、先来分析题目。我们需要把数组中每个数用二进制表示出来,计每一位数下为1时的数量大于k时的位数为t,最后求所有t的2次方的和。
2、知晓题意后解题思路就很清晰了,我们只需要按位计数,把小于 k 的都舍去即可。
3、如何利用位运算解决呢。
(1)我们利用与运算,用 0 和nums数组中的元素对应位进行与操作。当结果为1时,计数sum。最后判断sum和k。
while(i--){
for(auto e:nums)//一种简单遍历数组的方法。遍历数组nums,同时将值赋值给e
if((a&e)!=0) sum++;
a=a<<1; //位运算左移1位,即后右边填0
if(sum>=k) s+=1<<t; //与上相同,将1左移t位,即1后边填上t个0(当然是二进制表示),表示2的t次方
t++; //比较的位数加一
sum=0;
}
(2)你是否注意到while循环里的 i 。我们要求每一位下的 1 的数量,很容易想到for或while循环进行逐一遍历。我用for循环来举例:
for(begin,end,formuala) 例:1001101
我们已经知道begin是从右侧二进制例子中的最右侧开始的,显然最左侧便是终止条件。试想一下,我们把nums数组用上至下进行排列(二进制表示),很显然,值最大的最长。所以我们只需要找到nums数组中最大值,便可以轻松找到end了。
int max=*max_element(nums.begin(),nums.end()); //vector内部函数,求nums数组中的最大值
i=log2(max)+1; //#include<cmath>,数学公式
Ⅳ、便利的哈希
一、438. 找到字符串中所有字母异位词 (哈希表与滑动窗口的结合)
(一)题目介绍:
给定两个字符串 s
和 p
,找到 s
中所有 p
的 异位词 的子串,返回这些子串的起始索引。不考虑答案输出的顺序。
异位词 指由相同字母重排列形成的字符串(包括相同的字符串)。
示例 1:
输入: s = "cbaebabacd", p = "abc" 输出: [0,6] 解释: 起始索引等于 0 的子串是 "cba", 它是 "abc" 的异位词。 起始索引等于 6 的子串是 "bac", 它是 "abc" 的异位词。
(二)代码展示:
class Solution {
public:
vector<int> findAnagrams(string s, string p) {
if(p.length()>s.length()) return {};
vector<int> st;
unordered_map<char,int> mp;
for(auto e:p) mp[e]++;
string str=s.substr(0,p.length());
for(auto e:str) mp[e]--;
int i=0;
while(i<=s.length()-p.length()){
bool f=false;
for(auto e:mp) if(e.second!=0){
f=true;
break;
}
if(f==false) st.push_back(i);
mp[s[i++]]++;
if(i<=s.length()-p.length()) mp[s[i+p.length()-1]]--;
}
return st;
}
};
(三)题目要求与分析:
1、利用滑动窗口,每次只比较字符串 s 长度为p.length()时的子串str。
2、如何比较 str 和 p 是否是异位词?
方法1:直接将两个字符串进行排序,然后看看是否相等。时间复杂度和代码复杂度都比较高。
方法2:利用哈希表。既然是滑动窗口,所以我们在比较记录的哈希表时,只需要将移动前的第一个位置回复,再将移动后的最后一个位置加入哈希表即可。这样操作后时间复杂度低的同时,空间复杂度和代码复杂度都会降低。例如下图:
假设 p.length()=3,此时的窗口中有 b c d ,蓝底 b 就是窗口滑动后将要去掉的元素,黄色 a就是窗口滑动后将要加入的元素。
a | b | c | d | a |
窗口滑动后,蓝底 b 舍去,蓝底 a 加入
a | b | c | d | a |
mp[s[i++]]++; //前面我提过,i++先赋值后运算,++i先运算后赋值。把蓝底 b 舍去
if(i<=s.length()-p.length()) mp[s[i+p.length()-1]]--; //把黄色 a 加入
(四)总结归纳:
涉及的函数:
1、迭代器unordered_map<char,int> st; 可以看我之前的笔记。
2、字符串截取子串 string str=s.substr(pos,length); pos表示截取字符串s的初始位置,length 表示截取的长度。
二、560. 和为 K 的子数组(前缀和+哈希优化)
(一)题目介绍:
给你一个整数数组 nums
和一个整数 k
,请你统计并返回 该数组中和为 k
的子数组的个数 。
子数组是数组中元素的连续非空序列。
示例 1:
输入:nums = [1,1,1], k = 2 输出:2
(二)代码展示:
1、枚举法:
class Solution {
public:
int subarraySum(vector<int>& nums, int k) {
int n=nums.size();
int sum=0,ans=0;
for(int i=0;i<n;i++){
sum=nums[i];
if(sum==k) ans++;
for(int j=i+1;j<n;j++){
sum+=nums[j];
if(sum==k) ans++;
}
}
return ans;
}
};
2、前缀和+哈希优化:
class Solution {
public:
int subarraySum(vector<int>& nums, int k) {
unordered_map<int,int> mp;
int sum=0,ans=0;
mp[0]=1;
for(auto e:nums){
sum+=e;
if(mp.find(sum-k)!=mp.end()) ans+=mp[sum-k];
mp[sum]++;
}
return ans;
}
};
(三)题目要求与分析:
1、我用枚举法解决该题,然而在我看题解后,受益匪浅(题解非常详细且有动画,这里不做多余的赘述)。
2、值得强调的是
if(mp.find(sum-k)!=mp.end()) ans+=mp[sum-k];
mp[sum]++;
这两段代码的先后顺序,我们必须避免 k=0 时,先执行了mp[num]++操作,会导致结果比预期值更大。例如:
nums=[1],k=0 时,在for循环时,sum=1;如果这时我们先执行了mp[num]++,导致if判断时误判,影响结果。
(四)总结归纳:
算是一时兴起,也许会让你认为我很装、很虚伪。但我不得不提,就当作我学习中的警示,不能因为刷题而刷题,所以请批判性的看待。
在练习时,不应只把目标定在“做对了”这一简单的范畴上。当然,在考试或者(你懂的),结果永远是最重要的(请屏幕前的你深思)。话说不回来,灵感没了,改天吧。
Ⅴ、超乎想象的滑动窗口
一、滑动窗口最大值 优先队列&分块+预处理
(一)问题描述:
给你一个整数数组 nums
,有一个大小为 k
的滑动窗口从数组的最左侧移动到数组的最右侧。你只可以看到在滑动窗口内的 k
个数字。滑动窗口每次只向右移动一位。
返回 滑动窗口中的最大值 。
示例 1:
输入:nums = [1,3,-1,-3,5,3,6,7], k = 3 输出:[3,3,5,5,6,7] 解释: 滑动窗口的位置 最大值 --------------- ----- [1 3 -1] -3 5 3 6 7 3 1 [3 -1 -3] 5 3 6 7 3 1 3 [-1 -3 5] 3 6 7 5 1 3 -1 [-3 5 3] 6 7 5 1 3 -1 -3 [5 3 6] 7 6 1 3 -1 -3 5 [3 6 7] 7
(二)代码展示:
class Solution {
public:
vector<int> maxSlidingWindow(vector<int>& nums, int k) {
int n=nums.size();
priority_queue<pair<int,int>> q;
for(int i=0;i<k;i++) q.emplace(nums[i],i);
vector<int> ans={q.top().first};
for(int i=k;i<n;i++){
q.emplace(nums[i],i);
while(q.top().second<=i-k) q.pop();
ans.push_back(q.top().first);
}
return ans;
}
};
(三)分析:
之前也做过这类题,并没有想把它类为一个模块来收藏下来,直到遇见了这个题。
其实,想到用优先队列对我们来说并不是十分的困难。但有一部分人可能会跟我一样,当我们将队顶元素出队时,若出队元素并不是滑动窗口左端元素时,是不是就会果断放弃。解题人确实心思缜密,想的更深。
while(q.top().second<=i-k) q.pop();
他只用了一个while循环就解决了我的烦恼。那就是不断将栈顶不在滑动窗口内部的元素出队即可。
for(int i=0;i<n-k;i++){
q.emplace(nums[i],i);
}
我从i=0开始,执着于找出队顶元素和移除元素的物理地址。
事实上,在后边的元素入队时,如果栈顶元素在窗口内,就不需要出队操作。如果不在窗口内,那就一直出队,直到找到滑动窗口中最大的元素即可。
二、76. 最小覆盖子串 哈希表&双指针&滑动窗口
(一)问题描述:
给你一个字符串 s
、一个字符串 t
。返回 s
中涵盖 t
所有字符的最小子串。如果 s
中不存在涵盖 t
所有字符的子串,则返回空字符串 ""
。
注意:
- 对于
t
中重复字符,我们寻找的子字符串中该字符数量必须不少于t
中该字符数量。 - 如果
s
中存在这样的子串,我们保证它是唯一的答案。
示例 1:
输入:s = "ADOBECODEBANC", t = "ABC" 输出:"BANC" 解释:最小覆盖子串 "BANC" 包含来自字符串 t 的 'A'、'B' 和 'C'。
(二)代码展示:
未优化(超时):
class Solution {
public:
bool det(unordered_map<char,int> mp,string t){
for(auto e:t)
if(mp[e]>0) return false;
return true;
}
string minWindow(string s, string t) {
int m=s.length();
int n=t.length();
if(m<n) return "";
int length=n;
while(length<=m){
unordered_map<char,int> mp;
for(auto e:t) mp[e]++;
for(int i=0;i<length;i++) mp[s[i]]--;
for(int i=0;i<=m-length;i++){
if(det(mp,t)) return s.substr(i,length);
if(i==m-length) break;
mp[s[i]]++;
mp[s[i+length]]--;
}
length++;
}
return "";
}
};
优化代码(滑动窗口&哈希表&双指针)
class Solution {
public:
unordered_map<char,int> mp1,mp2;
bool check(){
for(auto &e:mp2)
if(mp1[e.first]<e.second) return false;
return true;
}
string minWindow(string s, string t) {
for(auto &e:t) mp2[e]++;
int len=INT_MAX,l=0,r=-1,pre=-1;
while(r<int(s.size())){
if(mp2.find(s[++r])!=mp2.end()) mp1[s[r]]++;
while(check()&&l<=r){
if(r-l+1<len) len=r-l+1,pre=l;
if(mp2.find(s[l])!=mp2.end()) --mp1[s[l]];
l++;
}
}
return pre==-1?string():s.substr(pre,len);
}
};