【专题归纳】字符串之回文

版权声明:本文为博主原创文章,转载请注明出处: https://blog.csdn.net/qq_38182125/article/details/89703325

前言

在春招或者秋招的时候,大厂的笔试题对于字符串的考察算是格外的情有独钟,而字符串问题中最为特别的就要数回文了。本文主要记录了笔者在笔试以及平时刷题中遇到的各种回文相关的题目,一方面方便自己后续复习,另一方面也希望能帮助到看这篇文章的各位。本文收录的问题如下:

  • 【华为OJ】字符串运用-密码截取

收集的问题会根据笔者遇到的题目进行不定期的补充。


1.【华为OJ】字符串运用-密码截取

题目描述

Catcher是MCA国的情报员,他工作时发现敌国会用一些对称的密码进行通信,比如像这些ABBA,ABA,A,123321,但是他们有时会在开始或结束时加入一些无关的字符以防止别国破解。比如进行下列变化 ABBA->12ABBA,ABA->ABAKK,123321->51233214。因为截获的串太长了,而且存在多种可能的情况(abaaab可看作是aba,或baaab的加密形式),Cathcer的工作量实在是太大了,他只能向电脑高手求助,你能帮Catcher找出最长的有效密码串吗?

输入:

ABBA

输出:

4

问题分析:

这道题说了这么多,本质上就是求字符串中最长回文子串的长度。而求最长回文子串的方法基本上可归纳为3种:

  • 暴力求解,穷举该字符串的每一种子串,然后找出最长的回文子串。时间复杂度为   o ( n 3 ) \ o(n^{3})
  • 将字符串中的每个字符和字符之间的空隙当作回文的中心,然后从中心向两边展开寻找回文子串,通过比较得出最长的回文子串。时间复杂度为   o ( n 2 ) \ o(n^{2})
  • 专门用于解决该问题的 Manacher’s Algorithm。时间复杂度为   o ( n ) \ o(n)

接下来我们就分别对上面三种方式的Java代码进行实现:

1).暴力求解法:

public class Main {

    public static void main(String[] args) {
        String s = "acecaace";
        System.out.println(findLongestPalindrome(s));
    }

    private static boolean isPalindrome(String s){
        int len = s.length();
        for (int i = 0; i < len; i++){
            if (s.charAt(i) != s.charAt(len-1-i)){
                return false;
            }
        }
        return true;
    }

    public static int findLongestPalindrome(String s) {
        if (s == null || s.length() == 0){
            return 0;
        }
        int max = 0;
        for (int i = 0; i < s.length(); i++){
            for (int j = i+1; j <= s.length(); j++){
                String subString = s.substring(i, j);
                if (isPalindrome(subString) && max < subString.length()){
                    max = subString.length();
                }
            }
        }
        return max;
    }
}

这种就是暴力穷举法了,过程没什么好说的,非常简单,复杂度的计算是这么来的:穷举出所有的子串在两个 for 循环中,复杂度为   o ( n 2 ) \ o(n^{2}) ,紧接着在穷举所有子串之后还要判断子串是否为回文,判断的过程复杂度为   o ( n ) \ o(n) ,所以相叠加起来就为   o ( n 3 ) \ o(n^{3}) 。但是在面试中写出这种复杂度的代码意味着你可以回去等通知了,所以千万不要写出这种代码来!

2).中心扩展法:

这里有必要说明一下中心扩展的检测方式,例如字符串为 abcvovovo,步骤如下所示:

在这里插入图片描述 在这里插入图片描述在这里插入图片描述

从上图可以看出,先以a为中心扩展,再以a、b之间的空隙为中心扩展,紧接着再以b为中心扩展,直到遍历完最后一个字符。那么我们应当如何以空隙为空心扩展呢?笔者在这里使用的是填充字符的方式,填充完之后的字符串如下所示:
在这里插入图片描述
在这里我们将字符间的空隙用字符’#'进行填充,巧妙地解决了该问题,这种方式的代码如下所示:

public class Main {

    public static void main(String[] args) {
        String s = "abcvovovo";
        System.out.println(findLongestPalindrome(s));
    }
	
	// 预处理字符串的方法,用于填充#
    private static String preHandleString(String s){
        StringBuilder sb = new StringBuilder("#");
        for (int i = 0; i < s.length(); i++){
            sb.append(s.charAt(i)).append('#');
        }
        return sb.toString();
    }

    public static int findLongestPalindrome(String s) {
        if (s == null || s.length() == 0){
            return 0;
        }
        String str = preHandleString(s);
        int max = 1;
        for (int i = 0; i < str.length(); i++){
            int len = 0;
            int j = i-1, k = i+1;
            // 寻找回文子串
            while (j >= 0 && k < str.length()){
                if (str.charAt(j--) != str.charAt(k++)){
                    break;
                }
                // 回文长度加1(不包含 i 字符)
                ++len;
            }
            if (max < len){
                max = len;
            }
        }
        return max;
    }
}

通过这种方式,我们就能在复杂度为   o ( n 2 ) \ o(n^{2}) 的条件下(for循环和while循环叠加)完成对最长回文子串长度的计算。这种方法在面试的时候写出来不算差,但是也不突出,因为还有更加好的方法来解决此类问题。

3).Manacher’s Algorithm:

该算法名称叫马拉车算法,专门用于解决此类求字符串中最长回文子串的问题,算法本身优化的地方在于充分利用了已知回文字符串所提供的信息来达到减少遍历的效果。这个算法的讲解推荐【面试现场】如何找到字符串中的最长回文子串?对于马拉车算法讲解的算是非常通透的了。接下来就是运用该算法实现的Java代码:

public class Main {

    public static void main(String[] args) {
        String s = "abcvovovo";
        System.out.println(findLongestPalindrome(s));
    }

    private static String preHandleString(String s){
        StringBuilder sb = new StringBuilder("#");
        for (int i = 0; i < s.length(); i++){
            sb.append(s.charAt(i)).append('#');
        }
        return sb.toString();
    }

    public static int findLongestPalindrome(String s) {
        if (s == null || s.length() == 0){
            return 0;
        }
        String str = preHandleString(s);
        int rightSide = 0;
        int rightCenter = 0;
        int[] lens = new int[str.length()];
        int max = 1;

        for (int i = 0; i < str.length(); i++){
            boolean needExpand = true;
            // 如果i点位于rightSide内
            if (i < rightSide){
                // 找出以rightCenter为中心,和i对称的左对称点
                int leftPoint = 2 * rightCenter - i;
                // 直接利用回文的特性得出回文个数
                lens[i] = lens[leftPoint];
                if (i + lens[i] > rightSide){
                    lens[i] = rightSide - i;
                }
                // 根据已知条件计算得出的最长回文小于右边界,则不需要扩展了
                if (i + lens[leftPoint] < rightSide){
                    needExpand = false;
                }
            }
            // 需要进行中心扩展
            if (needExpand){
                while (i - 1 - lens[i] >= 0 && i + 1 + lens[i] < str.length()){
                    if (str.charAt(i - 1 - lens[i]) != str.charAt(i + 1 + lens[i])){
                        break;
                    }
                    ++lens[i];
                }
                rightCenter = i;
                rightSide = i + lens[i];
                if (max < lens[i]){
                    max = lens[i];
                }
            }
        }

        return max;
    }
}

尽管代码里面有两层循环,由于内层的循环只对尚未匹配的部分进行,因此对于每一个字符而言,只会进行一次,所以时间复杂度为   o ( n ) \ o(n)


希望这篇文章对你有所帮助~

猜你喜欢

转载自blog.csdn.net/qq_38182125/article/details/89703325