iOS中的链表 - 单向链表_ios 链表怎么实现-CSDN博客
引言
在数据结构中,链表是一种常见的且灵活的线性存储方式。与数组不同,链表的元素在内存中不必连续存储,这使得它们在动态内存分配时更加高效。其中,双向链表作为链表的一个变体,提供了比单向链表更强大的操作能力。每个节点不仅指向下一个节点,还能指向前一个节点,这种双向链接使得从任意方向遍历变得更加容易。
在本文中,我们将深入探讨双向链表的定义、节点实现以及插入和删除操作。通过与单向链表的对对比,我们将更好地理解双向链表在实际应用中的优势与劣势。
定义
双向链表是一种链式存储结构,由一系列节点构成。每个节点包含三部分:数据部分、指向下一个节点的指针(后继指针)以及指向前一个节点的指针(前驱指针)。这种结构允许在链表中向前和向后遍历,从而提供了更大的灵活性。
在双向链表中,头节点的前驱指针通常指向空,尾结点的后继指针也指向空。通过双向链接,插入和删除节点操作变得更加高效,因为可以直接访问前驱和后继节点,而不需要从链表的一端遍历到目标节点。
节点实现
在双向链表中,节点是构成链表的基本单位。每个节点通常包含三个成员变量:数据部分、前驱指针和后继指针。以下是一个用Swift语言实现的节点类的示例:
class Node<T> {
var data: T
var prev: Node?
var next: Node?
init(data: T) {
self.data = data
self.prev = nil
self.next = nil
}
}
- data:存储节点的数据。
- prev:是一个指向前一个节点的可选引用。
- next:是一个指向下一个节点的可选引用。
通过这种结构,双向链表的节点可以方便地实现前后遍历和节点的插入与删除操作。
链表实现
双向链表的实现和单向链表区别并不大,通常包含一个头节点和一个尾结点,以及对链表的基本操作,例如插入、删除和遍历。以下是用Swift实现的双向链表的示例:
class DoublyLinkedList<T> {
var head: Node<T>?
var tail: Node<T>?
init() {
self.head = nil
self.tail = nil
}
....
}
插入操作
在双向链表中,插入操作可以在头部、尾部或任意位置进行。以下是不同情况下插入方法:
1.在头部插入
- 创建一个新节点并将其前驱指针指向nil,后继指针指向当前头节点。
- 更新当前头节点的前驱指针指向新节点,并将头指针更新为新节点。
func insertAtHead(data: T) {
let newNode = Node(data: data)
newNode.next = head
head?.prev = newNode
head = newNode
if tail == nil {
tail = newNode
}
}
2.在尾部插入
- 创建新的节点,更新指针
// 插入操作示例(在尾部插入)
func append(data: T) {
let newNode = Node(data: data)
if tail == nil {
head = newNode
tail = newNode
} else {
tail?.next = newNode
newNode.prev = tail
tail = newNode
}
}
3.在任意位置插入
- 首先找到要插入的位置,调整相邻节点的指针以插入新节点。
func insertAfter(node: Node<T>, data: T) {
let newNode = Node(data: data)
newNode.prev = node
newNode.next = node.next
node.next?.prev = newNode
node.next = newNode
if newNode.next == nil {
tail = newNode
}
}
通过这些操作,双向链表能够灵活地插入节点,确保前后指针的正确连接,从而保持链表的完整性。
删除操作
双向链表的删除操作通常涉及三个主要情况:删除头节点、删除尾节点以及删除任意节点。在删除过程中,关键是正确调整相邻节点的前驱和后继指针。
1.删除头节点
- 直接更新head指针指向下一个节点,并将新头节点的前驱指针设为nil。
- 如果链表中只有一个节点,则需要同时更新head和tail为nil。
func removeHead() {
guard let headNode = head else { return }
head = headNode.next
head?.prev = nil
if head == nil {
tail = nil
}
}
2.删除尾节点
- 更新tail指针指向前一个节点,并将新尾结点的后继指针设为nil。
- 如果链表中只有一个节点,则需要同时更新head和tail为nil。
func removeTail() {
guard let tailNode = tail else { return }
tail = tailNode.prev
tail?.next = nil
if tail == nil {
head = nil
}
}
3.删除任意节点
- 首先找到要删除的节点,然后调整该节点前驱和后继节点的指针,使踏马跳过被删除的节点。最后将该节点的前驱和后继指针设为nil,以便释放内存。
func remove(node: Node<T>) {
let prevNode = node.prev
let nextNode = node.next
prevNode?.next = nextNode
nextNode?.prev = prevNode
if node === head {
head = nextNode
}
if node === tail {
tail = prevNode
}
node.prev = nil
node.next = nil
}
链表使用
LRU(Lest Recently Used,最近最少使用)缓存时一种缓存淘汰策略,用于管理有限大小的缓存。双向链表与哈希表结合常用于实现LRU缓存,其基本思想是:
- 使用双向链表来维护缓存中数据的访问顺序,最常访问的放在链表头部,最少访问的放在尾部。
- 使用哈希表存储数据的引用,以便在O(1)时间内快速查找。
当缓存已满是,我们将删除尾部节点(即最近最少使用的节点),并将心的数据插入到头部。通过双向链表的高效插入和删除操作,这种策略的实现非常灵活和高效。
以下是一个简化的示例:
class LRUCache<T: Hashable> {
private class CacheNode {
var key: T
var next: CacheNode?
var prev: CacheNode?
init(key: T) {
self.key = key
}
}
private var capacity: Int
private var cache: [T: CacheNode] = [:]
private var head: CacheNode?
private var tail: CacheNode?
init(capacity: Int) {
self.capacity = capacity
}
// 获取缓存中的值
func get(key: T) -> T? {
guard let node = cache[key] else {
return nil
}
moveToHead(node: node)
return node.key
}
// 插入新的值到缓存
func put(key: T) {
if let node = cache[key] {
moveToHead(node: node)
} else {
let newNode = CacheNode(key: key)
if cache.count == capacity {
removeTail()
}
addNodeToHead(node: newNode)
cache[key] = newNode
}
}
// 将节点移到链表头部
private func moveToHead(node: CacheNode) {
removeNode(node: node)
addNodeToHead(node: node)
}
// 添加节点到链表头部
private func addNodeToHead(node: CacheNode) {
node.next = head
node.prev = nil
if head != nil {
head?.prev = node
}
head = node
if tail == nil {
tail = head
}
}
// 删除尾部节点
private func removeTail() {
guard let tailNode = tail else { return }
cache[tailNode.key] = nil
removeNode(node: tailNode)
}
// 删除某个节点
private func removeNode(node: CacheNode) {
let prevNode = node.prev
let nextNode = node.next
prevNode?.next = nextNode
nextNode?.prev = prevNode
if node === head {
head = nextNode
}
if node === tail {
tail = prevNode
}
}
}
在这个示例中:
- LRUCache类使用双向链表来保持缓存项的顺序,最常访问的项在链表头,最少访问的项在尾部。
- get方法会更新缓存项的顺序,将被访问的项移到链表的头部。
- put方法会插入新项,并在缓存满时删除尾部的旧项。
这种缓存机制在很多场景下都非常有用,比如操作系统的内存管理、浏览器的页面缓存等。
当我们深入研究自动释放池的时候,会发现它的数据除了分页存储之外,页与页之间也是个双向链表的数据结构。
双向链表vs单向链表
1.结构
- 单向链表:每个节点值包含数据和一个指向下一个节点的指针(next)。因此,节点只能沿一个方向遍历。
- 双向链表:每个节点包含数据、一个指向下一个节点的指针(next)和一个指向前一个节点的指针(prev)。这允许节点能够双向遍历。
2.遍历操作
- 单向链表:只能从头部节点开始向后遍历,无法从中间或尾部节点进行方向遍历。如果需要访问前一个节点,必须从头开始重新遍历。
- 双向链表:可以从任意节点开始,向前或向后遍历,操作更灵活。如果从尾部开始遍历链表也是非常方便的。
3.插入和删除操作
- 单向链表:在单向链表中,插入和删除节点时,需要获取前一个节点的引用才能进行操作。特别是删除节点时,必须先找到其前驱节点来修改指针。
- 双向链表:由于每个节点都有前驱和后继指针,插入或删除操作更为简单。可以直接通过节点本身找到前驱和后继节点,无需从头遍历链表来找到前驱节点,这在任意位置的插入和删除时效率更高。
4.内存使用
- 单向链表:因为每个节点只需要存储一个指针,所以内存占用相对较少。
- 双向链表:每个节点需要存储两个指针(前驱和后继),因此在内存使用上笔单向链表多出一倍的指针空间。
5.时间复杂度
- 单向链表:插入和删除操作的时间复杂度是O(1),前提是已经有对前驱节点的引用;查找某个节点的时间复杂度为O(n)。
- 双向链表:插入和删除的时间复杂度也是O(1),因为直接可以访问前驱和后继节点;查找节点的时间复杂度也是O(n),但双向链表可以更灵活地从两端开始查找。
6.适用场景
单向链表:适合简单的场景,例如只需要单向遍历,内存开销要求比较低是,单向链表是更合适的选择。
双向链表:当需要双向遍历或频繁地在中间进行插入、删除操作时,双向链表提供了更大的灵活性,适合如LRU缓存、自动释放池等场景。
结语
双向链表是一种强大且灵活的数据结构,能够在许多场景中提升操作效率,尤其是在需要频繁插入、删除和双向遍历的应用中。与单向链表相比,双向链表虽然增加了一定的内存开销,但在很多实际系统中(如LRU缓存和自动释放池)表现出色。理解并熟练掌握双向链表的原理和实现,将为开发者在处理复杂数据结构是提供更的工具和选择。