一、字符串去重
题目: 去除重复字母(LeetCode-困难)
给你一个仅包含小写字母的字符串,请你去除字符串中重复的字母,使得每个字母只出现一次。需保证返回结果的字典序最小(要求不能打乱其他字符的相对位置)
示例1:
输入:"bcabc"
输出:"abc"
分析:
- 判断字符串S可能出现的特殊情况, 例如空字符串, 字符串长度为1
- 遍历S, 用栈Stack记录S中每一个字符出现的次数.
- 用栈来记录去除重复字母的结果利用特性来找到正确的次序
- 遍历S, 判断当前S[i]是否存在栈中: 1.不存在,则直接入栈 2.存在,记录下来,用变量isExist标记
- 判断isExist, 1. 存在, Stack中当前字母次数减一, 继续下一个字母 2.不存在, 遍历栈, 判断当前字母是否小于栈顶并且后续还会出现栈顶元素, 成立则出栈,否则进栈
代码:
char * removeDuplicateLetters(char * s){
int len = (int)strlen(s);
//特殊情况
if (s == NULL || len == 1) {
return s;
}
char record[26];
memset(record, 0, 26);
//记录顺序
char *stack = (char *)malloc(sizeof(char) * len + 1);
int top = -1;
//记录每一个字符出现的次数
for (int i = 0; i < len; i++) {
record[s[i] - 'a']++;
}
//遍历字符串
for (int i = 0; i < len; i++) {
//空栈入栈
if (top == -1) {
stack[++top] = s[i];
continue;
}
//判断栈中是否存在当前字符
int isExist = 0;
for (int j = 0; j <= top; j++) {
if (s[i] == stack[j]) {
isExist = 1;
break;
}
}
if (isExist) {
//存在字符, 抛弃当前字符
record[s[i] - 'a']--;
} else {
//不存在, 遍历栈顶是否大于当前字符并且后续仍会有当前字符出现, 给当前字符挑选最适合的位置
while (top > -1 && stack[top] > s[i] && record[stack[top] - 'a'] > 1) {
record[stack[top] - 'a']--;
top--;
}
//找到合适位置, 进栈
stack[++top] = s[i];
}
}
//补\0
stack[++top] = '\0';
return stack;
}
二、字符串匹配
题目: 有一个主串S = {a, b, c, a, c, a, b, d, c},请找到模式串T在主串中第一次出现的位置;
提示: 不需要考虑字符串大小写问题, 字符均为小写字母; 例如: T= { a, b, d } 在主串 5 的位置
2.1、BF算法
来源于暴风, 其实就是暴力法
分析:
- 遍历主串, 判断当前字母s[i]是否与T[j]相同
- 如果不同, 则i++, 继续判断s[i]是否与T[j]相同
- 如果相同, 则判断s[i+1]是否与T[j+1]相同, 重复此步骤
代码:
int StrAssign(char *S, char *T) {
if (S == NULL || T == NULL) {
return -1;
}
int lenS = (int)strlen(S);
int lenT = (int)strlen(T);
for (int i = 0; i < lenS - lenT; i++) {
for (int j = 0; j < lenT; j++) {
//当两个字母不等, 则直接进入主串的下一个字符
if (S[i + j] != T[j]) {
break;
}
//子串遍历完成, 返回位置
if (j == lenT - 1) {
return i;
}
}
}
return -1;
}
2.2、RK算法
来源于 Rabin & Karp
在BF算法中, 假设碰到 aaaaa....aaab, 匹配 aaaaab, 会耗费大量的无用时间. 这就有了这个效率更高的算法, 但是复杂度却比BF高了不少
哈希值算法:
例如, 123 = 1 * 10^2 + 2 * 10^1 + 3 * 10^0 也就是在十进制中 每一个单独的数字是不会重复的.
那么在字母中, 单独的字母也是不会出现重复的. 我们可以把字母当成一个26进制.
那么 bcd = b * 26^2 + c * 26^1 + d * 26^0, 把a-z比做0-25.
换算下来就是 abc = 26^2 + 52 + 3 = 731
题目分析:
- RK算法核心: 将主串S拆分为lenS(主串长度) - lenT(匹配串长度) + 1个子串, 每一个子串长度与待匹配串T一致
- 将T转换为哈希值
- 将子串转换为哈希值, 判断是否与T哈希值想等
- 如果不等, 则可以用子串哈希值减去子串第一个字母, 接上下一个字母算出哈希值再次匹配
- 如果相等, 结束循环, 找到匹配的位置,进行二次确认
代码:
//字符串转换哈希值 S: 字符串 length: 需要转换的长度
int strToHash(char *s, int length) {
int value = 0;
for (int i = 0; i < length; i++) {
value = value * 26 + s[i] - 'a';
}
return value;
}
int StrAssign(char *S, char *T) {
int lenS = (int)strlen(S);
int lenT = (int)strlen(T);
//主串下第一个子串的hash
int hashS = strToHash(S, lenT);
//待匹配串的hash
int hashT = strToHash(T, lenT);
for (int i = 0; i < lenS - lenT + 1; i++) {
//子串首位hash
int headHash = S[i] - 'a';
for (int j = 0; j < lenT - 1; j++) {
headHash = headHash * 26;
}
if (hashS != hashT) {
//去掉子串hash中第一个字符的hash, 拼接上子串最后一位的下一个字符的hash
hashS = (hashS - firstHash) * 26 + S[i + lenT] - 'a';
}else {
int result = 1
//匹配成功, 检验是否正确
for (int k = 0; k < lenT; k++) {
if (S[k + i] != T[k]) {
result = -1;
break;
}
}
if (result) {
return i;
}
//去掉子串hash中第一个字符的hash, 拼接上子串最后一位的下一个字符的hash
hashS = (hashS - firstHash) * 26 + S[i + lenT] - 'a';
}
}
return -1;
}