字符串,库函数,食之有味,弃之当可惜!但若长久时,面试官刷之,竟无语凝噎
C++的string提供接口size()可以方便地把握大小,而在c中字符串是以'\0'结尾的,判断语句为:
char a[5] = "asd"; for (int i = 0; a[i] != '\0'; i++) { }
vector<char> 和string其实一样,但是string有接口,重载了+,等等,用起来爽的。
反转字符串
344. 反转字符串 - 力扣(LeetCode) (leetcode-cn.com)
编写一个函数,其作用是将输入的字符串反转过来。输入字符串以字符数组 char[] 的形式给出。
不要给另外的数组分配额外的空间,你必须原地修改输入数组、使用 $O(1)$ 的额外空间解决这一问题。
你可以假设数组中的所有字符都是 ASCII 码表中的可打印字符。
示例 1:
输入:["h","e","l","l","o"]
输出:["o","l","l","e","h"]
可以使用 s.reverse() 来实现,但是核心代码还是自己写比较好
void reverseString(vector<char>& s) { for(int i=0,j=s.size()-1;i<s.size()/2;i++,j--){ swap(s[i],s[j]); } }
python原地修改
from typing import List def reverseString(s: List[str]) -> None: """ Do not return anything, modify s in-place instead. """ left, right = 0, len(s) - 1 while left < right: s[left], s[right] = s[right], s[left] left += 1 right -= 1 s = ['n','c','k','j','h','f','e','q'] reverseString(s) print(s)
反转字符串第二题
541. 反转字符串 II - 力扣(LeetCode) (leetcode-cn.com)
给定一个字符串 s 和一个整数 k,你需要对从字符串开头算起的每隔 2k 个字符的前 k 个字符进行反转。
如果剩余字符少于 k 个,则将剩余字符全部反转。
如果剩余字符小于 2k 但大于或等于 k 个,则反转前 k 个字符,其余字符保持原样。
示例:
输入: s = "abcdefg", k = 2
输出: "bacdfeg"
- 主要是想清楚逻辑,什么时候可以反转那k个字符,反转的起始位置和结束位置怎么写
string reverseStr(string s, int k) { if(k==0) return s; for(int i = 0; i < s.size();i += 2 * k){ //判断是否可以反转前k部分的字符 if(i+k <= s.size()){ //非核心代码,可以用接口 reverse(s.begin()+i,s.begin()+i+k); continue; } //末尾字符不够k, 全部反转 reverse(s.begin()+i,s.end()); } return s; }
剑指Offer 05.替换空格
剑指 Offer 05. 替换空格 - 力扣(LeetCode) (leetcode-cn.com)
请实现一个函数,把字符串 s 中的每个空格替换成"%20"。
示例 1:
输入:s = "We are happy."
输出:"We%20are%20happy."
既然是替换,空间很可能是变大了
c++ 中的string 是可变的 所以 如果是' ' 变 '%20' 要考虑内存空间变大了
可以先增大空间,再重后向前修改数组减小复杂度
也可以说是用双指针。令i指向旧长度的末尾,j指向新长度的末尾,当i=j时就做完了替换:时间复杂度:$O(n)$, 空间复杂度:$O(1)$
string replaceSpace(string s) { int count=0; int oldSize = s.size(); for(int i=0;i<oldSize;i++){ if(s[i]==' '){ count++; } } //重新修改大小 s.resize(s.size()+2*count); //从后向前修改-双指针 int newSize = s.size(); for(int i=oldSize-1,j=newSize-1;i<j;i--,j--){ if(s[i]!=' '){ s[j]=s[i]; }else{ s[j]='0'; s[j-1]='2'; s[j-2]='%'; j-=2; } } return s; }
如果是用python写,可以用切片操作一次性替换掉'%20'
...
left, right = len(s) - 1, len(res) - 1
while left >= 0:
if res[left] != ' ':
res[right] = res[left]
right -= 1
else:
# [right - 2, right), 左闭右开
res[right - 2: right + 1] = '%20'
right -= 3
left -= 1
....
翻转字符串里的单词
151. 翻转字符串里的单词 - 力扣(LeetCode) (leetcode-cn.com)
给定一个字符串,逐个翻转字符串中的每个单词
- 输入字符串可以在前面或者后面包含多余的空格,但是反转后的字符不能包括。
- 如果两个单词间有多余的空格,将反转后单词间的空格减少到只含一个
- 不要使用辅助空间,空间复杂度要求为$O(1)$。
题解:
首先要去除头尾空格,再去除重复空格,然后反转各个单词。
反转各个单词怎么做?可以先反转整个字符串,然后逐个单词再做翻转。
移除冗余空格
移除空格很难吗?当然,移除之后字符串变小了,位置要做改动,参考卡哥代码随想录 (programmercarl.com)
如果用erase来做,一个erase是$O(n)$的操作,如果外面加一层for循环,就是$O(n^2)$了,所以说,自己写移除空格算法:
使用双指针法来去移除空格,最后resize(重新设置)一下字符串的大小,就可以做到$O(n)$的时间复杂度。
//双指针法 移除空格算法 O(n)O(1) void removeExtraSpaces(string& s) { int slowIndex = 0, fastIndex = 0; // 定义快指针,慢指针 // 去掉字符串前面的空格 while (s.size() > 0 && fastIndex < s.size() && s[fastIndex] == ' ') { fastIndex++; } for (; fastIndex < s.size(); fastIndex++) { // 去掉字符串中间部分的冗余空格 if (fastIndex - 1 > 0 && s[fastIndex - 1] == s[fastIndex] && s[fastIndex] == ' ') { continue; } else { s[slowIndex++] = s[fastIndex]; } } if (slowIndex - 1 > 0 && s[slowIndex - 1] == ' ') { // 去掉字符串末尾的空格 s.resize(slowIndex - 1); } else { s.resize(slowIndex); // 重新设置字符串大小 } }
如果仅仅只是移除元素可以这样写:
// 时间复杂度:O(n) // 空间复杂度:O(1) class Solution { public: int removeElement(vector<int>& nums, int val) { int slowIndex = 0; for (int fastIndex = 0; fastIndex < nums.size(); fastIndex++) { if (val != nums[fastIndex]) { nums[slowIndex++] = nums[fastIndex]; } } return slowIndex; } };
翻转单词
可以用reverse(begin, end)来做, 或者自己写swap核心代码
// 反转字符串s中左闭又闭的区间[start, end] void reverse(string& s, int start, int end) { for (int i = start, j = end; i < j; i++, j--) { swap(s[i], s[j]); } }
总体代码
class reverseWordSolution { public: void removeExtraSpaces(string& s) { int slowIndex = 0, fastIndex = 0; // 定义快指针,慢指针 // 去掉字符串前面的空格 while (s.size() > 0 && fastIndex < s.size() && s[fastIndex] == ' ') { fastIndex++; } for (; fastIndex < s.size(); fastIndex++) { // 去掉字符串中间部分的冗余空格 if (fastIndex - 1 > 0 && s[fastIndex - 1] == s[fastIndex] && s[fastIndex] == ' ') { continue; } else { s[slowIndex++] = s[fastIndex]; } } if (slowIndex - 1 > 0 && s[slowIndex - 1] == ' ') { // 去掉字符串末尾的空格 s.resize(slowIndex - 1); } else { s.resize(slowIndex); // 重新设置字符串大小 } } // 反转字符串s中左闭又闭的区间[start, end] void reverse(string& s, int start, int end) { for (int i = start, j = end; i < j; i++, j--) { swap(s[i], s[j]); } } string reverseWords(string s) { removeExtraSpaces(s); //反转字符串 reverse(s, 0, s.size() - 1); //反转每个单词,判断空格位置 for (int i = 0; i < s.size(); i++) { int j = i; while (j < s.size() && s[j] != ' ') j++; reverse(s, i, j - 1); //更新要反转的单词开始位置 i = j; } return s; } };
剑指Offer58-II.左旋转字符串
剑指 Offer 58 - II. 左旋转字符串 - 力扣(LeetCode) (leetcode-cn.com)
字符串的左旋转操作是把字符串前面的若干个字符转移到字符串的尾部。
- 比如,输入字符串"abcdefg"和数字2,该函数将返回左旋转两位得到的结果"cdefgab"。
- 提升难度:不能申请额外空间,只能在本串上操作。
从上面那道反转单词题目我们知道,全局翻转+局部反转可以达到反转整串单词的目的,怎么实现部分左旋呢?
局部反转+局部反转+全局反转=部分左旋
竟然写出了这样的代码:
class Solution { public: string reverseLeftWords(string s, int n) { reverse(s,0,n-1); reverse(s,0,s.size()-1); reverse(s,n,s.size()-1); return s; } };
当然是错掉了, 正确调研reverse接口之后
string reverseLeftWords(string s, int n) { reverse(s.begin(),s.begin()+n); reverse(s.begin()+n,s.end()); reverse(s.begin(),s.end()); return s; }
python可以使用切片和’+‘快速实现:
def reverseLeftWords(s: str, n: int) -> str: return s[n:] + s[0:n]
Knuth-Morris-Pratt (KMP)算法
字符串的前缀是指不包含最后一个字符的所有以第一个字符开头的连续子串;
后缀是指不包含第一个字符的所有以最后一个字符结尾的连续子串
最长相同前后缀长度是:前缀字符串 正好等于 后缀字符串 的长度 (eg aba 1; aabaa 2; a 0; abac 0; abcba 1;)
KMP算法就是在一个字符串中找是不是包含另一个串。
当出现字符串不匹配时,可以知道一部分之前已经匹配的文本内容,可以利用这些信息避免从头再去做匹配了。
记录已经匹配的文本内容?
next数组,即前缀表,前缀表就是用来当一个子串匹配失败后,选择最好的位置重新匹配用的。
例如:要在文本串:aabaabaafa 中查找是否出现过一个模式串:aabaaf。
当匹配到aabaab之后,发现不对劲,要换一个,那么从哪开始?a?第二个a? b?
当然是第三个‘a’! 如何match到这个位置,就交给前缀表next数组。
next数组里的数字表示的是什么,为什么这么表示?
前缀表中的元素即表示下标i之前(包括i)的字符串中,有多大长度的相同前缀后缀。
即在每个位置的最长相同前后缀长度。
这样之后,当遇到不匹配的地方时,理所当然要转跳到一个新的位置重新匹配 转跳到哪?
找到的不匹配的位置, 那么此时我们要看它的前一个字符的前缀表的数值是多少。
因为要找前面字符串的最长相同的前缀和后缀。所以要看前一位(这一位已经不匹配了日)的 前缀表的数值。
前一个字符的前缀表的数值是2, 所有把下标移动到(模式串)下标2的位置继续比配。 最后就在文本串中找到了和模式串匹配的子串了。
前缀表其实就是next数组,用来确定不匹配时回退的位置。
很多实现都是把前缀表统一减一(右移一位,初始位置为-1)之后作为next数组。
- next数组可以就是前缀表
- next数组也可以是前缀表统一减一(右移一位,初始位置为-1)
使用-1前缀表匹配
例如:要在文本串:aabaabaafa 中查找是否出现过一个模式串:aabaaf。
n为文本串长度,m为模式串长度, 匹配的过程是$O(n)$,之前要单独生成next数组,时间复杂度是$O(m)$。KMP算法时间复杂度$O(n+m)$。
暴力的解法显而易见是$O(n × m)$,可见KMP算法在字符串匹配中极大的提高的搜索的效率。
第一步:构造next数组。一般会用一个函数来构建next数组:
void getNext(int* next, const string& s)
前缀表即next,s为模式串。
void getNext(int* next, const string& s){ int j = -1; next[0] = j; for(int i = 1; i < s.size(); i++) { // 注意i从1开始 while (j >= 0 && s[i] != s[j + 1]) { // 前后缀不相同了 j = next[j]; // 向前回退 } if (s[i] == s[j + 1]) { // 找到相同的前后缀 j++; } next[i] = j; // 将j(前缀的长度)赋给next[i] } }
第二步:使用next数组来做匹配
定义两个下标j 指向模式串起始位置,i指向文本串起始位置。
int j = -1; // 因为next数组里记录的起始位置为-1 for (int i = 0; i < s.size(); i++) { // 注意i就从0开始,遍历文本串 while(j >= 0 && s[i] != t[j + 1]) { // 不匹配 j = next[j]; // j 寻找之前匹配的位置 } if (s[i] == t[j + 1]) { // 匹配,j和i同时向后移动 j++; // i的增加在for循环里 } if (j == (t.size() - 1) ) { // 文本串s里出现了模式串t return (i - t.size() + 1); } }
整体代码:
class Solution { public: void getNext(int* next, const string& s) { int j = -1;//j指向前缀 后面i指向后缀 next[0] = j;//对next赋初值 for (int i = 1; i < s.size(); i++) { // 注意i从1开始 while (j >= 0 && s[i] != s[j + 1]) { // 前后缀不相同了 j = next[j]; // 向前回退 } if (s[i] == s[j + 1]) { // 找到相同的前后缀 j++; } next[i] = j; // 将j(前缀的长度)赋给next[i] } } int strStr(string haystack, string needle) { if (needle.size() == 0) { return 0; } int size_needle = needle.size(); int * next = new int[size_needle]; getNext(next, needle); int j = -1; // // 因为next数组里记录的起始位置为-1 for (int i = 0; i < haystack.size(); i++) { // 注意i就从0开始 while (j >= 0 && haystack[i] != needle[j + 1]) { // 不匹配 j = next[j]; // j 寻找之前匹配的位置 } if (haystack[i] == needle[j + 1]) { // 匹配,j和i同时向后移动 j++; // i的增加在for循环里 } if (j == (needle.size() - 1)) { // 文本串s里出现了模式串t return (i - needle.size() + 1); } } delete [] next; return -1; } };
前缀表(不减一)C++实现
和减一前缀表实现来说,只是kmp算法实现问题,
class Solution { public: void getNext(int* next, const string& s) { int j = 0; next[0] = 0; for(int i = 1; i < s.size(); i++) { while (j > 0 && s[i] != s[j]) { j = next[j - 1]; } if (s[i] == s[j]) { j++; } next[i] = j; } } int strStr(string haystack, string needle) { if (needle.size() == 0) { return 0; } int size_needle = needle.size(); int * next = new int[size_needle]; getNext(next, needle); int j = 0; for (int i = 0; i < haystack.size(); i++) { while(j > 0 && haystack[i] != needle[j]) { j = next[j - 1]; } if (haystack[i] == needle[j]) { j++; } if (j == needle.size() ) { return (i - needle.size() + 1); } } delete [] next; return -1; } };
记不住就背下来吧
# 方法一 class KmpSolution1: def strStr(self, haystack: str, needle: str) -> int: a=len(needle) b=len(haystack) if a==0: return 0 next=self.getnext(a,needle) p=-1 for j in range(b): while p>=0 and needle[p+1]!=haystack[j]: p=next[p] if needle[p+1]==haystack[j]: p+=1 if p==a-1: return j-a+1 return -1 def getnext(self,a,needle): next=['' for i in range(a)] k=-1 next[0]=k for i in range(1,len(needle)): while (k>-1 and needle[k+1]!=needle[i]): k=next[k] if needle[k+1]==needle[i]: k+=1 next[i]=k return next task2 = KmpSolution1() print(task2.strStr("asd748zxc","8zx")) # 方法二 class KmpSolution2: def strStr(self, haystack: str, needle: str) -> int: a=len(needle) b=len(haystack) if a==0: return 0 i=j=0 next=self.getnext(a,needle) while(i<b and j<a): if j==-1 or needle[j]==haystack[i]: i+=1 j+=1 else: j=next[j] if j==a: return i-j else: return -1 def getnext(self,a,needle): next=['' for i in range(a)] j,k=0,-1 next[0]=k while(j<a-1): if k==-1 or needle[k]==needle[j]: k+=1 j+=1 next[j]=k else: k=next[k] return next task3 = KmpSolution2() print(task3.strStr("asd748zxc","8zx"))
参考github项目oiwiki解答:前缀函数与 KMP 算法 - OI Wiki (oi-wiki.org)
参考Carl的KMP精讲:代码随想录 (programmercarl.com)
strStr()找子字符串
在字符串中查找子串:
28. 实现 strStr() - 力扣(LeetCode) (leetcode-cn.com)
给定一个 haystack 字符串和一个 needle 字符串,在 haystack 字符串中找出 needle 字符串出现的第一个位置 (从0开始)。如果不存在,则返回 -1。
示例 1: 输入: haystack = "hello", needle = "ll" 输出: 2
示例 2: 输入: haystack = "aaaaa", needle = "bba" 输出: -1
说明: 当 needle 是空字符串时,我们应当返回什么值呢?应当返回 0 。这与C语言的 strstr() 以及 Java的 indexOf() 定义相符。
思路:按KMP算法,当匹配到不符合的子串时,需要确定下一个前缀开始的位置,怎么确定呢?构建next数组
class strStrSolution { public: void getNext(int* next, const string& s) { int j = -1; next[0] = j; for (int i = 1; i < s.size(); i++) { // 注意i从1开始 while (j >= 0 && s[i] != s[j + 1]) { // 前后缀不相同了 j = next[j]; // 向前回退 } if (s[i] == s[j + 1]) { // 找到相同的前后缀 j++; } next[i] = j; // 将j(前缀的长度)赋给next[i] } } int strStr(string haystack, string needle) { if (needle.size() == 0) { return 0; } int size_needle = needle.size(); int * next = new int[size_needle]; getNext(next, needle); int j = -1; // // 因为next数组里记录的起始位置为-1 for (int i = 0; i < haystack.size(); i++) { // 注意i就从0开始 while (j >= 0 && haystack[i] != needle[j + 1]) { // 不匹配 j = next[j]; // j 寻找之前匹配的位置 } if (haystack[i] == needle[j + 1]) { // 匹配,j和i同时向后移动 j++; // i的增加在for循环里 } if (j == (needle.size() - 1)) { // 文本串s里出现了模式串t return (i - needle.size() + 1); } } delete [] next; return -1; } };
重复的子字符串
459. 重复的子字符串 - 力扣(LeetCode) (leetcode-cn.com)
给定一个非空的字符串 s
,检查是否可以通过由它的一个子串重复多次构成。数组长度为:len。
- 首先对整个字符串生成-1前缀表
- 如果 next[len - 1] != -1,则说明字符串有最长相同的前后缀(就是字符串里的前缀子串和后缀子串相同的最长长度)。最长相等前后缀的长度为:next[len - 1] + 1。(这里的next数组是以减一的方式计算的,因此需要+1)
- len % (len - (next[len - 1] + 1)) == 0 ,则说明 (数组长度-最长相等前后缀的长度) 正好可以被 数组的长度整除,说明有该字符串有重复的子字符串
下面是两个不同串的-1前缀表:
class isnsubStrSolution { public: void getNext(int* next, const string& s) { next[0] = -1; int j = -1; for (int i = 1; i < s.size(); i++) { while (j >= 0 && s[i] != s[j + 1]) { j = next[j]; } if (s[i] == s[j + 1]) { j++; } next[i] = j; } } bool repeatedSubstringPattern(string s) { if (s.size() == 0) { return false; } int size_s = s.size(); int* next = new int[size_s]; getNext(next, s); int len = s.size(); if (next[len - 1] != -1 && len % (len - (next[len - 1] + 1)) == 0) {
rerurn true; return true; } delete[] next; return false; } };