Suppose your array is called A.
For each index i, you could compute the set of the elements in A[:i] that appear an odd number of times. Now your problem is equivalent to finding all i, j such that these sets are equal.
This is still O(n^2) in the worst case, but instead of using sets, you can use a hash of the sets. For efficiency, the hashes need to be incrementally computable from the hash of the previous set. One such way is to use the XOR of a (universal hash function) of the elements of the set. With this, you can add and remove single elements from the hash in O(1), and it has the benefit that adding and removing an element is exactly the same operation, making it very suitable for this problem, where the parity and not the exact count of the elements is important.
So compute this new array for indexes 0 to n inclusive:
B[0] = 0
B[i+1] = HASH(A[i]) XOR B[i]
Then count all 0<=i<j<=n such that B[i]=B[j] (which you can do in O(n) time, for example with a regular map).
This is a probabilistically correct algorithm, since if you are unlucky, a non-empty set can have zero hash. If you use a universal b-bit hash, an upper bound for the probability it's correct is approximately exp(-n²/2^(b+1)) -- obtained from the birthday problem probability. So if you use a 128-bit hash, you're pretty safe for any input you're likely to find in practice.
As examples, here's Python code that implements the naive version which uses sets and runs in O(n^2) in the worst case.
import collections
def naive_evens(A):
B = frozenset()
counts = collections.Counter()
counts[B] += 1
total = 0
for a in A:
B = B.symmetric_difference({a})
total += counts[B]
counts[B] += 1
return total
Here's the probabilistically correct version that uses hashing and runs in O(n) time. It uses HASH
as a universal hash (with random seed HA
), and parameters HW
and HM
which describe the word-size and number of bits of hash to create. To avoid hashing 0 to 0, the array elements are modified so that they're all positive (by adding something to each element so that the minimum element is always 1).
import collections
import random
HW = 256
HM = 128
HA = random.randrange(1 << HW)
def HASH(x):
h = (HA * x) % (1 << HW)
return h >> (HW - HM)
def smart_evens(A):
B = 0
counts = collections.Counter()
counts[B] += 1
total = 0
M = min(A)
for x in A:
B = B ^ HASH(x - M + 1)
total += counts[B]
counts[B] += 1
return total