2

I need to sort an array of elements based on their frequency, for example:

Input array: [1, 6, 6, 6, 6, 4, 3, 5, 5, 5, 2, 2]
Expected output: [1, 3, 4, 2, 2, 5, 5, 5, 6, 6, 6, 6]

I tried with the code below:

var set: NSCountedSet = [1, 6, 6, 6, 6, 4, 3, 5, 5, 5, 2, 2]

var dictionary = [Int: Int]()
set.forEach { (item) in
    dictionary[item as! Int] = set.count(for: item)
}
dictionary.keys.sorted()
print(dictionary)

Description: As 1, 3, 4 occur only once, they are shown at the beginning, 2 occurs two times, 5 three times, 6 four times. And [1, 3, 4] are sorted among them.

Expected result: Time complexity should be O(n)

Bappaditya
  • 9,494
  • 3
  • 20
  • 29
  • 4
    I dont think you can get to O(n) in worst case. If your requirement is that unique items are being sorted as well, that already gives you O(n*log n). – MartinM Jan 16 '19 at 10:47
  • 1
    Count sort is one choice to meet o(n) if the frequency is small – E.Coms Jan 17 '19 at 00:16

8 Answers8

9

You can achieve the results in O(nlogn) time by first creating a Dictionary containing the number of occurrences for each element (O(n)), then calling sorted on the Array (Swift uses Introsort, which is O(nlogn)) and using the values from the previously created Dictionary for the sorting. The elements of your array need to be Comparable for sorting to work and Hashable to be able to store them in a Dictionary, which provides O(1) element lookup.

extension Array where Element: Comparable & Hashable {
    func sortByNumberOfOccurences() -> [Element] {
        let occurencesDict = self.reduce(into: [Element:Int](), { currentResult, element in
            currentResult[element, default: 0] += 1
        })
        return self.sorted(by: { current, next in occurencesDict[current]! < occurencesDict[next]!})
    }
}

[1, 6, 6, 6, 6, 4, 3, 5, 5, 5, 2, 2].sortByNumberOfOccurences() // [1, 4, 3, 2, 2, 5, 5, 5, 6, 6, 6, 6]

The above solution preserves the order of elements that occur an equal number of times. If you actually want to sort such elements based on their compared values (which is what your sample output does), you can modify the closure in sorted like below:

return self.sorted(by: {occurencesDict[$0]! <= occurencesDict[$1]! && $0 < $1})

Or even shorter, comparing tuples for sorting:

return self.sorted(by: {(occurencesDict[$0]!,$0) < (occurencesDict[$1]!,$1)})

which produces the sample output you provided, [1, 3, 4, 2, 2, 5, 5, 5, 6, 6, 6, 6]

Dávid Pásztor
  • 51,403
  • 9
  • 85
  • 116
  • 1
    I was about to post this `let array = [1, 6, 6, 6, 6, 4, 3, 5, 5, 5, 2, 2] let freq = array.reduce(into: [:]) { $0[$1, default: 0] += 1 } let sortedArray = array.sorted() { freq[$0]! < freq[$1]! } sortedArray` – Leo Dabus Jan 16 '19 at 10:51
  • 1
    It cannot be done in O(n). You need to sort this array first, then use the method mentioned above. – Rakesha Shastri Jan 16 '19 at 10:53
  • @RakeshaShastri no, you don't need to sort the array first, that's done in the `sorted` call. However, you're right, the overall time complexity of my algorithm is `O(nlogn)`, updated my answer to reflect that – Dávid Pásztor Jan 16 '19 at 10:54
  • 2
    `sorted(by:)` is not O(n). – Martin R Jan 16 '19 at 10:54
  • @RobertDresler yes just change the sort to `freq[$0]! <= freq[$1]! && $0 < $1` – Leo Dabus Jan 16 '19 at 10:56
  • @MartinR you're absolutely right, I was wrong, updated my answer – Dávid Pásztor Jan 16 '19 at 10:57
  • 1
    @LeoDabus I was thinking about doing that for the second solution as well, but used `<` instead of `<=`, which didn't work, so came up with a longer solution, but updated my answer with your idea if you don't mind :) – Dávid Pásztor Jan 16 '19 at 11:00
  • You can simplify the (second) sort call by comparing tuples, compare e.g. https://stackoverflow.com/a/37612765 – Martin R Jan 16 '19 at 11:00
  • @MartinR thanks for the idea, I also included that solution in my answer! – Dávid Pásztor Jan 16 '19 at 11:04
3

You can try

let dd = [1, 6, 6, 6, 6, 4, 3, 5, 5, 5, 2, 2]
let res = dd.sorted { f, s in
    dd.filter { $0 == f }.count <   dd.filter { $0 == s }.count 
} 
print(res) // [1, 4, 3, 2, 2, 5, 5, 5, 6, 6, 6, 6]
Shehata Gamal
  • 98,760
  • 8
  • 65
  • 87
  • 6
    I wonder if this is O(n). – Note also that the sort is not *stable,* there is no guarantee that the order of numbers which occur equally often is preserved. – Martin R Jan 16 '19 at 10:26
  • @MartinR i don't think such problem be solved with in O(n) , sure there may be other less extensive answer also – Shehata Gamal Jan 16 '19 at 10:33
  • Thanks for the answer @Sh_Khan, but expected output should be `[1, 3, 4, 2, 2, 5, 5, 5, 6, 6, 6, 6]` – Bappaditya Jan 16 '19 at 17:05
2

There is no way to sort with O(n) time complexity. Look at the worst case complexity for popular algorithms at Wikipedia.

The better worst-case time complexity is O(nlogn). Here is how we can solve it with O(nlogn) time complexity:


    let array = [1, 6, 6, 6, 6, 4, 3, 5, 5, 5, 2, 2]

    extension Array where Element: Comparable & Hashable {
        func countableSorted() -> [Element] {
            var counts = [Element: Int]()
            // O(n)
            for element in self {
                counts[element] = (counts[element] ?? 0) + 1
            }

            // I think that standart method uses O(nlogn) time complexity.
            // O(nlogn) + O(n) approximately equal to O(nlogn).
            let sorted = counts.sorted { item1, item2 -> Bool in
                if item2.value > item1.value {
                    return true
                }

                if item2.value == item1.value {
                    return item2.key > item1.key
                }

                return false
            }

            var result = [Element]()
            // O(n)
            for item in sorted {
                let items = Array(repeating: item.key, count: item.value)
                result.append(contentsOf: items)
            }

            // Total time complexity for worst case scenario is O(nlogn)

            return result
        }
    }

    print(array.countableSorted())

    // Output: [1, 3, 4, 2, 2, 5, 5, 5, 6, 6, 6, 6]

J. Doe
  • 114
  • 5
1
var inputArray = [1, 6, 6, 6, 6, 4, 3, 5, 5, 5, 2, 2]
var map:[Int: Int] = [:]
for element in inputArray {
    let count = map[element]
    if count == nil {
        map[element] = 1
    } else {
        map[element] = count! + 1
    }
}
var keysArray = map.keys
let sortedKeys = keysArray.sorted { (number1, number2) -> Bool in
    if map[number1]! == map[number2]! {
        return number1 < number2
    } else {
        return map[number1]! < map[number2]!
    }
}
var finalArray: [Int] = []
for element in sortedKeys {
    for _ in 1...map[element]! {
        finalArray.append(element)
    }
}
print(finalArray)

Time Complexity: O(nlogn)

saransh
  • 29
  • 6
  • 1
    @LeoDabus Thanks. Edited. Further, optional checking shall be done as well. But this solves the basic problem for us. – saransh Jan 18 '19 at 11:48
1

You can try the below code, this worked properly.

var inputArray = [1, 6, 6, 6, 6, 4, 3, 5, 5, 5, 2, 2]
inputArray.sort()
let freq = inputArray.sorted { f, s in
    inputArray.filter { $0 == f}.count < inputArray.filter { $0 == s}.count
}
print(freq)

Not sure about the time complexity.

svs
  • 247
  • 2
  • 14
1

I want to add a solution in O(n)

Sorting takes O(nLogn) but this question can also be solved without using sorting by help of HashMap in Java because it contains the pairs sorted in accordance to the key.

import java.util.*; 

class Simple 
{ 
    public static void main(String[] arg) 
    {  int inputArray[] = {1, 6, 6, 6, 6, 4, 3, 5, 5, 5, 2, 2};
        Map<Integer,Integer> map = new HashMap<Integer,Integer>(); 
        Map<Integer,List<Integer>> map2 = new HashMap<Integer,List<Integer>>();
       for(int i: inputArray)
      {
                  if(map.get(i) == null){
                 map.put(i, 1) ;
                  }
                  else{
                  int a = map.get(i);
                  map.put(i,a+1);
                 }
      }

        // using for-each loop for iteration over Map.entrySet() 
        for (Map.Entry<Integer,Integer> entry : map.entrySet()) {
            if(map2.get(entry.getValue()) == null){
                map2.put(entry.getValue(), new ArrayList<Integer>()) ;
            }
            map2.get(entry.getValue()).add(entry.getKey());
        }

        for(Map.Entry<Integer,List<Integer>> entry : map2.entrySet()){
            for(int j=0; j<entry.getValue().size(); j++){
                for(int i=0; i<entry.getKey(); i++){
                System.out.print(entry.getValue().get(j) + " ");
            }
            }

        }    
    }         

}
  1. In First for loop I am iterating through array saving pair of (value,Occurrence) in map1(HashMap). This will take O(n) as HashMap put operation(insertion) takes O(1).
  2. In second for loop I am iterating map1 and inserting pair of (occurrence, list of numbers in the given array with that occurrence) in map2(HashMap2).
  3. Now in last for loop I am iterating through map2 and printing all the lists one by one it means I am printing each element of given array once i.e. I am iterating through the list of each key and printing each element of the list key number of times. So this would also take O(n).

more about HashMap Time Complexity : O(n)

Swift Version of above code

extension Array where Element: Comparable & Hashable {
func sortByNumberOfOccurences() -> [Element] {
    let occurencesDict = self.reduce(into: [Element:Int](), { currentResult, element in
        currentResult[element, default: 0] += 1
    })
    let dict = occurencesDict.sorted(by: {$0.0 < $1.0})
    var dictioanary = [Int:Array]()
    for (element,occurence) in dict {
        if dictioanary[occurence] == nil
        {
            dictioanary[occurence] = Array()
        }
        dictioanary[occurence]?.append(element)
    }


    var resultArray = Array()
    let finalDict = dictioanary.sorted(by: {$0.0  < $1.0})
    for (frequency,allValuesOccuringWithThisFrequncy) in finalDict {
       for i in allValuesOccuringWithThisFrequncy
       {
        var j = 0
        while(j < frequency)
        {
            resultArray.append(i)
            j = j + 1
        }
       }
    }
    print(resultArray)
    return resultArray
}

}

Time Complexity in Swift O(nLogn)

  • Can you try the same approach in swift? – Bappaditya Jan 18 '19 at 05:46
  • 1
    @Bappaditya Hi I tried to write the code in swift. I am adding the code. But as Dictionary in Swift does not maintain the order we need to apply the sort method in that and it makes complexity O(nLogn). But in java HashMap insert pairs in sorted way on the basis of key. I wish we had a similar type of collection in Swift too. Sad :( I think we need to write our own HashMap in Swift :P – saxenarishav Jan 18 '19 at 07:04
0

Try this solution. It worked for me like a charm :)

func numberOfOccurences(in array: [Int], of element: Int) -> Int {
    let object = NSCountedSet(array: array)
    return object.count(for: element)
}

var inputArray = [1, 6, 6, 6, 6, 4, 3, 5, 5, 5, 2, 2]

var uniqueElements = Array(Set(inputArray))

var otherArray: [Int] = []

var duplicateElements = uniqueElements.filter { (element) -> Bool in
    return (inputArray.contains(element) && numberOfOccurences(in: inputArray, of: element) > 1)
}

uniqueElements = uniqueElements.filter({ !duplicateElements.contains($0) }).sorted()

for item in duplicateElements {
    let occurences = numberOfOccurences(in: inputArray, of: item)
    for _ in 0...occurences - 1 {
        otherArray.append(item)
    }
}

otherArray = otherArray.sorted()

duplicateElements.removeAll()

let mergedArray = uniqueElements + otherArray

print(mergedArray)

0

I think this kind of sorting can be achieved in O(n), with something like the following:

let input = [1, 6, 6, 6, 6, 4, 3, 5, 5, 5, 2, 2]

// build the frequency dictionary (easy task)
let frequencies = input.reduce(into: [:]) { $0[$1] = ($0[$1] ?? 0) + 1 }

// allocate a 2-D array of ints, each item in this array will hold the numbers that
// appear I times in the input array
let frequencyTable: [[Int]] = frequencies.reduce(into: Array(repeating: [Int](), count: input.count)) {
    // substracting one as arrays are zero indexed
    // we can't get of of bounds here since the maximum frequency is the 
    // number of elements in the input array
    // Also replicating the numbers by their frequency, to have
    // the same contents as in the input array
    $0[$1.value - 1] += Array(repeating: $1.key, count: $1.value)
}

// lastly, simply flatten the 2-D array to get the output we need
let output = frequencyTable.flatMap { $0 }

print(output)

Sample result:

[4, 1, 3, 2, 2, 5, 5, 5, 6, 6, 6, 6]

Note that the order of numbers with the same frequency might differ based on how the dictionary hash function works.

Also we sacrifice space (allocated 2-D array) in favour of time.

The frequencyTable contents will look similar to this (again the order of 1, 4, 3 might differ):

[[4, 3, 1], [2, 2], [5, 5, 5], [6, 6, 6, 6], [], [], [], [], [], [], [], []]
Cristik
  • 30,989
  • 25
  • 91
  • 127