Нахождение повторяющихся целых чисел со знаком с O (n) во времени и O (1) в пространстве - PullRequest
13 голосов
/ 21 ноября 2011

(Это обобщение: Поиск дубликатов за O (n) время и O (1) пространство )

Проблема: Напишите функцию C ++ или C с временными и пространственными сложностями O (n) и O (1) соответственно, которые находят повторяющиеся целые числа в данном массиве, не изменяя его.

Пример: Данная функция {1, 0, -2, 4, 4, 1, 3, 1, -2} должна печатать 1, -2 и 4 один раз (в любом порядке).


РЕДАКТИРОВАТЬ: Следующее решение требует двойного бита (для представления 0, 1 и 2) для каждого целого числа в диапазоне от минимума до максимума массива. Количество необходимых байтов (независимо от размера массива) никогда не превышает (INT_MAX – INT_MIN)/4 + 1.
#include <stdio.h>

void set_min_max(int a[], long long unsigned size,\
                 int* min_addr, int* max_addr)
{
    long long unsigned i;

    if(!size) return;
    *min_addr = *max_addr = a[0];
    for(i = 1; i < size; ++i)
    {
        if(a[i] < *min_addr) *min_addr = a[i];
        if(a[i] > *max_addr) *max_addr = a[i];
    }
}

void print_repeats(int a[], long long unsigned size)
{
    long long unsigned i;
    int min, max = min;
    long long diff, q, r;
    char* duos;

    set_min_max(a, size, &min, &max);
    diff = (long long)max - (long long)min;
    duos = calloc(diff / 4 + 1, 1);
    for(i = 0; i < size; ++i)
    {
        diff = (long long)a[i] - (long long)min; /* index of duo-bit
                                                    corresponding to a[i]
                                                    in sequence of duo-bits */
        q = diff / 4; /* index of byte containing duo-bit in "duos" */
        r = diff % 4; /* offset of duo-bit */
        switch( (duos[q] >> (6 - 2*r )) & 3 )
        {
            case 0: duos[q] += (1 << (6 - 2*r));
                    break;
            case 1: duos[q] += (1 << (6 - 2*r));
                    printf("%d ", a[i]);
        }
    }
    putchar('\n');
    free(duos);
}

void main()
{
    int a[] = {1, 0, -2, 4, 4, 1, 3, 1, -2};
    print_repeats(a, sizeof(a)/sizeof(int));
}

Ответы [ 7 ]

7 голосов
/ 21 ноября 2011

Определение нотации big-O состоит в том, что ее аргумент является функцией ( f (x) ), которая в качестве переменной в функции ( x ) стремится к бесконечности, существует постоянная K , так что функция целевой стоимости будет меньше, чем Kf (x) . Обычно f выбирается как наименьшая такая простая функция, чтобы условие выполнялось. (Совершенно очевидно, как поднять вышесказанное для нескольких переменных.)

Это важно, потому что K - который вам не требуется указывать - позволяет скрывать все множество сложных действий. Например, если ядром алгоритма является O (n 2 ), он допускает все виды других O (1), O (logn), O (n), O (nlogn), O ( n 3/2 ) и т. д., поддерживающие скрываемые биты, , даже если для реалистичных входных данных эти части действительно являются доминирующими. Это верно, это может вводить в заблуждение! (Некоторые из более красивых алгоритмов bignum обладают этим свойством по-настоящему. Ложь с математикой - замечательная вещь.)

Так, куда это идет? Что ж, вы можете предположить, что int является достаточно фиксированным размером (например, 32-битным) и использовать эту информацию, чтобы пропустить много проблем, и выделить фиксированный размер массивов битов флага для хранения всех информация, которая вам действительно нужна. Действительно, используя два бита для каждого потенциального значения (один бит, чтобы сказать, видели ли вы значение вообще, другой, чтобы сказать, напечатали ли вы его), вы можете обрабатывать код с фиксированным фрагментом памяти размером 1 ГБ. Это даст вам достаточно информации о флаге, чтобы справиться с таким количеством 32-битных целых чисел, которое вы, возможно, когда-либо захотите обработать. (Черт, это даже практично на 64-битных машинах.) Да, потребуется некоторое время, чтобы настроить этот блок памяти, но он постоянен, поэтому формально O (1) и поэтому выпадает из анализа. Учитывая это, вы получаете постоянное (но колоссальное) потребление памяти и линейное время (вы должны посмотреть на каждое значение, чтобы увидеть, новое оно, увиденное один раз и т. Д.), И это именно то, о чем просили.

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


[EDIT] Пример C кода (это не C ++, но я не очень хорош в C ++; главное отличие будет в том, как распределяются и управляются массивы флагов):

#include <stdio.h>
#include <stdlib.h>

// Bit fiddling magic
int is(int *ary, unsigned int value) {
    return ary[value>>5] & (1<<(value&31));
}
void set(int *ary, unsigned int value) {
    ary[value>>5] |= 1<<(value&31);
}

// Main loop
void print_repeats(int a[], unsigned size) {
    int *seen, *done;
    unsigned i;

    seen = calloc(134217728, sizeof(int));
    done = calloc(134217728, sizeof(int));

    for (i=0; i<size; i++) {
        if (is(done, (unsigned) a[i]))
            continue;
        if (is(seen, (unsigned) a[i])) {
            set(done, (unsigned) a[i]);
            printf("%d ", a[i]);
        } else
            set(seen, (unsigned) a[i]);
    }

    printf("\n");
    free(done);
    free(seen);
}

void main() {
    int a[] = {1,0,-2,4,4,1,3,1,-2};
    print_repeats(a,sizeof(a)/sizeof(int));
}
5 голосов
/ 21 ноября 2011

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

2 голосов
/ 21 ноября 2011

Ограничение пространства O (1) неразрешимо.

Сам факт печати массива требует хранения O (N), по определению.

Теперь, чувствуя себя щедрым, я 'Я дам вам, что вы можете иметь O (1) хранилище для буфера в вашей программе и считать, что пространство, занимаемое вне программы, вас не касается, и, таким образом, вывод не является проблемой ...

Тем не менее, ограничение пространства O (1) кажется неразрешимым из-за ограничения неизменяемости входного массива.Возможно, это не так, но это так.

И ваше решение переполнено, потому что вы пытаетесь запомнить O (N) информацию в конечном типе данных.

1 голос
/ 21 ноября 2011

Я сомневаюсь, что это возможно.Предполагая, что есть решение, давайте посмотрим, как оно работает.Я постараюсь быть как можно более общим и покажу, что это не может работать ... Итак, как это работает?

Не теряя общности, мы можем сказать, что мы обрабатываем массив k раз, где kфиксированный.Решение также должно работать при наличии m дубликатов, где m >> k.Таким образом, по крайней мере, на одном из проходов мы должны быть в состоянии вывести x дубликатов, где x увеличивается с ростом m.Для этого некоторая полезная информация была вычислена в предыдущем проходе и сохранена в хранилище O (1).(Сам массив не может быть использован, это даст O (n) памяти.)

Проблема: у нас есть O (1) информации, когда мы идем по массиву, мы должны идентифицировать x чисел(выводить их).Нам нужно O (1) хранилище, которое может сказать нам за O (1) время, если в нем есть элемент.Или, по-другому, нам нужна структура данных для хранения n логических значений (из которых x true), которые используют пространство O (1) и занимают O (1) время для запроса.

Существует ли эта структура данных?Если нет, то мы не можем найти все дубликаты в массиве с O (n) временем и O (1) пространством (или есть какой-то причудливый алгоритм, который работает совершенно по-другому ???).

1 голос
/ 21 ноября 2011

Я действительно не понимаю, как вы можете иметь только O (1) пробел и не изменять исходный массив.Я предполагаю, что вам нужна дополнительная структура данных.Например, каков диапазон целых чисел?Если оно равно 0..N, как и в другом связанном вопросе, вы можете иметь массив дополнительных чисел размера N. Затем в O (N) просмотрите исходный массив и увеличьте счетчик в позиции текущего элемента.Затем пройдитесь по другому массиву и напечатайте числа с числом> = 2. Что-то вроде:

int* counts = new int[N];
for(int i = 0; i < N; i++) {
    counts[input[i]]++;
}

for(int i = 0; i < N; i++) {
    if(counts[i] >= 2) cout << i << " ";
}

delete [] counts;
1 голос
/ 21 ноября 2011

Здесь есть сложная проблема с определениями.Что означает O (n)?

В ответе Константина утверждается, что сложность времени сортировки по основанию равна O (n).Фактически это O (n log M), где основание логарифма - это выбранный радиус, а M - диапазон значений, которые могут иметь элементы массива.Так, например, 32-разрядные целочисленные двоичные числа типа radix будут иметь log M = 32.

Так что это в некотором смысле O (n), потому что log M является константой, независимой от n,Но если мы допустим это, то есть гораздо более простое решение: для каждого целого числа в диапазоне (все 4294967296 из них), просмотрите массив, чтобы увидеть, встречается ли оно более одного раза.В некотором смысле это также O (n), поскольку 4294967296 также является константой, не зависящей от n.

Я не думаю, что мое простое решение будет считаться ответом.Но если нет, то мы также не должны разрешать сортировку по основанию.

0 голосов
/ 21 ноября 2011

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

Это имеет серьезные ограничения, но работает в этом случае. Числа должны быть между -n / 2 и n / 2, и если они повторяются m раз, они будут напечатаны m / 2 раза.

void print_repeats(long a[], unsigned size) {
    long i, val, pos, topbit = 1 << 31, mask = ~topbit;
    for (i = 0; i < size; i++)
        a[i] &= mask;

    for (i = 0; i < size; i++) {
        val = a[i] & mask;
        if (val <= mask/2) {
           pos = val;
        } else {
            val += topbit;
            pos = size + val;
        }
        if (a[pos] < 0) {
            printf("%d\n", val);
            a[pos] &= mask;
        } else {
            a[pos] |= topbit;
        }
    }
}

void main() {
    long a[] = {1, 0, -2, 4, 4, 1, 3, 1, -2};
    print_repeats(a, sizeof (a) / sizeof (long));
}

печать

4
1
-2
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...