3

I have a simple array of a custom objects.

I would like to reduce the array to one instance of each color selected by the largest size.

The solutions I have come up with seem long an unwieldy, what would be the best approach, i've tried looking at reduce and filter but couldn't work out how to apply here.

class foo {

    var color: String
    var size: Int
    var shape: String

    init(color:String, size:Int, shape:String){
        self.color = color
        self.size = size
        self.shape = shape
    }

}

var array = [foo]()

array.append(foo(color: "Blue", size: 2, shape: "Round"))
array.append(foo(color: "Red", size: 3, shape: "Square"))
array.append(foo(color: "Blue", size: 5, shape: "Round"))
array.append(foo(color: "Yellow", size: 1, shape: "Triangle"))
array.append(foo(color: "Blue", size: 1, shape: "Hexagon"))
Magrafear
  • 573
  • 4
  • 17
  • I have seen the question that has been suggested. I have tried to adapt the solutions to my example, but to me it's not a simple as it' isn't just an array of ints. – Magrafear Feb 08 '16 at 08:49
  • 2
    you haven't shown your attempt code, or an example output – Wain Feb 08 '16 at 08:55

4 Answers4

4

You can avoid brute-force O(n^2) nested looping (and enumeration) solution by first sorting the array, and thereafter filtering out duplicate colour objects using e.g. hash value lookup (Method 1 below) or clever exclusion with regard to the sorted array (Method 2 below).

Note also class type naming convention (CamelCase): so Foo rather than foo.

Disclaimer: Don't stare yourself blind on the asymptotic complexity notations below, as premature optimization is, depending on the context and intended usage area of your program, generally something of a sin. I've included them below simply to have some measure to compare the different methods by. Choose the one that you think makes most sense to you.


Method 1

Worst case...

  • time complexity: O(n log n)

  • space complexity: O(n)

Where space complexity refers to space used in excess of the array to which the final result is assigned.

  • Let Foo conform to Hashable (let hashValue relate to .color property).
  • Sort the array of Foo instances w.r.t. decreasing size (.size property).
  • Filter the sorted array w.r.t. to first occurrence of each color, using the conformance to Hashable to swiftly use O(1) hash value lookup for existing color in a Foo:Bool dictionary. Adapted from the comments by Airspeed Velocity in the following answer.

Method 2 (as proposed by Nikolai Ruhe):

Worst case...

  • time complexity: O(n log n)

  • space complexity: O(1)

  • Sort the array on color (primary) and size (secondary).
  • Filter the sorted array for elements that has a different color than their predecessors.

For a third (probably the best one for this application) method, see Nikolai Ruhe:s answer below, presenting a method with O(n)/O(n) time/space worst case complexity, respectively.


Implementations

[This step is only needed for Method 1] Conform Foo to Hashable and Equatable:

/* Let Foo conform to Hashable */
class Foo : Hashable {

    var color: String
    var size: Int
    var shape: String

    init(color:String, size:Int, shape:String){
        self.color = color
        self.size = size
        self.shape = shape
    }

    var hashValue: Int {
        return color.hashValue
    }

}

/* And Equatable */
func ==(lhs: Foo, rhs: Foo) -> Bool {
    return lhs.color == rhs.color
}

Set up and example for the filter method(s) that follows shortly:

/* Foo array example */
var array = [Foo]()

array.append(Foo(color: "Blue", size: 2, shape: "Round"))
array.append(Foo(color: "Red", size: 3, shape: "Square"))
array.append(Foo(color: "Blue", size: 5, shape: "Round"))
array.append(Foo(color: "Yellow", size: 1, shape: "Triangle"))
array.append(Foo(color: "Blue", size: 1, shape: "Hexagon"))

Filter as per your specifications:

/* Method 1 (assumes Foo conforms to Hashable (& Equatable))   */
var addedDict = [Foo:Bool]()
var arrFiltered = array.sort{ $0.0.size > $0.1.size }
    .filter {addedDict.updateValue(true, forKey: $0) == nil }

/* Method 2 (as proposed by Nikolai Ruhe)                      */
var previousColor: String?
let arrFiltered = array.sort{ $0.color == $1.color ? $0.size > $1.size : $0.color < $1.color }
    .filter{ if $0.color != previousColor { previousColor = $0.color; return true }; return false }
    /* condensed .filter solution by @Nikolai Ruhe, thanks! */

Result:

for bar in arrFiltered {
    print(bar.color, bar.size)
}

/* Blue 5
   Red 3
   Yellow 1 */

The sorting step is the dominant step in this solution (for both methods). From swift/stdlib/public/core/Sort.swift.gyb, it seems as if Swift uses introsort (specifically, a hybrid of introsort combined with insertion sort), running in, worst case, as O(n log n).

Community
  • 1
  • 1
dfrib
  • 70,367
  • 12
  • 127
  • 192
  • Why not using `Set` and `maxElement`? :) – Eendje Feb 08 '16 at 09:56
  • @Eendje I did actually fiddle with that combination prior to resorting to sorting (no pun intended); I couldn't get the prior method to work as intended. But if it can be used, it's possibly an even swiftier solution. If you can fix that, you should add it as an answer! – dfrib Feb 08 '16 at 10:01
  • I did, but its questionable if it's better. I thought maybe you could make it better :p I'll post it as an answer to show you :) – Eendje Feb 08 '16 at 10:02
  • Do you mean `return lhs.color == rhs.color` in `func ==`? – Nikolai Ruhe Feb 08 '16 at 10:07
  • @NikolaiRuhe it's an intended lazy (lazy by me, not lazy, as the swift term) implementation, comparison of the `.hashValue` computed property equals comparison of the `.color` `String` property:s hashvalue. I'm realizing, however, that there's possibly some remote probability that two different strings return the same hash value, so I should rather make use of `.color` here instead. Thanks for the remark. – dfrib Feb 08 '16 at 10:12
  • @dfri Collisions in String's `hashValue` are very common, so comparing the hash value is clearly a mistake. – Nikolai Ruhe Feb 08 '16 at 10:28
  • @NikolaiRuhe Ok, didn't know it was as common as that, thanks. Anyway, updated to compare `.color` property after prompted by your last comment, thanks again. – dfrib Feb 08 '16 at 10:36
  • Nice catch using return value of `updateValue(...)`, I always forget this one. – Michaël Azevedo Feb 08 '16 at 14:31
3
let result = Set(array).flatMap { color in array.filter { $0 == color }.maxElement { $0.0.size < $0.1.size } }

A combination of Set and maxElement.

Since I used the examples of dfri's answer, it should be noted that the objects in array should conform to Hashable and Equatable. The answer is made just to show an alternative and personally I think the answer of dfri is a lot better (and faster).

Eendje
  • 8,815
  • 1
  • 29
  • 31
  • I was about to remove this "answer" (I just wanted to show dfri) but mind to tell me why the down vote? – Eendje Feb 08 '16 at 10:32
  • Maybe cause it's incomprehensible and cumbersome? My 2ct. – Nikolai Ruhe Feb 08 '16 at 10:37
  • I think it's a valid answer (not to be downvoted, anyway), even if it's a bit complex on its own. Eendje: possibly consider mentioning explicitly that this answer depends on `Foo` conforming `Hashable` and `Equatable` (conforming to the prior demands conforming to the latter): possibly someone tried your line of code without making certain of this conformance. – dfrib Feb 08 '16 at 10:39
  • Well like I said (as well as in the comments in dfri's answer) I was simply showing him how I used `Set` and `maxElement` together because posting it in the comment would be a bit hard to read. But I guess even for showing is worth a down vote. – Eendje Feb 08 '16 at 10:40
  • @dfri: Yea thats why I mentioned I was using your answer as base :) But I was planning on removing this answer either way :) – Eendje Feb 08 '16 at 10:42
  • @Eendje I think you should keep this answer up (+1 to reduce that drive-by downvote), it shows another alternative to the other two. But as I just mentioned, possibly clarify the needed conformance to `Hashable`/`Equatable`. Also, for future reference, note that https://gist.github.com is a great place to easily share code snippets (no need to login). – dfrib Feb 08 '16 at 10:43
  • True, thought this might be a bit faster and easier and thanks :) – Eendje Feb 08 '16 at 10:44
2

Here's a simple and very performant solution that does not need Hashable or any other modifications on Foo:

var biggestFoos = [String: Foo]()
for foo in array where biggestFoos[foo.color]?.size < foo.size {
    biggestFoos[foo.color] = foo
}
let result = Array(biggestFoos.values)
Nikolai Ruhe
  • 81,520
  • 17
  • 180
  • 200
  • On my bus ride home, I realized a `[String: Foo]` dictionary solution would be an even better approach; came come, wrote one up (however not as condensed as your `for ... where` above), and just as I enter this thread to update my answer, I see that you're already way ahead of me. I believe this `O(n)` solution (premature optimization is a sin, but since I've already gone Big-O notation in my solution ...) should be the accepted answer, +1. – dfrib Feb 08 '16 at 16:10
  • @dfri Thanks. I think your idea of sorting the original array has some benefits and could be improved to a nice solution. The advantage is that it does not need additional storage. So the ideal algorithm (if there's a space constraint) would be to sort the array on color (primary) and size (secondary), then pick each element that has a different color than its predecessor. An interesting exercise (and you'd get my upvote) :) – Nikolai Ruhe Feb 08 '16 at 16:22
  • Thanks for the feedback and the idea. And interesting exercise indeed, however I think it turned out [a bit messy](https://gist.github.com/dfrib/847071ff64c856db8f1c), but maybe I'm damaged by swift.SO:s inclination for condensed-code answers. – dfrib Feb 08 '16 at 18:11
  • @dfri Yes, condensing is not always a good thing. This is my version of filtering the array for first occurrence of a new color: `var previousColor: String?; let result = array.filter { if $0.color != previousColor { previousColor = $0.color; return true }; return false }` – Nikolai Ruhe Feb 09 '16 at 09:55
  • That's nice, naturally `.filter` is a neater solution than a narrow extension. Mind if I add the complete sort-filter solution to my answer (with credits where they are due, of course)? – dfrib Feb 09 '16 at 11:42
  • @dfri Sure, go ahead. – Nikolai Ruhe Feb 09 '16 at 19:48
1

You can try this method using filter, but it can be time-consuming when you array is huge since you have to iterate through the array for each element.

let arrayFiltered = array.filter { (fooElement) -> Bool in
    for (idx, fooItem) in array.enumerate() {

        if fooItem.color == fooElement.color && fooItem.size > fooElement.size {
            return false
        }
    }
    return true
}
Michaël Azevedo
  • 3,874
  • 7
  • 31
  • 45