← Back to DevBytes

Solving Count and Say in JavaScript: Step-by-Step Guide

Introduction to the Count and Say Problem

The Count and Say sequence is one of the most popular algorithmic problems on platforms like LeetCode. It is a fascinating sequence that reads itself aloud, term by term, generating each new term by describing the digits of the previous one. Despite its apparent simplicity, the problem tests your understanding of string manipulation, iteration, and careful edge-case handling.

In this tutorial, we will explore the Count and Say problem in depth, understand its mechanics, walk through a complete JavaScript solution, and discuss best practices to write clean, efficient code.

What Is the Count and Say Sequence?

The Count and Say sequence starts with the string "1". Each subsequent term is generated by reading the digits of the previous term aloud, counting consecutive identical digits, and writing the count followed by the digit.

For example:

Given a positive integer n, the task is to return the nth term of this sequence.

Why the Problem Matters

While the Count and Say sequence may seem like a brainteaser, it carries real value for developers:

Additionally, the sequence itself has interesting mathematical properties. It is related to the look-and-say sequence introduced by mathematician John Conway, who proved that no digit greater than 3 ever appears in the sequence (when starting from "1"), and that the sequence grows exponentially with a characteristic growth rate known as Conway's constant.

Breaking Down the Algorithm

Before writing code, let us break the problem into clear steps:

The key insight is that we never need to store the entire sequence — only the current term. This keeps memory usage low.

Identifying Consecutive Groups

The trickiest part for beginners is grouping consecutive identical digits. The standard approach uses a pointer that advances while the next character matches the current one. Once the run ends, we record the length of the run and the digit, then continue from the next new digit.

Implementing the Solution in JavaScript

Let us now implement the algorithm step by step.

Basic Iterative Solution

function countAndSay(n) {
  // Base case: the first term is always "1"
  let current = "1";

  // Generate terms 2 through n
  for (let term = 2; term <= n; term++) {
    let next = "";
    let i = 0;

    // Traverse the current string
    while (i < current.length) {
      let count = 1;

      // Count consecutive identical digits
      while (i + 1 < current.length && current[i] === current[i + 1]) {
        count++;
        i++;
      }

      // Append the count and the digit
      next += count.toString() + current[i];
      i++;
    }

    // Move to the next term
    current = next;
  }

  return current;
}

// Example usage
console.log(countAndSay(1)); // "1"
console.log(countAndSay(4)); // "1211"
console.log(countAndSay(5)); // "111221"

This solution is straightforward and easy to reason about. The outer loop controls which term we are generating, while the inner loop scans the current string and builds the next term.

Tracing Through an Example

To solidify understanding, let us trace countAndSay(4):

Optimizing with Array Joining

String concatenation in JavaScript creates a new string each time, which can become expensive for large inputs. A common optimization is to collect pieces in an array and join them at the end.

function countAndSayOptimized(n) {
  let current = "1";

  for (let term = 2; term <= n; term++) {
    const parts = [];
    let i = 0;

    while (i < current.length) {
      let count = 1;

      while (i + 1 < current.length && current[i] === current[i + 1]) {
        count++;
        i++;
      }

      parts.push(count, current[i]);
      i++;
    }

    current = parts.join("");
  }

  return current;
}

console.log(countAndSayOptimized(6)); // "312211"

By pushing both the count and the digit into the array and joining once, we reduce the number of intermediate string allocations. For typical interview constraints (n up to 30), this optimization is not strictly necessary, but it demonstrates good performance awareness.

Recursive Approach

Because each term depends on the previous one, the problem naturally lends itself to recursion. Here is a recursive implementation:

function countAndSayRecursive(n) {
  // Base case
  if (n === 1) {
    return "1";
  }

  // Get the previous term
  const previous = countAndSayRecursive(n - 1);

  // Build the current term from the previous one
  let result = "";
  let i = 0;

  while (i < previous.length) {
    let count = 1;

    while (i + 1 < previous.length && previous[i] === previous[i + 1]) {
      count++;
      i++;
    }

    result += count.toString() + previous[i];
    i++;
  }

  return result;
}

console.log(countAndSayRecursive(5)); // "111221"

The recursive version is elegant and mirrors the mathematical definition closely. However, it consumes additional stack space proportional to n. For very large values of n, the iterative approach is safer.

Handling Edge Cases

Robust code must handle edge cases gracefully. Consider the following scenarios:

function countAndSaySafe(n) {
  if (!Number.isInteger(n) || n < 1) {
    throw new Error("n must be a positive integer");
  }

  let current = "1";

  for (let term = 2; term <= n; term++) {
    const parts = [];
    let i = 0;

    while (i < current.length) {
      let count = 1;

      while (i + 1 < current.length && current[i] === current[i + 1]) {
        count++;
        i++;
      }

      parts.push(count, current[i]);
      i++;
    }

    current = parts.join("");
  }

  return current;
}

try {
  console.log(countAndSaySafe(0)); // throws
} catch (error) {
  console.error(error.message); // "n must be a positive integer"
}

Best Practices

Here are some best practices to keep in mind when solving the Count and Say problem and similar string-based challenges:

Writing Unit Tests

Testing is essential to ensure correctness. Here is a simple test suite using Node's built-in assert module:

const assert = require("assert");

function runTests() {
  const expected = [
    "1",      // n = 1
    "11",     // n = 2
    "21",     // n = 3
    "1211",   // n = 4
    "111221", // n = 5
    "312211", // n = 6
    "13112221", // n = 7
  ];

  for (let n = 1; n <= expected.length; n++) {
    assert.strictEqual(
      countAndSay(n),
      expected[n - 1],
      `Failed for n = ${n}`
    );
    console.log(`n = ${n} passed`);
  }

  console.log("All tests passed!");
}

runTests();

Running these tests gives you confidence that your implementation matches the expected sequence for the first several terms, which is usually sufficient for interview purposes.

Time and Space Complexity Analysis

Understanding the complexity of your solution is important for interviews and real-world applications.

For the typical constraint of n <= 30, both time and space are manageable on modern hardware.

Common Mistakes to Avoid

When implementing Count and Say, watch out for these frequent pitfalls:

Conclusion

The Count and Say problem is a deceptively simple exercise that rewards careful string traversal, clear iterative logic, and attention to edge cases. By starting from the base term "1" and repeatedly describing each term to produce the next, you can generate any term in the sequence efficiently. Whether you choose an iterative, recursive, or array-optimized approach, the key is to write code that is readable, tested, and mindful of performance characteristics. Mastering this problem not only prepares you for coding interviews but also sharpens fundamental skills in string manipulation and algorithmic thinking that transfer to countless other challenges.

— Ad —

Google AdSense will appear here after approval

← Back to all articles