美文网首页
Swift | Data Structures & Algori

Swift | Data Structures & Algori

作者: 清無 | 来源:发表于2024-01-15 15:51 被阅读0次

    Referrence

    https://github.com/kodecocodes/swift-algorithm-club

    Array

    Array is also a RandomAccessCollection which makes guarantees about efficiency.

    This means that no matter how big your array gets, computing count will always take the same amount of time.

    Other data structures such as linked lists and trees do not have constant time access.

    The most efficient scenario for adding an element to an array is to append it at the end of the array

    Inserting new elements from anywhere aside from the end of the array will force elements to shuffle backwards to make room for the new element

    If inserting elements in front of a collection is a common operation for your program, you may want to consider a different data structure to hold your data.

    The second factor that determines the speed of insertion is the array’s capacity.

    However, the standard library employs a subtle trick to prevent appends from being O(n) time. Each time it runs out of storage and needs to copy, it doubles the capacity.

    Dictionary

    Dictionaries don’t have any guarantees of order, nor can you insert at a specific index.

    Inserting into a dictionary is always O(1). Lookup operations are also done in O(1) time, which is significantly faster than the O(n) lookup time for a particular element in the array.

    Surprisingly, many of these other data structures can be built efficiently using these two simple standard library primitives.

    Linked list

    A linked list is a collection of values arranged in a linear unidirectional sequence.

    A linked list has several theoretical advantages over contiguous storage options such as the Swift Array:

    • Constant time insertion and removal from the front of the list.
    • Reliable performance characteristics.

    As the diagram suggests, a linked list is a chain of nodes. Nodes have two responsibilities:

    1. Hold a value.
    2. Hold a reference to the next node. A nil value represents the end of the list.
    class Node<V> {
        var value: V
        var next: Node?
        init(value: V, next: Node? = nil) {
            self.value = value
            self.next = next
        }
    }
    
    extension Node: CustomStringConvertible {
        var description: String {
            guard let next = next else { return "\(value)" }
            return "\(value) -> \(next)"
        }
    }
    
    let n1 = Node(value: 1)
    let n2 = Node(value: 2)
    let n3 = Node(value: 3)
    n1.next = n2
    n2.next = n3
    print(n1) // 1 -> 2 -> 3
    

    You can easily see that building long lists this way is impractical. A common way to alleviate this problem is to build a LinkedList that manages the Node objects

    LinkedList

    A linked list has the concept of a head and tail, which refers to the first and last nodes of the list respectively.

    There are three ways to add values to a linked list, each having their own unique performance characteristics:

    1. push: Adds a value at the front of the list.
    2. append: Adds a value at the end of the list.
    3. insert(after:): Adds a value after a particular node of the list.
    struct LinkedList<V> {
        var head: Node<V>?
        var tail: Node<V>?
        init(){}
        var isEmpty: Bool { head == nil }
    }
    

    push O(1)
    Adding a value at the front of the list is known as a push operation. This is also known as head-first insertion.

    mutating func push(value: V) {
            head = Node(value: value, next: head)
            if tail == nil {
                tail = head
            }
        }
    

    append O(1)
    The next operation you’ll look at is append. This is meant to add a value at the end of the list, and is known as tail-end insertion.

        mutating func append(value: V) {
            guard !isEmpty else {
                return push(value: value)
            }
            let newNode = Node(value: value)
            tail?.next = newNode
            tail = newNode
        }
    

    length

        var length: Int {
            var current = head
            var count = 0
            while current != nil {
                count += 1
                current = current?.next
            }
            return count
        }
    

    node(at:) O(i)

        func node(at index: Int) -> Node<V>? {
            var current = head
            var currentIdx = 0
            while current != nil, currentIdx < index {
                current = current?.next
                currentIdx += 1
            }
            return current
        }
    

    firstNode(withValue:)
    lastNode(withValue:)

    func firstNode(withValue value: V) -> Node<V>? {
            var current = head
            while current != nil {
                if current?.value == value {
                    break
                }
                current = current?.next
            }
            return current
        }
        
        func lastNode(withValue value: V) -> Node<V>? {
            var current = head
            var found: Node<V>?
            while current != nil {
                if current?.value == value {
                    found = current
                }
                current = current?.next
            }
            return found
        }
    

    insert(after:) O(i)

    @discardableResult
        mutating func insert(value: V, after node: Node<V>) -> Node<V>? {
            guard tail !== node else {
                append(value: value)
                return tail
            }
            node.next = Node(value: value, next: node.next)
            return node.next
        }
    

    By moving the head down a node, you’ve effectively removed the first node of the list. ARC will remove the old node from memory once the method finishes, since there will be no more references attached to it.

    Removals

    @discardableResult
        mutating func dropFirst() -> V? {
            defer {
                head = head?.next
                if isEmpty {
                    tail = nil
                }
            }
            return head?.value
        }
        
        @discardableResult
        mutating func dropLast() -> V? {
            guard !isEmpty else {
                return nil
            }
            if head === tail {
                return dropFirst()
            }
        var prev = head
        var current = head
        while let next = current.next {
          prev = current
          current = next
        }
        prev.next = nil
        tail = prev
    /*
            guard let lastPrevious = node(at: length - 2) else {
                return nil
            }
            lastPrevious.next = nil
            tail = lastPrevious
    */
            return tail?.value
        }
    
    @discardableResult
        mutating func remove(at index: Int) -> V? {
            guard let nodeCurrent = node(at: index) else {
                return nil
            }
            if nodeCurrent === head {
                return dropFirst()
            }
            else if nodeCurrent === tail {
                return dropLast()
            }
            guard let nodePrevious = node(at: index - 1),
                  let nodeAfter = node(at: index + 1) else {
                return nil
            }
            nodePrevious.next = nodeAfter
            return nodeCurrent.value
        }
        
    

    A linked list can earn two qualifications from the Swift collection protocols. First, since a linked list is a chain of nodes, adopting the Sequence protocol makes sense. Second, since the chain of nodes is a finite sequence, it makes sense to adopt the Collection protocol.

    Swift Collection

    A collection type is a finite sequence and provides nondestructive sequential access.

    The linked list cannot achieve O(1) subscript operations using integer indexes. Thus, your goal is to define a custom index that contains a reference to its respective node.

    /// The `..<` operator
    /// creates a range that doesn't include the upper bound, so it's always
    /// safe to use with `endIndex`. For example:
    
    let numbers = [10, 20, 30, 40, 50]
    if let index = numbers.firstIndex(of: 30) {
        print(numbers[index ..< numbers.endIndex])
    }
    

    Example:

    var list = LinkedList<Int>()
    for i in 0...9 {
        list.append(value: i)
    }
    print("first: \(list[list.startIndex])") // 0
    print("last: \(list[list.index(list.startIndex, offsetBy: list.count-1)])") // 9
    print("first 3: \(Array(list.prefix(3)))")
    print("last 1: \(Array(list.suffix(1)))")
    

    COW Copy-On-Write

    let arr1 = [1, 2]
    var arr2 = arr1
    arr2.append(3) // only arr2 was updated
    

    Unfortunately, your linked list does not have value semantics! This is because your underlying storage uses a reference type (Node).

    The strategy to achieve value semantics with COW is fairly straightforward. Before mutating the contents of the linked list, you want to perform a copy of the underlying storage and update all references (head and tail) to the new copy.

    // copy-on-write: recreate all nodes when mutating
        private mutating func _COW() {
        guard !isKnownUniquelyReferenced(&head) else {
          return
        }
            guard var oldHead = head else {
                return
            }
            head = Node(value: oldHead.value)
            var newTail = head
            while let next = oldHead.next {
                newTail.next = .init(value: next.value)
                newTail = newTail.next
                oldHead = next
            }
            tail = newTail
        }
    
    • In the Swift Standard Library lives a function named isKnownUniquelyReferenced. This function can be used to determine whether or not an object has exactly one reference to it

    • The unidirectional nature of the linked list means that head first insertions can ignore the “COW tax”!

    Stack

    LIFO (last in first out)

    When you add an item to a stack, you place it on top of the stack. When you remove an item from a stack, you always remove the topmost item.

    There are only two essential operations for a stack:

    • push - adding an element to the top of the stack
    • pop - removing the top element of the stack

    This means you can only add or remove elements from one side of the data structure.

    Choosing the right storage type for your stack is important. The array is an obvious choice, since it offers constant time insertions and deletions at one end via append and popLast.

    struct Stack<Element> {
        var storage: [Element] = []
        
        mutating func push(element: Element) {
            storage.append(element)
        }
        
        @discardableResult
        mutating func pop() -> Element? {
            guard !isEmpty else {
                return nil
            }
            return storage.popLast()
        }
        
        func peek() -> Element? {
            storage.last
        }
        
        var isEmpty: Bool {
            peek() == nil
        }
    }
    
    extension Stack: CustomStringConvertible {
        var description: String {
            return "------ top ------\n" +
            storage.map{ "\($0)" }.reversed().joined(separator: "\n") +
            "\n-----------------"
        }
    }
    
    var stack = Stack<Int>()
    stack.push(element: 1)
    stack.push(element: 2)
    stack.push(element: 3)
    print(stack)
    
    stack.pop()
    print(stack)
    
    print(stack.peek())
    

    push and pop both have a O(1) time complexity.The idea of peek is to look at the top element of the stack without mutating its contents.

    A stack's purpose is to limit the number of ways to access your data, and adopting protocols such as Collection would go against this goal by exposing all the elements via iterators and the subscript. In this case, less is more!

    Stacks are crucial to problems that search trees and graphs. Imagine finding your way through a maze. Each time you come to a decision point of left right or straight you can push all possible decisions onto your stack. When you hit a dead end, simply backtrack by popping from the stack and continuing until you escape or hit another dead end.

    Queue

    FIFO(first-in-first-out)

    Queues are handy when you need to maintain the order of your elements to process later.

    • enqueue: Insert an element at the back of the queue. Returns true if the operation was successful.
    • dequeue: Remove the element at the front of the queue and return it.
    • isEmpty: Check if the queue is empty.
    • peek: Return the element at the front of the queue without removing it.

    In the following sections, you will learn to create a queue in four
    different ways:

    • Using an array
    • Using a doubly linked list
    • Using a ring buffer
    • Using two stacks

    Array

    Here you’ve defined a generic QueueArray struct that adopts the Queue protocol. Note that the associated type Element is inferred by the type parameter T.

    Regardless of the size of the array, enqueueing an element is an O(1) operation.

    After adding multiple elements, the array will eventually be full. When you want to use more than the allocated space, the array must resize to make additional room.

    Resizing is an O(n) operation. Resizing requires the array to allocate new memory and copy all existing data over to the new array.

    Removing an element from the front of the queue is an O(n) operation. To dequeue, you remove the element from the beginning of the array. This is always a linear time operation, because it requires all the remaining elements in the array to be shifted in memory.

    protocol Queue {
        associatedtype Element
        mutating func enqueue(element: Element) -> Bool
        mutating func dequeue() -> Element?
        var isEmpty: Bool {get}
        func peek() -> Element?
    }
    
    struct ArrayQueue<T>: Queue {
        private var storage: [T] = []
        
        @discardableResult
        mutating func enqueue(element: T) -> Bool {
            storage.append(element)
            return true
        }
        
        @discardableResult
        mutating func dequeue() -> T? {
            guard !isEmpty else {
                return nil
            }
            return storage.removeFirst()
        }
        
        func peek() -> T? {
            storage.first
        }
        
        var isEmpty: Bool {
            storage.isEmpty
        }
    }
    
    extension ArrayQueue: CustomStringConvertible {
        var description: String {
            storage.description
        }
    }
    
    var q = ArrayQueue<Int>()
    q.enqueue(element: 1)
    q.enqueue(element: 2)
    q.enqueue(element: 3)
    print(q)
    q.dequeue()
    print(q)
    print(q.peek())
    

    There are some shortcomings to the implementation. Removing an item from the front of the queue can be inefficient, as removal causes all elements to shift up by one. This makes a difference for very large queues. Once the array gets full, it has to resize and may have unused space. This could increase your memory footprint over time.

    Doubly linked list

    A doubly linked list is simply a linked list in which nodes also contain a reference to the previous node.

    class Node<T> {
        var previous: Node<T>?
        var next: Node<T>?
        var value: T
        init(previous: Node<T>? = nil, next: Node<T>? = nil, value: T) {
            self.previous = previous
            self.next = next
            self.value = value
        }
    }
    
    extension Node: CustomStringConvertible {
        var description: String {
            return "\(value)"
        }
    }
    
    class DoublyLinkedList<T> {
        private var head: Node<T>?
        private var tail: Node<T>?
        init(){}
        
        var isEmpty: Bool { head == nil }
        var first: Node<T>? { head }
        var last: Node<T>? { tail }
        
        func append(value: T) {
            let newNode = Node(value: value)
            guard let tailNode = tail else {
                head = newNode
                tail = head
                return
            }
            newNode.previous = tailNode
            tailNode.next = newNode
            tail = newNode
        }
        
        @discardableResult
        func remove(node: Node<T>) -> T? {
            let nodePrevious = node.previous
            let nodeNext = node.next
            
            // is removing the first one
            if let previous = nodePrevious {
                previous.next = nodeNext
            } else {
                head = nodeNext
            }
            
            // is removing the last one
            if let next = nodeNext {
                next.previous = nodePrevious
            } else {
                tail = nodePrevious
            }
            
            node.previous = nil
            node.next = nil
            return node.value
        }
    }
    
    extension DoublyLinkedList: CustomStringConvertible {
        var description: String {
            guard !isEmpty else {
                return "Empty List"
            }
            var current = head
            var stringNodes = Array<String>()
            while let node = current {
                stringNodes.append("\(node.value)")
                current = node.next
            }
            return stringNodes.joined(separator: " <-> ")
        }
    }
    

    Queue with a doubly linked list:

    struct LinkedListQueue<T>: Queue {
        private var storage = DoublyLinkedList<T>()
        
        @discardableResult
        mutating func enqueue(element: T) -> Bool {
            storage.append(value: element) // insert at 0
            return true
        }
        
        @discardableResult
        mutating func dequeue() -> T? { // remove last
            guard let first = storage.first else {
                return nil
            }
            return storage.remove(node: first)
        }
        
        func peek() -> T? {
            storage.first?.value
        }
        
        var isEmpty: Bool {
            storage.isEmpty
        }
    }
    
    extension LinkedListQueue: CustomStringConvertible {
        var description: String {
            storage.description
        }
    }
    
    var q = LinkedListQueue<Int>()
    q.enqueue(element: 1)
    q.enqueue(element: 2)
    q.enqueue(element: 3)
    print(q) // (head)1 <-> 2 <-> 3(tail)
    q.dequeue()
    print(q) // 2 <-> 3
    print(q.peek()) // Optional(2)
    
    • Enqueue: O(1)
    • Dequeue: O(1)

    Compared to the array implementation, you didn’t have to shift elements one by one. Instead, in the diagram above, you simply update the next and previous pointers.

    Each element has to have extra storage for the forward and back reference. Moreover, every time you create a new element, it requires a relatively expensive dynamic allocation. By contrast QueueArray does bulk allocation which is faster.

    Ring buffer

    Adding new items to the back of the queue is fast, O(1), but removing items from the front of the queue is slow, O(n).Removing is slow because it requires the remaining array elements to be shifted in memory.

    You can use a queue based on a ring buffer to keep track of whose turn is coming up next.

    A ring buffer, also known as a circular buffer, is a fixed-size array.

    The ring buffer has two pointers that keep track of two things:

    1. The read pointer keeps track of the front of the queue.
    2. The write pointer keeps track of the next available slot so you can override existing elements that have already been read.

    Each time you add an item to the queue, the write pointer increments by one.

    Dequeuing is the equivalent of reading a ring buffer.

    Since the write pointer reached the end, it simply wraps around to the starting index again.

    The read pointer wraps to the beginning as well.

    As final observation, notice that whenever the read and write pointers are at the same index, that means the queue is empty.

    But hold up, you say, how does this wrapping around work? There are several ways to accomplish this and I chose a slightly controversial one. In this implementation, the writeIndex and readIndex always increment and never actually wrap around.

    • The reason this is a bit controversial is that writeIndex and readIndex always increment, so in theory these values could become too large to fit into an integer and the app will crash. However, a quick back-of-the-napkin calculation should take away those fears.
    • Both writeIndex and readIndex are 64-bit integers. If we assume that they are incremented 1000 times per second, which is a lot, then doing this continuously for one year requires ceil(log_2(365 * 24 * 60 * 60 * 1000)) = 35 bits. That leaves 28 bits to spare, so that should give you about 2^28 years before running into problems. That's a long time. :-)
    • You've seen that a ring buffer can make a more optimal queue but it also has a disadvantage: the wrapping makes it tricky to resize the queue. But if a fixed-size queue is suitable for your purposes, then you're golden.

    A ring buffer is also very useful for when a producer of data writes into the array at a different rate than the consumer of the data reads it. This happens often with file or network I/O. Ring buffers are also the preferred way of communicating between high priority threads (such as an audio rendering callback) and other, slower, parts of the system.

    The implementation given here is not thread-safe. It only serves as an example of how a ring buffer works. That said, it should be fairly straightforward to make it thread-safe for a single reader and single writer by using OSAtomicIncrement64() to change the read and write pointers.

    RingBuffer

    class RingBuffer<T> {
        private var writeIndex = 0
        private var readIndex = 0
        private var storage = Array<T?>()
        init(count: Int) {
            storage = Array<T?>(repeating: nil, count: count)
        }
        
        var first: T? {
            guard !isEmpty else {
                return nil
            }
            return storage[readIndex]
        }
        
        @discardableResult
        func write(_ element: T) -> Bool {
            print("write index: \(writeIndex)")
            if !isFull {
                storage[writeIndex % storage.count] = element
                writeIndex += 1
                return true
            }
            else {
                return false
            }
        }
        
        func read() -> T? {
            print("read index: \(readIndex)")
            if !isEmpty {
                let head = storage[readIndex % storage.count]
                readIndex += 1
                return head
            }
            else {
                return nil
            }
        }
        
        var isEmpty: Bool {
            availableSpaceForReading == 0
        }
        
        private var availableSpaceForReading: Int {
            writeIndex - readIndex
        }
        
        private var availableSpaceForWriting: Int {
            storage.count - availableSpaceForReading
        }
        
        private var isFull: Bool {
            availableSpaceForWriting == 0
        }
    }
    
    extension RingBuffer: CustomStringConvertible {
        public var description: String {
            print(storage)
            let values = (0..<availableSpaceForReading).map {
                String(describing: storage[($0 + readIndex) % storage.count]!)
            }
            return "[" + values.joined(separator: ", ") + "]"
        }
    }
    

    RingBufferQueue

    struct RingBufferQueue<T>: Queue {
        private var storage: RingBuffer<T>
        
        init(count: Int) {
            self.storage = .init(count: count)
        }
        
        @discardableResult
        mutating func enqueue(element: T) -> Bool {
            return storage.write(element)
        }
        
        @discardableResult
        mutating func dequeue() -> T? {
            return storage.read()
        }
        
        func peek() -> T? {
            storage.first
        }
        
        var isEmpty: Bool {
            storage.isEmpty
        }
    }
    
    extension RingBufferQueue: CustomStringConvertible {
        var description: String {
            storage.description
        }
    }
    
    var q = RingBufferQueue<Int>(count: 3)
    q.enqueue(element: 1) // true
    q.enqueue(element: 2) // true
    q.enqueue(element: 3) // true
    q.enqueue(element: 4) // false
    print(q) // [1, 2, 3]
    q.dequeue() // 1
    print(q) // [2, 3]
    print(q.peek()) // 2
    

    The ring buffer has a fixed size which means that enqueue can fail.

    Double stack

    The idea behind using two stacks is simple. Whenever you enqueue an element, it goes in the right stack. When you need to dequeue an
    element, you reverse the right stack and place it in the left stack so you can retrieve the elements using FIFO order.

    struct Stack<Element> {
        private var storage: [Element] = []
        
        var top: Element? {
            peek()
        }
        
        var bottom: Element? {
            storage.first
        }
        
        mutating func push(element: Element) {
            storage.append(element)
        }
        
        @discardableResult
        mutating func pop() -> Element? {
            guard !isEmpty else {
                return nil
            }
            return storage.popLast()
        }
        
        func peek() -> Element? {
            storage.last
        }
        
        var isEmpty: Bool {
            peek() == nil
        }
    }
    
    extension Stack {
        mutating func reversed() -> Self {
            .init(storage: storage.reversed())
        }
        
        mutating func removeAll() {
            storage.removeAll()
        }
        
        var elements: [Element] {
            storage.map{ $0 }
        }
    }
    
    extension Stack: CustomStringConvertible {
        var description: String {
            return "------ top ------\n" +
            storage.map{ "\($0)" }.reversed().joined(separator: "\n") +
            "\n-----------------"
        }
    }
    

    StacksQueue

    public struct StacksQueue<T> : Queue {
    private var leftStack: [T] = []
    private var rightStack: [T] = []
    public init() {}
    }
    
    extension StacksQueue: CustomStringConvertible {
        var description: String {
            print("left stack: \(leftStack), right stack: \(rightStack)")
            let all = (leftStack.elements + rightStack.elements).map{ "\($0)" }
            return "queue: [\(all.joined(separator: ", "))]"
        }
    }
    
    var q = StacksQueue<Int>()
    q.enqueue(element: 1)
    q.enqueue(element: 2)
    q.enqueue(element: 3)
    print(q)
    q.dequeue()
    print(q)
    print(q.peek())
    

    Remember, you only transfer the elements in the right stack when the left stack is empty!

    Moreover, your two stack implementation is fully dynamic and doesn’t have the fixed size restriction that your ring-buffer-based queue implementation has.

    Finally, it beats the linked list in terms of spacial locality. This is because array elements are next to each other in memory blocks. So a large number of elements will be loaded in a cache on first access.

    Compare this to a linked list where the elements aren’t in contiguous blocks of memory. This could lead to more cache misses which will increase access time.

    相关文章

      网友评论

          本文标题:Swift | Data Structures & Algori

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