Найти три числа появились только один раз - PullRequest
16 голосов
/ 09 июня 2010

В последовательности длиной n, где n = 2k + 3, то есть k уникальных чисел появилось дважды, а три числа появились только один раз.

Вопрос: как найти три уникальных числа, которые появились только один раз?

, например, в последовательности 1 1 2 6 3 6 5 7 7 триуникальные числа: 2 3 5.

Примечание: 3 <= n <1e6, и число будет варьироваться от 1 до 2e9 </p>

Ограничения памяти: 1000 КБ, это означает, что мы не можем сохранитьвся последовательность.

Метод, который я попробовал (Превышен лимит памяти):

Я инициализирую дерево, и при чтении в одно число я пытаюсь удалить его из дерева, если при удалении возвращается false (не найдено), я добавляю его в дерево.Наконец, дерево имеет три числа.Это работает, но предел памяти превышает.

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

мы сможем найти три, используя один и тот же метод (или какой-то похожий метод)?

Метод поиска одного / двух чисел появился только один раз:

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

Если их два, мы можем сначала применить XOR к последовательности, а затем отделитьПоследовательность в 2 части на один бит результата, который равен 1, и снова примените XOR к 2 частям, и мы найдем ответ.

Ответы [ 6 ]

9 голосов
/ 10 июня 2010

Для более общей версии этой проблемы (без этих глупых ограничений):

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

Вот (псевдо) код, чтобы найти только одно из чисел:

// Given an array arr with 2k+3 numbers, k of which are repeated twice
// and the remaining three are distinct: a,b,c.
// returns one of a,b,c.
int FindUnique(int []arr) {

    int s = 0; // This will ultimately hold a ^ b ^ c (bitwise XOR)

    for (int i = 0; i < arr.Length; i++) {
        s ^= arr[i];
    }

    int d = 0; // this holds diff(a,s) ^ diff(b,s) ^ diff(c,s)

    for (int i = 0; i < arr.Length; i++) {
        d ^= diff(arr[i],s);
    }

    int e = lowestBit(d); // This gives the position where one of a,b,c differs 
                          // from the others.

    int bucket1 = 0;
    int bucket2 = 0;

    for (int i = 0; i < arr.Length; i++) {
        if (arr[i] & e) {
            bucket1 ^= arr[i];
        } else {
            bucket2 ^= arr[i];
        }
    }

    int count1 = 0;
    int count2 = 0;

    for (int i = 0; i < arr.Length; i++) {
        if (arr[i] == bucket1) {
            count1++;
        }

        if (arr[i] == bucket2) {
            count2++;
        }
    }

    if (count1 == 1) return bucket1;

    return bucket2;
}

// return a number with the lowest bit of x ^ s set to 1 and rest 0.
// i.e. the lowest bit position where x and s differ.
int diff(int x, int s) {
    return lowestBit(x ^ s);
}

// Returns a number with only the lowest bit of y set.
int lowestBit(int y) {
    return y & ~(y-1);
}

Идея заключается в следующем:

Скажите, что числа, которые появляются однажды, являются a, b, c.

Теперь запустите XOR через массив, чтобы получить s = a XOR b XOR c.

Поскольку числа различны, обратите внимание, что s не может быть либо a, либо b, либо c (так как остальные два будут равны), таким образом, существует по крайней мере один бит (не обязательно в одной и той же позиции), где каждый из a, b, c отличается от s.

В случае двух чисел мы могли видеть, что s ненулевой, и выбирать бит, который дифференцирует a & b и работать с этим.

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

Для каждого числа x найдите младший бит, который отличается от s. Рассмотрим двоичное число, в котором только этот бит установлен в единицу, а остальные равны нулю. Назовите этот номер diff (x).

Теперь, если мы вычислим diff (x) для каждого числа и XOR их вместе, мы получим d = diff (a) XOR diff (b) XOR diff (c).

Обратите внимание, что d не может быть нулем.

Теперь найдите младший установленный бит d. Эта битовая позиция может использоваться для выгрузки одного из a, b, c, поскольку не все a, b, c могут иметь один и тот же бит в этой позиции: если они это сделали, то тот бит s, который является XOR этих три должны быть одинаковыми, но мы убедились, что мы выбрали этот бит s, чтобы он отличался по крайней мере от одного из соответствующих битов в a, b, c.

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

Чтобы найти разность, используйте битхак: x & ~(x-1), который является стандартным бит-хаком и может рассматриваться как O (1) (вместо O (количество бит)).

7 голосов
/ 09 июня 2010

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

Нам нужны два целых числа для каждого бита чисел (например, 32 бита).Для каждого числа, если этот бит равен нулю, XOR первое целое число с ним.Если это не так, то XOR второе целое с ним.

Кроме того, ведите подсчет того, сколько раз вы находите 1 или 0 в каждой позиции (нам нужно только проверить, является ли это четным или нечетным,оставьте логическое значение).

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

0, a^b^c
a^b, c
a^c, b
b^c, a

Для каждой пары проверьте целое число четного числа.Если оно равно нулю, то мы знаем, что другое целое число является a ^ b ^ c, поскольку никакие два наших результата не будут равны.В противном случае мы нашли значение в нечетном целом числе.

public static int[] find3(int[] list) {
    int[][] xors = new int[32][2];
    boolean[] counts = new boolean[32];
    for (int curr : list) {
        for (int i = 0; i < 32; i++) {
            xors[i][(curr & (1 << i)) >> i] ^= curr;
            counts[i] ^= ((curr & (1 << i)) == (1 << i));
        }
    }

    // this really shouldn't take so many lines
    int[] ret = new int[3];
    int found = 0;
    for (int i = 0; i < 32; i++) {
        int oddCount = xors[i][counts[i] ? 1 : 0];
        int evenCount = xors[i][counts[i] ? 0 : 1];
        if (evenCount != 0) { // avoid the 0, a^b^c case.
            if (found == 0) {
                ret[0] = oddCount;// a
                ret[2] = evenCount;// b^c for now
                found++;
            } else if (found == 1 && ret[0] != oddCount) {
                ret[1] = oddCount;// b
                ret[2] ^= oddCount;// (b^c)^b == c
                break;
            }
        }
    }
    return ret;
}
7 голосов
/ 09 июня 2010

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

Например, если числа в списке должны быть в диапазоне от 1 до 20, вы выделяете 20 битов - по одному для каждого числа и инициализируете каждый бит как 0.

Затем вы пересекаете список,Каждый раз, когда вы видите число, щелкните соответствующий бит.

Например: с вашим примером списка 2 6 3 6 5 7 7 мы могли бы выделить 7 бит (для 1 2 3 4 5 6 7).Затем, просматривая список, мы сделаем следующее:

  • перевернуть 2-й бит
  • перевернуть 6-й бит
  • перевернуть 3-й бит
  • перевернуть6-й бит
  • и т. Д.

Затем, пройдя по списку, вы можете прочитать биты, чтобы найти три уникальных числа.Все они будут представлены битами «1», а остальные числа будут представлены нулями.

Вы дважды просматриваете список, что занимает 2n времени, что равно O (n).


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

Однако может возникнуть одна проблема: список может быть очень маленьким, но некоторыеочень большие числа - эффективно делает диапазон слишком большим.Например:

1, 99999999999999999, 1, 99999999999999999, 2, 3, 4

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

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

  1. Пусть L обозначает исходный список, а C обозначает его копию.
  2. Удаляет все дубликаты из C (существует множество способов сделать это эффективно).
  3. Создайте хеш-таблицу H и для каждого элемента в C вставьте пару ключ / значение <<code>number, pos> в H, где number - текущий элементв C, а pos - его позиция в C.Итак, учитывая число, которое появляется в L, теперь мы можем использовать H, чтобы найти позицию этого числа в C.
  4. . Выделить количество бит, равное размеру C, иинициализируйте эти биты в 0.
  5. Traverse L.Каждый раз, когда мы пробегаем число, получаем его значение из H и добавляем этот бит в наш список битов.
  6. Переходим по списку битов - для каждого бита '1' получаем число из Cкоторый находится в этой позиции - это одно из уникальных чисел.
6 голосов
/ 09 июня 2010

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

Создайте два фильтра Блума. Первый (A) содержит числа, которые были найдены, по крайней мере, один, а второй (B) содержит числа, которые были найдены дважды.

псевдокод:

A = empty
B = empty

foreach x in the list
  if x in A
    add x to B
  else
    add x to A

foreach x in the list
  if x in A
    if !(x in B)
      print x

Если вы используете полную 1000KB, тогда вероятность ошибки будет смехотворно низкой.

1 голос
/ 09 июня 2010

Проблема становится все сложнее и сложнее, когда вы добавляете больше уникальных значений, главным образом потому, что вы можете выбрать A, B, C так, чтобы A xor B xor C = 0. Обнаружить, что поднабор значений имеетта же самая контрольная сумма, потому что она содержит все уникальные значения, или потому что она пропустила значения, которые произошли с xor до 0.

Вы можете сделать 3 значения в постоянном пространстве и O (n * k) времени, где k - этоколичество бит в наибольшем целом числе.(Так что O (n) время для вашего типичного случая: 32-разрядные целые числа.)

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

//Special check for 0, because otherwise we don't know A xor B xor C != A xor B
if items unique-contains 0 then
    return 0 ++ SubProblem2Unique(items - 0)
//Compute A xor B xor C
val x = fold xor items
//Try to find a split which separates A and B from C.
for i in 0..WORD_SIZE
    //see if the checksum splits
    val x1 = fold xor [e in items where e & (1<<i) == 0]
    val x2 = x xor x1
    if x1 == x or x2 == x then continue //ith bit was the same for A and B and C
    //C is either x1 or x2
    val C = if items unique-contains x1 then x1 else x2
    return C ++ SubProblem2Unique(items - C)

throw InvalidInput
0 голосов
/ 02 июля 2010

Почему бы не использовать хэш-сет? - Если номер уже существует, удалите из хэш-набора - если номер не существует, поместите в хешсет Конечный хэшсет содержит только уникальные числа. Время: O (n) Память: o (k) где k - количество различных элементов.

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

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