1、链表概述
链表是一种物理存储结构上非连续、非顺序的存储结构,数据元素的逻辑顺序是通过链表中的指针链接次序实现的。
顺序表的存储位置可以用一个简单直观的公式表示,它可以随机存取表中任意一个元素,但插入和删除需要移动大量元素。链式存储线性表时,不需要使用地址连续的存储单元,即不要求逻辑上相邻的元素在物理位置上也相邻,它通过“链”建立起元素之间的逻辑关系,因此插入和删除操作不需要移动元素,只需要修改指针,这也意味着链表失去了可随机存取的特点。
2、链表的分类
链表结构种类多样,可以按照是否带头、是否循环、单向或者双向大致分类。
(1)带头结点和不带头结点
(2)单向链表和双向链表
(3)循环链表和非循环链表
以上情况组合就有8中链表结构,但实际中应用最多的链表结构是无头单向非循环链表和带头双向循环链表。下面介绍两种链表的基本实现。
3、无头单向非循环链表的基本实现
为了建立数据元素之间的线性关系,链表结点除了存放数据,还需要存放一个指向其后继的指针。单链表可以解决顺序表需要大量连续存储单元的缺点。由于单链表的元素离散地分布在存储空间中,所以单链表时非随机存取的存储结构。
单链表的结点类型描述如下。
typedef int SLTDataType; typedef struct SListNode { SLTDataType data; struct SListNode* next; }SLTNode;
3.1单链表的打印
为了观感上更贴近单链表的定义,打印时先打印结点的值,“->”表示链表的指针,打印完所有元素后再打印“NULL”。
void SLTPrint(SLTNode* phead) { SLTNode* cur = phead; while (cur != NULL) { printf("%d->", cur->data); cur = cur->next; } printf("NULL\n"); }
3.2单链表的销毁
销毁时传入的是一级指针
void SLTDestroy(SLTNode* phead) { SLTNode* cur = phead; while (phead) { cur = phead; phead = phead->next; free(cur); cur = NULL; } printf("success\n"); }
3.3头插法插入结点
使用头插法插入新结点时,不用考虑链表是否为空,因为不涉及空指针的引用,直接插入即可。
void SLTPushFront(SLTNode** pphead, SLTDataType x) { assert(pphead); SLTNode* newnode = BuySLTNode(x); newnode->next = *pphead; *pphead = newnode; }
3.4尾插法插入结点
使用尾插法插入新结点时需要判断链表是否为空,因为插入过程中涉及到了空指针的引用。
当链表为空时,新结点即链表第一个结点;当链表不为空时,先找到链表的尾结点,然后直接将新结点插到尾结点后面。
void SLTPushBack(SLTNode** pphead, SLTDataType x) { assert(pphead); SLTNode* newnode = BuySLTNode(x); if (*pphead == NULL) *pphead = newnode; else { SLTNode* tail = *pphead; while (tail->next != NULL) { tail = tail->next; } tail->next = newnode; } }
3.5头删法删除结点
删除之前需要判断链表是否为空,链表不为空时,删除第一个结点。
void SLTPopFront(SLTNode** pphead) { assert(pphead); assert(*pphead); SLTNode* del = *pphead; *pphead = (*pphead)->next; free(del); del = NULL; }
3.6尾删法删除结点
删除之前同样需要判断链表是否为空,此外还需要判断链表是否只有一个结点,因为删除过程中涉及到空指针的引用。如果链表只有一个结点,直接将该结点删除释放;如果链表有多个元素,找到倒数第二个结点,然后删除该结点的后继。
void SLTPopBack(SLTNode** pphead) { assert(pphead); assert(*pphead); if ((*pphead)->next == NULL) { free(*pphead); *pphead = NULL; } else { SLTNode* tail = *pphead; while (tail->next->next != NULL) { tail = tail->next; } free(tail->next); tail->next = NULL; } }
3.7按值查找
从头开始依次对比,如果找到值为x的结点则返回该结点的地址,如果没找到则返回空指针。
SLTNode* SLTFind(SLTNode* phead, SLTDataType x) { assert(phead); SLTNode* cur = phead; while (cur) { if (cur->data == x) return cur; cur = cur->next; } return NULL; }
3.8在指定位置之前插入结点
插入分为链表只有一个结点和有多个结点这两种情况。当链表只有一个元素或者指定位置为第一个结点时,相当于头插法;当链表有多个元素或者指定位置为其他结点时,找到指定位置的前驱,然后修改其前驱的后继以及新结点的后继。
void SLTInsert(SLTNode** pphead, SLTNode* pos, SLTDataType x) { assert(pphead); assert(pos); if (pos == *pphead) SLTPushFront(pphead, x); else { SLTNode* posPrev = *pphead; SLTNode* newnode = BuySLTNode(x); while (posPrev->next != pos) { posPrev = posPrev->next; } posPrev->next = newnode; newnode->next = pos; } }
3.9在指定位置之后插入结点
在指定位置之后插入不用考虑链表有几个结点,直接插入即可。注意,要先修改新结点的后继,再修改指定位置结点的后继。
void SLTInsertAfter(SLTNode* pos, SLTDataType x) { assert(pos); SLTNode* newnode = BuySLTNode(x); newnode->next = pos->next; pos->next = newnode; }
3.10删除指定位置的结点
当链表只有一个元素或者指定位置为第一个结点时,相当于链表的头删;当链表有多个元素或者指定位置为其他结点时,找到指定位置结点的前驱,并修改其后继,再删除并释放指定位置结点。
void SLTErase(SLTNode** pphead, SLTNode* pos) { assert(pphead); assert(pos); if (pos == *pphead) SLTPopFront(pphead); else { SLTNode* posPrev = *pphead; while (posPrev->next != pos) { posPrev = posPrev->next; } posPrev->next = pos->next; free(pos); pos = NULL; } }
3.11删除指定位置之后的结点
在指定位置之后删除不用考虑链表有几个结点,直接删除即可。
void SLTEraseAfter(SLTNode* pos) { assert(pos); SLTNode* del = pos->next; pos->next = pos->next->next; free(del); del = NULL; }
4、带头双向循环链表的基本实现
单链表结点中只有一个指向其后继的指针,使得单链表只能从头结点依次顺序地向后遍历。要访问某个结点的前驱结点时,只能从头开始遍历,访问后继结点的时间复杂度为O(1),访问前驱结点的时间复杂度为O(n)。为了克服单链表的这些缺点,引入双链表,双链表中有两个指针prev和next,分别指向前驱结点和后继结点。
双链表中结点类型的描述如下:
typedef int LTDataType; typedef struct ListNode { LTDataType data; struct ListNode* prev; struct ListNode* next; }LNode;
双链表可以很方便找到前驱结点,因此,插入、删除操作的时间复杂度为O(1)。
此外,链表带头结点,无论链表是否为空,其头指针都是指向头结点的非空指针,因此在链表的插入和删除中空链表和非空链表的处理得到了统一。
4.1初始化双向链表
为了减少二级指针的使用,在初始化双链表时,使用一级指针定义并返回头结点。因此初始化时直接建立一个头结点并返回头结点的地址。
因为后续实现链表插入时还需要建立新结点,为了简化代码以及增强代码的可读性,定义一个建立新结点的函数,后续需要建立新结点时直接调用该函数即可。建立新结点函数如下。
LTNode* BuyLTNode(LTDataType x) { LTNode* newnode = (LTNode*)malloc(sizeof(LTNode)); if (newnode == NULL) { perror("malloc fail"); return NULL; } newnode->next = NULL; newnode->prev = NULL; newnode->data = x; return newnode; }
初始化双链表函数如下。
LTNode* Init() { LTNode* head = BuyLTNode(-1); head->next = head; head->prev = head; return head; }
4.2双向链表的打印
需要注意的是,打印时从phead的next开始打印。为了观感上更贴近双链表的定义,打印时先打印“guard”表示头结点,“<==>”表示链表的双指针,打印完所有元素后再打印一次“guard”表示尾结点的链接到头结点。
void LTPrint(LTNode* phead) { LTNode* cur = phead->next; printf("guard<==>"); while (cur != phead) { printf("%d<==>", cur->data); cur = cur->next; } printf("guard\n"); }
4.3双向链表的销毁
双链表的销毁同单链表的销毁类似,这里使用的仍是一级指针,所以需要用户在调用完销毁函数后,手动置空头结点。
void LTDestory(LTNode* phead) { LTNode* cur = phead->next; while (cur != phead) { LTNode* next = cur->next; free(cur); cur = next; } }
4.4头插法插入结点
在使用头插法插入新结点时,要注意指针修改顺序,指针顺序虽然不是唯一的,但也不是任意的。新结点前驱和后继的修改必须在头结点后继的修改之前进行,否则头结点的后继结点的指针就会丢掉,导致插入失败。如图所示,1和2必须在4之前进行。
void LTPushFront(LTNode* phead, LTDataType x) { assert(phead); LTNode* newnode = BuyLTNode(x); newnode->next = phead->next; phead->next->prev = newnode; newnode->prev = phead; phead->next = newnode; }
还有一种方法,可以不用考虑指针修改的先后顺序,就是重新定义一个结点存放头结点的后继结点,这样就不用担心头结点的后继指针丢失的问题了。
void LTPushFront(LTNode* phead, LTDataType x) { assert(phead); LTNode* newnode = BuyLTNode(x); LTNode* first = phead->next; newnode->prev = phead; phead->next = newnode; newnode->next = first; first->prev = newnode; }
4.5尾插法插入结点
使用尾插法时,不用像单链表那样遍历找尾,头结点的前驱结点就是尾结点。插入时同样需要注意指针的修改顺序,1和2必须要在3之前进行。
和头插法类似,重新定义一个结点保存头结点的前驱结点,就不用考虑指针修改的顺序了。
void LTPushBack(LTNode* phead, LTDataType x) { assert(phead); LTNode* newnode = BuyLTNode(x); LTNode* tail = phead->prev; tail->next = newnode; newnode->prev = tail; phead->prev = newnode; newnode->next = phead; }
4.6头删法删除结点
在删除之前需要将头结点的后继结点保存起来,否则无论在修改指针之前free后继结点还是在修改指针之后free后继结点都会出现错误。
注意,当链表为空时(只有头结点)时不能继续删除,所以在删除之前需要判断链表是否为空。
bool LTEmpty(LTNode* phead) { assert(phead); return phead->next == phead; }
void LTPopFront(LTNode* phead) { assert(phead); assert(!LTEmpty(phead)); LTNode* first = phead->next; phead->next = first->next; first->next->prev = phead; free(first); }
4.7尾删法删除结点
在删除之前需要将尾结点保存起来,为了方便,将尾结点的前驱结点也保存一下。与头删法同样,在删除前需要判断双链表是否为空,如果为空则不能继续删除。
void LTPopBack(LTNode* phead) { assert(phead); assert(!LTEmpty(phead)); LTNode* tail = phead->prev; LTNode* tailPrev = tail->prev; tailPrev->next = phead; phead->prev = tailPrev; free(tail); }
4.8按值查找
需要注意的是,从头结点的后继结点开始查找,并且停止查找的条件是当前结点不等于头结点。
LTNode* LTFind(LTNode* phead, LTDataType x) { LTNode* cur = phead->next; while (cur != phead) { if (cur->data == x) return cur; cur = cur->next; } return NULL; }
4.9在指定位置之前插入结点
在插入之前先保存pos的前驱结点,这样在插入时就不用考虑指针的修改顺序了。
void LTInsert(LTNode* pos, LTDataType x) { LTNode* newnode = BuyLTNode(x); LTNode* posPrev = pos->prev; posPrev->next = newnode; newnode->prev = posPrev; newnode->next = pos; pos->prev = newnode; }
头插法和尾插法插入结点可以通过该函数的复用实现。
//头插法 void LTPushFront(LTNode* phead, LTDataType x) { assert(phead); LTInsert(phead->next, x); } //尾插法 void LTPushBack(LTNode* phead, LTDataType x) { assert(phead); LTInsert(phead, x); }
注意,头插法传入的pos为头结点的后继结点;尾插法传入的pos为头结点,因为头结点的前驱结点就是尾结点。
4.10删除指定位置的结点
在删除之前先保存pos的前驱结点和后继结点。
void LTErase(LTNode* pos) { LTNode* posPrev = pos->prev; LTNode* posNext = pos->next; free(pos); posPrev->next = posNext; posNext->prev = posPrev; }
同样地,头删法和尾删法删除结点可以通过该函数的复用实现。
//头删法 void LTPopFront(LTNode* phead) { assert(phead); assert(!LTEmpty(phead)); LTErase(phead->next); } //尾删法 void LTPopBack(LTNode* phead) { assert(phead); assert(!LTEmpty(phead)); LTErase(phead->prev); }
注意,头删法传入的pos是头结点的后继结点;尾删法传入的pos是头结点的前驱结点。