链表的定义
链表是一种物理存储结构上非连续、非顺序的存储结构,数据元素的逻辑顺序是通过链表中的指针链接次序实现的 。
链表与数组
- 数组:可以方便的遍历查找需要的数据。在查询数组指定位置的操作中,只需要进行
1
次操作即可,时间复杂度为O(1)
。
但这种时间上的便利性,是因为数组在内存中占用了连续的空间,在进行类似的查找或者遍历时,本质是指针在内存中的定向偏移。
所以当需要对数组成员进行添加和删除的操作时,数组内完成这类操作的时间复杂度则变成了O(n)
。 - 链表:整体表现上比数组更加高效。例如当进行插入和删除操作时,链表操作的时间复杂度仅为
O(1)
。
另外,因为链表在内存中不是连续存储的,所以可以充分利用内存中的碎片空间。
【本篇博客讲解的是单向、非循环链表的接口实现】
实现
定义表结构
#pragma once //预处理指令:只编译一次
//定义链表中每一个"结点" 的结构体
typedef struct SListNode {
SLDataType data;
struct SListNode *next;
}SListNode;
//定义"链表" 的结构体
typedef struct SList {
SListNode *first; //指向链表第一个结点的指针
}SList;
初始化
- 思路:创建一个链表的头指针,默认为
NULL
,即初始化操作。
void SListInit(SList *list) {
assert(list != NULL);
list->first = NULL; //成为一个空链表
}
输出
- 思路:逐个打印每个结点的
data
值,为了显示统一格式,将尾结点置为NULL
。
void SListPrint(SList *list) {
for (SListNode *cur = list->first; cur != NULL; cur = cur->next) {
printf("%d --> ", cur->data);
}
printf("NULL\n");
}
查找
//找到返回 < 结点的地址 > .没找到返回 <NULL>。
//时间复杂度O(n),因为有遍历操作
SListNode* SListFind(SList* list, SLDataType data) {
for (SListNode *cur = list->first; cur != NULL; cur = cur->next) { //遍历
if (cur->data == data) {
return cur;
}
}
//否则没找到
return NULL;
}
头删
- 思路:排除空链表与无链表的情况。将首指针跳过一个元素指向下一个节点,然后释放掉跳过结点的内存空间即可。
//时间复杂度O(1)
void SListPopFront(SList *list) {
assert(list != NULL); //表示没有链表
assert(list->first != NULL); //表示有链表,但链表为空
SListNode *old_first = list->first;
list->first = list->first->next;
free(old_first);
}
尾删
- 思路:分类讨论:
- 如果链表只有一个结点,执行头插操作就是进行尾插操作。
- 但凡链表不是单结点情况,遍历链表直至倒数第二个结点,然后就可以通过下一结点的指针释放尾结点的内存空间。安全起见将释放后的内存置为
NULL
。
//时间复杂度O(n),因为有遍历操作,单链表
void SListPopBack(SList *list) {
assert(list != NULL);
assert(list->first != NULL); // 0 个结点
if (list->first->next == NULL) {
//只有 1 个结点,此时尾删就是头删
SListPopFront(list);
return;
}
//通常情况 >= 2 个结点
//方案一:
SListNode *cur;
for (cur = list->first; cur->next->next != NULL; cur = cur->next);
// cur 是倒数第二个结点
free(cur->next); //cur->next就变成了无效指针了
cur->next = NULL;
//方案二:
//也可以创建一个变量存储变为NULL之前的值
/*SListNode *last = cur->next;
cur->next = NULL;
free(last);*/
}
删除某一结点
- 思路:如果
pos
不是尾结点,删除此结点后指向原结点之后的结点。如果是尾结点执行尾删操作即刻。
//时间复杂度O(n),因为有遍历
void SListEraseAfter(SListNode *pos) {
assert(pos != NULL);
//方案一
SListNode *del = pos->next; //记录旧结点
pos->next = pos->next->next;
free(del);
////方案二
//SListNode *next = pos->next->next;
//free(pos->next);
//pos->next = next;
}
删除首次出现某一数据的结点
- 思路:遍历链表,查找结点数据,分类讨论:
- 如果首元素就是此数值,说明首节点就是希望删除的结点,调用头删函数。
- 如果链表中间元素为此数值,将前一个结点的
next
指向本结点的下一个结点。
void SListRemove(SList *list, SLDataType data) {
SListNode *previous = NULL; //赋初值,一般为 NULL
SListNode *cur = list->first;
while (cur != NULL && cur->data != data) {
previous = cur;
cur = cur->next;
}
//跳出 while 循环情况:
//1. cur 为空,表示没有找到
if (cur == NULL) {
return;
}
//2. 找到数据,cur != NULL 且 previous == NULL
//首结点就是目标结点,进行头删
if (previous == NULL) {
SListPopFront(list);
return;
}
//3. 找到数据,cur != NULL 且 previous != NULL
// cur 是中间结点,也是要删的结点,同时previous是要删除的前一个节点
previous->next = cur->next;
//释放掉无效内存
free(cur);
}
头插
- 思路:创建一个新结点,然后对结点的内容与
next
指向进行设置,然后使原链表的头指针指向本结点,本结点的下一结点置为原链表的首结点即可。
//时间复杂度O(1)
void SListPushFront(SList *list, SLDataType data) {
assert(list != NULL);
//1. 创建新 Node 空间
SListNode *node = (SListNode*)malloc(sizeof(SListNode));
assert(node);
//2. 赋值
node->data = data;
//3. 新结点第一个结点的下一个结点,也就是原链表的第一个结点
node->next = list->first;
list->first = node;
//4. 记录新的第一个结点
list->first = node;
//*****前2步也可以用下面封装出的 BuySListNode 函数实现*****
}
尾插
- 思路:先新建一个结点,设置完成后分类讨论:
- 如果要插入的链表为空:
对空链表进行尾插结点,可以直接调用设置好的头插函数进行操作。 - 如果要插入的链表非空:
对非空链表进行尾插结点,遍历链表直到最后一个结点,将创建好的结点连接在原链表的尾结点之后。
对于
新建结点
操作经常使用,所以选择封装
成为一个BuySListNode
函数直接反复调用。
//时间复杂度O(n),因为有遍历操作,单链表
SListNode *BuySListNode(SLDataType data) {
SListNode *node = (SListNode*)malloc(sizeof(SListNode));
assert(node != NULL);
node->data = data;
node->next = NULL;
return node;
}
void SListPushBack(SList *list, SLDataType data) {
assert(list != NULL);
//***********************************
//**** 链表中无结点(空链表)的情况 ****
//***********************************
if (list->first == NULL) {
//链表为空的尾插即头插,调用头插函数
SListPushFront(list, data);
return;
}
//***********************************
//**** 链表中已经有结点的情况 ****
//***********************************
//1. 找到最后一个结点
SListNode *last = list->first;
for (; last->next != NULL; last = last->next);
//此时last是最后一个结点
//2. 申请空间
SListNode *node = BuySListNode(data);
last->next = node;
}
在某一结点后插入新结点
- 思路:新建一个结点,设置完成后将新结点的
next
指向pos
结点之后的结点,然后将pos
结点的next
指向新结点,完成插入。
//时间复杂度O(n),因为有遍历
void SListInsertAfter(SListNode *pos, SLDataType data) {
SListNode *node = BuySListNode(data);
node->next = pos->next;
pos->next = node;
}
销毁申请内存
- 思路:从头结点开始遍历链表所有结点,将每个结点所申请的内存空间逐个释放。
//时间复杂度O(n),因为需要遍历
void SListDestroy(SList *list) {
SListNode *next; //这里需要一个结点的指针作为过渡,记录下一个结点的指向
for (SListNode *cur = list->first; cur != NULL; cur = next) {
next = cur->next;
free(cur);
}
list->first = NULL;
}
最后附上顺序表的接口实现:【https://blog.csdn.net/qq_42351880/article/details/87970232 】