Грокаем алгоритмы: Гайд по алгоритмам для тех, кому сложно решать задачи
Эта статья — для разработчиков, которые частично уже знают алгоритмы. Если вы еще не знакомы с ними, советуем пройти трек «Алгоритмы и структуры данных» в Хекслете. Вы изучите списки, стеки, очереди, структуры данных, которые помогут проектировать структуры и алгоритмы.
Бесплатные курсы по программированию в Хекслете
- Освойте азы современных языков программирования
- Изучите работу с Git и командной строкой
- Выберите себе профессию или улучшите навыки
Грокать алгоритмы или не грокать? Что делать, если вам не хочется решать сто задач к вашему следующему собеседованию?
Часть меня ненавидит технические собеседования, в первую очередь из-за того, что мне нужно повторять много материала. Кроме того, в процессе самого собеседования мне часто приходится предлагать какое-то особенное решение, а не то, которое я бы выбрал в своей будничной практике.
Несмотря на это, я люблю алгоритмы и люблю решать задачки по программированию. Для меня это веселое времяпровождение и хорошая тренировка для мозга. Учитывая все это, хочу рассказать вам о моей собственной технике подготовки к собеседованиям, которая намного интереснее и волнительнее, чем обычная подготовка.
Коротко расскажу о себе, чтобы вы убедились в моей экспертности. Я программирую уже 20 лет, за это время я много раз менял место работы. Всего я прошел около 30 воронок найма — больше 120 собеседований. Плюс к этому у меня есть опыт с той стороны баррикад: я провел около 300 технических собеседований и больше 200 собеседований по системному дизайну .
Где мы грокаем алгоритмы
Если вы когда-нибудь искали работу, то знаете, что есть ресурсы, которые собирают задачи с собеседований . Один из них — LeetCode, это самый популярный сайт, где очень много задач. Плюс вокруг него сложилось развитое сообщество, где можно обсуждать задачки с другими инженерами. Если выдается свободная минутка, я всегда не прочь провести ее на LeetCode. Там я решаю задачи или читаю чужие решения, после чего сравниваю со своими.
Самая большая проблема LeetCode в том, что сайту не хватает продуманной системы обучения. У него много разных задач, в которых легко потеряться. Сколько нужно таких задач, чтобы подготовиться к собеседованию? Я бы предпочел двигаться по продуманной программе, в конце которой я смогу ощутить уверенность в собственных знаниях. Но системы нет, а я ленивый, и вообще — не хочу решать 500+ задач.
Одно из популярных решений для этой проблемы — решать задачи, которые относятся к одной структуре данных (например, прорешать несколько задач с деревьями). Какая-то система обучения появляется, но это решение меня все равно не устраивает. Например, что делать, если задачу можно решить при помощи разных структур данных?
Какую систему обучения придумал я
Я бы предпочел такую систему, в которой задачи распределены по паттернам, а не по структурам данных. Мои любимые паттерны — скользящее окно, нахождение цикла и топологическая сортировка. Когда я научился пользоваться этими методами, я стал решать незнакомые задачи по аналогии с задачами, которые решал до этого. Благодаря этому весь процесс подготовки к собеседованиям стал более интересным и веселым. Ну и конечно же, более систематичным.
Я обнаружил 25 паттернов, которые лежат в основе решения большинства задач. Думаю, эти паттерны помогут кому угодно показывать на собеседованиях красивые и элегантные решения. Вся фишка этих паттернов в том, что понимая один из них, вы научитесь решать сразу несколько задач, десятки задач.
Самые распространенные паттерны для решения задач
- Метод скользящего окна
- Метод двух указателей
- Нахождение цикла
- Интервальное слияние
- Цикличная сортировка
- In-place Reversal для LinkedList
- Поиск в ширину
- Поиск в глубину
- Двоичная куча
- Подмножества
- Усовершенствованный бинарный поиск
- Побитовое исключающее ИЛИ
- Top K Elements
- K-образное слияние
- Задача о рюкзаке 0-1
- Задача о неограниченном рюкзаке
- Числа Фибоначчи
- Наибольшая последовательность-палиндром
- Наибольшая общая подстрока
- Топологическая сортировка
- Чтение префиксного дерева
- Задача: количество островов в матрице
- Метод проб и ошибок
- Система непересекающихся множеств
- Задача: найти уникальные маршруты
Читайте также: Это снова я, резиновая уточка: что такое метод Фейнмана и почему с его помощью так просто изучать программирование
Метод скользящего окна
Контекст: Мы используем этот метод, когда у нас есть входные данные с заданным размером окна.
Задачи для этого паттерна:
- Longest Substring with K Distinct Characters
- Fruits into Baskets
- Permutation in a String
Метод двух указателей
Контекст: Мы используем два указателя, чтобы перебрать все входные данные. Обычно два указателя движутся в противоположных направлениях с фиксированным интервалом.
Задачи для этого паттерна:
- Squaring a Sorted Array
- Dutch National Flag Problem
- Minimum Window Sort
Нахождение цикла
Контекст: Еще этот алгоритм называют алгоритмом черепахи и зайца. В отличие от предыдущего метода, два указателя тут движутся с разной скоростью.
Задачи для этого паттерна:
- Middle of the LinkedList
- Happy Number
- Cycle in a Circular Array
Интервальное слияние
Контекст: Этот метод применяют, если есть пересекающиеся интервалы. Например, на этом изображении мы видим, что интервалы a и b могут пересекаться шестью разными способами:
Задачи для этого паттерна:
- Intervals Intersection
- Conflicting Appointments
- Minimum Meeting Rooms
Цикличная сортировка
Контекст: Если входные данные лежат в заданном интервале, используйте цикличную сортировку.
Задачи для этого паттерна:
- Find all Missing Numbers
- Find all Duplicate Numbers
- Find the First K Missing Positive Numbers
In-place Reversal для LinkedList
Техника: Эта техника описывает эффективный способ перевернуть связи между узлами в LinkedList (класс Java). Часто мы ограничены in-place, то есть мы должны использовать исходные узлы.
Задачи для этого паттерна:
- Reverse every K-element Sub-list
- Rotate a LinkedList
Поиск в ширину
Контекст: Это метод для решения задач с деревьями.
Задачи для этого паттерна:
- Binary Tree Level Order Traversal
- Minimum Depth of a Binary Tree
- Connect Level Order Siblings
Поиск в глубину
Контекст: Тот же, что для предыдущего метода.
Задачи для этого паттерна:
- Path With Given Sequence
- Count Paths for a Sum
Двоичная куча
Контекст: Во многих задачах у нас есть набор элементов, который можно разделить на две части. Тогда мы могли бы выяснить, какой элемент является наименьшим в первой куче и какой является наибольшим во второй куче.
Задачи для этого паттерна:
- Find the Median of a Number Stream
- Next Interval
Подмножества
Контекст: Если задача требует перестановки или комбинаций элементов, используйте подмножества.
Задачи для этого паттерна:
- String Permutations by changing case
- Unique Generalized Abbreviations
Усовершенствованный бинарный поиск
Контекст: Эта техника использует логический оператор для наиболее эффективного поиска элементов.
Задачи для этого паттерна:
- Two Single Numbers
- Flip and Invert an Image
Наибольшее K элементов
Контекст: Эта техника используется, чтобы найти наибольший/наименьший или наиболее часто встречающийся набор k-элементов в коллекции.
Задачи для этого паттерна:
- ‘K’ Closest Points to the Origin
- Maximum Distinct Elements
Читайте также: Как решить задачу, если непонятно, с чего вообще начать: советы от Хекслета
K-образное слияние
Контекст: Используйте эту технику, если у вас есть список отсортированных массивов.
Задачи для этого паттерна:
- Kth Smallest Number in M Sorted Lists
- Kth Smallest Number in a Sorted Matrix
Рюкзак 0-1
Контекст: Этот паттерн используют для задач на оптимизацию. Используйте эту технику, чтобы выбирать элементы, которые дают максимум выгоды в данном наборе ограничений по вместимости. Учитывая то, что каждый элемент может быть выбран лишь единожды.
Задачи для этого паттерна:
- Equal Subset Sum Partition
- Minimum Subset Sum Difference
Неограниченный рюкзак
Контекст: То же самое, что в предыдущем паттерне, но только каждый элемент может быть выбран повторно сколько угодно раз.
Задачи для этого паттерна:
- Rod Cutting
- Coin Change
Числа Фибоначчи
Контекст: Как очевидно из названия, это паттерн для чисел Фибоначчи. Это последовательность, в которой каждое последующее число равно сумме двух предыдущих чисел.
Задачи для этого паттерна:
Наибольшая последовательность — палиндром
Контекст: Имеется в виду задача, которая может быть использована как для последовательности, так и для строк . По сути это задача на оптимизацию.
Задачи для этого паттерна:
- Longest Palindromic Subsequence
- Minimum Deletions in a String to make it a Palindrome
Наибольшая общая подстрока
Контекст: Как понятно из названия, это паттерн для работы со строками или другими последовательностями, а также для работы с наборами строк или последовательностей.
Задачи для этого паттерна:
- Maximum Sum Increasing Subsequence
- Edit Distance
Чтение префиксного дерева
Контекст: Это специфичная для структуры данных техника, с помощью которой читают или создают префиксное дерево.
Задачи для этого паттерна:
- Longest Word in Dictionary
- Palindrome Pairs
Острова в матрице
Контекст: Этот паттерн подходит для чтения любого двумерного массива, где нам нужно обнаружить связанные между собой элементы.
Задачи для этого паттерна:
- Number of Distinct Islands
- Maximum Area of Island
Путь проб и ошибок
Контекст: Этот паттерн подойдет для того, чтобы пройтись по массиву в поисках подходящего под требования элемента.
Задачи для этого паттерна:
- Find Kth Smallest Pair Distance
- Minimize Max Distance to Gas Station
Система непересекающихся множеств
Контекст: Если данные раскиданы по непересекающимся множествам, то они решаются одним и тем же способом.
Задачи для этого паттерна:
- Number of Provinces
- Bipartite Graph
Поиск уникального маршрута
Контекст: Этот паттерн подойдет для прохождения по любому многомерному массиву.
Задачи для этого паттерна:
- Find Unique Paths
- Dungeon Game
Бесплатные курсы по программированию в Хекслете
- Освойте азы современных языков программирования
- Изучите работу с Git и командной строкой
- Выберите себе профессию или улучшите навыки
Как Решать Задачи По Программированию
Наверное, у многих новичков бывало такое, когда ты смотришь решение какой-то задачи в Интернете или на уроке, читаешь чужой код и кажется что всё максимально просто и понятно. Но вот приступаю к решению такой же задачи самостоятельно, впадаешь в ступор. Что делать, с чего начать и т. д. Хотя казалась бы было всё было понятно. Знакомое чувство? С ним сталкивается каждый, кто только начинает изучать программирование. Поэтому сегодня я расскажу как решать зачади по программированию на языке Python, да и любом другом языке. Меня зовут Макс. Я один из авторов YouTube-канала PyLounge. Поехали!
Во-первых, ты должен чётко понимать, что от тебя требуется.
Как нельзя лучше сделать это тебе поможет декомпозиция. Декомпозиция — это когда ты разбиваешь одну большую сложную задачу на несколько мелких простых задача. При этом в момент дробления задачи не пытайся думать о программной реализации и программировании вообще. Старайся думать как ТЫ бы решал эту задачу, ты сам, будучи человеком. Опиши свои человеческие действия на каждом этапе. Перед этим вдумчиво прочитай текст задачи. Можно даже читать вслух. Часто проговаривание вслух помогает восприятию. И начинай декомпозировать с человеческой точки зрения.
Допустим перед нами стоит задача:
Написать функцию, которая получает из текстового файла словарь с настройками графики для игры.
Файл имеет следующий вид:
Начинаем смотреть на задачу глазами обычного человека. Вот у меня на Рабочем столе лежит текстовый файл. Чтобы мне прочитать содержимое файла его надо открыть. Соответственно это и есть первый пункт наше алгоритма — Открыть файл.
Далее я глазами вижу первую строчку файла. Здесь через пробел записаны два слова. Так как в файле записаны настройки графики то, очевидно, в каждой строчке хранится пара «название опции — её значение». Например, Разрешение 1920×1080. Соответственно я, будучи человеком, мысленно читаю первую строчку. Дальше мне нужно отделить «ключ настройки» от её значения. Как я будучи человеком понимаю, что в этой строчке два разных слова? Как я понимаю, что здесь не одно слово? Слова разделены между собой пробелами. Значит слово до пробела ключ, после значение. Это и есть следующий этап нашего алгоритма — читаем строку и делим её, за разделитель принимаем символ пробела. Затем первую половину строки заносим в ключ, вторую в значение.
Далее я будучи человеком, опускаю взгляд на следующую строку и повторяю описанное выше действие. Потом следующую и следующую. До тех пор, пока в файле не закончатся строки. Это и есть следующий шаг — по очереди обрабатывать каждую строчку из файла.
Теперь у меня в голове (или в переменной типа dict (словарь)) имеется вся нужная информация. По аналогии с функцией мы возвращаем словарь с данными. Потом я закрываю файл. Вот у нас и есть по сути готовый алгоритм:
1. Открыть файл.
2. Читаем строку из файла.
3. Делим её на две части через символ пробела.
4. Записываем первую часть строки в «ключ», а второю в «значение ключа» (например, ключ vsync — значение ключа off).
5. Аналогичным образом по очереди обрабатываем каждую следующую строку. Пока строчки в файл не кончатся.
6. Закрываем файл.
7. Сообщаем результат.
Можно для себя набросать псевдокод. То есть описание каждого шага алгоритма не на конкретно языке программирования, а абстрактно, на русском. Также неплохим вариантом может являться нарисовать блок-схему.
Псевдокод — это изложенные простым языком шаги алгоритма. Иными словами, это пошаговый план решения задачи. Если вы и так чётко понимаете процесс или задача относительно простая, то псевдокод и схему можно пропустить.
Теперь когда у нас имеется алгоритм, надо задать себе два вопроса:
1. Что мы имеем на вход (входящие данные).
2. Что мы имеем на выходе.
Снова смотри с позиции обычного человека. Чтобы я, будучи человеком, смог открыть какой-то файл и прочитать его, я должен знать где он лежит. Значит на вход мы и наша функция получает путь до файла.
Также в условии уже сказано, что мы должны сформировать словарь настроек. Значит словарь с ключами и значениями (куда сохраняются данные из файла) мы и должны вернуть из функции (вывести результат).
Теперь пытаемся переложить это на программный код. Также построчно всё проговаривая:
import os def read_settings_file(file_name): settings = dict() file = open(file_name, 'r') for row in file.readlines(): key, value = row.split(' ') settings[key] = value file.close() return settings print(read_settings_file('D://game_settings.txt')
Очень важно давать осмысленные и понятные названия переменным и функция. Это помогает делать код более читаемым и ты сам не запутаешься, если откроешь его спустя неделю. Рекомендую посмотреть наше видео «4 Совета Которые Помогут Сделать Твой Код Лучше».
Также стоит подумать, какие ошибки у нас могут возникнуть. Опять таки, задаём себе вопросы, например:
1. Что делать если файл не будет существовать?
Надо сделать проверку, добавить обработку такой ситуации.
2. Что случится если структура файла будет другой?
3. Кодировка файла будет нестандартной?
Нужно придумать как можно больше таких вопросов. И ответить на каждый из них, реализовав программное решение с помощью исключений, сообщений об ошибках и т.д.
Теперь когда у нас есть рабочее решение на руках, надо подумать, как его можно улучшить. То есть надо провести рефакторинг кода и сделать решение более эффективным.
Рефакторинг — это процесс улучшения кода, когда мы переписываем какие-то участки кода, которые можно сделать лучше.
При рефакторинге можно задавать себе следующие вопросы:
1. Можно ли получить этот результат как-то иначе? Какие еще подходы есть?
2. Понятно ли это решение с первого взгляда?
3. Можно ли использовать результат или метод для какой-то другой задачи?
4. Можно ли улучшить производительность решения?
5. Как эту задачу решают другие люди?
Никогда не пренебрегай внутренним диалогом с собой. Составляй план, блок-схему и т. д. Многие люди не делают этого, им кажется, что это бесполезная трата времени. Но на самом деле именно в этом и кроется ключевой момент в решении любой задачи.
В нашем случае мы могли бы работать с файлом через менеджер контекста, чтобы явно не вызывать функцию close () и метод readlines (). И применить к значением настроек функцию strip (), что бы не оставлять в строке символы переноса, табуляции и т. д. В случае с Python можно ещё и аннотации типов добавить:
from typing import Optional, Dict import os def read_settings_file(file_name:str) -> Optional[str]: if not os.path.exists(file_name): return None settings:Dict[str, str] = dict() with open(file_name, 'r') as file: for row in file: key, value = row.split(' ') settings[key] = value.strip() return settings print(read_settings_file('D://game_settings.txt')
К слову, очень важно смотреть, как другие решали подобную задачу. Так читая чужой код вы набираетесь опыта и вырабатываете надсмотренность. В чужом коде можно подсмотреть интересные решения, которые затем взять себе на вооружение.
Я в своё время очень часто смотрел чужие решения, сравнивал их со своим и это сильно мне помогло. Главное не смотреть ответ сразу) А именно сравнивать своё решение с чужим, если сам ну никак не можешь сделать.
Я привел пример простой задачи. Но более сложная задача будет отличаться лишь количеством этапов и инструментами, которые будет необходимо применить.
Запомните, умение решать задачи — это навык. Чем больше практики, тем лучше вам это будет удаваться. Если вы хотите много раз подтягиваться, то надо подтягиваться. Если вы хотите научиться быстро бегать, то надо бегать. Если вы хотите научиться решать задачи по программированию, то надо решать задачи. Истина всегда на поверхности.
Также есть чуть более подробная видеоверсия этой статьи на YouTube:
Как решить задачу, если непонятно, с чего начать: советы от Хекслета
Все сталкиваются с трудностями при изучении нового: особенно при решении задач, которые отличаются от всего, что мы знали до этого. Постепенно вы поймете, какие подходы нужны для решения различных задач, но пока этого не случилось, приходится просто двигаться на ощупь. В этой статье мы дадим несколько советов, которые могут помочь с прохождением сложных практических уроков на Хекслете.
С чего начать
Обычно самое сложное в решении задачи — сделать первый шаг. Особенно на старте обучения, когда опыта ещё нет, может показаться, что вы совершенно ничего не понимаете и не способны решить даже самое простое упражнение. Если вы читаете задание и у вас нет никаких идей, с чего же вообще начать, мы предлагаем начинать с самых маленьких шагов.
Задания связаны напрямую с теорией, в том числе с пройденной в более ранних курсах. Вы всегда можете вернуться к каким-то урокам, чтобы освежить нужную тему. И так в любых ситуациях — если вы понимаете, что не хватает каких-то знаний, просто вернитесь к предыдущим урокам. Очень сложно запомнить всю информацию, поэтому возврат к уже пройденному материалу — это нормально.
Главный принцип решения задач — дробить задачу на подзадачи. Не пытайтесь сразу решить большое и сложное упражнение с первого подхода, делайте это постепенно. Если в задании сказано написать и экспортировать функцию, напишите пустую функцию и экспортируйте её. Ваш код в большинстве случаев уже будет работать, хоть и не проходить наших тестов. Проверьте примеры вызова функции, которую вам надо написать, посмотрите, что в неё передаётся. И так шаг за шагом продвигайтесь к реализации полного и нужного вам решения.
Ищем ошибки в коде
Важная часть работы над задачей — это поиск ошибок, который начинается с логов. Это то, что выводит программа во время своего выполнения. Например, когда вы запускаете проверку вашего решения во вкладке OUTPUT отображается вывод работы тестов — это и есть логи. Вы можете добавлять к этому выводу любой собственный вывод, используя инструменты своего языка программирования (например в JS — console.log() ).
Залогируйте входящие данные — это поможет понять, с чем придётся работать внутри функции. Как это сделать, мы рассказываем в наших справочных материалах. Для удобства вывода логов вы можете добавлять свои метки, чтобы видеть, какая именно переменная выводится. Например, так может выглядеть логирование в JS:
export default (first, second) => console.log('--------------------- START ------------------'); console.log('параметры:'); console.log(first); console.log(second); . console.log('Результат'); console.log(result); >;
Отметка «START» нужна, чтобы увидеть, в какой именно момент вызывается ваша функция. Например, в упражнениях на Хекслете часто идёт проверка через тесты, которые несколько раз запускают нашу функцию с разными данными. Таким образом, в выводе информации можно будет легче разобраться, что и к какому запуску вашей функции относится.
Иногда вместо логирования гораздо удобнее использовать дебаггер. Например, если вы делаете упражнение, в котором есть Web-доступ, можно открыть DevTools браузера и посмотреть в нём исполняемый код. Здесь вы можете поставить на паузу выполнение программы и посмотреть, чему равны все значения переменных или констант в текущий момент.
Изолируем код
При знакомстве с задачей старайтесь сразу определять сложные моменты и максимально их прорабатывать. Например, если в решении нужно использовать цикл, а вы ещё плохо знакомы с ними, можно отдельно от решения попрактиковаться в их использовании. Старайтесь больше экспериментировать и подводить пример максимально близко к тому, что происходит в задаче.
Выделение изолированных участков кода — еще один важнейший механизм в решении задач. Попробуйте выделить в вашем алгоритме промежуточные результаты — так вы сможете написать отдельные модули, каждый из которых будет вычислять нужный результат. При этом логика таких модулей будет максимально простой и понятной. Тут мы подходим пожалуй к самой сложной вещи: построение алгоритма.
Описываем алгоритм
Решение любой задачи начинается с алгоритма, после чего уже идет его реализация. С ней обычно всё понятно: мы либо знаем, как нужно делать, либо не знаем — и вот тогда нужно искать информацию, читать документацию и так далее. Отдельным пунктом в реализации идёт работа над ошибками — об этом мы расскажем чуть позже.
Начните продумывать алгоритм без контекста языка программирования. Есть такое понятие как псевдокод — это конструкции, очень похожие на язык программирования, но не привязанные к какому-то конкретному языку. Они помогают составить алгоритм решения задачи и перенести его в код.
Попытайтесь описать решение простыми словами другому человеку. Есть даже такой приём, он называется «метод утёнка» — это когда вы описываете, что делает каждая строчка вашего кода воображаемому собеседнику (игрушечной уточке). Этот приём работает не только когда код уже написан, но и ещё при поиске верного алгоритма. Когда вы описываете проблему другому человеку, вам невольно приходится систематизировать всю информацию, формализовать некоторые идеи и возможные пути решения, а это уже первый шаг к составлению работающего алгоритма.
И не стесняйтесь просить помощи. Не стоит расстраиваться, что у вас не получается решить с первого раза. А когда задаёте вопрос, задавайте его правильно. Когда у вас уже есть какое-то решение, но оно не работает, наступает следующий этап работы.
Читайте также: Как сохранять фокус на протяжении всего обучения: советы от Хекслета
Проверяем каждую идею
Даже опытные программисты далеко не всегда с первого подхода реализуют правильное решение. Обычно создаётся первый, черновой вариант решения, и уже дальше он дорабатывается. В ходе работы над ошибками очень важно проверять каждый участок кода.
Постарайтесь проверить каждый промежуточный результат, залогируйте изменения данных, разберите на мелкие детали работу вашего кода. Одна из самых распространенных ошибок в программировании это построение дальнейшего решения на неверных выводах. Например, вы написали какой-то код и считаете, что он работает верно. Далее вы добавляете код, но что-то уже не работает как надо. Можно сделать вывод, что проблема в новом коде, однако вполне возможно, что просто изначальный код был не верен. Такая ситуация опасна тем, что вы можете потратить очень много времени впустую — потому что просто будете искать проблему не в том месте.
Чтобы избежать таких ситуаций, во-первых нужно перепроверять код, особенно когда вы еще только начинаете учиться. Старайтесь избегать утвердительных формулировок по типу «я сделал правильно» или «этот код делает то-то», если вы это точно не проверили. Во-вторых, вы можете показать своё решение другому человеку или с помощью метода утенка описать своими словами, что делает тот или иной участок кода. Есть большая вероятность, что вы сразу обнаружите слабые места в вашем решении, либо собеседник укажет на них.
Делаем шаг назад
Если долго не получается решить задачу, попробуйте отвлечься. Вы всегда можете вернуться к ней позже. Многим студентам часто приходили решения по утрам, а иногда — даже во сне. Также полезно возвращаться к старым задачам, особенно если вы уже подзабыли решения, либо если какая-то из тем кажется не до конца понятной. Помните, что какие бы ни были сложные задачи, все решения строятся на базовых простых вещах, как здание строится из маленьких кирпичиков.
Пройденный материал ещё полезно перепроходить и потому, что это хорошая подпитка вашей мотивации: вы будете быстрее решать уже пройденные задачи. Таким образом вы на себе почувствуете прогресс, это даст уверенности, что вы движетесь в правильном направлении.
Никогда не останавливайтесь: В программировании говорят, что нужно постоянно учиться даже для того, чтобы просто находиться на месте. Развивайтесь с нами — на Хекслете есть сотни курсов по разработке на разных языках и технологиях
Как решать задачи на программирование
Перевод статьи «How to Solve Coding Problems with a Simple Four Step Method».
Я потратила два месяца на подготовку к своему первому техническому собеседованию и думала, что уже готова. Но когда подошла к нему уже вплотную, внезапно меня озарило: я ж понятия не имею, как решать задачи на программирование.
Ни в одном туториале из тех, по которым я училась программированию, не рассказывалось о решении задач. А мне определенно нужно было овладеть этим искусством: от этого зависела вся моя будущая карьера разработчика. И я немедленно взялась за поиски.
То, что я нашла, было не просто подходом, а бесценной стратегией. Это был проверенный временем алгоритм действий в четыре шага, который по какой-то причине остался незамеченным в сфере разработки.
В этой статье я подробно расскажу об этой стратегии, чтобы и вы могли начать уверенно подходить к решению задач на программирование.
Решение задач — это не только часть процедуры собеседований. Это то, чем разработчики занимаются ежедневно. В конечном итоге, само написание кода — это и есть решение задач.
Универсальный подход к решению задач
Этот метод изложен в книге «Как решать задачу» Дьёрдя Пойа. Первое издание вышло еще в 1945 году, было продано больше миллиона экземпляров. (На русском языке книга публиковалась как пособие для учителей еще в 1959 году. — Прим. перев.).
Метод Пойа используют многие программисты, от профессоров информатики (см. курс «Intro to CS» на Udacity, который ведет профессор Дэвид Эванс) до преподавателей современной веб-разработки вроде Кольта Стила.
Давайте пройдемся по решению простой задачи на программирование с применением метода Пойа. Это позволит увидеть работу метода на практике и в результате лучше разобраться в нем. Для примера будем использовать язык JavaScript.
Создайте функцию, которая будет складывать два числа и возвращать их сумму.
Вот четыре шага по решению любой задачи:
- Понять задачу.
- Разработать план.
- Осуществить план.
- Оглянуться назад.
«Во-первых, мы должны понять задачу; мы должны ясно видеть, что в ней является искомым. Во-вторых, мы должны усмотреть, как связаны друг с другом различные элементы задачи, как неизвестное связано с данными. Это необходимо, чтобы получить представление о решении, чтобы составить план. В-третьих, мы осуществляем наш план. В-четвертых, оглядываясь назад на полученное решение, мы вновь изучаем и анализируем его», — «Как решать задачу», 1959 г.
Давайте сделаем первый шаг.
Шаг 1: понять задачу
Когда вы на собеседовании получаете задачу на программирование, возникает соблазн тут же приступить к написанию кода. Этому искушению трудно противостоять, особенно, если время ограничено.
Тем не менее, постарайтесь удержаться. Прежде чем приступать к решению задачи, убедитесь, что вы хорошо поняли, что от вас требуется.
Прочтите текст задачи. Можно даже читать вслух, если это поможет вам притормозить.
Прочитав текст задачи, выясните все моменты, которые вам не понятны. Если дело происходит на собеседовании, можно задать уточняющие вопросы интервьюеру. Если вы решаете задачу в гордом одиночестве, обдумайте непонятные части или даже поищите в Google ответы на возникшие вопросы.
Этот первый шаг жизненно важен. Мы часто не уделяем достаточного количества времени на то, чтобы разобраться в постановке задачи. А если вы не до конца понимаете задачу, вам будет куда труднее ее решить.
Чтобы помочь себе разобраться, спросите себя:
Каковы здесь входящие данные?
Какого рода input-ы следует ожидать? В нашем примере это аргументы, принимаемые функцией.
Читая текст задачи, мы понимаем, что входящими данными будут числа. Если подходить к делу более скрупулезно, мы можем спросить:
- всегда ли будет только два числа?
- что будет, если функция получит в качестве входящих данных три числа?
Эти вопросы мы можем задать интервьюеру или поискать ответ в описании задачи. (К задаче может прилагаться примечание типа «Исходите из того, что в функцию всегда будут передаваться только два числа». Конечно, это сразу все прояснит).
Можно задаться и другими вопросами. Всегда ли входящими данными будут числа? Что должна делать функция, если получит в качестве аргументов «a» и «b»? Уточните, всегда ли input будет числовым.
В качестве варианта — можно записать вероятные входящие данные в комментарии к коду, чтобы иметь представление о том, как они должны выглядеть.
// inputs: 2, 4
Далее следует спросить себя:
Каковы должны быть результаты?
Что функция будет возвращать? В нашем случае это должно быть оно число — сумма двух чисел, переданных в качестве аргументов. Убедитесь, что вы хорошо понимаете, каким должен быть output.
Придумайте простые примеры
Разобравшись в сути задачи и зная вероятные input-ы и output-ы, можно начать работать над конкретными примерами.
Примеры также могут использоваться для тестирования вашего решения. На техническом собеседовании или при подготовке к нему на сайтах вроде Codewars или HackerRank используются специальные редакторы. Большинство из них содержат уже готовые примеры или test cases. Несмотря на это, написание собственных примеров может помочь вам упрочить понимание задачи.
Начните с написания одного-двух простых примеров.
Давайте вернемся к нашей складывающей функции. Назовем ее «add».
Каким может быть input? Ну, допустим, таким:
// add(2, 3)
Каким будет результат при таких входящих данных? Записать это можно так:
// add(2, 3) ---> 5
Это показывает, что наша функция принимает в качестве input 2 и 3, а как output возвращает 5.
Придумайте сложные примеры
Продумывая более сложные примеры, вы можете уделить время поиску крайних случаев, которые стоит учесть.
Например, что будет, если наши входящие данные будут не числами, а строками? Что, если мы получим в качестве аргументов две строки, например, add(‘a’, ‘b’)?
Ваш интервьюер может сказать вам вернуть сообщение об ошибке, если входящих данных не будет или если они будут не того типа. В таком случае вы можете добавить соответствующий комментарий — в качестве напоминания себе.
// вернуть error, если input-ы - не числа.
Как вариант, интервьюер может сказать, что входящие данные всегда будут числами. В таком случае вам не понадобится писать никакой дополнительный код для обработки этого крайнего случая.
Если вы не на собеседовании, а просто решаете задачу, в ней может быть обозначено, что должно происходить при вводе невалидных данных.
Например, в задаче может говориться «При отсутствии input-а верните undefined». В таком случае можно написать комментарий:
// Проверить, если ли входящие данные. // Если входящих данных нет, вернуть undefined.
В нашем случае мы будем исходить из того, что input всегда будет числовым. Но в общем всегда лучше подумать о крайних случаях.
Профессор информатики Эванс советует писать т. н. безопасный код. Всегда думайте о том, что может пойти не по плану и как защитить свой код от возможных ошибок.
Прежде чем перейти ко второму шагу, давайте кратко повторим, что нужно сделать на первом шаге:
- прочитать задачу
- выяснить, какими будут входящие данные
- выяснить, каким должен быть результат
- создать простые примеры, затем — более сложные.
Шаг 2: разработать план решения задачи
Далее нужно составить план того, как вы, собственно, собираетесь решать задачу. Придумав план, запишите его в виде псевдокода.
Псевдокод — это изложенные простым языком шаги алгоритма. Иными словами, это пошаговый план решения задачи.
Опишите каждый этап решения. Если задача сложная, этапов может быть много. Если говорить о нашей задаче, мы можем написать:
// Создать переменную sum. // Сложить первый input со вторым, используя оператор сложения. // Сохранить значение суммы input-ов в переменной sum. // Вернуть переменную sum в качестве output.
Теперь у вас есть четырехэтапный план решения задачи.
Для более сложных задач профессор Эванс советует: «Полагайтесь на то, как задачи решаются людьми». То есть, забудьте на мгновение о том, как задачу может решить код, и подумайте о том, как ее решали бы вы — человек. Это может помочь вам более четко увидеть нужные шаги.
3. Шаг 3: осуществить план (решить задачу!)
Следующий шаг — собственно решение задачи. Руководствуясь псевдокодом, напишите настоящий код.
Профессор Эванс рекомендует сфокусироваться на простом, механическом решении. Чем проще и легче ваше решение, тем вероятнее, что вы напишете код правильно.
Если взять наш псевдокод, мы можем написать следующее:
function add(a, b)
Помните, что не следует преждевременно оптимизировать код. Написав решение, вы можете подумать, что ваш код неэффективный, и захотеть его улучшить. Но для начала создайте простое, механическое решение.
Что, если вы не можете решить задачу полностью? Например, если вы так и не нашли решения для какой-то ее части? Кольт Стил советует в таких ситуациях игнорировать эту сложную часть. Сконцентрируйтесь на всем остальном — том, что вы уже можете написать. Когда напишете, возвращайтесь к части, которая вызвала у вас затруднения.
Шаг 4: оглянуться назад
Когда у вас на руках будет готовое рабочее решение, подумайте, как его можно улучшить. На этом этапе можно провести рефакторинг и преобразовать свое решение в более эффективное.
Просматривая свою работу, вы можете задать себе несколько вопросов:
- Можно ли получить этот результат как-то иначе? Какие еще подходы есть?
- Понятно ли это решение с первого взгляда?
- Можно ли использовать результат или метод для какой-то другой задачи?
- Можно ли улучшить производительность решения?
- Не приходят ли вам на ум какие-то способы рефакторинга для этого решения?
- Как эту задачу решают другие люди?
Возвращаясь к нашему коду, мы можем сделать его более лаконичным, если удалим нашу переменную и используем имплицитный return:
function add(a, b)
Дойдя до четвертого шага, вы понимаете, что можете никогда не довести свое решение до совершенства. Даже великие разработчики пишут код, который впоследствии хотят изменить. Так что вопросы из приведенного списка — лишь направляющие, которые могут вам помочь.
Если вы решаете задачу на собеседовании, то для четвертого шага у вас может просто не остаться времени. Впрочем, если время есть, попробуйте улучшить свое решение. Ну а если вы просто решаете какую-нибудь задачу, обязательно проходите все описанные шаги.
Когда я практиковалась в решении задач, я практически всегда смотрела и чужие решения, которые часто были элегантнее и эффективнее моих.
Итоги
В этой статье мы рассмотрели четырехэтапную стратегию решения задач на программирование. Давайте повторим:
- Шаг 1: понять задачу
- Шаг2: создать пошаговый план решения
- Шаг 3: реализовать план и написать код решения
- Шаг 4: оглянуться назад и по возможности улучшить решение.
Применение этого подхода очень помогло мне при прохождении технических собеседований, да и вообще в моей работе.
Если вы не чувствуете себя уверенно, решая задачи, помните, что умение решать задачи — это навык. Чем больше практики, тем лучше вам это будет удаваться.