这道LeetCode 398题的核心是设计一个支持随机索引查询的数据结构。给定一个可能包含重复元素的数组,我们需要快速找到特定目标值的所有出现位置,并能够以均等概率返回其中任意一个索引。
实际开发中这类需求非常常见。比如:
最直观的做法是每次调用pick()时遍历整个数组:
swift复制func pick(_ target: Int) -> Int {
var indices = [Int]()
for (i, num) in nums.enumerated() {
if num == target {
indices.append(i)
}
}
return indices.randomElement()!
}
时间复杂度:每次pick()都是O(n)
空间复杂度:O(1)
当pick()被频繁调用时(如题目提示的10^4次),这种解法会导致O(n^2)的总时间复杂度,显然不可取。
更聪明的做法是在初始化阶段就建立好索引:
swift复制class Solution {
private var indexMap: [Int: [Int]] = [:]
init(_ nums: [Int]) {
for (i, num) in nums.enumerated() {
indexMap[num, default: []].append(i)
}
}
func pick(_ target: Int) -> Int {
let indices = indexMap[target]!
return indices[Int.random(in: 0..<indices.count)]
}
}
这种方案的特点是:
使用字典存储索引映射时需要考虑:
实测表明,Swift的Dictionary在元素数量<10^5时性能优异,完全满足本题需求。
Int.random(in: 0..<indices.count)使用的是Swift的系统级随机数生成器,它:
注意:不要使用
%运算来限制随机数范围,这会导致分布不均匀。正确的做法就是使用..<范围运算符。
虽然题目保证target一定存在,但实际工程中应该:
swift复制func pick(_ target: Int) -> Int? {
guard let indices = indexMap[target], !indices.isEmpty else {
return nil
}
return indices[Int.random(in: 0..<indices.count)]
}
| 操作 | 暴力解法 | 预处理解法 |
|---|---|---|
| 初始化 | O(1) | O(n) |
| 单次pick() | O(n) | O(1) |
| m次pick() | O(mn) | O(n+m) |
当m>1时,预处理方案优势明显。
预处理方案需要额外O(n)空间存储索引映射,这是典型的空间换时间策略。
如果需要支持数组动态修改,可以扩展为:
swift复制class DynamicSolution {
private var indexMap: [Int: [Int]] = [:]
private var nums: [Int]
init(_ nums: [Int]) {
self.nums = nums
// 初始化索引...
}
func update(index: Int, value: Int) {
let oldValue = nums[index]
// 从旧值的索引列表中移除
if let i = indexMap[oldValue]?.firstIndex(of: index) {
indexMap[oldValue]?.remove(at: i)
}
// 添加到新值的索引列表
indexMap[value, default: []].append(index)
nums[index] = value
}
}
如果需要支持不同索引有不同的选择权重,可以改造为:
swift复制struct WeightedIndex {
let index: Int
let weight: Double
}
class WeightedSolution {
private var indexMap: [Int: [WeightedIndex]] = [:]
func pickWeighted(_ target: Int) -> Int {
let indices = indexMap[target]!
let totalWeight = indices.reduce(0) { $0 + $1.weight }
let random = Double.random(in: 0..<totalWeight)
var sum = 0.0
for item in indices {
sum += item.weight
if random < sum {
return item.index
}
}
return indices.last!.index
}
}
完整的测试应该包括:
swift复制func testSolution() {
// 基础用例
let nums1 = [1,2,3,3,3]
let sol1 = Solution(nums1)
testPick(sol1, target: 3, possible: [2,3,4])
// 单元素数组
let nums2 = [5]
let sol2 = Solution(nums2)
assert(sol2.pick(5) == 0)
// 无重复元素
let nums3 = [10,20,30]
let sol3 = Solution(nums3)
assert(sol3.pick(20) == 1)
// 大规模数据
let nums4 = Array(repeating: 1, count: 10000) + [2]
let sol4 = Solution(nums4)
let start = Date()
_ = sol4.pick(1)
let time = Date().timeIntervalSince(start)
assert(time < 0.001) // 确保O(1)时间复杂度
}
func testPick(_ sol: Solution, target: Int, possible: [Int]) {
var counts = [Int: Int]()
let testCount = 10000
for _ in 0..<testCount {
let index = sol.pick(target)
counts[index, default: 0] += 1
}
let expected = Double(testCount) / Double(possible.count)
for index in possible {
let ratio = Double(counts[index] ?? 0) / expected
assert(abs(ratio - 1.0) < 0.1) // 误差<10%
}
}
在实际工程中选择解法时需要考虑:
数据特征:
操作模式:
资源限制:
掌握这个思路可以解决一系列类似问题:
LeetCode 382 链表随机节点:
LeetCode 528 按权重随机选择:
随机抽样系统设计:
内存优化:
并发安全:
持久化存储:
监控统计:
内存布局优化:
swift复制// 使用ContiguousArray提升数组访问性能
private var indexMap: [Int: ContiguousArray<Int>] = [:]
随机数生成优化:
swift复制// 预先生成随机数序列
private var randomBuffer: [Int] = []
private var randomIndex = 0
func pregenerateRandomNumbers(count: Int) {
randomBuffer = (0..<count).map { _ in
Int.random(in: 0..<Int.max)
}
}
批量处理优化:
swift复制func batchPick(target: Int, count: Int) -> [Int] {
let indices = indexMap[target]!
return (0..<count).map { _ in
indices[Int.random(in: 0..<indices.count)]
}
}
Swift特有的语言特性可以进一步优化代码:
属性包装器:
swift复制@propertyWrapper
struct RandomSelectable<T> {
private var values: [T]
init(wrappedValue: [T]) {
self.values = wrappedValue
}
var wrappedValue: [T] {
get { values }
set { values = newValue }
}
var projectedValue: T {
values.randomElement()!
}
}
泛型扩展:
swift复制extension Array where Element: Hashable {
func createIndexMap() -> [Element: [Int]] {
var map = [Element: [Int]]()
for (i, item) in self.enumerated() {
map[item, default: []].append(i)
}
return map
}
}
Result Builder:
swift复制@resultBuilder
struct RandomPickerBuilder {
static func buildBlock(_ components: Int...) -> Int {
return components.randomElement()!
}
}
完善的测试应该包括:
单元测试:
性能测试:
模糊测试:
支持更多查询类型:
分布式扩展:
机器学习集成:
命名改进:
swift复制class RandomIndexSelector {
private var valueToIndicesMap: [Int: [Int]]
init(data: [Int]) {
// ...
}
func randomlySelectIndex(for target: Int) -> Int {
// ...
}
}
文档注释:
swift复制/// 一个支持等概率随机选择目标值索引的数据结构
///
/// 使用说明:
/// 1. 使用init(data:)初始化
/// 2. 调用randomlySelectIndex(for:)获取随机索引
/// - Note: 保证target必须存在于初始化数据中
class RandomIndexSelector {
// ...
}
日志记录:
swift复制func randomlySelectIndex(for target: Int) -> Int {
let indices = valueToIndicesMap[target]!
let selected = indices[Int.random(in: 0..<indices.count)]
Logger.debug("Selected index \(selected) for target \(target)")
return selected
}
在实际项目中使用这类随机选择功能时,我有几点经验分享:
预处理时机的选择:
随机性质量评估:
性能权衡建议:
100万:评估是否需要分片或近似算法
错误处理经验:
这个随机索引选择问题虽然表面简单,但深入探究涉及数据结构设计、概率统计、性能工程等多个方面。掌握这类基础算法的优化思路,对提升工程实践能力大有裨益。