在 Swift 字典中,是否可以使用范围作为键?

5

为了简化,假设我有一些唯一的值——从1到10的数字。

现在我想把1-5映射到值“first”,并且我希望把6-10映射到值“second”。

是否有一种方法可以创建或扩展一个字典来实现以下功能?

let dict: [Range<Int> : String]

目标是获得以下结果:

print(dict[1]) // prints first
print(dict[2]) // prints first
print(dict[3]) // prints first
print(dict[7]) // prints second
print(dict[8]) // prints second
print(dict[9]) // prints second

我目前的做法是将多个键映射到同一个值。但我的字典有时可能会有6万个值。所以我想知道是否可以使用范围。

我知道我可以将值变成class,而不是struct,这样多个键就可以映射到同一个类对象,但我想知道是否可以简单地创建一个像上面那样工作的字典?


你提出的数据结构存在问题,即需要处理具有重叠范围的“键”的情况 - 在这些范围的交集中,值应该映射到什么值? - Hamish
请注意,在字典的上下文中,符合Hashable协议的实例的哈希值仅用于将keys放置在不同的桶中(基于哈希值),只有在尝试访问具有多个键的桶时,字典才会继续测试实际正在搜索的唯一键的相等性(所有这些都是为了允许字典通过键进行摊销O(1)访问值)。这意味着(对于理论上扩展到HashableRange),0...10是一个唯一的键,1...10也是一个唯一的键,即使它们的哈希值相同。 - dfrib
1
不是特定于Swift,但是这个问答可能是一个很好的起点。 - Hamish
2个回答

8

如果您坚持使用Dictionary,则必须等待Swift 3.1(目前处于beta版):

extension CountableClosedRange : Hashable {
    public var hashValue: Int {
        return "\(lowerBound) to \(upperBound)".hashValue
    }
}

// This feature is called concrete-type extension and requires Swift 3.1
extension Dictionary where Key == CountableClosedRange<Int> {
    subscript(rawValue rawValue: Int) -> Value? {
        for k in self.keys {
            if k ~= rawValue {
                return self[k]
            }
        }

        return nil
    }
}

let dict : [CountableClosedRange<Int>: String] = [
    1...5: "first",
    6...10: "second"
]

print(dict[rawValue: 1])
print(dict[rawValue: 2])
print(dict[rawValue: 3])
print(dict[rawValue: 7])
print(dict[rawValue: 8])
print(dict[rawValue: 9])

然而,如果你实现自己的数据模型,那么就会更加清晰:

struct MyRange {
    var ranges = [CountableClosedRange<Int>]()
    var descriptions = [String]()

    mutating func append(range: CountableClosedRange<Int>, description: String) {
        // You can check for overlapping range here if you want
        self.ranges.append(range)
        self.descriptions.append(description)
    }

    subscript(value: Int) -> String? {
        for (i, range) in self.ranges.enumerated() {
            if range ~= value {
                return descriptions[i]
            }
        }

        return nil
    }
}

var range = MyRange()
range.append(range: 1...5, description: "one")
range.append(range: 6...10, description: "second")

print(range[1])
print(range[2])
print(range[6])
print(range[7])
print(range[100])

上面的回答很好,但最好明确指出使用上述新下标实现时按键访问的值将是O(n)而不是摊销的O(1)(因为人们可能期望字典的后者)。此外,如果您想要全面地进行函数式编程,可以将上述subscript实现的主体压缩为单个表达式:return keys.first(where: { $0.1 ~= rawValue }).flatMap { self[$0.0] }return ranges.enumerated().first(where: { $0.1 ~= value }).flatMap { descriptions[$0.0] }分别用于顶部和底部的实现。 - dfrib
将此与应用于已排序列表的二分搜索方法进行比较(我相信OP指出了非重叠/唯一范围),其访问的最坏情况性能为O(log n) - dfrib
@dfri O(log n) 嗯.. 这意味着二分查找方法会更快。我正在寻找如何使用他的解决方案,但使搜索变为二进制。 - Just a coder
确实,这是一种访问方式。但是二分查找方法需要排序(例如,O(n log n))来最初构建范围列表(对于新插入到列表中的元素,需要O(n))。此外,仅当范围不重叠时,才适用二分查找方法。在上面的自定义方法中(第二个片段),您可以简单地确保“ranges”数组已排序,并在调用“subscript”时对其应用二分查找。然而,您可能需要以某种方式确定所有“ranges”都不重叠(检查需要O(n)),可能是每次向“ranges”添加新元素时。 - dfrib
由于将新元素添加到已排序的列表中(并保持其排序)已经是O(n),因此在插入新的(Range)元素时,您可以轻松执行这个不重叠的检查(因为您知道列表已排序)。 - dfrib

3

这是在Swift 3.0中的代码,可能不如Code Different的答案那么好看。

class MyRange: Hashable, Equatable {
    public var hashValue: Int {
        get {
            return (self.range.lowerBound + self.range.upperBound).hashValue
        }
    }

    var range: Range<Int>!

    public static func ==(_ lhs: MyRange, _ rhs: MyRange) -> Bool {
        return lhs.range == rhs.range
    }

    init(range: Range<Int>) {
        self.range = range
    }
}


extension Dictionary where Key: MyRange, Value: ExpressibleByStringLiteral {
    internal subscript(index: Int) -> [String] {
        return self.filter({$0.key.range.contains(index)}).map({$0.value as! String})
    }
}

现在,您可以像这样创建自己的字典:
var dict = Dictionary<MyRange, String>()
dict[MyRange(range: 0..<5)] = "first"
dict[MyRange(range: 5..<10)] = "second"

获取值可以使用整数和范围:

print(dict[1]) // ["first"]
print(dict[5]) // ["second"]
print(dict[11]) // []

print(dict[MyRange(range: 0..<5)]) // "first"
print(dict[MyRange(range: 0..<6)]) // nil

字典应该长成这样:
print(dict)
// [MyRange: "first", MyRange: "second"]

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接