数据结构3-单向链表

接着上一篇经过静态数组的扩容实现动态数组建立动态数组以后,这里再来建立经过单向链表实现一个动态数组。首先先来分析下动态数组的缺点,才可以了解到链表的意义。 首先回顾下以前动态数组添加和删除的过程:node

添加过程

动态数组添加元素的时候,最坏的状况是插入元素到数组的头部,则须要依次向后挪动因此元素,进行的操做数取决于当前元素的数量,复杂度为O(n),最好的状况是追加到数组的尾部,不须要挪动元素,复杂度为O(1)。平均复杂度为O(n)。扩容因为不是每次添加都须要的操做,只有在溢出的时候才须要扩容,扩容的时候复杂度为O(n),不扩容的时候为O(1),均摊复杂度依然为O(1)。git

删除过程

删除和添加基本相同,最好的状况删除队尾复杂度为O(1),最差的状况是删除队头,复杂度为O(n),平均复杂度为O(n)。github

而在根据index取值的时候,因为本质是经过index直接从数组中取值,数组中取值的复杂度为O(1),因此取元素的复杂度为O(1)。数组

注:从数组中值并不须要遍历,而是经过地址计算直接取值,复杂度为O(1)。bash

那么链表会不会比静态数组更快呢,下面咱们来实现一个自定义单向链表,而后比较一下就知道了。首先提示一下,单向链表可能没想象的那么快哦。数据结构

首先看一下数组和链表在内存中的不一样post

链表在内存中并非连续的,链表中每个存储的单元称为一个节点,在单向链表中,每个节点中存储两个值,一个是存放的指向存储元素的指针,另外一个是指向下一个节点的指针,这样链表就可以经过每个节点指向下一个节点的指针找到当前节点的下一个节点,只要获得链表的第一个节点,就可以访问到链表的因此节点,同时也就可以拿到链表的所有元素。

单向链表的节点结构

这样一看单向链表是否是很是的简单,在Objective-C语言中,就至关于每个节点对象有两个成员变量,一个是存值的,另外一个是存下一个节点对象的,下面就声明一个单向链表的节点对象:测试

#import <Foundation/Foundation.h>

NS_ASSUME_NONNULL_BEGIN

@interface JKRSingleLinkedListNode : NSObject

@property (nonatomic, strong, nullable) id object;
@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
复制代码

单向链表的结构

既然只须要拿到单向链表的头节点,就可以访问到所有节点,那么单向链表中须要存储成员变量只须要两个,一个是 _size,存储这链表的长度。另外一个是_first,保存链表的第一个节点。ui

注:_size存储在父类中,全部的接口声明也在父类中,父类的定义参见经过静态数组的扩容实现动态数组,所有源代码在文章结尾。atom

#import "JKRBaseList.h"
#import "JKRSingleLinkedListNode.h"

NS_ASSUME_NONNULL_BEGIN

@interface JKRSingleLinkedList : JKRBaseList {
    // NSUInteger _size; 
    JKRSingleLinkedListNode *_first;
}

@end

NS_ASSUME_NONNULL_END
复制代码

下面是完整的单向链表的内存结构图,能够更加直接的了解单向链表的结构:

经过index查找节点

上面的链表结构图能够看到,链表对象保存这链表的长度和链表的头节点,若是要经过index得到具体的某一个节点,须要从头节点开始逐一经过next指针日后查找,直到找到第index个节点。

时间复杂度取决于index,取index为0的节点只须要访问头节点,只须要1次访问。访问尾节点须要从头节点一直访问到尾节点,访问次数取决于节点数量。综上平均时间复杂度为O(n)。

- (JKRSingleLinkedListNode *)nodeWithIndex:(NSInteger)index {
    [self rangeCheckForExceptAdd:index];
    JKRSingleLinkedListNode *node = _first;
    for (NSInteger i = 0; i < index; i++) {
        node = node.next;
    }
    return node;
}
复制代码

经过index取值

上面已经实现了拿到index位置的节点,这里只须要调用方法获取节点,而后返回节点存储的值就能够了。 时间复杂度同查找节点:O(n)

- (id)objectAtIndex:(NSUInteger)index {
    return [self nodeWithIndex:index].object;
}
复制代码

添加节点

在链表中间插入节点

在链表中间插入节点,以下图,链表中存在三个节点,此时咱们须要在链表index为1的位置插入一个节点:

此时须要作的就是让index为0的节点的next指向新节点,并让新节点的next指向原来index为1的节点:

这样插入就成功的将一个节点插入到链表的两个节点中:

在链表头部插入节点

在链表头部插入节点以下图:

这时须要将新节点的next指向原来链表的_first,并将链表的_first指向新节点:

在链表头部成功插入节点后链表的结构:

链表添加的节点的代码实现

综上步骤,链表添加节点两种状况的代码实现以下:

- (void)insertObject:(id)anObject atIndex:(NSUInteger)index {
    [self rangeCheckForAdd:index];
    
    if (index == 0) {
        JKRSingleLinkedListNode *node = [[JKRSingleLinkedListNode alloc] initWithObject:anObject next:_first];
        _first = node;
    } else {
        JKRSingleLinkedListNode *prev = [self nodeWithIndex:index - 1];
        JKRSingleLinkedListNode *node = [[JKRSingleLinkedListNode alloc] initWithObject:anObject next:prev.next];
        prev.next = node;
    }
    
    _size++;
}
复制代码

添加时index的越界检查有动态数组的父类统一实现。

由于添加节点时,虽然插入只须要1次操做,可是涉及到查找index位置的节点,这个查找的复杂度为O(n),因此添加节点的复杂度为O(n)。

删除节点

在链表中间删除节点

假设删除链表index为1的节点,以下图:

只须要将被删除节点的前一个节点的next指针改变指向,指向被删除节点的下一个节点,那么被删除节点因为没有引用,就会自动被回收:

删除后链表的结构:

删除链表头节点

删除链表的头节点以下图:

只须要将单向链表的_first指针指向原来头节点下一个节点便可:

删除后链表的结构:

链表删除节点的代码实现

综上两种状况,链表删除的代码以下:

- (void)removeObjectAtIndex:(NSUInteger)index {
    [self rangeCheckForExceptAdd:index];
    
    JKRSingleLinkedListNode *node = _first;
    if (index == 0) {
        _first = _first.next;
    } else {
        JKRSingleLinkedListNode *prev = [self nodeWithIndex:index - 1];
        node = prev.next;
        prev.next = node.next;
    }
    _size--;
}
复制代码

同添加节点的操做,虽然删除节点只须要1次操做,可是涉及到查找index位置的节点,这个查找的复杂度为O(n),因此删除节点的复杂度也是为O(n)。

至于其余功能都是基于上面几个接口调用实现的,好比尾部追加、删除头节点等,就不一一列举了,最后源码中都有。

和动态数组对比时间复杂度

数据结构 动态数组 单向链表
详细分类 最好 最差 平均 最好 最差 平均
插入任意位置元素 O(1) O(n) O(n) O(1) O(n) O(n)
删除任意位置元素 O(1) O(n) O(n) O(1) O(n) O(n)
替换任意位置元素 O(1) O(1) O(1) O(1) O(n) O(n)
查找任意位置元素 O(1) O(1) O(1) O(1) O(n) O(n)
添加元素到尾部 O(1) O(n) O(1) O(n) O(n) O(n)
删除尾部元素 O(1) O(1) O(1) O(n) O(n) O(n)
添加元素到头部 O(n) O(n) O(n) O(1) O(1) O(1)
删除头部元素 O(n) O(n) O(n) O(1) O(1) O(1)

上面是总结出来的时间复杂度对比:

  • 插入任意位置元素:动态数组须要挪动元素,且越靠近头部挪动次数越多。单向链表须要找到对于的节点进行指针操做,且越靠近尾部查找次数越多。
  • 删除任意位置元素:动态数组须要挪动元素,且越靠近头部挪动次数越多。单向链表须要找到对于的节点进行指针操做,且越靠近尾部查找次数越多。
  • 替换任意位置元素:动态数组直接经过index取对于位置的值,操做数稳定为1。单向链表须要找到对于的节点进行指针操做,且越靠近尾部查找次数越多。
  • 查找任意位置元素:动态数组直接经过index取对于位置的值,操做数稳定为1。单向链表须要找到对于的节点进行指针操做,且越靠近尾部查找次数越多。
  • 添加元素到尾部:动态数组尾部添加直接在数组尾部对应的index添加值便可,虽然可能有扩容操做,可是均摊下来依旧是O(1)。单向链表须要从头节点一直找到尾节点,为O(n)。
  • 删除尾部元素:动态数组尾部添加直接在数组尾部对应的index添加值便可,固定1次操做。单向链表须要从头节点一直找到尾节点,为O(n)。
  • 添加元素到头部:动态数组须要最大的挪动元素次数,为O(n)。单向链表只须要1次操做。
  • 删除头部元素:动态数组须要最大的挪动元素次数,为O(n)。单向链表只须要1次操做。

由上面的分析能够直到,单向链表并非全部状况下时间复杂度都优于动态数组的,当须要频发的删除和添加到数组头部时,单向链表优于动态数组。当须要频发的删除和添加到数组尾部时,动态数组优于单向链表。

下面测试一下:

进行10000次头部的插入和删除操做,动态数组和单向链表对比:

进行10000次尾部的插入和删除操做,动态数组和单向链表对比:

因此并非说单向链表就必定时间复杂度上优于动态数组,依然要区分在不一样的应用场景。若是须要频繁的对数组头部进行插入和删除操做,单向链表是大大优于动态数组的。若是是须要频繁的在数组尾部进行插入和删除操做,动态数组又是大大优于单向链表的。

接下来

单向循环链表是基于单向链表的结构作了功能的扩展,有了单向链表的基础,下面就能够更容易理解单向循环链表的实现了。

源码

点击查看源码