Структура данных для быстрого поиска в C++

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

id   x     y     z
0    0.1   0.1   0.1
1    0.2   0.1   0.6
2    0.01  0.3   0.1
.....

Теперь мне нужно сопоставить двойные значения x, y, z и получить соответствующее значение id(int). Мне может понадобиться хранить около 400000 значений. Какую структуру данных следует использовать для эффективного поиска? Поставляется ли C++ с какими-либо встроенными структурами, которые будут поддерживать мое требование.


person Madz    schedule 16.03.2015    source источник
comment
Создайте struct (скажем, foo), содержащий значения x, y и z, а также необходимые предикаты. Затем используйте std::map<foo, int>; предполагая, что ваш id может быть представлен как int. Поиск будет O(log N). Единственное, с чем вам придется столкнуться, — это сравнение с плавающей запятой, но это зависит от того, как возникают значения-кандидаты для x, y и z.   -  person Bathsheba    schedule 16.03.2015
comment
Как вы проверяете равенство doubles? Возможные ответы будут сильно отличаться, если вы используете operator== вместо чего-то вроде abs(x-y) < threshold или чего-то еще.   -  person    schedule 16.03.2015
comment
Это может зависеть от производительности, которую вы хотите. Вас волнует время вставки или только поиск?   -  person rockeye    schedule 16.03.2015
comment
Возможно, вы ищете дерево kd, если вам нужна ближайшая точка к запросу ( и не только сам запрос, на случай, если вы точно не знаете, что ищете, а нужен ближайший сосед)   -  person amit    schedule 16.03.2015
comment
Поиск был бы более важным, так как я бы вставлял только один раз, но часто искал. Возможно, мне придется использовать abs из-за двойных значений.   -  person Madz    schedule 16.03.2015
comment
Я считаю, что лучший вариант - это хэш-таблица. Тем не менее, вы должны разработать хеш-функцию, чтобы иметь хорошее распределение.   -  person Matt    schedule 16.03.2015


Ответы (4)


Если вас не интересует поиск по NN, вы можете использовать std::unordered_set. Однако вам нужно будет определить свою собственную хеш-функцию.

Вот (вероятно, ужасный) пример:

struct entry
{
    int id;
    double x, y, z;

    // constructor if needed, etc...
};

struct entry_hasher
{
    size_t operator()(const entry &e) const
    {
        std::hash<double> h;
        return h(e.x) ^ (h(e.y) << 1) ^ (h(e.z) << 2);
    }
};

std::unordered_set<entry, entry_hasher> entries;

В противном случае стандарт не предоставляет контейнеры, способные выполнять геометрические запросы (например, NN).

person defube    schedule 16.03.2015
comment
Какие? Собираетесь ли вы основывать свой поиск на хеше значения с плавающей запятой и точном равенстве двух чисел с плавающей запятой? Это ужасная идея - person amit; 16.03.2015
comment
OP не упомянул о требовании к допуску поиска. - person defube; 16.03.2015
comment
С числами с плавающей запятой это всегда требуется. (и он сделал, в комментариях). - person amit; 16.03.2015
comment
Для точного сравнения числа с плавающей запятой изоморфны целым числам (они были разработаны для работы в старых базах данных, не поддерживающих FP). Нет, это не всегда является обязательным требованием: создание сетки по доменам FP требует точной арифметики, которая достигается путем обработки чисел с плавающей запятой как разреженных чисел с фиксированной точкой (расширений). - person defube; 16.03.2015
comment
Нет такой вещи, как точность при работе с числами с плавающей запятой, вот в чем дело. - person amit; 16.03.2015

Возможно, это плохие новости для вас, но я думаю, что для ваших целей лучше всего подходит дерево kd и не реализовано в стандартной библиотеке.

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

Тем не менее, эта DS очень популярна, и я уверен, что вы сможете найти ее онлайн-реализацию.

person amit    schedule 16.03.2015

Если вам нужно только выполнить точный поиск, вам подойдет хэш-таблица (unordered_map). хороший выбор. Сделайте ключ tuple или структурой, а значение — идентификатором int.

Если вам нужно выполнить интервальный поиск (например, найти элемент, ближайший к x), и вы всегда выполняете поиск по x, y, z по порядку, вам понадобится упорядоченная структура данных. Упорядоченное дерево (карта) должно работать. Используйте три уровня вложенности map, чтобы вы могли выполнять поиск, выполняя по существу mymap[x][y][z] с любыми правилами интервалов, которые вы хотите применить.

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

person Lie Ryan    schedule 16.03.2015
comment
Какие? Собираетесь ли вы основывать свой поиск на хеше значения с плавающей запятой и точном равенстве двух чисел с плавающей запятой? Это ужасная идея. И нет, упорядоченное дерево не будет просто работать для интервального поиска более чем по одному признаку. - person amit; 16.03.2015
comment
@amit хороший хэш (float, float, float) -> int с цепным хешем для разрешения коллизий должен помочь. В любом случае, нам нужна только целочисленная подсказка, чтобы быстро найти совпадение. - person Matt; 16.03.2015
comment
@ user4419802 Как вы обнаружите, что два значения (x1,y1,z1) и (x2,y2,z2) эквивалентны, если (например) z1 != z2, но abs(z1-z2) ‹ дельта, где дельта — это минимальный коэффициент допуска для двойников? - person amit; 16.03.2015
comment
@amit Давайте возьмем (плохой) пример: хэш = округлый (x + y + z) (я предполагаю, что 3 * fabs (дельта) ‹ 0,5). Эта функция (вероятно) плоха только из-за плохого дистрибутива. Но, в принципе, это работает: оба значения попадут в одну и ту же цепочку, где мы их и найдем линейным поиском. - person Matt; 16.03.2015
comment
@ user4419802 Нет, предположим, что x1=x2=y1=y2=0, z1 = 0,5+дельта/2, z2=0,5-дельта/2. Две точки будут разделены на два хеш-значения. Вероятность того, что это произойдет, увеличивается по мере того, как вы пытаетесь улучшить распределение своего хэша. - person amit; 16.03.2015
comment
@amit: иногда бывают допустимые варианты использования хэш-таблицы для чисел с плавающей запятой. Например, если число с плавающей запятой получается в результате синтаксического анализа, а не в результате вычисления, приличный синтаксический анализатор всегда будет анализировать одну и ту же строку в одно и то же число с плавающей запятой, поэтому ошибки округления будут исключены. Я не знаю, откуда ОП получает свои данные, поэтому об этом нужно упомянуть, потому что часто бывает достаточно точного поиска. - person Lie Ryan; 16.03.2015
comment
@amit Хорошо, тогда добавим любое значение дважды: к цепочкам h(x-d,y-d,z-d) и к цепочкам h(x+d,y+d,z+d) (если хэши действительно разные). - person Matt; 16.03.2015
comment
@ user4419802 Когда вы начинаете исправлять решения, это обычно плохой признак. Я понятия не имею, будет ли текущий патч работать или нет, но, как правило, не используйте хэш с плавающей запятой. Некоторые исключения могут быть терпимы, когда исходят из синтаксических анализов (как предлагает LieRyan), но если задействованы какие-либо вычисления - просто не делайте этого, вся литература предупреждает об этом снова и снова, вероятно, по уважительной причине. - person amit; 16.03.2015
comment
@amit Разработать мгновенное решение обычно означает создать ошибку. Нам всем нужно время, чтобы сделать все хорошо. И «заплатка», ну, вы также можете называть «заплаткой», например, сбалансированные деревья, потому что они просто «заплатка» нормальных деревьев поиска. Хэш — это больше, чем просто один алгоритм. Нам нужно 1) хорошее распределение, 2) возможность найти все бакеты-кандидаты (при этом их будет не слишком много). И это, очевидно, выполнимая задача. - person Matt; 16.03.2015
comment
@user4419802 user4419802 При всем уважении, если вы не можете предоставить какое-либо решение для хеширования, подкрепленное литературой, для хеширования триплетов двойников, я не потерплю его в своем коде. Я не говорю, что их нет, но это должно быть неотъемлемой частью любого ответа, который предлагает хеширование. - person amit; 16.03.2015
comment
@amit Извините, но я не понимаю вашей ссылки на «литературу». Всю «литературу» пишут люди. И мы оба тоже люди. Единственным разумным требованием является предоставление доказательств. А доказывать самому (без литературы) совершенно законно. - person Matt; 16.03.2015
comment
@user4419802 'литература' как - что-то, что было опубликовано в газете, было рецензировано экспертами и благодаря этому приобрело популярность. Если вы считаете, что у вас есть что-то новое, опубликуйте это в одной из статей и дайте пройти рецензирование. - person amit; 16.03.2015
comment
@amit Тема (специального) хэширования чисел с плавающей запятой не настолько велика, чтобы делать из нее статью. - person Matt; 16.03.2015
comment
@user: чтобы хэш-таблица имела разумную производительность, не должно быть никакой корреляции между хеш-значениями аналогичных входных данных. Это требование диаметрально противоположно нашему требованию, согласно которому аналогичные выходные данные сопоставляются с небольшим набором хеш-значений. То, что вы пытаетесь сделать, для меня больше похоже на дискретизацию. - person ; 16.03.2015
comment
@Hurkyl Здесь мы принимаем за определение, что все входные данные с разницей меньше, чем дельта, действительно одинаковы (а не просто похожи). Да, это дискретизация, но компьютеры могут справиться только с дискретизацией, так что это неизбежное зло. - person Matt; 16.03.2015
comment
@user: Отсюда следует, что все почти все входные данные с разницей менее чем в два раза дельта одинаковы (потому что, если x и y одинаковы, а y и z одинаковы, то x и z одинаковы). И почти все входы с разницей менее трехкратной дельты одинаковы... и так далее. - person ; 16.03.2015
comment
@Hurkyl Это обычная проблема с операциями с плавающей запятой на ПК. Изначально мы храним их с небольшой ошибкой округления. Далее, по мере выполнения вычислений, этих ошибок может стать больше и т. д. Дело в том, что мы должны обеспечить как разное начальное хранилище, так и «стабильность» вычислений, то есть ошибки округления не должны становиться слишком большими слишком быстро. Итак, в конце концов, наши результаты все еще имеют небольшие ошибки, которыми мы можем пренебречь. Это область его собственного интереса, но я не могу преподавать курс колледжа в комментариях. - person Matt; 16.03.2015
comment
@user: ... и важным свойством хеш-функции является то, что значения с небольшими различиями не имеют коррелированных хеш-значений, что противоречит нашему желанию пренебрегать небольшими ошибками. Я не говорю, что нельзя построить стратегию, включающую преобразование double в небольшой набор вещей, с которыми вы можете выполнить точное сопоставление, — я говорю, что это вводит в заблуждение и сбивает с толку (если не совершенно неправильно), чтобы описать это как хеширование значений с плавающей запятой. - person ; 16.03.2015
comment
@Hurkyl Опять же, для наших целей мы рассматриваем эти значения не как «имеющие небольшие различия», а как одинаковые. И одинаковые значения должны давать одинаковые хэши. Я не вижу здесь запутанности. - person Matt; 16.03.2015
comment
@user: тогда все числа сопоставляются с одним и тем же значением хеш-функции в соответствии с аргументом, который я привел выше. (Я не точен в крайних случаях; например, если мы исправим delta, тогда числам, достаточно большим, чтобы x + delta == x, вероятно, было разрешено собственное хеш-значение) - person ; 16.03.2015
comment
@Hurkyl Если вы не можете различать числа, вы не можете с ними работать. Если в начале у вас есть тройка (х-дельта), х, (х+дельта), то вы не знаете, чему равен х - либо (х-дельта), либо (х+дельта). Здесь плохие стартовые условия, а не (какой-либо) алгоритм. - person Matt; 16.03.2015
comment
@user: Существует гораздо больше возможных ключей, чем те два, которым я бы хотел, чтобы x соответствовал при поиске, например x или x + delta/2 или x - 17*delta/256 или .... Кроме того, начальные условия неплохие - они такие, какие есть. Что плохо, так это, например, пытаться применить алгоритм поиска точных совпадений, когда вашей целью является поиск приблизительных совпадений. - person ; 16.03.2015
comment
@Hurkyl Просто небезопасно работать с поплавками, когда реальная разница близка к точности. По мере распространения ошибок всего пара невинно выглядящих операций может все сломать. Это не мое желание, а стандартная мера безопасности, предполагающая, что для всех различных входных данных x, y выполняется, скажем, fabs(x-y) > 10*delta. Если нет, то начальные условия действительно неправильные. - person Matt; 16.03.2015
comment
@user: Я не могу понять твой комментарий. В случае, если мы говорим друг о друге, я начну сначала, с точки самого первого комментария: при поиске в таблице значений с плавающей запятой вам обычно не нужно точное совпадение: вместо этого вы хотите найти те y, что abs(x-y) < threshold. Следовательно, выполнение поиска на основе применения хеш-функции к float данным — плохая идея, поскольку они основаны на поиске точных совпадений. - person ; 16.03.2015
comment
@ user4419802: То, что вы (неправильно) назвали дельтой, на самом деле правильно называется ошибка эпсилон/округления. Проблема с дискретным округленным хешированием заключается в том, что оно помещает числа с плавающей запятой в дискретные сегменты, а это не так, как дельта/маржа. ошибок обычно обрабатываются при выполнении физических измерений в физических науках. Дельта/предел погрешности будет поддерживать свойство, состоящее в том, что числа с плавающей запятой довольно непрерывны вокруг величины дельты, пока дельта намного выше, чем эпсилон. - person Lie Ryan; 16.03.2015
comment
@ user4419802: (продолжение) Дискретное хэширование с округлением не удовлетворяет этому требованию непрерывности. Однако главное преимущество дискретного хеширования заключается в том, что оно не страдает от парадокса Сорита, который подразумевает некоторые хорошие свойства, такие как транзитивность отношений равенства, но его ошибки округления будут только расти. Дискретное хеширование с округлением может подойти для целей, где физическая точность не очень важна (например, игровая графика). Но если вам нужны точные физические расчеты, вы должны использовать вычисления дельты/маржи погрешности, чтобы ошибки округления не росли бесконтрольно. - person Lie Ryan; 16.03.2015
comment
@Hurkyl Давайте создадим пример. Для простоты я беру одномерный массив чисел с плавающей запятой, точность поиска 0,01 и H(n) - какой-нибудь "хороший" целочисленный хэш. Пусть h1(x) = H(ceil(100*x)) и h2(x) = H(floor(100*x)). Затем для любого заданного x я добавляю x к ведрам h1(x) и h2(x). Чтобы проверить некоторый y по хеш-таблице, я выполняю линейный поиск в корзинах h1(y) и h2(y). Так что же с этим не так? - person Matt; 16.03.2015

Вы можете использовать OcTree (http://en.wikipedia.org/wiki/Octree ). Это было бы быстрее и удобнее любого контейнера в std на основе бинарного дерева. Также вы не будете беспокоиться об индексных или хеш-функциях. Однако это займет больше памяти. Его можно использовать даже для поиска NN (ближайшие соседи). Другим вариантом является дерево Kd (http://en.wikipedia.org/wiki/K-d_tree). Оба не являются частью ЗППП. KdTree требует меньше памяти, чем OcTree, а иногда даже быстрее. Вы должны быть в состоянии найти хорошие реализации C++ OcTree или KdTree с помощью Google.

person Baj Mile    schedule 16.03.2015