双向链表及双向循环链表

417 阅读6分钟

双向链表与单向链表的区别

上一篇中我们说过了单向链表,今天我们来看一下双向链表,看看它与单向链表的区别及双向链表的创建、插入、删除等操作。

从图中我们可以看到,双向链表相比单向链表而言,每个结点结构中增加一个前指针域,且除了第一个结点的前指针域为NULL外,其他的结点的前指针域指向前一个结点。

双向链表的定义

//定义结点
typedef struct Node{
    ElemType data;
    struct Node *prior; // 较单向链表多出的前指针域
    struct Node *next;
}Node;

typedef struct Node * LinkList;

双向链表的创建

创建双向链表一般也会先创建一个空的头结点,便于后续对链表的操作。

  1. 先创建一个头结点,判定创建是否成功
  2. 预设链表一个结点的个数,采用后插法增加链表结点
  3. 确定结点之间的双向链表关系,记录最新一个结点的位置
Status createDoubleLinkList(linkList *L){
    // 创建一个空结点
    *L = (linkList)malloc(sizeof(Node));
    if (*L == NULL) return ERROR;
    (*L)->prior = NULL;
    (*L)->next = NULL;
    (*L)->data = -1; // 值没有意义,因为在后面打印的时候忽略打印
    
    // 使用一个临时指针最新的结点的位置,方便后续插入新的结点
    linkList p = *L;
    
    // 新增结点
    int maxNodeCount = 10;
    for (int i =0; i < maxNodeCount; i ++) {
        linkList temp = (linkList)malloc(sizeof(Node));
        if (temp == NULL) {
            return ERROR;
        }
        
        temp->data = i; // 可用其他赋值
        temp->prior = NULL;// 初始设为NULL,后期可以重新设定
        temp->next = NULL;
        
        // temp和P的关系:p的next指向temp,temp的prior指向p
        p->next = temp;
        temp->prior = p;
        
        // p记录最新一个的结点temp
        p = temp;// 或者写成p = p->next,但是个人感觉可读性不如p = temp
        
    }
    
    return OK;
}

打印双向链表

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");
    
}

双向链表的插入

  1. 判断插入的位置是否合法,place不能小于1
  2. 找到要插入位置place的的结点
  3. 新增一个临时结点temp
  4. 重新确定结点之间的链接关系
  5. 注意:一定要先将后一个结点B和临时结点temp的关系先确定,否则结点B容易丢失
Status insertDoubleLinkList(linkList *L,int place,ElemType data)
{
    // 判断插入位置是否合法
    if (place < 1) {
        return ERROR;
    }
    
    // 指向头结点
    linkList p = *L;
    
    // 找到插入位置place的结点
    for (int i = 1; i < place && p; i ++) {
        p = p->next;
    }
    
    if (p == NULL) {
        return ERROR;
    }
    
    // 新增一个新节点
    linkList temp = (linkList)malloc(sizeof(Node));
    if (temp == NULL) {
        return ERROR;
    }
    
    temp->data = data;
    temp->prior = NULL;
    temp->next = NULL;
    
    // temp 插入在place的后面,就是p的后面
    // 如果p是链表尾部结点,那么p->next就是插入的temp
    if (p->next == NULL) {
        p->next = temp;
        temp->prior = p;
    } else {
        // 1 要先将p的下一个结点B的prior指向temp,temp的next指向B
        linkList B = p->next;
        B->prior = temp;
        temp->next = B;
        
        // 2 再将P的next指向temp,temp的prior指向P
        p->next = temp;
        temp->prior = p;
        
        // 如果将1,2 的操作顺序弄反,会造成结点B丢失
    }
    
    return OK;
}

双向链表删除

  1. 判断链表合法性,如果是空的链表,删除也就没有意义了
  2. 找到要删除位置place的前一个结点P
  3. 修改P的next指针指向要删除的结点delTemp的next也就是图中的结点C
  4. 修改结点C的prior指向P
  5. 释放要删除的结点delTemp
Status deleteLinkList(linkList *L,int place,ElemType *deleteDate)
{
    // 判断链表的合法性
    if (*L == NULL) {
        return ERROR;
    }
    
    linkList p = *L;
    // 找到要删除位置的前一个结点P
    for (int i = 1; i < place && p; i ++) {
        p = p -> next;
    }
    
    if (p == NULL) {
        return ERROR;
    }
    
    // 创建临时指针指向要删除的结点
    linkList delTemp = p->next;
    *deleteDate = delTemp->data;
    
    linkList C = delTemp->next;
    p->next = C;
    
    // 如果删除的是最后一个结点即c为空,那么c就不能把prior指向p,否则c的prior指向P
    if (C != NULL) {
        C->prior = p;
    }
    
    // 释放要删除的结点
    free(delTemp);
    
    return OK;
}

双向链表删除指定元素

Status LinkListDeletVAL(LinkList *L, int data){
    
    LinkList p = *L;
    
    //1.遍历双向循环链表
    while (p) {
       
        //2.判断当前结点的数据域和data是否相等,若相等则删除该结点
        if (p->data == data) {
            
            //修改被删除结点的前驱结点的后继指针,参考图上步骤1️⃣
            p->prior->next = p->next;
            //修改被删除结点的后继结点的前驱指针,参考图上步骤2️⃣
            if(p->next != NULL){
                p->next->prior = p->prior;
            }
            //释放被删除结点p
            free(p);
            // 如果只想删除一个指定元素,break跳出循环
            // 想要删除所有相同的元素就不用break
            break; 
            
        }
        
        //没有找到该结点,则继续移动指针p
        p = p->next;
    }
    
    return OK;
}

以上就是双向链表的相关操作。双向链表的头结点的prior为NULL,尾结点的next为NULL,其他结点的prior和next各有结点链接。

当我们把头结点的prior指向尾结点,尾结点的next指向头结点,这样就形成了一个双向循环链表了。好,我们以下就来介绍双向循环链表。

双向循环链表的创建,添加,删除等操作可参考双向链表对应的操作,就不继续分析了。

总结

数据结构的存储一般常用包括:顺序存储结构和链式存储结构。

  1. 空间分配:
    顺序结构存储数据,需预先申请一整块足够大的存储空间,然后将数据按照次序逐一存储,数据之间紧密贴合,不留一丝空隙。

    链表的存储方式与顺序表截然相反,什么时候存储数据,什么时候才申请存储空间,数据之间的逻辑关系依靠每个数据元素携带的指针维持。

  2. 存储密度
    顺序表存储数据实行的是 "一次开辟,永久使用",即存储数据之前先开辟好足够的存储空间,空间一旦开辟后期无法改变大小(使用动态数组的情况除外)。

    链表则不同,链表存储数据时一次只开辟存储一个节点的物理空间,如果后期需要还可以再申请。

  3. 操作效率
    链表中数据元素之间的逻辑关系靠的是节点之间的指针,当需要在链表中某处插入或删除节点时,只需改变相应节点的指针指向即可,无需大量移动元素,因此链表中插入、删除或移动数据所耗费的时间复杂度为 O(1)。

    顺序表中,插入、删除和移动数据可能会牵涉到大量元素的整体移动,因此时间复杂度至少为 O(n)。

  4. 访问效率
    顺序表中存储的元素可以使用数组下标直接访问,无需遍历整个表,因此使用顺序表访问元素的时间复杂度为 O(1)。

    链表中访问数据元素,需要从表头依次遍历,直到找到指定节点,花费的时间复杂度为 O(n);