02--单向循环链表

287 阅读7分钟

01--单向链表中已经介绍使用头节点会带来哪些好处,为了让大家能更深刻的体会头节点的好处这篇文章在设计单向循环链表时就不加入头节点了。看一下在不加入头节点的情况下,实现单向链表是有多麻烦。

单向循环链表

单向循环链表区别于单向链表的最大差别是每一个数据元素都有前驱后续,使得整个单向循环链表形成了一个,如图:

image.png

  • 首元节点的前驱是尾节点,后续是它的后一个节点;
  • 尾节点的后续是首元节点,前驱是它的前一个节点;
  • 其他节点的前驱是它的前一个节点,后续是它的后一个节点;

准备工作

xcode工程创建一个macOS的命令行工程,在main.c中准备如下数据:

#define ERROR 0

#define TRUE 1

#define FALSE 0

#define OK 1

#define MAXSIZE 20 /* 存储空间初始分配量 */

typedef int Status;/* Status是函数的类型,其值是函数结果状态代码,如OK等 */

typedef int ElemType;/* ElemType类型根据实际情况而定,这里假设为int */

定义结点:

typedef struct Node{
    ElemType data;      //数据域
    struct Node *next;  //指针域
}Node;

typedef struct Node * LinkList; //重命名结构体类型

一、创建

Status CreateList(LinkList *L){
    int item; //记录输入的数据     
    LinkList temp = NULL; //指向要创建的节点
    LinkList target = NULL; //指向需求的目标节点
    printf("输入节点的值,输入0结束\n");
    while(1) 
    {
        scanf("%d",&item);
        if(item == 0) break; 
        //如果输入的链表是空。则创建一个新的节点,使其next指针指向自己
        if(*L == NULL)
        {
            *L = (LinkList)malloc(sizeof(Node));
            if(!L) return ERROR;
            (*L)->data = item;
            (*L)->next = *L;
        }
        else
        {
           //输入的链表不是空的,寻找链表的尾节点,使尾节点的next=新节点。新节点的next指向首元节点
            for (target = *L; target->next != *L; target = target->next);
            temp = (LinkList)malloc(sizeof(Node));
            if(!temp) return ERROR;
            
            temp->data = item;
            temp->next = *L;  //新节点指向首元节点
            target->next = temp;//尾节点指向新节点
        }
    }
   return OK;
}

以上创建单向循环链表的逻辑使用的是尾插法,大致是利用一个while循环,通过scanf输入数据作为节点的数据来递归创建一个单向循环链表: image.png

  • 第一次插入节点时,创建一个节点,这个节点即是首元节点也是尾结点,所以它的next要指向自身
  • 非第一次插入数据时,需要先找到尾节点,因为创建的新节点的next要指向首元节点,尾节点的next要指向新节点;

二、插入

Status ListInsert(LinkList *L, int place, int num){
    LinkList temp ,target;

    int i;
    if (place == 1) {
        //如果插入的位置为1,则属于插入首元结点,所以需要特殊处理:
        //1. 创建新节点temp,并判断是否创建成功,成功则赋值,否则返回ERROR;
        //2. 找到链表最后的节点_尾节点,
        //3. 让新节点的next 指向首元节点.
        //4. 尾节点的next 指向新的首元节点;
        //5. 让头指针指向temp(临时的新节点)
        //创建新节点
        temp = (LinkList)malloc(**sizeof**(Node));
        if (temp == NULL) {
            return ERROR;
        }
        temp->data = num;
        //找到尾节点
        for (target = *L; target->next != *L; target = target->next);
        //新节点的next指向原来的首元节点
        temp->next = *L;
        //尾节点的next指向新节点
        target->next = temp;
        //新节点成为首元节点
        *L = temp;
    }else
    {
        //如果插入的位置在其他位置:
        //1. 创建新节点temp,并判断是否创建成功,成功则赋值,否则返回ERROR;
        //2. 先找到插入的位置,如果超过链表长度,则自动插入队尾;
        //3. 通过target找到要插入位置的前一个节点, 让target->next = temp;
        //4. 插入节点的前驱指向新节点,新节点的next 指向target原来的next位置 ;
        //创建新节点
        temp = (LinkList)malloc(sizeof(Node));
        if (temp == NULL) {
            return ERROR;
        }
        temp->data = num;

        //查找到插入位置的节点的前一个节点
        for ( i = 1,target = *L; target->next != *L && i != place - 1; target = target->next,i++) ;
        //新节点的next指向要插入位置原来的节点
        temp->next = target->next;
        //插入位置的节点的前一个节点的next指向新节点
        target->next = temp;
    }
    return OK;
}

情况一:插入的位置在首元节点上: image.png

当插入的位置是第一个位置,即首元节点所在的位置时,会影响L的指向,尾节点的next的指向,以及首元节点的变化,原来的首元节点变成了第二个节点,新插入的节点变成了新的首元节点。

情况二:插入的位置在其他位置上: image.png

包括尾节点在内的其他非首元节点都有前驱和后续,所以只需要找到要插入位置的前一个结点,将新节点的next指向要插入位置原来的节点,将要指入的位置的前一个节点的next指向新节点即可完成新节点的插入了。

三、删除

Status  LinkListDelete(LinkList *L,int place){
    LinkList temp,target;
    int i;

    //temp 指向链表首元结点
    temp = *L;
    if(temp == NULL) return ERROR;
    
    if (place == 1) {
        //①.如果删除到只剩下首元结点了,则直接将*L置空;
        if((*L)->next == (*L)){
            (*L) = NULL;
            return OK;
        }
        //②.链表还有很多数据,但是删除的是首元节点;
        //1. 找到尾节点, 使得尾节点next 指向头节点的下一个节点 target->next = (*L)->next;
        //2. 新节点做为首元节点,则释放原来的首元节点
        //找到尾节点
        for (target = *L; target->next != *L; target = target->next);
        temp = *L; //指向首元节点
        *L = (*L)->next;//L指向首元节点的next,使其成为新的首元节点
        target->next = *L;//尾节点的next指向新的首元节点
        free(temp); //释放节点
    }else
    {
        //如果删除其他结点--其他节点
        //1. 找到删除节点前一个节点target
        //2. 使得target->next 指向下一个节点
        //3. 释放需要删除的节点temp
        
        //找到删除节点的前一个节点
        for(i=1,target = *L;target->next != *L && i != place -1;target = target->next,i++) ;
           
        temp = target->next; //指向要删除的节点
        target->next = temp->next;//删除节点的前一个节点的next指向删除节点的next
        free(temp); //释放要删除的节点
      }
    return OK;
}

删除和插入一样都要区分是否是首元节点。当删除到只剩最一个节点时,需要将L指向空。

四、遍历

void show(LinkList p)
{
    //如果链表是空
    if(p == NULL){
        printf("打印的链表为空!\n");
        return;
    }else{
        LinkList temp;
        temp = p;
        do{
            printf("%5d",temp->data);
            temp = temp->next;
        }while (temp != p);
        printf("\n");
    }
}

单向循环链表的遍历最重要的是知道如何结束遍历,结束遍历的条件就是节点的next == 首元节点,则该节点为尾节点,遍历结束。

五、查询

int findValue(LinkList L,int value){
    int i = 1;
    LinkList p;
    p = L;
    //寻找链表中的结点 data == value
    while (p->data != value && p->next != L) {
        i++;
        p = p->next;
    }
    //当尾结点指向头结点就会直接跳出循环,所以要额外增加一次判断尾结点的data == value;
    if (p->next == L && p->data != value) {
       return  -1;
    }
    return i;
}

查询和遍历一样要注意什么时候结束查询,即节点的next指向首元节点时结束查询,防止循环查找。

六、总结

单向循环链表的特性是所有的节点都有前驱后续,首元节点的前驱是尾节点,尾节点的后续是首元节点。在不使用头节点的情况下,需要对首元节点位置的插入和删除做额外的判断,导致代码变得更加复杂。关于有头节点的单向循环链表的设计读者可以自行实现,去体会一下头节占的好处。