一、数据结构与算法之静态链表
1.静态链表的定义
用数组描述的链表叫做静态链表,这种描述方法叫做游标实现法。(因为数组在声明的时候,就必须声明其占用多大的空间,不像动态链表,需要一个就动态的申请一个,不需要就动态释放掉)
2.线性表的静态链表存储结构
#define MAXSIZE 1000
typedef struct{
ElemType data; // 数据
int cur; // 游标(Cursor)
}Component,StaticLinkList[MAXSIZE];
我们看看这个静态链表的结构,我们需要注意:
- 两对特殊的元素,一对是下标为0的元素,它不存放数据,另一对是下标为MaxSize-1的(即最后一个元素)元素,同样不存放数据,即头和尾数据都是空的;
- 最后一个元素的游标存放的是第一个存放了数据的元素的下标地址,即1;
- 第一个元素的游标存放的是第一个未存放数据的元素的下标地址,即5;
- 每一个存放了数据的元素的游标则是指向该元素下一个元素的下标地址,如存放了A的游标指向的是A的下一个元素C的下标地址。
3.静态链表的初始化(即初始化数组)
Status InitList(StaticLinkList space){
int i;
for(i=0; i< MAXSIZE-1; i++){
space[i].cur = i+1;
space[MAXSIZE-1].cur = 0;
return OK;
}
}
重点:
- 我们对数组的第一个和最后一个元素做特殊处理,它们的data不存放数据;
- 我们通常把未使用的数组元素称为备用链表;
- 数组的第一个元素,即下标为0的那个元素的cur就存放备用链表的第一个结点的下标;
- 数组的最后一个元素,即下标为MAXSIZE-1的cur则存放第一个有数值的元素的下标,相当于单链表中的头结点作用;
- 数组中最后一个存放了数据的元素的游标指向第一个元素的下标地址,即0。
4.静态链表的插入操作
如何用静态链表模拟动态链表结构的存储空间分配,也就是需要的时候申请,不需要的时候释放。
为了判断出数组中哪些分量未被使用,解决的办法就是将所有未被使用过的和之前已被删除的分量用游标链成一个备用的链表。每当进行插入操作时,就可以从备用链表上取得第一个结点作为待插入的新结点,比如,如下图中,我们需要将新结点B插入到A的后面:
因为我们之前已经知道了,每个元素的游标的值即是指向该元素下一个元素的下标地址,而B要插入到A之后,所以A的游标值就必须指向B的下标地址(即A的游标值由原来的2变为现在的5),那么B的游标值就要指向C的下标地址,或者说指向原来A的游标(即B的游标值由原来的6变为现在的2)。
代码实现:
第一部分获取空闲分量的下标:
int Malloc_SLL(StaticLinkList space){
int i = space[0].cur; //记录第一个元素的游标,即空闲分量的下标
if(space[0].cur){ // 如果游标不为0,即表示当前数组是有数据元素的
space[0].cur = space[i].cur;
//把当前空闲分量的下一个分量记录下来,即记录插入后的空闲分量下标,到时将第一个元素的游标去指向这个下标
}
return i;
}
第二部分插入操作:
// 在静态链表L中第i个元素之前插入新的数据元素e
Status ListInsert(StaticLinkList L, int i, ElemType e){
int j, k, l;
k = MAX_SIZE - 1; //k数组的最后一个元素的下标
if(i<1 || i>ListLength(L)+1){
return ERROR;
}
j = Malloc_SLL(L); // j是第一个空闲分量的下标
if(j){ // j不为0
L[j].data = e; // 首先将空闲分量的数据域存放为e,e为插入的元素
for(l=1, l<=i-1; l++){
k = L[k].cur; //用k记录要插入位置的下标地址
}
L[j].cur = L[k].cur; // 将空闲分量的游标指向要插入位置下一个元素的下标,也就是指向插入位置的游标
L[k].cur = j; // 将这个要插入位置元素的游标指向空闲分量的下标
return OK;
}
return ERROR;
}
5.静态链表的删除操作
删除数组中的元素C,如下图为删除前后的下标与游标的变化
对比上图,我们分析下发生了什么变化:
- 首先,C被删除了,那么B的游标就应该指向D的下标(即2-->3)
- 然后,C这个位置就归为了空闲链表中的第一个元素,那么C之前所在位置的游标就应该指向下一个空闲分量的下标(即3-->6)
- 最后A的游标应该指向第一个空闲分量的下标,即改为C之前所在位置的下标(即6-->2)
实现代码:
// 删除静态链表L中的第i个数据元素
Status ListDelete(StaticLinkList L, int i){
int j, k;
if(i<1 || i>ListLength(L)){
return ERROR;
}
k = MAX_SIZE - 1; // 记录最后一个元素的下标
for(j=1; j<=i-1; j++){
k = L[k].cur; // 记录待删除的结点前一个结点的下标
}
j = L[k].cur; // 记录待删除结点前一个结点的游标
L[k].cur = L[j].cur; // 将待删除结点的前一个结点的游标指向待删除结点的后一个结点的下标地址
Free_SLL(L, j);
return OK;
}
// 将下标为k的空闲结点回收到备用链表
void Free_SLL(StaticLinkList space, int k){
space[k].cur = space[0].cur;
// 将待删除的游标指向第一个元素的游标(即待删除的结点将成为第一个空闲分量,所以,第一个空闲分量的游标就指向原来删除前 第一个空闲分量的的下标,即删除前第一个元素的游标值)
space[0].cur = k;
// 然后将第一个元素的下标指这个需要被删除的结点的下表,因为它被删除后就成为第一个空闲分量
}
// 返回链表的元素个数
int ListLength(StaticLinkList space){
int j = 0;
int i = L[MAXSIZE-1].cur; // 记录第一个数据元素的下标
while(i){ // i为0表示执行到了最后一个数据元素的游标,循环结束
i = L[i].cur; //
j++; // 每到一个数据元素,计数器就加1
}
return j;
}
6.静态链表优缺点总结
优点:在插入和删除操作时,只需要修改游标,不需要移动元素,从而改进了在顺序存储结构中的插入和删除操作需要移动大量元素的缺点;
缺点:
- 没有解决连续存储分配(数组)带来的表长难以确定的问题;
- 失去了顺序存储结构随机存取的特性。
总结:
- 总的来说,静态链表其实是为了给没有指针的编程语言设计一种实现单链表功能的方法;
- 尽管我们可以用单链表而不使用静态链表,但这样的思考方式是非常巧妙的,应该理解其思想,以备不时之需。
7.面试题目
题目:快速找到未知长度单链表的中间结点
(1)普通方法:首先遍历一遍单链表以确定单链表的长度L,然后再次从头结点开始循环L/2次找到单链表的中间结点;算法的复杂度为:O(L+L/2) = O(3L/2)
(2)高级方法:可以利用快慢指针,设置两个指针*Search、*mid都指向单链表的头结点。其中*Search的移动速度是*mid的2倍,当*Search指向末尾结点的时候,mid正好就在中间了。这也是标尺的思想。
代码实现:
// 快速找到未知长度单链表的中间结点
Status GetMidNode(LinkList L, ElemType *e){
LinkList search, mid;
mid = search = L;
while(search->next != NULL){
//search移动的速度是mid的2倍
if(search->next->next != NULL){
search = search->next->next;
mid = mid->next;
}else{
search = search->next;
}
}
*e = mid->data;
return OK;
}
本文为原创文章,如果对你有一点点的帮助,别忘了点赞哦!比心!如需转载,请注明出处,谢谢!