數據結構3-單向鏈表

接着上一篇經過靜態數組的擴容實現動態數組建立動態數組以後,這裏再來建立經過單向鏈表實現一個動態數組。首先先來分析下動態數組的缺點,纔可以瞭解到鏈表的意義。 首先回顧下以前動態數組添加和刪除的過程:node

添加過程

動態數組添加元素的時候,最壞的狀況是插入元素到數組的頭部,則須要依次向後挪動因此元素,進行的操做數取決於當前元素的數量,複雜度爲O(n),最好的狀況是追加到數組的尾部,不須要挪動元素,複雜度爲O(1)。平均複雜度爲O(n)。擴容因爲不是每次添加都須要的操做,只有在溢出的時候才須要擴容,擴容的時候複雜度爲O(n),不擴容的時候爲O(1),均攤複雜度依然爲O(1)。git

刪除過程

刪除和添加基本相同,最好的狀況刪除隊尾複雜度爲O(1),最差的狀況是刪除隊頭,複雜度爲O(n),平均複雜度爲O(n)。github

而在根據index取值的時候,因爲本質是經過index直接從數組中取值,數組中取值的複雜度爲O(1),因此取元素的複雜度爲O(1)。數組

注:從數組中值並不須要遍歷,而是經過地址計算直接取值,複雜度爲O(1)。bash

那麼鏈表會不會比靜態數組更快呢,下面咱們來實現一個自定義單向鏈表,而後比較一下就知道了。首先提示一下,單向鏈表可能沒想象的那麼快哦。數據結構

首先看一下數組和鏈表在內存中的不一樣post

鏈表在內存中並非連續的,鏈表中每個存儲的單元稱爲一個節點,在單向鏈表中,每個節點中存儲兩個值,一個是存放的指向存儲元素的指針,另外一個是指向下一個節點的指針,這樣鏈表就可以經過每個節點指向下一個節點的指針找到當前節點的下一個節點,只要獲得鏈表的第一個節點,就可以訪問到鏈表的因此節點,同時也就可以拿到鏈表的所有元素。

單向鏈表的節點結構

這樣一看單向鏈表是否是很是的簡單,在Objective-C語言中,就至關於每個節點對象有兩個成員變量,一個是存值的,另外一個是存下一個節點對象的,下面就聲明一個單向鏈表的節點對象:測試

#import <Foundation/Foundation.h>

NS_ASSUME_NONNULL_BEGIN

@interface JKRSingleLinkedListNode : NSObject

@property (nonatomic, strong, nullable) id object;
@property (nonatomic, strong, nullable) JKRSingleLinkedListNode *next;

- (instancetype)init __unavailable;
+ (instancetype)new __unavailable;
- (instancetype)initWithObject:(nullable id)object next:(nullable JKRSingleLinkedListNode *)next;

@end

NS_ASSUME_NONNULL_END
複製代碼

單向鏈表的結構

既然只須要拿到單向鏈表的頭節點,就可以訪問到所有節點,那麼單向鏈表中須要存儲成員變量只須要兩個,一個是 _size,存儲這鏈表的長度。另外一個是_first,保存鏈表的第一個節點。ui

注:_size存儲在父類中,全部的接口聲明也在父類中,父類的定義參見經過靜態數組的擴容實現動態數組,所有源代碼在文章結尾。atom

#import "JKRBaseList.h"
#import "JKRSingleLinkedListNode.h"

NS_ASSUME_NONNULL_BEGIN

@interface JKRSingleLinkedList : JKRBaseList {
    // NSUInteger _size; 
    JKRSingleLinkedListNode *_first;
}

@end

NS_ASSUME_NONNULL_END
複製代碼

下面是完整的單向鏈表的內存結構圖,能夠更加直接的瞭解單向鏈表的結構:

經過index查找節點

上面的鏈表結構圖能夠看到,鏈表對象保存這鏈表的長度和鏈表的頭節點,若是要經過index得到具體的某一個節點,須要從頭節點開始逐一經過next指針日後查找,直到找到第index個節點。

時間複雜度取決於index,取index爲0的節點只須要訪問頭節點,只須要1次訪問。訪問尾節點須要從頭節點一直訪問到尾節點,訪問次數取決於節點數量。綜上平均時間複雜度爲O(n)。

- (JKRSingleLinkedListNode *)nodeWithIndex:(NSInteger)index {
    [self rangeCheckForExceptAdd:index];
    JKRSingleLinkedListNode *node = _first;
    for (NSInteger i = 0; i < index; i++) {
        node = node.next;
    }
    return node;
}
複製代碼

經過index取值

上面已經實現了拿到index位置的節點,這裏只須要調用方法獲取節點,而後返回節點存儲的值就能夠了。 時間複雜度同查找節點:O(n)

- (id)objectAtIndex:(NSUInteger)index {
    return [self nodeWithIndex:index].object;
}
複製代碼

添加節點

在鏈表中間插入節點

在鏈表中間插入節點,以下圖,鏈表中存在三個節點,此時咱們須要在鏈表index爲1的位置插入一個節點:

此時須要作的就是讓index爲0的節點的next指向新節點,並讓新節點的next指向原來index爲1的節點:

這樣插入就成功的將一個節點插入到鏈表的兩個節點中:

在鏈表頭部插入節點

在鏈表頭部插入節點以下圖:

這時須要將新節點的next指向原來鏈表的_first,並將鏈表的_first指向新節點:

在鏈表頭部成功插入節點後鏈表的結構:

鏈表添加的節點的代碼實現

綜上步驟,鏈表添加節點兩種狀況的代碼實現以下:

- (void)insertObject:(id)anObject atIndex:(NSUInteger)index {
    [self rangeCheckForAdd:index];
    
    if (index == 0) {
        JKRSingleLinkedListNode *node = [[JKRSingleLinkedListNode alloc] initWithObject:anObject next:_first];
        _first = node;
    } else {
        JKRSingleLinkedListNode *prev = [self nodeWithIndex:index - 1];
        JKRSingleLinkedListNode *node = [[JKRSingleLinkedListNode alloc] initWithObject:anObject next:prev.next];
        prev.next = node;
    }
    
    _size++;
}
複製代碼

添加時index的越界檢查有動態數組的父類統一實現。

由於添加節點時,雖然插入只須要1次操做,可是涉及到查找index位置的節點,這個查找的複雜度爲O(n),因此添加節點的複雜度爲O(n)。

刪除節點

在鏈表中間刪除節點

假設刪除鏈表index爲1的節點,以下圖:

只須要將被刪除節點的前一個節點的next指針改變指向,指向被刪除節點的下一個節點,那麼被刪除節點因爲沒有引用,就會自動被回收:

刪除後鏈表的結構:

刪除鏈表頭節點

刪除鏈表的頭節點以下圖:

只須要將單向鏈表的_first指針指向原來頭節點下一個節點便可:

刪除後鏈表的結構:

鏈表刪除節點的代碼實現

綜上兩種狀況,鏈表刪除的代碼以下:

- (void)removeObjectAtIndex:(NSUInteger)index {
    [self rangeCheckForExceptAdd:index];
    
    JKRSingleLinkedListNode *node = _first;
    if (index == 0) {
        _first = _first.next;
    } else {
        JKRSingleLinkedListNode *prev = [self nodeWithIndex:index - 1];
        node = prev.next;
        prev.next = node.next;
    }
    _size--;
}
複製代碼

同添加節點的操做,雖然刪除節點只須要1次操做,可是涉及到查找index位置的節點,這個查找的複雜度爲O(n),因此刪除節點的複雜度也是爲O(n)。

至於其餘功能都是基於上面幾個接口調用實現的,好比尾部追加、刪除頭節點等,就不一一列舉了,最後源碼中都有。

和動態數組對比時間複雜度

數據結構 動態數組 單向鏈表
詳細分類 最好 最差 平均 最好 最差 平均
插入任意位置元素 O(1) O(n) O(n) O(1) O(n) O(n)
刪除任意位置元素 O(1) O(n) O(n) O(1) O(n) O(n)
替換任意位置元素 O(1) O(1) O(1) O(1) O(n) O(n)
查找任意位置元素 O(1) O(1) O(1) O(1) O(n) O(n)
添加元素到尾部 O(1) O(n) O(1) O(n) O(n) O(n)
刪除尾部元素 O(1) O(1) O(1) O(n) O(n) O(n)
添加元素到頭部 O(n) O(n) O(n) O(1) O(1) O(1)
刪除頭部元素 O(n) O(n) O(n) O(1) O(1) O(1)

上面是總結出來的時間複雜度對比:

  • 插入任意位置元素:動態數組須要挪動元素,且越靠近頭部挪動次數越多。單向鏈表須要找到對於的節點進行指針操做,且越靠近尾部查找次數越多。
  • 刪除任意位置元素:動態數組須要挪動元素,且越靠近頭部挪動次數越多。單向鏈表須要找到對於的節點進行指針操做,且越靠近尾部查找次數越多。
  • 替換任意位置元素:動態數組直接經過index取對於位置的值,操做數穩定爲1。單向鏈表須要找到對於的節點進行指針操做,且越靠近尾部查找次數越多。
  • 查找任意位置元素:動態數組直接經過index取對於位置的值,操做數穩定爲1。單向鏈表須要找到對於的節點進行指針操做,且越靠近尾部查找次數越多。
  • 添加元素到尾部:動態數組尾部添加直接在數組尾部對應的index添加值便可,雖然可能有擴容操做,可是均攤下來依舊是O(1)。單向鏈表須要從頭節點一直找到尾節點,爲O(n)。
  • 刪除尾部元素:動態數組尾部添加直接在數組尾部對應的index添加值便可,固定1次操做。單向鏈表須要從頭節點一直找到尾節點,爲O(n)。
  • 添加元素到頭部:動態數組須要最大的挪動元素次數,爲O(n)。單向鏈表只須要1次操做。
  • 刪除頭部元素:動態數組須要最大的挪動元素次數,爲O(n)。單向鏈表只須要1次操做。

由上面的分析能夠直到,單向鏈表並非全部狀況下時間複雜度都優於動態數組的,當須要頻發的刪除和添加到數組頭部時,單向鏈表優於動態數組。當須要頻發的刪除和添加到數組尾部時,動態數組優於單向鏈表。

下面測試一下:

進行10000次頭部的插入和刪除操做,動態數組和單向鏈表對比:

進行10000次尾部的插入和刪除操做,動態數組和單向鏈表對比:

因此並非說單向鏈表就必定時間複雜度上優於動態數組,依然要區分在不一樣的應用場景。若是須要頻繁的對數組頭部進行插入和刪除操做,單向鏈表是大大優於動態數組的。若是是須要頻繁的在數組尾部進行插入和刪除操做,動態數組又是大大優於單向鏈表的。

接下來

單向循環鏈表是基於單向鏈表的結構作了功能的擴展,有了單向鏈表的基礎,下面就能夠更容易理解單向循環鏈表的實現了。

源碼

點擊查看源碼

相關文章
相關標籤/搜索