How to solve the Leetcode 1657. Determine if Two Strings Are Close problem in JavaScript
Use a hash map to compare the letters and their frequencies
Table of contents
The Problem
This problem is described:
Two strings are considered close if you can attain one from the other using the following operations:
Operation 1: Swap any two existing characters.
- For example,
abcde -> aecdb
Operation 2: Transform every occurrence of one existing character into another existing character, and do the same with the other character.
- For example,
aacabb -> bbcbaa
(alla
's turn intob
's, and allb
's turn intoa
's)You can use the operations on either string as many times as necessary.
Given two strings,
word1
andword2
, returntrue
ifword1
andword2
are close, andfalse
otherwise.Example 1:
Input: word1 = "abc", word2 = "bca" Output: true Explanation: You can attain word2 from word1 in 2 operations. Apply Operation 1: "abc" -> "acb" Apply Operation 1: "acb" -> "bca"
Example 2:
Input: word1 = "a", word2 = "aa" Output: false Explanation: It is impossible to attain word2 from word1, or vice versa, in any number of operations.
Example 3:
Input: word1 = "cabbba", word2 = "abbccc" Output: true Explanation: You can attain word2 from word1 in 3 operations. Apply Operation 1: "cabbba" -> "caabbb" Apply Operation 2: "caabbb" -> "baaccc" Apply Operation 2: "baaccc" -> "abbccc"
Constraints:
1 <= word1.length, word2.length <= 105
word1
andword2
contain only lowercase English letters.
Key Insights
This problem does not seem that approachable until coming to the following two key insights:
You can freely reorder the existing letters in a string by swapping any two existing letters. This is done through operation 1 in the problem description. For example,
abcde -> aecdb
.You can also freely reassign the frequencies that any letter occurs. This is done by transforming every occurrence of one existing character into another existing character, and doing the same with the other character. This is done with operation 2 in the problem description. For example,
aacabb -> bbcbaa
, (alla
's turn intob
's, and allb
's turn intoa
's).
These insights are not easy to come by at first glance. A good way to approach something like this is to play around with the two operations with a few example strings. While doing this, think about questions like:
What is the operation really doing?
Is there a more basic, more abstract way to describe what the operation is doing?
If that doesn't work, luckily for this problem, the two insights are listed in the hints for the problem.
Solution
Since you can freely reorder the strings, and freely reassign the frequencies of the letters to other letters, this means that you just need to find a way to test whether the strings consist of the same letters, and whether the letter frequencies (separate from any particular letters) are the same for both strings.
Here is one way to solve the problem, annotated with explanations along the way:
var closeStrings = function(word1, word2) {
// If the strings are not the same length, return
// false as there is no way they'll be considered
// "close" per the problem description
if (word1.length !== word2.length)
return false;
/* Process each string with the helper function
getData (see below). The data1 & data2 variables get
assigned to an object returned by the function. That
object has an array of letters from the string and an
array of their frequencies, both sorted. The frequencies
are just an array of numbers, no longer associated with
any particular letter. */
const data1 = getData(word1);
const data2 = getData(word2);
// Loop through the array of letters from data1, which
// came from processing the first string.
for (let i = 0; i < data1.ltrs.length; i++) {
/* if the sorted arrays of letters from both strings
do not match, return false, or if the sorted arrays of
frequencies do not match, return false */
if(data1.ltrs[i] !== data2.ltrs[i]
|| data1.times[i] !== data2.times[i])
return false;
}
// Otherwise, return true, since the two strings
// are considered "close"
return true;
};
/* A helper function that is used above to process the
strings and return an object {ltrs: [array of letters],
times: [array of numbers representing frequencies that
letters occurred in the string]} */
function getData(word) {
/* Create an empty object. It will store letters from
a string as keys and the number of times they appear
in the string as values */
const map = {};
// Loop through the string
for (const char of word) {
// If the letter does not exist as a key in the
// map object, add it with a value of zero
if(!map[char]) map[char] = 0;
// Increase the value of the letter/key in the map
// object by 1. This is recording the number of times
// the letter occurs in the string
map[char]++;
}
/* Return an object with the key ltrs containing an
array of the string's letters. The key times contains
an array of the number of times the letters occurred.
Times is just an array of numbers (the frequencies).
The point here is that we ultimately want to test if
the letters from both strings are the same. We will
also test if the frequencies - separate from and not
associated with any letters because they can be freely
reassigned - are also the same. */
return {
// ltrs gets assigned the keys of the map object
// (which are letters), sorted
ltrs: Object.keys(map).sort(),
/* times gets the values of the map object
(frequencies), sorted. The callback is passed to
the sort function to sort numbers properly, because
sort tries to sort values after converting to
strings, which can cause errors when sorting numbers. */
times: Object.values(map).sort((a, b) => a - b)
}
}
/* Note - the letters and frequencies are both sorted so
that we can use a loop to compare whether they are the
same as letters and frequencies in the other string above.
Sorting makes it easy to loop through and check if the
values of arrays for both strings are the same at the
same index. */
Here is the solution without the comments:
var closeStrings = function(word1, word2) {
if (word1.length !== word2.length)
return false;
const data1 = getData(word1);
const data2 = getData(word2);
for (let i = 0; i < data1.ltrs.length; i++) {
if(data1.ltrs[i] !== data2.ltrs[i]
|| data1.times[i] !== data2.times[i])
return false;
}
return true;
};
function getData(word) {
const map = {};
for (const char of word) {
if(!map[char]) map[char] = 0;
map[char]++;
}
return {
ltrs: Object.keys(map).sort(),
times: Object.values(map).sort((a, b) => a - b)
}
}