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:
- Strings: Represent textual data, from user input to displaying messages. They are immutable sequences of characters.
- Arrays: Store collections of data, allowing you to organize and manipulate related values. They are mutable and can hold various data types.
- Ubiquity: Used in every programming language and paradigm. Mastering them in JavaScript provides a foundation applicable elsewhere.
- Efficiency: Understanding how to efficiently manipulate strings and arrays directly impacts application performance.
- 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:
- Reverse the string.
- Capitalize the first letter of each word in the string.
- Count the number of vowels (a, e, i, o, u) in the string.
- 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
- Reversing the string: We use
split("")
to convert the string into an array of characters, thenreverse()
to reverse the array, and finallyjoin("")
to convert the array back into a string. - Capitalizing the first letter of each word: We split the string into an array of words using
split(" ")
. Then, we usemap()
to iterate over each word, capitalizing the first letter usingcharAt(0).toUpperCase()
and concatenating it with the rest of the word usingslice(1)
. Finally, we usejoin(" ")
to join the words back into a string. - Counting vowels: We convert the string to lowercase using
toLowerCase()
. Then, we split the string into an array of characters usingsplit("")
. We usefilter()
to keep only the characters that are vowels (checking usingincludes()
). Finally, we get the length of the filtered array. - 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:
- Merge the two arrays into a single array.
- Remove duplicate numbers from the merged array.
- Sort the merged array in ascending order.
- 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
- Merging the arrays: We use the
concat()
method to combine the two arrays into a new array. - 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. - 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. - 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
- 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). Theg
flag ensures that all non-alphanumeric characters are replaced, and thei
flag makes the regex case-insensitive. We then convert the string to lowercase usingtoLowerCase()
. - Reversing the cleaned string: We use the same technique as in Challenge 1:
split("").reverse().join("")
. - 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
- Cleaning the strings: Similar to the palindrome checker, we remove non-alphanumeric characters and convert to lowercase.
- Length check: If the strings have different lengths, they cannot be anagrams. This is an important optimization.
- 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. - 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
- Using a Hash Map (Map): We create a
Map
(similar to a hash table) to store each number in the array and its index. - Iteration and Lookup: We iterate through the array. For each number
nums[i]
, we calculate thecomplement
(the value needed to reach thetarget
). - Checking for Complement: We check if the
complement
exists as a key in theMap
. If it does, we've found the two numbers that add up to thetarget
. We return an array containing the index of the complement (obtained from theMap
) and the current indexi
. - Storing Number and Index: If the
complement
is not found in theMap
, we store the current numbernums[i]
and its indexi
in theMap
.
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
- Modulo Operation: We use
k = k % n
to handle cases wherek
is larger than the array lengthn
. This ensures thatk
is always within the bounds of the array's indices. - Reversal Technique: The key idea is to reverse the array in three steps:
- Reverse the entire array.
- Reverse the first
k
elements. - Reverse the remaining
n - k
elements.
- 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. - 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 thestart
andend
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
- Two-Pointer Approach: We use a two-pointer approach.
nonZeroIndex
keeps track of the index where the next non-zero element should be placed. - 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. - Incrementing
nonZeroIndex
: After each swap, we incrementnonZeroIndex
to point to the next position where a non-zero element should be placed. - 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
- Handle Empty Input: The function first checks if the input array is empty or null. If so, it returns an empty string.
- Initial Prefix: It assumes the first string in the array is the initial common prefix.
- 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
usingindexOf(prefix) !== 0
. - 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. - 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.
- 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
- Length Check: If the lengths of the two strings are different, they cannot be anagrams. This is an important early optimization.
- Character Frequency Counting: A JavaScript object
charCounts
(acting as a hash map) is used to store the frequency of each character in strings
. For each character ins
, its count incharCounts
is incremented. - Decrementing Counts: The function then iterates through string
t
. For each character int
, it checks if the character exists incharCounts
and if its count is greater than zero. If not, it means thatt
contains a character that is not ins
or thatt
contains a character more times than it appears ins
. In either case, the function returnsfalse
. If the character is found and its count is greater than zero, the count is decremented. - Final Check: After iterating through string
t
, the function checks if all counts incharCounts
are zero. If they are, it means thatt
used all the characters froms
exactly once, and the function returnstrue
. Otherwise, it means thats
contains characters that were not used int
, and the function returnsfalse
.
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:
- Open brackets must be closed by the same type of brackets.
- Open brackets must be closed in the correct order.
- 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
- 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.
- 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.
- Iterating Through the String: The function iterates through each character in the input string
s
. - 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 returnsfalse
. - If they match, the top element is popped from the stack (because it has been correctly closed).
- The top element of the stack is retrieved (or a special character
- Opening Bracket Handling: If the character is an opening bracket, it's pushed onto the stack.
- 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
- 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.
- 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.
- Avoid Loops When Possible: Use built-in methods like
map()
,filter()
,reduce()
, andforEach()
to perform operations on arrays whenever possible. These methods are often more efficient and readable than traditional loops. - 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. - Regular Expressions: Master regular expressions for advanced string manipulation and pattern matching.
- 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!
```