数据结构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实现的话就会很是的简单了。单向链表和单向循环链表都会由于查找尾节点致使须要依次遍历链表全部节点而耗费时间,假如不须要去查找尾节点,那么链表应该会更加的高效。

接下来就欢迎双向链表的到来,它能够直接拿到链表的尾节点,那效率会不会一下变得更高呢?

源码

点击查看源码