Learn/DSA Patterns
DSA PatternsArrays & Two Pointerseasy13 min read

Moore's Voting Algorithm

Find the majority element in O(n) time and O(1) space. We build the idea from a surprisingly simple political metaphor, prove it can never miss, and extend it to the n/3 variant.

#moores-voting#majority-element#arrays#beginner#interview
Table of contents

Before we start

This is one of the most elegant algorithms in all of computer science — it looks like magic until you understand it, then it looks obvious. By the end of this page you will be able to:

  • See the voting metaphor so clearly that the algorithm runs itself in your head.
  • Explain out loud why the majority element can never be voted out.
  • Apply it to both the n/2 and n/3 variants confidently.

Stop at every Pause & Think box. Those moments are where the real learning happens.


Picture this first (no code yet)

A real-life story

Imagine a town election with a twist. The town has one rule: whenever two people from different parties meet, they cancel each other out — both leave the town forever. Same party? They stay together.

The town starts with 100 people. Some support Party A, some Party B, some Party C. People keep meeting and cancelling. Eventually the chaos stops.

Here is the key question: if one party started with more than 50 people (a true majority), can their last survivor be from a different party?

Think about it. Every cancellation removes one majority-party person and one non-majority person. The majority party loses people, yes — but the other side loses people at the same rate. Since the majority started with more people, they can survive every possible cancellation and always have someone left standing at the end.

The last person standing is guaranteed to be from the majority party.

That town, those cancellations — that is Moore's Voting Algorithm. Let's make it precise.


The actual problem

Given an array of n integers, find the element that appears more than n/2 times (the majority element). You may assume it always exists.

Example:

input  = [3, 2, 3, 1, 3, 2, 3]
n = 7,  majority threshold = 7/2 = 3.5  →  need more than 3 appearances
3 appears 4 times  →  answer is 3

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

Approach 1 — count everything with a hash map:

from collections import Counter
def majorityElement(nums):
    count = Counter(nums)
    return max(count, key=count.get)

Correct, but uses O(n) extra memory for the hash map. For n = 100 million, that's potentially gigabytes of memory just to count. Can we do it with O(1) space?

Approach 2 — sort and pick the middle:

def majorityElement(nums):
    nums.sort()
    return nums[len(nums) // 2]   # middle element must be the majority

Also correct (a majority element must occupy the middle after sorting), but sorting costs O(n log n) time. Can we do it in O(n) time AND O(1) space?

n = 1,000,000
Sorting  →  ~20,000,000 operations
Voting   →  1,000,000 operations   (20x faster)

The turning point

Back to the town. The key insight is that we do not need to count everyone. We only need to track one candidate and a score that rises when we see someone supporting that candidate and falls when we see someone opposing.

Pause & think

You're walking through the array. You've picked a candidate and your score is currently 3. You see an element that's different from your candidate. What should happen to the score?

And here's the deeper question: if the score ever hits zero — meaning all previous elements cancelled each other out — does it matter what our candidate was before that point?

When the score hits zero, all elements seen so far have perfectly cancelled each other out. They form a balanced group where no single value dominates. Whatever happened in that balanced group is completely irrelevant to what comes next — the majority element in the remaining unseen portion is still the majority element of the whole array (because all balanced cancellations removed equal numbers of majority and non-majority elements).

So when the score hits zero: reset the candidate to whatever comes next. Fresh start.


The one idea to remember

The entire pattern in one sentence

Walk through the array keeping one candidate and a score: same element as candidate → score up, different → score down; when score hits zero, the current element becomes the new candidate — the final candidate is guaranteed to be the majority element.

That's the whole algorithm. One pass, two variables.


Watch it happen, frame by frame

Array: [2, 2, 1, 1, 1, 2, 1]. The majority element is 1 (appears 4 times, n/2 = 3.5).

Start: candidate = None, score = 0

index 0 → value 2:  score == 0  →  candidate = 2, score = 1
index 1 → value 2:  value == candidate  →  score = 2
index 2 → value 1:  value != candidate  →  score = 1
index 3 → value 1:  value != candidate  →  score = 0
index 4 → value 1:  score == 0  →  candidate = 1, score = 1
index 5 → value 2:  value != candidate  →  score = 0
index 6 → value 1:  score == 0  →  candidate = 1, score = 1

Final candidate = 1

Notice what happened at index 3: 2 and 1 had perfectly cancelled each other out (two 2s vs two 1s). The slate wiped clean. Then 1 took over — and kept the throne.

Pause & think

Cover the trace below and run the algorithm on [3, 3, 4, 2, 3, 3, 3] yourself. What's the candidate at each step? What final value does it give?

Check your trace
index 0 → 3: score=0  →  candidate=3, score=1
index 1 → 3: match    →  score=2
index 2 → 4: differ   →  score=1
index 3 → 2: differ   →  score=0
index 4 → 3: score=0  →  candidate=3, score=1
index 5 → 3: match    →  score=2
index 6 → 3: match    →  score=3

Final candidate = 3  ✅  (appears 5 times, majority of 7)

Now, the code — line by line

def majorityElement(nums):
    candidate = None     # the current "town survivor" candidate
    score = 0            # net support count

    for num in nums:
        if score == 0:
            candidate = num    # slate wiped clean — this person is the new candidate
        if num == candidate:
            score += 1         # same party: gain support
        else:
            score -= 1         # different party: one cancellation

    return candidate

Mapping each line to the town story:

  • candidate = None, score = 0 — the town hasn't started processing yet.
  • if score == 0: candidate = num — the town is empty after all cancellations; whoever walks in next becomes the new hopeful.
  • score += 1 — another supporter arrives; candidate's strength grows.
  • score -= 1 — an opponent arrives; one supporter and one opponent cancel each other.
  • return candidate — whoever survives the entire process is our answer.

Why does it never miss?

The argument is clean. Define the majority element as M (appears more than n/2 times).

Every time score decreases by 1, it means one occurrence of M cancelled with one non-M element. Since M appears more than n/2 times, there are not enough non-M elements to cancel all of them. At least one occurrence of M survives every possible round of cancellations.

The final candidate is whatever's left standing. Since M is the only element that cannot be fully cancelled, M is the final candidate.

Formal argument (for the curious)

Claim. If a majority element M exists (count > n/2), majorityElement returns M.

Key observation. Every time score drops to zero, we've consumed a balanced prefix — an equal number of the current candidate and non-candidates. These balanced groups are irrelevant: in any balanced group of size 2k, at most k elements can be M, and at least k elements are non-M. So M loses at most k occurrences per balanced group.

Counting. Let M appear m > n/2 times. All other elements together appear n - m < n/2 times. Even if every single non-M cancels with an M, we need n - m copies of M for those cancellations. Since m > n - m (because m > n/2), M has copies left over: m - (n - m) = 2m - n > 0. Those leftover copies survive. The final candidate is one of them — M itself. ∎


Why is it so fast?

One for loop over the array. One comparison, one increment or decrement per element. No extra memory, no sorting.

n = 1,000,0001,000,000 steps
n = 100,000,000100,000,000 steps   (still under 1 second)
ApproachTimeExtra memory
Hash map countO(n)O(n)
Sort & middleO(n log n)O(1)
Moore's VotingO(n)O(1)

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

  • The problem asks for an element that appears more than n/2 times or more than n/3 times.
  • You need O(1) space — no hash maps allowed.
  • The problem guarantees a majority exists (or asks you to verify after finding the candidate).
  • Key phrase in problem: "majority element," "dominant element," "appears more than half the time."

Important: always verify if majority isn't guaranteed

Moore's Voting always returns a candidate, but if no majority element exists, it still returns something (just the wrong answer). If the problem does not guarantee a majority exists, do a second pass to count the candidate's actual occurrences and confirm.


The same trick in two disguises

Disguise 1 — Majority Element (LC #169)

Straightforward application. The problem guarantees a majority element exists.

def majorityElement(nums):
    candidate, score = None, 0
    for num in nums:
        if score == 0:
            candidate = num
        score += 1 if num == candidate else -1
    return candidate

Disguise 2 — Majority Element II: appear more than n/3 times (LC #229)

At most 2 elements can appear more than n/3 times (since 3 × (n/3+1) > n). So we track two candidates and two scores — the same cancellation logic, but now three-way: matching candidate1 → score1 up, matching candidate2 → score2 up, matching neither → both scores down by 1 (one triple cancellation).

def majorityElement2(nums):
    c1, c2, s1, s2 = None, None, 0, 0

    for num in nums:
        if num == c1:
            s1 += 1
        elif num == c2:
            s2 += 1
        elif s1 == 0:
            c1, s1 = num, 1
        elif s2 == 0:
            c2, s2 = num, 1
        else:
            s1 -= 1    # three-way cancellation
            s2 -= 1

    # verify — candidates may not actually be majority
    return [c for c in (c1, c2) if nums.count(c) > len(nums) // 3]

Same bones: candidates + scores, cancellation logic, survivors at the end. The only change is two candidates instead of one, and the cancellation is three-way.

Dry run on [1,1,1,3,3,2,2,2] (n=8, threshold > 2.67)
num=1: s1=0  →  c1=1,s1=1
num=1: c1    →  s1=2
num=1: c1    →  s1=3
num=3: s2=0  →  c2=3,s2=1
num=3: c2    →  s2=2
num=2: neither  →  s1=2,s2=1
num=2: neither  →  s1=1,s2=0
num=2: s2=0  →  c2=2,s2=1

Candidates: c1=1 (count=3>2.67 ✅), c2=2 (count=3>2.67 ✅)
Answer: [1, 2]

Traps that catch beginners

Watch out for these

  • Assuming the candidate is definitely the answer. Moore's Voting finds the only possible majority candidate — not a confirmed one. If the problem doesn't guarantee a majority exists, you must verify with a second pass.
  • Forgetting the score == 0 reset before setting the new candidate. If you set the candidate only on a non-zero check, you'll miss the first element.
  • Getting confused on the n/3 variant order. In the two-candidate version, always check num == c1 and num == c2 before checking for empty slots (s1 == 0, s2 == 0). Otherwise a value matching c1 might accidentally become c2.
BugFix
No verification passIf majority not guaranteed, count candidate after the loop
Wrong order in n/3 variantCheck num == c1, num == c2 first, then empty-slot checks
Off-by-one on threshold"more than n/2" means count > n/2, not ≥ n/2

Say it like a pro (interview one-liner)

"I'll use Moore's Voting — track one candidate and a score. Same element increases the score, different element decreases it. When score hits zero, reset the candidate. Since the majority element appears more than half the time, it can't be fully cancelled — it survives as the final candidate. One pass, O(n) time, O(1) space."


Remember this forever

Moore's Voting Algorithm

Walk the array with one candidate and one score. Match → score up. Differ → score down. Score hits zero → reset candidate to current element. Final candidate = majority element.


Cost: O(n) time, O(1) space · Trigger: "majority element" / "more than n/2 times" / O(1) space required · Skeleton: if score==0: cand=num; score += 1 if match else -1

Key trap: Always verify the candidate if a majority isn't guaranteed.


Check yourself

Why can the majority element never be fully cancelled out?

Because cancellation always removes one majority element and one non-majority element together. There are fewer non-majority elements than majority elements (by definition — majority is more than half). So no matter how many cancellations happen, the non-majority side runs out of elements first, leaving majority elements uncancelled.

After the voting loop finishes, can the final candidate ever be wrong (assuming majority exists)?

No. If a majority element M exists, it survives all cancellations (as argued above) and is the final candidate. However, if no majority exists, the final candidate is some element but not a guaranteed answer — that's why you verify when needed.

In the n/3 variant, why do we check `num == c1` and `num == c2` BEFORE the empty-slot checks?

Suppose c1 = 3 and num = 3. If we check s1 == 0 first and s1 happens to be 0, we'd reset c1 = 3 and s1 = 1 — which looks correct — but we'd skip the s2 decrement from a three-way cancellation. More dangerously, if s2 == 0 is checked first and num matches c1, the value could become c2 instead of incrementing c1's score. Always match existing candidates before accepting new ones.

What would you return if the array is [1, 2, 3] and the problem asks for majority element (guaranteed to exist)?

The guarantee means this input would never appear in that problem. But if you ran the algorithm: candidate would end up as 3, score=1. Without the guarantee, you'd verify: 3 appears once, not more than 1.5 — no majority. Return nothing. This illustrates why the guarantee matters.


Practice problems

ProblemDifficultyWhat to noticeLink
Majority ElementEasyStraightforward application — majority guaranteedLC #169
Majority Element IIMediumTwo candidates; n/3 threshold; verify bothLC #229
Check if Array is GoodEasyRecognise majority-detection needLC #2605

When you can solve LC #169 from memory and explain why the majority element can never be cancelled, you've learned this pattern.

Next up: Merge Intervals — a completely different flavour where we work with pairs (start, end) and learn to fold overlapping ranges into one.