1

I have a class called Room where I store information about a particular room in my home (its name, an image of it, etc). In my main ViewController, I create an array of Rooms and assign them some values. Later on, I want to search that array of Rooms for the room named "Study Room." I can use

func findARoom(named room: String) -> Room {
    for i in 0..<rooms.count {
        if rooms[i].roomName == room {    
            return rooms[i]
        }
    }
}

but I think there is a better way. I want to be able to call the findARoom() function in the same way but not iterate through the entire array. I have used Maps in C++, where some linked values are hashed into the map and you can search for one of the values using the other value (e.g. find a phone number attached to someone's last name). Is there any way I can use a similar structure in Swift to find a Room object based on its roomName parameter?

Room.swift:

import UIKit

struct Room {
    var roomName: String
    var roomImage: UIImage
    var port: UInt16

    init(roomName: String, roomImage: UIImage, port: UInt16 = 1883) {
        self.roomName = roomName
        self.roomImage = roomImage
        self.port = port
    }
}

ViewController:

import UIKit

class MainViewController: UIViewController, UITableViewDelegate, UITableViewDataSource, RoomCellDelegate, MQTTModelDelegate {

    var server = [MQTTModel()]
    let cellId = "cellId"
    var rooms: [Room] = [Room]()
    var roomTableView = UITableView()

    override func viewDidLoad() {
        super.viewDidLoad()
        createRoomArray()
        findARoom(named: "Study Room")

    }

    // Setting up the array of rooms
    func createRoomArray() {
        rooms.append(Room(roomName: "Living Room", roomImage: UIImage(named: "Living_Room")!, port: 48712))
        rooms.append(Room(roomName: "Study Room", roomImage: UIImage(named: "Study_Room")!, port: 1883))
        rooms.append(Room(roomName: "Bedroom", roomImage: UIImage(named: "Bedroom")!, port: 1883))    
    }        

    func findARoom(named room: String) -> Room {
        for i in 0..<rooms.count {
            if rooms[i].roomName == room {    
                return rooms[i]
            }
        }
    }
}

4 Answers 4

2

You can try

if let res = rooms.first(where:{ $0.roomName == "Study Room" }) {

}
Sign up to request clarification or add additional context in comments.

Comments

2

As has been pointed out, you can use first(where:), e.g.:

func findARoom(named room: String) -> Room? {
    return rooms.first { $0.roomName == room }
}

But you said:

I want to be able to call the findARoom() function in the same way but not iterate through the entire array.

Unfortunately, first(where:) does perform a for loop. Just look at the source code for that method. It’s still O(n), just like your own implementation.

If you were saying you just don’t want to have to write your own for loop, then first (or the like) is just a more concise way of writing the same thing, but realize that it is no more efficient than your approach.

If you really wanted to enjoy hashed performance, you could build a dictionary:

let rooms: [Room] = ...
let dictionary = Dictionary(grouping: rooms, by: { $0.roomName })

The resulting dictionary is a [String: [Room]].

Now, when you lookup a room in a this dictionary, you enjoy the hashed key performance:

if let room = dictionary["Study Room"]?.first { ... }

IMHO, this dictionary structure isn’t going to be worth it in an app like this, and first(where:) is a really nice, concise solution. But if you really don’t want it looping and truly need O(1) performance, you should know that first will not achieve that.

3 Comments

Thanks, the dictionary is what I was looking for. Could you elaborate on why you think the dictionary structure wouldn't be worth it in this case? Would it take extra memory while the array is so small that the looping would be insignificant, or something else? Thanks again for taking the time to answer.
Exactly, I’m assuming you’re talking about arrays small enough that the performance difference won’t be observable and wouldn’t justify the complication in the design. Feels like premature optimization. But I wanted to answer your question, because you said that you didn’t want the for loop, which actually is lurking behind the scenes in the first(where:) implementation. Also, the choice of dictionary vs array depends upon whether you design wants to preserve the order of the rooms or not...
Note that if you only have a few thousand items (or less) in the array, and you are only looking for a match once per user interaction, O(n) performance is not bad. The user would not notice it. It's only if you do repeated O(n) operations, or when you have a LOT of items in your array that you will have user-perceptible delays.
2

For most things, using array.first(where: { $0.name == "stringToMatch" } will be plenty fast.

I just did a quick benchmark, and running with optimization turned off on my now very low-end original iPad Air, with an array of 100,000 structs that contain a name field, first(where:) matched an item in around 0.026 seconds. You probably would not be able to see that amount of delay. (That's searching for an item that occurs at a random index in the array. Forcing the string to match the last element in the array slows first(where:) down to more like 0.047 seconds (≈1/20 of a second)

EDIT:

Here is the code I used to time first(where:):

    struct AStruct {
        var name: String
        var contents: String
    }
    
    func randomString(length: Int) -> String{
        var result = String()
        let chars = Array(UnicodeScalar("a").value ... UnicodeScalar("z").value) +
            Array(UnicodeScalar("A").value ... UnicodeScalar("Z").value)
        for _ in 1...length {
            result.append(String(UnicodeScalar(chars.randomElement()!)!))
        }
        return result
    }

    func searchTest() {
        var arrayOfStructs = [AStruct]()
        let arrayCount = 100_000
        for _ in 1...arrayCount {
            let name = randomString(length: 10)
            let contents = randomString(length: 20)
            arrayOfStructs.append(AStruct(name: name, contents: contents))
        }
        
        var averageTime: Double = 0
        let iterations = 20
        for _ in 1...20 {
            let aName = arrayOfStructs.randomElement()!.name
//            let aName = arrayOfStructs.last!.name //Force search to find the last item
            let start = Date().timeIntervalSinceReferenceDate
            let match = arrayOfStructs.first( where: {  $0.name == aName } )
            let elapsed = Date().timeIntervalSinceReferenceDate - start
            if match != nil {
                let elapsedString = String(format: "%0.12f", elapsed)
                print("found item in in an array of \(arrayOfStructs.count) items in \(elapsedString)")
            } else {
                print("Failed to find item after \(elapsed)")
            }
            averageTime += elapsed
        }
        averageTime /= Double(iterations)
        let averageTimeString = String(format: "%0.9f", averageTime)
        print("Average time = \(averageTimeString)")
    }

1 Comment

plus dictionary isn't even preferred here
1

If you want to be able to search for a Room without iterating through the entire array in O(1) time, you should consider using a Dictionary instead of an array, which is similar to Maps in C++.

Your ViewController code will need to be modified like this:

import UIKit

class MainViewController: UIViewController, UITableViewDelegate, UITableViewDataSource, RoomCellDelegate, MQTTModelDelegate {

    var server = [MQTTModel()]
    let cellId = "cellId"
    var rooms: [String: Room] = [String: Room]()  // Declare the dictionary
    var roomTableView = UITableView()

    override func viewDidLoad() {
        super.viewDidLoad()
        createRoomDictionary()
        findARoom(named: "Study Room")

    }

    // Setting up the dictionary of rooms
    func createRoomDictionary() {
        rooms["Living Room"] = Room(roomName: "Living Room", roomImage: UIImage(named: "Living_Room")!, port: 48712)
        rooms["Study Room"] = Room(roomName: "Study Room", roomImage: UIImage(named: "Study_Room")!, port: 1883)
        rooms["Bedroom"] = Room(roomName: "Bedroom", roomImage: UIImage(named: "Bedroom")!, port: 1883)
    }        

    func findARoom(named room: String) -> Room {
        return rooms[room]
    }
}

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.