数据结构 -线性表
文章目录
1.线性表及其逻辑结构
1.1线性表的定义
-
定义:线性表(linear list)是具有相同特性的数据元素的一个有限序列。
-
线性表的逻辑表示为:( a1,a2,…,ai,ai+1,…,an ) ai(1≤i≤n)表示第i(i表示逻辑位序)个元素。
-
前驱与后继:线性表中相邻的数据元素ai-1和ai之间存在序偶关系(ai-1, ai),即ai-1是ai的前驱, ai是ai-1的后继;a1 无前驱,an无后继,其它每个元素有且仅有一个前驱和一个后继。
-
线性表长度:线性表中元素的个数。
-
线性表L=(a1,a2,a3……an)的图形化表示:
1.2线性表的抽象数据类型描述
ADT
{
数据对象:
D={ ai | 1≤i≤n,n≥0,ai为ElemType类型} //ElemType是自定义类型标识符
数据关系:
R={<ai,ai+1>| ai、ai+1∈D,i=1,…,n-1}
基本运算:
InitList(&L):初始化线性表,构造一个空的线性表L。
DestroyList(&L):销毁线性表,释放线性表L占用的内存空间。
ListEmpty(L):判线性表是否为空表,若L为空表,则返回真,否则返回假。
ListLength(L):求线性表的长度,返回L中元素个数n。
DispList(L):输出线性表,线性表L不为空时,顺序显示L中各结点的值域。
GetElem(L,i,&e):求线性表L中指定位置的某个数据元素,用e返回L中第i(1≤i≤n)个元素的值。
LocateElem(L,e):定位查找,返回L中第一个值域与e相等的逻辑位序。若这样的元素不存在,则返回值为0。
ListInsert(&L,i,e):插入一个数据元素,在L的第i(1≤i≤n)个元素之前插入新的元素e,L的长度增1。
ListDelete(&L,i,&e):删除数据元素,删除L的第i(1≤i≤n)个元素,并用e返回其值,L的长度减1。
}
1.3 线性表的知识结构
2.线性表的顺序存储结构
2.1线性表的顺序存储 ——— 顺序表
- 线性表顺序存储结构:把线性表中的所有元素按照顺序存储方法进行存储。
- 顺序表的声明
#define MaxSize 50
typedef struct
{
ElemType data[MaxSize]; //存放线性表中的元素
int length; //存放线性表的长度
} SqList; //顺序表类型
注意:逻辑位序和物理位序相差1。
2.2顺序表基本运算的实现
假设ElemType为int类型,使用以下自定义类型语句
typedef int ElemType
2.2.1建立顺序表
void CreateList(SqList * &L,ElemType a[],int n)//整体建立顺序表,由a中的n个元素建立顺序表
{
int i=0,k=0; //k表示L中元素的个数,初始值为0
L=(SqList *)malloc(sizeof(SqList)); //分配存放线性表的空间
while (i<n) //i扫描a中元素
{
L->data[k]=a[i]; //将元素a[i]存放到L中
k++; i++; //k记录插入到L中的元素个数
}
L->length=k; //设置L的长度k
}
L是输出型参数,所以在形参L的前面需要加上引用符 “&”
2.2.2 顺序表的基本运算算法
- 初始化线性表:InitList(L)
void InitList(SqList *&L){
L=(SqList *)malloc(sizeof(SqList)); //分配存放线性表的顺序表空间
L->length=0; //置空线性表的长度为0
}
- 销毁线性表:DestroyList(L)
void DestroyList(SqList *&L)
{
free(L); //释放线性表L占用的内存空间
}
- 判定是否为空表:ListEmpty(L)
bool ListEmpty(SqList *L)
{
return(L->length==0);
}
//返回一个值表示L是否为空表。若L为空表,则返回true,否则返回false。
- 求线性表的长度:ListLength(L)
int ListLength(SqList *L)
{
return(L->length); //返回顺序表L的长度
}
- 输出线性表:DispList(L)
//当线性表L不为空时,顺序显示L中各元素的值
void DispList(SqList *L)
{
for (int i=0;i<L->length;i++) //扫描顺序表输出各元素值
printf("%d ",L->data[i]);
printf("\n");
}
- 按序号求线性表中的元素:GetElem(L,i,&e)
//返回L中第 i(1≤i≤ListLength(L))个元素的值,存放在e中。
bool GetElem(SqList *L,int i,ElemType &e)
{
if (i<1 || i>L->length)
return false; //参数i错误时返回false
e=L->data[i-1]; //取元素的值
return true; //成功找到元素时返回true
}
//体现顺序表的随机存取特性
- 按元素值查找:LocateElem(L,e)
//顺序查找第一个值域与e相等的元素的逻辑位序。若这样的元素不存在,则返回值为0。
int LocateElem(SqList *L,ElemType e)
{
int i=0;
while (i<L->length && L->data[i]!=e)
i++; //查找元素e
if (i>=L->length) //未找到时返回0
return 0;
else
return i+1; //找到后返回其逻辑序号
}
- 插入数据元素:ListInsert(L,i,e)
//在顺序表L的第i(1≤i≤ListLength(L)+1)个位置上插入新的元素e。
bool ListInsert(SqList *&L,int i,ElemType e)
{
int j;
if (i<1 || i>L->length+1 || L->length==MaxSize)
return false; //参数错误时返回false
i--; //将顺序表逻辑序号转化为物理序号
for (j=L->length;j>i;j--) //将data[i..n]元素后移一个位置
L->data[j]=L->data[j-1];
L->data[i]=e; //插入元素e
L->length++; //顺序表长度增1
return true; //成功插入返回true
}
- 时间复杂度分析:元素移动的次数不仅与表长L->length=n有关,而且与插入位置i有关:
- 当i=n+1时,移动次数为0;(算法最好时间复杂度为O(1))
- 当i=1时,移动次数为n,达到最大值。(算法最坏时间复杂度为O(n))
- 平均情况分析:
- 在插入元素ai时,若为等概率情况,则pi =1/(n+1)
- 此时需要将ai~an的元素均后移一个位置,共移动n-i+1个元素。
- 在长度为n的线性表中插入一个元素时所需移动元素的平均次数为:
因此插入算法的平均时间复杂度为O(n)。
- 删除数据元素:ListDelete(L,i,e)
bool ListDelete(SqList *&L,int i,ElemType &e)
{
int j;
if (i<1 || i>L->length) //参数错误时返回false
return false;
i--; //将顺序表逻辑序号转化为物理序号
e=L->data[i];
for (j=i;j<L->length-1;j++) //将data[i..n-1]元素前移
L->data[j]=L->data[j+1];
L->length--; //顺序表长度减1
return true; //成功删除返回true
}
- 删除元素时移动元素的过程
3.线性表的链式存储结构
3.1线性表的链式存储结构——— 链表
-
链表:线性表的链式存储结构称为链表
-
设计链式存储结构时,每个数据元素用一个结点单独存储,为了表示逻辑关系,增加指针域。
-
线性表两种存储结构的比较:
-
顺序表
- 优点
-
存储密度大:无须为表示线性表中元素之间的逻辑关系而增加额外的存储空间。
-
具有随机存取特性。
- 缺点
- 插入和删除操作需要移动大量元素。
- 初始空间大小分配难以掌握。
-
链表
- 优点
- 由于采用结点的动态分配方式,具有良好的适应性。
- 插入和删除操作只需修改相关指针域,不需要移动元素。
- 缺点
- 存储密度小:为表示线性表中元素之间的逻辑关系而需要增加额外的存储空间(指针域)。
- 不具有随机存取特性。
-
3.2单链表
- 单链表:每个结点增加一个指向后继结点的指针域。
- 单链表中结点类型LinkNode的声明
typedef struct LNode //声明单链表结点类型
{ ElemType data;
struct LNode *next; //指向后继结点
} LinkNode;
-
带头结点单链表
-
单链表的特点:当访问过一个结点后,只能接着访问它的后继结点,而无法访问它的前驱结点。
1.插入结点和删除结点操作
- 插入操作:将值为x 的新结点 s 插入到 p 结点之后。
- 特点:只需修改相关结点的指针域,不需要移动结点。
s->next = p->next;
p->next = s;
- 删除操作:删除p结点之后的一个结点。
- 特点:只需修改相关结点的指针域,不需要移动结点。
q=p->next; //q临时保存被删结点
p->next = q->next; //从链表中删除结点q
free(q); //释放结点q的空间
2.整体建立单链表
- 头插法
- 从一个空表开始,创建一个头结点。
- 依次读取字符数组 a 中的元素,生成新结点。
- 将新结点插入到当前链表的表头上,直到结束为止。
- 注意:链表的结点顺序与逻辑次序相反。
头插法建表算法:
void CreateListF(LinkNode *&L,ElemType a[],int n)
{
LinkNode *s;
int i;
L=(LinkNode *)malloc(sizeof(LinkNode));
L->next=NULL; //创建头结点,其next域置为NULL
for (i=0;i<n;i++) //循环建立数据结点
{
s=(LinkNode *)malloc(sizeof(LinkNode));
s->data=a[i]; //创建数据结点s
s->next=L->next; //将s插在首结点之前,头结点之后
L->next=s;
}
}
算法复杂度为O(n)
- 尾插法
- 从一个空表开始,创建一个头结点。
- 依次读取字符数组a中的元素,生成新结点。
- 将新结点插入到当前链表的表尾上,直到结束为止。
- 注意:链表的结点顺序与逻辑次序相同。
尾插法
void CreateListR(LinkNode *&L,ElemType a[],int n)
{
LinkNode *s,*r;
int i;
L=(LinkNode *)malloc(sizeof(LinkNode)); //创建头结点
r=L; //r始终指向尾结点,开始时指向头结点
for (i=0;i<n;i++) //循环建立数据结点
{
s=(LinkNode *)malloc(sizeof(LinkNode));
s->data=a[i]; //创建数据结点s
r->next=s; //将s插入r之后
r=s;
}
r->next=NULL; //尾结点next域置为NULL
}
算法复杂度为O(n)
3.线性表的基本运算在单链表中的实现
- 初始化线性表:InitList(&L)
//建立一个空的单链表,即创建一个头结点。
void InitList(LinkNode *&L)
{
L=(LinkNode *)malloc(sizeof(LinkNode)); //创建头结点
L->next=NULL; //将其next域置为NULL
}
- 销毁线性表:DestroyList(&L)
//释放单链表L占用的内存空间。逐一释放全部结点的空间。
void DestroyList(LinkNode *&L)
{
LinkNode *pre=L, *p=L->next; //pre指向p的前驱结点
while (p!=NULL) //扫描单链表L
{
free(pre); //释放pre结点
pre=p; //pre、p同步后移一个结点
p=pre->next;
}
free(pre); //循环结束时p为NULL,pre指向尾结点,释放它
}
3.判线性表是否为空表:ListEmpty(L)
//若单链表L没有数据结点,则返回真,否则返回假。
bool ListEmpty(LinkNode *L)
{
return(L->next==NULL);
}
- 求线性表的长度:ListLength(L)
//返回单链表L中数据结点的个数。
int ListLength(LinkNode *L)
{
int n=0;
LinkNode *p=L; //p指向头结点,n置为0(即头结点的序号为0)
while (p->next!=NULL)
{
n++;
p=p->next;
}
return(n); //循环结束,p指向尾,其序号n为结点个数
}
- 输出线性表:DispList(L)
//逐一扫描单链表L的每个结点,并显示各结点data域值。
void DispList(LinkNode *L)
{
LinkNode *p=L->next; //p指向开始结点
while (p!=NULL) //p不为NULL,输出p结点的data域
{
printf("%d ",p->data);
p=p->next; //p移向下一个结点
}
printf("\n");
}
- 求位置i的数据元素:GetElem(L,i,&e)
//在单链表L中从头开始找到第i个结点,若存在第i个数据结点,则将其data域值赋给变量e。
bool GetElem(LinkNode *L,int i,ElemType &e)
{
int j=0;
LinkNode *p=L; //p指向头结点,j置为0(即头结点的序号为0)
while (j<i && p!=NULL)
{
j++;
p=p->next;
}
if (p==NULL) //不存在第i个数据结点,返回false
return false;
else //存在第i个数据结点,返回true
{
e=p->data;
return true;
}
}
- 按元素值查找:LocateElem(L,e)
//在单链表L中从头开始找第一个值域与e相等的结点,若存在这样的结点,则返回位置,否则返回0。
int LocateElem(LinkNode *L,ElemType e)
{
int i=1;
LinkNode *p=L->next; //p指向开始结点,i置为1
while (p!=NULL && p->data!=e)
{
p=p->next; //查找data值为e的结点,其序号为i
i++;
}
if (p==NULL) //不存在元素值为e的结点,返回0
return(0);
else //存在元素值为e的结点,返回其逻辑序号i
return(i);
}
- 插入数据元素:ListInsert(&L,i,e)
//先在单链表L中找到第 i-1 个结点p,若存在这样的结点,将值为e的结点s插入到其后。
bool ListInsert(LinkNode *&L,int i,ElemType e)
{
int j=0;
LinkNode *p=L,*s; //p指向头结点,j置为0
while (j<i-1 && p!=NULL)
{
j++;
p=p->next;
}
if (p==NULL) //未找到第i-1个结点,返回false
return false;
else //找到第i-1个结点p,插入新结点并返回true
{
s=(LinkNode *)malloc(sizeof(LinkNode));
s->data=e; //创建新结点s,其data域置为e
s->next=p->next; //将s插入到p之后
p->next=s;
return true;
}
}
- 删除数据元素:ListDelete(&L,i,&e)
//先在单链表L中找到第i-1个结点p,若存在这样的结点,且也存在后继结点,则删除该后继结点。
bool ListDelete(LinkNode *&L,int i,ElemType &e)
{
int j=0;
LinkNode *p=L,*q; //p指向头结点,j置为0
while (j<i-1 && p!=NULL) //查找第i-1个结点
{
j++;
p=p->next;
}
if (p==NULL) //未找到第i-1个结点,返回false
return false;
else //找到第i-1个结点p
{
q=p->next; //q指向第i个结点
if (q==NULL) //若不存在第i个结点,返回false
return false;
e=q->data;
p->next=q->next; //从单链表中删除q结点
free(q); //释放q结点
return true; //返回true表示成功删除第i个结点
}
}
3.3双链表
- 双链表:在线性表的链式存储结构中,每个物理结点增加一个指向后继结点的指针域和一个指向前驱结点的指针域。
-
优点:
- 从任一结点出发可以快速找到其前驱结点和后继结点;
- 从任一结点出发可以访问其他结点。
-
双链表结点类型DLinkNode声明:
typedef struct DNode //双链表结点类型
{ ElemType data;
struct DNode *prior; //指向前驱结点
struct DNode *next; //指向后继结点
} DLinkNode;
1.双链表插入结点操作
- 操作语句:
s->next = p->next
p->next->prior = s
s->prior = p
p->next = s
2.双链表删除结点操作
- 操作语句:
p->next->next->prior = p
p->next = p->next->next
3.建立双链表
- 整体建立双链表也有两种方法:头插法和尾插法。与单链表的建表算法相似,主要是插入和删除的不同。
- 头插法建立双链表
void CreateListF(DLinkNode *&L,ElemType a[],int n)
{
DLinkNode *s; int i;
L=(DLinkNode *)malloc(sizeof(DLinkNode)); //创建头结点
L->prior=L->next=NULL; //前后指针域置为NULL
for (i=0;i<n;i++) //循环建立数据结点
{
s=(DLinkNode *)malloc(sizeof(DLinkNode));
s->data=a[i]; //创建数据结点s
s->next=L->next; //将s插入到头结点之后
if (L->next!=NULL) //若L存在数据结点,修改前驱指针
L->next->prior=s;
L->next=s;
s->prior=L;
}
}
- 尾插法建立双链表
void CreateListR(DLinkNode *&L,ElemType a[],int n)
{
DLinkNode *s,*r;
int i;
L=(DLinkNode *)malloc(sizeof(DLinkNode)); //创建头结点
L->prior=L->next=NULL; //前后指针域置为NULL
r=L; //r始终指向尾结点,开始时指向头结点
for (i=0;i<n;i++) //循环建立数据结点
{
s=(DLinkNode *)malloc(sizeof(DLinkNode));
s->data=a[i]; //创建数据结点s
r->next=s;s->prir=r; //将s插入r之后
r=s; //r指向尾结点
}
r->next=NULL; //尾结点next域置为NULL
}
4.线性表基本运算在双链表中的实现
- 双链表的插入算法:
bool ListInsert(DLinkNode *&L,int i,ElemType e) //在第i个元素后插入e
{
int j=0;
DLinkNode *p=L,*s; //p指向头结点,j设置为0
while (j<i-1 && p!=NULL) //查找第i-1个结点
{
j++;
p=p->next;
}
if (p==NULL) //未找到第i-1个结点,返回false
return false;
else //找到第i-1个结点p,在其后插入新结点s
{
s=(DLinkNode *)malloc(sizeof(DLinkNode));
s->data=e; //创建新结点s
s->next=p->next; //在p之后插入s结点
if (p->next!=NULL) //若存在后继结点,修改其前驱指针
p->next->prior=s;
s->prior=p;
p->next=s;
return true;
}
}
- 双链表的删除算法:
bool ListDelete(DLinkNode *&L,int i,ElemType &e) //删除第i个节点
{
int j=0; DLinkNode *p=L,*q; //p指向头结点,j设置为0
while (j<i-1 && p!=NULL) //查找第i-1个结点
{
j++;
p=p->next;
}
if (p==NULL) //未找到第i-1个结点
return false;
else //找到第i-1个结点p
{
q=p->next; //q指向第i个结点
if (q==NULL) //当不存在第i个结点时返回false
return false;
e=q->data;
p->next=q->next; //从双单链表中删除q结点
if (q->next!=NULL) //若q结点存在后继结点
q->next->prior=p; //修改q结点后继结点的前驱指针
free(q); //释放q结点
return true;
}
}
3.4循环链表
- 循环链表是另一种形式的链式存储结构形式。
- 循环单链表:将表中尾结点的指针域改为指向表头结点,整个链表形成一个环。由此从表中任一结点出发均可找到链表中其他结点。
- 循环双链表:形成两个环。