Connect with us

Программирование

Как программировать и двигаться вперед быстрее

Вот те вещи, которые, на мой взгляд, оказали наибольшее влияние.

Фото аватара

Опубликовано

/

     
     

Я не думаю, что я очень быстр в абсолютном смысле, но я гораздо быстрее, чем 5 лет назад.

Вот те вещи, которые, на мой взгляд, оказали наибольшее влияние.

Забота

Главное, что помогло — это желание стать быстрее.

В начале своей карьеры я больше заботился о написании «элегантного» кода или использовании модных инструментов, чем о реальном решении проблем. Может быть, это и не было явным убеждением, но эти приоритеты были очевидны из моих действий.

Вероятно, я также не знал, насколько быстрее можно работать. В начале своей карьеры я работал с людьми, которые были такими же медлительными и неопытными, как и я.

Со временем я начал замечать, что некоторые люди создают проекты, которые намного превосходят то, что я могу сделать за одну жизнь. Я захотел понять, как это сделать, что означало отказаться от своих прежних убеждений и попытаться выяснить, что же на самом деле работает.

Основной темой большинства приведенных ниже идей является систематический подход к совершенствованию. Я всегда был приверженцем таких командных процессов, как непрерывная интеграция, code review и анализ первопричин, но в течение долгого времени я совершенно бессистемно относился к своим собственным процессам.

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

Но даже бессистемное применение и частое игнорирование этих самых элементарных попыток приводит к значительным улучшениям.

Принимайте решения на основе целей

Я уже писал о постановке целей. Наличие четко сформулированных целей помогает избежать паралича при принятии решений. Всякий раз, когда я замечаю, что застрял на каком-то решении, я прохожу через этот процесс:

  • Имеет ли это значение? Если нет, то бросаю монетку, чтобы не тратить время на размышления.
  • Достаточно ли у меня информации, чтобы оценить ее влияние на достижение цели? Если нет, то придумайте, как получить эту информацию (например, провести исследование, создать прототип, подождать до следующего раза).
  • Какой выбор приблизит меня к цели?

Пример: Какую структуру данных использовать для текста в текстовом редакторе?

  • Имеет ли это значение? Возможно — это проблема производительности некоторых редакторов, а варианты сильно различаются по сложности.
  • Достаточно ли у меня информации? Нет, я не знаю, какая производительность мне нужна.
    • Просмотрев все текстовые файлы в моем домашнем каталоге, я обнаружил один файл размером 2 Мб и ни одного файла размером более 200 Кб. Большинство из них ближе к 1 Кб.
    • На моем ноутбуке копирование 1 Кб занимает ~0.5us, а копирование 1 Мб — ~0.5ms. Таким образом, если я использую массив байтов, то в худшем случае время вставки файла размером 2 Мб составит 1 мс.
    • Моя цель — 60 кадров в секунду, что означает, что у меня есть бюджет в 16.6 мс на кадр. Рендеринг занимает <<1 мс, так что у меня еще много свободного времени.
  • Какой из вариантов приблизит меня к цели?
    • Любой из вариантов будет достаточно быстрым.
    • Массив байтов — самый простой в реализации.

Некоторые решения не имеют большого значения по отдельностиа, но часто встречаются. Это, например, стиль кодирования, именование переменных, организация кода. Для них стоит потратить немного времени, чтобы выработать эмпирические правила, а не думать о них каждый раз, когда они возникают.

Пример: Раньше я тратил много времени на размышления о том, как разделить код на файлы и каталоги, основываясь на том, какой код «принадлежит» друг другу. Но это совершенно расплывчатый критерий, к тому же не имеющий никакой очевидной связи с моими реальными целями. Кроме того, обычно существует несколько различных осей, по которым можно сгруппировать вещи, например, компилятор может делить его по проходам или по особенностям языка. Поэтому сейчас я обычно помещаю все в один файл, пока не начинаю замечать трудности с навигацией, а затем разделяю то, что, как я заметил, я обычно читаю или редактирую одновременно. В отличие от предыдущих критериев, этот легко оценить, просто вспомнив, что я делал, и легко связать с моими целями — это означает меньше прыжков в будущих правках, что сделает кодирование и отладку немного быстрее.

Самый важный класс решений — «что делать дальше?». Вариантов всегда гораздо больше, чем времени. Наличие явных целей облегчает расстановку приоритетов.

Для инструментов, которые я использую сам, я определяю приоритеты по экономии времени или повышению качества. Для коммерческих проектов приоритеты определяются заказчиками. В исследовательских проектах приоритет отдается тому, что даст наибольшее количество информации по вопросу/гипотезе исследования.

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

Более тонким решением является вопрос о том, как долго работать над чем-то. Обычно отдача от вложенного времени снижается, поэтому может оказаться более ценным плохо сделать три задачи, чем сделать одну задачу идеально.

Пример: Мне потребовалось очень много времени, чтобы научиться писать «уродливый» или «грязный» код. В идеальном мире я бы поддерживал качество всей своей работы на пределе своих возможностей. Но я обнаружил, что могу написать значительно больше, если немного расслаблюсь. Я предпочитаю иметь готовый проект, отшлифованный на 80%, чем 1/3 проекта, отшлифованного на 100%. Это особенно верно для экспериментов, прототипирования и тестирования, где качество не имеет такого большого значения. Но даже производственный код часто имеет короткий период полураспада — я полагаю, что половина моего кода выбрасывается или переписывается в течение первого года.

Раньше я не задумывался об этом, и в результате тратил много времени:

  • метался туда-сюда между двумя вариантами дольше, чем требовалось для создания прототипа и оценки обоих вариантов
  • работал над технически впечатляющими, но не очень важными вещами
  • создавал сложные абстракции, которые приходилось отменять через месяц, когда требования менялись

Фокусируйтесь

Я работаю блоками по 2-3 часа, в течение которых я не делаю ничего другого — никакой электронной почты, Slack, Twitter, Hacker news, болтовни с соседом и т.д.

  • “Многозадачность” — это просто быстрое переключение контекста. Я часто отвечаю на письмо, пока что-то компилируется, а потом, когда возвращаюсь, забываю, что и зачем я компилировал. Это приводит к потере времени и ошибкам.
  • Предыдущие задачи продолжают отнимать внимание даже после переключения. Особенно это касается того, что вызывает сильные эмоции. Мне трудно сосредоточиться, если я каждые 15 минут открываю мессенджер и каждый раз вижу ту тему, где кто-то спорит со мной, и он совершенно не прав, и как он вообще может верить в то, что говорит, и что я опять делаю?
  • Подверженность аддиктивному взаимодействию приучила меня к самопрерыванию — всякий раз, когда я сталкивался с трудным решением или сложной ошибкой, я переключался на что-то более легкое и приносящее немедленное удовлетворение. Прогресс в решении сложных задач возможен только в том случае, если я не позволяю этим привычкам закрепляться.
  • Ожидание посторонних отвлечений (например, уведомлений, отвлекающих фоновых разговоров) мешает мне начать концентрироваться, поскольку на каком-то уровне я не ожидаю, что это будет иметь смысл, если меня все равно будут отвлекать по пустякам.

Я делаю небольшие перерывы в работе, но не делаю ничего, что могло бы переключить мое внимание. Так, я прогуливаюсь, потягиваюсь, завариваю чай и т.д., но не смотрю на телефон и не проверяю почту.

Музыка, безусловно, помогает мне сосредоточиться, но она также улучшает мое настроение, поэтому я иногда включаю один альбом, если мне трудно начать работу с утра. Обычно к тому времени, когда альбом заканчивается, я уже настолько погружаюсь в работу, что не замечаю ее отсутствия.

Если мне нужно прерваться в определенное время (например, на встречу), я ставлю будильник, а не пытаюсь вспомнить и не беспокоюсь об этом.

Я считаю, что лучше всего мне работается утром. Обычно мне трудно начать работу, но если я ее начинаю, то мне легко продолжать. Мне легче, когда у меня есть последовательный утренний распорядок дня: я просыпаюсь, завтракаю, иду на прогулку, планирую, над чем я хочу поработать во время прогулки, а затем начинаю работу, как только возвращаюсь. Мне не всегда удается придерживаться этого распорядка, но когда я его соблюдаю, мне не приходится тратить усилия на то, чтобы заставить себя начать работу, — все происходит само собой.

Эти изменения могут показаться тривиальными, но я не могу переоценить, насколько они изменят ситуацию, если применять их последовательно. Внимание и кратковременная память — это узкое место, через которое должно проходить все остальное, но они невероятно хрупки и все чаще подвергаются негативному воздействию.

Прочитайте также «Глубокая работа«, «Ваш мозг на работе» (последняя книга немного поп-психологическая, но все же полезная).

Работайте блоками

В последние пару лет я применяю идею отказа от многозадачности на более тонком уровне.

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

  1. Записать, чего я хочу добиться.
  2. Прикинуть, как я буду это делать.
  3. Пройтись по коду и составить краткий список изменений, которые необходимо внести.
  4. Вносить изменения одно за другим в порядке их появления в списке.
  5. Прочитать diff и исправить очевидные ошибки, улучшить комментарии, подобрать более удачные имена переменных и т.д.
  6. Протестировать и отладить. Вернуться к шагу 2, если я понял, что сделал неправильный выбор и нужно сделать что-то по-другому.
  7. Коммит, мердж, приготовление чая.

Часто на этапе 3 я понимаю, что мой план на самом деле не будет работать. Обнаружение этого до того, как я напишу кучу кода, экономит много времени, а отсутствие этих невозвратных затрат также снижает вероятность того, что я все равно попытаюсь реализовать задуманное.

На шаге 4, если я замечаю другие изменения, которые необходимо внести, я добавляю их в список, а не пытаюсь сделать два изменения одновременно (если только это не что-то очень маленькое, например, исправление комментария, который я пропустил). Когда я думаю, что смогу обойтись без одновременного внесения нескольких изменений, это обычно занимает больше времени, или я понимаю, что одно из изменений было плохой идеей, но теперь я не могу просто отменить его, потому что оно смешалось с другими.

Подобные пакеты я делаю повсеместно. Практически любая сложная задача включает в себя переключение между несколькими подзадачами. Если мне удается придумать, как их перераспределить, чтобы уменьшить количество переключений, я обычно обнаруживаю, что это экономит время и уменьшает количество ошибок.

Вносите небольшие изменения

Всякий раз, когда мне нужно внести большое изменение — любое, которое займет больше пары дней, — я хочу разбить его на небольшие изменения, которые можно объединить по отдельности. Это облегчает жизнь во многих отношениях:

  • Если я что-то сломаю, то гораздо быстрее найти причину в более мелких изменениях.
  • Если какая-то часть изменений окажется плохой идеей или потребует переделки, ее гораздо проще откатить назад, если она не смешана с множеством других изменений.
  • Слияние долгоживущих веток затруднено и часто приводит к появлению тонких ошибок, когда упускается взаимодействие между различными изменениями. Например, я изменяю функцию и должен обновить все вызовы, а в это время кто-то добавил новые вызовы в другой ветке.
  • Если возникает что-то срочное, я могу сразу же разобраться с этим и вернуться к текущей работе позже, не беспокоясь о том, что за это время она успела подпортиться.
  • В проектах, над которыми я не работаю постоянно, гораздо проще переключиться, если все оставлено в рабочем состоянии.
  • Все случаи, когда я чувствовал себя перегоревшим, начинались с того, что у меня был какой-то большой кусок работы и я постоянно думал: «Это почти готово, осталось сделать еще один большой рывок». Это никогда не бывает просто еще одним большим рывком. Лучше иметь возможность объединить что-то, что работает, взять перерыв и вернуться к этому свежим.

У меня есть хороший самодостаточный пример разбиения большого изменения. Я хотел изменить внутреннее представление, используемое везде в компиляторе. Вместо того чтобы пытаться сделать все сразу, я открыл параллельный конвейер для новой версии и создавал ее поэтапно, сохраняя при этом работоспособность старой версии. Как только новая версия была завершена и давала одинаковые результаты во всех тестах, я удалил старую версию.

Это заняло всего 4 рабочих блока, но они были распределены на пару недель. Если бы я был моложе, я бы, наверное, попытался просто отредактировать представление и внести все изменения сразу, что привело бы к тому, что этот проект даже не смог бы успешно скомпилироваться в течение нескольких недель. Каждый раз, возвращаясь к нему, я должен был бы вспоминать, что я уже закончил редактировать, а что еще нужно сделать. А когда я, наконец, попытался бы его скомпилировать, мне пришлось бы исправлять ошибки типа в коде, написанном двумя неделями ранее и уже не оставившим свежих воспоминаний.

Прочитайте также «Инкрементное программирование«.

Сокращайте циклы обратной связи

Чем больше времени проходит с момента совершения ошибки до ее обнаружения, тем сложнее ее отследить — растет число возможных причин и исчезает контекст.

Аналогично, чем быстрее я могу оценить последствия принятых решений, тем больше различных вариантов я могу попробовать и тем меньше вероятность того, что плохое решение закрепится и его будет трудно отменить.

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

Примеры:

  • Инструменты IDE, которые показывают ошибки в редакторе по мере ввода, часто позволяют исправить их сразу после ввода, пока контекст еще совсем свежий.
  • Я настраиваю свой редактор на выполнение тестов при каждом сохранении, так что к тому моменту, когда я смотрю на окно тестов, они уже завершены.
  • Для некоторых видов ошибок в тестах я хочу просмотреть их в отладчике. Вместо того чтобы настраивать это каждый раз, я всегда запускаю тесты с помощью rr record. Я интегрировал это в программу запуска тестов, чтобы она знала, что нужно сделать rr replay при неудаче теста, без необходимости открывать новый терминал.
  • В Materialize имеется более шести миллионов SQL-тестов. Они хороши для выявления крайних случаев, но слишком медленны для интерактивного запуска. Поэтому Нихил создал отдельную команду, которая запускает гораздо меньшее подмножество с хорошим покрытием. Быстрые тесты выполняются на prepush хуке, а медленные — в CI.
  • Тесты полной интеграции в Materialize требовали установки множества нестабильных сторонних зависимостей и занимали несколько минут только для запуска. Бреннан создал гораздо более простую версию бенчмарка, которая не полностью покрывала код Materialize, но легко редактировалась и занимала всего несколько секунд для запуска. Я использую быстрый бенчмарк десятки раз в день для измерения параметров и проверки идей и переключаюсь на медленный бенчмарк только для подтверждения результатов.
  • Я часто использую дешевые эксперименты, чтобы найти нижнюю/верхнюю границу ценности некоторой части работы.

Иногда можно обойти длинные петли обратной связи, работая параллельно. Когда я работаю над проектами с длительным временем компиляции (например, инкрементные сборки Materialize иногда занимают 14 минут), я иногда отвлекаюсь, и когда компиляция заканчивается, я уже забываю, что тестировал. Чтобы уменьшить это влияние, я иногда составляю список из нескольких экспериментов и запускаю по одному на каждом декстопе, при этом на каждом рабочем столе открыт текстовый файл с объяснением того, что я делаю. К тому времени, когда я закончу настройку последнего эксперимента, первый, надеюсь, уже завершится. Это все равно не так эффективно, как просто более короткие петли обратной связи, а для бенчмарков это требует наличия большого количества дополнительного оборудования, чтобы избежать помех. Но иногда это лучшее, что я могу сделать.

Сохранение коротких петель обратной связи особенно важно в крупных проектах. Если добавить какой-либо медленный процесс, его совсем скоро становится трудно устранить. Самый простой способ получить короткие петли обратной связи — это начать с них и затем очень строго следить за тем, чтобы они не регрессировали. Пример, который меня вдохновляет — это досекундная live перезагрузка от bitsquid. Они с самого начала поставили перед собой такую цель и просто отказались принимать любые другие варианты архитектуры, которые ее нарушали.

Записывайте материал

Я веду рабочий журнал — единый очень большой текстовый файл только с возможностью добавления. Я использую его по-разному:

  • Прежде чем вносить изменения, я записываю, чего я пытаюсь достичь и каков мой план. Напоминание о цели облегчает принятие решений. Записав план в явном виде, легче заметить, когда я что-то не продумал.
  • Когда я вношу изменения, я веду очень краткосрочный список дел — на ближайшие несколько часов или около того. Это позволяет мне не забывать шаги и, если я отвлекаюсь, напоминает мне, чем я занимался.
  • Когда мне приходится искать, как использовать тот или иной инструмент, или исправлять непонятное сообщение об ошибке, я записываю ответ. Часто я сталкиваюсь с тем же самым через год и могу просто найти его в своем журнале.
  • Когда мне трудно принять решение, я записываю свой внутренний монолог. Это помогает мне продвигаться вперед, а не зацикливаться на одних и тех же мыслях снова и снова. Кроме того, так становится очевиднее, когда мне нужно найти какую-то недостающую информацию.
  • Приняв решение, я записываю его обоснование. Это полезно, если через год или два я буду пересматривать проект и не смогу вспомнить, почему я вообще сделал эту странную вещь. (Я также пытаюсь записать это где-нибудь в коде, но часто такие комментарии не сохраняются).

Все это не требует больших усилий. Обычно я пишу около 100 слов в день.

Сократите количество частых ошибок

Часто я совершаю одну и ту же ошибку несколько раз в день. Когда я замечаю это, то стараюсь придумать, как избежать этой ошибки вообще.

Примеры:

  • У меня появилась привычка читать свои изменения перед запуском или фиксацией. Я мысленно представляю, как объясняю их кому-то другому. Это часто позволяет выявить такие глупые ошибки, как введение неправильного имени переменной.
  • Всякий раз, когда я исправляю ошибку или ошибку типа, я проверяю весь соседний код, чтобы увидеть, не допустил ли я ту же ошибку. Такие ошибки, как забывание клонирования или передача значения вместо ссылки, часто встречаются в виде последовательностей, и обычно быстрее найти их все сразу, чем проходить цикл компиляции/тестирования для каждой из них.
  • Недавно у меня было несколько ошибок, вызванных ранними возвратами. Менять код, чтобы избежать их, было неудобно, поэтому вместо этого я изменил подсветку синтаксиса, чтобы сделать break/continue/return более заметными. Когда я проверил, как это выглядит, то сразу же обнаружил еще одну ошибку.
  • Раньше я часто случайно коммитил операторы print или другой временный отладочный код. Друг, с которым я работал, предложил использовать инструмент интерактивных коммитов magit, который позволяет легко читать, искать и редактировать diff перед фиксацией.

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

  • Составить контрольный список для проверки кода. Например, для каждого нового выделения памяти найти, где она освобождается, для каждой новой возникшей ошибки найти, где она обрабатывается.
  • Записывать все свои ошибки в журнал в течение недели и искать закономерности.

Прочитайте также “Чек-лист. Как избежать глупых ошибок, ведущих к фатальным последствиям”.

Сделайте низкоуровневые навыки автоматическими

Ранее я писал, что многозадачность — это просто быстрое переключение контекста. На самом деле это справедливо только для тех навыков, которые требуют осознанного внимания. Некоторые виды навыков можно освоить настолько глубоко, что они уже не требуют внимания. Хорошими кандидатами являются:

  • Набор текста. Это интерфейс ко всему остальному в программном обеспечении (даже к мышлению — я лучше думаю, когда записываю свои идеи), поэтому это труднодоступное бутылочное горлышко. Я заметил, что возвращение назад, чтобы исправить опечатки, часто мешает мне сосредоточиться, так что, вероятно, я мог бы выиграть от повышения точности, а не скорости. Раньше мне приходилось смотреть на клавиатуру, чтобы найти некоторые символы, особенно в середине ряда цифр, поэтому я зачернил эти клавиши, чтобы заставить себя научиться точно набирать их с помощью слепого ввода.
  • Редактирование. Возможность писать код быстрее делает прототипирование более быстрым, уменьшает боль от очистки кода, позволяет тестировать больше различных оптимизаций и т.д. Я не очень глубоко в это вникал, но я получаю много пользы от некоторых простых изменений:
    • Установка дополнений для используемого языка и обучение навигации по списку с помощью сочетаний клавиш.
    • Обучение эффективному использованию несколько курсоров для структурированного редактирования.
    • Перемещение клавиш со стрелками так, чтобы мне не приходилось покидать начальный ряд для выполнения небольших движений.
    • Удерживание запястья на месте при использовании тачпада для больших движений, чтобы потом надежно вернуть пальцы в начальный ряд.
  • Навигация. Как и в предыдущем случае, я получил много пользы от нескольких изменений:
    • Научился использовать fuzzy-file-open, search, ripgrep, jump-to-definition, jump-to-uses, jump-to-errors и т.д. для перемещения по кодовой базе.
    • Настроил сочетаний клавиш для открытия обычных программ (редактор, терминал, браузер и т.д.) и для закрытия окон.
    • Установил ярлыки для переключения рабочих столов и стандартного использования каждого из них (код на 1, тесты/бенчмарки на 2, документы/справки на 3).
    • Использовал обратный поиск в bash, установил неограниченный размера истории в bash и сохранение истории при каждой команде (чтобы при работе с несколькими терминалами одна история не затирала другую).

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

Следствием этого является отказ от ненадежных инструментов, которые разрушают мышечную память.

Пример: Последней каплей, заставившей меня прекратить использование emacs, стало столкновение с несколькими ошибками, каждая из которых вызывала спорадические многосекундные паузы, во время которых нажатия клавиш попадали не в то окно или ставились в очередь в не реагирующее окно. Каждый раз это приводило к тому, что я не мог понять, в каком состоянии сейчас находится работа, а после исправления ошибки мне приходилось тратить больше времени на то, чтобы вспомнить, что я делал.

Отражайте

Всякий раз, когда я замечаю ненадежный процесс или то, что можно было бы сделать быстрее, я делаю быструю заметку в файле под названием «tools». Он полон подобных вещей:

zig pretty-print in gdb?
command-line version of magit to avoid emacs startup times
  try gitui
clipboard daemon with history
try tracy
fix focus hang/crash on big rg (need to limit input?)

Всякий раз, когда у меня есть несколько свободных часов, я просматриваю этот файл и пытаюсь исправить одну из записей.

В более крупных масштабах я стараюсь писать ретроспективы в своем рабочем журнале после каждого проекта.

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

И т.д.

Легко придумать и другие идеи, которые я еще не пробовал, но которые, как мне кажется, могут работать.

Примеры:

  • Майк Актон выступает за явную практику. Самое сложное здесь — найти правильные проблемы для практики. Они должны быть достаточно похожими, чтобы я отрабатывал конкретный навык, но не настолько похожими, чтобы я просто заучивал что-то, что не будет применяться в моей реальной работе. Для отладки я экспериментирую с автоматической вставкой ошибок. Если это получится, я попробую отработать другие навыки таким же образом. Курсы типа perf-ninja также кажутся ценными.
  • Записываю, как я программирую, и просматриваю запись на предмет неэффективности. Я немного занимался этим, когда работал в Eve, но больше для того, чтобы разрабатывать новые интерфейсы, чем для того, чтобы научиться лучше использовать существующие. Я помню, что разбивка по времени в видеозаписи совершенно не совпадала с моими воспоминаниями, так что, вероятно, есть много «низко висящих фруктов», которые я просто не замечаю.
  • Популярное упражнение в спорте — делать задачу все сложнее и сложнее (например, увеличивать скорость), пока не произойдет срыв. Наблюдение за тем, какой субнавык ломается первым, дает представление о том, что является узким местом в вашей обычной работе. В программировании это немного сложнее, поскольку я не часто сталкиваюсь с повторяющимися задачами, но, возможно, в каком-нибудь наборе задач, например, в leetcode, есть достаточно похожих задач, чтобы попробовать это сделать.

Источник

Если вы нашли опечатку - выделите ее и нажмите Ctrl + Enter! Для связи с нами вы можете использовать info@apptractor.ru.
Advertisement

Наши партнеры:

LEGALBET

Мобильные приложения для ставок на спорт
Telegram

Популярное

Сообщить об опечатке

Текст, который будет отправлен нашим редакторам: