3.1 线性表的定义
线性表(list):零个或者多个数据元素的有限序列。
线性表是个序列,其次是强调有限的。
在计算机中处理的对象都是有限的,那种无限的数列,只存在于数学的概念中。
如果用数学语言来进行定义。
若将线性表记为(a1,a2,a3,...,a(i-1),ai,a(i+1),...,an),则表中 a(i-1)领先于 ai,ai领先于a(i+1),称 a(i-1)是 ai 的直接前驱元素,a(i+1)是ai 的直接后继元素。当 i = 1,2,...,n-1时,ai 有且仅有一个直接后继,但i = 2,3,4,...,n时,ai有且仅有一个直接前驱。
所以线性表元素的个数 n (n ≥ 0) 定义为线性表的长度,当 n = 0 时,称为空表。
在非空表中的每个数据元素都有一个确定的位置,如 a1 是第一个数据元素,an是最后一个数据元素,ai 是第 i 个数据元素,称 i 为数据元素 ai 在线性表中的位序。
在较复杂的线性表中,一个数据元素可以由若干个数据项组成。
3.2 线性表的抽象数据类型
线性表的抽象数据类型定义:
ADT 线性表(List)
Data
线性表的数据对象集合为{a1,a2,....,an},每个元素的类型均为 DataType。其中除第一个元素 a1 外,每一个元素有且只有一个直接前驱元素,除了最后一个元素 an 外,每一个元素有且只有一个直接后继元素。数据元素之间的关系是一对一的关系。
Operation
InitList ( *L) : 初始化操作,建立一个空的线性表 L 。
ListEmpty(L) : 若线性表为空,返回true,否则返回 false。
ClearList(*L): 将线性表清空。
GetElem(L , i , *e ): 将线性表L中第 i 个位置元素返回给 e。
LocateElem( L , e ) : 在线性表L中查找与给定值 e 相等的元素,如果查找成功,返回该元素在表中序号表示成功;否则,返回0 表示失败。
ListInsert( *L ,i ,e ): 在线性表 L中的第 i 个位置 插入新元素 e。
ListDelete(*L , i , e ) : 删除线性表 L 中第 i 个位置元素,并用 e 返回其值。
ListLength(L): 返回线性表L 的元素个数。
endADT
对于不同的应用,线性表的基本操作是不同的,上述操作是最基本的,对于实际问题中设计的关于线性表的更复杂的操作,完全可以用这些基本的操作组合来实现。
案例:要实现两个线性表集合 A 和 B 的并集操作。就是把 存在集合B中但不存在A中的数据元素 插入到A中即可。假设La 表示集合A , Lb表示集合 B
/*将所有的在线性表Lb中 但不在La中的数据元素插入到La中*/
void union(List *La , List *Lb){
int La_len , Lb_len , i ;
ElemType e;
La_len = ListLength(La);
Lb_len = ListLength(Lb );
for( i = 1 ; i <= Lb_len ; i++){
GetElem(Lb , i , e ) ;
if( ! LocateElem(La ,e, equal ) ){
ListInsert(La . ++La_len , e);
}
}
}
3.3 线性表的顺序存储结构
3.3.1顺序存储定义
线性表的顺序存储结构,指的是用一段地址连续的存储单元依次存储线性表的数据元素。
3.3.2 顺序存储方式
说白了就是在内存中找了块地方通过占位的方式把一定的内存空间给占了。
线性表的每个数据元素的类型都相同,所以可以用C语言(其他语言也相同)的一维数组来实现顺序存储结构,即把第一个元素存到数组下标为 0 的位置中,接着把线性表相邻的元素存储在数组中相邻的位置。
线性表的顺序存储的结构代码:
#define MAXSIZE 20 /*存储空间初始分配量*/
typedef int ElemType; /*ElemType类型根据实际情况而定,这里假设为int*/
typedef struct{
ElemType data[MAXSIZE]; /*数组存储数据元素,最大值为MAXSIZE*/
int length; /*线性表当前长度*/
}SqList;
我们发现 描述顺序存储结构需要三个属性:
1.存储空间的起始位置:数组data,它的存储位置就是存储空间的存储位置。
2.线性表的最大存储容量:数组长度MAXSIZE;
3.线性表当前长度:length。
3.3.3 数据长度与线性表长度区别
数组的长度是存放线性表的存储空间的长度,存储分配后这个量是一般是不变的。
线性表的长度是线性表中数据元素的个数,随着线性表插入和删除操作的进行,这个量是变化的。
在任意时刻,线性表的长度都应该小于等于数组的长度。
3.3.4 地址计算方法
用数组存储顺序意味着要分配固定长度的数组空间,由于线性表中可以进行插入和删除操作,因此分配的数组空间要大于等于当前线性表的长度。
存储器中的每个存储单元都有自己的编号,这个编号称为地址。
由于每个数据元素,不管他是整型、实型还是字符型,他都是需要占用一定存储单元空间的。
假设占用的是 c 个存储单元,那么线性表中第 i +1 个数据元素的存储位置和第 i 个数据元素的存储位置满足下列关系(LOC表示获得存储位置的函数)。
通过这个公式,可以随时算出线性表中任意位置的地址,不管它是在哪一个位置,都是相同的时间。那么我们对每个线性表位置的存入和取出数据,对于计算机来说都是相同的时间,也就是一个常数,他的时间复杂度 为 O(1) 。我们通常把具有这一特定的存储结构称为 随机存取结构。
3.4 顺序存储结构的插入与删除
3.4.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;
}
3.4.2 插入操作
插入算法的思路:
1.如果插入位置不合理,抛出异常。
2.如果线性表长度大于等于数组长度,则抛出异常或动态增加容量;
3.从最后一个与三俗开始向前遍历到第 i 个位置,分别将它们都往后移动一个位置。
4.将要插入元素填入位置 i 处;
5.表长 加 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;
if ( i < 1 || i > L -> length+1) /* 当 i 不在范围内时 */
return ERROR;
if( i ≤ L -> length ){ /* 若插入数据位置不在表尾 */
for(k = L->length -1 ; k >= i-1; k--){ /* 将要插入位置后数据元素向后移动一位 */
L->data[k+1] = L->data[k];
}
}
L -> data[i-1] = e; /* 将新元素插入 */
L-> length ++;
return OK;
}
原版代码截图
3.4.3 删除操作
删除算法的思路:
1.如果删除位置不合理,抛出异常。
2.取出删除元素。
3.从删除元素位置开始遍历到最后一个元素位置,分别把他们都向前移动一个位置;
4.表长减 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]; /*将删除位置后继元素前移*/
}
}
L->length -- ;
reuturn OK;
}
分析 插入和删除 的时间复杂度
首先,最好的情况,如果元素要插入或者删除都是在最后一个位置发生,此时时间复杂度为O(1),因为不需要移动其他元素。
最坏情况,如果元素要插入到第一个位置或者删除第一个元素,那么这个时间复杂度为O(n)。
3.4.4 线性表顺序存储结构的优缺点
3.5 线性表的链式存储结构
3.5.1 线性表链式存储结构定义
线性表的链式存储结构的特点是用一组任意的存储单元存储线性表的数据元素,这组存储单元可以是连续的,也可以是不连续的。这就意味着,这些数据元素可以存在内存魏碑占用的任意位置。
为了表示每个数据元素ai 与其直接后继元素 a(i+1)之间的逻辑关系,对数据元素a1来说,除了存储其本身的信息之外,还需要存储一个指示其直接后继的信息(即直接后继的存储位置)。我们把存储数据元素信息的域称为 数据域,把存储直接后继位置的域称为 指针域。指针域中存储的信息称作 指针或链。这两部分信息组成数据元素ai 的存储映像,称为 节点(Node)。
n 个节点(ai的存储映像)链结成一个链表,即为线性表(a1,a2,....,an)的链式存储结构,因为此链表的每一个节点中只包含一个指针域,所以叫单链表。单链表正是通过每个节点的指针域将线性表的数据元素按其逻辑次序链接在一起。
对于线性表来说,总有头和尾。我们把链表中第一个节点的存储位置叫做头指针,那么整个链表的存取就必须从头指针开始。最后一个节点,当然就意味着直接后继不存在了,所以我们规定,线性链表的最后一个结点指针为“空”(通常用 NULL 或 “ ^ ” 符号表示 )。
有时,我们为了更加方便的对链表进行操作,会在单链表的第一个结点前附设一个结点,称为头结点。头结点的数据域可以不存储任何信息。也可以存储如线性表长度等附加信息,头结点的指针域存储指向第一个结点的指针。
3.5.2 头指针与头结点的异同
3.5.3 线性表链式存储结构代码描述
1.单链表示意图
2.带有头结点的单链表
3.空链表
单链表中,我们在C语言中可以用结构指针来描述
/* 线性表的单链表存储结构 */
typedef struct Node
{
ElemType data;
struct Node *next;
}Node;
typedef struct Node *LinkList; /*定义LinkList*/
从这个结构定义中,结点是由存放数据元素的数据域和存放后继结点地址的指针域组成。
3.6 单链表的读取
获取链表第 i 个数据的算法思路:
1.声明一个结点 p 指向链表的第一个结点,初始化 j 从1 开始。
2.当 j < i 时,就遍历链表,让 p 的指针向后移动,不断指向下一结点,j 累加 1;
3. 若到链表末尾p 为空,则说明第 i 个元素 不存在。
4.否则查找成功,返回结点p 的数据。
实现代码算法:
/* 初始条件:顺序线性表已存在,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 为计数器 */
while (p && j<i) { /* p 不为空或者计数器 j 还没有等于 i 时,循环继续 */
p = p ->next; /* 让p 指向下一个结点 */
++j;
}
if ( !p || j> i )
return ERROR; /* 第 i 个元素不存在 */
*e = p->data; /* 取第 i 个元素的数据 */
return OK;
}
说白了就是从头开始找,直到找到 i 个元素为止。因此最坏情况的时间复杂度为O(n)。
3.7单链表的插入与删除
3.7.1 单链表的插入
单链表第 i 个数据插入结点的算法思路:
1.声明一结点p 指向链表第一个结点,初始化 j 从1开始。
2.当j < i 时,就遍历链表,让p 的指针向后移动,不断指向下一结点,j 累加 1;
3.若到链表末尾 p 为空,则说明第 i 个元素不存在。
4.否则查找成功,在系统中生成一个空结点 s ;
5.将数据元素 e 赋值 给s ->data;
6.单链表的插入标准语句 s -> next = p ->next;p->next = s;
7.返回成功。
实现代码算法如下:
/* 初始条件:顺序线性表L已存在,1 ≤ i ≤ ListLength(L) */
/* 操作结果:在L中第 i 个位置之前插入新的数据元素e,L的长度加1 */
status ListInsert(LinkList *L , int i ,ElemType e)
{
int j;
LinkList p,s;
p = *L;
j=1;
while ( p && j < i ){ /* 寻找第 i 个结点 */
p = p -> next;
++j;
}
if ( !p || j > i )
return ERROR; /* 第 i 个元素不存在 */
s = (LinkList) malloc(sizeof(Node)); /* 生成新结点(C语言标准函数) */
s -> data = e;
s ->next = p ->next; /* 将p的后继结点赋值给s的后继 */
p ->next =s; /* 将s赋值给p的后继 */
return OK;
}
原版代码截图
3.7.2 单链表的删除
单链表第 i 个数据删除节点的算法思路:
1.声明一结点 p 指向链表第一个结点,初始化 j 从 1 开始。
2.当 j < i 时,就遍历链表,让p 的指针向后移动,不断指向下一个结点, j 累加 1;
3.若到链表末尾 p 为空,则说明第 i 个元素不存在;
4.否则查找成功,将欲删除的结点 p-> next 赋值给q。
5.单链表的删除标准语句 p->next =q->next。
6.将q结点中的数据赋值给e ,作为返回。
7.释放q结点。
8.返回成功。
实现代码算法:
/* 初始条件:顺序线性表L已存在,1 ≤ i ≤ ListLength(L) */
/* 操作结果:删除L的第 i 个数据元素,并用e 返回其值,L长度减 1 */
status ListDelete(LinkList *L,int i,ElemType *e)
{
int j;
LinkList p,q;
p = *L;
j = 1;
while( p->next && j< i ){ /* 遍历寻找第 i 个元素 */
p = p->next;
++j;
}
if ( !(p->next) || j > i ){
return ERROR; /* 第 i 个元素不存在 */
}
q = p->next;
p ->next = q ->next; /* 将q的后继赋值给p 的后继 */
*e = q->data; /* 将q结点中的数据给 e* */
free(q); /* 让系统回收此结点,释放内存 */
return OK;
}
书中原版代码截图
总结分析
从整体上来说,时间复杂度为O(n)。如果我们不知道第 i 个元素的指针位置,单链表数据结构在插入和删除上,与线性表的顺序存储结构上没有太大的优势。但如果我们希望从第 i 个位置上,插入或删除元素,时间复杂度都已变成O(1)。显然,对于插入或删除数据越频繁的操作,单链表的效率优势就越是明显。
3.8 单链表的整表创建
创建单链表的过程就是一个动态生成链表的过程。即从“空表”的初始状态起,依次建立各元素结点,并逐个插入链表。
单链表整表创建的算法思路:
1.声明一结点 p 和计数器变量 i ;
2.初始化一空链表L;
3.让L的头结点的指针指向NULL,即建立一个带头结点的单链表。
4.循环:
a. 生成一新结点赋值给 p;
b. 随机生成一数字赋值给 p 的数据域 p->data;
c.将p 插入到头结点与前一新节点之间
实现代码算法如下:
/* 随机产生 n 个元素的值,建立带表头结点的单链线性表L(头插法) */
void createListHead ( LinkList *L , int n )
{
LinkList p;
int i;
srand(time(0)); /* 初始化随机数种子 */
*L = (LinkList ) malloc(sizeof(Node));
(*L)->next = NULL; /* 先建立一个带头结点的单链表 */
for( i = 0 ; i < n ; i++){
p = ( LinkList ) malloc( sizeof( Node ) ); /* 生成新结点 */
p->data = rand() %100+1; /* 随机生成100以内的数字 */
p->next = (*L)->next;
(*L)->next = p; /* 插入到表头 */
}
}
这个方式始终让新结点在第一的位置上,我们将这种方式称为头插法。事实上,如果我们每次都把新插入的结点放在最后面,这种方式叫做尾插法。
尾插法实现代码算法:
/* 随机产生 n 个元素的值,建立带表头结点的单链线性表L(尾插法) */
void createListTail (LinkList *L ,int n)
{
LinkList p,r;
int i ;
srand(time(0)); /* 初始化随机数种子 */
*L = (LinkList) malloc(sizeof(Node)); /* 为整个线性表 */
r = *L;
for( i = 0; i < n ; i++ ){
p = (Node *) malloc( sizeof(Node) ); /* 生成新结点 */
p->data = rand() %100 +1; /* 随机生成100以内的数字 */
r->next = p; /* 将表尾终端结点的指针指向新结点 */
r = p; /* 将当前的新结点定义为表尾终端结点 */
}
r->next = NULL; /* 表示当前链表结束 */
}
3.9 单链表的整表删除
单链表整表删除的算法思路:
1.声明一结点 p 和q;
2.将第一个结点赋值给p ;
3循环:
a.将下一结点赋值给q;
b.释放p;
c.将q赋值给p;
实现代码算法如下:
/* 初始条件:顺序线性表L已存在,操作结果:将L重置为空表 */
status clearList(LinkList *L)
{
LinkList p,q;
p = (*L) ->next; /* p 指向第一个结点 */
while( p ){ /* 没到表尾 */
q = p->next;
free(p);
p = q ;
}
(*L) -> next = NULL; /* 头结点指针域为空 */
return OK;
}
q变量存在的意义:p 是一个结点,除了有数据域还有指针域。你在做free(p) 时,其实是对他整个结点进行删除和内存释放的工作。
3.10 单链表结构和顺序存储结构优缺点
经验性结论:
1.若线性表需要频繁查找,很少进行插入和删除操作时,宜采用顺序存储结构。若需要频繁插入和删除时,宜采用单链表结构。
2.当线性表中的元素个数变化较大或者根本不知道有多大时,最好采用单链表结构,这样可以不需要考虑存储空间的大小问题。而如果事先知道线性表的大致长度,顺序存储结构效率会更高。
3.11 静态链表
首先我们让数组的元素都是由两个数据域组成,data和cur。也就是说,数组的每个下标都对应一个data和一个cur。数据域data,用来存放数据元素,也就是通常我们要处理的数据;而游标cur相当于单链表中的next指针,存放该元素的后继在数组汇总的下标。
这种用数组描述的链表叫做静态链表,这种描述方法还有起名叫做游标实现法。
/* 线性表的静态链表存储结构 */
#define MAXSIZE 1000 /* 假设链表的最大长度是1000 */
typedef struct
/*对于不提供结构struct的程序设计语言,可以使用一对并行数组 data 和 cur 来处理*/
{
ElemType data;
int cur; /* 游标(cursor) ,为0时表示无指向 */
}Component,StaticLinkList[MAXSIZE];
另外我们对数组第一个和最后一个元素作为特殊元素处理,不存数据。我们通常把未被使用的数组元素称为备用链表。而数组第一个元素,即下标为0的元素的cur就存放备用链表的第一个结点的下标;而数组的最后一个元素的cur 则存放第一个有数值的元素的下标,相当于单链表中的头结点作用,当整个链表为空时,则为 0。
/* 有些书中把数组的第二个元素用来作为头结点,实现原理相同,只不过是取得存放位置不同 */
/* 将一维数组space中各分量链成一备用链表 */
/* space[0].cur 为头指针,“0”表示空指针 */
status initList(staticLinkList space)
{
int i;
for( i = 0 ;i < MAXSIZE-1; i++ ){
space[ i ].cur = i+1;
}
space[MAXSZIE - 1].cur = 0; /* 目前静态链表为空,最后一个元素的cur为0 */
return OK;
}
3.11.1 静态链表的插入操作
/* 若备用空间链表非空,则返回分配的结点下标,否则返回 0 */
int Malloc_SLL(staticLinlList space)
{
int i = space[ 0 ].cur; /* 当前数组第一个元素的cur存的值,*/
/*就是要返回的第一个备用空间的下标 */
if( space[0].cur ){
space[0].cur = space[i].cur; /* 由于要拿出一个分量来使用,所以我们*/
/*就得把他的下一个分量用来做备用 */
}
return i;
}
案例解释
/* 在L中第 i 个元素之前插入新的数据元素e */
/*左侧数组为行号*/
1. status listInsert(statucLinkList L, int i , ElemType e)
2. {
3. int j , k , l ;
4. k = MAX_SIZE -1; /*注意k 首先是最后一个元素的下标*/
5. if ( i <1 || i > ListLength(L) +1 )
6. return ERROR;
7. j = Malloc_SSL(L) ; /* 获得空闲分量的下标 */
8. if ( j )
9. {
10. L[ j ].data = e; /* 将数据赋值给此分量的data */
11. for ( l = 1; l < i-1 ; l++)
12. k = L[ k ].cur;
13. L[ j ].cur = L[ k ].cur; /* 把第 i 个元素之前的cur赋值给新元素的cur */
14. L[ k ].cur = j; /* 把新元素的下标赋值给第 i 个元素之前元素的cur */
15. return ok;
16. }
17. return ERROR;
18. }
当我们执行插入语句时,我们的目的是要在“乙”和“丁”之间插入“丙”。调用代码时,输入i 的值为3.
第4行让 k = MAX_SIZE - 1=999.
第7行, j = Malloc_SSL(L) = 7 。此时下标为 0 的cur 也因为7要被占用而更改备用链表的值为8;
第11 、 12 行,for循环 l 由1到2,执行两次。代码 k = L[k].cur;使得k=999,得到k=L[999].cur =1.再得到k = L[ 1 ].cur =2.
第13行,L[ j ].cur = L[ k ].cur;;因j = 7,而k = 2得到L[ 7 ].cur = L[ 2].cur =3.这就是刚才我说让“丙”把他的cur改为3的意思。
第14行, L[ k ].cur = j;意思就是L[ 2].cur = 7也就是让“乙”得点好处,把他的cur改为指向“丙”的下标7.
3.11.2 静态链表的删除操作
/* 删除在L中第 i 个数据元素 e */
status listDelete( staticList L; int i )
{
int j,k;
if ( i <1 || i > ListLength(L) )
return ERROR;
k = MAX_SIZE -1 ;
for ( j = 1; j <= i -1; j ++ )
k = L[ k ].cur;
j = L[ k ].cur;
L[ k ].cur = L[ j ].cur;
Free_SSL(L , j );
return OK;
}
void Free_SSL(staticLinkList space, int k)
{
space[ k ].cur =space[ 0 ].cur; /* 把第一个元素cur值赋给要删除的分量cur */
space[ 0 ].cur = k; /* 把要删除的分量下标赋值给第一个元素的cur */
}
int ListLength(StaticLinkList L)
{
int j =0;
int i = L[ MAXISZIE-1 ].cur;
while( i ){
i = L[ i ].cur;
j ++;
}
return j;
}
3.11.3 静态链表优缺点
3.12 循环链表
将单链表中终端结点的指针端由空指针改为指向头结点,就使整个单链表形成一个环,这种头尾相接的单链表称为单循环链表,简称循环链表。
循环链表带有头结点的空链表
非空的循环链表
循环链表和单链表的主要差异就在于循环的判断条件上,原来是判断 p->next是否为空,现在则是 p->next不是头结点,则循环未结束。
3.13 双向链表
双向链表实在单链表的每个节点中,在设置一个指向其前驱节点的指针域。所以在双向链表中的结点都有两个指针域,一个指向直接后继,一个指向直接前驱。
/* 线性表的双向链表存储结构 */
typedef struct DulNode
{
ElemType data;
struct DulNode *prior; /* 直接前驱指针 */
struct DulNode *next; /* 直接后继指针 */
}DulNode,* DuLinkList;
双向链表的循环贷头结点的空链表
非空的循环的带头结点的双向链表
3.14 总结回顾
1.线性表的定义:线性表是零个或者多个具有相同类型的数据元素的有限序列。
2.线性表的两大结构