C语言之链表
一.引言
假设需要建立一个学生信息表,学生人数无法估计,而且学生人数经常发生变化,应该如何实现?
一般我们会想到用数组来存储,但是数组属于静态存储分配,必须事先确定容量,故无法实现;
因此引出链表这个概念,链表属于动态存储分配,在运行时才分配空间,这种存储方式解决了这种问题。
二.链表的概述
链表是由一个个结点组成的。
单链表的结点有两个部分:数据域和指针域
数据域:存储的是当前结点的数据(int,double,struct等等)。
指针域:存储的是下一个结点的地址。
一般单链表的结点结构:可以用一个结构体来表示
typedef struct node
{
DataType data; //数据域
struct node *next; //指针域
}Node,*Link;
说明:
Node st;//等价于 struct node st;
Link p;//等价于 struct node *p; 指向这种(struct node)结构体的指针。
p = &st;//p指向Node类型的变量st的地址
思考:如何申请一个结点?
p=(Link)malloc(sizeof(Node));
这句代码的意思是:给一个结点变量申请了sizeof(Node)个字节的存储空间。即给st申请了sizeof(Node)个字节的存储空间。
思考:如何引用数据元素?
p -> data;//相当于(*p).data
思考:如何引用指针域?
p -> next;
三.链表的分类
- 空表(没有数据)
- 非空表(有数据)
如何将空表与非空表统一起来?
引入头结点。(头结点:在单链表的第一个元素结点之前附设一个类型相同的结点,以便空表和非空表处理统一。)
引入头结点后的格式如下:
空表
head–>|冗余值 |null|
非空表
head–>|冗余值 |next|–>第一个数据 下一个数据的地址…
四.链表的练习
使用链表完成学生信息管理
- 头文件内容(studentList.h)
#include <stdio.h>
#include <stdlib.h>
#include <stdbool.h>
#include <string.h>
//一些警告的处理
#pragma warning(disable:4996) //可使用非安全函数
#pragma warning(disable:6031) //有些函数的返回值不用定义,比如getchar()
#pragma warning(disable:6011) //警告 C6011 取消对 NULL 指针“head”的引用。
#define NUM_MAX 20
#define NAME_MAX 20
typedef struct studentInfo
{
char stu_num[NUM_MAX];
char stu_name[NAME_MAX];
}Stu;
typedef struct nodeInfo
{
Stu stu_data;
struct nodeInfo* next;
}Node, * Link;
void myMenu(void);//功能菜单
Link createHead(void);//创建头结点
void inputNode(Link p);//输入学生信息
bool addNode(Link head);//新增学生记录
void displayNode(Link head);//遍历所有学生记录
int countNode(Link head);//统计学生人数
void inputStudentNo(char s[], char no[]);//输入相应的学号和具体的操作
bool queryNode(Link head);//查询学生记录
void insertNode(Link head, Link newNode);//插入一个学生记录
bool modifyNode(Link head);//修改学生记录
bool deleteNode(Link head);//删除学生记录
void clearLink(Link head);//清除链表
- studentList.c文件内容
#include "studentList.h"
void myMenu(void) {
printf("\n");
printf(" * * * * * * * * * 菜 单 * * * * * * * * * *\n");
printf(" 1 增加学生记录 2 删除学生记录 \n");
printf(" 3 查找学生记录 4 修改学生记录 \n");
printf(" 5 统计学生人数 6 显示学生记录 \n");
printf(" 7 退出系统 \n");
printf(" * * * * * * * * * * * * * * * * * * * * * * * *\n");
}
Link createHead(void)
{
Link head = (Link)malloc(sizeof(Node));
head->next = NULL;//这句代码会引起警告C6011,取消对 NULL 指针“head”的引用,因此头文件中需忽略此警告
return head;
}
void inputNode(Link p)
{
printf("请输入学生的学号:");
scanf("%s", p->stu_data.stu_num);
getchar();
printf("请输入学生的姓名:");
scanf("%s", p->stu_data.stu_name);
getchar();
p->next = NULL;
}
bool addNode(Link head)
{
Link p, q;
Link newNode;
bool flag = false;
p = head;
q = head->next;
newNode = (Link)malloc(sizeof(Node));
inputNode(newNode);
if (head->next == NULL)
{
head->next = newNode;
flag = true;
}
else
{
while (q != NULL)
{
if (strcmp(newNode->stu_data.stu_num, q->stu_data.stu_num) < 0)
{
p->next = newNode;
newNode->next = q;
flag = true;
break;
}
else
{
p = q;
q = q->next;
}
}
}
//目前p->next=NULL,q已经在链表外了,只能将新结点添加到p结点的后面
if (q == NULL && flag == false)
{
p->next = newNode;
flag = true;
}
return flag;
}
//结点遍历的实质,从头结点的下一个结点开始一直遍历到最后一个,最后一个结点的next域为NULL。
void displayNode(Link head)
{
printf("\n* * * * * * * * * 学 生 列 表 * * * * * * * * * *\n");
printf("\t学号\t\t\t\t姓名\n");
Link p = head->next;
while (p != NULL)//这里的p!=NULL和p->next == NULL不相同,p!=NULL包含了p->next==NULL这个情况
{
printf("\t%s\t\t\t\t%s\n", p->stu_data.stu_num, p->stu_data.stu_name);
p = p->next;
}
}
int countNode(Link head)
{
Link p = head->next;
int count = 0;
while (p != NULL)
{
count++;
p = p->next;
}
return count;
}
void inputStudentNo(char s[], char no[])
{
printf("请输入要%s的学生学号:", s);
scanf("%s", no);
}
bool queryNode(Link head)
{
char no[NUM_MAX];
strcpy(no, "");
inputStudentNo("查询", no);
bool flag = false;
Link p = head->next;
while (p != NULL)
{
if (!strcmp(p->stu_data.stu_num, no))
{
printf("%s的姓名是%s\n", no, p->stu_data.stu_name);
flag = true;
break;
}
else
{
p = p->next;
}
}
return flag;
}
void insertNode(Link head, Link newNode)
{
Link p, q;
bool flag = false; //是否插入成功
p = head;
q = head->next;
//如果是空链表
if (head->next == NULL)
{
head->next = newNode;
flag = true;
}
else
{
//不是空链表
while (q != NULL)
{
if (strcmp(newNode->stu_data.stu_num, q->stu_data.stu_num) < 0)
{
p->next = newNode;
newNode->next = q;
flag = true;
break;
}
else
{
p = q;
q = q->next;
}
}
}
if (q == NULL && flag == false)
{
p->next = newNode;
}
}
bool modifyNode(Link head)
{
char no[NUM_MAX];
char new_no[NUM_MAX];
char new_name[NAME_MAX];
int flag = false;
strcpy(no, "");
strcpy(new_no, "");
strcpy(new_name, "");
int select; //功能选择
Link p, q;
inputStudentNo("修改", no);
printf("请输入需要修改的项目: 1 学号 2 姓名\n");
scanf("%d", &select);
getchar();
p = head;
q = head->next;
while (q != NULL)
{
if (!strcmp(q->stu_data.stu_num, no))
{
if (select == 1)
{
p->next = q->next;
q->next = NULL;//一般一个新的结点没有下一个结点,故新结点的next域为NULL
printf("请输入该学生的新学号:");
scanf("%s", new_no);
getchar();
strcpy(q->stu_data.stu_num, new_no);
insertNode(head, q);
}
else
{
printf("请输入该学生的新姓名:");
scanf("%s", new_name);
strcpy(q->stu_data.stu_name, new_name);
}
flag = true;
}
else
{
p = q;
q = q->next;
}
}
return flag;
}
bool deleteNode(Link head)
{
char no[NUM_MAX];
strcpy(no, "");
inputStudentNo("删除", no);
int flag = false;
Link p, q;
p = head;
q = head->next;
while (q != NULL)
{
if (!strcmp(q->stu_data.stu_num, no))
{
p->next = q->next;
free(q);
q = p->next;//如果不写这段代码会报警告C6001,使用未初始化的内存 q
flag = true;
}
else
{
p = q;
q = q->next;
}
}
return flag;
}
void clearLink(Link head)
{
Link p, q;
p = head;
q = head->next;
while (q != NULL)//不包括头结点
{
free(p);
p = q;
q = q->next;
}
free(p);//清除头结点
puts("\n\n~~~~~~~~~~~~~~~~~《程序退出,欢迎您再次使用》~~~~~~~~~~~~~~~~~~~~~~~");
}
- 测试内容(test.c)
#include "studentList.h"
int main(void)
{
int select = 0; //功能选项代码
int count = 0; //学生人数
//链表头
Link head = createHead();
while (1)
{
myMenu();
printf("\n请输入你的选择(1-7):");
scanf("%d", &select);
switch (select)
{
case 1:
if (addNode(head))
{
printf("成功增加一个学生记录。\n");
}
break;
case 2:
if (deleteNode(head))
{
printf("成功删除一个学生记录。\n");
}
else
{
printf("没有找到要删除的学生记录。\n");
}
break;
case 3:
if (queryNode(head))
{
printf("成功找到学生记录。\n");
}
else
{
printf("没有找到要查询的学生记录。\n");
}
break;
case 4:
if (modifyNode(head))
{
printf("成功修改一个学生记录。\n");
}
else
{
printf("没有找到要修改的学生记录。\n");
}
break;
case 5:
count = countNode(head);
printf("学生人数为:%d\n", count);
break;
case 6:
displayNode(head);
break;
case 7:
clearLink(head);
system("pause");
return 0;
default:
printf("输入不正确,请输入1-7之间的数。\n");
break;
}
}
}
五.完结
总结:关于链表的学习,一定要自己通过画图来理解。
代码是根据逻辑顺序完成的,因此测试的时候,先添加,然后遍历学生,然后统计,然后查询,然后修改,然后删除,最后退出。
认真理解,多多练习才能真正掌握!!!
本篇代码只用到了单向链表。
双向链表:结点有三个部分,指针域1(前一个数据的地址),数据域,指针域2(后一个数据的地址),思路和单向链表相同。
循环链表:使得最后一个结点指向头结点,思路也和单向链表相同。
《完结撒花》