Как найти число, которое встречается нечетное количество раз в массиве SORTED за время O(n)?

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

Вопрос в следующем: нам дан отсортированный массив, который состоит из набора значений, встречающихся ЧЕТНОЕ количество раз, за ​​исключением одного, которое встречается НЕЧЕТНОЕ количество раз. Нам нужно найти решение за log n времени.

Решение легко найти за время O(n), но выполнить его за время log n довольно сложно.


person AGeek    schedule 05.07.2010    source источник
comment
Являются ли числа в массиве последовательными целыми числами? Кажется, ни один из ответов пока не лучше, чем ((log N)^2), но также ни один из них напрямую не использует отсортированный характер массива.   -  person Pete Kirkham    schedule 05.07.2010
comment
Крутая проблема! Где задают такие милые задачки в качестве домашнего задания? :)   -  person Krystian    schedule 05.07.2010
comment
Ниже приведен отличный ответ от throwawayacct, который, кажется, показывает (я не могу найти ни одной ошибки в доказательстве), что любой алгоритм — это Ω((log n)^2).   -  person ShreevatsaR    schedule 06.07.2010
comment
technicalinterviewquestions.net/2009_01_01_archive.html — не решение вышеуказанного, а интересное решение в O(n) раз с памятью O(1).   -  person Will A    schedule 06.07.2010
comment
@Will A: O (N) время и O (1) решение тривиальны, я могу обнаружить k-е нечетное число с такими требованиями.   -  person Matthieu M.    schedule 06.07.2010
comment
@AGeek Пожалуйста, опубликуйте решение после окончания периода принятия вашего задания. Спасибо.   -  person Dave O.    schedule 06.07.2010
comment
@Bragboy будет сложно, если на все вопросы будет так же сложно ответить, как на этот^^.   -  person Dave O.    schedule 06.07.2010
comment
Это больше похоже на вопрос на собеседовании, чем на домашнее задание.   -  person Anonym Mus    schedule 07.07.2010
comment
Обратите внимание, что этот поиск (без временного ограничения ‹O(n)) широко публикуется как вопрос на собеседовании, причем предпочтительным ответом является XOR для каждого элемента (независимо от того, отсортирован массив или нет).   -  person joe snyder    schedule 09.07.2010
comment
@joe snyder и кто-либо еще: решение проблемы, которая выполняется за O (log (n) ^ 2), тривиально. Решение, которое работает даже за O(log(n)) еще предстоит найти(!) - или доказательство того, что такого алгоритма не существует.   -  person Dave O.    schedule 09.07.2010
comment
@Dave: стремление к более быстрому поиску было очевидным; я просто подумал, что XOR, не зависящий от сортировки, заслуживает внимания из-за его хитрости, хотя Уилл А на самом деле первым сослался на него.   -  person joe snyder    schedule 10.07.2010
comment
@Dave: я продумал этот вопрос и убежден, что у пользователя throwawayacct есть полное доказательство того, что вы не можете добиться большего, чем O (log (n) ^ 2). С таким же успехом вы могли бы просто принять его решение, потому что он сделал сложную часть.   -  person Greg Kuperberg    schedule 12.07.2010
comment
@Greg: я все еще не уверен в доказательстве (но это, вероятно, больше моя проблема, чем проблема с доказательством). Например, похоже, отсутствует возможность гарантировать время Omega(logn) для каждого определения границы. Утверждение, что одна граница «независима» от другой, кажется, требует большего обоснования, поскольку предыдущая история сравнений, выполненных алгоритмом, действительно помогает сузить будущие границы, но это, похоже, игнорируется в доказательстве.   -  person    schedule 12.07.2010
comment
Теперь я в этом убежден, после перезаписи с помощью throwawayacct и поддерживающего ответа Грега, fwiw.   -  person    schedule 12.07.2010
comment
@AGeek, нам все еще интересно узнать о решении / мнении вашего профессора об ответах сообщества SO! Пожалуйста, не держите нас в неведении ;)   -  person Dave O.    schedule 13.07.2010


Ответы (15)


Теорема: каждый детерминированный алгоритм для этой задачи проверяет Ω(log2 n) ячейки памяти в худший случай.

Доказательство (полностью переписано в более формальном стиле):

Пусть k > 0 — нечетное целое число, и пусть n = k2. Мы описываем противника, который заставляет (log2 (k + 1))2 = Ω(log2 n) зондов.

Мы называем максимальные подпоследовательности одинаковых элементов группами. Возможные входные данные злоумышленника состоят из k длины-k сегментов x1 x2 … xk. Для каждого отрезка xj существует целое число bj ∈ [0, k], такое что xj состоит из bj копий j - 1, за которыми следуют k - bj копий j. Каждая группа перекрывает не более двух сегментов, а каждый сегмент перекрывает не более двух групп.

Group boundaries
|   |     |   |   |
 0 0 1 1 1 2 2 3 3
|     |     |     |
Segment boundaries

Везде, где есть увеличение на два, мы условно предполагаем двойную границу.

Group boundaries
|     ||       |   |
 0 0 0  2 2 2 2 3 3

Утверждение: Расположение jй границы группы (1 ≤ j ≤ k) однозначно определяется отрезком xj.

Доказательство: сразу после ((j - 1) k + bj)th ячейки памяти и xj однозначно определяет bj. //

Мы говорим, что алгоритм обнаружил границу группы jth, если результаты его проверки xj однозначно определяют xj . По соглашению всегда соблюдаются начало и конец ввода. Алгоритм может однозначно определить местоположение границы группы, не наблюдая за ней.

Group boundaries
|   X   |   |     |
 0 0 ? 1 2 2 3 3 3
|     |     |     |
Segment boundaries

Учитывая только 0 0 ?, алгоритм не может точно сказать, является ли ? это 0 или 1. Однако в контексте ? должно быть 1, иначе было бы три нечетных группы, и можно вывести границу группы в точке X. Эти выводы могут быть проблематичными для противника, но оказывается, что они могут быть сделаны только после того, как рассматриваемая групповая граница станет «неактуальной».

Требование: В любой момент выполнения алгоритма следует рассмотреть набор наблюдаемых границ группы. Ровно одна последовательная пара находится на нечетном расстоянии, а нечетная группа лежит между ними.

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

Определите подпоследовательность нечетной длины, ограниченную специальной последовательной парой, как релевантную подпоследовательность.

Утверждение: Ни одна граница группы внутри соответствующей подпоследовательности не определена однозначно. Если существует хотя бы одна такая граница, то идентичность нечетной группы не определяется однозначно.

Доказательство: без ограничения общности предположим, что каждая ячейка памяти, не входящая в соответствующую подпоследовательность, была исследована и что каждый сегмент, содержащийся в соответствующей подпоследовательности, имеет ровно одну неисследованную ячейку. Предположим, что граница группы jth (назовем ее B) лежит внутри соответствующей подпоследовательности. Согласно гипотезе, запросы к xj определяют местоположение B до двух последовательных возможностей. Мы называем один на нечетном расстоянии от левой наблюдаемой границы нечетным-левым, а другой нечетным-правым. Для обеих возможностей мы работаем слева направо и фиксируем расположение каждой оставшейся внутренней границы группы, чтобы группа слева от нее была четной. (Мы можем сделать это, потому что у каждого из них также есть две последовательные возможности.) Если B находится в нечетном левом положении, то группа слева от него является единственной нечетной группой. Если B нечетно справа, то последняя группа в соответствующей подпоследовательности является уникальной нечетной группой. Оба являются допустимыми входными данными, поэтому алгоритм однозначно не определил ни местоположение B, ни нечетную группу. //

Пример:

Observed group boundaries; relevant subsequence marked by […]
[             ]   |
 0 0 Y 1 1 Z 2 3 3
|     |     |     |
Segment boundaries

Possibility #1: Y=0, Z=2
Possibility #2: Y=1, Z=2
Possibility #3: Y=1, Z=1

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

В любой заданный момент выполнения алгоритма злоумышленник внутренне привязан к одной возможности для каждой ячейки памяти за пределами соответствующей подпоследовательности. В начале релевантной подпоследовательностью являются все входные данные, поэтому первоначальных обязательств нет. Всякий раз, когда алгоритм проверяет незафиксированное местоположение xj, противник должен зафиксировать одно из двух значений: j - 1 или j. Если он может избежать наблюдения за границей jth, он выбирает значение, которое оставляет по крайней мере половину оставшихся возможностей (относительно наблюдения). В противном случае он выбирает так, чтобы сохранить по крайней мере половину групп в соответствующем интервале и фиксирует значения для остальных.

Таким образом, противник заставляет алгоритм соблюдать как минимум log2 (k + 1) групповых границ, а при соблюдении jй границы группы алгоритм вынужден сделать не менее log2 (k + 1) проб.


Расширения:

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

Это также распространяется на случай, когда никакая группа не может быть больше s копий; в этом случае нижняя граница равна Ω(log n log s).

person user382751    schedule 05.07.2010
comment
Ваш аргумент относится к классу алгоритмов, которые последовательно выполняют бинарный поиск, чтобы найти границы. Могут быть и другие подходы, поэтому написание каждого детерминированного алгоритма — это немного сложно. Ваш аргумент является ценным и довольно умным и касается подхода, который, вероятно, у всех на уме. Я бы отказался только от каждой части детерминированного алгоритма. - person Bolo; 06.07.2010
comment
Нет, это общая нижняя граница, поскольку структура входных данных ограничивает объем информации, которую можно собрать одним запросом. - person user382751; 06.07.2010
comment
Вы правы, это является общим аргументом и кажется убедительным. (Единственная сомнительная часть для меня заключалась в том, сможет ли противник сделать одну сторону странной, несмотря на предыдущие исследования, но да, может.) Отличный ответ! - person ShreevatsaR; 06.07.2010
comment
Я не совсем убежден. Почему каждый алгоритм должен находить границы? Зачем нам вообще нужна информация о границах? Даже если предположить, что каждый алгоритм должен каким-то образом определять границы, как мы можем обеспечить время проверки Omega(logn) для последующих границ? - person ; 06.07.2010
comment
О нижней границе омега (log n): скажем, алгоритм смотрит на 0. Шагает вправо, находит 3, делает шаг влево и находит 2, делает еще один шаг влево и находит 1, затем 0 и т. д. Теперь мы могли бы использовать информацию о 1, 2 и 3 при последующем нахождении границ, не так ли? Как вы добились того, что эта информация не повлияет на время проверки Omega(logn)? Поиск границы 0-1 не говорит вам, где находится граница 1-2, но помогает сузить ее. Так что он не совсем независимый... - person ; 06.07.2010
comment
В целом это верно, но для набора входных данных, используемых этой нижней границей, априори наборы местоположений для двух разных границ могут соприкасаться, но не перекрываться, предотвращая такой вывод. - person user382751; 06.07.2010
comment
Не уверен на 100%, но думаю, вы на правильном пути. Такой анализ проблемы стоит намного больше, чем исходный код с неправильными оценками, которые многие другие люди продолжают публиковать... - person R.. GitHub STOP HELPING ICE; 06.07.2010
comment
Я не понимаю, как можно избежать дублирования. Что, если алгоритм прыгнул на большее расстояние, чем sqrt(n) для некоторого теста? В любом случае все, что я говорю, это то, что самая правая 1 и самая левая 2 - это то, до чего сужается следующая граница. Как убедиться, что это Omega(n^c)? С несколькими границами у вас есть несколько таких диапазонов. В любом случае, извините за столько вопросов... - person ; 06.07.2010
comment
Та же проблема с потенциальной функцией: Случай 1) В лучшем случае вдвое сократить возможные местоположения границы. Мы могли бы повлиять на несколько границ и их возможное расположение. Случай 2) В какой-то момент, когда мы фиксируем границу, предыдущий набор возможностей для этой границы не обязательно должен быть равен k+1, и, следовательно, компенсирующий член увеличения не обязательно должен быть log(k+1). Извините, но я не убежден, но это, вероятно, моя неспособность понять, что именно вы делаете. Вы уже потратили много усилий, пытаясь объяснить мне, я больше не буду вас беспокоить. - person ; 06.07.2010
comment
Я думаю, что ваш ответ слишком пессимистичен из-за модели сложности/стоимости, которая у вас есть. Как указывает ответ Ротсора, все меняется, когда вы переходите к более реалистичным моделям. В частности, я думаю, что любая полезная нижняя граница должна также включать его параметр K. - person Jens Gustedt; 08.07.2010
comment
Поскольку это не является реальной проблемой, я не вижу причин, по которым реалистичная модель должна иметь параметр K. Тем не менее, с N элементами и не более чем K копиями любого одного элемента этот аргумент легко обобщается, чтобы получить нижнюю границу Ω(log N log К). - person user382751; 08.07.2010
comment
(Комментарии выше относятся к предыдущей версии.) - person user382751; 12.07.2010
comment
Отлично, я хотел бы снова проголосовать за это! См. также переписывание этого ответа Грегом Купербергом ниже (и более старую версию этого ответа). - person ShreevatsaR; 12.07.2010
comment
@throwawayacct спасибо за ваши усилия по переписыванию вашего аргумента. Только это помогло мне понять это. - person Dave O.; 13.07.2010
comment
@ user382751 - подумайте об этом. Учитывая диапазон R ст.ст. |Р| четно, мы можем сравнивать его конечные точки за постоянное время. Если они совпадают, мы можем исключить R из рассмотрения. Если нет, то это операция lg(R) (бинарный поиск), чтобы найти границу в R. Если мы выберем R, чтобы он находился в центре части массива, который мы ищем, это устраняет (примерно) половину пространства поиска. за время lg(R). Наши два случая состоят в том, что либо мы исключаем индексы R за постоянное время, либо n/2 за время lg(R). Решите R*lg(R) = n/2, чтобы найти наилучшее значение R. Это займет lg(R)*lg(N). - person Dave; 03.10.2014
comment
@user382751 user382751 На простом английском языке это означает, что задача не может быть выполнена за O (log N) временной сложности, не так ли? - person SadeepDarshana; 30.09.2017

Отсортированный массив предполагает бинарный поиск. Мы должны дать новое определение равенству и сравнению. Равенство простое означает нечетное количество элементов. Мы можем выполнить сравнение, наблюдая за индексом первого или последнего элемента группы. Первый элемент будет иметь четный индекс (начиная с 0) перед нечетной группой и нечетный индекс после нечетной группы. Мы можем найти первый и последний элементы группы с помощью бинарного поиска. Общая стоимость составляет O((log N)²).

ДОКАЗАТЕЛЬСТВО O((log N)²)

  T(2) = 1 //to make the summation nice
  T(N) = log(N) + T(N/2) //log(N) is finding the first/last elements

Для некоторого N=2^k,

T(2^k) = (log 2^k) + T(2^(k-1))
       = (log 2^k) + (log 2^(k-1)) + T(2^(k-2))
       = (log 2^k) + (log 2^(k-1)) + (log 2^(k-2)) + ... + (log 2^2) + 1
       = k + (k-1) + (k-2) + ... + 1
       = k(k+1)/2
       = (k² + k)/2
       = (log(N)² + log(N))/ 2
       = O(log(N)²)
person Nabb    schedule 05.07.2010
comment
Придирка: алгоритм O(log N) также является алгоритмом O(log‹sup›2‹/sup›N). Ω здесь интереснее. - person Matthieu M.; 06.07.2010
comment
@Matthieu... Придирка к мелочам: Тета еще интереснее :-) - person ; 06.07.2010
comment
@Moron: Да, я так думаю: p Однако текущая дискуссия, похоже, переросла в доказательство того, что это невозможно сделать менее чем за log2 N. Я все еще не понимаю, как это можно доказать, хотя алгоритмы сортировки доказали, что с учетом некоторых ограничений вы могли бы получить лучшие асимптотические характеристики, которые предлагали общие модели, поэтому мне интересно, можно ли это как-то применить здесь. - person Matthieu M.; 07.07.2010
comment
С другой стороны, хотя входные данные с 0, 1 и 2 специально созданы для сортировки подсчетом O(n), бинарный поиск по-прежнему оптимален в худшем случае при Θ(log n) запросах. - person user382751; 08.07.2010
comment
Это хороший ответ, но вопрос требует решения O (log n), а это не достигает его — только O ((log n) ^ 2), что также получили многие другие. Так почему же это самый популярный ответ? :п - person ShreevatsaR; 09.07.2010
comment
@ShreevatsaR: Я согласен, почему менее оптимальный алгоритм получает здесь голоса? - person Dean J; 12.07.2010
comment
Что ж, это все еще хороший ответ с кратким объяснением самого известного алгоритма и полным анализом. Учитывая доказательство, содержащееся в ответе, который, к счастью, получил наибольшее количество голосов, этот алгоритм является оптимальным и заслуживает одобрения. :-) - person ShreevatsaR; 12.07.2010

Посмотрите на средний элемент массива. С помощью пары подходящих бинарных поисков вы можете найти первое и последнее появление в массиве. Например, если средний элемент — «a», вам нужно найти i и j, как показано ниже:

[* * * * a a a a * * *]
         ^     ^ 
         |     |
         |     |
         i     j

j - i четное число? Вы сделали! В противном случае (и это ключевой момент здесь) следует задать вопрос является ли я четным или нечетным числом? Вы понимаете, что означает это знание? Тогда остальное легко.

person Dimitris Andreou    schedule 05.07.2010
comment
А, я вижу, Джерри отдал его! :) - person Dimitris Andreou; 05.07.2010
comment
Если ji четно, вам нужно повторить свой алгоритм с половиной массива. i-я итерация занимает (log N) - i шагов, поэтому у вас есть O ( (log N) ^ 2 ) шагов. - person Pete Kirkham; 05.07.2010
comment
Не совсем. На i-й итерации для двух бинарных поисков требуется только log(N / 2^i) - на каждом шаге часть массива, в отношении которой я должен выполнить два бинарных поиска, уменьшается как минимум вдвое. - person Dimitris Andreou; 05.07.2010
comment
@Dimitris - вы можете почти вдвое увеличить количество бинарных поисков, выполнив поиск только одной границы на каждой итерации. В половине случаев вы обнаружите, что нечетное число находится на большей стороне, но вам не нужно беспокоиться о несбалансированном делении пополам. Чтобы получить это, вам нужно большое количество повторений одного и того же значения, которое обрабатывается довольно быстро, когда оно охватывает центр текущего диапазона, так что это не худший случай. - person Steve314; 05.07.2010
comment
@Dimitris log(N / 2^i) = log(N) - i, поэтому равно O(log(n)^2) - person Rotsor; 05.07.2010
comment
@ Steve314, тоже верно. Хотя я не вижу, чтобы это повлияло на большой анализ. Все еще интересно, каким будет решение O (logn) (без предположений на входе). - person Dimitris Andreou; 05.07.2010

Этот ответ поддерживает ответ, опубликованный «throwawayacct». Он заслуживает награды. Я потратил некоторое время на этот вопрос, и я полностью убежден, что его доказательство верно, что вам нужно Ω (log (n) ^ 2) запросов, чтобы найти число, которое встречается нечетное количество раз. Я убежден, потому что в итоге я воссоздал точно такой же аргумент, только просмотрев его решение.

В решении злоумышленник создает входные данные, которые усложняют жизнь алгоритму, но также упрощают работу анализатора-человека. Входные данные состоят из k страниц, каждая из которых содержит k записей. Общее количество записей равно n = k^2, и важно, чтобы O(log(k)) = O(log(n)) и Ω(log(k)) = Ω(log(n)). Для ввода злоумышленник составляет строку длины k вида 00...011...1 с переходом в произвольную позицию. Затем каждый символ в строке расширяется до страницы длины k вида aa...abb...b, где на i-й странице a=i и b=i+1. Переход на каждой странице также находится в произвольной позиции, за исключением того, что четность согласуется с символом, из которого была расширена страница.

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

Вот несколько наблюдений на этом фоне:

1) Если вы хотите узнать четность перехода на странице, выполнив запросы на этой странице, вы должны узнать точную позицию перехода, и вам нужны запросы Ω(log(k)). Любой набор запросов ограничивает точку перехода интервалом, а любой интервал длины более 1 имеет обе четности. Наиболее эффективным поиском перехода на этой странице является бинарный поиск.

2) Самый тонкий и самый важный момент: Есть два способа определить четность перехода внутри конкретной страницы. Вы можете либо сделать достаточно запросов на этой странице, чтобы найти переход, либо определить четность, если найдете одинаковую четность как на более ранней, так и на более поздней странице. От этого «или-или» никуда не деться. Любой набор запросов ограничивает точку перехода на каждой странице некоторым интервалом. Единственное ограничение на четности исходит от интервалов длины 1. В противном случае точки перехода могут свободно шевелиться, чтобы иметь любые согласованные четности.

3) В методе противника нет удачных ударов. Например, предположим, что ваш первый запрос на какой-то странице направлен к одному концу, а не к середине. Поскольку противник не зафиксировал ответ, он может сделать переход на длинную сторону.

4) Конечным результатом является то, что вы вынуждены напрямую проверять четность на страницах Ω(log(k)), и работа для каждой из этих подзадач также составляет Ω(log(k)).

5) Со случайным выбором дела обстоят не намного лучше, чем с состязательным. Математика усложнилась, потому что теперь вы можете получить частичную статистическую информацию, а не строгое да, вы знаете четность или нет, вы ее не знаете. Но это мало что меняет. Например, вы можете указать длину каждой страницы k^2, так что с высокой вероятностью первые запросы log(k) на каждой странице почти ничего не сообщат вам о четности на этой странице. Противник может сделать случайный выбор в начале, и это все еще работает.

person Greg Kuperberg    schedule 12.07.2010
comment
Д'о. Похоже, у нас обоих была идея переписать этот аргумент :P - person user382751; 12.07.2010
comment
Такие вот дела. Я не имел в виду, что мой ответ будет только переписанным, но также и аннотацией или резюме. Может быть, это все еще полезно для этой цели. - person Greg Kuperberg; 12.07.2010
comment
действительно очень полезно. я смог проследить доказательство throwawayacct только после того, как он переписал его, и даже тогда это было очень сложно. Ваши объяснения очень ясны - хотелось бы, чтобы у моего профессора математики были такие же навыки. Спасибо за ваши усилия. слава. - person Dave O.; 13.07.2010
comment
@Dave: Пожалуйста, и, серьезно, похвала, подобная вашей, помогает мне двигаться вперед. - person Greg Kuperberg; 13.07.2010
comment
@GregKuperberg Что вы думаете об этом подходе? Предположим, что мы повторяем следующие действия: 1) определяем R как диапазон элементов четной длины в центре оставшегося пространства поиска. 2) Проверить конечные точки R. 3) Если они совпадают, то удалить R из пространства поиска; Если нет, то в R есть граница, которую мы можем найти с помощью бинарного поиска в R за время log R. То есть мы либо уменьшаем пространство поиска на R за O(1), либо примерно вдвое за O(R). - person Dave; 04.10.2014
comment
@DaveGalvin - Так или иначе, в худшем случае это не поможет, потому что user382751 доказал, что это Ω (log (n) ^ 2), и легко увидеть, что это O ((log N) ^ 2) . Вероятно, R в худшем случае слишком короток, чтобы помочь вам. - person Greg Kuperberg; 13.10.2014

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

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

person Jerry Coffin    schedule 05.07.2010
comment
Разве это не O((log n)^2), так как вы повторяете двоичный поиск log n раз в худшем случае? - person IVlad; 05.07.2010
comment
@IVlad: не совсем - после каждого поиска вы отбрасываете (по крайней мере) половину массива, поэтому при каждом последующем поиске искомая сумма также уменьшается логарифмически. - person Jerry Coffin; 05.07.2010
comment
(log n)+(log n/2)+(log n/4)+... = log(n^(log n)*2^(-(log n)((log n)-1)/2)) = (log n)²-(log n)((log n)-1)/2 = (log n)²/2+(log n)/2 = O((log n)²). Для WolframAlpha: сумма log_2(2^k/2^i) для i=0..k-1. - person Nabb; 05.07.2010
comment
да, но для того, чтобы отбросить эту половину, вам нужно запустить еще один двоичный поиск, который делает его логарифмическим. Я думаю, что публикация некоторого псевдокода сделает это более понятным. - person IVlad; 05.07.2010

У меня есть алгоритм, который работает в формате log(N/C)*log(K), где K — длина максимального диапазона одинаковых значений, а C — длина искомого диапазона.

Основное отличие этого алгоритма от большинства опубликованных ранее заключается в том, что он использует случай, когда все диапазоны одинаковых значений короткие. Он находит границы не путем двоичного поиска по всему массиву, а сначала быстро находит грубую оценку, возвращаясь назад на 1, 2, 4, 8,... (log(K) итераций) шагов, а затем выполняя двоичный поиск результирующий диапазон (снова log(K)).

Алгоритм следующий (написан на C#):

// Finds the start of the range of equal numbers containing the index "index", 
// which is assumed to be inside the array
// 
// Complexity is O(log(K)) with K being the length of range
static int findRangeStart (int[] arr, int index)
{
    int candidate = index;
    int value = arr[index];
    int step = 1;

    // find the boundary for binary search:
    while(candidate>=0 && arr[candidate] == value)
    {
        candidate -= step;
        step *= 2;
    }

    // binary search:
    int a = Math.Max(0,candidate);
    int b = candidate+step/2;
    while(a+1!=b)
    {
        int c = (a+b)/2;
        if(arr[c] == value)
            b = c;
        else
            a = c;
    }
    return b;
}

// Finds the index after the only "odd" range of equal numbers in the array.
// The result should be in the range (start; end]
// The "end" is considered to always be the end of some equal number range.
static int search(int[] arr, int start, int end)
{
    if(arr[start] == arr[end-1])
        return end;

    int middle = (start+end)/2;

    int rangeStart = findRangeStart(arr,middle);

    if((rangeStart & 1) == 0)
        return search(arr, middle, end);
    return search(arr, start, rangeStart);
}

// Finds the index after the only "odd" range of equal numbers in the array
static int search(int[] arr)
{
    return search(arr, 0, arr.Length);
}
person Rotsor    schedule 05.07.2010
comment
Я не читал ваш код полностью, но +1 для вас кажется первым, кто заметил, что K должно быть частью сложности проблемы. Может быть, вы могли бы добавить текстовое описание ваших идей? - person Jens Gustedt; 08.07.2010

Возьмите средний элемент e. Используйте бинарный поиск, чтобы найти первое и последнее вхождение. O(log(n)) Если нечетно, вернуть e. В противном случае рекурсивно перейти на сторону с нечетным числом элементов [....]eeee[....]

Время выполнения будет log(n) + log(n/2) + log(n/4).... = O(log(n)^2).

person Chad Brewbaker    schedule 05.07.2010

Аххх. Есть ответ.

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

В псевдокоде (это общая идея, не проверенная...):

    private static int FindOddBall(int[] ary)
    {
        int l = 0,
            r = ary.Length - 1;
        int n = (l+r)/2;
        while (r > l+2)
        {
            n = (l + r) / 2;
            while (ary[n] == ary[n-1])
                n = FindBreakIndex(ary, l, n);
            if (n % 2 == 0) // even index we are on or to the left of the oddball 
                l = n;
            else            // odd index we are to the right of the oddball
                r = n-1;
        }
        return ary[l];
    }
    private static int FindBreakIndex(int[] ary, int l, int n)
    {
        var t = ary[n];
        var r = n;
        while(ary[n] != t || ary[n] == ary[n-1])
            if(ary[n] == t)
            {
                r = n;
                n = (l + r)/2;
            }
            else
            {
                l = n;
                n = (l + r)/2;
            }
        return n;
    }
person Charles Bretana    schedule 05.07.2010
comment
Я так не думаю, так как этот алгоритм основан на бинарном поиске... удвоение размера массива не удвоит количество итераций, а только увеличит его на 1... Насколько я понимаю, это O (н) - person Charles Bretana; 05.07.2010
comment
@Charles Breatana - удвоение размера массива будет удвоить количество итераций вашего алгоритма. Рассмотрим массив из n - 1 одинаковых чисел и 1 другого числа. Ваш самый внутренний цикл while - O(n), потому что он будет перебирать n / 2 элементов. - person IVlad; 05.07.2010
comment
Ваш пример — это частный случай, который можно исправить, изменив внутренний цикл while, чтобы он делал то же самое. (см. РЕДАКТИРОВАТЬ). В общем, каждая внешняя итерация удаляет половину массива, половину на «неправильной» стороне проверяемого элемента, так что нет, это не будет... - person Charles Bretana; 05.07.2010
comment
@Charles Bretana - особый случай или нет, это делает ваш текущий алгоритм O(n). Я не вижу никакого редактирования, но если мы думаем об одном и том же, это все равно будет не O(log n), а O((log n)^2). - person IVlad; 05.07.2010
comment
Почему загадочные имена переменных? - person Svante; 05.07.2010
comment
@IVlad, возможно, вы правы насчет O((log n) ^2) В общем, вложенные двоичные итерации дадут вам O((log n) ^2), как вы сказали, но я думаю, что это сценарий. могут быть разными, потому что здесь они не являются полностью независимыми вложенными итерациями. Ни внутренняя, ни внешняя итерация не должны перебирать каждый элемент, а только те элементы, которые не были удалены другой итерацией... Это определенно уменьшит количество необходимых шагов обработки и может смягчить O-ness алгоритма (хотя я должен признаться что я не уверен, как именно это вычислить.) - person Charles Bretana; 06.07.2010
comment
@Charles - они на самом деле полностью независимы, почему ты говоришь, что это не так? Я не уверен, что вы подразумеваете под повторением каждого элемента - это бинарный поиск, конечно, они не перебирают каждый элемент. Проблема в том, что у вас есть внутренний поиск, который бинарно ищет диапазон, который был бинарно просканирован вашим внешним поиском (странное предложение, я знаю). Я уверен, что алгоритм очень быстр на практике, но, строго говоря, он равно O((log n)^2), потому что периодичность равна T(n) = log n + T(n / 2), что легко показать как логарифмический квадрат. - person IVlad; 06.07.2010
comment
@IVLad, я склоняюсь к вашему суждению, так как я сам не уверен, как рассчитать O=ness ... однако то, что я имел в виду, возможно, лучше всего проиллюстрировано вашим примером (массив из n - 1 равных чисел и 1 другое число) в этом случае моя внешняя итерация будет обрабатываться только один раз (возможно, дважды), а внутренняя итерация будет обрабатывать остальную часть работы.. тогда как для n/2 пар разных чисел и одного единственного числа внутренний поиск будет обрабатывать только один раз на пару, и итерация pouter будет обрабатывать основную часть работы... - person Charles Bretana; 06.07.2010
comment
@VLad, как это измерить? Запустите его с массивом из 1000 элементов, 10 000 элементов, а затем со 100 000 и сравните результаты? - person Charles Bretana; 06.07.2010
comment
@Charles - я думаю, будет сложно определить, является ли это логарифмическим квадратом или логарифмом, особенно если у вас быстрый процессор. Вы можете попробовать сравнить свой алгоритм с бинарным поиском миллиона или даже 10 миллионов элементов, желательно на более медленном компьютере. Это может нам что-то сказать. Обязательно запустите их несколько раз и возьмите среднее значение. - person IVlad; 06.07.2010
comment
@VLad, я попробую... Ты понимаешь, что я имею в виду? По мере поиска алгоритма каждая часть, сегмент (или кусок) массива обрабатывается и удаляется либо внешней итерацией, либо внутренней, но никогда одновременно... - person Charles Bretana; 06.07.2010
comment
@Charles - понятно, но не уверен, что согласен :). Было бы неплохо, если бы другие поделились своими мыслями. - person IVlad; 06.07.2010
comment
@VLad, я не мог просить большего ... честно говоря, пока я не проверю это, я тоже не уверен ... но ценю, что вы понимаете суть ... - person Charles Bretana; 06.07.2010

Вы можете использовать этот алгоритм:

int GetSpecialOne(int[] array, int length)
{
   int specialOne = array[0];

   for(int i=1; i < length; i++)
   {
      specialOne ^= array[i];
   }
   return specialOne;
}

Решается с помощью аналогичного вопроса, который можно найти здесь на http://www.technicalinterviewquestions.net

person pramod    schedule 21.06.2011

У нас нет никакой информации о распределении длин внутри массива и массива в целом, верно?

Таким образом, длина массива может быть 1, 11, 101, 1001 или что-то в этом роде, по крайней мере 1 без верхней границы, и должен содержать как минимум 1 тип элементов («число») до (длина-1)/2 + 1 элементов, для суммарных размеров 1, 11, 101: 1, от 1 до 6, от 1 до 51 элемента и так далее.

Должны ли мы принять все возможные размеры с равной вероятностью? Это привело бы к средней длине подмассивов размера / 4, не так ли?

Массив размера 5 можно разделить на 1, 2 или 3 подсписка.

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

Массив размера 5 может быть "разделен" на один подсписок только одним способом, с спорным правом называть это "делением". Это просто список из 5 элементов (ааааа). Чтобы избежать путаницы, давайте предположим, что элементы внутри списка являются упорядоченными символами, а не числами (a,b,c,...).

Разделенные на два подсписка, они могут быть (1, 4), (2, 3), (3, 2), (4, 1). (аббб, ааббб, ааабб, ааааб).

Теперь давайте вернемся к заявлению, сделанному ранее: следует ли предположить, что «деление» (5) имеет ту же вероятность, что и эти 4 деления на 2 подсписка? Или мы смешаем их вместе и будем считать каждое деление равновероятным (1/5)?

Или мы можем вычислить решение, не зная вероятности длины подсписков?

person user unknown    schedule 05.07.2010
comment
С моей точки зрения, описание проблемы предельно ясно: Массив длины n, содержащий отсортированные числа. Каждое число встречается четное число раз, кроме одного, которое встречается нечетное число раз. Исключение, кроме одного, подразумевает, что n ›= 1, никаких дальнейших следствий нет! (a), (a,a,a), (a,...(2*i умножить на a)...,a,b) и т. д. являются допустимыми входными данными. - person Dave O.; 07.07.2010
comment
Пока нет проблем, но чтобы решить, какой алгоритм лучше всего находит указанное число, мы должны сделать предположения о типичных массивах, о том, как часто встречается тот или иной случай, что не является тривиальным для неограниченного числа возможных проблем. Может быть, вы можете показать алгоритм, который создает все массивы длиной 5 или все массивы длиной до 5, или с другим ограничением, но то, как вы решите, как создавать эти массивы, будет влиять на вероятность. - person user unknown; 09.07.2010

Подсказка в том, что вы ищете log(n). Это меньше, чем n.

Перебирать весь массив по одному? Это н. Это не сработает.

Мы знаем, что первые два индекса в массиве (0 и 1) должны быть одинаковыми. То же самое с 50 и 51, если нечетное число в массиве после них.

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

Продолжайте идти оттуда.

person Dean J    schedule 12.07.2010
comment
Что, если на датчике не произойдет никакого изменения значения? - person Dave O.; 13.07.2010

Используйте хеш-таблицу

For each element E in the input set
    if E is set in the hash table
         increment it's value
    else        
         set E in the hash table and initialize it to 0

For each key K in hash table
    if K % 2 = 1
        return K

Поскольку этот алгоритм 2n, он принадлежит O (n)

person David Carpenter    schedule 15.07.2014

Попробуй это:

int getOddOccurrence(int ar[], int ar_size)
{
     int i;
     int xor = 0; 
     for (i=0; i < ar_size; i++)     
        xor = xor ^ ar[i];

     return res;
}

XOR будет отменяться каждый раз, когда вы выполняете XOR с одним и тем же числом, поэтому 1 ^ 1 = 0, но 1 ^ 1 ^ 1 = 1, поэтому каждая пара должна отменяться, оставляя нечетное число.

person appiconhero.co    schedule 16.10.2015

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

int a[]=new int[]{2,3,4,2,3,1,4,5,6,5,6,7,1};
int b[]=new int[1000];
for (int i=0;i<b.length;i++) {
    b[i]=0;
}
for (Int i=0;i<a.length;i++) {
    b[a[i]]++;
}
for (int i=0;i<b.length;i++) {
    if ( b[i]!=0) {
        if (b[i] %2==0) {
          system.out.println(i);  break;

    }
}
person dato datuashvili    schedule 07.07.2010
comment
Во-первых, даже одна итерация по входному массиву увеличивает время до O(N), что недопустимо. Во-вторых, числа в массиве могут быть сколь угодно большими (больше 1000 или любой другой конечной границы), поэтому ваш код неверен (будет вызывать ArrayIndexOutOfBoundsException для определенных входных данных). - person Bolo; 07.07.2010

Предположим, что индексация начинается с 0. Двоичный поиск наименьшего четного i такого, что x[i] != x[i+1]; ваш ответ х[i].

изменить: из-за общественного спроса, вот код

int f(int *x, int min, int max) {
  int size = max;
  min /= 2;
  max /= 2;
  while (min < max) {
    int i = (min + max)/2;
    if (i==0 || x[2*i-1] == x[2*i])
      min = i+1;
    else
      max = i-1;
  }
  if (2*max == size || x[2*max] != x[2*max+1])
    return x[2*max];
  return x[2*min];
}
person David Lehavi    schedule 06.07.2010
comment
Как можно искать наименьшее даже с помощью бинарного поиска? Представьте, что вы выбираете четный зонд в середине массива. Представим далее, что x[i] == x[i+1]. Как вы решаете, двигаться ли слева или справа от i? - person Dave O.; 06.07.2010
comment
@Dave: спасибо, что сняли вопрос, не дожидаясь ответа. Теперь у вас есть i = 2j и x[i] == x[i+1], на следующей итерации возьмем j = j / 2, i = 2j. - person David Lehavi; 06.07.2010
comment
@David Lehavi пусть будет x := {a, a, b, b, c}. Первый пробник: i == 2 =› j == 1. x[i] == x[i+1] == b (первый b). Вторая итерация: new_j == 0, new_i == 0. Вот тут я запутался. Кроме того, в моем примере нет четного i, для которого x[i] != x[i+1] - person Dave O.; 06.07.2010
comment
@ Дэйв, я отредактировал свой ответ, чтобы ответить на твой вопрос. - person David Lehavi; 06.07.2010
comment
Я не понимаю вторую итерацию. первый и последний - это минимальный и максимальный индекс, поэтому 0 и 4 в начале - верно? первый = первый / 2 = 0 / 2 = 0, последний = последний / 2 + 1 = 4 / 2 + 1 = 3 (!= 2 как в Вашем ответе). Далее, обратите внимание, что в a={0,0,1} нет четного i, для которого a[i] != a[i+1] - даже если бы Ваш алгоритм был правильным, Ваша формулировка не была бы правильной. Не могли бы вы написать функцию intoddNumber(int *array, int minIndex, int maxIndex) (индексы включены)? Спасибо заранее. - person Dave O.; 06.07.2010
comment
int nums [] = {1, 1, 1, 2, 2, 2, 2, 3, 3}; cout << f(nums, 0, 8); возвращает 2 вместо 1. Я предположил индексацию на основе 0 и что max является самым высоким допустимым индексом. - person IVlad; 06.07.2010
comment
@Dave, lVlad - ой, перепутал +1, -1 - person David Lehavi; 06.07.2010
comment
@David - все еще не работает в моем примере. Теперь он возвращает 3. - person IVlad; 06.07.2010
comment
@David Lehavi Спасибо за ваши усилия. Поскольку ваше решение является действительным кодом C, вы можете скопировать и вставить его в свою любимую IDE, скомпилировать его и увидеть, как он не работает, как я (a: {1,2,2}). Дорогой Дэвид, я вовсе не хочу тебя оскорблять! Меня очень интересует решение (настолько, что я начну щедрость). И когда я услышал, что кто-то придумал решение, я хотел убедиться, что оно правильное, и поэтому усиленно искал ошибки. Может быть, Вы ошиблись в рассуждениях или, может быть, Вы на правильном пути. Желаю Вам удачи в получении награды. Привет. - person Dave O.; 06.07.2010