Skip to content

Commit

Permalink
Anagram approaches (#2706)
Browse files Browse the repository at this point in the history
* feat: anagram approaches
* Trim snippets to 8 lines
  • Loading branch information
sudomateo authored Oct 7, 2023
1 parent 6ebc4fb commit aba4cc8
Show file tree
Hide file tree
Showing 6 changed files with 315 additions and 0 deletions.
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
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, "")
}
22 changes: 22 additions & 0 deletions exercises/practice/anagram/.approaches/config.json
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"]
}
]
}
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
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
}
119 changes: 119 additions & 0 deletions exercises/practice/anagram/.approaches/introduction.md
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

0 comments on commit aba4cc8

Please sign in to comment.