Swift 数据结构 - 链表

基础概念

链表

链表是一种物理存储单元上非连续、非顺序的存储结构数据元素的逻辑顺序是通过链表中的指针链接次序实现的。

节点

链表中每一个元素称为结点,结点可以在运行时动态生成。每个结点包括两个部分:一个是存储数据元素的数据域,另一个是存储下一个结点地址的指针域。(单向链表一个;双向链表两个)

链表的分类

单链表 每一个节点只包含一个指向链表中下一个节点的指针(引用)。

+--------+    +--------+    +--------+    +--------+
|        |    |        |    |        |    |        |
| node 0 |--->| node 1 |--->| node 2 |--->| node 3 |
|        |    |        |    |        |    |        |
+--------+    +--------+    +--------+    +--------+

双链表 每个节点包含两个指针(引用),一个指向前一个节点,一个指向后一个节点。

+--------+    +--------+    +--------+    +--------+
|        |--->|        |--->|        |--->|        |
| node 0 |    | node 1 |    | node 2 |    | node 3 |
|        |<---|        |<---|        |<---|        |
+--------+    +--------+    +--------+    +--------+

通常我们用 headtail 指针来记录链表的头和尾。

         +--------+    +--------+    +--------+    +--------+
head --->|        |--->|        |--->|        |--->|        |---> nil
         | node 0 |    | node 1 |    | node 2 |    | node 3 |
 nil <---|        |<---|        |<---|        |<---|        |<--- tail
         +--------+    +--------+    +--------+    +--------+

注意,最后一个节点的“下一个”指针是nil,第一个节点的“前一个”指针也是nil。

循环链表:是另一种形式的链式存贮结构。它的特点是表中最后一个结点的指针域指向头结点,整个链表形成一个环;

静态链表:用数组描述的链表,即称为静态链表。

链表和数组的比较

  1. 数组内存上是连续的存储空间,链表内存地址可以是不连续的,内存利用率高,链表由于增加了结点的指针域,空间开销比较大。
  2. 数据查询时,数组可以直接通过下标迅速访问数组中的元素。而链表则需要从第一个元素开始一直找到需要的元素位置,显然,数组的查询效率会比链表的高
  3. 当进行增加或删除元素时,在数组中增加或删除一个元素,需要移动大量元素,在内存中空出一个元素的空间。而链表只需改动元素中的指针即可实现增加或删除元素,链表的增删效率比数组高。
代码

首先定义一个描述节点的类型:

public class ListNode <T> {
  var value: T
  var next: LinkedListNode?
  weak var previous: LinkedListNode?

  public init(value: T) {
    self.value = value
  }
}

注意: 为避免循环强引用,我们声明previous指针为弱引用。 如果链表中有一个节点A后面跟着节点B,那么A指向B,而B指向A。 在某些情况下,即使在删除节点后,此循环强引用也可能导致节点保持活动状态。 所以我们使其中一个指针weak来打破循环。

构建 LinkedList

public class LinkedList<T> {
  public typealias Node = ListNode<T>

  //头指针
  private var head: Node?
  //尾指针
  private var tail: Node?
  //是否空链表
  public var isEmpty: Bool {
    return head == nil
  }

  //链表中的第一个节点
  public var first: Node? {
    return head
  }
    
  //链表中的最后一个节点
  public var last: Node? {
    return tail
  }
  
  //链表的结点数量
  public var count: Int = 0
    
  //新节点添加到链表的末尾
  public func append(value: T) {
    let newNode = Node(value: value)
    if let tailNode = tail {
        tailNode.next = node
        node.previous = tailNode
    }else {
        head = newNode
    }
    tail = newNode
    count += 1
  }
    
    /// 在链表中的特定索引处找到节点
    /// - Parameter index: 索引
    /// - Returns: 返回结果
  public func nodeAt(index: Int) -> Node? {
      guard index >= 0 else {
          return nil
      }
      var i = index
      var node = head
      while node != nil {
          if i == 0 {
              return node
          }
          node = node?.next
          i -= 1
      }
      return nil
  }
    
    /// node节点后插入值为val的节点
    /// - Parameter node: 目标节点啊
    /// - Parameter val: 插入的节点的值
    func insert(node: Node, val: T) {
        let newNode = Node(value: val)
        newNode.next = node.next
        node.next = newNode
    }
    
    /// 链表清空
    public func removeAll() {
        self.head = nil
        self.tail = nil
    }
    
    /// 删除一个节点(双向链表)
    /// - Parameter node: 要删除的节点
    public func dLLRemove(node: Node) {
        let prev = node.previous
        let next = node.next
        
        if let prev = prev {
            prev.next = next
        } else {
            head = next
        }
        next?.previous = prev
        
        if next == nil {
            tail = prev
        }
        
        node.previous = nil
        node.next = nil
    }
    /// 删除一个节点(单向链表)
    /// - Parameter node: 要删除的节点
    /// 当需要删除一个节点p时,只需要将p的前一个节点的next赋为p->next,但是由于是单向的,只知道p,无法知道p前一个节点,所以需要转换思路。将p下一个的节点覆盖到p节点即可,这样就等效于将p删除了。
    func sLLRemove(node: Node) {
        if let next = node.next {
            node.value = next.value
            node.next = next.next
        } else {
            self.pop()
        }
    }
    
    /// 去掉最后一个节点
    public func pop() -> Node? {
        /// 存在最后一个节点
        if let last = tail {
            /// 存在最后一个节点的前一个节点
            if let lp = last.previous {
                lp.next = nil
                tail = lp
            } else {/// 不存在
                head = nil
                tail = nil
            }
            count -= 1
        }
        tail?.previous = nil
        tail?.next = nil
        return tail
    }
}

练习:1. 判断是否是循环链表

快慢指针

解题思路:慢指针每次移动一步,快指针每次移动两步,如果存在环,那么两个指针一定会在环内相遇。

找到环的入口点

解题思路:两个指针相遇后,让其中一个指针回到链表的头部,另一个指针在原地,同时往前每次走一步,当它们再次相遇时,就是在环路的入口点。

找出环开始的节点证明

image.png

根据上图,我们可以得到下面的关系式:
w + n + y = 2 (w + y)
经过化简,我们可以得到:w = n - y;
这种情况下,我们就可以直接把 p2 放在 pHead 处,然后让两个指针以同样的速度走,那么,两者下一次就一定在入口节点相遇了。

extension LinkedList {
    /// 找到循环链表的一个相同节点
    /// - Returns: 返回结果
    func fineMeetNode() -> Node? {
        guard let h = head else {
            return nil
        }
        var fast: Node? = h
        var slow: Node? = h
        while (fast?.value != nil && fast?.next?.value != nil) {   //两个变量赛跑,找出相遇的节点
            fast = (fast?.next?.next)!
            slow = slow?.next!
            if (fast!.value == slow!.value) {
                return slow
            }
        }
        return nil
    }
    
    /// 找到循环链表的入口
    /// - Returns: 返回结果
    func findCycleEntrance() -> Node? {
        guard var meetNode = self.fineMeetNode() else {
            return nil
        }
        while meetNode.value != self.head?.value {
            meetNode = meetNode.next!
            self.head = self.head?.next
        }
        return meetNode
    }
}

练习:2. 删除链表中倒数第n个节点

题目描述:删除单链表倒数第 n 个节点,1 <= n <= length,尽量在一次遍历中完成。

解题思路:经典的双指针法。定义两个指针,第一个指针从链表的头指针开始遍历向前走n-1步,第二个指针保持不动,从第n步开始,第二个指针也开始从链表的头指针开始遍历,由于两个指针的距离保持在n-1,当第一个指针到达链表的尾节点时,第二个指针刚好指向倒数第n个节点。

/// 打印协议
extension LinkedList: CustomStringConvertible where T: CustomStringConvertible {
    public var description: String {
        /// 头节点
        var head = self.head
        
        var result: String = ""
        
        /// 当前节点不等于nil
        while head != nil {
            result += head?.value.description ?? ""
            head = head?.next
            if head != nil {
                result += "->"
            }
        }
        return result
    }
}

/// 移除倒数第n个节点
extension LinkedList where T == Int {
    
    func removeNthFromEnd(head: Node<T>?, _ n: Int) -> ListNode<T>?{
        guard let head = head else {
            return nil
        }
        
        let dummy = Node<Int>(value: 0)
        dummy.next = head
        
        var  first: Node? = dummy
        var  second: Node? = dummy
        /// 设置后一个节点的初始位置
        for _ in 0..<n {
            first = first?.next
        }
        
        /// 同时移动前后节点
        while first != nil, first?.next != nil {
            first = first?.next
            second = second?.next
        }
        
        // 删除节点
        second?.next = second?.next?.next
        return dummy.next
    }
}

let ll = LinkedList<Int>()
ll.append(value: 1)
ll.append(value: 2)
ll.append(value: 3)
ll.append(value: 4)
ll.append(value: 5)
ll.removeNthFromEnd(head: ll.head, 2)
let rrr = ll.description 

// 结果: "1->2->3->5"

练习:3. 翻转单链表

题目描述:输出一个单链表的逆序反转后的链表。

方案一:
迭代:在链表第一个和第二个元素断开链表,保存后半段,前半段拼在新head前方,然后赋值给新head:具体如下面示意


p: a -> b -> c -> d -> e -> nil
newHead = nil

temp = p.next        temp: b -> c -> d -> e -> nil
p.next = newHead        p: a -> nil
newHead = p       newHead: a -> nil
p = temp                p: b -> c -> d -> e -> nil

temp = p.next        temp: c -> d -> e -> nil
p.next = newHead        p: b -> a -> nil
newHead = p       newHead: b -> a -> nil
p = temp                p: c -> d -> e -> nil

...

newHead: d -> d -> c -> b -> a -> nil
-----------------------------------------------------------------------------------
    /// 返回反转后头结点
    func reverseList(_ head: ListNode?) -> ListNode? {
      if head == nil || head?.next == nil {
          return head
      }
      
       var newHead: ListNode? = nil
       var p = head
       while p != nil {
          let tmp = p?.next
          p?.next = newHead
          newHead = p
          p = tmp
       }
      return newHead
    }

方案二:
递归:递归找到最后一个节点作为新链表的头节点,然后再更新每一个node的next 值 ,实现链表的反转。而newhead 的值没有发生改变,为该链表的最后一个结点,所以,反转后,我们可以得到新链表的head。

  func reverseList(_ head: ListNode?) -> ListNode? {
    if head == nil || head?.next == nil {
        return head
    }

    let newHead = reverseList(head?.next)
    head?.next?.next = head
    head?.next = nil
    return newHead
   }

更多链表算法题

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 216,142评论 6 498
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,298评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 162,068评论 0 351
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,081评论 1 291
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,099评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,071评论 1 295
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,990评论 3 417
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,832评论 0 273
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,274评论 1 310
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,488评论 2 331
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,649评论 1 347
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,378评论 5 343
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,979评论 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,625评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,796评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,643评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,545评论 2 352

推荐阅读更多精彩内容

  • 目录 1、属性 2、链表和数组的区别 2.1、数组概述 2.2、数组和链表优缺点 2.3、链表和数组的比较 3、单...
    我哈啊哈啊哈阅读 2,803评论 1 41
  • 基本概念 链表的含义: 链表是一种用于存储数据集合的数据结构,具有以下属性 相邻元素之间通过指针相连 最后一个元素...
    古剑诛仙阅读 1,006评论 0 3
  • 本文来自本人著作《趣学数据结构》 链表是线性表的链式存储方式,逻辑上相邻的数据在计算机内的存储位置不一定相邻,那么...
    rainchxy阅读 3,698评论 6 20
  • 搞懂单链表常见面试题 Hello 继上次的 搞懂基本排序算法,这个一星期,我总结了,我所学习和思考的单链表基础知识...
    醒着的码者阅读 4,583评论 1 45
  • 线性表的定义 结构: 1. 线性结构: 结构中的数据元素之间均满足线性关系(一对一)。 由若干个数据项组成的数据元...
    cean_seven阅读 244评论 0 1