训练题:正则表达式匹配
题目: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+1std::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)```cppstring 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)其实就是用数组代替栈。```cppbool 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 + rowstring 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",结尾索引是headint 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拷贝。```cppstring 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;}};
