April01xxx

导航

字符串问题之最长真前缀,真后缀

1.题目

  好久没有写东西了,懈怠了.越来越觉得能坚持做一件时间,真的需要极大的毅力.这次是想把以前理解了的东西
形成文字记录下来.之前在LeetCode上刷题的时候,有些涉及到KMP算法的,当时查了写些料算是吃透了,但没记下
来,这次又遇到一个类似的问题,在想解法的时候,发现需要用到真前缀,真后缀的思路,又花了好久回忆怎么计算这
些东西,这次把自己的思路纪录在此.

Given a string s, you are allowed to convert it to a palindrome by adding
characters in front of it. Find and return the shortest palindrome you can
find by performing this transformation.
Example 1:
Input: "aacecaaa"
Output: "aaacecaaa"
Example 2:
Input: "abcd"
Output: "dcbabcd"

2.思路

  最直观的做法,先判断原字符串s是否回文串,若是则直接返回,若不是,则在原字符串之前添加一个与最后字符
相同的字符形成一个新的字符串,去除该字符串的首尾两个字符后,判断剩余字符串是否回文串.重复这一过程直到
整个串都是回文串.直接上代码:

bool
isPalindrome(char *s) {
  int i, j;

  i = 0, j = strlen(s) - 1;
  while (i < j) {
    if (s[i++] != s[j--])
      return false;
  }

  return true;
}

char *
shortestPalindrome(char *s) {
  char *ans;
  int len, ch, i;

  if (isPalindrome(s))
    return s;

  len = strlen(s);
  ans = malloc(2 * len * sizeof(char));
  for (i = 1; i < len; ++i) {
    ch = s[len - i];
    ans[i - 1] = ch;
    s[len - i] = 0;
    if (isPalindrome(s)) {
      s[len - i] = ch;
      break;
    }
    s[len - i] = ch;
  }
  strcpy(ans + i, s);

  return ans;
}

  判断字符串是否回文串的时间复杂度是O(N),外层循环O(N),整个算法的时间复杂度是O(N^2).不出意外地
在LeetCode上TLE了.

3.优化

  很容易发现以上解法存在大量重复的比较.另外我们可以一次性在原字符串前添加多个字符,而不用一次一个的
添加,但题目要求最后求得的回文串长度最短,所以添加时需要注意一些东西.以原字符串"babbbabbaba"为例具
体分析如下:
0 1 2 3 4 5 6 7 8 9 0
b a b b b a b b a b a

  1. 用两个指针i,j分别指向字符串的首和尾,比较s[i]是否等于s[j],此时直接在原字符串前添加一个字符'a'
    即可,i不变,j=j-1.
  2. 当i=4,j=5时,s[i]!=s[j],说明字符串不是回文串,题目最后要求的是最短回文串,所以我们知道要求的
    字符串的前面一段肯定是"ababba",那是不是把这一段直接添加在现有字符串前面就可以了呢?显然不是的,因
    为题目要求最后得到的回文串是最短的.那如何构造最短的呢?注意到第一步中我们已经在原字符串前面添加了字
    符'a',所以这一次只需要添加"babba"即可,又发现原字符串的前两个字符就是"ba",所以我们实际只需要再
    次添加"bab"即可,由于重用了两个字符,故i=2,j=j-1=4.
        0 1 2 3 4 5 6 7 8 9 0
    a b a b b a b b b a b b a b a
  3. 从示意图上可知,子串s[2:4]是回文串,所以最后要求的字符串是"ababbabbbabbaba".

上述第二步的分析过程实际就是求解字符串"babba"的最长真前后缀的过程.更进一步来说,对于字符串
"babbbabbaba",将其反转后的字符串"ababbabbbab"添加到原字符串前面所形成的字符串一定是
回文串,但这个回文串不是最短的,需要去掉其中的公共部分.

令原字符串为s,反转后的字符串为r,要找到一个最长的子串t,使得t即是r的后缀,也是s的前缀.
r: ababbabbbab
s: babbbabbaba
求解t的过程可以采用逐个字符比较的方法,也可以采用类似KMP算法中用到的最长真前缀后缀的方法.
首先计算s字符串的next数组:
s:b a b b b a b b a b a
n:-1 0 -1 1 1 0 -1 1 4 -1 3
将s与r进行模式匹配,即可确定t的最大长度.直接上代码了,细节的解释可以看代码注释.

void
getNext(char *s, int *next, int n) {
  int i, j, len;  /* i表示字符在s中的下标,j表示next中的下标. */

  len = strlen(s);

  j = 0;
  next[j] = -1;
  /**
   * next[j]表示前面j-1个字符组成的字符串中既是真前缀又是真后缀的子串的长度.
   */
  i = -1;
  while (j < len - 1) {
    if (i == -1 || s[i] == s[j]) {
      /**
       * 这里一般的做法是令next[j++] = ++i,但还有可以优化的空间.
       * 考虑字符串"ABCDABDE",若按照上述做法,得到的next数组如下:
       *  0  1  2  3  4  5  6  7
       *  A  B  C  D  A  B  D  E
       * -1  0  0  0  0  1  2  0
       * 若当i=4时出现不匹配,此时根据next[i]的值,应该从第0个字符
       * 开始重新匹配,但是s[0]明显肯定也不会匹配,故还可以优化.
       * 若s[++i] == s[++j],则next[j] = next[i];
       */
      if (s[++i] == s[++j])
        next[j] = next[i];
      else
        next[j] = i;
    } else {
      /**
       * 如果s[i]!=s[j],说明最长子串的长度小于next[i],应该往前找,一种直观
       * 的做法是逐个字符往前搜索,即i=i-1.但大可不必,因为next[i]记录的就是
       * 前面i-1个字符组成的字符串中既是真前缀又是真后缀的子串的长度.故直接从
       * i=next[i]处匹配s[j]即可.
       */
      i = next[i];
    }
  }
}

char *
shortestPalindrome(char *s) {
  char *ans;
  int i, j, len, *next;

  len = strlen(s);
  ans = malloc(2 * len * sizeof(char));
  next = malloc(len * sizeof(int));

  /* 计算s的next数组. */
  getNext(s, next, len);

  /* 将s反转后保存到ans中. */
  ans[len] = 0;
  for (i = 0, j = len - 1; j >= 0; --j, ++i)
    ans[i] = s[j];

  /* 查找既是ans的后缀,又是s的前缀的最长子串. */
  i = j = 0;
  while (i < len) {
    if (j == -1 || ans[i] == s[j]) {
      ++i;
      ++j;
    } else {
      j = next[j];
    }
  }

  /* 循环结束后i指向ans的末尾,j指向t的末尾. */
  strcpy(ans + i - j, s);

  return ans;
}

posted on 2018-10-28 15:47  April01xxx  阅读(979)  评论(0编辑  收藏  举报