双向链表的结构
双向链表和单向链表的对比
前面已经完成了单向链表的实现,双向链表相比单向链表,有以下的不同:
- 节点:单向链表每一个节点有一个next指针指向下一个节点。双向链表的节点在单向链表节点的基础上,增加了一个prev指针,指向改节点前一个节点。
- 链表:双向链表中除了_first指针指向链表的头节点之外,又增加了一个_last指针指向链表的尾节点。
双向链表数据结构和单向链表的对比如下图:
由于Objective-C循环引用的问题,这里每一个节点next使用强引用以维持链表中节点引用计数大于1,而prev使用弱引用以避免循环引用造成内存泄漏。
链表的头节点prev由于没有上一个节点,prev指向nil,链表的尾节点由于没有下一个结点,next指向nil。链表的first指向指向链表的头节点,链表的last指向链表的尾节点,这样就构成了一个完整的双向链表。
双向链表的结构
双向链表中,需要两个成员变量分别保存链表的头节点和尾节点。
#import "JKRBaseList.h"
#import "JKRLinkedListNode.h"
NS_ASSUME_NONNULL_BEGIN
@interface JKRLinkedList : JKRBaseList {
JKRLinkedListNode *_first;
JKRLinkedListNode *_last;
}
@end
NS_ASSUME_NONNULL_END
复制代码
双向链表的节点
双向链表的节点在单向链表的基础上,增加了prev指向该节点的前一个节点。这里prev使用弱引用,以避免Objective-C中的循环引用问题。
NS_ASSUME_NONNULL_BEGIN
@interface JKRLinkedListNode : NSObject
@property (nonatomic, strong, nullable) id object;
@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
复制代码
通过index查找节点
和之前的单向链表一样,链表的添加和删除操作,都无法避免的需要查找到要插入或者删除的index位置相关的节点,所以要完成添加和删除节点首先需要先完成通过index查找节点的功能。
双向链表通过index查找节点和单向链表相比,有一个非常大的优势就是单向链表只能够从头节点一个一个的向后查找。而双向链表由于可以直接获取链表的尾节点,并且可以通过尾节点的prev指针一步一步向前查找,这样就可以做一个查找优化:
- 当index位于链表的前半部分,就从头节点开始向后查找。
- 当index位于链表的后半部分,就从尾节点开始向前查找。 这样,通过index查找节点最多就需要查找链表一半的长度就可以了。
优化的查找代码如下:
- (JKRLinkedListNode *)nodeWithIndex:(NSInteger)index {
[self rangeCheckForExceptAdd:index];
// _size >> 1 相当于 floor(_size / 2),位运算可以大大节省计算时间
if (index < (_size >> 1)) { // 当index位于链表的前半,从头节点向后查找
JKRLinkedListNode *node = _first;
for (NSUInteger i = 0; i < index; i++) {
node = node.next;
}
return node;
} else { // 当index位于链表的后半,从尾节点向前查找
JKRLinkedListNode *node = _last;
for (NSUInteger i = _size - 1; i > index; i--) {
node = node.prev;
}
return node;
}
}
复制代码
添加节点
添加第一个节点
双向链表添加第一个节点时,链表的first和last都指向这个新节点,新节点的prev和next都指向nil,如下图:
代码逻辑如下:
if (_size == 0) {
JKRLinkedListNode *node = [[JKRLinkedListNode alloc] initWithPrev:nil object:anObject next:nil];
_last = node;
_first = node;
}
复制代码
链表尾部追加一个节点
在链表的尾部追加一个节点如下图,新添加的节点替换原来的尾节点称为新的尾节点。
需要的操作如下图:
- 新添加节点的prev指向链表原来的尾节点。
- 新添加节点的next指向null。
- 链表的尾节点指针last指向新添加的节点。
- 链表原来尾节点的next指向现在链表的新尾节点(即新添加的节点)。
代码逻辑如下:
if (index == _size) {
JKRLinkedListNode *oldLast = _last;
JKRLinkedListNode *node = [[JKRLinkedListNode alloc] initWithPrev:_last object:anObject next:nil];
_last = node;
oldLast.next = _last;
}
复制代码
添加第一个节点和尾部追加节点代码整合
if (_size == 0) {
JKRLinkedListNode *node = [[JKRLinkedListNode alloc] initWithPrev:nil object:anObject next:nil];
_last = node;
_first = node;
}
if (index == _size) {
JKRLinkedListNode *oldLast = _last;
JKRLinkedListNode *node = [[JKRLinkedListNode alloc] initWithPrev:_last object:anObject next:nil];
_last = node;
oldLast.next = _last;
}
复制代码
上面两段代码是添加头节点和尾部追加节点代码,首先可以发现,当添加的是第一个节点时候,_size == 0,index == 0,所以上面的这里可以整合成一个判断:
if (index == _size) {
if (_size == 0) {
JKRLinkedListNode *node = [[JKRLinkedListNode alloc] initWithPrev:nil object:anObject next:nil];
_last = node;
_first = node;
} else {
JKRLinkedListNode *oldLast = _last;
JKRLinkedListNode *node = [[JKRLinkedListNode alloc] initWithPrev:_last object:anObject next:nil];
_last = node;
oldLast.next = _last;
}
}
复制代码
当_size == 0 的时候,_first和_last都为空,所以代码可以再写成:
if (index == _size) {
if (_size == 0) {
JKRLinkedListNode *node = [[JKRLinkedListNode alloc] initWithPrev:_last object:anObject next:nil];
_last = node;
_first = node;
} else {
JKRLinkedListNode *oldLast = _last;
JKRLinkedListNode *node = [[JKRLinkedListNode alloc] initWithPrev:_last object:anObject next:nil];
_last = node;
oldLast.next = _last;
}
}
复制代码
提取相同的代码放在上面:
if (index == _size) {
JKRLinkedListNode *oldLast = _last;
JKRLinkedListNode *node = [[JKRLinkedListNode alloc] initWithPrev:_last object:anObject next:nil];
_last = node;
if (_size == 0) {
_first = node;
} else {
oldLast.next = _last;
}
}
复制代码
插入到链表头部
插入一个新节点到链表的头部如下图:
需要的操作如下图:
- 首先获取链表的头节点。
- 新节点的prev指向链表原头节点的prev(null)。
- 新节点的next指向链表原头节点。
- 链表原头节点的prev指向新节点。
- 链表的first指向新节点。
代码逻辑如下
if (index == _size) { // 插入到表尾 或者 空链表添加第一个节点
// ...
} else { // 插入到表的非空节点的位置上
if (index == 0) { // 插入到表头
JKRLinkedListNode *next = [self nodeWithIndex:0];
JKRLinkedListNode *prev = next.prev;
JKRLinkedListNode *node = [[JKRLinkedListNode alloc] initWithPrev:prev object:anObject next:next];
next.prev = node;
_first = node;
} else { // 插入到链表节点中间
}
}
复制代码
插入到链表节点中间
插入一个新节点到链表两个节点中间如下图:
需要的操作如下图:
- 首先获取插入位置index对应的节点。
- 新节点的prev指向链表插入位置原节点的prev。
- 新节点的next指向链表插入位置原节点。
- 链表插入位置原节点的prev指向新节点。
- 链表插入位置原节点的前一个节点的next指向新节点。
代码逻辑如下:
if (index == _size) { // 插入到表尾 或者 空链表添加第一个节点
// ...
} else { // 插入到表的非空节点的位置上
if (index == 0) { // 插入到表头
JKRLinkedListNode *next = [self nodeWithIndex:0];
JKRLinkedListNode *prev = next.prev;
JKRLinkedListNode *node = [[JKRLinkedListNode alloc] initWithPrev:prev object:anObject next:next];
next.prev = node;
_first = 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;
}
}
复制代码
插入到表的非空节点位置的代码逻辑整合
if (index == _size) { // 插入到表尾 或者 空链表添加第一个节点
// ...
} else { // 插入到表的非空节点的位置上
if (index == 0) { // 插入到表头
JKRLinkedListNode *next = [self nodeWithIndex:0];
JKRLinkedListNode *prev = next.prev;
JKRLinkedListNode *node = [[JKRLinkedListNode alloc] initWithPrev:prev object:anObject next:next];
next.prev = node;
_first = 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;
}
}
复制代码
将相同的代码逻辑提取出来:
if (index == _size) { // 插入到表尾 或者 空链表添加第一个节点
// ...
} else { // 插入到表的非空节点的位置上
JKRLinkedListNode *next = [self nodeWithIndex:0];
JKRLinkedListNode *prev = next.prev;
JKRLinkedListNode *node = [[JKRLinkedListNode alloc] initWithPrev:prev object:anObject next:next];
next.prev = node;
if (index == 0) { // 插入到表头
_first = node;
} else { // 插入到链表节点中间
prev.next = node;
}
}
复制代码
添加节点代码总结
- (void)insertObject:(id)anObject atIndex:(NSUInteger)index {
[self rangeCheckForAdd:index];
if (index == _size) { // index == size 相当于 插入到表尾 或者 空链表添加第一个节点
JKRLinkedListNode *oldLast = _last;
JKRLinkedListNode *node = [[JKRLinkedListNode alloc] initWithPrev:_last object:anObject next:nil];
_last = node;
// 还可以用 !oldLast 、 !_first 判断
if (_size == 0) { // 空链表添加第一个节点
_first = _last;
} else { // 添加到表尾
oldLast.next = _last;
}
} 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 == _first 判断
if (index == 0) { // 插入到表头
_first = node;
} else { // 插入到表中间
prev.next = node;
}
}
_size++;
}
复制代码
删除节点
删除头节点
删除头节点如下图:
需要的操作如下图:
- 头节点指针指向头节点的下一个节点
- 新头节点的prev指向原头节点的prev
删除头节点代码如下:
// 删除头节点index == 0
- (void)removeObjectAtIndex:(NSUInteger)index {
JKRLinkedListNode *node = [self nodeWithIndex:index];
JKRLinkedListNode *prev = node.prev;
JKRLinkedListNode *next = node.next
_first = next;
next.prev = prev;
}
复制代码
删除尾节点
删除尾节点如下图:
需要的操作如下图:
- 将尾节点的前一个节点的next指向尾节点的next。
- 将链表last指向尾节点的前一个节点。
代码如下:
// 删除尾节点index == _size - 1
- (void)removeObjectAtIndex:(NSUInteger)index {
JKRLinkedListNode *node = [self nodeWithIndex:index];
JKRLinkedListNode *prev = node.prev;
JKRLinkedListNode *next = node.next
prev.next = next;
_last = prev;
}
复制代码
删除唯一的节点
删除链表唯一的节点如下图:
需要的操作如下图:
- 将链表的first指向节点的next
- 将链表的last指向节点的prev
代码如下:
// 删除唯一节点 index == 0,_size == 1
- (void)removeObjectAtIndex:(NSUInteger)index {
JKRLinkedListNode *node = [self nodeWithIndex:index];
JKRLinkedListNode *prev = node.prev;
JKRLinkedListNode *next = node.next
_first = next;
_last = prev;
}
复制代码
删除链表节点中间的节点
删除链表节点中间的节点如下图:
需要的操作如下图:
- 被删除节点的前一个节点的next指向被删除节点的next。
- 被删除节点的后一个节点的prev指向被删除节点的prev。
代码如下:
// 删除链表节点中间的节点, index > 0 && index < _size - 1
- (void)removeObjectAtIndex:(NSUInteger)index {
JKRLinkedListNode *node = [self nodeWithIndex:index];
JKRLinkedListNode *prev = node.prev;
JKRLinkedListNode *next = node.next
prev.next = next;
next.prev = prev;
}
复制代码
删除代码逻辑整合
// 删除头节点index == 0
- (void)removeObjectAtIndex:(NSUInteger)index {
JKRLinkedListNode *node = [self nodeWithIndex:index];
JKRLinkedListNode *prev = node.prev;
JKRLinkedListNode *next = node.next
_first = next;
next.prev = prev;
}
// 删除尾节点index == _size - 1
- (void)removeObjectAtIndex:(NSUInteger)index {
JKRLinkedListNode *node = [self nodeWithIndex:index];
JKRLinkedListNode *prev = node.prev;
JKRLinkedListNode *next = node.next
prev.next = next;
_last = prev;
}
// 删除唯一节点 index == 0,_size == 1
- (void)removeObjectAtIndex:(NSUInteger)index {
JKRLinkedListNode *node = [self nodeWithIndex:index];
JKRLinkedListNode *prev = node.prev;
JKRLinkedListNode *next = node.next
_first = next;
_last = prev;
}
// 删除链表节点中间的节点, index > 0 && index < _size - 1
- (void)removeObjectAtIndex:(NSUInteger)index {
JKRLinkedListNode *node = [self nodeWithIndex:index];
JKRLinkedListNode *prev = node.prev;
JKRLinkedListNode *next = node.next
prev.next = next;
next.prev = prev;
}
复制代码
将上面三段代码相同逻辑提取出来,不同逻辑地方进行判断:
- (void)removeObjectAtIndex:(NSUInteger)index {
JKRLinkedListNode *node = [self nodeWithIndex:index];
JKRLinkedListNode *prev = node.prev;
JKRLinkedListNode *next = node.next
if (node == _first && node == _last) { // 删除链表节点中间的节点
_first = next;
_last = prev;
} else if (node == _first) { // 删除头节点
_first = next;
next.prev = prev;
} else if (node == _last) { // 删除尾节点
prev.next = next;
_last = prev;
} else { // 删除唯一节点
prev.next = next;
next.prev = prev;
}
}
复制代码
下面就是合并这段代码:
if (node == _first && node == _last) { // 删除唯一节点
_first = next;
_last = prev;
} else if (node == _first) { // 删除头节点
_first = next;
next.prev = prev;
} else if (node == _last) { // 删除尾节点
prev.next = next;
_last = prev;
} else { // 删除链表节点中间的节点
prev.next = next;
next.prev = prev;
}
复制代码
转化成:
if (node == _first) {
_first = next;
} else {
prev.next = next;
}
if (node == _last) {
_last = prev;
} else {
next.prev = prev;
}
复制代码
删除节点的完整代码为:
- (void)removeObjectAtIndex:(NSUInteger)index {
[self rangeCheckForExceptAdd:index];
JKRLinkedListNode *node = [self nodeWithIndex:index];
JKRLinkedListNode *prev = node.prev;
JKRLinkedListNode *next = node.next;
if (node == _first) {
_first = next;
} else {
prev.next = next;
}
if (node == _last) {
_last = prev;
} else {
next.prev = prev;
}
_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];
}
@end
复制代码
测试全部功能
(W 1) -> 2 -> (null): 意味着该节点存储的值为2,用弱引用指向前一个节点,前一个节点的值为1,用强引用指向后一个节点,后一个节点为null。
JKRBaseList *list = [JKRLinkedList new];
[list addObject:[Person personWithAge:1]];
NSLog(@"添加链表第一个节点 \n%@\n", list);
[list addObject:[Person personWithAge:3]];
NSLog(@"尾部追加一个节点 \n%@\n", list);
[list insertObject:[Person personWithAge:2] atIndex:1];
NSLog(@"插入到链表两个节点之间 \n%@\n", list);
[list insertObject:[Person personWithAge:0] atIndex:0];
NSLog(@"插入到链表头部 \n%@\n", list);
[list removeFirstObject];
NSLog(@"删除头节点 \n%@\n", list);
[list removeObjectAtIndex:1];
NSLog(@"删除链表两个节点之间的节点 \n%@\n", list);
[list removeLastObject];
NSLog(@"删除尾节点 \n%@\n", list);
[list removeAllObjects];
NSLog(@"删除链表唯一的节点 \n%@\n", list);
// 打印
添加链表第一个节点
Size: 1 [(W (null)) -> 1 -> ((null))]
尾部追加一个节点
Size: 2 [(W (null)) -> 1 -> (3), (W 1) -> 3 -> ((null))]
插入到链表两个节点之间
Size: 3 [(W (null)) -> 1 -> (2), (W 1) -> 2 -> (3), (W 2) -> 3 -> ((null))]
插入到链表头部
Size: 4 [(W (null)) -> 0 -> (1), (W 0) -> 1 -> (2), (W 1) -> 2 -> (3), (W 2) -> 3 -> ((null))]
0 dealloc
删除头节点
Size: 3 [(W (null)) -> 1 -> (2), (W 1) -> 2 -> (3), (W 2) -> 3 -> ((null))]
2 dealloc
删除链表两个节点之间的节点
Size: 2 [(W (null)) -> 1 -> (3), (W 1) -> 3 -> ((null))]
3 dealloc
删除尾节点
Size: 1 [(W (null)) -> 1 -> ((null))]
删除链表唯一的节点
Size: 0 []
1 dealloc
复制代码