mirror of
https://github.com/krahets/hello-algo.git
synced 2026-07-10 06:26:08 +00:00
deploy
This commit is contained in:
@@ -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&cumulative=false&curInstr=0&heapPrimitives=nevernest&mode=display&origin=opt-frontend.js&py=311&rawInputLstJSON=%5B%5D&textReferences=false</p>
|
||||
</details>
|
||||
<h3 id="2">2. Доступ к элементам<a class="headerlink" href="#2" title="Permanent link">¶</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 Вычисление адреса элемента массива </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 Пример вставки элемента в массив </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. Обход массива<a class="headerlink" href="#5" title="Permanent link">¶</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">Во весь экран ></a></div></p>
|
||||
</details>
|
||||
<h3 id="6">6. Поиск элемента<a class="headerlink" href="#6" title="Permanent link">¶</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 Типичные применения массива<a class="headerlink" href="#413" title="Permanent link">¶</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&cumulative=false&curInstr=3&heapPrimitives=nevernest&mode=display&origin=opt-frontend.js&py=311&rawInputLstJSON=%5B%5D&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. Вставка узла<a class="headerlink" href="#2" title="Permanent link">¶</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 Пример вставки узла в связный список </p>
|
||||
@@ -5819,14 +5819,14 @@
|
||||
<h2 id="424">4.2.4 Типичные применения связных списков<a class="headerlink" href="#424" title="Permanent link">¶</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 Основные операции со списком<a class="headerlink" href="#431" title="Permanent link">¶</a></h2>
|
||||
<h3 id="1">1. Инициализация списка<a class="headerlink" href="#1" title="Permanent link">¶</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&cumulative=false&curInstr=3&heapPrimitives=nevernest&mode=display&origin=opt-frontend.js&py=311&rawInputLstJSON=%5B%5D&textReferences=false</p>
|
||||
</details>
|
||||
<h3 id="6">6. Сортировка списка<a class="headerlink" href="#6" title="Permanent link">¶</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 Эффективность использования кэша структурами данных<a class="headerlink" href="#443" title="Permanent link">¶</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. Ключевые выводы<a class="headerlink" href="#1" title="Permanent link">¶</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 -->
|
||||
|
||||
|
||||
Reference in New Issue
Block a user