线性表

线性表的两大经典实现:顺序表、链表

顺序表

(比如 C 里的数组 int arr[]

元素存在连续的内存中。

优点:随机访问快,arr[i] O(1)。

缺点:插入/删除慢(要挪动一堆数据),扩容麻烦。

链表

(比如单链表)

元素用节点存,每个节点带个 next 指针。

优点:插入/删除快(只改指针),灵活分配内存。

缺点:访问第 k 个元素必须从头数,O(n)。


📌 单链表操作总结(带头指针、可选尾指针)

操作是否需要遍历?说明
新增头结点❌ O(1)新节点 next = head; head = 新节点
删除头结点❌ O(1)head = head->next
新增尾结点(有尾指针)❌ O(1)tail->next = 新节点; tail = 新节点
新增尾结点(无尾指针)✅ O(n)得从头走到尾再接上
删除尾结点(有尾指针)✅ O(n)要找到“倒数第二个节点”,才能改 tail(尾节点)
删除尾结点(无尾指针)✅ O(n)同理,还得遍历
在中间插入节点(已知前驱)❌ O(1)p->next = 新节点; 新节点->next = 原next
在中间删除节点(已知前驱)❌ O(1)p->next = p->next->next
  • 新增一个头结点通常只需要在链表的开始处插入一个新节点,并将它的next指针指向原链表的头结点。这个过程并不需要遍历整个链表
  • 新增一个尾结点通常意味着需要找到链表的最后一个节点,并将新节点的next指针设置为null(或对应语言中的空值),同时更新原链表的最后一个节点的next指针以指向新节点但是,如果链表有一个指向尾部的指针(比如双向链表或某些特定设计的单链表),那么就不需要遍历整个链表来找到尾结点

❓“有尾指针了,倒数第二个节点是不是也能直接找到?”

👉 答案是:不能。

为什么呢?

  • 尾指针(tail):只存了“最后一个节点”的地址。

    head → [1] → [2] → [3] → [4] → NULL
                                 ↑
                               tail
    

📌 删除尾结点对比

链表类型能不能直接删尾?复杂度原因
单链表 (Singly Linked List)❌ 不行O(n)tail 只能找到最后一个,但找不到倒数第二个(前驱节点),必须从头遍历
双向链表 (Doubly Linked List)✅ 可以O(1)tail,且每个节点有 prev 指针,能一步找到倒数第二个节点
循环链表 (Circular Linked List)取决于单/双单向循环:O(n) 双向循环:O(1)循环只是头尾相连,不改变“是否能回退”的本质;单向还是要遍历,双向能一步删掉

📌 动态小图理解

单链表(单向 + 尾指针):

head → [1] → [2] → [3] → [4] → NULL
                               ↑
                             tail

要删 [4],必须遍历找到 [3]痛点:不能倒着走。

双向链表(有 prev 指针):

head ↔ [1] ↔ [2] ↔ [3] ↔ [4]
                        ↑
                      tail

要删 [4],直接 tail = tail.prev; tail.next = NULL; 一步到位,O(1)。

循环链表(头尾相连):

  • 单向循环:
head → [1] → [2] → [3] → [4] -
       ↑-----------------------|

要删 [4],还是得找到 [3] → O(n)。

双向循环:

head ↔ [1] ↔ [2] ↔ [3] ↔ [4]
 ↑-------------------------↓

要删 [4]tail = tail.prev; 就行 → O(1)。

🎯 总结口诀

👉 “单链表尾删必遍历,双向链表一箭毙,循环不改单双性。”