W tym artykule sprawdzamy, czy wyrazy są anagramami. Wykorzystamy dwa podejścia, z których jedno obejmuje konwersję do listy i sortowanie oraz porównywanie, drugie zaś to popularne w różnych zadaniach algorytmicznych podejście „frequency counter”.

Wyraz jest anagramem innego wyrazu, jeżeli składa się z tych samych liter w takiej samej liczbie, ale różnej kolejności.

Wyrazy „kara” i „arak” są przykładami anagramów, ponieważ są utworzone z tych samych liter i w takiej samej ilości – np. litera a występuje 2 razy, pozostałe litery 1 raz.

Podejście pierwsze – sortowanie

Napisy można zamieniać na listy. Listy można sortować.

word1 = "arak"
word2 = "kara"

def is_anagram(str1, str2):
    arr1 = sorted(list(str1))
    arr2 = sorted(list(str2))
    print(arr1, arr2)

is_anagram(word1, word2)
#['a', 'a', 'k', 'r'] ['a', 'a', 'k', 'r']

Jak widać, te napisy zamienione na listy i posortowane wyglądają identycznie. Co jeszcze można w języku Python robić?

Zamieniać listy na napisy, napisy zaś porównywać ze sobą za jednym razem (zamiast stosować pętlę – listy są mutowalne, napisy nie i można je porównywać).

word1 = "arak"
word2 = "kara"

def is_anagram(str1, str2):
    first = "".join(sorted(list(str1)))
    second = "".join(sorted(list(str2)))
    print(first, second)

is_anagram(word1, word2)
#aakr aakr

Posortowaliśmy napisy, jesteśmy blisko celu. Teraz jedyne, co trzeba zrobić, to te napisy porównać:

word1 = "arak"
word2 = "kara"


def is_anagram(str1, str2):
    first = "".join(sorted(list(str1)))
    second = "".join(sorted(list(str2)))
    return first == second

print(is_anagram(word1, word2))
#True

Możemy jeszcze naszą funkcję udoskonalić tak, aby wielkość liter nie miała znaczenia:

word1 = "Arak"
word2 = "kaRA"


def is_anagram(str1, str2):
    first = "".join(sorted(list(str1.lower())))
    second = "".join(sorted(list(str2.lower())))
    return first == second

print(is_anagram(word1, word2))
#True

Mogłoby nas kusić, aby lower() zastosować dopiero w returnie, ale nie – musimy to zrobić przed posortowaniem. Sortowanie inaczej bowiem traktuje małe i wielkie litery.

Podejście drugie – frequency counter

Rzućmy okiem na tę funkcję, która będzie naszą funkcją pomocniczą:

def count_elements(arr):
    element_count = {}
    for element in arr:
        if element in element_count:
            element_count[element] += 1
        else:
            element_count[element] = 1
    return element_count


arr = [1, 2, 3, 4, 1, 2, 1]
print(count_elements(arr))  
# {1: 3, 2: 2, 3: 1, 4: 1}

Prawie dobrze, działa na tablicach liczb, zwraca element w kluczu i ilość wystąpień w wartości.

U nas będzie ona działać na napisach, ponadto musi traktować małe litery jak wielkie:

word1 = "Arak"
word2 = "kaRA"


def count_elements(word):
    element_count = {}
    for element in word.lower():
        if element in element_count:
            element_count[element] += 1
        else:
            element_count[element] = 1
    return element_count


print(count_elements(word1))
print(count_elements(word2))
# {'a': 2, 'r': 1, 'k': 1}
# {'k': 1, 'a': 2, 'r': 1}

Teraz, gdy wiemy jak już ta nasza funkcja count_elements działa najwyższy czas napisać funkcję główną, która będzie sprawdzać, czy wyrazy są anagramami.

def is_anagram(word1, word2):
    counter1 = count_elements(word1)
    counter2 = count_elements(word2)
    for key in counter1.keys():
        if key not in counter2.keys():
            return False
        if counter1[key] != counter2[key]:
            return False
    return True

Pakujemy do słowników litery i ich ilość wystąpień w obu wyrazach. Przechodzimy po kluczach z licznika pierwszego w pętli for.

Jeżeli w słowniku drugim nie ma takiego samego klucza, to oznacza, że litera z wyrazu pierwszego w drugim wyrazie w ogóle nie występuje – warunek oblany.

Jeżeli litera występuje w obu wyrazach, warto sprawdzić, czy występuje taką samą ilość razy – jeżeli nie, warunek oblany.

Potem zwracamy True. Zobaczmy, czy nasza funkcja działa:

word1 = "Arak"
word2 = "kaRA"


def count_elements(word):
    element_count = {}
    for element in word.lower():
        if element in element_count:
            element_count[element] += 1
        else:
            element_count[element] = 1
    return element_count


def is_anagram(word1, word2):
    counter1 = count_elements(word1)
    counter2 = count_elements(word2)
    for key in counter1.keys():
        if key not in counter2.keys():
            return False
        if counter1[key] != counter2[key]:
            return False
    return True

print(is_anagram(word1, word2))
#True

To podejście do rozwiązywania problemów programiści nazywają „frequency counter” i często występuje w najróżniejszych zadaniach algorytmicznych, dlatego warto znać.

Natomiast oba podejścia są zasadniczo dobre i pokazują naszą znajomość języka Python.

Wyzwanie – czy ta funkcja działa poprawnie?

Zobaczmy na tę funkcję. Wyzwanie polega na sprawdzeniu, czy rzeczywiście działa ona poprawnie i sprawdza, czy wyrazy są anagramami. Jeżeli nie – na wskazaniu gdzie leży problem i jak go rozwiązać.

w1 = "arak"
w2 = "kara"


def sus_anagram_check(word1, word2):
    if len(word1) != len(word2):
        return False
    return len(set(word1)) == len(set(word2))

print(sus_anagram_check(w1, w2))
#True

Wydaje się działać. Co robi ta funkcja? Sprawdza dwie rzeczy:

  • Sprawdza, czy wyrazy są jednakowej długości. Jeżeli nie – fałsz
  • Sprawdza, czy unikalne wartości występują odpowiednią ilość razy

Czy ta funkcja sprawdza, czy wyrazy są anagramami?

Nie.

w1 = "arak"
w2 = "okki"


def sus_anagram_check(word1, word2):
    if len(word1) != len(word2):
        return False
    return len(set(word1)) == len(set(word2))

print(sus_anagram_check(w1, w2))
#True

Tutaj mamy dwa wyrazy. Oba tej samej długości, oba mają taką samą liczbę unikalnych elementów.

Czy poniższa funkcja jest rozwiązaniem problemu?

def sus_anagram_check(word1, word2):
    if len(word1) != len(word2):
        return False
    return set(word1) == set(word2)

print(sus_anagram_check("arak", "okki"))
#False
print(sus_anagram_check("arak", "kara"))
#True

Należy się zastanowić, co ta funkcja robi. Po pierwsze, sprawdza, czy długość wyrazów jest taka sama. Po drugie, sprawdza, czy wyrazy zawierają takie same unikalne elementy.

Niestety ta funkcja nie jest dobra – nie sprawdza, czy te same elementy występują w takiej samej ilości w obu napisach:

def sus_anagram_check(word1, word2):
    if len(word1) != len(word2):
        return False
    return set(word1) == set(word2)

print(sus_anagram_check("arak", "karr"))
#True

Mamy tutaj dwa wyrazy jednakowej długości – „arak” i „karr”. Oba wyrazy mają jednakowe unikalne elementy – „a, k oraz r”. Nie są jednak anagramami – występują w innej ilości.

Czy da się tę funkcję naprawić?

Teoretycznie tak, w praktyce byłoby to użycie sposobu z sortowaniem. Naprawienie problemu wyeliminowałoby potrzebę użycia zbioru (set) w pierwszym momencie. Mielibyśmy w zasadzie podejście numer 1 z mniej czytelnym kodem.

Nie wszystko, co wydaje się pomysłem na świetne i podstępne rozwiązanie, musi działać.

Właśnie dlatego testujemy nasz kod i dobieramy funkcje, logikę oraz struktury danych według potrzeb.