链表(Linked list)是一种常见的基础数据结构,是一种线性表,但是并不会按线性的顺序存储数据,而是在每一个节点里存到下一个节点的指针(Pointer)。相对于数组,链表具有动态的大小和灵活的内存分配等优势。
基本概念
链表的基本概念如下:
-
节点(Node): 链表由一个个节点组成,每个节点包含两个部分:数据部分(存储元素的值)和指针部分(指向下一个节点的指针)。
-
头节点(Head Node): 链表的起始节点称为头节点,它是整个链表的入口点。
-
尾节点(Tail Node): 链表的最后一个节点称为尾节点,它的指针部分指向空值(NULL)。
常见的链表结构
定义一个链表节点类:
import Foundation
/// 定义链表节点类
class ListNode<T> {
var value: T
//一般而言,对于双向链表才有prev节点,这里统一加上
var prev: ListNode<T>?
var next: ListNode<T>?
init(value: T) {
self.value = value
self.prev = nil
self.next = nil
}
}
单向链表
单向链表(Singly Linked List):链表中最简单的一种是单向链表,它包含两个域,一个信息域和一个指针域。
+---+ +---+ +---+ +---+
| 1 | -> | 2 | -> | 3 | -> | 4 |
+---+ +---+ +---+ +---+
在单向链表中,每个节点只有一个指针,指向下一个节点。最后一个节点的指针指向空值(NULL)。
单链表简单实现:
/// 单链表
class SinglyLinkedList<T> where T: Equatable {
private var head: ListNode<T>?
private var tail: ListNode<T>?
func append(value: T) {
let newNode = ListNode(value: value)
if let tailNode = tail {
tailNode.next = newNode
} else {
head = newNode
}
tail = newNode
}
func printList() {
var currentNode = head
while let node = currentNode {
print(node.value)
currentNode = node.next
}
}
func verifyNextOfNode(nodeValue: T, expectedNextValue: T) -> Bool {
var currentNode = head
while let node = currentNode {
if node.value == nodeValue {
if let nextNode = node.next {
return nextNode.value == expectedNextValue
} else {
return false
}
}
currentNode = node.next
}
return false
}
}
// 测试示例
let linkedList = SinglyLinkedList<Int>()
linkedList.append(value: 1)
linkedList.append(value: 2)
linkedList.append(value: 3)
linkedList.printList()
let node1NextIs2 = linkedList.verifyNextOfNode(nodeValue: 1, expectedNextValue: 2)
print("Node 1's next is 2: \(node1NextIs2)")
双向链表
双向链表(Doubly Linked List):它包含三个域,一个信息域,一个向后的节点链接, 一个向前的节点链接。
+---+ +---+ +---+ +---+
NULL <- | 1 | <-> | 2 | <-> | 3 | <-> | 4 | -> NULL
+---+ +---+ +---+ +---+
在双向链表中,每个节点有两个指针,一个指向前一个节点,一个指向后一个节点。头节点的前指针和尾节点的后指针指向空值(NULL)。
双向链表简单实现:
/// 双向链表
class DoublyLinkedList<T> where T: Equatable {
private var head: ListNode<T>?
private var tail: ListNode<T>?
func append(value: T) {
let newNode = ListNode(value: value)
if let tailNode = tail {
tailNode.next = newNode
newNode.prev = tailNode
} else {
head = newNode
}
tail = newNode
}
func printList() {
var currentNode = head
while let node = currentNode {
print(node.value)
currentNode = node.next
}
}
func verifyAdjacentNodes(nodeValue: T, expectedPrevValue: T, expectedNextValue: T) -> Bool {
var currentNode = head
while let node = currentNode {
if node.value == nodeValue {
if let prevNode = node.prev, let nextNode = node.next {
return prevNode.value == expectedPrevValue && nextNode.value == expectedNextValue
} else {
return false
}
}
currentNode = node.next
}
return false
}
}
// 测试示例
let dlinkedList = DoublyLinkedList<Int>()
dlinkedList.append(value: 1)
dlinkedList.append(value: 2)
dlinkedList.append(value: 3)
dlinkedList.printList()
let node2PrevIs1NextIs3 = dlinkedList.verifyAdjacentNodes(nodeValue: 2, expectedPrevValue: 1, expectedNextValue: 3)
print("Node 2's next is 3, prev is 1: \(node2PrevIs1NextIs3)")
循环链表
循环链表(Circular Linked List): 首节点和末节点被连接在一起
+---+ +---+ +---+ +---+
| 1 | -> | 2 | -> | 3 | -> | 4 |
+---+ +---+ +---+ +---+
^ |
| v
+-----------------------+
在循环链表中,最后一个节点的指针不是指向空值(NULL),而是指向头节点,形成一个循环连接。
循环链表简单实现:
/// 循环列表
class CircularLinkedList<T> {
private var head: ListNode<T>?
func append(value: T) {
let newNode = ListNode(value: value)
if head == nil {
head = newNode
newNode.next = newNode
} else {
let tail = findTailNode()
tail.next = newNode
newNode.next = head
}
}
private func findTailNode() -> ListNode<T> {
var currentNode = head
while let node = currentNode, node.next !== head {
currentNode = node.next
}
return currentNode!
}
func printList() {
var currentNode = head
while let node = currentNode {
print(node.value)
currentNode = node.next
if currentNode === head {
break
}
}
}
}
// 测试示例
let clinkedList = CircularLinkedList<Int>()
clinkedList.append(value: 1)
clinkedList.append(value: 2)
clinkedList.append(value: 3)
clinkedList.printList()
数组与链表比较
数组和链表的主要区别和比较:
特征 | 数组 | 链表 |
---|---|---|
内存分配 | 连续的内存块 | 非连续的内存节点 |
大小的灵活性 | 固定大小,需要预先分配 | 动态大小,可以根据需要增长或缩小 |
随机访问 | 高效,通过索引直接访问 O(1) | 低效,需要从头节点开始遍历 O(n) |
插入和删除 | 低效,需要移动元素 O(n) | 高效,只需要更改指针的指向 O(1) |
内存利用 | 可能有浪费,如果未使用数组的全部空间 | 相对较高,每个节点需要额外的指针空间 |
插入和删除位置 | 通常在数组的末尾插入或删除元素 | 可以在任意位置插入或删除元素 |
实现复杂度 | 相对简单 | 相对复杂,需要处理指针的操作 |
适用场景 | 需要快速随机访问元素,元素数量较少且固定的情况 | 需要频繁的插入和删除操作,元素数量不确定或需要动态调整大小时 |
这只是对数组和链表的一般比较,具体应用还取决于特定的需求和上下文。有时,可以使用数组和链表的组合来充分发挥它们的优势。例如,可以使用数组来存储大量数据,并使用链表来处理插入和删除操作。
数组简单易用,在实现上使用的是连续的内存空间,可以借助 CPU 的缓存机制,预读数组中的数据,所以访问效率更高。而链表在内存中并不是连续存储,所以对 CPU 缓存不友好,没办法有效预读。数组的缺点是大小固定,一经声明就要占用整块连续内存空间。如果声明的数组过大,系统可能没有足够的连续内存空间分配给它,导致“内存不足(out of memory)”。如果声明的数组过小,则可能出现不够用的情况。这时只能再申请一个更大的内存空间,把原数组拷贝进去,非常费时。链表本身没有大小的限制,支持动态扩容,这也是它与数组最大的区别。
链表算法题
链表是一种常见的数据结构,解决链表相关的算法问题需要掌握链表的基本操作和一些常见的思考方法。以下是解决链表相关问题的一般思路:
- 理解问题:
- 首先,要充分理解问题陈述,包括输入和输出的要求,以及问题的约束条件。
- 定义节点结构:
- 定义链表节点的数据结构,通常包括一个值(或数据)和一个指向下一个节点的指针。
- 考虑边界情况:
- 确保你考虑了链表为空、只有一个节点、多个节点等各种可能的情况。
- 基本操作:
- 掌握链表的基本操作,包括插入节点、删除节点、反转链表、查找节点等。这些操作通常需要使用指针来遍历和操作链表。
- 使用快慢指针:
- 对于某些问题,快慢指针技巧非常有用。快慢指针通常用于检测链表中的循环、查找中间节点等。
- 使用递归:
- 一些链表问题可以通过递归来解决,如递归反转链表、合并有序链表等。递归方法可以简化问题的解决方案。
- 使用额外的数据结构:
- 在某些情况下,可以使用额外的数据结构(如哈希表)来解决链表问题。这可以提高查找效率,但要注意空间复杂度。
- 问题分类:
- 将链表问题分类,例如,分为单链表、双链表、循环链表等,根据不同类型的链表采用不同的解决方法。
- 思考优化:
- 一旦你有一个解决方案,思考是否有优化的空间,如减少遍历次数、节省内存空间等。
- 写代码和测试:
- 根据你的思路编写代码,并进行测试。确保你的代码能够处理各种情况,并且在边界条件下能够正确运行。
- 时间和空间复杂度分析:
- 分析你的算法的时间复杂度和空间复杂度。优化算法以减少复杂度通常是一个好主意。
- 处理异常情况:
- 考虑处理异常情况,如输入无效数据、内存不足等情况。
- 优化和改进:
- 如果时间允许,可以不断优化你的解决方案,提高性能或减少内存占用。
一些常见的链表问题包括反转链表、合并有序链表、检测循环、查找中间节点、删除节点等。根据具体的问题,你可以应用上述思路和技巧来解决链表相关的算法问题。不断练习和挑战不同类型的问题将有助于提高你的链表问题解决能力。
先定义一个链表节点类和链表打印方法,方便后续测试:
import Foundation
/// 定义链表节点类
class ListNode {
var value: Int
var next: ListNode?
init(value: Int) {
self.value = value
self.next = nil
}
}
/// 打印链表
func printLinkedList(_ head: ListNode?) {
var currentNode: ListNode? = head
while let node = currentNode {
//稍微处理下 便于后面测试输出结果对比
if node.next == nil {
print(node.value,terminator: "\n")
} else {
print(node.value,terminator: " ")
}
currentNode = node.next
}
}
链表节点的插入与删除
当在链表中插入新节点时,可以使用两种常见的策略:头插法(Head Insertion)和尾插法(Tail Insertion)。这些策略决定了新节点在链表中的位置。
- 头插法(Head Insertion):在头插法中,新节点被插入到链表的头部,成为新的头节点。这意味着新节点的指针将指向原来的头节点,而链表的头指针将指向新节点。头插法的时间复杂度为 O(1)。
假设有一个包含四个节点的链表:1 -> 2 -> 3 -> 4。头插法示意图:
+---+ +---+ +---+ +---+
| 4 | -> | 3 | -> | 2 | -> | 1 |
+---+ +---+ +---+ +---+
通过头插法,新节点被插入到链表的头部。新节点 4 成为新的头节点,其指针指向原来的头节点 1。每个节点包含一个值和一个指针,指针指向下一个节点。
头插法常用于需要逆序构建链表的情况,因为每次插入的新节点都会成为新的头节点,链表的顺序会被颠倒。
- 尾插法(Tail Insertion):在尾插法中,新节点被插入到链表的尾部,成为新的尾节点。这意味着新节点的指针将指向空值(NULL),而原来的尾节点的指针将指向新节点。尾插法的时间复杂度为 O(n)。
尾插法示意图:
+---+ +---+ +---+ +---+
| 1 | -> | 2 | -> | 3 | -> | 4 |
+---+ +---+ +---+ +---+
通过尾插法,新节点被插入到链表的尾部。新节点 4 成为新的尾节点,原来的尾节点 3 的指针指向新节点。每个节点包含一个值和一个指针,指针指向下一个节点。
尾插法常用于保持链表的顺序不变,每次插入新节点都在链表的尾部进行。
头插法及尾插法代码实现:
/// 定义链表
class LinkedList {
var head: ListNode?
var tail: ListNode?
// 头插法
func insertAtHead(_ value: Int) {
let newNode = ListNode(value:value)
if head == nil {
head = newNode
tail = newNode
} else {
newNode.next = head
head = newNode
}
}
// 尾插法
func insertAtTail(_ value: Int) {
let newNode = ListNode(value:value)
if head == nil {
head = newNode
tail = newNode
} else {
tail?.next = newNode
tail = newNode
}
}
}
// 示例使用
let linkedList = LinkedList()
// 头插法插入节点
linkedList.insertAtHead(3)
linkedList.insertAtHead(2)
linkedList.insertAtHead(1)
// 输出链表:1 -> 2 -> 3
printLinkedList(linkedList.head)
// 尾插法插入节点
linkedList.insertAtTail(4)
linkedList.insertAtTail(5)
// 输出链表:1 -> 2 -> 3 -> 4 -> 5
printLinkedList(linkedList.head)
- 删除指定节点:给定要删除的节点,直接将该节点从链表中移除。这需要修改节点前一个节点的 next 指针,将其指向要删除节点的下一个节点,从而跳过要删除的节点。
func deleteNode(_ node: ListNode) {
if let nextNode = node.next {
node.value = nextNode.value
node.next = nextNode.next
} else {
// 要删除的节点是尾节点,无法修改前一个节点的next指针,所以只能将其置为0
node.value = 0
}
}
- 删除指定值的节点:给定要删除的节点值,在链表中查找该值,并删除对应的节点。
func deleteNodeWithValue(_ head: ListNode?, value: Int) -> ListNode? {
let dummy: ListNode = ListNode(value: 0)
dummy.next = head
var prevNode: ListNode? = dummy
var currentNode: ListNode? = head
while let node = currentNode {
if node.value == value {
prevNode?.next = node.next
node.next = nil
break
}
prevNode = node
currentNode = node.next
}
return dummy.next
}
删除节点测试代码:
// 创建一个示例链表
let nd1 = ListNode(value: 1)
let nd2 = ListNode(value: 2)
let nd3 = ListNode(value: 3)
nd1.next = nd2
nd2.next = nd3
// 打印原始链表
print("Before delete nd2")
// 1 2 3
printLinkedList(nd1)
// 删除节点nd2
deleteNode(nd2)
// 删除指定值为2的节点
let updateHead = deleteNodeWithValue(nd1, value: 2)
// 1 3
printLinkedList(updateHead)
// 打印删除节点后的链表
print("After delete nd2")
// 1 3
printLinkedList(nd1)
反转单链表
给定一个单链表的头节点,返回反转后的链表的头节点。例如,输入1->2->3->4->5,输出5->4->3->2->1。可以用递归或者迭代两种方式来实现。
- 递归方式:递归的思路是从链表的尾部开始反转,每次将当前节点的next指向前一个节点,直到到达头节点。
// 递归方式反转单链表
func reverseListRecursively(_ head: ListNode?) -> ListNode? {
// 如果链表为空或者只有一个节点,则直接返回头节点
if head == nil || head?.next == nil {
return head
}
// 递归地反转剩余的链表
let newHead = reverseListRecursively(head?.next)
// 将当前节点的next指向前一个节点
head?.next?.next = head
// 将当前节点的next置为空
head?.next = nil
// 返回新的头节点
return newHead
}
- 迭代方式:迭代的思路是用一个prev指针记录前一个节点,一个curr指针记录当前节点,每次将curr的next指向prev,然后更新prev和curr,直到curr为空。
// 迭代方式反转单链表
func reverseListIteratively(_ head: ListNode?) -> ListNode? {
// 初始化前一个节点和当前节点
var prev: ListNode? = nil
var curr: ListNode? = head
// 遍历链表
while curr != nil {
// 记录当前节点的下一个节点
let next = curr?.next
// 将当前节点的next指向前一个节点
curr?.next = prev
// 更新前一个节点和当前节点
prev = curr
curr = next
}
// 返回新的头节点,即原来的尾节点
return prev
}
测试示例:
// 创建一个示例链表
let node1 = ListNode(value: 1)
let node2 = ListNode(value: 2)
let node3 = ListNode(value: 3)
let node4 = ListNode(value: 4)
let node5 = ListNode(value: 5)
node1.next = node2
node2.next = node3
node3.next = node4
node4.next = node5
// 打印原始链表
print("Before reversing")
printLinkedList(node1)
// 反转链表
//let reversedHead = reverseListRecursively(node1)
let reversedHead = reverseListIteratively(node1)
// 打印反转后的链表
print("After reversing")
printLinkedList(reversedHead)
//输出结果:
/*
Before reversing
1 2 3 4 5
After reversing
5 4 3 2 1
*/
可以看到,上述例子递归方式和迭代方式都实现了反转单链表的功能。
合并两个有序链表
给定两个升序排列的单链表,返回合并后的升序排列的单链表。例如,输入1->3->5和2->4->6,输出1->2->3->4->5->6。也可以用递归或者迭代两种方式来实现。
- 递归方式:递归的思路是比较两个链表的头节点,选择较小的那个作为合并后链表的头节点,并将其next指向剩余两个链表合并后的结果。
func mergeTwoListsRecursively(_ l1: ListNode?, _ l2: ListNode?) -> ListNode? {
// 如果其中一个链表为空,则返回另一个链表
if l1 == nil {
return l2
}
if l2 == nil {
return l1
}
// 比较两个链表的头节点,选择较小的那个作为合并后链表的头节点,并将其next指向剩余两个链表合并后的结果
if l1!.value < l2!.value {
l1!.next = mergeTwoListsRecursively(l1!.next, l2)
return l1
} else {
l2!.next = mergeTwoListsRecursively(l1, l2!.next)
return l2
}
}
- 迭代方式:迭代的思路是用一个哨兵节点作为合并后链表的虚拟头节点,并用一个指针记录当前位置,每次比较两个链表的头节点,选择较小的那个接在指针后面,并更新指针和对应的链表头节点,直到其中一个链表为空。
func mergeTwoListsIteratively(_ l1: ListNode?, _ l2: ListNode?) -> ListNode? {
// 创建一个哨兵节点作为合并后链表的虚拟头节点
let dummy = ListNode(value: 0)
// 用一个指针记录当前位置
var curr: ListNode? = dummy
// 用两个指针分别遍历两个链表
var p1 = l1
var p2 = l2
// 当两个链表都不为空时,循环比较它们的头节点,选择较小的那个接在curr后面,并更新curr和对应的链表头节点
while p1 != nil && p2 != nil {
if p1!.value < p2!.value {
curr?.next = p1
p1 = p1?.next
} else {
curr?.next = p2
p2 = p2?.next
}
curr = curr?.next
}
// 当其中一个链表为空时,将另一个链表的剩余部分接在curr后面
if p1 == nil {
curr?.next = p2
}
if p2 == nil {
curr?.next = p1
}
// 返回合并后链表的真实头节点,即哨兵节点的next
return dummy.next
}
测试示例:
// 创建两个示例链表
let n1 = ListNode(value: 1)
let n3 = ListNode(value: 3)
let n5 = ListNode(value: 5)
n1.next = n3
n3.next = n5
let n2 = ListNode(value: 2)
let n4 = ListNode(value: 4)
let n6 = ListNode(value: 6)
n2.next = n4
n4.next = n6
print("Before merging")
printLinkedList(n1)
printLinkedList(n2)
let mergedHead = mergeTwoListsRecursively(n1, n2)
//let mergedHead = mergeTwoListsIteratively(n1, n2)
print("After merging")
printLinkedList(mergedHead)
//输出结果:
/*
Before merging
1 3 5
2 4 6
After merging
1 2 3 4 5 6
*/
可以看到,上述例子递归方式和迭代方式也都实现了两个有序链表的合并功能。
删除链表中倒数第n个节点
给定一个单链表和一个整数n,返回删除了倒数第n个节点后的链表。例如,输入1->2->3->4->5和n=2,输出1->2->3->5。
可以用双指针法来实现。双指针法的思路是用两个指针从头节点开始遍历链表,一个快指针先走n步,然后两个指针同时走,直到快指针到达尾节点,此时慢指针指向的就是倒数第n+1个节点,将其next指向下下个节点即可删除倒数第n个节点。注意要考虑边界情况,比如n大于链表长度或者等于链表长度时,需要删除头节点。代码实现如下:
func removeNthFromEnd(_ head: ListNode?, _ n: Int) -> ListNode? {
// 创建一个哨兵节点作为虚拟头节点,并将其next指向真实头节点
let dummy = ListNode(value: 0)
dummy.next = head
// 初始化快慢指针,并让它们都指向哨兵节点
var fast: ListNode? = dummy
var slow: ListNode? = dummy
// 让快指针先走n步
for _ in 0..<n {
fast = fast?.next
}
// 让快慢指针同时走,直到快指针到达尾节点
while fast?.next != nil {
fast = fast?.next
slow = slow?.next
}
// 此时慢指针指向的是倒数第n+1个节点,将其next指向下下个节点即可删除倒数第n个节点
slow?.next = slow?.next?.next
// 返回真实头节点,即哨兵节点的next
return dummy.next
}
// 创建一个示例链表
let no1 = ListNode(value: 1)
let no2 = ListNode(value: 2)
let no3 = ListNode(value: 3)
let no4 = ListNode(value: 4)
let no5 = ListNode(value: 5)
no1.next = no2
no2.next = no3
no3.next = no4
no4.next = no5
print("Before removing")
printLinkedList(no1)
let removed2FromEnd = removeNthFromEnd(no1, 2)
print("After removing")
printLinkedList(removed2FromEnd)
//输出结果
/*
Before removing
1 2 3 4 5
After removing
1 2 3 5
*/
在上述代码中,首先创建了一个哑节点(dummy),将其指向链表的头节点。然后,使用两个指针fast和 slow,并将fast指针向前移动n+1次。接下来,同时移动fast和slow指针,直到fast指针到达链表末尾。
一旦fast指针到达末尾,slow指针将指向倒数第n+1个节点。可以通过将slow指针的next指针指向下下个节点,从而删除倒数第n个节点。
最后,返回哑节点(dummy)的next指针,即删除了倒数第n个节点后的链表。
判断链表是否有环
要判断一个链表是否有环,并返回环的入口节点,可以使用快慢指针的方法。示例代码如下:
func detectCycle(_ head: ListNode?) -> ListNode? {
var slow: ListNode? = head
var fast: ListNode? = head
// 判断链表是否有环
while fast != nil && fast?.next != nil {
slow = slow?.next
fast = fast?.next?.next
if slow === fast {
// 快慢指针相遇,链表有环
break
}
}
if fast == nil || fast?.next == nil {
// 链表无环
return nil
}
// 找到环的入口节点
slow = head
while slow !== fast {
slow = slow?.next
fast = fast?.next
}
return slow
}
// 创建一个有环的示例链表
let l1 = ListNode(value: 1)
let l2 = ListNode(value: 2)
let l3 = ListNode(value: 3)
let l4 = ListNode(value: 4)
let l5 = ListNode(value: 5)
l1.next = l2
l2.next = l3
l3.next = l4
l4.next = l5
l5.next = l3 // 创建环,将尾节点指向l3
// 判断链表是否有环,并返回环的入口节点
let cycleNode = detectCycle(l1)
if let cycleNode = cycleNode {
print("链表有环,环的入口节点值为: \(cycleNode.value)")
} else {
print("链表无环")
}
链表实现LRU(Least Recently Used)缓存淘汰算法
链表的顺序可以表示数据项的访问顺序,最近访问的数据项位于链表的头部,最久未访问的数据项位于链表的尾部。当需要淘汰缓存中的数据项时,可以选择链表尾部的节点进行删除。
以下是基于链表实现LRU缓存淘汰算法的示例:
/// 定义LRUCache链表节点类
class LRUCacheNode<Key, Value> {
var key: Key
var value: Value
var next: LRUCacheNode<Key, Value>?
var prev: LRUCacheNode<Key, Value>?
init(key: Key, value: Value) {
self.key = key
self.value = value
self.next = nil
self.prev = nil
}
}
/// 定义LRU缓存类
class LRUCache<Key: Hashable, Value> {
private let capacity: Int
private var cache: [Key: LRUCacheNode<Key, Value>]
private var head: LRUCacheNode<Key, Value>?
private var tail: LRUCacheNode<Key, Value>?
init(capacity: Int) {
self.capacity = capacity
self.cache = [:]
self.head = nil
self.tail = nil
}
/// 获取缓存中的值
func get(_ key: Key) -> Value? {
if let node = cache[key] {
moveToHead(node)
return node.value
}
return nil
}
/// 插入或更新缓存中的值
func put(_ key: Key, _ value: Value) {
if let node = cache[key] {
node.value = value
moveToHead(node)
} else {
let newNode = LRUCacheNode(key: key, value: value)
cache[key] = newNode
addToHead(newNode)
if cache.count > capacity {
if let lastNode = tail {
removeNode(lastNode)
cache[lastNode.key] = nil
}
}
}
}
/// 将节点移动到链表头部
private func moveToHead(_ node: LRUCacheNode<Key, Value>) {
if node === head {
return
}
removeNode(node)
addToHead(node)
}
/// 将节点添加到链表头部
private func addToHead(_ node: LRUCacheNode<Key, Value>) {
if head == nil {
head = node
tail = node
} else {
node.next = head
head?.prev = node
head = node
}
}
/// 删除节点
private func removeNode(_ node: LRUCacheNode<Key, Value>) {
if node === head {
head = node.next
}
if node === tail {
tail = node.prev
}
node.prev?.next = node.next
node.next?.prev = node.prev
}
}
在上述示例中,定义了链表节点 LRUCacheNode,其中包含键(Key)和值(Value)以及指向前一个节点和后一个节点的指针。
然后,定义了 LRUCache类,用于实现LRU缓存。LRUCache 类包含一个哈希表 cache,用于快速查找缓存中的节点。它还包含链表的头节点 head 和尾节点 tail,用于维护节点的访问顺序。capacity 表示缓存的最大容量。
LRUCache 类中的 get 方法用于获取缓存中的值。如果缓存中存在该键,则将对应的节点移动到链表头部,并返回节点的值。
LRUCache 类中的 put 方法用于插入或更新缓存中的值。如果缓存中已经存在该键,则更新对应节点的值,并将节点移动到链表头部。如果缓存中不存在该键,则创建一个新节点,并将其添加到链表头部。如果缓存超过最大容量,则删除链表尾部的节点。
LRUCache 类中的私有方法 moveToHead 用于将节点移动到链表头部,addToHead 用于将节点添加到链表头部,removeNode 用于从链表中删除节点。这些方法用于维护链表的结构和节点的顺序。
使用示例:
// 创建容量为 3 的 LRUCache
let cache = LRUCache<Int, String>(capacity: 3)
// 插入缓存项
cache.put(1, "Apple")
cache.put(2, "Banana")
cache.put(3, "Cherry")
// 获取缓存项
print(cache.get(2)) // 输出: Optional("Banana")
// 缓存项顺序: 2 -> 3 -> 1
cache.put(4, "Durian")
// 缓存项顺序: 4 -> 2 -> 3
print(cache.get(1)) // 输出: nil
// 缓存项顺序: 4 -> 2 -> 3
cache.put(5, "Elderberry")
// 缓存项顺序: 5 -> 4 -> 2
print(cache.get(3)) // 输出: nil
// 缓存项顺序: 5 -> 4 -> 2
在上述示例中,我们创建了一个容量为 3 的 LRUCache 实例,并使用 put 方法插入了三个缓存项(键值对)。然后,使用 get 方法获取缓存项的值。在插入第四个缓存项时,由于容量已满,最久未访问的缓存项 1 被淘汰。最后,我们再次使用 get 方法获取缓存项的值,并观察缓存项的顺序。
小结
解决链表相关的算法问题时,需要注意以下几点:
- 链表的操作往往需要用到指针或者引用,因此要注意指针的移动和更新,以及避免指针的丢失和循环引用。指针和引用的意思都是一样的,都是存储所指对象的内存地址。
- 链表的操作往往需要用到一些辅助节点,比如哨兵节点,来简化边界情况的处理。针对链表的插入、删除操作,需要对插入第一个结点和删除最后一个结点的情况进行特殊处理。
- 链表的操作往往可以用递归或者迭代两种方式来实现,递归的优点是代码简洁,缺点是空间复杂度高,迭代的优点是空间复杂度低,缺点是代码复杂。
- 链表的操作往往可以用一些巧妙的方法来优化时间复杂度,比如快慢指针法,哈希表法等。
网友评论