1 链表的概念及结构
链表是一种物理存储结构上非连续、非顺序的存储结构,数据元素的逻辑顺序是通过链表中的指针链接次序实现的。习惯上将存储数据的一块空间称为结点(节点)。
链表结构在物理上不一定是连续的:链表中的结点可以是内存中任意一块合法有效空间,每个结点的空间不一定要是连续的。但在每个结点中除了数据还包含了指向下一个结点的指针(有的还包含了指向上一个结点的指针),这样即使结点间的空间不连续,也可以通过结点中的指针找到下一个结点(或上一个结点),从而实现了结点间的链接,所以说链表结构在逻辑上是连续。
以单链表为例:
①单链表的逻辑结构
单链表的逻辑结构类似于我们现实生活中的火车,如下图所示:单链表的表头(头结点)相当于火车头,单链表中的一个个结点就相当于一节节的火车车厢,结点指针相当于连接车厢的车钩,将一个个结点链接起来。
②单链表的实际结构
假设在32位系统上,结点中值域为int型(大小为4个字节),指针域大小为4个字节,则结点的大小为8个字节,则单链表的实际结构如下图所示(图中地址值均为假设值):
在实际中,链表的结点一般是在堆空间中申请得到的,而在堆上申请出来的空间是按照一定的策略来分配的,两次申请的空间可能连续,也可能不连续。
2 链表的分类
在实际中,链表的结构是非常多样的,如带头或不带头的链表、单向或双向的链表、循环或者非循环的链表,这些组合起来一共有8中不同的链表结构(以下所展示的均为链表的逻辑结构):
①单向或双向
②带头或不带头
③循环或非循环
虽然链表的结构多样,但在实际中我们最常使用的还是以下两种结构:
①无头单向非循环链表: 结构简单,一般不会单独用来存放数据。实际中更多是作为其它数据结构的子结构,如哈希桶、图的邻接表等等。
②带头双向循环链表: 结构最复杂,一般用来单独存储数据。实际中使用的链表数据结构,大都是带头双向循环链表。虽然其结构复杂,但该结构的实现和使用反而更简单。
接下来文章将要介绍如何实现这两种常用的链表结构
3 无头单向非循环链表的实现
3.1 单链表的初始化、打印及销毁
如上所说,链表结点中包含两个部分,一是值域,二是指针域。基于此,我们声明一个结构体类型来作为链表的结点。
通常我们将指向链表头结点的指针(一级指针)或指向指向链表头结点的指针
的指针作为链表接口的参数,通过相应的指针,我们即可访问链表中的每个结点。
🍚单链表结点结构体的声明
typedef int SLTDataType; //链表数据类型
typedef struct SListNode {
SLTDataType data; //值域
struct SListNode* next; //指针域,指向下一个结点
}SListNode;
🍚单链表结点的动态申请与初始化
//动态申请一个结点
SListNode* BuySListNode(SLTDataType data) {
SListNode* node = (SListNode*)malloc(sizeof(SListNode));
if (node == NULL) {
perror("BuySListNode_malloc_failed");
exit(EXIT_FAILURE);
}
node->data = data;
node->next = NULL;
return node; //返回指向该结点的指针
}
🍚单链表打印
//单链表打印
//传入头指针
void SListPrint(SListNode* plist) {
if (plist == NULL) {
printf("NULL"); //表示链表为空
}
else {
//当结点中的指针为空时,表示该结点即为最后一个结点
while (plist->next) {
printf("[ %d | %p ] -> ", plist->data, plist->next); //打印结点值域及指针域内容
plist = plist->next;
}
printf("[ %d | %p ]\n", plist->data, plist->next); //输出样式可自己调整
}
}
🍚单链表销毁
在销毁单链表时,我们会依次销毁链表中的每一个结点,其中也包括头结点,这就意味着头结点指针的指向将会被改变(销毁完全部结点后,头指针应当置空,否则易造成野指针访问的错误),此时如果以一级指针作为参数,传入的将是头指针的拷贝值,我们可以通过指针去访问链表中的其它结点,但如果要修改头指针的指向,只在接口中对一级指针的拷贝值(形参)进行修改是不会影响到实际的头指针(实参)指向的。因此需要以二级指针(指向
指向链表头结点的指针
的指针)作为接口参数,传入二级指针的拷贝值,则通过对二级指针进行解引用即可访问对应的一级指针,也可通过解引用得到的一级指针再访问链表中的其它结点。
同样,在其它的接口实现中,如果涉及到需要更改头指针指向的,都应当以相应的二级指针作为参数。
//单链表的销毁
//传入二级指针
void SListDestroy(SListNode** plist) {
SListNode* cur = *plist;
SListNode* prev = *plist; //记录前一个结点指针,避免结点释放后无法找到下一个结点
//当结点指针为空时,即表示链表中结点已全部释放
while (cur) {
cur = cur->next;
free(prev); //逐个释放动态开辟的结点空间
prev = cur;
}
*plist = cur; //将头指针置空
}
3.2 单链表尾插
如图为单链表尾插示意图:
接口实现:
//单链表尾插
void SListPushBack(SListNode** pplist, SLTDataType data) {
//如果链表为空,则插入的结点即为头结点,此时更改头指针指向新插入的结点
if (*pplist == NULL) {
*pplist = BuySListNode(data);
}
//如果链表不为空,则通过指针访问到当前链表的最后一个结点,在最后一个结点后插入新结点
else {
SListNode* tail = *pplist;
//当结点中的指针为空时,即表示当前结点为最后一个结点
while (tail->next) {
tail = tail->next;
}
//让最后一个结点中的指针指向新结点,即将新结点链接到了链表尾部
tail->next = BuySListNode(data);
}
}
3.3 单链表头插
如图为单链表头插示意图:
接口实现:
//单链表头插
void SListPushFront(SListNode** pplist, SLTDataType data) {
SListNode* newhead = BuySListNode(data);
newhead->next = *pplist; //让新结点中的指针指向链表当前的头结点
*pplist = newhead; //更改头指针指向新结点
}
3.4 单链表尾删
如图为单链表尾删示意图:
接口实现:
//单链表尾删
void SListPopBack(SListNode** pplist) {
assert(*pplist); //确保链表不为空,空链表则没有结点可删
SListNode* tail = *pplist;
SListNode* prev = *pplist; //记录前一个结点
//找到最后一个结点
while (tail->next) {
prev = tail;
tail = tail->next;
}
//将最后一个结点的前一个结点中的指针置空,并释放最后一个结点
prev->next = NULL;
free(tail);
}
3.5 单链表头删
如图为单链表头删示意图:
接口实现:
//单链表头删
void SListPopFront(SListNode** pplist) {
assert(*pplist); //确保链表不为空
//链表头删时会改变头指针的指向,此时应将头指针更改为指向原链表头结点的下一个结点
SListNode* newhead = (*pplist)->next; //记录原链表头结点的下一个结点,避免释放头结点后无法找到原来的第二个结点
free(*pplist);
*pplist = newhead; //更新指向
}
3.6 单链表查找
接口实现:
//单链表查找
SListNode* SListFind(SListNode* plist, SLTDataType data) {
SListNode* cur = plist;
//遍历链表结点,对比查找是否有目标结点,如果找到目标则返回指向目标结点的指针,查找不到目标则返回空
while (cur) {
if (cur->data == data) {
break;
}
cur = cur->next;
}
return cur;
}
3.7 单链表在指定位置后插入结点
当知道确定位置时,在确定位置之后插入结点是比较方便的。基于单链表结构特性,我们只能访问给定位置及其之后的结点,如果是在指定位置之后插入结点,则只需要将新结点中的指针指向原链表中指定位置结点的后一个结点,同时将指定位置结点中的指针指向新节点即可。
接口实现:
//单链表在pos位置之后插入结点
void SListInsertAfter(SListNode* pos, SLTDataType data) {
assert(pos); //确保给出的位置不为空,通常指定位置通过查找接口返回值得到
SListNode* cur = pos; //记录当前结点位置
SListNode* next = cur->next; //记录指定位置结点的下一个结点位置
cur->next = BuySListNode(data); //将指定位置结点中的next指针指向新结点
cur = cur->next; //将当前指针指向新结点
cur->next = next; //将新结点中的指针指向原来记录的下一个结点
}
3.8 单链表在指定位置前插入结点
相比于在指定位置之后插入结点,在指定位置之前插入结点要稍微复杂一些。因为只知道指定位置是无法访问其前一个结点的,因此在对应接口中我们还需要给出头指针作为参数,通过头指针我们就可以逐步访问到指定位置的前一个结点,通过将前一个结点中的指针指向新结点,同时将新结点中的指针指向指定位置上的结点即可完成指定位置前的结点插入。
接口实现:
//单链表在pos位置之前插入结点
void SListInsertFront(SListNode** pplist, SListNode* pos, SLTDataType data) {
assert(pos);
if (*pplist == pos) {
//如果指定位置即为第一个结点,则执行头插操作
SListPushFront(pplist, data);
}
else {
SListNode* cur = *pplist;
//找到指定位置的前一个结点
while (cur->next != pos) {
cur = cur->next;
}
cur->next = BuySListNode(data);
cur = cur->next;
cur->next = pos;
}
}
3.9 单链表删除指定位置后的结点
删除指定位置后的结点要确保指定位置之后是有结点的,如果指定位置之后没有结点,则删除是无意义的,此时不执行任何操作
接口实现:
//单链表删除pos位置之后的结点
void SListEraseAfter(SListNode* pos) {
assert(pos);
if (pos->next != NULL) {
SListNode* cur = pos;
SListNode* next = (cur->next)->next; //记录指定位置的下一个结点的下一个结点位置
free(cur->next); //释放指定位置之后的结点
cur->next = next; //链接指定位置上的结点和前面记录的next位置上的结点
}
}
3.10 单链表删除指定位置上的结点
要删除指定位置上的结点,同时还要确保结点删除后,被删除结点的前后结点能够重新链接起来,就需要知道指定位置前的结点位置,此时也需要给出链表头指针作为接口参数以便找到指定位置前的结点。
接口实现:
//单链表删除pos位置上的结点
void SListEraseCurrent(SListNode** pplist, SListNode* pos) {
assert(pos);
if (*pplist == pos) {
//如果指定位置为头指针位置,则执行头删操作
SListPopFront(pplist);
}
else {
SListNode* cur = *pplist;
while (cur->next != pos) {
cur = cur->next;
}
cur->next = pos->next;
free(pos);
}
}
3.11 单链表整体实现
将单链表各接口及结构体的声明统一编写在一个头文件(这里是SingleList.h)中,并在该头文件中包含实现接口时所需要用到的库文件;接着将各接口的具体实现编写在与头文件对应的.c文件中(SingleList.c),并在该.c文件中包含对应的头文件(SeqList.h)。
SingleList.h:
#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>
#include <stdlib.h>
#include <assert.h>
typedef int SLTDataType;
typedef struct SListNode {
SLTDataType data;
struct SListNode* next;
}SListNode;
//动态申请一个节点
SListNode* BuySListNode(SLTDataType data);
//创建一个指定长度的单链表
SListNode* CreateSList(int length);
//单链表打印
void SListPrint(SListNode* plist);
//单链表尾插
void SListPushBack(SListNode** pplist, SLTDataType data);
//单链表头插
void SListPushFront(SListNode** pplist, SLTDataType data);
//单链表尾删
void SListPopBack(SListNode** pplist);
//单链表头删
void SListPopFront(SListNode** pplist);
//单链表查找
SListNode* SListFind(SListNode* plist, SLTDataType data);
//单链表在pos位置之后插入结点
void SListInsertAfter(SListNode* pos, SLTDataType data);
//单链表在pos位置之前插入结点
void SListInsertFront(SListNode** pplist, SListNode* pos, SLTDataType data);
//单链表删除pos位置之后的结点
void SListEraseAfter(SListNode* pos);
//单链表删除pos位置上的结点
void SListEraseCurrent(SListNode** pplist, SListNode* pos);
//单链表的销毁
void SListDestroy(SListNode** plist);
SingleList.c:
#define _CRT_SECURE_NO_WARNINGS 1
#include "SingleList.h"
//动态申请一个节点
SListNode* BuySListNode(SLTDataType data) {
SListNode* node = (SListNode*)malloc(sizeof(SListNode));
if (node == NULL) {
perror("BuySListNode:NULL");
exit(EXIT_FAILURE);
}
node->data = data;
node->next = NULL;
return node;
}
//创建一个单链表
SListNode* CreateSList(int length) {
if (length == 0) {
return NULL;
}
int i = 1;
SListNode* head = BuySListNode(i);
SListNode* tail = head;
for (i = 2; i <= length; i++) {
tail->next = BuySListNode(i);
tail = tail->next;
}
return head;
}
//单链表打印
void SListPrint(SListNode* plist) {
if (plist == NULL) {
printf("NULL");
}
else {
while (plist->next) {
printf("[ %d | %p ] -> ", plist->data, plist->next);
plist = plist->next;
}
printf("[ %d | %p ]\n", plist->data, plist->next);
}
}
//单链表尾插
void SListPushBack(SListNode** pplist, SLTDataType data) {
if (*pplist == NULL) {
*pplist = BuySListNode(data);
}
else {
SListNode* tail = *pplist;
while (tail->next) {
tail = tail->next;
}
tail->next = BuySListNode(data);
}
}
//单链表头插
void SListPushFront(SListNode** pplist, SLTDataType data) {
SListNode* newhead = BuySListNode(data);
newhead->next = *pplist;
*pplist = newhead;
}
//单链表尾删
void SListPopBack(SListNode** pplist) {
assert(*pplist);
SListNode* tail = *pplist;
SListNode* prev = *pplist;
while (tail->next) {
prev = tail;
tail = tail->next;
}
prev->next = NULL;
free(tail);
}
//单链表头删
void SListPopFront(SListNode** pplist) {
assert(*pplist);
SListNode* newhead = (*pplist)->next;
free(*pplist);
*pplist = newhead;
}
//单链表查找
SListNode* SListFind(SListNode* plist, SLTDataType data) {
SListNode* cur = plist;
while (cur) {
if (cur->data == data) {
break;
}
cur = cur->next;
}
return cur;
}
//单链表在pos位置之后插入结点
void SListInsertAfter(SListNode* pos, SLTDataType data) {
assert(pos);
SListNode* cur = pos;
SListNode* next = cur->next;
cur->next = BuySListNode(data);
cur = cur->next;
cur->next = next;
}
//单链表在pos位置之前插入结点
void SListInsertFront(SListNode** pplist, SListNode* pos, SLTDataType data) {
assert(pos);
if (*pplist == pos) {
SListPushFront(pplist, data);
}
else {
SListNode* cur = *pplist;
while (cur->next != pos) {
cur = cur->next;
}
cur->next = BuySListNode(data);
cur = cur->next;
cur->next = pos;
}
}
//单链表删除pos位置之后的结点
void SListEraseAfter(SListNode* pos) {
assert(pos);
if (pos->next != NULL) {
SListNode* cur = pos;
SListNode* next = (cur->next)->next;
free(cur->next);
cur->next = next;
}
}
//单链表删除pos位置上的结点
void SListEraseCurrent(SListNode** pplist, SListNode* pos) {
assert(pos);
if (*pplist == pos) {
SListPopFront(pplist);
}
else {
SListNode* cur = *pplist;
while (cur->next != pos) {
cur = cur->next;
}
cur->next = pos->next;
free(pos);
}
}
//单链表的销毁
void SListDestroy(SListNode** plist) {
SListNode* cur = *plist;
SListNode* prev = *plist;
while (cur) {
cur = cur->next;
free(prev);
prev = cur;
}
*plist = cur;
}
4 带头双向循环链表的实现
带头双向循环链表增加了哨兵位头结点,这就意味着即使链表是空的,实际上表中还是存在一个头结点的,只不过这个头结点中的数据是无意义的,同时由于哨兵位的存在,在对链表进行增删查改操作时都不会涉及到更改头指针指向,因此在实现相应接口时只需要传入指向头结点的一级指针作为参数即可;双向则意味着一个结点中不止包含的指向下一个结点的指针,还包含了指向前一个结点的指针;循环表示链表的最后一个结点中的指向下一结点的指针不为空,而是指向了链表的头结点,同时由于链表是双向的,所以头结点中的指向前一个结点的指针是指向链表的最后一个结点的。
以下是链表各接口的具体实现
4.1 链表的初始化、打印及销毁
🍚链表结点结构体声明:
typedef int LDataType; //结点值域类型
typedef struct ListNode {
LDataType data; //值域
struct ListNode* next; //指向下一个结点的指针
struct ListNode* prev; //指向前一个结点的指针
}ListNode;
🍚链表结点的动态申请和初始化:
//创建一个结点
ListNode* ListNodeCreate(LDataType data) {
ListNode* node = (ListNode*)malloc(sizeof(ListNode));
if (node == NULL) {
perror("node:NULL");
exit(-1);
}
node->data = data;
node->next = NULL;
node->prev = NULL;
return node; //返回指向结点的指针
}
🍚链表打印:
//链表打印
void ListPrint(ListNode* plist) {
ListNode* tail = plist;
printf(" <—> [ %p | head | %p ] <—> ", tail->prev, tail->next);
//头结点数据无意义,因此从头结点的下一个结点开始打印
tail = tail->next;
//当tail指针循环走回头结点时停止打印
while (tail != plist) {
printf("[ %p | %d | %p ] <—> ", tail->prev, tail->data, tail->next);
tail = tail->next;
}
printf("\n");
}
🍚链表销毁:
//链表销毁
void ListDestroy(ListNode* plist) {
ListNode* tail = plist;
//从头结点的下一个结点开始逐个释放
tail = tail->next;
while (tail != plist) {
tail = tail->next;
free(tail->prev);
}
//最后释放头结点
free(plist);
}
4.2 链表尾插
不同于单向非循环链表在尾插时需要根据头指针单向逐个访问到尾结点,双向循环链表头结点中的指向前一个结点的指针指向的即是尾结点,这样只需要一步就可以访问到尾结点,此时只需要再更改相关结点的指针指向,将新结点接入链表中即可完成尾插。
//双向链表尾插
void ListPushBack(ListNode* plist, LDataType data) {
assert(plist);
ListNode* newnode = ListNodeCreate(data);
ListNode* end = plist->prev; //找到尾结点位置
end->next = newnode;
newnode->prev = end;
newnode->next = plist;
plist->prev = newnode;
//ListInsertFront(plist, plist, data);//尾插
}
4.3 链表尾删
//双向链表尾删
void ListPopBack(ListNode* plist) {
assert(plist && (plist->next != plist)); //删除结点时要确保链表不为空
ListNode* end_prev = plist->prev->prev; //尾结点的前一个
free(plist->prev);
end_prev->next = plist;
plist->prev = end_prev;
//ListErase(plist, plist->prev);//尾删
}
4.4 链表头插
//双向链表头插
void ListPushFront(ListNode* plist, LDataType data) {
assert(plist);
ListNode* newnode = ListNodeCreate(data);
ListNode* next = plist->next;
plist->next = newnode;
newnode->prev = plist;
newnode->next = next;
next->prev = newnode;
//ListInsertFront(plist, plist->next, data);//头插
}
4.5 链表头删
//双向链表头删
void ListPopFront(ListNode* plist) {
assert(plist && (plist->next != plist));
ListNode* next = plist->next->next; //记录存有有效数据的第二个结点
free(plist->next);
plist->next = next;
next->prev = plist;
//ListErase(plist, plist->next);//头删
}
4.6 链表查找
//双向链表查找
ListNode* ListFind(ListNode* plist, LDataType data) {
assert(plist);
ListNode* tail = plist;
//从第一个有效结点开始遍历,当当前指针指向的结点中的指针指向头结点时,表示链表中的结点已全部对比过了,此时结束循环
while (tail->next != plist) {
tail = tail->next;
if (tail->data == data) {
return tail; //找到则返回指向目标结点的指针
}
}
return NULL; //找不到则返回空
}
4.7 链表指定位置前插入结点
事实上,由于链表是双向的,所以我们不需要传入头指针作为参数也可以方便的通过指定位置上的结点中的前后指针访问到其前后结点,也就可以方便的在指定位置前后插入新结点。
链表指定位置前插入结点示意图:
//双向链表在pos位置前面插入结点
//当指定位置为头结点的下一个结点时,接口相当于实现了头插操作
//当指定位置为头结点时,接口相当于实现了尾插操作
void ListInsertFront(ListNode* pos, LDataType data) {
assert(pos);
ListNode* prev = pos->prev;
ListNode* newnode = ListNodeCreate(data);
prev->next = newnode;
newnode->prev = prev;
newnode->next = pos;
pos->prev = newnode;
}
4.8 链表指定位置后插入结点
链表指定位置后插入结点示意图:
//双向链表在pos位置后面插入结点
//当指定位置为头结点时,接口相当于实现了头插操作
//当指定位置为尾结点时,接口相当于实现了尾插操作
void ListInsertAfter(ListNode* pos, LDataType data) {
assert(pos);
ListNode* next = pos->next;
ListNode* newnode = ListNodeCreate(data);
pos->next = newnode;
newnode->prev = pos;
newnode->next = next;
next->prev = newnode;
}
4.9 链表删除指定位置上的结点
//双向链表删除pos位置的结点
void ListErase(ListNode* plist, ListNode* pos) {
assert(pos && pos != plist); //确保链表不为空且删除的结点不是哨兵位头结点
ListNode* prev = pos->prev;
ListNode* next = pos->next;
free(pos);
prev->next = next;
next->prev = prev;
}
4.10 链表判空
考虑到在实现一些接口时通常会需要对链表进行判空,这里可以将这个需求也用一个接口来实现。
//链表判空
int ListIsEmpty(ListNode* plist) {
assert(plist);
//如果头结点中的指针指向了自己,则表示链表为空,表中只有一个头结点
if (plist->next == plist) {
return 1;
}
else {
return 0;
}
//return (plist->next == plist);
}
4.11 计算链表长度
//计算链表长度(不包括哨兵位头结点)
size_t ListSize(ListNode* plist) {
assert(plist);
size_t size = 0;
//从头结点的下一个结点计算
ListNode* tail = plist->next;
while (tail != plist) {
size++;
tail = tail->next;
}
return size;
}
4.12 带头双向循环链表的整体实现
DLList.h:
#define _CRT_SECURE_NO_WARNINGS 1
#pragma once
#include <stdio.h>
#include <stdlib.h>
#include <assert.h>
#include <stdbool.h>
typedef int LDataType;
typedef struct ListNode {
LDataType data;
struct ListNode* next;
struct ListNode* prev;
}ListNode;
//创建一个结点
ListNode* ListNodeCreate(LDataType data);
//创建一个双向链表
ListNode* ListCreate(int length);
//双向链表打印
void ListPrint(ListNode* plist);
//双向链表销毁
void ListDestroy(ListNode* plist);
//双向链表尾插
void ListPushBack(ListNode* plist, LDataType data);
//双向链表尾删
void ListPopBack(ListNode* plist);
//双向链表头插
void ListPushFront(ListNode* plist, LDataType data);
//双向链表头删
void ListPopFront(ListNode* plist);
//双向链表查找
ListNode* ListFind(ListNode* plist, LDataType data);
//双向链表在pos位置前面插入
void ListInsertFront(ListNode* pos, LDataType data);
//双向链表在pos位置后面插入
void ListInsertAfter(ListNode* pos, LDataType data);
//双向链表删除pos位置的结点
void ListErase(ListNode* plist, ListNode* pos);
//链表判空
int ListIsEmpty(ListNode* plist);
//计算链表长度
size_t ListSize(ListNode* plist);
DLList.c:
#define _CRT_SECURE_NO_WARNINGS 1
#include "DLList.h"
//实现带头双向循环链表的增删查改
//创建一个结点
ListNode* ListNodeCreate(LDataType data) {
ListNode* node = (ListNode*)malloc(sizeof(ListNode));
if (node == NULL) {
perror("node:NULL");
exit(-1);
}
node->data = data;
node->next = NULL;
node->prev = NULL;
return node;
}
//创建一个双向链表
ListNode* ListCreate(int length) {
//首先创建一个头结点,长度为空的话就只有一个哨兵位结点
ListNode* head = ListNodeCreate(0);
ListNode* tail = head;
int i = 1;
for (i = 1; i <= length; i++) {
ListNode* newnode = ListNodeCreate(i);
tail->next = newnode;
newnode->prev = tail;
tail = tail->next;
}
tail->next = head;
head->prev = tail;
return head;
}
//双向链表打印
void ListPrint(ListNode* plist) {
ListNode* tail = plist;
printf(" <—> [ %p | head | %p ] <—> ", tail->prev, tail->next);
tail = tail->next;
while (tail != plist) {
printf("[ %p | %d | %p ] <—> ", tail->prev, tail->data, tail->next);
tail = tail->next;
}
printf("\n");
}
//双向链表销毁
void ListDestroy(ListNode* plist) {
ListNode* tail = plist;
tail = tail->next;
while (tail != plist) {
tail = tail->next;
free(tail->prev);
}
free(plist);
}
//双向链表尾插
void ListPushBack(ListNode* plist, LDataType data) {
assert(plist);
ListNode* newnode = ListNodeCreate(data);
ListNode* end = plist->prev;
end->next = newnode;
newnode->prev = end;
newnode->next = plist;
plist->prev = newnode;
//ListInsertFront(plist, plist, data);//尾插
}
//双向链表尾删
void ListPopBack(ListNode* plist) {
assert(plist && (plist->next != plist));
ListNode* end_prev = plist->prev->prev; //尾结点的前一个
free(plist->prev);
end_prev->next = plist;
plist->prev = end_prev;
//ListErase(plist, plist->prev);//尾删
}
//双向链表头插
void ListPushFront(ListNode* plist, LDataType data) {
assert(plist);
ListNode* newnode = ListNodeCreate(data);
ListNode* next = plist->next;
plist->next = newnode;
newnode->prev = plist;
newnode->next = next;
next->prev = newnode;
//ListInsertFront(plist, plist->next, data);//头插
}
//双向链表头删
void ListPopFront(ListNode* plist) {
assert(plist && (plist->next != plist));
ListNode* next = plist->next->next;
free(plist->next);
plist->next = next;
next->prev = plist;
//ListErase(plist, plist->next);//头删
}
//双向链表查找
ListNode* ListFind(ListNode* plist, LDataType data) {
assert(plist);
ListNode* tail = plist;
while (tail->next != plist) {
tail = tail->next;
if (tail->data == data) {
return tail;
}
}
return NULL;
}
//双向链表在pos位置前面插入
void ListInsertFront(ListNode* pos, LDataType data) {
assert(pos);
ListNode* prev = pos->prev;
ListNode* newnode = ListNodeCreate(data);
prev->next = newnode;
newnode->prev = prev;
newnode->next = pos;
pos->prev = newnode;
}
//双向链表在pos位置后面插入
void ListInsertAfter(ListNode* pos, LDataType data) {
assert(pos);
ListNode* next = pos->next;
ListNode* newnode = ListNodeCreate(data);
pos->next = newnode;
newnode->prev = pos;
newnode->next = next;
next->prev = newnode;
}
//双向链表删除pos位置的结点
void ListErase(ListNode* plist, ListNode* pos) {
assert(pos && pos != plist);
ListNode* prev = pos->prev;
ListNode* next = pos->next;
free(pos);
prev->next = next;
next->prev = prev;
}
//链表判空
int ListIsEmpty(ListNode* plist) {
assert(plist);
if (plist->next == plist) {
return 1;
}
else {
return 0;
}
//return (plist->next == plist);
}
//计算链表长度
size_t ListSize(ListNode* plist) {
assert(plist);
size_t size = 0;
ListNode* tail = plist->next;
while (tail != plist) {
size++;
tail = tail->next;
}
return size;
}
5 总结
由于顺序表存储空间连续的结构特性,对于顺序表头部或中间位置的插入或删除操作会带来对目标位置之后所有数据挪动的消耗;同时,当顺序表空间不足时,因为无法预知接下来还需要多少空间,一般都会以两倍于之前的容量将顺序表扩容,这过程中拷贝数据,释放旧空间也会有不小的消耗,而且若之后不需要再插入数据了,那表中剩余的空间没有被使用会造成一定的空间浪费。但顺序表可以方便的通过下标随机访问表中数据。
相比之下,链表由于不要求物理上空间的连续,且链表结点是根据需要来开辟或释放的,这就有效避免了空间浪费,且链表通过指针链接各结点,在执行插入和删除操作时整体来说具有更高的效率(对于不带头单向非循环链表,方便头插头删;对于带头双向循环链表,在任意位置的插入删除都具有较高的效率)
以下对顺序表和链表(以带头双向循环链表为例)的区别做出总结:
不同点 | 顺序表 | 链表 |
---|---|---|
存储空间上 | 物理上一定连续 | 逻辑上连续,但物理上不一定连续 |
随机访问 | 支持,时间复杂度O(1) | 不支持,时间复杂度O(N) |
任意位置插入或删除元素 | 可能需要搬移元素,效率低,时间复杂度O(N) | 只需修改相关结点指针指向 |
空间使用 | 动态顺序表,容量不足时需要扩容,可能造成空间浪费 | 按需申请释放小块结点内存 |
应用场景 | 元素高效存储且需要频繁访问时 | 需要在任意位置频繁插入或删除元素时 |
缓存利用率 | 高 | 低 |
缓存相关的详细知识可参看链接文章
与程序员相关的CPU缓存知识
以上是我对数据结构中链表的一些学习记录总结,如有错误,希望大家帮忙指正,也欢迎大家给予建议和讨论,谢谢!