Acwing827. 双链表:深入解析与高效实现指南

Acwing827. 双链表:深入解析与高效实现指南

一、双链表基础概念解析

双链表(Doubly Linked List)是一种线性数据结构,由一组节点通过指针连接而成。与单链表相比,双链表的每个节点不仅包含指向下一个节点的指针(next),还包含指向前一个节点的指针(prev)。这种双向连接特性使得双链表在插入、删除操作时具有更高的灵活性。

1.1 节点结构定义

双链表的核心是节点(Node)的定义。每个节点通常包含三个部分:

  • 数据域(data):存储节点的实际数据。
  • 前驱指针(prev):指向当前节点的前一个节点。
  • 后继指针(next):指向当前节点的后一个节点。
  1. typedef struct DNode {
  2. int data;
  3. struct DNode *prev;
  4. struct DNode *next;
  5. } DNode;

1.2 双链表与单链表的对比

特性 单链表 双链表
指针数量 1个(next) 2个(prev, next)
访问前驱节点 需从头遍历 直接通过prev指针访问
插入/删除 需维护next指针 需同时维护prev和next指针
空间开销 较小 较大(多一个指针)
适用场景 只需单向遍历的场景 需要双向遍历或频繁插入删除

二、Acwing827题目核心要求

Acwing827题目要求实现一个双链表,并支持以下操作:

  1. 在链表头部插入节点(L.addFirst(x)
  2. 在链表尾部插入节点(L.addLast(x)
  3. 在指定节点后插入新节点(L.insert(p, x)
  4. 删除指定节点(L.erase(p)
  5. 查找第k个节点(L.getK(k)
  6. 查找前驱节点(L.getPrev(p)
  7. 查找后继节点(L.getNext(p)

三、双链表核心操作实现详解

3.1 初始化双链表

初始化时需创建头节点(head)和尾节点(tail),并设置它们的指针关系。

  1. DNode *initList() {
  2. DNode *head = (DNode *)malloc(sizeof(DNode));
  3. DNode *tail = (DNode *)malloc(sizeof(DNode));
  4. head->next = tail;
  5. tail->prev = head;
  6. head->prev = NULL;
  7. tail->next = NULL;
  8. return head;
  9. }

3.2 头部插入节点

在头部插入节点需修改头节点的next指针和新节点的prev指针。

  1. void addFirst(DNode *head, int x) {
  2. DNode *node = (DNode *)malloc(sizeof(DNode));
  3. node->data = x;
  4. node->next = head->next;
  5. node->prev = head;
  6. head->next->prev = node;
  7. head->next = node;
  8. }

3.3 尾部插入节点

尾部插入需修改尾节点的前驱指针和新节点的next指针。

  1. void addLast(DNode *head, int x) {
  2. DNode *tail = head;
  3. while (tail->next != NULL) {
  4. tail = tail->next;
  5. }
  6. DNode *node = (DNode *)malloc(sizeof(DNode));
  7. node->data = x;
  8. node->prev = tail;
  9. node->next = NULL;
  10. tail->next = node;
  11. }

优化:可维护一个尾指针(tail),避免每次遍历到链表尾部。

3.4 指定节点后插入

在节点p后插入新节点需修改pnext指针、新节点的prevnext指针,以及p后继节点的prev指针。

  1. void insertAfter(DNode *p, int x) {
  2. if (p == NULL) return;
  3. DNode *node = (DNode *)malloc(sizeof(DNode));
  4. node->data = x;
  5. node->next = p->next;
  6. node->prev = p;
  7. if (p->next != NULL) {
  8. p->next->prev = node;
  9. }
  10. p->next = node;
  11. }

3.5 删除指定节点

删除节点p需修改p前驱节点的next指针和p后继节点的prev指针。

  1. void erase(DNode *head, DNode *p) {
  2. if (p == NULL || p == head) return;
  3. p->prev->next = p->next;
  4. if (p->next != NULL) {
  5. p->next->prev = p->prev;
  6. }
  7. free(p);
  8. }

四、双链表应用场景与优化技巧

4.1 典型应用场景

  1. LRU缓存:双链表可高效实现最近最少使用算法,头部为最新访问节点,尾部为最久未访问节点。
  2. 文本编辑器:支持光标前后移动和插入删除操作。
  3. 图算法:如拓扑排序中维护入度为0的节点。

4.2 性能优化技巧

  1. 虚拟头尾节点:避免处理空链表或边界条件的特殊逻辑。
  2. 迭代器模式:封装节点指针,提供安全的遍历接口。
  3. 内存池:预分配节点内存,减少频繁malloc/free的开销。

五、Acwing827题目实现示例

  1. #include <stdio.h>
  2. #include <stdlib.h>
  3. typedef struct DNode {
  4. int data;
  5. struct DNode *prev;
  6. struct DNode *next;
  7. } DNode;
  8. DNode *initList() {
  9. DNode *head = (DNode *)malloc(sizeof(DNode));
  10. DNode *tail = (DNode *)malloc(sizeof(DNode));
  11. head->next = tail;
  12. tail->prev = head;
  13. head->prev = NULL;
  14. tail->next = NULL;
  15. return head;
  16. }
  17. void addFirst(DNode *head, int x) {
  18. DNode *node = (DNode *)malloc(sizeof(DNode));
  19. node->data = x;
  20. node->next = head->next;
  21. node->prev = head;
  22. head->next->prev = node;
  23. head->next = node;
  24. }
  25. void addLast(DNode *head, int x) {
  26. DNode *tail = head;
  27. while (tail->next != NULL) {
  28. tail = tail->next;
  29. }
  30. DNode *node = (DNode *)malloc(sizeof(DNode));
  31. node->data = x;
  32. node->prev = tail;
  33. node->next = NULL;
  34. tail->next = node;
  35. }
  36. void insertAfter(DNode *p, int x) {
  37. if (p == NULL) return;
  38. DNode *node = (DNode *)malloc(sizeof(DNode));
  39. node->data = x;
  40. node->next = p->next;
  41. node->prev = p;
  42. if (p->next != NULL) {
  43. p->next->prev = node;
  44. }
  45. p->next = node;
  46. }
  47. void erase(DNode *head, DNode *p) {
  48. if (p == NULL || p == head) return;
  49. p->prev->next = p->next;
  50. if (p->next != NULL) {
  51. p->next->prev = p->prev;
  52. }
  53. free(p);
  54. }
  55. int main() {
  56. DNode *L = initList();
  57. addFirst(L, 1);
  58. addFirst(L, 2);
  59. addLast(L, 3);
  60. DNode *p = L->next; // 第一个节点(2)
  61. insertAfter(p, 4); // 在2后插入4
  62. erase(L, p); // 删除节点2
  63. // 遍历链表
  64. DNode *cur = L->next;
  65. while (cur->next != NULL) {
  66. printf("%d ", cur->data);
  67. cur = cur->next;
  68. }
  69. return 0;
  70. }

六、总结与建议

双链表通过维护双向指针,在插入、删除操作上比单链表更高效,但需付出额外的空间开销。在实际开发中,建议:

  1. 优先使用虚拟头尾节点简化边界条件处理。
  2. 对于频繁插入删除的场景,双链表是更优选择。
  3. 结合具体业务场景,选择是否需要实现完整的双链表功能。

通过掌握Acwing827题目中的双链表实现,开发者能够深入理解双向链表的核心机制,为解决更复杂的算法问题打下坚实基础。