- Array / String (9)
- Two Pointers (1)
- Sliding Window (1)
- Matrix (1)
- Hashmap (3)
- Intervals (2)
- Stack (1)
- Linked List (3)
- Binary Tree General (9)
- Binary Tree BFS (2)
- Binary Search Tree (1)
- Graph BFS (2)
- Divide and Conquer (2)
- Binary Search (2)
- Heap (1)
- Bit Manipulation (1)
- Math (3)
- Multidimensional DP (4)
Array / String
1. Remove Duplicates from Sorted Array II
Medium · LC 80
Given a sorted array, remove duplicates in place so that each value appears at most twice, and return the length of the kept prefix. Walk the array once with a write pointer, copying each element forward whenever it differs from the element two slots behind the write position. The trick is that sorted order makes that single comparison sufficient: an element is a third-or-later copy exactly when it equals the value two slots back, so the filter is online with no lookahead.
list.pop(i) shifts the whole tail left on every delete. Passes on LeetCode's small tests but is quadratic — fine at n <= 2000, times out well before LeetCode's 3 * 10^4 worst case.
Scan each run of equal values and copy min(run, 2) of them forward. Linear now, and generalizes to "keep at most k" by changing a single constant — but it needs lookahead to find each run's end, and the nested loops obscure what is really a one-condition filter.
Sorted input means nums[i] is a 3rd-or-later copy exactly when it equals the element two slots behind the write pointer. One pass, one comparison per element, no lookahead — an online filter.
Walk each run of equal values, append min(run, 2) copies to a fresh vector, then copy the result back. Correct and linear, but the follow-up explicitly asks for O(1) extra memory.
Sorted input means nums[i] is a 3rd-or-later copy exactly when it equals the element two slots behind the write pointer, so keep everything else and overwrite in place — no scratch vector needed.
Walk each run of equal values, push min(run, 2) copies onto a fresh Vec, then copy the result back. Correct and linear, but the follow-up explicitly asks for O(1) extra memory.
Sorted input means nums[i] is a 3rd-or-later copy exactly when it equals the element two slots behind the write pointer, so keep everything else and overwrite in place — no scratch Vec needed.
Walk each run of equal values, push min(run, 2) copies onto a fresh array, then copy the result back. Correct and linear, but the follow-up explicitly asks for O(1) extra memory.
Sorted input means nums[i] is a 3rd-or-later copy exactly when it equals the element two slots behind the write pointer, so keep everything else and overwrite in place — no scratch array needed.
Walk each run of equal values, append min(run, 2) copies to a fresh slice, then copy the result back. Correct and linear, but the follow-up explicitly asks for O(1) extra memory.
Sorted input means nums[i] is a 3rd-or-later copy exactly when it equals the element two slots behind the write pointer, so keep everything else and overwrite in place — no scratch slice needed.
Walk each run of equal values, append min(run, 2) copies to a fresh array, then copy the result back. Correct and linear, but the follow-up explicitly asks for O(1) extra memory.
Sorted input means nums[i] is a 3rd-or-later copy exactly when it equals the element two slots behind the write pointer, so keep everything else and overwrite in place — no scratch array needed.
def removeDuplicates_pop(self, nums: List[int]) -> int:
i = 2
while i < len(nums):
if nums[i] == nums[i - 2]: # sorted => three equal in a row
nums.pop(i)
else:
i += 1
return len(nums)
def removeDuplicates_run_length(self, nums: List[int]) -> int:
n = len(nums)
write = read = 0
while read < n:
run_end = read
while run_end < n and nums[run_end] == nums[read]:
run_end += 1
for _ in range(min(run_end - read, 2)):
nums[write] = nums[read]
write += 1
read = run_end
return write
def removeDuplicates(self, nums: List[int]) -> int:
write = 0
for num in nums:
if write < 2 or num != nums[write - 2]:
nums[write] = num
write += 1
return write
int removeDuplicatesExtraArray(vector<int>& nums) {
vector<int> kept;
size_t read = 0;
while (read < nums.size()) {
size_t runEnd = read;
while (runEnd < nums.size() && nums[runEnd] == nums[read]) {
++runEnd;
}
for (size_t c = 0; c < min<size_t>(runEnd - read, 2); ++c) {
kept.push_back(nums[read]);
}
read = runEnd;
}
copy(kept.begin(), kept.end(), nums.begin());
return static_cast<int>(kept.size());
}
int removeDuplicates(vector<int>& nums) {
int write = 0;
for (int num : nums) {
if (write < 2 || num != nums[write - 2]) {
nums[write++] = num;
}
}
return write;
}
pub fn remove_duplicates_extra_array(nums: &mut Vec<i32>) -> i32 {
let mut kept: Vec<i32> = Vec::with_capacity(nums.len());
let mut read = 0usize;
while read < nums.len() {
let mut run_end = read;
while run_end < nums.len() && nums[run_end] == nums[read] {
run_end += 1;
}
for _ in 0..(run_end - read).min(2) {
kept.push(nums[read]);
}
read = run_end;
}
nums[..kept.len()].copy_from_slice(&kept);
kept.len() as i32
}
pub fn remove_duplicates(nums: &mut Vec<i32>) -> i32 {
let mut write = 0usize;
for i in 0..nums.len() {
let num = nums[i];
if write < 2 || num != nums[write - 2] {
nums[write] = num;
write += 1;
}
}
write as i32
}
function removeDuplicatesExtraArray(nums: number[]): number {
const kept: number[] = [];
let read = 0;
while (read < nums.length) {
let runEnd = read;
while (runEnd < nums.length && nums[runEnd] === nums[read]) {
runEnd += 1;
}
for (let c = 0; c < Math.min(runEnd - read, 2); c += 1) {
kept.push(nums[read]);
}
read = runEnd;
}
for (let i = 0; i < kept.length; i += 1) {
nums[i] = kept[i];
}
return kept.length;
}
function removeDuplicates(nums: number[]): number {
let write = 0;
for (const num of nums) {
if (write < 2 || num !== nums[write - 2]) {
nums[write] = num;
write += 1;
}
}
return write;
}
func removeDuplicatesExtraArray(nums []int) int {
kept := make([]int, 0, len(nums))
read := 0
for read < len(nums) {
runEnd := read
for runEnd < len(nums) && nums[runEnd] == nums[read] {
runEnd++
}
take := runEnd - read
if take > 2 {
take = 2
}
for c := 0; c < take; c++ {
kept = append(kept, nums[read])
}
read = runEnd
}
copy(nums, kept)
return len(kept)
}
func removeDuplicates(nums []int) int {
write := 0
for _, num := range nums {
if write < 2 || num != nums[write-2] {
nums[write] = num
write++
}
}
return write
}
func removeDuplicatesExtraArray(_ nums: inout [Int]) -> Int {
var kept: [Int] = []
kept.reserveCapacity(nums.count)
var read = 0
while read < nums.count {
var runEnd = read
while runEnd < nums.count && nums[runEnd] == nums[read] {
runEnd += 1
}
for _ in 0..<min(runEnd - read, 2) {
kept.append(nums[read])
}
read = runEnd
}
nums.replaceSubrange(0..<kept.count, with: kept)
return kept.count
}
func removeDuplicates(_ nums: inout [Int]) -> Int {
var write = 0
for num in nums {
if write < 2 || num != nums[write - 2] {
nums[write] = num
write += 1
}
}
return write
}
2. H-Index
Medium · LC 274
Given an array of citation counts, return the h-index, the largest h such that h papers have at least h citations. Clamp each count into a bucket array indexed zero through n, then sweep h from n downward, accumulating the running number of papers with at least h citations and returning the first h that the count reaches. The trick is the clamp: since h can never exceed n, any citation count above n behaves exactly like n, which caps the buckets and makes the whole thing linear with no sort.
After sorting high-to-low, position i (1-based) has i papers with >= citations[i-1] citations; the last position where the citation count still reaches i is the h-index. The sort dominates the cost.
"h papers have >= h citations" is monotone in h (true for all values up to the answer, false after), so binary search the largest true h. Drops Approach 1's sorted copy and never mutates the input.
h <= n, so any citation count above n acts exactly like n. Clamp every paper into buckets[0..n], then sweep h from n downward and return the first h where the running count of papers >= h. Linear time — no comparison sort, no repeated counting passes.
After sorting high-to-low, position i (1-based) has i papers with >= citations[i-1] citations; the last position where the citation count still reaches i is the h-index. The sort dominates the cost.
h <= n, so any citation count above n acts exactly like n. Clamp every paper into buckets[0..n], then sweep h from n downward and return the first h where the running count of papers with >= h citations reaches h. Beats the sort's n log n outright.
After sorting high-to-low, position i (1-based) has i papers with >= citations[i-1] citations; the last position where the citation count still reaches i is the h-index. The sort dominates the cost.
h <= n, so any citation count above n acts exactly like n. Clamp every paper into buckets[0..n], then sweep h from n downward and return the first h where the running count of papers with >= h citations reaches h. Beats the sort's n log n outright.
After sorting high-to-low, position i (1-based) has i papers with >= citations[i-1] citations; the last position where the citation count still reaches i is the h-index. The sort dominates the cost.
h <= n, so any citation count above n acts exactly like n. Clamp every paper into buckets[0..n], then sweep h from n downward and return the first h where the running count of papers with >= h citations reaches h. Beats the sort's n log n outright.
After sorting high-to-low, position i (1-based) has i papers with >= citations[i-1] citations; the last position where the citation count still reaches i is the h-index. The sort dominates the cost.
h <= n, so any citation count above n acts exactly like n. Clamp every paper into buckets[0..n], then sweep h from n downward and return the first h where the running count of papers with >= h citations reaches h. Beats the sort's n log n outright.
After sorting high-to-low, position i (1-based) has i papers with >= citations[i-1] citations; the last position where the citation count still reaches i is the h-index. The sort dominates the cost.
h <= n, so any citation count above n acts exactly like n. Clamp every paper into buckets[0..n], then sweep h from n downward and return the first h where the running count of papers with >= h citations reaches h. Beats the sort's n log n outright.
def hIndex_sort(self, citations: List[int]) -> int:
h = 0
for i, c in enumerate(sorted(citations, reverse=True), start=1):
if c >= i:
h = i
else:
break
return h
def hIndex_binary_search(self, citations: List[int]) -> int:
lo, hi = 0, len(citations)
while lo < hi:
mid = (lo + hi + 1) // 2 # upper mid so the loop shrinks
if sum(c >= mid for c in citations) >= mid:
lo = mid
else:
hi = mid - 1
return lo
def hIndex(self, citations: List[int]) -> int:
n = len(citations)
buckets = [0] * (n + 1)
for c in citations:
buckets[min(c, n)] += 1
papers = 0 # papers with >= h citations
for h in range(n, -1, -1):
papers += buckets[h]
if papers >= h:
return h
return 0 # unreachable: h = 0 always satisfies papers >= 0
int hIndexSort(vector<int>& citations) {
sort(citations.begin(), citations.end(), greater<int>());
int h = 0;
for (int i = 0; i < static_cast<int>(citations.size()); ++i) {
if (citations[i] >= i + 1) {
h = i + 1;
} else {
break;
}
}
return h;
}
int hIndex(vector<int>& citations) {
int n = static_cast<int>(citations.size());
vector<int> buckets(n + 1, 0);
for (int c : citations) {
++buckets[min(c, n)];
}
int papers = 0; // papers with >= h citations
for (int h = n; h >= 0; --h) {
papers += buckets[h];
if (papers >= h) {
return h;
}
}
return 0; // unreachable: h = 0 always satisfies papers >= 0
}
pub fn h_index_sort(citations: Vec<i32>) -> i32 {
let mut citations = citations;
citations.sort_unstable_by(|a, b| b.cmp(a));
let mut h = 0;
for (i, &c) in citations.iter().enumerate() {
if c >= i as i32 + 1 {
h = i as i32 + 1;
} else {
break;
}
}
h
}
pub fn h_index(citations: Vec<i32>) -> i32 {
let n = citations.len();
let mut buckets = vec![0i32; n + 1];
for c in citations {
buckets[(c as usize).min(n)] += 1;
}
let mut papers = 0i32; // papers with >= h citations
for h in (0..=n).rev() {
papers += buckets[h];
if papers >= h as i32 {
return h as i32;
}
}
0 // unreachable: h = 0 always satisfies papers >= 0
}
function hIndexSort(citations: number[]): number {
const sorted = [...citations].sort((a, b) => b - a);
let h = 0;
for (let i = 0; i < sorted.length; i += 1) {
if (sorted[i] >= i + 1) {
h = i + 1;
} else {
break;
}
}
return h;
}
function hIndex(citations: number[]): number {
const n = citations.length;
const buckets = new Array<number>(n + 1).fill(0);
for (const c of citations) {
buckets[Math.min(c, n)] += 1;
}
let papers = 0; // papers with >= h citations
for (let h = n; h >= 0; h -= 1) {
papers += buckets[h];
if (papers >= h) {
return h;
}
}
return 0; // unreachable: h = 0 always satisfies papers >= 0
}
func hIndexSort(citations []int) int {
sort.Sort(sort.Reverse(sort.IntSlice(citations)))
h := 0
for i, c := range citations {
if c >= i+1 {
h = i + 1
} else {
break
}
}
return h
}
func hIndex(citations []int) int {
n := len(citations)
buckets := make([]int, n+1)
for _, c := range citations {
if c > n {
c = n
}
buckets[c]++
}
papers := 0 // papers with >= h citations
for h := n; h >= 0; h-- {
papers += buckets[h]
if papers >= h {
return h
}
}
return 0 // unreachable: h = 0 always satisfies papers >= 0
}
func hIndexSort(_ citations: [Int]) -> Int {
let sorted = citations.sorted(by: >)
var h = 0
for (i, c) in sorted.enumerated() {
if c >= i + 1 {
h = i + 1
} else {
break
}
}
return h
}
func hIndex(_ citations: [Int]) -> Int {
let n = citations.count
var buckets = [Int](repeating: 0, count: n + 1)
for c in citations {
buckets[min(c, n)] += 1
}
var papers = 0 // papers with >= h citations
for h in stride(from: n, through: 0, by: -1) {
papers += buckets[h]
if papers >= h {
return h
}
}
return 0 // unreachable: h = 0 always satisfies papers >= 0
}
3. Insert Delete GetRandom O(1)
Medium · LC 380
Design a set that supports insert, remove, and getRandom in average O(1) time each. Pair a dense array of values with a hash map from value to index: insert appends and records the position, getRandom picks a uniform random array element, and remove swaps the victim with the last element before popping the tail. The trick is the swap-pop, which keeps the array free of holes so random selection stays uniform, and the pitfall is forgetting to update the moved element's map entry to its new index.
Correct, but misses the point of the problem: membership tests and deletions scan the whole array. Fine at a few thousand ops; the judge's 2 * 10^5 ops would crawl.
A val->index map kills the scans. remove() just tombstones the slot; the array is compacted once dead slots reach half its length, so at least half of every array is alive and getRandom's rejection loop finishes in < 2 expected draws.
The array gives uniform O(1) random access; the map gives O(1) lookup. remove() swap-pops the victim with the last element so the array never has holes — no tombstones, no compaction spikes, no wasted slots.
Correct, but misses the point of the problem: membership tests and erase() scan and shift the whole vector. Fine at a few thousand ops; the judge's 2 * 10^5 ops would crawl.
The vector gives uniform O(1) random access, the map gives O(1) lookup, and remove() swap-pops the victim with the last element so the vector never has holes — no scans, no tail shifts.
Correct, but misses the point of the problem: membership tests scan the whole Vec and Vec::remove shifts the tail. Fine at a few thousand ops; the judge's 2 * 10^5 ops would crawl.
The Vec gives uniform O(1) random access, the map gives O(1) lookup, and remove() swap-pops the victim with the last element so the Vec never has holes — no scans, no tail shifts.
Correct, but misses the point of the problem: membership tests scan the whole array and splice() shifts the tail. Fine at a few thousand ops; the judge's 2 * 10^5 ops would crawl.
The array gives uniform O(1) random access, the Map gives O(1) lookup, and remove() swap-pops the victim with the last element so the array never has holes — no scans, no tail shifts.
Correct, but misses the point of the problem: membership tests and deletions scan the whole slice. Fine at a few thousand ops; the judge's 2 * 10^5 ops would crawl.
The slice gives uniform O(1) random access, the map gives O(1) lookup, and Remove swap-pops the victim with the last element so the slice never has holes — no scans, no tail shifts.
Correct, but misses the point of the problem: membership tests scan the whole array and remove(at:) shifts the tail. Fine at a few thousand ops; the judge's 2 * 10^5 ops would crawl.
The array gives uniform O(1) random access, the dictionary gives O(1) lookup, and remove() swap-pops the victim with the last element so the array never has holes — no scans, no tail shifts.
class RandomizedSetScan:
"""Same interface as RandomizedSet, no auxiliary index."""
def __init__(self):
self.values = [] # current members, unordered
def insert(self, val: int) -> bool:
if val in self.values: # O(n) scan
return False
self.values.append(val)
return True
def remove(self, val: int) -> bool:
if val not in self.values: # O(n) scan
return False
self.values.remove(val) # another scan plus a tail shift
return True
def getRandom(self) -> int:
return random.choice(self.values)
class RandomizedSetLazy:
"""Same interface as RandomizedSet, genuinely different bookkeeping."""
_DEAD = object() # sentinel; never equal to a stored int
def __init__(self):
self.values = [] # members interleaved with _DEAD tombstones
self.index_of = {} # val -> position in self.values
self.dead = 0
def insert(self, val: int) -> bool:
if val in self.index_of:
return False
self.index_of[val] = len(self.values)
self.values.append(val)
return True
def remove(self, val: int) -> bool:
if val not in self.index_of:
return False
self.values[self.index_of.pop(val)] = self._DEAD
self.dead += 1
if self.dead * 2 >= len(self.values):
self._compact()
return True
def getRandom(self) -> int:
while True: # < 2 expected iterations: at least half the slots live
val = random.choice(self.values)
if val is not self._DEAD:
return val
def _compact(self):
self.values = [v for v in self.values if v is not self._DEAD]
self.index_of = {v: i for i, v in enumerate(self.values)}
self.dead = 0
class RandomizedSet:
"""LeetCode 380. Insert Delete GetRandom O(1) — array + index map."""
def __init__(self):
self.values = [] # dense array of current members
self.index_of = {} # val -> position in self.values
def insert(self, val: int) -> bool:
if val in self.index_of:
return False
self.index_of[val] = len(self.values)
self.values.append(val)
return True
def remove(self, val: int) -> bool:
if val not in self.index_of:
return False
idx = self.index_of.pop(val)
last = self.values.pop()
if idx < len(self.values): # victim wasn't the last element
self.values[idx] = last
self.index_of[last] = idx
return True
def getRandom(self) -> int:
return random.choice(self.values)
class RandomizedSetScan {
vector<int> values; // current members, unordered
mt19937 rng{381};
public:
RandomizedSetScan() {}
bool insert(int val) {
if (find(values.begin(), values.end(), val) != values.end()) {
return false;
}
values.push_back(val);
return true;
}
bool remove(int val) {
auto it = find(values.begin(), values.end(), val);
if (it == values.end()) {
return false;
}
values.erase(it); // shifts the whole tail left
return true;
}
int getRandom() {
uniform_int_distribution<int> dist(
0, static_cast<int>(values.size()) - 1);
return values[dist(rng)];
}
};
class RandomizedSet {
vector<int> values; // dense array of current members
unordered_map<int, int> indexOf; // val -> position in values
mt19937 rng{380};
public:
RandomizedSet() {}
bool insert(int val) {
if (indexOf.count(val)) {
return false;
}
indexOf[val] = static_cast<int>(values.size());
values.push_back(val);
return true;
}
bool remove(int val) {
auto it = indexOf.find(val);
if (it == indexOf.end()) {
return false;
}
int idx = it->second;
indexOf.erase(it);
int last = values.back();
values.pop_back();
if (idx < static_cast<int>(values.size())) { // victim wasn't last
values[idx] = last;
indexOf[last] = idx;
}
return true;
}
int getRandom() {
uniform_int_distribution<int> dist(
0, static_cast<int>(values.size()) - 1);
return values[dist(rng)];
}
};
struct RandomizedSetScan {
values: Vec<i32>, // current members, unordered
rng_state: Cell<u64>,
}
impl RandomizedSetScan {
fn new() -> Self {
RandomizedSetScan {
values: Vec::new(),
rng_state: Cell::new(0x0381_1234_5678_9abc),
}
}
fn insert(&mut self, val: i32) -> bool {
if self.values.contains(&val) {
return false;
}
self.values.push(val);
true
}
fn remove(&mut self, val: i32) -> bool {
match self.values.iter().position(|&v| v == val) {
None => false,
Some(idx) => {
self.values.remove(idx); // shifts the whole tail left
true
}
}
}
fn get_random(&self) -> i32 {
let x = xorshift64(&self.rng_state);
self.values[(x % self.values.len() as u64) as usize]
}
}
struct RandomizedSet {
values: Vec<i32>, // dense array of current members
index_of: HashMap<i32, usize>, // val -> position in values
rng_state: Cell<u64>,
}
impl RandomizedSet {
fn new() -> Self {
RandomizedSet {
values: Vec::new(),
index_of: HashMap::new(),
rng_state: Cell::new(0x0380_1234_5678_9abc),
}
}
fn insert(&mut self, val: i32) -> bool {
if self.index_of.contains_key(&val) {
return false;
}
self.index_of.insert(val, self.values.len());
self.values.push(val);
true
}
fn remove(&mut self, val: i32) -> bool {
match self.index_of.remove(&val) {
None => false,
Some(idx) => {
let last = self.values.pop().unwrap();
if idx < self.values.len() {
// victim wasn't the last element: swap it in
self.values[idx] = last;
self.index_of.insert(last, idx);
}
true
}
}
}
fn get_random(&self) -> i32 {
let x = xorshift64(&self.rng_state);
self.values[(x % self.values.len() as u64) as usize]
}
}
class RandomizedSetScan {
private values: number[] = []; // current members, unordered
insert(val: number): boolean {
if (this.values.includes(val)) { // O(n) scan
return false;
}
this.values.push(val);
return true;
}
remove(val: number): boolean {
const idx = this.values.indexOf(val); // O(n) scan
if (idx < 0) {
return false;
}
this.values.splice(idx, 1); // shifts the whole tail left
return true;
}
getRandom(): number {
return this.values[Math.floor(Math.random() * this.values.length)];
}
}
class RandomizedSet {
private values: number[] = []; // dense array of current members
private indexOf: Map<number, number> = new Map(); // val -> position in values
insert(val: number): boolean {
if (this.indexOf.has(val)) {
return false;
}
this.indexOf.set(val, this.values.length);
this.values.push(val);
return true;
}
remove(val: number): boolean {
const idx = this.indexOf.get(val);
if (idx === undefined) {
return false;
}
this.indexOf.delete(val);
const last = this.values.pop() as number;
if (idx < this.values.length) { // victim wasn't the last element
this.values[idx] = last;
this.indexOf.set(last, idx);
}
return true;
}
getRandom(): number {
return this.values[Math.floor(Math.random() * this.values.length)];
}
}
type RandomizedSetScan struct {
values []int // current members, unordered
rng *rand.Rand
}
func ConstructorScan() RandomizedSetScan {
return RandomizedSetScan{rng: rand.New(rand.NewSource(381))}
}
func (s *RandomizedSetScan) scanFor(val int) int {
for i, v := range s.values {
if v == val {
return i
}
}
return -1
}
func (s *RandomizedSetScan) Insert(val int) bool {
if s.scanFor(val) >= 0 {
return false
}
s.values = append(s.values, val)
return true
}
func (s *RandomizedSetScan) Remove(val int) bool {
idx := s.scanFor(val)
if idx < 0 {
return false
}
s.values = append(s.values[:idx], s.values[idx+1:]...) // shifts the tail
return true
}
func (s *RandomizedSetScan) GetRandom() int {
return s.values[s.rng.Intn(len(s.values))]
}
type RandomizedSet struct {
values []int // dense array of current members
indexOf map[int]int // val -> position in values
rng *rand.Rand
}
func Constructor() RandomizedSet {
return RandomizedSet{
indexOf: make(map[int]int),
rng: rand.New(rand.NewSource(380)),
}
}
func (s *RandomizedSet) Insert(val int) bool {
if _, ok := s.indexOf[val]; ok {
return false
}
s.indexOf[val] = len(s.values)
s.values = append(s.values, val)
return true
}
func (s *RandomizedSet) Remove(val int) bool {
idx, ok := s.indexOf[val]
if !ok {
return false
}
delete(s.indexOf, val)
last := s.values[len(s.values)-1]
s.values = s.values[:len(s.values)-1]
if idx < len(s.values) { // victim wasn't the last element
s.values[idx] = last
s.indexOf[last] = idx
}
return true
}
func (s *RandomizedSet) GetRandom() int {
return s.values[s.rng.Intn(len(s.values))]
}
class RandomizedSetScan {
private var values: [Int] = [] // current members, unordered
private var rng = SplitMix64(seed: 381)
init() {}
func insert(_ val: Int) -> Bool {
if values.contains(val) { // O(n) scan
return false
}
values.append(val)
return true
}
func remove(_ val: Int) -> Bool {
guard let idx = values.firstIndex(of: val) else { // O(n) scan
return false
}
values.remove(at: idx) // shifts the whole tail left
return true
}
func getRandom() -> Int {
return values[Int.random(in: 0..<values.count, using: &rng)]
}
}
class RandomizedSet {
private var values: [Int] = [] // dense array of current members
private var indexOf: [Int: Int] = [:] // val -> position in values
private var rng = SplitMix64(seed: 380)
init() {}
func insert(_ val: Int) -> Bool {
if indexOf[val] != nil {
return false
}
indexOf[val] = values.count
values.append(val)
return true
}
func remove(_ val: Int) -> Bool {
guard let idx = indexOf[val] else {
return false
}
indexOf[val] = nil
let last = values.removeLast()
if idx < values.count { // victim wasn't the last element
values[idx] = last
indexOf[last] = idx
}
return true
}
func getRandom() -> Int {
return values[Int.random(in: 0..<values.count, using: &rng)]
}
}
4. Integer to Roman
Medium · LC 12
Given an integer between 1 and 3999, convert it to a Roman numeral. Walk a table of thirteen value and symbol pairs from largest to smallest, using divmod to take as many copies of each symbol as fit before moving on. The trick is listing the subtractive forms like CM and IV directly in the table, which lets the plain greedy rule handle them with no special cases and extends past 3999 by just adding larger entries.
Mirrors how you'd write a numeral by hand: keep subtracting the largest value that still fits, one symbol at a time. Instructive, but re-tests the same table entry once per emitted symbol.
Each decimal digit maps independently to a fixed Roman chunk, so precompute all chunks and concatenate thousands/hundreds/tens/ones. No loops at all — but the 31 hardcoded chunks only cover 1..3999 and hide the greedy rule that generates them.
For each entry take as many copies as fit (divmod), then move on. One pass over 13 entries, no hardcoded chunk tables, and it extends past 3999 by just adding larger entries.
Each decimal digit maps independently to a fixed Roman chunk, so precompute all chunks and concatenate thousands/hundreds/tens/ones. No loops at all — but the 31 hardcoded chunks only cover 1..3999 and hide the greedy rule that generates them.
Peel copies of each entry from largest to smallest. One pass over 13 entries, no hardcoded chunk tables, and it extends past 3999 by just adding larger entries.
Each decimal digit maps independently to a fixed Roman chunk, so precompute all chunks and concatenate thousands/hundreds/tens/ones. No loops at all — but the 31 hardcoded chunks only cover 1..3999 and hide the greedy rule that generates them.
Peel copies of each entry from largest to smallest. One pass over 13 entries, no hardcoded chunk tables, and it extends past 3999 by just adding larger entries.
Each decimal digit maps independently to a fixed Roman chunk, so precompute all chunks and concatenate thousands/hundreds/tens/ones. No loops at all — but the 31 hardcoded chunks only cover 1..3999 and hide the greedy rule that generates them.
Peel copies of each entry from largest to smallest. One pass over 13 entries, no hardcoded chunk tables, and it extends past 3999 by just adding larger entries.
Each decimal digit maps independently to a fixed Roman chunk, so precompute all chunks and concatenate thousands/hundreds/tens/ones. No loops at all — but the 31 hardcoded chunks only cover 1..3999 and hide the greedy rule that generates them.
Peel copies of each entry from largest to smallest. One pass over 13 entries, no hardcoded chunk tables, and it extends past 3999 by just adding larger entries.
Each decimal digit maps independently to a fixed Roman chunk, so precompute all chunks and concatenate thousands/hundreds/tens/ones. No loops at all — but the 31 hardcoded chunks only cover 1..3999 and hide the greedy rule that generates them.
Peel copies of each entry from largest to smallest. One pass over 13 entries, no hardcoded chunk tables, and it extends past 3999 by just adding larger entries.
def intToRoman_subtraction(self, num: int) -> str:
parts = []
for value, symbol in self._TABLE:
while num >= value:
num -= value
parts.append(symbol)
return "".join(parts)
def intToRoman_digit_lookup(self, num: int) -> str:
thousands = ["", "M", "MM", "MMM"]
hundreds = ["", "C", "CC", "CCC", "CD", "D", "DC", "DCC", "DCCC", "CM"]
tens = ["", "X", "XX", "XXX", "XL", "L", "LX", "LXX", "LXXX", "XC"]
ones = ["", "I", "II", "III", "IV", "V", "VI", "VII", "VIII", "IX"]
return (thousands[num // 1000] + hundreds[num // 100 % 10]
+ tens[num // 10 % 10] + ones[num % 10])
def intToRoman(self, num: int) -> str:
parts = []
for value, symbol in self._TABLE:
if num == 0:
break
count, num = divmod(num, value)
parts.append(symbol * count)
return "".join(parts)
string intToRomanDigitLookup(int num) {
static const char* const kThousands[] = {"", "M", "MM", "MMM"};
static const char* const kHundreds[] = {
"", "C", "CC", "CCC", "CD", "D", "DC", "DCC", "DCCC", "CM"};
static const char* const kTens[] = {
"", "X", "XX", "XXX", "XL", "L", "LX", "LXX", "LXXX", "XC"};
static const char* const kOnes[] = {
"", "I", "II", "III", "IV", "V", "VI", "VII", "VIII", "IX"};
return string(kThousands[num / 1000]) + kHundreds[num / 100 % 10] +
kTens[num / 10 % 10] + kOnes[num % 10];
}
string intToRoman(int num) {
static const pair<int, const char*> kTable[] = {
{1000, "M"}, {900, "CM"}, {500, "D"}, {400, "CD"},
{100, "C"}, {90, "XC"}, {50, "L"}, {40, "XL"},
{10, "X"}, {9, "IX"}, {5, "V"}, {4, "IV"}, {1, "I"},
};
string out;
for (const auto& [value, symbol] : kTable) {
while (num >= value) {
num -= value;
out += symbol;
}
}
return out;
}
pub fn int_to_roman_digit_lookup(num: i32) -> String {
const THOUSANDS: [&str; 4] = ["", "M", "MM", "MMM"];
const HUNDREDS: [&str; 10] = [
"", "C", "CC", "CCC", "CD", "D", "DC", "DCC", "DCCC", "CM",
];
const TENS: [&str; 10] = ["", "X", "XX", "XXX", "XL", "L", "LX", "LXX", "LXXX", "XC"];
const ONES: [&str; 10] = ["", "I", "II", "III", "IV", "V", "VI", "VII", "VIII", "IX"];
let num = num as usize;
format!(
"{}{}{}{}",
THOUSANDS[num / 1000],
HUNDREDS[num / 100 % 10],
TENS[num / 10 % 10],
ONES[num % 10]
)
}
pub fn int_to_roman(num: i32) -> String {
const TABLE: [(i32, &str); 13] = [
(1000, "M"), (900, "CM"), (500, "D"), (400, "CD"),
(100, "C"), (90, "XC"), (50, "L"), (40, "XL"),
(10, "X"), (9, "IX"), (5, "V"), (4, "IV"), (1, "I"),
];
let mut num = num;
let mut out = String::new();
for (value, symbol) in TABLE {
while num >= value {
num -= value;
out.push_str(symbol);
}
}
out
}
function intToRomanDigitLookup(num: number): string {
const thousands = ["", "M", "MM", "MMM"];
const hundreds = ["", "C", "CC", "CCC", "CD", "D", "DC", "DCC", "DCCC", "CM"];
const tens = ["", "X", "XX", "XXX", "XL", "L", "LX", "LXX", "LXXX", "XC"];
const ones = ["", "I", "II", "III", "IV", "V", "VI", "VII", "VIII", "IX"];
return (
thousands[Math.floor(num / 1000)] +
hundreds[Math.floor(num / 100) % 10] +
tens[Math.floor(num / 10) % 10] +
ones[num % 10]
);
}
function intToRoman(num: number): string {
const table: Array<[number, string]> = [
[1000, "M"], [900, "CM"], [500, "D"], [400, "CD"],
[100, "C"], [90, "XC"], [50, "L"], [40, "XL"],
[10, "X"], [9, "IX"], [5, "V"], [4, "IV"], [1, "I"],
];
let out = "";
for (const [value, symbol] of table) {
while (num >= value) {
num -= value;
out += symbol;
}
}
return out;
}
func intToRomanDigitLookup(num int) string {
thousands := []string{"", "M", "MM", "MMM"}
hundreds := []string{"", "C", "CC", "CCC", "CD", "D", "DC", "DCC", "DCCC", "CM"}
tens := []string{"", "X", "XX", "XXX", "XL", "L", "LX", "LXX", "LXXX", "XC"}
ones := []string{"", "I", "II", "III", "IV", "V", "VI", "VII", "VIII", "IX"}
return thousands[num/1000] + hundreds[num/100%10] + tens[num/10%10] + ones[num%10]
}
func intToRoman(num int) string {
values := []int{1000, 900, 500, 400, 100, 90, 50, 40, 10, 9, 5, 4, 1}
symbols := []string{"M", "CM", "D", "CD", "C", "XC", "L", "XL", "X", "IX", "V", "IV", "I"}
var out strings.Builder
for i, value := range values {
for num >= value {
num -= value
out.WriteString(symbols[i])
}
}
return out.String()
}
func intToRomanDigitLookup(_ num: Int) -> String {
let thousands = ["", "M", "MM", "MMM"]
let hundreds = ["", "C", "CC", "CCC", "CD", "D", "DC", "DCC", "DCCC", "CM"]
let tens = ["", "X", "XX", "XXX", "XL", "L", "LX", "LXX", "LXXX", "XC"]
let ones = ["", "I", "II", "III", "IV", "V", "VI", "VII", "VIII", "IX"]
return thousands[num / 1000] + hundreds[num / 100 % 10]
+ tens[num / 10 % 10] + ones[num % 10]
}
func intToRoman(_ num: Int) -> String {
let table: [(value: Int, symbol: String)] = [
(1000, "M"), (900, "CM"), (500, "D"), (400, "CD"),
(100, "C"), (90, "XC"), (50, "L"), (40, "XL"),
(10, "X"), (9, "IX"), (5, "V"), (4, "IV"), (1, "I"),
]
var num = num
var out = ""
for (value, symbol) in table {
while num >= value {
num -= value
out += symbol
}
}
return out
}
5. Length of Last Word
Easy · LC 58
Given a string of words and spaces, return the length of the last word. Scan from the end of the string, first skipping any trailing spaces, then counting characters until the next space or the start of the string. The trick is that scanning backward never touches the front of a long string, so the typical cost is proportional to the last word rather than the whole input.
str.split() with no argument trims the ends and collapses runs of spaces, so the last token is exactly the last word. Costs a full token list just to look at one entry.
Track the word in progress; remember its length on every non-space char. Drops Approach 1's token list, but still walks the whole string even when only the tail matters.
Skip trailing spaces from the end, then count characters until the next space. Never touches the front of a long string.
Let a stringstream do the space handling: extraction skips runs of whitespace, so the last extracted token is the last word. Copies the whole string into a stream just to look at one token.
Skip trailing spaces from the end, then count characters until the next space. No copies, and never touches the front of a long string.
split_whitespace trims the ends and collapses runs of spaces, so the last token is exactly the last word. Materializing every token in a Vec just to read one entry is the naive part; the iterator could stream, but this is the version people write first.
Skip trailing spaces from the end, then count characters until the next space. No allocations, and never touches the front of a long string.
Split on single spaces and filter out the empty pieces that leading/trailing/repeated spaces produce; the last token is exactly the last word. Builds the whole token array to look at one entry.
Skip trailing spaces from the end, then count characters until the next space. No allocations, and never touches the front of a long string.
strings.Fields trims the ends and collapses runs of spaces, so the last token is exactly the last word. Allocates the whole token slice just to look at one entry.
Skip trailing spaces from the end, then count characters until the next space. No allocations, and never touches the front of a long string.
split(separator:) drops the empty pieces that leading/trailing/ repeated spaces would produce, so the last token is exactly the last word. Builds the whole token array just to look at one entry.
Skip trailing spaces from the end, then count characters until the next space. No token list; after the one-time array conversion (Swift strings lack integer indexing) only the tail is touched.
def lengthOfLastWord_split(self, s: str) -> int:
return len(s.split()[-1])
def lengthOfLastWord_forward(self, s: str) -> int:
last = current = 0
for char in s:
if char == ' ':
current = 0
else:
current += 1
last = current
return last
def lengthOfLastWord(self, s: str) -> int:
i = len(s) - 1
while i >= 0 and s[i] == ' ':
i -= 1
length = 0
while i >= 0 and s[i] != ' ':
length += 1
i -= 1
return length
int lengthOfLastWordSplit(string s) {
istringstream in(s);
string word, last;
while (in >> word) {
last = word;
}
return static_cast<int>(last.size());
}
int lengthOfLastWord(string s) {
int i = static_cast<int>(s.size()) - 1;
while (i >= 0 && s[i] == ' ') {
--i;
}
int length = 0;
while (i >= 0 && s[i] != ' ') {
++length;
--i;
}
return length;
}
pub fn length_of_last_word_split(s: String) -> i32 {
let words: Vec<&str> = s.split_whitespace().collect();
words.last().map_or(0, |w| w.len() as i32)
}
pub fn length_of_last_word(s: String) -> i32 {
let bytes = s.as_bytes();
let mut i = bytes.len();
while i > 0 && bytes[i - 1] == b' ' {
i -= 1;
}
let mut length = 0;
while i > 0 && bytes[i - 1] != b' ' {
length += 1;
i -= 1;
}
length
}
function lengthOfLastWordSplit(s: string): number {
const words = s.split(" ").filter((word) => word.length > 0);
return words[words.length - 1].length;
}
function lengthOfLastWord(s: string): number {
let i = s.length - 1;
while (i >= 0 && s[i] === " ") {
i--;
}
let length = 0;
while (i >= 0 && s[i] !== " ") {
length++;
i--;
}
return length;
}
func lengthOfLastWordSplit(s string) int {
words := strings.Fields(s)
return len(words[len(words)-1])
}
func lengthOfLastWord(s string) int {
i := len(s) - 1
for i >= 0 && s[i] == ' ' {
i--
}
length := 0
for i >= 0 && s[i] != ' ' {
length++
i--
}
return length
}
func lengthOfLastWordSplit(_ s: String) -> Int {
let words = s.split(separator: " ")
return words.last?.count ?? 0
}
func lengthOfLastWord(_ s: String) -> Int {
let chars = Array(s)
var i = chars.count - 1
while i >= 0 && chars[i] == " " {
i -= 1
}
var length = 0
while i >= 0 && chars[i] != " " {
length += 1
i -= 1
}
return length
}
6. Reverse Words in a String
Medium · LC 151
Given a string of words separated by arbitrary runs of spaces, return the words in reverse order joined by single spaces. Split the string with no separator argument, reverse the resulting word list, and join it back with single spaces. The trick is that argument-free split already trims both ends and collapses runs of spaces, doing all the whitespace cleanup for free, and since Python strings are immutable any answer costs O(n) space anyway.
Collect each word and prepend it to the answer built so far. Every prepend copies the whole result, so many short words go quadratic — fine at LeetCode's n <= 10^4, painful much beyond that.
The classic C/C++ in-place trick: reverse the whole char array so the words land in the right order (letters backwards), then reverse each word back while compacting spaces with a write pointer. Linear time — no copy per word like Approach 1 — at the price of extra passes.
Walk right-to-left; for each word locate its end, then its start, and slice it out. One backward pass instead of Approach 2's reverse-then-re-reverse, with no character-by-character rebuilding.
str.split() with no argument trims the ends and collapses runs of spaces, doing all the cleanup for free; then it is just a reversed join. Any answer must be O(n) space in Python (strings are immutable).
Tokenize with a stringstream and prepend each word to the answer built so far. Every prepend copies the whole result, so many short words go quadratic — fine at LeetCode's n <= 10^4, painful beyond.
Collect the words, reverse the vector, join with single spaces. Linear — each character is copied a constant number of times — but it buffers every word in a second container next to the input.
Reverse the whole string so the words land in the right order (letters backwards), then reverse each word back while compacting extra spaces with a write pointer. No auxiliary buffers at all.
Prepend each word to the answer built so far. Every prepend rebuilds the whole result string, so many short words go quadratic — fine at LeetCode's n <= 10^4, painful much beyond.
split_whitespace trims the ends and collapses runs of spaces, doing all the cleanup for free; then it is just a reversed join. Each byte is copied a constant number of times.
Grow each word char by char and prepend it to the answer built so far. Every prepend copies the whole result, so many short words go quadratic — fine at LeetCode's n <= 10^4, painful much beyond that.
Split on single spaces, filter out the empty pieces that leading/trailing/repeated spaces produce, then reverse and join with single spaces. Each character is copied a constant number of times.
Grow each word byte by byte and prepend it to the answer built so far. Both the append and the prepend copy their whole operand, so many short words go quadratic — fine at LeetCode's n <= 10^4, painful much beyond that.
strings.Fields trims the ends and collapses runs of spaces, doing all the cleanup for free; then reverse the slice in place and join with single spaces. Each byte is copied a constant number of times.
Prepend each word to the answer built so far. Every prepend copies the whole result string, so many short words go quadratic — fine at LeetCode's n <= 10^4, painful much beyond that.
split(separator:) drops the empty pieces that leading/trailing/ repeated spaces would produce, doing all the cleanup for free; then it is just a reversed join with single spaces. Each character is copied a constant number of times.
def reverseWords_concat(self, s: str) -> str:
result = ""
word = ""
for char in s + " ":
if char != ' ':
word += char
elif word:
result = word if not result else word + " " + result
word = ""
return result
def reverseWords_reverse_all(self, s: str) -> str:
chars = list(s)[::-1]
n = len(chars)
write = read = 0
while read < n:
while read < n and chars[read] == ' ':
read += 1
if read == n:
break
if write > 0: # single separator between words
chars[write] = ' '
write += 1
start = write
while read < n and chars[read] != ' ':
chars[write] = chars[read]
write += 1
read += 1
chars[start:write] = chars[start:write][::-1]
return "".join(chars[:write])
def reverseWords_two_pointer(self, s: str) -> str:
words = []
i = len(s) - 1
while i >= 0:
while i >= 0 and s[i] == ' ':
i -= 1
if i < 0:
break
end = i
while i >= 0 and s[i] != ' ':
i -= 1
words.append(s[i + 1:end + 1])
return " ".join(words)
def reverseWords(self, s: str) -> str:
return " ".join(reversed(s.split()))
string reverseWordsConcat(string s) {
istringstream in(s);
string word, result;
while (in >> word) {
result = result.empty() ? word : word + " " + result;
}
return result;
}
string reverseWordsSplit(string s) {
istringstream in(s);
vector<string> words;
string word;
while (in >> word) {
words.push_back(word);
}
reverse(words.begin(), words.end());
string out;
out.reserve(s.size());
for (size_t i = 0; i < words.size(); ++i) {
if (i > 0) {
out += ' ';
}
out += words[i];
}
return out;
}
string reverseWords(string s) {
reverse(s.begin(), s.end());
const int n = static_cast<int>(s.size());
int write = 0;
for (int read = 0; read < n;) {
while (read < n && s[read] == ' ') {
++read;
}
if (read == n) {
break;
}
if (write > 0) { // single separator between words
s[write++] = ' ';
}
const int start = write;
while (read < n && s[read] != ' ') {
s[write++] = s[read++];
}
reverse(s.begin() + start, s.begin() + write);
}
s.resize(write);
return s;
}
pub fn reverse_words_concat(s: String) -> String {
let mut result = String::new();
for word in s.split_whitespace() {
if result.is_empty() {
result = word.to_string();
} else {
result = format!("{} {}", word, result);
}
}
result
}
pub fn reverse_words(s: String) -> String {
s.split_whitespace().rev().collect::<Vec<_>>().join(" ")
}
function reverseWordsConcat(s: string): string {
let result = "";
let word = "";
for (const ch of s + " ") {
if (ch !== " ") {
word += ch;
} else if (word.length > 0) {
result = result.length === 0 ? word : word + " " + result;
word = "";
}
}
return result;
}
function reverseWords(s: string): string {
return s
.split(" ")
.filter((word) => word.length > 0)
.reverse()
.join(" ");
}
func reverseWordsConcat(s string) string {
result := ""
word := ""
for i := 0; i <= len(s); i++ {
if i < len(s) && s[i] != ' ' {
word += string(s[i])
} else if word != "" {
if result == "" {
result = word
} else {
result = word + " " + result
}
word = ""
}
}
return result
}
func reverseWords(s string) string {
words := strings.Fields(s)
for i, j := 0, len(words)-1; i < j; i, j = i+1, j-1 {
words[i], words[j] = words[j], words[i]
}
return strings.Join(words, " ")
}
func reverseWordsConcat(_ s: String) -> String {
var result = ""
for word in s.split(separator: " ") {
result = result.isEmpty ? String(word) : String(word) + " " + result
}
return result
}
func reverseWords(_ s: String) -> String {
return s.split(separator: " ").reversed().joined(separator: " ")
}
7. Zigzag Conversion
Medium · LC 6
Given a string and a row count, write the characters in a zigzag pattern down and diagonally back up across the rows, then read the result row by row. Keep one bucket per row plus a step direction, dropping each character into the current row and flipping the step whenever the top or bottom row is hit. The pitfall is a single row: with nowhere to bounce, the flip logic would walk off the end, so that case is returned unchanged up front.
Walk an actual numRows x n grid — down a column, then diagonally up-right — and read the non-empty cells row by row. Instructive, but almost every cell is wasted padding.
Row 0 and the last row take one char per cycle; each middle row also takes the up-stroke char at j + cycle - 2*row. Kills the grid: emits the answer in order with no storage beyond the output, at the cost of numRows passes over s and fiddly index arithmetic.
For char i, p = i % cycle is its position in the cycle; positions past the bottom fold back up to row cycle - p. One sequential pass over s (better locality than Approach 2's jumps), no direction state to maintain.
Drop each char into its row, bouncing off the top and bottom rows. Same single pass as Approach 3 with the modular arithmetic replaced by a direction flag — nothing to derive, nothing to misremember. numRows == 1 has nowhere to bounce (the flip logic would walk off the end), so it is returned as-is up front.
Walk an actual numRows x n grid — down a column, then diagonally up-right — and read the non-empty cells row by row. Instructive, but almost every cell is wasted padding.
The column never mattered — only the row order did. Drop each char into its row while bouncing off the top and bottom rows, then concatenate the rows. numRows == 1 has nowhere to bounce, so it is returned as-is.
Walk an actual num_rows x n grid — down a column, then diagonally up-right — and read the non-empty cells row by row. Instructive, but almost every cell is wasted padding.
The column never mattered — only the row order did. Drop each char into its row while bouncing off the top and bottom rows, then concatenate the rows. num_rows == 1 has nowhere to bounce, so it is returned as-is.
Walk an actual numRows x n grid — down a column, then diagonally up-right — and read the non-empty cells row by row. Instructive, but almost every cell is wasted padding.
The column never mattered — only the row order did. Drop each char into its row while bouncing off the top and bottom rows, then concatenate the rows. numRows === 1 has nowhere to bounce, so it is returned as-is.
Walk an actual numRows x n grid — down a column, then diagonally up-right — and read the non-empty cells row by row. Instructive, but almost every cell is wasted padding.
The column never mattered — only the row order did. Drop each char into its row while bouncing off the top and bottom rows, then concatenate the rows. numRows == 1 has nowhere to bounce, so it is returned as-is.
Walk an actual numRows x n grid — down a column, then diagonally up-right — and read the non-empty cells row by row. Instructive, but almost every cell is wasted padding.
The column never mattered — only the row order did. Drop each char into its row while bouncing off the top and bottom rows, then concatenate the rows. numRows == 1 has nowhere to bounce, so it is returned as-is.
def convert_grid(self, s: str, numRows: int) -> str:
if numRows == 1:
return s
n = len(s)
grid = [[''] * n for _ in range(numRows)]
row = col = 0
down = True
for char in s:
grid[row][col] = char
if down:
if row + 1 == numRows:
down = False
row -= 1
col += 1
else:
row += 1
elif row == 0:
down = True
row += 1
else:
row -= 1
col += 1
return "".join(char for r in grid for char in r if char)
def convert_jump(self, s: str, numRows: int) -> str:
if numRows == 1:
return s
n = len(s)
cycle = 2 * numRows - 2
out = []
for row in range(numRows):
for j in range(row, n, cycle):
out.append(s[j])
up = j + cycle - 2 * row
if 0 < row < numRows - 1 and up < n:
out.append(s[up])
return "".join(out)
def convert_fold(self, s: str, numRows: int) -> str:
if numRows == 1:
return s
cycle = 2 * numRows - 2
rows = [[] for _ in range(numRows)]
for i, char in enumerate(s):
p = i % cycle
rows[p if p < numRows else cycle - p].append(char)
return "".join("".join(r) for r in rows)
def convert(self, s: str, numRows: int) -> str:
if numRows == 1:
return s
rows = [[] for _ in range(numRows)]
row, step = 0, 1
for char in s:
rows[row].append(char)
if row == 0:
step = 1
elif row == numRows - 1:
step = -1
row += step
return "".join("".join(r) for r in rows)
string convertGrid(string s, int numRows) {
if (numRows == 1) {
return s;
}
const int n = static_cast<int>(s.size());
vector<vector<char>> grid(numRows, vector<char>(n, '\0'));
int row = 0, col = 0;
bool down = true;
for (char c : s) {
grid[row][col] = c;
if (down) {
if (row + 1 == numRows) {
down = false;
--row;
++col;
} else {
++row;
}
} else if (row == 0) {
down = true;
++row;
} else {
--row;
++col;
}
}
string out;
out.reserve(s.size());
for (const auto& r : grid) {
for (char c : r) {
if (c != '\0') {
out += c;
}
}
}
return out;
}
string convert(string s, int numRows) {
if (numRows == 1) {
return s;
}
vector<string> rows(numRows);
int row = 0, step = 1;
for (char c : s) {
rows[row] += c;
if (row == 0) {
step = 1;
} else if (row == numRows - 1) {
step = -1;
}
row += step;
}
string out;
out.reserve(s.size());
for (const string& r : rows) {
out += r;
}
return out;
}
pub fn convert_grid(s: String, num_rows: i32) -> String {
let num_rows = num_rows as usize;
if num_rows == 1 {
return s;
}
let bytes = s.as_bytes();
let n = bytes.len();
let mut grid = vec![vec![0u8; n]; num_rows];
let (mut row, mut col) = (0usize, 0usize);
let mut down = true;
for &b in bytes {
grid[row][col] = b;
if down {
if row + 1 == num_rows {
down = false;
row -= 1;
col += 1;
} else {
row += 1;
}
} else if row == 0 {
down = true;
row += 1;
} else {
row -= 1;
col += 1;
}
}
let mut out = Vec::with_capacity(n);
for r in &grid {
for &b in r {
if b != 0 {
out.push(b);
}
}
}
String::from_utf8(out).unwrap()
}
pub fn convert(s: String, num_rows: i32) -> String {
let num_rows = num_rows as usize;
if num_rows == 1 {
return s;
}
let mut rows = vec![String::new(); num_rows];
let mut row = 0usize;
let mut down = true;
for c in s.chars() {
rows[row].push(c);
if row == 0 {
down = true;
} else if row == num_rows - 1 {
down = false;
}
if down {
row += 1;
} else {
row -= 1;
}
}
rows.concat()
}
function convertGrid(s: string, numRows: number): string {
if (numRows === 1) {
return s;
}
const n = s.length;
const grid: string[][] = Array.from({ length: numRows }, () =>
new Array<string>(n).fill(""),
);
let row = 0;
let col = 0;
let down = true;
for (const ch of s) {
grid[row][col] = ch;
if (down) {
if (row + 1 === numRows) {
down = false;
row--;
col++;
} else {
row++;
}
} else if (row === 0) {
down = true;
row++;
} else {
row--;
col++;
}
}
let out = "";
for (const r of grid) {
for (const ch of r) {
out += ch;
}
}
return out;
}
function convert(s: string, numRows: number): string {
if (numRows === 1) {
return s;
}
const rows: string[] = new Array(numRows).fill("");
let row = 0;
let step = 1;
for (const ch of s) {
rows[row] += ch;
if (row === 0) {
step = 1;
} else if (row === numRows - 1) {
step = -1;
}
row += step;
}
return rows.join("");
}
func convertGrid(s string, numRows int) string {
if numRows == 1 {
return s
}
n := len(s)
grid := make([][]byte, numRows)
for r := range grid {
grid[r] = make([]byte, n)
}
row, col := 0, 0
down := true
for i := 0; i < n; i++ {
grid[row][col] = s[i]
if down {
if row+1 == numRows {
down = false
row--
col++
} else {
row++
}
} else if row == 0 {
down = true
row++
} else {
row--
col++
}
}
out := make([]byte, 0, n)
for _, r := range grid {
for _, b := range r {
if b != 0 {
out = append(out, b)
}
}
}
return string(out)
}
func convert(s string, numRows int) string {
if numRows == 1 {
return s
}
rows := make([][]byte, numRows)
row, step := 0, 1
for i := 0; i < len(s); i++ {
rows[row] = append(rows[row], s[i])
if row == 0 {
step = 1
} else if row == numRows-1 {
step = -1
}
row += step
}
out := make([]byte, 0, len(s))
for _, r := range rows {
out = append(out, r...)
}
return string(out)
}
func convertGrid(_ s: String, _ numRows: Int) -> String {
if numRows == 1 {
return s
}
let chars = Array(s)
let n = chars.count
var grid = Array(repeating: [Character?](repeating: nil, count: n), count: numRows)
var row = 0
var col = 0
var down = true
for ch in chars {
grid[row][col] = ch
if down {
if row + 1 == numRows {
down = false
row -= 1
col += 1
} else {
row += 1
}
} else if row == 0 {
down = true
row += 1
} else {
row -= 1
col += 1
}
}
var out = ""
out.reserveCapacity(n)
for r in grid {
for case let ch? in r {
out.append(ch)
}
}
return out
}
func convert(_ s: String, _ numRows: Int) -> String {
if numRows == 1 {
return s
}
var rows = [String](repeating: "", count: numRows)
var row = 0
var step = 1
for ch in s {
rows[row].append(ch)
if row == 0 {
step = 1
} else if row == numRows - 1 {
step = -1
}
row += step
}
return rows.joined()
}
8. Find the Index of the First Occurrence in a String
Easy · LC 28
Given a haystack string and a needle string, return the index of the needle's first occurrence, or -1 if it never appears. Use Knuth-Morris-Pratt: precompute for the needle the table of longest proper prefixes that are also suffixes, then scan the haystack once, letting the needle pointer fall back through the table on every mismatch. The payoff is that no haystack character is ever re-read, which replaces the quadratic worst case of naive alignment-by-alignment comparison with O(n + m).
Try every alignment and compare char by char, bailing on the first mismatch. The worst case needs pathological inputs ("aaaa..." vs "aaab"); for interview-sized strings this is the clearest start. A needle longer than the haystack yields an empty range -> -1.
What you would write outside an interview: CPython uses a tuned Crochemore-Perrin two-way search under the hood, so this is already linear. The catch is that the algorithm stays a black box — the next rung builds the linear-time machinery by hand.
Precompute the longest-proper-prefix-that-is-also-a-suffix table for the needle, then scan the haystack once; on a mismatch the needle pointer falls back via the table and no haystack char is re-read.
The first thing most people write: carve out the m-char substring at every start and compare it to the needle. Correct, but every probe allocates a fresh copy just to throw it away.
Same alignments, but compare in place char by char and bail on the first mismatch — no allocation per probe. A needle longer than the haystack makes the loop bound negative, so no alignment is tried and we return -1.
Try every start index — even the tail ones where the needle can no longer fit — and compare byte by byte behind an explicit bounds check. The wasted tail probes and the per-step bound test are what the next rung trims away.
Reject a too-long needle up front (which also keeps the n - m subtraction from underflowing), then compare the m-byte window at each remaining alignment with slice equality — a memcmp under the hood, with no per-step bounds checks.
The first thing most people write: carve out the m-char slice at every start and compare it to the needle. Correct, but every probe builds a fresh string just to throw it away.
Same alignments, but compare in place char by char and bail on the first mismatch — no slice per probe. A needle longer than the haystack leaves no alignment to try, so the loop never runs and we return -1.
Try every start index — even the tail ones where the needle can no longer fit — and compare byte by byte behind an explicit bounds check. The wasted tail probes and the per-step bound test are what the next rung trims away.
Bound the loop so only alignments where the needle fits are tried; the inner compare then needs no bounds check and bails on the first mismatch. A needle longer than the haystack leaves no alignment to try, so the loop never runs and we return -1.
The first thing most people write: copy out the m-char window at every start and compare whole arrays. Correct, but every probe materializes a fresh array just to throw it away.
Same alignments, but compare in place char by char and bail on the first mismatch — no copy per probe. A needle longer than the haystack is rejected up front (it also keeps the range bound non-negative).
def strStr_sliding(self, haystack: str, needle: str) -> int:
n, m = len(haystack), len(needle)
for start in range(n - m + 1):
j = 0
while j < m and haystack[start + j] == needle[j]:
j += 1
if j == m:
return start
return -1
def strStr_find(self, haystack: str, needle: str) -> int:
return haystack.find(needle)
def strStr(self, haystack: str, needle: str) -> int:
m = len(needle)
if m == 0:
return 0
# lps[i] = length of the longest proper prefix of needle[:i+1]
# that is also a suffix of it.
lps = [0] * m
length = 0
for i in range(1, m):
while length and needle[i] != needle[length]:
length = lps[length - 1]
if needle[i] == needle[length]:
length += 1
lps[i] = length
j = 0
for i, char in enumerate(haystack):
while j and char != needle[j]:
j = lps[j - 1]
if char == needle[j]:
j += 1
if j == m:
return i - m + 1
return -1
int strStrSubstr(string haystack, string needle) {
const int n = static_cast<int>(haystack.size());
const int m = static_cast<int>(needle.size());
for (int start = 0; start + m <= n; ++start) {
if (haystack.substr(start, m) == needle) {
return start;
}
}
return -1;
}
int strStr(string haystack, string needle) {
const int n = static_cast<int>(haystack.size());
const int m = static_cast<int>(needle.size());
for (int start = 0; start + m <= n; ++start) {
int j = 0;
while (j < m && haystack[start + j] == needle[j]) {
++j;
}
if (j == m) {
return start;
}
}
return -1;
}
pub fn str_str_nested(haystack: String, needle: String) -> i32 {
let h = haystack.as_bytes();
let nd = needle.as_bytes();
if nd.is_empty() {
return 0;
}
for start in 0..h.len() {
let mut j = 0;
while start + j < h.len() && j < nd.len() && h[start + j] == nd[j] {
j += 1;
}
if j == nd.len() {
return start as i32;
}
}
-1
}
pub fn str_str(haystack: String, needle: String) -> i32 {
let h = haystack.as_bytes();
let nd = needle.as_bytes();
let (n, m) = (h.len(), nd.len());
if m > n {
return -1;
}
for start in 0..=(n - m) {
if &h[start..start + m] == nd {
return start as i32;
}
}
-1
}
function strStrSlice(haystack: string, needle: string): number {
const n = haystack.length;
const m = needle.length;
for (let start = 0; start + m <= n; start++) {
if (haystack.slice(start, start + m) === needle) {
return start;
}
}
return -1;
}
function strStr(haystack: string, needle: string): number {
const n = haystack.length;
const m = needle.length;
for (let start = 0; start + m <= n; start++) {
let j = 0;
while (j < m && haystack[start + j] === needle[j]) {
j++;
}
if (j === m) {
return start;
}
}
return -1;
}
func strStrNested(haystack string, needle string) int {
n, m := len(haystack), len(needle)
if m == 0 {
return 0
}
for start := 0; start < n; start++ {
j := 0
for start+j < n && j < m && haystack[start+j] == needle[j] {
j++
}
if j == m {
return start
}
}
return -1
}
func strStr(haystack string, needle string) int {
n, m := len(haystack), len(needle)
for start := 0; start+m <= n; start++ {
j := 0
for j < m && haystack[start+j] == needle[j] {
j++
}
if j == m {
return start
}
}
return -1
}
func strStrSlice(_ haystack: String, _ needle: String) -> Int {
let h = Array(haystack)
let nd = Array(needle)
if nd.count > h.count {
return -1
}
for start in 0...(h.count - nd.count) {
if Array(h[start..<start + nd.count]) == nd {
return start
}
}
return -1
}
func strStr(_ haystack: String, _ needle: String) -> Int {
let h = Array(haystack)
let nd = Array(needle)
if nd.count > h.count {
return -1
}
for start in 0...(h.count - nd.count) {
var j = 0
while j < nd.count && h[start + j] == nd[j] {
j += 1
}
if j == nd.count {
return start
}
}
return -1
}
9. Text Justification
Hard · LC 68
Given a list of words and a maximum width, pack the words greedily into lines and pad each line with spaces to exactly that width, distributing extra spaces left-heavy between the words. Pack words until the next one no longer fits, then size the gaps with divmod, which gives the base gap width plus how many of the leftmost gaps receive one extra space, avoiding any per-space loop. The pitfall is the exceptions: the last line and any line holding a single word are left-justified and padded on the right instead of spread.
Pack by index ranges, then deal the leftover spaces one at a time onto the gaps from the left until none remain. Correct, but the O(W) dribble per line is what Approach 2 collapses into one divmod.
divmod(spaces, gaps) gives the base gap width plus how many of the leftmost gaps get one extra space — no per-space loops needed.
Pack by index ranges, then deal the leftover spaces one at a time onto the gaps from the left until none remain. Correct, but the O(W) dribble per line is what Approach 2 collapses into a div/mod.
(maxWidth - chars) div/mod gaps gives the base gap width plus how many of the leftmost gaps get one extra space — no per-space loops.
Pack by index ranges, then deal the leftover spaces one at a time onto the gaps from the left until none remain. Correct, but the O(W) dribble per line is what Approach 2 collapses into a div/mod.
(width - chars) div/mod gaps gives the base gap width plus how many of the leftmost gaps get one extra space — no per-space loops.
Pack by index ranges, then deal the leftover spaces one at a time onto the gaps from the left until none remain. Correct, but the O(W) dribble per line is what Approach 2 collapses into a div/mod.
(maxWidth - letters) div/mod gaps gives the base gap width plus how many of the leftmost gaps get one extra space — no per-space loops.
Pack by index ranges, then deal the leftover spaces one at a time onto the gaps from the left until none remain. Correct, but the O(W) dribble per line is what Approach 2 collapses into a div/mod.
(maxWidth - chars) div/mod gaps gives the base gap width plus how many of the leftmost gaps get one extra space — no per-space loops.
Pack by index ranges, then deal the leftover spaces one at a time onto the gaps from the left until none remain. Correct, but the O(W) dribble per line is what Approach 2 collapses into a div/mod.
(maxWidth - chars) div/mod gaps gives the base gap width plus how many of the leftmost gaps get one extra space — no per-space loops.
def fullJustify_roundrobin(self, words: List[str], maxWidth: int) -> List[str]:
lines: List[str] = []
i, n = 0, len(words)
while i < n:
j, width = i + 1, len(words[i]) # width counts 1 space per gap
while j < n and width + 1 + len(words[j]) <= maxWidth:
width += 1 + len(words[j])
j += 1
chunk = words[i:j]
if j == n or len(chunk) == 1: # last line or single word
lines.append(" ".join(chunk).ljust(maxWidth))
else:
gaps = [1] * (len(chunk) - 1)
spare, k = maxWidth - width, 0
while spare:
gaps[k % len(gaps)] += 1
k += 1
spare -= 1
pieces = [chunk[0]]
for gap, word in zip(gaps, chunk[1:]):
pieces.append(" " * gap)
pieces.append(word)
lines.append("".join(pieces))
i = j
return lines
def fullJustify(self, words: List[str], maxWidth: int) -> List[str]:
lines: List[str] = []
line: List[str] = [] # words packed into the current line
chars = 0 # letters only, no spaces
for word in words:
# len(line) = one mandatory space after each word already packed
if chars + len(line) + len(word) > maxWidth:
lines.append(self._justify(line, chars, maxWidth))
line, chars = [], 0
line.append(word)
chars += len(word)
lines.append(" ".join(line).ljust(maxWidth)) # last line: flush left
return lines
@staticmethod
def _justify(line: List[str], chars: int, maxWidth: int) -> str:
if len(line) == 1: # no gaps -> pad right
return line[0].ljust(maxWidth)
base, extra = divmod(maxWidth - chars, len(line) - 1)
pieces = []
for i, word in enumerate(line[:-1]):
pieces.append(word)
pieces.append(" " * (base + (1 if i < extra else 0)))
pieces.append(line[-1])
return "".join(pieces)
vector<string> fullJustifyRoundRobin(vector<string>& words, int maxWidth) {
vector<string> lines;
int n = (int)words.size();
int i = 0;
while (i < n) {
int j = i + 1;
int width = (int)words[i].size(); // counts 1 space per gap
while (j < n && width + 1 + (int)words[j].size() <= maxWidth) {
width += 1 + (int)words[j].size();
++j;
}
if (j == n || j - i == 1) { // last line or single word
string line = words[i];
for (int w = i + 1; w < j; ++w) {
line += ' ';
line += words[w];
}
line.append(maxWidth - (int)line.size(), ' ');
lines.push_back(line);
} else {
vector<int> gaps(j - i - 1, 1);
int spare = maxWidth - width;
for (size_t k = 0; spare > 0; --spare, ++k)
++gaps[k % gaps.size()];
string line = words[i];
for (int w = i + 1; w < j; ++w) {
line.append(gaps[w - i - 1], ' ');
line += words[w];
}
lines.push_back(line);
}
i = j;
}
return lines;
}
vector<string> fullJustify(vector<string>& words, int maxWidth) {
vector<string> lines;
vector<string> line; // words packed into the current line
int chars = 0; // letters only, no spaces
for (const string& word : words) {
// line.size() = one mandatory space after each packed word
if (chars + (int)line.size() + (int)word.size() > maxWidth) {
lines.push_back(justify(line, chars, maxWidth));
line.clear();
chars = 0;
}
line.push_back(word);
chars += (int)word.size();
}
string last; // final line: single spaces, pad right
for (size_t i = 0; i < line.size(); ++i) {
if (i > 0) last += ' ';
last += line[i];
}
last.append(maxWidth - (int)last.size(), ' ');
lines.push_back(last);
return lines;
}
private:
static string justify(const vector<string>& line, int chars, int maxWidth) {
if (line.size() == 1) { // no gaps -> pad right
string s = line[0];
s.append(maxWidth - (int)s.size(), ' ');
return s;
}
int gaps = (int)line.size() - 1;
int base = (maxWidth - chars) / gaps;
int extra = (maxWidth - chars) % gaps;
string s;
for (int i = 0; i < gaps; ++i) {
s += line[i];
s.append(base + (i < extra ? 1 : 0), ' ');
}
s += line.back();
return s;
}
pub fn full_justify_round_robin(words: Vec<String>, max_width: i32) -> Vec<String> {
let width = max_width as usize;
let mut lines: Vec<String> = Vec::new();
let n = words.len();
let mut i = 0;
while i < n {
let mut j = i + 1;
let mut used = words[i].len(); // counts 1 space per gap
while j < n && used + 1 + words[j].len() <= width {
used += 1 + words[j].len();
j += 1;
}
let chunk = &words[i..j];
if j == n || chunk.len() == 1 {
// last line or single word
let mut line = chunk.join(" ");
line.push_str(&" ".repeat(width - line.len()));
lines.push(line);
} else {
let slots = chunk.len() - 1;
let mut gaps = vec![1usize; slots];
let mut spare = width - used;
let mut k = 0;
while spare > 0 {
gaps[k % slots] += 1;
k += 1;
spare -= 1;
}
let mut line = chunk[0].clone();
for (gap, word) in gaps.iter().zip(&chunk[1..]) {
line.push_str(&" ".repeat(*gap));
line.push_str(word);
}
lines.push(line);
}
i = j;
}
lines
}
pub fn full_justify(words: Vec<String>, max_width: i32) -> Vec<String> {
let width = max_width as usize;
let mut lines: Vec<String> = Vec::new();
let mut line: Vec<&str> = Vec::new(); // words packed into this line
let mut chars = 0usize; // letters only, no spaces
for word in &words {
// line.len() = one mandatory space after each packed word
if chars + line.len() + word.len() > width {
lines.push(Self::justify(&line, chars, width));
line.clear();
chars = 0;
}
line.push(word);
chars += word.len();
}
let mut last = line.join(" "); // final line: single spaces, pad right
last.push_str(&" ".repeat(width - last.len()));
lines.push(last);
lines
}
fn justify(line: &[&str], chars: usize, width: usize) -> String {
if line.len() == 1 {
// no gaps -> pad right
return format!("{}{}", line[0], " ".repeat(width - line[0].len()));
}
let gaps = line.len() - 1;
let base = (width - chars) / gaps;
let extra = (width - chars) % gaps;
let mut s = String::new();
for (i, word) in line[..gaps].iter().enumerate() {
s.push_str(word);
s.push_str(&" ".repeat(base + usize::from(i < extra)));
}
s.push_str(line[gaps]);
s
}
function fullJustifyRoundRobin(words: string[], maxWidth: number): string[] {
const lines: string[] = [];
const n = words.length;
let i = 0;
while (i < n) {
let j = i + 1;
let width = words[i].length; // counts 1 space per gap
while (j < n && width + 1 + words[j].length <= maxWidth) {
width += 1 + words[j].length;
j++;
}
if (j === n || j - i === 1) { // last line or single word
lines.push(words.slice(i, j).join(" ").padEnd(maxWidth));
} else {
const gaps = new Array<number>(j - i - 1).fill(1);
for (let spare = maxWidth - width, k = 0; spare > 0; spare--, k++) {
gaps[k % gaps.length]++;
}
let line = words[i];
for (let w = i + 1; w < j; w++) {
line += " ".repeat(gaps[w - i - 1]) + words[w];
}
lines.push(line);
}
i = j;
}
return lines;
}
function fullJustify(words: string[], maxWidth: number): string[] {
const lines: string[] = [];
let line: string[] = []; // words packed into the current line
let chars = 0; // letters only, no spaces
const justify = (packed: string[], letters: number): string => {
if (packed.length === 1) { // no gaps -> pad right
return packed[0].padEnd(maxWidth);
}
const gaps = packed.length - 1;
const base = Math.floor((maxWidth - letters) / gaps);
const extra = (maxWidth - letters) % gaps;
let s = "";
for (let i = 0; i < gaps; i++) {
s += packed[i];
s += " ".repeat(base + (i < extra ? 1 : 0));
}
return s + packed[gaps];
};
for (const word of words) {
// line.length = one mandatory space after each packed word
if (chars + line.length + word.length > maxWidth) {
lines.push(justify(line, chars));
line = [];
chars = 0;
}
line.push(word);
chars += word.length;
}
lines.push(line.join(" ").padEnd(maxWidth)); // last line: flush left
return lines;
}
func fullJustifyRoundRobin(words []string, maxWidth int) []string {
var lines []string
n := len(words)
for i := 0; i < n; {
j, width := i+1, len(words[i]) // width counts 1 space per gap
for j < n && width+1+len(words[j]) <= maxWidth {
width += 1 + len(words[j])
j++
}
if j == n || j-i == 1 { // last line or single word
line := strings.Join(words[i:j], " ")
lines = append(lines, line+strings.Repeat(" ", maxWidth-len(line)))
} else {
gaps := make([]int, j-i-1)
for g := range gaps {
gaps[g] = 1
}
for spare, k := maxWidth-width, 0; spare > 0; spare-- {
gaps[k%len(gaps)]++
k++
}
var b strings.Builder
b.WriteString(words[i])
for w := i + 1; w < j; w++ {
b.WriteString(strings.Repeat(" ", gaps[w-i-1]))
b.WriteString(words[w])
}
lines = append(lines, b.String())
}
i = j
}
return lines
}
func fullJustify(words []string, maxWidth int) []string {
var lines []string
var line []string // words packed into the current line
chars := 0 // letters only, no spaces
for _, word := range words {
// len(line) = one mandatory space after each packed word
if chars+len(line)+len(word) > maxWidth {
lines = append(lines, justifyLine(line, chars, maxWidth))
line, chars = nil, 0
}
line = append(line, word)
chars += len(word)
}
last := strings.Join(line, " ") // final line: single spaces, pad right
last += strings.Repeat(" ", maxWidth-len(last))
return append(lines, last)
}
func justifyLine(line []string, chars, maxWidth int) string {
if len(line) == 1 { // no gaps -> pad right
return line[0] + strings.Repeat(" ", maxWidth-len(line[0]))
}
gaps := len(line) - 1
base, extra := (maxWidth-chars)/gaps, (maxWidth-chars)%gaps
var b strings.Builder
for i, word := range line[:gaps] {
b.WriteString(word)
width := base
if i < extra {
width++
}
b.WriteString(strings.Repeat(" ", width))
}
b.WriteString(line[gaps])
return b.String()
}
func fullJustifyRoundRobin(_ words: [String], _ maxWidth: Int) -> [String] {
var lines: [String] = []
let n = words.count
var i = 0
while i < n {
var j = i + 1
var width = words[i].count // counts 1 space per gap
while j < n && width + 1 + words[j].count <= maxWidth {
width += 1 + words[j].count
j += 1
}
if j == n || j - i == 1 { // last line or single word
let line = words[i..<j].joined(separator: " ")
lines.append(line + String(repeating: " ", count: maxWidth - line.count))
} else {
var gaps = [Int](repeating: 1, count: j - i - 1)
var spare = maxWidth - width
var k = 0
while spare > 0 {
gaps[k % gaps.count] += 1
k += 1
spare -= 1
}
var line = words[i]
for w in (i + 1)..<j {
line += String(repeating: " ", count: gaps[w - i - 1])
line += words[w]
}
lines.append(line)
}
i = j
}
return lines
}
func fullJustify(_ words: [String], _ maxWidth: Int) -> [String] {
var lines: [String] = []
var line: [String] = [] // words packed into the current line
var chars = 0 // letters only, no spaces
func justify(_ line: [String], _ chars: Int) -> String {
if line.count == 1 { // no gaps -> pad right
return line[0] + String(repeating: " ", count: maxWidth - line[0].count)
}
let gaps = line.count - 1
let base = (maxWidth - chars) / gaps
let extra = (maxWidth - chars) % gaps
var s = ""
for i in 0..<gaps {
s += line[i]
s += String(repeating: " ", count: base + (i < extra ? 1 : 0))
}
s += line[gaps]
return s
}
for word in words {
// line.count = one mandatory space after each packed word
if chars + line.count + word.count > maxWidth {
lines.append(justify(line, chars))
line = []
chars = 0
}
line.append(word)
chars += word.count
}
let last = line.joined(separator: " ") // final line: flush left
lines.append(last + String(repeating: " ", count: maxWidth - last.count))
return lines
}
Two Pointers
10. Is Subsequence
Easy · LC 392
Given two strings, determine whether the first is a subsequence of the second. Walk the containing string once with a pointer into the candidate, advancing the pointer only when the characters match; the candidate is a subsequence exactly when its pointer reaches the end. The trick is that greedy matching is safe, since taking the earliest usable occurrence of each character never hurts, while the follow-up with huge numbers of queries against one string calls for precomputed occurrence lists and binary search instead.
Chase each char of s to its next occurrence in t; every search resumes right after the previous match, so t is walked only once in total — but that single pass is hidden inside the find() calls.
The same greedy pass in one line: iter(t) is consumed left to right, so each membership test resumes where the previous match stopped — no manual position bookkeeping at all.
Approaches 1-2 rescan t for every query. For billions of s queries against one t, precompute where each char occurs, then binary-search for the next usable occurrence per char of s. (Rebuilt per call here; in the follow-up you build it once.)
For a single query nothing beats one explicit walk of t, advancing in s only on a match; s is a subsequence iff its pointer reaches the end. The invariant Approaches 1-2 hide, spelled out.
Chase each char of s to its next occurrence in t; every search resumes right after the previous match, so t is walked only once in total — but that single pass is hidden inside the find() calls.
The same single pass made explicit: walk t once and advance in s only on a match; s is a subsequence iff its pointer reaches the end. No library calls, and the invariant is plain to see.
Chase each char of s to its next occurrence in t; every search resumes right after the previous match, so t is walked only once in total — but that single pass is hidden inside the searches.
The same single pass made explicit: walk t once and advance in s only on a match; s is a subsequence iff its pointer reaches the end. No library calls, and the invariant is plain to see.
Chase each char of s to its next occurrence in t; every search resumes right after the previous match, so t is walked only once in total — but that single pass is hidden inside the indexOf calls.
The same single pass made explicit: walk t once and advance in s only on a match; s is a subsequence iff its pointer reaches the end. No library calls, and the invariant is plain to see.
Chase each char of s to its next occurrence in t; every search resumes right after the previous match, so t is walked only once in total — but that single pass is hidden inside the IndexByte calls.
The same single pass made explicit: walk t once and advance in s only on a match; s is a subsequence iff its pointer reaches the end. No library calls, and the invariant is plain to see.
Chase each char of s to its next occurrence in t; every search resumes right after the previous match, so t is walked only once in total — but that single pass is hidden inside the searches.
The same single pass made explicit: walk t once and advance in s only on a match; s is a subsequence iff its pointer reaches the end. No library calls, and the invariant is plain to see.
def isSubsequence_find_scan(self, s: str, t: str) -> bool:
pos = 0
for ch in s:
pos = t.find(ch, pos)
if pos == -1:
return False
pos += 1
return True
def isSubsequence_iterator(self, s: str, t: str) -> bool:
it = iter(t)
return all(ch in it for ch in s)
def isSubsequence_binary_search(self, s: str, t: str) -> bool:
positions: Dict[str, List[int]] = {}
for idx, ch in enumerate(t):
positions.setdefault(ch, []).append(idx)
prev = -1 # last index of t consumed so far
for ch in s:
idx_list = positions.get(ch)
if not idx_list:
return False
nxt = bisect_right(idx_list, prev)
if nxt == len(idx_list): # no occurrence left after prev
return False
prev = idx_list[nxt]
return True
def isSubsequence(self, s: str, t: str) -> bool:
i = 0
for ch in t:
if i < len(s) and s[i] == ch:
i += 1
return i == len(s)
bool isSubsequenceIndexScan(string s, string t) {
size_t pos = 0;
for (char ch : s) {
pos = t.find(ch, pos);
if (pos == string::npos) return false;
++pos;
}
return true;
}
bool isSubsequence(string s, string t) {
size_t i = 0;
for (char ch : t) {
if (i < s.size() && s[i] == ch) ++i;
}
return i == s.size();
}
pub fn is_subsequence_index_scan(s: String, t: String) -> bool {
let t = t.as_bytes();
let mut pos = 0usize;
for &ch in s.as_bytes() {
match t[pos..].iter().position(|&b| b == ch) {
Some(off) => pos += off + 1,
None => return false,
}
}
true
}
pub fn is_subsequence(s: String, t: String) -> bool {
let s = s.as_bytes();
let mut i = 0;
for &ch in t.as_bytes() {
if i < s.len() && s[i] == ch {
i += 1;
}
}
i == s.len()
}
function isSubsequenceIndexScan(s: string, t: string): boolean {
let pos = 0;
for (const ch of s) {
pos = t.indexOf(ch, pos);
if (pos < 0) {
return false;
}
pos++;
}
return true;
}
function isSubsequence(s: string, t: string): boolean {
let i = 0;
for (let j = 0; j < t.length; j++) {
if (i < s.length && s[i] === t[j]) {
i++;
}
}
return i === s.length;
}
func isSubsequenceIndexScan(s string, t string) bool {
pos := 0
for i := 0; i < len(s); i++ {
off := strings.IndexByte(t[pos:], s[i])
if off < 0 {
return false
}
pos += off + 1
}
return true
}
func isSubsequence(s string, t string) bool {
i := 0
for j := 0; j < len(t); j++ {
if i < len(s) && s[i] == t[j] {
i++
}
}
return i == len(s)
}
func isSubsequenceIndexScan(_ s: String, _ t: String) -> Bool {
let tChars = Array(t)
var pos = 0
for ch in s {
guard let idx = tChars[pos...].firstIndex(of: ch) else {
return false
}
pos = idx + 1
}
return true
}
func isSubsequence(_ s: String, _ t: String) -> Bool {
let sChars = Array(s)
var i = 0
for ch in t {
if i < sChars.count && sChars[i] == ch {
i += 1
}
}
return i == sChars.count
}
Sliding Window
11. Substring with Concatenation of All Words
Hard · LC 30
Given a string and a list of words that all share the same length, find every starting index of a substring formed by concatenating all the words in some order. Since every valid start is congruent to some offset modulo the word length, slide a word-count window word by word from each such offset, shrinking from the left whenever some word's count exceeds its quota and recording the left edge whenever the window holds exactly all the words. The trick is working at word granularity, with a hard window reset on any word not in the list, so each word slot is counted only about twice per offset instead of the brute force's full recount at every start.
Cut the next k words at every start and compare multisets. Simple and correct, but rebuilds the whole count at each index — fine at these test sizes, times out on LeetCode's 10^4-char strings.
Every valid start is congruent to some offset mod L, so slide a word-count window word-by-word from each of the L offsets, shrinking from the left whenever some word count exceeds its quota — each word slot is now counted twice per offset instead of once per start.
Cut the next k words at every start and burn them off a copy of the quota map. Simple and correct, but rebuilds the whole count at each index — fine here, times out on LeetCode's 10^4 chars.
Every valid start is congruent to some offset mod L, so slide a word-count window word-by-word from each of the L offsets, shrinking from the left when a word count exceeds its quota — each word slot is counted twice per offset instead of once per start.
Cut the next k words at every start and burn them off a copy of the quota map. Simple and correct, but rebuilds the whole count at each index — fine here, times out on LeetCode's 10^4 chars.
Every valid start is congruent to some offset mod L, so slide a word-count window word-by-word from each of the L offsets, shrinking from the left when a word count exceeds its quota — each word slot is counted twice per offset instead of once per start.
Cut the next k words at every start and burn them off a copy of the quota map. Simple and correct, but rebuilds the whole count at each index — fine here, times out on LeetCode's 10^4 chars.
Every valid start is congruent to some offset mod L, so slide a word-count window word-by-word from each of the L offsets, shrinking from the left when a word count exceeds its quota — each word slot is counted twice per offset instead of once per start.
Cut the next k words at every start and burn them off a copy of the quota map. Simple and correct, but rebuilds the whole count at each index — fine here, times out on LeetCode's 10^4 chars.
Every valid start is congruent to some offset mod L, so slide a word-count window word-by-word from each of the L offsets, shrinking from the left when a word count exceeds its quota — each word slot is counted twice per offset instead of once per start.
Cut the next k words at every start and burn them off a copy of the quota map. Simple and correct, but rebuilds the whole count at each index — fine here, times out on LeetCode's 10^4 chars.
Every valid start is congruent to some offset mod L, so slide a word-count window word-by-word from each of the L offsets, shrinking from the left when a word count exceeds its quota — each word slot is counted twice per offset instead of once per start.
def findSubstring_counter(self, s: str, words: List[str]) -> List[int]:
k, L = len(words), len(words[0])
total = k * L
need = Counter(words)
result: List[int] = []
for start in range(len(s) - total + 1):
seen = Counter(s[i:i + L] for i in range(start, start + total, L))
if seen == need:
result.append(start)
return result
def findSubstring(self, s: str, words: List[str]) -> List[int]:
n, k, L = len(s), len(words), len(words[0])
total = k * L
if n < total:
return []
need = Counter(words)
result: List[int] = []
for offset in range(L):
window: Counter = Counter()
matched = 0 # words currently inside the window
left = offset
for right in range(offset, n - L + 1, L):
word = s[right:right + L]
if word not in need:
window.clear() # hard reset past the junk word
matched = 0
left = right + L
continue
window[word] += 1
matched += 1
while window[word] > need[word]: # too many copies -> shrink
window[s[left:left + L]] -= 1
left += L
matched -= 1
if matched == k:
result.append(left)
window[s[left:left + L]] -= 1 # slide one word forward
left += L
matched -= 1
return result
vector<int> findSubstringBruteForce(string s, vector<string>& words) {
int n = (int)s.size();
int k = (int)words.size();
int len = (int)words[0].size();
int total = k * len;
unordered_map<string, int> need;
for (const string& w : words) ++need[w];
vector<int> result;
for (int start = 0; start + total <= n; ++start) {
unordered_map<string, int> remaining = need;
bool ok = true;
for (int pos = start; pos < start + total; pos += len) {
auto it = remaining.find(s.substr(pos, len));
if (it == remaining.end() || it->second == 0) {
ok = false;
break;
}
--it->second;
}
if (ok) result.push_back(start);
}
return result;
}
vector<int> findSubstring(string s, vector<string>& words) {
int n = (int)s.size();
int k = (int)words.size();
int len = (int)words[0].size();
vector<int> result;
if (n < k * len) return result;
unordered_map<string, int> need;
for (const string& w : words) ++need[w];
for (int offset = 0; offset < len; ++offset) {
unordered_map<string, int> window;
int matched = 0; // words currently inside the window
int left = offset;
for (int right = offset; right + len <= n; right += len) {
string word = s.substr(right, len);
auto it = need.find(word);
if (it == need.end()) {
window.clear(); // hard reset past the junk word
matched = 0;
left = right + len;
continue;
}
++window[word];
++matched;
while (window[word] > it->second) { // too many copies
--window[s.substr(left, len)];
left += len;
--matched;
}
if (matched == k) {
result.push_back(left);
--window[s.substr(left, len)]; // slide one word forward
left += len;
--matched;
}
}
}
return result;
}
pub fn find_substring_brute_force(s: String, words: Vec<String>) -> Vec<i32> {
let n = s.len();
let k = words.len();
let len = words[0].len();
let total = k * len;
let mut need: HashMap<&str, i32> = HashMap::new();
for w in &words {
*need.entry(w.as_str()).or_insert(0) += 1;
}
let mut result: Vec<i32> = Vec::new();
let mut start = 0;
while start + total <= n {
let mut remaining = need.clone();
let mut ok = true;
let mut pos = start;
for _ in 0..k {
let word = &s[pos..pos + len];
match remaining.get_mut(word) {
Some(count) if *count > 0 => *count -= 1,
_ => {
ok = false;
break;
}
}
pos += len;
}
if ok {
result.push(start as i32);
}
start += 1;
}
result
}
pub fn find_substring(s: String, words: Vec<String>) -> Vec<i32> {
let n = s.len();
let k = words.len();
let len = words[0].len();
let mut result: Vec<i32> = Vec::new();
if n < k * len {
return result;
}
let mut need: HashMap<&str, i32> = HashMap::new();
for w in &words {
*need.entry(w.as_str()).or_insert(0) += 1;
}
for offset in 0..len {
let mut window: HashMap<&str, i32> = HashMap::new();
let mut matched = 0; // words currently inside the window
let mut left = offset;
let mut right = offset;
while right + len <= n {
let word = &s[right..right + len];
match need.get(word) {
None => {
window.clear(); // hard reset past the junk word
matched = 0;
left = right + len;
}
Some("a) => {
*window.entry(word).or_insert(0) += 1;
matched += 1;
while window[word] > quota {
// too many copies -> shrink
*window.get_mut(&s[left..left + len]).unwrap() -= 1;
left += len;
matched -= 1;
}
if matched == k {
result.push(left as i32);
// slide one word forward
*window.get_mut(&s[left..left + len]).unwrap() -= 1;
left += len;
matched -= 1;
}
}
}
right += len;
}
}
result
}
function findSubstringBruteForce(s: string, words: string[]): number[] {
const k = words.length;
const len = words[0].length;
const total = k * len;
const need = new Map<string, number>();
for (const w of words) {
need.set(w, (need.get(w) ?? 0) + 1);
}
const result: number[] = [];
for (let start = 0; start + total <= s.length; start++) {
const remaining = new Map(need);
let ok = true;
for (let pos = start; pos < start + total; pos += len) {
const word = s.slice(pos, pos + len);
const count = remaining.get(word) ?? 0;
if (count === 0) {
ok = false;
break;
}
remaining.set(word, count - 1);
}
if (ok) {
result.push(start);
}
}
return result;
}
function findSubstring(s: string, words: string[]): number[] {
const n = s.length;
const k = words.length;
const len = words[0].length;
const result: number[] = [];
if (n < k * len) {
return result;
}
const need = new Map<string, number>();
for (const w of words) {
need.set(w, (need.get(w) ?? 0) + 1);
}
for (let offset = 0; offset < len; offset++) {
const window = new Map<string, number>();
let matched = 0; // words currently inside the window
let left = offset;
for (let right = offset; right + len <= n; right += len) {
const word = s.slice(right, right + len);
const quota = need.get(word);
if (quota === undefined) {
window.clear(); // hard reset past the junk word
matched = 0;
left = right + len;
continue;
}
window.set(word, (window.get(word) ?? 0) + 1);
matched++;
while ((window.get(word) ?? 0) > quota) { // too many copies
const drop = s.slice(left, left + len);
window.set(drop, (window.get(drop) ?? 0) - 1);
left += len;
matched--;
}
if (matched === k) {
result.push(left);
const drop = s.slice(left, left + len); // slide forward
window.set(drop, (window.get(drop) ?? 0) - 1);
left += len;
matched--;
}
}
}
return result;
}
func findSubstringBruteForce(s string, words []string) []int {
k, length := len(words), len(words[0])
total := k * length
need := make(map[string]int, k)
for _, w := range words {
need[w]++
}
result := []int{}
for start := 0; start+total <= len(s); start++ {
remaining := make(map[string]int, len(need))
for w, c := range need {
remaining[w] = c
}
ok := true
for pos := start; pos < start+total; pos += length {
word := s[pos : pos+length]
if remaining[word] == 0 {
ok = false
break
}
remaining[word]--
}
if ok {
result = append(result, start)
}
}
return result
}
func findSubstring(s string, words []string) []int {
n, k, length := len(s), len(words), len(words[0])
result := []int{}
if n < k*length {
return result
}
need := make(map[string]int, k)
for _, w := range words {
need[w]++
}
for offset := 0; offset < length; offset++ {
window := make(map[string]int)
matched := 0 // words currently inside the window
left := offset
for right := offset; right+length <= n; right += length {
word := s[right : right+length]
quota, known := need[word]
if !known {
window = make(map[string]int) // hard reset past the junk word
matched = 0
left = right + length
continue
}
window[word]++
matched++
for window[word] > quota { // too many copies -> shrink
window[s[left:left+length]]--
left += length
matched--
}
if matched == k {
result = append(result, left)
window[s[left:left+length]]-- // slide one word forward
left += length
matched--
}
}
}
return result
}
func findSubstringBruteForce(_ s: String, _ words: [String]) -> [Int] {
let chars = Array(s)
let n = chars.count
let k = words.count
let len = words[0].count
let total = k * len
var need: [String: Int] = [:]
for w in words { need[w, default: 0] += 1 }
var result: [Int] = []
var start = 0
while start + total <= n {
var remaining = need
var ok = true
var pos = start
for _ in 0..<k {
let word = String(chars[pos..<pos + len])
if let count = remaining[word], count > 0 {
remaining[word] = count - 1
} else {
ok = false
break
}
pos += len
}
if ok { result.append(start) }
start += 1
}
return result
}
func findSubstring(_ s: String, _ words: [String]) -> [Int] {
let chars = Array(s)
let n = chars.count
let k = words.count
let len = words[0].count
var result: [Int] = []
if n < k * len { return result }
var need: [String: Int] = [:]
for w in words { need[w, default: 0] += 1 }
for offset in 0..<len {
var window: [String: Int] = [:]
var matched = 0 // words currently inside the window
var left = offset
var right = offset
while right + len <= n {
let word = String(chars[right..<right + len])
guard let quota = need[word] else {
window.removeAll() // hard reset past the junk word
matched = 0
left = right + len
right += len
continue
}
window[word, default: 0] += 1
matched += 1
while window[word]! > quota { // too many copies -> shrink
window[String(chars[left..<left + len])]! -= 1
left += len
matched -= 1
}
if matched == k {
result.append(left)
// slide one word forward
window[String(chars[left..<left + len])]! -= 1
left += len
matched -= 1
}
right += len
}
}
return result
}
Matrix
12. Game of Life
Medium · LC 289
Given a grid of live and dead cells, apply one simultaneous step of Conway's Game of Life rules in place. Encode two generations in each cell with two bits, keeping the current state in the low bit and writing the next state into the second bit so neighbor counting reads only the low bit; a final pass shifts every cell right to reveal the new board. The trick is that simultaneous updates require every neighbor read to see the old state even after a cell's fate is decided, and the two-bit encoding preserves that without an O(m * n) snapshot copy.
Read every neighbor from an untouched copy of the board — the obvious baseline, at the cost of duplicating the whole grid.
Count neighbor "touches" around live cells only; a cell is alive next round iff it is touched 3 times, or 2 while already alive. Scales to huge, mostly-dead boards where even the snapshot's dense copy is unaffordable; here it writes back in bounds.
Bit 0 keeps the current state while bit 1 stores the next one, so neighbor counts read `& 1` and no copy is needed at all; a final pass shifts every cell right.
Read every neighbor from an untouched copy of the board — the obvious baseline, at the cost of duplicating the whole grid.
Bit 0 keeps the current state while bit 1 stores the next one, so neighbor counts read `& 1` and no copy is needed at all; a final pass shifts every cell right.
Read every neighbor from an untouched copy of the board — the obvious baseline, at the cost of duplicating the whole grid.
Bit 0 keeps the current state while bit 1 stores the next one, so neighbor counts read `& 1` and no copy is needed at all; a final pass shifts every cell right.
Read every neighbor from an untouched copy of the board — the obvious baseline, at the cost of duplicating the whole grid.
Bit 0 keeps the current state while bit 1 stores the next one, so neighbor counts read `& 1` and no copy is needed at all; a final pass shifts every cell right.
Read every neighbor from an untouched copy of the board — the obvious baseline, at the cost of duplicating the whole grid.
Bit 0 keeps the current state while bit 1 stores the next one, so neighbor counts read `& 1` and no copy is needed at all; a final pass shifts every cell right.
Read every neighbor from an untouched copy of the board — the obvious baseline, at the cost of duplicating the whole grid.
Bit 0 keeps the current state while bit 1 stores the next one, so neighbor counts read `& 1` and no copy is needed at all; a final pass shifts every cell right.
def gameOfLife_copy(self, board: List[List[int]]) -> None:
m, n = len(board), len(board[0])
snap = [row[:] for row in board]
for r in range(m):
for c in range(n):
live = sum(
snap[r + dr][c + dc]
for dr, dc in self._NEIGHBORS
if 0 <= r + dr < m and 0 <= c + dc < n
)
board[r][c] = int(live == 3 or (live == 2 and snap[r][c]))
def gameOfLife_sparse(self, board: List[List[int]]) -> None:
m, n = len(board), len(board[0])
live = {(r, c) for r in range(m) for c in range(n) if board[r][c]}
touches = Counter(
(r + dr, c + dc) for r, c in live for dr, dc in self._NEIGHBORS
)
next_live = {
cell for cell, cnt in touches.items()
if cnt == 3 or (cnt == 2 and cell in live)
}
for r in range(m):
for c in range(n):
board[r][c] = int((r, c) in next_live)
def gameOfLife(self, board: List[List[int]]) -> None:
m, n = len(board), len(board[0])
for r in range(m):
for c in range(n):
live = sum(
board[r + dr][c + dc] & 1
for dr, dc in self._NEIGHBORS
if 0 <= r + dr < m and 0 <= c + dc < n
)
# survive with 2-3 neighbors; birth with exactly 3
if live == 3 or (live == 2 and board[r][c] & 1):
board[r][c] |= 2
for r in range(m):
for c in range(n):
board[r][c] >>= 1
void gameOfLifeSnapshot(vector<vector<int>>& board) {
int m = (int)board.size();
int n = (int)board[0].size();
vector<vector<int>> snap = board;
for (int r = 0; r < m; ++r) {
for (int c = 0; c < n; ++c) {
int live = 0;
for (int dr = -1; dr <= 1; ++dr) {
for (int dc = -1; dc <= 1; ++dc) {
if (dr == 0 && dc == 0) continue;
int rr = r + dr, cc = c + dc;
if (rr >= 0 && rr < m && cc >= 0 && cc < n)
live += snap[rr][cc];
}
}
// survive with 2-3 neighbors; birth with exactly 3
board[r][c] = (live == 3 || (live == 2 && snap[r][c] == 1)) ? 1 : 0;
}
}
}
void gameOfLife(vector<vector<int>>& board) {
int m = (int)board.size();
int n = (int)board[0].size();
for (int r = 0; r < m; ++r) {
for (int c = 0; c < n; ++c) {
int live = 0;
for (int dr = -1; dr <= 1; ++dr) {
for (int dc = -1; dc <= 1; ++dc) {
if (dr == 0 && dc == 0) continue;
int rr = r + dr, cc = c + dc;
if (rr >= 0 && rr < m && cc >= 0 && cc < n)
live += board[rr][cc] & 1;
}
}
// survive with 2-3 neighbors; birth with exactly 3
if (live == 3 || (live == 2 && (board[r][c] & 1)))
board[r][c] |= 2;
}
}
for (int r = 0; r < m; ++r)
for (int c = 0; c < n; ++c)
board[r][c] >>= 1;
}
pub fn game_of_life_snapshot(board: &mut Vec<Vec<i32>>) {
let m = board.len() as i32;
let n = board[0].len() as i32;
let snap = board.clone();
for r in 0..m {
for c in 0..n {
let mut live = 0;
for dr in -1..=1 {
for dc in -1..=1 {
if dr == 0 && dc == 0 {
continue;
}
let (rr, cc) = (r + dr, c + dc);
if rr >= 0 && rr < m && cc >= 0 && cc < n {
live += snap[rr as usize][cc as usize];
}
}
}
// survive with 2-3 neighbors; birth with exactly 3
let was_live = snap[r as usize][c as usize] == 1;
board[r as usize][c as usize] =
i32::from(live == 3 || (live == 2 && was_live));
}
}
}
pub fn game_of_life(board: &mut Vec<Vec<i32>>) {
let m = board.len() as i32;
let n = board[0].len() as i32;
for r in 0..m {
for c in 0..n {
let mut live = 0;
for dr in -1..=1 {
for dc in -1..=1 {
if dr == 0 && dc == 0 {
continue;
}
let (rr, cc) = (r + dr, c + dc);
if rr >= 0 && rr < m && cc >= 0 && cc < n {
live += board[rr as usize][cc as usize] & 1;
}
}
}
// survive with 2-3 neighbors; birth with exactly 3
let cell = &mut board[r as usize][c as usize];
if live == 3 || (live == 2 && *cell & 1 == 1) {
*cell |= 2;
}
}
}
for row in board.iter_mut() {
for cell in row.iter_mut() {
*cell >>= 1;
}
}
}
function gameOfLifeSnapshot(board: number[][]): void {
const m = board.length;
const n = board[0].length;
const snap = board.map((row) => [...row]);
for (let r = 0; r < m; r++) {
for (let c = 0; c < n; c++) {
let live = 0;
for (let dr = -1; dr <= 1; dr++) {
for (let dc = -1; dc <= 1; dc++) {
if (dr === 0 && dc === 0) {
continue;
}
const rr = r + dr;
const cc = c + dc;
if (rr >= 0 && rr < m && cc >= 0 && cc < n) {
live += snap[rr][cc];
}
}
}
// survive with 2-3 neighbors; birth with exactly 3
board[r][c] = live === 3 || (live === 2 && snap[r][c] === 1) ? 1 : 0;
}
}
}
/**
Do not return anything, modify board in-place instead.
*/
function gameOfLife(board: number[][]): void {
const m = board.length;
const n = board[0].length;
for (let r = 0; r < m; r++) {
for (let c = 0; c < n; c++) {
let live = 0;
for (let dr = -1; dr <= 1; dr++) {
for (let dc = -1; dc <= 1; dc++) {
if (dr === 0 && dc === 0) {
continue;
}
const rr = r + dr;
const cc = c + dc;
if (rr >= 0 && rr < m && cc >= 0 && cc < n) {
live += board[rr][cc] & 1;
}
}
}
// survive with 2-3 neighbors; birth with exactly 3
if (live === 3 || (live === 2 && (board[r][c] & 1) === 1)) {
board[r][c] |= 2;
}
}
}
for (let r = 0; r < m; r++) {
for (let c = 0; c < n; c++) {
board[r][c] >>= 1;
}
}
}
func gameOfLifeSnapshot(board [][]int) {
m, n := len(board), len(board[0])
snap := make([][]int, m)
for r := range board {
snap[r] = append([]int{}, board[r]...)
}
for r := 0; r < m; r++ {
for c := 0; c < n; c++ {
live := 0
for dr := -1; dr <= 1; dr++ {
for dc := -1; dc <= 1; dc++ {
if dr == 0 && dc == 0 {
continue
}
rr, cc := r+dr, c+dc
if rr >= 0 && rr < m && cc >= 0 && cc < n {
live += snap[rr][cc]
}
}
}
// survive with 2-3 neighbors; birth with exactly 3
if live == 3 || (live == 2 && snap[r][c] == 1) {
board[r][c] = 1
} else {
board[r][c] = 0
}
}
}
}
func gameOfLife(board [][]int) {
m, n := len(board), len(board[0])
for r := 0; r < m; r++ {
for c := 0; c < n; c++ {
live := 0
for dr := -1; dr <= 1; dr++ {
for dc := -1; dc <= 1; dc++ {
if dr == 0 && dc == 0 {
continue
}
rr, cc := r+dr, c+dc
if rr >= 0 && rr < m && cc >= 0 && cc < n {
live += board[rr][cc] & 1
}
}
}
// survive with 2-3 neighbors; birth with exactly 3
if live == 3 || (live == 2 && board[r][c]&1 == 1) {
board[r][c] |= 2
}
}
}
for r := 0; r < m; r++ {
for c := 0; c < n; c++ {
board[r][c] >>= 1
}
}
}
func gameOfLifeSnapshot(_ board: inout [[Int]]) {
let m = board.count
let n = board[0].count
let snap = board
for r in 0..<m {
for c in 0..<n {
var live = 0
for dr in -1...1 {
for dc in -1...1 {
if dr == 0 && dc == 0 { continue }
let rr = r + dr, cc = c + dc
if rr >= 0 && rr < m && cc >= 0 && cc < n {
live += snap[rr][cc]
}
}
}
// survive with 2-3 neighbors; birth with exactly 3
board[r][c] = (live == 3 || (live == 2 && snap[r][c] == 1)) ? 1 : 0
}
}
}
func gameOfLife(_ board: inout [[Int]]) {
let m = board.count
let n = board[0].count
for r in 0..<m {
for c in 0..<n {
var live = 0
for dr in -1...1 {
for dc in -1...1 {
if dr == 0 && dc == 0 { continue }
let rr = r + dr, cc = c + dc
if rr >= 0 && rr < m && cc >= 0 && cc < n {
live += board[rr][cc] & 1
}
}
}
// survive with 2-3 neighbors; birth with exactly 3
if live == 3 || (live == 2 && board[r][c] & 1 == 1) {
board[r][c] |= 2
}
}
}
for r in 0..<m {
for c in 0..<n {
board[r][c] >>= 1
}
}
}
Hashmap
13. Ransom Note
Easy · LC 383
Given a ransom note and a magazine, decide whether the note can be built from the magazine's letters, using each letter at most once. Count the magazine into a fixed array of 26 slots indexed by letter, then walk the note decrementing each needed count and failing the moment a count is already zero. The trick is that the lowercase-only constraint lets a plain array replace a hash map, so every lookup is direct index arithmetic and the extra space is a true constant.
Strings are immutable, so "removing" a char rebuilds the whole string. Passes on LeetCode (tiny tests) but too slow for an interview.
Count both strings, then verify the magazine has >= each needed char.
Count the magazine once, then subtract the note's chars from it.
Same idea as Approach 3, but an array beats a hashmap because the constraint guarantees lowercase a-z, so we index directly.
Cool but strictly worse than 2/3. Sort both, walk them in lockstep.
For each needed char, find one copy in the magazine and erase it, shifting the whole tail left. Quadratic, so fine on suites like this one but too slow for LeetCode's 10^5-char strings.
Sorting groups equal letters, so one lockstep walk matches every needed char — no per-char rescan of the magazine like Approach 1.
Count the magazine's letters once, then spend counts while scanning the note; a deficit means the note cannot be built. Linear time and no sorting — the version to write in an interview.
For each needed char, find one copy in the magazine and remove it, shifting the whole tail left. Quadratic, so fine on suites like this one but too slow for LeetCode's 10^5-char strings.
Sorting groups equal letters, so one lockstep walk matches every needed char — no per-char rescan of the magazine like Approach 1.
Count the magazine's letters once, then spend counts while scanning the note; a deficit means the note cannot be built. Linear time and no sorting — the version to write in an interview.
For each needed char, find one copy in the magazine and rebuild the string without it. Quadratic, so fine on suites like this one but too slow for LeetCode's 10^5-char strings.
Sorting groups equal letters, so one lockstep walk matches every needed char — no per-char rescan of the magazine like Approach 1.
Count the magazine's letters once, then spend counts while scanning the note; a deficit means the note cannot be built. Linear time and no sorting — the version to write in an interview.
For each needed char, find one copy in the magazine and rebuild the string without it. Quadratic, so fine on suites like this one but too slow for LeetCode's 10^5-char strings.
Sorting groups equal letters, so one lockstep walk matches every needed char — no per-char rescan of the magazine like Approach 1.
Count the magazine's letters once, then spend counts while scanning the note; a deficit means the note cannot be built. Linear time and no sorting — the version to write in an interview.
For each needed char, find one copy in the magazine and remove it, shifting the whole tail left. Quadratic, so fine on suites like this one but too slow for LeetCode's 10^5-char strings.
Sorting groups equal letters, so one lockstep walk matches every needed char — no per-char rescan of the magazine like Approach 1.
Count the magazine's letters once, then spend counts while scanning the note; a deficit means the note cannot be built. Linear time and no sorting — the version to write in an interview.
def canConstruct_simulation(self, ransomNote: str, magazine: str) -> bool:
for char in ransomNote:
if char in magazine:
# rebuild magazine without one copy of `char` -> O(m)
magazine = magazine.replace(char, "", 1)
else:
return False
return True
def canConstruct_two_maps(self, ransomNote: str, magazine: str) -> bool:
if len(ransomNote) > len(magazine): # early exit optimization
return False
note_counts = {}
for char in ransomNote:
note_counts[char] = note_counts.get(char, 0) + 1
mag_counts = {}
for char in magazine:
mag_counts[char] = mag_counts.get(char, 0) + 1
for char, count in note_counts.items():
if mag_counts.get(char, 0) < count:
return False
return True
def canConstruct_one_map(self, ransomNote: str, magazine: str) -> bool:
if len(ransomNote) > len(magazine):
return False
mag_counts = {}
for char in magazine:
mag_counts[char] = mag_counts.get(char, 0) + 1
for char in ransomNote:
if mag_counts.get(char, 0) == 0:
return False
mag_counts[char] -= 1
return True
def canConstruct(self, ransomNote: str, magazine: str) -> bool:
letter_count = [0] * 26
for char in magazine:
letter_count[ord(char) - ord('a')] += 1
for char in ransomNote:
idx = ord(char) - ord('a')
if letter_count[idx] > 0:
letter_count[idx] -= 1
else:
return False
return True
def canConstruct_sorting(self, ransomNote: str, magazine: str) -> bool:
if len(ransomNote) > len(magazine):
return False
note = sorted(ransomNote)
mag = sorted(magazine)
i = j = 0
while i < len(note) and j < len(mag):
if note[i] == mag[j]: # matched a needed char
i += 1
j += 1
elif note[i] > mag[j]: # this magazine char is never needed
j += 1
else: # needed char can't appear later -> fail
return False
return i == len(note) # consumed every char we needed?
# ------------------------------------------------------------------
# Pythonic one-liners (great for showing off, fine in practice)
# ------------------------------------------------------------------
def canConstruct_counter_le(self, ransomNote: str, magazine: str) -> bool:
# Counter <= Counter is a multiset-subset test.
return Counter(ransomNote) <= Counter(magazine)
def canConstruct_counter_sub(self, ransomNote: str, magazine: str) -> bool:
# Counter subtraction drops zero/negative counts; leftover => missing chars.
return not (Counter(ransomNote) - Counter(magazine))
bool canConstructSimulation(std::string ransomNote, std::string magazine) {
for (char c : ransomNote) {
const size_t pos = magazine.find(c);
if (pos == std::string::npos) {
return false;
}
magazine.erase(pos, 1); // O(m) shift per erased char
}
return true;
}
bool canConstructSorting(std::string ransomNote, std::string magazine) {
if (ransomNote.size() > magazine.size()) {
return false;
}
std::sort(ransomNote.begin(), ransomNote.end());
std::sort(magazine.begin(), magazine.end());
size_t i = 0;
for (size_t j = 0; i < ransomNote.size() && j < magazine.size(); ++j) {
if (ransomNote[i] == magazine[j]) {
++i; // matched a needed char; also consumes magazine[j]
} else if (ransomNote[i] < magazine[j]) {
return false; // needed char cannot appear later
}
}
return i == ransomNote.size();
}
bool canConstruct(std::string ransomNote, std::string magazine) {
std::array<int, 26> counts{};
for (char c : magazine) {
++counts[c - 'a'];
}
for (char c : ransomNote) {
if (--counts[c - 'a'] < 0) {
return false;
}
}
return true;
}
pub fn can_construct_simulation(ransom_note: String, magazine: String) -> bool {
let mut mag = magazine.into_bytes();
for b in ransom_note.bytes() {
match mag.iter().position(|&m| m == b) {
Some(pos) => {
mag.remove(pos); // O(m) shift per removed char
}
None => return false,
}
}
true
}
pub fn can_construct_sorting(ransom_note: String, magazine: String) -> bool {
if ransom_note.len() > magazine.len() {
return false;
}
let mut note = ransom_note.into_bytes();
let mut mag = magazine.into_bytes();
note.sort_unstable();
mag.sort_unstable();
let (mut i, mut j) = (0, 0);
while i < note.len() && j < mag.len() {
if note[i] == mag[j] {
i += 1; // matched a needed char
j += 1;
} else if note[i] > mag[j] {
j += 1; // this magazine char is never needed
} else {
return false; // needed char cannot appear later
}
}
i == note.len()
}
pub fn can_construct(ransom_note: String, magazine: String) -> bool {
let mut counts = [0i32; 26];
for b in magazine.bytes() {
counts[(b - b'a') as usize] += 1;
}
for b in ransom_note.bytes() {
let idx = (b - b'a') as usize;
counts[idx] -= 1;
if counts[idx] < 0 {
return false;
}
}
true
}
function canConstructSimulation(ransomNote: string, magazine: string): boolean {
let mag = magazine;
for (const c of ransomNote) {
const pos = mag.indexOf(c);
if (pos < 0) {
return false;
}
mag = mag.slice(0, pos) + mag.slice(pos + 1); // O(m) rebuild per char
}
return true;
}
function canConstructSorting(ransomNote: string, magazine: string): boolean {
if (ransomNote.length > magazine.length) {
return false;
}
const note = [...ransomNote].sort();
const mag = [...magazine].sort();
let i = 0;
for (let j = 0; i < note.length && j < mag.length; j++) {
if (note[i] === mag[j]) {
i++; // matched a needed char; also consumes mag[j]
} else if (note[i] < mag[j]) {
return false; // needed char cannot appear later
}
}
return i === note.length;
}
function canConstruct(ransomNote: string, magazine: string): boolean {
const counts = new Array<number>(26).fill(0);
const a = "a".charCodeAt(0);
for (let i = 0; i < magazine.length; i++) {
counts[magazine.charCodeAt(i) - a]++;
}
for (let i = 0; i < ransomNote.length; i++) {
const idx = ransomNote.charCodeAt(i) - a;
if (counts[idx] === 0) {
return false;
}
counts[idx]--;
}
return true;
}
func canConstructSimulation(ransomNote string, magazine string) bool {
for i := 0; i < len(ransomNote); i++ {
pos := strings.IndexByte(magazine, ransomNote[i])
if pos < 0 {
return false
}
magazine = magazine[:pos] + magazine[pos+1:] // O(m) rebuild per char
}
return true
}
func canConstructSorting(ransomNote string, magazine string) bool {
if len(ransomNote) > len(magazine) {
return false
}
note := []byte(ransomNote)
mag := []byte(magazine)
sort.Slice(note, func(a, b int) bool { return note[a] < note[b] })
sort.Slice(mag, func(a, b int) bool { return mag[a] < mag[b] })
i := 0
for j := 0; i < len(note) && j < len(mag); j++ {
if note[i] == mag[j] {
i++ // matched a needed char; also consumes mag[j]
} else if note[i] < mag[j] {
return false // needed char cannot appear later
}
}
return i == len(note)
}
func canConstruct(ransomNote string, magazine string) bool {
var counts [26]int
for i := 0; i < len(magazine); i++ {
counts[magazine[i]-'a']++
}
for i := 0; i < len(ransomNote); i++ {
idx := ransomNote[i] - 'a'
if counts[idx] == 0 {
return false
}
counts[idx]--
}
return true
}
func canConstructSimulation(_ ransomNote: String, _ magazine: String) -> Bool {
var mag = Array(magazine)
for c in ransomNote {
guard let pos = mag.firstIndex(of: c) else {
return false
}
mag.remove(at: pos) // O(m) shift per removed char
}
return true
}
func canConstructSorting(_ ransomNote: String, _ magazine: String) -> Bool {
if ransomNote.count > magazine.count {
return false
}
let note = ransomNote.sorted()
let mag = magazine.sorted()
var i = 0
var j = 0
while i < note.count && j < mag.count {
if note[i] == mag[j] {
i += 1 // matched a needed char
j += 1
} else if note[i] > mag[j] {
j += 1 // this magazine char is never needed
} else {
return false // needed char cannot appear later
}
}
return i == note.count
}
func canConstruct(_ ransomNote: String, _ magazine: String) -> Bool {
var counts = [Int](repeating: 0, count: 26)
let a = Int(UInt8(ascii: "a"))
for b in magazine.utf8 {
counts[Int(b) - a] += 1
}
for b in ransomNote.utf8 {
let idx = Int(b) - a
counts[idx] -= 1
if counts[idx] < 0 {
return false
}
}
return true
}
14. Isomorphic Strings
Easy · LC 205
Given two strings, decide whether the characters of one can be replaced to obtain the other, with every occurrence of a character mapped consistently. Walk both strings in lockstep with two hash maps, one from the first string to the second and one back, and fail as soon as either direction contradicts an earlier pairing. The pitfall is forgetting the reverse map: a single forward map lets two different characters collapse onto the same target, which silently breaks the required one-to-one correspondence.
Two strings are isomorphic iff, position by position, their chars first appeared at the same index. str.index rescans the string on every call: quadratic — fine at n <= 2000, times out on LeetCode's 5 * 10^4.
Drops the quadratic rescans: the pairing is a bijection exactly when the number of distinct (s_char, t_char) pairs equals the number of distinct chars on each side. Still three full passes and no early exit on the first conflict.
The first-occurrence idea from Approach 1, memoized: one map per string of char -> first index. Single pass, and it stops at the first conflicting position instead of scanning everything.
Map s->t and t->s in one pass; a conflict in either direction means the required one-to-one character correspondence is broken. Same cost as Approach 3, but it states the bijection invariant directly — the version to reach for in an interview.
Two strings are isomorphic iff, position by position, their chars first appeared at the same index. find rescans from the front on every call: quadratic — fine at n <= 2000, times out on LeetCode's 5 * 10^4.
The first-occurrence idea from Approach 1, memoized: record each char's first position (+1 so 0 can mean "unseen") instead of rescanning. Single pass, early exit on the first conflict.
Store each character's partner (+1 so 0 can mean "unmapped") in both directions and reject any conflicting pairing. Same cost as Approach 2, but it states the bijection invariant directly — the version to reach for in an interview.
Two strings are isomorphic iff, position by position, their chars first appeared at the same index. position() rescans from the front on every call: quadratic — fine at n <= 2000, times out on LeetCode's 5 * 10^4.
The first-occurrence idea from Approach 1, memoized: record each char's first position (+1 so 0 can mean "unseen") instead of rescanning. Single pass, early exit on the first conflict.
Store each character's partner (+1 so 0 can mean "unmapped") in both directions and reject any conflicting pairing. Same cost as Approach 2, but it states the bijection invariant directly — the version to reach for in an interview.
Two strings are isomorphic iff, position by position, their chars first appeared at the same index. indexOf rescans from the front on every call: quadratic — fine at n <= 2000, times out on LeetCode's 5 * 10^4.
The first-occurrence idea from Approach 1, memoized: record each char's first position instead of rescanning. Single pass, early exit on the first conflict.
Walk both strings in lockstep with a Map in each direction, rejecting any conflicting pairing. Same cost as Approach 2, but it states the bijection invariant directly — the version to reach for in an interview.
Two strings are isomorphic iff, position by position, their chars first appeared at the same index. IndexByte rescans from the front on every call: quadratic — fine at n <= 2000, times out on LeetCode's 5 * 10^4.
The first-occurrence idea from Approach 1, memoized: record each char's first position (+1 so 0 can mean "unseen") instead of rescanning. Single pass, early exit on the first conflict.
Store each character's partner (+1 so 0 can mean "unmapped") in both directions and reject any conflicting pairing. Same cost as Approach 2, but it states the bijection invariant directly — the version to reach for in an interview.
Two strings are isomorphic iff, position by position, their chars first appeared at the same index. firstIndex rescans from the front on every call: quadratic — fine at n <= 2000, times out on LeetCode's 5 * 10^4.
The first-occurrence idea from Approach 1, memoized: record each char's first position instead of rescanning. Single pass, early exit on the first conflict.
Walk both strings in lockstep with a dictionary in each direction, rejecting any conflicting pairing. Same cost as Approach 2, but it states the bijection invariant directly — the version to reach for in an interview.
def isIsomorphic_index_of(self, s: str, t: str) -> bool:
return all(s.index(cs) == t.index(ct) for cs, ct in zip(s, t))
def isIsomorphic_zip_sets(self, s: str, t: str) -> bool:
return len(set(zip(s, t))) == len(set(s)) == len(set(t))
def isIsomorphic_first_index(self, s: str, t: str) -> bool:
first_s = {}
first_t = {}
for i, (cs, ct) in enumerate(zip(s, t)):
if first_s.setdefault(cs, i) != first_t.setdefault(ct, i):
return False
return True
def isIsomorphic(self, s: str, t: str) -> bool:
s_to_t = {}
t_to_s = {}
for cs, ct in zip(s, t):
if s_to_t.setdefault(cs, ct) != ct:
return False
if t_to_s.setdefault(ct, cs) != cs:
return False
return True
bool isIsomorphicIndexOf(std::string s, std::string t) {
for (size_t i = 0; i < s.size(); ++i) {
if (s.find(s[i]) != t.find(t[i])) {
return false;
}
}
return true;
}
bool isIsomorphicFirstIndex(std::string s, std::string t) {
std::array<int, 256> firstS{}; // first index + 1; 0 = unseen
std::array<int, 256> firstT{};
for (size_t i = 0; i < s.size(); ++i) {
int& fs = firstS[static_cast<unsigned char>(s[i])];
int& ft = firstT[static_cast<unsigned char>(t[i])];
if (fs != ft) {
return false;
}
if (fs == 0) {
fs = ft = static_cast<int>(i) + 1;
}
}
return true;
}
bool isIsomorphic(std::string s, std::string t) {
std::array<int, 256> mapS{}; // s-char -> t-char + 1 (0 = unmapped)
std::array<int, 256> mapT{}; // t-char -> s-char + 1
for (size_t i = 0; i < s.size(); ++i) {
const unsigned char cs = s[i];
const unsigned char ct = t[i];
if (mapS[cs] != 0 && mapS[cs] != ct + 1) {
return false;
}
if (mapT[ct] != 0 && mapT[ct] != cs + 1) {
return false;
}
mapS[cs] = ct + 1;
mapT[ct] = cs + 1;
}
return true;
}
pub fn is_isomorphic_index_of(s: String, t: String) -> bool {
let (sb, tb) = (s.as_bytes(), t.as_bytes());
for i in 0..sb.len() {
let first_s = sb.iter().position(|&c| c == sb[i]);
let first_t = tb.iter().position(|&c| c == tb[i]);
if first_s != first_t {
return false;
}
}
true
}
pub fn is_isomorphic_first_index(s: String, t: String) -> bool {
let mut first_s = [0usize; 256]; // first index + 1; 0 = unseen
let mut first_t = [0usize; 256];
for (i, (cs, ct)) in s.bytes().zip(t.bytes()).enumerate() {
let (fs, ft) = (first_s[cs as usize], first_t[ct as usize]);
if fs != ft {
return false;
}
if fs == 0 {
first_s[cs as usize] = i + 1;
first_t[ct as usize] = i + 1;
}
}
true
}
pub fn is_isomorphic(s: String, t: String) -> bool {
let mut map_s = [0u16; 256]; // partner byte + 1; 0 = unmapped
let mut map_t = [0u16; 256];
for (cs, ct) in s.bytes().zip(t.bytes()) {
let (i, j) = (cs as usize, ct as usize);
if map_s[i] != 0 && map_s[i] != ct as u16 + 1 {
return false;
}
if map_t[j] != 0 && map_t[j] != cs as u16 + 1 {
return false;
}
map_s[i] = ct as u16 + 1;
map_t[j] = cs as u16 + 1;
}
true
}
function isIsomorphicIndexOf(s: string, t: string): boolean {
for (let i = 0; i < s.length; i++) {
if (s.indexOf(s[i]) !== t.indexOf(t[i])) {
return false;
}
}
return true;
}
function isIsomorphicFirstIndex(s: string, t: string): boolean {
const firstS = new Map<string, number>();
const firstT = new Map<string, number>();
for (let i = 0; i < s.length; i++) {
const fs = firstS.get(s[i]) ?? -1;
const ft = firstT.get(t[i]) ?? -1;
if (fs !== ft) {
return false;
}
if (fs === -1) {
firstS.set(s[i], i);
firstT.set(t[i], i);
}
}
return true;
}
function isIsomorphic(s: string, t: string): boolean {
const sToT = new Map<string, string>();
const tToS = new Map<string, string>();
for (let i = 0; i < s.length; i++) {
const cs = s[i];
const ct = t[i];
const mappedT = sToT.get(cs);
if (mappedT !== undefined && mappedT !== ct) {
return false;
}
const mappedS = tToS.get(ct);
if (mappedS !== undefined && mappedS !== cs) {
return false;
}
sToT.set(cs, ct);
tToS.set(ct, cs);
}
return true;
}
func isIsomorphicIndexOf(s string, t string) bool {
for i := 0; i < len(s); i++ {
if strings.IndexByte(s, s[i]) != strings.IndexByte(t, t[i]) {
return false
}
}
return true
}
func isIsomorphicFirstIndex(s string, t string) bool {
var firstS, firstT [256]int // first index + 1; 0 = unseen
for i := 0; i < len(s); i++ {
fs, ft := firstS[s[i]], firstT[t[i]]
if fs != ft {
return false
}
if fs == 0 {
firstS[s[i]] = i + 1
firstT[t[i]] = i + 1
}
}
return true
}
func isIsomorphic(s string, t string) bool {
var mapS, mapT [256]int // partner byte + 1; 0 = unmapped
for i := 0; i < len(s); i++ {
cs, ct := s[i], t[i]
if mapS[cs] != 0 && mapS[cs] != int(ct)+1 {
return false
}
if mapT[ct] != 0 && mapT[ct] != int(cs)+1 {
return false
}
mapS[cs] = int(ct) + 1
mapT[ct] = int(cs) + 1
}
return true
}
func isIsomorphicIndexOf(_ s: String, _ t: String) -> Bool {
let sa = Array(s)
let ta = Array(t)
for i in 0..<sa.count {
if sa.firstIndex(of: sa[i]) != ta.firstIndex(of: ta[i]) {
return false
}
}
return true
}
func isIsomorphicFirstIndex(_ s: String, _ t: String) -> Bool {
var firstS = [Character: Int]()
var firstT = [Character: Int]()
for (i, (cs, ct)) in zip(s, t).enumerated() {
let fs = firstS[cs] ?? -1
let ft = firstT[ct] ?? -1
if fs != ft {
return false
}
if fs == -1 {
firstS[cs] = i
firstT[ct] = i
}
}
return true
}
func isIsomorphic(_ s: String, _ t: String) -> Bool {
var sToT = [Character: Character]()
var tToS = [Character: Character]()
for (cs, ct) in zip(s, t) {
if let mapped = sToT[cs], mapped != ct {
return false
}
if let mapped = tToS[ct], mapped != cs {
return false
}
sToT[cs] = ct
tToS[ct] = cs
}
return true
}
15. Word Pattern
Easy · LC 290
Given a pattern of characters and a sentence of words, decide whether the words follow the pattern under a full one-to-one correspondence. Split the sentence into words, then walk pattern and words in lockstep with two hash maps, character to word and word to character, failing on the first conflict in either direction. The pitfall is the pair of easy misses: a length mismatch between pattern and word list is an automatic no, and checking only one mapping direction lets two different characters claim the same word.
The pairing is a bijection iff every pair of positions agrees: pattern chars match exactly when the words match. No maps at all, but comparing all pairs is quadratic — fine at w <= 2000, too slow once w grows (LeetCode's limits happen to be tiny, so it passes there too).
Drops the quadratic pair loop: a bijection leaves exactly as many distinct (char, word) pairs as distinct chars and distinct words — any remap inflates the pairs. Still three passes and no early exit.
Chars and words must first appear at the same positions; comparing first-seen indices needs only one map per side and no pair storage, and it stops at the first conflicting position.
Map char->word and word->char in one pass; a conflict in either direction means the pairing is not one-to-one. Same cost as Approach 3, but it states the bijection invariant directly — the version to reach for in an interview.
The pairing is a bijection iff every pair of positions agrees: pattern chars match exactly when the words match. No maps at all, but comparing all pairs is quadratic — fine at w <= 2000, too slow once w grows.
Chars and words must first appear at the same positions (+1 so 0 can mean "unseen"). Drops the quadratic pair loop of Approach 1: single pass, early exit on the first conflict.
Keep char -> word and word -> char maps; any conflicting pairing breaks the required bijection. Same cost as Approach 2, but it states the bijection invariant directly — the version to reach for in an interview.
The pairing is a bijection iff every pair of positions agrees: pattern chars match exactly when the words match. No maps at all, but comparing all pairs is quadratic — fine at w <= 2000, too slow once w grows.
Chars and words must first appear at the same positions (+1 so 0 can mean "unseen"). Drops the quadratic pair loop of Approach 1: single pass, early exit on the first conflict.
Keep char -> word and word -> char maps; any conflicting pairing breaks the required bijection. Same cost as Approach 2, but it states the bijection invariant directly — the version to reach for in an interview.
The pairing is a bijection iff every pair of positions agrees: pattern chars match exactly when the words match. No maps at all, but comparing all pairs is quadratic — fine at w <= 2000, too slow once w grows.
Chars and words must first appear at the same positions. Drops the quadratic pair loop of Approach 1: single pass, early exit on the first conflict.
Keep char -> word and word -> char Maps; any conflicting pairing breaks the required bijection. Same cost as Approach 2, but it states the bijection invariant directly — the version to reach for in an interview.
The pairing is a bijection iff every pair of positions agrees: pattern chars match exactly when the words match. No maps at all, but comparing all pairs is quadratic — fine at w <= 2000, too slow once w grows.
Chars and words must first appear at the same positions (+1 so 0 can mean "unseen"). Drops the quadratic pair loop of Approach 1: single pass, early exit on the first conflict.
Keep char -> word and word -> char maps; any conflicting pairing breaks the required bijection. Same cost as Approach 2, but it states the bijection invariant directly — the version to reach for in an interview.
The pairing is a bijection iff every pair of positions agrees: pattern chars match exactly when the words match. No maps at all, but comparing all pairs is quadratic — fine at w <= 2000, too slow once w grows.
Chars and words must first appear at the same positions. Drops the quadratic pair loop of Approach 1: single pass, early exit on the first conflict.
Keep char -> word and word -> char dictionaries; any conflicting pairing breaks the required bijection. Same cost as Approach 2, but it states the bijection invariant directly — the version to reach for in an interview.
def wordPattern_pairwise(self, pattern: str, s: str) -> bool:
words = s.split()
if len(words) != len(pattern):
return False
n = len(words)
for i in range(n):
for j in range(i + 1, n):
if (pattern[i] == pattern[j]) != (words[i] == words[j]):
return False
return True
def wordPattern_zip_sets(self, pattern: str, s: str) -> bool:
words = s.split()
return (len(words) == len(pattern)
and len(set(zip(pattern, words)))
== len(set(pattern)) == len(set(words)))
def wordPattern_first_index(self, pattern: str, s: str) -> bool:
words = s.split()
if len(words) != len(pattern):
return False
first_char = {}
first_word = {}
for i, (char, word) in enumerate(zip(pattern, words)):
if first_char.setdefault(char, i) != first_word.setdefault(word, i):
return False
return True
def wordPattern(self, pattern: str, s: str) -> bool:
words = s.split()
if len(words) != len(pattern):
return False
char_to_word = {}
word_to_char = {}
for char, word in zip(pattern, words):
if char_to_word.setdefault(char, word) != word:
return False
if word_to_char.setdefault(word, char) != char:
return False
return True
bool wordPatternPairwise(std::string pattern, std::string s) {
std::vector<std::string> words;
std::istringstream stream(s);
std::string word;
while (stream >> word) {
words.push_back(word);
}
if (words.size() != pattern.size()) {
return false;
}
for (size_t i = 0; i < words.size(); ++i) {
for (size_t j = i + 1; j < words.size(); ++j) {
if ((pattern[i] == pattern[j]) != (words[i] == words[j])) {
return false;
}
}
}
return true;
}
bool wordPatternFirstIndex(std::string pattern, std::string s) {
std::vector<std::string> words;
std::istringstream stream(s);
std::string word;
while (stream >> word) {
words.push_back(word);
}
if (words.size() != pattern.size()) {
return false;
}
std::array<int, 256> firstChar{}; // first index + 1; 0 = unseen
std::unordered_map<std::string, int> firstWord;
for (size_t i = 0; i < words.size(); ++i) {
int& fc = firstChar[static_cast<unsigned char>(pattern[i])];
const auto it = firstWord.find(words[i]);
const int fw = it == firstWord.end() ? 0 : it->second;
if (fc != fw) {
return false;
}
if (fc == 0) {
fc = static_cast<int>(i) + 1;
firstWord.emplace(words[i], fc);
}
}
return true;
}
bool wordPattern(std::string pattern, std::string s) {
std::vector<std::string> words;
std::istringstream stream(s);
std::string word;
while (stream >> word) {
words.push_back(word);
}
if (words.size() != pattern.size()) {
return false;
}
std::unordered_map<char, std::string> charToWord;
std::unordered_map<std::string, char> wordToChar;
for (size_t i = 0; i < words.size(); ++i) {
const char c = pattern[i];
const auto [itC, newChar] = charToWord.emplace(c, words[i]);
if (!newChar && itC->second != words[i]) {
return false;
}
const auto [itW, newWord] = wordToChar.emplace(words[i], c);
if (!newWord && itW->second != c) {
return false;
}
}
return true;
}
pub fn word_pattern_pairwise(pattern: String, s: String) -> bool {
let words: Vec<&str> = s.split(' ').collect();
let pat = pattern.as_bytes();
if words.len() != pat.len() {
return false;
}
for i in 0..words.len() {
for j in i + 1..words.len() {
if (pat[i] == pat[j]) != (words[i] == words[j]) {
return false;
}
}
}
true
}
pub fn word_pattern_first_index(pattern: String, s: String) -> bool {
let words: Vec<&str> = s.split(' ').collect();
if words.len() != pattern.len() {
return false;
}
let mut first_char = [0usize; 256]; // first index + 1; 0 = unseen
let mut first_word: HashMap<&str, usize> = HashMap::new();
for (i, (c, w)) in pattern.bytes().zip(words).enumerate() {
let fc = first_char[c as usize];
let fw = *first_word.get(w).unwrap_or(&0);
if fc != fw {
return false;
}
if fc == 0 {
first_char[c as usize] = i + 1;
first_word.insert(w, i + 1);
}
}
true
}
pub fn word_pattern(pattern: String, s: String) -> bool {
let words: Vec<&str> = s.split(' ').collect();
if words.len() != pattern.len() {
return false;
}
let mut char_to_word: HashMap<char, &str> = HashMap::new();
let mut word_to_char: HashMap<&str, char> = HashMap::new();
for (c, w) in pattern.chars().zip(words) {
if *char_to_word.entry(c).or_insert(w) != w {
return false;
}
if *word_to_char.entry(w).or_insert(c) != c {
return false;
}
}
true
}
function wordPatternPairwise(pattern: string, s: string): boolean {
const words = s.split(" ");
if (words.length !== pattern.length) {
return false;
}
for (let i = 0; i < words.length; i++) {
for (let j = i + 1; j < words.length; j++) {
if ((pattern[i] === pattern[j]) !== (words[i] === words[j])) {
return false;
}
}
}
return true;
}
function wordPatternFirstIndex(pattern: string, s: string): boolean {
const words = s.split(" ");
if (words.length !== pattern.length) {
return false;
}
const firstChar = new Map<string, number>();
const firstWord = new Map<string, number>();
for (let i = 0; i < pattern.length; i++) {
const fc = firstChar.get(pattern[i]) ?? -1;
const fw = firstWord.get(words[i]) ?? -1;
if (fc !== fw) {
return false;
}
if (fc === -1) {
firstChar.set(pattern[i], i);
firstWord.set(words[i], i);
}
}
return true;
}
function wordPattern(pattern: string, s: string): boolean {
const words = s.split(" ");
if (words.length !== pattern.length) {
return false;
}
const charToWord = new Map<string, string>();
const wordToChar = new Map<string, string>();
for (let i = 0; i < pattern.length; i++) {
const c = pattern[i];
const w = words[i];
const seenWord = charToWord.get(c);
if (seenWord !== undefined && seenWord !== w) {
return false;
}
const seenChar = wordToChar.get(w);
if (seenChar !== undefined && seenChar !== c) {
return false;
}
charToWord.set(c, w);
wordToChar.set(w, c);
}
return true;
}
func wordPatternPairwise(pattern string, s string) bool {
words := strings.Split(s, " ")
if len(words) != len(pattern) {
return false
}
for i := 0; i < len(words); i++ {
for j := i + 1; j < len(words); j++ {
if (pattern[i] == pattern[j]) != (words[i] == words[j]) {
return false
}
}
}
return true
}
func wordPatternFirstIndex(pattern string, s string) bool {
words := strings.Split(s, " ")
if len(words) != len(pattern) {
return false
}
var firstChar [256]int // first index + 1; 0 = unseen
firstWord := make(map[string]int)
for i := 0; i < len(pattern); i++ {
fc, fw := firstChar[pattern[i]], firstWord[words[i]]
if fc != fw {
return false
}
if fc == 0 {
firstChar[pattern[i]] = i + 1
firstWord[words[i]] = i + 1
}
}
return true
}
func wordPattern(pattern string, s string) bool {
words := strings.Split(s, " ")
if len(words) != len(pattern) {
return false
}
charToWord := make(map[byte]string)
wordToChar := make(map[string]byte)
for i := 0; i < len(pattern); i++ {
c, w := pattern[i], words[i]
if seen, ok := charToWord[c]; ok && seen != w {
return false
}
if seen, ok := wordToChar[w]; ok && seen != c {
return false
}
charToWord[c] = w
wordToChar[w] = c
}
return true
}
func wordPatternPairwise(_ pattern: String, _ s: String) -> Bool {
let words = s.split(separator: " ")
let chars = Array(pattern)
guard words.count == chars.count else {
return false
}
for i in 0..<words.count {
for j in (i + 1)..<words.count {
if (chars[i] == chars[j]) != (words[i] == words[j]) {
return false
}
}
}
return true
}
func wordPatternFirstIndex(_ pattern: String, _ s: String) -> Bool {
let words = s.split(separator: " ")
guard words.count == pattern.count else {
return false
}
var firstChar = [Character: Int]()
var firstWord = [Substring: Int]()
for (i, (c, w)) in zip(pattern, words).enumerated() {
let fc = firstChar[c] ?? -1
let fw = firstWord[w] ?? -1
if fc != fw {
return false
}
if fc == -1 {
firstChar[c] = i
firstWord[w] = i
}
}
return true
}
func wordPattern(_ pattern: String, _ s: String) -> Bool {
let words = s.split(separator: " ")
guard words.count == pattern.count else {
return false
}
var charToWord = [Character: Substring]()
var wordToChar = [Substring: Character]()
for (c, w) in zip(pattern, words) {
if let seen = charToWord[c], seen != w {
return false
}
if let seen = wordToChar[w], seen != c {
return false
}
charToWord[c] = w
wordToChar[w] = c
}
return true
}
Intervals
16. Summary Ranges
Easy · LC 228
Given a sorted array of unique integers, return the shortest list of ranges that covers all the numbers exactly. Anchor the start of the current run, walk forward while each next value is exactly one more than the previous, then emit the run and jump past it. The trick is the emission step: a single-number run is printed by itself while a longer run joins its start and end with an arrow, and formatting runs directly during the walk keeps it one pass with no storage beyond the output.
Collect every index where a run starts (index 0 plus any spot where the gap to the previous value exceeds one), then format neighbors in a second pass. Easy to reason about, but keeps whole side lists.
Inside a consecutive run, value - index is constant (both step by one), so grouping on that key slices out the runs directly. Only one run is buffered at a time — no whole-array side lists.
Anchor the start of the current run, walk while each next value is exactly previous + 1, then emit the run and jump past it. A single walk that formats runs directly, with no intermediate storage.
First merge the values into a list of [start, end] runs, then format each run in a second pass. Easy to reason about, but the intermediate list stores every run twice.
Anchor the start of the current run, extend while each next value is exactly previous + 1, then emit "a->b" directly. A single walk that formats runs as it goes — no intermediate run list.
First merge the values into a list of (start, end) runs, then format each run in a second pass. Easy to reason about, but the intermediate list stores every run twice.
Anchor the start of the current run, extend while each next value is exactly previous + 1, then emit "a->b" directly. A single walk that formats runs as it goes — no intermediate run list.
First merge the values into a list of [start, end] runs, then format each run in a second pass. Easy to reason about, but the intermediate list stores every run twice.
Anchor the start of the current run, extend while each next value is exactly previous + 1, then emit "a->b" directly. A single walk that formats runs as it goes — no intermediate run list.
First merge the values into a list of [start, end] runs, then format each run in a second pass. Easy to reason about, but the intermediate list stores every run twice.
Anchor the start of the current run, extend while each next value is exactly previous + 1, then emit "a->b" directly. A single walk that formats runs as it goes — no intermediate run list.
First merge the values into a list of (start, end) runs, then format each run in a second pass. Easy to reason about, but the intermediate list stores every run twice.
Anchor the start of the current run, extend while each next value is exactly previous + 1, then emit "a->b" directly. A single walk that formats runs as it goes — no intermediate run list.
def summaryRanges_breakpoints(self, nums: list[int]) -> list[str]:
if not nums:
return []
starts = [0] + [i for i in range(1, len(nums))
if nums[i] != nums[i - 1] + 1]
ends = [i - 1 for i in starts[1:]] + [len(nums) - 1]
return [str(nums[a]) if a == b else f"{nums[a]}->{nums[b]}"
for a, b in zip(starts, ends)]
def summaryRanges_groupby(self, nums: list[int]) -> list[str]:
ranges = []
for _, run in groupby(enumerate(nums), key=lambda p: p[1] - p[0]):
pairs = list(run)
first, last = pairs[0][1], pairs[-1][1]
ranges.append(str(first) if first == last else f"{first}->{last}")
return ranges
def summaryRanges(self, nums: list[int]) -> list[str]:
ranges = []
i, n = 0, len(nums)
while i < n:
start = i
while i + 1 < n and nums[i + 1] == nums[i] + 1:
i += 1
if start == i:
ranges.append(str(nums[start]))
else:
ranges.append(f"{nums[start]}->{nums[i]}")
i += 1
return ranges
std::vector<std::string> summaryRangesTwoPass(std::vector<int>& nums) {
std::vector<std::pair<int, int>> runs; // inclusive [start, end]
for (int x : nums) {
if (!runs.empty() &&
static_cast<long long>(x) ==
static_cast<long long>(runs.back().second) + 1) {
runs.back().second = x;
} else {
runs.push_back({x, x});
}
}
std::vector<std::string> ranges;
ranges.reserve(runs.size());
for (const auto& [start, end] : runs) {
if (start == end) {
ranges.push_back(std::to_string(start));
} else {
ranges.push_back(std::to_string(start) + "->" +
std::to_string(end));
}
}
return ranges;
}
std::vector<std::string> summaryRanges(std::vector<int>& nums) {
std::vector<std::string> ranges;
const size_t n = nums.size();
for (size_t i = 0; i < n;) {
const size_t start = i;
while (i + 1 < n &&
static_cast<long long>(nums[i + 1]) ==
static_cast<long long>(nums[i]) + 1) {
++i;
}
if (start == i) {
ranges.push_back(std::to_string(nums[start]));
} else {
ranges.push_back(std::to_string(nums[start]) + "->" +
std::to_string(nums[i]));
}
++i;
}
return ranges;
}
pub fn summary_ranges_two_pass(nums: Vec<i32>) -> Vec<String> {
let mut runs: Vec<(i32, i32)> = Vec::new(); // inclusive (start, end)
for &x in &nums {
match runs.last_mut() {
Some(run) if x as i64 == run.1 as i64 + 1 => run.1 = x,
_ => runs.push((x, x)),
}
}
runs.iter()
.map(|&(start, end)| {
if start == end {
start.to_string()
} else {
format!("{}->{}", start, end)
}
})
.collect()
}
pub fn summary_ranges(nums: Vec<i32>) -> Vec<String> {
let mut ranges = Vec::new();
let n = nums.len();
let mut i = 0;
while i < n {
let start = i;
while i + 1 < n && nums[i + 1] as i64 == nums[i] as i64 + 1 {
i += 1;
}
if start == i {
ranges.push(nums[start].to_string());
} else {
ranges.push(format!("{}->{}", nums[start], nums[i]));
}
i += 1;
}
ranges
}
function summaryRangesTwoPass(nums: number[]): string[] {
const runs: Array<[number, number]> = []; // inclusive [start, end]
for (const x of nums) {
const last = runs[runs.length - 1];
if (last !== undefined && x === last[1] + 1) {
last[1] = x;
} else {
runs.push([x, x]);
}
}
return runs.map(([start, end]) => (start === end ? `${start}` : `${start}->${end}`));
}
function summaryRanges(nums: number[]): string[] {
const ranges: string[] = [];
const n = nums.length;
for (let i = 0; i < n; ) {
const start = i;
while (i + 1 < n && nums[i + 1] === nums[i] + 1) {
i++;
}
ranges.push(start === i ? `${nums[start]}` : `${nums[start]}->${nums[i]}`);
i++;
}
return ranges;
}
func summaryRangesTwoPass(nums []int) []string {
type run struct{ start, end int }
runs := []run{}
for _, x := range nums {
if len(runs) > 0 && x == runs[len(runs)-1].end+1 {
runs[len(runs)-1].end = x
} else {
runs = append(runs, run{x, x})
}
}
ranges := []string{}
for _, r := range runs {
if r.start == r.end {
ranges = append(ranges, strconv.Itoa(r.start))
} else {
ranges = append(ranges, strconv.Itoa(r.start)+"->"+strconv.Itoa(r.end))
}
}
return ranges
}
func summaryRanges(nums []int) []string {
ranges := []string{}
n := len(nums)
for i := 0; i < n; {
start := i
for i+1 < n && nums[i+1] == nums[i]+1 {
i++
}
if start == i {
ranges = append(ranges, strconv.Itoa(nums[start]))
} else {
ranges = append(ranges, strconv.Itoa(nums[start])+"->"+strconv.Itoa(nums[i]))
}
i++
}
return ranges
}
func summaryRangesTwoPass(_ nums: [Int]) -> [String] {
var runs = [(start: Int, end: Int)]() // inclusive
for x in nums {
if let last = runs.last, x == last.end + 1 {
runs[runs.count - 1].end = x
} else {
runs.append((x, x))
}
}
return runs.map { $0.start == $0.end ? "\($0.start)" : "\($0.start)->\($0.end)" }
}
func summaryRanges(_ nums: [Int]) -> [String] {
var ranges = [String]()
var i = 0
while i < nums.count {
let start = i
while i + 1 < nums.count && nums[i + 1] == nums[i] + 1 {
i += 1
}
ranges.append(start == i ? "\(nums[start])" : "\(nums[start])->\(nums[i])")
i += 1
}
return ranges
}
17. Minimum Number of Arrows to Burst Balloons
Medium · LC 452
Given balloons as horizontal intervals, find the minimum number of vertical arrows needed to burst them all. Sort the balloons by right end and shoot each arrow at the end of the first un-burst balloon, starting a new arrow only when a later balloon begins past the current arrow's position. The insight is that shooting at the earliest end is always safe, since every balloon overlapping that one must span its coordinate — and touching edges count as hits, so a new arrow is needed only when a start strictly exceeds the arrow's position.
Track the overlap window of the current group; when a balloon starts past the window, the group takes one arrow and a new group begins. Correct, but it carries an extra min() per balloon — more state than the problem actually needs.
Shooting at the earliest un-burst balloon's end is always safe: every balloon overlapping it must span that x, so one arrow there also bursts each later balloon whose start is <= that end. The window bookkeeping disappears: the arrow position only ever moves forward.
Track the overlap window of the current group; when a balloon starts past the window, the group takes one arrow and a new group begins. Correct, but it carries an extra min() per balloon — more state than the problem actually needs.
Shooting at the earliest un-burst balloon's end is always safe: every balloon overlapping it must span that x, so one arrow there also bursts each later balloon whose start is <= that end. The window bookkeeping disappears: the arrow position only ever moves forward.
Track the overlap window of the current group; when a balloon starts past the window, the group takes one arrow and a new group begins. Correct, but it carries an extra min() per balloon — more state than the problem actually needs.
Shooting at the earliest un-burst balloon's end is always safe: every balloon overlapping it must span that x, so one arrow there also bursts each later balloon whose start is <= that end. The window bookkeeping disappears: the arrow position only ever moves forward.
Track the overlap window of the current group; when a balloon starts past the window, the group takes one arrow and a new group begins. Correct, but it carries an extra Math.min per balloon — more state than the problem actually needs.
Shooting at the earliest un-burst balloon's end is always safe: every balloon overlapping it must span that x, so one arrow there also bursts each later balloon whose start is <= that end. The window bookkeeping disappears: the arrow position only ever moves forward.
Track the overlap window of the current group; when a balloon starts past the window, the group takes one arrow and a new group begins. Correct, but it carries an extra min per balloon — more state than the problem actually needs.
Shooting at the earliest un-burst balloon's end is always safe: every balloon overlapping it must span that x, so one arrow there also bursts each later balloon whose start is <= that end. The window bookkeeping disappears: the arrow position only ever moves forward.
Track the overlap window of the current group; when a balloon starts past the window, the group takes one arrow and a new group begins. Correct, but it carries an extra min() per balloon — more state than the problem actually needs.
Shooting at the earliest un-burst balloon's end is always safe: every balloon overlapping it must span that x, so one arrow there also bursts each later balloon whose start is <= that end. The window bookkeeping disappears: the arrow position only ever moves forward.
def findMinArrowShots_sort_by_start(self, points: List[List[int]]) -> int:
points.sort()
arrows = 1
window_end = points[0][1]
for start, end in points[1:]:
if start > window_end:
arrows += 1
window_end = end
else:
window_end = min(window_end, end)
return arrows
def findMinArrowShots(self, points: List[List[int]]) -> int:
points.sort(key=lambda p: p[1])
arrows = 1
arrow_x = points[0][1]
for start, end in points:
if start > arrow_x:
arrows += 1
arrow_x = end
return arrows
int findMinArrowShotsSortByStart(vector<vector<int>>& points) {
sort(points.begin(), points.end(),
[](const vector<int>& a, const vector<int>& b) {
if (a[0] != b[0]) return a[0] < b[0];
return a[1] < b[1];
});
int arrows = 1;
int windowEnd = points[0][1];
for (size_t i = 1; i < points.size(); ++i) {
if (points[i][0] > windowEnd) {
++arrows;
windowEnd = points[i][1];
} else {
windowEnd = min(windowEnd, points[i][1]);
}
}
return arrows;
}
int findMinArrowShots(vector<vector<int>>& points) {
sort(points.begin(), points.end(),
[](const vector<int>& a, const vector<int>& b) { return a[1] < b[1]; });
int arrows = 1;
int arrowX = points[0][1];
for (const auto& p : points) {
if (p[0] > arrowX) {
++arrows;
arrowX = p[1];
}
}
return arrows;
}
pub fn find_min_arrow_shots_sort_by_start(mut points: Vec<Vec<i32>>) -> i32 {
points.sort_unstable_by(|a, b| a[0].cmp(&b[0]).then(a[1].cmp(&b[1])));
let mut arrows = 1;
let mut window_end = points[0][1];
for p in points.iter().skip(1) {
if p[0] > window_end {
arrows += 1;
window_end = p[1];
} else {
window_end = window_end.min(p[1]);
}
}
arrows
}
pub fn find_min_arrow_shots(mut points: Vec<Vec<i32>>) -> i32 {
points.sort_unstable_by_key(|p| p[1]);
let mut arrows = 1;
let mut arrow_x = points[0][1];
for p in &points {
if p[0] > arrow_x {
arrows += 1;
arrow_x = p[1];
}
}
arrows
}
function findMinArrowShotsSortByStart(points: number[][]): number {
points.sort((a, b) => {
if (a[0] !== b[0]) return a[0] < b[0] ? -1 : 1;
return a[1] < b[1] ? -1 : a[1] > b[1] ? 1 : 0;
});
let arrows = 1;
let windowEnd = points[0][1];
for (let i = 1; i < points.length; i++) {
if (points[i][0] > windowEnd) {
arrows++;
windowEnd = points[i][1];
} else {
windowEnd = Math.min(windowEnd, points[i][1]);
}
}
return arrows;
}
function findMinArrowShots(points: number[][]): number {
points.sort((a, b) => (a[1] < b[1] ? -1 : a[1] > b[1] ? 1 : 0));
let arrows = 1;
let arrowX = points[0][1];
for (const p of points) {
if (p[0] > arrowX) {
arrows++;
arrowX = p[1];
}
}
return arrows;
}
func findMinArrowShotsSortByStart(points [][]int) int {
sort.Slice(points, func(i, j int) bool {
if points[i][0] != points[j][0] {
return points[i][0] < points[j][0]
}
return points[i][1] < points[j][1]
})
arrows := 1
windowEnd := points[0][1]
for _, p := range points[1:] {
if p[0] > windowEnd {
arrows++
windowEnd = p[1]
} else if p[1] < windowEnd {
windowEnd = p[1]
}
}
return arrows
}
func findMinArrowShots(points [][]int) int {
sort.Slice(points, func(i, j int) bool { return points[i][1] < points[j][1] })
arrows := 1
arrowX := points[0][1]
for _, p := range points {
if p[0] > arrowX {
arrows++
arrowX = p[1]
}
}
return arrows
}
func findMinArrowShotsSortByStart(_ points: [[Int]]) -> Int {
let sorted = points.sorted {
if $0[0] != $1[0] { return $0[0] < $1[0] }
return $0[1] < $1[1]
}
var arrows = 1
var windowEnd = sorted[0][1]
for p in sorted.dropFirst() {
if p[0] > windowEnd {
arrows += 1
windowEnd = p[1]
} else {
windowEnd = min(windowEnd, p[1])
}
}
return arrows
}
func findMinArrowShots(_ points: [[Int]]) -> Int {
let sorted = points.sorted { $0[1] < $1[1] }
var arrows = 1
var arrowX = sorted[0][1]
for p in sorted where p[0] > arrowX {
arrows += 1
arrowX = p[1]
}
return arrows
}
Stack
18. Basic Calculator
Hard · LC 224
Given a string containing digits, plus and minus signs, parentheses, and spaces, evaluate the expression without any built-in evaluator. Scan once, building each number digit by digit and folding it into a running result with the pending sign; an opening parenthesis pushes the outer result and sign onto a stack and starts fresh, while a closing one folds the inner result back into the popped context. The subtlety is that unary minus works for free because a fresh result starts at zero, and the last pending number must still be flushed into the result after the loop ends.
Each '(' recurses to evaluate its subexpression, mirroring the grammar directly — but the call stack grows with the nesting depth, which can hit Python's recursion limit on deeply nested input.
Trades the recursion for an explicit stack — no depth limit. Add every number straight into the total with (its own sign) times (the sign of the enclosing parens); ')' is then just a pop.
'(' saves the outer partial result and its sign, then starts fresh; ')' folds the inner result back into the saved context. Each char is handled exactly once with no inner digit loop, and unary minus works for free because a fresh result starts at 0.
Each '(' recurses to evaluate its subexpression, mirroring the grammar directly — but the call stack grows with the nesting depth, which risks stack overflow on deeply nested input.
Same idea with an explicit stack, so nesting depth can never blow the call stack: '(' saves the outer context and starts fresh; ')' folds the inner result back into it. Unary minus works for free because a fresh result starts at 0.
Each '(' recurses to evaluate its subexpression, mirroring the grammar directly — but the call stack grows with the nesting depth, which risks stack overflow on deeply nested input.
Same idea with an explicit stack, so nesting depth can never blow the call stack: '(' saves the outer context and starts fresh; ')' folds the inner result back into it. Unary minus works for free because a fresh result starts at 0.
Each '(' recurses to evaluate its subexpression, mirroring the grammar directly — but the call stack grows with the nesting depth, which risks a stack overflow on deeply nested input.
Same idea with an explicit stack, so nesting depth can never blow the call stack: '(' saves the outer context and starts fresh; ')' folds the inner result back into it. Unary minus works for free because a fresh result starts at 0.
Each '(' recurses to evaluate its subexpression, mirroring the grammar directly — but the call stack grows with the nesting depth, which risks stack growth on deeply nested input.
Same idea with an explicit stack, so nesting depth can never blow the call stack: '(' saves the outer context and starts fresh; ')' folds the inner result back into it. Unary minus works for free because a fresh result starts at 0.
Each '(' recurses to evaluate its subexpression, mirroring the grammar directly — but the call stack grows with the nesting depth, which risks stack overflow on deeply nested input.
Same idea with an explicit stack, so nesting depth can never blow the call stack: '(' saves the outer context and starts fresh; ')' folds the inner result back into it. Unary minus works for free because a fresh result starts at 0.
def calculate_recursive(self, s: str) -> int:
def parse(i: int) -> tuple[int, int]:
"""Evaluate from s[i]; return (value, index just past what was read)."""
result = 0
sign = 1
while i < len(s):
ch = s[i]
if ch.isdigit():
num = 0
while i < len(s) and s[i].isdigit():
num = num * 10 + int(s[i])
i += 1
result += sign * num
continue
if ch == "+":
sign = 1
elif ch == "-":
sign = -1
elif ch == "(":
value, i = parse(i + 1)
result += sign * value
continue
elif ch == ")":
return result, i + 1
i += 1
return result, i
return parse(0)[0]
def calculate_sign_stack(self, s: str) -> int:
total = 0
sign = 1
context = [1] # sign multiplier of each enclosing paren level
i = 0
while i < len(s):
ch = s[i]
if ch.isdigit():
num = 0
while i < len(s) and s[i].isdigit():
num = num * 10 + int(s[i])
i += 1
total += sign * num
continue
if ch == "+":
sign = context[-1]
elif ch == "-":
sign = -context[-1]
elif ch == "(":
context.append(sign)
elif ch == ")":
context.pop()
i += 1
return total
def calculate(self, s: str) -> int:
result = 0
sign = 1
num = 0
stack = [] # (outer result, outer sign) per open paren
for ch in s:
if ch.isdigit():
num = num * 10 + int(ch)
elif ch == "+":
result += sign * num
num, sign = 0, 1
elif ch == "-":
result += sign * num
num, sign = 0, -1
elif ch == "(":
stack.append((result, sign))
result, sign = 0, 1
elif ch == ")":
result += sign * num
num = 0
outer_result, outer_sign = stack.pop()
result = outer_result + outer_sign * result
return result + sign * num
int calculateRecursive(string s) {
size_t i = 0;
return static_cast<int>(parseExpr(s, i));
}
private:
// Evaluate s from index i until ')' or the end; leaves i just past
// what was read.
long long parseExpr(const string& s, size_t& i) {
long long result = 0, num = 0, sign = 1;
while (i < s.size()) {
char c = s[i];
if (c >= '0' && c <= '9') {
num = num * 10 + (c - '0');
} else if (c == '+') {
result += sign * num;
num = 0;
sign = 1;
} else if (c == '-') {
result += sign * num;
num = 0;
sign = -1;
} else if (c == '(') {
++i;
result += sign * parseExpr(s, i);
num = 0;
sign = 1;
continue; // i is already past the matching ')'
} else if (c == ')') {
++i;
return result + sign * num;
}
++i;
}
return result + sign * num;
}
public:
int calculate(string s) {
long long result = 0, num = 0, sign = 1;
vector<pair<long long, long long>> stack; // (outer result, outer sign)
for (char c : s) {
if (c >= '0' && c <= '9') {
num = num * 10 + (c - '0');
} else if (c == '+') {
result += sign * num;
num = 0;
sign = 1;
} else if (c == '-') {
result += sign * num;
num = 0;
sign = -1;
} else if (c == '(') {
stack.push_back({result, sign});
result = 0;
sign = 1;
} else if (c == ')') {
result += sign * num;
num = 0;
result = stack.back().first + stack.back().second * result;
stack.pop_back();
}
}
return static_cast<int>(result + sign * num);
}
pub fn calculate_recursive(s: String) -> i32 {
Self::parse_expr(s.as_bytes(), 0).0 as i32
}
// Evaluate s from index i until b')' or the end; returns the value and
// the index just past what was read.
fn parse_expr(s: &[u8], mut i: usize) -> (i64, usize) {
let mut result: i64 = 0;
let mut num: i64 = 0;
let mut sign: i64 = 1;
while i < s.len() {
match s[i] {
b'0'..=b'9' => num = num * 10 + i64::from(s[i] - b'0'),
b'+' => {
result += sign * num;
num = 0;
sign = 1;
}
b'-' => {
result += sign * num;
num = 0;
sign = -1;
}
b'(' => {
let (inner, next) = Self::parse_expr(s, i + 1);
result += sign * inner;
num = 0;
sign = 1;
i = next; // already past the matching ')'
continue;
}
b')' => return (result + sign * num, i + 1),
_ => {} // spaces
}
i += 1;
}
(result + sign * num, i)
}
pub fn calculate(s: String) -> i32 {
let mut result: i64 = 0;
let mut num: i64 = 0;
let mut sign: i64 = 1;
let mut stack: Vec<(i64, i64)> = Vec::new(); // (outer result, outer sign)
for c in s.bytes() {
match c {
b'0'..=b'9' => num = num * 10 + i64::from(c - b'0'),
b'+' => {
result += sign * num;
num = 0;
sign = 1;
}
b'-' => {
result += sign * num;
num = 0;
sign = -1;
}
b'(' => {
stack.push((result, sign));
result = 0;
sign = 1;
}
b')' => {
result += sign * num;
num = 0;
let (outer_result, outer_sign) = stack.pop().unwrap();
result = outer_result + outer_sign * result;
}
_ => {} // spaces
}
}
(result + sign * num) as i32
}
function calculateRecursive(s: string): number {
return parseExpr(s, 0)[0];
}
// Evaluate s from index i until ")" or the end; returns [value, index
// just past what was read].
function parseExpr(s: string, i: number): [number, number] {
let result = 0;
let num = 0;
let sign = 1;
while (i < s.length) {
const c = s.charAt(i);
if (c >= "0" && c <= "9") {
num = num * 10 + (s.charCodeAt(i) - 48);
} else if (c === "+") {
result += sign * num;
num = 0;
sign = 1;
} else if (c === "-") {
result += sign * num;
num = 0;
sign = -1;
} else if (c === "(") {
const [inner, next] = parseExpr(s, i + 1);
result += sign * inner;
num = 0;
sign = 1;
i = next; // already past the matching ")"
continue;
} else if (c === ")") {
return [result + sign * num, i + 1];
}
i++;
}
return [result + sign * num, i];
}
function calculate(s: string): number {
let result = 0;
let num = 0;
let sign = 1;
const stack: number[] = []; // flattened (outer result, outer sign) pairs
for (let i = 0; i < s.length; i++) {
const c = s.charAt(i);
if (c >= "0" && c <= "9") {
num = num * 10 + (s.charCodeAt(i) - 48);
} else if (c === "+") {
result += sign * num;
num = 0;
sign = 1;
} else if (c === "-") {
result += sign * num;
num = 0;
sign = -1;
} else if (c === "(") {
stack.push(result, sign);
result = 0;
sign = 1;
} else if (c === ")") {
result += sign * num;
num = 0;
const outerSign = stack.pop()!;
const outerResult = stack.pop()!;
result = outerResult + outerSign * result;
}
}
return result + sign * num;
}
func calculateRecursive(s string) int {
result, _ := parseExpr(s, 0)
return result
}
// parseExpr evaluates s from index i until ')' or the end; it returns the
// value and the index just past what was read.
func parseExpr(s string, i int) (int, int) {
result, num, sign := 0, 0, 1
for i < len(s) {
c := s[i]
switch {
case c >= '0' && c <= '9':
num = num*10 + int(c-'0')
case c == '+':
result += sign * num
num, sign = 0, 1
case c == '-':
result += sign * num
num, sign = 0, -1
case c == '(':
var inner int
inner, i = parseExpr(s, i+1)
result += sign * inner
num, sign = 0, 1
continue // i is already past the matching ')'
case c == ')':
return result + sign*num, i + 1
}
i++
}
return result + sign*num, i
}
func calculate(s string) int {
type frame struct{ result, sign int }
result, num, sign := 0, 0, 1
var stack []frame
for i := 0; i < len(s); i++ {
c := s[i]
switch {
case c >= '0' && c <= '9':
num = num*10 + int(c-'0')
case c == '+':
result += sign * num
num, sign = 0, 1
case c == '-':
result += sign * num
num, sign = 0, -1
case c == '(':
stack = append(stack, frame{result, sign})
result, sign = 0, 1
case c == ')':
result += sign * num
num = 0
outer := stack[len(stack)-1]
stack = stack[:len(stack)-1]
result = outer.result + outer.sign*result
}
}
return result + sign*num
}
func calculateRecursive(_ s: String) -> Int {
let bytes = Array(s.utf8)
var i = 0
return parseExpr(bytes, &i)
}
// Evaluate bytes from index i until ")" or the end; leaves i just past
// what was read.
private func parseExpr(_ bytes: [UInt8], _ i: inout Int) -> Int {
let zero = UInt8(ascii: "0")
let nine = UInt8(ascii: "9")
var result = 0
var num = 0
var sign = 1
while i < bytes.count {
let c = bytes[i]
if c >= zero && c <= nine {
num = num * 10 + Int(c - zero)
} else if c == UInt8(ascii: "+") {
result += sign * num
num = 0
sign = 1
} else if c == UInt8(ascii: "-") {
result += sign * num
num = 0
sign = -1
} else if c == UInt8(ascii: "(") {
i += 1
result += sign * parseExpr(bytes, &i)
num = 0
sign = 1
continue // i is already past the matching ")"
} else if c == UInt8(ascii: ")") {
i += 1
return result + sign * num
}
i += 1
}
return result + sign * num
}
func calculate(_ s: String) -> Int {
var result = 0
var num = 0
var sign = 1
var stack: [(result: Int, sign: Int)] = [] // one frame per open paren
for ch in s {
if let digit = ch.wholeNumberValue {
num = num * 10 + digit
} else if ch == "+" {
result += sign * num
num = 0
sign = 1
} else if ch == "-" {
result += sign * num
num = 0
sign = -1
} else if ch == "(" {
stack.append((result, sign))
result = 0
sign = 1
} else if ch == ")" {
result += sign * num
num = 0
let outer = stack.removeLast()
result = outer.result + outer.sign * result
}
}
return result + sign * num
}
Linked List
19. Remove Duplicates from Sorted List II
Medium · LC 82
Given a sorted linked list, delete every node whose value appears more than once, keeping only the values that occur exactly once. Anchor a dummy node before the head and track the last node known to survive; when the run in front of it spans more than one node of the same value, unlink the entire run, otherwise advance onto the unique node. The dummy head is the key move, turning duplicates at the head into a non-special case, and the pitfall is advancing the survivor pointer before confirming the run ahead really has length one.
Count every value, then stitch together the nodes seen exactly once. Ignores sortedness entirely (it would work on an unsorted list too), which is exactly why it pays for a hash map the problem never needs.
Uses sortedness: if the head starts a duplicate run, drop the whole run and recurse on the remainder; otherwise keep the head. The hash map is gone, but a long list still burns O(n) recursion stack.
`prev` is the last node known to survive. If the run in front of it is longer than one node, unlink the whole run; otherwise advance. The dummy head makes duplicates at the head a non-special case, and the stack and hash map of the earlier rungs both disappear.
Count every value, then stitch together the nodes seen exactly once. Ignores sortedness entirely (it would work unsorted too), which is exactly why it pays for a hash map the problem never needs.
`prev` is the last surviving node; if the run in front of it has length > 1, unlink the whole run, otherwise advance. The dummy head makes duplicates at the head a non-special case, and the hash map of the counting rung disappears.
Count every value, then rebuild the chain from the nodes seen exactly once. Ignores sortedness entirely (it would work unsorted too), which is exactly why it pays for a hash map the problem never needs.
Pop nodes off the input; if the next node shares the current value, drop the whole run, otherwise append the node to the result chain. One pass, no hash map — the counting rung's extra memory is gone.
Count every value, then stitch together the nodes seen exactly once. Ignores sortedness entirely (it would work unsorted too), which is exactly why it pays for a map the problem never needs.
`prev` is the last surviving node; if the run in front of it has length > 1, unlink the whole run, otherwise advance. The dummy head makes duplicates at the head a non-special case, and the map of the counting rung disappears.
Count every value, then stitch together the nodes seen exactly once. Ignores sortedness entirely (it would work unsorted too), which is exactly why it pays for a hash map the problem never needs.
prev is the last surviving node; if the run in front of it has length > 1, unlink the whole run, otherwise advance. The dummy head makes duplicates at the head a non-special case, and the hash map of the counting rung disappears.
Count every value, then stitch together the nodes seen exactly once. Ignores sortedness entirely (it would work unsorted too), which is exactly why it pays for a dictionary the problem never needs.
`prev` is the last surviving node; if the run in front of it has length > 1, unlink the whole run, otherwise advance. The dummy head makes duplicates at the head a non-special case, and the dictionary of the counting rung disappears.
def deleteDuplicates_counting(self, head: Optional[ListNode]) -> Optional[ListNode]:
counts = {}
node = head
while node:
counts[node.val] = counts.get(node.val, 0) + 1
node = node.next
dummy = tail = ListNode()
node = head
while node:
if counts[node.val] == 1:
tail.next = node
tail = node
node = node.next
tail.next = None
return dummy.next
def deleteDuplicates_recursion(self, head: Optional[ListNode]) -> Optional[ListNode]:
if not head or not head.next:
return head
if head.val == head.next.val:
run_val = head.val
while head and head.val == run_val:
head = head.next
return self.deleteDuplicates_recursion(head)
head.next = self.deleteDuplicates_recursion(head.next)
return head
def deleteDuplicates(self, head: Optional[ListNode]) -> Optional[ListNode]:
dummy = ListNode(0, head)
prev = dummy
while prev.next:
cur = prev.next
if cur.next and cur.next.val == cur.val:
run_val = cur.val
while prev.next and prev.next.val == run_val:
prev.next = prev.next.next # unlink every copy
else:
prev = cur # unique value, keep it
return dummy.next
ListNode* deleteDuplicatesCounting(ListNode* head) {
std::unordered_map<int, int> counts;
for (ListNode* node = head; node; node = node->next) ++counts[node->val];
ListNode dummy;
ListNode* tail = &dummy;
for (ListNode* node = head; node; node = node->next) {
if (counts[node->val] == 1) {
tail->next = node;
tail = node;
}
}
tail->next = nullptr;
return dummy.next;
}
ListNode* deleteDuplicates(ListNode* head) {
ListNode dummy(0, head);
ListNode* prev = &dummy;
while (prev->next) {
ListNode* cur = prev->next;
if (cur->next && cur->next->val == cur->val) {
int runVal = cur->val;
while (prev->next && prev->next->val == runVal)
prev->next = prev->next->next; // unlink every copy
} else {
prev = cur; // unique value, keep it
}
}
return dummy.next;
}
pub fn delete_duplicates_counting(head: Option<Box<ListNode>>) -> Option<Box<ListNode>> {
use std::collections::HashMap;
let mut counts: HashMap<i32, u32> = HashMap::new();
let mut p = head.as_ref();
while let Some(node) = p {
*counts.entry(node.val).or_insert(0) += 1;
p = node.next.as_ref();
}
let mut dummy = Box::new(ListNode::new(0));
let mut tail = &mut dummy;
let mut cur = head;
while let Some(mut node) = cur {
cur = node.next.take();
if counts[&node.val] == 1 {
tail.next = Some(node);
tail = tail.next.as_mut().unwrap();
}
}
dummy.next
}
pub fn delete_duplicates(head: Option<Box<ListNode>>) -> Option<Box<ListNode>> {
let mut dummy = Box::new(ListNode::new(0));
let mut tail = &mut dummy;
let mut cur = head;
while let Some(mut node) = cur {
cur = node.next.take();
if cur.as_ref().is_some_and(|n| n.val == node.val) {
// drop the whole run of this value
let run_val = node.val;
while cur.as_ref().is_some_and(|n| n.val == run_val) {
let mut skipped = cur.unwrap();
cur = skipped.next.take();
}
} else {
tail.next = Some(node); // unique value, keep it
tail = tail.next.as_mut().unwrap();
}
}
dummy.next
}
function deleteDuplicatesCounting(head: ListNode | null): ListNode | null {
const counts = new Map<number, number>();
for (let node = head; node !== null; node = node.next) {
counts.set(node.val, (counts.get(node.val) ?? 0) + 1);
}
const dummy = new ListNode();
let tail = dummy;
for (let node = head; node !== null; node = node.next) {
if (counts.get(node.val) === 1) {
tail.next = node;
tail = node;
}
}
tail.next = null;
return dummy.next;
}
function deleteDuplicates(head: ListNode | null): ListNode | null {
const dummy = new ListNode(0, head);
let prev = dummy;
while (prev.next !== null) {
const cur = prev.next;
if (cur.next !== null && cur.next.val === cur.val) {
const runVal = cur.val;
while (prev.next !== null && prev.next.val === runVal) {
prev.next = prev.next.next; // unlink every copy
}
} else {
prev = cur; // unique value, keep it
}
}
return dummy.next;
}
func deleteDuplicatesCounting(head *ListNode) *ListNode {
counts := map[int]int{}
for node := head; node != nil; node = node.Next {
counts[node.Val]++
}
dummy := &ListNode{}
tail := dummy
for node := head; node != nil; node = node.Next {
if counts[node.Val] == 1 {
tail.Next = node
tail = node
}
}
tail.Next = nil
return dummy.Next
}
func deleteDuplicates(head *ListNode) *ListNode {
dummy := &ListNode{Next: head}
prev := dummy
for prev.Next != nil {
cur := prev.Next
if cur.Next != nil && cur.Next.Val == cur.Val {
runVal := cur.Val
for prev.Next != nil && prev.Next.Val == runVal {
prev.Next = prev.Next.Next // unlink every copy
}
} else {
prev = cur // unique value, keep it
}
}
return dummy.Next
}
func deleteDuplicatesCounting(_ head: ListNode?) -> ListNode? {
var counts: [Int: Int] = [:]
var node = head
while let n = node {
counts[n.val, default: 0] += 1
node = n.next
}
let dummy = ListNode()
var tail = dummy
node = head
while let n = node {
if counts[n.val] == 1 {
tail.next = n
tail = n
}
node = n.next
}
tail.next = nil
return dummy.next
}
func deleteDuplicates(_ head: ListNode?) -> ListNode? {
let dummy = ListNode(0, head)
var prev = dummy
while let cur = prev.next {
if let next = cur.next, next.val == cur.val {
let runVal = cur.val
while let node = prev.next, node.val == runVal {
prev.next = node.next // unlink every copy
}
} else {
prev = cur // unique value, keep it
}
}
return dummy.next
}
20. Rotate List
Medium · LC 61
Given a linked list and a count k, rotate the list to the right by k places. Walk to the tail while counting the length, link the tail back to the head to close the list into a ring, then step forward from the head to the node that becomes the new tail and cut the ring just after it. The pitfall is that k can dwarf the length, so it must first be reduced modulo the node count, and a remainder of zero means the list is returned unchanged rather than cut.
Collect the nodes, compute the rotated order by slicing, relink. The simplest possible index arithmetic, at the price of an O(n) auxiliary array the pointer-only rungs below do without.
Drops the array: after reducing k, send a lead pointer k nodes ahead; when it reaches the tail, the lag pointer sits on the new tail. Constant space, but it walks the list three times in total.
Walk to the tail (learning n), link tail -> head to form a ring, then cut the ring after node n - k%n. Same bounds as the two-pointer walk but only one full pass plus a partial one — and no juggling of two moving pointers.
Each step walks to the second-to-last node and moves the tail to the front. Quadratic once k % n grows with n — fine at these test sizes, times out against LeetCode's n = 500, k = 2 * 10^9 only if you forget the k %= n reduction; still wasteful even with it.
Walk to the tail (learning n), link tail -> head to form a ring, then cut the ring after node n - k%n. All k steps collapse into a single cut, so the repeated tail-walks of Approach 1 disappear.
Each step detaches the tail node and pushes it onto the front. Quadratic once k % n grows with n — fine at these test sizes, wasteful on LeetCode's n = 500 even with the k %= n reduction.
Count the length, reduce k mod n, then split the list after n - k nodes and swap the two halves (take the second half, walk to its tail, reattach the first half). All k steps collapse into a single cut, so the repeated tail-walks of Approach 1 disappear.
Each step walks to the second-to-last node and moves the tail to the front. Quadratic once k % n grows with n — fine at these test sizes, wasteful on LeetCode's n = 500 even with the k %= n reduction.
Walk to the tail (learning n), link tail -> head to form a ring, then cut the ring after node n - k%n. All k steps collapse into a single cut, so the repeated tail-walks of Approach 1 disappear.
Each step walks to the second-to-last node and moves the tail to the front. Quadratic once k % n grows with n — fine at these test sizes, wasteful on LeetCode's n = 500 even with the k %= n reduction.
Walk to the tail (learning n), link tail -> head to form a ring, then cut the ring after node n - k%n. All k steps collapse into a single cut, so the repeated tail-walks of Approach 1 disappear.
Each step walks to the second-to-last node and moves the tail to the front. Quadratic once k % n grows with n — fine at these test sizes, wasteful on LeetCode's n = 500 even with the k % n cut.
Walk to the tail (learning n), link tail -> head to form a ring, then cut the ring after node n - k%n. All k steps collapse into a single cut, so the repeated tail-walks of Approach 1 disappear.
def rotateRight_array(self, head: Optional[ListNode], k: int) -> Optional[ListNode]:
nodes = []
node = head
while node:
nodes.append(node)
node = node.next
if not nodes:
return None
n = len(nodes)
k %= n
if k == 0:
return head
order = nodes[n - k:] + nodes[:n - k]
for a, b in zip(order, order[1:]):
a.next = b
order[-1].next = None
return order[0]
def rotateRight_two_pointers(self, head: Optional[ListNode], k: int) -> Optional[ListNode]:
if not head:
return None
n, node = 0, head
while node:
n += 1
node = node.next
k %= n
if k == 0:
return head
lead = head
for _ in range(k):
lead = lead.next
lag = head
while lead.next: # advance both until lead hits the tail
lead = lead.next
lag = lag.next
new_head = lag.next
lag.next = None
lead.next = head # old tail hooks back onto old head
return new_head
def rotateRight(self, head: Optional[ListNode], k: int) -> Optional[ListNode]:
if not head or not head.next:
return head
n, tail = 1, head
while tail.next:
tail = tail.next
n += 1
k %= n
if k == 0:
return head
tail.next = head # close into a ring
new_tail = head
for _ in range(n - k - 1): # new tail is the (n-k)-th node
new_tail = new_tail.next
new_head = new_tail.next
new_tail.next = None # cut the ring
return new_head
ListNode* rotateRightOneStep(ListNode* head, int k) {
if (!head || !head->next) return head;
int n = 1;
for (ListNode* p = head; p->next; p = p->next) ++n;
for (int step = 0; step < k % n; ++step) {
ListNode* p = head;
while (p->next->next) p = p->next; // second-to-last node
ListNode* tail = p->next;
p->next = nullptr;
tail->next = head; // tail becomes the new head
head = tail;
}
return head;
}
ListNode* rotateRight(ListNode* head, int k) {
if (!head || !head->next) return head;
int n = 1;
ListNode* tail = head;
while (tail->next) {
tail = tail->next;
++n;
}
k %= n;
if (k == 0) return head;
tail->next = head; // close into a ring
ListNode* newTail = head;
for (int i = 0; i < n - k - 1; ++i) // new tail is the (n-k)-th node
newTail = newTail->next;
ListNode* newHead = newTail->next;
newTail->next = nullptr; // cut the ring
return newHead;
}
pub fn rotate_right_one_step(head: Option<Box<ListNode>>, k: i32) -> Option<Box<ListNode>> {
if head.is_none() {
return head;
}
let mut n = 0usize;
let mut p = head.as_ref();
while let Some(node) = p {
n += 1;
p = node.next.as_ref();
}
let mut head = head;
for _ in 0..(k as usize) % n {
// Walk to the second-to-last node and take the tail off it.
let mut cut = head.as_mut().unwrap();
while cut.next.as_ref().unwrap().next.is_some() {
cut = cut.next.as_mut().unwrap();
}
let mut tail = cut.next.take().unwrap();
tail.next = head; // tail becomes the new head
head = Some(tail);
}
head
}
pub fn rotate_right(head: Option<Box<ListNode>>, k: i32) -> Option<Box<ListNode>> {
if head.is_none() {
return head;
}
let mut n = 0usize;
let mut p = head.as_ref();
while let Some(node) = p {
n += 1;
p = node.next.as_ref();
}
let k = (k as usize) % n;
if k == 0 {
return head;
}
// Cut after the (n-k)-th node.
let mut head = head;
let mut cut = head.as_mut().unwrap();
for _ in 0..n - k - 1 {
cut = cut.next.as_mut().unwrap();
}
let mut new_head = cut.next.take();
// Walk to the tail of the second half and reattach the first.
let mut tail = new_head.as_mut().unwrap();
while tail.next.is_some() {
tail = tail.next.as_mut().unwrap();
}
tail.next = head;
new_head
}
function rotateRightOneStep(head: ListNode | null, k: number): ListNode | null {
if (head === null || head.next === null) return head;
let n = 1;
for (let p = head; p.next !== null; p = p.next) n++;
for (let step = 0; step < k % n; step++) {
let prev: ListNode = head;
while (prev.next!.next !== null) prev = prev.next!; // second-to-last
const tail: ListNode = prev.next!;
prev.next = null;
tail.next = head; // tail becomes the new head
head = tail;
}
return head;
}
function rotateRight(head: ListNode | null, k: number): ListNode | null {
if (head === null || head.next === null) return head;
let n = 1;
let tail = head;
while (tail.next !== null) {
tail = tail.next;
n++;
}
k %= n;
if (k === 0) return head;
tail.next = head; // close into a ring
let newTail = head;
for (let i = 0; i < n - k - 1; i++) { // new tail is the (n-k)-th node
newTail = newTail.next!;
}
const newHead = newTail.next;
newTail.next = null; // cut the ring
return newHead;
}
func rotateRightOneStep(head *ListNode, k int) *ListNode {
if head == nil || head.Next == nil {
return head
}
n := 1
for p := head; p.Next != nil; p = p.Next {
n++
}
for step := 0; step < k%n; step++ {
p := head
for p.Next.Next != nil { // second-to-last node
p = p.Next
}
tail := p.Next
p.Next = nil
tail.Next = head // tail becomes the new head
head = tail
}
return head
}
func rotateRight(head *ListNode, k int) *ListNode {
if head == nil || head.Next == nil {
return head
}
n, tail := 1, head
for tail.Next != nil {
tail = tail.Next
n++
}
k %= n
if k == 0 {
return head
}
tail.Next = head // close into a ring
newTail := head
for i := 0; i < n-k-1; i++ { // new tail is the (n-k)-th node
newTail = newTail.Next
}
newHead := newTail.Next
newTail.Next = nil // cut the ring
return newHead
}
func rotateRightOneStep(_ head: ListNode?, _ k: Int) -> ListNode? {
guard var head = head, head.next != nil else { return head }
var n = 1
var p = head
while let next = p.next {
p = next
n += 1
}
for _ in 0..<(k % n) {
var prev = head
while prev.next!.next != nil { // second-to-last node
prev = prev.next!
}
let tail = prev.next!
prev.next = nil
tail.next = head // tail becomes the new head
head = tail
}
return head
}
func rotateRight(_ head: ListNode?, _ k: Int) -> ListNode? {
guard let head = head, head.next != nil else { return head }
var n = 1
var tail = head
while let next = tail.next {
tail = next
n += 1
}
let k = k % n
if k == 0 { return head }
tail.next = head // close into a ring
var newTail = head
for _ in 0..<(n - k - 1) { // new tail is the (n-k)-th node
newTail = newTail.next!
}
let newHead = newTail.next
newTail.next = nil // cut the ring
return newHead
}
21. Partition List
Medium · LC 86
Given a linked list and a value x, rearrange it so every node less than x comes before every node greater than or equal to x, preserving relative order inside each group. Thread each node in one pass onto one of two dummy-headed chains, one for the smaller values and one for the rest, then join them by pointing the first chain's tail at the second chain's head. The step people forget is terminating the second chain: without setting its tail's next pointer to null, the old tail can still point into the middle of the result and form a cycle.
Collect values, order them as [< x] + [>= x], write them back into the existing nodes. Cheats a little (no relinking) but is a handy cross-check and legal when node identity doesn't matter.
Keep `ins`, the tail of the "< x" region at the front. Whenever the scan finds a < x node past that region, unlink it and splice it in right after `ins`. Real relinking in constant space — no value array — but the four-pointer splice is easy to get subtly wrong.
Thread every node onto a "less" chain or a "greater-or-equal" chain in one pass, then join them. Same bounds as the splice but with one obvious invariant instead of pointer surgery. Terminating the second chain is the step people forget — without it the old tail can point into the middle of the result and form a cycle.
Collect values, order them as [< x] + [>= x], write them back into the existing nodes. Cheats a little (no relinking) but is a handy cross-check and legal when node identity doesn't matter.
Thread each node onto a "less" or "greater-or-equal" chain in one pass, terminate the second chain, then join them. Real relinking with no value array. Terminating the second chain is the step people forget — without it the old tail can form a cycle.
Collect values, order them as [< x] + [>= x], write them back into the existing nodes. Cheats a little (no relinking) but is a handy cross-check and legal when node identity doesn't matter.
Pop each node off the input and push it onto a "less" or a "greater-or-equal" chain, then join them. Real relinking with no value buffer; taking each node's `next` terminates the chains as a side effect, and relative order inside each group is preserved.
Collect values, order them as [< x] + [>= x], write them back into the existing nodes. Cheats a little (no relinking) but is a handy cross-check and legal when node identity doesn't matter.
Thread each node onto a "less" or "greater-or-equal" chain in one pass, terminate the second chain, then join them. Real relinking with no value array. Terminating the second chain is the step people forget — without it the old tail can form a cycle.
Collect values, order them as [< x] + [>= x], write them back into the existing nodes. Cheats a little (no relinking) but is a handy cross-check and legal when node identity doesn't matter.
Thread each node onto a "less" or "greater-or-equal" chain in one pass, terminate the second chain, then join them. Real relinking with no value slice. Terminating the second chain is the step people forget — without it the old tail can form a cycle.
Collect values, order them as [< x] + [>= x], write them back into the existing nodes. Cheats a little (no relinking) but is a handy cross-check and legal when node identity doesn't matter.
Thread each node onto a "less" or "greater-or-equal" chain in one pass, terminate the second chain, then join them. Real relinking with no value array. Terminating the second chain is the step people forget — without it the old tail can form a cycle.
def partition_values(self, head: Optional[ListNode], x: int) -> Optional[ListNode]:
vals = []
node = head
while node:
vals.append(node.val)
node = node.next
ordered = [v for v in vals if v < x] + [v for v in vals if v >= x]
node = head
for v in ordered:
node.val = v
node = node.next
return head
def partition_in_place(self, head: Optional[ListNode], x: int) -> Optional[ListNode]:
dummy = ListNode(0, head)
ins = dummy # end of the leading < x run
while ins.next and ins.next.val < x:
ins = ins.next
prev = ins
while prev.next:
cur = prev.next
if cur.val < x: # splice cur up behind ins
prev.next = cur.next
cur.next = ins.next
ins.next = cur
ins = cur
else:
prev = cur
return dummy.next
def partition(self, head: Optional[ListNode], x: int) -> Optional[ListNode]:
less = less_tail = ListNode()
geq = geq_tail = ListNode()
node = head
while node:
if node.val < x:
less_tail.next = node
less_tail = node
else:
geq_tail.next = node
geq_tail = node
node = node.next
geq_tail.next = None # terminate the second chain
less_tail.next = geq.next # join: [< x] then [>= x]
return less.next
ListNode* partitionValues(ListNode* head, int x) {
std::vector<int> ordered;
for (ListNode* node = head; node; node = node->next)
if (node->val < x) ordered.push_back(node->val);
for (ListNode* node = head; node; node = node->next)
if (node->val >= x) ordered.push_back(node->val);
size_t i = 0;
for (ListNode* node = head; node; node = node->next)
node->val = ordered[i++];
return head;
}
ListNode* partition(ListNode* head, int x) {
ListNode less, geq;
ListNode* lessTail = &less;
ListNode* geqTail = ≥
for (ListNode* node = head; node; node = node->next) {
if (node->val < x) {
lessTail->next = node;
lessTail = node;
} else {
geqTail->next = node;
geqTail = node;
}
}
geqTail->next = nullptr; // terminate the second chain
lessTail->next = geq.next; // join: [< x] then [>= x]
return less.next;
}
pub fn partition_values(head: Option<Box<ListNode>>, x: i32) -> Option<Box<ListNode>> {
let mut vals = Vec::new();
let mut p = head.as_ref();
while let Some(node) = p {
vals.push(node.val);
p = node.next.as_ref();
}
let ordered: Vec<i32> = vals
.iter()
.copied()
.filter(|&v| v < x)
.chain(vals.iter().copied().filter(|&v| v >= x))
.collect();
let mut head = head;
let mut p = head.as_mut();
for v in ordered {
let node = p.unwrap();
node.val = v;
p = node.next.as_mut();
}
head
}
pub fn partition(head: Option<Box<ListNode>>, x: i32) -> Option<Box<ListNode>> {
let mut less = Box::new(ListNode::new(0));
let mut geq = Box::new(ListNode::new(0));
let mut less_tail = &mut less;
let mut geq_tail = &mut geq;
let mut cur = head;
while let Some(mut node) = cur {
cur = node.next.take();
if node.val < x {
less_tail.next = Some(node);
less_tail = less_tail.next.as_mut().unwrap();
} else {
geq_tail.next = Some(node);
geq_tail = geq_tail.next.as_mut().unwrap();
}
}
less_tail.next = geq.next; // join: [< x] then [>= x]
less.next
}
function partitionValues(head: ListNode | null, x: number): ListNode | null {
const vals: number[] = [];
for (let node = head; node !== null; node = node.next) vals.push(node.val);
const ordered = vals.filter((v) => v < x).concat(vals.filter((v) => v >= x));
let i = 0;
for (let node = head; node !== null; node = node.next) node.val = ordered[i++];
return head;
}
function partition(head: ListNode | null, x: number): ListNode | null {
const less = new ListNode();
const geq = new ListNode();
let lessTail = less;
let geqTail = geq;
for (let node = head; node !== null; node = node.next) {
if (node.val < x) {
lessTail.next = node;
lessTail = node;
} else {
geqTail.next = node;
geqTail = node;
}
}
geqTail.next = null; // terminate the second chain
lessTail.next = geq.next; // join: [< x] then [>= x]
return less.next;
}
func partitionValues(head *ListNode, x int) *ListNode {
ordered := []int{}
for node := head; node != nil; node = node.Next {
if node.Val < x {
ordered = append(ordered, node.Val)
}
}
for node := head; node != nil; node = node.Next {
if node.Val >= x {
ordered = append(ordered, node.Val)
}
}
i := 0
for node := head; node != nil; node = node.Next {
node.Val = ordered[i]
i++
}
return head
}
func partition(head *ListNode, x int) *ListNode {
less, geq := &ListNode{}, &ListNode{}
lessTail, geqTail := less, geq
for node := head; node != nil; node = node.Next {
if node.Val < x {
lessTail.Next = node
lessTail = node
} else {
geqTail.Next = node
geqTail = node
}
}
geqTail.Next = nil // terminate the second chain
lessTail.Next = geq.Next // join: [< x] then [>= x]
return less.Next
}
func partitionValues(_ head: ListNode?, _ x: Int) -> ListNode? {
var vals: [Int] = []
var node = head
while let n = node {
vals.append(n.val)
node = n.next
}
let ordered = vals.filter { $0 < x } + vals.filter { $0 >= x }
node = head
for v in ordered {
node!.val = v
node = node!.next
}
return head
}
func partition(_ head: ListNode?, _ x: Int) -> ListNode? {
let less = ListNode()
let geq = ListNode()
var lessTail = less
var geqTail = geq
var node = head
while let cur = node {
if cur.val < x {
lessTail.next = cur
lessTail = cur
} else {
geqTail.next = cur
geqTail = cur
}
node = cur.next
}
geqTail.next = nil // terminate the second chain
lessTail.next = geq.next // join: [< x] then [>= x]
return less.next
}
Binary Tree General
22. Symmetric Tree
Easy · LC 101
Given a binary tree, decide whether it is a mirror image of itself around its center. Recurse on pairs of subtrees with a helper that requires the two roots to match, the first subtree's left child to mirror the second's right, and vice versa. The trick is comparing two subtrees at once with that crossed recursion, handling the null cases first so that two missing nodes match while exactly one missing node fails.
Serialize each level with None placeholders for missing children and require every level to read the same reversed. Correct, but it buffers whole levels and the placeholder bookkeeping obscures the real pairing between mirror nodes.
Compares exactly the two nodes that must match — no level buffers or placeholders, and it bails at the first mismatch. Handy when the tree is deep enough to threaten the recursion limit.
Two subtrees mirror each other iff their roots match, the first's left mirrors the second's right, and vice versa. Same pairing as the queue, but the call stack does the bookkeeping.
Serialize each level with null placeholders for missing children and require every level to read the same reversed. Correct, but it buffers whole levels and the placeholder bookkeeping obscures the real pairing between mirror nodes.
Two subtrees mirror each other iff their roots match, the first's left mirrors the second's right, and vice versa. Compares exactly the two nodes that must match — no level buffers or placeholders — and bails at the first mismatch.
Serialize each level with None placeholders for missing children and require every level to read the same reversed. Correct, but it buffers whole levels and the placeholder bookkeeping obscures the real pairing between mirror nodes.
Two subtrees mirror each other iff their roots match, the first's left mirrors the second's right, and vice versa. Compares exactly the two nodes that must match — no level buffers or placeholders — and bails at the first mismatch.
Serialize each level with null placeholders for missing children and require every level to read the same reversed. Correct, but it buffers whole levels and the placeholder bookkeeping obscures the real pairing between mirror nodes.
Two subtrees mirror each other iff their roots match, the first's left mirrors the second's right, and vice versa. Compares exactly the two nodes that must match — no level buffers or placeholders — and bails at the first mismatch.
Serialize each level with nil placeholders for missing children and require every level to read the same reversed. Correct, but it buffers whole levels and the placeholder bookkeeping obscures the real pairing between mirror nodes.
Two subtrees mirror each other iff their roots match, the first's left mirrors the second's right, and vice versa. Compares exactly the two nodes that must match — no level buffers or placeholders — and bails at the first mismatch.
Serialize each level with nil placeholders for missing children and require every level to read the same reversed. Correct, but it buffers whole levels and the placeholder bookkeeping obscures the real pairing between mirror nodes.
Two subtrees mirror each other iff their roots match, the first's left mirrors the second's right, and vice versa. Compares exactly the two nodes that must match — no level buffers or placeholders — and bails at the first mismatch.
def isSymmetric_level_palindrome(self, root: Optional[TreeNode]) -> bool:
if root is None:
return True
level = [root]
while level:
vals = []
nxt = []
for node in level:
for child in (node.left, node.right):
if child is not None:
vals.append(child.val)
nxt.append(child)
else:
vals.append(None)
if vals != vals[::-1]:
return False
level = nxt
return True
def isSymmetric_iterative_queue(self, root: Optional[TreeNode]) -> bool:
if root is None:
return True
queue = deque([(root.left, root.right)])
while queue:
a, b = queue.popleft()
if a is None and b is None:
continue
if a is None or b is None or a.val != b.val:
return False
queue.append((a.left, b.right))
queue.append((a.right, b.left))
return True
def isSymmetric(self, root: Optional[TreeNode]) -> bool:
def mirror(a: Optional[TreeNode], b: Optional[TreeNode]) -> bool:
if a is None and b is None:
return True
if a is None or b is None or a.val != b.val:
return False
return mirror(a.left, b.right) and mirror(a.right, b.left)
return root is None or mirror(root.left, root.right)
public:
bool isSymmetricLevelPalindrome(TreeNode* root) {
if (!root) return true;
vector<TreeNode*> level = {root};
while (!level.empty()) {
vector<optional<int>> vals;
vector<TreeNode*> next;
for (TreeNode* node : level) {
for (TreeNode* child : {node->left, node->right}) {
if (child) {
vals.push_back(child->val);
next.push_back(child);
} else {
vals.push_back(nullopt);
}
}
}
const size_t m = vals.size();
for (size_t l = 0; l < m / 2; ++l)
if (vals[l] != vals[m - 1 - l]) return false;
level = std::move(next);
}
return true;
}
private:
static bool mirror(TreeNode* a, TreeNode* b) {
if (!a && !b) return true;
if (!a || !b || a->val != b->val) return false;
return mirror(a->left, b->right) && mirror(a->right, b->left);
}
public:
bool isSymmetric(TreeNode* root) {
return !root || mirror(root->left, root->right);
}
pub fn is_symmetric_level_palindrome(root: Option<Rc<RefCell<TreeNode>>>) -> bool {
let mut level = match root {
None => return true,
Some(node) => vec![node],
};
while !level.is_empty() {
let mut vals: Vec<Option<i32>> = Vec::new();
let mut next = Vec::new();
for node in &level {
let node = node.borrow();
for child in [&node.left, &node.right] {
match child {
Some(c) => {
vals.push(Some(c.borrow().val));
next.push(Rc::clone(c));
}
None => vals.push(None),
}
}
}
let m = vals.len();
for l in 0..m / 2 {
if vals[l] != vals[m - 1 - l] {
return false;
}
}
level = next;
}
true
}
pub fn is_symmetric(root: Option<Rc<RefCell<TreeNode>>>) -> bool {
fn mirror(a: &Option<Rc<RefCell<TreeNode>>>, b: &Option<Rc<RefCell<TreeNode>>>) -> bool {
match (a, b) {
(None, None) => true,
(Some(x), Some(y)) => {
let x = x.borrow();
let y = y.borrow();
x.val == y.val && mirror(&x.left, &y.right) && mirror(&x.right, &y.left)
}
_ => false,
}
}
match root {
None => true,
Some(node) => {
let node = node.borrow();
mirror(&node.left, &node.right)
}
}
}
function isSymmetricLevelPalindrome(root: TreeNode | null): boolean {
if (root === null) return true;
let level: TreeNode[] = [root];
while (level.length > 0) {
const vals: (number | null)[] = [];
const next: TreeNode[] = [];
for (const node of level) {
for (const child of [node.left, node.right]) {
if (child !== null) {
vals.push(child.val);
next.push(child);
} else {
vals.push(null);
}
}
}
const m = vals.length;
for (let l = 0; l < Math.floor(m / 2); l++) {
if (vals[l] !== vals[m - 1 - l]) return false;
}
level = next;
}
return true;
}
function isSymmetric(root: TreeNode | null): boolean {
const mirror = (a: TreeNode | null, b: TreeNode | null): boolean => {
if (a === null && b === null) return true;
if (a === null || b === null || a.val !== b.val) return false;
return mirror(a.left, b.right) && mirror(a.right, b.left);
};
return root === null || mirror(root.left, root.right);
}
func isSymmetricLevelPalindrome(root *TreeNode) bool {
if root == nil {
return true
}
level := []*TreeNode{root}
for len(level) > 0 {
vals := []any{}
next := []*TreeNode{}
for _, node := range level {
for _, child := range []*TreeNode{node.Left, node.Right} {
if child != nil {
vals = append(vals, child.Val)
next = append(next, child)
} else {
vals = append(vals, nil)
}
}
}
for l, r := 0, len(vals)-1; l < r; l, r = l+1, r-1 {
if vals[l] != vals[r] {
return false
}
}
level = next
}
return true
}
func isSymmetric(root *TreeNode) bool {
if root == nil {
return true
}
return mirror(root.Left, root.Right)
}
func mirror(a, b *TreeNode) bool {
if a == nil && b == nil {
return true
}
if a == nil || b == nil || a.Val != b.Val {
return false
}
return mirror(a.Left, b.Right) && mirror(a.Right, b.Left)
}
func isSymmetricLevelPalindrome(_ root: TreeNode?) -> Bool {
guard let root = root else { return true }
var level: [TreeNode] = [root]
while !level.isEmpty {
var vals: [Int?] = []
var next: [TreeNode] = []
for node in level {
for child in [node.left, node.right] {
if let child = child {
vals.append(child.val)
next.append(child)
} else {
vals.append(nil)
}
}
}
let m = vals.count
for l in 0..<(m / 2) where vals[l] != vals[m - 1 - l] {
return false
}
level = next
}
return true
}
func isSymmetric(_ root: TreeNode?) -> Bool {
func mirror(_ a: TreeNode?, _ b: TreeNode?) -> Bool {
if a == nil && b == nil { return true }
guard let a = a, let b = b, a.val == b.val else { return false }
return mirror(a.left, b.right) && mirror(a.right, b.left)
}
guard let root = root else { return true }
return mirror(root.left, root.right)
}
23. Construct Binary Tree from Inorder and Postorder Traversal
Medium · LC 106
Given the inorder and postorder traversals of a binary tree with unique values, rebuild the original tree. Consume postorder from the back, where the last unused entry is always the current subtree's root, and use a hash map from value to inorder index to split the inorder range into left and right halves in constant time. The pitfall is the recursion order: postorder ends with left, right, root, so walking it backwards yields root then right then left, meaning the right subtree must be built before the left.
Find the root in inorder with a linear .index() scan, then recurse on freshly copied slices of both lists. Quadratic on skewed trees: fine at these test sizes, sluggish at LeetCode's 3000-node bound, and the slice copies burn memory for no benefit.
Walk postorder backwards (root, right, left), pushing as we go right. When the stack top equals the current inorder tail, that subtree's rightward run is done: pop back to the node whose LEFT child comes next. Linear time and leaner than a hashmap, but the invariant is subtle and easy to break under pressure.
The last unused postorder entry is always the current subtree's root; the hashmap finds its inorder position in O(1), splitting inorder into left/right index ranges — no slices, no scanning. Build the RIGHT subtree first — postorder ends ...left, right, root, so walking the array backwards yields root, right, left.
Scan the inorder range for the root value on every call. Quadratic when the tree is a chain — fine at these test sizes, sluggish at LeetCode's 3000-node bound — but it needs no extra bookkeeping.
The hashmap makes each split O(1): the last unused postorder entry is always the current subtree's root, and its inorder position divides the range. Build the RIGHT subtree first — postorder ends ...left, right, root, so walking backwards yields root, right, left.
Find the root (postorder's last element) in inorder with a linear scan, then recurse on the matching subslices. Quadratic when the tree is a chain — fine at these test sizes, sluggish at LeetCode's 3000-node bound — but it needs no extra bookkeeping.
The hashmap makes each split O(1): the last unused postorder entry is always the current subtree's root, and its inorder position divides the range. Build the RIGHT subtree first — postorder ends ...left, right, root, so walking backwards yields root, right, left.
Scan the inorder range for the root value on every call. Quadratic when the tree is a chain — fine at these test sizes, sluggish at LeetCode's 3000-node bound — but it needs no extra bookkeeping.
The hashmap makes each split O(1): the last unused postorder entry is always the current subtree's root, and its inorder position divides the range. Build the RIGHT subtree first — postorder ends ...left, right, root, so walking backwards yields root, right, left.
Scan the inorder range for the root value on every call. Quadratic when the tree is a chain — fine at these test sizes, sluggish at LeetCode's 3000-node bound — but it needs no extra bookkeeping.
The hashmap makes each split O(1): the last unused postorder entry is always the current subtree's root, and its inorder position divides the range. Build the RIGHT subtree first — postorder ends ...left, right, root, so walking backwards yields root, right, left.
Scan the inorder range for the root value on every call. Quadratic when the tree is a chain — fine at these test sizes, sluggish at LeetCode's 3000-node bound — but it needs no extra bookkeeping.
The hashmap makes each split O(1): the last unused postorder entry is always the current subtree's root, and its inorder position divides the range. Build the RIGHT subtree first — postorder ends ...left, right, root, so walking backwards yields root, right, left.
def buildTree_slicing(self, inorder: List[int], postorder: List[int]) -> Optional[TreeNode]:
if not inorder:
return None
val = postorder[-1]
mid = inorder.index(val)
node = TreeNode(val)
node.left = self.buildTree_slicing(inorder[:mid], postorder[:mid])
node.right = self.buildTree_slicing(inorder[mid + 1:], postorder[mid:-1])
return node
def buildTree_iterative(self, inorder: List[int], postorder: List[int]) -> Optional[TreeNode]:
if not postorder:
return None
root = TreeNode(postorder[-1])
stack = [root]
in_idx = len(inorder) - 1
for i in range(len(postorder) - 2, -1, -1):
val = postorder[i]
node = stack[-1]
if node.val != inorder[in_idx]:
node.right = TreeNode(val)
stack.append(node.right)
else:
while stack and stack[-1].val == inorder[in_idx]:
node = stack.pop()
in_idx -= 1
node.left = TreeNode(val)
stack.append(node.left)
return root
def buildTree(self, inorder: List[int], postorder: List[int]) -> Optional[TreeNode]:
idx = {val: i for i, val in enumerate(inorder)}
post = len(postorder) - 1
def build(lo: int, hi: int) -> Optional[TreeNode]:
nonlocal post
if lo > hi:
return None
val = postorder[post]
post -= 1
node = TreeNode(val)
mid = idx[val]
node.right = build(mid + 1, hi) # right before left!
node.left = build(lo, mid - 1)
return node
return build(0, len(inorder) - 1)
TreeNode* buildRangeLinear(const vector<int>& inorder, int ilo, int ihi,
const vector<int>& postorder, int plo, int phi) {
if (ilo > ihi) return nullptr;
int val = postorder[phi];
int mid = ilo;
while (inorder[mid] != val) ++mid; // the linear scan
int leftSize = mid - ilo;
TreeNode* node = new TreeNode(val);
node->left = buildRangeLinear(inorder, ilo, mid - 1,
postorder, plo, plo + leftSize - 1);
node->right = buildRangeLinear(inorder, mid + 1, ihi,
postorder, plo + leftSize, phi - 1);
return node;
}
public:
TreeNode* buildTreeLinearSearch(vector<int>& inorder, vector<int>& postorder) {
return buildRangeLinear(inorder, 0, (int)inorder.size() - 1,
postorder, 0, (int)postorder.size() - 1);
}
private:
unordered_map<int, int> idx; // value -> inorder position (values unique)
int post = 0; // next unused postorder index, from the back
TreeNode* build(const vector<int>& postorder, int lo, int hi) {
if (lo > hi) return nullptr;
int val = postorder[post--];
TreeNode* node = new TreeNode(val);
int mid = idx[val];
node->right = build(postorder, mid + 1, hi); // right before left!
node->left = build(postorder, lo, mid - 1);
return node;
}
public:
TreeNode* buildTree(vector<int>& inorder, vector<int>& postorder) {
idx.clear();
for (int i = 0; i < (int)inorder.size(); ++i) idx[inorder[i]] = i;
post = (int)postorder.size() - 1;
return build(postorder, 0, (int)inorder.size() - 1);
}
pub fn build_tree_linear_search(
inorder: Vec<i32>,
postorder: Vec<i32>,
) -> Option<Rc<RefCell<TreeNode>>> {
fn build(inorder: &[i32], postorder: &[i32]) -> Option<Rc<RefCell<TreeNode>>> {
let n = postorder.len();
if n == 0 {
return None;
}
let val = postorder[n - 1];
let mid = inorder.iter().position(|&v| v == val).unwrap(); // the linear scan
let node = Rc::new(RefCell::new(TreeNode::new(val)));
node.borrow_mut().left = build(&inorder[..mid], &postorder[..mid]);
node.borrow_mut().right = build(&inorder[mid + 1..], &postorder[mid..n - 1]);
Some(node)
}
build(&inorder, &postorder)
}
pub fn build_tree(inorder: Vec<i32>, postorder: Vec<i32>) -> Option<Rc<RefCell<TreeNode>>> {
// Recurse over the half-open inorder range [lo, hi); `post` counts
// down through the postorder array from the back.
fn build(
idx: &HashMap<i32, usize>,
postorder: &[i32],
post: &mut usize,
lo: usize,
hi: usize,
) -> Option<Rc<RefCell<TreeNode>>> {
if lo >= hi {
return None;
}
*post -= 1;
let val = postorder[*post];
let mid = idx[&val];
let node = Rc::new(RefCell::new(TreeNode::new(val)));
node.borrow_mut().right = build(idx, postorder, post, mid + 1, hi); // right first!
node.borrow_mut().left = build(idx, postorder, post, lo, mid);
Some(node)
}
// value -> inorder position (values are unique)
let idx: HashMap<i32, usize> = inorder.iter().enumerate().map(|(i, &v)| (v, i)).collect();
let mut post = postorder.len();
build(&idx, &postorder, &mut post, 0, inorder.len())
}
function buildTreeLinearSearch(inorder: number[], postorder: number[]): TreeNode | null {
const build = (ilo: number, ihi: number, plo: number, phi: number): TreeNode | null => {
if (ilo > ihi) return null;
const val = postorder[phi];
let mid = ilo;
while (inorder[mid] !== val) mid++; // the linear scan
const leftSize = mid - ilo;
const node = new TreeNode(val);
node.left = build(ilo, mid - 1, plo, plo + leftSize - 1);
node.right = build(mid + 1, ihi, plo + leftSize, phi - 1);
return node;
};
return build(0, inorder.length - 1, 0, postorder.length - 1);
}
function buildTree(inorder: number[], postorder: number[]): TreeNode | null {
// value -> inorder position (values are unique)
const idx = new Map<number, number>();
inorder.forEach((v, i) => idx.set(v, i));
let post = postorder.length - 1; // next unused postorder index
const build = (lo: number, hi: number): TreeNode | null => {
if (lo > hi) return null;
const val = postorder[post--];
const node = new TreeNode(val);
const mid = idx.get(val)!;
node.right = build(mid + 1, hi); // right before left!
node.left = build(lo, mid - 1);
return node;
};
return build(0, inorder.length - 1);
}
func buildTreeLinearSearch(inorder []int, postorder []int) *TreeNode {
var build func(ilo, ihi, plo, phi int) *TreeNode
build = func(ilo, ihi, plo, phi int) *TreeNode {
if ilo > ihi {
return nil
}
val := postorder[phi]
mid := ilo
for inorder[mid] != val { // the linear scan
mid++
}
leftSize := mid - ilo
node := &TreeNode{Val: val}
node.Left = build(ilo, mid-1, plo, plo+leftSize-1)
node.Right = build(mid+1, ihi, plo+leftSize, phi-1)
return node
}
return build(0, len(inorder)-1, 0, len(postorder)-1)
}
func buildTree(inorder []int, postorder []int) *TreeNode {
idx := make(map[int]int, len(inorder)) // value -> inorder position (values unique)
for i, v := range inorder {
idx[v] = i
}
post := len(postorder) - 1 // next unused postorder index, from the back
var build func(lo, hi int) *TreeNode
build = func(lo, hi int) *TreeNode {
if lo > hi {
return nil
}
val := postorder[post]
post--
node := &TreeNode{Val: val}
mid := idx[val]
node.Right = build(mid+1, hi) // right before left!
node.Left = build(lo, mid-1)
return node
}
return build(0, len(inorder)-1)
}
func buildTreeLinearSearch(_ inorder: [Int], _ postorder: [Int]) -> TreeNode? {
func build(_ ilo: Int, _ ihi: Int, _ plo: Int, _ phi: Int) -> TreeNode? {
if ilo > ihi { return nil }
let val = postorder[phi]
var mid = ilo
while inorder[mid] != val { mid += 1 } // the linear scan
let leftSize = mid - ilo
let node = TreeNode(val)
node.left = build(ilo, mid - 1, plo, plo + leftSize - 1)
node.right = build(mid + 1, ihi, plo + leftSize, phi - 1)
return node
}
return build(0, inorder.count - 1, 0, postorder.count - 1)
}
func buildTree(_ inorder: [Int], _ postorder: [Int]) -> TreeNode? {
// value -> inorder position (values are unique)
var idx = [Int: Int](minimumCapacity: inorder.count)
for (i, v) in inorder.enumerated() { idx[v] = i }
var post = postorder.count - 1 // next unused postorder index
func build(_ lo: Int, _ hi: Int) -> TreeNode? {
if lo > hi { return nil }
let val = postorder[post]
post -= 1
let node = TreeNode(val)
let mid = idx[val]!
node.right = build(mid + 1, hi) // right before left!
node.left = build(lo, mid - 1)
return node
}
return build(0, inorder.count - 1)
}
24. Populating Next Right Pointers in Each Node II
Medium · LC 117
Given a binary tree that need not be perfect, connect each node's next pointer to the node immediately to its right on the same level. Walk each level through the next pointers already installed, appending every child encountered onto a dummy-headed list; when the level is exhausted, the dummy's next is the head of the level below. The insight is that a fully linked level is a free traversal of that level, which eliminates the queue a plain breadth-first pass would need and drops the extra space to constant.
The obvious level-order pass, linking neighbors as they dequeue. Simple, but the queue costs O(w) — the follow-up asks for O(1).
The queue is dead weight: walk the current level via its next pointers, appending every child onto a dummy-headed list; dummy.next is then the next level's head.
The obvious level-order pass, linking neighbors as they dequeue. Simple, but the queue costs O(w) — the follow-up asks for O(1).
The queue is dead weight: walk the current level via its next pointers, appending every child onto a dummy-headed list — dummy.next is then the head of the level below.
The obvious level-order pass, linking neighbors as they dequeue. Simple, but the queue costs O(w) — the follow-up asks for O(1).
The queue is dead weight: walk the current level via its next pointers, appending every child onto a dummy-headed list — dummy.next is then the head of the level below.
The obvious level-order pass, linking neighbors as they dequeue. Simple, but the queue costs O(w) — the follow-up asks for O(1).
The queue is dead weight: walk the current level via its next pointers, appending every child onto a dummy-headed list — dummy.next is then the head of the level below.
The obvious level-order pass, linking neighbors as they dequeue. Simple, but the queue costs O(w) — the follow-up asks for O(1).
The queue is dead weight: walk the current level via its next pointers, appending every child onto a dummy-headed list — dummy.Next is then the head of the level below.
The obvious level-order pass, linking neighbors as they dequeue. Simple, but the queue costs O(w) — the follow-up asks for O(1).
The queue is dead weight: walk the current level via its next pointers, appending every child onto a dummy-headed list — dummy.next is then the head of the level below.
def connect_bfs(self, root: 'Node') -> 'Node':
if not root:
return None
queue = deque([root])
while queue:
prev = None
for _ in range(len(queue)):
node = queue.popleft()
if prev:
prev.next = node
prev = node
if node.left:
queue.append(node.left)
if node.right:
queue.append(node.right)
return root
def connect(self, root: 'Node') -> 'Node':
level = root
while level:
dummy = Node()
tail = dummy
node = level
while node:
if node.left:
tail.next = node.left
tail = tail.next
if node.right:
tail.next = node.right
tail = tail.next
node = node.next
level = dummy.next
return root
Node *connectBfs(Node *root) {
if (!root) return nullptr;
queue<Node *> q;
q.push(root);
while (!q.empty()) {
size_t width = q.size();
Node *prev = nullptr;
for (size_t i = 0; i < width; ++i) {
Node *node = q.front();
q.pop();
if (prev) prev->next = node;
prev = node;
if (node->left) q.push(node->left);
if (node->right) q.push(node->right);
}
}
return root;
}
Node *connect(Node *root) {
Node *level = root;
while (level) {
Node dummy;
Node *tail = &dummy;
for (Node *node = level; node; node = node->next) {
if (node->left) {
tail->next = node->left;
tail = tail->next;
}
if (node->right) {
tail->next = node->right;
tail = tail->next;
}
}
level = dummy.next;
}
return root;
}
pub fn connect_bfs(root: Link) -> Link {
let mut queue = VecDeque::new();
if let Some(r) = root.clone() {
queue.push_back(r);
}
while !queue.is_empty() {
let width = queue.len();
let mut prev: Link = None;
for _ in 0..width {
let node = queue.pop_front().unwrap();
if let Some(p) = prev {
p.borrow_mut().next = Some(Rc::clone(&node));
}
{
let n = node.borrow();
if let Some(left) = n.left.clone() {
queue.push_back(left);
}
if let Some(right) = n.right.clone() {
queue.push_back(right);
}
}
prev = Some(node);
}
}
root
}
pub fn connect(root: Link) -> Link {
let mut level = root.clone();
while level.is_some() {
let dummy = Rc::new(RefCell::new(Node::new(0)));
let mut tail = Rc::clone(&dummy);
let mut cur = level.clone();
while let Some(node) = cur {
let n = node.borrow();
if let Some(left) = n.left.clone() {
tail.borrow_mut().next = Some(Rc::clone(&left));
tail = left;
}
if let Some(right) = n.right.clone() {
tail.borrow_mut().next = Some(Rc::clone(&right));
tail = right;
}
cur = n.next.clone();
}
level = dummy.borrow().next.clone();
}
root
}
function connectBfs(root: _Node | null): _Node | null {
if (!root) return null;
const queue: _Node[] = [root];
let head = 0;
while (head < queue.length) {
const width = queue.length - head;
let prev: _Node | null = null;
for (let i = 0; i < width; i++) {
const node = queue[head++];
if (prev) prev.next = node;
prev = node;
if (node.left) queue.push(node.left);
if (node.right) queue.push(node.right);
}
}
return root;
}
function connect(root: _Node | null): _Node | null {
let level = root;
while (level !== null) {
const dummy = new _Node();
let tail = dummy;
for (let node: _Node | null = level; node !== null; node = node.next) {
if (node.left) {
tail.next = node.left;
tail = node.left;
}
if (node.right) {
tail.next = node.right;
tail = node.right;
}
}
level = dummy.next;
}
return root;
}
func connectBfs(root *Node) *Node {
if root == nil {
return nil
}
queue := []*Node{root}
for len(queue) > 0 {
width := len(queue)
var prev *Node
for i := 0; i < width; i++ {
node := queue[0]
queue = queue[1:]
if prev != nil {
prev.Next = node
}
prev = node
if node.Left != nil {
queue = append(queue, node.Left)
}
if node.Right != nil {
queue = append(queue, node.Right)
}
}
}
return root
}
func connect(root *Node) *Node {
level := root
for level != nil {
dummy := &Node{}
tail := dummy
for node := level; node != nil; node = node.Next {
if node.Left != nil {
tail.Next = node.Left
tail = tail.Next
}
if node.Right != nil {
tail.Next = node.Right
tail = tail.Next
}
}
level = dummy.Next
}
return root
}
func connectBfs(_ root: Node?) -> Node? {
guard let root = root else { return nil }
var queue = [root]
var head = 0
while head < queue.count {
let width = queue.count - head
var prev: Node? = nil
for _ in 0..<width {
let node = queue[head]
head += 1
prev?.next = node
prev = node
if let left = node.left { queue.append(left) }
if let right = node.right { queue.append(right) }
}
}
return root
}
func connect(_ root: Node?) -> Node? {
var level = root
while level != nil {
let dummy = Node(0)
var tail = dummy
var node = level
while let current = node {
if let left = current.left {
tail.next = left
tail = left
}
if let right = current.right {
tail.next = right
tail = right
}
node = current.next
}
level = dummy.next
}
return root
}
25. Flatten Binary Tree to Linked List
Medium · LC 114
Given a binary tree, flatten it in place into a linked list that follows preorder order, using each node's right pointer with every left pointer set to null. Walk down the right spine, and whenever a node has a left child, find the rightmost node of that left subtree — the node's preorder predecessor — splice the current right subtree onto it, then hoist the left subtree into the right slot. The trick is that this Morris-style rewiring needs no stack or recursion at all: each edge is walked at most twice, so the whole pass stays O(n) time in O(1) space.
Classic preorder (push right, then left, so left pops first); each popped node's right becomes whatever is next on the stack. Simple, but the stack can hold a whole path's siblings at once.
Visit right, left, root — exactly preorder reversed — and point each node's right at the previously visited node. The list assembles back-to-front, so no subtree is ever overwritten before it has been visited. Drops the stack to recursion depth only.
For each node with a left child, splice the right subtree onto the rightmost node of the left subtree (the node's preorder predecessor), then hoist the left subtree into the right slot. Each edge is walked at most twice, so the total stays linear — and no auxiliary storage at all.
Record every node in preorder, then chain them along the right pointers. Two passes and a whole extra array of pointers — the clarity of the definition, at the cost of linear scratch space.
For each node with a left child, splice the right subtree onto the rightmost node of the left subtree (the node's preorder predecessor), then hoist the left subtree into the right slot. Each edge is walked at most twice, so the total stays linear — no list, no recursion, no auxiliary storage at all.
Record every node in preorder, then chain them along the right pointers. Two passes and a whole extra vector of Rc handles — the clarity of the definition, at the cost of linear scratch space.
For each node with a left child, splice the right subtree onto the rightmost node of the left subtree (the node's preorder predecessor), then hoist the left subtree into the right slot. Each edge is walked at most twice, so the total stays linear — no list, no recursion, no auxiliary storage at all.
Record every node in preorder, then chain them along the right pointers. Two passes and a whole extra array of references — the clarity of the definition, at the cost of linear scratch space.
For each node with a left child, splice the right subtree onto the rightmost node of the left subtree (the node's preorder predecessor), then hoist the left subtree into the right slot. Each edge is walked at most twice, so the total stays linear — no list, no recursion, no auxiliary storage at all.
Record every node in preorder, then chain them along the right pointers. Two passes and a whole extra slice of pointers — the clarity of the definition, at the cost of linear scratch space.
For each node with a left child, splice the right subtree onto the rightmost node of the left subtree (the node's preorder predecessor), then hoist the left subtree into the right slot. Each edge is walked at most twice, so the total stays linear — no list, no recursion, no auxiliary storage at all.
Record every node in preorder, then chain them along the right pointers. Two passes and a whole extra array of references — the clarity of the definition, at the cost of linear scratch space.
For each node with a left child, splice the right subtree onto the rightmost node of the left subtree (the node's preorder predecessor), then hoist the left subtree into the right slot. Each edge is walked at most twice, so the total stays linear — no list, no recursion, no auxiliary storage at all.
def flatten_stack(self, root: Optional[TreeNode]) -> None:
if root is None:
return
stack = [root]
while stack:
node = stack.pop()
if node.right:
stack.append(node.right)
if node.left:
stack.append(node.left)
node.left = None
node.right = stack[-1] if stack else None
def flatten_reverse_preorder(self, root: Optional[TreeNode]) -> None:
prev = None
def visit(node: Optional[TreeNode]) -> None:
nonlocal prev
if node is None:
return
visit(node.right)
visit(node.left)
node.right = prev
node.left = None
prev = node
visit(root)
def flatten(self, root: Optional[TreeNode]) -> None:
node = root
while node:
if node.left:
pred = node.left
while pred.right:
pred = pred.right
pred.right = node.right
node.right = node.left
node.left = None
node = node.right
static void collectPreorder(TreeNode* node, vector<TreeNode*>& order) {
if (!node) return;
order.push_back(node);
collectPreorder(node->left, order);
collectPreorder(node->right, order);
}
public:
void flattenPreorderList(TreeNode* root) {
vector<TreeNode*> order;
collectPreorder(root, order);
for (size_t i = 0; i < order.size(); ++i) {
order[i]->left = nullptr;
order[i]->right = i + 1 < order.size() ? order[i + 1] : nullptr;
}
}
void flatten(TreeNode* root) {
for (TreeNode* node = root; node; node = node->right) {
if (node->left) {
TreeNode* pred = node->left;
while (pred->right) pred = pred->right;
pred->right = node->right;
node->right = node->left;
node->left = nullptr;
}
}
}
pub fn flatten_preorder_list(root: &mut Option<Rc<RefCell<TreeNode>>>) {
fn collect(node: &Option<Rc<RefCell<TreeNode>>>, order: &mut Vec<Rc<RefCell<TreeNode>>>) {
if let Some(n) = node {
order.push(Rc::clone(n));
let left = n.borrow().left.clone();
let right = n.borrow().right.clone();
collect(&left, order);
collect(&right, order);
}
}
let mut order = Vec::new();
collect(root, &mut order);
for i in 0..order.len() {
let mut node = order[i].borrow_mut();
node.left = None;
node.right = if i + 1 < order.len() {
Some(Rc::clone(&order[i + 1]))
} else {
None
};
}
}
pub fn flatten(root: &mut Option<Rc<RefCell<TreeNode>>>) {
let mut node = root.clone();
while let Some(n) = node {
let left = n.borrow_mut().left.take();
if let Some(l) = left {
// Find the rightmost node of the left subtree.
let mut pred = Rc::clone(&l);
loop {
let next = pred.borrow().right.clone();
match next {
Some(r) => pred = r,
None => break,
}
}
let right = n.borrow_mut().right.take();
pred.borrow_mut().right = right;
n.borrow_mut().right = Some(l);
}
node = n.borrow().right.clone();
}
}
function flattenPreorderList(root: TreeNode | null): void {
const order: TreeNode[] = [];
const collect = (node: TreeNode | null): void => {
if (node === null) return;
order.push(node);
collect(node.left);
collect(node.right);
};
collect(root);
for (let i = 0; i < order.length; i++) {
order[i].left = null;
order[i].right = i + 1 < order.length ? order[i + 1] : null;
}
}
/**
Do not return anything, modify root in-place instead.
*/
function flatten(root: TreeNode | null): void {
for (let node = root; node !== null; node = node.right) {
if (node.left !== null) {
let pred = node.left;
while (pred.right !== null) pred = pred.right;
pred.right = node.right;
node.right = node.left;
node.left = null;
}
}
}
func flattenPreorderList(root *TreeNode) {
order := []*TreeNode{}
var collect func(node *TreeNode)
collect = func(node *TreeNode) {
if node == nil {
return
}
order = append(order, node)
collect(node.Left)
collect(node.Right)
}
collect(root)
for i, node := range order {
node.Left = nil
if i+1 < len(order) {
node.Right = order[i+1]
} else {
node.Right = nil
}
}
}
func flatten(root *TreeNode) {
for node := root; node != nil; node = node.Right {
if node.Left != nil {
pred := node.Left
for pred.Right != nil {
pred = pred.Right
}
pred.Right = node.Right
node.Right = node.Left
node.Left = nil
}
}
}
func flattenPreorderList(_ root: TreeNode?) {
var order: [TreeNode] = []
func collect(_ node: TreeNode?) {
guard let node = node else { return }
order.append(node)
collect(node.left)
collect(node.right)
}
collect(root)
for (i, node) in order.enumerated() {
node.left = nil
node.right = i + 1 < order.count ? order[i + 1] : nil
}
}
func flatten(_ root: TreeNode?) {
var node = root
while let current = node {
if let left = current.left {
var pred = left
while let next = pred.right {
pred = next
}
pred.right = current.right
current.right = left
current.left = nil
}
node = current.right
}
}
26. Path Sum
Easy · LC 112
Given a binary tree and a target, decide whether any root-to-leaf path's values sum exactly to the target. Recurse down the tree subtracting each node's value from the target, and at a leaf check that the remaining target equals the leaf's value. The pitfall is that the path must end at a leaf and values may be negative, so hitting the target mid-path proves nothing and no branch can be pruned for overshooting.
Collect the total of every root-to-leaf path into a list, then ask whether the target is in it. Does all the traversal work up front: no early exit, and the list of sums is pure overhead.
Same math with an explicit stack — each entry remembers how much of the target is left once its node is consumed. Stops at the first matching leaf instead of enumerating them all, and no recursion limit to worry about.
At a leaf the remaining target must equal the leaf's value; inner nodes just pass the reduced target to both children. Short-circuits on the first hit, and the stack never exceeds the tree's height.
Collect the total of every root-to-leaf path, then look for the target among them. Does all the traversal work up front: no early exit, and the list of sums is pure overhead.
At a leaf the remaining target must equal the leaf's value; inner nodes just pass the reduced target to both children. Short-circuits on the first hit and stores nothing beyond the call stack.
Collect the total of every root-to-leaf path, then look for the target among them. Does all the traversal work up front: no early exit, and the vector of sums is pure overhead.
At a LEAF the remaining target must equal the leaf's value; inner nodes just pass the reduced target to both children. Short-circuits on the first hit and stores nothing beyond the call stack.
Collect the total of every root-to-leaf path, then look for the target among them. Does all the traversal work up front: no early exit, and the array of sums is pure overhead.
At a LEAF the remaining target must equal the leaf's value; inner nodes just pass the reduced target to both children. Short-circuits on the first hit and stores nothing beyond the call stack.
Collect the total of every root-to-leaf path, then look for the target among them. Does all the traversal work up front: no early exit, and the list of sums is pure overhead.
At a LEAF the remaining target must equal the leaf's value; inner nodes just pass the reduced target to both children. Short-circuits on the first hit and stores nothing beyond the call stack.
Collect the total of every root-to-leaf path, then look for the target among them. Does all the traversal work up front: no early exit, and the array of sums is pure overhead.
At a LEAF the remaining target must equal the leaf's value; inner nodes just pass the reduced target to both children. Short-circuits on the first hit and stores nothing beyond the call stack.
def hasPathSum_all_sums(self, root: Optional[TreeNode], targetSum: int) -> bool:
sums: List[int] = []
def collect(node: Optional[TreeNode], acc: int) -> None:
if node is None:
return
acc += node.val
if node.left is None and node.right is None:
sums.append(acc)
return
collect(node.left, acc)
collect(node.right, acc)
collect(root, 0)
return targetSum in sums
def hasPathSum_iterative_stack(self, root: Optional[TreeNode], targetSum: int) -> bool:
if root is None:
return False
stack = [(root, targetSum)]
while stack:
node, remaining = stack.pop()
if node.left is None and node.right is None:
if remaining == node.val:
return True
continue
if node.right:
stack.append((node.right, remaining - node.val))
if node.left:
stack.append((node.left, remaining - node.val))
return False
def hasPathSum(self, root: Optional[TreeNode], targetSum: int) -> bool:
if root is None:
return False
if root.left is None and root.right is None:
return targetSum == root.val
remaining = targetSum - root.val
return (self.hasPathSum(root.left, remaining)
or self.hasPathSum(root.right, remaining))
static void collectSums(TreeNode* node, int acc, vector<int>& sums) {
if (!node) return;
acc += node->val;
if (!node->left && !node->right) {
sums.push_back(acc);
return;
}
collectSums(node->left, acc, sums);
collectSums(node->right, acc, sums);
}
public:
bool hasPathSumAllSums(TreeNode* root, int targetSum) {
vector<int> sums;
collectSums(root, 0, sums);
for (int s : sums)
if (s == targetSum) return true;
return false;
}
bool hasPathSum(TreeNode* root, int targetSum) {
if (!root) return false;
if (!root->left && !root->right) return targetSum == root->val;
int remaining = targetSum - root->val;
return hasPathSum(root->left, remaining) || hasPathSum(root->right, remaining);
}
pub fn has_path_sum_all_sums(root: Option<Rc<RefCell<TreeNode>>>, target_sum: i32) -> bool {
fn collect(node: &Option<Rc<RefCell<TreeNode>>>, acc: i32, sums: &mut Vec<i32>) {
if let Some(n) = node {
let n = n.borrow();
let acc = acc + n.val;
if n.left.is_none() && n.right.is_none() {
sums.push(acc);
} else {
collect(&n.left, acc, sums);
collect(&n.right, acc, sums);
}
}
}
let mut sums = Vec::new();
collect(&root, 0, &mut sums);
sums.contains(&target_sum)
}
pub fn has_path_sum(root: Option<Rc<RefCell<TreeNode>>>, target_sum: i32) -> bool {
match root {
None => false,
Some(node) => {
let node = node.borrow();
if node.left.is_none() && node.right.is_none() {
return target_sum == node.val;
}
let remaining = target_sum - node.val;
Self::has_path_sum(node.left.clone(), remaining)
|| Self::has_path_sum(node.right.clone(), remaining)
}
}
}
function hasPathSumAllSums(root: TreeNode | null, targetSum: number): boolean {
const sums: number[] = [];
const collect = (node: TreeNode | null, acc: number): void => {
if (node === null) return;
acc += node.val;
if (node.left === null && node.right === null) {
sums.push(acc);
return;
}
collect(node.left, acc);
collect(node.right, acc);
};
collect(root, 0);
return sums.includes(targetSum);
}
function hasPathSum(root: TreeNode | null, targetSum: number): boolean {
if (root === null) return false;
if (root.left === null && root.right === null) return targetSum === root.val;
const remaining = targetSum - root.val;
return hasPathSum(root.left, remaining) || hasPathSum(root.right, remaining);
}
func hasPathSumAllSums(root *TreeNode, targetSum int) bool {
sums := []int{}
var collect func(node *TreeNode, acc int)
collect = func(node *TreeNode, acc int) {
if node == nil {
return
}
acc += node.Val
if node.Left == nil && node.Right == nil {
sums = append(sums, acc)
return
}
collect(node.Left, acc)
collect(node.Right, acc)
}
collect(root, 0)
for _, s := range sums {
if s == targetSum {
return true
}
}
return false
}
func hasPathSum(root *TreeNode, targetSum int) bool {
if root == nil {
return false
}
if root.Left == nil && root.Right == nil {
return targetSum == root.Val
}
remaining := targetSum - root.Val
return hasPathSum(root.Left, remaining) || hasPathSum(root.Right, remaining)
}
func hasPathSumAllSums(_ root: TreeNode?, _ targetSum: Int) -> Bool {
var sums: [Int] = []
func collect(_ node: TreeNode?, _ acc: Int) {
guard let node = node else { return }
let acc = acc + node.val
if node.left == nil && node.right == nil {
sums.append(acc)
return
}
collect(node.left, acc)
collect(node.right, acc)
}
collect(root, 0)
return sums.contains(targetSum)
}
func hasPathSum(_ root: TreeNode?, _ targetSum: Int) -> Bool {
guard let root = root else { return false }
if root.left == nil && root.right == nil {
return targetSum == root.val
}
let remaining = targetSum - root.val
return hasPathSum(root.left, remaining) || hasPathSum(root.right, remaining)
}
27. Sum Root to Leaf Numbers
Medium · LC 129
Given a binary tree whose nodes each hold a single digit, treat every root-to-leaf path as a decimal number and return the sum of all of them. Recurse from the root carrying a running prefix of prefix times ten plus the current digit; a leaf returns the finished number, and inner nodes return the sum of their two children's results. The trick is that the arithmetic replaces any string building, so each node costs O(1) and the only storage is the O(h) recursion stack.
The most literal reading of the statement: walk to every leaf while building the digit string, then int() each path and add them up. Copies an O(h) string at every step and stores every path.
Replace the strings with the arithmetic prefix*10 + digit, so each node costs O(1) and nothing is stored beyond the frontier. Queue width is the tree's widest level, which can still be O(n).
Depth-first order shrinks the frontier from a level (O(w)) to a root-to-leaf path (O(h)); each stack entry pairs a node with the number spelled by the path above it.
Same complexity as the explicit stack, with the bookkeeping handed to the call stack: carry prefix*10 + digit down the tree; a leaf returns the finished number, inner nodes sum their children.
The most literal reading of the statement: walk to every leaf while building the digit string, then parse each path and add them up. Copies an O(h) string at every step and stores every path.
Replace the strings with the arithmetic prefix*10 + digit, so each node costs O(1) and only the current root-to-leaf path is stored; each stack entry pairs a node with the number spelled above it.
Same complexity as the explicit stack, with the bookkeeping handed to the call stack: carry prefix*10 + digit down the tree; a leaf returns the finished number, inner nodes sum their children.
The most literal reading of the statement: walk to every leaf while building the digit string, then parse each path and add them up. Copies an O(h) string at every step and stores every path.
Replace the strings with the arithmetic prefix*10 + digit, so each node costs O(1) and only the current root-to-leaf path is stored; each stack entry pairs a node with the number spelled above it.
Same complexity as the explicit stack, with the bookkeeping handed to the call stack: carry prefix*10 + digit down the tree; a leaf returns the finished number, inner nodes sum their children.
The most literal reading of the statement: walk to every leaf while building the digit string, then parse each path and add them up. Copies an O(h) string at every step and stores every path.
Replace the strings with the arithmetic prefix*10 + digit, so each node costs O(1) and only the current root-to-leaf path is stored; each stack entry pairs a node with the number spelled above it.
Same complexity as the explicit stack, with the bookkeeping handed to the call stack: carry prefix*10 + digit down the tree; a leaf returns the finished number, inner nodes sum their children.
The most literal reading of the statement: walk to every leaf while building the digit string, then parse each path and add them up. Copies an O(h) string at every step and stores every path.
Replace the strings with the arithmetic prefix*10 + digit, so each node costs O(1) and only the current root-to-leaf path is stored; each stack entry pairs a node with the number spelled above it.
Same complexity as the explicit stack, with the bookkeeping handed to the call stack: carry prefix*10 + digit down the tree; a leaf returns the finished number, inner nodes sum their children.
The most literal reading of the statement: walk to every leaf while building the digit string, then parse each path and add them up. Copies an O(h) string at every step and stores every path.
Replace the strings with the arithmetic prefix*10 + digit, so each node costs O(1) and only the current root-to-leaf path is stored; each stack entry pairs a node with the number spelled above it.
Same complexity as the explicit stack, with the bookkeeping handed to the call stack: carry prefix*10 + digit down the tree; a leaf returns the finished number, inner nodes sum their children.
def sumNumbers_path_strings(self, root: "TreeNode | None") -> int:
paths: "list[str]" = []
def walk(node: "TreeNode | None", digits: str) -> None:
if node is None:
return
digits += str(node.val)
if node.left is None and node.right is None:
paths.append(digits)
return
walk(node.left, digits)
walk(node.right, digits)
walk(root, "")
return sum(int(p) for p in paths)
def sumNumbers_bfs(self, root: "TreeNode | None") -> int:
if root is None:
return 0
total = 0
queue = deque([(root, 0)])
while queue:
node, prefix = queue.popleft()
prefix = prefix * 10 + node.val
if node.left is None and node.right is None:
total += prefix
continue
if node.left is not None:
queue.append((node.left, prefix))
if node.right is not None:
queue.append((node.right, prefix))
return total
def sumNumbers_iterative(self, root: "TreeNode | None") -> int:
if root is None:
return 0
total = 0
stack = [(root, 0)]
while stack:
node, prefix = stack.pop()
prefix = prefix * 10 + node.val
if node.left is None and node.right is None:
total += prefix
continue
if node.right is not None:
stack.append((node.right, prefix))
if node.left is not None:
stack.append((node.left, prefix))
return total
def sumNumbers(self, root: "TreeNode | None") -> int:
def dfs(node: "TreeNode | None", prefix: int) -> int:
if node is None:
return 0
prefix = prefix * 10 + node.val
if node.left is None and node.right is None:
return prefix
return dfs(node.left, prefix) + dfs(node.right, prefix)
return dfs(root, 0)
int sumNumbersPathStrings(TreeNode* root) {
std::vector<std::string> paths;
auto collect = [&paths](auto&& self, TreeNode* node, std::string digits) -> void {
if (node == nullptr) return;
digits += static_cast<char>('0' + node->val);
if (node->left == nullptr && node->right == nullptr) {
paths.push_back(std::move(digits));
return;
}
self(self, node->left, digits);
self(self, node->right, std::move(digits));
};
collect(collect, root, "");
int total = 0;
for (const std::string& digits : paths) total += std::stoi(digits);
return total;
}
int sumNumbersIterative(TreeNode* root) {
if (root == nullptr) return 0;
int total = 0;
std::stack<std::pair<TreeNode*, int>> stack;
stack.push({root, 0});
while (!stack.empty()) {
auto [node, prefix] = stack.top();
stack.pop();
prefix = prefix * 10 + node->val;
if (node->left == nullptr && node->right == nullptr) {
total += prefix;
continue;
}
if (node->right != nullptr) stack.push({node->right, prefix});
if (node->left != nullptr) stack.push({node->left, prefix});
}
return total;
}
int sumNumbers(TreeNode* root) {
return dfs(root, 0);
}
private:
int dfs(TreeNode* node, int prefix) {
if (node == nullptr) return 0;
prefix = prefix * 10 + node->val;
if (node->left == nullptr && node->right == nullptr) return prefix;
return dfs(node->left, prefix) + dfs(node->right, prefix);
}
pub fn sum_numbers_path_strings(root: Option<Rc<RefCell<TreeNode>>>) -> i32 {
fn collect(node: &Option<Rc<RefCell<TreeNode>>>, digits: String, paths: &mut Vec<String>) {
if let Some(rc) = node {
let n = rc.borrow();
let digits = format!("{}{}", digits, n.val);
if n.left.is_none() && n.right.is_none() {
paths.push(digits);
} else {
collect(&n.left, digits.clone(), paths);
collect(&n.right, digits, paths);
}
}
}
let mut paths = Vec::new();
collect(&root, String::new(), &mut paths);
paths
.iter()
.map(|digits| digits.parse::<i32>().unwrap())
.sum()
}
pub fn sum_numbers_iterative(root: Option<Rc<RefCell<TreeNode>>>) -> i32 {
let mut total = 0;
let mut stack = Vec::new();
if let Some(rc) = root {
stack.push((rc, 0));
}
while let Some((node, prefix)) = stack.pop() {
let n = node.borrow();
let prefix = prefix * 10 + n.val;
if n.left.is_none() && n.right.is_none() {
total += prefix;
continue;
}
if let Some(right) = &n.right {
stack.push((Rc::clone(right), prefix));
}
if let Some(left) = &n.left {
stack.push((Rc::clone(left), prefix));
}
}
total
}
pub fn sum_numbers(root: Option<Rc<RefCell<TreeNode>>>) -> i32 {
fn dfs(node: &Option<Rc<RefCell<TreeNode>>>, prefix: i32) -> i32 {
match node {
None => 0,
Some(rc) => {
let n = rc.borrow();
let prefix = prefix * 10 + n.val;
if n.left.is_none() && n.right.is_none() {
prefix
} else {
dfs(&n.left, prefix) + dfs(&n.right, prefix)
}
}
}
}
dfs(&root, 0)
}
function sumNumbersPathStrings(root: TreeNode | null): number {
const paths: string[] = [];
const collect = (node: TreeNode | null, digits: string): void => {
if (node === null) return;
digits += String(node.val);
if (node.left === null && node.right === null) {
paths.push(digits);
return;
}
collect(node.left, digits);
collect(node.right, digits);
};
collect(root, "");
return paths.reduce((total, digits) => total + Number(digits), 0);
}
function sumNumbersIterative(root: TreeNode | null): number {
if (root === null) return 0;
let total = 0;
const stack: [TreeNode, number][] = [[root, 0]];
while (stack.length > 0) {
const [node, above] = stack.pop()!;
const prefix = above * 10 + node.val;
if (node.left === null && node.right === null) {
total += prefix;
continue;
}
if (node.right !== null) stack.push([node.right, prefix]);
if (node.left !== null) stack.push([node.left, prefix]);
}
return total;
}
function sumNumbers(root: TreeNode | null): number {
const dfs = (node: TreeNode | null, prefix: number): number => {
if (node === null) return 0;
prefix = prefix * 10 + node.val;
if (node.left === null && node.right === null) return prefix;
return dfs(node.left, prefix) + dfs(node.right, prefix);
};
return dfs(root, 0);
}
func sumNumbersPathStrings(root *TreeNode) int {
var paths []string
var collect func(node *TreeNode, digits string)
collect = func(node *TreeNode, digits string) {
if node == nil {
return
}
digits += strconv.Itoa(node.Val)
if node.Left == nil && node.Right == nil {
paths = append(paths, digits)
return
}
collect(node.Left, digits)
collect(node.Right, digits)
}
collect(root, "")
total := 0
for _, digits := range paths {
n, _ := strconv.Atoi(digits)
total += n
}
return total
}
func sumNumbersIterative(root *TreeNode) int {
if root == nil {
return 0
}
type frame struct {
node *TreeNode
prefix int
}
total := 0
stack := []frame{{root, 0}}
for len(stack) > 0 {
f := stack[len(stack)-1]
stack = stack[:len(stack)-1]
prefix := f.prefix*10 + f.node.Val
if f.node.Left == nil && f.node.Right == nil {
total += prefix
continue
}
if f.node.Right != nil {
stack = append(stack, frame{f.node.Right, prefix})
}
if f.node.Left != nil {
stack = append(stack, frame{f.node.Left, prefix})
}
}
return total
}
func sumNumbers(root *TreeNode) int {
return dfs(root, 0)
}
func dfs(node *TreeNode, prefix int) int {
if node == nil {
return 0
}
prefix = prefix*10 + node.Val
if node.Left == nil && node.Right == nil {
return prefix
}
return dfs(node.Left, prefix) + dfs(node.Right, prefix)
}
func sumNumbersPathStrings(_ root: TreeNode?) -> Int {
var paths: [String] = []
func collect(_ node: TreeNode?, _ digits: String) {
guard let node = node else { return }
let digits = digits + String(node.val)
if node.left == nil && node.right == nil {
paths.append(digits)
return
}
collect(node.left, digits)
collect(node.right, digits)
}
collect(root, "")
return paths.reduce(0) { $0 + Int($1)! }
}
func sumNumbersIterative(_ root: TreeNode?) -> Int {
guard let root = root else { return 0 }
var total = 0
var stack: [(node: TreeNode, prefix: Int)] = [(root, 0)]
while let (node, prefix) = stack.popLast() {
let value = prefix * 10 + node.val
if node.left == nil && node.right == nil {
total += value
continue
}
if let right = node.right { stack.append((right, value)) }
if let left = node.left { stack.append((left, value)) }
}
return total
}
func sumNumbers(_ root: TreeNode?) -> Int {
return dfs(root, 0)
}
private func dfs(_ node: TreeNode?, _ prefix: Int) -> Int {
guard let node = node else { return 0 }
let prefix = prefix * 10 + node.val
if node.left == nil && node.right == nil { return prefix }
return dfs(node.left, prefix) + dfs(node.right, prefix)
}
28. Binary Search Tree Iterator
Medium · LC 173
Design an iterator over a binary search tree that returns values in ascending order through next and hasNext calls. Keep a stack holding a paused inorder traversal, seeded with the root's left spine so the smallest pending node sits on top; popping a node returns its value and pushes the left spine of its right subtree. The trick is that each node is pushed and popped exactly once, making both calls O(1) amortized while the stack never holds more than one root-to-leaf path, O(h) space.
One full inorder traversal in the constructor, then walk an index. Simplest to write, but wastes memory and does all work eagerly — the whole point of the follow-up is to avoid this O(n) footprint.
Lazy at last: let Python's generator machinery keep the paused traversal state for us, holding one value of lookahead so hasNext() can answer without advancing. Leans on a language feature most other languages lack — see the next rung for the portable version.
Pause a manual inorder traversal with an explicit stack: it holds every node whose value is still owed, smallest on top. Popping a node pushes the left spine of its right subtree, so each node is pushed and popped exactly once.
One full inorder traversal in the constructor, then walk an index. Simplest to write, but wastes memory and does all work eagerly — the whole point of the follow-up is to avoid this O(n) footprint.
Pause a manual inorder traversal: the stack holds every node whose value is still owed, smallest on top. next() pops one node and pushes the left spine of its right subtree; each node is pushed and popped exactly once, so the whole tree is never materialized.
One full inorder traversal in the constructor, then walk an index. Simplest to write, but wastes memory and does all work eagerly — the whole point of the follow-up is to avoid this O(n) footprint.
Pause a manual inorder traversal: the stack holds every node whose value is still owed, smallest on top. next() pops one node and pushes the left spine of its right subtree; each node is pushed and popped exactly once, so the whole tree is never materialized.
One full inorder traversal in the constructor, then walk an index. Simplest to write, but wastes memory and does all work eagerly — the whole point of the follow-up is to avoid this O(n) footprint.
Pause a manual inorder traversal: the stack holds every node whose value is still owed, smallest on top. next() pops one node and pushes the left spine of its right subtree; each node is pushed and popped exactly once, so the whole tree is never materialized.
One full inorder traversal in the constructor, then walk an index. Simplest to write, but wastes memory and does all work eagerly — the whole point of the follow-up is to avoid this O(n) footprint.
Pause a manual inorder traversal: the stack holds every node whose value is still owed, smallest on top. Next() pops one node and pushes the left spine of its right subtree; each node is pushed and popped exactly once, so the whole tree is never materialized.
One full inorder traversal in the constructor, then walk an index. Simplest to write, but wastes memory and does all work eagerly — the whole point of the follow-up is to avoid this O(n) footprint.
Pause a manual inorder traversal: the stack holds every node whose value is still owed, smallest on top. next() pops one node and pushes the left spine of its right subtree; each node is pushed and popped exactly once, so the whole tree is never materialized.
class BSTIterator_flatten:
def __init__(self, root: "TreeNode | None"):
self._values: "list[int]" = []
self._index = 0
def inorder(node: "TreeNode | None") -> None:
if node is None:
return
inorder(node.left)
self._values.append(node.val)
inorder(node.right)
inorder(root)
def next(self) -> int:
val = self._values[self._index]
self._index += 1
return val
def hasNext(self) -> bool:
return self._index < len(self._values)
class BSTIterator_generator:
def __init__(self, root: "TreeNode | None"):
def inorder(node: "TreeNode | None"):
if node is not None:
yield from inorder(node.left)
yield node.val
yield from inorder(node.right)
self._gen = inorder(root)
self._lookahead = next(self._gen, None)
def next(self) -> int:
val = self._lookahead
self._lookahead = next(self._gen, None)
assert val is not None # LeetCode guarantees next() calls are valid
return val
def hasNext(self) -> bool:
return self._lookahead is not None
class BSTIterator:
def __init__(self, root: "TreeNode | None"):
self._stack: "list[TreeNode]" = []
self._push_left(root)
def _push_left(self, node: "TreeNode | None") -> None:
while node is not None:
self._stack.append(node)
node = node.left
def next(self) -> int:
node = self._stack.pop()
self._push_left(node.right)
return node.val
def hasNext(self) -> bool:
return bool(self._stack)
class BSTIteratorFlatten {
public:
BSTIteratorFlatten(TreeNode* root) {
inorder(root);
}
int next() {
return values_[index_++];
}
bool hasNext() {
return index_ < values_.size();
}
private:
void inorder(TreeNode* node) {
if (node == nullptr) return;
inorder(node->left);
values_.push_back(node->val);
inorder(node->right);
}
std::vector<int> values_;
size_t index_ = 0;
};
class BSTIterator {
public:
BSTIterator(TreeNode* root) {
pushLeftSpine(root);
}
int next() {
TreeNode* node = stack_.top();
stack_.pop();
pushLeftSpine(node->right);
return node->val;
}
bool hasNext() {
return !stack_.empty();
}
private:
void pushLeftSpine(TreeNode* node) {
for (; node != nullptr; node = node->left) stack_.push(node);
}
std::stack<TreeNode*> stack_;
};
struct BSTIteratorFlatten {
values: Vec<i32>,
index: usize,
}
impl BSTIteratorFlatten {
fn new(root: Option<Rc<RefCell<TreeNode>>>) -> Self {
fn inorder(node: &Option<Rc<RefCell<TreeNode>>>, values: &mut Vec<i32>) {
if let Some(rc) = node {
let n = rc.borrow();
inorder(&n.left, values);
values.push(n.val);
inorder(&n.right, values);
}
}
let mut values = Vec::new();
inorder(&root, &mut values);
BSTIteratorFlatten { values, index: 0 }
}
fn next(&mut self) -> i32 {
let val = self.values[self.index];
self.index += 1;
val
}
fn has_next(&self) -> bool {
self.index < self.values.len()
}
}
struct BSTIterator {
stack: Vec<Rc<RefCell<TreeNode>>>,
}
impl BSTIterator {
fn new(root: Option<Rc<RefCell<TreeNode>>>) -> Self {
let mut it = BSTIterator { stack: Vec::new() };
it.push_left_spine(root);
it
}
fn next(&mut self) -> i32 {
let node = self.stack.pop().expect("next() called on empty iterator");
let (val, right) = {
let n = node.borrow();
(n.val, n.right.clone())
};
self.push_left_spine(right);
val
}
fn has_next(&self) -> bool {
!self.stack.is_empty()
}
fn push_left_spine(&mut self, mut node: Option<Rc<RefCell<TreeNode>>>) {
while let Some(rc) = node {
node = rc.borrow().left.clone();
self.stack.push(rc);
}
}
}
class BSTIteratorFlatten {
private values: number[] = [];
private index = 0;
constructor(root: TreeNode | null) {
const inorder = (node: TreeNode | null): void => {
if (node === null) return;
inorder(node.left);
this.values.push(node.val);
inorder(node.right);
};
inorder(root);
}
next(): number {
return this.values[this.index++];
}
hasNext(): boolean {
return this.index < this.values.length;
}
}
class BSTIterator {
private stack: TreeNode[] = [];
constructor(root: TreeNode | null) {
this.pushLeftSpine(root);
}
next(): number {
const node = this.stack.pop()!;
this.pushLeftSpine(node.right);
return node.val;
}
hasNext(): boolean {
return this.stack.length > 0;
}
private pushLeftSpine(node: TreeNode | null): void {
for (; node !== null; node = node.left) this.stack.push(node);
}
}
type BSTIteratorFlatten struct {
values []int
index int
}
func ConstructorFlatten(root *TreeNode) BSTIteratorFlatten {
it := BSTIteratorFlatten{}
it.inorder(root)
return it
}
func (it *BSTIteratorFlatten) inorder(node *TreeNode) {
if node == nil {
return
}
it.inorder(node.Left)
it.values = append(it.values, node.Val)
it.inorder(node.Right)
}
func (it *BSTIteratorFlatten) Next() int {
v := it.values[it.index]
it.index++
return v
}
func (it *BSTIteratorFlatten) HasNext() bool {
return it.index < len(it.values)
}
type BSTIterator struct {
stack []*TreeNode
}
func Constructor(root *TreeNode) BSTIterator {
it := BSTIterator{}
it.pushLeftSpine(root)
return it
}
func (it *BSTIterator) pushLeftSpine(node *TreeNode) {
for ; node != nil; node = node.Left {
it.stack = append(it.stack, node)
}
}
func (it *BSTIterator) Next() int {
node := it.stack[len(it.stack)-1]
it.stack = it.stack[:len(it.stack)-1]
it.pushLeftSpine(node.Right)
return node.Val
}
func (it *BSTIterator) HasNext() bool {
return len(it.stack) > 0
}
class BSTIteratorFlatten {
private var values: [Int] = []
private var index = 0
init(_ root: TreeNode?) {
inorder(root)
}
func next() -> Int {
let val = values[index]
index += 1
return val
}
func hasNext() -> Bool {
return index < values.count
}
private func inorder(_ node: TreeNode?) {
guard let node = node else { return }
inorder(node.left)
values.append(node.val)
inorder(node.right)
}
}
class BSTIterator {
private var stack: [TreeNode] = []
init(_ root: TreeNode?) {
pushLeftSpine(root)
}
func next() -> Int {
let node = stack.removeLast()
pushLeftSpine(node.right)
return node.val
}
func hasNext() -> Bool {
return !stack.isEmpty
}
private func pushLeftSpine(_ node: TreeNode?) {
var node = node
while let current = node {
stack.append(current)
node = current.left
}
}
}
29. Count Complete Tree Nodes
Medium · LC 222
Given a complete binary tree, count its nodes in better than linear time. At each subtree measure the heights of the leftmost and rightmost spines: if they are equal the subtree is perfect and contributes 2^h - 1 nodes with no traversal, otherwise recurse into both children. The trick is that completeness guarantees one child is always perfect, so only one path actually descends and the whole count runs in O(log^2 n).
The one-liner every binary tree supports: count self plus both subtrees. Correct, but misses the entire point of the constraint — the follow-up asks for less than O(n).
First sub-O(n) idea: the last level has between 1 and 2^d leaves, filled left to right. Whether leaf index i exists is a single O(d) root-to-leaf probe (each bit of i says go left or right), so binary-search the leaf count. Fast, but fiddly index bookkeeping.
Same O(log^2 n) with far simpler reasoning than the index probes: if the leftmost and rightmost spines have equal height the subtree is perfect — 2^h - 1 nodes, no traversal needed. Otherwise recurse; at every level one child is perfect, so only one path descends.
The one-liner every binary tree supports: count self plus both subtrees. Correct, but misses the entire point of the constraint — the follow-up asks for less than O(n).
If the leftmost and rightmost spines have equal height the subtree is perfect: 2^h - 1 nodes with no traversal. Otherwise recurse into both children — at every level one of them is perfect, so only one path keeps descending: O(log n) levels x O(log n) spine walks.
The one-liner every binary tree supports: count self plus both subtrees. Correct, but misses the entire point of the constraint — the follow-up asks for less than O(n).
If the leftmost and rightmost spines have equal height the subtree is perfect: 2^h - 1 nodes with no traversal. Otherwise recurse into both children — at every level one of them is perfect, so only one path keeps descending: O(log n) levels x O(log n) spine walks.
The one-liner every binary tree supports: count self plus both subtrees. Correct, but misses the entire point of the constraint — the follow-up asks for less than O(n).
If the leftmost and rightmost spines have equal height the subtree is perfect: 2^h - 1 nodes with no traversal. Otherwise recurse into both children — at every level one of them is perfect, so only one path keeps descending: O(log n) levels x O(log n) spine walks.
The one-liner every binary tree supports: count self plus both subtrees. Correct, but misses the entire point of the constraint — the follow-up asks for less than O(n).
If the leftmost and rightmost spines have equal height the subtree is perfect: 2^h - 1 nodes with no traversal. Otherwise recurse into both children — at every level one of them is perfect, so only one path keeps descending: O(log n) levels x O(log n) spine walks.
The one-liner every binary tree supports: count self plus both subtrees. Correct, but misses the entire point of the constraint — the follow-up asks for less than O(n).
If the leftmost and rightmost spines have equal height the subtree is perfect: 2^h - 1 nodes with no traversal. Otherwise recurse into both children — at every level one of them is perfect, so only one path keeps descending: O(log n) levels x O(log n) spine walks.
def countNodes_linear(self, root: "TreeNode | None") -> int:
if root is None:
return 0
return 1 + self.countNodes_linear(root.left) + self.countNodes_linear(root.right)
def countNodes_binary_search(self, root: "TreeNode | None") -> int:
if root is None:
return 0
depth = 0 # edges from the root down to the leftmost leaf
node = root
while node.left is not None:
node = node.left
depth += 1
if depth == 0:
return 1
def exists(index: int) -> bool:
"""Probe for leaf `index` (0-based) on the last level."""
lo, hi = 0, (1 << depth) - 1
node = root
for _ in range(depth):
mid = (lo + hi) // 2
if index <= mid:
node = node.left
hi = mid
else:
node = node.right
lo = mid + 1
return node is not None
# Leaf 0 always exists; find how many leaves the last level holds.
lo, hi = 1, (1 << depth) - 1
while lo <= hi:
mid = (lo + hi) // 2
if exists(mid):
lo = mid + 1
else:
hi = mid - 1
return (1 << depth) - 1 + lo # full levels + last-level leaves
def countNodes(self, root: "TreeNode | None") -> int:
if root is None:
return 0
left_height = 0
node = root
while node is not None:
left_height += 1
node = node.left
right_height = 0
node = root
while node is not None:
right_height += 1
node = node.right
if left_height == right_height: # perfect subtree
return (1 << left_height) - 1
return 1 + self.countNodes(root.left) + self.countNodes(root.right)
int countNodesLinear(TreeNode* root) {
if (root == nullptr) return 0;
return 1 + countNodesLinear(root->left) + countNodesLinear(root->right);
}
int countNodes(TreeNode* root) {
if (root == nullptr) return 0;
int leftHeight = 0;
for (TreeNode* node = root; node != nullptr; node = node->left) ++leftHeight;
int rightHeight = 0;
for (TreeNode* node = root; node != nullptr; node = node->right) ++rightHeight;
if (leftHeight == rightHeight) // perfect subtree
return (1 << leftHeight) - 1;
return 1 + countNodes(root->left) + countNodes(root->right);
}
pub fn count_nodes_linear(root: Option<Rc<RefCell<TreeNode>>>) -> i32 {
fn count(node: &Option<Rc<RefCell<TreeNode>>>) -> i32 {
match node {
None => 0,
Some(rc) => {
let n = rc.borrow();
1 + count(&n.left) + count(&n.right)
}
}
}
count(&root)
}
pub fn count_nodes(root: Option<Rc<RefCell<TreeNode>>>) -> i32 {
fn count(node: &Option<Rc<RefCell<TreeNode>>>) -> i32 {
let rc = match node {
None => return 0,
Some(rc) => rc,
};
let mut left_height = 0;
let mut walker = Some(Rc::clone(rc));
while let Some(n) = walker {
left_height += 1;
walker = n.borrow().left.clone();
}
let mut right_height = 0;
walker = Some(Rc::clone(rc));
while let Some(n) = walker {
right_height += 1;
walker = n.borrow().right.clone();
}
if left_height == right_height {
return (1 << left_height) - 1; // perfect subtree
}
let n = rc.borrow();
1 + count(&n.left) + count(&n.right)
}
count(&root)
}
function countNodesLinear(root: TreeNode | null): number {
if (root === null) return 0;
return 1 + countNodesLinear(root.left) + countNodesLinear(root.right);
}
function countNodes(root: TreeNode | null): number {
if (root === null) return 0;
let leftHeight = 0;
for (let node: TreeNode | null = root; node !== null; node = node.left) leftHeight++;
let rightHeight = 0;
for (let node: TreeNode | null = root; node !== null; node = node.right) rightHeight++;
if (leftHeight === rightHeight) {
return (1 << leftHeight) - 1; // perfect subtree
}
return 1 + countNodes(root.left) + countNodes(root.right);
}
func countNodesLinear(root *TreeNode) int {
if root == nil {
return 0
}
return 1 + countNodesLinear(root.Left) + countNodesLinear(root.Right)
}
func countNodes(root *TreeNode) int {
if root == nil {
return 0
}
leftHeight := 0
for node := root; node != nil; node = node.Left {
leftHeight++
}
rightHeight := 0
for node := root; node != nil; node = node.Right {
rightHeight++
}
if leftHeight == rightHeight { // perfect subtree
return 1<<leftHeight - 1
}
return 1 + countNodes(root.Left) + countNodes(root.Right)
}
func countNodesLinear(_ root: TreeNode?) -> Int {
guard let root = root else { return 0 }
return 1 + countNodesLinear(root.left) + countNodesLinear(root.right)
}
func countNodes(_ root: TreeNode?) -> Int {
guard let root = root else { return 0 }
var leftHeight = 0
var node: TreeNode? = root
while let current = node {
leftHeight += 1
node = current.left
}
var rightHeight = 0
node = root
while let current = node {
rightHeight += 1
node = current.right
}
if leftHeight == rightHeight { // perfect subtree
return (1 << leftHeight) - 1
}
return 1 + countNodes(root.left) + countNodes(root.right)
}
30. Lowest Common Ancestor of a Binary Tree
Medium · LC 236
Given a binary tree and two nodes p and q, find their lowest common ancestor, where a node may count as its own ancestor. Recurse post-order so each subtree reports the target it contains or null; the first node whose left and right calls both report something is the split point, while a single report is simply passed upward. The trick is stopping the recursion as soon as it reaches p or q, which handles the case where one target sits below the other without ever searching deeper.
The most concrete mental model: record every node's parent, collect p's ancestor chain into a set, then walk up from q until the chains meet. What you would do with an explicit parent field (see LC 1650), at the cost of an O(n) map plus an O(h) set.
Find the full path to each target with a backtracking DFS, then the LCA is the last node the two paths share. Drops the O(n) parent map to O(h) path storage; still two passes over the tree.
One pass, no stored paths: a subtree reports the target (or LCA) it contains, else None. The first node whose left AND right both report is the split point; if only one side reports, pass that answer up (covers the ancestor case, because the search stops at p or q without going deeper).
Find the full path to each target with a backtracking DFS, then the LCA is the last node the two paths share. Very easy to reason about, but it walks the tree twice and stores both paths.
One pass, no stored paths: each subtree reports the target (or LCA) it contains, else null. The first node whose left AND right both report is the split point; if only one side reports, that answer bubbles up — this also covers the "q below p" case because the search stops at p or q.
Find the full path to each target with a backtracking DFS, then the LCA is the last node the two paths share. Very easy to reason about, but it walks the tree twice and stores both paths.
One pass, no stored paths: each subtree reports the target (or LCA) it contains, else None. The first node whose left AND right both report is the split point; if only one side reports, that answer bubbles up — this also covers the "q below p" case because the search stops at p or q.
Find the full path to each target with a backtracking DFS, then the LCA is the last node the two paths share. Very easy to reason about, but it walks the tree twice and stores both paths.
One pass, no stored paths: each subtree reports the target (or LCA) it contains, else null. The first node whose left AND right both report is the split point; if only one side reports, that answer bubbles up — this also covers the "q below p" case because the search stops at p or q.
Find the full path to each target with a backtracking DFS, then the LCA is the last node the two paths share. Very easy to reason about, but it walks the tree twice and stores both paths.
One pass, no stored paths: each subtree reports the target (or LCA) it contains, else nil. The first node whose left AND right both report is the split point; if only one side reports, that answer bubbles up — this also covers the "q below p" case because the search stops at p or q.
Find the full path to each target with a backtracking DFS, then the LCA is the last node the two paths share. Very easy to reason about, but it walks the tree twice and stores both paths.
One pass, no stored paths: each subtree reports the target (or LCA) it contains, else nil. The first node whose left AND right both report is the split point; if only one side reports, that answer bubbles up — this also covers the "q below p" case because the search stops at p or q.
def lowestCommonAncestor_parent_map(self, root: "TreeNode | None",
p: "TreeNode", q: "TreeNode") -> "TreeNode | None":
parent: "dict[TreeNode, TreeNode | None]" = {root: None}
stack = [root]
while p not in parent or q not in parent:
node = stack.pop()
for child in (node.left, node.right):
if child is not None:
parent[child] = node
stack.append(child)
ancestors = set()
node: "TreeNode | None" = p
while node is not None:
ancestors.add(node)
node = parent[node]
node = q
while node not in ancestors:
node = parent[node]
return node
def lowestCommonAncestor_paths(self, root: "TreeNode | None",
p: "TreeNode", q: "TreeNode") -> "TreeNode | None":
def path_to(target: "TreeNode") -> "list[TreeNode]":
path: "list[TreeNode]" = []
def dfs(node: "TreeNode | None") -> bool:
if node is None:
return False
path.append(node)
if node is target or dfs(node.left) or dfs(node.right):
return True
path.pop()
return False
dfs(root)
return path
lca = None
for a, b in zip(path_to(p), path_to(q)):
if a is b:
lca = a
else:
break
return lca
def lowestCommonAncestor(self, root: "TreeNode | None", p: "TreeNode",
q: "TreeNode") -> "TreeNode | None":
if root is None or root is p or root is q:
return root
left = self.lowestCommonAncestor(root.left, p, q)
right = self.lowestCommonAncestor(root.right, p, q)
if left is not None and right is not None:
return root # p and q found in different subtrees: split point
return left if left is not None else right
TreeNode* lowestCommonAncestorPaths(TreeNode* root, TreeNode* p, TreeNode* q) {
auto pathTo = [root](auto&& self, TreeNode* node, TreeNode* target,
std::vector<TreeNode*>& path) -> bool {
if (node == nullptr) return false;
path.push_back(node);
if (node == target || self(self, node->left, target, path) ||
self(self, node->right, target, path)) {
return true;
}
path.pop_back();
return false;
};
std::vector<TreeNode*> pathP, pathQ;
pathTo(pathTo, root, p, pathP);
pathTo(pathTo, root, q, pathQ);
TreeNode* lca = nullptr;
for (size_t i = 0; i < pathP.size() && i < pathQ.size() && pathP[i] == pathQ[i]; ++i) {
lca = pathP[i];
}
return lca;
}
TreeNode* lowestCommonAncestor(TreeNode* root, TreeNode* p, TreeNode* q) {
if (root == nullptr || root == p || root == q) return root;
TreeNode* left = lowestCommonAncestor(root->left, p, q);
TreeNode* right = lowestCommonAncestor(root->right, p, q);
if (left != nullptr && right != nullptr) return root; // split point
return left != nullptr ? left : right;
}
pub fn lowest_common_ancestor_paths(
root: Option<Rc<RefCell<TreeNode>>>,
p: Option<Rc<RefCell<TreeNode>>>,
q: Option<Rc<RefCell<TreeNode>>>,
) -> Option<Rc<RefCell<TreeNode>>> {
fn path_to(
node: &Option<Rc<RefCell<TreeNode>>>,
target: i32,
path: &mut Vec<Rc<RefCell<TreeNode>>>,
) -> bool {
let rc = match node {
None => return false,
Some(rc) => rc,
};
path.push(Rc::clone(rc));
let n = rc.borrow();
if n.val == target || path_to(&n.left, target, path) || path_to(&n.right, target, path)
{
return true;
}
path.pop();
false
}
let p_val = p.as_ref()?.borrow().val;
let q_val = q.as_ref()?.borrow().val;
let mut path_p = Vec::new();
let mut path_q = Vec::new();
path_to(&root, p_val, &mut path_p);
path_to(&root, q_val, &mut path_q);
let mut lca = None;
for (a, b) in path_p.iter().zip(path_q.iter()) {
if Rc::ptr_eq(a, b) {
lca = Some(Rc::clone(a));
} else {
break;
}
}
lca
}
pub fn lowest_common_ancestor(
root: Option<Rc<RefCell<TreeNode>>>,
p: Option<Rc<RefCell<TreeNode>>>,
q: Option<Rc<RefCell<TreeNode>>>,
) -> Option<Rc<RefCell<TreeNode>>> {
let p_val = p.as_ref()?.borrow().val;
let q_val = q.as_ref()?.borrow().val;
fn dfs(
node: &Option<Rc<RefCell<TreeNode>>>,
p_val: i32,
q_val: i32,
) -> Option<Rc<RefCell<TreeNode>>> {
let rc = node.as_ref()?;
let n = rc.borrow();
if n.val == p_val || n.val == q_val {
return Some(Rc::clone(rc));
}
let left = dfs(&n.left, p_val, q_val);
let right = dfs(&n.right, p_val, q_val);
match (left, right) {
// p and q found in different subtrees: split point
(Some(_), Some(_)) => Some(Rc::clone(rc)),
(left, right) => left.or(right),
}
}
dfs(&root, p_val, q_val)
}
function lowestCommonAncestorPaths(
root: TreeNode | null,
p: TreeNode | null,
q: TreeNode | null
): TreeNode | null {
const pathTo = (target: TreeNode | null): TreeNode[] => {
const path: TreeNode[] = [];
const dfs = (node: TreeNode | null): boolean => {
if (node === null) return false;
path.push(node);
if (node === target || dfs(node.left) || dfs(node.right)) return true;
path.pop();
return false;
};
dfs(root);
return path;
};
const pathP = pathTo(p);
const pathQ = pathTo(q);
let lca: TreeNode | null = null;
for (let i = 0; i < pathP.length && i < pathQ.length && pathP[i] === pathQ[i]; i++) {
lca = pathP[i];
}
return lca;
}
function lowestCommonAncestor(
root: TreeNode | null,
p: TreeNode | null,
q: TreeNode | null
): TreeNode | null {
if (root === null || root === p || root === q) return root;
const left = lowestCommonAncestor(root.left, p, q);
const right = lowestCommonAncestor(root.right, p, q);
if (left !== null && right !== null) {
return root; // p and q found in different subtrees: split point
}
return left !== null ? left : right;
}
func lowestCommonAncestorPaths(root, p, q *TreeNode) *TreeNode {
pathTo := func(target *TreeNode) []*TreeNode {
var path []*TreeNode
var dfs func(node *TreeNode) bool
dfs = func(node *TreeNode) bool {
if node == nil {
return false
}
path = append(path, node)
if node == target || dfs(node.Left) || dfs(node.Right) {
return true
}
path = path[:len(path)-1]
return false
}
dfs(root)
return path
}
pathP := pathTo(p)
pathQ := pathTo(q)
var lca *TreeNode
for i := 0; i < len(pathP) && i < len(pathQ) && pathP[i] == pathQ[i]; i++ {
lca = pathP[i]
}
return lca
}
func lowestCommonAncestor(root, p, q *TreeNode) *TreeNode {
if root == nil || root == p || root == q {
return root
}
left := lowestCommonAncestor(root.Left, p, q)
right := lowestCommonAncestor(root.Right, p, q)
if left != nil && right != nil {
return root // p and q found in different subtrees: split point
}
if left != nil {
return left
}
return right
}
func lowestCommonAncestorPaths(_ root: TreeNode?, _ p: TreeNode?, _ q: TreeNode?) -> TreeNode? {
func pathTo(_ target: TreeNode?) -> [TreeNode] {
var path: [TreeNode] = []
func dfs(_ node: TreeNode?) -> Bool {
guard let node = node else { return false }
path.append(node)
if node === target || dfs(node.left) || dfs(node.right) { return true }
path.removeLast()
return false
}
_ = dfs(root)
return path
}
let pathP = pathTo(p)
let pathQ = pathTo(q)
var lca: TreeNode? = nil
for (a, b) in zip(pathP, pathQ) {
if a === b { lca = a } else { break }
}
return lca
}
func lowestCommonAncestor(_ root: TreeNode?, _ p: TreeNode?, _ q: TreeNode?) -> TreeNode? {
guard let root = root else { return nil }
if root === p || root === q { return root }
let left = lowestCommonAncestor(root.left, p, q)
let right = lowestCommonAncestor(root.right, p, q)
if left != nil && right != nil {
return root // p and q found in different subtrees: split point
}
return left ?? right
}
Binary Tree BFS
31. Average of Levels in Binary Tree
Easy · LC 637
Given a binary tree, return the average value of the nodes on each level from top to bottom. Run a breadth-first traversal where each pass drains exactly one level: record the queue's current length, pop that many nodes while summing their values and enqueueing children, then divide the sum by the width. The trick is snapshotting the level width before the inner loop, which cleanly separates levels inside a single queue — and with no recursion there is no depth hazard on skewed trees.
One pass, no queue: bucket every value by its depth and divide at the end. Recursion depth equals tree height, though — a fully skewed 10^4-node tree would trip Python's ~1000-frame limit.
The queue holds exactly one level at a time, so each level's sum and width fall out of the traversal for free — and with no recursion there is no depth hazard on skewed trees.
One pass, no queue: bucket every value by its depth and divide at the end. Recursion depth equals tree height, though — a fully skewed tree leans on the call stack.
The queue holds exactly one level at a time, so each level's sum and width fall out of the traversal for free — and with no recursion there is no depth hazard on skewed trees.
One pass, no queue: bucket every value by its depth and divide at the end. Recursion depth equals tree height, though — a fully skewed tree leans on the call stack.
The queue holds exactly one level at a time, so each level's sum and width fall out of the traversal for free — and with no recursion there is no depth hazard on skewed trees.
One pass, no queue: bucket every value by its depth and divide at the end. Recursion depth equals tree height, though — a fully skewed tree leans on the call stack.
The current array holds exactly one level at a time, so each level's sum and width fall out of the traversal for free — and with no recursion there is no depth hazard on skewed trees.
One pass, no queue: bucket every value by its depth and divide at the end. Recursion depth equals tree height, though — a fully skewed tree leans on the call stack.
The queue holds exactly one level at a time, so each level's sum and width fall out of the traversal for free — and with no recursion there is no depth hazard on skewed trees.
One pass, no queue: bucket every value by its depth and divide at the end. Recursion depth equals tree height, though — a fully skewed tree leans on the call stack.
The current array holds exactly one level at a time, so each level's sum and width fall out of the traversal for free — and with no recursion there is no depth hazard on skewed trees.
def averageOfLevels_dfs(self, root: Optional[TreeNode]) -> List[float]:
sums: List[int] = []
counts: List[int] = []
def visit(node: Optional[TreeNode], depth: int) -> None:
if not node:
return
if depth == len(sums):
sums.append(0)
counts.append(0)
sums[depth] += node.val
counts[depth] += 1
visit(node.left, depth + 1)
visit(node.right, depth + 1)
visit(root, 0)
return [s / c for s, c in zip(sums, counts)]
def averageOfLevels(self, root: Optional[TreeNode]) -> List[float]:
averages: List[float] = []
queue = deque([root] if root else [])
while queue:
width = len(queue)
total = 0
for _ in range(width):
node = queue.popleft()
total += node.val
if node.left:
queue.append(node.left)
if node.right:
queue.append(node.right)
averages.append(total / width)
return averages
vector<double> averageOfLevelsDfs(TreeNode *root) {
vector<double> sums;
vector<int> counts;
function<void(TreeNode *, size_t)> visit = [&](TreeNode *node,
size_t depth) {
if (!node) return;
if (depth == sums.size()) {
sums.push_back(0.0);
counts.push_back(0);
}
sums[depth] += node->val;
++counts[depth];
visit(node->left, depth + 1);
visit(node->right, depth + 1);
};
visit(root, 0);
vector<double> averages;
averages.reserve(sums.size());
for (size_t d = 0; d < sums.size(); ++d)
averages.push_back(sums[d] / counts[d]);
return averages;
}
vector<double> averageOfLevels(TreeNode *root) {
vector<double> averages;
queue<TreeNode *> q;
if (root) q.push(root);
while (!q.empty()) {
size_t width = q.size();
double total = 0.0;
for (size_t i = 0; i < width; ++i) {
TreeNode *node = q.front();
q.pop();
total += node->val;
if (node->left) q.push(node->left);
if (node->right) q.push(node->right);
}
averages.push_back(total / static_cast<double>(width));
}
return averages;
}
pub fn average_of_levels_dfs(root: Option<Rc<RefCell<TreeNode>>>) -> Vec<f64> {
fn visit(
node: &Option<Rc<RefCell<TreeNode>>>,
depth: usize,
sums: &mut Vec<f64>,
counts: &mut Vec<u32>,
) {
if let Some(rc) = node {
let n = rc.borrow();
if depth == sums.len() {
sums.push(0.0);
counts.push(0);
}
sums[depth] += f64::from(n.val);
counts[depth] += 1;
visit(&n.left, depth + 1, sums, counts);
visit(&n.right, depth + 1, sums, counts);
}
}
let mut sums = Vec::new();
let mut counts = Vec::new();
visit(&root, 0, &mut sums, &mut counts);
sums.iter()
.zip(&counts)
.map(|(s, c)| s / f64::from(*c))
.collect()
}
pub fn average_of_levels(root: Option<Rc<RefCell<TreeNode>>>) -> Vec<f64> {
let mut averages = Vec::new();
let mut queue = VecDeque::new();
if let Some(r) = root {
queue.push_back(r);
}
while !queue.is_empty() {
let width = queue.len();
let mut total = 0.0;
for _ in 0..width {
let node = queue.pop_front().unwrap();
let n = node.borrow();
total += f64::from(n.val);
if let Some(left) = n.left.clone() {
queue.push_back(left);
}
if let Some(right) = n.right.clone() {
queue.push_back(right);
}
}
averages.push(total / width as f64);
}
averages
}
function averageOfLevelsDfs(root: TreeNode | null): number[] {
const sums: number[] = [];
const counts: number[] = [];
const visit = (node: TreeNode | null, depth: number): void => {
if (!node) return;
if (depth === sums.length) {
sums.push(0);
counts.push(0);
}
sums[depth] += node.val;
counts[depth] += 1;
visit(node.left, depth + 1);
visit(node.right, depth + 1);
};
visit(root, 0);
return sums.map((s, d) => s / counts[d]);
}
function averageOfLevels(root: TreeNode | null): number[] {
const averages: number[] = [];
let level: TreeNode[] = root ? [root] : [];
while (level.length > 0) {
let total = 0;
const next: TreeNode[] = [];
for (const node of level) {
total += node.val;
if (node.left) next.push(node.left);
if (node.right) next.push(node.right);
}
averages.push(total / level.length);
level = next;
}
return averages;
}
func averageOfLevelsDfs(root *TreeNode) []float64 {
sums := []float64{}
counts := []int{}
var visit func(node *TreeNode, depth int)
visit = func(node *TreeNode, depth int) {
if node == nil {
return
}
if depth == len(sums) {
sums = append(sums, 0)
counts = append(counts, 0)
}
sums[depth] += float64(node.Val)
counts[depth]++
visit(node.Left, depth+1)
visit(node.Right, depth+1)
}
visit(root, 0)
averages := make([]float64, 0, len(sums))
for d := range sums {
averages = append(averages, sums[d]/float64(counts[d]))
}
return averages
}
func averageOfLevels(root *TreeNode) []float64 {
averages := []float64{}
queue := []*TreeNode{}
if root != nil {
queue = append(queue, root)
}
for len(queue) > 0 {
width := len(queue)
total := 0.0
for i := 0; i < width; i++ {
node := queue[0]
queue = queue[1:]
total += float64(node.Val)
if node.Left != nil {
queue = append(queue, node.Left)
}
if node.Right != nil {
queue = append(queue, node.Right)
}
}
averages = append(averages, total/float64(width))
}
return averages
}
func averageOfLevelsDfs(_ root: TreeNode?) -> [Double] {
var sums: [Double] = []
var counts: [Int] = []
func visit(_ node: TreeNode?, _ depth: Int) {
guard let node = node else { return }
if depth == sums.count {
sums.append(0)
counts.append(0)
}
sums[depth] += Double(node.val)
counts[depth] += 1
visit(node.left, depth + 1)
visit(node.right, depth + 1)
}
visit(root, 0)
return zip(sums, counts).map { $0 / Double($1) }
}
func averageOfLevels(_ root: TreeNode?) -> [Double] {
var averages: [Double] = []
var queue: [TreeNode] = []
if let root = root { queue.append(root) }
while !queue.isEmpty {
var total = 0.0
var nextQueue: [TreeNode] = []
for node in queue {
total += Double(node.val)
if let left = node.left { nextQueue.append(left) }
if let right = node.right { nextQueue.append(right) }
}
averages.append(total / Double(queue.count))
queue = nextQueue
}
return averages
}
32. Binary Tree Zigzag Level Order Traversal
Medium · LC 103
Given a binary tree, return its level order traversal with the direction alternating: left to right on the first level, right to left on the next, and so on. Do a plain breadth-first traversal collecting each level into a list, and reverse that list before appending it whenever the number of completed levels is odd. The trick is that the after-the-fact reverse is cheaper than it looks — each node is flipped at most once across the whole tree, so it beats deque-based constructions in both simplicity and constants.
Preorder DFS visits left before right, so each depth bucket fills in left-to-right order; odd depths prepend instead of append. Recursion depth equals tree height — a fully skewed 2000-node tree would trip Python's ~1000-frame limit.
Iterative, so no recursion-depth hazard; appendleft on right-to-left levels writes each level already flipped, skipping any second pass. Costs a deque and a list(level) copy per level.
Same bounds, fewer moving parts: plain list appends beat deque constants, no per-level copy, and the flip touches each node at most once across the whole tree — the cleanest version.
Preorder DFS visits left before right, so each depth bucket fills in left-to-right order; a final pass flips the odd buckets. Recursion depth equals tree height — a skewed tree leans on the call stack, and the flip is a whole second pass.
Iterative, so no recursion hazard, and no reversal pass either: the queue length gives each level's width up front, so values on right-to-left levels land directly at index width-1-i. The mirror arithmetic is easy to get wrong, though.
Same bounds with the simplest loop body: plain appends, then one reverse per odd level — that flip touches each node at most once across the whole tree, so nothing is lost versus the mirror arithmetic and there is no index to fumble.
Preorder DFS visits left before right, so each depth bucket fills in left-to-right order; a final pass flips the odd buckets. Recursion depth equals tree height — a skewed tree leans on the call stack, and the flip is a whole second pass.
Iterative, so no recursion hazard, and no reversal pass either: the queue length gives each level's width up front, so values on right-to-left levels land directly at index width-1-i. The mirror arithmetic is easy to get wrong, though.
Same bounds with the simplest loop body: plain pushes, then one reverse per odd level — that flip touches each node at most once across the whole tree, so nothing is lost versus the mirror arithmetic and there is no index to fumble.
Preorder DFS visits left before right, so each depth bucket fills in left-to-right order; a final pass flips the odd buckets. Recursion depth equals tree height — a skewed tree leans on the call stack, and the flip is a whole second pass.
Iterative, so no recursion hazard, and no reversal pass either: the level array's width is known up front, so values on right-to-left levels land directly at index width-1-i. The mirror arithmetic is easy to get wrong, though.
Same bounds with the simplest loop body: plain pushes, then one reverse per odd level — that flip touches each node at most once across the whole tree, so nothing is lost versus the mirror arithmetic and there is no index to fumble.
Preorder DFS visits left before right, so each depth bucket fills in left-to-right order; a final pass flips the odd buckets. Recursion depth equals tree height — a skewed tree leans on the call stack, and the flip is a whole second pass.
Iterative, so no recursion hazard, and no reversal pass either: the queue length gives each level's width up front, so values on right-to-left levels land directly at index width-1-i. The mirror arithmetic is easy to get wrong, though.
Same bounds with the simplest loop body: plain appends, then one reverse per odd level — that flip touches each node at most once across the whole tree, so nothing is lost versus the mirror arithmetic and there is no index to fumble.
Preorder DFS visits left before right, so each depth bucket fills in left-to-right order; a final pass flips the odd buckets. Recursion depth equals tree height — a skewed tree leans on the call stack, and the flip is a whole second pass.
Iterative, so no recursion hazard, and no reversal pass either: the level array's width is known up front, so values on right-to-left levels land directly at index width-1-i. The mirror arithmetic is easy to get wrong, though.
Same bounds with the simplest loop body: plain appends, then one reverse per odd level — that flip touches each node at most once across the whole tree, so nothing is lost versus the mirror arithmetic and there is no index to fumble.
def zigzagLevelOrder_dfs(self, root: Optional[TreeNode]) -> List[List[int]]:
levels: List[deque] = []
def visit(node: Optional[TreeNode], depth: int) -> None:
if not node:
return
if depth == len(levels):
levels.append(deque())
if depth % 2:
levels[depth].appendleft(node.val)
else:
levels[depth].append(node.val)
visit(node.left, depth + 1)
visit(node.right, depth + 1)
visit(root, 0)
return [list(level) for level in levels]
def zigzagLevelOrder_deque(self, root: Optional[TreeNode]) -> List[List[int]]:
result: List[List[int]] = []
queue = deque([root] if root else [])
left_to_right = True
while queue:
level: deque = deque()
for _ in range(len(queue)):
node = queue.popleft()
if left_to_right:
level.append(node.val)
else:
level.appendleft(node.val)
if node.left:
queue.append(node.left)
if node.right:
queue.append(node.right)
result.append(list(level))
left_to_right = not left_to_right
return result
def zigzagLevelOrder(self, root: Optional[TreeNode]) -> List[List[int]]:
result: List[List[int]] = []
queue = deque([root] if root else [])
while queue:
level = []
for _ in range(len(queue)):
node = queue.popleft()
level.append(node.val)
if node.left:
queue.append(node.left)
if node.right:
queue.append(node.right)
if len(result) % 2:
level.reverse()
result.append(level)
return result
vector<vector<int>> zigzagLevelOrderDfs(TreeNode *root) {
vector<vector<int>> levels;
function<void(TreeNode *, size_t)> visit = [&](TreeNode *node,
size_t depth) {
if (!node) return;
if (depth == levels.size()) levels.push_back({});
levels[depth].push_back(node->val);
visit(node->left, depth + 1);
visit(node->right, depth + 1);
};
visit(root, 0);
for (size_t d = 1; d < levels.size(); d += 2)
reverse(levels[d].begin(), levels[d].end());
return levels;
}
vector<vector<int>> zigzagLevelOrderMirror(TreeNode *root) {
vector<vector<int>> result;
queue<TreeNode *> q;
if (root) q.push(root);
bool leftToRight = true;
while (!q.empty()) {
size_t width = q.size();
vector<int> level(width);
for (size_t i = 0; i < width; ++i) {
TreeNode *node = q.front();
q.pop();
level[leftToRight ? i : width - 1 - i] = node->val;
if (node->left) q.push(node->left);
if (node->right) q.push(node->right);
}
result.push_back(move(level));
leftToRight = !leftToRight;
}
return result;
}
vector<vector<int>> zigzagLevelOrder(TreeNode *root) {
vector<vector<int>> result;
queue<TreeNode *> q;
if (root) q.push(root);
while (!q.empty()) {
size_t width = q.size();
vector<int> level;
level.reserve(width);
for (size_t i = 0; i < width; ++i) {
TreeNode *node = q.front();
q.pop();
level.push_back(node->val);
if (node->left) q.push(node->left);
if (node->right) q.push(node->right);
}
if (result.size() % 2 == 1) reverse(level.begin(), level.end());
result.push_back(move(level));
}
return result;
}
pub fn zigzag_level_order_dfs(root: Option<Rc<RefCell<TreeNode>>>) -> Vec<Vec<i32>> {
fn visit(
node: &Option<Rc<RefCell<TreeNode>>>,
depth: usize,
levels: &mut Vec<Vec<i32>>,
) {
if let Some(rc) = node {
let n = rc.borrow();
if depth == levels.len() {
levels.push(Vec::new());
}
levels[depth].push(n.val);
visit(&n.left, depth + 1, levels);
visit(&n.right, depth + 1, levels);
}
}
let mut levels = Vec::new();
visit(&root, 0, &mut levels);
for level in levels.iter_mut().skip(1).step_by(2) {
level.reverse();
}
levels
}
pub fn zigzag_level_order_mirror(root: Option<Rc<RefCell<TreeNode>>>) -> Vec<Vec<i32>> {
let mut result: Vec<Vec<i32>> = Vec::new();
let mut queue = VecDeque::new();
if let Some(r) = root {
queue.push_back(r);
}
let mut left_to_right = true;
while !queue.is_empty() {
let width = queue.len();
let mut level = vec![0; width];
for i in 0..width {
let node = queue.pop_front().unwrap();
let n = node.borrow();
let slot = if left_to_right { i } else { width - 1 - i };
level[slot] = n.val;
if let Some(left) = n.left.clone() {
queue.push_back(left);
}
if let Some(right) = n.right.clone() {
queue.push_back(right);
}
}
result.push(level);
left_to_right = !left_to_right;
}
result
}
pub fn zigzag_level_order(root: Option<Rc<RefCell<TreeNode>>>) -> Vec<Vec<i32>> {
let mut result: Vec<Vec<i32>> = Vec::new();
let mut queue = VecDeque::new();
if let Some(r) = root {
queue.push_back(r);
}
while !queue.is_empty() {
let width = queue.len();
let mut level = Vec::with_capacity(width);
for _ in 0..width {
let node = queue.pop_front().unwrap();
let n = node.borrow();
level.push(n.val);
if let Some(left) = n.left.clone() {
queue.push_back(left);
}
if let Some(right) = n.right.clone() {
queue.push_back(right);
}
}
if result.len() % 2 == 1 {
level.reverse();
}
result.push(level);
}
result
}
function zigzagLevelOrderDfs(root: TreeNode | null): number[][] {
const levels: number[][] = [];
const visit = (node: TreeNode | null, depth: number): void => {
if (!node) return;
if (depth === levels.length) levels.push([]);
levels[depth].push(node.val);
visit(node.left, depth + 1);
visit(node.right, depth + 1);
};
visit(root, 0);
for (let d = 1; d < levels.length; d += 2) {
levels[d].reverse();
}
return levels;
}
function zigzagLevelOrderMirror(root: TreeNode | null): number[][] {
const result: number[][] = [];
let level: TreeNode[] = root ? [root] : [];
let leftToRight = true;
while (level.length > 0) {
const width = level.length;
const values: number[] = new Array(width);
const next: TreeNode[] = [];
for (let i = 0; i < width; i++) {
const node = level[i];
values[leftToRight ? i : width - 1 - i] = node.val;
if (node.left) next.push(node.left);
if (node.right) next.push(node.right);
}
result.push(values);
level = next;
leftToRight = !leftToRight;
}
return result;
}
function zigzagLevelOrder(root: TreeNode | null): number[][] {
const result: number[][] = [];
let level: TreeNode[] = root ? [root] : [];
while (level.length > 0) {
const values: number[] = [];
const next: TreeNode[] = [];
for (const node of level) {
values.push(node.val);
if (node.left) next.push(node.left);
if (node.right) next.push(node.right);
}
if (result.length % 2 === 1) values.reverse();
result.push(values);
level = next;
}
return result;
}
func zigzagLevelOrderDfs(root *TreeNode) [][]int {
levels := [][]int{}
var visit func(node *TreeNode, depth int)
visit = func(node *TreeNode, depth int) {
if node == nil {
return
}
if depth == len(levels) {
levels = append(levels, []int{})
}
levels[depth] = append(levels[depth], node.Val)
visit(node.Left, depth+1)
visit(node.Right, depth+1)
}
visit(root, 0)
for d := 1; d < len(levels); d += 2 {
level := levels[d]
for l, r := 0, len(level)-1; l < r; l, r = l+1, r-1 {
level[l], level[r] = level[r], level[l]
}
}
return levels
}
func zigzagLevelOrderMirror(root *TreeNode) [][]int {
result := [][]int{}
queue := []*TreeNode{}
if root != nil {
queue = append(queue, root)
}
leftToRight := true
for len(queue) > 0 {
width := len(queue)
level := make([]int, width)
for i := 0; i < width; i++ {
node := queue[0]
queue = queue[1:]
if leftToRight {
level[i] = node.Val
} else {
level[width-1-i] = node.Val
}
if node.Left != nil {
queue = append(queue, node.Left)
}
if node.Right != nil {
queue = append(queue, node.Right)
}
}
result = append(result, level)
leftToRight = !leftToRight
}
return result
}
func zigzagLevelOrder(root *TreeNode) [][]int {
result := [][]int{}
queue := []*TreeNode{}
if root != nil {
queue = append(queue, root)
}
for len(queue) > 0 {
width := len(queue)
level := make([]int, 0, width)
for i := 0; i < width; i++ {
node := queue[0]
queue = queue[1:]
level = append(level, node.Val)
if node.Left != nil {
queue = append(queue, node.Left)
}
if node.Right != nil {
queue = append(queue, node.Right)
}
}
if len(result)%2 == 1 {
for l, r := 0, len(level)-1; l < r; l, r = l+1, r-1 {
level[l], level[r] = level[r], level[l]
}
}
result = append(result, level)
}
return result
}
func zigzagLevelOrderDfs(_ root: TreeNode?) -> [[Int]] {
var levels: [[Int]] = []
func visit(_ node: TreeNode?, _ depth: Int) {
guard let node = node else { return }
if depth == levels.count {
levels.append([])
}
levels[depth].append(node.val)
visit(node.left, depth + 1)
visit(node.right, depth + 1)
}
visit(root, 0)
for d in stride(from: 1, to: levels.count, by: 2) {
levels[d].reverse()
}
return levels
}
func zigzagLevelOrderMirror(_ root: TreeNode?) -> [[Int]] {
var result: [[Int]] = []
var queue: [TreeNode] = []
if let root = root { queue.append(root) }
var leftToRight = true
while !queue.isEmpty {
let width = queue.count
var values = [Int](repeating: 0, count: width)
var nextQueue: [TreeNode] = []
for (i, node) in queue.enumerated() {
values[leftToRight ? i : width - 1 - i] = node.val
if let left = node.left { nextQueue.append(left) }
if let right = node.right { nextQueue.append(right) }
}
result.append(values)
queue = nextQueue
leftToRight.toggle()
}
return result
}
func zigzagLevelOrder(_ root: TreeNode?) -> [[Int]] {
var result: [[Int]] = []
var queue: [TreeNode] = []
if let root = root { queue.append(root) }
while !queue.isEmpty {
var level: [Int] = []
var nextQueue: [TreeNode] = []
for node in queue {
level.append(node.val)
if let left = node.left { nextQueue.append(left) }
if let right = node.right { nextQueue.append(right) }
}
if result.count % 2 == 1 { level.reverse() }
result.append(level)
queue = nextQueue
}
return result
}
Binary Search Tree
33. Minimum Absolute Difference in BST
Easy · LC 530
Given a binary search tree, return the minimum absolute difference between the values of any two nodes. Run a Morris inorder traversal, which visits values in sorted order by temporarily threading each inorder predecessor's right pointer back to the current node, and track the smallest gap between consecutive values. The insight is that in sorted order the answer must occur between adjacent neighbors, and the threading removes both recursion and stack, leaving an O(n) walk in O(1) space with all links restored.
Ignores the BST property entirely — this works on any binary tree. But sorting recreates an order the tree already stores; the rungs below read it out directly and drop the log factor.
Inorder visits BST values in sorted order, so only the previous value need be remembered — no list, no sort. Leans on the interpreter's call stack, though: a skewed 10^4-node tree would trip Python's ~1000-frame recursion limit.
The same walk with the recursion swapped for an explicit stack — identical bounds, but no recursion-depth worries on a skewed 10^4-node tree.
The same sorted walk with the stack gone too: temporarily thread each node's inorder predecessor's right pointer back to it, so the traversal needs no extra memory at all (links are restored).
Ignores the BST property entirely — this works on any binary tree. But sorting recreates an order the tree already stores; the rungs below read it out directly and drop the log factor.
Inorder visits BST values in sorted order, so only the previous value need be remembered — no vector, no sort. Recursion depth equals tree height, though, so a skewed tree leans on the call stack.
The same walk with the recursion swapped for an explicit stack — identical bounds, but no recursion-depth worries on a skewed 10^4-node tree.
Ignores the BST property entirely — this works on any binary tree. But sorting recreates an order the tree already stores; the inorder rung below reads it out directly and drops the log factor.
Inorder visits BST values in sorted order, so only the previous value need be remembered — no buffer, no sort, and the log factor is gone.
Ignores the BST property entirely — this works on any binary tree. But sorting recreates an order the tree already stores; the rungs below read it out directly and drop the log factor.
Inorder visits BST values in sorted order, so only the previous value need be remembered — no array, no sort. Recursion depth equals tree height, though, so a skewed tree leans on the call stack.
The same walk with the recursion swapped for an explicit stack — identical bounds, but no recursion-depth worries on a skewed 10^4-node tree.
Ignores the BST property entirely — this works on any binary tree. But sorting recreates an order the tree already stores; the rungs below read it out directly and drop the log factor.
Inorder visits BST values in sorted order, so only the previous value need be remembered — no slice, no sort. Recursion depth equals tree height, though, so a skewed tree leans on the call stack.
The same walk with the recursion swapped for an explicit stack — identical bounds, but no recursion-depth worries on a skewed 10^4-node tree.
Ignores the BST property entirely — this works on any binary tree. But sorting recreates an order the tree already stores; the rungs below read it out directly and drop the log factor.
Inorder visits BST values in sorted order, so only the previous value need be remembered — no array, no sort. Recursion depth equals tree height, though, so a skewed tree leans on the call stack.
The same walk with the recursion swapped for an explicit stack — identical bounds, but no recursion-depth worries on a skewed 10^4-node tree.
def getMinimumDifference_sorted(self, root: Optional[TreeNode]) -> int:
values = []
stack = [root]
while stack:
node = stack.pop()
if node:
values.append(node.val)
stack.append(node.left)
stack.append(node.right)
values.sort()
return min(b - a for a, b in zip(values, values[1:]))
def getMinimumDifference_recursive(self, root: Optional[TreeNode]) -> int:
prev = None
best = float("inf")
def inorder(node: Optional[TreeNode]) -> None:
nonlocal prev, best
if not node:
return
inorder(node.left)
if prev is not None:
best = min(best, node.val - prev)
prev = node.val
inorder(node.right)
inorder(root)
return best
def getMinimumDifference_stack(self, root: Optional[TreeNode]) -> int:
stack = []
node = root
prev = None
best = float("inf")
while stack or node:
while node:
stack.append(node)
node = node.left
node = stack.pop()
if prev is not None:
best = min(best, node.val - prev)
prev = node.val
node = node.right
return best # n >= 2, so at least one gap was measured
def getMinimumDifference(self, root: Optional[TreeNode]) -> int:
prev = None
best = float("inf")
node = root
while node:
if node.left:
pred = node.left
while pred.right and pred.right is not node:
pred = pred.right
if pred.right is None: # first visit: thread and dive left
pred.right = node
node = node.left
continue
pred.right = None # second visit: unthread, fall through
if prev is not None:
best = min(best, node.val - prev)
prev = node.val
node = node.right
return best
int getMinimumDifferenceSorted(TreeNode *root) {
vector<int> vals;
function<void(TreeNode *)> collect = [&](TreeNode *node) {
if (!node) return;
vals.push_back(node->val);
collect(node->left);
collect(node->right);
};
collect(root);
sort(vals.begin(), vals.end());
int best = INT_MAX;
for (size_t i = 1; i < vals.size(); ++i)
best = min(best, vals[i] - vals[i - 1]);
return best;
}
int getMinimumDifferenceRecursive(TreeNode *root) {
int best = INT_MAX;
int prev = -1; // values are >= 0 per the constraints
function<void(TreeNode *)> inorder = [&](TreeNode *node) {
if (!node) return;
inorder(node->left);
if (prev >= 0) best = min(best, node->val - prev);
prev = node->val;
inorder(node->right);
};
inorder(root);
return best;
}
int getMinimumDifference(TreeNode *root) {
stack<TreeNode *> st;
TreeNode *node = root;
int best = INT_MAX;
bool hasPrev = false;
int prev = 0;
while (!st.empty() || node) {
while (node) {
st.push(node);
node = node->left;
}
node = st.top();
st.pop();
if (hasPrev) best = min(best, node->val - prev);
prev = node->val;
hasPrev = true;
node = node->right;
}
return best; // n >= 2, so at least one gap was measured
}
pub fn get_minimum_difference_sorted(root: Option<Rc<RefCell<TreeNode>>>) -> i32 {
fn collect(node: &Option<Rc<RefCell<TreeNode>>>, vals: &mut Vec<i32>) {
if let Some(rc) = node {
let n = rc.borrow();
vals.push(n.val);
collect(&n.left, vals);
collect(&n.right, vals);
}
}
let mut vals = Vec::new();
collect(&root, &mut vals);
vals.sort_unstable();
vals.windows(2)
.map(|w| w[1] - w[0])
.min()
.unwrap_or(i32::MAX)
}
pub fn get_minimum_difference(root: Option<Rc<RefCell<TreeNode>>>) -> i32 {
fn inorder(
node: &Option<Rc<RefCell<TreeNode>>>,
prev: &mut Option<i32>,
best: &mut i32,
) {
if let Some(rc) = node {
let n = rc.borrow();
inorder(&n.left, prev, best);
if let Some(p) = *prev {
*best = (*best).min(n.val - p);
}
*prev = Some(n.val);
inorder(&n.right, prev, best);
}
}
let mut prev = None;
let mut best = i32::MAX;
inorder(&root, &mut prev, &mut best);
best // n >= 2, so at least one gap was measured
}
function getMinimumDifferenceSorted(root: TreeNode | null): number {
const vals: number[] = [];
const stack: (TreeNode | null)[] = [root];
while (stack.length > 0) {
const node = stack.pop()!;
if (node) {
vals.push(node.val);
stack.push(node.left, node.right);
}
}
vals.sort((a, b) => a - b);
let best = Infinity;
for (let i = 1; i < vals.length; i++) {
best = Math.min(best, vals[i] - vals[i - 1]);
}
return best;
}
function getMinimumDifferenceRecursive(root: TreeNode | null): number {
let best = Infinity;
let prev: number | null = null;
const inorder = (node: TreeNode | null): void => {
if (!node) return;
inorder(node.left);
if (prev !== null) best = Math.min(best, node.val - prev);
prev = node.val;
inorder(node.right);
};
inorder(root);
return best;
}
function getMinimumDifference(root: TreeNode | null): number {
const stack: TreeNode[] = [];
let node = root;
let prev: number | null = null;
let best = Infinity;
while (stack.length > 0 || node !== null) {
while (node !== null) {
stack.push(node);
node = node.left;
}
const top = stack.pop() as TreeNode;
if (prev !== null) best = Math.min(best, top.val - prev);
prev = top.val;
node = top.right;
}
return best; // n >= 2, so at least one gap was measured
}
func getMinimumDifferenceSorted(root *TreeNode) int {
vals := []int{}
stack := []*TreeNode{root}
for len(stack) > 0 {
node := stack[len(stack)-1]
stack = stack[:len(stack)-1]
if node != nil {
vals = append(vals, node.Val)
stack = append(stack, node.Left, node.Right)
}
}
sort.Ints(vals)
best := math.MaxInt32
for i := 1; i < len(vals); i++ {
if vals[i]-vals[i-1] < best {
best = vals[i] - vals[i-1]
}
}
return best
}
func getMinimumDifferenceRecursive(root *TreeNode) int {
best := math.MaxInt32
prev := -1 // values are >= 0 per the constraints
var inorder func(node *TreeNode)
inorder = func(node *TreeNode) {
if node == nil {
return
}
inorder(node.Left)
if prev >= 0 && node.Val-prev < best {
best = node.Val - prev
}
prev = node.Val
inorder(node.Right)
}
inorder(root)
return best
}
func getMinimumDifference(root *TreeNode) int {
stack := []*TreeNode{}
node := root
best := math.MaxInt32
prev := -1 // values are >= 0 per the constraints
for len(stack) > 0 || node != nil {
for node != nil {
stack = append(stack, node)
node = node.Left
}
node = stack[len(stack)-1]
stack = stack[:len(stack)-1]
if prev >= 0 && node.Val-prev < best {
best = node.Val - prev
}
prev = node.Val
node = node.Right
}
return best // n >= 2, so at least one gap was measured
}
func getMinimumDifferenceSorted(_ root: TreeNode?) -> Int {
var vals: [Int] = []
var stack: [TreeNode?] = [root]
while let node = stack.popLast() {
if let node = node {
vals.append(node.val)
stack.append(node.left)
stack.append(node.right)
}
}
vals.sort()
var best = Int.max
for i in 1..<vals.count {
best = min(best, vals[i] - vals[i - 1])
}
return best
}
func getMinimumDifferenceRecursive(_ root: TreeNode?) -> Int {
var best = Int.max
var prev: Int? = nil
func inorder(_ node: TreeNode?) {
guard let node = node else { return }
inorder(node.left)
if let p = prev {
best = min(best, node.val - p)
}
prev = node.val
inorder(node.right)
}
inorder(root)
return best
}
func getMinimumDifference(_ root: TreeNode?) -> Int {
var stack: [TreeNode] = []
var node = root
var prev: Int? = nil
var best = Int.max
while !stack.isEmpty || node != nil {
while let current = node {
stack.append(current)
node = current.left
}
let top = stack.removeLast()
if let p = prev {
best = min(best, top.val - p)
}
prev = top.val
node = top.right
}
return best // n >= 2, so at least one gap was measured
}
Graph BFS
34. Snakes and Ladders
Medium · LC 909
Given a snakes-and-ladders board labeled boustrophedon style, return the fewest die rolls needed to reach the final square, or -1 if it cannot be reached. Since every roll costs the same, run a breadth-first search from square 1 with a distance map: pop a square, try each of the six destinations, decode the label into board coordinates, follow any snake or ladder found there, and enqueue unseen squares at distance plus one. The pitfalls are the label decoding — rows alternate direction and count from the bottom — and the rule that a snake or ladder is taken exactly once, never chained.
No queue, no visit order — just sweep every square and relax dist[next] = dist[cur] + 1 until nothing changes. Shortest distances are exactly the fixpoint of that relaxation, which is the correctness argument all the later rungs lean on. Each sweep is O(n^2); n <= 20 keeps it instant despite the awful bound.
Replaces Approach 1's blind sweeping with a priority queue that settles each square once, in distance order — the general shortest-path tool this problem reduces to. Overkill here (every edge costs 1), which is what the next rung exploits.
Unit-weight edges make Approach 2's heap unnecessary: expanding whole frontiers level by level already settles squares in distance order, dropping the log factor. The loop depth IS the distance, so no per-node distance storage is needed.
Same traversal as Approach 3 with one deque instead of a rebuilt frontier set per level, and it records the roll count for every square it touches — not just the target. Each square is enqueued at most once; with unit-cost edges the first time the target is popped its distance is already optimal.
No queue, no visit order — sweep every square and relax dist[next] = dist[cur] + 1 until nothing changes. Shortest distances are exactly the fixpoint of that relaxation, which is the correctness argument BFS leans on too. Each sweep is O(n^2); n <= 20 keeps it instant despite the awful bound.
Replaces Approach 1's repeated sweeps with a queue that reaches the fixpoint in a single pass: each square is enqueued at most once, and with unit-cost edges the first time the target is popped its distance is already optimal.
No queue, no visit order — sweep every square and relax dist[next] = dist[cur] + 1 until nothing changes. Shortest distances are exactly the fixpoint of that relaxation, which is the correctness argument BFS leans on too. Each sweep is O(n^2); n <= 20 keeps it instant despite the awful bound.
Replaces Approach 1's repeated sweeps with a queue that reaches the fixpoint in a single pass: each square is enqueued at most once, and with unit-cost edges the first time the target is popped its distance is already optimal.
No queue, no visit order — sweep every square and relax dist[next] = dist[cur] + 1 until nothing changes. Shortest distances are exactly the fixpoint of that relaxation, which is the correctness argument BFS leans on too. Each sweep is O(n^2); n <= 20 keeps it instant despite the awful bound.
Replaces Approach 1's repeated sweeps with a queue that reaches the fixpoint in a single pass: each square is enqueued at most once, and with unit-cost edges the first time the target is popped its distance is already optimal.
No queue, no visit order — sweep every square and relax dist[next] = dist[cur] + 1 until nothing changes. Shortest distances are exactly the fixpoint of that relaxation, which is the correctness argument BFS leans on too. Each sweep is O(n^2); n <= 20 keeps it instant despite the awful bound.
Replaces Approach 1's repeated sweeps with a queue that reaches the fixpoint in a single pass: each square is enqueued at most once, and with unit-cost edges the first time the target is popped its distance is already optimal.
No queue, no visit order — sweep every square and relax dist[next] = dist[cur] + 1 until nothing changes. Shortest distances are exactly the fixpoint of that relaxation, which is the correctness argument BFS leans on too. Each sweep is O(n^2); n <= 20 keeps it instant despite the awful bound.
Replaces Approach 1's repeated sweeps with a queue that reaches the fixpoint in a single pass: each square is enqueued at most once, and with unit-cost edges the first time the target is popped its distance is already optimal.
def snakesAndLadders_bellman_ford(self, board: List[List[int]]) -> int:
n = len(board)
target = n * n
unreached = float("inf")
dist = [unreached] * (target + 1)
dist[1] = 0
changed = True
while changed:
changed = False
for cur in range(1, target + 1):
if dist[cur] == unreached:
continue
for roll in range(cur + 1, min(cur + 6, target) + 1):
jump = self._square_value(board, roll)
nxt = jump if jump != -1 else roll
if dist[cur] + 1 < dist[nxt]:
dist[nxt] = dist[cur] + 1
changed = True
return -1 if dist[target] == unreached else dist[target]
def snakesAndLadders_dijkstra(self, board: List[List[int]]) -> int:
n = len(board)
target = n * n
best = {1: 0}
heap = [(0, 1)]
while heap:
d, cur = heappop(heap)
if cur == target:
return d
if d > best.get(cur, float("inf")):
continue
for roll in range(cur + 1, min(cur + 6, target) + 1):
jump = self._square_value(board, roll)
nxt = jump if jump != -1 else roll
if d + 1 < best.get(nxt, float("inf")):
best[nxt] = d + 1
heappush(heap, (d + 1, nxt))
return -1
def snakesAndLadders_level_bfs(self, board: List[List[int]]) -> int:
n = len(board)
target = n * n
frontier = {1}
visited = {1}
rolls = 0
while frontier:
if target in frontier:
return rolls
nxt_frontier = set()
for cur in frontier:
for roll in range(cur + 1, min(cur + 6, target) + 1):
jump = self._square_value(board, roll)
nxt = jump if jump != -1 else roll
if nxt not in visited:
visited.add(nxt)
nxt_frontier.add(nxt)
frontier = nxt_frontier
rolls += 1
return -1
def snakesAndLadders(self, board: List[List[int]]) -> int:
n = len(board)
target = n * n
dist = {1: 0}
queue = deque([1])
while queue:
cur = queue.popleft()
if cur == target:
return dist[cur]
for roll in range(cur + 1, min(cur + 6, target) + 1):
jump = self._square_value(board, roll)
nxt = jump if jump != -1 else roll # take snake/ladder once
if nxt not in dist:
dist[nxt] = dist[cur] + 1
queue.append(nxt)
return -1
int snakesAndLaddersBellmanFord(vector<vector<int>>& board) {
const int n = static_cast<int>(board.size());
const int target = n * n;
auto squareValue = [&](int label) {
int rowFromBottom = (label - 1) / n;
int offset = (label - 1) % n;
int row = n - 1 - rowFromBottom;
int col = (rowFromBottom % 2 == 0) ? offset : n - 1 - offset;
return board[row][col];
};
const int unreached = target + 1; // no path uses more than target rolls
vector<int> dist(target + 1, unreached);
dist[1] = 0;
bool changed = true;
while (changed) {
changed = false;
for (int cur = 1; cur <= target; ++cur) {
if (dist[cur] == unreached) continue;
for (int roll = cur + 1; roll <= std::min(cur + 6, target); ++roll) {
int jump = squareValue(roll);
int nxt = (jump != -1) ? jump : roll;
if (dist[cur] + 1 < dist[nxt]) {
dist[nxt] = dist[cur] + 1;
changed = true;
}
}
}
}
return dist[target] == unreached ? -1 : dist[target];
}
int snakesAndLadders(vector<vector<int>>& board) {
const int n = static_cast<int>(board.size());
const int target = n * n;
auto squareValue = [&](int label) {
int rowFromBottom = (label - 1) / n;
int offset = (label - 1) % n;
int row = n - 1 - rowFromBottom;
int col = (rowFromBottom % 2 == 0) ? offset : n - 1 - offset;
return board[row][col];
};
vector<int> dist(target + 1, -1);
dist[1] = 0;
queue<int> pending;
pending.push(1);
while (!pending.empty()) {
int cur = pending.front();
pending.pop();
if (cur == target) return dist[cur];
for (int roll = cur + 1; roll <= std::min(cur + 6, target); ++roll) {
int jump = squareValue(roll);
int nxt = (jump != -1) ? jump : roll; // take snake/ladder once
if (dist[nxt] == -1) {
dist[nxt] = dist[cur] + 1;
pending.push(nxt);
}
}
}
return -1;
}
pub fn snakes_and_ladders_bellman_ford(board: Vec<Vec<i32>>) -> i32 {
let n = board.len();
let target = n * n;
let square_value = |label: usize| -> i32 {
let row_from_bottom = (label - 1) / n;
let offset = (label - 1) % n;
let row = n - 1 - row_from_bottom;
let col = if row_from_bottom % 2 == 0 {
offset
} else {
n - 1 - offset
};
board[row][col]
};
let unreached = target as i32 + 1; // no path uses more than target rolls
let mut dist = vec![unreached; target + 1];
dist[1] = 0;
let mut changed = true;
while changed {
changed = false;
for cur in 1..=target {
if dist[cur] == unreached {
continue;
}
for roll in cur + 1..=(cur + 6).min(target) {
let jump = square_value(roll);
let next = if jump != -1 { jump as usize } else { roll };
if dist[cur] + 1 < dist[next] {
dist[next] = dist[cur] + 1;
changed = true;
}
}
}
}
if dist[target] == unreached {
-1
} else {
dist[target]
}
}
pub fn snakes_and_ladders(board: Vec<Vec<i32>>) -> i32 {
let n = board.len();
let target = n * n;
let square_value = |label: usize| -> i32 {
let row_from_bottom = (label - 1) / n;
let offset = (label - 1) % n;
let row = n - 1 - row_from_bottom;
let col = if row_from_bottom % 2 == 0 {
offset
} else {
n - 1 - offset
};
board[row][col]
};
let mut dist = vec![-1; target + 1];
dist[1] = 0;
let mut queue = VecDeque::from([1usize]);
while let Some(cur) = queue.pop_front() {
if cur == target {
return dist[cur];
}
for roll in cur + 1..=(cur + 6).min(target) {
let jump = square_value(roll);
let next = if jump != -1 { jump as usize } else { roll };
if dist[next] == -1 {
dist[next] = dist[cur] + 1;
queue.push_back(next);
}
}
}
-1
}
function snakesAndLaddersBellmanFord(board: number[][]): number {
const n = board.length;
const target = n * n;
const squareValue = (label: number): number => {
const rowFromBottom = Math.floor((label - 1) / n);
const offset = (label - 1) % n;
const row = n - 1 - rowFromBottom;
const col = rowFromBottom % 2 === 0 ? offset : n - 1 - offset;
return board[row][col];
};
const unreached = target + 1; // no path uses more than target rolls
const dist = new Array<number>(target + 1).fill(unreached);
dist[1] = 0;
let changed = true;
while (changed) {
changed = false;
for (let cur = 1; cur <= target; cur++) {
if (dist[cur] === unreached) continue;
for (let roll = cur + 1; roll <= Math.min(cur + 6, target); roll++) {
const jump = squareValue(roll);
const next = jump !== -1 ? jump : roll;
if (dist[cur] + 1 < dist[next]) {
dist[next] = dist[cur] + 1;
changed = true;
}
}
}
}
return dist[target] === unreached ? -1 : dist[target];
}
function snakesAndLadders(board: number[][]): number {
const n = board.length;
const target = n * n;
const squareValue = (label: number): number => {
const rowFromBottom = Math.floor((label - 1) / n);
const offset = (label - 1) % n;
const row = n - 1 - rowFromBottom;
const col = rowFromBottom % 2 === 0 ? offset : n - 1 - offset;
return board[row][col];
};
const dist = new Array<number>(target + 1).fill(-1);
dist[1] = 0;
const queue: number[] = [1];
let head = 0;
while (head < queue.length) {
const cur = queue[head++];
if (cur === target) return dist[cur];
for (let roll = cur + 1; roll <= Math.min(cur + 6, target); roll++) {
const jump = squareValue(roll);
const next = jump !== -1 ? jump : roll; // take snake/ladder once
if (dist[next] === -1) {
dist[next] = dist[cur] + 1;
queue.push(next);
}
}
}
return -1;
}
func snakesAndLaddersBellmanFord(board [][]int) int {
n := len(board)
target := n * n
squareValue := func(label int) int {
rowFromBottom := (label - 1) / n
offset := (label - 1) % n
row := n - 1 - rowFromBottom
col := offset
if rowFromBottom%2 == 1 {
col = n - 1 - offset
}
return board[row][col]
}
unreached := target + 1 // no path uses more than target rolls
dist := make([]int, target+1)
for i := range dist {
dist[i] = unreached
}
dist[1] = 0
for changed := true; changed; {
changed = false
for cur := 1; cur <= target; cur++ {
if dist[cur] == unreached {
continue
}
for roll := cur + 1; roll <= cur+6 && roll <= target; roll++ {
next := roll
if jump := squareValue(roll); jump != -1 {
next = jump
}
if dist[cur]+1 < dist[next] {
dist[next] = dist[cur] + 1
changed = true
}
}
}
}
if dist[target] == unreached {
return -1
}
return dist[target]
}
func snakesAndLadders(board [][]int) int {
n := len(board)
target := n * n
squareValue := func(label int) int {
rowFromBottom := (label - 1) / n
offset := (label - 1) % n
row := n - 1 - rowFromBottom
col := offset
if rowFromBottom%2 == 1 {
col = n - 1 - offset
}
return board[row][col]
}
dist := make([]int, target+1)
for i := range dist {
dist[i] = -1
}
dist[1] = 0
queue := []int{1}
for len(queue) > 0 {
cur := queue[0]
queue = queue[1:]
if cur == target {
return dist[cur]
}
for roll := cur + 1; roll <= cur+6 && roll <= target; roll++ {
next := roll
if jump := squareValue(roll); jump != -1 {
next = jump // take snake/ladder exactly once
}
if dist[next] == -1 {
dist[next] = dist[cur] + 1
queue = append(queue, next)
}
}
}
return -1
}
func snakesAndLaddersBellmanFord(_ board: [[Int]]) -> Int {
let n = board.count
let target = n * n
func squareValue(_ label: Int) -> Int {
let rowFromBottom = (label - 1) / n
let offset = (label - 1) % n
let row = n - 1 - rowFromBottom
let col = rowFromBottom % 2 == 0 ? offset : n - 1 - offset
return board[row][col]
}
let unreached = target + 1 // no path uses more than target rolls
var dist = [Int](repeating: unreached, count: target + 1)
dist[1] = 0
var changed = true
while changed {
changed = false
for cur in 1...target where dist[cur] != unreached {
for roll in stride(from: cur + 1, through: min(cur + 6, target), by: 1) {
let jump = squareValue(roll)
let next = jump != -1 ? jump : roll
if dist[cur] + 1 < dist[next] {
dist[next] = dist[cur] + 1
changed = true
}
}
}
}
return dist[target] == unreached ? -1 : dist[target]
}
func snakesAndLadders(_ board: [[Int]]) -> Int {
let n = board.count
let target = n * n
func squareValue(_ label: Int) -> Int {
let rowFromBottom = (label - 1) / n
let offset = (label - 1) % n
let row = n - 1 - rowFromBottom
let col = rowFromBottom % 2 == 0 ? offset : n - 1 - offset
return board[row][col]
}
var dist = [Int](repeating: -1, count: target + 1)
dist[1] = 0
var queue = [1]
var head = 0
while head < queue.count {
let cur = queue[head]
head += 1
if cur == target { return dist[cur] }
for roll in stride(from: cur + 1, through: min(cur + 6, target), by: 1) {
let jump = squareValue(roll)
let next = jump != -1 ? jump : roll // take snake/ladder once
if dist[next] == -1 {
dist[next] = dist[cur] + 1
queue.append(next)
}
}
}
return -1
}
35. Minimum Genetic Mutation
Medium · LC 433
Given a start gene, an end gene, and a bank of valid genes, return the minimum number of single-character mutations that turns the start into the end, where every intermediate gene must appear in the bank. Since each mutation costs the same, run a breadth-first search from the start gene: for each gene popped, generate every string one character away over the alphabet ACGT, and enqueue those that are in the bank set and not yet seen. The trick is filtering candidates through the bank set instead of scanning the bank for neighbors, which keeps each step to 24 cheap membership tests and never lets the search leave valid genes.
Try every sequence of distinct bank genes that steps one character at a time, and keep the shallowest depth that reaches endGene. A shortest mutation path never revisits a gene, so enumerating all simple paths is exhaustive-but-correct; pruning at the best depth found so far and the 10-gene bank cap keep the blowup tame.
Collapses Approach 1's factorial blowup: BFS visits genes in distance order, so the first arrival at endGene is already optimal and each gene is expanded once. Neighbors are found by scanning the bank for genes exactly one character away — simple to reason about, fine for a tiny bank.
Two upgrades over Approach 2: neighbors are generated as the 3L one-char edits and filtered through a bank set (O(L) per expansion instead of an O(B) scan), and the search grows from both ends, meeting in the middle — the classic way to cut the search depth in half on word ladders.
Single-direction version of Approach 3's neighbor generation: each bank gene is enqueued once and expanded into its 3L candidate mutations. With depth capped at 8 the two-ended trick buys almost nothing here, and the plain queue is simpler, has no special cases, and yields the mutation count for every reachable gene.
Try every sequence of distinct bank genes that steps one character at a time, and keep the shallowest depth that reaches endGene. A shortest mutation path never revisits a gene, so enumerating all simple paths is exhaustive-but-correct; pruning at the best depth found so far and the 10-gene bank cap keep the blowup tame.
Collapses Approach 1's factorial blowup: BFS visits genes in distance order, so the first arrival at endGene is already optimal. Each popped gene expands into its 3L one-char edits, filtered through the bank set, so each bank gene is enqueued at most once and the search never leaves valid genes.
Try every sequence of distinct bank genes that steps one character at a time, and keep the shallowest depth that reaches endGene. A shortest mutation path never revisits a gene, so enumerating all simple paths is exhaustive-but-correct; pruning at the best depth found so far and the 10-gene bank cap keep the blowup tame.
Collapses Approach 1's factorial blowup: BFS visits genes in distance order, so the first arrival at endGene is already optimal. Each popped gene expands into its 3L one-char edits, filtered through the bank set, so each bank gene is enqueued at most once and the search never leaves valid genes.
Try every sequence of distinct bank genes that steps one character at a time, and keep the shallowest depth that reaches endGene. A shortest mutation path never revisits a gene, so enumerating all simple paths is exhaustive-but-correct; pruning at the best depth found so far and the 10-gene bank cap keep the blowup tame.
Collapses Approach 1's factorial blowup: BFS visits genes in distance order, so the first arrival at endGene is already optimal. Each popped gene expands into its 3L one-char edits, filtered through the bank set, so each bank gene is enqueued at most once and the search never leaves valid genes.
Try every sequence of distinct bank genes that steps one character at a time, and keep the shallowest depth that reaches endGene. A shortest mutation path never revisits a gene, so enumerating all simple paths is exhaustive-but-correct; pruning at the best depth found so far and the 10-gene bank cap keep the blowup tame.
Collapses Approach 1's factorial blowup: BFS visits genes in distance order, so the first arrival at endGene is already optimal. Each popped gene expands into its 3L one-char edits, filtered through the bank set, so each bank gene is enqueued at most once and the search never leaves valid genes.
Try every sequence of distinct bank genes that steps one character at a time, and keep the shallowest depth that reaches endGene. A shortest mutation path never revisits a gene, so enumerating all simple paths is exhaustive-but-correct; pruning at the best depth found so far and the 10-gene bank cap keep the blowup tame.
Collapses Approach 1's factorial blowup: BFS visits genes in distance order, so the first arrival at endGene is already optimal. Each popped gene expands into its 3L one-char edits, filtered through the bank set, so each bank gene is enqueued at most once and the search never leaves valid genes.
def minMutation_dfs_backtracking(self, startGene: str, endGene: str,
bank: List[str]) -> int:
def one_apart(a: str, b: str) -> bool:
return sum(x != y for x, y in zip(a, b)) == 1
best = len(bank) + 1 # any real answer uses <= B mutations
used = [False] * len(bank)
def dfs(gene: str, depth: int) -> None:
nonlocal best
if depth >= best: # cannot improve on this branch
return
if gene == endGene:
best = depth
return
for i, candidate in enumerate(bank):
if not used[i] and one_apart(gene, candidate):
used[i] = True
dfs(candidate, depth + 1)
used[i] = False
dfs(startGene, 0)
return best if best <= len(bank) else -1
def minMutation_bank_graph(self, startGene: str, endGene: str,
bank: List[str]) -> int:
def one_apart(a: str, b: str) -> bool:
return sum(x != y for x, y in zip(a, b)) == 1
queue = deque([(startGene, 0)])
seen = {startGene}
while queue:
gene, steps = queue.popleft()
if gene == endGene:
return steps
for candidate in bank:
if candidate not in seen and one_apart(gene, candidate):
seen.add(candidate)
queue.append((candidate, steps + 1))
return -1
def minMutation_bidirectional(self, startGene: str, endGene: str,
bank: List[str]) -> int:
if startGene == endGene:
return 0
bank_set = set(bank)
if endGene not in bank_set:
return -1
frontier = {startGene}
other = {endGene}
visited = {startGene, endGene}
steps = 0
while frontier and other:
if len(frontier) > len(other): # always expand the smaller side
frontier, other = other, frontier
steps += 1
nxt_frontier = set()
for gene in frontier:
for i in range(len(gene)):
for ch in "ACGT":
if ch == gene[i]:
continue
mutated = gene[:i] + ch + gene[i + 1:]
if mutated in other: # the two searches met
return steps
if mutated in bank_set and mutated not in visited:
visited.add(mutated)
nxt_frontier.add(mutated)
frontier = nxt_frontier
return -1
def minMutation(self, startGene: str, endGene: str, bank: List[str]) -> int:
bank_set = set(bank)
queue = deque([(startGene, 0)])
seen = {startGene}
while queue:
gene, steps = queue.popleft()
if gene == endGene:
return steps
for i in range(len(gene)):
for ch in "ACGT":
if ch == gene[i]:
continue
mutated = gene[:i] + ch + gene[i + 1:]
if mutated in bank_set and mutated not in seen:
seen.add(mutated)
queue.append((mutated, steps + 1))
return -1
int minMutationDfsBacktracking(string startGene, string endGene,
vector<string>& bank) {
auto oneApart = [](const string& a, const string& b) {
int diff = 0;
for (size_t i = 0; i < a.size(); ++i) diff += a[i] != b[i];
return diff == 1;
};
const int B = static_cast<int>(bank.size());
int best = B + 1; // any real answer uses <= B mutations
vector<bool> used(bank.size(), false);
std::function<void(const string&, int)> dfs = [&](const string& gene,
int depth) {
if (depth >= best) return; // cannot improve on this branch
if (gene == endGene) {
best = depth;
return;
}
for (size_t i = 0; i < bank.size(); ++i) {
if (!used[i] && oneApart(gene, bank[i])) {
used[i] = true;
dfs(bank[i], depth + 1);
used[i] = false;
}
}
};
dfs(startGene, 0);
return best <= B ? best : -1;
}
int minMutation(string startGene, string endGene, vector<string>& bank) {
std::unordered_set<string> bankSet(bank.begin(), bank.end());
std::unordered_set<string> seen{startGene};
std::queue<std::pair<string, int>> pending;
pending.push({startGene, 0});
const string alphabet = "ACGT";
while (!pending.empty()) {
auto [gene, steps] = pending.front();
pending.pop();
if (gene == endGene) return steps;
for (size_t i = 0; i < gene.size(); ++i) {
char original = gene[i];
for (char ch : alphabet) {
if (ch == original) continue;
gene[i] = ch;
if (bankSet.count(gene) && !seen.count(gene)) {
seen.insert(gene);
pending.push({gene, steps + 1});
}
}
gene[i] = original;
}
}
return -1;
}
pub fn min_mutation_dfs_backtracking(
start_gene: String,
end_gene: String,
bank: Vec<String>,
) -> i32 {
fn one_apart(a: &[u8], b: &[u8]) -> bool {
a.iter().zip(b).filter(|(x, y)| x != y).count() == 1
}
fn dfs(
gene: &[u8],
depth: i32,
end: &[u8],
bank: &[Vec<u8>],
used: &mut [bool],
best: &mut i32,
) {
if depth >= *best {
return; // cannot improve on this branch
}
if gene == end {
*best = depth;
return;
}
for i in 0..bank.len() {
if !used[i] && one_apart(gene, &bank[i]) {
used[i] = true;
dfs(&bank[i], depth + 1, end, bank, used, best);
used[i] = false;
}
}
}
let bank: Vec<Vec<u8>> = bank.into_iter().map(String::into_bytes).collect();
let start = start_gene.into_bytes();
let end = end_gene.into_bytes();
let mut used = vec![false; bank.len()];
let mut best = bank.len() as i32 + 1; // any real answer uses <= B mutations
dfs(&start, 0, &end, &bank, &mut used, &mut best);
if best <= bank.len() as i32 {
best
} else {
-1
}
}
pub fn min_mutation(start_gene: String, end_gene: String, bank: Vec<String>) -> i32 {
let bank_set: HashSet<Vec<u8>> = bank.into_iter().map(String::into_bytes).collect();
let start = start_gene.into_bytes();
let end = end_gene.into_bytes();
let mut seen = HashSet::from([start.clone()]);
let mut queue = VecDeque::from([(start, 0)]);
while let Some((gene, steps)) = queue.pop_front() {
if gene == end {
return steps;
}
for i in 0..gene.len() {
for &ch in b"ACGT" {
if ch == gene[i] {
continue;
}
let mut mutated = gene.clone();
mutated[i] = ch;
if bank_set.contains(&mutated) && !seen.contains(&mutated) {
seen.insert(mutated.clone());
queue.push_back((mutated, steps + 1));
}
}
}
}
-1
}
function minMutationDfsBacktracking(startGene: string, endGene: string, bank: string[]): number {
const oneApart = (a: string, b: string): boolean => {
let diff = 0;
for (let i = 0; i < a.length; i++) {
if (a[i] !== b[i]) diff++;
}
return diff === 1;
};
let best = bank.length + 1; // any real answer uses <= B mutations
const used = new Array<boolean>(bank.length).fill(false);
const dfs = (gene: string, depth: number): void => {
if (depth >= best) return; // cannot improve on this branch
if (gene === endGene) {
best = depth;
return;
}
for (let i = 0; i < bank.length; i++) {
if (!used[i] && oneApart(gene, bank[i])) {
used[i] = true;
dfs(bank[i], depth + 1);
used[i] = false;
}
}
};
dfs(startGene, 0);
return best <= bank.length ? best : -1;
}
function minMutation(startGene: string, endGene: string, bank: string[]): number {
const bankSet = new Set(bank);
const alphabet = ['A', 'C', 'G', 'T'];
const seen = new Set<string>([startGene]);
const queue: Array<[string, number]> = [[startGene, 0]];
let head = 0;
while (head < queue.length) {
const [gene, steps] = queue[head++];
if (gene === endGene) return steps;
const chars = gene.split('');
for (let i = 0; i < chars.length; i++) {
const original = chars[i];
for (const ch of alphabet) {
if (ch === original) continue;
chars[i] = ch;
const mutated = chars.join('');
if (bankSet.has(mutated) && !seen.has(mutated)) {
seen.add(mutated);
queue.push([mutated, steps + 1]);
}
}
chars[i] = original;
}
}
return -1;
}
func minMutationDfsBacktracking(startGene string, endGene string, bank []string) int {
oneApart := func(a, b string) bool {
diff := 0
for i := 0; i < len(a); i++ {
if a[i] != b[i] {
diff++
}
}
return diff == 1
}
best := len(bank) + 1 // any real answer uses <= B mutations
used := make([]bool, len(bank))
var dfs func(gene string, depth int)
dfs = func(gene string, depth int) {
if depth >= best { // cannot improve on this branch
return
}
if gene == endGene {
best = depth
return
}
for i, candidate := range bank {
if !used[i] && oneApart(gene, candidate) {
used[i] = true
dfs(candidate, depth+1)
used[i] = false
}
}
}
dfs(startGene, 0)
if best <= len(bank) {
return best
}
return -1
}
func minMutation(startGene string, endGene string, bank []string) int {
bankSet := make(map[string]bool, len(bank))
for _, gene := range bank {
bankSet[gene] = true
}
type item struct {
gene string
steps int
}
seen := map[string]bool{startGene: true}
queue := []item{{startGene, 0}}
alphabet := []byte{'A', 'C', 'G', 'T'}
for len(queue) > 0 {
cur := queue[0]
queue = queue[1:]
if cur.gene == endGene {
return cur.steps
}
bytes := []byte(cur.gene)
for i, original := range bytes {
for _, ch := range alphabet {
if ch == original {
continue
}
bytes[i] = ch
mutated := string(bytes)
if bankSet[mutated] && !seen[mutated] {
seen[mutated] = true
queue = append(queue, item{mutated, cur.steps + 1})
}
}
bytes[i] = original
}
}
return -1
}
func minMutationDfsBacktracking(_ startGene: String, _ endGene: String,
_ bank: [String]) -> Int {
func oneApart(_ a: [Character], _ b: [Character]) -> Bool {
var diff = 0
for i in 0..<a.count where a[i] != b[i] {
diff += 1
}
return diff == 1
}
let bankChars = bank.map { Array($0) }
let endChars = Array(endGene)
var best = bank.count + 1 // any real answer uses <= B mutations
var used = [Bool](repeating: false, count: bank.count)
func dfs(_ gene: [Character], _ depth: Int) {
if depth >= best { return } // cannot improve on this branch
if gene == endChars {
best = depth
return
}
for i in 0..<bankChars.count where !used[i] && oneApart(gene, bankChars[i]) {
used[i] = true
dfs(bankChars[i], depth + 1)
used[i] = false
}
}
dfs(Array(startGene), 0)
return best <= bank.count ? best : -1
}
func minMutation(_ startGene: String, _ endGene: String, _ bank: [String]) -> Int {
let bankSet = Set(bank)
let alphabet: [Character] = ["A", "C", "G", "T"]
var seen: Set<String> = [startGene]
var queue: [(gene: String, steps: Int)] = [(startGene, 0)]
var head = 0
while head < queue.count {
let (gene, steps) = queue[head]
head += 1
if gene == endGene { return steps }
var chars = Array(gene)
for i in 0..<chars.count {
let original = chars[i]
for ch in alphabet where ch != original {
chars[i] = ch
let mutated = String(chars)
if bankSet.contains(mutated) && !seen.contains(mutated) {
seen.insert(mutated)
queue.append((mutated, steps + 1))
}
}
chars[i] = original
}
}
return -1
}
Divide and Conquer
36. Convert Sorted Array to Binary Search Tree
Easy · LC 108
Given an integer array sorted in ascending order, convert it into a height-balanced binary search tree. Recurse on index ranges: make the middle element of the range the root, then build the left subtree from the left half and the right subtree from the right half. The trick is that picking the middle splits every range as evenly as possible, so subtree sizes differ by at most one at each node and balance falls out for free, while the array being exactly the tree's inorder traversal guarantees the ordering property.
The version you write when slicing feels free: root = middle, halves = nums[:mid] and nums[mid+1:]. Correct and height-balanced, but every level of the tree copies the whole array again.
Drops Approach 1's copying by describing each half as an index range instead of a new list, and replaces recursion with an explicit stack of placeholder nodes filled as their range is popped — useful when stack depth is a worry.
Removes the need for random access entirely: recurse on sizes only and consume values with a running cursor during an inorder walk — the trick that generalizes to sorted linked lists (LC 109) where indexing is not available.
When the input really is an array, none of the machinery above is needed: root = middle of [lo, hi], then build each half the same way. Same bounds as Approaches 2-3 with the least code — the balanced split caps recursion depth at ~log2(n) frames (about 14 at the n <= 10^4 constraint), so recursion is safe here.
The version you write when slicing feels free: root = middle, halves = fresh vectors. Correct and height-balanced, but every level of the tree copies the whole array again.
Drops Approach 1's copying by describing each half as an index range [lo, hi] into the original array: the middle of the range becomes the root and each half is built the same way.
The version you write when slicing feels free: root = middle, halves = freshly allocated Vecs. Correct and height-balanced, but every level of the tree copies the whole array again.
Drops Approach 1's copying: a borrowed &[i32] names each half without allocating, so splitting is free — Rust's zero-cost equivalent of recursing on index ranges.
The version you write when slicing feels free: root = middle, halves = nums.slice(...) copies. Correct and height-balanced, but every level of the tree copies the whole array again.
Drops Approach 1's copying by describing each half as an index range [lo, hi] into the original array: the middle of the range becomes the root and each half is built the same way.
The version you write when slicing feels free: root = middle, halves = fresh slices (copied here, since Go subslices would share the backing array anyway). Correct and height-balanced, but every level of the tree copies the whole array again.
Drops Approach 1's copying by describing each half as an index range [lo, hi] into the original array: the middle of the range becomes the root and each half is built the same way.
The version you write when slicing feels free: root = middle, halves = fresh Arrays materialized from the slices. Correct and height-balanced, but every level of the tree copies the whole array again.
Drops Approach 1's copying by describing each half as an index range [lo, hi] into the original array: the middle of the range becomes the root and each half is built the same way.
def sortedArrayToBST_slices(self, nums: List[int]) -> Optional[TreeNode]:
if not nums:
return None
mid = len(nums) // 2
node = TreeNode(nums[mid])
node.left = self.sortedArrayToBST_slices(nums[:mid])
node.right = self.sortedArrayToBST_slices(nums[mid + 1:])
return node
def sortedArrayToBST_iterative(self, nums: List[int]) -> Optional[TreeNode]:
if not nums:
return None
root = TreeNode()
stack = [(root, 0, len(nums) - 1)]
while stack:
node, lo, hi = stack.pop()
mid = (lo + hi) // 2
node.val = nums[mid]
if lo < mid:
node.left = TreeNode()
stack.append((node.left, lo, mid - 1))
if mid < hi:
node.right = TreeNode()
stack.append((node.right, mid + 1, hi))
return root
def sortedArrayToBST_inorder_sim(self, nums: List[int]) -> Optional[TreeNode]:
cursor = 0
def build(lo: int, hi: int) -> Optional[TreeNode]:
nonlocal cursor
if lo > hi:
return None
mid = (lo + hi) // 2
left = build(lo, mid - 1)
node = TreeNode(nums[cursor])
cursor += 1
node.left = left
node.right = build(mid + 1, hi)
return node
return build(0, len(nums) - 1)
def sortedArrayToBST(self, nums: List[int]) -> Optional[TreeNode]:
def build(lo: int, hi: int) -> Optional[TreeNode]:
if lo > hi:
return None
mid = (lo + hi) // 2
node = TreeNode(nums[mid])
node.left = build(lo, mid - 1)
node.right = build(mid + 1, hi)
return node
return build(0, len(nums) - 1)
TreeNode* sortedArrayToBSTSlices(vector<int>& nums) {
if (nums.empty()) return nullptr;
size_t mid = nums.size() / 2;
TreeNode* node = new TreeNode(nums[mid]);
vector<int> left(nums.begin(), nums.begin() + mid);
vector<int> right(nums.begin() + mid + 1, nums.end());
node->left = sortedArrayToBSTSlices(left);
node->right = sortedArrayToBSTSlices(right);
return node;
}
TreeNode* sortedArrayToBST(vector<int>& nums) {
return build(nums, 0, static_cast<int>(nums.size()) - 1);
}
private:
TreeNode* build(const vector<int>& nums, int lo, int hi) {
if (lo > hi) return nullptr;
int mid = lo + (hi - lo) / 2;
TreeNode* node = new TreeNode(nums[mid]);
node->left = build(nums, lo, mid - 1);
node->right = build(nums, mid + 1, hi);
return node;
}
pub fn sorted_array_to_bst_slices(nums: Vec<i32>) -> Option<Rc<RefCell<TreeNode>>> {
if nums.is_empty() {
return None;
}
let mid = nums.len() / 2;
let node = Rc::new(RefCell::new(TreeNode::new(nums[mid])));
node.borrow_mut().left = Self::sorted_array_to_bst_slices(nums[..mid].to_vec());
node.borrow_mut().right = Self::sorted_array_to_bst_slices(nums[mid + 1..].to_vec());
Some(node)
}
pub fn sorted_array_to_bst(nums: Vec<i32>) -> Option<Rc<RefCell<TreeNode>>> {
fn build(nums: &[i32]) -> Option<Rc<RefCell<TreeNode>>> {
if nums.is_empty() {
return None;
}
let mid = nums.len() / 2;
let node = Rc::new(RefCell::new(TreeNode::new(nums[mid])));
node.borrow_mut().left = build(&nums[..mid]);
node.borrow_mut().right = build(&nums[mid + 1..]);
Some(node)
}
build(&nums)
}
function sortedArrayToBSTSlices(nums: number[]): TreeNode | null {
if (nums.length === 0) return null;
const mid = Math.floor(nums.length / 2);
return new TreeNode(
nums[mid],
sortedArrayToBSTSlices(nums.slice(0, mid)),
sortedArrayToBSTSlices(nums.slice(mid + 1)),
);
}
function sortedArrayToBST(nums: number[]): TreeNode | null {
const build = (lo: number, hi: number): TreeNode | null => {
if (lo > hi) return null;
const mid = lo + Math.floor((hi - lo) / 2);
return new TreeNode(nums[mid], build(lo, mid - 1), build(mid + 1, hi));
};
return build(0, nums.length - 1);
}
func sortedArrayToBSTSlices(nums []int) *TreeNode {
if len(nums) == 0 {
return nil
}
mid := len(nums) / 2
left := append([]int(nil), nums[:mid]...)
right := append([]int(nil), nums[mid+1:]...)
return &TreeNode{
Val: nums[mid],
Left: sortedArrayToBSTSlices(left),
Right: sortedArrayToBSTSlices(right),
}
}
func sortedArrayToBST(nums []int) *TreeNode {
var build func(lo, hi int) *TreeNode
build = func(lo, hi int) *TreeNode {
if lo > hi {
return nil
}
mid := lo + (hi-lo)/2
return &TreeNode{
Val: nums[mid],
Left: build(lo, mid-1),
Right: build(mid+1, hi),
}
}
return build(0, len(nums)-1)
}
func sortedArrayToBSTSlices(_ nums: [Int]) -> TreeNode? {
if nums.isEmpty { return nil }
let mid = nums.count / 2
let node = TreeNode(nums[mid])
node.left = sortedArrayToBSTSlices(Array(nums[..<mid]))
node.right = sortedArrayToBSTSlices(Array(nums[(mid + 1)...]))
return node
}
func sortedArrayToBST(_ nums: [Int]) -> TreeNode? {
func build(_ lo: Int, _ hi: Int) -> TreeNode? {
if lo > hi { return nil }
let mid = lo + (hi - lo) / 2
return TreeNode(nums[mid], build(lo, mid - 1), build(mid + 1, hi))
}
return build(0, nums.count - 1)
}
37. Sort List
Medium · LC 148
Given the head of a linked list, sort it in ascending order. Bottom-up merge sort does it in passes: count the length once, then repeatedly split off runs of width one, two, four and so on, merging each adjacent pair back in place behind a dummy node. The trick is that iterating over doubling widths needs no recursion at all, turning the usual O(log n) call stack of top-down merge sort into the true O(1) extra space the follow-up asks for.
Copy values out, sort() them, write them back. Beats everything in wall-clock Python (Timsort in C) but uses linear extra memory and dodges the linked-list manipulation the problem is really about.
Split at the middle with slow/fast pointers (fast starts one ahead so a 2-node list splits 1+1), sort each half, merge. Real list surgery with only logarithmic extra space — the classic answer, but the recursion stack still isn't the O(1) the follow-up wants.
Merge runs of width 1, 2, 4, ... in passes over the list. Truly constant extra space — the full answer to the follow-up ("O(n log n) time, O(1) memory?"), with no recursion stack at all.
Copy values into a vector, std::sort, write them back. Simple and fast in practice, but it uses linear extra memory and dodges the linked-list manipulation the problem is really about.
Split at the middle with slow/fast pointers (fast starts one ahead so a 2-node list splits 1+1), sort each half, merge. Real list surgery: the value buffer of Approach 1 shrinks to a logarithmic recursion stack.
Copy values into a Vec, sort it, write the values back. Simple and fast in practice, but it uses linear extra memory and dodges the linked-list manipulation the problem is really about.
Count the length once, then recursively split after n/2 nodes (the ownership-friendly equivalent of the slow/fast split), sort each half, and merge. Real list surgery: the value buffer of Approach 1 shrinks to a logarithmic recursion stack.
Copy values into an array, sort it numerically, write them back. Simple and fast in practice, but it uses linear extra memory and dodges the linked-list manipulation the problem is really about.
Split at the middle with slow/fast pointers (fast starts one ahead so a 2-node list splits 1+1), sort each half, merge. Real list surgery: the value array of Approach 1 shrinks to a logarithmic recursion stack.
Copy values into a slice, sort.Ints, write them back. Simple and fast in practice, but it uses linear extra memory and dodges the linked-list manipulation the problem is really about.
Split at the middle with slow/fast pointers (fast starts one ahead so a 2-node list splits 1+1), sort each half, merge. Real list surgery: the value slice of Approach 1 shrinks to a logarithmic recursion stack.
Copy values into an array, sort it, write the values back. Simple and fast in practice, but it uses linear extra memory and dodges the linked-list manipulation the problem is really about.
Split at the middle with slow/fast pointers (fast starts one ahead so a 2-node list splits 1+1), sort each half, merge. Real list surgery: the value array of Approach 1 shrinks to a logarithmic recursion stack.
def sortList_values(self, head: Optional[ListNode]) -> Optional[ListNode]:
vals = []
node = head
while node:
vals.append(node.val)
node = node.next
vals.sort()
node = head
for v in vals:
node.val = v
node = node.next
return head
def sortList_top_down(self, head: Optional[ListNode]) -> Optional[ListNode]:
if not head or not head.next:
return head
slow, fast = head, head.next
while fast and fast.next:
slow = slow.next
fast = fast.next.next
mid = slow.next
slow.next = None # cut the list in two
return self._merge(self.sortList_top_down(head), self.sortList_top_down(mid))
def sortList(self, head: Optional[ListNode]) -> Optional[ListNode]:
n = 0
node = head
while node:
n += 1
node = node.next
dummy = ListNode(0, head)
width = 1
while width < n:
prev, cur = dummy, dummy.next
while cur:
left = cur
right = self._split(left, width)
cur = self._split(right, width)
prev.next = self._merge(left, right)
while prev.next: # advance to the merged tail
prev = prev.next
width *= 2
return dummy.next
# -- shared helpers ------------------------------------------------
def _split(self, head, count):
"""Detach the first `count` nodes; return the head of the remainder."""
if not head:
return None
for _ in range(count - 1):
if not head.next:
break
head = head.next
rest = head.next
head.next = None
return rest
def _merge(self, a, b):
"""Merge two sorted lists; stable (ties keep `a`'s node first)."""
dummy = tail = ListNode()
while a and b:
if a.val <= b.val:
tail.next, a = a, a.next
else:
tail.next, b = b, b.next
tail = tail.next
tail.next = a or b
return dummy.next
ListNode* sortListValues(ListNode* head) {
std::vector<int> vals;
for (ListNode* node = head; node; node = node->next)
vals.push_back(node->val);
std::sort(vals.begin(), vals.end());
size_t i = 0;
for (ListNode* node = head; node; node = node->next)
node->val = vals[i++];
return head;
}
ListNode* sortList(ListNode* head) {
if (!head || !head->next) return head;
ListNode* slow = head;
ListNode* fast = head->next;
while (fast && fast->next) {
slow = slow->next;
fast = fast->next->next;
}
ListNode* mid = slow->next;
slow->next = nullptr; // cut the list in two
return merge(sortList(head), sortList(mid));
}
private:
// Merge two sorted lists; stable (ties keep `a`'s node first).
ListNode* merge(ListNode* a, ListNode* b) {
ListNode dummy;
ListNode* tail = &dummy;
while (a && b) {
if (a->val <= b->val) {
tail->next = a;
a = a->next;
} else {
tail->next = b;
b = b->next;
}
tail = tail->next;
}
tail->next = a ? a : b;
return dummy.next;
}
pub fn sort_list_values(head: Option<Box<ListNode>>) -> Option<Box<ListNode>> {
let mut vals = Vec::new();
let mut p = head.as_ref();
while let Some(node) = p {
vals.push(node.val);
p = node.next.as_ref();
}
vals.sort_unstable();
let mut head = head;
let mut p = head.as_mut();
for v in vals {
let node = p.unwrap();
node.val = v;
p = node.next.as_mut();
}
head
}
pub fn sort_list(head: Option<Box<ListNode>>) -> Option<Box<ListNode>> {
let mut n = 0usize;
let mut p = head.as_ref();
while let Some(node) = p {
n += 1;
p = node.next.as_ref();
}
Self::merge_sort(head, n)
}
// Sort the first n nodes (the whole list) recursively.
fn merge_sort(head: Option<Box<ListNode>>, n: usize) -> Option<Box<ListNode>> {
if n <= 1 {
return head;
}
// Cut after n/2 nodes.
let mut head = head;
let mut cut = head.as_mut().unwrap();
for _ in 0..n / 2 - 1 {
cut = cut.next.as_mut().unwrap();
}
let right = cut.next.take();
let left = Self::merge_sort(head, n / 2);
let right = Self::merge_sort(right, n - n / 2);
Self::merge(left, right)
}
// Merge two sorted lists; stable (ties keep `a`'s node first).
fn merge(mut a: Option<Box<ListNode>>, mut b: Option<Box<ListNode>>) -> Option<Box<ListNode>> {
let mut dummy = Box::new(ListNode::new(0));
let mut tail = &mut dummy;
loop {
match (a.take(), b.take()) {
(Some(mut x), Some(mut y)) => {
if x.val <= y.val {
a = x.next.take();
b = Some(y);
tail.next = Some(x);
} else {
b = y.next.take();
a = Some(x);
tail.next = Some(y);
}
tail = tail.next.as_mut().unwrap();
}
(Some(x), None) => {
tail.next = Some(x);
break;
}
(None, Some(y)) => {
tail.next = Some(y);
break;
}
(None, None) => break,
}
}
dummy.next
}
function sortListValues(head: ListNode | null): ListNode | null {
const vals: number[] = [];
for (let node = head; node !== null; node = node.next) vals.push(node.val);
vals.sort((a, b) => a - b);
let i = 0;
for (let node = head; node !== null; node = node.next) node.val = vals[i++];
return head;
}
function sortList(head: ListNode | null): ListNode | null {
if (head === null || head.next === null) return head;
let slow = head;
let fast: ListNode | null = head.next;
while (fast !== null && fast.next !== null) {
slow = slow.next!;
fast = fast.next.next;
}
const mid = slow.next;
slow.next = null; // cut the list in two
return merge(sortList(head), sortList(mid));
}
// Merge two sorted lists; stable (ties keep `a`'s node first).
function merge(a: ListNode | null, b: ListNode | null): ListNode | null {
const dummy = new ListNode();
let tail = dummy;
while (a !== null && b !== null) {
if (a.val <= b.val) {
tail.next = a;
a = a.next;
} else {
tail.next = b;
b = b.next;
}
tail = tail.next;
}
tail.next = a !== null ? a : b;
return dummy.next;
}
func sortListValues(head *ListNode) *ListNode {
vals := []int{}
for node := head; node != nil; node = node.Next {
vals = append(vals, node.Val)
}
sort.Ints(vals)
i := 0
for node := head; node != nil; node = node.Next {
node.Val = vals[i]
i++
}
return head
}
func sortList(head *ListNode) *ListNode {
if head == nil || head.Next == nil {
return head
}
slow, fast := head, head.Next
for fast != nil && fast.Next != nil {
slow = slow.Next
fast = fast.Next.Next
}
mid := slow.Next
slow.Next = nil // cut the list in two
return merge(sortList(head), sortList(mid))
}
// merge joins two sorted lists; stable (ties keep a's node first).
func merge(a, b *ListNode) *ListNode {
dummy := &ListNode{}
tail := dummy
for a != nil && b != nil {
if a.Val <= b.Val {
tail.Next = a
a = a.Next
} else {
tail.Next = b
b = b.Next
}
tail = tail.Next
}
if a != nil {
tail.Next = a
} else {
tail.Next = b
}
return dummy.Next
}
func sortListValues(_ head: ListNode?) -> ListNode? {
var vals: [Int] = []
var node = head
while let n = node {
vals.append(n.val)
node = n.next
}
vals.sort()
node = head
for v in vals {
node!.val = v
node = node!.next
}
return head
}
func sortList(_ head: ListNode?) -> ListNode? {
guard let head = head, head.next != nil else { return head }
var slow = head
var fast = head.next
while let f = fast, let fn = f.next {
slow = slow.next!
fast = fn.next
}
let mid = slow.next
slow.next = nil // cut the list in two
return merge(sortList(head), sortList(mid))
}
// Merge two sorted lists; stable (ties keep `a`'s node first).
private func merge(_ a: ListNode?, _ b: ListNode?) -> ListNode? {
var a = a
var b = b
let dummy = ListNode()
var tail = dummy
while let x = a, let y = b {
if x.val <= y.val {
tail.next = x
a = x.next
} else {
tail.next = y
b = y.next
}
tail = tail.next!
}
tail.next = a ?? b
return dummy.next
}
Binary Search
38. Find Peak Element
Medium · LC 162
Given an array where no two adjacent elements are equal, return the index of any peak, an element strictly greater than both neighbors, with the borders treated as negative infinity. Binary search works even though the array is unsorted: if the middle element is smaller than its right neighbor the slope rises rightward and a peak must exist on that side, otherwise a peak lies at the middle or to its left, so half the range is discarded each step until one index survives. The subtlety is the asymmetric shrink, moving the low bound past the middle but the high bound only onto it, because the middle itself may be the peak.
The first index with nums[i] > nums[i + 1] is a peak: everything before it was strictly rising, so nums[i] also beats its left neighbor. A fully rising array peaks at the last index. Correct on every input, but ignores the O(log n) demand in the constraints.
Cuts Approach 1's O(n) walk to O(log n): comparing nums[mid] with nums[mid + 1] tells which half must contain a peak, so half the array is discarded per step — the recurrence the editorial presents first.
Same decision rule as Approach 2 with the recursion flattened into a loop, dropping the O(log n) call stack; when lo == hi the survivor is a peak.
The first index with nums[i] > nums[i + 1] is a peak: everything before it was strictly rising, so nums[i] also beats its left neighbor. A fully rising array peaks at the last index. Correct on every input, but ignores the O(log n) demand in the constraints.
Cuts Approach 1's O(n) walk to O(log n): if nums[mid] < nums[mid+1] the values rise to the right and a peak must exist on that side (the virtual -inf wall at each end stops the climb); otherwise mid itself may be the peak, so keep it.
The first index with nums[i] > nums[i + 1] is a peak: everything before it was strictly rising, so nums[i] also beats its left neighbor. A fully rising array peaks at the last index. Correct on every input, but ignores the O(log n) demand in the constraints.
Cuts Approach 1's O(n) walk to O(log n): if nums[mid] < nums[mid+1] the values rise to the right and a peak must exist on that side (the virtual -inf wall at each end stops the climb); otherwise mid itself may be the peak, so keep it.
The first index with nums[i] > nums[i + 1] is a peak: everything before it was strictly rising, so nums[i] also beats its left neighbor. A fully rising array peaks at the last index. Correct on every input, but ignores the O(log n) demand in the constraints.
Cuts Approach 1's O(n) walk to O(log n): if nums[mid] < nums[mid+1] the values rise to the right and a peak must exist on that side (the virtual -inf wall at each end stops the climb); otherwise mid itself may be the peak, so keep it.
The first index with nums[i] > nums[i+1] is a peak: everything before it was strictly rising, so nums[i] also beats its left neighbor. A fully rising array peaks at the last index. Correct on every input, but ignores the O(log n) demand in the constraints.
Cuts Approach 1's O(n) walk to O(log n): if nums[mid] < nums[mid+1] the values rise to the right and a peak must exist on that side (the virtual -inf wall at each end stops the climb); otherwise mid itself may be the peak, so keep it.
The first index with nums[i] > nums[i + 1] is a peak: everything before it was strictly rising, so nums[i] also beats its left neighbor. A fully rising array peaks at the last index. Correct on every input, but ignores the O(log n) demand in the constraints.
Cuts Approach 1's O(n) walk to O(log n): if nums[mid] < nums[mid+1] the values rise to the right and a peak must exist on that side (the virtual -inf wall at each end stops the climb); otherwise mid itself may be the peak, so keep it.
def findPeakElement_linear(self, nums: List[int]) -> int:
for i in range(len(nums) - 1):
if nums[i] > nums[i + 1]:
return i
return len(nums) - 1
def findPeakElement_recursive(self, nums: List[int]) -> int:
def search(lo: int, hi: int) -> int:
if lo == hi:
return lo
mid = (lo + hi) // 2
if nums[mid] < nums[mid + 1]:
return search(mid + 1, hi)
return search(lo, mid)
return search(0, len(nums) - 1)
def findPeakElement(self, nums: List[int]) -> int:
lo, hi = 0, len(nums) - 1
while lo < hi:
mid = (lo + hi) // 2
if nums[mid] < nums[mid + 1]:
lo = mid + 1 # peak lies strictly right of mid
else:
hi = mid # mid could itself be the peak
return lo
int findPeakElementLinearScan(vector<int>& nums) {
const int n = static_cast<int>(nums.size());
for (int i = 0; i + 1 < n; ++i) {
if (nums[i] > nums[i + 1]) return i;
}
return n - 1;
}
int findPeakElement(vector<int>& nums) {
int lo = 0, hi = static_cast<int>(nums.size()) - 1;
while (lo < hi) {
int mid = lo + (hi - lo) / 2;
if (nums[mid] < nums[mid + 1]) {
lo = mid + 1; // peak lies strictly right of mid
} else {
hi = mid; // mid could itself be the peak
}
}
return lo;
}
pub fn find_peak_element_linear_scan(nums: Vec<i32>) -> i32 {
for i in 0..nums.len() - 1 {
if nums[i] > nums[i + 1] {
return i as i32;
}
}
nums.len() as i32 - 1
}
pub fn find_peak_element(nums: Vec<i32>) -> i32 {
let (mut lo, mut hi) = (0, nums.len() - 1);
while lo < hi {
let mid = lo + (hi - lo) / 2;
if nums[mid] < nums[mid + 1] {
lo = mid + 1; // peak lies strictly right of mid
} else {
hi = mid; // mid could itself be the peak
}
}
lo as i32
}
function findPeakElementLinearScan(nums: number[]): number {
for (let i = 0; i + 1 < nums.length; i++) {
if (nums[i] > nums[i + 1]) return i;
}
return nums.length - 1;
}
function findPeakElement(nums: number[]): number {
let lo = 0;
let hi = nums.length - 1;
while (lo < hi) {
const mid = lo + Math.floor((hi - lo) / 2);
if (nums[mid] < nums[mid + 1]) {
lo = mid + 1; // peak lies strictly right of mid
} else {
hi = mid; // mid could itself be the peak
}
}
return lo;
}
func findPeakElementLinearScan(nums []int) int {
for i := 0; i+1 < len(nums); i++ {
if nums[i] > nums[i+1] {
return i
}
}
return len(nums) - 1
}
func findPeakElement(nums []int) int {
lo, hi := 0, len(nums)-1
for lo < hi {
mid := lo + (hi-lo)/2
if nums[mid] < nums[mid+1] {
lo = mid + 1 // peak lies strictly right of mid
} else {
hi = mid // mid could itself be the peak
}
}
return lo
}
func findPeakElementLinearScan(_ nums: [Int]) -> Int {
for i in 0..<(nums.count - 1) where nums[i] > nums[i + 1] {
return i
}
return nums.count - 1
}
func findPeakElement(_ nums: [Int]) -> Int {
var lo = 0
var hi = nums.count - 1
while lo < hi {
let mid = lo + (hi - lo) / 2
if nums[mid] < nums[mid + 1] {
lo = mid + 1 // peak lies strictly right of mid
} else {
hi = mid // mid could itself be the peak
}
}
return lo
}
39. Find First and Last Position of Element in Sorted Array
Medium · LC 34
Given a sorted array and a target, return the first and last index at which the target appears, or a pair of negative ones, in logarithmic time. One lower-bound binary search, returning the first index whose value is at least the query, answers both questions: called on the target it finds the first occurrence, and called on the target plus one it lands just past the last. The trick is verifying that the first result actually holds the target before trusting it, since a lower bound happily returns an insertion point when the value is absent.
Ignores the sortedness entirely: note the first and last index seen holding the target. Trivially correct, but misses the required O(log n) bound.
bisect_left/bisect_right ARE lower/upper bound; outside an interview reach for these instead of hand-rolling the loop.
The loop bisect hides, written out: lower_bound(target) is the first index holding target (if any), and lower_bound(target + 1) - 1 is the last. One helper, two calls.
Ignores the sortedness entirely: note the first and last index seen holding the target. Trivially correct, but misses the required O(log n) bound.
lowerBound(target) is the first index holding target (if any), and lowerBound(target + 1) - 1 is the last. One helper, two calls.
Ignores the sortedness entirely: note the first and last index seen holding the target. Trivially correct, but misses the required O(log n) bound.
lower_bound(target) is the first index holding target (if any), and lower_bound(target + 1) - 1 is the last. One helper, two calls.
Ignores the sortedness entirely: note the first and last index seen holding the target. Trivially correct, but misses the required O(log n) bound.
lowerBound(target) is the first index holding target (if any), and lowerBound(target + 1) - 1 is the last. One helper, two calls.
Ignores the sortedness entirely: note the first and last index seen holding the target. Trivially correct, but misses the required O(log n) bound.
lowerBound(target) is the first index holding target (if any), and lowerBound(target+1)-1 is the last. One helper, two calls.
Ignores the sortedness entirely: note the first and last index seen holding the target. Trivially correct, but misses the required O(log n) bound.
lowerBound(target) is the first index holding target (if any), and lowerBound(target + 1) - 1 is the last. One helper, two calls.
def searchRange_linear(self, nums: list[int], target: int) -> list[int]:
first = last = -1
for i, val in enumerate(nums):
if val == target:
if first == -1:
first = i
last = i
return [first, last]
def searchRange_bisect(self, nums: list[int], target: int) -> list[int]:
first = bisect_left(nums, target)
if first == len(nums) or nums[first] != target:
return [-1, -1]
return [first, bisect_right(nums, target) - 1]
def searchRange(self, nums: list[int], target: int) -> list[int]:
first = self._lower_bound(nums, target)
if first == len(nums) or nums[first] != target:
return [-1, -1]
last = self._lower_bound(nums, target + 1) - 1
return [first, last]
@staticmethod
def _lower_bound(nums: list[int], target: int) -> int:
"""First index i with nums[i] >= target (len(nums) if none)."""
lo, hi = 0, len(nums)
while lo < hi:
mid = (lo + hi) // 2
if nums[mid] < target:
lo = mid + 1
else:
hi = mid
return lo
vector<int> searchRangeLinear(vector<int>& nums, int target) {
int first = -1, last = -1;
for (int i = 0; i < (int)nums.size(); ++i) {
if (nums[i] == target) {
if (first == -1)
first = i;
last = i;
}
}
return {first, last};
}
vector<int> searchRange(vector<int>& nums, int target) {
int first = lowerBound(nums, target);
if (first == (int)nums.size() || nums[first] != target)
return {-1, -1};
int last = lowerBound(nums, target + 1) - 1;
return {first, last};
}
private:
// First index i with nums[i] >= target (nums.size() if none).
static int lowerBound(const vector<int>& nums, int target) {
int lo = 0, hi = (int)nums.size();
while (lo < hi) {
int mid = lo + (hi - lo) / 2;
if (nums[mid] < target)
lo = mid + 1;
else
hi = mid;
}
return lo;
}
pub fn search_range_linear(nums: Vec<i32>, target: i32) -> Vec<i32> {
let mut first = -1i32;
let mut last = -1i32;
for (i, &v) in nums.iter().enumerate() {
if v == target {
if first == -1 {
first = i as i32;
}
last = i as i32;
}
}
vec![first, last]
}
pub fn search_range(nums: Vec<i32>, target: i32) -> Vec<i32> {
let first = Self::lower_bound(&nums, target as i64);
if first == nums.len() || nums[first] != target {
return vec![-1, -1];
}
let last = Self::lower_bound(&nums, target as i64 + 1) - 1;
vec![first as i32, last as i32]
}
/// First index i with nums[i] >= target (nums.len() if none).
fn lower_bound(nums: &[i32], target: i64) -> usize {
let (mut lo, mut hi) = (0usize, nums.len());
while lo < hi {
let mid = lo + (hi - lo) / 2;
if (nums[mid] as i64) < target {
lo = mid + 1;
} else {
hi = mid;
}
}
lo
}
function searchRangeLinear(nums: number[], target: number): number[] {
let first = -1;
let last = -1;
for (let i = 0; i < nums.length; i++) {
if (nums[i] === target) {
if (first === -1) {
first = i;
}
last = i;
}
}
return [first, last];
}
function searchRange(nums: number[], target: number): number[] {
// First index i with nums[i] >= t (nums.length if none).
const lowerBound = (t: number): number => {
let lo = 0;
let hi = nums.length;
while (lo < hi) {
const mid = (lo + hi) >>> 1;
if (nums[mid] < t) {
lo = mid + 1;
} else {
hi = mid;
}
}
return lo;
};
const first = lowerBound(target);
if (first === nums.length || nums[first] !== target) {
return [-1, -1];
}
return [first, lowerBound(target + 1) - 1];
}
func searchRangeLinear(nums []int, target int) []int {
first, last := -1, -1
for i, v := range nums {
if v == target {
if first == -1 {
first = i
}
last = i
}
}
return []int{first, last}
}
func searchRange(nums []int, target int) []int {
first := lowerBound(nums, target)
if first == len(nums) || nums[first] != target {
return []int{-1, -1}
}
last := lowerBound(nums, target+1) - 1
return []int{first, last}
}
// lowerBound returns the first index i with nums[i] >= target (len(nums) if none).
func lowerBound(nums []int, target int) int {
lo, hi := 0, len(nums)
for lo < hi {
mid := (lo + hi) / 2
if nums[mid] < target {
lo = mid + 1
} else {
hi = mid
}
}
return lo
}
func searchRangeLinear(_ nums: [Int], _ target: Int) -> [Int] {
var first = -1
var last = -1
for (i, v) in nums.enumerated() where v == target {
if first == -1 {
first = i
}
last = i
}
return [first, last]
}
func searchRange(_ nums: [Int], _ target: Int) -> [Int] {
let first = lowerBound(nums, target)
if first == nums.count || nums[first] != target {
return [-1, -1]
}
return [first, lowerBound(nums, target + 1) - 1]
}
// First index i with nums[i] >= target (nums.count if none).
private func lowerBound(_ nums: [Int], _ target: Int) -> Int {
var lo = 0
var hi = nums.count
while lo < hi {
let mid = (lo + hi) / 2
if nums[mid] < target {
lo = mid + 1
} else {
hi = mid
}
}
return lo
}
Heap
40. Find K Pairs with Smallest Sums
Medium · LC 373
Given two sorted arrays and an integer k, return the k pairs taking one element from each array with the smallest sums. Picture the sums as a matrix whose rows and columns are sorted, and run a min-heap over its frontier: seed it with the first cell of at most the first k rows, then each time a pair is popped push only its right neighbor from the same row. The insight is that the heap never needs more than one cell per row, so when k is small only a few rows are ever seeded, beating a k-way merge that would prime every row up front.
nsmallest keeps a k-sized heap while scanning every pair. Ignores the sortedness entirely — fine at the m*n <= 30 sizes here, hopeless at LeetCode's m = n = 1e5 (1e10 pairs).
First use of the sortedness: row i of the implicit matrix nums1[i] + nums2[j] is a sorted stream, so merge the m streams and slice off the first k. Elegant, but seeds all m rows even when k is tiny.
Seed the heap with the first cell of the first min(k, m) rows only; each pop of (i, j) exposes its right neighbour (i, j + 1). The heap never holds more than one frontier cell per row, which drops the seeding cost of the k-way merge when k << m.
Ignores the sortedness entirely: materialize every pair, sort by sum, keep the first k. Fine at the m*n <= 30 sizes here, hopeless at LeetCode's m = n = 1e5 (1e10 pairs).
Row i of the implicit matrix nums1[i] + nums2[j] is sorted, so seed the heap with (i, 0) for the first min(k, m) rows and push (i, j + 1) after popping (i, j). Touches only O(k) cells instead of all m*n.
Ignores the sortedness entirely: materialize every pair, sort by sum, keep the first k. Fine at the m*n <= 30 sizes here, hopeless at LeetCode's m = n = 1e5 (1e10 pairs).
Row i of the implicit matrix nums1[i] + nums2[j] is sorted, so seed the heap with (i, 0) for the first min(k, m) rows and push (i, j + 1) after popping (i, j). Touches only O(k) cells instead of all m*n.
Ignores the sortedness entirely: materialize every pair, sort by sum, keep the first k. Fine at the m*n <= 30 sizes here, hopeless at LeetCode's m = n = 1e5 (1e10 pairs).
Row i of the implicit matrix nums1[i] + nums2[j] is sorted, so seed the heap with (i, 0) for the first min(k, m) rows and push (i, j + 1) after popping (i, j). Touches only O(k) cells instead of all m*n. JS has no stdlib priority queue, so a small array-backed binary min-heap of [sum, i, j] cells is inlined.
Ignores the sortedness entirely: materialize every pair, sort by sum, keep the first k. Fine at the m*n <= 30 sizes here, hopeless at LeetCode's m = n = 1e5 (1e10 pairs).
Row i of the implicit matrix nums1[i]+nums2[j] is sorted, so seed the heap with (i, 0) for the first min(k, m) rows and push (i, j+1) after popping (i, j). Touches only O(k) cells instead of all m*n.
Ignores the sortedness entirely: materialize every pair, sort by sum, keep the first k. Fine at the m*n <= 30 sizes here, hopeless at LeetCode's m = n = 1e5 (1e10 pairs).
Row i of the implicit matrix nums1[i] + nums2[j] is sorted, so seed the heap with (i, 0) for the first min(k, m) rows and push (i, j + 1) after popping (i, j). Touches only O(k) cells instead of all m*n. Swift has no stdlib priority queue, so a small array-backed binary min-heap is inlined.
def kSmallestPairs_brute(self, nums1: list[int], nums2: list[int],
k: int) -> list[list[int]]:
return heapq.nsmallest(k, ([a, b] for a in nums1 for b in nums2),
key=sum)
def kSmallestPairs_merge(self, nums1: list[int], nums2: list[int],
k: int) -> list[list[int]]:
def row(a: int):
return ((a + b, a, b) for b in nums2)
merged = heapq.merge(*(row(a) for a in nums1))
return [[a, b] for _, a, b in islice(merged, k)]
def kSmallestPairs(self, nums1: list[int], nums2: list[int],
k: int) -> list[list[int]]:
heap = [(nums1[i] + nums2[0], i, 0)
for i in range(min(k, len(nums1)))]
heapq.heapify(heap)
result: list[list[int]] = []
while heap and len(result) < k:
_, i, j = heapq.heappop(heap)
result.append([nums1[i], nums2[j]])
if j + 1 < len(nums2):
heapq.heappush(heap, (nums1[i] + nums2[j + 1], i, j + 1))
return result
vector<vector<int>> kSmallestPairsBruteForce(vector<int>& nums1,
vector<int>& nums2, int k) {
vector<tuple<long long, int, int>> all; // (sum, u, v)
all.reserve(nums1.size() * nums2.size());
for (int a : nums1)
for (int b : nums2)
all.emplace_back((long long)a + b, a, b);
sort(all.begin(), all.end());
vector<vector<int>> result;
for (int i = 0; i < (int)all.size() && i < k; ++i)
result.push_back({get<1>(all[i]), get<2>(all[i])});
return result;
}
vector<vector<int>> kSmallestPairs(vector<int>& nums1, vector<int>& nums2,
int k) {
using Cell = tuple<long long, int, int>; // (sum, i, j)
priority_queue<Cell, vector<Cell>, greater<Cell>> heap;
int m = (int)nums1.size(), n = (int)nums2.size();
for (int i = 0; i < min(k, m); ++i)
heap.emplace((long long)nums1[i] + nums2[0], i, 0);
vector<vector<int>> result;
while (!heap.empty() && (int)result.size() < k) {
auto [sum, i, j] = heap.top();
heap.pop();
result.push_back({nums1[i], nums2[j]});
if (j + 1 < n)
heap.emplace((long long)nums1[i] + nums2[j + 1], i, j + 1);
}
return result;
}
pub fn k_smallest_pairs_brute_force(nums1: Vec<i32>, nums2: Vec<i32>, k: i32) -> Vec<Vec<i32>> {
let mut all: Vec<(i64, i32, i32)> = Vec::with_capacity(nums1.len() * nums2.len());
for &a in &nums1 {
for &b in &nums2 {
all.push((a as i64 + b as i64, a, b));
}
}
all.sort_unstable();
all.into_iter()
.take(k as usize)
.map(|(_, a, b)| vec![a, b])
.collect()
}
pub fn k_smallest_pairs(nums1: Vec<i32>, nums2: Vec<i32>, k: i32) -> Vec<Vec<i32>> {
let k = k as usize;
let mut heap: BinaryHeap<Reverse<(i64, usize, usize)>> = BinaryHeap::new();
for i in 0..nums1.len().min(k) {
heap.push(Reverse((nums1[i] as i64 + nums2[0] as i64, i, 0)));
}
let mut result = Vec::new();
while result.len() < k {
let Some(Reverse((_, i, j))) = heap.pop() else {
break;
};
result.push(vec![nums1[i], nums2[j]]);
if j + 1 < nums2.len() {
heap.push(Reverse((nums1[i] as i64 + nums2[j + 1] as i64, i, j + 1)));
}
}
result
}
function kSmallestPairsBruteForce(
nums1: number[],
nums2: number[],
k: number,
): number[][] {
const all: number[][] = [];
for (const a of nums1) {
for (const b of nums2) {
all.push([a, b]);
}
}
all.sort((p, q) => p[0] + p[1] - (q[0] + q[1]));
return all.slice(0, k);
}
class MinHeap {
private items: [number, number, number][] = [];
get size(): number {
return this.items.length;
}
push(item: [number, number, number]): void {
const items = this.items;
items.push(item);
let child = items.length - 1;
while (child > 0) {
const parent = (child - 1) >> 1;
if (items[parent][0] <= items[child][0]) break;
const tmp = items[parent];
items[parent] = items[child];
items[child] = tmp;
child = parent;
}
}
pop(): [number, number, number] {
const items = this.items;
const top = items[0];
const last = items.pop() as [number, number, number];
if (items.length > 0) {
items[0] = last;
let parent = 0;
for (;;) {
const left = 2 * parent + 1;
if (left >= items.length) break;
let smallest = left;
const right = left + 1;
if (right < items.length && items[right][0] < items[left][0]) {
smallest = right;
}
if (items[parent][0] <= items[smallest][0]) break;
const tmp = items[parent];
items[parent] = items[smallest];
items[smallest] = tmp;
parent = smallest;
}
}
return top;
}
}
function kSmallestPairs(nums1: number[], nums2: number[], k: number): number[][] {
const heap = new MinHeap();
for (let i = 0; i < nums1.length && i < k; i++) {
heap.push([nums1[i] + nums2[0], i, 0]);
}
const result: number[][] = [];
while (heap.size > 0 && result.length < k) {
const [, i, j] = heap.pop();
result.push([nums1[i], nums2[j]]);
if (j + 1 < nums2.length) {
heap.push([nums1[i] + nums2[j + 1], i, j + 1]);
}
}
return result;
}
func kSmallestPairsBruteForce(nums1 []int, nums2 []int, k int) [][]int {
all := make([][]int, 0, len(nums1)*len(nums2))
for _, a := range nums1 {
for _, b := range nums2 {
all = append(all, []int{a, b})
}
}
sort.Slice(all, func(i, j int) bool {
return all[i][0]+all[i][1] < all[j][0]+all[j][1]
})
if k > len(all) {
k = len(all)
}
return all[:k]
}
type cell struct{ sum, i, j int }
type cellHeap []cell
func (h cellHeap) Len() int { return len(h) }
func (h cellHeap) Less(a, b int) bool { return h[a].sum < h[b].sum }
func (h cellHeap) Swap(a, b int) { h[a], h[b] = h[b], h[a] }
func (h *cellHeap) Push(x any) { *h = append(*h, x.(cell)) }
func (h *cellHeap) Pop() any {
old := *h
last := old[len(old)-1]
*h = old[:len(old)-1]
return last
}
func kSmallestPairs(nums1 []int, nums2 []int, k int) [][]int {
frontier := &cellHeap{}
for i := 0; i < len(nums1) && i < k; i++ {
*frontier = append(*frontier, cell{nums1[i] + nums2[0], i, 0})
}
heap.Init(frontier)
result := make([][]int, 0, k)
for frontier.Len() > 0 && len(result) < k {
top := heap.Pop(frontier).(cell)
result = append(result, []int{nums1[top.i], nums2[top.j]})
if top.j+1 < len(nums2) {
heap.Push(frontier, cell{nums1[top.i] + nums2[top.j+1], top.i, top.j + 1})
}
}
return result
}
func kSmallestPairsBruteForce(_ nums1: [Int], _ nums2: [Int], _ k: Int) -> [[Int]] {
var all: [[Int]] = []
all.reserveCapacity(nums1.count * nums2.count)
for a in nums1 {
for b in nums2 {
all.append([a, b])
}
}
all.sort { $0[0] + $0[1] < $1[0] + $1[1] }
return Array(all.prefix(k))
}
private struct MinHeap {
private var items: [(sum: Int, i: Int, j: Int)] = []
var isEmpty: Bool { items.isEmpty }
mutating func push(_ item: (sum: Int, i: Int, j: Int)) {
items.append(item)
var child = items.count - 1
while child > 0 {
let parent = (child - 1) / 2
if items[parent].sum <= items[child].sum { break }
items.swapAt(parent, child)
child = parent
}
}
mutating func pop() -> (sum: Int, i: Int, j: Int) {
let top = items[0]
items[0] = items[items.count - 1]
items.removeLast()
var parent = 0
while true {
let left = 2 * parent + 1
if left >= items.count { break }
var smallest = left
let right = left + 1
if right < items.count && items[right].sum < items[left].sum {
smallest = right
}
if items[parent].sum <= items[smallest].sum { break }
items.swapAt(parent, smallest)
parent = smallest
}
return top
}
}
func kSmallestPairs(_ nums1: [Int], _ nums2: [Int], _ k: Int) -> [[Int]] {
var heap = MinHeap()
for i in 0..<min(k, nums1.count) {
heap.push((nums1[i] + nums2[0], i, 0))
}
var result: [[Int]] = []
while !heap.isEmpty && result.count < k {
let (_, i, j) = heap.pop()
result.append([nums1[i], nums2[j]])
if j + 1 < nums2.count {
heap.push((nums1[i] + nums2[j + 1], i, j + 1))
}
}
return result
}
Bit Manipulation
41. Single Number II
Medium · LC 137
Given an array where every number appears exactly three times except one that appears once, find that number in linear time and constant space. Two integer masks, ones and twos, act as a per-bit counter modulo three: each incoming number XORs into ones unless that bit is already recorded in twos, then into twos unless it just landed in ones, so any bit seen three times cancels back to zero and the lone number is left sitting in ones. The trick is recognizing the update as a two-bit state machine that cycles through zero, one, and two occurrences per bit position, run across all bit positions at once.
3 * sum(unique) counts every element thrice, so the difference from sum(nums) is exactly two copies of the single. Easiest to reason about, but the set costs O(n) space and misses the follow-up.
Drops the O(n) set: sum each of the 32 bit positions over the array; the lone number owns exactly the positions whose count is not divisible by 3. Bit 31 is the sign bit, so fold the raw result back into signed 32-bit range.
The 32x inner loop of Approach 2 collapsed into two masks: per bit position, count-mod-3 cycles 00 -> 01 -> 10 -> 00; `ones` and `twos` hold the low/high state bits across all positions at once, so any bit seen three times cancels and the lone number is left in `ones`. Python ints act as infinite two's complement, so negatives fall out correctly with no masking.
Count every value, return the one seen once. The obvious first answer, but the map costs O(n) space and misses the follow-up.
Drops the O(n) map: sum each of the 32 bit positions over the array; the lone number owns exactly the positions whose count is not divisible by 3. Assemble in unsigned so the sign bit needs no special case.
The 32x inner loop of Approach 2 collapsed into two masks: per bit position, count-mod-3 cycles 00 -> 01 -> 10 -> 00. ones/twos hold the low/high state bits for all 32 positions at once, so bits seen three times cancel and the lone number remains in ones. Bitwise ops are well defined on negatives.
Count every value, return the one seen once. The obvious first answer, but the map costs O(n) space and misses the follow-up.
Drops the O(n) map: sum each of the 32 bit positions over the array; the lone number owns exactly the positions whose count is not divisible by 3. Assemble in u32 so the sign bit needs no special case.
The 32x inner loop of Approach 2 collapsed into two masks: per bit position, count-mod-3 cycles 00 -> 01 -> 10 -> 00. ones/twos hold the low/high state bits for all 32 positions at once, so bits seen three times cancel and the lone number remains in ones. Bitwise ops are well defined on negatives.
Count every value, return the one seen once. The obvious first answer, but the map costs O(n) space and misses the follow-up.
Drops the O(n) map: sum each of the 32 bit positions over the array; the lone number owns exactly the positions whose count is not divisible by 3. |= with 1 << 31 lands on the sign bit, so the signed result assembles itself.
The 32x inner loop of Approach 2 collapsed into two masks: per bit position, count-mod-3 cycles 00 -> 01 -> 10 -> 00. ones/twos hold the low/high state bits for all 32 positions at once, so bits seen three times cancel and the lone number remains in ones. Negatives fall out correctly under 32-bit bitwise semantics.
Count every value, return the one seen once. The obvious first answer, but the map costs O(n) space and misses the follow-up.
Drops the O(n) map: sum each of the 32 bit positions over the array; the lone number owns exactly the positions whose count is not divisible by 3. Assemble in uint32, then sign-extend via int32.
The 32x inner loop of Approach 2 collapsed into two masks: per bit position, count-mod-3 cycles 00 -> 01 -> 10 -> 00. ones/twos hold the low/high state bits for every position at once, so bits seen three times cancel and the lone number remains in ones. Works for negatives: sign-extended bits follow the same automaton.
Count every value, return the one seen once. The obvious first answer, but the dictionary costs O(n) space and misses the follow-up.
Drops the O(n) dictionary: sum each of the 32 bit positions over the array; the lone number owns exactly the positions whose count is not divisible by 3. Bit 31 is the sign bit of the 32-bit value, so fold the raw result back into signed 32-bit range.
The 32x inner loop of Approach 2 collapsed into two masks: per bit position, count-mod-3 cycles 00 -> 01 -> 10 -> 00. ones/twos hold the low/high state bits for every position at once, so bits seen three times cancel and the lone number remains in ones. Sign-extended bits follow the same automaton, so negatives fall out correctly.
def singleNumber_math(self, nums: list[int]) -> int:
return (3 * sum(set(nums)) - sum(nums)) // 2
def singleNumber_bit_count(self, nums: list[int]) -> int:
result = 0
for bit in range(32):
count = sum((num >> bit) & 1 for num in nums)
if count % 3:
result |= 1 << bit
return result - (1 << 32) if result >= (1 << 31) else result
def singleNumber(self, nums: list[int]) -> int:
ones = twos = 0
for num in nums:
ones = (ones ^ num) & ~twos
twos = (twos ^ num) & ~ones
return ones
int singleNumberHashCount(vector<int>& nums) {
unordered_map<int, int> counts;
for (int num : nums)
++counts[num];
for (const auto& [num, count] : counts)
if (count == 1)
return num;
return 0; // unreachable on valid input
}
int singleNumberBitCount(vector<int>& nums) {
unsigned result = 0;
for (int bit = 0; bit < 32; ++bit) {
int count = 0;
for (int num : nums)
count += ((unsigned)num >> bit) & 1u;
if (count % 3)
result |= 1u << bit;
}
return (int)result;
}
int singleNumber(vector<int>& nums) {
int ones = 0, twos = 0;
for (int num : nums) {
ones = (ones ^ num) & ~twos;
twos = (twos ^ num) & ~ones;
}
return ones;
}
pub fn single_number_hash_count(nums: Vec<i32>) -> i32 {
let mut counts: HashMap<i32, i32> = HashMap::new();
for num in nums {
*counts.entry(num).or_insert(0) += 1;
}
counts
.into_iter()
.find(|&(_, count)| count == 1)
.map(|(num, _)| num)
.unwrap_or(0) // unreachable on valid input
}
pub fn single_number_bit_count(nums: Vec<i32>) -> i32 {
let mut result: u32 = 0;
for bit in 0..32 {
let count = nums.iter().filter(|&&num| (num >> bit) & 1 == 1).count();
if count % 3 != 0 {
result |= 1 << bit;
}
}
result as i32
}
pub fn single_number(nums: Vec<i32>) -> i32 {
let (mut ones, mut twos) = (0i32, 0i32);
for num in nums {
ones = (ones ^ num) & !twos;
twos = (twos ^ num) & !ones;
}
ones
}
function singleNumberHashCount(nums: number[]): number {
const counts = new Map<number, number>();
for (const num of nums) {
counts.set(num, (counts.get(num) ?? 0) + 1);
}
for (const [num, count] of counts) {
if (count === 1) {
return num;
}
}
return 0; // unreachable on valid input
}
function singleNumberBitCount(nums: number[]): number {
let result = 0;
for (let bit = 0; bit < 32; bit++) {
let count = 0;
for (const num of nums) {
count += (num >> bit) & 1;
}
if (count % 3 !== 0) {
result |= 1 << bit;
}
}
return result;
}
function singleNumber(nums: number[]): number {
let ones = 0;
let twos = 0;
for (const num of nums) {
ones = (ones ^ num) & ~twos;
twos = (twos ^ num) & ~ones;
}
return ones;
}
func singleNumberHashCount(nums []int) int {
counts := make(map[int]int, len(nums))
for _, num := range nums {
counts[num]++
}
for num, count := range counts {
if count == 1 {
return num
}
}
return 0 // unreachable on valid input
}
func singleNumberBitCount(nums []int) int {
var result uint32
for bit := 0; bit < 32; bit++ {
count := 0
for _, num := range nums {
count += int(uint32(num)>>bit) & 1
}
if count%3 != 0 {
result |= 1 << bit
}
}
return int(int32(result))
}
func singleNumber(nums []int) int {
ones, twos := 0, 0
for _, num := range nums {
ones = (ones ^ num) &^ twos
twos = (twos ^ num) &^ ones
}
return ones
}
func singleNumberHashCount(_ nums: [Int]) -> Int {
var counts: [Int: Int] = [:]
for num in nums {
counts[num, default: 0] += 1
}
for (num, count) in counts where count == 1 {
return num
}
return 0 // unreachable on valid input
}
func singleNumberBitCount(_ nums: [Int]) -> Int {
var result = 0
for bit in 0..<32 {
var count = 0
for num in nums {
count += (num >> bit) & 1
}
if count % 3 != 0 {
result |= 1 << bit
}
}
if result >= (1 << 31) {
result -= (1 << 32)
}
return result
}
func singleNumber(_ nums: [Int]) -> Int {
var ones = 0
var twos = 0
for num in nums {
ones = (ones ^ num) & ~twos
twos = (twos ^ num) & ~ones
}
return ones
}
Math
42. Palindrome Number
Easy · LC 9
Given an integer, decide whether it reads the same forward and backward, ideally without converting it to a string. Reject negatives and any nonzero number ending in zero, then peel digits off the tail into a reversed accumulator until it catches up with the shrinking head, and compare the two halves, dropping the accumulator's last digit when the length is odd. Reversing only half means the accumulator can never overflow a 32-bit integer in stricter languages, and the ending-in-zero pre-check matters because such numbers would otherwise slip through the half-length comparison.
The obvious one-liner; costs an extra string copy. A leading '-' never equals the trailing digit, so negatives fall out naturally.
Drops the string copy: reverse every digit and compare. Fine in Python; in 32-bit languages the full reversal can overflow, which is why Approach 3 stops halfway.
Peel digits off the tail onto `reverted` until it catches up with the head, then compare (dropping the middle digit for odd lengths). No string, and the reversal can never overflow a 32-bit int elsewhere.
Convert, reverse, compare. Easy to write but allocates two strings. A leading '-' never equals the trailing digit, so negatives fall out naturally.
Peel digits off the tail onto `reverted` until it catches up with what remains of x, then compare (dropping the middle digit for odd lengths). No allocation, and reversing only half the digits means the 32-bit int can never overflow — a full reversal could.
Convert, reverse, compare. Easy to write but allocates two strings. A leading '-' never equals the trailing digit, so negatives fall out naturally.
Peel digits off the tail onto `reverted` until it catches up with what remains of x, then compare (dropping the middle digit for odd lengths). No allocation, and reversing only half the digits can never overflow i32 — a full reversal could.
Convert, reverse, compare. Easy to write but allocates an array and two strings. A leading '-' never equals the trailing digit, so negatives fall out naturally.
Peel digits off the tail onto `reverted` until it catches up with what remains of x, then compare (dropping the middle digit for odd lengths). No allocation, and reversing only half the digits is the habit that keeps 32-bit ports overflow-free — a full reversal could overflow there.
Convert, reverse the bytes, compare. Easy to write but allocates two strings. A leading '-' never equals the trailing digit, so negatives fall out naturally.
Peel digits off the tail onto reverted until it catches up with what remains of x, then compare (dropping the middle digit for odd lengths). No allocation, and reversing only half the digits can never overflow even a 32-bit int — a full reversal could.
Convert, reverse, compare. Easy to write but allocates two strings. A leading '-' never equals the trailing digit, so negatives fall out naturally.
Peel digits off the tail onto `reverted` until it catches up with what remains of x, then compare (dropping the middle digit for odd lengths). No allocation, and reversing only half the digits can never overflow even a 32-bit int — a full reversal could.
def isPalindrome_string(self, x: int) -> bool:
s = str(x)
return s == s[::-1]
def isPalindrome_full_reverse(self, x: int) -> bool:
if x < 0:
return False
reversed_x, remaining = 0, x
while remaining:
reversed_x = reversed_x * 10 + remaining % 10
remaining //= 10
return reversed_x == x
def isPalindrome(self, x: int) -> bool:
if x < 0 or (x % 10 == 0 and x != 0):
return False
reverted = 0
while x > reverted:
reverted = reverted * 10 + x % 10
x //= 10
return x == reverted or x == reverted // 10
bool isPalindromeString(int x) {
string s = to_string(x);
string reversed(s.rbegin(), s.rend());
return s == reversed;
}
bool isPalindrome(int x) {
if (x < 0 || (x % 10 == 0 && x != 0)) return false;
int reverted = 0;
while (x > reverted) {
reverted = reverted * 10 + x % 10;
x /= 10;
}
return x == reverted || x == reverted / 10;
}
pub fn is_palindrome_string(x: i32) -> bool {
let s = x.to_string();
let reversed: String = s.chars().rev().collect();
s == reversed
}
pub fn is_palindrome(x: i32) -> bool {
if x < 0 || (x % 10 == 0 && x != 0) {
return false;
}
let mut x = x;
let mut reverted = 0;
while x > reverted {
reverted = reverted * 10 + x % 10;
x /= 10;
}
x == reverted || x == reverted / 10
}
function isPalindromeString(x: number): boolean {
const s = String(x);
return s === [...s].reverse().join("");
}
function isPalindrome(x: number): boolean {
if (x < 0 || (x % 10 === 0 && x !== 0)) {
return false;
}
let reverted = 0;
while (x > reverted) {
reverted = reverted * 10 + (x % 10);
x = Math.floor(x / 10);
}
return x === reverted || x === Math.floor(reverted / 10);
}
func isPalindromeString(x int) bool {
s := strconv.Itoa(x)
b := []byte(s)
for i, j := 0, len(b)-1; i < j; i, j = i+1, j-1 {
b[i], b[j] = b[j], b[i]
}
return s == string(b)
}
func isPalindrome(x int) bool {
if x < 0 || (x%10 == 0 && x != 0) {
return false
}
reverted := 0
for x > reverted {
reverted = reverted*10 + x%10
x /= 10
}
return x == reverted || x == reverted/10
}
func isPalindromeString(_ x: Int) -> Bool {
let s = String(x)
return s == String(s.reversed())
}
func isPalindrome(_ x: Int) -> Bool {
if x < 0 || (x % 10 == 0 && x != 0) {
return false
}
var head = x
var reverted = 0
while head > reverted {
reverted = reverted * 10 + head % 10
head /= 10
}
return head == reverted || head == reverted / 10
}
43. Factorial Trailing Zeroes
Medium · LC 172
Given an integer n, count the trailing zeroes of n factorial without ever computing the factorial. Every trailing zero needs a factor of ten, and since twos vastly outnumber fives the answer is just the number of factors of five in the product, found by repeatedly floor-dividing n by five and summing the quotients. The elegance is that each successive quotient recounts the multiples of higher powers of five, so a multiple of twenty-five is credited twice and so on, giving the exact total in logarithmic time.
Only viable because Python integers are arbitrary precision; a slow but obviously-correct reference, and hopeless in 32/64-bit languages where n! overflows almost immediately.
Never materializes n!: walk 5, 10, 15, ... and add how many factors of 5 each contributes. Fixed-width safe, but still linear in n.
Count all multiples at once instead of visiting each: every term counts the multiples of that power of 5 up to n, so a multiple of 25 is counted twice, of 125 three times, and so on.
Walk 5, 10, 15, ... and add how many factors of 5 each contributes. Correct and overflow-free, but linear in n — fine at the n <= 10^4 constraint, wasteful next to the closed-form sum below.
Count all multiples at once instead of visiting each: every term counts the multiples of that power of 5 up to n, so a multiple of 25 is counted twice, of 125 three times, and so on.
Walk 5, 10, 15, ... and add how many factors of 5 each contributes. Correct and overflow-free, but linear in n — fine at the n <= 10^4 constraint, wasteful next to the closed-form sum below.
Count all multiples at once instead of visiting each: every term counts the multiples of that power of 5 up to n, so a multiple of 25 is counted twice, of 125 three times, and so on.
Walk 5, 10, 15, ... and add how many factors of 5 each contributes. Correct and overflow-free, but linear in n — fine at the n <= 10^4 constraint, wasteful next to the closed-form sum below.
Count all multiples at once instead of visiting each: every term counts the multiples of that power of 5 up to n, so a multiple of 25 is counted twice, of 125 three times, and so on.
Walk 5, 10, 15, ... and add how many factors of 5 each contributes. Correct and overflow-free, but linear in n — fine at the n <= 10^4 constraint, wasteful next to the closed-form sum below.
Count all multiples at once instead of visiting each: every term counts the multiples of that power of 5 up to n, so a multiple of 25 is counted twice, of 125 three times, and so on.
Walk 5, 10, 15, ... and add how many factors of 5 each contributes. Correct and overflow-free, but linear in n — fine at the n <= 10^4 constraint, wasteful next to the closed-form sum below.
Count all multiples at once instead of visiting each: every term counts the multiples of that power of 5 up to n, so a multiple of 25 is counted twice, of 125 three times, and so on.
def trailingZeroes_factorial(self, n: int) -> int:
factorial = math.factorial(n)
zeroes = 0
while factorial % 10 == 0:
factorial //= 10
zeroes += 1
return zeroes
def trailingZeroes_count_multiples(self, n: int) -> int:
zeroes = 0
for multiple in range(5, n + 1, 5):
while multiple % 5 == 0:
multiple //= 5
zeroes += 1
return zeroes
def trailingZeroes(self, n: int) -> int:
zeroes = 0
while n:
n //= 5
zeroes += n
return zeroes
int trailingZeroesCountMultiples(int n) {
int zeroes = 0;
for (int multiple = 5; multiple <= n; multiple += 5) {
for (int m = multiple; m % 5 == 0; m /= 5) {
++zeroes;
}
}
return zeroes;
}
int trailingZeroes(int n) {
int zeroes = 0;
while (n > 0) {
n /= 5;
zeroes += n;
}
return zeroes;
}
pub fn trailing_zeroes_count_multiples(n: i32) -> i32 {
let mut zeroes = 0;
let mut multiple = 5;
while multiple <= n {
let mut m = multiple;
while m % 5 == 0 {
m /= 5;
zeroes += 1;
}
multiple += 5;
}
zeroes
}
pub fn trailing_zeroes(n: i32) -> i32 {
let mut n = n;
let mut zeroes = 0;
while n > 0 {
n /= 5;
zeroes += n;
}
zeroes
}
function trailingZeroesCountMultiples(n: number): number {
let zeroes = 0;
for (let multiple = 5; multiple <= n; multiple += 5) {
for (let m = multiple; m % 5 === 0; m = Math.floor(m / 5)) {
zeroes++;
}
}
return zeroes;
}
function trailingZeroes(n: number): number {
let zeroes = 0;
while (n > 0) {
n = Math.floor(n / 5);
zeroes += n;
}
return zeroes;
}
func trailingZeroesCountMultiples(n int) int {
zeroes := 0
for multiple := 5; multiple <= n; multiple += 5 {
for m := multiple; m%5 == 0; m /= 5 {
zeroes++
}
}
return zeroes
}
func trailingZeroes(n int) int {
zeroes := 0
for n > 0 {
n /= 5
zeroes += n
}
return zeroes
}
func trailingZeroesCountMultiples(_ n: Int) -> Int {
var zeroes = 0
var multiple = 5
while multiple <= n {
var m = multiple
while m % 5 == 0 {
m /= 5
zeroes += 1
}
multiple += 5
}
return zeroes
}
func trailingZeroes(_ n: Int) -> Int {
var n = n
var zeroes = 0
while n > 0 {
n /= 5
zeroes += n
}
return zeroes
}
44. Max Points on a Line
Hard · LC 149
Given a set of distinct points in the plane, find the greatest number that lie on a single straight line. Anchor each point in turn and hash every later point by its direction from the anchor, the coordinate differences reduced by their gcd and forced to a canonical sign; the fullest bucket plus the anchor itself is the best line through that anchor. The pitfall is representing slope as a floating-point ratio, which breaks on precision and vertical lines, whereas the reduced integer pair is exact and costs only linear space per anchor.
For each pair, count the points whose cross product with it is zero. No hashing or normalization to get wrong, so it anchors the tests — cubic is fine at n <= 300, but it is the slowest rung by far.
Every pair defines a line; reduce (a, b, c) by their gcd with a canonical sign and collect the distinct points on each line. Drops a factor of n off the brute force, but materializes every line.
For each anchor point, bucket every later point by the direction (dy, dx) reduced by gcd and sign-canonicalized (dx > 0, or vertical stored as (1, 0)). The best line through the anchor is its fullest bucket plus the anchor itself — same quadratic time as Approach 2 with only O(n) live state.
For each pair, count the points whose cross product with it is zero. No hashing or normalization to get wrong, so it anchors the tests — cubic is fine at n <= 300, but the slowest rung by far.
For each anchor point, bucket every later point by its direction (dy, dx) reduced by gcd and sign-canonicalized (dx > 0, vertical stored as (1, 0)). The best line through the anchor is its fullest bucket plus the anchor itself — drops a factor of n off the brute force.
For each pair, count the points whose cross product with it is zero. No hashing or normalization to get wrong, so it anchors the tests — cubic is fine at n <= 300, but the slowest rung by far.
For each anchor point, bucket every later point by its direction (dy, dx) reduced by gcd and sign-canonicalized (dx > 0, vertical stored as (1, 0)). The best line through the anchor is its fullest bucket plus the anchor itself — drops a factor of n off the brute force.
For each pair, count the points whose cross product with it is zero. No hashing or normalization to get wrong, so it anchors the tests — cubic is fine at n <= 300, but the slowest rung by far.
For each anchor point, bucket every later point by its direction (dy, dx) reduced by gcd and sign-canonicalized (dx > 0, vertical stored as (1, 0)), keyed as the exact string "dy,dx". The best line through the anchor is its fullest bucket plus the anchor itself — drops a factor of n off the brute force.
For each pair, count the points whose cross product with it is zero. No hashing or normalization to get wrong, so it anchors the tests — cubic is fine at n <= 300, but the slowest rung by far.
For each anchor point, bucket every later point by its direction (dy, dx) reduced by gcd and sign-canonicalized (dx > 0, vertical stored as (1, 0)). The best line through the anchor is its fullest bucket plus the anchor itself — drops a factor of n off the brute force.
For each pair, count the points whose cross product with it is zero. No hashing or normalization to get wrong, so it anchors the tests — cubic is fine at n <= 300, but the slowest rung by far.
For each anchor point, bucket every later point by its direction (dy, dx) reduced by gcd and sign-canonicalized (dx > 0, vertical stored as (1, 0)); reduced components stay within +/-2e4, so dy * 100003 + dx is a collision-free key. The best line through the anchor is its fullest bucket plus the anchor itself — drops a factor of n off the brute force.
def maxPoints_bruteforce(self, points: list[list[int]]) -> int:
n = len(points)
if n <= 2:
return n
best = 2
for i, (x1, y1) in enumerate(points):
for j in range(i + 1, n):
x2, y2 = points[j]
count = 2
for x3, y3 in points[j + 1:]:
if (x2 - x1) * (y3 - y1) == (y2 - y1) * (x3 - x1):
count += 1
best = max(best, count)
return best
def maxPoints_line_hash(self, points: list[list[int]]) -> int:
n = len(points)
if n <= 2:
return n
lines: dict[tuple[int, int, int], set[int]] = {}
for i, (x1, y1) in enumerate(points):
for j in range(i + 1, n):
x2, y2 = points[j]
a, b = y2 - y1, x1 - x2
c = -(a * x1 + b * y1)
g = gcd(gcd(a, b), c) # positive: (a, b) != (0, 0)
a, b, c = a // g, b // g, c // g
if a < 0 or (a == 0 and b < 0):
a, b, c = -a, -b, -c
lines.setdefault((a, b, c), set()).update((i, j))
return max(len(members) for members in lines.values())
def maxPoints(self, points: list[list[int]]) -> int:
n = len(points)
if n <= 2:
return n
best = 2
for i in range(n - 1): # last point alone anchors nothing new
x1, y1 = points[i]
slopes: dict[tuple[int, int], int] = {}
for x2, y2 in points[i + 1:]:
dy, dx = y2 - y1, x2 - x1
g = gcd(dy, dx) # positive: distinct points
dy, dx = dy // g, dx // g
if dx < 0 or (dx == 0 and dy < 0):
dy, dx = -dy, -dx
slopes[dy, dx] = slopes.get((dy, dx), 0) + 1
best = max(best, max(slopes.values()) + 1)
return best
int maxPointsBruteForce(vector<vector<int>>& points) {
int n = (int)points.size();
if (n <= 2)
return n;
int best = 2;
for (int i = 0; i < n; ++i) {
for (int j = i + 1; j < n; ++j) {
long long x1 = points[i][0], y1 = points[i][1];
long long x2 = points[j][0], y2 = points[j][1];
int count = 2;
for (int l = j + 1; l < n; ++l) {
long long x3 = points[l][0], y3 = points[l][1];
if ((x2 - x1) * (y3 - y1) == (y2 - y1) * (x3 - x1))
++count;
}
best = max(best, count);
}
}
return best;
}
int maxPoints(vector<vector<int>>& points) {
int n = (int)points.size();
if (n <= 2)
return n;
int best = 2;
for (int i = 0; i < n - 1; ++i) {
unordered_map<long long, int> slopes;
for (int j = i + 1; j < n; ++j) {
int dy = points[j][1] - points[i][1];
int dx = points[j][0] - points[i][0];
int g = gcd(dy, dx); // positive: points are distinct
dy /= g;
dx /= g;
if (dx < 0 || (dx == 0 && dy < 0)) {
dy = -dy;
dx = -dx;
}
// Reduced components stay within +/-2e4, so this key is
// collision-free.
long long key = (long long)dy * 100003 + dx;
best = max(best, ++slopes[key] + 1);
}
}
return best;
}
pub fn max_points_brute_force(points: Vec<Vec<i32>>) -> i32 {
let n = points.len();
if n <= 2 {
return n as i32;
}
let mut best = 2;
for i in 0..n {
for j in i + 1..n {
let (x1, y1) = (points[i][0] as i64, points[i][1] as i64);
let (x2, y2) = (points[j][0] as i64, points[j][1] as i64);
let mut count = 2;
for p in points.iter().skip(j + 1) {
let (x3, y3) = (p[0] as i64, p[1] as i64);
if (x2 - x1) * (y3 - y1) == (y2 - y1) * (x3 - x1) {
count += 1;
}
}
best = best.max(count);
}
}
best
}
pub fn max_points(points: Vec<Vec<i32>>) -> i32 {
let n = points.len();
if n <= 2 {
return n as i32;
}
let mut best = 2;
for i in 0..n - 1 {
let mut slopes: HashMap<(i32, i32), i32> = HashMap::new();
for j in i + 1..n {
let mut dy = points[j][1] - points[i][1];
let mut dx = points[j][0] - points[i][0];
let g = Self::gcd(dy.abs(), dx.abs()); // positive: distinct points
dy /= g;
dx /= g;
if dx < 0 || (dx == 0 && dy < 0) {
dy = -dy;
dx = -dx;
}
let count = slopes.entry((dy, dx)).or_insert(0);
*count += 1;
best = best.max(*count + 1);
}
}
best
}
fn gcd(a: i32, b: i32) -> i32 {
if b == 0 {
a
} else {
Self::gcd(b, a % b)
}
}
function maxPointsBruteForce(points: number[][]): number {
const n = points.length;
if (n <= 2) {
return n;
}
let best = 2;
for (let i = 0; i < n; i++) {
for (let j = i + 1; j < n; j++) {
const [x1, y1] = points[i];
const [x2, y2] = points[j];
let count = 2;
for (let l = j + 1; l < n; l++) {
const [x3, y3] = points[l];
if ((x2 - x1) * (y3 - y1) === (y2 - y1) * (x3 - x1)) {
count++;
}
}
best = Math.max(best, count);
}
}
return best;
}
function maxPoints(points: number[][]): number {
const n = points.length;
if (n <= 2) {
return n;
}
const gcd = (a: number, b: number): number => (b === 0 ? a : gcd(b, a % b));
let best = 2;
for (let i = 0; i < n - 1; i++) {
const slopes: { [key: string]: number } = {};
for (let j = i + 1; j < n; j++) {
let dy = points[j][1] - points[i][1];
let dx = points[j][0] - points[i][0];
const g = gcd(Math.abs(dy), Math.abs(dx)); // positive: distinct points
dy /= g;
dx /= g;
if (dx < 0 || (dx === 0 && dy < 0)) {
dy = -dy;
dx = -dx;
}
const key = `${dy},${dx}`;
const count = (slopes[key] || 0) + 1;
slopes[key] = count;
if (count + 1 > best) {
best = count + 1;
}
}
}
return best;
}
func maxPointsBruteForce(points [][]int) int {
n := len(points)
if n <= 2 {
return n
}
best := 2
for i := 0; i < n; i++ {
for j := i + 1; j < n; j++ {
x1, y1 := points[i][0], points[i][1]
x2, y2 := points[j][0], points[j][1]
count := 2
for l := j + 1; l < n; l++ {
x3, y3 := points[l][0], points[l][1]
if (x2-x1)*(y3-y1) == (y2-y1)*(x3-x1) {
count++
}
}
if count > best {
best = count
}
}
}
return best
}
func maxPoints(points [][]int) int {
n := len(points)
if n <= 2 {
return n
}
best := 2
for i := 0; i < n-1; i++ {
slopes := map[[2]int]int{}
for j := i + 1; j < n; j++ {
dy := points[j][1] - points[i][1]
dx := points[j][0] - points[i][0]
g := gcd(dy, dx) // positive: points are distinct
dy, dx = dy/g, dx/g
if dx < 0 || (dx == 0 && dy < 0) {
dy, dx = -dy, -dx
}
key := [2]int{dy, dx}
slopes[key]++
if slopes[key]+1 > best {
best = slopes[key] + 1
}
}
}
return best
}
// gcd returns the positive greatest common divisor (gcd(x, 0) = |x|).
func gcd(a, b int) int {
if a < 0 {
a = -a
}
if b < 0 {
b = -b
}
for b != 0 {
a, b = b, a%b
}
return a
}
func maxPointsBruteForce(_ points: [[Int]]) -> Int {
let n = points.count
if n <= 2 {
return n
}
var best = 2
for i in 0..<n {
for j in (i + 1)..<n {
let (x1, y1) = (points[i][0], points[i][1])
let (x2, y2) = (points[j][0], points[j][1])
var count = 2
for l in (j + 1)..<n {
let (x3, y3) = (points[l][0], points[l][1])
if (x2 - x1) * (y3 - y1) == (y2 - y1) * (x3 - x1) {
count += 1
}
}
best = max(best, count)
}
}
return best
}
func maxPoints(_ points: [[Int]]) -> Int {
let n = points.count
if n <= 2 {
return n
}
var best = 2
for i in 0..<(n - 1) {
var slopes: [Int: Int] = [:]
for j in (i + 1)..<n {
var dy = points[j][1] - points[i][1]
var dx = points[j][0] - points[i][0]
let g = gcd(abs(dy), abs(dx)) // positive: points are distinct
dy /= g
dx /= g
if dx < 0 || (dx == 0 && dy < 0) {
dy = -dy
dx = -dx
}
let key = dy * 100003 + dx
let count = (slopes[key] ?? 0) + 1
slopes[key] = count
best = max(best, count + 1)
}
}
return best
}
private func gcd(_ a: Int, _ b: Int) -> Int {
b == 0 ? a : gcd(b, a % b)
}
Multidimensional DP
45. Triangle
Medium · LC 120
Given a triangle of numbers, find the minimum path sum from the apex to the base, where each step moves to one of the two adjacent entries in the row below. Work bottom-up: copy the last row into a one-dimensional array, then for each higher row overwrite each slot with its own value plus the smaller of the two entries beneath it, until the single surviving slot holds the answer. Going upward is the trick, because it erases the edge special cases and the final minimum scan that a top-down sweep needs, and since values can be negative no greedy shortcut is safe anyway.
The naive recursion is O(2^n); caching each (row, i) collapses the exponential tree into one visit per cell. Memory stays quadratic.
dp[i] = cheapest way to REACH (row, i) from the top — iteration drops the quadratic memo to one row. Edges of each row have one parent; interior cells have two; the answer is min over the last row.
Working upward, dp[i] = best path from (row, i) to the base; each cell only needs the two neighbors below it. Same costs as Approach 2, but the edge special-cases and the final min scan disappear: dp[0] is it.
The naive recursion is O(2^n); caching each (row, i) collapses the exponential tree into one visit per cell. Memory stays quadratic.
Working upward, dp[i] = best path from (row, i) to the base; each cell needs only the two neighbors below it, so one row of state replaces the quadratic memo and the recursion stack. dp[0] is the answer — no final scan, no per-row edge cases.
The naive recursion is O(2^n); caching each (row, i) collapses the exponential tree into one visit per cell. Memory stays quadratic.
Working upward, dp[i] = best path from (row, i) to the base; each cell needs only the two neighbors below it, so one row of state replaces the quadratic memo and the recursion stack. dp[0] is the answer — no final scan, no per-row edge cases.
The naive recursion is O(2^n); caching each (row, i) collapses the exponential tree into one visit per cell. Memory stays quadratic.
Working upward, dp[i] = best path from (row, i) to the base; each cell needs only the two neighbors below it, so one row of state replaces the quadratic memo and the recursion stack. dp[0] is the answer — no final scan, no per-row edge cases.
The naive recursion is O(2^n); caching each (row, i) collapses the exponential tree into one visit per cell. Memory stays quadratic.
Working upward, dp[i] = best path from (row, i) to the base; each cell needs only the two neighbors below it, so one row of state replaces the quadratic memo and the recursion stack. dp[0] is the answer — no final scan, no per-row edge cases.
The naive recursion is O(2^n); caching each (row, i) collapses the exponential tree into one visit per cell. Memory stays quadratic.
Working upward, dp[i] = best path from (row, i) to the base; each cell needs only the two neighbors below it, so one row of state replaces the quadratic memo and the recursion stack. dp[0] is the answer — no final scan, no per-row edge cases.
def minimumTotal_memo(self, triangle: list[list[int]]) -> int:
memo: dict[tuple[int, int], int] = {}
def best(row: int, i: int) -> int:
if row == len(triangle) - 1:
return triangle[row][i]
if (row, i) not in memo:
memo[row, i] = triangle[row][i] + min(best(row + 1, i),
best(row + 1, i + 1))
return memo[row, i]
return best(0, 0)
def minimumTotal_forward(self, triangle: list[list[int]]) -> int:
dp = [triangle[0][0]]
for row in range(1, len(triangle)):
nxt = [0] * (row + 1)
nxt[0] = dp[0] + triangle[row][0]
nxt[row] = dp[row - 1] + triangle[row][row]
for i in range(1, row):
nxt[i] = min(dp[i - 1], dp[i]) + triangle[row][i]
dp = nxt
return min(dp)
def minimumTotal(self, triangle: list[list[int]]) -> int:
dp = triangle[-1][:] # copy so we never mutate the input
for row in range(len(triangle) - 2, -1, -1):
for i in range(row + 1):
dp[i] = triangle[row][i] + min(dp[i], dp[i + 1])
return dp[0]
int minimumTotalMemo(std::vector<std::vector<int>>& triangle) {
int n = static_cast<int>(triangle.size());
// Path sums fit comfortably in int (|value| <= 10^4, n <= 200), so
// INT_MIN is a safe "unset" sentinel.
std::vector<std::vector<int>> memo(n, std::vector<int>(n, INT_MIN));
auto best = [&](auto&& self, int row, int i) -> int {
if (row == n - 1) return triangle[row][i];
if (memo[row][i] == INT_MIN) {
memo[row][i] = triangle[row][i] +
std::min(self(self, row + 1, i),
self(self, row + 1, i + 1));
}
return memo[row][i];
};
return best(best, 0, 0);
}
int minimumTotal(std::vector<std::vector<int>>& triangle) {
std::vector<int> dp = triangle.back();
for (int row = static_cast<int>(triangle.size()) - 2; row >= 0; --row) {
for (int i = 0; i <= row; ++i) {
dp[i] = triangle[row][i] + std::min(dp[i], dp[i + 1]);
}
}
return dp[0];
}
pub fn minimum_total_memo(triangle: Vec<Vec<i32>>) -> i32 {
fn best(
triangle: &[Vec<i32>],
memo: &mut [Vec<Option<i32>>],
row: usize,
i: usize,
) -> i32 {
if row == triangle.len() - 1 {
return triangle[row][i];
}
if memo[row][i].is_none() {
let down = best(triangle, memo, row + 1, i);
let diag = best(triangle, memo, row + 1, i + 1);
memo[row][i] = Some(triangle[row][i] + down.min(diag));
}
memo[row][i].unwrap()
}
let mut memo: Vec<Vec<Option<i32>>> =
(0..triangle.len()).map(|row| vec![None; row + 1]).collect();
best(&triangle, &mut memo, 0, 0)
}
pub fn minimum_total(triangle: Vec<Vec<i32>>) -> i32 {
let mut dp = triangle.last().unwrap().clone();
for row in (0..triangle.len() - 1).rev() {
for i in 0..=row {
dp[i] = triangle[row][i] + dp[i].min(dp[i + 1]);
}
}
dp[0]
}
function minimumTotalMemo(triangle: number[][]): number {
const n = triangle.length;
const memo = new Map<number, number>(); // key: row * n + i
const best = (row: number, i: number): number => {
if (row === n - 1) return triangle[row][i];
const key = row * n + i;
const cached = memo.get(key);
if (cached !== undefined) return cached;
const val = triangle[row][i] + Math.min(best(row + 1, i), best(row + 1, i + 1));
memo.set(key, val);
return val;
};
return best(0, 0);
}
function minimumTotal(triangle: number[][]): number {
const dp = [...triangle[triangle.length - 1]];
for (let row = triangle.length - 2; row >= 0; row--) {
for (let i = 0; i <= row; i++) {
dp[i] = triangle[row][i] + Math.min(dp[i], dp[i + 1]);
}
}
return dp[0];
}
func minimumTotalMemo(triangle [][]int) int {
n := len(triangle)
const unset = math.MinInt // path sums are tiny; MinInt is a safe sentinel
memo := make([][]int, n)
for row := range memo {
memo[row] = make([]int, row+1)
for i := range memo[row] {
memo[row][i] = unset
}
}
var best func(row, i int) int
best = func(row, i int) int {
if row == n-1 {
return triangle[row][i]
}
if memo[row][i] == unset {
memo[row][i] = triangle[row][i] + min(best(row+1, i), best(row+1, i+1))
}
return memo[row][i]
}
return best(0, 0)
}
func minimumTotal(triangle [][]int) int {
n := len(triangle)
dp := make([]int, n)
copy(dp, triangle[n-1])
for row := n - 2; row >= 0; row-- {
for i := 0; i <= row; i++ {
dp[i] = triangle[row][i] + min(dp[i], dp[i+1])
}
}
return dp[0]
}
func minimumTotalMemo(_ triangle: [[Int]]) -> Int {
let n = triangle.count
var memo: [[Int?]] = (0..<n).map { [Int?](repeating: nil, count: $0 + 1) }
func best(_ row: Int, _ i: Int) -> Int {
if row == n - 1 { return triangle[row][i] }
if memo[row][i] == nil {
memo[row][i] = triangle[row][i] + min(best(row + 1, i), best(row + 1, i + 1))
}
return memo[row][i]!
}
return best(0, 0)
}
func minimumTotal(_ triangle: [[Int]]) -> Int {
var dp = triangle[triangle.count - 1]
for row in stride(from: triangle.count - 2, through: 0, by: -1) {
for i in 0...row {
dp[i] = triangle[row][i] + min(dp[i], dp[i + 1])
}
}
return dp[0]
}
46. Best Time to Buy and Sell Stock III
Hard · LC 123
Given daily stock prices, maximize profit using at most two buy-sell transactions, selling each share before buying the next. One pass tracks four running states, the best cash while holding the first share, after selling it, while holding the second, and after selling that, each relaxed in turn against the current price. The subtle point is updating each buy before its matching sell, which allows a trade to open and close on the same day, a harmless no-op that keeps the recurrence simple and the space constant.
dp[t][i] = best profit using <= t trades through day i. The max-diff trick folds the inner "best buy day" scan into the same pass — this is exactly the LC 188 solution with k pinned to 2. The generic tool works, but it drags k rows of machinery through a two-trade problem.
left[i] = best one-transaction profit within prices[:i+1]; right[i] = best within prices[i:]. Split the timeline at every day and take the best sum (sharing day i is safe — sell+rebuy is a no-op). Two elementary LC 121 sweeps instead of layered DP: roughly half the work of Approach 1 and far easier to reason about, still O(n) memory.
Track the best cash after each stage: buy1 (holding 1st share), sell1 (closed 1st trade), buy2, sell2. Each price relaxes all four; updating buys before sells lets both trades touch the same day, which never hurts (a same-day buy+sell nets zero). Drops the O(n) arrays.
left[i] = best one-transaction profit within prices[0..i]; right[i] = best within prices[i..]. Split the timeline at every day and take the best sum (sharing day i is safe — sell+rebuy is a no-op). Two elementary LC 121 sweeps, but two extra arrays.
Track the best cash after each stage: buy1 (holding 1st share), sell1 (closed 1st trade), buy2, sell2. Each price relaxes all four; updating buys before sells lets both trades share a day, which never hurts (a same-day buy+sell nets zero). Drops the O(n) arrays.
left[i] = best one-transaction profit within prices[0..=i]; right[i] = best within prices[i..]. Split the timeline at every day and take the best sum (sharing day i is safe — sell+rebuy is a no-op). Two elementary LC 121 sweeps, but two extra arrays.
Track the best cash after each stage: buy1 (holding 1st share), sell1 (closed 1st trade), buy2, sell2. Each price relaxes all four; updating buys before sells lets both trades share a day, which never hurts (a same-day buy+sell nets zero). Drops the O(n) arrays.
left[i] = best one-transaction profit within prices[0..i]; right[i] = best within prices[i..]. Split the timeline at every day and take the best sum (sharing day i is safe — sell+rebuy is a no-op). Two elementary LC 121 sweeps, but two extra arrays.
Track the best cash after each stage: buy1 (holding 1st share), sell1 (closed 1st trade), buy2, sell2. Each price relaxes all four; updating buys before sells lets both trades share a day, which never hurts (a same-day buy+sell nets zero). Drops the O(n) arrays.
left[i] = best one-transaction profit within prices[0..i]; right[i] = best within prices[i..]. Split the timeline at every day and take the best sum (sharing day i is safe — sell+rebuy is a no-op). Two elementary LC 121 sweeps, but two extra arrays.
Track the best cash after each stage: buy1 (holding 1st share), sell1 (closed 1st trade), buy2, sell2. Each price relaxes all four; updating buys before sells lets both trades share a day, which never hurts (a same-day buy+sell nets zero). Drops the O(n) arrays.
left[i] = best one-transaction profit within prices[0...i]; right[i] = best within prices[i...]. Split the timeline at every day and take the best sum (sharing day i is safe — sell+rebuy is a no-op). Two elementary LC 121 sweeps, but two extra arrays.
Track the best cash after each stage: buy1 (holding 1st share), sell1 (closed 1st trade), buy2, sell2. Each price relaxes all four; updating buys before sells lets both trades share a day, which never hurts (a same-day buy+sell nets zero). Drops the O(n) arrays.
def maxProfit_general_k(self, prices: list[int]) -> int:
n = len(prices)
if n < 2:
return 0
prev = [0] * n # dp for t-1 transactions
for _ in range(2):
curr = [0] * n
best_buy = -prices[0] # max(prev[j] - prices[j]) so far
for i in range(1, n):
curr[i] = max(curr[i - 1], prices[i] + best_buy)
best_buy = max(best_buy, prev[i] - prices[i])
prev = curr
return prev[-1]
def maxProfit_two_pass(self, prices: list[int]) -> int:
n = len(prices)
if n < 2:
return 0
left = [0] * n
lo = prices[0]
for i in range(1, n):
left[i] = max(left[i - 1], prices[i] - lo)
lo = min(lo, prices[i])
right = [0] * n
hi = prices[-1]
for i in range(n - 2, -1, -1):
right[i] = max(right[i + 1], hi - prices[i])
hi = max(hi, prices[i])
return max(left[i] + right[i] for i in range(n))
def maxProfit(self, prices: list[int]) -> int:
buy1 = buy2 = float("-inf")
sell1 = sell2 = 0
for p in prices:
buy1 = max(buy1, -p)
sell1 = max(sell1, buy1 + p)
buy2 = max(buy2, sell1 - p)
sell2 = max(sell2, buy2 + p)
return int(sell2)
int maxProfitTwoPass(std::vector<int>& prices) {
int n = static_cast<int>(prices.size());
if (n < 2) return 0;
std::vector<int> left(n, 0), right(n, 0);
int lo = prices[0];
for (int i = 1; i < n; ++i) {
left[i] = std::max(left[i - 1], prices[i] - lo);
lo = std::min(lo, prices[i]);
}
int hi = prices[n - 1];
for (int i = n - 2; i >= 0; --i) {
right[i] = std::max(right[i + 1], hi - prices[i]);
hi = std::max(hi, prices[i]);
}
int best = 0;
for (int i = 0; i < n; ++i) best = std::max(best, left[i] + right[i]);
return best;
}
int maxProfit(std::vector<int>& prices) {
int buy1 = INT_MIN, sell1 = 0, buy2 = INT_MIN, sell2 = 0;
for (int p : prices) {
buy1 = std::max(buy1, -p); // updated before use: no overflow
sell1 = std::max(sell1, buy1 + p);
buy2 = std::max(buy2, sell1 - p);
sell2 = std::max(sell2, buy2 + p);
}
return sell2;
}
pub fn max_profit_two_pass(prices: Vec<i32>) -> i32 {
let n = prices.len();
if n < 2 {
return 0;
}
let mut left = vec![0; n];
let mut lo = prices[0];
for i in 1..n {
left[i] = left[i - 1].max(prices[i] - lo);
lo = lo.min(prices[i]);
}
let mut right = vec![0; n];
let mut hi = prices[n - 1];
for i in (0..n - 1).rev() {
right[i] = right[i + 1].max(hi - prices[i]);
hi = hi.max(prices[i]);
}
(0..n).map(|i| left[i] + right[i]).max().unwrap()
}
pub fn max_profit(prices: Vec<i32>) -> i32 {
let (mut buy1, mut sell1, mut buy2, mut sell2) = (i32::MIN, 0, i32::MIN, 0);
for p in prices {
buy1 = buy1.max(-p); // updated before use: no overflow
sell1 = sell1.max(buy1 + p);
buy2 = buy2.max(sell1 - p);
sell2 = sell2.max(buy2 + p);
}
sell2
}
function maxProfitTwoPass(prices: number[]): number {
const n = prices.length;
if (n < 2) return 0;
const left = new Array<number>(n).fill(0);
let lo = prices[0];
for (let i = 1; i < n; i++) {
left[i] = Math.max(left[i - 1], prices[i] - lo);
lo = Math.min(lo, prices[i]);
}
const right = new Array<number>(n).fill(0);
let hi = prices[n - 1];
for (let i = n - 2; i >= 0; i--) {
right[i] = Math.max(right[i + 1], hi - prices[i]);
hi = Math.max(hi, prices[i]);
}
let best = 0;
for (let i = 0; i < n; i++) {
best = Math.max(best, left[i] + right[i]);
}
return best;
}
function maxProfit(prices: number[]): number {
let buy1 = -Infinity;
let sell1 = 0;
let buy2 = -Infinity;
let sell2 = 0;
for (const p of prices) {
buy1 = Math.max(buy1, -p);
sell1 = Math.max(sell1, buy1 + p);
buy2 = Math.max(buy2, sell1 - p);
sell2 = Math.max(sell2, buy2 + p);
}
return sell2;
}
func maxProfitTwoPass(prices []int) int {
n := len(prices)
if n < 2 {
return 0
}
left := make([]int, n)
lo := prices[0]
for i := 1; i < n; i++ {
left[i] = max(left[i-1], prices[i]-lo)
lo = min(lo, prices[i])
}
right := make([]int, n)
hi := prices[n-1]
for i := n - 2; i >= 0; i-- {
right[i] = max(right[i+1], hi-prices[i])
hi = max(hi, prices[i])
}
best := 0
for i := 0; i < n; i++ {
best = max(best, left[i]+right[i])
}
return best
}
func maxProfit(prices []int) int {
buy1, sell1 := math.MinInt, 0
buy2, sell2 := math.MinInt, 0
for _, p := range prices {
buy1 = max(buy1, -p) // updated before use: no overflow
sell1 = max(sell1, buy1+p)
buy2 = max(buy2, sell1-p)
sell2 = max(sell2, buy2+p)
}
return sell2
}
func maxProfitTwoPass(_ prices: [Int]) -> Int {
let n = prices.count
if n < 2 { return 0 }
var left = [Int](repeating: 0, count: n)
var lo = prices[0]
for i in 1..<n {
left[i] = max(left[i - 1], prices[i] - lo)
lo = min(lo, prices[i])
}
var right = [Int](repeating: 0, count: n)
var hi = prices[n - 1]
for i in stride(from: n - 2, through: 0, by: -1) {
right[i] = max(right[i + 1], hi - prices[i])
hi = max(hi, prices[i])
}
var best = 0
for i in 0..<n {
best = max(best, left[i] + right[i])
}
return best
}
func maxProfit(_ prices: [Int]) -> Int {
var buy1 = Int.min, sell1 = 0, buy2 = Int.min, sell2 = 0
for p in prices {
buy1 = max(buy1, -p) // updated before use: no overflow
sell1 = max(sell1, buy1 + p)
buy2 = max(buy2, sell1 - p)
sell2 = max(sell2, buy2 + p)
}
return sell2
}
47. Best Time to Buy and Sell Stock IV
Hard · LC 188
Given daily stock prices and a cap k, maximize profit using at most k buy-sell transactions. When k is at least half the number of days the cap can never bind, so simply bank every day-over-day rise; otherwise widen the two-transaction state machine to k stages, keeping per-stage buy and sell values where each price relaxes the best cash while holding, or after closing, that stage's share. The key observation is that a transaction spans at least two days, which is what justifies the greedy shortcut and keeps the state at O(k) instead of a full day-by-transaction table.
The most literal encoding of the decision tree (skip / buy / sell); easiest to derive live, but the memo grows with k even when k is so large the cap can never bind.
dp[t][i] = best profit using <= t trades through day i. Folding "best prev[j] - prices[j] so far" into the sweep kills the inner loop that would otherwise make this O(k * n^2). Iteration replaces the recursion stack, capping k at n/2 shrinks the table to two O(n) rows.
A transaction needs >= 2 days, so k >= n/2 means the cap never binds: just bank every day-over-day rise. Otherwise run the LC 123 state machine widened to k stages: buy[j] / sell[j] = best cash while holding (resp. after closing) the j-th share. Buys update before sells so a trade may open and close on the same day (a harmless no-op). State shrinks from O(n) per row to O(k) total.
The most literal encoding of the decision tree (skip / buy / sell); easiest to derive live, but the memo grows with k even when k is so large the cap can never bind (fine for LC's k <= 100).
A transaction needs >= 2 days, so k >= n/2 means the cap never binds: just bank every day-over-day rise. Otherwise run the LC 123 state machine widened to k stages: buy[j] / sell[j] = best cash while holding (resp. after closing) the j-th share; buys update before sells so a trade may open and close on the same day (a harmless no-op). State shrinks from O(n * k) to O(k).
The most literal encoding of the decision tree (skip / buy / sell); easiest to derive live, but the memo grows with k even when k is so large the cap can never bind (fine for LC's k <= 100).
A transaction needs >= 2 days, so k >= n/2 means the cap never binds: just bank every day-over-day rise. Otherwise run the LC 123 state machine widened to k stages: buy[j] / sell[j] = best cash while holding (resp. after closing) the j-th share; buys update before sells so a trade may open and close on the same day (a harmless no-op). State shrinks from O(n * k) to O(k).
The most literal encoding of the decision tree (skip / buy / sell); easiest to derive live, but the memo grows with k even when k is so large the cap can never bind (fine for LC's k <= 100).
A transaction needs >= 2 days, so k >= n/2 means the cap never binds: just bank every day-over-day rise. Otherwise run the LC 123 state machine widened to k stages: buy[j] / sell[j] = best cash while holding (resp. after closing) the j-th share; buys update before sells so a trade may open and close on the same day (a harmless no-op). State shrinks from O(n * k) to O(k).
The most literal encoding of the decision tree (skip / buy / sell); easiest to derive live, but the memo grows with k even when k is so large the cap can never bind (fine for LC's k <= 100).
A transaction needs >= 2 days, so k >= n/2 means the cap never binds: just bank every day-over-day rise. Otherwise run the LC 123 state machine widened to k stages: buy[j] / sell[j] = best cash while holding (resp. after closing) the j-th share; buys update before sells so a trade may open and close on the same day (a harmless no-op). State shrinks from O(n * k) to O(k).
The most literal encoding of the decision tree (skip / buy / sell); easiest to derive live, but the memo grows with k even when k is so large the cap can never bind (fine for LC's k <= 100).
A transaction needs >= 2 days, so k >= n/2 means the cap never binds: just bank every day-over-day rise. Otherwise run the LC 123 state machine widened to k stages: buy[j] / sell[j] = best cash while holding (resp. after closing) the j-th share; buys update before sells so a trade may open and close on the same day (a harmless no-op). State shrinks from O(n * k) to O(k).
def maxProfit_memo(self, k: int, prices: list[int]) -> int:
n = len(prices)
if k == 0 or n < 2:
return 0
memo: dict[tuple[int, int, bool], int] = {}
def best(i: int, left: int, holding: bool) -> int:
if i == n or (left == 0 and not holding):
return 0
key = (i, left, holding)
if key not in memo:
res = best(i + 1, left, holding) # do nothing today
if holding:
res = max(res, prices[i] + best(i + 1, left - 1, False))
elif left > 0:
res = max(res, -prices[i] + best(i + 1, left, True))
memo[key] = res
return memo[key]
return best(0, k, False)
def maxProfit_dp2d(self, k: int, prices: list[int]) -> int:
n = len(prices)
if k == 0 or n < 2:
return 0
prev = [0] * n
for _ in range(min(k, n // 2)):
curr = [0] * n
best_buy = -prices[0] # max(prev[j] - prices[j]) so far
for i in range(1, n):
curr[i] = max(curr[i - 1], prices[i] + best_buy)
best_buy = max(best_buy, prev[i] - prices[i])
prev = curr
return prev[-1]
def maxProfit(self, k: int, prices: list[int]) -> int:
n = len(prices)
if k == 0 or n < 2:
return 0
if k >= n // 2: # unlimited transactions in disguise
return sum(max(0, prices[i] - prices[i - 1]) for i in range(1, n))
buy = [float("-inf")] * (k + 1)
sell = [0] * (k + 1)
for p in prices:
for j in range(1, k + 1):
buy[j] = max(buy[j], sell[j - 1] - p)
sell[j] = max(sell[j], buy[j] + p)
return int(sell[k])
int maxProfitMemo(int k, std::vector<int>& prices) {
int n = static_cast<int>(prices.size());
if (k == 0 || n < 2) return 0;
// memo[(i * (k+1) + left) * 2 + holding]; profits are never
// negative, so -1 is a safe "unset" sentinel.
std::vector<int> memo(static_cast<size_t>(n) * (k + 1) * 2, -1);
auto best = [&](auto&& self, int i, int left, int holding) -> int {
if (i == n || (left == 0 && holding == 0)) return 0;
int& slot = memo[(static_cast<size_t>(i) * (k + 1) + left) * 2 +
holding];
if (slot < 0) {
int res = self(self, i + 1, left, holding); // do nothing today
if (holding) {
res = std::max(res, prices[i] + self(self, i + 1, left - 1, 0));
} else if (left > 0) {
res = std::max(res, -prices[i] + self(self, i + 1, left, 1));
}
slot = res;
}
return slot;
};
return best(best, 0, k, 0);
}
int maxProfit(int k, std::vector<int>& prices) {
int n = static_cast<int>(prices.size());
if (k == 0 || n < 2) return 0;
if (k >= n / 2) { // unlimited transactions in disguise
int profit = 0;
for (int i = 1; i < n; ++i) {
profit += std::max(0, prices[i] - prices[i - 1]);
}
return profit;
}
std::vector<int> buy(k + 1, INT_MIN), sell(k + 1, 0);
for (int p : prices) {
for (int j = 1; j <= k; ++j) {
buy[j] = std::max(buy[j], sell[j - 1] - p); // before use below
sell[j] = std::max(sell[j], buy[j] + p);
}
}
return sell[k];
}
pub fn max_profit_memo(k: i32, prices: Vec<i32>) -> i32 {
// memo[(i * (k+1) + left) * 2 + holding]; profits are never
// negative, so -1 is a safe "unset" sentinel.
fn best(prices: &[i32], memo: &mut [i32], k: usize, i: usize, left: usize, holding: usize) -> i32 {
if i == prices.len() || (left == 0 && holding == 0) {
return 0;
}
let idx = (i * (k + 1) + left) * 2 + holding;
if memo[idx] < 0 {
let mut res = best(prices, memo, k, i + 1, left, holding); // do nothing
if holding == 1 {
res = res.max(prices[i] + best(prices, memo, k, i + 1, left - 1, 0));
} else if left > 0 {
res = res.max(-prices[i] + best(prices, memo, k, i + 1, left, 1));
}
memo[idx] = res;
}
memo[idx]
}
let k = k as usize;
let n = prices.len();
if k == 0 || n < 2 {
return 0;
}
let mut memo = vec![-1i32; n * (k + 1) * 2];
best(&prices, &mut memo, k, 0, k, 0)
}
pub fn max_profit(k: i32, prices: Vec<i32>) -> i32 {
let k = k as usize;
let n = prices.len();
if k == 0 || n < 2 {
return 0;
}
if k >= n / 2 {
// unlimited transactions in disguise
return prices.windows(2).map(|w| (w[1] - w[0]).max(0)).sum();
}
let mut buy = vec![i32::MIN; k + 1];
let mut sell = vec![0; k + 1];
for p in prices {
for j in 1..=k {
buy[j] = buy[j].max(sell[j - 1] - p); // updated before use below
sell[j] = sell[j].max(buy[j] + p);
}
}
sell[k]
}
function maxProfitMemo(k: number, prices: number[]): number {
const n = prices.length;
if (k === 0 || n < 2) return 0;
// memo[(i * (k+1) + left) * 2 + holding]; profits are never negative,
// so -1 is a safe "unset" sentinel.
const memo = new Array<number>(n * (k + 1) * 2).fill(-1);
const best = (i: number, left: number, holding: number): number => {
if (i === n || (left === 0 && holding === 0)) return 0;
const idx = (i * (k + 1) + left) * 2 + holding;
if (memo[idx] < 0) {
let res = best(i + 1, left, holding); // do nothing today
if (holding === 1) {
res = Math.max(res, prices[i] + best(i + 1, left - 1, 0));
} else if (left > 0) {
res = Math.max(res, -prices[i] + best(i + 1, left, 1));
}
memo[idx] = res;
}
return memo[idx];
};
return best(0, k, 0);
}
function maxProfit(k: number, prices: number[]): number {
const n = prices.length;
if (k === 0 || n < 2) return 0;
if (k >= Math.floor(n / 2)) {
// unlimited transactions in disguise
let profit = 0;
for (let i = 1; i < n; i++) {
profit += Math.max(0, prices[i] - prices[i - 1]);
}
return profit;
}
const buy = new Array<number>(k + 1).fill(-Infinity);
const sell = new Array<number>(k + 1).fill(0);
for (const p of prices) {
for (let j = 1; j <= k; j++) {
buy[j] = Math.max(buy[j], sell[j - 1] - p);
sell[j] = Math.max(sell[j], buy[j] + p);
}
}
return sell[k];
}
func maxProfitMemo(k int, prices []int) int {
n := len(prices)
if k == 0 || n < 2 {
return 0
}
// memo[(i*(k+1)+left)*2+holding]; profits are never negative, so -1
// is a safe "unset" sentinel.
memo := make([]int, n*(k+1)*2)
for i := range memo {
memo[i] = -1
}
var best func(i, left, holding int) int
best = func(i, left, holding int) int {
if i == n || (left == 0 && holding == 0) {
return 0
}
idx := (i*(k+1)+left)*2 + holding
if memo[idx] < 0 {
res := best(i+1, left, holding) // do nothing today
if holding == 1 {
res = max(res, prices[i]+best(i+1, left-1, 0))
} else if left > 0 {
res = max(res, -prices[i]+best(i+1, left, 1))
}
memo[idx] = res
}
return memo[idx]
}
return best(0, k, 0)
}
func maxProfit(k int, prices []int) int {
n := len(prices)
if k == 0 || n < 2 {
return 0
}
if k >= n/2 { // unlimited transactions in disguise
profit := 0
for i := 1; i < n; i++ {
profit += max(0, prices[i]-prices[i-1])
}
return profit
}
buy := make([]int, k+1)
sell := make([]int, k+1)
for j := range buy {
buy[j] = math.MinInt
}
for _, p := range prices {
for j := 1; j <= k; j++ {
buy[j] = max(buy[j], sell[j-1]-p) // updated before use below
sell[j] = max(sell[j], buy[j]+p)
}
}
return sell[k]
}
func maxProfitMemo(_ k: Int, _ prices: [Int]) -> Int {
let n = prices.count
if k == 0 || n < 2 { return 0 }
// memo[(i * (k+1) + left) * 2 + holding]; profits are never
// negative, so -1 is a safe "unset" sentinel.
var memo = [Int](repeating: -1, count: n * (k + 1) * 2)
func best(_ i: Int, _ left: Int, _ holding: Int) -> Int {
if i == n || (left == 0 && holding == 0) { return 0 }
let idx = (i * (k + 1) + left) * 2 + holding
if memo[idx] < 0 {
var res = best(i + 1, left, holding) // do nothing today
if holding == 1 {
res = max(res, prices[i] + best(i + 1, left - 1, 0))
} else if left > 0 {
res = max(res, -prices[i] + best(i + 1, left, 1))
}
memo[idx] = res
}
return memo[idx]
}
return best(0, k, 0)
}
func maxProfit(_ k: Int, _ prices: [Int]) -> Int {
let n = prices.count
if k == 0 || n < 2 { return 0 }
if k >= n / 2 { // unlimited transactions in disguise
var profit = 0
for i in 1..<n {
profit += max(0, prices[i] - prices[i - 1])
}
return profit
}
var buy = [Int](repeating: Int.min, count: k + 1)
var sell = [Int](repeating: 0, count: k + 1)
for p in prices {
for j in 1...k {
buy[j] = max(buy[j], sell[j - 1] - p) // updated before use below
sell[j] = max(sell[j], buy[j] + p)
}
}
return sell[k]
}
48. Maximal Square
Medium · LC 221
Given a binary matrix of character zeros and ones, find the area of the largest square made entirely of ones. Dynamic programming records, for each cell, the side of the biggest square whose bottom-right corner sits there, which on a one-cell is one plus the minimum of the sides above, to the left, and diagonally up-left; a single rolling row replaces the full table since each cell looks only one row up. The pitfall in the rolling version is the diagonal neighbor, whose slot is overwritten as the sweep passes, so its old value must be stashed in a temporary before each update, and the final answer is the best side squared, not the side itself.
From each '1', extend the side while the new right column and bottom row are all '1'. Fine at LeetCode's 300x300 bound, but every cell re-scans work its neighbors already did.
side(i, j) = biggest square whose bottom-right corner is (i, j) = min(top, left, diagonal) + 1 on a '1' cell — the three neighbors cap how far the square can extend, so no re-scanning. Easy to draw on a whiteboard, but keeps the whole table around.
Same recurrence as Approach 2, but each cell only looks one row up: one row plus a saved diagonal replaces the full m x n table.
From each '1', extend the side while the new right column and bottom row are all '1'. Fine at LeetCode's 300x300 bound, but every cell re-scans work its neighbors already did.
side(i, j) = biggest square whose bottom-right corner is (i, j) = min(top, left, diagonal) + 1 on a '1' cell — the three neighbors cap how far the square can extend, so no re-scanning. Easy to draw on a whiteboard, but keeps the whole table around.
Same recurrence as Approach 2, but each cell only looks one row up: one row plus a saved diagonal replaces the full m x n table.
From each '1', extend the side while the new right column and bottom row are all '1'. Fine at LeetCode's 300x300 bound, but every cell re-scans work its neighbors already did.
side(i, j) = biggest square whose bottom-right corner is (i, j) = min(top, left, diagonal) + 1 on a '1' cell — the three neighbors cap how far the square can extend, so no re-scanning. Easy to draw on a whiteboard, but keeps the whole table around.
Same recurrence as Approach 2, but each cell only looks one row up: one row plus a saved diagonal replaces the full m x n table.
From each '1', extend the side while the new right column and bottom row are all '1'. Fine at LeetCode's 300x300 bound, but every cell re-scans work its neighbors already did.
side(i, j) = biggest square whose bottom-right corner is (i, j) = min(top, left, diagonal) + 1 on a '1' cell — the three neighbors cap how far the square can extend, so no re-scanning. Easy to draw on a whiteboard, but keeps the whole table around.
Same recurrence as Approach 2, but each cell only looks one row up: one row plus a saved diagonal replaces the full m x n table.
From each '1', extend the side while the new right column and bottom row are all '1'. Fine at LeetCode's 300x300 bound, but every cell re-scans work its neighbors already did.
side(i, j) = biggest square whose bottom-right corner is (i, j) = min(top, left, diagonal) + 1 on a '1' cell — the three neighbors cap how far the square can extend, so no re-scanning. Easy to draw on a whiteboard, but keeps the whole table around.
Same recurrence as Approach 2, but each cell only looks one row up: one row plus a saved diagonal replaces the full m x n table.
From each '1', extend the side while the new right column and bottom row are all '1'. Fine at LeetCode's 300x300 bound, but every cell re-scans work its neighbors already did.
side(i, j) = biggest square whose bottom-right corner is (i, j) = min(top, left, diagonal) + 1 on a '1' cell — the three neighbors cap how far the square can extend, so no re-scanning. Easy to draw on a whiteboard, but keeps the whole table around.
Same recurrence as Approach 2, but each cell only looks one row up: one row plus a saved diagonal replaces the full m x n table.
def maximalSquare_brute(self, matrix: list[list[str]]) -> int:
if not matrix or not matrix[0]:
return 0
m, n = len(matrix), len(matrix[0])
best = 0
for i in range(m):
for j in range(n):
if matrix[i][j] != "1":
continue
side = 1
while i + side < m and j + side < n:
edge_ok = all(matrix[i + side][j + t] == "1"
for t in range(side + 1))
edge_ok = edge_ok and all(matrix[i + t][j + side] == "1"
for t in range(side))
if not edge_ok:
break
side += 1
best = max(best, side)
return best * best
def maximalSquare_dp2d(self, matrix: list[list[str]]) -> int:
if not matrix or not matrix[0]:
return 0
m, n = len(matrix), len(matrix[0])
side = [[0] * (n + 1) for _ in range(m + 1)] # padded top/left border
best = 0
for i in range(1, m + 1):
for j in range(1, n + 1):
if matrix[i - 1][j - 1] == "1":
side[i][j] = min(side[i - 1][j], side[i][j - 1],
side[i - 1][j - 1]) + 1
best = max(best, side[i][j])
return best * best
def maximalSquare(self, matrix: list[list[str]]) -> int:
if not matrix or not matrix[0]:
return 0
cols = len(matrix[0])
dp = [0] * (cols + 1) # dp[j+1] = side ending at (prev row, j)
best = 0
for row in matrix:
diag = 0 # dp value of (i-1, j-1) before this row overwrote it
for j, cell in enumerate(row):
tmp = dp[j + 1]
if cell == "1":
dp[j + 1] = min(dp[j], dp[j + 1], diag) + 1
best = max(best, dp[j + 1])
else:
dp[j + 1] = 0
diag = tmp
return best * best
int maximalSquareBruteForce(std::vector<std::vector<char>>& matrix) {
if (matrix.empty() || matrix[0].empty()) return 0;
int m = static_cast<int>(matrix.size());
int n = static_cast<int>(matrix[0].size());
int best = 0;
for (int i = 0; i < m; ++i) {
for (int j = 0; j < n; ++j) {
if (matrix[i][j] != '1') continue;
int side = 1;
while (i + side < m && j + side < n) {
bool edgeOk = true;
for (int t = 0; t <= side && edgeOk; ++t) {
if (matrix[i + side][j + t] != '1') edgeOk = false;
}
for (int t = 0; t < side && edgeOk; ++t) {
if (matrix[i + t][j + side] != '1') edgeOk = false;
}
if (!edgeOk) break;
++side;
}
best = std::max(best, side);
}
}
return best * best;
}
int maximalSquareFullTable(std::vector<std::vector<char>>& matrix) {
if (matrix.empty() || matrix[0].empty()) return 0;
int m = static_cast<int>(matrix.size());
int n = static_cast<int>(matrix[0].size());
// padded top/left border of zeros
std::vector<std::vector<int>> side(m + 1, std::vector<int>(n + 1, 0));
int best = 0;
for (int i = 1; i <= m; ++i) {
for (int j = 1; j <= n; ++j) {
if (matrix[i - 1][j - 1] == '1') {
side[i][j] = std::min({side[i - 1][j], side[i][j - 1],
side[i - 1][j - 1]}) + 1;
best = std::max(best, side[i][j]);
}
}
}
return best * best;
}
int maximalSquare(std::vector<std::vector<char>>& matrix) {
if (matrix.empty() || matrix[0].empty()) return 0;
int cols = static_cast<int>(matrix[0].size());
std::vector<int> dp(cols + 1, 0); // dp[j+1] = side ending at (i-1, j)
int best = 0;
for (const auto& row : matrix) {
int diag = 0; // side at (i-1, j-1) before this row overwrote it
for (int j = 0; j < cols; ++j) {
int tmp = dp[j + 1];
if (row[j] == '1') {
dp[j + 1] = std::min({dp[j], dp[j + 1], diag}) + 1;
best = std::max(best, dp[j + 1]);
} else {
dp[j + 1] = 0;
}
diag = tmp;
}
}
return best * best;
}
pub fn maximal_square_brute_force(matrix: Vec<Vec<char>>) -> i32 {
if matrix.is_empty() || matrix[0].is_empty() {
return 0;
}
let (m, n) = (matrix.len(), matrix[0].len());
let mut best = 0usize;
for i in 0..m {
for j in 0..n {
if matrix[i][j] != '1' {
continue;
}
let mut side = 1usize;
while i + side < m && j + side < n {
let row_ok = (0..=side).all(|t| matrix[i + side][j + t] == '1');
let col_ok = (0..side).all(|t| matrix[i + t][j + side] == '1');
if !(row_ok && col_ok) {
break;
}
side += 1;
}
best = best.max(side);
}
}
(best * best) as i32
}
pub fn maximal_square_full_table(matrix: Vec<Vec<char>>) -> i32 {
if matrix.is_empty() || matrix[0].is_empty() {
return 0;
}
let (m, n) = (matrix.len(), matrix[0].len());
let mut side = vec![vec![0i32; n + 1]; m + 1]; // padded top/left border
let mut best = 0;
for i in 1..=m {
for j in 1..=n {
if matrix[i - 1][j - 1] == '1' {
side[i][j] =
side[i - 1][j].min(side[i][j - 1]).min(side[i - 1][j - 1]) + 1;
best = best.max(side[i][j]);
}
}
}
best * best
}
pub fn maximal_square(matrix: Vec<Vec<char>>) -> i32 {
if matrix.is_empty() || matrix[0].is_empty() {
return 0;
}
let cols = matrix[0].len();
let mut dp = vec![0i32; cols + 1]; // dp[j+1] = side ending at (i-1, j)
let mut best = 0;
for row in &matrix {
let mut diag = 0; // side at (i-1, j-1) before this row overwrote it
for j in 0..cols {
let tmp = dp[j + 1];
if row[j] == '1' {
dp[j + 1] = dp[j].min(dp[j + 1]).min(diag) + 1;
best = best.max(dp[j + 1]);
} else {
dp[j + 1] = 0;
}
diag = tmp;
}
}
best * best
}
function maximalSquareBruteForce(matrix: string[][]): number {
if (matrix.length === 0 || matrix[0].length === 0) return 0;
const m = matrix.length;
const n = matrix[0].length;
let best = 0;
for (let i = 0; i < m; i++) {
for (let j = 0; j < n; j++) {
if (matrix[i][j] !== "1") continue;
let side = 1;
while (i + side < m && j + side < n) {
let edgeOk = true;
for (let t = 0; t <= side && edgeOk; t++) {
if (matrix[i + side][j + t] !== "1") edgeOk = false;
}
for (let t = 0; t < side && edgeOk; t++) {
if (matrix[i + t][j + side] !== "1") edgeOk = false;
}
if (!edgeOk) break;
side++;
}
best = Math.max(best, side);
}
}
return best * best;
}
function maximalSquareFullTable(matrix: string[][]): number {
if (matrix.length === 0 || matrix[0].length === 0) return 0;
const m = matrix.length;
const n = matrix[0].length;
// padded top/left border of zeros
const side: number[][] = Array.from({ length: m + 1 }, () =>
new Array<number>(n + 1).fill(0),
);
let best = 0;
for (let i = 1; i <= m; i++) {
for (let j = 1; j <= n; j++) {
if (matrix[i - 1][j - 1] === "1") {
side[i][j] = Math.min(side[i - 1][j], side[i][j - 1], side[i - 1][j - 1]) + 1;
best = Math.max(best, side[i][j]);
}
}
}
return best * best;
}
function maximalSquare(matrix: string[][]): number {
if (matrix.length === 0 || matrix[0].length === 0) return 0;
const cols = matrix[0].length;
const dp = new Array<number>(cols + 1).fill(0); // dp[j+1]: side at (i-1, j)
let best = 0;
for (const row of matrix) {
let diag = 0; // side at (i-1, j-1) before this row overwrote it
for (let j = 0; j < cols; j++) {
const tmp = dp[j + 1];
if (row[j] === "1") {
dp[j + 1] = Math.min(dp[j], dp[j + 1], diag) + 1;
best = Math.max(best, dp[j + 1]);
} else {
dp[j + 1] = 0;
}
diag = tmp;
}
}
return best * best;
}
func maximalSquareBruteForce(matrix [][]byte) int {
if len(matrix) == 0 || len(matrix[0]) == 0 {
return 0
}
m, n := len(matrix), len(matrix[0])
best := 0
for i := 0; i < m; i++ {
for j := 0; j < n; j++ {
if matrix[i][j] != '1' {
continue
}
side := 1
for i+side < m && j+side < n {
edgeOk := true
for t := 0; t <= side && edgeOk; t++ {
if matrix[i+side][j+t] != '1' {
edgeOk = false
}
}
for t := 0; t < side && edgeOk; t++ {
if matrix[i+t][j+side] != '1' {
edgeOk = false
}
}
if !edgeOk {
break
}
side++
}
best = max(best, side)
}
}
return best * best
}
func maximalSquareFullTable(matrix [][]byte) int {
if len(matrix) == 0 || len(matrix[0]) == 0 {
return 0
}
m, n := len(matrix), len(matrix[0])
side := make([][]int, m+1) // padded top/left border of zeros
for i := range side {
side[i] = make([]int, n+1)
}
best := 0
for i := 1; i <= m; i++ {
for j := 1; j <= n; j++ {
if matrix[i-1][j-1] == '1' {
side[i][j] = min(side[i-1][j], side[i][j-1], side[i-1][j-1]) + 1
best = max(best, side[i][j])
}
}
}
return best * best
}
func maximalSquare(matrix [][]byte) int {
if len(matrix) == 0 || len(matrix[0]) == 0 {
return 0
}
cols := len(matrix[0])
dp := make([]int, cols+1) // dp[j+1] = side ending at (i-1, j)
best := 0
for _, row := range matrix {
diag := 0 // side at (i-1, j-1) before this row overwrote it
for j := 0; j < cols; j++ {
tmp := dp[j+1]
if row[j] == '1' {
dp[j+1] = min(dp[j], dp[j+1], diag) + 1
best = max(best, dp[j+1])
} else {
dp[j+1] = 0
}
diag = tmp
}
}
return best * best
}
func maximalSquareBruteForce(_ matrix: [[Character]]) -> Int {
if matrix.isEmpty || matrix[0].isEmpty { return 0 }
let m = matrix.count, n = matrix[0].count
var best = 0
for i in 0..<m {
for j in 0..<n {
if matrix[i][j] != "1" { continue }
var side = 1
while i + side < m && j + side < n {
var edgeOk = true
for t in 0...side {
if matrix[i + side][j + t] != "1" {
edgeOk = false
break
}
}
if edgeOk {
for t in 0..<side {
if matrix[i + t][j + side] != "1" {
edgeOk = false
break
}
}
}
if !edgeOk { break }
side += 1
}
best = max(best, side)
}
}
return best * best
}
func maximalSquareFullTable(_ matrix: [[Character]]) -> Int {
if matrix.isEmpty || matrix[0].isEmpty { return 0 }
let m = matrix.count, n = matrix[0].count
// padded top/left border of zeros
var side = [[Int]](repeating: [Int](repeating: 0, count: n + 1), count: m + 1)
var best = 0
for i in 1...m {
for j in 1...n {
if matrix[i - 1][j - 1] == "1" {
side[i][j] = min(side[i - 1][j], side[i][j - 1], side[i - 1][j - 1]) + 1
best = max(best, side[i][j])
}
}
}
return best * best
}
func maximalSquare(_ matrix: [[Character]]) -> Int {
if matrix.isEmpty || matrix[0].isEmpty { return 0 }
let cols = matrix[0].count
var dp = [Int](repeating: 0, count: cols + 1) // dp[j+1]: side at (i-1, j)
var best = 0
for row in matrix {
var diag = 0 // side at (i-1, j-1) before this row overwrote it
for j in 0..<cols {
let tmp = dp[j + 1]
if row[j] == "1" {
dp[j + 1] = min(dp[j], dp[j + 1], diag) + 1
best = max(best, dp[j + 1])
} else {
dp[j + 1] = 0
}
diag = tmp
}
}
return best * best
}