CrackedRuby CrackedRuby

Bit Manipulation Algorithms

Overview

Bit manipulation algorithms operate directly on binary representations of numbers, using bitwise operators to perform computations at the bit level. These algorithms form the foundation for low-level optimizations, compression techniques, cryptographic operations, and systems programming tasks where direct control over individual bits provides performance advantages or specific functionality requirements.

The fundamental concept involves treating integers as sequences of bits rather than abstract numeric values. Each bit position represents a power of two, and operations can set, clear, toggle, or test individual bits or groups of bits. This direct manipulation bypasses higher-level arithmetic operations, often resulting in faster execution and more compact code.

Bit manipulation algorithms appear throughout computing systems. Network protocols use bit masking to extract packet headers. Graphics systems manipulate pixel data at the bit level for color operations. Database systems use bitmap indexes for query optimization. Embedded systems rely on bit manipulation to control hardware registers with minimal overhead.

# Binary representation of 13
number = 13
# => 0b1101 (binary: 1101)

# Check if third bit (0-indexed) is set
bit_set = (number & (1 << 3)) != 0
# => true (bit pattern: 1101, position 3 is 1)

The efficiency gains from bit manipulation stem from direct CPU support for bitwise operations. Modern processors execute bitwise operations in a single clock cycle, making them significantly faster than equivalent arithmetic or logical operations. A bit manipulation approach can replace multiple arithmetic operations with a single bitwise operation, reducing both execution time and code complexity.

Key Principles

Bit manipulation algorithms rely on fundamental bitwise operators that operate on the binary representation of integers. Ruby supports six core bitwise operators inherited from C-style languages.

The AND operator (&) performs a logical AND on corresponding bits. The result bit is 1 only when both input bits are 1. This operation masks specific bits, extracting portions of a binary value while zeroing out unwanted bits. Setting a mask with 1s in positions to preserve and 0s elsewhere, then ANDing with the target value, isolates the desired bits.

# Extract lower 4 bits from a byte
value = 0b11010110
mask = 0b00001111
result = value & mask
# => 0b00000110 (only lower 4 bits preserved)

The OR operator (|) performs a logical OR, producing 1 when either input bit is 1. This operation sets specific bits to 1 while leaving others unchanged. Creating a mask with 1s in positions to set, then ORing with the target value, forces those bits to 1.

The XOR operator (^) produces 1 when bits differ and 0 when they match. XOR serves multiple purposes: toggling bits, swapping values without temporary variables, and detecting differences between binary values. XORing a value with itself produces zero, and XORing with zero leaves the value unchanged.

# Toggle specific bits
value = 0b11010110
toggle_mask = 0b00110000
result = value ^ toggle_mask
# => 0b11100110 (bits at positions 4-5 toggled)

# Swap without temporary variable
a = 5  # 0b0101
b = 3  # 0b0011
a ^= b # a becomes 0b0110
b ^= a # b becomes 0b0101
a ^= b # a becomes 0b0011
# a is now 3, b is now 5

The NOT operator (~) inverts all bits, converting 0 to 1 and 1 to 0. In Ruby, this performs two's complement negation, effectively computing -(n+1). This behavior stems from how Ruby represents negative numbers using two's complement notation.

Left shift (<<) moves bits toward higher positions, filling vacated lower bits with zeros. Each left shift by one position multiplies the value by two. Right shift (>>) moves bits toward lower positions. Ruby provides arithmetic right shift that preserves the sign bit for negative numbers.

# Left shift multiplies by powers of 2
value = 5      # 0b0101
shifted = value << 2  # 0b010100
# => 20 (5 * 2^2)

# Right shift divides by powers of 2
value = 20     # 0b10100
shifted = value >> 2  # 0b101
# => 5 (20 / 2^2)

Bit manipulation algorithms combine these operators to perform complex operations. Setting a specific bit uses OR with a mask containing 1 at that position. Clearing a specific bit uses AND with a mask containing 0 at that position and 1s elsewhere. Testing a bit uses AND with a mask containing 1 at that position, checking if the result is non-zero.

The power of bit manipulation derives from understanding how binary representation relates to position values. The rightmost bit (position 0) represents 2^0 = 1. The next bit (position 1) represents 2^1 = 2. Each position represents twice the value of the position to its right. This positional notation allows algorithms to isolate and manipulate specific value ranges efficiently.

Ruby Implementation

Ruby provides direct access to bitwise operators through standard operators that work on integer values. Unlike languages with fixed-width integer types, Ruby integers grow automatically to accommodate any value, eliminating overflow concerns for most bit manipulation operations.

class BitManipulator
  # Set bit at position to 1
  def self.set_bit(number, position)
    number | (1 << position)
  end

  # Clear bit at position to 0
  def self.clear_bit(number, position)
    number & ~(1 << position)
  end

  # Toggle bit at position
  def self.toggle_bit(number, position)
    number ^ (1 << position)
  end

  # Check if bit at position is set
  def self.check_bit(number, position)
    (number & (1 << position)) != 0
  end
end

value = 0b1010
value = BitManipulator.set_bit(value, 0)
# => 0b1011
value = BitManipulator.clear_bit(value, 3)
# => 0b0011

Ruby's Integer class includes methods that support bit manipulation operations beyond basic operators. The [] operator extracts individual bits, providing cleaner syntax for bit testing. The bit_length method returns the number of bits required to represent the integer, excluding the sign bit.

number = 0b1101

# Extract bit at position using []
bit_2 = number[2]
# => 1

# Get number of significant bits
bits_needed = number.bit_length
# => 4

# Check multiple bit positions
(0...bits_needed).map { |i| number[i] }
# => [1, 0, 1, 1]

Counting set bits (popcount or Hamming weight) determines how many 1 bits appear in the binary representation. The Brian Kernighan algorithm provides an efficient implementation that runs in time proportional to the number of set bits rather than the total number of bits.

def count_set_bits(number)
  count = 0
  while number > 0
    number &= number - 1  # Clear rightmost set bit
    count += 1
  end
  count
end

count_set_bits(0b1011)
# => 3

# Alternative using String conversion
def count_set_bits_alt(number)
  number.to_s(2).count('1')
end

The operation number & (number - 1) clears the rightmost set bit. Subtracting 1 from a number flips all bits after the rightmost set bit, including that bit. ANDing the original with this value eliminates the rightmost 1. Repeating this operation until the number becomes zero counts all set bits.

Checking if a number is a power of two exploits the property that powers of two have exactly one set bit in their binary representation. A power of two ANDed with one less than itself always produces zero, since the single set bit in the power of two gets cleared.

def power_of_two?(number)
  return false if number <= 0
  (number & (number - 1)) == 0
end

power_of_two?(16)  # 0b10000
# => true
power_of_two?(18)  # 0b10010
# => false

Finding the rightmost set bit extracts the position of the lowest 1 bit. The operation number & -number isolates this bit. Negating a number in two's complement flips all bits and adds 1, which causes all trailing zeros to remain zero and the first 1 to stay 1, while clearing all higher bits.

def rightmost_set_bit(number)
  number & -number
end

rightmost_set_bit(0b1011000)
# => 0b1000 (bit at position 3)

def rightmost_set_bit_position(number)
  return -1 if number == 0
  isolated = number & -number
  (isolated.bit_length - 1)
end

rightmost_set_bit_position(0b1011000)
# => 3

Practical Examples

Bitmap-based sets represent collections of integers using individual bits. Each bit position corresponds to a possible element value, with 1 indicating presence and 0 indicating absence. This representation provides constant-time membership testing and set operations using bitwise operators.

class BitSet
  def initialize
    @bits = 0
  end

  def add(element)
    raise ArgumentError, "Element must be non-negative" if element < 0
    @bits |= (1 << element)
  end

  def remove(element)
    @bits &= ~(1 << element)
  end

  def include?(element)
    return false if element < 0
    (@bits & (1 << element)) != 0
  end

  def union(other)
    result = BitSet.new
    result.instance_variable_set(:@bits, @bits | other.instance_variable_get(:@bits))
    result
  end

  def intersection(other)
    result = BitSet.new
    result.instance_variable_set(:@bits, @bits & other.instance_variable_get(:@bits))
    result
  end

  def to_a
    elements = []
    temp = @bits
    position = 0
    while temp > 0
      if (temp & 1) == 1
        elements << position
      end
      temp >>= 1
      position += 1
    end
    elements
  end
end

set1 = BitSet.new
set1.add(1)
set1.add(3)
set1.add(5)
set1.to_a
# => [1, 3, 5]

set2 = BitSet.new
set2.add(3)
set2.add(4)
set2.add(5)

set1.union(set2).to_a
# => [1, 3, 4, 5]
set1.intersection(set2).to_a
# => [3, 5]

Permission systems often encode multiple boolean flags into a single integer. Each bit represents a specific permission, allowing compact storage and efficient permission checking using bitwise operations. This pattern appears in Unix file permissions, database access controls, and application authorization systems.

module Permissions
  READ    = 1 << 0  # 0b001
  WRITE   = 1 << 1  # 0b010
  EXECUTE = 1 << 2  # 0b100
  DELETE  = 1 << 3  # 0b1000
  ADMIN   = 1 << 4  # 0b10000
end

class User
  attr_reader :permissions

  def initialize
    @permissions = 0
  end

  def grant(permission)
    @permissions |= permission
  end

  def revoke(permission)
    @permissions &= ~permission
  end

  def has_permission?(permission)
    (@permissions & permission) == permission
  end

  def has_any_permission?(*perms)
    combined = perms.reduce(0) { |acc, p| acc | p }
    (@permissions & combined) != 0
  end
end

user = User.new
user.grant(Permissions::READ | Permissions::WRITE)
user.has_permission?(Permissions::READ)
# => true
user.has_permission?(Permissions::EXECUTE)
# => false
user.has_any_permission?(Permissions::EXECUTE, Permissions::WRITE)
# => true

Extracting and modifying bit ranges handles structured binary data where multiple fields pack into fixed-width values. Network protocols, file formats, and hardware registers use this pattern to maximize space efficiency.

class BitField
  def initialize(value = 0)
    @value = value
  end

  # Extract bits from start position with given length
  def extract(start, length)
    mask = (1 << length) - 1
    (@value >> start) & mask
  end

  # Set bits at start position with given length to new value
  def set_field(start, length, new_value)
    mask = (1 << length) - 1
    # Clear the field
    @value &= ~(mask << start)
    # Set the new value
    @value |= ((new_value & mask) << start)
  end

  def to_i
    @value
  end
end

# Example: RGB color packed into 24 bits
# Format: RRRRRRRRGGGGGGGGBBBBBBBB
color = BitField.new(0xFF8040)

red = color.extract(16, 8)
# => 255
green = color.extract(8, 8)
# => 128
blue = color.extract(0, 8)
# => 64

color.set_field(8, 8, 200)  # Change green component
color.to_i.to_s(16)
# => "ffc840"

Finding the next power of two greater than or equal to a given number supports memory allocation and buffer sizing. The algorithm sets all bits after the highest set bit to 1, then adds 1 to round up.

def next_power_of_two(number)
  return 1 if number <= 0
  return number if (number & (number - 1)) == 0  # Already power of 2

  # Set all bits after highest set bit
  number -= 1
  number |= number >> 1
  number |= number >> 2
  number |= number >> 4
  number |= number >> 8
  number |= number >> 16
  number |= number >> 32
  number + 1
end

next_power_of_two(100)
# => 128
next_power_of_two(64)
# => 64
next_power_of_two(65)
# => 128

Common Patterns

The single bit mask pattern creates masks for individual bit positions. Rather than hardcoding binary literals, shifting 1 by the position value generates the appropriate mask dynamically. This pattern appears in all bit manipulation operations that target specific positions.

# Create mask for bit position
def bit_mask(position)
  1 << position
end

# Use masks for multiple operations
position = 5
set_mask = bit_mask(position)
clear_mask = ~bit_mask(position)

value = 0b100000
value |= set_mask      # Set bit 5
value &= clear_mask    # Clear bit 5

The range mask pattern generates masks covering multiple consecutive bits. Creating a mask with N consecutive 1 bits involves shifting 1 left by N positions and subtracting 1. This produces N bits of value 1 starting from position 0.

def range_mask(length)
  (1 << length) - 1
end

# Extract lower 8 bits
value = 0xABCD
lower_byte = value & range_mask(8)
# => 0xCD

# Extract bits 4 through 7
def extract_range(value, start, length)
  (value >> start) & range_mask(length)
end

extract_range(0b11010110, 2, 4)
# => 0b0101

The clear lowest bit pattern repeatedly removes the rightmost set bit. The operation n & (n - 1) appears in algorithms that process each set bit individually or count total set bits. This pattern runs in time proportional to the number of set bits.

def process_each_set_bit(number)
  bits = []
  temp = number
  while temp > 0
    rightmost = temp & -temp  # Isolate rightmost bit
    bits << Math.log2(rightmost).to_i  # Get position
    temp &= temp - 1  # Clear rightmost bit
  end
  bits
end

process_each_set_bit(0b10110)
# => [1, 2, 4]

The XOR accumulator pattern detects duplicates or finds unique elements. XORing all elements together causes pairs to cancel out, leaving unpaired elements. This pattern solves the classic "find the unique element" problem when exactly one element appears an odd number of times.

def find_unique_element(array)
  array.reduce(0) { |acc, n| acc ^ n }
end

find_unique_element([4, 2, 4, 5, 2])
# => 5

# Find two unique elements when all others appear twice
def find_two_unique(array)
  xor_all = array.reduce(0) { |acc, n| acc ^ n }
  
  # Find a bit position where the two numbers differ
  rightmost_bit = xor_all & -xor_all
  
  # Partition into two groups and XOR each group
  group1 = group2 = 0
  array.each do |n|
    if (n & rightmost_bit) != 0
      group1 ^= n
    else
      group2 ^= n
    end
  end
  
  [group1, group2]
end

find_two_unique([4, 2, 4, 5, 2, 7])
# => [5, 7]

The bit counting pattern appears in various forms across algorithms. Beyond the Brian Kernighan algorithm, lookup table approaches precompute counts for small ranges and combine results for larger numbers.

# Build lookup table for 8-bit counts
BIT_COUNT_TABLE = (0..255).map { |i| i.to_s(2).count('1') }

def count_bits_fast(number)
  count = 0
  while number > 0
    count += BIT_COUNT_TABLE[number & 0xFF]
    number >>= 8
  end
  count
end

The sign extraction pattern determines if a number is positive, negative, or zero using bit operations. The sign bit (highest bit) indicates negativity in two's complement representation.

def sign(number)
  return 0 if number == 0
  # Right shift by word size minus 1 to move sign bit to LSB
  # Ruby integers are variable width, so use comparison instead
  number >> (number.bit_length)
end

# More portable sign extraction
def sign_portable(number)
  return 0 if number.zero?
  number.positive? ? 1 : -1
end

Performance Considerations

Bit manipulation operations execute as single CPU instructions, providing substantial performance advantages over equivalent arithmetic or logical operations. Modern processors complete bitwise AND, OR, XOR, and shift operations in one clock cycle, while arithmetic operations may require multiple cycles or pipeline stages.

require 'benchmark'

# Compare bit manipulation vs arithmetic for power of 2 check
n = 1_000_000

Benchmark.bm(20) do |x|
  x.report("Bit manipulation:") do
    n.times { |i| (i & (i - 1)) == 0 }
  end
  
  x.report("Math.log2:") do
    n.times { |i| Math.log2(i) % 1 == 0 rescue false }
  end
end

# Bit manipulation typically 10-50x faster

The performance characteristics of bit manipulation algorithms depend on the number of bits processed rather than the magnitude of the values. Counting set bits using Brian Kernighan's algorithm runs in O(k) time where k is the number of set bits, not the number of total bits. For sparse bit patterns, this provides significant advantages over iterating through all bit positions.

Memory access patterns affect bit manipulation performance. Operations on data already in CPU registers complete extremely fast, while loading values from memory introduces latency. Batch processing multiple bit manipulations on the same value amortizes memory access costs across operations.

# Inefficient: Multiple memory accesses
def slow_bit_operations(array)
  array.each do |value|
    check_bit(value, 0)
    check_bit(value, 1)
    check_bit(value, 2)
  end
end

# Efficient: Single load, multiple operations
def fast_bit_operations(array)
  array.each do |value|
    bit0 = (value & 1) != 0
    bit1 = (value & 2) != 0
    bit2 = (value & 4) != 0
    # Process all bits together
  end
end

Branch prediction failures impact performance in bit manipulation code that conditionally executes based on bit values. Modern CPUs predict branch outcomes based on historical patterns. Unpredictable branching on bit values causes pipeline stalls when predictions fail.

# Branch-heavy code prone to misprediction
def process_with_branches(numbers)
  numbers.map do |n|
    if (n & 1) != 0
      n * 2
    else
      n / 2
    end
  end
end

# Branchless using bit manipulation
def process_branchless(numbers)
  numbers.map do |n|
    odd_bit = n & 1
    # Multiply by 2 if odd (left shift), divide by 2 if even (right shift)
    (n << odd_bit) | (n >> (1 - odd_bit))
  end
end

Ruby's arbitrary precision integers introduce overhead for very large numbers. When bit counts exceed the native word size (typically 64 bits), Ruby allocates additional memory and performs multi-precision arithmetic. Keeping values within native integer sizes maximizes performance.

Lookup tables trade memory for speed by precomputing results for common inputs. Bit counting benefits significantly from lookup tables that map 8 or 16-bit values to their bit counts.

# Precompute counts for all 16-bit values
NIBBLE_COUNTS = (0..15).map { |i| i.to_s(2).count('1') }

def count_bits_table(number)
  count = 0
  while number > 0
    count += NIBBLE_COUNTS[number & 0xF]
    number >>= 4
  end
  count
end

Compiler optimizations may transform arithmetic operations into bit manipulations automatically. Multiplication or division by powers of two typically compiles to shift operations. Writing explicit bit manipulations documents intent and ensures the optimization occurs.

Common Pitfalls

Sign extension affects right shift operations on negative numbers. Ruby performs arithmetic right shift, which preserves the sign bit by filling shifted positions with copies of the sign bit. This maintains the value's sign but produces unexpected results when treating integers as unsigned bit patterns.

# Arithmetic right shift on negative number
value = -8  # Binary (two's complement): ...11111000
shifted = value >> 2
# => -2 (Binary: ...11111110, not ...00111110)

# Use bit masking for unsigned-style right shift
def unsigned_right_shift(value, positions)
  mask = (1 << (value.bit_length - positions)) - 1
  (value >> positions) & mask
end

Integer overflow does not occur in Ruby due to automatic bignum promotion, but algorithms designed for fixed-width integers may produce incorrect results when ported directly. Code that relies on overflow wrapping behavior requires explicit modulo operations.

# C-style overflow assumption (incorrect in Ruby)
def increment_with_overflow(byte_value)
  (byte_value + 1) & 0xFF  # Explicit mask needed
end

# Ruby integers grow instead of wrapping
value = 255
value + 1
# => 256 (not 0)

Off-by-one errors plague bit position calculations. Bits numbered from 0 to N-1 for an N-bit number, but intuitive counting starts at 1. Confusion between bit position and bit value causes incorrect mask generation.

# Wrong: Using bit value instead of position
def wrong_set_bit(number, position)
  number | position  # Should be (1 << position)
end

# Correct: Shift 1 to position
def correct_set_bit(number, position)
  number | (1 << position)
end

Negative numbers in two's complement representation have all high-order bits set to 1. Extracting bit ranges from negative numbers without masking includes these sign bits, producing unexpected values.

value = -1  # All bits set to 1
# Attempting to extract lower 8 bits
lower_bits = value & 0xFF
# => 255 (correct with mask)

# Without mask
wrong_extract = value >> 0
# => -1 (sign extension preserves all 1s)

Endianness affects interpretation of multi-byte values as bit sequences. When extracting bits from integers representing packed bytes, the bit numbering depends on whether the most significant byte appears first (big-endian) or last (little-endian).

# Packed as 0xAABBCCDD
value = 0xAABBCCDD

# Extract third byte (different results based on endianness assumption)
third_byte_big_endian = (value >> 8) & 0xFF
# => 0xCC (assuming big-endian bit numbering)

third_byte_little_endian = (value >> 16) & 0xFF
# => 0xBB (assuming little-endian bit numbering)

Bit manipulation on floats requires converting to integer representation. Ruby floats use IEEE 754 format, which encodes sign, exponent, and mantissa as separate bit fields. Extracting these fields requires proper understanding of the floating-point format.

# Cannot directly manipulate float bits in pure Ruby
value = 3.14

# Must pack/unpack to access bit representation
packed = [value].pack('D')
int_bits = packed.unpack1('Q')
# Now can manipulate bits of integer representation

Precedence errors occur when mixing bitwise and comparison operators. Bitwise operators have lower precedence than comparison operators, requiring parentheses to enforce intended evaluation order.

# Wrong precedence
value = 0b1010
if value & 0b0010 != 0  # Parsed as: value & (0b0010 != 0)
  puts "Bit 1 is set"
end

# Correct with parentheses
if (value & 0b0010) != 0
  puts "Bit 1 is set"
end

Reference

Bitwise Operators

Operator Name Description Example
& AND Returns 1 if both bits are 1 0b1100 & 0b1010 = 0b1000
| OR Returns 1 if either bit is 1 0b1100 | 0b1010 = 0b1110
^ XOR Returns 1 if bits differ 0b1100 ^ 0b1010 = 0b0110
~ NOT Inverts all bits ~0b1100 = -13
<< Left shift Shifts bits left, fills with 0 0b0011 << 2 = 0b1100
>> Right shift Shifts bits right, sign extends 0b1100 >> 2 = 0b0011

Common Bit Manipulation Operations

Operation Formula Purpose
Set bit at position n x | (1 << n) Sets nth bit to 1
Clear bit at position n x & ~(1 << n) Sets nth bit to 0
Toggle bit at position n x ^ (1 << n) Flips nth bit
Check bit at position n (x & (1 << n)) != 0 Tests if nth bit is 1
Clear rightmost set bit x & (x - 1) Removes lowest 1 bit
Isolate rightmost set bit x & -x Extracts lowest 1 bit
Create mask of n bits (1 << n) - 1 Generates n consecutive 1s
Check power of two (x & (x - 1)) == 0 True if exactly one bit set

Common Algorithms

Algorithm Time Complexity Space Complexity Use Case
Count set bits (Kernighan) O(k) where k = set bits O(1) Population count
Count set bits (lookup) O(n/8) where n = bit width O(256) for 8-bit table Fast counting with memory
Find unique element (XOR) O(n) O(1) Find non-duplicate in pairs
Check power of two O(1) O(1) Validate power-of-2 values
Next power of two O(log w) where w = word size O(1) Round up to power of 2
Extract bit range O(1) O(1) Parse packed fields

Ruby Integer Methods

Method Description Example
[position] Extract bit at position 0b1010[1] returns 1
bit_length Number of significant bits 15.bit_length returns 4
to_s(2) Convert to binary string 10.to_s(2) returns "1010"
& Bitwise AND 12 & 10 returns 8
| Bitwise OR 12 | 10 returns 14
^ Bitwise XOR 12 ^ 10 returns 6
~ Bitwise NOT ~10 returns -11
<< Left shift 5 << 2 returns 20
>> Right shift 20 >> 2 returns 5

Bit Pattern Identities

Identity Result Application
x & 0 0 Clear all bits
x & ~0 x Preserve all bits
x | 0 x No change
x | ~0 ~0 Set all bits
x ^ 0 x No change
x ^ ~0 ~x Invert all bits
x ^ x 0 Cancel out
x & x x Identity
x | x x Identity