Skip to content

Latest commit

 

History

History

Folders and files

NameName
Last commit message
Last commit date

parent directory

..
 
 

readme.md

week08

homework

基础问题

字符串操作

同构或异位词系列问题

字符串与动态规划

Part 1

最短路

最小生成树

Part 2

字符串基础知识

http://c.biancheng.net/view/18.html

字符串中的每一个元素叫做“字符”,在遍历或者单个获取字符串元素时可以获得字符。

Go语言的字符有以下两种: 一种是 uint8 类型,或者叫 byte 型,代表了 ASCII 码的一个字符。 另一种是 rune 类型,代表一个 UTF-8 字符,当需要处理中文、日文或者其他复合字符时,则需要用到 rune 类型。rune 类型等价于 int32 类型。

func myAtoi(s string) int {
    // '0' (type rune) 
    // "0" (type untyped string)
    // fmt.Println(s[0]) // 42
    // fmt.Println(reflect.TypeOf(s[0])) // uint8
    // fmt.Println(rune(s[0]) - '0') // uint8 => int32 
    // 字符串转整数模板
    index := 0
    // 1. 丢弃前导空格
    for index < len(s) && s[index] == ' ' {
        index++
    }
    // 2. 判断符号
    sign := 1
        if index < len(s) && (s[index] == '+' || s[index] == '-') {
        if s[index] == '-' { sign = sign * -1 }
        index++
    }
    // 3. 处理数字
    val := 0
    // ASCII table
    // ASCII码 '0'-'9'是相连的
    for index < len(s) && int(s[index]) >= int('0') && int(s[index]) <= int('9') {
        // if 数值范围
        // if (val * 10 + s[index] - '0') > 2147483648 {
        // 移项
        if val > ( math.MaxInt32 - int(s[index]) + int('0') ) / 10 {
        if sign == -1 { return -2147483648 } // −2^31
        return 2147483647 // 2^31 − 1
        }
        val = val * 10 + int(s[index]) - int('0')
        index++
    }
    // 4. 终止条件:遇到非数字停止
    // 已经体现在while循环中
    return val * sign
}

Rabin Karp字符串哈希算法

  • 引入
选用的 Hash 函数把字符串看作一个 b 进制数一个多项式),计算它在十进制下 p 取模的值

举例 b = 131p = 2^64

字符串 foobar  Hash 值为a=1b=2f=6o=15r=18)

(6*131^5 + 15*131^4 + 15*131^3 + 2*131^2 + 1*131^1 + 18*131^0) mod 2^64


选取b和p的值决定了Hash函数的质量

根据经验b=13113331p为大质数冲突概率极小

Hash值相等时可以再对比一下两个字符串避免Hash碰撞问题

c++可以自然溢出其他语言需要取模

如何快速计算一个子串的hash值?

类比十进制数 s = "0123":
计算前 i 个数字的 H 
H[0] = 0 // 初始化
H[1] = Hash(s[0...1-1]) = Hash[s[0...0]] = 0
H[2] = Hash(s[0...2-1]) = Hash[s[0...1]] = H[1] * 10^0 + 1 = 1
H[3] = Hash(s[0...3-1]) = Hash[s[0...2]] = H[2] * 10^1 + 2 = 12
H[4] = Hash(s[0...4-1]) = Hash[s[0...3]] = H[3] * 10^2 + 3 = 123
H[i] = Hash(s[0...i-1]) = H[i - 1] * 10^(i-2) (i > 1)

// 推导过程
s = "foobar"

先计算6个前缀子串的Hash值O(n):
H[i] = Hash(s[0...i-1]) = (H[i - 1] * b + s[i - 1]) * mod p

计算子串 oba  Hash 相当于b进制下两个数做减法H[5] - H[2] * b^3fooba
-fo000
------
   oba

假设下标从0开始Hash(oba) =>
Hash(s[2..4]) =>H[5] - H[2] * b^3归纳为Hash(s[l...r]) => ( H[r+1] - H[l] * b^(r-l+1) ) % p, O(1)
func strStr(haystack string, needle string) int {
    s := haystack // 原始字符串
    t := needle // 目标字符串
    // RKHash模版(取模法)
    if len(t) == 0 { return 0 }
    n := len(s)
    m := len(t)
    s = " " + s
    t = " " + t

    var p int64 = 1e9 + 7 // 10^9 + 7 是一个质数
    var tHash int64 = 0 // java long类型,int64
    for i := 1; i <= m; i++ {
        tHash = (tHash * 131 + int64(t[i] - 'a') + 1) % p
    }
    // 模版
    sHash := make([]int64, n + 1)
    sHash[0] = 0
    p131 := make([]int64, n + 1) // 131的次幂
    p131[0] = 1
    for i := 1; i <= n; i++ {
        // 计算s前i个字符的hash值
        sHash[i] = (sHash[i - 1] * 131 + int64(s[i] - 'a') + 1) % p
        // 预存次幂,节省时间
        p131[i] = p131[i - 1] * 131 % p
    }
    // hello
    // ll
    for i := m; i <= n; i++ { // 滑动窗结尾
        // s[i-m+1...i] 与 t[1...m] 是否相等
        //if calcHash(sHash, p131, p, i-m+1, i) == tHash { // 不考虑碰撞
        if calcHash(sHash, p131, p, i-m+1, i) == tHash && s[i-m+1:i+1] == t[1:] { // 考虑碰撞
            return i - m //+ 1 - 1 // 下标变回从0开始
        }
    }
    return -1
}
// 模板:O(1)得到子串[l..r]的Hash值
func calcHash(H,p131 []int64, p int64, l,r int) int64 {
    // 求 hello 的子串的hash值
    //  h  e  l l
    // -h  e  0 0
    // =      l l
    //   (l-1)l r => 下标位置
    // r - l + 1 => 长度次幂
    // ?+ p % p 是为了控制 0 ~ p 之间
    return ( (H[r] - H[l - 1] * p131[r - l + 1]) % p + p) % p
}

回文串系列

func isPalindrome(s string) bool {
    var t string
    for _,ch := range s {
        //fmt.Println(reflect.TypeOf(ch)) // int32
        //fmt.Println(reflect.TypeOf('0')) // int32
        // 只考虑字符和数字
        if (ch >= '0' && ch <= '9') || (ch >= 'A' && ch <= 'Z') || ch >= 'a' && ch <= 'z' {
            if ch >= 'A' && ch <= 'Z' {
                ch = ch - 'A' + 'a'
            }
            t = t + string(ch)
        }
    }
    l := 0
    r := len(t) - 1
    for l < r {
        if t[l] != t[r] {return false}
        l++
        r--
    }
    return true
}
// 效率较低 192ms
func validPalindrome(s string) bool {
    return validPalindromeHelper(s, 0, len(s) - 1, true)
}

func validPalindromeHelper(s string, l,r int, canDelete bool) bool {
    for l < r {
        if s[l] == s[r] {
            l++
            r--
        } else {
            if canDelete { // 能删除,尝试删除一个位置
                return validPalindromeHelper(s, l+1, r, false) || validPalindromeHelper(s, l, r-1, false)
            } else {
                return false
            }
        }
    }
    return true
}

解法1

func longestPalindrome(s string) string {
    if len(s) == 0 {return ""}
    // 枚举中点,向两边扩展,考虑奇偶
    n := len(s)
    s = " " + s
    anslen := 0
    ansstart := 0
    // 中心是一个字符的情况,比如aba
    for center := 1; center <= n; center++ {
        l := center - 1
        r := center + 1
        for l > 0 && r <= n && s[l] == s[r] {
            l--
            r++
        }
        // l+1 ~ r-1 // 最后l减1、r加1之前是回文串,需要加回去
        // (r-1) - (l+1) + 1 = r - l - 1 // 实际长度
        if r - l - 1 > anslen { // 比之前的结果长,则更新
            anslen = r - l - 1
            ansstart = l + 1 // 加回来
        }
    }
    // 中心是两个字符(一个空),比如abba
    for center := 1; center < n; center++ { // n-1,n
        l := center
        r := center + 1
        for l > 0 && r <= n && s[l] == s[r] {
            l--
            r++
        }
        // l+1 ~ r-1 // 最后l减1、r加1之前是回文串,需要加回去
        // (r-1) - (l+1) + 1 = r - l - 1 // 实际长度
        if r - l - 1 > anslen { // 比之前的结果长,则更新
            anslen = r - l - 1
            ansstart = l + 1 // 加回来
        }
    }
    return  s[ansstart:ansstart + anslen] // 前闭后开
}

解法2 二分答案+RKHash

中间向两边扩张 O(n2) 加入二分 + Rabin-Karp 优化,O(nlogn)

func longestPalindrome(s string) string {
    if len(s) == 0 {return ""}
    // 枚举中点,向两边扩展,考虑奇偶
    n := len(s)
    s = " " + s
    
    // RKHash模版
    var p int64 = 1e9 + 7
    preH := make([]int64, n+1) // 前缀Hash
    preH[0] = 0
    p131 := make([]int64, n+1) // 131次幂
    p131[0] = 1
    for i := 1; i <= n; i++ {
        preH[i] = (preH[i - 1] * 131 + int64(s[i] - 'a') + 1) % p
        p131[i] = p131[i - 1] * 131 % p
    }
    sufH := make([]int64, n+2) // 后缀Hash(反着读就是前缀字符串)
    sufH[n+1] = 0
    for i := n; i >= 1; i-- {
        sufH[i] = (sufH[i + 1] * 131 + int64(s[i] - 'a') + 1) % p
    }

    anslen := 0
    ansstart := 0
    // 中心是一个字符的情况,比如aba
    for center := 1; center <= n; center++ {
        // 二分查找,从当前字符串向两边可扩展的最大长度
        lenL := 0 // 下界
        lenR := n // 上界
        l := 0
        r := 0
        len := 0
        for lenL < lenR { // 二分模版部分
            len = (lenL + lenR + 1) >> 1 // mid
            l = center - len
            r = center + len
            if isPalindrome(s, n, preH, sufH, p131, p, l, r) { // 是回文,可以更长
                lenL = len
            } else {
                lenR = len - 1
            }
        }
        // 两侧最多扩LenL,再加一个中心
        if lenL * 2 + 1 > anslen {
            anslen = lenL * 2 + 1
            ansstart = center - lenL
        }
    }
    // 中心是两个字符(一个空),比如abba
    for center := 1; center < n; center++ { // n-1,n
        // 二分查找,从当前字符串向两边可扩展的最大长度
        lenL := 0 // 下界
        lenR := n // 上界
        l := 0
        r := 0
        len := 0
        for lenL < lenR { // 二分模版部分
            len = (lenL + lenR + 1) >> 1 // mid
            // 代入abba // 走len=0步,为空串;
            l = center - len + 1 // l~center
            r = center + len // center+1~r
            if isPalindrome(s, n, preH, sufH, p131, p, l, r) { // 是回文,可以更长
                lenL = len
            } else {
                lenR = len - 1
            }
        }
        // 两侧分别是l~center和center+1~r
        if lenL * 2 > anslen {
            anslen = lenL * 2
            ansstart = center - lenL + 1
        }
    }
    return  s[ansstart:ansstart + anslen] // 前闭后开
}
// RKHash判定是否回文
func isPalindrome(s string, n int, preH,sufH,p131 []int64, p int64, l, r int) bool {
    if l < 1 || r > n { return false }
    if l > r { return true }
    return calcPre(preH, p131, p, l, r) == calcSuf(sufH, p131, p, l, r)
}

// 模板:O(1)得到子串[l..r]的Hash值
func calcPre(preH,p131 []int64, p int64, l,r int) int64 {
    return ((preH[r] - preH[l - 1] * p131[r - l + 1]) %p + p) %p
}

// 模板:O(1)得到子串[l..r]反着读的Hash值
func calcSuf(sufH,p131 []int64, p int64, l,r int) int64 {
    return ((sufH[l] - sufH[r + 1] * p131[r - l + 1]) %p + p) %p
}

字符串与动态规划 1

func isMatch(s string, p string) bool {
    n := len(s)
    m := len(p)
    s = " " + s
    p = " " + p
    // 定义前i个和前j个是否匹配
    f := make([][]bool, n + 1)
    for i := range f {
        f[i] = make([]bool, m + 1)
    }
    f[0][0] = true
    // '*' => 匹配前面的子表达式零次或多次。例如,zo* 能匹配 "z" 以及 "zoo"。* 等价于{0,}
    for i := 2; i<= m && p[i] == '*'; i+=2 {
        f[0][i] = true // 原字符串里偶数位为'*',比如a*a*a*,当*取0时,最终还是能匹配到空串
    }
    for i := 1; i <= n; i++ {
        for j := 1; j <= m; j++ {
            if p[j] == '.' { 
                // 日常用法:匹配除换行符(\n、\r)之外的任何单个字符
                // 题目里,j 是 '.' 匹配任意字符,所以s[i]和p[j]一定能匹配,那么f[i][j]取决于f[i-1][j-1]能否匹配
                f[i][j] = f[i - 1][j - 1]
            } else if p[j] == '*' {
                // j 是 '*' 时,
                // (1)可以选择匹配0个,即不需要看 p[j-1]p[j] ,那么f[i][j]取决于f[i][j-2]能否匹配
                f[i][j] = f[i][j - 2]
                // (2)可以选择匹配1次以上,
                // 如果是 p[j-1]p[j] 是 .* 或者 s[i] == p[j-1],那么 s[i] 都能和 p[j-1]p[j] 匹配上,则f[i][j]取决于f[i-1][j] 
                if p[j - 1] == '.' || s[i] == p[j - 1] {
                    f[i][j] = f[i][j] || f[i - 1][j]
                }
            } else {
                // 前i个与前j个能匹配,一定是前i-1个字符匹配前j-1个字符,同时s[i] == p[j],否则匹配失败
                f[i][j] = f[i - 1][j - 1] && s[i] == p[j]
            }
        }
    }
    return f[n][m]
}
func numDistinct(s string, t string) int {
    // 定义s前i个字符里,有几个子序列等于t的前几个字符
    n := len(s)
    m := len(t)
    s = " " + s
    t = " " + t
    f := make([][]int, n + 1)
    for i := range f {
        f[i] = make([]int, m + 1)
    }
    for i := 0; i<= n; i++ {
        f[i][0] = 1 // s前i个字符出现空串的次数是1
    }
    // 根据i-1的状态推i的状态
    for i := 1; i <= n; i++ {
        for j := 1; j <= m; j++ {
            f[i][j] = f[i - 1][j] // 不要i这个位置的字符
            // 要i这个位置的字符
            if s[i] == t[j] {
                // 如果字符相等的话,就要加上f[i - 1][j - 1]的次数
                f[i][j] = f[i][j] + f[i - 1][j - 1]
            }
        }
    }
    return f[n][m]
}

KMP字符串模版