Day3 —— 单向循环链表的概念及基本操作的实现

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/qq_41221623/article/details/82731586
  • 首先,先来回顾一下单链表:

    • 对于单链表,由于每个结点只存储了向后的指针,到了尾部就停止了向后链的操作,这样,当某一结点就无法找到它的前驱结点了。
  • 那么,只需对单链表做个小改动就可变成循环链表,具体如下:

    • 将单链表中终端结点的指针端由空指针改为指向头结点,就使单链表形成了一个环,这种头尾相接的单链表称为单循环链表,简称循环链表。

  • 一般来说,为了方便操作,都会设一个头结点,所以带有头结点的循环链表为空链表时如下图所示:
    image

  • 对于非空的循环链表就如下图所示:
    image

  • 由此可看出循环链表和单链表的主要差异就在于循环的判断上,原来是判断p->next是否为空,现在则是p-next不等于头结点,则循环未结束。
  • 再回顾一下单链表,在单链表中,有了头结点可以用O(1)的时间访问第一个结点,但对于访问到最后一个结点,却需要O(n)的时间,因为需要将单链表全部遍历一遍。
  • 而在循环链表中,我们可以不用头指针,而是用指向尾结点的尾指针来表示循环链表,这样查找开始结点和尾结点都很方便了,如下图:
    image

  • 由此可见,若是用尾指针开始查找,则查找尾结点是O(1),而开始结点则是rear->next->next,时间复杂度也为O(1)。

  • 举个例子,若要将两个循环链表合并成一个表,有了尾指针就十分方便了,比如以下两表:
    image
  • 将他们合并只需如下操作即可:
    image
  • 代码如下:
p = rearA->next;                    //保存A表的头结点
rearA->next = rearB->next->next;    //将表B的第一个结点赋值给rearA->next,即将A,B两表链结上
rearB->next = p;                    //将原A表的头结点赋值给rearB->next,即构成一个环
free(p);                            //释放p

  • 注:以上总结于《大话数据结构》一书

  • 以下开始循环链表的基本操作实现:

  • 这里实现以下基本操作:
    1. 初始化链表
    2. 尾插法建表
    3. 获取链表长度
    4. 判断链表是否为空
    5. 用户通过输入一元素来查找表中相对应的结点并通过该结点遍历整个链表(查找元素和通过任一结点遍历的结合)
    6. 在用户指定的位置插入元素
    7. 删除用户指定位置的元素
    8. 遍历打印链表(第一个结点开始)
    9. 清空链表
    10. 销毁链表

  • 在实现这些操作前,我们先定义一些常用的常量、函数类型和统一的元素数据类型和链表结点结构,代码如下:
/****** 函数结果状态代码 ******/
#define OK      1
#define ERROR       0
#define TRUE        1
#define FALSE       0
#define OVERFLOW    -2

/****** 函数类型,返回值为函数结果状态代码 ******/
typedef int Status;
typedef int Bool;

/****** 统一的数据类型 ******/
typedef struct ElementType {
    //属于该数据类型的元素的数据项
    //这里以角色ID和名称为例
    int id;
    char name[100];
}ElementType;

/****** 循环链表的结点 ******/
typedef struct CircularNode
{
    ElementType data;               //数据域
    struct CircularNode * next;     //指向下个结点的指针域
}CircularNode;

typedef struct CircularNode * CircularLinkList;

以下依次实现这些操作:

1、链表的初始化
  • 对于链表的初始化,这里通过创建一个头结点来初始化链表,我们知道带头结点的循环链表为空表时头结点的后继指向它自己,所以我们以以下代码来实现此操作:

    /** 初始化链表 */
    Status InitCircularLinkList(CircularLinkList * clList)
    {
        *clList = (CircularLinkList)malloc(sizeof(CircularNode));   //生成一个头结点
        if((*clList) == NULL)
        {
            //生成失败
            exit(OVERFLOW);
        }
    
        //初始化头结点
        (*clList)->next = *clList;   //空表
    
        printf("初始化成功!\n");
    
        return OK;
    }

2、尾插法建表
  • 对于尾插法建表,有以下算法思路:
    1. 定义一个结点newNode,用于建表时生成新结点来存储数据并插入表中。
    2. 定义一个结点r,用于指向链表的尾结点(初始时先指向头结点并令它的后继指向它自己)。
    3. 定义一个整型变量n,用于保存创建的表长;和定义一个ElementType型变量data,用于用户输入建表的数据信息。
    4. 输入建表表长,进入循环建表:
      1. 输入建表数据信息。
      2. 生成新结点newNode。
      3. 将输入的数据信息赋值给newNode的数据域。
      4. 令尾结点r的后继指向新结点。
      5. 将插入的新结点赋值给尾结点,即每次插入都插入到链表尾部。
        5.循环结束后,令当前表中的尾结点的后继指向头结点,构成一个环,即循环链表建表成功。
  • 实现代码如下:
/** 尾插法建表 */
Status CreatListTail(CircularLinkList * clList)
{
    int n;  //需要创建的表长
    CircularLinkList newNode;   //定义一个结点,用于生成新结点存储数据插入表中
    CircularLinkList r;         //定义一个结点r,用于指向链表的尾结点
    ElementType data;           //要输入的建表数据

    r = *clList;                //刚开始时尾结点r指向头结点
    r->next = r;                //空表时令尾结点的后继为它自己,构成一个环

    printf("请输入要创建的表长:");
    scanf("%d", &n);
    fflush(stdin);      //清空缓冲区

    printf("请依次输入建表数据:\n");
    printf("%9s %s\n", "ID", "Name");

    for(int i = 0; i < n; i++)
    {
        printf("第%d个:", i + 1);
        scanf("%d%s", &data.id, data.name);
        fflush(stdin);

        newNode = (CircularLinkList)malloc(sizeof(CircularNode));   //生成新结点,用于存储插入的数据并将该结点插入链表中
        newNode->data = data;                                       //将输入的数据赋给newNode的数据域
        r->next = newNode;                                          //将尾结点的指针域指向新结点
        r = newNode;                                                //将当前插入的新结点定义为尾结点
    }

    r->next = (*clList);                                            //插入完成后令尾结点的指针域指向头结点,使整个链表构成一个环

    printf("建表成功!\n");

    return OK;
}

3、获取链表当前长度
  • 对于获取链表的长度,有以下算法思路:
    1. 定义一个整型变量len并赋初值0,用于存储计算出来的长度并返回。
    2. 生成一个结点start并指向表中第一个结点,用于循环遍历链表计算长度。
    3. 利用while循环,并根据循环链表的特性令循环条件为start != 头结点,进入循环:
      1. 将start的后继赋值给start,即一个个遍历链表元素。
      2. 令len + 1,即每遍历一个元素长度就加一。
    4. 循环结束后,返回len,即计算出的链表长度。
  • 实现代码如下:
/** 获取循环链表长度 */
int GetLength(CircularLinkList clList)
{
    int len = 0;    //链表长度

    if(clList == NULL)
    {
        printf("链表已被销毁,无法得出长度!\n");
        exit(OVERFLOW);
    }

    CircularLinkList start = clList->next;  //生成一个结点并指向表中第一个结点

    while(start != clList)  //根据循环链表的特性来计算长度
    {
        start = start->next;
        len++;
    }
    return len;
}

4、判断链表是否为空
  • 对于判断是否为空,就十分简单了,只需根据循环链表的特性,判断头结点的后继是否为它自己就行了,实现代码如下:
/** 判断链表是否为空 */
Bool IsEmptyCircularLinkList(CircularLinkList clList)
{,
    if(clList == NULL)
    {
        printf("链表已被销毁,无法判断!\n");
        exit(OVERFLOW);
    }

    return clList->next == clList ? TRUE : FALSE;
}

5、用户通过输入一元素来查找表中相对应的结点并通过该结点遍历整个链表(查找元素和通过任一结点遍历的结合)
  • 这里需要两个函数来互相结合,分别是查找结点的函数和根据指定结点来遍历链表的函数,先来说明查找结点的算法思路:
    1. 生成一个结点node并指向链表第一个结点,用于循环对比传入的元素数据是否与该结点的数据域的信息相同。
    2. 进入循环:
      1. 使用if语句判断传入元素的信息与node结点的数据域的信息是否相同,若相同则返回该结点。不相同则将node的后继赋值给node,进入下次循环继续判断,依次类推……
      2. 循环结束后,若还没找到则返回NULL
  • 通过用户指定的结点来遍历链表的思路:
    1. 生成一个结点lastNode并指向链表第一个结点。
    2. 使用lastNode通过循环找到链表最后一个结点并令最后一个结点的后继指向链表第一个结点,以便从表中任意一个结点开始遍历时不经过头结点,打印出不知名的数据。
    3. 生成一个结点origNode并将传入的node结点赋值给它,记录下初始时node的位置。
    4. 使用do while()循环遍历并打印结点吗,循环条件为node != origNode。
    5. 遍历结束后,再令最后一个结点的后继重新指向头结点,构成一个环。
  • 实现代码如下:
/** 通过给定的某个结点,循环遍历出链表的每个元素 */
Status PrintCircularLinkListByNode(CircularLinkList clList, CircularLinkList node)
{
    if(clList == NULL)
    {
        printf("链表已被销毁,无法打印!\n");
        return ERROR;
    }
    if(IsEmptyCircularLinkList(clList))
    {
        printf("链表为空,无法打印!\n");
        return ERROR;
    }
    if(node == NULL)
    {
        printf("给定的结点不存在,请重新给定!\n");
        return ERROR;
    }

    //找到链表的最后一个结点并改变其指针域使其指向表中第一个结点,以便遍历时不经过头结点打印出不知名的值
    CircularLinkList lastNode = clList->next;
    for (int i = 1; i < GetLength(clList); i++)
    {
        lastNode = lastNode->next;
    }
    lastNode->next = clList->next;

    //记录下初始的结点指针
    CircularLinkList origNode = node;

    printf("%-6s%-10s\n", "ID", "Name");
    do
    {
        printf("%-6d%-10s\n", node->data.id, node->data.name);
        node = node->next;
    }while(node != origNode);

    /** 遍历结束后,再令最后一个结点的后继重新指向头结点,构成一个环 */
    lastNode->next = clList;

    return OK;
}

6、在用户指定的位置插入元素
  • 对于插入元素,有以下算法思路:
    1. 生成一个结点node,用于存储传入的数据元素信息并插入到表中。
    2. 若插入的位置是表中第一个位置:
      • 若插入时表为空表(表的长度为0):
        1. 将头结点的后继指向node,即令表中第一个结点为node。
        2. 令node的后继为头结点,构成一个环。
      • 若插入时表不为空表:
        1. 生成一个结点lastNode用于找到链表的最后一个结点并改变其指针域(令其指向头结点构成环)。
        2. 令传入的结点node的后继为表中当前第一个结点。
        3. 令头结点的后继指向node,这样node就插入到表中第一个位置了。
    3. 若插入的位置为当前表中最后一个结点的下一位置:
      1. 生成一个结点lastNode用于找到当前表中最后一个结点。
      2. 令node的后继为头结点,即插入后node为最后一个结点。
      3. 令lastNode的后继为node,即令最后一个结点为node。
    4. 若插入的位置不是第一个位置以及最后一个结点之后的位置:
      1. 生成一个结点currNode用于找到第pos - 1个结点。
      2. 令node的后继指向currNode的后继,即node的后继为当前表中第pos个结点。
      3. 令currNode的后继指向node,即第pos个结点为node。
  • 实现代码如下:
/** 在循环链表的指定位置(第pos个)插入元素 */
Status InsertCircularLinkList(CircularLinkList * clList, int pos, ElementType element)
{
    /** 创建一个空结点,用于存储插入元素的数据并链结到表中 */
    CircularLinkList node = (CircularLinkList)malloc(sizeof(CircularNode));
    node->data = element;
    node->next = NULL;

    /** 若插入的位置是表中第一个位置 */
    if (pos == 1)
    {
        /** 若插入时表为空表(表的长度为0) */
        if (GetLength(*clList) == 0)
        {
            (*clList)->next = node;     //令表中第一个结点为node
            node->next = (*clList);     //令node的后继为头结点,构成一个环
            printf("插入成功!\n");
            return OK;
        }
        else
        {
            /** 若插入时表不为空表,就要找到链表的最后一个结点并改变其指针域(令其指向头结点构成环)*/
            CircularLinkList lastNode = (*clList)->next;    //生成一个结点用于找到链表的最后一个结点
            for (int i = 1; i < GetLength(*clList); i++)
            {
                lastNode = lastNode->next;
            }
            lastNode->next = (*clList);         //令最后一个结点的后继为头结点,使链表构成一个环
            node->next = (*clList)->next;       //令node的后继为当前表中第一个结点
            (*clList)->next = node;             //令表中第一个结点为node
            printf("插入成功!\n");
            return OK;
        }
    }

    /** 若插入的位置为当前表中最后一个结点的下一位置 */
    if(pos == GetLength(*clList) + 1)
    {
        CircularLinkList lastNode = (*clList)->next;    //生成一个结点用于找到当前表中最后一个结点
        for(int i = 1; lastNode && i < pos - 1; i++)
        {
            lastNode = lastNode->next;
        }
        node->next = (*clList);         //令node的后继为头结点,即插入后node为最后一个结点
        lastNode->next = node;          //令lastNode的后继为node,即令最后一个结点为node
        printf("插入成功!\n");
        return OK;
    }

    /** 插入的不是第一个结点和最后一个结点之后 */
    CircularLinkList currNode = (*clList)->next;    //生成一个结点用于找到第pos - 1个结点
    for(int i = 1; currNode && i < pos - 1; i++)
    {
        currNode = currNode->next;      //pos大于2时,循环结束后为第pos - 1个结点
    }
    if(currNode)
    {
        node->next = currNode->next;    //令node的后继为当前表中第pos个结点
        currNode->next = node;          //令第pos个结点为node
    }
    printf("插入成功!\n");
    return OK;
}

7、删除并返回用户指定位置的元素
  • 对于删除元素,有以下算法思路:
    1. 若删除的位置为表中第一个位置:
      1. 生成一个结点node并指向表中第一个结点,即要删除的结点。
      2. 将要删除的结点的数据赋值给传入的element以便返回。
      3. 生成一个结点lastNode通过循环找到最后一个结点,令其后继为头结点。
      4. 将头结点的后继指向node的后继,即当前表中第二个结点变为了第一个结点。
      5. 释放掉node的内存空间,删除成功。
    2. 若删除的位置不为表中第一个位置:
      1. 生成一个结点preNode,用于找到被删除结点的前驱结点。
      2. 生成一个结点node,用于找到要删除的结点。
      3. 通过循环找到被删除结点的前驱结preNode点和被删除的结点node。
      4. 将要删除的结点node的数据赋值给element以便返回。
      5. 令被删除的结点node的前驱结点preNode的后继为node的后继,即跳过了第pos个结点。
      6. 释放掉node的内存空间,删除成功。
  • 实现代码如下:
/** 删除并返回循环链表中指定位置(第pos个)的元素 */
Status DeleteCircularLinkList(CircularLinkList * clList, int pos, ElementType * element)
{
    /** 若删除的位置为表中第一个位置 */
    if(pos == 1)
    {
        CircularLinkList node = (*clList)->next;    //生成一个结点node并指向表中第一个结点,即要删除的结点
        if(node != NULL)
        {
            *element = node->data;  //将要删除的结点的数据赋值给element以便返回

            //找到最后一个结点,改变其指针域的指向
            CircularLinkList lastNode = (*clList)->next;
            for(int i = 1; i < GetLength(*clList); i++)
            {
                lastNode = lastNode->next;
            }
            lastNode->next = (*clList);     //令最后一个结点的后继为头结点,构成一个环
            (*clList)->next = node->next;   //令表中第一个结点为node的后继,即当前表中第二个结点变为了第一个结点
            free(node);                     //释放掉要删除的结点的内存空间,删除完毕
        }
        printf("删除成功!\n");
        return OK;
    }

    /** 若删除的位置不为表中第一个位置 */
    CircularLinkList preNode;                   //生成一个结点preNode,用于找到被删除结点的前驱结点
    CircularLinkList node = (*clList)->next;    //生成一个结点node,用于找到要删除的结点
    for(int i = 1; node && i < pos; i++)        //通过循环找到被删除结点的前驱结点和被删除的结点
    {
        preNode = node;
        node = node->next;
    }
    if(node)
    {
        *element = node->data;          //将要删除的结点的数据赋值给element以便返回
        preNode->next = node->next;     //令被删除的结点的前驱结点的后继为被删除的结点的后继,即跳过了第pos个结点
        free(node);                     //释放掉要删除的结点的内存空间,删除完毕
    }
    printf("删除成功!\n");
    return OK;
}

8、遍历打印链表(第一个结点开始)
  • 对于打印链表,就十分简单了,只需生成一个结点并指向表中第一个结点用于遍历时打印结点数据即可,实现代码如下:
/** 打印链表 */
Status PrintCircularLinkList(CircularLinkList clList)
{
    if(clList == NULL)
    {
        printf("链表已被销毁,无法打印!\n");
        return ERROR;
    }

    if(IsEmptyCircularLinkList(clList))
    {
        printf("链表为空,无法打印!\n");
        return ERROR;
    }

    CircularLinkList node = clList->next;   //生成一个结点并指向表中第一个结点用于遍历时打印结点数据

    printf("%-6s%-10s\n", "ID", "Name");
    for(int i = 0; i < GetLength(clList); i++)
    {
        printf("%-6d%-10s\n", node->data.id, node->data.name);
        node = node->next;
    }

    return OK;
}

9、清空链表
  • 对于清空链表,有以下思路:
    1. 定义一个结点clearNode,用于删除链表结点。
    2. 定义一个结点nextNode,用于暂存clearNode的后继。
    3. 将clearNode指向链表的第一个结点。
    4. clearNode不为头结点时,循环删除结点:
      1. 用nextNode暂存clearNode的后继。
      2. 释放要删除结点(clearNode)的内存空间
      3. 将nextNode赋值给clearNode,以备下个结点的删除。
    5. 循环删除结点结束后,令头结点的后继为它自己,表示空表。
  • 实现代码如下:
/** 清空链表 */
Status ClearCircularLinkList(CircularLinkList * clList)
{
    CircularLinkList clearNode;     //定义一个结点clearNode,用于删除链表结点
    CircularLinkList nextNode;      //定义一个结点nextNode,用于暂存clearNode的后继

    clearNode = (*clList)->next;    //将clearNode指向链表的第一个结点

    //clearNode不为头结点时,循环删除结点
    while(clearNode != (*clList))
    {
        nextNode = clearNode->next; //暂存clearNode的后继
        free(clearNode);            //释放要删除结点的内存空间
        clearNode = nextNode;       //将删除结点的后继赋值给刚刚清空空间的clearNode,以备下个结点的删除
    }

    (*clList)->next = (*clList);    //令头结点的后继为它自己,表示空表

    printf("清空成功!\n");

    return OK;
}

10、销毁链表
  • 对于销毁链表,有以下思路:
    1. 定义一个结点p和一个结点q,用于循环销毁链表结点。
    2. 令p指向链表的第一个结点。
    3. p不为头结点时,循环销毁链表结点:
      1. 用q暂存p的后继结点。
      2. 释放p的内存空间。
      3. 令p指向刚刚暂存的后继结点,到下一次循环时接着销毁。
    4. 销毁结束后,销毁完成,p,q不再使用,令它们指向NULL。
    5. 释放头结点的内存空间,销毁掉头结点。
    6. 头结点销毁完成后不再使用,令头结点为NULL。
  • 实现代码如下:
/** 销毁链表 */
Status DestroyCircularLinkList(CircularLinkList * clList)
{
    CircularLinkList p;     //定义一个结点p,用于循环销毁链表结点
    CircularLinkList q;     //定义一个结点q,用于循环销毁链表结点

    p = (*clList)->next;    //令p指向链表的第一个结点

    while(p != (*clList))
    {
        q = p->next;        //暂存p的后继结点
        free(p);            //释放p的内存空间
        p = q;              //令p指向刚刚暂存的后继结点,到下一次循环时接着销毁
    }
    p = NULL;               //销毁完成后不再使用,令p指向NULL
    q = NULL;               //销毁完成后不再使用,令q指向NULL

    free(*clList);          //释放头结点的内存空间

    *clList = NULL;         //销毁完成后不再使用,令头结点为NULL

    printf("销毁成功!\n");

    return OK;
}

  • 实现了以上操作后,来测试一下效果,代码如下:
//测试函数
void TestFunction();

int main()
{
    TestFunction();
    return 0;
}

//测试函数
void TestFunction()
{
    int choice;             //用户的选择
    Menu();                 //菜单

    CircularLinkList L;     //要操作的链表

    while(1)
    {
        printf("\n\n请选择(按数字0退出):");
        scanf("%d", &choice);

        switch(choice)
        {
        case 1:

            printf("初始化链表:\n");

            InitCircularLinkList(&L);

            break;

        case 2:

            printf("尾插法建表:\n");

            CreatListTail(&L);

            break;

        case 3:

            printf("获取链表当前长度:\n");

            int len = GetLength(L);
            printf("当前链表长度为:%d\n", len);

            break;

        case 4:

            printf("判断链表当前是否为空:\n");

            if(IsEmptyCircularLinkList(L))
            {
                printf("当前链表为空!\n");
            }
            else
            {
                printf("当前链表非空!\n");
            }

            break;

        case 5:

            printf("输入数据查找链表中相对应的结点并从该结点开始遍历:\n");

            ElementType data;       //用于用户输入查找元素信息

            printf("请输入要查找的结点信息:\n");

            printf("ID:");
            scanf("%d", &data.id);
            fflush(stdin);          //清空缓冲区

            printf("Name:");
            scanf("%s", data.name);
            fflush(stdin);          //清空缓冲区

            CircularLinkList node = GetCircularLinkListNode(L, data);   //查找相对应的结点

            printf("查找到的结点信息:\n");
            printf("ID<%d>\tName[%s]\n\n", node->data.id, node->data.name);

            //从该结点开始遍历链表
            PrintCircularLinkListByNode(L, node);

            break;

        case 6:

            printf("在用户指定的位置插入元素:\n");

            int pos;                    //要插入的位置
            ElementType InsertData;     //要插入的元素

            printf("请输入要插入的位置:");
            scanf("%d", &pos);
            fflush(stdin);

            printf("请输入插入数据:\n");

            printf("ID:");
            scanf("%d", &InsertData.id);
            fflush(stdin);                  //清空缓存区

            printf("Name:");
            scanf("%s", InsertData.name);
            fflush(stdin);                  //清空缓存区

            //插入元素
            InsertCircularLinkList(&L, pos, InsertData);

            printf("插入后:\n");
            PrintCircularLinkList(L);

            break;

        case 7:

            printf("删除用户指定位置的元素:");

            int DeletePos;              //删除位置
            ElementType DeleteData;     //用于保存被删除的元素

            printf("请输入要删除元素的位置:");
            scanf("%d", &DeletePos);
            fflush(stdin);

            //删除元素
            DeleteCircularLinkList(&L, DeletePos, &DeleteData);

            printf("被删除的元素:\n");
            printf("ID<%d>\tName[%s]\n\n", DeleteData.id, DeleteData.name);

            printf("删除后:\n");
            PrintCircularLinkList(L);

            break;

        case 8:

            printf("遍历打印链表:\n");

            PrintCircularLinkList(L);

            break;

        case 9:

            printf("清空链表:\n");

            printf("清空前:\n");
            PrintCircularLinkList(L);

            //清空链表
            ClearCircularLinkList(&L);

            printf("清空后:\n");
            PrintCircularLinkList(L);

            break;

        case 10:

            printf("销毁链表:\n");

            printf("销毁前:\n");
            PrintCircularLinkList(L);

            //销毁链表
            DestroyCircularLinkList(&L);

            printf("销毁后:\n");
            PrintCircularLinkList(L);

            break;

        case 0:

            printf("退出程序……\n");
            exit(0);
        }
    }

}

  • 测试结果:
    image
    image
    image
    image
    image
    image
    image

小结

- 相比单链表,可以很方便的从任一结点开始遍历链表。

——————————- 本文结束 ——————————-


猜你喜欢

转载自blog.csdn.net/qq_41221623/article/details/82731586