前言
在数据结构的学习过程中,循环单链表是一种非常重要的数据结构。它与普通单链表的主要区别在于,循环单链表的最后一个节点的next指针指向头结点,从而形成一个闭环。这种结构使得某些操作更加便捷,尤其是在需要频繁访问列表尾部的情况下。本文将详细介绍如何基于循环单链表(带头结点)实现线性表 List2 的一系列典型操作,并通过编写简单程序来测试和调试这些功能。总体设计思路图见下:
正文
1. 初始化 (InitList)
初始化函数创建一个空的循环单链表,该链表仅包含一个头结点,其next指针指向自身。即:
代码为:
void InitList(LinkNode*& L) {
L = (LinkNode*)malloc(sizeof(LinkNode));
L->next = L;
}
2. 销毁 (DestroyList)
销毁函数负责释放链表中所有节点占用的内存空间。使用DestroyList()函数沿着“头结点——>尾节点”的方向依次遍历并释放相应结点空间。即:
代码为:
void DestroyList(LinkNode*& L) {
LinkNode* pre = L, * p = pre->next;
while (p != L) {
free(pre);
pre = p;
p = pre->next;
}
free(pre); // 释放最后一个节点
}
3. 判空 (ListEmpty)
判断链表是否为空,即检查头结点的next指针是否指向自身。
bool ListEmpty(LinkNode* L) {
return(L->next == L);
}
4. 求表长 (ListLength)
遍历整个链表,计算并返回其中元素的数量。使用ListLength()函数沿着“头结点——>尾节点”的方向依次遍历,记录经过的结点个数,最后通过汇总结点数量得出链表长度。即:
代码为:
int ListLength(LinkNode* L) {
int i = 0;
LinkNode* p = L;
while (p->next != L) {
i++;
p = p->next;
}
return(i);
}
5. 遍历输出 (DispList)
从头到尾遍历链表,并输出每个节点的数据。
void DispList(LinkNode* L) {
LinkNode* p = L->next;
while (p != L) {
printf("%c ", p->data);
p = p->next;
}
printf("\n");
}
6. 获取元素 (GetElem)
根据给定的位置获取链表中的元素值。使用GetElem()函数时要进行遍历,不过这次遍历不是只有到尾节点才结束,而是遍历前i个结点(当然,如果都已经到尾节点了,还没有到第i个结点,也可以结束遍历),当到达第i个结点时,我们提取出来其中存储的信息进行保存,如果链表遍历结束后还没有到达第i个结点则查找失败。这个函数还应该加入异常值检测,比如i一定是正数。
bool GetElem(LinkNode* L, int i, ElemType& e) {
int j = 1;
LinkNode* p = L->next;
if (i <= 0 || L->next == L) return false; // 空表或非法位置
while (j < i && p != L) {
j++;
p = p->next;
}
if (p == L) return false; // 未找到
e = p->data;
return true;
}
7. 查找元素 (LocateElem)
查找链表中第一个值域为e的元素,并返回其位置。使用LocateElem()函数也是要依次遍历,如果找到相等数据就把此时结点的序号保存下来,如果遍历一遍之后还是没找到相等数据则查找失败。
int LocateElem(LinkNode* L, ElemType e) {
int i = 1;
LinkNode* p = L->next;
while (p != L && p->data != e) {
p = p->next;
i++;
}
if (p == L) return 0; // 未找到
return i;
}
8. 插入元素 (ListInsert)
在指定位置插入新元素。使用ListInset()函数需要先找到第i-1个结点,然后开辟一个结点空间,把要插入的数据存放到这个结点中,改变指针指向实现数据的插入。即:
代码为:
bool ListInsert(LinkNode*& L, int i, ElemType e) {
int j = 1;
LinkNode* p = L, * s;
if (i <= 0) return false; // 非法位置
if (L->next == L || i == 1) { // 特殊情况处理
s = (LinkNode*)malloc(sizeof(LinkNode));
s->data = e;
s->next = p->next;
p->next = s;
return true;
} else {
p = L->next;
while (j < i - 1 && p != L) {
j++;
p = p->next;
}
if (p == L) return false; // 未找到插入位置
s = (LinkNode*)malloc(sizeof(LinkNode));
s->data = e;
s->next = p->next;
p->next = s;
return true;
}
}
9. 删除元素 (ListDelete)
删除指定位置的元素。使用ListDelete()函数需要先找到第i-1个结点,然后把第i个结点的数据提取进行保存,改变指针指向,消除应该被删除的结点与链表的联系,最后释放这个结点的空间。即:
代码为:
bool ListDelete(LinkNode*& L, int i, ElemType& e) {
int j = 1;
LinkNode* p = L, * q;
if (i <= 0 || L->next == L) return false; // 非法位置或空表
if (i == 1) { // 特殊情况处理
q = L->next;
e = q->data;
L->next = q->next;
free(q);
return true;
} else {
p = L->next;
while (j < i - 1 && p != L) {
j++;
p = p->next;
}
if (p == L) return false; // 未找到删除位置
q = p->next;
e = q->data;
p->next = q->next;
free(q);
return true;
}
}
通过上述代码示例,我们可以看到如何使用C语言实现循环单链表的基本操作。接下来,我们将编写一个简单的程序来演示这些功能,并进行必要的测试和调试。
示例程序
以下是一个简单的主函数,用于展示循环单链表的各种操作:
int main() {
LinkNode* L;
ElemType e;
printf("顺序表的基本运算如下:\n");
printf(" (1)初始化顺序表L\n");
InitList(L);
printf(" (2)依次插入a,b,c,d,e元素\n");
ListInsert(L, 1, 'a');
ListInsert(L, 2, 'b');
ListInsert(L, 3, 'c');
ListInsert(L, 4, 'd');
ListInsert(L, 5, 'e');
printf(" (3)输出顺序表L:");
DispList(L);
printf(" (4)顺序表L长度:%d\n", ListLength(L));
printf(" (5)顺序表L为%s\n", (ListEmpty(L) ? "空" : "非空"));
if (GetElem(L, 6, e) == false)
printf(" (6)顺序表L的第6个元素:查找失败\n");
else
printf(" (6)顺序表L的第6个元素:%c\n", e);
printf(" (7)元素g的位置:%d\n", LocateElem(L, 'g'));
printf(" (8)在第9个元素位置上插入f元素\n");
if (ListInsert(L, 9, 'f') == false)
printf(" 插入失败\n");
printf(" (9)输出顺序表L:");
DispList(L);
printf(" (10)删除L的第7个元素\n");
if (ListDelete(L, 7, e) == false)
printf(" 删除失败");
printf(" (11)输出顺序表L:");
DispList(L);
printf(" (12)释放顺序表L\n");
DestroyList(L);
return 1;
}
此程序展示了如何利用循环单链表实现线性表的基本操作,下面我们将通过控制台输出观察每一步操作的结果。
测试结果
正常的运行结果测试见下图:
在(6)中,当查找的元素序号不在链表中时,会输出相应信息,见下图:
在(7)中,如果查找的元素不在链表中,会输出对应的序号是0,见下图:
在(8)中,如果插入的序号不对,也会提示相应信息,即插入失败,见下图:
在(10)中,如果删除的序号不对,也会提示相应信息,即删除失败,见下图: