当前位置: 首页 > news >正文

数据结构 02 线性表

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

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

核心特征再明确

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

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

与无表头链表的关键区别

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

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

双向循环链表插入新结点

答案是 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 插入到了 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->priorq 和 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自身的两个指针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 语言实现带头结点单链表倒置的代码:

#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 语言算法实现:

#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 语言代码实现:

#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 反转链表的自环错误

我写的错的代码:

/*** 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;
}

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

问题出在这行代码

cur -> next = cur;

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

正确的反转链表逻辑

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

以下是修正后的代码:

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)。
    • prev 和 cur 都后移一位(prev 到 curcur 到 temp)。
  4. 循环结束后,prev 就是新的头节点(原链表的最后一个节点),返回它即可。
http://www.dtcms.com/a/419398.html

相关文章:

  • 建设工商联网站的意义湟源县网站建设
  • 浙江网站建设技术公司淘宝客商品推广网站建设
  • 【HarmonyOS】鸿蒙应用实现微信分享-最新版
  • 房地产项目网站建设方案做外贸的网站简称为什么网站
  • Vue 3 开发的 HLS 视频流播放组件+异常处理
  • 前端核心框架vue之(路由核心案例篇3/5)
  • vue中不同的watch方法的坑
  • 网站首页排版设计广州网络公关公司
  • 批量重命名技巧:使用PowerShell一键整理图片文件命名规范
  • 手机版网站怎么做的企业解决方案架构师
  • 网站企业备案改个人备案专业微网站制作
  • 新天力科技以创新驱动发展,铸就食品包装容器行业领军者
  • crew AI笔记[7] - flow特性示例
  • 广州制作网站公司网站开发收税
  • 二阶可降阶微分方程的求解方法总结
  • 纯静态企业网站模板免费下载手机app编程
  • Redis在高并发场景中的核心优势
  • 教育网站 网页赏析网络营销推广的优缺点
  • 金溪县建设局网站品牌网站怎么建立
  • 中国气候政策不确定性数据(2000-2022)
  • 大发快三网站自做青海省城乡建设厅网站
  • 800G DR8与其他800G光模块的对比分析
  • 第四部分:VTK常用类详解(第100章 vtkHandleWidget句柄控件类)
  • Kafka 和 RabbitMQ 使用:消息队列的强大工具
  • 网站注册信息网站的建设有什么好处
  • 物理层-传输介质
  • npm 包构建与发布
  • 第四部分:VTK常用类详解(第99章 vtkBorderWidget边框控件类)
  • 如何播放 M3U8 格式的视频
  • 视频推拉流EasyDSS如何运用无人机直播技术高效排查焚烧烟火?