數據結構6-雙向循環鏈表

以前已經實現了單向循環鏈表,雙向循環鏈表的原理和單向鏈表很類似:尾節點的next指向鏈表的頭節點。在此基礎上,頭節點的prev指向尾節點,這樣就實現了雙向循環鏈表。一樣,爲了防止循環引用,尾節點指向頭節點要用弱引用。node

雙向循環鏈表的節點

#import <Foundation/Foundation.h>

NS_ASSUME_NONNULL_BEGIN

@interface JKRLinkedListNode : NSObject

@property (nonatomic, strong, nullable) id object;
@property (nonatomic, weak, nullable) JKRLinkedListNode *weakNext;
@property (nonatomic, strong, nullable) JKRLinkedListNode *next;
@property (nonatomic, weak, nullable) JKRLinkedListNode *prev;

- (instancetype)init __unavailable;
+ (instancetype)new __unavailable;

- (instancetype)initWithPrev:(JKRLinkedListNode *)prev object:(nullable id)object next:(nullable JKRLinkedListNode *)next;

@end

NS_ASSUME_NONNULL_END
複製代碼

添加節點

雙向循環鏈表添加節點和雙向鏈表基本同樣,只是多了頭節點的prev和尾節點的next的維護操做。git

添加鏈表的第一個節點

對比雙向鏈表,雙向循環鏈表除了將鏈表的頭節點和尾節點指向新節點以外,還須要將節點的prev、weakNext都指向它本身。github

代碼邏輯以下:數組

if (_size == 0 && index == 0) {
    JKRLinkedListNode *node = [[JKRLinkedListNode alloc] initWithPrev:nil object:anObject next:nil];
    _last = node;
    _first = _last;
    _first.prev = _first;
    _first.next = nil;
    _first.weakNext = _first;
}
複製代碼

鏈表尾部追加一個節點

新添加的節點替換原來的尾節點稱爲新的尾節點:bash

須要的操做以下圖:數據結構

  • 新添加節點的prev指向鏈表原來的尾節點。
  • 鏈表尾節點指針last指向新添加的節點。
  • 鏈表原來尾節點的next指向如今鏈表的新尾節點(即新添加的節點)。
  • 鏈表頭節點的prev指向新添加節點。
  • 新添加的尾節點的weakNext指向鏈表的頭節點。
if (_size == index && _size != 0) {
    JKRLinkedListNode *oldLast = _last;
    JKRLinkedListNode *node = [[JKRLinkedListNode alloc] initWithPrev:nil object:anObject next:nil];
    _last = node;
    oldLast.next = _last;
    oldLast.weakNext = nil;
    _first.prev = _last;
    _last.next = nil;
    _last.weakNext = _first;
}
複製代碼

添加第一個節點和尾部追加節點代碼整合

if (_size == 0 && index == 0) {
    JKRLinkedListNode *node = [[JKRLinkedListNode alloc] initWithPrev:nil object:anObject next:nil];
    _last = node;
    _first = _last;
    _first.prev = _first;
    _first.next = nil;
    _first.weakNext = _first;
}

if (_size == index && _size != 0) {
    JKRLinkedListNode *oldLast = _last;
    JKRLinkedListNode *node = [[JKRLinkedListNode alloc] initWithPrev:nil object:anObject next:nil];
    _last = node;
    oldLast.next = _last;
    oldLast.weakNext = nil;
    _first.prev = _last;
    _last.next = nil;
    _last.weakNext = _first;
}
複製代碼

上面兩段代碼將相同的判斷邏輯合併,不一樣的判斷邏輯分開:post

if (_size == index) {
    if (_size == 0) {
        JKRLinkedListNode *node = [[JKRLinkedListNode alloc] initWithPrev:nil object:anObject next:nil];
        _last = node;
        _first = _last;
        _first.prev = _first;
        _first.next = nil;
        _first.weakNext = _first;
    } else {
        JKRLinkedListNode *oldLast = _last;
        JKRLinkedListNode *node = [[JKRLinkedListNode alloc] initWithPrev:nil object:anObject next:nil];
        _last = node;
        oldLast.next = _last;
        oldLast.weakNext = nil;
        _first.prev = _last;
        _last.next = nil;
        _last.weakNext = _first;
    }
}
複製代碼

將相同的代碼提出出來:測試

if (_size == index) {
    JKRLinkedListNode *oldLast = _last;
    JKRLinkedListNode *node = [[JKRLinkedListNode alloc] initWithPrev:_last object:anObject next:_first];
    _last = node;
    // _size == 0
    // 還能夠使用 !oldLast 由於空鏈表_last爲空
    if (_size == 0) { // 添加鏈表第一個元素
        _first = _last;
        _first.prev = _first;
        _first.next = nil;
        _first.weakNext = _first;
    } else { // 插入到表尾
        oldLast.next = _last;
        oldLast.weakNext = nil;
        _first.prev = _last;
        _last.next = nil;
        _last.weakNext = _first;
    }
}
複製代碼

插入到鏈表頭部

插入一個新節點到鏈表的頭部以下圖:ui

須要的操做以下圖:atom

  • 新節點prev指向原來頭節點的prev。
  • 新節點的next指向原來的頭節點。
  • 原來頭節點的prev指向新節點。
  • 鏈表的first指針指向新節點。
  • 原來頭節點的prev(即尾節點)的weakNext指向新的頭節點。

節點插入操做完成後的鏈表以下:

代碼邏輯以下:

if (index == _size) { // 插入到表尾 或者 空鏈表添加第一個節點
    // ...
} else {
    if (index == 0) { // 插入到表頭
        JKRLinkedListNode *next = [self nodeWithIndex:index];
        JKRLinkedListNode *prev = next.prev;
        JKRLinkedListNode *node = [[JKRLinkedListNode alloc] initWithPrev:prev object:anObject next:next];
        next.prev = node;
        _first = node;
        prev.next = nil;
        prev.weakNext = node;
    } else { // 插入到兩個節點中間
        
    }
}
複製代碼

插入到鏈表的節點中間

插入一個新節點到鏈表兩個節點中間以下圖:

須要的操做以下圖:

  • 首先獲取插入位置index對應的節點。
  • 新節點prev指向鏈表插入位置原節點的prev。
  • 新節點的next指向鏈表插入位置原節點。
  • 鏈表插入位置原節點的prev指向新節點。
  • 鏈表插入位置原節點的前一個節點的next指向新節點。

節點插入操做完成後的鏈表以下:

代碼邏輯以下:

if (index == _size) { // 插入到表尾 或者 空鏈表添加第一個節點
    // ...
} else {
    if (index == 0) { // 插入到表頭
        JKRLinkedListNode *next = [self nodeWithIndex:index];
        JKRLinkedListNode *prev = next.prev;
        JKRLinkedListNode *node = [[JKRLinkedListNode alloc] initWithPrev:prev object:anObject next:next];
        next.prev = node;
        _first = node;
        prev.next = nil;
        prev.weakNext = node;
    } else { // 插入到兩個節點中間
        JKRLinkedListNode *next = [self nodeWithIndex:index];
        JKRLinkedListNode *prev = next.prev;
        JKRLinkedListNode *node = [[JKRLinkedListNode alloc] initWithPrev:prev object:anObject next:next];
        next.prev = node;
        prev.next = node;
        prev.weakNext = nil;
    }
}
複製代碼

插入到表的非空節點位置的代碼邏輯整合

if (index == 0) { // 插入到表頭
        JKRLinkedListNode *next = [self nodeWithIndex:index];
        JKRLinkedListNode *prev = next.prev;
        JKRLinkedListNode *node = [[JKRLinkedListNode alloc] initWithPrev:prev object:anObject next:next];
        next.prev = node;
        _first = node;
        prev.next = nil;
        prev.weakNext = node;
    } else { // 插入到兩個節點中間
        JKRLinkedListNode *next = [self nodeWithIndex:index];
        JKRLinkedListNode *prev = next.prev;
        JKRLinkedListNode *node = [[JKRLinkedListNode alloc] initWithPrev:prev object:anObject next:next];
        next.prev = node;
        prev.next = node;
        prev.weakNext = nil;
    }
複製代碼

將相同代碼邏輯提取出來:

JKRLinkedListNode *next = [self nodeWithIndex:index];
    JKRLinkedListNode *prev = next.prev;
    JKRLinkedListNode *node = [[JKRLinkedListNode alloc] initWithPrev:prev object:anObject next:next];
    next.prev = node;
    // 還可用 next == _first 判斷,插入到表頭即該位置的節點是鏈表的頭節點
    if (index == 0) { // 插入到表頭
        _first = node;
        prev.next = nil;
        prev.weakNext = node;
    } else { // 插入到兩個節點中間
        prev.next = node;
        prev.weakNext = nil;
    }
複製代碼

添加節點代碼總結

- (void)insertObject:(id)anObject atIndex:(NSUInteger)index {
    [self rangeCheckForAdd:index];
    
    // index == size 至關於 插入到表尾 或者 空鏈表添加第一個節點
    if (_size == index) {
        JKRLinkedListNode *oldLast = _last;
        JKRLinkedListNode *node = [[JKRLinkedListNode alloc] initWithPrev:_last object:anObject next:_first];
        _last = node;
        // _size == 0
        if (!oldLast) { // 添加鏈表第一個元素
            _first = _last;
            _first.prev = _first;
            _first.next = nil;
            _first.weakNext = _first;
        } else { // 插入到表尾
            oldLast.next = _last;
            oldLast.weakNext = nil;
            _first.prev = _last;
            _last.next = nil;
            _last.weakNext = _first;
        }
    } else { // 插入到表的非空節點的位置上
        JKRLinkedListNode *next = [self nodeWithIndex:index];
        JKRLinkedListNode *prev = next.prev;
        JKRLinkedListNode *node = [[JKRLinkedListNode alloc] initWithPrev:prev object:anObject next:next];
        next.prev = node;
        // index == 0
        if (next == _first) { // 插入到表頭
            _first = node;
            prev.next = nil;
            prev.weakNext = node;
        } else { // 插入到兩個節點中間
            prev.next = node;
            prev.weakNext = nil;
        }
    }

    _size++;
}
複製代碼

刪除節點

刪除惟一的節點

刪除鏈表惟一的節點以下圖:

須要的操做以下圖:

  • 將鏈表的頭節點指向null
  • 將鏈表的尾節點指向null

代碼以下:

if (_size == 1) { // 刪除惟一的節點
    _first = nil;
    _last = nil;
} 
複製代碼

刪除頭節點

刪除頭節點以下圖:

須要的操做以下圖:

  • 被刪除節點的上一個節點(尾節點)的weakNext指向被刪除節點的下一個節點。
  • 被刪除節點的後一個節點的prev指向被刪除節點的前一個節點。
  • 鏈表的頭節點指向被刪除節點的下一個節點。

刪除頭節點代碼以下:

if (_size == 1) { // 刪除惟一的節點
    _first = nil;
    _last = nil;
} else {
    // 被刪除的節點
    JKRLinkedListNode *node = [self nodeWithIndex:index];
    // 被刪除的節點的上一個節點
    JKRLinkedListNode *prev = node.prev;
    // 被刪除的節點的下一個節點
    JKRLinkedListNode *next = node.next;
    
    if (node == _first) { // 刪除頭節點
        prev.next = nil;
        prev.weakNext = next;
        next.prev = prev;
        _first = next;
    } else {
        // ...
    }
}
複製代碼

刪除尾節點

刪除頭節點以下圖:

須要的操做以下圖:

  • 將原來尾節點的前一個節點(新的尾節點)的weakNext指向原來尾節點的next(頭節點)。
  • 將原來尾節點的後一個節點(頭節點)的prev指向原來尾節點的前一個節點(新的尾節點)。
  • 鏈表的尾節點last指向原來尾節點的前一個節點(新的尾節點)。

代碼以下:

if (_size == 1) { // 刪除惟一的節點
    _first = nil;
    _last = nil;
} else {
    // 被刪除的節點
    JKRLinkedListNode *node = [self nodeWithIndex:index];
    // 被刪除的節點的上一個節點
    JKRLinkedListNode *prev = node.prev;
    // 被刪除的節點的下一個節點
    JKRLinkedListNode *next = node.next;
    
    if (node == _first) { // 刪除頭節點
        prev.next = nil;
        prev.weakNext = next;
        next.prev = prev;
        _first = next;
    } else if (node == _last) { // 刪除尾節點
        prev.next = nil;
        prev.weakNext = next;
        next.prev = prev;
        _last = prev;
    } else { // 刪除節點之間的節點
        // ...
    }
}
複製代碼

刪除鏈表節點中間的節點

刪除鏈表節點中間的節點以下圖:

須要的操做以下圖:

  • 被刪除節點的前一個節點的next指向被刪除節點的next。
  • 被刪除節點的後一個節點的prev指向被刪除節點的prev。

代碼以下:

if (_size == 1) { // 刪除惟一的節點
    _first = nil;
    _last = nil;
} else {
    // 被刪除的節點
    JKRLinkedListNode *node = [self nodeWithIndex:index];
    // 被刪除的節點的上一個節點
    JKRLinkedListNode *prev = node.prev;
    // 被刪除的節點的下一個節點
    JKRLinkedListNode *next = node.next;
    
    if (node == _first) { // 刪除頭節點
        prev.next = nil;
        prev.weakNext = next;
        next.prev = prev;
        _first = next;
    } else if (node == _last) { // 刪除尾節點
        prev.next = nil;
        prev.weakNext = next;
        next.prev = prev;
        _last = prev;
    } else { // 刪除節點之間的節點
        prev.next = next;
        next.prev = prev;
    }
}
複製代碼

添加節點代碼總結

- (void)removeObjectAtIndex:(NSUInteger)index {
    [self rangeCheckForExceptAdd:index];

    if (_size == 1) { // 刪除惟一的節點
        _first = nil;
        _last = nil;
    } else {
        // 被刪除的節點
        JKRLinkedListNode *node = [self nodeWithIndex:index];
        // 被刪除的節點的上一個節點
        JKRLinkedListNode *prev = node.prev;
        // 被刪除的節點的下一個節點
        JKRLinkedListNode *next = node.next;

        if (node == _first) { // 刪除頭節點
            prev.next = nil;
            prev.weakNext = next;
            next.prev = prev;
            _first = next;
        } else if (node == _last) { // 刪除尾節點
            prev.next = nil;
            prev.weakNext = next;
            next.prev = prev;
            _last = prev;
        } else { // 刪除節點之間的節點
            prev.next = next;
            next.prev = prev;
        }
    }

    _size--;
}
複製代碼

測試

依然採用和雙向鏈表同樣的測試用例:

void testCirleList() {
    JKRBaseList *list = [JKRLinkedCircleList new];
    [list addObject:[Person personWithAge:1]];
    printf("%s", [NSString stringWithFormat:@"添加鏈表第一個節點 \n%@\n\n", list].UTF8String);
    
    [list addObject:[Person personWithAge:3]];
    printf("%s", [NSString stringWithFormat:@"尾部追加一個節點 \n%@\n\n", list].UTF8String);
    
    [list insertObject:[Person personWithAge:2] atIndex:1];
    printf("%s", [NSString stringWithFormat:@"插入到鏈表兩個節點之間 \n%@\n\n", list].UTF8String);
    
    [list insertObject:[Person personWithAge:0] atIndex:0];
    printf("%s", [NSString stringWithFormat:@"插入到鏈表頭部 \n%@\n\n", list].UTF8String);
    
    [list removeFirstObject];
    printf("%s", [NSString stringWithFormat:@"刪除頭節點 \n%@\n\n", list].UTF8String);
    
    [list removeObjectAtIndex:1];
    printf("%s", [NSString stringWithFormat:@"刪除鏈表兩個節點之間的節點 \n%@\n\n", list].UTF8String);
    
    [list removeLastObject];
    printf("%s", [NSString stringWithFormat:@"刪除尾節點 \n%@\n\n", list].UTF8String);
    
    [list removeAllObjects];
    printf("%s", [NSString stringWithFormat:@"刪除鏈表惟一的節點 \n%@\n\n", list].UTF8String);
}
複製代碼

打印結果:

添加鏈表第一個節點 
Size: 1 [(W 1) -> 1 -> (W 1)]

尾部追加一個節點 
Size: 2 [(W 3) -> 1 -> (3), (W 1) -> 3 -> (W 1)]

插入到鏈表兩個節點之間 
Size: 3 [(W 3) -> 1 -> (2), (W 1) -> 2 -> (3), (W 2) -> 3 -> (W 1)]

插入到鏈表頭部 
Size: 4 [(W 3) -> 0 -> (1), (W 0) -> 1 -> (2), (W 1) -> 2 -> (3), (W 2) -> 3 -> (W 0)]


0 dealloc
刪除頭節點 
Size: 3 [(W 3) -> 1 -> (2), (W 1) -> 2 -> (3), (W 2) -> 3 -> (W 1)]


2 dealloc
刪除鏈表兩個節點之間的節點 
Size: 2 [(W 3) -> 1 -> (3), (W 1) -> 3 -> (W 1)]


3 dealloc
刪除尾節點 
Size: 1 [(W 1) -> 1 -> (W 1)]

刪除鏈表惟一的節點 
Size: 0 []
1 dealloc
複製代碼

能夠看到,全部節點都經過弱引用指向本身前一個節點,除尾節點以外,全部節點節點都經過強引用指向本身的後一個節點。尾節點的weakNext經過弱引用循環指向頭節點,頭節點通prev經過弱引用指向本身的尾節點。

時間複雜度分析

經過上面添加刪除的邏輯能夠知道,雙向循環鏈表在對頭尾操做時時間複雜度同雙向鏈表,也是O(1)。對於鏈表中間的節點,同雙向鏈表也是O(n),越靠近鏈表中間查詢次數越多,越靠近鏈表頭部或尾部查詢越快。

同上一節的測試用例,對比雙向循環鏈表和雙向鏈表不一樣位置進行50000次插入刪除操做時間對比:

雙向循環鏈表操做頭節點
耗時: 0.053 s
雙向鏈表操做頭節點
耗時: 0.034 s

雙向循環鏈表操做尾節點
耗時: 0.045 s
雙向鏈表操做尾節點
耗時: 0.032 s

雙向循環鏈表操做 index = 總節點數*0.25 節點
耗時: 12.046 s
雙向鏈表操做 index = 總節點數*0.25 節點
耗時: 11.945 s

單雙向循環鏈表操做 index = 總節點數*0.75 節點
耗時: 19.340 s
雙向鏈表操做 index = 總節點數*0.75 節點
耗時: 19.162 s

雙向循環鏈表操做中間節點
耗時: 37.876 s
雙向鏈表操做中間節點
耗時: 37.862 s
複製代碼

循環鏈表的應用:約瑟夫問題

聽說著名猶太曆史學家 Josephus有過如下的故事:在羅馬人佔領喬塔帕特後,39 個猶太人與Josephus及他的朋友躲到一個洞中,39個猶太人決定寧願死也不要被敵人抓到,因而決定了一個自殺方式,41我的排成一個圓圈,由第1我的開始報數,每報數到第3人該人就必須自殺,而後再由下一個從新報數,直到全部人都自殺身亡爲止。然而Josephus 和他的朋友並不想聽從。首先從一我的開始,越過k-2我的(由於第一我的已經被越過),並殺掉第k我的。接着,再越過k-1我的,並殺掉第k我的。這個過程沿着圓圈一直進行,直到最終只剩下一我的留下,這我的就能夠繼續活着。問題是,給定了和,一開始要站在什麼地方纔能避免被處決?Josephus要他的朋友先僞裝聽從,他將朋友與本身安排在第16個與第31個位置,因而逃過了這場死亡遊戲。

以前使用單向循環鏈表解決約瑟夫問題,這裏使用雙向循環鏈表一樣能夠:

void useLinkedCircleList() {
    JKRLinkedCircleList *list = [JKRLinkedCircleList new];
    for (NSUInteger i = 1; i <= 41; i++) {
        [list addObject:[NSNumber numberWithInteger:i]];
    }
    NSLog(@"%@", list);
    
    JKRLinkedListNode *node = list->_first;
    while (list.count) {
        node = node.next;
        node = node.next;
        printf("%s ", [[NSString stringWithFormat:@"%@", node.object] UTF8String]);
        [list removeObject:node.object];
        node = node.next;
    }
    printf("\n");
}
複製代碼

打印順序:

3 6 9 12 15 18 21 24 27 30 33 36 39 1 5 10 14 19 23 28 32 37 41 7 13 20 26 34 40 8 17 29 38 11 25 2 22 4 35 16 31 
複製代碼

最後兩個數字是16和31。

接下來

鏈表和數組如今都已經完成了,接下來能夠用這些簡單的數組結構實現以前一聽就比較厲害的數據結構:哈希表,實現以後發現其實也不是那麼的複雜呢。

源碼

點擊查看源碼

相關文章
相關標籤/搜索