• Алгоритмы и структуры данных

      Это ужасно! Я попытался собрать всю нужную информацию и ссылки в один файлик. Надеюсь, это будет хоть сколько-нибудь полезно. За точность не отвечаю, хотя я и пытался проверять. Были использованы следующие материалы и инструменты: чей-то украденный конспект (спасибо огромное этому человеку), информация из книги Кормена, старый файлик с ответами, Gemini-1.5-Pro.

      Плейлист на YouTube с записями (почти) всех лекций 2024 года: https://www.youtube.com/playlist?list=PL8bFx8PltE4MSAk_SaW_MSnpVO91Cw-a1. Ссылки на видео с лекций ниже приведены с учетом таймкодов (но я это не доделал).

      Книга Кормена c поиском: https://disk.yandex.ru/d/6PhppDCEL-5M0g

      Задания с ответами в самом конце!!!

      Перед использованием просьба провести фактчекинг

      :)

      PDF-версия: ADS.pdf; исходник на markdown: АСД.md

      Программа курса

      1. Основы анализа алгоритмов. Асимптотический анализ верхней и средней оценок сложности алгоритмов; сравнение наилучших, средних и наихудших оценок; O-, o-, ω- и θ-нотации; эмпирические измерения эффективности алгоритмов; накладные расходы алгоритмов по времени и памяти.

      2. Рекуррентные соотношения и анализ рекурсивных алгоритмов. Определение, методы подстановки, итераций, деревья рекурсии. Основная теорема о рекуррентных соотношениях.

      3. Динамические структуры данных. Линейные списки. Деревья: двоичные, деревья поиска, ветвящиеся деревья. Способы представления. Сбалансированные деревья: AVL-деревья, B-деревья, Красно-черные деревья, идеально сбалансированное дерево. Сравнение стоимости операций – асимптотические и эмпирические оценки.

      4. Амортизационный анализ. (Помнить, зачем это надо). Учётные стоимости операций, использование при оценке времени работы алгоритмов. Методы группировки, предоплаты, потенциалов. Требования к потенциальной функции. Системы непересекающихся множеств. Основные операции, оценка амортизированной стоимости операций.

      5. Графы. Представление графов – матрица смежности, списки смежности. Стратегии поиска в ширину, в глубину. Алгоритмы Дейкстры, Крускала, Прима. Топологическая сортировка. Алгоритм поиска сильно связных компонент.

      6. Хэш-функции, хэш-таблицы. Открытая адресация, таблицы на основе цепочек. Базовые виды хеш-функций. Проблемы кластеризации, удаления элементов. Реализация структуры данных «словарь» на основе хэш-таблицы. Хопскотч, Робин-Гуд, двухвыборное (не то же, что и двойное!!), кукушкино хеширование.

      7. Кучи. Двоичные кучи, сортировка с помощью двоичной кучи, оценка эффективности.

      8. Стратегии переборных алгоритмов. Полный перебор, метод ветвей и границ на примере задачи коммивояжера.

      9. Динамическое программирование. Признаки применимости, общая стратегия. Стратегии «сверху вниз» (используя механизм мемоизации) и «снизу вверх». Алгоритм перемножения матриц, поиск наибольшей общей подпоследовательности, задача о рюкзаке.

      10. Жадные алгоритмы. Признаки применимости жадных алгоритмов, задача о рюкзаке (непрерывный варианты).

      11. Алгоритмы поиска подстрок. «Наивный» алгоритм, алгоритмы РабинаКарпа, Кнута-Морриса-Пратта, Бойера-Мура (общая идея). Поиск подстрок с помощью конечных автоматов.

      Примечания: в алгоритмах знать оценки, сильные слабые стороны, при каких сценариях выгоднее; + биноминальные деревья и кучи, фиббоначиевы (представление о них) + сортировка подсчётом.

      1. Основы анализа алгоритмов

      Анализ алгоритмов — это процесс определения количества ресурсов, необходимых для выполнения алгоритма. Обычно оценивается время выполнения, но также могут быть рассмотрены и другие ресурсы, такие как память, пропускная способность сети или необходимое аппаратное обеспечение.

      Асимптотический анализ

      Асимптотический анализ фокусируется на поведении алгоритма при увеличении размера входных данных до бесконечности. Он позволяет абстрагироваться от деталей реализации и аппаратного обеспечения, сосредоточившись на порядке роста времени выполнения.

      Оценка сложности алгоритмов

      Сравнение оценок сложности

      Асимптотические обозначения

      В лекциях: https://youtu.be/Ju2tp7S-jfc?si=ZrUFi4eUqWcCT_Jd&t=1594

      Нотация Бахмана–Ландау:

      Замечания!

      Эмпирические измерения

      Асимптотический анализ дает теоретическую оценку времени выполнения, но на практике важно проводить эмпирические измерения. Для этого алгоритм реализуется в виде программы и запускается на реальном компьютере с различными наборами входных данных. Измеряется фактическое время выполнения, которое затем сопоставляется с теоретическими оценками.

      Накладные расходы

      При анализе алгоритма важно учитывать накладные расходы (overhead), связанные с его реализацией. Например, рекурсивный алгоритм может иметь большие накладные расходы, связанные с вызовами функций.

      Накладные расходы по времени

      Включают в себя время, затрачиваемое на выполнение вспомогательных операций, таких как:

      Накладные расходы по памяти

      Включают в себя память, используемую для хранения:

      Пример: Сортировка вставкой

      image-20240626121613654

      Время работы алгоритма.

      T(n)=c1n+c2(n1)+c4(n1)+c5j=2ntj+c6j=2ntj1+c7j=2ntj1+c8(n1)

      В лучшем случае:

      T(n)=c1n+c2(n1)+c4(n1)+c5(n1)+c8(n1)==(c1+c2+c4+c5+c8)n(c2+c4+c5+c8)

      В худшем случае:

      T(n)=c1n+c2(n1)+c4(n1)+c5(n(n+1)21)++c6(n(n+1)2)+c7(n(n+1)2)+c8(n1)==(c52+c62+c72)n2+(c1+c2+c4+c52c62c72+c8)n(c2+c4+c5+c8)

      2. Рекуррентные соотношения и анализ рекурсивных алгоритмов

      Рекуррентные соотношения (recurrence relations) — это уравнения, определяющие последовательность чисел, в которых каждый член последовательности выражается через предыдущие члены. Они часто используются для анализа времени работы рекурсивных алгоритмов, поскольку естественным образом описывают, как время работы задачи размером n зависит от времени работы подзадач меньшего размера.

      Определение

      Рекуррентное соотношение — это соотношение вида:

      T(n)={aT(nb)+f(n),n>1Θ(1),n=1 , где a1 и b1

      f(n) — функция, асимптотически положительная.

      Например, рекуррентное соотношение для времени работы алгоритма сортировки слиянием имеет вид:

      Методы решения рекуррентных соотношений

      Существует несколько методов решения рекуррентных соотношений, т.е. получения асимптотических границ для T(n).

      Метод подстановки

      В методе подстановки (substitution method) мы делаем предположение о виде решения, а затем используем математическую индукцию для доказательства его корректности.

      image-20240626170724953

      image-20240626170819470

      image-20240626170855610

      Метод итераций

      В методе итераций (iteration method) мы многократно подставляем рекуррентное уравнение в само себя, раскрывая рекурсию до тех пор, пока не получим выражение, не содержащее рекурсивных вызовов.

      1. Раскрытие рекурсии: Подставляем рекуррентное уравнение в само себя, заменяя T(ni) на f(T(ni1),T(ni2),,T(nik)).

      2. Упрощение: Упрощаем полученное выражение, объединяя подобные члены.

      3. Поиск закономерности: Ищем закономерность в полученных выражениях и обобщаем её для произвольного уровня рекурсии.

      4. Вычисление суммы: Вычисляем сумму, полученную в результате раскрытия рекурсии.

      тут скорее всего должно быть что-то еще

      Деревья рекурсии

      Дерево рекурсии (recursion tree) — это графическое представление рекурсивных вызовов алгоритма. Каждый узел дерева представляет один рекурсивный вызов, а ребра соединяют вызовы, связанные отношением "родитель-потомок". Стоимость каждого узла — это время работы соответствующего рекурсивного вызова, не включая время, затрачиваемое на рекурсивные вызовы, которые он выполняет.

      1. Построение дерева: Строим дерево рекурсии, начиная с корня, представляющего исходный вызов алгоритма.

      2. Оценка стоимости уровней: Оцениваем стоимость каждого уровня дерева, суммируя стоимости всех узлов на этом уровне.

      3. Вычисление общей стоимости: Суммируем стоимости всех уровней дерева, чтобы получить общую стоимость вычисления.

      image-20240626172159311

      image-20240626172300603

      image-20240626172356002

      image-20240626172412824

      Основная теорема о рекуррентных соотношениях

      Основная теорема (master theorem) предоставляет "кулинарную книгу" для решения рекуррентных соотношений вида:

      T(n)=aT(nb)+f(n)

      где a1 и b>1 — константы, а f(n) — асимптотическая функция.

      Основная теорема гласит следующее:

      1. Если f(n)=O(nlogbaϵ) для некоторого ϵ>0, то T(n)=Θ(nlogba)

      2. Если f(n)=Θ(nlogba), то T(n)=Θ(nlogbalgn)

      3. Если f(n)=Ω(nlogba+ϵ) для некоторого ϵ>0, и если af(nb)cf(n) для некоторого c<1 и достаточно больших n, то T(n)=Θ(f(n))

      Замечание!

      n2<n2logn, но n2logn<{n2+1n2+0.00001n2+ϵ

      Примеры решения рекуррентных соотношений с использованием основной теоремы

      Пример 1

      Рассмотрим рекуррентное соотношение для сортировки слиянием:

      T(n)=2T(n2)+n

      Здесь a=2, b=2, и f(n)=n.

      Следовательно, c=logba, и по второй части основной теоремы имеем:

      T(n)=Θ(nlgn)

      Пример 2

      Рассмотрим рекуррентное соотношение:

      T(n)=3T(n4)+n2

      Здесь a=3, b=4, и f(n)=n2.

      Поскольку c>logba, по третьей части основной теоремы имеем:

      T(n)=Θ(n2)

      3. Динамические структуры данных

      Динамические структуры данных — это структуры данных, которые могут изменяться во время выполнения программы: элементы могут добавляться, удаляться или изменяться. Это отличает их от статических структур данных, таких как массивы, размер которых фиксирован.

      Линейные списки

      Линейные списки — это структуры данных, в которых элементы упорядочены линейно. Каждый элемент содержит данные и ссылку на следующий элемент списка.

      Способы представления

      Стоимость операций

      ОперацияОдносвязный списокДвусвязный список
      Вставка в началоO(1)O(1)
      Вставка в конецO(n)O(1)
      УдалениеO(n)O(1)
      ПоискO(n)O(n)

      Алгоритм поиска

      Вставка в двусвязный список (в начало)

      Вставка в односвязный список (в начало)

      Вставка в конец односвязного списка

      Вставка в конец двусвязного списка

      Удаление из односвязного списка

      Удаление из двусвязного списка

      Деревья

      Деревья — это нелинейные структуры данных, в которых элементы организованы иерархически. Каждый элемент называется узлом, а связи между узлами — ребрами.

      Способы хранения деревьев (!дубль)

      1. В deque: каждый элемент знает только своего родителя.

      2. В обычном массиве. Родитель ребёнок, восстанавливаются по индексам. НО! Только когда дерево заполнено полностью, за исключением, быть может, нижнего уровня. Формула для родителя: i12 Левый дочерний: 2i+1 Правый дочерний: 2i+2

      3. С помощью узлов. Стоимость доступа O(1).

        NODE 4
        NODE 3
        NODE 2
        NODE 1
        parent
        data
        left child
        right child
        parent
        data
        left child
        right child
        parent
        data
        left child
        right child
        parent
        data
        left child
        right child

         

      Двоичные деревья

      Двоичное дерево — это дерево, в котором каждый узел имеет не более двух дочерних узлов.

      Деревья поиска

      Дерево поиска — это двоичное дерево, в котором для каждого узла x выполняется следующее свойство: все ключи в левом поддереве x меньше x.key, а все ключи в правом поддереве x больше x.key.

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10

       

      Замечания!

      Элементы по порядку записи в массиве:

      lower_bound — первый элемент, больший либо равный x

      upper_bound — первый элемент, больший x

      Тогда в диапазоне от lower_bound до upper_bound будут располагаться все дубликаты.

      Операции БДП
      Полный и слабый порядок

      Note

      Чтобы элементы могли находиться в set (и прочем подобном), необходимо, чтобы на этих элементах была определена операция «меньше». set может также работать с компаратором «больше», но не «меньше либо равно», «больше либо равно».

      Введем на произвольном множестве бинарное отношение:

      Note

      Weak ordering не используется в структурах по типу set — необходимо более строгое ограничение. multiset позволяет поддерживать неполный порядок.

      Strict weak ordering (отношение строго частичного порядка) — бинарное отношение на элементах множества со следующими свойствами:

      1. Irreflexivity: xS  cmp(x,x)=false

      2. Assymetry: x,yS  cmp(x,y)=true  cmp(y,x)=false

      3. Transitivity: x,y,zS  cmp(x,y)cmp(y,z)=true  cmp(x,z)=true

      4. Несравнимость: x,y,zS cmp(x,y)cmp(y,z)=true;cmp(y,z)cmp(z,y)=false;cmp(x,z)cmp(z,x)=false

      Замечание. true и false здесь заменяют «элементы связаны / не связаны отношением».

      Ветвящиеся деревья

      Ветвящееся дерево — это дерево, в котором каждый узел может иметь произвольное количество дочерних узлов.

      Способы представления

      1. Массив

      Этот способ часто используется для представления полных бинарных деревьев, таких как пирамиды (heaps). В таком представлении:

      Пример

      Рассмотрим полное бинарное дерево:

      10
      15
      30
      40
      50
      100
      40

      Его представление в массиве будет:

      2. Указатели

      Этот способ используется для представления произвольных деревьев, в которых каждый узел содержит указатели на своих дочерних узлов. В бинарном дереве каждый узел имеет два указателя: на левого и правого потомка.

      Пример

      Рассмотрим бинарное дерево:

      10
      15
      30
      40

      Каждый узел будет представляться структурой:

      И представление дерева будет выглядеть так:

      3. Левый потомок, правый брат

      Этот способ используется для представления ветвящихся деревьев, где узлы могут иметь произвольное количество потомков. Каждый узел имеет два указателя: на своего первого (левого) потомка и на следующего (правого) брата.

      Пример

      Рассмотрим дерево:

      1
      2
      3
      4
      5
      6

      Каждый узел будет представляться структурой:

      И представление дерева будет выглядеть так:

      В этом представлении:

      Таким образом, указанный способ позволяет эффективно представлять деревья с произвольным числом потомков для каждого узла.

      Сбалансированные деревья

      [Более строгое определение] Сбалансированное дерево — это дерево поиска, в котором для любого узла количество элементов в левом и правом поддереве различается не более, чем на 1.

      [Менее строгое определение] Дерево называется сбалансированным, если для любого узла x верно, что |height(x.left)height(x.right)|1

      При вставке или удалении требуется перестроение дерева. Таким образом, O(1). Чтобы сохранить O(logn), ослабим требование сбалансированности дерева.

      Красно-черные деревья

      RB-tree — частный случай B-tree.

      Красно-черное дерево представляет собой бинарное дерево поиска с одним дополнительным битом цвета в каждом узле. Цвет узла может быть либо красным (RED), либо черным (BLACK). В соответствии с накладываемыми на узлы дерева ограничениями ни один простой путь от корня в красно-черном дереве не отличается от другого по длине более чем в два раза, так что красно-черные деревья являются приближенно сбалансированными.

      Каждый узел дерева содержит атрибуты color, key, left, right и р. Если не существует дочернего или родительского узла по отношению к данному, соответствующий указатель принимает значение NIL. Мы будем рассматривать эти значения МП. как указатели на внешние узлы (листья) бинарного дерева поиска. При этом все «нормальные» узлы, содержащие поле ключа, становятся внутренними узлами дерева.

      Бинарное дерево поиска является красно-черным деревом, если оно удовлетворяет следующим красно-черным свойствам.

      Important

      1. Каждый узел является либо красным, либо черным.

      2. Корень дерева является черным узлом.

      3. Каждый лист дерева (NIL) является черным узлом.

      4. Если узел красный, то оба его дочерних узла черные.

      5. Для каждого узла все простые пути от него до листьев, являющихся потомками данного узла, содержат одно и то же количество черных узлов.

      image-20240626205933763

      Замечание. Можно обойтись без затрат на цвет, но обычно это не окупается. В худшем случае возможно не более, чем 2-кратное различие по высотам поддеревьев.

      Important

      Лемма. Красно-черное дерево с n внутренними узлами имеет высоту, не превышающую 2lg(n+1).

      Следствие. Такие операции над динамическими множествами, как Search, Minimum, Maximum, Predecessor и Successor, при использовании красно-черных деревьев выполняются за вpeмя O(lgh), поскольку, как показано в главе 12, время работы этих операций на дереве поиска высотой h составляет O(h), а любое красно-черное дерево с n узлами является деревом поиска высотой O(lgh).

      Повороты

      О вставке ниже.

      image-20240626211044250

      image-20240626211145538

      Правое вращение.

       

      Вставка

      Вставляем всегда красный элемент (для сохранения баланса). Но можем получить тогда «локальную проблему» – 2 подряд идущих красных узла. В таком случае, нужно восстановить красно-черные свойства.

      В RB-tree вводится вспомогательная операция вращения (выше).

      image-20240626211525612

      image-20240626211542158

      image-20240626211745382

      Удаление

      image-20240626212346673

      image-20240626212403636

      image-20240626212611800

      Оценка стоимости операций

      Черная высота узла (black height) — количество черных вершин в пути от узла к листу, не считая сам лист (цвет вершины, от которой идем, нас не интересует).

      Лемма 1. В пути от вершины с черной высотой bh(node) к листу не более bh(node) красных вершин высота вершины 2bh(node).

      Лемма 2. КЧД с n внутренними узлами (n узлов без листьев) имеет высоту 2log(n+1).

      Получается, что высота КЧД превосходит высоту идеально сбалансированного дерева не более, чем в 2 раза. Причем при больших n значение высоты КЧД будет стремиться в logn.

      Таким образом, получили, что у всех операций АС O(logn).

      AVL-деревья

      AVL-деревья, названные в честь своих создателей, Адельсона-Вельского и Ландиса, представляют собой самобалансирующиеся бинарные деревья поиска. Они гарантируют логарифмическую сложность основных операций (вставка, удаление, поиск) даже в худшем случае, предотвращая вырождение дерева в линейный список.

      Основные свойства
      1. Свойство бинарного дерева поиска: Для каждого узла все ключи в левом поддереве меньше ключа узла, а все ключи в правом поддереве больше.

      2. Свойство балансировки: Для каждого узла разница высот его левого и правого поддеревьев (называемая баланс-фактором) не превышает 1 (может быть -1, 0 или 1).

      Высота дерева 1.4405log(n+2)0.3277. Доказывается через Фиббоначиевы деревья. По сравнению с КЧД превосходит ИСД на 45%. Для больших n стремится к logn.

      Балансировка

      При вставке или удалении узла баланс дерева может нарушиться. Для восстановления баланса AVL-деревья используют повороты:

      Основные операции

      Для реализации каждая вершина имеет дополнительное поле — высота данного узла.

      1. Вставка O(logn)

        • Вставка нового узла происходит как в обычном бинарном дереве поиска.

        • После вставки проверяется баланс-фактор каждого узла на пути от нового узла до корня.

        • При нарушении баланса выполняется соответствующий поворот.

        • Требует 2 вращения.

      2. Удаление

        • Удаление узла происходит как в обычном бинарном дереве поиска.

        • После удаления проверяется баланс-фактор каждого узла на пути от родителя удаленного узла до корня.

        • При нарушении баланса выполняется соответствующий поворот.

        • Может потребовать вращений в количестве высоты дерева (медленнее, чем КЧД).

      3. Поиск O(logn)

        • Поиск ключа происходит как в обычном бинарном дереве поиска.

        • Быстрее поиска в КЧД.

      При построении случайных AVL-деревьев, наиболее велика вероятность получить идеальное.

      Сравнение с другими деревьями
      ОперацияAVL-деревоБинарное дерево поискаКрасно-черное деревоB-дерево
      ВставкаO(log n)O(n)O(log n)O(log n)
      УдалениеO(log n)O(n)O(log n)O(log n)
      ПоискO(log n)O(n)O(log n)O(log n)
      Преимущества AVL-деревьев
      Недостатки AVL-деревьев
      Замечания по реализации в C++

      Allocator – некий объект, сущность которого используется для выделения и освобождения памяти. Allocator = распределитель памяти.

      Сложность при создании map / set из диапазона:

      B-деревья

      B-деревья, в отличие от AVL-деревьев и красно-черных деревьев, оптимизированы для хранения данных на внешних носителях, таких как жесткие диски. Они отличаются от бинарных деревьев поиска тем, что каждый узел может содержать несколько ключей и указателей на дочерние узлы.

      Основные свойства
      1. Упорядоченность ключей: Ключи в каждом узле хранятся в отсортированном (неубывающем) порядке.

      2. Количество ключей: Каждый узел, кроме корня, содержит от t до 2t1 ключей, где t - минимальная степень B-дерева. Корень может содержать от 1 до 2t1 ключей.

      3. Количество потомков: Каждый узел имеет на один потомок больше, чем ключей.

      4. Сбалансированность: Все листья находятся на одной глубине.

      Высота. Для любого дерева с n (n>0) ключами, высотой h и минимальной степенью t>1 верно: hlogtn+12.

      Асимптотика O(logN).

      Операции
      1. Поиск:

        • Поиск ключа начинается с корня.

        • В каждом узле выполняется поиск ключа среди ключей узла.

        • Если ключ найден, поиск завершается.

        • Если ключ не найден, поиск продолжается в соответствующем поддереве.

      2. Вставка:

        • Новый ключ вставляется в соответствующий лист.

        • Если лист переполнен (содержит 2t ключей), он разбивается на два узла, содержащих по t ключей.

        • Средний ключ из переполненного листа перемещается в родительский узел.

        • Если родительский узел переполнен, он также разбивается, и этот процесс продолжается вверх по дереву до корня.

        • Если корень переполнен, создается новый корень, и высота дерева увеличивается на 1.

      3. Удаление:

        • Удаление ключа происходит из соответствующего узла.

        • Если узел становится недополненным (содержит меньше t ключей), он объединяется с соседним узлом.

        • Если соседний узел также не дополнен, объединение происходит с родительским узлом, и этот процесс продолжается вниз по дереву до листьев.

      Преимущества
      Недостатки
      Сравнение с другими деревьями

      B-деревья, как и AVL-деревья и красно-черные деревья, обеспечивают логарифмическую сложность операций. Однако B-деревья оптимизированы для дискового хранения, а AVL-деревья и красно-черные деревья - для оперативной памяти.

      Применение
      Пример
      L2_3
      L2_2
      L2_1
      L1
      Root
      22|23
      27
      32|33
      37
      12|13
      18
      1|2
      6|7
      9
      5|8
      15
      25|30|35
      10|20

      Идеально сбалансированное дерево

      Идеально сбалансированное дерево — это дерево, в котором все листья находятся на одной глубине.

      Сравнение стоимости операций

      Асимптотические оценки

      ОперацияДвоичное дерево поиска (наихудший случай)Сбалансированное дерево поиска
      ВставкаO(n)O(log n)
      УдалениеO(n)O(log n)
      ПоискO(n)O(log n)
      МинимумO(n)O(log n)
      МаксимумO(n)O(log n)
      ПреемникO(n)O(log n)
      ПредшественникO(n)O(log n)

      И еще неплохая табличка, украденная из чьего-то конспекта.

        ПоискInsert, begI, midI, endДоступ []Издержки
      Массивvector, array,
      deque
      O(N)O(N)
      O(1)
      O(N)
      O(N)
      O(N)
      O(1)
      O(1)0
       list,
      forward list
      O(N)O(1)O(N)O(1)
      O(N)
      O(N)2/1
      RBTset, multiset,
      map, multimap
      O(logN)O(logN)O(logN)O(logN)O(logN)3
      Hash-tableunordered_set,
      unordered_map
      O(1)O(1)O(1)O(1)O(1)0/2

      Замечание. Издержки – на хранение одного элемента.

      Эмпирические оценки

      На практике сбалансированные деревья поиска (например, красно-черные деревья) обычно работают быстрее, чем несбалансированные деревья поиска, даже для небольших наборов данных.

      4. Амортизационный анализ

      Амортизационный анализ — это метод анализа алгоритмов, который позволяет оценить среднее время выполнения операции в последовательности операций. Он особенно полезен, когда в последовательности встречаются как "дешевые", так и "дорогие" операции, и важно понять, как "дорогие" операции влияют на общую производительность.

      Зачем нужен амортизационный анализ?

      Учетные стоимости операций

      В амортизационном анализе каждой операции присваивается учетная стоимость (amortized cost), которая может отличаться от её фактической стоимости. Учетные стоимости выбираются таким образом, чтобы сумма учетных стоимостей для любой последовательности операций была не меньше суммы фактических стоимостей этих операций.

      Методы амортизационного анализа

      1. Метод группировки (aggregate method)

      В методе группировки мы определяем верхнюю границу T(n) для общей стоимости n операций, а затем делим T(n) на n, чтобы получить амортизированную стоимость каждой операции.

      Пример. Рассмотрим динамический массив, который увеличивает свой размер в два раза, когда он заполнен. Операция вставки элемента в массив имеет две возможные стоимости:

      Анализ:

      Еще пример (multistack). Операции push, pop, multipop(k). Пусть сделано n операций, значит всего multipop может вытолкнуть n элементов. Тогда худшая оценка O(n2).

      2. Метод предоплаты (accounting method)

      В методе предоплаты мы "предоплачиваем" за "дорогие" операции, выполняя "дешевые" операции. "Предоплата" накапливается в виде "кредита", который затем используется для оплаты "дорогих" операций.

      Основная идея. ci – фактическая стоимость i-й операции. ci^ – учетная стоимость i-й операции, она может быть больше или меньше фактической. Но i=1nci^i=1nci для любой последовательности. Значит нужно оценивать сверху i=1nci^.

      Пример: мультистек с операцией multipop

      Рассмотрим стек с операциями push, pop и multipop(k), которая удаляет k элементов из стека. Если [c_i^; c_i] – credit. Credit – для того, чтобы, если ci оказалось больше ci^.

      Учетные стоимости:

      Анализ:

      3. Метод потенциалов (potential method)

      В методе потенциалов мы вводим потенциальную функцию Φ, которая отображает состояние структуры данных в неотрицательное число. Амортизированная стоимость операции определяется как сумма её фактической стоимости и изменения потенциальной функции.

      Требования к потенциальной функции:

      Ограничения:

      Замечание. Метод потенциалов является обобщением метода предоплаты.

      Сумма разности потенциалов: i=1nci^=i=1nci+(Φ(Dn)Φ(D0)) /0/i=1nci

      Пример: Бинарный счетчик

      Рассмотрим бинарный счетчик, который хранит число в двоичном виде. Операция "increment" увеличивает число на 1.

      Потенциальная функция: Φ(счетчик) = количество единиц в двоичном представлении числа.

      Анализ:

      Пример: multistack

      Φ(Di) – количество элементов в multistack. Условие на функцию выполняется.

      Получаем O(n), усредненная O(1).

      Системы непересекающихся множеств

      Система непересекающихся множеств (disjoint-set data structure) - это структура данных, которая поддерживает операции над коллекцией непересекающихся множеств.

      Основные операции:

      Оценка амортизированной стоимости операций:

      С помощью амортизационного анализа можно показать, что амортизированная стоимость операций MakeSet, Union и FindSet в системе непересекающихся множеств с использованием объединения по рангу и сжатия пути составляет O(α(n)), где α(n) - обратная функция Аккермана, которая растет очень медленно.

      Для достижения высокой эффективности операций Union и FindSet применяются две эвристики:

      1. Объединение по рангу (Union by Rank): При объединении двух множеств дерево с меньшим рангом присоединяется к дереву с большим рангом. Ранг - это верхняя граница высоты дерева.

      2. Сжатие пути (Path Compression): Во время операции FindSet все узлы на пути от x до корня переподключаются непосредственно к корню.

      Для анализа амортизированной стоимости операций над disjoint-set data structure с использованием объединения по рангу и сжатия пути применим метод потенциалов.

      Определим потенциальную функцию Φ для леса непересекающихся множеств как сумму потенциалов всех узлов в лесу. Потенциал узла x определяется как:

      где α(n) - обратная функция Аккермана, которая растет очень медленно.

      1. MakeSet(x):

        • Фактическая стоимость: O(1)

        • Изменение потенциала: Φ(x)=0 (т.к. x - корень)

        • Амортизированная стоимость: O(1)+0=O(1)

      2. Union(x, y):

        • Фактическая стоимость: O(1) (с учетом объединения по рангу)

        • Изменение потенциала:

          • Если ранги корней равны, то ранг одного из корней увеличивается на 1, что приводит к увеличению потенциала на α(n) в худшем случае.

          • Если ранги корней различны, то потенциал не изменяется.

        • Амортизированная стоимость: O(1)+α(n)=O(α(n))

      3. FindSet(x):

        • Фактическая стоимость: O(k), где k - длина пути от x до корня.

        • Изменение потенциала:

          • Сжатие пути уменьшает потенциал каждого узла на пути от x до корня, кроме корня самого.

          • Суммарное уменьшение потенциала может быть оценено как O(kα(n)).

        • Амортизированная стоимость: O(k)O(kα(n))=O(α(n))

      Итог:

      Амортизированная стоимость операций MakeSet, Union и FindSet в системе непересекающихся множеств с использованием объединения по рангу и сжатия пути составляет O(α(n)).

      Замечания:

      5. Графы

      Графы — это мощные структуры данных, используемые для представления отношений между объектами. Они состоят из вершин (узлов) и ребер, соединяющих вершины. Графы находят широкое применение в различных областях, таких как социальные сети, транспортные системы, компьютерные сети и многие другие.

      Граф — отображение E: V×V{0,1}. Граф — пары (V;E).

      V – vertex (вершина), E - edge (ребро).

      ГРАФЫ
      Ориентированность
      Связность
      Взвешенность

      E=|V|(|V|1)2

      Если |V2||E|, то такой граф полный.

      Если |E|<|V2|, то граф называется разреженным.

      Представление графов

      2
      1
      3
      4

      Матрица смежности

      Матрица смежности - это двумерный массив, где строки и столбцы соответствуют вершинам графа. Элемент матрицы A[i][j] равен 1, если существует ребро между вершинами i и j, и 0 в противном случае.

      Преимущества:

      Недостатки:

       1234
      10110
      21011
      31101
      40110

      Списки смежности

      Списки смежности - это набор списков, где каждый список соответствует вершине графа и содержит её смежные вершины (те, с которыми она соединена ребрами).

      Преимущества:

      Недостатки:

      Пример

      Алгоритмы обхода графов

      2
      1
      3
      4

      Поиск в ширину (BFS)

      Поиск в ширину (Breadth-First Search) - это алгоритм обхода графа, который сначала посещает все вершины, смежные с начальной вершиной, затем все вершины, смежные с этими вершинами, и так далее. BFS использует очередь для хранения вершин, которые нужно посетить.

      Применение:

      Конкретный пример:

      Итого: 1, 2, 3, 4

      opened очередь

      closed HashTable, set (на быстрый поиск)

      Поиск в глубину (DFS)

      Поиск в глубину (Depth-First Search) - это алгоритм обхода графа, который идет "вглубь" графа, посещая вершины, пока не достигнет тупика, а затем возвращается назад и исследует другие ветви. DFS использует стек (или рекурсию) для хранения вершин, которые нужно посетить.

      Применение:

      На примере:

      1. Посещаем вершину 1. Вызываем рекурсивно DFS для соседа 2.

      2. Посещаем вершину 2. Вызываем рекурсивно DFS для непосещенного соседа 4.

      3. Посещаем вершину 4. Вызываем рекурсивно DFS для непосещенного соседа 3.

      4. Посещаем вершину 3. У нее нет непосещенных соседей. Возвращаемся на шаг 3.

      5. На шаге 3 все соседи вершины 4 посещены. Возвращаемся на шаг 2.

      6. На шаге 2 все соседи вершины 2 посещены. Возвращаемся на шаг 1.

      7. На шаге 1 все соседи вершины 1 посещены. Алгоритм завершает работу.

      Итого: 1, 2, 4, 3

      Примечание:

      Алгоритмы поиска кратчайших путей

      Алгоритм Дейкстры O(ElogV)

      Алгоритм Дейкстры находит кратчайшие пути от одной вершины до всех остальных вершин во взвешенном графе с неотрицательными весами ребер. Алгоритм использует очередь с приоритетами для выбора вершины с наименьшим расстоянием от начальной вершины.

      1. Инициализация:

        • Создаем таблицу расстояний distance, где distance[v] - текущее кратчайшее расстояние от вершины A до вершины v. Изначально distance[A] = 0, а для остальных вершин distance[v] = ∞.

        • Создаем множество посещенных вершин visited. Изначально оно пустое.

        • Добавляем вершину A в очередь с приоритетом queue, упорядоченную по возрастанию расстояния.

      2. Итерации: Пока очередь не пуста:

        • Извлекаем из очереди вершину u с наименьшим расстоянием distance[u].

        • Добавляем вершину u в множество visited.

        • Для каждого соседа v вершины u:

          • Если v не посещена и distance[u] + weight(u, v) < distance[v], то обновляем расстояние до v: distance[v] = distance[u] + weight(u, v).

          • Добавляем вершину v в очередь queue.

      3. Результат: После завершения алгоритма distance[F] будет содержать длину кратчайшего пути из A в F.

      Применение:

      Пример.

      2
      4
      1
      3
      6
      2
      3
      A
      B
      C
      D
      E
      F

      Задача:

      Найти кратчайший путь из вершины A в вершину F.

      Шаг Очередь queue (вершина, расстояние) visited distance (A, B, C, D, E, F)
      1 (A, 0) {} (0, ∞, ∞, ∞, ∞, ∞)
      2 (B, 2), (C, 4) {A} (0, 2, 4, ∞, ∞, ∞)
      3 (D, 3), (C, 4), (E, 5) {A, B} (0, 2, 4, 3, 5, ∞)
      4 (C, 4), (E, 5), (F, 5) {A, B, D} (0, 2, 4, 3, 5, 5)
      5 (E, 5), (F, 5) {A, B, C, D} (0, 2, 4, 3, 5, 5)
      6 (F, 5) {A, B, C, D, E} (0, 2, 4, 3, 5, 5)
      7 {} {A, B, C, D, E, F} (0, 2, 4, 3, 5, 5)

      Длина кратчайшего пути из A в F равна 5. Один из возможных путей: A -> B -> D -> F.

      Алгоритмы поиска минимального остовного дерева

      Алгоритм Крускала O(ElogE)

      Алгоритм Крускала находит минимальное остовное дерево (MST) во взвешенном графе. MST - это подграф, содержащий все вершины графа и минимально возможную сумму весов ребер. Алгоритм сортирует ребра по весу и добавляет их в MST, если они не создают циклов

      1. Сортировка ребер: Создаем список всех ребер графа и сортируем его по возрастанию веса:

      1. Инициализация:

        • Создаем пустое множество mst, которое будет хранить ребра MST.

        • Создаем структуру данных для хранения компонент связности вершин (например, систему непересекающихся множеств). Изначально каждая вершина находится в своей компоненте.

      2. Итерации: Проходим по отсортированному списку ребер:

        • Для каждого ребра (u, v):

          • Если вершины u и v находятся в разных компонентах связности:

            • Добавляем ребро (u, v) в mst.

            • Объединяем компоненты связности вершин u и v.

      3. Результат: После обработки всех ребер множество mst будет содержать ребра минимального остовного дерева.

      Пример. Найти минимальное остовное дерево (MST) для этого графа.

      2
      4
      1
      3
      6
      2
      3
      A
      B
      C
      D
      E
      F

      Пошаговое выполнение:

      ШагРебро (u, v, вес)mstКомпоненты связности
      1(B, D, 1){(B, D, 1)}{A}, {B, D}, {C}, {E}, {F}
      2(A, B, 2){(B, D, 1), (A, B, 2)}{A, B, D}, {C}, {E}, {F}
      3(D, F, 2){(B, D, 1), (A, B, 2), (D, F, 2)}{A, B, D, F}, {C}, {E}
      4(B, E, 3){(B, D, 1), (A, B, 2), (D, F, 2), (B, E, 3)}{A, B, D, E, F}, {C}
      5(E, F, 3)Пропускаем, т.к. E и F уже в одной компоненте{A, B, D, E, F}, {C}
      6(A, C, 4){(B, D, 1), (A, B, 2), (D, F, 2), (B, E, 3), (A, C, 4)}{A, B, C, D, E, F}
      7(C, E, 6)Пропускаем, т.к. все вершины уже в одной компоненте{A, B, C, D, E, F}
      2
      4
      1
      3
      2
      A
      B
      C
      D
      E
      F

       

      Алгоритм Прима

      aАлгоритм Прима также находит MST во взвешенном графе. Алгоритм начинает с произвольной вершины и добавляет к MST ребро с наименьшим весом, соединяющее вершину в MST с вершиной вне MST. Стоимость O((V+E)logV) (для реализации с очередью с приоритетом).

      1. Инициализация:

        • Выбираем произвольную вершину в качестве начальной (например, A).

        • Создаем множество mst, которое будет хранить ребра MST. Изначально оно пустое.

        • Создаем таблицу key, где key[v] - вес ребра с минимальным весом, соединяющего вершину v с деревом, построенным на данный момент. Изначально key[A] = 0, а для остальных вершин key[v] = ∞.

        • Создаем массив parent, где parent[v] - вершина, предшествующая вершине v в MST. Изначально parent[A] = None.

        • Создаем очередь с приоритетом queue, содержащую все вершины графа. Приоритет вершины определяется значением key.

      2. Итерации: Пока очередь queue не пуста:

        • Извлекаем из очереди вершину u с минимальным ключом key[u].

        • Для каждого соседа v вершины u:

          • Если v находится в очереди queue и вес ребра (u, v) меньше текущего ключа key[v]:

            • Обновляем ключ: key[v] = weight(u, v).

            • Устанавливаем предка: parent[v] = u.

            • Обновляем приоритет вершины v в очереди queue.

      Пример.

      Шагqueue (вершина, ключ)mstkey (A, B, C, D, E, F)parent (A, B, C, D, E, F)
      1(A, 0), (B, ∞), (C, ∞), (D, ∞), (E, ∞), (F, ∞){}(0, ∞, ∞, ∞, ∞, ∞)(None, None, None, None, None, None)
      2(B, 2), (C, 4), (D, ∞), (E, ∞), (F, ∞){}(0, 2, 4, ∞, ∞, ∞)(None, A, A, None, None, None)
      3(D, 3), (C, 4), (E, 5), (F, ∞){(A, B)}(0, 2, 4, 3, 5, ∞)(None, A, A, B, B, None)
      4(F, 5), (C, 4), (E, 5){(A, B), (B, D)}(0, 2, 4, 3, 5, 5)(None, A, A, B, B, D)
      5(C, 4), (E, 5){(A, B), (B, D), (D, F)}(0, 2, 4, 3, 5, 5)(None, A, A, B, B, D)
      6(E, 5){(A, B), (B, D), (D, F), (A, C)}(0, 2, 4, 3, 5, 5)(None, A, A, B, B, D)
      7{}{(A, B), (B, D), (D, F), (A, C), (B, E)}(0, 2, 4, 3, 5, 5)(None, A, A, B, B, D)
      2
      4
      1
      3
      2
      A
      B
      C
      D
      E
      F

      Топологическая сортировка

      Топологическая сортировка ориентированного ациклического графа (DAG) — это линейное упорядочение всех его вершин таким образом, что для любого ребра (u, v) вершина u стоит раньше вершины v в этом порядке.

      Note

      Ориентированный ациклический граф (Directed Acyclic Graph, DAG) - это такой ориентированный граф, в котором отсутствуют циклы.

      Дерево — это частный случай DAG, в котором каждая вершина (кроме корня) имеет ровно одного предка.

      Применение:

      Алгоритм топологической сортировки (на основе поиска в глубину):

      1. Инициализация:

        • Создаем список sorted, который будет хранить отсортированные вершины.

        • Создаем словарь visited, где visited[v] - True, если вершина v уже посещена, иначе False. Изначально все вершины не посещены.

      2. Обход в глубину:

        • Для каждой вершины u в графе:

          • Если вершина u не посещена (visited[u] == False):

            • Вызываем функцию dfs(u).

      3. Функция dfs(u):

        • Помечаем вершину u как посещенную: visited[u] = True.

        • Для каждого соседа vвершины u:

          • Если вершина vне посещена:

            • Рекурсивно вызываем dfs(v).

        • Добавляем вершину u в начало списка sorted.

      4. Результат: После завершения алгоритма список sorted будет содержать топологически отсортированные вершины.

      5
      0
      4
      2
      3
      1

      Выполнение алгоритма:

      1. Инициализация: sorted = [], visited = {0: False, 1: False, 2: False, 3: False, 4: False, 5: False}.

      2. Обход в глубину:

        • dfs(5): Посещаем вершины 5, 2, 3, 1 (в таком порядке) и добавляем их в начало sorted. sorted = [1, 3, 2, 5]

        • dfs(4): Посещаем вершины 4, 0, и добавляем их в начало sorted. sorted = [0, 4, 1, 3, 2, 5]

        • Вызовы dfs(0), dfs(2), dfs(3), dfs(1) не выполнятся, так как эти вершины уже посещены.

      3. Результат: sorted = [0, 4, 1, 3, 2, 5] - одна из возможных топологических сортировок данного графа.

      Алгоритм поиска сильно связных компонент

      Сильно связная компонента (SCC) - это максимальное подмножество вершин ориентированного графа, в котором существует путь между любой парой вершин. Алгоритм поиска SCC использует два прохода DFS для нахождения всех SCC в графе.

      Применение:

      Алгоритм:

      image-20240627171640442

      1. STRONGLY-CONNECTED-COMPONENTS(G): Функция, запускающая алгоритм поиска SCC для графа G.

      2. Вызов DFS(G) для вычисления времен завершения u.f для каждой вершины u:

        • Выполняем первый проход DFS на исходном графе G.

        • Для каждой вершины u записываем время ее завершения u.f (время, когда рекурсивный вызов DFS для u завершается).

      3. Вычисление G^T: Строим транспонированный граф G^T, меняя направление всех ребер в G.

      4. Вызов DFS(G^T), но в основном цикле процедуры DFS вершины рассматриваются в порядке убывания значений u.f, вычисленных в строке 1:

        • Выполняем второй проход DFS на транспонированном графе G^T.

        • Ключевой момент: Вершины выбираются для запуска DFS в порядке убывания времен завершения, полученных в первом проходе.

      5. Вывод вершин каждого дерева в лесу поиска в глубину, полученного в строке 3, в качестве отдельного сильно связного компонента:

        • Каждое дерево в лесу DFS, построенном во втором проходе, представляет собой одну SCC.

      Почему этот алгоритм работает?

      Пример.

      1. Рассмотрим ориентированный граф G с вершинами A, B, C, D, E, F, G, H и следующими ребрами:

      C
      A
      B
      D
      E
      F
      G
      H
      1. Первый DFS и времена завершения. Выполняем DFS на графе G (начальная вершина не важна) и записываем времена завершения для каждой вершины:

        Время можно посчитать вот так:

        • A.f = 12

        • B.f = 11

        • C.f = 4

        • D.f = 10

        • E.f = 9

        • F.f = 8

        • G.f = 16

        • H.f = 15

      1. Транспонированный граф. Строим транспонированный граф G^T:

        B
        A
        C
        D
        E
        F
        H
        G
      2. Второй DFS на G^T в порядке убывания времен завершения: Выполняем DFS на G^T, выбирая вершины в порядке убывания u.f:

        1. G (G.f = 16): DFS из G посещает H. Это первая SCC: {G, H}.

        2. A (A.f = 12): DFS из A посещает C, B. Это вторая SCC: {A, C, B}.

        3. D (D.f = 10): DFS из D посещает F, E. Это третья SCC: {D, F, E}.

      3. Результат:

        Алгоритм нашел три сильно связные компоненты в графе G:

        • {G, H}

        • {A, C, B}

        • {D, F, E}

      6. Хэш-функции, хэш-таблицы

      Хэш-таблицы — это эффективные структуры данных для реализации словарей, которые поддерживают операции вставки, удаления и поиска за время O(1) в среднем случае. Они основаны на использовании хэш-функций, которые отображают ключи на индексы в таблице.

      Требования к хеш-функциям

      1. Реализуются для любых данных

      2. Размер выхода не зависит от размера входа

      3. Необратимы

      4. Хорошее перемешивание

      5. Сложно скомпрометировать

      6. Быстро считаются

      Замечания. При создании хэш-функций стараются сделать так, чтобы количество коллизий было минимальным. Хэш-функции считаются за O(1). Размер хэш-таблицы изменить в процессе тяжело. Поэтому лучше заранее брать достаточно большой размер.

      Открытая адресация и таблицы на основе цепочек

      Существует два основных метода разрешения коллизий (когда разные ключи отображаются на один индекс):

      Пример (цепочки)

      Предположим, у нас есть хэш-таблица размером 10 и простая хэш-функция: сумма ASCII-кодов букв имени по модулю 10.

      1. Вставим "Alice: 25" Хэш("Alice") = (65 + 108 + 105 + 99 + 101) % 10 = 478 % 10 = 8 Помещаем в ячейку 8: [8] -> Alice: 25

      2. Вставим "Bob: 30" Хэш("Bob") = (66 + 111 + 98) % 10 = 275 % 10 = 5 Помещаем в ячейку 5: [5] -> Bob: 30

      3. Вставим "Charlie: 35" Хэш("Charlie") = (67 + 104 + 97 + 114 + 108 + 105 + 101) % 10 = 696 % 10 = 6 Помещаем в ячейку 6: [6] -> Charlie: 35

      4. Вставим "David: 28" Хэш("David") = (68 + 97 + 118 + 105 + 100) % 10 = 488 % 10 = 8 Коллизия! Ячейка 8 уже занята. Используем метод цепочек: [8] -> Alice: 25 -> David: 28

      Теперь наша хэш-таблица выглядит так:

      Чтобы найти возраст Чарли, мы вычисляем хэш("Charlie") = 6, идем в ячейку 6 и получаем значение 35. Для поиска возраста Дэвида, мы вычисляем хэш("David") = 8, идем в ячейку 8, проходим по цепочке и находим нужную запись. Этот пример демонстрирует основные принципы работы хэш-таблицы, включая вставку, разрешение коллизий методом цепочек и поиск элементов.

      Пример (открытая адресация)

      Предположим, у нас та же хэш-таблица размером 10 и та же хэш-функция: сумма ASCII-кодов букв имени по модулю 10.

      1. Вставим "Alice: 25" Хэш("Alice") = 8 Помещаем в ячейку 8: [8] Alice: 25

      2. Вставим "Bob: 30" Хэш("Bob") = 5 Помещаем в ячейку 5: [5] Bob: 30

      3. Вставим "Charlie: 35" Хэш("Charlie") = 6 Помещаем в ячейку 6: [6] Charlie: 35

      4. Вставим "David: 28" Хэш("David") = 8 Ячейка 8 занята. Пробуем следующую: 9 Помещаем в ячейку 9: [9] David: 28

      5. Вставим "Eve: 40" Хэш("Eve") = 8 Ячейки 8 и 9 заняты. Пробуем следующую: 0 Помещаем в ячейку 0: [0] Eve: 40

      Теперь наша хэш-таблица выглядит так:

      Поиск элемента. Чтобы найти возраст Чарли, мы вычисляем хэш("Charlie") = 6, идем в ячейку 6 и получаем значение 35.

      Для поиска возраста Дэвида:

      1. Вычисляем хэш("David") = 8

      2. Проверяем ячейку 8 - там Alice

      3. Проверяем следующую ячейку (9) - находим David: 28

      Для поиска возраста Евы:

      1. Вычисляем хэш("Eve") = 8

      2. Проверяем ячейку 8 - там Alice

      3. Проверяем ячейку 9 - там David

      4. Проверяем ячейку 0 - находим Eve: 40

      Удаление элемента. При удалении элемента в открытой адресации нельзя просто очистить ячейку, так как это может нарушить цепочку поиска для других элементов. Вместо этого обычно используют специальный маркер "удалено".

      Например, если мы удалим "Alice": [8] DELETED

      Этот пример демонстрирует, как работает открытая адресация с линейным пробированием для разрешения коллизий в хэш-таблице.

      Оценка стоимости операций

       CHAINCHAIN (repeats)OPEN ADR.OPEN ADR. (repeats)
      AssertΘ(1+α)Θ(1)11α – ожидаемое …… число проб
      FindΘ(1+α)Θ(1)1αm(11α) 
      RemoveΘ(1+α)Θ(1)  

      α=nm(колво добавленных элементовколво ячеек таблицы)load_factor (коэфф.заполненности)

      Замечание. Так как размер таблицы заранее выбирается большой, то α<1.

      Базовые виды хэш-функций

      Проблемы кластеризации и удаления элементов

      Реализация словаря на основе хэш-таблицы

      Хэш-таблицы предоставляют эффективный способ реализации структуры данных "словарь" (dictionary), которая хранит пары "ключ-значение" и обеспечивает быстрый доступ к значению по ключу. Рассмотрим подробнее реализацию основных операций словаря с использованием хэш-таблицы:

      1. Insert(key, value) - Вставка пары (ключ, значение):

      1. Вычисление хэш-значения: Вычисляем хэш ключа с помощью хэш-функции: index = hash(key) % table_size.

      2. Разрешение коллизий: Если ячейка index уже занята другой парой с другим ключом:

        • Открытая адресация: Ищем следующую свободную ячейку в соответствии с выбранной стратегией пробирования (линейное, квадратичное, двойное хеширование).

        • Цепочки: Добавляем новую пару (ключ, значение) в список (цепочку), связанный с данной ячейкой.

      3. Вставка: Помещаем новую пару (ключ, значение) в найденную свободную ячейку или в список.

      2. Delete(key) - Удаление пары с заданным ключом:

      1. Поиск ячейки: Вычисляем хэш ключа и находим соответствующую ячейку, как при операции Search.

      2. Удаление:

        • Открытая адресация: Помечаем ячейку как удаленную (например, специальным маркером), не удаляя данные физически, чтобы не нарушать логику пробирования.

        • Цепочки: Удаляем пару (ключ, значение) из списка, связанного с данной ячейкой.

      3. Search(key) - Поиск значения по ключу:

      1. Вычисление хэш-значения: Вычисляем хэш ключа.

      2. Поиск в таблице:

        • Открытая адресация: Проверяем ячейку, соответствующую хэшу ключа. Если ключ не совпадает, применяем стратегию пробирования, пока не найдем нужный ключ или пустую ячейку (ключ не найден).

        • Цепочки: Проходим по списку, связанному с вычисленным хэшем, и сравниваем ключи элементов списка с искомым.

      Пример реализации:

      Важно:

      Усовершенствованные методы хеширования

      Фильтр Блума

      Bloom Filter позволяет проверить принадлежность элемента множеству. Может ложно подтвердить наличие элемента при его отсутствии. Если подтверждает отсутствие, то это всегда истина.

      Общая идея:

      Замечание. Удалять из Фильтра Блума нельзя. Но работает с объединением фильров. Используется в ситуациях, когда требуется высокая эффективность по времени и памяти.

      7. Кучи (Heaps)

      Куча — это структура данных, представляющая собой полное бинарное дерево, для которого выполняется свойство кучи. Существуют два основных типа куч:

      Двоичные кучи (Binary Heaps)

      Двоичная куча (binary heap) — это наиболее распространенная реализация кучи, которая использует массив для хранения элементов дерева.

      20
      30
      40
      50
      60
      70
      100

      Свойства:

      1. Полное бинарное дерево: Все уровни дерева заполнены, кроме, возможно, последнего, который заполняется слева направо.

      2. Свойство кучи: Для каждого узла выполняется свойство max-кучи или min-кучи.

      3. Куча не сохраняет валидность итераторов при операциях с ней.

      Представление в массиве:

      Высота кучи: log2n.

      Число слоев: log2(n+1).

      Количество элементов: [2h;2h+11]N, log(N+1)1hlog(N)

      Операции над двоичной кучей

      Примеры реализации на псевдокоде:

      1. Heapify(A, i):

      2. BuildHeap(A):

      3. ExtractMax(A):

      4. Insert(A, key): Элемент добавляется в конец массива, сравнивается с родителем. Если родитель больше, то они меняются местами и сравнение продолжается, пока родитель не окажется меньше или элемент не станет корнем.

      5. IncreaseKey(A, i, key):

      6. Pop(A):

      Куча позволяет удалять только корень.

      Вспомогательные функции:

      Важно:

      Сортировка с помощью двоичной кучи (Heap Sort)

      Алгоритм сортировки с помощью двоичной кучи (Heap Sort) состоит из двух этапов:

      1. Построение кучи: BuildHeap(A) преобразует исходный массив в max-кучу.

      2. Сортировка:

        • Повторяем n1 раз:

          • Извлекаем максимальный элемент из кучи: max = ExtractMax(A).

          • Помещаем max в конец массива (на освободившееся место).

          • Уменьшаем размер кучи на 1.

      Еще как вариант:

       

      Оценка эффективности

      8. Стратегии переборных алгоритмов

      Переборные алгоритмы (brute-force algorithms) — это алгоритмы, которые основаны на полном переборе всех возможных решений задачи. Они гарантированно находят оптимальное решение, но могут быть очень неэффективными для задач с большим пространством решений.

      Полный перебор

      Полный перебор (exhaustive search) — это простейшая стратегия перебора, которая заключается в проверке всех возможных решений без каких-либо оптимизаций.

      Пример: Задача коммивояжера

      В задаче коммивояжера (Traveling Salesman Problem, TSP) нужно найти кратчайший маршрут, проходящий через все города ровно один раз и возвращающийся в исходный город.

      Метод ветвей и границ (Branch and Bound)

      Метод ветвей и границ (Branch and Bound) — это более эффективная стратегия перебора, которая использует оценки для отсечения заведомо неоптимальных решений.

      Идея:

      1. Разбиение: Разбиваем пространство решений на подмножества (ветви).

      2. Оценка: Для каждой ветви вычисляем нижнюю границу (lower bound) для оптимального решения в этой ветви.

      3. Отсечение: Если нижняя граница для ветви больше, чем текущее наилучшее решение, то эту ветвь можно отсечь, так как она не может содержать оптимального решения.

      4. Рекурсия: Рекурсивно применяем метод ветвей и границ к оставшимся ветвям.

      Пример: Задача коммивояжера

      Пример реализации на псевдокоде (упрощенный):

      9. Динамическое программирование

      Динамическое программирование (dynamic programming) — это мощный метод решения задач, который основан на разбиении задачи на подзадачи и использовании решений подзадач для решения исходной задачи. Он применим к задачам, которые обладают свойством оптимальной подструктуры и свойством перекрывающихся подзадач.

      Признаки применимости

      1. Свойство оптимальной подструктуры: Оптимальное решение задачи может быть построено из оптимальных решений её подзадач.

      2. Свойство перекрывающихся подзадач: В процессе решения задачи одни и те же подзадачи возникают многократно.

      Общая стратегия

      1. Разбиение на подзадачи: Разбиваем задачу на подзадачи меньшего размера.

      2. Рекуррентное соотношение: Записываем рекуррентное соотношение, выражающее решение задачи через решения её подзадач.

      3. Запоминание решений: Сохраняем решения подзадач в таблице (или другой структуре данных), чтобы избежать повторных вычислений.

      4. Построение решения: Используем таблицу с сохраненными решениями для построения решения исходной задачи.

      Стратегии реализации

      1. Сверху вниз (с мемоизацией)

      Инициализация
      Рекурсивное определение
      Запоминание результатов
      Использование результатов

      Мемоизация — сохранение результатов выполнения функций для предотвращения повторных вычислений. Это один из способов оптимизации, применяемый для увеличения скорости выполнения компьютерных программ.

      2. Снизу вверх

      Создание таблицы
      Заполнение таблицы
      Использование таблицы для получения ответа

      Примеры

      1. Алгоритм перемножения матриц

      2. Поиск наибольшей общей подпоследовательности (LCS)

      3. Задача о рюкзаке (Knapsack Problem)

      10. Жадные алгоритмы

      Жадные алгоритмы (greedy algorithms) - это алгоритмы, которые на каждом шаге делают локально оптимальный выбор в надежде, что это приведет к глобально оптимальному решению. Они часто просты в реализации и эффективны, но не всегда гарантируют нахождение оптимального решения.

      Признаки применимости

      1. Жадный выбор: На каждом шаге можно сделать локально оптимальный выбор, не учитывая будущие последствия.

      2. Оптимальная подструктура: Оптимальное решение задачи содержит оптимальные решения её подзадач.

      Важно: Не все задачи, обладающие свойством оптимальной подструктуры, могут быть решены жадными алгоритмами.

      Задача о рюкзаке (непрерывный вариант)

      В непрерывном варианте задачи о рюкзаке (Fractional Knapsack Problem) можно брать части предметов.

      Задача: Имеется рюкзак с максимальной грузоподъемностью W и набор предметов, каждый из которых имеет вес w[i] и ценность v[i]. Нужно выбрать предметы (или их части) так, чтобы максимизировать суммарную ценность, не превышая ограничение по весу.

      Жадный алгоритм:

      1. Вычисление удельной ценности: Для каждого предмета вычисляем удельную ценность v[i] / w[i].

      2. Сортировка: Сортируем предметы по убыванию удельной ценности.

      3. Заполнение рюкзака:

        • Начинаем с предмета с наибольшей удельной ценностью.

        • Берем весь предмет, если его вес не превышает оставшуюся грузоподъемность рюкзака.

        • Иначе, берем часть предмета, заполняющую оставшуюся грузоподъемность.

      Псевдокод:

      Гарантия оптимальности:

      Жадный алгоритм для непрерывного варианта задачи о рюкзаке гарантированно находит оптимальное решение.

      Важно: Жадный алгоритм не работает для дискретного варианта задачи о рюкзаке, где нельзя брать части предметов.

      11. Алгоритмы поиска подстрок

      Алгоритмы поиска подстрок (string matching algorithms) — это алгоритмы, предназначенные для поиска вхождений одной строки (образца, pattern) в другую строку (текст, text). Они широко используются в текстовых редакторах, поисковых системах, биоинформатике и других областях.

      Задача: найти все вхождения подстроки в строку.

      Задача: для заданного образца найти все допустимые сдвиги.

      «Наивный» алгоритм

      "Наивный" алгоритм (naive algorithm) — это простейший алгоритм поиска подстрок, который заключается в последовательном сравнении образца с каждым возможным подстрокой текста.

      Идея:

      1. Для каждого индекса i в тексте:

        • Сравниваем образец с подстрокой текста, начинающейся с i.

        • Если все символы совпадают, то найдено вхождение образца.

      Сложность: O(mn), где m - длина образца, n - длина текста.

      Псевдокод:

      Алгоритм Рабина-Карпа

      Алгоритм Рабина-Карпа (Rabin-Karp algorithm) использует хеширование для ускорения поиска подстрок.

      Идея:

      1. Вычисляем хэш образца.

      2. Для каждого индекса i в тексте:

        • Вычисляем хэш подстроки текста длины m, начинающейся с i.

        • Если хэши совпадают, сравниваем образец и подстроку текста посимвольно, чтобы убедиться, что это не ложное срабатывание (hash collision).

      Пример. T=1271326126126, P=126. Вариант решения: перевести число символов длины P.length в число, сам паттерн тоже в число. Сравнить. Сдвинуться на один вправо. Подразумеваем, что код символа = его значение.

      ti=(((ti1T[i]dm1modq)modq)d+T[i+m])modq, dm считаем заранее.

      Получается вычисление tiO(1)

      d=10 - если только с цифрами

      d=256 - если работаем с кодами символов

      Тогда получаем алгоритм:

      q: Переменная, хранящая текущую степень d (начинает с 1 и увеличивается в степени d на каждой итерации первого цикла). Используется для "сдвига" хэша влево.

      t: Хэш текущей подстроки текста T длины m.

      p: Хэш образца P.

      d: Основание системы счисления, используемое для хэширования.

      Возможно возникновение коллизии, в таком случае, если совпадает образец по коду, то сделать посимвольную проверку.

      Оценка стоимости O(n)+O(m)

      Алгоритм Кнута-Морриса-Пратта (KMP)

      Алгоритм Кнута-Морриса-Пратта (Knuth-Morris-Pratt algorithm) использует префикс-функцию для оптимизации поиска подстрок.

      Идея:

      1. Вычисляем префикс-функцию для образца. Префикс-функция pi[i] для индекса i в образце - это длина наибольшего собственного суффикса подстроки pattern[0..i], который также является префиксом образца.

      2. Используем префикс-функцию для сдвига образца при несовпадении символов, избегая повторных сравнений уже проверенных символов.

      image-20240630172811814

      Сдвигаем паттерн так, чтобы рассматриваемая часть сразу не конфликтовала с паттерном. Пример нахождения префикса в префикс-функции:

      image-20240630173032935

      Обязательно k<q.

      Сложность: O(m+n). У алгоритма всегда линейная оценка, даже в плохом случае T=an, P=am.

      Алгоритм Бойера-Мура (Boyer-Moore)

      Алгоритм Бойера-Мура (Boyer-Moore algorithm) использует два эвристических правила для ускорения поиска, при этом сравниваем с конца образца и начала фрагмента текста:

      1. Правило плохого символа (bad character rule): При несовпадении символов сдвигаем образец так, чтобы последний символ образца совпал с соответствующим символом в тексте. Если выдается отрицательный сдвиг, то игнорируем рекомендацию этой эвристики.

      2. Правило хорошего суффикса (good suffix rule): При совпадении суффикса образца с подстрокой текста сдвигаем образец так, чтобы этот суффикс совпал с предыдущим вхождением этого суффикса в образце. Всегда дает положительный сдвиг.

      Общая идея алгоритма:

      Примечание. Алгоритм хорош для больших последовательностей.

      Сложность: O(n/m) в лучшем случае (при длинных образцах), O(mn) в худшем случае.

      Поиск подстрок с помощью конечных автоматов

      Для поиска подстрок можно построить конечный автомат, который принимает текст на вход и переходит в состояние "найдено", если текущая подстрока текста совпадает с образцом.

      Конечный автомат M – это пятерка (Q,qo,A,Σ,δ), где:

      Для каждого паттерна строится свой конечный автомат.

      Идея:

      1. Строим конечный автомат для образца.

      2. Пропускаем текст через автомат, начиная с начального состояния.

      3. Если автомат переходит в допускающее состояние, то найдено вхождение образца.

      Суффикс-функция σ(x)=max{k:Pkx} — максимально большое количество совпадений символов конца слова с началом паттерна.

      σ(ϵ)=0

      Функция переходов: δ(q,a)=σ(Pqa)

      Pq — префикс паттерна размера q.

      δ(3,y)=σ(P3y)=σ("aba","a")=1

      Инвариант функции показывает конечное состояние автомата после прочтения i-го символа.

      φ(Ti)=σ(Ti)

      Алгоритм.

      Оценка O(mΣ+n)

      Пример автомата. Конечный автомат для поиска подстроки "aba"

      Состояние \ ВходabДругие
      0100
      1120
      2300
      3100
      a
      a
      b
      a
      b, другие
      другие
      b, другие
      0
      1
      2
      3

      Пример работы автомата:

      Входная строка: "aabababaa"

      ШагВходТекущее состояниеСледующее состояниеПодстрока найдена?
      1a01Нет
      2a11Нет
      3b12Нет
      4a23Да
      5b30Нет
      6a01Нет
      7b12Нет
      8a23Да

      Вывод: Подстрока "aba" найдена дважды, начиная с позиций 3 и 8.

      ДОПОЛНИТЕЛЬНО (не из программы, об этом говорилось на консультациях)

      Биномиальные деревья

      Биномиальное дерево - это рекурсивно определяемая структура данных, используемая для реализации биномиальных куч.

      Определение:

      Свойства:

      Биномиальные кучи

      Биномиальная куча - это структура данных, представляющая собой набор биномиальных деревьев, которые удовлетворяют свойству кучи (min-куча или max-куча) и свойству биномиальной кучи.

      Свойства:

      Операции:

      Преимущества биномиальных куч:

      Фибоначчиевы кучи

      Фибоначчиевы кучи (Fibonacci heaps) — это сложная, но очень эффективная структура данных для реализации очередей с приоритетами. Они обеспечивают амортизированно константное время для большинства операций, что делает их привлекательными для алгоритмов, где интенсивно используются операции с очередями с приоритетами, например, алгоритм Дейкстры для поиска кратчайших путей.

      Структура

      Ключевые особенности

      Операции

      Сложность операций

      ОперацияАмортизированная сложность
      MakeHeap, Insert, Minimum/Maximum, UnionO(1)
      ExtractMin/ExtractMax, DeleteO(logn)
      DecreaseKey/IncreaseKeyO(1)

      Преимущества

      Возможные задания

      Считай, ответы.

      Тестовая часть

      1. Верные утверждения: f(n)=Θ(g(n))f(n)=O(g(n)) f(n)=Θ(g(n))f(n)o(g(n)) f(n)=Θ(g(n))f(n)ω(g(n)) f(n)=ω(g(n))f(n)Θ(g(n))

      2. 2.1 Асимптотика какого алгоритма зависит от размера алфавита - Поиск подстрок спомощью автоматов** 2.2 Какой из приведенных алгоритмов работает лучше в худшем случае - Боуера-Мура 2.3 Асимптотика какого алгоритма зависит от размера машинного слова - Поиск подстрок с помощью автоматов, Рабина-Карпа 2.4 В алгоритме Кнута-Морриса-Пратта используется - Префикс-функция для образца 2.5 Какой алгоритм использует эвристику безопасного суффикса? Буера-Мура 2.6 Какой из приведённых алгоритмов начинает проверку с конца образца? Бойера-Мура

      3. 3.1 Результатом алгоритма топологической сортировки является упорядоченный набор вершин 3.2 Результатом алгоритма Крускала является минимальное остовное дерево 3.3 Результатом алгоритма Прима является минимальное остовное дерево 3.4 Результатом работы алгоритма Дейкстры является дерево кратчайших путей

      4. 4.1 Операция вращения являются вспомогательные для AVL-деревьев, RBT-деревьев 4.2 Какой вид деревьев имеет минимальную высоту: Б-деревья 4.3 В стандартной библиотеке C++ для реализации контейнера std::map используются красно-чёрные деревья

      5. 5.1 Рассмотрение отрицательного сдвига подстроки возможно в алгоритме Боуера-Мура 5.2 Ситуация “холостое срабатывание” возможна в алгоритме Рабина-Карпа 5.3 Что даст алгоритм топологической сортировки на графах с циклами? Неверный ответ 5.4 Алгоритм топологической сортировки неприменим и даёт неверный ответ в случае, если граф содержит циклы

      6. 6.1 Если основная теорема о рекуррентных соотношениях не применима - Возможно, оценка может быть получена другими методами 6.2 Основная теорема о рекуррентных соотношениях - Позволяет в некоторых случаях найти асимптотическую оценку рекуррентного соотношения 6.3 Что из приведенного используется для оценки рекуррентных соотношений? Метод подстановки. Метод рекурсий. Метод итераций.

      7. 7.1 Эффективные операции для RBT: Извлечение корня с образованием двух RBT, Извлечение минимального элемента 7.2 Операции, которые не используются для хеш-таблиц: Объединение хеш-таблиц, извлечение минимального, изменение размера таблицы 7.3 Какие маркеры ячеек обычно реализуются в хеш-таблицах на открытой адресации? - Элемент удален, Ячейка пуста 7.4 Чем деревья поиска отличаются от хеш-таблиц? Поддержание порядка на элементах, Поддержкой постоянных итераторов 7.5 Методы std::lower_bound и std::upper_bound в C++ дают одинаковый результат в случае, если элемент в контейнере отсутствует

      8. 8.1 Структура бинарной кучи соответствует структуре RBT и AVL 8.2 Почему бинарная куча обычно не предоставляет итераторов на элементы? Это не имеет смысла — элементы не упорядочены, Положение элементов в памяти часто меняется

      9. 9.1 Какие из приведенных структур можно рассматривать как набор деревьев? Биномиальные кучи, Фибоначчиевы кучи

      10. 10.1 Какие операции поддерживаются биномиальной кучей? Извлечение минимального эл-та, слияние 2-х куч 10.2 Какой ответ даст алгоритм топологической сортировки на графах с циклами? Гарантированно неверный ответ 10.3 Какие операции поддерживаются фибоначчиевой кучей? Извлечение минимального эл-та, слияние 2-х куч 10.4 Структура бинарной кучи соответствует структуре идеально сбалансированного дерева

      11. 11.1 Будет ли работать алгоритм Прима на графах с петлями? Да, с получением верного результата 11.2 Алгоритм Прима работает обычно на взвешенных графах 11.3 Будет ли работать алгоритм Крускала на графах с петлями? Да, с получением верного результата 11.4 Могут ли алгоритмы Крускала и Прима давать разные результаты на одном графе? Только при наличии рёбер с одинаковым весом

      12. 12.1 Что из приведенного верно? (про деревья) AVL-дерево можно представить в виде RBT-дерева(раскрасив вершины), RBT-дерево не всегда является AVL-деревом 12.2 Что из приведенного верно? (про жадные алгоритмы и динамику) Применимость этих стратегий не связана напрямую 12.3 Алгоритм Прима работает обычно на взвешенных графах

      13. 13.1 Критерии применимости динамического программирования: Оптимальность по подзадачам, Повторяющиеся подзадачи 13.2 Может ли фибоначчиева куча иметь такую же структуру, что и биномиальная: Да, если не выполнять объединение куч, Да, если не извлекать произвольные элементы

      14. 14.1 Какие эвристики применяются в системах непересекающихся множеств - Объединение по рангу(весовая), Эвристика сжатия путей 14.2 Эвристика “сжатия путей” применяется в алгоритме – Крускала 14.3 Какая структура данных “откладывает” восстановление нормального вида до завершения последовательности однотипных операций - Фибоначчиевы кучи 14.3 Эвристика “по весу” применяется в алгоритме - Крускала

      15. 15.1 В системах непересекающихся мн-в обычно не реализуется пересечение мн-в, разбиение мн-в, извлечение минимального эл-та Комментарий: В системах непересекающихся множеств реализуется только объединение множеств и проверка на принадлежность (по элементу получаем представителя его множества, либо индекс), остальное не реализуется.

        15.2 Укажите эффективный метод для решения задачи коммивояжере - Динамическое программирование

      16. 16.1 Амортизированная стоимость операций является усредненной оценкой стоимости операции для некоторого набора операций 16.2 Амортизированная стоимость операций является оценкой усредненной стоимости набора операций

      Задания с развернутым ответом

      Ссылки на места в документе, где есть ответ на соответствующий вопрос.

      1. 17.1 Дайте определение биноминальной кучи. Ссылка на ответ 17.2 Идея метода ветвей и границ на примере задачи коммивояжера. Ссылка на ответ 17.3 Укажите, какую операцию было бы неплохо добавить к очереди с приоритетами, чтобы в алгоритме Дейкстры не возникало дублирование элементов в очереди - Обновление приоритета элемента

      2. 18.1 Укажите оценки эффективности основных операций для хеш-таблицы на основе открытой адресации - Добавление, поиск и удаление O(1/(1α)), где α - коэф. заполненности таблицы 18.2 Укажите оценки эффективности основных операций для хеш-таблицы на основе цепочек Добавление, поиск и удаление O(1+α), где α - коэф. заполненности таблицы 18.3 Какой эффект можно получить, если в хеш-таблице плохо подобрать хеш-функцию? Коллизии, неравномерное распределение, снижение производительности, увеличение времени выполнения методов разрешения коллизий, увеличение использования памяти 18.4 Что такое потенциальная функция, требования и ограничения. Ссылка на ответ

      3. 19.1 Алгоритм Прима и оценка времени работы. Ссылка на ответ 19.2 Алгоритм Крускала и оценка времени работы. Ссылка на ответ

      4. 20.1 Алгоритм Рабина-Карпа. Ссылка на ответ 20.2 Алгоритм Кнута-Морриса-Пратта. Ссылка на ответ

      5. 21.1 Префикс-функция. π(q)=max{k:k<q,PkPq} 21.2 Суффикс-функция. σ(x)=max{k:Pkk}

      6. Конечный автомат для поиска подстроки. Ссылка на ответ

      7. Мастер-теорема. Ссылка

        1. T(n)=6T(n/3)+n2log(n) a=6,b=3,f(n)=n2log(n) Заметим, что n2logn>nlogb(a) (растет быстрее). Однако, ϵ>0:n2logn=O(nlog36+ε)). Так как f(n)=O(nlogb(a)ε)), мы находимся в случае 1 мастер-теоремы. Следовательно, T(n)=Θ(nlogb(a))=Θ(nlog3(6)).

        2. T(n)=4T(n/2)+nlog(n). Аналогично, 1 случай, ответ Θ(n2).

        3. T(n)=5T(n/2)+2n a=5,b=2,f(n)=2n Заметим, что 2n>nlog2(5) (растет быстрее). Также выполняется условие регулярности: af(n/b)=52n/2c2n=cf(n) для c=12 и достаточно больших n. Так как f(n)=Ω(nlogb(a)+ε) и выполняется условие регулярности, мы находимся в случае 3 мастер-теоремы. Следовательно, T(n)=Θ(f(n))=Θ(2n).

        4. T(n)=16T(n/4)+n a=16,b=4,f(n)=n Вычислим: logb(a)=log4(16)=2 В данном случае f(n)=n=nlogb(a)1, где ϵ=1>0. Так как f(n)=O(nlogb(a)ε), мы находимся в случае 1 мастер-теоремы. Следовательно, T(n)=Θ(nlogb(a))=Θ(n2).

      8. 24.1 Проиллюстрируйте добавление в указанное B-tree 85. image-20240630222413471 24.2 image-20240630222530269

        24.3 image-20240630222706513

        24.4 Дайте определение биноминального дерева. Ссылка на ответ

      9. 25.1 Продемонстрируйте добавление в RBT-дерево чисел от 10 до 2. image-20240630223946974

        25.2 Продемонстрируйте добавление в RBT-дерево букв от A до G image-20240630224055025 image-20240630224114224

        25.3 Продемонстрируйте добавление в AVL-дерево букв от A до G. image-20240630224211171

        25.4 Нарисуйте биномиальную кучу для 7 элементов.

        B0
        B1
        B2
        1
        2
        3
        4
        5
        6
        7
      10. 26.1 image-20240630225003389

        26.2 Является ли указанное дерево Б-деревом, если нет – почему, если да, то каким? (степень) image-20240630225119591

      11. 27.1 Пример хеш-функции на основе умножения с квадратичной последовательностью проб. h(x,i)=(h(x)+c1i+c2i)modM h(x)=(xAmod1)M

        27.2 Пример хеш-функции на основе деления с квадратичной последовательностью проб h(x,i)=(h(x)+c1i+c2i)modM h(x)=xmodM

        27.3 Почему эффективность хеш-таблицы на основе открытой адресации обычно ухудшается со временем и какая операция страдает больше всего? Из-за заполнения. С увеличением количества элементов увеличивается вероятность коллизий. Это увеличивает время работы операции поиска.

      12. 28.1 Алгоритм добавления элемента в биномиальную кучу. Ссылка 28.2 Алгоритм добавления элемента в бинарную кучу. Ссылка 28.3 Алгоритм добавления элемента в фибоначчиеву кучу. Ссылка 28.4 Какое отношение должно быть реализовано для элементов чтобы их можно было размещать в структуры наподобие std::set? Реализовать < или использовать компаратор

      13. 29.1 Выполните амортизационный анализ для структуры “мультистек” (push,pop,multipop). См. что-нибудь отсюда 29.2 Что такое «Фильтр Блума», для чего используется? Ссылка 29.3 Какой тип ошибки НЕ может выдать фильтр Блума: «ложноположительное срабатывание» или «ложноотрицательное срабатывание»? ложноотрицательное срабатывание

      14. 30.1 Возможны ли ситуации, в которых алгоритм с худшей асимптотической оценкой (в среднем) будет более предпочтителен, нежели алгоритм с лучшей асимптотикой? Да возможны

        30.2 Что такое ФЛОПСы (FLOPS), где и для чего они используются? Это единица измерения, показывающая, сколько операций с плавающей запятой компьютер может выполнить за одну секунду. Используется в научных вычислениях, графике, играх, ИИ.

      15. 31.1 Динамическое программирование «сверху вниз» - основная идея и схема алгоритма. 31.2 Что такое мемоизация? 31.3 Динамическое программирование "снизу вверх" - основная схема и идея алгоритма. Ссылка на ответы к 31

      16. 32.1 Укажите свойства хеш-функций, которые существенны для использования в хеш-таблицах. Равномерность, Быстродействие, Детерминированность, Минимизация коллизий, Скорость вычисления

        32.2 Что означает запись f(n)=Ω(g(n))? Это говорит о том, что f(n) растет быстрее, чем g(n), начиная с некоторого значения n0. Дополнительно.

      ❗️ Комментарий

      image-20240711173955966