数据结构与算法(八) -- 字符串去重与BF、RK算法

1,231 阅读4分钟

一、字符串去重

题目链接

题目: 去除重复字母(LeetCode-困难)

给你一个仅包含小写字母的字符串,请你去除字符串中重复的字母,使得每个字母只出现一次。需保证返回结果的字典序最小(要求不能打乱其他字符的相对位置)

示例1:

输入:"bcabc"

输出:"abc"

分析:

  1. 判断字符串S可能出现的特殊情况, 例如空字符串, 字符串长度为1
  2. 遍历S, 用栈Stack记录S中每一个字符出现的次数.
  3. 用栈来记录去除重复字母的结果利用特性来找到正确的次序
  4. 遍历S, 判断当前S[i]是否存在栈中: 1.不存在,则直接入栈 2.存在,记录下来,用变量isExist标记
  5. 判断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算法

来源于暴风, 其实就是暴力法

分析:

  1. 遍历主串, 判断当前字母s[i]是否与T[j]相同
  2. 如果不同, 则i++, 继续判断s[i]是否与T[j]相同
  3. 如果相同, 则判断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

题目分析:

  1. RK算法核心: 将主串S拆分为lenS(主串长度) - lenT(匹配串长度) + 1个子串, 每一个子串长度与待匹配串T一致
  2. 将T转换为哈希值
  3. 将子串转换为哈希值, 判断是否与T哈希值想等
  4. 如果不等, 则可以用子串哈希值减去子串第一个字母, 接上下一个字母算出哈希值再次匹配
  5. 如果相等, 结束循环, 找到匹配的位置,进行二次确认

代码:

//字符串转换哈希值 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;
}