Cracking The Coding Interview Together – Problem 5.8: Draw Line
Imagine you are writing a rendering engine for an ultra-low-power e-ink display or an embedded medical device. Memory is incredibly scarce, and every CPU cycle directly impacts battery life. In these environments, we do not have the luxury of modern high-level graphics libraries. Instead, we are dealing directly with a raw framebuffer: a single, contiguous array of bytes representing a monochrome screen.
If we need to draw a simple horizontal line, our first instinct might be to loop through every single pixel from start to finish, setting them one by one. But as senior engineers, we know that pixel-by-pixel manipulation is a performance bottleneck. It forces the CPU to perform redundant read-modify-write operations on the same byte of memory over and over again.
Our goal today is to design and implement an incredibly efficient, production-ready algorithm to draw a horizontal line. We will bypass the slow pixel-by-pixel approach and instead operate at the byte level, leveraging bitwise operations to write entire blocks of memory in a single instruction. Let's roll up our sleeves and master this low-level challenge together.
Problem Statement
The problem asks us to implement a function that draws a horizontal line on a monochrome screen. The screen is represented as a single-dimensional array of bytes (byte[] screen). Each byte contains 8 consecutive pixels, where a 1 represents a dark pixel (drawn) and a 0 represents a light pixel (empty).
The screen has a width W (in pixels), where W is guaranteed to be divisible by 8. The height of the screen can be inferred from the length of the array and the width. We need to implement the following method:
public static void drawLine(byte[] screen, int width, int x1, int x2, int y)This method must draw a horizontal line from coordinate (x1, y) to (x2, y) (inclusive) by modifying the screen array in-place.
Analyze the Problem Together
To solve this efficiently, we must first understand how the 2D screen coordinates map to our 1D byte array. Since the screen width is W pixels, and there are 8 pixels per byte, each row of the screen occupies exactly W / 8 bytes. Let's call this value the byteWidth.
For any given coordinate (x, y), we can locate its corresponding byte and bit position using the following formulas:
- Byte Index:
int byteIndex = (y * byteWidth) + (x / 8); - Bit Offset:
int bitOffset = x % 8;
The "Aha!" moment of this problem lies in recognizing that a horizontal line spans across three distinct regions of bytes:
- The Start Byte: A partial byte where the line begins at
x1. We only want to set the bits fromx1 % 8to the end of this byte. - The Middle Bytes: Zero or more fully aligned bytes that are completely covered by the line. We can set these entire bytes to
0xFF(all 1s) in a single write operation! - The End Byte: A partial byte where the line ends at
x2. We only want to set the bits from the start of this byte up tox2 % 8.
Let's compare the naive approach against our optimized byte-aligned approach to see why this distinction is so critical:
| Metric | Naive Pixel-by-Pixel Approach | Optimized Byte-Aligned Approach |
|---|---|---|
| Time Complexity | O(N) where N is the number of pixels (x2 - x1) | O(B) where B is the number of bytes (at most W / 8) |
| Memory Operations | Frequent read-modify-write cycles for every single pixel | Single-write operations for full bytes; at most two masked writes |
| Edge Case Handling | Implicitly handled, but highly inefficient | Requires careful handling when x1 and x2 are in the same byte |
By shifting our perspective from individual pixels to byte boundaries, we can dramatically reduce the number of memory writes, which is the most expensive operation in low-level rendering.
Questions for the Interviewer
Before we write a single line of code, a senior engineer always clarifies the boundaries and assumptions of the system. Here are the key questions we should ask our interviewer:
- Bit Ordering (Endianness): "Is pixel 0 represented by the Most Significant Bit (MSB,
0x80) or the Least Significant Bit (LSB,0x01) of the byte?"
Assumption: We will assume MSB-to-LSB ordering. This means the leftmost pixel in a byte corresponds to the highest bit (index 0 is10000000). - Coordinate Validation: "Can we assume that
x1,x2, andyare always within the valid bounds of the screen, and thatx1 <= x2?"
Assumption: Yes, but we should still write robust code that handles edge cases gracefully. - Single-Byte Lines: "How should we handle the case where
x1andx2fall within the exact same byte?"
Assumption: This is a critical edge case. We must handle it by applying a single combined mask to that specific byte, rather than treating them as separate start and end bytes.
Think About the Solution
Let's design the high-level architecture of our solution. We need to calculate the exact byte indices for our start and end points, and then handle the rendering in three logical phases:
- Phase 1: Calculate Boundaries. Find the byte indices for
x1andx2. Determine the bit offsets for both points. - Phase 2: Handle the Single-Byte Edge Case. If
x1 / 8 == x2 / 8, the entire line fits inside a single byte. We generate a mask that only sets the bits betweenx1 % 8andx2 % 8, apply it, and exit early. - Phase 3: Handle Multi-Byte Lines. If the line spans multiple bytes:
- Create a start mask to set bits from
x1 % 8to 7, and apply it to the start byte. - Fill all intermediate bytes between the start and end bytes with
0xFF. - Create an end mask to set bits from 0 to
x2 % 8, and apply it to the end byte.
- Create a start mask to set bits from
Let's visualize how we construct these masks using bitwise shifts. If we want to set bits from index 3 to 7 in a byte (where index 0 is MSB):
0xFF >>> 3 => 11111111 >>> 3 => 00011111If we want to set bits from index 0 to 5:
~(0xFF >>> (5 + 1)) => ~(0xFF >>> 6) => ~(00000011) => 11111100This elegant bitwise math allows us to generate arbitrary masks dynamically without loops!
Write Code and Explain
Here is the complete, production-ready Java implementation. It includes comprehensive comments explaining each bitwise operation.
public class ScreenRenderer {
/**
* Draws a horizontal line on a monochrome screen represented as a byte array.
*
* @param screen The byte array representing the screen.
* @param width The width of the screen in pixels (divisible by 8).
* @param x1 The starting x-coordinate of the line.
* @param x2 The ending x-coordinate of the line.
* @param y The y-coordinate of the line.
*/
public static void drawLine(byte[] screen, int width, int x1, int x2, int y) {
// Defensive checks to prevent out-of-bounds exceptions
if (screen == null || width <= 0 || x1 < 0 || x2 < 0 || y < 0) {
return;
}
if (x1 > x2) {
// Swap x1 and x2 if they are passed in reverse order
int temp = x1;
x1 = x2;
x2 = temp;
}
int byteWidth = width / 8;
int rowOffset = y * byteWidth;
// Calculate byte indices and bit offsets
int startByteIdx = x1 / 8;
int startBitOffset = x1 % 8;
int endByteIdx = x2 / 8;
int endBitOffset = x2 % 8;
// Edge Case: The entire line fits within a single byte
if (startByteIdx == endByteIdx) {
// Create a mask that sets bits from startBitOffset to endBitOffset
byte startMask = (byte) (0xFF >>> startBitOffset);
byte endMask = (byte) ~(0xFF >>> (endBitOffset + 1));
byte combinedMask = (byte) (startMask & endMask);
screen[rowOffset + startByteIdx] |= combinedMask;
return;
}
// Phase 3: Multi-byte line drawing
// 1. Handle the partial start byte
if (startBitOffset != 0) {
// Mask sets bits from startBitOffset to 7
byte startMask = (byte) (0xFF >>> startBitOffset);
screen[rowOffset + startByteIdx] |= startMask;
// The first full byte to write starts at the next index
startByteIdx++;
}
// 2. Handle the partial end byte
if (endBitOffset != 7) {
// Mask sets bits from 0 to endBitOffset
byte endMask = (byte) ~(0xFF >>> (endBitOffset + 1));
screen[rowOffset + endByteIdx] |= endMask;
// The last full byte to write ends at the previous index
endByteIdx--;
}
// 3. Fill all intermediate full bytes with 0xFF (all 1s)
for (int b = startByteIdx; b <= endByteIdx; b++) {
screen[rowOffset + b] = (byte) 0xFF;
}
}
}Step-by-Step Walkthrough
Let's trace this code with a concrete example. Suppose we have a screen of width 32 (which means 4 bytes per row). We want to draw a line from (3, 2) to (14, 2).
- Initialization:
byteWidth = 4. The row offset fory = 2is2 * 4 = 8. - Indices & Offsets:
startByteIdx = 3 / 8 = 0,startBitOffset = 3 % 8 = 3.endByteIdx = 14 / 8 = 1,endBitOffset = 14 % 8 = 6.
- Single Byte Check: Since
startByteIdx (0) != endByteIdx (1), we bypass the single-byte block. - Start Byte Masking: Since
startBitOffset (3) != 0, we createstartMask = 0xFF >>> 3, which is binary00011111. We apply this toscreen[8 + 0]using bitwise OR (|=). We then incrementstartByteIdxto1. - End Byte Masking: Since
endBitOffset (6) != 7, we createendMask = ~(0xFF >>> 7), which is binary11111110. We apply this toscreen[8 + 1]. We then decrementendByteIdxto0. - Middle Bytes Fill: The loop runs from
b = startByteIdx (1)tob = endByteIdx (0). Since1 <= 0is false, the loop does not execute. This is correct because our line only spanned two bytes, both of which were partial!
Time & Space Complexity
Let's analyze the efficiency of our optimized solution:
- Time Complexity:
O(W / 8)in the worst case, whereWis the width of the screen. More accurately, the time complexity isO(B), whereBis the number of full bytes spanned by the line. Generating masks and writing partial bytes takesO(1)constant time. Filling the middle bytes takesO(B). This is a massive improvement over the naiveO(N)pixel-by-pixel approach, especially for long lines. - Space Complexity:
O(1)auxiliary space. We modify thescreenarray in-place and only use a few local variables to store indices and masks.
Why This Problem Matters (The Senior Perspective)
This problem is not just an academic exercise; it is a fundamental pattern in systems programming and hardware optimization. Here is why this matters in the real world:
Memory Alignment and Cache Lines
Modern CPUs do not read and write single bits or even single bytes from RAM. They operate on cache lines (typically 64 bytes). When we write code that modifies memory byte-by-byte or word-by-word rather than bit-by-bit, we align our software operations with the physical hardware architecture. This minimizes cache thrashing and maximizes memory bus throughput.
Concurrency and Thread Safety
In a real-world graphics pipeline, multiple threads might be rendering different parts of the screen simultaneously. If two threads attempt to write to different pixels that reside within the same byte, they will cause a race condition (specifically, a write-write conflict). To prevent this, we must either synchronize access to the byte array or partition the screen so that threads only operate on distinct, byte-aligned boundaries.
Final Thoughts
Low-level bit manipulation can feel intimidating at first, but once you break the problem down into byte boundaries and masks, the solution becomes incredibly elegant and highly performant. We moved from a slow, naive pixel-by-pixel loop to a blazing-fast, hardware-friendly byte-aligned writer.
Next time you are faced with a grid or array problem, ask yourself: "Can I group these operations to process them in chunks rather than one by one?" Your CPU (and your users' battery life) will thank you.
Thanks for reading, and happy coding!
Member discussion