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团队无关&…...

GAN网络详解及涨点大全总结(源码)
(需要源码请私信或评论) GAN原理 GAN的基本原理建立在 生成模型和判别模型的博弈过程 上。这种独特的机制使得GAN能够在复杂的分布上实现高效的无监督学习。在这个过程中,生成器G和判别器D相互竞争,最终达到一种平衡状态,在此状态下,G能够产生高质量的合成样本,而D则无…...
【自动化】Python SeleniumUtil 工具 开启开发者模式 自动安装油猴用户脚本等
【自动化】Python SeleniumUtil 工具 【Python】使用Selenium 操作浏览器 自动化测试 记录-CSDN博客文章浏览阅读58次。文章浏览阅读42次。【附件】Selenium chromedriver 驱动及浏览器下载。【附件】Selenium chromedriver 驱动及浏览器下载-CSDN博客。3.安装Chrome浏览器驱动…...

【Linux打怪升级记 | 问题01】安装Linux系统忘记设置时区怎么办?3个方法教你回到东八区
🗺️博客地图 📍方法一、timedatectl 命令 📍方法二、手动链接 /etc/localtime 📍方法三、修改时区变量 在 Linux 系统中,可以通过以下3种方式将系统时区修改为 CST(中国标准时间,GMT8 或称 …...

android:sharedUserId 应用进程声明介绍
背景 adb install 安装系统软件报错,原因是签名不一致,进程改变。 代码分析 AndroidManifest.xml 定义的 android:sharedUserId 应用归属进程不同,从phone切换到system。 初始配置 <manifest xmlns:android="http://schemas.android.com/apk/res/android"c…...

解锁ApplicationContext vs BeanFactory: 谁更具选择性?
目录 一、聚焦源码回顾 (一)源码分析和理解 (二)简短的回顾对比建议 二、ApplicationContext vs BeanFactory特性对比 (一)主要特性总结 (二)直接建议 三、案例简单说明 &am…...

一篇梳理清楚http请求知识点
HTTP请求是Web开发中的重要组成部分,它涉及到客户端和服务器之间的通信。掌握HTTP请求的知识点对于前端开发和后端开发都至关重要。以下是关于HTTP请求的详细梳理,结合代码进行说明。 1. HTTP请求概述 HTTP(超文本传输协议)是一个…...

Kotlin - 协程结构化并发Structured Concurrency
前言 Kotlin的Project Lead,Roman Elizarov的一片文章https://elizarov.medium.com/structured-concurrency-722d765aa952介绍了Structured Concurrency发展的背景。相对Kotlin1.1时代,后来新增的Structured Concurrency理念,也就是我们现在所…...

新版国标GB28181设备端Android版EasyGBD支持国标GB28181-2022,支持语音对讲,支持位置上报,开源在Github
经过近3个月的迭代开发,新版本的国标GB28181设备端EasyGBD安卓Android版终于在昨天发布到Github了,最新的EasyGBD支持了国标GB28181-2022版,还支持了语音对讲、位置上报、本地录像等功能,比原有GB28181-2016版的EasyGBD更加高效、…...

豆包MarsCode测评:编程效率再提升
豆包MarsCode测评:编程效率再提升 本文正在参与豆包MarsCode AI 编程体验家活动 随着人工智能技术的发展,编程的方式也在悄然发生变化。最近,豆包推出的 AI 编程工具 MarsCode 在开发者社区引发了不小的关注。这是一款支持多种主流编程语言…...

二叉树 -- 堆(详解)
目录 1、堆的概念及结构 2、堆的实现(附代码) 2.1、向下调整算法建堆 3、堆的应用(附代码) 3.1、堆排序 3.2、TOP-K问题 1、堆的概念及结构 如果有一个关键码的集合K { k0,k1 ,k2 ,…,k(n-1) },把它的所有元素…...