线性表
线性表的定义:
List:零个或多个数据元素的有限序列
序列:第一个元素无前驱,最后一个元素无后继,其他每个元素都有且只有一个前驱和后继
在较为复杂的线性表中,一个数据元素可以由若干个数据项组成
相同类型的数据
一对一的关系
线性表的抽象数据类型:
线性表的抽象类型定义如下:
- 线性表重置为空表;
- 获取线性表的长度;
- 插入数据和删除数据;
线性表的顺序存储结构:
顺序存储:用一段地址连续的存储单元依次存储线性表的数据元素
存储方式:
- 一维数组:第一个数据元素存到数组下标为0的位置中,接着把线性表相邻的元素存储在数组中相邻的位置
- 起始位置:数组data,存储位置就是存储空间的存储位置;最大存储容量:数组长度,MaxSize; 当前长度:length
数据长度与线性表长度区别:
- 在任意时刻线性表的长度都应小于或等于数组长度
地址的计算方法:
-
内存编号,这个编号就是地址 ——>存储空间单元
-
(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 个数据的算法思路:
- 声明一个结点 P,指向链表的第一个结点,初始化 j 从1开始
- 当 i < i 时,就遍历链表,让 P 指针向后移动,不断指向下一个结点,j 累加1;
- 若到链表末尾 p 为空,则说明第 i 个元素不存在
- 否则查找成功,返回结点 p 的数据
//用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 && j<i) //p不为空或者计数器j还没有等于i时,循环继续
{
p = p->next; //让p指向下一个结点
++j;
}
if( !p || j < i)
{
return ERROR; //第i个元素不存在
}
*e = p->data; //取第i个元素的数据
return OK;
}
注:由于单链表结构中没有定义表长,所以不能事先知道要循环多少次,因此也就不方便使用for来控制循环。其主要核心思想就是**“工作指针后移”***
单链表的插入与删除:
-
单链表的插入:
s->next = p->next; p->next = s;
单链表第 i 个数据插入结点的算法思路:
- 声明一结点指向链表第一个结点,初始化 j 从1开始;
- 当 j < i 时,就遍历链表,让 p 的指针向后移动,不断指向下一结点,j ++;
- 若到链表末尾 p 为空,则说明第 i 个元素不存在;
- 否则查找成功,在系统中生成一个空结点 s;
- 将数据元素 e 赋值给 s->data;
- 单链表的插入标准语句 s->next = p->next; p->next = s;
- 返回成功;
// 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;
算法思路:
- 声明一个结点 p 指向链表第一个结点,初始化 j 从1 开始;
- 当 j < 1 时,就遍历链表,让 p 指针向后移动,不断指向下一个结点, j++;
- 若链表末尾 p 为空,则说明第 i 个元素不存在;
- 否则查找成功, 将欲删除的结点 p->next 赋值给 q;
- 单链表的删除标准语句 p->next = q->next;
- 将 q 结点中的数据赋值给 e ,作为返回;
- 释放 q 结点;
实现代码:
//删除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 , 即建立一个带头结点的单链表;
-
循环:
- 生成一新结点赋值给 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; 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;
-
循环:
- 将下一结点赋值给 q;
- 释放 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变量的作用,它使得下一个结点是谁得到了记录,以便于等当前节点释放后,把下一结点拿回来补充。
单链表结构与顺序存储结构的优缺点:
- 存储分配方式:
- 顺序存储结构用一段连续的存储单元依次存储线性表的数据元素
- 单链表采用链式存储结构,用一组任意的存储单元存放线性表的元素
- 时间性能:
- 查找
- 顺序存储O(1)
- 单链表O(n)
- 插入和删除
- 顺序存储需要平均移动表长一半的元素,时间为O(n)
- 单链表在找出某位置指针后,插入和删除时间仅为O(1)
- 查找
- 空间性能:
- 顺序存储结构需要预分配存储空间,分大了,浪费,分小了容易发生溢出
- 单链表不需要分配存储空间,只要有就可以分配,元素个数也不受限值
总结:
- 若线性表需要频繁的查找,很少进行插入和删除操作时,宜采用顺序存储结构。若需要频繁出入和删除时,宜采用单链表结构。
- 当线性表中元素个数变化较大或者根本不知道有多大时,最好用单链表结构,这样就可以不需要考虑存储空间大小问题。
静态链表:
(数组的第一个元素存放第一个备用空闲的下标,数组的最后一个元素的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);
空间换时间
总结回顾:
线性表
- 顺序存储结构:数组
- 插入和删除操作不方便
- 链式存储结构:
- 不受固定存储空间的限制,可以比较快捷的插入和删除操作的特点
- 不同形式
- 单链表
- 循环链表
- 双向链表
- 静态链表:不适用指针来处理链表结构