数据结构的链式存储结构

来源:互联网 发布:天刀英气女性捏脸数据 编辑:程序博客网 时间:2024/05/17 07:23

链式存储的结构

单链表

1、链接存储方法
 链接方式存储的线性表简称为链表(Linked List)。
 链表的具体存储表示为:
  ① 用一组任意的存储单元来存放线性表的结点(这组存储单元既可以是连续的,也可以是不连续的)
  ② 链表中结点的逻辑次序和物理次序不一定相同。为了能正确表示结点间的逻辑关系,在存储每个结点值的同时,还必须存储指示其后继结点的地址(或位置)信息(称为指针(pointer)或链(link))
注意:
  链式存储是最常用的存储方式之一,它不仅可用来表示线性表,而且可用来表示各种非线性的数据结构。

2、链表的结点结构
┌──┬──┐
│data│next│
└──┴──┘ 
 data域--存放结点值的数据域
 next域--存放结点的直接后继的地址(位置)的指针域(链域)
注意:
  ①链表通过每个结点的链域将线性表的n个结点按其逻辑顺序链接在一起的。
 ②每个结点只有一个链域的链表称为单链表(SingleLinked List)。
【例】线性表(bat,cat,eat,fat,hat,jat,lat,mat)的单链表示如示意图



3、头指针head和终端结点指针域的表示
 单链表中每个结点的存储地址是存放在其前趋结点next域中,而开始结点无前趋,故应设头指针head指向开始结点。
注意:
 链表由头指针唯一确定,单链表可以用头指针的名字来命名。
【例】头指针名是head的链表可称为表head。
  终端结点无后继,故终端结点的指针域为空,即NULL。

4、单链表的一般图示法
 由于我们常常只注重结点间的逻辑顺序,不关心每个结点的实际位置,可以用箭头来表示链域中的指针,线性表(bat,cat,fat,hat,jat,lat,mat)的单链表就可以表示为下图形式。


5、单链表类型描述
typedef char DataType; //假设结点的数据域类型为字符
typedef struct node{ //结点类型定义
DataType data; //结点的数据域
struct node *next;//结点的指针域
}ListNode;
typedef ListNode *LinkList;
ListNode *p;
LinkList head;
注意:
 ①LinkList和ListNode *是不同名字的同一个指针类型(命名的不同是为了概念上更明确)
 ②LinkList类型的指针变量head表示它是单链表的头指针
 ③ListNode *类型的指针变量p表示它是指向某一结点的指针

6、指针变量和结点变量

┌────┬────────────┬─────────────┐ 
│    │    指针变量    │     结点变量     │
├────┼────────────┼─────────────┤
│ 定义 │在变量说明部分显式定义 │在程序执行时,通过标准 │
│ │ │函数malloc生成 │
├────┼────────────┼─────────────┤
│ 取值 │ 非空时,存放某类型结点 │实际存放结点各域内容 │
│ │的地址 │ │
├────┼────────────┼─────────────┤
│操作方式│ 通过指针变量名访问 │ 通过指针生成、访问和释放 │
└────┴────────────┴─────────────┘

①生成结点变量的标准函数
 p=( ListNode *)malloc(sizeof(ListNode));
//函数malloc分配一个类型为ListNode的结点变量的空间,并将其首地址放入指针变量p中
②释放结点变量空间的标准函数 
free(p);//释放p所指的结点变量空间
③结点分量的访问 
  利用结点变量的名字*p访问结点分量
方法一:(*p).data和(*p).next
方法二:p-﹥data和p-﹥next
④指针变量p和结点变量*p的关系 
 指针变量p的值——结点地址
 结点变量*p的值——结点内容
 (*p).data的值——p指针所指结点的data域的值
 (*p).next的值——*p后继结点的地址
  *((*p).next)——*p后继结点
注意:
  ① 若指针变量p的值为空(NULL),则它不指向任何结点。此时,若通过*p来访问结点就意味着访问一个不存在的变量,从而引起程序的错误。
  ② 有关指针类型的意义和说明方式的详细解释。 

 

 

单链表的运算

算法:
1、创建要插入的新结点
2、确定插入的存储位置
3、修改链表指针 
1、建立单链表

假设线性表中结点的数据类型是字符,我们逐个输入这些字符型的结点,并以换行符'\n'为输入条件结束标志符。动态地建立单链表的常用方法有如下两种:
插入前:

插入后:

int link_ins(NODE **head,int i,elemtype x)
{
int j=1;
NODE *p,*q;
q=(NODE *)malloc(sizeof(NODE));
q->data=x;
if(i==0){ q->link=*head;
*head=q;
return(0);
}
p=*head;
j=0;
while(++j<i&&p!=NULL)
p=p->link;
if(i<0||j<i)
return(1);
else
{
q->link=p->link;
p->link=q;
return(0);
}
}

分析:在上面的插入算法中,不需要移动别的元素,但必须
从头开始查找第i结点的地址,一旦找到插入位置,则插入结点
只需两条语句就可完成。该算法的时间复杂度为O(n)


(1) 头插法建表
① 算法思路
 从一个空表开始,重复读入数据,生成新结点,将读入数据存放在新结点的数据域中,然后将新结点插入到当前链表的表头上,直到读入结束标志为止。



注意:
 该方法生成的链表的结点次序与输入顺序相反。

② 具体算法实现
LinkList CreatListF(void)
{//返回单链表的头指针
char ch;
LinkList head;//头指针
ListNode *s; //工作指针
head=NULL; //链表开始为空
ch=getchar(); //读入第1个字符
while(ch!='\n'){
s=(ListNode *)malloc(sizeof(ListNode));//生成新结点
s->data=ch; //将读入的数据放入新结点的数据域中
s->next=head;
head=s;
ch=getchar(); //读入下一字符
}
return head;

 

 

 

(2) 尾插法建表 
① 算法思路 
从一个空表开始,重复读入数据,生成新结点,将读入数据存放在新结点的数据域中,然后将新结点插入到当前链表的表尾上,直到读入结束标志为止。



注意:
 ⒈采用尾插法建表,生成的链表中结点的次序和输入顺序一致
  ⒉必须增加一个尾指针r,使其始终指向当前链表的尾结点

② 具体算法实现 
LinkList CreatListR(void)
{//返回单链表的头指针
char ch;
LinkList head;//头指针
ListNode *s,*r; //工作指针
head=NULL; //链表开始为空
r=NULL;//尾指针初值为空
ch=getchar(); //读入第1个字符
while(ch!='\n'){
s=(ListNode *)malloc(sizeof(ListNode));//生成新结点
s->data=ch; //将读入的数据放入新结点的数据域中
if (head!=NULL)
head=s;//新结点插入空表
else
r->next=s;//将新结点插到*r之后
r=s;//尾指针指向新表尾
ch=getchar(); //读入下一字符
}//endwhile
if (r!=NULL)
r->next=NULL;//对于非空表,将尾结点指针域置空head=s;
return head;

注意:
  ⒈开始结点插入的特殊处理
由于开始结点的位置是存放在头指针(指针变量)中,而其余结点的位置是在其前趋结点的指针域中,插入开始结点时要将头指针指向开始结点。
 ⒉空表和非空表的不同处理
若读入的第一个字符就是结束标志符,则链表head是空表,尾指针r亦为空,结点*r不存在;否则链表head非空,最后一个尾结点*r是终端结点,应将其指针域置空。  

 

(3)尾插法建带头结点的单链表 
①头结点及作用
 头结点是在链表的开始结点之前附加一个结点。它具有两个优点:
 ⒈由于开始结点的位置被存放在头结点的指针域中,所以在链表的第一个位置上的操作就和在表的其它位置上操作一致,无须进行特殊处理;
 ⒉无论链表是否为空,其头指针都是指向头结点的非空指针(空表中头结点的指针域空),因此空表和非空表的处理也就统一了。

②带头结点的单链表



注意:
头结点数据域的阴影表示该部分不存储信息。在有的应用中可用于存放表长等附加信息。

③尾插法建带头结点链表算法 
LinkListCreatListR1(void)
{//用尾插法建立带头结点的单链表
char ch;
LinkList head=(ListNode *)malloc(sizeof(ListNode));//生成头结点
ListNode *s,*r; //工作指针
r=head; // 尾指针初值也指向头结点
while((ch=getchar())!='\n'){
s=(ListNode *)malloc(sizeof(ListNode));//生成新结点
s->data=ch; //将读入的数据放入新结点的数据域中
r->next=s;
r=s;
}
r->next=NULL;//终端结点的指针域置空,或空表的头结点指针域置空
return head;

注意:
上述算法里,动态申请新结点空间时未加错误处理,这对申请空间极少的程序而言不会出问题。但在实用程序里,尤其是对空间需求较大的程序,凡是涉及动态申请空间,一定要加入错误处理以防系统无空间可供分配。

4算法时间复杂度 
 以上三个算法的时间复杂度均为0(n)。 



2.单链表的查找运算 
1)按序号查找
① 链表不是随机存取结构
 在链表中,即使知道被访问结点的序号i,也不能像顺序表中那样直接按序号i访问结点,而只能从链表的头指针出发,顺链域next逐个结点往下搜索,直至搜索到第i个结点为止。因此,链表不是随机存取结构。

② 查找的思想方法
 计数器j置为0后,扫描指针p指针从链表的头结点开始顺着链扫描。当p扫描下一个结点时,计数器j相应地加1。当j=i时,指针p所指的结点就是要找的第i个结点。而当p指针指为null且j≠i时,则表示找不到第i个结点。
注意:
 头结点可看做是第0个结点。

③具体算法实现
 
ListNode* GetNode(LinkList head,int i)
{//在带头结点的单链表head中查找第i个结点,若找到(0≤i≤n),
//则返回该结点的存储位置,否则返回NULL。
int j;
ListNode *p;
p=head;j=0;//从头结点开始扫描
while(p->next&&j<i){//顺指针向后扫描,直到p->next为NULL或i=j为止
p=p->next;
j++;
}
if(i==j)
return p;//找到了第i个结点
else return NULL;//当i<0或i>0时,找不到第i个结点


④算法分析

 算法中,while语句的终止条件是搜索到表尾或者满足j≥i,其频度最多为i,它和被寻找的位置有关。在等概率假设下,平均时间复杂度为:

 


2按值查找
①思想方法 
从开始结点出发,顺着链逐个将结点的值和给定值key作比较,若有结点的值与key相等,则返回首次找到的其值为key的结点的存储位置;否则返回NULL。

②具体算法实现

ListNode* LocateNode (LinkList head,DataType key)
{//在带头结点的单链表head中查找其值为key的结点
ListNode *p=head->next;//从开始结点比较。表非空,p初始值指向开始结点
while(p&&p->data!=key)//直到p为NULL或p->data为key为止
p=p->next;//扫描下一结点
return p;//若p=NULL,则查找失败,否则p指向值为key的结点
}

③算法分析

 该算法的执行时间亦与输入实例中key的取值相关,其平均时间复杂度分析类似于按序号查找,为O(n)。

3.插入运算
(1)思想方法
 插入运算是将值为x的新结点插入到表的第i个结点的位置上,即插入到ai-1与ai之间。
具体步骤: 
(1)找到ai-1存储位置p
 (2)生成一个数据域为x的新结点*s
 (3)令结点*p的指针域指向新结点
 (4)新结点的指针域指向结点ai




(2)具体算法实现
void InsertList(LinkList head,DataType x,int i)
{//将值为x的新结点插入到带头结点的单链表head的第i个结点的位置上
ListNode *p;
p=GetNode(head,i-1);//寻找第i-1个结点
if (p==NULL)//i<1或i>n+1时插入位置i有错
Error("position error");
s=(ListNode *)malloc(sizeof(ListNode));
s->data=x;s->next=p->next;p->next=s;


(3)算法分析
 算法的时间主要耗费在查找操作GetNode上,故时间复杂度亦为O(n)。

4.删除运算
(1)思想方法
  删除运算是将表的第i个结点删去。
具体步骤: 
(1)找到ai-1的存储位置p(因为在单链表中结点ai的存储地址是在其直接前趋结点ai-1的指针域next中)
 (2)令p->next指向ai的直接后继结点(即把ai从链上摘下)
 (3)释放结点ai的空间,将其归还给"存储池"。




(2)具体算法实现
void DeleteList(LinkList head,int i)
{//删除带头结点的单链表head上的第i个结点
ListNode *p,*r;
p=GetNode(head,i-1);//找到第i-1个结点
if (p==NULL||p->next==NULL)//i<1或i>n时,删除位置错
Error("position error");//退出程序运行
r=p->next;//使r指向被删除的结点ai
p->next=r->next;//将ai从链上摘下
free(r);//释放结点ai的空间给存储池

注意:
 设单链表的长度为n,则删去第i个结点仅当1≤i≤n时是合法的。
  当i=n+1时,虽然被删结点不存在,但其前趋结点却存在,它是终端结点。因此被删结点的直接前趋*p存在并不意味着被删结点就一定存在,仅当*p存在(即p!=NULL)且*p不是终端结点(即p->next!=NULL)时,才能确定被删结点存在。

(3)算法分析
 算法的时间复杂度也是O(n)。
 链表上实现的插入和删除运算,无须移动结点,仅需修改指针。 




循环链表(Circular Linked List)

 循环链表是一种首尾相接的链表。

1、循环链表

(1)单循环链表——在单链表中,将终端结点的指针域NULL改为指向表头结点或开始结点即可。

(2)多重链的循环链表
——将表中结点链在多个环上。

2、带头结点的单循环链表



注意:
 判断空链表的条件是head==head->next;

3、仅设尾指针的单循环链表
 用尾指针rear表示的单循环链表对开始结点a1和终端结点an查找时间都是O(1)。而表的操作常常是在表的首尾位置上进行,因此,实用中多采用尾指针表示单循环链表。带尾指针的单循环链表可见下图。


注意:
 判断空链表的条件为rear==rear->next;

4、循环链表的特点
 循环链表的特点是无须增加存储量,仅对表的链接方式稍作改变,即可使得表处理更加方便灵活。
【例】在链表上实现将两个线性表(a1,a2,…,an)和(b1,b2,…,bm)连接成一个线性表(a1,…,an,b1,…bm)的运算。

 分析:若在单链表或头指针表示的单循环表上做这种链接操作,都需要遍历第一个链表,找到结点an,然后将结点b1链到an的后面,其执行时间是O(n)。若在尾指针表示的单循环链表上实现,则只需修改指针,无须遍历,其执行时间是O(1)。


 相应的算法如下:
LinkList Connect(LinkList A,LinkList B)
{//假设A,B为非空循环链表的尾指针
LinkList p=A->next;//①保存A表的头结点位置
A->next=B->next->next;//②B表的开始结点链接到A表尾
free(B->next);//③释放B表的头结点
B->next=p;//④
return B;//返回新循环链表的尾指针
}
注意:
 ①循环链表中没有NULL指针。涉及遍历操作时,其终止条件就不再是像非循环链表那样判别p或p->next是否为空,而是判别它们是否等于某一指定指针,如头指针或尾指针等。
 ②在单链表中,从一已知结点出发,只能访问到该结点及其后续结点,无法找到该结点之前的其它结点。而在单循环链表中,从任一结点出发都可访问到表中所有结点,这一优点使某些运算在单循环链表上易于实现。






双链表的应用

线性表的插入运算(双向链表存储结构)
在双向循环链表L的位置p处插入一个新元素x的过程Insert可实现如下。


线性表的插入运算(双向链表存储结构)
在双向循环链表L的位置p处插入一个新元素x的过程Insert可实现如下。

算法:
typedef struct dnode

{ elemtype data;

struct dnode *prior,*next;

}DNODE;

int link_ins(DNODE **head,int i,elemtype x)
{
int j=1; //双向循环链表
DNODE *p,*q;
q=(DNODE *)malloc(sizeof(DNODE));
q->data=x;
if(i==0){ q->prior=*head;q->next=*head;
*head=q;
return(0);
}
p=*head;
j=0;
while(++j<i&&p!=NULL)
p=p->next;
if(i<0||j<i)
return(1);
else
{
q->next=p->next;
p->next=q;

p->next->prior=q;

q->prior=p;
return(0);
}
}
分析:在上面的插入算法中,不需要移动别的元素,但必须
从头开始查找第i结点的地址,一旦找到插入位置,则插入结点
只需两条语句就可完成。该算法的时间复杂度为O(n)

1、双向链表(Double Linked List)

 双(向)链表中有两条方向不同的链,即每个结点中除next域存放后继结点地址外,还增加一个指向其直接前趋的指针域prior。

注意:
 ①双链表由头指针head惟一确定的。
 ②带头结点的双链表的某些运算变得方便。
 ③将头结点和尾结点链接起来,为双(向)循环链表。

2、双向链表的结点结构和形式描述
①结点结构(见上图a)

②形式描述
typedef struct dlistnode{
DataType data;
struct dlistnode *prior,*next;
}DListNode;
typedef DListNode *DLinkList;
DLinkList head;

3、双向链表的前插和删除本结点操作
 由于双链表的对称性,在双链表能能方便地完成各种插入、删除操作。
①双链表的前插操作

void DInsertBefore(DListNode *p,DataType x)
{//在带头结点的双链表中,将值为x的新结点插入*p之前,设p≠NULL
DListNode *s=malloc(sizeof(DListNode));//①
s->data=x;//②
s->prior=p->prior;//③
s->next=p;//④
p->prior->next=s;//⑤
p->prior=s;//⑥
}
②双链表上删除结点*p自身的操作

void DDeleteNode(DListNode *p)
{//在带头结点的双链表中,删除结点*p,设*p为非终端结点
p->prior->next=p->next;//①
p->next->prior=p->prior;//②
free(p);//③
}
注意:
 与单链表上的插入和删除操作不同的是,在双链表中插入和删除必须同时修改两个方向上的指针。
 上述两个算法的时间复杂度均为O(1)。





顺序链表和双链表的比较

顺序表和链表各有短长。在实际应用中究竟选用哪一种存储结构呢?这要根据具体问题的要求和性质来决定。通常有以下几方面的考虑

┌───┬───────────────┬───────────────┐
│ │   顺序表    │   链表    │
├─┬─┼───────────────┼───────────────┤
│基│分│静态分配。程序执行之前必须明确动态分配只要内存空间尚有空闲,
│于│配│规定存储规模。若线性表长度n就不会产生溢出。因此,当线性表
│空│方│化较大,则存储规模难于预先确定│的长度变化较大,难以估计其存储
│间│式│估计过大将造成空间浪费,估计太规模时,以采用动态链表作为存储
│考│ │小又将使空间溢出机会增多。 │结构为好。 │
│虑├─┼───────────────┼───────────────┤
│ │存│为1。当线性表的长度变化不大, │<1 │
│ │储│易于事先确定其大小时,为了节约│ │
│ │密│存储空间,宜采用顺序表作为存储│ │
│ │度│结构。 │ │
├─┼─┼───────────────┼───────────────┤
基│存│随机存取结构,对表中任一结点都│顺序存取结构,链表中的结点,需│
│于│取│可在O(1)时间内直接取得 │从头指针起顺着链扫描才能取得。│
│时│方│线性表的操作主要是进行查找,很│ │

│少做插入和删除操作时,采用顺序│ │
│ │表做存储结构为宜。 │ │
├─┼───────────────┼───────────────┤
│ │
插│在顺序表中进行插入和删除,平均│在链表中的任何位置上进行插入和│
│ │入│要移动表中近一半的结点,尤其是│删除,都只需要修改指针。对于频│
│ │删│当每个结点的信息量较大时,移动│繁进行插入和删除的线性表,宜采│
│ │除│结点的时间开销就相当可观。 │用链表做存储结构。若表的插入和│

│ ││ │删除主要发生在表的首尾两端,则│
│ ││ │采用尾指针表示的单循环链表为宜│
└─┴─┴───────────────┴───────────────┘

存储密度Storage Density)是指结点数据本身所占的存储量和整个结点结构所占的存储量之比,即

存储密度=(结点数据本身所占的存储量)/(结点结构所占的存储总量)















 

1 0