『初阶数据结构 • C语言』⑨ - 基于结点的数据结构——链表(单链表&&双向循环链表)附完整源码
本章内容
1.什么是链表
2.链表常见几种形式
3.无头单向非循环链表的实现
3.1结点结构的定义
3.2函数接口的实现
3.2.1尾插
3.2.2尾删
4. 带头双向循环链表的实现
4.1结点结构的定义
4.2函数接口的实现
5.两种链表的差异
①尾插与尾删的时间复杂度
②头插与头删的时间复杂度
③函数形参为何一个是二级指针,一个是一级指针?
完整源码
无头单向非循环链表
SList.h
SList.c
test.c
带头双向循环链表
List.h
List.c
test.c
1.什么是链表
像数组一样,链表也用来表示一系列的元素。事实上,能用数组来做的事情,一般也可以用链表来做。然而,链表的实现跟数组是不一样的,在不同场景它们会有不同的性能表现。
计算机的内存就像一大堆格子,每格都可以用来保存比特形式的数据。当要创建数组时,程序会在内存中找出一组连续的空格子,给它们起个名字,以便你的应用存放数据。
我们之前说过,计算机能够直接跳到数组的某一索引上。如果代码要求它读取索引 4的值,那么计算机只需一步就可以完成任务。重申一次,之所以能够这样,是因为程序事先知道了数组开头所在的内存地址——例如地址是 1000——当它想去索引 4时,便会自动跳到 1004处。
与数组不同的是,组成链表的格子不是连续的。它们可以分布在内存的各个地方。这种不相邻的格子,就叫作结点。
那么问题来了,计算机怎么知道这些分散的结点里,哪些属于这个链表,哪些属于其他链表呢?这就是链表的关键了:每个结点除了保存数据,它还保存着链表里的下一结点的内存地址。
这份用来指示下一结点的内存地址的额外数据,被称为链。链表如下图所示。
注意:
1.从上图可以看出,链式结构在逻辑上是来连续的,但是在物理上不一定连续。
2.现实中的结点一般都是从堆上来申请的。
3.从堆上申请的空间,是按照一定的策略来分配的,两次申请的空间可能连续,也可能不连续。
2.链表常见几种形式
①单向或者双向
②带头或者不带头
虽然有这么多的链表的结构,但是我们实际中最常用到的还是这两种结构:
1. 无头单向非循环链表
结构简单,一般不会用来单独进行存储数据。实际中更多是作为其它数据结构的子结构,如哈希表、图的邻接表等等。另外这种结构在笔试面试中出现比较多。
2. 带头双向循环链表
结构最复杂,一般用于单独存储数据。实际中使用的链表数据结构,都是带头双向循环链表。另外这个结构虽然结构复杂,但是使用代码实现以后发现结构会带来很多优势,实现反而简单了。
3.无头单向非循环链表的实现
就我个人而言,我认为虽然无头单向非循环链表是这几个链表中结构最简单的,但是实现却是最复杂的。我们学会了头单向非循环链表的实现,别的链表实现应当不在话下。
3.1结点结构的定义
typedef int SLTDataType; typedef struct SListNode { SLTDataType data; struct SListNode* next; }SLTNode;
如上文所讲,一个结点包含的内容有数据data和保存下一个结点的地址的指针。
链表正式由一个个这样的结点串连起来的,所以我们只需要记住排在最前面的头结点的位置,就能访问链表里任意一个结点。
注意:无头单向非循环链表里的头结点指的是链表中的第一个结点,它本身也存储着有效数据。而后面所讲的带头双向循环链表中的头结点仅仅是作为链表起始的标志,并不会存储有效数据。
3.2函数接口的实现
//申请一个结点 SLTNode* BuySLTNode(SLTDataType data); //创建一个链表,包含数据为0~n SLTNode* CreateSList(int n); //释放内存 void SLTDestroy(SLTNode** pphead); //尾插 void SLTPushBack(SLTNode** pphead, SLTDataType data); //尾删 void SLTPopBack(SLTNode** pphead); //头插 void SLTPushFront(SLTNode** pphead, SLTDataType data); //头删 void SLTPopFront(SLTNode** pphead); //查找data的结点 SLTNode* SLTFind(SLTNode** pphead, SLTDataType data); //在pos位置前插入 void SLTInsert(SLTNode** pphead, SLTNode* pos, SLTDataType data); //在pos位置后插入 void SLTInsertAfter(SLTNode* pos, SLTDataType data); //删除pos结点 void SLTErase(SLTNode** pphead, SLTNode* pos); //打印链表内容 void SLTPrint(SLTNode* phead);
由于代码量过大,为了避免影响阅读体验太差,我将完整代码置于文章末尾。此处,我们来细致的学习两个函数尾插与尾删,其它函数与其原理几乎相同。
//尾插 void SLTPushBack(SLTNode** pphead, SLTDataType data); //尾删 void SLTPopBack(SLTNode** pphead);
3.2.1尾插
void SLTPushBack(SLTNode** pphead, SLTDataType data) { SLTNode* newNode = BuySLTNode(data); //若为第一次插入,则分情况处理 if (*pphead==NULL) { *pphead = newNode; return; } //找尾 SLTNode* tail = *pphead; while(tail->next) { tail = tail->next; } //找到了,进行尾插 tail->next = newNode; }
首先将tail也指向头结点,通过变化tail来找到尾结点,如下图所示为找尾的过程:
此时,tail所指向的结点的next为NULL,则循环结束,进行尾插。尾插只需要将tail所指向的结点的next指针赋值为newNode即可。
3.2.2尾删
void SLTPopBack(SLTNode** pphead) { assert(pphead); //分情况处理 if (*pphead == NULL) { free(*pphead); *pphead = NULL; } //找尾 SLTNode* tail = *pphead; while (tail->next->next) { tail = tail->next; } //找到了,进行尾删 free(tail->next); tail->next = NULL; }
同样的尾删也是先找尾,但与尾插的找尾有一点不同的是while循环的判断条件不相同。原因是我们删掉尾结点后,还有重要的一步是将尾结点的前一个结点(也就是新的尾结点)的next置为NULL。所以这里的tail指向的是尾结点的前一个结点。
此时tail->next->next 为NULL,所以循环结束,进行尾删。尾删只需要释放掉尾结点,并将新的尾结点的next置为NULL。
4. 带头双向循环链表的实现
带头双向循环链表看似结构复杂,其实在写代码时你会感到很轻松。其关键就在于它的头结点不一般。此处的头结点不存储有效数据。
4.1结点结构的定义
typedef int LTDataType; typedef struct ListNode { LTDataType data; struct ListNode* prev;//指向前一个结点 struct ListNode* next;//指向后一个结点 }LTNode;
既然是双向循环链表,那就得要求每个结点既保存下一个结点的地址,又要保存上一个结点的地址。结构中,prev指向前一个结点,next指向后一个结点。
4.2函数接口的实现
//申请一个结点 LTNode* BuyListNode(LTDataType data); //初始化头结点 LTNode* LTInit(); //释放申请的内存 void LTDestroy(LTNode* phead); //尾插 void LTPushBack(LTNode* phead, LTDataType data); //尾删 void LTPopBack(LTNode* phead); //头插 void LTPushFront(LTNode* phead, LTDataType data); //头删 void LTPopFront(LTNode* phead); //查找data LTNode* LTFind(LTNode* phead, LTDataType data); //在pos位置前插入 void LTInsert(LTNode* phead, LTNode* pos, LTDataType data); //删除pos位置 void LTErase(LTNode* phead, LTNode* pos); //判断链表是否为空 bool LTEmpty(LTNode* phead); //统计链表中数据的个数 size_t LTSize(LTNode* phead); //打印链表存储的数据 void LTPrint(LTNode* phead);
同样的,由于代码过长,为了避免影响阅读体验,我将完整源码置于文章末尾。带头双向循环链表的各个函数实现都比不带头的单链表简单很多。因此学会了之前单链表的实现,双向链表的实现自然轻松无比。这里就不做赘述。
5.两种链表的差异
①尾插与尾删的时间复杂度
对于单链表而言,尾插与尾删都是它的劣势。因为计算机无法直接访问到链表的尾结点,必须遍历完整个链表才能找到尾结点。所以单链表的尾插与尾删都为O(N)。
对于带头双向循环链表,找尾是极其方便的,因为尾结点就在头结点的前一个,可以一步就访问到。所以,带头双向循环链表的尾插与尾删都为O(1)。
②头插与头删的时间复杂度
两种链表头插与头删都为O(1)。对于链表这种数据结构而言,头插与头删都是其优势。上一章中提到顺序表的尾插与尾删效率极高,但是头插与头删却比较劣势。而链表正好相反,头插与头删效率很高。因此面对不同的场景,要学会使用合适的数据结构。
③函数形参为何一个是二级指针,一个是一级指针?
单链表,我们需要对phead的值做修改。那么就得利用phead的指针。而phead本身就是一个指针,所以函数的形参为pphead的二级指针。
双向循环链表,并不需要对phead做修改,而只是需要访问它prev和next所指向的结点,所以只用一级指针即可。
完整源码
无头单向非循环链表
SList.h
#pragma once #include #include #include typedef int SLTDataType; typedef struct SListNode { SLTDataType data; struct SListNode* next; }SLTNode; //申请一个结点 SLTNode* BuySLTNode(SLTDataType data); //创建一个链表,包含数据为0~n SLTNode* CreateSList(int n); //释放内存 void SLTDestroy(SLTNode** pphead); //尾插 void SLTPushBack(SLTNode** pphead, SLTDataType data); //尾删 void SLTPopBack(SLTNode** pphead); //头插 void SLTPushFront(SLTNode** pphead, SLTDataType data); //头删 void SLTPopFront(SLTNode** pphead); //查找data的结点 SLTNode* SLTFind(SLTNode** pphead, SLTDataType data); //在pos位置前插入 void SLTInsert(SLTNode** pphead, SLTNode* pos, SLTDataType data); //在pos位置后插入 void SLTInsertAfter(SLTNode* pos, SLTDataType data); //删除pos结点 void SLTErase(SLTNode** pphead, SLTNode* pos); //打印链表内容 void SLTPrint(SLTNode* phead);
SList.c
#define _CRT_SECURE_NO_DEPRECATE 1 #include"SList.h" SLTNode* BuySLTNode(SLTDataType data) { SLTNode* newNode = (SLTNode*)malloc(sizeof(SLTNode)); //检查是否申请成功 if (newNode == NULL) { perror("malloc fail"); exit(-1); } //对newNode进行初始化 newNode->data = data; newNode->next = NULL; //返回申请成功的结点 return newNode; } SLTNode* CreateSList(int n) { SLTNode* phead = NULL; SLTNode* ptail = NULL; for (int i = 0; i next = newNode; ptail = newNode; } } return phead; } void SLTDestroy(SLTNode** pphead) { assert(*pphead); SLTNode* cur = *pphead; //cur判断何时结束 while (cur) { SLTNode* next = cur->next; free(cur); cur = next; } *pphead = NULL; } void SLTPushBack(SLTNode** pphead, SLTDataType data) { SLTNode* newNode = BuySLTNode(data); //若为第一次插入,则分情况处理 if (*pphead==NULL) { *pphead = newNode; return; } //找尾 SLTNode* tail = *pphead; while(tail->next) { tail = tail->next; } //找到了,进行尾插 tail->next = newNode; } void SLTPopBack(SLTNode** pphead) { assert(pphead); //分情况处理 if (*pphead == NULL) { free(*pphead); *pphead = NULL; } //找尾 SLTNode* tail = *pphead; while (tail->next->next) { tail = tail->next; } //找到了,进行尾删 free(tail->next); tail->next = NULL; } void SLTPushFront(SLTNode** pphead, SLTDataType data) { SLTNode* newNode = BuySLTNode(data); //进行头插 newNode->next = *pphead; *pphead = newNode; } void SLTPopFront(SLTNode** pphead) { assert(*pphead); //进行头删 SLTNode* next = (*pphead)->next; free(*pphead); *pphead = next; } SLTNode* SLTFind(SLTNode** pphead, SLTDataType data) { assert(*pphead); SLTNode* cur = *pphead; while (cur) { //找到了,返回cur if (cur->data == data) { return cur; } cur = cur->next; } //没找到,返回NULL return NULL; } void SLTInsert(SLTNode** pphead, SLTNode* pos, SLTDataType data) { assert(pos); SLTNode* newNode = BuySLTNode(data); //若pos恰好是phead,相当于进行头插 if (pos == *pphead) { SLTPushFront(pphead, data); return; } //找pos前一个指针 SLTNode* prev = *pphead; while (prev->next != pos) { prev = prev->next; } //找到了,进行插入 prev->next = newNode; newNode->next = pos; } void SLTInsertAfter(SLTNode* pos, SLTDataType data) { assert(pos); SLTNode* newNode = BuySLTNode(data); SLTNode* next = pos->next; pos->next = newNode; newNode->next = next; } void SLTErase(SLTNode** pphead, SLTNode* pos) { assert(pphead); assert(pos); //若pos恰好就是phead,则相当于头删 if (pos == *pphead) { SLTPopFront(pphead); return; } //找pos前一个结点 SLTNode* prev = *pphead; while (prev->next != pos) { prev = prev->next; } //找到了,进行删除 prev->next = pos->next; free(pos); pos = NULL; } void SLTPrint(SLTNode* phead) { assert(phead); SLTNode* cur = phead; //cur控制何时结束 while (cur) { printf("%d->", cur->data); cur = cur->next; } printf("NULL\n"); }
test.c
#define _CRT_SECURE_NO_DEPRECATE 1 #include"SList.h" void test() { SLTNode* phead = CreateSList(10); SLTPushBack(&phead, 0); SLTPushBack(&phead, 0); SLTPushBack(&phead, 0); SLTPushBack(&phead, 0); SLTPushBack(&phead, 0); SLTPrint(phead); SLTPushFront(&phead, 1); SLTPushFront(&phead, 1); SLTPushFront(&phead, 1); SLTPushFront(&phead, 1); SLTPushFront(&phead, 1); SLTPrint(phead); SLTNode* pos = SLTFind(&phead, 3); SLTInsert(&phead, pos, 300); SLTInsertAfter(pos, 30); SLTPrint(phead); SLTPopBack(&phead); SLTPopBack(&phead); SLTPopBack(&phead); SLTPopBack(&phead); SLTPopBack(&phead); SLTPopFront(&phead); SLTPopFront(&phead); SLTPopFront(&phead); SLTPopFront(&phead); SLTPopFront(&phead); SLTPrint(phead); SLTDestroy(&phead); } int main() { test(); return 0; }
带头双向循环链表
List.h
#pragma once #include #include #include #include typedef int LTDataType; typedef struct ListNode { LTDataType data; struct ListNode* prev;//指向前一个结点 struct ListNode* next;//指向后一个结点 }LTNode; //申请一个结点 LTNode* BuyListNode(LTDataType data); //初始化头结点 LTNode* LTInit(); //释放申请的内存 void LTDestroy(LTNode* phead); //尾插 void LTPushBack(LTNode* phead, LTDataType data); //尾删 void LTPopBack(LTNode* phead); //头插 void LTPushFront(LTNode* phead, LTDataType data); //头删 void LTPopFront(LTNode* phead); //查找data LTNode* LTFind(LTNode* phead, LTDataType data); //在pos位置前插入 void LTInsert(LTNode* phead, LTNode* pos, LTDataType data); //删除pos位置 void LTErase(LTNode* phead, LTNode* pos); //判断链表是否为空 bool LTEmpty(LTNode* phead); //统计链表中数据的个数 size_t LTSize(LTNode* phead); //打印链表存储的数据 void LTPrint(LTNode* phead);
List.c
#define _CRT_SECURE_NO_DEPRECATE 1 #include"List.h" LTNode* BuyListNode(LTDataType data) { LTNode* newNode = (LTNode*)malloc(sizeof(LTNode)); if (newNode == NULL) { perror("malloc fail"); exit(-1); } newNode->data = data; newNode->prev = NULL; newNode->next = NULL; return newNode; } LTNode* LTInit() { LTNode* phead = BuyListNode(-1); phead->prev = phead; phead->next = phead; return phead; } void LTDestroy(LTNode* phead) { assert(phead); LTNode* cur = phead->next; while (cur != phead) { LTNode* next = cur->next; free(cur); cur = next; } free(phead); } void LTPushBack(LTNode* phead, LTDataType data) { assert(phead); LTNode* newNode = BuyListNode(data); LTNode* tail = phead->prev; newNode->next = phead; newNode->prev = tail; tail->next = newNode; phead->prev = newNode; } void LTPopBack(LTNode* phead) { assert(phead); assert(!LTEmpty(phead)); LTNode* tail = phead->prev; tail->prev->next = phead; phead->prev = tail->prev; free(tail); } void LTPushFront(LTNode* phead, LTDataType data) { assert(phead); LTNode* newNode = BuyListNode(data); newNode->next = phead->next; phead->next->prev = newNode; phead->next = newNode; newNode->prev = phead; } void LTPopFront(LTNode* phead) { assert(phead); assert(!LTEmpty(phead)); LTNode* cur = phead->next; phead->next = cur->next; cur->next->prev = phead; free(cur); } LTNode* LTFind(LTNode* phead, LTDataType data) { assert(phead); LTNode* cur = phead->next; while (cur != phead) { if (cur->data==data) { return cur; } cur = cur->next; } return NULL; } void LTInsert(LTNode* phead, LTNode* pos, LTDataType data) { assert(phead); LTNode* newNode = BuyListNode(data); pos->prev->next = newNode; newNode->prev = pos->prev; newNode->next = pos; pos->prev = newNode; } void LTErase(LTNode* phead,LTNode* pos) { assert(phead); pos->prev->next = pos->next; pos->next->prev = pos->prev; free(pos); } bool LTEmpty(LTNode* phead) { assert(phead); if (phead->next == phead) { return true; } return false; } size_t LTSize(LTNode* phead) { assert(phead); size_t size = 0; LTNode* cur = phead->next; while (cur != phead) { size++; cur = cur->next; } return size; } void LTPrint(LTNode* phead) { assert(phead); LTNode* cur = phead->next; while (cur != phead) { printf("%d->", cur->data); cur = cur->next; } printf("\n"); }
test.c
#define _CRT_SECURE_NO_DEPRECATE 1 #include"List.h" void test() { LTNode* phead = LTInit(); LTPushBack(phead, 2); LTPushBack(phead, 1); LTPushBack(phead, 1); LTPushBack(phead, 1); LTPushBack(phead, 1); LTPushBack(phead, 1); LTPushFront(phead, 0); LTPushFront(phead, 0); LTPushFront(phead, 0); LTPushFront(phead, 0); LTPushFront(phead, 0); LTPrint(phead); LTPopFront(phead); LTPopFront(phead); LTPopFront(phead); LTPrint(phead); LTPopBack(phead); LTPopBack(phead); LTPopBack(phead); LTPrint(phead); LTNode* pos = LTFind(phead, 2); LTInsert(phead, pos, 200); LTInsert(phead, pos, 200); LTInsert(phead, pos, 200); LTPrint(phead); LTErase(phead, pos); LTPrint(phead); LTDestroy(phead); } int main() { test(); return 0; }