数据结构与算法--字符匹配 & 字符去重

838 阅读9分钟

1. 去除重复字母

  • 题目

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

    示例1:
    输入:"bcabc"
    输出:"abc"
    
    示例2:
    输入:"cbacdcbc"
    输出:"acdb"
    
  • 题目分析

    题目的意思,你去除重复字母后,需要按最小的字典序返回.并且不能打乱其他字母的相对位置

    字典序:

    字符串之间比较和数字比较不一样,字符串比较是从头往后挨个字符比较,那个字符串大取决于两个字符串中第一个对应不相等的字符;

    例如: 
    任意一个 a 开头的字符串都大于任意一个 b 开头的字符串;
    例如字典中 apple 大于 book;
    
  • 思路

    1.判断字符串S是否为空,返回空,判断长度是否为1,直接返回S
    2.创建一个字符数组record,长度为26,来记录字符串中,每个字符出现的次数
    3.申请一个字符串栈stack,来记录去重后的字符,利用栈的特性,帮助我们找到正确的次序
    4.遍历字符串
    5.从0-top,遍历stack,判断当前字符是否存在stack中,定义一个标识isExist,记录当前字符是
      否存在栈中,0:不存在,1:存在
    6.如果当前字符存在在栈中,isExist = 1,record中对应字符位置上的出现次数减一,并继续遍历下一
      个字符; 表示当前的stack已经有这个字符了没有必要处理这个重复的字母;
    7.isExist = 0,表示当前字符不存在于栈中,需要找到正确的位置,入栈
      判断条件:栈非空 top>-1
              栈顶元素 > 当前字符
              栈顶元素,在以后字符中还会出现
      通过一个while循环找到将栈中位置错误的数据,出栈. 找当前合适的位置,则结束while循环;
    8.找到正确位置,当前字符入栈,
    9.拼接字符结束符\0
    
  • 代码

char *removeDuplicateLetters(char *s) {
    // ✅1.特殊情况判断
    if (s == NULL || strlen(s) == 0) {
        return "";
    }
    if (strlen(s) == 1) {
        return s;
    }
    
    // ✅2.创建一个字符数组record,长度为26
    char record[26] = {0};
    int len = (int)strlen(s);
    int i;
    // 统计每个字符的频次
    for (i = 0; i < len; i++) {
        record[s[i] - 'a'] ++;
    }
    
    // ✅3.申请一个字符串栈stack,来记录去重后的字符
    char* stack = (char*)malloc(sizeof(char) * len + 2);
    memset(stack, 0, sizeof(char) * len + 2);
    int top = -1;
    
    // ✅4.遍历字符串
    for (i = 0; i < len; i++) {
        // ✅定义一个标识isExist,记录当前字符是否存在栈中,0:不存在,1:存在
        int isExist = 0;
        
        // ✅5.从0-top,遍历stack,判断当前字符是否存在stack中
        for (int j = 0; j <= top; j++) {
            if (s[i] == stack[j]) {
                isExist = 1;
                break;
            }
        }
        
        // ✅6.当前字符存在在栈中,isExist = 1,record中对应字符位置上的出现次数减一
        if (isExist) {
            record[s[i] - 'a']--;
        } else {
            // ✅7.isExist = 0,表示当前字符不存在于栈中,需要找到正确的位置
            // 栈非空 top>-1 && 栈顶元素 > 当前字符 && 栈顶元素,在以后字符中还会出现
            //record[stack[top]] > 1表示后面还会出现
            //例如b,c因为不符合以下条件会直接入栈.stack[] = "bc",但是当当前字符是"a"时,由于bcabc,a不应该是在stack的顺序是"bca",所以要把位置不符合的字符出栈;
            //top = 1,stack[top] > s[i], c>a; 并且stack[top] 在之后还会重复的出现,所以我们可以安心的把stack中的栈顶C出栈,所以stack[]="b",top减一后等于0; 同时也需要将record[c]出现次数减一;
            //top=0,stack[top]>s[i],b>a,并且stack[top] 在之后还会出现,所以stack把栈顶b出栈,所以此时栈stack[]="",top减一后等于-1, 此时栈中位置不正确的字符都已经移除;
            while (top > -1 && stack[top] > s[i] && record[s[top] - 'a'] > 1) {
                // 跳过该元素,出现次数减一
                record[s[top] - 'a']--;
                // 出栈
                top--;
            }
            
            // ✅8.找到正确的位置,当前字符入栈
            top++;
            stack[top] = s[i];
        }
    }
    
    // ✅9.结束栈顶添加字符结束符
    stack[++top] = '\0';
    
    return stack;
}

2. 字符串匹配

题目:

给一个仅包含小写字母的字符串主串 S = abcacabdc,模式串 T = abd,请查找出模式串在主中第一次出现的位置;

提示:主串和模式串均为小写字母

2.1  BF算法-爆发匹配算法

BF 算法比较示意图

主串和模式串依次比较,直到达到相互匹配

  • 思路

    1.分别利用计数指针i和j指示主串S和模式串T中当前正待比较的字符位置,i为初值pos,j的初值为1
    2.如果2个串均为比较到串尾,即i和jx均小于等于S和T的长度,执行以下循环操作
      1.S[i]和T[j]比较,若相等,则i和j分别指示字符串下一个位置,继续比较后续的字符
      2.若不相等,指针后退,重新开始匹配,从主串的(i = i-j+2)起重新和模式串串第一个c个字
        符(j=1)比较
    3.如果j>T.length, 说明模式串中的每一个字符串依次和主串中的一个连续字符序列相等,则匹配成功,
      返回m和模式串T中第一个字符的在主串S中的序号(i-T.length);否则匹配失败,返回0
    
  • 代码实现

#define OK 1
#define ERROR 0
#define TRUE 1
#define FALSE 0

#define MAXSIZE 40    /* 存储空间初始分配量 */
typedef int Status;   /* Status是函数的类型,其值是函数结果状态代码,如OK等 */
typedef int ElemType; /* ElemType类型根据实际情况而定,这里假设为int */
typedef char String[MAXSIZE+1]; /*  0号单元存放串的长度 */

// 生成一个其值等于chars的串 T
Status StrAssign(String T,char *chars) {
    
    int i;
    if (strlen(chars) > MAXSIZE) {
        return ERROR;
    } else {
        T[0] = strlen(chars); // 第0个位置,记录的字符串长度
        for (i = 1; i <= T[0]; i++) {
            T[i] = *(chars+i-1);
        }
        return OK;
    }
}

/*  输出字符串T。 */
void StrPrint(String T)
{
    int i;
    for(i=1;i<=T[0];i++)
        printf("%c",T[i]);
    printf("\n");
}

int Index_BF(String S, String T,int pos){
     
    // i 主串S中当前位置下标值,pos为指定开始匹配位置
    int i = pos;
    // j 子串T中当前位置下标值
    int j = 1;
    
    // 开始比较,第0个位置,存储的是长度
    while (i <= S[0] && j <= T[0]) {
        // 比较2个字符相等
        if (S[i] == T[j]) {
            i++;
            j++;
        } else { // 不相等,指针回退
            i = i - j + 2;
            j = 1;
        }
    }
    
    // 3.如果j>T[0],则找到了匹配模式
    if (j > T[0]) {
        return i - T[0];
    } else {
        return -1;
    }

}

// 调用
        int i,*p;
        String s1,s2;
        
        StrAssign(s1, "abcdex");
        printf("s1子串为");
        StrPrint(s1);
        
        
        StrAssign(s2, "bc");
        printf("s2子串为");
        StrPrint(s2);
        
        i = Index_BF(s1, s2, 1);
        printf("i = %d\n",i);

假设模式串长度为10,恰巧比较到第10个字符时,发现匹配失败,接着往后平移比较,每次都比较到最后一个字符才发现匹配失败,这样效率比较低,接下来,看另外一种RK解法。

2.2  RK算法

RK 算法的核心:将主串依次拆分为模式串长度的子串,并通过哈希公式,计算出每个子串的哈希值,与模式串的哈希值比较,相等则匹配成功。

注意:

RK 算法 主串每拆分一个子串,就计算哈希进行比较,而不是将所有的子串拆分完成后,再依次比较,这样的
好处是,假如第一个子串就匹配成功,后面子串的拆分工作就浪费了

那么如何将拆分的子串或者模式串换算成哈希值呢?

我们在换算数字的时候通常是一下面的方式换算的:

我们可以将字符换算成`ASCII码值,然后用这样的方式换算出子串或者模式串的哈希值。

我们可以通过以下方式,将字符换算成数字

a - a = 0
b - a = 1
c - a = 2
...
z - a = 25

因为有26个字母,我们可以以26为进制,对字符串进行换算,如下对字符串cba的换算

 cba = c * 26 * 26 + b * 26 + a * 1
     = 2 ✖ ️26 ✖ 26 + 1 ✖ 26 + 0 ✖ 1
     = 1378

此时,已经有了模式串的哈希值,那么在子串未匹配成功往后平移时的哈希值是怎么计算和推导的呢?

假设模式串为123,如图:

那么子串127274的换算如下:

 127:  s[i] = 1 * 10^2 + 2 * 10^1 + 7 * 10^0
 274:  s[i+1] = 2 * 10^2 + 7 * 10^1 + 4 * 10^0
       s[i+1] = 10 * (127 - 1 * 10^2) + 4 * 10^0
       s[i+1] = 10 * (s[i] - 1 * 10^2) + 4 * 10^0

s[i+1]实际等于s[i]减去最高位数据,其余的(m-1)位字符乘以进制,在加上最后一个字符

主串分割后的子串的哈希值:

St[i+1] = (St[i] - d^m-1 * S[i])* d + S[i+m] d为进制,此次为26

代码实现:

//d 表示进制
#define d 26
//为了杜绝哈希冲突. 当前发现模式串和子串的HashValue 是一样的时候.还是需要二次确认2个字符串是否相等.
int isMatch(char *S, int i, char *P, int m)
{
    int is, ip;
    for(is=i, ip=0; is != m && ip != m; is++, ip++)
        if(S[is] != P[ip])
            return 0;
    return 1;
}
//d^(m-1)位的值;
int getMaxValue(int m){
    int h = 1;
    for(int i = 0;i < m - 1;i++){
        h = (h*d);
    }
    
    return h;
}
int RK(char *S, char *P) {
    // 1.计算长度 n主串长度 m模式串长度
    int n = (int)strlen(S)
    int m = (int)strlen(P)
    
    //A.模式串的哈希值; St.主串分解子串的哈希值;
    unsigned int A   = 0;
    unsigned int St  = 0;
    
    // 2.求得子串与主串中0~m字符串的哈希值[计算子串与主串0-m的哈希值]
    //cba = 2 , 2 * 26 + 1,  (2 * 26 + 1) * 26 + 0
    for (int i = 0; i != m; i++) {
        A = (d * A + (P[i] - 'a'));
        St = (d * St + (S[i] - 'a'));
    }
    // 3.获取d^m-1值(因为经常要用d^m-1进制值)
    int hValue = getMaxValue(m);
    // 4.比较哈希
    for (int i = 0; i < n - m; i++) {
        if (A = St) {
            // 防止哈希冲突,重新比较每个字符
            if (isMatch(S, i, P, m)) {
                return i + 1;
            }
        } else {
            St = (d * (St - hValue * (S[i] - 'a')) + (S[i+m]-'a'));
        }
    }
    return -1;
}

    // 调用
    char *buf="abcababcabx";
    char *ptrn="cax";
    printf("主串为%s\n",buf);
    printf("子串为%s\n",ptrn);
        
    int index = RK(buf, ptrn);
    printf("find index : %d\n",index);

复杂度分析:

如果不做防止哈希冲突,时间复杂度为O(n)
如果做防止哈希冲突,时间复杂度为O(n*m)