线性表

1.1  线性表的定义
线性表是一种线性结构,在一个线性表中数据元素的类型是相同的,或者说线性表是由
同一类型的数据元素构成的线性结构,定义如下:
线性表是具有相同数据类型的n(n≥0)个数据元素的有限序列,通常记为:
(a 1 ,a 2 ,… a i-1 ,a i ,a i+1 ,…a n )
其中n为表长, n=0 时称为空表。
需要说明的是:a i 为序号为 i 的数据元素(i=1,2,…,n),通常将它的数据类型抽象为
ElemType,ElemType根据具体问题而定。
1.2  线性表的实现
1.2.1  线性表的顺序存储结构
1.顺序表
线性表的顺序存储是指在内存中用地址连续的一块存储空间顺序存放线性表的各元素,
用这种存储形式存储的线性表称其为顺序表。因为内存中的地址空间是线性的,因此,用物
理上的相邻实现数据元素之间的逻辑相邻关系是既简单又自然的。
设a 1的存储地址为Loc(a 1),每个数据元素占d个存储地址,则第i个数据元素的地址为:
Loc(a i )=Loc(a 1 )+(i-1)*d 1≤i≤n
这就是说只要知道顺序表首地址和每个数据元素所占地址单元的个数就可求出第i个数
据元素的地址来,这也是顺序表具有按数据元素的序号随机存取的特点。

线性表的动态分配顺序存储结构:

#define LIST_INIT_SIZE 100 //存储空间的初始分配量
#define LISTINCREMENT 10 //存储空间的分配增量
typedef struct{
ElemType *elem; //线性表的存储空间基址
int length; //当前长度
int listsize; //当前已分配的存储空间
}SqList;

2.顺序表上基本运算的实现
(1)顺序表的初始化
顺序表的初始化即构造一个空表,这对表是一个加工型的运算,因此,将L设为引用参数,
首先动态分配存储空间,然后,将length置为0,表示表中没有数据元素。
int Init_SqList (SqList &L){
L.elem = (ElemType * )malloc(LIST_INIT_SIZE * sizeof(ElemType));
if (!L.elem) exit (OVERFLOW); //存储分配失败
L.length=0;
L. listsize = LIST_INIT_SIZE; //初始存储容量
return OK;
}

(2)插入运算
线性表的插入是指在表的第i(i的取值范围:1≤i≤n+1)个位置上插入一个值为 x 的新元素,
插入后使原表长为 n的表:
(a 1 ,a 2 ,... ,a i-1 ,a i ,a i+1 ,... ,a n )
成为表长为 n+1 表:
(a 1 ,a 2 ,...,a i-1 ,x,a i ,a i+1 ,...,a n ) 。
顺序表上完成这一运算则通过以下步骤进行:
① 将a i ~a n  顺序向下移动,为新元素让出位置;(注意数据的移动方向:从后往前依次
后移一个元素)
② 将 x 置入空出的第i个位置;
③ 修改表长。
int Insert_SqList (SqList &L,int i,ElemType x){
if (i < 1 || i > L.length+1) return ERROR; // 插入位置不合法
if (L.length >= L.listsize) return OVERFLOW; // 当前存储空间已满,不能插入
//需注意的是,若是采用动态分配的顺序表,当存储空间已满时也可增加分配
q = &(L.elem[i-1]); // q 指示插入位置
for (p = &(L.elem[L.length-1]); p >= q; --p)
*(p+1) = *p; // 插入位置及之后的元素右移
*q = e; // 插入e
++L.length; // 表长增1
return OK;
}

顺序表上的插入运算,时间主要消耗在了数据的移动上,在第i个位置上插入 x ,从 a i  到
a n  都要向下移动一个位置,共需要移动 n-i+1个元素。
(3)删除运算
线性表的删除运算是指将表中第 i (i 的取值范围为 :1≤ i≤n)个元素从线性表中去掉,
删除后使原表长为 n 的线性表:
(a 1 ,a 2 ,... ,a i-1 ,a i ,a i+1 ,...,a n )
成为表长为 n-1 的线性表:
(a 1 ,a 2 ,... ,a i-1 , a i+1 ,... ,a n )。
顺序表上完成这一运算的步骤如下:
① 将a i+1 ~a n  顺序向上移动;(注意数据的移动方向:从前往后依次前移一个元素)
② 修改表长。
int Delete_SqList (SqList &L;int i) {
if ((i < 1) || (i > L.length)) return ERROR; // 删除位置不合法
p = &(L.elem[i-1]); // p 为被删除元素的位置
e = *p; // 被删除元素的值赋给 e
q = L.elem+L.length-1; // 表尾元素的位置
for (++p; p <= q; ++p)
*(p-1) = *p; // 被删除元素之后的元素左移
--L.length; // 表长减1
return OK;
}

顺序表的删除运算与插入运算相同,其时间主要消耗在了移动表中元素上,删除第i个元
素时,其后面的元素 a i+1 ~a n 都要向上移动一个位置,共移动了 n-i 个元素,

顺序表的插入、删除需移动大量元素 O(n);但在尾端插入、删除效率高 O(1)。


1.2.2  线性表的链式存储结构
1.2.2.1  单链表
1.链表表示
链表是通过一组任意的存储单元来存储线性表中的数据元素的。为建立起数据元素之间
的线性关系,对每个数据元素a i ,除了存放数据元素的自身的信息 a i 之外,还需要和a i 一起

存放其后继 a i+1 所在的存储单元的地址,这两部分信息组成一个“结点”,结点的结构如图所示。

datanext
单链表结点结构

其中,存放数据元素信息的称为数据域,存放其后继地址的称为指针域。因此n个元素的

线性表通过每个结点的指针域拉成了一个“链”,称之为链表。因为每个结点中只有一个指向
后继的指针,所以称其为单链表。
线性表的单链表存储结构C语言描述下:
typedef struct LNode{
ElemType data; // 数据域
struct LNode *next; // 指针域
}LNode,*LinkList;
LinkList L; // L 为单链表的头指针

通常用“头指针”来标识一个单链表,如单链表L、单链表H等,是指某链表的第一个结点
的地址放在了指针变量 L、H 中, 头指针为“NULL”则表示一个空表。
2.单链表上基本运算的实现
(1)建立单链表
●头插法——在链表的头部插入结点建立单链表
链表与顺序表不同,它是一种动态管理的存储结构,链表中的每个结点占用的存储空间
不是预先分配,而是运行时系统根据需求而生成的,因此建立单链表从空表开始,每读入一
个数据元素则申请一个结点,然后插在链表的头部。
LinkList CreateListF ( ){
LinkList L=NULL; //空表
LNode *s;
int x; //设数据元素的类型为int
scanf("%d",&x);
while (x!=flag) {
s= (LNode *) malloc(sizeof(LNode));
s->data=x;
s->next=L; L=s;
scanf ("%d",&x);
}
return L;
}

●尾插法——在单链表的尾部插入结点建立单链表
头插入建立单链表简单,但读入的数据元素的顺序与生成的链表中元素的顺序是相反的,
若希望次序一致,则用尾插入的方法。因为每次是将新结点插入到链表的尾部,所以需加入
一个指针 r 用来始终指向链表中的尾结点,以便能够将新结点插入到链表的尾部。
初始状态,头指针L=NULL,尾指针 r=NULL; 按线性表中元素的顺序依次读入数据元素,
不是结束标志时,申请结点,将新结点插入到 r 所指结点的后面,然后 r 指向新结点(注意
第一个结点有所不同)。
LinkList CreateListR1 ( ){
LinkList L=NULL;
LNode *s,*r=NULL;
int x; //设数据元素的类型为int
scanf("%d",&x);
while (x!=flag) {
s=(LNode *)malloc(sizeof(LNode));
s->data=x;
if (L==NULL) L=s; //第一个结点的处理
else r->next=s; //其它结点的处理
r=s; //r 指向新的尾结点
scanf("%d",&x);
}
if ( r!=NULL) r->next=NULL; //对于非空表,最后结点的指针域放空指针
return L;
}

在算法CreateListR1中,第一个结点的处理和其它结点是不同的,原因是第一个结点加入
时链表为空,它没有直接前驱结点,它的地址就是整个链表的指针,需要放在链表的头指针
变量中;而其它结点有直接前驱结点,其地址放入直接前驱结点的指针域。“第一个结点”的
问题在很多操作中都会遇到,如在链表中插入结点时,将结点插在第一个位置和其它位置是
不同的,在链表中删除结点时,删除第一个结点和其它结点的处理也是不同的,等等。
为了方便操作,有时在链表的头部加入一个“头结点”,头结点的类型与数据结点一致,
标识链表的头指针变量L中存放该结点的地址,这样即使是空表,头指针变量L也不为空了。
头结点的加入使得“第一个结点”的问题不再存在,也使得“空表”和“非空表”的处理成为一致。
头结点的加入完全是为了运算的方便,它的数据域无定义,指针域中存放的是第一个数
据结点的地址,空表时为空。
尾插法建立带头结点的单链表,将算法CreateListR1改写成算法CreateListR2形式。
LinkList CreateListR2( ){
LinkList L=(LNode *)malloc(sizeof(LNode));
L->next=NULL; //空表
LNode *s,*r=L;
int x; //设数据元素的类型为int
scanf("%d",&x);
while (x!=flag) {
s=(LNode *)malloc(sizeof(LNode));
s->data=x;
r->next=s;
r=s; //r 指向新的尾结点
scanf("%d",&x);
}
r->next=NULL;
return L;
}

因此,头结点的加入会带来以下两个优点:
第一个优点:由于开始结点的位置被存放在头结点的指针域中,所以在链表的第一个位
置上的操作就和在表的其它位置上的操作一致,无需进行特殊处理;
第二个优点:无论链表是否为空,其头指针是指向头结点在的非空指针(空表中头结点
的指针域为空),因此空表和非空表的处理也就统一了。
在以后的算法中不加说明则认为单链表是带头结点的。
(2)查找操作
●按序号查找 Get_LinkList(L,i)
从链表的第一个元素结点起,判断当前结点是否是第i个,若是,则返回该结点的指针,
否则继续后一个,表结束为止,没有第i个结点时返回空。
LNode * Get_LinkList(LinkList L, int i); {
LNode * p=L;
int j=0;
while (p->next !=NULL && j<i ){
p=p->next; j++;
}
if (j==i) return p;
else return NULL;
}

(3)插入运算
●后插结点:设p指向单链表中某结点,s指向待插入的值为x的新结点,将*s插入到*p的

后面,插入示意图如图所示。


操作如下:
①s->next=p->next;
②p->next=s;
注意:两个指针的操作顺序不能交换。
(4)删除运算
●删除结点
设p指向单链表中某结点,删除*p。操作过程如图。要实现对结点*p的删除,首先要找到

*p的前驱结点*q,然后完成指针的操作即可。


操作如下:①q=L;
while (q->next!=p)
q=q->next; //找*p的直接前驱
②q->next=p->next;
free(p);
因为找*p前驱的时间复杂度为O(n),所以该操作的时间复杂度为O(n)
通过上面的基本操作我们得知:
(1) 单链表上插入、删除一个结点,必须知道其前驱结点。
(2) 单链表不具有按序号随机访问的特点,只能从头指针开始一个个顺序进行。
1.2.2.2  循环链表
对于单链表而言,最后一个结点的指针域是空指针,如果将该链表头指针置入该指针域,
则使得链表头尾结点相连,就构成了单循环链表。
在单循环链表上的操作基本上与非循环链表相同,只是将原来判断指针是否为 NULL 变
为是否是头指针而已,没有其它较大的变化。
对于单链表只能从头结点开始遍历整个链表,而对于单循环链表则可以从表中任意结点
开始遍历整个链表,不仅如此,有时对链表常做的操作是在表尾、表头进行,此时可以改变
一下链表的标识方法,不用头指针而用一个指向尾结点的指针 R 来标识,可以使得操作效率
得以提高。
1.2.2.3  双向链表
单链表的结点中只有一个指向其后继结点的指针域 next,因此若已知某结点的指针为 p,
其后继结点的指针则为 p->next ,而找其前驱则只能从该链表的头指针开始,顺着各结点的
next 域进行,也就是说找后继的时间性能是 O(1),找前驱的时间性能是 O(n),如果也希望找
前驱的时间性能达到 O(1),则只能付出空间的代价:每个结点再加一个指向前驱的指针域,

结点的结构为如图所示,用这种结点组成的链表称为双向链表。

priordatanext
线性表的双向链表存储结构C语言描述下:
typedef struct DuLNode{
ElemType data;
struct DuLNode *prior,*next;
}DuLNode,*DuLinkList;

和单链表类似,双向链表通常也是用头指针标识,也可以带头结点。
(1)双向链表中结点的插入:设 p 指向双向链表中某结点,s 指向待插入的值为 x 的新结点,
将*s 插入到*p 的前面,插入示意图如所示。
prior  next data
图 双向链表中的结点插入

操作如下:
① s->prior=p->prior;
② p->prior->next=s;
③ s->next=p;
④ p->prior=s;
指针操作的顺序不是唯一的,但也不是任意的,操作①必须要放到操作④的前面完成,
否则*p 的前驱结点的指针就丢掉了。

(2)双向链表中结点的删除:设 p 指向双向链表中某结点,删除*p。操作示意图如图所示。


操作如下:
①p->prior->next=p->next;
②p->next->prior=p->prior;
free(p);
1.2.2.4  顺序表和链表的比较
顺序表                                                     单链表
以地址相邻表示关系                              用指针表示关系
随机访问,取元素 O(1)                        顺序访问,取元素 O(n)
插入、删除需要移动元素 O(n)             插入、删除不用移动元素 O(n)(用于查找位置)


总之,两种存储结构各有长短,选择那一种由实际问题中的主要因素决定。通常“较稳定”
的线性表选择顺序存储,而频繁做插入删除的即动态性较强的线性表宜选择链式存储。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值