Лучший способ поиска в массиве объектов по значению (карта) - PullRequest
0 голосов
/ 10 января 2019

У меня есть класс Room, где я храню информацию о конкретной комнате в моем доме (ее название, изображение и т. Д.). В моем главном ViewController я создаю массив Rooms и присваиваю им некоторые значения. Позже я хочу найти в этом массиве Комнаты комнату с названием «Учебная комната». Я могу использовать

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

но я думаю, что есть лучший способ. Я хочу иметь возможность вызывать функцию findARoom() таким же образом, но не выполнять итерацию по всему массиву. Я использовал Карты в C ++, где некоторые связанные значения хэшируются на карте, и вы можете искать одно из значений, используя другое значение (например, найти номер телефона, прикрепленный к чьей-то фамилии). Есть ли способ использовать подобную структуру в Swift, чтобы найти объект Room на основе его параметра roomName?

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 ]

0 голосов
/ 10 января 2019

Для большинства вещей использование array.first(where: { $0.name == "stringToMatch" } будет достаточно быстрым.

Я только что сделал быстрый тест, и работа с оптимизацией отключена на моем теперь очень дешевом оригинальном iPad Air с массивом из 100 000 структур, содержащих поле имени, first(where:) соответствовал элементу примерно за 0,026 секунды. Вы, вероятно, не сможете увидеть такую ​​задержку. (Это поиск элемента, который встречается по случайному индексу в массиве. Принудительное совпадение строки с последним элементом в массиве замедляет first(where:) до более примерно 0,047 секунд (≈1 / 20 от второй)

EDIT:

Вот код, который я использовал для времени 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)")
    }
0 голосов
/ 10 января 2019

Если вы хотите иметь возможность поиска Room без итерации по всему массиву за O (1) время, вам следует рассмотреть возможность использования Dictionary вместо массива, что аналогично Maps в C ++.

Ваш код ViewController необходимо изменить следующим образом:

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]
    }
}
0 голосов
/ 10 января 2019

Как уже указывалось, вы можете использовать first(where:), например ::

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

Но вы сказали:

Я хочу иметь возможность вызывать функцию findARoom() таким же образом, но не выполнять итерацию по всему массиву.

К сожалению, first(where:) выполняет цикл for. Просто посмотрите на исходный код для этого метода. Это все еще O (n), как и ваша собственная реализация.

Если вы говорили, что просто не хотите писать свой собственный цикл for, то first (или тому подобное) - это просто более краткий способ написать то же самое, но понимайте, что это не более эффективен, чем ваш подход.

Если вы действительно хотите насладиться хэшированным исполнением, вы можете создать словарь:

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

Полученный dictionary является [String: [Room]].

Теперь, когда вы ищите комнату в этом dictionary, вы наслаждаетесь хешированными клавишами:

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

ИМХО, эта словарная структура не будет стоить этого в таком приложении, и first(where:) - действительно хорошее, краткое решение. Но если вы действительно не хотите, чтобы он был зациклен и действительно нуждался в производительности O (1), вы должны знать, что first этого не достигнет.

0 голосов
/ 10 января 2019

Вы можете попробовать

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

}
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...