CS 121 Study Notes: Chapter Two - Variables, Data and the For loop

Introduction
  • This chapter delves into foundational programming concepts crucial for aspiring developers, including the intricate mechanics of nested for loops, identifying and rectifying common programming errors, and strategies for writing highly adaptable and flexible code.

  • A core theme throughout is the importance of continuous learning and iterative improvement in programming, emphasizing that mastery comes from consistent practice and understanding underlying principles.

Nested For Loops
  • A nested loop refers to a control structure where one loop is entirely contained within the body of another loop. This structure is commonly used for tasks requiring iteration over two-dimensional data structures, generating patterns, or processing combinations of elements.

Basic Structure of Nested Loops:
  • Consider the following Java code example, illustrating a simple nested loop structure:

for (int i = 1; i <= 5; i++) {
      for (int j = 1; j <= 10; j++) {
          System.out.print("*");
      }
      System.out.println(); // to end the line and move to the next row
  }
  • Projected Output: This code will produce a rectangular pattern of asterisks:

**********  
**********  
**********  
**********  
**********
  • Explanation:

    • The outer loop, controlled by the variable i, iterates 55 times (from i=1i=1 to i=5i=5).

    • For each single iteration of the outer loop, the inner loop, controlled by the variable j, executes its entire cycle 1010 times (from j=1j=1 to j=10j=10).

    • This means the System.out.print("*") statement is executed 5×10=505 \times 10 = 50 times in total.

    • System.out.println() after the inner loop ensures that after 1010 asterisks are printed, the cursor moves to the next line, creating the rows.

Nested For Loop Exercises

These exercises demonstrate how changing the inner loop's condition can drastically alter the output pattern.

Exercise 1:
  • Code Example:

for (int i = 1; i <= 5; i++) {
      for (int j = 1; j <= i; j++) {
          System.out.print("*");
      }
      System.out.println();
  }
  • Output: This code generates a right-angled triangle pattern of asterisks.

*
** 
*** 
**** 
*****
  • Explanation: The inner loop's condition j <= i causes the number of asterisks printed per line to increase with each iteration of the outer loop. When i=1i=1, j runs 11 time; when i=2i=2, j runs 22 times, and so on.

Exercise 2:
  • Code Example:

for (int i = 1; i <= 5; i++) {
      for (int j = 1; j <= i; j++) {
          System.out.print(i);
      }
      System.out.println();
  }
  • Output: This exercise prints the outer loop's counter variable, i, multiple times per line.

1  
22 
333 
4444
55555
  • Explanation: Similar to Exercise 1, the number of characters printed per line is determined by i. However, instead of an asterisk, the value of i itself is printed, resulting in lines like 1, 22, 333, etc.

Common Errors in Nested Loops

Misconfigurations in loop conditions or increments are frequent sources of errors, often leading to infinite loops or incorrect outputs. An infinite loop occurs when a loop's termination condition is never met, causing the program to run indefinitely and consume system resources.

  • Error 1 - Code Example (produces an infinite loop):

for (int i = 1; i <= 5; i++) {
      for (int j = 1; i <= 10; j++) {
          System.out.print("*");
      }
      System.out.println();
  }
  • Explanation of Error 1: The inner loop's condition i <= 10 mistakenly refers to the outer loop's counter i instead of its own counter j. Since i is controlled by the outer loop and its value (151-5) will always satisfy i <= 10 within the inner loop, the inner loop's variable j will increment indefinitely without i ever causing the inner loop to terminate. This leads to an infinite loop for the inner loop.

  • Error 2 - Code Example (produces an infinite loop):

for (int i = 1; i <= 5; i++) {
      for (int j = 1; j <= 10; i++) {
          System.out.print("*");
      }
      System.out.println();
  }
  • Explanation of Error 2: The inner loop's increment i++ mistakenly modifies the outer loop's counter i instead of its own counter j. This causes i to increment much faster than intended. More critically, the inner loop's j variable does not increment, meaning its condition j <= 10 is always true, resulting in an infinite loop for the inner j loop.

Creating Complex Output

To construct more elaborate graphical patterns or structured output, a layered approach with nested loops is indispensable:

  • An outer vertical loop is used to control the number of lines (rows) of output, iterating through each distinct line.

  • Inner horizontal loop(s) are then employed within each iteration of the outer loop to populate the specific patterns or characters that appear within that particular line (columns).

  • Example of Outer Loop structure for generating 5 lines:

for (int line = 1; line <= 5; line++) {
      // Inner loops for pattern elements on each line
  }
  • The pattern of each line might involve various elements, such as printing a certain number of dots followed by a unique line number, or combining different character sequences.

Nested Loop Solution for Complexity

To achieve specific complex output patterns, precise control over the number of elements printed by each inner loop is required. For instance, creating a pattern where the number of leading dots decreases while the line number is printed:

  • Code Example:

for (int line = 1; line <= 5; line++) {
      for (int j = 1; j <= (5 - line); j++) {
          System.out.print("."); // Prints leading dots
      }
      System.out.println(line); // Prints the line number and moves to next line
  }
  • Output: This code produces a pattern where an increasing line number is preceded by a decreasing count of dots.

....1  
...2  
..3   
.4    
5
  • Explanation: The expression (5 - line) strategically controls the number of dots. When line is 11, (5 - 1) = 4 dots are printed. When line is 55, (5 - 5) = 0 dots are printed, causing the dots to gradually decrease with each subsequent line.

Nested For Loop Exercise 3:
  • Code Example that combines dots and repeated line numbers:

for (int line = 1; line <= 5; line++) {
      for (int j = 1; j <= (-1 * line + 5); j++) {
          System.out.print("."); // Prints leading dots, decreasing per line
      }
      for (int k = 1; k <= line; k++) {
          System.out.print(line); // Prints the line number 'line' times
      }
      System.out.println(); // Moves to the next line
  }
  • Answer Output: This output combines traits from previous exercises, showing decreasing dots followed by the line number repeated line times.

....1  
...22 
..333  
.4444  
55555
  • Explanation: The first inner loop j <= (-1 * line + 5) (which is equivalent to j <= (5 - line)) generates the decreasing number of dots. The second inner loop k <= line is responsible for printing the current line number a number of times equal to line itself.

Adjustable Loops
  • The adaptability of loop structures is a fundamental principle for writing robust, reusable, and maintainable code. Avoiding "magic numbers" (hardcoded values) and instead using variables or constants makes code more flexible.

Expressions for Counter:
  • Here is an example of a method design() that creates a symmetrical pattern. While functional, it hardcodes the size (55), limiting its reusability:

public void design() {
      for (int line = 1; line <= 5; line++) {
          for (int j = 1; j <= (-1 * line + 5); j++) {
              System.out.print(".");
          }
          System.out.print(line);
          for (int j = 1; j <= (line - 1); j++) {
              System.out.print(".");
          }
          System.out.println();
      }
  }
Adaptability in Loop Size Controls:
  • To significantly enhance adaptability, replace hardcoded values with variables. This allows the program's behavior to be easily modified without changing the core logic of the loops.

  • Example Code demonstrating enhanced adaptability using a size variable:

int size = 6;
  for (int line = 1; line <= size; line++) {
      for (int j = 1; j <= (-1 * line + size); j++) {
          System.out.print(".");
      }
      System.out.print(line);
      for (int j = 1; j <= (line - 1); j++) {
          System.out.print(".");
      }
      System.out.println();
  }
  • Note: By introducing the size variable, the outer loop condition (line <= size) and the inner loop condition j <= (-1 * line + size) dynamically adjust based on the size value. Changing size from 55 (implied in the previous example) to 66 now alters the entire pattern's dimensions. This provides immense flexibility, making the code reusable for various pattern sizes.

Final Code Example (Good Practice):
  • For values that are intended to remain constant throughout the program's execution, it is good practice to declare them as final and use uppercase variable names. This signals to other developers that the value should not change and enhances readability.

final int SIZE = 6;
  for (int line = 1; line <= SIZE; line++) {
      for (int j = 1; j <= (-1 * line + SIZE); j++) {
          System.out.print(".");
      }
      System.out.print(line);
      for (int j = 1; j <= (line - 1); j++) {
          System.out.print(".");
      }
      System.out.println();
  }
  • Explanation of Good Practice: Using final int SIZE = 6; explicitly defines SIZE as a constant. This improves code clarity, prevents accidental modification, and makes future adjustments (e.g., changing the global size of the pattern) centralized and straightforward.

Conclusion
  • This chapter strongly emphasizes the critical importance of adaptability and flexibility in programming. The practice of avoiding hardcoding values and instead utilizing variables and constants (especially final constants for fixed values) is crucial for creating maintainable, scalable, and reusable code.

  • The principles discussed, from understanding nested loop mechanics to error handling and adaptive design, are foundational for developing robust programming skills. Continuous learning and practical application are encouraged as key drivers for skill development throughout the course.

End of the Lecture
  • Let Learning Continue.

  • The reinforcement throughout the chapter highlights that effective programming demands not only a deep understanding of core concepts but also the crucial ability to foresee and adapt to evolving requirements and potential changes. This empowers developers to create more resilient and efficient software solutions.