數據結構4-單向循環鏈表

單向循環鏈表是在單向鏈表的基礎上,將最後一個節點的next指針指向鏈表的頭節點。可是基於Objective-C內存管理的機制,這樣會出現循環引用,因此最後一個節點指向頭節點應該用弱引用,如上圖所示。node

循環鏈表的節點

循環單向鏈表須要比單向鏈表的節點多一個weakNext指向鏈表的頭節點:git

#import <Foundation/Foundation.h>

NS_ASSUME_NONNULL_BEGIN

@interface JKRSingleLinkedListNode : NSObject

@property (nonatomic, strong, nullable) id object;
@property (nonatomic, weak, nullable) JKRSingleLinkedListNode *weakNext;
@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
複製代碼

爲了便於拿到節點的下一個節點,這裏在next的get方法中作了判斷,若是next爲空再去取weakNext,這樣就能夠經過獲取next來拿到鏈表的下一個節點,不用去單獨判斷哪一個爲空的狀況。因此這裏須要注意的就是:必定要維護好weakNext和next,使得它們不可以同時有值,而且只能在應該不爲空的時候有值。github

  • weakNext:當節點是鏈表最後一個節點的時候(包括鏈表只有一個節點的狀況),weakNext指向鏈表的頭節點(鏈表只有一個節點的時候指向本身),而且此時next爲nil。
  • next:當節點不是鏈表的最後一個節點的時候,next指向該節點的下一個節點,而且此時weakNext爲nil。
#import "JKRSingleLinkedListNode.h"

@implementation JKRSingleLinkedListNode

- (instancetype)initWithObject:(id)object next:(JKRSingleLinkedListNode *)next {
    self = [super init];
    self.object = object;
    self.next = next;
    return self;
}

- (JKRSingleLinkedListNode *)next {
    if (_next) {
        return _next;
    } else {
        return _weakNext;
    }
}

- (void)dealloc {
//    NSLog(@"<%@: %p>: %@ dealloc", self.class, self, self.object);
}

- (NSString *)description {
    NSString *tipString = @"";
    // 這裏是一個next和weakNext維護錯誤的提示,兩個節點都有值即顯示E (error),表明指針維護有問題
    if (_next && _weakNext) {
        tipString = @"E ";
    } else if (_weakNext) {
        tipString = @"W ";
    }
    return [NSString stringWithFormat:@"%@ -> (%@%@)", self.object, tipString, self.next.object];
}

@end
複製代碼

添加節點

添加第一個節點

插入第一個節點時,須要將鏈表的_first指針指向建立的節點,並將節點的weakNext指向本身。bash

插入到鏈表頭部

如圖,要將一個新節點插入到鏈表的頭部:post

須要的操做以下圖:測試

  • 新節點的next指針指向原來的頭節點。
  • 尾節點的weakNext指向新節點。
  • 頭節點指針_first指向新的節點。

添加後鏈表的結構:ui

鏈表尾部追加一個節點

如圖,要將一個新節點插入到鏈表的尾部:atom

須要的操做以下圖:spa

  • 原來尾節點的weakNext設爲nil,原來尾節點的next指向新節點。
  • 新節點的weakNext指向頭節點。

插入到鏈表節點中間

如圖,要將一個新節點插入到鏈表中已經存在的兩個節點之間:3d

須要的操做以下圖:

  • 將插入位置的前一個節點的next指針指向新節點。
  • 將新節點的next指針指向插入位置的原節點。

完成操做後的鏈表結構:

添加節點的代碼

綜上的添加邏輯,添加節點的代碼以下:

- (void)insertObject:(id)anObject atIndex:(NSUInteger)index {
    [self rangeCheckForAdd:index];
    if (index == 0) { // 插入鏈表頭部或添加第一個節點
        JKRSingleLinkedListNode *node = [[JKRSingleLinkedListNode alloc] initWithObject:anObject next:_first];
        JKRSingleLinkedListNode *last = (_size == 0) ? node : [self nodeWithIndex:_size - 1];
        last.next = nil;
        last.weakNext = node;
        _first = node;
    } else { // 插入到鏈表尾部或鏈表中間
        JKRSingleLinkedListNode *prev = [self nodeWithIndex:index - 1];
        JKRSingleLinkedListNode *node = [[JKRSingleLinkedListNode alloc] initWithObject:anObject next:prev.next];
        prev.next = node;
        prev.weakNext = nil;
        if (node.next == _first) { // 插入到鏈表尾部
            node.next = nil;
            node.weakNext = _first;
        }
    }
    _size++;
}
複製代碼

刪除節點

刪除當前惟一的節點

刪除當前惟一的節點,只須要將_first設置爲nil,節點就會自動被釋放:

刪除頭節點

刪除長度不爲1的鏈表的頭部節點,以下圖:

須要的操做以下:

  • 將_first指向原來頭節點的下一個節點。
  • 將尾節點的weakNext指向新的頭節點。

刪除尾節點

刪除鏈表尾部的節點以下圖:

要刪除尾部的節點,首先要先拿到鏈表尾部節點的上一個節點,並對其進行以下操做:

  • 將尾節點的前一個節點的next設置爲nil。
  • 將尾節點的前一個節點的weakNext設置爲頭節點。

刪除鏈表節點之間的節點

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

只須要找到被刪除節點的前一個節點,並將它的next指向被刪除節點的next便可:

刪除節點的代碼

綜上全部狀況,刪除節點的代碼爲:

- (void)removeObjectAtIndex:(NSUInteger)index {
    [self rangeCheckForExceptAdd:index];
    
    JKRSingleLinkedListNode *node = _first;
    if (index == 0) { // 刪除頭節點或者惟一的節點
        if (_size == 1) { // 刪除惟一的節點
            _first = nil;
        } else { // 刪除頭節點
            JKRSingleLinkedListNode *last = [self nodeWithIndex:_size - 1];
            _first = _first.next;
            last.next = nil;
            last.weakNext = _first;
        }
    } else { // 刪除尾節點或中間的節點
        JKRSingleLinkedListNode *prev = [self nodeWithIndex:index - 1];
        node = prev.next;
        if (node.next == _first) { // 刪除尾節點
            prev.next = nil;
            prev.weakNext = _first;
        } else { // 刪除中間節點
            prev.next = node.next;
            prev.weakNext = nil;
        }
    }
    _size--;
}
複製代碼

測試單向循環鏈表

測試對象Person

@interface Person : NSObject

@property (nonatomic, assign) NSInteger age;
+ (instancetype)personWithAge:(NSInteger)age;

@end


@implementation Person

+ (instancetype)personWithAge:(NSInteger)age {
    Person *p = [Person new];
    p.age = age;
    return p;
}

- (void)dealloc {
    NSLog(@"%@ dealloc", self);
}

- (NSString *)description {
    return [NSString stringWithFormat:@"%zd", self.age];
}
複製代碼

添加第一個節點

首先爲空的單向循環鏈表添加第一個節點:

JKRBaseList *list = [JKRSingleCircleLinkedList new];
[list addObject:[Person personWithAge:1]];
複製代碼

打印鏈表結果:

Size: 1 [1 -> (W 1)]
複製代碼

鏈表的weakNext指向本身。

插入到鏈表尾部

再添加一個元素到尾部:

[list addObject:[Person personWithAge:3]];
複製代碼

打印鏈表結果:

Size: 2 [1 -> (3), 3 -> (W 1)]
複製代碼

age爲1的person對象經過強引用指向新的age爲3的person對象,age爲3的person對象經過弱引用指向age爲1的person對象。

插入到鏈表中間

[list insertObject:[Person personWithAge:2] atIndex:1];
複製代碼

打印鏈表結果:

Size: 3 [1 -> (2), 2 -> (3), 3 -> (W 1)]
複製代碼

插入到鏈表頭部

[list insertObject:[Person personWithAge:0] atIndex:0];
複製代碼

打印鏈表結果:

Size: 4 [0 -> (1), 1 -> (2), 2 -> (3), 3 -> (W 0)]
複製代碼

刪除頭節點

[list removeFirstObject];
複製代碼

打印鏈表結果:

0 dealloc
Size: 3 [1 -> (2), 2 -> (3), 3 -> (W 1)]
複製代碼

刪除鏈表尾部

[list removeLastObject];
複製代碼

打印鏈表結果:

3 dealloc
Size: 2 [1 -> (2), 2 -> (W 1)]
複製代碼

刪除鏈表中間節點

從新將上面長度2的節點添加一個age爲3的person對象到尾部:

[list addObject:[Person personWithAge:3]];
複製代碼

打印鏈表結果:

Size: 3 [1 -> (2), 2 -> (3), 3 -> (W 1)]
複製代碼

刪除index爲1的節點,即中間age爲2的節點:

[list removeObjectAtIndex:1];
複製代碼

打印鏈表結果:

Size: 2 [1 -> (3), 3 -> (W 1)]
複製代碼

時間複雜度分析

單向循環鏈表和單向一樣,在操做插入和刪除節點時,對節點關係的維護上都是O(1),可是和單向鏈表一樣須要經過index查找到對於的節點進行操做,這個查找是從頭節點開始,依次經過節點的next指針找到下一個節點,直到找到第index個節點爲止,因此在添加、刪除、取值上和單向鏈表一致,平均都是O(n)。

不一樣的是,單向循環鏈表在對頭節點進行操做時,須要獲取尾節點,這個也是O(n)級別的查找,因此,單向鏈表只有在鏈表只有一個節點的狀況下才有O(1)級別的最優時間,其它狀況不管是操做頭節點仍是尾節點,都是O(n),而不是像單向鏈表同樣,操做頭節點都是O(1)。

對比單向鏈表和單向循環鏈表分別操做頭節點、中間節點、尾節點的時間:

void compareSingleLinkedListAndSingleCircleLinkedList() {
    [JKRTimeTool teskCodeWithBlock:^{
        JKRBaseList *array = [JKRSingleLinkedList new];
        for (NSUInteger i = 0; i < 10000; i++) {
            [array insertObject:[NSNumber numberWithInteger:i] atIndex:0];
        }
        for (NSUInteger i = 0; i < 10000; i++) {
            [array removeFirstObject];
        }
        NSLog(@"單向鏈表操做頭節點");
    }];
    [JKRTimeTool teskCodeWithBlock:^{
        JKRBaseList *array = [JKRSingleCircleLinkedList new];
        for (NSUInteger i = 0; i < 10000; i++) {
            [array insertObject:[NSNumber numberWithInteger:i] atIndex:0];
        }
        for (NSUInteger i = 0; i < 10000; i++) {
            [array removeFirstObject];
        }
        NSLog(@"單向循環鏈表操做頭節點");
    }];
    
    [JKRTimeTool teskCodeWithBlock:^{
        JKRBaseList *array = [JKRSingleLinkedList new];
        for (NSUInteger i = 0; i < 10000; i++) {
            [array addObject:[NSNumber numberWithInteger:i]];
        }
        for (NSUInteger i = 0; i < 10000; i++) {
            [array removeLastObject];
        }
        NSLog(@"單向鏈表操做尾節點");
    }];
    [JKRTimeTool teskCodeWithBlock:^{
        JKRBaseList *array = [JKRSingleCircleLinkedList new];
        for (NSUInteger i = 0; i < 10000; i++) {
            [array addObject:[NSNumber numberWithInteger:i]];
        }
        for (NSUInteger i = 0; i < 10000; i++) {
            [array removeLastObject];
        }
        NSLog(@"單向循環鏈表操做尾節點");
    }];
    
    [JKRTimeTool teskCodeWithBlock:^{
        JKRBaseList *array = [JKRSingleLinkedList new];
        for (NSUInteger i = 0; i < 10000; i++) {
            [array insertObject:[NSNumber numberWithInteger:i] atIndex:array.count >> 1];
        }
        for (NSUInteger i = 0; i < 10000; i++) {
            [array removeObjectAtIndex:array.count >> 1];
        }
        NSLog(@"單向鏈表操做中間節點");
    }];
    [JKRTimeTool teskCodeWithBlock:^{
        JKRBaseList *array = [JKRSingleCircleLinkedList new];
        for (NSUInteger i = 0; i < 10000; i++) {
            [array insertObject:[NSNumber numberWithInteger:i] atIndex:array.count >> 1];
        }
        for (NSUInteger i = 0; i < 10000; i++) {
            [array removeObjectAtIndex:array.count >> 1];
        }
        NSLog(@"單向循環鏈表操做中間節點");
    }];
}
複製代碼

打印結果:

單向鏈表操做頭節點
耗時: 0.004 s
單向循環鏈表操做頭節點
耗時: 1.947 s

單向鏈表操做尾節點
耗時: 1.980 s
單向循環鏈表操做尾節點
耗時: 1.962 s

單向鏈表操做中間節點
耗時: 0.984 s
單向循環鏈表操做中間節點
耗時: 0.972 s
複製代碼

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

問題來歷

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

問題分析

首先全部的人是圍城一圈的,並且須要循環不少圈纔可以將全部人依次排除,而這很是適合剛剛完成的單向循環鏈表才解決,尾節點的下一個節點又從新拿到的頭節點,剛剛和問題中的狀況契合。

首先咱們只要拿到鏈表的頭節點,而後依次經過頭節點的next指針日後拿到下一個節點,找到第3個移除鏈表,而後依次循環直到鏈表爲空,移除的順序就是咱們須要的死亡順序。

爲了作到這個功能,首先咱們須要先修改一下單向循環鏈表的_first訪問權限爲public,讓外部可以拿到頭節點。

@interface JKRSingleCircleLinkedList : JKRBaseList {
@public
    JKRSingleLinkedListNode *_first;
}

@end
複製代碼

單向循環鏈表求死亡順序

咱們能夠將39我的抽象成咱們以前測試的Person類,age屬性就是每一個人編號1-41。將41我的添加到單向循環鏈表中,先拿到頭節點,利用next指針日後查到找到後第三個報數的節點,移除它並打印。而後再將起始報數節點設爲被刪除的next。具體代碼以下:

void useSingleCircleList() {
    JKRSingleCircleLinkedList *list = [JKRSingleCircleLinkedList new];
    for (NSUInteger i = 1; i <= 41; i++) {
        [list addObject:[Person personWithAge:i]];
    }
    NSLog(@"%@", list);
    
    JKRSingleLinkedListNode *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 
複製代碼

接下來

單向循環鏈表相比單向鏈表來說並無太複雜,之因此更麻煩些仍是由於Objective-C內存管理機制的問題,須要解決循環引用的問題,若是用Java實現的話就會很是的簡單了。單向鏈表和單向循環鏈表都會由於查找尾節點致使須要依次遍歷鏈表全部節點而耗費時間,假如不須要去查找尾節點,那麼鏈表應該會更加的高效。

接下來就歡迎雙向鏈表的到來,它能夠直接拿到鏈表的尾節點,那效率會不會一下變得更高呢?

源碼

點擊查看源碼

相關文章
相關標籤/搜索