文章目录
线性表的定义
线性表是具有相同数据类型的n(n>=0)个数据元素的有限序列
一般表示为:L=(a1,a2,…,an)
a1:表头元素,除a1外其他元素以前只有一个直接前驱
an:表尾元素,除an外其他元素以前只有一个直接后继
线性表的特点
1、表中元素个数有限
2、表中元素具有逻辑上的顺序性,即有先后次序
3、表中元素都是数据元素,每个元素都是单个元素
4、表中元素的数据类型都相同
5、表中元素具有抽象性,即只讨论元素间的逻辑关系,独立于存储结构
线性表的顺序表示
一、顺序表的定义
顺序表是线性表的顺序存储,是一组地址连续的存储单元依次存储线性表中的数据元素,使得逻辑上相邻的两个元素在物理位置也相邻。
位序:元素在线性表的存储位置,元素ai的位序为i,位序也等于数组下标+1。
顺序表的特点
1.可以随机访问,通过首地址和元素序号可以找到指定元素
2.表中元素的逻辑顺序与其物理顺序相同
3.存储密度高
线性表的顺序存储类型描述如下
1.静态分配数组
typedef int ElemType;
#define MaxSize 50
typedef struct
{
ElemType data[MaxSize]; //存放数据元素的数组
int length; //顺序表的当前长度
}SqList;
2.动态分配数组
typedef int ElemType;
#define InitSize 100
typedef struct
{
ElemType* data;
int capacity; //动态数组的最大数组
int length;
}SeqList;
二、顺序表基本操作的实现
1.插入操作
代码如下:(输入是否合法?->是否还有空间插入?->依次后移,长度+1)
//插入操作 在第i个位置插入新元素e
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.length; j >= i; j--) //从第i个元素开始后移
L.data[j] = L.data[j - 1];
L.data[i - 1] = e; //移出空闲位置i,存入e
L.length++;
return true;
}
时间复杂度分析
插入的最好情况:在表尾插入,时间复杂度为O(1)
插入的最坏情况:在表头插入,时间复杂度为O(n)
线性表插入算法的平均时间复杂度为O(n)
2.删除操作
代码如下:(是否为空?-> 值赋给引用变量e ->依次前移,长度-1)
//删除操作 删除第i个位置的元素,该元素由引用变量e返回
bool ListDelete(SqList& L, int i, ElemType e)
{
if (i<1 || i>L.length + 1) //判断删除位置是否合法
return false;
if (L.length == 0) //线性表为空,无法删除
return false;
e = L.data[i - 1];
for (int j = i; j < L.length; j++) //第i个位置后的元素前移
L.data[j - 1] = L.data[j];
L.length--;
return true;
}
时间复杂度分析
删除的最好情况:在表尾删除,时间复杂度为O(1)
删除的最坏情况:在表头删除,时间复杂度为O(n)
线性表删除算法的平均时间复杂度为O(n)
3.查找操作
代码如下:按值查找/顺序查找
//查找操作 查找第一个元素值为e的元素,返回其位序(数组下标+1)
int LocateElem(SqList L, ElemType e)
{
for (int i = 0; i < L.length; i++)
if (L.data[i] == e)
return i + 1;
return 0;
}
时间复杂度分析
查找的最好情况:查找表头元素,时间复杂度为O(1)
查找的最坏情况:查找表尾元素(或查找元素不存在),时间复杂度为O(n)
线性表按值查找算法的平均时间复杂度为O(n)
4.修改操作
//修改操作 把第i个位置的元素改成e
bool ModifyElem(SqList& L, int i, ElemType e)
{
if (i<1 || i>L.length + 1) //判断位置i是否合法
return false;
L.data[i - 1] = e; //修改元素
return true;
}
线性表的链式表示
一、单链表
单链表的定义
单链表是一组任意的存储单元存储线性表中的数据元素。每个链表结点存放数据元素(数据域)和指向其后继的指针(指针域)。
data | next |
---|
单链表的优缺点
1.解决顺序表需要大量连续存储单元的缺点
2.附加了指针域,浪费存储空间
3.离散地发布在存储空间,是非随机存取的存储结构,查找某个特定结点时,需要从头遍历,依次查找
头指针:用来标识一个单链表,无论有无头结点,头指针始终指向链表第一个结点。
头结点:为了操作方便,在单链表第一个结点之前附加一个结点,数据域通常不存储任何信息。头结点的指针域指向线性表第一个元素结点。
引入头结点的优点:1、使链表第一个结点与其他位置的结点操作一致2、无论链表是否为空,头指针都指向头结点的非空指针,使得空表和非空表的处理方式统一。
//带有头结点的单链表
typedef struct LNode{
ElemType data;
struct LNode *next;//指向下一个结点
}LNode,*LinkList;
头插法建立单链表
采用头插法建立单链表,读入数据的顺序与生成链表中的元素顺序相反。
每个结点的插入时间为O(1),设表长为n,则时间复杂度为O(n)
//传入:结构体变量(未初始化,所以需要引用)
//返回:结构体变量(可有可无)
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,强制类型转换
s->data=x;//把读取到的值,给新空间中的data成员
s->next=L->next;//让新结点的next指针指向链表的第一个元素
L->next=s;//让s作为第一个元素
scanf("%d",&x);//读取标准输入
}
return L;
}
尾插法建立单链表
采用尾插法建立单链表可以使读入数据的顺序与生成链表中的元素顺序一致。
但必须增加一个尾指针使其始终指向当前链表的尾结点。
//传入:结构体变量(未初始化,所以需要引用)
//返回:结构体变量(可有可无)
LinkList List_TailInsert(LinkList &L)
{
int x;
L=(LinkList)malloc(sizeof(LNode));//带头节点的链表
LNode* s, * r = L;//LinkList s,r=L;也可以,r代表链表表尾结点,指向链表尾部
scanf("%d",&x);
while(x!=9999){
s=(LNode*)malloc(sizeof(LNode));
s->data=x;
r->next=s;//让尾部结点指向新结点
r=s;//r指向新的表尾结点
scanf("%d",&x);
}
r->next=NULL;//尾结点的next指针赋值为NULL
return L;
}
按序号查找结点值
从第一个结点出发,依次查找,直到找到第i个结点,找不到返回空。
按序号查找操作的时间复杂度为O(n)
//按序号查找结点值
LNode* GetElem(LinkList L, int i)
{
int j=1;
LNode *p=L->next;
if(i==0)
return L;
if(i<1)
return NULL;
while(p&&j<i){
p=p->next;
j++;
}
return p;
}
按值查找结点
按值查找的时间复杂度为O(n)
//按值查找结点
LNode* GetLNode(LinkList L, int e)
{
LNode* p = L->next;
while (p && p->data!=e)
{
p = p->next;
}
return p;
}
插入结点操作
将值为x的新结点插入单链表第i个位置上。
要想到检查第i个位置的合法性(到GetElem函数),插入在第i-1个结点后面。
算法主要开销在查找第i-1个结点,其时间复杂度为O(n),
插入的操作时间复杂度是O(1)
//中间插入结点
void InsertLNode(LinkList& L, int i, int e)
{
LNode* p = GetElem(L, i - 1);
if (p == NULL)
return;
LNode* x = (LinkList)malloc(sizeof(LNode));//新增结点
x->data = e;
x->next = p->next;
p->next = x;
}
删除结点操作
删除第i个结点,检查删除位置的合法性,找到前驱结点即第i-1个结点,断链······
时间复杂度为O(n)
void Delete_LNode(LinkList& L, int i)
{
LNode* p = GetElem(L, i - 1);
LNode* q = GetElem(L, i);
if (p == NULL||q == NULL)
return;
p->next = q->next;
free(q); //释放q的空间
q = NULL;//为了避免野指针
}
或者
void Delete_LNode(LinkList& L, int i)
{
LNode* p = GetElem(L, i - 1);
if (p == NULL)
return;
LinkList q = p->next;//LinkList=LNode*
if (q == NULL)
return;
p->next = q->next;
free(q); //释放q的空间
q = NULL;//为了避免野指针
}
二、双链表
单链表对访问某个结点的前驱结点只能从头遍历,时间复杂度为O(n),访问后继结点的时间复杂度为O(1),为了克服这一缺点,引入了双链表。
双链表增加了一个指向前驱的prior指针
双链表结点类型描述如下
typedef int ElemType;
typedef struct DNode
{
ElemType data;
struct DNode* prior, * next;
}DNode,*DLinklist;
头插法建立双链表
在p所指的结点后插入结点s,指针的变化:
①s->next=p->next
②p->next->prior=s;
③s->prior=p;
④p->next=s;
①和②必须在④之前
//头插法建立双向链表
DLinklist DList_HeadInsert(DLinklist& DL)
{
DNode* s; int x;
DL = (DLinklist)malloc(sizeof(DNode)); //创建头结点
DL->next = NULL;
DL->prior = NULL;
scanf("%d", &x);
while (x!=9999)
{
s = (DLinklist)malloc(sizeof(DNode));//申请一个结点空间
s->data = x;
s->next = DL->next;
if (DL->next != NULL)//不是第一个结点时
{
DL->next->prior = s;
}
s->prior = DL;//使要插入的结点指向头结点
DL->next = s;
scanf("%d", &x);
}
return DL;
}
尾插法建立双链表
//尾插法建立双向链表,增加一个尾指针
DLinklist DList_TailInsert(DLinklist& DL)
{
int x;
DL = (DLinklist)malloc(sizeof(DNode));//带头结点的链表
DLinklist r = DL; //r为尾指针
DL->prior = NULL;
scanf("%d", &x);
while (x != 9999)
{
DNode* s = (DNode*)malloc(sizeof(DNode));//申请一个结点空间
s->data = x;
r->next = s;
s->prior = r;
r = r->next;//r指向新的表尾结点
scanf("%d", &x);
}
r->next = NULL; //注意不要遗漏这步
return DL;
}
双链表删除结点
//删除第i个结点
void Delete_DNode(DLinklist& DL, int i)
{
DLinklist p = GetElem(DL, i - 1);
if (p == NULL)
return;
DLinklist q = p->next;
if (q == NULL)
return;
p->next = q->next;
if (q->next != NULL)//注意判断
{
q->next->prior = p;
}
free(q);
q = NULL;
}
双链表的按值查找、按位查找与单链表相同,
双链表的插入、删除操作时间复杂度为O(1)
三、循环链表
1、循环单链表
在单链表基础上,表中最后一个结点的指针改指向头结点,使整个链表形成一个环。
循环单链表的判空条件:头结点指针是否等于头指针
可以从任意位置开始遍历链表,操作效率高,对表头表尾的操作只需O(1)的时间复杂度
2、循环双链表
在双链表基础上,尾结点的next指针指向头节点,头结点的prior指针指向表尾结点。(p->next=DL)
循环双链表的判空条件:头结点的prior=头结点的next=DL
四、静态链表
借助数组描述线性表的链式存储结构,需要预先分配一块连续的存储空间。指针是结点的相对地址,也称游标。
静态链表以next == -1作为结束标志
静态链表结构类型描述如下
#define MaxSize 50
typedef struct
{
ElemType data;
int next;//下一个元素的数组下标
}SLinklist[MaxSize];
顺序表和链表的比较
1、存取(读写)方式
顺序表可以顺序存取,也可以随机存取
链表只能从表头开始顺序存取
2、逻辑结构与物理结构
顺序存储:逻辑上相邻的元素,其物理存储位置相邻
链式存储:逻辑上相邻的元素,在物理存储位置上不一定相邻,且逻辑关系通过指针链接来表示
3、增删查操作
按值查找:顺序表无序的时间复杂度是O(n),有序的时间复杂度是O(logn)
链表的时间复杂度是O(n)
按位查找:顺序表的时间复杂度为O(1),链表的时间复杂度为O(n)
插入删除操作两者的时间复杂度都是O(n),但是实际上链表插入删除结点只需O(1)的时间复杂度,查找元素的开销占主要
4、空间分配
顺序存储:静态存储需要预先分配足够大的存储空间,动态存储虽然存储空间可以扩充,但移动大量元素导致操作效率低
链式存储:结点空间在需要时申请,操作灵活、高效