双向链表

2,989 阅读15分钟

双向链表的结构

双向链表和单向链表的对比

前面已经完成了单向链表的实现,双向链表相比单向链表,有以下的不同:

  • 节点:单向链表每一个节点有一个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

双向链表的时间复杂度

查找节点

因为对节点的添加和删除操作需要查找对于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;
    }
}

当index == 0 或 index == _size - 1时,即查找头节点和尾节点时,直接就可以一次查找就拿到需要的节点。越靠近头节点或尾节点,查找需要遍历的次数越小,最坏的情况就是查找中间的节点,需要 n / 2 次查找。

添加和删除

- (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和next操作都是相同的,区别就是通过index查找节点,这取决于上面查找节点的复杂度。因此可以推出结论,双向链表删除头节点、尾节点都是O(1),越靠近头节点或尾节点操作时间越快,越靠近链表中间越慢。平均复杂度为O(n),而删除同添加。

双向链表时间复杂度

情况分类 添加 删除
最好 头节点 O(1) O(1)
最好 尾节点 O(1) O(1)
平均 O(n) O(n)

单向链表和双向链表时间复杂度对比

单向链表原理和实现中已经和动态数组相比对比并分析了单向链表的时间复杂度:

情况分类 添加 删除
最好 头节点 O(1) O(1)
最差 尾节点 O(n) O(n)
平均 O(n) O(n)

由上面的结论可以知道,单向链表和双向链表在操作头节点、链表的前半部分、链表的中间节点时都是一样的。而双向链表在操作尾节点、链表后半部分是大大优于单向链表的。

下面是一段对比测试,分别对比了单向链表和双向链表在对头节点、尾节点、前半部分节点、后半部分节点、中间节点这五种情况下的对比:

void compareSingleLinkedListAndLinkedList() {
    NSUInteger testCount = 50000;
    
    [JKRTimeTool teskCodeWithBlock:^{
        JKRBaseList *array = [JKRSingleLinkedList new];
        for (NSUInteger i = 0; i < testCount; i++) {
            [array insertObject:[NSNumber numberWithInteger:i] atIndex:0];
        }
        for (NSUInteger i = 0; i < testCount; i++) {
            [array removeFirstObject];
        }
        NSLog(@"单向链表操作头节点");
    }];
    [JKRTimeTool teskCodeWithBlock:^{
        JKRBaseList *array = [JKRLinkedList new];
        for (NSUInteger i = 0; i < testCount; i++) {
            [array insertObject:[NSNumber numberWithInteger:i] atIndex:0];
        }
        for (NSUInteger i = 0; i < testCount; i++) {
            [array removeFirstObject];
        }
        NSLog(@"双向链表操作头节点");
    }];
    
    
    [JKRTimeTool teskCodeWithBlock:^{
        JKRBaseList *array = [JKRSingleLinkedList new];
        for (NSUInteger i = 0; i < testCount; i++) {
            [array addObject:[NSNumber numberWithInteger:i]];
        }
        for (NSUInteger i = 0; i < testCount; i++) {
            [array removeLastObject];
        }
        NSLog(@"单向链表操作尾节点");
    }];
    [JKRTimeTool teskCodeWithBlock:^{
        JKRBaseList *array = [JKRLinkedList new];
        for (NSUInteger i = 0; i < testCount; i++) {
            [array addObject:[NSNumber numberWithInteger:i]];
        }
        for (NSUInteger i = 0; i < testCount; i++) {
            [array removeLastObject];
        }
        NSLog(@"双向链表操作尾节点");
    }];
    
    
    [JKRTimeTool teskCodeWithBlock:^{
        JKRBaseList *array = [JKRSingleLinkedList new];
        for (NSUInteger i = 0; i < testCount; i++) {
            [array insertObject:[NSNumber numberWithInteger:i] atIndex:array.count >> 2];
        }
        for (NSUInteger i = 0; i < testCount; i++) {
            [array removeObjectAtIndex:array.count >> 2];
        }
        NSLog(@"单向链表操作 index = 总节点数*0.25 节点");
    }];
    [JKRTimeTool teskCodeWithBlock:^{
        JKRBaseList *array = [JKRLinkedList new];
        for (NSUInteger i = 0; i < testCount; i++) {
            [array insertObject:[NSNumber numberWithInteger:i] atIndex:array.count >> 2];
        }
        for (NSUInteger i = 0; i < testCount; i++) {
            [array removeObjectAtIndex:array.count >> 2];
        }
        NSLog(@"双向链表操作 index = 总节点数*0.25 节点");
    }];
    
    
    [JKRTimeTool teskCodeWithBlock:^{
        JKRBaseList *array = [JKRSingleLinkedList new];
        for (NSUInteger i = 0; i < testCount; i++) {
            [array insertObject:[NSNumber numberWithInteger:i] atIndex:array.count * 0.75];
        }
        for (NSUInteger i = 0; i < testCount; i++) {
            [array removeObjectAtIndex:array.count * 0.75];
        }
        NSLog(@"单向链表操作 index = 总节点数*0.75 节点");
    }];
    [JKRTimeTool teskCodeWithBlock:^{
        JKRBaseList *array = [JKRLinkedList new];
        for (NSUInteger i = 0; i < testCount; i++) {
            [array insertObject:[NSNumber numberWithInteger:i] atIndex:array.count * 0.75];
        }
        for (NSUInteger i = 0; i < testCount; i++) {
            [array removeObjectAtIndex:array.count * 0.75];
        }
        NSLog(@"双向链表操作 index = 总节点数*0.75 节点");
    }];
    
    
    [JKRTimeTool teskCodeWithBlock:^{
        JKRBaseList *array = [JKRSingleLinkedList new];
        for (NSUInteger i = 0; i < testCount; i++) {
            [array insertObject:[NSNumber numberWithInteger:i] atIndex:array.count >> 1];
        }
        for (NSUInteger i = 0; i < testCount; i++) {
            [array removeObjectAtIndex:array.count >> 1];
        }
        NSLog(@"单向链表操作中间节点");
    }];
    [JKRTimeTool teskCodeWithBlock:^{
        JKRBaseList *array = [JKRLinkedList new];
        for (NSUInteger i = 0; i < testCount; i++) {
            [array insertObject:[NSNumber numberWithInteger:i] atIndex:array.count >> 1];
        }
        for (NSUInteger i = 0; i < testCount; i++) {
            [array removeObjectAtIndex:array.count >> 1];
        }
        NSLog(@"双向链表操作中间节点");
    }];
}

打印结果:

单向链表操作头节点
耗时: 0.020 s
双向链表操作头节点
耗时: 0.038 s

单向链表操作尾节点
耗时: 49.085 s
双向链表操作尾节点
耗时: 0.033 s

单向链表操作 index = 总节点数*0.25 节点
耗时: 12.144 s
双向链表操作 ndex = 总节点数*0.25 节点
耗时: 12.215 s

单向链表操作 index = 总节点数*0.75 节点
耗时: 35.735 s
双向链表操作 index = 总节点数*0.75 节点
耗时: 18.488 s

单向链表操作中间节点
耗时: 23.856 s
双向链表操作中间节点
耗时: 36.420 s

上面的打印可以看到,单向链表在操作头节点、尾节点、中间节点、前半部分节点时,总体是优于双向链表,这也是由于单向链表对节点操作次数少于双向链表,因为它不用操作节点指向前一个节点的指针。但是当操作后半部分节点、尾节点时,单向链表的效率就会大大低于双向链表。

所以,当只需要对数据的头部或者前半部分进行操作时,单向链表和双向链表时间复杂度一致,并且单向链表更加精简高效。但是如果需要对数据的头部、尾部都要进行操作时,双向链表大大优于单向链表。

接下来

既然单向链表有对应的单向循环链表,那么双向链表也可以做成循环链表,下面就介绍双向循环链表

源码

点击查看源码