1 顺序存储线性表:
顺序存储线性表结构,指的是用一段地址连续的存储单元依次存储线性表的数据元素。顺序线性表中各个结点元素类别是相同的。
连续的存储单元,元素类别相同?这个概念和C语言中的数组岂不是相似? 是的,使用一维数组恰可以实现顺序存储结构~
<span style="font-size:18px;"><span style="font-size:14px;">#define MAXLENGTH 20
typedef struct sequencelist
{
int data[MAXLENGTH];
int length;
}sqList;</span></span>
描述顺序结构的属性有三:
(1)存储空间起始位置:数组data,假设类型为整型,数组开始位置即为存储空间开始位置。
(2)线性表的最大容量:Maxlength。
(3)线性表当前长度,这是随着线性表的插入和删除操作而改变的。
引入了一维数组概念后,意味着可以方便的使用数组名与指针。
例如在对顺序列表插入的操作中,我们可以对插入函数函数头定义为int insert(sqList *L,int index,int element)
其中L为要操作的线性表,index为我们要插入的位置,elemnt为待插入的元素。
但是,这样一个简单的操作需要注意的有三点:
(1) 插入之间要判断,线性表是否可以被插?插入位置是否合法?
(2) 插入相当于把第index个元素到最后一个元素后移一个存储单元。
(3) 由于数组是从0开始计数,即data[0]是第一个存储单元,所以在插入时,新元素的位置为data[index-1]。
<span style="font-size:18px;"><span style="color:#009900;">//insert operation</span>
int insert(struct sqList *L,int index,int element)
{
int length =L->length;
if( index <= 0 || index > length+1 || length == MAXLENGTH)
return ERROR;
if(index<=L->length)
{
for(int k=L->length-1;k>=index-1;k--)
L->data[k+1]=L->data[k];
}
L->data[index-1]=e;
L->length++;
return OK;
}</span>
顺序存储结构在读、存数据时,不管在哪个位置,其时间复杂度都是O(1),而其插入和删除数据的时间复杂度都是O(n)。
这说明顺序存储结构适用于频繁的读写操作,不适合较多删除和插入操作,并且其顺序线性结构在初始化时要给定最大空间,不适用于空间不定的结构。
2 具有链式结构的线性表:
上面介绍的是一种连续存储的线性表结构,每当我们需要插入或者删除数据时,就要移动大量的元素。
而链式存储结构的线性表是一种带有指针域的,非连续存储的线性表结构。
对于这种链式结构线性表的基本单元,我们可以用结点(Node)来表示,一个结点包括数据域和指针域。
链表的第一个结点叫做头结点,其数据域可以为空,也可以存储线性表长度信息,头结点的指针域指向第一个结点。
指向第一个结点的指针定义为头指针。
头指针是链表的必要元素,但是头结点不是,即可以不存在头结点,但是一定要定义指向第一个结点的头指针。
2.1 单链表;
每个结点只含有一个指针域称之为单链表。
单链表是最简单的链表结构,其简单在于单向性,但缺点也在于不可逆性。
单链表结点元素定义:
<span style="font-size:18px;">// 定义链表节点
typedef struct _LNode
{
elemType data;
struct _LNode *Next;
}LNode;
</span>
对于单链表的操作来说,读取可没有顺序存储链表这么轻松,顺序存储链表无论链表多长,时间复杂度都是O(1)的事,可对于单链表,读取可要从头结点一个个结点读过去,所以时间复杂度和链表长度n有关系,时间复杂度为O(n)。
但是对于单链表的插入和删除操作来说,那就简单的多了,仅需要对要插入或删除位置的周边结点作些调整就行了。
如链表插入的操作具体如下:
<span style="font-size:18px;">//链表插入
int ListInsert(LNode *L,int i, elemType e)
{
LNode *p,*q;
int j=1;
p=L;
while(p&&j<i)
{
p=p->Next;
++j;
}
if(!p||j>1)
return ERROR;
q=(LNode *)malloc(sizeof(LNode));
q->data=e;
q->Next=p->Next;
p->Next=q;
return OK;
}</span>
显然,对于插入和删除数据越频繁的操作,单链表的效率优势就越是明显。
2.2 循环链表;
采用循环链表是为了链表两端的访问更加方便。
想想,如果像普通的单链表那样,想要访问链表的最后一个节点的话需要遍历整个链表,即时间复杂度是O(n)。单链表有一个头指针,指向头结点,同样我们可以定义尾指针指向尾结点。这样,我们想要访问尾结点时那就是分分钟的事情啦。
加一个尾结点称不上是循环链表,要将链表的首尾连起来。
head->1->2->3............->n->head
循环链表好像不够酷,最多提供的功能就是浏览到内容底端再返回到首部,最鸡肋的一点是单向遍历性!那么更酷一点的就是双向链表了。
2.3 双向链表;
为了克服单向性的缺点,人类发明了双向链表。双向链表是在单向链表的基础上再提供一个前驱结点指针。
其作用很明显,当我想访问之前的结点时,前驱结点指针就提供了便利。
双向链表不是一劳永逸的方案,当它给我们带来访问前向结点方便的同时,其他的操作也随之变得复杂。
在链表的插入操作中,结点指针的改变要注意顺序问题!
例如,下图中需要在双向结点p和p->next中间添加结点s。
其顺序应是:
s->next=p->next;
s->prior=p;
p->next=s;
p->next->prior=s;
即先将要新加入节点的前驱和后项指针搞定,再调整p和p->next的指针域部分。
对于双向链表来说,删除结点同样稍显复杂,但通过上面的增加结点可以举一反三。