线性表的表示和实现


线性表

线性表的基本结构是除了第一个元素无直接前驱和最后一个元素无直接后继之外,每个元素都存在一个前驱和后继,存在结构上的线性关系。(直接前驱:直接与该结点相邻的前驱结点;前驱:头结点到该结点中间的所有结点都算是前驱结点,不包括该结点)

循环链表和双向链表虽然看起来与普通的线性表存在不同,但其实是线性表特殊的存储结构,属于线性表的一种。

线性表按照存储结构差异可分为两种,一种是存储单元连续的顺序存储结构——顺序表;另一种是存储单元随机的链式存储结构——链表

顺序表顾名思义数据的存储是按照顺序的,创建顺序表时划定一块连续的存储空间,顺序表中数据所在的存储空间对应的地址连续。所以,找到地址等于找到数据,因为这一特性的随机存取特性决定了用数组来具体描述顺序存储结构。

结构体定义顺序表的存储结构

1
2
3
4
5
6
7
8
9
10
#define MAXSIZE 100

typedef struct

{
int *elem;//定义整型指针,代表存储空间的基地址,

int length;//当前长度

}SqList;//SqList为此结构体别名

初始化顺序表算法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
Status InitList(SqList &L)

/*构造一个空的顺序表,SqList &L是带地址传递,如果不带&、用SqList L的话,函数对于L的操作最后并不能返回到实参中去*/

{

L.elem=new int[MAXSIZE];

/*动态分配空间;当使用new运算符定义一个多维数组变量或数组对象时,它产生一个指向数组第一个元素的指针*/

if(!L.elem) exit(OVERFLOW);

L.length=0;//空表

return OK;

}//Status InitList(SqList &L)的Status为函数返回的状态码,C语言中定义了#define Status int

顺序表取值算法

1
2
3
4
5
6
7
8
9
10
11
Status GetElem(SqList L,int i,ElemType &e)

{

/*L.elem[i-1]取值操作不需要带地址传递,不需要改变实参,只需要赋值给e即可;这里的&e应该是调用该函数时给函数传一个存储单元的地址,进行带地址传递,不用return、通过读取该地址得到数值。*/

e=L.elem[i-1];//第i个元素的下标为i-1

return OK;

}

顺序表插入算法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
Status ListInsert(SqList &L,int i,ElemType e)

{

/*插入算法需要对原顺序表进行修改,所以需要带地址传递;ElemType代表所有可能的数据类型。代表在顺序表L的第i个位置插入新元素e*/

if((i<1)||(i>L.length+1)) return ERROR;//i值不合法

if(L.length==MAXSIZE) return ERROR;//存储空间已满

for(j=L.length-1;j>=i-1;j--)

L.elem[j+1]=L.elem[j];//从后往前,元素向后移动

L.elem[i-1]=e;//i位置之后的元素移动完成之后向空置的i存储单位赋值插入

++L.length;//表长+1

return OK;

}

顺序表删除算法:与插入算法类似,但不需要第三个参数e,只需要i+1个元素开始往前移动,最后表长-1即可。


以上所讲的顺序表的插入和删除如果出现在数据量大的情况下,其时间复杂度将会影响算法的效率,而线性表的另一种逻辑结构——链式存储结构完美解决该问题。

链表

单链表存储特点:用一组任意的存储单元存储线性表的数据元素,一个存储单元划分成两块,一块为data数据域,另一块为next指针域、用于存储指向后继的地址信息。可以通过修改指针域实现读取任意结点、删除插入结点的功能,一定程度上避免了大数据量的前提下顺序表插入删除等所进行的大规模的数据移动,加快了时间效率。

单链表结构如图,单链表初始化之后,头指针L存放头结点地址、指向头结点,通过L.data读取头结点的data域,头结点的data域可用于存放链表信息等非内容信息;L.next指向的是首元结点,,若不设置头结点,则L.next直接指向首元结点;

结构体定义单链表的存储结构

1
2
3
4
5
typedef struct LNode
{
ElemType data;//结点数据域
struct LNode *next;//结点指针域,用于存放下一个同类型的结点的地址
}LNode,*LinkList;//LingList为指向该结构体LNode的指针类型

单链表初始化

1
2
3
4
5
6
Status InitList(LinkList &L)
{
L=new LNode;//new结构体产生一个结点返回地址给头指针
L->next=NULL;//头指针next域置空
return OK;
}

单链表取值算法

1
2
3
4
5
6
7
8
9
10
11
12
13
Status GetElem(LinkList L,int i,ElemType &e)
{//带头结点的单链表L根据序号L获取元素的值,用e返回L中第i个元素的值
p=L->next;//p指向首元结点
j=1;//计数器,用于计算p指向第几个元素
while(p&&j<i)//当p为空或者p指向第i个元素时跳出循环
{
p=p->next;//未到第i个元素,则指向下一个结点
++j;//计数器加1,代表当前指向第几个元素
}
if(!p||j>i) return ERROR;//参数i不合法i>n或i<=0
e=p->data;//e存储空间保存第i个元素data域的值
return OK;
}

单链表按值查找算法

1
2
3
4
5
6
7
8
LNode *LocateElem(LinkList L,ElemType e)
{
/*LNode *代表函数返回类型为指向返回值为p的地址,返回的地址指向LNode类型;函数作用:在带头结点的单链表L中查找值为e的元素*/
p=L->next;//p指向首元结点
while(p&&p->data!=e)//当p不为空且p指向的结点的data域不等于e时循环继续
p=p->next;//指向下一个结点
return p;//返回一个指向p的地址?
}

单链表插入算法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
Status ListInsert(LinkList &L,int i,ElemType e)
{//在带头结点的单链表L中第i位置插入值为e的新结点
p=L,j=o;//把指向头结点L的地址赋值给p
while(p&&(j<i-1))//查找第i-1个结点,并使p指向该结点
{
p=p->next;
++j;
}
if(!p||j>i-1) return ERROR;//p为空或者i不合法则返回ERROR
s=new LNode;
/* 生成新结点*s,new得到一块空间后返回地址给指针s */
/*此处的算法是s已经被声明为指针了,只要把它指向一个结点即可;若没有声明则需要LNode *s=new LNode*/
s->data=e;//s结点数据域赋值为e
s->next=p->next;//把s结点的next域指向后一个结点
p->next=s;//把p的直接后继改为s结点,插入完毕
return OK;
}

单链表删除算法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
Status ListDelete(LinkList &L,int i)
{//在带头结点的单链表L中,删除第i个元素
p=L;
j=0;
while((p->next) && (j<i-1))//查找第i-1个结点,使p指向该结点
{
p=p->next;
++j;
}
if(!(p->next)||(j>i-1)) return ERROR;//i>n或i<1,删除位置不合法
q=p->next;/* q指针临时保存需要删除的i的地址,这个q也是事先声明好的,前面的算法同理也是事先声明好的 */
p->next=q->next;//把第i-1个结点的直接后继换成第i+1个元素
delete q;//删除保存在q的第i个结点
return OK;
}

顺序表和链表的比较

顺序表

优点:

  1. 存储密度等于1,不会造成存储空间的浪费。
  2. 取值效率高,只要得到下标就能取值。

缺点:

  1. 插入删除效率低,需要移动大量数据。
  2. 存储空间事先分配,容易造成溢出或者浪费。

链表

优点:

  1. 不需要事先分配空间。
  2. 插入删除效率高,只需修改next域的指向即可。

缺点:

  1. 存储密度小于1,有一部分存储空间用于存储下一结点的地址。
  2. 取值效率低,取值只能从表头开始。