常见算法之字符串

132 阅读4分钟

基础知识

字符串由任意长度的字符组成,是编程语言中表示文本的数据类型。String类型所表达的字符串是无法改变的,也就是说,只能对字符串进行读操作。如果对字符串进行写操作,那么修改的内容在返回值的字符串中,原来的字符串保持不变。

算法题目

一、字符串中的变位词

题目:输入字符串s1和s2,如何判断字符串s2中是否包含字符串s1的某个变位词?如果字符串s2中包含字符串s1的某个变位词,则字符串s1至少有一个变位词是字符串s2的子字符串。假设两个字符串中只包含英文小写字母。例如,字符串s1为"ac",字符串s2为"dgcaf",由于字符串s2中包含字符串s1的变位词"ca",因此输出为true。如果字符串s1为"ab",字符串s2为"dgcaf",则输出为false。

解题思路: 变位词具有以下几个特点。首先,一组变位词的长度一定相同;其次,组成变位词的字母集合一定相同,并且每个字母出现的次数也相同。 如果一个哈希表的键是字母,而哈希表中的值是对应字母出现的次数,那么这样一个哈希表很适合用来统计字符串中每个字母出现的次数。 将s1出现字符的情况记录在哈希表中。然后按字符s1长度差距的两个指针,在s2字符串上移动,每移动一个,减去哈希表对应的字符键的值。然后遍历哈希表,如果哈希表中全部为0时,就说明找到了变位词。

Golang代码:

func checkInclusion(s1 string, s2 string) bool {

   if len(s1) > len(s2) {
      return false
   }

   memo := make(map[uint8]int)
   for i := uint8('a'); i < uint8('a') + 26; i++ {
      memo[i] = 0
   }

   for i := 0; i < len(s1); i++ {
      memo[s1[i]]++
   }

   var (
      left = 0
      right = len(s1) - 1
   )

   for j := 0; j < len(s2); j++ {

      if j < len(s1) {
         memo[s2[j]]--
      } else {
         memo[s2[left]]++
         left++
         right++
         memo[s2[right]]--
      }

      if allZero(memo) {
         return true
      }

   }

   return false
}

func allZero(data map[uint8]int) bool {
   for _, item := range data {
      if item != 0 {
         return false
      }
   }
   return true
}

二、不含重复字符的最长子字符串

题目:输入一个字符串,求该字符串中不含重复字符的最长子字符串的长度。例如,输入字符串"babcca",其最长的不含重复字符的子字符串是"abc",长度为3。

解题思路: 和前面的题目一样,此处还是用一个哈希表统计子字符串中字符出现的次数。如果一个子字符串中不含重复的字符,那么每个字符都只出现一次,它们在哈希表中对应的值为1。没有在子字符串中出现的其他字符对应的值都是0。也就是说,如果子字符串中不含重复字符,那么它对应的哈希表中没有比1大的值。

func lengthOfLongestSubstring(s string) int {
   memo := make(map[uint8]int)
   for i := uint8('a'); i < uint8('a') + 26; i++ {
      memo[i] = 0
   }

   var(
      maxLen, sLen, left int
      strMap = make(map[uint8]int)
   )

   for i := 0; i < len(s); i++ {
      memo[s[i]]++
      if allOne(memo) {
         sLen++
         if sLen > maxLen {
            maxLen = sLen
         }
         strMap[s[i]] = i
      } else {
         dealMemo(memo, s[left:strMap[s[i]] + 1])
         left = strMap[s[i]] + 1
         sLen = i - left + 1
         strMap[s[i]] = i
      }
   }

   return maxLen
}

func dealMemo(memo map[uint8]int, s string) {
   for i := 0; i < len(s); i++ {
      memo[s[i]]--
   }
}

func allOne(data map[uint8]int) bool {
   for _, item := range data {
      if item > 1 {
         return false
      }
   }
   return true
}

三、最多删除一个字符得到回文

给定一个非空字符串 s,请判断如果 最多 从字符串中删除一个字符能否得到一个回文字符串。

解题思路: 双指针,头部指针P1向右移动,尾部指针P2向左移动,进行对比。如果碰到不相等的字符,就P1向右移动一位,再进行回文对比,或P2向左移动一位,再进行回文,如果有一种是回文,就表示成功。

Golang代码:

func validPalindrome(s string) bool {
   var (
      left int
      right = len(s) - 1
   )

   for left <= len(s)/2 {
      if s[left] == s[right] {
         left++
         right--
      } else {
         return palindrome(s[left+1:right+1]) || palindrome(s[left:right])
      }
   }

   return true
}

func palindrome(s string) bool {

   var (
      left int
      right = len(s) - 1
   )

   for left < right {
      if s[left] != s[right] {
         return false
      }
      left++
      right--
   }

   return true
}

四、回文子字符串的个数

给定一个字符串 s ,请计算这个字符串中有多少个回文子字符串。具有不同开始位置或结束位置的子串,即使是由相同的字符组成,也会被视作不同的子串。

解题思路: 判断回文的方式有两种:一种是从两头向中心移动对比字符,另一种是从中间向两头移动对比字符。本题就是用第二种方式。从头到尾遍历字符串,以每个字符为中心,向两边移动对比。也可能是偶数回文,就以当前和后一个字符为中心,向两边发散对比。每对比成功一次就count++,得到最终count。

Golang代码:

func countSubstrings(s string) int {
   var count int
   for i := 0; i < len(s); i++ {
      spreadFromCentre(s, i, &count)
   }
   return count
}

func spreadFromCentre(s string, i int, count *int) {
   var (
      left = i
      right = i
   )
   for left >= 0 && right < len(s) {
      if s[left] == s[right] {
         *count++
         left--
         right++
      } else {
         break
      }
   }

   left = i
   right = i
   right++
   if right < len(s) && s[left] == s[right] {
      for left >= 0 && right < len(s) {
         if s[left] == s[right] {
            *count++
            left--
            right++
         } else {
            break
         }
      }
   }

   return
}