第三篇周报记录——链表
从一开始的无头苍蝇,到现在渐渐有了一点头绪,在此记录一下成长过程吧。
首先,刚刚接触到一个东西时我们都会想问,它是什么,咋弄出来的,干嘛的。这几个问题真的困扰了我好久
网上和资料给出的是这些:
链表是一种常见的基础数据结构,结构体指针在这里得到了充分的利用。链表可以动态的进行存储分配,也就是说,链表是一个功能极为强大的数组,他可以在节点中定义多种数据类型,还可以根据需要随意增添,删除,插入节点。链表都有一个头指针,一般以head来表示,存放的是一个地址。链表中的节点分为两类,头结点和一般节点,头结点是没有数据域的。链表中每个节点都分为两部分,一个数据域,一个是指针域。说到这里你应该就明白了,链表就如同车链子一样,head指向第一个元素:第一个元素又指向第二个元素;……,直到最后一个元素,该元素不再指向其它元素,它称为“表尾”,它的地址部分放个“NULL”(表示“空地址”),链表到此结束。
刚看到这句话的时候我很懵逼,我说说我刚开始看到链表后的疑惑吧。
1.既然是储存数据,那它是如何表示存储?c语言数组可以说成
int s[3][2]={
{1,2},
{4,5},
{7,8},
结构体可以说成
typedef struct student//这里的typedef是类型定义的意思,为了更方便地使用这个结构体
{
int id;
char name[20];
int score[3];
float aver;
}STU;
那链表存储是怎么表示的?(链表是一种物理存储单元上非连续、非顺序的存储结构,数据元素的逻辑顺序是通过链表中的指针链接次序实现的)摘自百度百科
下面我们看看一个静态链表
#include<stdio.h>
struct student
{
long num;
float score;
struct student *next;
};
int main()
{
struct student a,b,c,*head,*p;
a.num=99101;a.score=88;
b.num=99103;b.score=89;
c.num=99107;c.score=85;
head=&a; a.next=&b;
b.next=&c; c.next=NULL;
p=head;
do
{
printf("%d %f\n",p->num,p->score);
p=p->next;
}
while(p!=NULL);
}
静态链表的特点是各节点是在程序中定义出来的,而不是临时开辟的,在程序执行过程中,不可能人为的再产生新的存储单元,也不能认为的使已开辟的存储单元消失,也就是说,它的节点就这么多,意义上像个被完整定义了的数组一样。
但我想这个链表就应该可以很明显地看出链表是以什么形式表现出来的了——结构体。
好了,我们再看看这样一串代码
#include <stdio.h>
#include "node.h"
#include "stdlib.h"
#include "string.h"
//typedef struct _node {
// int value;
// struct _node *next;//不能用Node,因为第六行Node还没出现
//}Node;
int main()
{
Node * head =NULL;
printf("head=%d,&head=%d\n",head,&head);
int number;
do{
scanf("%d",&number);
if(number!=-1){//每读一个数都要创造一个node的结构体
Node *p = (Node*)malloc(sizeof(Node));//malloc是动态内存分配,分配成功则返回指向被分配内存空间的指针
p->value=number;//将输入数据存入数据域
p->next=NULL;//p指针指向的对象的next属性为空,新来的那个node它的next指针为NULL
//find the last
Node *last=head;//让last从头开始直到变成真正的尾 ,其实就是NULL
if(last){//如果last不是NULL的话才开始遍历,防止一开始就是NULL
while(last->next){//遍历,last还能指向值的时候继续,直到指向NULL
last=last->next;
printf("last=%d,&last=%d,*last=%d\n",last,&last,*last);
}
last->next=p;//将新获得的地址给下一个结点
}else{
head=p;//运行时发现输入-1无事发生,因为p依然指向自己的head,被关在自己的结构体中
//printf("p=%d,*p=%d",p,*p);
}
}
}while(number!=-1);
return 0;
}
这是一个动态链表,这是我看慕课时跟着敲的,当时我大概了解了链表的形式,因为数据(住户)是储存在相应的地址(房子)中,那么让一个地址指向另一个地址,相当于在住户之间搭建了一个长廊,所以这个长廊我们可以称之它为——节点。
那么这个长廊的比喻意味着什么呢?
提现了优点,链表不需要连续的存储单元,增加数据和删除数据很容易,来个人可以随便住,比如来了个人要做到第三个房子,那他只需要把自己的住址告诉第二个人,然后问第二个人拿到原来第三个人的住址行了,其他人都不用动,修改链表的复杂度为O(1) (在不考虑查找时)。
也体现了缺点,无法直接找到指定节点,只能从头节点一步一步寻找,就像你必须按着长廊的顺序走到想去的房子,复杂度为O(n)。
让我们从比喻回归到上面的例程中
咦,我们先发现了一个包含了结构体的头函数
原来是用结构体来实现链表啊。结构体相当于一种数据类型。链表是数据结构的一种,可以用结构体来实现链表。
typedef struct _node {
int value;//(数据域)
struct _node *next;//(指针域)不能用Node,因为第六行Node还没出现
}Node;
首先创建了一个名为Node(节点)的结构体,这个节点包含一个value,一个叫next的结构体指针,我们把这样结构体分成两个部分——指针域和数据域
数据域,顾名思义,就是存放当前节点数据的地方,是房子里面的住户。
指针域,顾名思义,就是存一个指针用来指向下一个结点的位置,就像我们所比喻的那个走廊一样。指针一般称为next,用来指向下一个结点的位置。由于下一个结点也是链表类型,所以next的指针也要定义为链表类型。
那么应该很明显了,所谓链表,就是由一个个节点保存数据,每个节点都有自己的指针以表示自己的地址,链表遍历前我们一定是从头指针开始,不停地用一个next指针(这里我们习惯性用next形象地表示下一个指针)去一个个链性访问这一整条单链表。next就像一扇门一样,它告诉我们——打开了我,那就要通向下一个结构体咯所以我们不断地用next来遍历整个链表,这个很像数组里面的下标不断增加,只不过next通往的不是“隔壁”,而是下一个“目标”(结构体)。
还有需要记住的事情是,动态链表要用到三个函数,它们是malloc(),calloc(),free(),具体使用方法我们在后面的例程中带着讲。
我是在这样一个例程里面真正明白链表是干什么的
#include <stdio.h>
#include <stdlib.h>
typedef int Elemtype;//定义了一个数据类型
typedef struct linklist
{
Elemtype data;
struct linklist *next;
}list,*link_list;
link_list create_linklist(link_list l, int n)//创建单链表
{
link_list p,q;//q相当于是一个游标,是为了形成链表而设
int x;
l=(list*)malloc(sizeof(list));//创建单链表头结点
l->next=NULL; //其实NULL和0是一样的,只不过习惯上用NULL用于指针和对象,0用于数值
//l->data=10;//test
q=l;//给头结点一个备份
//printf("%d,%d",l->data,q->data);//test
printf("please input %dx:\n",n);
for(int i=1;i<=n;i++)
{
p=(list*)malloc(sizeof(list));//产生一个新结点
scanf("%d",&x);
p->data=x; //把输入的值存入数据域
p->next=NULL;
q->next=p;
q=q->next;//总是保证q指向最后一个节点,就能在循环中创建新节点时,让新节点能和前面一个节点形成链的关系
}
return l;
}
void display(link_list l)//显示链表
{
link_list p;
p=l->next;
while (p)
{
//printf("%d",p);
printf("%d->",p->data);
p=p->next;
}
printf("\n");
}
int main()
{
link_list x,y;
int a=4;
y=create_linklist(x, a);
printf("please output linklist :\n");
display(y);
return 0;
}
这是一个让人明确何为链表的单向链表,它的输出效果如下
我们根据结果可以看出,这个结果很形象地表明了链表的形式,一个值指向下一个只,他们(数据)被指针串了起来。
让我先看看简单链表是如何被拉长的:
for(int i=1;i<=n;i++)
{
p=(list*)malloc(sizeof(list));//产生一个新结点
scanf("%d",&x);
p->data=x; //把输入的值存入数据域
p->next=NULL;
q->next=p;
q=q->next;
}
malloc()是<stdlib.h>里面的一个函数用于申请一块连续的指定大小的内存块区域以void*类型返回分配的内存区域地址,当无法知道内存具体位置的时候,想要绑定真正的内存空间,就需要用到malloc进行动态的分配内存。我们知道链表的好处在于它可以“见缝插针”,见缝插针找malloc()。
我们可以发现这里我们给新找到的储存空间(大小为list那么大,因为用sizeof函数取了大小)把地址给了结构体指针p,p->data=x很形象地把刚刚输入的x数值给了指针p指向的结构体中的数据域data,然后我们把p->next给初始化,并将新节点的地址传给我们的新结构体指针域q->next中。
我加入了一些printf函数以让我们能得到更好的理解,如下
for(int i=1;i<=n;i++)
{
p=(list*)malloc(sizeof(list));//产生一个新结点
scanf("%d",&x);
p->data=x; //把输入的值存入数据域
p->next=NULL;
printf("p->next=%d,(*p).next=%d\n",p->next,(*p).next);
q->next=p;//q相当于是一个游标,是为了形成链表而设
printf("p=%d\n",p);
printf("q->next=%d\n",q->next);
q=q->next;//总是保证q指向最后一个节点,就能在循环中创建新节点时,让新节点能和前面一个节点形成链的关系
printf("q=%d\n",q);
}
相信这样就能很直观地明白我们的链表是如何创建的了,找空间,存空间,为下一个节点初始化一个指针,
我们再在输出代码中动一点手脚
//printf("%d",p);
printf("%d->",p->data);
p=p->next;//总是保证q指向最后一个节点,就能在循环中创建新节点时,让新节点能和前面一个节点形成链的关系
printf("(p=%d)",p);
}
printf("\n");
得到了上面这个结果
于是我为这个特征做了一个比喻(没有那么贴切,但也可以帮助理解)——我们在一个结构体中创造并找到了下一个结构体,而我们要想找到小套娃,就必须先找到最大的那个套娃并打开它,这就是一个链表的遍历过程。(这个比喻用来形容这个程序的查找的,不是所有的链表操作都适用与这个比喻)
我转载了其它博客上的一副图(很多书上都有这种图)
link_list p作为一个特殊的指针,指向另一个结构,而这个结构同样拥有一个指向其他结构的指针
于是我们在这种重复构造中得到了一个完整的链表
然后链表的基本操作其实也就是围绕着节点的寻找与指针的修改来进行的,
切记,我们操作的对象是节点的指针,就像上面涉及到的p,q等等,而不是节点本身,他们是指针而非节点。