一、前言
此文档仅做记录并且分享自己学习C语言数据结构的过程,学艺不精,code新人,如有错误欢迎在评论区讨论、指正!,若文章内容对你有帮助,希望可以给我点个赞!
此文章为数据结构链表的代码实现部分,内容不会面面俱到,但大体会囊括主要代码。
二、链表的代码实现
链表的精髓就在于我一次只要一个可以存储特定数据类型的空间即可,一有需要就申请空间;不存在大量的空间浪费。为了使大量的同类型数据产生联系,我们在申请一点空间用于存储下一个节点的地址。
1. 单链表
我们将构成链表的最小单位叫做 ”节点“,由链表的设计思路可知,节点应该是一个可以存储数据和下一个节点地址的自定义数据类型。也就是需要通过结构体来实现。
a. 创建单链表的节点
- 用于存储数据的数据域(约定其名称为 data )
- 可以指向下一个节点的结构体指针(约定其名称为 next )
//声明一个链表的节点
typedef struct node{
Type data; // 数据域,用于存储数据
struct node *next; //这里定义指针时不能使用pList因为程序执行到这里时它还不认识pList
}List, *pList; // pList 就是 struct node * 起别名
b. 创建头节点(初始化单链表)
头节点的作用是作为整条链表的”入口“,单链表的所有的操作都从这里开始。头节点的数据域可以存数据,也可以不存;这里规定头节点不存数据。
- 申请头节点空间
- 头节点指针指向 NULL
#include <stdlib.h> // malloc 函数所需头文件
pList list_init(void)
{
pList p = malloc(sizeof(List)); // 申请空间
if(NULL == p){ // 若空间申请失败
perror("malloc");
return NULL;
}
//初始化头节点
p->next = NULL;
return p;
}
c. 单链表判空
在约定头节点不存数据的情况下,头节点后无数据节点则视为空
//链表判空
int list_empty(pList l)
{
if(NULL == l) //判断链表是否存在
return -1;
if(NULL == l->next){ // 判断链表是否为空
return 1; // 为空返回 1
}else{
return 0; // 非空
}
}
d. 单链表添加元素
在看下面添加节点的代码之前,先需要明白下面这个语句是什么意思:
这条语句关乎到链表的遍历,之后会经常用到。
p = p->next;
这里的 p 是一个指向单链表节点的指针,假设现在它指向头节点。
p->next 是p指向节点的下一个节点
p = p->next; 实际上就是将 p 指向当前节点的下一个节点,p->next当然也会随之改变。
链表添加元素实际上就是在原表上添加新的节点,要添加新的节点首先我们需要创建节点,再将数据存入该节点,最后将该节点放到相应的位置。
接下来正式进入添加元素的代码实现部分:
- 头插法:每次都从头节点添加元素,需要注意的是不能丢失原来的数据。
- 创建新节点,新节点数据为 9,next 暂无指向
- 新节点指针指向头节点的下一个节点
- 头节点指向新节点
值得注意的是,以上两步顺序不可交换,否则会找不到原来单链表头节点后面的数据
颠倒操作结果如下:
//头插法插入
int list_insert_head(pList l, Type data)
{
if(NULL == l){
return -1;
}
//创建一个数据节点存数据
pList p = malloc(sizeof(List));
if(NULL == p){
perror("insert malloc");
return -1;
}
//存储数据
p->data = data;
//先记录头节点原来下一个节点
p->next = l->next;
//更改头节点的next指针指向
l->next = p;
return 0;
}
- 尾插法:每次添加节点从链表的最后一个节点处添加,最后一个节点的标志即 next 指向 NULL
- 创建新节点,新节点数据为 9,next 暂无指向
- 找到链表尾,并且链表尾 next 指向新节点,新节点 next 指向 NULL
//尾插法插入
int list_insert_tail(pList l, Type data)
{
//判断表头是否有效
if(NULL == l)
return -1;
//找链表的尾巴
while(l->next) //l->next != NULL
l = l->next;
//当程序执行到这里 说明l一定是指向尾节点
//创建一个节点存储数据
pList p = malloc(sizeof(List));
if(NULL == p)
return -1;
//存储插入的数据
p->data = data;
//在尾部插入节点p 且此时p成为新的尾节点
p->next = NULL;
l->next = p;
return 0;
}
e. 单链表删除元素
删除节点实际上需要两个指针,
- l (字母L的小写) 指针指向链表头,用于遍历链表
- 指针p用于指向要删除的节点;若不使用第二个指针记录要删除的节点,要删除节点就会被“流放”到内存空间中,后续无法找到这个节点,但是它又一直占用内存·。
假设这里删除数据节点 2 ,则步骤为:
- 遍历链表,找到需要删除的节点,让 p 指向节点 2
- l 指针指向节点的 next 指向要删除节点的下一个节点 2
- 释放 p 指向的节点
- 删除所有的值符合条件的节点
//按值删除对应节点
int list_delete_data(pList l, Type data)
{
if(NULL == l)
return -1;
pList p; //暂存需要删除的节点的指针
while(l->next){ //l->next ----> l->next != NULL
if(data == l->next->data){//判断l->next->data是否是需要删除的数据
p = l->next; //记录需要删除的节点
l->next = p->next; // l->next = l->next->next;
free(p); // 释放要删除的节点
continue; // 找到删除节点并删除时,无需移动指针,否则有些节点会被跳过
}
l = l->next;
}
return 0;
}
- 删除对应位置的节点
//按位置删除对应节点
int list_delete_pos(pList l, const int pos){
if(NULL == l)
return -1;
// 删除节点
int i = 0; // 记录现在所在位置
while(l->next){
i++;
if(pos == i)//到达要删除位置
{
pList p = l->next;
l->next = l->next->next;
free(p);
break;
}
}
return 0;
}
f. 单链表查询元素
查询和删除操作很像,只是找到对应节点所做的事情不一样。
- 按值查找对应节点的位置
//按值查找对应节点的位置
int list_select_data(pList l, Type data)
{
if(NULL == l)
return -1;
//记录对应值的下标
int index = 0;
while(l->next){
index++;
if(l->next->data == data)
return index;
l = l->next;
}
//当程序执行到这里时表明链表中没有对应的数据
return 0; //用0标记未找到的情况
}
- 按位置查找对应的节点的值
//根据位置查找值:函数返回值只表示查找成功还是失败 查到的值通过参数返回
int list_select_pos(pList l, int pos, Type *data)
{
if(NULL == l || NULL == data)
return -2;
if(pos <= 0)
return -1;
int index = 0;
while(l->next){
index++;
if(index == pos){
*data = l->next->data;
return 0;
}
l = l->next;
}
//当程序执行到这里说明遍历完了链表也没找到对应的pos
return -1;
}
g. 单链表修改元素
- 更改所有值符合条件的对应节点的值
//更改所有值符合条件的对应节点的值
int list_change_data(pList l, Type oldnum, Type newnum){
if(NULL == l)
return -1;
while(l->next){
if(oldnum == l->next->data){
l->next->data = newnum;
continue;
}
l = l->next;
}
return 0;
}
- 更改对应位置节点的值
//更改对应位置节点的值
int list_change_pos(pList l, const int pos, Type data){
if(NULL == l)
return -1;
// 删除节点
int i = 0; // 记录现在所在位置
while(l){
i++;
if(pos == i)//到达要删除位置
{
l->data = data;
break;
}
l = l->next;
}
return 0;
}
h. 单链表打印元素
void list_show(pList l)
{
//因为约定头节点不存数据所以可以不用show头
while(l->next != NULL){ //while(l->next)
printf("%d ", l->next->data);
l = l->next; //指针指向下一个节点
}
puts("");
}
i. 链表清空
链表清空就是删除所有数据节点保留头节点
int list_clear(pList l)
{
if(NULL == l)
return -1;
pList p;
while(l->next){
p = l->next; //记录要删除的节点
l->next = l->next->next; //l->next = p->next;
free(p); //释放对应的数据节点
}
return 0;
}
j. 销毁链表
连同头节点一起释放,由于需要更改到原本指向头节点的指针的指向,故需要用 pList * 类型做参数。
值得注意的是:当销毁链表时需要更改原头节点指针的指向,否则可能会导致程序出现段错误。 经历了前面内容的学习,你需要知道删除节点要释放节点所使用的空间,被释放的空间我们是没有使用权的;而当我们删除头节点时,头节点指针依然指向原空间。
释放头节点的空间有如下过程:
- 使用函数free(head)之后,头节点的指针依然指向原来那片空间
- 更改头节点指向为 NULL
//销毁链表:将头也释放 并且将指向头节点的实参指针指向NULL
int list_destroy(pList *L){
if(NULL == *L)
return -1;
pList p,q;
p = *L;
while(p){
q = p->next;
free(p);
p = q;
}
*L = NULL; //更改原本头节点的指向
return 0;
}
2.单向循环链表
单向循环链表与单向链表不同的点就在尾节点的指针指向头节点。
a. 创建单向循环链表节点
节点构造和单链表节点其实是相同的,同样只需要定义一个拥有数据域 data 和指向自己的指学域 next ;我们的构造思路同样是头节点数据域不存储数据。
typedef struct circle_list{
Type data;
struct circle_list *next;
}Cnode, *pClist;
b. 创建头节点(初始化单链表)
初始化和单链表就有所不同了,我们需要确保尾节点的指针指向头节点。而在初始化阶段,头节点就是尾节点。
//初始化
pClist Clist_init(void){
pClist p = malloc(sizeof(Cnode));
if(NULL == p){
perror("init error");
return NULL;
}
p->next = p; // 头节点指针域指向自己
return p;
}
c. 单向循环链表判空
就是看头节点的 next 是否指向自己,单向循环空的状态其实和它刚初始化完是一样的。
// 判空
bool Clist_empty(pClist p){
if(NULL == p){ // 判断链表是否存在
return false;
}
if(p->next == p){ // 链表判空
return true;
}
}
d. 单向循环链表添加元素
这里使用尾插法。
有下图单向循环链表,添加数据域为 3 的节点。
- 新节点指向头节点
- 旧尾节点指向新节点
// 尾插法
bool Clist_insert_tail(pClist l, const Type data){
if(!Clist_exit(l)) //若表不存在则返回false
return false;
//找到尾节点
while(head != l->next) // 循环结束则有 l->next == head
l = l->next;
pClist p = malloc(sizeof(Cnode));
if(NULL == p){
return false;
}
p->data = data;
p->next = l->next; // 因为此时 l->next就是 head
l->next = p;
return true;
}
e. 单向循环链表删除元素
按值删除节点,和单链表的删除操作相差不大,只是循环链表的终止条件不同。这里只给出按值删除(删除所有值为 data 的节点)
//按值删除节点
bool Clist_delete_data(pList l, Type data)
{
if(NULL == l || list_empty(l))
return false;
pList head = l, t; //t记录需要删除的节点
while(head != l->next){ // 注意循环终止条件即可
if(data == l->next->data){
t = l->next;
l->next = l->next->next; //l->next = t->next
free(t);
continue; // break;删除值为 data 的一个节点
}
l = l->next;
}
return true;
}
f. 单向循环链表查询元素
// 按位置查询节点的值
// 参数中data是地址传递,可以用于接收查询节点的数据,若不接收填 NULL 即可
bool Clist_select_pos(pClist l, const int pos, Type *data){
if(!Clist_exit(l))
return false;
if(0 >= pos) // 判断输入pos的合法性
return false;
int index = 0; // 用于逐渐逼近 pos 的数字
pClist head = l; //记录头节点位置
while(head != l->next){
index++;
if(pos == index){
*data = l->next->data;
return true;
}
l = l->next;
}
// 程序运行到此说明pos大于链表元素个数,是非法值
return false;
}
// 按值查询第一次出现该值的节点位置
bool Clist_select_data(pClist l, int *pos, const Type data){
if(!Clist_exit(l))
return false;
int index = 0;
pClist head = l;
while(head != l->next){
index++;
if(data == l->next->data){
*pos = index;
return true;
}
l = l->next;
}
// 程序运行到此说明表中没有这个data
return false;
}
g. 单向循环链表修改元素
// 按位置更改节点值
bool Clist_change_pos(pClist l, const int pos, const Type data){
if(!Clist_exit(l))
return false;
if(0 >= pos)
return false;
int index = 0; // 用于逐渐逼近 pos 的数字
pClist head = l; //记录头节点位置
while(head != l->next){
index++;
if(pos == index){
l->next->data = data;
return true;
}
l = l->next;
}
// 程序运行到此说明pos大于链表元素个数,是非法值
return false;
}
// 按值更改节点值
bool Clist_change_data(pClist l, const Type oldnum, const Type newnum){
if(!Clist_exit(l))
return false;
int index = 0;
pClist head = l;
while(head != l->next){
index++;
if(oldnum == l->next->data){
l->next->data = newnum;
continue;
}
l = l->next;
}
// 程序运行到此说明表中没有这个data
return false;
}
h. 单向循环链表打印元素
bool Clist_show(pClist l)
{
if(NULL == l)
return false;
//记录头节点 做为结束判断的依据
pClist head = l;
while(head != l->next){
printf("%d ", l->next->data);
l = l->next;
}
puts("");
return true;
}
i. 单向循环链表清空
实际上就是删除除了头节点之外的节点。
bool Clist_clear(pList l)
{
if(NULL == l || list_empty(l))
return false;
pList head = l, t; //t记录需要删除的节点
while(head != l->next){
t = l->next;
l->next = l->next->next; //l->next = t->next
free(t);
l = l->next;
}
return true;
}
j. 单向循环链表销毁
要记得我们需要更改头节点指向。程序逻辑和单链表相似,不做赘述。
bool Clist_clear(pList *l) //因为我们需要更改到原指针的指向,所以需要二级指针为参数
{
pList p = *l;
pList t;//用于记录要删除节点的指针
if(NULL == p)
return false;
while(*l != p->next){ // 循环结束链表只剩头节点
t = p->next;
p->next = p->next->next; //l->next = t->next
free(t);
}
free(p);// 释放头节点
*l = NULL;// 更改原头节点指针的指向
return true;
}
3. 双向循环链表
a. 创建双向循环链表的节点
初始化头节点,这里约定头节点不存数据。
typedef struct node{
int data;
struct node *pre, *next;
}List, *pList; //pList == struct node *
b. 创建头节点(初始化双向循环链表)
这次初始化函数的设计思路变换了一下:通过参数形式获得结构体的地址,返回值返回的是函数退出的状态。
int list_init(pList *L) //通过参数形式获得结构体的地址,返回值返回的是函数退出的状态
{
*L = malloc(sizeof(List));
if(NULL == *L)
return -1;
(*L)->next = (*L)->pre = *L;
return 0;
}
c. 双向循环链表判空
int list_empty(pList l)
{
if(NULL == l) // 判断链表是否存在
return -1;
if(l==l->pre==l->next)// 当一个节点的两个指针都指向头节点则空
return 0;
}
d. 双向循环链表添加元素
头插法:
头插法的思路是让新节点成为新的头节点,步骤同样先动新节点指针,再动链表指针
先动新节点指针:
新节点 pre 指向尾端节点, next 指向头节点
后动链表指针
头节点的 pre 指向新节点,原尾端节点的 next 指向新节点
int list_insert_tail(pList l, const int data)//用const可以保证自己不会修改到 data 的值
{
if(NULL == l)
return -1;
pList p = malloc(sizeof(List));
if(NULL == p)
return -1;
p->data = data;
//先动新节点指针
p->next = l;
p->pre = l->pre;
l->pre = p;
p->pre->next = p;
return 0;
}
尾插法:
尾插法步骤大致分为:先动新节点的指针,后动链表的指针
- 先动新节点指针:
a.新节点 pre 指向尾端节点(可以和 b 步骤颠倒)
b.新节点 next 指向头节点(可以和 a 步骤颠倒)
- 后动链表指针
c.头节点的 pre 指向新节点(可与 d 颠倒)
d.原尾端节点的 next 指向新节点(可与 c 颠倒)
// 尾插法
int list_insert_tail(pList head, const int data){
if(NULL == head)
return -1;
pList p = malloc(sizeof(List));
if(NULL == p)
return -1;
p->data = data;
p->pre = head->pre;
p->next = head;
p->next->pre = p;
p->pre->next = p;
return 0;
}
e. 双向循环链表删除元素
- b. 单方面断开链表与 p 所指节点的连接
- c.释放 p 所指的节点
int list_delete_data(pList l, int data)
{
if(NULL == l)
return -1;
pList head = l, q;
while(l->next != head){
if(data == l->next->data){
q = l->next;
l->next = q->next; //l->next = l->next->next;
l->next->front = l; //q->next->front = l;
free(q);
}else{
l = l->next;
}
}
return 0;
}
f. 双向循环链表查询元素
查询首次出现对应值的位置
int list_select_data(pList l, const int data, int *pos)
{
if(NULL == l)
return -1;
pList head = l;
int index = 0; //记录当前位置
while(head != l->next){
if(data == l->next->data)
{
*pos = index+1;// 获得对应节点的位置
printf("%d\n", index+1);
return 0; //找到则退出函数
}
l = l->next;
index++;
}
// 程序运行到此说明没有该节点
printf("值为 %d 的节点不存在", data);
return -1;
}
g. 双向循环链表修改元素
和查询操作差不多,只是找到节点做的操作不同。
这里给出按值修改首次出现该值的节点的值的代码
int list_change_data(pList l, const int olddata, const int newdata)
{
if(NULL == l)
return -1;
pList head = l;
while(head != l->next){
if(olddata == l->next->data)
{
l->next->data = newdata;
return 0; //修改后直接退出函数
}
l = l->next;
}
// 程序运行到此说明链表中不存在存有这个值的节点
printf("值为 %d 的节点不存在", olddata);
return -1;
}
h. 双向循环链表打印元素
int list_show(pList l)
{
if(NULL == l)
return -1;
pList head = l;
while(head != l->next){
printf("%d ", l->next->data);
l = l->next;
}
puts("");
return 0;
}
i. 双向循环链表清空
在删除节点函数的代码更改一下即可
int list_clear(pList l)
{
if(NULL == l)
return -1;
pList head = l, q;
while(l->next != head
{
q = l->next;
l->next = q->next; //l->next = l->next->next;
l->next->pre = l; //q->next->front = l;
free(q);
}
return 0;
}
j. 销毁双向循环链表
链表清空之后,释放头节点,并且需要更改头指针的指向。
int list_clear(pList *L)
{
if(NULL == *L)
return -1;
pList p = *L;
pList q;// 记录要删除的节点
// 清空链表
while(p->next != head
{
q = p->next;
p->next = q->next; //p->next = p->next->next;
p->next->pre = p; //q->next->front = p;
free(q);
}
free(p);
*L == NULL; //更改头指针指向
return 0;
}
三、总结
最重要的就是单链表,单链表是我们理解“链式存储”的起源,当掌握了单链表的逻辑和代码,相信学会后面的单向循环链表和双向循环链表对你来说不成问题!
不仅如此,掌握单链表在后面学习栈、对列、树都是很有帮助的,所以单链表的理解十分重要。
特别鸣谢
感谢能够看到这里的各位,文章有诸多不足,还望各位海涵,我们江湖再见。
- 文案:張嘉鑫
- 逻辑视图:張嘉鑫
- 代码:張嘉鑫、嵌入式扫地僧
- 其余图片:来自网络