0 added
0 removed
Original
2026-01-01
Modified
2026-02-21
1
<p><a>#статьи</a></p>
1
<p><a>#статьи</a></p>
2
<ul><li>18 янв 2022</li>
2
<ul><li>18 янв 2022</li>
3
<li>0</li>
3
<li>0</li>
4
</ul><p>Software Engineer Валерий Жила подробно рассказал, что такое O(n), и показал, как её считать на примере бинарного поиска и других алгоритмов.</p>
4
</ul><p>Software Engineer Валерий Жила подробно рассказал, что такое O(n), и показал, как её считать на примере бинарного поиска и других алгоритмов.</p>
5
<p>Кадр: фильм "Мальчишник в Вегасе"</p>
5
<p>Кадр: фильм "Мальчишник в Вегасе"</p>
6
<p>Онлайн-журнал для тех, кто влюблён в код и информационные технологии. Пишем для айтишников и об айтишниках.</p>
6
<p>Онлайн-журнал для тех, кто влюблён в код и информационные технологии. Пишем для айтишников и об айтишниках.</p>
7
<p>Валерий Жила</p>
7
<p>Валерий Жила</p>
8
<p><strong>об эксперте</strong></p>
8
<p><strong>об эксперте</strong></p>
9
<p>Software Engineer, разрабатывает системы управления городской инфраструктурой для мегаполисов. Основная деятельность: backend, database engineering. Ведёт твиттер<a>@ValeriiZhyla</a>и пишет научные работы по DevOps.</p>
9
<p>Software Engineer, разрабатывает системы управления городской инфраструктурой для мегаполисов. Основная деятельность: backend, database engineering. Ведёт твиттер<a>@ValeriiZhyla</a>и пишет научные работы по DevOps.</p>
10
<p>Сегодня я расскажу о сложности алгоритмов, или, как её часто называют, Big O Notation. Статья рассчитана на новичков, поэтому постараюсь изложить идеи и концепции просто, не теряя важных деталей.</p>
10
<p>Сегодня я расскажу о сложности алгоритмов, или, как её часто называют, Big O Notation. Статья рассчитана на новичков, поэтому постараюсь изложить идеи и концепции просто, не теряя важных деталей.</p>
11
<p><a>Алгоритмы</a>описывают с помощью двух характеристик -<em>времени</em>и <em>памяти</em>.</p>
11
<p><a>Алгоритмы</a>описывают с помощью двух характеристик -<em>времени</em>и <em>памяти</em>.</p>
12
<p><strong>Время</strong> - это… время, которое нужно алгоритму для обработки данных. Например, для маленького массива - 10 секунд, а для массива побольше - 100 секунд. Интуитивно понятно, что время выполнения алгоритма зависит от размера массива.</p>
12
<p><strong>Время</strong> - это… время, которое нужно алгоритму для обработки данных. Например, для маленького массива - 10 секунд, а для массива побольше - 100 секунд. Интуитивно понятно, что время выполнения алгоритма зависит от размера массива.</p>
13
<p>Но есть проблема: секунды, минуты и часы - это не очень показательные единицы измерения. Кроме того, время работы алгоритма зависит от железа, на котором он выполняется, и других внешних факторов. Поэтому время считают не в секундах и часах, а в количестве операций, которые алгоритм совершит. Это надёжная и, главное, независимая от железа метрика.</p>
13
<p>Но есть проблема: секунды, минуты и часы - это не очень показательные единицы измерения. Кроме того, время работы алгоритма зависит от железа, на котором он выполняется, и других внешних факторов. Поэтому время считают не в секундах и часах, а в количестве операций, которые алгоритм совершит. Это надёжная и, главное, независимая от железа метрика.</p>
14
<p>Когда говорят о <em>Time Complexity</em>или просто<em>Time</em>, то речь идёт именно о количестве операций. Для простоты расчётов разница в скорости между операциями обычно опускается. Поэтому, несмотря на то, что деление чисел с плавающей точкой требует от процессора больше действий, чем сложение целых чисел, обе операции в теории алгоритмов считаются равными по сложности.</p>
14
<p>Когда говорят о <em>Time Complexity</em>или просто<em>Time</em>, то речь идёт именно о количестве операций. Для простоты расчётов разница в скорости между операциями обычно опускается. Поэтому, несмотря на то, что деление чисел с плавающей точкой требует от процессора больше действий, чем сложение целых чисел, обе операции в теории алгоритмов считаются равными по сложности.</p>
15
<p>Запомните: в О-нотации на операции с одной или двумя переменными вроде<em>i++</em>,<em>a * b</em>,<em>a / 1024</em>,<em>max(a,b)</em>уходит всего одна единица времени.</p>
15
<p>Запомните: в О-нотации на операции с одной или двумя переменными вроде<em>i++</em>,<em>a * b</em>,<em>a / 1024</em>,<em>max(a,b)</em>уходит всего одна единица времени.</p>
16
<p><strong>Память, или место,</strong> - это объём оперативной памяти, который потребуется алгоритму для работы. Одна переменная - это одна ячейка памяти, а массив с тысячей ячеек - тысяча ячеек памяти.</p>
16
<p><strong>Память, или место,</strong> - это объём оперативной памяти, который потребуется алгоритму для работы. Одна переменная - это одна ячейка памяти, а массив с тысячей ячеек - тысяча ячеек памяти.</p>
17
<p>В теории алгоритмов все ячейки считаются равноценными. Например, int a на 4 байта и double b на 8 байт имеют один вес. Потребление памяти обычно называется<em>Space Complexity</em>или просто<em>Space</em>, редко -<em>Memory</em>.</p>
17
<p>В теории алгоритмов все ячейки считаются равноценными. Например, int a на 4 байта и double b на 8 байт имеют один вес. Потребление памяти обычно называется<em>Space Complexity</em>или просто<em>Space</em>, редко -<em>Memory</em>.</p>
18
<p>Алгоритмы, которые используют исходный массив как рабочее пространство, называют<em>in-place</em>. Они потребляют мало памяти и создают одиночные переменные - без копий исходного массива и промежуточных структур данных. Алгоритмы, требующие дополнительной памяти, называют<em>out-of-place</em>. Прежде чем использовать алгоритм, надо понять, хватит ли на него памяти, и если нет - поискать менее прожорливые альтернативы.</p>
18
<p>Алгоритмы, которые используют исходный массив как рабочее пространство, называют<em>in-place</em>. Они потребляют мало памяти и создают одиночные переменные - без копий исходного массива и промежуточных структур данных. Алгоритмы, требующие дополнительной памяти, называют<em>out-of-place</em>. Прежде чем использовать алгоритм, надо понять, хватит ли на него памяти, и если нет - поискать менее прожорливые альтернативы.</p>
19
<p>Статья написана на основе<a>треда</a>Валерия в его аккаунте в Twitter. Автор благодарит<a>@usehex</a>за помощь в создании материала.</p>
19
<p>Статья написана на основе<a>треда</a>Валерия в его аккаунте в Twitter. Автор благодарит<a>@usehex</a>за помощь в создании материала.</p>
20
<p>Давайте вспомним 8-й класс и разберёмся: что значит запись<em>O(n)</em>с математической точки зрения. При расчёте Big O Notation используют два правила:</p>
20
<p>Давайте вспомним 8-й класс и разберёмся: что значит запись<em>O(n)</em>с математической точки зрения. При расчёте Big O Notation используют два правила:</p>
21
<p><strong>Константы откидываются.</strong>Нас интересует только часть формулы, которая зависит от размера входных данных. Проще говоря, это само число n, его степени,<a>логарифмы, факториалы и экспоненты</a>, где число находится в степени n.</p>
21
<p><strong>Константы откидываются.</strong>Нас интересует только часть формулы, которая зависит от размера входных данных. Проще говоря, это само число n, его степени,<a>логарифмы, факториалы и экспоненты</a>, где число находится в степени n.</p>
22
<p><strong>Примеры:</strong></p>
22
<p><strong>Примеры:</strong></p>
23
<p><em>O(3n) = O(n)</em></p>
23
<p><em>O(3n) = O(n)</em></p>
24
<p><em>O(10000 n^2) = O(n^2)</em></p>
24
<p><em>O(10000 n^2) = O(n^2)</em></p>
25
<p><em>O(2n * log n) = O(n * log n)</em></p>
25
<p><em>O(2n * log n) = O(n * log n)</em></p>
26
<p><strong>Если в O есть сумма, нас интересует самое быстрорастущее слагаемое.</strong>Это называется асимптотической оценкой сложности.</p>
26
<p><strong>Если в O есть сумма, нас интересует самое быстрорастущее слагаемое.</strong>Это называется асимптотической оценкой сложности.</p>
27
<p><strong>Примеры:</strong></p>
27
<p><strong>Примеры:</strong></p>
28
<p><em>O(n^2 + n) = O(n^2)</em></p>
28
<p><em>O(n^2 + n) = O(n^2)</em></p>
29
<p><em>O(n^3 + 100n * log n) = O(n^3)</em></p>
29
<p><em>O(n^3 + 100n * log n) = O(n^3)</em></p>
30
<p><em>O(n! + 999) = O(n!)</em></p>
30
<p><em>O(n! + 999) = O(n!)</em></p>
31
<p><em>O(1,1^n + n^100) = O(1,1^n)</em></p>
31
<p><em>O(1,1^n + n^100) = O(1,1^n)</em></p>
32
<p>У каждого алгоритма есть худший, средний и лучший<em>сценарии</em>работы - в зависимости от того, насколько удачно выбраны входные данные. Часто их называют<em>случаями</em>.</p>
32
<p>У каждого алгоритма есть худший, средний и лучший<em>сценарии</em>работы - в зависимости от того, насколько удачно выбраны входные данные. Часто их называют<em>случаями</em>.</p>
33
<p><strong>Худший случай (worst case)</strong> - это когда входные данные требуют максимальных затрат времени и памяти.</p>
33
<p><strong>Худший случай (worst case)</strong> - это когда входные данные требуют максимальных затрат времени и памяти.</p>
34
<p>Например, если мы хотим отсортировать массив по возрастанию (Ascending Order, коротко ASC), то для простых алгоритмов сортировки худшим случаем будет массив, отсортированный по убыванию (Descending Order, коротко DESC).</p>
34
<p>Например, если мы хотим отсортировать массив по возрастанию (Ascending Order, коротко ASC), то для простых алгоритмов сортировки худшим случаем будет массив, отсортированный по убыванию (Descending Order, коротко DESC).</p>
35
<p>Для алгоритма поиска элемента в неотсортированном массиве worst case - это когда искомый элемент находится в конце массива или если элемента нет вообще.</p>
35
<p>Для алгоритма поиска элемента в неотсортированном массиве worst case - это когда искомый элемент находится в конце массива или если элемента нет вообще.</p>
36
<p><strong>Лучший случай</strong><strong>(best case)</strong> - полная противоположность worst case, самые удачные входные данные. Правильно отсортированный массив, с которым алгоритму сортировки вообще ничего делать не нужно. В случае поиска - когда алгоритм находит нужный элемент с первого раза.</p>
36
<p><strong>Лучший случай</strong><strong>(best case)</strong> - полная противоположность worst case, самые удачные входные данные. Правильно отсортированный массив, с которым алгоритму сортировки вообще ничего делать не нужно. В случае поиска - когда алгоритм находит нужный элемент с первого раза.</p>
37
<p><strong>Средний случай</strong><strong>(average case)</strong> - самый хитрый из тройки. Интуитивно понятно, что он сидит между best case и worst case, но далеко не всегда понятно, где именно. Часто он совпадает с worst case и всегда хуже best case, если best case не совпадает с worst case. Да, иногда они совпадают.</p>
37
<p><strong>Средний случай</strong><strong>(average case)</strong> - самый хитрый из тройки. Интуитивно понятно, что он сидит между best case и worst case, но далеко не всегда понятно, где именно. Часто он совпадает с worst case и всегда хуже best case, если best case не совпадает с worst case. Да, иногда они совпадают.</p>
38
<p>Как определяют средний<em></em>случай? Считают статистически усреднённый результат: берут алгоритм, прокручивают его с разными данными, составляют сводку результатов и смотрят, вокруг какой функции распределились результаты. В общем, расчёт average case - дело сложное. А мы приступаем к конкретным алгоритмам.</p>
38
<p>Как определяют средний<em></em>случай? Считают статистически усреднённый результат: берут алгоритм, прокручивают его с разными данными, составляют сводку результатов и смотрят, вокруг какой функции распределились результаты. В общем, расчёт average case - дело сложное. А мы приступаем к конкретным алгоритмам.</p>
39
<p>Начнём с самого простого алгоритма -<em>линейного поиска</em>, он же<em>linear search</em>. Дальнейшее объяснение подразумевает, что вы знаете, что такое числа и как устроены массивы. Напомню, это всего лишь набор проиндексированных ячеек.</p>
39
<p>Начнём с самого простого алгоритма -<em>линейного поиска</em>, он же<em>linear search</em>. Дальнейшее объяснение подразумевает, что вы знаете, что такое числа и как устроены массивы. Напомню, это всего лишь набор проиндексированных ячеек.</p>
40
<p>Допустим, у нас есть массив целых чисел<em>arr</em>, содержащий n элементов. Вообще, количество элементов, размер строк, массивов, списков и графов в алгоритмах всегда обозначают буквой<em>n</em>или<em>N</em>. Ещё дано целое число<em>x</em>. Для удобства обусловимся, что<em>arr</em>точно содержит<em>x</em>.</p>
40
<p>Допустим, у нас есть массив целых чисел<em>arr</em>, содержащий n элементов. Вообще, количество элементов, размер строк, массивов, списков и графов в алгоритмах всегда обозначают буквой<em>n</em>или<em>N</em>. Ещё дано целое число<em>x</em>. Для удобства обусловимся, что<em>arr</em>точно содержит<em>x</em>.</p>
41
<p><strong>Задача:</strong>найти, на каком месте в массиве<em>arr</em>находится элемент<em>3</em>, и вернуть его индекс.</p>
41
<p><strong>Задача:</strong>найти, на каком месте в массиве<em>arr</em>находится элемент<em>3</em>, и вернуть его индекс.</p>
42
Фото: Валерий Жила для Skillbox Media<p>Меткий человеческий глаз сразу видит, что искомый элемент содержится в ячейке с индексом 2, то есть в <em>arr[2]</em>. А менее зоркий компьютер будет перебирать ячейки друг за другом:<em>arr[0], arr[1]</em>… и так далее, пока не встретит тройку или конец массива, если тройки в нём нет.</p>
42
Фото: Валерий Жила для Skillbox Media<p>Меткий человеческий глаз сразу видит, что искомый элемент содержится в ячейке с индексом 2, то есть в <em>arr[2]</em>. А менее зоркий компьютер будет перебирать ячейки друг за другом:<em>arr[0], arr[1]</em>… и так далее, пока не встретит тройку или конец массива, если тройки в нём нет.</p>
43
<p>Теперь разберём случаи:</p>
43
<p>Теперь разберём случаи:</p>
44
<p><strong>Worst case.</strong>Больше всего шагов потребуется, если искомое число стоит в конце массива. В этом случае придётся перебрать все n ячеек, прочитать их содержимое и сравнить с искомым числом. Получается, worst case равен<em>O(n)</em>. В нашем массиве худшему случаю соответствует<em>x = 2</em>.</p>
44
<p><strong>Worst case.</strong>Больше всего шагов потребуется, если искомое число стоит в конце массива. В этом случае придётся перебрать все n ячеек, прочитать их содержимое и сравнить с искомым числом. Получается, worst case равен<em>O(n)</em>. В нашем массиве худшему случаю соответствует<em>x = 2</em>.</p>
45
<p><strong>Best case.</strong>Если бы искомое число стояло в самом начале массива, то мы бы получили ответ уже в первой ячейке. Best case линейного поиска -<em>O(1)</em>. Именно так обозначается константное время в Big O Notation. В нашем массиве best case наблюдается при<em>x = 7</em>.</p>
45
<p><strong>Best case.</strong>Если бы искомое число стояло в самом начале массива, то мы бы получили ответ уже в первой ячейке. Best case линейного поиска -<em>O(1)</em>. Именно так обозначается константное время в Big O Notation. В нашем массиве best case наблюдается при<em>x = 7</em>.</p>
46
<p><strong>Average case.</strong>В среднем случае результаты будут равномерно распределены по массиву. Средний элемент можно рассчитать по формуле (n + 1) / 2, но так как мы отбрасываем константы, то получаем просто<em>n.</em>Значит, average case равен<em>O(n)</em>. Хотя иногда в среднем случае константы оставляют, потому что запись O(n / 2) даёт чуть больше информации.</p>
46
<p><strong>Average case.</strong>В среднем случае результаты будут равномерно распределены по массиву. Средний элемент можно рассчитать по формуле (n + 1) / 2, но так как мы отбрасываем константы, то получаем просто<em>n.</em>Значит, average case равен<em>O(n)</em>. Хотя иногда в среднем случае константы оставляют, потому что запись O(n / 2) даёт чуть больше информации.</p>
47
<p>Хорошо, мы уже три минуты обсуждаем linear search, но до сих пор не видели кода. Потерпите, скоро всё будет. Но сперва познакомимся с очень полезным инструментом -<em>псевдокодом</em>.</p>
47
<p>Хорошо, мы уже три минуты обсуждаем linear search, но до сих пор не видели кода. Потерпите, скоро всё будет. Но сперва познакомимся с очень полезным инструментом -<em>псевдокодом</em>.</p>
48
<p>Разработчики пишут на разных языках программирования. Одни похожи друг на друга, а другие - сильно различаются. Часто мы точно знаем, какую операцию хотим выполнить, но не уверены в том, как она выглядит в конкретном языке.</p>
48
<p>Разработчики пишут на разных языках программирования. Одни похожи друг на друга, а другие - сильно различаются. Часто мы точно знаем, какую операцию хотим выполнить, но не уверены в том, как она выглядит в конкретном языке.</p>
49
<p>Возьмём хотя бы получение длины массива. По историческим причинам практически во всех языках эта операция называется по-разному:<em>.length, length(), len(), size(), .size</em> - попробуй угадай! Как же объяснить свой код коллегам, которые пишут на другом языке? Написать программу на псевдокоде.</p>
49
<p>Возьмём хотя бы получение длины массива. По историческим причинам практически во всех языках эта операция называется по-разному:<em>.length, length(), len(), size(), .size</em> - попробуй угадай! Как же объяснить свой код коллегам, которые пишут на другом языке? Написать программу на псевдокоде.</p>
50
<p>Псевдокод - достаточно формальный, но не слишком требовательный к мелочам инструмент для изложения мыслей, не связанный с конкретным языком программирования.</p>
50
<p>Псевдокод - достаточно формальный, но не слишком требовательный к мелочам инструмент для изложения мыслей, не связанный с конкретным языком программирования.</p>
51
<p>Прелесть псевдокода в том, что конкретных правил для его написания нет. Я, например, предпочитаю использовать смесь синтаксиса Python и C: обозначаю вложенность с помощью отступов и называю методы в стиле Python.</p>
51
<p>Прелесть псевдокода в том, что конкретных правил для его написания нет. Я, например, предпочитаю использовать смесь синтаксиса Python и C: обозначаю вложенность с помощью отступов и называю методы в стиле Python.</p>
52
<p>А вот и пример псевдокода для нашей задачи, со всеми допущениями и упрощениями. Метод должен возвращать -1, если<em>arr</em>пуст или не содержит<em>x</em>:</p>
52
<p>А вот и пример псевдокода для нашей задачи, со всеми допущениями и упрощениями. Метод должен возвращать -1, если<em>arr</em>пуст или не содержит<em>x</em>:</p>
53
int linear_search(int[] arr, int x): if arr is empty: return -1 for i in 0..n: if (arr[i] == x): return i return -1 //x was not found<p>В псевдокоде часто используют -1 в качестве invalid index, а если алгоритм возвращает объекты - null, nil (то же самое, что и null) или специальный символ "ничего", похожий на перевёрнутую букву "т". Также встречаются конструкции для исключений, вроде throw error ("Very Bad").</p>
53
int linear_search(int[] arr, int x): if arr is empty: return -1 for i in 0..n: if (arr[i] == x): return i return -1 //x was not found<p>В псевдокоде часто используют -1 в качестве invalid index, а если алгоритм возвращает объекты - null, nil (то же самое, что и null) или специальный символ "ничего", похожий на перевёрнутую букву "т". Также встречаются конструкции для исключений, вроде throw error ("Very Bad").</p>
54
<p>Уметь писать псевдокод полезно. Например, с его помощью можно решать задачи на доске на технических собеседованиях. Я пишу код на бумаге и доске почти так же, как в компьютере, но ещё явно выделяю отступы - иначе строчки кода разъезжаются куда глаза глядят:</p>
54
<p>Уметь писать псевдокод полезно. Например, с его помощью можно решать задачи на доске на технических собеседованиях. Я пишу код на бумаге и доске почти так же, как в компьютере, но ещё явно выделяю отступы - иначе строчки кода разъезжаются куда глаза глядят:</p>
55
Фото: Валерий Жила для Skillbox Media<p>Следующая остановка -<em>binary search</em>, он же<em>бинарный,</em>или<em>двоичный, поиск</em>.</p>
55
Фото: Валерий Жила для Skillbox Media<p>Следующая остановка -<em>binary search</em>, он же<em>бинарный,</em>или<em>двоичный, поиск</em>.</p>
56
<p>В чём отличие бинарного поиска от уже знакомого линейного? Чтобы его применить, массив<em>arr</em>должен быть отсортирован. В нашем случае - по возрастанию.</p>
56
<p>В чём отличие бинарного поиска от уже знакомого линейного? Чтобы его применить, массив<em>arr</em>должен быть отсортирован. В нашем случае - по возрастанию.</p>
57
<p>Часто binary search объясняют на примере с телефонным справочником. Возможно, многие читатели никогда не видели такую приблуду - это большая книга со списками телефонных номеров, отсортированных по фамилиям и именам жителей. Для простоты забудем об именах.</p>
57
<p>Часто binary search объясняют на примере с телефонным справочником. Возможно, многие читатели никогда не видели такую приблуду - это большая книга со списками телефонных номеров, отсортированных по фамилиям и именам жителей. Для простоты забудем об именах.</p>
58
<p>Итак, есть огромный справочник на тысячу страниц с десятками тысяч пар "фамилия - номер", отсортированных по фамилиям. Допустим, мы хотим найти номер человека по фамилии Жила. Как бы мы действовали в случае с линейным поиском? Открыли бы книгу и начали её перебирать, строчку за строчкой, страницу за страницей: Астафьев… Безье… Варнава… Ги… До товарища Жилы он дошёл бы за пару часов, а вот господин Янтарный заставил бы алгоритм попотеть ещё дольше.</p>
58
<p>Итак, есть огромный справочник на тысячу страниц с десятками тысяч пар "фамилия - номер", отсортированных по фамилиям. Допустим, мы хотим найти номер человека по фамилии Жила. Как бы мы действовали в случае с линейным поиском? Открыли бы книгу и начали её перебирать, строчку за строчкой, страницу за страницей: Астафьев… Безье… Варнава… Ги… До товарища Жилы он дошёл бы за пару часов, а вот господин Янтарный заставил бы алгоритм попотеть ещё дольше.</p>
59
<p>Бинарный поиск мудрее и хитрее. Он открывает книгу ровно посередине и смотрит на фамилию, например Мельник - буква "М". Книга отсортирована по фамилиям, и алгоритм знает, что буква "Ж" идёт перед "М".</p>
59
<p>Бинарный поиск мудрее и хитрее. Он открывает книгу ровно посередине и смотрит на фамилию, например Мельник - буква "М". Книга отсортирована по фамилиям, и алгоритм знает, что буква "Ж" идёт перед "М".</p>
60
<p>Алгоритм "разрывает" книгу пополам и выкидывает часть с буквами, которые идут после "М": "<em>Н", "О", "П"… "Я"</em>. Затем открывает оставшуюся половинку посередине - на этот раз на фамилии Ежов. Уже близко, но Ежов не Жила, а ещё буква "Ж" идёт после буквы "Е". Разрываем книгу пополам, а левую половину с буквами от "А" до "Е" выбрасываем. Алгоритм продолжает рвать книгу пополам до тех пор, пока не останется единственная измятая страничка с заветной фамилией и номером.</p>
60
<p>Алгоритм "разрывает" книгу пополам и выкидывает часть с буквами, которые идут после "М": "<em>Н", "О", "П"… "Я"</em>. Затем открывает оставшуюся половинку посередине - на этот раз на фамилии Ежов. Уже близко, но Ежов не Жила, а ещё буква "Ж" идёт после буквы "Е". Разрываем книгу пополам, а левую половину с буквами от "А" до "Е" выбрасываем. Алгоритм продолжает рвать книгу пополам до тех пор, пока не останется единственная измятая страничка с заветной фамилией и номером.</p>
61
<p>Перенесём этот принцип на массивы. У нас есть отсортированный массив<em>arr</em>и число 7, которое нужно найти. Почему поиск называется бинарным? Дело в том, что алгоритм на каждом шаге уменьшает проблему в два раза. Он буквально отрезает на каждом шаге половину<em>arr</em>, в которой гарантированно нет искомого числа.</p>
61
<p>Перенесём этот принцип на массивы. У нас есть отсортированный массив<em>arr</em>и число 7, которое нужно найти. Почему поиск называется бинарным? Дело в том, что алгоритм на каждом шаге уменьшает проблему в два раза. Он буквально отрезает на каждом шаге половину<em>arr</em>, в которой гарантированно нет искомого числа.</p>
62
Фото: Валерий Жила для Skillbox Media<p>На каждом шаге мы проверяем только середину. При этом есть три варианта развития событий:</p>
62
Фото: Валерий Жила для Skillbox Media<p>На каждом шаге мы проверяем только середину. При этом есть три варианта развития событий:</p>
63
<ul><li><strong>попадаем в 7</strong> - тогда проблема решена;</li>
63
<ul><li><strong>попадаем в 7</strong> - тогда проблема решена;</li>
64
<li><strong>нашли число меньше 7</strong> - отрезаем левую половину и ищем в правой половине;</li>
64
<li><strong>нашли число меньше 7</strong> - отрезаем левую половину и ищем в правой половине;</li>
65
<li><strong>нашли число больше 7</strong> - отрезаем правую половину и ищем в левой половине.</li>
65
<li><strong>нашли число больше 7</strong> - отрезаем правую половину и ищем в левой половине.</li>
66
</ul><p>Почему это работает? Вспомните про требование к отсортированности массива, и всё встанет на свои места.</p>
66
</ul><p>Почему это работает? Вспомните про требование к отсортированности массива, и всё встанет на свои места.</p>
67
<p>Итак, смотрим в середину. Карандаш будет служить нам указателем.</p>
67
<p>Итак, смотрим в середину. Карандаш будет служить нам указателем.</p>
68
Фото: Валерий Жила для Skillbox Media<p>В середине находится число 5, оно меньше 7. Значит, отрезаем левую половину и проверенное число. Смотрим в середину оставшегося массива:</p>
68
Фото: Валерий Жила для Skillbox Media<p>В середине находится число 5, оно меньше 7. Значит, отрезаем левую половину и проверенное число. Смотрим в середину оставшегося массива:</p>
69
Фото: Валерий Жила для Skillbox Media<p>В середине число 8, оно больше 7. Значит, отрезаем правую половинку и проверенное число. Остаётся число 7 - как раз его мы и искали. Поздравляю!</p>
69
Фото: Валерий Жила для Skillbox Media<p>В середине число 8, оно больше 7. Значит, отрезаем правую половинку и проверенное число. Остаётся число 7 - как раз его мы и искали. Поздравляю!</p>
70
Фото: Валерий Жила для Skillbox Media<p>Теперь давайте попробуем записать это в виде красивого псевдокода. Как обычно, назовём середину mid и будем перемещать "окно наблюдения", ограниченное двумя индексами -<em>low</em>(левая граница) и <em>high</em>(правая граница).</p>
70
Фото: Валерий Жила для Skillbox Media<p>Теперь давайте попробуем записать это в виде красивого псевдокода. Как обычно, назовём середину mid и будем перемещать "окно наблюдения", ограниченное двумя индексами -<em>low</em>(левая граница) и <em>high</em>(правая граница).</p>
71
int binary_search(int [] arr, int low, int high, int x): if high >= low: mid = round_down((low + high)/2) if arr[mid] == x: // check middle element return mid else if arr[mid] > x: // recursive check left hals return binary_search(arr, low, mid-1, x) else: // recursive check right half return binary_search(arr, mid+1, high, x) else: return -1 // x was not found<p>Алгоритм организован рекурсивно, то есть вызывает сам себя на строках 7 и 9. Есть итеративный вариант с циклом, без рекурсии, но он кажется мне уродливым. Если не находим искомый элемент, возвращаем -1. В начале работы алгоритма значение<em>low</em>совпадает с началом массива, а <em>high</em> - с его концом. И они бегут навстречу друг другу…</p>
71
int binary_search(int [] arr, int low, int high, int x): if high >= low: mid = round_down((low + high)/2) if arr[mid] == x: // check middle element return mid else if arr[mid] > x: // recursive check left hals return binary_search(arr, low, mid-1, x) else: // recursive check right half return binary_search(arr, mid+1, high, x) else: return -1 // x was not found<p>Алгоритм организован рекурсивно, то есть вызывает сам себя на строках 7 и 9. Есть итеративный вариант с циклом, без рекурсии, но он кажется мне уродливым. Если не находим искомый элемент, возвращаем -1. В начале работы алгоритма значение<em>low</em>совпадает с началом массива, а <em>high</em> - с его концом. И они бегут навстречу друг другу…</p>
72
<p>Чтобы запускать алгоритм, не задумываясь о начальных значениях индексов<em>low</em>и <em>high</em>, можно написать такую функцию-обёртку:</p>
72
<p>Чтобы запускать алгоритм, не задумываясь о начальных значениях индексов<em>low</em>и <em>high</em>, можно написать такую функцию-обёртку:</p>
73
int binary_search(int [] arr, int x): if arr_is_empty: return -1 lower_bound == 0 upper_bound == arr.length -1 return binary_search(arr, lower_bound, upper_bound, x)<p>Посчитаем сложность бинарного поиска:</p>
73
int binary_search(int [] arr, int x): if arr_is_empty: return -1 lower_bound == 0 upper_bound == arr.length -1 return binary_search(arr, lower_bound, upper_bound, x)<p>Посчитаем сложность бинарного поиска:</p>
74
<p><strong>Best case.</strong>Как и у линейного поиска, лучший случай равен<em>O(1)</em>, ведь искомое число может находиться в середине массива, и тогда мы найдём его с первой попытки.</p>
74
<p><strong>Best case.</strong>Как и у линейного поиска, лучший случай равен<em>O(1)</em>, ведь искомое число может находиться в середине массива, и тогда мы найдём его с первой попытки.</p>
75
<p><strong>Worst case.</strong>Чтобы найти худший случай, нужно ответить на вопрос: "Сколько раз нужно разделить массив на 2, чтобы в нём остался один элемент?" Или найти минимальное число<em>k</em>, при котором справедливо<em>2^k ≥ n</em>.</p>
75
<p><strong>Worst case.</strong>Чтобы найти худший случай, нужно ответить на вопрос: "Сколько раз нужно разделить массив на 2, чтобы в нём остался один элемент?" Или найти минимальное число<em>k</em>, при котором справедливо<em>2^k ≥ n</em>.</p>
76
<p>Надеюсь, что большинство читателей смогут вычислить<em>k</em>. Но на всякий случай подскажу решение:<em>k = log n</em>по основанию 2 (в алгоритмах практически все логарифмы двоичные). Поэтому worst case бинарного поиска - O(log n).</p>
76
<p>Надеюсь, что большинство читателей смогут вычислить<em>k</em>. Но на всякий случай подскажу решение:<em>k = log n</em>по основанию 2 (в алгоритмах практически все логарифмы двоичные). Поэтому worst case бинарного поиска - O(log n).</p>
77
<p><strong>Average case.</strong>Он тоже равен<em>O(log n)</em>. И если для линейного поиска average case в два раза лучше, чем worst case, то тут они обычно отличаются всего на несколько шагов.</p>
77
<p><strong>Average case.</strong>Он тоже равен<em>O(log n)</em>. И если для линейного поиска average case в два раза лучше, чем worst case, то тут они обычно отличаются всего на несколько шагов.</p>
78
<p>Часто студенты спрашивают: "Зачем нужен линейный поиск, если бинарный обходит его по всем позициям?" Отвечаю: линейный поиск работает с любыми массивами, а бинарный - только с отсортированными.</p>
78
<p>Часто студенты спрашивают: "Зачем нужен линейный поиск, если бинарный обходит его по всем позициям?" Отвечаю: линейный поиск работает с любыми массивами, а бинарный - только с отсортированными.</p>
79
<p>Мы дошли до важного принципа: чем сложнее структура данных, тем более быстрые алгоритмы к ней можно применять. Отсортированный массив - более сложная структура, чем неотсортированный. Забегу вперёд и скажу, что сортировка в общем случае требует от <em>O(n * log(n))</em>до <em>O(n^2)</em>времени.</p>
79
<p>Мы дошли до важного принципа: чем сложнее структура данных, тем более быстрые алгоритмы к ней можно применять. Отсортированный массив - более сложная структура, чем неотсортированный. Забегу вперёд и скажу, что сортировка в общем случае требует от <em>O(n * log(n))</em>до <em>O(n^2)</em>времени.</p>
80
<p>Создать дополнительные структуры данных несложно, вот только это не бесплатное удовольствие. Они едят много памяти. Как правило,<em>O(n).</em>Отсюда вытекает довольно логичный, но обидный вывод: время и память - "взаимообмениваемые" ресурсы. Алгоритм можно ускорить, пожертвовав памятью, или решать задачу медленно, зато in-place. Кроме того, почти всегда есть промежуточный вариант.</p>
80
<p>Создать дополнительные структуры данных несложно, вот только это не бесплатное удовольствие. Они едят много памяти. Как правило,<em>O(n).</em>Отсюда вытекает довольно логичный, но обидный вывод: время и память - "взаимообмениваемые" ресурсы. Алгоритм можно ускорить, пожертвовав памятью, или решать задачу медленно, зато in-place. Кроме того, почти всегда есть промежуточный вариант.</p>
81
<p>Решить одну и ту же проблему зачастую можно тремя способами. Задача разработчика - выбрать самый подходящий.</p>
81
<p>Решить одну и ту же проблему зачастую можно тремя способами. Задача разработчика - выбрать самый подходящий.</p>
82
<p>Мы рассмотрели два алгоритма и увидели примеры их сложности. Но так и не поговорили о том, как эту сложность определять. Есть три основных способа.</p>
82
<p>Мы рассмотрели два алгоритма и увидели примеры их сложности. Но так и не поговорили о том, как эту сложность определять. Есть три основных способа.</p>
83
<p>Первый и наиболее часто используемый способ. Именно так мы определяли сложность linear search и binary search. Обобщим эти примеры.</p>
83
<p>Первый и наиболее часто используемый способ. Именно так мы определяли сложность linear search и binary search. Обобщим эти примеры.</p>
84
<p><strong>Первый случай.</strong>Есть алгоритм<em>some_function</em>, который выполняет действие<em>А</em>, а после него - действие<em>В</em>. На <em>А</em>и <em>В</em>нужно<em>K</em>и <em>J</em>операций соответственно.</p>
84
<p><strong>Первый случай.</strong>Есть алгоритм<em>some_function</em>, который выполняет действие<em>А</em>, а после него - действие<em>В</em>. На <em>А</em>и <em>В</em>нужно<em>K</em>и <em>J</em>операций соответственно.</p>
85
int some_function(): action_A() // K operations action_B() // J operations<p>В случае последовательного выполнения действий сложность алгоритма будет равна<em>O(K + J)</em>, а значит,<em>O(max (K, J))</em>. Например, если<em>А</em>равно<em>n^2</em>, а <em>В</em> -<em>n</em>, то сложность алгоритма будет равна<em>O(n^2 + n)</em>. Но мы уже знаем, что нас интересует только самая быстрорастущая часть. Значит, ответ будет<em>O(n^2).</em></p>
85
int some_function(): action_A() // K operations action_B() // J operations<p>В случае последовательного выполнения действий сложность алгоритма будет равна<em>O(K + J)</em>, а значит,<em>O(max (K, J))</em>. Например, если<em>А</em>равно<em>n^2</em>, а <em>В</em> -<em>n</em>, то сложность алгоритма будет равна<em>O(n^2 + n)</em>. Но мы уже знаем, что нас интересует только самая быстрорастущая часть. Значит, ответ будет<em>O(n^2).</em></p>
86
<p><strong>Второй случай.</strong>Посчитаем сложность действий или вызова методов в циклах. Размер массива равен<em>n</em>, а над каждым элементом будет выполнено действие<i>А (</i>n раз<i>).</i>А дальше всё зависит от "содержимого"<em>A</em>.</p>
86
<p><strong>Второй случай.</strong>Посчитаем сложность действий или вызова методов в циклах. Размер массива равен<em>n</em>, а над каждым элементом будет выполнено действие<i>А (</i>n раз<i>).</i>А дальше всё зависит от "содержимого"<em>A</em>.</p>
87
<p>Посчитаем сложность бинарного поиска:</p>
87
<p>Посчитаем сложность бинарного поиска:</p>
88
int some_function(int [] arr): n = arr.length for i in 0..n: action_A() // K operations<p>Если на каждом шаге<em>A</em>работает с одним элементом, то, независимо от количества операций, получим сложность<em>O(n)</em>. Если же<em>A</em>обрабатывает<em>arr</em>целиком, то алгоритм совершит n операций n раз. Тогда получим<em>O(n *</em><em>n) = O(n^2)</em>. По такой же логике можно получить O(n * log n), O(n^3) и так далее.</p>
88
int some_function(int [] arr): n = arr.length for i in 0..n: action_A() // K operations<p>Если на каждом шаге<em>A</em>работает с одним элементом, то, независимо от количества операций, получим сложность<em>O(n)</em>. Если же<em>A</em>обрабатывает<em>arr</em>целиком, то алгоритм совершит n операций n раз. Тогда получим<em>O(n *</em><em>n) = O(n^2)</em>. По такой же логике можно получить O(n * log n), O(n^3) и так далее.</p>
89
<p><strong>Третий случай - комбо.</strong>Для закрепления соединим оба случая. Допустим, действие<em>А</em>требует<em>log(n)</em>операций, а действие<em>В</em> -<em>n</em>операций. На всякий случай напомню: в алгоритмах всегда идёт речь о двоичных логарифмах.</p>
89
<p><strong>Третий случай - комбо.</strong>Для закрепления соединим оба случая. Допустим, действие<em>А</em>требует<em>log(n)</em>операций, а действие<em>В</em> -<em>n</em>операций. На всякий случай напомню: в алгоритмах всегда идёт речь о двоичных логарифмах.</p>
90
<p>Добавим действие<em>С</em>с пятью операциями и вот что получим:</p>
90
<p>Добавим действие<em>С</em>с пятью операциями и вот что получим:</p>
91
int some_function(int [] arr): n = arr.length for i in 0..n: for j in 0..n: action_A(arr) // log(n) operations action_B(arr) // n operations action_C() // 5 operations<p>O(n * (n * log(n) + n) + 5) = O(n^2 * log(n) + n^2 + 5) = O(n^2 * log(n)).</p>
91
int some_function(int [] arr): n = arr.length for i in 0..n: for j in 0..n: action_A(arr) // log(n) operations action_B(arr) // n operations action_C() // 5 operations<p>O(n * (n * log(n) + n) + 5) = O(n^2 * log(n) + n^2 + 5) = O(n^2 * log(n)).</p>
92
<p>Мы видим, что самая дорогая часть алгоритма - действие<em>А</em>, которое выполняется во вложенном цикле. Поэтому именно оно доминирует в функции.</p>
92
<p>Мы видим, что самая дорогая часть алгоритма - действие<em>А</em>, которое выполняется во вложенном цикле. Поэтому именно оно доминирует в функции.</p>
93
<p>Есть разновидность определения на глаз - амортизационный анализ. Это относительно редкий, но достойный упоминания гость. В двух словах его можно объяснить так: если на <em>X</em>"дешёвых" операций (например, с O(1)) приходится одна "дорогая" (например, с O(n)), то на большом количестве операций суммарная сложность получится неотличимой от <em>O(1)</em>.</p>
93
<p>Есть разновидность определения на глаз - амортизационный анализ. Это относительно редкий, но достойный упоминания гость. В двух словах его можно объяснить так: если на <em>X</em>"дешёвых" операций (например, с O(1)) приходится одна "дорогая" (например, с O(n)), то на большом количестве операций суммарная сложность получится неотличимой от <em>O(1)</em>.</p>
94
<p>Частый пациент амортизационного анализа -<em>динамический массив</em>. Это массив, который при переполнении создаёт новый, больше оригинального в два раза. При этом элементы старого массива копируются в новый.</p>
94
<p>Частый пациент амортизационного анализа -<em>динамический массив</em>. Это массив, который при переполнении создаёт новый, больше оригинального в два раза. При этом элементы старого массива копируются в новый.</p>
95
<p>Практически всегда добавление элементов в такой массив "дёшево" - требует лишь одной операции. Но когда он заполняется, приходится тратить силы: создавать новый массив и копировать<em>N</em>старых элементов в новый. Но так как массив каждый раз увеличивается в два раза, переполнения случаются всё реже и реже, поэтому average case добавления элемента равен<em>O(1)</em>.</p>
95
<p>Практически всегда добавление элементов в такой массив "дёшево" - требует лишь одной операции. Но когда он заполняется, приходится тратить силы: создавать новый массив и копировать<em>N</em>старых элементов в новый. Но так как массив каждый раз увеличивается в два раза, переполнения случаются всё реже и реже, поэтому average case добавления элемента равен<em>O(1)</em>.</p>
96
<p>Слабое место прикидывания на глаз - рекурсия. С ней и правда приходится тяжко. Поэтому для оценки сложности рекурсивных алгоритмов широко используют<a>мастер-теорему</a>.</p>
96
<p>Слабое место прикидывания на глаз - рекурсия. С ней и правда приходится тяжко. Поэтому для оценки сложности рекурсивных алгоритмов широко используют<a>мастер-теорему</a>.</p>
97
<p>По сути, это набор правил по оценке сложности. Он учитывает, сколько новых ветвей рекурсии создаётся на каждом шаге и на сколько частей дробятся данные в каждом шаге рекурсии. Это если вкратце.</p>
97
<p>По сути, это набор правил по оценке сложности. Он учитывает, сколько новых ветвей рекурсии создаётся на каждом шаге и на сколько частей дробятся данные в каждом шаге рекурсии. Это если вкратце.</p>
98
<p><a>Метод Монте-Карло</a>применяют довольно редко - только если первые два применить невозможно. Особенно часто с его помощью описывают производительность систем, состоящих из множества алгоритмов.</p>
98
<p><a>Метод Монте-Карло</a>применяют довольно редко - только если первые два применить невозможно. Особенно часто с его помощью описывают производительность систем, состоящих из множества алгоритмов.</p>
99
<p>Суть метода: берём алгоритм и гоняем его на случайных данных разного размера, замеряем время и память. Полученные измерения выкладываем на отдельные графики для памяти и времени. А затем автоматически вычисляется функция, которая лучше всего описывает полученное облако точек.</p>
99
<p>Суть метода: берём алгоритм и гоняем его на случайных данных разного размера, замеряем время и память. Полученные измерения выкладываем на отдельные графики для памяти и времени. А затем автоматически вычисляется функция, которая лучше всего описывает полученное облако точек.</p>
100
<p>На протяжении всей статьи мы говорили про Big O Notation. А теперь сюрприз: это только одна из пяти существующих нотаций. Вот они слева направо: Намджун, Чонгук, Чингачгук… простите, не удержался. Сверху вниз:<em>Small o</em>,<em>Big O</em>,<em>Big Theta</em>,<em>Big Omega</em>,<em>Small omega</em>.<em>f</em> - это реальная функция сложности нашего алгоритма, а <em>g</em> - асимптотическая.</p>
100
<p>На протяжении всей статьи мы говорили про Big O Notation. А теперь сюрприз: это только одна из пяти существующих нотаций. Вот они слева направо: Намджун, Чонгук, Чингачгук… простите, не удержался. Сверху вниз:<em>Small o</em>,<em>Big O</em>,<em>Big Theta</em>,<em>Big Omega</em>,<em>Small omega</em>.<em>f</em> - это реальная функция сложности нашего алгоритма, а <em>g</em> - асимптотическая.</p>
101
Пять нотаций в математическом представлении. Фото: Валерий Жила для Skillbox Media<p>Несколько слов об этой весёлой компании:</p>
101
Пять нотаций в математическом представлении. Фото: Валерий Жила для Skillbox Media<p>Несколько слов об этой весёлой компании:</p>
102
<ul><li><strong>Big O</strong>обозначает верхнюю границу сложности алгоритма. Это идеальный инструмент для поиска worst case.</li>
102
<ul><li><strong>Big O</strong>обозначает верхнюю границу сложности алгоритма. Это идеальный инструмент для поиска worst case.</li>
103
<li><strong>Big Omega</strong>(которая пишется как подкова) обозначает нижнюю границу сложности, и её правильнее использовать для поиска best case.</li>
103
<li><strong>Big Omega</strong>(которая пишется как подкова) обозначает нижнюю границу сложности, и её правильнее использовать для поиска best case.</li>
104
<li><strong>Big Theta</strong>(пишется как О с чёрточкой) располагается между О и омегой и показывает точную функцию сложности алгоритма. С её помощью правильнее искать average case.</li>
104
<li><strong>Big Theta</strong>(пишется как О с чёрточкой) располагается между О и омегой и показывает точную функцию сложности алгоритма. С её помощью правильнее искать average case.</li>
105
<li><strong>Small o</strong>и<strong>Small omega</strong>находятся по краям этой иерархии и используются в основном для сравнения алгоритмов между собой.</li>
105
<li><strong>Small o</strong>и<strong>Small omega</strong>находятся по краям этой иерархии и используются в основном для сравнения алгоритмов между собой.</li>
106
</ul><p>"Правильнее" в данном контексте означает - с точки зрения математических пейперов по алгоритмам. А в статьях и рабочей документации, как правило, во всех случаях используют "Большое "О“".</p>
106
</ul><p>"Правильнее" в данном контексте означает - с точки зрения математических пейперов по алгоритмам. А в статьях и рабочей документации, как правило, во всех случаях используют "Большое "О“".</p>
107
<p>Если хотите подробнее узнать об остальных нотациях, посмотрите<a>интересный видос</a>на эту тему. Также полезно понимать, как сильно отличаются скорости возрастания различных функций. Вот хороший<a>cheat sheet</a>по сложности алгоритмов и наглядная картинка с графиками оттуда:</p>
107
<p>Если хотите подробнее узнать об остальных нотациях, посмотрите<a>интересный видос</a>на эту тему. Также полезно понимать, как сильно отличаются скорости возрастания различных функций. Вот хороший<a>cheat sheet</a>по сложности алгоритмов и наглядная картинка с графиками оттуда:</p>
108
<a>Сравнение</a>сложности алгоритмов. Скриншот: Валерий Жила для Skillbox Media<p>Хоть картинка и наглядная, она плохо передаёт всю бездну, лежащую между функциями. Поэтому я склепал таблицу со значениями времени для разных функций и N. За время одной операции взял 1 наносекунду:</p>
108
<a>Сравнение</a>сложности алгоритмов. Скриншот: Валерий Жила для Skillbox Media<p>Хоть картинка и наглядная, она плохо передаёт всю бездну, лежащую между функциями. Поэтому я склепал таблицу со значениями времени для разных функций и N. За время одной операции взял 1 наносекунду:</p>
109
Источник: Валерий Жила. Таблица: Евгений Рыбкин / Skillbox Media<p>В последнем разделе поговорим о скрытых константах. Это хитрая штука, там собака зарыта.</p>
109
Источник: Валерий Жила. Таблица: Евгений Рыбкин / Skillbox Media<p>В последнем разделе поговорим о скрытых константах. Это хитрая штука, там собака зарыта.</p>
110
<p>Возьмём умножение матриц. При размерности матрицы n * n наивный алгоритм, который многие знают с начальных курсов универа, "строчка на столбик" имеет кубическую временную сложность<em>O(n^3)</em>. Кубическая, зато честная. Без констант и <em>O(10000</em><em>*</em><em>n^3)</em>под капотом. И памяти не ест - только время.</p>
110
<p>Возьмём умножение матриц. При размерности матрицы n * n наивный алгоритм, который многие знают с начальных курсов универа, "строчка на столбик" имеет кубическую временную сложность<em>O(n^3)</em>. Кубическая, зато честная. Без констант и <em>O(10000</em><em>*</em><em>n^3)</em>под капотом. И памяти не ест - только время.</p>
111
<p>У Валерия есть отдельный<a>тред</a>об умножении матриц.</p>
111
<p>У Валерия есть отдельный<a>тред</a>об умножении матриц.</p>
112
<p>В некоторых алгоритмах умножения матриц степень<em>n</em>сильно "порезана". Самые "быстрые" из них выдают время около<em>O(n^2,37)</em>. Какой персик, правда? Почему бы нам не забыть про "наивный" алгоритм?</p>
112
<p>В некоторых алгоритмах умножения матриц степень<em>n</em>сильно "порезана". Самые "быстрые" из них выдают время около<em>O(n^2,37)</em>. Какой персик, правда? Почему бы нам не забыть про "наивный" алгоритм?</p>
113
<p>Проблема в том, что у таких алгоритмов огромные константы. В пейперах гонятся за более компактной экспонентой, а степени отбрасываются. Я не нашёл внятных значений констант. Даже авторы оригинальных пейперов называют их просто "очень большими".</p>
113
<p>Проблема в том, что у таких алгоритмов огромные константы. В пейперах гонятся за более компактной экспонентой, а степени отбрасываются. Я не нашёл внятных значений констант. Даже авторы оригинальных пейперов называют их просто "очень большими".</p>
114
<p>Давайте от балды возьмём не очень большую константу 100 и сравним<em>n^3</em>c <em>100 * n^2,37</em>. Правая функция даёт выигрыш по сравнению с левой для n, начинающихся с 1495. А ведь мы взяли довольно скромную константу. Подозреваю, что на практике они не в пример больше…</p>
114
<p>Давайте от балды возьмём не очень большую константу 100 и сравним<em>n^3</em>c <em>100 * n^2,37</em>. Правая функция даёт выигрыш по сравнению с левой для n, начинающихся с 1495. А ведь мы взяли довольно скромную константу. Подозреваю, что на практике они не в пример больше…</p>
115
<p>В то же время умножение матриц 1495 × 1495 - очень редкий случай. А матрицы миллиард на миллиард точно нигде не встретишь. Да простит меня Император Душнил с "Хабра" за вольное допущение :)</p>
115
<p>В то же время умножение матриц 1495 × 1495 - очень редкий случай. А матрицы миллиард на миллиард точно нигде не встретишь. Да простит меня Император Душнил с "Хабра" за вольное допущение :)</p>
116
<p>Такие алгоритмы называются галактическими, потому что дают выигрыш только на масштабах, нерелевантных для нас. А в программном умножении матриц, если я правильно помню курс алгоритмов и умею читать "Википедию", очень любят алгоритм Штрассена с его<em>O(2,807)</em>и маленькими константами. Но и те, к сожалению, жрут слишком много памяти.</p>
116
<p>Такие алгоритмы называются галактическими, потому что дают выигрыш только на масштабах, нерелевантных для нас. А в программном умножении матриц, если я правильно помню курс алгоритмов и умею читать "Википедию", очень любят алгоритм Штрассена с его<em>O(2,807)</em>и маленькими константами. Но и те, к сожалению, жрут слишком много памяти.</p>
117
<a><b>Бесплатный курс по Python ➞</b>Мини-курс для новичков и для опытных кодеров. 4 крутых проекта в портфолио, живое общение со спикером. Кликните и узнайте, чему можно научиться на курсе. Смотреть программу</a>
117
<a><b>Бесплатный курс по Python ➞</b>Мини-курс для новичков и для опытных кодеров. 4 крутых проекта в портфолио, живое общение со спикером. Кликните и узнайте, чему можно научиться на курсе. Смотреть программу</a>