Skip to content

Programming

2 posts with the tag “Programming”

Mastering XOR Magic: Essential Party Tricks for Every Programmer

The XOR operation, or exclusive OR, holds a deceptively simple yet profoundly powerful property: applying XOR with the same value twice restores the original. Mathematically, for any bits A and B, A XOR B XOR B = A. This idempotent behavior—where the operation is its own inverse—unlocks a treasure trove of clever programming hacks. Let’s explore these “party tricks” that demonstrate XOR’s elegance, from quick demos to data structure innovations.

To see this in action, fire up Python and test all bit combinations:

for a in range(2):
for b in range(2):
assert a ^ b ^ b == a, f"Failed for a={a}, b={b}"
print("XOR property holds for all 1-bit cases!")

Since it works per bit, it scales to entire integers. This foundation enables everything that follows.

XOR shines in symmetric encryption. Convert a message like “hello world” to integers, XOR each with a key (say, 69), and you’ve got ciphertext. Decrypt by XORing again with the same key:

def encrypt(message, key):
return ''.join(chr(ord(c) ^ key) for c in message)
msg = "hello world"
key = 69
encrypted = encrypt(msg, key)
decrypted = encrypt(encrypted, key)
print(decrypted) # Back to "hello world"
wrong_key_decrypt = encrypt(encrypted, 42) # Gibberish!

This is a toy example—vulnerable to frequency analysis and known-plaintext attacks. Never use it in production, but it’s a fantastic illustration of XOR’s reversibility.

Swapping Variables Without a Temp (Even in C)

Section titled “Swapping Variables Without a Temp (Even in C)”

Modern languages like Python allow a, b = b, a. In C, without multiple assignment, XOR does the heavy lifting:

#include <stdio.h>
int main() {
int a = 69, b = 420;
printf("Before: a=%d, b=%d\n", a, b);
a ^= b; // a = 69 ^ 420
b ^= a; // b = 420 ^ (69 ^ 420) = 69
a ^= b; // a = (69 ^ 420) ^ 69 = 420
printf("After: a=%d, b=%d\n", a, b);
return 0;
}

No extra variables needed! Compilers optimize anyway, but this bitwise dance is a classic interview flex. (Pro tip: Addition-based swaps like a += b; b = a - b; a -= b; exist too, but XOR avoids overflow.)

Detecting the Duplicate in an Unsorted Array

Section titled “Detecting the Duplicate in an Unsorted Array”

Given numbers 1 to 100 with one duplicate (array size 101), find it in O(n) time without sorting. XOR all expected numbers (1^2^…^100), then XOR with array elements. Unique pairs cancel; the duplicate remains:

#include <stdio.h>
#include <stdlib.h>
#include <time.h>
int main() {
srand(time(NULL));
int arr[101]; // 1-100 + one dupe
// Populate randomly with dupe (omitted for brevity)
int x = 0;
for (int i = 1; i <= 100; ++i) x ^= i;
for (int i = 0; i < 101; ++i) x ^= arr[i];
printf("Duplicate: %d\n", x);
return 0;
}

Brilliant for its constant space and linear time. Interviews love it—though it reveals more about memorization than skill.

The XOR Linked List: Half the Pointer Overhead

Section titled “The XOR Linked List: Half the Pointer Overhead”

Doubly linked lists store prev and next pointers per node, doubling pointer memory. XOR them into one field (zord = prev ^ next), halving usage (payload excluded).

Node traversal: Start with prev = NULL, compute next = zord ^ prev, print/update prev = current.

Here’s a minimal C implementation:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdint.h>
#include <assert.h>
typedef struct Node {
int value;
uintptr_t zord; // Size of pointer, XOR of prev ^ next
} Node;
Node* node_create(int value) {
Node* node = malloc(sizeof(*node));
memset(node, 0, sizeof(*node));
node->value = value;
return node;
}
typedef struct LinkedList {
Node* begin;
Node* end;
} LinkedList;
void list_append(LinkedList* list, int value) {
Node* new_node = node_create(value);
if (!list->end) { // Empty list
list->begin = list->end = new_node;
return;
}
// Link to end
list->end->zord ^= (uintptr_t)new_node;
new_node->zord = (uintptr_t)list->end;
list->end = new_node;
}
Node* node_next(Node** prev_ptr, Node* curr) {
Node* next = (Node*)(curr->zord ^ (uintptr_t)*prev_ptr);
*prev_ptr = curr;
return next;
}
int main() {
LinkedList list = {0};
for (int i = 5; i <= 10; ++i) {
list_append(&list, i);
}
Node* prev = NULL;
Node* it = list.begin;
do {
printf("%d ", it->value);
it = node_next(&prev, it);
} while (it);
printf("\n"); // 5 6 7 8 9 10
return 0;
}

Bonus: Start from end with prev = NULL for reverse traversal. Null endpoints simplify edge cases (zord == 0 means isolated).

These tricks, while not production staples, sharpen bitwise intuition. The property A XOR B XOR B = A (or x ^ 0 = x) is XOR’s superpower—commutative, associative, and self-inverse. Next coding interview, dazzle with it. Got more XOR hacks? The bit manipulation well runs deep.

Mastering Git: The Snapshot Database That Powers Your Workflow

You’ve likely used Git daily—committing code, pushing updates, pulling changes. It hums along smoothly until chaos strikes: a botched rebase at midnight, frantic Stack Overflow searches, and crossed fingers that things don’t spiral further. Even seasoned developers often treat Git like a black box, memorizing commands without grasping the mechanics. In this guide, we’ll dismantle Git from its core, rebuilding your understanding so you command it confidently.

At its heart, Git is a database of snapshots, where the atomic unit is the commit. Forget diffs or change lists—a commit captures your entire project state at a precise moment. Every file, unchanged or modified, frozen in time.

Each commit holds:

  • A full snapshot pointer (your codebase as-is).
  • Metadata (author, timestamp, message).
  • A pointer to its parent commit—the previous state.

New commits link backward, forming a chain: child to parent, parent to grandparent, back to the initial commit (with no parent). Merges later introduce commits with two parents, but the rule holds: pointers always flow backward. Parents remain oblivious to future offspring.

This setup yields a linear history for solo sequential commits. Real teams branch for features or fixes, creating diverging paths from shared parents. Merges reconnect them, birthing a DAG (Directed Acyclic Graph):

  • Directed: One-way arrows (children → parents).
  • Acyclic: No loops—history can’t circle back.
  • Graph: Nodes (commits) + edges (parent links).

Visualize it as an inverted family tree encoding every project decision. Git’s magic? Every commit’s completeness lets you teleport to any node, restoring the exact project state—no change replay needed.

Branches intimidate newcomers, evoking visions of duplicated codebases. Wrong. A branch is a sticky note—a lightweight file storing one commit hash.

  • git branch feature/login crafts a note at the current commit.
  • Commits ignore branches; branches chase commits.

Commit on a branch? Git adds the snapshot (parented to prior), then nudges the branch pointer forward. Creation is instantaneous—no copying.

main (or master)? Just the canonical sticky note. Multiple branches = multiple labels on the DAG.

Enter HEAD, Git’s cursor. Typically, it points to a branch (e.g., HEAD → main → commit). Switch branches (git checkout feature), and HEAD shifts.

Checkout a raw hash? HEAD detaches, pointing directly to the commit—“detached HEAD” state. Work proceeds: edit, stage, commit. But stray too far, and new commits orphan—no branch anchors them. Git’s garbage collector eventually prunes them.

Classic pitfall: Inspecting an old commit, fixing a bug, committing, then git checkout main. Poof—orphan commits vanish. heed the warning: branch first to preserve work.

Git juggles three realms:

  1. Working Directory: Your editable files (editor-visible).
  2. Staging Area (Index): Prep zone for the next commit.
  3. Repository: Immutable commit database.

Edit files → changes hit working directory (Git observes silently). git add → stage for commit. git commit → snapshot to repo. This triad unlocks nuanced commands.

Three “undo” tools, wildly distinct:

git checkout main or git checkout <hash> repositions HEAD. Working directory syncs to the target snapshot. Branches/commits untouched—just sightseeing history.

On main, git reset <commit> yanks main’s pointer back, orphaning ahead-commits. Modes dictate side effects:

ModeBranch MovesStagingWorking DirectoryUse Case
--softYesUnchangedUnchangedSquash commits (staged changes ready).
--mixed (default)YesReset to targetUnchanged (unstaged)Restage/split commits.
--hardYesResetReset (data loss!)Nuke uncommitted work—use sparingly.

--hard devours uncommitted files forever. Orphaned commits linger briefly (reflog-rescu-able); unstaged work? Eternal void.

No rewinds—git revert <commit> births a new commit undoing the target (e.g., +50 lines → -50 lines). History intact, auditable. Ideal for shared/pushed changes.

Quick Reference:

  • Checkout: Explore → safe.
  • Reset: Reshape local → cautious.
  • Revert: Amend shared → collaborative.

Feature branch forked pre-main advances (X, Y)? Integrate via:

  • Merge: Two-parent commit preserves parallel truth (messy but honest).
  • Rebase: Replay your commits atop new main.

Commits aren’t movable—their hash derives from content + metadata + parent. Rebase:

  1. Extracts changes from your commits (B → diff, C → diff).
  2. Applies atop tip (Y → B’ → C’).
  3. Repoints branch to C’; orphans B/C.

Power for local cleanliness; poison for shared history—colleagues’ clones see “new” commits, sparking duplicate/conflict hell. Rebase solo; merge teams.

Disaster? git reflog logs HEAD’s travels: checkouts, commits, resets. “Lost” commits (post-reset/rebase) often persist here. git branch recovery <hash> revives them. Git delays deletion (30-90 days)—act fast.

Git: Snapshot DAG. Branches/HEAD as pointers. Three trees for precision. Checkout views; reset reshapes; revert augments; rebase replays. Reflog recovers.

Next Git snag? Trace the graph. No more blind commands—you know.