1.顺序存储定义
线性表的顺序存储结构,指的是用一段地址连续的存储单元依次存储线性表的数据元素。
线性表(a1,a2,......,an)的顺序存储示意图如下:
a1 | a2 | ... | ai-1 | ai | ... | an |
线性表的顺序存储结构,说白了,和刚才的例子一样,就是在内存中找了块地儿,通过占位的形式,把一定内存空间给占了,然后把相同数据类型的数据元素依次存放在这块空地中。既然线性表的每个数据元素的类型都相同,所以可以用C语言(其他语言也相同)的一维数组来实现顺序存储结构,即把第一个数据元素存到数组下标为0的位置中,接着把线性表相邻的元素存储在数组中相邻的位置。
3.顺序存储的结构代码
/* 存储空间初始分配量 */
#define MAXSIZE 20
/* ElemType类型根据实际情况而定,这里假设为int */
typedef int ElemType;
typedef struct
{
/* 数组存储数据元素,最大值为MAXSIZE */
ElemType data[MAXSIZE];
/* 线性表当前长度 */
int length;
} SqList;
这里,我们就发现描述顺序存储结构需要三个属性:
存储空间的起始位置:数组data,它的存储位置就是存储空间的存储位置。
线性表的最大存储容量:数组长度MaxSize。
线性表的当前长度:length。
4.数组长度与线性表长度区别
数组的长度是存放线性表的存储空间的长度,存储分配后这个量一般是不变的。有个别同学可能会问,数组的大小一定不可以变吗?我怎么看到有书中谈到可以动态分配的一维数组。是的,一般高级语言,比如C、VB、C++都可以用编程手段实现动态分配数组,不过这会带来性能上的损耗。
线性表的长度是线性表中数据元素的个数,随着线性表插入和删除操作的进行,这个量是变化的。
在任意时刻,线性表的长度应该小于等于数组的长度。
5.获得元素操作
对于线性表的顺序存储结构来说,如果我们要实现GetElem操作,即将线性表L中的第i个位置元素值返回,其实是非常简单的。就程序而言,只要i的数值在数组下标范围内,就是把数组第i-1下标的值返回即可。来看代码:
#define OK 1
#define ERROR 0
#define TRUE 1
#define FALSE 0
typedef int Status;
/* Status是函数的类型,其值是函数结果状态代
码,如OK等 */
/* 初始条件:顺序线性表L已存在,1≤i≤
ListLength(L) */
/* 操作结果:用e返回L中第i个数据元素的值 */
Status GetElem(SqList L, int i, ElemType *e)
{
if (L.length == 0 || i < 1 ||
i > L.length)
return ERROR;
*e = L.data[i - 1];
return OK;
}
注意这里返回值类型Status是一个整型,返回OK代表1,ERROR代表0。之后代码中出现就不再详述。
6.插入操作
插入算法的思路:
如果插入位置不合理,抛出异常;
如果线性表长度大于等于数组长度,则抛出异常或动态增加容量;
从最后一个元素开始向前遍历到第i个位置,分别将它们都向后移动一个位置;
将要插入元素填入位置i处; ?表长加1。
实现代码如下:
/* 初始条件:顺序线性表L已存在,1≤i≤
ListLength(L), */
/* 操作结果:在L中第i个位置之前插入新的数据元
素e,L的长度加1 */
Status ListInsert(SqList *L, int i, ElemType e)
{
int k;
/* 顺序线性表已经满 */
if (L->length == MAXSIZE)
return ERROR;
/* 当i不在范围内时 */
if (i < 1 || i >L->length + 1)
return ERROR;
/* 若插入数据位置不在表尾 */
if (i <= L->length)
{
/*将要插入位置后数据元素向后移动一位 */
for (k = L->length - 1; k >= i - 1; k--)
应该说这代码不难理解。如果是以前学习其他语言的同学,可以考虑把它转换成你熟悉的语言再实现一遍,只要思路相同就可以了。
7.删除操作
删除算法的思路:
如果删除位置不合理,抛出异常;
取出删除元素;
从删除元素位置开始遍历到最后一个元素位置,分别将它们都向前移动一个位置;
表长减1。
实现代码如下:
/* 初始条件:顺序线性表L已存在,1≤i≤
ListLength(L) */
/* 操作结果:删除L的第i个数据元素,并用e返回
其值,L的长度减1 */
Status ListDelete(SqList *L, int i, ElemType *e)
{
int k;
/* 线性表为空 */
if (L->length == 0)
return ERROR;
/* 删除位置不正确 */
if (i < 1 || i > L->length)
return ERROR;
*e = L->data[i - 1];
/* 如果删除不是最后位置 */
if (i < L->length)
{
/* 将删除位置后继元素前移 */
for (k = i; k < L->length; k++)
L->data[k - 1] = L->data[k];
}
8.线性表顺序存储结构的优缺点
优点:
(1).无需为表中的逻辑关系增加额外的存储空间
(2).可以快速存取表中对象
缺点:
(1).插入和删除需要移动大量的对象
(2).存储设备的碎片化
(3).当线性表过大的时候,很难确定长度
9.头指针与头结点的异同
头指针:
--头指针是指链表指向第一个结点的指针,若链表有头结点,则是指向头结点的指针
--头指针具有标识作用,所以头指针冠以链表的名字(指针变量的名字)
--无论链表是否为空,头指针均不为空
--头指针是链表的必要元素
头结点:
--头结点是为了操作的统一和方便而设立的,放在第一个元素的结点之前,其数据域一般无意义(但也可以用来存放链表的长度)
--有了头结点,对在第一元素结点前插入结点和删除第一结点起操作与其它结点的操作就统一了
--头结点不一定是链表的必要元素
10.单链表的读取
在线性表的顺序存储结构中,我们要计算任意一个元素的存储位置是很容易的。但在单链表中,由于第i个元素到底在哪?没办法一开始就知道,必须得从头开始找。因此,对于单链表实现获取第i个元素的数据的操作GetElem,在算法上,相对要麻烦一些。
获得链表第i个数据的算法思路:
(1).声明一个指针p指向链表第一个结点,初始化j从1开始;
(2).当j<i时,就遍历链表,让p的指针向后移动,不断指向下一结点,j累加1;
(3).若到链表末尾p为空,则说明第i个结点不存在;
(4).否则查找成功,返回结点p的数据。
实现代码算法如下:
/* 初始条件:顺序线性表L已存在,1≤i≤
ListLength(L) */
/* 操作结果:用e返回L中第i个数据元素的值 */
Status GetElem(LinkList L, int i, ElemType *e)
{
int j;
LinkList p; /* 声明一指针p */
p = L->next; /* 让p指向链表L的第个结点 */
j = 1; /* j为计数器 */
/* p不为空且计数器j还没有等于i时,循环继续 */
while (p && j < i)
{
p = p->next; /* 让p指向下一个结点 */
++j;
}
if (!p || j > i)
return ERROR; /* 第i个结点不存在 */
*e = p->data; /* 取第i个结点的数据 */
return OK;
}
说白了,就是从头开始找,直到第i个结点为止。由于这个算法的时间复杂度取决于i的位置,当i=1时,则不需遍历,第一个就取出数据了,而当i=n时则遍历n-1次才可以。因此最坏情况的时间复杂度是O(n)。
由于单链表的结构中没有定义表长,所以不能事先知道要循环多少次,因此也就不方便使用for来控制循环。其主要核心思想就是“工作指针后移”,这其实也是很多算法的常用技术。
此时就有人说,这么麻烦,这数据结构有什么意思!还不如顺序存储结构呢。
哈,世间万物总是两面的,有好自然有不足,有差自然就有优势。下面我们来看一下在单链表中的如何实现“插入”和“删除”。