-
-
Notifications
You must be signed in to change notification settings - Fork 654
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
* feat: anagram approaches * Trim snippets to 8 lines
- Loading branch information
Showing
6 changed files
with
315 additions
and
0 deletions.
There are no files selected for viewing
68 changes: 68 additions & 0 deletions
68
exercises/practice/anagram/.approaches/case-insensitive-sorting/content.md
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,68 @@ | ||
# Case-insensitive Sorting | ||
|
||
```go | ||
// Package anagram contains a solution to the anagram Exercism exercise. | ||
package anagram | ||
|
||
import ( | ||
"sort" | ||
"strings" | ||
) | ||
|
||
// Detect determines which words in candidates are anagrams of the subject. | ||
func Detect(subject string, candidates []string) []string { | ||
anagrams := make([]string, 0) | ||
|
||
subject = strings.ToLower(subject) | ||
|
||
for _, candidate := range candidates { | ||
c := strings.ToLower(candidate) | ||
|
||
if isAnagram(subject, c) { | ||
anagrams = append(anagrams, candidate) | ||
} | ||
} | ||
|
||
return anagrams | ||
} | ||
|
||
// isAnagram determines whether a and b are anagrams of each other. | ||
func isAnagram(a, b string) bool { | ||
return a != b && sortString(a) == sortString(b) | ||
} | ||
|
||
// sortString sorts a string lexicographically in non-decreasing order. | ||
func sortString(s string) string { | ||
chars := strings.Split(s, "") | ||
sort.Strings(chars) | ||
return strings.Join(chars, "") | ||
} | ||
``` | ||
|
||
This approach normalizes both strings to lowercase, sorts them, and compares the | ||
resulting sorted strings to determine if they are anagrams. | ||
|
||
The [`strings.ToLower` function][strings.ToLower] is used to convert the strings | ||
into their lowercase form. This normalizes the strings so that the solution is | ||
case-insensitive (e.g., `foo` and `OOF` normalize to `foo` and `oof`). At this | ||
point if the two strings normalize to the same string, they cannot be anagrams | ||
since a word cannot be an anagram of itself. | ||
|
||
The normalized strings are then sorted using the [`sort` package][sort]. It | ||
doesn't matter if the strings are sorted in non-decreasing or non-increasing | ||
order as long as both strings are sorted in the same way. Sorting the strings | ||
allows us to determine if the strings are anagrams (e.g., `foo` and `oof` sort | ||
to `foo` and `foo`). In Go, sorting operates on slices, not strings, so the | ||
string is first split into a slice using `strings.Split`, sorted using | ||
`sort.Strings`, and then joined back into a string using `strings.Join`. | ||
|
||
Now that the strings are normalized and sorted, they can be compared directly to | ||
determine if they are anagrams. If the strings are the same, they are anagrams. | ||
Otherwise, they are not anagrams. | ||
|
||
This approach separates the sorting and anagram checking logic into their own | ||
functions for readability. That way, the `Detect` function can focus on | ||
iterating over the candidates and building the resulting slice of anagrams. | ||
|
||
[strings.ToLower]: https://pkg.go.dev/strings#ToLower | ||
[sort]: https://pkg.go.dev/sort |
7 changes: 7 additions & 0 deletions
7
exercises/practice/anagram/.approaches/case-insensitive-sorting/snippet.txt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,7 @@ | ||
func isAnagram(a, b string) bool { return a != b && sortString(a) == sortString(b) } | ||
|
||
func sortString(s string) string { | ||
chars := strings.Split(s, "") | ||
sort.Strings(chars) | ||
return strings.Join(chars, "") | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,22 @@ | ||
{ | ||
"introduction": { | ||
"authors": ["sudomateo"], | ||
"contributors": [] | ||
}, | ||
"approaches": [ | ||
{ | ||
"uuid": "13358d8a-2229-4d5e-a266-4cf64f241798", | ||
"slug": "case-insensitive-sorting", | ||
"title": "Case-insensitive Sorting", | ||
"blurb": "Use case-insensitive sorting to compare the strings", | ||
"authors": ["sudomateo"] | ||
}, | ||
{ | ||
"uuid": "2d1a176c-2251-4565-8b9e-fde486a9e11c", | ||
"slug": "frequency-counter", | ||
"title": "Frequency Counter", | ||
"blurb": "Use character counters to compare the strings", | ||
"authors": ["sudomateo"] | ||
} | ||
] | ||
} |
91 changes: 91 additions & 0 deletions
91
exercises/practice/anagram/.approaches/frequency-counter/content.md
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,91 @@ | ||
# Frequency Counter | ||
|
||
```go | ||
// Package anagram contains a solution to the anagram Exercism exercise. | ||
package anagram | ||
|
||
import ( | ||
"strings" | ||
"unicode" | ||
) | ||
|
||
// Detect determines which words in cases are anagrams of the subject, | ||
// ignoring words that are equal to subject (case-insensitive). | ||
func Detect(subject string, cases []string) []string { | ||
anagrams := make([]string, 0) | ||
|
||
for _, word := range cases { | ||
if isAnagram(subject, word) { | ||
anagrams = append(anagrams, word) | ||
} | ||
} | ||
|
||
return anagrams | ||
} | ||
|
||
// isAnagram determines whether a and b are anagrams of each other. | ||
func isAnagram(a, b string) bool { | ||
if strings.ToLower(a) == strings.ToLower(b) { | ||
return false | ||
} | ||
|
||
freqCounter := make(map[rune]int) | ||
|
||
for _, r := range a { | ||
r = unicode.ToLower(r) | ||
freqCounter[r]++ | ||
} | ||
|
||
for _, r := range b { | ||
r = unicode.ToLower(r) | ||
|
||
if _, ok := freqCounter[r]; !ok { | ||
return false | ||
} | ||
|
||
freqCounter[r]-- | ||
|
||
if freqCounter[r] == 0 { | ||
delete(freqCounter, r) | ||
} | ||
} | ||
|
||
return len(freqCounter) == 0 | ||
} | ||
``` | ||
|
||
This approach utilizes a frequency counter to determine if the strings are | ||
anagrams of one another. | ||
|
||
The [`strings.ToLower` function][strings.ToLower] is used to convert the strings | ||
into their lowercase form where they are then checked for equality. Two strings | ||
that are equal after converting to lowercase are not anagrams since they are the | ||
same string. | ||
|
||
A hash map of type `map[rune]int` is initialized as the frequency counter. The | ||
first string is iterated over and the frequency counter is updated to hold the | ||
number of occurances of each character in the string. Before a character is | ||
counted in the frequency counter, it's converted to lowercase to account for | ||
case-insensitive strings that should be anagrams (e.g., `foo` and `OOF`). | ||
|
||
The second string is then iterated over and the frequency counter is checked to | ||
see if the lowercase version of the character exists in the hash map. If it does | ||
not then the strings cannot possibly be anagrams of one another and the | ||
implementation returns early. Otherwise, the number of occurances of the current | ||
character is decremented and, if the count of that character reaches 0, the | ||
entry is removed from the hash map. | ||
|
||
After iterating through both strings and updating the frequency counter the two | ||
strings are anagrams if and only if the frequency counter is empty. That is, all | ||
of the characters that were counted in the first string have been accounted for | ||
when iterating through the second string. If the second string contained a | ||
charater that the first string did not, then the implementation would return | ||
early as described above. If the second string contained extra characters or | ||
different characters than the first string then we'd have a non-empty hash map | ||
left over at the end signifying a difference between the two strings. | ||
|
||
This approach separates the anagram checking logic into its own function for | ||
readability. That way, the `Detect` function can focus on iterating over the | ||
candidates and building the resulting slice of anagrams. | ||
|
||
[strings.ToLower]: https://pkg.go.dev/strings#ToLower |
8 changes: 8 additions & 0 deletions
8
exercises/practice/anagram/.approaches/frequency-counter/snippet.txt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,8 @@ | ||
func isAnagram(a, b string) bool { | ||
for _, r := range b { | ||
if _, ok := freqCounter[unicode.ToLower(r)]; !ok { return false } | ||
freqCounter[unicode.ToLower(r)]-- | ||
if freqCounter[unicode.ToLower(r)] == 0 { delete(freqCounter, unicode.ToLower(r)) } | ||
} | ||
return len(freqCounter) == 0 | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,119 @@ | ||
# Introduction | ||
|
||
There are several ways to solve Anagram. One approach is to convert both strings | ||
to lowercase, sort them, and compare them. Another approach is to build a | ||
frequency counter of the number of characters in each string and then ensure | ||
both frequency counters have the same keys and values. | ||
|
||
## General guidance | ||
|
||
Consider the following when choosing an approach. | ||
|
||
- Can you use additional memory? | ||
- Yes. The frequency counter approach has a faster running time at the expense | ||
of using additional memory. | ||
- No. The case-insensitive sorting approach has a slower running time but it | ||
uses no additional memory. | ||
|
||
## Approach: Case-insensitive Sorting | ||
|
||
```go | ||
// Package anagram contains a solution to the anagram Exercism exercise. | ||
package anagram | ||
|
||
import ( | ||
"sort" | ||
"strings" | ||
) | ||
|
||
// Detect determines which words in cases are anagrams of the subject. | ||
func Detect(subject string, candidates []string) []string { | ||
anagrams := make([]string, 0) | ||
|
||
subject = strings.ToLower(subject) | ||
|
||
for _, candidate := range candidates { | ||
c := strings.ToLower(candidate) | ||
|
||
if isAnagram(subject, c) { | ||
anagrams = append(anagrams, candidate) | ||
} | ||
} | ||
|
||
return anagrams | ||
} | ||
|
||
// isAnagram determines whether a and b are anagrams of each other. | ||
func isAnagram(a, b string) bool { | ||
return a != b && sortString(a) == sortString(b) | ||
} | ||
|
||
// sortString sorts a string lexicographically in non-decreasing order. | ||
func sortString(s string) string { | ||
chars := strings.Split(s, "") | ||
sort.Strings(chars) | ||
return strings.Join(chars, "") | ||
} | ||
``` | ||
|
||
For more information, check the [case-insensitive sorting approach][approach-case-insensitive-sorting]. | ||
|
||
## Approach: Frequency Counter | ||
|
||
```go | ||
// Package anagram contains a solution to the anagram Exercism exercise. | ||
package anagram | ||
|
||
import ( | ||
"strings" | ||
) | ||
|
||
// Detect determines which words in cases are anagrams of the subject. | ||
func Detect(subject string, cases []string) []string { | ||
anagrams := make([]string, 0) | ||
|
||
for _, word := range cases { | ||
if isAnagram(subject, word) { | ||
anagrams = append(anagrams, word) | ||
} | ||
} | ||
|
||
return anagrams | ||
} | ||
|
||
// isAnagram determines whether a and b are anagrams of each other. | ||
func isAnagram(a, b string) bool { | ||
a = strings.ToLower(a) | ||
b = strings.ToLower(b) | ||
|
||
if a == b { | ||
return false | ||
} | ||
|
||
freqCounter := make(map[rune]int) | ||
|
||
for _, r := range a { | ||
freqCounter[r]++ | ||
} | ||
|
||
for _, r := range b { | ||
if _, ok := freqCounter[r]; !ok { | ||
return false | ||
} | ||
|
||
freqCounter[r]-- | ||
|
||
if freqCounter[r] == 0 { | ||
delete(freqCounter, r) | ||
} | ||
} | ||
|
||
return len(freqCounter) == 0 | ||
} | ||
|
||
``` | ||
|
||
For more information, check the [frequency counter approach][approach-frequency-counter]. | ||
|
||
[approach-case-insensitive-sorting]: https://exercism.org/tracks/go/exercises/anagram/approaches/case-insensitive-sorting | ||
[approach-frequency-counter]: https://exercism.org/tracks/go/exercises/anagram/approaches/frequency-counter |