Start learning free

LESSON 367 OF 410 · RECURSION

Decompress File Paths in JavaScript (Time Complexity: O(n))

Practice Read the full explanation

A comma separated list of strings can be compressed by finding common substrings and grouping those strings together into Token Lists

More formally, a Token List is a list where each item can be one of the following:

  • a string
  • a concatenation of Token Lists

Examples:

ab,ac => a{b,c}
ac,ad,bc,bd => {a,b}{c,d}
axc,axd,bxc,bxd => {a,b}x{c,d}

This technique is used to compress folder paths since a folder like

README.md
resources/
    js/
        app.js
        boot.js
        comp/
            a.js
            b.js

can be represented as a list of strings:
[
    "README.md",
    "resources/js/app.js",
    "resources/js/boot.js",
    "resources/js/comp/a.js",
    "resources/js/comp/b.js"
]

and further encoded into comma separated string
“README.md,resources/js/app.js,resources/js/boot.js,resources/js/comp/a.js,resources/js/comp/b.js”
and then we can compress them to the following Token List:
README.md,resources/js/{app,boot,comp/{a,b}}.js

Given a Token List representing a compressed folder, return the initial list of paths.

Example 1:

Input:
"resources/{js/{app.js,comp/{a.js,b.js},boot.js}},README.md"

Output: [
    "README.md",
    "resources/js/app.js",
    "resources/js/boot.js",
    "resources/js/comp/a.js",
    "resources/js/comp/b.js"
]

Example 2:

Input:
"my_{first,second}_folder/{a,b}{.js,.py}"

Output: [
    "my_first_folder/a.js",
    "my_first_folder/a.py",
    "my_first_folder/b.js",
    "my_first_folder/b.py",
    "my_second_folder/a.js",
    "my_second_folder/a.py",
    "my_second_folder/b.js",
    "my_second_folder/b.py"
]

Note: The resulting array can be in any order


STEP 1 OF 19
    Ctrl + Enter to check

    JAVASCRIPT · RECURSION

    Decompress File Paths in JavaScript, explained

    Understanding the Problem

    The core challenge of this problem is to decompress a given Token List into its original list of paths. This involves parsing the Token List, identifying the grouped substrings, and generating all possible combinations of these substrings to form the original paths.

    This problem is significant in scenarios where file paths or similar hierarchical data structures are compressed for storage or transmission efficiency. Potential pitfalls include correctly handling nested Token Lists and ensuring all combinations are generated.

    Approach

    To solve this problem, we need to parse the Token List and generate all possible combinations of substrings. Here's a step-by-step approach:

    1. Identify and extract the substrings within curly braces `{}`.
    2. Generate all possible combinations of these substrings.
    3. Concatenate these combinations with the remaining parts of the string to form the original paths.

    A naive solution would involve recursively parsing the string and generating combinations, but this can be inefficient for deeply nested structures. An optimized approach involves using a stack to manage the nested structures and iteratively generating combinations.

    Algorithm

    Here's a step-by-step breakdown of the algorithm:

    1. Initialize a stack to keep track of the current context (i.e., the current part of the path being processed).
    2. Iterate through the Token List character by character.
    3. When encountering a `{`, push the current context onto the stack and start a new context.
    4. When encountering a `}`, pop the context from the stack and generate all combinations with the current context.
    5. Continue until the entire Token List is processed.
    6. Return the generated paths.

    Code Implementation

    /**
     * Decompresses a given Token List into its original list of paths.
     * @param {string} tokenList - The compressed Token List.
     * @return {string[]} - The original list of paths.
     */
    function decompressFilePaths(tokenList) {
        // Helper function to generate all combinations of two arrays
        function combine(arr1, arr2) {
            const result = [];
            for (const str1 of arr1) {
                for (const str2 of arr2) {
                    result.push(str1 + str2);
                }
            }
            return result;
        }
    
        // Stack to manage nested contexts
        const stack = [[]];
        let current = [''];
    
        for (let i = 0; i < tokenList.length; i++) {
            const char = tokenList[i];
    
            if (char === '{') {
                // Push current context to stack and start a new context
                stack.push(current);
                current = [''];
            } else if (char === '}') {
                // Pop from stack and combine with current context
                const prev = stack.pop();
                current = combine(prev, current);
            } else if (char === ',') {
                // Split current context
                stack[stack.length - 1].push(...current);
                current = [''];
            } else {
                // Append character to current context
                for (let j = 0; j < current.length; j++) {
                    current[j] += char;
                }
            }
        }
    
        // Combine remaining context with stack
        while (stack.length > 0) {
            current = combine(stack.pop(), current);
        }
    
        return current;
    }
    
    // Example usage:
    console.log(decompressFilePaths("resources/{js/{app.js,comp/{a.js,b.js},boot.js}},README.md"));
    // Output: ["README.md", "resources/js/app.js", "resources/js/boot.js", "resources/js/comp/a.js", "resources/js/comp/b.js"]
    
    console.log(decompressFilePaths("my_{first,second}_folder/{a,b}{.js,.py}"));
    // Output: ["my_first_folder/a.js", "my_first_folder/a.py", "my_first_folder/b.js", "my_first_folder/b.py", "my_second_folder/a.js", "my_second_folder/a.py", "my_second_folder/b.js", "my_second_folder/b.py"]
    

    Complexity Analysis

    The time complexity of this approach is O(n), where n is the length of the Token List. This is because we iterate through the Token List once and generate combinations iteratively. The space complexity is also O(n) due to the stack and the generated combinations.

    Edge Cases

    Potential edge cases include:

    • Empty Token List: The output should be an empty array.
    • Single path without any curly braces: The output should be the same as the input.
    • Deeply nested Token Lists: The algorithm should handle this efficiently without stack overflow.

    Examples:

    console.log(decompressFilePaths("")); // Output: []
    console.log(decompressFilePaths("README.md")); // Output: ["README.md"]
    console.log(decompressFilePaths("a{b,c{d,e}}f")); // Output: ["abf", "acdf", "acef"]
    

    Testing

    To test the solution comprehensively, consider the following test cases:

    • Simple paths without any curly braces.
    • Paths with single-level curly braces.
    • Paths with nested curly braces.
    • Edge cases as mentioned above.

    Use a testing framework like Jest or Mocha to automate the testing process.

    Thinking and Problem-Solving Tips

    When approaching such problems, consider the following tips:

    • Break down the problem into smaller, manageable parts.
    • Use helper functions to simplify complex operations.
    • Think about edge cases and how to handle them.
    • Practice similar problems to improve problem-solving skills.

    Conclusion

    In this blog post, we discussed how to decompress a given Token List into its original list of paths. We covered the problem definition, approach, algorithm, code implementation, complexity analysis, edge cases, and testing. Understanding and solving such problems is crucial for efficient data processing and manipulation. Keep practicing and exploring further to improve your skills.

    Additional Resources

    Keep going with JavaScriptAbout 340 step-by-step lessons, from your first line of code to coding interviews. Free to start.
    Start learning free