Go Guide

Interview prep · Lesson 48 of 62 · about 50 min

In-place array patterns

The O(1)-space tricks interviewers ask for in follow-ups: cyclic sort for values in 1..n, marking seen values by negating, XOR to find a missing number, Floyd's cycle detection to find a duplicate, Boyer-Moore majority vote, and next permutation.

On this page · 12 sections
  1. 01Cyclic sort: every value to its home
  2. 02Marking with signs
  3. 03XOR: the missing number
  4. 04Floyd: a duplicate without touching the array
  5. 05Boyer-Moore majority vote
  6. 06Next permutation
  7. 07The full program
  8. 08Complexity
  9. 09Recognizing the pattern
  10. 10Summary
  11. 11Check your understanding
  12. 12Practice in Go

Cyclic sort: every value to its home#

Whenever the input is "n numbers in the range 1..n" (or 0..n), value v has an obvious home: index v − 1. Walk the array; while the current value is not home and its home doesn't already hold a copy, swap it there. When the walk finishes, every slot that holds the wrong value marks a missing number (the index) and a duplicate (the value).

Run the trace

cyclic_trace.go
Go
package main
 
import "fmt"
 
func main() {
	nums := []int{4, 3, 2, 7, 8, 2, 3, 1} // values 1..n, some repeated, some missing
	i := 0
	for i < len(nums) {
		want := nums[i] - 1 // the index where nums[i] belongs
		if nums[i] != nums[want] {
			nums[i], nums[want] = nums[want], nums[i]
			fmt.Printf("i=%d swap %d into index %d -> %v\n", i, nums[want], want, nums)
		} else {
			i++ // in place, or its spot already holds a copy
		}
	}
	for i, n := range nums {
		if n != i+1 {
			fmt.Printf("index %d holds %d: %d is missing, %d is a duplicate\n", i, n, i+1, n)
		}
	}
}
Output
i=0 swap 4 into index 3 -> [7 3 2 4 8 2 3 1]
i=0 swap 7 into index 6 -> [3 3 2 4 8 2 7 1]
i=0 swap 3 into index 2 -> [2 3 3 4 8 2 7 1]
i=0 swap 2 into index 1 -> [3 2 3 4 8 2 7 1]
i=4 swap 8 into index 7 -> [3 2 3 4 1 2 7 8]
i=4 swap 1 into index 0 -> [1 2 3 4 3 2 7 8]
index 4 holds 3: 5 is missing, 3 is a duplicate
index 5 holds 2: 6 is missing, 2 is a duplicate

i stays at 0 for four swaps, which looks quadratic. It isn't: each swap puts one value in its final place forever, so there are at most n swaps over the whole run.

First Missing Positive

The hard version adds zeros, negatives and huge values. With n slots, the answer is at most n + 1, so values outside 1..n can never be the answer and can stay wherever they are. Add a range check to the swap guard; afterwards, the first index i with nums[i] != i+1 gives the answer i + 1.

Marking with signs#

If you may modify the array but don't want to move values, record "I have seen v" by negating nums[v−1]. Read values with abs, because earlier steps may have already negated them. Seeing a negative slot means v is a duplicate. After one pass, any slot still positive marks a missing value.

XOR: the missing number#

x ^ x == 0 and x ^ 0 == x, and XOR is order-independent. XOR every index 0..n with every value: each present number appears twice and cancels, and only the missing one remains. This is the same O(1)-space trick as Single Number in Bit manipulation. The sum formula n(n+1)/2 − sum also works, but in other languages it can overflow.

Floyd: a duplicate without touching the array#

Find the Duplicate Number forbids modifying the array, which rules out cyclic sort and sign marking. Treat every index as a node with an edge i → nums[i]. Values are in 1..n and there are n + 1 indices, so following edges from 0 must eventually loop, and the duplicate value is the entrance of the loop because two different indices point to it.

Drawing diagram…

For [1, 3, 4, 2, 2], both index 3 and index 4 point to node 2, which is where the cycle starts. Floyd's algorithm from Linked lists finds it: the slow and fast pointers meet somewhere inside the cycle; reset one to the start and step both once at a time, and they meet at the entrance.

Boyer-Moore majority vote#

If one value appears more than n/2 times, pair each of its copies with a different value and cancel them; at least one copy survives. As code, that is one candidate and one counter. When the counter is 0, adopt the current value; a match adds a vote, anything else removes one.

n2211122
candidate2222112
votes1210101

The generalization: at most k − 1 values can appear more than n/k times, so keep k − 1 candidates. For "more than n/3" that's two candidates. A different value cancels one vote from each, and a second pass verifies the survivors, because nothing guarantees they are real majorities.

Next permutation#

To get the next permutation in lexicographic order, change as little as possible, as far right as possible:

  1. From the right, find the first index i with nums[i] < nums[i+1]. Everything after i is descending, which means it is already the largest arrangement of that suffix.
  2. If there is such an i, swap nums[i] with the rightmost value larger than it.
  3. Reverse the suffix after i, so it goes from largest to smallest arrangement. If there was no i, the whole array reverses and wraps around to the first permutation.

The full program#

in_place.go
Go
package main
 
import (
	"fmt"
	"slices"
)
 
// cyclicSort places each value v (1..n) at index v-1. Each swap fixes one value, so it is O(n).
func cyclicSort(nums []int) {
	for i := 0; i < len(nums); {
		want := nums[i] - 1
		if want >= 0 && want < len(nums) && nums[i] != nums[want] {
			nums[i], nums[want] = nums[want], nums[i]
		} else {
			i++
		}
	}
}
 
// findDisappeared returns every value in 1..n that is missing.
func findDisappeared(nums []int) []int {
	cyclicSort(nums)
	var out []int
	for i, n := range nums {
		if n != i+1 {
			out = append(out, i+1)
		}
	}
	return out
}
 
// firstMissingPositive ignores values outside 1..n; the first slot that is wrong is the answer.
func firstMissingPositive(nums []int) int {
	cyclicSort(nums)
	for i, n := range nums {
		if n != i+1 {
			return i + 1
		}
	}
	return len(nums) + 1
}
 
// missingNumber in 0..n: XOR every index and value; pairs cancel, the missing one remains.
func missingNumber(nums []int) int {
	x := len(nums)
	for i, n := range nums {
		x ^= i ^ n
	}
	return x
}
 
// findDuplicate without modifying nums: values are "next" pointers, so a duplicate is a cycle entrance.
func findDuplicate(nums []int) int {
	slow, fast := nums[0], nums[nums[0]]
	for slow != fast {
		slow, fast = nums[slow], nums[nums[fast]]
	}
	slow = 0
	for slow != fast {
		slow, fast = nums[slow], nums[fast]
	}
	return slow
}
 
// findDuplicates marks a value v as seen by negating nums[v-1].
func findDuplicates(nums []int) []int {
	var out []int
	for _, n := range nums {
		v := abs(n)
		if nums[v-1] < 0 {
			out = append(out, v)
		} else {
			nums[v-1] = -nums[v-1]
		}
	}
	return out
}
 
func abs(x int) int {
	if x < 0 {
		return -x
	}
	return x
}
 
// majority: Boyer-Moore vote. A different value cancels one vote; the majority survives.
func majority(nums []int) int {
	cand, votes := 0, 0
	for _, n := range nums {
		if votes == 0 {
			cand = n
		}
		if n == cand {
			votes++
		} else {
			votes--
		}
	}
	return cand
}
 
// majorityThird: at most two values can appear more than n/3 times. Keep two candidates, then verify.
func majorityThird(nums []int) []int {
	c1, c2, v1, v2 := 0, 1, 0, 0 // distinct starting candidates
	for _, n := range nums {
		switch {
		case n == c1:
			v1++
		case n == c2:
			v2++
		case v1 == 0:
			c1, v1 = n, 1
		case v2 == 0:
			c2, v2 = n, 1
		default:
			v1--
			v2--
		}
	}
	var out []int
	for _, c := range []int{c1, c2} {
		count := 0
		for _, n := range nums {
			if n == c {
				count++
			}
		}
		if count > len(nums)/3 {
			out = append(out, c)
		}
	}
	return out
}
 
// nextPermutation rearranges nums into the next larger order, or the smallest if it is the last.
func nextPermutation(nums []int) {
	i := len(nums) - 2
	for i >= 0 && nums[i] >= nums[i+1] { // find the rightmost ascent
		i--
	}
	if i >= 0 {
		j := len(nums) - 1
		for nums[j] <= nums[i] { // rightmost value larger than nums[i]
			j--
		}
		nums[i], nums[j] = nums[j], nums[i]
	}
	slices.Reverse(nums[i+1:]) // the suffix was descending; make it ascending
}
 
func main() {
	fmt.Println(findDisappeared([]int{4, 3, 2, 7, 8, 2, 3, 1}))
	fmt.Println(firstMissingPositive([]int{3, 4, -1, 1}), firstMissingPositive([]int{7, 8, 9}), firstMissingPositive([]int{1, 2, 0}))
	fmt.Println(missingNumber([]int{3, 0, 1}), missingNumber([]int{9, 6, 4, 2, 3, 5, 7, 0, 1}))
	fmt.Println(findDuplicate([]int{1, 3, 4, 2, 2}), findDuplicate([]int{3, 1, 3, 4, 2}))
	fmt.Println(findDuplicates([]int{4, 3, 2, 7, 8, 2, 3, 1}))
	fmt.Println(majority([]int{2, 2, 1, 1, 1, 2, 2}), majorityThird([]int{3, 2, 3}), majorityThird([]int{1, 2}))
	for _, p := range [][]int{{1, 2, 3}, {1, 3, 2}, {3, 2, 1}, {1, 5, 8, 4, 7, 6, 5, 3, 1}} {
		before := slices.Clone(p)
		nextPermutation(p)
		fmt.Println(before, "->", p)
	}
}
Output
[5 6]
2 1 3
2 8
2 3
[2 3]
2 [3] [2 1]
[1 2 3] -> [1 3 2]
[1 3 2] -> [2 1 3]
[3 2 1] -> [1 2 3]
[1 5 8 4 7 6 5 3 1] -> [1 5 8 5 1 3 4 6 7]

Complexity#

ProblemTimeSpaceModifies input
cyclic sort (missing, duplicates)O(n)O(1)yes, reorders
first missing positiveO(n)O(1)yes
sign markingO(n)O(1)yes, restorable
missing number by XORO(n)O(1)no
find duplicate by FloydO(n)O(1)no
majority vote (n/2 or n/3)O(n)O(1)no
next permutationO(n)O(1)yes

Recognizing the pattern#

Look for "values in the range 1 to n" (or 0 to n) together with "missing," "duplicate," or "O(1) extra space". That combination almost always means cyclic sort or sign marking. "Without modifying the array" rules those out and points to Floyd or XOR. "Appears more than n/k times" is a vote counter. Next permutation shows up both on its own and as the core of "the next greater number with the same digits."

Summary#

  • Values in 1..n name indices: cyclic sort sends each to v − 1 in O(n) total swaps.
  • After cyclic sort, a wrong slot gives a missing value (the index + 1) and a duplicate (the value it holds).
  • Negating nums[v−1] marks v as seen without moving anything.
  • XOR of all indices and values leaves the missing number.
  • Treating i → nums[i] as a linked list turns "find the duplicate" into Floyd's cycle entrance.
  • Boyer-Moore keeps k − 1 candidates for "more than n/k"; verify unless a majority is guaranteed.
  • Next permutation: rightmost ascent, swap with the rightmost larger value, reverse the suffix.

Check your understanding

Answer before scrolling back up. If you miss one, reread the matching section, then try again.

  1. Q1.Cyclic sort has a loop inside a loop. Why is it still O(n)?

  2. Q2.First Missing Positive: what do you do with 0, negatives, and values larger than n?

  3. Q3.Find the Duplicate Number must not modify the array and must use O(1) space. Which idea works?

  4. Q4.Boyer-Moore majority vote returns a candidate. When must you verify it with a second pass?

  5. Q5.Next permutation of [1, 3, 2] is:

Practice in Go

Write a table test first, then the solution. On LeetCode, choose Go as the language. Open a hint only after 15 minutes stuck.

Finished this lesson?

Mark it done to track your progress. It is saved in this browser only.