【LeetCode】5.字符串系列
总目录:
0.理论基础
0.1.要点
很简单。
字符串可以视作char数组,可以使用索引访问char。
有很多标准库可以用,如可以很方便获得长度。
每个char是一个ASCII数值。
1.翻转字符串
1.1.问题描述
编写一个函数,其作用是将输入的字符串反转过来。输入字符串以字符数组 s 的形式给出。
不要给另外的数组分配额外的空间,你必须原地修改输入数组、使用 O(1) 的额外空间解决这一问题。
示例 1:输入:s = ["h","e","l","l","o"]输出:["o","l","l","e","h"]
链接:https://leetcode.cn/problems/reverse-string
1.2.要点
注意要求空间复杂度O(1)
1双指针,对撞即可
1.3.代码实例
双指针
1 class Solution { 2 public: 3 void reverseString(vector<char>& s) { 4 int dataLen=s.size(); 5 if(dataLen<=0){ 6 return; 7 } 8 9 int left=0,right=dataLen-1; 10 while(left<right){ 11 swap(s[left],s[right]); 12 left++; 13 right--; 14 } 15 } 16 };
2.翻转字符串2
2.1.问题描述
给定一个字符串 s 和一个整数 k,从字符串开头算起,每计数至 2k 个字符,就反转这 2k 字符中的前 k 个字符。
(1)如果剩余字符少于 k 个,则将剩余字符全部反转。
(2)如果剩余字符小于 2k 但大于或等于 k 个,则反转前 k 个字符,其余字符保持原样。
示例 1:输入:s = "abcdefg", k = 2;输出:"bacdfeg"
链接:https://leetcode.cn/problems/reverse-string-ii
2.2.要点
这是一种模拟题,按间距翻转,翻转一段不翻转一段
1迭代+状态机,保存当前段是否应该翻转
2双指针
左指针每步长2k,右指针为左指针+k-1,注意右指针初始化时不要超过s长度
2.3.代码实例
1状态机
1 class Solution { 2 public: 3 string reverseStr(string s, int k) { 4 int dataLen=s.length(); 5 string ret; 6 if(dataLen<=0){ 7 return ret; 8 } 9 10 11 bool needRev=true;//决定正序还是逆序,默认逆序 12 for(int i=0;i<dataLen;i+=k){ 13 if(needRev){ 14 for(int j=k-1;j>=0;j--){ 15 if((i+j)>=dataLen){ 16 continue; 17 } 18 ret+=s[i+j]; 19 } 20 } 21 else{ 22 for(int j=0;j<k;j++){ 23 if((i+j)>=dataLen){ 24 continue; 25 } 26 ret+=s[i+j]; 27 } 28 } 29 needRev=!needRev; 30 } 31 32 return ret; 33 } 34 };
2双指针
1 class Solution { 2 public: 3 string reverseStr(string s, int k) { 4 int dataLen=s.length(); 5 int left=0,right=0; 6 7 for(; left < dataLen; left += 2*k){ 8 right = min(left+k, dataLen); 9 reverse(s.begin()+left, s.begin() + right); 10 } 11 12 return s; 13 } 14 };
3.替换空格
3.1.问题描述
请实现一个函数,把字符串 s 中的每个空格替换成"%20"。
示例 1:输入:s = "We are happy.";输出:"We%20are%20happy."
链接:https://leetcode.cn/problems/ti-huan-kong-ge-lcof
3.2.要点
这里的方法区别仅仅是空间复杂度O(1)还是O(n),
即原地修改还是新建字符串。
3.3.代码实例
原地修改,双指针
1 class Solution { 2 public: 3 string replaceSpace(string s) { 4 int count = 0, len = s.size(); 5 // 统计空格数量 6 for (char c : s) { 7 if (c == ' ') count++; 8 } 9 // 修改 s 长度 10 s.resize(len + 2 * count); 11 // 倒序遍历修改 12 for(int i = len - 1, j = s.size() - 1; i < j; i--, j--) { 13 if (s[i] != ' ') 14 s[j] = s[i]; 15 else { 16 s[j - 2] = '%'; 17 s[j - 1] = '2'; 18 s[j] = '0'; 19 j -= 2; 20 } 21 } 22 return s; 23 } 24 };
新建字符串
1 class Solution { 2 public: 3 string replaceSpace(string s) { 4 int n=s.size(); 5 string str=""; 6 for(int i=0;i<n;++i) 7 str=(s[i]==' ' ? str+"%20" : str+s[i]); 8 return str; 9 } 10 };
4.翻转字符串中的单词
4.1.问题描述
给你一个字符串 s ,请你反转字符串中 单词 的顺序。
单词 是由非空格字符组成的字符串。s 中使用至少一个空格将字符串中的 单词 分隔开。
返回 单词 顺序颠倒且 单词 之间用单个空格连接的结果字符串。
注意:输入字符串 s中可能会存在前导空格、尾随空格或者单词间的多个空格。返回的结果字符串中,单词间应当仅用单个空格分隔,且不包含任何额外的空格。
链接:https://leetcode.cn/problems/reverse-words-in-a-string
4.2.要点
1两次翻转,非原地操作,先翻转整体,再翻转局部
2双端队列,插入内容比较方便
4.3.代码实例
两次翻转
1 class Solution { 2 public: 3 string reverseWords(string s) { 4 int dataLen=s.length(); 5 string newStr; 6 7 reverse(s.begin(),s.end()); 8 int left=0,right=0; 9 while(left<dataLen){ 10 //寻找第一个非空格 11 if(s[left]==' '){ 12 left++; 13 continue; 14 } 15 16 //寻找右边界 17 right=left; 18 while(right<dataLen){ 19 if(s[right]==' '){ 20 break; 21 } 22 right++; 23 } 24 25 //翻转局部 26 reverse(s.begin()+left,s.begin()+right); 27 for(int i=left;i<right;i++){ 28 newStr+=s[i]; 29 } 30 31 //查看后面是否还有有效数据 32 int nextAvaPos = 0; 33 for(int i = right; i < dataLen; i++){ 34 if(s[i] != ' '){ 35 nextAvaPos = i; 36 break; 37 } 38 } 39 40 //决定是否添加间隔符并继续迭代 41 if(nextAvaPos!=0){ 42 newStr+=' ';//附加间隔符 43 left=nextAvaPos; 44 } 45 else{ 46 break; 47 } 48 } 49 50 return newStr; 51 } 52 };
5.左旋转字符串
5.1.问题描述
字符串的左旋转操作是把字符串前面的若干个字符转移到字符串的尾部。请定义一个函数实现字符串左旋转操作的功能。比如,输入字符串"abcdefg"和数字2,该函数将返回左旋转两位得到的结果"cdefgab"。
示例 1:输入: s = "abcdefg", k = 2;输出: "cdefgab"
链接:https://leetcode.cn/problems/zuo-xuan-zhuan-zi-fu-chuan-lcof
5.2.要点
1字符串裁剪
2字符遍历
3三次翻转
5.3.代码实例
三次翻转
1 class Solution { 2 public: 3 string reverseLeftWords(string s, int n) { 4 /* 反转n前面的字符串 */ 5 reverse(s.begin(), s.begin() + n); 6 /* 反转n后面的字符串 */ 7 reverse(s.begin() + n, s.end()); 8 /* 反转整个字符串 */ 9 reverse(s.begin(), s.end()); 10 11 return s; 12 } 13 };
6.找出字符串第一个匹配项的下标
6.1.问题描述
给你两个字符串 haystack 和 needle ,请你在 haystack 字符串中找出 needle 字符串的第一个匹配项的下标(下标从 0 开始)。如果 needle 不是 haystack 的一部分,则返回 -1 。
示例 1:输入:haystack = "sadbutsad", needle = "sad";输出:0;解释:"sad" 在下标 0 和 6 处匹配。第一个匹配项的下标是 0 ,所以返回 0 。
示例 2:输入:haystack = "leetcode", needle = "leeto";输出:-1;解释:"leeto" 没有在 "leetcode" 中出现,所以返回 -1 。
链接:https://leetcode.cn/problems/find-the-index-of-the-first-occurrence-in-a-string
6.2.要点
1模拟,枚举,状态机
从匹配上第一个字符开始,就检查是否能匹配上后面所有
2KMP
6.3.代码实例
模拟、枚举
1 class Solution { 2 public: 3 int strStr(string s, string p) { 4 int n = s.size(), m = p.size(); 5 for(int i = 0; i <= n - m; i++){ 6 int j = i, k = 0; 7 while(k < m and s[j] == p[k]){ 8 j++; 9 k++; 10 } 11 if(k == m) return i; 12 } 13 return -1; 14 } 15 };
KMP
1 class Solution { 2 public: 3 int strStr(string s, string p) { 4 int n = s.size(), m = p.size(); 5 if(m == 0) return 0; 6 //设置哨兵 7 s.insert(s.begin(),' '); 8 p.insert(p.begin(),' '); 9 vector<int> next(m + 1); 10 //预处理next数组 11 for(int i = 2, j = 0; i <= m; i++){ 12 while(j and p[i] != p[j + 1]) j = next[j]; 13 if(p[i] == p[j + 1]) j++; 14 next[i] = j; 15 } 16 //匹配过程 17 for(int i = 1, j = 0; i <= n; i++){ 18 while(j and s[i] != p[j + 1]) j = next[j]; 19 if(s[i] == p[j + 1]) j++; 20 if(j == m) return i - m; 21 } 22 return -1; 23 } 24 };
7.重复的子字符串
7.1.问题描述
给定一个非空的字符串 s ,检查是否可以通过由它的一个子串重复多次构成。
示例 1:输入: s = "abab";输出: true;解释: 可由子串 "ab" 重复两次构成。
链接:https://leetcode.cn/problems/repeated-substring-pattern
7.2.要点
1模拟,状态机
子串增长,当母串的下一个字符与子串头相同时,进入尝试使用子串匹配后面所有内容的环节
2kmp算法,暂未看懂
7.3.代码实例
模拟,状态机
1 class Solution { 2 public: 3 //start\end都是闭 4 bool isMatch(string str,int start,string match){ 5 int tgtLen=match.length(); 6 if(str.length() < (start+tgtLen)){ 7 return false; 8 } 9 10 for(int i=0;i<tgtLen;i++){ 11 if(str[start+i]!=match[i]){ 12 return false; 13 } 14 } 15 16 return true; 17 } 18 19 bool repeatedSubstringPattern(string s) { 20 int dataLen=s.length(); 21 string strSub; 22 int strSubSize=0; 23 for(int i=0;i<dataLen;i++){ 24 strSub+=s[i]; 25 26 //如果前方到头或与子串开头不同,则继续 27 if(i == (dataLen-1) || s[i+1]!=strSub[0]){ 28 continue; 29 } 30 31 //是否可以被整除 32 strSubSize=strSub.size(); 33 if(dataLen%strSubSize!=0){ 34 continue; 35 } 36 37 //循环检查后面的内容 38 int start = i+1; 39 while(start < dataLen){ 40 //查询是否匹配 41 if(!isMatch(s,start,strSub)){ 42 break; 43 } 44 start += strSubSize; 45 if(start >= dataLen){ 46 return true; 47 } 48 } 49 } 50 51 52 return false; 53 } 54 };
KMP算法
1 class Solution { 2 public: 3 bool kmp(const string& query, const string& pattern) { 4 int n = query.size(); 5 int m = pattern.size(); 6 vector<int> fail(m, -1); 7 for (int i = 1; i < m; ++i) { 8 int j = fail[i - 1]; 9 while (j != -1 && pattern[j + 1] != pattern[i]) { 10 j = fail[j]; 11 } 12 if (pattern[j + 1] == pattern[i]) { 13 fail[i] = j + 1; 14 } 15 } 16 int match = -1; 17 for (int i = 1; i < n - 1; ++i) { 18 while (match != -1 && pattern[match + 1] != query[i]) { 19 match = fail[match]; 20 } 21 if (pattern[match + 1] == query[i]) { 22 ++match; 23 if (match == m - 1) { 24 return true; 25 } 26 } 27 } 28 return false; 29 } 30 31 bool repeatedSubstringPattern(string s) { 32 return kmp(s + s, s); 33 } 34 };
8.总结
8.1.KMP要点
KMP算法https://github.com/hitwzy/leetcode-master/blob/master/problems/0028.%E5%AE%9E%E7%8E%B0strStr.md
KMP主要思想是当出现字符串不匹配时,可以知道一部分之前已经匹配的文本内容,可以利用这些信息避免从头再去做匹配
精髓所在就是前缀表。
前缀:指不包含最后一个字符的所有以第一个字符开头的连续子串。
后缀:指不包含第一个字符的所有以最后一个字符结尾的连续子串。
针对前缀表到底要不要减一,这其实是不同KMP实现的方式。
其中主要理解j=next[x]这一步最为关键。
8.2常用套路
双指针法是字符串处理的常客,因为字符串可以很方便地获取总长度、最后的地址。
xxx.问题
xxx.1.问题描述
111
xxx.2.要点
222
xxx.3.代码实例
333