文章目录
注:内容参考王道2024考研复习指导以及《数据结构》
线性表的定义和基本操作
线性表是具有相同数据类型的 n ( n > 0 ) n(n>0) n(n>0)个数据元素的有限序列,其中 n n n为表长,当 n = 0 n=0 n=0时线性表是一个空表。线性表一般表示为 L = ( a 1 , a 2 , . . . , a i , . . . , a n ) L=(a_1,a_2,...,a_i,...,a_n) L=(a1,a2,...,ai,...,an)
几个概念:
a i a_i ai是线性表中的“第i个”元素在线性表中的位序。
a 1 a_1 a1是表头元素, a i a_i ai是表尾元素。
除第一个元素外,每个元素有且仅有一个直接前驱;除最后一个元素外,每个元素有且仅有一个直接后继。
线性表的抽象数据类型定义
ADT{
数据对象:$D$={$a_i|a_i\in ElemSet,i=1,2,3,...,n,n\geq0$}
数据关系:$R$={$<a_{i-1},a_i>|a_{i-1},a_i \in D,i=2,3,...n$}
基本操作:
InitList(&L)
操作结果:构造一个空的线性表L;
Destory(&L)
初始条件:线性表L已经存在
操作结果:销毁线性表L
ClearList(&L)
初始条件:线性表L已经存在
操作结果:将L重置为空表
ListEmpty(L)
初始条件:线性表L已存在
操作结果:若L为空表,则返回TRUE,否则返回FALSE
ListLength(L)
初始条件:线性表L已存在;
操作结果:返回线性表L中的数据元素个数;
GetElem(L,i,&e)
初始条件:线性表L已存在,1<=i<=ListLengh(L)
操作结果:用e返回L中第i个数据元素的值;
LocateElem(L,e)
初始条件:线性表L已存在
操作结果:返回L中第1个值与e相同的元素在L中的位置。若这样的数据元素不存在,则返回值为0。
PriorElem(L,cur_e,&pre_e)
初始条件:线性表L已存在
操作结果:若cur_e是L的数据元素,且不是第一个,则用pre_e 返回其前驱,否则操作失败, pre_e 无定义
NextElem(L,cur_e,&next_e)
初始条件:线性表L已存在
操作结果:若cur_e是L的数据元素,且不是最后一个,则用next_e返回其后继,否则操作失败, next_e无定义
ListInsert(L,i,&e)
初始条件:线性表L已存在,1≦i≦ListLength(L) +1
操作结果:在线性表L中的第i个位置之前插入新的元素e,L长度加1;
ListDelete(&l,i)
初始条件:线性表L已存在且非空,1≦i≦ListLength(L)
操作结果:删除L的第i个数据元素,L长度减1
TraverseList(L)
初始条件:线性表L已存在
操作结果:对线性表L进行遍历,在遍历过程中对L的每个 结点访问一次
}ADT List
线性表的顺序表示
顺序表的定义
顺序表——用顺序结构的方式实现线性表
特点:
- 随机访问,即可以在 O(1) 时间内找到第 i 个元素。
- 存储密度高,每个节点只存储数据元素。
- 拓展容量不方便(即便采用动态分配的方式实现,拓展长度的时间复杂度也比较高)。
- 插入、删除操作不方便,需要移动大量元素。
顺序表的实现——静态分配
给各个数据元素分配连续的存储空间,大小为MaxSize*sizeof(ElemType)。
“数组”存满后无法更改。
#define MaxSize 10//定义最大长度
typedef struct {
ElemType data[MaxSize];
int length;
}SqList;
//初始化线性表
void Initlist(SqList &L){
L.length=0;
}
顺序表的实现——动态分配
#define InitSize 10//定义最大长度
typedef struct {
ElemType *data;
int MaxSize;
int length;
}SqList;
//初始化线性表
void Initlist(SqList &L){
L.data=(ElemType *)malloc(InitSize*sizeof(ElemType));
L.length=0;
L.MaxSize=InitSize;
}
void IncreaseSize(SqList &L,int len){
int *p=L.data;
L.data=(ElemType *)malloc((L.MaxSize+len)*sizeof(ElemType));
for(int i=0;i<L.length;i++){
L.data[i]=p[i];//将数据复制到新区域
L.MaxSize=L.MaxSize+len;//顺序表最大长度增加1en
free(p);
}
顺序表的基本操作
顺序表的基本操作——插入
bool ListInsert(SqList &L,int i,ElemType e){
if(i<1 || i>L.length+1){
return false;
}
if(L.length >= MaxSize){
return false;
}
for(int j=L.lengthlj>=i;j--){
L.data[j]=L.data[j-1];
}
L.data[i-1]=e;
L.length++;
}
时间复杂度分析:
最好情况:新元素插入到表尾,不需要移动元素i = n+1,循环0次;最好时间复杂度 = O(1);
最坏情况:新元素插入到表头,需要将原有的 n 个元素全都向后移动i = 1,循环 n 次;最坏时间复杂度 = O(n);
平均情况:假设新元素插入到任何一个位置的概率相同,即 i = 1,2,3, … , length+1 的概率都是 p = 1 n + 1 p=\frac{1}{n+1} p=n+11,循环 n 次;i=2 时,循环 n-1 次;i=3,循环 n-2 次 …… i =n+1时,循环0次。平均循环次数 = n p + ( n − 1 ) p + ( n − 2 ) p + … … + 1 ⋅ p = n ( n + 1 ) 2 1 n + 1 = n 2 np + (n-1)p + (n-2)p + …… + 1⋅p =\frac{n(n+1)}{2}\frac{1}{n+1}=\frac{n}{2} np+(n−1)p+(n−2)p+……+1⋅p=2n(n+1)n+11=2n;**平均时间复杂度 = O(n) **
顺序表的基本操作——删除
bool ListDelete(SqList &L,int i,ElemType &e){
if(i<1||i>L.length)//判断i的范围是否有效
return false;
e=L.data[i-1];//将被删除的元素赋值给e
for(int j=i;j<L.length;j++){
L.data[j-1]=L.data[j];
}
L.length--;
return true;
}
时间复杂度分析:
最好情况:新元素插入到表尾,不需要移动元素i = n+1,循环0次;最好时间复杂度 = O(1);
最坏情况:新元素插入到表头,需要将原有的 n 个元素全都向后移动i = 1,循环 n 次;最坏时间复杂度 = O(n);
平均情况:假设新元素插入到任何一个位置的概率相同,即 i = 1,2,3, … , length+1 的概率都是 p = 1 n p=\frac{1}{n} p=n1,循环 n 次;i=2 时,循环 n-1 次;i=3,循环 n-2 次 …… i =n+1时,循环0次。平均循环次数 = n p + ( n − 1 ) p + ( n − 2 ) p + … … + 1 ⋅ p = n ( n − 1 ) 2 1 n = n − 1 2 np + (n-1)p + (n-2)p + …… + 1⋅p =\frac{n(n-1)}{2}\frac{1}{n}=\frac{n-1}{2} np+(n−1)p+(n−2)p+……+1⋅p=2n(n−1)n1=2n−1;**平均时间复杂度 = O(n) **
顺序表的按位查找
ElemType GetElem(SqList L, int i){
return L.data[i-1];
}
顺序表的按值查找
int LocateElem(SeqList L,ElemType e){
for(int i=0;i<L.length;i++){
if(L.data[i]==e){
return i+1; //数组下标为i的元素值等于e,返回其位序i+1
}
}
return 0; //退出循环,说明查找失败
}
时间复杂度分析:
最好情况:新元素插入到表尾,不需要移动元素i = n+1,循环0次;最好时间复杂度 = O(1);
最坏情况:新元素插入到表头,需要将原有的 n 个元素全都向后移动i = 1,循环 n 次;最坏时间复杂度 = O(n);
平均情况:假设新元素插入到任何一个位置的概率相同,即 i = 1,2,3, … , length+1 的概率都是 p = 1 n p=\frac{1}{n} p=n1,循环 n 次;i=2 时,循环 n-1 次;i=3,循环 n-2 次 …… i =n+1时,循环0次。平均循环次数 = n p + ( n − 1 ) p + ( n − 2 ) p + … … + 1 ⋅ p = n ( n + 1 ) 2 1 n = n 2 np + (n-1)p + (n-2)p + …… + 1⋅p =\frac{n(n+1)}{2}\frac{1}{n}=\frac{n}{2} np+(n−1)p+(n−2)p+……+1⋅p=2n(n+1)n1=2n;**平均时间复杂度 = O(n) **
线性表的链式表示
单链表的定义
链表——用链式结构的方式实现线性表
代码定义如下:
typedef struct LNode{
ElemType data;
struct LNode *next;
}LNode,*LinkList;
不带头结点的单链表
bool InitList(LinkList &L){
L=NULL;
return true;
}
bool ListEmpty(LinkList L){
return (L==NULL);
}
带头结点的单链表
bool InitList(LinkList &L){
L=(LNode *)malloc(sizeof(LNode));
if (L==NULL){
return false;
}
L->next=NULL;
return true;
}
bool ListEmpty(LinkList L){
return (L->next==NULL);
}
注:不带头结点,写代码更麻烦,对第一个数据结点和后续数据结点的处理需要用不同的代码逻辑,对空表和非空表的处理需要用不同的代码逻辑。
单链表的基本操作
单链表的插入
按位插入(带头结点)
bool ListInsert(LinkList &L,int i,ElemType e){
if(i<1){
return false;
}
LNode *p;
int j=0;
p=L;
while(p!=NULL && j<i-1){
p=p->next;
j++;
}
//以下代码可以在后插操作封装后替换称InsertNextNode(p,e)
if(p==NULL){
return false;
}
LNode *s=(LNode *)malloc(sizeof(LNode));
s->data=e;
s-next=p->next;
p->next=s;
return true;
}
注:不带头结点的链表按位插入,需要对插入第1个结点单独处理,即 i = = 1 i==1 i==1时做单独处理。
指定结点的后插操作
bool InsertNextNode(LNode *p,ElemType e){
if(p==NULL){
return false;
}
LNode *s=(LNode *)malloc(sizeof(LNode));
if(s==NULL){
return false;
}
s->data=e;
s-next=p->next;
p->next=s;
return true;
}
指定结点的前插操作
bool InsertPriorNode(LNode *p,ElemType e){
if(p==NULL){
return false;
}
LNode *s=(LNode *)malloc(sizeof(LNode));
if(s==NULL){
return false;
}
s->next=p-next;
p->next=s;
s->data=p->data;
p->data=e;
return true;
}
单链表的删除
按位删除
bool ListDelete(LinkList &L,int i,ElemType &e){
if(i<1){
return false;
}
LNode *p;
int j=0;
p=L;
while(p!=NULL && j<i-1){
p=p->next;
j++;
}
if(p->next==NULL){
return false;
}
LNode *q=p->next
p-next=q->next;
e=q->data;
free(q);
return true;
}
指定结点的删除
bool DeleteNode(LNode *p){
if(p==NULL){
return false;
}
LNode *q=p->next;
p->data=q->data;
p->next=q->next;
free(p);
return true;
}
单链表的查找
按位查找
LNode *GetElem(LinkList L,int i){
if(i<0){
return false;
}
LNode *p;
int j=0;
while(p!=NULL && j<i){
p=p->next;
j++;
}
return p;
}
按值查找
LNode *LocateElem(LinkList L,ElemType e){
LNode *p=L->next;
while(p!=NULL && p-data!=e){
p=p->next;
}
return p;
}
单链表的建立
尾插法建立单链表
LinkList List_TailInsert(LinkList &L){
int x;
L=(LinkList)malloc(sizeof(LNode));
L->next=NULL;
LNode *s,*r=L;
scanf("%d",&x);
while(x!=-1){
s=(LNode *)malloc(sizeof(LNode));
s->data=x;
r->next=s;
r=s;
scanf("%d",&x);
}
r->next=NULL;
return L;
}
当输入:10 16 27时,单链表如下所示
头插法建立单链表
重要应用于链表的逆置
LinkList List_HeadInsert(LinkList &L){
int x;
L=(LinkList)malloc(sizeof(LNode));
L->next=NULL;
LNode *s;
scanf("%d",&x);
while(x!=-1){
s=(LNode *)malloc(sizeof(LNode));
s->data=x;
s->next=L->next;
L->next=s;
scanf("%d",&x);
}
return L;
}
当输入:10 16 27时,单链表如下所示
双链表
双链表结点中有两个指针prior和next,分别指向其直接前驱和直接后继。表头结点的prior域和尾结点的next域都是NULL。对于单链表存在无法逆向检索的问题,双链表可进可退,但是存储密度相对于单链表更低一些。
结点定义如下:
typedef struct DNode{
ElemType data;
struct DNode *prior,*next;
}DNode,*DLinkList;
双链表的初始化(带头结点)
bool InitDLinkList(DLinkList &L){
L=(DlinkList)malloc(sizeof(DNode));
if(L==NULL){
return false;
}
L->next=NULL;
L->prior=NULL;
return true;
}
双链表的插入
在双链表中p所指的结点之后插入结点*s
bool InsertNextDNode(DNode *p,DNode *s){
if (p==NULL || s==NULL)//非法参数
return false;
s->next=p->next;
if(p->next != NULL)//如果p结点有后继结点
p->next->prior=s;
s->prior=p;
p->next=s;
return true;
}
双链表的删除
void DestoryList(DLinkList &L){
while(L->next != NULL){
DeleteNextDNode(L);
}
free(L);
L=NULL;
}
bool DeleteNextDNode(DNode *p){
if(p==NULL){
return false;
}
DNode *q=p-next;
if(q==NULL){
return false;
}
p->next=q->next;
if(q->next!=NULL){
q->next->prior=p;
}
free(q);
return true;
}
双链表的遍历
while (p!=NULL){//后向遍历
p =p->next;
}
while (p!=NULL){//前向遍历
p= p->prior;
}
while (p-> prior != NULL){//跳过头结点的前向遍历
p = p->prior;
}
循环链表
循环单链表
表尾结点的next指针指向头结点。
从一个结点出发可以找到其他任何一个结点。
在循环单链表中,表尾结点*r的next域指向L,故表中没有指针域为NULL的结点,因此,循环单链表的判空条件不是头结点的指针是否为空,而是它是否等于头指针L。
typedef struct LNode{
ElemType data;
struct LNode *next;
}LNode,*LinkList;
bool InitList(LinkList &L){
L=(LNode *)malloc(sizeof(LNode));
if (L==NULL){
return false;
}
L->next=L;//与单链表不同之处
return true;
}
循环双链表
表头结点的 prior 指向表尾结点,表尾结点的 next 指向头结点。
typedef struct DNode{
ElemType data;
struct DNode *prior,*next;
}DNode,*DLinkList;
bool InitDLinkList(DLinkList &L){
L=(DlinkList)malloc(sizeof(DNode));
if(L==NULL){
return false;
}
L->next=L;
L->prior=L;
return true;
}
静态链表
分配一整片连续的内存空间,各个结点集中安置。
代码定义如下:
#define MaxSize 10
typedef struct {
ElemType data;
int next;
}SLinkList[MaxSize];
void testSLinkList(){
SLinkList a;//a为静态链表
}
查找:从头结点出发挨个往后遍历结点
插入位序为 i 的结点:
- 找到一个空的结点(next为特殊值,例如-2表示结点为空),存入数据元素
- 从头结点出发找到位序为 i-1 的结点
- 修改新结点的 next
- 修改 i-1 号结点的 next
删除某个结点:
- 从头结点出发找到前驱结点
- 修改前驱结点的游标
- 被删除结点 next 设为 -2
-
优点:增、删 操作不需要大量移动元素
-
缺点:不能随机存取,只能从头结点开始依次往后查找;容量固定不可变
-
适用场景:①不支持指针的低级语言;②数据元素数量固定不变的场景(如操作系统的文件分配表FAT)
顺序表和链表的比较
类型 | 逻辑结构 | 存储结构 | 基本操作 |
---|---|---|---|
顺序表 | 线性表,线性结构。 | 支持随机存取、存取密度高;但是大片连续空间分配不方便、改变容量不方便。 | 在查找方面,顺序表支持随机存取,时间复杂度为 O ( 1 ) O(1) O(1),优于链表;在插入元素和删除元素方面,顺序表时间开销主要来自移动元素。 |
链表 | 线性表,线性结构。 | 离散的小空间分配方便、改变容量方便;但是不可随机存取、存储密度低。 | 创建方面,链表具有弹性,可扩容,优于顺序表;在插入元素和删除元素方面,链表只需要修改指针,时间开销主要来自查找元素,时间代价更低,优于顺序表。 |
问题: 请描述顺序表和链表的…实现线性表时,用顺序表还是链表好?
答:顺序表和链表的逻辑结构都是线性结构,都属于线性表。但是二者的存储结构不同,顺序表采用顺序存储…(特点,带来的优点缺点);链表采用链式存储…(特点、导致的优缺点)。由于采用不同的存储方式实现,因此基本操作的实现效率也不同。当初始化时…;当插入一个数据元素时…;当删除一个数据元素时…;当查找一个数据元素时…