标题:静态链表的实现
作者:浸
创建时间:2021/2/19
介绍:本文章是我在学习静态链表之后的一点心得,目的为了整理笔记,方便自己复习和他人学习,本人还只是一名大一学生,如果文章有错误之处,希望大神能够直接指出,而不是做出一些低素质的行为。
目录
1.静态链表的原理
简单的开头引言
相信在学习静态链表之前,我们已经学习过了动态链表,动态链表和静态链表,都有着存放数据的数据域以及指向下一个结点的指针域;对于数据域,都是采用一个类型的变量来存放这个结点的数据,如int data。但是他们的指针域呢?其实,在知道了动态链表的原理之后再去学习静态链表,就会发现其实是大同而小异
不同之处
A1.如何链接
静态链表中的指针域采用了另外一个变量来记录下一个节点的下标。什么?为什么是下标?那是因为静态链表其实本质还是一个结构体类型数组,只是在这个数组中包含着data,以及存放下一个结点在数组中下标的变量cur,举个栗子
下标 | … | n | n+1 | n+2 | n+3 | n+4 | … |
---|---|---|---|---|---|---|---|
data | A | B | C | D | E | ||
cur | n+1 | n+3 | n+4 | n+2 | n+5 |
不难发现,这里数组(ans)中每一个结点中的cur,不断指向下个结点在数组中对应的下标,这样就很容易找到对应的下一个结点;
这个图表中,我们假装第一个结点是ans[n],则通过ans[n].cur我们可以知道下个结点的下标是n+1,再通过ans[n+1].cur,则发现下个结点下标是n+3…
通过上面思路,我们可以发现这个链表的顺序其实是:
n -> n+1 -> n+3 -> n+2 -> n+4 -> n+5
就是这个道理,不是吗 [狗头]
A2.结点的设置
上面就只是小菜牛刀,下面的东西就比较绕了,我在这绕了好久啊,网上的链表思路又不是一样的,害我在这搞了一天(其实在划水),所以我会竭尽所能的讲的简单容易理解的
首先,我们要要理解两个名称:备用链表和数据链表
我提到过,静态链表的本质是数组,既然是数组,那肯定会有上限的吧,我用链表ans[100]举个例子,当我储存了一部分数据之后我让ans[2].data=2,ans[3].data=3…ans[6].data=6,就存放这5个数据,是不是就存在一个链表了啊,我就称这个下标从2到6的这个已经存放了数据的链表叫数据链表,没有存放数据并且可以用来存放数据的,这里是7到99-1(为什么不是99等会就知道了),这个他们也有通过指针域来相互连接,他们其实也是一个链表,这个就叫做备用链表
so,understand?
同样和动态链表一样,也需要设置头结点,不过因为一个数组包含两个链表(备用链表和数据链表),我们要设置两个头结点
结点设置:
数组设置为ans[max]
链表的第一个(下标为0)和最后一个元素(下标为max-1)不存放元素(data)
ans[0].cur 作为备用链表的头节点
ans[max-1].cur作为数据链表的头节点
结束条件:
动态链表中,链表结束标志是p->next=NULL时代表链表结束,静态链表中当某一个元素的cur为0,代表链表结束了
即ans[max-1].cur=0
因为没有存放数据,备用链表是满的,但是我们还是需要一个结束条件,我们让结点的cur值为0,代表此链表结束,于是我们让倒数第二个ans[max-2].cur=0,代表备用链表结束
同理, 一开始,链表没有储存数据,所以数据链表的头结点的cur为0,如果数据链表中存放了元素,那数据链表的最后一个结点cur=0,代表数据链表结束
下标 | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | … | max-2 | max-1 |
---|---|---|---|---|---|---|---|---|---|---|---|
data | \ | 空 | 空 | 空 | 空 | 空 | 空 | 空 | 空 | \ | |
cur | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 0 | 0 |
当我存放数据以后
下标 | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | … | max-2 | max-1 |
---|---|---|---|---|---|---|---|---|---|---|---|
data | \ | A | B | C | D | E | 空 | 空 | 空 | \ | |
cur | 6 | 2 | 3 | 4 | 5 | 0 | 7 | 8 | 0 | 1 |
可以发现,从备用链表中取出下标为1-5的结点并且存放了数据,并且ans[max-1].cur的值由0变为了数据链表的第一个结点的下标,现在备用链表的第一个结点下标变成了6,所以他的头结点的ans[0]的cur变成了6
数据存放满了之后
下标 | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | … | max-2 | max-1 |
---|---|---|---|---|---|---|---|---|---|---|---|
data | \ | A | B | C | D | E | F | G | X | \ | |
cur | 0 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 0 | 1 |
备用链表就空了,所以ans[0].cur=0
数据链表是满的,最后一个节点是ans[max-2]
OK!静态链表的原理就是这样啦;
下面让我们实际的操作吧!毕竟实践出真理
2.静态链表的实现
/***************************************************************
设置:链表的第一个和最后一个元素不存放
ans[0].cur存放备用链表的第一个节点位置
ans[max-1].cur存放数据链表的第一个节点位置
基本操作:
添加,删除,交换,清空
****************************************************************/
#include<stdio.h>
const int MAXN = 1000;//这里为了方便就先设置数组长度为1000
struct Node
{
int cur;
int data;
}ans[MAXN];
A.链表初始化
配上图一边看代码好理解点
/******************************************************************
初始化链表
开始的时候,备用列表最后一个元素,即ans[max-2].cur=0
初始化的时候,我们让ans[1]作为备用链表的第一个数据节点
并且,数据链表一开始还没有存放数据,所以ans[MAXN-1].cur=0
*******************************************************************/
void start(Node* ans)
{
for (int i = 0; i < MAXN-2; i++)
{
ans[i].cur = i + 1;
}
ans[MAXN - 2].cur = 0;
ans[MAXN - 1].cur = 0;//最后一位记录表头位置
下标 | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | … | max-2 | max-1 |
---|---|---|---|---|---|---|---|---|---|---|---|
data | \ | 空 | 空 | 空 | 空 | 空 | 空 | 空 | 空 | \ | |
cur | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 0 | 0 |
还有后面要用到的两个函数
1. 打印整个链表元素
/******************************************************************
打印整个链表元素
*******************************************************************/
void Print(Node* ans)
{
int k = ans[MAXN-1].cur;//获取第一个结点下标
while (k)//当k!=0,即当链表没有遇到结束条件(cur==0)
{
printf("%d ", ans[k].data);
k = ans[k].cur;
}
printf("\n");
}
2. 获取空闲的位置的下标
/******************************************************************
获取空闲的位置
ans[0].cur存放第一备用列表第一个节点
原本备用链表最后一个节点是ans[max-2],他的游标(cur)是0,
如果数据存储满了,那备用链表自然就没有元素,此时ans[0].cur=0
如果有空闲位置就返回下标,并且将备用链表指向下一个空闲的位置,这样就能把这个结点踢出备用链表,让其能给数据链表使用来存储数据
*******************************************************************/
int Malloc(Node* ans)
{
int pos = ans[0].cur;
if (pos)//pos!=0代表备用链表不为空
{
ans[0].cur = ans[pos].cur;//备用链表指向下一个空闲的位置
}
return pos;
}
B.链表的元素添加
简单画几幅图理解方便理解
/******************************************************************
将元素data插入静态链表中第pos个位置
检测位置合法性
先使用Malloc函数从备用链表中取出一个位置(下标为l)来插入
遍历到目标节点的前一个(下标p)
思路:
记录这个节点的数据后(ans[l].data = data),就是链表中位置的改变
先要将这个节点的cur赋值为上个位置的cur
再将上个位置的cur赋值为这个节点的下标l
上面两步是不能反的,想想为什么
*******************************************************************/
void add(Node* ans,int data,int pos)
{
int len = get_len(ans);
if (pos < 1 || pos > len+1)//检测位置合法性
{
puts("插入位置不合法!\n");
return;
}
int l = Malloc(ans);//获取空闲的位置的下标
int p = MAXN - 1;
if (l)//如果有位置
{
for (int i = 1; i <= pos - 1; ++i)//找到第pos个位置的前一个结点
{
p = ans[p].cur;
}
//下面这三句不懂的话,就看图吧
ans[l].data = data;
ans[l].cur = ans[p].cur;
ans[p].cur = l;
}
else
{
puts("无空位,插入失败!");
}
}
C.链表的元素删除
/***************************************************************************
删除链表中的第pos个元素
检测位置合法性之后,如果合法,就遍历到目标节点的前一个,并且记录下目标位置的下标k
设pos对应的前一个结点下标为p,让前一个指向目标的下一个,即ans[p].cur=ans[ans[p].cur].cur;
之后让删除的节点充当作备用节点的第一个,并且让ans[0].cur等于这个结点的下标
****************************************************************************/
void Delete(Node* ans, int pos)
{
int len = get_len(ans);
if (pos < 1 || pos > len)
{
puts("位置不合法!\n");
return;
}
int p = MAXN - 1;
for(int i=1;i<=pos-1;i++)
{
p = ans[p].cur;
}
int k = ans[p].cur;
//原来pos-1的下一个变成下一个的下一个
ans[p].cur = ans[k].cur;
//需要将删除掉的这个补到备用链表中,这里补到第一个
ans[k].cur = ans[0].cur;
ans[0].cur = k;
}
D.链表的元素交换
交换位置pos1和pos2在检测两个位置的合法性之后,
对pos1需要指向pos2的下一个结点,pos2也同样需要指向pos1的下一个结点。
但是如果pos1先指向pos2的下一个,那将会导致pos2要指向的就不是原本想要的,
因为pos1的下一个已经不是原本那个了,而是pos2的下一个,
这时pos2再去指向pos1的下一个,pos2指向的结果不会发生改变,还是原来的
这里使用的方法是分别保存上面所提及结点的下标
这里要讨论相邻位置和不相邻位置的两种情况!!!!
1.不相邻情况:
我用123456789举例子,交换27
首先需要将ans[1].cur=7,ans[6].cur=2
再将ans[2].cur=8,ans[7].cur=3
2.相邻情况
还是用123456789举例子,交换4和5
如果按照上面先将ans[3].cur=5,ans[4].cur=4,再将ans[4].cur=6,ans[5].cur=5
这会导致什么?,交换后链表顺序是1235,因为5指向的是自己(小丑竟是我自己),导链表致后面都没了
所以,要另外讨论
方法:让ans[3].cur=5,ans[5].cur=4,ans[4].cur=6,即可!
/**********************************************************************
交换链表中的其中两个位置的元素
***********************************************************************/
void Exchange(Node* ans, int pos1, int pos2)//pos1不等于pos2
{
int len1 = get_len(ans);
int len2 = get_len(ans);
if (pos1<1 || pos1>len1 + 1 || pos2<1 || pos2>len2 + 1)
{
puts("位置不合法!\n");
return;
}
if (pos1 > pos2)//确保pos2比较大
{
int tem = pos1;
pos1 = pos2;
pos2 = tem;
}
//p1是第pos1个位置前一个结点的下标
//p2是第pos2个位置前一个结点的下标
int p1_left = MAXN - 1, p2_left = MAXN - 1;
for (int i = 1; i <= pos1 - 1; ++i)
p1_left = ans[p1_left].cur;
for (int i = 1; i <= pos2 - 1; ++i)
p2_left = ans[p2_left].cur;
int p1 = ans[p1_left].cur, p2 = ans[p2_left].cur;//两个pos对应节点的下标
int p1_right = ans[p1].cur, p2_right = ans[p2].cur;//两个pos对应右边结点节点的下标
if (pos2 - pos1 != 1)//上面确保了pos2>pos1,如果等于1代表位置相邻
{
ans[p2_left].cur = p1;
ans[p1_left].cur = p2;
ans[p1].cur = p2_right;
ans[p2].cur = p1_right;
}
else
{
ans[p1_left].cur = p2;
ans[p2].cur = p1;
ans[p1].cur = p2_right;
}
}
E.链表清空!!
/**********************************************************************
清空链表
其实就是把数据链表中的元素最后一个的cur赋值为ans[0].cur
再让第一个元素的下标赋值给ans[0].cur
假如数据链表原本就是空呢?
这时上面的操作就会导致ans[0].cur=0,但备用链表实际不是空的
所以要加一个判断:链表是否为空
***********************************************************************/
void Free(Node* ans)
{
int pos_title = ans[MAXN - 1].cur;//第一个结点的下标
if (pos_title)
{
int pos_last=pos_title;//最后一个结点的下标
while (ans[pos_last].cur)
{
pos_last = ans[pos_last].cur;
}
//把整个数据链表直接拼接到备用链表的前面
ans[pos_last].cur = ans[0].cur;
ans[0].cur = pos_title;
//数据链表空了,ans[MAXN - 1].cur 需要为0
ans[MAXN - 1].cur = 0;
}
else
{
puts("链表已经为空!\n");
}
}
3.静态链表的优缺点
坦白了,我背不下来,我抄的。
A.静态链表的优点
在插入和删除操作肘,只需要修改游标cur,不需要移动元素,从而改进了在顺序存储结构中的插入和删除操作需要移动大量元素的缺点
B.静态链表的缺点
没有解决连续存储分配(数组)带来的表长难以确定的问题。
失去了顺序存储结构随机存取的特性。