【问题标题】:Faster input scanning更快的输入扫描
【发布时间】:2020-07-22 05:53:51
【问题描述】:

我正在尝试解决可以在here 找到的 SPOJ 问题

以下是我的解决方案:

package main

import "fmt"
import "bufio"
import "os"

func main() {
    var n, k int
    var num int
    var divisible int

    in := bufio.NewReader(os.Stdin)

    fmt.Fscan(in, &n)
    fmt.Fscan(in, &k)

    for n > 0 {
        fmt.Fscan(in, &num)

        if num%k == 0 {
            divisible++
        }

        n--
    }

    fmt.Println(divisible)
}

代码运行良好。这里的问题是我在 SPOJ 中执行它时超时。

我最初只使用fmt.Scan,但后来我遇到this 线程建议我使用bufio 来加快输入扫描速度。

但我仍然遇到超时问题。我只是循环获取所有输入,并且在这个循环本身中我确定输入是否可分。所以,我相信这不是循环,而是需要时间的输入扫描。如何改进这一点以更快地读取输入?还是其他地方的问题?

【问题讨论】:

    标签: string go


    【解决方案1】:

    您可以使用bufio.Scanner 从输入中读取行。

    由于我们一直在读取数字,因此我们可以创建一个高度优化的转换器来获取数字。我们应该避免使用创建stringScanner.Text(),因为我们可以从Scanner.Bytes() 返回的原始字节中获取数字。 Scanner.Text() 返回与 Scanner.Bytes() 相同的令牌,但它首先转换为 string,这显然更慢并生成“垃圾”并为 gc 工作。

    所以这是一个转换器函数,它从原始字节中获取int

    func toInt(buf []byte) (n int) {
        for _, v := range buf {
            n = n*10 + int(v-'0')
        }
        return
    }
    

    toInt() 有效,因为[]byte 包含数字的十进制格式的字符串表示的UTF-8 编码字节序列,其中仅包含'0'..'9' 范围内的数字,其UTF-8 编码字节一对一映射(一个字节用于一个数字)。从数字到字节的映射只是一个移位:'0' -> 48'1' -> 49 等。

    使用这个完整的应用程序:

    package main
    
    import (
        "bufio"
        "fmt"
        "os"
    )
    
    func main() {
        var n, k, c int
        scanner := bufio.NewScanner(os.Stdin)
    
        scanner.Scan()
        fmt.Sscanf(scanner.Text(), "%d %d", &n, &k)
    
        for ;n > 0; n-- {
            scanner.Scan()
            if toInt(scanner.Bytes())%k == 0 {
                c++
            }
        }
    
        fmt.Println(c)
    }
    
    func toInt(buf []byte) (n int) {
        for _, v := range buf {
            n = n*10 + int(v-'0')
        }
        return
    }
    

    例如,此解决方案比调用strconv.Atoi() 快​​约 4 倍。

    注意事项:

    在上述解决方案中,我假设输入是有效的,即它始终包含有效数字,并且在第一行之后至少包含n 行(这给了我们nk)。

    如果输入在n+1 行之后关闭,我们可以使用简化的for(我们甚至不需要递减并依赖n):

    for scanner.Scan() {
        if toInt(scanner.Bytes())%k == 0 {
            c++
        }
    }
    

    【讨论】:

    • 如何停止扫描输入? scanner.Scan() 循环继续,即使我只按回车键
    • @callmekatootie scanner.Scan() 返回 true 直到有输入。如果要在本地测试它,请使用修改后的循环来计算行数。请参阅编辑后的答案。
    • 好的。变量n 是输入的数量。所以,我假设如果我需要在 n 个输入后停止,我将 for 条件替换为n>0 条件并将scanner.Scan() 移动到 for 循环内
    • @callmekatootie 我编辑了答案以避免混淆。完整的示例现在循环读取n 行。简化的循环现在移至“注释”部分。
    • @callmekatootie,如果您使用的是 Linux,请按 Ctrl+D
    【解决方案2】:

    尝试使用bufio.Scanner(如您提到的线程中所建议的那样):

    fmt.Scan(&n)
    fmt.Scan(&k)
    
    scanner := bufio.NewScanner(os.Stdin)
    for n > 0 {
        scanner.Scan()
        k, _ := strconv.Atoi(scanner.Text())
        ...
    

    【讨论】:

    • 这也可以。但是,所用时间为 1.08 秒。另一个解决方案被接受,因为它需要 0.28 秒。
    • @callmekatootie,是的,有道理。 scanner.Text() 分配内存并且它比@icza 的解决方案慢
    【解决方案3】:

    我编写了 3 个版本来比较它们。 第一个使用fmt.Scanf("%d", &v),第二个从字节转换数字(如@icza),第三个使用strconv.Atoi 转换。要使用我以这种方式初始化扫描仪的功能:

    scanner := bufio.NewScanner(os.Stdin)
    scanner.Split(bufio.ScanWords)
    

    所以,每次我调用scanner.Scan 时,它都会返回一个由空格分隔的令牌。 功能如下:

    func scanFromBytes(scanner *bufio.Scanner) (n int) {
        scanner.Scan()
    
        buf := scanner.Bytes()
        for _, v := range buf {
            n = n*10 + int(v-'0')
        }
    
        return
    }
    

    还有:

    func scanAtoi(scanner *bufio.Scanner) (n int) {
        scanner.Scan()
        
        n, _ = strconv.Atoi(scanner.Text())
    
        return
    }
    

    我测试了一个大文件(40k 测试),每次测试读取大约 8 个整数。 fmt.Scanf 解决方案需要大约 1.9 秒,正如预期的那样(比其他解决方案更多)。 在这两个函数中,我得到了大约 0.8 秒。但是scanAtoi 总是比scanFromBytes 少大约0.05s,除了第一次(可能会发生一些缓存)。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 2013-02-04
      • 1970-01-01
      • 1970-01-01
      • 2019-01-18
      • 1970-01-01
      • 2017-09-04
      • 1970-01-01
      相关资源
      最近更新 更多