Back to Arrays
Q28Question 28 of 109

Find All Duplicates in an Array: Index-Marking or Frequency Map

Key Concepts
  • A frequency HashMap counts occurrences and collects keys with count 2 or more in O(n) time and O(n) space.
  • Index-marking works only when values are in range 1 to n: negate the element at the index encoded by each value.
  • If the target slot is already negative, the value is a duplicate; use absolute value when computing the target index.
  • Index-marking achieves O(n) time with O(1) extra space by storing the seen flag in the sign bit of the array.
  • Restore the array afterward by taking absolute values if the caller needs the original data.
  • Choose frequency map when the range constraint fails or mutation is forbidden; choose marking when the constraint holds and memory is tight.
  • In banking, this detects double-charged transaction references in a daily batch.

Definition

Count with a frequency map, or when values are one to n, flip the sign at the indexed slot and a pre-flipped slot means a duplicate.
  • Frequency map approach – use a HashMap to count occurrences, collecting keys with count >= 2 in O(n) time and O(n) space.
  • Index-marking approach – valid when values are in [1, n]; for each value v, negate a[|v| - 1]; if it is already negative, v is a duplicate. O(n) time, O(1) extra space.
  • Absolute value – always use Math.abs(a[i]) when computing the target index because the element may have been negated by an earlier visit.
  • Array restoration – after collecting duplicates, take absolute values to undo the marks if the caller needs the original array.
  • Choice – frequency map for arbitrary values or immutable arrays; index-marking for [1, n] constraint with mutation allowed.

The National Bank of India processes a batch of transaction reference numbers and the fraud team must spot every reference that appears more than once, since duplicates can mean a double-charge. The simple way is a tally sheet (HashMap). The clever way, when references are numbered 1 to batch-size, is to flip a mark at the slot each reference points to, and if that slot is already flipped, the reference is a repeat.

Understanding the Concepts

What is the Find All Duplicates problem?

Given an array of integers, report every value that appears more than once. For example, given [4, 3, 2, 7, 8, 2, 3, 1], the duplicates are [2, 3].

In banking, the fraud team at the National Bank of India must detect double-charged transaction reference numbers in a daily batch.

How does the frequency map approach work?

Walk the array once, incrementing each value's count in a HashMap. Collect a value the moment its count reaches exactly two to avoid reporting it multiple times.

JAVA
package com.bank.fraud;

import java.util.*;

public class DuplicateDetector {

    public static List<String> findDuplicateRefs(String[] refs) {
        Map<String, Integer> freq = new HashMap<>();
        List<String> duplicates = new ArrayList<>();

        for (String ref : refs) {
            int count = freq.merge(ref, 1, Integer::sum);
            if (count == 2) {
                duplicates.add(ref);
            }
        }
        return duplicates;
    }

    public static void main(String[] args) {
        String[] refs = {"TXN001", "TXN002", "TXN003", "TXN001", "TXN004", "TXN002"};
        System.out.println("Duplicate refs: " + findDuplicateRefs(refs));
    }
}
TEXT
Duplicate refs: [TXN001, TXN002]

How does the index-marking approach work?

  • When every value is in the range [1, n] and you may mutate the array, you can use the array itself as the tally sheet. For each value v:
  • Compute target index: idx = Math.abs(v) - 1 (values are 1-based, indices are 0-based).
  • Check a[idx]: if already negative, v is a duplicate.
  • Otherwise, negate a[idx] to mark that v has been seen.

Using Math.abs is essential because the element you read may have been negated by an earlier visit.

JAVA
package com.bank.fraud;

import java.util.*;

public class IndexMarkingDuplicates {

    public static List<Integer> findDuplicates(int[] batchIds) {
        List<Integer> duplicates = new ArrayList<>();

        for (int i = 0; i < batchIds.length; i++) {
            int idx = Math.abs(batchIds[i]) - 1;
            if (batchIds[idx] < 0) {
                duplicates.add(idx + 1);  // convert back to 1-based
            } else {
                batchIds[idx] = -batchIds[idx];
            }
        }
        return duplicates;
    }

    // Restore array after marking
    public static void restore(int[] batchIds) {
        for (int i = 0; i < batchIds.length; i++) {
            batchIds[i] = Math.abs(batchIds[i]);
        }
    }

    public static void main(String[] args) {
        int[] batch = {4, 3, 2, 7, 8, 2, 3, 1};
        System.out.println("Duplicates: " + findDuplicates(batch));
        restore(batch);
        System.out.println("Restored: " + Arrays.toString(batch));
    }
}
TEXT
Duplicates: [2, 3]
Restored: [4, 3, 2, 7, 8, 2, 3, 1]

How is index-marking different from the frequency map?

| Aspect | Frequency Map | Index-Marking |
|---|---|---|
| Value range | Any values | Must be in [1, n] |
| Mutation | Does not mutate input | Negates array elements |
| Extra space | O(n) for the map | O(1) |
| Time | O(n) | O(n) |
| Restoration needed | No | Yes, if caller needs originals |
| Use case | Arbitrary reference strings | Numbered batch IDs |

Why is the absolute value step essential?

When you read a value from the array, it may have been negated by an earlier iteration. Without Math.abs, you would compute a negative index, causing an ArrayIndexOutOfBoundsException. The absolute value recovers the original value for index computation.

JAVA
// Without abs: if batchIds[2] was negated to -2
int idx = batchIds[2] - 1;  // idx = -3 → ArrayIndexOutOfBoundsException!

// With abs: correctly recovers the value
int idx = Math.abs(batchIds[2]) - 1;  // idx = 1 → correct

What about restoring the array?

Negating elements mutates the input, which may be unacceptable if the caller needs the original values. After collecting duplicates, walk the array and take the absolute value of every element. Silently corrupting a caller's data is a real production bug, so always consider this trade-off.

What are the edge cases?

  • No duplicates – the result list is empty.
  • All elements the same – every element after the first is a duplicate; with the map approach, the value is collected once at count 2.
  • Single element – no duplicates possible.
  • Empty array – return an empty list.

When should you use each approach?

Use the frequency map when the value range constraint does not hold, or mutation is forbidden. Use index-marking when the constraint holds and memory is tight, such as processing huge batches. Rajesh at the National Bank of India would choose index-marking for numbered batch IDs to save memory on millions of transactions, but the map for arbitrary reference strings.

What are common interview follow-up questions?

  • Can you do it in O(1) space? – Yes, with index-marking if values are in [1, n].
  • What if values can appear more than twice? – The map handles any count; marking flags presence only, not exact count.
  • What about finding the missing number? – A similar index-marking trick finds the number not present.

Visual Overview

DIAGRAM
┌──────────────────────────────────────────────────────┐
│        Index-Marking Walkthrough                     │
├──────────────────────────────────────────────────────┤
│                                                      │
│  Array: [4, 3, 2, 7, 8, 2, 3, 1]   (1-indexed IDs) │
│                                                      │
│  i=0: v=4, idx=3 → a[3]=7>0 → negate → a[3]=-7     │
│  i=1: v=3, idx=2 → a[2]=2>0 → negate → a[2]=-2     │
│  i=2: v=|-2|=2, idx=1 → a[1]=3>0 → negate → a[1]=-3│
│  i=3: v=|-7|=7, idx=6 → a[6]=3>0 → negate → a[6]=-3│
│  i=4: v=8, idx=7 → a[7]=1>0 → negate → a[7]=-1     │
│  i=5: v=2, idx=1 → a[1]=-3<0 → DUPLICATE: 2        │
│  i=6: v=|-3|=3, idx=2 → a[2]=-2<0 → DUPLICATE: 3   │
│  i=7: v=|-1|=1, idx=0 → a[0]=4>0 → negate → a[0]=-4│
│                                                      │
│  Result: [2, 3]                                      │
└──────────────────────────────────────────────────────┘

Key Takeaway

Finding all duplicates reports every value that appears more than once, and you pick one of two approaches depending on the constraints. The general approach is a frequency map: walk the array once, increment each value's count, and collect a value the instant its count reaches exactly two. That is O(n) time and O(n) space and works for any values.

When the problem guarantees every value lies in the range 1 to n and allows mutation, index-marking drops the extra space to O(1). For each value, compute a target index as the absolute value minus one, check the element there: if it is already negative, an earlier value pointed here so this is a duplicate, otherwise negate it to mark it seen. The absolute value step is essential because the element you inspect may have been negated earlier.

The responsibility you always call out is restoring the array. Negating mutates the caller's input, so if the original values are needed afterward, walk the array and take absolute values. Silently corrupting a caller's data is a real production bug.

In a banking project at the National Bank of India, the fraud team uses this to flag every transaction reference that appeared more than once in a daily batch, which can signal a double-charge, using the map for arbitrary reference strings and index-marking for batches numbered 1 to size.

Remember one sentence: Use a frequency map for general duplicates, or negate elements at value-derived indices for O(1) space when values are in [1, n].

---

Share
Use ← → arrow keys to navigate