文章目录
一、定义和特点
逻辑上的线性关系罢了
定义:由
n
n
n(
n
n
n>=0)个数据特性相同的元素构成的有限序列
特点:
- 存在唯一一个被称为“第一个”的数据元素
- 存在唯一一个被称为“最后一个”的数据元素
- 除第一个元素外,每个数据元素均只有一个前驱
- 除最后一个元素外,每个数据元素均只有一个后驱
二、线性表的顺序表示和实现
2.1 线性表的顺序表示
顺序表:用一组地址连续的存储单位依次存储的线性表(Sequential List)
逻辑上相邻,物理地址也相邻。因此可以随机存取
顺序表的存储结构:
#define MAXSIZE 100
typedef struct
{
ElemType *elem;
int length;
}SqList;
//变量的定义语句
SqList L;
这里的指针elem通常指向一个大小为MAXSIZE的数组的基地址
由此可见,想要访问第i个数据(一般序号1为第一个数据),则可利用L.elem[i-1]来访问
【eg:多项式的顺序存储结构】
#define MAXSIZE 100
typedef struct
{
float coef;//系数
int expn;//指数
}Polynomial;
typedef struct
{
polynomial *elem;//存储空间的基地址
int length;//多项式中当前项的个数
}SqList;
2.2 顺序表中基本操作的实现
1.初始化
【算法步骤】
① 将顺序表L动态分配一个预定义大小的数组空间,使elem指向这段空间的基地址
② 将表的当前长度设为0
【算法描述】:
Status InitList(SqList &L)
{
L.elem=new ElemType[MAXSIZE];//分配空间
if(!L.elem) exit(OVERFLOW);//存储分配失败退出
L.length=0;//空表长度为0
return OK;
}
2.取值
【算法步骤】
① 判断指定位置序号i是否合理(1<=i<=L.length)
② 若i合理,则将第i个数据元素L.elem[i-1]赋给参数e来取值
【算法描述】
Status GetList(SqList L,int i,ElemType &e)
{
if(i<1||i>L.length) return ERROR;//判断i值是否合理
e=L.elem[i-1];//取值
return OK;
}
3.查找
【算法步骤】
①从第一个开始查找,直到找到和e相同的元素L.elem[i],则查找成功,返回该元素的序号i+1
②若查遍所有元素都没有,则查找失败
【算法描述】
int LocateElem(SqList L,ElemType e)
{
for(i=0;i<L.length;i++)
if(L.elem[i]==e) return i+1;
return 0;
}
【算法分析】
假设每个元素的查找概率相同,那么平均查找长度(Average Search Length,ASL)1
A
S
L
=
1
n
∑
i
=
1
n
i
=
n
+
1
2
ASL=\frac{1}{n} \sum_{i=1}^ni=\frac{n+1}{2}
ASL=n1i=1∑ni=2n+1
所以平均时间复杂度为
O
(
n
)
O(n)
O(n)
4.插入
【算法步骤】
以1为第一个序号
①判断插入位置i是否合法(1<=i<=L.length+1)
②判断顺序表的存储空间是否已满
③将最后一个至第i个位置的元素依次向后移动一位(i=L.length+1时无须移动)
④将要插入的新元素e放入第i个位置
⑤表长加1
【算法描述】
Status ListInsert(SqList &L,int i,ElemType 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;//将新元素e放入第i个位置
++L.length;//表长加1
return OK;
}
【算法分析】
假设每个元素的查找概率相同,那么平均要移动的次数2为
E
i
n
s
=
1
n
+
1
∑
i
=
1
n
+
1
(
n
−
i
+
1
)
=
n
2
E_{ins}=\frac{1}{n+1} \sum_{i=1}^{n+1}(n-i+1)=\frac{n}{2}
Eins=n+11i=1∑n+1(n−i+1)=2n
由此可见,顺序表插入算法的平均时间复杂度为
O
(
n
)
O(n)
O(n)
5.删除
【算法步骤】
以1为第一个序号
①判断删除位置i是否合法(1<=i<=n)
②将第i+1个至第n个元素依次向前移动一个位置(i=n时无须移动)
③表长减1
【算法描述】
Status ListDelete(SqList &L,int i)
{
if((i<1)||(i>L.length)) return ERROR;//判断i值是否合法
for(j=i;j<=L.length-1;j++)
L.elem[j-1]=L.elem[j];//被删除元素之后的元素前移
--L.length;//表长减1
return OK;
}
【算法分析】
假设每个元素的查找概率相同,那么平均要移动的次数3为
E
d
e
l
=
1
n
∑
i
=
1
n
(
n
−
i
)
=
n
−
1
2
E_{del}=\frac{1}{n} \sum_{i=1}^{n}(n-i)=\frac{n-1}{2}
Edel=n1i=1∑n(n−i)=2n−1
由此可见,顺序表插入算法的平均时间复杂度为
O
(
n
)
O(n)
O(n)
三、链式表示和实现
3.1单链表
3.1.1定义及表示
定义:由有限个包含数据域和指针域的节点链接而成的链表叫做线性链表或者单链表
存储结构:
typedef struct LNode
{
ElemType data;//节点的数据域
struct LNode *next;//节点的指针域
}LNode,*LinkList;//LinkList为指向结构体LNode的指针类型
LinkList和LNode * 本质上等价,但前者多强调定义头指针,后者多用来表示指向任意节点的指针
头节点是被头指针指向的节点(如果存在),头节点的指向的节点称为首元节点
不设头节点时,判断空表用L==NULL;设头节点时,判断空表用L->next==NULL
3.1.2基本操作的实现
1.初始化
【算法步骤】
①生成新节点作为头节点,用头指针L指向头节点
②头节点的指针域置空
Status InitList(LinkList &L)
{
L=new LNode;//生成新节点作为头节点,用头指针L指向头节点
L->next=NULL;//头节点的指针域置空
return OK;
}
注意参数表中的“&”,没有它,那函数做的操作都只是对L的镜像生效,L不会有变化
2.取值
【算法步骤】
①用指针p指向首元节点,用j做计算器初值赋为1
②从首元节点开始依次顺着链域next向下访问,只要指向当前节点的指针p不为空(NULL),并且没有到达序号为i的节点,则循环执行以下操作:
- p指向下一个节点;
- 计算器j相应加1。
③推出循环时,如果指针p为空,或者计数器j大于i,说明指定的序号i值不合法,取值失败;否则取值成功,此时j=i时,p所指的节点就是要找的第i个节点,用参数e保存当前节点的数据域。
【算法描述】
Status GetElem(LinkList L,int i,ElemType &e)
{
p=L->next;j=1;//初始化,p指向首元节点,计数器j初值赋为1
while(p&&j<i)//顺链域向后查找,知道p为空或者p指向第i个元素
{
p=p->next;//p指向下一个节点
j++;//计数器j相应加1
}
if(!p||j>i) return ERROR;//i值不合法i>n或i<=0
e=p->data;//取第i个节点的数据域
return OK;
}
【算法分析】
当i合法时候,搜索i-1次必定成功;若i>n(n为链表所有的节点数),则要搜索n次,所以最坏的时间复杂度为O(n)。
假设每个位置上的元素取值概率相同,则:
A
S
L
=
1
n
∑
i
=
1
n
(
i
−
1
)
=
n
−
1
2
ASL=\frac{1}{n} \sum_{i=1}^n(i-1)=\frac{n-1}{2}
ASL=n1i=1∑n(i−1)=2n−1
由此可见,单链表取值算法的平均时间复杂度为O(n)。
3.查找
【算法步骤】
①用指针p指向首元节点
②从首元节点开始顺着链域依次往下找,只要指针p没有指空,并且p指向的节点的数据域不等于给定值,则继续沿着链域向下查找节点
③返回p。若成功,p此时指向节点的地址值;若失败,p为NULL
【算法描述】
LNode *LocateElem(LinkList L,ElemType e)
{
p=L->next;//初始化,p指向首元节点
while(p&&p->data!=e)//顺链域查找,直到指针为空或者找到目标值
p=p->next;//p指向下一个节点
return p;//若成功则返回e值节点的地址,若失败则返回空值
}
【算法分析】
因为也要顺链域一个个查找,所以平均时间复杂度类似于链表取值,为O(n)。
4.插入
【算法步骤】
将值为e的新节点插入表的第i个节点的位置,即插入第i-1个节点和第i个节点中(这样新节点才能是第i个)
①查找节点ai-1并将指针p指向该节点
②生成一个新节点,并用指针s指向它
③将新节点的数据域置为e
④将新节点的指针域指向节点ai
⑤将p指向的节点的指针域指向新节点
【算法描述】
n表示链表节点总数,这对于计算机是不知道的
Status ListInsert(LinkList &L,int i,ElemType e)
{//在带头节点的单链表L中第i个位置插入值为e的新节点
p=L;j=0;
while(p&&(j<i-1))
{p=p->next;j++;}//查找第i-1个节点,p指向该节点
if(!p||j>i-1) return ERROR;//i>n+1或者i<1
s=new LNode;//生成新节点*s
s->data=e;//将节点*s的数据域置为e
s->next=p->next;//将节点*s的指针域指向第i个节点
p->next=s;//将节点*p的指针域指向节点*s
return OK;
}
【算法分析】
因为每次查找第i个节点都要查看前面的i-1个节点,原理同取值,所以平均时间复杂度也是O(n)。
5.删除
【算法步骤】
①查找节点ai-1并由指针p指向该节点
②临时保存待删除节点ai的地址在q中,以备释放
③将节点*p的指针域指向ai的直接后继节点
④释放节点ai的空间
【算法描述】
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;//临时保存被删节点的地址以备释放
p->next=q->next;//改变删除节点前驱节点的指针域
delete q;//释放删除节点的空间
return OK;
}
【算法分析】
类似于插入算法,删除算法时间复杂度亦为O(n)。
6.创建单链表
(1)前插法
【算法步骤】
就是疯狂在头节点后插入新节点
【算法描述】
void CreateList_H(LinkList &L,int n)
{//逆位序输入n个元素的值,建立带表头节点的单链表L
L=new LNode;
L->next=NULL;//先建立一个带头节点的空链表
for(i=0;i<n;i++)
{
p=new LNode;//生成新节点*p
cin>>p->data;//输入元素值赋给新节点*p的数据域
p->next=L->next;L->next=p;//将新节点*p插入到头节点之后
}
}
【算法分析】
因为要插入n次所以时间复杂度是O(n)。
(2)后插法
【算法步骤】
就是疯狂插到链表队尾,因此需要一个尾指针来确定最后一个节点的位置
【算法描述】
void CreateList_R(LinkList &L,int n)
{//正位序输入n个元素的值,建立带表头节点的单链表L
L=new LNode;
L->next=NULL;//先建立一个带头节点的空链表
r=L;//尾指针r指向头节点
for(i=0;i<n;i++)
{
p=new LNode;//生成新节点*p
cin>>p->data;//输入元素值赋给新节点*p的数据域
p->next=NULL;r->next=p;//将新节点*p插入尾节点*r之后
r=p;//r指向新的尾节点*p
}
}
【算法分析】
因为要插入n次所以时间复杂度是O(n)。
3.2循环链表
故名思意,这种链表的尾节点的指针域是指向头节点的。因此,判断链表是否遍历完的条件是p!=L或p->next!=L。
当存在两个循环链表需要合并时(重复不需要重叠的情况),要削去一个头节点。具体做法如下:
假设A指针是A链表的尾指针,B指针是B链表的尾指针
- p=B->next->next;//保存B链表的首元节点的地址
- B->next=A->next;//将B链表的尾部接到A链表的头节点上
- A->next=p;//将A链表接到B链表的首元节点的同时削去了B链表的头节点
3.3双向链表
3.3.1定义及表示
定义:由拥有两个指针域,一个指向直接前驱,一个指向直接后继的有限个节点组成的链表叫双向链表
存储结构:
typedef struct DULNode
{
ElemType data;//节点的数据域
struct LNode *prior//指向直接前驱
struct LNode *next;//指向直接后继
}DULNode,*DULinkList;
3.3.2基本操作的实现
在仅需涉及一个方向的指针的情况下,双向链表的算法和线性链表相似,但在插入、删除时有很大不同
1.双向链表的插入
将新节点插入i位置,这里知道i-1或者i的位置都行,因为是双向的。因为我们有现成的函数找位置,所以这里用i的位置
【算法描述】
Status ListInsert_Dul(DulinkList &L,int i,ElemType e)
{
if(!(p=GetElem_Dul(L,i)))
return ERROR;
s=new DulNode;
s->data=e;
s->prior=p->prior;
s->next=p;
p->prior->next=s;
p->prior=s;
return OK;
}
在确定i位置的情况下,倒数三行的顺序是不能换的,如果换了,*p节点的前驱就丢失了
2.双向链表的删除
因可以查到前驱,所以我们可以直接定位到i位置,而不用定位到i-1位置
【算法描述】
Status ListDelete_Dul(DuLinkList &L,int i)
{
if(!(p=GetElem_Dul(L,i)))
return ERROR;
p->prior->next=p->next;
p->next->prior=p->prior;
delete p;
return OK;
}
这里的顺序是可以改变的,因为p->prior和p->next都没有被改变
四、顺序表和链表的比较
比较项目 | 顺序表 | 链表 |
---|---|---|
存储空间 | 预先分配,会出现空间闲置或者溢出的现象 | 动态分配,不会出现存储空间闲置或者溢出现象 |
存储密度3 | 不用为表示节点间的逻辑关系而增加额外的存储开销,存储密度等于1 | 需要借助指针来体现元素间的逻辑关系,存储密度小于1 |
存取元素 | 随机存取,按位置访问元素的时间复杂度为O(1) | 顺序存取,按位置访问元素时间复杂度O(n) |
插入、删除 | 平均移动约表中一半元素,时间复杂度为O(n) | 不需移动元素,确定插入、删除位置后,时间复杂度为O(1) |
适用情况 | ①表长变化不大,且能事先确定变化的范围 ②很少进行插入或者删除操作,经常按元素位置序号访问数据元素 | ①长度变化较大②频繁进行插入或者删除操作 |
五、线性表的应用
1.线性表的合并
【例1】已知两个集合A和B,现要求一个新的结合A=A∪B。
【算法描述】
void MergeList(List &LA,List LB)
{
m=ListLength(LA);n=ListLength(LB);//求线性表的长度
for(i=1;i<=n;i++)
{
GetElem(LB,i,e);//去LB中第i个数据元素赋给e
if(!LocateElem(LA,e))//LA中不存在和e相同的数据元素
ListInsert(LA,++m,e);//将e插在LA的最后
}
}
2.有序表的合并
【例2】已知两个有序集合A和B(顺序由小到大),现求新集合C=A∪B,使C中的元素也是从小到大排列
【算法描述一|顺序表】
void MergeList_Sq(SqList LA,SqList LB,SqList &LC)
{
LC.length=LA.length+LB.length;//定新表长度
LC.elem=new ElemType[LC.length];//分配新空间
pc=LC.elem;//指针pc指向新表的第一个元素
pa=LA.elem;pb=LB.elem;//指针pa和pb的初值分别指向两表的第一个元素
pa_last=LA.elem+LA.length-1;//指针pa_last指向LA的最后一个元素
pb_last=LB.elem+LB.length-1;//指针pb_last指向LB的最后一个元素
while((pa<=pa_last)&&(pb<=pb_last))//依次摘取量表中较小的节点插入LC的最后
{
if(*pa<=*pb) *pc++=*pa++;
else *pc++=*pb++;
}
while(pa<=pa_last) *pc++=*pa++;//已到达LB表尾,依次将LA的剩余元素插入LC的最后
while(pb<=pb_last) *pc++=*pb++;//已到达LA表尾,依次将LB的剩余元素插入LC的最后
}
【算法描述二|链表】
void MergeList_L(LinkList &LA,LinkList &LB,LinkList &LC)
{
pa=LA->next;pb=LB->next;//pa和pb的初值分别指向两个表的第一个节点
LC=LA;//用LA的头节点作为LC的头节点
pc=LC;//pc的初值指向LC的头节点
while(pa&&pb)
{//LA和LB均未达到表尾,依次“摘取”两表中值较小的节点插入到LC的最后
if(pa->data<=pb->data)//摘取pa所指节点
{
pc->next=pa;
pc=pa;
pa=pa->next;
}
else//摘取pb所指节点
{
pc->next=pb;
pc=pb;
pb=pb->next;
}
}
pc->next=pa?pa:pb;//将非空表的剩余段插入到pc所指节点之后
delete LB;//释放LB的头节点
}
六、案例分析与实现
1.一元多项式的运算
2.稀释多项式的运算
在创建多项式的时候,要共要两个指针,一个在前一个在后的。在前的记录前驱,方便插入;在后的比较指数大小。每插入一个新节点都要从头检查一遍
相加时的操作和有序表的合并很像,需要两个指针来标记待相加的两个式子,一个指针用来确定链接哪个节点(用来记录结果的前驱),最终结果会以一开始的两个式子的头节点为自己的头节点,另一个则释放
3.图书馆信息管理系统
总结
学完本章后,应该熟练掌握顺序表和链表的查找、插入和删除算法,链表的创建算法,并能够设计除线性表应用的常用算法,比如线性表的合并等;能够从时间和空间复杂度的角度比较两种存储结构的不同特点及其适应场合,明确它们各自的优缺点。
pi为查找第i个元素的概率,Ci为找到相同值时已进行过比较的数据的个数 A S L = ∑ i = 1 n p i C i ASL=\sum_{i=1}^np_{i}C_{i} ASL=i=1∑npiCi ↩︎
在长度为n的线性表中插入一个元素平均要移动元素次数为
E i n s = ∑ i = 1 n + 1 p i ( n − i + 1 ) E_{ins}=\sum_{i=1}^{n+1}p_{i}(n-i+1) Eins=i=1∑n+1pi(n−i+1) ↩︎存储密度 = 数据元素本身占用的存储量 节点结构占用的存储量 存储密度=\frac{数据元素本身占用的存储量}{节点结构占用的存储量} 存储密度=节点结构占用的存储量数据元素本身占用的存储量 ↩︎ ↩︎