Reconstruct Itinerary

Turn tickets into sorted departure lists, always fly the smallest unused ticket, and fix each airport into the route only when it runs dry.

The memory hook

Fly the smallest ticket first, and write each airport down only when it has no tickets left — then read the list backwards.

HardHierholzer Eulerian pathO(E log E) timeO(V + E) space
01

Dead ends decide the order

intuition

A greedy smallest-first walk can strand tickets: flying JFK → KUL early leaves NRT → JFK unusable. The fix is to record airports post-order — an airport joins the route only after every ticket out of it is consumed.

1Sort departuresGroup tickets by airport and sort each list in reverse, so a plain pop() always yields the smallest lexical destination in O(1).
2Walk until stuckFrom the stack top, keep flying the smallest unused ticket and pushing each arrival. Getting stuck is progress, not failure.
3Fix and reverseAn airport with no tickets left pops off the stack into the route. Fixes happen trip-end first, so one reversal yields JFK-first order.
The one sentence to remember: Post-order fixing turns dead ends into correct suffixes: the first airport you get stuck at must end the trip.
02

Watch the walk backtrack itself

playable trace

Each frame flies one ticket or fixes one airport. Green stack airports still hold unused tickets; the red ghost is the airport just fixed; the final lime row is the reversed route.

Playable trace · Hierholzer Eulerian path
Frames are generated from the same logic as the code below.
Presets
Current event
Ready — press Play
Final itinerary
0
route (read backwards)
Tickets will be flown smallest-first; airports join the route only when they run out of departures.
unread current tickets still unused just fixed into route final itinerary
0 / 0
03

Walk small, fix late, reverse once

the solution
Python · Hierholzer Eulerian path
from collections import defaultdict

def findItinerary(tickets):
    graph = defaultdict(list)
    for src, dst in tickets:
        graph[src].append(dst)
    for src in graph:
        graph[src].sort(reverse=True)  # smallest leaves via pop()

    route, stack = [], ["JFK"]
    while stack:
        while graph[stack[-1]]:      # fly smallest unused ticket
            stack.append(graph[stack[-1]].pop())
        route.append(stack.pop())        # dead end: fix into route
    return route[::-1]              # built backwards

The inner while looks nested, but each ticket is popped exactly once; sorting the departure lists dominates the runtime.

Smallest-first edgepop() from a reverse-sorted listSorting descending makes every departure choice O(1) while still picking the lexical minimum.
Dead-end fixappend only when graph[top] is emptyAn airport enters the route only after all its departures are consumed — the post-order invariant.
Backward routeroute[::-1]Fixes happen from the trip end backwards, so one reversal yields JFK-first order.
Why can't greedy smallest-first alone work? With JFK → KUL, JFK → NRT, NRT → JFK, naive greedy flies JFK → KUL first and strands the NRT ticket. Post-order fixing never commits JFK early: NRT resolves into the route before KUL does, which forces the only Eulerian order.
TimeO(E log E)Every ticket is pushed and popped once; sorting all departure lists costs O(E log E) total.
SpaceO(V + E)The graph, stack, and route each hold at most one entry per ticket or airport: O(V + E).
04

Two quick mental checks

make it stick
[JFK,SFO],[JFK,ATL],[SFO,ATL],[ATL,JFK],[ATL,SFO]
→ JFK, ATL, JFK, SFO, ATL, SFO

JFK → SFO looks tempting but post-order buries it: ATL legs resolve first and the reversal restores lexical order.

[JFK,KUL],[JFK,NRT],[NRT,JFK]
→ JFK, NRT, JFK, KUL

Greedy smallest-first flies JFK → KUL and strands a ticket; fixing dead ends first forces NRT before KUL.

What to say out loud
I sort every airport’s departures, always fly the smallest unused ticket, and only record an airport once it is out of tickets. The record comes out backwards, so I reverse it — that reversal is what guarantees I use every ticket in lexical order.