XOR Operation Intuition
Asked Answered
N

8

42

I recently came across this question on Leetcode and figured out a solution that I need some clarification with:

Given an array of integers, every element appears twice except for one. Find that single one.

Note: Your algorithm should have a linear runtime complexity. Could you implement it without using extra memory?

class Solution {
public:
    int singleNumber(vector<int>& nums) {
        int result = 0;
        for(auto & c : nums) {
            result ^= c;
        }
        return result;
    }
};

First of all, what sorts of keywords should I be paying attention to in order to figure out that I should be using an XOR operation for this question?

Also, why does XOR'ing all items in the vector with each other give us the one that is not repeated?


Thank you all for these responses, here is some more information on bitwise properties for anyone else interested: More bitwise info

Nicias answered 31/1, 2017 at 17:32 Comment(12)
Well do you understand what happens when you xor a number by itself?Shelves
If you do know what xor does, and still can't reason about the code, best thing to do is grab some paper and dry run it, writing out the bits.Sacci
You can not always identify the algorithm to be used by looking for keywords in quizzes. A better approach is to familiarize yourself with many of them so you can think of one if you meet a familiar problem.Matriculation
It';s something you'd have to be very bright to work out for yourself, but if you've taken an interest in computing puzzles you might well have heard it.Stridor
There are no "keywords" to look for. You can't figure this out on your own if you don't already have a solid enough understanding of (bitwise) logical operations that you think "xor might do it" and then try.Florid
For most people, you recognize to use XOR for this because you've seen XOR used for similar tricks in the past.Assumptive
As you XOR a (cumulative) result to each item in the list then each pair of duplicates eventually cancel each other out. This only leaves the odd man left. Its roughly the equivalent of saying n+0==n ... or a-a+b-b+c-c .... +n == n --- even if you shuffle up all the other items in the sum. This only works because there's only a single unpaired item. Notably the odd man in this list doesn't have to have a distinct value --- a triplicate among many dupes will still result in one of the triplicate as your final value.Proscription
It's also notable that you cannot use this XOR trick to confirm that some list has the desired property --- that it's N pairs of numbers + 1 additional number. So the trick only works when you're guaranteed that your input has the stated properties; this makes it a puzzle rather than a practical technique.Proscription
There is a technicality that prevents this solution from being fully portable. On some unconventional architectures, there might be an intermediate result that is an illegal value.Afterdinner
This type of question is hard to figure out if you don't already know the answer because this is a sort of trick question. It's not there to assess your skills as a programmer instead it's there to figure out if you already know boolean algebra and if you've been paying attention in your Digital Logic class. If you've never taken a Digital Logic class then your fate depends on weather or not you've read questions such as this onlineGirl
@Baldrickk: No. [2,3,4,5], for example, gives 0 but does not consist of pairs.Sleave
@Sleave bah, yes, you are right. I should have thought about it moreMerrick
R
105
  1. A ^ 0 == A

  2. A ^ A == 0

  3. A ^ B == B ^ A

  4. (A ^ B) ^ C == A ^ (B ^ C)

(3) and (4) together mean that the order in which numbers are xored doesn't matter.

Which means that, for example, A^B^X^C^B^A^C is equal to A^A ^ B^B ^ C^C ^ X.

Because of the (2) that is equal to 0^0^0^X.

Because of the (1) that is equal to X.


I don't think there are any specific keywords that can help you to identify such problems. You just should know above properties of XOR.

Restaurateur answered 31/1, 2017 at 17:52 Comment(13)
I never realised that XOR forms and Abelian Group with binary integers. Might be worth mentioning that those rules are the Identity Element, Inverse Element, Commutativity and Associativity properties (respectively).Inellineloquent
@Inellineloquent "binary integers" ?? Surely you just mean "integers". And in this case a finite subset of them.Thanatopsis
@M.M: I'm not sure XOR makes sense without binary representation. If we take logical integers for example, where languages like C interprets 0 as false and any non-zero integer value as true then 15 ^ 22 = 0 and 155 ^ 0 = 1Girl
@Girl I would describe that as a property of the operator, not of the integer. 15, 0b1111, and 0xF all mean the same number. But you can then consider the two different operators "bitwise-XOR" or "logical-XOR"Thanatopsis
@M.M: My point being that XOR is only well defined in terms of binary symbols. That we use binary symbols in a string to represent numbers allow XOR to be used on numbers but that's only inside an electronic circuit (or programming language). Technically XOR isn't defined to operate on non-binary numbers be it integers or reals or rationals etc. So basically this is CPU math. Outside of the CPU this makes little sense. So it is correct to say binary integers instead of the more general integers we find in mathGirl
XOR is actually equivalent to addition of Nimbers which has a elementary natural formulation that, on the surface, has nothing to do with binary. This can also be thought of as an extension of XOR to infinite ordinals.Sleave
I understood what was going on in the question, but you're on another level for being able to explain it as clear, concise and unambiguous as this. +1!Antin
@Thanatopsis As slebetman correctly realised, when I said 'binary integers' I was referring to the fact that XOR typically only makes sense when defined on numbers represented in binary form. It is completely possible to define XOR on integers in decimal or hexadecimal representation, but it is more confusing as there is no clear rule without representing them in binary at some point.Inellineloquent
@Inellineloquent XOR is nothing more than addition mod 2, that's why you see an abelian group.Deberadeberry
@gigabytes 2^2=0, 5^3=6. Can you explain the addition mod 2, please?Bourne
@Bourne The question is about a specific use of XOR, not XOR in general, so I don't think it's the place to explain it. But "addition mod 2" is applied to individual bits, not whole numbers.Restaurateur
@Restaurateur Now it's clear to me, what gigabytes meant (applied to individual bits). No more explanation needed. Thx.Bourne
It’s the addition mod 2 between bits, not words. The addition mod2 between bits goes like this: 0+0=0, 1+0=1, 0+1=1, and, crucially, 1+1=0. As you see, it’s the xor truth table.Deberadeberry
B
25

The Xor operator is commutative:

1.      X ⊕ Y = Y ⊕ X                    for any integers X and Y

and associative:

2.      X ⊕ (Y ⊕ Z) = (X ⊕ Y) ⊕ Z      for any integers X, Y and Z

It follows that the result of any sequence of xor operations is completely independent of the order of the operands (that is, the order of the elements in the array).

3.     X ⊕ X = 0                         for any integer X

4.     X ⊕ 0 = 0 ⊕ X = X                for any integer X

In the problem we have an expression where each element Ai appears twice except some singular element B. the resulting Xor operation is equivalent to:

     (A1 ⊕ A1) ⊕ (A2 ⊕ A2) ⊕    ...   ⊕ B
 = 
         0      ⊕      0     ⊕    ...   ⊕ B
 = 
         B

what sorts of keywords should I be paying attention to in order to figure out that I should be using an XOR operation for this question

Some problems can be solved quickly using bit manipulation. After familiarizing with Boolean operators and their properties, and seeing enough applications like this one, you will naturally "feel" when they're useful to solve a given problem.

Brazier answered 31/1, 2017 at 17:51 Comment(2)
Ninja'd me by 57 seconds. :)Restaurateur
Yeah, probably, or that they don't like to formulate the answers in mathematical terms. Congrats for earning your memorable answer though :)Brazier
L
12

The key intuitive aspect that distinguishes xor from the other logical operators is that it is lossless, or non-lossy, meaning that, unlike and, and or (and more similar to unary-not in this regard), it is deterministcally reversible: You can exactly recover one of the input values given the rest of the computation history.

The following diagrams illustrate that and and or each have at least one case where the state of one of the inputs is irrecoverable, given a certain value of the other input. I indicate these as "lost" inputs.

AND and OR logical operators can lose information

For the xor gate, there is no condition in which an input or output value cannot be recovered, given the rest of the computation history. In fact, there's a symmetry that knowing any two values of the triple (in0, in1, out) allows you to recover the third. In other words, regardless of input or output, each of these three values is the xor of the other two!

XOR logical operator does not lose information

From the discussion so far, one intuition could be that xor works like a whack-a-mole.

On further examination, the picture also suggests that you can think of the xor operation is as a controllable unary-not gate. By toggling one of the inputs (the upper one in the example above), you can control whether or not the other (lower) one is negated.

Yet another equivalent view is that xor implements the positive-logic not-equals (≠) function with respect to its two inputs. And thus also the equals function (=) under negative-logic.

In accordance with its symmetry and information-preserving properties, xor should come to mind for problems that require reversibility or perfect data recovery. The most obvious example is that xoring a dataset with a constant 'key' trivially obscures the data such that knowing the key (which might be kept "secret"), allows for exact recovery.

Preserving all of the available information is also desirable in hashing. Because you want hash values that maximally discriminate amongst the source items, you want to make sure that as many of their distinguishing characteristics as possible are incorporated, minimizing loss, in the hash code. For example, to hash a 64-bit value into 32 bits, you would use the programming language xor operator ^ because it's an easy way to guarantee that each of the 64 input bits has an opportunity to influence the output:

uint GetHashCode(ulong ul)
{
    return (uint)ul ^ (uint)(ul >> 32); 
}

Note that in this example, information is lost even though xor was used. (In fact, "strategic information loss" is kind-of a definition of hashing). The original value of ul is not recoverable from the hash code, because with that value alone you don't have two out of the three 32-bit values that were used in the internal computation. Recall from above that you need to retain any two out of the three values for perfect reversal ("whack-a-mole"). Out of the resulting hash code and the two values that were xored, you may have saved the result, but typically do not save either of the latter to use as a key value for obtaining the other.1

As an aside, xor was helpful in the days of bit-twiddling hacks. My contribution back then was a way to conditionally set or clear bit(s) without branching in C/C++:

unsigned int v;       // input, output: the value to modify
unsigned int m;       // mask: which bit(s) to set or clear
int f;                // action: 0 to 'set', or 1 to 'clear' them

v ^= (-f ^ v) & m;    // if (f) v |= m; else v &= ~m;

Finally, the fact that xor is non-lossy has important information-theoretical implications for futuristic computing, due to an important relationship between information processing and the Second Law of Thermodynamics. As explained in an excellent and accessible book by Charles Seife, Decoding the Universe, it turns out that the loss of information during computation has an exact physical correspondence with the black-body radiation emanated by a processing system, a limit known as Landauer's principle.

Indeed, the notion of entropy plays a central role in quantifying how information "loss" is (re-)expressed as heat (this also being the same prominent relation from Steven Hawking's famous black hole wager).

Such talk regarding xor is not necessarily academic; Seife notes that modern CPU development currently faces fundamental toleration limitations on the watts/cm² of semiconducting materials, and that a solution would be to design reversible, or lossless, computing systems. In this speculative future-generation of CPUs, xor's ability to preserve information—and thus shunt away heat—would be invaluable for increasing computational density (i.e., MIPS/per cm²) despite such materials limitations.



1. In this simple example, the relevant 3 values would be the hash code plus the upper- and lower-parts of the original `ulong` value. Of course the original hashed 'data' itself, represented by `ul` here, likely *is* retained.
Lynching answered 1/2, 2017 at 3:36 Comment(1)
For the non-lossy computing, see also en.m.wikipedia.org/wiki/Reversible_computing, en.m.wikipedia.org/wiki/Controlled_NOT_gate, en.m.wikipedia.org/wiki/Toffoli_gate non-lossy computers potentially need less energy and produce less heat. For a quantum computer to stay coherent it has to be non-lossy (in order not to interact with the environment).Bourne
A
3

XOR is always defined in terms of binary digits (or some equivalent notions, like true or false statements). There is no special XOR for integers other than the XORing of corresponding bits of their binary representations.

Let A and B be two boolean variables, and let XOR be a boolean function that takes two boolean variables.
A⊕B = 1 if either (A = 0 and B = 1) or (A = 1 and B = 0) (i.e. they are different),
A⊕B=0 if either (A = 0 and B = 0) or (A = 1 and B = 1). (i.e they are same)

Thus taking your question into consideration since out of given n elements of the vector ,every element appears twice except one element,the idea is that the binary representation of the duplicate numbers would be same thus there XOR result would nullify each other as 1⊕1 =0 and 0⊕0= 0.

For A=5 ,its binary representation is 101,so A⊕A = (101)⊕(101) = 000 which is the decimal representation is 0.

REMEMBER IT DOES NOT MATTER IN WHICH ORDER THE NUMBERS APPEAR AFTER EACH OTHER BECAUSE ((A⊕B)⊕C) = (A⊕(B⊕C)) . Eventually what you get at the end after XORING every number is the number which occurs once .

To answer your question regarding when you need to use XOR operations for solving a question, practice some BIT MANIPULATION question eventually you will be able to figure out.
A hint: The question which asks to find one element which has unique property apart from rest, requires bit manipulation.
Example: Given an array where every element occurs three times, except one element which occurs only once. Find the element that occurs once .

Adamic answered 31/1, 2017 at 18:25 Comment(2)
I very much doubt that ((A⊕B)⊕C) = (A⊕(B⊕A)) for arbitrary C.Smocking
Sorry, it was a typo there .It should be ((A⊕B)⊕C) = (A⊕(B⊕C)).Adamic
W
3

There are many different ways of thinking about the XOR operation. Here are two of them and how they'd lead to the solution.

Intuition 1: XOR as Self-Inverting Addition

Let's suppose you have an array of numbers where all values but one appear twice and one value appears once. What happens if you add up all those numbers? You'd end up with something of the form 2a + b, where b is the value that appears once and a is the sum of all the values that appear twice. We'd like to somehow go from the sum 2a + b to just b, which means we need the 2a term to disappear. How can we do this?

With regular addition, you can't. But what if we had some sort of magical addition operator that had the property that x + x = 0 for any choice of x? In that case, the expression 2a + b would magically turn into b, the value you were looking for.

A very useful, helpful intuition to have for XOR is that it behaves kinda sorta ish like addition, except that adding the same value twice cancels out. This is a supremely useful property of XOR, and once you know it exists, you'll see it pop up from time to time. Here's sampler of where else it arises:

  • In explaining the solution to the impossible chessboard problem, Grant Sanderson of 3blue1brown fame describes solving the problem by recognizing he needed a weird form of addition where adding or subtracting a value are the same operation.
  • In the cuckoo filter data structure, this property of XOR is used to move items between different slots without necessarily being able to compute what those two slots are.
  • In an XOR linked list, this property is used to advance forward or backward in the linked list.
  • In cryptography, one-time pads, a theoretically unbreakable form of encryption, are often implemented using XOR (the Vernam cipher). XORing the message with the key generates the ciphertext, and XORing again produces the plaintext.
  • Fountain codes are a way of transmitting a message to a collection of receivers who aren't listening the whole time. XOR is used to pack multiple blocks together and then extract individual blocks when enough messages are received.

In that sense, if you're familiar with this particular perspective on the XOR operation, it's somewhat natural to arrive at the idea to apply it to this problem.

Intuition 2: XOR as Bit Parities

Here's another way to solve the problem. Let's try a concrete example. Suppose I have this list of numbers, where, as in the problem statement, all numbers appear twice except for one, which appears once.

45, 21, 21, 89, 41, 89, 41,
32, 96, 52, 30, 42, 76, 17,
36, 59, 96, 25, 15, 52, 36,
59, 76, 32, 15, 85, 95, 35,
25, 85, 30, 45, 95, 35, 17

For convenience, I've made everything exactly two digits long. How might we find which number appears exactly once? Well, since we know the number is two digits long, perhaps we could work it out one digit at a time. For example, what's its first digit? The main insight we need is to build a frequency histogram of the first digits of these numbers. If we do that, we get the following:

Digit:     0  1  2  3  4  5  6  7  8  9
Frequency: 0  2  4  8  5  4  0  2  4  6

Interesting - all of these frequencies are even except for the frequency of the digit 4. That must mean that the repeated number has a first digit of 4. Why? Well, whichever number appears only once will count once toward the frequency of its first digit. All other numbers, which appear twice, will count twice toward their digits. So the number that appears an odd number of times must be the one that corresponds to our mystery number. Neat! So that means our mystery number starts with a 4.

What about the second digit? We can work that out as well using the same trick. Here's the frequency histogram:

Digit:     0  1  2  3  4  5  6  7  8  9
Frequency: 2  4  5  0  0 12  6  2  0  4

Since 2 is the odd one out (literally!), it's the digit we want. So our number must be 42. Very cool!

This insight gives us an algorithm we can use to figure out what the mystery number is. Let's assume all our numbers are d digits long. We can then do the following:

for i = 1 to d:
    make a frequency histogram of the ith digits of each number
    find the one that appears an odd number of times
    write that down as the ith digit of the number
return the number we made this way

This algorithm has a runtime of O(nd) and uses O(d) auxiliary storage space. (There are d passes through the array, each of which visits n numbers, and overall we write down d digits.) If d is a constant - say, the numbers are all 10 digits or fewer, this runs in O(n) time and uses O(1) space.

This seems to bear little resemblance to our XOR solution, but it turns out that with just a little bit of tweaking we'll arrive at exactly our XOR algorithm. For starters, I was working with decimal digits in the example because that's how we typically think of numbers. But computers do all their work in binary (base 2), so perhaps we should meet the hardware where it is and use that instead. In that case, our algorithm looks like this, assuming our numbers are b bits long

for i = 1 to b:
    look at the ith bit of each number.
    if there's an odd number of 1 bits,
        write down that the ith bit of our number is 1
    else
        write down that the ith bit of our number is 0
return the number we made

This algorithm is basically the same as the one from before, just specialized for binary.

This is where XOR comes in, because another powerful intuition for XOR is that the XOR of a collection of bits is 1 if there's an odd number of 1 bits and 0 otherwise. So to implement the above algorithm, we could start by XORing together all the first bits of the numbers in our array, which will give us the first bit of the unique number, then XOR the second bits of our array, which will give us the second bit of our number, etc.

But we have a neat trick up our sleeves - we don't need to work one bit at a time! The bitwise XOR operator basically does this for all the bits of the number in a single go, and so we can just XOR all the numbers together and we'll be left with our mystery number.

This technique - using a bitwise XOR to determine the parity of a collection of bits - is another common application of XOR. You can see this same idea used in the following other places:

  • Parity bits are a simple way of checking for errors in data transmission. They work by writing down the XOR of all the bits in a message, which can detect if a single bit has been flipped. (This generalizes to Hamming codes), which uses a similar idea in a more elaborate way.
  • The odd sketch is a space-efficient data structure for estimating how many items are in a set by tracking whether an odd or even number of balls have landed in various bins. XORing them together combines the sketch together for two sets by adding the parities of the number of balls in each bin.

Hope this helps!

Weasel answered 28/6, 2022 at 3:57 Comment(0)
S
1

One simple operation that can be done on the array is to choose a property P and count the number of elements of the array that satisfy the property. For example, if P is the property of being divisible by 5, we can loop through the array and count the number of elements that are divisible by 5.

The precondition on the array allows us to gain information about the singleton element from such a count. If the number of elements satisfying P is odd, then the singleton has property P. If the number of elements satisfying P is even, then the singleton must not have property P. For example, if we count 3 elements divisible by 5, then the singleton must have been divisible by 5.

Therefore, if we can invent a collection of properties such that knowing whether each property is true or false will fully specify any element, we can get the answer by counting the number of elements with each property and checking the parity of the counts.

There are many different collections of properties that will work. We may as well be efficient, though. Given b different properties, there are (at most) 2**b different ways the tests could come out, so we can only distinguish from among this many possible elements. So, we need at least b different properties to fully specify a b-bit number.

One such minimal collection of properties that is easy for a computer to test is the collection where the nth property is "Does the number have a 1 in the nth bit?". If we know the answer to this question for each n, then clearly we can reconstruct the number.

Another optimization is that we don't have to keep track of the total count of elements satisfying a property; we only need the parity. If we just keep track of the count modulo 2, then we only need one bit to keep track instead of a whole size_t.

Since we are keeping track of b different bits of information, each one corresponding to a particular place value n, we can assemble these bits together into a b-bit number.

This is exactly the XOR solution presented in the question.

To start out with, the running count of how many numbers have each bit is 0 for each bit (hence result is initialized to 0). Then, when we XOR an element from the array, we are actually adding one modulo 2 to those bits of result where the element had a 1. Finally, result does not require any decoding, as the number with 1 bits exactly where result has 1 bits is result itself.

Sleave answered 1/2, 2017 at 9:42 Comment(0)
I
0

As I am sure you are familiar with, integers are stored in memory as tuples of binary digits. One can view each digit as a number in the field of two elements, which is essentially integers modulo 2. The ^ operator is component-wise xor, and with this interpretation, xor is simply addition. That is, we are adding the binary digits with each other.

In this field, 1 + 1 = 0, so one can say that two is zero. Since addition is commutative and associative we can combine all the numbers at once. Anything that is added an even number of times will add nothing, and only the number that is added a single time ends up in the result variable.

It might be interesting to know that the boolean operations are represented this way as (try it!): a xor b = a + b, a and b = ab, a or b = ab + a + b, not a = a + 1.

Inhere answered 1/2, 2017 at 14:9 Comment(0)
L
0
  • Several good answers are already posted.
  • Regarding your question, the note is pretty helpful:

Note: Your algorithm should have a linear runtime complexity. Could you implement it without using extra memory?

  • It says constant memory is desired on a linear time, which means we are going to visit each element of the array at least once, then we only need some type(s) of mathematical operations to solve the problem:

Here is a solution using math, with extra space though:

Python

class Solution:
    def singleNumber(self, nums):
        return (sum(set(nums)) << 1) - sum(nums)

Here are bitwise solutions:

C++

// The following block might slightly improve the execution time;
// Can be removed;
static const auto __optimize__ = []() {
    std::ios::sync_with_stdio(false);
    std::cin.tie(nullptr);
    std::cout.tie(nullptr);
    return 0;
}();

// Most of headers are already included;
// Can be removed;
#include <cstdint>

using ValueType = std::uint_fast16_t;

static const struct Solution {
    static const int singleNumber(
        const std::vector<int> nums
    ) {
        ValueType single_number = 0;

        for (ValueType index = 0; index < std::size(nums); index++) {
            single_number ^= nums[index];
        }

        return single_number;
    }
};

Java


public final class Solution {
    public static final int singleNumber(
        final int[] nums
    ) {
        int singleNumber = 0;

        for (int index = 0; index != nums.length; index++) {
            singleNumber ^= nums[index];
        }

        return singleNumber;
    }
}
Lighter answered 9/9, 2020 at 2:33 Comment(1)
For the Python solution, I think we can do this instead return sum(nums) - sum(set(nums)). It seems more natural to me and also reduces a shift operation.Drue

© 2022 - 2024 — McMap. All rights reserved.