03--双向循环链表

201 阅读5分钟

# 03--双向链表中通过加入头节点的方式设计了双向链表,这篇文章同样通过加入头节点的方式设计双向循环链表。

一、双向循环链表的设计

双向循环链表的节点设计与双向链表一样:

image.png

在都有头节点的设计下,双向循环表的与双向链表的差别在于,双向循环链表的尾节点的next指向头节点,头节点的prior指向尾结点image.png

双向循环链表的特性:

  • 1.有唯一第一个节点和最后一个节点;
  • 2.第一个节点的prior指向尾结点,最后一个节点的next指向头节点;
  • 3.其他节点的prior指向前驱,next指向后续。

准备

为方便函数调用返回一些状态值,对一些数据类型已经重定义:

#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 *prior; //指向前驱
    struct Node *next;  //指向后续
}Node;

typedef struct Node * LinkList;//重定义类型名

二、创建

Status creatLinkList(LinkList *L){
    //创建头结点并加入链表
    *L = (LinkList)malloc(sizeof(Node));

    if (*L == NULL) {
        return ERROR;
    }
    //创建时,头结点的next和prior都指向自己
    (*L)->next = (*L);
    (*L)->prior = (*L);

    //新增数据
    LinkList p = *L;

    for(int i=0; i < 10;i++){
        //1.创建新结点
        LinkList temp = (LinkList)malloc(sizeof(Node));
        temp->data = i;

        //2.为新增的结点建立双向链表关系
        //① temp 是p的后继
        p->next = temp;

        //② temp 的前驱是p
        temp->prior = p;

        //③ temp的后继是*L
        temp->next = (*L);
        
        //p <=> temp <=> *L

        //④ *L 的前驱是新建的temp
        *L->prior = temp;

        //⑤ p 要记录最后的结点的位置,方便下一次插入
        p = p->next;
    }
    
    return OK;
}
  • 双向循环链表的创建时,加入头结点后,为了保持双向循环链表的特性,头结点的nextprior都要指向自己

image.png

  • 使用尾插法在双向循环链表的表尾插入数据后,不仅需要将新结点与原来的尾结点建立连接关系,头结点的prior也要指向新结点;
  • 尾插法的关键是找到尾结点,所以用一个变量记录尾结点的位置可以有效提高下一次插入数据时的效率。

三、插入

双向循环链表的插入其实跟双向链表的插入的实现逻辑差不多,差别在于不用作插入位置为尾结点的判断,因为双向循环链表的尾结点的next指向头结点,头结点的prior指向原来的尾结点,尾结点的next指向不会指向NULL。而双向链表的尾结点的next指向NULL,如果用它去访问next会出现crash。

Status LinkListInsert(LinkList *L, int index, ElemType e){
    //1. 创建指针p,指向双向链表头
    LinkList p = (*L);
    int i = 1;

    //2.双向循环链表为空,则返回error
    if(*L == NULL) return ERROR;

    //3.找到插入位置的前一个结点
    while (i < index && p->next != *L) {
        p = p->next;
        i++;
    }

    //4.如果i>index 则返回error
    if (i > index)  return ERROR;

    //5.创建新结点temp
    LinkList temp = (LinkList)malloc(sizeof(Node));

    //6.temp 结点为空,则返回error
    if(temp == NULL) return ERROR;  

    //7.将生成的新结点temp数据域赋值e.
    temp->data = e;

    //8.将结点temp 的前驱结点为p;
    temp->prior = p;

    //9.temp的后继结点指向p->next;
    temp->next = p->next;

    //10.p的后继结点为新结点temp;
    p->next = temp;

    //11.temp节点的下一个结点的前驱为temp 结点
    temp->next->prior = temp;
    return OK;
}
  • 首先判断插入位置是否合法;
  • 找到目标位置的前一个结点p,判断是否找到有效的结点;
  • 创建新结点temp,设置数据;
  • 新结点temp的prior指向p,temp的next指向p的next
  • p的下一个结点的prior指向新结点temp,这样temp就成功插在了p的后面了;
  • 因为双向循环链表的每一个结点的next和prior都不会指向NULL,所以不用像双向链表的插入那样对尾结点做额外的判断

四、遍历

Status Display(LinkList L){
    if (L == NULL) {
        printf("打印的双向循环链表为空!\n\n");
        return ERROR;
    }

    printf("双向循环链表内容:  ");
    LinkList p = L->next;

    while (p != L) {
        printf("%d  ",p->data);
        p = p->next;
    }

    printf("\n\n");
    return OK;
}

遍历的关键是知道什么时候结束遍历,双向循环链表当next指向L时结束。

五、删除

Status LinkListDelete(LinkList *L,int index,ElemType *e){
    int i = 1;
    //判断链表是否存在
    LinkList temp = (*L)->next;
    if (*L == NULL) {
        return  ERROR;
    }
    
    //判断链表是否是空链表(只有首元结点)
    if(temp->next == *L){
        return ERROR;
    }

    //1.找到要删除的结点
    while (i <= index && temp != *L) {//temp != *L不加这个判断会在链表内循环查找
        temp = temp->next;
        i++;
    }
    //1.1没找到要删除的结点
    if(temp == *L) reture ERROR;
    
    //2.给e赋值要删除结点的数据域
    *e = temp->data;

    //3.删除结点temp的前一个结点的next指temp的next
    temp->prior->next = temp->next;

    //4.删除结点temp的后一个结点的prior指向temp的prior
    temp->next->prior = temp->prior;

    //5. 删除结点temp
    free(temp);
    
    return OK;
}
  • 删除结点的时候要先判断链表是否为空或有有效结点;
  • 找到删除结点temp,此时temp不能L,因为L指向的是头结点,如果为*L说明删除结点是无效的;
  • 删除结点temp前,要将temp的前驱和后续连接起来;
  • free释放结点。

六、总节

  • 双向循环链表的查找和修改和双向链表的实现逻辑是一样的,差别在于遍历结束的判断,双向链表结束的判断是:p->next == NULL;双向循环链表的结束判断是:p->next == L;
  • 双向循环链表不用对尾结点的插入和删除作额外判断,因为尾结点的next指向头结点,头结点可以访问prior而不引发crash。