最長迴文子串(Go,LeetCode)

目錄

題目描述

輸入/輸出示例

解決方案

暴力法

中心擴散法

Manacher算法

代碼

暴力法

中心擴散法

Manacher算法

代碼走讀

暴力法

中心擴散法

Manacher算法

傳送門


 

題目描述

給定一個字符串 s,找到 s 中最長的迴文子串。你可以假設 s 的最大長度爲 1000。

 

輸入/輸出示例

輸入 babad
輸出 bab
解釋 輸入的字符串中最長的迴文子串爲bab。注意aba也是一個正確答案。

 

解決方案

解決的方法大概分爲以下三種:

暴力法

很明顯,暴力法將選出所有子字符串可能的開始和結束位置,並檢驗它是不是迴文。因爲要循環遍歷每一個元素作爲子串的開頭,對每一個元素要尋找結尾,且對每一次可能的結尾要判斷子串是否迴文,因此暴力法的時間複雜度爲:O(n^{3}),空間複雜度爲O(1)

 

中心擴散法

暴力法雖然邏輯簡單清晰,但是會帶來太多的重複計算。我們通過觀察發現,單個字符肯定是迴文字符串。如果單個字符的左右兩邊擁有相鄰的字符,且左右兩邊相鄰的字符相等,那麼這三個字符所組成的字符串一定就是迴文字符串。按照這個邏輯依此類推,就可以找到中心字符串不斷向兩邊擴散的最長迴文串。我們通過循環可以將每個字符視爲一個“中心字符”,對每一箇中心字符向兩邊擴散,找到最長的記錄,就是我們要的結果。

但是會有一種情況在上面描述的邏輯中沒有考慮到:如果是長度爲偶數的迴文字符串,例如abba,使用上面以單個字符作爲中心向兩邊擴散的邏輯無法找到最終結果“abba”,而是會得到一個錯誤的結果“a”。因此我們還要考慮對長度爲偶數字符串的場景:我們選取兩個相鄰的字符串作爲“中心”。如果這兩個字符相等,那麼就依次向兩邊擴散,如果每次擴散的最外層左右兩邊字符相等,那麼這些字符所構成的字符串就是我們要找的迴文字符串。依次遍歷完全部字符串後找到的長度最大的迴文子串就是我們想要的結果。

綜上所述,要找到一個字符串中的最大回文子串,我們不僅要以單個字符向兩邊擴散求解,還要對兩個相同且相鄰的字符向兩邊擴散求解。最終對兩個方法求解的結果進行比較,找到長度最大的字符串,就是我們想要的結果,即最長迴文子串。

複雜度分析:時間複雜度爲O(n^2)。空間複雜度爲 O(1)

 

Manacher算法

Manacher算法是對中心擴散法的一種優化。因爲中心擴散法要對子串長度的奇偶分別進行計算,消耗了大量的時間。如果我們對奇偶轉換成統一的模式進行計算,就會減少大量的時間消耗。因此Manacher算法的核心是,將原字符串(無論奇偶)構造成一個長度完全是奇數的字符串:構造後無論是長度,還是內部字符和字符之間的距離都變成了奇數。這種構造新字符串的方式就是將原字符串的首尾和每個字符之間都插入一個特殊的標誌字符。例如假設原字符串爲“abba”,通過插入特殊字符的方式將字符串構造爲“#a#b#b#a”。需要注意的是,該特殊字符不能存在於原始字符串中。我們對構造後的字符串利用中心擴散法的思想求出最長迴文子串。Manacher的時間複雜度爲O(n)。 

 

代碼

暴力法

package main

import "fmt"

func longestPalindrome(s string) string {
	if len(s) < 2 {
		return s
	}

	maxSubPalindrome := s[:1]
	for i := 0; i < len(s)-1; i++ {
		for j := i + 1; j < len(s); j++ {
			if j-i+1 > len(maxSubPalindrome) && isPalindrome(s[i:j+1]) {
				maxSubPalindrome = s[i : j+1]
			}
		}
	}

	return maxSubPalindrome
}

func isPalindrome(subStr string) bool {
	for i := 0; i < len(subStr)/2; i++ {
		if subStr[len(subStr)-i-1] != subStr[i] {
			return false
		}
	}

	return true
}

 

中心擴散法

package main

import "fmt"

func longestPalindrome(s string) string {
	if len(s) < 1 {
		return s
	}

	maxSubPalindrome := s[:1]
	for i := 0; i < len(s); i++ {
		evenPalindrome := GetPalindrome(s, i, i)
		oddPalindrome := GetPalindrome(s, i, i+1)
		maxSubPalindrome = MaxString(
			evenPalindrome,
			oddPalindrome,
			maxSubPalindrome,
		)
	}
	return maxSubPalindrome
}

func GetPalindrome(s string, left, right int) string {
	for {
		if !(left >= 0 && right < len(s) && s[left] == s[right]) {
			break
		}

		left--
		right++
	}

	return s[left+1 : right]
}

func MaxString(strList ...string) string {
	max := ""
	for _, str := range strList {
		if len(str) > len(max) {
			max = str
		}
	}
	return max
}

 

Manacher算法

package main

import (
	"fmt"
	"strings"
)

func longestPalindrome(s string) string {
	if len(s) < 2 {
		return s
	}

	return Manacher(s)
}

func Manacher(s string) string {
	manacherStr := "#" + strings.Join(strings.Split(s, ""), "#") + "#"
	maxLength := 0
	start := 0
	for i := 0; i < len(manacherStr); i++ {
		left := i - 1
		right := i + 1
		step := 0

		for {
			if !(left >= 0 && right < len(manacherStr) &&
				manacherStr[left] == manacherStr[right]) {
				break
			}
			left--
			right++
			step++
		}

		if step > maxLength {
			maxLength = step
			start = (i - maxLength) / 2
		}
	}
	return s[start : start+maxLength]
}

 

代碼走讀

暴力法

package main

import "fmt"

func longestPalindrome(s string) string {
   // 空字符串或長度爲1的字符串肯定是迴文字符串,直接返回即可
   if len(s) < 2 {
      return s
   }

   // 初始化最長迴文子串:取首字符構成的子串作爲最長迴文子串
   maxSubPalindrome := s[:1]

   // 設置兩個遊標變量,i從頭到尾-1依次遍歷,將每個字符作爲子串的首字符下標,
   // j作爲子串的尾字符下標,依次檢查s[i:j+1]是否爲最長迴文字符串
   for i := 0; i < len(s)-1; i++ {
      for j := i + 1; j < len(s); j++ {
         if j-i+1 > len(maxSubPalindrome) && isPalindrome(s[i:j+1]) {
            maxSubPalindrome = s[i : j+1]
         }
      }
   }

   return maxSubPalindrome
}

// 判斷某個子串是否是迴文字符串
func isPalindrome(subStr string) bool {
   for i := 0; i < len(subStr)/2; i++ {
      if subStr[len(subStr)-i-1] != subStr[i] {
         return false
      }
   }

   return true
}

// 自測用例
func main() {
   fmt.Println(longestPalindrome("a"))
}

 

中心擴散法

package main

import "fmt"

func longestPalindrome(s string) string {
   if len(s) < 1 {
      return s
   }

   // 分別以奇數子串和偶數子串爲中心向兩邊擴散,找出最長子串
   maxSubPalindrome := s[:1]
   for i := 0; i < len(s); i++ {
      evenPalindrome := GetPalindrome(s, i, i)
      oddPalindrome := GetPalindrome(s, i, i+1)
      maxSubPalindrome = MaxString(
         evenPalindrome,
         oddPalindrome,
         maxSubPalindrome,
      )
   }
   return maxSubPalindrome
}

// 以s[left:right+1]向兩邊擴散,找出最長迴文子串
func GetPalindrome(s string, left, right int) string {
   for {
      if !(left >= 0 && right < len(s) && s[left] == s[right]) {
         break
      }

      left--
      right++
   }

   return s[left+1 : right]
}

// 找出最長字符串
func MaxString(strList ...string) string {
   max := ""
   for _, str := range strList {
      if len(str) > len(max) {
         max = str
      }
   }
   return max
}

// 自測用例
func main() {
   fmt.Println(longestPalindrome("babaddab"))
}

 

Manacher算法

package main

import (
   "fmt"
   "strings"
)

func longestPalindrome(s string) string {
   if len(s) < 2 {
      return s
   }

   return Manacher(s)
}

func Manacher(s string) string {
   // 將一個可能是偶數長/奇數長的字符串,首尾以及每個字符之間添加 "#",確保添加後長度變爲奇數的字符串
   manacherStr := "#" + strings.Join(strings.Split(s, ""), "#") + "#"

   // 記錄最長迴文子串的長度
   maxLength := 0

   // 記錄最長迴文子串的首字符對應原字符串s中首字符的索引位置
   start := 0
   for i := 0; i < len(manacherStr); i++ {
      left := i - 1
      right := i + 1
      // 擴散次數。因爲manacher字符串的構造,此時擴散次數等於最長迴文串的長度
      step := 0

      for {
         if !(left >= 0 && right < len(manacherStr) &&
            manacherStr[left] == manacherStr[right]) {
            break
         }
         left--
         right++
         step++
      }

      // 計算起始位置start
      if step > maxLength {
         maxLength = step
         start = (i - maxLength) / 2
      }
   }
   return s[start : start+maxLength]
}

func main() {
   fmt.Println(longestPalindrome("cababa"))
}

 

傳送門

LeetCode試題鏈接

strings.Split()函數

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章