Thursday

19-06-2025 Vol 19

Weekly Challenge: Strings and Arrays

Weekly Challenge: Mastering Strings and Arrays in JavaScript

Are you ready to level up your JavaScript skills? This week’s challenge focuses on two fundamental data structures: strings and arrays. These are the building blocks of virtually every application, and a solid understanding of them is crucial for any aspiring developer. This comprehensive guide will walk you through various challenges, providing explanations, code examples, and best practices to help you master these essential concepts.

Why Strings and Arrays Matter

Before we dive into the challenges, let’s understand why strings and arrays are so important:

  1. Strings: Represent textual data, from user input to displaying messages. They are immutable sequences of characters.
  2. Arrays: Store collections of data, allowing you to organize and manipulate related values. They are mutable and can hold various data types.
  3. Ubiquity: Used in every programming language and paradigm. Mastering them in JavaScript provides a foundation applicable elsewhere.
  4. Efficiency: Understanding how to efficiently manipulate strings and arrays directly impacts application performance.
  5. Algorithm Design: Many algorithms rely heavily on efficient string and array processing.

Challenge 1: String Manipulation

Challenge Description

Write a JavaScript function that takes a string as input and performs the following operations:

  1. Reverse the string.
  2. Capitalize the first letter of each word in the string.
  3. Count the number of vowels (a, e, i, o, u) in the string.
  4. Remove duplicate characters from the string, preserving the original order.

Example

Input: "hello world"

Output:

  • Reversed: "dlrow olleh"
  • Capitalized: "Hello World"
  • Vowel Count: 3
  • Without Duplicates: "helo wrd"

Solution

Here’s a JavaScript implementation of the string manipulation function:

    
function stringManipulation(str) {
  // Reverse the string
  const reversedString = str.split("").reverse().join("");

  // Capitalize the first letter of each word
  const capitalizedString = str
    .split(" ")
    .map((word) => word.charAt(0).toUpperCase() + word.slice(1))
    .join(" ");

  // Count the number of vowels
  const vowelCount = str.toLowerCase().split("").filter((char) => "aeiou".includes(char)).length;

  // Remove duplicate characters
  const uniqueChars = [...new Set(str.split(""))].join("");

  return {
    reversed: reversedString,
    capitalized: capitalizedString,
    vowelCount: vowelCount,
    uniqueChars: uniqueChars,
  };
}

// Example usage
const inputString = "hello world";
const result = stringManipulation(inputString);
console.log(result);
    
  

Explanation

  1. Reversing the string: We use split("") to convert the string into an array of characters, then reverse() to reverse the array, and finally join("") to convert the array back into a string.
  2. Capitalizing the first letter of each word: We split the string into an array of words using split(" "). Then, we use map() to iterate over each word, capitalizing the first letter using charAt(0).toUpperCase() and concatenating it with the rest of the word using slice(1). Finally, we use join(" ") to join the words back into a string.
  3. Counting vowels: We convert the string to lowercase using toLowerCase(). Then, we split the string into an array of characters using split(""). We use filter() to keep only the characters that are vowels (checking using includes()). Finally, we get the length of the filtered array.
  4. Removing duplicate characters: We use the Set object to create a collection of unique characters. The spread operator (...) converts the string into an array, then the `Set` automatically removes duplicates. Finally, we convert the `Set` back into a string using `join(“”)`.

Challenge 2: Array Operations

Challenge Description

Write a JavaScript function that takes two arrays of numbers as input and performs the following operations:

  1. Merge the two arrays into a single array.
  2. Remove duplicate numbers from the merged array.
  3. Sort the merged array in ascending order.
  4. Find the median of the sorted array.

Example

Input: arr1 = [1, 2, 3, 4, 5], arr2 = [3, 4, 5, 6, 7]

Output:

  • Merged: [1, 2, 3, 4, 5, 3, 4, 5, 6, 7]
  • Without Duplicates: [1, 2, 3, 4, 5, 6, 7]
  • Sorted: [1, 2, 3, 4, 5, 6, 7]
  • Median: 4

Solution

Here’s a JavaScript implementation of the array operations function:

    
function arrayOperations(arr1, arr2) {
  // Merge the two arrays
  const mergedArray = arr1.concat(arr2);

  // Remove duplicate numbers
  const uniqueArray = [...new Set(mergedArray)];

  // Sort the array in ascending order
  const sortedArray = uniqueArray.sort((a, b) => a - b);

  // Find the median
  const arrayLength = sortedArray.length;
  const middleIndex = Math.floor(arrayLength / 2);

  let median;
  if (arrayLength % 2 === 0) {
    // Even number of elements: average of the two middle elements
    median = (sortedArray[middleIndex - 1] + sortedArray[middleIndex]) / 2;
  } else {
    // Odd number of elements: the middle element
    median = sortedArray[middleIndex];
  }

  return {
    merged: mergedArray,
    unique: uniqueArray,
    sorted: sortedArray,
    median: median,
  };
}

// Example usage
const arr1 = [1, 2, 3, 4, 5];
const arr2 = [3, 4, 5, 6, 7];
const result = arrayOperations(arr1, arr2);
console.log(result);
    
  

Explanation

  1. Merging the arrays: We use the concat() method to combine the two arrays into a new array.
  2. Removing duplicate numbers: Again, we use the Set object to create a collection of unique numbers. The spread operator converts the array to a Set, removing duplicates, and then back to an array.
  3. Sorting the array: We use the sort() method with a comparison function (a, b) => a - b to sort the array in ascending order. This is important for numeric sorting; without it, JavaScript might treat the numbers as strings and sort them lexicographically.
  4. Finding the median: We first determine if the array has an even or odd number of elements. If it’s even, the median is the average of the two middle elements. If it’s odd, the median is simply the middle element. We use Math.floor(arrayLength / 2) to find the index of the middle element(s).

Challenge 3: Palindrome Checker

Challenge Description

Write a JavaScript function that checks if a given string is a palindrome (reads the same forwards and backward). The function should ignore case and non-alphanumeric characters (spaces, punctuation, etc.).

Example

Input: "A man, a plan, a canal: Panama"

Output: true

Input: "race a car"

Output: false

Solution

    
function isPalindrome(str) {
  // Remove non-alphanumeric characters and convert to lowercase
  const cleanStr = str.replace(/[^a-z0-9]/gi, "").toLowerCase();

  // Reverse the cleaned string
  const reversedStr = cleanStr.split("").reverse().join("");

  // Compare the cleaned string with its reversed version
  return cleanStr === reversedStr;
}

// Example usage
console.log(isPalindrome("A man, a plan, a canal: Panama")); // true
console.log(isPalindrome("race a car")); // false
    
  

Explanation

  1. Cleaning the string: The regular expression /[^a-z0-9]/gi matches any character that is not a letter (a-z) or a number (0-9). The g flag ensures that all non-alphanumeric characters are replaced, and the i flag makes the regex case-insensitive. We then convert the string to lowercase using toLowerCase().
  2. Reversing the cleaned string: We use the same technique as in Challenge 1: split("").reverse().join("").
  3. Comparing the strings: We simply compare the cleaned string with its reversed version using the strict equality operator (===).

Challenge 4: Anagram Checker

Challenge Description

Write a JavaScript function that checks if two given strings are anagrams of each other (contain the same characters in a different order). The function should ignore case and non-alphanumeric characters.

Example

Input: str1 = "listen", str2 = "silent"

Output: true

Input: str1 = "hello", str2 = "world"

Output: false

Solution

    
function areAnagrams(str1, str2) {
  // Clean the strings (remove non-alphanumeric characters and lowercase)
  const cleanStr1 = str1.replace(/[^a-z0-9]/gi, "").toLowerCase();
  const cleanStr2 = str2.replace(/[^a-z0-9]/gi, "").toLowerCase();

  // Check if the lengths are different
  if (cleanStr1.length !== cleanStr2.length) {
    return false;
  }

  // Sort the characters in each string
  const sortedStr1 = cleanStr1.split("").sort().join("");
  const sortedStr2 = cleanStr2.split("").sort().join("");

  // Compare the sorted strings
  return sortedStr1 === sortedStr2;
}

// Example usage
console.log(areAnagrams("listen", "silent")); // true
console.log(areAnagrams("hello", "world")); // false
console.log(areAnagrams("Dormitory", "dirty room##")); // true
    
  

Explanation

  1. Cleaning the strings: Similar to the palindrome checker, we remove non-alphanumeric characters and convert to lowercase.
  2. Length check: If the strings have different lengths, they cannot be anagrams. This is an important optimization.
  3. Sorting the characters: We split each string into an array of characters, sort the array alphabetically using sort() (without a comparison function, as we’re sorting strings), and then join the array back into a string.
  4. Comparing the sorted strings: If the sorted strings are equal, the original strings are anagrams.

Challenge 5: Two Sum Problem

Challenge Description

Given an array of integers nums and an integer target, return indices of the two numbers such that they add up to target.

You may assume that each input would have exactly one solution, and you may not use the same element twice.

You can return the answer in any order.

Example

Input: nums = [2,7,11,15], target = 9

Output: [0,1]

Explanation: Because nums[0] + nums[1] == 9, we return [0, 1].

Solution

    
function twoSum(nums, target) {
  const numMap = new Map();

  for (let i = 0; i < nums.length; i++) {
    const complement = target - nums[i];
    if (numMap.has(complement)) {
      return [numMap.get(complement), i];
    }
    numMap.set(nums[i], i);
  }

  return null; // Should never happen given the problem constraints
}

// Example usage
const nums = [2, 7, 11, 15];
const target = 9;
const result = twoSum(nums, target);
console.log(result); // [0, 1]
    
  

Explanation

  1. Using a Hash Map (Map): We create a Map (similar to a hash table) to store each number in the array and its index.
  2. Iteration and Lookup: We iterate through the array. For each number nums[i], we calculate the complement (the value needed to reach the target).
  3. Checking for Complement: We check if the complement exists as a key in the Map. If it does, we've found the two numbers that add up to the target. We return an array containing the index of the complement (obtained from the Map) and the current index i.
  4. Storing Number and Index: If the complement is not found in the Map, we store the current number nums[i] and its index i in the Map.

Challenge 6: Rotate Array

Challenge Description

Given an array, rotate the array to the right by k steps, where k is non-negative.

Example

Input: nums = [1,2,3,4,5,6,7], k = 3

Output: [5,6,7,1,2,3,4]

Explanation:
Rotate 1 steps to the right: [7,1,2,3,4,5,6]
Rotate 2 steps to the right: [6,7,1,2,3,4,5]
Rotate 3 steps to the right: [5,6,7,1,2,3,4]

Input: nums = [-1,-100,3,99], k = 2

Output: [3,99,-1,-100]

Explanation:
Rotate 1 steps to the right: [99,-1,-100,3]
Rotate 2 steps to the right: [3,99,-1,-100]

Solution

    
function rotateArray(nums, k) {
    const n = nums.length;
    k = k % n; // Handle cases where k is larger than the array length

    // Reverse the entire array
    nums.reverse();

    // Reverse the first k elements
    reverse(nums, 0, k - 1);

    // Reverse the remaining elements
    reverse(nums, k, n - 1);

    return nums;

    function reverse(arr, start, end) {
        while (start < end) {
            [arr[start], arr[end]] = [arr[end], arr[start]]; // Swap elements
            start++;
            end--;
        }
    }
}

// Example usage
const nums1 = [1, 2, 3, 4, 5, 6, 7];
const k1 = 3;
console.log(rotateArray(nums1, k1)); // [5, 6, 7, 1, 2, 3, 4]

const nums2 = [-1, -100, 3, 99];
const k2 = 2;
console.log(rotateArray(nums2, k2)); // [3, 99, -1, -100]
    
  

Explanation

  1. Modulo Operation: We use k = k % n to handle cases where k is larger than the array length n. This ensures that k is always within the bounds of the array's indices.
  2. Reversal Technique: The key idea is to reverse the array in three steps:
    1. Reverse the entire array.
    2. Reverse the first k elements.
    3. Reverse the remaining n - k elements.
  3. Reverse Helper Function: The reverse function takes an array, a start index, and an end index, and reverses the elements within that range using the two-pointer technique.
  4. Swapping Elements: Inside the reverse function, we use destructuring assignment [arr[start], arr[end]] = [arr[end], arr[start]] for a concise way to swap elements at the start and end indices.

Challenge 7: Move Zeroes

Challenge Description

Given an integer array nums, move all 0's to the end of it while maintaining the relative order of the non-zero elements.

Note that you must do this in-place without making a copy of the array.

Example

Input: nums = [0,1,0,3,12]

Output: [1,3,12,0,0]

Input: nums = [0]

Output: [0]

Solution

    
function moveZeroes(nums) {
    let nonZeroIndex = 0;

    // Iterate through the array
    for (let i = 0; i < nums.length; i++) {
        // If the current element is not zero
        if (nums[i] !== 0) {
            // Swap the current element with the element at nonZeroIndex
            [nums[nonZeroIndex], nums[i]] = [nums[i], nums[nonZeroIndex]];
            nonZeroIndex++;
        }
    }

    return nums;
}

// Example usage
const nums1 = [0, 1, 0, 3, 12];
console.log(moveZeroes(nums1)); // [1, 3, 12, 0, 0]

const nums2 = [0];
console.log(moveZeroes(nums2)); // [0]

const nums3 = [1, 0, 2, 0, 3];
console.log(moveZeroes(nums3)); // [1, 2, 3, 0, 0]
    
  

Explanation

  1. Two-Pointer Approach: We use a two-pointer approach. nonZeroIndex keeps track of the index where the next non-zero element should be placed.
  2. In-Place Modification: We iterate through the array. If we find a non-zero element, we swap it with the element at nonZeroIndex. This effectively moves the non-zero element to its correct position while keeping the relative order intact.
  3. Incrementing nonZeroIndex: After each swap, we increment nonZeroIndex to point to the next position where a non-zero element should be placed.
  4. Zeroes at the End: After the loop completes, all non-zero elements will be at the beginning of the array, and all zeroes will be at the end.

Challenge 8: Longest Common Prefix

Challenge Description

Write a function to find the longest common prefix string amongst an array of strings.

If there is no common prefix, return an empty string "".

Example

Input: strs = ["flower","flow","flight"]

Output: "fl"

Input: strs = ["dog","racecar","car"]

Output: ""

Explanation: There is no common prefix among the input strings.

Solution

    
function longestCommonPrefix(strs) {
  if (!strs || strs.length === 0) {
    return "";
  }

  // Take the first string as the base for comparison
  let prefix = strs[0];

  // Iterate through the remaining strings
  for (let i = 1; i < strs.length; i++) {
    // While the current string doesn't start with the current prefix
    while (strs[i].indexOf(prefix) !== 0) {
      // Shorten the prefix by one character from the end
      prefix = prefix.substring(0, prefix.length - 1);

      // If the prefix becomes empty, there is no common prefix
      if (prefix === "") {
        return "";
      }
    }
  }

  return prefix;
}


// Example usage
const strs1 = ["flower", "flow", "flight"];
console.log(longestCommonPrefix(strs1)); // "fl"

const strs2 = ["dog", "racecar", "car"];
console.log(longestCommonPrefix(strs2)); // ""

const strs3 = ["cir", "car"];
console.log(longestCommonPrefix(strs3)); // "c"
    
  

Explanation

  1. Handle Empty Input: The function first checks if the input array is empty or null. If so, it returns an empty string.
  2. Initial Prefix: It assumes the first string in the array is the initial common prefix.
  3. Iterate and Compare: It iterates through the remaining strings in the array. For each string, it checks if the string starts with the current prefix using indexOf(prefix) !== 0.
  4. Shorten Prefix: If a string does not start with the current prefix, the prefix is shortened by one character from the end using substring(0, prefix.length - 1). This process continues until the string starts with the shortened prefix, or the prefix becomes an empty string.
  5. No Common Prefix: If the prefix becomes an empty string during the shortening process, it means there is no common prefix among all the strings, and the function returns an empty string.
  6. Return Result: After iterating through all the strings, the remaining prefix is the longest common prefix, and the function returns it.

Challenge 9: Valid Anagram (using character counts)

Challenge Description

Given two strings s and t, return true if t is an anagram of s, and false otherwise.

An Anagram is a word or phrase formed by rearranging the letters of a different word or phrase, typically using all the original letters exactly once.

Example

Input: s = "anagram", t = "nagaram"

Output: true

Input: s = "rat", t = "car"

Output: false

Solution

    
function isAnagram(s, t) {
    if (s.length !== t.length) {
        return false;
    }

    const charCounts = {};

    // Count character frequencies in string s
    for (let char of s) {
        charCounts[char] = (charCounts[char] || 0) + 1;
    }

    // Decrement character counts based on string t
    for (let char of t) {
        if (!charCounts[char]) {
            return false; // Character not found in s, or count is already zero
        }
        charCounts[char]--;
    }

    // Check if all counts are zero (meaning t used all characters from s exactly once)
    for (let char in charCounts) {
        if (charCounts[char] !== 0) {
            return false; // Some characters in s were not used in t
        }
    }

    return true;
}

// Example usage
const s1 = "anagram";
const t1 = "nagaram";
console.log(isAnagram(s1, t1)); // true

const s2 = "rat";
const t2 = "car";
console.log(isAnagram(s2, t2)); // false
    
  

Explanation

  1. Length Check: If the lengths of the two strings are different, they cannot be anagrams. This is an important early optimization.
  2. Character Frequency Counting: A JavaScript object charCounts (acting as a hash map) is used to store the frequency of each character in string s. For each character in s, its count in charCounts is incremented.
  3. Decrementing Counts: The function then iterates through string t. For each character in t, it checks if the character exists in charCounts and if its count is greater than zero. If not, it means that t contains a character that is not in s or that t contains a character more times than it appears in s. In either case, the function returns false. If the character is found and its count is greater than zero, the count is decremented.
  4. Final Check: After iterating through string t, the function checks if all counts in charCounts are zero. If they are, it means that t used all the characters from s exactly once, and the function returns true. Otherwise, it means that s contains characters that were not used in t, and the function returns false.

Challenge 10: Valid Parentheses

Challenge Description

Given a string s containing just the characters '(', ')', '{', '}', '[' and ']', determine if the input string is valid.

An input string is valid if:

  1. Open brackets must be closed by the same type of brackets.
  2. Open brackets must be closed in the correct order.
  3. Every close bracket has a corresponding open bracket of the same type.

Example

Input: s = "()"

Output: true

Input: s = "()[]{}"

Output: true

Input: s = "(]"

Output: false

Solution

    
function isValid(s) {
    const stack = [];
    const mapping = {
        ')': '(',
        '}': '{',
        ']': '['
    };

    for (let char of s) {
        if (mapping[char]) { // Closing bracket
            const topElement = stack.length === 0 ? '#' : stack.pop(); // Handle empty stack case

            if (mapping[char] !== topElement) {
                return false; // Mismatched brackets
            }
        } else { // Opening bracket
            stack.push(char); // Push onto the stack
        }
    }

    return stack.length === 0; // Stack should be empty if all brackets are closed correctly
}

// Example usage
const s1 = "()";
console.log(isValid(s1)); // true

const s2 = "()[]{}";
console.log(isValid(s2)); // true

const s3 = "(]";
console.log(isValid(s3)); // false

const s4 = "([)]";
console.log(isValid(s4)); // false

const s5 = "{[]}";
console.log(isValid(s5)); // true
    
  

Explanation

  1. Stack Data Structure: A stack is used to keep track of opening brackets. When an opening bracket is encountered, it's pushed onto the stack.
  2. Bracket Mapping: A mapping (JavaScript object) is used to associate closing brackets with their corresponding opening brackets. This allows for easy checking of matching bracket types.
  3. Iterating Through the String: The function iterates through each character in the input string s.
  4. Closing Bracket Handling: If the character is a closing bracket:
    • The top element of the stack is retrieved (or a special character '#' if the stack is empty to handle cases where there's a closing bracket without a corresponding opening bracket).
    • The function checks if the top element of the stack matches the expected opening bracket for the current closing bracket using the mapping. If they don't match, the string is invalid, and the function returns false.
    • If they match, the top element is popped from the stack (because it has been correctly closed).
  5. Opening Bracket Handling: If the character is an opening bracket, it's pushed onto the stack.
  6. Final Stack Check: After iterating through the entire string, the function checks if the stack is empty. If it is, it means that all opening brackets have been correctly closed, and the string is valid. If the stack is not empty, it means that there are unclosed opening brackets, and the string is invalid.

Best Practices for Working with Strings and Arrays

  1. Immutability (Strings): Remember that strings are immutable in JavaScript. Operations that appear to modify a string actually create a new string. For performance-critical applications, consider using arrays of characters and joining them when done.
  2. Choosing the Right Data Structure: Understand the trade-offs between arrays and other data structures like Sets and Maps. Sets are great for uniqueness, and Maps are great for key-value lookups.
  3. Avoid Loops When Possible: Use built-in methods like map(), filter(), reduce(), and forEach() to perform operations on arrays whenever possible. These methods are often more efficient and readable than traditional loops.
  4. Use Spread Syntax (...) Wisely: The spread syntax is powerful for creating shallow copies of arrays and objects, merging arrays, and passing variable arguments to functions.
  5. Regular Expressions: Master regular expressions for advanced string manipulation and pattern matching.
  6. Performance Considerations: Be mindful of the time and space complexity of your string and array operations, especially when dealing with large datasets.

Conclusion

This weekly challenge has provided a comprehensive overview of essential string and array manipulation techniques in JavaScript. By tackling these challenges and understanding the underlying concepts, you'll be well-equipped to handle a wide range of programming tasks. Keep practicing, and you'll become a master of strings and arrays in no time!

```

omcoding

Leave a Reply

Your email address will not be published. Required fields are marked *