一、顺序存储结构与链式存储结构的区别
顺序存储就是从内存中取出一段连续地址的空间,将数据依次连续的存储在这段空间中。而链式存储结构是指数据存储在内存中的地址是离散的,以数据节点为单位,每个节点包括数据域和指针域。
二、链式结构数据的创建
栈(Stack)是限定仅在表尾进行插入和删除操作的线性表;队列(Queue)是只允许在一端进行插入操作,而在另一端进行删除操作的线性表。换句话说,栈和队列只是一种特殊的线性表。链式存储结构的数据单位是节点。节点由数据域和指针域组成,对于单链表,指针仅指向后一个数据单元(后继)的地址,而对于双向链表来说,每个节点的指针域还包括指向前一个数据单元的地址。以单链表的数据节点为例:
typedef struct Node
{
ElemType data;
struct Node *next;
}Node;
typedef struct Node *LinkList;
创建了数据节点之后,定义一个指向节点指针,开始对线性表进行操作。
对于栈和队列,仍需要以节点为单位,定义相应的链栈和链队列。
typedef struct StackNode
{
int data;
struct StackNode *next;
}StackNode, *LinkStackPointer;
typedef struct LinkStack
{
LinkStackPointer top;
int count;
}LinkStack;
对于链栈,*LinkStackPointer为指向结构体节点的指针。然后,在LinkStack中定义top指针和栈的容量。队列与栈的不同在于队列需要在两端分别进行插入和删除操作,所以在链队列中需要分别定义头和尾(指针)。
typedef struct QNode
{
int data;
struct QNode *next;
}QNode,*QueuePointer;
typedef struct
{
QueuePtr front,rear;
}LinkQueue;
三、链式结构数据的插入
建立数据节点与链栈、链队列之后,需要进行数据的插入。对于线性表的链式存储结构来说,插入数据的核心操作就是从内存中获取一个节点大小的空间,并初始化一个节点指针指向该数据段;同时,将需要插入的数据写入该数据段,并且将插入位置的上一节点的指针域赋值给该数据段的指针域,即将该数据段的后继指向插入位置的原后继;最后,将插入位置的上一节点的指针域指向该数据段。因为链式存储结构在插入数据时,不需要移动原数据节点的位置,所以时间复杂度为O(1)。
int ListInsert(LinkList *L,int i,int 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;
}
此处,LinkList本身为指向数据节点的指针,插入函数的形式参数中,第一个形参为
指针的指针,指向存储数据为节点地址的地址。i为需要插入数据的位置,e为待插入的数据。
对于链栈来说,插入操作也称为压栈(相反的,删除操作也称为弹栈),压栈与弹栈都在线性表尾进行操作。
int Push(LinkStack *S,int e)
{
LinkStackPtr s=(LinkStackPointer)malloc(sizeof(StackNode));
s->data=e;
s->next=S->top;
S->top=s;
S->count++;
return OK;
}
其插入原理与线性表一致。对于链队列来说,插入操作只能在表尾进行。即将从内存中获取的数据段链接到表尾即可。
int EnQueue(LinkQueue *Q,int e)
{
QueuePtr s=(QueuePointer)malloc(sizeof(QNode));
if(!s)
exit(OVERFLOW);
s->data=e;
s->next=NULL;
Q->rear->next=s;
Q->rear=s;
return OK;
}
四、链式结构数据的删除
与插入操作相反的原理,删除操作只需要找到需要删除的位置,将需要被删除的节点的指针域(后继)赋值给该节点的上一节点的指针域,然后将该节点所占的内存空间释放(还给系统,让系统调度)即可。
int ListDelete(LinkList *L,int i,int *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);
}
其中,通过指针p指向线性表,形参i为删除数据节点的位置,指针e指向被删除的数据。找到删除位置后,通过指针q持有p->next,然后将
p->next赋值为该节点的下一个节点地址,即将该节点地址排除在整个线性表之外,然后释放q所指定的内存空间。
出栈操作仍然需要在表尾进行。即将栈顶指针下移一位,指向后一节点,然后释放栈顶节点。
int Pop(LinkStack *S,int *e)
{
LinkStackPointer p;
if(StackEmpty(*S))
return ERROR;
*e=S->top->data;
p=S->top;
S->top=S->top->next;
free(p);
S->count--;
return OK;
}
出对操作需要在队头进行,即队列数据插入的另一端。其原理与以上所述线性表一致。
int DeQueue(LinkQueue *Q,int *e)
{
QueuePointer p;
if(Q->front==Q->rear)
return ERROR;
p=Q->front->next;
*e=p->data;
Q->front->next=p->next;
if(Q->rear==p)
Q->rear=Q->front;
free(p);
return OK;
}
需要注意的是,如果队头与队尾重合,删除后需要将rear指向头结点。
五、链式结构数据的遍历
对于顺序存储的数据结构来说,其随机访问某个元素的时间复杂度为O(1),而其插入与删除操作需要移动大量数据(线性表尾除外)。而对于链式存储的数据结构,插入和删除都不需要移动其他数据节点,时间复杂度为O(1),然后其随机访问需要从链表头根据后续指针一一访问,直到找到需要的那个数据节点,时间复杂度较高,如果被访问元素恰好落在表尾,则需要遍历所有的数据节点。int ListTraverse(LinkList L)
{
LinkList p=L->next;
while(p)
{
visit(p->data);
p=p->next;
}
printf("\n");
return OK;
}
如上所示,每次循环都将p指向p的下一个节点。在上述的插入操作中,需要在位置为i的地方插入数据,则需要依次循环至被查找的位置。
链栈与链队列的遍历与链表一致,只是初始指针的表示不同,对于链栈,初始指针p为LinkStack.top,对于链队列,初始指针p为LinkQueue.front(即从队头遍历至队尾)。
List是最基本的数据容器,很多其他的数据容器都是对其进行某些方面的限定而得到的特殊情况。List的存储结构分为最基本的顺序存储结构和链式存储结构,由于存储机理的不同,其特性差异较大,因此,需要根据应用的场合选取不同的存储结构。在高级语言中,List、Stack、Set、Map、Queue等都已封装好,只是在初始化的时候,根据需要选取不同的实例,灵活运用不同的数据结构,将会给程序带来极大的优化体验。
(注:部分源代码来自互联网)