2.1线性表的定义和基本操作
2.1.1线性表的定义
线性表是具有相同数据类型的n(n>=0)个数据元素的有限序列,其中n为表长,当n=0时线性表是一个空表。
位序: 从1开始,表示线性表中的第几个元素
下标: 从0开始
线性表中存在唯一的“第一个”数据元素,又称表头元素;唯一的“最后一个”数据元素,又称表尾元素。除第一个元素外,每个元素有且仅有一个直接前驱,除最后一个元素外,每个元素有且仅有一个直接后继。
2.1.2线性表的特点
- 表中元素的个数有限
- 表中元素具有逻辑上的顺序性,表中元素有其先后顺序
- 表中元素都是数据元素,每个元素都是单个元素
- 表中元素的数据类型都相同,这意味着每个元素占有相同大小的存储空间
- 表中元素具有抽象性,即仅讨论元素间的逻辑关系,而不考虑元素究竟表示什么内容
注: 线性表是一种逻辑结构,即线性结构,表示元素之间一对一的相邻关系;顺序表和链表是指存储结构,即顺序存储和链式存储。
2.1.3线性表基本操作
- InitList(&L):初始化表,构造一个空表
- DestroyList(&L):销毁表,并释放线性表L所占用的内存空间
- Length(L):求表长,即L中数据元素的个数
- LocateElem(L,e):按值查找,在表L中查找具有给定关键字值的元素
- GetElem(L,i):按位查找,获取表L中第i个位置的元素的值
- ListInsert(&L,i,e):插入操作,在表L中的第i个位置插入指定元素e
- ListDelete(&L,i,&e):删除操作,删除表L中第i个位置的元素,并用e返回删除元素的值
- PrintList(L):输出操作,按前后顺序输出线性表L中所有元素值
- Empty(L):判空操作,若L为空表,则返回true,否则返回false
注: ”&“表示C++语言中的引用调用,将操作结果“带回来 ”
void test(int x){
x=1024;
printf("test函数内部 x=%d\n",x);
}
int main(){
int x=1;
printf("调用test函数前 x=%d\n",x);
test(x);
printf("调用test函数后 x=%d\n",x);
}
/*
以上代码输出结果为:
调用test函数前 x=1
test函数内部 x=1024
调用test函数后 x=1
*/
//将test函数改为
void test(int &x){
x=1024;
printf("test函数内部 x=%d\n",x);
}
/*
以上代码输出结果为:
调用test函数前 x=1
test函数内部 x=1024
调用test函数后 x=1024
*/
//这就是&带回来效果
2.2线性表的顺序表示
2.2.1顺序表定义
线性表的顺序存储称为顺序表,它是一组地址连续 的存储单元依次存储线性表中的数据元素,从而使得逻辑上相邻的两个元素在物理位置上也相邻。
2.2.2顺序表特点
- 逻辑顺序与物理顺序相同
- 随机访问,即通过首地址和元素序号可在时间O(1)内找到指定的元素
- 存储密度高,每个节点只存储数据元素
- 拓展容量不方便
- 插入删除操作不方便
2.2.3静态分配与动态分配
//静态分配
#define MaxSize 50 //定义线性表最大长度
typedef struct{
ElemType data[MaxSize];//顺序表元素
int length;//顺序表当前长度
}SqList;//顺序表类型定义
//静态分配示例:
#define MaxSize 10 //定义最大长度
typedef struct{
int data[MaxSize];//用静态的“数组”存放数据元素
int length;//顺序表当前长度
}SqList;//顺序表类型定义
void InitList(SqList &L){//初始化一个顺序表
for(int i=0;i<MaxSize;i++){
L.data[i]=0;//将所有元素设置默认初始值,消除脏数据,可省略,因为相关操作都与length有关
L.length=0;//顺序表初始长度为0
}
}
int main(){
SqList L;//声明一个顺序表
InitList(L);//初始化顺序表
//...相关操作
return 0;
}
//动态分配
#define InitSize 100 //表长度的初始定义
typedef struct{
ElemType *data;//指示动态分配数组的指针
int MaxSize,length;//数组最大容量和当前个数
}SeqList;//动态分配数组顺序表的类型定义
//动态分配示例:
#define InitSize 10 //默认最大长度
typedef struct{
int *data;//指示动态分配数组的指针
int MaxSize,length;//数组最大容量和当前个数
}SeqList;//动态分配数组顺序表的类型定义
void InitList(SeqList &L){//初始化
//用malloc函数申请一片连续存储空间
L.data=(int *)malloc(InitSize*sizeof(int));//申请空间类型强制转换为与顺序表数组类型一致
L.length=0;
L.MaxSize=InitSize;
}
void IncreaseSize(SeqList &L,int len){//动态增加数组长度
int *q=L.data;
L.data=(int *)malloc((L.MaxSize+len)*sizeof(int));
for(int i=0;i<L.length;i++){
L.data[i]=p[i];//将数据复制到新区域
}
L.MaxSize=L.MaxSize+len;//顺序表最大长度增加len
free(p);//释放原来空间
}
int main(){
SeqList L;//声明一个顺序表
InitList(L);//初始化顺序表
//...增加元素
IncreaseSize(L,5);//扩展数组长度
return 0;
}
2.3顺序表基本操作的实现
2.3.1插入
在顺序表L的第i(1<=i<=L.length+1)个位置插入新元素e。若i输入不合法,则返回false,表示插入失败;否则,将第i个元素及其后的所有元素依次往后移一个位置,腾出一个空位置插入新元素e,顺序表长度增加1,插入成功,返回true。
bool ListInsert(SqList &L,int i,ElemType e){
if(i<1||i>L.length+1)//判断i范围是否有效
return false;
if(L.length>=MaxSize)//当存储空间已满,不能插入
return false;
for(int j=L.length;j>=i;j--)//将第i个及之后的元素后移
L.data[j]=L.data[j-1];
L.data[i-1]=e;//在位置i放入e
L.length++;//线性表长度加1
return true;
}
注: 区别位序和数组下标,时间复杂度:
- 最好情况:在表尾插入(即i=n+1),元素后移语句不再执行,时间复杂度为O(1);
- 最坏情况:在表头插入(即i=1),元素后移语句将执行n次,时间复杂度为O(n);
- 平均情况:O(n)。
2.3.2删除
删除顺序表L中第i(1<=i<=L.length)个位置的元素,用引用变量e返回。若i不合法,则返回false;否则将被删元素赋给引用变量e,并将第i+1个元素及其后的所有元素往前移动一个位置,返回true。
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=1;j<L.length;j++)//将第i个位置后的元素前移
L.data[j-1]=L.data[j];
L.length--;//线性表长度减1
return true;
}
注: 区别位序和数组下标,时间复杂度:
- 最好情况:删除表尾元素(即i=n),无需移动元素,时间复杂度为O(1);
- 最坏情况:删除表头元素(即i=1),需移动除表头元素外的所有元素,时间复杂度为O(n);
- 平均情况:O(n)。
2.3.3查找
//按位查找:
ElemType GetElem(SqList L,int i){//i表示位序
return L.data[i-1];//返回数值,静态动态存储都一样
}
//时间复杂度O(1)
//按值查找:
int LocateElem(SqList L,ElemType e){//在顺序表中查找第一个元素值等于e的元素,并返回其位序
for(int i=0;i<L.length;i++)
if(L.data[i]==e)
return i+1;//下标为i的元素值等于e,返回其位序i+1
return 0;//退出循环,查找失败
}
/*
时间复杂度:
最好情况:查找元素在表头,仅比较一次,时间复杂度为O(1)
最坏情况:查找元素在表尾,需比较n次,时间复杂度为O(n)
平均情况:O(n)
*/
2.4线性表的链式表示
2.4.1单链表的定义
线性表的链式存储又称单链表 ,它是指通过一组任意的存储单元来存储线性表中的数据元素
单链表特点:
- 不需要大块存储单元
- 附加指针,存在浪费存储空间的缺点
- 非随机存取,查找特定结点需从头开始遍历
typedef struct LNode{//定义单链表结点类型
ElemType data;//数据域
struct LNode *next;//指针域
}LNode, *LinkList;//别名
/*
LNode * L:强调这是一个结点
LinkList L:强调这是一个单链表
*/
//不带头结点的单链表初始化:
bool InitList(LinkList &L){
L=NULL;//置空,防止存在脏数据
return true;
}
//判断单链表是否为空:
bool Empty(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 Empty(LinkList L){
return (L->next==NULL);
}
/*
带头结点,写代码更方便;
不带头结点,写代码更麻烦:
对第一个数据结点和后续数据结点的处理需要用不同的代码逻辑
对空表和非空表的处理需要用不同的代码逻辑
*/
2.4.2单链表插入删除操作
//后插操作:在p结点之后插入元素e
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;//元素e保存至s结点中
s->next=p->next;//s的指针域指向p的指针域,即将p之后结点连接到s后
p->next=s;//将结点s连接到p之后
return true;
}
//前插操作:在p结点之前插入元素e
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之后结点连接在s上
p->next=s;//将s连接在p上
s->data=p->data;//将p中元素复制到s中
p->data=e;//e覆盖p中元素
}
/*
时间复杂度O(1)
*/
//按位序插入:在带头结点的单链表L中的第i个位置插入元素e
bool ListInsert(LinkList &L,int i,ElemType e){
if(i<1)//非法位序
return false;
LNode *p;//指针p当前扫描到的结点
int j=0;//当前p指向的是第几个结点
p=L;//扫描开始位置,L指向头结点,头结点是第0个节点,不存数据
while(p!=NULL && j<i-1){//循环找到第i-1个结点
p=p->next;
j++;
}
return InsertNextNode(p,e);//当前p扫描到第i-1个位置,执行后插法
}
/*
最好时间复杂度:O(1),插在表头,即i=1
最坏时间复杂度:O(n),插在表尾,i=n
平均时间复杂度:O(n)
*/
//按位序插入:在不带头结点的单链表L中的第i个位置插入元素e
bool ListInsert(LinkList &L,int i,ElemType e){
if(i<1)//非法位序
return false;
if(i==1){//插入第一个结点的操作与其他结点不同
LNode *s=(LNode *)malloc(sizeof(LNode))
s->data=e;
s->next=L;
L=s;//头指针指向新的结点
return true;
}
LNode *p;//指针p当前扫描到的结点
int j=1;//当前p指向的是第几个结点
p=L;//扫描开始位置,L指向头结点,头结点是第0个节点,不存数据
while(p!=NULL && j<i-1){//循环找到第i-1个结点
p=p->next;
j++;
}
return InsertNextNode(p,e);//当前p扫描到第i-1个位置,执行后插法
}
//按位序删除:删除带头结点的单链表L的第i个元素并返回删除元素e
bool ListDelete(LinkList &L,int i,ElemType e){
if(i<1)//非法位序
return false;
LNode *p;//指针p当前扫描到的结点
int j=0;//当前p指向的是第几个结点
p=L;//扫描开始位置,L指向头结点,头结点是第0个节点,不存数据
while(p!=NULL && j<i-1){//循环找到第i-1个结点
p=p->next;
j++;
}
if(p==NULL)//无效结点,i值不合法
return false;
if(p->next==NULL)//第i-1结点后以无其他结点
return false;
LNode *q=p->next;//令q指向被删结点
e=q->data;//用e返回元素的值
p->next=q->next;//将*q结点从链中断开
free(q);//释放结点的存储空间
return true;//删除成功
}
//删除指定结点
bool DeleteNode(LNode *p){
if(p==NULL)
return false;
if(p->next==NULL)
return false;//最后一个元素无法删除
LNode *q=p->next;//令q指向*p的后继结点
p->data=q->data;//和后继节点交换数据域
p->next=q->next;//将*q结点从链中断开
free(q);//释放后继节点存储空间
return true;
}
2.4.3单链表查找与求表长操作
//查找带头结点的单链表L的第i个元素(按位查找)
LNode * GetElem(LinkList L,int i){
if(i<0)
return NULL;
LNode *p;//指针p当前扫描到的结点
int j=0;//当前p指向的是第几个结点
p=L;//扫描开始位置,L指向头结点,头结点是第0个节点,不存数据
while(p!=NULL && j<i){//循环找到第i个结点
p=p->next;
j++;
}
return p;
}
/*
平均时间复杂度O(n)
*/
//拓展:改写插入操作
//按位序插入:在带头结点的单链表L中的第i个位置插入元素e
bool ListInsert(LinkList &L,int i,ElemType e){
if(i<1)//非法位序
return false;
LNode *p = GetElem(L,i-1);
return InsertNextNode(p,e);//当前p扫描到第i-1个位置,执行后插法
}
/*
封装的好处:避免重复代码,简洁易维护
*/
//查找带头结点的单链表L的的数据域为e的结点(按值查找)
LNode * LocateElem(LinkList L,ElemType e){
LNode *p=L->next;
//从第一个结点开始查找数据域为e的结点
while(p!=NULL && p->data!=e)
p=p->next;
return p;//找到后返回该结点指针,否则返回NULL(没找到就已经指向了NULL)
}
//求表的长度
int Length(LinkList L){
int len=0;
LNode *p=L;
while(p->next!=NULL){
p=p->next;
len++;
}
return len;
}
2.4.4单链表的建立
//尾插法建立单链表
LinkList List_TailInsert(LinkList &L){
int x;//设数据域为整型
InitList(L);//初始化带头结点单链表
LNode *s,*r=L;//r为表尾指针
scanf("插入数据:%d",&x);
while(x!=0000){
s=(LNode *)malloc(sizeof(LNode));
s->data=x;
r->next=s;
//以上三行代码等价于:InsertNextNode(r,x);//后插操作,内有*s
r=s;//r指向新的表尾结点
scanf("插入数据:%d",&x);
}
r->next=NULL;//表尾结点指针置空
return L;
}
/*
时间复杂度:O(n)
*/
//尾插法笨方法,时间复杂度O(n^2)
LinkList list_TailInsert_II(LinkList &L){
InitList(L);//初始化带头结点单链表
int x,length=0;//数据域设为整型,长度初始化
scanf("插入数据:%d",&x);
while(x!=0000){
ListInsert(L,length+1,x);//按位序插入:在带头结点的单链表L中的第length+1个位置插入元素x
length++;
scanf("插入数据:%d",&x);
}
return L;
}
//头插法建立单链表
LinkList List_HeadInsert(LinkList &L){
int x;
InitList(L);//初始化
scanf("插入数据:%d",&x);
while(x!=0000){
InsertNextNode(L,x);//后插操作,头结点后插入x
scanf("插入数据:%d",&x);
}
return L;
}
/*
时间复杂度O(n)
*/
2.4.5双链表
双链表结点中有两个指针prior和next,分别指向其前驱结点和后继结点。
//双链表结点类型描述
typedef struct DNode{//D:double
ElemType data;//数据域
struct DNode *prior,*next;//前驱和后继指针
}DNode,*DLinkList;
//初始化双链表(带头结点)
bool InitDLinkList(DLinkList &L){
L=(DNode *)malloc(sizeof(DNode));//分配一个头结点
if(L==NULL)//内存不足,分配失败
return false;
L->prior=NULL;//头结点的prior永远指向NULL
L->next=NULL;//头结点之后暂时还没有结点
return true;
}
//判断双链表是否为空(带头结点)
bool Empty(DLinkList L){
if(L->next==NULL)
return true;
else
return false;
}
//双链表的插入:在p结点之后插入s结点
bool InsertNextDNode(DNode *p,DNode *s){
if(p==NULL || s==NULL)
return false;
s->next=p->next;
if(p->next!=NULL)
p->next->prior=s;
s->prior=p;
p->next=s;
return true;
}
//双链表删除:删除p结点的后继结点
bool DeleteNextDNode(DNode *p){
if(p==NULL) return false;
DNode *q=p->next;//找到p的后继结点
if(q==NULL) return false;//p没有后继
p->next=q->next;
if(q->next!=NULL)//q结点不是最后一个结点
q->next->prior=p;
free(q);//释放q
return true;
}
//双链表的销毁
void DestoryList(DLinkList &L){
//循环释放头结点的后继结点
while(L->next!=NULL)
DeleteNextDNode(L);
free(L);//释放头结点
L=NULL;//头指针指向NULL
}
/*
双链表不可随机存取,按位查找、按值查找都只能用遍历的方式实现,时间复杂度O(n)
*/
2.4.6其他链表
- 循环单链表:最后一个结点指针指向头结点
- 循环双链表:头结点prior指针指向尾结点;尾结点next指针指向头结点
- 静态链表:数组
2.5顺序表和链表的比较
- 存取方式
顺序表:顺序存取和随机存取
链表:顺序存取 - 逻辑结构与物理结构
顺序表:逻辑上相邻,物理上也相邻
链表:物理不相邻,逻辑关系用指针链接 - 查找、插入和是删除操作
顺序表:无序按值查找O(n),有序按值查找O(log2n);按位查找O(1);插入删除需要移动大量元素
链表:按值查找O(n);按位查找O(n);插入删除只需要修改相关指针 - 空间分配
顺序表:静态存储空间不可扩充,动态操作麻烦
链表:操作灵活高效