iOS数据结构实战:蛇形矩阵与有序链表的类实现详解

📅 2026/7/27 4:40:52 👁️ 阅读次数 📝 编程学习
iOS数据结构实战:蛇形矩阵与有序链表的类实现详解

1. 项目概述:一个iOS准新手的“投名状”

最近在帮一个学弟复盘他准备河大iOS实验室的考核项目,挺有意思的。他给我的项目标题是“蛇形矩阵,有序插入链表,类实现”,一看就知道是个经典的、考察综合编程能力的题目。这不像那些花里胡哨的App,它更像是一份“投名状”——面试官不看你UI画得多漂亮,而是想透过这几行代码,看看你的基本功、数据结构理解、面向对象思维和代码风格是否扎实。很多新手一上来就想搞个酷炫的界面,但往往在链表指针指飞了、矩阵下标越界这种基础问题上栽跟头。这个项目恰恰是绕开表面繁华,直击核心功底的试金石。它适合所有正在学习数据结构、准备技术面试,尤其是想进入高校实验室或寻求初级开发岗位的朋友。通过实现它,你能系统性地检验自己对数组、链表、类封装等知识的掌握程度,而不仅仅是“知道”概念。

2. 核心需求与设计思路拆解

2.1 题目背后的三重考察点

这个标题看似简单,实则暗含了三个递进的考察层次,我们逐一拆解:

  1. 蛇形矩阵生成:这是对二维数组操作和逻辑控制能力的考察。所谓“蛇形”,指的是填充数字时路径像蛇一样蜿蜒曲折。常见的有两种:一种是从左上角开始,先向右,到底后向下,再向左……呈“回”字形填充;另一种是“之”字形,即奇数行从左到右,偶数行从右到左。这里通常指第一种“回”字形,它需要你精准地控制数组下标的移动方向(右、下、左、上)和边界判断(是否撞墙或遇到已填充位置)。

  2. 有序插入链表:这是对动态数据结构和基础算法的考察。链表的有序插入,核心是遍历找到正确的插入位置。这里的关键在于,你插入的数据是什么?题目通常会将蛇形矩阵中的元素(或其某种变换)作为数据源,按某种顺序(如数值大小)插入到一个初始为空的链表中。这考察了你对链表节点操作(创建、链接)、遍历逻辑和边界条件(插入头部、中间、尾部)的掌握。

  3. 类实现:这是对面向对象编程和代码组织能力的考察。你不能把所有的代码都堆在main函数里。面试官希望看到你将“矩阵”和“链表”这两个概念抽象成类(或结构体),并封装其数据(如矩阵的行列、元素数组;链表的头节点)和行为(如生成蛇形矩阵、打印矩阵、链表插入、链表遍历打印)。这体现了你的工程化思维,让代码更清晰、易维护、可复用。

2.2 整体方案设计

基于以上分析,一个清晰的设计方案浮出水面。我们将创建两个核心类:SnakeMatrixSortedLinkedList

  • SnakeMatrix

    • 属性rows(行数),cols(列数),matrix(一个二维整型数组,用于存储矩阵元素)。
    • 方法
      • init(rows: Int, cols: Int):构造函数,初始化一个指定行列的零矩阵。
      • generateSnake():核心方法,实现蛇形填充算法,将1到rows*cols的数字填入matrix
      • printMatrix():将矩阵格式化打印到控制台,便于调试和展示。
      • flattenedElements()(可选):将二维矩阵按行优先转换为一维数组,为链表插入提供数据源。
  • SortedLinkedList

    • 内部类Node,包含value(值)和next(指向下一个节点的指针)。
    • 属性head(头节点指针,初始为nil)。
    • 方法
      • insertSorted(_ value: Int):核心方法,将一个整数按升序插入链表。
      • printList():遍历链表并打印所有节点的值。

主程序逻辑将串联两者:先创建并生成蛇形矩阵,打印出来确认;然后获取矩阵的所有元素,依次调用链表的insertSorted方法插入;最后打印有序链表,验证结果。

注意:这里有一个关键设计决策。链表插入的数据源,我们选择将蛇形矩阵“拍平”(Flatten)成一个一维数组。这个数组的元素顺序是蛇形填充后的物理存储顺序(即行优先读取二维数组)。这意味着,链表最终的有序性,是基于元素本身的数值大小,而非它们在蛇形矩阵中的位置。这更能体现“有序插入”算法的通用性。

3. 核心细节解析与实操要点

3.1 蛇形矩阵生成的“转向”艺术

蛇形填充算法的精髓在于“方向控制”和“边界碰撞检测”。我们可以定义四个方向向量:右(0, 1)、下(1, 0)、左(0, -1)、上(-1, 0)。初始方向向右,当前位置在(0,0)

填充过程是一个循环,从1填到rows*cols。每一步:

  1. 将当前数字填入matrix[currentRow][currentCol]
  2. 计算下一个预探位置:根据当前方向,算出nextRow = currentRow + directionRow,nextCol = currentCol + directionCol
  3. 碰撞检测:判断nextRownextCol是否超出矩阵边界(<0>=rows/cols),或者matrix[nextRow][nextCol]是否已经被填充过(值不为0)。
  4. 转向:如果发生碰撞,就需要改变方向。方向切换的顺序是固定的:右 -> 下 -> 左 -> 上 -> 右……形成一个循环。我们可以用一个方向数组[(0,1), (1,0), (0,-1), (-1,0)]和当前方向索引来实现。
  5. 更新当前方向为新的方向,然后根据新方向重新计算下一个位置,并移动当前位置。
// 方向数组:右, 下, 左, 上 let directions = [(0, 1), (1, 0), (0, -1), (-1, 0)] var directionIndex = 0 // 起始方向:右 var currentRow = 0, currentCol = 0 for num in 1...totalCount { matrix[currentRow][currentCol] = num // 计算下一个预期位置 let nextRow = currentRow + directions[directionIndex].0 let nextCol = currentCol + directions[directionIndex].1 // 判断是否需要转向:越界或已填充 if nextRow < 0 || nextRow >= rows || nextCol < 0 || nextCol >= cols || matrix[nextRow][nextCol] != 0 { directionIndex = (directionIndex + 1) % 4 // 转向 } // 根据当前(或新的)方向移动 currentRow += directions[directionIndex].0 currentCol += directions[directionIndex].1 }

实操心得:最容易出错的地方在于转向后位置的更新。必须在判断碰撞并可能转向后,使用最新的方向来更新当前位置。如果直接用碰撞前计算的nextRownextCol,逻辑就乱了。另外,矩阵初始化一定要用0,这是判断位置是否“已访问”的关键标志。

3.2 链表有序插入的“探路”与“搭桥”

有序插入链表比普通尾部插入复杂,因为你需要为待插入的节点找到它的“家”。基本思路是遍历链表,找到第一个值大于待插入值的节点,然后将新节点插入到这个节点之前。这里要处理两个特殊情况:

  1. 插入头部:如果链表为空,或者待插入值比头节点值还小,新节点应成为新的头节点。
  2. 插入中间或尾部:需要维护一个previous(前驱)指针,指向当前检查节点的前一个节点。当找到合适位置时,操作是:newNode.next = currentNode; previous.next = newNode
func insertSorted(_ value: Int) { let newNode = Node(value: value) // 情况1:链表为空,或新值小于头节点值 if head == nil || value < head!.value { newNode.next = head head = newNode return } // 情况2:需要找到插入位置 var currentNode = head var previousNode: Node? = nil while currentNode != nil && currentNode!.value < value { previousNode = currentNode currentNode = currentNode!.next } // 此时,currentNode是第一个>=value的节点,previousNode是其前驱 newNode.next = currentNode previousNode!.next = newNode // 注意:因为情况1已处理,此处previousNode一定不为nil }

注意事项:在遍历寻找位置时,循环条件是currentNode != nil && currentNode!.value < value。这意味着我们寻找的是“第一个值不小于待插入值的节点”,以便插入其前。同时,要小心处理previousNode在插入头部时为nil的情况,我们在代码中已提前处理。这是链表操作中空指针异常的常见来源。

3.3 类的设计与Swift语法细节

在Swift中实现,我们需注意语言特性。我们将使用class来定义SnakeMatrixSortedLinkedList,因为我们需要引用语义。Node作为内部类,也使用class

  • 属性封装:矩阵的行列数在初始化后不应改变,可以声明为let常量。二维数组matrix需要被内部方法修改,但不应从外部直接篡改,应保持为private
  • 错误处理:简单的实现可以假设输入的行列是正整数。更健壮的实现可以在init中使用guard语句进行检查。
  • 打印函数:为了输出美观,可以在printMatrix中使用String(format:)来对齐数字,特别是在矩阵较大时。
class SnakeMatrix { let rows: Int let cols: Int private var matrix: [[Int]] init(rows: Int, cols: Int) { guard rows > 0, cols > 0 else { fatalError("行数和列数必须为正整数。") } self.rows = rows self.cols = cols self.matrix = Array(repeating: Array(repeating: 0, count: cols), count: rows) } // ... 其他方法 }

4. 完整实现与代码逐行解读

4.1 SnakeMatrix类的完整实现

class SnakeMatrix { let rows: Int let cols: Int private var matrix: [[Int]] init(rows: Int, cols: Int) { guard rows > 0, cols > 0 else { fatalError("行数和列数必须为正整数。") } self.rows = rows self.cols = cols // 初始化一个全0的二维数组 self.matrix = Array(repeating: Array(repeating: 0, count: cols), count: rows) } func generateSnake() { let total = rows * cols // 方向向量:右(0,1), 下(1,0), 左(0,-1), 上(-1,0) let dirs = [(0, 1), (1, 0), (0, -1), (-1, 0)] var dirIdx = 0 // 当前方向索引,从“右”开始 var row = 0, col = 0 // 当前位置 for num in 1...total { matrix[row][col] = num // 计算下一个预期位置 let nextRow = row + dirs[dirIdx].0 let nextCol = col + dirs[dirIdx].1 // 判断是否需要转向:越界或位置已被占用 if nextRow < 0 || nextRow >= rows || nextCol < 0 || nextCol >= cols || matrix[nextRow][nextCol] != 0 { dirIdx = (dirIdx + 1) % 4 // 顺时针转向 } // 根据当前方向(可能已转向)移动到下一个位置 row += dirs[dirIdx].0 col += dirs[dirIdx].1 } } func printMatrix() { let maxNumWidth = String(rows * cols).count // 计算最大数字的宽度用于对齐 for rowArray in matrix { for element in rowArray { print(String(format: "%\(maxNumWidth)d", element), terminator: " ") } print() // 换行 } } /// 将蛇形矩阵按行优先展开成一维数组 func flattenedElements() -> [Int] { var result = [Int]() for rowArray in matrix { result.append(contentsOf: rowArray) } return result } }

关键点解读

  • guard语句提供了基本的输入校验。
  • matrix的初始化使用了Swift的Array(repeating:count:)构造器,这是创建多维数组的简洁方式。
  • printMatrix中,我们根据最大数字的位数来格式化输出,这样即使数字位数不同,矩阵也能对齐,视觉上更清晰。String(format:)在这里非常实用。
  • flattenedElements()方法使用append(contentsOf:)来高效地拼接数组。

4.2 SortedLinkedList类的完整实现

class SortedLinkedList { class Node { let value: Int var next: Node? init(value: Int) { self.value = value self.next = nil } } private var head: Node? init() { self.head = nil } func insertSorted(_ value: Int) { let newNode = Node(value: value) // 情况1:插入到链表头部(链表为空或新值最小) if head == nil || value < head!.value { newNode.next = head head = newNode return } // 情况2:遍历寻找插入位置 var currentNode = head var previousNode: Node? = nil while currentNode != nil && currentNode!.value < value { previousNode = currentNode currentNode = currentNode!.next } // 插入到previousNode和currentNode之间 newNode.next = currentNode previousNode!.next = newNode // 由于情况1已排除,previousNode在此处必有值 } func printList() { var currentNode = head while let node = currentNode { print(node.value, terminator: " -> ") currentNode = node.next } print("nil") } }

关键点解读

  • Node被定义为SortedLinkedList的内部类,逻辑上更紧密。
  • insertSorted方法清晰地分为了头部插入和中间/尾部插入两种情况,逻辑层次分明。
  • 在遍历寻找位置时,使用while let的安全展开方式(在printList中)或强制展开(在insertSorted的循环中,因为head非空且value >= head!.value已保证),需要根据上下文谨慎选择。在insertSorted的循环后,我们确信previousNode不为nil,因此可以安全强制展开。
  • printList以“-> nil”结尾,是链表打印的常见格式,清晰地显示了链表的结束。

4.3 主程序与测试

// 主程序 func main() { print("=== 蛇形矩阵生成 ===") let snakeMatrix = SnakeMatrix(rows: 4, cols: 5) snakeMatrix.generateSnake() snakeMatrix.printMatrix() print("\n=== 将矩阵元素有序插入链表 ===") let sortedList = SortedLinkedList() let elements = snakeMatrix.flattenedElements() for element in elements { sortedList.insertSorted(element) } print("生成的有序链表:") sortedList.printList() // 验证:链表是否严格升序? print("\n=== 验证链表有序性 ===") var currentNode = sortedList.head // 注意:这里为了演示访问了私有属性,实际应通过方法暴露 var prevValue = Int.min var isSorted = true while let node = currentNode { if node.value < prevValue { isSorted = false break } prevValue = node.value currentNode = node.next } print("链表是否严格升序:\(isSorted)") } // 执行 main()

运行上述代码,你会看到类似以下输出:

=== 蛇形矩阵生成 === 1 2 3 4 5 14 15 16 17 6 13 20 19 18 7 12 11 10 9 8 === 将矩阵元素有序插入链表 === 生成的有序链表: 1 -> 2 -> 3 -> 4 -> 5 -> 6 -> 7 -> 8 -> 9 -> 10 -> 11 -> 12 -> 13 -> 14 -> 15 -> 16 -> 17 -> 18 -> 19 -> 20 -> nil === 验证链表有序性 === 链表是否严格升序:true

5. 常见问题、调试技巧与扩展思考

5.1 典型问题与排查清单

在实际编写和调试过程中,你可能会遇到以下问题:

问题现象可能原因排查与解决思路
蛇形矩阵填充时数组索引越界(Index out of range)1. 方向转向逻辑错误,导致计算出的下一个位置超出数组边界。
2. 循环终止条件有误,填充的数字超过了rows*cols
1.打印调试:在循环内打印currentRow,currentCol,dirIdx和计算出的nextRow,nextCol,观察转向时机是否正确。
2.检查边界条件:确保if判断中的边界是>= rows>= cols,而不是>。Swift数组索引从0开始。
蛇形矩阵填充出现重复数字或漏填碰撞检测条件matrix[nextRow][nextCol] != 0可能因为矩阵未正确初始化(非全0)而失效。1. 确认matrix初始化时所有元素为0。
2. 检查转向后,是否用新的方向更新了位置,而不是用碰撞前计算的nextRow/Col
链表插入后顺序不对1.insertSorted中遍历找位置的条件错误。
2. 插入头部和插入其他位置的逻辑混淆。
1.画图:在纸上画出链表节点和待插入值,手动模拟代码逻辑。
2.单元测试:分别测试插入空链表、插入比头小、插入中间、插入尾部的情况。
3. 检查while循环条件:是找第一个>=value的节点(插入其前),还是找最后一个<value的节点(插入其后),必须统一。
打印链表时程序崩溃(EXC_BAD_ACCESS)链表节点连接错误,形成了环(cycle),导致while循环无限进行。1. 在printList中加入计数器,如果打印次数远超预期节点数,很可能有环。
2. 仔细检查newNode.nextpreviousNode.next的赋值逻辑,确保不会让某个节点的next指向了之前的节点。
生成的蛇形矩阵不是预期的“回”字形方向向量顺序或转向逻辑与预期不符。确认dirs数组的顺序是[(0,1), (1,0), (0,-1), (-1,0)],这是顺时针“右下左上”的顺序。如果你想逆时针,需要调整顺序。

5.2 调试技巧:LLDB与打印的艺术

对于Swift项目,尤其是命令行工具,善用打印和LLDB调试器能极大提升效率。

  1. 条件打印:在复杂循环中,不要一股脑全打印。可以设置一个标志位,或者只在特定条件下打印。

    let debug = true if debug && (row == 0 && col == 0) { print("开始填充: dirIdx=\(dirIdx)") }
  2. 使用dump函数:打印对象内部状态比单纯用print更清晰。

    import Foundation // 在链表插入后,查看头节点及其后续结构 dump(sortedList.head)
  3. LLDB断点与命令:在Xcode或lldb命令行中,你可以在关键函数(如insertSorted)开始处打上断点。

    • po variableName: 打印对象描述。
    • fr v -L variableName: 更详细地查看变量。
    • n: 单步跳过(Step Over)。
    • s: 单步进入(Step Into)。
    • c: 继续运行。

5.3 项目扩展与思考

完成基础版本后,你可以尝试以下扩展,这会让你的项目在面试中更加出彩:

  1. 支持泛型:将SortedLinkedList改造成泛型类SortedLinkedList<T: Comparable>,使其可以排序任何遵循Comparable协议的类型(如String,Double)。
  2. 更复杂的蛇形路径:实现“之”字形蛇形矩阵(Zigzag),即奇数行从左到右,偶数行从右到左。
  3. 双向链表:将链表升级为双向链表(DoublyLinkedList),每个节点有prevnext指针。有序插入的逻辑需要同时维护前驱和后继的链接。
  4. 算法优化:当前链表插入算法时间复杂度为O(n²)(n个元素,每个插入最坏需遍历当前链表)。思考如何优化?可以考虑在插入时使用“哨兵节点”(Dummy Node)来简化边界判断代码。
  5. 单元测试:为SnakeMatrixSortedLinkedList编写单元测试(使用XCTest),测试各种边界情况(如1x1矩阵、单行矩阵、单列矩阵、空链表插入、重复值插入等)。这体现了你的工程素养。
  6. 可视化:尝试用SwiftUI简单绘制出蛇形矩阵的填充动画,或者将链表结构图形化展示出来。这能将枯燥的算法变得生动,虽然对实验室考核可能不是必须,但能展示你的综合能力和热情。

这个项目麻雀虽小,五脏俱全。它考察的不仅仅是编码能力,更是你思考问题的系统性、代码的整洁度以及对基础知识的深入理解。把它做精做透,远比堆砌一堆华而不实的功能更有说服力。在准备类似考核时,记得代码写完后,自己多读几遍,思考是否有更清晰的命名、更简洁的逻辑、更完善的错误处理。这些细节,往往是区分“不错”和“出色”的关键。