数据结构算法刷题笔记——三、线性表
3.1 线性表的定义
线性表:零个或多个数据元素的有限序列
记为:
A1,…,Ai-1,Ai,Ai+1,…,An(1<=i<=n)
- 是序列,元素之间有顺序
- 直接前驱元素、直接后继元素(第一个无前驱,最后一个无后继)
- 位序:第i个数据元素,i为其在线性表中位序
- 是有限的
- 线性表长度:线性表元素个数n,
- 空表:n=0
- 数据元素
- 一个数据元素,可以由若干数据项组成
3.2 线性表的顺序存储结构
3.2.1 顺序存储定义
线性表的顺序存储结构:用一段地址连续的存储单元一次存储线性表的数据元素。
3.2.1.1 顺序存储方式
顺序存储结构:一维数组来实现
- 把一定内存空间给占了,把相同数据类型的数据元素依次存放在这块空地中
- 最大存储容量:数组长度
- 线性表长度:元素个数,不能超过存储容量
顺序存储的结构代码:
#define MAXSIZE 20 /*存储空间初始分配量*/
typedef int Elemtype; /*ElemType类型根据实际情况而定,这里为int*/
typedef struct
{
ElemType data[MAXSIZE]; /*数组,存储数据元素*/
int length; /*线性表当前长度*/
}SqList;
- 三个属性:
- 存储空间起始位置:数组data
- 线性表的最大存储容量:数组长度MAXSIZE
- 线性表的当前长度:length
3.2.1.2 数组长度与线性表长度的区别
线性表的长度<=数组的长度
数组长度:存放线性表的存储空间长度,MAXSIZE,长度一般不变
线性表长度:线性表中数据元素的个数,随着插入和删除操作,长度发生改变
3.2.1.3 地址计算方法
线性表第 i 个元素
存储在数组下标i-1
的位置
地址:存储器中的每个存储单元都有自己的编号,称为地址
- 数组内存地址连续,可以其中一个元素地址和元素数据类型求出任意元素地址
- 该类型数据元素占用c个存储单元
3.2.2 顺序存储结构的查找
3.2.2.1 获取元素操作 GetElem(L, i, *e)
- 根据 3.2.1.3章节中的地址连续存储,根据数组的特性:
- 对于每个线性表位置的存入或者取出数据
- 时间都是相等的,为常数
- 时间复杂度O(1)
- 随机存取结构
- 实现将线性表L中的第i个位置元素值返回
#define OK 1
#define ERROR 0
/*Status是函数的类型,其值是函数结果状态代码,如OK等*/
typedef int Status;
/*初始条件:顺序线性表L已存在,1<=i<=ListLength(L)*/
/*操作结果:用e返回L中第i个数据元素的值,注意i是指位置,第1个位置的数据是从0开始*/
Status GetElem(SqList L,int i,ElemType *e)
{
if(L.length == 0 || i<1 || i>L.length)
return ERROR;
*e = L.data[i-1]; /*要返回的数据*/
return OK;
}
/**/
3.2.3 顺序存储结构的插入与删除
3.2.3.1 插入操作 ListInsert(*L, i, e)
时间复杂度:O(n)
插入算法思路:
- 如果线性表长度大于等于数组长度,抛出异常或动态增加容量;
- 如果插入位置不合理,抛出异常;
- 从最后一个元素开始向前遍历到第i个位置,分别将它们都向后移动一个位置;
- 将要插入元素填入位置i处;
- 表长加1
实现代码:
/*初始条件:顺序线性表L已存在,1<=i<=ListLength(L)*/
/*操作结果:在L中第i个位置之前插入新的数据元素e,L的长度加1*/
Status ListInsert(SqList *L, int i, ElemType e)
{
int k;
if(L->length == MAXSIZE) /*顺序线性表已满*/
return ERROR;
if(i<1 || i>L->length+1) /*插入位置不合理,i比第一个位置小,i比最后一位置后一位置还要大*/
return ERROR;
if(i<=L->length) /*若插入数据位置不在表尾*/
{
for(k=L->length-1; k>=i-1;k--)
L->data[k+1] = L->data[k];
}
L->data[i-1] = e; /*将新元素插入,若果是插入在表尾,也是通过这步实现*/
L->length++;
return OK;
}
3.2.3.2 删除操作 ListDelete(*L, i, *e)
时间复杂度:O(n)
删除算法思路:
- 判断线性表是否为空
- 如果删除位置不合理,抛出异常;
- 取出删除元素;
- 从删除元素位置开始遍历到最后一个元素位置,分别将他们都向前移动一个位置;
- 表长减1
实现代码:
/*初始条件:顺序线性表L已存在,1<=i<=ListLength(L)*/
/*操作结果:删除L的第i个数据元素,并用e返回其值,L的长度减1*/
Status ListDelete(SqList *L, int i, ElemType *e)
{
int k;
if(L->length == 0) /*线性表为空*/
return ERROR;
if(i < 1 || i>L->length) /*删除位置不正确*/
return ERROR;
*e = L->data[i-1];
if(i<L->length) /*如果删除的不是最后位置,所以需要向前移动元素*/
{
for(k = i; k<L->length; k++)
L->data[k-1] = L->data[k];
}
L->length--; /*删除一个元素后,长度减1。同时删除是最后位置的情况,只需要将长度减1即可*/
return OK;
}
3.2.4 线性表顺序存储结构的优缺点
优点
:
- 无需为表示表中元素之间的逻辑关系而增加额外的存储空间
- 可以快速地存去表中任一位置地元素
缺点
:
- 插入和删除操作需要移动大量元素
- 当线性表长度变化较大时,难以确定存储空间的容量
- 造成存储空间的碎片
3.3 线性表的链式存储结构
3.3.1 顺序存储结构不足的解决办法
顺序存去结构最大缺点
:插入和删除时需要移动大量元素,非常耗费时间,时间复杂度O(n)
造成该问题的原因
:
- 相邻两元素的存储位置也具有邻居关系
- 内存中的位置也是相邻的,中间没有空隙
- 无法快速插入,删除后就会留有空隙需要填补
3.3.2 线性表链式存储结构定义
链式存储结构:n个结点(ai的存储映像)链接成一个链表,即为线性表(a1,a2,…,an)的链式存储结构
-
单链表:每个结点中只包含一个指针域
-
特点:用一组任意的存储单元存储线性表的数据元素
-
数据域:存储数据元素信息的域
-
指针域:存储直接后继位置的域(最后一个结点指针为空"NULL"或"^",无直接后继)
-
指针或链:指针域中存储的信息
-
结点:数据域和指针域组成数据元素ai的存储映像
-
头指针:链表中第一个结点的存储位置叫做头指针
-
头结点:在单链表的第一个结点前附设一个结点
- 数据域:
- 不存储任何信息
- 可存储线性表长度等信息
- 指针域:指向第一个结点的指针
- 数据域:
3.3.2.1 头指针与头节点的异同
头指针:
- 指链表第一个结点的指针(若链表有头结点,则指向头节点的指针)
- 具有标志作用,常用头指针冠以链表的名字
- 无论链表是否为空,头指针均不为空
- 头指针是链表的必要元素
头结点
- 为了操作的统一和方便而设立的(元素位置和下标统一)
- 放在第一个元素的结点之前,数据域一般无意义(可存储线性表长度等信息)
- 有头节点,在第一个元素结点前插入和删除结点,其操作就会统一
- 头结点不一定是链表必需要素
3.3.2.2 线性表链式存储结构代码描述
单链表:
带头节点的单链表:
空链表:
链式存储结构的结构代码:
/*线性表的单链表存储结构*/
typedef struct Node
{
ElemType data;
struct Node *next;
}Node;
typedef struct Node *LinkList;/*定义LinkList*/
- 结点由存放数据元素的数据域和存放后继结点地址的指针域组成
3.3.3 单链表的读取 GetElem( L, i, *e)
单链表的读取:
- 实现获取第i个元素的数据
- 表长:未知
时间复杂度:O(n)
读取算法思路:
- 声明一个指针p指向链表第一个结点,初始化j从1开始
- 当j<i时,就遍历链表,让p的指针向后移动,不断指向下一结点,j累加1;
- 若到链表末尾p为空,则说明第i个结点不存在;
- 否则查找成功,返回结点p的数据;
实现代码:
/*初始条件:链式线性表L已存在,1<=i<=ListLength(L)*/
/*操作结果:用e返回L中第i个数据元素的值*/
Status GetElem(LinkList L, int i, ElemType *e)
{
int j;
LinkList p; /*声明一个结点p*/
p->L->next; /*让p指向链表L的第一个结点*/
j =1;
while(p && j<i)
{
p = p->next; /*让p指向下一个结点*/
++j;
}
if(!p || j>i)
return ERROR;
*e = p->data; /*第i个元素不存在*/
return OK; /*取第i个元素的数据*/
}
3.3.4单链表的插入与删除
3.3.4.1 单链表的插入 LinkInsert(*L, i, e)
在p和p->next中插入s结点:
- 让p的后继结点改成s
- 让s的后继结点改成p的后继结点
插入到中间位置:
插入到表头、表尾位置:
时间复杂度:查找位置为O(n),插入为O(1)
插入算法思路:
- 声明一指针p指向链表头结点,初始化j从1开始
- 当j<i时,就遍历链表,让p的指针向后移动,不断指向下一结点,j累加1;
- 若到链表末尾p为空,则说明第i个结点不存在;
- 否则查找成功,在系统中生成一个空结点s;
- 将数据元素e赋值给s->data;
- 单链表的插入标准语句s->next = p->next; p->next=s;
- 返回成功;
实现代码:
/*初始条件:链式线性表L已存在,1<=i<=ListLength(L)*/
/*操作结果:在L中第i个位置之前插入新的数据元素e,L的长度加1*/
Status ListInsert(LinkList *L, int i, ElemType e)
{
int j;
LinkList p, s;
p = *L;
j = 1;
while(p && j<i) /*寻找第i个结点*/
{
p = p->next;
++j;
}
if(!p || j>i)
return ERROR; /*第i个元素不存在*/
s = (LinkList)malloc(sizeof(Node)); /**/
s->data = e;
s->next = p->next; /*将p的后继结点赋值给s的后继*/
p->next = s; /*将s赋值给p的后继*/
return OK;
}
3.3.4.2 单链表的删除
将结点q删除单链表的操作:
- 将它的前继结点的指针绕过,指向它的后继结点
- p->next=p->next->next
时间复杂度:查找位置为O(n),删除为O(1)
删除算法思路:
- 声明一指针p指向链表头结点,初始化j从1开始;
- 当j<i时,就遍历链表,让p的指针向后移动,不断指向下一个结点,j累加1;
- 若到链表末尾p为空,则说明第i个结点不存在;
- 否则查找成功,将欲删除的结点p->next赋值给q;
- 单链表的删除标准语句p->next = q->next;
- 将q结点中的数据赋值给e,作为返回
- 释放q结点;
- 返回成功
实现代码:
/*初始条件:链式线性表L已存在,1<=i<=ListLength(L)*/
/*操作结果:删除L的第i个数据元素,并用e返回其值,L的长度加1*/
Status ListDelete(LinkList *L, int i, ElemType *e)
{
int j;
LinkList p, q;
p = *L;
j = 1;
while(p->next && j<i) /*遍历寻找第i个元素*/
{
p = p->next;
++j;
}
if(!(p-next) || j>i)
return ERROR; /*第i个元素不存在*/
q = p->next;
p->next = q->next; /*将q的后继赋值给p的后继*/
*e = q->data; /*将q结点中的数据给e*/
free(q); /*让系统回收此结点,释放内存*/
return OK;
}
对于插入或删除数据越频繁的操作,单链表的效率优势就越明显
3.3.5 单链表的整表创建
单链表创建:
- 一种动态结构
- 所占用空间的大小和位置不需要预先分配划定
- 根据系统的情况和实际需求即时生成
创建单链表的过程:
- 一个动态生成链表的过程
- 从空表的初始状态起,依次建立各元素结点
整表创建算法思路:
- 声明一指针p和计数器变量i
- 初始化一空链表L
- 让L的头结点的指针指向NULL,即建立一个带头结点的单链表
- 循环
- 声明一新节点赋值给p;
- 随机生成一数字赋值给p的数据域p->data;
- 将p插入到头结点与前一新结点之间
实现代码(头插法):
头插法:始终让新结点在第一的位置
/*头插法,填充链表*/
/*随机产生n个元素值,建立带表头结点的单链线性表L(头插法)*/
void CreateListHead(LinkList *L, int n)
{
LinkList p;
int i;
srand(time(0)); /*初始化随机数种子*/
*L = (LinkList)malloc(sizeof(Node));
(*L)->next = NULL; /*先建立一个带头结点的单链表*/
for(i=0; i<n; i++)
{
p = (LinkList)malloc(sizeof(Node)); /*生成新结点*/
p->data = rand() % 100 + 1; /*随机生成100以内的数字*/
p->next = (*L)->next;
(*L)->next = p; /*插入到表头*/
}
}
实现代码(尾插法):
尾插法:始终让新结点插在终端结点的后面
/*尾插法,填充链表*/
/*随机产生n个元素的值,建立带表头结点的单链线性表L(尾插法)*/
void CreateListTail(LinkList *L, int n)
{
LinkList p, r;
int i;
srand(time(0)); /*初始化随机数种子*/
*L = (LinkList)malloc(sizeof(Node)); /*L为整个线性表*/
r = *L; /*r指向尾部的结点*/
for(i=0; i<n; i++)
{
p = (LinkList)malloc(sizeof(Node)); /*生成新结点*/
p->data = rand() % 100 + 1; /*随机生成100以内的数字*/
r->next = p; /*将表尾终端结点的指针指向新结点*/
r = p; /*将当前的新结点定义为表尾终端结点*/
}
r->next = NULL; /*表示当前链表结束*/
}
3.3.6 单链表的整表删除
整表删除:不使用单链表时,在内存中将它释放掉
整表删除算法思路:
- 声明一指针p和q
- 将第一个结点赋值给p
- 循环
- 将下一节点赋值给q
- 释放p
- 将q赋值给p
实现代码
/*重置线性表L为空表*/
/*初始条件:链式线性表L已存在*/
/*操作结果:将L重置为空表*/
Status ClearList(LinkList *L)
{
LinkList p, q;
p = (*L)->next; /*p指向第一个结点*/
while(p) /*没到表尾*/
{
q = p->next;
free(p);
p = q;
}
(*L)->next = NULL; /*头节点指针域置空*/
return OK;
}
3.3.7 单链表结构与顺序存储结构的优缺点
结构 | 存储方式 | 查找时间性能 | 增删时间性能 | 空间性能 |
---|---|---|---|---|
顺序存储结构 | 连续存储单元依次存储线性表的数据元素 | O(1) | O(n) | 预分配存储空间,打了浪费,小了上溢 |
单链表结构 | 一组任意的存储单元存放线性表的元素 | O(n) | 在找出位置后O(1) | 不需要分配存储空间,元素个数不受限制 |
- 顺序存储结构:
- 线性表需要频繁查找,很少进行插入和删除
- 链式存储结构:
- 频繁插入和删除
- 元素个数变化较大/不知道有多大
3.4 静态链表
静态链表:用数组描述的链表叫做静态链表
- 用数组代替指针来描述单链表
- 为了插入数据方便,吧数组建的大一些
- 数据域:
- 数据域data:存放数据元素
- 游标cur:相当于单链表中的next指针,存放元素的后继在数组中的下标
- 第一个和最后一个元素不存数据
静态链表构造:
#define MAXSIZE 1000 /* 存储空间初始分配量 */
/* 线性表的静态链表存储结构 */
typedef struct
{
ElemType data;
int cur; /* 游标(Cursor) ,为0时表示无指向 */
} Component,StaticLinkList[MAXSIZE];
- 备用链表:未被使用的数组元素
- 数组第一个元素(下标为0)的cur存放备用链表第一个结点的下表
- 数组最后一个元素的cur存放第一个有数值的元素的下表(相当于头结点,空表时为0)
初始化静态链表:
/* 将一维数组space中各分量链成一个备用链表,space[0].cur为头指针,"0"表示空指针 */
Status InitList(StaticLinkList space)
{
int i;
for (i=0; i<MAXSIZE-1; i++)
space[i].cur = i+1;
space[MAXSIZE-1].cur = 0; /* 目前静态链表为空,最后一个元素的cur为0 */
return OK;
}
3.4.1 静态链表的插入操作
解决:如何用静态模拟动态链表结构的存储空间的分配,需要时申请,无用时释放。
辨明哪些分量未被使用:
- 将所有未被使用过的及已被删除的分量用游标链成-个备用的链表
- 每当进行插入时, 便可以从备用链表上取得第—个结点作为待插入的新结点
/* 若备用空间链表非空,则返回分配的结点下标,否则返回0 */
int Malloc_SSL(StaticLinkList space)
{
int i = space[0].cur; /* 当前数组第一个元素的cur存的值 */
/* 就是要返回的第一个备用空闲的下标 */
if (space[0]. cur)
space[0]. cur = space[i].cur; /* 由于要拿出一个分量来使用了, */
/* 所以我们就得把它的下一个 */
/* 分量用来做备用 */
return i;
}
新元素‘‘丙” ,想插队是吧?先放在队伍最后—排第7个游标位置
将乙的游标改为7,丙的游标改为3
/* 在L中第i个元素之前插入新的数据元素e */
Status ListInsert(StaticLinkList L, int i, ElemType e)
{
int j, k, l;
k = MAXSIZE - 1; /* 注意k首先是最后一个元素的下标 */
if (i < 1 || i > ListLength(L) + 1)
return ERROR;
j = Malloc_SSL(L); /* 获得空闲分量的下标 */
if (j)
{
L[j].data = e; /* 将数据赋值给此分量的data */
for(l = 1; l <= i - 1; l++) /* 找到第i个元素之前的位置 */
k = L[k].cur;
L[j].cur = L[k].cur; /* 把第i个元素之前的cur赋值给新元素的cur */
L[k].cur = j; /* 把新元素的下标赋值给第i个元素之前元素的ur */
return OK;
}
return ERROR;
}
3.4.2 静态链表的删除操作
删除元素甲
- 将甲的游标改为原来头结点的游标(原来备用链表的第一个游标地址)
- 将头结点的游标改为甲的cur
/* 删除在L中第i个数据元素 */
Status ListDelete(StaticLinkList L, int i)
{
int j, k;
if (i < 1 || i > ListLength(L))
return ERROR;
k = MAXSIZE - 1;
for (j = 1; j <= i - 1; j++)
k = L[k].cur;
j = L[k].cur;
L[k].cur = L[j].cur;
Free_SSL(L, j);
return OK;
}
/* 将下标为k的空闲结点回收到备用链表 */
void Free_SSL(StaticLinkList space, int k)
{
space[k].cur = space[0].cur; /* 把第一个元素的cur值赋给要删除的分量cur */
space[0].cur = k; /* 把要删除的分量下标赋值给第一个元素的cur */
}
计算链表元素个数:
/* 初始条件:静态链表L已存在。操作结果:返回L中数据元素个数 */
int ListLength(StaticLinkList L)
{
int j=0;
int i=L[MAXSIZE-1].cur;
while(i)
{
i=L[i].cur;
j++;
}
return j;
}
3.4.3 静态链表的优缺点
静态链表是为了给没有指针的高级语言设计的一种实现单链表能力的方法
优点
:
- 在插入和删除时,只需要修改游标,不需要实际的进行移动
- 改进了顺序存储结构中插入和删除操作需要移动大量元素的缺点
缺点
:
- 没有解决连续存储分配带来的表长难以确定的问题
- 失去了链式存储结构随机存取的特性
3.5 循环链表
由来:单链表每个结点只存储了向后的指着,尾标志就停止了向后链的操作,某一个结点无法找到它的前驱结点
改进:
- 将单链表终端结点的指针端由空指针改为指向头结点
- 使整个单链表形成一个环
循环链表:头尾相接的单链表成为单循环链表,简称循环链表
- 解决了:从当中一个结点出发,访问到链表的全部结点
- 为使空链表和非空链表处理一致:
- 设里头结点(非必须),查找开始结点O(1),终端结点O(n)
- 设立尾指针, 查找开始结点O(1),终端结点O(1)
3.6 双向链表
由来:
- 总是从头到尾找结点,不能正反遍历都可以吗
- 由next指针,找下一个结点的时间复杂度为O(1)
- 找上一个结点的时间复杂度为O(n)
改进:克服单向性的缺点,设计双向链表,有前驱结点
双向链表:在单链表的每个结点中,再设置一个指向其前驱结点的指针域
- 双向链表都有两个指针域
- next 直接后继
- prior 直接后继
双向链表构造:
/*线性表的双向链表存储结构*/
typedef struct DulNode
{
ElemType data;
struct DuLNode *prior; /*直接前驱指针*/
struct DuLNode *next; /*直接后继指针*/
} DulNode, *DuLinkList;
双向链表也可以是循环链表
双向链表是单链表扩展出来的结构,很多操作是和单链表相同
p->next->prior = p = p->prior->next
3.6.1 双向链表插入操作
s - >prior = p; /*把p赋值给s的前驱,如图中①*/
s -> next = p -> next; /*把p->next赋值给s的后继,如图中②*/
p -> next -> prior = s; /*把s赋值给p->next的前驱,如图中③*/
p -> next = s; /*把s赋值给p的后继,如图中④*/
3.6.2 双向链表删除操作
p->prior->next=p->next; /*把p->next赋值给p->prior的后继,如图中①*/
p->next->prior=p->prior; /*把p->prior赋值给p->next的前驱,如图中②*/
free(p); /*释放结点*/