Learn/DSA Patterns
DSA PatternsArrays & Two Pointersmedium11 min read

In-Place Array Manipulation — Negation Marking

When you need O(1) extra space but must remember which values you've visited, the array itself is your notebook. Flip a cell negative to mark it — and the original value is still recoverable with abs(). A single elegant trick unlocks three classic interview problems.

#arrays#in-place#negation#marking#duplicates#visited#interview
Table of contents

Before we start

By the end of this page you will be able to:

  • See why flipping a number negative is a reversible "sticky note" on the array itself.
  • Explain out loud why this always uses O(1) extra space even when tracking many visited positions.
  • Recognise this trick in "find duplicates," "find the disappeared," and "find the corrupt pair" problems when the constraint is O(1) space.

Stop at every Pause & Think box.


Picture this first (no code yet)

A real-life story

A hotel inspector must check every room in a building numbered 1 to N. She has no notepad — the rules say she can't carry anything. But she can flip the room's own keycard upside-down to mark it as "inspected."

Here's the clever part: a flipped card still shows the room number — it just looks different. She can always read the original number by flipping it back. And at any point she can glance at a card and know immediately whether that room has been inspected (upside down) or not (right-side up).

After her rounds, she walks through the corridor: every room with a right-side-up card was never visited — that room's number is missing from her route.

The hotel inspector's trick — mark by flipping, read original with abs() — is the negation marking pattern.


The actual problem

You are given an array of length N where every value is in [1, N]. Some values appear twice, some appear once. Find all values that appear twice, in O(n) time and O(1) extra space.

input  : [4, 3, 2, 7, 8, 2, 3, 1]
output : [2, 3]

No hash set. No extra array. Just the input itself.


First, the slow way (so you feel the pain)

With a hash set: O(n) time, O(n) space — straightforward but violates the space constraint.

seen = set()
duplicates = []
for x in arr:
    if x in seen:
        duplicates.append(x)
    else:
        seen.add(x)

Sort first: O(n log n) time — also violates the constraint that we shouldn't modify order (and it's slower).

The negation approach is O(n) time and O(1) space — the best possible for both simultaneously.


The turning point

The values in the array are in [1, N], and the indices are 0 to N-1. Value v and index v - 1 are linked.

The key question: can we use the array itself to record "I have seen value v" — without using extra memory?

Pause & think

You want to mark "I have seen value 3" inside the array [4, 3, 2, 7, 8, 2, 3, 1] without any extra memory. The array already contains numbers. What could you do to the number at index 3 - 1 = 2 that is reversible (you can still get the original value back) but visually distinct (you can tell "marked" from "unmarked")?

Flip it negative. Index 2 holds the value 2. After marking: -2. When you later read index 2, abs(-2) = 2 — the original value is perfectly preserved. The sign is your sticky note.

When you encounter value v:

  1. Compute index idx = abs(v) - 1 (use abs because a previous step might have already flipped v itself).
  2. Look at arr[idx]. If it's already negative — someone visited index idx before, meaning value idx + 1 was seen twice. Duplicate found.
  3. If it's positive — first time visiting. Flip: arr[idx] = -arr[idx].

The one idea to remember

The entire pattern in one sentence

Use the sign of arr[v-1] as a boolean flag for value v — flip it negative when you visit v, and check if it's already negative to detect a repeat — because abs() always recovers the original value.


Watch it happen, frame by frame

Array: [4, 3, 2, 7, 8, 2, 3, 1]. Indices 0–7.

i=0: value=4. idx=3. arr[3]=7>0mark: arr[3]=-7.
     Array: [4, 3, 2,-7, 8, 2, 3, 1]

i=1: value=3. idx=2. arr[2]=2>0mark: arr[2]=-2.
     Array: [4, 3,-2,-7, 8, 2, 3, 1]

i=2: value=-2. abs(-2)=2. idx=1. arr[1]=3>0mark: arr[1]=-3.
     Array: [4,-3,-2,-7, 8, 2, 3, 1]

i=3: value=-7. abs(-7)=7. idx=6. arr[6]=3>0mark: arr[6]=-3.
     Array: [4,-3,-2,-7, 8, 2,-3, 1]

i=4: value=8. idx=7. arr[7]=1>0mark: arr[7]=-1.
     Array: [4,-3,-2,-7, 8, 2,-3,-1]

i=5: value=2. idx=1. arr[1]=-3<0ALREADY MARKED. 2 is a DUPLICATEi=6: value=-3. abs(-3)=3. idx=2. arr[2]=-2<0ALREADY MARKED. 3 is a DUPLICATEi=7: value=1. idx=0. arr[0]=4>0mark: arr[0]=-4.
     Array: [-4,-3,-2,-7, 8, 2,-3,-1]

Result: [2, 3]

Pause & think

Cover the trace below. Try it on [1, 1, 2]. Which index gets marked when you encounter the first 1? What happens when you encounter the second 1?

Check your trace
i=0: value=1. idx=0. arr[0]=1>0mark: arr[0]=-1.
     Array: [-1, 1, 2]

i=1: value=1. idx=0. arr[0]=-1<0ALREADY MARKED. 1 is a DUPLICATEi=2: value=2. idx=1. arr[1]=1>0mark: arr[1]=-2.
     Array: [-1,-2, 2]

Result: [1]

Now, the code — line by line

def findDuplicates(nums):
    duplicates = []

    for v in nums:
        idx = abs(v) - 1           # value v maps to index v-1; abs() handles already-flipped values

        if nums[idx] < 0:          # already negative = already visited = DUPLICATE
            duplicates.append(idx + 1)
        else:
            nums[idx] = -nums[idx] # first visit — flip negative as a marker

    return duplicates

Every line mapped to the hotel story:

  • idx = abs(v) - 1 — read the room number from the keycard (abs handles already-upside-down cards).
  • if nums[idx] < 0: — is the keycard for room idx+1 already upside-down (visited)?
  • duplicates.append(idx + 1) — yes: this room was already inspected → the value idx + 1 appeared before.
  • nums[idx] = -nums[idx] — no: first visit → flip the keycard upside-down.

Why is this always correct?

The mapping is bijective: value v ↔ index v-1. For each value v seen, we write a marker at a unique index. Two values never write to the same index (unless they're duplicates of each other). Reading abs(nums[idx]) always recovers the original value regardless of how many times the sign has been flipped.

Time: O(n) — one pass through the array. Space: O(1) — no extra data structures; we use the input array itself as the marking board.


When should I reach for this? (the trigger list)

Reach for negation marking when:

  • Values are in the range [1, N] (so they map cleanly to indices [0, N-1]).
  • You need to detect duplicates or missing values in O(1) extra space.
  • A hash set would solve it but the problem forbids O(n) extra memory.
  • The problem says "do not use extra space" or "use the input array itself."
  • You already know Cyclic Sort (1.12) — negation marking is its complement: Cyclic Sort moves values to their homes; negation marking marks homes in-place without moving values.

The same trick in three disguises

Disguise 1 — Find All Numbers Disappeared (LC #448)

Same array [1,N], but now report which values are missing (never appeared, so their corresponding index was never marked negative).

def findDisappearedNumbers(nums):
    for v in nums:
        idx = abs(v) - 1
        if nums[idx] > 0:
            nums[idx] = -nums[idx]   # mark visited

    # Any index still positive → its value (idx+1) never appeared
    return [i + 1 for i in range(len(nums)) if nums[i] > 0]

Same marking pass. Different final scan: look for positive indices (unvisited), not negative ones (visited twice).

Disguise 2 — Set Mismatch (LC #645): missing and duplicate together

One value appears twice; one value is missing. One pass finds the duplicate; the second scan finds the unvisited index.

def findErrorNums(nums):
    duplicate = -1
    for v in nums:
        idx = abs(v) - 1
        if nums[idx] < 0:
            duplicate = idx + 1      # already negative → duplicate
        else:
            nums[idx] = -nums[idx]

    missing = next(i + 1 for i in range(len(nums)) if nums[i] > 0)
    return [duplicate, missing]

Disguise 3 — First Missing Positive (LC #41) with negation

Values outside [1, N] can't be used as indices — clean them out first by setting to N+1 (a sentinel), then use negation marking for the valid values.

def firstMissingPositive(nums):
    n = len(nums)
    # Step 1: replace out-of-range values with a sentinel
    for i in range(n):
        if nums[i] <= 0 or nums[i] > n:
            nums[i] = n + 1

    # Step 2: mark using negation
    for v in nums:
        idx = abs(v) - 1
        if idx < n and nums[idx] > 0:
            nums[idx] = -nums[idx]

    # Step 3: first unvisited index → first missing positive
    for i in range(n):
        if nums[i] > 0:
            return i + 1
    return n + 1
Level up — Why sentinel N+1 instead of 0 or -1?

The algorithm uses the sign of array values as markers, so 0 is problematic (we can't distinguish positive-0 from negative-0 in most languages — there's only one zero). We also don't want to accidentally use a value like -1 as a legitimate index. Setting out-of-range values to n+1 (a large positive) makes them harmless: their "index" would be n+1-1 = n, which is out of bounds (idx < n guard catches it).


Traps that catch beginners

Watch out for these

  • Forgetting abs(v) when computing the index. If a value was already flipped negative by a previous step, using v directly gives a negative index — an immediate IndexError. Always use abs(v) - 1.
  • Applying to unsorted or arbitrary-range arrays. Negation marking requires values in [1, N] so they map to valid indices. If values can be 0, negative, or > N, you must sentinel-clean first (as in First Missing Positive).
  • Reading the final answer from values, not indices. When a duplicate is found at nums[idx] < 0, the duplicate value is idx + 1 — not v, not nums[idx]. The index tells you the value.
  • Mutating input when the problem doesn't allow it. Negation marking modifies the original array. If the problem says "do not modify the array," use a hash set instead.
BugFix
idx = v - 1 (negative index crash)Use idx = abs(v) - 1
Returning nums[idx] as the duplicateReturn idx + 1 (the value whose index was marked)
Works on [1,N] values but gets called on arbitrary inputSentinel-clean out-of-range values to n+1 first

Say it like a pro (interview one-liner)

"Since values are in [1, N], they map directly to indices [0, N-1]. I'll use the sign of each indexed cell as a boolean flag — flip it negative to mark 'visited.' When I land on an already-negative cell, that value appeared before: it's a duplicate. abs() always recovers the original value, so the marking is fully reversible. O(n) time, O(1) space, one pass."


Remember this forever

In-Place Negation Marking

Value v → index v-1. Flip nums[v-1] negative to mark "seen v." If already negative → v is a duplicate. Always use abs(v) when reading a potentially-flipped value.


Trigger: find duplicates / missing values in [1,N] array with O(1) space

Key rule: idx = abs(v) - 1 · nums[idx] < 0 → duplicate · nums[idx] > 0 → first visit

Cost: O(n) time, O(1) space

Caveat: modifies the input array; out-of-range values need sentinel cleaning first


Check yourself

Why must you use abs(v) when computing the index, not just v?

By the time you process element at position i, earlier iterations may have already flipped nums[i] negative as part of marking a visit. If you use v directly and it's negative, idx = v - 1 would be a large negative number — an invalid index. abs(v) strips the marking sign and gives you the original value for index computation.

How is negation marking different from Cyclic Sort? When do you choose each?

Cyclic Sort physically moves each value to its correct index (value v ends up at index v-1). It sorts the array. Negation marking leaves values where they are and uses the sign bit as a memo. Cyclic Sort is better when you want the array sorted afterward; negation marking is better when you only need to detect duplicates/missing without rearranging, and when the problem says you can't sort.

When marking is done, how do you find the missing number (not the duplicate)?

After the marking pass, scan the array. Any index i where nums[i] > 0 (still positive, never flipped) means value i + 1 was never seen — it's the missing number.


Practice problems

ProblemDifficultyWhat to noticeLink
Find All Duplicates in an ArrayMediumCore pattern — mark and checkLC #442
Find All Numbers DisappearedEasySame marking; scan for positives at endLC #448
Set MismatchEasyDuplicate from marking pass; missing from final scanLC #645
First Missing PositiveHardSentinel-clean out-of-range values firstLC #41

When you can write the abs(v) - 1 indexing from memory and explain why abs is non-negotiable, you've learned this pattern.

Next up: Subarray Counting with Prefix Sums — where a running total and a hash map together count subarrays in a single pass.