25

聊聊算法——回文字符串

 3 years ago
source link: http://www.cnblogs.com/xxbiao/p/13396157.html
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

我这天天写核心业务的人,都没用什么算法啊!其实算法无处不在,栈队列树链表都包含算法思想,算法并不是单纯指用代码解决那些深奥难懂的数学逻辑问题,而是代码中的普适化思维。并且算法也不可怕,是基本功,就像足球中的体能训练,微软谷歌,想不想去?他们都是用算法来伺候上门人的,所以还是别太片面地看待问题。算法也是非常讲究的,稍有破绽,失之千里。熟悉各种框架各种源码,不如下笔写算法!因为框架是「用」,算法是「想」。今天来分析下回文字符串的解法。

「准备」

Idea2019.03/Gradle6.0.1/Maven3.6.3/JDK11.0.4

「难度」新手-- 战士 --老兵--大师

「目标」

1.查找字符串的最长回文子串算法

1 需求

找回文是很常见的一种场景,所以拿来做个典型。所谓回文,即左右对称的字符串,如“ABCBA”,它有三种解法,我这里只说两种:「中心扩展法」和「动态规划」,还有个Manacher 算法,此文略!

2 中心扩展法

「思路:既然回文是对称的,肯定有个中心,如从中心开始向两个方向同步扩展,直到遇到不同字符,即为最长回文子串。」

代码(Java版):

public class N005 {
    public static void main(String[] args) {
        String s = "DECCED";
        System.out.println(sub(s));
    }
    /** 中心扩展 因扩展有两种可能 一是ABA型 二是ABBA型,故可以写一个公用的扩展函数expand()*/
    private static String sub(String s){
        int start = 0, end = 0;
        for (int i = 0; i < s.length(); i++) {
            // ABBA型
            int len1 = expand(s,i,i + 1);
            // ABA型
            int len2 = expand(s,i,i );
            int len = Math.max(len1,len2);
            // 只保留最大的子串
            if (len > end - start) {
                start = i - (len - 1) / 2;
                end = i + len / 2;
            }
        }
        return s.substring(start,end + 1);
    }

    private static int expand(String s,int left,int right){
        int l = left;
        int r = right;
        while (l >= 0 && r < s.length() && s.charAt(l)==s.charAt(r)){
            l--;
            r++;
        }
        return r - l - 1;
    }
}

以上代码解析:

  1. 回文对称有两种,一是”ABA”型,另一种是”ABBA”型,for循环中将字符串每个字符都假设为对称中心,使用两种类型的假设来左右扩展,并保存最大回文信息;
  2. 为啥独立出来一个expand函数?因为对称有两种类型,写个函数来复用,代码更简洁;
  3. expand函数的作用是获得回文的长度,比如”ABA”返回3,”ABBA”则返回4 ,两个下标差再减去 1 ,要注意的细节是这两个指针的最终位置,如下图,”ABA”型(上),”ABBA”型如 (上):

ec01e9da-13ae-4616-aeff-0db49305be23.pngee3f028e-8c54-4885-a8cf-ba65a24facf0.png

  1. 关于代码中start和end 变量的计算:
  • start=i-(len-1)/2,考虑上面两种类型的情况,按上面图例来说,i=2,start结果为 0 和 -0.5,但 (int) 0 和 (int) -0.5 都等于 0 ;
  • end=i+len/2,同理,按上面图例来说,i=2,end结果为 4.5 和 5,(int) 4.5 和 (int) 5 等于 4 和 5 ;

使用这样的写法,虽然有点绕,主要是综合了两种类型的可能结果,分开写也可有其他形式。

  1. 最后的细节就是subString方法,“ABCDE”.subString(0,5) 输出才是“ABCDE”。

3 动态规划法

我在前篇文章已经专门聊了「动态规划」,虽然动态规划严格讲是用于解决阶段决策问题的,但其核心思想(类似数学归纳法)也可用于其他场景,使用的就是状态转移方程。动态规划一定会使用dpTable来记录中间结果。

「思路:如果一个字符串是回文串,那么去掉首尾字符的子串也肯定是回文串,反过来想,如果子串 sub[i+1,j-1] 是回文串,只需要看 i 和 j 位置的字符是否相同,即可判断sub[i,j] 是否属于回文串。」

因为有 i 和 j ,我们使用一个二维数组做dpTable,如果定义dp[i][j]为位置 i 到 j的子串(包含首尾字符)是否为回文串,按照思路即可写出状态转移方程(Λ意为and):

dp[i][j] = dp[i+1][j-1] Λ (char[i] == char[j])

那么这个dpTable 的对角线都是 true,因为 i = j 时,只有单个字符,同时,因为是从i 到 j 的子串,可以肯定 i < j,故只需计算dpTable右上部分:

38a17bbd-e4e9-4f43-85ee-9cac86432349.png

于是可以先写一个初始版本(Java):

private String sub2(String s){
    int len = s.length();
    boolean[][] dp = new boolean[len][len];
    // 初始化dp表的部分值
    for (int i = 0; i < len; i++) {
        dp[i][i] = true;
    }
    //
    for (int j = 1; j < len; j++) {
        for (int i = 0; i < j; i++) {
            if (s.charAt(i) != s.charAt(j)){
                dp[i][j] = false;
            }else {
                dp[i][j] = dp[i + 1][j - 1];
            }
        }
    }
    // 记录回文串的开始位置和长度
    int start = 0;
    int length = 0;
    return s.substring(start,length);
}

再考虑边界情况,在char[i] == char[j] 时:

即子串subStr(i+1,j-1)长度无法构成子串,长度小于2,(j-1)-(i-1)+1<2,简化为 j-i<3 ,等价于subStr(i,j)长度为 2 或 3:

  • 如果子串subStr(i+1,j-1)是空,那么subStr(i,j)是回文串;
  • 如果子串subStr(i+1,j-1)只有一个字符,显然一个字符是回文串,那么subStr(i,j)是回文串;

补充边界后 :

private static String sub2(String s){
    int len = s.length();
    boolean[][] dp = new boolean[len][len];
    // 记录回文串的开始位置和长度
    int start = 0;
    int length = 0;
    // 初始化dp表的部分值
    for (int i = 0; i < len; i++) {
        dp[i][i] = true;
    }
    //
    for (int j = 1; j < len; j++) {
        for (int i = 0; i < j; i++) {
            if (s.charAt(i) != s.charAt(j)){
                dp[i][j] = false;
            }else {
                if (j - i < 3){
                    dp[i][j] = true;
                }else {
                    dp[i][j] = dp[i + 1][j - 1];
                }
            }
            // 记录最长回文信息
            if (dp[i][j] && j-i+1 > length){
                start = i;
                length = j - i + 1;
            }
        }
    }
    return s.substring(start,length);
}

以上代码分析:1 双重for循环,计算出dpTable中的值;2 最长回文信息只需记录起点和长度即可,当然也可记录起止位置,效果一样; 3 s.charAt(i) != s.charAt(j) 且 subStr(i,j)长度大于3 时,才会使用状态转移方程!

至此,寻找回文字符串算法你学会了吗?

「全文完!」

我近期其他文章:

只写原创,敬请关注

QzmaQ3e.png!mobile


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK