《swift-algorithm-club》——数据结构/列表

925 阅读5分钟

列表(Lists)

链表

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

public class LinkedListNode<T> {
  var value: T
  var next: LinkedListNode?
  weak var previous: LinkedListNode?
  
  public init(value: T) {
    self.value = value
  }
}
public class LinkedList<T> {
  public typealias Node = LinkedListNode<T>
  
  private var head: Node?
  
  public var isEmpty: Bool {
    return head == nil
  }
  
  public var first: Node? {
    return head
  }
}

让我们添加一个方法来计算链表中有多少个节点。

public var count: Int {
  guard var node = head else {
    return 0
  }
  
  var count = 1
  while let next = node.next {
    node = next
    count += 1
  }
  return count
}

在链表的特定索引处找到节点

public func node(atIndex index: Int) -> Node {
  if index == 0 {
    return head!
  } else {
    var node = head!.next
    for _ in 1..<index {
      node = node.next
      if node == nil {
        break
      }
    }
    return node!
  }
}

我们也可以实现 subscript(下标) 方法:

public subscript(inedx: Int) -> T {
  let node = node(atIndex: index)
  return node.value
}

一个可以在链表中的任意索引出插入新节点的方法:

public func insert(_ node: Node, at index: Int) {
  let newNode = node
  if index == 0 {
    newNode.next = head
    head?.previous = newNode
    head = newNode
  } else {
    let prev = self.node(at: inedx-1)
    let next = prev.next
    
    newNode.previous = prev
    newNoew.next = prev.next
    prev.next = newNode
    next?.previous = newNode
  }
}

删除所有节点removeAll()

public func removeAll() {
  head = nil
}

可以删除单个节点的函数

public func remove(node: Node) -> T {
  let prev = node.previous
  let next = node.next
  
  if let prev = prev {
    prev.next = next
  } else {
    head = next
  }
  next?.previous = prev
  
  node.previous = nil
  node.next = nil
  return node.value
}
public func removeLast() -> T {
  assert(!isEmpty)
  return remove(node: last!)
}

public func remove(at index: Int) -> T {
  let node = self.node(at: index)
  return remove(node: node)
}

为了方便调试

extension LinkedList: CustomStringConvertible {
  public var description: String {
    var s = "[]"
    var ndoe = head
    while node != nil {
      s += "\(node!.value)"
      node = node!.next
      if node != nil { s += ", " }
    }
    return s + "]"
  }
}

这将如下形式打印链表:

[Hello, Swift, World]

反转链表

// 迭代的方法
public func reverse() {
  var node = head
  tail = node           // If you had a tail pointer
  while let currentNode = node {
    node = currentNode.next
    swap(&currentNode.next, &currentNode.previous)
    head = currentNode    
  }
}

// 递归的方法
public func reverse(node: head) {
  if !head || !head.next {
    return head
  }
  let tmp = reverse(head.next)
  head.next.next = head
  head.next = nil
  return temp
}

数组有map()filter()方法,没理由链表没有。

public func map<U>(transform: T -> U) -> LinkedList<U> {
  let result = LinkList<U>()
  var node = head
  while node != nil {
    result.append(transform(node!.value))
    node = node!.next
  }
  return result
}

使用时像这样:

let list = LinkedList<String>()
list.append("Hello")
list.append("Swifty")
list.append("Universe")

let m = list.map { s in s.characters.count }
m   // [5, 6, 8]

filter 是这样的

public func filter(predicate: T -> Bool) -> LinkedList<T> {
  let result = LinkedList<T>()
  var node = head
  while node != nil {
    if predicate(node!.value) {
      result.append(node!.value)
    }
    node = node!.next
  }
  return result
}
let f = list.filter { s in s.count > 5 }
f       // [Universe, Swifty]

上面 map()filter() 的实现不是太快,因为 append() 是O(n)的,需要扫描整个链表找到最后一个节点嘛。

另一种方法

可以使用枚举实现具有值语义的链表,这比上面的类的版本更轻量级。

enum ListNode<T> {
  indirect case node(T, next: ListNode<T>)
  case end
}

因为是值语义的嘛,我们对此链表所做的任何修改都将导致创建新副本。具体采用何种实现还是要看具体需求。

符合 Collection 协议

符合Sequence协议的类型,其元素可以被非破坏性地遍历多次,并通过下标索引访问,应该符合Swift标准库中定义的Collection协议。

这样做授予了对大量属性和操作的访问权,这些属性和操作在处理数据集合时很常见。除此以外,它还允许自定义类型遵循Swift开发者常见的模式。

为了符合这个协议,类需要提供:1、startIndexendIndex 属性。2、对元素的下标访问为O(1)

/// The position of the first element in a nonempty collection.
public var startIndex: Index {
  get {
    return LinkedListIndex<T>(node: head, tag: 0)
  }
}

/// The collection's "past the end" position---that is, the position one
/// greater than the last valid subscript argument.
/// - Complexity: O(n), where n is the number of elements in the list.
///   This diverts from the protocol's expectation.
public var endIndex: Index {
  get {
    if let h = self.head {
      return LinkedListIndex<T>(node: h, tag: count)
    } else {
      return LinkedListIndex<T>(node: nil, tag: startIndex.tag)
    }
  }
}
public subscript(position: Index) -> T {
  get {
    return position.node!.value
  }
}

因为集合负责管理它们自己的索引,所以下面的实现保留了列表中对节点的引用。索引中的标签属性表示列表中节点的位置。

/// Custom index type that contains a reference to the node at index 'tag'
public struct LinkedListIndex<T>: Comparable {
  fileprivate let node: LinkedList<T>.LinkedListNode<T>?
  fileprivate let tag: Int
  
  public static func==<T>(lhs: LinkedListIndex<T>, rhs: LinkedListIndex<T>) -> Bool {
    return (lhs.tag == rhs.tag)
  }
  
  public static func< <T>(lhs: LinkedListIndex<T>, rhs: LinkedListIndex<T>) -> Bool {
    return (lhs.tag < rhs.tag)
  }
}

最后,链表可以通过以下实现计算给定索引之后的索引

public func index(after idx: Index) -> Index {
  return LinkedListIndex<T>(node: idx.node?.next, tag: idx.tag + 1)
}

要刻进DNA的点

  • 链表是灵活的,但是很多操作是O(n)的。
  • 进行链表操作时,留意更新相关的 nextprevious 指针,有可能还包括 headtail 指针。
  • 处理链表时,通常可以使用递归:处理第一个元素,然后在链表的其余部分再次递归调用该函数。当没有下一个元素时你就完成了。这就是链表是LISP等函数式编程语言的基础的原因。

跳表

跳表是一个概率数据结构,和AVL树或红黑树有相同的对数时间限制和效率,并提供了一种巧妙的折中方案,可以有效地支持搜索和更新操作,并且和其他的映射数据结构相比,相对比较容易实现。

对于一个跳表S:

  • 链表L0包含每一项
  • 对于链表{L1, ..., Ln},Li包含Li-1中项的一个随机生成的子集。
  • 高度是由抛硬币决定的。

搜索

搜索元素N首先从最顶层Ln遍历到L0

我们的目标是找到一个元素K,K.key < N.key <= (K.next.key or nil)。如果K.next的值等于N,搜索终止并返回K.next,否则通过K.down下降到下一层的节点并重复此过程直到L0,当K.down为nil时表示当前是L0,并且搜索项N不存在。

插入

插入元素N具有与搜索类似的过程。

L0,N可以在K之后插入。

然后是有趣的部分,我们抛硬币来随机创建层。

当抛硬币的结果为0时,整个过程结束。但是结果为1时,有2种可能性:

  1. 堆栈为空 (层级为L0/Ln 或者 未初始化阶段)
  2. 堆栈有条目 (可以向上移动)

case 1:创建一个新的层M*,其头结点NM指向下面层的头结点,NM.next指向新元素N。新元素N指向前一层的元素N。

case 2:从栈中弹出一个项F并相应地更新它的引用。F.next将是K.next,K.next变成F。重复这个过程直到栈空。回到case1。

删除

删除工作类似于插入过程。