训练题:正则表达式匹配
题目:https://leetcode-cn.com/problems/zheng-ze-biao-da-shi-pi-pei-lcof/
方法一:递归寻路
正则表达式有点像寻路,当前位置有多种匹配可能,相当于有多条路,如果一条路行不通还得回来走另一条路。
这里的多条路的情况就是.或者a的匹配情况,其他情况都是只有一条路也就是要么能匹配,要么就是不能匹配。这里我们就详细讨论有*号的情况。
// -: 用来表示任意字符或者为空
// 源字符 正则字符
// --a-- --a*--
// --a-- --.*--
// 这两种情况本质就是一种情况,可以统一看待。
// 有两条路:先走一条,走不通,返回来走另一条。
// 第一条: a*选择匹配a,则继续遍历源字符串下一个字符,正则串则不移动a*继续匹配下一个字符。
// 第二条: a*选择不匹配a,则源字符串不移动,正则串移动到下下个字符(跳过*号)
bool isMatch(string s, string p) {
return match(s, p, s.size(), p.size(), 0, 0);
}
bool match(const string&s, const string&p, const int& size_s, const int& size_p, int idx_s, int idx_p){
// 完整匹配的情况就是双双都遍历到结尾。
if(idx_s == size_s && idx_p >= size_p) return true; // 源字符串和正则串都到结尾,完全匹配。
// 正则串当前遍历位置的下一位是不是*号。
bool nextIsStar = idx_p+1 < size_p && p[idx_p+1] == '*';
// 源字符串与正则串当前位置是否匹配。
bool matched = idx_s < size_s && idx_p < size_p && (s[idx_s] == p[idx_p] || p[idx_p] == '.');
// 正则串的下一位是*
if(nextIsStar) {
//
return
// 选择第一条路
(matched && match(s, p, size_s, size_p, idx_s+1,idx_p)) ||
// 选择第二条路
match(s, p, size_s, size_p, idx_s,idx_p+2);
}
// 就是肯定不是*的情况,也就是必须精确匹配。
return matched && match(s, p, size_s, size_p, ++idx_s, ++idx_p);
}
优化
bool isMatch(string s, string p) {
return match(s, p, s.size(), p.size(), 0, 0);
}
bool match(const string&s, const string&p, const int& size_s, const int& size_p, int idx_s, int idx_p){
if(idx_s == size_s && idx_p >= size_p) return true; // 源字符串和正则串都到结尾,完全匹配。
// 正则串当前遍历位置的下一位是不是*号。
bool nextIsStar = idx_p+1 < size_p && p[idx_p+1] == '*';
// 源字符串与正则串当前位置是否匹配。
bool matched = idx_s < size_s && idx_p < size_p && (s[idx_s] == p[idx_p] || p[idx_p] == '.');
// 正则串的下一位是*
if(nextIsStar) {
return (matched && match(s, p, size_s, size_p, idx_s+1,idx_p)) || match(s, p, size_s, size_p, idx_s,idx_p+2);
}
return matched && match(s, p, size_s, size_p, ++idx_s, ++idx_p);
}
训练题:字符串排列
排列组合,首先想到回溯算法。
// 回溯算法结构
void backtrack( ... )
{
if("终止条件") {
...; // 可选逻辑
return;
}
for(i = start; i != end; ++i){
......; // 可选逻辑
......; // 做出选择,设置新参数
backtrack( "新参数" );
......; // 可选逻辑
......; // 撤销选择,参数恢复。
}
}
vector<string> permutation(string s) {
if(s.size() > 8 || s.empty()) return {};
vector<string> ret;
const char size = static_cast<char>(s.size());
backtrack(s, ret, size, 0);
return ret;
}
//
// ["abc"]的全排列 = a为首的全排列 + b为首的全排列 + c为首的全排列
//
// 递归子过程:a为首的全排列 = a为首 + [b、c为次首]的全排列
//
// ["abb"]的全排列 = a为首的全排列 + b为首的全排列 + b为首的全排列(与前一个重复,去除)
//
// backtrack为求[start, ..., end]的全排列
// [start, ..., end]的全排列 = [start, ..., end]中每个字符为首的全排列之和。
// 注意[start, ..., end]中字符可能重复,重复的只考虑其中一个。
void backtrack(string& s, vector<string>& container, const char& size, char start)
{
// 最后一位,只有一个排列,可确定。
if(start == size - 1) {
ret.push_back(s);
return;
};
// 记录首字符的情况,避免重复首字符。这里的首位就是start位置
set<int> fuck;
for(char i = start; i < size; ++i) {
if(fuck.count(s[i])) continue; // 有重复首字符,直接跳过
// 求以s[i]为首字符的全排列
fuck.insert(s[i]);
std::swap(s[start], s[i]); // 将s[i]换到首位
backtrack(s, container, size, start + 1); // 再递归次首,即start+1
std::swap(s[start], s[i]); // 统计完之后记得恢复位置
}
}
训练题:字符串转数字
int strToInt(string str) {
int ret = 0;
do
{
const auto size = str.size();
int start = 0; // 遍历索引
char positive = 1; // 正负
char tmpNum = 0;
// 过滤头部空字符
for(; start < size && str[start] == ' '; ++start);
if(start == size) break;
// 过滤头部的正负号
if(str[start] == '-'){
positive = -1;
++start;
}
else if(str[start] == '+') ++start;
// 转换有效部分
for(; start < size && str[start] <= '9' && str[start] >= '0'; ++start) {
// 转化成数字
tmpNum = str[start] - '0';
if(ret > 214748364) return positive > 0 ? 2147483647 : -2147483648;
// 溢出判断
else if (ret == 214748364){
if(tmpNum >= 7 && positive > 0) return 2147483647;
else if(tmpNum >= 8 && positive < 0) return -2147483648;
}
ret = ret * 10 + tmpNum;
}
ret *= positive;
} while(false);
return ret;
}
训练题:左旋转字符串
- 时间复杂度:O(n)
- 空间复杂度:O(1)
string reverseLeftWords(string s, int k) {
if(s.size() <= k || s.empty() || k == 0) return s;
reverse(s.begin(), s.begin() + k);
reverse(s.begin() + k, s.end());
reverse(s.begin(), s.end());
return s;
}
训练题:第一个只出现一次字符
方法一:哈希表
char firstUniqChar(string s) {
if(s.empty()) return ' ';
unordered_map<char, bool> dic;
for(char c : s) dic[c] = dic.find(c) == dic.end();
for(char c : s) if(dic[c]) return c;
return ' ';
}
方法二:哈希表+队列
char firstUniqChar(string s) {
if(s.empty()) return ' ';
unordered_map<char, bool> dic; // char:字符,bool:字符是否重复
queue<char> q; // 队列表头一定是不重复的字符。
for(auto &c : s){
if(!dic.count(c)){
dic[c] = false;
q.push(c);
}
else{
dic[c] = true;
// 每当遍历到重复字符,就刷新一次队列,确保队列头一直是不重复的字符。
for(; !q.empty() && dic[q.front()]; q.pop());
}
}
return q.empty() ? ' ' : q.front();
}
训练题:判断数值
方法一
- 1、先按e/E分解字符串
- 2、对e的前面按.号分解
- 1、对.号的前面判断是否是整数
- 2、对.号的后面判断是否是整数(不能有+/-号的)
- 3、对e的后面判断是否是整数 ```cpp
bool isNumber( string s ) { if( s.empty() ) { return false; }
const auto size = s.size();
char startIdx = 0; // 字符串的有效区域起始和结尾。
char endIdx = s.size() - 1; // 有效区域即指:头部去除空格和第一个+/-号,以及尾部去除空格。
// 缩减掉两头的空格和开头的第一个+/-号(这么做是考虑+.8的奇葩情况)
while( startIdx < size && s[startIdx] == ' ' ) { ++startIdx; }
if( s[startIdx] == '+' || s[startIdx] == '-' ) { ++startIdx; }
if( startIdx == size ) { return false; } // 没有有效区域
while( endIdx >= 0 && s[endIdx] == ' ' ) { --endIdx; }
// 找出第一个e的位置。
char idx_e = startIdx;
for( ; idx_e <= endIdx && s[idx_e] != 'e' && s[idx_e] != 'E'; ++idx_e );
// 没有找到e,就判断有效区域是不是一个整数/小数。
if( idx_e > endIdx ) { return _isNumber( s, startIdx, endIdx ); }
// 如果找到e,就分别判断e的前后:
// e的前面:是不是一个整数/小数
// e的后面:是不是一个整数
return _isNumber( s, startIdx, idx_e - 1 ) && _isInteger( s, true, idx_e + 1, endIdx );
}
// 判断是不是一个整数/小数(不带e) bool _isNumber( const string &s, char start, char end ) { if( start > end ) { return false; }
// 找到第一个'.'号
char idx_dot = start;
for( ; idx_dot <= end && s[idx_dot] != '.'; ++idx_dot );
// 找到了第一个'.'号,
if( idx_dot <= end )
{
// 特殊情况'.'号在第一位,只需判断后面是不是整数(不带e,不带+/-号的)
if( idx_dot == start ) { return _isInteger( s, false, idx_dot + 1, end ); }
// 特殊情况'.'在最后一位,只需判断前面是不是整数(不带e,不带+/-号的)
else if( idx_dot == end ) { return _isInteger( s, false, start, idx_dot - 1 ); }
// 一般情况,就判断前后两半部分是不是整数(不带e,不带+/-号的)
else { return _isInteger( s, false, start, idx_dot - 1 ) && _isInteger( s, false, idx_dot + 1, end ); }
}
// 没找到.号,就判断是不是整数
return _isInteger( s, false, start, end );
}
// 判断是不是一个整数(不带e的,可以是带+/-号的) // sign:true表示开头可以有+/-号 bool _isInteger( const string &s, const bool &sign, char start, char end ) { if( sign && start <= end && ( s[start] == ‘+’ || s[start] == ‘-‘ ) ) { ++start; } if( start > end ) { return false; }
// 找到第一个非数字的字符
for( ; start <= end & s[start] >= '0' && s[start] <= '9'; ++start );
return start > end;
}
<a name="x8IIv"></a>
## 方法二:有限状态机
<a name="CH7Lf"></a>
# 训练题:字符串替换
题目:[http://t.cn/A62bGwLd](http://t.cn/A62bGwLd)
```cpp
string replaceSpace(string s) {
if( s.empty() ) { return ""; }
const auto oldSize = s.size(); // 原始字符串长度
int count = 0; // 空格数量
for( auto &shit : s ) if( shit == ' ' ) { ++count; }
if( count )
{
// 将string resize成newSize,刚好装下替换后的字符串。
int newSize = oldSize + ( count << 1 );
s.resize( newSize );
// 倒序修改原字符串
for( int oldIdx = oldSize - 1, newIdx = newSize - 1; oldIdx >= 0 && count; --oldIdx )
{
if( s[oldIdx] == ' ' )
{
s[newIdx--] = '0';
s[newIdx--] = '2';
s[newIdx--] = '%';
--count;
}
else { s[newIdx--] = s[oldIdx]; }
}
}
return s;
}
训练题:括号序列
方法一:栈
- 时间复杂度:O(n)
- 空间复杂度:O(n) ```cpp
bool isValid(string s) {
stack
for( auto &c : s )
{
switch( c )
{
case '(':
stk.push( ')' );
break;
case '[':
case '{':
stk.push( c + 2 );
break;
case ']':
case ')':
case '}':
{
if( !stk.empty() && stk.top() == c ) { stk.pop(); }
else { return false; }
}
break;
}
}
return stk.empty();
}
<a name="Hrz6t"></a>
## 方法二:数组
- 时间复杂度:O(n)
- 空间复杂度:O(n)
其实就是用数组代替栈。
```cpp
bool isValid(string s) {
char stk[100000] = {0};
const auto size = s.length();
// top表示栈顶索引。
for( int i = 0, top = -1; i < size; ++i )
{
switch( s[i] )
{
case '(':
stk[++top] = ')';
break;
case '[':
case '{':
stk[++top] = s[i] + 2;
break;
case ']':
case ')':
case '}':
{
if( top > -1 && stk[top] == s[i] ) { stk[top--] = 0; }
else { return false; }
}
break;
}
}
return !stk[0]; // 栈是否为空。
}
训练题:最长无重复串
情形一:int类型元素
方法一:set保存不重复串
int maxLength( vector<int> &arr )
{
const auto arr_size = arr.size();
if( arr_size < 2 ) { return arr_size; }
set<int> s;
int maxLen = 0;
for( int left = 0, right = 0; right < arr_size; ++right )
{
// s中有和arr[right]重复的元组,则从left索引开始删除,直到没有重复。
while( s.count( arr[right] ) ) { s.erase( arr[left++] ); }
s.insert( arr[right] );
maxLen = std::max( maxLen, right - left + 1 );
}
return maxLen;
}
方法二:数组保存不重复串
int maxLength(vector<int>& arr) {
const auto arr_size = arr.size();
if(arr_size < 2) return arr_size;
int hit[100000] = {0}; // 题目说明了数据规模小于10^5。
int maxLen = 0;
for(int left = 0, right = 0; right < arr_size;)
{
if(!hit[arr[right]])
{
hit[arr[right]] = 1;
maxLen = max(maxLen, right++ - left + 1);
}
else hit[arr[left++]] = 0;
}
return maxLen;
}
情形二:字符类型元素
方法一:set集合
int lengthOfLongestSubstring(string s) {
set<int> dataSet;
const auto size = s.size();
int maxLen = 0;
for( int left = 0, right = 0; right < size; ++right )
{
while( dataSet.count( s[right] ) ) { dataSet.erase( s[left++] ); }
dataSet.insert( s[right] );
maxLen = std::max( maxLen, right - left + 1 );
}
return maxLen;
}
方法二:数组
int lengthOfLongestSubstring(string s) {
char dataSet[1 << ( 1 << 3 * sizeof( char ) )] = {0};
const auto size = s.size();
int maxLen = 0;
for( int left = 0, right = 0; right < size; ++right )
{
while( dataSet[s[right]] ) { dataSet[s[left++]] = 0; }
dataSet[s[right]] = 1;
maxLen = std::max( maxLen, right - left + 1 );
}
return maxLen;
}
训练题:Z字形变换
// 字符串:PAYPALISHIRING,行数为3时
//
// 0 1 2 3 4 5 6
// 0 P A H N
// 1 A P L S I I G
// 2 Y I R
//
// 输出字符串:PAHNAPLSIIGYIR
// 字符串:PAYPALISHIRING,行数为4时
//
// 0 1 2 3 4 5 6
// 0 P I N
// 1 A L S I G
// 2 Y A H R
// 3 P I
//
// 输出字符串PINALSIGYAHRPI
// 行数为5时
// 0 1 2 3 4 5 6
// 0 P I A
// 1 A A A A
// 2 Y A A A
// 3 P A A A
// 4 A A A
方法一:模拟法
- 时间复杂度:O(n)
- 空间复杂度:O(n) + O(n),一个是返回的字符串空间,一个是vector的空间。 ```cpp
// 输出字符串PAYPALISHIRING,行数为3,排列如下: // P A H N vector1装这一行,在这里flag变为+1,可以理解为row增加的方向(是+1还是-1) // A P L S I I G vector2装这一行, // Y I R vector3装这一行,在这里flag变为-1, // // 输出字符串的时候拼接这3个vector即可。
string convert(string s, int numRows) { if( numRows == 1 ) { return s; }
const auto size = s.size();
vector<string> datamap( min(numRows, (int)size) );
char flag = -1;
short row = 0;
for( int idx = 0; idx < size; idx++ )
{
datamap[row].append(1, s[idx] );
if( !( idx % ( numRows - 1 ) ) ) { flag = -flag; }
row += flag;
}
string reverseStr;
reverseStr.reserve( size );
for( auto &row : datamap ) { reverseStr.append( row.begin(), row.end() ); }
return reverseStr;
}
<a name="tpVhB"></a>
## 方法二
- 时间复杂度:O(n)
- 空间复杂度:O(n),为返回的字符串空间。
```cpp
// 逆向思维,从结果逆向分析。
// -----------------------------------------------
// 字符串:PAYPALISHIRING,行数为4时
// 0 1 2 3 4 5 6
// 0 P I N
// 1 A L S I G
// 2 Y A H R
// 3 P I
// -----------------------------------------------
// 按行,从上到下,从左到右输出,设row为行,idx为row行的第idx个字符。numRows为行数。
// 则:row的区间为[0, numRows-1]
// 我们需要找到源字符串索引oldIdx关于row、idx的表达式即可。
// 分析排列规律我们可以得出:
// row = 0或numRows-1时,oldIdx = ( numRows * 2 - 2 ) * idx + row;
// row = 其他时,我们需要从奇偶索引区分,设idx为第k个奇数/偶数,k从0开始。k = idx/2
// 当idx为奇数时,oldIdx = ( numRows * 2 - 2 )*k + (numRows * 2 - 2) - row
// 当idx为偶数时,oldIdx = ( numRows * 2 - 2 )*k + row
string convert( string s, int numRows )
{
if( numRows == 1 ) { return s; }
const auto size = s.size();
string ret;
ret.reserve( size );
for( short row = 0, oldIdx = -1; row <= numRows - 1; ++row )
{
for( short idx = 0;; ++idx )
{
if( row == 0 || row == numRows - 1 )
{
oldIdx = ( numRows * 2 - 2 ) * idx + row;
}
else
{
oldIdx = ( numRows * 2 - 2 ) * ( idx / 2 ) + ( ( idx & 0x1 ) ? ( numRows * 2 - 2 - row ) : row );
}
if( oldIdx < size )
{
ret.append( 1, s[oldIdx] );
}
else { break; }
}
}
return ret;
}
训练题:翻转单词顺序
方法一:模拟法
string reverseWords(string s) {
if(s.empty()) return "";
string ret;
const auto size = s.size();
ret.reserve(s.size());
// idx:从尾到头遍历字符串
// left,right为搜索到的单词索引范围
for(int idx = size - 1, left, right; idx >= 0; --idx){
// 找到第一个非空字符,也即是单词的结尾字母索引。
for(; idx >= 0 && s[idx] == ' '; --idx);
// 没有找到,直接结束
if(idx < 0) break;
right = idx;
// 找到这个单词的头部索引
for(; idx > 0 && s[idx - 1] != ' '; --idx);
left = idx;
// 追加
ret.append(s.begin() + left, s.begin() + right + 1).append(" ");
}
// 最后结尾会多一个空格。
ret.pop_back();
return ret;
}
方法二:栈
- 时间复杂度:O(n)
- 新开辟一个string
- 空间复杂度:O(n) ```cpp
string reverseWords( string s )
{
stack
// 遍历一遍字符串,统计出所有单词起始/结束索引,保存到stk栈中。
for( int on = 0, head = 0; head < inputSize; ++head )
{
if( on && s[head] == ' ' ) // 遍历到第一个空格字符且前一次遍历的是非空格字符,则表示到了单词的结尾,将索引压栈
{
newLen += head - stk.top(); // 将该单词的长度统计到newLen中。
stk.push( head - 1 ); // 将单词结束索引压栈
on = 0; // head当前并不在单词的索引中,即head所指的是空格索引
}
else if( !on && s[head] != ' ' ) // 遍历到第一个非空格字符,即将索引压栈,表示当前单词的起始索引。
{
stk.push( head ); // 将单词起始索引压栈
on = 1; // 此后遍历的非空格字符并不是第一个,即只是单词的中间索引
}
if( head == inputSize - 1 && on ) // 特殊情况,当正在遍历一个单词,且刚好又到了结尾时。
{
// lastIdx表示这个单词的结尾索引,有两种情况:
// 情况一:"...abcd ",结尾索引是head-1
// 情况二:"...abcd",结尾索引是head
int lastIdx = s[head] == ' ' ? head - 1 : head;
newLen += lastIdx - stk.top() + 1;
stk.push( lastIdx );
}
}
if( !newLen ) { return ""; } // 没有统计到单词,直接返回空。
string reverseStr;
reverseStr.reserve( newLen + ( stk.size() >> 1 ) - 1 ); // 设置好新字符串预计长度(加上空格数),避免发生“内存重组”
// start,end表示一个单词的起始结尾索引。
for( int start = -1, end = -1, newStart = 0; !stk.empty(); )
{
end = stk.top();
stk.pop();
start = stk.top();
stk.pop();
reverseStr.append( s.begin() + start, s.begin() + end + 1 ); // 将单词拼接到reverseStr结尾
reverseStr.append( " " ); // 别忘了加上空格
}
reverseStr.pop_back(); // 最后一个单词结尾不需要空格
return reverseStr;
}
<a name="vaek3"></a>
## 方法三:stringstream+栈
- 时间复杂度:O(n)
- 空间复杂度:O(n) + O(n)
和上方法的差别是,本方法借助stringstream从字符串中直接提取字符串,逻辑上简单了不少,但是效率大大减低,因为有多次string拷贝。
```cpp
string reverseWords( string s )
{
int newLen = 0; // 新的字符串的长度
stringstream ss( s );
string tmp;
stack<string> stk; // 每个栈元素都意味着发生了一个内存分配以及拷贝,效率会大大降低。
while( ss >> tmp )
{
newLen += tmp.size();
stk.push( tmp );
}
if( !newLen ) { return ""; }
string retStr;
retStr.reserve( newLen + stk.size() - 1 );
while( !stk.empty() )
{
retStr.append( stk.top() );
retStr.append( " " );
stk.pop();
}
retStr.pop_back();
return retStr;
}
训练题:字符串转整数
class Solution {
public:
int myAtoi(string s) {
if(s.empty()) return 0;
const auto size = s.size();
int start = 0;
int ret = 0;
char tmp = 0;
bool positive = true;
for(; start < size && s[start] == ' '; ++start);
if(start == size) return 0;
if(s[start] == '+' && ++start);
else if(s[start] == '-' && ++start) positive = false;
if(start == size) return 0;
for(; start < size && s[start] >= '0' && s[start] <= '9'; ++start){
tmp = s[start] - '0';
if(ret > 214748364) return positive ? 2147483647 : -2147483648;
if(ret == 214748364 ){
if(positive && tmp >= 7) return 2147483647;
else if(!positive && tmp >= 8) return -2147483648;
}
ret = ret * 10 + tmp;
}
return positive ? ret : ret*-1;
}
};