資料結構系列:Objective-C實現雙鏈表
接續上一篇《資料結構系列:Objective-C實現單鏈表》
雙向連結串列

摘自《雙向連結串列-維基百科,自由的百科全書》
雙向連結串列,又稱為 雙鏈表 ,是 ofollow,noindex">連結串列 的一種,它的每個資料結點中都有兩個 指標 ,分別指向直接後繼和直接前驅。所以,從雙向連結串列中的任意一個結點開始,都可以很方便地訪問它的前驅結點和後繼結點。
摘自《連結串列-維基百科,自由的百科全書》
一種更復雜的連結串列是“雙向連結串列”或“雙面連結串列”。每個節點有兩個連線:一個指向前一個節點,(當此“連線”為第一個“連線”時,指向空值或者空列表);而另一個指向下一個節點,(當此“連線”為最後一個“連線”時,指向空值或者空列表)
在一些低階語言中, XOR-linking 提供一種在雙向連結串列中通過用一個詞來表示兩個連結(前後),我們通常不提倡這種做法。
雙向連結串列也叫 雙鏈表 。 雙向連結串列 中不僅有指向後一個節點的指標,還有指向前一個節點的指標。這樣可以從任何一個節點訪問前一個節點,當然也可以訪問後一個節點,以至整個連結串列。一般是在需要大批量的另外儲存資料在連結串列中的位置的時候用。雙向連結串列也可以配合下面的其他連結串列的擴充套件使用。
由於另外儲存了指向連結串列內容的指標,並且可能會修改相鄰的節點,有的時候第一個節點可能會被刪除或者在之前新增一個新的節點。這時候就要修改指向首個節點的指標。有一種方便的可以消除這種特殊情況的方法是在最後一個節點之後、第一個節點之前儲存一個永遠不會被刪除或者移動的虛擬節點,形成一個下面說的迴圈連結串列。這個虛擬節點之後的節點就是真正的第一個節點。這種情況通常可以用這個虛擬節點直接表示這個連結串列,對於把連結串列單獨的存在 陣列 裡的情況,也可以直接用這個陣列表示連結串列並用第0個或者第-1個(如果編譯器支援)節點固定的表示這個虛擬節點。
@implementation DoubleLinkList /** 初始化一個雙向連結串列 @param node 連結串列的頭節點 @return 返回一個已初始化的雙向連結串列 */ - (instancetype)initWithNode:(DoubleLinkListNode *)node { self = [super init]; if (self) { self.headNode = node; } return self; } /** 判斷連結串列是否為空 @return 為空返回YES,反之為NO */ - (BOOL)isEmpty { return self.headNode == nil; } /** 獲取連結串列擁有的總節點數 @return 總節點數 */ - (NSInteger)length { DoubleLinkListNode *cur = self.headNode; NSInteger count = 0; while (cur) { count++; cur = cur.nextNode; } return count; } /** 遍歷連結串列 */ - (void)travel { // 空連結串列的情況 if ([self isEmpty]) return; DoubleLinkListNode *cur = self.headNode; while (cur) { NSLog(@"%ld",cur.element); cur = cur.nextNode; } } // ==============以上與單向連結串列完全一致============================== /** 頭插法:在連結串列的頭部插入節點 @param item 插入的元素 */ - (void)insertNodeAtHeadWithItem:(NSInteger)item { DoubleLinkListNode *node = [[DoubleLinkListNode alloc] initWithItem: item]; node.nextNode = self.headNode; // =====只加了這一句===== self.headNode.prevNode = node; self.headNode = node; } /** 尾插法:在連結串列的尾部插入節點 @param item 插入的元素 */ - (void)appendNodeWithItem:(NSInteger)item { DoubleLinkListNode *node = [[DoubleLinkListNode alloc] initWithItem: item]; if ([self isEmpty]) { self.headNode = node; } else { DoubleLinkListNode *cur = self.headNode; while (cur.nextNode) { cur = cur.nextNode; } cur.nextNode = node; // =====只加了這一句===== node.prevNode = cur; } } /** 指定位置插入節點 @param item 插入的元素 @param index 位置的索引 */ - (void)insertNodeWithItem:(NSInteger)item atIndex:(NSInteger)index { if (index <= 0) { [self insertNodeAtHeadWithItem: item]; } else if (index > ([self length] - 1)) { [self appendNodeWithItem: item]; } else { DoubleLinkListNode *cur = self.headNode; for (int i = 0; i < index; i++) // i < index -1 改為 i < index { cur = cur.nextNode; } DoubleLinkListNode *node = [[DoubleLinkListNode alloc] initWithItem: item]; node.nextNode = cur; node.prevNode = cur.prevNode; cur.prevNode.nextNode = node; cur.prevNode = node; } } /** 刪除節點 @param item 刪除的元素 */ - (void)removeNodeWithItem:(NSInteger)item { DoubleLinkListNode *cur = self.headNode; while (cur != nil) { if (cur.element == item) { // 先判斷此節點是不是頭節點 // 頭節點 if (cur == self.headNode) { // 如果連結串列只有一個節點那麼cur.nextNode == nil self.headNode = cur.nextNode; // cur.nextNode不為空即表示連結串列不只有一個節點 if (cur.nextNode) { // 刪除頭節點後下一個節點的prev節點仍然儲存著原頭節點,所以要置空 cur.nextNode.prevNode = nil; } } else { // 刪除普通節點 // 一、讓cur前一個節點的next連上cur後一個節點 cur.prevNode.nextNode = cur.nextNode; // 二、如果cur的後一個節點存在,讓cur後一個節點的prev連上cur前一個節點 if (cur.nextNode) { cur.nextNode.prevNode = cur.prevNode; } break; } } else { cur = cur.nextNode; } } } /** 查詢某個節點是否存在 @param item 查詢的元素 @return 存在返回YES,反之為NO */ - (BOOL)searchNodeWithItem:(NSInteger)item { DoubleLinkListNode *cur = self.headNode; while (cur != nil) { if (cur.element == item) { return YES; } else { cur = cur.nextNode; } } return NO; } @end
實際使用案例
// ViewDidLoad - (void)viewDidLoad { [super viewDidLoad]; DoubleLinkList *dll = [[DoubleLinkList alloc] initWithNode: nil]; //NSLog(@"%d", [dll isEmpty]); //NSLog(@"%ld", [dll length]); [dll appendNodeWithItem: 1]; //NSLog(@"%d", [dll isEmpty]); //NSLog(@"%ld", [dll length]); [dll appendNodeWithItem: 2]; [dll insertNodeAtHeadWithItem: 8]; [dll appendNodeWithItem: 3]; [dll appendNodeWithItem: 4]; [dll appendNodeWithItem: 5]; [dll appendNodeWithItem: 6]; //[dll travel]; // 8 1 2 3 4 5 6 [dll insertNodeWithItem: 9 atIndex: -1]; //[dll travel]; // 9 8 1 2 3 4 5 6 [dll insertNodeWithItem: 100 atIndex: 3]; //[dll travel]; // 9 8 1 100 2 3 4 5 6 [dll insertNodeWithItem: 200 atIndex: 10]; //[dll travel]; // 9 8 1 100 2 3 4 5 6 200 [dll removeNodeWithItem: 100]; [dll travel]; // 9 8 1 2 3 4 5 6 200 }
寡人的思路
-
雙向連結串列的節點構成與單鏈表有一點區別:多了一個
prevNode
前驅(前一個節點)/** 節點元素值 */ @property (nonatomic, assign) NSInteger element; /** 下一個節點 */ @property (nonatomic, strong) DoubleLinkListNode *nextNode; /** 前一個節點 */ @property (nonatomic, strong) DoubleLinkListNode *prevNode;
-
初始化一個雙向連結串列:與單向連結串列初始化思路完全一致
-
判斷連結串列是否為空:與單向連結串列思路完全一致
-
獲取連結串列擁有的總節點數:與單向連結串列思路完全一致
-
遍歷連結串列:與單向連結串列思路完全一致
-
頭插法--在連結串列的頭部插入節點:
只加了一句程式碼:
self.headNode.prevNode = node;
,這句程式碼旨在將新頭節點的前驅prevNode
與尾節點相連。因為雙向連結串列的節點多了前驅,所以在頭尾節點處理的時候要多處理一下。 -
尾插法--在連結串列的尾部插入節點:
同樣只加了一句程式碼:
node.prevNode = cur;
,同樣的,這句程式碼是在處理新尾節點的前驅prevNode
,讓其與原尾節點相連。 -
指定位置插入節點:
與單向連結串列有兩點不同:
- 迴圈次數不同 :單向連結串列的迴圈次數為
index - 1
次,雙向連結串列的迴圈次數為index
次。造成不同的原因仍然是因為雙向連結串列節點構成中多出了一個前驅prevNode
。單向連結串列中沒有前驅,只能通過後繼節點nextNode
進行遍歷,所以迴圈次數要少一次,為了讓遊標停在插入位置的前一個節點。雙向連結串列節點本身的構成中就擁有前驅屬性,所以可以乾脆停在插入位置,通過前驅prevNode
和後繼nextNode
來進行遍歷。 - 處理多出來的前驅 :雙向連結串列插入的節點不但要處理與前一個節點和後一個節點的後繼
nextNode
的關係,還要處理與前一個節點和後一個節點的前驅prevNode
的關係。
- 迴圈次數不同 :單向連結串列的迴圈次數為
-
刪除節點:
與單向連結串列仍然有兩點不同:
- 迴圈條件不同 :這與 指定位置插入節點 的 迴圈次數不同 原因一樣。
- 處理多出來的前驅 :需要注意兩個位置,一個是刪除頭節點後下一個節點的prev節點仍然儲存著原頭節點,所以需要置空。另外正常情況下,如果cur的後一個節點存在,讓cur後一個節點的prev連上cur前一個節點。
-
查詢某個節點是否存在:與單向連結串列的邏輯基本一致,唯一就是判斷條件不同。
雙向迴圈連結串列

雙向迴圈連結串列的節點構成與雙向連結串列相同,包括節點元素 element
、前驅節點 prevNode
、後繼節點 nextNode
。與雙向連結串列不同的是雙向迴圈連結串列的尾節點的後繼節點 nextNode
要指向頭節點,頭節點的前驅節點 prevNode
要指向尾節點,以構成迴圈連結串列。
在下面的原始碼中,有大量的語句與單向迴圈連結串列或雙向連結串列相同,只有少量的邏輯有差別。與雙向連結串列相比差別主要有以下幾點:
-
雙向迴圈連結串列要處理尾節點的後繼節點與頭節點的前驅節點。
-
雙向迴圈連結串列在尋找尾節點或者說從頭節點遍歷到尾節點時的判斷條件不同,這是因為雙向連結串列尾節點的後繼節點指向
nil
,頭節點的前驅節點也指向nil
,它是不考慮“首尾相連”的。而雙向迴圈連結串列需要考慮所謂的“首尾相連”,即頭節點的前驅節點要指向尾節點,尾節點的後繼節點要指向頭節點。
/** 初始化一個雙向迴圈連結串列 @param node 連結串列的頭節點 @return 返回一個已初始化的雙向迴圈連結串列 */ - (instancetype)initWithNode:(DoubleCycleLinkListNode *)node { self = [super init]; if (self) { self.headNode = node; // 判斷node不為空的情況,迴圈指向自己 if (node) { node.nextNode = node; node.prevNode = node; } } return self; } /** 判斷連結串列是否為空 @return 為空返回YES,反之為NO */ - (BOOL)isEmpty { return self.headNode == nil; } /** 獲取連結串列擁有的總節點數 @return 總節點數 */ - (NSInteger)length { if ([self isEmpty]) return 0; DoubleCycleLinkListNode *cur = self.headNode; NSInteger count = 1; while (cur.nextNode != self.headNode) { count++; cur = cur.nextNode; } return count; } /** 遍歷連結串列 */ - (void)travel { // 空連結串列的情況 if ([self isEmpty]) return; DoubleCycleLinkListNode *cur = self.headNode; while (cur.nextNode != self.headNode) { NSLog(@"%ld", cur.element); cur = cur.nextNode; } // 退出迴圈,cur指向尾節點,但尾節點的元素未列印 NSLog(@"%ld", cur.element); } /** 頭插法:在連結串列的頭部插入節點 @param item 插入的元素 */ - (void)insertNodeAtHeadWithItem:(NSInteger)item { DoubleCycleLinkListNode *node = [[DoubleCycleLinkListNode alloc] initWithItem:item]; if ([self isEmpty]) { self.headNode = node; node.nextNode = node; node.prevNode = node; } else { DoubleCycleLinkListNode *cur = self.headNode; while (cur.nextNode != self.headNode) { cur = cur.nextNode; } /** * 退出迴圈後,cur指向尾節點。 * 因為是頭插法,所以node是新頭節點 */ node.nextNode = self.headNode;// node向後是原head self.headNode.prevNode = node;// 原head向前是node node.prevNode = cur;// node向前,迴圈到尾節點 cur.nextNode = node;// 尾節點向後,迴圈到現頭節點node self.headNode = node; } } /** 尾插法:在連結串列的尾部插入節點 @param item 插入的元素 */ - (void)appendNodeWithItem:(NSInteger)item { DoubleCycleLinkListNode *node = [[DoubleCycleLinkListNode alloc] initWithItem:item]; if ([self isEmpty]) { self.headNode = node; node.nextNode = node; node.prevNode = node; } else { DoubleCycleLinkListNode *cur = self.headNode; while (cur.nextNode != self.headNode) { cur = cur.nextNode; } /** * 退出迴圈後,cur指向尾節點。 * 因為是尾插法,所以node是新尾節點 */ cur.nextNode = node;// 原尾節點cur向後是現尾節點node node.prevNode = cur;// 現尾節點node向前是原尾節點cur node.nextNode = self.headNode;// 現尾節點node向後,迴圈到頭節點 self.headNode.prevNode = node;// 頭節點向前,迴圈到現尾節點node } } /** 指定位置插入節點 @param item 插入的元素 @param index 位置的索引 */ - (void)insertNodeWithItem:(NSInteger)item atIndex:(NSInteger)index { if (index <= 0) { [self insertNodeAtHeadWithItem: item]; } else if (index > ([self length] - 1)) { [self appendNodeWithItem: item]; } else { DoubleCycleLinkListNode *cur = self.headNode; NSInteger count = 0; while (count < index) { count++; cur = cur.nextNode; } /* * 當迴圈退出後,cur指向index位置 * cur是原index位置節點 * node是現index位置節點 */ DoubleCycleLinkListNode *node = [[DoubleCycleLinkListNode alloc] initWithItem:item]; node.nextNode = cur;// node向後是cur node.prevNode = cur.prevNode;// 未修改cur的prevNode前,node向前是cur的prevNode cur.prevNode.nextNode = node;// 未修改cur的prevNode前,cur的prevNode向後是node cur.prevNode = node;// 修改了cur的prevNode後,cur向前是node } } /** 刪除節點 @param item 刪除的元素 */ - (void)removeNodeWithItem:(NSInteger)item { if ([self isEmpty]) return; DoubleCycleLinkListNode *cur = self.headNode; while (cur.nextNode != self.headNode) { if (cur.element == item) { // 1.刪除頭節點的情況 if (cur == self.headNode) { // 先找到尾節點 DoubleCycleLinkListNode *tail = self.headNode; while (tail.nextNode != self.headNode) { tail = tail.nextNode; } // 迴圈結束後,tail指向當前尾節點 self.headNode = cur.nextNode;// cur就是頭節點。讓頭節點指向其下一節點,即刪除頭節點 self.headNode.prevNode = tail;// 新頭節點向前,迴圈到尾節點rear tail.nextNode = self.headNode;// 尾節點rear向後,迴圈到新頭節點 } // 2.刪除非頭節點的情況 else { cur.prevNode.nextNode = cur.nextNode;// 看圖解析 cur.nextNode.prevNode = cur.prevNode;// 看圖解析 } return; } else { cur = cur.nextNode; } // 退出迴圈,cur指向尾節點 if (cur.element == item) { if (cur == self.headNode) { // 如果cur指向頭節點,證明連結串列只有一個節點 self.headNode = nil; } else { cur.prevNode.nextNode = cur.nextNode;// 看圖解析 cur.nextNode.prevNode = cur.prevNode;// 看圖解析 } } } } /** 查詢某個節點是否存在 @param item 查詢的元素 @return 存在返回YES,反之為NO */ - (BOOL)searchNodeWithItem:(NSInteger)item { if ([self isEmpty]) return NO; DoubleCycleLinkListNode *cur = self.headNode; while (cur.nextNode != self.headNode) { if (cur.element == item) { return YES; } else { cur = cur.nextNode; } } // 退出迴圈,cur指向尾節點 if (cur.element == item) { return YES; } return NO; } @end
實際使用案例
// ViewController - (void)viewDidLoad { [super viewDidLoad]; DoubleCycleLinkList *dcll = [[DoubleCycleLinkList alloc] init]; //NSLog(@"%d", [dcll isEmpty]); // 1 //NSLog(@"%ld", [dcll length]); // 0 [dcll appendNodeWithItem: 1]; //NSLog(@"%d", [dcll isEmpty]); // 0 //NSLog(@"%ld", [dcll length]); // 1 [dcll appendNodeWithItem: 2]; [dcll insertNodeAtHeadWithItem:8]; [dcll appendNodeWithItem: 3]; [dcll appendNodeWithItem: 4]; //[dcll travel]; // 8 1 2 3 4 [dcll insertNodeWithItem:9 atIndex:-1]; //[dcll travel]; // 9 8 1 2 3 4 [dcll insertNodeWithItem:100 atIndex:3]; //[dcll travel]; // 9 8 1 100 2 3 4 [dcll insertNodeWithItem:200 atIndex:10]; //[dcll travel]; // 9 8 1 100 2 3 4 200 [dcll removeNodeWithItem:9]; //[dcll travel]; // 8 1 100 2 3 4 200 [dcll removeNodeWithItem:100]; //[dcll travel]; // 8 1 2 3 4 200 [dcll removeNodeWithItem:200]; [dcll travel]; // 8 1 2 3 4 NSLog(@"result:%d", [dcll searchNodeWithItem:100]); }