上篇文章用C语言简单实现了一个可变数组,但是,它有一个缺点:每一次变大时,都要申请一块新的内存空间,可以容纳下全部数据,然后再进行原来空间向新空间数据的拷贝。随着数组增大,数据越来越多,拷贝需要花很多时间。所以这种方法不高效,试想我们可以采用链表的方式:原来的内存不动,如果不够用,这时不是再申请更大一块再进行数据拷贝,而是就申请一个BLOCK大的一块内存,然后把它们链起来,如下:
这样不仅避免了反复的拷贝,而且可以利用内存的每一个角落十分高效。
结构示意图如下:
程序背景:不断读入number,直到读到-1为止,则程序结束。
方便起见,这里逐一对每一个函数进行分开讲解。首先是:添加元素。
这里我们首先可以直接在main函数中按照逻辑写下代码,然后再放入函数中。
list.h文件
#ifndef _NODE_H_
#define _NODE_H_
typedef struct _node
{
int value;
struct _node*next;
}Node;
#endif
list.c文件
#define _CRT_SECURE_NO_WARNINGS 1
#include"list.h"
#include<stdio.h>
#include<stdlib.h>
int main()
{
Node*head = NULL;
int number;
do
{
scanf("%d", &number);
if (number != -1)
{
//add to linked-list
Node*p = (Node*)malloc(sizeof(Node));
p->value = number;
p->next = NULL;
//find the last 遍历,找到最后一个节点,接上去
Node*last = head;
while (last->next)
{
last = last->next;
}
//attach
last->next = p;
}
} while (number != -1);
system("pause");
return 0;
}
但是这个代码是有问题的。开始时,head指向NULL,赋给last指针,这时last也指向空,下面while语句判断last->next明显会出错,空指针的arrow是无效的。所以我们要用last,必须判断last是否为空。所以代码改为-->
int main()
{
Node*head = NULL;
int number;
do
{
scanf("%d", &number);
if (number != -1)
{
//add to linked-list
Node*p = (Node*)malloc(sizeof(Node));
p->value = number;
p->next = NULL;
//find the last
Node*last = head;
if (last)//判断last指针是否为空,不为空才可以进行next的访问
{
while (last->next)
{
last = last->next;
}
//attach
last->next = p;
}
else
{
head = p;
}
}
} while (number != -1);
//打印所有数字
Node*p;
p = head;
while (p)
{
printf("%d ", p->value);
p = p->next;
}
system("pause");
return 0;
}
输入:1 2 3 -1,运行结果:
综上,我们所做的事是:得到一个number后,制造了一个节点,并把它链在链表尾部。如果设计成函数,那么应该怎么写?把上面在main函数中的代码截取下来,封装在add函数中。得到如下
void add(Node*head, int number);
int main()
{
Node*head = NULL;
int number;
do
{
scanf("%d", &number);
if (number != -1)
{
add(head, number);
}
} while (number != -1);
//打印所有数字
Node*p;
p = head;
while (p)
{
printf("%d ", p->value);
p = p->next;
}
system("pause");
return 0;
}
void add(Node*head, int number)
{
//add to linked-list
Node*p = (Node*)malloc(sizeof(Node));
p->value = number;
p->next = NULL;
//find the last
Node*last = head;
if (last)
{
while (last->next)
{
last = last->next;
}
//attach
last->next = p;
}
else
{
head = p;
}
}
输入:1 2 3 -1,输出结果,这里并没有打印出1,2,3。说明程序出了问题。函数中的参数传入head,并且在函数中修改了head,但是main函数中的Node*head = NULL;,这个head并没有被修改,所以每次出了add的函数作用域,head仍然为NULL,这就导致每次加入新节点,head都是指向新节点的,所以一连串的节点并没有被链起来,这就导致我们遍历时,程序没有输出每个节点的value值。图解如下-->
这里有几种方案可以解决这个问题,下面分别对这几种方案做个归纳,对比。
方案一:add函数返回一个Node*指针,函数结束时,把head传出去,在调用函数的地方head=add(head,number)接收。
//main函数中调用add函数的部分用head来接收返回值
head=add(head, number);
//add函数返回值不再是void,而是返回Node*的指针
Node* add(Node*head, int number)
{
//add to linked-list
Node*p = (Node*)malloc(sizeof(Node));
p->value = number;
p->next = NULL;
//find the last
Node*last = head;
if (last)
{
while (last->next)
{
last = last->next;
}
//attach
last->next = p;
}
else
{
head = p;
}
return head;
}
输入:1 2 3 -1,输出结果,程序可以正常遍历链表,打印出每个节点的值。但是这样做还是有个小缺点:需要使用add函数的程序员必须记得在调用add函数时,用head把返回值接收过来;如果忘了,那么对空链表的add就是错的。
方案二:add函数中不传head,而是传入head的指针(即head=add(&head, number);)那么add函数就要改为Node* add(Node**head, int number),这样的话最后要不要有返回值就无所谓了,因为传入的是指向指针的指针,这就使我们能够在函数内部对指针的值做修改。可以对比swap交换两个数的场景。
//add函数声明
void add(Node**phead, int number);
int main()
{
Node*head = NULL;
int number;
do
{
scanf("%d", &number);
if (number != -1)
{
add(&head, number);//传入head指针的地址
}
} while (number != -1);
//打印所有数字
Node*p;
p = head;
while (p)
{
printf("%d ", p->value);
p = p->next;
}
system("pause");
return 0;
}
//传入指向head的指针
void add(Node**phead, int number)
{
//add to linked-list
Node*p = (Node*)malloc(sizeof(Node));
p->value = number;
p->next = NULL;
//find the last
Node*last = *phead;
if (last)
{
while (last->next)
{
last = last->next;
}
//attach
last->next = p;
}
else
{
*phead = p;
}
}
输入:1 2 3 -1,输出结果,程序可以正常遍历链表,打印出每个节点的值。
方案三:再定义一个结构体List,成员是Node*head,,add函数传入List*的一个指针。
//再定义一个结构体
typedef struct _list
{
Node*head;
}List;
//函数声明
void add(List*plist, int number);
int main()
{
List list;
list.head = NULL;
int number;
do
{
scanf("%d", &number);
if (number != -1)
{
add(&list, number);
}
} while (number != -1);
//打印所有数字
Node*p;
p = list.head;
while (p)
{
printf("%d ", p->value);
p = p->next;
}
system("pause");
return 0;
}
void add(List*plist, int number)
{
//add to linked-list
Node*p = (Node*)malloc(sizeof(Node));
p->value = number;
p->next = NULL;
//find the last
Node*last = plist->head;
if (last)
{
while (last->next)
{
last = last->next;
}
//attach
last->next = p;
}
else
{
plist->head = p;
}
}
输入:1 2 3 -1,输出结果
表面上看起来这种结构(传list指针)与上一种方法(传head)指针是一回事,本质都一样,但是这种方法的好处在于:我们定义了一种自己定义的数据结构List来代表整个链表,现在在这个List结构中只放了一个head,但是以后可以有各种扩充:比如上述代码中,每次链新节点的时候,都要用last指针遍历一遍前面整个链表,找到当前的最后一个节点位置,链上去,如果可以有一个tail指针,记录当前最后一个节点位置,那么就不用每次加入新节点的时候遍历链表找最后一个节点位置了,直接链在tail指针所指的节点后就可以。结构设计为-->
typedef struct _list
{
Node*head;
Node*tail;
}List;
总结起来,这种设计结构的好处就是:便于将来改进List。如果不这样设计,就只是一个悬在外面的head。对于一个工程化的程序来说,合理安排结构,是很有必要的,不单单只是学会基本的“增删查改”。所以这种思想也值得我们学习。
打印函数:
void print(List *plist);
int main()
{
List list;
list.head = NULL;
int number;
do
{
scanf("%d", &number);
if (number != -1)
{
//增加新元素
add(&list, number);
}
} while (number != -1);
//调用打印函数
print(&list);
system("pause");
return 0;
}
//打印函数->第一种写法用while循环
void print(List *plist)
{
Node*p;
p = plist->head;
while (p)
{
printf("%d ", p->value);
p = p->next;
}
}
//打印函数->第二种写法用for循环
void print(List *plist)
{
Node*p;
for (p = plist->head; p; p=p->next)
{
printf("%d ", p->value);
}
printf("\n");
}
查找函数:
void search(List *plist, int number)
{
int isfound = 0;
Node*p;
p = plist->head;
while (p)
{
if (p->value == number)
{
printf("找到了\n");
isfound = 1;
break;
}
p = p->next;
}
if (!isfound)
{
printf("没找到\n");
}
}
删除:
删除节点的时候,比如删除p节点。第一件事:首先要找到p前面的那个节点,改变它的指针指向,让它指向p后面的那个节点;第二件事:free(p)。问题就在于P前面的那个节点是什么?如何找到?
由于是单向链表。只有指向下一个节点的指针,并没有指向前一个节点的指针,所以得循环遍历找到前一个节点。若有另一个指针q记录前一个节点,则只需要改变q->next的指针指向,让它指向p->next;然后free(p)就可以了。图示如下:
代码:
//删除一个节点
void remove(List *plist, int number)
{
Node*p;
Node*q;
for (q = NULL, p = plist->head; p; q = p, p = p->next)
{
if (p->value == number)
{
if (q)//这里一定要主要判断q,如果删的是第一个节点,则循环一进来q就是NULL,不能进行q->next;而应该让list.head = p->next;
{
q->next = p->next;
}
else
{
plist->head = p->next;
}
free(p);
break;
}
}
}
注:这里删除节点的函数只考虑了number与value相等的情况(即对应节点存在),还会有删除一个不存在的节点情况,完整的函数设计应该先调用search函数,利用search函数的返回值判断对应节点是否存在,如果存在再进行删除。由于本节只是对链表的一个初探索,主要熟悉结构的定义与设计,所以这里就没有考虑那么完善;包括search函数,也是没有设计返回值的。
清除整个链表
void _delete(List *plist)
{
Node*p;
Node*q;
for (p = plist->head; p; p = q)
{
q = p->next;
free(p);
}
plist->head = NULL;//全部节点删除后,一定记得把head节点置空
}
全部代码如下:
list.h文件
#ifndef _NODE_H_
#define _NODE_H_
typedef struct _node
{
int value;
struct _node*next;
}Node;
#endif
list.cpp文件
#define _CRT_SECURE_NO_WARNINGS 1
#include"list.h"
#include<stdio.h>
#include<stdlib.h>
//typedef struct _node
//{
// int number;
// struct _node*next;
//}Node;
typedef struct _list
{
Node*head;
}List;
//Node* add(Node*head, int number);
//void add(Node**phead, int number);
void add(List*plist, int number);
//Node* add(Node*head, int number);
void print(List *plist);
void search(List *plist, int number);
void remove(List *plist, int number);
void _delete(List *plist);
int main()
{
//Node*head = NULL;
List list;
list.head = NULL;
int number;
do
{
scanf("%d", &number);
if (number != -1)
{
//add(&head, number);
add(&list, number);
}
} while (number != -1);
//打印所有数字
/*Node*p;
p = list.head;
while (p)
{
printf("%d ", p->value);
p = p->next;
}*/
print(&list);
//scanf("%d", &number);
//search(&list, number);
//Node*p;
//Node*q;
//for (q = NULL, p = list.head; p; q = p, p = p->next)
//{
// if (p->value == number)
// {
// if (q)//这里一定要主要判断q,如果删的是第一个节点,则循环一进来q就是NULL,不能进行q->next;而应该让list.head = p->next;
// {
// q->next = p->next;
// }
// else
// {
// list.head = p->next;
// }
// free(p);
// break;
// }
//}
//remove(&list, number);
//print(&list);
//删除整个链表
/*for (p = head; p; p = q)
{
q = p->next;
free(p);
}*/
_delete(&list);
print(&list);
system("pause");
return 0;
}
void add(List*plist, int number)
//void add(Node**phead, int number)
{
//add to linked-list
Node*p = (Node*)malloc(sizeof(Node));
p->value = number;
p->next = NULL;
//find the last
Node*last = plist->head;
if (last)
{
while (last->next)
{
last = last->next;
}
//attach
last->next = p;
}
else
{
//*phead = p;
plist->head = p;
//head = p;
}
}
//void print(List *plist)
//{
// Node*p;
// p = plist->head;
// while (p)
// {
// printf("%d ", p->value);
// p = p->next;
// }
//}
void print(List *plist)
{
Node*p;
for (p = plist->head; p; p=p->next)
{
printf("%d ", p->value);
}
printf("\n");
}
void search(List *plist, int number)
{
int isfound = 0;
Node*p;
p = plist->head;
while (p)
{
if (p->value == number)
{
printf("找到了\n");
isfound = 1;
break;
}
p = p->next;
}
if (!isfound)
{
printf("没找到\n");
}
}
void remove(List *plist, int number)
{
Node*p;
Node*q;
for (q = NULL, p = plist->head; p; q = p, p = p->next)
{
if (p->value == number)
{
if (q)//这里一定要主要判断q,如果删的是第一个节点,则循环一进来q就是NULL,不能进行q->next;而应该让list.head = p->next;
{
q->next = p->next;
}
else
{
plist->head = p->next;
}
free(p);
break;
}
}
}
void _delete(List *plist)
{
Node*p;
Node*q;
for (p = plist->head; p; p = q)
{
q = p->next;
free(p);
}
plist->head = NULL;//全部节点删除后,一定记得把head节点置空
}