接着上一篇經過靜態數組的擴容實現動態數組建立動態數組以後,這裏再來建立經過單向鏈表實現一個動態數組。首先先來分析下動態數組的缺點,纔可以瞭解到鏈表的意義。 首先回顧下以前動態數組添加和刪除的過程: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得到具體的某一個節點,須要從頭節點開始逐一經過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位置的節點,這裏只須要調用方法獲取節點,而後返回節點存儲的值就能夠了。 時間複雜度同查找節點: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) |
上面是總結出來的時間複雜度對比:
由上面的分析能夠直到,單向鏈表並非全部狀況下時間複雜度都優於動態數組的,當須要頻發的刪除和添加到數組頭部時,單向鏈表優於動態數組。當須要頻發的刪除和添加到數組尾部時,動態數組優於單向鏈表。
下面測試一下:
進行10000次頭部的插入和刪除操做,動態數組和單向鏈表對比:
進行10000次尾部的插入和刪除操做,動態數組和單向鏈表對比:
因此並非說單向鏈表就必定時間複雜度上優於動態數組,依然要區分在不一樣的應用場景。若是須要頻繁的對數組頭部進行插入和刪除操做,單向鏈表是大大優於動態數組的。若是是須要頻繁的在數組尾部進行插入和刪除操做,動態數組又是大大優於單向鏈表的。
單向循環鏈表是基於單向鏈表的結構作了功能的擴展,有了單向鏈表的基礎,下面就能夠更容易理解單向循環鏈表的實現了。