Accessing Swift set elements by their hash value

笑着哭i 提交于 2020-05-26 04:39:50

问题


Say I'm making a chess app, in which a position is stored like this:

struct Position {
    var pieces: Set<Piece>
    // some other stuff
}

Piece is defined as follows:

struct Piece: Hashable {
    var row: Int
    var column: Int
    let player: Player
    let type: PieceType

    var hashValue: Int {
        return 8 * row + column
    }
}

Player and PieceType are simple enums:

enum Player {
    case White, Black
}

enum PieceType {
    case Queen, King, ...
}

Now, I would like to access a Piece in a Position by its position on the board. The hash value of a Piece is uniquely determined by its position, so access to a Piece should be possible in constant time. However, a Swift set doesn't have a function to get one of its elements by its hash value. All I could come up with is

for piece in pieces {
    if piece.hashValue == 25 {
        // do something
    }
}

...but obviously, this runs in linear time, not in constant time.

A way to solve this problem is to not use a set, but an array instead:

var pieces: [Piece?]

Then I can access the piece at a certain position simply with pieces[25], in constant time. I do find this less elegant, because this method stores the position of each Piece twice: by the position in the pieces array and by the values of the row and column variables.

Is there a way to access a set element by its hash value (in constant time)? Or should I just use an array?


回答1:


Preamble:

  1. Since there are 64 squares an array of optional pieces is probably best
  2. In general you use a dictionary with an Int key as a sparse array (see example below)
  3. You should not use hashValue for location alone since it should take into account all properties (I changed to location in example below) - though I get that in this case except transiently no 2 pieces can have the same location and therefore it is probably OK

How I might code it:

struct Piece {
    var row: Int
    var column: Int
    let player: Player
    let type: PieceType

    var location: Int {
        return 8 * row + column
    }
}

struct Board {
    var pieces = [Int : Piece]()

    init(pieces: Piece...) {
        pieces.forEach {
            self.pieces[$0.location] = $0
        }
    }
}

let whiteKing = Piece(row: 0, column: 4, player: .White, type: .King)
let blackKing = Piece(row: 7, column: 3, player: .Black, type: .King)

let board = Board(pieces: whiteKing, blackKing)

board.pieces // [4: {row 0, column 4, White, King}, 59: {row 7, column 3, Black, King}]



回答2:


Getting an element in a Set by it's hashValue in constant time generally wouldn't be possible since the properties used to compute the hashValue might change (e.g. the Piece moves) and the Set doesn't know when this has happened. If the Set could know somehow then it might be able to store the element in a new location with respect to the hashValue to allow subsequent constant lookups. Basically that's what you would be doing if you put them in a [Position: Piece] dictionary and added/removed keys for every move.

In this case a simple array is probably the best solution. As for the array wasting space, it's a classic case of a space–time tradeoff.




回答3:


Or you can do:

let piece = pieces.filter { $0.hashValue == 25 }[0]

You are scanning an 8x8 chessboard. Stop worrying about complexity.




回答4:


To make it more elegant, I would add a subscript to a Board structure.

struct Board {

  var grid: [Piece?]
  let rows: Int, columns: Int

  init(rows: Int, columns: Int) {
    self.rows = rows
    self.columns = columns
    grid = [Piece?](count: rows * columns, repeatedValue: nil)
  }

  func isValidIndex(row: Int, column: Int) -> Bool {
    return row >= 0 && row < rows && column >= 0 && column < columns
  }

  subscript(row: Int, column: Int) -> Piece? {
    get {
      assert(isValidIndex(row, column: column), "Index out of range")
      return grid[(row * columns) + column]
    }
    set(newValue) {
      assert(isValidIndex(row, column: column), "Index out of range")
      grid[(row * columns) + column] = newValue
    }
  }
}

And get/set pieces like this:

var board = Board(rows: 8, columns: 8)
board[0, 1] = Piece(player: .White, type: .King)
board[0, 2] // nil



回答5:


I would stick with an array.

I do find this less elegant, because this method stores the position of each Piece twice: by the position in the pieces array and by the values of the row and column variables.

If you could access a value from a set by hash (which you can't) you would have the same problem where the values are stored twice. And an array would be the nicer way to access elements.

// array
pieces[25]

// set (not possible)
pieces.elementWithHash(25)

I would actually change it to use a 2d array since it is more readable what you are accessing.

var pieces: [[Piece?]]
pieces[3][1]


来源:https://stackoverflow.com/questions/34425578/accessing-swift-set-elements-by-their-hash-value

标签
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!