iOS中的链表 - 双向链表

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
    }
}
  1. data:存储节点的数据。
  2. prev:是一个指向前一个节点的可选引用。
  3. 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
        }
    }
}

在这个示例中:

  1. LRUCache类使用双向链表来保持缓存项的顺序,最常访问的项在链表头,最少访问的项在尾部。
  2. get方法会更新缓存项的顺序,将被访问的项移到链表的头部。
  3. put方法会插入新项,并在缓存满时删除尾部的旧项。

这种缓存机制在很多场景下都非常有用,比如操作系统的内存管理、浏览器的页面缓存等。

当我们深入研究自动释放池的时候,会发现它的数据除了分页存储之外,页与页之间也是个双向链表的数据结构。

双向链表vs单向链表

1.结构

  • 单向链表:每个节点值包含数据和一个指向下一个节点的指针(next)。因此,节点只能沿一个方向遍历。
  • 双向链表:每个节点包含数据、一个指向下一个节点的指针(next)和一个指向前一个节点的指针(prev)。这允许节点能够双向遍历。

2.遍历操作

  • 单向链表:只能从头部节点开始向后遍历,无法从中间或尾部节点进行方向遍历。如果需要访问前一个节点,必须从头开始重新遍历。
  • 双向链表:可以从任意节点开始,向前或向后遍历,操作更灵活。如果从尾部开始遍历链表也是非常方便的。

3.插入和删除操作

  • 单向链表:在单向链表中,插入和删除节点时,需要获取前一个节点的引用才能进行操作。特别是删除节点时,必须先找到其前驱节点来修改指针。
  • 双向链表:由于每个节点都有前驱和后继指针,插入或删除操作更为简单。可以直接通过节点本身找到前驱和后继节点,无需从头遍历链表来找到前驱节点,这在任意位置的插入和删除时效率更高。

4.内存使用

  • 单向链表:因为每个节点只需要存储一个指针,所以内存占用相对较少。
  • 双向链表:每个节点需要存储两个指针(前驱和后继),因此在内存使用上笔单向链表多出一倍的指针空间。

5.时间复杂度

  • 单向链表:插入和删除操作的时间复杂度是O(1),前提是已经有对前驱节点的引用;查找某个节点的时间复杂度为O(n)。
  • 双向链表:插入和删除的时间复杂度也是O(1),因为直接可以访问前驱和后继节点;查找节点的时间复杂度也是O(n),但双向链表可以更灵活地从两端开始查找。

6.适用场景

单向链表:适合简单的场景,例如只需要单向遍历,内存开销要求比较低是,单向链表是更合适的选择。

双向链表:当需要双向遍历或频繁地在中间进行插入、删除操作时,双向链表提供了更大的灵活性,适合如LRU缓存、自动释放池等场景。

结语

双向链表是一种强大且灵活的数据结构,能够在许多场景中提升操作效率,尤其是在需要频繁插入、删除和双向遍历的应用中。与单向链表相比,双向链表虽然增加了一定的内存开销,但在很多实际系统中(如LRU缓存和自动释放池)表现出色。理解并熟练掌握双向链表的原理和实现,将为开发者在处理复杂数据结构是提供更的工具和选择。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值