字符串:简称为串,是由零个或多个字符组成的有限序列。一般记为
组成的字符串就是字符串的值,一般用双引号括起来;
可以是字母,数字或者其他字符。i
是该字符在字符串中的位置;
n
成为字符串的长度;''
表示;n-1
,长度为k的子串称为「后缀」;举个例子来说明一下:
str = "Hello World"
在示例代码中,str
是一个字符串的变量名称,hello world
则是该字符串的值,字符串的长度为11,该字符串的表示如下图所示:
在这里插入图片描述
根据字符串的特点,我们可以将字符串问题分为以下几种:
两个数字之间很容易比较大小,例如 1 < 2。而字符串之间的比较相对来说复杂一点。字符串之间的大小取决于它们按顺序排列字符的前后顺序。比如字符串 str1 = "abc"
和 str2 = "acc"
, 它们的第一个字母都是 a,而第二个字母,由于字母 b 比字母 c 要靠前,所以 b < c
,于是我们可以 说 "abc" < "acd"
,也可以说 str1 < str2
。字符串之间的比较是通过组成字符串的字符之间的「字符编码」来决定的。而字符编码指的是字符在对 应字符集中的序号。
而对于两个不相等的字符串,我们可以以下面的规则定义两个字符串的大小:
str1[i]
对应的字符编码等于 str2[i]
对应的字符编码,则比较下一位字符。str1[i]
对应的字符编码小于 str2[i]
对应的字符编码,则说明 str1 < str2
。比如:"abc" < "acc"
。str1[i]
对应的字符编码大于 str2[i] 对应的字符编码,则说明 str1 > str2
。比如:"bcd" > "bad"
。len(str1) < len(str2)
。则 str1 < str2
。比如:"abc" < "abcde"
len(str1) > len(str2)
。则 str1 > str2
。比如:"abcde" > "abc"
str1 == str2
, 比如:"abcd" == "abcd"
按照上面的规则,我们可以定义一个 strcmp
方法,并且规定:
python实现如下:
def strcmp(str1, str2):
index1, index2 = 0, 0
while index1 < len(str1) and index2 < len(str2):
if ord(str1[index1]) == ord(str2[index2]):
index1 += 1
index2 += 1
elif ord(str1[index1]) < ord(str2[index2]):
return -1
else:
return 1
if len(str1) < len(str2):
return -1
elif len(str1) > len(str2):
return 1
else:
return 0
以计算机中常用字符使用的 ASCII 编码为例。最早的时候,人们制定了一个包含 127 个字符的编码表 ASCII 到计算机系统中。ASCII 编码表中的字符包含了大小写的英文字母、数字和一些符号。每个字符 对应一个编码,比如大写字母 A 的编码是 65,小写字母 a 的编码是 97。后来又针对特殊字符,将 ASCII 扩展到了 256 位。ASCII 编码可以解决以英语为主的语言,可是无法满足中文编码。为了解决中文编码,我国制定了 GB2312、GBK、GB18030 等中文编码标准,将中文编译进去。但是世界上有上百种语言和文字,各 国有各国的标准,就会不可避免的产生冲突,于是就有了 Unicode 编码。Unicode 编码最常用的就 是 UTF-8 编码,UTF-8 编码把一个 Unicode 字符根据不同的数字大小编码成 1 ~ 6 个字节,常用的 英文字母被编码成 1 个字节,汉字通常是 3 个字节。
字符串的存储结构跟线性表相同,分为 「顺序存储结构」和「链式存储结构」
字符串匹配 :又称 「模式匹配」。可以简单理解为,给定字符串 T
和 p
,在主串 T
中寻找子串 p
。主 串 T
又被称为 「文本串」 ,子串 p
又被称为 「模式串」 。在字符串问题中,最重要的问题之一就是字符串匹配问题。而按照模式串的个数,可以将字符串匹 配问题分为:「单模式串匹配问题」和「多模式串匹配问题
单模式匹配问题:给定一个文本串T = t_1t_2 ...t_n ,再给定一组特定模式串P = {P_1P_2…P_n}。要求从文本 串T
找出特定模式串p
的所有出现位置。有很多算法可以解决单模式匹配问题。而根据在文本中搜索模式串方式的不同,可以将单模式匹配 算法分为以下三种:
多模式串匹配算法大多使用了一种基本的数据结构:「字典树(Trie)」。著名的 「AC 自动机算法」 就是在 KMP 算法 的基础上,与「字典树」结构相结合而诞生的。而「AC 自动机算法」也是多模式串 匹配算法中最有效的算法之一。所以学习多模式匹配算法,重点是要掌握 「字典树」 和 「AC 自动机算法」。
Brute Force算法:中文意思是暴力匹配算法,也可以叫做朴素匹配算法。BF算法思想:对于给定文本串T与模式串p
,从文本串的第一个字符开始与模式串p
的第一个字符进 行比较,如果相等,则继续逐个比较后续字符,否则从文本串T
的第二个字符起重新和模式串p
进行 比较。依次类推,直到模式串p
中每个字符依次与文本串T的一个连续子串相等,则模式匹配成功。否则模式匹配失败。
BF算法步骤
1 .对于给定的文本串T
与模式串p
,求出文本串T
的长度为n
,模式串p
的长度为m
2 .同时遍历文本串T
和模式串p
,先将T[0]
与p[0]
进行比较
3 .如果相等,则继续比较T[1]
和p[1]
。以此类推,一直到模式串p
的末尾p[m - 1]
为止
4 .如果不相等,则将文本串T
移动到上次匹配开始位置的下一个字符位置,模式串p
则回退到开始位置,再依次进行比较。
5 .当遍历完文本串T或者模式串p
的时候停止搜索。
python实现
def bruthForce(T:str, p:str) -> str:
n, m = len(T), len(p)
i, j = 0, 0
while i < n and j < m:
if p[j] == T[i]:
i += 1
j += 1
else:
i = i-(j-1) # 匹配失败,i移动到上次匹配开始位置的下一个位置
j = 0
if j == m: # 匹配成功,返回匹配的开始位置
return i-j
return -1 # 匹配失败,返回-1
单模式串朴素匹配算法分析
p
直接回到开始位置,文本串也回到匹配开始位置的下一个位置,再重新开 始比较。在
KMP算法:全称叫做[ Knuth Morris Pratt算法」,是由它的三位发明者Donald Knuth、James H. Morris. Vaughan Pratt的名字来命名的。KMP算法是他们三人在1977年联合发表的。KMP算法思想:对于给定文本串T
与模式串p
,当发现文本串T的某个字符与模式串p不匹配的时候,可以利用匹配失败后的信息,尽量减少模式串与文本串的匹配次数,避免文本串位置的回退,以达到快速匹配的目的。
KMP算法的改进 如果我们可以通过每一次的失配而得到一些 「信息」,并且这些 「信息」 可以帮助我们跳过那些不可能匹配成功的位置,那么我们就能大大减少模式串与文本串的匹配次数,从而达到快速匹配的目的。每一次失配所告诉我们的信息是:主串的某一个子串等于模式串的某一个前缀。 这个信息的意思是:
如果文本串T[i: i + m]与模式串p
的失配是下标位置j
上发生的,那么文本串T
从下标位置i
开始连续的j
个字符,一定与模式串p
的前j
个字符一模一样,即
在这里插入图片描述
KMP算法的改进
KMP算法就是使用了这样的思路,对模式串p
进行了预处理,计算出一个 「部分匹配表」,用一个数组next来记录。然后在每次失配发生时,不回退文本串的指针i
,而是根据 「部分匹配表」 中模式串失配位置j
的前一个位置的值,即next[j -1]
的值来决定模式串可以向右移动的位数。比如上述示例中模式串p
是在j = 5的位置上发生失配的,则说明文本串的子串T[i: i + 5]
和模式串p[0: 5]
的字符是一致的,即"ABCAB" == "ABCAB"
而根据 「部分匹配表」 中next[4] = = 2 ,所以不用回退i
,而是将j
移动到下标为2的位置,让T[i + 5]
直接对准p[2]
,然后继续进行比对。
如果文本串T[i:i+m]
与模式串p
的失配是在第j
个下标位置发生的,那么:
T
从下标位置i开始连续的j
个字符,一定与模式串p
的前j
个字符一模一样,即:$T[i: i +j]==p[0: j]可以推出:文本串子串的后k
位后缓和模式串子串的前k
位是相同的,即
,不需要再比较了,可以直接跳过。
那么我们就可以将文本串中的T[i + m]
对准模式串中的p[k]
,继续进行对比。这里的k
其实就是next[j-1]
next数组的构造我们可以通过递推的方式构造next数组。
p
拆分成left、right两部分。left表示前缀串开始所在的下标位置,right表示后 缀串开始所在的下标位置,起始时p[left]
和p[right]
来进行判断。 ,说明当前的前后缀不相同。则让后缀开始位置k
不动,前缀串开始位置 left 不断回退到
位置,直到
为止。
,说明当前的前后缀相同,则可以先让
,此时left既是前缀 下一次进行上匕较的下标位置,又是当前最长前后缀的长度。
p
中,最长相等前后缀的长度为left ,即。
python实现
# 生成next数组
# next[j]表示下标j之前的模式串p中,最长相等前后缀的长度
def generateNext(p: str):
m = len(p)
next = [0 for _ in range(m)] # 初始化数组元素全部为0
left = 0 # left表示前缀串开始所在的下标位置
for right in range(1, m): # right表示后缀串开始所在的下标位置
while left > 0 and p[left] != p[right]: # 匹配不成功,left进行回退,left==0时停止回退
left = next[left-1] # left进行回退操作
if p[left] == p[right]: # 匹配成功,找到相同的前后缀,先让left+=1, 此时left为前缀长度
left += 1
next[right] = left # 记录前缀长度,更新next[right], 结束本次循环,right+=1
return next
# KMP 匹配算法,T为文本串,p为模式串
def kmp(T:str, p:str) -> int:
n = len(T)
m = len(p)
next = generateNext(p) # 生成next数组
j = 0 # j为模式串中当前匹配的位置
for i in range(n): # i 为文本串中当前匹配的位置
while j > 0 and T[i] != p[j]: # 如果模式串前缀匹配不成功,将模式串进行回退,j==0时候停止回退
j = next[j-1]
if T[i] == p[j]: # 当前模式串前缀匹配成功,令j+=1,继续匹配
j += 1
if j == m: # 当前模式串完全匹配成功,返回匹配开始位置
return i-j+1
return -1 # 匹配失败,返回-1
KMP匹配算法分析
,其中m
是模式串p
的长度
i
并没有进行回退,可以看出匹配阶段的时间复杂度是,其中n
是文本串T
的长度
,相对于朴素匹配算法
的时间复杂度,KMP算法的效率有了很大的提升
字符串题目一般考虑使用滑动窗,双指针,kmp算法。
题目大意:描述:给定一个字符串 s。要求:验证它是否是回文串,如果是回文串,则返回 True,否则返回 False。只考虑字母和数字字符, 可以忽略字母的大小写。
示例 :
输入: "A man, a plan, a canal: Panama"
输出: true
解释:"amanaplanacanalpanama" 是回文串
输入: "race a car"
输出: false
解释:"raceacar" 不是回文串
解题思路:使用对撞指针求解。具体步骤如下:
left
, right
, left
指向字符串开始位置,right
指向字符串结束位置left
右移、right
左移的方式过滤掉字母和数字以外的字符s[left]
是否和s[right]
相等(注意大小写)left
右移,right
左移,继续进行下一次过滤和判断,跳出循环,则说明该字符串是回文串,返回True。
python实现
class Solution:
def isPalindrome(self, s: str) -> bool:
if not s or len(s) < 1:
return False
length = len(s)
if length == 1:
return True
left = 0
right = length-1
while left < right:
if not s[left].isalnum():
left += 1
continue
if not s[right].isalnum():
right -= 1
continue
if s[left].lower() == s[right].lower():
left += 1
right -= 1
else:
return False
return True
C++实现
class Solution {
public:
bool isPalindrome(string s) {
string sgood;
for(char ch:s)
{
if(isalnum(ch))
{
sgood += tolower(ch);
}
}
int n = sgood.size();
int left = 0;
int right = n-1;
while(left < right)
{
if (sgood[left] != sgood[right])
{
return false;
}
++left;
--right;
}
return true;
}
};
题目大意:描述:给定一个字符串 s。要求:逐个翻转字符串中所有的单词。注意:
示例 :
输入:s = "the sky is blue"
输出:"blue is sky the"
输入:s = " hello world "
输出:"world hello"
解释:输入字符串可以在前面或者后面包含多余的空格,但是翻转后的字符不能包括。
输入:s = "a good example"
输出:"example good a"
解释:如果两个单词间有多余的空格,将翻转后单词间的空格减少到只含一个。
输入:s = " Bob Loves Alice "
输出:"Alice Loves Bob"
输入:s = "Alice does not even like bob"
输出:"bob like even not does Alice"
解题思路:第一种思路比较简单,就是直接调用一些函数的库函数,对字符串进行切片,翻转,然后拼合成字符串 第二种思路根据API的思路写出模拟代码,具体步骤如下:
words
进行翻转操作,令words[i]
, words[len(words)- 1-i]
交换元素。words
中的单词连接起来,中间拼接上空格,将其作为答案返回。fig
python实现
class Solution:
def reverseWords(self, s: str) -> str:
ss = s.split()
return ' '.join(ss[::-1])
或者
class Solution:
def reverseWords(self, s: str) -> str:
words = []
cur == ''
for ch in s:
if ch == ' ':
if cur:
words.append(cur)
cur = ''
else:
cur += ch
if cur:
words.append(cur)
return ' '.join(words[::-1])
自己实现
fig
class Solution:
def trim_spaces(self, s: str) -> list:
left, right = 0, len(s) - 1
# 去掉字符串开头的空白字符
while left <= right and s[left] == ' ':
left += 1
# 去掉字符串末尾的空白字符
while left <= right and s[right] == ' ':
right -= 1
# 将字符串间多余的空白字符去除
output = []
while left <= right:
if s[left] != ' ':
output.append(s[left])
elif output[-1] != ' ':
output.append(s[left])
left += 1
return output
def reverse(self, l: list, left: int, right: int) -> None:
while left < right:
l[left], l[right] = l[right], l[left]
left, right = left + 1, right - 1
def reverse_each_word(self, l: list) -> None:
n = len(l)
start = end = 0
while start < n:
# 循环至单词的末尾
while end < n and l[end] != ' ':
end += 1
# 翻转单词
self.reverse(l, start, end - 1)
# 更新start,去找下一个单词
start = end + 1
end += 1
def reverseWords(self, s: str) -> str:
l = self.trim_spaces(s)
# 翻转字符串
self.reverse(l, 0, len(l) - 1)
# 翻转每个单词
self.reverse_each_word(l)
return ''.join(l)
C++实现
class Solution {
public:
string reverseWords(string s) {
// 反转整个字符串
reverse(s.begin(), s.end());
int n = s.size();
int idx = 0;
for (int start = 0; start < n; ++start) {
if (s[start] != ' ') {
// 填一个空白字符然后将idx移动到下一个单词的开头位置
if (idx != 0) s[idx++] = ' ';
// 循环遍历至单词的末尾
int end = start;
while (end < n && s[end] != ' ') s[idx++] = s[end++];
// 反转整个单词
reverse(s.begin() + idx - (end - start), s.begin() + idx);
// 更新start,去找下一个单词
start = end;
}
}
s.erase(s.begin() + idx, s.end());
return s;
}
};
给定一个非空字符串 s
,最多删除一个字符。判断是否能成为回文字符串。
示例 :
输入: s = "aba"
输出: true
输入: s = "abca"
输出: true
解释: 你可以删除c字符。
输入: s = "abc"
输出: false
解题思路
首先考虑如果不允许删除字符,如何判断一个字符串是否是回文串。常见的做法是使用双指针。定义左右指针,初始时分别指向字符串的第一个字符和最后一个字符,每次判断左右指针指向的字符是否相同,如果不相同,则不是回文串;如果相同,则将左右指针都往中间移动一位,直到左右指针相遇,则字符串是回文串。
在允许最多删除一个字符的情况下,同样可以使用双指针,通过贪心实现。初始化两个指针 low
和 high
分别指向字符串的第一个字符和最后一个字符。每次判断两个指针指向的字符是否相同,如果相同,则更新指针,将 low 加 1
,high 减 1
,然后判断更新后的指针范围内的子串是否是回文字符串。如果两个指针指向的字符不同,则两个字符中必须有一个被删除,此时就分成两种情况:即删除左指针对应的字符,留下子串
,或者删除右指针对应的字符,留下子串
。当这两个子串中至少有一个是回文串时,就说明原始字符串删除一个字符之后就以成为回文串。
fig1
class Solution:
def validPalindrome(self, s: str) -> bool:
length = len(s)
if not str or length < 1:
return False
if length == 1:
return True
left = 0
right = length-1
while left < right:
if s[left] == s[right]:
left += 1
right -= 1
else:
return self.isPalindrome(s, left+1, right) or self.isPalindrome(s, left, right-1) # 递归
return True
def isPalindrome(self, s:str, left, right) -> bool:
while left < right:
if s[left] == s[right]:
left += 1
right -= 1
else:
return False
return True
class Solution {
public:
bool validPalindrome(string s) {
int low = 0, high = s.length()-1;
while(low<high)
{
char c1 = s[low];
char c2 = s[high];
if(c1==c2)
{
++low;
--high;
}
else
{
return validPalindrome(s, low, high-1) || validPalindrome(s, low+1, high);
}
}
return true;
}
bool validPalindrome(string s, int low, int high)
{
for(int i=low, j=high; i<j; ++i, --j)
{
char c1 = s[i];
char c2 = s[j];
if(c1 != c2)
{
return false;
}
}
return true;
}
};
给你一个整数 columnNumber
,返回它在 Excel 表中相对应的列名称。
例如:
A -> 1
B -> 2
C -> 3
...
Z -> 26
AA -> 27
AB -> 28
...
示例 :
输入:columnNumber = 1
输出:"A"
输入:columnNumber = 28
输出:"AB"
输入:columnNumber = 701
输出:"ZY"
输入:columnNumber = 2147483647
输出:"FXSHRXW"
解题思路:
要将数字转化为字母,就需要就将数据对26取模,再转化为对应字母的ascii码。
但是这里需要注意的是,1-26
对26取余后的结果是1-25与0
,此时转化为A-Z
时则会出现循环错位,要么分类讨论,要么就寻找更好的方法。
而当对数据一律减1
后取余,使得1-26减1
后取余后的结果0-25
再+'A',就可以与A-Z
对应。
class Solution:
def convertToTitle(self, columnNumber: int) -> str:
ans = list()
while columnNumber > 0:
columnNumber -= 1
ans.append(chr(columnNumber%26 + ord("A")))
columnNumber //= 26
return ''.join(ans[::-1])
class Solution {
public:
string convertToTitle(int columnNumber) {
string res;
while(columnNumber != 0)
{
columnNumber--;
res += (columnNumber % 26) + 'A';
columnNumber /= 26;
}
reverse(res.begin(), res.end());
return res;
}
};
给定一个经过编码的字符串,返回它解码后的字符串。
编码规则为: k[encoded_string]
,表示其中方括号内部的 encoded_string
正好重复 k
次。注意 k
保证为正整数。
你可以认为输入字符串总是有效的;输入字符串中没有额外的空格,且输入的方括号总是符合格式要求的。
此外,你可以认为原始数据不包含数字,所有的数字只表示重复的次数 k
,例如不会出现像 3a
或 2[4]
的输入。
示例 :
输入:s = "3[a]2[bc]"
输出:"aaabcbc"
输入:s = "3[a2[c]]"
输出:"accaccacc"
输入:s = "2[abc]3[cd]ef"
输出:"abcabccdcdcdef"
输入:s = "abc3[cd]xyz"
输出:"abccdcdcdxyz"
1 <= s.length <= 30
``s由小写英文字母、数字和方括号
'[]'` 组成s
保证是一个 有效 的输入。s
中所有整数的取值范围为[1, 300]
解题思路:
python实现
class Solution:
def decodeString(self, s: str) -> str:
stack = []
for ch in s:
if ch != ']':
stack.append(ch)
else:
tmp = []
num = ''
# 先找要重复的子字符串
while stack[-1] != '[' and not stack[-1].isdigit():
tmp.append(stack.pop())
tmp = ''.join(tmp[::-1]) # 反序拼接
# 移除'['
stack.pop()
# 再找重复的次数(数字)
while stack and stack[-1].isdigit():
num += stack.pop()
num = num[::-1]
num = int(num) if num else num
# 重复子字符串,并加入到stack中
# print(tmp, num)
if num:
stack.append(tmp*num)
# print(ch, stack)
return ''.join(stack)
C++实现
class Solution {
public:
/*
* 1、栈题,最好使用栈,那么必有入栈出栈操作
* 2、基本元素:必定有判断、必定有循环
* 3、字符题,判断必带字符判断
* 4、必定有简单的判断思路,再加上巧妙的过程(这里将完整的字符串描述成一串字符串,部分的字符串描述成一块字符串)
*/
std::string decodeString(std::string s) {
std::stack<std::pair<int, std::string>> st; // 使用pair存储一块数字和一块字符串
int count = 0;
std::string onepiece = "";
for(auto& c : s) // 重要思路:每一个pair都入栈一对 ( 数字 + 一块字符串 )
{
if(c >= '0' && c <= '9')
count = count * 10 + (int)(c-'0'); //[1]遇到数字,数字计算公式
else if(c == '[')
{
st.emplace(std::pair<int, std::string>(count, onepiece)); //[2]遇到 [ , pair先入栈数字+空的一块字符串, 或者入栈数字+数字前的只有1次的一块字符串
onepiece = ""; // 置空
count = 0; // 置空
}
else if(c == ']') //[4]遇到 ] , pair的second进行字符串赋值
{
std::string tmp = onepiece;
std::string tmp1 = "";
for(int i = 0; i < st.top().first; ++i) // 循环拼接有k次的一块字符串,st.top().first为数字
tmp1 += tmp;
st.top().second = st.top().second + tmp1; //将一块字符串赋值给second,存储的second带有只有1次的一块字符串,所以拼接时要带上second来进行拼接,故有second+tmp1
onepiece = st.top().second; // onepiece继续拼接,不断的扩展,由一块字符串不断拼接成一串字符串
st.pop(); // 弹出,因为 ] 的位置
}
else
onepiece += c; //[3]遇到字符就拼接组成一块字符串
}
return onepiece;
}
};