在Swift数组中如何计算一个元素出现的次数?

97

我看过一些类似的例子,但所有这些例子似乎都依赖于知道要统计出现次数的元素。我的数组是动态生成的,所以我无法知道要统计哪些元素的出现次数(我想统计它们全部的出现次数)。有人能给予建议吗?

编辑:

也许我应该更清楚一点,数组将包含多个不同的字符串(例如 ["FOO", "FOO", "BAR", "FOOBAR"]

如何在不事先知道它们是什么的情况下统计foo、bar和foobar的出现次数呢?


不要将Swift数组与NSArray混淆。它们并不相同。 - Juan Catalan
16个回答

166

Swift 3 和 Swift 2:

您可以使用类型为 [String: Int] 的字典来累计每个 [String] 中的项目计数:

let arr = ["FOO", "FOO", "BAR", "FOOBAR"]
var counts: [String: Int] = [:]

for item in arr {
    counts[item] = (counts[item] ?? 0) + 1
}

print(counts)  // "[BAR: 1, FOOBAR: 1, FOO: 2]"

for (key, value) in counts {
    print("\(key) occurs \(value) time(s)")
}

输出:

BAR occurs 1 time(s)
FOOBAR occurs 1 time(s)
FOO occurs 2 time(s)

Swift 4:

Swift 4 引入 (SE-0165)了向字典查找中添加默认值的能力,而且结果值可通过 +=-= 这样的操作进行改变,因此:

counts[item] = (counts[item] ?? 0) + 1

变成:

counts[item, default: 0] += 1

这使得使用 forEach 在一行中完成计数操作变得容易:

let arr = ["FOO", "FOO", "BAR", "FOOBAR"]
var counts: [String: Int] = [:]

arr.forEach { counts[$0, default: 0] += 1 }

print(counts)  // "["FOOBAR": 1, "FOO": 2, "BAR": 1]"

Swift 4: reduce(into:_:)

Swift 4引入了一个新版本的reduce,它使用一个inout变量来累积结果。使用它,计数器的创建真正成为了一行代码:

let arr = ["FOO", "FOO", "BAR", "FOOBAR"]
let counts = arr.reduce(into: [:]) { counts, word in counts[word, default: 0] += 1 }

print(counts)  // ["BAR": 1, "FOOBAR": 1, "FOO": 2]

或使用默认参数:

let counts = arr.reduce(into: [:]) { $0[$1, default: 0] += 1 }

最后,您可以将其作为Sequence的扩展,以便可以在包含Hashable项(包括ArrayArraySliceStringString.SubSequence)的任何Sequence上调用它:
extension Sequence where Element: Hashable {
    var histogram: [Element: Int] {
        return self.reduce(into: [:]) { counts, elem in counts[elem, default: 0] += 1 }
    }
}

这个想法借鉴自这个问题,但我将其改为了一个计算属性。感谢@LeoDabus建议扩展Sequence而不是Array以获取其他类型。 示例:
print("abacab".histogram)
["a": 3, "b": 2, "c": 1]
print("Hello World!".suffix(6).histogram)
["l": 1, "!": 1, "d": 1, "o": 1, "W": 1, "r": 1]
print([1,2,3,2,1].histogram)
[2: 2, 3: 1, 1: 2]
print([1,2,3,2,1,2,1,3,4,5].prefix(8).histogram)
[1: 3, 2: 3, 3: 2]
print(stride(from: 1, through: 10, by: 2).histogram)
[1: 1, 3: 1, 5: 1, 7: 1, 9: 1]

3
字典查找返回一个可选值,因为键可能不存在于字典中。您需要解包该可选值才能使用它。在这里,我使用了nil 合并运算符 **??**,它表示如果存在值,则解包该值,否则使用提供的默认值 0。第一次遇到字符串时,它不会出现在 counts 字典中,因此 count["FOO"] 将返回 nil,而 ?? 将其转换为 0。下次我们遇到 "FOO" 时,count["FOO"] 将返回 Optional(1),而 ?? 将其解开为 1 - vacawama
如果我想要处理自定义模型的数组,该怎么办? - A.s.ALI
1
@A.s.ALI,请让您的模型类/结构符合“Hashable”协议。 - vacawama
@A.s.ALI. 实现 hash(into:)==。例如,参见此答案 - vacawama
1
@vacawama 最好扩展“Sequence”以支持字符串和子序列。 - Leo Dabus
显示剩余7条评论

161
array.filter{$0 == element}.count

11
为什么人们不认为这是最佳答案,对我来说完全无法理解。 - ClayJ
4
到目前为止最简洁的答案。 - 36 By Design
14
因为这个问题(至少在当前的形式下)要求获取每个元素的计数。这对于一个特定元素的计数非常好。 - Quinn Taylor
6
虽然这个解决方案非常优雅,但效率可能较低,因为它创建了第二个数组,然后计算其元素数量。更高效的解决方案可以是 var count = 0 array.forEach { x in if x == element { count += 1 }} - Cristina De Rito
4
为什么这个回答会得到投票支持?它根本没有回答原问题。 - bojan
显示剩余7条评论

55

使用 Swift 5,根据您的需求,您可以选择以下7个Playground示例代码之一来计算数组中可哈希项的出现次数。


#1. 使用Arrayreduce(into:_:)Dictionarysubscript(_:default:)下标

let array = [4, 23, 97, 97, 97, 23]
let dictionary = array.reduce(into: [:]) { counts, number in
    counts[number, default: 0] += 1
}
print(dictionary) // [4: 1, 23: 2, 97: 3]

#2. 使用repeatElement(_:count:)函数、zip(_:_:)函数和Dictionaryinit(_:uniquingKeysWith:)初始化器


let array = [4, 23, 97, 97, 97, 23]

let repeated = repeatElement(1, count: array.count)
//let repeated = Array(repeating: 1, count: array.count) // also works

let zipSequence = zip(array, repeated)

let dictionary = Dictionary(zipSequence, uniquingKeysWith: { (current, new) in
    return current + new
})
//let dictionary = Dictionary(zipSequence, uniquingKeysWith: +) // also works

print(dictionary) // prints [4: 1, 23: 2, 97: 3]
#3.使用字典的初始化器init(grouping: by:)和方法mapValues(_:)
let array = [4, 23, 97, 97, 97, 23]

let dictionary = Dictionary(grouping: array, by: { $0 })

let newDictionary = dictionary.mapValues { (value: [Int]) in
    return value.count
}

print(newDictionary) // prints: [97: 3, 23: 2, 4: 1]

#4. 使用Dictionaryinit(grouping:by:)初始化器和map(_:)方法


let array = [4, 23, 97, 97, 97, 23]

let dictionary = Dictionary(grouping: array, by: { $0 })

let newArray = dictionary.map { (key: Int, value: [Int]) in
    return (key, value.count)
}

print(newArray) // prints: [(4, 1), (23, 2), (97, 3)]
#5. 使用 for 循环和 Dictionary 的 subscript(_:) 下标
extension Array where Element: Hashable {

    func countForElements() -> [Element: Int] {
        var counts = [Element: Int]()
        for element in self {
            counts[element] = (counts[element] ?? 0) + 1
        }
        return counts
    }

}

let array = [4, 23, 97, 97, 97, 23]
print(array.countForElements()) // prints [4: 1, 23: 2, 97: 3]
#6. 使用 NSCountedSetNSEnumeratormap(_:) 方法(需要 Foundation)
import Foundation

extension Array where Element: Hashable {

    func countForElements() -> [(Element, Int)] {
        let countedSet = NSCountedSet(array: self)
        let res = countedSet.objectEnumerator().map { (object: Any) -> (Element, Int) in
            return (object as! Element, countedSet.count(for: object))
        }
        return res
    }

}

let array = [4, 23, 97, 97, 97, 23]
print(array.countForElements()) // prints [(97, 3), (4, 1), (23, 2)]
#7. 使用NSCountedSetAnyIterator(需要Foundation库)
import Foundation

extension Array where Element: Hashable {

    func counForElements() -> Array<(Element, Int)> {
        let countedSet = NSCountedSet(array: self)
        var countedSetIterator = countedSet.objectEnumerator().makeIterator()
        let anyIterator = AnyIterator<(Element, Int)> {
            guard let element = countedSetIterator.next() as? Element else { return nil }
            return (element, countedSet.count(for: element))
        }
        return Array<(Element, Int)>(anyIterator)
    }

}

let array = [4, 23, 97, 97, 97, 23]
print(array.counForElements()) // [(97, 3), (4, 1), (23, 2)]

致谢:


2
太棒了! - rmvz3
1
全面而深思熟虑。赞! - Juan Felipe Gallo

11

我更新了oisdk的回答以适用于Swift2。

16/04/14 我将此代码更新为Swift2.2。

16/10/11 更新为Swift3。


可哈希:

extension Sequence where Self.Iterator.Element: Hashable {
    private typealias Element = Self.Iterator.Element

    func freq() -> [Element: Int] {
        return reduce([:]) { (accu: [Element: Int], element) in
            var accu = accu
            accu[element] = accu[element]?.advanced(by: 1) ?? 1
            return accu
        }
    }
}

可比较的:

extension Sequence where Self.Iterator.Element: Equatable {
    private typealias Element = Self.Iterator.Element

    func freqTuple() -> [(element: Element, count: Int)] {

        let empty: [(Element, Int)] = []

        return reduce(empty) { (accu: [(Element, Int)], element) in
            var accu = accu
            for (index, value) in accu.enumerated() {
                if value.0 == element {
                    accu[index].1 += 1
                    return accu
                }
            }

            return accu + [(element, 1)]
        }
    }
}

使用方法

let arr = ["a", "a", "a", "a", "b", "b", "c"]
print(arr.freq()) // ["b": 2, "a": 4, "c": 1]
print(arr.freqTuple()) // [("a", 4), ("b", 2), ("c", 1)]

for (k, v) in arr.freq() {
    print("\(k) -> \(v) time(s)")
}
// b -> 2 time(s)
// a -> 4 time(s)
// c -> 1 time(s)

for (element, count) in arr.freqTuple() {
    print("\(element) -> \(count) time(s)")
}
// a -> 4 time(s)
// b -> 2 time(s)
// c -> 1 time(s)

1
一个不错的解决方案 - 我借鉴了这个方法 - 但不幸的是,Swift 语法将很快(对于 v.3)被更改以禁止在函数参数中使用 var。我尝试着修复它,但到目前为止还没有成功... - MassivePenguin
1
@MassivePenguin 谢谢!我已经更新了这个答案到Swift2.2,请使用它 :) - ken0nek

4
使用NSCountedSet。在Objective-C中:
NSCountedSet* countedSet = [[NSCountedSet alloc] initWithArray:array];
for (NSString* string in countedSet)
    NSLog (@"String %@ occurs %zd times", string, [countedSet countForObject:string]);

我认为您可以自己将其翻译成Swift。

4
如何考虑:
func freq<S: SequenceType where S.Generator.Element: Hashable>(seq: S) -> [S.Generator.Element:Int] {

  return reduce(seq, [:]) {

    (var accu: [S.Generator.Element:Int], element) in
    accu[element] = accu[element]?.successor() ?? 1
    return accu

  }
}

freq(["FOO", "FOO", "BAR", "FOOBAR"]) // ["BAR": 1, "FOOBAR": 1, "FOO": 2]

这个操作是通用的,只要您的元素可以散列,它就可以使用:

freq([1, 1, 1, 2, 3, 3]) // [2: 1, 3: 2, 1: 3]

freq([true, true, true, false, true]) // [false: 1, true: 4]

如果您不能使元素可哈希,则可以使用元组来实现:

func freq<S: SequenceType where S.Generator.Element: Equatable>(seq: S) -> [(S.Generator.Element, Int)] {

  let empty: [(S.Generator.Element, Int)] = []

  return reduce(seq, empty) {

    (var accu: [(S.Generator.Element,Int)], element) in

    for (index, value) in enumerate(accu) {
      if value.0 == element {
        accu[index].1++
        return accu
      }
    }

    return accu + [(element, 1)]

  }
}

freq(["a", "a", "a", "b", "b"]) // [("a", 3), ("b", 2)]

4

Swift 4

let array = ["FOO", "FOO", "BAR", "FOOBAR"]

// Merging keys with closure for conflicts
let mergedKeysAndValues = Dictionary(zip(array, repeatElement(1, count: array.count)), uniquingKeysWith: +) 

// mergedKeysAndValues is ["FOO": 2, "BAR": 1, "FOOBAR": 1]

4

我喜欢避免使用内部循环,尽可能多地使用.map。因此,如果我们有一个字符串数组,我们可以按照以下方式计算出现次数:

var occurances = ["tuples", "are", "awesome", "tuples", "are", "cool", "tuples", "tuples", "tuples", "shades"]

var dict:[String:Int] = [:]

occurances.map{
    if let val: Int = dict[$0]  {
        dict[$0] = val+1
    } else {
        dict[$0] = 1
    }
}

打印
["tuples": 5, "awesome": 1, "are": 2, "cool": 1, "shades": 1]

2
另一种方法是使用过滤器方法。我认为这是最优雅的方法。
var numberOfOccurenses = countedItems.filter(
{
    if $0 == "FOO" || $0 == "BAR" || $0 == "FOOBAR"  {
        return true
    }else{
        return false
    }
}).count

2
你可以使用这个函数来统计数组中每个元素出现的次数。最初的回答包含了如何使用该函数。
func checkItemCount(arr: [String]) {       
    var dict = [String: Any]()

    for x in arr {  
        var count = 0 
        for y in arr {
            if y == x {
                count += 1
            }
        }

        dict[x] = count
    }

    print(dict)
}

你可以这样实现 -
let arr = ["FOO", "FOO", "BAR", "FOOBAR"]
checkItemCount(arr: arr)

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