Overview
Searching in a linked list means following pointers rather than indexing by position. This makes the baseline search sequential—Θ(n) comparisons in the worst case—yet linked lists remain valuable when inserts/deletes dominate and stable references to nodes are required. This note catalogs practical search patterns for singly and doubly linked lists, shows how to handle k-th, predecessor, and remove-by-value tasks, and explains when arrays or other indexed structures are preferable.
Note
Throughout,
headis the first node (or a sentinel before it), nodes storevalueand pointers (next, and optionallyprev), andNILmarks the end unless a sentinel is used.
Motivation
Linked lists excel when:
-
Items are frequently inserted/removed mid-structure with
O(1)pointer rewiring given a node handle. -
Elements must remain at stable addresses (e.g., referenced elsewhere) without reallocation.
-
Batches are small enough that
Θ(n)scans are acceptable.
They are poor for random access and binary search (no indexing); many search problems are better served by arrays, hash tables, or balanced trees depending on the need for order and operations.
Definition and Formalism
A singly linked list node:
struct Node { value: T, next: Node* }
A doubly linked list node:
struct Node { value: T, next: Node*, prev: Node* }
Lists may use sentinels (dummy head/tail nodes) to simplify edge cases. A search over values uses a predicate P(x) (e.g., x == key).
Baseline invariants
-
Sequential scan maintains: nodes before
curhave been examined and do not satisfyP. -
If using a predecessor pointer, maintain
prev.next == curin singly lists.
Sequential membership search (singly)
function FIND(head, key):
cur = head
while cur ≠ NIL:
if cur.value == key: return cur
cur = cur.next
return NILFind with predecessor (for deletion by value, singly)
function FIND_WITH_PREV(head, key):
prev = NIL
cur = head
while cur ≠ NIL and cur.value ≠ key:
prev = cur
cur = cur.next
return (prev, cur) // cur may be NIL if not foundRemoving (first) match:
function REMOVE_BY_VALUE(head_ref, key):
(prev, cur) = FIND_WITH_PREV(head_ref.value, key)
if cur == NIL: return false
if prev == NIL: // removing head
head_ref.value = cur.next
else:
prev.next = cur.next
free(cur); return trueDoubly linked removal (faster once node known)
Given a node handle x, you can unlink in O(1):
function UNLINK_DLL(x):
if x.prev ≠ NIL: x.prev.next = x.next
if x.next ≠ NIL: x.next.prev = x.prev
free(x)Properties and Relationships
-
No indexing: access by position
krequiresΘ(k)traversal. -
Stable references: nodes keep addresses as long as they remain linked; good for external handles/iterators.
-
Cache behavior: pointer-chasing is cache-unfriendly compared to arrays’ contiguous storage.
-
Order awareness: if the list is known to be sorted, early termination is possible when
cur.value > keyunder a strict order.
Tip
If searches predominate, consider storing an auxiliary index (hash map from key → node*) or switching to a structure designed for fast lookup.
Implementation or Practical Context
Common search tasks
1) Find first/last occurrence
-
First: standard forward scan returns the first match.
-
Last:
-
Singly: scan entire list remembering the last seen match (still
Θ(n)). -
Doubly: scan from tail backward for the last match (
Θ(n)but often fewer steps with a good starting point).
-
2) Find k-th element (by index)
function KTH(head, k): // 0-based
cur = head
while cur ≠ NIL and k > 0:
cur = cur.next
k = k - 1
return cur // NIL if out-of-boundsStill Θ(k). For frequent random indexing, use an array or dynamic array (see Dynamic Arrays).
3) Find k-th from the end (two pointers)
Maintain a gap of k nodes between fast and slow.
function KTH_FROM_END(head, k):
slow = head; fast = head
for i in 1..k:
if fast == NIL: return NIL
fast = fast.next
while fast ≠ NIL:
slow = slow.next
fast = fast.next
return slow4) Search with move-to-front (self-organizing lists)
Heuristic for skewed workloads: after a successful search, move the found node to the head to reduce future search cost on hot keys. Good when a small set of keys dominate.
function FIND_MTF(head_ref, key):
prev = NIL; cur = head_ref.value
while cur ≠ NIL and cur.value ≠ key:
prev = cur; cur = cur.next
if cur ≠ NIL and prev ≠ NIL:
prev.next = cur.next
cur.next = head_ref.value
head_ref.value = cur
return cur5) Cycle detection before/while searching
Use Floyd’s tortoise and hare:
function HAS_CYCLE(head):
slow = head; fast = head
while fast ≠ NIL and fast.next ≠ NIL:
slow = slow.next
fast = fast.next.next
if slow == fast: return true
return falseIf your search might traverse arbitrary input, guard against cycles to avoid infinite loops.
Sorted list searches
If the list is sorted by value with a strict comparator:
-
Early exit: stop when
cur.value > key. -
Insert position: track predecessor where
prev.value < key ≤ cur.value. This is stillΘ(n)but avoids post-search passes. -
Duplicates: to find the first occurrence, keep walking left via
prevon a DLL or track the last==seen in SLL.
Sentinels and error-prone edges
Using sentinel nodes (head for SLL, head+tail for DLL) simplifies:
-
Removal of the first/last element (no special-case nulling).
-
Uniform loop bodies without
NILchecks on each step. It doesn’t change asymptotics but reduces bugs.
Remove by value (first match) patterns
-
SLL: you must know the predecessor to rewire
prev.next. -
DLL: you can unlink with only a pointer to the node due to
prev.
When to choose arrays (or others) instead
-
Frequent random access or binary search → Dynamic Arrays or static arrays.
-
Frequent lookups by key without order → Hash Tables.
-
Ordered set/map with logarithmic search → balanced trees (e.g., Red–Black Tree, AVL Tree).
-
Ordered list with occasional mid-inserts but faster search → consider skip lists (layered indexes) or a vector + gap buffer depending on edits.
Tip
If you must stick with a list but need faster search for hot keys, maintain a side index (hash from key → node*). Update the index on insert/delete for
O(1)expected membership checks.
Performance notes
-
Time: sequential search is
Θ(n). Move-to-front can yield good amortized behavior under locality. -
Space: SLL stores one pointer per node; DLL stores two (higher overhead but
O(1)unlink by handle). -
Cache: lists suffer from pointer chasing; small arrays often outperform lists even when asymptotics match.
-
Concurrency: mutation while searching needs synchronization; iterators can be invalidated by unlink operations unless the API defines safe semantics.
Common Misunderstandings
Warning
Binary search on a list. Not feasible in
O(log n)—jumping to the middle isΘ(n). Any binary-search-like method becomesΘ(n)just to move pointers.
Warning
Forgetting predecessor in SLL deletion. You cannot unlink
curinO(1)withoutprev(unless you overwritecur.valuewithcur.next.valueand bypasscur.next, which fails for the tail and violates invariants).
Warning
Cycle-unsafe traversals. Lists constructed from user input or after complex merges may contain cycles; guard long scans with cycle checks when needed.
Warning
Assuming DLL always faster. Doubly links cost memory and write traffic; if you never traverse backward or unlink by handle, SLL may be preferable.
Summary
Searching in linked lists is fundamentally sequential: you walk pointers until you find a match, track predecessors for safe rewiring, and lean on fast/slow techniques for tasks like k-th-from-end or cycle detection. For sorted lists, you gain early exit and clean insertion points but not better asymptotics. If searches dominate, switch structures (arrays, hash tables, trees) or add a side index. Keep implementations robust with sentinels, clear invariants, and careful handling of deletion and cycles.