本文记录作者刷题过程中与字符串相关的题目。
一、分割字符串
1816. 截断句子(简单难度)
class Solution {
public:
/*解析:分割+截断+拼接
最简单的思路是先分割字符串为单词数组,然后反转数组元素,再拼接单词并添加上新的空格,
该思路的难点是根据空格分割字符串为单词数组*/
string truncateSentence(string s, int k) {
// 分割字符串为字符串数组
vector<string> words=splitWord(s);
// 截断字符串数组
vector<string> temp(words.begin(),words.begin()+k);
// 拼接为一个字符串
string res;
for(int i=0;i<temp.size();i++) {
if(i==0)res+=temp[i];
else{
res+=" ";
res+=temp[i];
}
}
return res;
}
vector<string> splitWord(string str){
vector<string> words;
string word;
for(char c:str){
// 遇到空格字符并且word有字符就添加word并且重置word,保证不会添加开头空格
if(c==' '&&word.size()>0){
words.push_back(word);
word="";
}
// 不添加空格
else if(c!=' '){
word+=c;
}
}
// str末位不一定有空格
if(word.size()) words.push_back(word);
return words;
}
};
二、反转字符串
344. 反转字符串(简单难度)
class Solution {
public:
/* 本题题意简单,难点在于原地修改字符数组,显然要使用双指针法,首尾交换即可*/
void reverseString(vector<char>& s) {
int left=0;// 左指针
int right=s.size()-1;// 右指针
// 反转字符串
while(left<right){
swap(s[left++],s[right--]);//交换
}
}
};
541. 反转字符串 II
/*本题的题意是反转一小段字符串中的固定长度,每隔 2k 个字符的就将前 k 个字符进行反转
显然只要实现一个反转指定区间[start,end]的reverse()函数,
接下来问题就变成了遍历指定区间即可*/
void reverse(string& s, int start, int end) {
int left = start;// 左指针
int right = end;//右指针
while(left < right){
swap(s[left], s[right]);
left++;
right--;
}
}
string reverseStr(string s, int k) {
// 遍历字符串[0,len),i每次增加2k
for(int i=0;i<s.size();i+=2*k){
// 2. 剩余字符小于 2k 但大于或等于 k 个,则反转前 k 个字符
if (i + k <= s.size()) {
reverse(s, i, i + k - 1);
}
else{
// 3. 剩余字符少于 k 个,则将剩余字符全部反转。
reverse(s, i, s.size() - 1);
}
}
return s;
}
151. 翻转字符串里的单词(中等难度)
剑指 Offer 58 - I. 翻转单词顺序(简单难度)
方法一:分割+反转+拼接
class Solution {
public:
/*解析:这道题是以空格分隔单词,难点在于开头、中间、结尾的空格数量都不确定。
方法一:分割+反转+拼接
最简单的思路是先分割字符串为单词数组,然后反转数组元素,再拼接单词并添加上新的空格,
该思路的难点是根据空格分割字符串为单词数组。
*/
string reverseWords(string s) {
// 分割字符串为字符串数组
vector<string> words=splitWord(s);
// 反转字符串数组
reverse(words.begin(),words.end());
// 拼接为一个字符串
string res;
for(int i=0;i<words.size();i++) {
if(i==0)res+=words[i];
else{
res+=" ";
res+=words[i];
}
}
return res;
}
vector<string> splitWord(string str){
vector<string> words;
string word;
for(char c:str){
// 遇到空格字符并且word有字符就添加word并且重置word,保证不会添加开头空格
if(c==' '&&word.size()>0){
words.push_back(word);
word="";
}
// 不添加空格
else if(c!=' '){
word+=c;
}
}
// str末位不一定有空格
if(word.size()) words.push_back(word);
return words;
}
};
方法二:原地双指针
class Solution {
public:
/*解析:这道题是以空格分隔单词,难点在于开头、中间、结尾的空格数量都不确定。
方法二:原地双指针(不容易想到)
因为该方法需要反转,所以从后向前遍历:
left,right都从最后一个元素出发,right先走到第一个字符处,left=right,走到第一个空格处
[left+1,right]就是单词所在索引区间,循环重复
*/
string reverseWords(string s) {
string res;// 结果字符串
int n = s.size();
if(n == 0) return res;
int left =n-1;// 左指针
int right = n - 1;// 右指针
while(right >= 0){
//从后往前寻找第一字符(如果不是空且没有到头,right就左移)
while(right >= 0 && s[right] == ' ') right--;
if(right < 0) break;
//从后往前寻找第一个空格
int left = right;
while( left >= 0 && s[left] != ' ' ) left--;
//添加单词到结果,单词所在位置[left+1,right]
res += s.substr(left + 1, right - left);
res += ' ';
//继续往前分割单词
right = left;
}
//去除最后一个字符空格
if (!res.empty()) res.pop_back();
return res;
}
}
186. 翻转字符串里的单词 II(中等难度)
/* 本题的是反转一个句子中的单词,难点在于必须使用原地算法,即双指针法。
先对每个单词进行reverse,再对整个字符串reverse,即可得到翻转单词顺序,但是单词中字母顺序不变的效果
因为是反转,所以从后向前遍历,left指针和right指针都指向字符串最后一个字符
right先从后向前寻找第一个字符,占据最后一个单词的right
left从left出发
*/
void reverseWords(vector<char>& s) {
int left = 0;//左指针
int right = 0;//右指针
int len = s.size();
while (left < len) {
// left从左到右找到第一个非空字符
while(left<len&&s[left]==' ') left++;
if(left>=len) break;//left不可以=len,后续要使用left
// right从left出发,找到第一个空字符
right=left;
while(right<len&&s[right]!=' ') right++;
// 反转[left,right-1]
reverse(s,left,right-1);
// 更新right
left=right;
}
reverse(s,0,len-1);
}
void reverse(vector<char>& s,int start,int end){
int left=start;
int right=end;
while(left<right){
swap(s[left],s[right]);
left++;
right--;
}
}
三、旋转字符串
剑指 Offer 58 - II. 左旋转字符串(简单难度)
class Solution {
public:
/*解析:本题的意思是将字符串的[0,n)移动到字符串的末尾
方法一:列表遍历拼接(借助额外内存空间):先拼接[n,s.length()),再拼接[0,n)
方法二:列表遍历拼接(取余操作简化)
方法三:3次反转:反转[0,n);反转[n,s.length());反转[0,s.length())
*/
string reverseLeftWords(string s, int n) {
//方法一:列表遍历拼接(借助额外内存空间)
// string res;
// // 获取[n,s.length())加入res中
// for(int i=n;i<s.length();i++){
// res+=s[i];
// }
// // 获取[0,n)加入res中
// for(int i=0;i<n;i++){
// res+=s[i];
// }
// return res;
//方法二:列表遍历拼接(取余操作简化)
// string res;
// int len=s.length();
// for(int i=n;i<n+len;i++){
// res+=s[i%len];
// }
// return res;
//方法三:巧妙反转
reverse(s.begin(),s.begin()+n);
reverse(s.begin()+n,s.end());
reverse(s.begin(),s.end());
return s;
}
};
796. 旋转字符串(简单难度)
class Solution {
public:
/*本题是判断s是否能够通过多次左旋转来得到goal
我们已经知道左旋转字符串的操作(剑指 Offer 58 - II. 左旋转字符串)
那么可以通过模拟左旋转来判断,左旋转的次数是有限的:1~s.length()
*/
bool rotateString(string s, string goal) {
// 旋转1~s.length()次
for(int n=1;n<=s.length();n++){
// 左旋转1个字符
reverse(s.begin(),s.begin()+1);
reverse(s.begin()+1,s.end());
reverse(s.begin(),s.end());
// 检查本次旋转后是否==goal
if(s==goal) return true;
}
return false;
}
};
四、匹配字符串
28. 实现 strStr()(简单难度)
class Solution {
public:
/*本题是字符串匹配的经典问题,在文本串txt中查找是否存在模式串ptn
方法一:暴力搜索:时间复杂度O(mn),不需要额外空间
首先遍历文本串txt,让当前字符c和模式串的第一个字符ptn[0]比较,
如果相等就遍历ptn的其他字符串,看是否继续相等,不相等就退出内层循环。
方法二:KMP算法:花费更多空间,效率更高
*/
//获取前缀表(本质上就是一个dp数组),其只和ptn有关
int* getNext(string& s) {
int n=s.size();
//初始next数组
int next[n];
next[0] = 0;//初始装填,next数组第一个一定是0
int j = 0;//j指向前缀末尾位置,从0开始,前缀的长度,前缀的指示长度
int i = 1;//i指向后缀末尾位置,从1开始
//遍历模式串ptn的最长后缀
for(; i < n; i++) //注意i从1开始
{
//如果后缀末尾字符和前缀末尾字符不一样,j回退
while (j > 0 && s[i] != s[j]) j = next[j-1];
//如果后缀末尾字符和前缀末尾字符一样,说明找到了相同的前后缀,则前缀++
if (s[i] == s[j]) j++;
// 记录此时前后缀的最长长度
next[i] = j;// 将j(前缀的长度)赋给next[i]
}
return next;
}
//获取前缀表(本质上就是一个dp数组),其只和ptn有关
int* getNext1(string& s) {
int n=s.size();
//初始next数组
int next[n];
next[0] = -1;//j=0时,
int j = 0;//j指向前缀起始位置,从0开始,前缀的长度,前缀的指示长度
int i = -1;//当P[j]!=T[i]时,j指针的下一步移动位置
//遍历模式串ptn的前缀
while(j < n-1) //注意i从1开始
{
// 当前前缀字符和当前后缀字符相同
if (i == -1 || s[j] == s[i])
{
j++; i++;
next[j] = i;
}
else i = next[i];
}
return next;
}
int strStr(string txt, string ptn) {
//如果模式串为空,返回0
if(ptn=="") return 0;
//获取前缀表
int* next=getNext(ptn);
int j = 0;//负责遍历模式串的字符,和txt字符相同时移动,移动到末尾时说明匹配成功
//循环遍历文本串每个字符
for (int i = 0; i < txt.size(); i++) {
//如果txt当前字符和ptn当前字符不同,借助前缀表回溯模式串当前字符
while(j > 0 && txt[i] != ptn[j]) j = next[j-1];
//如果txt当前字符和ptn当前字符相同,ptn当前字符+1
if (txt[i] == ptn[j]) j++;
//如果j指向末尾,说明匹配完毕,返回txt的对应初始索引
if (j == ptn.size() ) return (i - ptn.size() + 1);
}
return -1;
}
};
459. 重复的子字符串(简单难度)
//获取前缀表(本质上就是一个dp数组),其只和ptn有关
int* getNext(string& s) {
int n=s.size();
//初始next数组
int next[n];
next[0] = 0;//初始装填,next数组第一个一定是0
int j = 0;//j指向前缀末尾位置,从0开始,前缀的长度,前缀的指示长度
int i = 1;//i指向后缀末尾位置,从1开始
//遍历模式串ptn的最长后缀
for(; i < n; i++) //注意i从1开始
{
//如果后缀末尾字符和前缀末尾字符不一样,j回退
while (j > 0 && s[i] != s[j]) j = next[j-1];
//如果后缀末尾字符和前缀末尾字符一样,说明找到了相同的前后缀,则前缀++
if (s[i] == s[j]) j++;
// 记录此时前后缀的最长长度
next[i] = j;// 将j(前缀的长度)赋给next[i]
}
return next;
}
bool repeatedSubstringPattern(string s) {
//获取前缀表
int* next=getNext(s);
//如果next[len - 1] != 0则表示存在最长相等前后缀,如果数组长度可以被周期长度整除
int len = s.size();
if (next[len - 1] != 0 && len % (len - next[len - 1]) == 0) {
return true;
}
return false;
}
剑指 Offer 20. 表示数值的字符串(中等难度)
class Solution {
public:
/*该题是判断一个字符串是否可以表示为一个数值
字符串中可能存在首尾空格、小数点、e/E、正负号
1、空格只会出现在首尾,所以可以首先通过双指针法去除
2、正负号只能出现在首位或者e/E后一位
3、小数点只能出现一次,其前后至少有一个地方出现一个数字,不能出现e/E
4、e/E只能出现一次并且其前后都必须存在至少一个数字,不能出现小数点
5、整个字符串至少存在一个数字*/
bool isNumber(string s) {
if(s.length()==0) return false;
//去掉s的首尾部空格
int left=0,right=s.length()-1;
while(left<s.length()&&s[left]==' ') left++;
while(right>=0&&s[right]==' ')right--;
if(right>=left) s = s.substr(left,right-left+1);
else return false;
//
bool numFlag = false;// 是否出现数字,确保至少出现1个数字
bool dotFlag = false;//是否出现过一次小数点,确保只出现1次
bool eFlag = false;// 是否出现过一次e/E,确保只出现1次
// 遍历整个字符串
for (int i = 0; i < s.length(); i++) {
//如果是数字,numFlag设为true
if (s[i] >= '0' && s[i] <= '9') numFlag = true;
//如果是+/-,且出现在第1位或者e/E后一位,符合
else if ((s[i] == '+' || s[i] == '-') && (i == 0 || s[i-1] == 'e' || s[i-1] == 'E')) continue;
//如果是. 且之前没出现过也没出现过e/E, dotFlag = true;
else if (s[i] == '.' && !dotFlag &&!eFlag) dotFlag = true;
//判定为e,且之前没出现过也没出现过.并且出过数字
else if ((s[i] == 'e' || s[i] == 'E') && !eFlag && numFlag) {
eFlag = true;
numFlag = false;//为了避免e后面没有数字,出现e之后就标志为false
}
//其他情况,都是非法的
else return false;
}
return numFlag;
}
};
五、字符串排序
剑指 Offer 45. 把数组排成最小的数(中等难度)
class Solution {
public:
/*解析:本题是一个排序问题,但难点在于如何比较2个数?
小的数排在前面不一定数就小,例如3和30,330>303.
解决方案是将整数转成字符串,这样排序a和b两个数时,可以比较a+b和b+a谁更大
在代码实现时需要知道如何实现自定义排序方式
*/
// 自定义排序方式
static bool cmp(string& a,string& b){
return a + b < b + a;
}
string minNumber(vector<int>& nums) {
// 将整数数组转换为字符串数组
vector<string> strs;
for(int num:nums) strs.push_back(to_string(num));
// 自定义排序
sort(strs.begin(), strs.end(), cmp);
// 将字符串数组拼接为字符串
string res;
for(string s : strs) res+=s;
return res;
}
};
六、替换字符串
剑指 Offer 05. 替换空格(简单难度)
class Solution {
public:
/*本题是将字符串中的空格替换成指定的字符串,难点在于指定字符的长度大于1,不能直接替换原有的空格字符
方法一:额外空间
新建一个结果字符串,
遍历原有字符串,
如果当前字符是空格,就将替换字符串加入结果字符串;
否则就将当前字符加入结果字符串。
方法二:原地指针法
*/
string replaceSpace(string s) {
// 方法一:使用额外空间
// string res;
// for(char c:s) {
// if(c == ' ') res+="%20";
// else res+=c;
// }
// return res;
// 方法二:扩充原字符串大小,同时从后向前
int count = 0; // 统计空格的个数
int sOldSize = s.size();
for (int i = 0; i < s.size(); i++) {
if (s[i] == ' ') {
count++;
}
}
// 扩充字符串s的大小,也就是每个空格替换成"%20"之后的大小
s.resize(s.size() + count * 2);
int sNewSize = s.size();
// 同时从后向前遍历原有字符串,将空格替换为"%20"
// [0,Oldlen)
// [0, NewLen)
int fast=sOldSize - 1;// 负责遍历原字符串的字符
int slow=sNewSize - 1;// 负责指向
while(fast>=0||slow>=0){
if (s[fast] != ' ') {
s[slow] = s[fast];
} else {
s[slow] = '0';
s[slow-1] = '2';
s[slow-2] = '%';
slow-=2;
}
fast--;
slow--;
}
return s;
}
};
七、字典树
208. 实现 Trie (前缀树)(中等难度)
/ 字典树节点
struct TrieNode{
bool isEnd;// 从根节点到当前节点至此是否是一个完整的单词(该节点是否是一个单词的结尾)
vector<TrieNode*> children;//子节点数组,数组下标为[0,25],作为26个字母,存储子节点
TrieNode():isEnd(false),children(26){} // 构造函数
};
class Trie {
private:
TrieNode* root;
public:
Trie(){root = new TrieNode();}// 构造函数
// 向前缀树中插入字符串word
void insert(string word) {
// 按照word的字符,从根节点开始,一直向下走:
TrieNode* cur=root;//当前节点
for(char c:word){
int index=c-'a';
// 如果当前节点对应的子节点为空,就新建。不为空直接跳过
if(cur->children[index]==nullptr) cur->children[index]=new TrieNode();
cur=cur->children[index];//更新当前节点
}
cur->isEnd=true;//最后一个单词的节点是结尾
}
// 搜索字符串word是否在前缀树中
bool search(string word) {
// TrieNode* cur = root;
// for (char c:word) {
// int index=c-'a';
// if (cur->children[index] == nullptr) return false;
// cur=cur->children[index];//更新当前节点
// }
// return cur->isEnd;
return match(word, root, 0);
}
bool match(string word, TrieNode* node, int curIndex){
if(node==nullptr) return false;
if(curIndex==word.length()) return node->isEnd;// 当前索引已经越界,不必再递归
int index=word[curIndex]-'a';// 当前字符的索引
bool isExist=node->children[index] != nullptr;//当前节点的子节点是否对应当前字符
bool nextExit=match(word,node->children[index],curIndex+1);//当前节点子节点的子节点是否对应下一个字符
return isExist&&nextExit;
}
// 搜索是否存在前缀为prefix的word
bool startsWith(string prefix) {
TrieNode* cur = root;
for (char c:prefix) {
int index=c-'a';
if (cur->children[index] == nullptr) return false;
cur=cur->children[index];//更新当前节点
}
return true;
}
};
820. 单词的压缩编码(中等难度)
// 字典树节点
struct TrieNode{
bool isEnd;// 从根节点到当前节点至此是否是一个完整的单词(该节点是否是一个单词的结尾)
vector<TrieNode*> children;//子节点数组,数组下标为[0,25],作为26个字母,存储子节点
TrieNode():isEnd(false),children(26){}// 构造函数
};
// 字典树
class Trie{
TrieNode* root;
public:
Trie(){root = new TrieNode();}// 构造函数
//向字典树插入单词word
int insert(string word){
// 按照word的字符,从根节点开始,一直向下走:
TrieNode* cur=root;//当前节点
bool isNew = false;//是否是新插入单词
for(char c:word){
int index=c-'a';
// 如果当前节点对应的子节点为空,就新建。不为空直接跳过
if(cur->children[index]==nullptr) {
cur->children[index]=new TrieNode();
isNew = true;
}
cur=cur->children[index];//更新当前节点
}
cur->isEnd=true;//最后一个单词的节点是结尾
return isNew? word.length() + 1 : 0;
}
};
class Solution {
public:
/* 本题的题意是:给定多个单词,将其编码为一个助记字符串s,其中多个单词可能会由于后缀相同就合并到一起
要求返回助记字符串s的最小长度
思路:后缀字典树
后缀相同或者前缀相同,联想到字典树,考虑将所有单词加入反缀的字典树,
每次往字典树插入一个"新的word"时,就 += 该word的长度 + 1(#)
需要注意的是,我们必须先插入长单词,再插入旧单词
*/
static bool cmp(string a,string b){
return a.length()>b.length();
}
int minimumLengthEncoding(vector<string>& words) {
int res=0;
// 将单词词组按照单词长度从大到小排序,先插入长单词
sort(words.begin(),words.end(),cmp);
// 向字典树中插入所有单词
Trie* trie = new Trie();
for(string word : words){
reverse(word.begin(),word.end());//反转单词,因为我们定义的字典树是前缀树
res += trie->insert(word);//插入新单词,返回单词长度+1;插入旧单词,返回0
}
return res;
}
};
676. 实现一个魔法字典(中等难度)
// 字典树节点
struct TrieNode{
bool isEnd;// 从根节点到当前节点至此是否是一个完整的单词(该节点是否是一个单词的结尾)
vector<TrieNode*> children;//子节点数组,数组下标为[0,25],作为26个字母,存储子节点
TrieNode():isEnd(false),children(26){}// 构造函数
};
class MagicDictionary {
TrieNode* root;
public:
MagicDictionary() {root = new TrieNode();}
void insert(string word){
// 按照word的字符,从根节点开始,一直向下走:
TrieNode* cur=root;//当前节点
for(char c:word){
int index=c-'a';
// 如果当前节点对应的子节点为空,就新建。不为空直接跳过
if(cur->children[index]==nullptr) cur->children[index]=new TrieNode();
cur=cur->children[index];//更新当前节点
}
cur->isEnd=true;//最后一个单词的节点是结尾
}
void buildDict(vector<string> dictionary) {
for(string word:dictionary) insert(word);
}
bool search(string word) {
return match(word, root, 0, true);
}
bool match(string word, TrieNode* node, int start, bool hasChance){
if(start == word.length()){
return node->isEnd && !hasChance; // 因为"必须变一个字符",因此 "&& !hasChance"
}
for(int i = 0; i < 26; i++){
if(node->children[i] != nullptr){
int index=word[start]-'a';
if(index == i && match(word, node->children[i], start + 1, hasChance))return true;
if(index != i && hasChance && match(word, node->children[i], start + 1, false))return true;
}
}
return false;
}
};