领域算法 - 字符串匹配算法

news2025/2/23 5:34:46

字符串匹配算法

文章目录

  • 字符串匹配算法
    • 一:KMP算法
      • 1:算法概述
      • 2:部分匹配表
      • 3:算法实现
    • 二:Moore算法
      • 1:算法概述
      • 2:代码实现
      • 3:完整实现
    • 三:马拉车算法
      • 1:算法概述
      • 2:Manacher算法原理与实现:
        • 2.1:Len数组简介与性质
        • 2.2:Len数组的计算
      • 3:算法实现

一:KMP算法

1:算法概述

在这里插入图片描述

首先,字符串"BBC ABCDAB ABCDABCDABDE"的第一个字符与搜索词"ABCDABD"的第一个字符,进行比较。因为B与A不匹配,所以搜索词后移一位。

在这里插入图片描述

因为B与A不匹配,搜索词再往后移。

在这里插入图片描述

就这样,直到字符串有一个字符,与搜索词的第一个字符相同为止。

在这里插入图片描述

接着比较字符串和搜索词的下一个字符,还是相同。

在这里插入图片描述

直到字符串有一个字符,与搜索词对应的字符不相同为止。

在这里插入图片描述

这时,最自然的反应是,将搜索词整个后移一位,再从头逐个比较。这样做虽然可行,但是效率很差,因为你要把"搜索位置"移到已经比较过的位置,重比一遍。

在这里插入图片描述

一个基本事实是,当空格与D不匹配时,你其实知道前面六个字符是"ABCDAB"。

KMP算法的想法是,设法利用这个已知信息,不要把"搜索位置"移回已经比较过的位置,继续把它向后移,这样就提高了效率

在这里插入图片描述

怎么做到这一点呢?可以针对搜索词,算出一张部分匹配表。这张表是如何产生的,后面再介绍,这里只要会用就可以了。

在这里插入图片描述

已知空格与D不匹配时,前面六个字符"ABCDAB"是匹配的。查表可知,最后一个匹配字符B对应的"部分匹配值"为2,因此按照下面的公式算出向后移动的位数:

移动位数 = 已匹配的字符数 - 对应的部分匹配值

因为 6 - 2 等于4,所以将搜索词向后移动4位。

在这里插入图片描述

因为空格与C不匹配,搜索词还要继续往后移。这时,已匹配的字符数为2(“AB”),对应的"部分匹配值"为0。所以,移动位数 = 2 - 0,结果为 2,于是将搜索词向后移2位。

在这里插入图片描述

因为空格与A不匹配,继续后移一位。

在这里插入图片描述

逐位比较,直到发现C与D不匹配。于是,移动位数 = 6 - 2,继续将搜索词向后移动4位。

在这里插入图片描述

逐位比较,直到搜索词的最后一位,发现完全匹配,于是搜索完成。如果还要继续搜索(即找出全部匹配),移动位数 = 7 - 0,再将搜索词向后移动7位,这里就不再重复了。

2:部分匹配表

核心 -> 找前缀和后缀的公用元素长度

在这里插入图片描述

--------------- 以上面的ABCDABD为例 -----------------

- "A"的前缀和后缀都为空集,共有元素的长度为0;

- "AB"的前缀为[A],后缀为[B],共有元素的长度为0;

- "ABC"的前缀为[A, AB],后缀为[BC, C],共有元素的长度0;

- "ABCD"的前缀为[A, AB, ABC],后缀为[BCD, CD, D],共有元素的长度为0;

- "ABCDA"的前缀为[A, AB, ABC, ABCD],后缀为[BCDA, CDA, DA, A],共有元素为"A",长度为1;

- "ABCDAB"的前缀为[A, AB, ABC, ABCD, ABCDA],后缀为[BCDAB, CDAB, DAB, AB, B],共有元素为"AB",长度为2;

- "ABCDABD"的前缀为[A, AB, ABC, ABCD, ABCDA, ABCDAB],后缀为[BCDABD, CDABD, DABD, ABD, BD, D],共有元素的长度为0。

3:算法实现

package com.cui;

/**
 * @author cui haida
 * 2025/1/20
 * desc -> KMP算法
 */
public class KmpDemo {
    public static void main(String[] args) {
        String s1 = "BBC ABCDAB ABCDABCDABDE";
        String s2 = "ABCDABD";
        System.out.println(kmpSearch(s1, s2, kmpNext(s2)));
    }

    /**
     * K字符串匹配
     * 使用KMP算法在主字符串s1中查找子字符串s2的第一次出现的位置
     * @param s1 主字符串
     * @param s2 子字符串
     * @param next 子字符串s2的next数组,用于指示在不匹配情况下应该从子字符串的哪个位置继续比较
     * @return 如果找到子字符串,返回子字符串在主字符串中第一次出现的位置;如果未找到,返回-1
     */
    public static int kmpSearch(String s1, String s2, int[] next) {
        // 遍历主字符串s1,从索引1开始,索引0不参与比较
        for (int i = 1, j = 0; i < s1.length(); i++) {
            // 当前字符不匹配时,根据next数组调整子字符串的索引j
            while (j > 0 && s1.charAt(i) != s2.charAt(j)) {
                j = next[j - 1];
            }
            // 当前字符匹配,子字符串索引j前进
            if (s1.charAt(i) == s2.charAt(j)) {
                j++;
            }
            // 子字符串完全匹配,返回匹配的起始位置
            if (j == s2.length()) {
                return i - j + 1;
            }
        }
        // 未找到匹配的子字符串,返回-1
        return -1;
    }


    /**
     * 得到一个字符串的部分匹配值
     *
     * 该方法用于计算KMP算法中的next数组,next数组是KMP算法的关键,
     * 它用于指示当当前字符不匹配时,应该从模式串的哪个位置继续比较。
     *
     * @param s 字符串
     * @return 部分匹配值
     */
    public static int[] kmpNext(String s) {
        // 初始化next数组,长度与输入字符串相同
        int[] next = new int[s.length()];
        // 字符串的第一个字符的部分匹配值为0
        next[0] = 0;
        // 遍历字符串,计算每个字符的部分匹配值
        for (int i = 1, j = 0; i < s.length(); i++) {
            // 当前字符与j位置的字符不匹配时,更新j值
            while (j > 0 && s.charAt(i) != s.charAt(j)) {
                j = next[j - 1];
            }
            // 当前字符与j位置的字符匹配时,增加j值
            if (s.charAt(i) == s.charAt(j)) {
                j++;
            }
            // 记录当前字符的部分匹配值
            next[i] = j;
        }
        // 返回计算得到的next数组
        return next;
    }

}

二:Moore算法

1:算法概述

ctrl-F就是基于这种算法

根据Moore教授自己的例子来解释这种算法

  1. 假定字符串为"HERE IS A SIMPLE EXAMPLE",搜索词为"EXAMPLE"。

在这里插入图片描述

  1. 首先,"字符串"与"搜索词"头部对齐,从尾部开始比较

    这是一个很聪明的想法,因为如果尾部字符不匹配,那么只要一次比较,就可以知道前7个字符(整体上)肯定不是要找的结果

    我们看到,"S"与"E"不匹配。这时,“S"就被称为"坏字符”(bad character),即不匹配的字符。

    我们还发现,"S"不包含在搜索词"EXAMPLE"之中,这意味着可以把搜索词直接移到"S"的后一位

    在这里插入图片描述

  2. 依然从尾部开始比较,发现"P"与"E"不匹配,所以"P"是"坏字符"。

    在这里插入图片描述

    但是,"P"包含在搜索词"EXAMPLE"之中。所以,将搜索词后移两位,两个"P"对齐。

在这里插入图片描述

因此:后移位数 = 坏字符的位置 - 搜索词中的上一次出现位置

如果"坏字符"不包含在搜索词之中,则上一次出现位置为 -1

  1. 依然从尾部开始比较,发现"E"与"E"匹配。

在这里插入图片描述

  1. 比较前面一位,"LE"与"LE"匹配。

在这里插入图片描述

  1. 比较前面一位,"PLE"与"PLE"匹配。

在这里插入图片描述

  1. 比较前面一位,"MPLE"与"MPLE"匹配。

    我们把这种情况称为"好后缀"(good suffix),即所有尾部匹配的字符串。注意,“MPLE”、“PLE”、“LE”、"E"都是好后缀。

    后移位数 = 好后缀的位置 - 搜索词中的上一次出现位置

    "好后缀"的位置以最后一个字符为准。假定"ABCDEF"的"EF"是好后缀,则它的位置以"F"为准,即5(从0开始计算)。 
    如果"好后缀"在搜索词中只出现一次,则它的上一次出现位置为 -1。比如,"EF"在"ABCDEF"之中只出现一次,则它的上一次出现位置为-1(即未出现)。 
    如果"好后缀"有多个,则除了最长的那个"好后缀",其他"好后缀"的上一次出现位置必须在头部。
    

在这里插入图片描述

  1. 比较前一位,发现"I"与"A"不匹配。所以,“I"是"坏字符”

在这里插入图片描述

  1. 那么这时候就要权衡下好后缀和坏字符两个规则,用哪个了-> 当然是用后移多的了
  • 坏字符规则"只能移3位
  • “好后缀规则"可以移6位[所有的"好后缀”(MPLE、PLE、LE、E)之中,只有"E"在"EXAMPLE"还出现在头部,所以后移 6 - 0 = 6位]
  1. 继续从尾部开始比较,“P"与"E"不匹配,因此"P"是"坏字符”。根据"坏字符规则",后移 6 - 4 = 2位。

在这里插入图片描述

  1. 从尾部开始逐位比较,发现全部匹配,于是搜索结束。

在这里插入图片描述

更巧妙的是,这两个规则的移动位数,只与搜索词有关,与原字符串无关

可以预先计算生成坏字符规则表和好后缀规则表。使用时,只要查表比较一下就可以了。

2:代码实现

在上面,我们说到了,在BM算法中有两个关键概念–坏字符和好后缀,所以我们的代码实现将分为三个步骤。

  • 利用坏字符算法,计算匹配串可以滑动的距离
  • 利用好后缀算法,计算匹配串可以滑动的距离
  • 结合坏字符算法和好后缀算法,实现BM算法,查看匹配串在主串中存在的位置

step1: 坏字符算法,经过之前的分析,我们找到坏字符之后,需要查找匹配串中是否出现过坏字符,如果出现多个,我们滑动匹配串,将靠后的坏字符与主串坏字符对齐。如果不存在,则完全匹配。如果我们每次找到坏字符都去查找一次匹配串中是否出现过,效率不高,所以我们可以用一个hash表保存匹配串中出现的字符以及最后出现的位置,提高查找效率。

我们设定的只有小写字母,可以直接利用一个26大小的数组存储,数组下标存储出现的字符(字符-‘a’),数组值存储出现的位置。

int[] modelStrIndex;
private void badCharInit(char[] modelStr) {
    modelStrIndex = new int[26];
    //-1表示该字符在匹配串中没有出现过
    for (int i = 0 ; i < 26 ; i ++) {
        modelStrIndex[i] = -1;
    }
    for (int i = 0 ; i < modelStr.length ; i++) {
        //直接依次存入,出现相同的直接覆盖,
        //保证保存的时候靠后出现的位置
        modelStrIndex[modelStr[i] - 'a'] = i;
    }
}

查找坏字符出现位置badCharIndex,未出现,匹配成功,直接返回0。

查找匹配串中出现的坏字符位置modelStrIndex,未出现,滑动到坏字符位置之后,直接返回匹配串的长度。

返回badCharIndex - modelStrIndex。

注:坏字符是指与匹配串字符不匹配的主串字符,是看的主串,但是我们计算的位置,是匹配串中的位置。

/**
 * @param mainStr 主串
 * @param modelStr 模式串
 * @param start 模式串在主串中的起始位置
 * @return 模式串可滑动距离,如果为0则匹配上
 */
private int badChar(char[] mainStr, char[] modelStr, int start) {
    //坏字符位置
    int badCharIndex = -1;
    char badChar = '\0';
    //开始从匹配串后往前进行匹配
    for (int i = modelStr.length - 1 ; i >= 0 ; i --) {
        int mainStrIndex = start + i;
        //第一个出现不匹配的即为坏字符
        if (mainStr[mainStrIndex] != modelStr[i]) {
            badCharIndex = i;
            badChar = mainStr[mainStrIndex];
            break;
        }
    }
    if (-1 == badCharIndex) {
        //不存在坏字符,需匹配成功,要移动距离为0
        return 0;
    }
    //查看坏字符在匹配串中出现的位置
    if (modelStrIndex[badChar - 'a'] > -1) {
        //出现过
        return badCharIndex - modelStrIndex[badChar - 'a'];
    }
    return modelStr.length;
}

**step2:**好后缀算法,经过之前的分析,我们在实现好后缀算法的时候,有一个后缀前缀匹配的过程,这里我们仍然可以事先进行处理。将匹配串一分为二,分别匹配前缀和后缀字串。

ps:开始我的处理是两个数组,将前缀后缀存下来,需要的时候进行匹配,但是在写文章的时候,我突然回过神来,我已经处理了一遍了,为什么不直接标记是否匹配呢?

初始化匹配串前缀后缀是否匹配数组,标志当前长度的前缀后缀是否匹配。

//对应位置的前缀后缀是否匹配
boolean[] isMatch;
public void goodSuffixInit(char[] modelStr) {
    isMatch = new boolean[modelStr.length / 2];
    StringBuilder prefixStr = new StringBuilder();
    List<Character> suffixChar = new ArrayList<>(modelStr.length / 2);
    for (int i = 0 ; i < modelStr.length / 2 ; i ++) {
        prefixStr.append(modelStr[i]);
        suffixChar.add(0, modelStr[modelStr.length - i - 1]);
        isMatch[i] = this.madeSuffix(suffixChar).equals(prefixStr.toString());
    }
}
/**
 * 组装后缀数据
 * @param characters
 * @return
 */
private String madeSuffix(List<Character> characters) {
    StringBuilder sb = new StringBuilder();
    for (Character ch : characters) {
        sb.append(ch);
    }
    return sb.toString();
}

step3: 结合坏字符和好后缀算法实现BM算法,起始就是每一次匹配,同时调用坏字符和好后缀算法,如果返回移动距离为0,表示已经匹配成功,直接返回当前匹配的起始距离。其余情况下,滑动坏字符和好后缀算法返回的较大值。如果主串匹配完还没有匹配成功,则返回-1。

注:加了一些日志打印匹配过程

public int bmStrMatch(char[] mainStr, char[] modelStr) {
    //初始化坏字符和好后缀需要的数据
    this.badCharInit(modelStr);
    this.goodSuffixInit(modelStr);
    int start = 0;
    while (start + modelStr.length <= mainStr.length) {
        //坏字符计算的需要滑动的距离
        int badDistance = this.badChar(mainStr, modelStr, start);
        //好后缀计算的需要滑动的距离
        int goodSuffixDistance = this.goodSuffix(mainStr, modelStr, start);
        System.out.println("badDistance = " +badDistance  + ", goodSuffixDistance = " + goodSuffixDistance);
        //任意一个匹配成功即成功(可以计算了坏字符和好后缀之后分别判断一下)
        //减少一次操作
        if (0 == badDistance || 0 == goodSuffixDistance) {
            System.out.println("匹配到的位置 :" + start);
            return start;
        }
        start += Math.max(badDistance, goodSuffixDistance);
        System.out.println("滑动至:" + start);
    }
    return -1;
}

public static void main(String[] args) {
    BoyerMoore moore = new BoyerMoore();
    char[] mainStr = new char[]{'a','b', 'c', 'a', 'g', 'f', 'a', 'c', 'j', 'k', 'a', 'c', 'k', 'e', 'a', 'c'};
    char[] modelStr = new char[]{'a', 'c', 'k', 'e', 'a', 'c'};
    System.out.println(moore.bmStrMatch(mainStr, modelStr));
}

3:完整实现

package com.cui.mystring;

import java.util.HashMap;
import java.util.Map;

/**
 * @author cui haida
 * 2025/1/20
 */
public class BoyerMoore {
    public static void main(String[] args) {
        String text = "here is a simple example";
        String pattern = "example";
        BoyerMoore bm = new BoyerMoore();
        bm.boyerMoore(pattern, text);
    }

    /**
     * 使用Boyer-Moore算法在文本中查找指定模式的出现位置
     * Boyer-Moore算法利用比较多次的字符跳跃来提高查找效率,特别适用于长文本的查找
     *
     * @param pattern 要查找的模式字符串
     * @param text    待查找的文本字符串
     */
    public void boyerMoore(String pattern, String text) {
        // 模式字符串的长度
        int m = pattern.length();
        // 文本字符串的长度
        int n = text.length();
        // bmBc表:用于存储每个字符在模式中的最后位置,以实现快速跳跃
        Map<String, Integer> bmBc = new HashMap<String, Integer>();
        // bmGs表:用于存储跳跃值,根据匹配情况决定跳跃距离
        int[] bmGs = new int[m];
        // 预处理阶段:构建bmBc和bmGs表
        preBmBc(pattern, m, bmBc);
        preBmGs(pattern, m, bmGs);
        // 搜索阶段:使用预处理好的表进行模式匹配
        int j = 0;
        int i = 0;
        // 计数器,用于统计字符比较次数
        int count = 0;
        // 遍历文本,直到没有足够的字符进行比较
        while (j <= n - m) {
            // 从模式的末尾开始向前匹配,直到找到不匹配的字符或匹配完整个模式
            for (i = m - 1; i >= 0 && pattern.charAt(i) == text.charAt(i + j); i--) { // 用于计数
                count++;
            }
            // 如果整个模式都匹配成功,则打印匹配位置并根据bmGs表移动到下一个位置
            if (i < 0) {
                System.out.println("one position is:" + j);
                j += bmGs[0];
            } else {
                // 如果模式匹配失败,根据bmGs表和bmBc表计算最大跳跃距离
                j += Math.max(bmGs[i], getBmBc(String.valueOf(text.charAt(i + j)), bmBc, m) - m + 1 + i);
            }
        }
        // 打印字符比较次数
        System.out.println("count:" + count);
    }


    /**
     * 预处理构建坏字符表
     * 该方法用于在执行Boyer-Moore算法前,根据模式串预计算每个字符在模式串中最后出现的位置
     * 这有助于在匹配过程中快速确定当字符不匹配时,模式串应该移动的位置
     *
     * @param pattern 模式串,即我们需要在文本中查找的字符串
     * @param patLength 模式串的长度,用于计算字符位置
     * @param bmBc 一个映射表,存储每个字符最后出现的位置,用于快速查找
     */
    private void preBmBc(String pattern, int patLength, Map<String, Integer> bmBc) {
        System.out.println("bmbc start process...");
        // 从模式串的倒数第二个字符开始向前遍历,最后一个字符不需要处理,因为它在匹配失败时不会导致模式串移动
        for (int i = patLength - 2; i >= 0; i--) {
            // 检查当前字符是否已经存在于映射表中
            // 如果当前字符不在映射表中,则将它添加到映射表中,并设置其值为从当前位置到模式串末尾的距离
            // 这个距离用于在匹配失败时确定模式串应该移动的位数
            if (!bmBc.containsKey(String.valueOf(pattern.charAt(i)))) {
                bmBc.put(String.valueOf(pattern.charAt(i)), (Integer) (patLength - i - 1));
            }
        }
    }


    /**
     * Boyer-Moore算法的预处理函数,用于构建bmGs(Good Suffix)表
     * 此函数计算在模式匹配中,当出现字符不匹配时,模式串应该跳过的位数
     * 通过分析模式串的后缀和前缀,来优化匹配过程,减少不必要的比较
     *
     * @param pattern   模式串,即需要在文本中查找的字符串
     * @param patLength 模式串的长度
     * @param bmGs      存储每个位置上好后缀的长度,以便在不匹配时确定移动的位数
     */
    private void preBmGs(String pattern, int patLength, int[] bmGs) {
        int i, j;
        int[] suffix = new int[patLength];
        suffix(pattern, patLength, suffix);

        // 模式串中没有子串匹配上好后缀,也找不到一个最大前缀
        for (i = 0; i < patLength; i++) {
            bmGs[i] = patLength;
        }

        // 模式串中没有子串匹配上好后缀,但找到一个最大前缀
        j = 0;
        for (i = patLength - 1; i >= 0; i--) {
            if (suffix[i] == i + 1) {
                for (; j < patLength - 1 - i; j++) {
                    if (bmGs[j] == patLength) {
                        bmGs[j] = patLength - 1 - i;
                    }
                }
            }
        }

        // 模式串中有子串匹配上好后缀
        for (i = 0; i < patLength - 1; i++) {
            bmGs[patLength - 1 - suffix[i]] = patLength - 1 - i;
        }

        // 打印bmGs表,便于调试和理解
        System.out.print("bmGs:");
        for (i = 0; i < patLength; i++) {
            System.out.print(bmGs[i] + ",");
        }
        System.out.println();
    }


    /**
     * 计算模式字符串的后缀长度数组
     * 该方法用于填充suffix数组,suffix[i]表示从模式字符串的第i个字符开始到末尾的子串,与模式字符串的末尾子串的最长公共部分的长度
     *
     * @param pattern 模式字符串,用于进行匹配
     * @param patLength 模式字符串的长度
     * @param suffix 存储后缀长度的数组,由方法填充
     */
    private void suffix(String pattern, int patLength, int[] suffix) {
        // 最后一个字符的后缀长度是模式字符串的长度
        suffix[patLength - 1] = patLength;
        // 初始化变量q,用于后续的比较
        int q = 0;
        // 从倒数第二个字符开始,向前遍历模式字符串
        for (int i = patLength - 2; i >= 0; i--) {
            // 初始化q为当前字符的索引
            q = i;
            // 在模式字符串中向前比较字符,直到比较失败或者q回到起始位置前
            while (q >= 0 && pattern.charAt(q) == pattern.charAt(patLength - 1 - i + q)) {
                q--;
            }
            // 计算当前位置的后缀长度并存储到suffix数组中
            suffix[i] = i - q;
        }
    }


    /**
     * 根据字符获取BM算法的坏字符表中的位移量
     * 如果字符在坏字符表中,则返回该字符对应的位移量;
     * 如果字符不在坏字符表中,则返回模式串的长度作为位移量
     *
     * @param c 单个字符,用于在坏字符表中查找
     * @param bmBc 坏字符表,映射字符到其在模式串中的位移量
     * @param m 模式串的长度,当字符不在坏字符表中时使用
     * @return 字符在坏字符表中对应的位移量或模式串的长度
     */
    private int getBmBc(String c, Map<String, Integer> bmBc, int m) {
        // 如果在规则中则返回相应的值,否则返回pattern的长度
        return bmBc.getOrDefault(c, m);
    }
}

三:马拉车算法

1:算法概述

Manacher算法:也叫 “马拉车”算法。

Manacher算法的应用范围要狭窄得多,但是它的思想和拓展kmp算法有很多共通支出,所以在这里介绍一下。Manacher算法是查找一个字符串的最长回文子串的线性算法。

什么是回文串:

所谓回文串,简单来说就是正着读和反着读都是一样的字符串,比如abba,noon等等,一个字符串的最长回文子串即为这个字符串的子串中,是回文串的最长的那个。

计算回文串的几种方式:其它几种方法请点击此链接

计算字符串的最长回文字串最简单的算法就是枚举该字符串的每一个子串,并且判断这个子串是否为回文串,这个算法的时间复杂度为O(n^3)的,显然无法令人满意,稍微优化的一个算法是枚举回文串的中点,这里要分为两种情况,一种是回文串长度是奇数的情况,另一种是回文串长度是偶数的情况,枚举中点再判断是否是回文串,这样能把算法的时间复杂度降为O(n2),但是当n比较大的时候仍然无法令人满意,Manacher算法可以在线性时间复杂度内求出一个字符串的最长回文字串,达到了理论上的下界。

2:Manacher算法原理与实现:

首先,Manacher算法提供了一种巧妙地办法,将长度为奇数的回文串和长度为偶数的回文串一起考虑,具体做法是,在原字符串的每个相邻两个字符中间插入一个分隔符,同时在首尾也要添加一个分隔符,分隔符的要求是不在原串中出现,一般情况下可以用#号。下面举一个例子
在这里插入图片描述

2.1:Len数组简介与性质

Manacher算法用一个辅助数组Len[i]表示以字符T[i]为中心的最长回文字串的最右字符到T[i]的长度,比如以T[i]为中心的最长回文字串是T[l,r],那么Len[i]=r-i+1。

对于上面的例子,可以得出Len[i]数组为:

在这里插入图片描述
Len数组有一个性质,那就是Len[i]-1就是该回文子串在原字符串S中的长度,至于证明,首先在转换得到的字符串T中,所有的回文字串的长度都为奇数,那么对于以T[i]为中心的最长回文字串,其长度就为2*Len[i]-1,经过观察可知,T中所有的回文子串,其中分隔符的数量一定比其他字符的数量多1,也就是有Len[i]个分隔符,剩下Len[i]-1个字符来自原字符串,所以该回文串在原字符串中的长度就为Len[i]-1。

有了这个性质,那么原问题就转化为求所有的Len[i]。下面介绍如何在线性时间复杂度内求出所有的Len。

2.2:Len数组的计算

首先从左往右依次计算Len[i],当计算Len[i]时,Lenj已经计算完毕。设P为之前计算中最长回文子串的右端点的最大值,并且设取得这个最大值的位置为po,分两种情况:

第一种情况:i<=P

那么找到i相对于po的对称位置,设为j,

那么如果Len[j],如下图:

在这里插入图片描述
那么说明以j为中心的回文串一定在以po为中心的回文串的内部,且j和i关于位置po对称,由回文串的定义可知,一个回文串反过来还是一个回文串,所以以i为中心的回文串的长度至少和以j为中心的回文串一样,即Len[i]>=Len[j]。因为Len[j]如果Len[j]>=P-i 由对称性,说明以i为中心的回文串可能会延伸到P之外,而大于P的部分我们还没有进行匹配,所以要从P+1位置开始一个一个进行匹配,直到发生失配,从而更新P和对应的po以及Len[i]。

在这里插入图片描述
第二种情况: i>P

如果i比P还要大,说明对于中点为i的回文串还一点都没有匹配,这个时候,就只能老老实实地一个一个匹配了,匹配完成后要更新P的位置和对应的po以及Len[i]。
在这里插入图片描述

3:算法实现

package com.cui.mystring;

public class ManacherDemo {
    public static void main(String[] args) {
        // 示例调用
        String result = manacher("abcbabcd");
        System.out.println("最长回文子串: " + result);
    }

    /**
     * 使用Manacher算法找到字符串中的最长回文子串
     * Manacher算法通过在每个字符间插入特殊字符(例如#),将问题转化为每个字符(包括插入的特殊字符)为中心的回文子串问题
     * 这种转换确保了回文子串的长度总是奇数,简化了算法的实现
     *
     * @param str 输入字符串
     * @return 最长回文子串
     */
    public static String manacher(String str) {
        if (str == null || str.length() == 0) {
            return "";
        }

        // 预处理字符串,使其适合Manacher算法的要求
        StringBuilder sb = new StringBuilder();
        sb.append("#");
        for (char c : str.toCharArray()) {
            sb.append(c).append("#");
        }
        String processedStr = sb.toString();

        int n = processedStr.length();
        int[] next = new int[n];
        int C = 0; // 中心位置
        int R = 0; // 回文右边界的最远位置

        for (int i = 0; i < n; i++) {
            int mirror = 2 * C - i; // 对称位置

            if (i < R) {
                next[i] = Math.min(R - i, next[mirror]);
            }

            // 尝试扩展回文半径
            while (i + next[i] < n && i - next[i] >= 0 && processedStr.charAt(i + next[i]) == processedStr.charAt(i - next[i])) {
                next[i]++;
            }

            // 更新中心位置和最远右边界
            if (i + next[i] > R) {
                C = i;
                R = i + next[i];
            }
        }

        // 找到最大回文半径及其对应的中心位置
        int maxLen = 0;
        int centerIndex = 0;
        for (int i = 0; i < n; i++) {
            if (next[i] > maxLen) {
                maxLen = next[i];
                centerIndex = i;
            }
        }

        // 计算原字符串中的最长回文子串
        int start = (centerIndex - maxLen) / 2;
        return str.substring(start, start + maxLen - 1);
    }
}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2280114.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

小红书用户作品列表 API 系列,返回值说明

item_search_shop_video-获得某书用户作品列表 公共参数 名称类型必须描述keyString是调用key&#xff08;必须以GET方式拼接在URL中&#xff09;secretString是调用密钥api_nameString是API接口名称&#xff08;包括在请求地址中&#xff09;[item_search,item_get,item_sea…

LeetCode hot 力扣热题100 排序链表

归并忘了 直接抄&#xff01; class Solution { // 定义一个 Solution 类&#xff0c;包含链表排序的相关方法。// 使用快慢指针找到链表的中间节点&#xff0c;并断开链表为两部分ListNode* middleNode(ListNode* head) { ListNode* slow head; // 慢指针 slow 初始化为链表…

JavaScript正则表达式解析:模式、方法与实战案例

目录 一、什么是正则表达式 1.创建正则表达式 2.标志&#xff08;Flags&#xff09; 3.基本模式 &#xff08;1&#xff09;字符匹配 &#xff08;2&#xff09;位置匹配 &#xff08;3&#xff09;数量匹配 二、常用的正则表达式方法和属性 1.test()‌ 2.match()‌ …

Nginx在Linux中的最小化安装方式

1. 安装依赖 需要安装的东西&#xff1a; wget​&#xff0c;方便我们下载Nginx的包。如果是在Windows下载&#xff0c;然后使用SFTP上传到服务器中&#xff0c;那么可以不安装这个软件包。gcc g​&#xff0c;Nginx是使用C/C开发的服务器&#xff0c;等一下安装会用到其中的…

【大模型】ChatGPT 高效处理图片技巧使用详解

目录 一、前言 二、ChatGPT 4 图片处理介绍 2.1 ChatGPT 4 图片处理概述 2.1.1 图像识别与分类 2.1.2 图像搜索 2.1.3 图像生成 2.1.4 多模态理解 2.1.5 细粒度图像识别 2.1.6 生成式图像任务处理 2.1.7 图像与文本互动 2.2 ChatGPT 4 图片处理应用场景 三、文生图操…

基于python+Django+mysql鲜花水果销售商城网站系统设计与实现

博主介绍&#xff1a;黄菊华老师《Vue.js入门与商城开发实战》《微信小程序商城开发》图书作者&#xff0c;CSDN博客专家&#xff0c;在线教育专家&#xff0c;CSDN钻石讲师&#xff1b;专注大学生毕业设计教育、辅导。 所有项目都配有从入门到精通的基础知识视频课程&#xff…

-bash: /java: cannot execute binary file

在linux安装jdk报错 -bash: /java: cannot execute binary file 原因是jdk安装包和linux的不一致 程序员的面试宝典&#xff0c;一个免费的刷题平台

免费为企业IT规划WSUS:Windows Server 更新服务 (WSUS) 之快速入门教程(一)

哈喽大家好&#xff0c;欢迎来到虚拟化时代君&#xff08;XNHCYL&#xff09;&#xff0c;收不到通知请将我点击星标&#xff01;“ 大家好&#xff0c;我是虚拟化时代君&#xff0c;一位潜心于互联网的技术宅男。这里每天为你分享各种你感兴趣的技术、教程、软件、资源、福利…

面试--你的数据库中密码是如何存储的?

文章目录 三种分类使用 MD5 加密存储加盐存储Base64 编码:常见的对称加密算法常见的非对称加密算法https 传输加密 在开发中需要存储用户的密码&#xff0c;这个密码一定是加密存储的&#xff0c;如果是明文存储那么如果数据库被攻击了&#xff0c;密码就泄露了。 我们要对数据…

模型部署工具01:Docker || 用Docker打包模型 Build Once Run Anywhere

Docker 是一个开源的容器化平台&#xff0c;可以让开发者和运维人员轻松构建、发布和运行应用程序。Docker 的核心概念是通过容器技术隔离应用及其依赖项&#xff0c;使得软件在不同的环境中运行时具有一致性。无论是开发环境、测试环境&#xff0c;还是生产环境&#xff0c;Do…

Restormer: Efficient Transformer for High-Resolution Image Restoration解读

论文地址&#xff1a;Restormer: Efficient Transformer for High-Resolution Image Restoration。 摘要 由于卷积神经网络&#xff08;CNN&#xff09;在从大规模数据中学习可推广的图像先验方面表现出色&#xff0c;这些模型已被广泛应用于图像复原及相关任务。近年来&…

四、CSS效果

一、box-shadow box-shadow:在元素的框架上添加阴影效果 /* x 偏移量 | y 偏移量 | 阴影颜色 */ box-shadow: 60px -16px teal; /* x 偏移量 | y 偏移量 | 阴影模糊半径 | 阴影颜色 */ box-shadow: 10px 5px 5px black; /* x 偏移量 | y 偏移量 | 阴影模糊半径 | 阴影扩散半…

火狐浏览器Firefox一些配置

没想到还会开这个…都是Ubuntu的错 一些个人习惯吧 标签页设置 常规-标签页 1.按最近使用顺序切换标签页 2.打开新标签而非新窗口&#xff08;讨厌好多窗口&#xff09; 3.打开新链接不直接切换过去&#xff08;很打断思路诶&#xff09; 4.关闭多个标签页时不向我确认 启动…

MECD+: 视频推理中事件级因果图推理--VLM长视频因果推理

论文链接&#xff1a;https://arxiv.org/pdf/2501.07227v1 1. 摘要及主要贡献点 摘要&#xff1a; 视频因果推理旨在从因果角度对视频内容进行高层次的理解。然而&#xff0c;目前的研究存在局限性&#xff0c;主要表现为以问答范式执行&#xff0c;关注包含孤立事件和基本因…

Python基于Django的社区爱心养老管理系统设计与实现【附源码】

博主介绍&#xff1a;✌Java老徐、7年大厂程序员经历。全网粉丝12w、csdn博客专家、掘金/华为云/阿里云/InfoQ等平台优质作者、专注于Java技术领域和毕业项目实战✌ &#x1f345;文末获取源码联系&#x1f345; &#x1f447;&#x1f3fb; 精彩专栏推荐订阅&#x1f447;&…

Docker 单机快速部署大数据各组件

文章目录 一、Spark1.1 NetWork 网络1.2 安装 Java81.3 安装 Python 环境1.4 Spark 安装部署 二、Kafka三、StarRocks四、Redis五、Rabbitmq六、Emqx6.1 前言6.2 安装部署 七、Flink八、Nacos九、Nginx 一、Spark 1.1 NetWork 网络 docker network lsdocker network create -…

【MySQL】:Linux 环境下 MySQL 使用全攻略

&#x1f4c3;个人主页&#xff1a;island1314 &#x1f525;个人专栏&#xff1a;MySQL学习 ⛺️ 欢迎关注&#xff1a;&#x1f44d;点赞 &#x1f442;&#x1f3fd;留言 &#x1f60d;收藏 &#x1f49e; &#x1f49e; &#x1f49e; 1. 背景 &#x1f680; 世界上主…

【思科】NAT配置

网络拓扑图 这个网络拓扑的核心是Router1&#xff0c;它通过配置多个VLAN子接口来实现对不同VLAN的支持&#xff0c;并通过NAT进行地址转换&#xff0c;使得内部网络能够与外部网络进行通信。Router1上配置了FastEthernet0/0.x接口&#xff0c;并启用了802.1Q封装&#xff0c;…

WGAN - 瓦萨斯坦生成对抗网络

1. 背景与问题 生成对抗网络&#xff08;Generative Adversarial Networks, GANs&#xff09;是由Ian Goodfellow等人于2014年提出的一种深度学习模型。它包括两个主要部分&#xff1a;生成器&#xff08;Generator&#xff09;和判别器&#xff08;Discriminator&#xff09;…

【数学建模美赛速成系列】O奖论文绘图复现代码

文章目录 引言折线图 带误差棒得折线图单个带误差棒得折线图立体饼图完整复现代码 引言 美赛的绘图是非常重要得&#xff0c;这篇文章给大家分享我自己复现2024年美赛O奖优秀论文得代码&#xff0c;基于Matalab来实现&#xff0c;可以直接运行出图。 折线图 % MATLAB 官方整理…