This commit is contained in:
krahets
2026-04-14 18:06:19 +08:00
parent 17b2a0b630
commit cf0747ba3e
131 changed files with 604 additions and 609 deletions
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+1 -1
View File
@@ -251,4 +251,4 @@
initAutoSlide();
}
})();
/*! update cache: 20260410225926 */
/*! update cache: 20260414173614 */
+1 -1
View File
@@ -8,4 +8,4 @@ document$.subscribe(({ body }) => {
],
});
});
/*! update cache: 20260410225926 */
/*! update cache: 20260414173614 */
+1 -1
View File
@@ -15,4 +15,4 @@ window.MathJax = {
document$.subscribe(() => {
MathJax.typesetPromise();
});
/*! update cache: 20260410225926 */
/*! update cache: 20260414173614 */
+1 -1
View File
@@ -469,4 +469,4 @@
return Starfield;
});
/*! update cache: 20260410225926 */
/*! update cache: 20260414173614 */
+1 -1
View File
@@ -176,4 +176,4 @@
font-size: 0.7rem;
}
}
/*! update cache: 20260410225926 */
/*! update cache: 20260414173614 */
+1 -1
View File
@@ -806,4 +806,4 @@ a:hover .device-on-hover {
margin: 0 0 1em;
}
}
/*! update cache: 20260410225926 */
/*! update cache: 20260414173614 */
+1 -1
View File
@@ -122,4 +122,4 @@ main .gsc-loading-image {
.gsc-reply-content::-webkit-scrollbar-track {
background: transparent;
}
/*! update cache: 20260410225926 */
/*! update cache: 20260414173614 */
+1 -1
View File
@@ -153,4 +153,4 @@ main {
.gsc-reply-content::-webkit-scrollbar-track {
background: transparent;
}
/*! update cache: 20260410225926 */
/*! update cache: 20260414173614 */
File diff suppressed because one or more lines are too long
+1 -1
View File
@@ -251,4 +251,4 @@
initAutoSlide();
}
})();
/*! update cache: 20260410225937 */
/*! update cache: 20260414173625 */
+1 -1
View File
@@ -8,4 +8,4 @@ document$.subscribe(({ body }) => {
],
});
});
/*! update cache: 20260410225937 */
/*! update cache: 20260414173625 */
+1 -1
View File
@@ -15,4 +15,4 @@ window.MathJax = {
document$.subscribe(() => {
MathJax.typesetPromise();
});
/*! update cache: 20260410225937 */
/*! update cache: 20260414173625 */
+1 -1
View File
@@ -469,4 +469,4 @@
return Starfield;
});
/*! update cache: 20260410225937 */
/*! update cache: 20260414173625 */
+1 -1
View File
@@ -176,4 +176,4 @@
font-size: 0.7rem;
}
}
/*! update cache: 20260410225937 */
/*! update cache: 20260414173625 */
+1 -1
View File
@@ -806,4 +806,4 @@ a:hover .device-on-hover {
margin: 0 0 1em;
}
}
/*! update cache: 20260410225937 */
/*! update cache: 20260414173625 */
+1 -1
View File
@@ -122,4 +122,4 @@ main .gsc-loading-image {
.gsc-reply-content::-webkit-scrollbar-track {
background: transparent;
}
/*! update cache: 20260410225937 */
/*! update cache: 20260414173625 */
+1 -1
View File
@@ -153,4 +153,4 @@ main {
.gsc-reply-content::-webkit-scrollbar-track {
background: transparent;
}
/*! update cache: 20260410225937 */
/*! update cache: 20260414173625 */
+1 -1
View File
@@ -251,4 +251,4 @@
initAutoSlide();
}
})();
/*! update cache: 20260410225905 */
/*! update cache: 20260414173552 */
+1 -1
View File
@@ -8,4 +8,4 @@ document$.subscribe(({ body }) => {
],
});
});
/*! update cache: 20260410225905 */
/*! update cache: 20260414173552 */
+1 -1
View File
@@ -15,4 +15,4 @@ window.MathJax = {
document$.subscribe(() => {
MathJax.typesetPromise();
});
/*! update cache: 20260410225905 */
/*! update cache: 20260414173552 */
+1 -1
View File
@@ -469,4 +469,4 @@
return Starfield;
});
/*! update cache: 20260410225905 */
/*! update cache: 20260414173552 */
File diff suppressed because one or more lines are too long
+4 -4
View File
@@ -4387,11 +4387,11 @@
<p>В этой же открытой книге цикл обновления содержания сокращается до нескольких дней, а иногда даже до нескольких часов.</p>
</div>
<h3 id="1">1. &nbsp; Небольшие правки содержания<a class="headerlink" href="#1" title="Permanent link">&para;</a></h3>
<p>Как показано на рисунке 16-3, в правом верхнем углу каждой страницы есть "значок редактирования". Текст или код можно изменить следующим образом.</p>
<p>Как показано на рисунке 16-3, в правом верхнем углу каждой страницы есть «значок редактирования». Текст или код можно изменить следующим образом.</p>
<ol>
<li>Нажмите на "значок редактирования". Если появится сообщение "You need to fork this repository", согласитесь с этим действием.</li>
<li>Нажмите на «значок редактирования». Если появится сообщение «You need to fork this repository», согласитесь с этим действием.</li>
<li>Измените содержимое исходного Markdown-файла, проверьте корректность правок и постарайтесь сохранить единый стиль оформления.</li>
<li>Внизу страницы заполните описание изменений, затем нажмите кнопку "Propose file change". После перехода на следующую страницу нажмите кнопку "Create pull request", чтобы отправить pull request.</li>
<li>Внизу страницы заполните описание изменений, затем нажмите кнопку «Propose file change». После перехода на следующую страницу нажмите кнопку «Create pull request», чтобы отправить pull request.</li>
</ol>
<p><img alt="Кнопка редактирования страницы" class="animation-figure" src="../contribution.assets/edit_markdown.png" /></p>
<p align="center"> Рисунок 16-3 &nbsp; Кнопка редактирования страницы </p>
@@ -4404,7 +4404,7 @@
<li>Перейдите на страницу своего Fork-репозитория и с помощью команды <code>git clone</code> клонируйте репозиторий локально.</li>
<li>Создавайте и редактируйте содержание локально, затем проведите полное тестирование и проверьте корректность кода.</li>
<li>Зафиксируйте локальные изменения, после чего выполните Push в удаленный репозиторий.</li>
<li>Обновите страницу репозитория и нажмите кнопку "Create pull request", чтобы инициировать pull request.</li>
<li>Обновите страницу репозитория и нажмите кнопку «Create pull request», чтобы инициировать pull request.</li>
</ol>
<h3 id="3-docker">3. &nbsp; Развертывание Docker<a class="headerlink" href="#3-docker" title="Permanent link">&para;</a></h3>
<p>В корневом каталоге <code>hello-algo</code> выполните следующий Docker-скрипт, после чего проект станет доступен по адресу <code>http://localhost:8000</code>:</p>
+2 -2
View File
@@ -4594,7 +4594,7 @@
<p><img alt="Загрузка VS Code с официального сайта" class="animation-figure" src="../installation.assets/vscode_installation.png" /></p>
<p align="center"> Рисунок 16-1 &nbsp; Загрузка VS Code с официального сайта </p>
<p>VS Code обладает мощной экосистемой расширений и поддерживает выполнение и отладку большинства языков программирования. Например, после установки расширения "Python Extension Pack" можно отлаживать код на Python. Процесс установки показан на рисунке 16-2.</p>
<p>VS Code обладает мощной экосистемой расширений и поддерживает выполнение и отладку большинства языков программирования. Например, после установки расширения «Python Extension Pack» можно отлаживать код на Python. Процесс установки показан на рисунке 16-2.</p>
<p><img alt="Установка расширений VS Code" class="animation-figure" src="../installation.assets/vscode_extension_installation.png" /></p>
<p align="center"> Рисунок 16-2 &nbsp; Установка расширений VS Code </p>
@@ -4607,7 +4607,7 @@
</ol>
<h3 id="2-cc">2. &nbsp; Среда C/C++<a class="headerlink" href="#2-cc" title="Permanent link">&para;</a></h3>
<ol>
<li>В Windows требуется установить <a href="https://sourceforge.net/projects/mingw-w64/files/">MinGW</a> (<a href="https://blog.csdn.net/qq_33698226/article/details/129031241">руководство по настройке</a>); в macOS компилятор Clang уже установлен по умолчанию.</li>
<li>В Windows требуется установить <a href="https://sourceforge.net/projects/mingw-w64/files/">MinGW</a> (<a href="https://blog.csdn.net/qq_33698226/article/details/129031241">руководство по настройке</a>). В macOS компилятор Clang уже установлен по умолчанию.</li>
<li>В магазине расширений VS Code найдите <code>c++</code> и установите C/C++ Extension Pack.</li>
<li>(Необязательно) Откройте страницу Settings, найдите параметр форматирования <code>Clang_format_fallback Style</code> и задайте значение <code>{ BasedOnStyle: Microsoft, BreakBeforeBraces: Attach }</code>.</li>
</ol>
@@ -4651,11 +4651,11 @@
<p>https://pythontutor.com/render.html#code=%23%20%D0%98%D0%BD%D0%B8%D1%86%D0%B8%D0%B0%D0%BB%D0%B8%D0%B7%D0%B8%D1%80%D0%BE%D0%B2%D0%B0%D1%82%D1%8C%20%D0%BC%D0%B0%D1%81%D1%81%D0%B8%D0%B2%0Aarr%20%3D%20%5B0%5D%20%2A%205%20%20%23%20%5B%200%2C%200%2C%200%2C%200%2C%200%20%5D%0Anums%20%3D%20%5B1%2C%203%2C%202%2C%205%2C%204%5D&amp;cumulative=false&amp;curInstr=0&amp;heapPrimitives=nevernest&amp;mode=display&amp;origin=opt-frontend.js&amp;py=311&amp;rawInputLstJSON=%5B%5D&amp;textReferences=false</p>
</details>
<h3 id="2">2. &nbsp; Доступ к элементам<a class="headerlink" href="#2" title="Permanent link">&para;</a></h3>
<p>Элементы массива хранятся в непрерывной области памяти, что упрощает вычисление их адресов. Зная адрес массива в памяти (то есть адрес первого элемента) и индекс некоторого элемента, мы можем по формуле с рисунка ниже вычислить адрес этого элемента и напрямую обратиться к нему.</p>
<p>Элементы массива хранятся в непрерывной области памяти, что упрощает вычисление их адресов. Зная адрес массива в памяти (то есть адрес первого элемента) и индекс некоторого элемента, мы можем вычислить адрес этого элемента по формуле, показанной на рисунке 4-2, и напрямую обратиться к нему.</p>
<p><img alt="Вычисление адреса элемента массива" class="animation-figure" src="../array.assets/array_memory_location_calculation.png" /></p>
<p align="center"> Рисунок 4-2 &nbsp; Вычисление адреса элемента массива </p>
<p>Если посмотреть на рисунок 4-2, можно заметить, что индекс первого элемента массива равен <span class="arithmatex">\(0\)</span> , и это кажется не слишком интуитивным, ведь естественнее было бы начинать счет с <span class="arithmatex">\(1\)</span> . Однако с точки зрения формулы адресации <strong>индекс по сути является смещением относительно адреса памяти</strong>. Смещение первого элемента равно <span class="arithmatex">\(0\)</span> , поэтому индекс <span class="arithmatex">\(0\)</span> полностью логичен.</p>
<p>Как видно на рисунке 4-2, индекс первого элемента массива равен <span class="arithmatex">\(0\)</span> , и это кажется не слишком интуитивным, ведь естественнее было бы начинать счет с <span class="arithmatex">\(1\)</span> . Однако с точки зрения формулы адресации <strong>индекс по сути является смещением относительно адреса памяти</strong>. Смещение первого элемента равно <span class="arithmatex">\(0\)</span> , поэтому индекс <span class="arithmatex">\(0\)</span> полностью логичен.</p>
<p>Доступ к элементам массива очень эффективен: любой элемент массива можно получить за <span class="arithmatex">\(O(1)\)</span> времени.</p>
<div class="tabbed-set tabbed-alternate" data-tabs="2:13"><input checked="checked" id="__tabbed_2_1" name="__tabbed_2" type="radio" /><input id="__tabbed_2_2" name="__tabbed_2" type="radio" /><input id="__tabbed_2_3" name="__tabbed_2" type="radio" /><input id="__tabbed_2_4" name="__tabbed_2" type="radio" /><input id="__tabbed_2_5" name="__tabbed_2" type="radio" /><input id="__tabbed_2_6" name="__tabbed_2" type="radio" /><input id="__tabbed_2_7" name="__tabbed_2" type="radio" /><input id="__tabbed_2_8" name="__tabbed_2" type="radio" /><input id="__tabbed_2_9" name="__tabbed_2" type="radio" /><input id="__tabbed_2_10" name="__tabbed_2" type="radio" /><input id="__tabbed_2_11" name="__tabbed_2" type="radio" /><input id="__tabbed_2_12" name="__tabbed_2" type="radio" /><input id="__tabbed_2_13" name="__tabbed_2" type="radio" /><div class="tabbed-labels"><label for="__tabbed_2_1">Python</label><label for="__tabbed_2_2">C++</label><label for="__tabbed_2_3">Java</label><label for="__tabbed_2_4">C#</label><label for="__tabbed_2_5">Go</label><label for="__tabbed_2_6">Swift</label><label for="__tabbed_2_7">JS</label><label for="__tabbed_2_8">TS</label><label for="__tabbed_2_9">Dart</label><label for="__tabbed_2_10">Rust</label><label for="__tabbed_2_11">C</label><label for="__tabbed_2_12">Kotlin</label><label for="__tabbed_2_13">Ruby</label></div>
<div class="tabbed-content">
@@ -4814,7 +4814,7 @@
<p><img alt="Пример вставки элемента в массив" class="animation-figure" src="../array.assets/array_insert_element.png" /></p>
<p align="center"> Рисунок 4-3 &nbsp; Пример вставки элемента в массив </p>
<p>Стоит отметить, что длина массива фиксирована, поэтому вставка нового элемента неизбежно приведет к потере элемента на конце массива. Решение этой проблемы мы оставим для обсуждения в разделе о "списках".</p>
<p>Стоит отметить, что длина массива фиксирована, поэтому вставка нового элемента неизбежно приведет к потере элемента на конце массива. Решение этой проблемы мы оставим для обсуждения в разделе о «списках».</p>
<div class="tabbed-set tabbed-alternate" data-tabs="3:13"><input checked="checked" id="__tabbed_3_1" name="__tabbed_3" type="radio" /><input id="__tabbed_3_2" name="__tabbed_3" type="radio" /><input id="__tabbed_3_3" name="__tabbed_3" type="radio" /><input id="__tabbed_3_4" name="__tabbed_3" type="radio" /><input id="__tabbed_3_5" name="__tabbed_3" type="radio" /><input id="__tabbed_3_6" name="__tabbed_3" type="radio" /><input id="__tabbed_3_7" name="__tabbed_3" type="radio" /><input id="__tabbed_3_8" name="__tabbed_3" type="radio" /><input id="__tabbed_3_9" name="__tabbed_3" type="radio" /><input id="__tabbed_3_10" name="__tabbed_3" type="radio" /><input id="__tabbed_3_11" name="__tabbed_3" type="radio" /><input id="__tabbed_3_12" name="__tabbed_3" type="radio" /><input id="__tabbed_3_13" name="__tabbed_3" type="radio" /><div class="tabbed-labels"><label for="__tabbed_3_1">Python</label><label for="__tabbed_3_2">C++</label><label for="__tabbed_3_3">Java</label><label for="__tabbed_3_4">C#</label><label for="__tabbed_3_5">Go</label><label for="__tabbed_3_6">Swift</label><label for="__tabbed_3_7">JS</label><label for="__tabbed_3_8">TS</label><label for="__tabbed_3_9">Dart</label><label for="__tabbed_3_10">Rust</label><label for="__tabbed_3_11">C</label><label for="__tabbed_3_12">Kotlin</label><label for="__tabbed_3_13">Ruby</label></div>
<div class="tabbed-content">
<div class="tabbed-block">
@@ -5127,7 +5127,7 @@
<ul>
<li><strong>Высокая временная сложность</strong>: средняя временная сложность и вставки, и удаления равна <span class="arithmatex">\(O(n)\)</span> , где <span class="arithmatex">\(n\)</span> - длина массива.</li>
<li><strong>Потеря элементов</strong>: поскольку длина массива неизменяема, после вставки элементы, выходящие за пределы длины массива, будут потеряны.</li>
<li><strong>Потери памяти</strong>: можно заранее инициализировать более длинный массив и использовать только его переднюю часть; тогда теряемые при вставке элементы на конце не будут нести смысла, но такой подход приводит к лишнему расходу памяти.</li>
<li><strong>Потери памяти</strong>: можно заранее инициализировать более длинный массив и использовать только его переднюю часть. Тогда теряемые при вставке элементы на конце не будут нести смысла, но такой подход приводит к лишнему расходу памяти.</li>
</ul>
<h3 id="5">5. &nbsp; Обход массива<a class="headerlink" href="#5" title="Permanent link">&para;</a></h3>
<p>В большинстве языков программирования массив можно обходить как по индексу, так и напрямую перебирая каждый элемент:</p>
@@ -5347,7 +5347,7 @@
<div style="margin-top: 5px;"><a href="https://pythontutor.com/iframe-embed.html#code=def%20traverse%28nums%3A%20list%5Bint%5D%29%3A%0A%20%20%20%20%22%22%22%D0%9E%D0%B1%D1%85%D0%BE%D0%B4%20%D0%BC%D0%B0%D1%81%D1%81%D0%B8%D0%B2%D0%B0%22%22%22%0A%20%20%20%20count%20%3D%200%0A%20%20%20%20%23%20%D0%9E%D0%B1%D1%85%D0%BE%D0%B4%20%D0%BC%D0%B0%D1%81%D1%81%D0%B8%D0%B2%D0%B0%20%D0%BF%D0%BE%20%D0%B8%D0%BD%D0%B4%D0%B5%D0%BA%D1%81%D0%B0%D0%BC%0A%20%20%20%20for%20i%20in%20range%28len%28nums%29%29%3A%0A%20%20%20%20%20%20%20%20count%20%2B%3D%20nums%5Bi%5D%0A%20%20%20%20%23%20%D0%9D%D0%B5%D0%BF%D0%BE%D1%81%D1%80%D0%B5%D0%B4%D1%81%D1%82%D0%B2%D0%B5%D0%BD%D0%BD%D0%BE%20%D0%BE%D0%B1%D1%85%D0%BE%D0%B4%D0%B8%D1%82%D1%8C%20%D1%8D%D0%BB%D0%B5%D0%BC%D0%B5%D0%BD%D1%82%D1%8B%20%D0%BC%D0%B0%D1%81%D1%81%D0%B8%D0%B2%D0%B0%0A%20%20%20%20for%20num%20in%20nums%3A%0A%20%20%20%20%20%20%20%20count%20%2B%3D%20num%0A%20%20%20%20%23%20%D0%9E%D0%B4%D0%BD%D0%BE%D0%B2%D1%80%D0%B5%D0%BC%D0%B5%D0%BD%D0%BD%D0%BE%20%D0%BE%D0%B1%D1%85%D0%BE%D0%B4%D0%B8%D1%82%D1%8C%20%D0%B8%D0%BD%D0%B4%D0%B5%D0%BA%D1%81%D1%8B%20%D0%B8%20%D1%8D%D0%BB%D0%B5%D0%BC%D0%B5%D0%BD%D1%82%D1%8B%20%D0%B4%D0%B0%D0%BD%D0%BD%D1%8B%D1%85%0A%20%20%20%20for%20i%2C%20num%20in%20enumerate%28nums%29%3A%0A%20%20%20%20%20%20%20%20count%20%2B%3D%20nums%5Bi%5D%0A%20%20%20%20%20%20%20%20count%20%2B%3D%20num%0A%0A%22%22%22Driver%20Code%22%22%22%0Aif%20__name__%20%3D%3D%20%22__main__%22%3A%0A%20%20%20%20%23%20%D0%98%D0%BD%D0%B8%D1%86%D0%B8%D0%B0%D0%BB%D0%B8%D0%B7%D0%B0%D1%86%D0%B8%D1%8F%20%D0%BC%D0%B0%D1%81%D1%81%D0%B8%D0%B2%D0%B0%0A%20%20%20%20nums%20%3D%20%5B1%2C%203%2C%202%2C%205%2C%204%5D%0A%20%20%20%20print%28%22%D0%9C%D0%B0%D1%81%D1%81%D0%B8%D0%B2%20nums%20%3D%22%2C%20nums%29%0A%0A%20%20%20%20%23%20%D0%9E%D0%B1%D1%85%D0%BE%D0%B4%20%D0%BC%D0%B0%D1%81%D1%81%D0%B8%D0%B2%D0%B0%0A%20%20%20%20traverse%28nums%29&codeDivHeight=800&codeDivWidth=600&cumulative=false&curInstr=6&heapPrimitives=nevernest&origin=opt-frontend.js&py=311&rawInputLstJSON=%5B%5D&textReferences=false" target="_blank" rel="noopener noreferrer">Во весь экран &gt;</a></div></p>
</details>
<h3 id="6">6. &nbsp; Поиск элемента<a class="headerlink" href="#6" title="Permanent link">&para;</a></h3>
<p>Чтобы найти заданный элемент в массиве, нужно пройти по массиву и на каждой итерации проверять, совпадает ли значение; если совпадает, вернуть соответствующий индекс.</p>
<p>Чтобы найти заданный элемент в массиве, нужно пройти по массиву и на каждой итерации проверять, совпадает ли значение. Если совпадает, вернуть соответствующий индекс.</p>
<p>Поскольку массив - это линейная структура данных, такая операция поиска называется линейным поиском.</p>
<div class="tabbed-set tabbed-alternate" data-tabs="6:13"><input checked="checked" id="__tabbed_6_1" name="__tabbed_6" type="radio" /><input id="__tabbed_6_2" name="__tabbed_6" type="radio" /><input id="__tabbed_6_3" name="__tabbed_6" type="radio" /><input id="__tabbed_6_4" name="__tabbed_6" type="radio" /><input id="__tabbed_6_5" name="__tabbed_6" type="radio" /><input id="__tabbed_6_6" name="__tabbed_6" type="radio" /><input id="__tabbed_6_7" name="__tabbed_6" type="radio" /><input id="__tabbed_6_8" name="__tabbed_6" type="radio" /><input id="__tabbed_6_9" name="__tabbed_6" type="radio" /><input id="__tabbed_6_10" name="__tabbed_6" type="radio" /><input id="__tabbed_6_11" name="__tabbed_6" type="radio" /><input id="__tabbed_6_12" name="__tabbed_6" type="radio" /><input id="__tabbed_6_13" name="__tabbed_6" type="radio" /><div class="tabbed-labels"><label for="__tabbed_6_1">Python</label><label for="__tabbed_6_2">C++</label><label for="__tabbed_6_3">Java</label><label for="__tabbed_6_4">C#</label><label for="__tabbed_6_5">Go</label><label for="__tabbed_6_6">Swift</label><label for="__tabbed_6_7">JS</label><label for="__tabbed_6_8">TS</label><label for="__tabbed_6_9">Dart</label><label for="__tabbed_6_10">Rust</label><label for="__tabbed_6_11">C</label><label for="__tabbed_6_12">Kotlin</label><label for="__tabbed_6_13">Ruby</label></div>
<div class="tabbed-content">
@@ -5718,7 +5718,7 @@
<p>Непрерывное хранение данных - это палка о двух концах, и у него есть следующие ограничения.</p>
<ul>
<li><strong>Низкая эффективность вставки и удаления</strong>: когда элементов в массиве много, вставка и удаление требуют сдвига большого количества элементов.</li>
<li><strong>Неизменяемая длина</strong>: после инициализации длина массива фиксирована; расширение массива требует копирования всех данных в новый массив, что стоит дорого.</li>
<li><strong>Неизменяемая длина</strong>: после инициализации длина массива фиксирована. Расширение массива требует копирования всех данных в новый массив, что стоит дорого.</li>
<li><strong>Потери памяти</strong>: если выделенный массив больше, чем реально необходимо, лишнее пространство пропадает впустую.</li>
</ul>
<h2 id="413">4.1.3 &nbsp; Типичные применения массива<a class="headerlink" href="#413" title="Permanent link">&para;</a></h2>
@@ -4890,9 +4890,9 @@
<summary>Визуализация выполнения</summary>
<p>https://pythontutor.com/render.html#code=class%20ListNode%3A%0A%20%20%20%20%22%22%22%D1%81%D0%B2%D1%8F%D0%B7%D0%BD%D1%8B%D0%B9%20%D1%81%D0%BF%D0%B8%D1%81%D0%BE%D0%BA%D1%83%D0%B7%D0%B5%D0%BB%D0%BA%D0%BB%D0%B0%D1%81%D1%81%22%22%22%0A%20%20%20%20def%20__init__%28self%2C%20val%3A%20int%29%3A%0A%20%20%20%20%20%20%20%20self.val%3A%20int%20%3D%20val%20%20%23%20%D0%97%D0%BD%D0%B0%D1%87%D0%B5%D0%BD%D0%B8%D0%B5%20%D1%83%D0%B7%D0%BB%D0%B0%0A%20%20%20%20%20%20%20%20self.next%3A%20ListNode%20%7C%20None%20%3D%20None%20%20%23%20%D0%A1%D1%81%D1%8B%D0%BB%D0%BA%D0%B0%20%D0%BD%D0%B0%20%D1%81%D0%BB%D0%B5%D0%B4%D1%83%D1%8E%D1%89%D0%B8%D0%B9%20%D1%83%D0%B7%D0%B5%D0%BB%0A%0A%22%22%22Driver%20Code%22%22%22%0Aif%20__name__%20%3D%3D%20%22__main__%22%3A%0A%20%20%20%20%23%20%D0%98%D0%BD%D0%B8%D1%86%D0%B8%D0%B0%D0%BB%D0%B8%D0%B7%D0%B8%D1%80%D0%BE%D0%B2%D0%B0%D1%82%D1%8C%20%D1%81%D0%B2%D1%8F%D0%B7%D0%BD%D1%8B%D0%B9%20%D1%81%D0%BF%D0%B8%D1%81%D0%BE%D0%BA%201%20-%3E%203%20-%3E%202%20-%3E%205%20-%3E%204%0A%20%20%20%20%23%20%D0%98%D0%BD%D0%B8%D1%86%D0%B8%D0%B0%D0%BB%D0%B8%D0%B7%D0%B8%D1%80%D0%BE%D0%B2%D0%B0%D1%82%D1%8C%20%D0%BA%D0%B0%D0%B6%D0%B4%D1%8B%D0%B9%20%D1%83%D0%B7%D0%B5%D0%BB%0A%20%20%20%20n0%20%3D%20ListNode%281%29%0A%20%20%20%20n1%20%3D%20ListNode%283%29%0A%20%20%20%20n2%20%3D%20ListNode%282%29%0A%20%20%20%20n3%20%3D%20ListNode%285%29%0A%20%20%20%20n4%20%3D%20ListNode%284%29%0A%20%20%20%20%23%20%D0%9F%D0%BE%D1%81%D1%82%D1%80%D0%BE%D0%B8%D1%82%D1%8C%20%D1%81%D1%81%D1%8B%D0%BB%D0%BA%D0%B8%20%D0%BC%D0%B5%D0%B6%D0%B4%D1%83%20%D1%83%D0%B7%D0%BB%D0%B0%D0%BC%D0%B8%0A%20%20%20%20n0.next%20%3D%20n1%0A%20%20%20%20n1.next%20%3D%20n2%0A%20%20%20%20n2.next%20%3D%20n3%0A%20%20%20%20n3.next%20%3D%20n4&amp;cumulative=false&amp;curInstr=3&amp;heapPrimitives=nevernest&amp;mode=display&amp;origin=opt-frontend.js&amp;py=311&amp;rawInputLstJSON=%5B%5D&amp;textReferences=false</p>
</details>
<p>Массив в целом - это одна переменная: например, массив <code>nums</code> содержит элементы <code>nums[0]</code> , <code>nums[1]</code> и т.д. Связный список же состоит из множества независимых объектов-узлов. <strong>Обычно в качестве обозначения всего связного списка используют головной узел</strong>; например, в приведенном выше коде связный список можно обозначить как <code>n0</code> .</p>
<p>Массив в целом - это одна переменная: например, массив <code>nums</code> содержит элементы <code>nums[0]</code> , <code>nums[1]</code> и т.д. Связный список же состоит из множества независимых объектов-узлов. <strong>Обычно в качестве обозначения всего связного списка используют головной узел</strong>. Например, в приведенном выше коде связный список можно обозначить как <code>n0</code> .</p>
<h3 id="2">2. &nbsp; Вставка узла<a class="headerlink" href="#2" title="Permanent link">&para;</a></h3>
<p>Вставить узел в связный список очень легко. Как показано на рисунке 4-6, предположим, что мы хотим вставить новый узел <code>P</code> между двумя соседними узлами <code>n0</code> и <code>n1</code> ; <strong>для этого нужно изменить всего две ссылки (указателя)</strong>, а временная сложность будет равна <span class="arithmatex">\(O(1)\)</span> .</p>
<p>Вставить узел в связный список очень легко. Как показано на рисунке 4-6, предположим, что мы хотим вставить новый узел <code>P</code> между двумя соседними узлами <code>n0</code> и <code>n1</code>. <strong>Для этого нужно изменить всего две ссылки (указателя)</strong>, а временная сложность будет равна <span class="arithmatex">\(O(1)\)</span> .</p>
<p>Для сравнения: временная сложность вставки элемента в массив составляет <span class="arithmatex">\(O(n)\)</span> , и при большом объеме данных это менее эффективно.</p>
<p><img alt="Пример вставки узла в связный список" class="animation-figure" src="../linked_list.assets/linkedlist_insert_node.png" /></p>
<p align="center"> Рисунок 4-6 &nbsp; Пример вставки узла в связный список </p>
@@ -5819,14 +5819,14 @@
<h2 id="424">4.2.4 &nbsp; Типичные применения связных списков<a class="headerlink" href="#424" title="Permanent link">&para;</a></h2>
<p>Односвязные списки обычно используются для реализации стеков, очередей, хеш-таблиц и графов.</p>
<ul>
<li><strong>Стеки и очереди</strong>: если операции вставки и удаления выполняются на одном конце связного списка, он проявляет свойства LIFO, соответствующие стеку; если вставка происходит на одном конце, а удаление на другом, он проявляет свойства FIFO, соответствующие очереди.</li>
<li><strong>Стеки и очереди</strong>: если операции вставки и удаления выполняются на одном конце связного списка, он проявляет свойства LIFO, соответствующие стеку. Если вставка происходит на одном конце, а удаление на другом, он проявляет свойства FIFO, соответствующие очереди.</li>
<li><strong>Хеш-таблицы</strong>: метод цепочек - один из основных способов разрешения коллизий в хеш-таблицах. В этом подходе все конфликтующие элементы помещаются в связный список.</li>
<li><strong>Графы</strong>: список смежности - это распространенный способ представления графа, при котором каждой вершине графа соответствует связный список, а каждый элемент этого списка представляет другую вершину, соединенную с данной.</li>
</ul>
<p>Двусвязные списки обычно используются там, где нужен быстрый доступ как к предыдущему, так и к следующему элементу.</p>
<ul>
<li><strong>Продвинутые структуры данных</strong>: например, в красно-черных деревьях и B-деревьях нам нужен доступ к родительскому узлу; этого можно добиться, сохранив в узле ссылку на родителя, по аналогии с двусвязным списком.</li>
<li><strong>История браузера</strong>: когда пользователь в браузере нажимает кнопки "вперед" или "назад", браузеру нужно знать предыдущую и следующую посещенные страницы. Свойства двусвязного списка делают такую операцию простой.</li>
<li><strong>Продвинутые структуры данных</strong>: например, в красно-черных деревьях и B-деревьях нам нужен доступ к родительскому узлу. Этого можно добиться, сохранив в узле ссылку на родителя, по аналогии с двусвязным списком.</li>
<li><strong>История браузера</strong>: когда пользователь в браузере нажимает кнопки «вперед» или «назад», браузеру нужно знать предыдущую и следующую посещенные страницы. Свойства двусвязного списка делают такую операцию простой.</li>
<li><strong>Алгоритм LRU</strong>: в алгоритмах вытеснения из кэша (LRU) нужно быстро находить наименее недавно использованные данные, а также быстро добавлять и удалять узлы. Для этого двусвязный список подходит очень хорошо.</li>
</ul>
<p>Циклические списки часто применяются в сценариях, требующих циклических операций, например при планировании ресурсов в операционной системе.</p>
@@ -4506,9 +4506,9 @@
<li>Связный список естественным образом можно рассматривать как список: он поддерживает операции добавления, удаления, поиска и изменения элементов и может гибко расширяться динамически.</li>
<li>Массив тоже поддерживает операции добавления, удаления, поиска и изменения элементов, но из-за неизменяемости длины его можно считать лишь списком с ограниченной длиной.</li>
</ul>
<p>Когда список реализуется с помощью массива, <strong>неизменяемость длины снижает его практическую полезность</strong>. Причина в том, что мы обычно не можем заранее точно знать, сколько данных нужно хранить, а значит, трудно выбрать подходящую длину списка. Если длина слишком мала, она может не покрыть реальные потребности; если слишком велика, будет зря расходоваться память.</p>
<p>Когда список реализуется с помощью массива, <strong>неизменяемость длины снижает его практическую полезность</strong>. Причина в том, что мы обычно не можем заранее точно знать, сколько данных нужно хранить, а значит, трудно выбрать подходящую длину списка. Если длина слишком мала, она может не покрыть реальные потребности. Если слишком велика, будет зря расходоваться память.</p>
<p>Чтобы решить эту проблему, можно использовать <u>динамический массив (dynamic array)</u> для реализации списка. Он сохраняет все преимущества массива и при этом может динамически расширяться во время выполнения программы.</p>
<p>На практике <strong>списки из стандартных библиотек многих языков программирования реализованы именно на основе динамических массивов</strong>, например <code>list</code> в Python, <code>ArrayList</code> в Java, <code>vector</code> в C++ и <code>List</code> в C#. В дальнейшем обсуждении мы будем считать понятия "список" и "динамический массив" эквивалентными.</p>
<p>На практике <strong>списки из стандартных библиотек многих языков программирования реализованы именно на основе динамических массивов</strong>, например <code>list</code> в Python, <code>ArrayList</code> в Java, <code>vector</code> в C++ и <code>List</code> в C#. В дальнейшем обсуждении мы будем считать понятия «список» и «динамический массив» эквивалентными.</p>
<h2 id="431">4.3.1 &nbsp; Основные операции со списком<a class="headerlink" href="#431" title="Permanent link">&para;</a></h2>
<h3 id="1">1. &nbsp; Инициализация списка<a class="headerlink" href="#1" title="Permanent link">&para;</a></h3>
<p>Обычно используются два способа инициализации: без начальных значений и с начальными значениями:</p>
@@ -5226,7 +5226,7 @@
<p>https://pythontutor.com/render.html#code=%22%22%22Driver%20Code%22%22%22%0Aif%20__name__%20%3D%3D%20%22__main__%22%3A%0A%20%20%20%20%23%20%D0%98%D0%BD%D0%B8%D1%86%D0%B8%D0%B0%D0%BB%D0%B8%D0%B7%D0%B8%D1%80%D0%BE%D0%B2%D0%B0%D1%82%D1%8C%20%D1%81%D0%BF%D0%B8%D1%81%D0%BE%D0%BA%0A%20%20%20%20nums%20%3D%20%5B1%2C%203%2C%202%2C%205%2C%204%5D%0A%20%20%20%20%0A%20%20%20%20%23%20%D0%9E%D0%B1%D1%8A%D0%B5%D0%B4%D0%B8%D0%BD%D0%B8%D1%82%D1%8C%20%D0%B4%D0%B2%D0%B0%20%D1%81%D0%BF%D0%B8%D1%81%D0%BA%D0%B0%0A%20%20%20%20nums1%20%3D%20%5B6%2C%208%2C%207%2C%2010%2C%209%5D%0A%20%20%20%20nums%20%2B%3D%20nums1%20%20%23%20%D0%9F%D1%80%D0%B8%D1%81%D0%BE%D0%B5%D0%B4%D0%B8%D0%BD%D0%B8%D1%82%D1%8C%20%D1%81%D0%BF%D0%B8%D1%81%D0%BE%D0%BA%20nums1%20%D0%BA%20nums&amp;cumulative=false&amp;curInstr=3&amp;heapPrimitives=nevernest&amp;mode=display&amp;origin=opt-frontend.js&amp;py=311&amp;rawInputLstJSON=%5B%5D&amp;textReferences=false</p>
</details>
<h3 id="6">6. &nbsp; Сортировка списка<a class="headerlink" href="#6" title="Permanent link">&para;</a></h3>
<p>После сортировки списка мы сможем применять алгоритмы "двоичный поиск" и "два указателя", которые очень часто встречаются в задачах по массивам.</p>
<p>После сортировки списка мы сможем применять алгоритмы «двоичный поиск» и «два указателя», которые очень часто встречаются в задачах по массивам.</p>
<div class="tabbed-set tabbed-alternate" data-tabs="6:13"><input checked="checked" id="__tabbed_6_1" name="__tabbed_6" type="radio" /><input id="__tabbed_6_2" name="__tabbed_6" type="radio" /><input id="__tabbed_6_3" name="__tabbed_6" type="radio" /><input id="__tabbed_6_4" name="__tabbed_6" type="radio" /><input id="__tabbed_6_5" name="__tabbed_6" type="radio" /><input id="__tabbed_6_6" name="__tabbed_6" type="radio" /><input id="__tabbed_6_7" name="__tabbed_6" type="radio" /><input id="__tabbed_6_8" name="__tabbed_6" type="radio" /><input id="__tabbed_6_9" name="__tabbed_6" type="radio" /><input id="__tabbed_6_10" name="__tabbed_6" type="radio" /><input id="__tabbed_6_11" name="__tabbed_6" type="radio" /><input id="__tabbed_6_12" name="__tabbed_6" type="radio" /><input id="__tabbed_6_13" name="__tabbed_6" type="radio" /><div class="tabbed-labels"><label for="__tabbed_6_1">Python</label><label for="__tabbed_6_2">C++</label><label for="__tabbed_6_3">Java</label><label for="__tabbed_6_4">C#</label><label for="__tabbed_6_5">Go</label><label for="__tabbed_6_6">Swift</label><label for="__tabbed_6_7">JS</label><label for="__tabbed_6_8">TS</label><label for="__tabbed_6_9">Dart</label><label for="__tabbed_6_10">Rust</label><label for="__tabbed_6_11">C</label><label for="__tabbed_6_12">Kotlin</label><label for="__tabbed_6_13">Ruby</label></div>
<div class="tabbed-content">
<div class="tabbed-block">
@@ -4452,7 +4452,7 @@
<p>С другой стороны, во время выполнения программы <strong>при многократном выделении и освобождении памяти фрагментация свободной памяти становится все более серьезной</strong>, что снижает эффективность ее использования. Массивы из-за непрерывного хранения относительно менее подвержены фрагментации. Напротив, элементы связного списка распределены по памяти, и частые операции вставки и удаления легче приводят к фрагментации.</p>
<h2 id="443">4.4.3 &nbsp; Эффективность использования кэша структурами данных<a class="headerlink" href="#443" title="Permanent link">&para;</a></h2>
<p>Хотя по объему кэш намного меньше оперативной памяти, он значительно быстрее и играет критически важную роль в скорости выполнения программ. Поскольку объем кэша ограничен и в нем можно хранить только небольшую долю часто используемых данных, когда CPU пытается обратиться к данным, которых в кэше нет, происходит <u>промах кэша (cache miss)</u> , и CPU вынужден загружать нужные данные из более медленной памяти.</p>
<p>Очевидно, что <strong>чем меньше промахов кэша, тем выше эффективность чтения и записи данных CPU</strong>, а значит, тем лучше производительность программы. Долю обращений, при которых CPU успешно получает данные из кэша, называют <u>коэффициентом попадания в кэш (cache hit rate)</u> ; этот показатель обычно используют для оценки эффективности кэша.</p>
<p>Очевидно, что <strong>чем меньше промахов кэша, тем выше эффективность чтения и записи данных CPU</strong>, а значит, тем лучше производительность программы. Долю обращений, при которых CPU успешно получает данные из кэша, называют <u>коэффициентом попадания в кэш (cache hit rate)</u>. Этот показатель обычно используют для оценки эффективности кэша.</p>
<p>Чтобы добиться как можно большей эффективности, кэш использует следующие механизмы загрузки данных.</p>
<ul>
<li><strong>Строки кэша</strong>: кэш хранит и загружает данные не по одному байту, а строками кэша. По сравнению с передачей по байтам это гораздо эффективнее.</li>
@@ -4468,7 +4468,7 @@
<li><strong>Пространственная локальность</strong>: массив хранится в компактной области памяти, поэтому данные рядом с уже загруженными с большей вероятностью скоро будут использованы.</li>
</ul>
<p>В целом <strong>массивы имеют более высокий коэффициент попадания в кэш, поэтому по эффективности операций они обычно превосходят связные списки</strong>. Именно поэтому при решении алгоритмических задач структуры данных на основе массивов часто оказываются предпочтительнее.</p>
<p>Важно понимать, что <strong>высокая эффективность кэша не означает, что массивы во всех случаях лучше связных списков</strong>. В реальных приложениях выбор структуры данных должен определяться конкретными требованиями. Например, и массивы, и списки могут использоваться для реализации "стека" (подробнее об этом будет рассказано в следующей главе), но подходят они для разных сценариев.</p>
<p>Важно понимать, что <strong>высокая эффективность кэша не означает, что массивы во всех случаях лучше связных списков</strong>. В реальных приложениях выбор структуры данных должен определяться конкретными требованиями. Например, и массивы, и списки могут использоваться для реализации «стека» (подробнее об этом будет рассказано в следующей главе), но подходят они для разных сценариев.</p>
<ul>
<li>При решении алгоритмических задач мы обычно предпочитаем стек на основе массива, потому что он дает более высокую эффективность операций и поддерживает произвольный доступ, а цена за это - необходимость заранее выделить некоторый объем памяти под массив.</li>
<li>Если объем данных очень велик, структура сильно динамична, а ожидаемый размер стека трудно оценить заранее, то более уместен стек на основе связного списка. Список позволяет распределить большой объем данных по разным участкам памяти и избегает накладных расходов, связанных с расширением массива.</li>
@@ -4360,8 +4360,8 @@
<h3 id="1">1. &nbsp; Ключевые выводы<a class="headerlink" href="#1" title="Permanent link">&para;</a></h3>
<ul>
<li>Массивы и связные списки - это две базовые структуры данных, представляющие два способа хранения данных в памяти компьютера: хранение в непрерывном пространстве и хранение в разрозненном пространстве. Их свойства во многом взаимно дополняют друг друга.</li>
<li>Массив поддерживает произвольный доступ и занимает меньше памяти; однако вставка и удаление элементов в нем неэффективны, а длина после инициализации фиксирована.</li>
<li>Связный список позволяет эффективно вставлять и удалять узлы путем изменения ссылок (указателей), а также гибко менять длину; однако доступ к узлам менее эффективен, а памяти он занимает больше. Распространенные типы списков включают односвязные, циклические и двусвязные списки.</li>
<li>Массив поддерживает произвольный доступ и занимает меньше памяти. Однако вставка и удаление элементов в нем неэффективны, а длина после инициализации фиксирована.</li>
<li>Связный список позволяет эффективно вставлять и удалять узлы путем изменения ссылок (указателей), а также гибко менять длину. Однако доступ к узлам менее эффективен, а памяти он занимает больше. Распространенные типы списков включают односвязные, циклические и двусвязные списки.</li>
<li>Список - это упорядоченная коллекция элементов, поддерживающая добавление, удаление, поиск и изменение, и обычно реализуемая на основе динамического массива. Он сохраняет преимущества массива и при этом может гибко менять длину.</li>
<li>Появление списка значительно повысило практическую ценность массива, хотя это и может приводить к потере части памяти.</li>
<li>Во время работы программы данные в основном хранятся в оперативной памяти. Массив обеспечивает более высокую эффективность использования пространства памяти, а связный список дает большую гибкость в использовании памяти.</li>
@@ -4372,13 +4372,13 @@
<p><strong>Q</strong>: Влияет ли хранение массива в стеке или в куче на временную и пространственную эффективность?</p>
<p>Массивы, расположенные и в стеке, и в куче, все равно хранятся в непрерывной области памяти, поэтому эффективность операций с данными у них в целом одинакова. Однако у стека и кучи есть собственные особенности, из-за которых возникают следующие различия.</p>
<ol>
<li>Эффективность выделения и освобождения: стек представляет собой относительно небольшой участок памяти, а выделение в нем обычно выполняется автоматически компилятором; куча же обычно больше, может выделяться динамически из кода и легче фрагментируется. Поэтому выделение и освобождение памяти в куче обычно медленнее, чем в стеке.</li>
<li>Эффективность выделения и освобождения: стек представляет собой относительно небольшой участок памяти, а выделение в нем обычно выполняется автоматически компилятором. Куча же обычно больше, может выделяться динамически из кода и легче фрагментируется. Поэтому выделение и освобождение памяти в куче обычно медленнее, чем в стеке.</li>
<li>Ограничение размера: объем стека относительно невелик, а размер кучи обычно ограничивается доступной памятью. Поэтому куча лучше подходит для хранения больших массивов.</li>
<li>Гибкость: размер массива в стеке должен быть известен во время компиляции, а размер массива в куче может определяться динамически во время выполнения.</li>
</ol>
<p><strong>Q</strong>: Почему для массива требуется, чтобы все элементы были одного типа, а для связного списка это не подчеркивается?</p>
<p>Связный список состоит из узлов, а узлы соединяются между собой через ссылки (указатели), поэтому каждый узел в принципе может хранить данные разного типа, например <code>int</code> , <code>double</code> , <code>string</code> , <code>object</code> и т.д.</p>
<p>Напротив, элементы массива должны быть одного типа, иначе нельзя будет вычислять адрес элемента через смещение. Например, если массив одновременно содержит <code>int</code> и <code>long</code> , один элемент занимает 4 байта, а другой - 8 байт ; в этом случае формула ниже уже не позволит вычислить смещение, потому что в массиве будут присутствовать элементы разной длины.</p>
<p>Напротив, элементы массива должны быть одного типа, иначе нельзя будет вычислять адрес элемента через смещение. Например, если массив одновременно содержит <code>int</code> и <code>long</code> , один элемент занимает 4 байта, а другой - 8 байт. В этом случае формула ниже уже не позволит вычислить смещение, потому что в массиве будут присутствовать элементы разной длины.</p>
<div class="highlight"><pre><span></span><code><a id="__codelineno-0-1" name="__codelineno-0-1" href="#__codelineno-0-1"></a><span class="c1"># Адрес элемента в памяти = адрес массива в памяти (адрес первого элемента) + длина элемента * индекс элемента</span>
</code></pre></div>
<p><strong>Q</strong>: После удаления узла <code>P</code> нужно ли присваивать <code>P.next = None</code> ?</p>
@@ -4386,16 +4386,16 @@
<p>С точки зрения задач по структурам данных и алгоритмам, отсутствие такого разрыва обычно не критично, если логика программы остается корректной. Но с точки зрения стандартной библиотеки разорвать связь безопаснее и логичнее. Если этого не сделать и удаленный узел не будет нормально собран, он может мешать освобождению памяти последующих узлов.</p>
<p><strong>Q</strong>: Временная сложность вставки и удаления в связном списке равна <span class="arithmatex">\(O(1)\)</span> . Но до вставки или удаления обычно еще нужно потратить <span class="arithmatex">\(O(n)\)</span> на поиск элемента. Почему тогда общая сложность не <span class="arithmatex">\(O(n)\)</span> ?</p>
<p>Если сначала искать элемент, а потом удалять его, то временная сложность действительно будет <span class="arithmatex">\(O(n)\)</span> . Однако преимущество связного списка с <span class="arithmatex">\(O(1)\)</span> вставкой и удалением проявляется в других сценариях. Например, двустороннюю очередь удобно реализовывать именно на связном списке: мы поддерживаем указатели на голову и хвост, и тогда каждая операция вставки или удаления остается <span class="arithmatex">\(O(1)\)</span> .</p>
<p><strong>Q</strong>: На рисунке "Определение связного списка и способ хранения" светло-голубой блок с указателем узла - это отдельный адрес памяти? Или он делит память пополам со значением узла?</p>
<p>Этот рисунок дает только качественное представление; количественно все зависит от конкретных условий.</p>
<p><strong>Q</strong>: На рисунке «Определение связного списка и способ хранения» светло-голубой блок с указателем узла - это отдельный адрес памяти? Или он делит память пополам со значением узла?</p>
<p>Этот рисунок дает только качественное представление. Количественно все зависит от конкретных условий.</p>
<ul>
<li>Значения узлов разных типов занимают разный объем памяти, например <code>int</code> , <code>long</code> , <code>double</code> и объекты-экземпляры.</li>
<li>Размер памяти, занимаемой переменной-указателем, зависит от операционной системы и среды компиляции и обычно составляет 8 байт или 4 байта.</li>
</ul>
<p><strong>Q</strong>: Всегда ли добавление элемента в конец списка имеет сложность <span class="arithmatex">\(O(1)\)</span> ?</p>
<p>Если при добавлении элемента длина списка превышается, то сначала приходится расширять список, а уже затем добавлять новый элемент. Система выделяет новый участок памяти и переносит туда все элементы исходного списка, и в этот момент временная сложность становится <span class="arithmatex">\(O(n)\)</span> .</p>
<p><strong>Q</strong>: В утверждении "появление списка сильно повысило практическую полезность массива, но может приводить к потере части памяти" под потерями памяти имеется в виду дополнительная память под такие переменные, как емкость, длина и коэффициент расширения?</p>
<p>Потери памяти здесь в основном имеют два значения: во-первых, список обычно имеет некоторую начальную емкость, которая может быть нам не нужна целиком; во-вторых, чтобы избежать слишком частых расширений, емкость при расширении обычно умножается на некоторый коэффициент, например <span class="arithmatex">\(\times 1.5\)</span> . Из-за этого появляется много пустых слотов, которые обычно нельзя полностью заполнить.</p>
<p><strong>Q</strong>: В утверждении «появление списка сильно повысило практическую полезность массива, но может приводить к потере части памяти» под потерями памяти имеется в виду дополнительная память под такие переменные, как емкость, длина и коэффициент расширения?</p>
<p>Потери памяти здесь в основном имеют два значения: во-первых, список обычно имеет некоторую начальную емкость, которая может быть нам не нужна целиком. Во-вторых, чтобы избежать слишком частых расширений, емкость при расширении обычно умножается на некоторый коэффициент, например <span class="arithmatex">\(\times 1.5\)</span> . Из-за этого появляется много пустых слотов, которые обычно нельзя полностью заполнить.</p>
<p><strong>Q</strong>: В Python после инициализации <code>n = [1, 2, 3]</code> адреса этих трех элементов выглядят непрерывными, но после <code>m = [2, 1, 3]</code> можно заметить, что <code>id</code> элементов не идут подряд, а совпадают с одинаковыми числами из <code>n</code> . Если адреса элементов не непрерывны, остается ли <code>m</code> массивом?</p>
<p>Предположим, что элементами списка являются узлы <code>n = [n1, n2, n3, n4, n5]</code> . Обычно эти 5 объектов-узлов тоже будут храниться в разных местах памяти. Однако, имея индекс списка, мы по-прежнему можем за <span class="arithmatex">\(O(1)\)</span> получить адрес памяти соответствующего узла и обратиться к нему. Это связано с тем, что в массиве хранятся ссылки на узлы, а не сами узлы.</p>
<p>В отличие от многих других языков, в Python даже числа обернуты в объекты, и в списке хранятся не сами числа, а ссылки на них. Поэтому мы и наблюдаем, что одинаковые числа в двух массивах имеют один и тот же <code>id</code> , а адреса этих чисел не обязаны быть непрерывными.</p>
@@ -4411,8 +4411,8 @@
<p>Если нужно, чтобы каждый <code>[0]</code> был независимым, можно использовать <code>res = [[0] for _ in range(n)]</code> . В этом варианте создаются <span class="arithmatex">\(n\)</span> независимых объектов-списков <code>[0]</code> .</p>
<p><strong>Q</strong>: Операция <code>res = [0] * n</code> создает список. Каждый целочисленный <code>0</code> в нем независим?</p>
<p>В этом списке все целые числа <code>0</code> являются ссылками на один и тот же объект. Это связано с тем, что Python использует механизм кэш-пула для маленьких целых чисел (обычно от -5 до 256), чтобы максимально переиспользовать объекты и повысить производительность.</p>
<p>Хотя все элементы указывают на один и тот же объект, мы все равно можем независимо изменять элементы списка, потому что целые числа в Python - это "неизменяемые объекты". Когда мы изменяем некоторый элемент, на самом деле происходит переключение ссылки на другой объект, а не изменение исходного объекта.</p>
<p>Однако если элементами списка являются "изменяемые объекты" (например списки, словари или экземпляры классов), то изменение одного элемента прямо меняет сам объект, и все элементы, ссылающиеся на него, увидят одно и то же изменение.</p>
<p>Хотя все элементы указывают на один и тот же объект, мы все равно можем независимо изменять элементы списка, потому что целые числа в Python - это «неизменяемые объекты». Когда мы изменяем некоторый элемент, на самом деле происходит переключение ссылки на другой объект, а не изменение исходного объекта.</p>
<p>Однако если элементами списка являются «изменяемые объекты» (например списки, словари или экземпляры классов), то изменение одного элемента прямо меняет сам объект, и все элементы, ссылающиеся на него, увидят одно и то же изменение.</p>
<!-- Source file information -->
File diff suppressed because one or more lines are too long
@@ -4381,7 +4381,7 @@
<h1 id="134-n">13.4 &nbsp; Задача о n ферзях<a class="headerlink" href="#134-n" title="Permanent link">&para;</a></h1>
<div class="admonition question">
<p class="admonition-title">Question</p>
<p>Согласно правилам шахмат ферзь может атаковать фигуры, находящиеся с ним на одной строке, в одном столбце или на одной диагонали. Даны <span class="arithmatex">\(n\)</span> ферзей и шахматная доска размера <span class="arithmatex">\(n \times n\)</span> ; требуется найти такие расстановки, при которых ни одна пара ферзей не может атаковать друг друга.</p>
<p>Согласно правилам шахмат ферзь может атаковать фигуры, находящиеся с ним на одной строке, в одном столбце или на одной диагонали. Даны <span class="arithmatex">\(n\)</span> ферзей и шахматная доска размера <span class="arithmatex">\(n \times n\)</span>. Требуется найти такие расстановки, при которых ни одна пара ферзей не может атаковать друг друга.</p>
</div>
<p>Как показано на рисунке 13-15, при <span class="arithmatex">\(n = 4\)</span> существует два решения. С точки зрения поиска с возвратом доска размера <span class="arithmatex">\(n \times n\)</span> содержит <span class="arithmatex">\(n^2\)</span> клеток, которые образуют все возможные выборы <code>choices</code> . По мере поочередного размещения ферзей состояние доски непрерывно меняется, и текущее содержимое доски образует состояние <code>state</code> .</p>
<p><img alt="Решения задачи о 4 ферзях" class="animation-figure" src="../n_queens_problem.assets/solution_4_queens.png" /></p>
@@ -4394,7 +4394,7 @@
<h3 id="1">1. &nbsp; Построчная стратегия размещения<a class="headerlink" href="#1" title="Permanent link">&para;</a></h3>
<p>Число ферзей и число строк доски одинаково и равно <span class="arithmatex">\(n\)</span> , поэтому легко получить следующий вывод: <strong>в каждой строке доски разрешено и нужно разместить ровно одного ферзя</strong>.</p>
<p>Иначе говоря, можно использовать построчную стратегию: начиная с первой строки, размещать по одному ферзю в каждой строке, пока не будет достигнута последняя.</p>
<p>На рисунке 13-17 показан процесс построчного размещения для задачи о 4 ферзях. Из-за ограничений размера изображения на нем раскрыта только одна ветвь поиска для первой строки, а все варианты, не удовлетворяющие ограничениям по столбцам и диагоналям, были отсечены.</p>
<p>На рисунке 13-17 показан процесс построчного размещения для задачи о 4 ферзях. Из-за ограничений размера изображения на рисунке 13-17 показана лишь одна ветвь поиска для первой строки, а все варианты, не удовлетворяющие ограничениям по столбцам и диагоналям, были отсечены.</p>
<p><img alt="Построчная стратегия размещения" class="animation-figure" src="../n_queens_problem.assets/n_queens_placing.png" /></p>
<p align="center"> Рисунок 13-17 &nbsp; Построчная стратегия размещения </p>
@@ -4524,7 +4524,7 @@
<p class="admonition-title">Question</p>
<p>Дан массив целых чисел, в котором нет повторяющихся элементов. Верните все возможные перестановки.</p>
</div>
<p>С точки зрения поиска с возвратом <strong>процесс построения перестановок можно представить как результат последовательности выборов</strong>. Пусть входной массив равен <span class="arithmatex">\([1, 2, 3]\)</span> ; если мы сначала выберем <span class="arithmatex">\(1\)</span> , затем <span class="arithmatex">\(3\)</span> , а потом <span class="arithmatex">\(2\)</span> , то получим перестановку <span class="arithmatex">\([1, 3, 2]\)</span> . Откат здесь означает отмену одного из выборов с последующей попыткой других вариантов.</p>
<p>С точки зрения поиска с возвратом <strong>процесс построения перестановок можно представить как результат последовательности выборов</strong>. Пусть входной массив равен <span class="arithmatex">\([1, 2, 3]\)</span>. Если мы сначала выберем <span class="arithmatex">\(1\)</span> , затем <span class="arithmatex">\(3\)</span> , а потом <span class="arithmatex">\(2\)</span> , то получим перестановку <span class="arithmatex">\([1, 3, 2]\)</span> . Откат здесь означает отмену одного из выборов с последующей попыткой других вариантов.</p>
<p>С точки зрения кода поиска с возвратом множество кандидатов <code>choices</code> состоит из всех элементов входного массива, а состояние <code>state</code> - из элементов, уже выбранных к текущему моменту. Поскольку каждый элемент разрешено выбирать только один раз, <strong>все элементы в <code>state</code> должны быть уникальны</strong>.</p>
<p>Как показано на рисунке 13-5, процесс поиска можно развернуть в дерево рекурсии, где каждый узел представляет текущее состояние <code>state</code> . Начиная от корня, после трех раундов выбора мы попадаем в листья, и каждый лист соответствует одной перестановке.</p>
<p><img alt="Дерево рекурсии для перестановок" class="animation-figure" src="../permutations_problem.assets/permutations_i.png" /></p>
@@ -4540,9 +4540,9 @@
<p><img alt="Пример обрезки в задаче о перестановках" class="animation-figure" src="../permutations_problem.assets/permutations_i_pruning.png" /></p>
<p align="center"> Рисунок 13-6 &nbsp; Пример обрезки в задаче о перестановках </p>
<p>Из рисунка видно, что такая обрезка уменьшает размер пространства поиска с <span class="arithmatex">\(O(n^n)\)</span> до <span class="arithmatex">\(O(n!)\)</span> .</p>
<p>Как видно на рисунке 13-6, такая обрезка уменьшает размер пространства поиска с <span class="arithmatex">\(O(n^n)\)</span> до <span class="arithmatex">\(O(n!)\)</span> .</p>
<h3 id="2">2. &nbsp; Реализация кода<a class="headerlink" href="#2" title="Permanent link">&para;</a></h3>
<p>После прояснения всей логики можно просто "заполнить пропуски" в шаблоне поиска с возвратом. Чтобы сократить общий объем кода, мы не будем отдельно реализовывать каждую функцию из каркаса, а раскроем их прямо внутри <code>backtrack()</code> :</p>
<p>После прояснения всей логики можно просто «заполнить пропуски» в шаблоне поиска с возвратом. Чтобы сократить общий объем кода, мы не будем отдельно реализовывать каждую функцию из каркаса, а раскроем их прямо внутри <code>backtrack()</code> :</p>
<div class="tabbed-set tabbed-alternate" data-tabs="1:13"><input checked="checked" id="__tabbed_1_1" name="__tabbed_1" type="radio" /><input id="__tabbed_1_2" name="__tabbed_1" type="radio" /><input id="__tabbed_1_3" name="__tabbed_1" type="radio" /><input id="__tabbed_1_4" name="__tabbed_1" type="radio" /><input id="__tabbed_1_5" name="__tabbed_1" type="radio" /><input id="__tabbed_1_6" name="__tabbed_1" type="radio" /><input id="__tabbed_1_7" name="__tabbed_1" type="radio" /><input id="__tabbed_1_8" name="__tabbed_1" type="radio" /><input id="__tabbed_1_9" name="__tabbed_1" type="radio" /><input id="__tabbed_1_10" name="__tabbed_1" type="radio" /><input id="__tabbed_1_11" name="__tabbed_1" type="radio" /><input id="__tabbed_1_12" name="__tabbed_1" type="radio" /><input id="__tabbed_1_13" name="__tabbed_1" type="radio" /><div class="tabbed-labels"><label for="__tabbed_1_1">Python</label><label for="__tabbed_1_2">C++</label><label for="__tabbed_1_3">Java</label><label for="__tabbed_1_4">C#</label><label for="__tabbed_1_5">Go</label><label for="__tabbed_1_6">Swift</label><label for="__tabbed_1_7">JS</label><label for="__tabbed_1_8">TS</label><label for="__tabbed_1_9">Dart</label><label for="__tabbed_1_10">Rust</label><label for="__tabbed_1_11">C</label><label for="__tabbed_1_12">Kotlin</label><label for="__tabbed_1_13">Ruby</label></div>
<div class="tabbed-content">
<div class="tabbed-block">
@@ -5021,7 +5021,7 @@
<p>Как же убрать повторяющиеся перестановки? Самый прямолинейный способ - воспользоваться хеш-множеством и удалить дубликаты уже после генерации результата. Но это не слишком изящно, <strong>потому что ветви поиска, порождающие дубликаты, вообще не нужно посещать: их следует распознавать заранее и отсекать</strong>, что дополнительно повышает эффективность алгоритма.</p>
<h3 id="1_1">1. &nbsp; Обрезка равных элементов<a class="headerlink" href="#1_1" title="Permanent link">&para;</a></h3>
<p>Посмотрите на рисунок 13-8: в первом раунде выбрать <span class="arithmatex">\(1\)</span> или выбрать <span class="arithmatex">\(\hat{1}\)</span> - это одно и то же, а значит, все перестановки, полученные из этих двух выборов, будут дублироваться. Поэтому ветвь <span class="arithmatex">\(\hat{1}\)</span> нужно отсечь.</p>
<p>Как видно на рисунке 13-8, в первом раунде выбрать <span class="arithmatex">\(1\)</span> или выбрать <span class="arithmatex">\(\hat{1}\)</span> - это одно и то же, а значит, все перестановки, полученные из этих двух выборов, будут дублироваться. Поэтому ветвь <span class="arithmatex">\(\hat{1}\)</span> нужно отсечь.</p>
<p>Точно так же, если в первом раунде выбрать <span class="arithmatex">\(2\)</span> , то во втором раунде выборы <span class="arithmatex">\(1\)</span> и <span class="arithmatex">\(\hat{1}\)</span> снова создадут дублирующиеся ветви, поэтому и в этом случае ветвь <span class="arithmatex">\(\hat{1}\)</span> нужно отсечь.</p>
<p>Иначе говоря, <strong>наша цель заключается в том, чтобы на каждом раунде выбора каждый из нескольких равных элементов выбирался только один раз</strong>.</p>
<p><img alt="Обрезка повторяющихся перестановок" class="animation-figure" src="../permutations_problem.assets/permutations_ii_pruning.png" /></p>
@@ -5522,7 +5522,7 @@
<p><div style="height: 549px; width: 100%;"><iframe class="pythontutor-iframe" src="https://pythontutor.com/iframe-embed.html#code=def%20backtrack%28%0A%20%20%20%20state%3A%20list%5Bint%5D%2C%20choices%3A%20list%5Bint%5D%2C%20selected%3A%20list%5Bbool%5D%2C%20res%3A%20list%5Blist%5Bint%5D%5D%0A%29%3A%0A%20%20%20%20%22%22%22%D0%90%D0%BB%D0%B3%D0%BE%D1%80%D0%B8%D1%82%D0%BC%20%D0%B1%D1%8D%D0%BA%D1%82%D1%80%D0%B5%D0%BA%D0%B8%D0%BD%D0%B3%D0%B0%3A%20%D0%B2%D1%81%D0%B5%20%D0%BF%D0%B5%D1%80%D0%B5%D1%81%D1%82%D0%B0%D0%BD%D0%BE%D0%B2%D0%BA%D0%B8%20II%22%22%22%0A%20%20%20%20%23%20%D0%9A%D0%BE%D0%B3%D0%B4%D0%B0%20%D0%B4%D0%BB%D0%B8%D0%BD%D0%B0%20%D1%81%D0%BE%D1%81%D1%82%D0%BE%D1%8F%D0%BD%D0%B8%D1%8F%20%D1%80%D0%B0%D0%B2%D0%BD%D0%B0%20%D1%87%D0%B8%D1%81%D0%BB%D1%83%20%D1%8D%D0%BB%D0%B5%D0%BC%D0%B5%D0%BD%D1%82%D0%BE%D0%B2%2C%20%D0%B7%D0%B0%D0%BF%D0%B8%D1%81%D0%B0%D1%82%D1%8C%20%D1%80%D0%B5%D1%88%D0%B5%D0%BD%D0%B8%D0%B5%0A%20%20%20%20if%20len%28state%29%20%3D%3D%20len%28choices%29%3A%0A%20%20%20%20%20%20%20%20res.append%28list%28state%29%29%0A%20%20%20%20%20%20%20%20return%0A%20%20%20%20%23%20%D0%9F%D0%B5%D1%80%D0%B5%D0%B1%D0%BE%D1%80%20%D0%B2%D1%81%D0%B5%D1%85%20%D0%B2%D0%B0%D1%80%D0%B8%D0%B0%D0%BD%D1%82%D0%BE%D0%B2%20%D0%B2%D1%8B%D0%B1%D0%BE%D1%80%D0%B0%0A%20%20%20%20duplicated%20%3D%20set%5Bint%5D%28%29%0A%20%20%20%20for%20i%2C%20choice%20in%20enumerate%28choices%29%3A%0A%20%20%20%20%20%20%20%20%23%20%D0%9E%D1%82%D1%81%D0%B5%D1%87%D0%B5%D0%BD%D0%B8%D0%B5%3A%20%D0%BD%D0%B5%D0%BB%D1%8C%D0%B7%D1%8F%20%D0%B2%D1%8B%D0%B1%D0%B8%D1%80%D0%B0%D1%82%D1%8C%20%D0%BE%D0%B4%D0%B8%D0%BD%20%D0%B8%20%D1%82%D0%BE%D1%82%20%D0%B6%D0%B5%20%D1%8D%D0%BB%D0%B5%D0%BC%D0%B5%D0%BD%D1%82%20%D0%BF%D0%BE%D0%B2%D1%82%D0%BE%D1%80%D0%BD%D0%BE%20%D0%B8%20%D0%BD%D0%B5%D0%BB%D1%8C%D0%B7%D1%8F%20%D0%BF%D0%BE%D0%B2%D1%82%D0%BE%D1%80%D0%BD%D0%BE%20%D0%B2%D1%8B%D0%B1%D0%B8%D1%80%D0%B0%D1%82%D1%8C%20%D1%80%D0%B0%D0%B2%D0%BD%D1%8B%D0%B5%20%D1%8D%D0%BB%D0%B5%D0%BC%D0%B5%D0%BD%D1%82%D1%8B%0A%20%20%20%20%20%20%20%20if%20not%20selected%5Bi%5D%20and%20choice%20not%20in%20duplicated%3A%0A%20%20%20%20%20%20%20%20%20%20%20%20%23%20%D0%9F%D0%BE%D0%BF%D1%8B%D1%82%D0%BA%D0%B0%3A%20%D1%81%D0%B4%D0%B5%D0%BB%D0%B0%D1%82%D1%8C%20%D0%B2%D1%8B%D0%B1%D0%BE%D1%80%20%D0%B8%20%D0%BE%D0%B1%D0%BD%D0%BE%D0%B2%D0%B8%D1%82%D1%8C%20%D1%81%D0%BE%D1%81%D1%82%D0%BE%D1%8F%D0%BD%D0%B8%D0%B5%0A%20%20%20%20%20%20%20%20%20%20%20%20duplicated.add%28choice%29%20%20%23%20%D0%97%D0%B0%D0%BF%D0%B8%D1%81%D0%B0%D1%82%D1%8C%20%D0%B7%D0%BD%D0%B0%D1%87%D0%B5%D0%BD%D0%B8%D1%8F%20%D1%83%D0%B6%D0%B5%20%D0%B2%D1%8B%D0%B1%D1%80%D0%B0%D0%BD%D0%BD%D1%8B%D1%85%20%D1%8D%D0%BB%D0%B5%D0%BC%D0%B5%D0%BD%D1%82%D0%BE%D0%B2%0A%20%20%20%20%20%20%20%20%20%20%20%20selected%5Bi%5D%20%3D%20True%0A%20%20%20%20%20%20%20%20%20%20%20%20state.append%28choice%29%0A%20%20%20%20%20%20%20%20%20%20%20%20%23%20%D0%9F%D0%B5%D1%80%D0%B5%D0%B9%D1%82%D0%B8%20%D0%BA%20%D1%81%D0%BB%D0%B5%D0%B4%D1%83%D1%8E%D1%89%D0%B5%D0%BC%D1%83%20%D0%B2%D1%8B%D0%B1%D0%BE%D1%80%D1%83%0A%20%20%20%20%20%20%20%20%20%20%20%20backtrack%28state%2C%20choices%2C%20selected%2C%20res%29%0A%20%20%20%20%20%20%20%20%20%20%20%20%23%20%D0%9E%D1%82%D0%BA%D0%B0%D1%82%3A%20%D0%BE%D1%82%D0%BC%D0%B5%D0%BD%D0%B8%D1%82%D1%8C%20%D0%B2%D1%8B%D0%B1%D0%BE%D1%80%20%D0%B8%20%D0%B2%D0%BE%D1%81%D1%81%D1%82%D0%B0%D0%BD%D0%BE%D0%B2%D0%B8%D1%82%D1%8C%20%D0%BF%D1%80%D0%B5%D0%B4%D1%8B%D0%B4%D1%83%D1%89%D0%B5%D0%B5%20%D1%81%D0%BE%D1%81%D1%82%D0%BE%D1%8F%D0%BD%D0%B8%D0%B5%0A%20%20%20%20%20%20%20%20%20%20%20%20selected%5Bi%5D%20%3D%20False%0A%20%20%20%20%20%20%20%20%20%20%20%20state.pop%28%29%0A%0A%0Adef%20permutations_ii%28nums%3A%20list%5Bint%5D%29%20-%3E%20list%5Blist%5Bint%5D%5D%3A%0A%20%20%20%20%22%22%22%D0%92%D1%81%D0%B5%20%D0%BF%D0%B5%D1%80%D0%B5%D1%81%D1%82%D0%B0%D0%BD%D0%BE%D0%B2%D0%BA%D0%B8%20II%22%22%22%0A%20%20%20%20res%20%3D%20%5B%5D%0A%20%20%20%20backtrack%28state%3D%5B%5D%2C%20choices%3Dnums%2C%20selected%3D%5BFalse%5D%20%2A%20len%28nums%29%2C%20res%3Dres%29%0A%20%20%20%20return%20res%0A%0A%0A%22%22%22Driver%20Code%22%22%22%0Aif%20__name__%20%3D%3D%20%22__main__%22%3A%0A%20%20%20%20nums%20%3D%20%5B1%2C%202%2C%202%5D%0A%0A%20%20%20%20res%20%3D%20permutations_ii%28nums%29%0A%0A%20%20%20%20print%28f%22%D0%92%D1%85%D0%BE%D0%B4%D0%BD%D0%BE%D0%B9%20%D0%BC%D0%B0%D1%81%D1%81%D0%B8%D0%B2%20nums%20%3D%20%7Bnums%7D%22%29%0A%20%20%20%20print%28f%22%D0%92%D1%81%D0%B5%20%D0%BF%D0%B5%D1%80%D0%B5%D1%81%D1%82%D0%B0%D0%BD%D0%BE%D0%B2%D0%BA%D0%B8%20res%20%3D%20%7Bres%7D%22%29&codeDivHeight=472&codeDivWidth=350&cumulative=false&curInstr=13&heapPrimitives=nevernest&origin=opt-frontend.js&py=311&rawInputLstJSON=%5B%5D&textReferences=false"> </iframe></div>
<div style="margin-top: 5px;"><a href="https://pythontutor.com/iframe-embed.html#code=def%20backtrack%28%0A%20%20%20%20state%3A%20list%5Bint%5D%2C%20choices%3A%20list%5Bint%5D%2C%20selected%3A%20list%5Bbool%5D%2C%20res%3A%20list%5Blist%5Bint%5D%5D%0A%29%3A%0A%20%20%20%20%22%22%22%D0%90%D0%BB%D0%B3%D0%BE%D1%80%D0%B8%D1%82%D0%BC%20%D0%B1%D1%8D%D0%BA%D1%82%D1%80%D0%B5%D0%BA%D0%B8%D0%BD%D0%B3%D0%B0%3A%20%D0%B2%D1%81%D0%B5%20%D0%BF%D0%B5%D1%80%D0%B5%D1%81%D1%82%D0%B0%D0%BD%D0%BE%D0%B2%D0%BA%D0%B8%20II%22%22%22%0A%20%20%20%20%23%20%D0%9A%D0%BE%D0%B3%D0%B4%D0%B0%20%D0%B4%D0%BB%D0%B8%D0%BD%D0%B0%20%D1%81%D0%BE%D1%81%D1%82%D0%BE%D1%8F%D0%BD%D0%B8%D1%8F%20%D1%80%D0%B0%D0%B2%D0%BD%D0%B0%20%D1%87%D0%B8%D1%81%D0%BB%D1%83%20%D1%8D%D0%BB%D0%B5%D0%BC%D0%B5%D0%BD%D1%82%D0%BE%D0%B2%2C%20%D0%B7%D0%B0%D0%BF%D0%B8%D1%81%D0%B0%D1%82%D1%8C%20%D1%80%D0%B5%D1%88%D0%B5%D0%BD%D0%B8%D0%B5%0A%20%20%20%20if%20len%28state%29%20%3D%3D%20len%28choices%29%3A%0A%20%20%20%20%20%20%20%20res.append%28list%28state%29%29%0A%20%20%20%20%20%20%20%20return%0A%20%20%20%20%23%20%D0%9F%D0%B5%D1%80%D0%B5%D0%B1%D0%BE%D1%80%20%D0%B2%D1%81%D0%B5%D1%85%20%D0%B2%D0%B0%D1%80%D0%B8%D0%B0%D0%BD%D1%82%D0%BE%D0%B2%20%D0%B2%D1%8B%D0%B1%D0%BE%D1%80%D0%B0%0A%20%20%20%20duplicated%20%3D%20set%5Bint%5D%28%29%0A%20%20%20%20for%20i%2C%20choice%20in%20enumerate%28choices%29%3A%0A%20%20%20%20%20%20%20%20%23%20%D0%9E%D1%82%D1%81%D0%B5%D1%87%D0%B5%D0%BD%D0%B8%D0%B5%3A%20%D0%BD%D0%B5%D0%BB%D1%8C%D0%B7%D1%8F%20%D0%B2%D1%8B%D0%B1%D0%B8%D1%80%D0%B0%D1%82%D1%8C%20%D0%BE%D0%B4%D0%B8%D0%BD%20%D0%B8%20%D1%82%D0%BE%D1%82%20%D0%B6%D0%B5%20%D1%8D%D0%BB%D0%B5%D0%BC%D0%B5%D0%BD%D1%82%20%D0%BF%D0%BE%D0%B2%D1%82%D0%BE%D1%80%D0%BD%D0%BE%20%D0%B8%20%D0%BD%D0%B5%D0%BB%D1%8C%D0%B7%D1%8F%20%D0%BF%D0%BE%D0%B2%D1%82%D0%BE%D1%80%D0%BD%D0%BE%20%D0%B2%D1%8B%D0%B1%D0%B8%D1%80%D0%B0%D1%82%D1%8C%20%D1%80%D0%B0%D0%B2%D0%BD%D1%8B%D0%B5%20%D1%8D%D0%BB%D0%B5%D0%BC%D0%B5%D0%BD%D1%82%D1%8B%0A%20%20%20%20%20%20%20%20if%20not%20selected%5Bi%5D%20and%20choice%20not%20in%20duplicated%3A%0A%20%20%20%20%20%20%20%20%20%20%20%20%23%20%D0%9F%D0%BE%D0%BF%D1%8B%D1%82%D0%BA%D0%B0%3A%20%D1%81%D0%B4%D0%B5%D0%BB%D0%B0%D1%82%D1%8C%20%D0%B2%D1%8B%D0%B1%D0%BE%D1%80%20%D0%B8%20%D0%BE%D0%B1%D0%BD%D0%BE%D0%B2%D0%B8%D1%82%D1%8C%20%D1%81%D0%BE%D1%81%D1%82%D0%BE%D1%8F%D0%BD%D0%B8%D0%B5%0A%20%20%20%20%20%20%20%20%20%20%20%20duplicated.add%28choice%29%20%20%23%20%D0%97%D0%B0%D0%BF%D0%B8%D1%81%D0%B0%D1%82%D1%8C%20%D0%B7%D0%BD%D0%B0%D1%87%D0%B5%D0%BD%D0%B8%D1%8F%20%D1%83%D0%B6%D0%B5%20%D0%B2%D1%8B%D0%B1%D1%80%D0%B0%D0%BD%D0%BD%D1%8B%D1%85%20%D1%8D%D0%BB%D0%B5%D0%BC%D0%B5%D0%BD%D1%82%D0%BE%D0%B2%0A%20%20%20%20%20%20%20%20%20%20%20%20selected%5Bi%5D%20%3D%20True%0A%20%20%20%20%20%20%20%20%20%20%20%20state.append%28choice%29%0A%20%20%20%20%20%20%20%20%20%20%20%20%23%20%D0%9F%D0%B5%D1%80%D0%B5%D0%B9%D1%82%D0%B8%20%D0%BA%20%D1%81%D0%BB%D0%B5%D0%B4%D1%83%D1%8E%D1%89%D0%B5%D0%BC%D1%83%20%D0%B2%D1%8B%D0%B1%D0%BE%D1%80%D1%83%0A%20%20%20%20%20%20%20%20%20%20%20%20backtrack%28state%2C%20choices%2C%20selected%2C%20res%29%0A%20%20%20%20%20%20%20%20%20%20%20%20%23%20%D0%9E%D1%82%D0%BA%D0%B0%D1%82%3A%20%D0%BE%D1%82%D0%BC%D0%B5%D0%BD%D0%B8%D1%82%D1%8C%20%D0%B2%D1%8B%D0%B1%D0%BE%D1%80%20%D0%B8%20%D0%B2%D0%BE%D1%81%D1%81%D1%82%D0%B0%D0%BD%D0%BE%D0%B2%D0%B8%D1%82%D1%8C%20%D0%BF%D1%80%D0%B5%D0%B4%D1%8B%D0%B4%D1%83%D1%89%D0%B5%D0%B5%20%D1%81%D0%BE%D1%81%D1%82%D0%BE%D1%8F%D0%BD%D0%B8%D0%B5%0A%20%20%20%20%20%20%20%20%20%20%20%20selected%5Bi%5D%20%3D%20False%0A%20%20%20%20%20%20%20%20%20%20%20%20state.pop%28%29%0A%0A%0Adef%20permutations_ii%28nums%3A%20list%5Bint%5D%29%20-%3E%20list%5Blist%5Bint%5D%5D%3A%0A%20%20%20%20%22%22%22%D0%92%D1%81%D0%B5%20%D0%BF%D0%B5%D1%80%D0%B5%D1%81%D1%82%D0%B0%D0%BD%D0%BE%D0%B2%D0%BA%D0%B8%20II%22%22%22%0A%20%20%20%20res%20%3D%20%5B%5D%0A%20%20%20%20backtrack%28state%3D%5B%5D%2C%20choices%3Dnums%2C%20selected%3D%5BFalse%5D%20%2A%20len%28nums%29%2C%20res%3Dres%29%0A%20%20%20%20return%20res%0A%0A%0A%22%22%22Driver%20Code%22%22%22%0Aif%20__name__%20%3D%3D%20%22__main__%22%3A%0A%20%20%20%20nums%20%3D%20%5B1%2C%202%2C%202%5D%0A%0A%20%20%20%20res%20%3D%20permutations_ii%28nums%29%0A%0A%20%20%20%20print%28f%22%D0%92%D1%85%D0%BE%D0%B4%D0%BD%D0%BE%D0%B9%20%D0%BC%D0%B0%D1%81%D1%81%D0%B8%D0%B2%20nums%20%3D%20%7Bnums%7D%22%29%0A%20%20%20%20print%28f%22%D0%92%D1%81%D0%B5%20%D0%BF%D0%B5%D1%80%D0%B5%D1%81%D1%82%D0%B0%D0%BD%D0%BE%D0%B2%D0%BA%D0%B8%20res%20%3D%20%7Bres%7D%22%29&codeDivHeight=800&codeDivWidth=600&cumulative=false&curInstr=13&heapPrimitives=nevernest&origin=opt-frontend.js&py=311&rawInputLstJSON=%5B%5D&textReferences=false" target="_blank" rel="noopener noreferrer">Во весь экран &gt;</a></div></p>
</details>
<p>Если предположить, что все элементы попарно различны, то из <span class="arithmatex">\(n\)</span> элементов можно получить <span class="arithmatex">\(n!\)</span> перестановок; при записи результата требуется копировать список длины <span class="arithmatex">\(n\)</span> , что занимает <span class="arithmatex">\(O(n)\)</span> времени. <strong>Следовательно, временная сложность равна <span class="arithmatex">\(O(n!n)\)</span></strong> .</p>
<p>Если предположить, что все элементы попарно различны, то из <span class="arithmatex">\(n\)</span> элементов можно получить <span class="arithmatex">\(n!\)</span> перестановок. При записи результата требуется копировать список длины <span class="arithmatex">\(n\)</span> , что занимает <span class="arithmatex">\(O(n)\)</span> времени. <strong>Следовательно, временная сложность равна <span class="arithmatex">\(O(n!n)\)</span></strong> .</p>
<p>Максимальная глубина рекурсии равна <span class="arithmatex">\(n\)</span> , что требует <span class="arithmatex">\(O(n)\)</span> стековой памяти. Массив <code>selected</code> занимает <span class="arithmatex">\(O(n)\)</span> пространства. Одновременно может существовать до <span class="arithmatex">\(n\)</span> хеш-множеств <code>duplicated</code> , что дает <span class="arithmatex">\(O(n^2)\)</span> памяти. <strong>Следовательно, пространственная сложность равна <span class="arithmatex">\(O(n^2)\)</span></strong> .</p>
<h3 id="3">3. &nbsp; Сравнение двух видов обрезки<a class="headerlink" href="#3" title="Permanent link">&para;</a></h3>
<p>Обратите внимание: хотя и <code>selected</code> , и <code>duplicated</code> используются для обрезки, их цели различаются.</p>
@@ -4494,7 +4494,7 @@
<h2 id="1331">13.3.1 &nbsp; Случай без повторяющихся элементов<a class="headerlink" href="#1331" title="Permanent link">&para;</a></h2>
<div class="admonition question">
<p class="admonition-title">Question</p>
<p>Дан массив положительных целых чисел <code>nums</code> и целое положительное значение <code>target</code> . Найдите все возможные комбинации, сумма элементов которых равна <code>target</code> . Во входном массиве нет повторяющихся элементов, и каждый элемент можно выбирать неограниченное число раз. Верните эти комбинации в виде списка; в результате не должно быть повторяющихся комбинаций.</p>
<p>Дан массив положительных целых чисел <code>nums</code> и целое положительное значение <code>target</code> . Найдите все возможные комбинации, сумма элементов которых равна <code>target</code> . Во входном массиве нет повторяющихся элементов, и каждый элемент можно выбирать неограниченное число раз. Верните эти комбинации в виде списка. В результате не должно быть повторяющихся комбинаций.</p>
</div>
<p>Например, для входного множества <span class="arithmatex">\(\{3, 4, 5\}\)</span> и целевого значения <span class="arithmatex">\(9\)</span> решениями будут <span class="arithmatex">\(\{3, 3, 3\}\)</span> и <span class="arithmatex">\(\{4, 5\}\)</span> . При этом важно учитывать два обстоятельства.</p>
<ul>
@@ -4502,7 +4502,7 @@
<li>Подмножество не различает порядок элементов, поэтому <span class="arithmatex">\(\{4, 5\}\)</span> и <span class="arithmatex">\(\{5, 4\}\)</span> считаются одним и тем же подмножеством.</li>
</ul>
<h3 id="1">1. &nbsp; Отталкиваемся от решения задачи о перестановках<a class="headerlink" href="#1" title="Permanent link">&para;</a></h3>
<p>Как и в задаче о перестановках, можно представлять построение подмножеств как результат последовательности выборов и во время выбора динамически обновлять "сумму элементов"; когда эта сумма становится равной <code>target</code> , соответствующее подмножество записывается в список результатов.</p>
<p>Как и в задаче о перестановках, можно представлять построение подмножеств как результат последовательности выборов и во время выбора динамически обновлять «сумму элементов». Когда эта сумма становится равной <code>target</code> , соответствующее подмножество записывается в список результатов.</p>
<p>Однако в отличие от задачи о перестановках <strong>в этой задаче элементы множества можно выбирать неограниченное число раз</strong>, поэтому нам не нужен булев список <code>selected</code> для записи того, был ли выбран элемент. Можно слегка изменить код для перестановок и получить первоначальную версию решения:</p>
<div class="tabbed-set tabbed-alternate" data-tabs="1:13"><input checked="checked" id="__tabbed_1_1" name="__tabbed_1" type="radio" /><input id="__tabbed_1_2" name="__tabbed_1" type="radio" /><input id="__tabbed_1_3" name="__tabbed_1" type="radio" /><input id="__tabbed_1_4" name="__tabbed_1" type="radio" /><input id="__tabbed_1_5" name="__tabbed_1" type="radio" /><input id="__tabbed_1_6" name="__tabbed_1" type="radio" /><input id="__tabbed_1_7" name="__tabbed_1" type="radio" /><input id="__tabbed_1_8" name="__tabbed_1" type="radio" /><input id="__tabbed_1_9" name="__tabbed_1" type="radio" /><input id="__tabbed_1_10" name="__tabbed_1" type="radio" /><input id="__tabbed_1_11" name="__tabbed_1" type="radio" /><input id="__tabbed_1_12" name="__tabbed_1" type="radio" /><input id="__tabbed_1_13" name="__tabbed_1" type="radio" /><div class="tabbed-labels"><label for="__tabbed_1_1">Python</label><label for="__tabbed_1_2">C++</label><label for="__tabbed_1_3">Java</label><label for="__tabbed_1_4">C#</label><label for="__tabbed_1_5">Go</label><label for="__tabbed_1_6">Swift</label><label for="__tabbed_1_7">JS</label><label for="__tabbed_1_8">TS</label><label for="__tabbed_1_9">Dart</label><label for="__tabbed_1_10">Rust</label><label for="__tabbed_1_11">C</label><label for="__tabbed_1_12">Kotlin</label><label for="__tabbed_1_13">Ruby</label></div>
<div class="tabbed-content">
@@ -4999,7 +4999,7 @@
<li>Сравнение подмножеств (то есть массивов) само по себе довольно затратно: сначала приходится сортировать массивы, а затем поэлементно сравнивать их.</li>
</ul>
<h3 id="2">2. &nbsp; Обрезка повторяющихся подмножеств<a class="headerlink" href="#2" title="Permanent link">&para;</a></h3>
<p><strong>Поэтому стоит выполнять устранение дубликатов прямо во время поиска, с помощью обрезки</strong>. Посмотрите на рисунок 13-11: повторяющиеся подмножества возникают тогда, когда элементы массива выбираются в разном порядке, например так.</p>
<p><strong>Поэтому стоит выполнять устранение дубликатов прямо во время поиска, с помощью обрезки</strong>. Как видно на рисунке 13-11, повторяющиеся подмножества возникают тогда, когда элементы массива выбираются в разном порядке, например так.</p>
<ol>
<li>Если в первом и втором раундах выбрать соответственно <span class="arithmatex">\(3\)</span> и <span class="arithmatex">\(4\)</span> , то будут сгенерированы все подмножества, содержащие эти два элемента, и их можно обозначить как <span class="arithmatex">\([3, 4, \dots]\)</span> .</li>
<li>После этого, если в первом раунде выбрать <span class="arithmatex">\(4\)</span> , <strong>то во втором раунде нужно пропустить <span class="arithmatex">\(3\)</span></strong> , потому что подмножества <span class="arithmatex">\([4, 3, \dots]\)</span> полностью дублируют подмножества, уже построенные на шаге <code>1.</code> .</li>
@@ -5013,13 +5013,13 @@
<p><img alt="Повторяющиеся подмножества из-за разного порядка выбора" class="animation-figure" src="../subset_sum_problem.assets/subset_sum_i_pruning.png" /></p>
<p align="center"> Рисунок 13-11 &nbsp; Повторяющиеся подмножества из-за разного порядка выбора </p>
<p>В общем виде, если входной массив имеет вид <span class="arithmatex">\([x_1, x_2, \dots, x_n]\)</span> , а последовательность выборов в ходе поиска равна <span class="arithmatex">\([x_{i_1}, x_{i_2}, \dots, x_{i_m}]\)</span> , то она должна удовлетворять условию <span class="arithmatex">\(i_1 \leq i_2 \leq \dots \leq i_m\)</span> ; <strong>все последовательности выборов, не удовлетворяющие этому условию, приводят к дубликатам и должны отсекаться</strong>.</p>
<p>В общем виде, если входной массив имеет вид <span class="arithmatex">\([x_1, x_2, \dots, x_n]\)</span> , а последовательность выборов в ходе поиска равна <span class="arithmatex">\([x_{i_1}, x_{i_2}, \dots, x_{i_m}]\)</span> , то она должна удовлетворять условию <span class="arithmatex">\(i_1 \leq i_2 \leq \dots \leq i_m\)</span>. <strong>Все последовательности выборов, не удовлетворяющие этому условию, приводят к дубликатам и должны отсекаться</strong>.</p>
<h3 id="3">3. &nbsp; Реализация кода<a class="headerlink" href="#3" title="Permanent link">&para;</a></h3>
<p>Чтобы реализовать такую обрезку, инициализируем переменную <code>start</code> , которая будет указывать начальную точку обхода. <strong>После выбора элемента <span class="arithmatex">\(x_i\)</span> следующий раунд начинается с индекса <span class="arithmatex">\(i\)</span></strong>. Благодаря этому последовательность выборов всегда удовлетворяет условию <span class="arithmatex">\(i_1 \leq i_2 \leq \dots \leq i_m\)</span> , а значит, каждое подмножество создается только один раз.</p>
<p>Помимо этого, мы внесем в код еще два улучшения.</p>
<ul>
<li>Перед началом поиска отсортируем массив <code>nums</code> . Тогда при обходе всех вариантов <strong>можно сразу прервать цикл, как только сумма подмножества превысит <code>target</code></strong> , потому что все последующие элементы будут еще больше и их сумма тоже превысит <code>target</code> .</li>
<li>Откажемся от отдельной переменной суммы <code>total</code> и <strong>будем учитывать сумму через вычитание из <code>target</code></strong> ; когда <code>target</code> станет равным <span class="arithmatex">\(0\)</span> , решение фиксируется.</li>
<li>Откажемся от отдельной переменной суммы <code>total</code> и <strong>будем учитывать сумму через вычитание из <code>target</code></strong>. Когда <code>target</code> станет равным <span class="arithmatex">\(0\)</span> , решение фиксируется.</li>
</ul>
<div class="tabbed-set tabbed-alternate" data-tabs="2:13"><input checked="checked" id="__tabbed_2_1" name="__tabbed_2" type="radio" /><input id="__tabbed_2_2" name="__tabbed_2" type="radio" /><input id="__tabbed_2_3" name="__tabbed_2" type="radio" /><input id="__tabbed_2_4" name="__tabbed_2" type="radio" /><input id="__tabbed_2_5" name="__tabbed_2" type="radio" /><input id="__tabbed_2_6" name="__tabbed_2" type="radio" /><input id="__tabbed_2_7" name="__tabbed_2" type="radio" /><input id="__tabbed_2_8" name="__tabbed_2" type="radio" /><input id="__tabbed_2_9" name="__tabbed_2" type="radio" /><input id="__tabbed_2_10" name="__tabbed_2" type="radio" /><input id="__tabbed_2_11" name="__tabbed_2" type="radio" /><input id="__tabbed_2_12" name="__tabbed_2" type="radio" /><input id="__tabbed_2_13" name="__tabbed_2" type="radio" /><div class="tabbed-labels"><label for="__tabbed_2_1">Python</label><label for="__tabbed_2_2">C++</label><label for="__tabbed_2_3">Java</label><label for="__tabbed_2_4">C#</label><label for="__tabbed_2_5">Go</label><label for="__tabbed_2_6">Swift</label><label for="__tabbed_2_7">JS</label><label for="__tabbed_2_8">TS</label><label for="__tabbed_2_9">Dart</label><label for="__tabbed_2_10">Rust</label><label for="__tabbed_2_11">C</label><label for="__tabbed_2_12">Kotlin</label><label for="__tabbed_2_13">Ruby</label></div>
<div class="tabbed-content">
@@ -5526,10 +5526,10 @@
<h2 id="1332">13.3.2 &nbsp; Учет повторяющихся элементов<a class="headerlink" href="#1332" title="Permanent link">&para;</a></h2>
<div class="admonition question">
<p class="admonition-title">Question</p>
<p>Дан массив положительных целых чисел <code>nums</code> и целое положительное значение <code>target</code> . Найдите все возможные комбинации, сумма элементов которых равна <code>target</code> . <strong>Во входном массиве могут присутствовать повторяющиеся элементы, и каждый элемент разрешено выбирать только один раз</strong>. Верните эти комбинации в виде списка; в результате не должно быть повторяющихся комбинаций.</p>
<p>Дан массив положительных целых чисел <code>nums</code> и целое положительное значение <code>target</code> . Найдите все возможные комбинации, сумма элементов которых равна <code>target</code> . <strong>Во входном массиве могут присутствовать повторяющиеся элементы, и каждый элемент разрешено выбирать только один раз</strong>. Верните эти комбинации в виде списка. В результате не должно быть повторяющихся комбинаций.</p>
</div>
<p>По сравнению с предыдущей задачей <strong>во входном массиве теперь могут присутствовать повторяющиеся элементы</strong>, и это создает новую проблему. Например, если дан массив <span class="arithmatex">\([4, \hat{4}, 5]\)</span> и целевое значение <span class="arithmatex">\(9\)</span> , то существующий код вернет результат <span class="arithmatex">\([4, 5], [\hat{4}, 5]\)</span> , то есть с повторяющимся подмножеством.</p>
<p><strong>Причина появления дублей в том, что равные элементы выбираются несколько раз в одном и том же раунде</strong>. На рисунке 13-13 в первом раунде существует три варианта выбора, и два из них равны <span class="arithmatex">\(4\)</span> ; из-за этого появляются две дублирующиеся ветви поиска и, соответственно, повторяющиеся подмножества. Точно так же два элемента <span class="arithmatex">\(4\)</span> во втором раунде тоже порождают дубликаты.</p>
<p><strong>Причина появления дублей в том, что равные элементы выбираются несколько раз в одном и том же раунде</strong>. На рисунке 13-13 в первом раунде существует три варианта выбора, и два из них равны <span class="arithmatex">\(4\)</span>. Из-за этого появляются две дублирующиеся ветви поиска и, соответственно, повторяющиеся подмножества. Точно так же два элемента <span class="arithmatex">\(4\)</span> во втором раунде тоже порождают дубликаты.</p>
<p><img alt="Повторяющиеся подмножества из-за равных элементов" class="animation-figure" src="../subset_sum_problem.assets/subset_sum_ii_repeat.png" /></p>
<p align="center"> Рисунок 13-13 &nbsp; Повторяющиеся подмножества из-за равных элементов </p>
+4 -4
View File
@@ -4360,11 +4360,11 @@
<h3 id="1">1. &nbsp; Ключевые выводы<a class="headerlink" href="#1" title="Permanent link">&para;</a></h3>
<ul>
<li>Алгоритм поиска с возвратом по своей сути является методом полного перебора: он ищет решения путем обхода пространства решений в глубину. Во время поиска он фиксирует решения, удовлетворяющие условиям, пока не найдет все такие решения или пока обход не завершится.</li>
<li>Процесс поиска с возвратом состоит из двух частей: попытки и отката. Он с помощью поиска в глубину пробует разные варианты выбора; когда встречается состояние, не удовлетворяющее ограничениям, алгоритм отменяет предыдущий выбор, возвращается к прошлому состоянию и продолжает пробовать другие варианты. Попытка и откат являются двумя противоположными по направлению действиями.</li>
<li>Процесс поиска с возвратом состоит из двух частей: попытки и отката. Он с помощью поиска в глубину пробует разные варианты выбора. Когда встречается состояние, не удовлетворяющее ограничениям, алгоритм отменяет предыдущий выбор, возвращается к прошлому состоянию и продолжает пробовать другие варианты. Попытка и откат являются двумя противоположными по направлению действиями.</li>
<li>Задачи поиска с возвратом обычно содержат несколько ограничений, которые можно использовать для обрезки. Обрезка позволяет заранее завершать ненужные ветви поиска и тем самым значительно повышать эффективность.</li>
<li>Алгоритм поиска с возвратом в первую очередь применяется для решения поисковых задач и задач с ограничениями. Задачи комбинаторной оптимизации тоже можно решать с его помощью, но для них часто существуют более эффективные или более подходящие методы.</li>
<li>Задача о перестановках нацелена на поиск всех возможных перестановок элементов данного множества. Мы используем массив для записи того, был ли выбран каждый элемент, и отсекаем ветви, где один и тот же элемент выбирается повторно, чтобы гарантировать однократный выбор каждого элемента.</li>
<li>В задаче о перестановках, если во множестве присутствуют повторяющиеся элементы, в итоговом результате возникнут повторяющиеся перестановки. Поэтому нужно ограничить выбор равных элементов так, чтобы в каждом раунде каждый из них выбирался только один раз; обычно это реализуется с помощью хеш-множества.</li>
<li>В задаче о перестановках, если во множестве присутствуют повторяющиеся элементы, в итоговом результате возникнут повторяющиеся перестановки. Поэтому нужно ограничить выбор равных элементов так, чтобы в каждом раунде каждый из них выбирался только один раз. Обычно это реализуется с помощью хеш-множества.</li>
<li>Цель задачи о сумме подмножеств - найти все подмножества данного множества, сумма которых равна целевому значению. В множестве порядок элементов не важен, однако процесс поиска порождает результаты во всех возможных порядках, из-за чего появляются повторяющиеся подмножества. Поэтому перед запуском поиска с возвратом мы сортируем данные и вводим переменную, указывающую начальную точку обхода в каждом раунде, чтобы отсечь ветви, создающие дубликаты.</li>
<li>В задаче о сумме подмножеств равные элементы массива также порождают повторяющиеся множества. При наличии предварительной сортировки их можно отсекать, проверяя равенство соседних элементов, и тем самым гарантировать, что в каждом раунде равные элементы будут выбираться только один раз.</li>
<li>Задача о <span class="arithmatex">\(n\)</span> ферзях состоит в поиске способов разместить <span class="arithmatex">\(n\)</span> ферзей на доске размера <span class="arithmatex">\(n \times n\)</span> так, чтобы никакие два ферзя не атаковали друг друга. Ограничения этой задачи включают строки, столбцы, главные диагонали и побочные диагонали. Чтобы выполнить ограничение по строкам, используется построчная стратегия размещения, гарантирующая по одному ферзю в каждой строке.</li>
@@ -4372,10 +4372,10 @@
</ul>
<h3 id="2">2. &nbsp; Вопросы и ответы<a class="headerlink" href="#2" title="Permanent link">&para;</a></h3>
<p><strong>Q</strong>: Как понять связь между поиском с возвратом и рекурсией?</p>
<p>В целом поиск с возвратом - это скорее "алгоритмическая стратегия", а рекурсия больше похожа на "инструмент".</p>
<p>В целом поиск с возвратом - это скорее «алгоритмическая стратегия», а рекурсия больше похожа на «инструмент».</p>
<ul>
<li>Алгоритмы поиска с возвратом обычно реализуются на основе рекурсии. Однако поиск с возвратом - это лишь один из вариантов применения рекурсии, а именно ее использование в поисковых задачах.</li>
<li>Структура рекурсии отражает парадигму разбиения на подзадачи и часто применяется для решения задач "разделяй и властвуй", поиска с возвратом, динамического программирования (мемоизированной рекурсии) и других подобных задач.</li>
<li>Структура рекурсии отражает парадигму разбиения на подзадачи и часто применяется для решения задач «разделяй и властвуй», поиска с возвратом, динамического программирования (мемоизированной рекурсии) и других подобных задач.</li>
</ul>
<!-- Source file information -->
@@ -4706,7 +4706,7 @@
<p><div style="height: 423px; width: 100%;"><iframe class="pythontutor-iframe" src="https://pythontutor.com/iframe-embed.html#code=def%20for_loop%28n%3A%20int%29%20-%3E%20int%3A%0A%20%20%20%20%22%22%22%D0%A6%D0%B8%D0%BA%D0%BB%20for%22%22%22%0A%20%20%20%20res%20%3D%200%0A%20%20%20%20%23%20%D0%A6%D0%B8%D0%BA%D0%BB%D0%B8%D1%87%D0%B5%D1%81%D0%BA%D0%BE%D0%B5%20%D1%81%D1%83%D0%BC%D0%BC%D0%B8%D1%80%D0%BE%D0%B2%D0%B0%D0%BD%D0%B8%D0%B5%201%2C%202%2C%20...%2C%20n-1%2C%20n%0A%20%20%20%20for%20i%20in%20range%281%2C%20n%20%2B%201%29%3A%0A%20%20%20%20%20%20%20%20res%20%2B%3D%20i%0A%20%20%20%20return%20res%0A%0A%22%22%22Driver%20Code%22%22%22%0Aif%20__name__%20%3D%3D%20%22__main__%22%3A%0A%20%20%20%20n%20%3D%205%0A%20%20%20%20res%20%3D%20for_loop%28n%29%0A%20%20%20%20print%28f%22%5Cn%D0%A0%D0%B5%D0%B7%D1%83%D0%BB%D1%8C%D1%82%D0%B0%D1%82%20%D1%81%D1%83%D0%BC%D0%BC%D0%B8%D1%80%D0%BE%D0%B2%D0%B0%D0%BD%D0%B8%D1%8F%20%D0%B2%20%D1%86%D0%B8%D0%BA%D0%BB%D0%B5%20for%20res%20%3D%20%7Bres%7D%22%29&codeDivHeight=472&codeDivWidth=350&cumulative=false&curInstr=3&heapPrimitives=nevernest&origin=opt-frontend.js&py=311&rawInputLstJSON=%5B%5D&"> </iframe></div>
<div style="margin-top: 5px;"><a href="https://pythontutor.com/iframe-embed.html#code=def%20for_loop%28n%3A%20int%29%20-%3E%20int%3A%0A%20%20%20%20%22%22%22%D0%A6%D0%B8%D0%BA%D0%BB%20for%22%22%22%0A%20%20%20%20res%20%3D%200%0A%20%20%20%20%23%20%D0%A6%D0%B8%D0%BA%D0%BB%D0%B8%D1%87%D0%B5%D1%81%D0%BA%D0%BE%D0%B5%20%D1%81%D1%83%D0%BC%D0%BC%D0%B8%D1%80%D0%BE%D0%B2%D0%B0%D0%BD%D0%B8%D0%B5%201%2C%202%2C%20...%2C%20n-1%2C%20n%0A%20%20%20%20for%20i%20in%20range%281%2C%20n%20%2B%201%29%3A%0A%20%20%20%20%20%20%20%20res%20%2B%3D%20i%0A%20%20%20%20return%20res%0A%0A%22%22%22Driver%20Code%22%22%22%0Aif%20__name__%20%3D%3D%20%22__main__%22%3A%0A%20%20%20%20n%20%3D%205%0A%20%20%20%20res%20%3D%20for_loop%28n%29%0A%20%20%20%20print%28f%22%5Cn%D0%A0%D0%B5%D0%B7%D1%83%D0%BB%D1%8C%D1%82%D0%B0%D1%82%20%D1%81%D1%83%D0%BC%D0%BC%D0%B8%D1%80%D0%BE%D0%B2%D0%B0%D0%BD%D0%B8%D1%8F%20%D0%B2%20%D1%86%D0%B8%D0%BA%D0%BB%D0%B5%20for%20res%20%3D%20%7Bres%7D%22%29&codeDivHeight=800&codeDivWidth=600&cumulative=false&curInstr=3&heapPrimitives=nevernest&origin=opt-frontend.js&py=311&rawInputLstJSON=%5B%5D&" target="_blank" rel="noopener noreferrer">Во весь экран &gt;</a></div></p>
</details>
<p>Ниже представлена блок-схема этой функции суммирования.</p>
<p>На рисунке 2-1 представлена блок-схема этой функции суммирования.</p>
<p><img alt="Блок-схема функции суммирования" class="animation-figure" src="../iteration_and_recursion.assets/iteration.png" /></p>
<p align="center"> Рисунок 2-1 &nbsp; Блок-схема функции суммирования </p>
@@ -5353,7 +5353,7 @@
<p><div style="height: 459px; width: 100%;"><iframe class="pythontutor-iframe" src="https://pythontutor.com/iframe-embed.html#code=def%20nested_for_loop%28n%3A%20int%29%20-%3E%20str%3A%0A%20%20%20%20%22%22%22%D0%94%D0%B2%D0%BE%D0%B9%D0%BD%D0%BE%D0%B9%20%D1%86%D0%B8%D0%BA%D0%BB%20for%22%22%22%0A%20%20%20%20res%20%3D%20%22%22%0A%20%20%20%20%23%20%D0%A6%D0%B8%D0%BA%D0%BB%20%D0%BF%D0%BE%20i%20%3D%201%2C%202%2C%20...%2C%20n-1%2C%20n%0A%20%20%20%20for%20i%20in%20range%281%2C%20n%20%2B%201%29%3A%0A%20%20%20%20%20%20%20%20%23%20%D0%A6%D0%B8%D0%BA%D0%BB%20%D0%BF%D0%BE%20j%20%3D%201%2C%202%2C%20...%2C%20n-1%2C%20n%0A%20%20%20%20%20%20%20%20for%20j%20in%20range%281%2C%20n%20%2B%201%29%3A%0A%20%20%20%20%20%20%20%20%20%20%20%20res%20%2B%3D%20f%22%28%7Bi%7D%2C%20%7Bj%7D%29%2C%20%22%0A%20%20%20%20return%20res%0A%0A%22%22%22Driver%20Code%22%22%22%0Aif%20__name__%20%3D%3D%20%22__main__%22%3A%0A%20%20%20%20n%20%3D%205%0A%20%20%20%20res%20%3D%20nested_for_loop%28n%29%0A%20%20%20%20print%28f%22%5Cn%D0%A0%D0%B5%D0%B7%D1%83%D0%BB%D1%8C%D1%82%D0%B0%D1%82%20%D0%BE%D0%B1%D1%85%D0%BE%D0%B4%D0%B0%20%D0%B2%20%D0%B4%D0%B2%D0%BE%D0%B9%D0%BD%D0%BE%D0%BC%20%D1%86%D0%B8%D0%BA%D0%BB%D0%B5%20for%20%7Bres%7D%22%29&codeDivHeight=472&codeDivWidth=350&cumulative=false&curInstr=3&heapPrimitives=nevernest&origin=opt-frontend.js&py=311&rawInputLstJSON=%5B%5D&textReferences=false"> </iframe></div>
<div style="margin-top: 5px;"><a href="https://pythontutor.com/iframe-embed.html#code=def%20nested_for_loop%28n%3A%20int%29%20-%3E%20str%3A%0A%20%20%20%20%22%22%22%D0%94%D0%B2%D0%BE%D0%B9%D0%BD%D0%BE%D0%B9%20%D1%86%D0%B8%D0%BA%D0%BB%20for%22%22%22%0A%20%20%20%20res%20%3D%20%22%22%0A%20%20%20%20%23%20%D0%A6%D0%B8%D0%BA%D0%BB%20%D0%BF%D0%BE%20i%20%3D%201%2C%202%2C%20...%2C%20n-1%2C%20n%0A%20%20%20%20for%20i%20in%20range%281%2C%20n%20%2B%201%29%3A%0A%20%20%20%20%20%20%20%20%23%20%D0%A6%D0%B8%D0%BA%D0%BB%20%D0%BF%D0%BE%20j%20%3D%201%2C%202%2C%20...%2C%20n-1%2C%20n%0A%20%20%20%20%20%20%20%20for%20j%20in%20range%281%2C%20n%20%2B%201%29%3A%0A%20%20%20%20%20%20%20%20%20%20%20%20res%20%2B%3D%20f%22%28%7Bi%7D%2C%20%7Bj%7D%29%2C%20%22%0A%20%20%20%20return%20res%0A%0A%22%22%22Driver%20Code%22%22%22%0Aif%20__name__%20%3D%3D%20%22__main__%22%3A%0A%20%20%20%20n%20%3D%205%0A%20%20%20%20res%20%3D%20nested_for_loop%28n%29%0A%20%20%20%20print%28f%22%5Cn%D0%A0%D0%B5%D0%B7%D1%83%D0%BB%D1%8C%D1%82%D0%B0%D1%82%20%D0%BE%D0%B1%D1%85%D0%BE%D0%B4%D0%B0%20%D0%B2%20%D0%B4%D0%B2%D0%BE%D0%B9%D0%BD%D0%BE%D0%BC%20%D1%86%D0%B8%D0%BA%D0%BB%D0%B5%20for%20%7Bres%7D%22%29&codeDivHeight=800&codeDivWidth=600&cumulative=false&curInstr=3&heapPrimitives=nevernest&origin=opt-frontend.js&py=311&rawInputLstJSON=%5B%5D&textReferences=false" target="_blank" rel="noopener noreferrer">Во весь экран &gt;</a></div></p>
</details>
<p>Ниже приведена блок-схема такого вложенного цикла.</p>
<p>На рисунке 2-2 приведена блок-схема такого вложенного цикла.</p>
<p><img alt="Блок-схема вложенного цикла" class="animation-figure" src="../iteration_and_recursion.assets/nested_iteration.png" /></p>
<p align="center"> Рисунок 2-2 &nbsp; Блок-схема вложенного цикла </p>
@@ -5548,7 +5548,7 @@
<p><div style="height: 459px; width: 100%;"><iframe class="pythontutor-iframe" src="https://pythontutor.com/iframe-embed.html#code=def%20recur%28n%3A%20int%29%20-%3E%20int%3A%0A%20%20%20%20%22%22%22%D0%A0%D0%B5%D0%BA%D1%83%D1%80%D1%81%D0%B8%D1%8F%22%22%22%0A%20%20%20%20%23%20%D0%A3%D1%81%D0%BB%D0%BE%D0%B2%D0%B8%D0%B5%20%D0%B7%D0%B0%D0%B2%D0%B5%D1%80%D1%88%D0%B5%D0%BD%D0%B8%D1%8F%0A%20%20%20%20if%20n%20%3D%3D%201%3A%0A%20%20%20%20%20%20%20%20return%201%0A%20%20%20%20%23%20%D0%A0%D0%B5%D0%BA%D1%83%D1%80%D1%81%D0%B8%D1%8F%3A%20%D1%80%D0%B5%D0%BA%D1%83%D1%80%D1%81%D0%B8%D0%B2%D0%BD%D1%8B%D0%B9%20%D0%B2%D1%8B%D0%B7%D0%BE%D0%B2%0A%20%20%20%20res%20%3D%20recur%28n%20-%201%29%0A%20%20%20%20%23%20%D0%92%D0%BE%D0%B7%D0%B2%D1%80%D0%B0%D1%82%3A%20%D0%B2%D0%B5%D1%80%D0%BD%D1%83%D1%82%D1%8C%20%D1%80%D0%B5%D0%B7%D1%83%D0%BB%D1%8C%D1%82%D0%B0%D1%82%0A%20%20%20%20return%20n%20%2B%20res%0A%0A%22%22%22Driver%20Code%22%22%22%0Aif%20__name__%20%3D%3D%20%22__main__%22%3A%0A%20%20%20%20n%20%3D%205%0A%20%20%20%20res%20%3D%20recur%28n%29%0A%20%20%20%20print%28f%22%5Cn%D0%A0%D0%B5%D0%B7%D1%83%D0%BB%D1%8C%D1%82%D0%B0%D1%82%20%D1%81%D1%83%D0%BC%D0%BC%D0%B8%D1%80%D0%BE%D0%B2%D0%B0%D0%BD%D0%B8%D1%8F%20%D0%B2%20%D1%80%D0%B5%D0%BA%D1%83%D1%80%D1%81%D0%B8%D0%B2%D0%BD%D0%BE%D0%B9%20%D1%84%D1%83%D0%BD%D0%BA%D1%86%D0%B8%D0%B8%20res%20%3D%20%7Bres%7D%22%29&codeDivHeight=472&codeDivWidth=350&cumulative=false&curInstr=3&heapPrimitives=nevernest&origin=opt-frontend.js&py=311&rawInputLstJSON=%5B%5D&textReferences=false"> </iframe></div>
<div style="margin-top: 5px;"><a href="https://pythontutor.com/iframe-embed.html#code=def%20recur%28n%3A%20int%29%20-%3E%20int%3A%0A%20%20%20%20%22%22%22%D0%A0%D0%B5%D0%BA%D1%83%D1%80%D1%81%D0%B8%D1%8F%22%22%22%0A%20%20%20%20%23%20%D0%A3%D1%81%D0%BB%D0%BE%D0%B2%D0%B8%D0%B5%20%D0%B7%D0%B0%D0%B2%D0%B5%D1%80%D1%88%D0%B5%D0%BD%D0%B8%D1%8F%0A%20%20%20%20if%20n%20%3D%3D%201%3A%0A%20%20%20%20%20%20%20%20return%201%0A%20%20%20%20%23%20%D0%A0%D0%B5%D0%BA%D1%83%D1%80%D1%81%D0%B8%D1%8F%3A%20%D1%80%D0%B5%D0%BA%D1%83%D1%80%D1%81%D0%B8%D0%B2%D0%BD%D1%8B%D0%B9%20%D0%B2%D1%8B%D0%B7%D0%BE%D0%B2%0A%20%20%20%20res%20%3D%20recur%28n%20-%201%29%0A%20%20%20%20%23%20%D0%92%D0%BE%D0%B7%D0%B2%D1%80%D0%B0%D1%82%3A%20%D0%B2%D0%B5%D1%80%D0%BD%D1%83%D1%82%D1%8C%20%D1%80%D0%B5%D0%B7%D1%83%D0%BB%D1%8C%D1%82%D0%B0%D1%82%0A%20%20%20%20return%20n%20%2B%20res%0A%0A%22%22%22Driver%20Code%22%22%22%0Aif%20__name__%20%3D%3D%20%22__main__%22%3A%0A%20%20%20%20n%20%3D%205%0A%20%20%20%20res%20%3D%20recur%28n%29%0A%20%20%20%20print%28f%22%5Cn%D0%A0%D0%B5%D0%B7%D1%83%D0%BB%D1%8C%D1%82%D0%B0%D1%82%20%D1%81%D1%83%D0%BC%D0%BC%D0%B8%D1%80%D0%BE%D0%B2%D0%B0%D0%BD%D0%B8%D1%8F%20%D0%B2%20%D1%80%D0%B5%D0%BA%D1%83%D1%80%D1%81%D0%B8%D0%B2%D0%BD%D0%BE%D0%B9%20%D1%84%D1%83%D0%BD%D0%BA%D1%86%D0%B8%D0%B8%20res%20%3D%20%7Bres%7D%22%29&codeDivHeight=800&codeDivWidth=600&cumulative=false&curInstr=3&heapPrimitives=nevernest&origin=opt-frontend.js&py=311&rawInputLstJSON=%5B%5D&textReferences=false" target="_blank" rel="noopener noreferrer">Во весь экран &gt;</a></div></p>
</details>
<p>Ниже представлен рекурсивный процесс этой функции.</p>
<p>На рисунке 2-3 представлен рекурсивный процесс этой функции.</p>
<p><img alt="Рекурсивный процесс функции суммирования" class="animation-figure" src="../iteration_and_recursion.assets/recursion_sum.png" /></p>
<p align="center"> Рисунок 2-3 &nbsp; Рекурсивный процесс функции суммирования </p>
@@ -5744,10 +5744,10 @@
<p>Обратите внимание: многие компиляторы и интерпретаторы не поддерживают оптимизацию хвостовой рекурсии. Например, Python по умолчанию такую оптимизацию не выполняет, поэтому даже функция в хвостово-рекурсивной форме все равно может привести к переполнению стека.</p>
</div>
<h3 id="3_1">3. &nbsp; Дерево рекурсии<a class="headerlink" href="#3_1" title="Permanent link">&para;</a></h3>
<p>При решении задач, связанных с алгоритмами типа "разделяй и властвуй", рекурсия часто оказывается более интуитивной и читабельной, чем итерация. Рассмотрим в качестве примера последовательность Фибоначчи.</p>
<p>При решении задач, связанных с алгоритмами типа «разделяй и властвуй», рекурсия часто оказывается более интуитивной и читабельной, чем итерация. Рассмотрим в качестве примера последовательность Фибоначчи.</p>
<div class="admonition question">
<p class="admonition-title">Question</p>
<p>Дана последовательность Фибоначчи <span class="arithmatex">\(0, 1, 1, 2, 3, 5, 8, 13, \dots\)</span> ; найди <span class="arithmatex">\(n\)</span>-й элемент этой последовательности.</p>
<p>Дана последовательность Фибоначчи <span class="arithmatex">\(0, 1, 1, 2, 3, 5, 8, 13, \dots\)</span>. Найди <span class="arithmatex">\(n\)</span>-й элемент этой последовательности.</p>
</div>
<p>Обозначив <span class="arithmatex">\(n\)</span>-й член последовательности Фибоначчи как <span class="arithmatex">\(f(n)\)</span> , можно сформулировать два утверждения.</p>
<ul>
@@ -5935,10 +5935,10 @@
<p><img alt="Дерево рекурсии последовательности Фибоначчи" class="animation-figure" src="../iteration_and_recursion.assets/recursion_tree.png" /></p>
<p align="center"> Рисунок 2-6 &nbsp; Дерево рекурсии последовательности Фибоначчи </p>
<p>По своей сути рекурсия отражает парадигму мышления "разбиение задачи на более мелкие подзадачи", что делает стратегию "разделяй и властвуй" крайне важной.</p>
<p>По своей сути рекурсия отражает парадигму мышления «разбиение задачи на более мелкие подзадачи», что делает стратегию «разделяй и властвуй» крайне важной.</p>
<ul>
<li>С точки зрения <strong>алгоритмов</strong> многие важные алгоритмические стратегии, такие как поиск, сортировка, возврат, "разделяй и властвуй" и динамическое программирование, прямо или косвенно используют этот подход.</li>
<li>С точки зрения <strong>структур данных</strong> рекурсия естественно подходит для решения задач, связанных со списками, деревьями и графами, поскольку они очень хорошо поддаются анализу с использованием идеи "разделяй и властвуй".</li>
<li>С точки зрения <strong>алгоритмов</strong> многие важные алгоритмические стратегии, такие как поиск, сортировка, возврат, «разделяй и властвуй» и динамическое программирование, прямо или косвенно используют этот подход.</li>
<li>С точки зрения <strong>структур данных</strong> рекурсия естественно подходит для решения задач, связанных со списками, деревьями и графами, поскольку они очень хорошо поддаются анализу с использованием идеи «разделяй и властвуй».</li>
</ul>
<h2 id="223">2.2.3 &nbsp; Сравнение<a class="headerlink" href="#223" title="Permanent link">&para;</a></h2>
<p>Подводя итог, можно сказать, что итерация и рекурсия различаются по реализации, производительности и применимости, как показано в таблице 2-1.</p>
@@ -5972,20 +5972,20 @@
<tr>
<td>Сфера использования</td>
<td>Подходит для простых циклических задач, код интуитивно понятен и хорошо читаем</td>
<td>Подходит для разбиения на подзадачи, для структур деревья и графы, алгоритмов "разделяй и властвуй", возврата и т. д.; структура кода проста и ясна</td>
<td>Подходит для разбиения на подзадачи, для структур деревья и графы, алгоритмов «разделяй и властвуй», возврата и т. д.. Структура кода проста и ясна</td>
</tr>
</tbody>
</table>
</div>
<div class="admonition tip">
<p class="admonition-title">Tip</p>
<p>Если дальнейшее содержание кажется сложным, можно вернуться к нему после чтения главы о "стеке".</p>
<p>Если дальнейшее содержание кажется сложным, можно вернуться к нему после чтения главы о «стеке».</p>
</div>
<p>Какова же внутренняя связь между итерацией и рекурсией? В рассмотренном примере рекурсивной функции операция сложения выполняется на этапе возврата рекурсии. Это означает, что функция, вызванная первой, фактически завершает операцию сложения последней, <strong>что соответствует принципу стека "первым пришел - последним вышел"</strong>.</p>
<p>На самом деле такие термины рекурсии, как "стек вызовов" и "пространство стекового кадра", уже намекают на тесную связь между рекурсией и стеком.</p>
<p>Какова же внутренняя связь между итерацией и рекурсией? В рассмотренном примере рекурсивной функции операция сложения выполняется на этапе возврата рекурсии. Это означает, что функция, вызванная первой, фактически завершает операцию сложения последней, <strong>что соответствует принципу стека «первым пришел - последним вышел»</strong>.</p>
<p>На самом деле такие термины рекурсии, как «стек вызовов» и «пространство стекового кадра», уже намекают на тесную связь между рекурсией и стеком.</p>
<ol>
<li><strong>Вызов</strong>: когда вызывается функция, система выделяет для нее новый стековый кадр в "стеке вызовов" для хранения локальных переменных функции, параметров, адреса возврата и других данных.</li>
<li><strong>Возврат</strong>: когда функция завершает выполнение и возвращает результат, соответствующий стековый кадр удаляется из "стека вызовов", восстанавливая среду выполнения предыдущей функции.</li>
<li><strong>Вызов</strong>: когда вызывается функция, система выделяет для нее новый стековый кадр в «стеке вызовов» для хранения локальных переменных функции, параметров, адреса возврата и других данных.</li>
<li><strong>Возврат</strong>: когда функция завершает выполнение и возвращает результат, соответствующий стековый кадр удаляется из «стека вызовов», восстанавливая среду выполнения предыдущей функции.</li>
</ol>
<p>Таким образом, <strong>можно использовать явный стек для моделирования поведения стека вызовов</strong>, чтобы преобразовать рекурсию в итеративную форму:</p>
<div class="tabbed-set tabbed-alternate" data-tabs="8:13"><input checked="checked" id="__tabbed_8_1" name="__tabbed_8" type="radio" /><input id="__tabbed_8_2" name="__tabbed_8" type="radio" /><input id="__tabbed_8_3" name="__tabbed_8" type="radio" /><input id="__tabbed_8_4" name="__tabbed_8" type="radio" /><input id="__tabbed_8_5" name="__tabbed_8" type="radio" /><input id="__tabbed_8_6" name="__tabbed_8" type="radio" /><input id="__tabbed_8_7" name="__tabbed_8" type="radio" /><input id="__tabbed_8_8" name="__tabbed_8" type="radio" /><input id="__tabbed_8_9" name="__tabbed_8" type="radio" /><input id="__tabbed_8_10" name="__tabbed_8" type="radio" /><input id="__tabbed_8_11" name="__tabbed_8" type="radio" /><input id="__tabbed_8_12" name="__tabbed_8" type="radio" /><input id="__tabbed_8_13" name="__tabbed_8" type="radio" /><div class="tabbed-labels"><label for="__tabbed_8_1">Python</label><label for="__tabbed_8_2">C++</label><label for="__tabbed_8_3">Java</label><label for="__tabbed_8_4">C#</label><label for="__tabbed_8_5">Go</label><label for="__tabbed_8_6">Swift</label><label for="__tabbed_8_7">JS</label><label for="__tabbed_8_8">TS</label><label for="__tabbed_8_9">Dart</label><label for="__tabbed_8_10">Rust</label><label for="__tabbed_8_11">C</label><label for="__tabbed_8_12">Kotlin</label><label for="__tabbed_8_13">Ruby</label></div>
@@ -4371,15 +4371,15 @@
<p>Методы оценки эффективности делятся на два типа: практическое тестирование и теоретическую оценку.</p>
<h2 id="211">2.1.1 &nbsp; Практическое тестирование<a class="headerlink" href="#211" title="Permanent link">&para;</a></h2>
<p>Предположим, у нас есть алгоритмы <code>A</code> и <code>B</code>, которые решают одну и ту же задачу, и необходимо сравнить их эффективность. Самый прямой метод - это запустить оба алгоритма на компьютере и зафиксировать время их выполнения и объем используемой памяти. Этот метод отражает реальную ситуацию, но имеет значительные ограничения.</p>
<p>С одной стороны, <strong>сложно исключить влияние факторов тестовой среды</strong>. Аппаратная конфигурация влияет на производительность алгоритма. Например, если алгоритм обладает высокой степенью параллелизма, он будет лучше работать на многоядерных CPU; если алгоритм интенсивно использует память, его производительность будет выше на высокопроизводительной памяти. Это означает, что результаты тестирования на разных машинах могут значительно отличаться, а для получения средней эффективности пришлось бы тестировать на различных платформах, что крайне затруднительно.</p>
<p>С одной стороны, <strong>сложно исключить влияние факторов тестовой среды</strong>. Аппаратная конфигурация влияет на производительность алгоритма. Например, если алгоритм обладает высокой степенью параллелизма, он будет лучше работать на многоядерных CPU. Если алгоритм интенсивно использует память, его производительность будет выше на высокопроизводительной памяти. Это означает, что результаты тестирования на разных машинах могут значительно отличаться, а для получения средней эффективности пришлось бы тестировать на различных платформах, что крайне затруднительно.</p>
<p>С другой стороны, <strong>проведение полного тестирования требует значительных ресурсов</strong>. С изменением объема входных данных алгоритмы демонстрируют разную эффективность. Например, при небольшом объеме данных алгоритм <code>A</code> может работать быстрее, чем алгоритм <code>B</code>, но при большом объеме данных результат может быть противоположным. Следовательно, для получения убедительных выводов необходимо тестировать различные масштабы входных данных, что требует значительных вычислительных ресурсов.</p>
<h2 id="212">2.1.2 &nbsp; Теоретическая оценка<a class="headerlink" href="#212" title="Permanent link">&para;</a></h2>
<p>Из-за значительных ограничений практического тестирования можно рассмотреть возможность оценки эффективности алгоритмов только с помощью вычислений. Такой метод называется <u>анализом асимптотической сложности (asymptotic complexity analysis)</u>, или сокращенно <u>анализом сложности</u>.</p>
<p>Анализ сложности позволяет отразить зависимость между ресурсами времени и пространства, необходимыми для выполнения алгоритма, и размером входных данных. <strong>Он описывает тенденцию роста времени и пространства, необходимых для выполнения алгоритма, по мере увеличения размера входных данных</strong>. Это определение может показаться сложным, но его можно разбить на три ключевых момента.</p>
<ul>
<li>"Ресурсы времени и пространства" соответствуют <u>временной сложности (time complexity)</u> и <u>пространственной сложности (space complexity)</u>.</li>
<li>"По мере увеличения размера входных данных" означает, что сложность отражает зависимость эффективности алгоритма от объема входных данных.</li>
<li>"Тенденция роста времени и пространства" указывает, что анализ сложности фокусируется не на конкретных значениях времени выполнения или объема занимаемой памяти, а на скорости их роста.</li>
<li>«Ресурсы времени и пространства» соответствуют <u>временной сложности (time complexity)</u> и <u>пространственной сложности (space complexity)</u>.</li>
<li>«По мере увеличения размера входных данных» означает, что сложность отражает зависимость эффективности алгоритма от объема входных данных.</li>
<li>«Тенденция роста времени и пространства» указывает, что анализ сложности фокусируется не на конкретных значениях времени выполнения или объема занимаемой памяти, а на скорости их роста.</li>
</ul>
<p><strong>Анализ сложности преодолевает недостатки метода практического тестирования</strong>, что выражается в следующих аспектах.</p>
<ul>
@@ -4535,7 +4535,7 @@
<p>Временное пространство можно дополнительно разделить на три части.</p>
<ul>
<li><strong>Временные данные</strong>: используются для хранения различных констант, переменных, объектов и т.д., возникающих во время выполнения алгоритма.</li>
<li><strong>Пространство кадров стека</strong>: используется для хранения контекстных данных вызываемых функций. При каждом вызове функции система создает на вершине стека новый кадр; после возврата функции пространство этого кадра освобождается.</li>
<li><strong>Пространство кадров стека</strong>: используется для хранения контекстных данных вызываемых функций. При каждом вызове функции система создает на вершине стека новый кадр. После возврата функции пространство этого кадра освобождается.</li>
<li><strong>Пространство инструкций</strong>: используется для хранения скомпилированных инструкций программы и в реальном подсчете обычно не учитывается.</li>
</ul>
<p>При анализе пространственной сложности программы <strong>обычно учитываются временные данные, пространство стека и выходные данные</strong>, как показано на рисунке 2-15.</p>
@@ -4864,7 +4864,7 @@
<p>Рассмотрим следующий код. Понятие худшей пространственной сложности здесь имеет два значения.</p>
<ol>
<li><strong>Ориентир на худшие входные данные</strong>: когда <span class="arithmatex">\(n &lt; 10\)</span> , пространственная сложность равна <span class="arithmatex">\(O(1)\)</span> ; но когда <span class="arithmatex">\(n &gt; 10\)</span> , инициализированный массив <code>nums</code> занимает <span class="arithmatex">\(O(n)\)</span> пространства, поэтому худшая пространственная сложность равна <span class="arithmatex">\(O(n)\)</span> .</li>
<li><strong>Ориентир на пиковое использование памяти во время выполнения</strong>: например, до выполнения последней строки программа занимает <span class="arithmatex">\(O(1)\)</span> пространства; при инициализации массива <code>nums</code> она занимает <span class="arithmatex">\(O(n)\)</span> пространства, поэтому худшая пространственная сложность также равна <span class="arithmatex">\(O(n)\)</span> .</li>
<li><strong>Ориентир на пиковое использование памяти во время выполнения</strong>: например, до выполнения последней строки программа занимает <span class="arithmatex">\(O(1)\)</span> пространства. При инициализации массива <code>nums</code> она занимает <span class="arithmatex">\(O(n)\)</span> пространства, поэтому худшая пространственная сложность также равна <span class="arithmatex">\(O(n)\)</span> .</li>
</ol>
<div class="tabbed-set tabbed-alternate" data-tabs="2:13"><input checked="checked" id="__tabbed_2_1" name="__tabbed_2" type="radio" /><input id="__tabbed_2_2" name="__tabbed_2" type="radio" /><input id="__tabbed_2_3" name="__tabbed_2" type="radio" /><input id="__tabbed_2_4" name="__tabbed_2" type="radio" /><input id="__tabbed_2_5" name="__tabbed_2" type="radio" /><input id="__tabbed_2_6" name="__tabbed_2" type="radio" /><input id="__tabbed_2_7" name="__tabbed_2" type="radio" /><input id="__tabbed_2_8" name="__tabbed_2" type="radio" /><input id="__tabbed_2_9" name="__tabbed_2" type="radio" /><input id="__tabbed_2_10" name="__tabbed_2" type="radio" /><input id="__tabbed_2_11" name="__tabbed_2" type="radio" /><input id="__tabbed_2_12" name="__tabbed_2" type="radio" /><input id="__tabbed_2_13" name="__tabbed_2" type="radio" /><div class="tabbed-labels"><label for="__tabbed_2_1">Python</label><label for="__tabbed_2_2">C++</label><label for="__tabbed_2_3">Java</label><label for="__tabbed_2_4">C#</label><label for="__tabbed_2_5">Go</label><label for="__tabbed_2_6">Swift</label><label for="__tabbed_2_7">JS</label><label for="__tabbed_2_8">TS</label><label for="__tabbed_2_9">Dart</label><label for="__tabbed_2_10">Rust</label><label for="__tabbed_2_11">C</label><label for="__tabbed_2_12">Kotlin</label><label for="__tabbed_2_13">Ruby</label></div>
<div class="tabbed-content">
@@ -5246,7 +5246,7 @@
</div>
<p>Функции <code>loop()</code> и <code>recur()</code> имеют временную сложность <span class="arithmatex">\(O(n)\)</span> , но их пространственная сложность различается.</p>
<ul>
<li>Функция <code>loop()</code> вызывает <code>function()</code> в цикле <span class="arithmatex">\(n\)</span> раз; на каждой итерации <code>function()</code> возвращается и освобождает пространство своего кадра стека, поэтому пространственная сложность по-прежнему равна <span class="arithmatex">\(O(1)\)</span> .</li>
<li>Функция <code>loop()</code> вызывает <code>function()</code> в цикле <span class="arithmatex">\(n\)</span> раз. На каждой итерации <code>function()</code> возвращается и освобождает пространство своего кадра стека, поэтому пространственная сложность по-прежнему равна <span class="arithmatex">\(O(1)\)</span> .</li>
<li>Рекурсивная функция <code>recur()</code> во время выполнения одновременно содержит <span class="arithmatex">\(n\)</span> еще не завершившихся экземпляров <code>recur()</code> , поэтому занимает <span class="arithmatex">\(O(n)\)</span> пространства кадров стека.</li>
</ul>
<h2 id="243">2.4.3 &nbsp; Распространенные типы<a class="headerlink" href="#243" title="Permanent link">&para;</a></h2>
@@ -6207,7 +6207,7 @@
<p><div style="height: 405px; width: 100%;"><iframe class="pythontutor-iframe" src="https://pythontutor.com/iframe-embed.html#code=def%20quadratic%28n%3A%20int%29%3A%0A%20%20%20%20%22%22%22%D0%9A%D0%B2%D0%B0%D0%B4%D1%80%D0%B0%D1%82%D0%B8%D1%87%D0%BD%D0%B0%D1%8F%20%D1%81%D0%BB%D0%BE%D0%B6%D0%BD%D0%BE%D1%81%D1%82%D1%8C%22%22%22%0A%20%20%20%20%23%20%D0%94%D0%B2%D1%83%D0%BC%D0%B5%D1%80%D0%BD%D1%8B%D0%B9%20%D1%81%D0%BF%D0%B8%D1%81%D0%BE%D0%BA%20%D0%B7%D0%B0%D0%BD%D0%B8%D0%BC%D0%B0%D0%B5%D1%82%20O%28n%5E2%29%20%D0%BF%D0%B0%D0%BC%D1%8F%D1%82%D0%B8%0A%20%20%20%20num_matrix%20%3D%20%5B%5B0%5D%20%2A%20n%20for%20_%20in%20range%28n%29%5D%0A%0A%22%22%22Driver%20Code%22%22%22%0Aif%20__name__%20%3D%3D%20%22__main__%22%3A%0A%20%20%20%20n%20%3D%205%0A%20%20%20%20print%28%22%D0%A0%D0%B0%D0%B7%D0%BC%D0%B5%D1%80%20%D0%B2%D1%85%D0%BE%D0%B4%D0%BD%D1%8B%D1%85%20%D0%B4%D0%B0%D0%BD%D0%BD%D1%8B%D1%85%20n%20%3D%22%2C%20n%29%0A%0A%20%20%20%20%23%20%D0%9A%D0%B2%D0%B0%D0%B4%D1%80%D0%B0%D1%82%D0%B8%D1%87%D0%BD%D0%B0%D1%8F%20%D1%81%D0%BB%D0%BE%D0%B6%D0%BD%D0%BE%D1%81%D1%82%D1%8C%0A%20%20%20%20quadratic%28n%29&codeDivHeight=472&codeDivWidth=350&cumulative=false&curInstr=16&heapPrimitives=nevernest&origin=opt-frontend.js&py=311&rawInputLstJSON=%5B%5D&textReferences=false"> </iframe></div>
<div style="margin-top: 5px;"><a href="https://pythontutor.com/iframe-embed.html#code=def%20quadratic%28n%3A%20int%29%3A%0A%20%20%20%20%22%22%22%D0%9A%D0%B2%D0%B0%D0%B4%D1%80%D0%B0%D1%82%D0%B8%D1%87%D0%BD%D0%B0%D1%8F%20%D1%81%D0%BB%D0%BE%D0%B6%D0%BD%D0%BE%D1%81%D1%82%D1%8C%22%22%22%0A%20%20%20%20%23%20%D0%94%D0%B2%D1%83%D0%BC%D0%B5%D1%80%D0%BD%D1%8B%D0%B9%20%D1%81%D0%BF%D0%B8%D1%81%D0%BE%D0%BA%20%D0%B7%D0%B0%D0%BD%D0%B8%D0%BC%D0%B0%D0%B5%D1%82%20O%28n%5E2%29%20%D0%BF%D0%B0%D0%BC%D1%8F%D1%82%D0%B8%0A%20%20%20%20num_matrix%20%3D%20%5B%5B0%5D%20%2A%20n%20for%20_%20in%20range%28n%29%5D%0A%0A%22%22%22Driver%20Code%22%22%22%0Aif%20__name__%20%3D%3D%20%22__main__%22%3A%0A%20%20%20%20n%20%3D%205%0A%20%20%20%20print%28%22%D0%A0%D0%B0%D0%B7%D0%BC%D0%B5%D1%80%20%D0%B2%D1%85%D0%BE%D0%B4%D0%BD%D1%8B%D1%85%20%D0%B4%D0%B0%D0%BD%D0%BD%D1%8B%D1%85%20n%20%3D%22%2C%20n%29%0A%0A%20%20%20%20%23%20%D0%9A%D0%B2%D0%B0%D0%B4%D1%80%D0%B0%D1%82%D0%B8%D1%87%D0%BD%D0%B0%D1%8F%20%D1%81%D0%BB%D0%BE%D0%B6%D0%BD%D0%BE%D1%81%D1%82%D1%8C%0A%20%20%20%20quadratic%28n%29&codeDivHeight=800&codeDivWidth=600&cumulative=false&curInstr=16&heapPrimitives=nevernest&origin=opt-frontend.js&py=311&rawInputLstJSON=%5B%5D&textReferences=false" target="_blank" rel="noopener noreferrer">Во весь экран &gt;</a></div></p>
</details>
<p>Как показано на рисунке 2-18, глубина рекурсии этой функции равна <span class="arithmatex">\(n\)</span> , и в каждой рекурсивной функции инициализируется массив длины <span class="arithmatex">\(n\)</span> , <span class="arithmatex">\(n-1\)</span> , <span class="arithmatex">\(\dots\)</span> , <span class="arithmatex">\(2\)</span> , <span class="arithmatex">\(1\)</span> ; его средняя длина равна <span class="arithmatex">\(n / 2\)</span> , поэтому в сумме используется <span class="arithmatex">\(O(n^2)\)</span> пространства:</p>
<p>Как показано на рисунке 2-18, глубина рекурсии этой функции равна <span class="arithmatex">\(n\)</span> , и в каждой рекурсивной функции инициализируется массив длины <span class="arithmatex">\(n\)</span> , <span class="arithmatex">\(n-1\)</span> , <span class="arithmatex">\(\dots\)</span> , <span class="arithmatex">\(2\)</span> , <span class="arithmatex">\(1\)</span>. Его средняя длина равна <span class="arithmatex">\(n / 2\)</span> , поэтому в сумме используется <span class="arithmatex">\(O(n^2)\)</span> пространства:</p>
<div class="tabbed-set tabbed-alternate" data-tabs="8:13"><input checked="checked" id="__tabbed_8_1" name="__tabbed_8" type="radio" /><input id="__tabbed_8_2" name="__tabbed_8" type="radio" /><input id="__tabbed_8_3" name="__tabbed_8" type="radio" /><input id="__tabbed_8_4" name="__tabbed_8" type="radio" /><input id="__tabbed_8_5" name="__tabbed_8" type="radio" /><input id="__tabbed_8_6" name="__tabbed_8" type="radio" /><input id="__tabbed_8_7" name="__tabbed_8" type="radio" /><input id="__tabbed_8_8" name="__tabbed_8" type="radio" /><input id="__tabbed_8_9" name="__tabbed_8" type="radio" /><input id="__tabbed_8_10" name="__tabbed_8" type="radio" /><input id="__tabbed_8_11" name="__tabbed_8" type="radio" /><input id="__tabbed_8_12" name="__tabbed_8" type="radio" /><input id="__tabbed_8_13" name="__tabbed_8" type="radio" /><div class="tabbed-labels"><label for="__tabbed_8_1">Python</label><label for="__tabbed_8_2">C++</label><label for="__tabbed_8_3">Java</label><label for="__tabbed_8_4">C#</label><label for="__tabbed_8_5">Go</label><label for="__tabbed_8_6">Swift</label><label for="__tabbed_8_7">JS</label><label for="__tabbed_8_8">TS</label><label for="__tabbed_8_9">Dart</label><label for="__tabbed_8_10">Rust</label><label for="__tabbed_8_11">C</label><label for="__tabbed_8_12">Kotlin</label><label for="__tabbed_8_13">Ruby</label></div>
<div class="tabbed-content">
<div class="tabbed-block">
@@ -6375,7 +6375,7 @@
<p align="center"> Рисунок 2-18 &nbsp; Квадратичная пространственная сложность, порождаемая рекурсивной функцией </p>
<h3 id="4-o2n">4. &nbsp; Экспоненциальная сложность <span class="arithmatex">\(O(2^n)\)</span><a class="headerlink" href="#4-o2n" title="Permanent link">&para;</a></h3>
<p>Экспоненциальная сложность часто встречается у бинарных деревьев. Полное бинарное дерево с <span class="arithmatex">\(n\)</span> уровнями содержит <span class="arithmatex">\(2^n - 1\)</span> узлов и занимает <span class="arithmatex">\(O(2^n)\)</span> пространства:</p>
<p>Экспоненциальная сложность часто встречается у бинарных деревьев. Как видно на рисунке 2-19, полное бинарное дерево с <span class="arithmatex">\(n\)</span> уровнями содержит <span class="arithmatex">\(2^n - 1\)</span> узлов и занимает <span class="arithmatex">\(O(2^n)\)</span> пространства:</p>
<div class="tabbed-set tabbed-alternate" data-tabs="9:13"><input checked="checked" id="__tabbed_9_1" name="__tabbed_9" type="radio" /><input id="__tabbed_9_2" name="__tabbed_9" type="radio" /><input id="__tabbed_9_3" name="__tabbed_9" type="radio" /><input id="__tabbed_9_4" name="__tabbed_9" type="radio" /><input id="__tabbed_9_5" name="__tabbed_9" type="radio" /><input id="__tabbed_9_6" name="__tabbed_9" type="radio" /><input id="__tabbed_9_7" name="__tabbed_9" type="radio" /><input id="__tabbed_9_8" name="__tabbed_9" type="radio" /><input id="__tabbed_9_9" name="__tabbed_9" type="radio" /><input id="__tabbed_9_10" name="__tabbed_9" type="radio" /><input id="__tabbed_9_11" name="__tabbed_9" type="radio" /><input id="__tabbed_9_12" name="__tabbed_9" type="radio" /><input id="__tabbed_9_13" name="__tabbed_9" type="radio" /><div class="tabbed-labels"><label for="__tabbed_9_1">Python</label><label for="__tabbed_9_2">C++</label><label for="__tabbed_9_3">Java</label><label for="__tabbed_9_4">C#</label><label for="__tabbed_9_5">Go</label><label for="__tabbed_9_6">Swift</label><label for="__tabbed_9_7">JS</label><label for="__tabbed_9_8">TS</label><label for="__tabbed_9_9">Dart</label><label for="__tabbed_9_10">Rust</label><label for="__tabbed_9_11">C</label><label for="__tabbed_9_12">Kotlin</label><label for="__tabbed_9_13">Ruby</label></div>
<div class="tabbed-content">
<div class="tabbed-block">
@@ -6559,11 +6559,11 @@
<p align="center"> Рисунок 2-19 &nbsp; Экспоненциальная пространственная сложность, порождаемая полным бинарным деревом </p>
<h3 id="5-olog-n">5. &nbsp; Логарифмическая сложность <span class="arithmatex">\(O(\log n)\)</span><a class="headerlink" href="#5-olog-n" title="Permanent link">&para;</a></h3>
<p>Логарифмическая сложность часто встречается в алгоритмах "разделяй и властвуй". Например, при сортировке слиянием входной массив длины <span class="arithmatex">\(n\)</span> на каждом шаге рекурсии делится пополам, образуя рекурсивное дерево высоты <span class="arithmatex">\(\log n\)</span> и используя <span class="arithmatex">\(O(\log n)\)</span> пространства кадров стека.</p>
<p>Логарифмическая сложность часто встречается в алгоритмах «разделяй и властвуй». Например, при сортировке слиянием входной массив длины <span class="arithmatex">\(n\)</span> на каждом шаге рекурсии делится пополам, образуя рекурсивное дерево высоты <span class="arithmatex">\(\log n\)</span> и используя <span class="arithmatex">\(O(\log n)\)</span> пространства кадров стека.</p>
<p>Еще один пример - преобразование числа в строку. Если задано положительное целое число <span class="arithmatex">\(n\)</span> , то количество его цифр равно <span class="arithmatex">\(\lfloor \log_{10} n \rfloor + 1\)</span> , то есть длина соответствующей строки тоже равна <span class="arithmatex">\(\lfloor \log_{10} n \rfloor + 1\)</span> , следовательно, пространственная сложность составляет <span class="arithmatex">\(O(\log_{10} n + 1) = O(\log n)\)</span> .</p>
<h2 id="244">2.4.4 &nbsp; Компромисс между временем и пространством<a class="headerlink" href="#244" title="Permanent link">&para;</a></h2>
<p>В идеальных условиях хотелось бы, чтобы и временная, и пространственная сложность алгоритма были оптимальными. Однако на практике одновременно оптимизировать и время, и память обычно очень трудно.</p>
<p><strong>Снижение временной сложности обычно достигается ценой увеличения пространственной сложности, и наоборот</strong>. Подход, при котором жертвуют памятью ради ускорения работы алгоритма, называется обменом пространства на время; обратный подход называется обменом времени на пространство.</p>
<p><strong>Снижение временной сложности обычно достигается ценой увеличения пространственной сложности, и наоборот</strong>. Подход, при котором жертвуют памятью ради ускорения работы алгоритма, называется обменом пространства на время. Обратный подход называется обменом времени на пространство.</p>
<p>Выбор между этими двумя идеями зависит от того, что важнее в конкретной задаче. В большинстве случаев время ценнее памяти, поэтому стратегия обмена пространства на время используется чаще. Но при очень больших объемах данных контроль пространственной сложности тоже становится крайне важным.</p>
<!-- Source file information -->
@@ -4391,10 +4391,10 @@
<li>Java и C# - объектно-ориентированные языки программирования, в которых блоки кода (методы) обычно являются частью класса. Статические методы по поведению похожи на функции, потому что они привязаны к классу и не могут обращаться к конкретным переменным экземпляра.</li>
<li>C++ и Python поддерживают как процедурное программирование (функции), так и объектно-ориентированное программирование (методы).</li>
</ul>
<p><strong>Q</strong>: Отражает ли диаграмма "распространенных типов пространственной сложности" абсолютный размер занятой памяти?</p>
<p><strong>Q</strong>: Отражает ли диаграмма «распространенных типов пространственной сложности» абсолютный размер занятой памяти?</p>
<p>Нет, эта диаграмма показывает пространственную сложность, а значит отражает именно тенденцию роста, а не абсолютный объем занятого пространства.</p>
<p>Если взять <span class="arithmatex">\(n = 8\)</span> , можно заметить, что значения на кривых не совпадают напрямую с соответствующими функциями. Это связано с тем, что каждая кривая содержит константный член, который сжимает диапазон значений до визуально удобного масштаба.</p>
<p>На практике, поскольку мы обычно не знаем, какова "константная" сложность каждого метода, только по сложности мы, как правило, не можем выбрать оптимальное решение для случая <span class="arithmatex">\(n = 8\)</span> . Но для <span class="arithmatex">\(n = 8^5\)</span> выбор уже очевиден: в этой области доминирует именно тенденция роста.</p>
<p>На практике, поскольку мы обычно не знаем, какова «константная» сложность каждого метода, только по сложности мы, как правило, не можем выбрать оптимальное решение для случая <span class="arithmatex">\(n = 8\)</span> . Но для <span class="arithmatex">\(n = 8^5\)</span> выбор уже очевиден: в этой области доминирует именно тенденция роста.</p>
<p><strong>Q</strong>: Бывают ли случаи, когда в реальных сценариях алгоритм специально проектируют так, чтобы жертвовать временем ради пространства или пространством ради времени?</p>
<p>На практике в большинстве случаев выбирают обмен пространства на время. Например, для индексов в базах данных обычно строят B+ деревья или хеш-индексы, расходуя значительный объем памяти ради эффективных запросов уровня <span class="arithmatex">\(O(\log n)\)</span> или даже <span class="arithmatex">\(O(1)\)</span>.</p>
<p>В сценариях, где память особенно дорога, наоборот, могут жертвовать временем ради пространства. Например, в embedded-разработке память устройства очень ограничена, поэтому инженеры могут отказаться от хеш-таблиц и выбрать последовательный поиск по массиву, экономя память ценой более медленного поиска.</p>
@@ -4830,7 +4830,7 @@
<p>Но на практике <strong>подсчитывать реальное время выполнения алгоритма и неразумно, и нереалистично</strong>. Во-первых, мы не хотим привязывать оценку времени к конкретной платформе, потому что алгоритм должен запускаться на самых разных платформах. Во-вторых, нам трудно определить время выполнения каждого типа операций, а это делает точную оценку крайне затруднительной.</p>
<h2 id="231">2.3.1 &nbsp; Подсчет тенденции роста времени<a class="headerlink" href="#231" title="Permanent link">&para;</a></h2>
<p>Анализ временной сложности оценивает не само время выполнения алгоритма, <strong>а тенденцию роста этого времени по мере увеличения объема данных</strong>.</p>
<p>Понятие "тенденции роста времени" выглядит довольно абстрактным, поэтому разберем его на примере. Предположим, размер входных данных равен <span class="arithmatex">\(n\)</span> , и даны три алгоритма <code>A</code> , <code>B</code> и <code>C</code> :</p>
<p>Понятие «тенденции роста времени» выглядит довольно абстрактным, поэтому разберем его на примере. Предположим, размер входных данных равен <span class="arithmatex">\(n\)</span> , и даны три алгоритма <code>A</code> , <code>B</code> и <code>C</code> :</p>
<div class="tabbed-set tabbed-alternate" data-tabs="2:13"><input checked="checked" id="__tabbed_2_1" name="__tabbed_2" type="radio" /><input id="__tabbed_2_2" name="__tabbed_2" type="radio" /><input id="__tabbed_2_3" name="__tabbed_2" type="radio" /><input id="__tabbed_2_4" name="__tabbed_2" type="radio" /><input id="__tabbed_2_5" name="__tabbed_2" type="radio" /><input id="__tabbed_2_6" name="__tabbed_2" type="radio" /><input id="__tabbed_2_7" name="__tabbed_2" type="radio" /><input id="__tabbed_2_8" name="__tabbed_2" type="radio" /><input id="__tabbed_2_9" name="__tabbed_2" type="radio" /><input id="__tabbed_2_10" name="__tabbed_2" type="radio" /><input id="__tabbed_2_11" name="__tabbed_2" type="radio" /><input id="__tabbed_2_12" name="__tabbed_2" type="radio" /><input id="__tabbed_2_13" name="__tabbed_2" type="radio" /><div class="tabbed-labels"><label for="__tabbed_2_1">Python</label><label for="__tabbed_2_2">C++</label><label for="__tabbed_2_3">Java</label><label for="__tabbed_2_4">C#</label><label for="__tabbed_2_5">Go</label><label for="__tabbed_2_6">Swift</label><label for="__tabbed_2_7">JS</label><label for="__tabbed_2_8">TS</label><label for="__tabbed_2_9">Dart</label><label for="__tabbed_2_10">Rust</label><label for="__tabbed_2_11">C</label><label for="__tabbed_2_12">Kotlin</label><label for="__tabbed_2_13">Ruby</label></div>
<div class="tabbed-content">
<div class="tabbed-block">
@@ -5077,11 +5077,11 @@
</div>
</div>
</div>
<p>Ниже показаны временные сложности трех приведенных выше функций.</p>
<p>На рисунке 2-7 показаны временные сложности трех приведенных выше функций.</p>
<ul>
<li>У алгоритма <code>A</code> есть только одна операция вывода, и время его работы не растет с увеличением <span class="arithmatex">\(n\)</span> . Такую временную сложность называют постоянной.</li>
<li>В алгоритме <code>B</code> операция вывода выполняется в цикле <span class="arithmatex">\(n\)</span> раз, поэтому время работы растет линейно по мере увеличения <span class="arithmatex">\(n\)</span> . Такая временная сложность называется линейной.</li>
<li>В алгоритме <code>C</code> операция вывода выполняется <span class="arithmatex">\(1000000\)</span> раз; хотя время работы велико, оно не зависит от размера входных данных <span class="arithmatex">\(n\)</span> . Поэтому временная сложность <code>C</code> такая же, как у <code>A</code> , и тоже является постоянной.</li>
<li>В алгоритме <code>C</code> операция вывода выполняется <span class="arithmatex">\(1000000\)</span> раз. Хотя время работы велико, оно не зависит от размера входных данных <span class="arithmatex">\(n\)</span> . Поэтому временная сложность <code>C</code> такая же, как у <code>A</code> , и тоже является постоянной.</li>
</ul>
<p><img alt="Тенденции роста времени для алгоритмов A, B и C" class="animation-figure" src="../time_complexity.assets/time_complexity_simple_example.png" /></p>
<p align="center"> Рисунок 2-7 &nbsp; Тенденции роста времени для алгоритмов A, B и C </p>
@@ -5253,16 +5253,16 @@
</div>
</div>
</div>
<p>Пусть количество операций алгоритма является функцией от размера входных данных <span class="arithmatex">\(n\)</span> и обозначается как <span class="arithmatex">\(T(n)\)</span> ; тогда для приведенной выше функции число операций равно:</p>
<p>Пусть количество операций алгоритма является функцией от размера входных данных <span class="arithmatex">\(n\)</span> и обозначается как <span class="arithmatex">\(T(n)\)</span>. Тогда для приведенной выше функции число операций равно:</p>
<div class="arithmatex">\[
T(n) = 3 + 2n
\]</div>
<p><span class="arithmatex">\(T(n)\)</span> - линейная функция, а это означает, что тенденция роста времени работы линейна, следовательно, временная сложность здесь тоже линейна.</p>
<p>Линейную временную сложность записывают как <span class="arithmatex">\(O(n)\)</span> ; этот математический символ называется <u>нотацией Big <span class="arithmatex">\(O\)</span> (big-<span class="arithmatex">\(O\)</span> notation)</u> и обозначает <u>асимптотическую верхнюю границу (asymptotic upper bound)</u> функции <span class="arithmatex">\(T(n)\)</span> .</p>
<p>Линейную временную сложность записывают как <span class="arithmatex">\(O(n)\)</span>. Этот математический символ называется <u>нотацией Big <span class="arithmatex">\(O\)</span> (big-<span class="arithmatex">\(O\)</span> notation)</u> и обозначает <u>асимптотическую верхнюю границу (asymptotic upper bound)</u> функции <span class="arithmatex">\(T(n)\)</span> .</p>
<p>Иными словами, анализ временной сложности сводится к определению асимптотической верхней границы числа операций <span class="arithmatex">\(T(n)\)</span>, и у этого понятия есть строгое математическое определение.</p>
<div class="admonition note">
<p class="admonition-title">Асимптотическая верхняя граница функции</p>
<p>Если существуют положительное действительное число <span class="arithmatex">\(c\)</span> и действительное число <span class="arithmatex">\(n_0\)</span> , такие что для всех <span class="arithmatex">\(n &gt; n_0\)</span> выполняется <span class="arithmatex">\(T(n) \leq c \cdot f(n)\)</span> , то можно считать, что <span class="arithmatex">\(f(n)\)</span> задает асимптотическую верхнюю границу для <span class="arithmatex">\(T(n)\)</span> ; это записывается как <span class="arithmatex">\(T(n) = O(f(n))\)</span> .</p>
<p>Если существуют положительное действительное число <span class="arithmatex">\(c\)</span> и действительное число <span class="arithmatex">\(n_0\)</span> , такие что для всех <span class="arithmatex">\(n &gt; n_0\)</span> выполняется <span class="arithmatex">\(T(n) \leq c \cdot f(n)\)</span> , то можно считать, что <span class="arithmatex">\(f(n)\)</span> задает асимптотическую верхнюю границу для <span class="arithmatex">\(T(n)\)</span>. Это записывается как <span class="arithmatex">\(T(n) = O(f(n))\)</span> .</p>
</div>
<p>Как показано на рисунке 2-8, вычислить асимптотическую верхнюю границу - значит найти такую функцию <span class="arithmatex">\(f(n)\)</span> , что при стремлении <span class="arithmatex">\(n\)</span> к бесконечности функции <span class="arithmatex">\(T(n)\)</span> и <span class="arithmatex">\(f(n)\)</span> имеют один и тот же порядок роста и отличаются только постоянным коэффициентом <span class="arithmatex">\(c\)</span>.</p>
<p><img alt="Асимптотическая верхняя граница функции" class="animation-figure" src="../time_complexity.assets/asymptotic_upper_bound.png" /></p>
@@ -5276,7 +5276,7 @@ T(n) = 3 + 2n
<ol>
<li><strong>Игнорировать константы в <span class="arithmatex">\(T(n)\)</span></strong>. Они не зависят от <span class="arithmatex">\(n\)</span> , а значит не влияют на временную сложность.</li>
<li><strong>Опускать все коэффициенты</strong>. Например, циклы на <span class="arithmatex">\(2n\)</span> раз или <span class="arithmatex">\(5n + 1\)</span> раз можно упростить до <span class="arithmatex">\(n\)</span> раз, потому что коэффициент перед <span class="arithmatex">\(n\)</span> не влияет на временную сложность.</li>
<li><strong>При вложенных циклах использовать умножение</strong>. Общее число операций равно произведению числа операций внешнего и внутреннего циклов; при этом для каждого уровня цикла по-прежнему можно применять приемы из пунктов <code>1.</code> и <code>2.</code> .</li>
<li><strong>При вложенных циклах использовать умножение</strong>. Общее число операций равно произведению числа операций внешнего и внутреннего циклов. При этом для каждого уровня цикла по-прежнему можно применять приемы из пунктов <code>1.</code> и <code>2.</code> .</li>
</ol>
<p>Для заданной функции мы можем использовать перечисленные выше приемы и подсчитать число операций:</p>
<div class="tabbed-set tabbed-alternate" data-tabs="4:13"><input checked="checked" id="__tabbed_4_1" name="__tabbed_4" type="radio" /><input id="__tabbed_4_2" name="__tabbed_4" type="radio" /><input id="__tabbed_4_3" name="__tabbed_4" type="radio" /><input id="__tabbed_4_4" name="__tabbed_4" type="radio" /><input id="__tabbed_4_5" name="__tabbed_4" type="radio" /><input id="__tabbed_4_6" name="__tabbed_4" type="radio" /><input id="__tabbed_4_7" name="__tabbed_4" type="radio" /><input id="__tabbed_4_8" name="__tabbed_4" type="radio" /><input id="__tabbed_4_9" name="__tabbed_4" type="radio" /><input id="__tabbed_4_10" name="__tabbed_4" type="radio" /><input id="__tabbed_4_11" name="__tabbed_4" type="radio" /><input id="__tabbed_4_12" name="__tabbed_4" type="radio" /><input id="__tabbed_4_13" name="__tabbed_4" type="radio" /><div class="tabbed-labels"><label for="__tabbed_4_1">Python</label><label for="__tabbed_4_2">C++</label><label for="__tabbed_4_3">Java</label><label for="__tabbed_4_4">C#</label><label for="__tabbed_4_5">Go</label><label for="__tabbed_4_6">Swift</label><label for="__tabbed_4_7">JS</label><label for="__tabbed_4_8">TS</label><label for="__tabbed_4_9">Dart</label><label for="__tabbed_4_10">Rust</label><label for="__tabbed_4_11">C</label><label for="__tabbed_4_12">Kotlin</label><label for="__tabbed_4_13">Ruby</label></div>
@@ -5498,7 +5498,7 @@ T(n) = 3 + 2n
</div>
</div>
</div>
<p>Следующая формула показывает результаты подсчета до и после использования перечисленных выше приемов; в обоих случаях выводимая временная сложность равна <span class="arithmatex">\(O(n^2)\)</span> .</p>
<p>Следующая формула показывает результаты подсчета до и после использования перечисленных выше приемов. В обоих случаях выводимая временная сложность равна <span class="arithmatex">\(O(n^2)\)</span> .</p>
<div class="arithmatex">\[
\begin{aligned}
T(n) &amp; = 2n(n + 1) + (5n + 1) + 2 &amp; \text{полный подсчет (-.-|||)} \newline
@@ -5544,7 +5544,7 @@ T(n) &amp; = n^2 + n &amp; \text{ленивый подсчет (o.O)}
</table>
</div>
<h2 id="234">2.3.4 &nbsp; Распространенные типы<a class="headerlink" href="#234" title="Permanent link">&para;</a></h2>
<p>Пусть размер входных данных равен <span class="arithmatex">\(n\)</span> ; распространенные типы временной сложности показаны на рисунке 2-9 в порядке от меньшей к большей.</p>
<p>Пусть размер входных данных равен <span class="arithmatex">\(n\)</span>. Распространенные типы временной сложности показаны на рисунке 2-9 в порядке от меньшей к большей.</p>
<div class="arithmatex">\[
\begin{aligned}
&amp; O(1) &lt; O(\log n) &lt; O(n) &lt; O(n \log n) &lt; O(n^2) &lt; O(2^n) &lt; O(n!) \newline
@@ -6029,7 +6029,7 @@ T(n) &amp; = n^2 + n &amp; \text{ленивый подсчет (o.O)}
<p><div style="height: 459px; width: 100%;"><iframe class="pythontutor-iframe" src="https://pythontutor.com/iframe-embed.html#code=def%20array_traversal%28nums%3A%20list%5Bint%5D%29%20-%3E%20int%3A%0A%20%20%20%20%22%22%22%D0%9B%D0%B8%D0%BD%D0%B5%D0%B9%D0%BD%D0%B0%D1%8F%20%D1%81%D0%BB%D0%BE%D0%B6%D0%BD%D0%BE%D1%81%D1%82%D1%8C%20%28%D0%BE%D0%B1%D1%85%D0%BE%D0%B4%20%D0%BC%D0%B0%D1%81%D1%81%D0%B8%D0%B2%D0%B0%29%22%22%22%0A%20%20%20%20count%20%3D%200%0A%20%20%20%20%23%20%D0%A7%D0%B8%D1%81%D0%BB%D0%BE%20%D0%B8%D1%82%D0%B5%D1%80%D0%B0%D1%86%D0%B8%D0%B9%20%D0%BF%D1%80%D0%BE%D0%BF%D0%BE%D1%80%D1%86%D0%B8%D0%BE%D0%BD%D0%B0%D0%BB%D1%8C%D0%BD%D0%BE%20%D0%B4%D0%BB%D0%B8%D0%BD%D0%B5%20%D0%BC%D0%B0%D1%81%D1%81%D0%B8%D0%B2%D0%B0%0A%20%20%20%20for%20num%20in%20nums%3A%0A%20%20%20%20%20%20%20%20count%20%2B%3D%201%0A%20%20%20%20return%20count%0A%0A%22%22%22Driver%20Code%22%22%22%0Aif%20__name__%20%3D%3D%20%22__main__%22%3A%0A%20%20%20%20n%20%3D%208%0A%20%20%20%20print%28%22%D0%A0%D0%B0%D0%B7%D0%BC%D0%B5%D1%80%20%D0%B2%D1%85%D0%BE%D0%B4%D0%BD%D1%8B%D1%85%20%D0%B4%D0%B0%D0%BD%D0%BD%D1%8B%D1%85%20n%20%3D%22%2C%20n%29%0A%0A%20%20%20%20count%20%3D%20array_traversal%28%5B0%5D%20%2A%20n%29%0A%20%20%20%20print%28%22%D0%A7%D0%B8%D1%81%D0%BB%D0%BE%20%D0%BE%D0%BF%D0%B5%D1%80%D0%B0%D1%86%D0%B8%D0%B9%20%D0%BB%D0%B8%D0%BD%D0%B5%D0%B9%D0%BD%D0%BE%D0%B9%20%D1%81%D0%BB%D0%BE%D0%B6%D0%BD%D0%BE%D1%81%D1%82%D0%B8%20%28%D0%BE%D0%B1%D1%85%D0%BE%D0%B4%20%D0%BC%D0%B0%D1%81%D1%81%D0%B8%D0%B2%D0%B0%29%20%3D%22%2C%20count%29&codeDivHeight=472&codeDivWidth=350&cumulative=false&curInstr=3&heapPrimitives=nevernest&origin=opt-frontend.js&py=311&rawInputLstJSON=%5B%5D&textReferences=false"> </iframe></div>
<div style="margin-top: 5px;"><a href="https://pythontutor.com/iframe-embed.html#code=def%20array_traversal%28nums%3A%20list%5Bint%5D%29%20-%3E%20int%3A%0A%20%20%20%20%22%22%22%D0%9B%D0%B8%D0%BD%D0%B5%D0%B9%D0%BD%D0%B0%D1%8F%20%D1%81%D0%BB%D0%BE%D0%B6%D0%BD%D0%BE%D1%81%D1%82%D1%8C%20%28%D0%BE%D0%B1%D1%85%D0%BE%D0%B4%20%D0%BC%D0%B0%D1%81%D1%81%D0%B8%D0%B2%D0%B0%29%22%22%22%0A%20%20%20%20count%20%3D%200%0A%20%20%20%20%23%20%D0%A7%D0%B8%D1%81%D0%BB%D0%BE%20%D0%B8%D1%82%D0%B5%D1%80%D0%B0%D1%86%D0%B8%D0%B9%20%D0%BF%D1%80%D0%BE%D0%BF%D0%BE%D1%80%D1%86%D0%B8%D0%BE%D0%BD%D0%B0%D0%BB%D1%8C%D0%BD%D0%BE%20%D0%B4%D0%BB%D0%B8%D0%BD%D0%B5%20%D0%BC%D0%B0%D1%81%D1%81%D0%B8%D0%B2%D0%B0%0A%20%20%20%20for%20num%20in%20nums%3A%0A%20%20%20%20%20%20%20%20count%20%2B%3D%201%0A%20%20%20%20return%20count%0A%0A%22%22%22Driver%20Code%22%22%22%0Aif%20__name__%20%3D%3D%20%22__main__%22%3A%0A%20%20%20%20n%20%3D%208%0A%20%20%20%20print%28%22%D0%A0%D0%B0%D0%B7%D0%BC%D0%B5%D1%80%20%D0%B2%D1%85%D0%BE%D0%B4%D0%BD%D1%8B%D1%85%20%D0%B4%D0%B0%D0%BD%D0%BD%D1%8B%D1%85%20n%20%3D%22%2C%20n%29%0A%0A%20%20%20%20count%20%3D%20array_traversal%28%5B0%5D%20%2A%20n%29%0A%20%20%20%20print%28%22%D0%A7%D0%B8%D1%81%D0%BB%D0%BE%20%D0%BE%D0%BF%D0%B5%D1%80%D0%B0%D1%86%D0%B8%D0%B9%20%D0%BB%D0%B8%D0%BD%D0%B5%D0%B9%D0%BD%D0%BE%D0%B9%20%D1%81%D0%BB%D0%BE%D0%B6%D0%BD%D0%BE%D1%81%D1%82%D0%B8%20%28%D0%BE%D0%B1%D1%85%D0%BE%D0%B4%20%D0%BC%D0%B0%D1%81%D1%81%D0%B8%D0%B2%D0%B0%29%20%3D%22%2C%20count%29&codeDivHeight=800&codeDivWidth=600&cumulative=false&curInstr=3&heapPrimitives=nevernest&origin=opt-frontend.js&py=311&rawInputLstJSON=%5B%5D&textReferences=false" target="_blank" rel="noopener noreferrer">Во весь экран &gt;</a></div></p>
</details>
<p>Стоит отметить, что <strong>размер входных данных <span class="arithmatex">\(n\)</span> нужно определять конкретно в зависимости от типа входа</strong>. Например, в первом примере переменная <span class="arithmatex">\(n\)</span> сама является размером входных данных; во втором примере размером данных служит длина массива.</p>
<p>Стоит отметить, что <strong>размер входных данных <span class="arithmatex">\(n\)</span> нужно определять конкретно в зависимости от типа входа</strong>. Например, в первом примере переменная <span class="arithmatex">\(n\)</span> сама является размером входных данных. Во втором примере размером данных служит длина массива.</p>
<h3 id="3-on2">3. &nbsp; Квадратичная сложность <span class="arithmatex">\(O(n^2)\)</span><a class="headerlink" href="#3-on2" title="Permanent link">&para;</a></h3>
<p>Квадратичная сложность характеризуется тем, что число операций растет квадратично относительно размера входных данных <span class="arithmatex">\(n\)</span> . Квадратичная сложность обычно встречается во вложенных циклах: временная сложность внешнего и внутреннего циклов равна <span class="arithmatex">\(O(n)\)</span> , поэтому общая временная сложность составляет <span class="arithmatex">\(O(n^2)\)</span> :</p>
<div class="tabbed-set tabbed-alternate" data-tabs="8:13"><input checked="checked" id="__tabbed_8_1" name="__tabbed_8" type="radio" /><input id="__tabbed_8_2" name="__tabbed_8" type="radio" /><input id="__tabbed_8_3" name="__tabbed_8" type="radio" /><input id="__tabbed_8_4" name="__tabbed_8" type="radio" /><input id="__tabbed_8_5" name="__tabbed_8" type="radio" /><input id="__tabbed_8_6" name="__tabbed_8" type="radio" /><input id="__tabbed_8_7" name="__tabbed_8" type="radio" /><input id="__tabbed_8_8" name="__tabbed_8" type="radio" /><input id="__tabbed_8_9" name="__tabbed_8" type="radio" /><input id="__tabbed_8_10" name="__tabbed_8" type="radio" /><input id="__tabbed_8_11" name="__tabbed_8" type="radio" /><input id="__tabbed_8_12" name="__tabbed_8" type="radio" /><input id="__tabbed_8_13" name="__tabbed_8" type="radio" /><div class="tabbed-labels"><label for="__tabbed_8_1">Python</label><label for="__tabbed_8_2">C++</label><label for="__tabbed_8_3">Java</label><label for="__tabbed_8_4">C#</label><label for="__tabbed_8_5">Go</label><label for="__tabbed_8_6">Swift</label><label for="__tabbed_8_7">JS</label><label for="__tabbed_8_8">TS</label><label for="__tabbed_8_9">Dart</label><label for="__tabbed_8_10">Rust</label><label for="__tabbed_8_11">C</label><label for="__tabbed_8_12">Kotlin</label><label for="__tabbed_8_13">Ruby</label></div>
@@ -6521,8 +6521,8 @@ T(n) &amp; = n^2 + n &amp; \text{ленивый подсчет (o.O)}
<div style="margin-top: 5px;"><a href="https://pythontutor.com/iframe-embed.html#code=def%20bubble_sort%28nums%3A%20list%5Bint%5D%29%20-%3E%20int%3A%0A%20%20%20%20%22%22%22%D0%9A%D0%B2%D0%B0%D0%B4%D1%80%D0%B0%D1%82%D0%B8%D1%87%D0%BD%D0%B0%D1%8F%20%D1%81%D0%BB%D0%BE%D0%B6%D0%BD%D0%BE%D1%81%D1%82%D1%8C%20%28%D0%BF%D1%83%D0%B7%D1%8B%D1%80%D1%8C%D0%BA%D0%BE%D0%B2%D0%B0%D1%8F%20%D1%81%D0%BE%D1%80%D1%82%D0%B8%D1%80%D0%BE%D0%B2%D0%BA%D0%B0%29%22%22%22%0A%20%20%20%20count%20%3D%200%20%20%23%20%D0%A1%D1%87%D0%B5%D1%82%D1%87%D0%B8%D0%BA%0A%20%20%20%20%23%20%D0%92%D0%BD%D0%B5%D1%88%D0%BD%D0%B8%D0%B9%20%D1%86%D0%B8%D0%BA%D0%BB%3A%20%D0%BD%D0%B5%D0%BE%D1%82%D1%81%D0%BE%D1%80%D1%82%D0%B8%D1%80%D0%BE%D0%B2%D0%B0%D0%BD%D0%BD%D1%8B%D0%B9%20%D0%B4%D0%B8%D0%B0%D0%BF%D0%B0%D0%B7%D0%BE%D0%BD%20%5B0%2C%20i%5D%0A%20%20%20%20for%20i%20in%20range%28len%28nums%29%20-%201%2C%200%2C%20-1%29%3A%0A%20%20%20%20%20%20%20%20%23%20%D0%92%D0%BD%D1%83%D1%82%D1%80%D0%B5%D0%BD%D0%BD%D0%B8%D0%B9%20%D1%86%D0%B8%D0%BA%D0%BB%3A%20%D0%BF%D0%B5%D1%80%D0%B5%D0%BC%D0%B5%D1%81%D1%82%D0%B8%D1%82%D1%8C%20%D0%BC%D0%B0%D0%BA%D1%81%D0%B8%D0%BC%D0%B0%D0%BB%D1%8C%D0%BD%D1%8B%D0%B9%20%D1%8D%D0%BB%D0%B5%D0%BC%D0%B5%D0%BD%D1%82%20%D0%BD%D0%B5%D0%BE%D1%82%D1%81%D0%BE%D1%80%D1%82%D0%B8%D1%80%D0%BE%D0%B2%D0%B0%D0%BD%D0%BD%D0%BE%D0%B3%D0%BE%20%D0%B4%D0%B8%D0%B0%D0%BF%D0%B0%D0%B7%D0%BE%D0%BD%D0%B0%20%5B0%2C%20i%5D%20%D0%B2%20%D0%B5%D0%B3%D0%BE%20%D0%BF%D1%80%D0%B0%D0%B2%D1%8B%D0%B9%20%D0%BA%D0%BE%D0%BD%D0%B5%D1%86%0A%20%20%20%20%20%20%20%20for%20j%20in%20range%28i%29%3A%0A%20%20%20%20%20%20%20%20%20%20%20%20if%20nums%5Bj%5D%20%3E%20nums%5Bj%20%2B%201%5D%3A%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%23%20%D0%9F%D0%BE%D0%BC%D0%B5%D0%BD%D1%8F%D1%82%D1%8C%20%D0%BC%D0%B5%D1%81%D1%82%D0%B0%D0%BC%D0%B8%20nums%5Bj%5D%20%D0%B8%20nums%5Bj%20%2B%201%5D%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20tmp%20%3D%20nums%5Bj%5D%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20nums%5Bj%5D%20%3D%20nums%5Bj%20%2B%201%5D%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20nums%5Bj%20%2B%201%5D%20%3D%20tmp%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20count%20%2B%3D%203%20%20%23%20%D0%9E%D0%B1%D0%BC%D0%B5%D0%BD%20%D1%8D%D0%BB%D0%B5%D0%BC%D0%B5%D0%BD%D1%82%D0%BE%D0%B2%20%D0%B2%D0%BA%D0%BB%D1%8E%D1%87%D0%B0%D0%B5%D1%82%203%20%D1%8D%D0%BB%D0%B5%D0%BC%D0%B5%D0%BD%D1%82%D0%B0%D1%80%D0%BD%D1%8B%D0%B5%20%D0%BE%D0%BF%D0%B5%D1%80%D0%B0%D1%86%D0%B8%D0%B8%0A%20%20%20%20return%20count%0A%0A%22%22%22Driver%20Code%22%22%22%0Aif%20__name__%20%3D%3D%20%22__main__%22%3A%0A%20%20%20%20n%20%3D%208%0A%20%20%20%20print%28%22%D0%A0%D0%B0%D0%B7%D0%BC%D0%B5%D1%80%20%D0%B2%D1%85%D0%BE%D0%B4%D0%BD%D1%8B%D1%85%20%D0%B4%D0%B0%D0%BD%D0%BD%D1%8B%D1%85%20n%20%3D%22%2C%20n%29%0A%0A%20%20%20%20nums%20%3D%20%5Bi%20for%20i%20in%20range%28n%2C%200%2C%20-1%29%5D%20%20%23%20%5Bn%2C%20n-1%2C%20...%2C%202%2C%201%5D%0A%20%20%20%20count%20%3D%20bubble_sort%28nums%29%0A%20%20%20%20print%28%22%D0%A7%D0%B8%D1%81%D0%BB%D0%BE%20%D0%BE%D0%BF%D0%B5%D1%80%D0%B0%D1%86%D0%B8%D0%B9%20%D0%BA%D0%B2%D0%B0%D0%B4%D1%80%D0%B0%D1%82%D0%B8%D1%87%D0%BD%D0%BE%D0%B9%20%D1%81%D0%BB%D0%BE%D0%B6%D0%BD%D0%BE%D1%81%D1%82%D0%B8%20%28%D0%BF%D1%83%D0%B7%D1%8B%D1%80%D1%8C%D0%BA%D0%BE%D0%B2%D0%B0%D1%8F%20%D1%81%D0%BE%D1%80%D1%82%D0%B8%D1%80%D0%BE%D0%B2%D0%BA%D0%B0%29%20%3D%22%2C%20count%29&codeDivHeight=800&codeDivWidth=600&cumulative=false&curInstr=3&heapPrimitives=nevernest&origin=opt-frontend.js&py=311&rawInputLstJSON=%5B%5D&textReferences=false" target="_blank" rel="noopener noreferrer">Во весь экран &gt;</a></div></p>
</details>
<h3 id="4-o2n">4. &nbsp; Экспоненциальная сложность <span class="arithmatex">\(O(2^n)\)</span><a class="headerlink" href="#4-o2n" title="Permanent link">&para;</a></h3>
<p>Типичный пример экспоненциального роста в биологии - деление клеток: в начальном состоянии есть одна клетка, после одного деления их становится 2, после двух делений - 4 и так далее; после <span class="arithmatex">\(n\)</span> раундов деления клеток становится <span class="arithmatex">\(2^n\)</span> .</p>
<p>На рисунке 2-11 и в следующем коде моделируется процесс деления клеток; временная сложность равна <span class="arithmatex">\(O(2^n)\)</span> . Здесь входное значение <span class="arithmatex">\(n\)</span> обозначает число раундов деления, а возвращаемое значение <code>count</code> обозначает общее число делений.</p>
<p>Типичный пример экспоненциального роста в биологии - деление клеток: в начальном состоянии есть одна клетка, после одного деления их становится 2, после двух делений - 4 и так далее. После <span class="arithmatex">\(n\)</span> раундов деления клеток становится <span class="arithmatex">\(2^n\)</span> .</p>
<p>На рисунке 2-11 и в следующем коде моделируется процесс деления клеток. Временная сложность равна <span class="arithmatex">\(O(2^n)\)</span> . Здесь входное значение <span class="arithmatex">\(n\)</span> обозначает число раундов деления, а возвращаемое значение <code>count</code> обозначает общее число делений.</p>
<div class="tabbed-set tabbed-alternate" data-tabs="10:13"><input checked="checked" id="__tabbed_10_1" name="__tabbed_10" type="radio" /><input id="__tabbed_10_2" name="__tabbed_10" type="radio" /><input id="__tabbed_10_3" name="__tabbed_10" type="radio" /><input id="__tabbed_10_4" name="__tabbed_10" type="radio" /><input id="__tabbed_10_5" name="__tabbed_10" type="radio" /><input id="__tabbed_10_6" name="__tabbed_10" type="radio" /><input id="__tabbed_10_7" name="__tabbed_10" type="radio" /><input id="__tabbed_10_8" name="__tabbed_10" type="radio" /><input id="__tabbed_10_9" name="__tabbed_10" type="radio" /><input id="__tabbed_10_10" name="__tabbed_10" type="radio" /><input id="__tabbed_10_11" name="__tabbed_10" type="radio" /><input id="__tabbed_10_12" name="__tabbed_10" type="radio" /><input id="__tabbed_10_13" name="__tabbed_10" type="radio" /><div class="tabbed-labels"><label for="__tabbed_10_1">Python</label><label for="__tabbed_10_2">C++</label><label for="__tabbed_10_3">Java</label><label for="__tabbed_10_4">C#</label><label for="__tabbed_10_5">Go</label><label for="__tabbed_10_6">Swift</label><label for="__tabbed_10_7">JS</label><label for="__tabbed_10_8">TS</label><label for="__tabbed_10_9">Dart</label><label for="__tabbed_10_10">Rust</label><label for="__tabbed_10_11">C</label><label for="__tabbed_10_12">Kotlin</label><label for="__tabbed_10_13">Ruby</label></div>
<div class="tabbed-content">
<div class="tabbed-block">
@@ -6958,8 +6958,8 @@ T(n) &amp; = n^2 + n &amp; \text{ленивый подсчет (o.O)}
</details>
<p>Экспоненциальный рост происходит очень быстро и часто встречается в переборных методах, грубой силе, поиске с возвратом и тому подобных подходах. Для задач большого масштаба экспоненциальная сложность неприемлема, и обычно приходится применять динамическое программирование, жадные алгоритмы и другие стратегии.</p>
<h3 id="5-olog-n">5. &nbsp; Логарифмическая сложность <span class="arithmatex">\(O(\log n)\)</span><a class="headerlink" href="#5-olog-n" title="Permanent link">&para;</a></h3>
<p>В противоположность экспоненциальной, логарифмическая сложность описывает ситуацию, когда <strong>в каждом раунде размер задачи уменьшается вдвое</strong>. Пусть размер входных данных равен <span class="arithmatex">\(n\)</span> ; так как на каждом шаге размер уменьшается вдвое, число итераций равно <span class="arithmatex">\(\log_2 n\)</span> , то есть является обратной функцией к <span class="arithmatex">\(2^n\)</span> .</p>
<p>На рисунке 2-12 и в следующем коде моделируется процесс, в котором <strong>в каждом раунде размер задачи уменьшается вдвое</strong>; временная сложность равна <span class="arithmatex">\(O(\log_2 n)\)</span> и кратко записывается как <span class="arithmatex">\(O(\log n)\)</span> :</p>
<p>В противоположность экспоненциальной, логарифмическая сложность описывает ситуацию, когда <strong>в каждом раунде размер задачи уменьшается вдвое</strong>. Пусть размер входных данных равен <span class="arithmatex">\(n\)</span>. Так как на каждом шаге размер уменьшается вдвое, число итераций равно <span class="arithmatex">\(\log_2 n\)</span> , то есть является обратной функцией к <span class="arithmatex">\(2^n\)</span> .</p>
<p>На рисунке 2-12 и в следующем коде моделируется процесс, в котором <strong>в каждом раунде размер задачи уменьшается вдвое</strong>. Временная сложность равна <span class="arithmatex">\(O(\log_2 n)\)</span> и кратко записывается как <span class="arithmatex">\(O(\log n)\)</span> :</p>
<div class="tabbed-set tabbed-alternate" data-tabs="12:13"><input checked="checked" id="__tabbed_12_1" name="__tabbed_12" type="radio" /><input id="__tabbed_12_2" name="__tabbed_12" type="radio" /><input id="__tabbed_12_3" name="__tabbed_12" type="radio" /><input id="__tabbed_12_4" name="__tabbed_12" type="radio" /><input id="__tabbed_12_5" name="__tabbed_12" type="radio" /><input id="__tabbed_12_6" name="__tabbed_12" type="radio" /><input id="__tabbed_12_7" name="__tabbed_12" type="radio" /><input id="__tabbed_12_8" name="__tabbed_12" type="radio" /><input id="__tabbed_12_9" name="__tabbed_12" type="radio" /><input id="__tabbed_12_10" name="__tabbed_12" type="radio" /><input id="__tabbed_12_11" name="__tabbed_12" type="radio" /><input id="__tabbed_12_12" name="__tabbed_12" type="radio" /><input id="__tabbed_12_13" name="__tabbed_12" type="radio" /><div class="tabbed-labels"><label for="__tabbed_12_1">Python</label><label for="__tabbed_12_2">C++</label><label for="__tabbed_12_3">Java</label><label for="__tabbed_12_4">C#</label><label for="__tabbed_12_5">Go</label><label for="__tabbed_12_6">Swift</label><label for="__tabbed_12_7">JS</label><label for="__tabbed_12_8">TS</label><label for="__tabbed_12_9">Dart</label><label for="__tabbed_12_10">Rust</label><label for="__tabbed_12_11">C</label><label for="__tabbed_12_12">Kotlin</label><label for="__tabbed_12_13">Ruby</label></div>
<div class="tabbed-content">
<div class="tabbed-block">
@@ -7376,10 +7376,10 @@ T(n) &amp; = n^2 + n &amp; \text{ленивый подсчет (o.O)}
<p><div style="height: 423px; width: 100%;"><iframe class="pythontutor-iframe" src="https://pythontutor.com/iframe-embed.html#code=def%20log_recur%28n%3A%20int%29%20-%3E%20int%3A%0A%20%20%20%20%22%22%22%D0%9B%D0%BE%D0%B3%D0%B0%D1%80%D0%B8%D1%84%D0%BC%D0%B8%D1%87%D0%B5%D1%81%D0%BA%D0%B0%D1%8F%20%D1%81%D0%BB%D0%BE%D0%B6%D0%BD%D0%BE%D1%81%D1%82%D1%8C%20%28%D1%80%D0%B5%D0%BA%D1%83%D1%80%D1%81%D0%B8%D0%B2%D0%BD%D0%B0%D1%8F%20%D1%80%D0%B5%D0%B0%D0%BB%D0%B8%D0%B7%D0%B0%D1%86%D0%B8%D1%8F%29%22%22%22%0A%20%20%20%20if%20n%20%3C%3D%201%3A%0A%20%20%20%20%20%20%20%20return%200%0A%20%20%20%20return%20log_recur%28n%20%2F%202%29%20%2B%201%0A%0A%22%22%22Driver%20Code%22%22%22%0Aif%20__name__%20%3D%3D%20%22__main__%22%3A%0A%20%20%20%20n%20%3D%208%0A%20%20%20%20print%28%22%D0%A0%D0%B0%D0%B7%D0%BC%D0%B5%D1%80%20%D0%B2%D1%85%D0%BE%D0%B4%D0%BD%D1%8B%D1%85%20%D0%B4%D0%B0%D0%BD%D0%BD%D1%8B%D1%85%20n%20%3D%22%2C%20n%29%0A%0A%20%20%20%20count%20%3D%20log_recur%28n%29%0A%20%20%20%20print%28%22%D0%A7%D0%B8%D1%81%D0%BB%D0%BE%20%D0%BE%D0%BF%D0%B5%D1%80%D0%B0%D1%86%D0%B8%D0%B9%20%D0%BB%D0%BE%D0%B3%D0%B0%D1%80%D0%B8%D1%84%D0%BC%D0%B8%D1%87%D0%B5%D1%81%D0%BA%D0%BE%D0%B9%20%D1%81%D0%BB%D0%BE%D0%B6%D0%BD%D0%BE%D1%81%D1%82%D0%B8%20%28%D1%80%D0%B5%D0%BA%D1%83%D1%80%D1%81%D0%B8%D0%B2%D0%BD%D0%B0%D1%8F%20%D1%80%D0%B5%D0%B0%D0%BB%D0%B8%D0%B7%D0%B0%D1%86%D0%B8%D1%8F%29%20%3D%22%2C%20count%29&codeDivHeight=472&codeDivWidth=350&cumulative=false&curInstr=4&heapPrimitives=nevernest&origin=opt-frontend.js&py=311&rawInputLstJSON=%5B%5D&textReferences=false"> </iframe></div>
<div style="margin-top: 5px;"><a href="https://pythontutor.com/iframe-embed.html#code=def%20log_recur%28n%3A%20int%29%20-%3E%20int%3A%0A%20%20%20%20%22%22%22%D0%9B%D0%BE%D0%B3%D0%B0%D1%80%D0%B8%D1%84%D0%BC%D0%B8%D1%87%D0%B5%D1%81%D0%BA%D0%B0%D1%8F%20%D1%81%D0%BB%D0%BE%D0%B6%D0%BD%D0%BE%D1%81%D1%82%D1%8C%20%28%D1%80%D0%B5%D0%BA%D1%83%D1%80%D1%81%D0%B8%D0%B2%D0%BD%D0%B0%D1%8F%20%D1%80%D0%B5%D0%B0%D0%BB%D0%B8%D0%B7%D0%B0%D1%86%D0%B8%D1%8F%29%22%22%22%0A%20%20%20%20if%20n%20%3C%3D%201%3A%0A%20%20%20%20%20%20%20%20return%200%0A%20%20%20%20return%20log_recur%28n%20%2F%202%29%20%2B%201%0A%0A%22%22%22Driver%20Code%22%22%22%0Aif%20__name__%20%3D%3D%20%22__main__%22%3A%0A%20%20%20%20n%20%3D%208%0A%20%20%20%20print%28%22%D0%A0%D0%B0%D0%B7%D0%BC%D0%B5%D1%80%20%D0%B2%D1%85%D0%BE%D0%B4%D0%BD%D1%8B%D1%85%20%D0%B4%D0%B0%D0%BD%D0%BD%D1%8B%D1%85%20n%20%3D%22%2C%20n%29%0A%0A%20%20%20%20count%20%3D%20log_recur%28n%29%0A%20%20%20%20print%28%22%D0%A7%D0%B8%D1%81%D0%BB%D0%BE%20%D0%BE%D0%BF%D0%B5%D1%80%D0%B0%D1%86%D0%B8%D0%B9%20%D0%BB%D0%BE%D0%B3%D0%B0%D1%80%D0%B8%D1%84%D0%BC%D0%B8%D1%87%D0%B5%D1%81%D0%BA%D0%BE%D0%B9%20%D1%81%D0%BB%D0%BE%D0%B6%D0%BD%D0%BE%D1%81%D1%82%D0%B8%20%28%D1%80%D0%B5%D0%BA%D1%83%D1%80%D1%81%D0%B8%D0%B2%D0%BD%D0%B0%D1%8F%20%D1%80%D0%B5%D0%B0%D0%BB%D0%B8%D0%B7%D0%B0%D1%86%D0%B8%D1%8F%29%20%3D%22%2C%20count%29&codeDivHeight=800&codeDivWidth=600&cumulative=false&curInstr=4&heapPrimitives=nevernest&origin=opt-frontend.js&py=311&rawInputLstJSON=%5B%5D&textReferences=false" target="_blank" rel="noopener noreferrer">Во весь экран &gt;</a></div></p>
</details>
<p>Логарифмическая сложность часто встречается в алгоритмах, основанных на стратегии "разделяй и властвуй", и отражает идеи разбиения на части и упрощения сложной задачи. Она растет медленно и считается одной из самых желательных временных сложностей после константной.</p>
<p>Логарифмическая сложность часто встречается в алгоритмах, основанных на стратегии «разделяй и властвуй», и отражает идеи разбиения на части и упрощения сложной задачи. Она растет медленно и считается одной из самых желательных временных сложностей после константной.</p>
<div class="admonition tip">
<p class="admonition-title">Каково основание у <span class="arithmatex">\(O(\log n)\)</span> ?</p>
<p>Точнее говоря, "разделение на <span class="arithmatex">\(m\)</span> частей" соответствует временной сложности <span class="arithmatex">\(O(\log_m n)\)</span> . А по формуле перехода к другому основанию логарифма мы получаем равные по сложности выражения с разными основаниями:</p>
<p>Точнее говоря, «разделение на <span class="arithmatex">\(m\)</span> частей» соответствует временной сложности <span class="arithmatex">\(O(\log_m n)\)</span> . А по формуле перехода к другому основанию логарифма мы получаем равные по сложности выражения с разными основаниями:</p>
<div class="arithmatex">\[
O(\log_m n) = O(\log_k n / \log_k m) = O(\log_k n)
\]</div>
@@ -7775,7 +7775,7 @@ n! = n \times (n - 1) \times (n - 2) \times \dots \times 2 \times 1
<p>Следует отметить, что поскольку при <span class="arithmatex">\(n \geq 4\)</span> всегда выполняется <span class="arithmatex">\(n! &gt; 2^n\)</span> , факториальная сложность растет еще быстрее, чем экспоненциальная, и при больших <span class="arithmatex">\(n\)</span> становится неприемлемой.</p>
<h2 id="235">2.3.5 &nbsp; Худшая, лучшая и средняя временная сложность<a class="headerlink" href="#235" title="Permanent link">&para;</a></h2>
<p><strong>Временная эффективность алгоритма часто не фиксирована, а зависит от распределения входных данных</strong>. Предположим, на вход подается массив <code>nums</code> длины <span class="arithmatex">\(n\)</span> , состоящий из чисел от <span class="arithmatex">\(1\)</span> до <span class="arithmatex">\(n\)</span> , каждое из которых встречается ровно один раз; при этом порядок элементов случайно перемешан. Задача состоит в том, чтобы вернуть индекс элемента <span class="arithmatex">\(1\)</span> . Тогда можно сделать следующие выводы.</p>
<p><strong>Временная эффективность алгоритма часто не фиксирована, а зависит от распределения входных данных</strong>. Предположим, на вход подается массив <code>nums</code> длины <span class="arithmatex">\(n\)</span> , состоящий из чисел от <span class="arithmatex">\(1\)</span> до <span class="arithmatex">\(n\)</span> , каждое из которых встречается ровно один раз. При этом порядок элементов случайно перемешан. Задача состоит в том, чтобы вернуть индекс элемента <span class="arithmatex">\(1\)</span> . Тогда можно сделать следующие выводы.</p>
<ul>
<li>Когда <code>nums = [?, ?, ..., 1]</code> , то есть когда последний элемент равен <span class="arithmatex">\(1\)</span> , нужно полностью пройти по массиву, <strong>что дает худшую временную сложность <span class="arithmatex">\(O(n)\)</span></strong> .</li>
<li>Когда <code>nums = [1, ?, ?, ...]</code> , то есть когда первый элемент равен <span class="arithmatex">\(1\)</span> , независимо от длины массива продолжать обход не нужно, <strong>что дает лучшую временную сложность <span class="arithmatex">\(\Omega(1)\)</span></strong> .</li>
@@ -8139,12 +8139,12 @@ n! = n \times (n - 1) \times (n - 2) \times \dots \times 2 \times 1
<div style="margin-top: 5px;"><a href="https://pythontutor.com/iframe-embed.html#code=import%20random%0A%0Adef%20random_numbers%28n%3A%20int%29%20-%3E%20list%5Bint%5D%3A%0A%20%20%20%20%22%22%22%D0%A1%D0%B3%D0%B5%D0%BD%D0%B5%D1%80%D0%B8%D1%80%D0%BE%D0%B2%D0%B0%D1%82%D1%8C%20%D0%BC%D0%B0%D1%81%D1%81%D0%B8%D0%B2%20%D1%81%20%D1%8D%D0%BB%D0%B5%D0%BC%D0%B5%D0%BD%D1%82%D0%B0%D0%BC%D0%B8%201%2C%202%2C%20...%2C%20n%20%D0%B2%20%D1%81%D0%BB%D1%83%D1%87%D0%B0%D0%B9%D0%BD%D0%BE%D0%BC%20%D0%BF%D0%BE%D1%80%D1%8F%D0%B4%D0%BA%D0%B5%22%22%22%0A%20%20%20%20%23%20%D0%A1%D0%BE%D0%B7%D0%B4%D0%B0%D1%82%D1%8C%20%D0%BC%D0%B0%D1%81%D1%81%D0%B8%D0%B2%20nums%20%3D%3A%201%2C%202%2C%203%2C%20...%2C%20n%0A%20%20%20%20nums%20%3D%20%5Bi%20for%20i%20in%20range%281%2C%20n%20%2B%201%29%5D%0A%20%20%20%20%23%20%D0%A1%D0%BB%D1%83%D1%87%D0%B0%D0%B9%D0%BD%D0%BE%20%D0%BF%D0%B5%D1%80%D0%B5%D0%BC%D0%B5%D1%88%D0%B0%D1%82%D1%8C%20%D1%8D%D0%BB%D0%B5%D0%BC%D0%B5%D0%BD%D1%82%D1%8B%20%D0%BC%D0%B0%D1%81%D1%81%D0%B8%D0%B2%D0%B0%0A%20%20%20%20random.shuffle%28nums%29%0A%20%20%20%20return%20nums%0A%0Adef%20find_one%28nums%3A%20list%5Bint%5D%29%20-%3E%20int%3A%0A%20%20%20%20%22%22%22%D0%9D%D0%B0%D0%B9%D1%82%D0%B8%20%D0%B8%D0%BD%D0%B4%D0%B5%D0%BA%D1%81%20%D1%87%D0%B8%D1%81%D0%BB%D0%B0%201%20%D0%B2%20%D0%BC%D0%B0%D1%81%D1%81%D0%B8%D0%B2%D0%B5%20nums%22%22%22%0A%20%20%20%20for%20i%20in%20range%28len%28nums%29%29%3A%0A%20%20%20%20%20%20%20%20%23%20%D0%9A%D0%BE%D0%B3%D0%B4%D0%B0%20%D1%8D%D0%BB%D0%B5%D0%BC%D0%B5%D0%BD%D1%82%201%20%D0%BD%D0%B0%D1%85%D0%BE%D0%B4%D0%B8%D1%82%D1%81%D1%8F%20%D0%B2%20%D0%BD%D0%B0%D1%87%D0%B0%D0%BB%D0%B5%20%D0%BC%D0%B0%D1%81%D1%81%D0%B8%D0%B2%D0%B0%2C%20%D0%B4%D0%BE%D1%81%D1%82%D0%B8%D0%B3%D0%B0%D0%B5%D1%82%D1%81%D1%8F%20%D0%BB%D1%83%D1%87%D1%88%D0%B0%D1%8F%20%D0%B2%D1%80%D0%B5%D0%BC%D0%B5%D0%BD%D0%BD%D0%B0%D1%8F%20%D1%81%D0%BB%D0%BE%D0%B6%D0%BD%D0%BE%D1%81%D1%82%D1%8C%20O%281%29%0A%20%20%20%20%20%20%20%20%23%20%D0%9A%D0%BE%D0%B3%D0%B4%D0%B0%20%D1%8D%D0%BB%D0%B5%D0%BC%D0%B5%D0%BD%D1%82%201%20%D0%BD%D0%B0%D1%85%D0%BE%D0%B4%D0%B8%D1%82%D1%81%D1%8F%20%D0%B2%20%D0%BA%D0%BE%D0%BD%D1%86%D0%B5%20%D0%BC%D0%B0%D1%81%D1%81%D0%B8%D0%B2%D0%B0%2C%20%D0%B4%D0%BE%D1%81%D1%82%D0%B8%D0%B3%D0%B0%D0%B5%D1%82%D1%81%D1%8F%20%D1%85%D1%83%D0%B4%D1%88%D0%B0%D1%8F%20%D0%B2%D1%80%D0%B5%D0%BC%D0%B5%D0%BD%D0%BD%D0%B0%D1%8F%20%D1%81%D0%BB%D0%BE%D0%B6%D0%BD%D0%BE%D1%81%D1%82%D1%8C%20O%28n%29%0A%20%20%20%20%20%20%20%20if%20nums%5Bi%5D%20%3D%3D%201%3A%0A%20%20%20%20%20%20%20%20%20%20%20%20return%20i%0A%20%20%20%20return%20-1%0A%0A%22%22%22Driver%20Code%22%22%22%0Aif%20__name__%20%3D%3D%20%22__main__%22%3A%0A%20%20%20%20n%20%3D%2010%0A%20%20%20%20nums%20%3D%20random_numbers%28n%29%0A%20%20%20%20index%20%3D%20find_one%28nums%29%0A%20%20%20%20print%28%22%5Cn%D0%9C%D0%B0%D1%81%D1%81%D0%B8%D0%B2%20%5B1%2C%202%2C%20...%2C%20n%5D%20%D0%BF%D0%BE%D1%81%D0%BB%D0%B5%20%D0%BF%D0%B5%D1%80%D0%B5%D0%BC%D0%B5%D1%88%D0%B8%D0%B2%D0%B0%D0%BD%D0%B8%D1%8F%20%3D%22%2C%20nums%29%0A%20%20%20%20print%28%22%D0%98%D0%BD%D0%B4%D0%B5%D0%BA%D1%81%20%D1%87%D0%B8%D1%81%D0%BB%D0%B0%201%20%3D%22%2C%20index%29&codeDivHeight=800&codeDivWidth=600&cumulative=false&curInstr=25&heapPrimitives=nevernest&origin=opt-frontend.js&py=311&rawInputLstJSON=%5B%5D&textReferences=false" target="_blank" rel="noopener noreferrer">Во весь экран &gt;</a></div></p>
</details>
<p>Стоит отметить, что на практике лучшая временная сложность используется редко, поскольку обычно она достигается лишь с очень малой вероятностью и может вводить в заблуждение. <strong>Худшая временная сложность гораздо практичнее, потому что задает безопасную оценку эффективности</strong> и позволяет уверенно использовать алгоритм.</p>
<p>Из приведенного выше примера видно, что худшая и лучшая временные сложности возникают только при особых распределениях данных; вероятность таких случаев может быть низкой, и они не всегда реально отражают эффективность алгоритма. Напротив, <strong>средняя временная сложность способна показать эффективность алгоритма на случайных входных данных</strong> и обозначается символом <span class="arithmatex">\(\Theta\)</span> .</p>
<p>Для некоторых алгоритмов можно относительно просто вывести средний случай при случайном распределении данных. Например, в приведенном выше примере входной массив перемешан, а вероятность появления элемента <span class="arithmatex">\(1\)</span> на любом индексе одинакова; следовательно, среднее число итераций алгоритма равно половине длины массива, то есть <span class="arithmatex">\(n / 2\)</span> , а средняя временная сложность равна <span class="arithmatex">\(\Theta(n / 2) = \Theta(n)\)</span> .</p>
<p>Из приведенного выше примера видно, что худшая и лучшая временные сложности возникают только при особых распределениях данных. Вероятность таких случаев может быть низкой, и они не всегда реально отражают эффективность алгоритма. Напротив, <strong>средняя временная сложность способна показать эффективность алгоритма на случайных входных данных</strong> и обозначается символом <span class="arithmatex">\(\Theta\)</span> .</p>
<p>Для некоторых алгоритмов можно относительно просто вывести средний случай при случайном распределении данных. Например, в приведенном выше примере входной массив перемешан, а вероятность появления элемента <span class="arithmatex">\(1\)</span> на любом индексе одинакова. Следовательно, среднее число итераций алгоритма равно половине длины массива, то есть <span class="arithmatex">\(n / 2\)</span> , а средняя временная сложность равна <span class="arithmatex">\(\Theta(n / 2) = \Theta(n)\)</span> .</p>
<p>Однако для более сложных алгоритмов вычислить среднюю временную сложность часто непросто, потому что трудно проанализировать полное математическое ожидание на заданном распределении данных. В таких случаях обычно используют худшую временную сложность как критерий оценки эффективности алгоритма.</p>
<div class="admonition question">
<p class="admonition-title">Почему символ <span class="arithmatex">\(\Theta\)</span> встречается так редко?</p>
<p>Возможно, потому что символ <span class="arithmatex">\(O\)</span> звучит слишком привычно, и мы часто используем его для обозначения средней временной сложности. Но строго говоря, это некорректно. В этой книге и в других материалах, если встретится выражение вроде "средняя временная сложность <span class="arithmatex">\(O(n)\)</span>", просто понимай его как <span class="arithmatex">\(\Theta(n)\)</span> .</p>
<p>Возможно, потому что символ <span class="arithmatex">\(O\)</span> звучит слишком привычно, и мы часто используем его для обозначения средней временной сложности. Но строго говоря, это некорректно. В этой книге и в других материалах, если встретится выражение вроде «средняя временная сложность <span class="arithmatex">\(O(n)\)</span>», просто понимай его как <span class="arithmatex">\(\Theta(n)\)</span> .</p>
</div>
<!-- Source file information -->
@@ -4273,7 +4273,7 @@
<li>Целочисленные типы <code>byte</code> , <code>short</code> , <code>int</code> , <code>long</code> .</li>
<li>Типы с плавающей точкой <code>float</code> , <code>double</code> , используемые для представления дробных чисел.</li>
<li>Символьный тип <code>char</code> , используемый для представления букв, знаков препинания и даже эмодзи в разных языках.</li>
<li>Логический тип <code>bool</code> , используемый для представления суждений "да" и "нет".</li>
<li>Логический тип <code>bool</code> , используемый для представления суждений «да» и «нет».</li>
</ul>
<p><strong>Базовые типы данных хранятся в компьютере в двоичной форме</strong>. Один двоичный разряд равен <span class="arithmatex">\(1\)</span> биту. В большинстве современных операционных систем <span class="arithmatex">\(1\)</span> байт (byte) состоит из <span class="arithmatex">\(8\)</span> битов (bit).</p>
<p>Диапазон значений базовых типов данных зависит от объема занимаемого ими пространства. Ниже в качестве примера используется Java.</p>
@@ -4281,7 +4281,7 @@
<li>Целочисленный тип <code>byte</code> занимает <span class="arithmatex">\(1\)</span> байт = <span class="arithmatex">\(8\)</span> бит и может представлять <span class="arithmatex">\(2^{8}\)</span> чисел.</li>
<li>Целочисленный тип <code>int</code> занимает <span class="arithmatex">\(4\)</span> байта = <span class="arithmatex">\(32\)</span> бита и может представлять <span class="arithmatex">\(2^{32}\)</span> чисел.</li>
</ul>
<p>В таблице 3-1 перечислены объем памяти, диапазон значений и значения по умолчанию для различных базовых типов данных в Java. Эту таблицу не нужно заучивать наизусть; достаточно иметь общее представление и при необходимости обращаться к ней.</p>
<p>В таблице 3-1 перечислены объем памяти, диапазон значений и значения по умолчанию для различных базовых типов данных в Java. Эту таблицу не нужно заучивать наизусть. Достаточно иметь общее представление и при необходимости обращаться к ней.</p>
<p align="center"> Таблица 3-1 &nbsp; Объем памяти и диапазоны значений базовых типов данных </p>
<div class="center-table">
@@ -4364,16 +4364,16 @@
</tbody>
</table>
</div>
<p>Обрати внимание: приведенная выше таблица относится именно к базовым типам данных Java. В каждом языке программирования свои определения типов, поэтому объем памяти, диапазон значений и значения по умолчанию могут различаться.</p>
<p>Обрати внимание: в таблице 3-1 приведены данные именно для базовых типов данных Java. В каждом языке программирования свои определения типов, поэтому объем памяти, диапазон значений и значения по умолчанию могут различаться.</p>
<ul>
<li>В Python целочисленный тип <code>int</code> может иметь произвольный размер, ограниченный только доступной памятью; тип <code>float</code> является 64-битным числом двойной точности; типа <code>char</code> нет, а отдельный символ на деле является строкой <code>str</code> длины 1.</li>
<li>В C и C++ размер базовых типов данных явно не зафиксирован и зависит от реализации и платформы. таблица 3-1 соответствует модели данных LP64 <a href="https://en.cppreference.com/w/cpp/language/types#Properties">data model</a>, используемой в 64-битных Unix-системах, включая Linux и macOS.</li>
<li>Размер символа <code>char</code> в C и C++ составляет 1 байт, а в большинстве других языков программирования зависит от конкретного способа кодирования символов; подробнее это рассматривается в разделе "Кодирование символов".</li>
<li>В Python целочисленный тип <code>int</code> может иметь произвольный размер, ограниченный только доступной памятью. Тип <code>float</code> является 64-битным числом двойной точности. Типа <code>char</code> нет, а отдельный символ на деле является строкой <code>str</code> длины 1.</li>
<li>В C и C++ размер базовых типов данных явно не зафиксирован и зависит от реализации и платформы. В таблице 3-1 приведены данные для модели LP64 <a href="https://en.cppreference.com/w/cpp/language/types#Properties">data model</a>, используемой в 64-битных Unix-системах, включая Linux и macOS.</li>
<li>Размер символа <code>char</code> в C и C++ составляет 1 байт, а в большинстве других языков программирования зависит от конкретного способа кодирования символов. Подробнее это рассматривается в разделе «Кодирование символов».</li>
<li>Хотя для представления логического значения достаточно 1 бита ( <span class="arithmatex">\(0\)</span> или <span class="arithmatex">\(1\)</span> ), в памяти оно обычно хранится как 1 байт. Это связано с тем, что современные CPU обычно используют 1 байт как минимальную адресуемую единицу памяти.</li>
</ul>
<p>Какова же связь между базовыми типами данных и структурами данных? Мы знаем, что структура данных - это способ организации и хранения данных в компьютере. Подлежащее в этой фразе - "структура", а не "данные".</p>
<p>Если мы хотим представить "ряд чисел", то естественно подумаем об использовании массива. Это связано с тем, что линейная структура массива может выразить отношения соседства и порядка между числами, а то, что именно хранится внутри - целые <code>int</code> , вещественные <code>float</code> или символы <code>char</code> , - к "структуре данных" отношения не имеет.</p>
<p>Иными словами, <strong>базовые типы данных задают "тип содержимого" данных, а структуры данных задают "способ организации" данных</strong>. Например, в следующем коде мы используем одну и ту же структуру данных (массив) для хранения и представления различных базовых типов данных, включая <code>int</code> , <code>float</code> , <code>char</code> , <code>bool</code> и т.д.</p>
<p>Какова же связь между базовыми типами данных и структурами данных? Мы знаем, что структура данных - это способ организации и хранения данных в компьютере. Подлежащее в этой фразе - «структура», а не «данные».</p>
<p>Если мы хотим представить «ряд чисел», то естественно подумаем об использовании массива. Это связано с тем, что линейная структура массива может выразить отношения соседства и порядка между числами, а то, что именно хранится внутри - целые <code>int</code> , вещественные <code>float</code> или символы <code>char</code> , - к «структуре данных» отношения не имеет.</p>
<p>Иными словами, <strong>базовые типы данных задают «тип содержимого» данных, а структуры данных задают «способ организации» данных</strong>. Например, в следующем коде мы используем одну и ту же структуру данных (массив) для хранения и представления различных базовых типов данных, включая <code>int</code> , <code>float</code> , <code>char</code> , <code>bool</code> и т.д.</p>
<div class="tabbed-set tabbed-alternate" data-tabs="1:13"><input checked="checked" id="__tabbed_1_1" name="__tabbed_1" type="radio" /><input id="__tabbed_1_2" name="__tabbed_1" type="radio" /><input id="__tabbed_1_3" name="__tabbed_1" type="radio" /><input id="__tabbed_1_4" name="__tabbed_1" type="radio" /><input id="__tabbed_1_5" name="__tabbed_1" type="radio" /><input id="__tabbed_1_6" name="__tabbed_1" type="radio" /><input id="__tabbed_1_7" name="__tabbed_1" type="radio" /><input id="__tabbed_1_8" name="__tabbed_1" type="radio" /><input id="__tabbed_1_9" name="__tabbed_1" type="radio" /><input id="__tabbed_1_10" name="__tabbed_1" type="radio" /><input id="__tabbed_1_11" name="__tabbed_1" type="radio" /><input id="__tabbed_1_12" name="__tabbed_1" type="radio" /><input id="__tabbed_1_13" name="__tabbed_1" type="radio" /><div class="tabbed-labels"><label for="__tabbed_1_1">Python</label><label for="__tabbed_1_2">C++</label><label for="__tabbed_1_3">Java</label><label for="__tabbed_1_4">C#</label><label for="__tabbed_1_5">Go</label><label for="__tabbed_1_6">Swift</label><label for="__tabbed_1_7">JS</label><label for="__tabbed_1_8">TS</label><label for="__tabbed_1_9">Dart</label><label for="__tabbed_1_10">Rust</label><label for="__tabbed_1_11">C</label><label for="__tabbed_1_12">Kotlin</label><label for="__tabbed_1_13">Ruby</label></div>
<div class="tabbed-content">
<div class="tabbed-block">
@@ -4423,9 +4423,9 @@
<!-- Page content -->
<h1 id="34">3.4 &nbsp; Кодирование символов *<a class="headerlink" href="#34" title="Permanent link">&para;</a></h1>
<p>В компьютере все данные хранятся в виде двоичных чисел, и символьный тип данных <code>char</code> не является исключением. Для представления символов необходимо задать "таблицу символов", которая устанавливает взаимно-однозначное соответствие между каждым символом и двоичным числом. С помощью этой таблицы компьютер может преобразовывать двоичные числа в символы.</p>
<p>В компьютере все данные хранятся в виде двоичных чисел, и символьный тип данных <code>char</code> не является исключением. Для представления символов необходимо задать «таблицу символов», которая устанавливает взаимно-однозначное соответствие между каждым символом и двоичным числом. С помощью этой таблицы компьютер может преобразовывать двоичные числа в символы.</p>
<h2 id="341-ascii">3.4.1 &nbsp; Таблица символов ASCII<a class="headerlink" href="#341-ascii" title="Permanent link">&para;</a></h2>
<p><u>Код ASCII</u> - это самая ранняя таблица символов; ее полное название - American Standard Code for Information Interchange (американский стандартный код обмена информацией). Для представления символов в ней используются 7 двоичных битов (нижние 7 битов одного байта), что позволяет закодировать до 128 различных символов. Как показано на рисунке 3-6, ASCII включает заглавные и строчные буквы английского алфавита, цифры 0 ~ 9, некоторые знаки препинания, а также некоторые управляющие символы (например перевод строки и табуляцию).</p>
<p><u>Код ASCII</u> - это самая ранняя таблица символов. Ее полное название - American Standard Code for Information Interchange (американский стандартный код обмена информацией). Для представления символов в ней используются 7 двоичных битов (нижние 7 битов одного байта), что позволяет закодировать до 128 различных символов. Как показано на рисунке 3-6, ASCII включает заглавные и строчные буквы английского алфавита, цифры 0 ~ 9, некоторые знаки препинания, а также некоторые управляющие символы (например перевод строки и табуляцию).</p>
<p><img alt="Таблица ASCII" class="animation-figure" src="../character_encoding.assets/ascii_table.png" /></p>
<p align="center"> Рисунок 3-6 &nbsp; Таблица ASCII </p>
@@ -4435,11 +4435,11 @@
<p>Позже люди обнаружили, что <strong>кодов EASCII все равно недостаточно для количества символов во многих языках</strong>. Например, китайских иероглифов существует почти сто тысяч, а в повседневном употреблении нужны тысячи. В 1980 году Государственное управление стандартов Китая выпустило таблицу символов <u>GB2312</u>, включающую 6763 иероглифа, что в основном удовлетворило потребности компьютерной обработки китайского текста.</p>
<p>Однако GB2312 не умеет работать с некоторыми редкими иероглифами и традиционными формами письма. Таблица символов <u>GBK</u> представляет собой расширение GB2312 и в общей сложности содержит 21886 иероглифов. В схеме кодирования GBK символы ASCII представляются одним байтом, а китайские иероглифы - двумя байтами.</p>
<h2 id="343-unicode">3.4.3 &nbsp; Таблица символов Unicode<a class="headerlink" href="#343-unicode" title="Permanent link">&para;</a></h2>
<p>С бурным развитием компьютерной техники таблицы символов и стандарты кодирования начали стремительно множиться, и это породило множество проблем. С одной стороны, такие таблицы обычно определяли символы только для конкретных языков и не могли нормально работать в многоязычной среде. С другой стороны, для одного и того же языка существовало несколько стандартов кодирования; если две машины использовали разные стандарты, при обмене информацией возникали искажения текста.</p>
<p>С бурным развитием компьютерной техники таблицы символов и стандарты кодирования начали стремительно множиться, и это породило множество проблем. С одной стороны, такие таблицы обычно определяли символы только для конкретных языков и не могли нормально работать в многоязычной среде. С другой стороны, для одного и того же языка существовало несколько стандартов кодирования. Если две машины использовали разные стандарты, при обмене информацией возникали искажения текста.</p>
<p>Исследователи той эпохи задумались: <strong>если создать достаточно полную таблицу символов, которая включит все языки и знаки мира, разве это не решит проблемы многоязычной среды и искаженного текста</strong>? Под влиянием этой идеи и появилась большая и всеобъемлющая таблица символов Unicode.</p>
<p><u>Unicode</u> по-китайски называется "единый код" и теоретически способен вместить более миллиона символов. Его цель - собрать символы со всего мира в единую таблицу символов, предоставить универсальный стандарт для обработки и отображения текстов на разных языках и уменьшить количество проблем с искажением текста, вызванных различиями стандартов кодирования. С момента публикации в 1991 году Unicode непрерывно расширялся, добавляя новые языки и символы. По состоянию на сентябрь 2022 года Unicode уже включал 149186 символов, в том числе буквы разных языков, знаки, а также эмодзи.</p>
<p>Как универсальный набор символов, Unicode по сути присваивает каждому символу уникальную "кодовую точку" (числовой идентификатор символа), диапазон которой составляет от U+0000 до U+10FFFF, образуя единое пространство нумерации символов. Однако <strong>Unicode не определяет, как именно хранить эти кодовые точки в компьютере</strong>. Тут неизбежно возникает вопрос: если в одном тексте одновременно встречаются кодовые точки Unicode разной длины, как система должна разбирать символы? Например, если дан код длиной 2 байта, как понять, является ли это одним 2-байтовым символом или двумя 1-байтовыми?</p>
<p>Для этой проблемы <strong>прямолинейное решение состоит в том, чтобы хранить все символы в кодировке одинаковой длины</strong>. Как показано на рисунке 3-7, каждый символ в "Hello" занимает 1 байт, а каждый символ в "алгоритм" занимает 2 байта. Мы можем дополнить старшие биты нулями и закодировать все символы в "Hello алгоритм" в виде 2-байтовых единиц. Тогда система сможет считывать по одному символу каждые 2 байта и восстановить эту фразу.</p>
<p><u>Unicode</u> по-китайски называется «единый код» и теоретически способен вместить более миллиона символов. Его цель - собрать символы со всего мира в единую таблицу символов, предоставить универсальный стандарт для обработки и отображения текстов на разных языках и уменьшить количество проблем с искажением текста, вызванных различиями стандартов кодирования. С момента публикации в 1991 году Unicode непрерывно расширялся, добавляя новые языки и символы. По состоянию на сентябрь 2022 года Unicode уже включал 149186 символов, в том числе буквы разных языков, знаки, а также эмодзи.</p>
<p>Как универсальный набор символов, Unicode по сути присваивает каждому символу уникальную «кодовую точку» (числовой идентификатор символа), диапазон которой составляет от U+0000 до U+10FFFF, образуя единое пространство нумерации символов. Однако <strong>Unicode не определяет, как именно хранить эти кодовые точки в компьютере</strong>. Тут неизбежно возникает вопрос: если в одном тексте одновременно встречаются кодовые точки Unicode разной длины, как система должна разбирать символы? Например, если дан код длиной 2 байта, как понять, является ли это одним 2-байтовым символом или двумя 1-байтовыми?</p>
<p>Для этой проблемы <strong>прямолинейное решение состоит в том, чтобы хранить все символы в кодировке одинаковой длины</strong>. Как показано на рисунке 3-7, каждый символ в «Hello» занимает 1 байт, а каждый символ в «алгоритм» занимает 2 байта. Мы можем дополнить старшие биты нулями и закодировать все символы в «Hello алгоритм» в виде 2-байтовых единиц. Тогда система сможет считывать по одному символу каждые 2 байта и восстановить эту фразу.</p>
<p><img alt="Пример кодирования Unicode" class="animation-figure" src="../character_encoding.assets/unicode_hello_algo.png" /></p>
<p align="center"> Рисунок 3-7 &nbsp; Пример кодирования Unicode </p>
@@ -4449,9 +4449,9 @@
<p>Правила кодирования UTF-8 не слишком сложны и делятся на два случая.</p>
<ul>
<li>Для символов длиной 1 байт старший бит устанавливается в <span class="arithmatex">\(0\)</span> , а оставшиеся 7 битов содержат кодовую точку Unicode. Стоит отметить, что символы ASCII занимают первые 128 кодовых точек в наборе Unicode. Иными словами, <strong>кодировка UTF-8 обратно совместима с ASCII</strong>. Это означает, что мы можем использовать UTF-8 для разбора очень старых ASCII-текстов.</li>
<li>Для символов длиной <span class="arithmatex">\(n\)</span> байт (где <span class="arithmatex">\(n &gt; 1\)</span>) старшие <span class="arithmatex">\(n\)</span> битов первого байта устанавливаются в <span class="arithmatex">\(1\)</span> , а <span class="arithmatex">\((n + 1)\)</span>-й бит устанавливается в <span class="arithmatex">\(0\)</span> ; начиная со второго байта, старшие 2 бита каждого байта устанавливаются в <span class="arithmatex">\(10\)</span> ; все остальные биты используются для заполнения кодовой точки Unicode соответствующего символа.</li>
<li>Для символов длиной <span class="arithmatex">\(n\)</span> байт (где <span class="arithmatex">\(n &gt; 1\)</span>) старшие <span class="arithmatex">\(n\)</span> битов первого байта устанавливаются в <span class="arithmatex">\(1\)</span> , а <span class="arithmatex">\((n + 1)\)</span>-й бит устанавливается в <span class="arithmatex">\(0\)</span>. Начиная со второго байта, старшие 2 бита каждого байта устанавливаются в <span class="arithmatex">\(10\)</span>. Все остальные биты используются для заполнения кодовой точки Unicode соответствующего символа.</li>
</ul>
<p>На рисунке 3-8 показана UTF-8-кодировка для строки "Hello алгоритм". Можно заметить, что поскольку старшие <span class="arithmatex">\(n\)</span> битов установлены в <span class="arithmatex">\(1\)</span> , система может определить длину символа как <span class="arithmatex">\(n\)</span> , подсчитав число ведущих единиц.</p>
<p>На рисунке 3-8 показана UTF-8-кодировка для строки «Hello алгоритм». Можно заметить, что поскольку старшие <span class="arithmatex">\(n\)</span> битов установлены в <span class="arithmatex">\(1\)</span> , система может определить длину символа как <span class="arithmatex">\(n\)</span> , подсчитав число ведущих единиц.</p>
<p>Но почему старшие 2 бита всех остальных байтов устанавливаются в <span class="arithmatex">\(10\)</span> ? На самом деле это <span class="arithmatex">\(10\)</span> играет роль контрольного маркера. Если система начнет разбирать текст с неверного байта, префикс <span class="arithmatex">\(10\)</span> поможет быстро обнаружить аномалию.</p>
<p>Причина выбора <span class="arithmatex">\(10\)</span> в качестве контрольного маркера в том, что по правилам UTF-8 символ не может иметь старшие два бита, равные <span class="arithmatex">\(10\)</span> . Это можно доказать от противного: если предположить, что у некоторого символа старшие два бита равны <span class="arithmatex">\(10\)</span> , то длина такого символа должна быть 1 байт, то есть это ASCII. Но у ASCII старший бит обязан быть <span class="arithmatex">\(0\)</span> , что противоречит предположению.</p>
<p><img alt="Пример кодировки UTF-8" class="animation-figure" src="../character_encoding.assets/utf-8_hello_algo.png" /></p>
@@ -4459,10 +4459,10 @@
<p>Помимо UTF-8, распространены еще два следующих способа кодирования.</p>
<ul>
<li><strong>Кодировка UTF-16</strong>: использует 2 или 4 байта для представления символа. Все символы ASCII и часто используемые неанглийские символы представляются 2 байтами; небольшая часть символов требует 4 байта. Для 2-байтовых символов кодировка UTF-16 совпадает с кодовой точкой Unicode.</li>
<li><strong>Кодировка UTF-16</strong>: использует 2 или 4 байта для представления символа. Все символы ASCII и часто используемые неанглийские символы представляются 2 байтами. Небольшая часть символов требует 4 байта. Для 2-байтовых символов кодировка UTF-16 совпадает с кодовой точкой Unicode.</li>
<li><strong>Кодировка UTF-32</strong>: каждый символ занимает 4 байта. Это означает, что UTF-32 требует больше места, чем UTF-8 и UTF-16, особенно в текстах с большой долей ASCII-символов.</li>
</ul>
<p>С точки зрения занимаемого места UTF-8 очень эффективна для английских символов, потому что им нужен всего 1 байт; а для некоторых неанглийских символов (например китайских) UTF-16 может быть эффективнее, потому что ей требуется только 2 байта, тогда как UTF-8 может потребовать 3 байта.</p>
<p>С точки зрения занимаемого места UTF-8 очень эффективна для английских символов, потому что им нужен всего 1 байт. А для некоторых неанглийских символов (например китайских) UTF-16 может быть эффективнее, потому что ей требуется только 2 байта, тогда как UTF-8 может потребовать 3 байта.</p>
<p>С точки зрения совместимости у UTF-8 наилучшая универсальность, и многие инструменты и библиотеки в первую очередь поддерживают именно UTF-8.</p>
<h2 id="345">3.4.5 &nbsp; Кодирование символов в языках программирования<a class="headerlink" href="#345" title="Permanent link">&para;</a></h2>
<p>Для большинства языков программирования прошлого строки во время выполнения программы использовали фиксированные по длине кодировки, такие как UTF-16 или UTF-32. При кодировке фиксированной длины строку можно обрабатывать как массив, и такой подход дает следующие преимущества.</p>
@@ -4473,14 +4473,14 @@
</ul>
<p>Вообще говоря, проектирование схем кодирования символов в языках программирования - очень интересная тема, в которой учитывается множество факторов.</p>
<ul>
<li>Тип <code>String</code> в Java использует кодировку UTF-16, и каждый символ занимает 2 байта. Это связано с тем, что на раннем этапе проектирования Java считалось, что 16 битов достаточно для представления всех возможных символов. Но это оказалось неверным предположением. Позднее Unicode вышел за пределы 16 битов, поэтому символы в Java теперь могут представляться парой 16-битных значений (так называемой "суррогатной парой").</li>
<li>Тип <code>String</code> в Java использует кодировку UTF-16, и каждый символ занимает 2 байта. Это связано с тем, что на раннем этапе проектирования Java считалось, что 16 битов достаточно для представления всех возможных символов. Но это оказалось неверным предположением. Позднее Unicode вышел за пределы 16 битов, поэтому символы в Java теперь могут представляться парой 16-битных значений (так называемой «суррогатной парой»).</li>
<li>Строки в JavaScript и TypeScript используют UTF-16 по причинам, похожим на Java. Когда Netscape впервые выпустила JavaScript в 1995 году, Unicode еще находился на ранней стадии развития, и 16-битного кодирования тогда было достаточно для представления всех символов Unicode.</li>
<li>C# использует UTF-16 главным образом потому, что платформа .NET была разработана Microsoft, а многие технологии Microsoft (включая Windows) широко используют именно UTF-16.</li>
</ul>
<p>Из-за недооценки общего числа символов перечисленным выше языкам пришлось использовать "суррогатные пары" для представления Unicode-символов длиной больше 16 бит. Это вынужденный компромисс. С одной стороны, в строках с суррогатными парами один символ может занимать 2 байта или 4 байта, из-за чего теряется преимущество кодировки фиксированной длины. С другой стороны, обработка суррогатных пар требует дополнительного кода, что повышает сложность разработки и отладки.</p>
<p>Из-за недооценки общего числа символов перечисленным выше языкам пришлось использовать «суррогатные пары» для представления Unicode-символов длиной больше 16 бит. Это вынужденный компромисс. С одной стороны, в строках с суррогатными парами один символ может занимать 2 байта или 4 байта, из-за чего теряется преимущество кодировки фиксированной длины. С другой стороны, обработка суррогатных пар требует дополнительного кода, что повышает сложность разработки и отладки.</p>
<p>По этим причинам некоторые языки программирования предложили иные схемы кодирования.</p>
<ul>
<li><code>str</code> в Python использует Unicode и гибкое строковое представление, где длина хранимого символа зависит от наибольшей кодовой точки Unicode в строке. Если все символы строки принадлежат ASCII, каждый символ занимает 1 байт; если есть символы за пределами ASCII, но все они лежат в базовой многоязычной плоскости (BMP), каждый символ занимает 2 байта; если встречаются символы за пределами BMP, каждый символ занимает 4 байта.</li>
<li><code>str</code> в Python использует Unicode и гибкое строковое представление, где длина хранимого символа зависит от наибольшей кодовой точки Unicode в строке. Если все символы строки принадлежат ASCII, каждый символ занимает 1 байт. Если есть символы за пределами ASCII, но все они лежат в базовой многоязычной плоскости (BMP), каждый символ занимает 2 байта. Если встречаются символы за пределами BMP, каждый символ занимает 4 байта.</li>
<li>Тип <code>string</code> в Go внутри использует кодировку UTF-8. Язык Go также предоставляет тип <code>rune</code>, предназначенный для представления одной кодовой точки Unicode.</li>
<li>Типы <code>str</code> и <code>String</code> в Rust внутри используют UTF-8. В Rust также есть тип <code>char</code>, представляющий одну кодовую точку Unicode.</li>
</ul>
@@ -4359,16 +4359,16 @@
<h1 id="31">3.1 &nbsp; Классификация структур данных<a class="headerlink" href="#31" title="Permanent link">&para;</a></h1>
<p>К распространенным структурам данных относятся массивы, связные списки, стеки, очереди, хеш-таблицы, деревья, кучи и графы. Их можно классифицировать по двум измерениям: логической структуре и физической структуре.</p>
<h2 id="311">3.1.1 &nbsp; Логическая структура: линейная и нелинейная<a class="headerlink" href="#311" title="Permanent link">&para;</a></h2>
<p><strong>Логическая структура раскрывает логические отношения между элементами данных</strong>. В массивах и связных списках данные расположены в определенном порядке, что отражает линейные отношения между элементами. В деревьях данные расположены по уровням сверху вниз, что демонстрирует отношения "предок" и "потомок". Графы состоят из вершин и ребер, отражая сложные сетевые отношения.</p>
<p><strong>Логическая структура раскрывает логические отношения между элементами данных</strong>. В массивах и связных списках данные расположены в определенном порядке, что отражает линейные отношения между элементами. В деревьях данные расположены по уровням сверху вниз, что демонстрирует отношения «предок» и «потомок». Графы состоят из вершин и ребер, отражая сложные сетевые отношения.</p>
<p>Как показано на рисунке 3-1, логические структуры делятся на две большие категории: линейные и нелинейные. Линейные структуры более интуитивны, поскольку в них данные расположены линейно и логически связаны. Нелинейные структуры, напротив, представляют собой нелинейное расположение элементов данных.</p>
<ul>
<li><strong>Линейные структуры данных</strong>: массивы, связные списки, стеки, очереди, хеш-таблицы, в которых элементы связаны отношением "один к одному".</li>
<li><strong>Линейные структуры данных</strong>: массивы, связные списки, стеки, очереди, хеш-таблицы, в которых элементы связаны отношением «один к одному».</li>
<li><strong>Нелинейные структуры данных</strong>: деревья, кучи, графы, хеш-таблицы.</li>
</ul>
<p>Нелинейные структуры данных можно дополнительно разделить на древовидные и сетевые.</p>
<ul>
<li><strong>Древовидные структуры</strong>: деревья, кучи, хеш-таблицы, в которых элементы связаны отношением "один ко многим".</li>
<li><strong>Сетевые структуры</strong>: графы, в которых элементы связаны отношением "многие ко многим".</li>
<li><strong>Древовидные структуры</strong>: деревья, кучи, хеш-таблицы, в которых элементы связаны отношением «один ко многим».</li>
<li><strong>Сетевые структуры</strong>: графы, в которых элементы связаны отношением «многие ко многим».</li>
</ul>
<p><img alt="Линейные и нелинейные структуры данных" class="animation-figure" src="../classification_of_data_structure.assets/classification_logic_structure.png" /></p>
<p align="center"> Рисунок 3-1 &nbsp; Линейные и нелинейные структуры данных </p>
@@ -4381,19 +4381,19 @@
<div class="admonition tip">
<p class="admonition-title">Tip</p>
<p>Стоит отметить, что сравнение памяти с таблицей Excel - это упрощенная аналогия; реальный механизм работы памяти гораздо сложнее и включает такие понятия, как адресное пространство, управление памятью, кэш-механизмы, виртуальная и физическая память.</p>
<p>Стоит отметить, что сравнение памяти с таблицей Excel - это упрощенная аналогия. Реальный механизм работы памяти гораздо сложнее и включает такие понятия, как адресное пространство, управление памятью, кэш-механизмы, виртуальная и физическая память.</p>
</div>
<p>Память - общий ресурс для всех программ. Когда некоторый участок памяти занят одной программой, другие программы обычно не могут использовать его одновременно. <strong>Поэтому при проектировании структур данных и алгоритмов память занимает важное место</strong>. Например, пиковое потребление памяти алгоритмом не должно превышать объем доступной свободной памяти системы; если не хватает непрерывных крупных участков памяти, выбранная структура данных должна уметь размещаться в разрозненных областях памяти.</p>
<p>Память - общий ресурс для всех программ. Когда некоторый участок памяти занят одной программой, другие программы обычно не могут использовать его одновременно. <strong>Поэтому при проектировании структур данных и алгоритмов память занимает важное место</strong>. Например, пиковое потребление памяти алгоритмом не должно превышать объем доступной свободной памяти системы. Если не хватает непрерывных крупных участков памяти, выбранная структура данных должна уметь размещаться в разрозненных областях памяти.</p>
<p>Как показано на рисунке 3-3, <strong>физическая структура отражает способ хранения данных в памяти компьютера</strong>. Ее можно разделить на хранение в непрерывном пространстве (массивы) и хранение в разрозненном пространстве (связные списки). Физическая структура на низком уровне определяет способы доступа к данным, их обновления, вставки и удаления. Эти два типа физических структур взаимно дополняют друг друга по временной и пространственной эффективности.</p>
<p><img alt="Хранение в непрерывном и разрозненном пространстве" class="animation-figure" src="../classification_of_data_structure.assets/classification_phisical_structure.png" /></p>
<p align="center"> Рисунок 3-3 &nbsp; Хранение в непрерывном и разрозненном пространстве </p>
<p>Стоит отметить, что <strong>все структуры данных реализуются на основе массивов, связных списков или их комбинации</strong>. Например, стек и очередь можно реализовать как с помощью массивов, так и с помощью связных списков; реализация хеш-таблицы также может одновременно включать массивы и связные списки.</p>
<p>Стоит отметить, что <strong>все структуры данных реализуются на основе массивов, связных списков или их комбинации</strong>. Например, стек и очередь можно реализовать как с помощью массивов, так и с помощью связных списков. Реализация хеш-таблицы также может одновременно включать массивы и связные списки.</p>
<ul>
<li><strong>Можно реализовать на основе массивов</strong>: стеки, очереди, хеш-таблицы, деревья, кучи, графы, матрицы, тензоры (массивы размерности <span class="arithmatex">\(\geq 3\)</span> ) и т.д.</li>
<li><strong>Можно реализовать на основе связных списков</strong>: стеки, очереди, хеш-таблицы, деревья, кучи, графы и т.д.</li>
</ul>
<p>После инициализации длину связного списка все еще можно изменять во время выполнения программы, поэтому его также называют "динамической структурой данных". Длина массива после инициализации неизменна, поэтому его также называют "статической структурой данных". Стоит отметить, что массив может изменять длину за счет повторного выделения памяти, тем самым приобретая определенную "динамичность".</p>
<p>После инициализации длину связного списка все еще можно изменять во время выполнения программы, поэтому его также называют «динамической структурой данных». Длина массива после инициализации неизменна, поэтому его также называют «статической структурой данных». Стоит отметить, что массив может изменять длину за счет повторного выделения памяти, тем самым приобретая определенную «динамичность».</p>
<div class="admonition tip">
<p class="admonition-title">Tip</p>
<p>Если тебе пока трудно понять физическую структуру, рекомендуется сначала прочитать следующую главу, а затем вернуться к этому разделу.</p>
@@ -4363,11 +4363,11 @@
</div>
<h2 id="331">3.3.1 &nbsp; Прямой, обратный и дополнительный коды<a class="headerlink" href="#331" title="Permanent link">&para;</a></h2>
<p>В таблице из предыдущего раздела можно заметить, что все целочисленные типы могут представлять на одно отрицательное число больше, чем положительных. Например, диапазон <code>byte</code> равен <span class="arithmatex">\([-128, 127]\)</span> . Это выглядит не слишком интуитивно, и внутренняя причина связана с прямым, обратным и дополнительным кодами.</p>
<p>Прежде всего нужно отметить, что <strong>числа хранятся в компьютере в виде "дополнительного кода"</strong>. Прежде чем разбирать причины такого решения, сначала дадим определения всем трем способам представления.</p>
<p>Прежде всего нужно отметить, что <strong>числа хранятся в компьютере в виде «дополнительного кода»</strong>. Прежде чем разбирать причины такого решения, сначала дадим определения всем трем способам представления.</p>
<ul>
<li><strong>Прямой код</strong>: старший бит двоичного представления числа рассматривается как знаковый, где <span class="arithmatex">\(0\)</span> означает положительное число, а <span class="arithmatex">\(1\)</span> - отрицательное; остальные биты представляют значение числа.</li>
<li><strong>Обратный код</strong>: для положительного числа обратный код совпадает с прямым; для отрицательного числа он получается инверсией всех битов прямого кода, кроме знакового бита.</li>
<li><strong>Дополнительный код</strong>: для положительного числа дополнительный код совпадает с прямым; для отрицательного числа он получается добавлением <span class="arithmatex">\(1\)</span> к его обратному коду.</li>
<li><strong>Прямой код</strong>: старший бит двоичного представления числа рассматривается как знаковый, где <span class="arithmatex">\(0\)</span> означает положительное число, а <span class="arithmatex">\(1\)</span> - отрицательное. Остальные биты представляют значение числа.</li>
<li><strong>Обратный код</strong>: для положительного числа обратный код совпадает с прямым. Для отрицательного числа он получается инверсией всех битов прямого кода, кроме знакового бита.</li>
<li><strong>Дополнительный код</strong>: для положительного числа дополнительный код совпадает с прямым. Для отрицательного числа он получается добавлением <span class="arithmatex">\(1\)</span> к его обратному коду.</li>
</ul>
<p>На рисунке 3-4 показаны способы преобразования между прямым, обратным и дополнительным кодами.</p>
<p><img alt="Преобразования между прямым, обратным и дополнительным кодами" class="animation-figure" src="../number_encoding.assets/1s_2s_complement.png" /></p>
@@ -4377,8 +4377,8 @@
<div class="arithmatex">\[
\begin{aligned}
&amp; 1 + (-2) \newline
&amp; \rightarrow 0000 \; 0001 + 1000 \; 0010 \newline
&amp; = 1000 \; 0011 \newline
&amp; \rightarrow 0000 \. 0001 + 1000 \. 0010 \newline
&amp; = 1000 \. 0011 \newline
&amp; \rightarrow -3
\end{aligned}
\]</div>
@@ -4386,45 +4386,45 @@
<div class="arithmatex">\[
\begin{aligned}
&amp; 1 + (-2) \newline
&amp; \rightarrow 0000 \; 0001 \; \text{(прямой код)} + 1000 \; 0010 \; \text{(прямой код)} \newline
&amp; = 0000 \; 0001 \; \text{(обратный код)} + 1111 \; 1101 \; \text{(обратный код)} \newline
&amp; = 1111 \; 1110 \; \text{(обратный код)} \newline
&amp; = 1000 \; 0001 \; \text{(прямой код)} \newline
&amp; \rightarrow 0000 \. 0001 \. \text{(прямой код)} + 1000 \. 0010 \. \text{(прямой код)} \newline
&amp; = 0000 \. 0001 \. \text{(обратный код)} + 1111 \. 1101 \. \text{(обратный код)} \newline
&amp; = 1111 \. 1110 \. \text{(обратный код)} \newline
&amp; = 1000 \. 0001 \. \text{(прямой код)} \newline
&amp; \rightarrow -1
\end{aligned}
\]</div>
<p>С другой стороны, **в прямом коде у нуля есть два представления: <span class="arithmatex">\(+0\)</span> и <span class="arithmatex">\(-0\)</span> **. Это означает, что числу ноль соответствуют два разных двоичных кода, что может приводить к неоднозначности. Например, если в условном выражении не различать положительный и отрицательный ноль, можно получить ошибочный результат. А если специально обрабатывать такую неоднозначность, придется вводить дополнительные проверки, что может снизить вычислительную эффективность компьютера.</p>
<div class="arithmatex">\[
\begin{aligned}
+0 &amp; \rightarrow 0000 \; 0000 \newline
-0 &amp; \rightarrow 1000 \; 0000
+0 &amp; \rightarrow 0000 \. 0000 \newline
-0 &amp; \rightarrow 1000 \. 0000
\end{aligned}
\]</div>
<p>Как и прямой код, обратный код тоже страдает от неоднозначности положительного и отрицательного нуля, поэтому компьютеры ввели <u>дополнительный код (2's complement)</u>. Сначала посмотрим на процесс преобразования отрицательного нуля из прямого кода в обратный, а затем в дополнительный:</p>
<div class="arithmatex">\[
\begin{aligned}
-0 \rightarrow \; &amp; 1000 \; 0000 \; \text{(прямой код)} \newline
= \; &amp; 1111 \; 1111 \; \text{(обратный код)} \newline
= 1 \; &amp; 0000 \; 0000 \; \text{(дополнительный код)} \newline
-0 \rightarrow \. &amp; 1000 \. 0000 \. \text{(прямой код)} \newline
= \. &amp; 1111 \. 1111 \. \text{(обратный код)} \newline
= 1 \. &amp; 0000 \. 0000 \. \text{(дополнительный код)} \newline
\end{aligned}
\]</div>
<p>При добавлении <span class="arithmatex">\(1\)</span> к обратному коду отрицательного нуля возникает перенос, но длина типа <code>byte</code> составляет всего 8 бит, поэтому переполнившаяся в 9-й бит единица отбрасывается. Иными словами, <strong>дополнительный код отрицательного нуля равен <span class="arithmatex">\(0000 \; 0000\)</span> и совпадает с дополнительным кодом положительного нуля</strong>. Значит, в представлении дополнительного кода существует только один ноль, и проблема неоднозначности положительного и отрицательного нуля тем самым устраняется.</p>
<p>При добавлении <span class="arithmatex">\(1\)</span> к обратному коду отрицательного нуля возникает перенос, но длина типа <code>byte</code> составляет всего 8 бит, поэтому переполнившаяся в 9-й бит единица отбрасывается. Иными словами, <strong>дополнительный код отрицательного нуля равен <span class="arithmatex">\(0000 \. 0000\)</span> и совпадает с дополнительным кодом положительного нуля</strong>. Значит, в представлении дополнительного кода существует только один ноль, и проблема неоднозначности положительного и отрицательного нуля тем самым устраняется.</p>
<p>Остается последний вопрос: диапазон типа <code>byte</code> равен <span class="arithmatex">\([-128, 127]\)</span> , откуда берется лишнее отрицательное число <span class="arithmatex">\(-128\)</span> ? Мы замечаем, что у всех целых чисел из интервала <span class="arithmatex">\([-127, +127]\)</span> есть соответствующие прямой, обратный и дополнительный коды, а прямой и дополнительный коды можно преобразовывать друг в друга.</p>
<p>Однако <strong>дополнительный код <span class="arithmatex">\(1000 \; 0000\)</span> является исключением: у него нет соответствующего прямого кода</strong>. Согласно правилу преобразования, прямой код для этого дополнительного кода должен быть равен <span class="arithmatex">\(0000 \; 0000\)</span> . Это очевидное противоречие, потому что такой прямой код обозначает число <span class="arithmatex">\(0\)</span> , а его дополнительный код должен совпадать с ним самим. Компьютер просто определяет, что этот особый дополнительный код <span class="arithmatex">\(1000 \; 0000\)</span> представляет число <span class="arithmatex">\(-128\)</span> . На самом деле результат вычисления <span class="arithmatex">\((-1) + (-127)\)</span> в дополнительном коде как раз и равен <span class="arithmatex">\(-128\)</span> .</p>
<p>Однако <strong>дополнительный код <span class="arithmatex">\(1000 \. 0000\)</span> является исключением: у него нет соответствующего прямого кода</strong>. Согласно правилу преобразования, прямой код для этого дополнительного кода должен быть равен <span class="arithmatex">\(0000 \. 0000\)</span> . Это очевидное противоречие, потому что такой прямой код обозначает число <span class="arithmatex">\(0\)</span> , а его дополнительный код должен совпадать с ним самим. Компьютер просто определяет, что этот особый дополнительный код <span class="arithmatex">\(1000 \. 0000\)</span> представляет число <span class="arithmatex">\(-128\)</span> . На самом деле результат вычисления <span class="arithmatex">\((-1) + (-127)\)</span> в дополнительном коде как раз и равен <span class="arithmatex">\(-128\)</span> .</p>
<div class="arithmatex">\[
\begin{aligned}
&amp; (-127) + (-1) \newline
&amp; \rightarrow 1111 \; 1111 \; \text{(прямой код)} + 1000 \; 0001 \; \text{(прямой код)} \newline
&amp; = 1000 \; 0000 \; \text{(обратный код)} + 1111 \; 1110 \; \text{(обратный код)} \newline
&amp; = 1000 \; 0001 \; \text{(дополнительный код)} + 1111 \; 1111 \; \text{(дополнительный код)} \newline
&amp; = 1000 \; 0000 \; \text{(дополнительный код)} \newline
&amp; \rightarrow 1111 \. 1111 \. \text{(прямой код)} + 1000 \. 0001 \. \text{(прямой код)} \newline
&amp; = 1000 \. 0000 \. \text{(обратный код)} + 1111 \. 1110 \. \text{(обратный код)} \newline
&amp; = 1000 \. 0001 \. \text{(дополнительный код)} + 1111 \. 1111 \. \text{(дополнительный код)} \newline
&amp; = 1000 \. 0000 \. \text{(дополнительный код)} \newline
&amp; \rightarrow -128
\end{aligned}
\]</div>
<p>Ты, вероятно, уже заметил, что все приведенные выше вычисления были операциями сложения. Это указывает на важный факт: <strong>аппаратные схемы внутри компьютера в основном проектируются на основе операций сложения</strong>. Причина в том, что сложение по сравнению с другими операциями (например умножением, делением и вычитанием) проще реализуется на аппаратном уровне, легче распараллеливается и выполняется быстрее.</p>
<p>Обрати внимание: это не означает, что компьютер умеет только складывать. <strong>Комбинируя сложение с некоторыми базовыми логическими операциями, компьютер может реализовать и другие математические операции</strong>. Например, вычитание <span class="arithmatex">\(a - b\)</span> можно преобразовать в сложение <span class="arithmatex">\(a + (-b)\)</span> ; умножение и деление можно свести к многократному сложению или вычитанию.</p>
<p>Обрати внимание: это не означает, что компьютер умеет только складывать. <strong>Комбинируя сложение с некоторыми базовыми логическими операциями, компьютер может реализовать и другие математические операции</strong>. Например, вычитание <span class="arithmatex">\(a - b\)</span> можно преобразовать в сложение <span class="arithmatex">\(a + (-b)\)</span>. Умножение и деление можно свести к многократному сложению или вычитанию.</p>
<p>Теперь можно подвести итог, почему компьютеры используют дополнительный код: с представлением в дополнительном коде компьютер может использовать одни и те же схемы и операции для сложения положительных и отрицательных чисел, без необходимости проектировать специальные аппаратные схемы для вычитания и без особой обработки неоднозначности положительного и отрицательного нуля. Это значительно упрощает аппаратную архитектуру и повышает эффективность вычислений.</p>
<p>Идея дополнительного кода очень изящна; из-за ограничений по объему мы на этом остановимся. Если тебе интересно, стоит изучить эту тему глубже.</p>
<p>Идея дополнительного кода очень изящна. Из-за ограничений по объему мы на этом остановимся. Если тебе интересно, стоит изучить эту тему глубже.</p>
<h2 id="332">3.3.2 &nbsp; Кодирование чисел с плавающей точкой<a class="headerlink" href="#332" title="Permanent link">&para;</a></h2>
<p>Внимательный читатель может заметить: <code>int</code> и <code>float</code> имеют одинаковую длину, по 4 байта , но почему диапазон значений у <code>float</code> намного больше, чем у <code>int</code> ? Это выглядит парадоксально, ведь <code>float</code> должен еще представлять дробные числа, а значит диапазон вроде бы должен быть меньше.</p>
<p>На самом деле <strong>это связано с тем, что число с плавающей точкой <code>float</code> использует другой способ представления</strong>. Обозначим двоичное число длиной 32 бита как:</p>
@@ -4455,12 +4455,12 @@ b_{31} b_{30} b_{29} \ldots b_2 b_1 b_0
<p><img alt="Пример вычисления float по стандарту IEEE 754" class="animation-figure" src="../number_encoding.assets/ieee_754_float.png" /></p>
<p align="center"> Рисунок 3-5 &nbsp; Пример вычисления float по стандарту IEEE 754 </p>
<p>Посмотрим на рисунок 3-5: если взять пример <span class="arithmatex">\(\mathrm{S} = 0\)</span> , <span class="arithmatex">\(\mathrm{E} = 124\)</span> , <span class="arithmatex">\(\mathrm{N} = 2^{-2} + 2^{-3} = 0.375\)</span> , то получим:</p>
<p>Как видно на рисунке 3-5, если взять пример <span class="arithmatex">\(\mathrm{S} = 0\)</span> , <span class="arithmatex">\(\mathrm{E} = 124\)</span> , <span class="arithmatex">\(\mathrm{N} = 2^{-2} + 2^{-3} = 0.375\)</span> , то получим:</p>
<div class="arithmatex">\[
\text { val } = (-1)^0 \times 2^{124 - 127} \times (1 + 0.375) = 0.171875
\]</div>
<p>Теперь мы можем ответить на исходный вопрос: <strong>в представлении <code>float</code> присутствуют биты экспоненты, поэтому его диапазон значений намного больше, чем у <code>int</code></strong>. Согласно приведенным выше вычислениям, максимально возможное положительное число для <code>float</code> равно <span class="arithmatex">\(2^{254 - 127} \times (2 - 2^{-23}) \approx 3.4 \times 10^{38}\)</span> ; если изменить бит знака, получим минимальное отрицательное число.</p>
<p><strong>Хотя число с плавающей точкой <code>float</code> расширяет диапазон значений, побочным эффектом становится потеря точности</strong>. Целочисленный тип <code>int</code> использует все 32 бита для представления числа, и числа распределены равномерно; а из-за существования битов экспоненты у <code>float</code> чем больше число, тем больше обычно становится разница между двумя соседними представимыми значениями.</p>
<p>Теперь мы можем ответить на исходный вопрос: <strong>в представлении <code>float</code> присутствуют биты экспоненты, поэтому его диапазон значений намного больше, чем у <code>int</code></strong>. Согласно приведенным выше вычислениям, максимально возможное положительное число для <code>float</code> равно <span class="arithmatex">\(2^{254 - 127} \times (2 - 2^{-23}) \approx 3.4 \times 10^{38}\)</span>. Если изменить бит знака, получим минимальное отрицательное число.</p>
<p><strong>Хотя число с плавающей точкой <code>float</code> расширяет диапазон значений, побочным эффектом становится потеря точности</strong>. Целочисленный тип <code>int</code> использует все 32 бита для представления числа, и числа распределены равномерно. А из-за существования битов экспоненты у <code>float</code> чем больше число, тем больше обычно становится разница между двумя соседними представимыми значениями.</p>
<p>Как показано в таблице 3-2, значения экспоненты <span class="arithmatex">\(\mathrm{E} = 0\)</span> и <span class="arithmatex">\(\mathrm{E} = 255\)</span> имеют специальный смысл и <strong>используются для представления нуля, бесконечности, <span class="arithmatex">\(\mathrm{NaN}\)</span> и т.д.</strong></p>
<p align="center"> Таблица 3-2 &nbsp; Значение поля экспоненты </p>
+14 -14
View File
@@ -4372,32 +4372,32 @@
</ul>
<h3 id="2-q-a">2. &nbsp; Q &amp; A<a class="headerlink" href="#2-q-a" title="Permanent link">&para;</a></h3>
<p><strong>Q</strong>: Почему хеш-таблица одновременно включает линейные и нелинейные структуры данных?</p>
<p>В основе хеш-таблицы лежит массив, а для разрешения коллизий мы можем использовать "цепочки адресации" (об этом будет рассказано в последующем разделе "Хеш-коллизии"): каждый бакет массива указывает на связный список, а если длина списка превышает некоторый порог, он может быть преобразован в дерево (обычно в красно-черное дерево).</p>
<p>В основе хеш-таблицы лежит массив, а для разрешения коллизий мы можем использовать «цепочки адресации» (об этом будет рассказано в последующем разделе «Хеш-коллизии»): каждый бакет массива указывает на связный список, а если длина списка превышает некоторый порог, он может быть преобразован в дерево (обычно в красно-черное дерево).</p>
<p>С точки зрения хранения данных в основе хеш-таблицы находится массив, где каждый слот бакета может содержать либо отдельное значение, либо связный список, либо дерево. Поэтому хеш-таблица действительно может одновременно включать линейные структуры данных (массивы, списки) и нелинейные структуры данных (деревья).</p>
<p><strong>Q</strong>: Длина типа <code>char</code> равна 1 байту?</p>
<p>Длина типа <code>char</code> определяется используемым в языке программирования способом кодирования. Например, Java, JavaScript, TypeScript и C# используют кодировку UTF-16 (для хранения кодовых точек Unicode), поэтому длина <code>char</code> у них равна 2 байтам.</p>
<p><strong>Q</strong>: Не является ли двусмысленным утверждение, что структуры данных, реализованные на основе массива, также называются "статическими структурами данных"? Ведь стек тоже поддерживает операции push и pop, а они явно "динамические".</p>
<p>Стек действительно может поддерживать динамические операции над данными, но сама структура данных при этом остается "статической" (ее длина неизменна). Хотя структуры на основе массива могут динамически добавлять и удалять элементы, их емкость фиксирована. Если количество данных превышает заранее выделенный размер, приходится создавать новый, более крупный массив и копировать в него содержимое старого.</p>
<p><strong>Q</strong>: При построении стека (очереди) его размер не задается явно, почему же его относят к "статическим структурам данных"?</p>
<p>В языках высокого уровня нам не нужно вручную задавать начальную емкость стека (очереди): это автоматически делает сама реализация класса. Например, начальная емкость <code>ArrayList</code> в Java обычно равна 10. Кроме того, автоматом реализуется и расширение емкости. Подробнее это рассматривается в последующем разделе о "списках".</p>
<p><strong>Q</strong>: Если метод преобразования из прямого кода в дополнительный - это "сначала инвертировать, затем прибавить 1", то обратное преобразование из дополнительного кода в прямой, по идее, должно быть обратной операцией "сначала вычесть 1, затем инвертировать". Почему же дополнительный код также можно перевести в прямой тем же способом "сначала инвертировать, затем прибавить 1"?</p>
<p>Это связано с тем, что взаимное преобразование прямого и дополнительного кодов по сути является вычислением "дополнения". Сначала дадим определение дополнения: если <span class="arithmatex">\(a + b = c\)</span> , то говорят, что <span class="arithmatex">\(a\)</span> является дополнением числа <span class="arithmatex">\(b\)</span> до <span class="arithmatex">\(c\)</span> ; аналогично, <span class="arithmatex">\(b\)</span> является дополнением числа <span class="arithmatex">\(a\)</span> до <span class="arithmatex">\(c\)</span> .</p>
<p>Для двоичного числа длины <span class="arithmatex">\(n = 4\)</span> со значением <span class="arithmatex">\(0010\)</span> , если рассматривать его как прямой код (не учитывая знаковый бит), то его дополнительный код получается правилом "сначала инвертировать, затем прибавить 1":</p>
<p><strong>Q</strong>: Не является ли двусмысленным утверждение, что структуры данных, реализованные на основе массива, также называются «статическими структурами данных»? Ведь стек тоже поддерживает операции push и pop, а они явно «динамические».</p>
<p>Стек действительно может поддерживать динамические операции над данными, но сама структура данных при этом остается «статической» (ее длина неизменна). Хотя структуры на основе массива могут динамически добавлять и удалять элементы, их емкость фиксирована. Если количество данных превышает заранее выделенный размер, приходится создавать новый, более крупный массив и копировать в него содержимое старого.</p>
<p><strong>Q</strong>: При построении стека (очереди) его размер не задается явно, почему же его относят к «статическим структурам данных»?</p>
<p>В языках высокого уровня нам не нужно вручную задавать начальную емкость стека (очереди): это автоматически делает сама реализация класса. Например, начальная емкость <code>ArrayList</code> в Java обычно равна 10. Кроме того, автоматом реализуется и расширение емкости. Подробнее это рассматривается в последующем разделе о «списках».</p>
<p><strong>Q</strong>: Если метод преобразования из прямого кода в дополнительный - это «сначала инвертировать, затем прибавить 1», то обратное преобразование из дополнительного кода в прямой, по идее, должно быть обратной операцией «сначала вычесть 1, затем инвертировать». Почему же дополнительный код также можно перевести в прямой тем же способом «сначала инвертировать, затем прибавить 1»?</p>
<p>Это связано с тем, что взаимное преобразование прямого и дополнительного кодов по сути является вычислением «дополнения». Сначала дадим определение дополнения: если <span class="arithmatex">\(a + b = c\)</span> , то говорят, что <span class="arithmatex">\(a\)</span> является дополнением числа <span class="arithmatex">\(b\)</span> до <span class="arithmatex">\(c\)</span>. Аналогично, <span class="arithmatex">\(b\)</span> является дополнением числа <span class="arithmatex">\(a\)</span> до <span class="arithmatex">\(c\)</span> .</p>
<p>Для двоичного числа длины <span class="arithmatex">\(n = 4\)</span> со значением <span class="arithmatex">\(0010\)</span> , если рассматривать его как прямой код (не учитывая знаковый бит), то его дополнительный код получается правилом «сначала инвертировать, затем прибавить 1»:</p>
<div class="arithmatex">\[
0010 \rightarrow 1101 \rightarrow 1110
\]</div>
<p>Мы видим, что сумма прямого и дополнительного кодов равна <span class="arithmatex">\(0010 + 1110 = 10000\)</span> , то есть дополнительный код <span class="arithmatex">\(1110\)</span> является "дополнением" прямого кода <span class="arithmatex">\(0010\)</span> до <span class="arithmatex">\(10000\)</span> . **Это означает, что описанная выше операция "сначала инвертировать, затем прибавить 1" на самом деле вычисляет дополнение до <span class="arithmatex">\(10000\)</span> **.</p>
<p>Тогда чему равно "дополнение" дополнительного кода <span class="arithmatex">\(1110\)</span> до <span class="arithmatex">\(10000\)</span> ? Мы снова можем получить его правилом "сначала инвертировать, затем прибавить 1":</p>
<p>Мы видим, что сумма прямого и дополнительного кодов равна <span class="arithmatex">\(0010 + 1110 = 10000\)</span> , то есть дополнительный код <span class="arithmatex">\(1110\)</span> является «дополнением» прямого кода <span class="arithmatex">\(0010\)</span> до <span class="arithmatex">\(10000\)</span> . **Это означает, что описанная выше операция «сначала инвертировать, затем прибавить 1» на самом деле вычисляет дополнение до <span class="arithmatex">\(10000\)</span> **.</p>
<p>Тогда чему равно «дополнение» дополнительного кода <span class="arithmatex">\(1110\)</span> до <span class="arithmatex">\(10000\)</span> ? Мы снова можем получить его правилом «сначала инвертировать, затем прибавить 1»:</p>
<div class="arithmatex">\[
1110 \rightarrow 0001 \rightarrow 0010
\]</div>
<p>Иначе говоря, прямой и дополнительный коды являются взаимными "дополнениями" друг друга до <span class="arithmatex">\(10000\)</span> , поэтому и "прямой код -&gt; дополнительный код", и "дополнительный код -&gt; прямой код" можно реализовать одной и той же операцией (сначала инвертировать, затем прибавить 1).</p>
<p>Разумеется, можно получить прямой код из дополнительного кода <span class="arithmatex">\(1110\)</span> и обратной операцией, то есть "сначала вычесть 1, затем инвертировать":</p>
<p>Иначе говоря, прямой и дополнительный коды являются взаимными «дополнениями» друг друга до <span class="arithmatex">\(10000\)</span> , поэтому и «прямой код -&gt; дополнительный код», и «дополнительный код -&gt; прямой код» можно реализовать одной и той же операцией (сначала инвертировать, затем прибавить 1).</p>
<p>Разумеется, можно получить прямой код из дополнительного кода <span class="arithmatex">\(1110\)</span> и обратной операцией, то есть «сначала вычесть 1, затем инвертировать»:</p>
<div class="arithmatex">\[
1110 \rightarrow 1101 \rightarrow 0010
\]</div>
<p>В итоге и "сначала инвертировать, затем прибавить 1", и "сначала вычесть 1, затем инвертировать" - это два эквивалентных способа вычисления дополнения до <span class="arithmatex">\(10000\)</span> .</p>
<p>По сути операция "инвертировать" сама по себе вычисляет дополнение до <span class="arithmatex">\(1111\)</span> (потому что всегда выполняется <code>прямой код + обратный код = 1111</code> ); а дополнительный код, получающийся после добавления 1 к обратному коду, и есть дополнение до <span class="arithmatex">\(10000\)</span> .</p>
<p>В итоге и «сначала инвертировать, затем прибавить 1», и «сначала вычесть 1, затем инвертировать» - это два эквивалентных способа вычисления дополнения до <span class="arithmatex">\(10000\)</span> .</p>
<p>По сути операция «инвертировать» сама по себе вычисляет дополнение до <span class="arithmatex">\(1111\)</span> (потому что всегда выполняется <code>прямой код + обратный код = 1111</code> ). А дополнительный код, получающийся после добавления 1 к обратному коду, и есть дополнение до <span class="arithmatex">\(10000\)</span> .</p>
<p>Приведенный выше пример использовал <span class="arithmatex">\(n = 4\)</span> , но его можно обобщить на двоичные числа любой длины.</p>
<!-- Source file information -->
@@ -3202,7 +3202,7 @@
<a href="#1" class="md-nav__link">
<span class="md-ellipsis">
1. &nbsp; Реализация двоичного поиска на основе "разделяй и властвуй"
1. &nbsp; Реализация двоичного поиска на основе «разделяй и властвуй»
</span>
</a>
@@ -4290,7 +4290,7 @@
<a href="#1" class="md-nav__link">
<span class="md-ellipsis">
1. &nbsp; Реализация двоичного поиска на основе "разделяй и властвуй"
1. &nbsp; Реализация двоичного поиска на основе «разделяй и властвуй»
</span>
</a>
@@ -4340,32 +4340,32 @@
<li><strong>Полный перебор</strong>: реализуется через обход структуры данных, временная сложность равна <span class="arithmatex">\(O(n)\)</span> .</li>
<li><strong>Адаптивный поиск</strong>: использует особую организацию данных или априорную информацию, временная сложность может достигать <span class="arithmatex">\(O(\log n)\)</span> и даже <span class="arithmatex">\(O(1)\)</span> .</li>
</ul>
<p>На практике <strong>алгоритмы поиска с временной сложностью <span class="arithmatex">\(O(\log n)\)</span> обычно реализуются на основе стратегии "разделяй и властвуй"</strong>, например двоичный поиск и деревья.</p>
<p>На практике <strong>алгоритмы поиска с временной сложностью <span class="arithmatex">\(O(\log n)\)</span> обычно реализуются на основе стратегии «разделяй и властвуй»</strong>, например двоичный поиск и деревья.</p>
<ul>
<li>На каждом шаге двоичный поиск раскладывает задачу (поиск целевого элемента в массиве) на более мелкую задачу (поиск целевого элемента в одной половине массива), и этот процесс продолжается, пока массив не станет пустым или пока не будет найден целевой элемент.</li>
<li>Деревья являются типичными представителями идей "разделяй и властвуй"; в таких структурах данных, как двоичное дерево поиска, AVL-дерево и куча, временная сложность различных операций равна <span class="arithmatex">\(O(\log n)\)</span> .</li>
<li>Деревья являются типичными представителями идей «разделяй и властвуй». В таких структурах данных, как двоичное дерево поиска, AVL-дерево и куча, временная сложность различных операций равна <span class="arithmatex">\(O(\log n)\)</span> .</li>
</ul>
<p>Стратегия "разделяй и властвуй" для двоичного поиска выглядит следующим образом.</p>
<p>Стратегия «разделяй и властвуй» для двоичного поиска выглядит следующим образом.</p>
<ul>
<li><strong>Задача раскладывается на части</strong>: двоичный поиск рекурсивно разбивает исходную задачу (поиск в массиве) на подзадачу (поиск в одной половине массива), и это достигается сравнением среднего элемента с целевым значением.</li>
<li><strong>Подзадачи независимы</strong>: в двоичном поиске на каждом шаге обрабатывается только одна подзадача, и она не зависит от других подзадач.</li>
<li><strong>Решения подзадач не нужно объединять</strong>: двоичный поиск нацелен на поиск конкретного элемента, поэтому объединять решения подзадач не требуется. Как только подзадача решена, одновременно считается решенной и исходная задача.</li>
</ul>
<p>Иными словами, стратегия "разделяй и властвуй" повышает эффективность поиска потому, что при полном переборе за один шаг удается исключить только один вариант, <strong>тогда как при поиске на основе "разделяй и властвуй" за один шаг можно исключить половину вариантов</strong>.</p>
<h3 id="1">1. &nbsp; Реализация двоичного поиска на основе "разделяй и властвуй"<a class="headerlink" href="#1" title="Permanent link">&para;</a></h3>
<p>В предыдущих главах двоичный поиск реализовывался через итерацию. Теперь реализуем его с помощью стратегии "разделяй и властвуй", то есть через рекурсию.</p>
<p>Иными словами, стратегия «разделяй и властвуй» повышает эффективность поиска потому, что при полном переборе за один шаг удается исключить только один вариант, <strong>тогда как при поиске на основе «разделяй и властвуй» за один шаг можно исключить половину вариантов</strong>.</p>
<h3 id="1">1. &nbsp; Реализация двоичного поиска на основе «разделяй и властвуй»<a class="headerlink" href="#1" title="Permanent link">&para;</a></h3>
<p>В предыдущих главах двоичный поиск реализовывался через итерацию. Теперь реализуем его с помощью стратегии «разделяй и властвуй», то есть через рекурсию.</p>
<div class="admonition question">
<p class="admonition-title">Question</p>
<p>Дан отсортированный массив <code>nums</code> длины <span class="arithmatex">\(n\)</span> , в котором все элементы уникальны. Найдите элемент <code>target</code> .</p>
</div>
<p>С точки зрения стратегии "разделяй и властвуй" обозначим подзадачу, соответствующую интервалу поиска <span class="arithmatex">\([i, j]\)</span> , через <span class="arithmatex">\(f(i, j)\)</span> .</p>
<p>С точки зрения стратегии «разделяй и властвуй» обозначим подзадачу, соответствующую интервалу поиска <span class="arithmatex">\([i, j]\)</span> , через <span class="arithmatex">\(f(i, j)\)</span> .</p>
<p>Начиная с исходной задачи <span class="arithmatex">\(f(0, n-1)\)</span> , выполняем двоичный поиск по следующим шагам.</p>
<ol>
<li>Вычислить середину <span class="arithmatex">\(m\)</span> интервала поиска <span class="arithmatex">\([i, j]\)</span> и с ее помощью исключить половину интервала.</li>
<li>Рекурсивно решить подзадачу вдвое меньшего размера; это может быть либо <span class="arithmatex">\(f(i, m-1)\)</span> , либо <span class="arithmatex">\(f(m+1, j)\)</span> .</li>
<li>Рекурсивно решить подзадачу вдвое меньшего размера. Это может быть либо <span class="arithmatex">\(f(i, m-1)\)</span> , либо <span class="arithmatex">\(f(m+1, j)\)</span> .</li>
<li>Повторять шаг <code>1.</code> и шаг <code>2.</code> , пока не будет найден <code>target</code> или пока интервал не станет пустым.</li>
</ol>
<p>На рисунке 12-4 показан процесс применения стратегии "разделяй и властвуй" для поиска элемента <span class="arithmatex">\(6\)</span> в массиве.</p>
<p>На рисунке 12-4 показан процесс применения стратегии «разделяй и властвуй» для поиска элемента <span class="arithmatex">\(6\)</span> в массиве.</p>
<p><img alt="Процесс двоичного поиска в стиле разделяй и властвуй" class="animation-figure" src="../binary_search_recur.assets/binary_search_recur.png" /></p>
<p align="center"> Рисунок 12-4 &nbsp; Процесс двоичного поиска в стиле разделяй и властвуй </p>
File diff suppressed because one or more lines are too long
@@ -3174,7 +3174,7 @@
<a href="#1211" class="md-nav__link">
<span class="md-ellipsis">
12.1.1 &nbsp; Как определить задачу "разделяй и властвуй"
12.1.1 &nbsp; Как определить задачу «разделяй и властвуй»
</span>
</a>
@@ -3185,12 +3185,12 @@
<a href="#1212" class="md-nav__link">
<span class="md-ellipsis">
12.1.2 &nbsp; Повышение эффективности с помощью "разделяй и властвуй"
12.1.2 &nbsp; Повышение эффективности с помощью «разделяй и властвуй»
</span>
</a>
<nav class="md-nav" aria-label="12.1.2 Повышение эффективности с помощью &quot;разделяй и властвуй&quot;">
<nav class="md-nav" aria-label="12.1.2 Повышение эффективности с помощью «разделяй и властвуй»">
<ul class="md-nav__list">
<li class="md-nav__item">
@@ -3224,7 +3224,7 @@
<a href="#1213" class="md-nav__link">
<span class="md-ellipsis">
12.1.3 &nbsp; Типичные применения стратегии "разделяй и властвуй"
12.1.3 &nbsp; Типичные применения стратегии «разделяй и властвуй»
</span>
</a>
@@ -4340,7 +4340,7 @@
<a href="#1211" class="md-nav__link">
<span class="md-ellipsis">
12.1.1 &nbsp; Как определить задачу "разделяй и властвуй"
12.1.1 &nbsp; Как определить задачу «разделяй и властвуй»
</span>
</a>
@@ -4351,12 +4351,12 @@
<a href="#1212" class="md-nav__link">
<span class="md-ellipsis">
12.1.2 &nbsp; Повышение эффективности с помощью "разделяй и властвуй"
12.1.2 &nbsp; Повышение эффективности с помощью «разделяй и властвуй»
</span>
</a>
<nav class="md-nav" aria-label="12.1.2 Повышение эффективности с помощью &quot;разделяй и властвуй&quot;">
<nav class="md-nav" aria-label="12.1.2 Повышение эффективности с помощью «разделяй и властвуй»">
<ul class="md-nav__list">
<li class="md-nav__item">
@@ -4390,7 +4390,7 @@
<a href="#1213" class="md-nav__link">
<span class="md-ellipsis">
12.1.3 &nbsp; Типичные применения стратегии "разделяй и властвуй"
12.1.3 &nbsp; Типичные применения стратегии «разделяй и властвуй»
</span>
</a>
@@ -4435,12 +4435,12 @@
<!-- Page content -->
<h1 id="121">12.1 &nbsp; Стратегия разделяй и властвуй<a class="headerlink" href="#121" title="Permanent link">&para;</a></h1>
<p><u>Разделяй и властвуй (divide and conquer)</u> - это очень важная и широко используемая стратегия построения алгоритмов. Обычно она реализуется через рекурсию и включает два этапа: "разделение" и "объединение".</p>
<p><u>Разделяй и властвуй (divide and conquer)</u> - это очень важная и широко используемая стратегия построения алгоритмов. Обычно она реализуется через рекурсию и включает два этапа: «разделение» и «объединение».</p>
<ol>
<li><strong>Разделение (этап декомпозиции)</strong>: рекурсивно разбить исходную задачу на две или более подзадачи, пока не будет достигнута наименьшая подзадача.</li>
<li><strong>Объединение (этап синтеза)</strong>: начиная с уже известных решений наименьших подзадач, снизу вверх объединять решения подзадач и тем самым получать решение исходной задачи.</li>
</ol>
<p>Как показано на рисунке 12-1, "сортировка слиянием" является одним из типичных примеров применения стратегии "разделяй и властвуй".</p>
<p>Как показано на рисунке 12-1, «сортировка слиянием» является одним из типичных примеров применения стратегии «разделяй и властвуй».</p>
<ol>
<li><strong>Разделение</strong>: рекурсивно разделить исходный массив (исходную задачу) на два подмассива (подзадачи), пока в подмассиве не останется только один элемент (наименьшая подзадача).</li>
<li><strong>Объединение</strong>: снизу вверх объединять упорядоченные подмассивы (решения подзадач), чтобы получить упорядоченный исходный массив (решение исходной задачи).</li>
@@ -4448,8 +4448,8 @@
<p><img alt="Стратегия разделяй и властвуй в сортировке слиянием" class="animation-figure" src="../divide_and_conquer.assets/divide_and_conquer_merge_sort.png" /></p>
<p align="center"> Рисунок 12-1 &nbsp; Стратегия разделяй и властвуй в сортировке слиянием </p>
<h2 id="1211">12.1.1 &nbsp; Как определить задачу "разделяй и властвуй"<a class="headerlink" href="#1211" title="Permanent link">&para;</a></h2>
<p>Чтобы понять, подходит ли задача для решения методом "разделяй и властвуй", обычно можно ориентироваться на следующие критерии.</p>
<h2 id="1211">12.1.1 &nbsp; Как определить задачу «разделяй и властвуй»<a class="headerlink" href="#1211" title="Permanent link">&para;</a></h2>
<p>Чтобы понять, подходит ли задача для решения методом «разделяй и властвуй», обычно можно ориентироваться на следующие критерии.</p>
<ol>
<li><strong>Задача раскладывается на части</strong>: исходную задачу можно разбить на более мелкие и похожие подзадачи, причем такое разбиение можно применять рекурсивно.</li>
<li><strong>Подзадачи независимы</strong>: подзадачи не пересекаются, не зависят друг от друга и могут решаться независимо.</li>
@@ -4461,11 +4461,11 @@
<li><strong>Подзадачи независимы</strong>: каждый подмассив можно сортировать отдельно (то есть каждую подзадачу можно решать независимо).</li>
<li><strong>Решения подзадач можно объединить</strong>: два упорядоченных подмассива (решения подзадач) можно объединить в один упорядоченный массив (решение исходной задачи).</li>
</ol>
<h2 id="1212">12.1.2 &nbsp; Повышение эффективности с помощью "разделяй и властвуй"<a class="headerlink" href="#1212" title="Permanent link">&para;</a></h2>
<p><strong>Стратегия "разделяй и властвуй" не только позволяет эффективно решать алгоритмические задачи, но и часто повышает эффективность самих алгоритмов</strong>. Именно поэтому быстрая сортировка, сортировка слиянием и пирамидальная сортировка обычно работают быстрее, чем сортировка выбором, пузырьком и вставками.</p>
<p>Тогда возникает естественный вопрос: <strong>почему стратегия "разделяй и властвуй" повышает эффективность алгоритма и какова внутренняя логика этого подхода</strong>? Иными словами, почему разбиение большой задачи на несколько подзадач, решение этих подзадач и последующее объединение их решений оказывается эффективнее, чем прямое решение исходной задачи? Этот вопрос можно рассмотреть с двух сторон: через число операций и через параллельные вычисления.</p>
<h2 id="1212">12.1.2 &nbsp; Повышение эффективности с помощью «разделяй и властвуй»<a class="headerlink" href="#1212" title="Permanent link">&para;</a></h2>
<p><strong>Стратегия «разделяй и властвуй» не только позволяет эффективно решать алгоритмические задачи, но и часто повышает эффективность самих алгоритмов</strong>. Именно поэтому быстрая сортировка, сортировка слиянием и пирамидальная сортировка обычно работают быстрее, чем сортировка выбором, пузырьком и вставками.</p>
<p>Тогда возникает естественный вопрос: <strong>почему стратегия «разделяй и властвуй» повышает эффективность алгоритма и какова внутренняя логика этого подхода</strong>? Иными словами, почему разбиение большой задачи на несколько подзадач, решение этих подзадач и последующее объединение их решений оказывается эффективнее, чем прямое решение исходной задачи? Этот вопрос можно рассмотреть с двух сторон: через число операций и через параллельные вычисления.</p>
<h3 id="1">1. &nbsp; Оптимизация числа операций<a class="headerlink" href="#1" title="Permanent link">&para;</a></h3>
<p>Рассмотрим "сортировку пузырьком": для массива длины <span class="arithmatex">\(n\)</span> ей требуется <span class="arithmatex">\(O(n^2)\)</span> времени. Предположим, что мы разделим массив на два подмассива в середине, как показано на рисунке 12-2. Тогда само разбиение потребует <span class="arithmatex">\(O(n)\)</span> времени, сортировка каждого подмассива займет <span class="arithmatex">\(O((n / 2)^2)\)</span> времени, а объединение двух подмассивов потребует еще <span class="arithmatex">\(O(n)\)</span> времени. Общая временная сложность будет равна:</p>
<p>Рассмотрим «сортировку пузырьком»: для массива длины <span class="arithmatex">\(n\)</span> ей требуется <span class="arithmatex">\(O(n^2)\)</span> времени. Предположим, что мы разделим массив на два подмассива в середине, как показано на рисунке 12-2. Тогда само разбиение потребует <span class="arithmatex">\(O(n)\)</span> времени, сортировка каждого подмассива займет <span class="arithmatex">\(O((n / 2)^2)\)</span> времени, а объединение двух подмассивов потребует еще <span class="arithmatex">\(O(n)\)</span> времени. Общая временная сложность будет равна:</p>
<div class="arithmatex">\[
O(n + (\frac{n}{2})^2 \times 2 + n) = O(\frac{n^2}{2} + 2n)
\]</div>
@@ -4480,36 +4480,36 @@ n^2 - \frac{n^2}{2} - 2n &amp; &gt; 0 \newline
n(n - 4) &amp; &gt; 0
\end{aligned}
\]</div>
<p><strong>Это означает, что при <span class="arithmatex">\(n &gt; 4\)</span> число операций после разбиения становится меньше, а значит, сортировка должна работать быстрее</strong>. При этом важно заметить, что временная сложность после разбиения все еще остается квадратичной, то есть <span class="arithmatex">\(O(n^2)\)</span> ; уменьшается лишь константный множитель.</p>
<p>Если пойти дальше и <strong>продолжать делить каждый подмассив пополам</strong>, пока в нем не останется только один элемент, то мы фактически получим "сортировку слиянием", чья временная сложность равна <span class="arithmatex">\(O(n \log n)\)</span> .</p>
<p><strong>Это означает, что при <span class="arithmatex">\(n &gt; 4\)</span> число операций после разбиения становится меньше, а значит, сортировка должна работать быстрее</strong>. При этом важно заметить, что временная сложность после разбиения все еще остается квадратичной, то есть <span class="arithmatex">\(O(n^2)\)</span>. Уменьшается лишь константный множитель.</p>
<p>Если пойти дальше и <strong>продолжать делить каждый подмассив пополам</strong>, пока в нем не останется только один элемент, то мы фактически получим «сортировку слиянием», чья временная сложность равна <span class="arithmatex">\(O(n \log n)\)</span> .</p>
<p>Можно пойти еще дальше и спросить: <strong>что если задать несколько точек разделения</strong> и равномерно разбить исходный массив на <span class="arithmatex">\(k\)</span> подмассивов? Такая ситуация очень похожа на блочную сортировку, которая особенно хорошо подходит для сортировки очень больших объемов данных и теоретически может достигать временной сложности <span class="arithmatex">\(O(n + k)\)</span> .</p>
<h3 id="2">2. &nbsp; Оптимизация параллельных вычислений<a class="headerlink" href="#2" title="Permanent link">&para;</a></h3>
<p>Мы знаем, что подзадачи, порождаемые стратегией "разделяй и властвуй", являются независимыми, <strong>а значит, их обычно можно решать параллельно</strong>. Иначе говоря, "разделяй и властвуй" не только может уменьшить временную сложность алгоритма, <strong>но и хорошо сочетается с параллельной оптимизацией на уровне системы</strong>.</p>
<p>Мы знаем, что подзадачи, порождаемые стратегией «разделяй и властвуй», являются независимыми, <strong>а значит, их обычно можно решать параллельно</strong>. Иначе говоря, «разделяй и властвуй» не только может уменьшить временную сложность алгоритма, <strong>но и хорошо сочетается с параллельной оптимизацией на уровне системы</strong>.</p>
<p>Параллельная оптимизация особенно эффективна в среде с несколькими ядрами или несколькими процессорами, потому что система может одновременно обрабатывать разные подзадачи, лучше загружая вычислительные ресурсы и тем самым заметно сокращая общее время работы.</p>
<p>Например, в показанной ниже "блочной сортировке" большой объем данных равномерно распределяется по блокам. Тогда сортировку каждого блока можно поручить отдельным вычислительным единицам, а после завершения просто объединить результаты.</p>
<p>Например, в «блочной сортировке», показанной на рисунке 12-3, большой объем данных равномерно распределяется по блокам. Тогда сортировку каждого блока можно поручить отдельным вычислительным единицам, а после завершения просто объединить результаты.</p>
<p><img alt="Параллельные вычисления в блочной сортировке" class="animation-figure" src="../divide_and_conquer.assets/divide_and_conquer_parallel_computing.png" /></p>
<p align="center"> Рисунок 12-3 &nbsp; Параллельные вычисления в блочной сортировке </p>
<h2 id="1213">12.1.3 &nbsp; Типичные применения стратегии "разделяй и властвуй"<a class="headerlink" href="#1213" title="Permanent link">&para;</a></h2>
<p>С одной стороны, стратегию "разделяй и властвуй" можно использовать для решения многих классических алгоритмических задач.</p>
<h2 id="1213">12.1.3 &nbsp; Типичные применения стратегии «разделяй и властвуй»<a class="headerlink" href="#1213" title="Permanent link">&para;</a></h2>
<p>С одной стороны, стратегию «разделяй и властвуй» можно использовать для решения многих классических алгоритмических задач.</p>
<ul>
<li><strong>Поиск ближайшей пары точек</strong>: сначала множество точек делится на две части, затем ищется ближайшая пара в каждой части, а затем ближайшая пара, пересекающая границу между двумя частями.</li>
<li><strong>Умножение больших чисел</strong>: например, алгоритм Карацубы, который раскладывает умножение больших чисел на несколько умножений и сложений меньших чисел.</li>
<li><strong>Умножение матриц</strong>: например, алгоритм Штрассена, который раскладывает умножение больших матриц на несколько умножений и сложений матриц меньшего размера.</li>
<li><strong>Задача о Ханойской башне</strong>: задача о Ханойской башне решается рекурсивно и является типичным примером применения стратегии "разделяй и властвуй".</li>
<li><strong>Подсчет инверсий</strong>: если в последовательности предыдущее число больше следующего, то такая пара образует инверсию. Эту задачу можно решить с помощью идей "разделяй и властвуй", опираясь на сортировку слиянием.</li>
<li><strong>Задача о Ханойской башне</strong>: задача о Ханойской башне решается рекурсивно и является типичным примером применения стратегии «разделяй и властвуй».</li>
<li><strong>Подсчет инверсий</strong>: если в последовательности предыдущее число больше следующего, то такая пара образует инверсию. Эту задачу можно решить с помощью идей «разделяй и властвуй», опираясь на сортировку слиянием.</li>
</ul>
<p>С другой стороны, стратегия "разделяй и властвуй" очень широко применяется при проектировании алгоритмов и структур данных.</p>
<p>С другой стороны, стратегия «разделяй и властвуй» очень широко применяется при проектировании алгоритмов и структур данных.</p>
<ul>
<li><strong>Двоичный поиск</strong>: двоичный поиск делит отсортированный массив на две части по индексу середины, а затем, в зависимости от результата сравнения целевого значения со средним элементом, исключает одну из половин и повторяет ту же операцию на оставшемся интервале.</li>
<li><strong>Сортировка слиянием</strong>: она уже была рассмотрена в начале этого раздела, поэтому не будем повторяться.</li>
<li><strong>Быстрая сортировка</strong>: в ней выбирается опорное значение, после чего массив делится на два подмассива: один содержит элементы меньше опорного, а другой - больше. Затем такая же операция повторяется для обеих частей, пока в подмассиве не останется один элемент.</li>
<li><strong>Блочная сортировка</strong>: ее основная идея заключается в распределении данных по нескольким блокам, сортировке элементов внутри каждого блока и последующем последовательном извлечении элементов из блоков для построения отсортированного массива.</li>
<li><strong>Деревья</strong>: например, двоичные деревья поиска, AVL-деревья, красно-черные деревья, B-деревья, B+ деревья и т.д. Их операции поиска, вставки и удаления можно рассматривать как применение стратегии "разделяй и властвуй".</li>
<li><strong>Кучи</strong>: куча является особым видом полного двоичного дерева, а такие операции, как вставка, удаление и упорядочивание, по сути содержат идеи "разделяй и властвуй".</li>
<li><strong>Хеш-таблицы</strong>: хотя хеш-таблицы напрямую не используют стратегию "разделяй и властвуй", некоторые способы разрешения коллизий косвенно опираются на эту идею. Например, длинные цепочки в методе цепочек могут преобразовываться в красно-черные деревья для повышения эффективности поиска.</li>
<li><strong>Деревья</strong>: например, двоичные деревья поиска, AVL-деревья, красно-черные деревья, B-деревья, B+ деревья и т.д. Их операции поиска, вставки и удаления можно рассматривать как применение стратегии «разделяй и властвуй».</li>
<li><strong>Кучи</strong>: куча является особым видом полного двоичного дерева, а такие операции, как вставка, удаление и упорядочивание, по сути содержат идеи «разделяй и властвуй».</li>
<li><strong>Хеш-таблицы</strong>: хотя хеш-таблицы напрямую не используют стратегию «разделяй и властвуй», некоторые способы разрешения коллизий косвенно опираются на эту идею. Например, длинные цепочки в методе цепочек могут преобразовываться в красно-черные деревья для повышения эффективности поиска.</li>
</ul>
<p>Нетрудно заметить, что <strong>"разделяй и властвуй" - это "тихая" алгоритмическая идея</strong>, скрыто присутствующая внутри самых разных алгоритмов и структур данных.</p>
<p>Нетрудно заметить, что <strong>«разделяй и властвуй» - это «тихая» алгоритмическая идея</strong>, скрыто присутствующая внутри самых разных алгоритмов и структур данных.</p>
<!-- Source file information -->
@@ -4434,7 +4434,7 @@
<p>Процесс решения задачи <span class="arithmatex">\(f(2)\)</span> можно кратко описать так: <strong>переместить два диска с <code>A</code> на <code>C</code> с помощью <code>B</code></strong> . Здесь <code>C</code> называется целевым стержнем, а <code>B</code> - буферным стержнем.</p>
<h3 id="2">2. &nbsp; Разбиение на подзадачи<a class="headerlink" href="#2" title="Permanent link">&para;</a></h3>
<p>Для задачи <span class="arithmatex">\(f(3)\)</span> , то есть когда имеется три диска, ситуация становится сложнее.</p>
<p>Поскольку решения <span class="arithmatex">\(f(1)\)</span> и <span class="arithmatex">\(f(2)\)</span> уже известны, можно подойти к задаче с точки зрения стратегии "разделяй и властвуй" и <strong>рассматривать два верхних диска на <code>A</code> как единое целое</strong>, выполняя шаги, показанные на рисунке 12-13. Так три диска успешно перемещаются с <code>A</code> на <code>C</code> .</p>
<p>Поскольку решения <span class="arithmatex">\(f(1)\)</span> и <span class="arithmatex">\(f(2)\)</span> уже известны, можно подойти к задаче с точки зрения стратегии «разделяй и властвуй» и <strong>рассматривать два верхних диска на <code>A</code> как единое целое</strong>, выполняя шаги, показанные на рисунке 12-13. Так три диска успешно перемещаются с <code>A</code> на <code>C</code> .</p>
<ol>
<li>Сделать <code>B</code> целевым стержнем, а <code>C</code> буферным, и переместить два диска с <code>A</code> на <code>B</code> .</li>
<li>Переместить оставшийся один диск с <code>A</code> напрямую на <code>C</code> .</li>
@@ -4459,7 +4459,7 @@
<p align="center"> Рисунок 12-13 &nbsp; Решение задачи размера 3 </p>
<p>Иначе говоря, <strong>мы разбиваем задачу <span class="arithmatex">\(f(3)\)</span> на две подзадачи <span class="arithmatex">\(f(2)\)</span> и одну подзадачу <span class="arithmatex">\(f(1)\)</span></strong> . Если последовательно решить эти три подзадачи, исходная задача тоже будет решена. Это показывает, что подзадачи независимы и что их решения можно объединить.</p>
<p>Таким образом, можно сформулировать показанную на рисунке 12-14 стратегию "разделяй и властвуй" для задачи о Ханойской башне: исходная задача <span class="arithmatex">\(f(n)\)</span> разбивается на две подзадачи <span class="arithmatex">\(f(n-1)\)</span> и одну подзадачу <span class="arithmatex">\(f(1)\)</span> , которые затем решаются в следующем порядке.</p>
<p>Таким образом, можно сформулировать показанную на рисунке 12-14 стратегию «разделяй и властвуй» для задачи о Ханойской башне: исходная задача <span class="arithmatex">\(f(n)\)</span> разбивается на две подзадачи <span class="arithmatex">\(f(n-1)\)</span> и одну подзадачу <span class="arithmatex">\(f(1)\)</span> , которые затем решаются в следующем порядке.</p>
<ol>
<li>Переместить <span class="arithmatex">\(n-1\)</span> дисков с <code>A</code> на <code>B</code> с помощью <code>C</code> .</li>
<li>Переместить оставшийся <span class="arithmatex">\(1\)</span> диск напрямую с <code>A</code> на <code>C</code> .</li>
@@ -4900,7 +4900,7 @@
<p><div style="height: 549px; width: 100%;"><iframe class="pythontutor-iframe" src="https://pythontutor.com/iframe-embed.html#code=def%20move%28src%3A%20list%5Bint%5D%2C%20tar%3A%20list%5Bint%5D%29%3A%0A%20%20%20%20%22%22%22%D0%9F%D0%B5%D1%80%D0%B5%D0%BC%D0%B5%D1%81%D1%82%D0%B8%D1%82%D1%8C%20%D0%BE%D0%B4%D0%B8%D0%BD%20%D0%B4%D0%B8%D1%81%D0%BA%22%22%22%0A%20%20%20%20%23%20%D0%A1%D0%BD%D1%8F%D1%82%D1%8C%20%D0%B4%D0%B8%D1%81%D0%BA%20%D1%81%20%D0%B2%D0%B5%D1%80%D1%88%D0%B8%D0%BD%D1%8B%20src%0A%20%20%20%20pan%20%3D%20src.pop%28%29%0A%20%20%20%20%23%20%D0%9F%D0%BE%D0%BB%D0%BE%D0%B6%D0%B8%D1%82%D1%8C%20%D0%B4%D0%B8%D1%81%D0%BA%20%D0%BD%D0%B0%20%D0%B2%D0%B5%D1%80%D1%88%D0%B8%D0%BD%D1%83%20tar%0A%20%20%20%20tar.append%28pan%29%0A%0A%0Adef%20dfs%28i%3A%20int%2C%20src%3A%20list%5Bint%5D%2C%20buf%3A%20list%5Bint%5D%2C%20tar%3A%20list%5Bint%5D%29%3A%0A%20%20%20%20%22%22%22%D0%A0%D0%B5%D1%88%D0%B8%D1%82%D1%8C%20%D0%B7%D0%B0%D0%B4%D0%B0%D1%87%D1%83%20%D0%A5%D0%B0%D0%BD%D0%BE%D0%B9%D1%81%D0%BA%D0%BE%D0%B9%20%D0%B1%D0%B0%D1%88%D0%BD%D0%B8%20f%28i%29%22%22%22%0A%20%20%20%20%23%20%D0%95%D1%81%D0%BB%D0%B8%20%D0%B2%20src%20%D0%BE%D1%81%D1%82%D0%B0%D0%BB%D1%81%D1%8F%20%D1%82%D0%BE%D0%BB%D1%8C%D0%BA%D0%BE%20%D0%BE%D0%B4%D0%B8%D0%BD%20%D0%B4%D0%B8%D1%81%D0%BA%2C%20%D1%81%D1%80%D0%B0%D0%B7%D1%83%20%D0%BF%D0%B5%D1%80%D0%B5%D0%BC%D0%B5%D1%81%D1%82%D0%B8%D1%82%D1%8C%20%D0%B5%D0%B3%D0%BE%20%D0%B2%20tar%0A%20%20%20%20if%20i%20%3D%3D%201%3A%0A%20%20%20%20%20%20%20%20move%28src%2C%20tar%29%0A%20%20%20%20%20%20%20%20return%0A%20%20%20%20%23%20%D0%9F%D0%BE%D0%B4%D0%B7%D0%B0%D0%B4%D0%B0%D1%87%D0%B0%20f%28i-1%29%3A%20%D0%BF%D0%B5%D1%80%D0%B5%D0%BC%D0%B5%D1%81%D1%82%D0%B8%D1%82%D1%8C%20%D0%B2%D0%B5%D1%80%D1%85%D0%BD%D0%B8%D0%B5%20i-1%20%D0%B4%D0%B8%D1%81%D0%BA%D0%BE%D0%B2%20%D0%B8%D0%B7%20src%20%D0%B2%20buf%20%D1%81%20%D0%BF%D0%BE%D0%BC%D0%BE%D1%89%D1%8C%D1%8E%20tar%0A%20%20%20%20dfs%28i%20-%201%2C%20src%2C%20tar%2C%20buf%29%0A%20%20%20%20%23%20%D0%9F%D0%BE%D0%B4%D0%B7%D0%B0%D0%B4%D0%B0%D1%87%D0%B0%20f%281%29%3A%20%D0%BF%D0%B5%D1%80%D0%B5%D0%BC%D0%B5%D1%81%D1%82%D0%B8%D1%82%D1%8C%20%D0%BE%D1%81%D1%82%D0%B0%D0%B2%D1%88%D0%B8%D0%B9%D1%81%D1%8F%20%D0%BE%D0%B4%D0%B8%D0%BD%20%D0%B4%D0%B8%D1%81%D0%BA%20%D0%B8%D0%B7%20src%20%D0%B2%20tar%0A%20%20%20%20move%28src%2C%20tar%29%0A%20%20%20%20%23%20%D0%9F%D0%BE%D0%B4%D0%B7%D0%B0%D0%B4%D0%B0%D1%87%D0%B0%20f%28i-1%29%3A%20%D0%BF%D0%B5%D1%80%D0%B5%D0%BC%D0%B5%D1%81%D1%82%D0%B8%D1%82%D1%8C%20%D0%B2%D0%B5%D1%80%D1%85%D0%BD%D0%B8%D0%B5%20i-1%20%D0%B4%D0%B8%D1%81%D0%BA%D0%BE%D0%B2%20%D0%B8%D0%B7%20buf%20%D0%B2%20tar%20%D1%81%20%D0%BF%D0%BE%D0%BC%D0%BE%D1%89%D1%8C%D1%8E%20src%0A%20%20%20%20dfs%28i%20-%201%2C%20buf%2C%20src%2C%20tar%29%0A%0A%0Adef%20solve_hanota%28A%3A%20list%5Bint%5D%2C%20B%3A%20list%5Bint%5D%2C%20C%3A%20list%5Bint%5D%29%3A%0A%20%20%20%20%22%22%22%D0%A0%D0%B5%D1%88%D0%B8%D1%82%D1%8C%20%D0%B7%D0%B0%D0%B4%D0%B0%D1%87%D1%83%20%D0%A5%D0%B0%D0%BD%D0%BE%D0%B9%D1%81%D0%BA%D0%BE%D0%B9%20%D0%B1%D0%B0%D1%88%D0%BD%D0%B8%22%22%22%0A%20%20%20%20n%20%3D%20len%28A%29%0A%20%20%20%20%23%20%D0%9F%D0%B5%D1%80%D0%B5%D0%BC%D0%B5%D1%81%D1%82%D0%B8%D1%82%D1%8C%20%D0%B2%D0%B5%D1%80%D1%85%D0%BD%D0%B8%D0%B5%20n%20%D0%B4%D0%B8%D1%81%D0%BA%D0%BE%D0%B2%20%D0%B8%D0%B7%20A%20%D0%B2%20C%20%D1%81%20%D0%BF%D0%BE%D0%BC%D0%BE%D1%89%D1%8C%D1%8E%20B%0A%20%20%20%20dfs%28n%2C%20A%2C%20B%2C%20C%29%0A%0A%0A%22%22%22Driver%20Code%22%22%22%0Aif%20__name__%20%3D%3D%20%22__main__%22%3A%0A%20%20%20%20%23%20%D0%A5%D0%B2%D0%BE%D1%81%D1%82%20%D1%81%D0%BF%D0%B8%D1%81%D0%BA%D0%B0%20%D1%81%D0%BE%D0%BE%D1%82%D0%B2%D0%B5%D1%82%D1%81%D1%82%D0%B2%D1%83%D0%B5%D1%82%20%D0%B2%D0%B5%D1%80%D1%88%D0%B8%D0%BD%D0%B5%20%D1%81%D1%82%D0%BE%D0%BB%D0%B1%D1%86%D0%B0%0A%20%20%20%20A%20%3D%20%5B5%2C%204%2C%203%2C%202%2C%201%5D%0A%20%20%20%20B%20%3D%20%5B%5D%0A%20%20%20%20C%20%3D%20%5B%5D%0A%20%20%20%20print%28%22%D0%98%D1%81%D1%85%D0%BE%D0%B4%D0%BD%D0%BE%D0%B5%20%D1%81%D0%BE%D1%81%D1%82%D0%BE%D1%8F%D0%BD%D0%B8%D0%B5%3A%22%29%0A%20%20%20%20print%28f%22A%20%3D%20%7BA%7D%22%29%0A%20%20%20%20print%28f%22B%20%3D%20%7BB%7D%22%29%0A%20%20%20%20print%28f%22C%20%3D%20%7BC%7D%22%29%0A%0A%20%20%20%20solve_hanota%28A%2C%20B%2C%20C%29%0A%0A%20%20%20%20print%28%22%D0%9F%D0%BE%D1%81%D0%BB%D0%B5%20%D0%B7%D0%B0%D0%B2%D0%B5%D1%80%D1%88%D0%B5%D0%BD%D0%B8%D1%8F%20%D0%BF%D0%B5%D1%80%D0%B5%D0%BC%D0%B5%D1%89%D0%B5%D0%BD%D0%B8%D1%8F%20%D0%B4%D0%B8%D1%81%D0%BA%D0%BE%D0%B2%3A%22%29%0A%20%20%20%20print%28f%22A%20%3D%20%7BA%7D%22%29%0A%20%20%20%20print%28f%22B%20%3D%20%7BB%7D%22%29%0A%20%20%20%20print%28f%22C%20%3D%20%7BC%7D%22%29&codeDivHeight=472&codeDivWidth=350&cumulative=false&curInstr=12&heapPrimitives=nevernest&origin=opt-frontend.js&py=311&rawInputLstJSON=%5B%5D&textReferences=false"> </iframe></div>
<div style="margin-top: 5px;"><a href="https://pythontutor.com/iframe-embed.html#code=def%20move%28src%3A%20list%5Bint%5D%2C%20tar%3A%20list%5Bint%5D%29%3A%0A%20%20%20%20%22%22%22%D0%9F%D0%B5%D1%80%D0%B5%D0%BC%D0%B5%D1%81%D1%82%D0%B8%D1%82%D1%8C%20%D0%BE%D0%B4%D0%B8%D0%BD%20%D0%B4%D0%B8%D1%81%D0%BA%22%22%22%0A%20%20%20%20%23%20%D0%A1%D0%BD%D1%8F%D1%82%D1%8C%20%D0%B4%D0%B8%D1%81%D0%BA%20%D1%81%20%D0%B2%D0%B5%D1%80%D1%88%D0%B8%D0%BD%D1%8B%20src%0A%20%20%20%20pan%20%3D%20src.pop%28%29%0A%20%20%20%20%23%20%D0%9F%D0%BE%D0%BB%D0%BE%D0%B6%D0%B8%D1%82%D1%8C%20%D0%B4%D0%B8%D1%81%D0%BA%20%D0%BD%D0%B0%20%D0%B2%D0%B5%D1%80%D1%88%D0%B8%D0%BD%D1%83%20tar%0A%20%20%20%20tar.append%28pan%29%0A%0A%0Adef%20dfs%28i%3A%20int%2C%20src%3A%20list%5Bint%5D%2C%20buf%3A%20list%5Bint%5D%2C%20tar%3A%20list%5Bint%5D%29%3A%0A%20%20%20%20%22%22%22%D0%A0%D0%B5%D1%88%D0%B8%D1%82%D1%8C%20%D0%B7%D0%B0%D0%B4%D0%B0%D1%87%D1%83%20%D0%A5%D0%B0%D0%BD%D0%BE%D0%B9%D1%81%D0%BA%D0%BE%D0%B9%20%D0%B1%D0%B0%D1%88%D0%BD%D0%B8%20f%28i%29%22%22%22%0A%20%20%20%20%23%20%D0%95%D1%81%D0%BB%D0%B8%20%D0%B2%20src%20%D0%BE%D1%81%D1%82%D0%B0%D0%BB%D1%81%D1%8F%20%D1%82%D0%BE%D0%BB%D1%8C%D0%BA%D0%BE%20%D0%BE%D0%B4%D0%B8%D0%BD%20%D0%B4%D0%B8%D1%81%D0%BA%2C%20%D1%81%D1%80%D0%B0%D0%B7%D1%83%20%D0%BF%D0%B5%D1%80%D0%B5%D0%BC%D0%B5%D1%81%D1%82%D0%B8%D1%82%D1%8C%20%D0%B5%D0%B3%D0%BE%20%D0%B2%20tar%0A%20%20%20%20if%20i%20%3D%3D%201%3A%0A%20%20%20%20%20%20%20%20move%28src%2C%20tar%29%0A%20%20%20%20%20%20%20%20return%0A%20%20%20%20%23%20%D0%9F%D0%BE%D0%B4%D0%B7%D0%B0%D0%B4%D0%B0%D1%87%D0%B0%20f%28i-1%29%3A%20%D0%BF%D0%B5%D1%80%D0%B5%D0%BC%D0%B5%D1%81%D1%82%D0%B8%D1%82%D1%8C%20%D0%B2%D0%B5%D1%80%D1%85%D0%BD%D0%B8%D0%B5%20i-1%20%D0%B4%D0%B8%D1%81%D0%BA%D0%BE%D0%B2%20%D0%B8%D0%B7%20src%20%D0%B2%20buf%20%D1%81%20%D0%BF%D0%BE%D0%BC%D0%BE%D1%89%D1%8C%D1%8E%20tar%0A%20%20%20%20dfs%28i%20-%201%2C%20src%2C%20tar%2C%20buf%29%0A%20%20%20%20%23%20%D0%9F%D0%BE%D0%B4%D0%B7%D0%B0%D0%B4%D0%B0%D1%87%D0%B0%20f%281%29%3A%20%D0%BF%D0%B5%D1%80%D0%B5%D0%BC%D0%B5%D1%81%D1%82%D0%B8%D1%82%D1%8C%20%D0%BE%D1%81%D1%82%D0%B0%D0%B2%D1%88%D0%B8%D0%B9%D1%81%D1%8F%20%D0%BE%D0%B4%D0%B8%D0%BD%20%D0%B4%D0%B8%D1%81%D0%BA%20%D0%B8%D0%B7%20src%20%D0%B2%20tar%0A%20%20%20%20move%28src%2C%20tar%29%0A%20%20%20%20%23%20%D0%9F%D0%BE%D0%B4%D0%B7%D0%B0%D0%B4%D0%B0%D1%87%D0%B0%20f%28i-1%29%3A%20%D0%BF%D0%B5%D1%80%D0%B5%D0%BC%D0%B5%D1%81%D1%82%D0%B8%D1%82%D1%8C%20%D0%B2%D0%B5%D1%80%D1%85%D0%BD%D0%B8%D0%B5%20i-1%20%D0%B4%D0%B8%D1%81%D0%BA%D0%BE%D0%B2%20%D0%B8%D0%B7%20buf%20%D0%B2%20tar%20%D1%81%20%D0%BF%D0%BE%D0%BC%D0%BE%D1%89%D1%8C%D1%8E%20src%0A%20%20%20%20dfs%28i%20-%201%2C%20buf%2C%20src%2C%20tar%29%0A%0A%0Adef%20solve_hanota%28A%3A%20list%5Bint%5D%2C%20B%3A%20list%5Bint%5D%2C%20C%3A%20list%5Bint%5D%29%3A%0A%20%20%20%20%22%22%22%D0%A0%D0%B5%D1%88%D0%B8%D1%82%D1%8C%20%D0%B7%D0%B0%D0%B4%D0%B0%D1%87%D1%83%20%D0%A5%D0%B0%D0%BD%D0%BE%D0%B9%D1%81%D0%BA%D0%BE%D0%B9%20%D0%B1%D0%B0%D1%88%D0%BD%D0%B8%22%22%22%0A%20%20%20%20n%20%3D%20len%28A%29%0A%20%20%20%20%23%20%D0%9F%D0%B5%D1%80%D0%B5%D0%BC%D0%B5%D1%81%D1%82%D0%B8%D1%82%D1%8C%20%D0%B2%D0%B5%D1%80%D1%85%D0%BD%D0%B8%D0%B5%20n%20%D0%B4%D0%B8%D1%81%D0%BA%D0%BE%D0%B2%20%D0%B8%D0%B7%20A%20%D0%B2%20C%20%D1%81%20%D0%BF%D0%BE%D0%BC%D0%BE%D1%89%D1%8C%D1%8E%20B%0A%20%20%20%20dfs%28n%2C%20A%2C%20B%2C%20C%29%0A%0A%0A%22%22%22Driver%20Code%22%22%22%0Aif%20__name__%20%3D%3D%20%22__main__%22%3A%0A%20%20%20%20%23%20%D0%A5%D0%B2%D0%BE%D1%81%D1%82%20%D1%81%D0%BF%D0%B8%D1%81%D0%BA%D0%B0%20%D1%81%D0%BE%D0%BE%D1%82%D0%B2%D0%B5%D1%82%D1%81%D1%82%D0%B2%D1%83%D0%B5%D1%82%20%D0%B2%D0%B5%D1%80%D1%88%D0%B8%D0%BD%D0%B5%20%D1%81%D1%82%D0%BE%D0%BB%D0%B1%D1%86%D0%B0%0A%20%20%20%20A%20%3D%20%5B5%2C%204%2C%203%2C%202%2C%201%5D%0A%20%20%20%20B%20%3D%20%5B%5D%0A%20%20%20%20C%20%3D%20%5B%5D%0A%20%20%20%20print%28%22%D0%98%D1%81%D1%85%D0%BE%D0%B4%D0%BD%D0%BE%D0%B5%20%D1%81%D0%BE%D1%81%D1%82%D0%BE%D1%8F%D0%BD%D0%B8%D0%B5%3A%22%29%0A%20%20%20%20print%28f%22A%20%3D%20%7BA%7D%22%29%0A%20%20%20%20print%28f%22B%20%3D%20%7BB%7D%22%29%0A%20%20%20%20print%28f%22C%20%3D%20%7BC%7D%22%29%0A%0A%20%20%20%20solve_hanota%28A%2C%20B%2C%20C%29%0A%0A%20%20%20%20print%28%22%D0%9F%D0%BE%D1%81%D0%BB%D0%B5%20%D0%B7%D0%B0%D0%B2%D0%B5%D1%80%D1%88%D0%B5%D0%BD%D0%B8%D1%8F%20%D0%BF%D0%B5%D1%80%D0%B5%D0%BC%D0%B5%D1%89%D0%B5%D0%BD%D0%B8%D1%8F%20%D0%B4%D0%B8%D1%81%D0%BA%D0%BE%D0%B2%3A%22%29%0A%20%20%20%20print%28f%22A%20%3D%20%7BA%7D%22%29%0A%20%20%20%20print%28f%22B%20%3D%20%7BB%7D%22%29%0A%20%20%20%20print%28f%22C%20%3D%20%7BC%7D%22%29&codeDivHeight=800&codeDivWidth=600&cumulative=false&curInstr=12&heapPrimitives=nevernest&origin=opt-frontend.js&py=311&rawInputLstJSON=%5B%5D&textReferences=false" target="_blank" rel="noopener noreferrer">Во весь экран &gt;</a></div></p>
</details>
<p>Как показано на рисунке 12-15, задача о Ханойской башне формирует дерево рекурсии высоты <span class="arithmatex">\(n\)</span> , в котором каждый узел представляет подзадачу и соответствует одному открытому вызову <code>dfs()</code> ; <strong>поэтому временная сложность равна <span class="arithmatex">\(O(2^n)\)</span> , а пространственная сложность равна <span class="arithmatex">\(O(n)\)</span></strong> .</p>
<p>Как показано на рисунке 12-15, задача о Ханойской башне формирует дерево рекурсии высоты <span class="arithmatex">\(n\)</span> , в котором каждый узел представляет подзадачу и соответствует одному открытому вызову <code>dfs()</code>. <strong>Поэтому временная сложность равна <span class="arithmatex">\(O(2^n)\)</span> , а пространственная сложность равна <span class="arithmatex">\(O(n)\)</span></strong> .</p>
<p><img alt="Дерево рекурсии задачи о Ханойской башне" class="animation-figure" src="../hanota_problem.assets/hanota_recursive_tree.png" /></p>
<p align="center"> Рисунок 12-15 &nbsp; Дерево рекурсии задачи о Ханойской башне </p>
+1 -1
View File
@@ -4280,7 +4280,7 @@
<div class="admonition abstract">
<p class="admonition-title">Abstract</p>
<p>Сложная задача раскладывается слой за слоем, и каждое новое разбиение делает ее проще.</p>
<p>Принцип "разделяй и властвуй" показывает важный факт: если начать с простого, многое перестает быть сложным.</p>
<p>Принцип «разделяй и властвуй» показывает важный факт: если начать с простого, многое перестает быть сложным.</p>
</div>
<h2 id="_1">Содержание главы<a class="headerlink" href="#_1" title="Permanent link">&para;</a></h2>
<ul>
@@ -4337,13 +4337,13 @@
<h1 id="125">12.5 &nbsp; Резюме<a class="headerlink" href="#125" title="Permanent link">&para;</a></h1>
<h3 id="1">1. &nbsp; Ключевые выводы<a class="headerlink" href="#1" title="Permanent link">&para;</a></h3>
<ul>
<li>"Разделяй и властвуй" - это распространенная стратегия проектирования алгоритмов, которая включает два этапа: разделение (декомпозицию) и объединение (синтез), и обычно реализуется с помощью рекурсии.</li>
<li>«Разделяй и властвуй» - это распространенная стратегия проектирования алгоритмов, которая включает два этапа: разделение (декомпозицию) и объединение (синтез), и обычно реализуется с помощью рекурсии.</li>
<li>Критерии применимости этой стратегии к задаче включают: возможность разложения задачи, независимость подзадач и возможность объединения их решений.</li>
<li>Сортировка слиянием является типичным применением стратегии "разделяй и властвуй": она рекурсивно делит массив на два равных по длине подмассива, пока не останется массив из одного элемента, после чего начинает поэтапное объединение.</li>
<li>Использование стратегии "разделяй и властвуй" часто позволяет повысить эффективность алгоритма. С одной стороны, она уменьшает число операций; с другой - после разбиения способствует параллельной оптимизации на уровне системы.</li>
<li>"Разделяй и властвуй" не только помогает решать многие алгоритмические задачи, но и широко используется при проектировании структур данных и алгоритмов, поэтому его можно встретить буквально повсюду.</li>
<li>По сравнению с полным перебором адаптивный поиск работает эффективнее. Алгоритмы поиска со сложностью <span class="arithmatex">\(O(\log n)\)</span> обычно реализуются на основе стратегии "разделяй и властвуй".</li>
<li>Двоичный поиск - еще одно типичное применение стратегии "разделяй и властвуй", в котором отсутствует шаг объединения решений подзадач. Его можно реализовать рекурсивно, опираясь на эту стратегию.</li>
<li>Сортировка слиянием является типичным применением стратегии «разделяй и властвуй»: она рекурсивно делит массив на два равных по длине подмассива, пока не останется массив из одного элемента, после чего начинает поэтапное объединение.</li>
<li>Использование стратегии «разделяй и властвуй» часто позволяет повысить эффективность алгоритма. С одной стороны, она уменьшает число операций. С другой - после разбиения способствует параллельной оптимизации на уровне системы.</li>
<li>«Разделяй и властвуй» не только помогает решать многие алгоритмические задачи, но и широко используется при проектировании структур данных и алгоритмов, поэтому его можно встретить буквально повсюду.</li>
<li>По сравнению с полным перебором адаптивный поиск работает эффективнее. Алгоритмы поиска со сложностью <span class="arithmatex">\(O(\log n)\)</span> обычно реализуются на основе стратегии «разделяй и властвуй».</li>
<li>Двоичный поиск - еще одно типичное применение стратегии «разделяй и властвуй», в котором отсутствует шаг объединения решений подзадач. Его можно реализовать рекурсивно, опираясь на эту стратегию.</li>
<li>В задаче построения двоичного дерева исходная задача построения дерева может быть разбита на две подзадачи: построение левого и правого поддеревьев, а реализуется это через разбиение индексных интервалов прямого и симметричного обходов.</li>
<li>В задаче о Ханойской башне задача размера <span class="arithmatex">\(n\)</span> разбивается на две подзадачи размера <span class="arithmatex">\(n-1\)</span> и одну подзадачу размера <span class="arithmatex">\(1\)</span> . После последовательного решения этих трех подзадач исходная задача также оказывается решенной.</li>
</ul>
@@ -4357,10 +4357,10 @@
<!-- Page content -->
<h1 id="142">14.2 &nbsp; Свойства задач динамического программирования<a class="headerlink" href="#142" title="Permanent link">&para;</a></h1>
<p>В предыдущем разделе мы увидели, как динамическое программирование решает исходную задачу через разложение на подзадачи. На самом деле разложение на подзадачи - это общий алгоритмический подход, но в методе "разделяй и властвуй", динамическом программировании и поиске с возвратом акценты расставлены по-разному.</p>
<p>В предыдущем разделе мы увидели, как динамическое программирование решает исходную задачу через разложение на подзадачи. На самом деле разложение на подзадачи - это общий алгоритмический подход, но в методе «разделяй и властвуй», динамическом программировании и поиске с возвратом акценты расставлены по-разному.</p>
<ul>
<li>Алгоритмы "разделяй и властвуй" рекурсивно раскладывают исходную задачу на несколько независимых подзадач, пока не будет достигнута наименьшая подзадача, а затем в процессе возврата объединяют решения подзадач в решение исходной задачи.</li>
<li>Динамическое программирование тоже раскладывает задачу рекурсивно, но его главное отличие от метода "разделяй и властвуй" в том, что подзадачи здесь зависят друг от друга и в процессе разложения возникает много перекрывающихся подзадач.</li>
<li>Алгоритмы «разделяй и властвуй» рекурсивно раскладывают исходную задачу на несколько независимых подзадач, пока не будет достигнута наименьшая подзадача, а затем в процессе возврата объединяют решения подзадач в решение исходной задачи.</li>
<li>Динамическое программирование тоже раскладывает задачу рекурсивно, но его главное отличие от метода «разделяй и властвуй» в том, что подзадачи здесь зависят друг от друга и в процессе разложения возникает много перекрывающихся подзадач.</li>
<li>Алгоритм поиска с возвратом перебирает все возможные решения через попытки и откат и с помощью обрезки избегает ненужных ветвей поиска. Решение исходной задачи состоит из последовательности решений, и подзадачей можно считать префикс этой последовательности решений.</li>
</ul>
<p>На практике динамическое программирование часто применяется для задач оптимизации. Такие задачи не только содержат перекрывающиеся подзадачи, но и обладают еще двумя важными свойствами: оптимальной подструктурой и отсутствием последствий.</p>
@@ -4380,7 +4380,7 @@ dp[i] = \min(dp[i-1], dp[i-2]) + cost[i]
\]</div>
<p>Отсюда и возникает смысл оптимальной подструктуры: <strong>оптимальное решение исходной задачи строится из оптимальных решений подзадач</strong>.</p>
<p>Очевидно, что эта задача обладает оптимальной подструктурой: мы берем лучшее из двух оптимальных решений подзадач <span class="arithmatex">\(dp[i-1]\)</span> и <span class="arithmatex">\(dp[i-2]\)</span> и на его основе строим оптимальное решение исходной задачи <span class="arithmatex">\(dp[i]\)</span> .</p>
<p>А обладает ли оптимальной подструктурой исходная задача о числе способов подъема по лестнице из прошлого раздела? Формально она не про оптимум, а про подсчет количества. Но если переформулировать ее как "найдите максимальное количество способов", мы неожиданно увидим, что <strong>хотя исходная задача осталась по сути той же, оптимальная подструктура стала явной</strong>: максимальное число способов добраться до ступени <span class="arithmatex">\(n\)</span> равно сумме максимальных чисел способов добраться до ступеней <span class="arithmatex">\(n-1\)</span> и <span class="arithmatex">\(n-2\)</span> . То есть объяснение оптимальной подструктуры в разных задачах может быть довольно гибким.</p>
<p>А обладает ли оптимальной подструктурой исходная задача о числе способов подъема по лестнице из прошлого раздела? Формально она не про оптимум, а про подсчет количества. Но если переформулировать ее как «найдите максимальное количество способов», мы неожиданно увидим, что <strong>хотя исходная задача осталась по сути той же, оптимальная подструктура стала явной</strong>: максимальное число способов добраться до ступени <span class="arithmatex">\(n\)</span> равно сумме максимальных чисел способов добраться до ступеней <span class="arithmatex">\(n-1\)</span> и <span class="arithmatex">\(n-2\)</span> . То есть объяснение оптимальной подструктуры в разных задачах может быть довольно гибким.</p>
<p>Зная уравнение перехода состояния, а также начальные состояния <span class="arithmatex">\(dp[1] = cost[1]\)</span> и <span class="arithmatex">\(dp[2] = cost[2]\)</span> , мы можем сразу написать код динамического программирования:</p>
<div class="tabbed-set tabbed-alternate" data-tabs="1:13"><input checked="checked" id="__tabbed_1_1" name="__tabbed_1" type="radio" /><input id="__tabbed_1_2" name="__tabbed_1" type="radio" /><input id="__tabbed_1_3" name="__tabbed_1" type="radio" /><input id="__tabbed_1_4" name="__tabbed_1" type="radio" /><input id="__tabbed_1_5" name="__tabbed_1" type="radio" /><input id="__tabbed_1_6" name="__tabbed_1" type="radio" /><input id="__tabbed_1_7" name="__tabbed_1" type="radio" /><input id="__tabbed_1_8" name="__tabbed_1" type="radio" /><input id="__tabbed_1_9" name="__tabbed_1" type="radio" /><input id="__tabbed_1_10" name="__tabbed_1" type="radio" /><input id="__tabbed_1_11" name="__tabbed_1" type="radio" /><input id="__tabbed_1_12" name="__tabbed_1" type="radio" /><input id="__tabbed_1_13" name="__tabbed_1" type="radio" /><div class="tabbed-labels"><label for="__tabbed_1_1">Python</label><label for="__tabbed_1_2">C++</label><label for="__tabbed_1_3">Java</label><label for="__tabbed_1_4">C#</label><label for="__tabbed_1_5">Go</label><label for="__tabbed_1_6">Swift</label><label for="__tabbed_1_7">JS</label><label for="__tabbed_1_8">TS</label><label for="__tabbed_1_9">Dart</label><label for="__tabbed_1_10">Rust</label><label for="__tabbed_1_11">C</label><label for="__tabbed_1_12">Kotlin</label><label for="__tabbed_1_13">Ruby</label></div>
<div class="tabbed-content">
@@ -4883,7 +4883,7 @@ dp[i] = \min(dp[i-1], dp[i-2]) + cost[i]
</details>
<h2 id="1422">14.2.2 &nbsp; Отсутствие последствий<a class="headerlink" href="#1422" title="Permanent link">&para;</a></h2>
<p>Отсутствие последствий - одно из ключевых свойств, благодаря которому динамическое программирование вообще может эффективно работать. Его определение таково: <strong>если текущее состояние задано однозначно, то его дальнейшее развитие зависит только от него самого и не зависит от всей истории предыдущих состояний</strong>.</p>
<p>Для примера снова рассмотрим задачу о лестнице. Если дано состояние <span class="arithmatex">\(i\)</span> , то из него можно перейти в состояния <span class="arithmatex">\(i+1\)</span> и <span class="arithmatex">\(i+2\)</span> , соответствующие прыжкам на <span class="arithmatex">\(1\)</span> и на <span class="arithmatex">\(2\)</span> ступени. Чтобы сделать один из этих выборов, не нужно знать, какими были состояния до <span class="arithmatex">\(i\)</span> ; на будущее влияет только текущее состояние <span class="arithmatex">\(i\)</span> .</p>
<p>Для примера снова рассмотрим задачу о лестнице. Если дано состояние <span class="arithmatex">\(i\)</span> , то из него можно перейти в состояния <span class="arithmatex">\(i+1\)</span> и <span class="arithmatex">\(i+2\)</span> , соответствующие прыжкам на <span class="arithmatex">\(1\)</span> и на <span class="arithmatex">\(2\)</span> ступени. Чтобы сделать один из этих выборов, не нужно знать, какими были состояния до <span class="arithmatex">\(i\)</span>. На будущее влияет только текущее состояние <span class="arithmatex">\(i\)</span> .</p>
<p>Однако если добавить в задачу дополнительное ограничение, ситуация изменится.</p>
<div class="admonition question">
<p class="admonition-title">Подъем по лестнице с ограничением</p>
@@ -4910,7 +4910,7 @@ dp[i, 2] = dp[i-2, 1] + dp[i-2, 2]
<p><img alt="Рекуррентная связь с учетом ограничения" class="animation-figure" src="../dp_problem_features.assets/climbing_stairs_constraint_state_transfer.png" /></p>
<p align="center"> Рисунок 14-9 &nbsp; Рекуррентная связь с учетом ограничения </p>
<p>В конце достаточно вернуть <span class="arithmatex">\(dp[n, 1] + dp[n, 2]\)</span> ; эта сумма и представляет общее число способов добраться до ступени <span class="arithmatex">\(n\)</span> :</p>
<p>В конце достаточно вернуть <span class="arithmatex">\(dp[n, 1] + dp[n, 2]\)</span>. Эта сумма и представляет общее число способов добраться до ступени <span class="arithmatex">\(n\)</span> :</p>
<div class="tabbed-set tabbed-alternate" data-tabs="3:13"><input checked="checked" id="__tabbed_3_1" name="__tabbed_3" type="radio" /><input id="__tabbed_3_2" name="__tabbed_3" type="radio" /><input id="__tabbed_3_3" name="__tabbed_3" type="radio" /><input id="__tabbed_3_4" name="__tabbed_3" type="radio" /><input id="__tabbed_3_5" name="__tabbed_3" type="radio" /><input id="__tabbed_3_6" name="__tabbed_3" type="radio" /><input id="__tabbed_3_7" name="__tabbed_3" type="radio" /><input id="__tabbed_3_8" name="__tabbed_3" type="radio" /><input id="__tabbed_3_9" name="__tabbed_3" type="radio" /><input id="__tabbed_3_10" name="__tabbed_3" type="radio" /><input id="__tabbed_3_11" name="__tabbed_3" type="radio" /><input id="__tabbed_3_12" name="__tabbed_3" type="radio" /><input id="__tabbed_3_13" name="__tabbed_3" type="radio" /><div class="tabbed-labels"><label for="__tabbed_3_1">Python</label><label for="__tabbed_3_2">C++</label><label for="__tabbed_3_3">Java</label><label for="__tabbed_3_4">C#</label><label for="__tabbed_3_5">Go</label><label for="__tabbed_3_6">Swift</label><label for="__tabbed_3_7">JS</label><label for="__tabbed_3_8">TS</label><label for="__tabbed_3_9">Dart</label><label for="__tabbed_3_10">Rust</label><label for="__tabbed_3_11">C</label><label for="__tabbed_3_12">Kotlin</label><label for="__tabbed_3_13">Ruby</label></div>
<div class="tabbed-content">
<div class="tabbed-block">
@@ -5208,7 +5208,7 @@ dp[i, 2] = dp[i-2, 1] + dp[i-2, 2]
<p><div style="height: 549px; width: 100%;"><iframe class="pythontutor-iframe" src="https://pythontutor.com/iframe-embed.html#code=def%20climbing_stairs_constraint_dp%28n%3A%20int%29%20-%3E%20int%3A%0A%20%20%20%20%22%22%22%D0%9F%D0%BE%D0%B4%D1%8A%D0%B5%D0%BC%20%D0%BF%D0%BE%20%D0%BB%D0%B5%D1%81%D1%82%D0%BD%D0%B8%D1%86%D0%B5%20%D1%81%20%D0%BE%D0%B3%D1%80%D0%B0%D0%BD%D0%B8%D1%87%D0%B5%D0%BD%D0%B8%D1%8F%D0%BC%D0%B8%3A%20%D0%B4%D0%B8%D0%BD%D0%B0%D0%BC%D0%B8%D1%87%D0%B5%D1%81%D0%BA%D0%BE%D0%B5%20%D0%BF%D1%80%D0%BE%D0%B3%D1%80%D0%B0%D0%BC%D0%BC%D0%B8%D1%80%D0%BE%D0%B2%D0%B0%D0%BD%D0%B8%D0%B5%22%22%22%0A%20%20%20%20if%20n%20%3D%3D%201%20or%20n%20%3D%3D%202%3A%0A%20%20%20%20%20%20%20%20return%201%0A%20%20%20%20%23%20%D0%98%D0%BD%D0%B8%D1%86%D0%B8%D0%B0%D0%BB%D0%B8%D0%B7%D0%B0%D1%86%D0%B8%D1%8F%20%D1%82%D0%B0%D0%B1%D0%BB%D0%B8%D1%86%D1%8B%20dp%20%D0%B4%D0%BB%D1%8F%20%D1%85%D1%80%D0%B0%D0%BD%D0%B5%D0%BD%D0%B8%D1%8F%20%D1%80%D0%B5%D1%88%D0%B5%D0%BD%D0%B8%D0%B9%20%D0%BF%D0%BE%D0%B4%D0%B7%D0%B0%D0%B4%D0%B0%D1%87%0A%20%20%20%20dp%20%3D%20%5B%5B0%5D%20%2A%203%20for%20_%20in%20range%28n%20%2B%201%29%5D%0A%20%20%20%20%23%20%D0%9D%D0%B0%D1%87%D0%B0%D0%BB%D1%8C%D0%BD%D0%BE%D0%B5%20%D1%81%D0%BE%D1%81%D1%82%D0%BE%D1%8F%D0%BD%D0%B8%D0%B5%3A%20%D0%B7%D0%B0%D1%80%D0%B0%D0%BD%D0%B5%D0%B5%20%D0%B7%D0%B0%D0%B4%D0%B0%D1%82%D1%8C%20%D1%80%D0%B5%D1%88%D0%B5%D0%BD%D0%B8%D1%8F%20%D0%BD%D0%B0%D0%B8%D0%BC%D0%B5%D0%BD%D1%8C%D1%88%D0%B8%D1%85%20%D0%BF%D0%BE%D0%B4%D0%B7%D0%B0%D0%B4%D0%B0%D1%87%0A%20%20%20%20dp%5B1%5D%5B1%5D%2C%20dp%5B1%5D%5B2%5D%20%3D%201%2C%200%0A%20%20%20%20dp%5B2%5D%5B1%5D%2C%20dp%5B2%5D%5B2%5D%20%3D%200%2C%201%0A%20%20%20%20%23%20%D0%9F%D0%B5%D1%80%D0%B5%D1%85%D0%BE%D0%B4%20%D1%81%D0%BE%D1%81%D1%82%D0%BE%D1%8F%D0%BD%D0%B8%D0%B9%3A%20%D0%BF%D0%BE%D1%81%D1%82%D0%B5%D0%BF%D0%B5%D0%BD%D0%BD%D0%BE%D0%B5%20%D1%80%D0%B5%D1%88%D0%B5%D0%BD%D0%B8%D0%B5%20%D0%B1%D0%BE%D0%BB%D1%8C%D1%88%D0%B8%D1%85%20%D0%BF%D0%BE%D0%B4%D0%B7%D0%B0%D0%B4%D0%B0%D1%87%20%D1%87%D0%B5%D1%80%D0%B5%D0%B7%20%D0%BC%D0%B5%D0%BD%D1%8C%D1%88%D0%B8%D0%B5%0A%20%20%20%20for%20i%20in%20range%283%2C%20n%20%2B%201%29%3A%0A%20%20%20%20%20%20%20%20dp%5Bi%5D%5B1%5D%20%3D%20dp%5Bi%20-%201%5D%5B2%5D%0A%20%20%20%20%20%20%20%20dp%5Bi%5D%5B2%5D%20%3D%20dp%5Bi%20-%202%5D%5B1%5D%20%2B%20dp%5Bi%20-%202%5D%5B2%5D%0A%20%20%20%20return%20dp%5Bn%5D%5B1%5D%20%2B%20dp%5Bn%5D%5B2%5D%0A%0A%0A%22%22%22Driver%20Code%22%22%22%0Aif%20__name__%20%3D%3D%20%22__main__%22%3A%0A%20%20%20%20n%20%3D%209%0A%0A%20%20%20%20res%20%3D%20climbing_stairs_constraint_dp%28n%29%0A%20%20%20%20print%28f%22%D0%9A%D0%BE%D0%BB%D0%B8%D1%87%D0%B5%D1%81%D1%82%D0%B2%D0%BE%20%D1%81%D0%BF%D0%BE%D1%81%D0%BE%D0%B1%D0%BE%D0%B2%20%D0%BF%D0%BE%D0%B4%D0%BD%D1%8F%D1%82%D1%8C%D1%81%D1%8F%20%D0%BF%D0%BE%20%D0%BB%D0%B5%D1%81%D1%82%D0%BD%D0%B8%D1%86%D0%B5%20%D0%B8%D0%B7%20%7Bn%7D%20%D1%81%D1%82%D1%83%D0%BF%D0%B5%D0%BD%D0%B5%D0%B9%20%3D%20%7Bres%7D%22%29&codeDivHeight=472&codeDivWidth=350&cumulative=false&curInstr=4&heapPrimitives=nevernest&origin=opt-frontend.js&py=311&rawInputLstJSON=%5B%5D&textReferences=false"> </iframe></div>
<div style="margin-top: 5px;"><a href="https://pythontutor.com/iframe-embed.html#code=def%20climbing_stairs_constraint_dp%28n%3A%20int%29%20-%3E%20int%3A%0A%20%20%20%20%22%22%22%D0%9F%D0%BE%D0%B4%D1%8A%D0%B5%D0%BC%20%D0%BF%D0%BE%20%D0%BB%D0%B5%D1%81%D1%82%D0%BD%D0%B8%D1%86%D0%B5%20%D1%81%20%D0%BE%D0%B3%D1%80%D0%B0%D0%BD%D0%B8%D1%87%D0%B5%D0%BD%D0%B8%D1%8F%D0%BC%D0%B8%3A%20%D0%B4%D0%B8%D0%BD%D0%B0%D0%BC%D0%B8%D1%87%D0%B5%D1%81%D0%BA%D0%BE%D0%B5%20%D0%BF%D1%80%D0%BE%D0%B3%D1%80%D0%B0%D0%BC%D0%BC%D0%B8%D1%80%D0%BE%D0%B2%D0%B0%D0%BD%D0%B8%D0%B5%22%22%22%0A%20%20%20%20if%20n%20%3D%3D%201%20or%20n%20%3D%3D%202%3A%0A%20%20%20%20%20%20%20%20return%201%0A%20%20%20%20%23%20%D0%98%D0%BD%D0%B8%D1%86%D0%B8%D0%B0%D0%BB%D0%B8%D0%B7%D0%B0%D1%86%D0%B8%D1%8F%20%D1%82%D0%B0%D0%B1%D0%BB%D0%B8%D1%86%D1%8B%20dp%20%D0%B4%D0%BB%D1%8F%20%D1%85%D1%80%D0%B0%D0%BD%D0%B5%D0%BD%D0%B8%D1%8F%20%D1%80%D0%B5%D1%88%D0%B5%D0%BD%D0%B8%D0%B9%20%D0%BF%D0%BE%D0%B4%D0%B7%D0%B0%D0%B4%D0%B0%D1%87%0A%20%20%20%20dp%20%3D%20%5B%5B0%5D%20%2A%203%20for%20_%20in%20range%28n%20%2B%201%29%5D%0A%20%20%20%20%23%20%D0%9D%D0%B0%D1%87%D0%B0%D0%BB%D1%8C%D0%BD%D0%BE%D0%B5%20%D1%81%D0%BE%D1%81%D1%82%D0%BE%D1%8F%D0%BD%D0%B8%D0%B5%3A%20%D0%B7%D0%B0%D1%80%D0%B0%D0%BD%D0%B5%D0%B5%20%D0%B7%D0%B0%D0%B4%D0%B0%D1%82%D1%8C%20%D1%80%D0%B5%D1%88%D0%B5%D0%BD%D0%B8%D1%8F%20%D0%BD%D0%B0%D0%B8%D0%BC%D0%B5%D0%BD%D1%8C%D1%88%D0%B8%D1%85%20%D0%BF%D0%BE%D0%B4%D0%B7%D0%B0%D0%B4%D0%B0%D1%87%0A%20%20%20%20dp%5B1%5D%5B1%5D%2C%20dp%5B1%5D%5B2%5D%20%3D%201%2C%200%0A%20%20%20%20dp%5B2%5D%5B1%5D%2C%20dp%5B2%5D%5B2%5D%20%3D%200%2C%201%0A%20%20%20%20%23%20%D0%9F%D0%B5%D1%80%D0%B5%D1%85%D0%BE%D0%B4%20%D1%81%D0%BE%D1%81%D1%82%D0%BE%D1%8F%D0%BD%D0%B8%D0%B9%3A%20%D0%BF%D0%BE%D1%81%D1%82%D0%B5%D0%BF%D0%B5%D0%BD%D0%BD%D0%BE%D0%B5%20%D1%80%D0%B5%D1%88%D0%B5%D0%BD%D0%B8%D0%B5%20%D0%B1%D0%BE%D0%BB%D1%8C%D1%88%D0%B8%D1%85%20%D0%BF%D0%BE%D0%B4%D0%B7%D0%B0%D0%B4%D0%B0%D1%87%20%D1%87%D0%B5%D1%80%D0%B5%D0%B7%20%D0%BC%D0%B5%D0%BD%D1%8C%D1%88%D0%B8%D0%B5%0A%20%20%20%20for%20i%20in%20range%283%2C%20n%20%2B%201%29%3A%0A%20%20%20%20%20%20%20%20dp%5Bi%5D%5B1%5D%20%3D%20dp%5Bi%20-%201%5D%5B2%5D%0A%20%20%20%20%20%20%20%20dp%5Bi%5D%5B2%5D%20%3D%20dp%5Bi%20-%202%5D%5B1%5D%20%2B%20dp%5Bi%20-%202%5D%5B2%5D%0A%20%20%20%20return%20dp%5Bn%5D%5B1%5D%20%2B%20dp%5Bn%5D%5B2%5D%0A%0A%0A%22%22%22Driver%20Code%22%22%22%0Aif%20__name__%20%3D%3D%20%22__main__%22%3A%0A%20%20%20%20n%20%3D%209%0A%0A%20%20%20%20res%20%3D%20climbing_stairs_constraint_dp%28n%29%0A%20%20%20%20print%28f%22%D0%9A%D0%BE%D0%BB%D0%B8%D1%87%D0%B5%D1%81%D1%82%D0%B2%D0%BE%20%D1%81%D0%BF%D0%BE%D1%81%D0%BE%D0%B1%D0%BE%D0%B2%20%D0%BF%D0%BE%D0%B4%D0%BD%D1%8F%D1%82%D1%8C%D1%81%D1%8F%20%D0%BF%D0%BE%20%D0%BB%D0%B5%D1%81%D1%82%D0%BD%D0%B8%D1%86%D0%B5%20%D0%B8%D0%B7%20%7Bn%7D%20%D1%81%D1%82%D1%83%D0%BF%D0%B5%D0%BD%D0%B5%D0%B9%20%3D%20%7Bres%7D%22%29&codeDivHeight=800&codeDivWidth=600&cumulative=false&curInstr=4&heapPrimitives=nevernest&origin=opt-frontend.js&py=311&rawInputLstJSON=%5B%5D&textReferences=false" target="_blank" rel="noopener noreferrer">Во весь экран &gt;</a></div></p>
</details>
<p>В этом примере достаточно дополнительно учитывать только одно предыдущее состояние, поэтому после расширения определения состояния задача снова начинает удовлетворять свойству отсутствия последствий. Однако в некоторых задачах "зависимость от прошлого" бывает гораздо серьезнее.</p>
<p>В этом примере достаточно дополнительно учитывать только одно предыдущее состояние, поэтому после расширения определения состояния задача снова начинает удовлетворять свойству отсутствия последствий. Однако в некоторых задачах «зависимость от прошлого» бывает гораздо серьезнее.</p>
<div class="admonition question">
<p class="admonition-title">Подъем по лестнице с порождением препятствий</p>
<p>Дана лестница из <span class="arithmatex">\(n\)</span> ступеней. За один шаг можно подняться на <span class="arithmatex">\(1\)</span> или на <span class="arithmatex">\(2\)</span> ступени. <strong>При этом, если вы попали на ступень <span class="arithmatex">\(i\)</span> , система автоматически создает препятствие на ступени <span class="arithmatex">\(2i\)</span> , и на всех последующих шагах становиться на ступень <span class="arithmatex">\(2i\)</span> уже нельзя</strong>. Например, если в первых двух раундах вы попали на ступени <span class="arithmatex">\(2\)</span> и <span class="arithmatex">\(3\)</span> , то после этого нельзя будет попадать на ступени <span class="arithmatex">\(4\)</span> и <span class="arithmatex">\(6\)</span> . Сколько существует способов добраться до вершины?</p>
@@ -4464,22 +4464,22 @@
</ol>
<h2 id="1431">14.3.1 &nbsp; Определение задачи<a class="headerlink" href="#1431" title="Permanent link">&para;</a></h2>
<p>В целом, если задача содержит перекрывающиеся подзадачи, оптимальную подструктуру и удовлетворяет свойству отсутствия последствий, то она обычно подходит для решения с помощью динамического программирования. Однако извлечь все эти свойства напрямую из формулировки задачи бывает трудно. Поэтому на практике мы обычно ослабляем требования и <strong>сначала смотрим, подходит ли задача для решения методом поиска с возвратом (полного перебора)</strong>.</p>
<p><strong>Задачи, подходящие для поиска с возвратом, обычно удовлетворяют "модели дерева решений"</strong>. Такие задачи можно описать деревом, где каждый узел представляет одно решение, а каждый путь представляет последовательность решений.</p>
<p><strong>Задачи, подходящие для поиска с возвратом, обычно удовлетворяют «модели дерева решений»</strong>. Такие задачи можно описать деревом, где каждый узел представляет одно решение, а каждый путь представляет последовательность решений.</p>
<p>Иначе говоря, если в задаче есть четко выраженные решения и ответ порождается последовательностью таких решений, то она удовлетворяет модели дерева решений и обычно допускает решение через поиск с возвратом.</p>
<p>Поверх этого у задач динамического программирования есть и некоторые дополнительные "плюсы".</p>
<p>Поверх этого у задач динамического программирования есть и некоторые дополнительные «плюсы».</p>
<ul>
<li>В условии задачи фигурируют слова "максимальный", "минимальный", "наибольший", "наименьший" и другие формулировки оптимизации.</li>
<li>В условии задачи фигурируют слова «максимальный», «минимальный», «наибольший», «наименьший» и другие формулировки оптимизации.</li>
<li>Состояния задачи можно описать списком, многомерной матрицей или деревом, и между состоянием и соседними состояниями существует рекуррентная зависимость.</li>
</ul>
<p>Соответственно, существуют и некоторые "минусы".</p>
<p>Соответственно, существуют и некоторые «минусы».</p>
<ul>
<li>Цель задачи состоит в поиске всех возможных решений, а не одного оптимального решения.</li>
<li>В формулировке явно присутствуют признаки комбинаторного перечисления, и требуется вернуть сразу много конкретных вариантов.</li>
</ul>
<p>Если задача удовлетворяет модели дерева решений и имеет достаточно явные "плюсы", мы можем предположить, что это задача динамического программирования, а затем проверить это предположение уже в процессе решения.</p>
<p>Если задача удовлетворяет модели дерева решений и имеет достаточно явные «плюсы», мы можем предположить, что это задача динамического программирования, а затем проверить это предположение уже в процессе решения.</p>
<h2 id="1432">14.3.2 &nbsp; Этапы решения задачи<a class="headerlink" href="#1432" title="Permanent link">&para;</a></h2>
<p>Конкретный процесс решения задач динамического программирования зависит от природы и сложности задачи, но обычно включает следующие шаги: описание решений, определение состояний, построение таблицы <span class="arithmatex">\(dp\)</span> , вывод уравнения перехода состояния, определение граничных условий и порядка переходов.</p>
<p>Чтобы нагляднее показать этот процесс, рассмотрим классическую задачу "минимальная сумма пути".</p>
<p>Чтобы нагляднее показать этот процесс, рассмотрим классическую задачу «минимальная сумма пути».</p>
<div class="admonition question">
<p class="admonition-title">Question</p>
<p>Дана двумерная сетка <code>grid</code> размера <span class="arithmatex">\(n \times m\)</span> , в каждой клетке которой записано неотрицательное целое число, означающее стоимость прохождения через эту клетку. Робот стартует из левой верхней клетки и за один шаг может двигаться только вправо или вниз, пока не достигнет правой нижней клетки. Верните минимальную сумму пути от левой верхней клетки до правой нижней.</p>
@@ -4489,7 +4489,7 @@
<p align="center"> Рисунок 14-10 &nbsp; Пример данных для задачи о минимальной сумме пути </p>
<p><strong>Шаг 1: понять решения на каждом раунде, определить состояние и тем самым получить таблицу <span class="arithmatex">\(dp\)</span></strong></p>
<p>В этой задаче на каждом раунде решение состоит в том, чтобы из текущей клетки сделать один шаг вниз или вправо. Пусть индексы строки и столбца текущей клетки равны <span class="arithmatex">\([i, j]\)</span> ; тогда после шага вниз или вправо индексы становятся равными <span class="arithmatex">\([i+1, j]\)</span> или <span class="arithmatex">\([i, j+1]\)</span> . Значит, состояние должно включать два переменных индекса: строки и столбца, то есть состояние обозначается как <span class="arithmatex">\([i, j]\)</span> .</p>
<p>В этой задаче на каждом раунде решение состоит в том, чтобы из текущей клетки сделать один шаг вниз или вправо. Пусть индексы строки и столбца текущей клетки равны <span class="arithmatex">\([i, j]\)</span>. Тогда после шага вниз или вправо индексы становятся равными <span class="arithmatex">\([i+1, j]\)</span> или <span class="arithmatex">\([i, j+1]\)</span> . Значит, состояние должно включать два переменных индекса: строки и столбца, то есть состояние обозначается как <span class="arithmatex">\([i, j]\)</span> .</p>
<p>Подзадача, соответствующая состоянию <span class="arithmatex">\([i, j]\)</span> , такова: минимальная сумма пути от стартовой клетки <span class="arithmatex">\([0, 0]\)</span> до клетки <span class="arithmatex">\([i, j]\)</span> . Ее решение обозначается через <span class="arithmatex">\(dp[i, j]\)</span> .</p>
<p>На этом этапе мы получаем двумерную матрицу <span class="arithmatex">\(dp\)</span> , показанную на рисунке 14-11, размер которой совпадает с размером входной сетки <code>grid</code> .</p>
<p><img alt="Определение состояния и таблицы dp" class="animation-figure" src="../dp_solution_pipeline.assets/min_path_sum_solution_state_definition.png" /></p>
@@ -4498,7 +4498,7 @@
<div class="admonition note">
<p class="admonition-title">Note</p>
<p>Как в динамическом программировании, так и в поиске с возвратом, решение задачи можно описать как последовательность решений, а состояние образуется всеми переменными решений. Оно должно содержать всю информацию, достаточную для вывода следующего состояния.</p>
<p>Каждому состоянию соответствует некоторая подзадача, и для хранения решений всех подзадач мы определяем таблицу <span class="arithmatex">\(dp\)</span> ; каждая независимая переменная состояния становится одним измерением таблицы <span class="arithmatex">\(dp\)</span> . По сути таблица <span class="arithmatex">\(dp\)</span> - это отображение от состояния к решению соответствующей подзадачи.</p>
<p>Каждому состоянию соответствует некоторая подзадача, и для хранения решений всех подзадач мы определяем таблицу <span class="arithmatex">\(dp\)</span>. Каждая независимая переменная состояния становится одним измерением таблицы <span class="arithmatex">\(dp\)</span> . По сути таблица <span class="arithmatex">\(dp\)</span> - это отображение от состояния к решению соответствующей подзадачи.</p>
</div>
<p><strong>Шаг 2: найти оптимальную подструктуру и на ее основе вывести уравнение перехода состояния</strong></p>
<p>Для состояния <span class="arithmatex">\([i, j]\)</span> возможны только два источника: клетка сверху <span class="arithmatex">\([i-1, j]\)</span> и клетка слева <span class="arithmatex">\([i, j-1]\)</span> . Следовательно, оптимальная подструктура выглядит так: минимальная сумма пути до <span class="arithmatex">\([i, j]\)</span> определяется меньшим из двух значений - минимальной суммы пути до <span class="arithmatex">\([i-1, j]\)</span> и минимальной суммы пути до <span class="arithmatex">\([i, j-1]\)</span> .</p>
@@ -4525,7 +4525,7 @@ dp[i, j] = \min(dp[i-1, j], dp[i, j-1]) + grid[i, j]
<p>В динамическом программировании граничные условия используются для инициализации таблицы <span class="arithmatex">\(dp\)</span> , а в поиске - для обрезки.</p>
<p>Смысл порядка перехода состояния в том, чтобы к моменту вычисления текущей подзадачи все более мелкие подзадачи, от которых она зависит, уже были вычислены корректно.</p>
</div>
<p>После этого анализа мы уже можем напрямую написать код динамического программирования. Однако разложение на подзадачи - это мышление "сверху вниз", поэтому с точки зрения мышления более естественно реализовывать задачу в порядке "полный перебор <span class="arithmatex">\(\rightarrow\)</span> поиск с мемоизацией <span class="arithmatex">\(\rightarrow\)</span> динамическое программирование".</p>
<p>После этого анализа мы уже можем напрямую написать код динамического программирования. Однако разложение на подзадачи - это мышление «сверху вниз», поэтому с точки зрения мышления более естественно реализовывать задачу в порядке «полный перебор <span class="arithmatex">\(\rightarrow\)</span> поиск с мемоизацией <span class="arithmatex">\(\rightarrow\)</span> динамическое программирование».</p>
<h3 id="1-1">1. &nbsp; Метод 1: полный перебор<a class="headerlink" href="#1-1" title="Permanent link">&para;</a></h3>
<p>Начав со состояния <span class="arithmatex">\([i, j]\)</span> , мы непрерывно раскладываем его на меньшие состояния <span class="arithmatex">\([i-1, j]\)</span> и <span class="arithmatex">\([i, j-1]\)</span> . Рекурсивная функция при этом имеет следующие элементы.</p>
<ul>
@@ -4789,7 +4789,7 @@ dp[i, j] = \min(dp[i-1, j], dp[i, j-1]) + grid[i, j]
<p><div style="height: 549px; width: 100%;"><iframe class="pythontutor-iframe" src="https://pythontutor.com/iframe-embed.html#code=from%20math%20import%20inf%0A%0Adef%20min_path_sum_dfs%28grid%3A%20list%5Blist%5Bint%5D%5D%2C%20i%3A%20int%2C%20j%3A%20int%29%20-%3E%20int%3A%0A%20%20%20%20%22%22%22%D0%9C%D0%B8%D0%BD%D0%B8%D0%BC%D0%B0%D0%BB%D1%8C%D0%BD%D0%B0%D1%8F%20%D1%81%D1%83%D0%BC%D0%BC%D0%B0%20%D0%BF%D1%83%D1%82%D0%B8%3A%20%D0%BF%D0%BE%D0%BB%D0%BD%D1%8B%D0%B9%20%D0%BF%D0%B5%D1%80%D0%B5%D0%B1%D0%BE%D1%80%22%22%22%0A%20%20%20%20%23%20%D0%95%D1%81%D0%BB%D0%B8%20%D1%8D%D1%82%D0%BE%20%D0%B2%D0%B5%D1%80%D1%85%D0%BD%D1%8F%D1%8F%20%D0%BB%D0%B5%D0%B2%D0%B0%D1%8F%20%D1%8F%D1%87%D0%B5%D0%B9%D0%BA%D0%B0%2C%20%D0%B7%D0%B0%D0%B2%D0%B5%D1%80%D1%88%D0%B8%D1%82%D1%8C%20%D0%BF%D0%BE%D0%B8%D1%81%D0%BA%0A%20%20%20%20if%20i%20%3D%3D%200%20and%20j%20%3D%3D%200%3A%0A%20%20%20%20%20%20%20%20return%20grid%5B0%5D%5B0%5D%0A%20%20%20%20%23%20%D0%95%D1%81%D0%BB%D0%B8%20%D0%B8%D0%BD%D0%B4%D0%B5%D0%BA%D1%81%D1%8B%20%D1%81%D1%82%D1%80%D0%BE%D0%BA%D0%B8%20%D0%B8%D0%BB%D0%B8%20%D1%81%D1%82%D0%BE%D0%BB%D0%B1%D1%86%D0%B0%20%D0%B2%D1%8B%D1%85%D0%BE%D0%B4%D1%8F%D1%82%20%D0%B7%D0%B0%20%D0%B3%D1%80%D0%B0%D0%BD%D0%B8%D1%86%D1%8B%2C%20%D0%B2%D0%B5%D1%80%D0%BD%D1%83%D1%82%D1%8C%20%D1%81%D1%82%D0%BE%D0%B8%D0%BC%D0%BE%D1%81%D1%82%D1%8C%20%2B%E2%88%9E%0A%20%20%20%20if%20i%20%3C%200%20or%20j%20%3C%200%3A%0A%20%20%20%20%20%20%20%20return%20inf%0A%20%20%20%20%23%20%D0%92%D1%8B%D1%87%D0%B8%D1%81%D0%BB%D0%B8%D1%82%D1%8C%20%D0%BC%D0%B8%D0%BD%D0%B8%D0%BC%D0%B0%D0%BB%D1%8C%D0%BD%D1%83%D1%8E%20%D1%81%D1%82%D0%BE%D0%B8%D0%BC%D0%BE%D1%81%D1%82%D1%8C%20%D0%BF%D1%83%D1%82%D0%B8%20%D0%B8%D0%B7%20%D0%BB%D0%B5%D0%B2%D0%BE%D0%B3%D0%BE%20%D0%B2%D0%B5%D1%80%D1%85%D0%BD%D0%B5%D0%B3%D0%BE%20%D1%83%D0%B3%D0%BB%D0%B0%20%D0%B4%D0%BE%20%28i-1%2C%20j%29%20%D0%B8%20%28i%2C%20j-1%29%0A%20%20%20%20up%20%3D%20min_path_sum_dfs%28grid%2C%20i%20-%201%2C%20j%29%0A%20%20%20%20left%20%3D%20min_path_sum_dfs%28grid%2C%20i%2C%20j%20-%201%29%0A%20%20%20%20%23%20%D0%92%D0%B5%D1%80%D0%BD%D1%83%D1%82%D1%8C%20%D0%BC%D0%B8%D0%BD%D0%B8%D0%BC%D0%B0%D0%BB%D1%8C%D0%BD%D1%83%D1%8E%20%D1%81%D1%82%D0%BE%D0%B8%D0%BC%D0%BE%D1%81%D1%82%D1%8C%20%D0%BF%D1%83%D1%82%D0%B8%20%D0%B8%D0%B7%20%D0%BB%D0%B5%D0%B2%D0%BE%D0%B3%D0%BE%20%D0%B2%D0%B5%D1%80%D1%85%D0%BD%D0%B5%D0%B3%D0%BE%20%D1%83%D0%B3%D0%BB%D0%B0%20%D0%B4%D0%BE%20%28i%2C%20j%29%0A%20%20%20%20return%20min%28left%2C%20up%29%20%2B%20grid%5Bi%5D%5Bj%5D%0A%0A%0A%22%22%22Driver%20Code%22%22%22%0Aif%20__name__%20%3D%3D%20%22__main__%22%3A%0A%20%20%20%20grid%20%3D%20%5B%5B1%2C%203%2C%201%2C%205%5D%2C%20%5B2%2C%202%2C%204%2C%202%5D%2C%20%5B5%2C%203%2C%202%2C%201%5D%2C%20%5B4%2C%203%2C%205%2C%202%5D%5D%0A%20%20%20%20n%2C%20m%20%3D%20len%28grid%29%2C%20len%28grid%5B0%5D%29%0A%0A%20%20%20%20%23%20%D0%9F%D0%BE%D0%BB%D0%BD%D1%8B%D0%B9%20%D0%BF%D0%B5%D1%80%D0%B5%D0%B1%D0%BE%D1%80%0A%20%20%20%20res%20%3D%20min_path_sum_dfs%28grid%2C%20n%20-%201%2C%20m%20-%201%29%0A%20%20%20%20print%28f%22%D0%9C%D0%B8%D0%BD%D0%B8%D0%BC%D0%B0%D0%BB%D1%8C%D0%BD%D0%B0%D1%8F%20%D1%81%D1%83%D0%BC%D0%BC%D0%B0%20%D0%BF%D1%83%D1%82%D0%B8%20%D0%B8%D0%B7%20%D0%BB%D0%B5%D0%B2%D0%BE%D0%B3%D0%BE%20%D0%B2%D0%B5%D1%80%D1%85%D0%BD%D0%B5%D0%B3%D0%BE%20%D1%83%D0%B3%D0%BB%D0%B0%20%D0%B2%20%D0%BF%D1%80%D0%B0%D0%B2%D1%8B%D0%B9%20%D0%BD%D0%B8%D0%B6%D0%BD%D0%B8%D0%B9%20%3D%20%7Bres%7D%22%29&codeDivHeight=472&codeDivWidth=350&cumulative=false&curInstr=6&heapPrimitives=nevernest&origin=opt-frontend.js&py=311&rawInputLstJSON=%5B%5D&textReferences=false"> </iframe></div>
<div style="margin-top: 5px;"><a href="https://pythontutor.com/iframe-embed.html#code=from%20math%20import%20inf%0A%0Adef%20min_path_sum_dfs%28grid%3A%20list%5Blist%5Bint%5D%5D%2C%20i%3A%20int%2C%20j%3A%20int%29%20-%3E%20int%3A%0A%20%20%20%20%22%22%22%D0%9C%D0%B8%D0%BD%D0%B8%D0%BC%D0%B0%D0%BB%D1%8C%D0%BD%D0%B0%D1%8F%20%D1%81%D1%83%D0%BC%D0%BC%D0%B0%20%D0%BF%D1%83%D1%82%D0%B8%3A%20%D0%BF%D0%BE%D0%BB%D0%BD%D1%8B%D0%B9%20%D0%BF%D0%B5%D1%80%D0%B5%D0%B1%D0%BE%D1%80%22%22%22%0A%20%20%20%20%23%20%D0%95%D1%81%D0%BB%D0%B8%20%D1%8D%D1%82%D0%BE%20%D0%B2%D0%B5%D1%80%D1%85%D0%BD%D1%8F%D1%8F%20%D0%BB%D0%B5%D0%B2%D0%B0%D1%8F%20%D1%8F%D1%87%D0%B5%D0%B9%D0%BA%D0%B0%2C%20%D0%B7%D0%B0%D0%B2%D0%B5%D1%80%D1%88%D0%B8%D1%82%D1%8C%20%D0%BF%D0%BE%D0%B8%D1%81%D0%BA%0A%20%20%20%20if%20i%20%3D%3D%200%20and%20j%20%3D%3D%200%3A%0A%20%20%20%20%20%20%20%20return%20grid%5B0%5D%5B0%5D%0A%20%20%20%20%23%20%D0%95%D1%81%D0%BB%D0%B8%20%D0%B8%D0%BD%D0%B4%D0%B5%D0%BA%D1%81%D1%8B%20%D1%81%D1%82%D1%80%D0%BE%D0%BA%D0%B8%20%D0%B8%D0%BB%D0%B8%20%D1%81%D1%82%D0%BE%D0%BB%D0%B1%D1%86%D0%B0%20%D0%B2%D1%8B%D1%85%D0%BE%D0%B4%D1%8F%D1%82%20%D0%B7%D0%B0%20%D0%B3%D1%80%D0%B0%D0%BD%D0%B8%D1%86%D1%8B%2C%20%D0%B2%D0%B5%D1%80%D0%BD%D1%83%D1%82%D1%8C%20%D1%81%D1%82%D0%BE%D0%B8%D0%BC%D0%BE%D1%81%D1%82%D1%8C%20%2B%E2%88%9E%0A%20%20%20%20if%20i%20%3C%200%20or%20j%20%3C%200%3A%0A%20%20%20%20%20%20%20%20return%20inf%0A%20%20%20%20%23%20%D0%92%D1%8B%D1%87%D0%B8%D1%81%D0%BB%D0%B8%D1%82%D1%8C%20%D0%BC%D0%B8%D0%BD%D0%B8%D0%BC%D0%B0%D0%BB%D1%8C%D0%BD%D1%83%D1%8E%20%D1%81%D1%82%D0%BE%D0%B8%D0%BC%D0%BE%D1%81%D1%82%D1%8C%20%D0%BF%D1%83%D1%82%D0%B8%20%D0%B8%D0%B7%20%D0%BB%D0%B5%D0%B2%D0%BE%D0%B3%D0%BE%20%D0%B2%D0%B5%D1%80%D1%85%D0%BD%D0%B5%D0%B3%D0%BE%20%D1%83%D0%B3%D0%BB%D0%B0%20%D0%B4%D0%BE%20%28i-1%2C%20j%29%20%D0%B8%20%28i%2C%20j-1%29%0A%20%20%20%20up%20%3D%20min_path_sum_dfs%28grid%2C%20i%20-%201%2C%20j%29%0A%20%20%20%20left%20%3D%20min_path_sum_dfs%28grid%2C%20i%2C%20j%20-%201%29%0A%20%20%20%20%23%20%D0%92%D0%B5%D1%80%D0%BD%D1%83%D1%82%D1%8C%20%D0%BC%D0%B8%D0%BD%D0%B8%D0%BC%D0%B0%D0%BB%D1%8C%D0%BD%D1%83%D1%8E%20%D1%81%D1%82%D0%BE%D0%B8%D0%BC%D0%BE%D1%81%D1%82%D1%8C%20%D0%BF%D1%83%D1%82%D0%B8%20%D0%B8%D0%B7%20%D0%BB%D0%B5%D0%B2%D0%BE%D0%B3%D0%BE%20%D0%B2%D0%B5%D1%80%D1%85%D0%BD%D0%B5%D0%B3%D0%BE%20%D1%83%D0%B3%D0%BB%D0%B0%20%D0%B4%D0%BE%20%28i%2C%20j%29%0A%20%20%20%20return%20min%28left%2C%20up%29%20%2B%20grid%5Bi%5D%5Bj%5D%0A%0A%0A%22%22%22Driver%20Code%22%22%22%0Aif%20__name__%20%3D%3D%20%22__main__%22%3A%0A%20%20%20%20grid%20%3D%20%5B%5B1%2C%203%2C%201%2C%205%5D%2C%20%5B2%2C%202%2C%204%2C%202%5D%2C%20%5B5%2C%203%2C%202%2C%201%5D%2C%20%5B4%2C%203%2C%205%2C%202%5D%5D%0A%20%20%20%20n%2C%20m%20%3D%20len%28grid%29%2C%20len%28grid%5B0%5D%29%0A%0A%20%20%20%20%23%20%D0%9F%D0%BE%D0%BB%D0%BD%D1%8B%D0%B9%20%D0%BF%D0%B5%D1%80%D0%B5%D0%B1%D0%BE%D1%80%0A%20%20%20%20res%20%3D%20min_path_sum_dfs%28grid%2C%20n%20-%201%2C%20m%20-%201%29%0A%20%20%20%20print%28f%22%D0%9C%D0%B8%D0%BD%D0%B8%D0%BC%D0%B0%D0%BB%D1%8C%D0%BD%D0%B0%D1%8F%20%D1%81%D1%83%D0%BC%D0%BC%D0%B0%20%D0%BF%D1%83%D1%82%D0%B8%20%D0%B8%D0%B7%20%D0%BB%D0%B5%D0%B2%D0%BE%D0%B3%D0%BE%20%D0%B2%D0%B5%D1%80%D1%85%D0%BD%D0%B5%D0%B3%D0%BE%20%D1%83%D0%B3%D0%BB%D0%B0%20%D0%B2%20%D0%BF%D1%80%D0%B0%D0%B2%D1%8B%D0%B9%20%D0%BD%D0%B8%D0%B6%D0%BD%D0%B8%D0%B9%20%3D%20%7Bres%7D%22%29&codeDivHeight=800&codeDivWidth=600&cumulative=false&curInstr=6&heapPrimitives=nevernest&origin=opt-frontend.js&py=311&rawInputLstJSON=%5B%5D&textReferences=false" target="_blank" rel="noopener noreferrer">Во весь экран &gt;</a></div></p>
</details>
<p>На рисунке 14-14 показано дерево рекурсии с корнем в <span class="arithmatex">\(dp[2, 1]\)</span> ; в нем содержатся перекрывающиеся подзадачи, и их число будет резко расти вместе с размером сетки <code>grid</code> .</p>
<p>На рисунке 14-14 показано дерево рекурсии с корнем в <span class="arithmatex">\(dp[2, 1]\)</span>. В нем содержатся перекрывающиеся подзадачи, и их число будет резко расти вместе с размером сетки <code>grid</code> .</p>
<p>По своей сути причина появления перекрывающихся подзадач такова: <strong>существует много разных путей от левого верхнего угла до одной и той же клетки</strong>.</p>
<p><img alt="Дерево рекурсии полного перебора" class="animation-figure" src="../dp_solution_pipeline.assets/min_path_sum_dfs.png" /></p>
<p align="center"> Рисунок 14-14 &nbsp; Дерево рекурсии полного перебора </p>
@@ -4385,7 +4385,7 @@
<p>Даны две строки <span class="arithmatex">\(s\)</span> и <span class="arithmatex">\(t\)</span> . Верните минимальное число шагов редактирования, необходимое для преобразования <span class="arithmatex">\(s\)</span> в <span class="arithmatex">\(t\)</span> .</p>
<p>Для строки допускаются три операции редактирования: вставка одного символа, удаление одного символа и замена одного символа на произвольный другой символ.</p>
</div>
<p>Как показано на рисунке 14-27, для преобразования <code>kitten</code> в <code>sitting</code> требуется 3 шага редактирования: 2 операции замены и 1 операция вставки; для преобразования <code>hello</code> в <code>algo</code> также требуется 3 шага: 2 замены и 1 удаление.</p>
<p>Как показано на рисунке 14-27, для преобразования <code>kitten</code> в <code>sitting</code> требуется 3 шага редактирования: 2 операции замены и 1 операция вставки. Для преобразования <code>hello</code> в <code>algo</code> также требуется 3 шага: 2 замены и 1 удаление.</p>
<p><img alt="Пример данных для задачи о расстоянии редактирования" class="animation-figure" src="../edit_distance_problem.assets/edit_distance_example.png" /></p>
<p align="center"> Рисунок 14-27 &nbsp; Пример данных для задачи о расстоянии редактирования </p>
@@ -4398,7 +4398,7 @@
<h3 id="1">1. &nbsp; Идея динамического программирования<a class="headerlink" href="#1" title="Permanent link">&para;</a></h3>
<p><strong>Шаг 1: продумать решения на каждом раунде, определить состояние и тем самым получить таблицу <span class="arithmatex">\(dp\)</span></strong></p>
<p>На каждом раунде решение состоит в выполнении одной операции редактирования над строкой <span class="arithmatex">\(s\)</span> .</p>
<p>Нам нужно, чтобы в ходе выполнения операций размер задачи постепенно уменьшался; только тогда можно строить подзадачи. Пусть длины строк <span class="arithmatex">\(s\)</span> и <span class="arithmatex">\(t\)</span> равны соответственно <span class="arithmatex">\(n\)</span> и <span class="arithmatex">\(m\)</span> ; сначала рассмотрим последние символы этих строк, то есть <span class="arithmatex">\(s[n-1]\)</span> и <span class="arithmatex">\(t[m-1]\)</span> .</p>
<p>Нам нужно, чтобы в ходе выполнения операций размер задачи постепенно уменьшался. Только тогда можно строить подзадачи. Пусть длины строк <span class="arithmatex">\(s\)</span> и <span class="arithmatex">\(t\)</span> равны соответственно <span class="arithmatex">\(n\)</span> и <span class="arithmatex">\(m\)</span>. Сначала рассмотрим последние символы этих строк, то есть <span class="arithmatex">\(s[n-1]\)</span> и <span class="arithmatex">\(t[m-1]\)</span> .</p>
<ul>
<li>Если <span class="arithmatex">\(s[n-1]\)</span> и <span class="arithmatex">\(t[m-1]\)</span> совпадают, их можно просто пропустить и сразу перейти к сравнению <span class="arithmatex">\(s[n-2]\)</span> и <span class="arithmatex">\(t[m-2]\)</span> .</li>
<li>Если <span class="arithmatex">\(s[n-1]\)</span> и <span class="arithmatex">\(t[m-1]\)</span> различны, нужно выполнить над <span class="arithmatex">\(s\)</span> одну операцию редактирования (вставку, удаление или замену), чтобы последние символы стали одинаковыми, после чего можно перейти к задаче меньшего размера.</li>
@@ -4409,9 +4409,9 @@
<p><strong>Шаг 2: найти оптимальную подструктуру и на ее основе вывести уравнение перехода состояния</strong></p>
<p>Рассмотрим подзадачу <span class="arithmatex">\(dp[i, j]\)</span> . Ее последние символы - это <span class="arithmatex">\(s[i-1]\)</span> и <span class="arithmatex">\(t[j-1]\)</span> . В зависимости от операции редактирования возможны три случая, показанные на рисунке 14-29.</p>
<ol>
<li>Вставить после <span class="arithmatex">\(s[i-1]\)</span> символ <span class="arithmatex">\(t[j-1]\)</span> ; тогда остается подзадача <span class="arithmatex">\(dp[i, j-1]\)</span> .</li>
<li>Удалить <span class="arithmatex">\(s[i-1]\)</span> ; тогда остается подзадача <span class="arithmatex">\(dp[i-1, j]\)</span> .</li>
<li>Заменить <span class="arithmatex">\(s[i-1]\)</span> на <span class="arithmatex">\(t[j-1]\)</span> ; тогда остается подзадача <span class="arithmatex">\(dp[i-1, j-1]\)</span> .</li>
<li>Вставить после <span class="arithmatex">\(s[i-1]\)</span> символ <span class="arithmatex">\(t[j-1]\)</span>. Тогда остается подзадача <span class="arithmatex">\(dp[i, j-1]\)</span> .</li>
<li>Удалить <span class="arithmatex">\(s[i-1]\)</span>. Тогда остается подзадача <span class="arithmatex">\(dp[i-1, j]\)</span> .</li>
<li>Заменить <span class="arithmatex">\(s[i-1]\)</span> на <span class="arithmatex">\(t[j-1]\)</span>. Тогда остается подзадача <span class="arithmatex">\(dp[i-1, j-1]\)</span> .</li>
</ol>
<p><img alt="Переходы состояния в задаче о расстоянии редактирования" class="animation-figure" src="../edit_distance_problem.assets/edit_distance_state_transfer.png" /></p>
<p align="center"> Рисунок 14-29 &nbsp; Переходы состояния в задаче о расстоянии редактирования </p>
@@ -4865,7 +4865,7 @@ dp[i, j] = dp[i-1, j-1]
<h3 id="3">3. &nbsp; Оптимизация пространства<a class="headerlink" href="#3" title="Permanent link">&para;</a></h3>
<p>Поскольку <span class="arithmatex">\(dp[i,j]\)</span> зависит от значения сверху <span class="arithmatex">\(dp[i-1, j]\)</span> , слева <span class="arithmatex">\(dp[i, j-1]\)</span> и слева сверху <span class="arithmatex">\(dp[i-1, j-1]\)</span> , прямой обход после оптимизации памяти теряет значение слева сверху, а обратный обход не позволяет заранее построить значение слева <span class="arithmatex">\(dp[i, j-1]\)</span> . Значит, оба наивных варианта обхода здесь непригодны.</p>
<p>Чтобы решить эту проблему, можно использовать переменную <code>leftup</code> для временного сохранения значения слева сверху <span class="arithmatex">\(dp[i-1, j-1]\)</span> ; после этого остается учитывать только верхнее и левое значения. Тогда ситуация становится аналогичной задаче о полном рюкзаке, и можно выполнять прямой обход. Код приведен ниже:</p>
<p>Чтобы решить эту проблему, можно использовать переменную <code>leftup</code> для временного сохранения значения слева сверху <span class="arithmatex">\(dp[i-1, j-1]\)</span>. После этого остается учитывать только верхнее и левое значения. Тогда ситуация становится аналогичной задаче о полном рюкзаке, и можно выполнять прямой обход. Код приведен ниже:</p>
<div class="tabbed-set tabbed-alternate" data-tabs="3:13"><input checked="checked" id="__tabbed_3_1" name="__tabbed_3" type="radio" /><input id="__tabbed_3_2" name="__tabbed_3" type="radio" /><input id="__tabbed_3_3" name="__tabbed_3" type="radio" /><input id="__tabbed_3_4" name="__tabbed_3" type="radio" /><input id="__tabbed_3_5" name="__tabbed_3" type="radio" /><input id="__tabbed_3_6" name="__tabbed_3" type="radio" /><input id="__tabbed_3_7" name="__tabbed_3" type="radio" /><input id="__tabbed_3_8" name="__tabbed_3" type="radio" /><input id="__tabbed_3_9" name="__tabbed_3" type="radio" /><input id="__tabbed_3_10" name="__tabbed_3" type="radio" /><input id="__tabbed_3_11" name="__tabbed_3" type="radio" /><input id="__tabbed_3_12" name="__tabbed_3" type="radio" /><input id="__tabbed_3_13" name="__tabbed_3" type="radio" /><div class="tabbed-labels"><label for="__tabbed_3_1">Python</label><label for="__tabbed_3_2">C++</label><label for="__tabbed_3_3">Java</label><label for="__tabbed_3_4">C#</label><label for="__tabbed_3_5">Go</label><label for="__tabbed_3_6">Swift</label><label for="__tabbed_3_7">JS</label><label for="__tabbed_3_8">TS</label><label for="__tabbed_3_9">Dart</label><label for="__tabbed_3_10">Rust</label><label for="__tabbed_3_11">C</label><label for="__tabbed_3_12">Kotlin</label><label for="__tabbed_3_13">Ruby</label></div>
<div class="tabbed-content">
<div class="tabbed-block">
@@ -4411,7 +4411,7 @@
<p><img alt="Число способов подняться на 3-ю ступень" class="animation-figure" src="../intro_to_dynamic_programming.assets/climbing_stairs_example.png" /></p>
<p align="center"> Рисунок 14-1 &nbsp; Число способов подняться на 3-ю ступень </p>
<p>Цель этой задачи - вычислить количество способов. <strong>Поэтому можно попробовать использовать для ее решения метод поиска с возвратом</strong>. Если представить подъем по лестнице как последовательность решений, то мы начинаем от земли и на каждом раунде выбираем прыжок на <span class="arithmatex">\(1\)</span> или на <span class="arithmatex">\(2\)</span> ступени; всякий раз, когда достигаем вершины, увеличиваем число способов на <span class="arithmatex">\(1\)</span> , а если перескакиваем вершину, обрезаем эту ветвь. Код выглядит так:</p>
<p>Цель этой задачи - вычислить количество способов. <strong>Поэтому можно попробовать использовать для ее решения метод поиска с возвратом</strong>. Если представить подъем по лестнице как последовательность решений, то мы начинаем от земли и на каждом раунде выбираем прыжок на <span class="arithmatex">\(1\)</span> или на <span class="arithmatex">\(2\)</span> ступени. Всякий раз, когда достигаем вершины, увеличиваем число способов на <span class="arithmatex">\(1\)</span> , а если перескакиваем вершину, обрезаем эту ветвь. Код выглядит так:</p>
<div class="tabbed-set tabbed-alternate" data-tabs="1:13"><input checked="checked" id="__tabbed_1_1" name="__tabbed_1" type="radio" /><input id="__tabbed_1_2" name="__tabbed_1" type="radio" /><input id="__tabbed_1_3" name="__tabbed_1" type="radio" /><input id="__tabbed_1_4" name="__tabbed_1" type="radio" /><input id="__tabbed_1_5" name="__tabbed_1" type="radio" /><input id="__tabbed_1_6" name="__tabbed_1" type="radio" /><input id="__tabbed_1_7" name="__tabbed_1" type="radio" /><input id="__tabbed_1_8" name="__tabbed_1" type="radio" /><input id="__tabbed_1_9" name="__tabbed_1" type="radio" /><input id="__tabbed_1_10" name="__tabbed_1" type="radio" /><input id="__tabbed_1_11" name="__tabbed_1" type="radio" /><input id="__tabbed_1_12" name="__tabbed_1" type="radio" /><input id="__tabbed_1_13" name="__tabbed_1" type="radio" /><div class="tabbed-labels"><label for="__tabbed_1_1">Python</label><label for="__tabbed_1_2">C++</label><label for="__tabbed_1_3">Java</label><label for="__tabbed_1_4">C#</label><label for="__tabbed_1_5">Go</label><label for="__tabbed_1_6">Swift</label><label for="__tabbed_1_7">JS</label><label for="__tabbed_1_8">TS</label><label for="__tabbed_1_9">Dart</label><label for="__tabbed_1_10">Rust</label><label for="__tabbed_1_11">C</label><label for="__tabbed_1_12">Kotlin</label><label for="__tabbed_1_13">Ruby</label></div>
<div class="tabbed-content">
<div class="tabbed-block">
@@ -4796,8 +4796,8 @@
<div style="margin-top: 5px;"><a href="https://pythontutor.com/iframe-embed.html#code=def%20backtrack%28choices%3A%20list%5Bint%5D%2C%20state%3A%20int%2C%20n%3A%20int%2C%20res%3A%20list%5Bint%5D%29%20-%3E%20int%3A%0A%20%20%20%20%22%22%22%D0%91%D1%8D%D0%BA%D1%82%D1%80%D0%B5%D0%BA%D0%B8%D0%BD%D0%B3%22%22%22%0A%20%20%20%20%23%20%D0%9A%D0%BE%D0%B3%D0%B4%D0%B0%20%D0%BF%D0%BE%D0%B4%D1%8A%D0%B5%D0%BC%20%D0%B4%D0%BE%D1%81%D1%82%D0%B8%D0%B3%D0%B0%D0%B5%D1%82%20n-%D0%B9%20%D1%81%D1%82%D1%83%D0%BF%D0%B5%D0%BD%D0%B8%2C%20%D1%87%D0%B8%D1%81%D0%BB%D0%BE%20%D0%B2%D0%B0%D1%80%D0%B8%D0%B0%D0%BD%D1%82%D0%BE%D0%B2%20%D1%83%D0%B2%D0%B5%D0%BB%D0%B8%D1%87%D0%B8%D0%B2%D0%B0%D0%B5%D1%82%D1%81%D1%8F%20%D0%BD%D0%B0%201%0A%20%20%20%20if%20state%20%3D%3D%20n%3A%0A%20%20%20%20%20%20%20%20res%5B0%5D%20%2B%3D%201%0A%20%20%20%20%23%20%D0%9F%D0%B5%D1%80%D0%B5%D0%B1%D0%BE%D1%80%20%D0%B2%D1%81%D0%B5%D1%85%20%D0%B2%D0%B0%D1%80%D0%B8%D0%B0%D0%BD%D1%82%D0%BE%D0%B2%20%D0%B2%D1%8B%D0%B1%D0%BE%D1%80%D0%B0%0A%20%20%20%20for%20choice%20in%20choices%3A%0A%20%20%20%20%20%20%20%20%23%20%D0%9E%D1%82%D1%81%D0%B5%D1%87%D0%B5%D0%BD%D0%B8%D0%B5%3A%20%D0%BD%D0%B5%D0%BB%D1%8C%D0%B7%D1%8F%20%D0%B2%D1%8B%D1%85%D0%BE%D0%B4%D0%B8%D1%82%D1%8C%20%D0%B7%D0%B0%20n-%D1%8E%20%D1%81%D1%82%D1%83%D0%BF%D0%B5%D0%BD%D1%8C%0A%20%20%20%20%20%20%20%20if%20state%20%2B%20choice%20%3E%20n%3A%0A%20%20%20%20%20%20%20%20%20%20%20%20continue%0A%20%20%20%20%20%20%20%20%23%20%D0%9F%D0%BE%D0%BF%D1%8B%D1%82%D0%BA%D0%B0%3A%20%D1%81%D0%B4%D0%B5%D0%BB%D0%B0%D1%82%D1%8C%20%D0%B2%D1%8B%D0%B1%D0%BE%D1%80%20%D0%B8%20%D0%BE%D0%B1%D0%BD%D0%BE%D0%B2%D0%B8%D1%82%D1%8C%20%D1%81%D0%BE%D1%81%D1%82%D0%BE%D1%8F%D0%BD%D0%B8%D0%B5%0A%20%20%20%20%20%20%20%20backtrack%28choices%2C%20state%20%2B%20choice%2C%20n%2C%20res%29%0A%20%20%20%20%20%20%20%20%23%20%D0%9E%D1%82%D0%BA%D0%B0%D1%82%0A%0A%0Adef%20climbing_stairs_backtrack%28n%3A%20int%29%20-%3E%20int%3A%0A%20%20%20%20%22%22%22%D0%9F%D0%BE%D0%B4%D1%8A%D0%B5%D0%BC%20%D0%BF%D0%BE%20%D0%BB%D0%B5%D1%81%D1%82%D0%BD%D0%B8%D1%86%D0%B5%3A%20%D0%B1%D1%8D%D0%BA%D1%82%D1%80%D0%B5%D0%BA%D0%B8%D0%BD%D0%B3%22%22%22%0A%20%20%20%20choices%20%3D%20%5B1%2C%202%5D%20%20%23%20%D0%9C%D0%BE%D0%B6%D0%BD%D0%BE%20%D0%BF%D0%BE%D0%B4%D0%BD%D1%8F%D1%82%D1%8C%D1%81%D1%8F%20%D0%BD%D0%B0%201%20%D0%B8%D0%BB%D0%B8%202%20%D1%81%D1%82%D1%83%D0%BF%D0%B5%D0%BD%D0%B8%0A%20%20%20%20state%20%3D%200%20%20%23%20%D0%9D%D0%B0%D1%87%D0%B0%D1%82%D1%8C%20%D0%BF%D0%BE%D0%B4%D1%8A%D0%B5%D0%BC%20%D1%81%200-%D0%B9%20%D1%81%D1%82%D1%83%D0%BF%D0%B5%D0%BD%D0%B8%0A%20%20%20%20res%20%3D%20%5B0%5D%20%20%23%20%D0%98%D1%81%D0%BF%D0%BE%D0%BB%D1%8C%D0%B7%D0%BE%D0%B2%D0%B0%D1%82%D1%8C%20res%5B0%5D%20%D0%B4%D0%BB%D1%8F%20%D1%85%D1%80%D0%B0%D0%BD%D0%B5%D0%BD%D0%B8%D1%8F%20%D1%87%D0%B8%D1%81%D0%BB%D0%B0%20%D1%80%D0%B5%D1%88%D0%B5%D0%BD%D0%B8%D0%B9%0A%20%20%20%20backtrack%28choices%2C%20state%2C%20n%2C%20res%29%0A%20%20%20%20return%20res%5B0%5D%0A%0A%0A%22%22%22Driver%20Code%22%22%22%0Aif%20__name__%20%3D%3D%20%22__main__%22%3A%0A%20%20%20%20n%20%3D%204%0A%0A%20%20%20%20res%20%3D%20climbing_stairs_backtrack%28n%29%0A%20%20%20%20print%28f%22%D0%9A%D0%BE%D0%BB%D0%B8%D1%87%D0%B5%D1%81%D1%82%D0%B2%D0%BE%20%D1%81%D0%BF%D0%BE%D1%81%D0%BE%D0%B1%D0%BE%D0%B2%20%D0%BF%D0%BE%D0%B4%D0%BD%D1%8F%D1%82%D1%8C%D1%81%D1%8F%20%D0%BF%D0%BE%20%D0%BB%D0%B5%D1%81%D1%82%D0%BD%D0%B8%D1%86%D0%B5%20%D0%B8%D0%B7%20%7Bn%7D%20%D1%81%D1%82%D1%83%D0%BF%D0%B5%D0%BD%D0%B5%D0%B9%20%3D%20%7Bres%7D%22%29&codeDivHeight=800&codeDivWidth=600&cumulative=false&curInstr=5&heapPrimitives=nevernest&origin=opt-frontend.js&py=311&rawInputLstJSON=%5B%5D&textReferences=false" target="_blank" rel="noopener noreferrer">Во весь экран &gt;</a></div></p>
</details>
<h2 id="1411-1">14.1.1 &nbsp; Метод 1: полный перебор<a class="headerlink" href="#1411-1" title="Permanent link">&para;</a></h2>
<p>Алгоритм поиска с возвратом обычно не раскладывает задачу явно на подзадачи; вместо этого он рассматривает решение как последовательность решений, используя попытки и обрезку для поиска всех возможных ответов.</p>
<p>Попробуем посмотреть на задачу именно как на разложение подзадач. Пусть число способов добраться до ступени <span class="arithmatex">\(i\)</span> равно <span class="arithmatex">\(dp[i]\)</span> ; тогда <span class="arithmatex">\(dp[i]\)</span> - это исходная задача, а ее подзадачи включают:</p>
<p>Алгоритм поиска с возвратом обычно не раскладывает задачу явно на подзадачи. Вместо этого он рассматривает решение как последовательность решений, используя попытки и обрезку для поиска всех возможных ответов.</p>
<p>Попробуем посмотреть на задачу именно как на разложение подзадач. Пусть число способов добраться до ступени <span class="arithmatex">\(i\)</span> равно <span class="arithmatex">\(dp[i]\)</span>. Тогда <span class="arithmatex">\(dp[i]\)</span> - это исходная задача, а ее подзадачи включают:</p>
<div class="arithmatex">\[
dp[i-1], dp[i-2], \dots, dp[2], dp[1]
\]</div>
@@ -5041,7 +5041,7 @@ dp[i] = dp[i-1] + dp[i-2]
<p><img alt="Дерево рекурсии для подъема по лестнице" class="animation-figure" src="../intro_to_dynamic_programming.assets/climbing_stairs_dfs_tree.png" /></p>
<p align="center"> Рисунок 14-3 &nbsp; Дерево рекурсии для подъема по лестнице </p>
<p>Если посмотреть на рисунок 14-3, то видно, что <strong>экспоненциальная временная сложность порождается "перекрывающимися подзадачами"</strong>. Например, <span class="arithmatex">\(dp[9]\)</span> раскладывается в <span class="arithmatex">\(dp[8]\)</span> и <span class="arithmatex">\(dp[7]\)</span> , а <span class="arithmatex">\(dp[8]\)</span> - в <span class="arithmatex">\(dp[7]\)</span> и <span class="arithmatex">\(dp[6]\)</span> ; обе ветви содержат подзадачу <span class="arithmatex">\(dp[7]\)</span> .</p>
<p>Как видно на рисунке 14-3, <strong>экспоненциальная временная сложность порождается «перекрывающимися подзадачами»</strong>. Например, <span class="arithmatex">\(dp[9]\)</span> раскладывается в <span class="arithmatex">\(dp[8]\)</span> и <span class="arithmatex">\(dp[7]\)</span> , а <span class="arithmatex">\(dp[8]\)</span> - в <span class="arithmatex">\(dp[7]\)</span> и <span class="arithmatex">\(dp[6]\)</span>. Обе ветви содержат подзадачу <span class="arithmatex">\(dp[7]\)</span> .</p>
<p>Продолжая это рассуждение, мы видим, что подзадачи порождают все более мелкие перекрывающиеся подзадачи без конца. Подавляющая часть вычислительных ресурсов уходит именно на них.</p>
<h2 id="1412-2">14.1.2 &nbsp; Метод 2: поиск с мемоизацией<a class="headerlink" href="#1412-2" title="Permanent link">&para;</a></h2>
<p>Чтобы ускорить алгоритм, <strong>мы хотим, чтобы каждая перекрывающаяся подзадача вычислялась только один раз</strong>. Для этого объявим массив <code>mem</code> для хранения решения каждой подзадачи и будем обрезать повторные вычисления в процессе поиска.</p>
@@ -5381,9 +5381,9 @@ dp[i] = dp[i-1] + dp[i-2]
<p align="center"> Рисунок 14-4 &nbsp; Дерево рекурсии для поиска с мемоизацией </p>
<h2 id="1413-3">14.1.3 &nbsp; Метод 3: динамическое программирование<a class="headerlink" href="#1413-3" title="Permanent link">&para;</a></h2>
<p><strong>Поиск с мемоизацией - это метод "сверху вниз"</strong> : мы начинаем с исходной задачи (корня), рекурсивно раскладываем более крупные подзадачи на меньшие, пока не достигнем наименьших подзадач с уже известным ответом (листьев). Затем в процессе возврата постепенно собираем решения подзадач и тем самым получаем решение исходной задачи.</p>
<p>Напротив, <strong>динамическое программирование - это метод "снизу вверх"</strong> : начиная с решений наименьших подзадач, мы итеративно строим решения для более крупных подзадач, пока не получим ответ на исходную задачу.</p>
<p>Поскольку в динамическом программировании нет этапа возврата, для его реализации достаточно обычных циклов, без рекурсии. В приведенном ниже коде мы инициализируем массив <code>dp</code> для хранения решений подзадач; он выполняет ту же роль, что и массив <code>mem</code> в мемоизированном поиске:</p>
<p><strong>Поиск с мемоизацией - это метод «сверху вниз»</strong> : мы начинаем с исходной задачи (корня), рекурсивно раскладываем более крупные подзадачи на меньшие, пока не достигнем наименьших подзадач с уже известным ответом (листьев). Затем в процессе возврата постепенно собираем решения подзадач и тем самым получаем решение исходной задачи.</p>
<p>Напротив, <strong>динамическое программирование - это метод «снизу вверх»</strong> : начиная с решений наименьших подзадач, мы итеративно строим решения для более крупных подзадач, пока не получим ответ на исходную задачу.</p>
<p>Поскольку в динамическом программировании нет этапа возврата, для его реализации достаточно обычных циклов, без рекурсии. В приведенном ниже коде мы инициализируем массив <code>dp</code> для хранения решений подзадач. Он выполняет ту же роль, что и массив <code>mem</code> в мемоизированном поиске:</p>
<div class="tabbed-set tabbed-alternate" data-tabs="4:13"><input checked="checked" id="__tabbed_4_1" name="__tabbed_4" type="radio" /><input id="__tabbed_4_2" name="__tabbed_4" type="radio" /><input id="__tabbed_4_3" name="__tabbed_4" type="radio" /><input id="__tabbed_4_4" name="__tabbed_4" type="radio" /><input id="__tabbed_4_5" name="__tabbed_4" type="radio" /><input id="__tabbed_4_6" name="__tabbed_4" type="radio" /><input id="__tabbed_4_7" name="__tabbed_4" type="radio" /><input id="__tabbed_4_8" name="__tabbed_4" type="radio" /><input id="__tabbed_4_9" name="__tabbed_4" type="radio" /><input id="__tabbed_4_10" name="__tabbed_4" type="radio" /><input id="__tabbed_4_11" name="__tabbed_4" type="radio" /><input id="__tabbed_4_12" name="__tabbed_4" type="radio" /><input id="__tabbed_4_13" name="__tabbed_4" type="radio" /><div class="tabbed-labels"><label for="__tabbed_4_1">Python</label><label for="__tabbed_4_2">C++</label><label for="__tabbed_4_3">Java</label><label for="__tabbed_4_4">C#</label><label for="__tabbed_4_5">Go</label><label for="__tabbed_4_6">Swift</label><label for="__tabbed_4_7">JS</label><label for="__tabbed_4_8">TS</label><label for="__tabbed_4_9">Dart</label><label for="__tabbed_4_10">Rust</label><label for="__tabbed_4_11">C</label><label for="__tabbed_4_12">Kotlin</label><label for="__tabbed_4_13">Ruby</label></div>
<div class="tabbed-content">
<div class="tabbed-block">
@@ -5628,7 +5628,7 @@ dp[i] = dp[i-1] + dp[i-2]
<p><img alt="Процесс динамического программирования для подъема по лестнице" class="animation-figure" src="../intro_to_dynamic_programming.assets/climbing_stairs_dp.png" /></p>
<p align="center"> Рисунок 14-5 &nbsp; Процесс динамического программирования для подъема по лестнице </p>
<p>Как и в поиске с возвратом, в динамическом программировании используется понятие "состояние" для обозначения некоторого этапа решения задачи; каждое состояние соответствует одной подзадаче и ее локально оптимальному решению. Например, в задаче о лестнице состояние определяется текущим номером ступени <span class="arithmatex">\(i\)</span> .</p>
<p>Как и в поиске с возвратом, в динамическом программировании используется понятие «состояние» для обозначения некоторого этапа решения задачи. Каждое состояние соответствует одной подзадаче и ее локально оптимальному решению. Например, в задаче о лестнице состояние определяется текущим номером ступени <span class="arithmatex">\(i\)</span> .</p>
<p>На основе сказанного можно подвести несколько часто используемых терминов динамического программирования.</p>
<ul>
<li>Массив <code>dp</code> называют <u>таблицей dp</u>, а <span class="arithmatex">\(dp[i]\)</span> обозначает решение подзадачи, соответствующей состоянию <span class="arithmatex">\(i\)</span> .</li>
@@ -5636,7 +5636,7 @@ dp[i] = dp[i-1] + dp[i-2]
<li>Рекуррентную формулу <span class="arithmatex">\(dp[i] = dp[i-1] + dp[i-2]\)</span> называют <u>уравнением перехода состояния</u>.</li>
</ul>
<h2 id="1414">14.1.4 &nbsp; Оптимизация пространства<a class="headerlink" href="#1414" title="Permanent link">&para;</a></h2>
<p>Внимательный читатель мог заметить, что <strong>поскольку <span class="arithmatex">\(dp[i]\)</span> зависит только от <span class="arithmatex">\(dp[i-1]\)</span> и <span class="arithmatex">\(dp[i-2]\)</span> , нам не нужен весь массив <code>dp</code> для хранения ответов всех подзадач</strong> ; достаточно двух переменных, которые будут "перекатываться" вперед. Код имеет вид:</p>
<p>Внимательный читатель мог заметить, что <strong>поскольку <span class="arithmatex">\(dp[i]\)</span> зависит только от <span class="arithmatex">\(dp[i-1]\)</span> и <span class="arithmatex">\(dp[i-2]\)</span> , нам не нужен весь массив <code>dp</code> для хранения ответов всех подзадач</strong>. Достаточно двух переменных, которые будут «перекатываться» вперед. Код имеет вид:</p>
<div class="tabbed-set tabbed-alternate" data-tabs="5:13"><input checked="checked" id="__tabbed_5_1" name="__tabbed_5" type="radio" /><input id="__tabbed_5_2" name="__tabbed_5" type="radio" /><input id="__tabbed_5_3" name="__tabbed_5" type="radio" /><input id="__tabbed_5_4" name="__tabbed_5" type="radio" /><input id="__tabbed_5_5" name="__tabbed_5" type="radio" /><input id="__tabbed_5_6" name="__tabbed_5" type="radio" /><input id="__tabbed_5_7" name="__tabbed_5" type="radio" /><input id="__tabbed_5_8" name="__tabbed_5" type="radio" /><input id="__tabbed_5_9" name="__tabbed_5" type="radio" /><input id="__tabbed_5_10" name="__tabbed_5" type="radio" /><input id="__tabbed_5_11" name="__tabbed_5" type="radio" /><input id="__tabbed_5_12" name="__tabbed_5" type="radio" /><input id="__tabbed_5_13" name="__tabbed_5" type="radio" /><div class="tabbed-labels"><label for="__tabbed_5_1">Python</label><label for="__tabbed_5_2">C++</label><label for="__tabbed_5_3">Java</label><label for="__tabbed_5_4">C#</label><label for="__tabbed_5_5">Go</label><label for="__tabbed_5_6">Swift</label><label for="__tabbed_5_7">JS</label><label for="__tabbed_5_8">TS</label><label for="__tabbed_5_9">Dart</label><label for="__tabbed_5_10">Rust</label><label for="__tabbed_5_11">C</label><label for="__tabbed_5_12">Kotlin</label><label for="__tabbed_5_13">Ruby</label></div>
<div class="tabbed-content">
<div class="tabbed-block">
@@ -5835,7 +5835,7 @@ dp[i] = dp[i-1] + dp[i-2]
<div style="margin-top: 5px;"><a href="https://pythontutor.com/iframe-embed.html#code=def%20climbing_stairs_dp_comp%28n%3A%20int%29%20-%3E%20int%3A%0A%20%20%20%20%22%22%22%D0%9F%D0%BE%D0%B4%D1%8A%D0%B5%D0%BC%20%D0%BF%D0%BE%20%D0%BB%D0%B5%D1%81%D1%82%D0%BD%D0%B8%D1%86%D0%B5%3A%20%D0%B4%D0%B8%D0%BD%D0%B0%D0%BC%D0%B8%D1%87%D0%B5%D1%81%D0%BA%D0%BE%D0%B5%20%D0%BF%D1%80%D0%BE%D0%B3%D1%80%D0%B0%D0%BC%D0%BC%D0%B8%D1%80%D0%BE%D0%B2%D0%B0%D0%BD%D0%B8%D0%B5%20%D1%81%20%D0%BE%D0%BF%D1%82%D0%B8%D0%BC%D0%B8%D0%B7%D0%B0%D1%86%D0%B8%D0%B5%D0%B9%20%D0%BF%D0%B0%D0%BC%D1%8F%D1%82%D0%B8%22%22%22%0A%20%20%20%20if%20n%20%3D%3D%201%20or%20n%20%3D%3D%202%3A%0A%20%20%20%20%20%20%20%20return%20n%0A%20%20%20%20a%2C%20b%20%3D%201%2C%202%0A%20%20%20%20for%20_%20in%20range%283%2C%20n%20%2B%201%29%3A%0A%20%20%20%20%20%20%20%20a%2C%20b%20%3D%20b%2C%20a%20%2B%20b%0A%20%20%20%20return%20b%0A%0A%0A%22%22%22Driver%20Code%22%22%22%0Aif%20__name__%20%3D%3D%20%22__main__%22%3A%0A%20%20%20%20n%20%3D%209%0A%0A%20%20%20%20res%20%3D%20climbing_stairs_dp_comp%28n%29%0A%20%20%20%20print%28f%22%D0%9A%D0%BE%D0%BB%D0%B8%D1%87%D0%B5%D1%81%D1%82%D0%B2%D0%BE%20%D1%81%D0%BF%D0%BE%D1%81%D0%BE%D0%B1%D0%BE%D0%B2%20%D0%BF%D0%BE%D0%B4%D0%BD%D1%8F%D1%82%D1%8C%D1%81%D1%8F%20%D0%BF%D0%BE%20%D0%BB%D0%B5%D1%81%D1%82%D0%BD%D0%B8%D1%86%D0%B5%20%D0%B8%D0%B7%20%7Bn%7D%20%D1%81%D1%82%D1%83%D0%BF%D0%B5%D0%BD%D0%B5%D0%B9%20%3D%20%7Bres%7D%22%29&codeDivHeight=800&codeDivWidth=600&cumulative=false&curInstr=4&heapPrimitives=nevernest&origin=opt-frontend.js&py=311&rawInputLstJSON=%5B%5D&textReferences=false" target="_blank" rel="noopener noreferrer">Во весь экран &gt;</a></div></p>
</details>
<p>Из кода видно, что после отказа от массива <code>dp</code> пространственная сложность уменьшается с <span class="arithmatex">\(O(n)\)</span> до <span class="arithmatex">\(O(1)\)</span> .</p>
<p>Во многих задачах динамического программирования текущее состояние зависит лишь от ограниченного числа предыдущих состояний. Тогда можно сохранять только действительно нужные состояния и за счет "уменьшения размерности" экономить память. <strong>Этот прием оптимизации памяти называют "скользящими переменными" или "скользящим массивом"</strong>.</p>
<p>Во многих задачах динамического программирования текущее состояние зависит лишь от ограниченного числа предыдущих состояний. Тогда можно сохранять только действительно нужные состояния и за счет «уменьшения размерности» экономить память. <strong>Этот прием оптимизации памяти называют «скользящими переменными» или «скользящим массивом»</strong>.</p>
<!-- Source file information -->
@@ -4412,9 +4412,9 @@
<p align="center"> Рисунок 14-17 &nbsp; Пример данных для задачи о рюкзаке 0-1 </p>
<p>Задачу о рюкзаке 0-1 можно рассматривать как процесс из <span class="arithmatex">\(n\)</span> раундов принятия решений: для каждого предмета есть два решения - не класть его в рюкзак или положить в рюкзак. Поэтому задача удовлетворяет модели дерева решений.</p>
<p>Цель задачи - найти "максимальную суммарную стоимость при ограниченной вместимости рюкзака", а это с большой вероятностью указывает на задачу динамического программирования.</p>
<p>Цель задачи - найти «максимальную суммарную стоимость при ограниченной вместимости рюкзака», а это с большой вероятностью указывает на задачу динамического программирования.</p>
<p><strong>Шаг 1: продумать решения на каждом раунде, определить состояние и тем самым получить таблицу <span class="arithmatex">\(dp\)</span></strong></p>
<p>Для каждого предмета возможны два случая: не класть его в рюкзак, тогда вместимость не меняется; или положить его в рюкзак, тогда оставшаяся вместимость уменьшается. Отсюда получается определение состояния: текущий номер предмета <span class="arithmatex">\(i\)</span> и текущая вместимость рюкзака <span class="arithmatex">\(c\)</span> , то есть состояние обозначается как <span class="arithmatex">\([i, c]\)</span> .</p>
<p>Для каждого предмета возможны два случая: не класть его в рюкзак, тогда вместимость не меняется. Или положить его в рюкзак, тогда оставшаяся вместимость уменьшается. Отсюда получается определение состояния: текущий номер предмета <span class="arithmatex">\(i\)</span> и текущая вместимость рюкзака <span class="arithmatex">\(c\)</span> , то есть состояние обозначается как <span class="arithmatex">\([i, c]\)</span> .</p>
<p>Подзадача, соответствующая состоянию <span class="arithmatex">\([i, c]\)</span> , такова: <strong>максимальная стоимость, которую можно получить, используя первые <span class="arithmatex">\(i\)</span> предметов и рюкзак вместимости <span class="arithmatex">\(c\)</span></strong>. Ее решение обозначается через <span class="arithmatex">\(dp[i, c]\)</span> .</p>
<p>Искомым значением является <span class="arithmatex">\(dp[n, cap]\)</span> , значит, нам нужна двумерная таблица <span class="arithmatex">\(dp\)</span> размера <span class="arithmatex">\((n+1) \times (cap+1)\)</span> .</p>
<p><strong>Шаг 2: найти оптимальную подструктуру и на ее основе вывести уравнение перехода состояния</strong></p>
@@ -4429,7 +4429,7 @@ dp[i, c] = \max(dp[i-1, c], dp[i-1, c - wgt[i-1]] + val[i-1])
\]</div>
<p>Нужно учитывать, что если вес текущего предмета <span class="arithmatex">\(wgt[i - 1]\)</span> превышает оставшуюся вместимость <span class="arithmatex">\(c\)</span> , то предмет можно только не брать.</p>
<p><strong>Шаг 3: определить граничные условия и порядок переходов</strong></p>
<p>Когда предметов нет или вместимость рюкзака равна <span class="arithmatex">\(0\)</span> , максимальная стоимость равна <span class="arithmatex">\(0\)</span> ; то есть весь первый столбец <span class="arithmatex">\(dp[i, 0]\)</span> и вся первая строка <span class="arithmatex">\(dp[0, c]\)</span> заполняются нулями.</p>
<p>Когда предметов нет или вместимость рюкзака равна <span class="arithmatex">\(0\)</span> , максимальная стоимость равна <span class="arithmatex">\(0\)</span>. То есть весь первый столбец <span class="arithmatex">\(dp[i, 0]\)</span> и вся первая строка <span class="arithmatex">\(dp[0, c]\)</span> заполняются нулями.</p>
<p>Текущее состояние <span class="arithmatex">\([i, c]\)</span> зависит от состояния сверху <span class="arithmatex">\([i-1, c]\)</span> и состояния слева сверху <span class="arithmatex">\([i-1, c-wgt[i-1]]\)</span> , поэтому достаточно двумя вложенными циклами пройти по всей таблице <span class="arithmatex">\(dp\)</span> в прямом порядке.</p>
<p>После этого анализа реализуем по порядку: полный перебор, поиск с мемоизацией и динамическое программирование.</p>
<h3 id="1-1">1. &nbsp; Метод 1: полный перебор<a class="headerlink" href="#1-1" title="Permanent link">&para;</a></h3>
@@ -4699,7 +4699,7 @@ dp[i, c] = \max(dp[i-1, c], dp[i-1, c - wgt[i-1]] + val[i-1])
<p><div style="height: 549px; width: 100%;"><iframe class="pythontutor-iframe" src="https://pythontutor.com/iframe-embed.html#code=def%20knapsack_dfs%28wgt%3A%20list%5Bint%5D%2C%20val%3A%20list%5Bint%5D%2C%20i%3A%20int%2C%20c%3A%20int%29%20-%3E%20int%3A%0A%20%20%20%20%22%22%22%D0%A0%D1%8E%D0%BA%D0%B7%D0%B0%D0%BA%200-1%3A%20%D0%BF%D0%BE%D0%BB%D0%BD%D1%8B%D0%B9%20%D0%BF%D0%B5%D1%80%D0%B5%D0%B1%D0%BE%D1%80%22%22%22%0A%20%20%20%20%23%20%D0%95%D1%81%D0%BB%D0%B8%20%D0%B2%D1%81%D0%B5%20%D0%BF%D1%80%D0%B5%D0%B4%D0%BC%D0%B5%D1%82%D1%8B%20%D1%83%D0%B6%D0%B5%20%D1%80%D0%B0%D1%81%D1%81%D0%BC%D0%BE%D1%82%D1%80%D0%B5%D0%BD%D1%8B%20%D0%B8%D0%BB%D0%B8%20%D0%B2%20%D1%80%D1%8E%D0%BA%D0%B7%D0%B0%D0%BA%D0%B5%20%D0%BD%D0%B5%20%D0%BE%D1%81%D1%82%D0%B0%D0%BB%D0%BE%D1%81%D1%8C%20%D0%BC%D0%B5%D1%81%D1%82%D0%B0%2C%20%D0%B2%D0%B5%D1%80%D0%BD%D1%83%D1%82%D1%8C%20%D1%81%D1%82%D0%BE%D0%B8%D0%BC%D0%BE%D1%81%D1%82%D1%8C%200%0A%20%20%20%20if%20i%20%3D%3D%200%20or%20c%20%3D%3D%200%3A%0A%20%20%20%20%20%20%20%20return%200%0A%20%20%20%20%23%20%D0%95%D1%81%D0%BB%D0%B8%20%D0%B2%D0%BC%D0%B5%D1%81%D1%82%D0%B8%D0%BC%D0%BE%D1%81%D1%82%D1%8C%20%D1%80%D1%8E%D0%BA%D0%B7%D0%B0%D0%BA%D0%B0%20%D0%BF%D1%80%D0%B5%D0%B2%D1%8B%D1%88%D0%B5%D0%BD%D0%B0%2C%20%D0%BC%D0%BE%D0%B6%D0%BD%D0%BE%20%D1%82%D0%BE%D0%BB%D1%8C%D0%BA%D0%BE%20%D0%BD%D0%B5%20%D0%BA%D0%BB%D0%B0%D1%81%D1%82%D1%8C%20%D0%BF%D1%80%D0%B5%D0%B4%D0%BC%D0%B5%D1%82%20%D0%B2%20%D1%80%D1%8E%D0%BA%D0%B7%D0%B0%D0%BA%0A%20%20%20%20if%20wgt%5Bi%20-%201%5D%20%3E%20c%3A%0A%20%20%20%20%20%20%20%20return%20knapsack_dfs%28wgt%2C%20val%2C%20i%20-%201%2C%20c%29%0A%20%20%20%20%23%20%D0%92%D1%8B%D1%87%D0%B8%D1%81%D0%BB%D0%B8%D1%82%D1%8C%20%D0%BC%D0%B0%D0%BA%D1%81%D0%B8%D0%BC%D0%B0%D0%BB%D1%8C%D0%BD%D1%83%D1%8E%20%D1%81%D1%82%D0%BE%D0%B8%D0%BC%D0%BE%D1%81%D1%82%D1%8C%20%D0%B4%D0%BB%D1%8F%20%D1%81%D0%BB%D1%83%D1%87%D0%B0%D0%B5%D0%B2%2C%20%D0%BA%D0%BE%D0%B3%D0%B4%D0%B0%20%D0%BF%D1%80%D0%B5%D0%B4%D0%BC%D0%B5%D1%82%20i%20%D0%BD%D0%B5%20%D0%BA%D0%BB%D0%B0%D0%B4%D1%83%D1%82%20%D0%B8%20%D0%BA%D0%BB%D0%B0%D0%B4%D1%83%D1%82%0A%20%20%20%20no%20%3D%20knapsack_dfs%28wgt%2C%20val%2C%20i%20-%201%2C%20c%29%0A%20%20%20%20yes%20%3D%20knapsack_dfs%28wgt%2C%20val%2C%20i%20-%201%2C%20c%20-%20wgt%5Bi%20-%201%5D%29%20%2B%20val%5Bi%20-%201%5D%0A%20%20%20%20%23%20%D0%92%D0%B5%D1%80%D0%BD%D1%83%D1%82%D1%8C%20%D0%B2%D0%B0%D1%80%D0%B8%D0%B0%D0%BD%D1%82%20%D1%81%20%D0%B1%D0%BE%D0%BB%D1%8C%D1%88%D0%B5%D0%B9%20%D1%81%D1%82%D0%BE%D0%B8%D0%BC%D0%BE%D1%81%D1%82%D1%8C%D1%8E%20%D0%B8%D0%B7%20%D0%B4%D0%B2%D1%83%D1%85%20%D0%B2%D0%BE%D0%B7%D0%BC%D0%BE%D0%B6%D0%BD%D1%8B%D1%85%0A%20%20%20%20return%20max%28no%2C%20yes%29%0A%0A%0A%22%22%22Driver%20Code%22%22%22%0Aif%20__name__%20%3D%3D%20%22__main__%22%3A%0A%20%20%20%20wgt%20%3D%20%5B10%2C%2020%2C%2030%2C%2040%2C%2050%5D%0A%20%20%20%20val%20%3D%20%5B50%2C%20120%2C%20150%2C%20210%2C%20240%5D%0A%20%20%20%20cap%20%3D%2050%0A%20%20%20%20n%20%3D%20len%28wgt%29%0A%0A%20%20%20%20%23%20%D0%9F%D0%BE%D0%BB%D0%BD%D1%8B%D0%B9%20%D0%BF%D0%B5%D1%80%D0%B5%D0%B1%D0%BE%D1%80%0A%20%20%20%20res%20%3D%20knapsack_dfs%28wgt%2C%20val%2C%20n%2C%20cap%29%0A%20%20%20%20print%28f%22%D0%9C%D0%B0%D0%BA%D1%81%D0%B8%D0%BC%D0%B0%D0%BB%D1%8C%D0%BD%D0%B0%D1%8F%20%D1%81%D1%82%D0%BE%D0%B8%D0%BC%D0%BE%D1%81%D1%82%D1%8C%20%D0%BF%D1%80%D0%B5%D0%B4%D0%BC%D0%B5%D1%82%D0%BE%D0%B2%20%D0%B1%D0%B5%D0%B7%20%D0%BF%D1%80%D0%B5%D0%B2%D1%8B%D1%88%D0%B5%D0%BD%D0%B8%D1%8F%20%D0%B2%D0%BC%D0%B5%D1%81%D1%82%D0%B8%D0%BC%D0%BE%D1%81%D1%82%D0%B8%20%D1%80%D1%8E%D0%BA%D0%B7%D0%B0%D0%BA%D0%B0%20%3D%20%7Bres%7D%22%29&codeDivHeight=472&codeDivWidth=350&cumulative=false&curInstr=7&heapPrimitives=nevernest&origin=opt-frontend.js&py=311&rawInputLstJSON=%5B%5D&textReferences=false"> </iframe></div>
<div style="margin-top: 5px;"><a href="https://pythontutor.com/iframe-embed.html#code=def%20knapsack_dfs%28wgt%3A%20list%5Bint%5D%2C%20val%3A%20list%5Bint%5D%2C%20i%3A%20int%2C%20c%3A%20int%29%20-%3E%20int%3A%0A%20%20%20%20%22%22%22%D0%A0%D1%8E%D0%BA%D0%B7%D0%B0%D0%BA%200-1%3A%20%D0%BF%D0%BE%D0%BB%D0%BD%D1%8B%D0%B9%20%D0%BF%D0%B5%D1%80%D0%B5%D0%B1%D0%BE%D1%80%22%22%22%0A%20%20%20%20%23%20%D0%95%D1%81%D0%BB%D0%B8%20%D0%B2%D1%81%D0%B5%20%D0%BF%D1%80%D0%B5%D0%B4%D0%BC%D0%B5%D1%82%D1%8B%20%D1%83%D0%B6%D0%B5%20%D1%80%D0%B0%D1%81%D1%81%D0%BC%D0%BE%D1%82%D1%80%D0%B5%D0%BD%D1%8B%20%D0%B8%D0%BB%D0%B8%20%D0%B2%20%D1%80%D1%8E%D0%BA%D0%B7%D0%B0%D0%BA%D0%B5%20%D0%BD%D0%B5%20%D0%BE%D1%81%D1%82%D0%B0%D0%BB%D0%BE%D1%81%D1%8C%20%D0%BC%D0%B5%D1%81%D1%82%D0%B0%2C%20%D0%B2%D0%B5%D1%80%D0%BD%D1%83%D1%82%D1%8C%20%D1%81%D1%82%D0%BE%D0%B8%D0%BC%D0%BE%D1%81%D1%82%D1%8C%200%0A%20%20%20%20if%20i%20%3D%3D%200%20or%20c%20%3D%3D%200%3A%0A%20%20%20%20%20%20%20%20return%200%0A%20%20%20%20%23%20%D0%95%D1%81%D0%BB%D0%B8%20%D0%B2%D0%BC%D0%B5%D1%81%D1%82%D0%B8%D0%BC%D0%BE%D1%81%D1%82%D1%8C%20%D1%80%D1%8E%D0%BA%D0%B7%D0%B0%D0%BA%D0%B0%20%D0%BF%D1%80%D0%B5%D0%B2%D1%8B%D1%88%D0%B5%D0%BD%D0%B0%2C%20%D0%BC%D0%BE%D0%B6%D0%BD%D0%BE%20%D1%82%D0%BE%D0%BB%D1%8C%D0%BA%D0%BE%20%D0%BD%D0%B5%20%D0%BA%D0%BB%D0%B0%D1%81%D1%82%D1%8C%20%D0%BF%D1%80%D0%B5%D0%B4%D0%BC%D0%B5%D1%82%20%D0%B2%20%D1%80%D1%8E%D0%BA%D0%B7%D0%B0%D0%BA%0A%20%20%20%20if%20wgt%5Bi%20-%201%5D%20%3E%20c%3A%0A%20%20%20%20%20%20%20%20return%20knapsack_dfs%28wgt%2C%20val%2C%20i%20-%201%2C%20c%29%0A%20%20%20%20%23%20%D0%92%D1%8B%D1%87%D0%B8%D1%81%D0%BB%D0%B8%D1%82%D1%8C%20%D0%BC%D0%B0%D0%BA%D1%81%D0%B8%D0%BC%D0%B0%D0%BB%D1%8C%D0%BD%D1%83%D1%8E%20%D1%81%D1%82%D0%BE%D0%B8%D0%BC%D0%BE%D1%81%D1%82%D1%8C%20%D0%B4%D0%BB%D1%8F%20%D1%81%D0%BB%D1%83%D1%87%D0%B0%D0%B5%D0%B2%2C%20%D0%BA%D0%BE%D0%B3%D0%B4%D0%B0%20%D0%BF%D1%80%D0%B5%D0%B4%D0%BC%D0%B5%D1%82%20i%20%D0%BD%D0%B5%20%D0%BA%D0%BB%D0%B0%D0%B4%D1%83%D1%82%20%D0%B8%20%D0%BA%D0%BB%D0%B0%D0%B4%D1%83%D1%82%0A%20%20%20%20no%20%3D%20knapsack_dfs%28wgt%2C%20val%2C%20i%20-%201%2C%20c%29%0A%20%20%20%20yes%20%3D%20knapsack_dfs%28wgt%2C%20val%2C%20i%20-%201%2C%20c%20-%20wgt%5Bi%20-%201%5D%29%20%2B%20val%5Bi%20-%201%5D%0A%20%20%20%20%23%20%D0%92%D0%B5%D1%80%D0%BD%D1%83%D1%82%D1%8C%20%D0%B2%D0%B0%D1%80%D0%B8%D0%B0%D0%BD%D1%82%20%D1%81%20%D0%B1%D0%BE%D0%BB%D1%8C%D1%88%D0%B5%D0%B9%20%D1%81%D1%82%D0%BE%D0%B8%D0%BC%D0%BE%D1%81%D1%82%D1%8C%D1%8E%20%D0%B8%D0%B7%20%D0%B4%D0%B2%D1%83%D1%85%20%D0%B2%D0%BE%D0%B7%D0%BC%D0%BE%D0%B6%D0%BD%D1%8B%D1%85%0A%20%20%20%20return%20max%28no%2C%20yes%29%0A%0A%0A%22%22%22Driver%20Code%22%22%22%0Aif%20__name__%20%3D%3D%20%22__main__%22%3A%0A%20%20%20%20wgt%20%3D%20%5B10%2C%2020%2C%2030%2C%2040%2C%2050%5D%0A%20%20%20%20val%20%3D%20%5B50%2C%20120%2C%20150%2C%20210%2C%20240%5D%0A%20%20%20%20cap%20%3D%2050%0A%20%20%20%20n%20%3D%20len%28wgt%29%0A%0A%20%20%20%20%23%20%D0%9F%D0%BE%D0%BB%D0%BD%D1%8B%D0%B9%20%D0%BF%D0%B5%D1%80%D0%B5%D0%B1%D0%BE%D1%80%0A%20%20%20%20res%20%3D%20knapsack_dfs%28wgt%2C%20val%2C%20n%2C%20cap%29%0A%20%20%20%20print%28f%22%D0%9C%D0%B0%D0%BA%D1%81%D0%B8%D0%BC%D0%B0%D0%BB%D1%8C%D0%BD%D0%B0%D1%8F%20%D1%81%D1%82%D0%BE%D0%B8%D0%BC%D0%BE%D1%81%D1%82%D1%8C%20%D0%BF%D1%80%D0%B5%D0%B4%D0%BC%D0%B5%D1%82%D0%BE%D0%B2%20%D0%B1%D0%B5%D0%B7%20%D0%BF%D1%80%D0%B5%D0%B2%D1%8B%D1%88%D0%B5%D0%BD%D0%B8%D1%8F%20%D0%B2%D0%BC%D0%B5%D1%81%D1%82%D0%B8%D0%BC%D0%BE%D1%81%D1%82%D0%B8%20%D1%80%D1%8E%D0%BA%D0%B7%D0%B0%D0%BA%D0%B0%20%3D%20%7Bres%7D%22%29&codeDivHeight=800&codeDivWidth=600&cumulative=false&curInstr=7&heapPrimitives=nevernest&origin=opt-frontend.js&py=311&rawInputLstJSON=%5B%5D&textReferences=false" target="_blank" rel="noopener noreferrer">Во весь экран &gt;</a></div></p>
</details>
<p>Как показано на рисунке 14-18, поскольку каждый предмет создает две ветви поиска - "не брать" и "брать", временная сложность равна <span class="arithmatex">\(O(2^n)\)</span> .</p>
<p>Как показано на рисунке 14-18, поскольку каждый предмет создает две ветви поиска - «не брать» и «брать», временная сложность равна <span class="arithmatex">\(O(2^n)\)</span> .</p>
<p>Посмотрев на дерево рекурсии, легко заметить наличие перекрывающихся подзадач, например <span class="arithmatex">\(dp[1, 10]\)</span> и подобных. Когда число предметов растет, вместимость рюкзака велика, а особенно когда много предметов с одинаковым весом, количество перекрывающихся подзадач быстро увеличивается.</p>
<p><img alt="Дерево полного перебора для задачи о рюкзаке 0-1" class="animation-figure" src="../knapsack_problem.assets/knapsack_dfs.png" /></p>
<p align="center"> Рисунок 14-18 &nbsp; Дерево полного перебора для задачи о рюкзаке 0-1 </p>
@@ -4339,23 +4339,23 @@
<ul>
<li>Динамическое программирование раскладывает задачу на подзадачи и повышает вычислительную эффективность за счет хранения решений этих подзадач и устранения повторных вычислений.</li>
<li>Если не учитывать затраты времени, то любую задачу динамического программирования можно решить с помощью поиска с возвратом (полного перебора), однако в дереве рекурсии возникает множество перекрывающихся подзадач, из-за чего эффективность крайне низка. После введения таблицы памяти можно хранить решения всех уже вычисленных подзадач и гарантировать, что каждая перекрывающаяся подзадача будет вычисляться только один раз.</li>
<li>Поиск с мемоизацией - это рекурсивный метод "сверху вниз", а соответствующее ему динамическое программирование - это итеративный метод "снизу вверх", похожий на заполнение таблицы. Поскольку текущее состояние обычно зависит только от части локальных состояний, можно убрать одно измерение таблицы <span class="arithmatex">\(dp\)</span> и тем самым снизить пространственную сложность.</li>
<li>Разложение на подзадачи - это общий алгоритмический подход, но в методе "разделяй и властвуй", динамическом программировании и поиске с возвратом он имеет разные свойства.</li>
<li>Поиск с мемоизацией - это рекурсивный метод «сверху вниз», а соответствующее ему динамическое программирование - это итеративный метод «снизу вверх», похожий на заполнение таблицы. Поскольку текущее состояние обычно зависит только от части локальных состояний, можно убрать одно измерение таблицы <span class="arithmatex">\(dp\)</span> и тем самым снизить пространственную сложность.</li>
<li>Разложение на подзадачи - это общий алгоритмический подход, но в методе «разделяй и властвуй», динамическом программировании и поиске с возвратом он имеет разные свойства.</li>
<li>Для задач динамического программирования характерны три главных свойства: перекрывающиеся подзадачи, оптимальная подструктура и отсутствие последствий.</li>
<li>Если оптимальное решение исходной задачи можно построить из оптимальных решений подзадач, то задача обладает оптимальной подструктурой.</li>
<li>Отсутствие последствий означает, что для данного состояния его дальнейшее развитие определяется только этим состоянием и не зависит от всех прошлых состояний. Многие задачи комбинаторной оптимизации этим свойством не обладают и потому не могут эффективно решаться с помощью динамического программирования.</li>
</ul>
<p><strong>Задачи о рюкзаке</strong></p>
<ul>
<li>Задача о рюкзаке - один из самых типичных классов задач динамического программирования; она включает варианты 0-1 рюкзака, полного рюкзака, многократного рюкзака и другие.</li>
<li>Задача о рюкзаке - один из самых типичных классов задач динамического программирования. Она включает варианты 0-1 рюкзака, полного рюкзака, многократного рюкзака и другие.</li>
<li>В задаче о рюкзаке 0-1 состояние определяется как максимальная стоимость первых <span class="arithmatex">\(i\)</span> предметов в рюкзаке вместимости <span class="arithmatex">\(c\)</span> . Рассматривая два решения - не брать предмет и брать предмет, - можно получить оптимальную подструктуру и вывести уравнение перехода состояния. При оптимизации памяти, поскольку каждое состояние зависит от значения сверху и слева сверху, внутренний цикл нужно выполнять в обратном порядке, чтобы не перезаписать нужное значение.</li>
<li>В задаче о полном рюкзаке число экземпляров каждого предмета не ограничено, поэтому при выборе предмета переход состояния отличается от варианта 0-1. Поскольку состояние зависит от значения сверху и слева, после оптимизации памяти внутренний цикл следует выполнять в прямом порядке.</li>
<li>Задача о размене монет - это вариант задачи о полном рюкзаке. Здесь вместо "максимальной стоимости" ищется "минимальное число монет", поэтому в уравнении перехода <span class="arithmatex">\(\max()\)</span> заменяется на <span class="arithmatex">\(\min()\)</span> . Кроме того, вместо условия "не превышать вместимость рюкзака" нужно <strong>ровно</strong> набрать целевую сумму, поэтому значение <span class="arithmatex">\(amt + 1\)</span> используется как обозначение недопустимого решения "сумму набрать нельзя".</li>
<li>В задаче о размене монет II вместо "минимального числа монет" требуется найти "число комбинаций монет", поэтому в уравнении перехода оператор <span class="arithmatex">\(\min()\)</span> заменяется на суммирование.</li>
<li>Задача о размене монет - это вариант задачи о полном рюкзаке. Здесь вместо «максимальной стоимости» ищется «минимальное число монет», поэтому в уравнении перехода <span class="arithmatex">\(\max()\)</span> заменяется на <span class="arithmatex">\(\min()\)</span> . Кроме того, вместо условия «не превышать вместимость рюкзака» нужно <strong>ровно</strong> набрать целевую сумму, поэтому значение <span class="arithmatex">\(amt + 1\)</span> используется как обозначение недопустимого решения «сумму набрать нельзя».</li>
<li>В задаче о размене монет II вместо «минимального числа монет» требуется найти «число комбинаций монет», поэтому в уравнении перехода оператор <span class="arithmatex">\(\min()\)</span> заменяется на суммирование.</li>
</ul>
<p><strong>Задача о расстоянии редактирования</strong></p>
<ul>
<li>Расстояние редактирования (расстояние Левенштейна) используется для измерения сходства двух строк и определяется как минимальное число операций редактирования, необходимых для преобразования одной строки в другую; допустимые операции - вставка, удаление и замена.</li>
<li>Расстояние редактирования (расстояние Левенштейна) используется для измерения сходства двух строк и определяется как минимальное число операций редактирования, необходимых для преобразования одной строки в другую. Допустимые операции - вставка, удаление и замена.</li>
<li>В задаче о расстоянии редактирования состояние определяется как минимальное число шагов редактирования, необходимых для преобразования первых <span class="arithmatex">\(i\)</span> символов строки <span class="arithmatex">\(s\)</span> в первые <span class="arithmatex">\(j\)</span> символов строки <span class="arithmatex">\(t\)</span> . Если <span class="arithmatex">\(s[i] \ne t[j]\)</span> , то существуют три решения: вставка, удаление и замена, и каждому из них соответствует своя остаточная подзадача. На этой основе выводятся оптимальная подструктура и уравнение перехода состояния. Если же <span class="arithmatex">\(s[i] = t[j]\)</span> , то редактировать текущий символ не нужно.</li>
<li>В задаче о расстоянии редактирования состояние зависит от значений сверху, слева и слева сверху. Поэтому после оптимизации памяти ни прямой, ни обратный обход сам по себе не дает корректного перехода состояния. Для решения этой проблемы значение слева сверху временно сохраняется в отдельной переменной, что делает ситуацию эквивалентной задаче о полном рюкзаке и позволяет использовать прямой обход.</li>
</ul>
@@ -4623,7 +4623,7 @@
<p align="center"> Рисунок 14-22 &nbsp; Пример данных для задачи о полном рюкзаке </p>
<h3 id="1">1. &nbsp; Идея динамического программирования<a class="headerlink" href="#1" title="Permanent link">&para;</a></h3>
<p>Задача о полном рюкзаке очень похожа на задачу о рюкзаке 0-1; <strong>разница состоит только в том, что количество выборов каждого предмета не ограничено</strong>.</p>
<p>Задача о полном рюкзаке очень похожа на задачу о рюкзаке 0-1. <strong>Разница состоит только в том, что количество выборов каждого предмета не ограничено</strong>.</p>
<ul>
<li>В задаче о рюкзаке 0-1 каждого предмета существует только один экземпляр, поэтому после того как предмет <span class="arithmatex">\(i\)</span> помещен в рюкзак, выбирать можно только из первых <span class="arithmatex">\(i-1\)</span> предметов.</li>
<li>В задаче о полном рюкзаке количество предметов не ограничено, поэтому после того как предмет <span class="arithmatex">\(i\)</span> помещен в рюкзак, <strong>можно продолжать выбирать из первых <span class="arithmatex">\(i\)</span> предметов</strong>.</li>
@@ -4638,7 +4638,7 @@
dp[i, c] = \max(dp[i-1, c], dp[i, c - wgt[i-1]] + val[i-1])
\]</div>
<h3 id="2">2. &nbsp; Реализация кода<a class="headerlink" href="#2" title="Permanent link">&para;</a></h3>
<p>Если сравнить код этой задачи с кодом задачи о рюкзаке 0-1, то окажется, что в переходе состояний меняется только одна деталь: вместо <span class="arithmatex">\(i-1\)</span> появляется <span class="arithmatex">\(i\)</span> ; все остальное остается таким же:</p>
<p>Если сравнить код этой задачи с кодом задачи о рюкзаке 0-1, то окажется, что в переходе состояний меняется только одна деталь: вместо <span class="arithmatex">\(i-1\)</span> появляется <span class="arithmatex">\(i\)</span>. Все остальное остается таким же:</p>
<div class="tabbed-set tabbed-alternate" data-tabs="1:13"><input checked="checked" id="__tabbed_1_1" name="__tabbed_1" type="radio" /><input id="__tabbed_1_2" name="__tabbed_1" type="radio" /><input id="__tabbed_1_3" name="__tabbed_1" type="radio" /><input id="__tabbed_1_4" name="__tabbed_1" type="radio" /><input id="__tabbed_1_5" name="__tabbed_1" type="radio" /><input id="__tabbed_1_6" name="__tabbed_1" type="radio" /><input id="__tabbed_1_7" name="__tabbed_1" type="radio" /><input id="__tabbed_1_8" name="__tabbed_1" type="radio" /><input id="__tabbed_1_9" name="__tabbed_1" type="radio" /><input id="__tabbed_1_10" name="__tabbed_1" type="radio" /><input id="__tabbed_1_11" name="__tabbed_1" type="radio" /><input id="__tabbed_1_12" name="__tabbed_1" type="radio" /><input id="__tabbed_1_13" name="__tabbed_1" type="radio" /><div class="tabbed-labels"><label for="__tabbed_1_1">Python</label><label for="__tabbed_1_2">C++</label><label for="__tabbed_1_3">Java</label><label for="__tabbed_1_4">C#</label><label for="__tabbed_1_5">Go</label><label for="__tabbed_1_6">Swift</label><label for="__tabbed_1_7">JS</label><label for="__tabbed_1_8">TS</label><label for="__tabbed_1_9">Dart</label><label for="__tabbed_1_10">Rust</label><label for="__tabbed_1_11">C</label><label for="__tabbed_1_12">Kotlin</label><label for="__tabbed_1_13">Ruby</label></div>
<div class="tabbed-content">
<div class="tabbed-block">
@@ -4957,7 +4957,7 @@ dp[i, c] = \max(dp[i-1, c], dp[i, c - wgt[i-1]] + val[i-1])
</details>
<h3 id="3">3. &nbsp; Оптимизация пространства<a class="headerlink" href="#3" title="Permanent link">&para;</a></h3>
<p>Поскольку текущее состояние переходит из состояния слева и состояния сверху, <strong>после оптимизации памяти каждую строку таблицы <span class="arithmatex">\(dp\)</span> нужно обходить слева направо</strong>.</p>
<p>Этот порядок обхода как раз противоположен задаче о рюкзаке 0-1. Разницу удобно понять по рисунку ниже.</p>
<p>Этот порядок обхода как раз противоположен задаче о рюкзаке 0-1. Эту разницу удобно понять, рассмотрев то, что показано на рисунке 14-23.</p>
<div class="tabbed-set tabbed-alternate" data-tabs="2:6"><input checked="checked" id="__tabbed_2_1" name="__tabbed_2" type="radio" /><input id="__tabbed_2_2" name="__tabbed_2" type="radio" /><input id="__tabbed_2_3" name="__tabbed_2" type="radio" /><input id="__tabbed_2_4" name="__tabbed_2" type="radio" /><input id="__tabbed_2_5" name="__tabbed_2" type="radio" /><input id="__tabbed_2_6" name="__tabbed_2" type="radio" /><div class="tabbed-labels"><label for="__tabbed_2_1">&lt;1&gt;</label><label for="__tabbed_2_2">&lt;2&gt;</label><label for="__tabbed_2_3">&lt;3&gt;</label><label for="__tabbed_2_4">&lt;4&gt;</label><label for="__tabbed_2_5">&lt;5&gt;</label><label for="__tabbed_2_6">&lt;6&gt;</label></div>
<div class="tabbed-content">
<div class="tabbed-block">
@@ -5317,9 +5317,9 @@ dp[i, c] = \max(dp[i-1, c], dp[i, c - wgt[i-1]] + val[i-1])
<p align="center"> Рисунок 14-24 &nbsp; Пример данных для задачи о размене монет </p>
<h3 id="1_1">1. &nbsp; Идея динамического программирования<a class="headerlink" href="#1_1" title="Permanent link">&para;</a></h3>
<p><strong>Задачу о размене монет можно рассматривать как частный случай задачи о полном рюкзаке</strong> ; между ними существуют следующие соответствия и различия.</p>
<p><strong>Задачу о размене монет можно рассматривать как частный случай задачи о полном рюкзаке</strong>. Между ними существуют следующие соответствия и различия.</p>
<ul>
<li>Эти две задачи можно взаимно преобразовать: "предмет" соответствует "монете", "вес предмета" соответствует "номиналу монеты", а "вместимость рюкзака" соответствует "целевой сумме".</li>
<li>Эти две задачи можно взаимно преобразовать: «предмет» соответствует «монете», «вес предмета» соответствует «номиналу монеты», а «вместимость рюкзака» соответствует «целевой сумме».</li>
<li>Цель оптимизации противоположна: в задаче о полном рюкзаке нужно максимизировать стоимость предметов, а в задаче о размене монет - минимизировать число монет.</li>
<li>В задаче о полном рюкзаке ищется решение, не превышающее вместимость, а в задаче о размене монет требуется <strong>ровно</strong> набрать целевую сумму.</li>
</ul>
@@ -5337,10 +5337,10 @@ dp[i, a] = \min(dp[i-1, a], dp[i, a - coins[i-1]] + 1)
\]</div>
<p><strong>Шаг 3: определить граничные условия и порядок переходов</strong></p>
<p>Когда целевая сумма равна <span class="arithmatex">\(0\)</span> , минимальное число монет для ее набора равно <span class="arithmatex">\(0\)</span> , то есть весь первый столбец <span class="arithmatex">\(dp[i, 0]\)</span> заполняется нулями.</p>
<p>Когда монет нет, <strong>невозможно набрать никакую целевую сумму <span class="arithmatex">\(&gt; 0\)</span></strong> ; это и есть недопустимое решение. Чтобы функция <span class="arithmatex">\(\min()\)</span> в уравнении перехода состояния могла распознавать и отбрасывать такие недопустимые решения, удобно использовать значение <span class="arithmatex">\(+ \infty\)</span> ; то есть всю первую строку <span class="arithmatex">\(dp[0, a]\)</span> нужно инициализировать значением <span class="arithmatex">\(+ \infty\)</span> .</p>
<p>Когда монет нет, <strong>невозможно набрать никакую целевую сумму <span class="arithmatex">\(&gt; 0\)</span></strong>. Это и есть недопустимое решение. Чтобы функция <span class="arithmatex">\(\min()\)</span> в уравнении перехода состояния могла распознавать и отбрасывать такие недопустимые решения, удобно использовать значение <span class="arithmatex">\(+ \infty\)</span>. То есть всю первую строку <span class="arithmatex">\(dp[0, a]\)</span> нужно инициализировать значением <span class="arithmatex">\(+ \infty\)</span> .</p>
<h3 id="2_1">2. &nbsp; Реализация кода<a class="headerlink" href="#2_1" title="Permanent link">&para;</a></h3>
<p>Большинство языков программирования не предоставляет представление для <span class="arithmatex">\(+ \infty\)</span> в целочисленном виде, поэтому обычно приходится заменять его на максимальное значение типа <code>int</code> . Но тогда возникает риск переполнения: операция <span class="arithmatex">\(+ 1\)</span> в уравнении перехода может переполнить большое число.</p>
<p>Поэтому здесь мы используем число <span class="arithmatex">\(amt + 1\)</span> как обозначение недопустимого решения, потому что для набора суммы <span class="arithmatex">\(amt\)</span> максимум нужно не больше чем <span class="arithmatex">\(amt\)</span> монет. Перед возвратом результата проверяем, равно ли <span class="arithmatex">\(dp[n, amt]\)</span> значению <span class="arithmatex">\(amt + 1\)</span> ; если да, то возвращаем <span class="arithmatex">\(-1\)</span> , что означает невозможность набрать целевую сумму. Код приведен ниже:</p>
<p>Поэтому здесь мы используем число <span class="arithmatex">\(amt + 1\)</span> как обозначение недопустимого решения, потому что для набора суммы <span class="arithmatex">\(amt\)</span> максимум нужно не больше чем <span class="arithmatex">\(amt\)</span> монет. Перед возвратом результата проверяем, равно ли <span class="arithmatex">\(dp[n, amt]\)</span> значению <span class="arithmatex">\(amt + 1\)</span>. Если да, то возвращаем <span class="arithmatex">\(-1\)</span> , что означает невозможность набрать целевую сумму. Код приведен ниже:</p>
<div class="tabbed-set tabbed-alternate" data-tabs="4:13"><input checked="checked" id="__tabbed_4_1" name="__tabbed_4" type="radio" /><input id="__tabbed_4_2" name="__tabbed_4" type="radio" /><input id="__tabbed_4_3" name="__tabbed_4" type="radio" /><input id="__tabbed_4_4" name="__tabbed_4" type="radio" /><input id="__tabbed_4_5" name="__tabbed_4" type="radio" /><input id="__tabbed_4_6" name="__tabbed_4" type="radio" /><input id="__tabbed_4_7" name="__tabbed_4" type="radio" /><input id="__tabbed_4_8" name="__tabbed_4" type="radio" /><input id="__tabbed_4_9" name="__tabbed_4" type="radio" /><input id="__tabbed_4_10" name="__tabbed_4" type="radio" /><input id="__tabbed_4_11" name="__tabbed_4" type="radio" /><input id="__tabbed_4_12" name="__tabbed_4" type="radio" /><input id="__tabbed_4_13" name="__tabbed_4" type="radio" /><div class="tabbed-labels"><label for="__tabbed_4_1">Python</label><label for="__tabbed_4_2">C++</label><label for="__tabbed_4_3">Java</label><label for="__tabbed_4_4">C#</label><label for="__tabbed_4_5">Go</label><label for="__tabbed_4_6">Swift</label><label for="__tabbed_4_7">JS</label><label for="__tabbed_4_8">TS</label><label for="__tabbed_4_9">Dart</label><label for="__tabbed_4_10">Rust</label><label for="__tabbed_4_11">C</label><label for="__tabbed_4_12">Kotlin</label><label for="__tabbed_4_13">Ruby</label></div>
<div class="tabbed-content">
<div class="tabbed-block">
+6 -6
View File
@@ -4464,21 +4464,21 @@ G &amp; = \{ V, E \} \newline
<p><img alt="Связный и несвязный графы" class="animation-figure" src="../graph.assets/connected_graph.png" /></p>
<p align="center"> Рисунок 9-3 &nbsp; Связный и несвязный графы </p>
<p>Мы также можем добавить к ребрам переменную "вес" и получить показанный ниже <u>взвешенный граф (weighted graph)</u>. Например, в мобильных играх вроде Honor of Kings система рассчитывает "близость" между игроками по времени совместной игры, и такую сеть близости можно представить взвешенным графом.</p>
<p>Мы также можем добавить к ребрам переменную «вес» и получить <u>взвешенный граф (weighted graph)</u>, как показано на рисунке 9-4. Например, в мобильных играх вроде Honor of Kings система рассчитывает «близость» между игроками по времени совместной игры, и такую сеть близости можно представить взвешенным графом.</p>
<p><img alt="Взвешенный и невзвешенный графы" class="animation-figure" src="../graph.assets/weighted_graph.png" /></p>
<p align="center"> Рисунок 9-4 &nbsp; Взвешенный и невзвешенный графы </p>
<p>Со структурой данных "граф" связаны следующие основные термины.</p>
<p>Со структурой данных «граф» связаны следующие основные термины.</p>
<ul>
<li><u>Смежность (adjacency)</u>: если между двумя вершинами существует ребро, то такие вершины называются смежными. На рисунке 9-4 с вершиной 1 смежны вершины 2, 3 и 5.</li>
<li><u>Путь (path)</u>: последовательность ребер от вершины A до вершины B называется путем из A в B. На рисунке 9-4 последовательность ребер 1-5-2-4 является одним из путей от вершины 1 к вершине 4.</li>
<li><u>Степень (degree)</u>: количество ребер, принадлежащих вершине. Для ориентированного графа <u>входящая степень (in-degree)</u> показывает, сколько ребер входит в вершину, а <u>исходящая степень (out-degree)</u> показывает, сколько ребер из нее выходит.</li>
</ul>
<h2 id="912">9.1.2 &nbsp; Представление графа<a class="headerlink" href="#912" title="Permanent link">&para;</a></h2>
<p>Распространенные способы представления графа включают "матрицу смежности" и "список смежности". Ниже для примера рассматривается неориентированный граф.</p>
<p>Распространенные способы представления графа включают «матрицу смежности» и «список смежности». Ниже для примера рассматривается неориентированный граф.</p>
<h3 id="1">1. &nbsp; Матрица смежности<a class="headerlink" href="#1" title="Permanent link">&para;</a></h3>
<p>Пусть число вершин графа равно <span class="arithmatex">\(n\)</span> ; тогда <u>матрица смежности (adjacency matrix)</u> использует матрицу размера <span class="arithmatex">\(n \times n\)</span> для представления графа, где каждая строка и каждый столбец соответствуют вершине, а элементы матрицы показывают наличие или отсутствие ребра.</p>
<p>Как показано на рисунке 9-5, обозначим матрицу смежности через <span class="arithmatex">\(M\)</span> , а список вершин через <span class="arithmatex">\(V\)</span> ; тогда элемент матрицы <span class="arithmatex">\(M[i, j] = 1\)</span> означает наличие ребра между вершинами <span class="arithmatex">\(V[i]\)</span> и <span class="arithmatex">\(V[j]\)</span> , а элемент <span class="arithmatex">\(M[i, j] = 0\)</span> означает отсутствие ребра.</p>
<p>Пусть число вершин графа равно <span class="arithmatex">\(n\)</span>. Тогда <u>матрица смежности (adjacency matrix)</u> использует матрицу размера <span class="arithmatex">\(n \times n\)</span> для представления графа, где каждая строка и каждый столбец соответствуют вершине, а элементы матрицы показывают наличие или отсутствие ребра.</p>
<p>Как показано на рисунке 9-5, обозначим матрицу смежности через <span class="arithmatex">\(M\)</span> , а список вершин через <span class="arithmatex">\(V\)</span>. Тогда элемент матрицы <span class="arithmatex">\(M[i, j] = 1\)</span> означает наличие ребра между вершинами <span class="arithmatex">\(V[i]\)</span> и <span class="arithmatex">\(V[j]\)</span> , а элемент <span class="arithmatex">\(M[i, j] = 0\)</span> означает отсутствие ребра.</p>
<p><img alt="Представление графа матрицей смежности" class="animation-figure" src="../graph.assets/adjacency_matrix.png" /></p>
<p align="center"> Рисунок 9-5 &nbsp; Представление графа матрицей смежности </p>
@@ -4495,7 +4495,7 @@ G &amp; = \{ V, E \} \newline
<p align="center"> Рисунок 9-6 &nbsp; Представление графа списком смежности </p>
<p>Список смежности хранит только реально существующие ребра, а общее число ребер обычно значительно меньше <span class="arithmatex">\(n^2\)</span> , поэтому он лучше экономит память. Однако для поиска ребра в списке смежности требуется обходить список, поэтому по времени он уступает матрице смежности.</p>
<p>Если посмотреть на рисунок 9-6, можно заметить, что <strong>структура списка смежности очень похожа на цепную адресацию в хеш-таблицах, поэтому здесь можно использовать похожие методы оптимизации эффективности</strong>. Например, если список слишком длинный, его можно преобразовать в AVL-дерево или красно-черное дерево, чтобы снизить временную сложность с <span class="arithmatex">\(O(n)\)</span> до <span class="arithmatex">\(O(\log n)\)</span> ; также список можно преобразовать в хеш-таблицу, чтобы довести временную сложность до <span class="arithmatex">\(O(1)\)</span> .</p>
<p>Как видно на рисунке 9-6, <strong>структура списка смежности очень похожа на цепную адресацию в хеш-таблицах, поэтому здесь можно использовать похожие методы оптимизации эффективности</strong>. Например, если список слишком длинный, его можно преобразовать в AVL-дерево или красно-черное дерево, чтобы снизить временную сложность с <span class="arithmatex">\(O(n)\)</span> до <span class="arithmatex">\(O(\log n)\)</span>. Также список можно преобразовать в хеш-таблицу, чтобы довести временную сложность до <span class="arithmatex">\(O(1)\)</span> .</p>
<h2 id="913">9.1.3 &nbsp; Типичные применения графов<a class="headerlink" href="#913" title="Permanent link">&para;</a></h2>
<p>Как показано в таблице 9-1, многие реальные системы можно моделировать с помощью графов, а соответствующие задачи затем сводить к задачам вычислений на графах.</p>
<p align="center"> Таблица 9-1 &nbsp; Распространенные графы в реальной жизни </p>
+13 -13
View File
@@ -4379,14 +4379,14 @@
<!-- Page content -->
<h1 id="92">9.2 &nbsp; Базовые операции графа<a class="headerlink" href="#92" title="Permanent link">&para;</a></h1>
<p>Базовые операции графа можно разделить на операции над "ребрами" и операции над "вершинами". При двух способах представления, "матрице смежности" и "списке смежности", реализация этих операций различается.</p>
<p>Базовые операции графа можно разделить на операции над «ребрами» и операции над «вершинами». При двух способах представления, «матрице смежности» и «списке смежности», реализация этих операций различается.</p>
<h2 id="921">9.2.1 &nbsp; Реализация на основе матрицы смежности<a class="headerlink" href="#921" title="Permanent link">&para;</a></h2>
<p>Пусть дан неориентированный граф с числом вершин <span class="arithmatex">\(n\)</span> . Тогда способы реализации различных операций показаны на рисунках ниже.</p>
<p>Пусть дан неориентированный граф с числом вершин <span class="arithmatex">\(n\)</span> . Тогда способы реализации различных операций показаны на рисунке 9-7.</p>
<ul>
<li><strong>Добавление или удаление ребра</strong>: достаточно изменить соответствующее ребро в матрице смежности, что требует <span class="arithmatex">\(O(1)\)</span> времени. Поскольку граф неориентированный, необходимо одновременно обновить ребра в обоих направлениях.</li>
<li><strong>Добавление вершины</strong>: в конец матрицы смежности добавляется строка и столбец, полностью заполненные нулями; это требует <span class="arithmatex">\(O(n)\)</span> времени.</li>
<li><strong>Удаление вершины</strong>: из матрицы смежности удаляется строка и столбец. При удалении первой строки и первого столбца достигается худший случай, когда требуется "сдвинуть влево вверх" <span class="arithmatex">\((n-1)^2\)</span> элементов, поэтому используется <span class="arithmatex">\(O(n^2)\)</span> времени.</li>
<li><strong>Инициализация</strong>: передаются <span class="arithmatex">\(n\)</span> вершин, затем инициализируется список вершин <code>vertices</code> длины <span class="arithmatex">\(n\)</span> , что требует <span class="arithmatex">\(O(n)\)</span> времени; после этого инициализируется матрица смежности <code>adjMat</code> размера <span class="arithmatex">\(n \times n\)</span> , что требует <span class="arithmatex">\(O(n^2)\)</span> времени.</li>
<li><strong>Добавление вершины</strong>: в конец матрицы смежности добавляется строка и столбец, полностью заполненные нулями. Это требует <span class="arithmatex">\(O(n)\)</span> времени.</li>
<li><strong>Удаление вершины</strong>: из матрицы смежности удаляется строка и столбец. При удалении первой строки и первого столбца достигается худший случай, когда требуется «сдвинуть влево вверх» <span class="arithmatex">\((n-1)^2\)</span> элементов, поэтому используется <span class="arithmatex">\(O(n^2)\)</span> времени.</li>
<li><strong>Инициализация</strong>: передаются <span class="arithmatex">\(n\)</span> вершин, затем инициализируется список вершин <code>vertices</code> длины <span class="arithmatex">\(n\)</span> , что требует <span class="arithmatex">\(O(n)\)</span> времени. После этого инициализируется матрица смежности <code>adjMat</code> размера <span class="arithmatex">\(n \times n\)</span> , что требует <span class="arithmatex">\(O(n^2)\)</span> времени.</li>
</ul>
<div class="tabbed-set tabbed-alternate" data-tabs="1:5"><input checked="checked" id="__tabbed_1_1" name="__tabbed_1" type="radio" /><input id="__tabbed_1_2" name="__tabbed_1" type="radio" /><input id="__tabbed_1_3" name="__tabbed_1" type="radio" /><input id="__tabbed_1_4" name="__tabbed_1" type="radio" /><input id="__tabbed_1_5" name="__tabbed_1" type="radio" /><div class="tabbed-labels"><label for="__tabbed_1_1">&lt;1&gt;</label><label for="__tabbed_1_2">&lt;2&gt;</label><label for="__tabbed_1_3">&lt;3&gt;</label><label for="__tabbed_1_4">&lt;4&gt;</label><label for="__tabbed_1_5">&lt;5&gt;</label></div>
<div class="tabbed-content">
@@ -5566,13 +5566,13 @@
<div style="margin-top: 5px;"><a href="https://pythontutor.com/iframe-embed.html#code=class%20GraphAdjMat%3A%0A%0A%20%20%20%20def%20__init__%28self%2C%20vertices%3A%20list%5Bint%5D%2C%20edges%3A%20list%5Blist%5Bint%5D%5D%29%3A%0A%20%20%20%20%20%20%20%20self.vertices%3A%20list%5Bint%5D%20%3D%20%5B%5D%0A%20%20%20%20%20%20%20%20self.adj_mat%3A%20list%5Blist%5Bint%5D%5D%20%3D%20%5B%5D%0A%20%20%20%20%20%20%20%20for%20val%20in%20vertices%3A%0A%20%20%20%20%20%20%20%20%20%20%20%20self.add_vertex%28val%29%0A%20%20%20%20%20%20%20%20for%20e%20in%20edges%3A%0A%20%20%20%20%20%20%20%20%20%20%20%20self.add_edge%28e%5B0%5D%2C%20e%5B1%5D%29%0A%0A%20%20%20%20def%20size%28self%29%20-%3E%20int%3A%0A%20%20%20%20%20%20%20%20return%20len%28self.vertices%29%0A%0A%20%20%20%20def%20add_vertex%28self%2C%20val%3A%20int%29%3A%0A%20%20%20%20%20%20%20%20n%20%3D%20self.size%28%29%0A%20%20%20%20%20%20%20%20self.vertices.append%28val%29%0A%20%20%20%20%20%20%20%20new_row%20%3D%20%5B0%5D%20%2A%20n%0A%20%20%20%20%20%20%20%20self.adj_mat.append%28new_row%29%0A%20%20%20%20%20%20%20%20for%20row%20in%20self.adj_mat%3A%0A%20%20%20%20%20%20%20%20%20%20%20%20row.append%280%29%0A%0A%20%20%20%20def%20remove_vertex%28self%2C%20index%3A%20int%29%3A%0A%20%20%20%20%20%20%20%20if%20index%20%3E%3D%20self.size%28%29%3A%0A%20%20%20%20%20%20%20%20%20%20%20%20raise%20IndexError%28%29%0A%20%20%20%20%20%20%20%20self.vertices.pop%28index%29%0A%20%20%20%20%20%20%20%20self.adj_mat.pop%28index%29%0A%20%20%20%20%20%20%20%20for%20row%20in%20self.adj_mat%3A%0A%20%20%20%20%20%20%20%20%20%20%20%20row.pop%28index%29%0A%0A%20%20%20%20def%20add_edge%28self%2C%20i%3A%20int%2C%20j%3A%20int%29%3A%0A%20%20%20%20%20%20%20%20if%20i%20%3C%200%20or%20j%20%3C%200%20or%20i%20%3E%3D%20self.size%28%29%20or%20%28j%20%3E%3D%20self.size%28%29%29%20or%20%28i%20%3D%3D%20j%29%3A%0A%20%20%20%20%20%20%20%20%20%20%20%20raise%20IndexError%28%29%0A%20%20%20%20%20%20%20%20self.adj_mat%5Bi%5D%5Bj%5D%20%3D%201%0A%20%20%20%20%20%20%20%20self.adj_mat%5Bj%5D%5Bi%5D%20%3D%201%0A%0A%20%20%20%20def%20remove_edge%28self%2C%20i%3A%20int%2C%20j%3A%20int%29%3A%0A%20%20%20%20%20%20%20%20if%20i%20%3C%200%20or%20j%20%3C%200%20or%20i%20%3E%3D%20self.size%28%29%20or%20%28j%20%3E%3D%20self.size%28%29%29%20or%20%28i%20%3D%3D%20j%29%3A%0A%20%20%20%20%20%20%20%20%20%20%20%20raise%20IndexError%28%29%0A%20%20%20%20%20%20%20%20self.adj_mat%5Bi%5D%5Bj%5D%20%3D%200%0A%20%20%20%20%20%20%20%20self.adj_mat%5Bj%5D%5Bi%5D%20%3D%200%0A%27Driver%20Code%27%0Aif%20__name__%20%3D%3D%20%27__main__%27%3A%0A%20%20%20%20vertices%20%3D%20%5B1%2C%203%2C%202%2C%205%2C%204%5D%0A%20%20%20%20edges%20%3D%20%5B%5B0%2C%201%5D%2C%20%5B0%2C%203%5D%2C%20%5B1%2C%202%5D%2C%20%5B2%2C%203%5D%2C%20%5B2%2C%204%5D%2C%20%5B3%2C%204%5D%5D%0A%20%20%20%20graph%20%3D%20GraphAdjMat%28vertices%2C%20edges%29%0A%20%20%20%20graph.add_edge%280%2C%202%29%0A%20%20%20%20graph.remove_edge%280%2C%201%29%0A%20%20%20%20graph.add_vertex%286%29%0A%20%20%20%20graph.remove_vertex%281%29&codeDivHeight=800&codeDivWidth=600&cumulative=false&curInstr=3&heapPrimitives=nevernest&origin=opt-frontend.js&py=311&rawInputLstJSON=%5B%5D&textReferences=false" target="_blank" rel="noopener noreferrer">Во весь экран &gt;</a></div></p>
</details>
<h2 id="922">9.2.2 &nbsp; Реализация на основе списка смежности<a class="headerlink" href="#922" title="Permanent link">&para;</a></h2>
<p>Пусть неориентированный граф содержит в сумме <span class="arithmatex">\(n\)</span> вершин и <span class="arithmatex">\(m\)</span> ребер. Тогда различные операции можно реализовать способом, показанным на рисунках ниже.</p>
<p>Пусть неориентированный граф содержит в сумме <span class="arithmatex">\(n\)</span> вершин и <span class="arithmatex">\(m\)</span> ребер. Тогда различные операции можно реализовать способом, показанным на рисунке 9-8.</p>
<ul>
<li><strong>Добавление ребра</strong>: достаточно добавить ребро в конец списка, соответствующего вершине; это требует <span class="arithmatex">\(O(1)\)</span> времени. Поскольку граф неориентированный, необходимо одновременно добавить ребра в обоих направлениях.</li>
<li><strong>Удаление ребра</strong>: нужно найти и удалить указанное ребро в списке, соответствующем вершине; это требует <span class="arithmatex">\(O(m)\)</span> времени. В неориентированном графе необходимо удалить ребра в обоих направлениях.</li>
<li><strong>Добавление вершины</strong>: в список смежности добавляется еще один список, а новая вершина становится его головным узлом; это требует <span class="arithmatex">\(O(1)\)</span> времени.</li>
<li><strong>Удаление вершины</strong>: требуется пройти по всему списку смежности и удалить все ребра, содержащие указанную вершину; это требует <span class="arithmatex">\(O(n + m)\)</span> времени.</li>
<li><strong>Инициализация</strong>: в списке смежности создаются <span class="arithmatex">\(n\)</span> вершин и <span class="arithmatex">\(2m\)</span> ребер; это требует <span class="arithmatex">\(O(n + m)\)</span> времени.</li>
<li><strong>Добавление ребра</strong>: достаточно добавить ребро в конец списка, соответствующего вершине. Это требует <span class="arithmatex">\(O(1)\)</span> времени. Поскольку граф неориентированный, необходимо одновременно добавить ребра в обоих направлениях.</li>
<li><strong>Удаление ребра</strong>: нужно найти и удалить указанное ребро в списке, соответствующем вершине. Это требует <span class="arithmatex">\(O(m)\)</span> времени. В неориентированном графе необходимо удалить ребра в обоих направлениях.</li>
<li><strong>Добавление вершины</strong>: в список смежности добавляется еще один список, а новая вершина становится его головным узлом. Это требует <span class="arithmatex">\(O(1)\)</span> времени.</li>
<li><strong>Удаление вершины</strong>: требуется пройти по всему списку смежности и удалить все ребра, содержащие указанную вершину. Это требует <span class="arithmatex">\(O(n + m)\)</span> времени.</li>
<li><strong>Инициализация</strong>: в списке смежности создаются <span class="arithmatex">\(n\)</span> вершин и <span class="arithmatex">\(2m\)</span> ребер. Это требует <span class="arithmatex">\(O(n + m)\)</span> времени.</li>
</ul>
<div class="tabbed-set tabbed-alternate" data-tabs="3:5"><input checked="checked" id="__tabbed_3_1" name="__tabbed_3" type="radio" /><input id="__tabbed_3_2" name="__tabbed_3" type="radio" /><input id="__tabbed_3_3" name="__tabbed_3" type="radio" /><input id="__tabbed_3_4" name="__tabbed_3" type="radio" /><input id="__tabbed_3_5" name="__tabbed_3" type="radio" /><div class="tabbed-labels"><label for="__tabbed_3_1">&lt;1&gt;</label><label for="__tabbed_3_2">&lt;2&gt;</label><label for="__tabbed_3_3">&lt;3&gt;</label><label for="__tabbed_3_4">&lt;4&gt;</label><label for="__tabbed_3_5">&lt;5&gt;</label></div>
<div class="tabbed-content">
@@ -5595,7 +5595,7 @@
</div>
<p align="center"> Рисунок 9-8 &nbsp; Инициализация списка смежности, добавление и удаление ребер и вершин </p>
<p>Ниже приведен код списка смежности. По сравнению с рисунками выше, реальная реализация имеет следующие отличия.</p>
<p>Ниже приведен код списка смежности. По сравнению с тем, что показано на рисунке 9-8, реальная реализация имеет следующие отличия.</p>
<ul>
<li>Чтобы упростить добавление и удаление вершин, а также сделать код проще, мы используем список, то есть динамический массив, вместо связного списка.</li>
<li>Для хранения списка смежности используется хеш-таблица, где <code>key</code> - это экземпляр вершины, а <code>value</code> - список смежных вершин данной вершины.</li>
@@ -6757,7 +6757,7 @@
</tbody>
</table>
</div>
<p>Если смотреть только на таблицу, может показаться, что список смежности на основе хеш-таблицы является лучшим и по времени, и по памяти. Но на практике операции над ребрами в матрице смежности обычно выполняются быстрее, потому что там нужен лишь один доступ к массиву или одно присваивание. В целом матрица смежности воплощает принцип "обмена пространства на время", а список смежности - принцип "обмена времени на пространство".</p>
<p>Если судить только по данным в таблице 9-2, может показаться, что список смежности на основе хеш-таблицы является лучшим и по времени, и по памяти. Но на практике операции над ребрами в матрице смежности обычно выполняются быстрее, потому что там нужен лишь один доступ к массиву или одно присваивание. В целом матрица смежности воплощает принцип «обмена пространства на время», а список смежности - принцип «обмена времени на пространство».</p>
<!-- Source file information -->
+10 -10
View File
@@ -4469,7 +4469,7 @@
<!-- Page content -->
<h1 id="93">9.3 &nbsp; Обход графа<a class="headerlink" href="#93" title="Permanent link">&para;</a></h1>
<p>Дерево представляет отношение "один ко многим", тогда как граф обладает большей свободой и может выражать произвольные отношения "многие ко многим". Поэтому дерево можно рассматривать как частный случай графа. Очевидно, что <strong>операции обхода дерева также являются частным случаем операций обхода графа</strong>.</p>
<p>Дерево представляет отношение «один ко многим», тогда как граф обладает большей свободой и может выражать произвольные отношения «многие ко многим». Поэтому дерево можно рассматривать как частный случай графа. Очевидно, что <strong>операции обхода дерева также являются частным случаем операций обхода графа</strong>.</p>
<p>И графы, и деревья требуют применения алгоритмов обхода. Способы обхода графа также делятся на два типа: <u>обход в ширину</u> и <u>обход в глубину</u>.</p>
<h2 id="931">9.3.1 &nbsp; Обход в ширину<a class="headerlink" href="#931" title="Permanent link">&para;</a></h2>
<p><strong>Обход в ширину - это способ обхода от ближнего к дальнему, при котором начиная с некоторого узла сначала посещают ближайшие вершины, а затем слой за слоем расширяются наружу</strong>. Как показано на рисунке 9-9, начиная с вершины в левом верхнем углу, мы сначала обходим все смежные вершины этой вершины, затем все смежные вершины следующей вершины и так далее, пока не будут посещены все вершины.</p>
@@ -4477,7 +4477,7 @@
<p align="center"> Рисунок 9-9 &nbsp; Обход графа в ширину </p>
<h3 id="1">1. &nbsp; Реализация алгоритма<a class="headerlink" href="#1" title="Permanent link">&para;</a></h3>
<p>BFS обычно реализуется с помощью очереди, код приведен ниже. Очередь обладает свойством "первым пришел - первым вышел", что хорошо соответствует идее BFS "от ближнего к дальнему".</p>
<p>BFS обычно реализуется с помощью очереди, код приведен ниже. Очередь обладает свойством «первым пришел - первым вышел», что хорошо соответствует идее BFS «от ближнего к дальнему».</p>
<ol>
<li>Поместить стартовую вершину обхода <code>startVet</code> в очередь и запустить цикл.</li>
<li>На каждой итерации цикла извлекать вершину из головы очереди и записывать ее посещение, после чего добавлять все смежные вершины этой вершины в хвост очереди.</li>
@@ -4916,7 +4916,7 @@
<p><div style="height: 549px; width: 100%;"><iframe class="pythontutor-iframe" src="https://pythontutor.com/iframe-embed.html#code=from%20collections%20import%20deque%0A%0Aclass%20Vertex%3A%0A%0A%20%20%20%20def%20__init__%28self%2C%20val%3A%20int%29%3A%0A%20%20%20%20%20%20%20%20self.val%20%3D%20val%0A%0Adef%20vals_to_vets%28vals%3A%20list%5Bint%5D%29%20-%3E%20list%5B%27Vertex%27%5D%3A%0A%20%20%20%20return%20%5BVertex%28val%29%20for%20val%20in%20vals%5D%0A%0Aclass%20GraphAdjList%3A%0A%0A%20%20%20%20def%20__init__%28self%2C%20edges%3A%20list%5Blist%5BVertex%5D%5D%29%3A%0A%20%20%20%20%20%20%20%20self.adj_list%20%3D%20dict%5BVertex%2C%20list%5BVertex%5D%5D%28%29%0A%20%20%20%20%20%20%20%20for%20edge%20in%20edges%3A%0A%20%20%20%20%20%20%20%20%20%20%20%20self.add_vertex%28edge%5B0%5D%29%0A%20%20%20%20%20%20%20%20%20%20%20%20self.add_vertex%28edge%5B1%5D%29%0A%20%20%20%20%20%20%20%20%20%20%20%20self.add_edge%28edge%5B0%5D%2C%20edge%5B1%5D%29%0A%0A%20%20%20%20def%20add_edge%28self%2C%20vet1%3A%20Vertex%2C%20vet2%3A%20Vertex%29%3A%0A%20%20%20%20%20%20%20%20if%20vet1%20not%20in%20self.adj_list%20or%20vet2%20not%20in%20self.adj_list%20or%20vet1%20%3D%3D%20vet2%3A%0A%20%20%20%20%20%20%20%20%20%20%20%20raise%20ValueError%28%29%0A%20%20%20%20%20%20%20%20self.adj_list%5Bvet1%5D.append%28vet2%29%0A%20%20%20%20%20%20%20%20self.adj_list%5Bvet2%5D.append%28vet1%29%0A%0A%20%20%20%20def%20add_vertex%28self%2C%20vet%3A%20Vertex%29%3A%0A%20%20%20%20%20%20%20%20if%20vet%20in%20self.adj_list%3A%0A%20%20%20%20%20%20%20%20%20%20%20%20return%0A%20%20%20%20%20%20%20%20self.adj_list%5Bvet%5D%20%3D%20%5B%5D%0A%0Adef%20graph_bfs%28graph%3A%20GraphAdjList%2C%20start_vet%3A%20Vertex%29%20-%3E%20list%5BVertex%5D%3A%0A%20%20%20%20res%20%3D%20%5B%5D%0A%20%20%20%20visited%20%3D%20set%5BVertex%5D%28%5Bstart_vet%5D%29%0A%20%20%20%20que%20%3D%20deque%5BVertex%5D%28%5Bstart_vet%5D%29%0A%20%20%20%20while%20len%28que%29%20%3E%200%3A%0A%20%20%20%20%20%20%20%20vet%20%3D%20que.popleft%28%29%0A%20%20%20%20%20%20%20%20res.append%28vet%29%0A%20%20%20%20%20%20%20%20for%20adj_vet%20in%20graph.adj_list%5Bvet%5D%3A%0A%20%20%20%20%20%20%20%20%20%20%20%20if%20adj_vet%20in%20visited%3A%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20continue%0A%20%20%20%20%20%20%20%20%20%20%20%20que.append%28adj_vet%29%0A%20%20%20%20%20%20%20%20%20%20%20%20visited.add%28adj_vet%29%0A%20%20%20%20return%20res%0A%27Driver%20Code%27%0Aif%20__name__%20%3D%3D%20%27__main__%27%3A%0A%20%20%20%20v%20%3D%20vals_to_vets%28%5B0%2C%201%2C%202%2C%203%2C%204%5D%29%0A%20%20%20%20edges%20%3D%20%5B%5Bv%5B0%5D%2C%20v%5B1%5D%5D%2C%20%5Bv%5B0%5D%2C%20v%5B3%5D%5D%2C%20%5Bv%5B1%5D%2C%20v%5B2%5D%5D%2C%20%5Bv%5B1%5D%2C%20v%5B4%5D%5D%2C%20%5Bv%5B3%5D%2C%20v%5B4%5D%5D%5D%0A%20%20%20%20graph%20%3D%20GraphAdjList%28edges%29%0A%20%20%20%20del%20edges%0A%20%20%20%20res%20%3D%20graph_bfs%28graph%2C%20v%5B0%5D%29&codeDivHeight=472&codeDivWidth=350&cumulative=false&curInstr=131&heapPrimitives=nevernest&origin=opt-frontend.js&py=311&rawInputLstJSON=%5B%5D&textReferences=false"> </iframe></div>
<div style="margin-top: 5px;"><a href="https://pythontutor.com/iframe-embed.html#code=from%20collections%20import%20deque%0A%0Aclass%20Vertex%3A%0A%0A%20%20%20%20def%20__init__%28self%2C%20val%3A%20int%29%3A%0A%20%20%20%20%20%20%20%20self.val%20%3D%20val%0A%0Adef%20vals_to_vets%28vals%3A%20list%5Bint%5D%29%20-%3E%20list%5B%27Vertex%27%5D%3A%0A%20%20%20%20return%20%5BVertex%28val%29%20for%20val%20in%20vals%5D%0A%0Aclass%20GraphAdjList%3A%0A%0A%20%20%20%20def%20__init__%28self%2C%20edges%3A%20list%5Blist%5BVertex%5D%5D%29%3A%0A%20%20%20%20%20%20%20%20self.adj_list%20%3D%20dict%5BVertex%2C%20list%5BVertex%5D%5D%28%29%0A%20%20%20%20%20%20%20%20for%20edge%20in%20edges%3A%0A%20%20%20%20%20%20%20%20%20%20%20%20self.add_vertex%28edge%5B0%5D%29%0A%20%20%20%20%20%20%20%20%20%20%20%20self.add_vertex%28edge%5B1%5D%29%0A%20%20%20%20%20%20%20%20%20%20%20%20self.add_edge%28edge%5B0%5D%2C%20edge%5B1%5D%29%0A%0A%20%20%20%20def%20add_edge%28self%2C%20vet1%3A%20Vertex%2C%20vet2%3A%20Vertex%29%3A%0A%20%20%20%20%20%20%20%20if%20vet1%20not%20in%20self.adj_list%20or%20vet2%20not%20in%20self.adj_list%20or%20vet1%20%3D%3D%20vet2%3A%0A%20%20%20%20%20%20%20%20%20%20%20%20raise%20ValueError%28%29%0A%20%20%20%20%20%20%20%20self.adj_list%5Bvet1%5D.append%28vet2%29%0A%20%20%20%20%20%20%20%20self.adj_list%5Bvet2%5D.append%28vet1%29%0A%0A%20%20%20%20def%20add_vertex%28self%2C%20vet%3A%20Vertex%29%3A%0A%20%20%20%20%20%20%20%20if%20vet%20in%20self.adj_list%3A%0A%20%20%20%20%20%20%20%20%20%20%20%20return%0A%20%20%20%20%20%20%20%20self.adj_list%5Bvet%5D%20%3D%20%5B%5D%0A%0Adef%20graph_bfs%28graph%3A%20GraphAdjList%2C%20start_vet%3A%20Vertex%29%20-%3E%20list%5BVertex%5D%3A%0A%20%20%20%20res%20%3D%20%5B%5D%0A%20%20%20%20visited%20%3D%20set%5BVertex%5D%28%5Bstart_vet%5D%29%0A%20%20%20%20que%20%3D%20deque%5BVertex%5D%28%5Bstart_vet%5D%29%0A%20%20%20%20while%20len%28que%29%20%3E%200%3A%0A%20%20%20%20%20%20%20%20vet%20%3D%20que.popleft%28%29%0A%20%20%20%20%20%20%20%20res.append%28vet%29%0A%20%20%20%20%20%20%20%20for%20adj_vet%20in%20graph.adj_list%5Bvet%5D%3A%0A%20%20%20%20%20%20%20%20%20%20%20%20if%20adj_vet%20in%20visited%3A%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20continue%0A%20%20%20%20%20%20%20%20%20%20%20%20que.append%28adj_vet%29%0A%20%20%20%20%20%20%20%20%20%20%20%20visited.add%28adj_vet%29%0A%20%20%20%20return%20res%0A%27Driver%20Code%27%0Aif%20__name__%20%3D%3D%20%27__main__%27%3A%0A%20%20%20%20v%20%3D%20vals_to_vets%28%5B0%2C%201%2C%202%2C%203%2C%204%5D%29%0A%20%20%20%20edges%20%3D%20%5B%5Bv%5B0%5D%2C%20v%5B1%5D%5D%2C%20%5Bv%5B0%5D%2C%20v%5B3%5D%5D%2C%20%5Bv%5B1%5D%2C%20v%5B2%5D%5D%2C%20%5Bv%5B1%5D%2C%20v%5B4%5D%5D%2C%20%5Bv%5B3%5D%2C%20v%5B4%5D%5D%5D%0A%20%20%20%20graph%20%3D%20GraphAdjList%28edges%29%0A%20%20%20%20del%20edges%0A%20%20%20%20res%20%3D%20graph_bfs%28graph%2C%20v%5B0%5D%29&codeDivHeight=800&codeDivWidth=600&cumulative=false&curInstr=131&heapPrimitives=nevernest&origin=opt-frontend.js&py=311&rawInputLstJSON=%5B%5D&textReferences=false" target="_blank" rel="noopener noreferrer">Во весь экран &gt;</a></div></p>
</details>
<p>Код сравнительно абстрактен, поэтому рекомендуется сверяться с рисунками ниже для лучшего понимания.</p>
<p>Код сравнительно абстрактен, поэтому для лучшего понимания рекомендуется сопоставлять его с тем, что показано на рисунке 9-10.</p>
<div class="tabbed-set tabbed-alternate" data-tabs="2:11"><input checked="checked" id="__tabbed_2_1" name="__tabbed_2" type="radio" /><input id="__tabbed_2_2" name="__tabbed_2" type="radio" /><input id="__tabbed_2_3" name="__tabbed_2" type="radio" /><input id="__tabbed_2_4" name="__tabbed_2" type="radio" /><input id="__tabbed_2_5" name="__tabbed_2" type="radio" /><input id="__tabbed_2_6" name="__tabbed_2" type="radio" /><input id="__tabbed_2_7" name="__tabbed_2" type="radio" /><input id="__tabbed_2_8" name="__tabbed_2" type="radio" /><input id="__tabbed_2_9" name="__tabbed_2" type="radio" /><input id="__tabbed_2_10" name="__tabbed_2" type="radio" /><input id="__tabbed_2_11" name="__tabbed_2" type="radio" /><div class="tabbed-labels"><label for="__tabbed_2_1">&lt;1&gt;</label><label for="__tabbed_2_2">&lt;2&gt;</label><label for="__tabbed_2_3">&lt;3&gt;</label><label for="__tabbed_2_4">&lt;4&gt;</label><label for="__tabbed_2_5">&lt;5&gt;</label><label for="__tabbed_2_6">&lt;6&gt;</label><label for="__tabbed_2_7">&lt;7&gt;</label><label for="__tabbed_2_8">&lt;8&gt;</label><label for="__tabbed_2_9">&lt;9&gt;</label><label for="__tabbed_2_10">&lt;10&gt;</label><label for="__tabbed_2_11">&lt;11&gt;</label></div>
<div class="tabbed-content">
<div class="tabbed-block">
@@ -4958,10 +4958,10 @@
<div class="admonition question">
<p class="admonition-title">Является ли последовательность обхода в ширину единственной?</p>
<p>Нет. Обход в ширину требует только соблюдения порядка "от ближнего к дальнему", <strong>а порядок обхода нескольких вершин на одинаковом расстоянии может произвольно меняться</strong>. Например, на рисунке 9-10 можно поменять местами порядок посещения вершин <span class="arithmatex">\(1\)</span> и <span class="arithmatex">\(3\)</span> , а вершины <span class="arithmatex">\(2\)</span>, <span class="arithmatex">\(4\)</span>, <span class="arithmatex">\(6\)</span> также можно переставлять произвольно.</p>
<p>Нет. Обход в ширину требует только соблюдения порядка «от ближнего к дальнему», <strong>а порядок обхода нескольких вершин на одинаковом расстоянии может произвольно меняться</strong>. Например, на рисунке 9-10 можно поменять местами порядок посещения вершин <span class="arithmatex">\(1\)</span> и <span class="arithmatex">\(3\)</span> , а вершины <span class="arithmatex">\(2\)</span>, <span class="arithmatex">\(4\)</span>, <span class="arithmatex">\(6\)</span> также можно переставлять произвольно.</p>
</div>
<h3 id="2">2. &nbsp; Анализ сложности<a class="headerlink" href="#2" title="Permanent link">&para;</a></h3>
<p><strong>Временная сложность</strong>: все вершины по одному разу помещаются в очередь и извлекаются из нее, что требует <span class="arithmatex">\(O(|V|)\)</span> времени; при обходе смежных вершин, поскольку граф неориентированный, все ребра будут посещены по <span class="arithmatex">\(2\)</span> раза, что требует <span class="arithmatex">\(O(2|E|)\)</span> времени; в сумме получается <span class="arithmatex">\(O(|V| + |E|)\)</span> .</p>
<p><strong>Временная сложность</strong>: все вершины по одному разу помещаются в очередь и извлекаются из нее, что требует <span class="arithmatex">\(O(|V|)\)</span> времени. При обходе смежных вершин, поскольку граф неориентированный, все ребра будут посещены по <span class="arithmatex">\(2\)</span> раза, что требует <span class="arithmatex">\(O(2|E|)\)</span> времени. В сумме получается <span class="arithmatex">\(O(|V| + |E|)\)</span> .</p>
<p><strong>Пространственная сложность</strong>: список <code>res</code> , хеш-множество <code>visited</code> и очередь <code>que</code> в худшем случае могут содержать до <span class="arithmatex">\(|V|\)</span> вершин, поэтому требуется <span class="arithmatex">\(O(|V|)\)</span> памяти.</p>
<h2 id="932">9.3.2 &nbsp; Обход в глубину<a class="headerlink" href="#932" title="Permanent link">&para;</a></h2>
<p><strong>Обход в глубину - это способ обхода, при котором сначала идут до самого конца, а когда дальше идти нельзя, возвращаются назад</strong>. Как показано на рисунке 9-11, начиная с вершины в левом верхнем углу, мы выбираем некоторую смежную вершину текущей вершины, идем до упора, затем возвращаемся назад, снова идем до упора и так далее, пока не будут посещены все вершины.</p>
@@ -4969,7 +4969,7 @@
<p align="center"> Рисунок 9-11 &nbsp; Обход графа в глубину </p>
<h3 id="1_1">1. &nbsp; Реализация алгоритма<a class="headerlink" href="#1_1" title="Permanent link">&para;</a></h3>
<p>Такой алгоритмический шаблон "дойти до конца и вернуться" обычно реализуется через рекурсию. Подобно обходу в ширину, в обходе в глубину мы также используем хеш-множество <code>visited</code> для записи уже посещенных вершин и тем самым избегаем повторного посещения.</p>
<p>Такой алгоритмический шаблон «дойти до конца и вернуться» обычно реализуется через рекурсию. Подобно обходу в ширину, в обходе в глубину мы также используем хеш-множество <code>visited</code> для записи уже посещенных вершин и тем самым избегаем повторного посещения.</p>
<div class="tabbed-set tabbed-alternate" data-tabs="3:13"><input checked="checked" id="__tabbed_3_1" name="__tabbed_3" type="radio" /><input id="__tabbed_3_2" name="__tabbed_3" type="radio" /><input id="__tabbed_3_3" name="__tabbed_3" type="radio" /><input id="__tabbed_3_4" name="__tabbed_3" type="radio" /><input id="__tabbed_3_5" name="__tabbed_3" type="radio" /><input id="__tabbed_3_6" name="__tabbed_3" type="radio" /><input id="__tabbed_3_7" name="__tabbed_3" type="radio" /><input id="__tabbed_3_8" name="__tabbed_3" type="radio" /><input id="__tabbed_3_9" name="__tabbed_3" type="radio" /><input id="__tabbed_3_10" name="__tabbed_3" type="radio" /><input id="__tabbed_3_11" name="__tabbed_3" type="radio" /><input id="__tabbed_3_12" name="__tabbed_3" type="radio" /><input id="__tabbed_3_13" name="__tabbed_3" type="radio" /><div class="tabbed-labels"><label for="__tabbed_3_1">Python</label><label for="__tabbed_3_2">C++</label><label for="__tabbed_3_3">Java</label><label for="__tabbed_3_4">C#</label><label for="__tabbed_3_5">Go</label><label for="__tabbed_3_6">Swift</label><label for="__tabbed_3_7">JS</label><label for="__tabbed_3_8">TS</label><label for="__tabbed_3_9">Dart</label><label for="__tabbed_3_10">Rust</label><label for="__tabbed_3_11">C</label><label for="__tabbed_3_12">Kotlin</label><label for="__tabbed_3_13">Ruby</label></div>
<div class="tabbed-content">
<div class="tabbed-block">
@@ -5349,12 +5349,12 @@
<p><div style="height: 549px; width: 100%;"><iframe class="pythontutor-iframe" src="https://pythontutor.com/iframe-embed.html#code=class%20Vertex%3A%0A%0A%20%20%20%20def%20__init__%28self%2C%20val%3A%20int%29%3A%0A%20%20%20%20%20%20%20%20self.val%20%3D%20val%0A%0Adef%20vals_to_vets%28vals%3A%20list%5Bint%5D%29%20-%3E%20list%5B%27Vertex%27%5D%3A%0A%20%20%20%20return%20%5BVertex%28val%29%20for%20val%20in%20vals%5D%0A%0Aclass%20GraphAdjList%3A%0A%0A%20%20%20%20def%20__init__%28self%2C%20edges%3A%20list%5Blist%5BVertex%5D%5D%29%3A%0A%20%20%20%20%20%20%20%20self.adj_list%20%3D%20dict%5BVertex%2C%20list%5BVertex%5D%5D%28%29%0A%20%20%20%20%20%20%20%20for%20edge%20in%20edges%3A%0A%20%20%20%20%20%20%20%20%20%20%20%20self.add_vertex%28edge%5B0%5D%29%0A%20%20%20%20%20%20%20%20%20%20%20%20self.add_vertex%28edge%5B1%5D%29%0A%20%20%20%20%20%20%20%20%20%20%20%20self.add_edge%28edge%5B0%5D%2C%20edge%5B1%5D%29%0A%0A%20%20%20%20def%20add_edge%28self%2C%20vet1%3A%20Vertex%2C%20vet2%3A%20Vertex%29%3A%0A%20%20%20%20%20%20%20%20if%20vet1%20not%20in%20self.adj_list%20or%20vet2%20not%20in%20self.adj_list%20or%20vet1%20%3D%3D%20vet2%3A%0A%20%20%20%20%20%20%20%20%20%20%20%20raise%20ValueError%28%29%0A%20%20%20%20%20%20%20%20self.adj_list%5Bvet1%5D.append%28vet2%29%0A%20%20%20%20%20%20%20%20self.adj_list%5Bvet2%5D.append%28vet1%29%0A%0A%20%20%20%20def%20add_vertex%28self%2C%20vet%3A%20Vertex%29%3A%0A%20%20%20%20%20%20%20%20if%20vet%20in%20self.adj_list%3A%0A%20%20%20%20%20%20%20%20%20%20%20%20return%0A%20%20%20%20%20%20%20%20self.adj_list%5Bvet%5D%20%3D%20%5B%5D%0A%0Adef%20dfs%28graph%3A%20GraphAdjList%2C%20visited%3A%20set%5BVertex%5D%2C%20res%3A%20list%5BVertex%5D%2C%20vet%3A%20Vertex%29%3A%0A%20%20%20%20res.append%28vet%29%0A%20%20%20%20visited.add%28vet%29%0A%20%20%20%20for%20adjVet%20in%20graph.adj_list%5Bvet%5D%3A%0A%20%20%20%20%20%20%20%20if%20adjVet%20in%20visited%3A%0A%20%20%20%20%20%20%20%20%20%20%20%20continue%0A%20%20%20%20%20%20%20%20dfs%28graph%2C%20visited%2C%20res%2C%20adjVet%29%0A%0Adef%20graph_dfs%28graph%3A%20GraphAdjList%2C%20start_vet%3A%20Vertex%29%20-%3E%20list%5BVertex%5D%3A%0A%20%20%20%20res%20%3D%20%5B%5D%0A%20%20%20%20visited%20%3D%20set%5BVertex%5D%28%29%0A%20%20%20%20dfs%28graph%2C%20visited%2C%20res%2C%20start_vet%29%0A%20%20%20%20return%20res%0A%27Driver%20Code%27%0Aif%20__name__%20%3D%3D%20%27__main__%27%3A%0A%20%20%20%20v%20%3D%20vals_to_vets%28%5B0%2C%201%2C%202%2C%203%2C%204%5D%29%0A%20%20%20%20edges%20%3D%20%5B%5Bv%5B0%5D%2C%20v%5B1%5D%5D%2C%20%5Bv%5B0%5D%2C%20v%5B3%5D%5D%2C%20%5Bv%5B1%5D%2C%20v%5B2%5D%5D%2C%20%5Bv%5B1%5D%2C%20v%5B4%5D%5D%2C%20%5Bv%5B3%5D%2C%20v%5B4%5D%5D%5D%0A%20%20%20%20graph%20%3D%20GraphAdjList%28edges%29%0A%20%20%20%20res%20%3D%20graph_dfs%28graph%2C%20v%5B0%5D%29&codeDivHeight=472&codeDivWidth=350&cumulative=false&curInstr=130&heapPrimitives=nevernest&origin=opt-frontend.js&py=311&rawInputLstJSON=%5B%5D&textReferences=false"> </iframe></div>
<div style="margin-top: 5px;"><a href="https://pythontutor.com/iframe-embed.html#code=class%20Vertex%3A%0A%0A%20%20%20%20def%20__init__%28self%2C%20val%3A%20int%29%3A%0A%20%20%20%20%20%20%20%20self.val%20%3D%20val%0A%0Adef%20vals_to_vets%28vals%3A%20list%5Bint%5D%29%20-%3E%20list%5B%27Vertex%27%5D%3A%0A%20%20%20%20return%20%5BVertex%28val%29%20for%20val%20in%20vals%5D%0A%0Aclass%20GraphAdjList%3A%0A%0A%20%20%20%20def%20__init__%28self%2C%20edges%3A%20list%5Blist%5BVertex%5D%5D%29%3A%0A%20%20%20%20%20%20%20%20self.adj_list%20%3D%20dict%5BVertex%2C%20list%5BVertex%5D%5D%28%29%0A%20%20%20%20%20%20%20%20for%20edge%20in%20edges%3A%0A%20%20%20%20%20%20%20%20%20%20%20%20self.add_vertex%28edge%5B0%5D%29%0A%20%20%20%20%20%20%20%20%20%20%20%20self.add_vertex%28edge%5B1%5D%29%0A%20%20%20%20%20%20%20%20%20%20%20%20self.add_edge%28edge%5B0%5D%2C%20edge%5B1%5D%29%0A%0A%20%20%20%20def%20add_edge%28self%2C%20vet1%3A%20Vertex%2C%20vet2%3A%20Vertex%29%3A%0A%20%20%20%20%20%20%20%20if%20vet1%20not%20in%20self.adj_list%20or%20vet2%20not%20in%20self.adj_list%20or%20vet1%20%3D%3D%20vet2%3A%0A%20%20%20%20%20%20%20%20%20%20%20%20raise%20ValueError%28%29%0A%20%20%20%20%20%20%20%20self.adj_list%5Bvet1%5D.append%28vet2%29%0A%20%20%20%20%20%20%20%20self.adj_list%5Bvet2%5D.append%28vet1%29%0A%0A%20%20%20%20def%20add_vertex%28self%2C%20vet%3A%20Vertex%29%3A%0A%20%20%20%20%20%20%20%20if%20vet%20in%20self.adj_list%3A%0A%20%20%20%20%20%20%20%20%20%20%20%20return%0A%20%20%20%20%20%20%20%20self.adj_list%5Bvet%5D%20%3D%20%5B%5D%0A%0Adef%20dfs%28graph%3A%20GraphAdjList%2C%20visited%3A%20set%5BVertex%5D%2C%20res%3A%20list%5BVertex%5D%2C%20vet%3A%20Vertex%29%3A%0A%20%20%20%20res.append%28vet%29%0A%20%20%20%20visited.add%28vet%29%0A%20%20%20%20for%20adjVet%20in%20graph.adj_list%5Bvet%5D%3A%0A%20%20%20%20%20%20%20%20if%20adjVet%20in%20visited%3A%0A%20%20%20%20%20%20%20%20%20%20%20%20continue%0A%20%20%20%20%20%20%20%20dfs%28graph%2C%20visited%2C%20res%2C%20adjVet%29%0A%0Adef%20graph_dfs%28graph%3A%20GraphAdjList%2C%20start_vet%3A%20Vertex%29%20-%3E%20list%5BVertex%5D%3A%0A%20%20%20%20res%20%3D%20%5B%5D%0A%20%20%20%20visited%20%3D%20set%5BVertex%5D%28%29%0A%20%20%20%20dfs%28graph%2C%20visited%2C%20res%2C%20start_vet%29%0A%20%20%20%20return%20res%0A%27Driver%20Code%27%0Aif%20__name__%20%3D%3D%20%27__main__%27%3A%0A%20%20%20%20v%20%3D%20vals_to_vets%28%5B0%2C%201%2C%202%2C%203%2C%204%5D%29%0A%20%20%20%20edges%20%3D%20%5B%5Bv%5B0%5D%2C%20v%5B1%5D%5D%2C%20%5Bv%5B0%5D%2C%20v%5B3%5D%5D%2C%20%5Bv%5B1%5D%2C%20v%5B2%5D%5D%2C%20%5Bv%5B1%5D%2C%20v%5B4%5D%5D%2C%20%5Bv%5B3%5D%2C%20v%5B4%5D%5D%5D%0A%20%20%20%20graph%20%3D%20GraphAdjList%28edges%29%0A%20%20%20%20res%20%3D%20graph_dfs%28graph%2C%20v%5B0%5D%29&codeDivHeight=800&codeDivWidth=600&cumulative=false&curInstr=130&heapPrimitives=nevernest&origin=opt-frontend.js&py=311&rawInputLstJSON=%5B%5D&textReferences=false" target="_blank" rel="noopener noreferrer">Во весь экран &gt;</a></div></p>
</details>
<p>Алгоритмический процесс обхода в глубину показан на рисунках ниже.</p>
<p>Алгоритмический процесс обхода в глубину показан на рисунке 9-12.</p>
<ul>
<li><strong>Прямая пунктирная линия обозначает нисходящую рекурсию</strong> , то есть запуск нового рекурсивного метода для посещения новой вершины.</li>
<li><strong>Изогнутая пунктирная линия обозначает восходящую рекурсию</strong> , то есть данный рекурсивный метод завершился и управление вернулось туда, откуда он был вызван.</li>
</ul>
<p>Чтобы лучше понять алгоритм, рекомендуется совместить рисунки ниже с кодом и мысленно проследить весь процесс DFS, включая моменты запуска и возврата каждого рекурсивного вызова.</p>
<p>Чтобы лучше понять алгоритм, рекомендуется сопоставить код с тем, что показано на рисунке 9-12, и мысленно проследить весь процесс DFS, включая моменты запуска и возврата каждого рекурсивного вызова.</p>
<div class="tabbed-set tabbed-alternate" data-tabs="4:11"><input checked="checked" id="__tabbed_4_1" name="__tabbed_4" type="radio" /><input id="__tabbed_4_2" name="__tabbed_4" type="radio" /><input id="__tabbed_4_3" name="__tabbed_4" type="radio" /><input id="__tabbed_4_4" name="__tabbed_4" type="radio" /><input id="__tabbed_4_5" name="__tabbed_4" type="radio" /><input id="__tabbed_4_6" name="__tabbed_4" type="radio" /><input id="__tabbed_4_7" name="__tabbed_4" type="radio" /><input id="__tabbed_4_8" name="__tabbed_4" type="radio" /><input id="__tabbed_4_9" name="__tabbed_4" type="radio" /><input id="__tabbed_4_10" name="__tabbed_4" type="radio" /><input id="__tabbed_4_11" name="__tabbed_4" type="radio" /><div class="tabbed-labels"><label for="__tabbed_4_1">&lt;1&gt;</label><label for="__tabbed_4_2">&lt;2&gt;</label><label for="__tabbed_4_3">&lt;3&gt;</label><label for="__tabbed_4_4">&lt;4&gt;</label><label for="__tabbed_4_5">&lt;5&gt;</label><label for="__tabbed_4_6">&lt;6&gt;</label><label for="__tabbed_4_7">&lt;7&gt;</label><label for="__tabbed_4_8">&lt;8&gt;</label><label for="__tabbed_4_9">&lt;9&gt;</label><label for="__tabbed_4_10">&lt;10&gt;</label><label for="__tabbed_4_11">&lt;11&gt;</label></div>
<div class="tabbed-content">
<div class="tabbed-block">
@@ -5397,10 +5397,10 @@
<div class="admonition question">
<p class="admonition-title">Является ли последовательность обхода в глубину единственной?</p>
<p>Как и в случае обхода в ширину, последовательность DFS тоже не является единственной. Для заданной вершины допустимо сначала углубиться в любое направление, то есть порядок смежных вершин может быть произвольным, и все такие варианты будут корректными обходами в глубину.</p>
<p>Если взять в качестве примера обход дерева, то варианты "корень <span class="arithmatex">\(\rightarrow\)</span> лево <span class="arithmatex">\(\rightarrow\)</span> право", "лево <span class="arithmatex">\(\rightarrow\)</span> корень <span class="arithmatex">\(\rightarrow\)</span> право" и "лево <span class="arithmatex">\(\rightarrow\)</span> право <span class="arithmatex">\(\rightarrow\)</span> корень" соответствуют прямому, симметричному и обратному обходам соответственно. Они показывают три разных приоритета обхода, но все они относятся к обходу в глубину.</p>
<p>Если взять в качестве примера обход дерева, то варианты «корень <span class="arithmatex">\(\rightarrow\)</span> лево <span class="arithmatex">\(\rightarrow\)</span> право», «лево <span class="arithmatex">\(\rightarrow\)</span> корень <span class="arithmatex">\(\rightarrow\)</span> право» и «лево <span class="arithmatex">\(\rightarrow\)</span> право <span class="arithmatex">\(\rightarrow\)</span> корень» соответствуют прямому, симметричному и обратному обходам соответственно. Они показывают три разных приоритета обхода, но все они относятся к обходу в глубину.</p>
</div>
<h3 id="2_1">2. &nbsp; Анализ сложности<a class="headerlink" href="#2_1" title="Permanent link">&para;</a></h3>
<p><strong>Временная сложность</strong>: все вершины будут посещены по <span class="arithmatex">\(1\)</span> разу, что требует <span class="arithmatex">\(O(|V|)\)</span> времени; все ребра будут посещены по <span class="arithmatex">\(2\)</span> раза, что требует <span class="arithmatex">\(O(2|E|)\)</span> времени; суммарно получается <span class="arithmatex">\(O(|V| + |E|)\)</span> .</p>
<p><strong>Временная сложность</strong>: все вершины будут посещены по <span class="arithmatex">\(1\)</span> разу, что требует <span class="arithmatex">\(O(|V|)\)</span> времени. Все ребра будут посещены по <span class="arithmatex">\(2\)</span> раза, что требует <span class="arithmatex">\(O(2|E|)\)</span> времени. Суммарно получается <span class="arithmatex">\(O(|V| + |E|)\)</span> .</p>
<p><strong>Пространственная сложность</strong>: число вершин в списке <code>res</code> и хеш-множестве <code>visited</code> в худшем случае достигает <span class="arithmatex">\(|V|\)</span> , максимальная глубина рекурсии тоже равна <span class="arithmatex">\(|V|\)</span> , поэтому требуется <span class="arithmatex">\(O(|V|)\)</span> памяти.</p>
<!-- Source file information -->
+6 -6
View File
@@ -4363,22 +4363,22 @@
<li>По сравнению с линейными отношениями (связный список) и отношениями разделения (дерево), сетевые отношения (граф) обладают большей свободой и потому более сложны.</li>
<li>Ребра ориентированного графа имеют направление, в связном графе любые вершины достижимы, а во взвешенном графе каждое ребро содержит переменную веса.</li>
<li>Матрица смежности использует матрицу для представления графа: каждая строка и каждый столбец соответствуют вершине, а элементы матрицы показывают, есть между двумя вершинами ребро или нет. Матрица смежности эффективна в операциях добавления, удаления, поиска и изменения, но расходует больше памяти.</li>
<li>Список смежности использует несколько списков для представления графа; <span class="arithmatex">\(i\)</span>-й список соответствует вершине <span class="arithmatex">\(i\)</span> и хранит все ее смежные вершины. По сравнению с матрицей смежности список смежности экономит пространство, но для поиска ребра в нем приходится обходить список, поэтому по времени он уступает.</li>
<li>Список смежности использует несколько списков для представления графа. <span class="arithmatex">\(i\)</span>-й список соответствует вершине <span class="arithmatex">\(i\)</span> и хранит все ее смежные вершины. По сравнению с матрицей смежности список смежности экономит пространство, но для поиска ребра в нем приходится обходить список, поэтому по времени он уступает.</li>
<li>Когда списки в списке смежности становятся слишком длинными, их можно преобразовать в красно-черное дерево или хеш-таблицу, чтобы повысить эффективность поиска.</li>
<li>С точки зрения алгоритмической идеи матрица смежности отражает принцип "обмена пространства на время", а список смежности - принцип "обмена времени на пространство".</li>
<li>С точки зрения алгоритмической идеи матрица смежности отражает принцип «обмена пространства на время», а список смежности - принцип «обмена времени на пространство».</li>
<li>Графы можно использовать для моделирования различных реальных систем, таких как социальные сети, линии метро и так далее.</li>
<li>Дерево является частным случаем графа, а обход дерева - частным случаем обхода графа.</li>
<li>Обход графа в ширину представляет собой способ поиска, который расширяется от ближнего к дальнему и обычно реализуется с помощью очереди.</li>
<li>Обход графа в глубину представляет собой способ поиска, который сначала идет до самого конца, а затем возвращается назад, когда путь исчерпан; обычно он реализуется на основе рекурсии.</li>
<li>Обход графа в глубину представляет собой способ поиска, который сначала идет до самого конца, а затем возвращается назад, когда путь исчерпан. Обычно он реализуется на основе рекурсии.</li>
</ul>
<h3 id="2-q-a">2. &nbsp; Q &amp; A<a class="headerlink" href="#2-q-a" title="Permanent link">&para;</a></h3>
<p><strong>Q</strong>: Что считается путем: последовательность вершин или последовательность ребер?</p>
<p>Определение в разных языковых версиях Википедии различается: в английской версии путь определяется как "последовательность ребер", а в русской версии - как "последовательность вершин". В английской версии исходная формулировка выглядит так: In graph theory, a path in a graph is a finite or infinite sequence of edges which joins a sequence of vertices.</p>
<p>Определение в разных языковых версиях Википедии различается: в английской версии путь определяется как «последовательность ребер», а в русской версии - как «последовательность вершин». В английской версии исходная формулировка выглядит так: In graph theory, a path in a graph is a finite or infinite sequence of edges which joins a sequence of vertices.</p>
<p>В этой книге путь рассматривается как последовательность ребер, а не как последовательность вершин. Причина в том, что между двумя вершинами может существовать несколько ребер, и в таком случае каждому ребру соответствует свой путь.</p>
<p><strong>Q</strong>: Есть ли в несвязном графе вершины, до которых нельзя дойти?</p>
<p>В несвязном графе, начиная из некоторой вершины, по крайней мере одна вершина оказывается недостижимой. Чтобы обойти весь несвязный граф, нужно задать несколько стартовых точек и обойти все связные компоненты графа.</p>
<p><strong>Q</strong>: Есть ли требования к порядку вершин в списке "всех вершин, соединенных с данной вершиной" в списке смежности?</p>
<p>Порядок может быть произвольным. Но на практике может понадобиться сортировка по определенному правилу, например по порядку добавления вершин или по возрастанию значений вершин; это помогает быстро находить вершины с некоторым экстремальным свойством.</p>
<p><strong>Q</strong>: Есть ли требования к порядку вершин в списке «всех вершин, соединенных с данной вершиной» в списке смежности?</p>
<p>Порядок может быть произвольным. Но на практике может понадобиться сортировка по определенному правилу, например по порядку добавления вершин или по возрастанию значений вершин. Это помогает быстро находить вершины с некоторым экстремальным свойством.</p>
<!-- Source file information -->
@@ -4396,7 +4396,7 @@
<p align="center"> Рисунок 15-4 &nbsp; Ценность предмета на единицу веса </p>
<h3 id="1">1. &nbsp; Определение жадной стратегии<a class="headerlink" href="#1" title="Permanent link">&para;</a></h3>
<p>Максимизация общей ценности предметов в рюкзаке <strong>по сути равносильна максимизации ценности на единицу веса</strong>. Отсюда естественно выводится следующая жадная стратегия.</p>
<p>Максимизация общей ценности предметов в рюкзаке <strong>по сути равносильна максимизации ценности на единицу веса</strong>. Отсюда естественно выводится жадная стратегия, показанная на рисунке 15-5.</p>
<ol>
<li>Отсортировать предметы по убыванию удельной ценности.</li>
<li>Перебирать все предметы и <strong>на каждом шаге жадно выбирать предмет с наибольшей удельной ценностью</strong>.</li>
@@ -4708,7 +4708,7 @@
<p>У вас может невольно вырваться: «Эврика!» Жадный алгоритм решает задачу размена монет всего примерно десятью строками кода.</p>
<h2 id="1511">15.1.1 &nbsp; Преимущества и ограничения жадного алгоритма<a class="headerlink" href="#1511" title="Permanent link">&para;</a></h2>
<p><strong>Жадный алгоритм не только прост в реализации, но и обычно очень эффективен</strong>. В приведенном выше коде обозначим минимальный номинал монеты через <span class="arithmatex">\(\min(coins)\)</span>, тогда жадный выбор выполняется не более чем <span class="arithmatex">\(amt / \min(coins)\)</span> раз, а временная сложность равна <span class="arithmatex">\(O(amt / \min(coins))\)</span>. Это на порядок меньше, чем временная сложность решения через динамическое программирование <span class="arithmatex">\(O(n \times amt)\)</span>.</p>
<p>Однако <strong>для некоторых наборов номиналов монет жадный алгоритм не может найти оптимальный ответ</strong>. Ниже показаны два примера.</p>
<p>Однако <strong>для некоторых наборов номиналов монет жадный алгоритм не может найти оптимальный ответ</strong>. На рисунке 15-2 показаны два примера.</p>
<ul>
<li><strong>Положительный пример <span class="arithmatex">\(coins = [1, 5, 10, 20, 50, 100]\)</span></strong>: для такого набора монет при любом <span class="arithmatex">\(amt\)</span> жадный алгоритм находит оптимальное решение.</li>
<li><strong>Отрицательный пример <span class="arithmatex">\(coins = [1, 20, 50]\)</span></strong>: пусть <span class="arithmatex">\(amt = 60\)</span>. Жадный алгоритм найдет только комбинацию <span class="arithmatex">\(50 + 1 \times 10\)</span>, то есть всего <span class="arithmatex">\(11\)</span> монет, тогда как динамическое программирование находит оптимум <span class="arithmatex">\(20 + 20 + 20\)</span>, где требуется лишь <span class="arithmatex">\(3\)</span> монеты.</li>
@@ -4409,7 +4409,7 @@ cap[i, j] = \min(ht[i], ht[j]) \times (j - i)
<p align="center"> Рисунок 15-10 &nbsp; Состояние после перемещения короткой перегородки внутрь </p>
<p>Отсюда и выводится жадная стратегия для этой задачи: инициализировать два указателя по краям контейнера и на каждом шаге сдвигать внутрь указатель, соответствующий короткой перегородке, пока указатели не встретятся.</p>
<p>На рисунках ниже показан процесс выполнения этой жадной стратегии.</p>
<p>На рисунке 15-11 показан процесс выполнения этой жадной стратегии.</p>
<ol>
<li>В начальном состоянии указатели <span class="arithmatex">\(i\)</span> и <span class="arithmatex">\(j\)</span> стоят на двух концах массива.</li>
<li>Вычислить вместимость текущего состояния <span class="arithmatex">\(cap[i, j]\)</span> и обновить максимальную вместимость.</li>
+7 -7
View File
@@ -4402,7 +4402,7 @@
<!-- Page content -->
<h1 id="63">6.3 &nbsp; Алгоритмы хеширования<a class="headerlink" href="#63" title="Permanent link">&para;</a></h1>
<p>В двух предыдущих разделах мы рассмотрели принципы работы хеш-таблицы и способы обработки хеш-коллизий. Однако и открытая адресация, и метод цепочек <strong>лишь позволяют хеш-таблице корректно работать при возникновении коллизий, но не уменьшают вероятность появления самих коллизий</strong>.</p>
<p>Если хеш-коллизии происходят слишком часто, производительность хеш-таблицы резко деградирует. Как показано на рисунке 6-8, для хеш-таблицы с методом цепочек в идеальном случае пары ключ-значение равномерно распределены по всем бакетам, и это дает наилучшую эффективность поиска; в худшем же случае все пары ключ-значение оказываются в одном бакете, и временная сложность вырождается до <span class="arithmatex">\(O(n)\)</span> .</p>
<p>Если хеш-коллизии происходят слишком часто, производительность хеш-таблицы резко деградирует. Как показано на рисунке 6-8, для хеш-таблицы с методом цепочек в идеальном случае пары ключ-значение равномерно распределены по всем бакетам, и это дает наилучшую эффективность поиска. В худшем же случае все пары ключ-значение оказываются в одном бакете, и временная сложность вырождается до <span class="arithmatex">\(O(n)\)</span> .</p>
<p><img alt="Лучший и худший случаи хеш-коллизий" class="animation-figure" src="../hash_algorithm.assets/hash_collision_best_worst_condition.png" /></p>
<p align="center"> Рисунок 6-8 &nbsp; Лучший и худший случаи хеш-коллизий </p>
@@ -4421,7 +4421,7 @@
<p>На практике хеш-алгоритмы используются не только для реализации хеш-таблиц, но и во многих других областях.</p>
<ul>
<li><strong>Хранение паролей</strong>: чтобы защищать пароли пользователей, система обычно хранит не сами пароли в открытом виде, а их хеш-значения. Когда пользователь вводит пароль, система вычисляет хеш-значение введенного пароля и сравнивает его с сохраненным значением. Если они совпадают, пароль считается правильным.</li>
<li><strong>Проверка целостности данных</strong>: отправитель может вычислить хеш-значение данных и отправить его вместе с самими данными; получатель затем вычисляет хеш-значение повторно и сравнивает его с полученным. Если они совпадают, данные считаются целостными.</li>
<li><strong>Проверка целостности данных</strong>: отправитель может вычислить хеш-значение данных и отправить его вместе с самими данными. Получатель затем вычисляет хеш-значение повторно и сравнивает его с полученным. Если они совпадают, данные считаются целостными.</li>
</ul>
<p>Для приложений, связанных с криптографией, чтобы не допустить восстановления исходного пароля по хеш-значению и иных форм обратного анализа, хеш-алгоритм должен обладать более строгими свойствами безопасности.</p>
<ul>
@@ -4429,12 +4429,12 @@
<li><strong>Устойчивость к коллизиям</strong>: должно быть крайне трудно найти два разных входа, имеющих одинаковое хеш-значение.</li>
<li><strong>Эффект лавины</strong>: даже небольшое изменение во входных данных должно приводить к заметному и непредсказуемому изменению результата.</li>
</ul>
<p>Обрати внимание: <strong>"равномерное распределение" и "устойчивость к коллизиям" - это два независимых понятия</strong> , и выполнение первого не означает автоматического выполнения второго. Например, при случайном распределении входных <code>key</code> хеш-функция <code>key % 100</code> может выдавать достаточно равномерное распределение. Однако этот хеш-алгоритм слишком прост: все <code>key</code> с одинаковыми двумя последними цифрами будут иметь одинаковый результат, а значит, по хеш-значению можно легко подобрать подходящие <code>key</code> и, например, взломать пароль.</p>
<p>Обрати внимание: <strong>«равномерное распределение» и «устойчивость к коллизиям» - это два независимых понятия</strong> , и выполнение первого не означает автоматического выполнения второго. Например, при случайном распределении входных <code>key</code> хеш-функция <code>key % 100</code> может выдавать достаточно равномерное распределение. Однако этот хеш-алгоритм слишком прост: все <code>key</code> с одинаковыми двумя последними цифрами будут иметь одинаковый результат, а значит, по хеш-значению можно легко подобрать подходящие <code>key</code> и, например, взломать пароль.</p>
<h2 id="632-">6.3.2 &nbsp; Проектирование хеш-алгоритма<a class="headerlink" href="#632-" title="Permanent link">&para;</a></h2>
<p>Разработка хеш-алгоритма - это сложная задача, в которой нужно учитывать множество факторов. Однако для некоторых нетребовательных сценариев мы можем спроектировать и несколько простых хеш-алгоритмов.</p>
<ul>
<li><strong>Аддитивный хеш</strong>: складываем ASCII-коды всех символов входной строки и используем полученную сумму как хеш-значение.</li>
<li><strong>Мультипликативный хеш</strong>: используем "некоррелированность" умножения; на каждом шаге умножаем текущее значение на константу и добавляем ASCII-код очередного символа.</li>
<li><strong>Мультипликативный хеш</strong>: используем «некоррелированность» умножения. На каждом шаге умножаем текущее значение на константу и добавляем ASCII-код очередного символа.</li>
<li><strong>XOR-хеш</strong>: последовательно накапливаем элементы входных данных в одном хеш-значении через операцию XOR.</li>
<li><strong>Ротационный хеш</strong>: последовательно накапливаем ASCII-коды символов, причем перед каждым накоплением выполняем циклический сдвиг хеш-значения.</li>
</ul>
@@ -5021,7 +5021,7 @@
\text{hash} &amp; = \{ 0, 3, 6, 0, 3, 6, 0, 3, 6, 0, 3, 6,\dots \}
\end{aligned}
\]</div>
<p>Если входные <code>key</code> как раз удовлетворяют такому распределению в виде арифметической прогрессии, то хеш-значения начнут скучиваться, а это усугубит хеш-коллизии. Теперь предположим, что мы заменили <code>modulus</code> на простое число <span class="arithmatex">\(13\)</span> ; поскольку между <code>key</code> и <code>modulus</code> нет общих делителей, равномерность распределения хеш-значений заметно улучшится.</p>
<p>Если входные <code>key</code> как раз удовлетворяют такому распределению в виде арифметической прогрессии, то хеш-значения начнут скучиваться, а это усугубит хеш-коллизии. Теперь предположим, что мы заменили <code>modulus</code> на простое число <span class="arithmatex">\(13\)</span>. Поскольку между <code>key</code> и <code>modulus</code> нет общих делителей, равномерность распределения хеш-значений заметно улучшится.</p>
<div class="arithmatex">\[
\begin{aligned}
\text{modulus} &amp; = 13 \newline
@@ -5037,7 +5037,7 @@
<p>На протяжении почти ста лет хеш-алгоритмы непрерывно развивались и оптимизировались. Одни исследователи старались повысить их производительность, а другие исследователи и хакеры сосредоточивались на поиске уязвимостей в их безопасности. В таблице 6-2 приведены распространенные хеш-алгоритмы, которые часто встречаются в реальных приложениях.</p>
<ul>
<li>MD5 и SHA-1 уже многократно были успешно атакованы, поэтому они выведены из большинства сценариев, где требуется безопасность.</li>
<li>SHA-256 из семейства SHA-2 является одним из самых надежных хеш-алгоритмов; на сегодняшний день не известно успешных практических атак, поэтому он широко используется в самых разных протоколах и системах безопасности.</li>
<li>SHA-256 из семейства SHA-2 является одним из самых надежных хеш-алгоритмов. На сегодняшний день не известно успешных практических атак, поэтому он широко используется в самых разных протоколах и системах безопасности.</li>
<li>SHA-3 по сравнению с SHA-2 требует меньших затрат на реализацию и обеспечивает более высокую вычислительную эффективность, но на данный момент распространен слабее, чем семейство SHA-2.</li>
</ul>
<p align="center"> Таблица 6-2 &nbsp; Распространенные хеш-алгоритмы </p>
@@ -5096,7 +5096,7 @@
<p>Мы знаем, что <code>key</code> в хеш-таблице могут быть целыми числами, вещественными числами, строками и другими типами данных. Языки программирования обычно предоставляют встроенные хеш-алгоритмы для этих типов, чтобы вычислять индексы бакетов в хеш-таблице. Возьмем Python: в нем можно вызвать функцию <code>hash()</code> , чтобы вычислить хеш-значения для различных типов данных.</p>
<ul>
<li>Хеш-значение целого числа и булева значения совпадает с самим значением.</li>
<li>Вычисление хеш-значений для вещественных чисел и строк устроено сложнее; интересующиеся читатели могут изучить это самостоятельно.</li>
<li>Вычисление хеш-значений для вещественных чисел и строк устроено сложнее. Интересующиеся читатели могут изучить это самостоятельно.</li>
<li>Хеш-значение кортежа получается путем хеширования каждого элемента, а затем объединения этих хеш-значений в одно итоговое значение.</li>
<li>Хеш-значение объекта обычно строится на основе его адреса в памяти. Если переопределить метод хеширования объекта, можно реализовать вычисление хеша по содержимому.</li>
</ul>
+7 -7
View File
@@ -5950,13 +5950,13 @@
</details>
<p>Следует отметить, что когда связный список становится очень длинным, эффективность поиска <span class="arithmatex">\(O(n)\)</span> оказывается низкой. <strong>В этом случае список можно преобразовать в AVL-дерево или красно-черное дерево</strong> , чтобы оптимизировать временную сложность поиска до <span class="arithmatex">\(O(\log n)\)</span> .</p>
<h2 id="622">6.2.2 &nbsp; Открытая адресация<a class="headerlink" href="#622" title="Permanent link">&para;</a></h2>
<p><u>Открытая адресация (open addressing)</u> не вводит дополнительных структур данных, а обрабатывает хеш-коллизии с помощью многократного пробирования; основные варианты пробирования включают линейное пробирование, квадратичное пробирование и повторное хеширование.</p>
<p><u>Открытая адресация (open addressing)</u> не вводит дополнительных структур данных, а обрабатывает хеш-коллизии с помощью многократного пробирования. Основные варианты пробирования включают линейное пробирование, квадратичное пробирование и повторное хеширование.</p>
<p>Ниже на примере линейного пробирования рассмотрим механизм работы хеш-таблицы с открытой адресацией.</p>
<h3 id="1">1. &nbsp; Линейное пробирование<a class="headerlink" href="#1" title="Permanent link">&para;</a></h3>
<p>Линейное пробирование использует линейный поиск с фиксированным шагом. Его методы работы отличаются от обычной хеш-таблицы.</p>
<ul>
<li><strong>Вставка элемента</strong>: по хеш-функции вычисляется индекс бакета; если бакет уже занят, то от места конфликта выполняется линейный обход вперед (шаг обычно равен <span class="arithmatex">\(1\)</span> ), пока не будет найден пустой бакет, после чего элемент вставляется туда.</li>
<li><strong>Поиск элемента</strong>: если возник конфликт, то с тем же шагом продолжается линейный обход вперед, пока не будет найден целевой элемент и возвращено <code>value</code> ; если встречается пустой бакет, это означает, что искомого элемента в хеш-таблице нет, и возвращается <code>None</code> .</li>
<li><strong>Вставка элемента</strong>: по хеш-функции вычисляется индекс бакета. Если бакет уже занят, то от места конфликта выполняется линейный обход вперед (шаг обычно равен <span class="arithmatex">\(1\)</span> ), пока не будет найден пустой бакет, после чего элемент вставляется туда.</li>
<li><strong>Поиск элемента</strong>: если возник конфликт, то с тем же шагом продолжается линейный обход вперед, пока не будет найден целевой элемент и возвращено <code>value</code>. Если встречается пустой бакет, это означает, что искомого элемента в хеш-таблице нет, и возвращается <code>None</code> .</li>
</ul>
<p>На рисунке 6-6 показано распределение пар ключ-значение в хеш-таблице с открытой адресацией (линейное пробирование). Для этой хеш-функции все <code>key</code> с одинаковыми двумя последними цифрами отображаются в один и тот же бакет. Благодаря линейному пробированию они по очереди сохраняются в этом бакете и в следующих за ним бакетах.</p>
<p><img alt="Распределение пар ключ-значение в хеш-таблице с открытой адресацией (линейное пробирование)" class="animation-figure" src="../hash_collision.assets/hash_table_linear_probing.png" /></p>
@@ -5968,7 +5968,7 @@
<p align="center"> Рисунок 6-7 &nbsp; Проблема поиска после удаления элемента в открытой адресации </p>
<p>Чтобы решить эту проблему, можно использовать механизм <u>ленивого удаления (lazy deletion)</u>: он не удаляет элемент из хеш-таблицы напрямую, **а помечает этот бакет специальной константой <code>TOMBSTONE</code> **. В этом механизме и <code>None</code> , и <code>TOMBSTONE</code> означают пустой бакет, и оба могут быть использованы для размещения пары ключ-значение. Но есть важное различие: при линейном пробировании, встретив <code>TOMBSTONE</code> , нужно продолжать обход, потому что ниже него все еще могут существовать пары ключ-значение.</p>
<p>Однако <strong>ленивое удаление может ускорять деградацию производительности хеш-таблицы</strong>. Это связано с тем, что каждая операция удаления создает новую метку удаления; по мере роста числа <code>TOMBSTONE</code> время поиска тоже увеличивается, потому что линейное пробирование может быть вынуждено перескакивать через множество <code>TOMBSTONE</code> , прежде чем найдет целевой элемент.</p>
<p>Однако <strong>ленивое удаление может ускорять деградацию производительности хеш-таблицы</strong>. Это связано с тем, что каждая операция удаления создает новую метку удаления. По мере роста числа <code>TOMBSTONE</code> время поиска тоже увеличивается, потому что линейное пробирование может быть вынуждено перескакивать через множество <code>TOMBSTONE</code> , прежде чем найдет целевой элемент.</p>
<p>Поэтому имеет смысл при линейном пробировании запоминать индекс первого встреченного <code>TOMBSTONE</code> и затем менять найденный целевой элемент местами с этим <code>TOMBSTONE</code> . Преимущество такого подхода в том, что при каждом поиске или добавлении элемент будет перемещаться в бакет, расположенный ближе к его идеальной позиции (начальной точке пробирования), а значит, эффективность поиска улучшится.</p>
<p>Ниже приведена реализация хеш-таблицы с открытой адресацией, то есть с линейным пробированием, включающая ленивое удаление. Чтобы пространство хеш-таблицы использовалось более полно, мы рассматриваем ее как кольцевой массив: когда обход выходит за конец массива, он возвращается к началу и продолжается.</p>
<div class="tabbed-set tabbed-alternate" data-tabs="2:13"><input checked="checked" id="__tabbed_2_1" name="__tabbed_2" type="radio" /><input id="__tabbed_2_2" name="__tabbed_2" type="radio" /><input id="__tabbed_2_3" name="__tabbed_2" type="radio" /><input id="__tabbed_2_4" name="__tabbed_2" type="radio" /><input id="__tabbed_2_5" name="__tabbed_2" type="radio" /><input id="__tabbed_2_6" name="__tabbed_2" type="radio" /><input id="__tabbed_2_7" name="__tabbed_2" type="radio" /><input id="__tabbed_2_8" name="__tabbed_2" type="radio" /><input id="__tabbed_2_9" name="__tabbed_2" type="radio" /><input id="__tabbed_2_10" name="__tabbed_2" type="radio" /><input id="__tabbed_2_11" name="__tabbed_2" type="radio" /><input id="__tabbed_2_12" name="__tabbed_2" type="radio" /><input id="__tabbed_2_13" name="__tabbed_2" type="radio" /><div class="tabbed-labels"><label for="__tabbed_2_1">Python</label><label for="__tabbed_2_2">C++</label><label for="__tabbed_2_3">Java</label><label for="__tabbed_2_4">C#</label><label for="__tabbed_2_5">Go</label><label for="__tabbed_2_6">Swift</label><label for="__tabbed_2_7">JS</label><label for="__tabbed_2_8">TS</label><label for="__tabbed_2_9">Dart</label><label for="__tabbed_2_10">Rust</label><label for="__tabbed_2_11">C</label><label for="__tabbed_2_12">Kotlin</label><label for="__tabbed_2_13">Ruby</label></div>
@@ -7673,7 +7673,7 @@
</div>
</div>
<h3 id="2">2. &nbsp; Квадратичное пробирование<a class="headerlink" href="#2" title="Permanent link">&para;</a></h3>
<p>Квадратичное пробирование похоже на линейное пробирование и тоже является одной из распространенных стратегий открытой адресации. При возникновении конфликта оно не пропускает фиксированное число шагов, а переходит на расстояние, равное "квадрату числа попыток", то есть на <span class="arithmatex">\(1, 4, 9, \dots\)</span> шагов.</p>
<p>Квадратичное пробирование похоже на линейное пробирование и тоже является одной из распространенных стратегий открытой адресации. При возникновении конфликта оно не пропускает фиксированное число шагов, а переходит на расстояние, равное «квадрату числа попыток», то есть на <span class="arithmatex">\(1, 4, 9, \dots\)</span> шагов.</p>
<p>Квадратичное пробирование имеет следующие основные преимущества.</p>
<ul>
<li>Квадратичное пробирование пытается смягчить эффект кластеризации линейного пробирования, так как пропускает расстояния, равные квадрату номера попытки.</li>
@@ -7688,7 +7688,7 @@
<p>Как видно из названия, метод повторного хеширования использует для пробирования несколько хеш-функций <span class="arithmatex">\(f_1(x)\)</span>, <span class="arithmatex">\(f_2(x)\)</span>, <span class="arithmatex">\(f_3(x)\)</span>, <span class="arithmatex">\(\dots\)</span> .</p>
<ul>
<li><strong>Вставка элемента</strong>: если хеш-функция <span class="arithmatex">\(f_1(x)\)</span> вызывает конфликт, то пробуем <span class="arithmatex">\(f_2(x)\)</span> , и так далее, пока не будет найдено пустое место для вставки элемента.</li>
<li><strong>Поиск элемента</strong>: поиск идет в том же порядке хеш-функций, пока не будет найден целевой элемент; если встречается пустая позиция или уже были опробованы все хеш-функции, это означает, что элемента в хеш-таблице нет, и возвращается <code>None</code> .</li>
<li><strong>Поиск элемента</strong>: поиск идет в том же порядке хеш-функций, пока не будет найден целевой элемент. Если встречается пустая позиция или уже были опробованы все хеш-функции, это означает, что элемента в хеш-таблице нет, и возвращается <code>None</code> .</li>
</ul>
<p>По сравнению с линейным пробированием метод повторного хеширования меньше подвержен кластеризации, но несколько хеш-функций приносят дополнительные вычислительные затраты.</p>
<div class="admonition tip">
@@ -7700,7 +7700,7 @@
<ul>
<li>Python использует открытую адресацию. В словаре <code>dict</code> для пробирования применяются псевдослучайные числа.</li>
<li>Java использует метод цепочек. Начиная с JDK 1.8, когда длина массива внутри <code>HashMap</code> достигает 64, а длина списка достигает 8, этот список преобразуется в красно-черное дерево для повышения производительности поиска.</li>
<li>Go использует метод цепочек. В Go установлено, что каждый бакет может хранить не более 8 пар ключ-значение; при переполнении подключается overflow-бакет, а когда таких бакетов становится слишком много, выполняется специальное расширение того же масштаба, чтобы сохранить производительность.</li>
<li>Go использует метод цепочек. В Go установлено, что каждый бакет может хранить не более 8 пар ключ-значение. При переполнении подключается overflow-бакет, а когда таких бакетов становится слишком много, выполняется специальное расширение того же масштаба, чтобы сохранить производительность.</li>
</ul>
<!-- Source file information -->
+4 -4
View File
@@ -4380,7 +4380,7 @@
<!-- Page content -->
<h1 id="61-">6.1 &nbsp; Хеш-таблица<a class="headerlink" href="#61-" title="Permanent link">&para;</a></h1>
<p><u>Хеш-таблица (hash table)</u>, также называемая <u>таблицей рассеяния</u>, реализует эффективный поиск элементов за счет установления соответствия между ключом <code>key</code> и значением <code>value</code> . Иначе говоря, если передать в хеш-таблицу ключ <code>key</code> , то можно за <span class="arithmatex">\(O(1)\)</span> времени получить соответствующее значение <code>value</code> .</p>
<p>Как показано на рисунке 6-1, пусть есть <span class="arithmatex">\(n\)</span> студентов, и у каждого из них есть два поля данных: имя и номер студенческого билета. Если мы хотим реализовать запрос вида "ввести номер студенческого билета и вернуть соответствующее имя", то для этого можно использовать показанную ниже хеш-таблицу.</p>
<p>Как показано на рисунке 6-1, пусть есть <span class="arithmatex">\(n\)</span> студентов, и у каждого из них есть два поля данных: имя и номер студенческого билета. Если мы хотим реализовать запрос вида «ввести номер студенческого билета и вернуть соответствующее имя», то для этого можно воспользоваться хеш-таблицей, изображенной на рисунке 6-1.</p>
<p><img alt="Абстрактное представление хеш-таблицы" class="animation-figure" src="../hash_map.assets/hash_table_lookup.png" /></p>
<p align="center"> Рисунок 6-1 &nbsp; Абстрактное представление хеш-таблицы </p>
@@ -4905,7 +4905,7 @@
<div class="highlight"><pre><span></span><code><a id="__codelineno-26-1" name="__codelineno-26-1" href="#__codelineno-26-1"></a><span class="nv">index</span><span class="w"> </span><span class="o">=</span><span class="w"> </span>hash<span class="o">(</span>key<span class="o">)</span><span class="w"> </span>%<span class="w"> </span>capacity
</code></pre></div>
<p>После этого можно использовать <code>index</code> для доступа к соответствующему бакету в хеш-таблице и получения <code>value</code> .</p>
<p>Пусть длина массива <code>capacity = 100</code> , а хеш-алгоритм <code>hash(key) = key</code> . Тогда легко получить хеш-функцию <code>key % 100</code> . На рисунке 6-2 на примере <code>key</code> "номер студенческого билета" и <code>value</code> "имя" показан принцип работы хеш-функции.</p>
<p>Пусть длина массива <code>capacity = 100</code> , а хеш-алгоритм <code>hash(key) = key</code> . Тогда легко получить хеш-функцию <code>key % 100</code> . На рисунке 6-2 на примере <code>key</code> «номер студенческого билета» и <code>value</code> «имя» показан принцип работы хеш-функции.</p>
<p><img alt="Принцип работы хеш-функции" class="animation-figure" src="../hash_map.assets/hash_function.png" /></p>
<p align="center"> Рисунок 6-2 &nbsp; Принцип работы хеш-функции </p>
@@ -6072,7 +6072,7 @@
<div style="margin-top: 5px;"><a href="https://pythontutor.com/iframe-embed.html#code=class%20Pair%3A%0A%0A%20%20%20%20def%20__init__%28self%2C%20key%3A%20int%2C%20val%3A%20str%29%3A%0A%20%20%20%20%20%20%20%20self.key%20%3D%20key%0A%20%20%20%20%20%20%20%20self.val%20%3D%20val%0A%0Aclass%20ArrayHashMap%3A%0A%0A%20%20%20%20def%20__init__%28self%29%3A%0A%20%20%20%20%20%20%20%20self.buckets%3A%20list%5BPair%20%7C%20None%5D%20%3D%20%5BNone%5D%20%2A%2020%0A%0A%20%20%20%20def%20hash_func%28self%2C%20key%3A%20int%29%20-%3E%20int%3A%0A%20%20%20%20%20%20%20%20index%20%3D%20key%20%25%2020%0A%20%20%20%20%20%20%20%20return%20index%0A%0A%20%20%20%20def%20get%28self%2C%20key%3A%20int%29%20-%3E%20str%3A%0A%20%20%20%20%20%20%20%20index%3A%20int%20%3D%20self.hash_func%28key%29%0A%20%20%20%20%20%20%20%20pair%3A%20Pair%20%3D%20self.buckets%5Bindex%5D%0A%20%20%20%20%20%20%20%20if%20pair%20is%20None%3A%0A%20%20%20%20%20%20%20%20%20%20%20%20return%20None%0A%20%20%20%20%20%20%20%20return%20pair.val%0A%0A%20%20%20%20def%20put%28self%2C%20key%3A%20int%2C%20val%3A%20str%29%3A%0A%20%20%20%20%20%20%20%20pair%20%3D%20Pair%28key%2C%20val%29%0A%20%20%20%20%20%20%20%20index%3A%20int%20%3D%20self.hash_func%28key%29%0A%20%20%20%20%20%20%20%20self.buckets%5Bindex%5D%20%3D%20pair%0A%0A%20%20%20%20def%20remove%28self%2C%20key%3A%20int%29%3A%0A%20%20%20%20%20%20%20%20index%3A%20int%20%3D%20self.hash_func%28key%29%0A%20%20%20%20%20%20%20%20self.buckets%5Bindex%5D%20%3D%20None%0A%0A%20%20%20%20def%20entry_set%28self%29%20-%3E%20list%5BPair%5D%3A%0A%20%20%20%20%20%20%20%20result%3A%20list%5BPair%5D%20%3D%20%5B%5D%0A%20%20%20%20%20%20%20%20for%20pair%20in%20self.buckets%3A%0A%20%20%20%20%20%20%20%20%20%20%20%20if%20pair%20is%20not%20None%3A%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20result.append%28pair%29%0A%20%20%20%20%20%20%20%20return%20result%0A%0A%20%20%20%20def%20key_set%28self%29%20-%3E%20list%5Bint%5D%3A%0A%20%20%20%20%20%20%20%20result%20%3D%20%5B%5D%0A%20%20%20%20%20%20%20%20for%20pair%20in%20self.buckets%3A%0A%20%20%20%20%20%20%20%20%20%20%20%20if%20pair%20is%20not%20None%3A%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20result.append%28pair.key%29%0A%20%20%20%20%20%20%20%20return%20result%0A%0A%20%20%20%20def%20value_set%28self%29%20-%3E%20list%5Bstr%5D%3A%0A%20%20%20%20%20%20%20%20result%20%3D%20%5B%5D%0A%20%20%20%20%20%20%20%20for%20pair%20in%20self.buckets%3A%0A%20%20%20%20%20%20%20%20%20%20%20%20if%20pair%20is%20not%20None%3A%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20result.append%28pair.val%29%0A%20%20%20%20%20%20%20%20return%20result%0A%0A%20%20%20%20def%20print%28self%29%3A%0A%20%20%20%20%20%20%20%20for%20pair%20in%20self.buckets%3A%0A%20%20%20%20%20%20%20%20%20%20%20%20if%20pair%20is%20not%20None%3A%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20print%28pair.key%2C%20%27-%3E%27%2C%20pair.val%29%0A%27Driver%20Code%27%0Aif%20__name__%20%3D%3D%20%27__main__%27%3A%0A%20%20%20%20hmap%20%3D%20ArrayHashMap%28%29%0A%20%20%20%20hmap.put%2812836%2C%20%27%D0%A1%D1%8F%D0%BE%20%D0%A5%D0%B0%27%29%0A%20%20%20%20hmap.put%2815937%2C%20%27%D0%A1%D1%8F%D0%BE%20%D0%9B%D0%BE%27%29%0A%20%20%20%20hmap.put%2816750%2C%20%27%D0%A1%D1%8F%D0%BE%20%D0%A1%D1%83%D0%B0%D0%BD%D1%8C%27%29%0A%20%20%20%20hmap.put%2813276%2C%20%27%D0%A1%D1%8F%D0%BE%20%D0%A4%D0%B0%27%29%0A%20%20%20%20hmap.put%2810583%2C%20%27%D0%A1%D1%8F%D0%BE%20%D0%AF%27%29%0A%20%20%20%20name%20%3D%20hmap.get%2815937%29%0A%20%20%20%20hmap.remove%2810583%29%0A%20%20%20%20print%28%27%5Cn%D0%9E%D1%82%D0%B4%D0%B5%D0%BB%D1%8C%D0%BD%D1%8B%D0%B9%20%D0%BE%D0%B1%D1%85%D0%BE%D0%B4%20%D0%BF%D0%B0%D1%80%20%D0%BA%D0%BB%D1%8E%D1%87-%D0%B7%D0%BD%D0%B0%D1%87%D0%B5%D0%BD%D0%B8%D0%B5%27%29%0A%20%20%20%20for%20pair%20in%20hmap.entry_set%28%29%3A%0A%20%20%20%20%20%20%20%20print%28pair.key%2C%20%27-%3E%27%2C%20pair.val%29&codeDivHeight=800&codeDivWidth=600&cumulative=false&curInstr=4&heapPrimitives=nevernest&origin=opt-frontend.js&py=311&rawInputLstJSON=%5B%5D&textReferences=false" target="_blank" rel="noopener noreferrer">Во весь экран &gt;</a></div></p>
</details>
<h2 id="613-">6.1.3 &nbsp; Хеш-коллизии и расширение<a class="headerlink" href="#613-" title="Permanent link">&para;</a></h2>
<p>По сути, хеш-функция отображает входное пространство, состоящее из всех <code>key</code> , в выходное пространство, состоящее из всех индексов массива, а входное пространство обычно значительно больше выходного. Поэтому <strong>теоретически неизбежно существование ситуации "несколько входов соответствуют одному выходу"</strong>.</p>
<p>По сути, хеш-функция отображает входное пространство, состоящее из всех <code>key</code> , в выходное пространство, состоящее из всех индексов массива, а входное пространство обычно значительно больше выходного. Поэтому <strong>теоретически неизбежно существование ситуации «несколько входов соответствуют одному выходу»</strong>.</p>
<p>Для хеш-функции из приведенного выше примера, если последние две цифры <code>key</code> совпадают, то совпадает и результат хеш-функции. Например, если искать студентов с номерами 12836 и 20336, то получим:</p>
<div class="highlight"><pre><span></span><code><a id="__codelineno-40-1" name="__codelineno-40-1" href="#__codelineno-40-1"></a><span class="m">12836</span><span class="w"> </span>%<span class="w"> </span><span class="nv">100</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="m">36</span>
<a id="__codelineno-40-2" name="__codelineno-40-2" href="#__codelineno-40-2"></a><span class="m">20336</span><span class="w"> </span>%<span class="w"> </span><span class="nv">100</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="m">36</span>
@@ -6086,7 +6086,7 @@
<p><img alt="Расширение хеш-таблицы" class="animation-figure" src="../hash_map.assets/hash_table_reshash.png" /></p>
<p align="center"> Рисунок 6-4 &nbsp; Расширение хеш-таблицы </p>
<p>Подобно расширению массива, расширение хеш-таблицы требует перенести все пары ключ-значение из старой таблицы в новую, а это очень затратно по времени; кроме того, поскольку емкость хеш-таблицы <code>capacity</code> изменилась, нам приходится с помощью хеш-функции заново вычислять позиции хранения всех пар ключ-значение, что дополнительно увеличивает вычислительные расходы процесса расширения. Поэтому языки программирования обычно заранее резервируют достаточно большую емкость хеш-таблицы, чтобы избежать частых расширений.</p>
<p>Подобно расширению массива, расширение хеш-таблицы требует перенести все пары ключ-значение из старой таблицы в новую, а это очень затратно по времени. Кроме того, поскольку емкость хеш-таблицы <code>capacity</code> изменилась, нам приходится с помощью хеш-функции заново вычислять позиции хранения всех пар ключ-значение, что дополнительно увеличивает вычислительные расходы процесса расширения. Поэтому языки программирования обычно заранее резервируют достаточно большую емкость хеш-таблицы, чтобы избежать частых расширений.</p>
<p><u>Коэффициент загрузки (load factor)</u> - важное понятие хеш-таблицы. Он определяется как отношение числа элементов в хеш-таблице к числу бакетов и используется для оценки степени серьезности хеш-коллизий, <strong>а также часто служит условием срабатывания расширения хеш-таблицы</strong>. Например, в Java, когда коэффициент загрузки превышает <span class="arithmatex">\(0.75\)</span> , система расширяет хеш-таблицу до <span class="arithmatex">\(2\)</span> раз от исходной емкости.</p>
<!-- Source file information -->
+3 -3
View File
@@ -4362,7 +4362,7 @@
<li>Передав <code>key</code> , мы можем получить <code>value</code> из хеш-таблицы за <span class="arithmatex">\(O(1)\)</span> времени, поэтому она очень эффективна.</li>
<li>К типичным операциям хеш-таблицы относятся поиск, добавление пары ключ-значение, удаление пары ключ-значение и обход хеш-таблицы.</li>
<li>Хеш-функция отображает <code>key</code> в индекс массива, после чего можно обратиться к соответствующему бакету и получить <code>value</code> .</li>
<li>Два разных <code>key</code> после хеш-функции могут дать один и тот же индекс массива, что приводит к ошибочному результату поиска; это явление называется хеш-коллизией.</li>
<li>Два разных <code>key</code> после хеш-функции могут дать один и тот же индекс массива, что приводит к ошибочному результату поиска. Это явление называется хеш-коллизией.</li>
<li>Чем больше емкость хеш-таблицы, тем ниже вероятность хеш-коллизий. Поэтому хеш-коллизии можно смягчать путем расширения хеш-таблицы. Как и у массива, операция расширения у хеш-таблицы очень затратна.</li>
<li>Коэффициент загрузки определяется как отношение числа элементов в хеш-таблице к числу бакетов, отражает степень серьезности хеш-коллизий и часто используется как условие запуска расширения хеш-таблицы.</li>
<li>Метод цепочек превращает одиночный элемент в связный список и хранит все конфликтующие элементы в одном списке. Однако слишком длинный список снижает эффективность поиска, поэтому его можно дополнительно преобразовать в красно-черное дерево.</li>
@@ -4382,12 +4382,12 @@
<p>Во-первых, у хеш-таблицы повышается временная эффективность, но снижается пространственная эффективность. Значительная часть ее памяти остается неиспользованной.</p>
<p>Во-вторых, она быстрее только в определенных сценариях. Если одну и ту же задачу можно реализовать на массиве или связном списке с той же асимптотикой, то часто такая реализация окажется быстрее, чем хеш-таблица. Причина в том, что вычисление хеш-функции само по себе стоит времени, то есть константа в сложности получается выше.</p>
<p>Наконец, временная сложность хеш-таблицы тоже может деградировать. Например, при методе цепочек мы все равно выполняем поиск в связном списке или красно-черном дереве, поэтому риск деградации до <span class="arithmatex">\(O(n)\)</span> сохраняется.</p>
<p><strong>Q</strong>: Есть ли у повторного хеширования недостаток "нельзя напрямую удалять элементы"? Можно ли повторно использовать место, помеченное как удаленное?</p>
<p><strong>Q</strong>: Есть ли у повторного хеширования недостаток «нельзя напрямую удалять элементы»? Можно ли повторно использовать место, помеченное как удаленное?</p>
<p>Повторное хеширование - это разновидность открытой адресации, а у всех методов открытой адресации есть недостаток: элементы нельзя удалять напрямую, поэтому приходится использовать метку удаления. Пространство, помеченное как удаленное, можно использовать повторно. Когда новый элемент вставляется в хеш-таблицу и в процессе пробирования попадает на такую отмеченную позицию, эта позиция может быть занята новым элементом. Такой подход сохраняет последовательность пробирования и одновременно поддерживает приемлемую эффективность использования памяти.</p>
<p><strong>Q</strong>: Почему при линейном пробировании во время поиска элемента вообще возникает хеш-коллизия?</p>
<p>Во время поиска мы через хеш-функцию находим соответствующий бакет и соответствующую пару ключ-значение, но видим, что <code>key</code> не совпадает, а это и означает наличие хеш-коллизии. Поэтому метод линейного пробирования в соответствии с заранее заданным шагом последовательно движется дальше, пока не найдет правильную пару ключ-значение или не убедится, что поиск завершился неудачей.</p>
<p><strong>Q</strong>: Почему расширение хеш-таблицы помогает смягчать хеш-коллизии?</p>
<p>Последний шаг хеш-функции обычно состоит во взятии по модулю длины массива <span class="arithmatex">\(n\)</span> , чтобы результат попадал в диапазон индексов массива; после расширения длина массива <span class="arithmatex">\(n\)</span> меняется, а значит, может измениться и индекс, соответствующий данному <code>key</code> . Несколько <code>key</code> , которые раньше попадали в один бакет, после расширения могут распределиться по нескольким бакетам, и тем самым хеш-коллизии будут ослаблены.</p>
<p>Последний шаг хеш-функции обычно состоит во взятии по модулю длины массива <span class="arithmatex">\(n\)</span> , чтобы результат попадал в диапазон индексов массива. После расширения длина массива <span class="arithmatex">\(n\)</span> меняется, а значит, может измениться и индекс, соответствующий данному <code>key</code> . Несколько <code>key</code> , которые раньше попадали в один бакет, после расширения могут распределиться по нескольким бакетам, и тем самым хеш-коллизии будут ослаблены.</p>
<p><strong>Q</strong>: Если нам нужен быстрый доступ, почему бы просто не использовать массив?</p>
<p>Когда <code>key</code> данных - это непрерывные целые числа из маленького диапазона, действительно можно напрямую использовать массив: это просто и эффективно. Но если <code>key</code> имеют другой тип данных (например, строки), тогда нужен хеш-алгоритм, который отобразит <code>key</code> в индекс массива, а хранение элементов будет выполняться через массив бакетов. Такая структура и называется хеш-таблицей.</p>
+4 -4
View File
@@ -4383,11 +4383,11 @@
<h2 id="821">8.2.1 &nbsp; Реализация через операцию добавления в кучу<a class="headerlink" href="#821" title="Permanent link">&para;</a></h2>
<p>Сначала мы создаем пустую кучу, затем обходим список и для каждого элемента по очереди выполняем операцию добавления в кучу: сначала помещаем элемент в хвост кучи, а затем выполняем для него упорядочивание снизу вверх.</p>
<p>Каждый раз, когда элемент добавляется в кучу, ее длина увеличивается на единицу. Поскольку узлы последовательно добавляются в двоичное дерево сверху вниз, куча строится сверху вниз.</p>
<p>Пусть число элементов равно <span class="arithmatex">\(n\)</span> ; так как каждая операция добавления требует <span class="arithmatex">\(O(\log{n})\)</span> времени, временная сложность такого построения кучи составляет <span class="arithmatex">\(O(n \log n)\)</span> .</p>
<p>Пусть число элементов равно <span class="arithmatex">\(n\)</span>. Так как каждая операция добавления требует <span class="arithmatex">\(O(\log{n})\)</span> времени, временная сложность такого построения кучи составляет <span class="arithmatex">\(O(n \log n)\)</span> .</p>
<h2 id="822">8.2.2 &nbsp; Реализация через обход и упорядочивание<a class="headerlink" href="#822" title="Permanent link">&para;</a></h2>
<p>На самом деле можно реализовать и более эффективный способ построения кучи, который состоит из двух шагов.</p>
<ol>
<li>Без изменений добавить все элементы списка в кучу; в этот момент свойства кучи еще не выполняются.</li>
<li>Без изменений добавить все элементы списка в кучу. В этот момент свойства кучи еще не выполняются.</li>
<li>Обойти кучу в обратном порядке, то есть в порядке, обратном обходу по уровням, и по очереди выполнить упорядочивание сверху вниз для каждого нелистового узла.</li>
</ol>
<p><strong>После того как некоторый узел был упорядочен, поддерево с этим узлом в качестве корня становится корректной подкучей</strong>. А поскольку обход выполняется в обратном порядке, куча строится снизу вверх.</p>
@@ -4678,11 +4678,11 @@
<li>В процессе упорядочивания сверху вниз каждый узел в худшем случае может просеяться до листа, поэтому максимальное число итераций равно высоте двоичного дерева <span class="arithmatex">\(\log n\)</span> .</li>
</ul>
<p>Перемножив эти два значения, можно получить временную сложность построения кучи <span class="arithmatex">\(O(n \log n)\)</span> . <strong>Но эта оценка неточна, потому что мы не учли свойство двоичного дерева: на нижних уровнях узлов гораздо больше, чем на верхних</strong>.</p>
<p>Далее выполним более точный расчет. Чтобы упростить вычисления, предположим, что дано "идеальное двоичное дерево" высоты <span class="arithmatex">\(h\)</span> с числом узлов <span class="arithmatex">\(n\)</span> ; это предположение не повлияет на корректность результата.</p>
<p>Далее выполним более точный расчет. Чтобы упростить вычисления, предположим, что дано «идеальное двоичное дерево» высоты <span class="arithmatex">\(h\)</span> с числом узлов <span class="arithmatex">\(n\)</span>. Это предположение не повлияет на корректность результата.</p>
<p><img alt="Число узлов на каждом уровне идеального двоичного дерева" class="animation-figure" src="../build_heap.assets/heapify_operations_count.png" /></p>
<p align="center"> Рисунок 8-5 &nbsp; Число узлов на каждом уровне идеального двоичного дерева </p>
<p>Как показано на рисунке 8-5, максимальное число итераций упорядочивания сверху вниз для некоторого узла равно расстоянию от этого узла до листового узла, а это расстояние как раз и есть высота узла. Поэтому мы можем просуммировать для каждого уровня выражение "число узлов <span class="arithmatex">\(\times\)</span> высота узла" и <strong>получить суммарное число итераций упорядочивания для всех узлов</strong>.</p>
<p>Как показано на рисунке 8-5, максимальное число итераций упорядочивания сверху вниз для некоторого узла равно расстоянию от этого узла до листового узла, а это расстояние как раз и есть высота узла. Поэтому мы можем просуммировать для каждого уровня выражение «число узлов <span class="arithmatex">\(\times\)</span> высота узла» и <strong>получить суммарное число итераций упорядочивания для всех узлов</strong>.</p>
<div class="arithmatex">\[
T(h) = 2^0h + 2^1(h-1) + 2^2(h-2) + \dots + 2^{(h-1)}\times1
\]</div>
+7 -7
View File
@@ -4538,7 +4538,7 @@
</table>
</div>
<p>В реальных приложениях мы можем напрямую использовать классы кучи, предоставляемые языком программирования, или классы очереди с приоритетом.</p>
<p>Подобно сортировкам "по возрастанию" и "по убыванию", мы можем переключаться между "минимальной кучей" и "максимальной кучей", изменяя <code>flag</code> или модифицируя <code>Comparator</code> . Код приведен ниже:</p>
<p>Подобно сортировкам «по возрастанию» и «по убыванию», мы можем переключаться между «минимальной кучей» и «максимальной кучей», изменяя <code>flag</code> или модифицируя <code>Comparator</code> . Код приведен ниже:</p>
<div class="tabbed-set tabbed-alternate" data-tabs="1:13"><input checked="checked" id="__tabbed_1_1" name="__tabbed_1" type="radio" /><input id="__tabbed_1_2" name="__tabbed_1" type="radio" /><input id="__tabbed_1_3" name="__tabbed_1" type="radio" /><input id="__tabbed_1_4" name="__tabbed_1" type="radio" /><input id="__tabbed_1_5" name="__tabbed_1" type="radio" /><input id="__tabbed_1_6" name="__tabbed_1" type="radio" /><input id="__tabbed_1_7" name="__tabbed_1" type="radio" /><input id="__tabbed_1_8" name="__tabbed_1" type="radio" /><input id="__tabbed_1_9" name="__tabbed_1" type="radio" /><input id="__tabbed_1_10" name="__tabbed_1" type="radio" /><input id="__tabbed_1_11" name="__tabbed_1" type="radio" /><input id="__tabbed_1_12" name="__tabbed_1" type="radio" /><input id="__tabbed_1_13" name="__tabbed_1" type="radio" /><div class="tabbed-labels"><label for="__tabbed_1_1">Python</label><label for="__tabbed_1_2">C++</label><label for="__tabbed_1_3">Java</label><label for="__tabbed_1_4">C#</label><label for="__tabbed_1_5">Go</label><label for="__tabbed_1_6">Swift</label><label for="__tabbed_1_7">JS</label><label for="__tabbed_1_8">TS</label><label for="__tabbed_1_9">Dart</label><label for="__tabbed_1_10">Rust</label><label for="__tabbed_1_11">C</label><label for="__tabbed_1_12">Kotlin</label><label for="__tabbed_1_13">Ruby</label></div>
<div class="tabbed-content">
<div class="tabbed-block">
@@ -4897,7 +4897,7 @@
<h2 id="812">8.1.2 &nbsp; Реализация кучи<a class="headerlink" href="#812" title="Permanent link">&para;</a></h2>
<p>Ниже реализуется максимальная куча. Чтобы преобразовать ее в минимальную кучу, достаточно инвертировать всю логику сравнений по величине, например заменить <span class="arithmatex">\(\geq\)</span> на <span class="arithmatex">\(\leq\)</span> . Заинтересованные читатели могут попробовать реализовать это самостоятельно.</p>
<h3 id="1">1. &nbsp; Хранение и представление кучи<a class="headerlink" href="#1" title="Permanent link">&para;</a></h3>
<p>В разделе "Двоичные деревья" мы уже говорили, что полное двоичное дерево очень удобно представлять массивом. Поскольку куча как раз и является полным двоичным деревом, <strong>для хранения кучи мы также будем использовать массив</strong>.</p>
<p>В разделе «Двоичные деревья» мы уже говорили, что полное двоичное дерево очень удобно представлять массивом. Поскольку куча как раз и является полным двоичным деревом, <strong>для хранения кучи мы также будем использовать массив</strong>.</p>
<p>Когда двоичное дерево представляется массивом, элементы массива соответствуют значениям узлов, а индексы - положениям этих узлов в двоичном дереве. <strong>Указатели на узлы реализуются через формулы отображения индексов</strong>.</p>
<p>Как показано на рисунке 8-2, для заданного индекса <span class="arithmatex">\(i\)</span> индекс левого дочернего узла равен <span class="arithmatex">\(2i + 1\)</span> , правого дочернего узла - <span class="arithmatex">\(2i + 2\)</span> , а родительского узла - <span class="arithmatex">\((i - 1) / 2\)</span> с округлением вниз. Если индекс выходит за допустимые границы, это означает пустой узел или отсутствие узла.</p>
<p><img alt="Представление и хранение кучи" class="animation-figure" src="../heap.assets/representation_of_heap.png" /></p>
@@ -5229,8 +5229,8 @@
<div style="margin-top: 5px;"><a href="https://pythontutor.com/iframe-embed.html#code=class%20MaxHeap%3A%0A%20%20%20%20%22%22%22%D0%9C%D0%B0%D0%BA%D1%81%D0%B8%D0%BC%D0%B0%D0%BB%D1%8C%D0%BD%D0%B0%D1%8F%20%D0%BA%D1%83%D1%87%D0%B0%22%22%22%0A%0A%20%20%20%20def%20__init__%28self%2C%20nums%3A%20list%5Bint%5D%29%3A%0A%20%20%20%20%20%20%20%20%22%22%22%D0%9A%D0%BE%D0%BD%D1%81%D1%82%D1%80%D1%83%D0%BA%D1%82%D0%BE%D1%80%22%22%22%0A%20%20%20%20%20%20%20%20%23%20%D0%94%D0%BE%D0%B1%D0%B0%D0%B2%D0%B8%D1%82%D1%8C%20%D1%8D%D0%BB%D0%B5%D0%BC%D0%B5%D0%BD%D1%82%D1%8B%20%D1%81%D0%BF%D0%B8%D1%81%D0%BA%D0%B0%20%D0%B2%20%D0%BA%D1%83%D1%87%D1%83%20%D0%B1%D0%B5%D0%B7%20%D0%B8%D0%B7%D0%BC%D0%B5%D0%BD%D0%B5%D0%BD%D0%B8%D0%B9%0A%20%20%20%20%20%20%20%20self.max_heap%20%3D%20nums%0A%0A%20%20%20%20def%20left%28self%2C%20i%3A%20int%29%20-%3E%20int%3A%0A%20%20%20%20%20%20%20%20%22%22%22%D0%9F%D0%BE%D0%BB%D1%83%D1%87%D0%B8%D1%82%D1%8C%20%D0%B8%D0%BD%D0%B4%D0%B5%D0%BA%D1%81%20%D0%BB%D0%B5%D0%B2%D0%BE%D0%B3%D0%BE%20%D0%B4%D0%BE%D1%87%D0%B5%D1%80%D0%BD%D0%B5%D0%B3%D0%BE%20%D1%83%D0%B7%D0%BB%D0%B0%22%22%22%0A%20%20%20%20%20%20%20%20return%202%20%2A%20i%20%2B%201%0A%0A%20%20%20%20def%20right%28self%2C%20i%3A%20int%29%20-%3E%20int%3A%0A%20%20%20%20%20%20%20%20%22%22%22%D0%9F%D0%BE%D0%BB%D1%83%D1%87%D0%B8%D1%82%D1%8C%20%D0%B8%D0%BD%D0%B4%D0%B5%D0%BA%D1%81%20%D0%BF%D1%80%D0%B0%D0%B2%D0%BE%D0%B3%D0%BE%20%D0%B4%D0%BE%D1%87%D0%B5%D1%80%D0%BD%D0%B5%D0%B3%D0%BE%20%D1%83%D0%B7%D0%BB%D0%B0%22%22%22%0A%20%20%20%20%20%20%20%20return%202%20%2A%20i%20%2B%202%0A%0A%20%20%20%20def%20parent%28self%2C%20i%3A%20int%29%20-%3E%20int%3A%0A%20%20%20%20%20%20%20%20%22%22%22%D0%9F%D0%BE%D0%BB%D1%83%D1%87%D0%B8%D1%82%D1%8C%20%D0%B8%D0%BD%D0%B4%D0%B5%D0%BA%D1%81%20%D1%80%D0%BE%D0%B4%D0%B8%D1%82%D0%B5%D0%BB%D1%8C%D1%81%D0%BA%D0%BE%D0%B3%D0%BE%20%D1%83%D0%B7%D0%BB%D0%B0%22%22%22%0A%20%20%20%20%20%20%20%20return%20%28i%20-%201%29%20%2F%2F%202%20%20%23%20%D0%9E%D0%BA%D1%80%D1%83%D0%B3%D0%BB%D0%B5%D0%BD%D0%B8%D0%B5%20%D0%B2%D0%BD%D0%B8%D0%B7%20%D0%BF%D1%80%D0%B8%20%D0%B4%D0%B5%D0%BB%D0%B5%D0%BD%D0%B8%D0%B8%0A%0A%20%20%20%20def%20size%28self%29%20-%3E%20int%3A%0A%20%20%20%20%20%20%20%20%22%22%22%D0%9F%D0%BE%D0%BB%D1%83%D1%87%D0%B5%D0%BD%D0%B8%D0%B5%20%D1%80%D0%B0%D0%B7%D0%BC%D0%B5%D1%80%D0%B0%20%D0%BA%D1%83%D1%87%D0%B8%22%22%22%0A%20%20%20%20%20%20%20%20return%20len%28self.max_heap%29%0A%0A%20%20%20%20def%20is_empty%28self%29%20-%3E%20bool%3A%0A%20%20%20%20%20%20%20%20%22%22%22%D0%9F%D1%80%D0%BE%D0%B2%D0%B5%D1%80%D0%BA%D0%B0%2C%20%D0%BF%D1%83%D1%81%D1%82%D0%B0%20%D0%BB%D0%B8%20%D0%BA%D1%83%D1%87%D0%B0%22%22%22%0A%20%20%20%20%20%20%20%20return%20self.size%28%29%20%3D%3D%200%0A%0A%20%20%20%20def%20peek%28self%29%20-%3E%20int%3A%0A%20%20%20%20%20%20%20%20%22%22%22%D0%94%D0%BE%D1%81%D1%82%D1%83%D0%BF%20%D0%BA%20%D1%8D%D0%BB%D0%B5%D0%BC%D0%B5%D0%BD%D1%82%D1%83%20%D0%BD%D0%B0%20%D0%B2%D0%B5%D1%80%D1%88%D0%B8%D0%BD%D0%B5%20%D0%BA%D1%83%D1%87%D0%B8%22%22%22%0A%20%20%20%20%20%20%20%20return%20self.max_heap%5B0%5D%0A%0A%0A%22%22%22Driver%20Code%22%22%22%0Aif%20__name__%20%3D%3D%20%22__main__%22%3A%0A%20%20%20%20%23%20%D0%98%D0%BD%D0%B8%D1%86%D0%B8%D0%B0%D0%BB%D0%B8%D0%B7%D0%B0%D1%86%D0%B8%D1%8F%20%D0%BC%D0%B0%D0%BA%D1%81%D0%B8%D0%BC%D0%B0%D0%BB%D1%8C%D0%BD%D0%BE%D0%B9%20%D0%BA%D1%83%D1%87%D0%B8%0A%20%20%20%20%23%20%D0%9E%D0%B1%D1%80%D0%B0%D1%82%D0%B8%D1%82%D0%B5%20%D0%B2%D0%BD%D0%B8%D0%BC%D0%B0%D0%BD%D0%B8%D0%B5%3A%20%D0%B2%D1%85%D0%BE%D0%B4%D0%BD%D0%BE%D0%B9%20%D0%BC%D0%B0%D1%81%D1%81%D0%B8%D0%B2%20%D1%83%D0%B6%D0%B5%20%D1%8F%D0%B2%D0%BB%D1%8F%D0%B5%D1%82%D1%81%D1%8F%20%D0%BA%D0%BE%D1%80%D1%80%D0%B5%D0%BA%D1%82%D0%BD%D0%BE%D0%B9%20%D0%BA%D1%83%D1%87%D0%B5%D0%B9%0A%20%20%20%20max_heap%20%3D%20MaxHeap%28%5B9%2C%208%2C%206%2C%206%2C%207%2C%205%2C%202%2C%201%2C%204%2C%203%2C%206%2C%202%5D%29%0A%0A%20%20%20%20%23%20%D0%9F%D0%BE%D0%BB%D1%83%D1%87%D0%B5%D0%BD%D0%B8%D0%B5%20%D1%8D%D0%BB%D0%B5%D0%BC%D0%B5%D0%BD%D1%82%D0%B0%20%D1%81%20%D0%B2%D0%B5%D1%80%D1%88%D0%B8%D0%BD%D1%8B%20%D0%BA%D1%83%D1%87%D0%B8%0A%20%20%20%20peek%20%3D%20max_heap.peek%28%29%0A%20%20%20%20print%28f%22%5Cn%D0%AD%D0%BB%D0%B5%D0%BC%D0%B5%D0%BD%D1%82%20%D0%BD%D0%B0%20%D0%B2%D0%B5%D1%80%D1%88%D0%B8%D0%BD%D0%B5%20%D0%BA%D1%83%D1%87%D0%B8%20%3D%20%7Bpeek%7D%22%29&codeDivHeight=800&codeDivWidth=600&cumulative=false&curInstr=7&heapPrimitives=nevernest&origin=opt-frontend.js&py=311&rawInputLstJSON=%5B%5D&textReferences=false" target="_blank" rel="noopener noreferrer">Во весь экран &gt;</a></div></p>
</details>
<h3 id="3">3. &nbsp; Добавление элемента в кучу<a class="headerlink" href="#3" title="Permanent link">&para;</a></h3>
<p>Пусть дан элемент <code>val</code> . Сначала мы помещаем его в основание кучи. После добавления свойства кучи могут нарушиться, потому что <code>val</code> может оказаться больше, чем другие элементы в куче. <strong>Поэтому необходимо восстановить порядок на пути от вставленного узла к корню</strong> ; эта операция называется упорядочиванием кучи.</p>
<p>Рассмотрим ситуацию, когда упорядочивание выполняется <strong>снизу вверх</strong>, начиная от только что вставленного узла. Как показано на рисунках ниже, мы сравниваем значение вставленного узла со значением его родителя; если вставленный узел больше, то меняем их местами. Затем продолжаем выполнять ту же операцию и последовательно восстанавливать корректность всех узлов по пути снизу вверх, пока не выйдем за корень или не встретим узел, для которого обмен не требуется.</p>
<p>Пусть дан элемент <code>val</code> . Сначала мы помещаем его в основание кучи. После добавления свойства кучи могут нарушиться, потому что <code>val</code> может оказаться больше, чем другие элементы в куче. <strong>Поэтому необходимо восстановить порядок на пути от вставленного узла к корню</strong>. Эта операция называется упорядочиванием кучи.</p>
<p>Рассмотрим ситуацию, когда упорядочивание выполняется <strong>снизу вверх</strong>, начиная от только что вставленного узла. Как показано на рисунке 8-3, мы сравниваем значение вставленного узла со значением его родителя. Если вставленный узел больше, то меняем их местами. Затем продолжаем выполнять ту же операцию и последовательно восстанавливать корректность всех узлов по пути снизу вверх, пока не выйдем за корень или не встретим узел, для которого обмен не требуется.</p>
<div class="tabbed-set tabbed-alternate" data-tabs="4:9"><input checked="checked" id="__tabbed_4_1" name="__tabbed_4" type="radio" /><input id="__tabbed_4_2" name="__tabbed_4" type="radio" /><input id="__tabbed_4_3" name="__tabbed_4" type="radio" /><input id="__tabbed_4_4" name="__tabbed_4" type="radio" /><input id="__tabbed_4_5" name="__tabbed_4" type="radio" /><input id="__tabbed_4_6" name="__tabbed_4" type="radio" /><input id="__tabbed_4_7" name="__tabbed_4" type="radio" /><input id="__tabbed_4_8" name="__tabbed_4" type="radio" /><input id="__tabbed_4_9" name="__tabbed_4" type="radio" /><div class="tabbed-labels"><label for="__tabbed_4_1">&lt;1&gt;</label><label for="__tabbed_4_2">&lt;2&gt;</label><label for="__tabbed_4_3">&lt;3&gt;</label><label for="__tabbed_4_4">&lt;4&gt;</label><label for="__tabbed_4_5">&lt;5&gt;</label><label for="__tabbed_4_6">&lt;6&gt;</label><label for="__tabbed_4_7">&lt;7&gt;</label><label for="__tabbed_4_8">&lt;8&gt;</label><label for="__tabbed_4_9">&lt;9&gt;</label></div>
<div class="tabbed-content">
<div class="tabbed-block">
@@ -5618,7 +5618,7 @@
<li>После обмена удалить основание кучи из списка. Стоит отметить, что, поскольку обмен уже выполнен, фактически удаляется исходный элемент вершины кучи.</li>
<li>Начиная от корневого узла, <strong>выполнить упорядочивание кучи сверху вниз</strong>.</li>
</ol>
<p>Как показано на рисунках ниже, <strong>направление операции упорядочивания кучи сверху вниз противоположно операции упорядочивания кучи снизу вверх</strong>. Мы сравниваем значение корневого узла со значениями двух дочерних узлов, выбираем больший дочерний узел и меняем его местами с корневым узлом. Затем циклически повторяем ту же операцию, пока не выйдем за листовой узел или не встретим узел, который уже не требует обмена.</p>
<p>Как показано на рисунке 8-4, <strong>направление операции упорядочивания кучи сверху вниз противоположно операции упорядочивания кучи снизу вверх</strong>. Мы сравниваем значение корневого узла со значениями двух дочерних узлов, выбираем больший дочерний узел и меняем его местами с корневым узлом. Затем циклически повторяем ту же операцию, пока не выйдем за листовой узел или не встретим узел, который уже не требует обмена.</p>
<div class="tabbed-set tabbed-alternate" data-tabs="6:10"><input checked="checked" id="__tabbed_6_1" name="__tabbed_6" type="radio" /><input id="__tabbed_6_2" name="__tabbed_6" type="radio" /><input id="__tabbed_6_3" name="__tabbed_6" type="radio" /><input id="__tabbed_6_4" name="__tabbed_6" type="radio" /><input id="__tabbed_6_5" name="__tabbed_6" type="radio" /><input id="__tabbed_6_6" name="__tabbed_6" type="radio" /><input id="__tabbed_6_7" name="__tabbed_6" type="radio" /><input id="__tabbed_6_8" name="__tabbed_6" type="radio" /><input id="__tabbed_6_9" name="__tabbed_6" type="radio" /><input id="__tabbed_6_10" name="__tabbed_6" type="radio" /><div class="tabbed-labels"><label for="__tabbed_6_1">&lt;1&gt;</label><label for="__tabbed_6_2">&lt;2&gt;</label><label for="__tabbed_6_3">&lt;3&gt;</label><label for="__tabbed_6_4">&lt;4&gt;</label><label for="__tabbed_6_5">&lt;5&gt;</label><label for="__tabbed_6_6">&lt;6&gt;</label><label for="__tabbed_6_7">&lt;7&gt;</label><label for="__tabbed_6_8">&lt;8&gt;</label><label for="__tabbed_6_9">&lt;9&gt;</label><label for="__tabbed_6_10">&lt;10&gt;</label></div>
<div class="tabbed-content">
<div class="tabbed-block">
@@ -6149,8 +6149,8 @@
</details>
<h2 id="813">8.1.3 &nbsp; Типичные применения кучи<a class="headerlink" href="#813" title="Permanent link">&para;</a></h2>
<ul>
<li><strong>Очередь с приоритетом</strong>: куча обычно является предпочтительной структурой данных для реализации очереди с приоритетом; добавление и извлечение элементов имеют временную сложность <span class="arithmatex">\(O(\log n)\)</span> , а построение кучи - <span class="arithmatex">\(O(n)\)</span> , и все эти операции выполняются очень эффективно.</li>
<li><strong>Пирамидальная сортировка</strong>: для заданного набора данных можно построить кучу, а затем непрерывно извлекать из нее элементы, получая отсортированные данные. Однако на практике мы обычно используем более изящную реализацию пирамидальной сортировки; подробности см. в разделе "Пирамидальная сортировка".</li>
<li><strong>Очередь с приоритетом</strong>: куча обычно является предпочтительной структурой данных для реализации очереди с приоритетом. Добавление и извлечение элементов имеют временную сложность <span class="arithmatex">\(O(\log n)\)</span> , а построение кучи - <span class="arithmatex">\(O(n)\)</span> , и все эти операции выполняются очень эффективно.</li>
<li><strong>Пирамидальная сортировка</strong>: для заданного набора данных можно построить кучу, а затем непрерывно извлекать из нее элементы, получая отсортированные данные. Однако на практике мы обычно используем более изящную реализацию пирамидальной сортировки. Подробности см. в разделе «Пирамидальная сортировка».</li>
<li><strong>Получение наибольших <span class="arithmatex">\(k\)</span> элементов</strong>: это классическая алгоритмическая задача и одновременно типичное применение кучи. Например, выбор 10 самых горячих новостей для списка популярных тем или выбор 10 самых продаваемых товаров.</li>
</ul>
+2 -2
View File
@@ -4360,7 +4360,7 @@
<h3 id="1">1. &nbsp; Ключевые выводы<a class="headerlink" href="#1" title="Permanent link">&para;</a></h3>
<ul>
<li>Куча представляет собой полное двоичное дерево и делится на максимальную кучу и минимальную кучу. Элемент на вершине максимальной (минимальной) кучи является наибольшим (наименьшим).</li>
<li>Очередь с приоритетом определяется как очередь, элементы которой извлекаются в соответствии с приоритетом; обычно ее реализуют с помощью кучи.</li>
<li>Очередь с приоритетом определяется как очередь, элементы которой извлекаются в соответствии с приоритетом. Обычно ее реализуют с помощью кучи.</li>
<li>К основным операциям кучи и их временным сложностям относятся: добавление элемента в кучу <span class="arithmatex">\(O(\log n)\)</span> , извлечение элемента с вершины кучи <span class="arithmatex">\(O(\log n)\)</span> и доступ к вершине кучи <span class="arithmatex">\(O(1)\)</span> .</li>
<li>Полное двоичное дерево очень удобно представлять массивом, поэтому кучу обычно тоже хранят в массиве.</li>
<li>Операция упорядочивания кучи используется для поддержания свойств кучи и применяется как при добавлении элемента, так и при извлечении элемента.</li>
@@ -4368,7 +4368,7 @@
<li>Top-k - это классическая алгоритмическая задача, которую можно эффективно решать с помощью кучи за <span class="arithmatex">\(O(n \log k)\)</span> .</li>
</ul>
<h3 id="2-q-a">2. &nbsp; Q &amp; A<a class="headerlink" href="#2-q-a" title="Permanent link">&para;</a></h3>
<p><strong>Q</strong>: Является ли "куча" как структура данных тем же самым понятием, что и "куча" в управлении памятью?</p>
<p><strong>Q</strong>: Является ли «куча» как структура данных тем же самым понятием, что и «куча» в управлении памятью?</p>
<p>Это не одно и то же, просто у них случайно совпало название. Куча в памяти компьютерной системы является частью динамического распределения памяти: во время выполнения программы она используется для хранения данных. Программа может запросить определенный объем памяти в куче для хранения сложных структур, таких как объекты и массивы. Когда эти данные больше не нужны, память нужно освободить, чтобы не допустить утечек. По сравнению со стековой памятью управление памятью в куче требует большей осторожности, а неправильное использование может привести к утечкам памяти и проблемам с указателями.</p>
<!-- Source file information -->
+5 -5
View File
@@ -4385,23 +4385,23 @@
</div>
<p>Для этой задачи мы сначала покажем два относительно прямолинейных способа решения, а затем более эффективный способ на основе кучи.</p>
<h2 id="831-1">8.3.1 &nbsp; Метод 1: выбор через обход<a class="headerlink" href="#831-1" title="Permanent link">&para;</a></h2>
<p>Как показано на рисунке 8-6, можно выполнить <span class="arithmatex">\(k\)</span> проходов по массиву и на каждом проходе извлекать соответственно <span class="arithmatex">\(1\)</span>-й, <span class="arithmatex">\(2\)</span>-й, <span class="arithmatex">\(\dots\)</span> , <span class="arithmatex">\(k\)</span>-й по величине элемент; временная сложность такого подхода равна <span class="arithmatex">\(O(nk)\)</span> .</p>
<p>Как показано на рисунке 8-6, можно выполнить <span class="arithmatex">\(k\)</span> проходов по массиву и на каждом проходе извлекать соответственно <span class="arithmatex">\(1\)</span>-й, <span class="arithmatex">\(2\)</span>-й, <span class="arithmatex">\(\dots\)</span> , <span class="arithmatex">\(k\)</span>-й по величине элемент. Временная сложность такого подхода равна <span class="arithmatex">\(O(nk)\)</span> .</p>
<p>Этот метод подходит только для случая <span class="arithmatex">\(k \ll n\)</span> , потому что когда <span class="arithmatex">\(k\)</span> приближается к <span class="arithmatex">\(n\)</span> , его временная сложность стремится к <span class="arithmatex">\(O(n^2)\)</span> , а это уже очень затратно.</p>
<p><img alt="Поиск наибольших k элементов через обход" class="animation-figure" src="../top_k.assets/top_k_traversal.png" /></p>
<p align="center"> Рисунок 8-6 &nbsp; Поиск наибольших k элементов через обход </p>
<div class="admonition tip">
<p class="admonition-title">Tip</p>
<p>Когда <span class="arithmatex">\(k = n\)</span> , мы получаем полную упорядоченную последовательность, и в этот момент задача становится эквивалентной алгоритму "сортировка выбором".</p>
<p>Когда <span class="arithmatex">\(k = n\)</span> , мы получаем полную упорядоченную последовательность, и в этот момент задача становится эквивалентной алгоритму «сортировка выбором».</p>
</div>
<h2 id="832-2">8.3.2 &nbsp; Метод 2: сортировка<a class="headerlink" href="#832-2" title="Permanent link">&para;</a></h2>
<p>Как показано на рисунке 8-7, можно сначала отсортировать массив <code>nums</code> , а затем вернуть его крайние правые <span class="arithmatex">\(k\)</span> элементов; временная сложность такого метода равна <span class="arithmatex">\(O(n \log n)\)</span> .</p>
<p>Как показано на рисунке 8-7, можно сначала отсортировать массив <code>nums</code> , а затем вернуть его крайние правые <span class="arithmatex">\(k\)</span> элементов. Временная сложность такого метода равна <span class="arithmatex">\(O(n \log n)\)</span> .</p>
<p>Очевидно, что этот способ делает слишком много, потому что нам нужно только найти наибольшие <span class="arithmatex">\(k\)</span> элементов, а сортировать остальные элементы совсем не обязательно.</p>
<p><img alt="Поиск наибольших k элементов через сортировку" class="animation-figure" src="../top_k.assets/top_k_sorting.png" /></p>
<p align="center"> Рисунок 8-7 &nbsp; Поиск наибольших k элементов через сортировку </p>
<h2 id="833-3">8.3.3 &nbsp; Метод 3: куча<a class="headerlink" href="#833-3" title="Permanent link">&para;</a></h2>
<p>Задачу Top-k можно решить гораздо эффективнее с помощью кучи, как показано на рисунках ниже.</p>
<p>Задачу Top-k можно решить гораздо эффективнее с помощью кучи, как показано на рисунке 8-8.</p>
<ol>
<li>Инициализировать минимальную кучу, у которой вершина содержит наименьший элемент.</li>
<li>Сначала по очереди поместить в кучу первые <span class="arithmatex">\(k\)</span> элементов массива.</li>
@@ -4812,7 +4812,7 @@
<p><div style="height: 549px; width: 100%;"><iframe class="pythontutor-iframe" src="https://pythontutor.com/iframe-embed.html#code=import%20heapq%0A%0Adef%20top_k_heap%28nums%3A%20list%5Bint%5D%2C%20k%3A%20int%29%20-%3E%20list%5Bint%5D%3A%0A%20%20%20%20%22%22%22%D0%9D%D0%B0%D0%B9%D1%82%D0%B8%20k%20%D0%BD%D0%B0%D0%B8%D0%B1%D0%BE%D0%BB%D1%8C%D1%88%D0%B8%D1%85%20%D1%8D%D0%BB%D0%B5%D0%BC%D0%B5%D0%BD%D1%82%D0%BE%D0%B2%20%D0%BC%D0%B0%D1%81%D1%81%D0%B8%D0%B2%D0%B0%20%D1%81%20%D0%BF%D0%BE%D0%BC%D0%BE%D1%89%D1%8C%D1%8E%20%D0%BA%D1%83%D1%87%D0%B8%22%22%22%0A%20%20%20%20%23%20%D0%98%D0%BD%D0%B8%D1%86%D0%B8%D0%B0%D0%BB%D0%B8%D0%B7%D0%B0%D1%86%D0%B8%D1%8F%20%D0%BC%D0%B8%D0%BD%D0%B8%D0%BC%D0%B0%D0%BB%D1%8C%D0%BD%D0%BE%D0%B9%20%D0%BA%D1%83%D1%87%D0%B8%0A%20%20%20%20heap%20%3D%20%5B%5D%0A%20%20%20%20%23%20%D0%9F%D0%BE%D0%BC%D0%B5%D1%81%D1%82%D0%B8%D1%82%D1%8C%20%D0%BF%D0%B5%D1%80%D0%B2%D1%8B%D0%B5%20k%20%D1%8D%D0%BB%D0%B5%D0%BC%D0%B5%D0%BD%D1%82%D0%BE%D0%B2%20%D0%BC%D0%B0%D1%81%D1%81%D0%B8%D0%B2%D0%B0%20%D0%B2%20%D0%BA%D1%83%D1%87%D1%83%0A%20%20%20%20for%20i%20in%20range%28k%29%3A%0A%20%20%20%20%20%20%20%20heapq.heappush%28heap%2C%20nums%5Bi%5D%29%0A%20%20%20%20%23%20%D0%9D%D0%B0%D1%87%D0%B8%D0%BD%D0%B0%D1%8F%20%D1%81%20%D1%8D%D0%BB%D0%B5%D0%BC%D0%B5%D0%BD%D1%82%D0%B0%20k%2B1%2C%20%D0%BF%D0%BE%D0%B4%D0%B4%D0%B5%D1%80%D0%B6%D0%B8%D0%B2%D0%B0%D1%82%D1%8C%20%D0%B4%D0%BB%D0%B8%D0%BD%D1%83%20%D0%BA%D1%83%D1%87%D0%B8%20%D1%80%D0%B0%D0%B2%D0%BD%D0%BE%D0%B9%20k%0A%20%20%20%20for%20i%20in%20range%28k%2C%20len%28nums%29%29%3A%0A%20%20%20%20%20%20%20%20%23%20%D0%95%D1%81%D0%BB%D0%B8%20%D1%82%D0%B5%D0%BA%D1%83%D1%89%D0%B8%D0%B9%20%D1%8D%D0%BB%D0%B5%D0%BC%D0%B5%D0%BD%D1%82%20%D0%B1%D0%BE%D0%BB%D1%8C%D1%88%D0%B5%20%D1%8D%D0%BB%D0%B5%D0%BC%D0%B5%D0%BD%D1%82%D0%B0%20%D0%BD%D0%B0%20%D0%B2%D0%B5%D1%80%D1%88%D0%B8%D0%BD%D0%B5%20%D0%BA%D1%83%D1%87%D0%B8%2C%20%D0%B8%D0%B7%D0%B2%D0%BB%D0%B5%D1%87%D1%8C%20%D0%B2%D0%B5%D1%80%D1%88%D0%B8%D0%BD%D1%83%20%D0%BA%D1%83%D1%87%D0%B8%20%D0%B8%20%D0%B4%D0%BE%D0%B1%D0%B0%D0%B2%D0%B8%D1%82%D1%8C%20%D1%82%D0%B5%D0%BA%D1%83%D1%89%D0%B8%D0%B9%20%D1%8D%D0%BB%D0%B5%D0%BC%D0%B5%D0%BD%D1%82%20%D0%B2%20%D0%BA%D1%83%D1%87%D1%83%0A%20%20%20%20%20%20%20%20if%20nums%5Bi%5D%20%3E%20heap%5B0%5D%3A%0A%20%20%20%20%20%20%20%20%20%20%20%20heapq.heappop%28heap%29%0A%20%20%20%20%20%20%20%20%20%20%20%20heapq.heappush%28heap%2C%20nums%5Bi%5D%29%0A%20%20%20%20return%20heap%0A%0A%22%22%22Driver%20Code%22%22%22%0Aif%20__name__%20%3D%3D%20%22__main__%22%3A%0A%20%20%20%20nums%20%3D%20%5B1%2C%207%2C%206%2C%203%2C%202%5D%0A%20%20%20%20k%20%3D%203%0A%0A%20%20%20%20res%20%3D%20top_k_heap%28nums%2C%20k%29&codeDivHeight=472&codeDivWidth=350&cumulative=false&curInstr=6&heapPrimitives=nevernest&origin=opt-frontend.js&py=311&rawInputLstJSON=%5B%5D&textReferences=false"> </iframe></div>
<div style="margin-top: 5px;"><a href="https://pythontutor.com/iframe-embed.html#code=import%20heapq%0A%0Adef%20top_k_heap%28nums%3A%20list%5Bint%5D%2C%20k%3A%20int%29%20-%3E%20list%5Bint%5D%3A%0A%20%20%20%20%22%22%22%D0%9D%D0%B0%D0%B9%D1%82%D0%B8%20k%20%D0%BD%D0%B0%D0%B8%D0%B1%D0%BE%D0%BB%D1%8C%D1%88%D0%B8%D1%85%20%D1%8D%D0%BB%D0%B5%D0%BC%D0%B5%D0%BD%D1%82%D0%BE%D0%B2%20%D0%BC%D0%B0%D1%81%D1%81%D0%B8%D0%B2%D0%B0%20%D1%81%20%D0%BF%D0%BE%D0%BC%D0%BE%D1%89%D1%8C%D1%8E%20%D0%BA%D1%83%D1%87%D0%B8%22%22%22%0A%20%20%20%20%23%20%D0%98%D0%BD%D0%B8%D1%86%D0%B8%D0%B0%D0%BB%D0%B8%D0%B7%D0%B0%D1%86%D0%B8%D1%8F%20%D0%BC%D0%B8%D0%BD%D0%B8%D0%BC%D0%B0%D0%BB%D1%8C%D0%BD%D0%BE%D0%B9%20%D0%BA%D1%83%D1%87%D0%B8%0A%20%20%20%20heap%20%3D%20%5B%5D%0A%20%20%20%20%23%20%D0%9F%D0%BE%D0%BC%D0%B5%D1%81%D1%82%D0%B8%D1%82%D1%8C%20%D0%BF%D0%B5%D1%80%D0%B2%D1%8B%D0%B5%20k%20%D1%8D%D0%BB%D0%B5%D0%BC%D0%B5%D0%BD%D1%82%D0%BE%D0%B2%20%D0%BC%D0%B0%D1%81%D1%81%D0%B8%D0%B2%D0%B0%20%D0%B2%20%D0%BA%D1%83%D1%87%D1%83%0A%20%20%20%20for%20i%20in%20range%28k%29%3A%0A%20%20%20%20%20%20%20%20heapq.heappush%28heap%2C%20nums%5Bi%5D%29%0A%20%20%20%20%23%20%D0%9D%D0%B0%D1%87%D0%B8%D0%BD%D0%B0%D1%8F%20%D1%81%20%D1%8D%D0%BB%D0%B5%D0%BC%D0%B5%D0%BD%D1%82%D0%B0%20k%2B1%2C%20%D0%BF%D0%BE%D0%B4%D0%B4%D0%B5%D1%80%D0%B6%D0%B8%D0%B2%D0%B0%D1%82%D1%8C%20%D0%B4%D0%BB%D0%B8%D0%BD%D1%83%20%D0%BA%D1%83%D1%87%D0%B8%20%D1%80%D0%B0%D0%B2%D0%BD%D0%BE%D0%B9%20k%0A%20%20%20%20for%20i%20in%20range%28k%2C%20len%28nums%29%29%3A%0A%20%20%20%20%20%20%20%20%23%20%D0%95%D1%81%D0%BB%D0%B8%20%D1%82%D0%B5%D0%BA%D1%83%D1%89%D0%B8%D0%B9%20%D1%8D%D0%BB%D0%B5%D0%BC%D0%B5%D0%BD%D1%82%20%D0%B1%D0%BE%D0%BB%D1%8C%D1%88%D0%B5%20%D1%8D%D0%BB%D0%B5%D0%BC%D0%B5%D0%BD%D1%82%D0%B0%20%D0%BD%D0%B0%20%D0%B2%D0%B5%D1%80%D1%88%D0%B8%D0%BD%D0%B5%20%D0%BA%D1%83%D1%87%D0%B8%2C%20%D0%B8%D0%B7%D0%B2%D0%BB%D0%B5%D1%87%D1%8C%20%D0%B2%D0%B5%D1%80%D1%88%D0%B8%D0%BD%D1%83%20%D0%BA%D1%83%D1%87%D0%B8%20%D0%B8%20%D0%B4%D0%BE%D0%B1%D0%B0%D0%B2%D0%B8%D1%82%D1%8C%20%D1%82%D0%B5%D0%BA%D1%83%D1%89%D0%B8%D0%B9%20%D1%8D%D0%BB%D0%B5%D0%BC%D0%B5%D0%BD%D1%82%20%D0%B2%20%D0%BA%D1%83%D1%87%D1%83%0A%20%20%20%20%20%20%20%20if%20nums%5Bi%5D%20%3E%20heap%5B0%5D%3A%0A%20%20%20%20%20%20%20%20%20%20%20%20heapq.heappop%28heap%29%0A%20%20%20%20%20%20%20%20%20%20%20%20heapq.heappush%28heap%2C%20nums%5Bi%5D%29%0A%20%20%20%20return%20heap%0A%0A%22%22%22Driver%20Code%22%22%22%0Aif%20__name__%20%3D%3D%20%22__main__%22%3A%0A%20%20%20%20nums%20%3D%20%5B1%2C%207%2C%206%2C%203%2C%202%5D%0A%20%20%20%20k%20%3D%203%0A%0A%20%20%20%20res%20%3D%20top_k_heap%28nums%2C%20k%29&codeDivHeight=800&codeDivWidth=600&cumulative=false&curInstr=6&heapPrimitives=nevernest&origin=opt-frontend.js&py=311&rawInputLstJSON=%5B%5D&textReferences=false" target="_blank" rel="noopener noreferrer">Во весь экран &gt;</a></div></p>
</details>
<p>Всего выполняется <span class="arithmatex">\(n\)</span> операций добавления и извлечения из кучи, а максимальная длина кучи равна <span class="arithmatex">\(k\)</span> , поэтому временная сложность равна <span class="arithmatex">\(O(n \log k)\)</span> . Этот метод очень эффективен: когда <span class="arithmatex">\(k\)</span> мало, временная сложность стремится к <span class="arithmatex">\(O(n)\)</span> ; когда <span class="arithmatex">\(k\)</span> велико, она все равно не превышает <span class="arithmatex">\(O(n \log n)\)</span> .</p>
<p>Всего выполняется <span class="arithmatex">\(n\)</span> операций добавления и извлечения из кучи, а максимальная длина кучи равна <span class="arithmatex">\(k\)</span> , поэтому временная сложность равна <span class="arithmatex">\(O(n \log k)\)</span> . Этот метод очень эффективен: когда <span class="arithmatex">\(k\)</span> мало, временная сложность стремится к <span class="arithmatex">\(O(n)\)</span>. Когда <span class="arithmatex">\(k\)</span> велико, она все равно не превышает <span class="arithmatex">\(O(n \log n)\)</span> .</p>
<p>Кроме того, этот метод подходит и для сценариев с динамическим потоком данных. При непрерывном поступлении новых данных мы можем продолжать поддерживать содержимое кучи, тем самым динамически обновляя наибольшие <span class="arithmatex">\(k\)</span> элементов.</p>
<!-- Source file information -->
+6 -6
View File
@@ -4255,19 +4255,19 @@
<!-- Page content -->
<h1 id="_1">Перед началом<a class="headerlink" href="#_1" title="Permanent link">&para;</a></h1>
<p>Несколько лет назад я публиковал на LeetCode разборы серии задач "Sword for Offer" и получил поддержку и ободрение от многих читателей. Во время общения с ними мне чаще всего задавали один и тот же вопрос: "как начать изучать алгоритмы?" Постепенно этот вопрос начал меня по-настоящему занимать.</p>
<p>Слепо бросаться в решение задач кажется самым популярным способом: он прост, прямолинеен и действительно работает. Но решение задач похоже на игру в "Сапера": люди с сильными навыками самообучения способны обезвредить мины одну за другой, а тем, у кого не хватает базы, легко набить себе шишки и шаг за шагом отступить под давлением неудач. Полностью проходить учебники тоже принято часто, но для тех, кто готовится к поиску работы, диплом, резюме, письменные тесты и собеседования уже отнимают большую часть сил, и потому толстые книги нередко превращаются в тяжелое испытание.</p>
<p>Если ты тоже сталкиваешься с такими трудностями, то можно сказать, что эта книга сама "нашла" тебя. Она стала моим ответом на этот вопрос: пусть и не идеальным, но как минимум честной и активной попыткой. Эта книга сама по себе не гарантирует предложения о работе, но поможет тебе увидеть "карту знаний" по структурам данных и алгоритмам, понять форму, размер и расположение разных "мин" и освоить разные "способы разминирования". Освоив это, ты сможешь увереннее решать задачи и читать технические материалы, шаг за шагом выстраивая целостную систему знаний.</p>
<p>Я глубоко согласен со словами профессора Фейнмана: "Knowledge isn't free. You have to pay attention." В этом смысле книга не совсем "бесплатна". Чтобы не подвести то драгоценное "внимание", которое ты ей уделишь, я постараюсь вложить в ее создание максимум собственного "внимания".</p>
<p>Несколько лет назад я публиковал на LeetCode разборы серии задач «Sword for Offer» и получил поддержку и ободрение от многих читателей. Во время общения с ними мне чаще всего задавали один и тот же вопрос: «как начать изучать алгоритмы?» Постепенно этот вопрос начал меня по-настоящему занимать.</p>
<p>Слепо бросаться в решение задач кажется самым популярным способом: он прост, прямолинеен и действительно работает. Но решение задач похоже на игру в «Сапера»: люди с сильными навыками самообучения способны обезвредить мины одну за другой, а тем, у кого не хватает базы, легко набить себе шишки и шаг за шагом отступить под давлением неудач. Полностью проходить учебники тоже принято часто, но для тех, кто готовится к поиску работы, диплом, резюме, письменные тесты и собеседования уже отнимают большую часть сил, и потому толстые книги нередко превращаются в тяжелое испытание.</p>
<p>Если ты тоже сталкиваешься с такими трудностями, то можно сказать, что эта книга сама «нашла» тебя. Она стала моим ответом на этот вопрос: пусть и не идеальным, но как минимум честной и активной попыткой. Эта книга сама по себе не гарантирует предложения о работе, но поможет тебе увидеть «карту знаний» по структурам данных и алгоритмам, понять форму, размер и расположение разных «мин» и освоить разные «способы разминирования». Освоив это, ты сможешь увереннее решать задачи и читать технические материалы, шаг за шагом выстраивая целостную систему знаний.</p>
<p>Я глубоко согласен со словами профессора Фейнмана: «Knowledge isn't free. You have to pay attention.» В этом смысле книга не совсем «бесплатна». Чтобы не подвести то драгоценное «внимание», которое ты ей уделишь, я постараюсь вложить в ее создание максимум собственного «внимания».</p>
<p>Я хорошо понимаю пределы собственных знаний. Хотя материал этой книги уже довольно долго шлифовался, в нем наверняка все еще осталось немало ошибок, поэтому я искренне прошу преподавателей и читателей указывать на неточности и недоработки.</p>
<p><img alt="Hello Algo" class="cover-image" src="../assets/covers/chapter_hello_algo.jpg" /></p>
<p><img alt="Hello Algo" class="«cover-image»" src="../assets/covers/chapter_hello_algo.jpg" /></p>
<div style="text-align: center;">
<h2 style="margin-top: 0.8em; margin-bottom: 0.8em;">Hello, алгоритмы!</h2>
</div>
<p>Появление компьютеров радикально изменило мир. Благодаря высокой скорости вычислений и отличной программируемости они стали идеальной средой для исполнения алгоритмов и обработки данных. Реалистичная графика в играх, интеллектуальные решения в автономном вождении, впечатляющие партии AlphaGo и естественное взаимодействие ChatGPT: все это изящные проявления алгоритмов на компьютере.</p>
<p>На самом деле еще до появления компьютеров алгоритмы и структуры данных уже существовали во всех уголках мира. Ранние алгоритмы были сравнительно простыми: например, древние способы счета или последовательности действий при изготовлении инструментов. По мере развития цивилизации алгоритмы становились тоньше и сложнее. За мастерством ремесленников, промышленными продуктами, освобождающими производительные силы, и даже за научными законами движения Вселенной почти всегда стоит изобретательная алгоритмическая мысль.</p>
<p>Точно так же структуры данных встречаются повсюду: от социальных сетей до схем метро многие системы можно моделировать как "граф"; от государства до семьи основные формы общественной организации обладают свойствами "дерева"; зимняя одежда похожа на "стек", где то, что надевают первым, снимают последним; тубус для бадминтонных воланов похож на "очередь", где элементы добавляются с одного конца и извлекаются с другого; словарь похож на "хеш-таблицу", позволяющую быстро находить нужную статью.</p>
<p>Точно так же структуры данных встречаются повсюду: от социальных сетей до схем метро многие системы можно моделировать как «граф». От государства до семьи основные формы общественной организации обладают свойствами «дерева». Зимняя одежда похожа на «стек», где то, что надевают первым, снимают последним. Тубус для бадминтонных воланов похож на «очередь», где элементы добавляются с одного конца и извлекаются с другого. Словарь похож на «хеш-таблицу», позволяющую быстро находить нужную статью.</p>
<p>Эта книга стремится с помощью понятных анимированных иллюстраций и исполняемых примеров кода помочь читателю понять ключевые идеи алгоритмов и структур данных и научиться реализовывать их программно. На этой основе книга также пытается показать живые проявления алгоритмов в сложном мире и раскрыть их красоту. Надеюсь, она окажется для тебя полезной.</p>
<!-- Source file information -->
@@ -4269,45 +4269,45 @@
<h1 id="11">1.1 &nbsp; Алгоритмы повсюду<a class="headerlink" href="#11" title="Permanent link">&para;</a></h1>
<p>Говоря об алгоритмах, естественно вспомнить о математике. Однако на самом деле многие алгоритмы не связаны со сложной математикой, а больше полагаются на базовую логику, которая повсеместно встречается в нашей повседневной жизни.</p>
<p>Прежде чем углубиться в обсуждение алгоритмов, стоит упомянуть интересный факт: <strong>вы уже точно освоили множество алгоритмов и привыкли применять их в повседневной жизни</strong>. Далее приведем несколько конкретных примеров, чтобы подтвердить этот факт.</p>
<p><strong>Пример 1: поиск в словаре</strong>. В словаре все слова упорядочены по алфавиту. Предположим, нам нужно найти слово, начинающееся на букву <span class="arithmatex">\(r\)</span>; обычно для этого нужно выполнить следующие действия.</p>
<p><strong>Пример 1: поиск в словаре</strong>. В словаре все слова упорядочены по алфавиту. Предположим, нам нужно найти слово, начинающееся на букву <span class="arithmatex">\(r\)</span>. Обычно это делают так, как показано на рисунке 1-1.</p>
<ol>
<li>Откройте словарь примерно на половине страниц и посмотрите, какая буква является первой на этой странице; предположим, это буква <span class="arithmatex">\(m\)</span>.</li>
<li>Откройте словарь примерно на половине страниц и посмотрите, какая буква является первой на этой странице. Предположим, это буква <span class="arithmatex">\(m\)</span>.</li>
<li>Поскольку в алфавите буква <span class="arithmatex">\(r\)</span> идет после <span class="arithmatex">\(m\)</span>, исключаем первую половину словаря, и область поиска сужается до второй половины.</li>
<li>Продолжайте повторять шаги <code>1.</code> и <code>2.</code> , пока не найдете страницу, где первой буквой слов будет <span class="arithmatex">\(r\)</span>.</li>
</ol>
<div class="tabbed-set tabbed-alternate" data-tabs="1:5"><input checked="checked" id="__tabbed_1_1" name="__tabbed_1" type="radio" /><input id="__tabbed_1_2" name="__tabbed_1" type="radio" /><input id="__tabbed_1_3" name="__tabbed_1" type="radio" /><input id="__tabbed_1_4" name="__tabbed_1" type="radio" /><input id="__tabbed_1_5" name="__tabbed_1" type="radio" /><div class="tabbed-labels"><label for="__tabbed_1_1">&lt;1&gt;</label><label for="__tabbed_1_2">&lt;2&gt;</label><label for="__tabbed_1_3">&lt;3&gt;</label><label for="__tabbed_1_4">&lt;4&gt;</label><label for="__tabbed_1_5">&lt;5&gt;</label></div>
<div class="tabbed-content">
<div class="tabbed-block">
<p><img alt="Этапы поиска в словаре. Шаг 1" class="animation-figure" src="../algorithms_are_everywhere.assets/binary_search_dictionary_step1.png" /></p>
<p><img alt="Этапы поиска в словаре" class="animation-figure" src="../algorithms_are_everywhere.assets/binary_search_dictionary_step1.png" /></p>
</div>
<div class="tabbed-block">
<p><img alt="Этапы поиска в словаре. Шаг 2" class="animation-figure" src="../algorithms_are_everywhere.assets/binary_search_dictionary_step2.png" /></p>
<p><img alt="binary_search_dictionary_step2" class="animation-figure" src="../algorithms_are_everywhere.assets/binary_search_dictionary_step2.png" /></p>
</div>
<div class="tabbed-block">
<p><img alt="Этапы поиска в словаре. Шаг 3" class="animation-figure" src="../algorithms_are_everywhere.assets/binary_search_dictionary_step3.png" /></p>
<p><img alt="binary_search_dictionary_step3" class="animation-figure" src="../algorithms_are_everywhere.assets/binary_search_dictionary_step3.png" /></p>
</div>
<div class="tabbed-block">
<p><img alt="Этапы поиска в словаре. Шаг 4" class="animation-figure" src="../algorithms_are_everywhere.assets/binary_search_dictionary_step4.png" /></p>
<p><img alt="binary_search_dictionary_step4" class="animation-figure" src="../algorithms_are_everywhere.assets/binary_search_dictionary_step4.png" /></p>
</div>
<div class="tabbed-block">
<p><img alt="Этапы поиска в словаре. Шаг 5" class="animation-figure" src="../algorithms_are_everywhere.assets/binary_search_dictionary_step5.png" /></p>
<p><img alt="binary_search_dictionary_step5" class="animation-figure" src="../algorithms_are_everywhere.assets/binary_search_dictionary_step5.png" /></p>
</div>
</div>
</div>
<p align="center"> Рисунок 1-1 &nbsp; Этапы поиска в словаре. Шаг 1 </p>
<p align="center"> Рисунок 1-1 &nbsp; Этапы поиска в словаре </p>
<p>Навык поиска в словаре, которым владеет каждый школьник, на самом деле является известным алгоритмом двоичного поиска. С точки зрения структуры данных словарь можно рассматривать как отсортированный массив; с точки зрения алгоритма последовательность операций по поиску в словаре можно считать двоичным поиском.</p>
<p><strong>Пример 2: упорядочивание карт</strong>. Во время игры в карты необходимо каждый раз упорядочивать карты в руке от меньшего к большему. Для этого нужно выполнить следующие действия.</p>
<p>Навык поиска в словаре, которым владеет каждый школьник, на самом деле является известным алгоритмом двоичного поиска. С точки зрения структуры данных словарь можно рассматривать как отсортированный массив. С точки зрения алгоритма последовательность операций по поиску в словаре можно считать двоичным поиском.</p>
<p><strong>Пример 2: упорядочивание карт</strong>. Во время игры в карты необходимо каждый раз упорядочивать карты в руке от меньшего к большему. Обычно это делают так, как показано на рисунке 1-2.</p>
<ol>
<li>Разделите карты на упорядоченную и неупорядоченную части, предполагая, что изначально самая левая карта уже упорядочена.</li>
<li>Из неупорядоченной части извлеките одну карту и вставьте ее в правильное место в упорядоченной части; после этого две самые левые карты станут упорядоченными.</li>
<li>Из неупорядоченной части извлеките одну карту и вставьте ее в правильное место в упорядоченной части. После этого две самые левые карты станут упорядоченными.</li>
<li>Повторяйте шаг <code>2.</code> , каждый раз перемещая одну карту из неупорядоченной части в упорядоченную, пока все карты не станут упорядоченными.</li>
</ol>
<p><img alt="Этапы упорядочивания карт" class="animation-figure" src="../algorithms_are_everywhere.assets/playing_cards_sorting.png" /></p>
<p align="center"> Рисунок 1-2 &nbsp; Этапы упорядочивания карт </p>
<p>Метод упорядочивания карт по своей сути является алгоритмом сортировки вставками, который весьма эффективен при обработке небольших наборов данных. Многие функции сортировки в библиотеках программирования используют именно этот алгоритм.</p>
<p><strong>Пример 3: сдача</strong>. Предположим, что в супермаркете мы купили товар стоимостью <span class="arithmatex">\(69\)</span> руб. и дали кассиру купюру в <span class="arithmatex">\(100\)</span> руб. Кассир должен вернуть нам <span class="arithmatex">\(31\)</span> руб. Для этого ему нужно выполнить следующие действия.</p>
<p><strong>Пример 3: сдача</strong>. Предположим, что в супермаркете мы купили товар стоимостью <span class="arithmatex">\(69\)</span> руб. и дали кассиру купюру в <span class="arithmatex">\(100\)</span> руб. Кассир должен вернуть нам <span class="arithmatex">\(31\)</span> руб. Обычно он рассуждает так, как показано на рисунке 1-3.</p>
<ol>
<li>Варианты выбора - это купюры номиналом меньше <span class="arithmatex">\(31\)</span> руб. Пусть у нас имеются номиналы <span class="arithmatex">\(1\)</span> , <span class="arithmatex">\(5\)</span> , <span class="arithmatex">\(10\)</span> и <span class="arithmatex">\(20\)</span> руб.</li>
<li>Возьмем самую крупную доступную купюру в <span class="arithmatex">\(20\)</span> руб. Остаток сдачи составит <span class="arithmatex">\(31 - 20 = 11\)</span> руб.</li>
+4 -4
View File
@@ -4360,7 +4360,7 @@
<h3 id="1">1. &nbsp; Ключевые выводы<a class="headerlink" href="#1" title="Permanent link">&para;</a></h3>
<ul>
<li>Алгоритмы повсеместно присутствуют в нашей повседневной жизни и не являются недосягаемыми сложными знаниями. На самом деле мы уже освоили множество алгоритмов, которые помогают решать различные жизненные задачи.</li>
<li>Принцип поиска в словаре соответствует алгоритму двоичного поиска. Двоичный поиск иллюстрирует важную идею алгоритмов "разделяй и властвуй".</li>
<li>Принцип поиска в словаре соответствует алгоритму двоичного поиска. Двоичный поиск иллюстрирует важную идею алгоритмов «разделяй и властвуй».</li>
<li>Процесс сортировки карт в колоде очень похож на алгоритм сортировки вставками, который хорошо подходит для сортировки небольших наборов данных.</li>
<li>Процесс размена по своей сути является жадным алгоритмом, в котором на каждом этапе принимается наилучшее на данный момент решение.</li>
<li>Алгоритм представляет собой набор инструкций или шагов, предназначенных для решения конкретной задачи в ограниченное время, а структура данных - это способ организации и хранения данных в компьютере.</li>
@@ -4369,13 +4369,13 @@
</ul>
<h3 id="2-q-a">2. &nbsp; Q &amp; A<a class="headerlink" href="#2-q-a" title="Permanent link">&para;</a></h3>
<p><strong>Q</strong>: Я программист и в повседневной работе никогда не использовал алгоритмы для решения задач, поскольку часто используемые алгоритмы уже встроены в языки программирования и ими можно пользоваться напрямую. Значит ли это, что рабочие задачи еще не требуют применения алгоритмов?</p>
<p>Если сравнить конкретные профессиональные навыки с приемами в боевых искусствах, то базовые дисциплины скорее напоминают "внутреннюю силу".</p>
<p>Если сравнить конкретные профессиональные навыки с приемами в боевых искусствах, то базовые дисциплины скорее напоминают «внутреннюю силу».</p>
<p>Я считаю, что изучение алгоритмов и других базовых дисциплин важно не для того, чтобы реализовывать их с нуля в работе, а для того, чтобы на основе полученных знаний принимать профессиональные решения и оценки при решении задач, тем самым повышая общее качество работы. Простой пример: каждый язык программирования имеет встроенные функции сортировки.</p>
<ul>
<li>Если бы мы не изучали структуры данных и алгоритмы, то, получив любые данные, возможно, просто передали бы их этой функции сортировки. Все работает гладко, производительность хорошая, и на первый взгляд проблем нет.</li>
<li>Однако если мы изучили алгоритмы, то знаем, что временная сложность встроенной функции сортировки составляет <span class="arithmatex">\(O(n \log n)\)</span> ; если же данные представлены целыми числами фиксированной разрядности, например номерами студентов, то можно использовать более эффективный метод поразрядной сортировки, снизив временную сложность до <span class="arithmatex">\(O(nk)\)</span> , где <span class="arithmatex">\(k\)</span> - это количество разрядов, а при больших объемах данных выиграть во времени, затратах и пользовательском опыте.</li>
<li>Однако если мы изучили алгоритмы, то знаем, что временная сложность встроенной функции сортировки составляет <span class="arithmatex">\(O(n \log n)\)</span>. Если же данные представлены целыми числами фиксированной разрядности, например номерами студентов, то можно использовать более эффективный метод поразрядной сортировки, снизив временную сложность до <span class="arithmatex">\(O(nk)\)</span> , где <span class="arithmatex">\(k\)</span> - это количество разрядов, а при больших объемах данных выиграть во времени, затратах и пользовательском опыте.</li>
</ul>
<p>В инженерной практике множество задач трудно решить оптимальным образом, и многие из них решаются "как-то". Сложность задачи зависит как от ее природы, так и от уровня знаний и опыта человека, который ее анализирует. Чем более полными знаниями и большим опытом обладает человек, тем глубже он может проанализировать проблему и тем изящнее может быть ее решение.</p>
<p>В инженерной практике множество задач трудно решить оптимальным образом, и многие из них решаются «как-то». Сложность задачи зависит как от ее природы, так и от уровня знаний и опыта человека, который ее анализирует. Чем более полными знаниями и большим опытом обладает человек, тем глубже он может проанализировать проблему и тем изящнее может быть ее решение.</p>
<!-- Source file information -->
@@ -4446,7 +4446,7 @@
<p>Стоит отметить, что структуры данных и алгоритмы не зависят от языка программирования. Именно поэтому данная книга предлагает их реализации на различных языках.</p>
<div class="admonition tip">
<p class="admonition-title">Принятое сокращение</p>
<p>В реальных обсуждениях выражение "структуры данных и алгоритмы" обычно сокращают до просто "алгоритмы". Например, хорошо известные задачи LeetCode на деле одновременно проверяют знания и по структурам данных, и по алгоритмам.</p>
<p>В реальных обсуждениях выражение «структуры данных и алгоритмы» обычно сокращают до просто «алгоритмы». Например, хорошо известные задачи LeetCode на деле одновременно проверяют знания и по структурам данных, и по алгоритмам.</p>
</div>
<!-- Source file information -->
+5 -10
View File
@@ -4398,27 +4398,22 @@
<ul>
<li><strong>Анализ сложности</strong>: критерии и методы оценки структур данных и алгоритмов. Методы расчета временной и пространственной сложности, распространенные типы, примеры и т. д.</li>
<li><strong>Структуры данных</strong>: классификация основных типов данных и структур данных. Определение, преимущества и недостатки, основные операции, распространенные типы, типичные приложения и методы реализации массивов, списков, стеков, очередей, хеш-таблиц, деревьев, куч и графов.</li>
<li><strong>Алгоритмы</strong>: определение, преимущества и недостатки, эффективность, области применения, этапы решения и примеры задач для поиска, сортировки, алгоритма "разделяй и властвуй", поиска с возвратом, динамического программирования и жадных алгоритмов.</li>
<li><strong>Алгоритмы</strong>: определение, преимущества и недостатки, эффективность, области применения, этапы решения и примеры задач для поиска, сортировки, алгоритма «разделяй и властвуй», поиска с возвратом, динамического программирования и жадных алгоритмов.</li>
</ul>
<p><img alt="Основное содержание книги" class="animation-figure" src="../about_the_book.assets/hello_algo_mindmap.png" /></p>
<p align="center"> Рисунок 0-1 &nbsp; Основное содержание книги </p>
<h2 id="013">0.1.3 &nbsp; Благодарности<a class="headerlink" href="#013" title="Permanent link">&para;</a></h2>
<p>Эта книга постоянно совершенствуется благодаря совместным усилиям множества участников открытого сообщества. Благодарим каждого автора, вложившего свое время и силы; их имена перечислены в порядке, автоматически сгенерированном GitHub: krahets, coderonion, Gonglja, nuomi1, Reanon, justin-tse, hpstory, danielsss, curtishd, night-cruise, S-N-O-R-L-A-X, rongyi, msk397, gvenusleo, khoaxuantu, rivertwilight, K3v123, gyt95, zhuoqinyue, yuelinxin, Zuoxun, mingXta, Phoenix0415, FangYuan33, GN-Yu, longsizhuo, pengchzn, QiLOL, Cathay-Chen, guowei-gong, xBLACKICEx, IsChristina, JoseHung, qualifier1024, hello-ikun, magentaqin, Guanngxu, thomasq0, sunshinesDL, L-Super, Transmigration-zhou, WSL0809, Slone123c, lhxsm, yuan0221, what-is-me, theNefelibatas, Shyam-Chen, sangxiaai, longranger2, codeberg-user, xiongsp, JeffersonHuang, prinpal, seven1240, Wonderdch, malone6, xiaomiusa87, gaofer, bluebean-cloud, a16su, SamJin98, hongyun-robot, nanlei, XiaChuerwu, yd-j, iron-irax, mgisr, steventimes, junminhong, heshuyue, danny900714, Nigh, Dr-XYZ, MolDuM, XC-Zero, reeswell, PXG-XPG, NI-SW, Horbin-Magician, Enlightenus, YangXuanyi, xjr7670, beatrix-chan, DullSword, qq909244296, iStig, boloboloda, hts0000, gledfish, fbigm, echo1937, jiaxianhua, wenjianmin, keshida, kilikilikid, lclc6, lwbaptx, linyejoe2, liuxjerry, szu17dmy, dshlstarr, Yucao-cy, coderlef, czruby, bongbongbakudan, beintentional, ZongYangL, ZhongYuuu, ZhongGuanbin, hezhizhen, linzeyan, ZJKung, JTCPOWI, KawaiiAsh, luluxia, xb534, ztkuaikuai, yw-1021, ElaBosak233, baagod, zhouLion, yishangzhang, yi427, yanedie, yabo083, weibk, wangwang105, th1nk3r-ing, tao363, 4yDX3906, syd168, sslmj2020, smilelsb, siqyka, selear, sdshaoda, Xi-Row, popozhu, nuquist19, noobcodemaker, XiaoK29, chadyi, lyl625760, lucaswangdev, llql1211, 0130w, shanghai-Jerry, EJackYang, Javesun99, eltociear, lipusheng, KNChiu, BlindTerran, ShiMaRing, lovelock, FreddieLi, FloranceYeh, fanchenggang, gltianwen, goerll, nedchu, curly210102, CuB3y0nd, KraHsu, CarrotDLaw, youshaoXG, bubble9um, Asashishi, Asa0oo0o0o, fanenr, eagleanurag, akshiterate, 52coder, foursevenlove, KorsChen, hopkings2008, yang-le, realwujing, Evilrabbit520, Umer-Jahangir, Turing-1024-Lee, Suremotoo, paoxiaomooo, Chieko-Seren, Senrian, Allen-Scai, 19santosh99, ymmmas, Risuntsy, Richard-Zhang1019, RafaelCaso, qingpeng9802, primexiao, Urbaner3, codetypess, nidhoggfgg, MwumLi, CreatorMetaSky, martinx, ZnYang2018, hugtyftg, logan-qiu, psychelzh, Kunchen-Luo, Keynman и KeiichiKasai.</p>
<p>Эта книга постоянно совершенствуется благодаря совместным усилиям множества участников открытого сообщества. Благодарим каждого автора, вложившего свое время и силы. Их имена перечислены в порядке, автоматически сгенерированном GitHub: krahets, coderonion, Gonglja, nuomi1, Reanon, justin-tse, hpstory, danielsss, curtishd, night-cruise, S-N-O-R-L-A-X, rongyi, msk397, gvenusleo, khoaxuantu, rivertwilight, K3v123, gyt95, zhuoqinyue, yuelinxin, Zuoxun, mingXta, Phoenix0415, FangYuan33, GN-Yu, longsizhuo, pengchzn, QiLOL, Cathay-Chen, guowei-gong, xBLACKICEx, IsChristina, JoseHung, qualifier1024, hello-ikun, magentaqin, Guanngxu, thomasq0, sunshinesDL, L-Super, Transmigration-zhou, WSL0809, Slone123c, lhxsm, yuan0221, what-is-me, theNefelibatas, Shyam-Chen, sangxiaai, longranger2, codeberg-user, xiongsp, JeffersonHuang, prinpal, seven1240, Wonderdch, malone6, xiaomiusa87, gaofer, bluebean-cloud, a16su, SamJin98, hongyun-robot, nanlei, XiaChuerwu, yd-j, iron-irax, mgisr, steventimes, junminhong, heshuyue, danny900714, Nigh, Dr-XYZ, MolDuM, XC-Zero, reeswell, PXG-XPG, NI-SW, Horbin-Magician, Enlightenus, YangXuanyi, xjr7670, beatrix-chan, DullSword, qq909244296, iStig, boloboloda, hts0000, gledfish, fbigm, echo1937, jiaxianhua, wenjianmin, keshida, kilikilikid, lclc6, lwbaptx, linyejoe2, liuxjerry, szu17dmy, dshlstarr, Yucao-cy, coderlef, czruby, bongbongbakudan, beintentional, ZongYangL, ZhongYuuu, ZhongGuanbin, hezhizhen, linzeyan, ZJKung, JTCPOWI, KawaiiAsh, luluxia, xb534, ztkuaikuai, yw-1021, ElaBosak233, baagod, zhouLion, yishangzhang, yi427, yanedie, yabo083, weibk, wangwang105, th1nk3r-ing, tao363, 4yDX3906, syd168, sslmj2020, smilelsb, siqyka, selear, sdshaoda, Xi-Row, popozhu, nuquist19, noobcodemaker, XiaoK29, chadyi, lyl625760, lucaswangdev, llql1211, 0130w, shanghai-Jerry, EJackYang, Javesun99, eltociear, lipusheng, KNChiu, BlindTerran, ShiMaRing, lovelock, FreddieLi, FloranceYeh, fanchenggang, gltianwen, goerll, nedchu, curly210102, CuB3y0nd, KraHsu, CarrotDLaw, youshaoXG, bubble9um, Asashishi, Asa0oo0o0o, fanenr, eagleanurag, akshiterate, 52coder, foursevenlove, KorsChen, hopkings2008, yang-le, realwujing, Evilrabbit520, Umer-Jahangir, Turing-1024-Lee, Suremotoo, paoxiaomooo, Chieko-Seren, Senrian, Allen-Scai, 19santosh99, ymmmas, Risuntsy, Richard-Zhang1019, RafaelCaso, qingpeng9802, primexiao, Urbaner3, codetypess, nidhoggfgg, MwumLi, CreatorMetaSky, martinx, ZnYang2018, hugtyftg, logan-qiu, psychelzh, Kunchen-Luo, Keynman и KeiichiKasai.</p>
<p>Рецензирование кода книги выполнили coderonion, curtishd, Gonglja, gvenusleo, hpstory, justin-tse, khoaxuantu, krahets, night-cruise, nuomi1, Reanon и rongyi (в алфавитном порядке). Благодарим их за потраченное время и силы, которые обеспечили стандартизацию и единообразие кода на различных языках.</p>
<p>Английскую версию книги вычитали yuelinxin, K3v123, magentaqin, QiLOL, Phoenix0415, SamJin98, yanedie, RafaelCaso, pengchzn и thomasq0; японскую версию - eltociear; русскую версию - И. А. Шевкун и Yuyan Huang; традиционную китайскую версию - Shyam-Chen и Dr-XYZ. Именно благодаря их вкладу эта книга может служить более широкому кругу читателей, и мы искренне благодарим их.</p>
<p>Английскую версию книги вычитали yuelinxin, K3v123, magentaqin, QiLOL, Phoenix0415, SamJin98, yanedie, RafaelCaso, pengchzn и thomasq0. Японскую версию - eltociear. Русскую версию - И. А. Шевкун и Yuyan Huang. Традиционную китайскую версию - Shyam-Chen и Dr-XYZ. Именно благодаря их вкладу эта книга может служить более широкому кругу читателей, и мы искренне благодарим их.</p>
<p>Инструмент генерации ePub-версии этой книги разработал zhongfq. Благодарим его за вклад, который дал читателям более гибкий способ чтения.</p>
<p>В процессе создания этой книги мне помогало много людей.</p>
<ul>
<li>Благодарю моего наставника в компании, доктора Ли Си: в одной из бесед вы вдохновили меня быстрее начать, что укрепило мою решимость написать эту книгу;</li>
<li>Благодарю мою девушку Bubble, первого читателя этой книги: с позиции новичка в алгоритмах она дала много ценных советов, благодаря которым книга стала более понятной и доступной;</li>
<li>Благодарю Tengbao, Qibao и Feibao за креативное название книги, которое навевает приятные воспоминания о первой строке кода "Hello World!";</li>
<li>Благодарю Xiaoquan за профессиональную помощь в вопросах интеллектуальной собственности, что сыграло важную роль в совершенствовании этой открытой книги;</li>
<li>Благодарю Sutong за дизайн обложки и логотипа книги, а также за терпение при многочисленных исправлениях по моим просьбам;</li>
<li>Благодарю @squidfunk за советы по оформлению и за разработку открытой темы документации <a href="https://github.com/squidfunk/mkdocs-material">Material-for-MkDocs</a>.</li>
<li>Благодарю моего наставника в компании, доктора Ли Си: в одной из бесед вы вдохновили меня быстрее начать, что укрепило мою решимость написать эту книгу. - Благодарю мою девушку Bubble, первого читателя этой книги: с позиции новичка в алгоритмах она дала много ценных советов, благодаря которым книга стала более понятной и доступной. - Благодарю Tengbao, Qibao и Feibao за креативное название книги, которое навевает приятные воспоминания о первой строке кода «Hello World!». - Благодарю Xiaoquan за профессиональную помощь в вопросах интеллектуальной собственности, что сыграло важную роль в совершенствовании этой открытой книги. - Благодарю Sutong за дизайн обложки и логотипа книги, а также за терпение при многочисленных исправлениях по моим просьбам. - Благодарю @squidfunk за советы по оформлению и за разработку открытой темы документации <a href="https://github.com/squidfunk/mkdocs-material">Material-for-MkDocs</a>.</li>
</ul>
<p>В процессе написания книги я ознакомился с множеством учебников и статей по структурам данных и алгоритмам. Эти работы послужили отличным образцом для этой книги, обеспечив ее точность и качество. Я искренне благодарю всех преподавателей и предшественников за их выдающийся вклад!</p>
<p>Эта книга пропагандирует метод обучения, сочетающий умственную и практическую деятельность; в этом отношении на меня сильно повлияла <a href="https://github.com/d2l-ai/d2l-zh">Dive into Deep Learning</a>. Я настоятельно рекомендую эту замечательную работу всем читателям.</p>
<p>Эта книга пропагандирует метод обучения, сочетающий умственную и практическую деятельность. В этом отношении на меня сильно повлияла <a href="https://github.com/d2l-ai/d2l-zh">Dive into Deep Learning</a>. Я настоятельно рекомендую эту замечательную работу всем читателям.</p>
<p><strong>Сердечно благодарю моих родителей: именно ваша постоянная поддержка и ободрение дали мне возможность заняться этим увлекательным делом</strong>.</p>
<!-- Source file information -->
+8 -8
View File
@@ -4432,8 +4432,8 @@
<li>Главы, помеченные <code>*</code> в заголовке, являются дополнительными и содержат более сложный материал. Если времени мало, их можно пропустить.</li>
<li>Профессиональные термины выделяются полужирным шрифтом в печатной и PDF-версии или подчеркиванием в веб-версии, например <u>массив (array)</u>. Рекомендуется запоминать их для удобства чтения литературы.</li>
<li>Важные моменты и обобщающие фразы будут <strong>выделяться полужирным шрифтом</strong>, и на такие тексты следует обращать особое внимание.</li>
<li>Слова и выражения со специальным смыслом будут отмечаться "кавычками", чтобы избежать неоднозначности.</li>
<li>Когда термины различаются между языками программирования, в качестве стандарта используется Python; например, <code>None</code> применяется для обозначения "пустого" значения.</li>
<li>Слова и выражения со специальным смыслом будут отмечаться «кавычками», чтобы избежать неоднозначности.</li>
<li>Когда термины различаются между языками программирования, в качестве стандарта используется Python. Например, <code>None</code> применяется для обозначения «пустого» значения.</li>
<li>В некоторых местах книга отходит от стандартов комментирования программного кода ради более компактного оформления. Комментарии в основном делятся на три типа: заголовочные, содержательные и многострочные.</li>
</ul>
<div class="tabbed-set tabbed-alternate" data-tabs="1:13"><input checked="checked" id="__tabbed_1_1" name="__tabbed_1" type="radio" /><input id="__tabbed_1_2" name="__tabbed_1" type="radio" /><input id="__tabbed_1_3" name="__tabbed_1" type="radio" /><input id="__tabbed_1_4" name="__tabbed_1" type="radio" /><input id="__tabbed_1_5" name="__tabbed_1" type="radio" /><input id="__tabbed_1_6" name="__tabbed_1" type="radio" /><input id="__tabbed_1_7" name="__tabbed_1" type="radio" /><input id="__tabbed_1_8" name="__tabbed_1" type="radio" /><input id="__tabbed_1_9" name="__tabbed_1" type="radio" /><input id="__tabbed_1_10" name="__tabbed_1" type="radio" /><input id="__tabbed_1_11" name="__tabbed_1" type="radio" /><input id="__tabbed_1_12" name="__tabbed_1" type="radio" /><input id="__tabbed_1_13" name="__tabbed_1" type="radio" /><div class="tabbed-labels"><label for="__tabbed_1_1">Python</label><label for="__tabbed_1_2">C++</label><label for="__tabbed_1_3">Java</label><label for="__tabbed_1_4">C#</label><label for="__tabbed_1_5">Go</label><label for="__tabbed_1_6">Swift</label><label for="__tabbed_1_7">JS</label><label for="__tabbed_1_8">TS</label><label for="__tabbed_1_9">Dart</label><label for="__tabbed_1_10">Rust</label><label for="__tabbed_1_11">C</label><label for="__tabbed_1_12">Kotlin</label><label for="__tabbed_1_13">Ruby</label></div>
@@ -4586,7 +4586,7 @@
<p align="center"> Рисунок 0-2 &nbsp; Пример анимированной иллюстрации </p>
<h2 id="023">0.2.3 &nbsp; Углубление понимания через практику кода<a class="headerlink" href="#023" title="Permanent link">&para;</a></h2>
<p>Сопроводительный код этой книги размещен в <a href="https://github.com/krahets/hello-algo">репозитории GitHub</a>. Как показано ниже, <strong>исходный код содержит тестовые примеры и может быть запущен одним нажатием кнопки</strong>.</p>
<p>Сопроводительный код этой книги размещен в <a href="https://github.com/krahets/hello-algo">репозитории GitHub</a>. Как показано на рисунке 0-3, <strong>исходный код содержит тестовые примеры и может быть запущен одним нажатием кнопки</strong>.</p>
<p>Если позволяет время, <strong>рекомендуется самостоятельно набирать код</strong>. Если времени на обучение мало, по крайней мере <strong>просмотрите и выполните весь код</strong>.</p>
<p>Процесс написания кода приносит больше пользы, чем его чтение. <strong>Настоящее обучение - это обучение на практике</strong>.</p>
<p><img alt="Пример запуска кода" class="animation-figure" src="../../index.assets/running_code.gif" /></p>
@@ -4597,7 +4597,7 @@
<p><strong>Шаг 2: клонирование или загрузка репозитория кода</strong>. Перейдите в <a href="https://github.com/krahets/hello-algo">репозиторий GitHub</a>. Если у вас уже установлен <a href="https://git-scm.com/downloads">Git</a>, репозиторий можно клонировать следующей командой:</p>
<div class="highlight"><pre><span></span><code><a id="__codelineno-13-1" name="__codelineno-13-1" href="#__codelineno-13-1"></a>git<span class="w"> </span>clone<span class="w"> </span>https://github.com/krahets/hello-algo.git
</code></pre></div>
<p>Также можно нажать кнопку "Download ZIP" в месте, показанном на рисунке 0-4, напрямую скачать архив с кодом и затем распаковать его локально.</p>
<p>Также можно нажать кнопку «Download ZIP» в месте, показанном на рисунке 0-4, напрямую скачать архив с кодом и затем распаковать его локально.</p>
<p><img alt="Клонирование репозитория и загрузка кода" class="animation-figure" src="../suggestions.assets/download_code.png" /></p>
<p align="center"> Рисунок 0-4 &nbsp; Клонирование репозитория и загрузка кода </p>
@@ -4605,7 +4605,7 @@
<p><img alt="Блоки кода и соответствующие исходные файлы" class="animation-figure" src="../suggestions.assets/code_md_to_repo.png" /></p>
<p align="center"> Рисунок 0-5 &nbsp; Блоки кода и соответствующие исходные файлы </p>
<p>Помимо локального запуска, <strong>веб-версия также поддерживает визуальное выполнение Python-кода</strong> (на базе <a href="https://pythontutor.com/">pythontutor</a>). Как показано ниже, можно нажать "Визуализировать выполнение" под блоком кода, чтобы раскрыть окно и наблюдать за выполнением алгоритма; также можно нажать "Полноэкранный режим" для более удобного просмотра.</p>
<p>Помимо локального запуска, <strong>веб-версия также поддерживает визуальное выполнение Python-кода</strong> (на базе <a href="https://pythontutor.com/">pythontutor</a>). Как показано на рисунке 0-6, можно нажать «Визуализировать выполнение» под блоком кода, чтобы раскрыть окно и наблюдать за выполнением алгоритма. Также можно нажать «Полноэкранный режим» для более удобного просмотра.</p>
<p><img alt="Визуальный запуск Python-кода" class="animation-figure" src="../suggestions.assets/pythontutor_example.png" /></p>
<p align="center"> Рисунок 0-6 &nbsp; Визуальный запуск Python-кода </p>
@@ -4619,10 +4619,10 @@
<p>В целом процесс изучения структур данных и алгоритмов можно разделить на три этапа.</p>
<ol>
<li><strong>Этап 1: введение в алгоритмы</strong>. Необходимо познакомиться с особенностями и применением различных структур данных, изучить принципы, процессы, назначение и эффективность различных алгоритмов.</li>
<li><strong>Этап 2: решение алгоритмических задач</strong>. Рекомендуется начинать с популярных задач и решить не менее 100 из них, чтобы познакомиться с основными алгоритмическими проблемами. При первых попытках "забывание знаний" может стать испытанием, но это нормально. Следуйте при повторении задач "кривой забывания Эббингауза", и обычно после 3-5 циклов повторения материал хорошо запоминается. Рекомендуемые списки задач и планы практики см. в этом <a href="https://github.com/krahets/LeetCode-Book">репозитории GitHub</a>.</li>
<li><strong>Этап 3: построение системы знаний</strong>. В процессе обучения можно читать статьи по алгоритмам, изучать каркасы решений и учебники, чтобы постоянно обогащать свою систему знаний. В решении задач можно применять продвинутые стратегии, например классификацию по темам, несколько решений одной задачи или одно решение для нескольких задач; соответствующий опыт можно найти в различных сообществах.</li>
<li><strong>Этап 2: решение алгоритмических задач</strong>. Рекомендуется начинать с популярных задач и решить не менее 100 из них, чтобы познакомиться с основными алгоритмическими проблемами. При первых попытках «забывание знаний» может стать испытанием, но это нормально. Следуйте при повторении задач «кривой забывания Эббингауза», и обычно после 3-5 циклов повторения материал хорошо запоминается. Рекомендуемые списки задач и планы практики см. в этом <a href="https://github.com/krahets/LeetCode-Book">репозитории GitHub</a>.</li>
<li><strong>Этап 3: построение системы знаний</strong>. В процессе обучения можно читать статьи по алгоритмам, изучать каркасы решений и учебники, чтобы постоянно обогащать свою систему знаний. В решении задач можно применять продвинутые стратегии, например классификацию по темам, несколько решений одной задачи или одно решение для нескольких задач. Соответствующий опыт можно найти в различных сообществах.</li>
</ol>
<p>Как показано на рисунке 0-8, содержание этой книги в основном охватывает "этап 1" и призвано помочь вам более эффективно перейти к обучению на этапах 2 и 3.</p>
<p>Как показано на рисунке 0-8, содержание этой книги в основном охватывает «этап 1» и призвано помочь вам более эффективно перейти к обучению на этапах 2 и 3.</p>
<p><img alt="Дорожная карта изучения алгоритмов" class="animation-figure" src="../suggestions.assets/learning_route.png" /></p>
<p align="center"> Рисунок 0-8 &nbsp; Дорожная карта изучения алгоритмов </p>
+1 -1
View File
@@ -4260,7 +4260,7 @@
<p>[3] Robert Sedgewick и др. Algorithms (4<sup>th</sup> Edition).</p>
<p>[4] Yan Weimin. Data Structures (C Language Edition).</p>
<p>[5] Deng Junhui. Data Structures (C++ Language Edition, 3<sup>rd</sup> Edition).</p>
<p>[6] Mark Allen Weiss; пер. Chen Yue. Data Structures and Algorithm Analysis: Java Description (3<sup>rd</sup> Edition).</p>
<p>[6] Mark Allen Weiss. Пер. Chen Yue. Data Structures and Algorithm Analysis: Java Description (3<sup>rd</sup> Edition).</p>
<p>[7] Cheng Jie. A Plainspoken Guide to Data Structures.</p>
<p>[8] Wang Zheng. The Beauty of Data Structures and Algorithms.</p>
<p>[9] Gayle Laakmann McDowell. Cracking the Coding Interview: 189 Programming Questions and Solutions (6<sup>th</sup> Edition).</p>
@@ -4357,7 +4357,7 @@
<!-- Page content -->
<h1 id="101">10.1 &nbsp; Двоичный поиск<a class="headerlink" href="#101" title="Permanent link">&para;</a></h1>
<p><u>Двоичный поиск (binary search)</u> - это эффективный алгоритм поиска, основанный на стратегии "разделяй и властвуй". Он использует упорядоченность данных, сокращая на каждом шаге область поиска вдвое, пока не будет найден целевой элемент или пока интервал поиска не опустеет.</p>
<p><u>Двоичный поиск (binary search)</u> - это эффективный алгоритм поиска, основанный на стратегии «разделяй и властвуй». Он использует упорядоченность данных, сокращая на каждом шаге область поиска вдвое, пока не будет найден целевой элемент или пока интервал поиска не опустеет.</p>
<div class="admonition question">
<p class="admonition-title">Question</p>
<p>Дан массив <code>nums</code> длины <span class="arithmatex">\(n\)</span>, элементы которого расположены в порядке возрастания и не повторяются. Найдите и верните индекс элемента <code>target</code> в этом массиве. Если массив не содержит этого элемента, верните <span class="arithmatex">\(-1\)</span> . Пример показан на рисунке 10-1.</p>
@@ -4996,7 +4996,7 @@
<div style="margin-top: 5px;"><a href="https://pythontutor.com/iframe-embed.html#code=def%20binary_search_lcro%28nums%3A%20list%5Bint%5D%2C%20target%3A%20int%29%20-%3E%20int%3A%0A%20%20%20%20%22%22%22%D0%91%D0%B8%D0%BD%D0%B0%D1%80%D0%BD%D1%8B%D0%B9%20%D0%BF%D0%BE%D0%B8%D1%81%D0%BA%20%28%D0%BB%D0%B5%D0%B2%D0%BE%20%D0%B7%D0%B0%D0%BC%D0%BA%D0%BD%D1%83%D1%82%D1%8B%D0%B9%2C%20%D0%BF%D1%80%D0%B0%D0%B2%D0%BE%20%D0%BE%D1%82%D0%BA%D1%80%D1%8B%D1%82%D1%8B%D0%B9%20%D0%B8%D0%BD%D1%82%D0%B5%D1%80%D0%B2%D0%B0%D0%BB%29%22%22%22%0A%20%20%20%20%23%20%D0%98%D0%BD%D0%B8%D1%86%D0%B8%D0%B0%D0%BB%D0%B8%D0%B7%D0%B8%D1%80%D0%BE%D0%B2%D0%B0%D1%82%D1%8C%20%D0%BB%D0%B5%D0%B2%D0%BE%20%D0%B7%D0%B0%D0%BC%D0%BA%D0%BD%D1%83%D1%82%D1%8B%D0%B9%2C%20%D0%BF%D1%80%D0%B0%D0%B2%D0%BE%20%D0%BE%D1%82%D0%BA%D1%80%D1%8B%D1%82%D1%8B%D0%B9%20%D0%B8%D0%BD%D1%82%D0%B5%D1%80%D0%B2%D0%B0%D0%BB%20%5B0%2C%20n%29%2C%20%D1%82%D0%BE%20%D0%B5%D1%81%D1%82%D1%8C%20i%20%D0%B8%20j%20%D1%83%D0%BA%D0%B0%D0%B7%D1%8B%D0%B2%D0%B0%D1%8E%D1%82%20%D0%BD%D0%B0%20%D0%BF%D0%B5%D1%80%D0%B2%D1%8B%D0%B9%20%D1%8D%D0%BB%D0%B5%D0%BC%D0%B5%D0%BD%D1%82%20%D0%BC%D0%B0%D1%81%D1%81%D0%B8%D0%B2%D0%B0%20%D0%B8%20%D0%BF%D0%BE%D0%B7%D0%B8%D1%86%D0%B8%D1%8E%20%D1%81%D1%80%D0%B0%D0%B7%D1%83%20%D0%B7%D0%B0%20%D0%BF%D0%BE%D1%81%D0%BB%D0%B5%D0%B4%D0%BD%D0%B8%D0%BC%20%D1%8D%D0%BB%D0%B5%D0%BC%D0%B5%D0%BD%D1%82%D0%BE%D0%BC%20%D1%81%D0%BE%D0%BE%D1%82%D0%B2%D0%B5%D1%82%D1%81%D1%82%D0%B2%D0%B5%D0%BD%D0%BD%D0%BE%0A%20%20%20%20i%2C%20j%20%3D%200%2C%20len%28nums%29%0A%20%20%20%20%23%20%D0%A6%D0%B8%D0%BA%D0%BB%20%D0%B7%D0%B0%D0%B2%D0%B5%D1%80%D1%88%D0%B0%D0%B5%D1%82%D1%81%D1%8F%2C%20%D0%BA%D0%BE%D0%B3%D0%B4%D0%B0%20%D0%B4%D0%B8%D0%B0%D0%BF%D0%B0%D0%B7%D0%BE%D0%BD%20%D0%BF%D0%BE%D0%B8%D1%81%D0%BA%D0%B0%20%D0%BF%D1%83%D1%81%D1%82%20%28%D0%BF%D1%80%D0%B8%20i%20%3D%20j%20%D0%B4%D0%B8%D0%B0%D0%BF%D0%B0%D0%B7%D0%BE%D0%BD%20%D0%BF%D1%83%D1%81%D1%82%29%0A%20%20%20%20while%20i%20%3C%20j%3A%0A%20%20%20%20%20%20%20%20m%20%3D%20%28i%20%2B%20j%29%20%2F%2F%202%20%20%23%20%D0%92%D1%8B%D1%87%D0%B8%D1%81%D0%BB%D0%B8%D1%82%D1%8C%20%D0%B8%D0%BD%D0%B4%D0%B5%D0%BA%D1%81%20%D1%81%D0%B5%D1%80%D0%B5%D0%B4%D0%B8%D0%BD%D1%8B%20m%0A%20%20%20%20%20%20%20%20if%20nums%5Bm%5D%20%3C%20target%3A%0A%20%20%20%20%20%20%20%20%20%20%20%20i%20%3D%20m%20%2B%201%20%20%23%20%D0%AD%D1%82%D0%BE%20%D0%BE%D0%B7%D0%BD%D0%B0%D1%87%D0%B0%D0%B5%D1%82%2C%20%D1%87%D1%82%D0%BE%20target%20%D0%BD%D0%B0%D1%85%D0%BE%D0%B4%D0%B8%D1%82%D1%81%D1%8F%20%D0%B2%20%D0%B8%D0%BD%D1%82%D0%B5%D1%80%D0%B2%D0%B0%D0%BB%D0%B5%20%5Bm%2B1%2C%20j%29%0A%20%20%20%20%20%20%20%20elif%20nums%5Bm%5D%20%3E%20target%3A%0A%20%20%20%20%20%20%20%20%20%20%20%20j%20%3D%20m%20%20%23%20%D0%AD%D1%82%D0%BE%20%D0%BE%D0%B7%D0%BD%D0%B0%D1%87%D0%B0%D0%B5%D1%82%2C%20%D1%87%D1%82%D0%BE%20target%20%D0%BD%D0%B0%D1%85%D0%BE%D0%B4%D0%B8%D1%82%D1%81%D1%8F%20%D0%B2%20%D0%B8%D0%BD%D1%82%D0%B5%D1%80%D0%B2%D0%B0%D0%BB%D0%B5%20%5Bi%2C%20m%29%0A%20%20%20%20%20%20%20%20else%3A%0A%20%20%20%20%20%20%20%20%20%20%20%20return%20m%20%20%23%20%D0%A6%D0%B5%D0%BB%D0%B5%D0%B2%D0%BE%D0%B9%20%D1%8D%D0%BB%D0%B5%D0%BC%D0%B5%D0%BD%D1%82%20%D0%BD%D0%B0%D0%B9%D0%B4%D0%B5%D0%BD%2C%20%D0%B2%D0%B5%D1%80%D0%BD%D1%83%D1%82%D1%8C%20%D0%B5%D0%B3%D0%BE%20%D0%B8%D0%BD%D0%B4%D0%B5%D0%BA%D1%81%0A%20%20%20%20return%20-1%20%20%23%20%D0%A6%D0%B5%D0%BB%D0%B5%D0%B2%D0%BE%D0%B9%20%D1%8D%D0%BB%D0%B5%D0%BC%D0%B5%D0%BD%D1%82%20%D0%BD%D0%B5%20%D0%BD%D0%B0%D0%B9%D0%B4%D0%B5%D0%BD%2C%20%D0%B2%D0%B5%D1%80%D0%BD%D1%83%D1%82%D1%8C%20-1%0A%0A%0A%22%22%22Driver%20Code%22%22%22%0Aif%20__name__%20%3D%3D%20%22__main__%22%3A%0A%20%20%20%20target%20%3D%206%0A%20%20%20%20nums%20%3D%20%5B1%2C%203%2C%206%2C%208%2C%2012%2C%2015%2C%2023%2C%2026%2C%2031%2C%2035%5D%0A%0A%20%20%20%20%23%20%D0%91%D0%B8%D0%BD%D0%B0%D1%80%D0%BD%D1%8B%D0%B9%20%D0%BF%D0%BE%D0%B8%D1%81%D0%BA%20%28%D0%BB%D0%B5%D0%B2%D0%BE%20%D0%B7%D0%B0%D0%BC%D0%BA%D0%BD%D1%83%D1%82%D1%8B%D0%B9%2C%20%D0%BF%D1%80%D0%B0%D0%B2%D0%BE%20%D0%BE%D1%82%D0%BA%D1%80%D1%8B%D1%82%D1%8B%D0%B9%20%D0%B8%D0%BD%D1%82%D0%B5%D1%80%D0%B2%D0%B0%D0%BB%29%0A%20%20%20%20index%20%3D%20binary_search_lcro%28nums%2C%20target%29%0A%20%20%20%20print%28%22%D0%98%D0%BD%D0%B4%D0%B5%D0%BA%D1%81%20%D1%86%D0%B5%D0%BB%D0%B5%D0%B2%D0%BE%D0%B3%D0%BE%20%D1%8D%D0%BB%D0%B5%D0%BC%D0%B5%D0%BD%D1%82%D0%B0%206%20%3D%20%22%2C%20index%29&codeDivHeight=800&codeDivWidth=600&cumulative=false&curInstr=5&heapPrimitives=nevernest&origin=opt-frontend.js&py=311&rawInputLstJSON=%5B%5D&textReferences=false" target="_blank" rel="noopener noreferrer">Во весь экран &gt;</a></div></p>
</details>
<p>Как показано на рисунке 10-3, в этих двух вариантах представления интервала различаются инициализация, условие цикла и операция сужения интервала в алгоритме двоичного поиска.</p>
<p>Поскольку в записи "двойной замкнутый интервал" обе границы являются закрытыми, операции сужения интервала при помощи указателей <span class="arithmatex">\(i\)</span> и <span class="arithmatex">\(j\)</span> тоже получаются симметричными. Из-за этого в таком варианте сложнее допустить ошибку, <strong>поэтому обычно рекомендуется использовать именно запись "двойной замкнутый интервал"</strong>.</p>
<p>Поскольку в записи «двойной замкнутый интервал» обе границы являются закрытыми, операции сужения интервала при помощи указателей <span class="arithmatex">\(i\)</span> и <span class="arithmatex">\(j\)</span> тоже получаются симметричными. Из-за этого в таком варианте сложнее допустить ошибку, <strong>поэтому обычно рекомендуется использовать именно запись «двойной замкнутый интервал»</strong>.</p>
<p><img alt="Два определения интервалов" class="animation-figure" src="../binary_search.assets/binary_search_ranges.png" /></p>
<p align="center"> Рисунок 10-3 &nbsp; Два определения интервалов </p>
@@ -5010,7 +5010,7 @@
<ul>
<li>Двоичный поиск применим только к упорядоченным данным. Если входные данные неупорядочены, специально сортировать их ради двоичного поиска невыгодно. Это связано с тем, что временная сложность алгоритмов сортировки обычно составляет <span class="arithmatex">\(O(n \log n)\)</span> , что выше, чем у линейного и двоичного поиска. Если элементы приходится часто вставлять, то для сохранения порядка в массиве их нужно помещать в конкретные позиции, а это требует <span class="arithmatex">\(O(n)\)</span> времени и тоже обходится дорого.</li>
<li>Двоичный поиск применим только к массивам. Для него нужен скачкообразный доступ к элементам, а в связном списке такой доступ малоэффективен, поэтому двоичный поиск не подходит для списков и структур данных, построенных на их основе.</li>
<li>При малом объеме данных линейный поиск работает лучше. В линейном поиске на каждом шаге нужна всего одна операция сравнения; в двоичном поиске требуется 1 сложение, 1 деление, от 1 до 3 сравнений и еще 1 сложение или вычитание, то есть всего от 4 до 6 элементарных операций. Поэтому при небольшом <span class="arithmatex">\(n\)</span> линейный поиск может оказаться быстрее двоичного.</li>
<li>При малом объеме данных линейный поиск работает лучше. В линейном поиске на каждом шаге нужна всего одна операция сравнения. В двоичном поиске требуется 1 сложение, 1 деление, от 1 до 3 сравнений и еще 1 сложение или вычитание, то есть всего от 4 до 6 элементарных операций. Поэтому при небольшом <span class="arithmatex">\(n\)</span> линейный поиск может оказаться быстрее двоичного.</li>
</ul>
<!-- Source file information -->
@@ -4964,9 +4964,9 @@
</details>
<div class="admonition tip">
<p class="admonition-title">Tip</p>
<p>Код в этом разделе записан в стиле "двойного замкнутого интервала". При желании можно самостоятельно реализовать вариант "слева закрыт, справа открыт".</p>
<p>Код в этом разделе записан в стиле «двойного замкнутого интервала». При желании можно самостоятельно реализовать вариант «слева закрыт, справа открыт».</p>
</div>
<p>Если смотреть в целом, суть двоичного поиска сводится к тому, что для указателей <span class="arithmatex">\(i\)</span> и <span class="arithmatex">\(j\)</span> заранее задаются ориентиры поиска; целью может быть конкретный элемент, например <code>target</code> , а может быть и диапазон элементов, например все элементы, меньшие <code>target</code> .</p>
<p>Если смотреть в целом, суть двоичного поиска сводится к тому, что для указателей <span class="arithmatex">\(i\)</span> и <span class="arithmatex">\(j\)</span> заранее задаются ориентиры поиска. Целью может быть конкретный элемент, например <code>target</code> , а может быть и диапазон элементов, например все элементы, меньшие <code>target</code> .</p>
<p>В ходе непрерывного двоичного деления указатели <span class="arithmatex">\(i\)</span> и <span class="arithmatex">\(j\)</span> постепенно приближаются к заранее заданной цели. В конце они либо успешно находят ответ, либо останавливаются после выхода за границы.</p>
<!-- Source file information -->
@@ -4363,7 +4363,7 @@
<p>Дан массив целых чисел <code>nums</code> и целевой элемент <code>target</code> . Найдите в массиве два элемента, сумма которых равна <code>target</code> , и верните их индексы. Подойдет любой корректный ответ.</p>
</div>
<h2 id="1041">10.4.1 &nbsp; Линейный поиск: обмен времени на пространство<a class="headerlink" href="#1041" title="Permanent link">&para;</a></h2>
<p>Рассмотрим прямой перебор всех возможных комбинаций. Как показано на рисунке 10-9, мы запускаем два вложенных цикла и на каждом шаге проверяем, равна ли сумма двух целых чисел <code>target</code> ; если да, то возвращаем их индексы.</p>
<p>Рассмотрим прямой перебор всех возможных комбинаций. Как показано на рисунке 10-9, мы запускаем два вложенных цикла и на каждом шаге проверяем, равна ли сумма двух целых чисел <code>target</code>. Если да, то возвращаем их индексы.</p>
<p><img alt="Линейный поиск для задачи о двух суммах" class="animation-figure" src="../replace_linear_by_hashing.assets/two_sum_brute_force.png" /></p>
<p align="center"> Рисунок 10-9 &nbsp; Линейный поиск для задачи о двух суммах </p>
@@ -4576,7 +4576,7 @@
<h2 id="1042-">10.4.2 &nbsp; Хеш-поиск: обмен пространства на время<a class="headerlink" href="#1042-" title="Permanent link">&para;</a></h2>
<p>Рассмотрим вариант с использованием хеш-таблицы, где ключами и значениями будут элементы массива и их индексы. При циклическом обходе массива на каждом шаге выполняются действия, показанные на рисунке 10-10.</p>
<ol>
<li>Проверить, находится ли число <code>target - nums[i]</code> в хеш-таблице; если да, то сразу вернуть индексы этих двух элементов.</li>
<li>Проверить, находится ли число <code>target - nums[i]</code> в хеш-таблице. Если да, то сразу вернуть индексы этих двух элементов.</li>
<li>Добавить в хеш-таблицу пару из ключа <code>nums[i]</code> и индекса <code>i</code> .</li>
</ol>
<div class="tabbed-set tabbed-alternate" data-tabs="2:3"><input checked="checked" id="__tabbed_2_1" name="__tabbed_2" type="radio" /><input id="__tabbed_2_2" name="__tabbed_2" type="radio" /><input id="__tabbed_2_3" name="__tabbed_2" type="radio" /><div class="tabbed-labels"><label for="__tabbed_2_1">&lt;1&gt;</label><label for="__tabbed_2_2">&lt;2&gt;</label><label for="__tabbed_2_3">&lt;3&gt;</label></div>
@@ -4389,17 +4389,17 @@
<h2 id="1051">10.5.1 &nbsp; Полный перебор<a class="headerlink" href="#1051" title="Permanent link">&para;</a></h2>
<p>Полный перебор заключается в том, что мы обходим каждый элемент структуры данных, чтобы найти целевой элемент.</p>
<ul>
<li>"Линейный поиск" применяется к линейным структурам данных, таким как массивы и списки. Он начинается с одного конца структуры данных и последовательно проверяет элементы, пока не найдет целевой элемент или пока не достигнет другого конца структуры данных.</li>
<li>"Обход в ширину" и "обход в глубину" - это две стратегии обхода графов и деревьев. Обход в ширину стартует из начального узла и исследует все узлы текущего уровня, прежде чем переходить к следующему. Обход в глубину стартует из начального узла, проходит один путь до конца, затем возвращается назад и пробует другие пути, пока не будет полностью пройдена вся структура данных.</li>
<li>«Линейный поиск» применяется к линейным структурам данных, таким как массивы и списки. Он начинается с одного конца структуры данных и последовательно проверяет элементы, пока не найдет целевой элемент или пока не достигнет другого конца структуры данных.</li>
<li>«Обход в ширину» и «обход в глубину» - это две стратегии обхода графов и деревьев. Обход в ширину стартует из начального узла и исследует все узлы текущего уровня, прежде чем переходить к следующему. Обход в глубину стартует из начального узла, проходит один путь до конца, затем возвращается назад и пробует другие пути, пока не будет полностью пройдена вся структура данных.</li>
</ul>
<p>Преимущество полного перебора состоит в его простоте и универсальности, <strong>поскольку он не требует предварительной обработки данных и использования дополнительных структур данных</strong>.</p>
<p>Однако <strong>временная сложность таких алгоритмов равна <span class="arithmatex">\(O(n)\)</span></strong> , где <span class="arithmatex">\(n\)</span> - число элементов, поэтому при больших объемах данных их производительность невысока.</p>
<h2 id="1052">10.5.2 &nbsp; Адаптивный поиск<a class="headerlink" href="#1052" title="Permanent link">&para;</a></h2>
<p>Адаптивный поиск использует специфические свойства данных (например, упорядоченность), чтобы оптимизировать процесс поиска и тем самым эффективнее находить целевой элемент.</p>
<ul>
<li>"Двоичный поиск" использует упорядоченность данных для эффективного поиска и применим только к массивам.</li>
<li>"Хеш-поиск" использует хеш-таблицу для построения отображения между поисковыми данными и целевыми данными, благодаря чему запросы выполняются эффективно.</li>
<li>"Поиск в дереве" ведется в конкретной древовидной структуре (например, в двоичном дереве поиска) и позволяет быстро отсекать узлы на основе сравнения значений, чтобы найти цель.</li>
<li>«Двоичный поиск» использует упорядоченность данных для эффективного поиска и применим только к массивам.</li>
<li>«Хеш-поиск» использует хеш-таблицу для построения отображения между поисковыми данными и целевыми данными, благодаря чему запросы выполняются эффективно.</li>
<li>«Поиск в дереве» ведется в конкретной древовидной структуре (например, в двоичном дереве поиска) и позволяет быстро отсекать узлы на основе сравнения значений, чтобы найти цель.</li>
</ul>
<p>Преимущество этих алгоритмов заключается в высокой эффективности: <strong>их временная сложность может достигать <span class="arithmatex">\(O(\log n)\)</span> и даже <span class="arithmatex">\(O(1)\)</span></strong> .</p>
<p>Однако <strong>для использования таких алгоритмов обычно требуется предварительная обработка данных</strong>. Например, для двоичного поиска нужно заранее отсортировать массив, а хеш-поиск и поиск в дереве требуют дополнительных структур данных, поддержание которых тоже отнимает время и память.</p>
@@ -4481,13 +4481,13 @@
</ul>
<p><strong>Двоичный поиск</strong></p>
<ul>
<li>Подходит для больших наборов данных и демонстрирует стабильную эффективность; его худшая временная сложность равна <span class="arithmatex">\(O(\log n)\)</span> .</li>
<li>Подходит для больших наборов данных и демонстрирует стабильную эффективность. Его худшая временная сложность равна <span class="arithmatex">\(O(\log n)\)</span> .</li>
<li>Объем данных не должен быть слишком большим, потому что массив требует непрерывного участка памяти.</li>
<li>Не подходит для сценариев с частыми вставками и удалениями данных, так как поддержание массива в отсортированном виде требует больших затрат.</li>
</ul>
<p><strong>Хеш-поиск</strong></p>
<ul>
<li>Подходит для сценариев, в которых требования к скорости запросов очень высоки; средняя временная сложность равна <span class="arithmatex">\(O(1)\)</span> .</li>
<li>Подходит для сценариев, в которых требования к скорости запросов очень высоки. Средняя временная сложность равна <span class="arithmatex">\(O(1)\)</span> .</li>
<li>Не подходит для сценариев, где требуется упорядоченность данных или поиск по диапазону, потому что хеш-таблица не умеет поддерживать порядок данных.</li>
<li>Сильно зависит от хеш-функции и стратегии обработки коллизий, поэтому риск деградации производительности сравнительно велик.</li>
<li>Не подходит для слишком больших объемов данных, так как хеш-таблице требуется дополнительное пространство, чтобы максимально снизить число коллизий и обеспечить хорошую производительность поиска.</li>
+1 -1
View File
@@ -4341,7 +4341,7 @@
<li>Полный перебор находит данные путем обхода структуры данных. Линейный поиск подходит для массивов и списков, а обход в ширину и обход в глубину подходят для графов и деревьев. Эти алгоритмы универсальны и не требуют предварительной обработки данных, но их временная сложность <span class="arithmatex">\(O(n)\)</span> сравнительно велика.</li>
<li>Хеш-поиск, поиск в дереве и двоичный поиск относятся к эффективным методам поиска и позволяют быстро находить целевой элемент в конкретных структурах данных. Такие алгоритмы обладают высокой эффективностью, их временная сложность может достигать <span class="arithmatex">\(O(\log n)\)</span> и даже <span class="arithmatex">\(O(1)\)</span> , но обычно им нужны дополнительные структуры данных.</li>
<li>На практике нужно анализировать размер данных, требования к производительности поиска, а также частоту запросов и обновлений данных, чтобы выбрать подходящий метод поиска.</li>
<li>Линейный поиск подходит для небольших или часто обновляемых наборов данных; двоичный поиск - для больших отсортированных данных; хеш-поиск - для сценариев с высокими требованиями к скорости запросов и без необходимости поиска по диапазону; поиск в дереве - для больших динамических данных, где нужно поддерживать порядок и выполнять диапазонные запросы.</li>
<li>Линейный поиск подходит для небольших или часто обновляемых наборов данных. Двоичный поиск - для больших отсортированных данных. Хеш-поиск - для сценариев с высокими требованиями к скорости запросов и без необходимости поиска по диапазону. Поиск в дереве - для больших динамических данных, где нужно поддерживать порядок и выполнять диапазонные запросы.</li>
<li>Замена линейного поиска на хеш-поиск - это распространенная стратегия ускорения, которая позволяет снизить временную сложность с <span class="arithmatex">\(O(n)\)</span> до <span class="arithmatex">\(O(1)\)</span> .</li>
</ul>
+9 -9
View File
@@ -4380,7 +4380,7 @@
<!-- Page content -->
<h1 id="113">11.3 &nbsp; Сортировка пузырьком<a class="headerlink" href="#113" title="Permanent link">&para;</a></h1>
<p><u>Сортировка пузырьком (bubble sort)</u> реализует сортировку путем последовательного сравнения и обмена соседних элементов. Этот процесс напоминает всплытие пузырьков снизу вверх, откуда и произошло название алгоритма.</p>
<p>Как показано на рисунке 11-4, процесс "всплытия" можно смоделировать через операцию обмена элементов: начиная от левого края массива и двигаясь вправо, мы последовательно сравниваем соседние элементы и, если "левый элемент &gt; правый элемент", меняем их местами. После завершения прохода максимальный элемент будет перемещен в самый правый конец массива.</p>
<p>Как показано на рисунке 11-4, процесс «всплытия» можно смоделировать через операцию обмена элементов: начиная от левого края массива и двигаясь вправо, мы последовательно сравниваем соседние элементы и, если «левый элемент &gt; правый элемент», меняем их местами. После завершения прохода максимальный элемент будет перемещен в самый правый конец массива.</p>
<div class="tabbed-set tabbed-alternate" data-tabs="1:7"><input checked="checked" id="__tabbed_1_1" name="__tabbed_1" type="radio" /><input id="__tabbed_1_2" name="__tabbed_1" type="radio" /><input id="__tabbed_1_3" name="__tabbed_1" type="radio" /><input id="__tabbed_1_4" name="__tabbed_1" type="radio" /><input id="__tabbed_1_5" name="__tabbed_1" type="radio" /><input id="__tabbed_1_6" name="__tabbed_1" type="radio" /><input id="__tabbed_1_7" name="__tabbed_1" type="radio" /><div class="tabbed-labels"><label for="__tabbed_1_1">&lt;1&gt;</label><label for="__tabbed_1_2">&lt;2&gt;</label><label for="__tabbed_1_3">&lt;3&gt;</label><label for="__tabbed_1_4">&lt;4&gt;</label><label for="__tabbed_1_5">&lt;5&gt;</label><label for="__tabbed_1_6">&lt;6&gt;</label><label for="__tabbed_1_7">&lt;7&gt;</label></div>
<div class="tabbed-content">
<div class="tabbed-block">
@@ -4409,11 +4409,11 @@
<p align="center"> Рисунок 11-4 &nbsp; Моделирование пузырька через обмен элементов </p>
<h2 id="1131">11.3.1 &nbsp; Алгоритм<a class="headerlink" href="#1131" title="Permanent link">&para;</a></h2>
<p>Пусть длина массива равна <span class="arithmatex">\(n\)</span> ; тогда шаги сортировки пузырьком показаны на рисунке 11-5.</p>
<p>Пусть длина массива равна <span class="arithmatex">\(n\)</span>. Тогда шаги сортировки пузырьком показаны на рисунке 11-5.</p>
<ol>
<li>Сначала выполнить один проход "всплытия" по <span class="arithmatex">\(n\)</span> элементам, <strong>переместив максимальный элемент массива на правильную позицию</strong>.</li>
<li>Затем выполнить "всплытие" по оставшимся <span class="arithmatex">\(n - 1\)</span> элементам, <strong>переместив второй по величине элемент на правильную позицию</strong>.</li>
<li>Продолжать по аналогии; после <span class="arithmatex">\(n - 1\)</span> раундов "всплытия" <strong>первые <span class="arithmatex">\(n - 1\)</span> по величине элементы окажутся на правильных позициях</strong>.</li>
<li>Сначала выполнить один проход «всплытия» по <span class="arithmatex">\(n\)</span> элементам, <strong>переместив максимальный элемент массива на правильную позицию</strong>.</li>
<li>Затем выполнить «всплытие» по оставшимся <span class="arithmatex">\(n - 1\)</span> элементам, <strong>переместив второй по величине элемент на правильную позицию</strong>.</li>
<li>Продолжать по аналогии. После <span class="arithmatex">\(n - 1\)</span> раундов «всплытия» <strong>первые <span class="arithmatex">\(n - 1\)</span> по величине элементы окажутся на правильных позициях</strong>.</li>
<li>Оставшийся единственный элемент обязательно является минимальным, сортировать его уже не нужно, поэтому сортировка завершена.</li>
</ol>
<p><img alt="Процесс сортировки пузырьком" class="animation-figure" src="../bubble_sort.assets/bubble_sort_overview.png" /></p>
@@ -4648,8 +4648,8 @@
<div style="margin-top: 5px;"><a href="https://pythontutor.com/iframe-embed.html#code=def%20bubble_sort%28nums%3A%20list%5Bint%5D%29%3A%0A%20%20%20%20%22%22%22%D0%9F%D1%83%D0%B7%D1%8B%D1%80%D1%8C%D0%BA%D0%BE%D0%B2%D0%B0%D1%8F%20%D1%81%D0%BE%D1%80%D1%82%D0%B8%D1%80%D0%BE%D0%B2%D0%BA%D0%B0%22%22%22%0A%20%20%20%20n%20%3D%20len%28nums%29%0A%20%20%20%20%23%20%D0%92%D0%BD%D0%B5%D1%88%D0%BD%D0%B8%D0%B9%20%D1%86%D0%B8%D0%BA%D0%BB%3A%20%D0%BD%D0%B5%D0%BE%D1%82%D1%81%D0%BE%D1%80%D1%82%D0%B8%D1%80%D0%BE%D0%B2%D0%B0%D0%BD%D0%BD%D1%8B%D0%B9%20%D0%B4%D0%B8%D0%B0%D0%BF%D0%B0%D0%B7%D0%BE%D0%BD%20%5B0%2C%20i%5D%0A%20%20%20%20for%20i%20in%20range%28n%20-%201%2C%200%2C%20-1%29%3A%0A%20%20%20%20%20%20%20%20%23%20%D0%92%D0%BD%D1%83%D1%82%D1%80%D0%B5%D0%BD%D0%BD%D0%B8%D0%B9%20%D1%86%D0%B8%D0%BA%D0%BB%3A%20%D0%BF%D0%B5%D1%80%D0%B5%D0%BC%D0%B5%D1%81%D1%82%D0%B8%D1%82%D1%8C%20%D0%BC%D0%B0%D0%BA%D1%81%D0%B8%D0%BC%D0%B0%D0%BB%D1%8C%D0%BD%D1%8B%D0%B9%20%D1%8D%D0%BB%D0%B5%D0%BC%D0%B5%D0%BD%D1%82%20%D0%BD%D0%B5%D0%BE%D1%82%D1%81%D0%BE%D1%80%D1%82%D0%B8%D1%80%D0%BE%D0%B2%D0%B0%D0%BD%D0%BD%D0%BE%D0%B3%D0%BE%20%D0%B4%D0%B8%D0%B0%D0%BF%D0%B0%D0%B7%D0%BE%D0%BD%D0%B0%20%5B0%2C%20i%5D%20%D0%B2%20%D0%B5%D0%B3%D0%BE%20%D0%BF%D1%80%D0%B0%D0%B2%D1%8B%D0%B9%20%D0%BA%D0%BE%D0%BD%D0%B5%D1%86%0A%20%20%20%20%20%20%20%20for%20j%20in%20range%28i%29%3A%0A%20%20%20%20%20%20%20%20%20%20%20%20if%20nums%5Bj%5D%20%3E%20nums%5Bj%20%2B%201%5D%3A%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%23%20%D0%9F%D0%BE%D0%BC%D0%B5%D0%BD%D1%8F%D1%82%D1%8C%20%D0%BC%D0%B5%D1%81%D1%82%D0%B0%D0%BC%D0%B8%20nums%5Bj%5D%20%D0%B8%20nums%5Bj%20%2B%201%5D%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20nums%5Bj%5D%2C%20nums%5Bj%20%2B%201%5D%20%3D%20nums%5Bj%20%2B%201%5D%2C%20nums%5Bj%5D%0A%0A%22%22%22Driver%20Code%22%22%22%0Aif%20__name__%20%3D%3D%20%22__main__%22%3A%0A%20%20%20%20nums%20%3D%20%5B4%2C%201%2C%203%2C%201%2C%205%2C%202%5D%0A%20%20%20%20bubble_sort%28nums%29%0A%20%20%20%20print%28%22%D0%9F%D0%BE%D1%81%D0%BB%D0%B5%20%D0%BF%D1%83%D0%B7%D1%8B%D1%80%D1%8C%D0%BA%D0%BE%D0%B2%D0%BE%D0%B9%20%D1%81%D0%BE%D1%80%D1%82%D0%B8%D1%80%D0%BE%D0%B2%D0%BA%D0%B8%20nums%20%3D%22%2C%20nums%29&codeDivHeight=800&codeDivWidth=600&cumulative=false&curInstr=4&heapPrimitives=nevernest&origin=opt-frontend.js&py=311&rawInputLstJSON=%5B%5D&textReferences=false" target="_blank" rel="noopener noreferrer">Во весь экран &gt;</a></div></p>
</details>
<h2 id="1132">11.3.2 &nbsp; Оптимизация эффективности<a class="headerlink" href="#1132" title="Permanent link">&para;</a></h2>
<p>Если в каком-либо раунде "всплытия" не произошло ни одного обмена, значит, массив уже отсортирован и можно сразу вернуть результат. Поэтому можно добавить флаг <code>flag</code> для отслеживания этой ситуации и немедленного выхода.</p>
<p>После такой оптимизации худшая и средняя временные сложности сортировки пузырьком по-прежнему равны <span class="arithmatex">\(O(n^2)\)</span> ; однако если входной массив уже полностью упорядочен, достигается лучшая временная сложность <span class="arithmatex">\(O(n)\)</span> .</p>
<p>Если в каком-либо раунде «всплытия» не произошло ни одного обмена, значит, массив уже отсортирован и можно сразу вернуть результат. Поэтому можно добавить флаг <code>flag</code> для отслеживания этой ситуации и немедленного выхода.</p>
<p>После такой оптимизации худшая и средняя временные сложности сортировки пузырьком по-прежнему равны <span class="arithmatex">\(O(n^2)\)</span>. Однако если входной массив уже полностью упорядочен, достигается лучшая временная сложность <span class="arithmatex">\(O(n)\)</span> .</p>
<div class="tabbed-set tabbed-alternate" data-tabs="3:13"><input checked="checked" id="__tabbed_3_1" name="__tabbed_3" type="radio" /><input id="__tabbed_3_2" name="__tabbed_3" type="radio" /><input id="__tabbed_3_3" name="__tabbed_3" type="radio" /><input id="__tabbed_3_4" name="__tabbed_3" type="radio" /><input id="__tabbed_3_5" name="__tabbed_3" type="radio" /><input id="__tabbed_3_6" name="__tabbed_3" type="radio" /><input id="__tabbed_3_7" name="__tabbed_3" type="radio" /><input id="__tabbed_3_8" name="__tabbed_3" type="radio" /><input id="__tabbed_3_9" name="__tabbed_3" type="radio" /><input id="__tabbed_3_10" name="__tabbed_3" type="radio" /><input id="__tabbed_3_11" name="__tabbed_3" type="radio" /><input id="__tabbed_3_12" name="__tabbed_3" type="radio" /><input id="__tabbed_3_13" name="__tabbed_3" type="radio" /><div class="tabbed-labels"><label for="__tabbed_3_1">Python</label><label for="__tabbed_3_2">C++</label><label for="__tabbed_3_3">Java</label><label for="__tabbed_3_4">C#</label><label for="__tabbed_3_5">Go</label><label for="__tabbed_3_6">Swift</label><label for="__tabbed_3_7">JS</label><label for="__tabbed_3_8">TS</label><label for="__tabbed_3_9">Dart</label><label for="__tabbed_3_10">Rust</label><label for="__tabbed_3_11">C</label><label for="__tabbed_3_12">Kotlin</label><label for="__tabbed_3_13">Ruby</label></div>
<div class="tabbed-content">
<div class="tabbed-block">
@@ -4944,9 +4944,9 @@
</details>
<h2 id="1133">11.3.3 &nbsp; Характеристики алгоритма<a class="headerlink" href="#1133" title="Permanent link">&para;</a></h2>
<ul>
<li><strong>Временная сложность равна <span class="arithmatex">\(O(n^2)\)</span>, алгоритм адаптивен</strong>: длины диапазонов, проходящих "всплытие" в разных раундах, последовательно равны <span class="arithmatex">\(n - 1\)</span>, <span class="arithmatex">\(n - 2\)</span>, <span class="arithmatex">\(\dots\)</span>, <span class="arithmatex">\(2\)</span>, <span class="arithmatex">\(1\)</span> , а их сумма равна <span class="arithmatex">\((n - 1) n / 2\)</span> . После добавления оптимизации с <code>flag</code> лучшая временная сложность может достигать <span class="arithmatex">\(O(n)\)</span> .</li>
<li><strong>Временная сложность равна <span class="arithmatex">\(O(n^2)\)</span>, алгоритм адаптивен</strong>: длины диапазонов, проходящих «всплытие» в разных раундах, последовательно равны <span class="arithmatex">\(n - 1\)</span>, <span class="arithmatex">\(n - 2\)</span>, <span class="arithmatex">\(\dots\)</span>, <span class="arithmatex">\(2\)</span>, <span class="arithmatex">\(1\)</span> , а их сумма равна <span class="arithmatex">\((n - 1) n / 2\)</span> . После добавления оптимизации с <code>flag</code> лучшая временная сложность может достигать <span class="arithmatex">\(O(n)\)</span> .</li>
<li><strong>Пространственная сложность равна <span class="arithmatex">\(O(1)\)</span>, сортировка выполняется на месте</strong>: указатели <span class="arithmatex">\(i\)</span> и <span class="arithmatex">\(j\)</span> используют константный объем дополнительной памяти.</li>
<li><strong>Стабильная сортировка</strong>: поскольку при "всплытии" равные элементы не обмениваются местами.</li>
<li><strong>Стабильная сортировка</strong>: поскольку при «всплытии» равные элементы не обмениваются местами.</li>
</ul>
<!-- Source file information -->
+4 -4
View File
@@ -4379,8 +4379,8 @@
<!-- Page content -->
<h1 id="118">11.8 &nbsp; Блочная сортировка<a class="headerlink" href="#118" title="Permanent link">&para;</a></h1>
<p>Рассмотренные выше алгоритмы сортировки относятся к "сортировкам на основе сравнений": они упорядочивают данные, сравнивая элементы друг с другом. Временная сложность таких алгоритмов не может быть лучше <span class="arithmatex">\(O(n \log n)\)</span> . Далее мы рассмотрим несколько "сортировок без сравнений", чья временная сложность может достигать линейного порядка.</p>
<p><u>Блочная сортировка (bucket sort)</u> является типичным применением стратегии "разделяй и властвуй". Она создает набор упорядоченных по величине блоков, где каждый блок соответствует определенному диапазону данных; затем элементы равномерно распределяются по этим блокам, внутри каждого блока отдельно выполняется сортировка, а в конце результаты объединяются в порядке блоков.</p>
<p>Рассмотренные выше алгоритмы сортировки относятся к «сортировкам на основе сравнений»: они упорядочивают данные, сравнивая элементы друг с другом. Временная сложность таких алгоритмов не может быть лучше <span class="arithmatex">\(O(n \log n)\)</span> . Далее мы рассмотрим несколько «сортировок без сравнений», чья временная сложность может достигать линейного порядка.</p>
<p><u>Блочная сортировка (bucket sort)</u> является типичным применением стратегии «разделяй и властвуй». Она создает набор упорядоченных по величине блоков, где каждый блок соответствует определенному диапазону данных. Затем элементы равномерно распределяются по этим блокам, внутри каждого блока отдельно выполняется сортировка, а в конце результаты объединяются в порядке блоков.</p>
<h2 id="1181">11.8.1 &nbsp; Алгоритм<a class="headerlink" href="#1181" title="Permanent link">&para;</a></h2>
<p>Рассмотрим массив длины <span class="arithmatex">\(n\)</span>, элементы которого являются числами с плавающей запятой из диапазона <span class="arithmatex">\([0, 1)\)</span> . Процесс блочной сортировки показан на рисунке 11-13.</p>
<ol>
@@ -4798,9 +4798,9 @@
<li>Является ли блочная сортировка стабильной, зависит от того, стабилен ли алгоритм сортировки внутри каждого блока.</li>
</ul>
<h2 id="1183">11.8.3 &nbsp; Как добиться равномерного распределения<a class="headerlink" href="#1183" title="Permanent link">&para;</a></h2>
<p>Теоретически временная сложность блочной сортировки может достигать <span class="arithmatex">\(O(n)\)</span> ; <strong>ключ к этому - как можно более равномерно распределить элементы по блокам</strong>. На практике данные часто распределены неравномерно. Например, если нужно распределить все товары на маркетплейсе по 10 ценовым блокам, количество товаров дешевле 100 рублей может быть очень большим, а товаров дороже 1000 рублей - очень маленьким. Если просто разбить диапазон цен на 10 равных частей, число товаров в каждом блоке будет сильно различаться.</p>
<p>Теоретически временная сложность блочной сортировки может достигать <span class="arithmatex">\(O(n)\)</span>. <strong>Ключ к этому - как можно более равномерно распределить элементы по блокам</strong>. На практике данные часто распределены неравномерно. Например, если нужно распределить все товары на маркетплейсе по 10 ценовым блокам, количество товаров дешевле 100 рублей может быть очень большим, а товаров дороже 1000 рублей - очень маленьким. Если просто разбить диапазон цен на 10 равных частей, число товаров в каждом блоке будет сильно различаться.</p>
<p>Чтобы добиться более равномерного распределения, можно сначала задать грубую линию раздела и приблизительно распределить данные по 3 блокам. <strong>После этого блоки с большим числом товаров можно снова делить на 3 блока и продолжать процесс до тех пор, пока число элементов в каждом блоке не станет примерно одинаковым</strong>.</p>
<p>Как показано на рисунке 11-14, по сути этот метод строит рекурсивное дерево, цель которого - сделать значения в листьях как можно более равномерными. Конечно, совсем не обязательно каждый раз делить данные именно на 3 блока; конкретную схему разбиения можно выбирать в зависимости от свойств данных.</p>
<p>Как показано на рисунке 11-14, по сути этот метод строит рекурсивное дерево, цель которого - сделать значения в листьях как можно более равномерными. Конечно, совсем не обязательно каждый раз делить данные именно на 3 блока. Конкретную схему разбиения можно выбирать в зависимости от свойств данных.</p>
<p><img alt="Рекурсивное разбиение по блокам" class="animation-figure" src="../bucket_sort.assets/scatter_in_buckets_recursively.png" /></p>
<p align="center"> Рисунок 11-14 &nbsp; Рекурсивное разбиение по блокам </p>
+5 -5
View File
@@ -4403,10 +4403,10 @@
<h1 id="119">11.9 &nbsp; Сортировка подсчетом<a class="headerlink" href="#119" title="Permanent link">&para;</a></h1>
<p><u>Сортировка подсчетом (counting sort)</u> реализует сортировку за счет подсчета количества вхождений элементов и обычно используется для массивов целых чисел.</p>
<h2 id="1191">11.9.1 &nbsp; Простая реализация<a class="headerlink" href="#1191" title="Permanent link">&para;</a></h2>
<p>Сначала рассмотрим простой пример. Дан массив <code>nums</code> длины <span class="arithmatex">\(n\)</span> , элементы которого являются "неотрицательными целыми числами". Общий процесс сортировки подсчетом показан на рисунке 11-16.</p>
<p>Сначала рассмотрим простой пример. Дан массив <code>nums</code> длины <span class="arithmatex">\(n\)</span> , элементы которого являются «неотрицательными целыми числами». Общий процесс сортировки подсчетом показан на рисунке 11-16.</p>
<ol>
<li>Пройти по массиву, найти в нем максимальное число, обозначить его как <span class="arithmatex">\(m\)</span> , а затем создать вспомогательный массив <code>counter</code> длины <span class="arithmatex">\(m + 1\)</span> .</li>
<li><strong>С помощью <code>counter</code> подсчитать, сколько раз каждое число встречается в <code>nums</code></strong>; при этом <code>counter[num]</code> хранит число вхождений значения <code>num</code> . Делается это просто: достаточно пройти по <code>nums</code> (пусть текущее число равно <code>num</code> ) и на каждом шаге увеличить <code>counter[num]</code> на <span class="arithmatex">\(1\)</span> .</li>
<li><strong>С помощью <code>counter</code> подсчитать, сколько раз каждое число встречается в <code>nums</code></strong>. При этом <code>counter[num]</code> хранит число вхождений значения <code>num</code> . Делается это просто: достаточно пройти по <code>nums</code> (пусть текущее число равно <code>num</code> ) и на каждом шаге увеличить <code>counter[num]</code> на <span class="arithmatex">\(1\)</span> .</li>
<li><strong>Поскольку индексы массива <code>counter</code> изначально упорядочены, можно считать, что все числа уже отсортированы</strong>. Далее остается пройти по <code>counter</code> и в соответствии с числом вхождений записать значения обратно в <code>nums</code> в порядке возрастания.</li>
</ol>
<p><img alt="Процесс сортировки подсчетом" class="animation-figure" src="../counting_sort.assets/counting_sort_overview.png" /></p>
@@ -4743,7 +4743,7 @@
</div>
<h2 id="1192">11.9.2 &nbsp; Полная реализация<a class="headerlink" href="#1192" title="Permanent link">&para;</a></h2>
<p>Внимательный читатель мог заметить, что <strong>если входные данные представлены объектами, то описанный выше шаг <code>3.</code> перестает работать</strong>. Например, если входными данными являются объекты товаров и мы хотим отсортировать их по цене (полю класса), то описанный алгоритм сможет выдать только отсортированный ряд цен, но не исходные объекты в нужном порядке.</p>
<p>Как же получить корректный порядок исходных данных? Сначала вычислим "префиксную сумму" массива <code>counter</code> . Как следует из названия, префиксная сумма в индексе <code>i</code> , обозначаемая как <code>prefix[i]</code> , равна сумме первых <code>i</code> элементов массива:</p>
<p>Как же получить корректный порядок исходных данных? Сначала вычислим «префиксную сумму» массива <code>counter</code> . Как следует из названия, префиксная сумма в индексе <code>i</code> , обозначаемая как <code>prefix[i]</code> , равна сумме первых <code>i</code> элементов массива:</p>
<div class="arithmatex">\[
\text{prefix}[i] = \sum_{j=0}^i \text{counter[j]}
\]</div>
@@ -4752,7 +4752,7 @@
<li>Записать <code>num</code> в массив <code>res</code> по индексу <code>prefix[num] - 1</code> .</li>
<li>Уменьшить префиксную сумму <code>prefix[num]</code> на <span class="arithmatex">\(1\)</span> , чтобы получить индекс следующего размещения элемента <code>num</code> .</li>
</ol>
<p>После завершения прохода массив <code>res</code> будет содержать отсортированный результат; остается только переписать <code>res</code> обратно в <code>nums</code> . Полный процесс сортировки подсчетом показан на рисунке 11-17.</p>
<p>После завершения прохода массив <code>res</code> будет содержать отсортированный результат. Остается только переписать <code>res</code> обратно в <code>nums</code> . Полный процесс сортировки подсчетом показан на рисунке 11-17.</p>
<div class="tabbed-set tabbed-alternate" data-tabs="2:8"><input checked="checked" id="__tabbed_2_1" name="__tabbed_2" type="radio" /><input id="__tabbed_2_2" name="__tabbed_2" type="radio" /><input id="__tabbed_2_3" name="__tabbed_2" type="radio" /><input id="__tabbed_2_4" name="__tabbed_2" type="radio" /><input id="__tabbed_2_5" name="__tabbed_2" type="radio" /><input id="__tabbed_2_6" name="__tabbed_2" type="radio" /><input id="__tabbed_2_7" name="__tabbed_2" type="radio" /><input id="__tabbed_2_8" name="__tabbed_2" type="radio" /><div class="tabbed-labels"><label for="__tabbed_2_1">&lt;1&gt;</label><label for="__tabbed_2_2">&lt;2&gt;</label><label for="__tabbed_2_3">&lt;3&gt;</label><label for="__tabbed_2_4">&lt;4&gt;</label><label for="__tabbed_2_5">&lt;5&gt;</label><label for="__tabbed_2_6">&lt;6&gt;</label><label for="__tabbed_2_7">&lt;7&gt;</label><label for="__tabbed_2_8">&lt;8&gt;</label></div>
<div class="tabbed-content">
<div class="tabbed-block">
@@ -5233,7 +5233,7 @@
<ul>
<li><strong>Временная сложность равна <span class="arithmatex">\(O(n + m)\)</span>, алгоритм не является адаптивным</strong> : необходимо пройти по <code>nums</code> и по <code>counter</code> , а оба этих прохода занимают линейное время. Обычно выполняется <span class="arithmatex">\(n \gg m\)</span> , поэтому временная сложность стремится к <span class="arithmatex">\(O(n)\)</span> .</li>
<li><strong>Пространственная сложность равна <span class="arithmatex">\(O(n + m)\)</span>, сортировка не выполняется на месте</strong>: используются массивы <code>res</code> и <code>counter</code> длины <span class="arithmatex">\(n\)</span> и <span class="arithmatex">\(m\)</span> соответственно.</li>
<li><strong>Стабильная сортировка</strong>: порядок заполнения <code>res</code> идет "справа налево", поэтому обратный проход по <code>nums</code> позволяет сохранить относительный порядок равных элементов и тем самым реализовать стабильную сортировку. Вообще говоря, прямой проход по <code>nums</code> тоже даст правильный результат сортировки, но он будет нестабильным.</li>
<li><strong>Стабильная сортировка</strong>: порядок заполнения <code>res</code> идет «справа налево», поэтому обратный проход по <code>nums</code> позволяет сохранить относительный порядок равных элементов и тем самым реализовать стабильную сортировку. Вообще говоря, прямой проход по <code>nums</code> тоже даст правильный результат сортировки, но он будет нестабильным.</li>
</ul>
<h2 id="1194">11.9.4 &nbsp; Ограничения<a class="headerlink" href="#1194" title="Permanent link">&para;</a></h2>
<p>На этом этапе сортировка подсчетом может показаться очень изящной: она позволяет эффективно сортировать данные, опираясь только на подсчет числа вхождений. Однако условия ее применения довольно строгие.</p>
+5 -5
View File
@@ -4359,16 +4359,16 @@
<h1 id="117">11.7 &nbsp; Пирамидальная сортировка<a class="headerlink" href="#117" title="Permanent link">&para;</a></h1>
<div class="admonition tip">
<p class="admonition-title">Tip</p>
<p>Перед чтением этого раздела убедитесь, что вы уже изучили главу "Куча".</p>
<p>Перед чтением этого раздела убедитесь, что вы уже изучили главу «Куча».</p>
</div>
<p><u>Пирамидальная сортировка (heap sort)</u> - это эффективный алгоритм сортировки, основанный на структуре данных "куча". Для его реализации можно использовать уже изученные нами "построение кучи" и "извлечение элементов из кучи".</p>
<p><u>Пирамидальная сортировка (heap sort)</u> - это эффективный алгоритм сортировки, основанный на структуре данных «куча». Для его реализации можно использовать уже изученные нами «построение кучи» и «извлечение элементов из кучи».</p>
<ol>
<li>Подать на вход массив и построить из него мин-кучу; в этот момент минимальный элемент будет находиться в вершине кучи.</li>
<li>Подать на вход массив и построить из него мин-кучу. В этот момент минимальный элемент будет находиться в вершине кучи.</li>
<li>Непрерывно выполнять извлечение из кучи и по порядку записывать извлеченные элементы - так получится последовательность, отсортированная по возрастанию.</li>
</ol>
<p>Хотя этот метод и работоспособен, он требует дополнительного массива для хранения извлеченных элементов и потому расходует лишнюю память. На практике обычно используют более изящную реализацию.</p>
<h2 id="1171">11.7.1 &nbsp; Алгоритм<a class="headerlink" href="#1171" title="Permanent link">&para;</a></h2>
<p>Пусть длина массива равна <span class="arithmatex">\(n\)</span> ; тогда процесс пирамидальной сортировки показан на рисунке 11-12.</p>
<p>Пусть длина массива равна <span class="arithmatex">\(n\)</span>. Тогда процесс пирамидальной сортировки показан на рисунке 11-12.</p>
<ol>
<li>Подать на вход массив и построить из него макс-кучу. После этого максимальный элемент окажется в вершине кучи.</li>
<li>Обменять элемент в вершине кучи (первый элемент) с элементом внизу кучи (последний элемент). После обмена длина кучи уменьшается на <span class="arithmatex">\(1\)</span> , а число уже отсортированных элементов увеличивается на <span class="arithmatex">\(1\)</span> .</li>
@@ -4421,7 +4421,7 @@
</div>
<p align="center"> Рисунок 11-12 &nbsp; Шаги пирамидальной сортировки </p>
<p>В коде используется та же функция просеивания сверху вниз <code>sift_down()</code>, что и в главе "Куча". Важно помнить, что длина кучи уменьшается по мере извлечения максимального элемента, поэтому функции <code>sift_down()</code> нужно передавать параметр длины <span class="arithmatex">\(n\)</span> , чтобы указать текущую действительную длину кучи. Код приведен ниже:</p>
<p>В коде используется та же функция просеивания сверху вниз <code>sift_down()</code>, что и в главе «Куча». Важно помнить, что длина кучи уменьшается по мере извлечения максимального элемента, поэтому функции <code>sift_down()</code> нужно передавать параметр длины <span class="arithmatex">\(n\)</span> , чтобы указать текущую действительную длину кучи. Код приведен ниже:</p>
<div class="tabbed-set tabbed-alternate" data-tabs="2:13"><input checked="checked" id="__tabbed_2_1" name="__tabbed_2" type="radio" /><input id="__tabbed_2_2" name="__tabbed_2" type="radio" /><input id="__tabbed_2_3" name="__tabbed_2" type="radio" /><input id="__tabbed_2_4" name="__tabbed_2" type="radio" /><input id="__tabbed_2_5" name="__tabbed_2" type="radio" /><input id="__tabbed_2_6" name="__tabbed_2" type="radio" /><input id="__tabbed_2_7" name="__tabbed_2" type="radio" /><input id="__tabbed_2_8" name="__tabbed_2" type="radio" /><input id="__tabbed_2_9" name="__tabbed_2" type="radio" /><input id="__tabbed_2_10" name="__tabbed_2" type="radio" /><input id="__tabbed_2_11" name="__tabbed_2" type="radio" /><input id="__tabbed_2_12" name="__tabbed_2" type="radio" /><input id="__tabbed_2_13" name="__tabbed_2" type="radio" /><div class="tabbed-labels"><label for="__tabbed_2_1">Python</label><label for="__tabbed_2_2">C++</label><label for="__tabbed_2_3">Java</label><label for="__tabbed_2_4">C#</label><label for="__tabbed_2_5">Go</label><label for="__tabbed_2_6">Swift</label><label for="__tabbed_2_7">JS</label><label for="__tabbed_2_8">TS</label><label for="__tabbed_2_9">Dart</label><label for="__tabbed_2_10">Rust</label><label for="__tabbed_2_11">C</label><label for="__tabbed_2_12">Kotlin</label><label for="__tabbed_2_13">Ruby</label></div>
<div class="tabbed-content">
<div class="tabbed-block">
+7 -7
View File
@@ -4381,7 +4381,7 @@
<h1 id="114">11.4 &nbsp; Сортировка вставками<a class="headerlink" href="#114" title="Permanent link">&para;</a></h1>
<p><u>Сортировка вставками (insertion sort)</u> - это простой алгоритм сортировки, принцип которого очень похож на ручную сортировку карт в колоде.</p>
<p>Точнее говоря, в неотсортированном диапазоне выбирается опорный элемент, после чего он сравнивается с элементами слева в уже отсортированном диапазоне и вставляется в правильную позицию.</p>
<p>На рисунке 11-6 показан процесс вставки элемента в массив. Пусть опорный элемент обозначен как <code>base</code> ; нам нужно сдвинуть все элементы от целевого индекса до <code>base</code> на одну позицию вправо, а затем записать <code>base</code> в целевой индекс.</p>
<p>На рисунке 11-6 показан процесс вставки элемента в массив. Пусть опорный элемент обозначен как <code>base</code>. Нам нужно сдвинуть все элементы от целевого индекса до <code>base</code> на одну позицию вправо, а затем записать <code>base</code> в целевой индекс.</p>
<p><img alt="Одна операция вставки" class="animation-figure" src="../insertion_sort.assets/insertion_operation.png" /></p>
<p align="center"> Рисунок 11-6 &nbsp; Одна операция вставки </p>
@@ -4389,9 +4389,9 @@
<p>Общий процесс сортировки вставками показан на рисунке 11-7.</p>
<ol>
<li>В начальном состоянии отсортирован только первый элемент массива.</li>
<li>Выбрать второй элемент массива как <code>base</code> ; после вставки в правильную позицию <strong>первые два элемента массива окажутся отсортированными</strong>.</li>
<li>Выбрать третий элемент как <code>base</code> ; после вставки в правильную позицию <strong>первые три элемента массива окажутся отсортированными</strong>.</li>
<li>Продолжать по аналогии; в последнем раунде в качестве <code>base</code> берется последний элемент, и после его вставки <strong>все элементы массива будут отсортированы</strong>.</li>
<li>Выбрать второй элемент массива как <code>base</code>. После вставки в правильную позицию <strong>первые два элемента массива окажутся отсортированными</strong>.</li>
<li>Выбрать третий элемент как <code>base</code>. После вставки в правильную позицию <strong>первые три элемента массива окажутся отсортированными</strong>.</li>
<li>Продолжать по аналогии. В последнем раунде в качестве <code>base</code> берется последний элемент, и после его вставки <strong>все элементы массива будут отсортированы</strong>.</li>
</ol>
<p><img alt="Процесс сортировки вставками" class="animation-figure" src="../insertion_sort.assets/insertion_sort_overview.png" /></p>
<p align="center"> Рисунок 11-7 &nbsp; Процесс сортировки вставками </p>
@@ -4629,11 +4629,11 @@
</ul>
<h2 id="1143">11.4.3 &nbsp; Преимущества сортировки вставками<a class="headerlink" href="#1143" title="Permanent link">&para;</a></h2>
<p>Временная сложность сортировки вставками равна <span class="arithmatex">\(O(n^2)\)</span> , а у быстрой сортировки, которую мы скоро изучим, временная сложность равна <span class="arithmatex">\(O(n \log n)\)</span> . Несмотря на более высокую асимптотическую сложность, <strong>на малых объемах данных сортировка вставками обычно работает быстрее</strong>.</p>
<p>Этот вывод похож на сравнение линейного и двоичного поиска. Алгоритмы уровня <span class="arithmatex">\(O(n \log n)\)</span> , такие как быстрая сортировка, относятся к алгоритмам на основе стратегии "разделяй и властвуй" и обычно включают больше элементарных вычислений. Когда объем данных мал, значения <span class="arithmatex">\(n^2\)</span> и <span class="arithmatex">\(n \log n\)</span> близки друг к другу, поэтому асимптотика не доминирует, а решающим становится число элементарных операций в каждом раунде.</p>
<p>На практике встроенные функции сортировки во многих языках программирования (например, в Java) используют сортировку вставками. Общая идея такова: для длинных массивов применять алгоритмы сортировки на основе стратегии "разделяй и властвуй", например быструю сортировку; для коротких массивов сразу использовать сортировку вставками.</p>
<p>Этот вывод похож на сравнение линейного и двоичного поиска. Алгоритмы уровня <span class="arithmatex">\(O(n \log n)\)</span> , такие как быстрая сортировка, относятся к алгоритмам на основе стратегии «разделяй и властвуй» и обычно включают больше элементарных вычислений. Когда объем данных мал, значения <span class="arithmatex">\(n^2\)</span> и <span class="arithmatex">\(n \log n\)</span> близки друг к другу, поэтому асимптотика не доминирует, а решающим становится число элементарных операций в каждом раунде.</p>
<p>На практике встроенные функции сортировки во многих языках программирования (например, в Java) используют сортировку вставками. Общая идея такова: для длинных массивов применять алгоритмы сортировки на основе стратегии «разделяй и властвуй», например быструю сортировку. Для коротких массивов сразу использовать сортировку вставками.</p>
<p>Хотя сортировка пузырьком, выбором и вставками имеют одинаковую временную сложность <span class="arithmatex">\(O(n^2)\)</span> , в реальных задачах <strong>сортировка вставками используется заметно чаще, чем сортировка пузырьком и сортировка выбором</strong>. Основные причины таковы.</p>
<ul>
<li>Сортировка пузырьком основана на обмене элементов, для чего нужна временная переменная и суммарно выполняются 3 элементарные операции; сортировка вставками основана на присваивании элементов и требует всего 1 элементарной операции. Поэтому <strong>вычислительные затраты сортировки пузырьком обычно выше, чем у сортировки вставками</strong>.</li>
<li>Сортировка пузырьком основана на обмене элементов, для чего нужна временная переменная и суммарно выполняются 3 элементарные операции. Сортировка вставками основана на присваивании элементов и требует всего 1 элементарной операции. Поэтому <strong>вычислительные затраты сортировки пузырьком обычно выше, чем у сортировки вставками</strong>.</li>
<li>Временная сложность сортировки выбором в любом случае равна <span class="arithmatex">\(O(n^2)\)</span> . <strong>Если входные данные уже частично упорядочены, сортировка вставками обычно эффективнее сортировки выбором</strong>.</li>
<li>Сортировка выбором нестабильна, поэтому ее нельзя использовать для многоуровневой сортировки.</li>
</ul>
+6 -6
View File
@@ -4379,21 +4379,21 @@
<!-- Page content -->
<h1 id="116">11.6 &nbsp; Сортировка слиянием<a class="headerlink" href="#116" title="Permanent link">&para;</a></h1>
<p><u>Сортировка слиянием (merge sort)</u> - это алгоритм сортировки, основанный на стратегии "разделяй и властвуй", который включает этапы "разделения" и "слияния", показанные на рисунке 11-10.</p>
<p><u>Сортировка слиянием (merge sort)</u> - это алгоритм сортировки, основанный на стратегии «разделяй и властвуй», который включает этапы «разделения» и «слияния», показанные на рисунке 11-10.</p>
<ol>
<li><strong>Этап разделения</strong>: массив рекурсивно делится пополам, и задача сортировки длинного массива превращается в задачи сортировки более коротких массивов.</li>
<li><strong>Этап слияния</strong>: когда длина подмассива становится равной 1, разделение завершается и начинается слияние; два коротких упорядоченных массива непрерывно объединяются в один более длинный упорядоченный массив, пока процесс не завершится.</li>
<li><strong>Этап слияния</strong>: когда длина подмассива становится равной 1, разделение завершается и начинается слияние. Два коротких упорядоченных массива непрерывно объединяются в один более длинный упорядоченный массив, пока процесс не завершится.</li>
</ol>
<p><img alt="Этапы разделения и слияния в сортировке слиянием" class="animation-figure" src="../merge_sort.assets/merge_sort_overview.png" /></p>
<p align="center"> Рисунок 11-10 &nbsp; Этапы разделения и слияния в сортировке слиянием </p>
<h2 id="1161">11.6.1 &nbsp; Алгоритм<a class="headerlink" href="#1161" title="Permanent link">&para;</a></h2>
<p>Как показано на рисунке 11-11, на этапе "разделения" массив рекурсивно разбивается сверху вниз по середине на два подмассива.</p>
<p>Как показано на рисунке 11-11, на этапе «разделения» массив рекурсивно разбивается сверху вниз по середине на два подмассива.</p>
<ol>
<li>Вычислить середину массива <code>mid</code> и рекурсивно разделить левый подмассив (интервал <code>[left, mid]</code> ) и правый подмассив (интервал <code>[mid + 1, right]</code> ).</li>
<li>Рекурсивно повторять шаг <code>1.</code> , пока длина подмассива не станет равной 1.</li>
</ol>
<p>Этап "слияния" снизу вверх объединяет левый и правый подмассивы в один упорядоченный массив. Следует заметить, что начиная с подмассивов длины 1, каждый подмассив в фазе слияния уже является упорядоченным.</p>
<p>Этап «слияния» снизу вверх объединяет левый и правый подмассивы в один упорядоченный массив. Следует заметить, что начиная с подмассивов длины 1, каждый подмассив в фазе слияния уже является упорядоченным.</p>
<div class="tabbed-set tabbed-alternate" data-tabs="1:10"><input checked="checked" id="__tabbed_1_1" name="__tabbed_1" type="radio" /><input id="__tabbed_1_2" name="__tabbed_1" type="radio" /><input id="__tabbed_1_3" name="__tabbed_1" type="radio" /><input id="__tabbed_1_4" name="__tabbed_1" type="radio" /><input id="__tabbed_1_5" name="__tabbed_1" type="radio" /><input id="__tabbed_1_6" name="__tabbed_1" type="radio" /><input id="__tabbed_1_7" name="__tabbed_1" type="radio" /><input id="__tabbed_1_8" name="__tabbed_1" type="radio" /><input id="__tabbed_1_9" name="__tabbed_1" type="radio" /><input id="__tabbed_1_10" name="__tabbed_1" type="radio" /><div class="tabbed-labels"><label for="__tabbed_1_1">&lt;1&gt;</label><label for="__tabbed_1_2">&lt;2&gt;</label><label for="__tabbed_1_3">&lt;3&gt;</label><label for="__tabbed_1_4">&lt;4&gt;</label><label for="__tabbed_1_5">&lt;5&gt;</label><label for="__tabbed_1_6">&lt;6&gt;</label><label for="__tabbed_1_7">&lt;7&gt;</label><label for="__tabbed_1_8">&lt;8&gt;</label><label for="__tabbed_1_9">&lt;9&gt;</label><label for="__tabbed_1_10">&lt;10&gt;</label></div>
<div class="tabbed-content">
<div class="tabbed-block">
@@ -5046,10 +5046,10 @@
<h2 id="1163">11.6.3 &nbsp; Сортировка связного списка<a class="headerlink" href="#1163" title="Permanent link">&para;</a></h2>
<p>Для связных списков сортировка слиянием имеет заметное преимущество перед другими алгоритмами сортировки: <strong>пространственную сложность задачи сортировки списка можно оптимизировать до <span class="arithmatex">\(O(1)\)</span></strong>.</p>
<ul>
<li><strong>Этап разделения</strong>: работу по разбиению списка можно реализовать с помощью "итерации" вместо "рекурсии", тем самым устранив расход памяти на стек вызовов.</li>
<li><strong>Этап разделения</strong>: работу по разбиению списка можно реализовать с помощью «итерации» вместо «рекурсии», тем самым устранив расход памяти на стек вызовов.</li>
<li><strong>Этап слияния</strong>: в связном списке добавление и удаление узлов требует только изменения ссылок (указателей), поэтому при слиянии двух коротких упорядоченных списков в один длинный упорядоченный список не нужно создавать дополнительный список.</li>
</ul>
<p>Детали реализации достаточно сложны; заинтересованные читатели могут обратиться к соответствующим материалам самостоятельно.</p>
<p>Детали реализации достаточно сложны. Заинтересованные читатели могут обратиться к соответствующим материалам самостоятельно.</p>
<!-- Source file information -->
File diff suppressed because one or more lines are too long
+3 -3
View File
@@ -4357,13 +4357,13 @@
<!-- Page content -->
<h1 id="1110">11.10 &nbsp; Поразрядная сортировка<a class="headerlink" href="#1110" title="Permanent link">&para;</a></h1>
<p>В предыдущем разделе была рассмотрена сортировка подсчетом: она хорошо подходит для случаев, когда объем данных <span class="arithmatex">\(n\)</span> велик, а диапазон значений <span class="arithmatex">\(m\)</span> сравнительно мал. Предположим теперь, что нужно отсортировать <span class="arithmatex">\(n = 10^6\)</span> номеров студентов, причем каждый номер представляет собой <span class="arithmatex">\(8\)</span>-значное число. Тогда диапазон данных <span class="arithmatex">\(m = 10^8\)</span> оказывается очень большим; сортировка подсчетом потребует огромного объема памяти, а поразрядная сортировка позволяет этого избежать.</p>
<p>В предыдущем разделе была рассмотрена сортировка подсчетом: она хорошо подходит для случаев, когда объем данных <span class="arithmatex">\(n\)</span> велик, а диапазон значений <span class="arithmatex">\(m\)</span> сравнительно мал. Предположим теперь, что нужно отсортировать <span class="arithmatex">\(n = 10^6\)</span> номеров студентов, причем каждый номер представляет собой <span class="arithmatex">\(8\)</span>-значное число. Тогда диапазон данных <span class="arithmatex">\(m = 10^8\)</span> оказывается очень большим. Сортировка подсчетом потребует огромного объема памяти, а поразрядная сортировка позволяет этого избежать.</p>
<p><u>Поразрядная сортировка (radix sort)</u> по своей основной идее совпадает с сортировкой подсчетом и тоже реализует сортировку через подсчет количества. При этом поразрядная сортировка использует соотношение между разрядами числа и последовательно сортирует данные по каждому разряду, получая итоговый упорядоченный результат.</p>
<h2 id="11101">11.10.1 &nbsp; Алгоритм<a class="headerlink" href="#11101" title="Permanent link">&para;</a></h2>
<p>Рассмотрим пример со студенческими номерами: будем считать, что младший разряд имеет номер <span class="arithmatex">\(1\)</span> , а старший - номер <span class="arithmatex">\(8\)</span> . Тогда процесс поразрядной сортировки показан на рисунке 11-18.</p>
<ol>
<li>Инициализировать номер разряда <span class="arithmatex">\(k = 1\)</span> .</li>
<li>Выполнить "сортировку подсчетом" по <span class="arithmatex">\(k\)</span>-му разряду студенческого номера. После этого данные будут упорядочены по <span class="arithmatex">\(k\)</span>-му разряду по возрастанию.</li>
<li>Выполнить «сортировку подсчетом» по <span class="arithmatex">\(k\)</span>-му разряду студенческого номера. После этого данные будут упорядочены по <span class="arithmatex">\(k\)</span>-му разряду по возрастанию.</li>
<li>Увеличить <span class="arithmatex">\(k\)</span> на <span class="arithmatex">\(1\)</span> и вернуться к шагу <code>2.</code> , продолжая процесс, пока сортировка не будет выполнена для всех разрядов.</li>
</ol>
<p><img alt="Процесс поразрядной сортировки" class="animation-figure" src="../radix_sort.assets/radix_sort_overview.png" /></p>
@@ -5059,7 +5059,7 @@ x_k = \lfloor\frac{x}{d^{k-1}}\rfloor \bmod d
<ul>
<li><strong>Временная сложность равна <span class="arithmatex">\(O(nk)\)</span>, алгоритм не является адаптивным</strong>: пусть объем данных равен <span class="arithmatex">\(n\)</span> , числа записаны в системе счисления с основанием <span class="arithmatex">\(d\)</span> , а максимальное число разрядов равно <span class="arithmatex">\(k\)</span> . Тогда выполнение сортировки подсчетом для одного разряда требует <span class="arithmatex">\(O(n + d)\)</span> времени, а сортировка по всем <span class="arithmatex">\(k\)</span> разрядам требует <span class="arithmatex">\(O((n + d)k)\)</span> времени. Обычно <span class="arithmatex">\(d\)</span> и <span class="arithmatex">\(k\)</span> сравнительно малы, поэтому временная сложность стремится к <span class="arithmatex">\(O(n)\)</span> .</li>
<li><strong>Пространственная сложность равна <span class="arithmatex">\(O(n + d)\)</span>, сортировка не выполняется на месте</strong>: как и в сортировке подсчетом, здесь требуются массивы <code>res</code> и <code>counter</code> длины <span class="arithmatex">\(n\)</span> и <span class="arithmatex">\(d\)</span> .</li>
<li><strong>Стабильная сортировка</strong>: если сортировка подсчетом стабильна, то и поразрядная сортировка стабильна; если же сортировка подсчетом нестабильна, поразрядная сортировка не может гарантировать корректный результат.</li>
<li><strong>Стабильная сортировка</strong>: если сортировка подсчетом стабильна, то и поразрядная сортировка стабильна. Если же сортировка подсчетом нестабильна, поразрядная сортировка не может гарантировать корректный результат.</li>
</ul>
<!-- Source file information -->
+2 -2
View File
@@ -4336,7 +4336,7 @@
<!-- Page content -->
<h1 id="112">11.2 &nbsp; Сортировка выбором<a class="headerlink" href="#112" title="Permanent link">&para;</a></h1>
<p><u>Сортировка выбором (selection sort)</u> работает очень просто: запускается цикл, и на каждом шаге из неотсортированного диапазона выбирается минимальный элемент, после чего он переносится в конец уже отсортированного диапазона.</p>
<p>Пусть длина массива равна <span class="arithmatex">\(n\)</span> ; тогда процесс сортировки выбором выглядит так, как показано на рисунке 11-2.</p>
<p>Пусть длина массива равна <span class="arithmatex">\(n\)</span>. Тогда процесс сортировки выбором выглядит так, как показано на рисунке 11-2.</p>
<ol>
<li>В начальном состоянии все элементы не отсортированы, то есть неотсортированный диапазон индексов равен <span class="arithmatex">\([0, n-1]\)</span> .</li>
<li>Выбрать минимальный элемент из диапазона <span class="arithmatex">\([0, n-1]\)</span> и поменять его местами с элементом в позиции <span class="arithmatex">\(0\)</span> . После этого первый элемент массива отсортирован.</li>
@@ -4642,7 +4642,7 @@
</details>
<h2 id="1121">11.2.1 &nbsp; Характеристики алгоритма<a class="headerlink" href="#1121" title="Permanent link">&para;</a></h2>
<ul>
<li><strong>Временная сложность равна <span class="arithmatex">\(O(n^2)\)</span>, сортировка не является адаптивной</strong>: внешний цикл выполняется <span class="arithmatex">\(n - 1\)</span> раз; в первом раунде длина неотсортированного диапазона равна <span class="arithmatex">\(n\)</span> , а в последнем - <span class="arithmatex">\(2\)</span> , то есть отдельные раунды содержат <span class="arithmatex">\(n\)</span>, <span class="arithmatex">\(n - 1\)</span>, <span class="arithmatex">\(\dots\)</span>, <span class="arithmatex">\(3\)</span>, <span class="arithmatex">\(2\)</span> проходов внутреннего цикла, их сумма равна <span class="arithmatex">\(\frac{(n - 1)(n + 2)}{2}\)</span> .</li>
<li><strong>Временная сложность равна <span class="arithmatex">\(O(n^2)\)</span>, сортировка не является адаптивной</strong>: внешний цикл выполняется <span class="arithmatex">\(n - 1\)</span> раз. В первом раунде длина неотсортированного диапазона равна <span class="arithmatex">\(n\)</span> , а в последнем - <span class="arithmatex">\(2\)</span> , то есть отдельные раунды содержат <span class="arithmatex">\(n\)</span>, <span class="arithmatex">\(n - 1\)</span>, <span class="arithmatex">\(\dots\)</span>, <span class="arithmatex">\(3\)</span>, <span class="arithmatex">\(2\)</span> проходов внутреннего цикла, их сумма равна <span class="arithmatex">\(\frac{(n - 1)(n + 2)}{2}\)</span> .</li>
<li><strong>Пространственная сложность равна <span class="arithmatex">\(O(1)\)</span>, сортировка выполняется на месте</strong>: указатели <span class="arithmatex">\(i\)</span> и <span class="arithmatex">\(j\)</span> используют константный объем дополнительной памяти.</li>
<li><strong>Нестабильная сортировка</strong>: как показано на рисунке 11-3, элемент <code>nums[i]</code> может быть переставлен вправо от другого равного ему элемента, из-за чего их относительный порядок изменится.</li>
</ul>
@@ -4385,7 +4385,7 @@
<a id="__codelineno-0-16" name="__codelineno-0-16" href="#__codelineno-0-16"></a><span class="w"> </span><span class="o">(</span><span class="s1">&#39;E&#39;</span>,<span class="w"> </span><span class="m">23</span><span class="o">)</span>
</code></pre></div>
<p><strong>Адаптивность</strong>: <u>адаптивная сортировка</u> умеет использовать уже существующий порядок входных данных, чтобы сократить вычисления и добиться лучшей эффективности. Лучшая временная сложность адаптивных алгоритмов обычно лучше их средней временной сложности.</p>
<p><strong>Основанность на сравнении</strong>: <u>сортировка на основе сравнений</u> использует операторы сравнения (<span class="arithmatex">\(&lt;\)</span>, <span class="arithmatex">\(=\)</span>, <span class="arithmatex">\(&gt;\)</span>), чтобы определить относительный порядок элементов и отсортировать массив; ее теоретически лучшая временная сложность равна <span class="arithmatex">\(O(n \log n)\)</span> . А вот <u>сортировка без сравнений</u> не опирается на операторы сравнения, поэтому может достигать <span class="arithmatex">\(O(n)\)</span> , но универсальность у нее ниже.</p>
<p><strong>Основанность на сравнении</strong>: <u>сортировка на основе сравнений</u> использует операторы сравнения (<span class="arithmatex">\(&lt;\)</span>, <span class="arithmatex">\(=\)</span>, <span class="arithmatex">\(&gt;\)</span>), чтобы определить относительный порядок элементов и отсортировать массив. Ее теоретически лучшая временная сложность равна <span class="arithmatex">\(O(n \log n)\)</span> . А вот <u>сортировка без сравнений</u> не опирается на операторы сравнения, поэтому может достигать <span class="arithmatex">\(O(n)\)</span> , но универсальность у нее ниже.</p>
<h2 id="1112">11.1.2 &nbsp; Идеальный алгоритм сортировки<a class="headerlink" href="#1112" title="Permanent link">&para;</a></h2>
<p><strong>Быстрый, выполняющийся на месте, стабильный, адаптивный и универсальный</strong>. Очевидно, что на сегодняшний день не существует алгоритма сортировки, который одновременно обладал бы всеми этими свойствами. Поэтому при выборе алгоритма сортировки нужно исходить из конкретных особенностей данных и требований задачи.</p>
<p>Далее мы последовательно изучим разные алгоритмы сортировки и на основании приведенных выше критериев разберем их преимущества и недостатки.</p>
+9 -9
View File
@@ -4362,9 +4362,9 @@
<li>Сортировка пузырьком выполняет сортировку за счет обмена соседних элементов. Если добавить флаг для досрочного выхода, лучшую временную сложность пузырьковой сортировки можно оптимизировать до <span class="arithmatex">\(O(n)\)</span> .</li>
<li>Сортировка вставками на каждом раунде вставляет элемент из неотсортированного диапазона в правильную позицию внутри отсортированного диапазона. Хотя ее временная сложность равна <span class="arithmatex">\(O(n^2)\)</span> , она очень популярна для задач сортировки небольших массивов, поскольку число элементарных операций у нее сравнительно невелико.</li>
<li>Быстрая сортировка основана на операции разделения с опорным элементом. При неудачном выборе опорного элемента на каждом раунде ее временная сложность может деградировать до <span class="arithmatex">\(O(n^2)\)</span> . Использование медианы трех элементов или случайного опорного элемента уменьшает вероятность этой деградации. Если всегда рекурсивно обрабатывать более короткий поддиапазон первым, можно эффективно уменьшить глубину рекурсии и оптимизировать пространственную сложность до <span class="arithmatex">\(O(\log n)\)</span> .</li>
<li>Сортировка слиянием включает этапы разделения и слияния и служит типичным проявлением стратегии "разделяй и властвуй". Для сортировки массива ей требуется вспомогательный массив, поэтому пространственная сложность равна <span class="arithmatex">\(O(n)\)</span> ; однако при сортировке связного списка пространственную сложность можно оптимизировать до <span class="arithmatex">\(O(1)\)</span> .</li>
<li>Блочная сортировка включает три этапа: распределение данных по блокам, сортировку внутри блоков и объединение результатов. Она тоже отражает стратегию "разделяй и властвуй" и подходит для очень больших объемов данных. Ключ к эффективности блочной сортировки - равномерное распределение данных.</li>
<li>Сортировка подсчетом является частным случаем блочной сортировки; она реализует сортировку через подсчет числа вхождений данных. Сортировка подсчетом подходит для случаев, когда объем данных велик, но диапазон значений ограничен, и при этом данные можно преобразовать в положительные целые числа.</li>
<li>Сортировка слиянием включает этапы разделения и слияния и служит типичным проявлением стратегии «разделяй и властвуй». Для сортировки массива ей требуется вспомогательный массив, поэтому пространственная сложность равна <span class="arithmatex">\(O(n)\)</span>. Однако при сортировке связного списка пространственную сложность можно оптимизировать до <span class="arithmatex">\(O(1)\)</span> .</li>
<li>Блочная сортировка включает три этапа: распределение данных по блокам, сортировку внутри блоков и объединение результатов. Она тоже отражает стратегию «разделяй и властвуй» и подходит для очень больших объемов данных. Ключ к эффективности блочной сортировки - равномерное распределение данных.</li>
<li>Сортировка подсчетом является частным случаем блочной сортировки. Она реализует сортировку через подсчет числа вхождений данных. Сортировка подсчетом подходит для случаев, когда объем данных велик, но диапазон значений ограничен, и при этом данные можно преобразовать в положительные целые числа.</li>
<li>Поразрядная сортировка выполняет сортировку данных путем последовательной сортировки по каждому разряду и требует, чтобы данные можно было представить в виде чисел фиксированной разрядности.</li>
<li>В общем случае нам хотелось бы найти алгоритм сортировки, который одновременно обладал бы высокой эффективностью, стабильностью, выполнением на месте и адаптивностью. Но, как и в других разделах алгоритмов и структур данных, не существует одного алгоритма сортировки, способного удовлетворить всем этим требованиям одновременно. На практике приходится выбирать подходящий алгоритм в зависимости от свойств данных.</li>
<li>На рисунке 11-19 сравниваются эффективность, стабильность, выполнение на месте и адаптивность основных алгоритмов сортировки.</li>
@@ -4376,14 +4376,14 @@
<p><strong>В</strong>: В каких случаях стабильность алгоритма сортировки является обязательной?</p>
<p>В реальных задачах нам может понадобиться сортировать объекты по некоторому атрибуту. Например, у студентов есть два атрибута: имя и рост. Мы хотим выполнить многоуровневую сортировку: сначала отсортировать по имени и получить <code>(A, 180) (B, 185) (C, 170) (D, 170)</code> , а затем отсортировать по росту. Если используемый алгоритм сортировки нестабилен, то мы можем получить <code>(D, 170) (C, 170) (A, 180) (B, 185)</code> .</p>
<p>Нетрудно увидеть, что в этом случае студенты D и C поменялись местами, порядок по имени разрушился, а именно этого мы и не хотим.</p>
<p><strong>В</strong>: Можно ли поменять местами порядок "поиска справа налево" и "поиска слева направо" в разделении с опорным элементом?</p>
<p>Нет. Если в качестве опорного элемента выбирается самый левый элемент, необходимо сначала выполнять "поиск справа налево", а уже затем - "поиск слева направо". Этот вывод кажется немного неочевидным, поэтому разберем его подробнее.</p>
<p>Последний шаг <code>partition()</code> - это обмен <code>nums[left]</code> и <code>nums[i]</code> . После обмена все элементы слева от опорного должны быть <code>&lt;=</code> опорного, <strong>а значит, перед этим обменом должно выполняться условие <code>nums[left] &gt;= nums[i]</code></strong>. Если сначала выполнять "поиск слева направо", то в случае, когда не удается найти элемент больше опорного, <strong>цикл завершится в состоянии <code>i == j</code> , и при этом может оказаться, что <code>nums[j] == nums[i] &gt; nums[left]</code></strong>. Иными словами, на последнем шаге обмена элемент, больший опорного, будет помещен в начало массива, из-за чего разделение завершится неверно.</p>
<p>Например, для массива <code>[0, 0, 0, 0, 1]</code> , если сначала выполнять "поиск слева направо", после разделения получится <code>[1, 0, 0, 0, 0]</code> , а это неправильный результат.</p>
<p>Если же выбрать <code>nums[right]</code> в качестве опорного элемента, то ситуация станет противоположной, и тогда сначала нужно выполнять "поиск слева направо".</p>
<p><strong>В</strong>: Можно ли поменять местами порядок «поиска справа налево» и «поиска слева направо» в разделении с опорным элементом?</p>
<p>Нет. Если в качестве опорного элемента выбирается самый левый элемент, необходимо сначала выполнять «поиск справа налево», а уже затем - «поиск слева направо». Этот вывод кажется немного неочевидным, поэтому разберем его подробнее.</p>
<p>Последний шаг <code>partition()</code> - это обмен <code>nums[left]</code> и <code>nums[i]</code> . После обмена все элементы слева от опорного должны быть <code>&lt;=</code> опорного, <strong>а значит, перед этим обменом должно выполняться условие <code>nums[left] &gt;= nums[i]</code></strong>. Если сначала выполнять «поиск слева направо», то в случае, когда не удается найти элемент больше опорного, <strong>цикл завершится в состоянии <code>i == j</code> , и при этом может оказаться, что <code>nums[j] == nums[i] &gt; nums[left]</code></strong>. Иными словами, на последнем шаге обмена элемент, больший опорного, будет помещен в начало массива, из-за чего разделение завершится неверно.</p>
<p>Например, для массива <code>[0, 0, 0, 0, 1]</code> , если сначала выполнять «поиск слева направо», после разделения получится <code>[1, 0, 0, 0, 0]</code> , а это неправильный результат.</p>
<p>Если же выбрать <code>nums[right]</code> в качестве опорного элемента, то ситуация станет противоположной, и тогда сначала нужно выполнять «поиск слева направо».</p>
<p><strong>В</strong>: Почему при оптимизации глубины рекурсии в быстрой сортировке выбор короткого массива гарантирует, что глубина рекурсии не превысит <span class="arithmatex">\(\log n\)</span> ?</p>
<p>Глубина рекурсии - это число текущих рекурсивных вызовов, которые еще не завершились. На каждом раунде разделения исходный массив разбивается на два подмассива. После оптимизации глубины рекурсии длина подмассива, в который мы продолжаем рекурсивный спуск, не превышает половины длины исходного массива. Если рассматривать худший случай, когда длина каждый раз становится ровно вдвое меньше, итоговая глубина рекурсии и будет равна <span class="arithmatex">\(\log n\)</span> .</p>
<p>В исходной версии быстрой сортировки может происходить последовательный рекурсивный вызов для более длинных массивов; в худшем случае это будут длины <span class="arithmatex">\(n\)</span> , <span class="arithmatex">\(n - 1\)</span> , <span class="arithmatex">\(\dots\)</span> , <span class="arithmatex">\(2\)</span> , <span class="arithmatex">\(1\)</span> , а глубина рекурсии окажется равной <span class="arithmatex">\(n\)</span> . Оптимизация глубины рекурсии как раз и позволяет избежать такого сценария.</p>
<p>В исходной версии быстрой сортировки может происходить последовательный рекурсивный вызов для более длинных массивов. В худшем случае это будут длины <span class="arithmatex">\(n\)</span> , <span class="arithmatex">\(n - 1\)</span> , <span class="arithmatex">\(\dots\)</span> , <span class="arithmatex">\(2\)</span> , <span class="arithmatex">\(1\)</span> , а глубина рекурсии окажется равной <span class="arithmatex">\(n\)</span> . Оптимизация глубины рекурсии как раз и позволяет избежать такого сценария.</p>
<p><strong>В</strong>: Если все элементы массива равны, будет ли временная сложность быстрой сортировки равна <span class="arithmatex">\(O(n^2)\)</span> ? Как справиться с таким вырождением?</p>
<p>Да. Для этого случая можно рассмотреть разделение массива на три части: элементы меньше опорного, равные опорному и большие опорного. Рекурсию нужно продолжать только для частей меньше и больше опорного. При таком подходе массив, целиком состоящий из одинаковых элементов, будет отсортирован всего за один раунд разделения.</p>
<p><strong>В</strong>: Почему худшая временная сложность блочной сортировки равна <span class="arithmatex">\(O(n^2)\)</span> ?</p>
+3 -3
View File
@@ -4836,7 +4836,7 @@
<h3 id="1">1. &nbsp; Реализация на основе двусвязного списка<a class="headerlink" href="#1" title="Permanent link">&para;</a></h3>
<p>Вспомним предыдущий раздел: там мы использовали обычный односвязный список для реализации очереди, потому что он позволяет удобно удалять головной узел, что соответствует операции <code>dequeue</code> , и добавлять новый узел после хвостового узла, что соответствует операции <code>enqueue</code> .</p>
<p>Для двусторонней очереди и голова, и хвост допускают операции добавления и удаления элементов. Иначе говоря, двусторонняя очередь требует реализации еще одного симметричного направления операций. Поэтому в качестве базовой структуры данных двусторонней очереди удобно использовать двусвязный список.</p>
<p>Как показано на рисунках ниже, мы рассматриваем головной и хвостовой узлы двусвязного списка как голову и хвост двусторонней очереди и одновременно реализуем функции добавления и удаления узлов с обеих сторон.</p>
<p>Как показано на рисунке 5-8, мы рассматриваем головной и хвостовой узлы двусвязного списка как голову и хвост двусторонней очереди и одновременно реализуем функции добавления и удаления узлов с обеих сторон.</p>
<div class="tabbed-set tabbed-alternate" data-tabs="2:5"><input checked="checked" id="__tabbed_2_1" name="__tabbed_2" type="radio" /><input id="__tabbed_2_2" name="__tabbed_2" type="radio" /><input id="__tabbed_2_3" name="__tabbed_2" type="radio" /><input id="__tabbed_2_4" name="__tabbed_2" type="radio" /><input id="__tabbed_2_5" name="__tabbed_2" type="radio" /><div class="tabbed-labels"><label for="__tabbed_2_1">&lt;1&gt;</label><label for="__tabbed_2_2">&lt;2&gt;</label><label for="__tabbed_2_3">&lt;3&gt;</label><label for="__tabbed_2_4">&lt;4&gt;</label><label for="__tabbed_2_5">&lt;5&gt;</label></div>
<div class="tabbed-content">
<div class="tabbed-block">
@@ -6563,7 +6563,7 @@
</div>
</div>
<h3 id="2">2. &nbsp; Реализация на основе массива<a class="headerlink" href="#2" title="Permanent link">&para;</a></h3>
<p>Как показано на рисунках ниже, аналогично реализации обычной очереди на массиве мы также можем использовать кольцевой массив для реализации двусторонней очереди.</p>
<p>Как показано на рисунке 5-9, аналогично реализации обычной очереди на массиве мы также можем использовать кольцевой массив для реализации двусторонней очереди.</p>
<div class="tabbed-set tabbed-alternate" data-tabs="4:5"><input checked="checked" id="__tabbed_4_1" name="__tabbed_4" type="radio" /><input id="__tabbed_4_2" name="__tabbed_4" type="radio" /><input id="__tabbed_4_3" name="__tabbed_4" type="radio" /><input id="__tabbed_4_4" name="__tabbed_4" type="radio" /><input id="__tabbed_4_5" name="__tabbed_4" type="radio" /><div class="tabbed-labels"><label for="__tabbed_4_1">&lt;1&gt;</label><label for="__tabbed_4_2">&lt;2&gt;</label><label for="__tabbed_4_3">&lt;3&gt;</label><label for="__tabbed_4_4">&lt;4&gt;</label><label for="__tabbed_4_5">&lt;5&gt;</label></div>
<div class="tabbed-content">
<div class="tabbed-block">
@@ -7991,7 +7991,7 @@
</div>
<h2 id="533">5.3.3 &nbsp; Применение двусторонней очереди<a class="headerlink" href="#533" title="Permanent link">&para;</a></h2>
<p>Двусторонняя очередь сочетает в себе логику стека и очереди, <strong>поэтому она может покрыть все сценарии применения обеих структур и при этом предоставляет более высокую степень свободы</strong>.</p>
<p>Мы знаем, что функция "undo" в программном обеспечении обычно реализуется с помощью стека: система помещает каждое изменение в стек с помощью <code>push</code> , а затем использует <code>pop</code> для отмены. Однако, учитывая ограниченность системных ресурсов, программы обычно ограничивают число шагов отмены, например разрешают хранить только <span class="arithmatex">\(50\)</span> шагов. Когда длина стека превышает этот предел, программе нужно удалить элемент с дна стека, то есть с головы очереди. <strong>Но стек не может реализовать такую операцию, и в этом случае его приходится заменять двусторонней очередью</strong>. Обрати внимание: основная логика "undo" по-прежнему следует стековому правилу LIFO, просто двусторонняя очередь позволяет более гибко реализовать некоторые дополнительные механизмы.</p>
<p>Мы знаем, что функция «undo» в программном обеспечении обычно реализуется с помощью стека: система помещает каждое изменение в стек с помощью <code>push</code> , а затем использует <code>pop</code> для отмены. Однако, учитывая ограниченность системных ресурсов, программы обычно ограничивают число шагов отмены, например разрешают хранить только <span class="arithmatex">\(50\)</span> шагов. Когда длина стека превышает этот предел, программе нужно удалить элемент с дна стека, то есть с головы очереди. <strong>Но стек не может реализовать такую операцию, и в этом случае его приходится заменять двусторонней очередью</strong>. Обрати внимание: основная логика «undo» по-прежнему следует стековому правилу LIFO, просто двусторонняя очередь позволяет более гибко реализовать некоторые дополнительные механизмы.</p>
<!-- Source file information -->
+1 -1
View File
@@ -4280,7 +4280,7 @@
<div class="admonition abstract">
<p class="admonition-title">Abstract</p>
<p>Стек и очередь - две базовые линейные структуры данных.</p>
<p>Они соответственно воплощают принципы "последним пришел - первым вышел" и "первым пришел - первым вышел".</p>
<p>Они соответственно воплощают принципы «последним пришел - первым вышел» и «первым пришел - первым вышел».</p>
</div>
<h2 id="_1">Содержание главы<a class="headerlink" href="#_1" title="Permanent link">&para;</a></h2>
<ul>
+5 -5
View File
@@ -4435,8 +4435,8 @@
<!-- Page content -->
<h1 id="52">5.2 &nbsp; Очередь<a class="headerlink" href="#52" title="Permanent link">&para;</a></h1>
<p><u>Очередь (queue)</u> - это линейная структура данных, подчиняющаяся правилу "первым пришел - первым вышел". Как видно из названия, очередь моделирует обычную ситуацию ожидания: новые люди непрерывно присоединяются к хвосту очереди, а стоящие в начале по одному уходят.</p>
<p>Как показано на рисунке 5-4, начало очереди называется головой очереди, а конец - хвостом очереди; операцию добавления элемента в хвост называют <code>enqueue</code>, а операцию удаления элемента из головы - <code>dequeue</code>.</p>
<p><u>Очередь (queue)</u> - это линейная структура данных, подчиняющаяся правилу «первым пришел - первым вышел». Как видно из названия, очередь моделирует обычную ситуацию ожидания: новые люди непрерывно присоединяются к хвосту очереди, а стоящие в начале по одному уходят.</p>
<p>Как показано на рисунке 5-4, начало очереди называется головой очереди, а конец - хвостом очереди. Операцию добавления элемента в хвост называют <code>enqueue</code>, а операцию удаления элемента из головы - <code>dequeue</code>.</p>
<p><img alt="Правило FIFO для очереди" class="animation-figure" src="../queue.assets/queue_operations.png" /></p>
<p align="center"> Рисунок 5-4 &nbsp; Правило FIFO для очереди </p>
@@ -4792,7 +4792,7 @@
<p>https://pythontutor.com/render.html#code=from%20collections%20import%20deque%0A%0A%22%22%22Driver%20Code%22%22%22%0Aif%20__name__%20%3D%3D%20%22__main__%22%3A%0A%20%20%20%20%23%20%D0%98%D0%BD%D0%B8%D1%86%D0%B8%D0%B0%D0%BB%D0%B8%D0%B7%D0%B8%D1%80%D0%BE%D0%B2%D0%B0%D1%82%D1%8C%20%D0%BE%D1%87%D0%B5%D1%80%D0%B5%D0%B4%D1%8C%0A%20%20%20%20%23%20%D0%92%20Python%20%D0%B4%D0%B2%D1%83%D1%81%D1%82%D0%BE%D1%80%D0%BE%D0%BD%D0%BD%D1%8E%D1%8E%20%D0%BE%D1%87%D0%B5%D1%80%D0%B5%D0%B4%D1%8C%20deque%20%D0%BE%D0%B1%D1%8B%D1%87%D0%BD%D0%BE%20%D0%B8%D1%81%D0%BF%D0%BE%D0%BB%D1%8C%D0%B7%D1%83%D1%8E%D1%82%20%D0%BA%D0%B0%D0%BA%20%D0%BE%D1%87%D0%B5%D1%80%D0%B5%D0%B4%D1%8C%0A%20%20%20%20%23%20%D0%A5%D0%BE%D1%82%D1%8F%20queue.Queue%28%29%20%D1%8F%D0%B2%D0%BB%D1%8F%D0%B5%D1%82%D1%81%D1%8F%20%D0%BD%D0%B0%D1%81%D1%82%D0%BE%D1%8F%D1%89%D0%B8%D0%BC%20%D0%BA%D0%BB%D0%B0%D1%81%D1%81%D0%BE%D0%BC%20%D0%BE%D1%87%D0%B5%D1%80%D0%B5%D0%B4%D0%B8%2C%20%D0%BF%D0%BE%D0%BB%D1%8C%D0%B7%D0%BE%D0%B2%D0%B0%D1%82%D1%8C%D1%81%D1%8F%20%D0%B8%D0%BC%20%D0%BD%D0%B5%20%D1%81%D0%BB%D0%B8%D1%88%D0%BA%D0%BE%D0%BC%20%D1%83%D0%B4%D0%BE%D0%B1%D0%BD%D0%BE%0A%20%20%20%20que%20%3D%20deque%28%29%0A%0A%20%20%20%20%23%20%D0%9F%D0%BE%D0%BC%D0%B5%D1%81%D1%82%D0%B8%D1%82%D1%8C%20%D1%8D%D0%BB%D0%B5%D0%BC%D0%B5%D0%BD%D1%82%20%D0%B2%20%D0%BE%D1%87%D0%B5%D1%80%D0%B5%D0%B4%D1%8C%0A%20%20%20%20que.append%281%29%0A%20%20%20%20que.append%283%29%0A%20%20%20%20que.append%282%29%0A%20%20%20%20que.append%285%29%0A%20%20%20%20que.append%284%29%0A%20%20%20%20print%28%22%D0%BE%D1%87%D0%B5%D1%80%D0%B5%D0%B4%D1%8C%20que%20%3D%22%2C%20que%29%0A%0A%20%20%20%20%23%20%D0%9F%D0%BE%D0%BB%D1%83%D1%87%D0%B8%D1%82%D1%8C%20%D1%8D%D0%BB%D0%B5%D0%BC%D0%B5%D0%BD%D1%82%20%D0%B2%20%D0%BD%D0%B0%D1%87%D0%B0%D0%BB%D0%B5%20%D0%BE%D1%87%D0%B5%D1%80%D0%B5%D0%B4%D0%B8%0A%20%20%20%20front%20%3D%20que%5B0%5D%0A%20%20%20%20print%28%22%D0%AD%D0%BB%D0%B5%D0%BC%D0%B5%D0%BD%D1%82%20%D0%B2%20%D0%BD%D0%B0%D1%87%D0%B0%D0%BB%D0%B5%20%D0%BE%D1%87%D0%B5%D1%80%D0%B5%D0%B4%D0%B8%20front%20%3D%22%2C%20front%29%0A%0A%20%20%20%20%23%20%D0%98%D0%B7%D0%B2%D0%BB%D0%B5%D1%87%D1%8C%20%D1%8D%D0%BB%D0%B5%D0%BC%D0%B5%D0%BD%D1%82%20%D0%B8%D0%B7%20%D0%BE%D1%87%D0%B5%D1%80%D0%B5%D0%B4%D0%B8%0A%20%20%20%20pop%20%3D%20que.popleft%28%29%0A%20%20%20%20print%28%22%D0%98%D0%B7%D0%B2%D0%BB%D0%B5%D1%87%D0%B5%D0%BD%D0%BD%D1%8B%D0%B9%20%D0%B8%D0%B7%20%D0%BE%D1%87%D0%B5%D1%80%D0%B5%D0%B4%D0%B8%20%D1%8D%D0%BB%D0%B5%D0%BC%D0%B5%D0%BD%D1%82%20pop%20%3D%22%2C%20pop%29%0A%20%20%20%20print%28%22que%20%D0%BF%D0%BE%D1%81%D0%BB%D0%B5%20%D0%B8%D0%B7%D0%B2%D0%BB%D0%B5%D1%87%D0%B5%D0%BD%D0%B8%D1%8F%20%3D%22%2C%20que%29%0A%0A%20%20%20%20%23%20%D0%9F%D0%BE%D0%BB%D1%83%D1%87%D0%B8%D1%82%D1%8C%20%D0%B4%D0%BB%D0%B8%D0%BD%D1%83%20%D0%BE%D1%87%D0%B5%D1%80%D0%B5%D0%B4%D0%B8%0A%20%20%20%20size%20%3D%20len%28que%29%0A%20%20%20%20print%28%22%D0%94%D0%BB%D0%B8%D0%BD%D0%B0%20%D0%BE%D1%87%D0%B5%D1%80%D0%B5%D0%B4%D0%B8%20size%20%3D%22%2C%20size%29%0A%0A%20%20%20%20%23%20%D0%9F%D1%80%D0%BE%D0%B2%D0%B5%D1%80%D0%B8%D1%82%D1%8C%2C%20%D0%BF%D1%83%D1%81%D1%82%D0%B0%20%D0%BB%D0%B8%20%D0%BE%D1%87%D0%B5%D1%80%D0%B5%D0%B4%D1%8C%0A%20%20%20%20is_empty%20%3D%20len%28que%29%20%3D%3D%200%0A%20%20%20%20print%28%22%D0%9F%D1%83%D1%81%D1%82%D0%B0%20%D0%BB%D0%B8%20%D0%BE%D1%87%D0%B5%D1%80%D0%B5%D0%B4%D1%8C%20%3D%22%2C%20is_empty%29&amp;cumulative=false&amp;curInstr=3&amp;heapPrimitives=nevernest&amp;mode=display&amp;origin=opt-frontend.js&amp;py=311&amp;rawInputLstJSON=%5B%5D&amp;textReferences=false</p>
</details>
<h2 id="522">5.2.2 &nbsp; Реализация очереди<a class="headerlink" href="#522" title="Permanent link">&para;</a></h2>
<p>Чтобы реализовать очередь, нам нужна такая структура данных, которая позволяет добавлять элементы с одного конца и удалять их с другого; и связный список, и массив этим требованиям удовлетворяют.</p>
<p>Чтобы реализовать очередь, нам нужна такая структура данных, которая позволяет добавлять элементы с одного конца и удалять их с другого. И связный список, и массив этим требованиям удовлетворяют.</p>
<h3 id="1">1. &nbsp; Реализация на основе связного списка<a class="headerlink" href="#1" title="Permanent link">&para;</a></h3>
<p>Как показано на рисунке 5-5, мы можем рассматривать головной узел и хвостовой узел связного списка как голову очереди и хвост очереди соответственно, договорившись, что добавлять узлы можно только в хвост, а удалять - только из головы.</p>
<div class="tabbed-set tabbed-alternate" data-tabs="2:3"><input checked="checked" id="__tabbed_2_1" name="__tabbed_2" type="radio" /><input id="__tabbed_2_2" name="__tabbed_2" type="radio" /><input id="__tabbed_2_3" name="__tabbed_2" type="radio" /><div class="tabbed-labels"><label for="__tabbed_2_1">&lt;1&gt;</label><label for="__tabbed_2_2">&lt;2&gt;</label><label for="__tabbed_2_3">&lt;3&gt;</label></div>
@@ -5715,7 +5715,7 @@
</details>
<h3 id="2">2. &nbsp; Реализация на основе массива<a class="headerlink" href="#2" title="Permanent link">&para;</a></h3>
<p>Удаление первого элемента из массива имеет временную сложность <span class="arithmatex">\(O(n)\)</span> , из-за чего операция <code>dequeue</code> оказывается неэффективной. Однако этого можно избежать с помощью следующего приема.</p>
<p>Мы можем использовать переменную <code>front</code> , указывающую на индекс элемента в голове очереди, и поддерживать переменную <code>size</code> , которая хранит длину очереди. Определим <code>rear = front + size</code> ; эта формула дает позицию <code>rear</code>, указывающую на ячейку сразу после хвоста очереди.</p>
<p>Мы можем использовать переменную <code>front</code> , указывающую на индекс элемента в голове очереди, и поддерживать переменную <code>size</code> , которая хранит длину очереди. Определим <code>rear = front + size</code>. Эта формула дает позицию <code>rear</code>, указывающую на ячейку сразу после хвоста очереди.</p>
<p>Исходя из этого, <strong>эффективный диапазон элементов массива равен <code>[front, rear - 1]</code></strong>, а различные операции реализуются, как показано на рисунке 5-6.</p>
<ul>
<li>Операция <code>enqueue</code>: записать входной элемент по индексу <code>rear</code> и увеличить <code>size</code> на 1.</li>
@@ -6669,7 +6669,7 @@
<h2 id="523">5.2.3 &nbsp; Типичные применения очереди<a class="headerlink" href="#523" title="Permanent link">&para;</a></h2>
<ul>
<li><strong>Очереди заказов</strong>. После оформления заказа покупателем заказ попадает в очередь, а затем система обрабатывает заказы по порядку. Во время крупных распродаж за короткое время возникает огромный поток заказов, и высокая конкурентная нагрузка становится ключевой инженерной проблемой.</li>
<li><strong>Различные отложенные задачи</strong>. Любой сценарий, где нужно реализовать принцип "кто раньше пришел, тот раньше обслуживается", например очередь заданий принтера или очередь блюд на кухне ресторана, хорошо моделируется очередью, которая эффективно поддерживает нужный порядок обработки.</li>
<li><strong>Различные отложенные задачи</strong>. Любой сценарий, где нужно реализовать принцип «кто раньше пришел, тот раньше обслуживается», например очередь заданий принтера или очередь блюд на кухне ресторана, хорошо моделируется очередью, которая эффективно поддерживает нужный порядок обработки.</li>
</ul>
<!-- Source file information -->

Some files were not shown because too many files have changed in this diff Show More