LintCode Scramble String
判断给定两个等长的字符串是不是攀爬字符串。
对于给定的字符串,我们通过不断将其分割成两个非空子字符串的方式构建一棵二叉树。我们可以选择任一个非叶子节点然后交换其孩子节点,再重新从底部攀爬上去形成一个新字符串,那么该新字符串就与原字符串形成了攀爬字符串。
思路1
由于二叉树是递归的建立的,那么我们就可以尝试从递归的角度来解决这个问题。对于两个字符串树treeA和treeB,分别记其左右子树为treeA(B)_left,那么可以得到如下关系:
isScramble(treeA, treeB) = isScramble(treeA_left, treeB_left) && isScramble(treeA_right, treeB_right) ||
isScramble(treeA_left, treeB_right) && isScramble(treeA_right, treeB_left)。
用别人的话来说,就是把字符串s1分割成s11和s12,把字符串s2在同样位置分割成s21和s22,如果s11与s21,s12与s22都为攀爬字符串或者s11与s22,s12与s21为攀爬字符串,那么字符串s1和s2就同为攀爬字符串。
代码1
bool isScramble(string s1, string s2) {
//注意递归终结的条件
if (s1 == s2) return true;
string tmp1 = s1, tmp2 = s2;
sort(tmp1.begin(), tmp1.end());
sort(tmp2.begin(), tmp2.end());
if (tmp1 != tmp2) return false;
int len = s1.size();
for (int i = 1; i < len; i++) {
string s11 = s1.substr(0, i);
string s12 = s1.substr(i);
string s21 = s2.substr(0, i);
string s22 = s2.substr(i);
if (isScramble(s11, s21) && isScramble(s12, s22)) return true;
s21 = s2.substr(len-i);
s22 = s2.substr(0, len-i);
if (isScramble(s11, s21) && isScramble(s12, s22)) return true;
}
return false;
}
思路2
还有一种解法是动态规划,为什么可以这样说呢?因为发现判断s1和s2是不是攀爬字符串时用到的是s1子串和s2子串的信息,即可以理解为用到了历史信息,所以可以考虑用动态规划来解决。我们考虑状态dp[i][j][len]代表从字符串s1的i位开始,字符串s2的第j位开始,长度为len的字符串是不是攀爬字符串。那么根据之前的递归思路,我们可以写出递推关系:
dp[i][j][len] = dp[i][j][k]&&dp[i+k][j+k][len-k] || dp[i][j+len-k][k] && dp[i+k][j][len-k]。1 <= k < len,k可以理解为将字符串一分为二的那个位置。
注意dp[i][j][k]这些存储的历史信息,这里我们要把len循环放在最外层。边界情况就是len==1的情况,需要先单独处理一下。
代码2
bool isScramble(string s1, string s2) {
// write your code here
if (s1.size() != s2.size()) return false;
int n = s1.size();
vector<vector<vector<bool> >> dp(n, vector<vector<bool>>(n, vector<bool>(n+1, false)));
for (int i = 0; i < n; i++) {
for (int j = 0; j < n; j++) {
dp[i][j][1] = s1[i] == s2[j];
}
}
for (int len = 2; len <= n; len++) {
for (int i = 0; i <= n-len; i++) {
for (int j = 0; j <= n-len; j++) {
for (int k = 1; k < len; k++) {
if (dp[i][j][k]&&dp[i+k][j+k][len-k] ||
dp[i][j+len-k][k] && dp[i+k][j][len-k]) {
dp[i][j][len] = true;
}
}
}
}
}
return dp[0][0][n];
}
LintCode String to IntegerII
实现atoi函数,遇到非法表示则返回0,最后的整数如果溢出int型则返回INT_MAX或INT_MIN。
思路
先介绍一下atoi函数的处理思路: 首先跳过前导空格,然后判断一下符号有没有(+或-),遇上第一个数字字符开始转换,遇到第一个非数字字符便停止转换。这里采用的顺讯转换可以学习一下,因为平时我都是逆序+权来转换的。
代码
int atoi(string str) {
// write your code here
bool positive = true;
int i = 0;
while (str[i] == ' ' && i < str.size()) i++;
if (i == str.size()) return 0;
if (str[i] == '+' || str[i] == '-') {
positive = str[i] == '+';
i++;
}
int res = 0;
while (i < str.size()) {
if (isdigit(str[i])) {
res = res*10 + (str[i++]-'0'); //原数乘10然后加上字符,顺序转换。
}
else break;
if (res < 0) break; //向上溢出int型范围,此时res就变成了负数
}
//if (res < 0) return positive? INT_MAX : INT_MIN;
return res < 0 ? (positive ? INT_MAX : INT_MIN) : (positive ? res : -res);
}
注意
这题是基础而且很重要,面试有可能被问到。
LintCode Valid Number
给定一个字符串,问你该字符串是否是一个有效的数字,包含整数,小数,以及科学计数法。
思路
刚开始拿到这道题的时候有点感觉像atof的实现,于是按照思路进行处理,提交,发现有什么情况没考虑到就开始小修小补代码,后来也提交通过了。百度了一下,发现有更简洁的想法与做法。
首先明确一共有几种符合题意的字符串:数字、’.’、e,正负号,还有空格。常规思路先跳过前导空格和正负号,然后开始处理,便利到的结果有如下几种:
1)遍历到数字: 那么就跳过,遍历下一字符。
2)遍历到小数点:根据有一些测试样例可以知道,”1.”与”.1”都是符合题意的,所以对于小数点有如下要求,一是第一次出现,二是不能出现在指数e的后面。
3)遍历到指数: 同理,指数必须是第一次出现,而且指数前后必须要有数字出现。
4)遍历到正负号:由于起始的正负号已经处理过了,所以这里的正负号只能出现在指数的后面。
5)其他字符: 显然就不符合题意了,后来发现存在末尾0的情况,所以在刚开始的时候需要跳过前导0和末尾0。
综上所述,对于前面四种情况都需要相应的bool变量来标示。
代码
bool isNumber(string s) {
// write your code here
int i = 0, e = s.size()-1;
while (i <= e && isspace(s[i])) i++;
while (e >= i && isspace(s[e])) e--;
if (i > e) return false;
if (s[i] == '+' || s[i] == '-') i++;
bool num = false; // represent a num
bool dot = false; // represent a dot
bool exp = false; // represent eorE
while (i <= e) {
if (isdigit(s[i])) {
num = true;
} else if (s[i] == '.') {
if (exp || dot) return false;
dot = true;
} else if (s[i] == 'e' || s[i] == 'E') {
if (exp || !num) return false;
exp = true;
num = false;
} else if (s[i] == '+' || s[i] == '-') {
if (s[i-1] != 'e' || s[i-1] != 'E') return false;
} else
return false;
i++;
}
return num;
}
注意
后来了解到这道题还可以用 有限状态机 或者 正则表达式来做,代码简单的一笔。
有限状态机:http://blog.csdn.net/kenden23/article/details/18696083
正则表达式:http://blog.csdn.net/fightforyourdream/article/details/12900751
LintCode Wildcard Matching
判断两个字符串是不是匹配。这道题和Regular Expression Matching很像,不过后者的”“代表前一字符的序列,而此处的”“代表任一字符串,个人感觉,这道题要简单一点。
思路1
递归和动态规划。递归超时了,所以这里提及一下动态规划。我们维护一个状态dp[i][j]代表字符串s的前i个字符和字符串的前j个字符是不是匹配的。有了之前那一题的经验,这里我们只考虑p中会出现符号。
那么显然有两种情况,一是s[i-1] == p[j-1] || p[j-1] == ‘?’,代表第i-1位和第j-1位匹配,那么dp[i][j] = dp[i-1][j-1]。
第二种情况是p[j-1] == ‘‘,这种情况下dp[i][j] = dp[i][j] || dp[i-k][j-1],(0 <= k <= i) 也就是说把这个”“分别想象成空字符串,一个字符,两个字符,然后从历史存储信息中如何得到当前状态即可。
然后考虑边界情况,显然dp[0][0] = 1,然后提及一下这里的i需要从0开始遍历,为什么?因为如果s为空字符串,而p只有”*”也是匹配的,而如果p为空字符串,s为非空字符串是无论如何都不可能匹配的。
代码1
bool isMatch(string s, string p) {
// write your code here
int m = s.size(), n = p.size();
vector<vector<bool>> dp(m+1, vector<bool>(n+1, false));
dp[0][0] = 1;
// i = 0?
for (int i = 0; i <= m; i++) {
for (int j = 1; j <= n; j++) {
if (i > 0 && (s[i-1] == p[j-1] || p[j-1] == '?')) {
dp[i][j] = dp[i-1][j-1];
}
if (p[j-1] == '*') {
for (int k = 0; k <= i; k++) {
if (dp[i-k][j-1]) {
dp[i][j] = true;
break;
}
}
}
}
}
return dp[m][n];
}
类似正则表达式匹配的思想,其实k只需要取空字符串和一个字符串即可。
代码2
bool isMatch(string s, string p) {
// write your code here
int m = s.size(), n = p.size();
vector<vector<bool>> dp(m+1, vector<bool>(n+1, false));
dp[0][0] = 1;
// i = 0?
for (int i = 0; i <= m; i++) {
for (int j = 1; j <= n; j++) {
if (i > 0 && (s[i-1] == p[j-1] || p[j-1] == '?')) {
dp[i][j] = dp[i-1][j-1];
}
if (p[j-1] == '*') {
dp[i][j] = (i > 0 && dp[i-1][j]) || dp[i][j-1];
}
}
}
return dp[m][n];
}
思路2
发现了网上的贪心做法,虽然暂时没觉得贪在哪里。
代码3
bool isMatch(string s, string p) {
// write your code here
int pcurr = 0, scurr = 0, pstar = -1, sstar = -1;
while (scurr < s.size()) {
if (s[scurr] == p[pcurr] || p[pcurr] == '?') {
scurr++;
pcurr++;
} else if (p[pcurr] == '*') {
pstar = pcurr++;
sstar = scurr;
} else if (pstar != -1) {
pcurr = pstar+1;
scurr = ++sstar;
} else return false;
}
while (p[pcurr] == '*') pcurr++;
return pcurr == p.size();
}
感想
暑假快结束了,说好的刷完LintCode也没有完成,估计开学到了学校更难静下心来刷题,算法之路果然是遥遥无期啊:(