第二章 线性表
定义:线性表是具有相同数据类型的n(n>=0)个**数据元素的有限**序列。
“&”表示C++语言中的引用调用,在C语言中采用指针也可以达到相同的效果。
线性表主要操作(参考即可):
InitList(&L) //初始化表。构造一个空的线性表。
Length(L) //求表长。返回线性表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。
DestoryList(&L) //销毁操作。销毁线性表,并释放线性表L所占的内存空间。
线性表的顺序表示
定义:线性表的顺序存储又称顺序表。
特点:
- 表中元素的逻辑顺序和物理顺序相同
- 最主要的特点是随机访问,具有随机存取的存储结构
- 通常用数组来描述
- 存储密度高,每个节点只存储数据元素。
- 拓展容量不方便(即使使用动态分配的方式实现,时间复杂度也比较高)
- 插入、删除操作不方便,需要移动大量元素
空间分配
静态分配
缺点:由于数组的大小和空间实现固定,一旦空间占满,再加入新的数据会导致溢出,进而程序崩溃。
#define MaxSize 50 //定义线性表的最大长度
typedef struct{ //定义结构体
ElemType data[MaxSize]; //顺序表的元素
int length; //顺序表的当前长度
}SqList; //定义结构体的名字为Sqlist
//定义了一个名为SqList的结构体
//SqList L; //即声明一个SqList类型的变量L
//L.length; L.data[i]; //获取成员变量
动态分配
存储数组的空间是在程序执行过程中,通过动态存储分配语句分配的,一旦数据空间占满,就另外开辟一块更大的存储空间,用以替换原本的存储空间。
#include<stdlib.h> //malloc、free函数的头文件
#define InitSize 100 //表长度的初始定义
//定义结构体
typedef struct{
ElemType *data; //指示动态分配数组的指针
int MaxSize; //数组的最大容量
int length; //数组的当前长度
}SqList; //使用typedef将结构体命名为Sqlist
void InitList(SqList &L){
//用malloc函数申请一片连续的存储空间
L.data=(int*)malloc(InitSize*sizeof(int));
L.length=0;
L.MaxSize=InitSize;
}
void IncreaseSize(Sqlist &L,int len){
//增加动态数组的长度
int *p=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(){
SqList L; //声明一个顺序表
InitList(L); //初始化顺序表
//往顺序表中随便插入几个元素
IncreaseSize(L,5);
return 0;
}
基本操作
插入操作
在顺序表的第i个位置插入新元素e。
时间复杂度:最好O(1)、最差O(n)、平均O(n)。
//'&L'表示取L的地址,直接通过地址对L进行操作,不用返回值,因为操作的就是L本体。
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;
}
//注意区分顺序表的位序和数组下标,位序从1开始,下标从0开始
删除操作
删除顺序表L中第 i ( i <= i <= L.length)个位置的元素,用引用变量e返回。
时间复杂度:最好O(1)、最差O(n)、平均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--) //将第i个元素及之后的元素前移
L.data[j]=L.data[j-1];
L.length--; //线性表长度减1
return true;
}
按值查找(顺序查找)
在顺序表L中查找第一个元素值等于e的元素,并返回其位序。
时间复杂度:最好O(1)、最差O(n)、平均O(n)。
int LocateElem(SqList L, ElemType e){ //无需修改L,故只需正常传入L即可
for(int i=0; i<L.length; i++)
if(L.data[i]==e)
return i+1; //下标为i的元素值等于e,返回其位序i+1
return 0; //退出循环,说明查找失败
}
线性表的链式表示
链式存储线性表时,不需要使用地址连续的存储单元,即不要求逻辑上相邻的元素在物理上也相邻。插入或删除只需要修改指针就可,不可以随机存取。
单链表
定义:线性表的链式存储又称单链表。通过一组任意的存储单元来存储线性表中的数据元素。非随机存取的存储结构。
单链表中结点类型的描述如下:
typedef struct LNode{ //定义单链表结点类型
ElemType data; //数据域——存放数据元素
struct LNode *next; //指针域——指向其后继的指针//next指针指向的类型为LNode结构体类型
}LNode, *LinkList;
为了便于理解,将其简化,等价于:
//定义了一个struct LNode类型并初始化一个结点
struct LNode{ //标准定义,定义一个LNode结构类型
ElemType data;
struct LNode *next;
};
typedef struct LNode LNode1; //使用typedef将struct LNode重命名为LNode1
LNode1 *LinkList; //定义一个名为LinkList的指向LNode1数据类型的指针
//本质上等价
//LinkList ————强调这是一个单链表
//LNode* ————强调这是一个结点
优缺点:利用单链表可以解决顺序表需要大量连续存储单元的缺点,但附加指针域,造成浪费存储空间的缺点。
头指针:头指针通常用来标识一个单链表,如单链表L,头指针为NULL时表示一个空表。
头节点:为了操作方便,在单链表第一个结点之前附加一个结点,称为头节点。头节点数据域可以为空,也可以用来记录表长等信息。
引入头节点的优点:
- 由于第一个数据结点的位置被存放在头结点的指针域中,因此在链表的第一个位置上的操作和在表的其他位置的操作一致,无须进行特殊处理。
- 无论链表是否为空,其头指针都指向头结点的非空指针(空表中头结点的指针域为空),因此空表和非空表的处理也就得到了统一。
头结点
不带头结点的单链表
typedef struct LNode{ //定义单链表节点类型
ElemType data; //每个节点存放一个数据元素
struct LNode *next; //指针指向下一节点
}LNode,*LinkList;
//初始化一个空的单链表
bool InitList(LinkList &L){
L=NULL; //空表,暂时还没有任何节点
return true;
}
//判断单链表是否为空
bool Empty(LinkList L){
return (L==NULL);
}
void test(){
LinkList L; //声明一个指向单链表的指针
InitList(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=NULL; //头结点之后暂时还没有节点
return true;
}
//判断单链表是否为空
bool Empty(LinkList L){
return (L->next==NULL);
}
void test(){
LinkList L; //声明一个指向单链表的指针
InitList(L);//初始化一个空表
//后续代码
}
比较
带头结点更方便
查找
按位查找
时间复杂度:O(n)
//按位查找,返回第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个结点开始找,查找第i个结点
p=p->next;
j++;
}
return p; //返回第i个结点的指针,若i大于表长则返回NULL
}
按值查找
时间复杂度:O(n)
//按值查找,找到数据域==e的结点
LNode * LocateElem(LinkList L, ElemType e){
LNode *p=L->next;
//从第一个结点开始查找数据域为e的结点
while(p!=NULL&&p->data!=e)
p=p->next;
return p; //找到后返回该结点指针,否则返回NULL
}
求表的长度
时间复杂度:O(n)
//求表的长度
int Length(LinkList L){
int len=0; //统计表长
LNode *p=L;
while(p->next!=NULL){
p=p->next;
len++;
}
return len;
}
插入
按位序插入(带头结点)
时间复杂度:最坏O(n),最好O(1),平均O(n)。
//在第i个位置插入元素e(带头结点)
bool ListInsert(LinkList &L, int i, ElemType e){
if(i<1)
return false;
LNode *p=GetElem(L,i-1); //找到第i-1个结点
if(p==NULL) //i值不合法
return false;
LNode *s=(LNode*)malloc(sizeof(LNode));
s->data=e;
s->next=p->next;
p->next=s; //将结点s连到p之后
return true; //插入成功
}
按位序插入(不带头结点)
//在第i个位置插入元素e(不带头结点)
bool ListInsert(LinkList &L, int i, ElemType e){
if(i<1)
return false;
if(i==1){ //插入第1个结点的操作与其他结点操作不同
LNode *s=(LNode*)malloc(sizeof(LNode));
s->data=e;
s->next=L;
L=s;
return true;
}
LNode *p=GetElem(L,i-1); //找到第i-1个结点
if(p==NULL) //i值不合法
return false;
LNode *s=(LNode*)malloc(sizeof(LNode));
s->data=e;
s->next=p->next;
p->next=s; //将结点s连到p之后
return true; //插入成功
}
指定节点的后插操作
时间复杂度O(1)
//后插操作:在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; //用结点s保存数据元素e
s->next=p->next;
p->next=s; //将结点s连到p之后
return true;
}
//在第i个位置插入元素e(带头结点)
bool ListInsert(LinkList &L, int i, ElemType e){
if(i<1)
return false;
LNode *p=GetElem(L,i-1); //找到第i-1个结点
return InsertNextNode(p,e); //体会封装!
}
指定结点的前插操作
//前插操作:在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->next=s; //将结点s连到p之后
s->data=p->data;//将p中元素复制到e
p->data=e; //p中元素覆盖为e
return true;
}
//前插操作:在p结点之前插入结点s
bool InsertPriorNode(LNode *p, LNode *s){
if(p==NULL||s==NULL)
return false;
s->next=p->next;
p->next=s; //将结点s连到p之后
ElemType temp=p->data; //交换数据域部分
p->data=s->data;
s->data=temp;
return true;
}
删除
按位序删除(带头结点)
//按位序删除
bool ListDelete(LinkList &L, int i, ElemType &e){
if(i<1)
return false;
LNode *p=GetElem(L,i-1); //找到第i-1个结点
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; //删除成功
}
指定结点的删除
//删除指定结点p
bool DeleteNode(LNode *p){
if(p==NULL)
return false;
LNode *q=p->next; //令q指向*p的后继节点
p->data=p->next->data; //和后继节点交换数据域
p->next=q->next; //将*q结点从链中“断开”
free(q); //释放后继节点的存储空间
return true;
}
建立
头插法建立单链表
从空表开始,生成新节点,将读取到的数据存放在新节点的数据域,然后将新结点插入到当前链表的表头,即头结点之后。读入数据的顺序与生成的链表中的元素顺序相反。设单链表长为n。
时间复杂度:O(n)
可以用于链表逆置,常考
头插法建立单链表的算法如下:
Linklist List_HeadInsert(LinkList &L){ //逆向建立单链表
LNode *s; //定义一个指向LNode类型的指针s
int x;
L=(LinkList)malloc(sizeof(LNode)); //创建头结点
L->next=NULL; //初始为空链表
scanf("%d", &x); //输入结点的值
while(x!=9999){ //输入9999表示结束
s=(LNode*)malloc(sizeof(LNode));//创建新结点
s->data=x;
s->next=L->next;
L->next=s; //将新结点插入表中,L为头指针
scanf("%d",&x);
}
return L;
}
尾插法建立单链表
头插法简单,但顺序不一致。尾插法可以使两者次序一致。此法将新节点插到当前链表的表尾,为此需要增加一个尾指针r,使其始终指向当前链表的尾结点。
时间复杂度:O(n)
尾插法建立单链表的算法如下:
Linklist List_TailInsert(LinkList &L){ //正向建立单链表
int x; //设元素类型为整型
L=(LinkList)malloc(sizeof(LNode)); //创建头结点
LNode *s,*r=L; //r为表尾指针
scanf("%d", &x); //输入结点的值
while(x!=9999){ //输入9999表示结束
s=(LNode*)malloc(sizeof(LNode));//创建新结点
s->data=x;
r->next=s;
r=s; //r指向新的表尾结点
scanf("%d",&x);
}
r->next=NULL; //尾结点指针置空
return L;
}
*插入结点操作
插入结点操作将值为x的新结点插入到单链表的第i个位置上。先检查插入位置的合法性,然后找到待插入位置的前驱结点,即第i-1个结点,再在其后插入新节点。
首先调用按序号查找算法GetElem(L, i-1),查找第i-1个结点。假设返回的第i-1个结点为* p,然后令新节点* s的指针域指向* p的后继结点,再令结点* p的指针域指向新插入的结点* s。
实现插入结点的代码片段如下:(O(n))
p=GetElem(L,i-1); //查找插入位置的前驱结点
s->next=p->next; //令待插入结点s的指针域指向p的后继结点
p->next=s; //将s赋给p的后继指针,即将p的后继节点改为s
*删除结点操作
即删除单链表的第i个结点。先检查删除位置合法性,后查找出表中第i-1个结点,即被删结点的前驱结点,再将其删除。
实现删除结点的代码片段如下:(O(n))
p=GetElem(L, i-1); //查找删除位置的前驱结点
q=p->next; //令q指向被删除结点
p->next=q->next //将*q结点从链中“断开”
free(q); //释放结点的存储空间
双链表
因为单链表只有一个向后的指针,所以只能从前往后按顺序遍历。为了克服这个问题,引入双链表,双链表有两个指针prior和next,分别指向其前驱和后继结点。
双链表中结点类型的描述如下:
typedef struct DNode{ //定义双链表结点类型
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){
return(L->next==NULL);
}
void testDLinkList(){
//初始化双链表
DLinklist L;
InitDLinkList(L);
//后续代码
}
插入
//在双链表中p所指的结点之后插入结点*s。
bool InsertNextDNode(DNode *p,DNode *s){
//以p为基准来找后面的结点的,所以p的后继指针时最后改的。
if(p==NULL||s==NULL) //非法参数
return false;
s->next=p->next;//将结点*s插入到结点*p之后。待插入节点s的后继指针——>p结点的后继结点
if(p->next!=NULL) //若p结点有后继结点
p->next->prior=s; //p后继结点的前驱指针指向s
s->prior=p; //s的前驱指针指向p
p->next=s; //p的后继指针指向s
return true;
}
删除
//删除p结点的后继结点
bool DeleteNextDNode(DNode *p){
if(p==NULL)
return false;
DNode *q=p->next; //找到p的后继节点q
if(q==NULL) //p没有后继
return false;
p->next=q->next;//将待删除结点q的后继结点与结点p的后继指针链接,从前往后
if(q->next!=NULL) //q不是最后一个结点
q->next->prior=p;//将待删除结点的后继节点的前驱指针赋为p,从后往前
free(q);
return true;
}
//循环释放各个数据结点
void DestoryList(DLinklist &L){
while(L->next!=NULL)
DeleteNextDNode(L);
free(L); //释放头结点
L=NULL; //头指针指向NULL
}
遍历
//后向遍历
while(p!=NULL){
//对结点p做相应处理,如打印
p=p->next;
}
//前向遍历
while(p!=NULL){
//对结点p做相应处理,如打印
p=p->prior;
}
//前向遍历(跳过头结点)
while(p->prior!=NULL){
//对结点p做相应处理,如打印
p=p->prior;
}
查找
双链表不可随机存取,按位查找、按值查找操作都只能用遍历的方式实现。时间复杂度为O(n)。
循环链表
循环单链表
定义:在单链表的基础上,最后一个结点的指针不是NULL,而改为指向头结点,从而形成一个环。
判空条件:头结点的指针是否等于头指针
特点:循环单链表可以从表中任意一个结点开始遍历整个链表。有时对单链表常做的操作在表头和表尾进行的,此时就对循环单链表不设头指针而仅设尾指针,效率更高。
(简而言之,因为它是一个环,而且单链表只能向一个方向遍历,若设头指针,每次就需要从第一个开始,遍历一圈,而设的是尾指针的话,有两个优点,第一可以很容易的找到尾结点,第二,因为是一个环,所以尾结点的下一个就是头结点,十分方便首位的操作)
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; //头结点的next指向头结点
return true;
}
//判断循环单链表是否为空
bool Empty(LinkList L){
return(L->next==L);
}
循环双链表
定义:在双链表的基础上,首尾相连,形成一个环。
判空条件:头结点的next指针是否指向了它自身
在循环双链表L中,某节点*p为尾结点时,p->next==L;当循环双链表为空表时,其头结点的prior和next都为L。
typedef struct DNode{
ElemType data;
struct DNode *prior,*next;
}DNode,*DinkList;
//初始化空的循环双链表
bool InitDLinkList(DLinklist &L){
L=(DNode *)malloc(sizeof(DNode));
if(L==NULL)
return false;
L->prior=L;
L->next=L;
return true;
}
void testDLinkList(){
//初始化循环双链表
DLinklist L;
InitDLinkList(L);
//后续代码
}
//判空
bool Empty(DLinklist L){
return(L->next==L);
}
静态链表
定义:借助数组来描述线性表的链式存储结构。结点也有数据域data和指针域next,静态链表的指针是结点的相对地址,又称游标。同顺序表相同,静态链表也要预先分配一块连续的内存空间。
静态链表结构类型的描述如下:
#define MaxSize 50 //静态链表最大长度
typedef struct{ //静态链表结构类型的定义
ElemType data; //存储数据元素
int next; //下一个元素的数组下标
}SLinkList[MaxSize];
//SLinkList b————相当于定义了一个长度为MaxSize的Node型数组
以next==-1为结束标志。
顺序表与链表的比较
略