常见字符串匹配算法Python实现

常见字符串匹配算法Python实现

class StringMatching(object):
    """常见字符串匹配算法"""

    @staticmethod
    def bf(main_str, sub_str):
        """
        BF 是 Brute Force 的缩写,中文叫作暴力匹配算法
        在主串中,检查起始位置分别是 0、1、2…n-m 且长度为 m 的 n-m+1 个子串,看有没有跟模式串匹配的
        """
        a = len(main_str)
        b = len(sub_str)
        for i in range(a - b + 1):
            if main_str[i:i + b] == sub_str:
                return i
        return -1

    @staticmethod
    def generate_bc(sub_str):
        """
        :param sub_str: 是模式串
        :return: 返回一个散列表  散列表的下标是 b_char 中每个字符的 ascii 码值,下标对应的值是该字符在b_char中最后一次出现的位置
        """
        ascii_size = 256
        ascii_list = [-1] * ascii_size  # 初始化一个散列表

        for i in range(0, len(sub_str)):
            ascii_val = ord(sub_str[i])  # 计算 b_char中每个字符的 ascii 值
            ascii_list[ascii_val] = i  # 存每个字符在 b_char 最后一次出现的位置
        return ascii_list

    @staticmethod
    def move_by_gs(j, m, suffix, prefix):
        """
        :param j: 表示坏字符对应的模式串中的字符下标
        :param m: m 表示模式串的长度
        :param suffix:  suffix 数组中下标k,表示后缀子串的长度,下标对应的数组值存储的是后缀子串在字符串sub_str的位置
        :param prefix: 而 prefix 数组中下标k,表示后缀子串的长度,对应的值 True 或者 False 则表示该后缀子串跟相同长度的前缀子串是否相对
        :return: 应该往后滑动的距离
        """
        k = m - 1 - j  # k 好后缀长度
        if suffix[k] != -1:  # 如果后缀在字符串中的所有前缀子串中存在
            return j - suffix[k] + 1
        for r in range(j + 2, m):  # 如果有后缀等于前缀
            if prefix[m - r]:
                return r
        return m  # 如果前面两个规则都不使用,则直接往后滑动 m 位

    @staticmethod
    def generate_gs(sub_str):
        """
        假如字符串sub_str = cabcab, 那么后缀子串有[b,ab,cab,bcab,abcab]
        suffix 数组中下标k,表示后缀子串的长度,下标对应的数组值存储的是后缀子串在字符串sub_str的位置
        如:suffix[1] = 2, suffix[2] = 1, suffix[3] = 0, suffix[4] = -1, suffix[5] = -1

        而 prefix 数组中下标k,表示后缀子串的长度,对应的值 True 或者 False 则表示该后缀子串跟相同长度的前缀子串是否相对
        如 prefix[1] = False, prefix[2] = False, prefix[3] = True (表示 后缀cba = 前缀cba), prefix[4] = False, prefix[5] = False,
        """
        m = len(sub_str)
        suffix = [-1] * m
        prefix = [False] * m
        for i in range(0, m - 1):
            j = i
            k = 0  # 公共后缀串长度,
            while j >= 0 and sub_str[j] == sub_str[m - 1 - k]:  # 求公共后缀子串,先计算第一个跟模式子串最后一个字符相匹配的位置
                j -= 1  # 然后依次比较前面的字符是否相等,比如:cabcab,先从前往后历遍,计算得到 b 字符是符合要求的,
                k += 1  # 此时再从 b 字符前一个位置与模式串倒数第二个位置的字符去比较,如果还是相等,则继续,循环了 k 次说明匹配的字符长度就是 k
                suffix[k] = j + 1  # j+1 表示公共后缀子串在 sub_str[0:i]中的起始下标

            if j == -1:  # 如果公共后缀子串也是模式串的前缀子串
                prefix[k] = True
        return suffix, prefix

    def bm_simple(self, main_str, sub_str):
        """
        :param main_str: 主字符串
        :param sub_str: 模式串
        :return:
        仅用坏字符规则,并且不考虑 si-xi 计算得到的移动位数可能会出现负数的情况的代码实现如下
        """
        bc = self.generate_bc(sub_str)  # 记录模式串中每个字符最后出现的位置,也就是坏字符的哈希表
        i = 0  # 表示主串和模式串对齐的第一个字符
        n = len(main_str)  # 表示主串的长度
        m = len(sub_str)  # 表示子串的长度
        while i <= n - m:  # i 最多滑动到两个字符右对齐的位置
            j = m - 1  # 数组下标从0开始算,这里实际从子字符串的最后一个位置开始找坏字符
            while j >= 0:
                if main_str[i + j] != sub_str[j]:  # 坏字符对应模式串中的下标是 j
                    break
                j -= 1
            if j < 0:  # 没有坏字符,全部都匹配上了
                return i  # 匹配成功,返回主串和模式串第一个匹配的字符的位置
            bad_str_index = bc[ord(main_str[i + j])]  # 坏字符在子模式串中的位置
            i += (j - bad_str_index)  # 这里等同于将模式串往后滑动  j - bc[ord(main_str[i + j]) 位
        return -1

    def bm(self, main_str, sub_str):
        """
        :param main_str: 主字符串
        :param sub_str: 模式串
        :return:
        """
        bc = self.generate_bc(sub_str)  # 记录模式串中每个字符最后出现的位置,也就是坏字符的哈希表
        suffix, prefix = self.generate_gs(sub_str)

        i = 0  # 表示主串和模式串对齐的第一个字符
        n = len(main_str)  # 表示主串的长度
        m = len(sub_str)  # 表示子串的长度
        while i <= n - m:
            j = m - 1
            while j >= 0:
                if main_str[i + j] != sub_str[j]:  # 坏字符对应模式串中的下标是 j
                    break
                j -= 1
            if j < 0:
                return i  # 匹配成功,返回主串和模式串第一个匹配的字符的位置

            x = j - bc[ord(main_str[i + j])]  # 计算坏字符规则每次需要往后滑动的次数

            y = 0
            if j < m - 1:  # 如果有好后缀的话
                y = self.move_by_gs(j, m, suffix, prefix)  # 计算好后缀规则每次需要往后滑动的次数
            i = i + max(x, y)

        return -1

    @staticmethod
    def get_next_list(sub_str):
        """计算如: abababzabababa 每个位置的前缀集合与后缀集合的交集中最长元素的长度
           输出为: [-1, -1, 0, 1, 2, 3, -1, 0, 1, 2, 3, 4, 5, 4]
           计算最后一个字符 a 的逻辑如下:
           abababzababab 前缀后缀最大匹配了 6 个(ababab),次大匹配是4 个(abab),次大匹配的前缀后缀只可能在 ababab 中,
           所以次大匹配数就是 ababab 的最大匹配数,在数组中查到出该值为 3。第三大的匹配数同理,它既然比 3 要小,那前缀后缀也只能在 abab 中找,
           即 abab 的最大匹配数,查表可得该值为 1。再往下就没有更短的匹配了。来计算最后位置 a 的值:既然末尾字母不是 z,
           那么就不能直接 5+1=7 了,我们回退到次大匹配 abab,刚好 abab 之后的 a 与末尾的 a 匹配,所以 a 处的最大匹配数为 4。
        """
        m = len(sub_str)
        next_list = [None] * m
        next_list[0] = -1
        k = -1
        i = 1
        while i < m:
            while k != -1 and sub_str[k + 1] != sub_str[i]:  # k+1 是每个前缀子串的最后一个字符,i 是每个后缀子串的第一个字符
                k = next_list[k]  # 求次大匹配数,次大匹配数如 abab 是上层 ababab 的最大匹配数
            if sub_str[k + 1] == sub_str[i]:  # 如果前缀子串的最后一个字符和后缀子串的第一个字符相等,则增加前缀子串和后缀子串的长度再比较
                k += 1
            next_list[i] = k  # sub_str[0:i] 的前缀集合与后缀集合的交集中最长元素的长度
            i += 1
        return next_list

    def kmp(self, main_str, sub_str):
        n = len(main_str)
        m = len(sub_str)
        next_list = self.get_next_list(sub_str)
        i = 0
        j = 0
        while i < n and j < m:
            if j == -1 or main_str[i] == sub_str[j]:  # 如果子串的每个字符和主串的每个字符相等就继续循环
                i += 1
                j += 1
            else:  # 在模式串j处失配,需要找到sub_str[0:j-1]的前缀集合与后缀集合的交集中最长元素的长度, 然后直接将j更新
                j = next_list[j]  # 为next_list[j],本来是需要将main_str[i] 处的字符依次与 sub_str[0:j]中的每个元素依次再比较的
        if j == m:
            return i - j
        return -1

猜你喜欢

转载自blog.csdn.net/qq_20116223/article/details/110354908