12-C语言单向链表
一、链表的概述
1.链表与数组对比
- 遍历数组中的数据,查询数据比较方便,但往数组中插入、删除数据需要移动大量数据;
- 相反。链表遍历、查询数据不方便,但是插入、删除数据比较方便,不需要移动大量数据,直接在指定位置插入或删除指定节点,这与链表节点的结构有关;
- 数组的一个个元素在内存中是连续的,链表的节点间内存不连续。
2.链表节点
2.1链表节点的概念
链表的节点:用来保存数据信息的,分为数据域、指针域两个部分;数据域用来存放核心数据;指针域用来存放下一个节点的地址。
- 链表一般分为无头链表和有头链表:
- 无头链表:即由一个指针变量保存首个节点的起始地址;
- 有头链表:由一个节点的指针域保存首节点的起始地址,且该节点没有数据域,只有指针域。
2.2链表节点类型定义
链表节点类型定义和结构体一样,只不过分为了数据域和指针域,指针域是一个结构体指针成员。
- 代码演示
typedef struct stu
{// 定义数据域int num;char name[32];float score;// 定义指针域struct stu *next;
}STU;
二、单向链表应用
1.单向链表
单向链表又叫静态链表,它是一个接着一个节点依次单向排列分布的,上一个节点的指针域指向下一个节点的起始地址,最后一个节点的指针域指向NULL。
- 创建一个单向链表
int main()
{// 定义5个节点STU stu1 = {"小明", 18, 90.0f, NULL};STU stu2 = {"小王", 20, 75.5f, NULL};STU stu3 = {"小红", 19, 87.0f, NULL};STU stu4 = {"小当家", 18, 86.5f, NULL};STU stu5 = {"小魔仙", 21, 88.8f, NULL};// 定义节点头STU *stus = &stu1;// 构建链表stu1.next = &stu2;stu2.next = &stu3;stu3.next = &stu4;stu4.next = &stu5;// 遍历链表STU *p = stus;while (p != NULL){printf("姓名:%-7s,年龄:%-2d,分数:%.2f\n", p->name, p->age, p->score);p = p->next;}return 0;
}
- 运行结果
姓名:小明 ,年龄:18,分数:90.00
姓名:小王 ,年龄:20,分数:75.50
姓名:小红 ,年龄:19,分数:87.00
姓名:小当家 ,年龄:18,分数:86.50
姓名:小魔仙 ,年龄:21,分数:88.80
- 说明:
- 定义节点和定义结构体变量一样,初始化先将 next 指向 NULL;
- 定义一个节点头指针变量指向第一个节点,然后第一个节点的 next 指向下一个节点,反复执行,直到最后一个节点,将最后一个节点的 next 指向NULL;
- 在遍历链表的时候,通过一个临时指针变量保存链表头,用这个临时指针变量去遍历,因为链表头不能随意改变指向;
- 每遍历一个节点就将临时指针变量指向当前节点的下一个节点的起始位置。
2.单向链表综合案例:简易的学生信息管理系统
项目要求:通过单项链表,开发一个简易的学生管理系统,系统功能包含学生信息的增删改查等基本操作,通过输入关键词命令调用相应的功能函数来满足用户需求。
2.1项目基本框架
在开始具体的功能函数前,先整理思路,理清大致框架,想好需要添加哪些功能,然后再逐一去实现这些功能函数。
- 菜单界面和主函数框架
// 一个简单的学生管理系统
typedef struct stu {// 定义数据域int num;char name[32];float score;// 定义指针域struct stu *next;
} STU;// 定义函数,打印菜单界面
void func_list(void) {printf("-----------------------------------\n");printf("-->insert:插入学生信息 |\n");printf("-->print:遍历信息 |\n");printf("-->search:查询某个学生的信息 |\n");printf("-->delete:删除某个学生的信息 |\n");printf("-->free:删除所有学生 |\n");printf("-->reverse:链表逆序 |\n");printf("-->sort:链表排序 |\n");printf("-->quit:退出整个程序 |\n");printf("-----------------------------------\n");return;
}int main() {// 调用函数:打印功能菜单func_list();// 获取指令char cmd[16] = "";while(1){printf("请输入要操作的指令:");scanf("%s", cmd);if (strcmp("insert", cmd) == 0){printf("插入学生信息\n");}else if (strcmp("print", cmd) == 0){printf("遍历信息\n");}else if (strcmp("search", cmd) == 0){printf("查询某个学生的信息\n");}else if (strcmp("delete", cmd) == 0){printf("删除某个学生的信息\n");}else if (strcmp("free", cmd) == 0){printf("删除所有学生\n");}else if (strcmp("reverse", cmd) == 0){printf("链表逆序\n");}else if (strcmp("sort", cmd) == 0){printf("链表排序\n");}else if (strcmp("quit", cmd) == 0){printf("程序已经退出\n");break;}else{printf("请输入有效命令\n");}}return 0;
}
- 运行结果
-----------------------------------
-->insert:插入学生信息 |
-->print:遍历信息 |
-->search:查询某个学生的信息 |
-->delete:删除某个学生的信息 |
-->free:删除所有学生 |
-->reverse:链表逆序 |
-->sort:链表排序 |
-->quit:退出整个程序 |
-----------------------------------
请输入要操作的指令:insert
插入学生信息
请输入要操作的指令:printf
请输入有效命令
请输入要操作的指令:quit
程序已经退出
- 说明:
- 因为学生信息包含的数据类型多样,因此通过结构体变量来存储学生信息,为了完成增删改查等复杂操作,通过链表的形式实现;
- 定义一个函数用来打印功能菜单;
- 主函数循环获取用户输入的命令,条件判断调用相应的功能函数。
2.2插入学生信息
2.2.1主函数
- 主函数部分
int main() {// 调用函数:打印功能菜单func_list();// 定义一个链表头STU *head = NULL;// 获取指令char cmd[16] = "";while(1){printf("请输入要操作的指令:");scanf("%s", cmd);if (strcmp("insert", cmd) == 0){// 定义临时变量用于接收输入的学生信息STU stu_temp;int n = 0;printf("请输入要添加的学生的个数:");scanf("%d", &n);printf("请输入学生信息(学号 姓名 分数):");// 调用函数:添加学生信息int i = 0;for (i = 0; i < n; i++){scanf("%d %s %f", &stu_temp.num, stu_temp.name, &stu_temp.score);// 调用函数:添加学生信息// head = insert_func_head(head, stu_temp); // 头部插入// head = insert_func_tail(head, stu_temp); // 尾部插入head = insert_func_sequence(head, stu_temp); // 顺序插入}printf("已添加%d位学生信息\n", n);}else if (strcmp("print", cmd) == 0){// 调用函数:打印学生信息print_func(head);}// .........省略其它功能}
}
- 说明:
- 这里主函数部分列出了插入和打印的功能,其它功能代码省略;
- 因为很多功能函数都会用到链表头,因此把它定义在主函数最外层作用域下,为所有功能函数公有;
- 插入函数只负责插入功能,学生信息的获取在主函数完成,也可以单独封装一个获取学生信息的函数,将获取到的学生信息通过一个临时结构体变量,作为插入函数的实参传入。
2.2.2头部插入学生信息
- 代码演示
// 定义函数:在头部插入学生信息
STU *insert_func_head(STU *head, STU stu_temp)
{// 申请堆区空间,存储一个学生的信息STU *new_stu = (STU *)malloc(sizeof(STU));if (NULL == new_stu){printf("空间申请失败\n");return head;}// 将获取到的学生信息存入申请到的空间*new_stu = stu_temp;// 将新的学生节点插入链表// 链表为空if (NULL == head){head = new_stu;new_stu->next = NULL;}// 链表不为空else{new_stu->next = head;head = new_stu;}return head;
}
- 说明:
- 因为是插入信息,需要函数内部改变链表结构或者链表头的指向,因此应将 head 的地址传入,为一个二级指针。但为了方便操作,也可以传入一级指针,然后将函数内部的临时局部变量 head 保存的头节点的地址返回,主函数用 head 接收,保存新的链表头节点位置,即使函数调用结束,临时局部变量 head 释放也无所谓,它释放的只是这个变量保存的地址编号,并不会释放地址指向的内存空间,主函数的 head 已经保存了新链表的头节点位置,可以正常访问;
- 如果堆区空间申请失败,不要返回 NULL,失败相当于不改变链表结构,直接原样返回就行,否则会将原来的链表丢失,还会造成内存泄漏,堆区空间无法释放的问题;
- 链表头为空的话,直接将 head 指向新添加的学生节点就行,再将新学生节点指针域指向 NULL;
- 链表头不为空的话,先将新学生节点的指针域指向 head 指向的节点,再将 head 指向新学生节点,这里顺序不能颠倒;
- 如果颠倒,head 指向了新学生节点,那之前的链表就丢失了,新学生节点的指针域找不到之前 head 指向的头节点。也可以先定义一个临时指针变量指向 head 指向的头节点,然后顺序可以颠倒了,但感觉多此一举。
2.2.3遍历所有学生信息
- 代码演示
// 定义函数:打印学生信息
void print_func(STU *head)
{// 判断链表是否为空if (NULL == head){printf("未添加任何学生信息\n");return;}// 链表不为空,遍历学生信息STU *p = head;printf("---------------学生信息---------------\n");while(p != NULL){printf("学号:%03d, 姓名:%-6s, 分数:%.2f\n", p->num, p->name, p->score);p = p->next;}printf("---------------末行显示---------------\n");
}
-
说明:
- 遍历所有学生信息,是读操作,不需要返回值;
- 遍历前先判断链表是否为空再遍历,否则访问非法内存;
- 定义一个临时指针变量,并将 head 赋值给它,因为读操作节点头不能改变,需要一个临时指针变量去改变指向,遍历信息。
-
头部插入运行结果
请输入要操作的指令:insert
请输入要添加的学生的个数:3
请输入学生信息(学号 姓名 分数):101 小明 88.8
102 小红 85
103 王天霸 65.5
已添加3位学生信息
请输入要操作的指令:print
---------------学生信息---------------
学号:103, 姓名:王天霸, 分数:65.50
学号:102, 姓名:小红 , 分数:85.00
学号:101, 姓名:小明 , 分数:88.80
---------------末行显示---------------
请输入要操作的指令:quit
程序已经退出
- 说明:从输出结果可以看出,头部插入去遍历的时候,类似于先入后出。
2.2.4尾部插入学生信息
- 代码演示
// 定义函数:在尾部插入学生信息
STU *insert_func_tail(STU *head, STU stu_temp)
{// 申请堆区空间,存储一个学生的信息STU *new_stu = (STU *)malloc(sizeof(STU));if (NULL == new_stu){printf("空间申请失败\n");return head;}// 将获取到的学生信息存入申请到的空间*new_stu = stu_temp;new_stu->next = NULL;// 将新的学生节点插入链表// 链表为空if (NULL == head){head = new_stu;}// 链表不为空else{STU *p = head;while(p->next != NULL){p = p->next;}p->next = new_stu;}return head;
}
-
说明:
- 参数传递,返回值等操作同头部插入;
- 因为是尾部插入,新学生节点的指针域一定指向 NULL,因此在判断链表是否为空前就可以将指针域指向空
new_stu->next = NULL;
,减少重复代码; - 链表不为空时,定义一个临时指针变量用于定位到链表的最后一个节点,然后将最后一个节点的 next 指向新学生节点即可。
-
尾部插入运行结果
请输入要操作的指令:insert
请输入要添加的学生的个数:3
请输入学生信息(学号 姓名 分数):101 小明 88.8
102 小红 85
103 王天霸 65.5
已添加3位学生信息
请输入要操作的指令:print
---------------学生信息---------------
学号:101, 姓名:小明 , 分数:88.80
学号:102, 姓名:小红 , 分数:85.00
学号:103, 姓名:王天霸, 分数:65.50
---------------末行显示---------------
- 说明:从运行结果可以看出,尾部插入学生,遍历结果类似于先入先出。
2.2.5有序插入学生信息
这里按照学生学号从小到大的顺序排序。
- 代码演示
// 定义函数:按照学号顺序插入
STU *insert_func_sequence(STU *head, STU stu_temp)
{// 申请堆区空间,存储一个学生的信息STU *new_stu = (STU *)malloc(sizeof(STU));if (NULL == new_stu){printf("空间申请失败\n");return head;}// 将获取到的学生信息存入申请到的空间*new_stu = stu_temp;new_stu->next = NULL;// 将新的学生节点插入链表// 链表为空if (NULL == head){head = new_stu;}// 链表不为空else{// 定义两个指针:pa用于寻找插入位置(定位指针),pb用于保存pa的上一个节点位置STU *pa = head, *pb = head;while((pa != NULL) && (pa->num < new_stu->num)){pb = pa;pa = pa->next;}// 插入链表最后一个if (NULL == pa){pb->next = new_stu;// new_stu = pb->next; // 错误写法}// 插入链表第一个else if(pa == head){new_stu->next = head;head = new_stu;}// 插入到链表的中间else{pb->next = new_stu;new_stu->next = pa;}}return head;
}
-
说明:
- 顺序插入除了链表不为空的情况,其它同前面两种方式;
- 需要依次比较新学生的学号与链表中其它学生学号的大小确定插入点的位置,因此需要定义一个临时指针变量定位,考虑到如果是插入的话,需要截断以前的链表,将截断位置的上一个节点的 next 指向新节点,新节点的 next 指向下一个节点,下一个节点就是定位指针当前指向的位置,但上一个节点不知道,因此需要定义另外一个临时指针变量保存定位指针的上一个节点位置;
- 通过条件循环定位插入位置,当新节点的学号大于当前定位节点的学号且当前节点的next不指向NULL时指针继续后移,知道其中一个不满足,退出循环;
- 循环结束,如果
NULL == pa
,说明新节点num
值最大,插入到末尾,注意:while((pa != NULL) && (pa->num < new_stu->num))
必须先判断pa != NULL
,否则当pa指向NULL时,先判断pa->num < new_stu->num
会访问非法内存; - 注意:
pb->next = new_stu;
与new_stu = pb->next;
是有区别的,前者是将pb
的 next 指向new_stu
,后者是将new_stu
指向pb
的 next 指向的节点,注意区分; - 如果循环因为条件
pa->num < new_stu->num
不满足退出,当pa == head
时说明新节点的 num 值最小,插入到头部,让新节点的 next 指向 head 指向的节点,再用 head 指向新节点; - 如果上面情况都不是,那插入到中间位置,让新节点指向定位指针指向的节点,再让定位指针前一个节点的 next 指向新节点,这两步位置可调换。
-
运行结果
请输入要操作的指令:insert
请输入要添加的学生的个数:3
请输入学生信息(学号 姓名 分数):105 张大炮 85
101 吕小布 83.5
103 王刚 75
已添加3位学生信息
请输入要操作的指令:print
---------------学生信息---------------
学号:101, 姓名:吕小布, 分数:83.50
学号:103, 姓名:王刚 , 分数:75.00
学号:105, 姓名:张大炮, 分数:85.00
---------------末行显示---------------
- 说明:有序插入不管输入数据的先后,结果都是按照规定的顺序插入的。
2.3查询某个学生信息
- 主函数代码
// ......省略部分代码
else if (strcmp("search", cmd) == 0){char name[32] = "";printf("请输入要查找的学生姓名:");scanf("%s", name);// 定义一个指针变量保存查找到的值STU *ret = search_func(head, name);if (ret == NULL)printf("未找到该学生信息\n");else{printf("---------------学生信息---------------\n");printf("学号:%03d, 姓名:%-6s, 分数:%.2f\n", ret->num, ret->name, ret->score);printf("---------------末行提示---------------\n");}}
// ......省略部分代码
- 功能函数代码
// 定义函数:查找学生信息
STU *search_func(STU *head, char *name)
{STU *p = head;// 判断链表是否为空if (NULL == head){printf("未添加任何学生信息\n");return NULL;}else{while((strcmp(name, p->name) != 0) && (p->next != NULL)){p = p->next;}if (strcmp(name, p->name) == 0)return p;elsereturn NULL;}
}
- 说明:
- 通过姓名查找学生信息,返回第一个找到的学生信息。功能函数只负责查找,学生姓名的获取在主函数完成;
- 定义一个临时指针变量用于保存找到的节点地址,函数将找到的节点地址通过返回值返回。
请输入要操作的指令:insert
请输入要添加的学生的个数:2
请输入学生信息(学号 姓名 分数):101 小明 88.5
102 小红 85
已添加2位学生信息
请输入要操作的指令:search
请输入要查找的学生姓名:李华
未找到该学生信息
请输入要操作的指令:search
请输入要查找的学生姓名:小红
---------------学生信息---------------
学号:102, 姓名:小红 , 分数:85.00
---------------末行提示---------------
2.4删除某个学生的信息
- 主函数代码
// ......省略部分代码
else if (strcmp("delete", cmd) == 0){int num = 0;printf("请输入要删除的学生学号:");scanf("%d", &num);// 调用函数:删除指定学号信息head = delete_func(head, num);}
// ......省略部分代码
- 功能函数代码
// 定义函数:删除指定学号学生信息
STU *delete_func(STU *head, int num)
{// 定义两个指针:一个用于查找,另外一个用于STU *pa = head, *pb = head;// 判断链表是否存在if (NULL == head){printf("未添加任何学生信息\n");return head;}else{while ((pa->num != num) && (pa->next != NULL)){pb = pa;pa = pa->next;}// 如果要删除的信息存在if (pa->num == num){// 节点在头部if (pa == head){head = head->next;printf("已删除信息:%d\n", pa->num);free(pa);}// 节点在尾部
// else if (pa->next == NULL)
// {
// pb->next == NULL;
// free(pa);
// }// 节点在中尾部else{pb->next = pa->next;printf("已删除信息:%d\n", pa->num);free(pa);}}// 信息不存在elseprintf("要删除的信息不存在\n");}return head;
}
-
说明:
- 因为删除要修改链表结构,或者更改 head 头指向,因此需要将处理后的 head 值返回给函数外部 head 变量接收;
- 需要定义两个指针变量,一个用于查找定位,一个用于记录定位指针的上一个元素。因为要删除节点,因此需要将被删除节点的上一个节点的 next 指向被删除节点的下一个节点;
- 一定要先将被删除节点的上一个节点的 next 指向下一个节点后,再删除该节点,否则该节点以后的链表丢失;
- 节点在中间和节点在尾部时的代码是等价的,因此可以合并,减少代码量。
-
运行结果
请输入要操作的指令:insert
请输入要添加的学生的个数:4
请输入学生信息(学号 姓名 分数):101 小明 88.8
102 小红 85
103 张大炮 83.5
104 王刚 75
已添加4位学生信息
请输入要操作的指令:print
---------------学生信息---------------
学号:101, 姓名:小明 , 分数:88.80
学号:102, 姓名:小红 , 分数:85.00
学号:103, 姓名:张大炮, 分数:83.50
学号:104, 姓名:王刚 , 分数:75.00
---------------末行显示---------------
请输入要操作的指令:delete
请输入要删除的学生学号:103 // 删中间
已删除信息:103
请输入要操作的指令:print
---------------学生信息---------------
学号:101, 姓名:小明 , 分数:88.80
学号:102, 姓名:小红 , 分数:85.00
学号:104, 姓名:王刚 , 分数:75.00
---------------末行显示---------------
请输入要操作的指令:delete
请输入要删除的学生学号:101 //删开头
已删除信息:101
请输入要操作的指令:delete
请输入要删除的学生学号:104
已删除信息:104 // 删末尾
请输入要操作的指令:print
---------------学生信息---------------
学号:102, 姓名:小红 , 分数:85.00
---------------末行显示---------------
请输入要操作的指令:delete
请输入要删除的学生学号:105 // 信息不存在
要删除的信息不存在
2.5清空学生信息
- 主函数代码
// ......省略部分代码
else if (strcmp("free", cmd) == 0){unsigned int password = 0;printf("请输入密码验证:");scanf("%u", &password);if (password == 123456)head = free_func(head);elseprintf("密码错误,无权操作!\n");}
// ......省略部分代码
- 功能函数代码
// d定义函数:清空学生信息
STU *free_func(STU *head)
{STU *p = head;// 判断链表是否为空if (NULL == head){printf("未添加任何学生信息\n");return head;}while (p != NULL){head = head->next;free(p);p = head;}printf("学生信息已清空\n");return NULL;
}
-
说明:
- 释放链表从头到尾逐一释放,注意:要先将 head 指向下一个节点,再释放当前节点;
- 链表清空以后记得返回 NULL 给外部 head 变量;
- 同时可以为该功能添加权限验证,毕竟删除所有学生信息是件很危险的事情。
-
运行结果
请输入要操作的指令:insert
请输入要添加的学生的个数:4
请输入学生信息(学号 姓名 分数):101 小明 88.8
102 小红 85.5
103 张大炮 75
104 黄天霸 65
已添加4位学生信息
请输入要操作的指令:print
---------------学生信息---------------
学号:101, 姓名:小明 , 分数:88.80
学号:102, 姓名:小红 , 分数:85.50
学号:103, 姓名:张大炮, 分数:75.00
学号:104, 姓名:黄天霸, 分数:65.00
---------------末行显示---------------
请输入要操作的指令:free
请输入密码验证:123456
学生信息已清空
请输入要操作的指令:print
未添加任何学生信息
2.6链表逆序
- 主函数代码
// ......省略部分代码
else if (strcmp("reverse", cmd) == 0){// 调用函数,逆序链表head = reverse_func(head);}
// ......省略部分代码
- 功能函数代码
// 定义函数逆序整个链表
STU *reverse_func(STU *head)
{// 定义两个指针变量:pa的next用于指向前一个节点,pb指向pa下一个节点STU *pa = head, *pb = head;// 判断链表是否存在if (NULL == head){printf("未添加任何学生信息\n");return head;}else{pa = head->next;head->next = NULL;while(pa != NULL){pb = pa->next;pa->next = head;head = pa;pa = pb;}printf("链表逆序成功!\n");}return head;
}
-
说明:
- 这里的逆序,不是按照反向排序,只是将链表整体颠倒,不存在排序的作用;
- 逆序链表,即将第一个节点的 next 指向 NULL,将第二个节点的 next 指向第一个节点,第三个节点的 next 指向第二个节点,以此类推,直到最后一个节点的 next 指向倒数第二个节点,将 head 指向最后一个节点;
- 由上面推理,需要一个指针变量 pa 指向需要改变 next 的节点,但当这个节点的 next 指向前一个节点时,下一个节点开始就丢失了,因此需要定义另外一个指针 pb 指向下一个节点,并且要在 pa 的 next 指向上一个节点前将 pb 指向 pa 指向的下一个节点;
- 当 pa 指向 pb 指向的节点前,先将 head 指向 pa 指向的节点,一直重复上面的操作,直到 pa 指向 NULL时,head 就指向了原始链表的尾部,逆序完成;
- 如果链表就一个节点,那 pa 刚开始就指向 NULL,不会进入循环,代码也成立,不需要分情况判断了。
-
运行结果
请输入要操作的指令:insert
请输入要添加的学生的个数:4
请输入学生信息(学号 姓名 分数):101 小明 88.8
102 小红 85.5
103 张大炮 75
104 黄天霸 65
已添加4位学生信息
请输入要操作的指令:print
---------------学生信息---------------
学号:101, 姓名:小明 , 分数:88.80
学号:102, 姓名:小红 , 分数:85.50
学号:103, 姓名:张大炮, 分数:75.00
学号:104, 姓名:黄天霸, 分数:65.00
---------------末行显示---------------
请输入要操作的指令:reverse
链表逆序成功!
请输入要操作的指令:print
---------------学生信息---------------
学号:104, 姓名:黄天霸, 分数:65.00
学号:103, 姓名:张大炮, 分数:75.00
学号:102, 姓名:小红 , 分数:85.50
学号:101, 姓名:小明 , 分数:88.80
---------------末行显示---------------
2.7链表排序
先了解排序的算法。
2.7.1数值数组冒泡排序
冒泡排序,正向排序时,前后两两比较,前一个小于后一个保持不变,前一个大于后一个,两两交换,指针后移一位,每循环一次可以将最大的值排到当次排序的末尾。
例如:为数组int nums = {2, 3, 1, 5, 4}
排序。
2 | 3 | 1 | 5 | 4 |
---|---|---|---|---|
nums | nums + 1 |
- 正向排序时遵循相邻比较,前小于后不变,前大于后交换位置,步骤如下:
第0轮:i = 0
2 | 1 | 3 | 4 | 5 |
---|
找到一个最大值 5 放到数组最后,既然 5 已经是本轮最大,就没必要参与下一轮比较了。第0轮进行了4次比较。
第1轮:i = 1
1 | 2 | 3 | 4 |
---|
第1轮这里虽然看似已经排序完成了,但为了代码的通用性,我们需要考虑极端的情况,例如:int nums = {5, 4, 3, 2, 1}
,可以通过这个例子自行推演。第1轮经历了3次比较。
第2轮:i = 2
1 | 2 | 3 |
---|
第2轮经历了2次比较。
第3轮:i = 3
1 | 2 |
---|
第3轮经历1次比较,排序完成。
- 由上面的推演过程,我们可以总结出规律:假设一个数值数组有 n 个数,会经历 n -1 轮比较,每轮比较会经历 (当前排序的元素个数 - 1) 次比较,轮数从0开始计算,(元素个数 = n - 轮数),联立可得:
(每轮比较次数 = n - i - 1)
。 - 案例:键盘获取10个数为数组赋值,通过冒泡排序法排序,遍历数组
// 定义函数,用于给数组排序
void sort_func(int *nums, int n)
{int i = 0;for (i = 0; i < n - 1; i++) // 每循环一次是一轮{int j = 0;for (j = 0; j < n - i - 1; j++) // 每轮比较的次数{if(nums[j] > nums[j + 1]){int temp = nums[j];nums[j] = nums[j + 1];nums[j + 1] = temp;}}}
}int main()
{// 定义一个数值数组int nums[10] = {0};int n = sizeof(nums) / sizeof(nums[0]);printf("请输入10个整数:");int i = 0;for (i = 1; i < n; i++){scanf("%d", &nums[i]);}// 调用函数为数组排序sort_func(nums, n);// 遍历数组for (i = 1; i < n; i++){printf("%d ", nums[i]);}printf("\n");return 0;
}
- 运行结果
请输入10个整数:21 35 12 5 89 74 36 92 9 16
5 9 12 21 35 36 74 89 92
2.7.2数值数组选择排序
前面的冒泡排序,每轮能找出一个最大值,这里的选择排序则是每轮找出一个最小值。定义一个最小值变量,假设每轮第一个值最小,然后第一个数与后面其它数比较,小于就继续与下一个比较,大于就将 min 指向较小的那一个,比较结束,如果 min 已经不是第一个元素,那么与第一个元素交换位置。
例如:为数组int nums = {2, 3, 1, 5, 4}
排序。
2 | 3 | 1 | 5 | 4 |
---|---|---|---|---|
nums | nums + 1 |
第0轮:i = 0
1 | 3 | 2 | 5 | 4 |
---|
第1轮:i = 1
2 | 3 | 5 | 4 |
---|
第2轮:i = 2
3 | 5 | 4 |
---|
第3轮:i = 3
4 | 5 |
---|
-
由上面的推演过程,我们可以总结出规律:假设一个数值数组有 n 个数,会经历 n -1 轮比较,轮数从0开始计算,每轮会进行
n - i - 1
次比较。 -
代码演示
// 定义函数,用于给数组排序
void sort_func(int *nums, int n)
{int i = 0;for (i = 0; i < n - 1; i++){// 定义一个最小值索引,并假设第一个元素值最小int min = 0, j = 0;for (min = i, j = i + 1; j < n; j++){// 依次比较,如果min不是最小,就定位到最小if (nums[min] > nums[j])min = j;}// 每轮比较完,如果不是假设的第一个值,那么就交换两个的值if (min != i){int temp = nums[min];nums[min] = nums[i];nums[i] = temp;}}
}
- 运行结果
请输入10个整数:21 35 12 5 89 74 36 92 9 16
5 9 12 21 35 36 74 89 92
- 说明:相比于冒泡排序,选择排序交换的次数更少。
2.7.3链表排序实现
链表排序最麻烦的步骤就是两个节点的交换了,由上面两种排序方式对比,选择排序交换次数较少,这里选用选择排序演示。
- 代码演示
// 定义函数:为链表排序
void sort_func(STU *head)
{// 判断链表是否存在if (NULL == head){printf("未添加任何学生信息\n");return;}else{STU *p_i = NULL;for (p_i = head; p_i->next != NULL; p_i = p_i->next){STU *p_min = NULL, *p_j = NULL;for (p_min = p_i, p_j = p_i->next; p_j != NULL; p_j = p_j->next){if (p_min->num > p_j->num)p_min = p_j;}if (p_min != p_i){// 整体交换节点成员STU temp = *p_min;*p_min = *p_i;*p_i = temp;// 将指针域恢复原样temp.next = p_min->next;p_min->next = p_i->next;p_i->next = temp.next;}}printf("排序完成\n");}
}
- 运行结果
请输入要操作的指令:insert
请输入要添加的学生的个数:4
请输入学生信息(学号 姓名 分数):105 小明 88.8
101 小红 85.5
103 张伟 90
102 张大炮 65
已添加4位学生信息
请输入要操作的指令:print
---------------学生信息---------------
学号:105, 姓名:小明 , 分数:88.80
学号:101, 姓名:小红 , 分数:85.50
学号:103, 姓名:张伟 , 分数:90.00
学号:102, 姓名:张大炮, 分数:65.00
---------------末行显示---------------
请输入要操作的指令:sort
排序完成
请输入要操作的指令:print
---------------学生信息---------------
学号:101, 姓名:小红 , 分数:85.50
学号:102, 姓名:张大炮, 分数:65.00
学号:103, 姓名:张伟 , 分数:90.00
学号:105, 姓名:小明 , 分数:88.80
---------------末行显示---------------
请输入要操作的指令:reverse
链表逆序成功!
请输入要操作的指令:print
---------------学生信息---------------
学号:105, 姓名:小明 , 分数:88.80
学号:103, 姓名:张伟 , 分数:90.00
学号:102, 姓名:张大炮, 分数:65.00
学号:101, 姓名:小红 , 分数:85.50
---------------末行显示---------------
请输入要操作的指令:quit
程序已经退出
- 说明:
- 链表排序和数组排序原理一样,只不过把数组是通过下标索引定位具体的元素,链表换成了指针变量定位节点;
- 数组排序是
i++
操作,在链表里换成了p_i -> next
操作; - 数组的
i < n
换成了链表的p_i -> next != NULL
; - 数组的位置交换,换成了节点数据域交换(注意:只是节点数据域交换,不是节点交换位置),为方便操作先将节点数据整体交换,再复原指针域。
三、完整代码演示(单文件版)
- 代码演示
#include <stdio.h>
#include <stdlib.h>
#include <string.h>// 一个简单的学生管理系统
typedef struct stu {// 定义数据域int num;char name[32];float score;// 定义指针域struct stu *next;
} STU;// 定义函数,打印菜单界面
void func_list(void) {printf("-----------------------------------\n");printf("-->insert:插入学生信息 |\n");printf("-->print:遍历信息 |\n");printf("-->search:查询某个学生的信息 |\n");printf("-->delete:删除某个学生的信息 |\n");printf("-->free:删除所有学生 |\n");printf("-->reverse:链表逆序 |\n");printf("-->sort:链表排序 |\n");printf("-->quit:退出整个程序 |\n");printf("-----------------------------------\n");return;
}// 定义函数:在头部插入学生信息
STU *insert_func_head(STU *head, STU stu_temp)
{// 申请堆区空间,存储一个学生的信息STU *new_stu = (STU *)malloc(sizeof(STU));if (NULL == new_stu){printf("空间申请失败\n");return head;}// 将获取到的学生信息存入申请到的空间*new_stu = stu_temp;// 将新的学生节点插入链表// 链表为空if (NULL == head){head = new_stu;new_stu->next = NULL;}// 链表不为空else{new_stu->next = head;head = new_stu;}return head;
}// 定义函数:打印学生信息
void print_func(STU *head)
{// 判断链表是否为空if (NULL == head){printf("未添加任何学生信息\n");return;}// 链表不为空,遍历学生信息STU *p = head;printf("---------------学生信息---------------\n");while(p != NULL){printf("学号:%03d, 姓名:%-6s, 分数:%.2f\n", p->num, p->name, p->score);p = p->next;}printf("---------------末行显示---------------\n");
}// 定义函数:在尾部插入学生信息
STU *insert_func_tail(STU *head, STU stu_temp)
{// 申请堆区空间,存储一个学生的信息STU *new_stu = (STU *)malloc(sizeof(STU));if (NULL == new_stu){printf("空间申请失败\n");return head;}// 将获取到的学生信息存入申请到的空间*new_stu = stu_temp;new_stu->next = NULL;// 将新的学生节点插入链表// 链表为空if (NULL == head){head = new_stu;}// 链表不为空else{STU *p = head;while(p->next != NULL){p = p->next;}p->next = new_stu;}return head;
}// 定义函数:按照学号顺序插入
STU *insert_func_sequence(STU *head, STU stu_temp)
{// 申请堆区空间,存储一个学生的信息STU *new_stu = (STU *)malloc(sizeof(STU));if (NULL == new_stu){printf("空间申请失败\n");return head;}// 将获取到的学生信息存入申请到的空间*new_stu = stu_temp;new_stu->next = NULL;// 将新的学生节点插入链表// 链表为空if (NULL == head){head = new_stu;}// 链表不为空else{// 定义两个指针:pa用于寻找插入位置,pb用于保存pa的上一个节点位置STU *pa = head, *pb = head;while((pa != NULL) && (pa->num < new_stu->num)){pb = pa;pa = pa->next;}// 插入链表最后一个if (NULL == pa){pb->next = new_stu;// new_stu = pb->next;}// 插入链表第一个else if(pa == head){new_stu->next = head;head = new_stu;}// 插入到链表的中间else{pb->next = new_stu;new_stu->next = pa;}}return head;
}// 定义函数:查找学生信息
STU *search_func(STU *head, char *name)
{STU *p = head;// 判断链表是否为空if (NULL == head){printf("未添加任何学生信息\n");return NULL;}else{while((strcmp(name, p->name) != 0) && (p->next != NULL)){p = p->next;}if (strcmp(name, p->name) == 0)return p;elsereturn NULL;}
}// 定义函数:删除指定学号学生信息
STU *delete_func(STU *head, int num)
{// 定义两个指针:一个用于查找,另外一个用于STU *pa = head, *pb = head;// 判断链表是否存在if (NULL == head){printf("未添加任何学生信息\n");return head;}else{while ((pa->num != num) && (pa->next != NULL)){pb = pa;pa = pa->next;}// 如果要删除的信息存在if (pa->num == num){// 节点在头部if (pa == head){head = head->next;printf("已删除信息:%d\n", pa->num);free(pa);}// 节点在尾部
// else if (pa->next == NULL)
// {
// pb->next == NULL;
// free(pa);
// }// 节点在中尾部else{pb->next = pa->next;printf("已删除信息:%d\n", pa->num);free(pa);}}// 信息不存在elseprintf("要删除的信息不存在\n");}return head;
}// d定义函数:清空学生信息
STU *free_func(STU *head)
{STU *p = head;// 判断链表是否为空if (NULL == head){printf("未添加任何学生信息\n");return head;}while (p != NULL){head = head->next;free(p);p = head;}printf("学生信息已清空\n");return NULL;
}// 定义函数逆序整个链表
STU *reverse_func(STU *head)
{// 定义两个指针变量:pa的next用于指向前一个节点,pb指向pa下一个节点STU *pa = head, *pb = head;// 判断链表是否存在if (NULL == head){printf("未添加任何学生信息\n");return head;}else{pa = head->next;head->next = NULL;while(pa != NULL){pb = pa->next;pa->next = head;head = pa;pa = pb;}printf("链表逆序成功!\n");}return head;
}// 定义函数:为链表排序
void sort_func(STU *head)
{// 判断链表是否存在if (NULL == head){printf("未添加任何学生信息\n");return;}else{STU *p_i = NULL;for (p_i = head; p_i->next != NULL; p_i = p_i->next){STU *p_min = NULL, *p_j = NULL;for (p_min = p_i, p_j = p_i->next; p_j != NULL; p_j = p_j->next){if (p_min->num > p_j->num)p_min = p_j;}if (p_min != p_i){// 整体交换节点成员STU temp = *p_min;*p_min = *p_i;*p_i = temp;// 将指针域恢复原样temp.next = p_min->next;p_min->next = p_i->next;p_i->next = temp.next;}}printf("排序完成\n");}
}int main() {// 调用函数:打印功能菜单func_list();// 定义一个链表头STU *head = NULL;// 获取指令char cmd[16] = "";while(1){printf("请输入要操作的指令:");scanf("%s", cmd);if (strcmp("insert", cmd) == 0){// 定义临时变量用于接收输入的学生信息STU stu_temp;int n = 0;printf("请输入要添加的学生的个数:");scanf("%d", &n);printf("请输入学生信息(学号 姓名 分数):");// 调用函数:添加学生信息int i = 0;for (i = 0; i < n; i++){scanf("%d %s %f", &stu_temp.num, stu_temp.name, &stu_temp.score);// 调用函数:添加学生信息// head = insert_func_head(head, stu_temp); // 头部插入head = insert_func_tail(head, stu_temp); // 尾部插入// head = insert_func_sequence(head, stu_temp); // 顺序插入}printf("已添加%d位学生信息\n", n);}else if (strcmp("print", cmd) == 0){// 调用函数:打印学生信息print_func(head);}else if (strcmp("search", cmd) == 0){char name[32] = "";printf("请输入要查找的学生姓名:");scanf("%s", name);// 定义一个指针变量保存查找到的值STU *ret = search_func(head, name);if (ret == NULL)printf("未找到该学生信息\n");else{printf("---------------学生信息---------------\n");printf("学号:%03d, 姓名:%-6s, 分数:%.2f\n", ret->num, ret->name, ret->score);printf("---------------末行提示---------------\n");}}else if (strcmp("delete", cmd) == 0){int num = 0;printf("请输入要删除的学生学号:");scanf("%d", &num);// 调用函数:删除指定学号信息head = delete_func(head, num);}else if (strcmp("free", cmd) == 0){unsigned int password = 0;printf("请输入密码验证:");scanf("%u", &password);if (password == 123456)head = free_func(head);elseprintf("密码错误,无权操作!\n");}else if (strcmp("reverse", cmd) == 0){// 调用函数,逆序链表head = reverse_func(head);}else if (strcmp("sort", cmd) == 0){// 调用函数:为链表中学生按照学号排序sort_func(head);}else if (strcmp("quit", cmd) == 0){printf("程序已经退出\n");break;}else{printf("请输入有效命令\n");}}return 0;
}
- 说明:在实际项目中,不会把所有的代码都放入一个文件中,一般都是多文件编程,
main.c
里面只放主函数代码,其它具体的功能函数会放到某个单独的文件里,实现某一类特定功能,如:func.c
,然后创建一个头文件:func.h
,将func.c
中定义的函数在头文件里声明,最后main.c
里包含头文件即可:#include "func.h"
。
相关文章:
12-C语言单向链表
一、链表的概述 1.链表与数组对比 遍历数组中的数据,查询数据比较方便,但往数组中插入、删除数据需要移动大量数据;相反。链表遍历、查询数据不方便,但是插入、删除数据比较方便,不需要移动大量数据,直接…...

2024年11月 蓝桥杯青少组 STEMA考试 Scratch真题
2024年11月 蓝桥杯青少组 STEMA考试 Scratch真题(选择题) 题目总数:5 总分数:50 选择题 第 1 题 单选题 Scratch运行以下程宇后,小兔子会( )。 A. 变小 B. 变大 C. 变色 D. …...

FFmpeg 4.3 音视频-多路H265监控录放C++开发二十一.2,RTP协议-RTP协议概述,协议详情
前提: 为什么要学习 RTP(Real-time Transport Protocol)重点 简介:RTP是一个实时传输媒体数据的协议,通常与RTSP一起使用。它负责在网络上传输音视频数据。特点:RTP通过UDP或TCP传输媒体数据,提供时间戳和序…...

Linux系统编程——系统内核中的信号
目录 一、前言 二、系统内核中的信号 三、sigset_t 四、信号集操作 1、sigpending(); 2、sigemptyset(); 3、sigfillset(sigset_t *set); 4、int sigaddset ()和sigdelset() 编辑 5、sigismember() 6、sigprocmask() 五、信号集操作代码演示 六、深入理解进程的信…...

delve调试环境搭建—golang
原文地址:delve调试环境搭建—golang – 无敌牛 欢迎参观我的个人博客:无敌牛 – 技术/著作/典籍/分享等 由于平时不用 IDE 开发环境,习惯在 linux终端vim 环境下开发,所以找了golang的调试工具,delve类似gdb的调试界…...

shell脚本的循环-----while和for循环
一、while 1.格式 while 条件表达式; do 命令 done 2.案例 : ping测试子网段的主机网段由用户输入,例如用户输入192.168.101 ,则ping192.168.101.125 — 192.101.131 UP: /tmp/host_up.txt Down: /tmp/host_down.txt &#…...

【游戏设计原理】21 - 解谜游戏的设计
你想象一下,刚坐下准备玩游戏,想着“今天得挑战一下我的智商极限!”可结果碰上一个谜题,傻眼了,心里默念:“这啥玩意儿?这游戏是在玩我吗?”如果这个谜题太简单了,你可能…...
【漏洞复现】Wordpress GutenKit 插件 远程文件写入致RCE漏洞复现(CVE-2024-9234)
🏘️个人主页: 点燃银河尽头的篝火(●’◡’●) 如果文章有帮到你的话记得点赞👍+收藏💗支持一下哦 一、漏洞概述 1.1漏洞简介 漏洞名称:Wordpress GutenKit 插件 远程文件写入致RCE漏洞复现漏洞编号:CVE-2024-9234漏洞威胁等级:超危影响范围:GutenKit <= 2.1.0…...
深度学习任务简介:分类、回归和生成
深度学习任务简介:分类、回归和生成 文章目录 深度学习任务简介:分类、回归和生成一、分类任务(Classification Task)什么是分类任务?**分类任务的常见应用**分类任务的输出主要算法 二、回归任务(Regressi…...

【测试】Unittest
近期更新完毕,建议关注收藏! 目录 简介TestCaseTestSuiteTestRunnerTestLoaderFixture tips:ctrl? 可以看方法的help文档 简介 python自带的单元测试框架,也可以做自动化测试 组织多个用例去执行,用例都是用单独的目录存放的 丰…...
java 根据路径下载文件转换为MultipartFile,并且上传到服务器
直接上代码 controller层 GetMapping("/downloadAndUploadAttachment")UpdateOperationLogging(msg "根据路径下载文件转换为MultipartFile,并且上传到服务器")Operation(summary "根据路径下载文件转换为MultipartFile,并且上传到服务器", de…...
Onvif服务端开发
实现了Onvif服务端的设备搜索和RTSP流的功能。用 ONVIF Device Manager 测试工具可以成功搜索到设备和获取到RTSP流,有的路由器可能不支持239.255.255.250组播,我一开始用的电信的那种光猫路由器二合一的,一直搜不到设备,后面用So…...
【jvm】主要参数
Java 虚拟机(JVM)有许多参数用于控制其行为和性能,下面是一些 主要的 JVM 启动参数,这些参数通常分为以下几类: 内存管理相关参数 这些参数主要用来配置 JVM 的内存分配策略、堆内存、栈内存等。 -Xms 设置 JVM 启动…...

【优选算法】—移动零(双指针算法)
云边有个稻草人-CSDN博客 想当一名牛的程序员怎么能少的了练习算法呢?! 今天就立即开启一个新专栏,专干算法,提高算法能力(废柴的我也在准备蓝桥杯哈哈)—— 目录 1.【 283. 移动零 - 力扣(Lee…...
PostgreSQL标识符长度限制不能超过63字节
文章目录 问题:标识符太长会被截断分析相关源码可以尝试以下案例 问题:标识符太长会被截断 在创建表时,发现表名太长会自动被截断,导致查询表时报错了。 分析 参考:https://www.postgresql.org/docs/current/limits…...
嵌入式硬件面试题
1、请问什么是通孔、盲孔和埋孔?孔径多大可以做机械孔,孔径多小必须做激光孔?请问激光微型孔可以直接打在元件焊盘上吗,为什么? 通孔是贯穿整个PCB的过孔,盲孔是从PCB表层连接到内层的过孔,埋孔…...

深度解析 OneCode 混合编译:创新驱动的开发变革
前言 在软件开发领域,不断追求高效、灵活与强大的开发模式是永恒的主题。OneCode 作为一款引领潮流的开发工具,其混合编译特性正逐渐成为开发界瞩目的焦点。本文将深入剖析 OneCode 的混合编译机制,揭示它如何为软件开发带来前所未有的变革与…...

[文献阅读] Unsupervised Deep Embedding for Clustering Analysis (无监督的深度嵌入式聚类)
文章目录 Abstract:摘要聚类深度聚类 KL散度深度嵌入式聚类(DEC)KL散度聚类软分配(soft assignment)KL散度损失训练编码器的初始化聚类中心的初始化 实验评估总结 Abstract: This week I read Unsupervised Deep Embedding for Clustering Analysis .It…...
ajax中get和post的区别,datatype返回的数据类型有哪些?web开发中数据提交的几种方式,有什么区别。
在 Web 开发中,GET 和 POST 是两种常见的 HTTP 请求方法,它们有一些显著的区别。此外,datatype 参数在 jQuery 的 ajax() 请求中指定了预期的响应数据类型。接下来,我会详细解释这些问题。 1. GET 和 POST 请求的区别 GET 请求 和…...
网络七层杀伤链
声明! 学习视频来自B站up主 **泷羽sec** 有兴趣的师傅可以关注一下,如涉及侵权马上删除文章,笔记只是方便各位师傅的学习和探讨,文章所提到的网站以及内容,只做学习交流,其他均与本人以及泷羽sec团队无关&…...

JavaSec-RCE
简介 RCE(Remote Code Execution),可以分为:命令注入(Command Injection)、代码注入(Code Injection) 代码注入 1.漏洞场景:Groovy代码注入 Groovy是一种基于JVM的动态语言,语法简洁,支持闭包、动态类型和Java互操作性,…...

零基础设计模式——行为型模式 - 责任链模式
第四部分:行为型模式 - 责任链模式 (Chain of Responsibility Pattern) 欢迎来到行为型模式的学习!行为型模式关注对象之间的职责分配、算法封装和对象间的交互。我们将学习的第一个行为型模式是责任链模式。 核心思想:使多个对象都有机会处…...

selenium学习实战【Python爬虫】
selenium学习实战【Python爬虫】 文章目录 selenium学习实战【Python爬虫】一、声明二、学习目标三、安装依赖3.1 安装selenium库3.2 安装浏览器驱动3.2.1 查看Edge版本3.2.2 驱动安装 四、代码讲解4.1 配置浏览器4.2 加载更多4.3 寻找内容4.4 完整代码 五、报告文件爬取5.1 提…...

蓝桥杯3498 01串的熵
问题描述 对于一个长度为 23333333的 01 串, 如果其信息熵为 11625907.5798, 且 0 出现次数比 1 少, 那么这个 01 串中 0 出现了多少次? #include<iostream> #include<cmath> using namespace std;int n 23333333;int main() {//枚举 0 出现的次数//因…...

如何在网页里填写 PDF 表格?
有时候,你可能希望用户能在你的网站上填写 PDF 表单。然而,这件事并不简单,因为 PDF 并不是一种原生的网页格式。虽然浏览器可以显示 PDF 文件,但原生并不支持编辑或填写它们。更糟的是,如果你想收集表单数据ÿ…...

力扣热题100 k个一组反转链表题解
题目: 代码: func reverseKGroup(head *ListNode, k int) *ListNode {cur : headfor i : 0; i < k; i {if cur nil {return head}cur cur.Next}newHead : reverse(head, cur)head.Next reverseKGroup(cur, k)return newHead }func reverse(start, end *ListNode) *ListN…...
C++课设:简易日历程序(支持传统节假日 + 二十四节气 + 个人纪念日管理)
名人说:路漫漫其修远兮,吾将上下而求索。—— 屈原《离骚》 创作者:Code_流苏(CSDN)(一个喜欢古诗词和编程的Coder😊) 专栏介绍:《编程项目实战》 目录 一、为什么要开发一个日历程序?1. 深入理解时间算法2. 练习面向对象设计3. 学习数据结构应用二、核心算法深度解析…...
Vite中定义@软链接
在webpack中可以直接通过符号表示src路径,但是vite中默认不可以。 如何实现: vite中提供了resolve.alias:通过别名在指向一个具体的路径 在vite.config.js中 import { join } from pathexport default defineConfig({plugins: [vue()],//…...

pikachu靶场通关笔记19 SQL注入02-字符型注入(GET)
目录 一、SQL注入 二、字符型SQL注入 三、字符型注入与数字型注入 四、源码分析 五、渗透实战 1、渗透准备 2、SQL注入探测 (1)输入单引号 (2)万能注入语句 3、获取回显列orderby 4、获取数据库名database 5、获取表名…...

永磁同步电机无速度算法--基于卡尔曼滤波器的滑模观测器
一、原理介绍 传统滑模观测器采用如下结构: 传统SMO中LPF会带来相位延迟和幅值衰减,并且需要额外的相位补偿。 采用扩展卡尔曼滤波器代替常用低通滤波器(LPF),可以去除高次谐波,并且不用相位补偿就可以获得一个误差较小的转子位…...