Cairn
algorithms · greedy · O(n)

↩ back to Greedy

Boyer–Moore Majority Vote Algorithm

The site's twelfth Greedy entry, and the first that asks a frequency question instead of a scheduling or covering one: given a list of votes, is there a candidate who received a strict majority — more than half — and if so, who? The obvious approach tallies every candidate in a hash map and reads off whichever count clears half, which is correct but spends O(n) extra space on the tally. Boyer–Moore's algorithm answers the same question in one left-to-right pass using two variables total — a current candidate and a counter — greedily backing whoever's ahead right now and discarding that guess the moment the count runs out. The catch, and this page's main pitfall, is that the pass alone never proves a majority exists; it only ever tells you who it would be, if one does.

Try it

Two presets, both processed vote by vote, left to right. Election (real majority) is a 10-vote tally where A wins with 6 of 10 — watch the running candidate swing to D, then B (twice), before A's own votes finally take over for good. Split vote (no majority) is a 6-vote tally where nobody clears half — the pass still ends holding some candidate, and the skip verification pass checkbox shows what happens if you trust that candidate without checking it (see Pitfalls).

step 0
Press Step or Run.

Why it works

Think of every vote for the current candidate as a person still standing, and every count-- as that person stepping aside together with whoever just voted differently — a cancelling pair, one for one. That's a real constraint on the bookkeeping: a cancelling pair is always two unequal votes (the whole reason it triggers is that the new vote doesn't match the candidate), so a true majority candidate can never be one half of a pair against another copy of itself — every cancellation that removes one of its votes must spend one non-majority vote to do it. On the default election preset, A holds 6 of the 10 votes and the other three candidates (D, C, B×2) hold only 4 between them — so at most 4 of A's 6 votes can ever be cancelled away before the non-A votes needed to pair against them simply run out. At least 6 − 4 = 2 of A's votes must survive every possible cancellation, uncancelled, all the way to the end — and since whatever survives uncancelled is exactly what the running candidate/count ends up holding, that guarantees A is still the final candidate, not just a plausible one.

The same argument works for any array with a true majority: if it has n votes and one candidate holds m > n/2 of them, the other n − m < n/2 < m votes can cancel at most n − m of the majority's own votes, leaving at least m − (n − m) = 2m − n > 0 standing — so the vote can never fully erase a true majority, on any input, for any reason. What it does not guarantee is who's holding the lead at every point along the way: on the election preset, the running candidate is briefly D (1 vote, immediately cancelled by C), then B twice (each time immediately cancelled by the very next vote), before A ever takes the lead for good at index 5. Only the final candidate is meaningful — nothing about an intermediate leader is.

Reference implementation

Two passes, both O(n), neither needing more than a couple of variables. The first is the vote itself; the second is the check every real use of this algorithm needs and the naive version above skips — see Pitfalls for exactly what goes wrong without it:

function majorityVote(votes) {
  let candidate = null;
  let count = 0;
  for (const v of votes) {
    if (count === 0) {
      candidate = v;      // the old guess is gone; back a fresh one
      count = 1;
    } else if (v === candidate) {
      count++;
    } else {
      count--;             // cancel: this vote and one of candidate's are both spent
    }
  }

  // verification pass — the tally above never proves a majority exists
  let occurrences = 0;
  for (const v of votes) if (v === candidate) occurrences++;
  return occurrences > votes.length / 2 ? candidate : null;
}

Pitfalls

Skipping the verification pass reports a majority even when none exists — and the candidate it names doesn't even have to be a plausible runner-up. The vote loop above always ends holding some candidate, for any non-empty input; nothing about the loop itself can produce "no answer." On the split-vote preset (A, A, B, B, C, A — A leads with 3 of 6, short of the 4 a majority needs), the naive version confidently names C, the single least-common vote in the entire array (1 occurrence, against A's 3) — not a near-miss, a different candidate entirely, and the internal counter that produced it has already decayed to 0 by the last vote. Checked across 20,000 random 4-to-16-vote arrays engineered so no true majority exists in 16,900 of them: the naive/unverified version is wrong 100% of the time in those 16,900 by construction (it always names some candidate as "the majority" when none exists), and in 9,590 of the 16,900 (56.7%) the candidate it names isn't even the single most-frequent vote in that array — the same failure mode the split-vote preset shows, not a rare edge case.

The counter's final value is not the margin of victory. It's tempting to read the leftover count as "how much" a candidate won by, the way a scoreboard would. On the election preset, A actually wins by 6 − 4 = 2 votes — but the pass's own internal counter ends at 4, double the true margin, because two of the three losing candidates (D once, B twice) each briefly built up their own short-lived lead before A's votes cancelled them back out, and every one of those side excursions spends a non-A vote without ever touching the running count the way a smooth, uninterrupted A-only cancellation would. Checked directly across the same 20,000 engineered-majority arrays used above (3,100 of which have a genuine majority): the leftover count differs from the true margin in 1,938 of them (9.7%) — not a fluke of this one hand-picked example.

Complexity

Time: O(n) — two linear passes (vote, then verify), never a nested loop or a sort. Space: O(1) beyond the input itself: one candidate variable and one counter, full stop. That's a genuinely different trade-off from the obvious hash-map tally, which is also O(n) time but spends O(n) space building a full frequency table — this algorithm answers the identical question in the same time class while giving that space back, because it never needs to remember more than one candidate's standing at once.

This site's guide, Choosing a Greedy Strategy, compares this entry's cancellation-pair proof against the other eleven Greedy entries' own exchange, bound-matching, and no-wasted-proposal arguments.