Wykonujemy ćwiczenie sprawdzające, czy dany wyraz jest palindromem. Zrobimy to na klika sposobów, wykorzystując język Python.
Palindrom to taki wyraz, który zapisany od tyłu daje to samo słowo co od przodu. Najlepszym przykładem palindromu może być imię „anna” albo wyraz „kajak”.
Sprawdzenie, czy dany wyraz jest palindromem, napisanie takiej funkcji, to bardzo częste zadanie na różnych rozmowach kwalifikacyjnych bądź w ramach ćwiczenia na różnych stopniach uczenia się programowania.
Sposób 1 – odwrócenie
Napiszmy taką funkcję w Pythonie:
def is_palindrome(word):
return word == word[::-1]
print(is_palindrome("anna"))
#True
print(is_palindrome("kajak"))
#True
To najłatwiejszy sposób na zabranie się za to zadanie. Zwracamy warunek, że nasze słowo równa się naszemu słowu zapisanemu wspak. Oczywiście, można to opisać w sposób bardziej obszerny:
def is_palindrome(word):
if word == word[::-1]:
return True
else:
return False
print(is_palindrome("anna"))
#True
print(is_palindrome("kajak"))
#True
Albo i lekko mniej obszerny:
def is_palindrome(word):
if word == word[::-1]:
return True
return False
print(is_palindrome("anna"))
#True
print(is_palindrome("kajak"))
#True
W obu przypadkach sprawdzamy, czy wyraz to to samo co wyraz zapisany wspak i zwracamy prawdę a w przeciwnym wypadku fałsz. Aczkolwiek samo sprawdzenie czy wyraz i wyraz wspak to to samo już nam daje prawdę/fałsz i to zastosowaliśmy w pierwszym przykładzie. Taką funkcję można zapisać jako lambdę:
is_palindrome = lambda word: word == word[::-1]
print(is_palindrome("anna"))
#True
print(is_palindrome("kajak"))
#True
Przyjmuje słowo, zwraca wynik porównania słowa ze słowem wspak i jest albo prawda albo fałsz.
Dociekliwi mogą zauważyć, że przecież „anna” mogła zostać zapisana wielką literą i wtedy takie porównanie da nam fałsz (w każdym z prezentowanych przypadków) a przecież taki wyraz palindromem jest:
is_palindrome = lambda word: word == word[::-1]
print(is_palindrome("Anna"))
#False
print(is_palindrome("kajak"))
#True
W takim wypadku należy przed porównaniem opuścić nasz wyraz do małej litery. Niech „Anna” albo „ANNA” zostanie zamienione na „anna” i po problemie:
def is_palindrome(word):
return word.lower() == word[::-1].lower()
print(is_palindrome("anna"))
#True
print(is_palindrome("Anna"))
#True
Możemy to zapisać inaczej, żeby zbytnio się nie powtarzać:
def is_palindrome(word):
word = word.lower()
return word == word[::-1]
print(is_palindrome("anna"))
#True
print(is_palindrome("Anna"))
#True
Natomiast w funkcji lambda musielibyśmy wszystko zapisać w jednej linijce, lambda nie ma wcięcia na ciało funkcji więc musi być jednolinijkowcem:
is_palindrome = lambda word : word.lower() == word[::-1].lower()
print(is_palindrome("anna"))
#True
print(is_palindrome("Anna"))
#True
Sposób 2 – pętla for, reversed, enumerate
Możemy zastosować inne podejście. Mamy coś takiego jak pętla for oraz funkcja reversed:
for letter in reversed("Hello"):
print(letter)
Mamy coś takiego jak enumerate, które pozwala nam się dobrać do indeksu:
for idx, letter in enumerate(reversed("Hello")):
print(idx, letter)
#0 o
#1 l
#2 l
#3 e
#4 H
A zatem połączmy to:
def is_palindrome(word):
for idx, letter in enumerate(reversed(word)):
if letter != word[idx]:
return False
return True
print(is_palindrome("anna"))
#True
print(is_palindrome("hello"))
#False
Tutaj bierzemy wyraz, np. „anna”. Następnie iterujemy po indeksie i literze tego wyrazu zapisanego wspak. I sprawdzamy czy „anna” i „anna” wspak mają takie same litery pod tym samym indeksem. Jeżeli nie – fałsz. Jeżeli tak, pętla się kończy, zwracamy prawdę.
„anna” i „anna” wspak mają takie same litery pod tymi samymi indeksami. „hello” i „olleh” nie – już pod indeksem zerowym jeden wyraz ma 'h’ zaś drugi 'o’.
Może sposób trudniejszy i wymaga nieco intelektualnego wysiłku, ale zazwyczaj po to się te ćwiczenia wymyśla, aby się trochę pobawić a nie iść na łatwiznę i pozwalać komputerowi robić wszystko za nas.
Za nas to on może co najwyżej zmniejszyć litery do małych aby „Anna” zapisana wspak i normalnie miała dokładnie to samo pod każdym indeksem:
def is_palindrome(word):
word = word.lower()
for idx, letter in enumerate(reversed(word)):
if letter != word[idx]:
return False
return True
print(is_palindrome("anna"))
#True
print(is_palindrome("Anna"))
#True
print(is_palindrome("hello"))
#False
Sposób 3 – dwa wskaźniki
Kolejny sposób dla lubiących się bawić. Tutaj podam przykład, który wypisała mi sztuczna inteligencja poproszona o taką funkcję z metodą dwóch wskaźników, omówimy go sobie:
def is_palindrome(s):
# Convert the string to lowercase and remove non-alphanumeric characters
s = ''.join(char.lower() for char in s if char.isalnum())
# Initialize two pointers, one at the beginning and one at the end of the string
left, right = 0, len(s) - 1
# Iterate until the pointers meet
while left < right:
# If characters at the pointers don't match, return False
if s[left] != s[right]:
return False
# Move the pointers towards the center
left += 1
right -= 1
# If the loop completes without finding a mismatch, the string is a palindrome
return True
# Test the function
print(is_palindrome("A man, a plan, a canal, Panama")) # True
print(is_palindrome("race a car")) # False
print(is_palindrome("anna"))
#True
print(is_palindrome("Anna"))
#True
print(is_palindrome("hello"))
#False
Czyli tak, rzecz pierwsza – palindrom to może być nie tylko „anna” czy też „Anna” (której litery trzeba zmniejszyć do małej aby funkcja działała) ale także zdanie zawierające spacje, znaki i tak dalej.
Zatem bierzemy nasz napis i zapisujemy do niego (w pewien sposób filtrujemy) tylko te znaki, które są alfanumeryczne, w dodatku zapisujemy małą literą. Tak to wygląda:
msg = "A man, a plan, a canal, Panama"
print(''.join(char.lower() for char in msg if char.isalnum()))
#amanaplanacanalpanama
Następnie musimy utworzyć lewy i prawy wskaźnik. Wskaźnik lewy to indeks 0, wskaźnik prawy to długość (czyli funkcja len) ale -1 bo indeksuje się od zera. Proszę zobaczyć:
name = "Anna"
left = 0
right = len(name) -1
print(name[left]) #A
print(name[right]) #a
Dla ułatwienia napisałem pierwszą literę wielką, żebyśmy widzieli, co się mniej więcej dzieje. A teraz pomyślmy, jakby tu te wskaźniki przesunąć.
Aby lewy poszedł w prawo zwiększymy go o 1, zaś aby prawy poszedł w lewo – zmniejszymy go o 1. Tak to wygląda:
name = "Anna"
left = 0
right = len(name) -1
print(name[left]) #A
print(name[right]) #a
left += 1
print(name[left]) #n
right -= 1
print(name[right]) #n
Teraz potrzebna nam pętla while, która sprawdza, czy wskaźniki się nie minęły,
def is_palindrome(s):
s = ''.join(char.lower() for char in s if char.isalnum())
left, right = 0, len(s) - 1
while left < right:
if s[left] != s[right]:
return False
left += 1
right -= 1
return True
print(is_palindrome("A man, a plan, a canal, Panama")) # True
print(is_palindrome("race a car")) # False
print(is_palindrome("anna"))
#True
print(is_palindrome("Anna"))
#True
print(is_palindrome("hello"))
#False
Tak długo, jak lewy jest mniejszy od prawego mamy do czynienia z sytuacją, w której lewy idzie do środka i prawy idzie do środka. W przypadku „anna” najpierw lewy to 0 a prawy to 3, porównanie „a” do „a”. Potem lewy to 1 a prawy to 2, porównanie „n” do „n”. Potem lewy to 2 a prawy to 1, a to oznacza, że pora wychodzić z pętli bo już nic do porównania nam nie zostało.
I to się dzieje. Jeżeli tylko wszystkie litery z lewej i prawej były takie same nie otrzymaliśmy return False, a zatem pora zwrócić True czyli prawdę.
Tak ta funkcja działa. Częste zadanie na myślenie w sposób algorytmiczny, rozwiązywanie problemów. Oczywiście – sposób pierwszy jest jak najbardziej na miejscu, pokazuje naszą znajomość Pythona i umiejętność wykorzystania tej wiedzy do pisania łatwego i czytelnego kodu.
W tym przykładzie w zasadzie każdy sposób jest dobry a nawet można by pokusić się o stworzenie kilku nowych. Każdy sposób obrazuje naszą umiejętność rozwiązywania zadania przy użyciu wiedzy z Pythona i własnego, logicznego myślenia.
Zadanie – sposób z wykorzystaniem listy
Zróbmy sobie zatem takie zadanie, w którym ten konkretny problem mamy rozwiązać za pomocą listy. Nie używamy dwóch wskaźników, w każdym razie nie w taki sposób, nie używamy reversed ani innych sztuczek, takich jak [::-1] które listę też tam odwrócą, chcemy, aby rozwiązać to zadanie w sposób tak „listowy” jak to tylko możliwe.
Rzecz pierwsza, zamiana „anna” na listę:
name = "anna"
lst = list(name)
print(lst)
#['a', 'n', 'n', 'a']
Inny sposób, bardziej nam przydatny:
name = "anna"
lst = [letter for letter in name]
print(lst)
#['a', 'n', 'n', 'a']
Rzecz druga, zamiana wielkich liter na małe:
name = "Anna"
lst = [letter.lower() for letter in name]
print(lst)
#['a', 'n', 'n', 'a']
Rzecz trzecia, pozbycie się zbędnych spacji i innych znaków niealfanumerycznych:
name = "Anna anna"
lst = [letter.lower() for letter in name if letter.isalnum()]
print(lst)
#['a', 'n', 'n', 'a', 'a', 'n', 'n', 'a']
Rzecz piąta, jak z listy „zrzucić” ostatnią literę:
name = "Anna"
lst = [letter.lower() for letter in name if letter.isalnum()]
print(lst)
#['a', 'n', 'n', 'a']
right = lst.pop()
print(right) #a
Rzecz szósta, jak zrzucić pierwszy element:
name = "Anna"
lst = [letter.lower() for letter in name if letter.isalnum()]
print(lst)
#['a', 'n', 'n', 'a']
right = lst.pop()
print(right) #a
left = lst.pop(0)
print(left) #a
print(lst) #['n', 'n']
Rzecz nr 7, jak wypisać w pętli „a” „a” oraz „n” „n” z anny?
name = "Anna"
lst = [letter.lower() for letter in name if letter.isalnum()]
print(lst)
#['a', 'n', 'n', 'a']
while len(lst) > 0:
print(lst.pop())
print(lst.pop(0))
Rzecz nr 8, co zrobić z kajakiem? W przypadku wyrazu „kajak” interesuje nas „k” „k” oraz „a” „a”, „j” już nie ma z czym porównywać. A zatem ma działać i na annie („a” „a”, „n” „n”) i na kajaku („k” „k”, „a” „a”).
name = "anna"
lst = [letter.lower() for letter in name if letter.isalnum()]
print(lst)
#['a', 'n', 'n', 'a']
while len(lst) > 1:
print(lst.pop())
print(lst.pop(0))
name = "kajak"
lst = [letter.lower() for letter in name if letter.isalnum()]
print(lst)
#['k', 'a', 'j', 'a', 'k']
while len(lst) > 1:
print(lst.pop())
print(lst.pop(0))
Teraz zbieramy całość „do kupy”:
def is_palindrome(s):
lst = [letter.lower() for letter in s if letter.isalnum()]
while len(lst) > 1:
left = lst.pop(0)
right = lst.pop()
if left != right:
return False
return True
print(is_palindrome("anna")) #True
print(is_palindrome("kajak")) #True
print(is_palindrome("hello")) #False
Czyli nasz s (od string, napis) zamieniamy na listę znaków tylko alfanumerycznych zapisanych do małej litery, bez spacji i znaków specjalnych.
Następnie tak długo jak długość listy jest większa niż 1 (czyli lista nie jest pusta, jak po zrzuceniu „a” „a” oraz „n” „n” z „anna” ani nie ma jednego elementu np. „j” jak kajak po zrzuceniu „k” „k” oraz „a” „a”) bierzemy sobie zmienną left, do której pakujemy zrzucony element z lewej strony listy oraz zmienną right, do której pakujemy zrzucony element z prawej strony listy.
Jeżeli te elementy nie są takie same to pora zwrócić False.
Jeżeli pętla nam się skończyła (lista jest albo pusta, albo jednoelementowa) to pora zwracać True, prawdę, bo mamy do czynienia z palindromem.
Jestem pewien, że jak człowiek pomyśli, to znajdzie jeszcze kolejne sposoby na rozwiązanie tego problemu. Natomiast wydaje mi się, że tyle wystarczy.