diff --git "a/\320\243\321\200\320\276\320\272 7. \320\237\321\200\320\260\320\272\321\202\320\270\321\207\320\265\321\201\320\272\320\276\320\265 \320\267\320\260\320\264\320\260\320\275\320\270\320\265/task_1.py" "b/\320\243\321\200\320\276\320\272 7. \320\237\321\200\320\260\320\272\321\202\320\270\321\207\320\265\321\201\320\272\320\276\320\265 \320\267\320\260\320\264\320\260\320\275\320\270\320\265/task_1.py" index 72a810e8..b2605018 100644 --- "a/\320\243\321\200\320\276\320\272 7. \320\237\321\200\320\260\320\272\321\202\320\270\321\207\320\265\321\201\320\272\320\276\320\265 \320\267\320\260\320\264\320\260\320\275\320\270\320\265/task_1.py" +++ "b/\320\243\321\200\320\276\320\272 7. \320\237\321\200\320\260\320\272\321\202\320\270\321\207\320\265\321\201\320\272\320\276\320\265 \320\267\320\260\320\264\320\260\320\275\320\270\320\265/task_1.py" @@ -1,14 +1,79 @@ -""" -1. Отсортируйте по убыванию методом "пузырька" одномерный целочисленный массив, -заданный случайными числами на промежутке [-100; 100). Выведите на экран -исходный и отсортированный массивы. Сортировка должна быть реализована в -виде функции. Обязательно доработайте алгоритм (сделайте его умнее). - -Идея доработки: если за проход по списку не совершается ни одной сортировки, -то завершение -Обязательно сделайте замеры времени обеих реализаций -и обосновать дала ли оптимизация эффективность - -Подсказка: обратите внимание, сортируем не по возрастанию, как в примере, -а по убыванию -""" +""" +1. Отсортируйте по убыванию методом "пузырька" одномерный целочисленный массив, +заданный случайными числами на промежутке [-100; 100). Выведите на экран +исходный и отсортированный массивы. + +Сортировка должна быть реализована в +виде функции. + +Обязательно доработайте алгоритм (сделайте его умнее)! + +Идея доработки: если за проход по списку не совершается ни одной сортировки, +то завершение +Обязательно сделайте замеры времени обеих реализаций +и обосновать дала ли оптимизация эффективность + +Подсказка: обратите внимание, сортируем не по возрастанию, как в примере, +а по убыванию. + +Сделайте выводы!!! +Опишите в чем была ваша доработка и помогла ли вам доработка?? +""" + +from random import randint +from timeit import timeit + + +def bubbled(alist): + a = alist[::] + for j in range(len(a)-1, 1, -1): + for i in range(j): + if a[i] < a[i+1]: + a[i], a[i+1] = a[i+1], a[i] + return a + + +def smartbubbled(alist): + a = alist[::] + # Добавляем этот флаг ... + issorted = True + for j in range(len(a)-1, 1, -1): + for i in range(j): + if a[i] < a[i+1]: + a[i], a[i+1] = a[i+1], a[i] + # ... и эту проверку + issorted = False + if issorted: + return a + return a + + +SIZE = 1000 + +# сначала проверим на отсортированных данных +a = list(range(999, -1, -1)) +print( + 'с оптимизацией', + timeit("smartbubbled(a)", globals=globals(), number=100)) +print( + 'без оптимизации', + timeit("bubbled(a)", globals=globals(), number=100)) +# --- +# с оптимизацией 0.008331865072250366 +# без оптимизации 3.6351050139637664 +# --- + +# а потом на случайных +a = [randint(-100, 100) for i in range(SIZE)] +print( + 'с оптимизацией', + timeit("smartbubbled(a)", globals=globals(), number=100)) +print( + 'без оптимизации', + timeit("bubbled(a)", globals=globals(), number=100)) +# --- +# с оптимизацией 6.508605795097537 +# без оптимизации 6.436502464930527 +# --- +# Как и ожидалось, получаем небольшой проигрыш +# из-за дополнительной проверки diff --git "a/\320\243\321\200\320\276\320\272 7. \320\237\321\200\320\260\320\272\321\202\320\270\321\207\320\265\321\201\320\272\320\276\320\265 \320\267\320\260\320\264\320\260\320\275\320\270\320\265/task_2.py" "b/\320\243\321\200\320\276\320\272 7. \320\237\321\200\320\260\320\272\321\202\320\270\321\207\320\265\321\201\320\272\320\276\320\265 \320\267\320\260\320\264\320\260\320\275\320\270\320\265/task_2.py" index 73d3dfb6..83837413 100644 --- "a/\320\243\321\200\320\276\320\272 7. \320\237\321\200\320\260\320\272\321\202\320\270\321\207\320\265\321\201\320\272\320\276\320\265 \320\267\320\260\320\264\320\260\320\275\320\270\320\265/task_2.py" +++ "b/\320\243\321\200\320\276\320\272 7. \320\237\321\200\320\260\320\272\321\202\320\270\321\207\320\265\321\201\320\272\320\276\320\265 \320\267\320\260\320\264\320\260\320\275\320\270\320\265/task_2.py" @@ -1,10 +1,250 @@ -""" -2. Отсортируйте по возрастанию методом слияния одномерный вещественный массив, -заданный случайными числами на промежутке [0; 50). Выведите на экран исходный -и отсортированный массивы. - -Пример: -Введите число элементов: 5 -Исходный - [46.11436617832828, 41.62921998361278, 18.45859540989644, 12.128870723745806, 8.025098788570562] -Отсортированный - [8.025098788570562, 12.128870723745806, 18.45859540989644, 41.62921998361278, 46.11436617832828] -""" +""" +2. Отсортируйте по возрастанию методом слияния одномерный вещественный массив, +заданный случайными числами на промежутке [0; 50). Выведите на экран исходный +и отсортированный массивы. + +Хотя в примерах к уроку уже есть вариант реализации слияния, попробуйте предложить другой +(придумать или найти) + +И попытаться сделать замеры на массивах разной длины: 10, 100, 1000, ... + +Пример: +Введите число элементов: 5 +Исходный - [46.11436617832828, 41.62921998361278, 18.45859540989644, 12.128870723745806, 8.025098788570562] +Отсортированный - [8.025098788570562, 12.128870723745806, 18.45859540989644, 41.62921998361278, 46.11436617832828] +""" + +from random import random, randint +from timeit import timeit + +# Это декоратор для замеров времени и памяти +from timememit import timememit + + +# Сначала реализуем функцию слияния двух соседних +# подмассивов внутри большого массива +# r -- массив +# beg -- индекс начала первого массива +# mid -- индекс конца первого масива и начала второго +# end -- индекс конца второго массива +def mergemem(r, beg, mid, end): + # память выделяется здесь + a = r[beg:mid] + b = r[mid:end] + lena = len(a) + lenb = len(b) + ia, ib = 0, 0 + ir = beg + while True: + if a[ia] < b[ib]: + r[ir] = a[ia] + ia += 1 + ir += 1 + if ia == lena: + r[ir:ir + lenb - ib] = b[ib:lenb] + break + else: + r[ir] = b[ib] + ib += 1 + ir += 1 + if ib == lenb: + r[ir:ir + lena - ia] = a[ia:lena] + break + + +# К этому интерфейсу можно адаптировать фрагмент +# функции из конспекта +def mergelect(lst_obj, beg, mid, end): + left = lst_obj[beg:mid] + right = lst_obj[mid:end] + + i, j, k = 0, 0, beg + + while i < len(left) and j < len(right): + if left[i] < right[j]: + lst_obj[k] = left[i] + i += 1 + else: + lst_obj[k] = right[j] + j += 1 + k += 1 + + while i < len(left): + lst_obj[k] = left[i] + i += 1 + k += 1 + + while j < len(right): + lst_obj[k] = right[j] + j += 1 + k += 1 + return None + + +# Реализуем функцию, которая будет рекурсивно сортировать, +# в конце вызывая функцию слияния, полученную в качестве +# параметра +def mergesort(a, beg=0, end=-1, mergefun=mergemem): + if end < 0: + beg = 0 + end = len(a) + diff = end - beg + if diff < 2: + return None + half = diff//2 + mid = beg + half + # оптимизация самого частого случая + if diff == 2: + if a[beg] > a[mid]: + a[beg], a[mid] = a[mid], a[beg] + return None + mergesort(a, beg, mid, mergefun) + mergesort(a, mid, end, mergefun) + mergefun(a, beg, mid, end) + + +# Для сравнения используем пример из конспекта +def merge_sort(lst_obj): + if len(lst_obj) > 1: + center = len(lst_obj) // 2 + # Здесь выделяется память + left = lst_obj[:center] + right = lst_obj[center:] + + merge_sort(left) + merge_sort(right) + + # перестали делить + # выполняем слияние + i, j, k = 0, 0, 0 + + while i < len(left) and j < len(right): + if left[i] < right[j]: + lst_obj[k] = left[i] + i += 1 + else: + lst_obj[k] = right[j] + j += 1 + k += 1 + + while i < len(left): + lst_obj[k] = left[i] + i += 1 + k += 1 + + while j < len(right): + lst_obj[k] = right[j] + j += 1 + k += 1 + return lst_obj + + +# Упростим запуск измерений с различными параметрами +def test_sort(size, nruns): + global arr + arr = [randint(1000, 9999) for _ in range(size)] + + print('оригинальные сортировка и слияние: ', timeit( + "a = arr[:]; mergesort(a)", + globals=globals(), number=nruns)) + + print( + 'оригинальная сортировка, слияние из конспекта: ', + timeit( + "a = arr[:]; mergesort(a, mergefun=mergelect)", + globals=globals(), number=nruns)) + + print('пример из конспекта', timeit( + "a = arr[:]; merge_sort(a)", + globals=globals(), number=nruns)) + + print('встроенная функция', timeit( + "a = arr[:]; a.sort()", + globals=globals(), number=nruns)) + + +# test_sort(1000, 100) +# --- +# оригинальные сортировка и слияние: 0.1758444319711998 +# оригинальная сортировка, слияние из конспекта: 0.23253241798374802 +# пример из конспекта 0.25738181499764323 +# встроенная функция 0.007573371985927224 +# --- +# Сравним с пузырьком +# python task_1.py +# --- +# 6.638946379069239 +# --- + +test_sort(10000, 10) +# --- +# оригинальные сортировка и слияние: 0.2432736890623346 +# оригинальная сортировка, слияние из конспекта: 0.3292464029509574 +# пример из конспекта 0.3471352079650387 +# встроенная функция 0.013397364993579686 +# --- +# Сравним с пузырьком +# python task_1.py +# --- +# 66.67357723996975 +# --- + +# Можно сделать вывод, что наиболее эффективно +# оптимизации работают в рекурсивной функции, +# когда удается сократить число вызовов. +# Оптимизация функции слияния дает существенно +# меньший выигрыш производительности. + + +# Теперь проверим реализацию слияния in-place, которая +# не выделяет дополнительной памяти +def merge(a, beg, mid, end): + while True: + if a[beg] < a[mid]: + beg += 1 + else: + tmp = a[mid] + # Возможно этот цикл ... + for i in range(mid + 1, end): + if a[beg] < a[i]: + break + else: + i = end + # ... вместе с этим копированием дают O(n^2) + a[mid:i - 1] = a[mid + 1:i] + a[i - 1] = a[beg] + a[beg] = tmp + beg += 1 + if beg == mid: + break + + +arr = [randint(1000, 9999) for _ in range(30000)] + + +@timememit +def with_merge(): + a = arr[:] + mergesort(a) + + +@timememit +def with_merge_inplace(): + a = arr[:] + mergesort(a, mergefun=merge) + + +with_merge() +# --- +# with_merge: 0.2094 s, 0.515625 MiB +# --- + +with_merge_inplace() +# --- +# with_merge_inplace: 16.4028 s, 0.515625 MiB +# --- + +# Мы видим, что память в python выделяется очень +# эффективно, поэтому inplace-сортировка слиянием +# не приносит заметного выигрыша в потреблении памяти, +# однако очень сильно ухудшает производительность diff --git "a/\320\243\321\200\320\276\320\272 7. \320\237\321\200\320\260\320\272\321\202\320\270\321\207\320\265\321\201\320\272\320\276\320\265 \320\267\320\260\320\264\320\260\320\275\320\270\320\265/task_3.py" "b/\320\243\321\200\320\276\320\272 7. \320\237\321\200\320\260\320\272\321\202\320\270\321\207\320\265\321\201\320\272\320\276\320\265 \320\267\320\260\320\264\320\260\320\275\320\270\320\265/task_3.py" index cf5789f5..c4469f18 100644 --- "a/\320\243\321\200\320\276\320\272 7. \320\237\321\200\320\260\320\272\321\202\320\270\321\207\320\265\321\201\320\272\320\276\320\265 \320\267\320\260\320\264\320\260\320\275\320\270\320\265/task_3.py" +++ "b/\320\243\321\200\320\276\320\272 7. \320\237\321\200\320\260\320\272\321\202\320\270\321\207\320\265\321\201\320\272\320\276\320\265 \320\267\320\260\320\264\320\260\320\275\320\270\320\265/task_3.py" @@ -1,15 +1,207 @@ -""" -3. Массив размером 2m + 1, где m – натуральное число, заполнен случайным образом. -Найдите в массиве медиану. Медианой называется элемент ряда, делящий его на -две равные части: в одной находятся элементы, которые не меньше медианы, -в другой – не больше медианы. - -Задачу можно решить без сортировки исходного -массива. - -Но если это слишком сложно, то используйте метод сортировки, -который не рассматривался на уроках: Шелла, Гномья, ... - -arr[m] -from statistics import median -""" +""" +3. Массив размером 2m + 1, где m – натуральное число, заполнен случайным образом. +Найдите в массиве медиану. Медианой называется элемент ряда, делящий его на +две равные по длине части: в одной находятся элементы, которые не меньше медианы, +в другой – не больше медианы. + +Задачу можно решить без сортировки исходного +массива. + +Но если это слишком сложно, то используйте метод сортировки, +который не рассматривался на уроках: Шелла, Гномья, Кучей... + +[5, 3, 4, 3, 3, 3, 3] + +[3, 3, 3, 3, 3, 4, 5] + +my_lst +new_lts + +arr[m] + + +from statistics import median + +[3, 4, 3, 3, 5, 3, 3] + + +left.clear() +right.clear() + + +m = 3 +len = 7 +i +left = [] +right = [] + +left == right and + +for i in + for + left == right + left.clear() + right.clear() + + +""" + +from random import randint +from timeit import timeit +import statistics as st +from argparse import ArgumentParser + +p = ArgumentParser("Median functions") +p.add_argument( + '-s', '--size', + type=int, default=10000, + help='Array size') +p.add_argument( + '-n', '--number', + type=int, default=100, + help='Number of timeit runs') +p.add_argument( + '-f', '--firsttry', action='store_true', + help='Run only first function') +args = p.parse_args() + + +# Алгоритм состоит из двух частей. +# Во-первых, реализована основная часть алгоритма +# быстрой сортировки (quicksort), которая делит +# массив на две части по выделенному элементу (pivot). +# Справа от pivot большие числа, слева -- меньшие. +# Во-вторых, применяется бинарный поиск, который +# сужает окно сортировки на каждой итерации +# в зависимости от положения pivot по отношению +# к середине массива. +def median(arr): + m = len(arr)//2 + b = arr[::] + # Вначале делаем "окно" (winright, winleft) равным всему списку + winleft = 0 + winright = len(b) - 1 + while True: + # В начале цикла делаем копию списка, чтобы заполнять ее + # справа и слева + a = b[::] + # Проходим по "окну". + # Берем значение pivot из середины списка и формируем + # новый список, в котором значения меньше pivot + # слева, а больше -- справа. + pivot = a[m] + left = winleft + right = winright + for i in range(winleft, winright + 1): + if i == m: + continue + if a[i] <= pivot: + b[left] = a[i] + left += 1 + else: + b[right] = a[i] + right -= 1 + b[left] = pivot + # В зависимости от того, в какую часть попадает середина списка + # сужаем "окно" или слева, или справа. + # Повторяем до тех пор, пока границы окна не "сойдутся" в середине. + if left == m: + break + if left < m: + winleft = left + 1 + else: + winright = left - 1 + return b[m] + + +# Проверим производительность +arr = [randint(1, 9999) for i in range(2*args.size + 1)] +# Эта переменная нужна, чтобы возвращать значение из timeit +res = [0] + +if args.firsttry: + print( + "встроенная: ", + timeit("res[0] = st.median(arr)", globals=globals(), number=100), + "s, результат: ", + res[0]) + print( + "с копированием: ", + timeit("res[0] = median(arr)", globals=globals(), number=100), + "s, результат: ", + res[0]) +# --- +# встроенная: 0.28833733906503767 s, результат: 5012 +# с копированием: 1.088268078980036 s, результат: 5012 +# --- +# Хуже всего в 3 раза, чем встроенная функция, +# это отличный результат. + + +# Но у нас есть потенциал для оптимизации. Можно +# избежать копирования массива в начале цикла +def median_nocopy(arr): + m = len(arr)//2 + a = arr[::] + winleft = 0 + winright = len(a) - 1 + while True: + # Тогда pivot будет в конце "окна", обозначим его + # квадратными скобками + # 9 7 1 4 6 3 2 8 [5] + # показывает текущий элемент, + # -- границу малых и больших значений + pivot = a[winright] + front = winleft + for i in range(winleft, winright): + # Пока значения больше, чем pivot, просто + # передвигаем индекс текущего элемента + # 9 7 1 4 6 3 2 8 [5] + if a[i] > pivot: + continue + else: + # В противном случае меняем текущий элемент + # местами с числом перед front + # 1 7 9 4 6 3 2 8 [5] + a[i], a[front] = a[front], a[i] + front += 1 + # В конце цикла должно получиться + # 1 4 3 2 [5] 9 7 8 6 + a[winright], a[front] = a[front], a[winright] + # изменение границ окна полностью соответствует + # предыдущей функции + if front == m: + return pivot + if front < m: + winleft = front + 1 + else: + winright = front - 1 + + +if not args.firsttry: + print( + "встроенная: ", + timeit("res[0] = st.median(arr)", globals=globals(), number=100), + "s, результат: ", + res[0]) + print( + "с копированием: ", + timeit("res[0] = median(arr)", globals=globals(), number=100), + "s, результат: ", + res[0]) + print( + "без копирования: ", + timeit( + "res[0] = median_nocopy(arr)", + globals=globals(), number=100), + "s, результат: ", + res[0]) + +# --- +# встроенная: 0.293416955973953 s, результат: 5045 +# с копированием: 0.9642031160183251 s, результат: 5045 +# без копирования: 0.49384179897606373 s, результат: 5045 +# --- + +# Результат улучшился примерно вдвое, и стал медленнее +# встроенной функции всего в 1.6 раз diff --git "a/\320\243\321\200\320\276\320\272 7. \320\237\321\200\320\260\320\272\321\202\320\270\321\207\320\265\321\201\320\272\320\276\320\265 \320\267\320\260\320\264\320\260\320\275\320\270\320\265/timememit.py" "b/\320\243\321\200\320\276\320\272 7. \320\237\321\200\320\260\320\272\321\202\320\270\321\207\320\265\321\201\320\272\320\276\320\265 \320\267\320\260\320\264\320\260\320\275\320\270\320\265/timememit.py" new file mode 100644 index 00000000..c6a8475c --- /dev/null +++ "b/\320\243\321\200\320\276\320\272 7. \320\237\321\200\320\260\320\272\321\202\320\270\321\207\320\265\321\201\320\272\320\276\320\265 \320\267\320\260\320\264\320\260\320\275\320\270\320\265/timememit.py" @@ -0,0 +1,16 @@ +from functools import wraps +from time import perf_counter +from memory_profiler import memory_usage + + +def timememit(fun): + @wraps(fun) + def wrapper_timer(*args): + mem0 = memory_usage()[0] + t0 = perf_counter() + ret = fun(*args) + dmem = memory_usage()[0] - mem0 + dt = perf_counter() - t0 + print(f"{fun.__name__}: {dt:.4f} s, {dmem:4f} MiB") + return ret + return wrapper_timer