一、链表
在链表存储中,每个结点不仅包含所存元素的信息,还包含元素之间逻辑关系的信息,如单链表中前驱结点包含后继结点的地址信息,这样就可以通过前驱结点中的地址信息找到后继结点的位置。
链表的特性:
- 不支持随机访问
- 支持存储空间的动态分配
- 链表中进行插入操作无须移动元素
二、 单链表
在每个结构中除了包含数据域外,还包含一个指针域,用以指向其后继结点。
- 带头结点的单链表中,头指针 head 指向头结点,头结点的值域不包含任何信息,从头结点的后继结点开始存储数据信息。头指针 head 始终不等于 NULL,head -> next 等于 NULL 的时候,链表为空。
- 不带头结点的单链表中的头指针 head 直接指向开始结点,当 head 等于 NULL 的时候,链表为空。
两者最明显的区别是,带头结点的的单链表中有一个结点不存储信息,只是作为标志,而不带头结点的单链表的所有结点都存储信息。
结构体定义
// 结构体定义
typedef struct LNode
{
int data;
struct LNode *next;
}LNode;
查找算法
// 查找算法:在单链表L中查找值为e的元素(带头结点)
LNode* findElem(LNode *C,int e)
{
LNode *p=C->next; // 结构体是指针,访问时用->
while(p!=NULL)
{
if(p->data==e)
return p; // 返回结点指针
p=p->next;
}
return p; // 返回NULL
}
插入算法
// 插入算法:往结点p的后面插入结点s
void insertElem(LNode *&p,LNode *&s)
{
s->next=p->next;
p->next=s;
}
删除算法
// 删除算法:删除结点p后面的结点
void delElem(LNode *&p)
{
q=p->next;
p->next=q->next;
free(q);
}
尾插法建立链表
// 尾插法建立链表:假设有n个元素存储在数组a中,用尾插法建立链表C
void createListR(LNode *&C,int a[],int n) // 因为需要对链表C进行修改,故要加上&表示引用
{
C=(LNode *)malloc(sizeof(LNode)); // 申请C的头结点空间
C->next=NULL;
int i;
LNode *s,*r; // s用来指向新申请的结点,r用来指向链表C的尾结点
r=C;
for(i=0;i<n;i++)
{
s=(LNode *)malloc(sizeof(LNode));
s->data=a[i];
r->next=s;
r=r->next; // 由于r是指针(地址),故修改r也相当于修改了C
}
r->next=NULL;
}
头插法建立链表
// 头插法建立链表:假设有n个元素存储在数组a中,用头插法建立链表C
void createListR(LNode *&C,int a[],int n) // 因为需要对链表C进行修改,故要加上&表示引用
{
C=(LNode *)malloc(sizeof(LNode)); // 申请C的头结点空间
C->next=NULL;
int i;
LNode *s; // s用来指向新申请的结点
for(i=0;i<n;i++)
{
s=(LNode *)malloc(sizeof(LNode));
s->data=a[i];
s->next=C->next; // s所指新结点的指针域 next 指向C中的开始结点
C->next=s; // 头结点的指针域 next 指向s结点,使得s成为新的开始结点
}
}
单链表的归并
// 单链表的归并:A和B是两个单链表(带表头结点),其中元素递增有序。设计一个算法,将A和B归并成一个按元素值
// 非递减有序的链表C,C由A和B中的结点组成
void merge(LNode *A,LNode *B,LNode *&C)
{
LNode *p=A->next; // 指针p指向链表A的开始结点(最小值结点)
LNode *q=B->next;
LNode *r; // r始终指向C的终端结点
C=A; // 用A的头结点作为C的头结点
C->next=NULL;
free(B);
r=C;
while(p!=NULL&&q!=NULL )
{
if(q->data>=p->data)
{
r->next=p;
p=p->next;
r=r->next;
}
else
{
r->next=q;
q=q->next;
r=r->next;
}
}
r->next=NULL;
if(p!=NULL)r->next=p;
if(q!=NULL)r->next=q;
}
二、双链表
在每个结构中除了包含数据域外,还包含两个指针域,用以分别指向其后继结点和前驱结点。同样,双链表也分为带头结点的双链表和不带头结点的双链表,情况类似於单链表。
- 带头结点的双链表,当 head ->next 为 NULL 时链表为空
- 不带头结点的双链表,当 head 为 NULL 时链表为空
结构体定义
// 结构体定义
typedef struct DLNode
{
int data;
struct DLNode *next;
struct DLNode *prior;
}DLNode;
查找算法
// 查找算法:在双链表L中查找值为e的元素(带头结点)
DLNode* findElem(DLNode *C,int e)
{
DLNode *p=C->next; // 结构体是指针,访问时用->
while(p!=NULL)
{
if(p->data==e)
return p; // 返回结点指针
p=p->next;
}
return p; // 返回NULL
}
插入算法
// 插入算法:往结点p的后面插入结点s
void insertElem(DLNode *&p,DLNode *&s)
{
s->next=p->next;
s->prior=p;
p->next->prior=s;
p->next=s;
}
删除算法
// 删除算法:删除结点p后面的结点
void delElem(DLNode *&p)
{
q=p->next;
p->next=q->next;
q->next->prior=p;
free(q);
}
尾插法建立链表
// 尾插法建立链表:假设有n个元素存储在数组a中,用尾插法建立链表C
void createDlistR(LNode *&C,int a[],int n) // 因为需要对链表C进行修改,故要加上&表示引用
{
C=(LNode *)malloc(sizeof(LNode)); // 申请C的头结点空间
C->next=NULL;
int i;
DLNode *s,*r; // s用来指向新申请的结点,r用来指向链表C的尾结点
r=C;
for(i=0;i<n;i++)
{
s=(DLNode *)malloc(sizeof(DLNode));
s->data=a[i];
r->next=s;
s->prior=r; // 与单链表不同之处
r=r->next; // 由于r是指针(地址),故修改r也相当于修改了C
}
r->next=NULL;
}
三、循环单链表
只要将单链表的终端结点的 next 指针指向链表中的第一个结点(头结点或开始结点)即可。
- 循环单链表可以实现从任意一个结点出发访问链表中的任何结点,而单链表从任一结点出发后只能访问这个结点本身及其后边的所有结点。
- 带头结点的循环单链表,当 head ->next 为 NULL 时链表为空
- 不带头结点的双链表,当 head 为 NULL 时链表为空
四、循环双链表
将双链表的终端结点的 next 指针指向链表中的第一个结点(头结点或开始结点),将双链表的第一个结点的 prior 指针指向链表中的终端结点。
- 带头结点的循环单链表,当 head ->next == head 或 head ->prior == head 时链表为空
- 不带头结点的双链表,当 head 为 NULL 时链表为空
五、静态链表
-
一般链表结点空间来自于整个内存,静态链表则来自于一个结构体数组。
-
数组中的每一个结点含有两个分量:一个是数据元素分量 data ,另一个是指针分量,指示了当前结点的直接后继结点在数组中的位置。
-
静态链表中的指针不是我们通常所说的C语言中用来存储内存地址的指针型变量,而是一个存储数组下标的整型变量,通过它可以找到后继结点在数组中的位置,其功能类似于真实的指针,因此称其为指针。