静态链表
文章目录
- 静态链表
- 静态链表是什么,有什么优点?
- 代码部分
- 初始化
- 增删查的准备工作
- 插入节点
- 删除结点
- 查找结点
- 其他
静态链表是什么,有什么优点?
这里就不引用官方话术了,容易越来越混,静态链表叫链表,但它是一个结构体数组
每个结构体中有两个元素,放数据的和找数据的
这个“找数据的”在正常的单链表中体现为指针,而静态链表正是模拟了这一特性,用一个数组下标元素去找到下一个位置来访问数据,即游标,姑且可以称之为指针域,但并不是指针
给出定义也许能更清晰
typedef struct staticlist{
int data;//数据域
int cur;//“指针域”
}NODE,*LPNODE;
可以看到结构体中的两个元素均为整型变量,当然,这只是单个结构体的定义
那么用这个结构有什么优点呢?
- 我们知道,指针是c语言的灵魂,但大多数语言是没有指针的,那么静态链表的出现便很好的解决了这一问题,用游标来巧妙地实现链表以进行进一步操作
- 它兼备了顺序表和链式表的优点,数组实现链表下的O(1)时间复杂度它有,指针实现链表随意修改其内的值它也可以做到,但必须记住的是,静态链表仍然是以数组实现的,所以和寻常的顺序表一样需要提前规定最大存储量
可以说静态链表是顺序和链式表的小升级版
代码部分
还有很重要的一点,诚如上面谈到的,静态链表需要提前规定最大存储量,但与顺序表又有所不同,它的内部去按需分配的,没有存放数据的位置处于“沉睡”状态,拿着备用,只有当需要存储值时,才会把备用变为数据,这样实际上出现了两个链表,备用链表与数据链表两大阵营
最开始分配的时候表内当然没有数据,这意味着什么呢?
在初始化静态链表时是先将其内部所有的元素初始化为沉睡状态,即一整条备用链表,来个图码结合感受一下
每一个结构体内的游标指向下一个结构体,以数组的下标来实现访问
初始化
#define MAX 100
LPNODE* InitList() {
LPNODE *a = (LPNODE*)malloc(sizeof(LPNODE)*MAX);//对数组进行内存分配
assert(a);
for (int i = 0; i < MAX; i++) {
a[i] = (LPNODE)malloc(sizeof(NODE));//对数组内的每个结构体进行内存分配
assert(a[i]);
a[i]->cur = i + 1;//规定a[0]->cur一般不变,可以看做头结点
}//游标用来找到下一个结点,即数组的下标
a[MAX - 1]->cur = 0;//我们规定当游标为0时备用链表的部分就结束
return a;
}
当然看着写的挺复杂,别忘了,静态链表实质是一个结构体数组,一个结构体是一个结点,表最多有数组内结构体数量个结点,而每个结点内包含了放数据的和找数据的两部分
增删查的准备工作
备用链表好了,show一下数据链表的产生
int AllocNode(LPNODE *a) {
int i = a[0]->cur;//始终记住cur是一个整型变量
if (i == 0) {//数据链表和备用链表一样,都是游标到0表示结束
return i;//这里是当下面所有的备用都存放了数据时采用的下下之策
} //返回0表示没有地方可以放了,要放只能把头结点也存了,这显然不现实
a[0]->cur = a[i]->cur;
return i;//返回一下角标的位置
}
这里其实就是和上面说到的一样,有一个数据拿一个备用过来,要一个给一点,按需分配,这个不一定要按着顺序申请下来,中间空一个空几个备用也是可以的,存储数组是随意的,只是代码中挨着分比较好理解,每次再找下一个可用结点时总是找一次就能找来用
这里来理解一下这个所谓的随意存储
基于上面一整条的备用链表,现在插一个数据3进去
可以看到当放入数据的时候,存数据的那个结点的游标变为了0,和代码中的注释是一个意思,数据链表的结点到0表示结束,而备用链表头结点中的游标则需要继续指向下一个备用处,它的结点也随之变化
下来不按代码中挨着插,随意插一个数据6进去
这次变的地方就多了,但其实还是那个道理,数据跟着数据,备用跟着备用,当数据从备用那抢了一个结点过来的时候也要和“老鹰捉小鸡”一样连着。同时备用也要保护好自己下面的备用,这个数据6是插到a[4]那去了,这就是所谓的“随意”,现在多少有点理解了吧,只是代码中为了操作方便挨着存
既然有AllocNode
数据方从备用方抢一个结点,江湖规矩“有借有还” ,对应的自然有还结点的函数FreeNode
void FreeNode(LPNODE *a, int i) {
(*a)->cur = a[0]->cur;
a[0]->cur = i;
}
这个操作就简单多了,改一下角标就好了,让角标回到上一步的位置,这个还是基于上面那个借的代码的,如果是随意分配的,那就和单个线性表那边一样麻烦了
所以,抢来抢去有什么用呢 ,当然是用于熟悉的增删了
不过在这之前,抢和借的操作有了,备用链表有自己的大本营,即初始化的结果,那数据链表抢了结点放哪?So
int CreateList(LPNODE *a, int n) {
int t = AllocNode(a);//结点拿到了,先变成自己的头结点有个着落
int head = t;//数据链表也要有自己的头结点
int m;
for (int i = 1; i <= n; i++) {//有几个数据要几个结点
m = AllocNode(a);
scanf("%d",&a[m]->data);
a[t]->cur = m;//放了数据后别忘了更新角标
t = m;//每次都要记得保留一下最后角标的位置,以防出循环时漏了一个
}
a[t]->cur = 0;//让最后的角标变为0表示结束
return head;//数据链表的头结点
}
给数据链表也来一个大本营,需要提醒的一点,这个“大本营”其实还是在最开始最开始那个结构体数组内,所以返回值只是一个头结点,依靠一个接一个角标构成整个“链表”,可以理解为数据链表是个病毒,不断感染备用链表的结点,它们始终是“一体”的
插入节点
双方已经就位了,插入结点实现如下
int InsertNode(LPNODE *a, int head, int i, int x) {//x是需要插入的数据
if (i < 1) {//这个插入是在第i个结点的前面插入,所以0前面是不可能的,做一下处理
printf("i is not experted\n");
return 0;
}
int t =head;
for (int j = 0; 0 != t && j < i - 1; j++) {
t = a[t]->cur;
}//从角标找一下插入的目标,遍历一遍
if (0 == t) {
printf("i is not found\n");
return 0;
}//找到末尾了也没找到,直接返回把
int m = AllocNode(a);//要加一个数组肯定还是得先从备用那要一个结点
if (0 != m){//万一抢到的结点是头结点就不好了
a[m]->data = x;
a[m]->cur = a[t]->cur;
a[t]->cur = m;
return 1;//成功的正常操作,依然以角标的更新为重点
}
else {
return 0;
}
}
别看这么一大串,其实办事的是从定义m的时候开始的,前面都是对特殊情况的处理
删除结点
int DeleteNode(LPNODE *a, int head, int i, int *tep) {
if (i < 1) {//同上不述
printf("i is not experted\n");
return 0;
}
int t = head;
for (int j = 0; j < i - 1 && 0 != a[t]->cur; j++) {//如果用t去与0作比较,退循环的时候就来不及了
t = a[t]->cur;//所以这里采用角标去找
}
if (0 == a[t]->cur ){//同上不述
printf("i is not found\n");
return 0;
}
//从这往下就是正常的删除操作了
//有一说一和双向链表迭代法换结点连去连去一样绕,建议多画图看一看,博主快被绕死了
int m = a[t]->cur;
a[t]->cur = a[m]->cur;
*tep = a[m]->data;
FreeNode(a, m);
return 1;
}
真正删除的部分肯定不一样,特殊处理好像一样?
那你就大错特错了,在第一个for
循环处其实是改了循环的条件的,如果依然按照之前插入时候的思路,会删到数组外面越界的
插入是对目标结点的前一个结点操作,而删除则是直接对目标结点操作,甚至会牵连到后一个结点,所以思路必须有所改变
查找结点
int LocateNode(LPNODE *a, int head, int x) {
int t = a[head]->cur;
while (a[t]->data != x && 0 != t) {
t = a[t]->cur;
}
if (t == 0) {
printf("x is not found\n");
return 0;
}
return t;
}
这个真没什么好说的,想找到x
这个值暴力遍历一遍就好了,没找到做一下处理即可
其他
同样销毁和改值这里略去,改就是查到改一下数据域就好了,销毁提个醒,要给每个结构体都释放内存,用个循环就好了,释放完了别忘了最外面的数组也要释放内存