1035-doubly-linked — Doubly-Linked List
Tutorial
The Problem
Doubly-linked lists support O(1) insertion and removal at any node when you already hold a reference to that node. This makes them ideal for LRU cache eviction (move a node to the front when accessed), editor cursor movement, and undo/redo history. The challenge in Rust is that each node needs a pointer to both its predecessor and successor, creating a cycle of shared ownership that cannot be expressed with simple Box<T>.
The standard safe solution uses Rc<RefCell<Node<T>>> — reference counting for shared ownership and interior mutability for the back-pointer updates.
🎯 Learning Outcomes
Rc<RefCell<Node<T>>> as the node link type for shared mutable ownershipRc<RefCell<_>> versus raw pointersArc<Mutex<_>> instead for thread-safe variantsCode Example
#![allow(clippy::all)]
// 1035: Doubly-Linked List — Rc<RefCell<Node>> Approach
// Safe doubly-linked list using reference counting + interior mutability
use std::cell::RefCell;
use std::fmt;
use std::rc::Rc;
type Link<T> = Option<Rc<RefCell<DNode<T>>>>;
struct DNode<T> {
value: T,
prev: Link<T>,
next: Link<T>,
}
struct DoublyLinkedList<T> {
head: Link<T>,
tail: Link<T>,
len: usize,
}
impl<T> DoublyLinkedList<T> {
fn new() -> Self {
DoublyLinkedList {
head: None,
tail: None,
len: 0,
}
}
fn push_back(&mut self, value: T) {
let new_node = Rc::new(RefCell::new(DNode {
value,
prev: self.tail.clone(),
next: None,
}));
match self.tail.take() {
Some(old_tail) => {
old_tail.borrow_mut().next = Some(new_node.clone());
}
None => {
self.head = Some(new_node.clone());
}
}
self.tail = Some(new_node);
self.len += 1;
}
fn push_front(&mut self, value: T) {
let new_node = Rc::new(RefCell::new(DNode {
value,
prev: None,
next: self.head.clone(),
}));
match self.head.take() {
Some(old_head) => {
old_head.borrow_mut().prev = Some(new_node.clone());
}
None => {
self.tail = Some(new_node.clone());
}
}
self.head = Some(new_node);
self.len += 1;
}
fn pop_front(&mut self) -> Option<T>
where
T: Default,
{
self.head.take().map(|old_head| {
let mut old_head_ref = old_head.borrow_mut();
match old_head_ref.next.take() {
Some(new_head) => {
new_head.borrow_mut().prev = None;
self.head = Some(new_head);
}
None => {
self.tail = None;
}
}
self.len -= 1;
std::mem::take(&mut old_head_ref.value)
})
}
fn pop_back(&mut self) -> Option<T>
where
T: Default,
{
self.tail.take().map(|old_tail| {
let mut old_tail_ref = old_tail.borrow_mut();
match old_tail_ref.prev.take() {
Some(new_tail) => {
new_tail.borrow_mut().next = None;
self.tail = Some(new_tail);
}
None => {
self.head = None;
}
}
self.len -= 1;
std::mem::take(&mut old_tail_ref.value)
})
}
fn to_vec(&self) -> Vec<T>
where
T: Clone,
{
let mut result = Vec::new();
let mut current = self.head.clone();
while let Some(node) = current {
result.push(node.borrow().value.clone());
current = node.borrow().next.clone();
}
result
}
fn len(&self) -> usize {
self.len
}
}
impl<T: fmt::Debug + Clone> fmt::Debug for DoublyLinkedList<T> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{:?}", self.to_vec())
}
}
fn basic_operations() {
let mut list: DoublyLinkedList<i32> = DoublyLinkedList::new();
list.push_back(1);
list.push_back(2);
list.push_back(3);
list.push_front(0);
assert_eq!(list.to_vec(), vec![0, 1, 2, 3]);
assert_eq!(list.len(), 4);
assert_eq!(list.pop_front(), Some(0));
assert_eq!(list.pop_back(), Some(3));
assert_eq!(list.to_vec(), vec![1, 2]);
}
fn bidirectional_traversal() {
let mut list: DoublyLinkedList<i32> = DoublyLinkedList::new();
for i in 1..=5 {
list.push_back(i);
}
// Forward traversal
let forward = list.to_vec();
assert_eq!(forward, vec![1, 2, 3, 4, 5]);
// Backward traversal
let mut backward = Vec::new();
let mut current = list.tail.clone();
while let Some(node) = current {
backward.push(node.borrow().value);
current = node.borrow().prev.clone();
}
assert_eq!(backward, vec![5, 4, 3, 2, 1]);
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_basic() {
basic_operations();
}
#[test]
fn test_bidirectional() {
bidirectional_traversal();
}
#[test]
fn test_empty() {
let mut list: DoublyLinkedList<i32> = DoublyLinkedList::new();
assert_eq!(list.pop_front(), None);
assert_eq!(list.pop_back(), None);
assert_eq!(list.len(), 0);
}
#[test]
fn test_single() {
let mut list: DoublyLinkedList<i32> = DoublyLinkedList::new();
list.push_back(42);
assert_eq!(list.pop_front(), Some(42));
assert!(list.head.is_none());
assert!(list.tail.is_none());
}
}Key Differences
Rc cycles leak unless Weak<T> is used for back-pointers.RefCell to mutate nodes through shared Rc references; OCaml uses mutable record fields directly.Arc<Mutex<_>> for thread-safe doubly-linked lists; OCaml uses Mutex explicitly.Rc<RefCell<_>> adds two heap allocations and reference counting per node; OCaml has one heap allocation per node.OCaml Approach
OCaml's mutable doubly-linked list uses ref for the pointers:
type 'a node = {
mutable value: 'a;
mutable prev: 'a node option;
mutable next: 'a node option;
}
OCaml's GC handles cycles — a doubly-linked list forms a reference cycle but the GC's cycle collector reclaims it. Rust's Rc<RefCell<_>> creates reference cycles that cause memory leaks unless you use Weak<T> for back-pointers.
Full Source
#![allow(clippy::all)]
// 1035: Doubly-Linked List — Rc<RefCell<Node>> Approach
// Safe doubly-linked list using reference counting + interior mutability
use std::cell::RefCell;
use std::fmt;
use std::rc::Rc;
type Link<T> = Option<Rc<RefCell<DNode<T>>>>;
struct DNode<T> {
value: T,
prev: Link<T>,
next: Link<T>,
}
struct DoublyLinkedList<T> {
head: Link<T>,
tail: Link<T>,
len: usize,
}
impl<T> DoublyLinkedList<T> {
fn new() -> Self {
DoublyLinkedList {
head: None,
tail: None,
len: 0,
}
}
fn push_back(&mut self, value: T) {
let new_node = Rc::new(RefCell::new(DNode {
value,
prev: self.tail.clone(),
next: None,
}));
match self.tail.take() {
Some(old_tail) => {
old_tail.borrow_mut().next = Some(new_node.clone());
}
None => {
self.head = Some(new_node.clone());
}
}
self.tail = Some(new_node);
self.len += 1;
}
fn push_front(&mut self, value: T) {
let new_node = Rc::new(RefCell::new(DNode {
value,
prev: None,
next: self.head.clone(),
}));
match self.head.take() {
Some(old_head) => {
old_head.borrow_mut().prev = Some(new_node.clone());
}
None => {
self.tail = Some(new_node.clone());
}
}
self.head = Some(new_node);
self.len += 1;
}
fn pop_front(&mut self) -> Option<T>
where
T: Default,
{
self.head.take().map(|old_head| {
let mut old_head_ref = old_head.borrow_mut();
match old_head_ref.next.take() {
Some(new_head) => {
new_head.borrow_mut().prev = None;
self.head = Some(new_head);
}
None => {
self.tail = None;
}
}
self.len -= 1;
std::mem::take(&mut old_head_ref.value)
})
}
fn pop_back(&mut self) -> Option<T>
where
T: Default,
{
self.tail.take().map(|old_tail| {
let mut old_tail_ref = old_tail.borrow_mut();
match old_tail_ref.prev.take() {
Some(new_tail) => {
new_tail.borrow_mut().next = None;
self.tail = Some(new_tail);
}
None => {
self.head = None;
}
}
self.len -= 1;
std::mem::take(&mut old_tail_ref.value)
})
}
fn to_vec(&self) -> Vec<T>
where
T: Clone,
{
let mut result = Vec::new();
let mut current = self.head.clone();
while let Some(node) = current {
result.push(node.borrow().value.clone());
current = node.borrow().next.clone();
}
result
}
fn len(&self) -> usize {
self.len
}
}
impl<T: fmt::Debug + Clone> fmt::Debug for DoublyLinkedList<T> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{:?}", self.to_vec())
}
}
fn basic_operations() {
let mut list: DoublyLinkedList<i32> = DoublyLinkedList::new();
list.push_back(1);
list.push_back(2);
list.push_back(3);
list.push_front(0);
assert_eq!(list.to_vec(), vec![0, 1, 2, 3]);
assert_eq!(list.len(), 4);
assert_eq!(list.pop_front(), Some(0));
assert_eq!(list.pop_back(), Some(3));
assert_eq!(list.to_vec(), vec![1, 2]);
}
fn bidirectional_traversal() {
let mut list: DoublyLinkedList<i32> = DoublyLinkedList::new();
for i in 1..=5 {
list.push_back(i);
}
// Forward traversal
let forward = list.to_vec();
assert_eq!(forward, vec![1, 2, 3, 4, 5]);
// Backward traversal
let mut backward = Vec::new();
let mut current = list.tail.clone();
while let Some(node) = current {
backward.push(node.borrow().value);
current = node.borrow().prev.clone();
}
assert_eq!(backward, vec![5, 4, 3, 2, 1]);
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_basic() {
basic_operations();
}
#[test]
fn test_bidirectional() {
bidirectional_traversal();
}
#[test]
fn test_empty() {
let mut list: DoublyLinkedList<i32> = DoublyLinkedList::new();
assert_eq!(list.pop_front(), None);
assert_eq!(list.pop_back(), None);
assert_eq!(list.len(), 0);
}
#[test]
fn test_single() {
let mut list: DoublyLinkedList<i32> = DoublyLinkedList::new();
list.push_back(42);
assert_eq!(list.pop_front(), Some(42));
assert!(list.head.is_none());
assert!(list.tail.is_none());
}
}#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_basic() {
basic_operations();
}
#[test]
fn test_bidirectional() {
bidirectional_traversal();
}
#[test]
fn test_empty() {
let mut list: DoublyLinkedList<i32> = DoublyLinkedList::new();
assert_eq!(list.pop_front(), None);
assert_eq!(list.pop_back(), None);
assert_eq!(list.len(), 0);
}
#[test]
fn test_single() {
let mut list: DoublyLinkedList<i32> = DoublyLinkedList::new();
list.push_back(42);
assert_eq!(list.pop_front(), Some(42));
assert!(list.head.is_none());
assert!(list.tail.is_none());
}
}
Deep Comparison
Doubly-Linked List — Comparison
Core Insight
Doubly-linked lists require bidirectional pointers — each node is shared by its neighbors. In Rust, this violates single-ownership rules, requiring Rc (shared ownership) + RefCell (interior mutability). OCaml either uses mutable records (imperative) or zippers (functional alternative).
OCaml Approach
mutable prev/next fields with option types{ left; focus; right } for functional bidirectional accessRust Approach
Rc<RefCell<Node<T>>> — reference-counted cells with runtime borrow checkingclone() on Rc creates shared referencesborrow() / borrow_mut() for access (panics if rules violated at runtime)std::collections::LinkedList exists but is rarely recommendedComparison Table
| Feature | OCaml (mutable) | Rust (Rc<RefCell>) |
|---|---|---|
| Shared ownership | GC handles it | Rc reference counting |
| Interior mutability | mutable keyword | RefCell runtime checks |
| Cycle handling | GC collects cycles | Must break manually |
| Borrow checking | None (runtime safe) | Runtime via RefCell |
| Ergonomics | Simple mutation | Verbose .borrow_mut() |
| Recommendation | Fine for imperative | Use Vec/VecDeque instead |
Exercises
Weak<RefCell<DNode<T>>> back-pointers instead of Rc for the prev link to prevent memory leaks in cyclic structures.HashMap<K, Rc<RefCell<DNode<(K, V)>>>> for O(1) node lookup.prev pointers.