1 带头结点的单链表,头结点head
始终存在
当带头结点的单链表为空时,头结点的next
指针指向NULL
,也就是head->next == NULL
。
核心特征再明确
基于你给出的定义,带头结点单链表的空表状态有两个核心特征:
- 头结点必存在 :无论链表是否存储数据,
head
指针始终指向一个有效的头结点,不会为NULL
。 - 唯一判断标志 :空表的唯一判定条件是头结点的
next
指针指向NULL
,即head->next == NULL
。此时链表中除头结点外,无任何数据结点。
与无表头链表的关键区别
为了更清晰理解其设计优势,可与 "无表头结点的单链表" 做对比:
对比维度 | 带头结点的单链表(空表时) | 无表头结点的单链表(空表时) |
---|---|---|
头指针 head |
指向有效头结点(非 NULL) | 直接指向 NULL |
空表判断条件 | head->next == NULL |
head == NULL |
插入 / 删除首元素 | 操作统一,无需改 head |
需特殊处理,可能修改 head |
2 双向循环链表插入新结点

答案是 C。下面给你详细解释每一步的作用,保证通俗易懂~
在双向循环链表中,每个结点都有两个指针:prior
(指向前一个结点)和next
(指向后一个结点)。现在要在p
指向的结点前插入q
指向的新结点,需要处理好q
、p
以及p
原来前驱结点之间的指针关系,步骤如下:
步骤 1:q->next = p;
这一步是让q
的next
指针指向p
。因为q
要插入到p
的前面,插入后q
的下一个结点就是p
,所以先把q
的next
和p
连起来。
步骤 2:q->prior = p->prior;
这一步是让q
的prior
指针指向p
原来的前驱结点。这样q
就和p
原来的前驱结点建立了联系,为后续修改该前驱结点的next
指针做准备。
步骤 3:p->prior->next = q;
p->prior
是p
原来的前驱结点,现在要让这个前驱结点的next
指针指向q
。因为q
插入到了p
前面,所以原来p
的前驱结点的下一个结点就变成了q
,这样就把q
和p
原来的前驱结点连接好了。
步骤 4:p->prior = q;
最后,让p
的prior
指针指向q
。因为q
现在在p
的前面,所以p
的前一个结点就是q
,这样p
和q
也连接好了。
这样四步操作完成后,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
就是刚才的node
(p
原来的前驱)。现在要让node
的后继 指向q
(因为q
插入到了node
和p
之间,node
的下一个结点从p
变成了q
)。p->prior = q;
:最后让p
的前驱 指向q
(因为q
现在在p
前面,p
的前一个结点变成了q
)。
为什么顺序不能乱?
如果先动原有结点(比如先改 p->prior->next
或 p->prior
),会出现 **"找不到目标结点"** 的问题:
- 若先执行
p->prior->next = q;
,但此时q->prior
还没指向p->prior
,q
和node
没建立联系,这步操作逻辑不完整。 - 若先执行
p->prior = q;
,会直接丢失p
原来的前驱node
(因为p->prior
被改成q
了,后面再想找node
就找不到了)。
所以必须先把 q
自身的前驱、后继确定好 ,再去更新原有结点(node
和 p
)与 q
的连接,这样才不会 "断链"。
【可以先q->prior = p->prior;再q->next = p; 吗?】
可以的!
在双向循环链表中,q->prior = p->prior;
(让q
的前驱指向p
原来的前驱)和q->next = p;
(让q
的后继指向p
)这两步的顺序可以交换。
因为这两步都是在设置q
自身的两个指针 (prior
和next
),这两个操作之间没有依赖关系(既不需要先知道q
的next
,也不需要先知道q
的prior
,才能设置另一个)。
不管先设置q
的prior
,还是先设置q
的next
,只要最终q
的prior
指向p
原来的前驱、q
的next
指向p
,这部分就完成了。
后续的p->prior->next = q;
(让p
原来前驱的next
指向q
)和p->prior = q;
(让p
的prior
指向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;
}
代码说明
createNode
函数:用于创建新的链表节点,为节点分配内存并初始化数据和指针。buildList
函数:构建一个带头结点的单链表,通过从尾插入的方式添加节点,用户需要输入节点个数和每个节点的数据。reverseList
函数 :实现链表的倒置。使用三个指针prev
、curr
、nextTemp
,逐步将节点的指针反转,最后将头结点的next
指向新的头节点(原链表的尾节点)。printList
函数:用于打印链表中的数据。freeList
函数:释放链表所占用的内存,防止内存泄漏。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;
}
代码说明
createNode
函数:用于创建新的链表节点,为节点分配内存并初始化数据和指针。buildList
函数:构建一个带头结点的单链表,通过从尾插入的方式添加节点,用户需要输入节点个数和每个节点的数据。deleteMin
函数 :- 首先判断链表是否为空,若为空则直接返回。
- 然后初始化指针,
minNode
指向第一个数据节点,认为其是最小值节点,prevMin
指向头结点(最小值节点的前驱)。 - 遍历链表,找到值最小的节点及其前驱节点。
- 最后删除最小值节点,并释放其内存。
printList
函数:用于打印链表中的数据。freeList
函数:释放链表所占用的内存,防止内存泄漏。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;
}
代码说明
createNode
函数:用于创建新的链表节点,为节点分配内存并初始化数据和指针。buildList
函数:构建一个带头结点的单链表,通过从尾插入的方式添加节点,用户需要输入节点个数和每个节点的数据(要求递增有序)。getIntersection
函数 :- 首先创建交集链表 C 的头结点。
- 分别用指针 p 和 q 指向链表 A 和 B 的第一个数据节点,用指针 r 指向 C 的尾节点(初始为头结点)。
- 同时遍历 A 和 B:
- 如果 p 和 q 所指节点数据相同,说明是交集元素,创建新节点加入 C,然后 p、q 都后移。
- 如果 p 所指节点数据小于 q 所指节点数据,p 后移。
- 否则 q 后移。
printList
函数:用于打印链表中的数据。freeList
函数:释放链表所占用的内存,防止内存泄漏。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
(同时 2
和 3
也被错误连接),后续遍历就会在这个自环里无限循环,永远无法结束。
正确的反转链表逻辑
反转链表需要三个指针 :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;
}
修正思路
- 用
prev
记录前一个节点,初始为NULL
(因为反转后,原头节点的next
要指向NULL
)。 - 用
temp
临时保存cur->next
,因为反转时会修改cur->next
,如果不提前保存,后续就找不到下一个节点了。 - 每次循环做三件事:
- 保存下一个节点到
temp
。 - 反转当前节点的
next
(指向prev
)。 prev
和cur
都后移一位(prev
到cur
,cur
到temp
)。
- 保存下一个节点到
- 循环结束后,
prev
就是新的头节点(原链表的最后一个节点),返回它即可。