数据结构 02 线性表

1 带头结点的单链表,头结点head始终存在

当带头结点的单链表为空时,头结点的next指针指向NULL,也就是head->next == NULL

核心特征再明确

基于你给出的定义,带头结点单链表的空表状态有两个核心特征:

  1. 头结点必存在 :无论链表是否存储数据,head 指针始终指向一个有效的头结点,不会为 NULL
  2. 唯一判断标志 :空表的唯一判定条件是头结点的 next 指针指向 NULL,即 head->next == NULL。此时链表中除头结点外,无任何数据结点。

与无表头链表的关键区别

为了更清晰理解其设计优势,可与 "无表头结点的单链表" 做对比:

对比维度 带头结点的单链表(空表时) 无表头结点的单链表(空表时)
头指针 head 指向有效头结点(非 NULL) 直接指向 NULL
空表判断条件 head->next == NULL head == NULL
插入 / 删除首元素 操作统一,无需改 head 需特殊处理,可能修改 head

2 双向循环链表插入新结点

答案是 C。下面给你详细解释每一步的作用,保证通俗易懂~

在双向循环链表中,每个结点都有两个指针:prior(指向前一个结点)和next(指向后一个结点)。现在要在p指向的结点前插入q指向的新结点,需要处理好qp以及p原来前驱结点之间的指针关系,步骤如下:

步骤 1:q->next = p;

这一步是让qnext指针指向p。因为q要插入到p的前面,插入后q的下一个结点就是p,所以先把qnextp连起来。

步骤 2:q->prior = p->prior;

这一步是让qprior指针指向p原来的前驱结点。这样q就和p原来的前驱结点建立了联系,为后续修改该前驱结点的next指针做准备。

步骤 3:p->prior->next = q;

p->priorp原来的前驱结点,现在要让这个前驱结点的next指针指向q。因为q插入到了p前面,所以原来p的前驱结点的下一个结点就变成了q,这样就把qp原来的前驱结点连接好了。

步骤 4:p->prior = q;

最后,让pprior指针指向q。因为q现在在p的前面,所以p的前一个结点就是q,这样pq也连接好了。

这样四步操作完成后,q就成功插入到了p的前面,双向循环链表的指针关系也都正确维护好了。

【顺序怎么考虑?】

要理解这个顺序,得结合双向循环链表的结构特点 (每个结点有 prior 指向前驱、next 指向后继,且链表首尾相连成环),以及**"插入结点时,需同时维护前驱和后继的双向指针关系"** 的核心逻辑。我们可以把插入过程拆成 "连接 q 与前后结点""更新前后结点与 q 的连接" 两步来分析:

第一步:先把 q 自身的两个指针 "锚定" 好

  • q->next = p;:确定 q后继p(因为 q 要插在 p 前面,插入后 q 的下一个结点就是 p)。
  • q->prior = p->prior;:确定 q前驱p 原来的前驱(记为 node)。这一步是为了让 q 先 "找到"p 之前的邻居,为后续更新 node 的指针做准备。

第二步:更新原有结点与 q 的连接

  • p->prior->next = q;p->prior 就是刚才的 nodep 原来的前驱)。现在要让 node后继 指向 q(因为 q 插入到了 nodep 之间,node 的下一个结点从 p 变成了 q)。
  • p->prior = q;:最后让 p前驱 指向 q(因为 q 现在在 p 前面,p 的前一个结点变成了 q)。

为什么顺序不能乱?

如果先动原有结点(比如先改 p->prior->nextp->prior),会出现 **"找不到目标结点"** 的问题:

  • 若先执行 p->prior->next = q;,但此时 q->prior 还没指向 p->priorqnode 没建立联系,这步操作逻辑不完整。
  • 若先执行 p->prior = q;,会直接丢失 p 原来的前驱 node(因为 p->prior 被改成 q 了,后面再想找 node 就找不到了)。

所以必须先把 q 自身的前驱、后继确定好 ,再去更新原有结点(nodep)与 q 的连接,这样才不会 "断链"。

【可以先q->prior = p->prior;再q->next = p; 吗?】

可以的!

在双向循环链表中,q->prior = p->prior;(让q的前驱指向p原来的前驱)和q->next = p;(让q的后继指向p)这两步的顺序可以交换

因为这两步都是在设置q自身的两个指针priornext),这两个操作之间没有依赖关系(既不需要先知道qnext,也不需要先知道qprior,才能设置另一个)。

不管先设置qprior,还是先设置qnext,只要最终qprior指向p原来的前驱、qnext指向p,这部分就完成了。

后续的p->prior->next = q;(让p原来前驱的next指向q)和p->prior = q;(让pprior指向q),则需要基于前两步已经设置好的q的指针来执行。

3 时间效率题

顺序表(选项 A)

顺序表是用一段连续的存储单元依次存储线性表的数据元素。

  • 存取任一指定序号的元素:由于元素在内存中是连续存储的,可通过 "基地址 + (序号 - 1)× 元素大小" 直接计算出元素的存储位置,时间复杂度为 \(O(1)\)。
  • 在最后进行插入和删除运算:顺序表在表尾进行插入和删除时,不需要移动其他元素(除非表已满需要扩容,但题目中未提及扩容情况,默认空间足够),时间复杂度为 \(O(1)\)。所以顺序表能很好地满足这两种常用操作的时间效率要求。

双链表(选项 B)

双链表每个结点有两个指针,分别指向直接前驱和直接后继。

  • 存取任一指定序号的元素:需要从链表的头结点(或尾结点)开始,沿着指针依次遍历,直到找到指定序号的元素,时间复杂度为 \(O(n)\)(n 为线性表长度),效率低于顺序表。
  • 在最后进行插入和删除运算:虽然双链表可以通过尾指针快速找到表尾,但相比于顺序表的直接操作,还是需要进行指针的修改等操作,且存取指定序号元素效率不高,整体不如顺序表。

带头结点的双循环链表(选项 C)

是双链表的一种特殊形式,链表首尾相连,且有头结点。

  • 存取任一指定序号的元素:同样需要遍历链表,时间复杂度 \(O(n)\),无法快速存取指定序号元素。
  • 在最后进行插入和删除运算:操作相对双链表更规范,但存取指定序号元素的短板仍存在,不满足要求。

单循环链表(选项 D)

是单链表的循环形式,表尾结点的指针指向头结点。

  • 存取任一指定序号的元素:需要遍历链表,时间复杂度 \(O(n)\),不能快速存取指定序号元素。
  • 在最后进行插入和删除运算:需要先找到表尾结点(遍历),然后进行操作,效率不如顺序表。

4 实现带头结点单链表倒置

以下是用 C 语言实现带头结点单链表倒置的代码:

cpp 复制代码
#include <stdio.h>
#include <stdlib.h>

// 定义链表节点结构
typedef struct Node {
    int data;
    struct Node *next;
} Node;

// 创建新节点
Node *createNode(int data) {
    Node *newNode = (Node *)malloc(sizeof(Node));
    if (newNode == NULL) {
        printf("内存分配失败\n");
        exit(1);
    }
    newNode->data = data;
    newNode->next = NULL;
    return newNode;
}

// 构建带头结点的单链表(从尾插入)
Node *buildList() {
    Node *head = (Node *)malloc(sizeof(Node));
    if (head == NULL) {
        printf("内存分配失败\n");
        exit(1);
    }
    head->next = NULL;

    int n, data;
    printf("请输入链表节点个数:");
    scanf("%d", &n);

    for (int i = 0; i < n; i++) {
        printf("请输入第 %d 个节点的数据:", i + 1);
        scanf("%d", &data);
        Node *newNode = createNode(data);
        // 从尾插入
        Node *p = head;
        while (p->next != NULL) {
            p = p->next;
        }
        p->next = newNode;
    }
    return head;
}

// 倒置带头结点的单链表
void reverseList(Node *head) {
    if (head == NULL || head->next == NULL || head->next->next == NULL) {
        return;
    }
    Node *prev = head->next;
    Node *curr = prev->next;
    prev->next = NULL; // 第一个节点变为尾节点

    while (curr != NULL) {
        Node *nextTemp = curr->next;
        curr->next = prev;
        prev = curr;
        curr = nextTemp;
    }
    head->next = prev;
}

// 打印链表
void printList(Node *head) {
    Node *p = head->next;
    while (p != NULL) {
        printf("%d ", p->data);
        p = p->next;
    }
    printf("\n");
}

// 释放链表内存
void freeList(Node *head) {
    Node *p = head;
    while (p != NULL) {
        Node *temp = p;
        p = p->next;
        free(temp);
    }
}

int main() {
    Node *head = buildList();

    printf("原始链表:");
    printList(head);

    reverseList(head);

    printf("倒置后链表:");
    printList(head);

    freeList(head);

    return 0;
}

代码说明

  1. createNode函数:用于创建新的链表节点,为节点分配内存并初始化数据和指针。
  2. buildList函数:构建一个带头结点的单链表,通过从尾插入的方式添加节点,用户需要输入节点个数和每个节点的数据。
  3. reverseList函数 :实现链表的倒置。使用三个指针prevcurrnextTemp,逐步将节点的指针反转,最后将头结点的next指向新的头节点(原链表的尾节点)。
  4. printList函数:用于打印链表中的数据。
  5. freeList函数:释放链表所占用的内存,防止内存泄漏。
  6. main函数 :程序的主函数,先构建链表,打印原始链表,然后调用reverseList函数倒置链表,再打印倒置后的链表,最后释放链表内存。

5 在带头结点的单链表中删除最小值结点

以下是在带头结点的单链表中删除最小值结点的 C 语言算法实现:

cpp 复制代码
#include <stdio.h>
#include <stdlib.h>

// 定义链表节点结构
typedef struct Node {
    int data;
    struct Node *next;
} Node, *Linklist;

// 创建新节点
Node *createNode(int data) {
    Node *newNode = (Node *)malloc(sizeof(Node));
    if (newNode == NULL) {
        printf("内存分配失败\n");
        exit(1);
    }
    newNode->data = data;
    newNode->next = NULL;
    return newNode;
}

// 构建带头结点的单链表(从尾插入)
void buildList(Linklist &L) {
    L = (Linklist)malloc(sizeof(Node));
    if (L == NULL) {
        printf("内存分配失败\n");
        exit(1);
    }
    L->next = NULL;

    int n, data;
    printf("请输入链表节点个数:");
    scanf("%d", &n);

    for (int i = 0; i < n; i++) {
        printf("请输入第 %d 个节点的数据:", i + 1);
        scanf("%d", &data);
        Node *newNode = createNode(data);
        // 从尾插入
        Node *p = L;
        while (p->next != NULL) {
            p = p->next;
        }
        p->next = newNode;
    }
}

// 删除最小值结点
void deleteMin(Linklist &L) {
    if (L->next == NULL) {
        return; // 链表为空,无需操作
    }

    Node *p = L->next;    // 指向第一个数据节点
    Node *minNode = p;    // 初始认为第一个数据节点是最小值节点
    Node *prev = L;       // 指向最小值节点的前驱节点
    Node *prevMin = prev; // 初始前驱节点为头结点

    while (p != NULL) {
        if (p->data < minNode->data) {
            minNode = p;
            prevMin = prev;
        }
        prev = p;
        p = p->next;
    }

    // 删除最小值节点
    prevMin->next = minNode->next;
    free(minNode);
}

// 打印链表
void printList(Linklist L) {
    Node *p = L->next;
    while (p != NULL) {
        printf("%d ", p->data);
        p = p->next;
    }
    printf("\n");
}

// 释放链表内存
void freeList(Linklist L) {
    Node *p = L;
    while (p != NULL) {
        Node *temp = p;
        p = p->next;
        free(temp);
    }
}

int main() {
    Linklist L;
    buildList(L);

    printf("原始链表:");
    printList(L);

    deleteMin(L);

    printf("删除最小值节点后链表:");
    printList(L);

    freeList(L);

    return 0;
}

代码说明

  1. createNode函数:用于创建新的链表节点,为节点分配内存并初始化数据和指针。
  2. buildList函数:构建一个带头结点的单链表,通过从尾插入的方式添加节点,用户需要输入节点个数和每个节点的数据。
  3. deleteMin函数
    • 首先判断链表是否为空,若为空则直接返回。
    • 然后初始化指针,minNode指向第一个数据节点,认为其是最小值节点,prevMin指向头结点(最小值节点的前驱)。
    • 遍历链表,找到值最小的节点及其前驱节点。
    • 最后删除最小值节点,并释放其内存。
  4. printList函数:用于打印链表中的数据。
  5. freeList函数:释放链表所占用的内存,防止内存泄漏。
  6. main函数 :程序的主函数,先构建链表,打印原始链表,然后调用deleteMin函数删除最小值节点,再打印删除后的链表,最后释放链表内存。

6 求两个带头结点且元素递增有序的单链表 A 和 B 的交集 C

以下是求两个带头结点且元素递增有序的单链表 A 和 B 的交集 C 的 C 语言代码实现:

cpp 复制代码
#include <stdio.h>
#include <stdlib.h>

// 定义链表节点结构
typedef struct Node {
    int data;
    struct Node *next;
} Node, *LinkList;

// 创建新节点
Node *createNode(int data) {
    Node *newNode = (Node *)malloc(sizeof(Node));
    if (newNode == NULL) {
        printf("内存分配失败\n");
        exit(1);
    }
    newNode->data = data;
    newNode->next = NULL;
    return newNode;
}

// 构建带头结点的单链表(从尾插入,元素递增有序)
void buildList(LinkList &L) {
    L = (LinkList)malloc(sizeof(Node));
    if (L == NULL) {
        printf("内存分配失败\n");
        exit(1);
    }
    L->next = NULL;

    int n, data;
    printf("请输入链表节点个数:");
    scanf("%d", &n);

    for (int i = 0; i < n; i++) {
        printf("请输入第 %d 个节点的数据(需递增有序):", i + 1);
        scanf("%d", &data);
        Node *newNode = createNode(data);
        // 从尾插入
        Node *p = L;
        while (p->next != NULL) {
            p = p->next;
        }
        p->next = newNode;
    }
}

// 求两个递增有序单链表的交集
LinkList getIntersection(LinkList A, LinkList B) {
    // 创建交集链表C的头结点
    LinkList C = (LinkList)malloc(sizeof(Node));
    if (C == NULL) {
        printf("内存分配失败\n");
        exit(1);
    }
    C->next = NULL;

    Node *p = A->next; // 指向A的第一个数据节点
    Node *q = B->next; // 指向B的第一个数据节点
    Node *r = C;       // 指向C的尾节点,用于插入新节点

    while (p != NULL && q != NULL) {
        if (p->data == q->data) {
            // 找到共同元素,加入到C中
            Node *newNode = createNode(p->data);
            r->next = newNode;
            r = newNode;
            p = p->next;
            q = q->next;
        } else if (p->data < q->data) {
            // A中当前元素较小,A的指针后移
            p = p->next;
        } else {
            // B中当前元素较小,B的指针后移
            q = q->next;
        }
    }

    return C;
}

// 打印链表
void printList(LinkList L) {
    Node *p = L->next;
    while (p != NULL) {
        printf("%d ", p->data);
        p = p->next;
    }
    printf("\n");
}

// 释放链表内存
void freeList(LinkList L) {
    Node *p = L;
    while (p != NULL) {
        Node *temp = p;
        p = p->next;
        free(temp);
    }
}

int main() {
    LinkList A, B, C;

    printf("构建链表A:\n");
    buildList(A);

    printf("构建链表B:\n");
    buildList(B);

    printf("链表A:");
    printList(A);

    printf("链表B:");
    printList(B);

    C = getIntersection(A, B);

    printf("A和B的交集C:");
    printList(C);

    freeList(A);
    freeList(B);
    freeList(C);

    return 0;
}

代码说明

  1. createNode函数:用于创建新的链表节点,为节点分配内存并初始化数据和指针。
  2. buildList函数:构建一个带头结点的单链表,通过从尾插入的方式添加节点,用户需要输入节点个数和每个节点的数据(要求递增有序)。
  3. getIntersection函数
    • 首先创建交集链表 C 的头结点。
    • 分别用指针 p 和 q 指向链表 A 和 B 的第一个数据节点,用指针 r 指向 C 的尾节点(初始为头结点)。
    • 同时遍历 A 和 B:
      • 如果 p 和 q 所指节点数据相同,说明是交集元素,创建新节点加入 C,然后 p、q 都后移。
      • 如果 p 所指节点数据小于 q 所指节点数据,p 后移。
      • 否则 q 后移。
  4. printList函数:用于打印链表中的数据。
  5. freeList函数:释放链表所占用的内存,防止内存泄漏。
  6. main函数 :程序的主函数,先构建链表 A 和 B,打印它们,然后调用 getIntersection 函数求交集 C,打印 C,最后释放所有链表的内存。

7 头节点不能随便动

8 反转链表的自环错误

我写的错的代码:

cpp 复制代码
/**
 * Definition for singly-linked list.
 * struct ListNode {
 *     int val;
 *     struct ListNode *next;
 * };
 */
struct ListNode* reverseList(struct ListNode* head) {
    if (head == NULL || head -> next == NULL ){
        return head ;
    }
    struct ListNode *cur = head;
    while (cur != NULL && cur -> next != NULL ){
        struct ListNode *temp = cur -> next ;
        cur -> next = cur;
        cur = temp;
    }
    return head;
}

你的代码超出时间限制,是因为出现了循环引用(自环),导致链表无法正常遍历,陷入无限循环。

问题出在这行代码

cpp 复制代码
cur -> next = cur;

这行代码把当前节点的 next 指针指向了自身 ,比如原本链表是 1 -> 2 -> 3,执行后会变成 1 -> 1(同时 23 也被错误连接),后续遍历就会在这个自环里无限循环,永远无法结束。

正确的反转链表逻辑

反转链表需要三个指针prev(前一个节点)、cur(当前节点)、temp(临时保存下一个节点),核心是断开旧连接,建立新连接,同时逐个移动指针。

以下是修正后的代码:

cpp 复制代码
struct ListNode* reverseList(struct ListNode* head) {
    // 处理空链表或只有一个节点的情况
    if (head == NULL || head->next == NULL) {
        return head;
    }
    struct ListNode *prev = NULL;  // 前一个节点,初始为NULL(反转后最后一个节点的next是NULL)
    struct ListNode *cur = head;   // 当前节点,从head开始
    struct ListNode *temp;         // 临时保存下一个节点

    while (cur != NULL) {
        temp = cur->next;  // 先保存下一个节点,防止断开后找不到
        cur->next = prev;  // 反转:当前节点的next指向前一个节点
        prev = cur;        // prev移动到当前节点
        cur = temp;        // cur移动到下一个节点(之前保存的temp)
    }
    // 循环结束后,prev就是新的头节点(原链表的最后一个节点)
    return prev;
}

修正思路

  1. prev 记录前一个节点,初始为 NULL(因为反转后,原头节点的 next 要指向 NULL)。
  2. temp 临时保存 cur->next,因为反转时会修改 cur->next,如果不提前保存,后续就找不到下一个节点了。
  3. 每次循环做三件事:
    • 保存下一个节点到 temp
    • 反转当前节点的 next(指向 prev)。
    • prevcur 都后移一位(prevcurcurtemp)。
  4. 循环结束后,prev 就是新的头节点(原链表的最后一个节点),返回它即可。
相关推荐
-雷阵雨-2 小时前
数据结构——包装类&&泛型
java·开发语言·数据结构·intellij-idea
2401_841495642 小时前
【计算机视觉】图像去雾技术
人工智能·python·opencv·算法·计算机视觉·技术·图像去雾
河北北重机械9662 小时前
汽车安全性能测试与铸铁底座的重要性
人工智能·算法·机器学习·铸铁底座·铁底座装配·试验台基底座加工
gsfl2 小时前
redis常见数据结构及其编码方式
数据结构·redis
北京地铁1号线3 小时前
数据结构笔试选择题:题组2
算法
im_AMBER3 小时前
数据结构 01 线性表
数据结构·学习
_码力全开_3 小时前
Python从入门到实战 (14):工具落地:用 PyInstaller 打包 Python 脚本为可执行文件
开发语言·数据结构·python·个人开发
XCOSnTh3 小时前
XCOSnTh单片机的串口
c语言·单片机·嵌入式硬件·算法·xcosnth
Yunfeng Peng3 小时前
2- 十大排序算法(希尔排序、计数排序、桶排序)
java·算法·排序算法