2665. Counter II


Problem Description

The task here is to design a function named createCounter that when executed will return an object composed of three methods. This generated object empowers us to manage a counter's value, which is initialized using the parameter init. The init parameter is an integer that sets the starting point for the counter.

The three methods that must be present in the returned object enable different operations on the counter:

  • increment(): This function should increase the current value of the counter by 1 whenever it is called, then return the updated value.
  • decrement(): Conversely, this function is responsible for reducing the current value of the counter by 1 each time it is invoked, then returning the new, decreased value.
  • reset(): This method serves to restore the counter's value back to the initial value specified by init when the counter was first created and should return the reset value.

The specified operations must adhere to encapsulation, ensuring the inner state of the counter cannot be directly modified from outside the object, except through these three functions. Implementing such functionality suggests an understanding of closure in JavaScript/TypeScript, as it's necessary to maintain the current value of the counter between function calls.

Intuition

The solution requires the concept of closure, which allows a function to remember and access its lexical scope even when it's executed outside that lexical scope. In JavaScript and TypeScript, functions can be nested within other functions, and the inner function will have access to the variables declared in its surrounding (outer) scope.

By utilizing closures, we can create private variables that are only accessible to the nested functions. In this scenario, we declare a variable val inside the createCounter function, which holds the value of the counter. This val variable is not directly accessible from the outside but can be manipulated only through the methods increment, decrement, and reset that modify and return val.

The increment and decrement methods make simple use of the pre-increment and pre-decrement operators (++ and --, respectively), which alter the value of val and then return it.

The reset method, on the other hand, reassigns val to the original init value, effectively resetting the counter.

The createCounter function then returns an object literal with these three methods. Each method has access to the val variable through closure, which allows these functions to maintain and modify the state of val without exposing it directly to the outside world, preserving data privacy and integrity.

Not Sure What to Study? Take the 2-min Quiz to Find Your Missing Piece:

Which of the following uses divide and conquer strategy?

Solution Approach

The implementation of the createCounter function employs the closure feature of JavaScript/TypeScript, which enables us to create an environment where a private variable that stores the current value of the counter (val) is accessible only within the scope of the function that creates it.

Here's a breakdown of the algorithm and patterns used:

  1. Initialization: When createCounter is called with an init value, we initiate a local variable val and assign it the value of init. This val acts as a private variable that holds the state of our counter.

  2. Encapsulation: The object returned by createCounter encapsulates the increment, decrement, and reset functions. Each of these functions has a specific purpose, and all of them have access to the val variable through closure. This ensures that val cannot be modified directly from outside the object, maintaining encapsulation and data privacy.

  3. Increment Operation: The increment method is straightforward; it uses the pre-increment operator (++val) to increase the value of val before returning it. This change persists within the closure so that subsequent calls to increment or the other methods will see the updated value.

  4. Decrement Operation: Similarly, the decrement method uses the pre-decrement operator (--val) to decrease val by 1 and then returns the new value.

  5. Reset Operation: The reset method sets val back to the initial value (init) and then returns it. This restores the counter to its initial state.

  6. Closure: Importantly, each of these methods is a closure; they all 'remember' the environment in which they were created (which includes the val variable). This is how they maintain the current value of the counter between function calls without exposing val directly.

Understanding how closures work is essential in this solution because they provide the mechanism to retain and manipulate the current value of the counter in a controlled way. The local variables within the createCounter function form a state that persists across the function calls through the returned object methods.

This pattern of using closures to maintain a private state is common in JavaScript and TypeScript for creating encapsulated structures, which is a foundational concept in functional programming.

In summary, the solution approach capitalizes on the language's functional programming capabilities, utilizing closures for state management while ensuring that the state remains private and mutable only through specific methods.

Discover Your Strengths and Weaknesses: Take Our 2-Minute Quiz to Tailor Your Study Plan:

How would you design a stack which has a function min that returns the minimum element in the stack, in addition to push and pop? All push, pop, min should have running time O(1).

Example Walkthrough

Let's take for an instance we call createCounter with an init value of 5. Here’s how we might walk through the solution approach:

  1. Initialization: We call createCounter(5). Inside createCounter, a variable val is set to 5. This variable val is the counter's current value and is private.

  2. Encapsulation: createCounter returns an object with three methods: increment, decrement, and reset. Each of these methods can operate on val, but nothing outside of these methods can alter val.

  3. Increment Operation: We call the increment method on the returned object. It uses ++val, so val becomes 6, and this value is returned. If we call increment() again, val then becomes 7.

  4. Decrement Operation: After a couple of increments, we call the decrement method. If val is currently at 7, decrement uses --val, decreasing the value to 6, and returns this new value.

  5. Reset Operation: At any point, if we call the reset method, val is set back to the initial value, which is 5 in this example. Calling reset() would return this initial value.

  6. Closure: Throughout the operations, our counter maintains the current value because increment, decrement, and reset are closures. They have access to val from their creation environment, even though the environment is not accessible directly.

By following this walkthrough with an initial value of 5, it becomes clear how each call influences the state of the counter and how the counter's value progresses through the operations, while still being encapsulated within the closure created by createCounter.

Solution Implementation

1# Define the operations that the Counter object will support
2class CounterOperations:
3    def increment(self) -> int:
4        pass
5
6    def decrement(self) -> int:
7        pass
8
9    def reset(self) -> int:
10        pass
11
12# The current value of the counter
13current_value: int
14
15# Initialize the counter with a specific value
16def initialize_counter(initial_value: int) -> None:
17    global current_value
18    current_value = initial_value
19
20# Increment the counter and return the new value
21def increment_counter() -> int:
22    global current_value
23    current_value += 1
24    return current_value
25
26# Decrement the counter and return the new value
27def decrement_counter() -> int:
28    global current_value
29    current_value -= 1
30    return current_value
31
32# Reset the counter to the initial value and return it
33def reset_counter(initial_value: int) -> int:
34    global current_value
35    current_value = initial_value
36    return current_value
37
38# Create a counter with the given initial value and return an object with operation methods
39def create_counter(initial_value: int) -> CounterOperations:
40    initialize_counter(initial_value)
41  
42    # This inner class represents a Counter and implements the CounterOperations interface
43    class Counter(CounterOperations):        
44        def increment(self) -> int:
45            return increment_counter()
46
47        def decrement(self) -> int:
48            return decrement_counter()
49
50        def reset(self) -> int:
51            return reset_counter(initial_value)
52
53    # Return an instance of the nested Counter class
54    return Counter()
55
56# Example usage:
57# counter = create_counter(5)
58# print(counter.increment())  # Outputs: 6
59# print(counter.reset())      # Outputs: 5
60# print(counter.decrement())  # Outputs: 4
61
1// Interface defining the operations of a counter
2interface CounterOperations {
3    int increment();
4    int decrement();
5    int reset();
6}
7
8// Class that encapsulates the logic of a counter
9class Counter implements CounterOperations {
10    // The current value of the counter
11    private int currentValue;
12  
13    // The initial value of the counter for reset purposes
14    private int initialValue;
15  
16    // Constructor to initialize the counter with a specific value
17    public Counter(int initialValue) {
18        this.initialValue = initialValue;
19        this.currentValue = initialValue;
20    }
21  
22    // Increment the counter and return the new value
23    @Override
24    public int increment() {
25        return ++currentValue;
26    }
27  
28    // Decrement the counter and return the new value
29    @Override
30    public int decrement() {
31        return --currentValue;
32    }
33  
34    // Reset the counter to the initial value and return it
35    @Override
36    public int reset() {
37        currentValue = initialValue;
38        return currentValue;
39    }
40}
41
42// The main class to demonstrate the usage of the Counter class
43public class Main {
44    public static void main(String[] args) {
45        // Create a counter with initial value 5
46        CounterOperations counter = new Counter(5);
47
48        // Increment the counter and output the value
49        System.out.println(counter.increment());  // Outputs: 6
50      
51        // Reset the counter and output the value
52        System.out.println(counter.reset());      // Outputs: 5
53      
54        // Decrement the counter and output the value
55        System.out.println(counter.decrement());  // Outputs: 4
56    }
57}
58
1#include <iostream>
2
3// Class to manage counter operations
4class CounterOperations {
5private:
6    int currentValue; // The current value of the counter
7
8public:
9    // Constructor initializes the counter with a specific value
10    CounterOperations(int initialValue) : currentValue(initialValue) {}
11
12    // Increment the counter and return the new value
13    int increment() {
14        return ++currentValue;
15    }
16
17    // Decrement the counter and return the new value
18    int decrement() {
19        return --currentValue;
20    }
21
22    // Reset the counter to the initial value and return it
23    int reset() {
24        currentValue = initialValue;
25        return currentValue;
26    }
27};
28
29// Function to create a counter with the given initial value
30// and return an instance with operation methods
31CounterOperations createCounter(int initialValue) {
32    CounterOperations counter(initialValue);
33    return counter;
34}
35
36// Example usage:
37int main() {
38    // Create a counter starting at 5
39    CounterOperations counter = createCounter(5);
40    std::cout << counter.increment() << std::endl; // Outputs: 6
41    std::cout << counter.reset() << std::endl;     // Outputs: 5
42    std::cout << counter.decrement() << std::endl; // Outputs: 4
43    return 0;
44}
45
1// Define the type for the object returned by 'createCounter' function
2type CounterOperations = {
3    increment: () => number;
4    decrement: () => number;
5    reset: () => number;
6};
7
8// The current value of the counter
9let currentValue: number;
10
11// Initialize the counter with a specific value
12function initializeCounter(initialValue: number): void {
13    currentValue = initialValue;
14}
15
16// Increment the counter and return the new value
17function incrementCounter(): number {
18    return ++currentValue;
19}
20
21// Decrement the counter and return the new value
22function decrementCounter(): number {
23    return --currentValue;
24}
25
26// Reset the counter to the initial value and return it
27function resetCounter(initialValue: number): number {
28    currentValue = initialValue;
29    return currentValue;
30}
31
32// Create a counter with the given initial value and return an object with operation methods
33function createCounter(initialValue: number): CounterOperations {
34    initializeCounter(initialValue);
35    return {
36        increment: incrementCounter,
37        decrement: decrementCounter,
38        reset: () => resetCounter(initialValue),
39    };
40}
41
42// Example usage:
43// const counter = createCounter(5)
44// console.log(counter.increment()); // Outputs: 6
45// console.log(counter.reset());     // Outputs: 5
46// console.log(counter.decrement()); // Outputs: 4
47
Not Sure What to Study? Take the 2-min Quiz:

What are the most two important steps in writing a depth first search function? (Select 2)

Time and Space Complexity

Time Complexity

The functions within createCounter:

  • increment() has a time complexity of O(1) because it performs a single operation of incrementing the val variable.
  • decrement() also has a time complexity of O(1) due to a single operation of decrementing the val.
  • reset() has a time complexity of O(1) as it assigns the init value to val.

Therefore, all methods provided by createCounter are constant time operations.

Space Complexity

The space complexity of createCounter is O(1) because it uses a fixed amount of space:

  • A single closure is created that captures the val variable.
  • No additional space that grows with the input size is used inside createCounter. The functions increment, decrement, and reset do not allocate any additional memory that depends on the size of the input or number of operations performed.

In summary, the space requirement remains constant regardless of the initial value or the number of operations performed.

Fast Track Your Learning with Our Quick Skills Quiz:

Which of these pictures shows the visit order of a depth-first search?


Recommended Readings


Got a question? Ask the Teaching Assistant anything you don't understand.

Still not clear? Ask in the Forum,  Discord or Submit the part you don't understand to our editors.


TA 👨‍🏫