数据结构与算法:链表

1,207 阅读10分钟

1. 前言

本篇是「数据结构与算法系列」的第2篇,主题是链表

链表是计算机中比较基础的一种数据结构,它对应的是数据在计算机上的链式存储。常见的有:单链表双链表

下面进入正文。

2. 链表

2.1 基础概念

链表是由一系列节点(Node,也叫结点)组成,节点是链表的基本元素。每个节点包括两个部分:一个是存储数据元素的数据域,另一个是存储下一个节点地址的指针域,节点与节点之间是通过指针进行联系的(所以称链表是链式存储的)。

一个简单的节点结构如下:

typedef struct ListNode {
    id element; // 数据域
    struct ListNode *next; // 指针域
} ListNode;

由一系列上述节点组成的链表,就是单链表,也就是我们常说的链表。如果在这个节点的基础上再加个指向前一个节点的指针,如下所示:

typedef struct DoublyListNode {
    id element; // 数据域
    struct ListNode *prev; // 指针域
    struct ListNode *next; // 指针域
} DoublyListNode;

由这种节点组成的链表,就是双链表

我们通常把next指针(指向下一个节点)称为后继指针(由后继指针指向的节点叫做后继节点),把prev指针(指向前一个节点)称为前驱指针(由前驱指针指向的节点叫做前驱节点)。

总的来说,双链表的节点比单链表的节点多了个前驱指针

无论是单链表,还是双链表,它们的共性是:

  • 链表的第1个真正意义上的节点被称为首元节点,最后1个节点被称为尾节点
  • 尾节点的next指针指向的是NULL,其他节点的next指针指向的是当前节点的后继节点;
    • 特别的,双链表的首元节点的prev指针指向的是NULL
  • 可以有头节点(Head),也可以没有,区别就是:
    • 如果有头节点,则链表的头指针指向的是头节点头节点next指针指向的是首元节点
    • 如果没有头节点,则链表的头指针指向的是首元节点

关于头节点,补充说明如下:

  1. 头节点的数据域可以不存放任何数据,也可以存放链表的长度;
  2. 头节点的作用是使所有链表(包括空表)的头指针非空,并使对链表的插入、删除操作不需要区分是否为空表或是否在第一个位置进行,从而与其他位置的插入、删除操作一致。——摘自《百度百科:头结点》

2.2 循环链表

如果单链表的尾节点的next指针指向的是首元节点,构成一个完整的循环,就形成了单向循环链表;如果双链表的尾节点的next指针指向首元节点,同时首元节点的prev指针指向尾节点,就形成了双向循环链表

2.3 链表的实现

由于单链表类双链表类的相应操作类似,实现思路也类似,而后者较前者稍微复杂一下,本文只介绍双链表类(DoublyLinkedList)的具体实现方案。

另外,博主窃以为头节点的意义不是很大,只需要做好相应的逻辑判断即可,因此本文的链表实现方案不会加入头节点。

2.3.1 节点设计

首先是节点,如下:

#define ELEMENT_NOT_FOUND -1L

// 双链表的节点
typedef struct DoublyListNode {
    id element;
    struct DoublyListNode *prev;
    struct DoublyListNode *next;
} DoublyListNode;

static inline DoublyListNode*
DoublyListNodeMake(id element, struct DoublyListNode *prev, struct DoublyListNode *next)
{
    DoublyListNode *node = malloc(sizeof(DoublyListNode));
    if (node != NULL) {
        node->element = element;
        node->prev = prev;
        node->next = next;
    }
    return node;
}

2.3.2 接口设计

我们用链表是为了存储数据,因此对链表的要求是其必须满足对数据的增删改查等相关操作,由此可设计接口如下:

@protocol LinkedListProtocol <NSObject>

@required

/// 元素的数量
- (NSUInteger)size;

/// 是否为空
- (BOOL)isEmpty;

/// 是否包含
- (BOOL)contains:(id)element;

/// 添加元素到尾部
- (void)add:(id)element;

/// 添加元素到指定位置
- (void)add:(id)element atIndex:(NSInteger)index;

/// 设置index位置的元素
- (id)set:(id)element atIndex:(NSInteger)index;

/// 获取index位置的元素
- (id)get:(NSInteger)index;

/// 查看元素的索引
- (NSInteger)indexOf:(id)element;

/// 删除index位置的元素
- (id)remove:(NSInteger)index;

/// 删除某元素
- (BOOL)removeElement:(id)element;

/// 清空所有元素
- (void)clear;

@end

2.3.3 类设计

协议接口设计好后,再创建DoublyLinkedList类,令其遵循协议即可。

  1. DoublyLinkedList.h:遵循LinkedListProtocol
@interface DoublyLinkedList : NSObject <LinkedListProtocol>

@end
  1. DoublyLinkedList.m的设计思路如下:

1). 为了减少可能的循环次数,这里引入_first_last两个成员变量,分别指向首元节点、尾节点;同时用成员变量_size标记当前链表的长度。

@implementation DoublyLinkedList {
    @private
    NSUInteger _size;
    DoublyListNode *_first;
    DoublyListNode *_last;
}

- (instancetype)init
{
    self = [super init];
    if (self) {
        _size = 0;
        _first = NULL;
        _last = NULL;
    }
    return self;
}

2). 增加几个私有方法,分别是越界异常处理、获取指定位置的节点

#pragma mark - Private

- (void)rangeCheck:(NSInteger)index
{
    if (index < 0 || index >= _size) {
        [self outofBounds:index];
    }
}

- (void)rangeCheckForAdd:(NSInteger)index
{
    if (index < 0 || index > _size) {
        [self outofBounds:index];
    }
}

- (void)outofBounds:(NSInteger)index
{
    NSString *msg = [NSString stringWithFormat:@"out of bounds: index %lld beyond bounds [0 .. %lld]", (long long)index, (long long)_size];
    @throw [NSException exceptionWithName:NSRangeException reason:msg userInfo:nil];
}

/// 获取index位置的节点
- (DoublyListNode *)getNode:(NSInteger)index
{
    [self rangeCheck:index];
    
    if (index < (_size >> 1)) {// 前遍历
        DoublyListNode *node = _first;
        for (NSInteger i = 0; i < index; i++) {
            node = node->next;
        }
        return node;
    } else {// 后遍历
        DoublyListNode *node = _last;
        for (NSInteger i = _size - 1; i > index; i--) {
            node = node->prev;
        }
        return node;
    }
}

3). LinkedListProtocol接口实现

#pragma mark - LinkedListProtocol

/// 元素的数量
- (NSUInteger)size
{
    return _size;
}

/// 是否为空
- (BOOL)isEmpty
{
    return _size == 0;
}

/// 是否包含
- (BOOL)contains:(id)element
{
    return [self indexOf:element] != ELEMENT_NOT_FOUND;
}

/// 添加元素到尾部
- (void)add:(id)element
{
    [self add:element atIndex:_size];
}

/// 添加元素到指定位置
- (void)add:(id)element atIndex:(NSInteger)index
{
    [self rangeCheckForAdd:index];
    
    if (index == _size) { // 往最后面添加
        DoublyListNode *oldLast = _last;
        _last = DoublyListNodeMake(element, oldLast, NULL);
        if (oldLast == NULL) { // 这是链表的第1个节点
            _first = _last;
        } else {
            oldLast->next = _last;
        }
    } else {
        DoublyListNode *next = [self getNode:index];
        DoublyListNode *prev = next->prev;
        DoublyListNode *node = DoublyListNodeMake(element, prev, next);
        next->prev = node;
        if (prev == NULL) { // index == 0
            _first = node;
        } else {
            prev->next = node;
        }
    }
    _size++;
}

/// 设置index位置的元素
- (id)set:(id)element atIndex:(NSInteger)index
{
    DoublyListNode *node = [self getNode:index];
    id old = node->element;
    node->element = element;
    return old;
}

/// 获取index位置的元素
- (id)get:(NSInteger)index
{
    return [self getNode:index]->element;
}

/// 查看元素的索引
- (NSInteger)indexOf:(id)element
{
    if (element == nil) {
        DoublyListNode *node = _first;
        for (NSInteger i = 0; i < _size; i++) {
            if (node->element == nil) return i;
            node = node->next;
        }
    } else {
        DoublyListNode *node = _first;
        for (NSInteger i = 0; i < _size; i++) {
            if ([element isEqualTo:node->element]) return i;
            node = node->next;
        }
    }
    return ELEMENT_NOT_FOUND;
}

/// 删除index位置的元素
- (id)remove:(NSInteger)index
{
    DoublyListNode *node = [self getNode:index];
    DoublyListNode *prev = node->prev;
    DoublyListNode *next = node->next;
    
    if (prev == NULL) { // index == 0
        _first = next;
    } else {
        prev->next = next;
    }
    
    if (next == NULL) { // index == size - 1
        _last = prev;
    } else {
        next->prev = prev;
    }
    
    _size--;
    id element = node->element;
    free(node);
    return element;
}

/// 删除某元素
- (BOOL)removeElement:(id)element
{
    NSInteger index = [self indexOf:element];
    if (index != ELEMENT_NOT_FOUND) {
        [self remove:index];
        return YES;
    }
    return NO;
}

/// 清空所有元素
- (void)clear
{
    if (_size == 0) {
        return;
    }
    DoublyListNode *node = _first;
    for (NSInteger i = 0; i < _size; i++) {
        DoublyListNode *next = node->next;
        free(node);
        node = next;
    }
    _size = 0;
    _first = NULL;
    _last = NULL;
}

4). 关于打印,可以通过重写类的description方法

/// 打印所有元素
- (NSString *)description
{
    if ([self isEmpty]) {
        return @"size:0, items:[]";
    }
    DoublyListNode *node = _first;
    NSMutableString *string = [[NSMutableString alloc] initWithFormat:@"size:%lu, items:[", (unsigned long)_size];
    for (NSInteger i = 0; i < _size; i++) {
        if (i != 0) {
            [string appendString:@", "];
        }
        if (node->element == nil) {
            [string appendString:@"nil"];
        } else {
            [string appendString:[node->element description]];
        }
        node = node->next;
    }
    [string appendString:@"]"];
    return string.copy;
}

以上就是双链表类(DoublyLinkedList)的实现方案,下面跑一下测试代码。

2.3.4 代码测试

为了方便后续的代码测试(刷题等),博主创建了一个DoublyLinkedListDemo类,只需要在main.m中调用相关测试方法即可

/// 测试双链表
+ (void)testDoublyLinkedList
{
    DoublyLinkedList *list = [[DoublyLinkedList alloc] init];
    [list add:@11];
    [list add:@22];
    [list add:@33];
    [list add:@44];
    [list add:@55 atIndex:0];           // [55, 11, 22, 33, 44]
    [list add:@66 atIndex:2];           // [55, 11, 66, 22, 33, 44]
    [list add:@77 atIndex:list.size];   // [55, 11, 66, 22, 33, 44, 77]
    NSLog(@"%@", list.description);
    
    [list remove:0];                    // [11, 66, 22, 33, 44, 77]
    [list remove:(list.size - 1)];      // [11, 66, 22, 33, 44]
    [list removeElement:@33];           // [11, 66, 22, 44]
    NSLog(@"%@", list.description);
}

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        [DoublyLinkedListDemo testDoublyLinkedList];
    }
    return 0;
}

测试结果如下:

image.png

显然测试通过!

说明:双链表的实现方案有很多种,欢迎评论区留言,大家和谐讨论~

2.4 头插法与尾插法

头插法指的是每次数据的插入位置(index)都是0尾插法则是在链表的尾部插入。

不难得出,头插法会导致链表的元素顺序与添加顺序相反,即逆序;尾插法的结果则是正序的。针对不同的场景,可以选择不同的插入法。

3. 练习

leetcode 上有一些链表相关的题目可以帮助大家熟悉链表,本文会选取一二讲解。

在此之前,先介绍一下双指针:通俗地讲就是两个指针。

在一些leetcode的题目中(包括但不限于链表),应用双指针可以让解题变得简单明了,如

3.1 环形链表

本题来自 leetcode_141_环形链表

3.1.1 问题描述

给定一个链表,判断链表中是否有环。

如果链表中有某个节点,可以通过连续跟踪next指针再次到达,则链表中存在环。如果链表中存在环,则返回true;否则,返回false

要求:O(1)(即常量)的内存解决问题。

3.1.2 解题思路

本方法需要读者对Floyd 判圈算法(又称龟兔赛跑算法)有所了解。

假想「乌龟」和「兔子」在链表上移动,「兔子」跑得快,「乌龟」跑得慢。当「乌龟」和「兔子」从链表上的同一个节点开始移动时,如果该链表中没有环,那么「兔子」将一直处于「乌龟」的前方;如果该链表中有环,那么「兔子」会先于「乌龟」进入环,并且一直在环内移动。等到「乌龟」进入环时,由于「兔子」的速度快,它一定会在某个时刻与乌龟相遇,即套了「乌龟」若干圈。

我们可以根据上述思路来解决本题。具体地,我们定义两个指针,一快一满。慢指针每次只移动一步,而快指针每次移动两步。初始时,慢指针在位置head,而快指针在位置head.next。这样一来,如果在移动的过程中,快指针反过来追上慢指针,就说明该链表为环形链表。否则快指针将到达链表尾部,该链表不为环形链表。

以上分析来自leetcode

3.1.3 具体实现

  1. 首先是节点代码
// 单链表的节点
typedef struct ListNode {
    int val;
    struct ListNode *next;
} ListNode;

static inline ListNode*
ListNodeMake(int val, ListNode *next)
{
    ListNode *node = malloc(sizeof(ListNode));
    if (node != NULL) {
        node->val = val;
        node->next = next;
    }
    return node;
}
  1. 实现代码如下
+ (BOOL)hasCycle:(ListNode *)head
{
    if (head == NULL || head->next == NULL) {
        return NO;
    }
    ListNode *slow = head;
    ListNode *fast = head->next;
    while (slow != fast) {
        if (fast == NULL || fast->next == NULL) {
            return NO;
        }
        slow = slow->next;
        fast = fast->next->next;
    }
    return YES;
}

3.1.4 代码测试

我们分别用无环链表环形链表进行测试:

+ (void)testHasCycle
{
    ListNode *head = ListNodeMake(10, NULL);
    ListNode *cur = head;
    for (int i = 1; i < 10; i++) {
        ListNode *node = ListNodeMake(10 + i, NULL);
        cur->next = node;
        cur = node;
    }
    // 最后一个节点的next指向NULL,此时无环!
    NSLog(@"是否有环:%@", [DoublyLinkedListDemo hasCycle:head] ? @"true" : @"false");
    
    // 最后一个节点的next指向第3个节点,因而有环!
    cur->next = head->next->next;
    NSLog(@"是否有环:%@", [DoublyLinkedListDemo hasCycle:head] ? @"true" : @"false");
}

结果如下:

image.png

4 友情链接

5. 补充说明

  • 原文链接,转载请注明出处!
  • 文中所有代码都已上传到 github,感兴趣的同学可以去查看。