iOS中的链表 - 双向链表

news2024/12/23 17:28:17

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缓存和自动释放池)表现出色。理解并熟练掌握双向链表的原理和实现,将为开发者在处理复杂数据结构是提供更的工具和选择。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2184843.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Pikachu-Cross-Site Scripting-DOM型xss_x

查看代码&#xff0c;输入的内容&#xff0c;通过get请求方式&#xff0c;用text 参数带过去&#xff1b; 获取text内容&#xff0c;赋值给xss 然后拼接到 dom 里&#xff1b;构造payload的关键语句&#xff1a; <a href"xss">就让往事都随风,都随风吧</a&…

【SQL】DDL语句

文章目录 1.SQL通用语法2.SQL的分类3.DDL3.1数据库操作3.2 表操作3.2.1 表操作--数据类型3.2.2 表操作--修改3.2.3 表操作--删除 SQL 全称 Structured Query Language&#xff0c;结构化查询语言。操作关系型数据库的编程语言&#xff0c;定义了一套操作关系型数据库统一标准 。…

【Python语言初识(六)】

一、网络编程入门 1.1、TCP/IP模型 实现网络通信的基础是网络通信协议&#xff0c;这些协议通常是由互联网工程任务组 &#xff08;IETF&#xff09;制定的。所谓“协议”就是通信计算机双方必须共同遵从的一组约定&#xff0c;例如怎样建立连接、怎样互相识别等&#xff0c;…

解决MySQL报Incorrect datetime value错误

目录 一、前言二、问题分析三、解决方法 一、前言 欢迎大家来到权权的博客~欢迎大家对我的博客进行指导&#xff0c;有什么不对的地方&#xff0c;我会及时改进哦~ 博客主页链接点这里–>&#xff1a;权权的博客主页链接 二、问题分析 这个错误通常出现在尝试将一个不…

基于C++和Python的进程线程CPU使用率监控工具

文章目录 0. 概述1. 数据可视化示例2. 设计思路2.1 系统架构2.2 设计优势 3. 流程图3.1 C录制程序3.2 Python解析脚本 4. 数据结构说明4.1 CpuUsageData 结构体 5. C录制代码解析5.1 主要模块5.2 关键函数5.2.1 CpuUsageMonitor::Run()5.2.2 CpuUsageMonitor::ComputeCpuUsage(…

Python库matplotlib之五

Python库matplotlib之五 小部件(widget)RangeSlider构造器APIs应用实列 TextBox构造器APIs应用实列 小部件(widget) 小部件(widget)可与任何GUI后端一起工作。所有这些小部件都要求预定义一个Axes实例&#xff0c;并将其作为第一个参数传递。 Matplotlib不会试图布局这些小部件…

【数学分析笔记】第4章第1节 微分和导数(1)

4. 微分 4.1 微分和导数 考虑一个函数 y f ( x ) yf(x) yf(x)&#xff0c;当 x x x做一些微小的变动&#xff0c;函数值也会有微小的变动&#xff0c;比如&#xff1a; x → x △ x x\to x\bigtriangleup x x→x△x&#xff0c;则 f ( x ) → f ( x △ x ) f(x)\to f(x\bi…

足球青训俱乐部管理:Spring Boot技术驱动

摘 要 随着社会经济的快速发展&#xff0c;人们对足球俱乐部的需求日益增加&#xff0c;加快了足球健身俱乐部的发展&#xff0c;足球俱乐部管理工作日益繁忙&#xff0c;传统的管理方式已经无法满足足球俱乐部管理需求&#xff0c;因此&#xff0c;为了提高足球俱乐部管理效率…

数据结构——“AVL树”的四种数据旋转的方法

因为上次普通的二叉搜索树在极端情况下极容易造成我们的链式结构&#xff08;这会导致我们查询的时间复杂度变为O(n)&#xff09;&#xff0c;然而AVL树就很好的解决了这一问题&#xff08;归功于四种旋转的方法&#xff09;&#xff0c;它让我们的树的查询的时间复杂度变得接近…

TIM(Timer)定时器的原理

一、介绍 硬件定时器的工作原理基于时钟信号源提供稳定的时钟信号作为计时器的基准。计数器从预设值开始计数&#xff0c;每当时钟信号到达时计数器递增。当计数器达到预设值时&#xff0c;定时器会触发一个中断信号通知中断控制器处理相应的中断服务程序。在中断服务程序中&a…

无人化焦炉四大车系统 武汉正向科技 工业机车无人远程控制系统

焦炉四大车无人化系统介绍 采用格雷母线光编码尺双冗余定位技术&#xff0c;炉门视觉定位自学习技术&#xff0c;wifi5G无线通讯技术&#xff0c;激光雷达安全识别技术&#xff0c;焦化智慧调度&#xff0c;手机APP监控功能。 焦炉四大车无人化系统功能 该系统能自动生成生产…

遥感图像垃圾处理场分割,北京地区高分2图像,3500张图像,共2GB,分割为背景,空地,垃圾,垃圾处理设施四类

遥感图像垃圾处理场分割&#xff0c;北京地区高分2图像&#xff0c;3500张图像&#xff0c;共2GB&#xff0c;分割为背景&#xff0c;空地&#xff0c;垃圾&#xff0c;垃圾处理设施四类 遥感图像垃圾处理场分割数据集 规模 图像数量&#xff1a;3500张数据量&#xff1a;2G…

黑科技!Llama 3.2多模态AI震撼发布

黑科技&#xff01;Llama 3.2多模态AI震撼发布 Meta发布Llama 3.2模型&#x1f680;&#xff0c;引领AI新潮流&#xff01;它能处理文字、图片、视频&#x1f4f8;&#xff0c;满足不同需求&#xff0c;性能媲美大牌选手✨。一键启动包已准备好&#xff0c;让你轻松体验AI的魔…

模版and初识vector

一、引言 在C语言中&#xff0c;不论是数组&#xff0c;还是结构体定义的数组&#xff0c;功能都比较欠缺&#xff0c;不是单纯的添加几个变量就能够解决的。缺少增删查改的功能&#xff0c;为了解决这个问题&#xff0c;C决定填上C语言这个坑&#xff0c;但是填过坑的人都知道…

秋招突击——算法练习——复习{双指针:移动零、盛最多的水、三数之和}——新作{接雨水}

文章目录 引言复习移动零盛最多的水三数之和 新作接雨水个人实现参考实现 总结 引言 这段时间还是很迷茫的&#xff0c;秋招到了一个阶段&#xff0c;但是收获并不是很多&#xff0c;基本上都在泡池子&#xff0c;没有意向。也就没有在坚持刷题&#xff0c;只是整理一些专门的…

Arduino UNO R3自学笔记15 之 Arduino如何驱动数码管?

注意&#xff1a;学习和写作过程中&#xff0c;部分资料搜集于互联网&#xff0c;如有侵权请联系删除。 前言&#xff1a;学习使用数码管。 1.数码管介绍 数码管的一种是半导体发光器件&#xff0c;数码管可分为七段数码管和八段数码管&#xff0c;区别在于八段数码管比七段数…

【数据结构】图论基础

文章目录 图的概念图的基本概念图的类型图的表示方法 图的相关基本概念1. 路径&#xff08;Path&#xff09;2. 连通性&#xff08;Connectivity&#xff09;3. 图的度&#xff08;Degree&#xff09;4. 子图&#xff08;Subgraph&#xff09;5. 生成树&#xff08;Spanning Tr…

LabVIEW提高开发效率技巧----快速实现原型和测试

在LabVIEW开发中&#xff0c;DAQ助手&#xff08;DAQ Assistant&#xff09;和Express VI为快速构建原型和测试功能提供了极大的便利&#xff0c;特别适合于简单系统的开发和早期验证阶段。 DAQ助手&#xff1a;是一种可视化配置工具&#xff0c;通过图形界面轻松设置和管理数据…

CSS3渐变

一、线性渐变 通过background-image: linear-gradient(...)设置线性渐变 语法&#xff1a; linear-gradient(direction,color1,color2, . . ) direction&#xff1a;渐变方向&#xff0c;默认从上到下&#xff0c;可选值&#xff1a; 简单选取&#xff1a; ① to right&…

Python和C++及MATLAB和R时间序列中数学物理金融气象运动和电子材料

&#x1f3af;要点 小波分析&#xff0c;量化噪声概率分布和统计推理物理量和化学量数值计算确定性非线性系统金融资本市场和市场流动性波形传播气象建模评估 Python时间序列数学 时间序列分析是一种强大的统计技术&#xff0c;广泛应用于经济学、金融学、环境科学和工程学…