问题
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:
- Since there are 64 squares an array of optional pieces is probably best
- In general you use a dictionary with an Int key as a sparse array (see example below)
- 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