【Lehr】【数据结构与算法】【C语言】链表及其相关操作
单链表是什么
基本概念
在数组中,每个元素的地址都是连续的,所以如果我们需要访问下一个元素,只需要按照秩序移动地址即可。但是这样却会造成对内存空间的浪费。链表是一种数据结构,链表中的每一个部分被称为节点,由一个数据域(可以是结构体)和一个指针域组成。每个节点中,数据域负责存放数据,而指针域则指向下一个节点。相比数组,我们可以由当前节点的指针来访问到下一个节点,所以地址不必是连续的。多个节点连接在一起就组成了一个长长的链表,从表头节点(第一个节点)开始逐个访问,就能获取整条链表的信息。
举个例子:
数组就像军队中的一排士兵,每个人都是连续排列的,你旁边的人一定就是你的下一号。所以遍历数组的过程就像喊整排士兵报号一样,从头到尾一个挨一个。
但是一个链表就像是一条卧底的情报链,每个节点就是一个卧底,他有自己的情报,但他只知道自己的下级是谁(这个下级并不一定是他旁边的人)。所以如果需要了解所有的情报,就需要从老大(表头节点)那里,一级一级地按照每个人所知道的自己的下级,来遍历整个情报链。
链表结构体
//数据域 这里使用了一个结构体
//如果你懒,可以不写这个,然后节点数据域直接是个int或者char之类的
typedef struct Data{
int ID;
char name[MAX_OF_NAME];
char weapon[MAX_OF_NAME];
int ammo;
} Data;
//定义节点
typedef struct Node{
Data data; //数据域
struct Node *next; //指向下一个节点的指针
} Node;
头空式链表
为了各种操作的方便,往往第一个节点是不存信息的,这样的单链表被称为头空式单链表。
用结构体表示链表整体
就像上面的那个例子讲的一样,如果链表代表一个卧底机构,那么如果每次只能从头开始找到老大然后慢慢遍历整个组织肯定很不方便,所以这里引入一个结构体来更方便第表示和管理整个链表。
这个结构体包含了:
指向表头的指针:用来一下找到头节点。
指向表尾的指针:一下找到最后一个节点。
节点数量记录:记录你这个链表里一共有多少个节点。
//定义链表结构
typedef struct MyList {
Node* head;
Node* tail;
int num;
}MyList;
如果用刚才的例子解释的话,就像这样:
比如这个组织叫Hydra…
typedef struct Hydra {
Node* 老大的地址;
Node* 底层萌新的地址;
int 总人手;
}Hydra;
主函数中对链表结构体的初始化操作
//创建头节点和尾节点
Node* head = (Node*)malloc(sizeof(Node)); //随便开个空地址给头节点
head->next = NULL; //头节点后面是空的
Node* tail = head; //因为只有个头,所以头即是尾
//创建链表结构
MyList myList = {head,tail,0}; //一开始长度为0(头节点不算)
这样之后,myList就代表你创建的链表,想要知道头在哪里,只需要用myList.head来获取即可了。
初始化后内存里的空间大概是这样的:
基本操作
增加节点
这里的增加节点就只是简单地新建一个节点然后填入信息而已,至于指针指向哪里,那是接下来生成链表或者插入链表中与大部队汇合时才需要的操作。
Node* NewNode(){
//开辟空间
Node* p = (Node*)malloc(sizeof(Node));
//提示你要录入些什么内容
printf("请输入ID:");
scanf("%d", &p->data.ID);
getchar();
printf("请输入名称:");
gets(p->data.name);
printf("请输入武器:");
gets(p->data.weapon);
printf("请输入弹药量:");
scanf("%d", &p->data.ammo);
//为了安全起见,指针域要指向NULL
p->next = NULL;
//把这个节点的地址返出去(这样别人才知道指哪里才能指到你)
return p;
}
生成链表
//你要在哪里链表中创建节点?创建多少个?
void InitList(MyList* myList, int length){
//利用循环创建节点
for(int i=0;i<length;i++){
Node* p = NewNode();
//这个新建的节点应该接在尾巴后面
myList->tail->next = p;
//然后这个节点就变成了尾巴
myList->tail = p;
//长度也要增加
myList->num++;
}
}
增加过程像这样了:
打印节点
这个不解释了
void PrintNode(Node* p){
//输出信息
printf("ID:%d\t名称:%s\t武器:%s\t弹药量:%d\n",
(p->data).ID,
(p->data).name,
(p->data).weapon,
(p->data).ammo);
}
打印链表
就像刚才生成单个节点和生成整个链表的过程一样,循环即可:
//传入你需要打印的那个链表
void PrintList(MyList* myList){
//提示
printf("当前共有%d个项目", myList->num);
//从表头的下一个有效数据节点开始,循环打印
for(Node* p = myList->head->next;p;p = p->next){ //这是链表的遍历写法
PrintNode(p); //如果下一个存在
} //就让指针指向下一个
}
删除节点
我们来直接看个图,看下节点是怎么被删除的:
首先,我们发现,要删除一个节点,必须知道它前面的那个节点,然后把前面的那个节点的后继指针指向要删的节点的后面那个地方,也就是绕过了要删的节点,所以这样一来,整个链表的路线也就是图中蓝色的了,也就是说,这个节点现在被挤出链表了,所以我们可以直接把它甩到垃圾桶里去了。
这里说一下,怎么让前面节点的指针指向目标节点后面的那个地方(这段话看起来是不是很绕口XD),为了方便说明,我们这里举例B节点
为要删除的节点,它前面和后面的节点分别是A节点
和C节点
。
A的后继指针是指向B,也就是:B等于A后
B的后继指针是指向C,也就是:C等于B后
现在需要绕过B,让A的后继指针指向C,
也就是说: 让A的后继指针指向(B后)
用代码来讲就是:
a->next = b->next;
好了现在来看代码:
//在哪个链表里?删哪个节点?
void DeletNode(MyList* myList,Node* n){
Node* p = myList->head; //p代表头节点
//通过遍历法,找到目标节点的前一个节点
for(;p->next != n;p=p->next);
//调整关系:前一个节点指向本节点的后面,也就是绕过这个点了
//所以这个点就从链表的关系链里面被排出去了
p->next = n->next;
//如果删的是尾巴,记得把尾巴改成前面那个
if(n == myList->tail){
myList->tail = p;
}
//计数的要少一个
myList->num--;
//回收节点的空间
free(n);
}
插入节点
插入节点说白了就是删除的逆操作,也需要知道两个节点:
还是用刚才ABC的例子。在AC中间插入B的过程如下:
A的后继由C改为指向B;
B的后继由NULL改为指向C;
完成!
下面是代码:
//在这之前请自己new一个节点,也就是形参里的n
void InsertNode(MyList* myList, int pos, Node* n){
//判断越界
if(pos<1 || pos >myList->num+1){
return ;
}
//输入数字就是插入后变成的第几个,支持尾插(总长度加一的数字就是)
//比如原来长度为5,如果你想插入到6号位置,也就是直接在尾巴上加一节
//计数器
int i = 1;
Node* q = myList->head;
//p代表q的下一个节点
Node* p = q->next;
//利用循环得到本节点前一个和本节点
for(;q;q=p, p=p->next){
if(i == pos){
//找到了本节点就break,这时q是前一个节点,p是要插入的位置的节点
break;
}
i++;
}
//调整关系
q->next = n;
n->next = p;
//长度增加
(myList->num)++;
//如果是尾巴就记得标记上
if(pos == myList->num+1) {
myList->tail = n;
}
}
查找节点
遍历的方式一个一个去匹配就对了。
Node* GetByName(MyList* myList, char* name){
Node* p = myList->head;
for(;p && strcmp(p->data.name,name);p=p->next);
return p;
}
节点排序
说白了就是个基本排序算法:对比某个属性的值,只不过这里交换的是整个数据域
Status BubbleSortByID(MyList* myList){
Node* p = myList->head->next;
Node* q ;
Data temp;
for(;p;p=p->next){
for(q=p->next;q;q=q->next){
if(p->data.ID > q->data.ID){
temp = p->data;
p->data = q->data;
q->data= temp;
}
}
}
}
高级操作
通过文件生成链表
将链表保存至文件
链表合并
单链表逆序
双链表的思路
循环链表的思路
单链表基础操作的全代码
#include<stdio.h>
#include<malloc.h>
#include<windows.h>
#include<string.h>
#define MAX_OF_NAME 100
//Lehr的头空单链表
//在插入 删除 操作中 需要知道 前一项 因为头部有空的节点所以不用管 尾部也是 所以不用特殊处理
//定义自己的数据类型
typedef struct Data{
int ID;
char name[MAX_OF_NAME];
char weapon[MAX_OF_NAME];
int ammo;
} Data;
//定义节点
typedef struct Node{
Data data;
struct Node *next;
} Node;
//定义链表结构
typedef struct MyList {
Node* head;
Node* tail;
int num;
}MyList;
//定义枚举
enum Status {
success,fail, fatal, range_error
};//成功 失败 字面错误(内存分配失败) 超出界限
//函数声明
Node* NewNode();
//创建一个新节点并且返回遥控器
Status InitList(MyList* myList, int length);
//初始化生成链表,按照给定需求创造对应长度的链表
Status PrintNode(Node* p);
//打印该节点的内容
Status PrintList(MyList* myList);
//打印整个链表
Status InsertNode(MyList* myList, int pos, Node* n);
//在指定位置插入节点,pos代表插入后将成为第几个节点,范围是1到length+1
Status DeletNode(MyList* myList,Node* n);
//在链表中删除对应节点
Node* GetByPos(MyList* myList, int pos);
//在链表中按节点位置查找对应节点
Node* GetByData(MyList* myList, int ID);
//在链表中按ID查找对应节点
Node* GetByName(MyList* myList, char* name);
//在链表中按名字查找对应节点
Status BubbleSortByID(MyList* myList);
//在链表中按年龄从小到大进行排序
void gotoxy(int x,int y);
//显示操作
//函数的实现
void gotoxy(int x,int y){
COORD c;
c.X=x;
c.Y=y;
SetConsoleCursorPosition(GetStdHandle(STD_OUTPUT_HANDLE),c);
}
Status InitList(MyList* myList, int length){
Status s = fail;
//利用循环创建节点
for(int i=0;i<length;i++){
gotoxy(50,1);
printf("******录入第%d个******\n", i+1);
Node* p = NewNode();
//异常处理
if(p == NULL){
return s =fatal;
}
//前后连接
myList->tail->next = p;
//尾部标记转移
myList->tail = p;
//记录长度
myList->num++;
}
return s = success;
}
Status PrintNode(Node* p){
Status s = fail;
//输出信息
printf("ID:%d\t名称:%s\t武器:%s\t弹药量:%d\n",
(p->data).ID,
(p->data).name,
(p->data).weapon,
(p->data).ammo);
return s = success;
}
Status PrintList(MyList* myList){
Status s = fail;
int i=0;
//提示
gotoxy(48,1);
printf("当前共有%d个项目", myList->num);
//循环打印
for(Node* p = myList->head->next;p;p = p->next){
gotoxy(35,3+i);
PrintNode(p);
i+=2;
}
gotoxy(1,27);
system("pause");
system("cls");
return s = success;
}
Status InsertNode(MyList* myList, int pos, Node* n){
//一步通用插入法
//判断越界
if(pos<1 || pos >myList->num+1){
return range_error;
}
//输入数字就是插入后变成的第几个,支持尾插(总长度加一的数字就是)
int i = 1;
Node* q = myList->head;
//p代表q的下一个节点
Node* p = q->next;
//利用循环得到本节点前一个和本节点
for(;q;q=p, p=p->next){
if(i == pos){
break;
}
i++;
}
//调整关系
q->next = n;
n->next = p;
//长度增加
(myList->num)++;
//如果是尾巴就记得标记上
if(pos == myList->num+1) {
myList->tail = n;
}
return s = success;
}
Status DeletNode(MyList* myList,Node* n){ //配合搜素函数得到节点然后再调用删除
//一步通用的了
Node* p = myList->head->next; //从首个节点开始数
//找到倒数第二个节点p
for(;p->next != n;p=p->next);
//保障如果输入外来的怎么办 虽然在程序中会用搜素去检查保证 这里二重保险嘛
//如果是外来p就不存在的
if(!p){
return range_error;
}
//调整关系
p->next = n->next;
//记得标记尾巴
if(n == myList->tail){
myList->tail = p;
}
myList->num--;
//回收
free(n);
return success;
}
Node* GetByPos(MyList* myList, int pos){
int i = 1; //只能从有数据的节点开始搜素
Node*p = myList->head->next;
for(;i!=pos && p;i++,p=p->next);
//异常处理 如果是 超了就会返回一个NULL
return p;
}
Node* GetByData(MyList* myList, int ID){ //这里以ID为例子而已
Node*p = myList->head->next;
for(;p && p->data.ID != ID;p=p->next); //一定要先判断p存在再来判断后面的 顺序不能反
return p;
}
Node* GetByName(MyList* myList, char* name){
Node* p = myList->head;
for(;p && strcmp(p->data.name,name);p=p->next);
return p;
}
Node* NewNode(){
//开辟空间
Node* p = (Node*)malloc(sizeof(Node));
//异常处理
if(p == NULL){
printf("存储空间分配失败");
return NULL; //出错后返回空值 不用写入数据了
}
//提示内容录入
gotoxy(50,2);
printf("请输入ID:");
scanf("%d", &p->data.ID);
getchar();
gotoxy(50,3);
printf("请输入名称:");
gets(p->data.name);
gotoxy(50,4);
printf("请输入武器:");
gets(p->data.weapon);
gotoxy(50,5);
printf("请输入弹药量:");
scanf("%d", &p->data.ammo);
system("cls");
p->next = NULL;
return p;
}
Status BubbleSortByID(MyList* myList){ //这个貌似是选择排序
Status s = fail;
Node* p = myList->head->next;
Node* q ;
Data temp;
for(;p;p=p->next){
for(q=p->next;q;q=q->next){
if(p->data.ID > q->data.ID){
temp = p->data;
p->data = q->data;
q->data= temp;
}
}
}
return s = success;
}
int main(){
//声明各类变量
int i;
int pos;
int ID;
int length;
char name[MAX_OF_NAME];
Node* p = NULL;
Status s;
//创建头节点和尾节点
Node* head = (Node*)malloc(sizeof(Node));
head->next = NULL;
Node* tail = head;
//创建链表结构
MyList myList = {head,tail,0};
//初始化操作
gotoxy(50,1);
printf("您想创建多少个项目:");
scanf("%d", &length);
system("cls");
InitList(&myList,length);
//显示
PrintList(&myList);
//功能菜单
while(1){
system("cls");
gotoxy(50,2);
printf("1.插入项目");
gotoxy(50,3);
printf("2.按ID排序");
gotoxy(50,4);
printf("3.按照位置搜索");
gotoxy(50,5);
printf("4.按照ID搜素");
gotoxy(50,6);
printf("5.按照名称搜素");
gotoxy(50,7);
printf("6.按照ID删除");
gotoxy(50,8);
printf("7.打印所有项目");
gotoxy(50,9);
printf("**按其他任意键退出**");
gotoxy(50,10);
printf("输入您的选择:");
scanf("%d", &i);
system("cls");
switch(i){
case 1:
//创建链表
p = NewNode();
if(p==NULL){
gotoxy(50,2);
printf("创建出错!");
break;
}
//提示
gotoxy(50,2);
printf("请输入您想插入的位置:");
scanf("%d", &pos);
system("cls");
//插入操作
s = InsertNode(&myList,pos,p);
//提示
if(s==success){
gotoxy(50,2);
printf("插入成功,");
}
else{
gotoxy(50,2);
printf("插入失败,");
}
//停顿
system("pause");
break;
case 2:
//排序操作
BubbleSortByID(&myList);
//提示
gotoxy(50,2);
printf("排序成功,");
//停顿
system("pause");
break;
case 3:
//提示
gotoxy(50,2);
printf("请输入目标的位置:");
//输入并查找
scanf("%d", &pos);
p = GetByPos(&myList,pos);
system("cls");
gotoxy(35,3);
if(p == NULL){
printf("目标不存在!");
}
else{
PrintNode(p);
}
//停顿
system("pause");
break;
case 4:
//提示
gotoxy(50,2);
printf("请输入目标的ID:");
//输入并查找
scanf("%d", &ID);
p = GetByData(&myList,ID);
system("cls");
gotoxy(35,3);
if(p == NULL){
printf("目标不存在!");
}
else{
PrintNode(p);
}
//停顿
system("pause");
break;
case 5:
//提示
gotoxy(50,2);
printf("请输入目标的名称:");
//吸收之前那个回车
getchar();
//输入并查找
gets(name);
p = GetByName(&myList,name);
system("cls");
gotoxy(35,3);
if(p == NULL){
printf("目标不存在!");
}
else{
PrintNode(p);
}
//停顿
system("pause");
break;
case 6:
//提示
gotoxy(50,2);
printf("请输入目标的ID:");
//输入并删除
scanf("%d", &ID);
p = GetByData(&myList,ID);
system("cls");
gotoxy(50,2);
if(p == NULL){
printf("目标不存在!");
}
else{
DeletNode(&myList,p);
printf("删除成功,");
}
//停顿
system("pause");
break;
case 7:
PrintList(&myList);
break;
default:
exit(0);
}
}
return 0;
}