1. 概念
链表就是将线性表的每个结点,用指针串起来,而非直接访问地址方法。
2. 接口实现
2.1. 定义 任意结点 的结构体
typedef int LLDataType;
typedef struct LinkListNode
{
LLDataType data; //数据域
struct LinkListNode *next; //指针域
} LLN, *LL;
LLN:struct LinkListNode
LL:struct LinkListNode *
LL:LLN *
2.2. 创建 空的 有头单向链表(即只有头结点)
- 想要使用 有头单向链表,需要先创建并初始化一个头结点。
- 需要 同链表结构体类型 的指针,指向头结点的地址,即头指针。
2.3. 插入数据 到指定下标位置
注意:这里下标从0开始,需要手动遍历到指定位置
问题:向插入post位置,插入结点
本图思路: 在post - 1结点身后,尾插
- 先获取
post位置前 的结点地址,ph先指向post - 1的结点 - pnew指向生成的新结点地址
- 将新结点,与原
post位置结点,和post - 1位置结点 链接
练习:
一个单链表LinkList中的结点PH后面插入一个结点X下列操作正确的是( )
PH->next = PX->next; PH->next = PX;PX->next = PH->next; PH->next = PX;PH->next = PX; PX->next = PH->next;PH->next = PX; PH->next = PX->next;
第一步pnew链接post - 1的身后
第二部post - 1链接pnew
2.4. 求链表长度(长度 即 有效元素个数)
int LLLength(LL H)
{
int len = 0;
// 头结点,不算长度内,不算有效数据节点
while (H->next != NULL)
{
H = H->next;
len++;
}
return len;
}
2.5. 打印 遍历
值传递的头指针,当其指针域不空,就遍历身后,然后 后移。
2.6. 判断 单向有头链表 是否为空(即仅有 头结点 吗)
头结点的指针域,若为空 则表空 返回真,否则假 返回0
2.7. 查找 指定数据 首次出现的下标位置
逐个遍历的同时,进行匹配
2.8. 修改 指定位置的 数据值
对比插入:
- 插入用的,head 指向 post-1 位置
- 修改用的,head 指向 post 位置
- 删除,也是先移动到post-1位置,再用pdel指向post位置
2.9. 删除 指定位置的结点
思路
- post容错判断
- 移动head 指向 post-1 位置;
- 用pdel指向post位置,即 pdel = head->next
- 链接pdel 的 前驱 与 后继
- 释放被删除的 结点
对比 顺序表的删除
在已知 被删除结点的确切位置 的情况下,链表仅需要删除释放目标结点 重新建立链接,而不需要 移动部分元素。
2.10. 删除 指定数据 的所有结点
思路:在当前结点,侦测其身后结点,而不是指向目标结点后再判断
2.11. 清空 所有数据结点 只剩头结点
头结点的指针域不为空,就删除其后面所有
2.12. 转置 有头单向链表
把 无头链表 的 结点,依次插入到 头结点head 身后
3. 思路总结
- 链表插入删除方便,顺序表插入删除需要移动元素。
- 链表访问元素比较麻烦,需要遍历,顺序表查找方便因为内存连续,通过下标即可查找。
- 顺序表长度固定,链表长度不固定。
4. 总体代码
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#define DEBUG(Str) printf("%s %s %s %d\n", Str, __func__, __FILE__, __LINE__);
// 1. 定义用来操作有头单向链表的结构体(单向链表结点)
typedef int LLDataType;
typedef struct LinkListNode
{
LLDataType data; //数据域
struct LinkListNode *next; //指针域
} LLN, *LL;
// 2. 创建 空的 有头单向链表(即只有头结点)
LL LLInit(void)
{
// 开辟头结点
LL H = (LL)malloc(sizeof(LLN));
if (H == NULL)
{
printf("\n");
return NULL;
}
//初始化头结点,并返回其堆地址
H->next = NULL;
return H;
}
// 3. 计算 长度 即有效元素个数
int LLLength(LL H)
{
int len = 0;
// 头结点,不算长度内,不算有效数据节点
while (H->next != NULL)
{
H = H->next;
len++;
}
return len;
}
// 4. 判断 单向有头链表 是否为空(即仅有 头结点)
int LLisEmpty(LL H)
{
return H->next == NULL ? 1 : 0;
}
// 5. 插入数据 到指定下标位置
// 在post-1位置 尾部插入
int LLInsert(LL H, int post, LLDataType data)
{
// 1. 容错判断[0, 长度len]
// 新位置即len 即长度,可以等于,不可大于
if (post < 0 || post > LLLength(H))
{
DEBUG("insert post err");
return -1;
}
// 2. 移动头指针使其指向post前一个结点,
// 否则无修改其前驱结点的next指针域
// i不能=post,不然就指向了post位置结点
for (int i = 0; i < post; i++)
H = H->next;
// 3. 开辟新结点存放数据
LL PNew = (LL)malloc(sizeof(LLN));
if (NULL == PNew)
{
DEBUG("new node malloc err");
return -1;
}
//赋值初始化
PNew->data = data;
PNew->next = NULL; //最好如此,保险
PNew->next = H->next; //新node 指向 旧post结点
H->next = PNew; //post-1 结点 指向 新node
return 0;
}
// 6. 打印 遍历
void LLPrint(LL H)
{
if (LLisEmpty(H))
{
DEBUG("empty linklist, can't print!");
return;
}
// 头结点next不空,就打印并后挪
while (H->next != NULL)
{
H = H->next;
printf("%d\t", H->data);
}
putchar(10);
}
// 7. 查找 指定数据 首次出现的下标位置
int LLFind(LL H, LLDataType data)
{
// 若存在,则下标从0开始
int post = 0;
while (H->next != NULL)
{
H = H->next;
// 上回的post即当前位置
if (H->data == data)
return post;
else
post++;
}
return -1;
}
// 8. 修改 指定位置的 数据值
int LLModify(LL H, int post, LLDataType data)
{
// 容错判断:有效位置:[0, 长度len)
// 注意:len是长度,比下标大1
// 所以post 不可以等于 len
if (post < 0 || post >= LLLength(H))
{
DEBUG("err post to Modify");
return -1;
}
// 遍历将头指针H指向post结点
for (int i = 0; i <= post; i++)
H = H->next;
// 修改
H->data = data;
return 0;
}
// 9.删除 指定位置的结点
int LLDeletePost(LL H, int post)
{
// 容错判断 [0, len)
// 长度len是不存在的,不可删除
if (post < 0 || post >= LLLength(H))
{
DEBUG("err post to del");
return -1;
}
// 遍历,将头指针指向删除结点的前驱
// 需要将删除结点前驱的指针域next指向删除结点后继
for (int i = 0; i < post; i++)
H = H->next;
// 定义用来删除结点的指针PDel指向被删除的结点,但是不能直接对其释放,否则丢失其后继信息。
LL PDel = H->next;
// 被删除结点 前驱的指针域next指向删除结点后继
// H->next = H->next->next;
H->next = PDel->next;
// 释放被删除的结点
free(PDel);
PDel = NULL;
return 0;
}
// 10.删除 指定数据 的所有结点
void LLDeleteData(LL H, LLDataType data)
{
LL PDel = NULL; // 用于指向被删除结点的一个指针。
while (H->next) // 等价 H->next != NULL
{
if (H->next->data == data)
{
PDel = H->next; //指向被删除的结点
H->next = H->next->next; //跳过被删除结点
free(PDel); //释放被删除结点
PDel = NULL; //指针安全化
}
else
H = H->next; //移动到下一个,继续
}
}
// 11.清空 所有数据结点 只剩头结点
void LLClear(LL H)
{
LL PDel = NULL;
while (H->next != NULL) //身后若有则继续
{
PDel = H->next; //指向被删除的结点
H->next = PDel->next; //跳过,链接到被删除结点的后继
free(PDel);
PDel = NULL;
}
}
// 12.转置 有头单向链表
// {头结点} 11 12 13 14 15 ==>
// {头结点} 15 14 13 12 11
void LLInvert(LL H)
{
// 断开 有头单向链表,断开前需保存头结点,和其后继
// 将其分成 空的有头单向链表 和 无头单向链表
LL P = H->next; //无头单向链表
H->next = NULL; //空的有头单向链表
// 遍历无头单向链表,将每个结点 头插 进 有头单向链表中
while (P != NULL)
{
// 保存无头单向链表中,将要头插结点的 后继的地址
LL T = P->next; //用来下次成为新的头结点(无头链表中)
// 头插
P->next = H->next; //链接 头结点的后继
H->next = P; //成为head的直接后继
// P重新指向无头单向链表的新的起始结点
P = T; //T是临时变量
}
}
int main(int argc, char *argv[])
{
// 创建并初始化,头结点
LL head = LLInit();
// 判断有头单向链表是否空
LLisEmpty(head) ? puts("LL Empty") : puts("LL not Empty");
// 指定位置 插入数据
for (int i = 0; i < 5; i++)
LLInsert(head, i, i * 11 + 1);
// 打印 遍历
LLPrint(head);
// 查看 指定数据 第一次出现的下标位置
printf("%d at post:%d\n", 34, LLFind(head, 34));
// 修改 指定位置 的数据值
LLModify(head, 3, 3333);
LLPrint(head);
// 删除 指定下标位置 的结点
LLDeletePost(head, 3);
LLPrint(head);
// 删除 所有该数据的结点
LLDeleteData(head, 23);
LLPrint(head);
// 有头单向链表的转置
LLInvert(head);
LLPrint(head);
// 清空链表
LLClear(head);
LLPrint(head);
return 0;
}