跳转至

AB21-分别采用头插法和尾插法创建一个带头节点的双向链表

代码实现

尾插法

C
/**
 * 采用尾插法创建带头结点的双向链表
 * @return 指向双向链表头结点的指针
 */
DinkList create_tail() {
    // 1. 创建并初始化头结点
    DinkList L = (DinkList)malloc(sizeof(DNode));
    if (L == NULL) {
        printf("内存分配失败\n");
        return NULL;
    }
    L->data = 0;        // 头结点数据域可设为默认值
    L->next = NULL;     // 头结点后继指针初始化为空
    L->prev = NULL;     // 头结点前驱指针初始化为空

    // r始终指向链表的尾结点,便于尾插操作
    DinkList r = L;

    // 读取数据
    int x;
    printf("请输入数据(输入9999结束):");
    scanf("%d", &x);

    // 2. 循环创建结点,直到输入结束标志
    while (x != 9999) {
        // 创建新结点
        DinkList s = (DinkList)malloc(sizeof(DNode));
        if (s == NULL) {
            printf("内存分配失败\n");
            return L;  // 返回已创建的部分链表
        }

        // 设置新结点数据
        s->data = x;
        s->next = NULL;     // 新结点后继指针初始化为空

        // 3. 尾插操作:建立双向连接
        r->next = s;        // 尾结点指向新结点
        s->prev = r;        // 新结点指向前驱(原尾结点)
        r = s;              // 更新尾指针,指向新的尾结点

        // 读取下一个数据
        scanf("%d", &x);
    }

    return L;  // 返回链表头指针
}

头插法

C
/**
 * 采用头插法创建带头结点的双向链表
 * @return 指向双向链表头结点的指针
 */
DinkList create_head() {
    // 1. 创建并初始化头结点
    DinkList L = (DinkList)malloc(sizeof(DNode));
    if (L == NULL) {
        printf("内存分配失败\n");
        return NULL;
    }
    L->data = 0;
    L->next = NULL;
    L->prev = NULL;

    // 读取数据
    int x;
    printf("请输入数据(输入9999结束):");
    scanf("%d", &x);

    // 2. 循环创建结点,直到输入结束标志
    while (x != 9999) {
        // 创建新结点
        DinkList s = (DinkList)malloc(sizeof(DNode));
        if (s == NULL) {
            printf("内存分配失败\n");
            return L;
        }

        // 设置新结点数据
        s->data = x;

        // 3. 头插操作:建立双向连接
        s->next = L->next;          // 新结点指向原第一个结点
        if (L->next != NULL) {      // 如果原链表不为空
            L->next->prev = s;      // 原第一个结点的前驱指向新结点
        }
        L->next = s;                // 头结点指向新结点
        s->prev = L;                // 新结点指向前驱(头结点)

        // 读取下一个数据
        scanf("%d", &x);
    }

    return L;  // 返回链表头指针
}

算法执行流程图解

双向链表结构:

Text Only
1
2
3
4
5
6
双向链表结点结构:
┌─────────┬─────────┬─────────┐
│  prev   │   data  │  next   │
└─────────┴─────────┴─────────┘
    ↑                   ↑
  指向前驱           指向后继

尾插法执行过程:

Text Only
输入序列:1 2 3 9999

初始状态:L → [ ] ↔ NULL
          r

输入1:   L → [ ] ↔ [1] ↔ NULL
          ↑       ↑
          头     r(尾)

输入2:   L → [ ] ↔ [1] ↔ [2] ↔ NULL
          ↑             ↑
          头           r(尾)

输入3:   L → [ ] ↔ [1] ↔ [2] ↔ [3] ↔ NULL
          ↑                   ↑
          头                 r(尾)

头插法执行过程:

Text Only
输入序列:1 2 3 9999

初始状态:L → [ ] ↔ NULL

输入1:   L → [ ] ↔ [1] ↔ NULL
               新插入

输入2:   L → [ ] ↔ [2] ↔ [1] ↔ NULL
                ↑     ↑
               新插入

输入3:   L → [ ] ↔ [3] ↔ [2] ↔ [1] ↔ NULL
                ↑     ↑     ↑
               新插入

双向连接建立过程图解

尾插法连接建立:

Text Only
插入新结点s到尾结点r之后:

步骤1:r->next = s
L → [...] ↔ [r] → [s]    [s] ← NULL

步骤2:s->prev = r  
L → [...] ↔ [r] ↔ [s]    [s] ← NULL

步骤3:r = s
L → [...] ↔ [r/s] ↔ NULL

头插法连接建立:

Text Only
插入新结点s到头结点L之后:

原状态:L → [ ] ↔ [A] ↔ [B] ↔ NULL

步骤1:s->next = L->next
[s] → [A]

步骤2:L->next->prev = s (如果存在)
[A] → [s]

步骤3:L->next = s
L → [s]

步骤4:s->prev = L
L ↔ [s]

最终:L → [ ] ↔ [s] ↔ [A] ↔ [B] ↔ NULL

复杂度分析

时间复杂度:O(n)

  • 创建结点:n次malloc操作,每次O(1),总计O(n)
  • 连接操作
  • 尾插法:每次4次指针操作,总计O(n)
  • 头插法:每次4次指针操作,总计O(n)
  • 输入操作:n次scanf操作,总计O(n)
  • 总体时间复杂度O(n)

空间复杂度:O(n)

  • 结点存储:n个结点,每个结点占用O(1)空间,总计O(n)
  • 辅助变量:指针变量L、r、s等,O(1)
  • 总体空间复杂度O(n)

优化版本:添加错误处理和更清晰的逻辑

C
/**
 * 优化版本:添加错误处理和更清晰的逻辑
 */
DinkList create_tail_optimized() {
    DinkList L = (DinkList)malloc(sizeof(DNode));
    if (!L) return NULL;

    L->next = NULL;
    L->prev = NULL;
    DinkList r = L;
    int x;

    printf("请输入数据(输入-1结束):");
    while (scanf("%d", &x) && x != -1) {
        DinkList s = (DinkList)malloc(sizeof(DNode));
        if (!s) {
            printf("内存分配失败\n");
            break;
        }

        s->data = x;
        s->next = NULL;
        s->prev = r;
        r->next = s;
        r = s;
    }

    return L;
}

优化版本:带计数功能的头插法

C
/**
 * 带计数功能的头插法
 */
DinkList create_head_with_count(int *count) {
    DinkList L = (DinkList)malloc(sizeof(DNode));
    if (!L) return NULL;

    L->next = NULL;
    L->prev = NULL;
    *count = 0;
    int x;

    printf("请输入数据(输入-1结束):");
    while (scanf("%d", &x) && x != -1) {
        DinkList s = (DinkList)malloc(sizeof(DNode));
        if (!s) {
            printf("内存分配失败\n");
            break;
        }

        s->data = x;
        s->next = L->next;
        s->prev = L;
        if (L->next) {
            L->next->prev = s;
        }
        L->next = s;
        (*count)++;
    }

    return L;
}

双向链表与单向链表对比

特性 单向链表 双向链表
存储空间 每个结点1个指针 每个结点2个指针
插入操作 需要前驱结点 可直接操作
删除操作 需要前驱结点 可直接操作
遍历方向 只能向前 可前向/后向
实现复杂度 简单 复杂

算法特点

核心思想: - 尾插法:维护尾指针,每次在尾部添加新结点 - 头插法:每次在头部插入新结点,建立双向连接

关键技巧: 1. 双向连接:正确设置prev和next指针 2. 边界处理:处理空链表和第一个结点的特殊情况 3. 指针操作顺序:避免断链和野指针

注意点: - 每次插入都要同时维护前驱和后继指针 - 头插法需要特别注意原第一个结点的前驱指针更新 - 及时检查内存分配是否成功

优势: - 可以双向遍历 - 插入和删除操作更加灵活 - 某些算法实现更简单

这种双向链表的创建方法是数据结构中的基础操作,为后续的插入、删除等操作提供了良好的基础。