數(shù)據(jù)結(jié)構(gòu)和算法:鏈表(Linked List)

一角撞、什么是鏈表

鏈表跟數(shù)組類似哲鸳,也是一個有序集合摹量。但他們的區(qū)別在于涤伐,創(chuàng)建數(shù)組時需要分配一大塊內(nèi)存用來存儲元素,而鏈表中的元素在內(nèi)存分配上是相互獨立的缨称,元素與元素之間是通過指針或者引用連接起來的凝果。

+--------+      +--------+      +--------+      +--------+
|        | next |        | next |        | next |        |
| node 0 |----->| node 1 |----->| node 2 |----->| node 3 |
|        |      |        |      |        |      |        |
+--------+      +--------+      +--------+      +--------+
                    圖 1. 單向鏈表

1. 節(jié)點

一般我們把鏈表中的元素稱為“節(jié)點”。

2. 單向鏈表與雙向鏈表(Singly vs doubly linked lists)

節(jié)點與節(jié)點之間會通過指針或者引用連接起來睦尽,根據(jù)節(jié)點連接方式的不同器净,我們可以把鏈表分為單向鏈表雙向鏈表兩種。

單向鏈表

上面圖 1 中所示的就是一個單向鏈表当凡。單向鏈表中的節(jié)點只有一個指向下一個元素的指針/引用(“next”指針)山害。

雙向鏈表

下面圖 2 中所示的是一個雙向鏈表。雙向鏈表中的節(jié)點有一個指向下一個元素的指針/引用(“next”指針)沿量,同時還有一個指向上一個元素的指針/引用(“previous”指針)浪慌。

+--------+ next +--------+ next +--------+ next +--------+
|        |----->|        |----->|        |----->|        |
| node 0 |      | node 1 |      | node 2 |      | node 3 |
|        |<-----|        |<-----|        |<-----|        |
+--------+ prev +--------+ prev +--------+ prev +--------+
                    圖 2. 雙向鏈表

3. head 指針和 tail 指針

在使用鏈表時,我們一般需要知道鏈表從哪里開始的朴则,所以鏈表一般會有一個 head 指針权纤,指向鏈表中的第一個元素。此時第一個元素的 previous 指針為 nil乌妒。

有時候汹想,我們還會有一個 tail 指針,指向鏈表中的最后一個元素撤蚊。此時最后一個元素的 next 指針為 nil古掏。

        +--------+ next +--------+ next +--------+ next +--------+
head--->|        |----->|        |----->|        |----->|        |--->nil
        | node 0 |      | node 1 |      | node 2 |      | node 3 |
nil <---|        |<-----|        |<-----|        |<-----|        |<---tail
        +--------+ prev +--------+ prev +--------+ prev +--------+
                    圖 3. head 指針和 tail 指針

4. 其他類型的鏈表

  • 循環(huán)鏈表(Circular Linked list)
  • 多重表(Multiply linked list)

二、為什么使用鏈表(鏈表的特點)

相比數(shù)組侦啸,鏈表的插入和刪除效率更高冗茸,對于不需要搜索但變動頻繁且無法預(yù)知數(shù)量上限的數(shù)據(jù),更適合用鏈表匹中。

比如,當(dāng)我們從一個數(shù)組中移除第一個元素后豪诲,需要將后面的元素在內(nèi)存中的位置都往前移顶捷,這就意味著需要重新進行內(nèi)存分配和內(nèi)存布局,因為數(shù)組中的元素在內(nèi)存上是連續(xù)的屎篱。但是對于鏈表服赎,我們只需要把 head 指針/引用指向第二個元素就可以了葵蒂。

所以鏈表的特點顯而易見:

  • 優(yōu)點
    • 鏈表是一個動態(tài)的數(shù)據(jù)結(jié)構(gòu),其容量可以隨意增大和減小
    • 鏈表不需要在初始化時重虑,提前申請用于存儲元素的內(nèi)存
    • 鏈表是一個將多個局部節(jié)點連接起來的數(shù)據(jù)結(jié)構(gòu)(類似的還有樹和圖)
    • 插入刪除不需要移動其他元素践付,操作起來非常簡單
  • 缺點
    • 鏈表比數(shù)組要占用更多的內(nèi)存,因為鏈表除了需要用于值存儲之外缺厉,還有一些指針也需要占用空間
    • 讀取鏈表的節(jié)點時永高,必須要從表頭或者表尾開始一個一個按順序讀
    • 從鏈表中查找元素是比較費時的,因為鏈表中的節(jié)點不是連續(xù)存儲的提针,所以訪問單個節(jié)點的效率比較低

三命爬、鏈表的實現(xiàn)

實現(xiàn)鏈表有幾個要點:

  • 設(shè)計鏈表的兩個要素
    • 節(jié)點
      • next指針
      • previous 指針(雙向鏈表)
    • 表頭和表尾
      • head 指針
      • tail 指針(可選)
  • 操作鏈表時的兩個要點
    • 更新 next指針和 previous 指針
    • 更新 head 指針和 tail 指針

1. Swift(源碼

注:這里是基于 class 實現(xiàn)的,當(dāng)然你也可以使用 enum辐脖。

/// 鏈表節(jié)點
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
    }


    // 從 head 開始往后一步一步走饲宛,直到最后一個節(jié)點
    // 注:如果我們有一個記錄 tail 節(jié)點的實例變量,那么這個 last 方法就可以直接返回 tail 節(jié)點嗜价。但是我們在這里沒有這么做艇抠,所以這個 last 方法是一個比較耗時的操作,尤其是當(dāng)鏈表特別長的時候
    public var last: Node? {
    
        if var node = head {
            while let next = node.next {
                node = next
            }
            return node
        } else {
            return nil
        }
    }
    
    // 找到最后一個節(jié)點久锥,將新節(jié)點拼接在后面
    public func append(_ value: T) {
    
        let newNode = Node(value: value)
        
        if let lastNode = self.last {
            newNode.previous = lastNode
            lastNode.next = newNode
        } else {
            head = newNode
        }
    }

    // 從 head 開始往后一步一步走家淤,每走一步 count 加 1
    // 注:這種方式的復(fù)雜度為 O(n),如果我們給鏈表添加一個實例變量用來追蹤 count 值的話奴拦,復(fù)雜度就變成了O(1)媒鼓,但是這樣我們就需要在每次添加或者移除一個節(jié)點的同時,去更新這個變量
    public var count: Int {
    
        if var node = head {
            var c = 1
            while let next = node.next {
                node = next
                c += 1
            }
            return c
        } else {
            return 0
        }
    }
    
    

    // 從 head 開始往后走错妖,走 index 步就可以得到結(jié)果了
    // 0(head) -> 1 -> 2 -> 3
    public func nodeAt(_ index: Int) -> Node? {
    
        if index >= 0 {
            var node = head
            var leftStep = index // 需要走多少步
            
            while node != nil {
            
                if leftStep == 0 { return node }  // 直到剩余 0 步時才返回
                leftStep -= 1                     // 剩余步數(shù)減 1
                node = node!.next                 // 往前走一步
            }
     
//            // 另一種方案
//            for _ in 0..<index {
//                if let tempNode = node, tempNode.next != nil {
//                    node = tempNode.next
//                } else {
//                    node = nil
//                }
//            }
//            return node

        }
        return nil
    }
    
    // 下標(biāo)方法绿鸣,內(nèi)部直接調(diào)用的是 nodeAt 方法,同時做了越界拋異常處理
    public subscript(index: Int) -> T {
        let node = nodeAt(index)
        assert(node != nil)
        return node!.value
    }
    
    /*  示意圖:
     *  head --> A --> B --> C --> D --> E --> F --> nil
     *          prev  next
     */
     // 這個方法和 nodesBeforeAndAfter 方法同樣適用于單向鏈表暂氯,因為它們的實現(xiàn)不依賴于 prev 指針
    public func insert(_ value: T, atIndex index: Int) {
        // 找到要插入的位置的前后節(jié)點潮模,要注意為 nil 的情況
        let (prev, next) = nodesBeforeAndAfter(index)
        
        // 將新節(jié)點插入鏈表中,將其與前后節(jié)點鏈接起來
        let newNode = Node(value: value)
        newNode.previous = prev
        newNode.next = next
        prev?.next = newNode
        next?.previous = newNode
        
        // 當(dāng)插入的位置是 0痴施,那么就需要更新 head 了
        // 注:如果有 tail 并且插入的位置是最后的話擎厢,也需要更新 tail
        if prev == nil {
            head = newNode
        }
    }
    
    // 跟 nodeAt 方法類似,也是從 head 開始辣吃,一個一個往后找
    // 這個方法是用來查找位于 index 位置的節(jié)點以及位于 index 前面一位的節(jié)點
    private func nodesBeforeAndAfter(_ index: Int) -> (Node?, Node?) {
        assert(index >= 0)
        
        var i = index
        var next = head
        var prev: Node?
        
        while next != nil && i > 0 {
            i -= 1
            prev = next
            next = next!.next
        }
        
        assert(i == 0)  // 越界的處理
        
        return (prev, next)
    }
    
    
    // 移除某個節(jié)點
    // 這個方法的操作最簡單动遭,因為它不需要從頭開始一個一個去找這個節(jié)點
    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 removeAt(_ index: Int) -> T {
        let node = nodeAt(index)
        assert(node != nil)
        return remove(node: node!)
    }

    
    // 移除所有的節(jié)點
    // 注:如果有 tail 的話,也要把 tail 置為 nil
    public func removeAll() {
        head = nil
    }
    
    
    
    
        /*
        
      pre +--------+  pre +--------+  pre +--------+  pre +--------+
 nil <----|        |<-----|        |<-----|        |<-----|        |
          | node 0 |      | node 1 |      | node 2 |      | node 3 |
head ---->|        |----->|        |----->|        |----->|        |-----> nil
          +--------+ next +--------+ next +--------+ next +--------+ next 
          
         
     
     next +--------+  pre +--------+  pre +--------+  pre +--------+
nil <-----|        |<-----|        |<-----|        |<-----|        |
          | node 0 |      | node 1 |      | node 2 |      | node 3 |
          |        |----->|        |----->|        |----->|        |-----> nil
          +--------+  pre +--------+ next +--------+ next +--------+ next
                              ∧
                              |
                             head

     next +--------+ next +--------+  pre +--------+  pre +--------+
nil <-----|        |<-----|        |<-----|        |<-----|        |
          | node 0 |      | node 1 |      | node 2 |      | node 3 |
          |        |----->|        |----->|        |----->|        |
          +--------+  pre +--------+ pre  +--------+ next +--------+
                                               ∧
                                               |
                                              head

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

        */
    
    // 反轉(zhuǎn)鏈表
    // 如上圖所示:一步一步往后走神得,依次將每個節(jié)點的 prev 指針和 next 指針反轉(zhuǎn)過來厘惦,同時還要把 head 和 tail(如果有的話)調(diào)換一下
    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
        }
    }
    

2. Objective-C(源碼

NS_ASSUME_NONNULL_BEGIN

/**
 鏈表節(jié)點
 */
@interface SCLinkedListNode<__covariant ObjectType> : NSObject

@property (strong, nonatomic) ObjectType value;
@property (strong, nonatomic, nullable) SCLinkedListNode *next;
@property (weak, nonatomic, nullable) SCLinkedListNode *previous;

- (instancetype)initWithValue:(ObjectType)value;

@end

NS_ASSUME_NONNULL_END

@implementation SCLinkedListNode

- (instancetype)initWithValue:(id)value {

    self = [super init];
    if (self) {
        _value = value;
    }
    return self;
}

- (NSString *)description {
    return self.value;
}

@end


NS_ASSUME_NONNULL_BEGIN

typedef SCLinkedListNode SCNode;

/**
 一個雙向鏈表
 有 head,但是沒有 tail
 
 0(head/first) -> 1 -> 2 -> 3 -> 4(last)
 
 */
@interface SCLinkedList<__covariant ObjectType> : NSObject

@property (assign, nonatomic, readonly) BOOL isEmpty;     ///< 是否為空
@property (strong, nonatomic, readonly) SCNode *first;    ///< 第一個節(jié)點
@property (strong, nonatomic, readonly) SCNode *last;     ///< 最后一個節(jié)點

@property (assign, nonatomic) NSUInteger count; ///< 節(jié)點個數(shù)


// 插入節(jié)點
- (void)appendNodeWithValue:(ObjectType)value;
- (void)insertNodeWithValue:(ObjectType)value atIndex:(NSInteger)index;

// 訪問節(jié)點
- (SCNode *)nodeAtIndex:(NSInteger)index;
- (ObjectType)objectAtIndexedSubscript:(NSInteger)index;

// 移除操作
- (SCNode *)removeNode:(SCNode *)node;
- (SCNode *)removeNodeAtIndex:(NSUInteger)index;
- (SCNode *)removeLastNode;
- (void)removeAllNodes;

// 翻轉(zhuǎn)鏈表
- (void)reverse;

@end

NS_ASSUME_NONNULL_END

@interface SCLinkedList ()

@property (strong, nonatomic, nullable) SCNode *head;     ///< 鏈表頭

@end

@implementation SCLinkedList

#pragma mark - Getter
- (BOOL)isEmpty {
    return self.head == nil;
}

- (SCNode *)first {
    return self.head;
}

// 從 head 開始往后一步一步走哩簿,直到最后一個節(jié)點
// 注:如果我們有一個記錄 tail 節(jié)點的實例變量宵蕉,那么這個 last 方法就可以直接返回 tail 節(jié)點酝静。但是我們在這里沒有這么做,所以這個 last 方法是一個比較耗時的操作羡玛,尤其是當(dāng)鏈表特別長的時候
- (SCNode *)last {
    
    // 如果表頭為空别智,直接返回 nil
    if (self.head == nil) {
        return nil;
    }
    
    
    // 0(head) -> 1 -> 2 -> 3 -> 4(last)
    
    SCNode *currentNode = self.head;
    while (currentNode.next != nil) { // 下一個節(jié)點不是空的就去下一個
        currentNode = currentNode.next;
    }
    
    return currentNode;
}

// 從 head 開始往后一步一步走,每走一步 count 加 1
// 注:這種方式的復(fù)雜度為 O(n)稼稿,如果我們給鏈表添加一個實例變量用來追蹤 count 值的話薄榛,復(fù)雜度就變成了O(1),但是這樣我們就需要在每次添加或者移除一個節(jié)點的同時渺杉,去更新這個變量
- (NSUInteger)count {
    
    // 0(head) -> 1 -> 2 -> 3 -> 4(last)
    //
    
    if (self.head == nil) {
        return 0;
    }
    
    SCNode *currentNode = self.head;
    
    NSInteger count = 1;
    while (currentNode.next != nil) { // 下一個節(jié)點不是空的蛇数,就往下去一個
        count++;
        currentNode = currentNode.next;
    }
    
    return count;
}



#pragma mark - 讀取節(jié)點

// 從 head 開始往后走,走 index 步就可以得到結(jié)果了
// 0(head) -> 1 -> 2 -> 3
- (SCNode *)nodeAtIndex:(NSInteger)index {
    NSAssert(index >= 0, @"Error: Out of bounds");

    // 鏈表為空就直接返回 nil
    if (self.head == nil) {
        return nil;
    }
    
    
    SCNode *currentNode = self.head; // 從 head 往后走
    
    NSInteger step = index;
    
    // 剩余步數(shù)不為 0
    while (step > 0) {
        
        // 往后走一步
        step--;
        currentNode = currentNode.next;
        
        // 出界了就返回 nil
        if (currentNode == nil) {
            return nil;
        }
    }
    
    return currentNode;
}


// http://www.cnblogs.com/zenny-chen/p/3593660.html
// 下標(biāo)方法是越,內(nèi)部直接調(diào)用的是 nodeAt 方法耳舅,同時做了越界拋異常處理
- (id)objectAtIndexedSubscript:(NSInteger)index {
    
    SCNode *node = [self nodeAtIndex:index];
    NSAssert(node != nil, nil);
    
    return node.value;
}


#pragma mark - 插入節(jié)點
// 找到最后一個節(jié)點,將新節(jié)點拼接在后面
- (void)appendNodeWithValue:(id)value {
    
    // 0(head) -> 1 -> 2 -> 3(last)
    // 0(head) -> 1 -> 2 -> 3 -> 4(last)
    
    SCNode *newNode = [[SCNode alloc] initWithValue:value];
    
    if (self.last) {
        newNode.previous = self.last;
        self.last.next = newNode;
    } else {
        self.head = newNode;
    }
}

// 跟 nodeAt 方法類似倚评,也是從 head 開始浦徊,一個一個往后找
// 這個方法是用來查找位于 index 位置的節(jié)點以及位于 index 前面一位的節(jié)點
- (void)p_findNodesBeforeAndAtIndex:(NSInteger)index completion:(void(^)(SCNode *prev, SCNode *this))completion {
    NSAssert(index >= 0, @"Error: out of bounds");
    
    // 0(head) -> 1 -> 2 -> 3
    
    NSInteger step = index;
    SCNode *currentNode = self.head;
    SCNode *prevOfCurrentNode = nil;
    
    while (step > 0 && currentNode.next) {
        step--;
        prevOfCurrentNode = currentNode;
        currentNode = currentNode.next;
    }
    
    NSAssert(step == 0, @"Error: Out of bounds");
    
    // 返回結(jié)果
    if (completion) {
        completion(prevOfCurrentNode, currentNode);
    }


}


/*  示意圖:
 *  head --> A --> B --> C --> D --> E --> F --> nil
 *          prev  next
 */
// 這個方法和 nodesBeforeAndAtIndex 方法同樣適用于單向鏈表,因為它們的實現(xiàn)不依賴于 prev 指針
- (void)insertNodeWithValue:(id)value atIndex:(NSInteger)index {

    [self p_findNodesBeforeAndAtIndex:index completion:^(SCNode *prev, SCNode *next) {
        
        
        // 將新節(jié)點插入鏈表中天梧,將其與前后節(jié)點鏈接起來
        SCNode *newNode = [[SCNode alloc] initWithValue:value];
        newNode.previous = prev;
        newNode.next = next;
        prev.next = newNode;
        next.previous = newNode;
        
        
        // 當(dāng)插入的位置是 0盔性,那么就需要更新 head 了
        // 注:如果有 tail 并且插入的位置是最后的話,也需要更新 tail
        if (prev == nil) {
            self.head = prev;
        }
        
    }];
    
}

#pragma mark - 移除節(jié)點
- (SCNode *)removeNode:(SCNode *)node {
    
    // head --> A --> B --> C --> D --> E
    SCNode *prev = node.previous;
    SCNode *next = node.next;
    
    if (node == self.head) { // 要移除的是 head
        
        self.head = next;
        next.previous = nil;
        
    } else { // 要移除的不是 head呢岗,而是 A冕香,B.....
    
        prev.next = next;
        next.previous = prev;
    }
    
    
    // 切斷 node 的前后連接
    node.previous = nil;
    node.next = nil;
    
    return node.value;
}

- (SCNode *)removeNodeAtIndex:(NSUInteger)index {
    
    SCNode *node = [self nodeAtIndex:index];
    
    NSAssert(node != nil, nil);
    
    return [self removeNode:node];
}

- (SCNode *)removeLastNode {
    NSAssert(self.isEmpty == NO, nil);
    
    return [self removeNode:self.last];
}


// 移除所有的節(jié)點
// 注:如果有 tail 的話,也要把 tail 置為 nil
- (void)removeAllNodes {
    self.head = nil;
}

#pragma mark - 翻轉(zhuǎn)鏈表

       /*
        
      pre +--------+  pre +--------+  pre +--------+  pre +--------+
 nil <----|        |<-----|        |<-----|        |<-----|        |
          | node 0 |      | node 1 |      | node 2 |      | node 3 |
head ---->|        |----->|        |----->|        |----->|        |-----> nil
          +--------+ next +--------+ next +--------+ next +--------+ next 
          
         
     
     next +--------+  pre +--------+  pre +--------+  pre +--------+
nil <-----|        |<-----|        |<-----|        |<-----|        |
          | node 0 |      | node 1 |      | node 2 |      | node 3 |
          |        |----->|        |----->|        |----->|        |-----> nil
          +--------+  pre +--------+ next +--------+ next +--------+ next
                              ∧
                              |
                             head

     next +--------+ next +--------+  pre +--------+  pre +--------+
nil <-----|        |<-----|        |<-----|        |<-----|        |
          | node 0 |      | node 1 |      | node 2 |      | node 3 |
          |        |----->|        |----->|        |----->|        |
          +--------+  pre +--------+ pre  +--------+ next +--------+
                                               ∧
                                               |
                                              head

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

        */


// 反轉(zhuǎn)鏈表
// 一步一步往后走后豫,依次將每個節(jié)點的 prev 指針和 next 指針反轉(zhuǎn)過來悉尾,同時還要把 head 和 tail(如果有的話)調(diào)換一下
- (void)reverse {
    
    
    
    SCNode *currentNode = self.head;
    
    while (currentNode != nil) {
        // 獲取當(dāng)前的這一步的節(jié)點
        SCNode *nodeToSwap = currentNode;
        
        // 往前走一步
        currentNode = nodeToSwap.next;
        
        // 翻轉(zhuǎn)當(dāng)前的這一步的節(jié)點
        SCNode *previous = nodeToSwap.previous;
        SCNode *next = nodeToSwap.next;
        nodeToSwap.next = previous;
        nodeToSwap.previous = next;
        
        // 設(shè)置 head
        self.head = currentNode;
    }
}

@end


3. C(TODO)

四、復(fù)雜度

1. 訪問鏈表的單個元素的復(fù)雜度為 O(n)

如果我們想要訪問鏈表中的第 3 個元素挫酿,并不能想數(shù)組那樣直接通過 list[2] 就能訪問到构眯。因為我們沒有直接指向鏈表中每個元素的引用,我們只能從 head 指針指向的第一個元素開始查找早龟,然后通過每個節(jié)點 next 指針惫霸,一個節(jié)點一個節(jié)點往后查找,直到指定位置的節(jié)點葱弟。

所以壹店,查找元素并不是鏈表的特長。

2. 更快捷的插入和刪除操作

從前面的『為什么使用鏈表』部分芝加,我們就已經(jīng)了解到茫打,鏈表相比數(shù)組的優(yōu)勢就在于更高效的插入和刪除。

如果你已經(jīng)有了一個指向鏈表中某節(jié)點的指針/引用,那么在該位置插入新節(jié)點和刪除該節(jié)點的復(fù)雜度都為 O(1)老赤。

所以,我們在操作鏈表時制市,應(yīng)該盡可能利用已知的節(jié)點抬旺,比如借助 head、tail 插入新節(jié)點祥楣。

五开财、鏈表的應(yīng)用

六、我所理解的鏈表

  • 鏈表是一種有序集合
  • 鏈表是一種最基本误褪、最簡單责鳍、最常見的數(shù)據(jù)結(jié)構(gòu)之一,它可以被用來實現(xiàn)其它的一些抽象數(shù)據(jù)結(jié)構(gòu)(ADT)兽间,比如表历葛、棧、隊列
  • 鏈表中的元素就是節(jié)點嘀略,節(jié)點包含存儲內(nèi)容的 value 變量和 next 指針(雙向鏈表的節(jié)點還包含previous 指針)
  • 鏈表除了節(jié)點還有指向第 1 個元素的 head 指針和指向最后一個元素的 tail 指針
  • 鏈表的優(yōu)勢在于插入刪除操作的高效
  • 鏈表的查找操作復(fù)雜度為 O(n)恤溶,無須查找的插入或者刪除操作的復(fù)雜度為 O(1)

七、面試題

  1. 為什么在使用 for-in 語句遍歷 NSArray 變量時帜羊,同時插入或者刪除元素會導(dǎo)致 crash咒程?

八、參考資料


如果你也喜歡交流技術(shù)奶段、喜歡閱讀饥瓷、積極踐行,歡迎關(guān)注我的公眾號:祥龍Shannon寫字的地方忧饭,一起成長扛伍。

qrcode_for_gh_cc686217be41_344.jpg
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市词裤,隨后出現(xiàn)的幾起案子刺洒,更是在濱河造成了極大的恐慌,老刑警劉巖吼砂,帶你破解...
    沈念sama閱讀 218,755評論 6 507
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件逆航,死亡現(xiàn)場離奇詭異,居然都是意外死亡渔肩,警方通過查閱死者的電腦和手機因俐,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,305評論 3 395
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人抹剩,你說我怎么就攤上這事撑帖。” “怎么了澳眷?”我有些...
    開封第一講書人閱讀 165,138評論 0 355
  • 文/不壞的土叔 我叫張陵胡嘿,是天一觀的道長。 經(jīng)常有香客問我钳踊,道長衷敌,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,791評論 1 295
  • 正文 為了忘掉前任拓瞪,我火速辦了婚禮缴罗,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘祭埂。我一直安慰自己面氓,他們只是感情好,可當(dāng)我...
    茶點故事閱讀 67,794評論 6 392
  • 文/花漫 我一把揭開白布沟堡。 她就那樣靜靜地躺著侧但,像睡著了一般。 火紅的嫁衣襯著肌膚如雪航罗。 梳的紋絲不亂的頭發(fā)上禀横,一...
    開封第一講書人閱讀 51,631評論 1 305
  • 那天,我揣著相機與錄音粥血,去河邊找鬼柏锄。 笑死,一個胖子當(dāng)著我的面吹牛复亏,可吹牛的內(nèi)容都是我干的趾娃。 我是一名探鬼主播,決...
    沈念sama閱讀 40,362評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼缔御,長吁一口氣:“原來是場噩夢啊……” “哼抬闷!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起耕突,我...
    開封第一講書人閱讀 39,264評論 0 276
  • 序言:老撾萬榮一對情侶失蹤笤成,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后眷茁,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體炕泳,經(jīng)...
    沈念sama閱讀 45,724評論 1 315
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,900評論 3 336
  • 正文 我和宋清朗相戀三年上祈,在試婚紗的時候發(fā)現(xiàn)自己被綠了培遵。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片浙芙。...
    茶點故事閱讀 40,040評論 1 350
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖籽腕,靈堂內(nèi)的尸體忽然破棺而出嗡呼,到底是詐尸還是另有隱情,我是刑警寧澤节仿,帶...
    沈念sama閱讀 35,742評論 5 346
  • 正文 年R本政府宣布晤锥,位于F島的核電站,受9級特大地震影響廊宪,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜女轿,卻給世界環(huán)境...
    茶點故事閱讀 41,364評論 3 330
  • 文/蒙蒙 一箭启、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧蛉迹,春花似錦傅寡、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,944評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至珍策,卻和暖如春托启,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背攘宙。 一陣腳步聲響...
    開封第一講書人閱讀 33,060評論 1 270
  • 我被黑心中介騙來泰國打工屯耸, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人蹭劈。 一個月前我還...
    沈念sama閱讀 48,247評論 3 371
  • 正文 我出身青樓疗绣,卻偏偏與公主長得像,于是被迫代替她去往敵國和親铺韧。 傳聞我的和親對象是個殘疾皇子多矮,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 44,979評論 2 355

推薦閱讀更多精彩內(nèi)容