前言:
本专栏属于数据结构相关内容,附带一些代码加深对一些内容的理解,为方便读者观看,本专栏内的所有文章会同时附带C语言和Python对应的代码,(可自行通过目录跳转到对应的部分)辅助不同主修语言的读者去更好的理解对应的内容,若是代码0基础的读者,可先去博主其他专栏学习一下基础的语法及知识点:
魔法天才的跳转链接:
Python语言:python1_Gu_shiwww的博客-CSDN博客
其他数据结构内容可见:数据结构_Gu_shiwww的博客-CSDN博客
什么是单链表
单链表(Singly Linked List)是一种链式存储结构,由一系列**节点(Node)**组成,每个节点包含两部分
1 链表的特征
逻辑结构:线性结构
存储结构:链式存储结构
特点:内存不连续,通过指针实现
解决顺序表问题:顺序表长度固定和插入删除效率低的问题。
操作:增删改查
链表就是将节点用链串起来的线性表,链就是节点中的引用
链表分为带头结点链表和不带头节点的链表,两者在逻辑结构上都属于链式存储结构,只是带头节点的链表单独拿出一个节点存放首个有效节点的地址,头节点内部也存在数据域,且也有数据存储,但是在逻辑上这个数据域内部的元素无效。带头与不带头节点在代码编写上有细微差别,本文以带头节点的单向链表来进行讲解
【例子】通过一个表格给定一个链表遍历的例子(可自行理解)
(赵, 钱, 孙, 李, 周, 吴, 郑, 王)

以下是用C语言实现单链表的一些具体操作,Python的具体编程实现详见魔法天才预设的跳转路径:
2 编程实现链表
C 语言编程实现
C.a 节点的构建
在学习链表之前,我们要先定义好每个链表连接的节点,在C语言中可以用结构体完成节点的构建
cpp
typedef struct node_t
{
int data; //数据域:存数据
struct node_t *next; //指针域:存放下一个节点的地址
} link_node_t, *link_node_p;
以上定义了一个名为link_node_t的结构体(typedef是对后面的结构体定义进行重命名,link_node_t等价于struct node_t),而重定义名之后的*link_node_p是对结构体指针的名进行重定义
【练习】建立A、B、C、D四个节点,内部数据域可为空,用指针进行连接,最后通过首节点进行逐个遍历打印
1.1 遍历无头单向链表

cpp
#include <stdio.h>
typedef struct node_t
{
int data; //数据域:存数据
struct node_t *next; //指针域:存放下一个节点的地址
} link_node_t, *link_node_p;
int main(int argc, char const *argv[])
{
//1. 定义四个节点
link_node_t A = {1, NULL};
link_node_t B = {2, NULL};
link_node_t C = {3, NULL};
link_node_t D = {4, NULL};
//2. 连接节点
A.next = &B;
B.next = &C;
C.next = &D;
//3. 定义一个头指针指向第一个节点,用于遍历无头单向链表
link_node_p p = &A;
//4. 遍历无头单向链表
while (p != NULL)
{
printf("%d ", p->data); //打印所指节点数据
p = p->next; //将指针向后移动一个单位
}
printf("\n");
return 0;
}
1.2 遍历有头单向链表

cpp
#include <stdio.h>
typedef struct node_t
{
int data; //数据域:存数据
struct node_t *next; //指针域:存放下一个节点的地址
} link_node_t, *link_node_p;
int main(int argc, char const *argv[])
{
//1. 定义四个节点
link_node_t A = {1, NULL};
link_node_t B = {2, NULL};
link_node_t C = {3, NULL};
link_node_t D = {4, NULL};
//2. 连接节点
A.next = &B;
B.next = &C;
C.next = &D;
//3. 定义一个头节点,数据域无效,指针域指向第一个节点
link_node_t H;
H.next = &A;
//4. 定义一个头指针指向头节点,用于遍历有头单向链表
link_node_p p = &H;
//5. 遍历有头单向链表
#if 0
//方法一: 循环里面先移动再打印
while (p->next != NULL)
{
p = p->next; //向后移动一个单位
printf("%d ", p->data);
}
printf("\n");
#else
//方法二:
//先跨越头节点,相当于让头指针指向了一个无头单向链表
p = p->next;
while (p != NULL)
{
printf("%d ", p->data);
p = p->next;
}
printf("\n");
#endif
return 0;
}
1.3 链表尾插法
写一个有头单向链表,用于保存输入的学生成绩,实现一输入学生成绩就创建一个新的节点,将成绩保存起来。再将该节点链接到链表的尾,直到输入-1结束。
要求:每个链表的节点由动态内存分配得到 , 也就是用malloc。
过程:
- malloc申请空间link_node_t大小作为头节点
- 将新节点放到链表尾部
cpp
#include <stdio.h>
#include<stdlib.h>
typedef struct node_t
{
int data;
struct node_t *next;
} link_node_t, *link_node_p;
int main(int argc, char const *argv[])
{
link_node_p pnew = NULL; //用于指向新建节点
link_node_p ptail = NULL; //用于指向尾节点
int score;
//1. 创建一个头节点并初始化
link_node_p p = (link_node_p)malloc(sizeof(link_node_t));
if (NULL == p)
{
perror("p malloc err");
return -1;
}
p->next = NULL; //初始化头节点
//2.让尾指针指向头节点
ptail = p;
//3.循环输入学生成绩-1结束,如果不是-1那就新建一个节点保存成绩尾插到链表
while (1)
{
scanf("%d", &score);
if (-1 == score)
break;
//(1) 开辟新节点空间,让pnew指向新节点
pnew = (link_node_p)malloc(sizeof(link_node_t));
if (NULL == pnew)
{
perror("pnew err");
return -1;
}
//(2) 初始化新节点
pnew->data = score;
pnew->next = NULL;
//(3) 连接新节点到链表尾部
ptail->next = pnew;
//(4) 移动尾指针到新节点
ptail = pnew;
}
//4. 遍历有头链表
p = p->next;
while (p != NULL)
{
printf("%d ", p->data);
p = p->next;
}
printf("\n");
return 0;
}
C.b 有头链表的函数操作
C.1 C语言编程操作的函数接口
cpp
#ifndef __LINKLIST_H__
#define __LINKLIST_H__
typedef int datatype;
typedef struct node_t
{
datatype data;
struct node_t *next;
}link_node_t,*link_node_p;
//1.创建一个空的有头单向链表
link_node_p createEmptyLinkList();
//2.链表指定位置插入数据
int insertIntoPostLinkList(link_node_p p,int post, datatype data);
//3.计算链表的长度。
int lengthLinkList(link_node_p p);
//4.遍历链表
void showLinkList(link_node_p p);
//5.链表指定位置删除数据
int deletePostLinkList(link_node_p p, int post);
//6.判断链表是否为空
int isEmptyLinkList(link_node_p p);
//7.清空单向链表
void clearLinkList(link_node_p p);
//8.修改指定位置的数据 post 被修改的位置 data修改成的数据
int changePostLinkList(link_node_p p, int post, datatype data);
//9.查找指定数据出现的位置 data被查找的数据 //search 查找
int searchDataLinkList(link_node_p p, datatype data);
//10.删除单向链表中出现的指定数据,data代表将单向链表中出现的所有data数据删除
int deleteDataLinkList(link_node_p p, datatype data);
//11.转置链表
void reverseLinkList(link_node_p p);
#endif
C.2 创建一个空的有头单向链表
cpp
//创建一个空的有头单向链表
link_node_p createEmptyLinkList()
{
//1.开辟头节点空间
link_node_p p = (link_node_p)malloc(sizeof(link_node_t));
if (NULL == p)
{
perror("p err");
return NULL;
}
//2. 初始化头节点
p->next = NULL;
//3. 返回头节点地址
return p;
}
定义了一个名为createEmptyLinkList()的函数,返回的数据类型是结构体指针,内部用malloc函数动态开辟了一个头节点,且将头节点初始化(即将头节点的next指针指向NULL),返回开辟成功的结构体指针
C.3 计算链表长度:length
cpp
//计算链表长度 length:长度
int lengthLinkList(link_node_p p)
{
int len = 0;
while (p->next != NULL)
{
p = p->next;
len++;
}
return len;
}
通过while循环遍历整个链表,只要链表的next指针不为空,就进入while循环,将p的指针指向下一个节点,直到最后一个next域为空的节点停止循环,同时在循环外部定义一个len变量记录整个链表的长度只要while循环执行一次len就要+1,最终返回len(即链表的长度)
C.4 向单向链表的指定位置插入数据
cpp
//向单向链表的指定位置插入数据
//p保存链表的头指针 post 插入的位置 data插入的数据
int insertIntoPostLinkList(link_node_p p, int post, datatype data)
{
// 1. 容错判断: post<0 || post>长度
if (post < 0 || post > lengthLinkList(p))
{
printf("insert err\n");
return -1;
}
// 2. malloc新建一个节点
link_node_p pnew = (link_node_p)malloc(sizeof(link_node_t));
if (NULL == pnew)
{
perror("pnew err");
return -1;
}
// 3. 初始化新节点
pnew->data = data;
// 4. 将指针移动到插入位置的前一个
for (int i = 0; i < post; i++)
p = p->next;
// 5. 将新节点连接到链表(先连后面再连前面)
pnew->next = p->next;
p->next = pnew;
return 0;
}
首先容错判断,判断要插入位置post的合理性,不能小于0也不能大于链表的长度
2、3步新建一个节点,并且初始化将传入的数据data初始化到节点内部
第4步通过for循环遍历链表,将指针指向要插入元素的前一个,之后第5步将该节点连入链表,区分代码中的p和pnew,先连后面再连前面(顺序交换也无所谓)
注意:为什么要将p移动到前面一个位置是因为要找到被插入位置上一个节点的next指针,因为单链表是单向遍历的,只能从前往后遍历,找到post位置的前一个位置才能与插入位置的前一个节点的next指针进行连接
C.5 遍历单向链表
cpp
//遍历单向链表
void showLinkList(link_node_p p)
{
while (p->next != NULL)
{
p = p->next;
printf("%d ", p->data);
}
printf("\n");
}
移动p指针,打印p的data域,注意函数中p指针的移动并不会改变开辟的动态结构体链表的头指针
C.6 判断链表为空,为空返回1,不为空返回0
cpp
//判断链表为空,为空返回1,不为空返回0
int isEmptyLinkList(link_node_p p)
{
return p->next == NULL;
}
函数可以直接返回p的next域是否为NULL的判断是结果,满足为1,不满足为0
C.7 删除单向链表中指定位置的节点
cpp
//删除单向链表中指定位置的数据 post 代表的是删除的位置
int deletePostLinkList(link_node_p p, int post)
{
// 1.容错判断:判空 || post<0 || post>=长度
if (isEmptyLinkList(p) || post < 0 || post >= lengthLinkList(p))
{
printf("delete err\n");
return -1;
}
// 2.将指针移动到删除位置的前一个节点
for (int i = 0; i < post; i++)
p = p->next;
// 3.设指针pdel指向要删除节点
link_node_p pdel = p->next;
// 4.前后跨过要删除节点
p->next = pdel->next;
// 5.释放要删除节点
free(pdel);
return 0;
}
首先容错判断,判断post值的合法性以及链表是否为空,若链表为空则无元素可删
第2步同插入步骤,找到要删除节点的前一个位置(因为要断开post位置的前一个节点的next指针)然后定义一个pdel指针指向要删除的节点,也可以通过p->next = p->next->next直接删除post位置的节点,最后不要忘记释放新定义的节点
C.8 删除指定数据的所有节点
cpp
//删除单向链表中出现的指定数据,data代表将单向链表中出现的所有data数据删除
void deleteDataLinkList(link_node_p p, datatype data)
{
link_node_p t = p->next; //让t指向头节点的后一个节点
while (t != NULL) //相当于让t遍历无头链表
{
if (t->data == data) //判断成功删除节点并向后继续遍历
{
//(1)跨过要删除节点
p->next = t->next;
//(2)释放要删除节点
free(t);
//(3)让t指向p的下一个继续向后遍历
t = p->next;
}
else //p和t继续向后遍历
{
p = p->next;
t = t->next;
}
}
}
只需要从头开始遍历,如果发现某一个节点的数据与传参的数据相同,则进入if判断,断开当前节点就好,请注意此时t指针和p指针指向的并不是同一个节点,p指针在t指针指向的前一个结点,于是可以通过p指针访问到该元素的前一个节点。还需注意要删除所有与data相同的数据,所以并不是找到一个数据就结束了,要循环遍历到链表尾。
C.9 清空链表
cpp
//清空链表数据
//思想:一直删除头节点的后一个,直到为空链表为止
void clearLinkList(link_node_p p)
{
while (p->next != NULL)
{
link_node_p pdel = p->next;
p->next = pdel->next;
free(pdel);
}
}
从头开始遍历,遍历到一个断开一个,记得将被断开的节点free掉
C.10 修改指定位置的数据
cpp
//修改指定位置的数据 post 被修改的位置 data修改成的数据
int changePostLinkList(link_node_p p, int post, datatype data)
{
// 1.容错判断:判空 || post<0 || post>=长度
if (isEmptyLinkList(p) || post < 0 || post >= lengthLinkList(p))
{
printf("change err\n");
return -1;
}
//2.将指针遍历到修改节点位置
for (int i = 0; i <= post; i++)
p = p->next;
//3.修改节点中数据
p->data = data;
return 0;
}
修改指定位置,首先也要进行容错判断,看post值是否合法,以及判断链表是否为空,空链表无内容可改。
接着通过for循环遍历到要修改的节点,直接进行data域的内容修改
C.11 查找指定数据出现的位置
cpp
//查找指定数据出现的位置 data被查找的数据 //search 查找
int searchDataLinkList(link_node_p p, datatype data)
{
int post = 0; //记录查找的位置
while (p->next != NULL)
{
p = p->next;
if (p->data == data)
return post;
post++;
}
return -1; //说明数据不存在
}
首先循环遍历整个链表,当遍历到与传入的参数data值相同的数据时,直接return,函数内部循环不再执行,同时定义一个post变量记录下表,若查询到结果时直接返回post
C.12 转置链表
cpp
//转置链表
//解题思想:
//(1) 将头节点与当前链表断开,断开前保存下头节点的下一个节点,保证后面链表能找得到,定义一个q保存头节点的下一个节点,断开后前面相当于一个空的链表,后面是一个无头的单向链表
//(2) 遍历无头链表的所有节点,将每一个节点当做新节点插入空链表头节点的下一个节点(每次插入的头节点的下一个节点位置)
void reverseLinkList(link_node_p p)
{
link_node_p t = NULL; //让t在循环里一直记录q的下一个,防止头插q之后链表找不到了
link_node_p q = p->next; //让q记录一下头节点的后一个节点
p->next = NULL; //断开头节点
while (q != NULL) //相当于遍历无头单向链表
{
// 让t记录q的下一个,不然头插以后链表找不到了
t = q->next;
//头插:将q插入到p后面,先连后面再连前面
q->next = p->next;
p->next = q;
//让q去找t
q = t;
}
}
总结:先断开头节点与后面数据的连接,然后再定义一个指针去记录要头插节点的后一个节点,一次向后移动一个一个头插如原来链表头节点的后面,实现整个链表的逆置
C.13 完整代码(可运行)
cpp
#include <stdio.h>
#include <stdlib.h>
typedef int datatype;
typedef struct node_t
{
datatype data;
struct node_t *next;
}link_node_t,*link_node_p;
//创建一个空的有头单向链表
link_node_p createEmptyLinkList()
{
//1.开辟头节点空间
link_node_p p = (link_node_p)malloc(sizeof(link_node_t));
if (NULL == p)
{
perror("p err");
return NULL;
}
//2. 初始化头节点
p->next = NULL;
//3. 返回头节点地址
return p;
}
//计算链表长度 length:长度
int lengthLinkList(link_node_p p)
{
int len = 0;
while (p->next != NULL)
{
p = p->next;
len++;
}
return len;
}
//向单向链表的指定位置插入数据
//p保存链表的头指针 post 插入的位置 data插入的数据
int insertIntoPostLinkList(link_node_p p, int post, datatype data)
{
// 1. 容错判断: post<0 || post>长度
if (post < 0 || post > lengthLinkList(p))
{
printf("insert err\n");
return -1;
}
// 2. malloc新建一个节点
link_node_p pnew = (link_node_p)malloc(sizeof(link_node_t));
if (NULL == pnew)
{
perror("pnew err");
return -1;
}
// 3. 初始化新节点
pnew->data = data;
// 4. 将指针移动到插入位置的前一个
for (int i = 0; i < post; i++)
p = p->next;
// 5. 将新节点连接到链表(先连后面再连前面)
pnew->next = p->next;
p->next = pnew;
return 0;
}
//遍历单向链表
void showLinkList(link_node_p p)
{
while (p->next != NULL)
{
p = p->next;
printf("%d ", p->data);
}
printf("\n");
}
//判断链表为空,为空返回1,不为空返回0
int isEmptyLinkList(link_node_p p)
{
return p->next == NULL;
}
//删除单向链表中指定位置的数据 post 代表的是删除的位置
int deletePostLinkList(link_node_p p, int post)
{
// 1.容错判断:判空 || post<0 || post>=长度
if (isEmptyLinkList(p) || post < 0 || post >= lengthLinkList(p))
{
printf("delete err\n");
return -1;
}
// 2.将指针移动到删除位置的前一个节点
for (int i = 0; i < post; i++)
p = p->next;
// 3.设指针pdel指向要删除节点
link_node_p pdel = p->next;
// 4.前后跨过要删除节点
p->next = pdel->next;
// 5.释放要删除节点
free(pdel);
return 0;
}
//思想:一直删除头节点的后一个,直到为空链表为止
void clearLinkList(link_node_p p)
{
while (p->next != NULL)
{
link_node_p pdel = p->next;
p->next = pdel->next;
free(pdel);
}
}
//修改指定位置的数据 post 被修改的位置 data修改成的数据
int changePostLinkList(link_node_p p, int post, datatype data)
{
// 1.容错判断:判空 || post<0 || post>=长度
if (isEmptyLinkList(p) || post < 0 || post >= lengthLinkList(p))
{
printf("change err\n");
return -1;
}
//2.将指针遍历到修改节点位置
for (int i = 0; i <= post; i++)
p = p->next;
//3.修改节点中数据
p->data = data;
return 0;
}
//查找指定数据出现的位置 data被查找的数据 //search 查找
int searchDataLinkList(link_node_p p, datatype data)
{
int post = 0; //记录查找的位置
while (p->next != NULL)
{
p = p->next;
if (p->data == data)
return post;
post++;
}
return -1; //说明数据不存在
}
//删除单向链表中出现的指定数据,data代表将单向链表中出现的所有data数据删除
void deleteDataLinkList(link_node_p p, datatype data)
{
link_node_p t = p->next; //让t指向头节点的后一个节点
while (t != NULL) //相当于让t遍历无头链表
{
if (t->data == data) //判断成功删除节点并向后继续遍历
{
//(1)跨过要删除节点
p->next = t->next;
//(2)释放要删除节点
free(t);
//(3)让t指向p的下一个继续向后遍历
t = p->next;
}
else //p和t继续向后遍历
{
p = p->next;
t = t->next;
}
}
}
//转置链表
//解题思想:
//(1) 将头节点与当前链表断开,断开前保存下头节点的下一个节点,保证后面链表能找得到,定义一个q保存头节点的下一个节点,断开后前面相当于一个空的链表,后面是一个无头的单向链表
//(2) 遍历无头链表的所有节点,将每一个节点当做新节点插入空链表头节点的下一个节点(每次插入的头节点的下一个节点位置)
void reverseLinkList(link_node_p p)
{
link_node_p t = NULL; //让t在循环里一直记录q的下一个,防止头插q之后链表找不到了
link_node_p q = p->next; //让q继续一下头节点的后一个节点
p->next = NULL; //断开头节点
while (q != NULL) //相当于遍历无头单向链表
{
// 让t记录q的下一个,不然头插以后链表找不到了
t = q->next;
//头插:将q插入到p后面,先连后面再连前面
q->next = p->next;
p->next = q;
//让q去找t
q = t;
}
}
int main(int argc, char const *argv[])
{
link_node_p p = createEmptyLinkList();
insertIntoPostLinkList(p, 0, 10);
insertIntoPostLinkList(p, 1, 20);
insertIntoPostLinkList(p, 2, 30);
insertIntoPostLinkList(p, 3, 40);
insertIntoPostLinkList(p, 4, 60);
showLinkList(p);
deletePostLinkList(p, 2);
showLinkList(p);
changePostLinkList(p, 1, 50);
showLinkList(p);
printf("50 post is: %d\n", searchDataLinkList(p, 50));
insertIntoPostLinkList(p, 2, 50);
showLinkList(p);
deleteDataLinkList(p, 50);
showLinkList(p);
reverseLinkList(p); //转置链表
showLinkList(p);
// clearLinkList(p);
// if (isEmptyLinkList(p))
// printf("empty!\n");
return 0;
}