之前已经实现了单向循环链表,双向循环链表的原理和单向链表很相似:尾节点的next指向链表的头节点。在此基础上,头节点的prev指向尾节点,这样就实现了双向循环链表。同样,为了防止循环引用,尾节点指向头节点要用弱引用。
双向循环链表的节点
#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的维护操作。
添加链表的第一个节点
对比双向链表,双向循环链表除了将链表的头节点和尾节点指向新节点之外,还需要将节点的prev、weakNext都指向它自己。
代码逻辑如下:
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;
}
复制代码
链表尾部追加一个节点
新添加的节点替换原来的尾节点称为新的尾节点:
需要的操作如下图:
- 新添加节点的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;
}
复制代码
上面两段代码将相同的判断逻辑合并,不同的判断逻辑分开:
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;
}
}
复制代码
插入到链表头部
插入一个新节点到链表的头部如下图:
需要的操作如下图:
- 新节点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。