数据结构3线性表——单链表(C)
前言:
本专栏属于数据结构相关内容,附带一些代码加深对一些内容的理解,为方便读者观看,本专栏内的所有文章会同时附带C语言和Python对应的代码,(可自行通过目录跳转到对应的部分)辅助不同主修语言的读者去更好的理解对应的内容,若是代码0基础的读者,可先去博主其他专栏学习一下基础的语法及知识点:
魔法天才的跳转链接:
C语言:C基础_Gu_shiwww的博客-CSDN博客
Python语言:python1_Gu_shiwww的博客-CSDN博客
其他数据结构内容可见:数据结构_Gu_shiwww的博客-CSDN博客
什么是单链表
单链表(Singly Linked List)是一种链式存储结构,由一系列**节点(Node)**组成,每个节点包含两部分
1 链表的特征
逻辑结构:线性结构
存储结构:链式存储结构
特点:内存不连续,通过指针实现
解决顺序表问题:顺序表长度固定和插入删除效率低的问题。
操作:增删改查
链表就是将节点用链串起来的线性表,链就是节点中的引用
链表分为带头结点链表和不带头节点的链表,两者在逻辑结构上都属于链式存储结构,只是带头节点的链表单独拿出一个节点存放首个有效节点的地址,头节点内部也存在数据域,且也有数据存储,但是在逻辑上这个数据域内部的元素无效。带头与不带头节点在代码编写上有细微差别,本文以带头节点的单向链表来进行讲解
【例子】通过一个表格给定一个链表遍历的例子(可自行理解)
(赵, 钱, 孙, 李, 周, 吴, 郑, 王)
以下是用C语言实现单链表的一些具体操作,Python的具体编程实现详见魔法天才预设的跳转路径:
2 编程实现链表
C 语言编程实现
C.a 节点的构建
在学习链表之前,我们要先定义好每个链表连接的节点,在C语言中可以用结构体完成节点的构建
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 遍历无头单向链表
#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 遍历有头单向链表
#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");
#endifreturn 0;
}
1.3 链表尾插法
写一个有头单向链表,用于保存输入的学生成绩,实现一输入学生成绩就创建一个新的节点,将成绩保存起来。再将该节点链接到链表的尾,直到输入-1结束。
要求:每个链表的节点由动态内存分配得到 , 也就是用malloc。
过程:
- malloc申请空间link_node_t大小作为头节点
- 将新节点放到链表尾部
#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语言编程操作的函数接口
#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 创建一个空的有头单向链表
//创建一个空的有头单向链表
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
//计算链表长度 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 向单向链表的指定位置插入数据
//向单向链表的指定位置插入数据
//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 遍历单向链表
//遍历单向链表
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
//判断链表为空,为空返回1,不为空返回0
int isEmptyLinkList(link_node_p p)
{return p->next == NULL;
}
函数可以直接返回p的next域是否为NULL的判断是结果,满足为1,不满足为0
C.7 删除单向链表中指定位置的节点
//删除单向链表中指定位置的数据 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 删除指定数据的所有节点
//删除单向链表中出现的指定数据,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 清空链表
//清空链表数据
//思想:一直删除头节点的后一个,直到为空链表为止
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 修改指定位置的数据
//修改指定位置的数据 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 查找指定数据出现的位置
//查找指定数据出现的位置 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 转置链表
//转置链表
//解题思想:
//(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去找tq = t;}
}
总结:先断开头节点与后面数据的连接,然后再定义一个指针去记录要头插节点的后一个节点,一次向后移动一个一个头插如原来链表头节点的后面,实现整个链表的逆置
C.13 完整代码(可运行)
#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去找tq = 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;
}