Задача «Фруктовый салат» на Python: решение и разбор ошибок

Половинки фруктов на разделочной доске рядом с ноутбуком Программирование и разработка

Фруктовый салат на Python решается функцией из шести строк: каждое название фрукта режем пополам через len(fruit) // 2, все кусочки складываем в один список, сортируем через sorted() и склеиваем "".join(). Для ["apple", "pear", "grapes"] функция возвращает apargrapepesple.

Весь код запускался на Python 3.14.3, вывод программ приведён как есть.

Условие задачи

Задача с англоязычного тренажёра, в оригинале Fruit Salad. Нужно написать функцию fruit_salad(), которая получает список названий фруктов, разрезает каждое слово пополам, сортирует получившиеся кусочки по алфавиту и возвращает их одной строкой.

Если в названии нечётное число букв, правая половина должна быть длиннее левой: apple режется на ap и ple. Все названия даны строчными латинскими буквами.

Вызов Ожидаемый результат
fruit_salad(["apple", "pear", "grapes"]) "apargrapepesple"
fruit_salad(["raspberries", "mango"]) "erriesmangoraspb"
fruit_salad(["banana"]) "anaban"

Второй пример самый полезный: в raspberries 11 букв, и на нём ломается решение, где середину слова ищут через round().

Решение на Python

def fruit_salad(fruits):
    chunks = []
    for fruit in fruits:
        middle = len(fruit) // 2      # для нечётной длины правая часть длиннее
        chunks.append(fruit[:middle])
        chunks.append(fruit[middle:])
    return "".join(sorted(chunks))


print(fruit_salad(["apple", "pear", "grapes"]))
print(fruit_salad(["raspberries", "mango"]))
print(fruit_salad(["banana"]))

Вывод:

apargrapepesple
erriesmangoraspb
anaban

Оператор // делит нацело и отбрасывает дробную часть вниз. Для apple это 5 // 2 = 2, срез fruit[:2] даёт ap, а fruit[2:] забирает всё остальное, то есть ple. Условие про длинную правую половину выполняется само, без отдельной проверки на чётность.

sorted() не трогает исходный список и возвращает новый, уже упорядоченный. Метод "".join() склеивает его элементы через пустую строку.

Как получаются кусочки: разбор по шагам

Этот вариант печатает каждый кусочек фрукта и порядок, в котором он попадает в салат.

fruits = ["apple", "pear", "grapes"]
chunks = []
for fruit in fruits:
    middle = len(fruit) // 2
    print(f"{fruit:<7} длина {len(fruit)}, середина {middle}: {fruit[:middle]!r} + {fruit[middle:]!r}")
    chunks += [fruit[:middle], fruit[middle:]]

print(chunks)
print(sorted(chunks))
print("".join(sorted(chunks)))
apple   длина 5, середина 2: 'ap' + 'ple'
pear    длина 4, середина 2: 'pe' + 'ar'
grapes  длина 6, середина 3: 'gra' + 'pes'
['ap', 'ple', 'pe', 'ar', 'gra', 'pes']
['ap', 'ar', 'gra', 'pe', 'pes', 'ple']
apargrapepesple

Строки Python сравнивает посимвольно, по кодам Unicode, которые возвращает ord(). У ap и ar первая буква одинаковая, решает вторая: p раньше r. Кусочек pe стоит перед pes, потому что он короче, а всё, что в нём есть, совпадает с началом pes.

Кусочки яблока, груши и винограда выложены в ряд на деревянном столе

Другие варианты решения

Цикл выше удобнее читать новичку. Тот же салат собирается и короче, через генераторное выражение внутри sorted():

def fruit_salad(fruits):
    return "".join(sorted(
        part
        for fruit in fruits
        for part in (fruit[:len(fruit) // 2], fruit[len(fruit) // 2:])
    ))


print(fruit_salad(["apple", "pear", "grapes"]))
print(fruit_salad(["raspberries", "mango"]))
print(fruit_salad(["banana"]))
apargrapepesple
erriesmangoraspb
anaban

Результат тот же, но середина считается дважды, а два for в одной скобке новичок читает с трудом. На собеседовании или в учебном репозитории мы бы оставили цикл.

Читайте также:  Парсер выражений: как разобрать математическое выражение из строки

Третий вариант выносит нарезку в отдельный генератор. Его удобно проверять отдельно от сортировки:

def chunks(fruits):
    for fruit in fruits:
        middle = len(fruit) // 2
        yield fruit[:middle]
        yield fruit[middle:]


def fruit_salad(fruits):
    return "".join(sorted(chunks(fruits)))


print(list(chunks(["kiwi", "lime"])))
print(fruit_salad(["kiwi", "lime"]))
['ki', 'wi', 'li', 'me']
kilimewi

sorted() принимает любой итерируемый объект, поэтому генератор можно передать ему напрямую, без промежуточного списка.

Три ошибки, на которых решение не проходит тесты

Разработчик смотрит на ошибку в редакторе кода, рядом тарелка с нарезанными фруктами

Деление через / вместо //

Деление / двух целых чисел в Python 3 возвращает float, даже если они делятся нацело. Срез с дробным индексом не работает:

fruit = "apple"
middle = len(fruit) / 2
print(middle)
print(fruit[:middle])
2.5
Traceback (most recent call last):
  File "e1.py", line 4, in <module>
    print(fruit[:middle])
          ~~~~~^^^^^^^^^
TypeError: slice indices must be integers or None or have an __index__ method

Исправление одно: len(fruit) // 2. Обёртка int(len(fruit) / 2) тоже даст 2, но это лишний шаг.

round() вместо целочисленного деления

Эта ошибка хитрее, потому что первый пример проходит. Функция round() округляет половинки к ближайшему чётному числу: round(2.5) даёт 2, а round(5.5) даёт 6.

def salad_round(fruits):
    chunks = []
    for fruit in fruits:
        middle = round(len(fruit) / 2)    # ошибка: банковское округление
        chunks += [fruit[:middle], fruit[middle:]]
    return "".join(sorted(chunks))


print(salad_round(["apple", "pear", "grapes"]))
print(salad_round(["raspberries", "mango"]))
print(round(2.5), round(3.5), round(5.5))
apargrapepesple
mangoraspberries
2 4 6

В raspberries 11 букв, середина 5,5 округлилась до 6, и левая половина raspbe вышла длиннее правой rries. Кусочки после сортировки случайно сложились обратно в целые слова, а ожидался erriesmangoraspb.

Правило такое: половинку round() округляет к ближайшему чётному числу. Если половина длины слова дробная, а целая часть у неё нечётная, округление идёт вверх, и левая часть получается длиннее. Это слова из 3, 7, 11, 15 букв, так что короткие названия тоже под ударом:

for fruit in ["fig", "avocado", "apple", "raspberries"]:
    n = len(fruit)
    print(f"{fruit:<11} {n:>2}  round: {round(n / 2)}  //: {n // 2}")
fig          3  round: 2  //: 1
avocado      7  round: 4  //: 3
apple        5  round: 2  //: 2
raspberries 11  round: 6  //: 5

У fig функция round() отрезает слева fi вместо f, у avocado получается avoc вместо avo. Совпадения на apple случайны, поэтому середину слова мы бы считали только через //.

Крупный план: яблоко разрезано на две неравные части, правая больше

sort() вместо sorted()

Метод list.sort() сортирует список на месте и возвращает None. Если результат сразу передать в join(), программа упадёт:

chunks = ["ple", "ap", "pe", "ar"]
result = chunks.sort()
print(result)
print("".join(result))
None
Traceback (most recent call last):
  File "e2.py", line 4, in <module>
    print("".join(result))
          ~~~~~~~^^^^^^^^
TypeError: can only join an iterable

Со sort() работать можно, но в две строки: сначала chunks.sort(), потом "".join(chunks). Когда отсортированный список нужен один раз и сразу уходит в склейку, удобнее sorted().

Граничные случаи: пустой список, одна буква, регистр

По условию всё приходит строчными латинскими буквами. Настоящие данные бывают другими. Вызовы ниже идут к функции fruit_salad() из раздела «Решение на Python».

print(repr(fruit_salad([])))
print(repr(fruit_salad(["fig", "a"])))
print(fruit_salad(["banana", "Cherry"]))
print(ord("C"), ord("b"))
print(fruit_salad(["яблоко", "ёжевика", "груша"]))
print(ord("а"), ord("я"), ord("ё"))
''
'afig'
Cheanabanrry
67 98
викагрокоушаяблёже
1072 1103 1105

Пустой список даёт пустую строку, без ошибки. У однобуквенного a левая половина пустая: len("a") // 2 даёт середину 0, срез [:0] возвращает '', и в салат попадает только a.

Пустая белая тарелка и одна виноградина рядом на столе

Заглавные латинские буквы идут раньше строчных: код C равен 67, а b равен 98. Поэтому Che встал первым, хотя по алфавиту ana и ban должны идти раньше него. С русскими названиями своя ловушка: у ё код 1105, у я 1103, и кусочек ёже уехал в самый конец.

Читайте также:  Строки в PHP: кавычки, echo, функции и работа с UTF-8

Чинить это лучше ключом сортировки, а не переделкой строк: сами кусочки в ответе остаются как были.

def fruit_salad(fruits):
    chunks = []
    for fruit in fruits:
        middle = len(fruit) // 2
        chunks += [fruit[:middle], fruit[middle:]]
    return "".join(sorted(chunks, key=str.casefold))


print(fruit_salad(["Apple", "banana", "Cherry"]))
print(sorted(["Ap", "ple", "ban", "ana", "Che", "rry"]))
anaApbanCheplerry
['Ap', 'Che', 'ana', 'ban', 'ple', 'rry']

Для буквы ё ключ другой, замена на е только при сравнении:

chunks = ["вика", "гр", "око", "уша", "ябл", "ёже"]
print(sorted(chunks))
print(sorted(chunks, key=lambda s: s.replace("ё", "е")))
['вика', 'гр', 'око', 'уша', 'ябл', 'ёже']
['вика', 'гр', 'ёже', 'око', 'уша', 'ябл']

Сортировка в Python устойчивая: кусочки с одинаковым ключом сохраняют исходный порядок. В задаче с тренажёра все названия строчные латинские, там ключ ничего не изменит.

Как проверить решение перед отправкой

Примеры из условия проще прогнать у себя через assert, чем отправлять код наугад и гадать по сообщению о непройденном тесте.

def fruit_salad(fruits):
    chunks = []
    for fruit in fruits:
        middle = len(fruit) // 2
        chunks.append(fruit[:middle])
        chunks.append(fruit[middle:])
    return "".join(sorted(chunks))


cases = [
    (["apple", "pear", "grapes"], "apargrapepesple"),
    (["raspberries", "mango"], "erriesmangoraspb"),
    (["banana"], "anaban"),
    ([], ""),
]

for fruits, expected in cases:
    result = fruit_salad(fruits)
    assert result == expected, f"{fruits}: ждали {expected!r}, получили {result!r}"
print("Все", len(cases), "проверки прошли")
Все 4 проверки прошли

Если подставить версию с round(), второй assert остановит программу с сообщением AssertionError: ['raspberries', 'mango']: ждали 'erriesmangoraspb', получили 'mangoraspberries'.

Когда список фруктов нужно читать с клавиатуры, например в школьной системе проверки, названия вводятся одной строкой через пробел. Функция та же, меняется только вызов:

fruits = input().split()
print(fruit_salad(fruits))

На ввод apple pear grapes программа печатает:

apargrapepesple

Миска с фруктовым салатом из мелких кусочков рядом с клавиатурой

Сколько работает решение

Из n фруктов получается 2n кусочков. Нарезка проходит по списку один раз. Дольше всего идёт сортировка: алгоритм на сравнениях в среднем не может обойтись меньше чем примерно n·log n сравнений, а каждое сравнение двух строк идёт по символам до первого различия. В примерах из условия не больше трёх фруктов, так что скорость здесь роли не играет.

Читайте также:  Си или Си плюс плюс: чем отличаются C и C++ и что выбрать

Частые вопросы

Можно ли решить фруктовый салат без sorted()?

Можно отсортировать список методом chunks.sort(), а потом склеить. Свою сортировку пузырьком писать не нужно: пузырёк делает порядка n² сравнений, а встроенной сортировке хватает порядка n·log n.

Почему мой ответ не совпадает с ожидаемым?

Проверьте два места. Середина должна считаться через //: round() округляет половинки к чётному числу, и у слов из 3, 7, 11, 15 букв левая часть выходит длиннее правой. Сортировать нужно общий список всех половинок одним вызовом sorted(): если упорядочить кусочки каждого фрукта по отдельности, порядок в строке выйдет другим.

Работает ли решение с русскими названиями фруктов?

Работает: срезы и sorted() обрабатывают русские буквы так же, как латинские. Порядок при этом идёт по кодам символов, и буква ё окажется после я. Если нужен словарный порядок, передайте в sorted() ключ с заменой ё на е.

В какой версии Python работает этот код?

Весь код из статьи запускался на Python 3.14.3. Само решение обходится срезами, //, sorted() и join(), ничего из новых версий языка в нём нет. Строки вида f"..." в отладочных примерах появились в Python 3.6, на более старых версиях эти примеры не запустятся.

Оцените статью
bestprogrammer.ru
Добавить комментарий