数据结构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。

接下来

链表和数组如今都已经完成了,接下来能够用这些简单的数组结构实现以前一听就比较厉害的数据结构:哈希表,实现以后发现其实也不是那么的复杂呢。

源码

点击查看源码