03--双向链表

462 阅读7分钟

# 01--单向链表# 02--单向循环链表已经介绍了单向链表,这篇文章来开始介绍一下双向链表。

一、双向链表的设计

在单向链表中设计了一个next指针指向下一个节点,在双向链表中需要设计一个prior指针指向上一个节点,使得当前节点即可以通过next拿到后续,也能prior拿到前驱。节点设计如下:

image.png

通过这样的节点设计,可以得到双向链表的链式结构形如:

image.png 双向链表的两种设计思路:

  • 1.第一种是有头节点的双向链表的设计;
  • 2.第二种是没有头节点的双向链表的设计。

双向链表的特性:

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

# 01--单向链表# 02--单向循环链表中已经通过比较知道了头节点的好处,所以在设计双向链表时我们也设计一个头节点。

准备

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

#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 createLinkList(LinkList *L){
    //创建一个头节点并将*L 指向头节点
    *L = (LinkList)malloc(sizeof(Node));

    if (*L == NULL) return ERROR;
    //头节点的next和prior都指向NULL
    (*L)->prior = NULL;
    (*L)->next = NULL;
    (*L)->data = -1;

    //新增数据
    LinkList p = *L;//p指向最后一个节点

    for(int i=0; i < 10;i++){

        //1.创建新节点
        LinkList temp = (LinkList)malloc(sizeof(Node));
        temp->prior = NULL;
        temp->next = NULL;
        temp->data = i;

        //2.将新节点加入链表(尾插法)
        p->next = temp;//插到链表的尾部

        //新节点的prior指向原来的尾节点
        temp->prior = p;
        
        //3.p指向尾节点,方便下一次插入
        p = p->next;
    }

    return OK;
}
  • 创建双向链表时,需要创建一个头节点,根据双向链表的特性,此时头节点的prior和next都要指向NULL;
  • 使用尾插法向链表中插入数据时,新创建的节点放在链表的尾部,原来的尾节点的next指向新节点,新节点的prior指向原来的尾节点;
  • 记录尾节点的位置方便下一次插入数据。

三、遍历

void display(LinkList L){
    LinkList temp = L->next;

    if(temp == NULL){
        printf("打印的双向链表为空!\n");
        return;
    }

    while (temp) {
        printf("%d  ",temp->data);
        temp = temp->next;
    }
    printf("\n");
}

遍历的关键是知道在哪里结束,根据双向链表的特性,当节点的next指向NULL时即结束。

四、插入

Status ListInsert(LinkList *L, int i, ElemType data){    

    //1. 插入的位置不合法 为0或者为负数
    if(i < 1) return ERROR;

    //2. 新建结点
    LinkList temp = (LinkList)malloc(sizeof(Node));
    temp->data = data;
    temp->prior = NULL;
    temp->next = NULL;

    //3.将p指向头结点!
    LinkList p = *L;

    //4. 找到插入位置i直接的结点
    for(int j = 1; j < i && p;j++) {
        p = p->next;
    }

    //5. 如果插入的位置超过链表本身的长度
    if(p == NULL){
        return  ERROR;
    }

    //6. 判断插入位置是否为链表尾部;
    if (p->next == NULL) {
        p->next = temp;
        temp->prior = p;
    }else
    {
        //1️⃣ 将p->next 结点的前驱prior = temp
        p->next->prior = temp;

        //2️⃣ 将temp->next 指向原来的p->next
        temp->next = p->next;

        //3️⃣ p->next 更新成新创建的temp
        p->next = temp;

        //4️⃣ 新创建的temp前驱 = p
        temp->prior = p;
    }
    return  OK;
}
  • 插入数据时,首先需要判断插入的位置是否合法
  • 找到目标位置的前一个节点,判断最终是否找到了有效的插入位置;
  • 创建新节点,根据双向链表的特性进行操作:首先目标位置的前一个节点的next指向新结点;其次有如下有两种情况:
  • 1.如果插入的位置是尾节点的位置,新节点的next指向前面节点的next(此时指向的是NULL),新节点的prior指向前面的节点
  • 2.如果插入的位置不是尾节点,新节点的next指向后面的节点prior指向前面的节点前面节点的next指向新节点后面节点的prior指向新节点。

image.png

如上图所示,在向链表中插入p时,p插入的位置可能是首元节点的位置,可能是尾节点的位置,也可能是其它位置。因为有头节点的存在,结合双向链表的特性我们知道,除尾结点以外的其它任何节点nextprior的指向都不会NULL,所以需要针对尾节点特殊处理

五、删除指定位置

Status ListDelete(LinkList *L, int i, ElemType *e){

    int k = 1;
    LinkList p = (*L);   

    //1.判断双向链表是否为空,如果为空则返回ERROR;
    if (*L == NULL) {
        return ERROR;

    }

    //2. 找到目标位置的前一个节点
    while (k < i && p != NULL) {
        p = p->next;
        k++;
    }

    //3.如果k>i 或者 p == NULL 则返回ERROR
    if (k>i || p == NULL) {
        return  ERROR;
    }

    //4.创建临时指针delTemp 指向要删除的结点,并将要删除的结点的data 赋值给*e带回
    LinkList delTemp = p->next;
    *e = delTemp->data;

    //5. p->next 等于要删除的结点的下一个结点
    p->next = delTemp->next;

    //6. 如果删除结点的下一个结点不为空,则将将要删除的下一个结点的前驱指针赋值p;
    if (delTemp->next != NULL) {
        delTemp->next->prior = p;
    }

    //7.删除delTemp结点
    free(delTemp);

    return OK;
}
  • 删除同样要先判断当前删除的位置是否合法;
  • 找到删除节点和删除节点的前驱
  • 前驱节点的next指向删除节点的next;
  • 插入一样,需要判断删除节点是否是尾节点
  • 1.如果删除的是尾结点,什么都不做(此时前驱节点的next指向的是NULL,思考一下为什么?);
  • 2.如果删除的不是尾结点,需要将删除节点的后续的prior指向删除节点的前驱

image.png

六、删除指定元素

Status LinkListDeletVAL(LinkList *L, int data){
    LinkList p = *L;

    //1.遍历双向循环链表
    while (p) {

        //2.判断当前结点的数据域和data是否相等,若相等则删除该结点
        if (p->data == data) {
            //前驱的next指向后续
            p->prior->next = p->next;

            //不是尾节点时,后续的prior指向前驱
            if(p->next != NULL){
                p->next->prior = p->prior;
            }

            //释放被删除结点p
            free(p);

            //退出循环
            break;
        }

        //没有找到该结点,则继续移动指针p
        p = p->next;
    }
    return OK;
}

链表中可能有各们数据域数据相同的数据,所以要遍历整个链表找到所有符合条件的数据进行删除。

七、查询

int selectElem(LinkList L,ElemType elem){

    LinkList p = L->next;
    int i = 1;
    
    while (p) {
        if (p->data == elem) {
           return i;
        }
        i++;
        p = p->next;
    }

    return  -1;
}

查询分为查询指定位置的元素和查询指定元素的位置,前者的结果是唯一的,后者的结果可能是多个的,上面的方法是查询指定元素的位置,在查询到第一个符合条件的位置后就返回了。

八、修改

Status replaceLinkList(LinkList *L,int index,ElemType newElem){
    if (index < 1) return ERROR;
    
    LinkList p = (*L)->next;

    for (int i = 1; i < index; i++) {
        p = p->next;
    }

    p->data = newElem;
    return OK;
}

不管理是修改、查询、还是删除和插入,最先应该判断的就是操作的位置是否合法。对于异常情况的处理是保证一个算法稳定健壮的关键

九、总结

头结点的重要性在双向链表中也有明显体现,在包含头节点的双向链表删除、插入中要对尾节点已经特殊的处理。