2

Go:创建高性能的字符串

 3 years ago
source link: https://studygolang.com/articles/33219
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

【译文】 原文地址

我最近一直在用空闲时间来解决LeetCode中的问题,我认为这是一个锻炼Golang的机会,并使用Go作为练习LeetCode的主要语言。Go是一种令人惊叹的编程语言,我从根本上赞同go语言的设计,即一种和C语言一样高性能。而且和JavaScript和Python这些脚本语言一样高效和易于编程。我建议你有机会也学习和尝试下。是的,我非常喜欢Go。

下面回到我LeetCode问题: leetcode 1249 ,该问题要求删除最少数量的圆括号,直到字符串达到平衡,即字母加上有效圆括号。首先,该问题解决方法需要构建一组需要删除圆括号的索引,然后循环遍历字符串的每个字符,并构建一个新的包含所需的有效字符的字符串(减去无效的圆括号)。

因此我解决了这个问题,但是总觉得有些不对劲。我的运行时间是824ms,击败了大约25.29%的Go提交者。我在想Go不是应该很快的吗?这也是我学习这门语言的原因。我知道我的方案已经优化了的,因为我验证了作者的解决方法,因此应该不是算法的原因,尽管有一点也不是最根本原因。

bii6Bf.png!mobile

后来我用JavaScript重写了这个问题,看看运行效果如何。令我惊讶的是,在这个问题上JavaScript比Golang运行速度快了9倍。等等,什么?感觉不对。这时我开始进一步调查原因。我再次运行了这个问题的程序看是否是编译错误或其他原因,但不是Golang还是比JavaScript慢9倍。

Golang代码:

package main

import (
   "fmt"
)

func minRemoveToMakeValid(s string) string {

   stack := make([]int, 0)
   toRemove := make([]int, 0)

   for i := 0; i < len(s); i++ {

      if string(s[i]) == "(" {
         stack = append(stack, i)
      }else if string(s[i]) == ")" && len(stack) == 0 {
         toRemove = append(toRemove, i)
      } else if string(s[i]) == ")" && len(stack) > 0 {
         stack = stack[:len(stack) - 1]
      }
   }

   newString := ""
   toRemove = append(toRemove, stack...)

   removeMap := make(map[int]bool)

   for i:=0; i<len(toRemove); i++ {
      removeMap[toRemove[i]] = true
   }

   for i := 0; i < len(s); i++ {
      if !removeMap[i] {
         newString += string(s[i])
      }
   }

   return newString
}

func main() {
   s:= minRemoveToMakeValid("lee(t(c)o)de)")
   fmt.Println(s)
}

JavaScript代码:

/**
 * @param {string} s
 * @return {string}
 */
const minRemoveToMakeValid = (s) => {
    let stack = [];
    let toRemove = [];
    let stringOut = ""

    for(let i=0; i< s.length;i++){
        if(s[i] === "("){
            stack.push(i)
        }else if(s[i] === ")" && stack.length === 0){
            toRemove.push(i)

        }else if(s[i] === ")" && stack.length > 0){
            stack.pop();
        }
    }

    toRemove = toRemove.concat(stack)
    let removeMap = {}

    for(let i = 0; i<toRemove.length; i++){
        removeMap[toRemove[i]] = true
    }

    for(let i=0; i< s.length;i++){
        if(!removeMap[i]){
            stringOut+=s[i]
        }
    }

    return stringOut
};

console.log(minRemoveToMakeValid("lee(t(c)o)de)"));

正如你所看到的,以上两种代码几乎是相同的,所以这并不是js代码有特殊的优化。JS只是重写了go代码,两种方法应该是可比较的,而不是快这么多。

我在网上对这个问题进一步研究,我发现Golang字符串string对象是不可变的,运行时对字符串截取部分或者增加新的字符到字符串常量,Go都会创建新的string对象并拷贝。

让我们看看那些非常低效的字符串代码:

newString := ""
newString += string(s[i])
for(let i=0; i< s.length;i++){
    if(!removeMap[i]){
        stringOut+=s[i]
    }
}

在JavaScript中,字符串常量的创建是非常常用的,因此初始化空字符串并添加字符到字符串是有优化过的。但是Golang字符串是不可变的,所以每次添加字符到字符串变量,Go都会创建一个字符串的拷贝,这从内存和运行的角度来看都是很低效的。

在Golang中,如果你想创建一个字符串,需要使用strings包。Builder类型是bytes.Buffer的一个子集。Golang将字符串视为字节切片,其中每个字节都是字符串中字符的ASCII码。如果您想在内存和运行时上高效地对字符串进行操作,就需要使用byte buffer或者更高级的字符串结构体。

因此,我继续优化之前的Go代码:

// --- Initialize the string builder
var b strings.Builder
b.Grow(len(s) - len(toRemove))
// --- Write bytes in the bytes buffer
for i := 0; i < len(s); i++ {
   if !removeMap[i] {
      b.Write([]byte{s[i]})
   }
}

在我解决了这个优化的问题后,我再次提交我的解决方案,令我惊讶的是:

a2yemyv.png!mobile

通过这个小小的优化,我可以使我的Golang程序运行速度提高80倍。同样,我的程序比最初的版本快80被,比JavaScript版本的快9倍。这下验证了我当初的信念Go确实是一门快速的编程语言。

因此,对刚刚接触Golang的程序员来说,清楚这类细节,因为如果不正确使用Go,您的生产应用程序可能运行的很慢,会失去许多优化和语言优势。

有疑问加站长微信联系(非本文作者)

eUjI7rn.png!mobile

About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK