1

I have a sorted array. I would like to iterate through the array and increment a counter as I find pairs of values. I'm not finding an elegant solution to this.

var pairs = 0
    let colors = [10, 20, 20, 10, 10, 30, 50, 10, 20
    let sortedColors = colors.sorted{ $0 < $1}
    // [10, 10, 10, 10, 20, 20, 20, 30, 50] -> pairs should equal 3

    for i in 0..<colors.count - 1 {
        if sortedColors[i+1] != colors.count && sortedColors[i] == sortedColors[i+1] {
            pairs += 1
        } 
    }

print(pairs)
Martin Muldoon
  • 3,388
  • 4
  • 24
  • 55
  • When you say "pairs" do you mean values that occur more than once? Can you define what a "pair of values" is. It's hard to tell from your question. :) – Fogmeister Mar 13 '18 at 16:44
  • values that occur twice. [10,10,10,10] would be two pairs. pairs == 2 – Martin Muldoon Mar 13 '18 at 16:46
  • Sorry, maybe I was a bit quick to dupe-mark this one. Should I re-open? – dfrib Mar 13 '18 at 16:46
  • Surely that should be three pairs? (0, 1) (1, 2) (2, 3) are all pairs? OK... in the array [1, 2, 2, 3] is that 1 pair or 0? – Fogmeister Mar 13 '18 at 16:47
  • @dfri I think so :) – Fogmeister Mar 13 '18 at 16:47
  • Check this answer https://stackoverflow.com/questions/40841663/swift-whats-the-best-way-to-pair-up-elements-of-an-array –  Mar 13 '18 at 16:47
  • @Fogmeister. Sorry I'm not clear. in your array.. [1,2,2,3] that is one pair of matching numbers. One pair of twos. – Martin Muldoon Mar 13 '18 at 16:51
  • So Martin, in your example, the **3** pairs are `(10, 10)`, `(10, 10)` and `(20, 20)`? – dfrib Mar 13 '18 at 16:52
  • No worries :D Just trying to work out why the middle two tens [10, (10, 10), 10] in your array is not counted as a pair :) So a pair is any number followed by the same number but the second number is then excluded from the check for the third? – Fogmeister Mar 13 '18 at 16:53
  • Yes. So [2,2,2,1] only contains one pair. Index 0 and index 1 match up. Index 2 would have no match. – Martin Muldoon Mar 13 '18 at 16:54

3 Answers3

3

You could as well use new Dictionary syntax like so,

With grouping syntax,

let pairs = Dictionary(grouping: colors){ $0 }
                        .map { $1.count / 2 }
                        .reduce(0, +)
print(pairs)

With uniquing syntax,

let pairs = Dictionary( zip( colors, Array(repeating: 1, count: colors.count)),
                       uniquingKeysWith: +)
                      .reduce(0, { $0 + $1.1 / 2})
Sandeep
  • 20,908
  • 7
  • 66
  • 106
  • 1
    Beauty. Nicest answer here, IMO. – Alexander Mar 13 '18 at 18:05
  • @Alexander It does look beautiful indeed. Thanks ! – Sandeep Mar 13 '18 at 18:13
  • @Alexander I did it now with uniquing syntax as well :) – Sandeep Mar 13 '18 at 18:24
  • `Array(repeating: 1, count: colors.count)` could just be `[colors.count]` – Alexander Mar 13 '18 at 18:48
  • 1
    I don't get how the second example works, it seems quite unclear – Alexander Mar 13 '18 at 18:49
  • 1
    @Alexander I also agree that this is neat, but it should be noted that it might not be as efficient as the other two answers, as it unnecessarily creates a number of arrays (the values of the `Dictionary`; e.g. `[50: [50], 10: [10, 10, 10, 10], 20: [20, 20, 20], 30: [30]]`) all having the same member repeated a number of times, where the sole information used in these same-element arrays is their count. Nice nonetheless with an alternative approach to the "frequency counting" ones of the other answer! The second approach above (although not as neat) is kind of clever though. – dfrib Mar 13 '18 at 19:04
  • Well, I agree that this might not be efficient as others. But, code looks neat and a bit more swifty to say :) – Sandeep Mar 13 '18 at 19:05
  • @dfri Yeah, I wish there was a `lazy` variant of grouping, or perhaps one that lets you instantly `mapValues` inplace – Alexander Mar 13 '18 at 19:09
  • 1
    First one, a bit tighter: `let pairs = Dictionary(grouping: colors){ $0 }.map { $1.count / 2 }.reduce(0, +)` – vacawama Mar 13 '18 at 19:19
  • 1
    @vacawama I wanted to write exactly the same! A single `Dictioary.reduce` call could be used but I like it better this way. Or just a `map` over `.values`. – Sulthan Mar 13 '18 at 19:20
2

I would just count the repetitions and then divide the number of repetitions by 2 to count the pairs. For example, if a number appears 3 times, there is one pair:

let colors = [10, 20, 20, 10, 10, 30, 50, 10, 20]

let countedSet = NSCountedSet(array: colors)
let pairs = countedSet.map { countedSet.count(for: $0) / 2 }.reduce(0, +)
print(pairs) // 3

Unfortunately, there is no Swift CountedSet yet :(

Sulthan
  • 128,090
  • 22
  • 218
  • 270
  • In your array there are only 2 pairs. 20, 20 and 10, 10. – Fogmeister Mar 13 '18 at 16:59
  • @Fogmeister There are two "10" pairs – Sulthan Mar 13 '18 at 16:59
  • Not by the rules of the OP :) Only adjacent equivalent values count as a pair. – Fogmeister Mar 13 '18 at 16:59
  • @Fogmeister There is a comment that says `[10, 10, 10, 10]` are two pairs :) – Sulthan Mar 13 '18 at 17:00
  • Yes. `array[0, 1]` and `array[2, 3]`. In your example the only pairs are `colors[1, 2]` and `colors[3, 4]`. `colors[0]` and `colors[3]` are not a pair. – Fogmeister Mar 13 '18 at 17:01
  • 1
    @Fogmeister Note that I have skipped the sorting because I don't need it, therefore I don't have to care which pairs are adjacent. – Sulthan Mar 13 '18 at 17:02
  • @Fogmeister, OP is sorting the array before looking for pairs – vacawama Mar 13 '18 at 17:02
  • 1
    I got that solution too, but used directly map on `countedSet` and didn't use `allObjects` in between. Just wondering since I'm not a Swift developer the benefits or potential issues. – Larme Mar 13 '18 at 17:11
  • 1
    @Larme I just didn't realize that I can use `map` directly. – Sulthan Mar 13 '18 at 17:12
  • @Sulthan Thanks for the information. I was wondering if it was just a Playground "allowed" stuff, CocoaTouch/Cocoa difference because I tend to use directly suggestions/autocomplete from XCode to find methods in Swift. – Larme Mar 13 '18 at 17:13
  • @Sulthan Please check my answer, I attempted to use Dictionary grouping syntax and I think this would work. – Sandeep Mar 13 '18 at 18:02
2

An alternative but similar approach as @Sulthan's answer is to use a dictionary to count occurrences rather than NSCountedSet:

let colors = [10, 20, 20, 10, 10, 30, 50, 10, 20]
let numberOfPairs = colors
  .reduce(into: [:]) { counts, num in counts[num, default: 0] += 1 }
  .reduce(0) { cumsum, kv in cumsum + kv.value / 2 } // 3

Or, using shorthand argument names in the two closures:

let numberOfPairs = colors
  .reduce(into: [:]) { $0[$1, default: 0] += 1 }
  .reduce(0) { $0 + $1.value / 2 }

Where above, for the number occurrence count, we make use of @vacawama's answer in the Q&A that I initially used as target for dupe-marking this Q&A.

dfrib
  • 70,367
  • 12
  • 127
  • 192