golang -- 字符串就地取反

字符串

定義

在golang中字符串是一種不可變的字節序列,它能夠包含任意的數據,包括0值字節,但主要是人類能夠閱讀的文本。golang中默認字符串被解讀爲utf-8編碼的Unicode碼點(文字符號)序列。golang

特性

golang中字符串具備不可變性。例如windows

str := "hello 世界!"

str[0] = 'L'

這種寫法會引發編譯錯誤:str[0] 不可賦值數組

字符串支持相似數組中分片的引用寫法:緩存

fmt.Println(str[:5]) // 輸出 hello

fmt.Println(str[7:]) // 輸出 世界

fmt.Println(str[len(s)+1:) // 宕機

str[i:j] , 當i、j 越界 (j 、i < 0 或 j、i > len(str) )或 j < i 時會發生宕機。安全

str := "Hello"

t := str

str += "world"

這種寫法能夠經過,雖然str指向了一個新的字符串「Hello world」,但t指向的舊字符串仍然存在。
不可變意味着兩個字符串可以安全的共享同一段底層內存,是的複製任何長度字符串的開銷都低廉,相似的字符串s及其子串(s[n:])字符串的複製也開銷低廉。函數

常見問題

  1. 順序輸出字符串中的每個字符。

這個問題乍一看十分的簡單,直接遍歷就行了:性能

str := "Hello 世界!" 

for i := 0; i < len(str) ; i++{
    fmt.Printf(「%c \t」,str[i])
}

然而事實沒那麼簡單,其輸出結果以下:測試

H   e   l   l   o       ä   ¸   ­   å   ›   ½   ï   ¼   

中文字符部分所有爲亂碼。這與utf-8的編碼規則有關, utf-8是以字節爲單位對unicode碼點進行變長編碼。每個文字符號用1~4個字節表示,ASCII字符僅僅佔1字節的內存,其餘經常使用的文書符號會佔到2~3個字節。一個字符編碼的首字節高位指明後面還有多少個字節:優化

規則 表示範圍 說明
0xxxxxxx 文字符號0~127 Ascii 字符
110xxxxx 10xxxxxx 128~2047 少於128個未使用的值
1110xxxxx 10xxxxxx 10xxxxxx 2048~65535 少於2048個未使用的值
1110xxxxx 10xxxxxx 10xxxxxx 10xxxxxx 65536~0x10ffffff 其餘未使用值

在上面提到的例子裏面Hello子串中的字符爲Ascii字符,佔用一個字節, 而 世界這兩個符號佔用的字符爲 3 個,因此遍歷的時候會出現亂碼的狀況。編碼

咱們這裏換一種寫法:

for i , v := range str {
    fmt.Printf("%d\t%c\n", i , v)
}

其顯示結果以下:

0   H
1   e
2   l
3   l
4   o
5    
6   中
9   國
12 !

以上代碼正常的輸出了每個字符包括中文字符。爲何使用range會成功? 由於range在循環的同時進行了隱式的解碼。其中 i 表示該字符在字符串中起始的下角標,v要重點說一下,它表示的是字符對應的unicode 點碼,在golang中它有一個專門的變量類型 rune (文字符號) ,它是int32類型的別名。在golang中int佔用內存大小取決於操做系統底層和計算機硬件,但int32必定是佔用 4 bytes ,rune類型在打印輸出的時候使用「%c」。

有了能遍歷輸出的函數天然很容易的就能夠寫出取反函數:

func Reverse(str string)(res string)  {
    for _, v := range str {
        res = string(v) + res
    }

    return
}

分析和優化

咱們測試一下這個函數的性能:

func BenchmarkReverse1(b *testing.B) {
    tStr := "Hello 中國!"
    for i := 0; i < b.N; i++{
        Reverse(tStr)
    }
}

做者在winx10/arm64 的操做系統中進行測試,cpu 爲core i3,內存爲 4g(硬件設施比較老舊了),最後得出的分析結果以下:

goos: windows
goarch: amd64
pkg: project/learn/chapeter2
BenchmarkReverse1-4      2000000           788 ns/op
PASS

ROUTINE ======================== project/learn/chapeter2.Reverse1 in D:\gopath\src\project\learn\chapeter2\str.go

 250ms      2.25s (flat, cum) 94.54% of Total
     .          .     14:
     .          .     15:   res = string(rnStr)
     .          .     16:   return
     .          .     17:}
     .          .     18:
  20ms       20ms     19:func Reverse1(str string)(res string)  {
     .          .     20:
  80ms      110ms     21:   for _, v := range str {
 150ms      2.12s     22:           res = string(v) + res
     .          .     23:           //res = fmt.Sprintf("%c%s", v ,res)
     .          .     24:   }
     .          .     25:
     .          .     26:   return
     .          .     27:}

能夠看見最耗時的操做就是res 從新賦值的部分,此時有兩種狀況:一、res字符串執行 + 操做很費時; 二、進行字符轉化的時候費時,咱們把代碼調整一下:

func Reverse(str string)(res string)  {
    for _, v := range str {
                temp := string(v) 
        res = temp + res
    }

    return
}

性能測試結果以下

ROUTINE ======================== project/learn/chapeter2.Reverse1 in D:\gopath\src\project\learn\chapeter2\str.go

 230ms      2.31s (flat, cum) 93.90% of Total
     .          .     14:
     .          .     15:   res = string(rnStr)
     .          .     16:   return
     .          .     17:}
     .          .     18:
  10ms       10ms     19:func Reverse1(str string)(res string)  {
  90ms      210ms     20:   for _, v := range str {
  30ms      200ms     21:           temp := string(v)
  90ms      1.88s     22:           res = temp + res
     .          .     23:
     .          .     24:   }
     .          .     25:
  10ms       10ms     26:   return
     .          .     27:}
     .          .     28:

可見res 執行 + 操做要更費時一些,在執行+操做的過程當中,要經歷 字符串拷貝、底層字節數組內存從新分配(可能被觸發)。
優化的思路很簡單,建立一片‘緩存’,用來存儲字符串對應的字節數據,最後再統一轉化爲字符串。

func Reverse(str string)(res string)  {
    i:=0
    cache := make([]byte, len(str))

    for _, v := range str {
        i += utf8.RuneLen(v)
        utf8.EncodeRune(cache[len(str) - i:], v)
    }

    res = string(cache)
    return
}

執行結果以下:

goos: windows
goarch: amd64
pkg: project/learn/chapeter2
BenchmarkReverse2-4      5000000               253 ns/op
PASS
ok      project/learn/chapeter2 1.831s

ROUTINE ======================== project/learn/chapeter2.Reverse2 in D:\gopath\src\project\learn\chapeter2\str.go

 510ms      1.45s (flat, cum) 90.62% of Total
     .          .     29:
  20ms       20ms     30:func Reverse2(str string)(res string)  {
     .          .               31:   i:=0
  20ms      210ms     32:   cache := make([]byte, len(str))
     .          .     33:
 280ms      460ms     34:   for _, v := range str {
  50ms       90ms     35:           i += utf8.RuneLen(v)
 100ms      250ms     36:         utf8.EncodeRune(cache[len(str) - i:], v)
     .          .     37:   }
     .          .     38:
  20ms      400ms     39:   res = string(cache)
  20ms       20ms     40:   return
     .          .     41:}

優化率接近68%。從以上過程咱們能夠對golang的字符串類型的變量有一個直觀的認識。

相關文章
相關標籤/搜索