Swift 實戰:用鏈表和哈希表寫出高性能的貪吃蛇引擎(LeetCode 353)

在這里插入圖片描述
在這里插入圖片描述

文章目錄

    • 摘要
    • 描述
    • 解決方案
    • 解析問題與解決方案
      • 關鍵細節逐條講
    • 示例與運行結果
    • 時間復雜度
    • 空間復雜度
    • 總結

摘要

這題的目標是設計一個“貪吃蛇”核心引擎:給定棋盤大小和一串食物位置,支持不斷調用 move(direction) 推進游戲,返回當前分數,撞墻或咬到自己就結束(返回 -1)。本文用 Swift 從零實現一個能跑起來的 Demo,包括完整的 SnakeGame 類、關鍵數據結構設計、邊界與碰撞處理、以及幾組樣例跑數。代碼貼近實戰,既能交作業,也能當你寫小型游戲/面試題的參考模板。

描述

題目要我們實現三個能力:

  • SnakeGame(width, height, food): 初始化棋盤和食物隊列。

  • move(direction: String) -> Int: 讓蛇向 U/D/L/R 走一步;

    • 吃到食物:長度 +1,分數 +1。
    • 普通移動:頭前進一格,尾巴同時挪走一格。
    • 撞墻/咬到自己:返回 -1(游戲結束)。
  • 返回值:當前分數(吃到幾次食物)。若結束,始終返回 -1。

要點在于:如何讓每次 move 都保持 O(1) 或接近 O(1) 的時間復雜度。

解決方案

高效思路(也是業界常見寫法):

  1. 雙端結構存蛇身
    用一個可 O(1) 頭插、尾刪的結構維護蛇身順序(頭在尾端更直觀),我們用一個簡易的 雙向鏈表

  2. 快速查重靠哈希集合
    Set 存蛇身占用的格子,一步就能判斷“咬到自己”。

  3. 一維編碼坐標
    位置 (r, c) 編碼為 id = r * width + c。既省內存又快。

  4. 先判斷是否吃到食物

    • 吃到:不移除尾巴(蛇變長)。
    • 沒吃到:把尾巴從集合里移除(因為尾巴會移動),再判斷新頭是否與身體沖突。
  5. 邊界/自撞檢查

    • 越界直接結束。
    • 自撞:若新頭位置已經在集合里(注意“沒吃到食物”的情況下我們先拿掉尾巴再查重)。

解析問題與解決方案

下面是完整可運行的 Swift 代碼(命令行/Playground 都能跑)。為了 O(1) 頭插尾刪,我們寫了個輕量的雙向鏈表 Deque;蛇身用 Deque<Int> 維護,哈希表記錄占用。

import Foundation// 輕量雙向鏈表(Deque)——支持 O(1) 頭刪尾插等操作
final class Deque<T> {final class Node<T> {var val: Tvar prev: Node<T>?var next: Node<T>?init(_ v: T) { self.val = v }}private var head: Node<T>?private var tail: Node<T>?private(set) var count: Int = 0var isEmpty: Bool { count == 0 }func pushBack(_ v: T) {let node = Node(v)if let t = tail {t.next = nodenode.prev = ttail = node} else {head = nodetail = node}count += 1}func popFront() -> T? {guard let h = head else { return nil }let v = h.vallet nh = h.nextnh?.prev = nilhead = nhif nh == nil { tail = nil }count -= 1return v}func back() -> T? { tail?.val }func front() -> T? { head?.val }
}// 核心:SnakeGame
final class SnakeGame {private let width: Intprivate let height: Intprivate let food: [[Int]]private var foodIndex: Int = 0// 蛇身:尾在 front,頭在 backprivate var body = Deque<Int>()// 占用表:快速判斷“撞到自己”private var occupied = Set<Int>()private var score = 0// 方向映射private let dirMap: [String: (Int, Int)] = ["U": (-1, 0), "D": (1, 0),"L": (0, -1), "R": (0, 1)]init(_ width: Int, _ height: Int, _ food: [[Int]]) {self.width = widthself.height = heightself.food = food// 初始蛇:長度為 1,在 (0,0)let startId = 0body.pushBack(startId)occupied.insert(startId)score = 0foodIndex = 0}// 坐標與一維 id 的互轉private func idOf(_ r: Int, _ c: Int) -> Int { r * width + c }private func rcOf(_ id: Int) -> (Int, Int) { (id / width, id % width) }// 一步移動:返回當前分數,若失敗返回 -1func move(_ direction: String) -> Int {guard let (dr, dc) = dirMap[direction] else { return -1 }guard let headId = body.back() else { return -1 }let (hr, hc) = rcOf(headId)let nr = hr + drlet nc = hc + dc// 1) 越界?if nr < 0 || nr >= height || nc < 0 || nc >= width {return -1}let newHead = idOf(nr, nc)// 2) 是否吃到食物?let willEat = (foodIndex < food.count &&food[foodIndex][0] == nr &&food[foodIndex][1] == nc)// 3) 如果不會吃到,尾巴要移動:先從占用表移除尾巴if !willEat {if let tailId = body.front() {_ = body.popFront()occupied.remove(tailId)}}// 4) 自撞?if occupied.contains(newHead) {return -1}// 5) 頭前進:加入蛇身與占用表body.pushBack(newHead)occupied.insert(newHead)// 6) 吃到食物:分數+1,食物指針后移if willEat {score += 1foodIndex += 1}return score}
}

關鍵細節逐條講

  • 為什么先“挪尾再查撞”?
    蛇不吃的時候,下一步尾巴會離開原位置。所以你可以“允許頭移動到原尾巴位置”。實現上就表現為:把尾巴從 occupied 里移除,再查新頭是否在 occupied。這能避免錯判“咬到自己”。

  • 數據結構選型

    • Deque:我們只需要 O(1) 地在“尾部加頭”、“頭部去尾”,鏈表最直覺;自己實現很輕,避免 Array.removeFirst() 的 O(n)。
    • Set:哈希查重 O(1),非常關鍵。
    • 一維編碼位置:讓 Set<Int> 更輕量,少了元組 hash 的開銷。
  • 食物邏輯

    • “吃到食物不移除尾巴”是增長的本質。
    • foodIndex 單調遞增就行,省去額外數據結構。
  • 方向映射
    簡單 Dictionary,易于拓展(比如以后加入斜向就是改這兒)。

示例與運行結果

下面給一段可運行的 Demo,包含兩組測試:一組是經典樣例,一組是我自定義的壓力測試(快速吃兩個食物、觸發自撞)。

// MARK: - Demo 1(LeetCode 常見用例)
do {let game = SnakeGame(3, 2, [[1,2],[0,1]])print("Demo 1")print(game.move("R")) // 0print(game.move("D")) // 0print(game.move("R")) // 1 (吃到 [1,2])print(game.move("U")) // 1print(game.move("L")) // 2 (吃到 [0,1])print(game.move("U")) // -1 (撞墻)print("----")
}// MARK: - Demo 2(連續吃食物 + 自撞)
do {let game = SnakeGame(4, 3, [[0,1],[0,2],[0,3]])print("Demo 2")print(game.move("R")) // 1print(game.move("R")) // 2print(game.move("R")) // 3print(game.move("D")) // 3print(game.move("L")) // 3print(game.move("U")) // -1(向上回到自己身體)print("----")
}

可能輸出(你的控制臺應該是一致的):

Demo 1
0
0
1
1
2
-1
----
Demo 2
1
2
3
3
3
-1
----

你可以在 Playground 或命令行直接復制粘貼運行,感受一下每一步的狀態變化。

時間復雜度

  • 每次 move
    全部是 O(1) 操作:

    • 鏈表尾插/頭刪 O(1)
    • Set 增刪查 O(1)
    • 一次邊界和食物判斷 O(1)
      因此整題可以輕松達到 均攤 O(1)
  • 初始化
    O(1)(食物數組只是引用,未做預處理)。

空間復雜度

  • 蛇身鏈表:最多占用 O(k)(k 為當前蛇長)。
  • 占用集合:同樣 O(k)。
  • 食物數組:O(f)(f 為食物個數)。
  • 其他都是常數。

整體空間:O(k + f)

總結

這道題表面是小游戲,實則是數據結構設計題。抓住三件事就能寫得又快又穩:

  1. Deque + Set 保證 O(1) 移動與查重;
  2. “不吃時先挪尾巴再查撞”這一步是避免誤判的關鍵;
  3. 一維編碼位置提升哈希效率,讓實現更簡潔。

如果你以后要把它擴展成單機小游戲,渲染層(SwiftUI、SpriteKit)只需要消費 move 的結果,按照 Deque 里的坐標把節點畫出來即可;引擎層完全復用本文代碼。

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/diannao/95467.shtml
繁體地址,請注明出處:http://hk.pswp.cn/diannao/95467.shtml
英文地址,請注明出處:http://en.pswp.cn/diannao/95467.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

2025-08-15:按對角線進行矩陣排序。用go語言,給你一個 n × n 的整數矩陣,要求返回一個按下面規則調整后的矩陣: - 將每一條與主對角線平行的斜線視為一個序列。對于位于主對角線及其下方的

2025-08-15&#xff1a;按對角線進行矩陣排序。用go語言&#xff0c;給你一個 n n 的整數矩陣&#xff0c;要求返回一個按下面規則調整后的矩陣&#xff1a;將每一條與主對角線平行的斜線視為一個序列。對于位于主對角線及其下方的那些斜線&#xff08;即所在位置的行索引 ≥ …

MySQL相關概念和易錯知識點(5)(索引、事務、MVCC)

目錄1.索引&#xff08;1&#xff09;局部性原理a.局部性原理在計算機中的地位b.pagec.池化技術&#xff08;Buffer Pool&#xff09;&#xff08;2&#xff09;如何理解索引&#xff08;3&#xff09;索引的原理a.page的構成b.多層目錄c.基于B樹的索引①B樹的特性在索引中的作…

SQLite 子查詢

SQLite 子查詢 SQLite 是一個輕量級的數據庫管理系統&#xff0c;廣泛應用于移動設備、嵌入式系統和桌面應用。在處理復雜的查詢時&#xff0c;子查詢&#xff08;Subquery&#xff09;是SQLite數據庫查詢語言中的一個強大工具。本文將詳細介紹SQLite子查詢的概念、用法及其在數…

區塊鏈系統審計方法論:全面指南與Python實踐

目錄 區塊鏈系統審計方法論:全面指南與Python實踐 1. 引言 2. 區塊鏈審計框架 3. 智能合約審計關鍵技術 3.1 靜態代碼分析 3.2 符號執行(Symbolic Execution) 4. 共識機制審計 4.1 PoW共識驗證 4.2 PBFT共識模擬 5. 數據完整性審計 5.1 Merkle樹驗證 6. 完整審計系統實現 7.…

分布式鎖—Redisson的公平鎖

1.Redisson公平鎖RedissonFairLock概述 (1)非公平和公平的可重入鎖 一.非公平可重入鎖 鎖被釋放后&#xff0c;排隊獲取鎖的線程會重新無序獲取鎖&#xff0c;沒有任何順序性可言。 二.公平可重入鎖 鎖被釋放后&#xff0c;排隊獲取鎖的線程會按照請求獲取鎖時候的順序去獲取…

上網行為安全概述和組網方案

一、上網行為安全概述1. 背景與需求互聯網的雙刃劍特性&#xff1a;網絡普及改變工作生活方式&#xff0c;業務向互聯網遷移。缺乏管理導致風險&#xff1a;帶寬濫用、監管困難、信息泄露、網絡違法、安全威脅。核心問題&#xff1a;帶寬濫用&#xff1a;P2P/流媒體占用70%帶寬…

某處賣600的【獨角仙】尾盤十分鐘短線 尾盤短線思路 手機電腦通用無未來函數

通達信指標【獨角仙】尾盤十分鐘套裝-主圖-副圖-選古指標&#xff0c;支持手機電腦使用。在股市收盤的前十分鐘第二天沖高賣出&#xff0c;信號可以盤中預警也可以尾盤選股&#xff0c;如果要保證信號固定建議是尾盤選股即可&#xff0c;當天信號固定后&#xff0c;不會產生漂移…

日志數據鏈路的 “搬運工”:Flume 分布式采集的組件分工與原理

flume詳解&#xff1a;分布式日志采集的核心原理與組件解析 在大數據體系中&#xff0c;日志采集是數據處理的第一步。Flume 作為 Apache 旗下的分布式日志采集工具&#xff0c;以高可用、高可靠、易擴展的特性&#xff0c;成為處理海量日志數據的首選方案。本文將從 Flume 的…

大消費新坐標中的淘寶大會員

一站式消費需要一站式權益。作者|古廿編輯|楊舟淘寶的大會員體系落地了。8月6日&#xff0c;淘寶首次整合餓了么、飛豬等阿里系平臺資源&#xff0c;推出覆蓋購物、外賣、出行、旅游的一體化會員體系——用戶在三大平臺的消費&#xff0c;都能累積淘氣值&#xff0c;根據淘氣值…

MIME(多用途互聯網郵件擴展)

MIME&#xff08;Multipurpose Internet Mail Extensions&#xff09; MIME 是 多用途互聯網郵件擴展 的縮寫&#xff0c;它最初是為了解決傳統電子郵件只能傳輸純文本的局限性而設計的&#xff0c;后來逐漸成為互聯網中 數據格式標識與傳輸 的通用標準&#xff0c;被廣泛應用…

PHP imagick擴展安裝以及應用

Date: 2025-08-13 10:48:12 author: lijianzhan php_imagick是PHP的一個強大的擴展模塊&#xff0c;用于調用ImageMagick圖像處理庫的功能&#xff0c;支持處理JPEG、PNG、GIF等超過185種格式的圖像&#xff0c;實現縮放、旋轉、動畫生成等操作&#xff0c;常用于網頁圖片動態生…

2025年度14款CRM銷售管理系統橫向評測

本文深入對比了以下14款CRM銷售管理軟件&#xff1a;1.紛享銷客&#xff1b; 2.Zoho CRM&#xff1b; 3.紅圈銷售&#xff1b; 4.銷幫幫&#xff1b; 5.Salesforce&#xff1b; 6.Pipedrive&#xff1b; 7.Microsoft Dynamics 365&#xff1b; 8.悟空 CRM&#xff1b; 9.勵銷云…

akamai鼠標軌跡

各位肯定被akamai鼠標軌跡、點擊事件、鍵盤事件&#xff0c;網頁交互困擾 那么我們就研究一下鼠標軌跡、點擊事件AST解混淆, 拿到解混淆后的代碼&#xff0c; 如下&#xff0c;sensor_data就是我們要搞的參數 如何解混淆這里就不贅述了&#xff0c;需要的可以看我上一篇文章&am…

飛算JavaAI開發全流程解析:從自然語言到可運行工程的智能進化

引言 在數字經濟時代&#xff0c;企業級應用開發面臨著需求多變、交付周期緊、質量要求高的三重挑戰。傳統Java開發模式依賴人工進行需求確認、架構設計、代碼編寫和測試驗證&#xff0c;導致開發效率低下、溝通成本高企。據統計&#xff0c;一個中等規模的項目需要平均8周完成…

垃圾回收標記算法:三色標記

文章目錄1 三色標記流程1.1 初始標記1.2 并發標記1.3 重新標記1.4 清除階段&#xff08;Sweep&#xff09;1.5 為什么初始標記和重新標記需要STW&#xff0c;而并發標記不需要?2 并發標記的寫屏障3 多標問題4.漏標問題4.1 漏標的兩個必要條件4.2 解決方案一&#xff1a;增量更…

反射的詳解

目錄一、反射1.JDK,JRE,JVM的關系2.什么是反射3. 三種獲取Class對象(類的字節碼)的方式4.Class常用方法5. 獲取類的構造器6.反射獲取成員變量&使用7.反射獲取成員方法8.綜合例子一、反射 1.JDK,JRE,JVM的關系 三者是Java運行環境的核心組成部分&#xff0c;從包含關系上看…

Grafana Tempo日志跟蹤平臺

以下是Grafana Tempo文檔的總結&#xff08;基于最新版文檔內容&#xff09;&#xff1a; 核心概念 分布式追蹤系統&#xff1a;Tempo是開源的分布式追蹤后端&#xff0c;專注于高吞吐量、低成本存儲和與現有監控生態的深度集成 架構組成&#xff1a; Distributor&#xff1a…

Qt基本控件

Qt 的基本控件是構建用戶界面的基礎&#xff0c;涵蓋了按鈕、輸入框、容器、顯示組件等&#xff0c;適用于傳統 Widget 開發&#xff08;基于 QWidget&#xff09;。以下是常用基本控件的分類總結&#xff1a;一、按鈕類控件用于觸發交互操作&#xff0c;如提交、取消、選擇等。…

用Voe3做AI流量視頻,條條10W+(附提示詞+白嫖方法)

最近 AI 視頻的風從大洋彼岸吹過來&#xff0c;Voe3 的技術升級&#xff0c;誕生了很多很有意思的玩法。 比如&#xff1a;AI ASMR 切水果解壓視頻&#xff0c;卡皮巴拉旅行博主、雪怪 AI Vlog&#xff0c;動物奧運會、第一人稱視角穿越古戰場直播。 這些視頻的流量很好&…

嵌入式學習的第四十八天-中斷+OCP原則

一、GIC通用中斷控制器 1.GIC通用中斷控制器 GIC 是 ARM 公司給 Cortex-A/R 內核提供的一個中斷控制器&#xff0c;GIC接收眾多外部中斷&#xff0c;然后對其進行處理&#xff0c;最終通過VFIQ、VIRQ、FIQ 和 IRQ給內核&#xff1b;這四個 信號的含義如下&#xff1a; VFIQ:虛擬…