Swift 实战:高效设计 Tic-Tac-Toe 游戏逻辑(LeetCode 348)

在这里插入图片描述
在这里插入图片描述

摘要

如果让你实现一个两人对战的井字棋(Tic-Tac-Toe)游戏逻辑,你会怎么做?是不是会直接用二维数组存每个格子的状态,然后在每次落子后从头到尾检查是否赢了?

这种方式虽然直观,但效率并不高,特别是当棋盘扩大到 n×n 的时候。LeetCode 348 这个题目就挑战我们去优化这个过程:每次落子之后都要快速判断胜负,尽可能降低计算开销。

今天我们用 Swift 来实现这个游戏逻辑,看看怎么用一套简洁又高效的数据结构,把这个小游戏做得更聪明些。

描述

这个问题让我们自己“设计”一个井字棋游戏类。也就是说,不是解一个单独的逻辑判断题,而是写出能不断调用和处理的游戏控制结构。

题目需求:

设计一个井字棋游戏类 TicTacToe

init(_ n: Int)
  • 初始化一个 n x n 的棋盘。
func move(_ row: Int, _ col: Int, _ player: Int) -> Int
  • 玩家 player(row, col) 落子。

  • 玩家编号为 1 或 2。

  • 返回值为:

    • 0:没有胜者;
    • 1:玩家 1 获胜;
    • 2:玩家 2 获胜。

示例:

TicTacToe toe = TicTacToe(3)
toe.move(0, 0, 1) // 返回 0(无胜者)
toe.move(0, 2, 2) // 返回 0
toe.move(2, 2, 1) // 返回 0
toe.move(1, 1, 2) // 返回 0
toe.move(2, 0, 1) // 返回 0
toe.move(1, 0, 2) // 返回 0
toe.move(2, 1, 1) // 返回 1(玩家 1 胜出)

题解答案

这道题如果用传统方式去解决,比如在 move() 每次落子后遍历整行、整列、对角线判断是否有玩家胜出,那每次 move 的时间复杂度就是 O(n)。

但我们可以更优化地去思考:我们其实不需要知道整个棋盘状态,只需要知道某一行、某一列或对角线的总和是否等于 n 就可以判断胜负。

于是我们换个角度:

  • 为每行、每列维护一个计数数组;
  • 为两个对角线也维护两个值;
  • 玩家 1 每次落子时计为 +1
  • 玩家 2 落子时计为 -1
  • 当某行、列或对角线的总和变为 +n-n 时,就表示某一方胜出。

这样做的核心优势在于:每次落子都是 O(1) 时间复杂度。

题解代码分析

Swift 解法代码:

class TicTacToe {
    private var rows: [Int]
    private var cols: [Int]
    private var diagonal: Int
    private var antiDiagonal: Int
    private let n: Int

    init(_ n: Int) {
        self.n = n
        self.rows = Array(repeating: 0, count: n)
        self.cols = Array(repeating: 0, count: n)
        self.diagonal = 0
        self.antiDiagonal = 0
    }

    func move(_ row: Int, _ col: Int, _ player: Int) -> Int {
        // 玩家 1 记作 +1,玩家 2 记作 -1
        let toAdd = (player == 1) ? 1 : -1

        // 更新对应行列
        rows[row] += toAdd
        cols[col] += toAdd

        // 判断对角线
        if row == col {
            diagonal += toAdd
        }

        // 判断反对角线
        if row + col == n - 1 {
            antiDiagonal += toAdd
        }

        // 胜负判断
        if abs(rows[row]) == n || abs(cols[col]) == n || abs(diagonal) == n || abs(antiDiagonal) == n {
            return player
        }

        // 没人赢
        return 0
    }
}

代码详细解释:

1. 初始化:
self.rows = Array(repeating: 0, count: n)
  • 用一个数组来记录每一行的累积值,初始为 0。
2. 落子记录:
let toAdd = (player == 1) ? 1 : -1
  • 用不同正负值来区别玩家,从而避免需要分别用两个数组。
3. 判断胜负:
if abs(rows[row]) == n
  • 一行里如果都是 +1(或都是 -1),其绝对值就是 n,说明这个玩家在该行获胜。

这种判断方式不仅逻辑清晰,而且效率极高。

示例测试及结果

我们来跑一组测试用例:

let toe = TicTacToe(3)
print(toe.move(0, 0, 1)) // 输出 0
print(toe.move(0, 2, 2)) // 输出 0
print(toe.move(2, 2, 1)) // 输出 0
print(toe.move(1, 1, 2)) // 输出 0
print(toe.move(2, 0, 1)) // 输出 0
print(toe.move(1, 0, 2)) // 输出 0
print(toe.move(2, 1, 1)) // 输出 1,玩家 1 获胜!

输出结果:

0
0
0
0
0
0
1

可以看到,落子之后能快速返回胜负结果,且没有任何冗余遍历。

时间复杂度

每次调用 move()

  • 更新对应行列、对角线,只涉及常数次操作。
  • 所以时间复杂度是 O(1) —— 这是这道题的优化关键。

相比之下,传统方法每次都要 O(n) 遍历行列,性能差很多。

空间复杂度

  • 需要用两个数组记录每一行和列:O(n)
  • 还需两个整数变量记录对角线:O(1)
  • 所以空间复杂度是 O(n)

非常节省空间,且没有使用二维数组模拟棋盘(那样是 O(n²))。

总结

LeetCode 348 虽然看起来是一个游戏类设计题,但它背后真正考验的是:

  • 你能不能通过数据结构建模问题状态;
  • 能否在落子后常数时间内判断胜负
  • 在 n 比较大时是否还能稳定运行。

通过维护“计数数组”和两个对角线值,我们把传统 O(n) 的遍历过程优化成了 O(1) 的更新和判断。这个思路在很多游戏逻辑、模拟类题目中都非常有价值。

如果你将来要实现一个支持在线对战的 Tic-Tac-Toe 游戏,这套高效结构完全可以用在后台逻辑中,极大提升响应速度。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

网罗开发

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值