Interview Prep

Coding Interview Questions

The coding questions that come up most often in Go interviews — implementing core data structures from scratch, concurrency patterns built on goroutines and channels, and classic algorithm problems solved the idiomatic Go way. Every solution below was compiled and run against a real Go 1.26 toolchain, and every "Open in Go Playground" link was created through the real share API and verified to return the exact code shown.

Jump to questions ↓

Coding Questions

Pick a category, then work through the problems — each has a full problem statement, a complete runnable solution, and a link to run it yourself in the Go Playground.

1. Implement a singly linked list

Build a singly linked list supporting insertion at the head and tail, deletion by value, and forward traversal for printing.

package main

import "fmt"

type Node struct {
	val  int
	next *Node
}

type List struct {
	head *Node
	// tracking tail separately makes InsertTail O(1); without it we'd have
	// to walk the whole list on every append
	tail *Node
}

func (l *List) InsertHead(v int) {
	n := &Node{val: v, next: l.head}
	l.head = n
	// first node ever inserted is both head and tail
	if l.tail == nil {
		l.tail = n
	}
}

func (l *List) InsertTail(v int) {
	n := &Node{val: v}
	// empty list: head and tail both need to point at the new node,
	// there's no existing tail to link from
	if l.tail == nil {
		l.head = n
		l.tail = n
		return
	}
	l.tail.next = n
	l.tail = n
}

func (l *List) Delete(v int) bool {
	// prev starts nil so deleting the head is just "no prev to relink"
	var prev *Node
	cur := l.head
	for cur != nil {
		if cur.val == v {
			if prev == nil {
				// deleting the head: no predecessor to relink, just advance head
				l.head = cur.next
			} else {
				prev.next = cur.next
			}
			// deleting the tail: tail must move back to the predecessor,
			// otherwise tail would dangle on a node no longer reachable from head
			if cur == l.tail {
				l.tail = prev
			}
			return true
		}
		prev = cur
		cur = cur.next
	}
	return false
}

func (l *List) String() string {
	s := ""
	for cur := l.head; cur != nil; cur = cur.next {
		if s != "" {
			s += " -> "
		}
		s += fmt.Sprintf("%d", cur.val)
	}
	return s
}

func main() {
	l := &List{}
	l.InsertTail(1)
	l.InsertTail(2)
	l.InsertTail(3)
	l.InsertHead(0)
	fmt.Println("After inserts:", l.String())

	l.Delete(2)
	fmt.Println("After deleting 2:", l.String())

	l.Delete(0)
	fmt.Println("After deleting head 0:", l.String())

	l.InsertTail(4)
	fmt.Println("After inserting tail 4:", l.String())
}
After inserts: 0 -> 1 -> 2 -> 3 After deleting 2: 0 -> 1 -> 3 After deleting head 0: 1 -> 3 After inserting tail 4: 1 -> 3 -> 4

2. Reverse a singly linked list

Reverse the direction of every next pointer in a singly linked list using the standard three-pointer iterative technique, in a single O(n) pass.

package main

import "fmt"

type Node struct {
	val  int
	next *Node
}

func build(vals []int) *Node {
	// keep a tail pointer so appending stays O(1) instead of walking the
	// whole list to find the end on every insert
	var head, tail *Node
	for _, v := range vals {
		n := &Node{val: v}
		if head == nil {
			head = n
			tail = n
		} else {
			tail.next = n
			tail = n
		}
	}
	return head
}

func reverse(head *Node) *Node {
	// prev trails cur by one step; once cur passes a node, prev is the
	// only link back to it, so nothing is lost even though we're about
	// to overwrite that node's forward pointer
	var prev *Node
	cur := head
	for cur != nil {
		// stash next before we clobber cur.next, or we'd lose the rest of the list
		next := cur.next
		cur.next = prev
		prev = cur
		cur = next
	}
	// when cur becomes nil, prev is sitting on the old tail, i.e. the new head
	return prev
}

func print(head *Node) string {
	s := ""
	for cur := head; cur != nil; cur = cur.next {
		if s != "" {
			s += " -> "
		}
		s += fmt.Sprintf("%d", cur.val)
	}
	return s
}

func main() {
	head := build([]int{1, 2, 3, 4, 5})
	fmt.Println("Original:", print(head))

	reversed := reverse(head)
	fmt.Println("Reversed:", print(reversed))

	// a one-node list is its own reverse; exercises the loop running exactly once
	single := build([]int{42})
	fmt.Println("Single node reversed:", print(reverse(single)))
}
Original: 1 -> 2 -> 3 -> 4 -> 5 Reversed: 5 -> 4 -> 3 -> 2 -> 1 Single node reversed: 42

3. Detect a cycle in a linked list

Use Floyd's tortoise-and-hare algorithm — a slow pointer and a fast pointer — to detect whether a linked list contains a cycle, without extra memory.

package main

import "fmt"

type Node struct {
	val  int
	next *Node
}

func hasCycle(head *Node) bool {
	// Floyd's tortoise and hare: fast moves twice as fast as slow. If there's
	// a cycle, fast eventually laps slow inside the loop and they collide; if
	// the list is acyclic, fast simply runs off the end first
	slow, fast := head, head
	// checking fast and fast.next (not just fast) lets fast.next.next below
	// run safely without a nil dereference
	for fast != nil && fast.next != nil {
		slow = slow.next
		fast = fast.next.next
		if slow == fast {
			return true
		}
	}
	return false
}

func main() {
	// Build a clean list: 1 -> 2 -> 3 -> 4 -> nil
	n4 := &Node{val: 4}
	n3 := &Node{val: 3, next: n4}
	n2 := &Node{val: 2, next: n3}
	n1 := &Node{val: 1, next: n2}
	fmt.Println("List without cycle, hasCycle:", hasCycle(n1))

	// Introduce a cycle: n4 -> n2 (loop back)
	n4.next = n2
	fmt.Println("List with cycle (4 -> 2), hasCycle:", hasCycle(n1))

	// Single node pointing to itself: a cycle of length 1, so slow and fast
	// collide on the very first step of the loop
	self := &Node{val: 99}
	self.next = self
	fmt.Println("Single self-looping node, hasCycle:", hasCycle(self))

	// Empty list: head is nil, so the loop condition fails immediately
	// rather than needing a special-case check
	fmt.Println("Empty list, hasCycle:", hasCycle(nil))
}
List without cycle, hasCycle: false List with cycle (4 -> 2), hasCycle: true Single self-looping node, hasCycle: true Empty list, hasCycle: false

4. Find the middle node of a linked list in one pass

Find the middle node of a singly linked list in a single traversal using two pointers, one advancing twice as fast as the other.

package main

import "fmt"

type Node struct {
	val  int
	next *Node
}

func build(vals []int) *Node {
	// tail pointer avoids re-walking the list to find the append point
	var head, tail *Node
	for _, v := range vals {
		n := &Node{val: v}
		if head == nil {
			head = n
			tail = n
		} else {
			tail.next = n
			tail = n
		}
	}
	return head
}

func middle(head *Node) *Node {
	// fast covers two nodes for every one slow covers, so when fast runs
	// out of list, slow has covered exactly half the distance -- one pass,
	// no need to count the length first
	slow, fast := head, head
	for fast != nil && fast.next != nil {
		slow = slow.next
		fast = fast.next.next
	}
	// for even-length lists fast runs out one step early, leaving slow on
	// the second (upper) of the two middle nodes
	return slow
}

func main() {
	odd := build([]int{1, 2, 3, 4, 5})
	fmt.Println("Odd length [1..5], middle:", middle(odd).val)

	even := build([]int{1, 2, 3, 4, 5, 6})
	fmt.Println("Even length [1..6], middle:", middle(even).val)

	single := build([]int{7})
	fmt.Println("Single node [7], middle:", middle(single).val)

	two := build([]int{10, 20})
	fmt.Println("Two nodes [10,20], middle:", middle(two).val)
}
Odd length [1..5], middle: 3 Even length [1..6], middle: 4 Single node [7], middle: 7 Two nodes [10,20], middle: 20

5. Implement a stack (push, pop, peek, isEmpty)

Implement a LIFO stack backed by a Go slice, with Push, Pop, Peek, and IsEmpty operations, returning an error when popping or peeking an empty stack.

package main

import (
	"errors"
	"fmt"
)

// a slice already grows from one end in amortized O(1), which is exactly
// the shape a stack needs -- no separate node/pointer bookkeeping required
type Stack struct {
	items []int
}

func (s *Stack) Push(v int) {
	// the end of the slice is the top of the stack
	s.items = append(s.items, v)
}

func (s *Stack) Pop() (int, error) {
	// return an error instead of panicking so callers can decide how to
	// handle "nothing to pop" rather than crashing the program
	if s.IsEmpty() {
		return 0, errors.New("stack is empty")
	}
	n := len(s.items) - 1
	v := s.items[n]
	// re-slicing to drop the last element reuses the backing array --
	// no allocation, the old last element just becomes unreachable
	s.items = s.items[:n]
	return v, nil
}

func (s *Stack) Peek() (int, error) {
	if s.IsEmpty() {
		return 0, errors.New("stack is empty")
	}
	return s.items[len(s.items)-1], nil
}

func (s *Stack) IsEmpty() bool {
	return len(s.items) == 0
}

func main() {
	s := &Stack{}
	s.Push(1)
	s.Push(2)
	s.Push(3)

	top, _ := s.Peek()
	fmt.Println("Peek:", top)

	v, _ := s.Pop()
	fmt.Println("Pop:", v)

	v, _ = s.Pop()
	fmt.Println("Pop:", v)

	fmt.Println("IsEmpty:", s.IsEmpty())

	v, _ = s.Pop()
	fmt.Println("Pop:", v)

	fmt.Println("IsEmpty:", s.IsEmpty())

	_, err := s.Pop()
	fmt.Println("Pop on empty, error:", err)
}
Peek: 3 Pop: 3 Pop: 2 IsEmpty: false Pop: 1 IsEmpty: true Pop on empty, error: stack is empty

6. Implement a queue (enqueue, dequeue, front, isEmpty)

Implement a FIFO queue backed by a Go slice, with Enqueue, Dequeue, Front, and IsEmpty operations, returning an error when dequeuing from an empty queue.

package main

import (
	"errors"
	"fmt"
)

type Queue struct {
	items []int
}

func (q *Queue) Enqueue(v int) {
	// new items always join at the back; combined with Dequeue reading
	// from index 0, this is what gives FIFO order (vs. a stack's LIFO)
	q.items = append(q.items, v)
}

func (q *Queue) Dequeue() (int, error) {
	if q.IsEmpty() {
		return 0, errors.New("queue is empty")
	}
	v := q.items[0]
	// re-slicing off the front is simple and correct, but note it just
	// advances the slice's start pointer -- the backing array still holds
	// the dequeued elements until the whole array is garbage collected, so
	// a long-lived, high-churn queue would want a ring buffer instead
	// (see the circular buffer example)
	q.items = q.items[1:]
	return v, nil
}

func (q *Queue) Front() (int, error) {
	if q.IsEmpty() {
		return 0, errors.New("queue is empty")
	}
	return q.items[0], nil
}

func (q *Queue) IsEmpty() bool {
	return len(q.items) == 0
}

func main() {
	q := &Queue{}
	q.Enqueue(1)
	q.Enqueue(2)
	q.Enqueue(3)

	front, _ := q.Front()
	fmt.Println("Front:", front)

	v, _ := q.Dequeue()
	fmt.Println("Dequeue:", v)

	v, _ = q.Dequeue()
	fmt.Println("Dequeue:", v)

	fmt.Println("IsEmpty:", q.IsEmpty())

	v, _ = q.Dequeue()
	fmt.Println("Dequeue:", v)

	fmt.Println("IsEmpty:", q.IsEmpty())

	_, err := q.Dequeue()
	fmt.Println("Dequeue on empty, error:", err)
}
Front: 1 Dequeue: 1 Dequeue: 2 IsEmpty: false Dequeue: 3 IsEmpty: true Dequeue on empty, error: queue is empty

7. Implement a stack using two queues

Implement a LIFO stack using only two FIFO queues as the underlying storage, making Push do the extra work of re-ordering elements so Pop always removes the most recently pushed item.

package main

import "fmt"

// StackTwoQueues implements a LIFO stack using two FIFO queues.
type StackTwoQueues struct {
	q1 []int
	q2 []int
}

// dequeue pops the front element by reslicing past index 0 (no shifting
// or copying of the remaining elements).
func dequeue(q *[]int) int {
	v := (*q)[0]
	*q = (*q)[1:]
	return v
}

// Push makes the new element the front of q1 by rotating existing
// elements through q2, keeping the most recently pushed item first.
func (s *StackTwoQueues) Push(v int) {
	s.q2 = append(s.q2, v)
	for len(s.q1) > 0 {
		s.q2 = append(s.q2, dequeue(&s.q1))
	}
	s.q1, s.q2 = s.q2, s.q1
}

// Pop is O(1) because all the reordering work happened in Push: q1 is
// always kept with the most-recently-pushed item at the front.
func (s *StackTwoQueues) Pop() int {
	return dequeue(&s.q1)
}

func (s *StackTwoQueues) IsEmpty() bool {
	return len(s.q1) == 0
}

func main() {
	s := &StackTwoQueues{}
	s.Push(1)
	s.Push(2)
	s.Push(3)

	fmt.Println("Pop:", s.Pop())
	fmt.Println("Pop:", s.Pop())

	s.Push(4)
	fmt.Println("Pop:", s.Pop())
	fmt.Println("Pop:", s.Pop())

	fmt.Println("IsEmpty:", s.IsEmpty())
}
Pop: 3 Pop: 2 Pop: 4 Pop: 1 IsEmpty: true

8. Implement a queue using two stacks

Implement a FIFO queue using only two LIFO stacks: an input stack for enqueuing and an output stack that lazily reverses elements so Dequeue always removes the oldest item.

package main

import "fmt"

// QueueTwoStacks implements a FIFO queue using two LIFO stacks.
type QueueTwoStacks struct {
	in  []int
	out []int
}

// Enqueue is always O(1): new items just pile onto in, in reverse-of-
// dequeue order, and get flipped into out lazily by shift.
func (q *QueueTwoStacks) Enqueue(v int) {
	q.in = append(q.in, v)
}

// shift moves all elements from in to out (reversing order) only when
// out is empty, so dequeue returns them in original insertion order.
func (q *QueueTwoStacks) shift() {
	if len(q.out) == 0 {
		for len(q.in) > 0 {
			n := len(q.in) - 1
			q.out = append(q.out, q.in[n])
			q.in = q.in[:n]
		}
	}
}

// Dequeue only pays the O(n) shift cost once per batch of enqueues;
// each element is moved from in to out exactly once over its lifetime,
// so the amortized cost per operation is still O(1).
func (q *QueueTwoStacks) Dequeue() int {
	q.shift()
	n := len(q.out) - 1
	v := q.out[n]
	q.out = q.out[:n]
	return v
}

func (q *QueueTwoStacks) IsEmpty() bool {
	return len(q.in) == 0 && len(q.out) == 0
}

func main() {
	q := &QueueTwoStacks{}
	q.Enqueue(1)
	q.Enqueue(2)
	q.Enqueue(3)

	fmt.Println("Dequeue:", q.Dequeue())
	fmt.Println("Dequeue:", q.Dequeue())

	q.Enqueue(4)
	fmt.Println("Dequeue:", q.Dequeue())
	fmt.Println("Dequeue:", q.Dequeue())

	fmt.Println("IsEmpty:", q.IsEmpty())
}
Dequeue: 1 Dequeue: 2 Dequeue: 3 Dequeue: 4 IsEmpty: true

9. Implement a doubly linked list

Build a doubly linked list with PushFront, PushBack, and O(1) Remove of an arbitrary node, supporting traversal in both directions via prev and next pointers.

package main

import "fmt"

type DNode struct {
	val  int
	prev *DNode
	next *DNode
}

type DoublyList struct {
	head *DNode
	tail *DNode
}

// PushBack wires n.prev up front (from the current tail) so the two
// pointer updates below only ever have to look one direction: forward
// from the old tail, or straight into head/tail if the list was empty.
func (l *DoublyList) PushBack(v int) *DNode {
	n := &DNode{val: v, prev: l.tail}
	if l.tail != nil {
		l.tail.next = n
	} else {
		// list was empty: n is both the first and last node
		l.head = n
	}
	l.tail = n
	return n
}

// PushFront is PushBack's mirror image: n.next is set from the old
// head instead of n.prev from the old tail.
func (l *DoublyList) PushFront(v int) *DNode {
	n := &DNode{val: v, next: l.head}
	if l.head != nil {
		l.head.prev = n
	} else {
		// list was empty: n is both the first and last node
		l.tail = n
	}
	l.head = n
	return n
}

// Remove is O(1) given a node pointer, unlike a singly linked list where
// you'd need to walk from head to find the predecessor first. Each side
// is patched independently: if n has no prev/next, it was the head/tail,
// so the list's head/tail pointer moves instead of a neighbor's link.
func (l *DoublyList) Remove(n *DNode) {
	if n.prev != nil {
		n.prev.next = n.next
	} else {
		l.head = n.next
	}
	if n.next != nil {
		n.next.prev = n.prev
	} else {
		l.tail = n.prev
	}
}

func (l *DoublyList) Forward() string {
	s := ""
	for cur := l.head; cur != nil; cur = cur.next {
		if s != "" {
			s += " <-> "
		}
		s += fmt.Sprintf("%d", cur.val)
	}
	return s
}

// Backward walks via prev pointers instead of next - the whole reason
// to pay for a second pointer per node is to support this in O(n) instead
// of needing to reverse or re-walk from head each time.
func (l *DoublyList) Backward() string {
	s := ""
	for cur := l.tail; cur != nil; cur = cur.prev {
		if s != "" {
			s += " <-> "
		}
		s += fmt.Sprintf("%d", cur.val)
	}
	return s
}

func main() {
	l := &DoublyList{}
	l.PushBack(2)
	l.PushBack(3)
	mid := l.PushFront(1)
	l.PushBack(4)
	fmt.Println("Forward: ", l.Forward())
	fmt.Println("Backward:", l.Backward())

	l.Remove(mid)
	fmt.Println("After removing 1:")
	fmt.Println("Forward: ", l.Forward())
	fmt.Println("Backward:", l.Backward())
}
Forward: 1 <-> 2 <-> 3 <-> 4 Backward: 4 <-> 3 <-> 2 <-> 1 After removing 1: Forward: 2 <-> 3 <-> 4 Backward: 4 <-> 3 <-> 2

10. Implement a binary search tree

Implement a binary search tree supporting Insert, Search, and an in-order traversal that returns the stored values in sorted order.

package main

import "fmt"

type TreeNode struct {
	val   int
	left  *TreeNode
	right *TreeNode
}

type BST struct {
	root *TreeNode
}

func (t *BST) Insert(v int) {
	t.root = insertNode(t.root, v)
}

// insertNode returns the (possibly new) subtree root so each caller on
// the way back up can rewire its left/right pointer - this is what lets
// a brand-new leaf get attached without the caller needing a nil check
// of its own. Equal values are silently dropped (no else branch), so the
// tree only ever holds one node per distinct key.
func insertNode(n *TreeNode, v int) *TreeNode {
	if n == nil {
		return &TreeNode{val: v}
	}
	if v < n.val {
		n.left = insertNode(n.left, v)
	} else if v > n.val {
		n.right = insertNode(n.right, v)
	}
	return n
}

// Search follows the same left/right comparison as insertNode but can
// be a plain loop instead of recursion: it never needs to rewire a
// pointer on the way back up, only to answer yes/no.
func (t *BST) Search(v int) bool {
	cur := t.root
	for cur != nil {
		if v == cur.val {
			return true
		} else if v < cur.val {
			cur = cur.left
		} else {
			cur = cur.right
		}
	}
	return false
}

// InOrder visits left-subtree, node, right-subtree in that order, which
// for a BST always produces keys in sorted order - that's the defining
// property this traversal is used to demonstrate.
func (t *BST) InOrder() []int {
	var result []int
	// declared separately so the closure can reference walk recursively
	var walk func(n *TreeNode)
	walk = func(n *TreeNode) {
		if n == nil {
			return
		}
		walk(n.left)
		result = append(result, n.val)
		walk(n.right)
	}
	walk(t.root)
	return result
}

func main() {
	t := &BST{}
	for _, v := range []int{8, 3, 10, 1, 6, 14, 4, 7, 13} {
		t.Insert(v)
	}

	fmt.Println("In-order traversal:", t.InOrder())
	fmt.Println("Search 6:", t.Search(6))
	fmt.Println("Search 9:", t.Search(9))
	fmt.Println("Search 13:", t.Search(13))
}
In-order traversal: [1 3 4 6 7 8 10 13 14] Search 6: true Search 9: false Search 13: true

11. Implement a min-heap / priority queue

Implement a min-heap priority queue for ints by satisfying container/heap.Interface, then use heap.Push and heap.Pop to always retrieve the smallest remaining element.

package main

import (
	"container/heap"
	"fmt"
)

// MinHeap is a min-heap of ints implementing container/heap.Interface.
type MinHeap []int

func (h MinHeap) Len() int            { return len(h) }
// Less is the only line that decides min-heap vs max-heap: reporting the
// smaller element as "less" is what keeps the minimum at index 0.
func (h MinHeap) Less(i, j int) bool  { return h[i] < h[j] }
func (h MinHeap) Swap(i, j int)       { h[i], h[j] = h[j], h[i] }
// Push/Pop only manage the backing slice's length; they are never called
// directly. Package heap wraps them: heap.Push appends here then sifts
// the new element up, and heap.Pop swaps the root to the last slot and
// sifts down before calling this Pop - which is why it's always safe to
// take the element off the end rather than off the front.
func (h *MinHeap) Push(x interface{}) { *h = append(*h, x.(int)) }
func (h *MinHeap) Pop() interface{} {
	old := *h
	n := len(old)
	v := old[n-1]
	*h = old[:n-1]
	return v
}

func main() {
	h := &MinHeap{}
	heap.Init(h)

	for _, v := range []int{5, 2, 9, 1, 7, 3} {
		heap.Push(h, v)
	}
	fmt.Println("Heap size after pushes:", h.Len())

	fmt.Print("Popped in order: ")
	for h.Len() > 0 {
		fmt.Print(heap.Pop(h), " ")
	}
	fmt.Println()

	heap.Push(h, 42)
	heap.Push(h, 10)
	fmt.Println("Min after two pushes:", (*h)[0])
}
Heap size after pushes: 6 Popped in order: 1 2 3 5 7 9 Min after two pushes: 10

12. Implement an LRU cache

Implement an LRU cache with O(1) Get and Put by combining a map for key lookup with a doubly linked list that tracks recency, evicting the least-recently-used entry when the cache is full.

package main

import "fmt"

type lruNode struct {
	key, val   int
	prev, next *lruNode
}

// LRUCache gives O(1) Get/Put using a map for lookup plus a doubly
// linked list to track recency: front is most-recently-used, back is
// least-recently-used and the first to be evicted.
type LRUCache struct {
	capacity   int
	items      map[int]*lruNode
	head, tail *lruNode // head = most recent, tail = least recent
}

func NewLRUCache(capacity int) *LRUCache {
	return &LRUCache{capacity: capacity, items: make(map[int]*lruNode)}
}

// remove unlinks n from the list in O(1) - no need to search for it,
// since the map already gave us the node pointer directly.
func (c *LRUCache) remove(n *lruNode) {
	if n.prev != nil {
		n.prev.next = n.next
	} else {
		c.head = n.next
	}
	if n.next != nil {
		n.next.prev = n.prev
	} else {
		c.tail = n.prev
	}
	n.prev, n.next = nil, nil
}

// pushFront marks n as the most-recently-used entry by making it the
// new head. remove+pushFront together are how both Get and Put "touch"
// a key without duplicating the splice logic.
func (c *LRUCache) pushFront(n *lruNode) {
	n.next = c.head
	n.prev = nil
	if c.head != nil {
		c.head.prev = n
	}
	c.head = n
	if c.tail == nil {
		c.tail = n
	}
}

// Get is itself a "write": a successful read counts as a use, so the
// node has to move to the front or it would look stale next eviction.
func (c *LRUCache) Get(key int) (int, bool) {
	n, ok := c.items[key]
	if !ok {
		return 0, false
	}
	c.remove(n)
	c.pushFront(n)
	return n.val, true
}

func (c *LRUCache) Put(key, val int) {
	// key already present: overwrite in place and just re-touch it -
	// no eviction check needed since the map size isn't growing.
	if n, ok := c.items[key]; ok {
		n.val = val
		c.remove(n)
		c.pushFront(n)
		return
	}
	// only a genuinely new key can push us over capacity, and c.tail is
	// by construction the least-recently-used node - the one to evict.
	if len(c.items) >= c.capacity {
		lru := c.tail
		c.remove(lru)
		delete(c.items, lru.key)
	}
	n := &lruNode{key: key, val: val}
	c.items[key] = n
	c.pushFront(n)
}

func main() {
	c := NewLRUCache(2)
	c.Put(1, 100)
	c.Put(2, 200)
	v, ok := c.Get(1)
	fmt.Println("Get(1):", v, ok)

	c.Put(3, 300) // evicts key 2 (least recently used)
	_, ok = c.Get(2)
	fmt.Println("Get(2) after eviction:", ok)

	v, ok = c.Get(3)
	fmt.Println("Get(3):", v, ok)

	c.Put(4, 400) // evicts key 1 (least recently used now)
	_, ok = c.Get(1)
	fmt.Println("Get(1) after eviction:", ok)

	v, ok = c.Get(4)
	fmt.Println("Get(4):", v, ok)
}
Get(1): 100 true Get(2) after eviction: false Get(3): 300 true Get(1) after eviction: false Get(4): 400 true

13. Reverse a doubly linked list

Given a doubly linked list, reverse it in place by swapping each node's prev/next pointers and swapping the list's head/tail references.

package main

import "fmt"

type dnode struct {
	val        int
	prev, next *dnode
}

type dlist struct {
	head, tail *dnode
}

func (l *dlist) pushBack(v int) {
	n := &dnode{val: v}
	// empty list is a special case: head and tail both point at the sole node
	if l.head == nil {
		l.head, l.tail = n, n
		return
	}
	n.prev = l.tail
	l.tail.next = n
	l.tail = n
}

// a doubly linked list reverses in O(n) with no new nodes: every node already
// holds both neighbors, so reversing just means swapping each node's prev/next,
// then swapping which end of the list is called head vs tail.
func (l *dlist) reverse() {
	cur := l.head
	// flip the list's own head/tail pointers first, before touching individual nodes
	l.head, l.tail = l.tail, l.head
	for cur != nil {
		// swap this node's own prev/next so traversal direction inverts locally
		cur.prev, cur.next = cur.next, cur.prev
		cur = cur.prev // after the swap, .prev holds the old .next, so this still walks forward
	}
}

func (l *dlist) forward() []int {
	var out []int
	for n := l.head; n != nil; n = n.next {
		out = append(out, n.val)
	}
	return out
}

func (l *dlist) backward() []int {
	var out []int
	for n := l.tail; n != nil; n = n.prev {
		out = append(out, n.val)
	}
	return out
}

func main() {
	l := &dlist{}
	for _, v := range []int{1, 2, 3, 4, 5} {
		l.pushBack(v)
	}
	fmt.Println("before:", l.forward())
	l.reverse()
	fmt.Println("after: ", l.forward())
	fmt.Println("backward from tail:", l.backward())
}
before: [1 2 3 4 5] after: [5 4 3 2 1] backward from tail: [1 2 3 4 5]

14. Merge two sorted linked lists into one sorted linked list

Given two singly linked lists whose values are already sorted ascending, splice them together into a single sorted list without allocating new nodes.

package main

import "fmt"

type node struct {
	val  int
	next *node
}

// a dummy head node sidesteps the need to special-case "which list starts the
// result": we always have a node to hang tail.next off of, even before the
// first real element is chosen.
func mergeSorted(a, b *node) *node {
	dummy := &node{}
	tail := dummy
	for a != nil && b != nil {
		// splice the smaller node in directly and reuse it, rather than allocating
		// a new node and copying the value, so the merge is O(1) extra space
		if a.val <= b.val {
			tail.next = a
			a = a.next
		} else {
			tail.next = b
			b = b.next
		}
		tail = tail.next
	}
	// one list is exhausted, but the other is still sorted, so its remaining
	// tail can be attached in one shot instead of walking it node by node
	if a != nil {
		tail.next = a
	} else {
		tail.next = b
	}
	return dummy.next // skip past the dummy to return the real head
}

func build(vals []int) *node {
	dummy := &node{}
	tail := dummy
	for _, v := range vals {
		tail.next = &node{val: v}
		tail = tail.next
	}
	return dummy.next
}

func toSlice(n *node) []int {
	var out []int
	for ; n != nil; n = n.next {
		out = append(out, n.val)
	}
	return out
}

func main() {
	a := build([]int{1, 3, 5, 7})
	b := build([]int{2, 4, 6, 8, 10})
	fmt.Println("list a:", toSlice(a))
	fmt.Println("list b:", toSlice(b))
	merged := mergeSorted(a, b)
	fmt.Println("merged: ", toSlice(merged))
}
list a: [1 3 5 7] list b: [2 4 6 8 10] merged: [1 2 3 4 5 6 7 8 10]

15. Remove the Nth node from the end of a linked list in one pass

Delete the node that is n positions from the end of a singly linked list using a fast/slow two-pointer technique, without first computing the list's length.

package main

import "fmt"

type node struct {
	val  int
	next *node
}

func build(vals []int) *node {
	dummy := &node{}
	tail := dummy
	for _, v := range vals {
		tail.next = &node{val: v}
		tail = tail.next
	}
	return dummy.next
}

func toSlice(n *node) []int {
	var out []int
	for ; n != nil; n = n.next {
		out = append(out, n.val)
	}
	return out
}

// removeNthFromEnd deletes the nth node counting from the end (1-indexed)
// using two pointers separated by n steps, in a single pass.
func removeNthFromEnd(head *node, n int) *node {
	// dummy precedes head so slow can end up pointing at the node *before* the
	// one to remove even when that's the real head itself (n == length)
	dummy := &node{next: head}
	fast, slow := dummy, dummy
	// walk fast n steps ahead first, so the gap between fast and slow is fixed at n
	for i := 0; i < n; i++ {
		fast = fast.next
	}
	// advance both until fast falls off the end; slow is then exactly n nodes
	// behind, i.e. sitting right before the node to delete
	for fast.next != nil {
		fast = fast.next
		slow = slow.next
	}
	slow.next = slow.next.next // unlink the target node
	return dummy.next
}

func main() {
	head := build([]int{1, 2, 3, 4, 5})
	fmt.Println("original:", toSlice(head))

	head = removeNthFromEnd(head, 2)
	fmt.Println("remove 2nd from end:", toSlice(head))

	head = removeNthFromEnd(head, 1)
	fmt.Println("remove last:", toSlice(head))

	head = removeNthFromEnd(head, 3)
	fmt.Println("remove head (3rd from end of 3):", toSlice(head))
}
original: [1 2 3 4 5] remove 2nd from end: [1 2 3 5] remove last: [1 2 3] remove head (3rd from end of 3): [2 3]

16. Implement a trie (prefix tree)

Build a trie with Insert, Search, and StartsWith operations, using a fixed 26-entry child array per node for lowercase letters.

package main

import "fmt"

// a fixed-size array indexed by letter is a common trie shortcut when the
// alphabet is known and small (lowercase a-z); a map[rune]*trieNode would be
// needed for arbitrary characters, at the cost of extra allocation per node
type trieNode struct {
	children [26]*trieNode
	isEnd    bool // marks "a real word ends here", distinct from "a prefix passes through here"
}

type trie struct {
	root *trieNode
}

func newTrie() *trie {
	return &trie{root: &trieNode{}}
}

func (t *trie) Insert(word string) {
	cur := t.root
	for _, c := range word {
		i := c - 'a' // map the letter directly to its slot in the 26-wide array
		if cur.children[i] == nil {
			cur.children[i] = &trieNode{} // lazily create nodes only for paths that are actually used
		}
		cur = cur.children[i]
	}
	cur.isEnd = true // only the final letter's node is flagged as a complete word
}

// find walks as far down the shared prefix path as the string allows, and is
// shared by both Search and StartsWith since they differ only in what they
// check once they reach the end of the path (isEnd vs simply existing)
func (t *trie) find(word string) *trieNode {
	cur := t.root
	for _, c := range word {
		i := c - 'a'
		if cur.children[i] == nil {
			return nil // path breaks partway through: neither a word nor a prefix exists
		}
		cur = cur.children[i]
	}
	return cur
}

func (t *trie) Search(word string) bool {
	n := t.find(word)
	// reaching the node isn't enough on its own: "gop" reaches a real node in
	// this trie (via "gopher") but was never inserted as its own word
	return n != nil && n.isEnd
}

func (t *trie) StartsWith(prefix string) bool {
	return t.find(prefix) != nil // merely existing on the path is enough, isEnd is irrelevant here
}

func main() {
	t := newTrie()
	words := []string{"go", "gopher", "golang", "goroutine", "channel"}
	for _, w := range words {
		t.Insert(w)
	}

	fmt.Println("Search(go):", t.Search("go"))
	fmt.Println("Search(gop):", t.Search("gop"))
	fmt.Println("StartsWith(gop):", t.StartsWith("gop"))
	fmt.Println("Search(golang):", t.Search("golang"))
	fmt.Println("StartsWith(chan):", t.StartsWith("chan"))
	fmt.Println("StartsWith(chart):", t.StartsWith("chart"))
	fmt.Println("Search(channel):", t.Search("channel"))
}
Search(go): true Search(gop): false StartsWith(gop): true Search(golang): true StartsWith(chan): true StartsWith(chart): false Search(channel): true

17. Graph as an adjacency list with BFS traversal

Represent an undirected graph as a map[string][]string adjacency list and perform a breadth-first traversal from a given starting node using a queue.

package main

import "fmt"

type graph struct {
	adj map[string][]string
}

func newGraph() *graph {
	return &graph{adj: make(map[string][]string)}
}

// undirected graph: every edge is recorded on both endpoints' adjacency lists
func (g *graph) addEdge(u, v string) {
	g.adj[u] = append(g.adj[u], v)
	g.adj[v] = append(g.adj[v], u)
}

func (g *graph) bfs(start string) []string {
	// start is marked visited immediately (not when dequeued) so it can never
	// be queued a second time by another node that also points to it
	visited := map[string]bool{start: true}
	queue := []string{start}
	var order []string

	for len(queue) > 0 {
		// FIFO queue is what makes this breadth-first: all nodes at the current
		// distance are visited before any node one hop further out
		node := queue[0]
		queue = queue[1:]
		order = append(order, node)

		for _, next := range g.adj[node] {
			// mark visited at discovery time, not dequeue time, so the same
			// neighbor reached via two different paths is only queued once
			if !visited[next] {
				visited[next] = true
				queue = append(queue, next)
			}
		}
	}
	return order
}

func main() {
	g := newGraph()
	edges := [][2]string{
		{"A", "B"}, {"A", "C"}, {"B", "D"},
		{"C", "D"}, {"D", "E"}, {"E", "F"},
	}
	for _, e := range edges {
		g.addEdge(e[0], e[1])
	}

	fmt.Println("BFS from A:", g.bfs("A"))
	fmt.Println("BFS from E:", g.bfs("E"))
}
BFS from A: [A B C D E F] BFS from E: [E D F B C A]

18. Graph as an adjacency list with DFS traversal

Using the same adjacency-list representation, perform an iterative depth-first traversal with an explicit stack instead of recursion.

package main

import "fmt"

type graph struct {
	adj map[string][]string
}

func newGraph() *graph {
	return &graph{adj: make(map[string][]string)}
}

func (g *graph) addEdge(u, v string) {
	g.adj[u] = append(g.adj[u], v)
	g.adj[v] = append(g.adj[v], u)
}

// dfs performs an iterative depth-first traversal using an explicit stack:
// swapping bfs's queue for a LIFO stack is the only structural change needed
// to turn breadth-first into depth-first, since it's the pop order that
// decides whether we fan out sideways or plunge down one path first.
func (g *graph) dfs(start string) []string {
	visited := map[string]bool{}
	stack := []string{start}
	var order []string

	for len(stack) > 0 {
		node := stack[len(stack)-1]
		stack = stack[:len(stack)-1]

		// unlike the BFS version, duplicates can still be pushed onto the stack
		// before being marked visited, so the check has to happen here on pop,
		// not at push time, and repeats are just skipped rather than prevented
		if visited[node] {
			continue
		}
		visited[node] = true
		order = append(order, node)

		neighbors := g.adj[node]
		// push neighbors in reverse order so that, since a stack pops last-in-
		// first-out, they still get visited in their original left-to-right order
		for i := len(neighbors) - 1; i >= 0; i-- {
			if !visited[neighbors[i]] {
				stack = append(stack, neighbors[i])
			}
		}
	}
	return order
}

func main() {
	g := newGraph()
	edges := [][2]string{
		{"A", "B"}, {"A", "C"}, {"B", "D"},
		{"C", "D"}, {"D", "E"}, {"E", "F"},
	}
	for _, e := range edges {
		g.addEdge(e[0], e[1])
	}

	fmt.Println("DFS from A:", g.dfs("A"))
	fmt.Println("DFS from E:", g.dfs("E"))
}
DFS from A: [A B D C E F] DFS from E: [E D B A C F]

19. Check whether a binary tree is height-balanced

Determine whether every node's left and right subtree heights differ by at most 1, computing heights and detecting imbalance in a single bottom-up pass.

package main

import "fmt"

type tnode struct {
	val         int
	left, right *tnode
}

// height returns -1 as a sentinel through balanced to short-circuit
// once an imbalance is found anywhere in the subtree.
func height(n *tnode, balanced *bool) int {
	if n == nil {
		return 0
	}
	lh := height(n.left, balanced)
	rh := height(n.right, balanced)
	diff := lh - rh
	// AVL-style balance invariant: sibling subtree heights may differ
	// by at most 1, at every node, not just at the root.
	if diff < -1 || diff > 1 {
		*balanced = false
	}
	if lh > rh {
		return lh + 1
	}
	return rh + 1
}

func isBalanced(root *tnode) bool {
	balanced := true
	height(root, &balanced)
	return balanced
}

func main() {
	// Balanced tree:
	//        4
	//       / \
	//      2   6
	//     / \ / \
	//    1  3 5  7
	balancedTree := &tnode{4,
		&tnode{2, &tnode{val: 1}, &tnode{val: 3}},
		&tnode{6, &tnode{val: 5}, &tnode{val: 7}},
	}

	// Skewed / unbalanced tree: 1 -> 2 -> 3 -> 4 (all left children)
	unbalancedTree := &tnode{val: 1, left: &tnode{val: 2, left: &tnode{val: 3, left: &tnode{val: 4}}}}

	fmt.Println("balancedTree isBalanced:", isBalanced(balancedTree))
	fmt.Println("unbalancedTree isBalanced:", isBalanced(unbalancedTree))
	fmt.Println("nil tree isBalanced:", isBalanced(nil))
}
balancedTree isBalanced: true unbalancedTree isBalanced: false nil tree isBalanced: true

20. Level-order (BFS) traversal of a binary tree

Traverse a binary tree breadth-first and return its values grouped as a slice of slices, one inner slice per depth level.

package main

import "fmt"

type tnode struct {
	val         int
	left, right *tnode
}

// levelOrder returns node values grouped level by level using a
// queue and a per-level size snapshot.
func levelOrder(root *tnode) [][]int {
	if root == nil {
		return nil
	}
	var levels [][]int
	queue := []*tnode{root}

	for len(queue) > 0 {
		// Snapshot the queue length *before* enqueuing this level's
		// children: children get pushed onto the same queue, so without
		// freezing size up front the inner loop would drift into the
		// next level instead of stopping at the current one.
		size := len(queue)
		level := make([]int, 0, size)
		for i := 0; i < size; i++ {
			// Dequeue from the front (FIFO) so nodes are visited in the
			// order they were discovered, which is what gives BFS its
			// level-by-level shape.
			n := queue[0]
			queue = queue[1:]
			level = append(level, n.val)
			if n.left != nil {
				queue = append(queue, n.left)
			}
			if n.right != nil {
				queue = append(queue, n.right)
			}
		}
		levels = append(levels, level)
	}
	return levels
}

func main() {
	root := &tnode{1,
		&tnode{2, &tnode{val: 4}, &tnode{val: 5}},
		&tnode{3, nil, &tnode{val: 6}},
	}

	for i, level := range levelOrder(root) {
		fmt.Printf("level %d: %v\n", i, level)
	}
}
level 0: [1] level 1: [2 3] level 2: [4 5 6]

21. Lowest common ancestor of two nodes in a binary search tree

Given a BST and two values, find their lowest common ancestor by walking down from the root and exploiting BST ordering, without building parent pointers.

package main

import "fmt"

type tnode struct {
	val         int
	left, right *tnode
}

func insert(root *tnode, v int) *tnode {
	if root == nil {
		return &tnode{val: v}
	}
	if v < root.val {
		root.left = insert(root.left, v)
	} else {
		// Duplicates fall through to the right subtree; this input
		// has no repeats, so it doesn't matter here, but the ordering
		// invariant the LCA search below relies on still holds either way.
		root.right = insert(root.right, v)
	}
	return root
}

// lowestCommonAncestor exploits the BST ordering property: the LCA is the
// first node where p and q's values fall on opposite sides (or match it).
func lowestCommonAncestor(root *tnode, p, q int) *tnode {
	cur := root
	for cur != nil {
		switch {
		case p < cur.val && q < cur.val:
			cur = cur.left
		case p > cur.val && q > cur.val:
			cur = cur.right
		default:
			// p and q no longer agree on a side (one is <= cur.val and
			// the other is >=), or cur itself equals one of them: this
			// is the deepest node both still descend from, so it's the LCA.
			return cur
		}
	}
	return nil
}

func main() {
	var root *tnode
	for _, v := range []int{8, 3, 10, 1, 6, 14, 4, 7, 13} {
		root = insert(root, v)
	}

	lca1 := lowestCommonAncestor(root, 4, 7)
	fmt.Println("LCA(4, 7):", lca1.val)

	lca2 := lowestCommonAncestor(root, 1, 7)
	fmt.Println("LCA(1, 7):", lca2.val)

	lca3 := lowestCommonAncestor(root, 3, 14)
	fmt.Println("LCA(3, 14):", lca3.val)

	lca4 := lowestCommonAncestor(root, 13, 14)
	fmt.Println("LCA(13, 14):", lca4.val)
}
LCA(4, 7): 6 LCA(1, 7): 3 LCA(3, 14): 8 LCA(13, 14): 14

22. Implement a circular (ring) buffer with fixed capacity

Build a fixed-capacity ring buffer that, once full, overwrites the oldest element on Push rather than rejecting the write; supports Push, Pop, and a Snapshot of current contents in order.

package main

import "fmt"

// ringBuffer is a fixed-capacity circular buffer. When full, Push
// overwrites the oldest element rather than rejecting the write.
type ringBuffer struct {
	data []int
	head int // index of oldest element
	size int
	cap  int
}

func newRingBuffer(capacity int) *ringBuffer {
	return &ringBuffer{data: make([]int, capacity), cap: capacity}
}

func (r *ringBuffer) Push(v int) {
	// tail is derived from head+size (mod cap) instead of stored
	// separately, and size is tracked explicitly rather than inferred
	// from head==tail, because head==tail alone can't tell an empty
	// buffer apart from a full one.
	tail := (r.head + r.size) % r.cap
	r.data[tail] = v
	if r.size < r.cap {
		r.size++
	} else {
		// full: overwrite oldest, advance head
		r.head = (r.head + 1) % r.cap
	}
}

func (r *ringBuffer) Pop() (int, bool) {
	if r.size == 0 {
		return 0, false
	}
	v := r.data[r.head]
	r.head = (r.head + 1) % r.cap
	r.size--
	return v, true
}

func (r *ringBuffer) Snapshot() []int {
	out := make([]int, 0, r.size)
	for i := 0; i < r.size; i++ {
		out = append(out, r.data[(r.head+i)%r.cap])
	}
	return out
}

func main() {
	rb := newRingBuffer(3)
	rb.Push(1)
	rb.Push(2)
	rb.Push(3)
	fmt.Println("after pushing 1,2,3:", rb.Snapshot())

	rb.Push(4) // overwrites 1 (oldest)
	fmt.Println("after pushing 4 (full, overwrite oldest):", rb.Snapshot())

	v, ok := rb.Pop()
	fmt.Println("pop:", v, ok)
	fmt.Println("after pop:", rb.Snapshot())

	rb.Push(5)
	rb.Push(6)
	fmt.Println("after pushing 5,6:", rb.Snapshot())
}
after pushing 1,2,3: [1 2 3] after pushing 4 (full, overwrite oldest): [2 3 4] pop: 2 true after pop: [3 4] after pushing 5,6: [4 5 6]

23. Implement a union-find / disjoint-set data structure

Build a disjoint-set structure with path compression on Find and union by rank on Union, and track the number of distinct sets as merges happen.

package main

import "fmt"

// unionFind implements disjoint-set with path compression and union by rank.
type unionFind struct {
	parent []int
	rank   []int
	count  int // number of distinct sets
}

func newUnionFind(n int) *unionFind {
	uf := &unionFind{parent: make([]int, n), rank: make([]int, n), count: n}
	for i := range uf.parent {
		uf.parent[i] = i
	}
	return uf
}

func (uf *unionFind) Find(x int) int {
	if uf.parent[x] != x {
		// path compression: rewire x straight to the set's root as we
		// unwind, so every future Find along this path is O(1) instead
		// of re-walking the same chain
		uf.parent[x] = uf.Find(uf.parent[x])
	}
	return uf.parent[x]
}

func (uf *unionFind) Union(a, b int) bool {
	ra, rb := uf.Find(a), uf.Find(b)
	if ra == rb {
		return false
	}
	// Union by rank: always hang the shorter (or equal) tree under the
	// taller one's root, so ra ends up the root either way. This keeps
	// tree depth logarithmic instead of letting chains of unions build
	// a long, slow-to-Find spine. Rank only grows when two trees of
	// equal height merge, since that's the one case that increases the
	// worst-case depth of the result.
	switch {
	case uf.rank[ra] < uf.rank[rb]:
		ra, rb = rb, ra
	case uf.rank[ra] == uf.rank[rb]:
		uf.rank[ra]++
	}
	uf.parent[rb] = ra
	uf.count--
	return true
}

func (uf *unionFind) Connected(a, b int) bool {
	return uf.Find(a) == uf.Find(b)
}

func main() {
	uf := newUnionFind(10)

	edges := [][2]int{{0, 1}, {1, 2}, {3, 4}, {5, 6}, {6, 7}, {7, 8}}
	for _, e := range edges {
		uf.Union(e[0], e[1])
	}

	fmt.Println("Connected(0, 2):", uf.Connected(0, 2))
	fmt.Println("Connected(0, 3):", uf.Connected(0, 3))
	fmt.Println("Connected(5, 8):", uf.Connected(5, 8))
	fmt.Println("Connected(4, 9):", uf.Connected(4, 9))
	fmt.Println("distinct sets before merging 2-3:", uf.count)

	uf.Union(2, 3)
	fmt.Println("Connected(0, 4) after union(2,3):", uf.Connected(0, 4))
	fmt.Println("distinct sets after merging 2-3:", uf.count)
}
Connected(0, 2): true Connected(0, 3): false Connected(5, 8): true Connected(4, 9): false distinct sets before merging 2-3: 4 Connected(0, 4) after union(2,3): true distinct sets after merging 2-3: 3

24. Validate whether a binary tree is a valid binary search tree

Check the BST invariant correctly by threading a valid (min, max) range down through the recursion, rather than only comparing a node against its immediate parent.

package main

import (
	"fmt"
	"math"
)

type tnode struct {
	val         int
	left, right *tnode
}

// isValidBST checks the BST invariant by threading a valid (min, max)
// range down through the recursion instead of just comparing children.
func isValidBST(n *tnode, min, max int) bool {
	if n == nil {
		return true
	}
	if n.val <= min || n.val >= max {
		return false
	}
	// Left child inherits the same floor but its ceiling tightens to
	// n.val; the right child inherits the same ceiling but its floor
	// rises to n.val. That's how every deeper node stays constrained
	// by every ancestor above it, not just its direct parent.
	return isValidBST(n.left, min, n.val) && isValidBST(n.right, n.val, max)
}

func validate(root *tnode) bool {
	// Root has no ancestors yet, so seed the range with the widest
	// possible bounds instead of a sentinel like 0 that could collide
	// with a real node value.
	return isValidBST(root, math.MinInt, math.MaxInt)
}

func main() {
	// Valid BST:
	//        5
	//       / \
	//      3   8
	//     / \   \
	//    1   4   9
	valid := &tnode{5,
		&tnode{3, &tnode{val: 1}, &tnode{val: 4}},
		&tnode{8, nil, &tnode{val: 9}},
	}

	// Invalid: right subtree's left child (3) satisfies "< its parent 8"
	// but violates the ancestor rule (must be > root 5), so a naive
	// "compare with immediate parent only" check would miss it.
	invalid := &tnode{5,
		&tnode{3, &tnode{val: 1}, &tnode{val: 4}},
		&tnode{8, &tnode{val: 3}, &tnode{val: 9}},
	}

	// Invalid: duplicate value equal to an ancestor.
	dup := &tnode{5,
		&tnode{val: 5},
		&tnode{val: 8},
	}

	fmt.Println("valid tree isValidBST:", validate(valid))
	fmt.Println("invalid tree isValidBST:", validate(invalid))
	fmt.Println("duplicate-value tree isValidBST:", validate(dup))
}
valid tree isValidBST: true invalid tree isValidBST: false duplicate-value tree isValidBST: false

← Back to the roadmap