第二章—线性表
1.定义与特点
线性表是具有“相同特性”的数据元素的一个“有限”序列。是一种典型的线性结构。
特点:
*数据元素的个数是有限的
*一个线性表如果没有数据元素称为“空表”
*每个数据元素称为结点,数据元素的个数称为表的长度。
*有且只有一个初始结点(排第一位),有且只有一个终端结点(最后一个)
*每个结点只有一个前驱,一个后继。且初始结点没有前驱,终端结点没有后继。
例子1:
(
a
1
,
a
2
…
…
a
i
−
1
,
a
i
,
a
i
+
1
,
…
…
a
n
)
(a_1,a_2……a_{i-1},a_i,a_{i+1},……a_n)
(a1,a2……ai−1,ai,ai+1,……an)
这里的a₁是起始结点,它只有一个后继结点a₂,没有前驱。an是终端结点,它只有一个前驱,没有后继。
当n=0时,为空表。
2.抽象线性表类型定义
抽象数据类型线性表的定义
ADT List{
数据对象:D = {ai | ai∈ElemSet,(i=1,2,3……n,n≥0)}
数据关系:R = {<a_{i-1},a_i> | a_{i-1},a_i∈D,(i=2,3,……,n)} //a_{i-1}表示下标为i-1的元素a;a_i表示下标为i的元素a
基本操作:
InitList(&L); //英文单词:Initialization List
操作结果:构造一个空的线性表L。
DestroyList(&L);
初始条件:线性表L已存在。
操作结果:销毁线性表L。
ClearList(&L);
初始条件:线性表L已存在。
操作结果:将线性表L重置为空表。
ListEmpty(L);
初始条件:线性表L已存在。
操作结果:判断L是否为空表,是返回true,否返回false。
ListLength(L);
初始条件:线性表L已存在。
操作结果:返回L中数据元素个数
GetElem(L,i,&e);
初始条件:线性表L已存在。
操作结果:用e返回线性表L中第i个数据元素的值。
LocateElem(L,e);
初始条件:线性表L已存在。
操作结果:返回与e值相同元素在L中的位置,如果不存在返回0。
PriorElem(L,cur_e,&pre_e);
初始条件:线性表L已存在。
操作结果:若cur_e是L数据元素且不是第一个,则用pre_e返回它的前驱;如果操作失败前驱无定义。
NextElem(L,cur_e,&next_e);
初始条件:线性表L已存在。
操作结果:若cur_e是L数据元素且不是最后一个,则用next_返回它的后继;如果操作失败后继无定义。
ListInsert(&L,i,e);
初始条件:线性表L已存在,且1≤i≤ListLength(L)+1。
操作结果:在L中第i个位置插入新的数据元素e,L长度加1。
ListDelete(&L,i,&e);
初始条件:线性表L已存在,且1≤i≤ListLength(L)
操作结果:删除L中第i个位置元素,将值返回给e,L长度减1。
TraverseList(L);
初始条件:线性表L已存在。
操作结果:对线性表L进行遍历。遍历过程中对L每个结点访问一次。
}ADT List
3.线性表的顺序存储(顺序表)
1.定义
把逻辑上相邻的数据元素存储在物理上相邻的存储单元中的存储结构。线性表顺序存储结构占用“一片连续”的存储空间。
优点:任一元素可以“随机存取”。
例如:线性表(1,2,3,4,5,6)的存储结构
a[0] | a[1] | a[2] | a[3] | a[4] |
---|---|---|---|---|
1 | 2 | 3 | 4 | 5 |
这样存储起来,是一个典型的线性表顺序存储结构。也就是地址连续,没有空出存储单元。
注:计算机存储的下标是从0开始。在学习的过程中需要区别注意。
使用这种顺序存储结构,若知道某个元素的存储位置,就可以计算出其他元素的存储位置。
例如:
a_1 | a_2 | …… | a_{i-1} | a_i | a_{i+1} | …… | a_n |
---|
注:a_{i-1}表示a下标为i-1。a_1也就是首个地址称为"基地址"。
如果线性表每个元素需要占用L个存储单元,则第i+1个数据元素的存储位置和第i个元素的存储关系:
L
O
C
(
a
i
+
1
)
=
L
O
C
(
a
i
)
+
L
LOC(a_{i+1}) = LOC(a_i)+L
LOC(ai+1)=LOC(ai)+L
假设a_i存储位置是2000单元,每个元素占用8个存储单元,则a_{i+1}的存储位置为“2000+8=2008”单元。
由上面的关系可推出:
L
O
C
(
a
i
+
1
)
=
L
O
C
(
a
1
)
+
(
i
−
1
)
L
LOC(a_{i+1}) = LOC(a_1)+(i-1)L
LOC(ai+1)=LOC(a1)+(i−1)L
线性表顺序存储结构图示:
内存状态 | a_1 | a_2 | …… | a_i | …… | a_n |
---|---|---|---|---|---|---|
存储地址 | LOC(a_1) | LOC(a_1)+L | …… | LOC(a_1)+(i-1)L | …… | LOC(a_1)+(n-1)L |
2.实现
2.1.“类C语言”有关操作补充
类C语言属于"伪代码",不在意是否能运行,只在意在学习数据结构中的思路
ElemType data[];//这里的ElemType指的是数据类型,也就是需要什么类型直接填充。
//例如
char data[];
int data[];
//或者使用typedef定义某个类型
//例如
typedef char ElemType;
ElemType data[]; //这样这里的ElemType就指的是char类型。
//---分割线--
typedef int ElemType;
ElemType data[];//这样这里的ElemType指的是int类型。以此类推
若一个数据元素是一个复杂类型
//例如:存储多项式,需要存储系数P和指数e
typedef struct{
float p; //定义了系数
int e; //定义了指数
}Polynomial;//把这个定义好的复杂的数据类型命名为Polynomial
//接下来就可以直接定义这个复杂类型了
Polynomial *elem; //这里的*elem作为一个指针,相当于定义这个复杂类型的变量的基地址。
数组定义:静态分配和动态分配的区别
//数组静态分配
typedef struct{
ElemType data[MAXSIZE]; //直接定义了一整块存储空间,MAXSIZE为数组最大存储个数。
int length;
}SqList;
//数组动态分配
typedef struct{
ElemType *data; //这里定义了一个基地址,可以理解是data[0]的地址
int length;
}SqList;
//这种动态分配,定义的时候分配内存语句
SqList L;
L.data = (ElemType*)malloc(sizeof(ElemType)*MAXSIZE);
关于内存分配函数:
//需要加载头文件:<stdlib.h>
#include <stdilb.h>
malloc(m); //开辟m字节长度的地址空间,并返回这段空间的首地址。
sizeof(x); //x可以是变量,也可以是数据类型,计算x的长度。
free(p); //释放指针p所指变量的存储空间,即彻底删除这个变量。
//动态分配内存语句
L.data = (ElemType*) malloc(sizeof(ElemType)*MAXSIZE);
C++的动态分配:
//分配内存:new 类型名(初始值列表)
//例如
int *p1 = new int;
//或者
int *p1 = new int(10);
//释放内存:delete 指针P,P必须是new操作的返回值
//例如上面定义的p1
delete p1;
参数传递(以C++语法为描述):
*传值方式:参数传递后只是传了参数的值,不会改变原来参数的值。
*传引用方式:这里C++可以参数的"指针变量"或"引用类型"或"数组名",根据函数调用后原来参数的值也会改变。函数直接对传入的变量进行操作。
//传值方式:把实参的值传送给函数局部工作区的副本中,函数修改的是副本的值,实参的值不会变。
#include <iostream.h>
void swap(float m, float n){ //这里函数的意思是把传入的m和n的对应的值进行调换
float temp;
temp = m;
m = n;
n = temp;
}
void main(){
float a,b;
cin>>a>>b;
swap(a,b); //调用上面函数后,a和b原来的值不会变。
cout<<a<<endl<<b<<endl;
}
//传地址(指针变量做参数):形参变化影响实参。也就是传入的变量对着函数调用也会相应的变化。
//a,b的值发生变化写法
#include <iostream.h>
void swap(float *m,float *n){//这里函数的意思是把传入的m和n的对应的值进行调换
float t;
t=*m;
*m = *n;
*n = t;
}
void main(){
float a,b,*p1,*p2;
cin>>a>>b;
p1=&a;p2=&b;
swap(p1,p2);//这里经过调用上面函数后,a和b的值随着函数调用后交换了(也就是值变了)。
cont<<a<<endl<<b<<endl;
}
//以数组名作参数
#include<iostream.h>
void sub(char b[]){//这里函数的意识是把传入的数组值换成"world"
b[]="world";
}
void main(){
char a[10] = "hello";
sub(a); //这里a数组的值变为了"world"
cout<<a<<endl;
}
2.2. 线性表的顺序存储定义
在C语言中,使用一维数组定义没法对数组大小作动态定义。所以用一个变量来表示顺序表的长度属性(也就是已存储的个数)。
使用C语言实现对顺序表的定义:
模板一(静态数组定义):
#define MAXSIZE 100 //定义了一个常量,用作存储空间初始分配量
typedef struct{
ElemType elem[MAXSIZE]; //定义了数组,ElemType表示数据类型
int length; //当前长度
}SqList;
模板二(动态数组定义):
#define MAXSIZE 100
typedef struct{
ElemType *elem; //基地址(首个元素的地址使用指针表示)
int length;
}SqList;
SqList L;
L.elem = (ElemType *)malloc(sizeof(ElemType) * MAXSIZE);
例子一:存储一个多项式
P
n
(
x
)
=
P
1
x
e
1
+
P
2
x
e
2
+
…
…
+
P
m
x
e
m
P_n(x) = P_1x^{e_1} + P_2x^{e_2}+……+P_mx^{e_m}
Pn(x)=P1xe1+P2xe2+……+Pmxem
这里存储系数P以及指数e
#define MAXSIZE 100 //定义一个常量作为存储空间初始分配量
//将二项式定义为一个类型
typedef struct{ //多项式非零项的类型定义
float p; //系数
int e; //指数
}Polynomial;
//定义存储类型
typedef struct{
Polynomial *elem; //定义二项式存储空间的基地址。
int length; //定义当前已经存储的个数,即长度。
}SqList;
例子二:图书表的顺序存储结构,图书信息包括ISBN,书名,定价三个属性。
#define MAXSIZE 10000 //定义一个常量作为存储空间初始分配量
//图书信息定义
typedef struct{
char ISBN[20]; //图书的ISBN
char name[50]; //图书的名字
float price; //图书的价格
}Book;
typedef struct{
Book *elem; //存储空间的基地址
int length; //表示已存储图书个数
}SqList;
2.3线性表顺序存储的基本操作
补充:操作算法中用到的预定常量和类型
//函数结果状态码
#define TRUE 1
#define FALSE 0
#define OK 1
#define ERROR 0
#define INFEASIBLE -1
#define OVERFLOW -2
//Status 是函数的类型,其值是函数结果状态代码
typedef int Status;
typedef char ElemType;
注:基本操作所涉及需要变动的变量等直接在引用参数上加上"&"即可。
1.顺序表的初始化
【算法步骤】
①为顺序表L动态分配一个预定大小的数组空间,使elem指向这段空间的基地址。
②将表的长度设置为0。
Status InitList(SqList &L){
//构造一个空的顺序表
L.elem = new ElemType[MAXSIZE]; //为顺序表分配一个大小为MAXSIZE的数组空间
if(!elem) exit(OVERFLOW); //如果存储分配失败,退出
L.length = 0; //设置空表长度为0
return OK; //初始化成功
}
2.销毁顺序表L
void DestoryList(SqList &L){
if(L.elem) delete L.elem; //直接释放存储空间
}
3.清空顺序表L
void ClearList(SqList &L){
L.length = 0; //长度设置为0
}
4.求顺序表L长度
void GetLength(SqList L){
return (L.length);
}
5.判断顺序表是否为空
void IsEmpty(SqList L){
if(L.length == 0) return 1; //根据长度判断,是0为空
else return 0;
}
6.顺序表按位置取值
【算法步骤】
①判断指定位置i是否合理(1≤ i ≤ L.length),若不合理,返回ERROR;
②若合理,则将第i个元素即L.elem[ i - 1]赋值给参数e。由于计算机数组下标从0开始,所以第i个元素的下标是i-1。
int GetElem(SqList L,int i,ElemType &e){
if(i<1 || i>L.length) return ERROD;
e = L.elem[i-1];
return OK;
}
显然:顺序表是随机存取,也就是可以任意存取某个元素的值。顺序表取值算法时间复杂度为O(1)。
7.按值查找(顺序查找法)
【算法步骤】
①从第一个元素起,把每个元素和e比较,若L.elem[i]与e相等,则返回i+1。计算机下标从0开始,注意这一点。
②若查遍整个数组为找到,则查找失败,返回0。
写法一:
int LocateElem(SqList L,ElemType e){
for(i=0;i<L.length;i++){
if(L.elem[i] == e) return i+1; //查找成功,返回序号i+1。
return 0; //没找到,返回0。
}
}
写法二:
int LocateElem(SqList L,ElemType e){
int i =0;
while(i<L.length && L.elem[i]!e)
i++;
if(i<L.length) return i+1;
return 0;
}
复杂度分析:
最好情况:查找的值在第一个,复杂度O(1)。
最坏情况:查找的值在最后一个,或者查找失败,复杂度O(n)。
查找成功平均时间复杂度:假设每个元素查找概率相等,即概率为1/n。所以平均查找长度(Average Search Length,ASL)期望值计算为
A
S
L
=
∑
i
=
1
n
P
i
C
i
=
1
n
∑
i
=
1
n
i
=
n
+
1
2
ASL =\sum_{i=1}^{n}P_iC_i =\frac{1}{n}\sum_{i=1}^{n}i=\frac{n+1}{2}
ASL=i=1∑nPiCi=n1i=1∑ni=2n+1
查找成功平均时间复杂度O(n)。
8.顺序表的插入
【算法步骤】
①判断插入i是否合法(1≤ i ≤ n+1),若不合法返回ERROR。
②判断顺序表的存储空间是否已满,满则返回ERROR。
③将最后一个到第i个位置的元素“从后往前”一个一个往后移一位。
④将要插入的新元素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(int j=L.length-1;j>=i-1;j--){ //注意:计算机中下标从0开始,所以这里起始j=L.length-1才是位置
L.elem[j+1] = L.elem[j]; //从后往前,每个位置往后移一位。
}
L.elem[i-1] = e; //把值赋值给第i个位置,下标为i-1
++L.length; //长度自增一位
return OK;
}
复杂度分析:
最好情况:插入元素在尾结点之后,无需移动,时间复杂度O(1)。
最坏情况:插入元素在首结点,表中元素全部往后移位,时间复杂度O(n)。
插入成功平均复杂度:假设顺序表上任何位置插入概率为等概率,则每个插入概率为1/n+1。
所以平均移动次数期望值
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)。
9.顺序表的删除
【算法步骤】
①判断删除位置i是否合法(1≤ i ≤ L.length),若不合法则返回ERROR。
②将第 “i+1”位置的元素到“最后一个”元素,从前往后每个向前一个位置。
③表长减1。
Status ListDelete(SqList &L,int i){
if(i<1 || i>L.length) return ERROR; //i不合法返回错误
for(int j = i;j<L.length;j++){ //这里不需要到n,只需要到n-1
L.elem[j-1]=L.elem[j]; //把i位置后面的元素,从i到n以此往前移一位。
}
--L.length;
return OK;
}
复杂度分析:
最好情况:删除元素在尾结点,无需移动,时间复杂度O(1)。
最坏情况:删除元素在首结点,把后面的元素依次往前移,时间复杂度O(n)。
删除成功平均复杂度:假设线性表任何位置上删除元素等概率,即1/n。
所以平均删除次数期望值
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)。
4线性表的链式存储(单链表)
1.定义
链式存储结构:结点在存储器中的位置是任意的,即逻辑上相邻的数据元素在物理上不一定相邻。
*链式存储是用一组物理位置"任意的存储单元"来存放线性表的数据元素,这组存储单元不用是连续的。
*链表中逻辑次序和物理次序不一定相同。
*链表的结点是由"数据域"和"指针域"组成:
-数据域:存放元素数值数据。
-指针域:存放直接后继结点的存储位置。
*链表的第一个地址作为“头指针”。可根据“头指针”访问链表。
链表的示意图如下:
特点(顺序存取):
访问链表时只能通过头指针进入链表,并通过每个结点的指针域依次向后顺序扫描其余结点,所以寻找第一个结点和最后一个结点所花费的时间不一样。
1.1单链表、双链表、循环链表
(1)单链表:结点只有一个指针域的链表,称为单链表或线性链表。一般情况下指针域指向下一个结点。(注:“^”这个符号表示空的意思)
(2)双链表:结点有两个指针域的链表成为双链表。即有一个指针指向前驱,一个指针指向后继。
(3)循环链表:首尾相连的链表成为循环链表。一般情况下最后一个结点指针域指向第一个结点。
1.2头指针、头结点和首元结点
(1)定义
头指针:是指向链表中第一个结点的指针。
首元结点:链表中存储第一个数据元素的结点。
头结点:有时候,会在链表的首元结点之前附设一个结点,这个结点称头结点。
(2)链表的存储结构有两种形式
-不带头结点示意图:头指针存放第一个元素的地址。
-带头结点示意图:头指针指向头结点,头结点的指针域存储首元结点的地址。
(3)空表的表示
*无头结点时,头指针为空时表示空表。
*有头结点时,头结点指针域为空时表示空表。
(4)设置头结点优势
*便于首元结点的处理:首元结点的地址如果保存在头结点的指针域中,第一个位置(首元结点)上的操作与其他位置的操作一样,不用特殊处理。
*空表和非空表处理:有头结点的链表无论链表是否为空,头指针都是指向头结点的非空指针,可以统一空表和非空表的处理。
(5)头结点的数据域
*头结点的数据域可以为空,也可以存放线性表的表长等附加信息,但此结点不计入链表长度。
1.3定义实现
定义一个链表
typedef struct Lnode{
ElemType data;
struct Lnode *next;
}LNode,*LinkList;
2.实现
1.单链表初始化
单链表的初始化:构造一个空表
【算法步骤】
(1)生成新结点作头结点,用头指针"L"指向头结点。
(2)将头结点的指针域置空。
Status InitList L(LinkList &L){
L = new LNode; //或者L = (LinkList) malloc (sizeof(LNode));
L->next = NULL;
return OK;
}
2.单链表补充操作
(1)判断单链表是否为空:链表中无元素,成为空链表。
【算法思路】
判断头结点指针域是否为空
int ListEmpty(LinkList L){ //若L为空表,返回1,否则返回0
if(L->next) //非空
return 0;
else
return 1;
}
(2)单链表销毁:链表销毁后不存在
【算法思路】
从头指针开始,依次释放所有结点
Status DestroyList_L(LinkList &L){
Lnode *p;
while(L){
p=L; //p指向头结点
L=L->next;
delete p;
}
}
(3)清空链表:链表仍存在,只是清除链表中元素,成为空链表。(头指针和头结点还在)
【算法思路】
依次释放所有结点,并将头结点指针域设置为空。
Status ClearList(LinkList &L){
Lnode *p,*q; //或者LinkList p,q;
p=L->next;
while(p){ //未到表尾
q=p->next;
delete p;
p=q;
}
L->next = null; //头结点指针域为空
return OK;
}
(4)求单链表的表长
【算法思路】
从首元结点开始,依次计数所有结点。
int ListLength_L(LinkList L){
LNode *p; //或者LinkList p;
p=L->next; //p指向第一个结点
int length=0; //定义长度计数变量
while(p){ //遍历单链表,统计结点计数
length++;
p=p->next; //p指向下一个结点
}
return length;
}
3.单链表取值
取值:取单链表中第“ i "个元素的内容
【算法思路】
从链表的头指针出发,顺着链域的next逐个结点往下搜索,直至搜索到第i个结点为止。因此,链表不是随机存取结构。
【算法步骤】
①从第一个结点(L->next)顺链扫描,用指针p指向当前扫描到的结点,p初始值p=L->next。
②j做计数器,累计当前扫描过的结点数,j初始值为1。
③当p指向扫描到下一个结点时,计数器j加1。
④j == i 时,p所指的结点就是要找的第i个结点。
Status GetElem_L(LinkList L,int i,ElemType &e){//获取线性表L中的某个数据元素的内容,通过变量e返回。
p=L->next; //初始化
j = 1; //计数器
while(p && j<i){ //p不为空或者j<i,操作
p = p->next;
++j;
}
if(!p || j>i) //如果p为空,或者j大于i(i=-1时),返回错误
return ERROR;
e=p->data;
return OK;
}
4.单链表查找
1.根据指定数据获取该数据所在位置(地址)
【算法思路】
从链表头指针出发,顺着链域的next逐个结点往下比较要查找的值,直到匹配为止。
【算法步骤】
①从第一个结点起,依次和要查找的元素值" e "比较
②如果找到一个其值与“ e ”相等的数据元素,则返回其在链表中的“位置”或地址
③如果遍历完整个链表还未有值与“ e “相等的数据元素,则返回0或者”null”。
(1)返回地址
LNode *LocateElem_L(LinkList L,ElemType e){
p=L->next;
while(p && p->data!=e){
p=p->next;
}
return p;
}
由于链表只能顺序存取,即只能从头指针开始往下找起,查找时间复杂度为O(n)。
(2)返回位置序号
int LocateElem_L(LinkList L,ElemType e){
p=L->next;
j =1;
while(p && p->data!=e){
p=p->next;
j++;
}
if(p) //如果p不是空,返回位置
return j;
else //如果p是空,返回0
return 0;
}
5.单链表插入
插入:在第i个结点插入值为e的新结点。
【算法步骤】
①首先找到第“ i -1 ”位置存储为p。在其后面插入新结点。
②生成一个数据域为e的新结点s。
③插入新结点:新结点指针域指向第 “ i "位置结点。(s->next = p->next)
第"i - 1"位置结点指针域指向新结点。(p->next=s)
Status ListInsert(LinkList &L,int i,ElemType e){
p=L;j=0;
while(p && j<i-1){ //找到第i-1个元素
p=p->next;
++j;
}
if(!p || j>i-1)
return ERROR; //如果p为空(i>L.length)或者j大于所要插入的位置i(i=-1)
s = new LNode; //生成新结点
s->data = e; //赋值
s->next = p->next; //开始插入
p->next = s;
return OK;
} //ListInsert
线性表不需要移动元素,但是进行插入时需要从头查找前驱结点,时间复杂度为O(n)。
6.单链表删除
删除:删除第i个结点
【算法步骤】
①首先找到第“ i-1 "位置存储为p,保存要删除的第“ i "的值。
②令p->next指向第” i + 1"位置。(p->next = p->next->next)
③释放第"i"位置结点的空间
Status ListDelete(LinkList &L,int i,ElemType &e){
p=L;j=0;
while(p->next && j<i-1){//找到第i-1个元素
p=p->next;
++;
}
if(!(p->next) || j>i-1)
return ERROR;
q = p-next; //保存要删除的结点
//把要删除的结点后继(即i+1元素)递给要删除结点前驱(即i-1元素)
p->next = p->next->next; //p->next = q->next;
e=q->data;
dalete q;
return OK;
}//ListDelete
线性表不需要移动元素,但是进行删除时需要从头查找前驱结点,时间复杂度为O(n)。
7.单链表的建立
1.头插法:元素插入时插入到链表头部。
【算法步骤】
①从一个空表开始,重复读入数据
②生成新结点,将读入数据存放到新结点数据域中
③从最后一个结点开始,依次将各结点插入到链表的前端
例子:建立链表L(a,b,c,d,e)。示意图如下
实现代码
void CreateList_H(LinkList &L,int n){//给了插入n次
L=new LNode;
L->next = NULL; //建立一个带头结点的空单链表
for(int n;i>0;i--){
p=new LNode;//生成一个新结点 c写法:p=(LNode*)malloc(sizeof(LNode));
cin>>p-data; //输入元素值 c写法:scanf(&p->data);
p->next = L->next; //插入到表头
L-next = p;
}
}
时间复杂度为O(n)。
2.尾插法:元素插入时插入到链表尾部。
【算法步骤】
①从一个空表L开始,将新结点逐个插入到链表尾部,尾指针r指向链表的尾结点。
②初始时,r和L均指向头结点。每读入一个数据元素则申请一个新结点,将新结点插入到尾结点后,r指向新结点。
void CreateList_R(LinkList &L,int n){//给了插入n次
L=new LNode;
L->next = NULL; //建立一个带头结点的空单链表
r=L;//尾指针r指向头结点
for(i=0;i<n;i++){
p=new LNode; //生成新结点,输入元素值
cin>>p->data;
r->next=p; //插入到表尾
r=p; //r指向新的尾结点
}
}
时间复杂度为O(n)。
5单循环链表
循环链表:一种头尾相接的链表。(即:表中最后一个结点的指针域指向头结点,整个链表形成一个环)
优点:从表中任意结点出发均可以找到表中其他结点。
由于循环链表没有NULL指针,所以在遍历操作时,终止条件为判断某个结点或者某个结点的后继是否等于头指针。
循环条件:“p!=L” 或者 “p->next !=L”
①使用头指针时,单循环链表找第一个元素时间复杂度O(1),找最后一个元素时间复杂度O(n)。使用起来不是很方便。
②使用尾指针(尾指针标记为R)时,单循环链表找第一个位置"R->next->next",时间复杂度为O(1),寻找最后一个元素时间复杂度也为O(1)。
所以经常在表的首尾上进行操作时,可使用尾指针。
带尾指针循环链表合并:
将如图所示的两个带尾指针的循环链表合并,将Tb合并在Ta后面。
操作:
①存放表头结点:p=Ta->next
②Tb表头链接到Ta表尾:Ta->next=Tb->next->next
③释放Tb表头结点。delete Tb->next
④修改指针。Tb->next=p;
实现代码:
LinkList Connect(LinkList Ta,LinkList Tb){
//假设Ta、Tb都是非空单循环链表
p=Ta->next; //p存放Ta的表头结点
Ta->next=Tb->next->next; //将Tb表头链接Ta表尾
delete Tb->next; //释放Tb表头结点
Tb->next =p; //修改指针
return Tb;
}
时间复杂度O(1)。
6双向链表
双向链表:在单链表的每个结点再增加一个指向直接前驱的指针域"prior",这样链表中就形成了有两个方向不同的链。
双向链表定义:
typedef struct DuLNode{
ElemType data;
struct DuLnode *prior,*next;
}DuLNode,*DuLinkList;
双向链表示意图:
双向循环链表:让头结点的前驱指向链表最后一个结点,让最后一个结点的后继指向头结点。
双向循环链表示意图:
双向链表的对称性:p->prior->next = p = p->next->prior
1.双向链表的插入
在某个结点p的前面插入一个新结点。
①把p结点的前驱赋值给插入结点的前驱:s->prior=p->prior
②把p结点的前驱结点,它的后继指向s:p->prior->next=s
③把新结点的后继指向p:s->next=p
④把p结点的前驱指向s:p->prior=s
代码实现:
void ListInsert_DuL(DuLinkList &L,int i,ElemType e){
//在带头结点的双向循环链表L中第i个位置插入元素e
if(!(p=GetElemP_DuL(L,i))) //在链表L中确定第i个位置指针p
return ERROR; //p为NULL时,第i个元素不存在
s=new DuLNode; //生成新结点s
s->data =e; //把e赋值给s数据域
s->prior = p->prior; //①
p->prior->next=s; //②
s->next=p; //③
p->prior=s; //④
return OK;
}//ListInsert_DuL
时间复杂度O(n)
2.双线链表的删除
删除某个结点p。
①将p的前驱a,它的后继指向p的后继c:p->prior->next=p->next
②将p的后继c作为p的前驱a的后继:p->next->prior=p->prior
代码实现:
void ListDelete_DuL(DuLink &;,int i, ElemType &e){
//删除带头结点的双向循环链表L第i个元素,并用e返回
if(!(p=GetElemP_DuL(L,i))) //在链表中确定第i个位置的指针p
return ERROR; //p为NULL时,第i个元素不存在
e=p->data;
p->prior->next=p->next;
p->next->prior=p->prior;
free(p);
return OK;
}//ListDelete_DuL
时间复杂度O(n)
7.单链表、循环链表和双向链表的时间效率比较
查找表头结点 | 查找表尾结点 | 查找结点*P的前驱 | |
---|---|---|---|
带头结点的单链表L | L->next 时间复杂度O(1) | L->next依次向后遍历 时间复杂度O(n) | 通过p->next无法查找其前驱 |
带头结点仅设头指针L 的循环单链表 | L->next 时间复杂度O(1) | L->next依次向后遍历 时间复杂度O(n) | 通过p->next可以找到其前驱 时间复杂度O(n) |
带头结点仅设尾指针R的循环单链表 | R->next 时间复杂度O(1) | R 时间复杂度O(1) | 通过p->next可以找到其前驱 时间复杂度O(n) |
带头结点的双向循环链表 | L->next 时间复杂度O(1) | L->prior 时间复杂度O(1) | p->prior 时间复杂度O(1) |
8.顺序表和链表比较
*存储密度=结点数据本身占用的空间/结点占用空间总量
一般的,存储密度越大,存储空间的利用率越高。显然,顺序表的存储密度为1(100%),而链表的存储密度小于1。
学习视频:数据结构——王卓;
参考文献:数据机构C语言版第2班——严蔚敏