Format words into lines of exactly maxWidth characters. Greedily pack as many words as fit per line, then spread the leftover spaces as evenly as possible — extra spaces favoring the leftmost gaps. The last line (and any single-word line) is left-justified and padded on the right. The algorithm is easy; the tricky part is the careful spacing simulation.
Given a list of words and a width, return each line padded to exactly maxWidth characters. Take words = ["This","is","an","example","of","text","justification."] with maxWidth = 16. The greedy packing gives lines This is an, example of text, and justification.. After spacing:
"This is an"
"example of text"
"justification. "Line 1 has 3 words: 10 letters, 6 spaces over 2 gaps → 3 + 3. Line 2 has 3 words: 12 letters, 4 spaces over 2 gaps → 2 + 1 (the extra goes to the leftgap). Line 3 is the last line, so it's left-justified and padded on the right.
The leftover space count is maxWidth − (sum of word lengths). Split it over gaps = numWords − 1: every gap gets ⌊total / gaps⌋, and the first total mod gaps gaps get one extra. That single floor/remainder pair is the whole trick.
i, keep adding the next word while lineLen + 1 + nextWord.length ≤ maxWidth (the +1 is the mandatory single space).numWords words make numWords − 1 gaps and maxWidth − totalLetters spaces to place.base = ⌊spaces / gaps⌋; the leftmost spaces mod gaps gaps get +1.i to the next word, repeat until words run out.k ≤ extra, not k > gaps − extra.⌊s / g⌋ and dropping the remainder leaves lines too short; ignoring the last-line rule mis-pads the tail.This is mostly careful simulation, not a clever data structure. The greedy choice (pack as many words as fit) is provably optimal for this fixed format; the only real bugs are off-by-one space counting and forgetting the special-cased lines.
base + (k ≤ extra)) also shows up in round-robin scheduling, fair-share allocation, and dealing cards evenly.16 chars. Padding spaces are shown as · so you can count them.1function fullJustify(words: string[], maxWidth: number): string[] {2▶ const lines: string[] = [];3▶ let i = 0;45 while (i < words.length) {6 // 1 ── Greedily pack words into the current line.7 // lineLen tracks chars assuming exactly ONE space between words.8 let j = i;9 let lineLen = words[i].length;10 while (j + 1 < words.length &&11 lineLen + 1 + words[j + 1].length <= maxWidth) {12 lineLen += 1 + words[j + 1].length;13 j++;14 }1516 const numWords = j - i + 1;17 const lastWord = j === words.length - 1;1819 // 2 ── Build the line.20 if (numWords === 1 || lastWord) {21 // Left-justify: single space between words, pad the right.22 let line = words.slice(i, j + 1).join(' ');23 line += ' '.repeat(maxWidth - line.length);24 lines.push(line);25 } else {26 // Full-justify: spread spaces across (numWords - 1) gaps.27 const gaps = numWords - 1;28 const totalSpaces = maxWidth - (lineLen - gaps); // chars w/o any spaces29 const base = Math.floor(totalSpaces / gaps); // each gap gets this30 const extra = totalSpaces % gaps; // leftmost gaps get +131 let line = words[i];32 for (let k = 1; k < numWords; k++) {33 const spaces = base + (k <= extra ? 1 : 0);34 line += ' '.repeat(spaces) + words[i + k];35 }36 lines.push(line);37 }3839 i = j + 1; // advance to the next line's first word40 }4142 return lines;43}
function fullJustify(words: string[], maxWidth: number): string[] {
const lines: string[] = [];
let i = 0;
while (i < words.length) {
// 1 ── Greedily pack words into the current line.
// lineLen tracks chars assuming exactly ONE space between words.
let j = i;
let lineLen = words[i].length;
while (j + 1 < words.length &&
lineLen + 1 + words[j + 1].length <= maxWidth) {
lineLen += 1 + words[j + 1].length;
j++;
}
const numWords = j - i + 1;
const lastWord = j === words.length - 1;
// 2 ── Build the line.
if (numWords === 1 || lastWord) {
// Left-justify: single space between words, pad the right.
let line = words.slice(i, j + 1).join(' ');
line += ' '.repeat(maxWidth - line.length);
lines.push(line);
} else {
// Full-justify: spread spaces across (numWords - 1) gaps.
const gaps = numWords - 1;
const totalSpaces = maxWidth - (lineLen - gaps); // chars w/o any spaces
const base = Math.floor(totalSpaces / gaps); // each gap gets this
const extra = totalSpaces % gaps; // leftmost gaps get +1
let line = words[i];
for (let k = 1; k < numWords; k++) {
const spaces = base + (k <= extra ? 1 : 0);
line += ' '.repeat(spaces) + words[i + k];
}
lines.push(line);
}
i = j + 1; // advance to the next line's first word
}
return lines;
}i. Keep absorbing the next word as long as it fits with exactly one separating space (+1). The running lineLen assumes single spaces, which is exactly enough to test the fit.numWords is how many we grabbed; lastWord is true when we consumed the final word. These two flags decide left-justify vs full-justify.maxWidth.base, and the remainder extra tells how many of the leftmost gaps get one bonus space (k <= extra).i = j + 1 so the next iteration starts at the first word of the next line. Loop until every word is placed.O(total characters) — linear. Output uses O(total characters) space; aside from the result, only a handful of integer counters.function fullJustify(words: string[], maxWidth: number): string[] {
const lines: string[] = [];
let i = 0;
while (i < words.length) {
// 1 ── Greedily pack words into the current line.
// lineLen tracks chars assuming exactly ONE space between words.
let j = i;
let lineLen = words[i].length;
while (j + 1 < words.length &&
lineLen + 1 + words[j + 1].length <= maxWidth) {
lineLen += 1 + words[j + 1].length;
j++;
}
const numWords = j - i + 1;
const lastWord = j === words.length - 1;
// 2 ── Build the line.
if (numWords === 1 || lastWord) {
// Left-justify: single space between words, pad the right.
let line = words.slice(i, j + 1).join(' ');
line += ' '.repeat(maxWidth - line.length);
lines.push(line);
} else {
// Full-justify: spread spaces across (numWords - 1) gaps.
const gaps = numWords - 1;
const totalSpaces = maxWidth - (lineLen - gaps); // chars w/o any spaces
const base = Math.floor(totalSpaces / gaps); // each gap gets this
const extra = totalSpaces % gaps; // leftmost gaps get +1
let line = words[i];
for (let k = 1; k < numWords; k++) {
const spaces = base + (k <= extra ? 1 : 0);
line += ' '.repeat(spaces) + words[i + k];
}
lines.push(line);
}
i = j + 1; // advance to the next line's first word
}
return lines;
}O(total characters) — linear. Output uses O(total characters) space; aside from the result, only a handful of integer counters.Same greedy packing, but instead of a divmodformula it lays down a base space in every gap, then walks the gaps left-to-right handing out one leftover space at a time — making "extras go to the left gaps" literally visible in the loop.
function fullJustify(words: string[], maxWidth: number): string[] {
const lines: string[] = [];
let i = 0;
while (i < words.length) {
// 1 ── Greedily pack words into the current line (single-space fit test).
let j = i;
let lineLen = words[i].length;
while (j + 1 < words.length &&
lineLen + 1 + words[j + 1].length <= maxWidth) {
lineLen += 1 + words[j + 1].length;
j++;
}
const lineWords = words.slice(i, j + 1);
const numWords = lineWords.length;
const gaps = numWords - 1;
const lastLine = j === words.length - 1;
if (gaps === 0 || lastLine) {
// 2a ── Left-justify: one space between words, pad the right.
let line = lineWords.join(' ');
line += ' '.repeat(maxWidth - line.length);
lines.push(line);
} else {
// 2b ── Full-justify via round robin.
// Total spaces to place across the gaps.
const letters = lineLen - gaps; // chars w/o any spaces
const totalSpaces = maxWidth - letters;
const base = Math.floor(totalSpaces / gaps);
// Give every gap the base count, then hand out the leftovers
// one at a time, left to right, until none remain.
const gapSizes: number[] = new Array(gaps).fill(base);
let leftover = totalSpaces - base * gaps;
let g = 0;
while (leftover > 0) {
gapSizes[g]++;
g++;
leftover--;
}
// 3 ── Stitch words and gaps together.
let line = lineWords[0];
for (let k = 0; k < gaps; k++) {
line += ' '.repeat(gapSizes[k]) + lineWords[k + 1];
}
lines.push(line);
}
i = j + 1; // advance to the next line's first word
}
return lines;
}leftover < gaps always, the round-robin pass touches each leftmost gap exactly once — identical output to the base + (k ≤ extra) formula, just spelled out as an explicit loop. The only extra cost is an O(gaps) array per line, bounded by maxWidth, so the asymptotics are unchanged.| "lines of exactly maxWidth chars" | greedy fixed-width packing |
| "distribute spaces evenly" | floor + remainder (base + extra) |
| "extra spaces to the left" | k <= extra gets +1 |
| "last line / single word" | left-justify + right pad |
let i = 0;
while (i < words.length) {
let j = i, lineLen = words[i].length;
while (j + 1 < n && lineLen + 1 + words[j+1].length <= maxWidth)
lineLen += 1 + words[++j].length;
const last = j === n - 1, gaps = j - i;
if (gaps === 0 || last) {
let line = words.slice(i, j+1).join(' ');
lines.push(line + ' '.repeat(maxWidth - line.length));
} else {
const slots = maxWidth - (lineLen - gaps);
const base = Math.floor(slots / gaps), extra = slots % gaps;
let line = words[i];
for (let k = 1; k <= gaps; k++)
line += ' '.repeat(base + (k <= extra ? 1 : 0)) + words[i+k];
lines.push(line);
}
i = j + 1;
}lineLen + 1 + next.length <= maxWidth (the +1is one separating space). Stop at the first word that doesn't fit.spaces = 5 must be spread over gaps = 2, the gaps get: