GO语言基础篇(十四)- 字符串&字节slice详解

850 阅读4分钟

这是我参与8月更文挑战的第 14 天,活动详情查看: 8月更文挑战

本文主要分享字符串以及字符slice的相关内容,下边会通过一个leetcode中的例题来分享字符串、字节slice,以及rune类型的使用场景及区别

字符串

基础使用介绍

字符串是不可变的字节序列,它可以包含任意的数据,包括0值字节。习惯上,文本字符串被解读成UTF-8编码的Unicode码点(文字符号)序列(对UTF-8及码点不了解的点这里

内置的len函数返回字符串的字节数(并非文字符号的数目),下标访问操作s[i]则取得第i个字符,其中0<=i<len(s)

s := "hello, world"
fmt.Println(len(s))//"12"
fmt.Println(s[0], s[7])//"104  119"

字符串的第i个字节,不一定就是第i个字符,因为非ASCII字符的UTF-8码点需要两个字节或多个字节。子串生成操作s[i:j]产生一个新字符串,内容取自原字符串字节,下标从i(含边界值)开始,直到j(不含边界值),结果是j-i个字节

fmt.Println(s[:5])// "hello"
fmt.Println(s[7:])// "world"
fmt.Println(s[:])// "hello,world"

加号(+)运算符可用于连接两个字符串而生成一个新字符串

fmt.Println("goodbye" + s[5:]) // "goodbye, world"

尽管可以将新值赋给字符串变量,但是字符串值无法改变。字符串值本身所包含的字节序列永不可变。要在一个字符串后面添加另一个字符串,可以这样写

s := "left root"
t := s
s += ", right root"
fmt.Println(s) // "left root, right root"
fmt.Println(t)// "left root"

这并没有改变s原有的字符串值,只是将+=语句生成的新字符串赋予s(联系前边分享的切片理解,点这里)。同时,t仍然持有旧的字符串值。因为字符串不可改变,所以字符串内部的数据不允许修改(不可变意味着两个字符串能安全的公用一段底层内存,使得复制任何长度字符串的开销都低廉

s[0] = 'L' // 编译错误:s[0]无法赋值

字符串和字节slice

4个标准包对字符串操作特别重要: bytesstringsstrconvunicode

  • strings 包提供了许多函数,用于搜索、替换、比较、修整、切分与连接字符串
  • bytes 包也有类似的函数,用于操作字节 slice([]byte类型,其某些属性和字符串相同)。由于字符串不可变,因此按增量方式构建字符串会导致多次内存分配和复制。这种情况下,使用bytes.buffer类型会更高效
  • strconv 包具备的函数,主要用于转换布尔值、整数、浮点数为与之对应的字符串形式,或者把字符串转换为布尔值、整数、浮点数,另外还有为字符串添加/去除引号的函数
  • unicode 包备有判别文字符号值特性的函数,如IsDigit、IsLetter、IsUpper和IsLower。每个函数以单个文字符号值作为参数,并返回布尔值。若文字符号值是英文字母,转换函数(如ToUpper和ToLower)将其转换成指定的大小写。上面所有函数都遵循 Unicode 标准对字母数字等的分类原则。strings包也有类似的函数,函数名也是ToUpper和ToLower,它们对原字符串的每个字符做指定变换,生成并返回一个新字符串

字符串可以和字符slice相互转换

s := "abc"
b := []byte(s)
s2 := string(b)
fmt.Println(s2)

概念上,[]byte(s)转换操作会分配新的字节数组,拷贝填人s含有的字节,并生成一个slice引用,指向整个数组。具备优化功能的编译器在某些情况下可能会避免分配内存和复制内容,但一般而言,复制有必要确保s的字节维持不变(即使b的字节在转换后发生改变)。反之,用string(b)将字节slice转换成字符串也会产生一份副本,保证s2也不可变

为了避免转换和不必要的内存分配,bytes包和strings包都预备了许多对应的实用函数,它们两两对应。strings包具备下边6个函数:

func Contains(s, substr string) bool
func Count(s , sep string) int
func Fields(s string) []string
func HasPrefix (s , prefix string) bool
func Index(s , sep string) int
func Join(s [][]byte, sep []byte) []byte

bytes 包里面的对应函数为:


func Contains(b , subslice [] byte) bool
func Count(s , sep [ ] byte) int
func Fields(s [ ] byte ) [] [] byte
func HasPrefix(s , prefix [] byte) bool
func Index(s , sep [] byte) int
func Join(s [][]byte, sep []byte) []byte

字符串和数字的相互转换

处理字符串、文字符号和字节之间的转换,常常也需要相互转换数值及其字符串表示形式。这个由strconv包的函数完成

要将整数转换成字符串,一种方法是fmt.Sprintf(),另一种是用strconv.Itoa()

x := 123
y := fmt.Sprintf("%d", x)
fmt.Println(y, strconv.Itoa(x)) // "123   123"

FormatInt和FormatUint可以按不同的进位制格式化数据

fmt.Println(strconv.FormatInt(int64(x), 2))//”1111011“  将x转2进制

strconv包内的Atoi函数或ParseInt函数用于解释表示整数的字符串,ParseUint用于无符号整数

x, err := strconv.Atoi("123")//x是整形 
y, err := strconv.ParseInt("123", 10, 64)//十进制,最长为64

ParseInt的第三个参数,指定结果必须匹配何种大小的整形,例如,16表示int16,而0最为特殊值表示int

使用演示

题目:请从字符串中找出一个最长的不包含重复字符的子字符串,计算该最长子字符串的长度 来源:LeetCode

示例

输入: "abcabcbb"
输出: 3 
解释: 因为无重复字符的最长子串是 "abc",所以其长度为 3。

输入: "bbbbb"
输出: 1
解释: 因为无重复字符的最长子串是 "b",所以其长度为 1。

输入: "pwwkew"
输出: 3
解释: 因为无重复字符的最长子串是 "wke",所以其长度为 3。
     请注意,你的答案必须是 子串 的长度,"pwke" 是一个子序列,不是子串。

思路

假设有下边这样一个字符串,我们从左往右扫描,只需要扫描一遍就可以了。如果扫描到字母X,首先我们记录一个start,它表示当前找到的最长不含重复字符子串的开始位置。当我们遇到一个字符X,就需要去看一下从start到X的下标减1的位置,是否存在这个字母X

如何看start到X的下标减1的位置是否含有字母X?可以通过一个map来记录扫描过程中,每个字母最后出现的位置lastOccurred[x],如果该字符不包含在该map中,或出现在start之前,那这种情况就不需要处理。如果它出现在了start和X下标的中间,那此时就需要更改start的位置为该字符最后出现的位置加1

梳理思路:

对于每一个字母x

  • lastOccurred[x]不存在,或者小于start,则无需操作
  • lastOccurred[x]大于start,则更新start
  • 更新lastOccurred[x],更新最长子串的长度

解题

func lengthOfNonrepeatingSubStr(s string) int {
		lastOccured := make(map[byte]int)
		start := 0
		maxLength := 0
		for i, ch := range []byte(s) {
			lastId, ok := lastOccured[ch]
			if  ok && lastId >= start {
				start = lastId + 1
			}
			if i - start + 1 > maxLength {
				maxLength = i - start + 1
			}
			lastOccured[ch] = i
		}

	return maxLength
}

func main() {
    fmt.Println(newNonRepeat("abcabcbb"))//3
    fmt.Println(newNonRepeat("bbbbb"))//1
    fmt.Println(newNonRepeat("pwwkew"))//3
    fmt.Println(newNonRepeat(""))//0
    fmt.Println(newNonRepeat("b"))//1
    fmt.Println(newNonRepeat("abcdefg"))//7
}

这道题这样解,放到leetcode中是可以通过的,但是如果我们需要验证的字符串是一个包含中文的字符串,这个程序就不准确了,所以需要对该程序进行改造,使它能够支持中文

支持中文字符串

其实,使它支持中文的关键,就是如何使用go语言中的rune类型。看下边的例子

s := "Yes我是一个测试!"
for _, b := range []byte(s) { //将字符串类型强制转换成字节切片类型,每次取的是一个字节
    fmt.Printf("%X ", b) //以十六进制的形式输出(可以看出来每个中文占3个字节)
    //打印结果:59 65 73 E6 88 91 E6 98 AF E4 B8 80 E4 B8 AA E6 B5 8B E8 AF 95 21
}
fmt.Println()

for i, ch := range s {//直接遍历字符串,此次取的是一个字符
    fmt.Printf("(%d %X)", i, ch)//可以发现到遍历中文的时候,下标每次就移动不是1个位置了
    //打印结果:(0 59)(1 65)(2 73)(3 6211)(6 662F)(9 4E00)(12 4E2A)(15 6D4B)(18 8BD5)(21 21)
}
fmt.Println()
fmt.Println("Rune count:", utf8.RuneCountInString(s))//10 该方法可以获取字符数量
	

for i, ch := range []rune(s) { //rune是int32的别名,每一个代表四个字节
    fmt.Printf("(%d %c)", i, ch)
    //打印结果:(0 Y)(1 e)(2 s)(3 我)(4 是)(5 一)(6 个)(7 测)(8 试)(9 !)
}

这种遍历方式,就可以将一个字符串正常遍历,而不用关心中文了

改造上边的查找最长不含重复字符子串

func newNonRepeat(s string) int {
	lastOccured := make(map[rune]int)
	start := 0
	maxLength := 0
	for i, ch := range []rune(s) {
		lastId, ok := lastOccured[ch]
		if ok && lastId >= start {
			start = lastId + 1
		}
		if i - start +1 > maxLength {
			maxLength = i - start + 1
		}
		lastOccured[ch] = i
	}

	return maxLength
}

fmt.Println(newNonRepeat("我是个测试"))//5
fmt.Println(newNonRepeat("一二三二一"))//3