18.11.14
链式思想 -> 顺藤摸瓜
(初学者食用)
之前的合并线性表是用顺序存储结构做的,这里链式存储结构是线性表的另一种构建方式。
总的来说, 这种线性表由一个头结点(通常是有的)和很多个结点(每个结点由数据域‘data’和指针域‘next’,也就是一个存数据的地方和一个存指针的地方)组成。
有头结点是为了增强可读性,它的好处是我们对第一个结点做插入和删除时不需要做特殊处理。(调用函数的时候通常是把头结点的地址交给函数)
线性链表有几个很重要的特点:
**1.查找元素必须从头指针开始找
2.做插入删除不需要移动元素,时间复杂度O(1)
3.存储空间不连续,结点之间指针相连**
废话不多说,开始操练吧
开始的开始,先对结点的结构体进行封装,以下都是用C语言实现的(只会C语言)
头文件以及结点结构体设定:
#include<stdio.h>/*该文件包含printf()等函数*/
#include<stdlib.h>//包含了exit()等函数
#include<malloc.h>//包含了malloc(),free()等函数
typedef int ElemType;//定义ElemType 为 int
typedef struct node{//结点包含数据域和指针域
ElemType data;//数据设为ElemType类型,也就是int
struct node *next;//指针域定为struct node类型,注意了这是一种类型,
//而且*next指向的也是这种类型所以就先设为struct node,
// 后面 我们会将sruct node 改为Node的
}Node,*pNode;//看,刚刚说的 后面 就是这里,我们统一叫struct node 为Node了,而且规定它的指针形式为*pNode;
以上我们写好了头文件,封装好了结点,开始写函数啦~
封装结点函数:
pNode CLH(void){//制作头结点的函数
//看这个函数头,pNode表示返回结点的地址,CLH是自己起的名字,全面CreateListHead
//这个void表示函数不用传入任何参数
pNode L=(pNode)malloc(sizeof(Node));//对这个凭空冒出来的L进行动态内存分配
//上面这一句是申请分配一个Node类型大小的内存,然后把他转为pNode,相当于它变成一个地址了
if(!L)
exit(-1);//这句很简单,分配失败就退出,注意看这里需要了stdlib的头文件
L->next=NULL;//L的下一个结点的数据域的地址还是一片虚无,设为NULL;
//注意上面指向的是next,不是*next,因为指向的是一个地址吧
return L;//将头结点地址返回
}
这样设置头结点的函数就算做好了,现在得到的是一个除了头结点以外空空如也没有灵魂的链表,接下来就是往函数里面插东西,使其饱满
插入函数:
int Insert(pNode L,int pos,int e){//插入函数
pNode p=L;//新设一个结点,使其和L地址一样,其作用为定位指针
pNode pNew;//插入元素要用的结点
int i=1;
while(p&&i<pos){//p结点存在而且i还没达到pos
p=p->next;
i++;
}//出循环后,p会指向插入位置的前一个结点,也就是pos-1的位置
if(!p||p<pos)
return 0;
pNew=(pNode)malloc(sizeof(Node));//开始对pNew结点进行封装
pNew->data=e;
pNew->next=p->next;//一定要先走这一步
p->next=pNew;
return 1;
}
现在是有肉体的链表了,我们还需要增添一些函数使其更有活力,接下来是求链表长度,思路很简单,需要一个定位指针,以及一个计数器,定位指针每往后指一个,计数器加一个数,代码实现如下
求链表长度函数:
int ListLength(pNode L){//求链表函数长度
//需要定位指针p,以及计数器i
pNode p=L->next;//设置定位指针
int i=0;//设置计数器
while(p){//如果L不为空
p=p->next;
i++;
}
return i;
}
求完长度,我们试着将链表的每一个值打印出来看,思路便是需要一个定位指针,在不断往后指的过程中打印所指到的每一个数,代码实现如下
打印函数:
void Print(pNode L){
pNode p=L->next;
while(p){//只要p不为空,我就可以打印
printf("%d, ",p->data);
p=p->next;//完事以后不要忘记指向下一个结点
}
}
接下来我们要面对两个比较相近的函数,删除函数和获取元素函数,咱们就只以删除指定位置元素作为示例,代码如下
int Delete(pNode L,int pos,int *e){//删除函数,需要传入链表头结点和位置,
//还需要一个东西装被删除的元素(即*e)虽然我也不懂为什么但是大家都这样做我也这样做了。。
//都删掉了还留着干啥
pNode p=L;
pNode s;
int i=1;
while(p->next && i<pos){//通过循环找到pos的位置,当i等于pos出循环
p=p->next;
i++;
}
if(!(p->next)||i>pos)
return 0;
s=p->next;//删除过程
*e=s->data;
p->next=s->next;
free(s);//来也匆匆去也匆匆的s,一定记得free掉,程序不能毛躁
return 1;
}
有个小问题:
可以发现,我们每次调用Insert函数时,都要对前面的结点重新遍历一遍,程序看那些重复的数值都看烦了,所以这就是这个函数的弊端,效率太低,当然这个Insert函数是可以选插入指定位置的,但如果想让函数输出的顺序和我们输入的一样,那现在我们考虑把加进来的插在前面,每来一个数值就插在最前面,也就是 L->next 的位置,这样我们只需要在打印函数的时候倒叙遍历就好了
于是乎,我们就有了一个新的前插函数 NewInsert,代码如下
新的前插函数:
int NewInsert(pNode L,int e){//前插算法,插入成功返回1,否则返回0
pNode pNew;
pNew=(pNode)malloc(sizeof(Node));
if(pNew){
pNew->data=e;
pNew->next=L->next;
L->next=pNew;
return 1;
}
else
return 0;
}
还有与之相配套的倒叙打印函数,NewPrint,代码如下
新打印函数(倒叙):
int NewPrint(pNode L){//与前插算法配套使用的打印函数,可倒叙打印出每一个数值,成功返回1
int i,len;
len=ListLength(L);
int a[len];//这个数组相当于一个栈的用途
pNode p=L->next;
if(!L)
return 0;
else
{
for(i=0;i<len;i++){//将链表中每一个值赋给数组a
a[i]=p->data;
p=p->next;
}
for(i=len-1;i>=0;i--){//倒叙输出每一个值
printf("%d, ",a[i]);
}
}
printf("\n");
return 1;
}
源程序测试:
//线性表链式存储结构的实现
#include<stdio.h>
#include<stdlib.h>//包含了exit()等函数
#include<malloc.h>//包含了malloc()等函数
typedef int ElemType;//定义ElemType 为 int
//开始设置结点的结构体指针
typedef struct node{//结点包含数据域和指针域
ElemType data;//数据设为ElemType类型,也就是int
struct node *next;//指针域定为struct node类型,注意了这是一种类型,
//而且*next指向的也是这种类型所以就先设为struct node,
//后面我们会将sruct node 改为Node的
}Node,*pNode;//看,刚刚说的后面就是这里,我们统一叫struct node 为Node了,而且规定它的指针形式为*pNode;
//结点的结构体已经封装好了,可以开始使用了
pNode CLH(void);
int Insert(pNode L,int pos,int e);
int ListLength(pNode L);
void Print(pNode L);
int NewInsert(pNode L,int e);//前插算法,插入成功返回1,否则返回0
int NewPrint(pNode L);//与前插算法配套使用的打印函数,可倒叙打印出每一个数值
int Delete(pNode L,int pos,int *e);
int main(){//主函数测试
pNode L,L1;
int i,e;
int t;
L=CLH();//现在L是一个带有头结点的空链表
L1=CLH();
if(!L)
printf("建立链表失败\n");
else{
for(i=0;i<10;i++){
Insert(L,i+1,i);
}
Print(L);
}
printf("%d\n",ListLength(L));
printf("删除了第一个数据:");
Delete(L,1,&t);
Print(L);
printf("输入数值,当输入-1时结束输入:\n");
scanf("%d",&e);
while(e!=-1){//不断往L1中插入直到插入-1后停止
NewInsert(L1,e);
scanf("%d",&e);
}
NewPrint(L1);
}
pNode CLH(void){
//看这个函数头,pNode表示返回结点的地址,CLH是自己起的名字,全面CreateListHead
//这个void表示函数不用传入任何参数
pNode L=(pNode)malloc(sizeof(Node));//对这个凭空冒出来的L进行动态内存分配
//上面这一句是申请分配一个Node类型大小的内存,然后把他转为pNode,相当于它变成一个地址了
if(!L)
exit(-1);//这句很简单,分配失败就退出,注意看这里需要了stdlib的头文件
L->next=NULL;//L的下一个结点的数据域的地址还是一片虚无,设为NULL;
//注意上面指向的是next,不是*next,因为指向的是一个地址吧
return L;//将头结点地址返回
}
int Insert(pNode L,int pos,int e){
pNode p=L;//新设一个结点,使其和L地址一样,其作用为定位指针
pNode pNew;//插入元素要用的结点
int i=1;
while(p&&i<pos){//p结点存在而且i还没达到pos
p=p->next;
i++;
}//出循环后,p会指向插入位置的前一个结点,也就是pos-1的位置
if(!p||i<pos)
return 0;
pNew=(pNode)malloc(sizeof(Node));//开始对pNew结点进行封装
pNew->data=e;
pNew->next=p->next;//一定要先走这一步
p->next=pNew;
return 1;
}
int ListLength(pNode L){
//需要定位指针p,以及计数器i
pNode p=L->next;//设置定位指针
int i=0;//设置计数器
while(p){//如果L不为空
p=p->next;
i++;
}
return i;
}
void Print(pNode L){
pNode p=L->next;
while(p){//只要p不为空,我就可以打印
printf("%d, ",p->data);
p=p->next;//完事以后不要忘记指向下一个结点
}
printf("\n");
}
int NewInsert(pNode L,int e){//前插算法,插入成功返回1,否则返回0
pNode pNew;
pNew=(pNode)malloc(sizeof(Node));
if(pNew){
pNew->data=e;
pNew->next=L->next;
L->next=pNew;
return 1;
}
else
return 0;
}
int NewPrint(pNode L){//与前插算法配套使用的打印函数,可倒叙打印出每一个数值
int i,len;
len=ListLength(L);
int a[len];//这个数组相当于一个栈的用途
pNode p=L->next;
if(!L)
return 0;
else
{
for(i=0;i<len;i++){//将链表中每一个值赋给数组a
a[i]=p->data;
p=p->next;
}
for(i=len-1;i>=0;i--){//倒叙输出每一个值
printf("%d, ",a[i]);
}
}
printf("\n");
return 1;
}
int Delete(pNode L,int pos,int *e){//删除函数,需要传入链表头结点和位置,
//还需要一个东西装被删除的元素(即*e)虽然我也不懂为什么但是大家都这样做我也这样做了。。
//都删掉了还留着干啥
pNode p=L;
pNode s;
int i=1;
while(p->next && i<pos){
p=p->next;
i++;
}
if(!(p->next)||i>pos)
return 0;
s=p->next;
*e=s->data;
p->next=s->next;
free(s);
return 1;
}
测试截图:
有个大问题:
在调用上面程序的时候,我们会发现链表的顺序非常的混乱,我们采用了前插数值的方法,导致了整个链表是倒过来的,倒叙打印倒像是欲盖弥彰,调用删除和获取元素的时候比较容易出错(其实是我自己写代码的时候容易忽略这个倒置的问题)其根本原因还是出在插入函数
说重点 很多教材都会讲到合并成顺序链表的问题,上面的程序为什么会没有,因为用上面的插入函数来实现创建链表进而实现线性表的合并是很不明智的做法,上面的通过前插得来的链表是没有顺序的,而合并成顺序表,则要求我们对链表进行排列,重新排序会使得程序效率变得及其低下,因为我们要不断的重复遍历这个混乱的链表,再进行合并,所以,上述的前插函数,如果只是进行一些打印输出删除之类的操作是比较轻松,但是对合并可能就不是很友好了
所以下一期就是专门记录链表合并成顺序表的问题
总结:
1.指向下一个结点时用的是 L->next,而不是L->*next
2.插入新结点时,如果先执行p->next = pNew,会导致p->next原来所指的地方找不到了,所以必须先执行pNew->next=p->next
3.我们可以发现,在对链表进行处理的时候,都是要从第一个元素开始往后查找,所以基本套路是需要一个定位指针,即 pNode p=L->next; 这句,从第一个元素依次往后遍历
4.我们可以习惯性的在函数头旁注释函数的用途以及返回回来的是个什么东西
5.会发现Insert函数调用要好久,看到测试截图后面大大的11.138秒了吗,大部分都是它的功劳,因为它每次都要遍历插入位置之前的全部结点,所以效率非常的低
6.单纯采用前插算法在合并成顺序表的时候会非常难受,在插入函数时就排序可能是个不错的选择