List:零个或多个数据元素的有限序列
序列:第一个元素无前驱,最后一个元素无后继,其他每个元素都有且只有一个前驱和后继
在较为复杂的线性表中,一个数据元素可以由若干个数据项组成
相同类型的数据
一对一的关系
线性表的抽象类型定义如下:
顺序存储:用一段地址连续的存储单元依次存储线性表的数据元素
存储方式:
数据长度与线性表长度区别:
地址的计算方法:
内存编号,这个编号就是地址 ——>存储空间单元
(LOC表示获得存储位置的函数)
L O C ( a i + 1 ) = L O C ( a i ) + c LOC(ai+1) = LOC(ai)+c LOC(ai+1)=LOC(ai)+c
随机存取结构:0(1)
获取元素操作:
/**
* 初始条件:顺序线性表L已存在,1 <= i <= ListLength(L)
* 操作结果:用e返回L中第i个数据元素的值
*/
Status GerElem(SqList L,int i, ElemType *e)
{
if(L.length == 0 || i < 1 || i > L.length)
{
return ERROR;
}
*e = L.data[i - 1];
return OK;
}
插入操作:先检查插入位置是否合理,将要插入位置后面的元素全部向后移动一个位置!
//在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不在范围内时
{
return ERROR;
}
if( i <= L->length)//若插入数据位置不在表尾
{ // 将要插入位置后数据元素向后移动一位
for (K = L->length - 1; k >= i - 1; k--)
{
L->length[k + 1] = L->data[k];
}
L->data[i - 1] = e;//将新元素插入
l->length++;
return ok;
}
}
删除操作:删除位置之后的元素全部向前移动一个位置;
//删除L中第i个数据元素,并用e返回其值,L的长度减一
Status ListDelet(SqList *L,int i,ElemType *e)
{
int k;//不定义k直接使用i也是可以的
if (L->length == NULL) //顺序线性表是空的
return ERROR;
if (i < 1 || i > L->length + 1) //删除位置不正确
return ERROR;
//*号 和 i-1 错了两个地方,位置也不对,放到if里,
*e = L->data[i-1];
if( i < L->length ) //如果删除不是最后位置
{
for (k = i; k < L->length + 1;k++)//将删除位置后继元素前移
{
L->data[k - 1] = L->data[k];
}
L->length--;
return OK;
}
}
结论:线性表,顺序存储结构,在存、读数据时,不管是那个位置,时间复杂度都是O(1).而插入和删除时,时间复杂度都是O(n).
线性表顺序存储结构的优缺点:
优点:
缺点:
顺序存储结构不足的解决办法:
线性表链式存储结构定义:
特点:用一组任意的存储单元存储线性表的数据元素
除存数据元素信息外,还要存储它的后继元素的存储地址
数据域:存储数据元素的信息
指针域:存储直接后继位置的域——>存储的是信息称作指针或链
结点(Node):数据域和指针域组成数据元素ai的存储映像
链式存储结构:n个结点(ai的存储映像)链接成一个链表,即为线性表的链式存储结构
单链表:链表的每个结点中只包含一个指针域——>结点的指针域将线性表的数据元素按其逻辑次序连接起来
头指针:链表中第一个结点的存储位置——>最后一个结点指针为“NULL”,
头结点:单链表的第一个结点前附设一个结点,成为头结点。头结点的数据域可以不存储任何信息,也可以存储如线性表长度等附加信息。头结点的指针域指向第一个结点的指针
头指针与头结点的异同:
头指针:
头结点:
线性表链式存储结构代码描述:
//线性表的单链表存储结构
typedef struct Node
{
ElemType data; //数据域
struct Node *next; //指针域
}Node;
typedef struct Node *LinkList;//定义LinkList
获取第 i 个数据的算法思路:
//用e 返回L中第i个数据元素的值
Status GetElem(LinkList L, int i, ElemType *e)
{
int j;
LinkList p; //声明一个结点P
p = L->next; //让p指向链表L的第一个结点
j = i; //j为计数器
while (p && jnext; //让p指向下一个结点
++j;
}
if( !p || j < i)
{
return ERROR; //第i个元素不存在
}
*e = p->data; //取第i个元素的数据
return OK;
}
注:由于单链表结构中没有定义表长,所以不能事先知道要循环多少次,因此也就不方便使用for来控制循环。其主要核心思想就是**“工作指针后移”***
单链表的插入:
s->next = p->next; p->next = s;
单链表第 i 个数据插入结点的算法思路:
// 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));//生成新结点(C标准函数)
s->data = e;
s->next = p->next; //将p的后继结点赋值给s的后继
p->next = s; //将s赋值给p的后继
return ok;
}
注:malloc 标准函数,生成以恶个新的结点,其类型与Node是一样的,其实质就是在内存中找了一小块空地,准备用来存放 e 数据 s 结点。
单链表的删除:
将他的前继结点的指针绕过,指向他的后继结点即可
q = p->next; p->next = q->next; ==> p->next = p->next->next;
算法思路:
实现代码:
//删除L的第i个元素
Status ListDelete(LinkList *L, int i;ElemType *e)
{
int j;
LinkList p, q;
p = *L;//内容copy
j = 1;
while ( p->next && j < i)//遍历寻找第i个元素
{
p = p->next;
++j;
}
if ( !(p->next) || j > i)//第i个元素不存在
return ERROR;
q = p->next;
p->next = q->next; //将q的后继赋值给p的后继
*e = q->data; //将q结点中的数据给e
free(q); //让系统回收此结点,释放内存
return OK;
}
小结:插入和删除都是由两部分组成,第一部分是遍历查找第 i 个元素,;第二部分就是插入和删除元素;对于插入和删除数据越频繁的操作,单链表的效率优势就越是明显。
动态生成链表的过程:
声明一结点 P 和计数器变量 i ;
初始化一空链表 L ;
让 L 的头结点指针指向 NULL , 即建立一个带头结点的单链表;
循环:
头插法:始终让新结点在第一的位置。
//随机产生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;
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));
r = *L; //r为指向尾部的结点
for (i = 0; i < n; i++)
{
p = (LinkList)malloc(sizeof(Node)); //生成新结点
p->data = rand() % 100 + 1;
//p->next = (*L)->next;
r->next = p; //将表尾终端结点的指针指向新结点
r = p; //将当前的新结点定义为表尾终端结点
}
r->next = NULL; //表示当前链表结束
}
算法思路:
声明一结点 p 和 q;
将第一个结点赋值给 p;
循环:
代码实现:
//整表删除
Status ClearList(LinkList *L)
{
LinkList q, p;
p = (*L)->next; //p指向第一个结点
while (p) //没到表尾
{
q = p->next;
free(p);
p = q;
}
(*L)->next = NULL; //头结点指针域为空
return OK;
}
注:q变量的作用,它使得下一个结点是谁得到了记录,以便于等当前节点释放后,把下一结点拿回来补充。
总结:
(数组的第一个元素存放第一个备用空闲的下标,数组的最后一个元素的cur存放第一个元素)
def:用数组描述的链表叫做静态链表。–游标实现法
要解决的是:如何用静态模拟动态链表结构的存储空间的分配,需要时申请,无用时释放
解决方法:将所有未被使用过的及已经被删除的分量用游标链成一个备用的链表,每当进行插入时,便可以从备用链表上取得第一个结点作为待插入的新结点。
静态链表初始化:
将一维数组链成备用链表
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-MiazXh1d-1639986586608)(F:/%E5%AD%A6%E4%B9%A0%E8%B5%84%E6%96%99/%E9%87%8D%E8%A6%81%E8%B5%84%E6%96%99/%E7%AC%94%E8%AE%B0/image_md/image-20211220140550526.png)]
//将一维数组space中各分量链成一备用链表
Status InitList(StaticLinkList space)
{
int i;
for (i = 0; i < MAXSIZE - 1; i++)
space[i].cur = i + 1;
space[MAXSIZE - 1].cur = 0; //目前静态链表为空,最后一个元素的cur为空
return OK;
}
从备用链表上取出待插入的结点下表
//若备用空间链表非空,则返回分配的结点下标,否则返回0
int Malloc_SLL(StaticLinkList space)
{ //当前数组第一个元素的cur存的值,就是要返回的第一个备用空闲下标
int i = space[0].cur;
if (sapce[0].cur)
space[0].cur = space[i].cur;//由于要拿出一个分量来使用了,所以我们就得把他的下一个分量拿来备用
return i;
}
静态链表的插入操作:
??? 给我绕蒙了,这个代码也太晦涩难懂了,
一个: k = L[999].cur = 1
代码实现:
//在L中第i 个元素之前插入新的数据元素e
Status ListInsert(StaticLinkList L, int i, ElemType e)
{
int j, k, l;
k = MAX_SIZE - 1; //注意k首先是最后一个元素的下标 999
if ( i < 1 || i > ListLength(L) + 1 )
return ERROR;
j = Malloc_SLL(L); //获得空闲分量的下标 待插入位置7,0->8
if (j)
{
L[j].data = e; //将数据赋值给此分量的data
for (l = 1; l <= i - 1; l++) //找到第i个元素之前的位置
k = L[k].cur;//i=3,执行两次,第一次是通过最后一个元素的下下标值直接找到第一个元素,然后第二次循环是指向第二个元素,执行结束后 k 是第二个元素,i(第三个元素)的cur
L[j].cur = L[k].cur;//把第i个元素之前的cur赋值给新元素的cur
l[k].cur = j; //把新元素的下标赋值给第i个元素之前的元素的cur
return OK;
}
return ERROR;
}
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-UO4Q9LcL-1639986586609)(F:/%E5%AD%A6%E4%B9%A0%E8%B5%84%E6%96%99/%E9%87%8D%E8%A6%81%E8%B5%84%E6%96%99/%E7%AC%94%E8%AE%B0/image_md/image-20211220142857681.png)]
静态链表的删除操作:
//删除在L中第i个数据元素e
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;
}
Free_SSL函数:将删除元素的空间释放
//将下标为k的空闲结点回收到备用链表
void Free_SSL(StaticLinkList space, int k)
{
space[k].cur = space[0].cur; //把第一个元素cur值赋给要删除的分量cur
space[0].cur = k; //把要删除的分量下标赋值给第一个元素cur
}
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-DedmYnTd-1639986586610)(F:/%E5%AD%A6%E4%B9%A0%E8%B5%84%E6%96%99/%E9%87%8D%E8%A6%81%E8%B5%84%E6%96%99/%E7%AC%94%E8%AE%B0/image_md/image-20211220150023295.png)]
静态链表的优缺点:
def:将单链表中终端结点的指针端由空指针改为指向头结点
use:从一个结点出发,访问到链表的全部结点
尾指针 : rear
合并两个循环链表:
p = rearA->next; //保存A表的头结点
//将本事指向B表的第一个结点(不是头结点)赋值给rearA->next
rearA->next = rearB->next->next;
rearB->next = p; //将原A表的头结点赋值给rearB->next
free(p); //释放p
def:在单链表的每个结点中,在设置一个指向其前驱结点的指针域
use:在点链表的基础上,增加了反向遍历
bad:在插入和删除时,需要更改两个指针变量
typedef struct DulNode
{
ElemType data;
struct DulNode *prior; //直接前驱指针
struct DulNode *next; //直接后继指针
}DulNode, *DuLinkList;
双向链表中某一结点P的后继的前驱是谁?
p->next->piror = p = p->piror->next;
插入结点:先搞定s的前驱和后继,在搞定结点的前驱,最后解决前结点的后继
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ZAYgUfOS-1639986586611)(F:/%E5%AD%A6%E4%B9%A0%E8%B5%84%E6%96%99/%E9%87%8D%E8%A6%81%E8%B5%84%E6%96%99/%E7%AC%94%E8%AE%B0/image_md/image-20211220153958955.png)]
s->prior = p; //把p赋值给s的前驱
s->next = p->next; //把p->next 赋值给s的后继
p->next->piror = s; //把s赋值给p->next的前驱
p->next = s; //把s赋值给p的后继
删除操作:
p->piror->next = p->next; //把p->next赋值给p->prior的后继
p->next->piror = p->next; //把p->prior赋值给p->next的前驱
free(p);
空间换时间
线性表