数据结构——第二章 线性表
第二章 线性表
2.1 线性表的定义和基本操作
- 线性表(Linear List)是具有相同数据类型的n(n>0)个数据元素的有限序列,其中n为表长,当n=0时线性表是一个空表;若用L命名线性表,则其一般表示为:L=(a1,a2,…,an)
- ai是线性表中的“第i个”元素线性表中的位序;a1是表头元素,an是表尾元素;除第一个元素外,每个元素有且仅有一个直接前驱;除最后一个元素外,每个元素有且仅有一个直接后继
- 注意:位序从1开始,数组下标从0开始
- C语言函数的定义:<返回值类型>函数名(<参数1类型>参数1,<参数2类型>参数2,…)
- 函数名、参数的形式和命名都可以改变,但要有可读性
- 传入引用“&”,对参数的修改结果需要带回来
2.2 顺序表
2.2.1 顺序表的定义
- 顺序表:用顺序存储的方式实现线性表顺序存储
- 把逻辑上相邻的元素存储在物理位置上也相邻的存储单元中,元素之间的关系由存储单元的领接关系来体现
- C语言中知道一个数据元素的大小:
sizeof(ElemType)
- 顺序表中也可以存放结构类型的数据,命名为Customer的结构,其中包含两个整数,即此数据类型占8个字节
typedef sruct{
int num;
int people;
}Customer
*顺序表的实现——静态分配
#define MaxSize 10 //定义最大长度
typedef struct{
ElemType data[MaxSize]; //用静态的"数组"存放数据元素
int length; //顺序表的当前长度
}SqList; //顺序表的类型定义(静态分配方式)
- 给各个数据元素分配连续的存储空间,大小为
MaxSize*sizeof(ElemType)
- Sq:sequence ——顺序,序列
#include<stdio.h>
#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; //将所有数据元素设置为默认初始值
L.length=0;
}
int main(){
SqList L; //声明一个顺序表
InitList(L); //初始化一个顺序表
//尝试“违规”打印整个data数组
for(int i=0;i<MaxSize;i++){
printf("data[%d]=%d\n",i,L.data[i]);
}
return 0;
}
- 如果没有设置数据元素的默认值,内存中会有遗留的“脏数据”
- 在内存中分配存储顺序表L的空间,包括
MaxSize*sizeof(ElemType)
和存储length的空间
*顺序表的实现——动态分配
#define InitSize 10 //默认的最大长度
typedef struct{
ElemType *data; //指示动态分配数组的指针
int MaxSize; //顺序表的最大容量
int length; //顺序表的当前长度
}SeqList; //顺序表的类型定义(静态分配方式)
- C++动态申请和释放内存空间:new、delete关键字
- C语言动态申请和释放内存空间:malloc、free函数
L.data =(ElemType*)malloc(InitSize*sizeof(ElemType)) ;
malloc函数返回一个指针,需要强制转型为定义的数据元素类型指针(ElemType*)
#include<stdlib.h>//malloc、free函数的头文件
#define InitSize 10
typedef struct{
int *data;
int MaxSize;
int length;
}SeqList;
void InitList(SeqList &L){
L.data =(int*)malloc(InitSize*sizeof(int)); //用malloc 函数申请一片连续的存储空间
L.length=0;
L.MaxSize=InitSize;
}
//增加动态数组的长度
void IncreaseSize(SeqList &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(void){
SeqList L;
InitList(L);
//......
IncreaseSize(L,5);
return 0;
}
- 虽然动态分配可以改变顺序表的大小,但时间开销大
- realloc函数也可实现增加数组长度
- 顺序表的特点:
- 随机访问 ,即可以在O(1)时间内找到第i个元素
- 存储密度高,每个节点只存储数据元素
- 拓展容量不方便
- 插入、删除操作不方便,需要移动大量元素
2.2.2(1) 顺序表的插入删除
*顺序表的基本操作——插入
插入操作:在表L中的第i个位置上插入指定元素e
#define MaxSize 10
typedef struct{
int data[MaxSize];
int Length;
}SqList;
//基本操作——在L的位序i处插入元素e
void ListInsert(SqList &L, int i, int e){
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;
}
int main(){
SqList L;
InitList(L);
//...插入几个元素
ListInsert(L,3,3);
return 0;
}
//bool类型实现插入操作——具有健壮性
bool ListInsert(SqList &L, int i, int 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--){
L.data[j]=L.data[j-1];
}
L.data[i-1]=e;
L.length++;
return true;
}
-
注意:位序、数组下标的关系,并从后面的元素依次移动
-
平均时间复杂度 : T(n)=O(n)
*顺序表的基本操作——删除
删除操作:删除表L中第i个位置的元素,并用e返回删除元素的值
bool LisDelete(SqList &L, int i, int &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-1]=L.data[j];
L.length--; //线性表长度减1
return true;
}
int main(){
SqList L;
InitList(L);
//...插入几个元素
int e = -1; //用变量e把删除的元素“带回来”
if(LisDelete(L,3,e))
printf("已删除第三个元素,删除元素值=%d\n",e);
else
printf("位序i不合法,删除失败\n");
return 0;
}
-
注意:位序、数组下标的关系,并从前面的元素依次移动
-
平均时间复杂度 : T(n)=O(n)
2.2.2(2) 顺序表的查找
*顺序表的按位查找
- 按位查找操作:获取表L中第i个位置的元素的值
//基于静态分配的代码实现
#define MaxSize 10
typedef struct{
ElemType data[MaxSize];
int Length;
}SqList;
ElemType GetElem(SqList L, int i){
return L.data[i-1]; //注意是i-1
}
//基于动态分配的代码实现
#define InitSize 10
typedef struct{
ElemType *data;
int MaxSize;
int length;
}SeqList;
ElemType GetElem(SqList L, int i){
return L.data[i-1]; //指针也能用数组下标,和访问普通数组的方法一样
}
- 平均时间复杂度 : T(n)=O(1);具有“随机存取”特性
*顺序表的按值查找
- 按值查找操作:在表L中查找具有给定关键字值的元素
#define InitSize 10
typedef struct{
ElemTyp *data;
int Length;
}SqList;
int LocateElem(SqList L, ElemType e){
for(int i=0; i<L.lengthl i++)
if(L.data[i] == e)
return i+1; //数组下标为i的元素值等于e,返回其位序i+1
return 0; //退出循环,说明查找失败
}
- 基本数据类型:int、char、double、float等可以直接用运算符“==”比较;但如果为结构类型不能直接进行比较,需要依次对比各个分量来判断两个结构体是否相等,更好的办法为定义一个函数比较
typedef struct{
int num;
int people;
}Customer;
void test(){
Customer a;
a.num=1;a.people=1;
Customer b;
b.num=1;b.people=1;
if (a.num == b.num && a.people == b.people){
printf("相等");
}else{
printf("不相等");
}
}
//更好的办法:定义一个函数
bool isCustomerEqual(Customer a,Customer b){
if(a.num == b.num && a.people == b.people)
return true;
else
return false;
}
- 平均时间复杂度 : T(n)=O(n)
2.3 单链表
2.3.1 单链表的定义
-
单链表:每个结点除了存放数据元素外,还要存储指向下一个结点的指针
-
顺序表的优点:可随机存取,存储密度高;缺点:要求大片连续空间,改变容量不方便
-
链表的优点:不要求大片连续空间,改变容量方便;缺点不可随机存取,要耗费一定的空间存放指针
-
typedef关键字——数据类型重命名;使用方法:typedef <数据类型> <别名> 例如:typedef int zhengshu;
*单链表的定义
typedef struct LNode{
ElemType data;
struct LNode *next;
}Lnode,*Linklist;
//相当于上述代码
struct LNode{
ElemType data;
struct LNode *next;
}
typedef struct LNode LNode; //强调这是一个单链表
typedef struct LNode *Linklist; //强调这是一个结点
*不带头节点的单链表初始化
typedef struct LNode{
ElemType data;
struct LNode *next;
}Lnode,*Linklist;
//初始化一个空的单链表
bool InitList(LinkList &L){
L=NULL; //没有结点设为空表,为防止脏数据
return true;
}
void test(){
Linklist L;
InitList (L);
//...后续代码...
}
//判断单链表是否为空
bool Empty(LinkList L){
if(L==NULL)
return true;
else
return false;
}
//或
bool Empty(LinkList L){
return(L==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=NULL;
return true;
}
void test(){
Linklist L;
InitList (L);
//...后续代码...
}
//判断单链表是否为空
bool Empty(LinkList L){
if(L->next==NULL)
return true;
else
return false;
}
- 带头结点的代码,操作更方便
2.3.2(1) 单链表的插入删除
*按位序插入(带头结点)
- 插入操作:在表L中的第i个位置上插入指定元素e
- 操作分析:
- 找到第i-1个结点
- 将新结点插入其后
typedef struct LNode{
ElemType data;
struct LNode *next;
}Lnode,*Linklist;
bool ListInsert(LinkList &L,int i,ElemType e){
if(i<1)
return false;
LNode *p;
int j=0; //j表示当前p指向的是第几个结点
p=L; //p指向头结点L,头结点是第0个结点
while(p!=NULL&&j<i-1){
p=p->next;
j++;
}
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;
}
- 平均时间复杂度 : T(n)=O(n)
*按位序插入(不带头结点)
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;
int j=1;
p=L; //p指向第一个结点(不是头结点)
while(p!=NULL&&j<i-1){ //后续逻辑和带头结点的相同
p=p->next;
j++;
}
if(p==NULL)
return false;
LNode *s=(LNode*)malloc(sizeof(LNode));
s->data=e;
s->next=p->next;
p->next=s;
return true;
}
*指定结点的后插操作
- 后插操作:在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;
return true;
}
//“封装” 在第i个位置插入元素e也可以简化为
bool ListInsert(LinkList &L,int i,ElemType e){
if(i<1)
return false;
LNode *p;
int j=0; //当前p指向的是第几个结点
p=L; //p指向头结点
while(p!=NULL&&j<i-1){
p=p->next;
j++;
}
return InsertNextNode(p,e); //进行后插操作
}
*指定结点的前插操作
-
前插操作:在p结点之前插入元素e或结点s
-
方法一:传入头结点,循环查找p的前驱q,再对q后插,时间复杂度为O(n)
-
方法二:申请新结点,作为p的后继结点,交换两结点数据,时间复杂度为O(1)
//在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->data=p->data;
p->data=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;
ElemType temp=p->data; //交换数据域部分
p->data=s->data;
s->data=temp;
return true;
}
*按位序删除(带头结点)
- 删除操作:删除表L中第i个位置的元素,并用e返回删除元素的值
- 操作分析:
- 找到第i-1个结点,指向第i+1个结点
- 释放第i个结点
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==NULL)
return false; //与按位序插入相似
if(p->next=NULL) //第i-1个结点之后无其他结点
return false;
LNode *q=p->next; //q指向被删结点
e=q->data; //e返回被删元素
p->next=q->next; //断开,指向第i+1个结点
free(q);
return true;
}
- 平均时间复杂度 : T(n)=O(n)
*指定结点的删除
-
方法一:传入头结点,循环查找p的前驱q,再删除
-
方法二:q指针指向p的后继结点,后q数据复制到前p,指向q的下一结点并删除q
bool DeleteNode(LNode *p0){
if(p==NULL)
return false;
LNode *q=p->next;
p->data=p->next->data; //存在bug:可能p指针为最后一个结点
p->next=q->next; //此时,只能用方法一
free(q):
return ture;
}
- 如果p是最后一个结点,只能从表头开始依次寻找p的前驱
- Tips:体会有“头结点”和“封装”的代码
2.3.2(2) 单链表的查找
*按位查找
- 按位查找:获取表L中第i个位置元素的值,返回第i个元素(与上述查找第i-1个类似)
- 平均时间复杂度 : T(n)=O(n)
LNode *GetELem(LinkList L,int i){
if(i<0) //考虑i=0时,为头结点情况;p=L指向头结点
return false;
LNode *p;
int j=0;
p=L;
while(p!=NULL&&j<i){ //i值不合法(大于链长);循环找到第i个结点
p=p->next;
j++;
}
return p;
}
//第二种写法
LNode *GetELem(LinkList L,int i){
int j=1; //p指向首元结点
LNode *p=L->next;
if(i==0) //考虑i=0时,为头结点情况;
return L;
if(i<1)
return NULL;
while(p!=NULL&&j<i){
p=p->next;
j++;
}
return p;
}
//在第i个位置插入元素e(带头结点)
bool ListInsert(LinkList &L,int i,ElemType e){
if(i<1)
return false;
LNode *p=*GetELem(L,int i-1);
return InsertNextNode(p,e);
}
//补充上述return后的函数
bool InsertNextNode(LNode *p,ElemType e){
if(p==NULL) //有必要,具有健壮性
return false; //考虑到GetELem函数的非法情况会返回NULL
LNode *s=(LNode*)malloc(sizeof(LNode));
if(s==NULL)
return false;
s->data=e;
s->next=p->next;
p->next=s;
return true;
}
//删除第i个位置的元素
bool ListDelete(LinkList &L,int i,ElemType &e){
if(i<1)
return false;
LNode *p=*GetELem(L,int i-1);
if(p==NULL)
return false;
if(p->next=NULL)
return false;
LNode *q=p->next;
e=q->data;
p->next=q->next;
free(q);
return ture;
}
- 体会“封装”的好处:避免重复代码,简介、易维护
*按值查找
- 按值查找:在表L中查找给定关键字值e的元素
LNode *LocateElem(LinkList L,ElemType e){
LNode *p=L->next;
while(p!=NULL&&p->data!=e)
p=p->next;
return p;
}
- 如果数据类型为结构类型就不能使用!=符号
*求表的长度
int Length(LinkList L){
int len=0;
LNode *p=L;
while(p->next!=NULL){
p=p->next;
len++;
}
return len;
}
- 平均时间复杂度 : T(n)=O(n)
2.3.2(3) 单链表的建立
*尾插法
-
操作分析:
-
初始化单链表
InitList(L)
-
设置变量length记录链表长度
-
while{ //每次取一个元素e; //调用ListInsert(L,length+1,e);把元素插到尾部 //length++; }
-
-
ListInsert(L,length+1,e)
每次找表尾都要循环一次,时间复杂度O(n);可以设置一个表尾指针,然后进行后插操作
LinkList List_TailInsert(LinkList &L){ //尾插法 正向建立单链表
int x; //设ElemType为整形
L=(LinkList)malloc(sizeof(LNode)); //建立头结点,初始化空表
LNode *s,*r=L; //s为新结点,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;
}
*头插法
-
操作分析:
-
初始化单链表
InitList(L)
-
while{ //每次取一个元素e; //调用InsertNextNode(L,e);在头结点进行后插操作; //length++; }
-
LinkList List_HeadInsert(LinkList &L){ //头插法 逆向建立单链表
LNode *s;
int x;
L=(LinkList)malloc(sizeof(LNode));
L->next=NULL; //养成好习惯,初始为空链表,防止脏数据
scanf("%d",&x); //也可以输入一个链表或者数组
while(x!=9999){
s=(LNode*)malloc(sizeof(LNode));
s->data=x;
s->next=L->next;
L->next=s;
scanf("%d",&x);
}
return L;
}
-
重要应用:单链表的逆置,给定链表逆置的核心逻辑不改变
-
方法一:新建链表,按顺序取原链表元素,再采用头插法插入新链表
-
方法二:按顺序取结点插到头结点后,可以让链表原地逆置
2.3.3 双链表
*双链表的定义
- 双链表可进可退,但存储密度会更低
typedef struct DNode{
ElemType data; //数据域
struct DNode *prior,*next; //前驱和后继指针
}DNode,*DLinkList;
*双链表的初始化
typedef struct DNode{
ElemType data;
struct DNode *prior,*next;
}DNode,*DLinkList;
//初始化一个空的双链表
bool InitDLinkList(DLinkList &L){
L=(DNode*)malloc(sizeof(DNode)); //分配一个头结点;用DNode*强调为一个结点
if(L==NULL) //内存不足,分配失败
return false;
L->prior=NULL; //头结点的prior永远指向NULL
L->next=NULL;
return ture;
}
void testDLinkList(){
DLinkList L;
InitDLinkList(L);
//...后续代码...
}
//判断双链表是否为空
bool Empty(DLinkList L){
if(L->next==NULL)
return ture;
else
return false;
}
*双链表的插入
bool InsertNextDNode(DNode *p,DNode *s){
s->next=p->next;
p->next->prior=s;
s->prior=p;
p->next=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 ture;
}
-
按位序插入:找到某一个位序的前驱节点,进行后插操作
-
前插操作:找到给定结点的前驱节点,进行后插操作
*双链表的删除
bool DeleteNextDNode(DNode *p){ //p为删除结点前驱
if(p==NULL)
return false;
DNode *q=p->next;
if(q==NULL) //p没有后继结点,删除位置q为空
return false;
p->next=q->next;
if(q->next!=NULL) //删除位置q的后继结点不为空
q->next->prior=p;
free(q);
return true;
}
//销毁一个双链表
void DestoryList(DLinkList &L){
while(L->next!=NULL)
DeleteNextNNode(L);
free(L); //释放头结点
L=NULL; //头指针指向NULL
}
*双链表的遍历
while(p!=NULL){ //后向遍历
p=p->next;
}
while(p!=NULL){ //前向遍历
p=p->prior;
}
while(p->prior!=NULL){ //前向遍历(跳过头结点)
p=p->prior;
}
- 双链表不可随机存取,按位查找、按值查找操作都只能用遍历的方式实现
- 平均时间复杂度 : T(n)=O(n)
- 在双链表的插入和删除操作时,注意边界情况,如插入和删除结点为最后一个结点位置
2.3.4 循环链表
- 循环单链表:表尾结点的next指针指向头结点
*循环单链表的定义
typedef struct LNode{
ElemType data;
struct LNode *next;
}LNode,*LinkList;
*循环单链表的初始化
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 ture;
}
//判断循环单链表是否为空
bool Empty(LinkList L){
if(L->next==L)
return ture;
else
return false;
}
//判断结点p是否为循环单链表的表尾结点
bool isTail(LinkList L,LNode *p){
if(p->next==L)
return ture;
else
return false;
}
- 如果单链表频繁在表头、表尾操作,可以让L指向表尾元素(插入和删除时可能需要修改L)
*循环双链表的初始化
typedef struct DNode{
ElemType data;
struct DNode *prior,*next;
}DNode,*DinkList;
//初始化一个循环双链表
bool InitDLinkList(DLinkList &L){
L = (LNode*)malloc(sizeof(Lnode));
if(L==NULL){
return false;
}
L->next = L; //头结点的prior指向头结点;双链表指向NULL
L->prior = L; //头结点的next指向头结点
return true;
}
//判断循环双链表是否为空
bool Empty(DLinkList L){
if(L->next == L) //判断条件与双链表不同
return true;
else
return false;
}
//判断结点p是否为循环双链表的表尾结点
bool isTail(LinkList L,LNode *p){
if(p->next == L)
return true;
else
return false;
}
- 在使用循环双链表时,不需要考虑边界情况,如插入和删除结点为最后一个结点
2.3.5 静态链表
- 静态链表:分配一整片连续的内存空间,各个结点集中安置
- 单链表的指针域:指向下一个结点的指针(地址);静态链表的指针:指向下一个结点的数组下标
- 0号结点为头结点,游标充当指针,游标为-1表示已经到达表尾
*静态链表的定义
#define MaxSize 10
struct Node{ //静态链表结构类型的定义
ElemType data; //存储数据元素
int next; //下一个元素的数组下标
};
void testSLinkList(){
struct Node a[MaxSize];
//...后续代码
}
//另一种写法
#define MaxSize 10
struct Node{
ElemType data;
int next;
}SLinkList[MaxSize];
//等价于
#define MaxSize 10
struct Node{
ElemType data;
int next;
};
typedef struct Node SLinkList[MaxSize]; //可用SLinkList定义“一个长度为MaxSize的Node型数组”
//另一种写法
#define MaxSize 10
typedef struct{
ELemType data;
int next;
}SLinkList[MaxSize];
void testSLinkList(){
SLinkList a;
}
- 注意:
SLinkList a
强调a是静态链表;struct Node a
强调a是一个Node型数组、
*静态链表的基本操作
- 初始化静态链表:把a[0]的next设为-1
- 查找某个位序的结点(不是数组下标,是各个结点在逻辑上的顺序):从头结点出发挨个往后遍历结点;时间复杂度 T(n)=O(n)
- 插入位序为i的结点:
- 找到一个空的结点,存入数据元素
- 从头结点出发找到位序为i-1的结点
- 修改新结点的next
- 修改i-1号结点的next
- 删除某个结点:
- 从头结点出发找到前驱结点
- 修改前驱节点的游标
- 被删除节点next设为-2
- 静态链表:用数组的方式实现的链表
- 优点:增删操作不需要大量移动元素
- 缺点:不能随机存取,只能从头结点开始依次往后查
2.3.6 顺序表和链表的比较
-
逻辑结构:顺序表和链表都属于线性表,都是线性结构
-
存储结构:
- 顺序表——顺序存储;优点:支持随机存取,存储密度高;缺点:大片连续空间分配不方便,改变容量不方便
- 链表——链式存储;优点:离散的小空间分配方便,改变容量方便;缺点:不可随机存取,存储密度低
-
基本操作——创建:
- 顺序表:需要预分配大片连续空间。若分配空间过小,则之后不方便拓展容量;若分配空间过大,则浪费内存资源
- 静态分配:静态数组,容量不可改变
- 动态分配:动态数组,容量可以改变,但是需要移动大量元素,时间代价高(malloc(),free())
- 链表:只需要分配一个头结点或者只声明一个头指针
-
基本操作——销毁:
-
顺序表:修改 Length = 0
静态数组——系统自动回收空间
typedef struct{ ElemType *data; int MaxSize; int length; }SeqList;
动态分配:动态数组——需要手动free()
//创建动态数组 L.data = (ELemType *)malloc(sizeof(ElemType) *InitSize) //销毁动态数组 free(L.data); //malloc() 和 free() 必须成对出现
-
-
基本操作——增/删:
- 顺序表:插入/删除元素要将后续元素后移/前移;时间复杂度=O(n),时间开销主要来自于移动元素
- 链表:插入/删除元素只需要修改指针;时间复杂度=O(n),时间开销主要来自查找目标元素
-
基本操作——查:
- 顺序表:按位查找为O(1),按值查找为O(n);若表内元素有序,可在O(log2n)时间内找到
- 链表:按位查找和按值查找均为O(n)