Skip to content
Commitwise
Menu
Coding Interview

Linked lists

Reverse, merge and walk linked lists with pointer moves, dummy heads and fast/slow pointers, with Reverse Linked List, Remove Nth Node From End and cycle detection in Swift, Kotlin, Java and TypeScript.

beginner

The core idea with one simple example.Step by step, with more examples and exercises.A cheat sheet: key APIs, gotchas and trade-offs.Internals, trade-offs and production pitfalls.

You’ll almost never write a linked list in an app: arrays are faster for nearly everything. Interviewers ask about them anyway, because linked-list problems test one skill very directly: changing pointers in the right order without losing anything. Get one assignment out of order and half the list is gone.

This tutorial covers the three techniques that solve most of these problems: rewiring pointers carefully, a dummy head, and fast and slow pointers.

New to some of these? Open what you need, or skip ahead.

Basics:What are classes and objects?

A class is a blueprint; an object (or instance) is one thing built from it. A User class says every user has a name and an email; User(name: "Ana", email: "ana@example.com") creates one specific user.

  • Properties (fields) are the data each object holds.
  • Methods are functions that belong to the class and work with that data.
  • Swift also has struct, Kotlin has data class and Java has record: shorter ways to define types that mostly hold data. In TypeScript, plain objects described by an interface or type often play that role.
Basics:What do null and nil mean?

null (Kotlin, Java, TypeScript, which also has undefined) and nil (Swift) mean “no value here”: a user without a middle name, a search with no result. Using a missing value as if it were there is the classic crash: NullPointerException on the JVM, “unexpectedly found nil” in Swift.

Swift and Kotlin mark types that may be empty with ? (String?), TypeScript with | null, and they make you handle the empty case. The Null Safety tutorial covers this in detail.

Basics:Same value, or same object?

Two variables can hold equal values (the same data) or the same object (one thing in memory, referred to twice):

  • Same data: == in Swift and Kotlin, equals() in Java.
  • Same object: === in Swift and Kotlin, == in Java.

In TypeScript, === compares numbers and strings by value but objects by identity, so “same data” for objects means comparing their fields. For data like numbers and strings you almost always want “same data”. For linked structures (nodes pointing to nodes) you usually want “same object”, because two different nodes can hold the same number.

Basics:What is recursion?

A recursive function calls itself on a smaller version of the problem, until it reaches a case small enough to answer directly (the base case):

countdown(3) → prints 3, calls countdown(2) → prints 2, calls countdown(1) → prints 1, calls countdown(0) → stops

Two rules keep it correct: always have a base case, and make sure every call gets closer to it. Each call waits on the call stack until the smaller call finishes, so very deep recursion (hundreds of thousands of levels) can run out of stack space.

Part Not started

Nodes and pointers

Think of a treasure hunt: each clue tells you where the next clue is. To find the fifth clue, you must follow the first four. A linked list works the same way:

  • each node holds a value and a reference to the next node,
  • the list is just a reference to the first node, the head,
  • the last node’s next is empty (nil / null).

Unlike an array, you can’t jump to position 5 directly (that’s O(n)), but you can insert or remove a node anywhere in O(1) if you already have the node before it: just change one next.

A singly linked list trades O(1) indexed access for O(1) insertion and removal at a known node, at the cost of poor memory locality and one pointer per element. In interviews the data structure is a vehicle for pointer manipulation under invariants: maintain prev/current/next correctly, use sentinels to remove head special cases, and use two pointers moving at different speeds to find middles, k-th from end, and cycles in O(1) extra space. The Detailed version adds the math behind cycle detection and production details like recursion depth and ARC deallocation of long chains.

final class ListNode {
    var value: Int
    var next: ListNode?
    init(_ value: Int, _ next: ListNode? = nil) { self.value = value; self.next = next }
}

func printList(_ head: ListNode?) {
    var node = head
    while let current = node {
        print(current.value, terminator: " ")
        node = current.next
    }
}

let head = ListNode(1, ListNode(2, ListNode(3)))   // 1 → 2 → 3
printList(head)                                      // 1 2 3
class ListNode(var value: Int, var next: ListNode? = null)

fun printList(head: ListNode?) {
    var node = head
    while (node != null) {
        print("${node.value} ")
        node = node.next
    }
}

val head = ListNode(1, ListNode(2, ListNode(3)))   // 1 → 2 → 3
printList(head)                                      // 1 2 3
class ListNode {
    int value;
    ListNode next;
    ListNode(int value, ListNode next) { this.value = value; this.next = next; }
    ListNode(int value) { this(value, null); }
}

static void printList(ListNode head) {
    for (ListNode node = head; node != null; node = node.next) {
        System.out.print(node.value + " ");
    }
}

ListNode head = new ListNode(1, new ListNode(2, new ListNode(3)));   // 1 → 2 → 3
printList(head);                                                       // 1 2 3
class ListNode {
  constructor(public value: number, public next: ListNode | null = null) {}
}

function printList(head: ListNode | null) {
  const values: number[] = [];
  for (let node = head; node !== null; node = node.next) values.push(node.value);
  console.log(values.join(" "));
}

const head = new ListNode(1, new ListNode(2, new ListNode(3)));   // 1 → 2 → 3
printList(head);                                                   // 1 2 3

Reversing a list in place

The classic first question: turn 1 → 2 → 3 → 4 → 5 into 5 → 4 → 3 → 2 → 1 without a second list. Walk the list once with three references: prev (the part already reversed), current, and next, saved before you change current.next:

func reverseList(_ head: ListNode?) -> ListNode? {
    var prev: ListNode? = nil
    var current = head
    while let node = current {
        let next = node.next      // 1. save the rest of the list
        node.next = prev          // 2. turn this node around
        prev = node               // 3. the reversed part now starts here
        current = next            // 4. continue with the saved rest
    }
    return prev
}
fun reverseList(head: ListNode?): ListNode? {
    var prev: ListNode? = null
    var current = head
    while (current != null) {
        val next = current.next   // 1. save the rest of the list
        current.next = prev       // 2. turn this node around
        prev = current            // 3. the reversed part now starts here
        current = next            // 4. continue with the saved rest
    }
    return prev
}
ListNode reverseList(ListNode head) {
    ListNode prev = null;
    ListNode current = head;
    while (current != null) {
        ListNode next = current.next;   // 1. save the rest of the list
        current.next = prev;            // 2. turn this node around
        prev = current;                 // 3. the reversed part now starts here
        current = next;                 // 4. continue with the saved rest
    }
    return prev;
}
function reverseList(head: ListNode | null): ListNode | null {
  let prev: ListNode | null = null;
  let current = head;
  while (current !== null) {
    const next = current.next;   // 1. save the rest of the list
    current.next = prev;         // 2. turn this node around
    prev = current;              // 3. the reversed part now starts here
    current = next;              // 4. continue with the saved rest
  }
  return prev;
}

O(n) time, O(1) space.

Losing the rest of the list

The order of those four lines is the whole problem. Here’s a version with one step in the wrong place:

Spot the bugA reverse that returns one node

Tap the line with the bug.

func reverseList(_ head: ListNode?) -> ListNode? {
    var prev: ListNode? = nil
    var current = head
    while let node = current {
        node.next = prev
        prev = node
        current = node.next
    }
    return prev
}
fun reverseList(head: ListNode?): ListNode? {
    var prev: ListNode? = null
    var current = head
    while (current != null) {
        current.next = prev
        prev = current
        current = current.next
    }
    return prev
}
ListNode reverseList(ListNode head) {
    ListNode prev = null;
    ListNode current = head;
    while (current != null) {
        current.next = prev;
        prev = current;
        current = current.next;
    }
    return prev;
}
function reverseList(head: ListNode | null): ListNode | null {
  let prev: ListNode | null = null;
  let current = head;
  while (current !== null) {
    current.next = prev;
    prev = current;
    current = current.next;
  }
  return prev;
}

  • Read before you overwrite: save next, rewire, advance. Iterative reversal is O(n) time and O(1) space; the recursive one uses O(n) stack and overflows on long lists.
var prev: ListNode? = nil, current = head
while let node = current { let next = node.next; node.next = prev; prev = node; current = next }
var prev: ListNode? = null; var current = head
while (current != null) { val next = current.next; current.next = prev; prev = current; current = next }
ListNode prev = null, current = head;
while (current != null) { ListNode next = current.next; current.next = prev; prev = current; current = next; }
let prev: ListNode | null = null, current = head;
while (current !== null) { const next: ListNode | null = current.next; current.next = prev; prev = current; current = next; }

Check this part

  1. What is the cost of reaching the 1,000th node of a linked list?

  2. When reversing a list, what must you do before setting current.next = prev?

  3. A recursive reverse works in tests. What happens on a 100,000-node list?

Each node holds a value and a next reference. Reversing in place is four moves in a fixed order: save next, turn the node around, advance prev, continue with the saved next. Rewire before saving and the rest of the list is lost.

Part Not started

The dummy head

Many list problems have a special case for the first node: removing it, or building a new list whose head you don’t know yet. A dummy node placed before the real head removes the special case: every real node, including the first, now has a node before it.

func mergeTwoLists(_ a: ListNode?, _ b: ListNode?) -> ListNode? {
    let dummy = ListNode(0)
    var tail = dummy
    var a = a, b = b
    while let x = a, let y = b {
        if x.value <= y.value { tail.next = x; a = x.next } else { tail.next = y; b = y.next }
        tail = tail.next!
    }
    tail.next = a ?? b              // attach whatever is left
    return dummy.next               // the real head
}
fun mergeTwoLists(a: ListNode?, b: ListNode?): ListNode? {
    val dummy = ListNode(0)
    var tail = dummy
    var x = a
    var y = b
    while (x != null && y != null) {
        if (x.value <= y.value) { tail.next = x; x = x.next } else { tail.next = y; y = y.next }
        tail = tail.next!!
    }
    tail.next = x ?: y              // attach whatever is left
    return dummy.next               // the real head
}
ListNode mergeTwoLists(ListNode a, ListNode b) {
    ListNode dummy = new ListNode(0);
    ListNode tail = dummy;
    while (a != null && b != null) {
        if (a.value <= b.value) { tail.next = a; a = a.next; } else { tail.next = b; b = b.next; }
        tail = tail.next;
    }
    tail.next = (a != null) ? a : b;   // attach whatever is left
    return dummy.next;                 // the real head
}
function mergeTwoLists(a: ListNode | null, b: ListNode | null): ListNode | null {
  const dummy = new ListNode(0);
  let tail = dummy;
  while (a !== null && b !== null) {
    if (a.value <= b.value) { tail.next = a; a = a.next; } else { tail.next = b; b = b.next; }
    tail = tail.next!;
  }
  tail.next = a ?? b;              // attach whatever is left
  return dummy.next;               // the real head
}

Without the dummy, you’d need an if before the loop to pick the first node. With it, the loop treats every node the same.

Practice: drawing before coding

Linked-list bugs are almost always “a pointer was changed before it was read” or “the head case was forgotten”. A routine that prevents both:

  1. Draw three or four nodes as boxes with arrows, plus nil at the end.
  2. Name every pointer you’ll need (prev, current, next, dummy, slow, fast) and mark where each starts.
  3. For each line of the loop, update the drawing. If an arrow is about to disappear and nothing else points to its target, save it first.
  4. Check the edge cases: an empty list, one node, two nodes, and the operation on the first and last node. A dummy head usually makes the first-node case disappear.
Exercisemedium

Remove Nth Node From End of List

Given the head of a linked list, remove the n-th node from the end and return the head. Do it in one pass.

1 → 2 → 3 → 4 → 5, n = 2   →   1 → 2 → 3 → 5
1, n = 1                   →   (empty)
1 → 2, n = 2               →   2
›

If one pointer is n nodes ahead of another, when the first reaches the end, where is the second?

›

Start both pointers at a dummy node before the head (so removing the head works the same as any other node). Move fast n + 1 steps ahead, then move both until fast is empty. slow is now just before the node to remove.

SolutionTry it yourself firstTime O(n)Space O(1)
func removeNthFromEnd(_ head: ListNode?, _ n: Int) -> ListNode? {
    let dummy = ListNode(0, head)
    var fast: ListNode? = dummy
    var slow: ListNode = dummy
    for _ in 0...n { fast = fast?.next }          // n + 1 steps ahead
    while fast != nil {
        fast = fast?.next
        slow = slow.next!
    }
    slow.next = slow.next?.next                    // skip the n-th from the end
    return dummy.next
}
fun removeNthFromEnd(head: ListNode?, n: Int): ListNode? {
    val dummy = ListNode(0, head)
    var fast: ListNode? = dummy
    var slow: ListNode = dummy
    repeat(n + 1) { fast = fast?.next }            // n + 1 steps ahead
    while (fast != null) {
        fast = fast?.next
        slow = slow.next!!
    }
    slow.next = slow.next?.next                    // skip the n-th from the end
    return dummy.next
}
ListNode removeNthFromEnd(ListNode head, int n) {
    ListNode dummy = new ListNode(0, head);
    ListNode fast = dummy, slow = dummy;
    for (int i = 0; i <= n; i++) fast = fast.next; // n + 1 steps ahead
    while (fast != null) {
        fast = fast.next;
        slow = slow.next;
    }
    slow.next = slow.next.next;                    // skip the n-th from the end
    return dummy.next;
}
function removeNthFromEnd(head: ListNode | null, n: number): ListNode | null {
  const dummy = new ListNode(0, head);
  let fast: ListNode | null = dummy;
  let slow: ListNode = dummy;
  for (let i = 0; i <= n; i++) fast = fast!.next;    // n + 1 steps ahead
  while (fast !== null) {
    fast = fast.next;
    slow = slow.next!;
  }
  slow.next = slow.next?.next ?? null;               // skip the n-th from the end
  return dummy.next;
}

The gap between the pointers stays n + 1 nodes, so when fast steps off the end, slow is the node just before the one to delete. The dummy makes the 1 → 2, n = 2 case (removing the real head) work without an if: slow stays on the dummy and skips the head. The problem guarantees n is valid; in production code you’d check the list is long enough.

Production notes

  • Recursion depth: recursive list algorithms (recursive reverse, recursive merge) use O(n) stack, and 100,000 nodes can overflow the JVM’s default thread stack (and JavaScript’s limit of roughly ten thousand frames). Prefer iteration.
  • Deallocation: in Swift, releasing the head of a long list releases the next node inside the first node’s deinit, and so on, recursively (the puzzle below).
  • Identity vs equality: algorithms on nodes compare references (=== in Swift, Kotlin and TypeScript, == in Java). Value-based equals on nodes is wrong for cycle checks and can recurse forever (the bug below).
  • Real libraries: java.util.LinkedList is rarely the right choice (no locality, O(n) get(i)); ArrayDeque beats it as a queue. Linked structures earn their place in LRU caches, intrusive lists in allocators, and lock-free queues.

Dropping a long list

Predict the output

A linked list of 1,000,000 nodes is built, then the only reference to its head is dropped. What happens?

var head: ListNode? = ListNode(0)
var tail = head!
for i in 1..<1_000_000 {
    tail.next = ListNode(i)
    tail = tail.next!
}
head = nil                              // the last reference to node 0
var head: ListNode? = ListNode(0)
var tail = head!!
for (i in 1 until 1_000_000) {
    tail.next = ListNode(i)
    tail = tail.next!!
}
head = null                             // the last reference to node 0
ListNode head = new ListNode(0);
ListNode tail = head;
for (int i = 1; i < 1_000_000; i++) {
    tail.next = new ListNode(i);
    tail = tail.next;
}
head = null;                            // the last reference to node 0
let head: ListNode | null = new ListNode(0);
let tail = head;
for (let i = 1; i < 1_000_000; i++) {
  tail.next = new ListNode(i);
  tail = tail.next;
}
head = null;                            // the last reference to node 0
TechniqueUse forKey move
prev / current / nextreverse (whole, sublist, k-group)save next before rewiring
dummy headmerge, remove, partition, build a new listreturn dummy.next
fast / slowmiddle, cycle detection and start, palindrome checkfast moves 2, slow 1; compare identity
gap of nn-th from endmove fast n + 1 ahead from a dummy
hash map node → nodecopy with random pointersmap old nodes to new

Complexities: traversal O(n), insert/remove at a known node O(1), index access O(n); all techniques above O(1) extra space. Gotchas: rewiring before saving, first/last node cases, comparing values instead of identity, recursion depth on long lists, recursive deallocation of long Swift chains.

Check this part

  1. What problem does a dummy head node solve?

  2. What does mergeTwoLists return at the end?

  3. The only reference to the head of a 1,000,000-node list is dropped. What happens?

  4. When is a linked list the right choice in production code?

A dummy node before the real head gives every node, including the first, a node before it, so merging, removing and building lists need no first-node special case. Return dummy.next.

Part Not started

Fast and slow pointers

Two pointers start at the head; slow moves one step at a time, fast moves two. When fast reaches the end, slow is in the middle:

Predict the output

The list is 1 → 2 → 3 → 4 → 5 → 6. What gets printed?

func middle(_ head: ListNode?) -> ListNode? {
    var slow = head, fast = head
    while fast != nil && fast?.next != nil {
        slow = slow?.next
        fast = fast?.next?.next
    }
    return slow
}
print(middle(list)!.value)
fun middle(head: ListNode?): ListNode? {
    var slow = head
    var fast = head
    while (fast?.next != null) {
        slow = slow?.next
        fast = fast.next?.next
    }
    return slow
}
println(middle(list)!!.value)
ListNode middle(ListNode head) {
    ListNode slow = head, fast = head;
    while (fast != null && fast.next != null) {
        slow = slow.next;
        fast = fast.next.next;
    }
    return slow;
}
System.out.println(middle(list).value);
function middle(head: ListNode | null): ListNode | null {
  let slow = head;
  let fast = head;
  while (fast !== null && fast.next !== null) {
    slow = slow!.next;
    fast = fast.next.next;
  }
  return slow;
}
console.log(middle(list)!.value);

Cycle detection and where the cycle starts

Floyd’s algorithm: if the list has a cycle, the fast pointer eventually laps the slow one inside the loop; if not, fast reaches the end. O(n) time, O(1) space, versus O(n) space for a set of visited nodes.

To find the start of the cycle: let the distance from head to the cycle start be a, the cycle length c, and the meeting point b steps into the cycle. Slow walked a + b, fast walked 2(a + b), and fast is a whole number of laps ahead: a + b = k·c, so a = k·c − b. A pointer starting at the head and one starting at the meeting point, both moving one step at a time, therefore meet exactly at the cycle start. That derivation is what interviewers want to hear (the exercise below).

Comparing nodes the wrong way

Spot the bugCycle detection that's wrong or never ends

Tap the line with the bug.

extension ListNode: Equatable {
    static func == (a: ListNode, b: ListNode) -> Bool { a.value == b.value }
}

func hasCycle(_ head: ListNode?) -> Bool {
    var slow = head, fast = head
    while let f = fast, let next = f.next {
        slow = slow?.next
        fast = next.next
        if let s = slow, let f2 = fast, s == f2 { return true }
    }
    return false
}
data class ListNode(var value: Int, var next: ListNode? = null)

fun hasCycle(head: ListNode?): Boolean {
    var slow = head
    var fast = head
    while (fast?.next != null) {
        slow = slow?.next
        fast = fast.next?.next
        if (slow == fast) return true
    }
    return false
}
// ListNode overrides equals() to compare value and next
boolean hasCycle(ListNode head) {
    ListNode slow = head, fast = head;
    while (fast != null && fast.next != null) {
        slow = slow.next;
        fast = fast.next.next;
        if (slow.equals(fast)) return true;
    }
    return false;
}
function hasCycle(head: ListNode | null): boolean {
  let slow = head;
  let fast = head;
  while (fast !== null && fast.next !== null) {
    slow = slow!.next;
    fast = fast.next.next;
    if (slow?.value === fast?.value) return true;
  }
  return false;
}

Exercisemedium

Linked List Cycle II

Given the head of a linked list, return the node where the cycle begins, or empty if there’s no cycle. Use O(1) extra memory, and explain why your method works.

3 → 2 → 0 → -4
    ↑________|        → returns the node with value 2
›

First detect the cycle with fast and slow pointers. The meeting point isn’t the start, but it’s at a useful distance from it.

›

From the meeting point, move one pointer back to the head. Advance both one step at a time: they meet at the cycle’s start (see the derivation above).

SolutionTry it yourself firstTime O(n)Space O(1)
func detectCycle(_ head: ListNode?) -> ListNode? {
    var slow = head, fast = head
    while let f = fast, let afterF = f.next {
        slow = slow!.next
        fast = afterF.next
        if slow === fast {                          // inside the cycle
            var entry = head
            while entry !== slow {                  // both move one step at a time
                entry = entry!.next
                slow = slow!.next
            }
            return entry                            // the start of the cycle
        }
    }
    return nil
}
fun detectCycle(head: ListNode?): ListNode? {
    var slow = head
    var fast = head
    while (fast?.next != null) {
        slow = slow!!.next
        fast = fast.next!!.next
        if (slow === fast) {                        // inside the cycle
            var entry = head
            while (entry !== slow) {                // both move one step at a time
                entry = entry!!.next
                slow = slow!!.next
            }
            return entry                            // the start of the cycle
        }
    }
    return null
}
ListNode detectCycle(ListNode head) {
    ListNode slow = head, fast = head;
    while (fast != null && fast.next != null) {
        slow = slow.next;
        fast = fast.next.next;
        if (slow == fast) {                         // inside the cycle
            ListNode entry = head;
            while (entry != slow) {                 // both move one step at a time
                entry = entry.next;
                slow = slow.next;
            }
            return entry;                           // the start of the cycle
        }
    }
    return null;
}
function detectCycle(head: ListNode | null): ListNode | null {
  let slow = head;
  let fast = head;
  while (fast !== null && fast.next !== null) {
    slow = slow!.next;
    fast = fast.next.next;
    if (slow === fast) {                          // inside the cycle
      let entry = head;
      while (entry !== slow) {                    // both move one step at a time
        entry = entry!.next;
        slow = slow!.next;
      }
      return entry;                               // the start of the cycle
    }
  }
  return null;
}

Why it works: with a steps from the head to the cycle start, a cycle of length c, and the first meeting b steps into the cycle, slow has walked a + b and fast 2(a + b). Fast’s extra distance is whole laps, so a + b is a multiple of c, which means a ≡ c − b (mod c): walking a steps from the meeting point lands exactly on the cycle start, the same place a pointer reaches after a steps from the head. Follow-ups: the cycle length (keep one pointer still and count steps until the other returns), and the same trick on arrays where values are indices (Find the Duplicate Number).

Deep dives

Deep diveLRU cache: a hash map plus a doubly linked listInterview favorite

“Design an LRU cache with O(1) get and put” is one of the most common interview questions, and it’s a linked-list question in disguise. A hash map finds a key’s node in O(1); a doubly linked list keeps the nodes in order of use, so moving a node to the front and removing the last one are O(1) too.

get moves the node to the front; put inserts at the front and, when full, removes the tail (and its map entry). Dummy head and tail nodes remove the edge cases. In Java, LinkedHashMap with accessOrder = true and an overridden removeEldestEntry is the same structure in a few lines.

  • Floyd: fast laps slow inside a cycle, O(1) space. For the start, move one pointer back to the head and advance both one step: with a steps to the start and the meeting b into a cycle of length c, a + b is a multiple of c.
  • Compare identity: value equality gives false positives, and structural equals (Kotlin data classes) recurses around the cycle.

Check this part

  1. Fast moves two steps and slow moves one. Where is slow when fast reaches the end?

  2. The list 1 → 2 → 3 → 4 → 5 → 6 has two middles. Which one does the usual loop return?

  3. Why must cycle detection compare nodes with ===?Why must cycle detection compare nodes with ===, and not use a data class node?Why must cycle detection compare nodes with == rather than equals?Why must cycle detection compare the nodes themselves (slow === fast) rather than their values?

  4. After fast and slow meet in a cycle, how do you find where the cycle starts?

Slow moves one step, fast moves two: when fast reaches the end, slow is in the middle, and if there's a cycle, fast laps slow inside it. Compare nodes by identity, never by value.

Key takeaways

  • A linked list is nodes with a value and a next reference; access by position is O(n), insert/remove at a known node is O(1).
  • When rewiring, save next before you overwrite it; draw boxes and arrows for three nodes.
  • A dummy head removes first-node special cases; return dummy.next.
  • Fast and slow pointers find the middle and the n-th from the end in one pass.
  • Floyd’s algorithm detects cycles in O(1) space; the distance argument finds the cycle start.
  • Compare node identity, never value equality; never use data classes or structural equals for nodes.
  • Prefer iteration over recursion on long lists, and beware recursive deallocation of long Swift chains.
  • In production, arrays and ArrayDeque usually beat linked lists; linked structures shine in LRU caches and intrusive lists.

Finished this page?Nice work, this page is complete!

Mark it as done to track your progress. It's saved in this browser, no account needed.

Its cards are now in your Bites review ⚡
Esc

Type to search the whole site.