CrackedRuby CrackedRuby

Overview

Interpolation search improves upon binary search by estimating where a target value should be located within a sorted array. While binary search always divides the search space in half, interpolation search calculates a probe position based on the target value's expected location, similar to how a person searches a phone book by opening it near the beginning for names starting with 'A' or near the end for names starting with 'Z'.

The algorithm applies to sorted arrays where values follow a relatively uniform distribution. When data meets this criterion, interpolation search achieves O(log log n) time complexity for successful searches, compared to binary search's O(log n). However, for non-uniform distributions or worst-case scenarios, performance degrades to O(n).

The core calculation uses linear interpolation to estimate the probe position:

# Position calculation formula
pos = low + ((target - arr[low]) * (high - low)) / (arr[high] - arr[low])

This formula positions the probe proportionally between the low and high bounds based on where the target value falls within the range of values at those bounds.

Interpolation search originated in the 1960s as researchers explored alternatives to binary search for specific data patterns. The algorithm finds practical application in scenarios with uniformly distributed sorted data, such as searching through sorted timestamps, sequential identifiers, or evenly spaced numerical datasets.

Key Principles

Interpolation search operates on three fundamental principles: sorted data, value-based position estimation, and adaptive search space reduction.

Sorted Data Requirement

The algorithm requires input data sorted in ascending or descending order. Unlike linear search, interpolation search depends on the ordering to make meaningful position estimates. Unsorted data produces incorrect probe positions and invalid results.

Value-Based Position Estimation

The algorithm calculates probe positions using the numeric values of the target and the elements at the current search boundaries. The interpolation formula determines what proportion of the remaining search space should be skipped based on where the target value falls within the range of boundary values.

For an array with values [10, 20, 30, 40, 50] when searching for 35:

  • Low bound value: 10 at index 0
  • High bound value: 50 at index 4
  • Target value: 35
  • Position estimate: 0 + ((35 - 10) * (4 - 0)) / (50 - 10) = 2.5 ≈ 2

This calculation places the probe at index 2 (value 30), closer to the target than a binary search's midpoint would be.

Adaptive Search Space Reduction

After each probe, the algorithm adjusts the search space based on the comparison between the probed value and the target. When the probed value is less than the target, the new low bound becomes the probe position plus one. When the probed value exceeds the target, the new high bound becomes the probe position minus one. This continues until the target is found or the search space becomes invalid.

Distribution Sensitivity

The algorithm's performance directly correlates with data distribution uniformity. Uniformly distributed data allows accurate position estimates, reducing the number of probes needed. Non-uniform distributions cause position estimates to deviate from optimal locations, increasing probe counts and degrading performance toward linear search characteristics.

Numeric Value Requirements

Interpolation search requires numeric comparisons for position calculation. While the algorithm can work with custom objects, those objects must provide numeric representations that maintain the sorted order's meaning. Character data requires conversion to numeric codes, and complex objects need explicit numeric key extraction.

Termination Conditions

The search terminates under three conditions: the target value matches the probed element, the search space becomes empty (low > high), or the remaining search space contains values outside the target's possible range. The third condition provides early termination when the target cannot exist within the remaining elements.

Ruby Implementation

Ruby's duck typing and operator overloading enable clean interpolation search implementations that work with various numeric types. The basic implementation handles integer and float arrays with uniform distribution assumptions.

def interpolation_search(arr, target)
  low = 0
  high = arr.length - 1
  
  while low <= high && target >= arr[low] && target <= arr[high]
    # Handle single element case
    return low if low == high && arr[low] == target
    return -1 if low == high
    
    # Calculate probe position
    pos = low + ((target - arr[low]) * (high - low)) / 
                (arr[high] - arr[low])
    
    # Check if target found
    return pos if arr[pos] == target
    
    # Adjust search space
    if arr[pos] < target
      low = pos + 1
    else
      high = pos - 1
    end
  end
  
  -1  # Target not found
end

# Example usage
numbers = [10, 20, 30, 40, 50, 60, 70, 80, 90, 100]
result = interpolation_search(numbers, 70)
# => 6

Float Handling

Float arrays require tolerance-based comparisons to handle floating-point precision issues:

def interpolation_search_float(arr, target, epsilon = 1e-10)
  low = 0
  high = arr.length - 1
  
  while low <= high && target >= arr[low] - epsilon && 
        target <= arr[high] + epsilon
    return low if low == high && (arr[low] - target).abs < epsilon
    return -1 if low == high
    
    # Position calculation with float division
    pos = low + ((target - arr[low]) * (high - low).to_f / 
                (arr[high] - arr[low]))
    pos = pos.round.clamp(low, high)
    
    if (arr[pos] - target).abs < epsilon
      return pos
    elsif arr[pos] < target
      low = pos + 1
    else
      high = pos - 1
    end
  end
  
  -1
end

# Example with floats
prices = [10.5, 20.3, 30.7, 40.1, 50.9, 60.2, 70.8, 80.4, 90.6, 100.0]
result = interpolation_search_float(prices, 60.2)
# => 5

Custom Object Implementation

Objects with numeric attributes can use interpolation search through key extraction:

class Record
  attr_reader :id, :name
  
  def initialize(id, name)
    @id = id
    @name = name
  end
end

def interpolation_search_by_key(arr, target_key)
  low = 0
  high = arr.length - 1
  
  while low <= high && target_key >= arr[low].id && 
        target_key <= arr[high].id
    return low if low == high && arr[low].id == target_key
    return -1 if low == high
    
    pos = low + ((target_key - arr[low].id) * (high - low)) / 
                (arr[high].id - arr[low].id)
    
    return pos if arr[pos].id == target_key
    
    if arr[pos].id < target_key
      low = pos + 1
    else
      high = pos - 1
    end
  end
  
  -1
end

# Example usage
records = [
  Record.new(100, "Alice"),
  Record.new(200, "Bob"),
  Record.new(300, "Carol"),
  Record.new(400, "David"),
  Record.new(500, "Eve")
]

index = interpolation_search_by_key(records, 300)
# => 2

Range-Based Search

Finding all occurrences within a range requires modifications to track both boundaries:

def interpolation_search_range(arr, target)
  index = interpolation_search(arr, target)
  return [] if index == -1
  
  # Find leftmost occurrence
  left = index
  while left > 0 && arr[left - 1] == target
    left -= 1
  end
  
  # Find rightmost occurrence
  right = index
  while right < arr.length - 1 && arr[right + 1] == target
    right += 1
  end
  
  (left..right).to_a
end

# Example with duplicates
values = [10, 20, 30, 30, 30, 40, 50, 60, 70, 80]
indices = interpolation_search_range(values, 30)
# => [2, 3, 4]

Recursive Implementation

A recursive approach provides a functional programming style alternative:

def interpolation_search_recursive(arr, target, low = 0, high = arr.length - 1)
  return -1 if low > high || target < arr[low] || target > arr[high]
  return low if low == high && arr[low] == target
  return -1 if low == high
  
  pos = low + ((target - arr[low]) * (high - low)) / (arr[high] - arr[low])
  
  return pos if arr[pos] == target
  
  if arr[pos] < target
    interpolation_search_recursive(arr, target, pos + 1, high)
  else
    interpolation_search_recursive(arr, target, low, pos - 1)
  end
end

# Example usage
data = [5, 10, 15, 20, 25, 30, 35, 40, 45, 50]
result = interpolation_search_recursive(data, 25)
# => 4

Practical Examples

Searching Timestamp Logs

Log files with sequential timestamps demonstrate interpolation search's effectiveness with uniformly distributed time-based data:

class LogEntry
  attr_reader :timestamp, :message
  
  def initialize(timestamp, message)
    @timestamp = timestamp
    @message = message
  end
end

def search_logs_by_time(logs, target_time)
  low = 0
  high = logs.length - 1
  
  while low <= high && target_time >= logs[low].timestamp && 
        target_time <= logs[high].timestamp
    return low if low == high && logs[low].timestamp == target_time
    return -1 if low == high
    
    pos = low + ((target_time - logs[low].timestamp) * (high - low)) / 
                (logs[high].timestamp - logs[low].timestamp)
    
    return pos if logs[pos].timestamp == target_time
    
    if logs[pos].timestamp < target_time
      low = pos + 1
    else
      high = pos - 1
    end
  end
  
  -1
end

# Example: hourly log entries over 24 hours
logs = (0..23).map { |hour| LogEntry.new(hour * 3600, "Event at hour #{hour}") }

# Find log entry at 15:00 (54000 seconds)
index = search_logs_by_time(logs, 15 * 3600)
puts logs[index].message if index != -1
# => "Event at hour 15"

Database Index Simulation

Simulating a database index search where record IDs follow a uniform sequence:

class DatabaseRecord
  attr_reader :id, :data
  
  def initialize(id, data)
    @id = id
    @data = data
  end
  
  def self.interpolation_find(records, target_id)
    low = 0
    high = records.length - 1
    probes = 0
    
    while low <= high && target_id >= records[low].id && 
          target_id <= records[high].id
      probes += 1
      
      return { record: records[low], probes: probes } if 
        low == high && records[low].id == target_id
      return { record: nil, probes: probes } if low == high
      
      pos = low + ((target_id - records[low].id) * (high - low)) / 
                  (records[high].id - records[low].id)
      
      if records[pos].id == target_id
        return { record: records[pos], probes: probes }
      elsif records[pos].id < target_id
        low = pos + 1
      else
        high = pos - 1
      end
    end
    
    { record: nil, probes: probes }
  end
end

# Generate 1000 records with IDs 1000-2000
records = (1000..2000).step(1).map { |id| 
  DatabaseRecord.new(id, "Data for record #{id}") 
}

result = DatabaseRecord.interpolation_find(records, 1750)
puts "Found in #{result[:probes]} probes"
# => "Found in 1 probes" (vs ~10 probes for binary search)

Sensor Reading Analysis

Finding specific measurements in time-series sensor data with regular intervals:

class SensorReading
  attr_reader :timestamp, :temperature, :humidity
  
  def initialize(timestamp, temperature, humidity)
    @timestamp = timestamp
    @temperature = temperature
    @humidity = humidity
  end
end

def find_reading_at_time(readings, target_time)
  low = 0
  high = readings.length - 1
  closest_index = -1
  min_diff = Float::INFINITY
  
  while low <= high && target_time >= readings[low].timestamp && 
        target_time <= readings[high].timestamp
    if low == high
      closest_index = low
      break
    end
    
    pos = low + ((target_time - readings[low].timestamp) * (high - low)) / 
                (readings[high].timestamp - readings[low].timestamp)
    
    # Track closest reading
    diff = (readings[pos].timestamp - target_time).abs
    if diff < min_diff
      min_diff = diff
      closest_index = pos
    end
    
    return pos if readings[pos].timestamp == target_time
    
    if readings[pos].timestamp < target_time
      low = pos + 1
    else
      high = pos - 1
    end
  end
  
  closest_index
end

# Generate readings every 5 minutes for 24 hours
base_time = Time.now.to_i
readings = 288.times.map { |i|
  time = base_time + (i * 300)  # 300 seconds = 5 minutes
  SensorReading.new(time, 20 + rand * 10, 40 + rand * 20)
}

# Find reading closest to 12 hours from start
target = base_time + (12 * 3600)
index = find_reading_at_time(readings, target)
reading = readings[index]
puts "Temperature: #{reading.temperature.round(1)}°C"

Sequential File Block Search

Locating data blocks in a sequential file structure where block offsets are uniformly spaced:

class FileBlock
  attr_reader :offset, :size, :checksum
  
  def initialize(offset, size, checksum)
    @offset = offset
    @size = size
    @checksum = checksum
  end
end

def find_block_by_offset(blocks, target_offset)
  low = 0
  high = blocks.length - 1
  
  while low <= high && target_offset >= blocks[low].offset && 
        target_offset <= blocks[high].offset
    return low if low == high && 
                  target_offset >= blocks[low].offset && 
                  target_offset < blocks[low].offset + blocks[low].size
    
    return -1 if low == high
    
    # Calculate position based on offset distribution
    pos = low + ((target_offset - blocks[low].offset) * (high - low)) / 
                (blocks[high].offset - blocks[low].offset)
    
    block = blocks[pos]
    
    # Check if target offset falls within this block
    if target_offset >= block.offset && 
       target_offset < block.offset + block.size
      return pos
    elsif target_offset < block.offset
      high = pos - 1
    else
      low = pos + 1
    end
  end
  
  -1
end

# Create blocks at regular 4KB intervals
blocks = 256.times.map { |i|
  FileBlock.new(i * 4096, 4096, rand(2**32))
}

# Find block containing byte offset 524288 (128th block)
target_offset = 524288
block_index = find_block_by_offset(blocks, target_offset)
puts "Block #{block_index} contains offset #{target_offset}"
# => "Block 128 contains offset 524288"

Design Considerations

The choice between interpolation search and alternative algorithms depends on data characteristics, performance requirements, and implementation constraints.

Data Distribution Analysis

Interpolation search performs optimally with uniformly distributed data where values increase at a roughly constant rate. Calculate distribution uniformity by examining the variance in intervals between consecutive elements. High variance indicates non-uniform distribution where binary search may perform better.

For data with unknown distribution, profile a sample to determine algorithm selection. If the standard deviation of intervals between consecutive elements exceeds 30% of the mean interval, binary search typically provides more consistent performance.

Binary Search Comparison

Binary search maintains O(log n) performance regardless of data distribution, making it the safer choice for heterogeneous datasets. Interpolation search achieves O(log log n) for uniform distributions but degrades to O(n) for adversarial patterns. The decision matrix:

Choose interpolation search when:

  • Data distribution is known and uniform
  • Dataset is large (>10,000 elements) where O(log log n) benefits materialize
  • Data source controls distribution (sequential IDs, timestamps, evenly-spaced measurements)
  • Performance profiling confirms faster average-case behavior

Choose binary search when:

  • Distribution is unknown or non-uniform
  • Dataset is small (<1,000 elements) where algorithm differences are negligible
  • Worst-case guarantees matter more than average-case performance
  • Implementation simplicity is prioritized

Hybrid Approaches

Combining interpolation and binary search provides robustness against distribution variations. Start with interpolation search and fall back to binary search if probe positions become unstable:

def hybrid_search(arr, target)
  low = 0
  high = arr.length - 1
  interpolation_attempts = 0
  max_interpolation_attempts = 3
  
  while low <= high
    if interpolation_attempts < max_interpolation_attempts &&
       target >= arr[low] && target <= arr[high] && arr[high] != arr[low]
      # Try interpolation
      pos = low + ((target - arr[low]) * (high - low)) / 
                  (arr[high] - arr[low])
      interpolation_attempts += 1
    else
      # Fall back to binary search
      pos = (low + high) / 2
    end
    
    return pos if arr[pos] == target
    
    if arr[pos] < target
      low = pos + 1
    else
      high = pos - 1
    end
  end
  
  -1
end

Memory and Caching

Interpolation search accesses array elements in potentially non-sequential patterns, reducing cache effectiveness compared to binary search's more predictable access pattern. For memory-resident data, this difference is minimal. For disk-backed or distributed data, the cache misses can negate theoretical performance improvements.

When searching large datasets that exceed CPU cache sizes, measure actual performance rather than relying on theoretical complexity. Sequential binary search probes may outperform interpolation search's scattered access pattern due to better cache utilization.

Parallelization Potential

Both algorithms offer limited parallelization opportunities due to their sequential decision-making nature. However, searching multiple targets within the same sorted array can be parallelized effectively with both approaches. Interpolation search's superior average-case complexity per search amplifies benefits when performing parallel multi-target searches.

Performance Considerations

Interpolation search performance varies dramatically based on data distribution, dataset size, and implementation details.

Complexity Analysis

Average-case time complexity: O(log log n) for uniformly distributed data
Worst-case time complexity: O(n) for adversarial distributions
Best-case time complexity: O(1) when the first probe hits the target
Space complexity: O(1) for iterative implementation, O(log log n) for recursive

The O(log log n) average case assumes uniform distribution. For a dataset of 1,000,000 elements:

  • Interpolation search: ~3-4 probes average
  • Binary search: ~20 probes guaranteed

Distribution Impact Measurement

Measuring data distribution helps predict interpolation search performance:

def calculate_distribution_uniformity(arr)
  return 1.0 if arr.length < 2
  
  intervals = (1...arr.length).map { |i| arr[i] - arr[i-1] }
  mean = intervals.sum.to_f / intervals.length
  
  # Calculate coefficient of variation
  variance = intervals.map { |x| (x - mean) ** 2 }.sum / intervals.length
  std_dev = Math.sqrt(variance)
  
  coefficient_of_variation = std_dev / mean
  
  # Return uniformity score (1.0 = perfect, 0.0 = highly varied)
  [0.0, 1.0 - coefficient_of_variation].max
end

# Uniform distribution
uniform = (1..100).to_a
puts calculate_distribution_uniformity(uniform)
# => ~1.0

# Non-uniform distribution
non_uniform = [1, 2, 3, 10, 11, 12, 50, 51, 52, 100]
puts calculate_distribution_uniformity(non_uniform)
# => ~0.4

Probe Count Benchmarking

Comparing probe counts between algorithms reveals performance differences:

def benchmark_search_algorithms(arr, target)
  interpolation_probes = 0
  binary_probes = 0
  
  # Interpolation search with probe counting
  low, high = 0, arr.length - 1
  while low <= high && target >= arr[low] && target <= arr[high]
    interpolation_probes += 1
    break if low == high
    
    pos = low + ((target - arr[low]) * (high - low)) / (arr[high] - arr[low])
    break if arr[pos] == target
    
    if arr[pos] < target
      low = pos + 1
    else
      high = pos - 1
    end
  end
  
  # Binary search with probe counting
  low, high = 0, arr.length - 1
  while low <= high
    binary_probes += 1
    mid = (low + high) / 2
    break if arr[mid] == target
    
    if arr[mid] < target
      low = mid + 1
    else
      high = mid - 1
    end
  end
  
  { interpolation: interpolation_probes, binary: binary_probes }
end

# Test with uniform distribution
uniform_data = (1..100000).step(1).to_a
result = benchmark_search_algorithms(uniform_data, 75000)
puts "Interpolation: #{result[:interpolation]} probes"
puts "Binary: #{result[:binary]} probes"
# => Interpolation: 2-3 probes, Binary: 16-17 probes

Integer Division Precision

Integer division in the interpolation formula can introduce position errors. For large ranges, converting to float improves accuracy:

# Less accurate with integer division
pos = low + ((target - arr[low]) * (high - low)) / (arr[high] - arr[low])

# More accurate with float conversion
pos = low + ((target - arr[low]) * (high - low).to_f / (arr[high] - arr[low]))
pos = pos.round

Cache Performance

Interpolation search exhibits less predictable memory access patterns than binary search. Profiling cache hit rates reveals the impact:

require 'benchmark'

def compare_cache_efficiency(size)
  arr = (1..size).to_a
  targets = Array.new(1000) { rand(1..size) }
  
  interpolation_time = Benchmark.realtime do
    targets.each { |t| interpolation_search(arr, t) }
  end
  
  binary_time = Benchmark.realtime do
    targets.each { |t| arr.bsearch { |x| x >= t } }
  end
  
  puts "Size: #{size}"
  puts "Interpolation: #{interpolation_time.round(4)}s"
  puts "Binary: #{binary_time.round(4)}s"
  puts "Ratio: #{(interpolation_time / binary_time).round(2)}x"
end

compare_cache_efficiency(100000)
# Results vary by hardware and cache size

Common Pitfalls

Division by Zero

When all remaining elements have the same value, the interpolation formula produces division by zero:

# Problem: arr[high] - arr[low] = 0
arr = [5, 5, 5, 5, 5]
target = 5

# Fix: Check for same values at bounds
if arr[high] == arr[low]
  return arr[low] == target ? low : -1
end

pos = low + ((target - arr[low]) * (high - low)) / (arr[high] - arr[low])

Integer Overflow

Large array ranges combined with large values cause integer overflow in position calculation:

# Problem: multiplication can overflow
low = 0
high = 100000
target = 90000
arr[low] = 1
arr[high] = 100000

# Problematic calculation
pos = low + ((target - arr[low]) * (high - low)) / (arr[high] - arr[low])

# Fix: Use float arithmetic or BigDecimal for very large values
pos = low + ((target - arr[low]).to_f * (high - low) / (arr[high] - arr[low]))
pos = pos.round.clamp(low, high)

Negative Index Calculation

When the target is less than the first element or greater than the last, the algorithm may calculate negative or out-of-bounds positions:

# Problem: target outside array bounds
arr = [10, 20, 30, 40, 50]
target = 5  # Less than arr[0]

# Position calculation yields negative result
pos = 0 + ((5 - 10) * (4 - 0)) / (50 - 10)  # => -0.5

# Fix: Validate bounds before interpolation
while low <= high && target >= arr[low] && target <= arr[high]
  # Safe to interpolate here
end

Non-Numeric Data Misuse

Applying interpolation search to non-numeric or improperly ordered data produces incorrect results:

# Problem: string comparison doesn't work with arithmetic
words = ["apple", "banana", "cherry", "date", "fig"]
target = "cherry"

# This fails - can't subtract strings
pos = low + ((target - words[low]) * (high - low)) / 
            (words[high] - words[low])

# Fix: Convert to numeric representation
def string_to_value(str)
  str.bytes.take(8).each_with_index.sum { |byte, i| byte * (256 ** i) }
end

Float Precision Errors

Comparing floating-point values for exact equality fails due to precision limitations:

# Problem: exact equality fails
arr = [1.1, 2.2, 3.3, 4.4, 5.5]
target = 3.3

# May not find exact match due to float representation
return pos if arr[pos] == target  # Unreliable

# Fix: Use epsilon tolerance
epsilon = 1e-10
return pos if (arr[pos] - target).abs < epsilon

Duplicate Value Handling

Interpolation search finds one occurrence of a value but doesn't guarantee finding the first or last occurrence:

# Problem: may find any occurrence
arr = [1, 2, 3, 3, 3, 4, 5]
target = 3

# Could return index 2, 3, or 4
index = interpolation_search(arr, 3)

# Fix: For leftmost, scan backward
while index > 0 && arr[index - 1] == target
  index -= 1
end

Unsorted Array Assumptions

Applying interpolation search to unsorted data produces unpredictable results without error indication:

# Problem: array not sorted
arr = [30, 10, 50, 20, 40]
target = 20

# Produces incorrect result silently
index = interpolation_search(arr, target)  # Wrong answer

# Fix: Validate sort order in development
def validate_sorted(arr)
  (1...arr.length).all? { |i| arr[i] >= arr[i-1] }
end

raise "Array must be sorted" unless validate_sorted(arr)

Reference

Algorithm Complexity

Case Time Complexity Space Complexity Condition
Best O(1) O(1) First probe hits target
Average O(log log n) O(1) Uniform distribution
Worst O(n) O(1) Non-uniform or adversarial
Recursive Same as above O(log log n) Call stack depth

Core Formula

Component Expression Description
Position Estimate pos = low + ((target - arr[low]) * (high - low)) / (arr[high] - arr[low]) Interpolated probe position
Range Check target >= arr[low] && target <= arr[high] Ensures target within bounds
Search Space Update (Lower) low = pos + 1 When arr[pos] < target
Search Space Update (Upper) high = pos - 1 When arr[pos] > target

Implementation Checklist

Step Action Purpose
1 Verify sorted order Prevent incorrect results
2 Initialize low = 0, high = n-1 Set search boundaries
3 Check bounds: low <= high Validate search space exists
4 Check range: target in [arr[low], arr[high]] Enable interpolation
5 Handle same-value bounds Avoid division by zero
6 Calculate probe position Estimate target location
7 Clamp position to valid range Prevent array overflow
8 Compare arr[pos] with target Determine direction
9 Update low or high Narrow search space
10 Return index or -1 Indicate success or failure

Performance Comparison

Algorithm Average Case Worst Case Best For
Interpolation Search O(log log n) O(n) Uniform distribution
Binary Search O(log n) O(log n) Unknown distribution
Linear Search O(n) O(n) Unsorted data
Jump Search O(√n) O(√n) Sorted array, forward-only

Common Data Patterns

Pattern Type Uniformity Recommended Algorithm
Sequential IDs High Interpolation
Timestamps High Interpolation
Random integers Medium Binary
Clustered values Low Binary
Exponential growth Very Low Binary
Fibonacci sequence Low Binary

Ruby Method Signatures

Method Parameters Returns Description
interpolation_search arr, target Integer Returns index or -1
interpolation_search_float arr, target, epsilon Integer Float-tolerant search
interpolation_search_by_key arr, target_key Integer Search by object attribute
interpolation_search_range arr, target Array All indices with target value
interpolation_search_recursive arr, target, low, high Integer Recursive implementation

Error Conditions

Condition Detection Handling
Division by zero arr[high] == arr[low] Return early if match, else -1
Integer overflow Large value * large range Use float arithmetic
Out of bounds pos < low or pos > high Clamp to valid range
Unsorted array arr[i] > arr[i+1] exists Validate before search
Empty array arr.length == 0 Return -1 immediately
Nil target target.nil? Define comparison semantics