[Это моя интерпретация / разбивка раздела в CLRS. Это что-то новое, я думаю попробовать, так как у меня есть свободное время. Я собираюсь пройтись по каждому разделу и обобщить как можно больше, работая над несколькими примерами в процессе. Это может быть хороший ресурс для людей, которым нравится разбирать информацию на мелкие кусочки.]
Анализ алгоритмов включает в себя поиск ресурсов, которые необходимы алгоритму, которые в большинстве случаев относятся к времени. Проще говоря:
Анализ алгоритма - это оценка вычислительного времени (или ресурсов в целом), необходимого для выполнения определенного алгоритма, решающего определенную проблему.
Теперь вопрос в том, как именно анализировать алгоритм? На большинстве курсов студенты анализируют результаты с помощью проверки зрения. На самом деле проверка зрения сводится к тому, чтобы посмотреть, сколько циклов содержится в функции и сколько раз вы повторяете n элементов. Это хороший способ проанализировать большинство алгоритмов, не использующих принцип «разделяй и властвуй». Единственным недостатком этого подхода является то, что вы ограничиваетесь рассмотрением только сценариев наихудшего случая. Иногда все работает в нашу пользу, поэтому стоит знать, как подготовиться к каждому случаю алгоритма, чтобы мы могли сравнивать множество различных возможностей друг с другом в зависимости от типа данных, которые нам предоставляются.
Давайте посмотрим на алгоритм:
//Insertion-Sort Algorithm. Using Golang func insertion_sort(arr []int){ for j := 1; j < len(arr); j++ { key := arr[j] i := j - 1 for i >= 0 && arr[i] > key { arr[i + 1] = arr[i] i = i - 1 } arr[i + 1] = key } }
Построчная разбивка алгоритма
func insertion_sort(arr []int) {...}
Эта строка - объявление функции. arr []int
- это параметр, который говорит, что эта функция запрашивает slice
. В Go
slice
- это просто указатель на массив. Тип slice
предоставляет доступ к изменяемому массиву, что упрощает весь этот процесс. Если бы мы не использовали изменяемый массив, нам пришлось бы объявить и инициализировать новый массив внутри функции, то есть точную копию массива, отправленного нам в качестве аргумента, а затем вернуть вновь созданный массив после того, как мы закончили сортировку .
for j := 1; j < len(arr); j++ {...}
Это начало нашего for-loop
. В Go
вы можете выполнить быстрое объявление + инициализацию переменной с помощью оператора :=
. Итак, j :=
это то же самое, что int j; j = 1
. Мы устанавливаем j
в 1, потому что мы собираемся начать процесс со 2-го места (1-й индекс) в массиве. Мы проверяем, меньше ли j
длины массива, и если да, то запускаем цикл. Если нет, то мы прекращаем работу.
key := arr[j]
Мы создаем новую переменную с именем key
, которая будет содержать текущее место (current index
), в котором мы находимся. Если у нас есть массив arr = []int{3, 4, 5, 1, 6}
, тогда key = 4
при первом запуске этого цикла.
i := j - 1
Затем мы инициализируем другую переменную с именем i
, которая будет отслеживать индексы. Мы устанавливаем это значение на 1 меньше, чем текущая позиция, потому что мы собираемся перейти от current index
до 0th index
и проверить, какие значения в этом диапазоне больше, чем значение в нашем current index
.
for i >=0 && arr[i] > key { ... }
В Go
нет while-loop
. Вместо этого у вас есть только for-loop
минус этап инициализации и итерации, поэтому в основном for-loop
, который только содержит условие. Это говорит о том, что пока индекс i
не выходит за пределы начала нашего массива, а предыдущее значение все еще больше нашего current index
, продолжайте выполнение цикла.
arr[i + 1] = arr[i]
Пока условие цикла истинно (пока мы нашли значения в диапазоне ([0th index
: current index
]), поменяйте местами значение индекса i + 1
и значение перед ним на i
.
i = i - 1
После обмена уменьшите значение i
, чтобы мы переместились вниз с current index
на 0th index
.
arr[i + 1] = key
Затем, как только мы либо а) достигли самого начала массива, либо б) нашли значение, которое не превышает нашего key
, мы помещаем наш key
в индекс после индекса, который мы в данный момент проверили. .
Когда дело доходит до анализа этого алгоритма, нам нужно кое-что предположить. Предположим, что для компиляции каждой строки / шага кода требуется c
времени, где c
- константа. Это означает, что обе эти строки теоретически потребуют одинакового времени для компиляции, если запускаются по отдельности:
i = i - 1 /* Takes c time to compile */ arr[i + 1] = key /* Also takes c time to compile */
Теперь, когда мы знаем, что каждая строка / шаг стоит нам c
постоянного времени, мы должны знать, сколько раз или, лучше сказать, как часто мы запускаем каждую строку, чтобы что мы можем определить общую стоимость алгоритма. Проще говоря:
Cost of Step = Cost of line * Number of Times We Run that Line
И
Cost of Algorithm = Summation of Cost of each Step
Авария
for j := 1; j < len(arr); j++
Эта строка выполняется n
раз. Причина в том, что вы начинаете с индекса 1 и выполняете до тех пор, пока не будет запущено len (arr) which = n для всего n-1
, но вам нужно выполнить одну дополнительную проверку, чтобы знать, как завершить цикл, который оставляет ты с n - 1 + 1 = n
бегаешь. Постоянное время для этого будет обозначено c1.
key = arr[j] i = j-1
Эти две строки выполняются n-1
раз, а не n
раз, как первая строка, просто потому, что первая строка должна проверить одну больше времени, чтобы знать, когда завершить. Постоянным временем для этих прогонов будет c2 и c4.
for i > 0 && arr[i] > key {...}
Эта часть становится сложной. Мы не знаем точно, сколько раз это while-loop
запускается, поскольку оно может варьироваться в зависимости от массива, поэтому нам нужен новый идентификатор для того, как часто while-loop
запускается, и мы назовем его tj
.
tj
просто указывает, сколько раз выполняется цикл. Например, 1-й цикл может выполняться всего t1
= 2 раза, а 2-й цикл может выполняться для t2
= 4. Таким образом, эта строка выполняется несколько раз, пока следующие строки:
arr[i + 1] = arr[i] i = i - 1
Выполнить всего tj- 1
раза, потому что не выполняется дополнительное время, необходимое для проверки условия. Константы для этих трех строк будут c5, c6 и c7.
arr[i + 1] = key
Выполняется в общей сложности n - 1
раз. Константа для этой строки будет c8.
Общее время работы
Допустим, мы оцениваем T(n)
как лучшее время работы. В лучшем случае это будет означать, что массив, который мы хотим отсортировать, уже отсортирован. Если это так, то нам ни в коем случае не нужно запускать цикл while, это означает, что все значения tj будут равны 0, а наивысший порядок нашей полиномиальной функции будет равен 1, поэтому наш полином будет выглядеть примерно так. к:
В худшем случае мы будем сортировать массив в обратном порядке. Это означает, что каждый цикл while должен выполняться максимальное количество раз (tj = j в суммировании), и нам действительно нужно будет решить T(n)
выше, чтобы узнать, какой член является наивысшим. Это можно сделать путем осмотра (и немного веры).
Возьмите термин:
Если бы мы расширили это, мы получили бы следующее:
Если это наихудший сценарий, то и. Это имеет смысл, поскольку внутренний цикл while, в худшем случае, в самом конце массива, должен переместить наименьшее значение (поскольку массив находится в обратном порядке) до самого начала массива. Также помните, что каждый представляет собой линию / шаг. Это означает, что цикл while выполняется [(n * (n + 1)) / 2] раз. Например, массив из 6 элементов,
Это означает, что условие цикла while выполняется всего 21 раз в наихудшем сценарии. Это можно проверить с помощью следующего кода:
package main import "fmt" func insertion_sort(arr []int) { for j := 1; j < len(arr); j++ { key := arr[j] i := j - 1 count := 0 sum := 0 for i >= 0 && arr[i] > key { count += 1 sum += count arr[i + 1] = arr[i] i = i - 1 } /* this makes sure to take into consideration the extra check on the for-loop for termination*/ count +=1 sum += count fmt.Printf("This is the %d run\n", sum) arr[i + 1] = key } } func main() { arr := []int{6, 5, 4, 3, 2, 1} fmt.Printf("Here's our array: %v\n", arr) insertion_sort(arr) fmt.Printf("Here's our sorted array: %v\n", arr) }
Зная, что наш самый высокий член в полиноме имеет степень 2, мы знаем, что форма T(n)
будет квадратичной для худшего сценария:
Этот алгоритм также является линейным в лучшем случае:
Время работы для наихудшего случая, известное как Big-O notation
.
Эта нотация будет обсуждаться далее в другой статье, но то, что она означает здесь, дается алгоритму, например insertion sort
, если бы мы должны были протестировать его на различных размерах n
, то его форма в координатной плоскости выглядела бы так квадратичная функция, основанная на функции T(n)
, которую мы вывели ранее.
Просто взглянуть на эту информацию, имея за плечами один алгоритм, мало что значит. Как только вы начнете сравнивать время работы, вы поймете важность уравнений.