字符串
一、引言
字符串可以看成是字符组成的数组。由于字符串是程序里经常需要处理的数据类型,因此有 很多针对字符串处理的题目,以下是一些常见的类型。
二、经典问题
1. 字符串比较
给定两个字符串 s 和 t ,编写一个函数来判断 t 是否是 s 的字母异位词。
注意:若 s 和 t 中每个字符出现的次数都相同,则称 s 和 t 互为字母异位词。
我们可以利用哈希表或者数组统计两个数组中每个数字出现的频次,若频次相同,则说明它们包含的字符完全相同。
class Solution {
public:
bool isAnagram(string s, string t) {
if(s.length() != t.length()){
return false;
}
vector<int> count(26, 0);
for(int i=0; i<s.length(); ++i){
++count[s[i] - 'a'];
--count[t[i] - 'a'];
}
for(int i=0; i<26; ++i){
if(count[i]){
return false;
}
}
return true;
}
};
给定两个字符串 s 和 t ,判断它们是否是同构的。
如果 s 中的字符可以按某种映射关系替换得到 t ,那么这两个字符串是同构的。
每个出现的字符都应当映射到另一个字符,同时不改变字符的顺序。不同字符不能映射到同一个字符上,相同字符只能映射到同一个字符上,字符可以映射到自己本身。
我们可以将问题转化一下:记录两个字符串每个位置的字符第一次出现的位置,如果两个字 符串中相同位置的字符与它们第一次出现的位置一样,那么这两个字符串同构。举例来说,对于 “paper”和“title”,假设我们现在遍历到第三个字符“p”和“t”,发现它们第一次出现的位置都 在第一个字符,则说明目前位置满足同构。
class Solution {
public:
bool isIsomorphic(string s, string t) {
vector<int> s_first_index(256, 0), t_first_index(256, 0);
for(int i=0; i<s.length(); ++i){
if(s_first_index[s[i]] != t_first_index[t[i]]){
return false;
}
s_first_index[s[i]] = t_first_index[t[i]] = i + 1;
}
return true;
}
};
给你一个字符串 s ,请你统计并返回这个字符串中 回文子串 的数目。
回文字符串 是正着读和倒过来读一样的字符串。
子字符串 是字符串中的由连续字符组成的一个序列。
具有不同开始位置或结束位置的子串,即使是由相同的字符组成,也会被视作不同的子串。
我们可以从字符串的每个位置开始,向左向右延长,判断存在多少以当前位置为中轴的回文子字符串。
class Solution {
public:
int countSubstrings(string s) {
int count = 0;
for(int i=0; i<s.length(); ++i){
count += extendSubstrings(s, i, i); // 奇数长度
count += extendSubstrings(s, i, i + 1); // 偶数长度
}
return count;
}
int extendSubstrings(string s, int l, int r){
int count = 0;
while(l >= 0 && r < s.length() && s[l] == s[r]){
--l;
++r;
++count;
}
return count;
}
};
给定一个字符串 s,统计并返回具有相同数量 0 和 1 的非空(连续)子字符串的数量,并且这些子字符串中的所有 0 和所有 1 都是成组连续的。
重复出现(不同位置)的子串也要统计它们出现的次数。
从左往右遍历数组,记录和当前位置数字相同且连续的长度,以及其之前连续的不同数字的 长度。举例来说,对于 00110 的最后一位,我们记录的相同数字长度是 1,因为只有一个连续 0; 我们记录的不同数字长度是 2,因为在 0 之前有两个连续的 1。若不同数字的连续长度大于等于 当前数字的连续长度,则说明存在一个且只存在一个以当前数字结尾的满足条件的子字符串。
class Solution {
public:
int countBinarySubstrings(string s) {
int pre = 0, cur = 1, count = 0;
for(int i=1; i<s.length(); ++i){
if(s[i] == s[i - 1]){
++cur;
}else{
pre = cur;
cur = 1;
}
if(pre >= cur){
++count;
}
}
return count;
}
};
2. 字符串理解
给你一个字符串表达式 s ,请你实现一个基本计算器来计算并返回它的值。
整数除法仅保留整数部分。
你可以假设给定的表达式总是有效的。所有中间结果将在 [-231, 231 - 1] 的范围内。
注意:不允许使用任何将字符串作为数学表达式计算的内置函数,比如 eval() 。
如果我们在字符串左边加上一个加号,可以证明其并不改变运算结果,且字符串可以分割成 多个 < 一个运算符,一个数字 > 对子的形式;这样一来我们就可以从左往右处理了。由于乘除的 优先级高于加减,因此我们需要使用一个中间变量来存储高优先度的运算结果。 此类型题也考察很多细节处理,如无运算符的情况,和多个空格的情况等等。
class Solution {
public:
// 主函数
int calculate(string s) {
int i = 0;
return parseExpr(s, i);
}
// 辅函数 - 递归parse从位置i开始的剩余字符串
int parseExpr(const string& s, int& i){
char op = '+'; //初 始化op为'+' 即完成<一个运算符,一个数字>对子的形式
long left = 0, right = 0;
while(i < s.length()){
if(s[i] != ' '){
long n = parseNum(s, i);
switch(op){ // 将数字分成两部分 方便处理优先级
case '+': left += right; right = n; break;
case '-': left += right; right = -n; break;
case '*': right *= n; break;
case '/': right /= n; break;
}
if(i < s.length()){
op = s[i];
}
}
++i;
}
return left + right;
}
// 辅函数 - parse从位置i开始的一个数字
long parseNum(const string& s, int& i){
long n = 0;
while(i < s.length() && isdigit(s[i])){
n = n * 10 + (s[i++] - '0');
}
return n;
}
};
由于乘除优先于加减计算,因此不妨考虑先进行所有乘除运算,并将这些乘除运算后的整数值放回原表达式的相应位置,则随后整个表达式的值,就等于一系列整数加减后的值。
基于此,我们可以用一个栈,保存这些(进行乘除运算后的)整数的值。对于加减号后的数字,将其直接压入栈中;对于乘除号后的数字,可以直接与栈顶元素计算,并替换栈顶元素为计算后的结果。
具体来说,遍历字符串 s,并用变量 preSign 记录每个数字之前的运算符,对于第一个数字,其之前的运算符视为加号。每次遍历到数字末尾时,根据 preSign 来决定计算方式:
加号:将数字压入栈;
减号:将数字的相反数压入栈;
乘除号:计算数字与栈顶元素,并将栈顶元素替换为计算结果。
代码实现中,若读到一个运算符,或者遍历到字符串末尾,即认为是遍历到了数字末尾。处理完该数字后,更新 preSign 为当前遍历的字符。遍历完字符串 s 后,将栈中元素累加,即为该字符串表达式的值。
class Solution {
public:
int calculate(string s) {
vector<int> stk;
char preSign = '+';
int num = 0;
for(int i=0; i<s.length(); ++i){
if(isdigit(s[i])){
num = num * 10 + (s[i] - '0');
}
if(!isdigit(s[i]) && s[i] != ' ' || i == s.length() - 1){
switch(preSign){
case '+': stk.push_back(num); break;
case '-': stk.push_back(-num); break;
case '*': stk.back() *= num; break;
case '/': stk.back() /= num; break;
}
preSign = s[i];
num = 0;
}
}
return accumulate(stk.begin(), stk.end(), 0);
}
};
3. 字符串匹配
28. Find the Index of the First Occurrence in a String
给你两个字符串 haystack 和 needle ,请你在 haystack 字符串中找出 needle 字符串的第一个匹配项的下标(下标从 0 开始)。如果 needle 不是 haystack 的一部分,则返回 -1 。
使用著名的 Knuth-Morris-Pratt(KMP) 算法,可以在 O(m + n) 时间利用动态规划完成匹配。
class Solution {
public:
// 主函数
int strStr(string haystack, string needle) {
int n = haystack.length(), p = needle.length();
if(p == 0) return 0;
vector<int> next(p, -1); // -1表示不存在相同的最大前缀和后缀
calNext(needle, next); // 计算next数组
int k = -1;
for(int i=0; i<n; ++i){
while(k > -1 && needle[k + 1] != haystack[i]){ // 不匹配,且k>-1,表示有部分匹配,往前回溯
k = next[k];
}
if(needle[k + 1] == haystack[i]){
++k;
}
if(k == p - 1){ // 说明k移动到needle的最末端,返回相应的位置
//cout << "在位置" << i - p + 1 << endl;
//k = -1; // 重新初始化,寻找下一个
//i = i - p + 1; // i定位到该位置,外层for循环++i可以继续找下一个(这里默认存在两个匹配字符串可以部分重叠)
return i - p + 1;
}
}
return -1;
}
// 辅函数 - 计算next数组
void calNext(const string &needle, vector<int> &next){
next[0] = -1;
int p = -1;
for(int j = 1; j < needle.length(); ++j){
while(p > -1 && needle[p + 1] != needle[j]){ // 如果下一位不同,往前回溯
p = next[p];
}
if(needle[p + 1] == needle[j]){ // 如果下一位相同,更新相同的最大前缀和最大后缀长
++p;
}
next[j] = p;
}
}
};
三、巩固练习
3. Longest Substring Without Repeating Characters
5. Longest Palindromic Substring
欢迎大家共同学习和纠正指教