(十四)单链表
前面我们讲结构体说过,之所以发明结构体,就是因为最基本的数据类型颗粒度太小,如果只用最基本的数据类型,那我们上面例子中的书籍录入的实现就会非常麻烦,所以我们发明了结构体。结构体的实质就是,把最基本的、最细颗粒的数据类型进行打包封装,然后我们直接用即可,所以图书信息录入的实现简单了很多。但是,随着业务需求的更加复杂,结构体也变成了非常基本的颗粒了,人们就在结构体的基础搭配指针等其他元素的特性,发明了链表结构。
现在链表也是一种常见的基础数据结构。有单链表、双链表、循环链表、块状链表等。
1、什么是链表
从上图可以看出,链表里面的各个元素在内存中可以不必紧密挨在一起,只要有一个细小的空间能容纳一个节点,就能存储一个节点的信息,而无需申请一个连续的大空间,将所有的节点紧挨放进去。那这种随意找地方写入的一个个节点,读取的时候,怎么能按照顺序一个个有序的读出呢?从上图可以看出,只要我们把每个节点的最后一个指针成员的值写成下一个节点的地址即可。
逻辑是这样的:第一个头指针head就是一个普通指针,这个指针指向的空间的数据类型是节点的数据类型。然后我们把第一个节点的地址赋值给指针变量head。那我们就可以通过head找到第一个节点了。那找到第一个节点,是不是就可以读出第一个节点里面的成员p的值了,前面说了,我们给p的值赋的是它下一个节点的地址。所以,此时我们是不是就可以把第一个节点里面的p值(第二个节点的地址)赋值给head,此时是不是就可以通过head找到第二个节点的信息了。以此类推,是不是依次就读出所有节点的信息了。直到最后一个节点的p值是NULL,就读不到任何信息了。
可见,这种数据结构相对于数组来说要灵活很多。不用realloc频繁的申请大块空间。而且每个节点的p成员不同的指法就会有不同的结构,真是变幻无穷,非常巧妙和精美。
2、把图书的案例改成链表的形式:
#include <stdio.h>
#include <stdlib.h>
struct Book
{
char title[128];
char author[40];
struct Book *next;
};
void getInput(struct Book *book);
void addBook(struct Book **library);
void printLibrary(struct Book *library);
void releaseLibrary(struct Book **library);
void getInput(struct Book *book)
{
printf("请输入书名:");
scanf("%s", book->title);
printf("请输入作者:");
scanf("%s", book->author);
}
void addBook(struct Book **library)
{
struct Book *book, *temp;
book = (struct Book *)malloc(sizeof(struct Book));
if(book == NULL){
printf("内存分配失败!\n");
exit(1);
}
getInput(book);
if(*library != NULL){
temp = *library;
*library = book;
book->next=temp;
}
else{
*library = book;
book->next=NULL;
}
}
void printLibrary(struct Book *library)
{
struct Book *book;
int count =1;
book = library;
while (book != NULL){
printf("你录入的第 %d 本书的书名是:%s,作者是%s\n", count, book->title, book->author);
book = book->next;
count++;
}
}
void releaseLibrary(struct Book **library)
{
struct Book *temp;
while(*library != NULL){
temp = *library;
*library = (*library)->next;
free(temp);
}
}
int main(void)
{
struct Book *library = NULL;
int ch;
while(1){
printf("是否要录入图书信息(y/n):");
do{
ch = getchar();
}while(ch != 'y' && ch !='n');
if(ch == 'y'){
addBook(&library);
}
else{
break;
}
}
printf("是否需要打印图书信息(y/n):");
do{
ch = getchar();
}while(ch != 'y' && ch != 'n');
if(ch == 'y'){
printLibrary(library);
}
releaseLibrary(&library);
return 0;
}
从上面例子中可以看到:我们最后录入的书籍是头指针header的值;我们最后录入的书籍是第一个节点;我们最后录入的书籍中的p成员的值是倒数第二个录入的书籍的地址;倒数第二个录入的书籍是第二个节点;倒数第二个录入的数据中的p成员是倒数第三个录入的书籍的地址。由于我们只录入了3本书籍,所以倒数第三个录入的书籍就是我们第一个录入的书籍。所以,我们打印的时候是倒序打印出了的。
如果有的同学没有看懂,下面我再写一个简版的:
#include <stdio.h>
#include <stdlib.h>
struct Book
{
char a[128];
char b[64];
struct Book *p;
};
void addbook(struct Book **header);
void printbook(struct Book *header);
void freebook(struct Book *header);
int main()
{
struct Book *header=NULL;
int flag;
while(1){
printf("要录入图书信息吗?(y/n)");
do{
flag = getchar();
}while(flag != 'y' && flag!= 'n');
if(flag == 'y'){
addbook(&header);
}
else{
break;
}
}
printf("是否要打印您刚才录入的书籍信息?(y/n)");
do{
flag = getchar();
}while(flag !='y' && flag!='n');
if(flag == 'y'){
printbook(header);
}
freebook(header);
return 0;
}
void addbook(struct Book **header){
struct Book *book, *temp;
book = (struct Book *)malloc(sizeof(struct Book));
printf("请输入书名:");
scanf("%s", book->a);
printf("请输入作者名:");
scanf("%s", book->b);
if(*header == NULL){
book->p = NULL;
*header = book;
}
else{
temp = *header;
book->p = temp;
*header = book;
}
}
void printbook(struct Book *header){
struct Book *temp;
int count = 1;
temp = header;
while(temp != NULL){
printf("您录入的第%d本书,书名是:%s, 作者是:%s\n", count, temp->a, temp->b);
temp = temp->p;
count++;
}
}
void freebook(struct Book *header){
struct Book *temp;
while(header != NULL){
temp = header;
header = header->p;
free(temp);
}
}
这种把后面的节点都放到最前面的做法叫头查法。下面在简版的基础上演示尾插法:
有没有人发现尾插法要遍历所有的节点才能找到最后一个节点,是不是效率太低了,如果有100万本书,尾插法是不是就很尴尬了。但解决方法也不是没有的,只要我们再定义一个指针,这个指针永远指向链表的尾部节点,不就轻松解决了吗。下面代码示例:
当然这只是我自己的写法,也可以不用改变main函数,只改变addbook函数:在addbook函数里面添加一个静态变量即可:
此时tail变量就变成了一个全局变量,每次调用addbook函数的时候,它都不会被初始化。
3、搜索单链表
比如当用户输入书名或作者名的时候,就要对单链表进行搜索操作,搜索也就是遍历各个节点,然后找到相关的节点数据并打印出了。下面代码示例:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
struct Book
{
char a[128];
char b[64];
struct Book *p;
};
void addbook(struct Book **header);
void printbook(struct Book *header);
void freebook(struct Book *header);
void searchbook(struct Book *header, char *input);
int main()
{
struct Book *header=NULL;
int flag;
char input[128];
while(1){
printf("要录入图书信息吗?(y/n)");
do{
flag = getchar();
}while(flag != 'y' && flag!= 'n');
if(flag == 'y'){
addbook(&header);
}
else{
break;
}
}
printf("是否要打印您刚才录入的书籍信息?(y/n)");
do{
flag = getchar();
}while(flag !='y' && flag!='n');
if(flag == 'y'){
printbook(header);
}
printf("\n请输入你要搜索的书名或作者名:");
scanf("%s", input);
searchbook(header, input);
freebook(header);
return 0;
}
void printbook(struct Book *header){
struct Book *temp;
int count = 1;
temp = header;
while(temp != NULL){
printf("您录入的第%d本书,书名是:%s, 作者是:%s\n", count, temp->a, temp->b);
temp = temp->p;
count++;
}
}
void freebook(struct Book *header){
struct Book *temp;
while(header != NULL){
temp = header;
header = header->p;
free(temp);
}
}
void addbook(struct Book **header){
struct Book *book;
static struct Book *tail;
book = (struct Book *)malloc(sizeof(struct Book));
printf("请输入书名:");
scanf("%s", book->a);
printf("请输入作者:");
scanf("%s", book->b);
if(*header == NULL){
book->p = NULL;
*header = book;
tail = book;
}
else{
book->p = NULL;
tail->p = book;
tail = book;
}
}
void searchbook(struct Book *header, char *input){
struct Book *temp;
int counter = 0;
temp = header;
while(temp != NULL){
if(strcmp(temp->a, input) && strcmp(temp->b, input)){
temp = temp->p;
}
else{
printf("书名:%s, 作者:%s\n", temp->a, temp->b);
temp = temp->p;
counter++;
}
}
if(counter == 0){
printf("没有找到相关的书籍\n\n");
}
}
4、单链表的插入
相对数组来说,单链表的节点插入就非常简单方便了。下面代码示例:节点信息域是一个整型数字的节点,用链表插入的方法把这些整型数字插入链表,并且数据是从小到大排列起来的。
#include <stdio.h>
#include <stdlib.h>
struct Node
{
int a;
struct Node *p;
};
void insertnode(struct Node **header, struct Node **tail, int input);
void printnode(struct Node *header, struct Node *tail);
void freenode(struct Node *header);
int main(void)
{
struct Node *header = NULL;
struct Node *tail = NULL;
struct Node *temp;
int input;
printf("请输入一个正整数,输入-1表示结束:");
scanf("%d", &input);
getchar();
if(input == -1){
exit(0);
}
else{
temp = (struct Node *)malloc(sizeof(struct Node));
temp->a = input;
temp->p = NULL;
header = temp;
tail = temp;
}
printf("%d--\n", header->a);
printf("请输入一个正整数,输入-1表示结束:");
scanf("%d", &input);
getchar();
if(input == -1){
exit(0);
}
else{
temp = (struct Node *)malloc(sizeof(struct Node));
temp->a = input;
if(temp->a > header->a){
temp->p = NULL;
header->p = temp;
tail = temp;
}
else{
temp->p = header;
tail = header;
header = temp;
}
}
printf("%d--%d--\n", header->a, tail->a);
while(1){
printf("请输入一个正整数,输入-1表示结束:");
scanf("%d", &input);
if(input == -1){
exit(0);
}
else{
insertnode(&header, &tail, input);
printnode(header, tail);
}
}
freenode(header);
return 0;
}
void insertnode(struct Node **header, struct Node **tail, int input){
struct Node *new, *temp0, *temp1;
new = (struct Node *)malloc(sizeof(struct Node));
new -> a = input;
if(new->a <= (*header)->a){
new->p = *header;
*header = new;
}
else if(new->a >=(*tail)->a){
new->p = NULL;
(*tail)->p = new;
*tail = new;
}
else{
temp0 = *header;
while(new->a > temp0->a){
temp1 = temp0;
temp0 = temp0->p;
}
new->p = temp1->p;
temp1->p = new;
}
}
void printnode(struct Node *header, struct Node *tail){
struct Node *temp;
temp = header;
while(temp->p != tail->p){
printf("%d--",temp->a);
temp = temp->p;
}
printf("%d--\n", tail->a);
}
void freenode(struct Node *header){
struct Node *temp;
while(header != NULL){
temp = header;
header = header->p;
free(temp);
}
free(header);
}
说明:上面的示例的代码是我没有看视频,先自己按照自己的思路写的,由于我也是一枚初学者,所以写得好不好我不知道,也就是正常运行出结果而已,其他方面没有做更多的考虑。所以如果有要参考的同学,就请移驾至小甲鱼的《带你学C带你飞》视频中的单链表3视频,参考资深人士的代码吧。不过我觉得还是挺难的,就这个代码我写了好几个小时,一直报错,来来回回写了好久。中间想放弃好几次,但一想这个需求挺简单的呀,于是又写,写完一运行就报错,也是非常折磨。欣慰的是最后在一点一点的细扣下,终于运行出来了。
还是把小甲鱼的代码也敲出来吧,以后可以做个参照:
5、单链表的删除
延续上面的例子,数字排序完毕后,再删除节点:
#include <stdio.h>
#include <stdlib.h>
struct Node
{
int value;
struct Node *next;
};
void insertNode(struct Node **head, int value){
struct Node *previous;
struct Node *current;
struct Node *new;
current = *head;
previous = NULL;
while(current != NULL && current->value < value){
previous = current;
current = current->next;
}
new = (struct Node *)malloc(sizeof(struct Node));
if(new == NULL){
printf("内存分配失败!");
exit(1);
}
new->value = value;
new->next = current;
if(previous == NULL){
*head = new;
}
else{
previous->next = new;
}
}
void printNode(struct Node *head){
struct Node *current;
current = head;
while(current != NULL){
printf("%d ", current->value);
current = current->next;
}
putchar('\n');
}
void deleteNode(struct Node **head, int value){
struct Node *previous;
struct Node *current;
current = *head;
previous = NULL;
while(current != NULL && current->value != value){
previous = current;
current = current->next;
}
if(current == NULL){
printf("找不到匹配的节点!\n");
return;
}
else{
if(previous == NULL){
*head = current->next;
}
else{
previous->next = current->next;
}
free(current);
}
}
int main(void)
{
struct Node *head = NULL;
int input;
printf("开始测试插入整数。。。\n");
while(1){
printf("请输入一个整数(输入-1表示结束):");
scanf("%d", &input);
if(input == -1){
break;
}
insertNode(&head, input);
printNode(head);
}
printf("开始测试删除整数。。。\n");
while(1){
printf("请输入一个要删除的整数(输入-1表示结束:)");
scanf("%d", &input);
if(input == -1){
break;
}
deleteNode(&head, input);
printNode(head);
}
return 0;
}
6、单链表案例:通讯录管理程序
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
struct Person
{
char name[40];
char phone[20];
struct Person *next;
};
void getInput(struct Person *person);
void printPerson(struct Person *person);
void addPerson(struct Person **contacts);
void changePerson(struct Person *contacts);
void delPerson(struct Person **contacts);
struct Person *findPerson(struct Person *contacts);
void displayContacts(struct Person *contacts);
void releaseContacts(struct Person **contacts);
void getInput(struct Person *person){
printf("请输入姓名:");
scanf("%s", person->name);
printf("请输入电话:");
scanf("%s", person->phone);
}
void addPerson(struct Person **contacts){
struct Person *person;
struct Person *temp;
person = (struct Person *)malloc(sizeof(struct Person));
if(person == NULL){
printf("内存分配失败!\n");
exit(1);
}
getInput(person);
if(*contacts != NULL){
temp = *contacts;
*contacts = person;
person->next = temp;
}
else{
*contacts = person;
person->next = NULL;
}
}
void printPerson(struct Person *person){
printf("联系人:%s\n", person->name);
printf("电话:%s\n", person->phone);
}
struct Person *findPerson(struct Person *contacts){
struct Person *current;
char input[40];
printf("请输入联系人:");
scanf("%s", input);
current = contacts;
while(current!=NULL && strcmp(current->name, input)){
current = current->next;
}
return current;
}
void changePerson(struct Person *contacts){
struct Person *person;
person = findPerson(contacts);
if(person == NULL){
printf("找不到该联系人!\n");
}
else{
printf("请输入新的联系电话:");
scanf("%s", person->phone);
}
}
void delPerson(struct Person **contacts){
struct Person *temp;
struct Person *person;
struct Person *current;
struct Person *previous;
person = findPerson(*contacts);
if(person == NULL){
printf("找不到该联系人!\n");
}
else{
current = *contacts;
previous = NULL;
while(current != NULL && current != person){
previous = current;
current = current->next;
}
if(previous == NULL){
*contacts = current->next;
}
else{
previous->next = current->next;
}
free(person);
}
}
void displayContacts(struct Person *contacts){
struct Person *current;
current = contacts;
while(current != NULL){
printPerson(current);
current = current->next;
}
}
void releaseContacts(struct Person **contacts){
struct Person *temp;
while(*contacts != NULL){
temp = *contacts;
*contacts = (*contacts)->next;
free(temp);
}
}
int main(void)
{
int code;
struct Person *contacts = NULL;
struct Person *person;
printf("| 欢迎使用通讯录管理程序 |\n");
printf("|---1:插入新的联系人 ---|\n");
printf("|---2:查找已有的联系人--|\n");
printf("|---3:更改已有联系人----|\n");
printf("|---4:删除已有联系人----|\n");
printf("|---5:显示当前通讯录----|\n");
printf("|---6:退出通讯录程序----|\n");
while(1){
printf("\n请输入指令代码:");
scanf("%d", &code);
switch(code){
case 1: addPerson(&contacts);break;
case 2: person = findPerson(contacts);
if(person == NULL){
printf("找不到该联系人!\n");
}
else{
printPerson(person);
}
break;
case 3: changePerson(contacts);break;
case 4: delPerson(&contacts);break;
case 5: displayContacts(contacts);break;
case 6: goto END;
}
}
END:
releaseContacts(&contacts);
return 0;
}
7、单链表案例:给通讯录程序加一个内存池管理
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#define MAX 1024
struct Person
{
char name[40];
char phone[20];
struct Person *next;
};
struct Person *pool = NULL;
int count;
void getInput(struct Person *person);
void printPerson(struct Person *person);
void addPerson(struct Person **contacts);
void changePerson(struct Person *contacts);
void delPerson(struct Person **contacts);
struct Person *findPerson(struct Person *contacts);
void displayContacts(struct Person *contacts);
void releaseContacts(struct Person **contacts);
void releasePool(void);
void getInput(struct Person *person){
printf("请输入姓名:");
scanf("%s", person->name);
printf("请输入电话:");
scanf("%s", person->phone);
}
void addPerson(struct Person **contacts){
struct Person *person;
struct Person *temp;
if(pool != NULL){
person = pool;
pool = pool->next;
count--;
}
else{
person = (struct Person *)malloc(sizeof(struct Person));
if(person == NULL){
printf("内存分配失败!\n");
exit(1);
}
}
getInput(person);
if(*contacts != NULL){
temp = *contacts;
*contacts = person;
person->next = temp;
}
else{
*contacts = person;
person->next = NULL;
}
}
void printPerson(struct Person *person){
printf("联系人:%s\n", person->name);
printf("电话:%s\n", person->phone);
}
struct Person *findPerson(struct Person *contacts){
struct Person *current;
char input[40];
printf("请输入联系人:");
scanf("%s", input);
current = contacts;
while(current!=NULL && strcmp(current->name, input)){
current = current->next;
}
return current;
}
void changePerson(struct Person *contacts){
struct Person *person;
person = findPerson(contacts);
if(person == NULL){
printf("找不到该联系人!\n");
}
else{
printf("请输入新的联系电话:");
scanf("%s", person->phone);
}
}
void delPerson(struct Person **contacts){
struct Person *temp;
struct Person *person;
struct Person *current;
struct Person *previous;
person = findPerson(*contacts);
if(person == NULL){
printf("找不到该联系人!\n");
}
else{
current = *contacts;
previous = NULL;
while(current != NULL && current != person){
previous = current;
current = current->next;
}
if(previous == NULL){
*contacts = current->next;
}
else{
previous->next = current->next;
}
if(count < MAX){
if(pool != NULL){
temp = pool;
pool = person;
person->next = temp;
}
else{
pool = person;
person->next = NULL;
}
count++;
}
else{
free(person);
}
}
}
void displayContacts(struct Person *contacts){
struct Person *current;
current = contacts;
while(current != NULL){
printPerson(current);
current = current->next;
}
}
void releaseContacts(struct Person **contacts){
struct Person *temp;
while(*contacts != NULL){
temp = *contacts;
*contacts = (*contacts)->next;
free(temp);
}
}
void releasePool(void){
struct Person *temp;
while(pool != NULL){
temp = pool;
pool = pool->next;
free(temp);
}
}
int main(void)
{
int code;
struct Person *contacts = NULL;
struct Person *person;
printf("| 欢迎使用通讯录管理程序 |\n");
printf("|---1:插入新的联系人 ---|\n");
printf("|---2:查找已有的联系人--|\n");
printf("|---3:更改已有联系人----|\n");
printf("|---4:删除已有联系人----|\n");
printf("|---5:显示当前通讯录----|\n");
printf("|---6:退出通讯录程序----|\n");
while(1){
printf("\n请输入指令代码:");
scanf("%d", &code);
switch(code){
case 1: addPerson(&contacts);break;
case 2: person = findPerson(contacts);
if(person == NULL){
printf("找不到该联系人!\n");
}
else{
printPerson(person);
}
break;
case 3: changePerson(contacts);break;
case 4: delPerson(&contacts);break;
case 5: displayContacts(contacts);break;
case 6: goto END;
}
}
END:
releaseContacts(&contacts);
releasePool();
return 0;
}
下图是我自己写的,实际写的过程中,发现还是有很多要考虑的。而且写得也不完美。