<?xml version="1.0" encoding="UTF-8"?><rss version="2.0"
	xmlns:content="http://purl.org/rss/1.0/modules/content/"
	xmlns:wfw="http://wellformedweb.org/CommentAPI/"
	xmlns:dc="http://purl.org/dc/elements/1.1/"
	xmlns:atom="http://www.w3.org/2005/Atom"
	xmlns:sy="http://purl.org/rss/1.0/modules/syndication/"
	xmlns:slash="http://purl.org/rss/1.0/modules/slash/"
	>

<channel>
	<title>Python Алгоритмы - DataTalks.RU. Data Engineering / DWH / Data Pipeline</title>
	<atom:link href="https://datatalks.ru/tag/python-%D0%B0%D0%BB%D0%B3%D0%BE%D1%80%D0%B8%D1%82%D0%BC%D1%8B/feed/" rel="self" type="application/rss+xml" />
	<link>https://datatalks.ru/tag/python-алгоритмы/</link>
	<description>RoadMap для инженера данных. Дорожная карта по инструментам Data Engineer</description>
	<lastBuildDate>Sun, 25 Jan 2026 14:55:38 +0000</lastBuildDate>
	<language>ru-RU</language>
	<sy:updatePeriod>
	hourly	</sy:updatePeriod>
	<sy:updateFrequency>
	1	</sy:updateFrequency>
	<generator>https://wordpress.org/?v=6.9.4</generator>

<image>
	<url>https://datatalks.ru/wp-content/uploads/2024/12/cropped-logo_datatalks-32x32.png</url>
	<title>Python Алгоритмы - DataTalks.RU. Data Engineering / DWH / Data Pipeline</title>
	<link>https://datatalks.ru/tag/python-алгоритмы/</link>
	<width>32</width>
	<height>32</height>
</image> 
	<item>
		<title>Введение в Структуры данных (Data Structures) и алгоритмы</title>
		<link>https://datatalks.ru/data-structures-and-algorithms/</link>
					<comments>https://datatalks.ru/data-structures-and-algorithms/#respond</comments>
		
		<dc:creator><![CDATA[Data Engineer (Admin)]]></dc:creator>
		<pubDate>Sat, 19 Jul 2025 15:27:09 +0000</pubDate>
				<category><![CDATA[Python]]></category>
		<category><![CDATA[Python Interview]]></category>
		<category><![CDATA[Python Алгоритмы]]></category>
		<category><![CDATA[Python Собеседование]]></category>
		<category><![CDATA[Python Структуры данных]]></category>
		<guid isPermaLink="false">https://datatalks.ru/?p=1830</guid>

					<description><![CDATA[<p>Введение Нотация Big O Нотация Big O — это математический способ описывать асимптотическую сложность алгоритмов, то есть то, как растут затраты ресурсов при увеличении размера входных данных n. Она абстрагируется от конкретного железа, языка и констант и показывает порядок роста, что делает её универсальным инструментом анализа. Временная сложность (time complexity) описывает, как растёт количество элементарных [&#8230;]</p>
<p>Сообщение <a href="https://datatalks.ru/data-structures-and-algorithms/">Введение в Структуры данных (Data Structures) и алгоритмы</a> появились сначала на <a href="https://datatalks.ru">DataTalks.RU. Data Engineering / DWH / Data Pipeline</a>.</p>
]]></description>
										<content:encoded><![CDATA[<h1>Введение</h1>
<h2>Нотация Big O</h2>
<p><strong>Нотация Big O</strong> — это математический способ описывать <strong>асимптотическую сложность</strong> алгоритмов, то есть то, как растут затраты ресурсов при увеличении <strong>размера входных данных n</strong>. Она абстрагируется от конкретного железа, языка и констант и показывает порядок роста, что делает её универсальным инструментом анализа.</p>
<p><strong>Временная сложность (time complexity)</strong> описывает, как растёт количество элементарных операций алгоритма в зависимости от <strong>n</strong>. Формально говорят, что алгоритм имеет сложность <code>O(f(n))</code>, если существует такая константа <strong>C</strong>, что при достаточно больших <strong>n</strong> время работы не превышает <code>C · f(n)</code>. <strong>Важный момент:</strong> учитывается <strong>худший случай</strong>, если не указано иное. Константы, коэффициенты и члены меньшего порядка отбрасываются, потому что при <strong>больших n</strong> они не влияют на характер роста. Например, <code>3n² + 10n + 5</code> в <strong>Big O</strong> записывается как <code>O(n²)</code>.</p>
<p><strong>Пространственная сложность (space complexity)</strong> описывает, как растёт <strong>объём дополнительной памяти</strong>, используемой алгоритмом, также как функция от <strong>n</strong>. Обычно считают дополнительную память, а не входные данные. Например, <strong>сортировка «на месте»</strong> может иметь <code>O(1)</code> по памяти, а алгоритм, создающий вспомогательный массив размера <strong>n</strong>, — <code>O(n)</code>.</p>
<p>Интуитивно <strong>Big O</strong> отвечает не на вопрос «сколько именно секунд или мегабайт», а на вопрос <strong>«насколько хуже станет, если данных станет в 10 раз больше»</strong>. Именно поэтому нотация широко используется в теории алгоритмов и на практике при выборе подходящего решения.</p>
<p>Часто встречающиеся порядки роста, от лучшего к худшему, выглядят так:</p>
<ul>
<li><strong>константный O(1)</strong> — затраты не зависят от размера входа;</li>
<li><strong>логарифмический O(log n)</strong> — рост очень медленный (типично для бинарного поиска);</li>
<li><strong>линейный O(n)</strong> — один проход по данным;</li>
<li><strong>квазилинейный O(n log n)</strong> — характерен для эффективных сортировок;</li>
<li><strong>квадратичный O(n²)</strong> и выше — быстро становятся непрактичными при больших <strong>n</strong>.</li>
</ul>
<p><a href="https://datatalks.ru/wp-content/uploads/2025/07/big_o_graph.jpeg"><img fetchpriority="high" decoding="async" class="aligncenter size-full wp-image-2735" src="https://datatalks.ru/wp-content/uploads/2025/07/big_o_graph.jpeg" alt="" width="1330" height="950" srcset="https://datatalks.ru/wp-content/uploads/2025/07/big_o_graph.jpeg 1330w, https://datatalks.ru/wp-content/uploads/2025/07/big_o_graph-300x214.jpeg 300w, https://datatalks.ru/wp-content/uploads/2025/07/big_o_graph-1024x731.jpeg 1024w, https://datatalks.ru/wp-content/uploads/2025/07/big_o_graph-768x549.jpeg 768w, https://datatalks.ru/wp-content/uploads/2025/07/big_o_graph-450x321.jpeg 450w, https://datatalks.ru/wp-content/uploads/2025/07/big_o_graph-780x557.jpeg 780w" sizes="(max-width: 1330px) 100vw, 1330px" /></a></p>
<p>Важно понимать, что <strong>Big O</strong> — это верхняя оценка, а не точный прогноз. Она не заменяет профилирование, но позволяет заранее отсеять заведомо плохие алгоритмы и сравнивать решения на концептуальном уровне.</p>
<table>
<thead>
<tr>
<th>Нотация Big O</th>
<th>Название</th>
<th>Описание</th>
<th>Пример алгоритма</th>
</tr>
</thead>
<tbody>
<tr>
<td><strong>O(1)</strong></td>
<td>Константная</td>
<td>Время и/или память не зависят от размера входных данных</td>
<td>Доступ к элементу массива по индексу <code inline="">a[i]</code>, <code inline="">dict[key]</code></td>
</tr>
<tr>
<td><strong>O(log n)</strong></td>
<td>Логарифмическая</td>
<td>Рост очень медленный: при увеличении входа в 2 раза добавляется 1 шаг</td>
<td>Бинарный поиск, операции в сбалансированных деревьях</td>
</tr>
<tr>
<td><strong>O(log² n)</strong></td>
<td>Полилогарифмическая</td>
<td>Чаще всего результат вложенных логарифмических операций</td>
<td>Некоторые алгоритмы на деревьях и графах</td>
</tr>
<tr>
<td><strong>O(√n)</strong></td>
<td>Сублинейная</td>
<td>Медленнее линейной, но быстрее логарифмической</td>
<td>Проверка простоты числа перебором до √n</td>
</tr>
<tr>
<td><strong>O(n)</strong></td>
<td>Линейная</td>
<td>Время пропорционально размеру входа</td>
<td>Линейный поиск, подсчёт суммы элементов</td>
</tr>
<tr>
<td><strong>O(n log n)</strong></td>
<td>Квазилинейная</td>
<td>Типична для эффективных алгоритмов сортировки</td>
<td>Merge sort, Heap sort, Quick sort (в среднем)</td>
</tr>
<tr>
<td><strong>O(n log² n)</strong></td>
<td>Почти квазилинейная</td>
<td>Встречается в более сложных алгоритмах</td>
<td>Некоторые алгоритмы на строках и графах</td>
</tr>
<tr>
<td><strong>O(n²)</strong></td>
<td>Квадратичная</td>
<td>Обычно два вложенных цикла по входным данным</td>
<td>Bubble sort, Insertion sort (в худшем случае)</td>
</tr>
<tr>
<td><strong>O(n³)</strong></td>
<td>Кубическая</td>
<td>Три вложенных цикла, быстро становится непрактичной</td>
<td>Алгоритм Флойда–Уоршелла</td>
</tr>
<tr>
<td><strong>O(nᵏ)</strong></td>
<td>Полиномиальная</td>
<td>Обобщение квадратичной и кубической</td>
<td>Многие алгоритмы из теории NP</td>
</tr>
<tr>
<td><strong>O(2ⁿ)</strong></td>
<td>Экспоненциальная</td>
<td>Удваивается при каждом новом элементе</td>
<td>Перебор всех подмножеств</td>
</tr>
<tr>
<td><strong>O(3ⁿ)</strong></td>
<td>Экспоненциальная</td>
<td>Ещё быстрее растущая экспонента</td>
<td>Некоторые задачи динамики без оптимизации</td>
</tr>
<tr>
<td><strong>O(n!)</strong></td>
<td>Факториальная</td>
<td>Практически неиспользуема для больших n</td>
<td>Задача коммивояжёра перебором</td>
</tr>
<tr>
<td><strong>O(cⁿ)</strong></td>
<td>Экспоненциальная (обобщённая)</td>
<td>Любая константа &gt; 1 в степени n</td>
<td>Brute-force задачи</td>
</tr>
<tr>
<td><strong>O(n + m)</strong></td>
<td>Линейная по входу</td>
<td>Зависит от нескольких параметров</td>
<td>Алгоритмы на графах (вершины + рёбра)</td>
</tr>
<tr>
<td><strong>O(n·m)</strong></td>
<td>Двухпараметрическая</td>
<td>Часто в графах и матрицах</td>
<td>Обход всех пар вершин</td>
</tr>
<tr>
<td><strong>O(1) память</strong></td>
<td>Константная память</td>
<td>Используется фиксированное количество памяти</td>
<td>In-place сортировки</td>
</tr>
<tr>
<td><strong>O(n) память</strong></td>
<td>Линейная память</td>
<td>Дополнительная память пропорциональна входу</td>
<td>Вспомогательный массив</td>
</tr>
<tr>
<td><strong>O(n²) память</strong></td>
<td>Квадратичная память</td>
<td>Хранение матрицы или таблицы</td>
<td>Матрицы смежности</td>
</tr>
</tbody>
</table>
<h3><strong>Шпаргалка по Big O</strong></h3>
<p><a href="https://datatalks.ru/wp-content/uploads/2025/07/shpargalka_big_o-scaled.png"><img decoding="async" class="aligncenter size-full wp-image-2732" src="https://datatalks.ru/wp-content/uploads/2025/07/shpargalka_big_o-scaled.png" alt="" width="2560" height="1841" srcset="https://datatalks.ru/wp-content/uploads/2025/07/shpargalka_big_o-scaled.png 2560w, https://datatalks.ru/wp-content/uploads/2025/07/shpargalka_big_o-300x216.png 300w, https://datatalks.ru/wp-content/uploads/2025/07/shpargalka_big_o-1024x737.png 1024w, https://datatalks.ru/wp-content/uploads/2025/07/shpargalka_big_o-768x552.png 768w, https://datatalks.ru/wp-content/uploads/2025/07/shpargalka_big_o-1536x1105.png 1536w, https://datatalks.ru/wp-content/uploads/2025/07/shpargalka_big_o-2048x1473.png 2048w, https://datatalks.ru/wp-content/uploads/2025/07/shpargalka_big_o-450x324.png 450w, https://datatalks.ru/wp-content/uploads/2025/07/shpargalka_big_o-780x561.png 780w, https://datatalks.ru/wp-content/uploads/2025/07/shpargalka_big_o-1600x1151.png 1600w" sizes="(max-width: 2560px) 100vw, 2560px" /></a></p>
<h3><strong>Профилирование</strong></h3>
<p><strong>Профилирование</strong> — это процесс измерения и анализа фактического поведения программы во время выполнения. Его цель — понять, куда реально тратится время и память, какие части кода являются узкими местами и какие оптимизации действительно имеют смысл.</p>
<p>В отличие от асимптотического анализа и <strong>Big O</strong>, профилирование работает не с абстрактными оценками, а с конкретными метриками: временем выполнения функций, числом вызовов, потреблением памяти, частотой аллокаций, загрузкой CPU. Часто оказывается, что код с хорошей теоретической сложностью работает медленно из-за констант, кэш-промахов, лишних аллокаций или неудачных структур данных — и именно профилирование это выявляет.</p>
<p><strong>Обычно процесс выглядит так:</strong> программу запускают с реальными или приближенными к реальным входными данными, собирают статистику выполнения, затем анализируют отчёт профайлера. Почти всегда подтверждается <strong>правило 80/20</strong>: небольшая часть кода потребляет большую часть ресурсов. Оптимизация без профилирования часто приводит к бессмысленной усложнённости и микроправкам там, где выигрыш минимален.</p>
<p><strong>Различают несколько основных видов профилирования:</strong></p>
<ul>
<li><strong>Временное профилирование</strong> показывает, сколько времени тратится на функции и строки кода.</li>
<li><strong>Профилирование памяти</strong> показывает рост и распределение потребления памяти, утечки и лишние аллокации.</li>
<li><strong>CPU-профилирование</strong> анализирует загрузку процессора и горячие участки кода.</li>
<li>Есть также <strong>событийное</strong> и <strong>трассировочное профилирование</strong>, полезное для сложных систем.</li>
</ul>
<p>В контексте Python профилирование особенно важно, потому что язык интерпретируемый, и реальные узкие места часто находятся не там, где их ожидают по Big O. Стандартный модуль <strong>cProfile</strong> позволяет получить детальный отчёт по времени выполнения функций, <code>line_profiler</code> показывает стоимость отдельных строк, а <code>memory_profiler</code> помогает анализировать использование памяти.</p>
<p><strong>Ключевая идея проста:</strong> <strong>Big O</strong> говорит, как алгоритм будет масштабироваться, а <strong>профилирование</strong> показывает, как он работает на самом деле. В хорошей инженерной практике <strong>сначала пишут корректный и понятный код, затем профилируют</strong>, и только после этого оптимизируют то, что действительно мешает производительности.</p>
<h3><strong>Динамическое программирование</strong></h3>
<p><strong>Динамическое программирование (dynamic programming, DP)</strong> — это метод проектирования алгоритмов, который применяется к задачам, где решение можно разложить на перекрывающиеся подзадачи, а итоговый ответ строится из решений этих подзадач. Ключевая идея: не решать одну и ту же подзадачу больше одного раза.</p>
<p>Если кратко: рекурсия даёт корректность, а динамическое программирование — эффективность.</p>
<p>Когда задача решается динамическим программированием</p>
<p>У задачи должны быть два свойства.</p>
<p>Первое — оптимальная подструктура. Это означает, что оптимальное решение всей задачи можно получить из оптимальных решений её подзадач. Например, кратчайший путь до точки i проходит через кратчайший путь до предыдущих точек.</p>
<p>Второе — перекрывающиеся подзадачи. Одни и те же подзадачи возникают многократно. Обычная рекурсия будет пересчитывать их снова и снова, а DP — сохраняет и переиспользует результат.</p>
<p>Если хотя бы одно из этих свойств отсутствует, динамическое программирование либо не нужно, либо не применимо.</p>
<h4>Основные подходы DP</h4>
<p><strong>Top-down (memoization)</strong></p>
<p>Решение пишется как рекурсивная функция, но результаты вызовов сохраняются в кэше. При повторном обращении к подзадаче результат берётся из памяти, а не пересчитывается.</p>
<p>Плюс: код близок к математическому определению.<br />
Минус: накладные расходы рекурсии и риск переполнения стека.</p>
<p><strong>Bottom-up (tabulation)</strong></p>
<p>Решение строится итеративно, начиная с самых простых подзадач и постепенно переходя к более сложным.</p>
<p>Плюс: нет рекурсии, обычно быстрее и надёжнее.<br />
Минус: иногда сложнее понять и вывести формулы.</p>
<p>Оба подхода эквивалентны по асимптотике.</p>
<h4><strong>Классический пример: числа Фибоначчи</strong></h4>
<p>Наивная рекурсия имеет экспоненциальную сложность, потому что одно и то же значение вычисляется много раз.</p>
<p>DP-решение хранит уже вычисленные значения и получает сложность O(n) по времени. При этом память можно сократить до O(1), храня только два последних значения. Это важный момент: DP часто можно оптимизировать по памяти.</p>
<h4><strong>Как выглядит DP-решение концептуально</strong></h4>
<p>Почти любая задача DP решается по одному шаблону:</p>
<p>Определяется состояние DP — что именно мы храним (например, dp[i] — ответ для первых i элементов).</p>
<p>Формулируется переход — как получить dp[i] из более простых состояний.</p>
<p>Задаются базовые случаи.</p>
<p>Определяется порядок вычислений.</p>
<p>При необходимости оптимизируется память.</p>
<p>Умение правильно выбрать состояние — самый сложный этап.</p>
<h4><strong>Основные типы задач динамического программирования</strong></h4>
<p>DP на одномерном массиве<br />
Примеры: Фибоначчи, максимальная сумма подмассива, лестница с шагами.</p>
<p>DP на двумерной таблице<br />
Примеры: наибольшая общая подпоследовательность (LCS), расстояние Левенштейна, рюкзак.</p>
<p>DP на строках<br />
Сравнение строк, редактирование, палиндромы.</p>
<p>DP на деревьях (Tree DP)<br />
Решения строятся в postorder-обходе. Примеры: диаметр дерева, максимальный путь.</p>
<p>DP по подмножествам (Bitmask DP)<br />
Используется, когда количество элементов мало (обычно до 20). Пример: задача коммивояжёра.</p>
<p>DP с оптимизацией переходов<br />
Монотонная очередь, divide and conquer DP, convex hull trick — продвинутые темы для сильных кандидатов.</p>
<h4><strong>Временная и пространственная сложность</strong></h4>
<p>Сложность DP обычно равна количеству состояний, умноженному на стоимость перехода.<br />
Например, таблица n × m с O(1) переходом даёт O(nm) по времени и памяти.</p>
<p>Очень частая ошибка — недооценивать размер пространства состояний и получить решение, которое не помещается в память или не укладывается по времени.</p>
<h4><strong>Типичные ошибки на собеседованиях</strong></h4>
<p>Писать рекурсию без мемоизации.<br />
Выбирать слишком большое состояние DP.<br />
Не замечать, что задачу можно решить жадно или через бинарный поиск.<br />
Забывать про оптимизацию памяти, когда она очевидна.</p>
<h4><strong>Как понять, что тут DP</strong></h4>
<p>Фразы-триггеры в условиях:<br />
«найти максимальный/минимальный…»,<br />
«сколькими способами…»,<br />
«оптимальное решение»,<br />
«учитывая предыдущие шаги».</p>
<p>Если решение зависит от предыдущих решений и эти зависимости повторяются — почти наверняка это динамическое программирование.</p>
<h4><strong>Резюме</strong></h4>
<p>Динамическое программирование — это не конкретный алгоритм, а образ мышления. Ты разбиваешь задачу на подзадачи, сохраняешь результаты и собираешь ответ снизу вверх или сверху вниз. Именно поэтому DP считается одной из самых сложных, но и самых мощных тем на собеседованиях.</p>
<h2><strong>Алгоритмы сортировки</strong></h2>
<p><strong>Bubble Sort</strong> — одна из самых простых и наименее эффективных сортировок. Алгоритм многократно проходит по массиву и сравнивает соседние элементы, меняя их местами, если они стоят в неправильном порядке. После каждого прохода самый большой элемент «всплывает» в конец массива, отсюда и название. В худшем и среднем случае сложность по времени составляет O(n²), так как выполняются вложенные проходы. В лучшем случае (если массив уже отсортирован и есть оптимизация с флагом) — O(n). По памяти алгоритм работает за O(1), так как сортирует массив на месте. Bubble Sort стабилен, но практически не используется из-за низкой эффективности.</p>
<p><strong>Selection Sort</strong> работает иначе: на каждом шаге он находит минимальный элемент в неотсортированной части массива и ставит его на правильную позицию. Число сравнений всегда одинаково, независимо от входных данных, поэтому и лучший, и худший, и средний случаи имеют сложность O(n²). Память — O(1), так как сортировка выполняется на месте. Алгоритм не является стабильным, но имеет предсказуемое поведение и минимальное число обменов, что иногда полезно при дорогих операциях записи.</p>
<p><strong>Insertion Sort</strong> имитирует процесс сортировки карт вручную. Алгоритм берёт элементы по одному и вставляет каждый новый элемент в уже отсортированную часть массива, сдвигая большие элементы вправо. В худшем и среднем случае сложность O(n²), но в лучшем случае, когда массив почти отсортирован, — O(n). По памяти — O(1). Insertion Sort стабилен и очень эффективен на маленьких массивах или как вспомогательная сортировка внутри более сложных алгоритмов.</p>
<p><strong>QuickSort</strong> — одна из самых популярных и быстрых сортировок на практике. Он использует стратегию «разделяй и властвуй»: выбирается опорный элемент (pivot), массив разбивается на элементы меньше и больше него, после чего рекурсивно сортируются подмассивы. Средняя сложность по времени — O(n log n), но в худшем случае (неудачный выбор pivot, например уже отсортированный массив) — O(n²). Память обычно O(log n) из-за глубины рекурсии. QuickSort не стабилен, но выигрывает за счёт хорошей локальности данных и отсутствия дополнительной памяти.</p>
<p><strong>Merge Sort</strong> также основан на «разделяй и властвуй». Массив рекурсивно делится пополам, затем отсортированные половины сливаются в один отсортированный массив. В отличие от QuickSort, Merge Sort всегда работает за O(n log n), независимо от входных данных. Основной минус — необходимость дополнительной памяти O(n) для слияния. Алгоритм стабилен и хорошо подходит для сортировки связанных списков и внешней сортировки (когда данные не помещаются в память).</p>
<p><strong>Heap Sort</strong> использует структуру данных «куча» (обычно бинарная max-heap). Сначала массив преобразуется в кучу, затем максимальный элемент извлекается и помещается в конец массива, после чего куча перестраивается. Сложность по времени — O(n log n) во всех случаях, память — O(1), так как сортировка выполняется на месте. Heap Sort не стабилен и на практике часто медленнее QuickSort из-за плохой кэш-локальности, но ценится за гарантированное время работы.</p>
<p><strong>Counting Sort</strong> — не сравнительная сортировка. Она работает только для целочисленных ключей из ограниченного диапазона. Алгоритм подсчитывает, сколько раз встречается каждое значение, а затем по этим подсчётам восстанавливает отсортированный массив. Время работы — O(n + k), где k — размер диапазона значений. Память также O(n + k). Counting Sort стабилен и очень быстр, но применим только при небольшом диапазоне ключей.</p>
<p><strong>Radix Sort</strong> — тоже не сравнительная сортировка. Она сортирует числа (или строки) поразрядно, начиная с младшего или старшего разряда, используя стабильную сортировку (часто Counting Sort) на каждом шаге. Если число разрядов равно d, а диапазон разряда k, то сложность — O(d · (n + k)). Память — O(n + k). Radix Sort эффективен для больших массивов чисел фиксированной длины, но менее универсален, чем сравнительные алгоритмы.</p>
<h2><strong>Алгоритмы обхода деревьев</strong></h2>
<p><strong>Бинарное дерево</strong> — это иерархическая структура данных, состоящая из узлов, где у каждого узла не более двух потомков. Эти потомки обычно называют левым и правым ребёнком. Структура начинается с одного выделенного узла — корня, от которого рекурсивно строятся все остальные узлы.</p>
<p><a href="https://datatalks.ru/wp-content/uploads/2025/07/binary_tree.jpeg"><img decoding="async" class="aligncenter size-full wp-image-2740" src="https://datatalks.ru/wp-content/uploads/2025/07/binary_tree.jpeg" alt="" width="512" height="357" srcset="https://datatalks.ru/wp-content/uploads/2025/07/binary_tree.jpeg 512w, https://datatalks.ru/wp-content/uploads/2025/07/binary_tree-300x209.jpeg 300w, https://datatalks.ru/wp-content/uploads/2025/07/binary_tree-450x314.jpeg 450w" sizes="(max-width: 512px) 100vw, 512px" /></a></p>
<p>Каждый <strong>узел</strong> бинарного дерева содержит <strong>значение (данные)</strong> и <strong>ссылки</strong> на своих детей. Если у <strong>узла</strong> нет детей, он называется <strong>листом</strong>. Если у узла есть хотя бы один ребёнок, он считается <strong>внутренним узлом</strong>. Важная особенность бинарного дерева в том, что порядок детей имеет значение: <strong>левый</strong> и <strong>правый потомок</strong> — это разные позиции, даже если они содержат одинаковые значения.</p>
<p><strong>Бинарное дерево удобно описывать рекурсивно:</strong> дерево либо пусто, либо состоит из корня, левого поддерева и правого поддерева, каждое из которых само является бинарным деревом. Это рекурсивное определение напрямую связано с тем, как такие структуры обрабатываются в алгоритмах.</p>
<p>Важно отличать бинарное дерево от других похожих структур:</p>
<ul>
<li><strong>Бинарное дерево</strong> — это общее понятие, не накладывающее ограничений на значения в узлах.</li>
<li><strong>Бинарное дерево поиска (BST)</strong> — это частный случай, где для каждого узла все значения в левом поддереве меньше (или равны), а в правом — больше (или равны) значения узла.</li>
<li>Также существуют <strong>полные</strong>, <strong>совершенные</strong>, <strong>сбалансированные</strong> бинарные деревья — это уже свойства формы, а не данных.</li>
</ul>
<p>С точки зрения формы бинарное дерево может быть очень разным. Оно может быть вырожденным, когда каждый узел имеет только одного потомка, и тогда структура по сути превращается в список. В таком случае высота дерева равна количеству узлов, и многие алгоритмы работают медленно. В противоположность этому, сбалансированное бинарное дерево имеет высоту порядка log n, что делает операции поиска и обхода эффективными.</p>
<p>Бинарные деревья широко используются потому, что они <strong>хорошо отражают иерархии</strong> и позволяют <strong>эффективно реализовывать алгоритмы</strong>. На их основе строятся <strong>деревья поиска</strong>, <strong>кучи</strong>, <strong>синтаксические деревья выражений</strong>, <strong>структуры для парсинга</strong>, <strong>индексы в базах данных</strong> и множество алгоритмических задач на собеседованиях.</p>
<p>Под деревом ниже подразумевается в первую очередь <strong>бинарное дерево</strong>, но большинство идей обобщаются и на <strong>n-арные деревья</strong>.</p>
<h3><strong>Depth-First Search (DFS) — обход в глубину</strong></h3>
<p>Это общий класс обходов, при котором алгоритм уходит как можно глубже по одной ветке, прежде чем возвращаться назад. Почти всегда реализуется рекурсивно или с помощью стека. Все классические <strong>«in/pre/post-order»</strong> — это частные случаи <strong>DFS</strong>. Временная сложность любого DFS — <code>O(n)</code>, память — <code>O(h)</code>, где h — высота дерева (в худшем случае <code>O(n)</code>).</p>
<p><strong>DFS</strong> часто спрашивают концептуально: чем отличается от BFS, где используется стек, где рекурсия, какие риски (переполнение стека).</p>
<h3><strong>Preorder Traversal (прямой обход)</strong></h3>
<p><strong>Порядок: </strong>сначала текущий узел → затем левое поддерево → затем правое поддерево.</p>
<p><strong>Идея:</strong> «обработать узел до его детей».<br />
Используется для сериализации дерева, копирования структуры, построения выражений в префиксной форме.</p>
<p><strong>Сложность: </strong>время O(n), память O(h).<br />
Реализуется рекурсивно или итеративно через стек.</p>
<p>На собеседованиях часто спрашивают: как восстановить дерево по preorder + inorder.</p>
<h3><strong>Inorder Traversal (симметричный обход)</strong></h3>
<p><strong>Порядок:</strong> левое поддерево → текущий узел → правое поддерево.</p>
<p><strong>Ключевое свойство:</strong> для бинарного дерева поиска (BST) inorder-обход выдаёт отсортированную последовательность.</p>
<p>Это один из самых любимых обходов интервьюеров, потому что он напрямую связан с BST, проверкой корректности дерева поиска, поиском k-го минимального элемента.</p>
<p><strong>Сложность стандартная:</strong> O(n) по времени, O(h) по памяти.</p>
<h3><strong>Postorder Traversal (обратный обход)</strong></h3>
<p>Порядок: левое поддерево → правое поддерево → текущий узел.</p>
<p>Идея: «обработать узел после детей».<br />
Используется для удаления дерева, освобождения памяти, вычисления выражений в постфиксной форме, задач динамического программирования на деревьях (когда результат узла зависит от детей).</p>
<p>Часто спрашивают как более «неочевидный» обход, особенно в итеративной реализации.</p>
<h3><strong>DFS с явным стеком (итеративный DFS)</strong></h3>
<p>Любой из трёх обходов выше можно реализовать без рекурсии, используя стек. Это важно для языков с ограниченным стеком вызовов.</p>
<p>На собеседованиях могут спросить:<br />
почему рекурсия = стек,<br />
как переписать рекурсивный обход в итеративный,<br />
чем они отличаются по памяти.</p>
<h3><strong>Morris Traversal</strong></h3>
<p>Это продвинутый вариант inorder (иногда preorder), который выполняется за O(1) дополнительной памяти.</p>
<p>Идея: временно «перепрошивать» указатели дерева, чтобы избежать стека и рекурсии, а потом восстанавливать структуру.</p>
<p>Время — O(n), память — O(1).<br />
Алгоритм сложный и редко используется на практике, но иногда встречается как «вопрос со звёздочкой» для сильных кандидатов.</p>
<h3><strong>Breadth-First Search (BFS) / Level Order Traversal — обход в ширину</strong></h3>
<p>Обход по уровням: сначала корень, затем все узлы уровня 1, затем уровня 2 и так далее.</p>
<p>Реализуется с помощью очереди, а не стека.<br />
Время — O(n), память — O(w), где w — максимальная ширина дерева (в худшем случае O(n)).</p>
<p>BFS часто спрашивают в задачах на уровни дерева:<br />
вывод по уровням,<br />
максимальная/минимальная глубина,<br />
зигзагообразный обход,<br />
правый или левый вид дерева.</p>
<h3><strong>Level Order с вариантами</strong></h3>
<p>Это не отдельные алгоритмы, а частые модификации BFS, которые почти гарантированно встречаются на собеседованиях:</p>
<ul>
<li>обход по уровням с группировкой значений</li>
<li>zigzag level order</li>
<li>обход с указанием уровня</li>
<li>поиск первого элемента, удовлетворяющего условию</li>
</ul>
<p>Важно понимать, что все они — один и тот же BFS с небольшой логикой поверх.</p>
<h3><strong>Обход n-арного дерева</strong></h3>
<p>Концептуально тот же <strong>DFS</strong> или <strong>BFS</strong>, но вместо левого и правого поддерева — список детей. На собеседованиях проверяют, понимаешь ли ты, что логика не меняется, меняется только цикл по детям.</p>
<h3><strong>Обход с возвратом значения (Tree DP)</strong></h3>
<p>Часто интервьюеры не называют это «обходом», но по сути это <strong>postorder DFS</strong>, где функция возвращает значение наверх: высоту, сумму, флаг, максимум и т.д.</p>
<p><strong>Примеры задач:</strong></p>
<ul>
<li>высота дерева</li>
<li>диаметр дерева</li>
<li>проверка сбалансированности</li>
<li>максимальный путь</li>
</ul>
<p>Это один из самых важных практических паттернов.</p>
<h2><strong>Алгоритмы поиска чисел</strong></h2>
<p>Под «поиском чисел» здесь понимается поиск элемента в массиве, диапазоне или числовом пространстве.</p>
<h3><strong>Линейный поиск (Linear Search)</strong></h3>
<p>Это самый простой и универсальный алгоритм поиска. Он последовательно проверяет элементы массива один за другим, пока не найдёт нужное значение или не дойдёт до конца.</p>
<p>Алгоритм не делает никаких предположений о структуре данных: массив может быть неотсортированным, произвольным, даже потоковым. Именно поэтому линейный поиск часто используется как базовый вариант или когда данные малы.</p>
<p>Временная сложность в худшем и среднем случае — O(n), в лучшем — O(1), если искомый элемент находится в начале. Память — O(1).</p>
<p>Пример: поиск числа в неотсортированном списке, поиск первого элемента, удовлетворяющего условию.</p>
<h3><strong>Бинарный поиск (Binary Search)</strong></h3>
<p>Бинарный поиск работает только с отсортированными данными. Алгоритм многократно делит диапазон поиска пополам: сравнивает искомое число с элементом в середине массива и отбрасывает половину, где элемента быть не может.</p>
<p>Ключевая идея — уменьшать пространство поиска в два раза на каждом шаге.</p>
<p>Временная сложность — O(log n) во всех случаях, память — O(1) в итеративной реализации или O(log n) при рекурсии.</p>
<p>Бинарный поиск — один из самых частых алгоритмов на собеседованиях. Его спрашивают не только напрямую, но и в виде вариаций: поиск первого/последнего вхождения, нижняя и верхняя границы, поиск по условию.</p>
<p>Пример: поиск числа в отсортированном массиве, поиск позиции вставки.</p>
<h3><strong>Поиск по ответу (Binary Search on Answer)</strong></h3>
<p>Это обобщение бинарного поиска, но применяется не к массиву, а к пространству возможных ответов. Используется, когда есть монотонное условие вида: «если ответ X подходит, то все ответы больше (или меньше) тоже подходят».</p>
<p>Алгоритм не ищет конкретное число в массиве, а подбирает минимальное или максимальное значение, удовлетворяющее условию.</p>
<p>Сложность — O(log R), где R — диапазон возможных значений, умноженная на стоимость проверки условия.</p>
<p>Очень популярный паттерн на собеседованиях.</p>
<p>Пример: найти минимальную скорость, за которую можно выполнить работу за заданное время; найти минимальный размер, при котором условие выполняется.</p>
<h3><strong>Интерполяционный поиск (Interpolation Search)</strong></h3>
<p>Интерполяционный поиск — улучшенная версия бинарного поиска для равномерно распределённых чисел. Вместо середины массива он вычисляет позицию элемента пропорционально значению искомого числа.</p>
<p>Если данные действительно равномерны, алгоритм работает очень быстро.</p>
<p>Средняя сложность — O(log log n), худшая — O(n). Память — O(1).</p>
<p>На практике используется редко, но иногда появляется в теоретических вопросах.</p>
<h3><strong>Jump Search</strong></h3>
<p>Алгоритм разбивает отсортированный массив на блоки фиксированного размера и «прыгает» по ним, пока не найдёт блок, в котором может находиться элемент. Затем выполняется линейный поиск внутри блока.</p>
<p>Оптимальный размер блока — √n, что даёт временную сложность O(√n).</p>
<p>Используется редко, но полезен как промежуточный вариант между линейным и бинарным поиском.</p>
<h3><strong>Exponential Search</strong></h3>
<p>Этот алгоритм применяется для поиска в очень больших или потенциально бесконечных отсортированных массивах. Сначала он экспоненциально увеличивает диапазон поиска (1, 2, 4, 8, …), пока не превысит искомое значение, а затем запускает бинарный поиск в найденном диапазоне.</p>
<p>Сложность — O(log n), память — O(1).</p>
<p>Пример: поиск в массиве неизвестного размера, потоках данных.</p>
<h3><strong>Поиск в хеш-таблице</strong></h3>
<p>Формально это не алгоритм поиска чисел в массиве, но на практике — один из самых частых способов поиска.</p>
<p>Идея: число преобразуется хеш-функцией в индекс, по которому элемент хранится в таблице.</p>
<p>Средняя сложность — O(1), худшая — O(n) при большом количестве коллизий. Память — O(n).</p>
<p>Пример: поиск числа в set или dict в Python.</p>
<h3><strong>Поиск в бинарном дереве поиска (BST)</strong></h3>
<p>Если числа хранятся в бинарном дереве поиска, то поиск идёт по тому же принципу, что и бинарный поиск: на каждом шаге выбирается левое или правое поддерево.</p>
<p>Средняя сложность — O(log n), но в худшем случае (несбалансированное дерево) — O(n). Память — O(h), где h — высота дерева.</p>
<p>Пример: поиск элемента в сбалансированном дереве (AVL, Red-Black Tree).</p>
<h3><strong>Линейный поиск с двумя указателями</strong></h3>
<p>Часто используется в задачах с отсортированными массивами. Вместо поиска одного числа ищется пара, сумма, разность или другое условие.</p>
<p>Алгоритм движется с двух концов массива навстречу друг другу.</p>
<p>Сложность — O(n), память — O(1).</p>
<p>Пример: найти два числа с заданной суммой.</p>
<h3><strong>Поиск с использованием битовых операций</strong></h3>
<p>Применяется в узких задачах: поиск уникального числа, поиск отсутствующего числа, поиск дубликата при ограничениях.</p>
<p>Алгоритмы используют свойства XOR, битовых масок и арифметики.</p>
<p>Сложность — O(n), память — O(1).</p>
<p>Пример: найти число, которое встречается один раз, когда остальные встречаются дважды.</p>
<h3><strong>Важное резюме для собеседований</strong></h3>
<p>Если данные не отсортированы — начинай с линейного поиска или хеш-таблицы.<br />
Если данные отсортированы — почти всегда подходит бинарный поиск или его вариации.<br />
Если ищешь минимальный/максимальный ответ по условию — это бинарный поиск по ответу.<br />
Если ограничения на память жёсткие — избегай хеш-таблиц.</p>
<p>Сообщение <a href="https://datatalks.ru/data-structures-and-algorithms/">Введение в Структуры данных (Data Structures) и алгоритмы</a> появились сначала на <a href="https://datatalks.ru">DataTalks.RU. Data Engineering / DWH / Data Pipeline</a>.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://datatalks.ru/data-structures-and-algorithms/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
	</channel>
</rss>
