想踏踏实实打好基础,把链表再认真复习一次,也为了上课更有趣一点
一、单链表
1、单链表的节点结构
typedef struct Node {
DataType data;
struct Node *next;
}Node,*Link;
//Node st == struct Node st;
//Link p == struct Node *p
//p = &st;
// p = (Link)malloc(sizeof(Node)) == p = (struct Node*)malloc(sizeof(Node))
Link其实就是一个指向结构体的指针(==是等价的意思)
2、申请节点
p = (Link) malloc (sizeof(Node));
//malloc返回类型是void*,会把首地址返回给指针p。在使用这块空间时需要强制类型转换
//p是Link类型。
//Link == struct Node *(前文有说)
3、引用数据元素
(*p).data;
p -> data;
p -> next;
4、存储结构
//空表
head = NULL;
//最后一个节点指针域为空
//虚拟头节点
5、单链表的遍历操作
以下操作中head都是虚拟头节点
//操作接口 void displayNode (Link head)
void displayNode (Link head) {
Link p = head -> next;
while (p != NULL) {
printf("%d", p -> data);
p = p -> next;//遍历
}
}
Q: p++能否完成指针后移?
链表结点并非一定连续,不能完成。
6、求单链表的元素个数
//操作接口: int length (Link head)
int length (Link head)
{
int count = 0;
Link p = head -> next;
while (p != NULL) {
count++;
p = p -> next;
}
return count;
}
7、单链表的查找操作
bool queryNode (Link head, DataType x) {
Link p = head -> next;
count = 0;
while (p != NULL) {
if (p -> data == x) {
printf("%d", p -> data);
return true;
}
p = p -> next;
}
return false;//只要找到了就提前出去了,不会执行到这里啦。
}
8、单链表插入操作
//由于链表带了头节点,表头表中表尾的语句完全一致
//由于p始终指向待插入位置的前一个位置,因此要在第i个位置插入节点,就必须让p指向第i-1个节点
bool insertNode (Link head, int i, DataType x) {
Link p = head;
count = 0;
while (p != NULL) {
p = p -> next;
count++
} //查找操作
if (p == NULL) {
return false; //p已经指向表尾了,说明没有找到第i-1个节点
}
else {
Link node;
node = (Link)malloc(sizeof(Node));
node -> data = x;
node -> next = p -> next;
p -> next = node;
return true;
}
}
9、创建一个单链表——头插法
Link newList(DataType a[], int n) {
//事先把元素存放在数组a中
Link head;
head = (Link)malloc(sizeof(Node));
head -> next = NULL;
//这是一个很好的习惯,就是每创建一个节点,就给他的指针域赋为空
for (int i = 0; i < n; i++) {
Link node;
node = (Link)malloc(sizeof(Node));
node -> data = a[i];
node -> next = head -> next; //head -> next = NULL
head -> next = node;
}
return head;
}
头插法插入的第一个元素是数组中第一个元素,之后不断在head后面插入元素,也就是说链表的元素顺序和数组元素顺序是相反的
10、创建一个单链表——尾插法
Link newList_AtTail(DataType a[], int n) {
Link head;
head = (Link)malloc(sizeof(Node));
head -> next = NULL;
Link rear;
rear = head; // 设置一个尾部指针
for (int i = 0; i < n; i++) {
Link node;
node = (Link)malloc(sizeof(Node));
node -> next = NULL;
node -> data = a[i];
node -> next = rear -> next;
rear = node;
//最后一个元素的节点指针域要置空
}
//如果没有node置空,就写这个:rear -> next = NULL;
return head;
}
最后一个节点指针域置空可能忘记,所以做一个小改动。即在node建立时就置空。
这是一个好习惯!
11、单链表结点的删除
bool deleteNode(Link head, DataType x) {
//关键就是有一个前驱节点
//一个双指针问题
//1.重点是如何保证p,q一前一后?那就是在初始化上做文章
//2.边界考虑:如果传入的是一个空表呢? head == NULL或者head -> next == NULL
if (head == NULL || head -> next == NULL) return false;
//边界处理完毕
Link q, p;
q = head;
p = head -> next;
while (p != NULL) {
//说明没到表尾
if (p -> data == x) {
q -> next = p -> next;
free(p);
return true;
}
else {
q = p;
p = p -> next;
}
}
//p == NULL时,已经知道表尾了,说明没有找到。没有提前返回true就说明没找到
return false;
}
12、单链表的释放
void clearLink(Link head) {
while (head != NULL) {
Link q = head;
head = head -> next;
free(q);
}
}
二、循环链表、双向链表
循环链表没有明显的尾端,怎么才能避免死循环
循环退出条件:(详细见约瑟夫环问题)
//p != NULL
p != head;
//p -> next != NULL
p -> next != head;
三、学生管理系统
1、定义结构体和链表节点
//定义学生结构体
typedef struct Student {
char studentNo[20]; //学号
char studentName[11]; //姓名
} st;
//定义链表节点
typedef struct node {
struct Student data; //数据域是一个结构体
struct node *next;
}Node, *Link;
//Node为node类型的别名。Link为node类型的指针别名
2、链表的建立
字符串的输入 | |
字符串的比较函数strcmp | int strcmp(const char *str1, const char *str2) |
小于,返回值<0 大于,返回值>0 相等,返回值==0 |
//假定插入是有顺序的(要按照学号从小到大排序),也就是说每次准备插入一个学生节点时先要找到节点应该插到哪个节点的前面
//要考虑的问题有什么(首先创建一个头节点)
//1、当前链表为空,也就是说head -> next = NULL,此时应该node = malloc, node -> next = NULL; (链表初始化)
//2、当前链表已经有了节点,题目要求按学号顺序插入,也就是说我们要比较node -> data -> No 和p -> data -> No的大小,注意学号是数组。 插入用双指针法。学号小于时,插入p和前驱q之间。大于的话p、q同时向后移动。
bool addLink (Link head) {
Link q,p;
Link node;
node = (Link)malloc(sizeof(Node));
fgets(node -> data.studentNo, 19, stdin);
fgets(node -> data.studentName, 19, stdin); //字符串的输入很重要。
q = head;
p = haed -> next;
if (head -> next == NULL)
head -> next = node;
else {
while (p != NULL) {
if ( strcmp(p -> data.studentNo, node -> data.studentNo) > 0) {
//node的学号小于p的学号,要插在p和q之间,然后退出本次函数
node -> next = p;
q -> next = q;
return true;
}
//学号是字符串类型的数组,可以利用strcmp函数来比较大小
else {
q = p;
p = p => next;
}
}
//如果没有退出循环,说明前面没有插入node,说明node需要插入表尾。
p -> next = node;
return false;
}
}
3、链表删除
//仍然是熟悉的双指针法
//p遍历,q前驱,找到删除,找不到返回false
//参数有学生的学号,要比较给的学生学号和p的学号的大小
bool deleteLink (Link head, char *num) {
if (strlen(num) != 19) return false; //输入字符长度规范。
Link p,q;
q = head;
p = head -> next;
while (p != NULL) {
if (strcmp(p -> data.studentNo, num) == 0) {
//说明两个字符串相等
q -> next = p -> next;
free(p);
}
else {
q = p;
p = p -> next;
}
}
//如果还能运行到这里,说明前面没有找到
return false;
}
4、修改链表
字符串整体赋值问题 | |
指针赋值法 | 指针赋值法其实是把我么们准备好的新的字符串的首地址传给指针,相当于修改了指针的指向。并不能达到我们期望的整体赋值的效果。 |
strcpy函数 | char *strcpy(char *str1, const char *str2) str1是被赋值的字符串,str2是新字符串。 |
//修改学号,且修改后不能破坏链表的有序性。也就是说还需要排序。
//字符串Name是被修改的姓名,字符串num是要修改的学号
bool changeStudentNo (Link head, char *Name, char *num) {
Link q, p;
if (head -> next == NULL) return false;
q = head;
p = head -> next;
while (p != NULL) {
//如果找到了,把p赋给node
if (strcmp(Name, p -> data.studentName) == 0) {
node = p;
strcpy(node -> data.No, num); //修改node的学号
//由于学号改大了和改小了的操作是不一样的。因此我们选择插入元素后从头开始对链表进行排序。
}
//排序部分内容等学完排序后再回来写
}
//走到这里说明没找到
return false;
}
5、链表清除
释放内存(很重要)
void clearLink (Link head) {
Link p = head;
while (head != NULL) {
p = head;
head = head -> next;
free(p);
}
}
本节内容有两个坑留下了,以后要记得填坑 | |
链表排序 | 学完最后一章排序内容,要选择效率高的写法进行编写 |
学生管理系统 | 自己完成一版完整的学生管理系统,并且可以学着做一个用户界面出来 |