【问题标题】:What is the BigO of Swift's String.count?Swift 的 String.count 的 BigO 是什么?
【发布时间】:2025-12-20 21:25:11
【问题描述】:

swift使用String.count的时候是:

O(n) 每次调用它时,我们都会遍历整个 String 以对其进行计数

O(1),其中 swift 之前存储了这个数组的大小并简单地访问它。

【问题讨论】:

  • 肯定是O(n)。来自Swift Book:“请注意,count 属性必须遍历整个字符串中的 Unicode 标量,以确定该字符串的字符”。唯一的问题是,一旦确定了count 并且字符串没有被修改,是否有缓存机制
  • @CodeDifferent 有趣。该段似乎暗示Array.count 将是O(1) 如果每个元素占用相同数量的空间。
  • @Deco 该段专门针对String.count 而不是Array.count。尽管 String 在 Swift 4 中实际上是 Array<Character>,但您永远不应忘记,由于 Unicode 字形簇和普通字符的性质,它的行为并不总是像普通的 Array。这对于索引和计数String 中的字符尤其重要。请参阅同一文档中的这句话:“因此,如果不遍历字符串以确定其扩展的字素簇边界,就无法计算字符串中的字符数。”
  • 从技术上讲,O(n) 是一个渐近的上限,所以即使String 确实缓存了count,描述时间在技术上仍然是正确的复杂性为O(n) ;) 此外,the documentation for Collection 表示如果集合符合RandomAccessCollectioncount 的性能为“O(1);否则为O(n),其中@987654343 @ 是集合的长度”——所以鉴于String 不是RandomAccessCollection,它是O(n)
  • @Honey A RandomAccessCollection 是一个集合,其索引可以在O(1) 时间偏移n 位置。因为count 是衡量您可以将startIndex 偏移多少次以最终到达endIndex 的量度,所以随机访问集合总是可以在恒定时间内获得它们的计数。但是,作为随机访问集合并不一定意味着具有整数索引。例如,ReversedCollection<[Int]>RandomAccessCollection(感谢条件一致性),但它具有跟踪基本集合索引的不透明索引类型。

标签: arrays swift big-o


【解决方案1】:

肯定是O(n)。来自Swift Book

因此,如果不遍历字符串以确定其扩展字素簇边界,就无法计算字符串中的字符数。如果您正在处理特别长的字符串值,请注意count 属性必须遍历整个字符串中的 Unicode 标量才能确定该字符串的字符。

这有一些影响,其中最大的一个是整数下标(即str[5])在标准库中不可用。在内部,String 使用 ASCII 或 UTF-16 编码(从 Swift 5 开始,它使用 UTF-8 only)。如果字符串仅使用 ASCII 字符,则 count 可以是 O(1) 但 ASCII 只有 127 个字符,因此请将此视为例外而不是规则。

另一方面,NSString 始终使用 UTF-16,因此访问其lengthO(1)。还要记住NSString.length != String.count(尝试带有表情符号的字符串,你会看到)。

至于您的第二个问题,它不会缓存count 以供后续调用。因此,每次调用count 都是O(n),即使字符串没有改变。 Foundation repo 中的代码也证实了这一点。

【讨论】:

    【解决方案2】:

    在未能找到关于此的文档或无法在源代码中找到此函数后,我自己使用如下所述的性能测试对其进行了测试。它假设 O(1) 是可能的基于 PHP's Array 是 O(1)。 Swifts String.count 函数似乎是 O(n)

    结果

    count 在之前被调用时是否已缓存? (否)

    我还测试了一次调用String.count 是否会缓存它。通过比较已经调用 count 和存储到变量中的结果,以确保在我们的正常测试中调用 .count 之前它没有被存储。

    测试

    import XCTest
    
    class CountTests: XCTestCase {
    
        func test100K() {
            let testString = String(repeating: "a", count: 100000)
            self.measure {
                _ = testString.count
            }
        }
    
        func test1000K() {
            let testString = String(repeating: "a", count: 1000000)
            self.measure {
                _ = testString.count
            }
        }
    
        func test10000K() {
            let testString = String(repeating: "a", count: 10000000)
            self.measure {
                _ = testString.count
            }
        }
    
        func test10000KCached() {
            let testString = String(repeating: "a", count: 10000000)
            _ = testString.count
            self.measure {
                _ = testString.count
            }
        }
    
        func test10000KStrong() {
            let testString = String(repeating: "a", count: 10000000)
            let count = testString.count
            self.measure {
                _ = count
            }
        }
    }
    

    【讨论】:

    • 这些测试甚至没有任何意义,因为你没有改变testString.count,只是改变你调用testString.count的次数......这与渐近时间复杂度无关String.count.
    • @DávidPásztor 你说得对,谢谢。我已经更新了我的答案,这准确吗?看起来这是O(n)
    • 您更新的测试至少确实考虑了输入的大小,但它们仍然不是真正的结论,因为您只测试纯 ASCII 字符串。此外,由于它是在问题本身的 cmets 中建立的,文档确实指出 String.countO(n),因此无需运行任何测试
    • PHP 的字符串有O(1) 计数和索引也就不足为奇了。它使用固定长度(8 位)的代码单元,不支持 Unicode。
    【解决方案3】:

    根据快速 Playground 测试,对我来说看起来像 O(n)。

    for step in 1...10 {
        let length = step * 100000
        let string = String(repeating: "x", count: length)
        let start = Date()
        let stringLength = string.count
        let end = Date()
        print("Length: \(stringLength), time: \(end.timeIntervalSince(start))")
    }
    
    // Length: 100000, time: 0.00178205966949463
    // Length: 200000, time: 0.00132298469543457
    // Length: 300000, time: 0.00184988975524902
    // Length: 400000, time: 0.00218689441680908
    // Length: 500000, time: 0.00302803516387939
    // Length: 600000, time: 0.00368499755859375
    // Length: 700000, time: 0.0039069652557373
    // Length: 800000, time: 0.00444602966308594
    // Length: 900000, time: 0.0052180290222168
    // Length: 1000000, time: 0.00539696216583252
    

    【讨论】:

    • 切勿使用 Playground 进行任何类型的性能测试。
    • 您并没有真正检查count。您还在检查生成数据需要多长时间。无论如何,您只检查 ASCII 字符串,考虑到实现,这是一个错误。
    • 诚然又快又脏,但字符串是在我捕获开始时间之前创建的。
    • @closetCoder 即使您的测试代码不是快速和肮脏,您也不应该评估 Playground 中的性能,因为 Playground 执行或在真实设备上执行之间可能存在巨大的性能差异,无论哪种您定位的 Apple 平台。
    • @Honey 因为操场在幕后做着各种各样的工作。这不是一个公平的测试。