Generate Array of Unique Random Numbers within Inclusive Range
Asked Answered
S

8

9

I am trying to write a function in Apple Swift (iOS) that will generate any given amount of unique random numbers that are within a given inclusive range, say between 0 and 10. So if I say I want 5 unique random numbers between 0 and 10, it would return an array with [7, 10, 2, 3, 0] or [7, 10, 2, 8, 0], etc.

I have that part working with:

// Returns an array of unique numbers
func uniqueRandoms(numberOfRandoms: Int, minNum: Int, maxNum: UInt32) -> [Int] {

    var uniqueNumbers = [Int]()

    while uniqueNumbers.count < numberOfRandoms {

        let randomNumber = Int(arc4random_uniform(maxNum + 1)) + minNum
        var found = false

        for var index = 0; index < uniqueNumbers.count; ++index {
                if uniqueNumbers[index] == randomNumber {
                    found = true
                    break
                }
        }

        if found == false {
            uniqueNumbers.append(randomNumber)
        }

    }

    return uniqueNumbers
}

print(uniqueRandoms(5, minNum: 0, maxNum: 10))

Now I want to add the ability to blacklist a single number within that range that I don’t want. Say I still want 5 unique random numbers between 0 and 10 BUT I don’t want it to ever include 8.

That part causes an endless loop (25%+ of the time or more) and I can’t figure out why? Here’s what I have:

var blackListNum = 8

// Returns an array of unique numbers
func uniqueRandoms(numberOfRandoms: Int, minNum: Int, maxNum: UInt32, checkBlackList: Bool = false) -> [Int] {

    var uniqueNumbers = [Int]()

    while uniqueNumbers.count < numberOfRandoms {

        let randomNumber = Int(arc4random_uniform(maxNum + 1)) + minNum
        var found = false

        for var index = 0; index < uniqueNumbers.count; ++index {
            if checkBlackList == false {
                if uniqueNumbers[index] == randomNumber {
                    found = true
                    break
                }
            } else {
                if uniqueNumbers[index] == randomNumber || uniqueNumbers[index] == blackListNum  {
                    found = true
                    break
                }
            }
        }

        if found == false {
            uniqueNumbers.append(randomNumber)
        }

    }

    return uniqueNumbers
}

print(uniqueRandoms(5, minNum: 0, maxNum: 10, checkBlackList: true))

I understand that my function is far from efficient because I am just starting to learn Swift but I want to keep it similar as I want to understand how it works. I don’t want to simply copy and paste someone else’s more efficient solution and not understand it. I have just learned variables, constants, if, while, for, etc. statements and the other basics and want to keep it to that.

Soughtafter answered 25/9, 2015 at 1:56 Comment(0)
V
13

You could make your live much easier using a Set to store all random numbers until you reach the expected number of randoms:

func uniqueRandoms(numberOfRandoms: Int, minNum: Int, maxNum: UInt32) -> [Int] {
    var uniqueNumbers = Set<Int>()
    while uniqueNumbers.count < numberOfRandoms {
        uniqueNumbers.insert(Int(arc4random_uniform(maxNum + 1)) + minNum)
    }
    return uniqueNumbers.shuffled()
}

print(uniqueRandoms(numberOfRandoms: 5, minNum: 0, maxNum: 10))

func uniqueRandoms(numberOfRandoms: Int, minNum: Int, maxNum: UInt32, blackList: Int?) -> [Int] {
    var uniqueNumbers = Set<Int>()
    while uniqueNumbers.count < numberOfRandoms {
        uniqueNumbers.insert(Int(arc4random_uniform(maxNum + 1)) + minNum)
    }
    if let blackList = blackList {
        if uniqueNumbers.contains(blackList) {
            while uniqueNumbers.count < numberOfRandoms+1 {
                uniqueNumbers.insert(Int(arc4random_uniform(maxNum + 1)) + minNum)
            }
            uniqueNumbers.remove(blackList)
        }
    }
    return uniqueNumbers.shuffled()
}

uniqueRandoms(numberOfRandoms: 3, minNum: 0, maxNum: 10, blackList: 8)  // [0, 10, 7]
Vireo answered 25/9, 2015 at 2:24 Comment(0)
A
7

A straight forward approach is to create an array of numbers to choose from and then remove the numbers as you choose them:

// create an array of 0 through 10
var nums = Array(0...10)

// remove the blacklist number
nums.removeAtIndex(nums.indexOf(8)!)

var randoms = [Int]()
for _ in 1...5 {
    let index = Int(arc4random_uniform(UInt32(nums.count)))
    randoms.append(nums[index])
    nums.removeAtIndex(index)
}

The advantage of this method is that you only need to generate as many random numbers as you want values in your array. Since you are selecting from the numbers that are still available each time, you never have to check to see if you already have a random value.

Aurar answered 25/9, 2015 at 2:39 Comment(0)
T
7

Solutions and Performance breakdown in Swift 4.0

I recently found myself needing a solution to this very same problem, but without the blacklist, and I saw answers on this page and also over at this question, but I was concerned about performance when the set of numbers to choose from was very large and also when choosing a lot of numbers (like, to where you're choosing more than 50% of the numbers in the overall pool).

So I tried out a few solutions.

The first was the kind where you choose a number at random, check if the number has already been chosen before, and either choose a different one or add it to the list of numbers.

func randomNumber(between lower: Int, and upper: Int) -> Int {
    return Int(arc4random_uniform(UInt32(upper - lower))) + lower
}

func generateRandomUniqueNumbers1(forLowerBound lower: Int, andUpperBound upper:Int, andNumNumbers iterations: Int) -> [Int] {
    guard iterations <= (upper - lower) else { return [] }
    var numbers: [Int] = []
    (0..<iterations).forEach { _ in
        var nextNumber: Int
        repeat {
            nextNumber = randomNumber(between: lower, and: upper)
        } while numbers.contains(nextNumber)
        numbers.append(nextNumber)
    }
    return numbers
}

The second solution is pretty much the same as the one that vacawama is suggesting. You start by loading up an array of all the available numbers, choose one at random, and remove it from the available array, and add it to your numbers array. Repeat for as many numbers as you want.

func generateRandomUniqueNumbers2(forLowerBound lower: Int, andUpperBound upper:Int, andNumNumbers iterations: Int) -> [Int] {
    guard iterations <= (upper - lower) else { return [] }
    var indices: [Int] = (lower..<upper).sorted()
    var numbers: [Int] = []
    (0..<iterations).forEach { _ in
        let nextNumberIndex = randomNumber(between: 0, and: indices.count)
        let nextNumber: Int = indices[nextNumberIndex]
        indices.remove(at: nextNumberIndex)
        numbers.append(nextNumber)
    }
    return numbers
}

The third solution was an adaptation of the first solution to address the fact that arrays are slow at finding elements within them. By changing the stored numbers array to a Set, that operation should be faster.

func generateRandomUniqueNumbers3(forLowerBound lower: Int, andUpperBound upper:Int, andNumNumbers iterations: Int) -> [Int] {
    guard iterations <= (upper - lower) else { return [] }
    var numbers: Set<Int> = Set<Int>()
    (0..<iterations).forEach { _ in
        let beforeCount = numbers.count
        repeat {
            numbers.insert(randomNumber(between: lower, and: upper))
        } while numbers.count == beforeCount
    }
    return numbers.map{ $0 }
}

I was pretty sure that solution 1 would bog down in situations like where you have 100 numbers to choose from, and you want 90 unique ones. By the time you are choosing the 80th number, you only have a 20% chance of choosing one that hasn't been chosen yet. And it scales really badly if you have like 5000 numbers and you still want 90% of them.

I figured that solution 2 would handle it better, but I wasn't sure what kind of a performance hit removing so many values from a large array would have.

I wasn't sure what to make of solution 3. Probably somewhere in the middle was my guess.

I set up XCTest to measure some performance on both algorithms under different load conditions. There were 2 parameters: population and density. Population is the total number of numbers to choose from, and density is what percentage of the population that we wanted to pull out (ie: a population of 80 means 80 numbers to choose from, and a density of 50% means we want to choose 40 unique numbers at random from the population of 80).

I did 9 tests for each combination of 3 different populations (5, 250, and 12,500) and density values (10%, 50%, and 90%). Depending on how quickly or slowly the test was able to finish, I adjusted how many iterations of the test I performed (varied from just one pass to as many as 2,500 passes).

These were the results:

Solution 1

(Population: 5;      Density: 10%; Iterations: 2,500): 0.0056s
(Population: 250;    Density: 10%; Iterations: 50)   : 0.0046s
(Population: 12,500; Density: 10%; Iterations: 10)   : 1.33s
(Population: 5;      Density: 50%; Iterations: 2,500): 0.0131s
(Population: 250;    Density: 50%; Iterations: 50)   : 0.0912s
(Population: 12,500; Density: 50%; Iterations: 1)    : 4.09s
(Population: 5;      Density: 90%; Iterations: 2,500): 0.0309s
(Population: 250;    Density: 90%; Iterations: 10)   : 0.0993s
(Population: 12,500; Density: 90%; Iterations: 1)    : 23s

Solution 2

(Population: 5;      Density: 10%; Iterations: 2,500): 0.0184s
(Population: 250;    Density: 10%; Iterations: 50)   : 0.0086s
(Population: 12,500; Density: 10%; Iterations: 10)   : 0.103s
(Population: 5;      Density: 50%; Iterations: 2,500): 0.0233s
(Population: 250;    Density: 50%; Iterations: 50)   : 0.0125s
(Population: 12,500; Density: 50%; Iterations: 1)    : 0.0209s
(Population: 5;      Density: 90%; Iterations: 2,500): 0.0242s
(Population: 250;    Density: 90%; Iterations: 10)   : 0.0046s
(Population: 12,500; Density: 90%; Iterations: 1)    : 0.0278s

Solution 3

(Population: 5;      Density: 10%; Iterations: 2,500): 0.00672s
(Population: 250;    Density: 10%; Iterations: 50)   : 0.0024s
(Population: 12,500; Density: 10%; Iterations: 10)   : 0.0148s
(Population: 5;      Density: 50%; Iterations: 2,500): 0.0134s
(Population: 250;    Density: 50%; Iterations: 50)   : 0.00769s
(Population: 12,500; Density: 50%; Iterations: 1)    : 0.00789s
(Population: 5;      Density: 90%; Iterations: 2,500): 0.0209s
(Population: 250;    Density: 90%; Iterations: 10)   : 0.00397s
(Population: 12,500; Density: 90%; Iterations: 1)    : 0.0163s

Comparison

(Case 1): Solution 1 is fastest; then 3; then 2
(Case 2): Solution 3 is fastest; then 1; then 2
(Case 3): Solution 3 is fastest; then 2; then 3
(Case 4): Solution 1 is fastest; then 3; then 2
(Case 5): Solution 3 is fastest; then 2; then 1
(Case 6): Solution 3 is fastest; then 2; then 1
(Case 7): Solution 3 is fastest; then 2; then 1
(Case 8): Solution 3 is fastest; then 2; then 1
(Case 9): Solution 3 is fastest; then 2; then 1

Conclusion

As I suspected from the first solution, it really bogged down with large populations and high densities. It's still plenty quick when you don't have that much population or when you are only choosing like 2 numbers, but all solutions were pretty fast overall in those conditions. Even if it was the case that solution 1 could choose 25 random numbers from a population of 250 faster than solution 2 or 3 could, the real-time difference was pretty small.

However, it's useful to point out that if you want very few unique numbers from a really large population (ie: 2 unique numbers from a pool of 12,500), solution 1 is fastest, about 77% faster than solution 3, and both are orders of magnitude faster than solution 2. For my specific case, that's closer to what I will be doing almost all the time, so I will probably make a specific function that uses solution 1 for specifically choosing 2 unique numbers from a large pool of data.

Overall, solution 3 is pretty close to an all-around best algorithm. But with large data sets, consider each of these solutions based on what you plan on using them for.

Timmi answered 4/9, 2017 at 1:20 Comment(1)
Huge answer. Thank you.Reseau
R
4

Here's my solution. SwiftStub:

extension Array {
    func shuffle() -> Array<Element> {
        var newArray = self

        for i in 0..<newArray.count {
            let j = Int(arc4random_uniform(UInt32(newArray.count)))
            guard i != j else { continue }
            swap(&newArray[i], &newArray[j])
        }

        return newArray
    }
}

func uniqueRandoms(count: Int, inRange range: Range<Int>, blacklist: [Int] = []) -> [Int] {
    var r = [Int](range)
        .filter{ !blacklist.contains($0) }
        .shuffle()

    return Array(r[0..<count])
}

let x = uniqueRandoms(5, inRange: 1...10000)
let y = uniqueRandoms(5, inRange: 1...10, blacklist: [2,4,6,8,10])
print(x)
print(y)

filter filters out the numbers on the black list.

shuffle is an extension added to the Array class. You implement it as a separate function if you want.

return Array(r[0..<count]) creates a new array from a Slice of the existing array.

This has a potential index out of bound bug when the list is smaller than the count asked for. For examples, these will crash:

let a = uniqueRandoms(10, inRange: 1...5)
let b = uniqueRandoms(3, inRange: 1...5, blacklist: [1,2,3,4])

Handling that is left as an exercise for the OP.

Reticle answered 25/9, 2015 at 14:32 Comment(1)
swift 4.2 replace Range<Int> to ClosedRange<Int>Coda
S
0
// 1st version where I only blacklisted 8
var randomNumbers = [Int]()
for _ in 1...7 {
    var number = Int(arc4random_uniform(8))+1
    while  randomNumbers.contains(number) || number == 8{
        number = Int(arc4random_uniform(8))+1
    }
    randomNumbers.append(number)
}
print(randomNumbers)

// 2nd version where I created an array of blacklisted numbers
var randomNumbers = [Int]()
var blackList = [8, 5, 2, 7]
for _ in 1...3 {
    var number = Int(arc4random_uniform(10))+1
    while  randomNumbers.contains(number) || blackList.contains(number){
        number = Int(arc4random_uniform(10))+1
    }
    randomNumbers.append(number)
}
print(randomNumbers)

I created 2 empty arrays "randomNumbers" and "blackList" then set up a for loop statement. In the loop statement I initialize the variable "number." I assign "number" to a random number, then I use a while statement and .contians to check if "number" is already in the array "randomNumbers" or if the "number" is in the array "blackList" if either array contains "number," "number is reassigned until neither array contains "number". Then I randomNumbers.append(number) to append "number" into "randomNumbers" then the loop starts over again.

Steffie answered 2/10, 2017 at 6:32 Comment(0)
Q
0

UPDATED ...

Here I am a few year later, this two lines produce an ordered and an unordered list.

let ordered = Array(0..<nodes.count)
var unordered = ordered.shuffled()

ORIGINAL ...

An extension based on ranges and Sets. Won't scale well, but works for small ranges.

extension Int {
  static func randomV(in range: ClosedRange<Int>, excluding usedRange: inout Set<Int>) -> Int
  {
    if usedRange.count == range.upperBound - 1 {
      usedRange = []
    }
    var r = Int.random(in: Range(uncheckedBounds: (range.lowerBound, range.upperBound)))
    while usedRange.contains(r) {
      r = Int.random(in: Range(uncheckedBounds: (range.lowerBound, range.upperBound)))
    }
    if usedRange.count < range.upperBound - 1 {
      usedRange.insert(r)
    }
    return r
  }
}

That you need to call to that looks like this...

var usedPool = Set<Int>()
for loop in 0..<24 {
  let rnd = Int.randomV(in: 1...4, excluding: &usedPool)
}

This is an alternative too, that is more deterministic as in you can look up the performance of the shuffled function to understand how bad this will be with a larger set.

extension Int {
  static func randomX(randomSet:Set<Int>, working usedRange: inout Set<Int>) -> Int {
  if usedRange.isEmpty {
   usedRange = randomSet
  }
  let r = usedRange.first!
  usedRange.remove(at: usedRange.startIndex)
  usedRange = Set(usedRange.shuffled())
  return r
 }
}

You call it with this ...

let randomPool:Set = [1,2,3,4]
var usedPool = randomPool
for i in 0...10 {
let foo = Int.randomX(randomSet: randomPool, working:&usedPool)
  print("foo ",foo)
}
Quiteris answered 18/11, 2021 at 10:33 Comment(0)
Q
0

Here I am a few year later, this two lines produce an ordered and an unordered list.

let ordered = Array(0..<nodes.count) var unordered = ordered.shuffled()

Quiteris answered 16/3, 2023 at 9:19 Comment(0)
S
-1

so much easy way is :

  let UniqueIdNumber = CFUUIDCreateString(nil, CFUUIDCreate(nil))
Schear answered 13/4, 2016 at 5:6 Comment(4)
UUIDs are not numbers. The asker is clearly using Integers for their example, and UUIDs, while they are certainly unique and random, are not helpful at all to this question.Timmi
Yes, you get a UUID (like this: "37165960-ED1F-4271-92A0-41D9730CF250"), which is a String, not an Int.Timmi
I dont think you know or understanding the meaning of the UUID! please see page -> en.wikipedia.org/wiki/Universally_unique_identifier You can not create unique identifier with numbers!! At some point it wont be unique! What you have pasted above is the potential unique id. And it is easy way to create the UUID! So please before you give -1 to any comments make sure you know this area really good.Schear
I'm very aware of what a UUID is. And I'm surprised your answer didn't already have any downvotes. From the original question: "I am trying to write a function in Apple Swift (iOS) that will generate any given amount of unique random numbers that are within a given inclusive range, say between 0 and 10." The asker wants NUMBERS, which are Integers. A UUID is a String. That alone means there's no way a UUID is what he's looking for. Second, it needs to be inside an inclusive range. UUIDs are not part of an ordered sequence. Perhaps you though you were answering a different question?Timmi

© 2022 - 2024 — McMap. All rights reserved.