美文网首页
Swift 数据结构 - 链表

Swift 数据结构 - 链表

作者: 6ffd6634d577 | 来源:发表于2020-04-19 17:45 被阅读0次

基础概念

链表

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

节点

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

链表的分类

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

+--------+    +--------+    +--------+    +--------+
|        |    |        |    |        |    |        |
| 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
   }

更多链表算法题

相关文章

  • 无标题文章

    Swift算法俱乐部:Swift 链表数据结构@(Swift)在本教程中,我们将会在Swift 3中实现链表。##...

  • Swift 算法实战之路:排序

    以前的文章中,我们主要是在讲数据结构:比如数组、链表、队列、树。这些数据结构都是了解Swift和算法的基础。从今以...

  • 算法与数据结构知识汇总(二、链表)

    1、概念 2、链表的数据结构 单向链表的数据结构如下图: 上图数据结构为单向链表,简称单链表,该数据结构由若干个节...

  • Swift 数据结构 - 链表

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

  • iOS 数据结构之链表

    iOS 数据结构之链表 iOS 数据结构之链表

  • 数据结构 & 算法 in Swift (一):Swift

    数据结构 & 算法 in Swift (一):Swift基础和数据结构 数据结构 & 算法 in Swift (一...

  • 数据结构 | 其二 链表

    冰河winner - 数据结构之链表 2.1 单向链表 数据结构(一) 单链表的实现-JAVA 2.2 双端链表 ...

  • Go语言数据结构和算法-LinkedList(链表)

    Go语言数据结构和算法-LinkedList(链表) 节点和链表数据结构 Prepend(item) 在链表头新增...

  • 集合-LinkedList解析

    一、概要 Java中底层数据结构是链表、双端链表,Android中数据结构是双向循环链表 非线程安全数据结构,允许...

  • 常见数据结构和算法

    常见数据结构 线性数据结构(按顺序具有数据元素的数据结构):数组,堆栈,链表(单链表 双链表),队列非线性数据结...

网友评论

      本文标题:Swift 数据结构 - 链表

      本文链接:https://www.haomeiwen.com/subject/dahpvhtx.html