文章目录
- 摘要
- 描述
- 题解答案
- 题解代码分析
- 分段讲解
- 示例测试及结果
- 时间复杂度
- 空间复杂度
- 总结
摘要
在开发中,链表结构经常出现在缓存淘汰、操作系统任务调度、或是 LRU 算法中,尤其是对节点位置的灵活操作更是链表的强项。LeetCode 第 328 题「奇偶链表」就给我们出了一个非常典型的“按位置分组再重组”的链表题目。今天我们用 Swift 带你一步步理清它的逻辑,不仅实现,还要分析每一行代码的作用。
描述
给定单链表的头节点 head
,将所有索引为奇数的节点和索引为偶数的节点分别分组,保持它们原有的相对顺序,然后把偶数索引节点分组连接到奇数索引节点分组之后,返回重新排序的链表。
第一个节点的索引被认为是 奇数 , 第二个节点的索引为 偶数 ,以此类推。
请注意,偶数组和奇数组内部的相对顺序应该与输入时保持一致。
你必须在 O(1)
的额外空间复杂度和 O(n)
的时间复杂度下解决这个问题。
示例 1:
输入: head = [1,2,3,4,5]
输出: [1,3,5,2,4]
示例 2:
输入: head = [2,1,3,5,6,4,7]
输出: [2,3,6,7,1,5,4]
提示:
n ==
链表中的节点数0 <= n <= 104
-106 <= Node.val <= 106
题解答案
我们可以把链表分成两条线:
- 一条是奇数索引节点组成的链;
- 一条是偶数索引节点组成的链;
我们遍历一遍,把这两条链连起来就搞定了。因为我们只用了几个额外指针,空间上是 O(1)
;每个节点最多访问一次,时间上是 O(n)
。
题解代码分析
class ListNode {var val: Intvar next: ListNode?init(_ val: Int) {self.val = valself.next = nil}
}func oddEvenList(_ head: ListNode?) -> ListNode? {guard let head = head, let next = head.next else {return head // 如果链表为空或只有一个节点,直接返回}var odd = head // 奇数位置的起点var even = next // 偶数位置的起点let evenHead = even // 保存偶数起点,最后连接用while let nextOdd = even.next {odd.next = nextOddodd = nextOddeven.next = nextOdd.nextif let nextEven = even.next {even = nextEven}}odd.next = evenHead // 把奇数链的尾巴接上偶数链的头return head
}
分段讲解
- 初始化阶段:
guard let head = head, let next = head.next else { return head }
判断是否为空链表或仅有一个节点,不用处理,直接返回。
- 定义指针:
var odd = head
var even = next
let evenHead = even
奇链和偶链分别以 odd
和 even
为头,注意我们还要保留 evenHead
,后面连接会用到。
- 链表分离过程:
while let nextOdd = even.next {odd.next = nextOddodd = nextOddeven.next = nextOdd.nextif let nextEven = even.next {even = nextEven}
}
每一轮迭代,我们都让 odd
跳两个节点,even
也跳两个。直到链表末尾。遍历中奇偶节点自动分组完成。
- 拼接两个链表:
odd.next = evenHead
把奇数链表的尾部,接上偶数链表的头部。
示例测试及结果
我们来测试下:
// 构建测试链表 [1,2,3,4,5]
let node1 = ListNode(1)
let node2 = ListNode(2)
let node3 = ListNode(3)
let node4 = ListNode(4)
let node5 = ListNode(5)node1.next = node2
node2.next = node3
node3.next = node4
node4.next = node5// 调用函数
let newHead = oddEvenList(node1)// 打印结果
var current = newHead
while current != nil {print(current!.val, terminator: " ")current = current?.next
}
// 输出:1 3 5 2 4
时间复杂度
整条链表我们只遍历了一次,每个节点访问不超过两次。
- 时间复杂度:
O(n)
,其中n
是链表的长度。
空间复杂度
只用了固定数量的指针变量,没有创建新的节点或结构。
- 空间复杂度:
O(1)
,常数空间。
总结
这道题的关键在于两个链表交替构建,同时维护好头尾指针。在实际开发中,链表的拆分与重组很常见,比如分页展示、批量处理等功能也可能需要对数据链按规则拆分。
通过这道题,不仅练习了链表的结构操作,还能加深对“指针引用不等于复制”的理解。如果你能写出无 bug 的链表处理代码,那你对 Swift 的指针语义已经很熟练了!