在大屏或电脑上体验更佳——动画和操作区域更宽敞。
输入

双向链表

双向链表的节点同时保存 next 和 prev,链表保存 head 与 tail。它支持反向遍历;已持有目标节点时,可用 O(1) 将其移除,不必再找前驱。

代价是额外内存与指针维护。中间插入要写新节点的两个指针,再写邻居各一个指针,共四次。漏写一侧可能让正向和反向遍历看到不同的链。

双向链表如何工作

每个节点像两端都有连接器的车厢。已找到车厢后,让前后邻居互连,就能把它移出。

在 pred 与 succ 之间插入时,依次写 newNode.prev = pred、newNode.next = succ、pred.next = newNode、succ.prev = newNode。动画分两拍:新节点先指向两侧,邻居再指回新节点。

删除内部节点时,node.prev、node.next 已给出邻居。执行 node.prev.next = node.next 和 node.next.prev = node.prev 即可,时间 O(1)。但按值找到节点仍需 O(n)。单链表若只持有目标节点,还需知道前驱;双向链表用额外指针保存了这项信息。

search 从 head 沿 next 前进,search_back 从 tail 沿 prev 后退。比较规则相同,遍历方向相反。

容量滑块 4–10、默认 7,对应画布大小和代码的 max。它不是双向链表的固定容量要求。

逐步演示

从空表开始:

  1. 头插 5:5.prev、5.next 都是 null,head、tail 同时指向 5。
  2. 头插 8:先 8.next = 5、8.prev = null,再 5.prev = 8,head 改为 8。得到 8 ⇄ 5。
  3. 尾插 3:tail 直接定位 5,无需遍历。写 3.prev = 5、3.next = null,再 5.next = 3,tail 改为 3。得到 8 ⇄ 5 ⇄ 3。
  4. 索引 1 插入 7:curr 到索引 0 的前驱 8。写 7.prev = 8、7.next = 5,再 8.next = 7、5.prev = 7。得到 8 ⇄ 7 ⇄ 5 ⇄ 3。
  5. 正向搜索 7:索引 0 的 8 ≠ 7,索引 1 的 7 = 7,返回 1。
  6. 反向搜索 5:从尾部索引 3 比较 3 ≠ 5,沿 prev 到索引 2,5 = 5,返回 2。
  7. 删除 7:正向搜索让 curr 位于目标 7,而非前驱。其 prev 为 8、next 为 5,写 8.next = 5、5.prev = 8,移出 7。结果 8 ⇄ 5 ⇄ 3。

画布上方显示 next,下方显示 prev。待插节点在指针写入完成前留在上方;删除时游标指向目标,两条旁路箭头同时越过它。

时间与空间复杂度

操作成本原因
索引访问O(n)从一端沿指针遍历
头插O(1)常数次写入
尾插O(1)已保存 tail
索引 k 插入O(k)本课正向定位前驱,再接入
删除已持有节点O(1)prev、next 给出两侧
按值删除O(n)查找 O(n),移除 O(1)
正向或反向搜索O(n)逐节点比较
空间O(n)每节点一个值、两个指针

本课相较只保存 head 的单链表,增加了反向链接与 tail。反向链接支持逆向遍历和已知节点删除;尾插的 O(1) 则来自维护 tail,单链表也可单独增加尾指针。

边界情况

  • 空表插入:唯一节点成为 head 和 tail,两侧为空。
  • 删除头部:node.prev 为空,改写 head = node.next;若新头存在,再设 head.prev = null。
  • 删除尾部:改写 tail = node.prev;若新尾存在,再设 tail.next = null。
  • 删除唯一节点:两侧都为空,head、tail 一起清空。
  • 值不存在:正向或反向走到末尾,删除返回 False,搜索返回 -1,不抛异常。
  • 插入索引越过末尾:展示代码缺少完整边界检查,会走到空引用;可视化停止并说明。k = 0 调用 insert_head,k = size 调用 insert_tail。

常见错误

  • 只更新一个方向。 写 pred.next = newNode 却漏掉 succ.prev = newNode,正向可能正常,反向会跳过新节点。
  • 忘记端点引用。 删除头尾后必须重设 head、tail;唯一节点要同时处理两者。
  • 已有节点还遍历找前驱。 prev 已保存前驱,重复遍历丢失了双向链接的优势。
  • 默认使用双向链表。 若只从单端压入、弹出和正向遍历,额外指针可能没有收益。根据实际操作选择单链表或数组。