diff --git a/src/sorting_algorithms/__init__.py b/src/sorting_algorithms/__init__.py new file mode 100644 index 0000000..bb87c77 --- /dev/null +++ b/src/sorting_algorithms/__init__.py @@ -0,0 +1,14 @@ +""" +Пакет с алгоритмами сортировки: +- bubble_sort - Сортировка пузырьком +- merge_sort - Сортировка слиянием +- heap_sort - Сортировка кучей +- quick_sort - Быстрая сортировка +""" + +from .bubble_sort import bubble_sort +from .heap_sort import heap_sort +from .merge_sort import merge_sort +from .quick_sort import quick_sort + +__all__ = ["bubble_sort", "merge_sort", "heap_sort", "quick_sort"] diff --git a/src/sorting_algorithms/bubble_sort.py b/src/sorting_algorithms/bubble_sort.py new file mode 100644 index 0000000..f7a12e4 --- /dev/null +++ b/src/sorting_algorithms/bubble_sort.py @@ -0,0 +1,16 @@ +def bubble_sort(arr): + """Сортировка пузырьком (Bubble sort)""" + if len(arr) <= 1: + return arr + + n = len(arr) + for i in range(n - 1): + swapped = False + for j in range(n - i - 1): + if arr[j] > arr[j + 1]: + arr[j], arr[j + 1] = arr[j + 1], arr[j] + swapped = True + if not swapped: + break + + return arr diff --git a/src/sorting_algorithms/heap_sort.py b/src/sorting_algorithms/heap_sort.py new file mode 100644 index 0000000..9bec969 --- /dev/null +++ b/src/sorting_algorithms/heap_sort.py @@ -0,0 +1,38 @@ +def heap_sort(arr): + """Сортировка кучей (Heap Sort)""" + if len(arr) <= 1: + return arr + + n = len(arr) + + # Построение max-heap + for i in range(n // 2 - 1, -1, -1): + heapify(arr, n, i) + + # Извлечение элементов + for i in range(n - 1, 0, -1): + arr[0], arr[i] = arr[i], arr[0] + heapify(arr, i, 0) + + return arr + + +def heapify(arr, n, i): + """Вспомогательная функция для преобразования поддерева в max-heap""" + + while True: + largest = i + left = 2 * i + 1 + right = 2 * i + 2 + + if left < n and arr[left] > arr[largest]: + largest = left + + if right < n and arr[right] > arr[largest]: + largest = right + + if largest == i: + break + + arr[i], arr[largest] = arr[largest], arr[i] + i = largest diff --git a/src/sorting_algorithms/merge_sort.py b/src/sorting_algorithms/merge_sort.py new file mode 100644 index 0000000..4e075a8 --- /dev/null +++ b/src/sorting_algorithms/merge_sort.py @@ -0,0 +1,43 @@ +def merge_sort(arr): + """Сортировка слиянием (Merge sort)""" + if len(arr) <= 1: + return arr + + temp = [0] * len(arr) + merge_sort_recursive(arr, temp, 0, len(arr) - 1) + return arr + + +def merge_sort_recursive(arr, temp, left, right): + """Рекурсивная вспомогательная функция для сортировки слиянием""" + if left >= right: + return + + mid = (left + right) // 2 + + merge_sort_recursive(arr, temp, left, mid) + merge_sort_recursive(arr, temp, mid + 1, right) + + merge(arr, temp, left, mid, right) + + +def merge(arr, temp, left, mid, right): + """Слияние двух отсортированных частей""" + i, j, k = left, mid + 1, left + + for m in range(left, right + 1): + temp[m] = arr[m] + + while i <= mid and j <= right: + if temp[i] <= temp[j]: + arr[k] = temp[i] + i += 1 + else: + arr[k] = temp[j] + j += 1 + k += 1 + + while i <= mid: + arr[k] = temp[i] + i += 1 + k += 1 diff --git a/src/sorting_algorithms/quick_sort.py b/src/sorting_algorithms/quick_sort.py new file mode 100644 index 0000000..50fab8a --- /dev/null +++ b/src/sorting_algorithms/quick_sort.py @@ -0,0 +1,31 @@ +def quick_sort(arr): + """Быстрая сортировка (Quick Sort) - эффективная сортировка с разделением""" + if len(arr) <= 1: + return arr + + quick_sort_recursive(arr, 0, len(arr) - 1) + return arr + + +def quick_sort_recursive(arr, low, high): + """Рекурсивная часть быстрой сортировки""" + if low < high: + pivot_index = _partition(arr, low, high) + + quick_sort_recursive(arr, low, pivot_index - 1) + quick_sort_recursive(arr, pivot_index + 1, high) + + +def _partition(arr, low, high): + """Функция разделения массива - выбирает опорный элемент и переставляет другие (слева - < опорного, справа - > опорного)""" + pivot = arr[high] + + i = low - 1 + + for j in range(low, high): + if arr[j] <= pivot: + i += 1 + arr[i], arr[j] = arr[j], arr[i] + + arr[i + 1], arr[high] = arr[high], arr[i + 1] + return i + 1 diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 0000000..6d2ea33 --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1,76 @@ +""" +Конфигурационный файл pytest с фикстурами +""" + +import random + +import pytest + + +@pytest.fixture +def empty_list(): + """Пустой список""" + return [] + + +@pytest.fixture +def single_element_list(): + """Список с одним элементом""" + return [42] + + +@pytest.fixture +def sorted_list(): + """Отсортированный список""" + return [1, 2, 3, 4, 5, 6, 7, 8, 9, 10] + + +@pytest.fixture +def reverse_sorted_list(): + """Обратно отсортированный список""" + return [10, 9, 8, 7, 6, 5, 4, 3, 2, 1] + + +@pytest.fixture +def list_with_duplicates(): + """Список с повторяющимися элементами""" + return [3, 1, 4, 1, 5, 9, 2, 6, 5, 3, 5] + + +@pytest.fixture +def list_with_negatives(): + """Список с отрицательными числами""" + return [-5, -1, -3, -2, -4, 0, 2, 1, -6] + + +@pytest.fixture() +def random_list_small(): + """Маленький случайный список""" + random.seed(42) + return [random.randint(-100, 100) for _ in range(20)] + + +@pytest.fixture() +def random_list_large(): + """Большой случайный список""" + random.seed(42) + return [random.randint(-10000, 10000) for _ in range(1000)] + + +@pytest.fixture(params=[10, 100, 500]) +def random_list_parametrized(request): + """Списки разных размеров (параметризованная фикстура)""" + random.seed(request.param) + return [random.randint(-1000, 1000) for _ in range(request.param)] + + +@pytest.fixture +def edge_case_all_equal(): + """Спсиок из одинаковых элементов""" + return [7, 7, 7, 7, 7, 7, 7] + + +@pytest.fixture +def edge_case_floats(): + """Список из чисел с плавающей точкой""" + return [1.5, 2.3, 0.1, -1.2, 3.7, -0.5] diff --git a/tests/test_sorting_algorithms.py b/tests/test_sorting_algorithms.py new file mode 100644 index 0000000..0c72011 --- /dev/null +++ b/tests/test_sorting_algorithms.py @@ -0,0 +1,208 @@ +import pytest + +from sorting_algorithms import bubble_sort, heap_sort, merge_sort, quick_sort + +SORTING_ALGORITHMS = [ + pytest.param(bubble_sort, "bubble_sort"), + pytest.param(merge_sort, "merge_sort"), + pytest.param(heap_sort, "heap_sort"), + pytest.param(quick_sort, "quick_sort"), +] + + +class TestSortingAlgorithms: + """Класс обычных тестов и тестов крайних случаев для всех алгоритмов сортировки""" + + @pytest.mark.parametrize("sort_func, name", SORTING_ALGORITHMS) + def test_empty_list(self, sort_func, name, empty_list): + """Тест сортировки пустого списка""" + result = sort_func(empty_list.copy()) + assert result == [], f"{name}: пустой список обработан неверно" + + @pytest.mark.parametrize("sort_func, name", SORTING_ALGORITHMS) + def test_single_element(self, sort_func, name, single_element_list): + """Тест сортировки списка с одним элементом""" + result = sort_func(single_element_list.copy()) + assert result == [42], f"{name}: список из одного элемента обработан неверно" + + @pytest.mark.parametrize("sort_func, name", SORTING_ALGORITHMS) + def test_already_sorted(self, sort_func, name, sorted_list): + """Тест сортировки уже отсортированного списка""" + result = sort_func(sorted_list.copy()) + assert result == sorted_list, f"{name}: отсортированный список изменился" + + @pytest.mark.parametrize("sort_func, name", SORTING_ALGORITHMS) + def test_reverse_sorted(self, sort_func, name, reverse_sorted_list): + """Тест сортировки обратно отсортированного списка""" + result = sort_func(reverse_sorted_list.copy()) + expected = sorted(reverse_sorted_list) + assert result == expected, f"{name}: обратный список отсортирован неверно" + + @pytest.mark.parametrize("sort_func, name", SORTING_ALGORITHMS) + def test_with_duplicates(self, sort_func, name, list_with_duplicates): + """Тест сортировки списка с повторяющимися элементами""" + result = sort_func(list_with_duplicates.copy()) + expected = sorted(list_with_duplicates) + assert result == expected, ( + f"{name}: список с повторяющимися значениями отсортирован неверно" + ) + + @pytest.mark.parametrize("sort_func, name", SORTING_ALGORITHMS) + def test_with_negatives(self, sort_func, name, list_with_negatives): + """Тест сортировки списка с отрицательными числами""" + result = sort_func(list_with_negatives.copy()) + expected = sorted(list_with_negatives) + assert result == expected, ( + f"{name}: список с отрицательными числами отсортирован неверно" + ) + + @pytest.mark.parametrize("sort_func, name", SORTING_ALGORITHMS) + def test_with_random_small(self, sort_func, name, random_list_small): + """Тест сортировки маленького случайного списка""" + result = sort_func(random_list_small.copy()) + expected = sorted(random_list_small) + assert result == expected, ( + f"{name}: маленький случайный список отсортирован неверно" + ) + + @pytest.mark.parametrize("sort_func, name", SORTING_ALGORITHMS) + def test_with_random_large(self, sort_func, name, random_list_large): + """Тест сортировки большого случайного списка""" + result = sort_func(random_list_large.copy()) + expected = sorted(random_list_large) + assert result == expected, ( + f"{name}: большой случайный список отсортирован неверно" + ) + + @pytest.mark.parametrize("sort_func, name", SORTING_ALGORITHMS) + def test_with_parametrized_random(self, sort_func, name, random_list_parametrized): + """Тест сортировки списка разных размеров с параметризованной фикстурой""" + result = sort_func(random_list_parametrized.copy()) + expected = sorted(random_list_parametrized) + assert result == expected, ( + f"{name}: параметризованный список отсортирован неверно" + ) + + +class TestEgdeCasesSortingAlgorithms: + @pytest.mark.parametrize("sort_func, name", SORTING_ALGORITHMS) + def test_edge_case_all_equal(self, sort_func, name, edge_case_all_equal): + """Тест крайнего случая: все элементы одинаковые.""" + result = sort_func(edge_case_all_equal.copy()) + assert result == edge_case_all_equal, f"{name}: одинаковые элементы изменились" + + @pytest.mark.parametrize("sort_func, name", SORTING_ALGORITHMS) + def test_edge_case_floats(self, sort_func, name, edge_case_floats): + """Тест крайнего случая: список из чисел с плавающей точкой""" + result = sort_func(edge_case_floats.copy()) + expected = sorted(edge_case_floats) + assert result == expected, ( + f"{name}: список из чисел с плавающей точкой отсортирован неверно" + ) + + @pytest.mark.parametrize("sort_func, name", SORTING_ALGORITHMS) + def test_none_input_raises(self, sort_func, name): + """Тест на вызов исключения при передаче None""" + with pytest.raises((TypeError, AttributeError)): + sort_func(None) + + @pytest.mark.parametrize("sort_func, name", SORTING_ALGORITHMS) + def test_wrong_type_input_raises(self, sort_func, name): + """Тест на вызов исключения при передача не-списка""" + with pytest.raises((TypeError, AttributeError)): + sort_func("не список") + + +class TestPropertyBasedSortingAlgorithms: + """Класс property-based тестов""" + + @pytest.mark.parametrize("sort_func1, name1", SORTING_ALGORITHMS) + @pytest.mark.parametrize("sort_func2, name2", SORTING_ALGORITHMS) + def test_all_algorithms_consistent( + self, sort_func1, name1, sort_func2, name2, random_list_small + ): + """ + Property-based тест выдачи одинакового результата всеми алгоритмами сортировки""" + arr = random_list_small.copy() + result1 = sort_func1(arr.copy()) + result2 = sort_func2(arr.copy()) + + assert result1 == result2, ( + f"Алгоритмы {name1} и {name2} дали разные результаты:\n" + f"{name1}: {result1}\n{name2}: {result2}" + ) + + @pytest.mark.parametrize("sort_func, name", SORTING_ALGORITHMS) + def test_sorting_properties(self, sort_func, name, random_list_small): + """ + Property-based тест проверки следующих свойств отсортированного массива: + 1. Длина сохраняется + 2. Все элементы из исходного массива присутствуют + 3. Массив отсортирован в неубывающем порядке + """ + arr = random_list_small.copy() + result = sort_func(arr.copy()) + + assert len(result) == len(arr), f"{name}: длина массива изменилась" + + assert sorted(result) == sorted(arr), f"{name}: элементы не сохранились" + + for i in range(len(result) - 1): + assert result[i] <= result[i + 1], ( + f"{name}: массив не отсортирован на позициях {i}, {i + 1}: " + f"{result[i]} > {result[i + 1]}" + ) + + @pytest.mark.parametrize("sort_func, name", SORTING_ALGORITHMS) + def test_idempotency(self, sort_func, name, random_list_small): + """Property-based тест проверики того, что массив не меняется при повторной сортировке""" + + arr = random_list_small.copy() + + sorted_once = sort_func(arr.copy()) + + sorted_twice = sort_func(sorted_once.copy()) + + assert sorted_once == sorted_twice, ( + f"{name}: сортировка меняет уже отсортированный массив" + ) + + +class TestPerformanceSortingAlgorithms: + def test_performance_comparison(self, random_list_large): + """Тест сравнения производительности всех алгоритмов на большом списке.""" + import time + + algorithms = [ + (bubble_sort, "bubble_sort"), + (merge_sort, "merge_sort"), + (heap_sort, "heap_sort"), + (quick_sort, "quick_sort"), + ] + + results = [] + + for sort_func, name in algorithms: + arr_copy = random_list_large.copy() + start_time = time.perf_counter() + sort_func(arr_copy) + end_time = time.perf_counter() + + # Проверяем корректность + assert arr_copy == sorted(random_list_large), f"{name}: сортировка неверна" + + time_taken = end_time - start_time + results.append((name, time_taken)) + + # Сортируем по времени выполнения + results.sort(key=lambda x: x[1]) + + if len(results) > 1: + slowest = results[-1][0] + fastest = results[0][0] + + print(f"\nСамый быстрый: {fastest}") + print(f"Самый медленный: {slowest}") + + if "bubble_sort" in [name for name, _ in results]: + print("bubble_sort один из самых медленных алгоритмов")