链表产生的原因:
对于比较初级的编程学习者,在存储数据时,比较熟悉的是利用数组进行存储。
但是静态数组需要在存储时指定需要存储的数量,这在生产生活中不具有现实意义,因此,可以使用动态数组的方法(malloc函数)申请内存,这是可以的。
但是使用动态数组存在以下几个问题:
1. 当数据达到一定量,需要重新申请内存,然后将原来的数据复制到新的内存空间中,数据的复制需要时间,然后还需要将原先的数据释放;
2. 内存中会存在这样一种情况:明明还有内存剩余,但是由于新申请的内存空间大于剩余内存空间,无法申请成功,因此内存空间不能得到有效使用。
因此动态数组也具有一定的局限性。基于此,人们想出链表的方法,解决数据存储和内存之间的问题。
链表存储数据的原理:
链表的存储原理如下:
首先需要定义一个链表的结点:包含本结点的数据和指向下一结点的指针。类似于“数据+引线”,数据可以存储该节点的数据,而引线可以指向下一结点,本节点的数据也可以被上一引线所指。
链表的c实现:
下面的代码展示链表的c实现:
首先需要定义链表的node,即结点。根据链表的原理,应该包含一个值(value)和指向下一个结点的指针(*next),如下面的程序,定义了用户自定义的结构体,包含以上两个部分。这里,将节点的定义写在了.h的头文件中。
#ifndef _NODE_H
#define _NODE_H
typedef struct _node{
int value;
struct _node *next;
}Node;
#endif
结点定义好之后,就可以存储数据了。首先需要将头结点初始化
Node *head=NULL;
然后将用户输入的数据存储到链表中,这里我们假设一直存储用户输入的数据,直到用户输入-1,停止存储数据。因此程序伪代码可以写成如下形式:
do{
scanf("%d",number);
if(number!=-1){
//将用户输入数据存储到链表中
}while(number!=-1);
下面重点的就是这句注释了:
//将用户输入数据存储到链表中
我们想一想,将数据存入链表好像存在两个无法回避的问题:什么时候可以将数据存在里面,以及如何将数据存入里面。
第一个问题至关重要,因为这可能导致后面存储数据的方式不同(例如,只有头结点的情况下和在头结点后面的结点来存储数据还不太一样)。另外,数据也不是一直都可以存进去的,比如这里有-1输入的限制。即使没有这个限制,也应该加一个如果申请不到内存了,这种情况的处理办法,是警告用户无法再进行存储还是舍去最前面的数据,存储后来的数据。
根据上面的分析,可以继续精化上面的伪代码:
对于问题1:什么时候可以将数据存在里面
//若是头结点,就直接将数据存储到头结点,否则,需要存储到尾结点
对于问题2:如何将数据存入里面
//需要新申请结点大小的内存空间,然后将数据存到里面,在将原来最后的结点指针指向新的结点。
精化之后,可以形成以下的思路:先将数据存入链表的结点(申请内存,存储数据),再将链表尾结点的指针指向新存入数据的结点的数据,而新存入数据的指针指向null。这样就可以完成一个结点连接到链表尾部的过程。
先来看将数据存入链表的结点
这里主要有两个步骤:申请结点内存,存储结点数据。
申请结点内存时需要知道结点内存的大小,因此需要malloc(size(Node)),申请完之后需要类型转化成结点类型,因此代码如下:
Node *p=(Node*)malloc(sizeof(Node));
这里申请的结点定义为指针类型的结点p。这里p是该结点的指针,结点p里面包含本结点的数据p->value和指向下一节点指针p->next。根据刚才精化形成的思路,我们将输入的数据赋给p->value,并将p->next指向null。代码如下:
//add to linked-list
Node *p=(Node*)malloc(sizeof(Node));
p->value=number;
p->next=NULL;
这里number在使用之前需要先进行类型定义。
将链表尾结点的指针指向新存入数据的结点的数据
在将原来链表的尾结点指向新结点之前需要首先知道尾结点在哪里,这里需要使用遍历来寻找尾结点。首先,我们需要定义尾结点last,并将头结点head赋给尾结点last,接着需要遍历其他结点。遍历的方法是看尾结点部分指向下一节点的指针是否是null,如果非空,则继续遍历,把last->next赋给last,否则,就找到了尾结点。此时需要将本是null的last->next指向p。
代码如下:
//Find the last
Node *last=head;
while(last->next){
last=last->next;
}
last->next=p;
这里我们默认头结点是存在的,只要头结点存在,无论头结点后面有没有其他结点,尾结点就一定会存在,后面的操作都比较符合常理。但是很可能头结点都不存在,也就是用户输入前不存在这个链表,因此用户输入的数字应该是这个链表的头结点,即head=p。因此在将
Node *last=head;
之后,需要增加一个head或者说last是否存在的判断,存在,则进行上面的遍历;否则,直接将用户输入数据的新结点为头结点。因此完善的代码如下:
//Find the last
Node *last=head;
if(last){
while(last->next){
last=last->next;
}
//attach
last->next=p;
} else{
head=p;
}
这样,就完成了一个链表的创建。完整代码如下:
#include <stdio.h>
#include <stdlib.h>
#include "node.h"
/* run this program using the console pauser or add your own getch, system("pause") or input loop */
int main(int argc, char *argv[]) {
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){
while(last->next){
last=last->next;
}
//attach
last->next=p;
} else{
head=p;
}
}
}while(number!=-1);
return 0;
}
链表的特点:
使用链表结构可以克服数组链表需要预先知道数据大小的缺点,链表结构可以充分利用计算机内存空间,实现灵活的内存动态管理。但是链表失去了数组随机读取的优点,同时链表由于增加了结点的指针域,空间开销比较大。链表最明显的好处就是,常规数组排列关联项目的方式可能不同于这些数据项目在记忆体或磁盘上顺序,数据的存取往往要在不同的排列顺序中转换。链表允许插入和移除表上任意位置上的节点,但是不允许随机存取。链表有很多种不同的类型:单向链表,双向链表以及循环链表。
参考资料:
2、https://baike.baidu.com/item/%E9%93%BE%E8%A1%A8/9794473?fr=aladdin