基于循环单链表实现线性表 List 的典型操作

210 阅读6分钟

前言

在数据结构的学习过程中,循环单链表是一种非常重要的数据结构。它与普通单链表的主要区别在于,循环单链表的最后一个节点的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)中,如果删除的序号不对,也会提示相应信息,即删除失败,见下图: