Why this matters
Many shapes look right only when both sides match. A tree, the letter A, a logo or a simple house all look wrong when one side is longer than the other. Pattern questions often ask for a symmetric shape, and this lesson shows how to get the symmetry without writing two sets of rules, one for each side.
The same idea appears outside pictures. A palindrome is a word or number that reads the same forwards and backwards, such as level or 12321. Checking whether a word is a palindrome is a common first exercise in programming interviews. It uses the same thinking as this lesson: a position on the left must match a position on the right.
Words used in this lesson
- Mirror. A copy flipped across a line, like the reflection in a bathroom mirror.
- Symmetry. Two sides that match, with one side being the mirror of the other.
- Centre line. The imaginary vertical line down the middle of a shape.
- Index. The position number of an item. C++ counts from 0, so the first row is row 0. In a shape with
nrows, the last row is rown - 1. - Standard library. The ready-made tools that come with C++. This lesson uses two of them.
std::coutprints text to the screen.std::stringis the standard text type. std::string(count, ch). Builds text by repeating one character.std::string(3, '*')makes***. The first value is how many copies to make, and the second is the character.
This lesson counts rows and columns from 0, as C++ does. Row 0 is the first row. When you see n - 1, read it as "the last index".
Start with the picture
Here is the shape we will build, for n = 4:
* *
** **
*** ***
********Look at the left side first. Row 0 has one star, row 1 has two, row 2 has three, and row 3 has four. That is a staircase, where each row adds one star. The right side is the same staircase, reflected. Between the two halves there is a gap of spaces.
Now look at the gap. Row 0 has 6 spaces, row 1 has 4, row 2 has 2, and row 3 has 0. The gap shrinks by 2 on each row. Each row, both halves move one step toward the middle, so the gap loses one space on the left and one on the right.
The count n - 1 - i tells you how many steps remain before the last row. For n = 4 and row 0, that is 4 - 1 - 0, which is 3. The gap is twice that, so 6 spaces. For row 3 it is 4 - 1 - 3, which is 0, so there is no gap at all. The gap for row i is:
gap = 2 * (n - 1 - i)Why n - 1 and not n? The last row, row n - 1, must have no gap. Using n - 1 at the top makes the last row come out to 0.
Apply the four questions from the row and column method. The number of rows is n. Row i has a left half of i + 1 stars, a gap of 2 * (n - 1 - i) spaces, and a right half that matches the left half. Every row ends with a newline.
Step 1: print the left half
Start with the half only. The loop below prints the staircase on the left.
#include <iostream>
#include <string>
int main() {
const int n = 4;
for (int i = 0; i < n; ++i) {
std::cout << std::string(i + 1, '*') << '\n';
}
}Compiled with g++ -std=c++20 -Wall, the program prints:
*
**
***
****The line #include <iostream> opens the toolbox that contains std::cout. The line #include <string> opens the text toolbox. const int n = 4; sets the number of rows. The word const means the value cannot change after this line. The loop runs n times. In row i, the call std::string(i + 1, '*') builds i + 1 stars. The '\n' character moves the cursor to the next line.
If you need a refresher on loops, the row and column method lesson explains the outer and inner loop shape. This lesson uses one loop per row and builds each row as text, which is shorter.
Step 2: add the gap and mirror the half
Now add the gap and the right half. The right half is the same text as the left half, so you build it once and print it twice.
#include <iostream>
#include <string>
int main() {
const int n = 4;
for (int i = 0; i < n; ++i) {
std::string half(i + 1, '*');
std::string gap(2 * (n - 1 - i), ' ');
std::cout << half << gap << half << '\n';
}
}The variable half holds the left staircase for one row. The variable gap holds the spaces in the middle. Printing half a second time puts a mirror copy on the right.
Compiled and run, the program prints:
* *
** **
*** ***
********This matches the picture. Notice what the code does not contain. There is no rule for the right side. The right side is a copy of the left side, so one rule covers both halves. That is the point of mirror symmetry: write the half, then copy it. Copying works here because a star looks the same when read in reverse. Numbers do not have that property, which is why Example 2 needs a second loop.
Try this to see why the gap formula matters. If you change 2 * (n - 1 - i) to 2 * i, the gap grows as you go down. The Common mistakes section below shows the output.
Example 2: a mirrored number pyramid
The same idea works with numbers. Take this row for n = 4:
1
121
12321
1234321Each row is a palindrome. Row 2 reads 12321 in both directions. The left half counts up from 1 to i + 1. The right half counts back down from i to 1. The peak number is printed once, by the first loop.
The leading spaces are the same n - 1 - i as before. Here they push each row toward the centre, so the rows line up as a pyramid.
#include <iostream>
#include <string>
int main() {
const int n = 4;
for (int i = 0; i < n; ++i) {
std::cout << std::string(n - 1 - i, ' ');
for (int k = 1; k <= i + 1; ++k) {
std::cout << k;
}
for (int k = i; k >= 1; --k) {
std::cout << k;
}
std::cout << '\n';
}
}Compiled and run, the output is:
1
121
12321
1234321Look at the second loop. It starts at i, not at i + 1. If it started at i + 1, the peak number would be printed twice, and row 2 would read 123321. The first loop already printed the peak, so the mirror must begin one step lower.
There is a limit here. Each number is printed with std::cout << k, which prints one or more characters. For numbers from 1 to 9 the shape is correct. For k of 10 or more, a number takes two characters, and the columns no longer line up. Printing numbers with a fixed width is covered in the aligned tables lesson.
Example 3: test each column with a mirror index
The first two examples build text for each half. There is a second way to see the same shape. Think of the row as a list of columns, numbered from 0. For each column, ask one question: should this column be a star or a space?
For n = 4, the shape is 2 * n = 8 columns wide. Column numbers run from 0 to 7. A column is a star in one of two cases:
- It is on the left half:
j <= i. - It is on the right half:
j >= width - 1 - i.
The right half starts at column width - 1 - i. For row 1 of the n = 4 shape, that is 8 - 1 - 1, which is 6. Columns 6 and 7 are stars, and columns 2 to 5 are spaces. This is the same shape as before.
The mirror index is the key idea. A column j on the left matches column width - 1 - j on the right. The code below uses that rule through the two checks in the loop. It also wraps the work in a function, so you can print any size you ask for.
#include <iostream>
void print_mirror(int n) {
if (n < 1) {
std::cout << "n must be at least 1\n";
return;
}
const int width = 2 * n;
for (int i = 0; i < n; ++i) {
for (int j = 0; j < width; ++j) {
bool on_left = j <= i;
bool on_right = j >= width - 1 - i;
std::cout << (on_left || on_right ? '*' : ' ');
}
std::cout << '\n';
}
}
int main() {
std::cout << "n = 1\n";
print_mirror(1);
std::cout << "n = 3\n";
print_mirror(3);
std::cout << "n = 0\n";
print_mirror(0);
}Compiled and run, the output is:
n = 1
**
n = 3
* *
** **
******
n = 0
n must be at least 1The function print_mirror takes one value, n, and draws the shape for that value. The if at the top is a guard. A guard is a check that stops a function from doing work it cannot do. Here, n = 0 has no rows to draw, so the function prints a message and returns.
The case n = 1 is worth a look. The one row is **, with two stars touching and no gap. The gap formula gives 2 * (1 - 1 - 0), which is 0. This shows that the shape is always exactly 2 * n columns wide, even at the smallest size.
Both versions produce the same shape. The first version is shorter. The column version is useful when you need to decide for each position separately, for example when a position may hold a different character for other reasons.
Check the shape before you trust it
Three quick checks catch most mistakes in a mirror shape. Run them on paper for a small n, such as 2 or 3, before you trust the program for a large one.
- Every row should be exactly
2 * ncharacters wide, counting spaces. Forn = 4, that is 8. Count the characters in the row, not only the stars. - The number of stars on each row should be
2 * (i + 1)for the star version. Row 0 has 2 stars, row 1 has 4, and row 3 has 8. An odd count on any row means one half is missing a star or has an extra one. - For the number version, each row should read the same forwards and backwards. The row
12321passes this test. The row1232fails it, because its last digit is 2 and its first digit is 1.
Common mistakes
Mistake 1: using the row index for the gap
Students often write 2 * i for the gap, because it looks like the opposite of the staircase. The output shows the shape going the wrong way.
std::string gap(2 * i, ' ');Compiled and run with this one change, the output is:
**
** **
*** ***
**** ****The gap now grows from 0 to 6 spaces. The shape widens as it goes down, which is the opposite of a mirror staircase. The fix is 2 * (n - 1 - i). Check the gap for the first row and the last row. Row 0 should have the widest gap, and the last row should have none.
Mistake 2: running the loop one row too far
If the loop runs while i <= n, it runs one extra time with i equal to n. The gap formula then gives a negative number. Here is the output from that program:
terminate called after throwing an instance of 'std::length_error'
what(): basic_string::_M_createWhen I ran this program, the exit code was 134, which means the program stopped with an abort. When the output goes to a file or a pipe, the rows printed before the crash are lost. std::cout holds text in a buffer, which is a small holding area in memory, and an abort does not flush it. In a terminal, the rows appear before the error. The error message text can differ between compilers. The cause is the same everywhere: a count of 2 * (n - 1 - i) with i = n is -2. The std::string constructor expects a count that cannot be negative. Converting -2 to the unsigned count type makes it a very large number, and the standard library refuses to build it.
The fix is i < n, which runs exactly n times.
Mistake 3: comparing an int with a size
This warning appears when you loop over a string with an int counter:
#include <iostream>
#include <string>
int main() {
std::string row = "*";
for (int i = 0; i < row.size(); ++i) {
std::cout << row[i] << '\n';
}
}Compiled with -Wall, g++ 15 reports (the file name in the first line is the name you saved the file as):
l10_mistake_sign.cpp: In function ‘int main()’:
l10_mistake_sign.cpp:6:23: warning: comparison of integer expressions of different signedness: ‘int’ and ‘std::__cxx11::basic_string<char>::size_type’ {aka ‘long unsigned int’} [-Wsign-compare]
6 | for (int i = 0; i < row.size(); ++i) {
| ~~^~~~~~~~~~~~The word "signedness" means whether a number type can hold negative values. int can, and size() returns a type that cannot. The warning is a real signal, not noise. The cleanest fix in C++20 is std::ssize, which returns a signed size. It needs the <iterator> header:
#include <iostream>
#include <iterator>
#include <string>
int main() {
std::string row = "*";
for (int i = 0; i < std::ssize(row); ++i) {
std::cout << row[i] << '\n';
}
}This version compiles without the warning and prints *.
Practice
Each exercise is followed by its answer.
- Print a left-only staircase for
n = 5using the character#. Each row should have one more#than the row above it. The first row should be a single#.
Show the answer
#include <iostream>
#include <string>
int main() {
const int n = 5;
for (int i = 0; i < n; ++i) {
std::cout << std::string(i + 1, '#') << '\n';
}
}The output is #, ##, ###, #### and #####, one per line.
- For
n = 5, write the gap in spaces for each of the five rows of the mirror shape from Step 2.
Show the answer
Use 2 * (n - 1 - i) with n = 5. The rows give 8, 6, 4, 2 and 0 spaces. Row 0 has 8 spaces, and row 4 has none.
- Predict the output of the Step 2 program for
n = 2. Write each line, then check by running the program.
Show the answer
The first row has one star, a gap of 2 * (2 - 1 - 0), which is 2 spaces, and one more star. That gives * *. The second row has two stars and a gap of 0, which gives ****. Compiled and run, the output is:
* *
****- Using the number pyramid from Example 2, predict the output for
n = 3. Write the three lines before you run the program.
Show the answer
The leading spaces are 2, 1 and 0, and the peaks are 1, 2 and 3. The output is 1, 121 and 12321. Compiled and run with n = 3, the program prints exactly those three lines.
- In the column version from Example 3,
n = 4gives a shape 8 columns wide. Without running the program, state the column number, counting from 0, of the first star on the right side of row 1. Then check the answer by looking at the output.
Show the answer
The formula is width - 1 - i. With width = 8 and i = 1, the result is 6. Row 1 of the output is ** **. Columns 0 and 1 are stars, columns 2 to 5 are spaces, and columns 6 and 7 are stars. The first right-side star is column 6.
Summary
- Build the left half of a symmetric shape once, then print it again on the right.
- The gap in row
ifornrows is2 * (n - 1 - i). It shrinks by 2 on each row and reaches 0 on the last row. - A mirror image of a column
jis at columnwidth - 1 - j, wherewidthis the total number of columns. - Use
i < n, noti <= n, for a loop that runs once per row. A count that goes negative makesstd::stringthrow an error. - Use
std::ssizewhen you compare a loop counter with the length of a string, which avoids the sign comparison warning.