数据结构学习-链表(LinkedList)

692 阅读5分钟

我们知道数组在内存中是连续分配的,如果当前分配的内存满了,然后又往里面添加元素的时候,系统会在当前的内存大小基础上进行一定倍数的扩容来创建一个新的内存区域,扩容成功后再把原来内存的数组元素拷贝到新开辟的连续内存中。

这样可能会有一个问题就是数组里面的一些内存可能是用不到的,造成一定的浪费。那么能不能添加一个元素就申请一块内存?链表就是一个动态分配每个元素内存的线性数据结构(链表里面元素的内存不一定是连续分配的)。

链表的数据结构:

节点 (Node)

链表里面每个元素用节点表示,节点有以下特性:

  • 保存一个值
  • 有下一个节点的引用

节点代码实现:

class Node<Value> {
    var value: Value  // 保存节点的值
    var next: Node?   // 指向下一个节点,可以为空
    
    init(value: Value, next: Node? = nil) {
        self.value = value
        self.next = next
    }
}

extension Node: CustomStringConvertible {
    
    var description: String {
        guard let next = next else {
            return "\(value)"
        }
        return "\(value) -> " + String(describing: next) + " "
    }
}

简单测试:

    func nodeTest() {
        let node1 = Node(value: 1)
        let node2 = Node(value: 2)
        let node3 = Node(value: 3)
        node1.next = node2
        node2.next = node3
        print(node1)
    }

可以看到输出是这样的:

1 -> 2 -> 3  

节点为什么使用 class 实现,而不是 struct?这是因为使用引用类型在没有指针指向的时候,对象能够在合适的时候销毁。

链表的实现

链表里面的元素使用节点来表示,开始的节点称为头节点 head, 结束的节点为尾节点 tail。

基本代码:

struct LinkedList<Value> {

    var head: Node<Value>?  // 指向头节点
    var tail: Node<Value>?  // 指向尾节点
}

extension LinkedList: CustomStringConvertible {
    var description: String {
        guard let head = head else {
            return "Empty list"
        }
        return String(describing: head)
    }
}

链表在实际使用的时候主要有添加和删除节点功能。

添加功能

添加节点有下面3种方式:

  • push, 添加节点到链表头部;

  • append, 添加节点到链表末尾;

  • insert(after:), 插入节点到某个位置。

push 实现

    // 时间复杂度 O(1)
    mutating func push(_ value: Value) {
        head = Node(value: value, next: head)
        if tail == nil {
            tail = head
        }
    }

append 实现

    // 判断链表是否为空
    var isEmpty: Bool {
        head == nil
    }
    
    // 时间复杂度 O(1)
    mutating func append(_ value: Value) {
        if isEmpty {
            push(value)
            return
        }
        tail?.next = Node(value: value, next: nil)
        tail = tail?.next
    }

insert(after:) 实现

把节点插入到某个位置需要两个步骤:

1.找到要插入到节点的位置;

2.插入新节点。

找到某个位置的节点

    // 时间复杂度 O(n)
    func node(at index: Int) -> Node<Value>? {
        var current = head
        var currentIndex = 0
        while current != nil && currentIndex < index {
            current = current?.next
            currentIndex += 1
        }
        return current
    }

插入新节点

    // 时间复杂度 O(1)
    @discardableResult
    func insert(_ value: Value, after node: Node<Value>) -> Node<Value>  {
        guard tail !== node else {
            return tail!
        }
        node.next = Node(value: value, next: node.next)
        return node.next!
    }

到此需要实现的节点添加功能就都实现了,下面是测试代码:

    func listAddTest() {
        var list = LinkedList<Int>()
        list.push(1)
        list.append(2)
        list.append(7)
        print("before inserting, list: \(list)")
        if let node = list.node(at: 1) {
            list.insert(10, after: node)
        }
        print("after inserting, list: \(list)")
        print("list is empty: \(list.isEmpty)")
    }

查看输出:

before inserting, list: 1 -> 2 -> 7  
after inserting, list: 1 -> 2 -> 10 -> 7   
list is empty: false

删除功能

删除节点有下面3种方式:

  • pop, 删除链表头节点;

  • removeLast, 删除尾节点;

  • remove(after:), 删除某个节点。

pop 实现

    // 时间复杂度 O(1)
    @discardableResult
    mutating func pop() -> Value? {
        defer {
            head = head?.next
            if isEmpty {
                tail = nil
            }
        }
        return head?.value
    }

removeLast 实现

移除尾节点的关键点是找到尾节点的前一个节点,移除以后再重置尾节点的指向。

    // 时间复杂度 O(n)
    @discardableResult
    mutating func removeLast() -> Value? {
        guard let head = head else {
            return nil
        }
        guard head.next != nil else {
            return pop()
        }
        var prev = head
        var current = head
        while let next = current.next {
            prev = current
            current = next
        }
        prev.next = nil
        tail = prev
        return current.value
    }

remove(after:) 实现

    // 时间复杂度 O(1)
    @discardableResult
    mutating func remove(after node: Node<Value>) -> Value? {
        defer {
            if node.next === tail {
                tail = node
            }
            node.next = node.next?.next
        }
        return node.next?.value
    }

测试删除功能代码:

    func listDeleteTest() {
        var list = LinkedList<Int>()
        list.push(1)
        list.push(7)
        list.append(5)
        list.append(9)
        list.append(3)

        print("before pop, list: \(list)")
        list.pop()
        print("after pop, list: \(list)")
        list.removeLast()
        print("after removeLast, list: \(list)")
        list.remove(after: list.node(at: 0)!)
        print("remove node after index 0, list: \(list)")
    }

查看输出:

before pop, list: 7 -> 1 -> 5 -> 9 -> 3    
after pop, list: 1 -> 5 -> 9 -> 3   
after removeLast, list: 1 -> 5 -> 9  
remove node after index 0, list: 1 -> 9 

目前链表的主要功能就都已经实现了,在实际的代码实现的时候需要注意的是添加和删除的边界问题,比如链表是否为空,或者链表是否只有一个节点。

(由于在Swift中,struct类型的数据使用的时候是值类型的,并且像这种集合数据结构最好需要实现类似数组的写时复制功能,但是目前的重点是数据结构,所以那些功能就不实现了)

一些练习

反转输出链表数据

比如链表是这样的:
       
1 -> 5 -> 9

输出:
9
5
1

实现代码:

    // 时间复杂度O(n)
    func reversedPrintList(_ list: LinkedList<Int>) {
        var tmpList = list
        var arry: [Int] = [] // 空间复杂度O(n)
        while let node = tmpList.pop() {
            arry.append(node)
        }
        while !arry.isEmpty {
            let node = arry.removeLast()
            print(node)
        }
    }

查看打印测试输出,可以看到是OK的。但是目前的实现有一个问题就是会临时创建一个数组,空间复杂度为 O(n)。

下面是一个使用递归优化后的实现:

    func printInReverse<T>(_ node: Node<T>?) {
        guard let node = node else { return } // 如果想的话,这一步可以直接判断是否为空,不用创建临时变量
        printInReverse(node.next) // 关键
        print("\(node.value)")
    }
    
    func printInReverseForList<T>(_ list: LinkedList<T>) {
        printInReverse(list.head)
    }

找到中间节点

// 中间节点为 index = 2, value = 10
1 -> 5 -> 10 -> 9

// 中间节点为 index = 1, value = 5
1 -> 5 -> 10

代码实现:

    func middleNode() -> Node<Value>? {
        if isEmpty {
            return nil
        }
        
        // 主要是找到链表的大小,然后除2
        var current = head
        var curentIndex = 0
        
        while let next = current?.next {
            current = next
            curentIndex += 1
        }
        let count = curentIndex + 1
        
        let middle = count/2
        
        return node(at: middle)
    }

使用快慢指针实现(核心的想法是慢指针走1步,快指针走2步):

    func middleNode() -> Node<Value>? {
        var fast = head
        var slow = head

        while let fastNext = fast?.next {
            fast = fastNext.next
            slow = slow?.next
        }
        return slow
    }

如果想要更多链表的练习,到 LeetCode 筛选就有很多。

在实际的练习解答中,如果思路比较模糊,拿只笔多画一下或许就清晰了。