用数组描述的链表称为静态链表,这种描述方法叫游标实现法。游标用于存放数组的下标。
静态链表的结构体构造如下:
#define MAXSIZE 1000
typedef int ElemType;
typedef struct
{
ElemType data; //数据
int cur; //游标cursor
}Component, StaticLinkList[MAXSIZE];
数组下标为0和999的位置不存放数据,数组下标为1的位置存放第一个数据A。最后一个游标指向第一个存放数据的数组的下标1,数组的第一个元素对应的游标存放第一个没有存放数据的数组的下标5。除此之外每个元素对应的游标都指向数组的下一个位置的下标。最后一个有元素的位置的游标为0。
那么具体怎么使用游标和数组呢?首先通过下标999找到游标1的位置,游标1就是下标1,找到了数据A;然后数据A的游标为5,那么就找到了下标为5的数据B,B的游标为2,那么找到下标为2的数据C,C的游标为3,那么就找到下标为3的数据D。依此类推。
静态链表的初始化如下:
Status InitList(StaticLinkList space)
{
int i;
for (i = 0; i < MAXSIZE - 1; i++)
{
space[i].cur = i + 1; //我们对数组的第一个元素和最后一个元素做特殊处理,他们的data不存放数据
}
space[MAXSIZE - 1].cur = 0;
return true;
}
静态链表的插入和删除:静态链表如何模拟动态链表的存储空间分配?为了辨明数组中哪些分量未被使用,解决的办法是将所有未被使用过的及已被删除的用游标连成一个备用链表。每当进行插入时,便可以从备用链表上取得第一个结点作为待插入的新结点。
插入过程由两部分组成,首先是获取空闲分量的下标。
int Malloc_SLL(StaticLinkList space)
{
int i = space[0].cur; //找到第一个空闲位置,该位置储存在下标为0的游标里
if (space[0].cur) //判断是否为最后一个元素,cur=0表示为最后一个元素
space[0].cur = space[i].cur;//把第二个空闲位置作为备用
return i;
}
Status ListInsert(StaticLinkList L, int i, ElemType e)
//在第i个元素前插入e
{
int j,k,l;
k = MAXSIZE - 1; //数组的最后一个元素
if (i<1 || L>ListLength(L) + 1)
return false;
j = Malloc_SLL(L);
if (j)
{
L[j].data = e;
for (l = 1; l <= i - 1; l++) //更新游标,更新插入位置之前的游标
{
k = L[k].cur;
}
L[j].cur = L[k].cur;
L[k].cur = j;
return true;
}
return false;
}
静态链表的删除操作,删除后空出的位置要归纳到备用链表中,完成备用链表的游标匹配。因此删除操作也由两部分组成。
Status ListDelete(StaticLinkList L, int i)
//删除链表L中的第i个数据元素
{
int j, k;
if (i<1 || i>LengthList(L))
{
return false;
}
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_SLL(L, j);
return true;
}
//将下标为k的空闲结点回收到备用链表
void Free_SLL(StaticLinkList space, int k)
{
space[k].cur = space[0].cur;
space[0].cur = k;
}
int ListLength(StaticLinkList L)
{
int j = 0;
int i = L[MAXSIZE - 1].cur;
while (i)
{
i = L[i].cur;
j++;
}
return j;
}
静态链表的优点:在插入和删除操作时,只需要修改游标,不需要移动元素,从而改进了在顺序存储结构中插入和删除操作需要移动大量元素的缺点。
总的来说,静态链表其实是给没有指针的编程语言设计的一种实现单链表功能的方法。
将单链表中终端结点的指针由空指针改变为指向头结点,就使整个单链表形成一个环,这种头尾相接的单链表称为单循环链表,简称循环链表。
注意的是,循环链表不一定要有头结点。
此外,循环链表的空表判断与单链表不同,原来是判断head->next==NULL,现在是head->next==head。
循环链表的初始化存在两种情况,一是第一次开始创建,二是已经创建,往里再添加数据。所以最初需要进行一次判断,判断时候为第一次创建链表。
#include"stdio.h"
#include"stdlib.h"
typedef struct CLinkList
{
int data;
struct CLinkList* next;
}node;
//在C语言中,node就是struct CLinkList的别名
//node == struct CLinkList
void CLinkListInit(node* pNode)
{
//pNode是我们假设的第一个结点
int item;
node* temp; //指向node结点的指针
node* target; //指向node结点的指针
printf("输入结点的值,输入0完成初始化\n");
if (item == 0)
return;
if ((pNode) == NULL)
{
//此时链表中只有一个结点,该节点指向NULL,因此还不是循环链表
pNode = (node*)malloc(sizeof(node));//分配地址空间
if (!(pNode))
exit(0); //如果分配空间出错,退出
(pNode)->data = item; //将输入数据赋值给临时结点
(pNode)->next = pNode;
}
else
{
for (target = pNode; target->next != pNode; target = target->next)
;//找到最尾部的结点,把数据插在这里,但是要先分配空间
temp = (node*)malloc(sizeof(node));
if (!temp)
exit(0);
temp->data = item;
temp->next = pNode; //最后一个位置插入时,它的指针域应指向首节点
target->next = temp; //最后一个结点的上一个结点更新指针域
}
}
case 1:插入
void CLinkListInsert(node* pNode, int i)
//*pNode为链表头结点,i为插入位置
{
node* temp;
node* target;
node* p;
int item;
int j = 1;
printf("输入要插入的值:\n");
scanf("%d", &item);
if (i == 1)
{
//插入元素的位置是第一个
temp = (node*)malloc(sizeof(node));//分配新结点空间
if (!temp)
exit(0);
temp->data = item;
for (target = pNode; target->next != pNode; target = target->next)
;//找到最后一个结点
temp->next = pNode;//原来的第一个结点顺延为第二个
target->next = temp;//最后一个结点连向新的第一个结点
pNode = temp;//重新将第一个结点称为pNode
}
else
{
for (target = pNode; j < (i - 1); j++)
{
target = target->next; //找到插入位置,此时target指向插入位置
}
temp = (node*)malloc(sizeof(node));//分配新结点空间
if (!temp)
exit(0);
temp->data = item;
p = target->next; //原来这个位置的结点保留在p里
target->next = temp;//将新结点放在target后面
temp->next = p;
}
}
case 2:查找
int CLinkListSearch(node* pNode, int elem)
{
node* target;
int i = 1;
for (target = pNode; target->data != elem && target->next != pNode;i++)
{
target = target->next;
}
if (target->next == pNode)
return 0;
else
return i;
}
2.3基于循环链表的约瑟夫问题
据说著名犹太历史学家Josephus有过以下的故事:在罗马人占领乔塔帕特后,39 个犹太人与Josephus及他的朋友躲到一个洞中,39个犹太人决定宁愿死也不要被敌人抓到,于是决定了一个自杀方式,41个人排成一个圆圈,由第1个人开始报数,每报数到第3人该人就必须自杀,然后再由下一个重新报数,直到所有人都自杀身亡为止。然而Josephus 和他的朋友并不想遵从。首先从一个人开始,越过k-2个人(因为第一个人已经被越过),并杀掉第k个人。接着,再越过k-1个人,并杀掉第k个人。这个过程沿着圆圈一直进行,直到最终只剩下一个人留下,这个人就可以继续活着。问题是,给定了和,一开始要站在什么地方才能避免被处决。Josephus要他的朋友先假装遵从,他将朋友与自己安排在第16个与第31个位置,于是逃过了这场死亡游戏。
一般来说,会要求我们输出自杀的顺序,那么怎么做呢?
首先,多少个人围城一圈,启发我们用循环链表来表示。结构体中除了指针,还应该有一个变量来标记该人是否被杀。这样从第一个人开始计数,每到3就将标记改为0,表示此人已死。或者说直接将此人移出链表。
#include"stdio.h"
#include"stdlib.h"
typedef struct node
{
int data;
struct node* next;
}node;
node* create(int n) //生成链表,n为结点数
{
node* p = NULL;
node* head;
head = (node*)malloc(sizeof(node)); //创建头结点
p = head; //使p指向头结点,p是一个指向当前结点的指针,会经常变
node* s;
int i = 1;
if (n != 0)
{
while (i<=n)
{
s= (node*)malloc(sizeof(node));
s->data = i++; //记录结点编号
p->next = s; //头结点的下一个结点为s
p = s; //更新s为当前结点
}
s->next = head->next;
}
free(head);//头结点仅作为一个索引,构成环以后就可以删去
return s->next;
}
int main()
{
int n = 41;
int m = 3;
int i;
node* p = create(n); //p为链表第一个结点
node* temp;
m %= n; //m=2
while (p != p->next)//疯狂删除,直到无人
{
for (i = 1; i < m - 1; i++)
{
p = p->next; //找到第三个人,此时p是指向第2个人的
}
printf("%d->", p->next->data);
temp = p->next; //存储第三个人
p->next = temp->next;//第2个结点指向第四个结点
free(temp);
p = p->next; //第二次循环从第4人开始
}
printf("%d\n", p->data);
return 0;
}