Подтвердить что ты не робот

Хотя мы понижаем константу в большой нотации O, имеет ли это значение в реальных ситуациях?

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

Чтобы напечатать значение node в связанном списке k мест с конца списка.

Учитывая node:

/* Link list node */
struct node
{
  int data;
  struct node* next;
};

Решение 1 O (n)

Это решение выполняет итерацию по списку дважды, один раз, чтобы найти длину списка, и второй раз, чтобы перейти в конец списка - N.

void printNthFromLast(struct node* head, int n)
{
    int len = 0, i;
    struct node *temp = head;


    // 1) Count the number of nodes in Linked List
    while (temp != NULL)
    {
        temp = temp->next;
        len++;
    }

    // Check if value of n is not more than length of the linked list
    if (len < n)
      return;

    temp = head;

    // 2) Get the (n-len+1)th node from the begining
    for (i = 1; i < len-n+1; i++)
    {
       temp = temp->next;
    }
    printf ("%d", temp->data);

    return;
}

Решение 2 O (n)

Это решение выполняет только итерацию по списку один раз. Указатель ref_ptr ведет, а второй указатель (main_ptr) следует за ним. Когда ref_ptr достигнет конца списка, main_ptr должен указывать на правильный node (k мест из конца списка).

void printNthFromLast(struct node *head, int n)
{
  struct node *main_ptr = head;
  struct node *ref_ptr = head;

  int count = 0;
  if(head != NULL)
  {
    while( count < n )
     {
        if(ref_ptr == NULL)
        {
           return;
        }
        ref_ptr = ref_ptr->next;
        count++;
     }

     while(ref_ptr != NULL)
     {
        main_ptr = main_ptr->next;
        ref_ptr  = ref_ptr->next;
     }
  }
}

Вопрос: Несмотря на то, что оба решения O (n) в то же время оставляют значительную нотацию O, является ли второе решение более эффективным, чем первый для очень длинного списка, поскольку он только итерации над списком один раз?

4b9b3361

Ответ 1

Да. В конкретном примере, где происходит одна и та же работа, один цикл, вероятно, будет более эффективным, чем цикл по набору данных дважды. Но идея O(2n) ~ O(n) заключается в том, что 2 ns vs 1 ns могут не иметь большого значения. Big O работает лучше, чтобы показать, как фрагмент кода может масштабироваться, например. если вы сделали цикл O(n^2), то разница O(n) vs O(2n) намного меньше O(n) vs O(n^2).

Если ваш связанный список содержит terrabytes данных, тогда это может стоить уменьшить до единственной циклической итерации. Большая метрика O, в данном случае может оказаться недостаточной для описания вашего наихудшего случая; вам было бы лучше отключить код и учитывать потребности приложения.

Другой пример - во встроенном программном обеспечении, где 1 мс против 2 мс может быть разницей между 500 Hz и контуром управления 1 kHz.

Изученный урок состоит в том, что он зависит от приложения.

Ответ 2

Константа имеет значение только в том случае, если порядок одинаков, а операции сопоставимы по сложности. Если они не того же порядка, то тот, у которого более высокий порядок, гарантированно займет больше времени, как только у вас будет достаточно большой n. Иногда n должен быть больше, чем ваш типичный набор данных, и единственный способ выбрать наиболее эффективный алгоритм - сравнить их.

Ответ 3

Я считаю, что с моей точки зрения разница между двумя подпрограммами, которые являются O (n) и O (n), например, на самом деле не является точкой нотации O. Ключевыми различиями являются, например, O (n ^ 2) и O (n). [n ^ 2, конечно, n квадрат]

Таким образом, в общем случае мощность p для O (n ^ p) является критичной в том, как эффективность рутинной шкалы с размером.

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

Примером кода, где является масштабирование, является Преобразование Фурье, где некоторые методы дают O (n ^ 2), а другие дают O (n log n).

Ответ 4

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

Да.

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

У нас есть цикл, который должен выполнить N раз. В этом цикле мы используем возвращаемое значение F несколько раз, чтобы вычислить что-то.

Вход в F всегда одинаковый для данной итерации этого цикла.

У нас есть две потенциальные реализации этого цикла.

  • Реализация # 1:

    loop:
        set inputs to something;
        value = F(inputs);
        do something with value;
        do something else with value;
        do something else else with value;
    done
    
  • Реализация # 2:

    loop:
        set inputs to something;
        value = F(inputs);
        do something with value;
        value = F(inputs);
        do something else with value;
        value = F(inputs);
        do something else else with value;
    done
    

Обе реализации цикла повторяются одинаково. Оба получат тот же результат. Очевидно, что реализация № 2 менее эффективна, так как она выполняет больше работы за итерацию.

В этом тривиальном примере компилятор может заметить, что F всегда возвращает одно и то же значение для одного и того же ввода, и он может заметить, что мы вызываем его с одинаковыми входами каждый раз, но для любого компилятора мы можем построить пример, эквивалентный O(C*n) vs O(n), где C действительно имеет значение на практике.

Ответ 5

Да, это может иметь значение. Я не проверял ваш код за правильность, но считаю следующее:

Первое решение проходит через список один раз до конца и другое время до n. Второе решение циклически перебирается по списку один раз, но оно использует ->next() во втором указателе n раз. Поэтому в основном они должны называть ->next() примерно столько же раз (возможно, + -1 или около того).

Независимо от вашего примера, это не то, о чем большая нотация O. Речь идет о приближении к тому, как алгоритм масштабируется, если количество данных увеличивается. Если у вас есть алгоритм O(n) и сократить его время выполнения на 10% (независимо от того, как вы это делаете), то, конечно, это преимущество. Но если вы удвоите данные, его время выполнения все равно будет удвоено и что будет означать O(n). (Алгоритм An O(n^2), например, будет иметь время выполнения, умноженное на коэффициент 4, если вы удвоите данные.)

Ответ 6

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

Безусловно, большое значение имеет значение, если ваши наборы данных, вероятно, будут очень большими, где "очень большой" вам решать. Иногда размер набора данных является первоочередной задачей. Конечно, не всегда.

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

Что обычно не учит в школе, так это то, как найти большие факторы ускорения. Например, в совершенно хорошо написанном программном обеспечении большие ускорения могут скрываться, как в этом примере.

Ключом к получению ускорений является не пропустить ни одного. Просто найти некоторые, но не все, недостаточно, и большинство инструментов имеют огромные слепые пятна. Эта ссылка указывает на метод, который опытные программисты узнали.

Ответ 7

Постоянная, безусловно, имеет значение, и во многих случаях может быть склонна сказать "это единственное, что имеет значение".

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

Мощность ALU постоянно повышалась намного быстрее, чем пропускная способность памяти (и, что более важно, латентность), или доступ к другим устройствам, таким как диски в течение последних двух десятилетий. На графических процессорах это еще более выражено, чем на процессорах (к тому времени, когда DMA и ROP получаются в 2-3 раза быстрее, ALU становится в 15-20 раз быстрее)

Алгоритм с сложностью O (log N) (например, двоичный поиск), который вызывает одностраничную ошибку, может быть в несколько тысяч раз медленнее, чем алгоритм O (N) (скажем, линейный поиск), который избегает этой ошибки.

Хэш-таблицы - это O (1), но они неоднократно показывались медленнее, чем другие алгоритмы с более высокой степенью сложности. Связанные списки, как правило, имеют одинаковую (или лучшую) алгоритмическую сложность по сравнению с векторами. Тем не менее, вектор почти всегда значительно превосходит список, поскольку списки делают больше распределений и имеют больше промахов в кеше. Если объекты не огромны, даже перемещение вокруг нескольких тысяч элементов в векторе для вставки чего-то в середине обычно быстрее, чем выделение и вставку node в список.

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

Итерация двумерного массива так или иначе (первые строки/столбцы) в точности идентична по сложности и даже по количеству операций. У одного, однако, есть константа, которая в тысячу раз больше и будет работать в тысячу раз медленнее.