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指针指向的是首元节点; - 如果没有
头节点,则链表的头指针指向的是首元节点
- 如果有
关于
头节点,补充说明如下:
头节点的数据域可以不存放任何数据,也可以存放链表的长度;头节点的作用是使所有链表(包括空表)的头指针非空,并使对链表的插入、删除操作不需要区分是否为空表或是否在第一个位置进行,从而与其他位置的插入、删除操作一致。——摘自《百度百科:头结点》
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类,令其遵循协议即可。
DoublyLinkedList.h:遵循LinkedListProtocol
@interface DoublyLinkedList : NSObject <LinkedListProtocol>
@end
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;
}
测试结果如下:
显然测试通过!
说明:
双链表的实现方案有很多种,欢迎评论区留言,大家和谐讨论~
2.4 头插法与尾插法
头插法指的是每次数据的插入位置(index)都是0;尾插法则是在链表的尾部插入。
不难得出,头插法会导致链表的元素顺序与添加顺序相反,即逆序;尾插法的结果则是正序的。针对不同的场景,可以选择不同的插入法。
3. 练习
leetcode 上有一些链表相关的题目可以帮助大家熟悉链表,本文会选取一二讲解。
在此之前,先介绍一下双指针:通俗地讲就是两个指针。
在一些leetcode的题目中(包括但不限于链表),应用双指针可以让解题变得简单明了,如
- 在 leetcode_141_环形链表 中,应用
快慢指针可以做到O(1)级别的空间复杂度、O(n)级别的时间复杂度; - 在 leetcode_125_验证回文串 中,应用
头尾指针从双端往中间靠拢遍历,也可以简化解题思路。
3.1 环形链表
本题来自 leetcode_141_环形链表
3.1.1 问题描述
给定一个链表,判断链表中是否有环。
如果链表中有某个节点,可以通过连续跟踪next指针再次到达,则链表中存在环。如果链表中存在环,则返回true;否则,返回false。
要求:O(1)(即常量)的内存解决问题。
3.1.2 解题思路
本方法需要读者对Floyd 判圈算法(又称龟兔赛跑算法)有所了解。
假想「乌龟」和「兔子」在链表上移动,「兔子」跑得快,「乌龟」跑得慢。当「乌龟」和「兔子」从链表上的同一个节点开始移动时,如果该链表中没有环,那么「兔子」将一直处于「乌龟」的前方;如果该链表中有环,那么「兔子」会先于「乌龟」进入环,并且一直在环内移动。等到「乌龟」进入环时,由于「兔子」的速度快,它一定会在某个时刻与乌龟相遇,即套了「乌龟」若干圈。
我们可以根据上述思路来解决本题。具体地,我们定义两个指针,一快一满。慢指针每次只移动一步,而快指针每次移动两步。初始时,慢指针在位置head,而快指针在位置head.next。这样一来,如果在移动的过程中,快指针反过来追上慢指针,就说明该链表为环形链表。否则快指针将到达链表尾部,该链表不为环形链表。
以上分析来自
leetcode
3.1.3 具体实现
- 首先是节点代码
// 单链表的节点
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;
}
- 实现代码如下
+ (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");
}
结果如下: