HTML Diff
0 added 0 removed
Original 2026-01-01
Modified 2026-03-10
1 <p>Алгоритмы сортировки данных широко используются в программировании для решения различных задач. В этой статье мы рассмотрим несколько основных алгоритмов сортировки данных в массиве.</p>
1 <p>Алгоритмы сортировки данных широко используются в программировании для решения различных задач. В этой статье мы рассмотрим несколько основных алгоритмов сортировки данных в массиве.</p>
2 <p>Для начала давайте вспомним, что<strong>массив</strong>- это структура данных, которая хранит набор значений. Компоненты массива идентифицируются по индексу либо набору индексов, которые принимают целые значения из некоторого непрерывного заданного диапазона.</p>
2 <p>Для начала давайте вспомним, что<strong>массив</strong>- это структура данных, которая хранит набор значений. Компоненты массива идентифицируются по индексу либо набору индексов, которые принимают целые значения из некоторого непрерывного заданного диапазона.</p>
3 <p>Но прежде чем идти дальше и говорить про алгоритмы сортировки, давайте вспомним про метод Swap. Мы введём этот метод для упрощения и улучшения читаемости кода. Он нужен, чтобы менять значения местами в массиве по индексу.</p>
3 <p>Но прежде чем идти дальше и говорить про алгоритмы сортировки, давайте вспомним про метод Swap. Мы введём этот метод для упрощения и улучшения читаемости кода. Он нужен, чтобы менять значения местами в массиве по индексу.</p>
4 void Swap(T[] items, int left, int right) { if (left != right) { T temp = items[left]; items[left] = items[right]; items[right] = temp; } }<p>Что же, теперь можно приступать и к рассмотрению алгоритмов сортировки. Начнём с пузырьковой.</p>
4 void Swap(T[] items, int left, int right) { if (left != right) { T temp = items[left]; items[left] = items[right]; items[right] = temp; } }<p>Что же, теперь можно приступать и к рассмотрению алгоритмов сортировки. Начнём с пузырьковой.</p>
5 <h2>Пузырьковая сортировка данных</h2>
5 <h2>Пузырьковая сортировка данных</h2>
6 <p><strong>Сортировка пузырьком</strong>считается самым простым алгоритмом сортировки. В данном случае алгоритм проходит по массиву несколько раз, причём на каждом этапе происходит перемещение в конец массива самого большого из неотсортированных значений.</p>
6 <p><strong>Сортировка пузырьком</strong>считается самым простым алгоритмом сортировки. В данном случае алгоритм проходит по массиву несколько раз, причём на каждом этапе происходит перемещение в конец массива самого большого из неотсортированных значений.</p>
7 <p>Представим, что у нас есть массив целых чисел:</p>
7 <p>Представим, что у нас есть массив целых чисел:</p>
8 <p>Во время первого прохода по массиву сравниваются значения 3 и 7. Так как семь больше, всё остаётся в первоначальном виде. Далее сравниваются 7 и 4. Т. к. четыре меньше, цифры меняются местами:</p>
8 <p>Во время первого прохода по массиву сравниваются значения 3 и 7. Так как семь больше, всё остаётся в первоначальном виде. Далее сравниваются 7 и 4. Т. к. четыре меньше, цифры меняются местами:</p>
9 <p>В общем, процесс повторяется, пока 7 не дойдёт практически до конца. Почему практически? Потому что, как вы уже наверняка догадались, последний элемент - это 8, который больше семи, поэтому обмен не происходит. Всё чрезвычайно просто:</p>
9 <p>В общем, процесс повторяется, пока 7 не дойдёт практически до конца. Почему практически? Потому что, как вы уже наверняка догадались, последний элемент - это 8, который больше семи, поэтому обмен не происходит. Всё чрезвычайно просто:</p>
10 <p>Однако пока обмен происходит, сортировка продолжается, в результате чего перемещается 6:</p>
10 <p>Однако пока обмен происходит, сортировка продолжается, в результате чего перемещается 6:</p>
11 <p>При очередном проходе обмен не выполняется, т. к. все значения массива расположены по порядку. В итоге алгоритм сортировки пузырьком заканчивает свою работу.</p>
11 <p>При очередном проходе обмен не выполняется, т. к. все значения массива расположены по порядку. В итоге алгоритм сортировки пузырьком заканчивает свою работу.</p>
12 public void Sort(T[] items) { bool swapped; do { swapped = false; for (int i = 1; i &lt; items.Length; i++) { if (items[i - 1].CompareTo(items[i]) &gt; 0) { Swap(items, i - 1, i); swapped = true; } } } while (swapped != false); }<h2>Сортировка данных вставками</h2>
12 public void Sort(T[] items) { bool swapped; do { swapped = false; for (int i = 1; i &lt; items.Length; i++) { if (items[i - 1].CompareTo(items[i]) &gt; 0) { Swap(items, i - 1, i); swapped = true; } } } while (swapped != false); }<h2>Сортировка данных вставками</h2>
13 <p>Эта сортировка работает путём прохождения по массиву и перемещения нужного значения в его начало. Важно помнить, что сортировка обрабатывает элементы массива по порядку. Т. к. алгоритм работает слева направо, становится очевидно, что всё, что находится слева от текущего индекса, - отсортировано. Давайте посмотрим на увеличение отсортированной части массива с каждым последующим проходом:</p>
13 <p>Эта сортировка работает путём прохождения по массиву и перемещения нужного значения в его начало. Важно помнить, что сортировка обрабатывает элементы массива по порядку. Т. к. алгоритм работает слева направо, становится очевидно, что всё, что находится слева от текущего индекса, - отсортировано. Давайте посмотрим на увеличение отсортированной части массива с каждым последующим проходом:</p>
14 <p>По ходу работы отсортированная часть массива растёт, таким образом, в конечном итоге, массив становится упорядоченным.</p>
14 <p>По ходу работы отсортированная часть массива растёт, таким образом, в конечном итоге, массив становится упорядоченным.</p>
15 <p>Приведём пример. Вот неотсортированный массив:</p>
15 <p>Приведём пример. Вот неотсортированный массив:</p>
16 <p>Алгоритм сортировки начнёт работать с индекса "ноль" и значения "три". Т. к. это 1-й индекс, массив до него включительно будет считаться уже отсортированным.</p>
16 <p>Алгоритм сортировки начнёт работать с индекса "ноль" и значения "три". Т. к. это 1-й индекс, массив до него включительно будет считаться уже отсортированным.</p>
17 <p>Потом перейдём к семёрке. Семь больше любого значения в отсортированной части, значит, осуществляется переход к последующему элементу. Отметим, что на прошедшем этапе были отсортированы компоненты с индексами 0..1, про компоненты с индексами 2..n мы пока ничего не знаем.</p>
17 <p>Потом перейдём к семёрке. Семь больше любого значения в отсортированной части, значит, осуществляется переход к последующему элементу. Отметим, что на прошедшем этапе были отсортированы компоненты с индексами 0..1, про компоненты с индексами 2..n мы пока ничего не знаем.</p>
18 <p>Теперь алгоритм проверяет четвёрку. Четыре меньше, чем 7, поэтому переносится на другую, более правильную позицию, которая находится в отсортированной части массива. Позиция определяется методом FindInsertionIndex. Метод сравнивает переданное значение (в нашем случае это 4) с каждым значением из отсортированной части и так до тех пор, пока не будет найдено место для вставки.</p>
18 <p>Теперь алгоритм проверяет четвёрку. Четыре меньше, чем 7, поэтому переносится на другую, более правильную позицию, которая находится в отсортированной части массива. Позиция определяется методом FindInsertionIndex. Метод сравнивает переданное значение (в нашем случае это 4) с каждым значением из отсортированной части и так до тех пор, пока не будет найдено место для вставки.</p>
19 <p>Так для вставки был определён индекс 1. Вставка осуществляется методом Insert. Вставляемое значение удаляется из массива, все остальные цифры сдвигаются вправо, начиная с индекса для вставки. Вот как стал выглядеть массив после сортировки:</p>
19 <p>Так для вставки был определён индекс 1. Вставка осуществляется методом Insert. Вставляемое значение удаляется из массива, все остальные цифры сдвигаются вправо, начиная с индекса для вставки. Вот как стал выглядеть массив после сортировки:</p>
20 <p>Итог работы алгоритма сортировки вставками очевиден:</p>
20 <p>Итог работы алгоритма сортировки вставками очевиден:</p>
21 public void Sort(T[] items) { int sortedRangeEndIndex = 1; while (sortedRangeEndIndex &lt; items.Length) { if (items[sortedRangeEndIndex].CompareTo(items[sortedRangeEndIndex - 1]) &lt; 0) { int insertIndex = FindInsertionIndex(items, items[sortedRangeEndIndex]); Insert(items, insertIndex, sortedRangeEndIndex); } sortedRangeEndIndex++; } } private int FindInsertionIndex(T[] items, T valueToInsert) { for (int index = 0; index &lt; items.Length; index++) { if (items[index].CompareTo(valueToInsert) &gt; 0) { return index; } } throw new InvalidOperationException("The insertion index was not found"); } private void Insert(T[] itemArray, int indexInsertingAt, int indexInsertingFrom) { // itemArray = 0 1 2 4 5 6 3 7 // insertingAt = 3 // insertingFrom = 6 // // Действия: // 1: Сохраняем текущий индекс в temp // 2: Меняем indexInsertingAt на indexInsertingFrom // 3: Меняем indexInsertingAt на indexInsertingFrom в позиции +1 // Сдвигаем элементы влево на один. // 4: Записываем temp на позицию в массиве + 1. // Шаг 1. T temp = itemArray[indexInsertingAt]; // Шаг 2. itemArray[indexInsertingAt] = itemArray[indexInsertingFrom]; // Шаг 3. for (int current = indexInsertingFrom; current &gt; indexInsertingAt; current--) { itemArray[current] = itemArray[current - 1]; } // Шаг 4. itemArray[indexInsertingAt + 1] = temp; }<h2>Сортировка данных выбором</h2>
21 public void Sort(T[] items) { int sortedRangeEndIndex = 1; while (sortedRangeEndIndex &lt; items.Length) { if (items[sortedRangeEndIndex].CompareTo(items[sortedRangeEndIndex - 1]) &lt; 0) { int insertIndex = FindInsertionIndex(items, items[sortedRangeEndIndex]); Insert(items, insertIndex, sortedRangeEndIndex); } sortedRangeEndIndex++; } } private int FindInsertionIndex(T[] items, T valueToInsert) { for (int index = 0; index &lt; items.Length; index++) { if (items[index].CompareTo(valueToInsert) &gt; 0) { return index; } } throw new InvalidOperationException("The insertion index was not found"); } private void Insert(T[] itemArray, int indexInsertingAt, int indexInsertingFrom) { // itemArray = 0 1 2 4 5 6 3 7 // insertingAt = 3 // insertingFrom = 6 // // Действия: // 1: Сохраняем текущий индекс в temp // 2: Меняем indexInsertingAt на indexInsertingFrom // 3: Меняем indexInsertingAt на indexInsertingFrom в позиции +1 // Сдвигаем элементы влево на один. // 4: Записываем temp на позицию в массиве + 1. // Шаг 1. T temp = itemArray[indexInsertingAt]; // Шаг 2. itemArray[indexInsertingAt] = itemArray[indexInsertingFrom]; // Шаг 3. for (int current = indexInsertingFrom; current &gt; indexInsertingAt; current--) { itemArray[current] = itemArray[current - 1]; } // Шаг 4. itemArray[indexInsertingAt + 1] = temp; }<h2>Сортировка данных выбором</h2>
22 <p>Сортировка выбором - некий гибрид между сортировкой вставками и пузырьковой сортировкой. Давайте посмотрим, как работает эта сортировка на нашем массиве:</p>
22 <p>Сортировка выбором - некий гибрид между сортировкой вставками и пузырьковой сортировкой. Давайте посмотрим, как работает эта сортировка на нашем массиве:</p>
23 <p>Во время первого же прохода алгоритм посредством метода FindIndexOfSmallestFromIndex пробует найти самое меньшее значение для перемещения его в начало массива.</p>
23 <p>Во время первого же прохода алгоритм посредством метода FindIndexOfSmallestFromIndex пробует найти самое меньшее значение для перемещения его в начало массива.</p>
24 <p>Так как в нашем примере массив небольшой, мы легко скажем, что это "три", да и вообще, эта цифра уже находится там, где надо. После второго прохода мы получим следующее:</p>
24 <p>Так как в нашем примере массив небольшой, мы легко скажем, что это "три", да и вообще, эта цифра уже находится там, где надо. После второго прохода мы получим следующее:</p>
25 <p>И ещё после 2-х проходов алгоритм сортировки завершит работу:</p>
25 <p>И ещё после 2-х проходов алгоритм сортировки завершит работу:</p>
26 public void Sort(T[] items) { int sortedRangeEnd = 0; while (sortedRangeEnd &lt; items.Length) { int nextIndex = FindIndexOfSmallestFromIndex(items, sortedRangeEnd); Swap(items, sortedRangeEnd, nextIndex); sortedRangeEnd++; } } private int FindIndexOfSmallestFromIndex(T[] items, int sortedRangeEnd) { T currentSmallest = items[sortedRangeEnd]; int currentSmallestIndex = sortedRangeEnd; for (int i = sortedRangeEnd + 1; i &lt; items.Length; i++) { if (currentSmallest.CompareTo(items[i]) &gt; 0) { currentSmallest = items[i]; currentSmallestIndex = i; } } return currentSmallestIndex; }<h2>Сортировка данных слиянием</h2>
26 public void Sort(T[] items) { int sortedRangeEnd = 0; while (sortedRangeEnd &lt; items.Length) { int nextIndex = FindIndexOfSmallestFromIndex(items, sortedRangeEnd); Swap(items, sortedRangeEnd, nextIndex); sortedRangeEnd++; } } private int FindIndexOfSmallestFromIndex(T[] items, int sortedRangeEnd) { T currentSmallest = items[sortedRangeEnd]; int currentSmallestIndex = sortedRangeEnd; for (int i = sortedRangeEnd + 1; i &lt; items.Length; i++) { if (currentSmallest.CompareTo(items[i]) &gt; 0) { currentSmallest = items[i]; currentSmallestIndex = i; } } return currentSmallestIndex; }<h2>Сортировка данных слиянием</h2>
27 <p>Предыдущее алгоритмы - линейные (имея квадратичную сложность, они используют мало памяти). Сортировка слиянием соответствует принципу "Divide and conquer" ("разделяй и властвуй"). Она работает путём разделения крупной задачи на более мелкие, которые решаются проще.</p>
27 <p>Предыдущее алгоритмы - линейные (имея квадратичную сложность, они используют мало памяти). Сортировка слиянием соответствует принципу "Divide and conquer" ("разделяй и властвуй"). Она работает путём разделения крупной задачи на более мелкие, которые решаются проще.</p>
28 <h4>Отвлечёмся на минутку</h4>
28 <h4>Отвлечёмся на минутку</h4>
29 <p>Представьте, что вы работаете на крыше или стройплощадке, и у вас повредился электрокабель, от которого запитывается важный инструмент. Строительные и кровельные кабели очень длинные и часто достигают 100 и более метров. Вам нужно срочно окончить работу, но у вас нет средств диагностики, чтобы починить провод. Неопытные работники просто прекращают все действия, доложив начальству. Мастера-сдельщики режут кабель пополам, получая в 99 % случаев 50 метров работающего провода. Если нужно, они режут пополам и неработающую часть, что позволяет либо достаточно быстро найти место внешнего повреждения, внимательно изучив четверть кабеля (25 м), либо получить в итоге 75 метров, которых хватит для выполнения большинства строительных задач. Всё, что потребуется, - нож и моток изоленты.</p>
29 <p>Представьте, что вы работаете на крыше или стройплощадке, и у вас повредился электрокабель, от которого запитывается важный инструмент. Строительные и кровельные кабели очень длинные и часто достигают 100 и более метров. Вам нужно срочно окончить работу, но у вас нет средств диагностики, чтобы починить провод. Неопытные работники просто прекращают все действия, доложив начальству. Мастера-сдельщики режут кабель пополам, получая в 99 % случаев 50 метров работающего провода. Если нужно, они режут пополам и неработающую часть, что позволяет либо достаточно быстро найти место внешнего повреждения, внимательно изучив четверть кабеля (25 м), либо получить в итоге 75 метров, которых хватит для выполнения большинства строительных задач. Всё, что потребуется, - нож и моток изоленты.</p>
30 <p>Пример достаточно отдалённый, но всё же помогает понять, что такое сортировка слиянием. При выполнении этого алгоритма массив делится пополам до тех пор, пока каждый участок не будет иметь длину в один элемент. Далее участки сливаются (возвращаются на место), но уже в правильном порядке.</p>
30 <p>Пример достаточно отдалённый, но всё же помогает понять, что такое сортировка слиянием. При выполнении этого алгоритма массив делится пополам до тех пор, пока каждый участок не будет иметь длину в один элемент. Далее участки сливаются (возвращаются на место), но уже в правильном порядке.</p>
31 <p>Итак, наш массив:</p>
31 <p>Итак, наш массив:</p>
32 <p>Он делится наполовину:</p>
32 <p>Он делится наполовину:</p>
33 <p>И потом опять, и опять наполовину:</p>
33 <p>И потом опять, и опять наполовину:</p>
34 <p>Потом сливание/соединение в верном порядке:</p>
34 <p>Потом сливание/соединение в верном порядке:</p>
35 <p>И результат:</p>
35 <p>И результат:</p>
36 <p>Алгоритм работает путём реализации следующих операций: 1. Рекурсивное разделение массива на группы с помощью метода Sort. 2. Слияние в верном порядке через метод Merge.</p>
36 <p>Алгоритм работает путём реализации следующих операций: 1. Рекурсивное разделение массива на группы с помощью метода Sort. 2. Слияние в верном порядке через метод Merge.</p>
37 <p>Сортировка слиянием делит и склеивает массив вне зависимости от того, был ли он изначально отсортирован либо нет. Это значит, что данный алгоритм - не самое оптимальное решение, если наш массив уже частично упорядочен и отсортирован (производительность сортировки слиянием может быть ниже, чем у линейного алгоритма).</p>
37 <p>Сортировка слиянием делит и склеивает массив вне зависимости от того, был ли он изначально отсортирован либо нет. Это значит, что данный алгоритм - не самое оптимальное решение, если наш массив уже частично упорядочен и отсортирован (производительность сортировки слиянием может быть ниже, чем у линейного алгоритма).</p>
38 public void Sort(T[] items) { if (items.Length &lt;= 1) { return; } int leftSize = items.Length / 2; int rightSize = items.Length - leftSize; T[] left = new T[leftSize]; T[] right = new T[rightSize]; Array.Copy(items, 0, left, 0, leftSize); Array.Copy(items, leftSize, right, 0, rightSize); Sort(left); Sort(right); Merge(items, left, right); } private void Merge(T[] items, T[] left, T[] right) { int leftIndex = 0; int rightIndex = 0; int targetIndex = 0; int remaining = left.Length + right.Length; while(remaining &gt; 0) { if (leftIndex &gt;= left.Length) { items[targetIndex] = right[rightIndex++]; } else if (rightIndex &gt;= right.Length) { items[targetIndex] = left[leftIndex++]; } else if (left[leftIndex].CompareTo(right[rightIndex]) &lt; 0) { items[targetIndex] = left[leftIndex++]; } else { items[targetIndex] = right[rightIndex++]; } targetIndex++; remaining--; } }<h2>Быстрая сортировка данных</h2>
38 public void Sort(T[] items) { if (items.Length &lt;= 1) { return; } int leftSize = items.Length / 2; int rightSize = items.Length - leftSize; T[] left = new T[leftSize]; T[] right = new T[rightSize]; Array.Copy(items, 0, left, 0, leftSize); Array.Copy(items, leftSize, right, 0, rightSize); Sort(left); Sort(right); Merge(items, left, right); } private void Merge(T[] items, T[] left, T[] right) { int leftIndex = 0; int rightIndex = 0; int targetIndex = 0; int remaining = left.Length + right.Length; while(remaining &gt; 0) { if (leftIndex &gt;= left.Length) { items[targetIndex] = right[rightIndex++]; } else if (rightIndex &gt;= right.Length) { items[targetIndex] = left[leftIndex++]; } else if (left[leftIndex].CompareTo(right[rightIndex]) &lt; 0) { items[targetIndex] = left[leftIndex++]; } else { items[targetIndex] = right[rightIndex++]; } targetIndex++; remaining--; } }<h2>Быстрая сортировка данных</h2>
39 <p>Быстрая сортировка тоже представляет собой алгоритм, отвечающий типу "разделяй и властвуй". Сортировка данных происходит рекурсивно и поэтапно: 1. Выбирается ключевой индекс, массив делится по нему на 2 части. Выбор осуществляется разными способами, мы же возьмём случайное число. 2. Все элементы, которые больше ключевого, перемещаются в правую часть массива, которые меньше - в левую. Теперь ключевой элемент больше любого значения слева и меньше любого значения справа. 3. Первые два шага повторяются до полной сортировки массива.</p>
39 <p>Быстрая сортировка тоже представляет собой алгоритм, отвечающий типу "разделяй и властвуй". Сортировка данных происходит рекурсивно и поэтапно: 1. Выбирается ключевой индекс, массив делится по нему на 2 части. Выбор осуществляется разными способами, мы же возьмём случайное число. 2. Все элементы, которые больше ключевого, перемещаются в правую часть массива, которые меньше - в левую. Теперь ключевой элемент больше любого значения слева и меньше любого значения справа. 3. Первые два шага повторяются до полной сортировки массива.</p>
40 <p>Смотрим на работу алгоритма:</p>
40 <p>Смотрим на работу алгоритма:</p>
41 <p>Выбираем ключевой элемент случайным образом:</p>
41 <p>Выбираем ключевой элемент случайным образом:</p>
42 int pivotIndex = _pivotRng.Next(left, right);<p>И переносим значения справа от ключевого индекса, располагая их в верном положении (используем метод partition).</p>
42 int pivotIndex = _pivotRng.Next(left, right);<p>И переносим значения справа от ключевого индекса, располагая их в верном положении (используем метод partition).</p>
43 <p>Потом повторяем этот процесс и для левой части. Рекурсивно вызываем метод quicksort для каждой из частей. Ключевым элементом слева становится пятерка - она меняет свой индекс при перемещении значений. Важно не забывать, что нас интересует в первую очередь именно ключевое значение, а не его индекс.</p>
43 <p>Потом повторяем этот процесс и для левой части. Рекурсивно вызываем метод quicksort для каждой из частей. Ключевым элементом слева становится пятерка - она меняет свой индекс при перемещении значений. Важно не забывать, что нас интересует в первую очередь именно ключевое значение, а не его индекс.</p>
44 <p>И снова быстрая сортировка:</p>
44 <p>И снова быстрая сортировка:</p>
45 <p>И опять:</p>
45 <p>И опять:</p>
46 <p>В итоге работа алгоритма завершается.</p>
46 <p>В итоге работа алгоритма завершается.</p>
47 Random _pivotRng = new Random(); public void Sort(T[] items) { quicksort(items, 0, items.Length - 1); } private void quicksort(T[] items, int left, int right) { if (left &lt; right) { int pivotIndex = _pivotRng.Next(left, right); int newPivot = partition(items, left, right, pivotIndex); quicksort(items, left, newPivot - 1); quicksort(items, newPivot + 1, right); } } private int partition(T[] items, int left, int right, int pivotIndex) { T pivotValue = items[pivotIndex]; Swap(items, pivotIndex, right); int storeIndex = left; for (int i = left; i &lt; right; i++) { if (items[i].CompareTo(pivotValue) &lt; 0) { Swap(items, i, storeIndex); storeIndex += 1; } } Swap(items, storeIndex, right); return storeIndex; }<p>Статья подготовлена специально для OTUS по материалам "<a>Sorting Algorithms</a>".</p>
47 Random _pivotRng = new Random(); public void Sort(T[] items) { quicksort(items, 0, items.Length - 1); } private void quicksort(T[] items, int left, int right) { if (left &lt; right) { int pivotIndex = _pivotRng.Next(left, right); int newPivot = partition(items, left, right, pivotIndex); quicksort(items, left, newPivot - 1); quicksort(items, newPivot + 1, right); } } private int partition(T[] items, int left, int right, int pivotIndex) { T pivotValue = items[pivotIndex]; Swap(items, pivotIndex, right); int storeIndex = left; for (int i = left; i &lt; right; i++) { if (items[i].CompareTo(pivotValue) &lt; 0) { Swap(items, i, storeIndex); storeIndex += 1; } } Swap(items, storeIndex, right); return storeIndex; }<p>Статья подготовлена специально для OTUS по материалам "<a>Sorting Algorithms</a>".</p>
48 <p><em>Хотите знать больше? Записывайтесь на курс "Алгоритмы для разработчиков"!</em></p>
48 <p><em>Хотите знать больше? Записывайтесь на курс "Алгоритмы для разработчиков"!</em></p>
49  
49