继续学习数据结构:
前面的线性表的顺序存储结构,它是有缺点的,最大的缺点就是删除和插入时需要移动大量元素,明显需要消耗打开量时间。为了解决这种问题,引入了线性表的链式存储结构。
链表中有两个易混的概念:
(1)头指针:头指针是指向第一个节点的指针
(2)头结点:头结点是第一个节点之前的节点,这个节点中只有指针域有值,且值是第一个节点的地址,它的数据域中无值。
1.下面首先是定义节点的数据类型:
typedef struct Node
{
ElemType data;
struct Node *next;
}Node;
typedef struct Node *LinkList;
注意:这里面处理的细节:
(1)定义的同时改名为Node。
(2)更关键的是下面的语句typedef struct Node *LinkList;是指将struct Node * 型的数据类型改名为LinkList,所以后面定义此节点数据类型的指针节可以直接用LinkList来定义,就像定义节点数据类型时,可以用Node来定义一样。
(3)注意,如果写成下面这种形式也是一样的:
typedef struct Node
{
ElemType data;
struct Node *next;
}Node,*LinkList;
2.单链表的读取
//获得链表第i个数据的算法
//初始条件:顺序线性表L已存在,1<=i<=ListLength(L)
//操作结果:用e返回L中第i个数据元素的值
Status GetElem(LinkList L,int i,ElemType *e)
{
int j;
LinkList p;
p=L->next;//L是头节点,则此时p指向表中第一个节点。
j=1;
while(p&&j<i)
{
p=p->next;
j++;
}
if(!p||j>i)
{
return ERROR;
}
*e=p->data;
return OK;
}
解析:
①函数参数中的L是:LinkList L 来的,注意这种表示L其实是头结点,所以p=L->next 就使得p指向第一个节点。
②在下面的循环中,注意j和p的对应关系,j初始值为1,p在循环开始时也指向的是第一个节点。然后每次循环都会使得j加1,使得p指向下一个节点。所以j和p是一一对应的。
③因此j时用来计数的,用在while循环中,判断语句是
while(p&&j<i)
...
即是p不为空且计数器j还没有到i时,循环都会继续
④然后,
if(!p||j>i)
{
return ERROR;
}
表示循环完整个链表,没有达到i,也就是i的值不正确,i小于1或i大于总数据个数,就报错。
⑤最后,
*e=p->data;
如果在上面找到了正确的第i个节点,就用e返回这第i个节点的数据。
3.单链表的插入和删除
①
//单链表的插入
//初始条件,顺序线性表L已存在,1<=i<=ListLength(L)
//操作结果,在L中第i个节点位置之前插入新的数据元素e,L的长度加1
Status ListInsert(LinkList *L,int i,ElemType e)
{
int j;
LinkList p,s;
p=*L;
j=1;'
while(p&&j<i)
{
p=p->next'
j++;
}
if(!p||j>i)
{
return ERROR;
}
s=(LinkList)malloc(sizeof(Node));
s->data=e;
s->next=p->next;
p->next=s;
return OK;
}
解析:
①难理解的是此处对L的定义是:LinkList *L ,则L是一个二级指针,并且这里可以从下面一句 p=*L;且说明了此时p指向头结点,所以L原来一定是指向头结点的指针的指针。
②注意这里p与j的对应关系变了:p指向头结点,可以看成是指向第0个节点,而 j 初始值还是1,同时每次循环p还是会指向下一个,j还是会加1,所以当 j 达到 i 时,p指向的就是第i-1个节点。
③为什么要使p指向的是第i-1个节点呢?主要是因为这里使用的是尾插法:p指向的是第i-1个元素,那么将新的节点s插在p后面,就是插在了第 i 个位置上。
④当然还有前插,在我之前的管理系统里面用的插入都是用的前插,这里我也说明,好将两种方法进行比较。
Status ListInsert(LinkList *L,int i,ElemType e)
{
int j;
LinkList h,q,p,s;
h=*L;q=*L;p=q->next;
j=1;
while(p&&j<i)
{
q=p;
p=p->next;
j++;
}
if(!p||j>i)
{
return ERROR;
}
s=(LinkList)malloc(sizeof(Node));
s->data=e;
s->next=p;
q->next=s;
return OK;
}
简单来说就是定义一个q始终指向当前节点p的前一个节点;且p是从第一个节点开始进入循环的,p和j就是一一对应的,j 等于 i 的时候,p就指向的是第 i 个节点;核心部分 s->next=p; q->next=s;很容易理解,不多阐述。
②
//单链表的删除
//初始条件:顺序线性表L已存在,1<=i<=ListLength(L)
//操作结果:删除L中的第i个节点,并用e返回其值,L的长度减1
Status ListDelete(LinkList *L,int i,ElemType *e)
{
int j;
LinkList p,q;
p=*L;
j=1;
while(p->next&&j>i)
{
p=p-next;
j++;
}
if(!(p->next)||j>i)
{
return ERROR;
}
q=p->next;
p->next=q->next;
*e=q->data;
free(q);
return OK;
}
解析:
①注意p还是如上面插入时一样的操作,所以p在执行了语句p=*L;后就是指向的头结点。
②循环体while的判断语句:
while(p->next&&j>i)
用的是p->next,则p->next开始指向的是第一个节点,j 开始也是等于1,所以j 就是与p->next同步变化的,所以当j 等于 i 时,p->next 就指向的是第i 个节点,所以p就指向的是第i-1个节点。
③下面的实现删除的核心部分
q=p->next;
p->next=q->next;
可以比较我之前在管理系统当中用的方法
q->next=p->next;
free(p);
4.单链表的整表创建:
//单链表的整表创建
//随机产生n个元素的值,采用头插法,建立带头节点的单链线性表L
void CreateListHead(LinkList *L,int n)
{
LinkList p;
int i;
srand(time(0));
*L=(LinkList)malloc(sizeof(Node));
(*L)->next=NULL;
for(i=0;i<n;i++)
{
p=(LinkList)malloc(sizeof(Node));
p->data=rand()%100+1;
p->next=(*L)->next;
(*L)->next=p;
}
}
解析:
①产生1001以内的随机数的方法:
srand(time(0));
p->data=rand()%100+1;
注意需要引入的头文件包括两个:
#include<stdlib.h>
#include<math.h>
②这里使用的是头插法:每一个新的节点进来都插在头结点之后,都是成为第一个节点。
③链表创建时当然还有尾插法,我之前的管理系统当中创建链表时用的方法就是尾插法,如下
尾插法(1):与上面头插法出自同一本书,以L为头指针的尾插法
void CreateListTail(LinkList *L,int n)
{
LinkList p,r;
int i;
srand(time(0));
*L=(LinkList)malloc(sizeof(Node));
r=*L;
for(i=0;i<n;i++)
{
p=(Node *)malloc(sizeof(Node));
p->next=rand()%100+1;
r->next=p;
r=p;
}
r->next=NULL;
}
注意:这里面和下面的方法中,唯一的不同就是头结点的表示方法不同,这里因为L是由LinkList * 定义来的,而LinkList 本身又是节点数据类型的指针,是Node * 型的,所以L就是Node型的二级指针,所以*L就表示的是一个节点的指针,语句
*L=(LinkList)malloc(sizeof(Node));
就是在为头结点开辟空间。
尾插法(2):我自己之前管理系统当中的
JD *h,*q,*p;
q=(JD *)malloc(sizeof(JD));
h=q;
while(1)
{
p=(JD *)malloc(sizeof(JD));
p->data=rand()%100+1;
q->next=p;
q=p;
//注意添加跳出循环条件
}
q=0;
return h;
可以将这两种方法进行比较,并注意这里返回的是头结点自身的指针,是JD * 型的指针,相当于这里的LinkList 型。如果这种建立链表时,返回的是JD **型,或者说是这里的LinkList *型的,那么在之前的插入和删除两个操作中使用到的 p=*L; 就能够正确理解,p确实是指向的头结点了。