Проблема с сохранением каждого интервала в качестве пары (начало, конец) заключается в том, что если вы добавите новый диапазон, который включает в себя N ранее сохраненных интервалов, вам необходимо уничтожить каждый из этих интервалов, что делает N шагов, хранятся в дереве, веревке или связанном списке.
(Вы можете оставить их для автоматической сборки мусора, но это займет время и работает только на некоторых языках.)
Возможным решением для этого может быть сохранение значений (не начальной и конечной точек интервалов) в N-ary tree, где каждый узел представляет диапазон и хранит две N-битовые карты, представляющие N поддиапазонов, диапазоны и все ли присутствующие значения в этих поддиапазонах, все отсутствующие или смешанные. В случае смешанного будет указатель на дочерний узел, который представляет этот диапазон.
Пример: (с использованием дерева с и высотой 2)
full range: 0-511 ; store interval 100-300
0-511:
0- 63 64-127 128-191 192-255 256-319 320-383 384-447 448-511
0 mixed 1 1 mixed 0 0 0
64-127:
64- 71 72- 79 80- 87 88- 95 96-103 104-111 112-119 120-127
0 0 0 0 mixed 1 1 1
96-103:
96 97 98 99 100 101 102 103
0 0 0 0 1 1 1 1
256-319:
256-263 264-271 272-279 280-287 288-295 296-303 304-311 312-319
1 1 1 1 1 mixed 0 0
296-303:
296 297 298 299 300 301 302 303
1 1 1 1 1 0 0 0
Так дерево будет содержать эти пять узлов:
- values: 00110000, mixed: 01001000, 2 pointers to sub-nodes
- values: 00000111, mixed: 00001000, 1 pointer to sub-node
- values: 00001111, mixed: 00000000
- values: 11111000, mixed: 00000100, 1 pointer to sub-node
- values: 11111000, mixed: 00000000
Точка хранения интервал таким образом, является то, что вы можете отменить интервал, не удаляя его. Предположим, вы добавили новый диапазон 200-400; в этом случае вы измените диапазон 256-319 в корневом узле от «смешанного» до «1», не удаляя или не обновляя сами узлы 256-319 и 296-303; эти узлы могут быть сохранены для последующего повторного использования (или отключены и помещены в очередь повторно используемых суб-деревьев или удалены в запрограммированной сборке мусора, когда программа работает на холостом ходу или работает на низкой памяти).
Если вы просматриваете интервал, вам нужно идти только по глубине, по мере необходимости; при поиске, например. 225-275, вы обнаружите, что 192-255 все-присутствует, 256-319 смешанно, 256-263 и 264-271 и 272-279 - все присутствующие, и вы знаете, что ответ верен. Поскольку эти значения будут храниться в виде растровых изображений (один для настоящего/отсутствующего, один для смешанного/сплошного), все значения в узле могут быть проверены только с несколькими побитовыми сравнениями. не
использования Re-узлы:
Если узел имеет дочерний узел, а соответствующее значение позже установлено от смешанного ко всем-отсутствуют или все присутствует, дочерний узел больше не имеет соответствующие значения (но игнорируется). Когда значение будет изменено на смешанное, дочерний узел может быть обновлен, установив все его значения в его значение в родительском узле (до того, как он был изменен на смешанный), а затем внесет необходимые изменения.
В приведенном выше примере, если мы добавим диапазон 0-200, это изменит дерево:
- values: 11110000, mixed: 00001000, 2 pointers to sub-nodes
- (values: 00000111, mixed: 00001000, 1 pointer to sub-node)
- (values: 00001111, mixed: 00000000)
- values: 11111000, mixed: 00000100, 1 pointer to sub-node
- values: 11111000, mixed: 00000000
Второй и третий узел теперь содержат устаревшие значения, и игнорируются. Если мы удалим диапазон 80-95, значение для диапазона 64-127 в корневом узле снова будет изменено на смешанное, а узел для диапазона 64-127 будет повторно использован. Сначала мы устанавливаем все значения в нем на все-присутствующие (поскольку это было предыдущее значение родительского узла), а затем мы установили значения для 80-87 и 88-95 для всех отсутствующих. Третий узел для диапазона 96-103 остается неработоспособным.
- values: 00110000, mixed: 01001000, 2 pointers to sub-nodes
- values: 11001111, mixed: 00000000, 1 pointer to sub-node
- (values: 00001111, mixed: 00000000)
- values: 11111000, mixed: 00000100, 1 pointer to sub-node
- values: 11111000, mixed: 00000000
Если мы затем добавляем значения 100, значение для диапазона 96-103 во втором узле будет изменены, чтобы снова перемешивают, а третий узел будет обновляться на все-отсутствующее (предыдущее значение во втором узел), а затем значение 100 будет установлено в настоящее время:
- values: 00110000, mixed: 01001000, 2 pointers to sub-nodes
- values: 11000111, mixed: 00001000, 1 pointer to sub-node
- values: 00001000, mixed: 00000000
- values: 11111000, mixed: 00000100, 1 pointer to sub-node
- values: 11111000, mixed: 00000000
на первый взгляд может показаться, что эта структура данных использует много места для хранения по сравнению с растворами, которые хранят интервалы как (начало, конец) пар.Однако, давайте посмотрим на (теоретический) наихудший сценарий, где каждый четное число присутствует и каждое нечетное число отсутствует, во всем диапазоне 64-битном:
Total range: 0 ~ 18,446,744,073,709,551,615
Intervals: 9,223,372,036,854,775,808
структура данных, которая хранит их как (начало, конец) пар будет использовать:
Nodes: 9,223,372,036,854,775,808
Size of node: 16 bytes
TOTAL: 147,573,952,589,676,412,928 bytes
Если структура данных использует узлы, которые связаны с помощью (64-битовых) указателей, которые бы добавить:
Data: 147,573,952,589,676,412,928 bytes
Pointers: 73,786,976,294,838,206,456 bytes
TOTAL: 221,360,928,884,514,619,384 bytes
N-кратную дерево с разветвлением коэффициент 64 (и 16 на последнем уровне, чтобы получить полный диапазон 10 × 6 + 1 × 4 = 64 бит) будет использовать:
Nodes (branch): 285,942,833,483,841
Size of branch: 528 bytes
Nodes (leaf): 18,014,398,509,481,984
Size of leaf: 144 bytes
TOTAL: 2,745,051,201,444,873,744 bytes
который является 53,76 раза меньше, чем (начало, конец) парные структуры (или 80,64 раза меньше, включая указатели).
Расчет был сделан со следующим N-арной дерева:
Branch (9 levels):
value: 64-bit map
mixed: 64-bit map
sub-nodes: 64 pointers
TOTAL: 528 bytes
Leaf:
value: 64-bit map
mixed: 64-bit map
sub-nodes: 64 16-bit maps (more efficient than pointing to sub-node)
TOTAL: 144 bytes
Это, конечно, сравнение наихудший; средний случай будет сильно зависеть от конкретного ввода.
Вот первый пример кода, который я написал для проверки идеи. Узлы имеют коэффициент ветвления 16, так что каждый уровень хранит 4 бита целых чисел, а общие битовые глубины могут быть получены без разных листьев и ветвей. В качестве примера создается дерево глубины 3, представляющее диапазон 4 = 16 бит.
function setBits(pattern, value, mask) { // helper function (make inline)
return (pattern & ~mask) | (value ? mask : 0);
}
function Node(value) { // CONSTRUCTOR
this.value = value ? 65535 : 0; // set all values to 1 or 0
this.mixed = 0; // set all to non-mixed
this.sub = null; // no pointer array yet
}
Node.prototype.prepareSub = function(pos, mask, value) {
if ((this.mixed & mask) == 0) { // not mixed, child possibly outdated
var prev = (this.value & mask) >> pos;
if (value == prev) return false; // child doesn't require setting
if (!this.sub) this.sub = []; // create array of pointers
if (this.sub[pos]) {
this.sub[pos].value = prev ? 65535 : 0; // update child node values
this.sub[pos].mixed = 0;
}
else this.sub[pos] = new Node(prev); // create new child node
}
return true; // child requires setting
}
Node.prototype.set = function(value, a, b, step) {
var posA = Math.floor(a/step), posB = Math.floor(b/step);
var maskA = 1 << posA, maskB = 1 << posB;
a %= step; b %= step;
if (step == 1) { // node is leaf
var vMask = (maskB | (maskB - 1))^(maskA - 1); // bits posA to posB inclusive
this.value = setBits(this.value, value, vMask);
}
else if (posA == posB) { // only 1 sub-range to be set
if (a == 0 && b == step - 1) // a-b is full sub-range
{
this.value = setBits(this.value, value, maskA);
this.mixed = setBits(this.mixed, 0, maskA);
}
else if (this.prepareSub(posA, maskA, value)) { // child node requires setting
var solid = this.sub[posA].set(value, a, b, step >> 4); // recurse
this.value = setBits(this.value, solid ? value : 0, maskA); // set value
this.mixed = setBits(this.mixed, solid ? 0 : 1, maskA); // set mixed
}
}
else { // multiple sub-ranges to set
var vMask = (maskB - 1)^(maskA | (maskA - 1)); // bits between posA and posB
this.value = setBits(this.value, value, vMask); // set inbetween values
this.mixed &= ~vMask; // set inbetween to solid
var solidA = true, solidB = true;
if (a != 0 && this.prepareSub(posA, maskA, value)) { // child needs setting
solidA = this.sub[posA].set(value, a, step - 1, step >> 4);
}
if (b != step - 1 && this.prepareSub(posB, maskB, value)) { // child needs setting
solidB = this.sub[posB].set(value, 0, b, step >> 4);
}
this.value = setBits(this.value, solidA ? value : 0, maskA); // set value
this.value = setBits(this.value, solidB ? value : 0, maskB);
if (solidA) this.mixed &= ~maskA; else this.mixed |= maskA; // set mixed
if (solidB) this.mixed &= ~maskB; else this.mixed |= maskB;
}
return this.mixed == 0 && this.value == 0 || this.value == 65535; // solid or mixed
}
Node.prototype.check = function(a, b, step) {
var posA = Math.floor(a/step), posB = Math.floor(b/step);
var maskA = 1 << posA, maskB = 1 << posB;
a %= step; b %= step;
var vMask = (maskB - 1)^(maskA | (maskA - 1)); // bits between posA and posB
if (step == 1) {
vMask = posA == posB ? maskA : vMask | maskA | maskB;
return (this.value & vMask) == vMask;
}
if (posA == posB) {
var present = (this.mixed & maskA) ? this.sub[posA].check(a, b, step >> 4) : this.value & maskA;
return !!present;
}
var present = (this.mixed & maskA) ? this.sub[posA].check(a, step - 1, step >> 4) : this.value & maskA;
if (!present) return false;
present = (this.mixed & maskB) ? this.sub[posB].check(0, b, step >> 4) : this.value & maskB;
if (!present) return false;
return (this.value & vMask) == vMask;
}
function NaryTree(factor, depth) { // CONSTRUCTOR
this.root = new Node();
this.step = Math.pow(factor, depth);
}
NaryTree.prototype.addRange = function(a, b) {
this.root.set(1, a, b, this.step);
}
NaryTree.prototype.delRange = function(a, b) {
this.root.set(0, a, b, this.step);
}
NaryTree.prototype.hasRange = function(a, b) {
return this.root.check(a, b, this.step);
}
var intervals = new NaryTree(16, 3); // create tree for 16-bit range
// CREATE RANDOM DATA AND RUN TEST
document.write("Created N-ary tree for 16-bit range.<br>Randomly adding/deleting 100000 intervals...");
for (var test = 0; test < 100000; test++) {
var a = Math.floor(Math.random() * 61440);
var b = a + Math.floor(Math.random() * 4096);
if (Math.random() > 0.5) intervals.addRange(a,b);
else intervals.delRange(a,b);
}
document.write("<br>Checking a few random intervals:<br>");
for (var test = 0; test < 8; test++) {
var a = Math.floor(Math.random() * 65280);
var b = a + Math.floor(Math.random() * 256);
document.write("Tree has interval " + a + "-" + b + " ? " + intervals.hasRange(a,b),".<br>");
}
Я побежал тест, чтобы проверить, сколько узлов создаются, и сколько из них являются активными или бездействует. Я использовал общий диапазон 24-битных (чтобы я мог протестировать наихудший вариант без исчерпания памяти), разделенный на 6 уровней по 4 бита (так что каждый узел имеет 16 поддиапазонов); количество узлов, которые необходимо проверять или обновлять при добавлении, удалении или проверке интервала, равно 11 или меньше. Максимальное количество узлов в этой схеме составляет 1118481.
На приведенном ниже графике показано количество активных узлов, когда вы продолжаете добавлять/удалять случайные интервалы с диапазоном 1 (одиночные целые числа), 1 ~ 16, 1 ~ 256 ... 1 ~ 16M (полный диапазон).
Добавление и удаление одиночных целых чисел (темно-зеленая линия) создает активные узлы до близкого к максимальным 1,118,481 узлам, при этом почти не узлы делаются в состоянии покоя. Максимум достигается после добавления и удаления около 16M целых чисел (= число целых чисел в диапазоне).
Если вы добавляете и удаляете случайные интервалы в большем диапазоне, количество создаваемых узлов примерно одинаково, но многие из них становятся бездействующими. Если вы добавляете случайные интервалы в полный диапазон 1 ~ 16M (ярко-желтая линия), в любое время активны менее 64 узлов, независимо от того, сколько интервалов вы добавляете или удаляете.
Это уже дает представление о том, где эта структура данных может быть полезна в отличие от других: чем больше узлов становятся бездействующими, тем больше интервалов/узлов необходимо будет удалить в других схемах.
С другой стороны, это показывает, как эта структура данных может быть слишком пространственно-неэффективной для определенных диапазонов, а также типов и объемов ввода. Вы можете ввести систему рециркуляции спящего узла, но это устраняет преимущество неактивных узлов, которые могут быть повторно использованы повторно.
Много места в N-арном дереве занято указателями на дочерние узлы. Если весь диапазон достаточно мал, вы можете сохранить дерево в массиве. Для 32-битного диапазона, который будет занимать 580 МБ (546 МБ для растровых изображений «значение» и 34 МБ для «смешанных» растровых изображений). Это более экономично, потому что вы храните только растровые изображения, и вам не нужны указатели на дочерние узлы, потому что все имеет фиксированное место в массиве. У вас будет преимущество дерева с глубиной 7, поэтому любая операция может быть выполнена путем проверки 15 «узлов» или меньше, и никакие узлы не должны создаваться или удаляться во время операций добавления/удаления/проверки.
Вот пример кода, который я использовал, чтобы опробовать идею N-ary-tree-in-a-array; он использует 580 МБ для хранения N-арного дерева с коэффициентом ветвления 16 и глубиной 7 для 32-разрядного диапазона (к сожалению, диапазон выше 40 бит или около того, вероятно, выходит за рамки возможностей памяти любого обычного компьютера). В дополнение к запрошенным функциям он также может проверить, полностью ли отсутствует интервал, используя notValue() и notRange().
#include <iostream>
inline uint16_t setBits(uint16_t pattern, uint16_t mask, uint16_t value) {
return (pattern & ~mask) | (value & mask);
}
class NaryTree32 {
uint16_t value[0x11111111], mixed[0x01111111];
bool set(uint32_t a, uint32_t b, uint16_t value = 0xFFFF, uint8_t bits = 28, uint32_t offset = 0) {
uint8_t posA = a >> bits, posB = b >> bits;
uint16_t maskA = 1 << posA, maskB = 1 << posB;
uint16_t mask = maskB^(maskA - 1)^(maskB - 1);
// IF NODE IS LEAF: SET VALUE BITS AND RETURN WHETHER VALUES ARE MIXED
if (bits == 0) {
this->value[offset] = setBits(this->value[offset], mask, value);
return this->value[offset] != 0 && this->value[offset] != 0xFFFF;
}
uint32_t offsetA = offset * 16 + posA + 1, offsetB = offset * 16 + posB + 1;
uint32_t subMask = ~(0xFFFFFFFF << bits);
a &= subMask; b &= subMask;
// IF SUB-RANGE A IS MIXED OR HAS WRONG VALUE
if (((this->mixed[offset] & maskA) != 0 || (this->value[offset] & maskA) != (value & maskA))
&& (a != 0 || posA == posB && b != subMask)) {
// IF SUB-RANGE WAS PREVIOUSLY SOLID: UPDATE TO PREVIOUS VALUE
if ((this->mixed[offset] & maskA) == 0) {
this->value[offsetA] = (this->value[offset] & maskA) ? 0xFFFF : 0x0000;
if (bits != 4) this->mixed[offsetA] = 0x0000;
}
// RECURSE AND IF SUB-NODE IS MIXED: SET MIXED BIT AND REMOVE A FROM MASK
if (this->set(a, posA == posB ? b : subMask, value, bits - 4, offsetA)) {
this->mixed[offset] |= maskA;
mask ^= maskA;
}
}
// IF SUB-RANGE B IS MIXED OR HAS WRONG VALUE
if (((this->mixed[offset] & maskB) != 0 || (this->value[offset] & maskB) != (value & maskB))
&& b != subMask && posA != posB) {
// IF SUB-RANGE WAS PREVIOUSLY SOLID: UPDATE SUB-NODE TO PREVIOUS VALUE
if ((this->mixed[offset] & maskB) == 0) {
this->value[offsetB] = (this->value[offset] & maskB) ? 0xFFFF : 0x0000;
if (bits > 4) this->mixed[offsetB] = 0x0000;
}
// RECURSE AND IF SUB-NODE IS MIXED: SET MIXED BIT AND REMOVE A FROM MASK
if (this->set(0, b, value, bits - 4, offsetB)) {
this->mixed[offset] |= maskB;
mask ^= maskB;
}
}
// SET VALUE AND MIXED BITS THAT HAVEN'T BEEN SET YET AND RETURN WHETHER NODE IS MIXED
if (mask) {
this->value[offset] = setBits(this->value[offset], mask, value);
this->mixed[offset] &= ~mask;
}
return this->mixed[offset] != 0 || this->value[offset] != 0 && this->value[offset] != 0xFFFF;
}
bool check(uint32_t a, uint32_t b, uint16_t value = 0xFFFF, uint8_t bits = 28, uint32_t offset = 0) {
uint8_t posA = a >> bits, posB = b >> bits;
uint16_t maskA = 1 << posA, maskB = 1 << posB;
uint16_t mask = maskB^(maskA - 1)^(maskB - 1);
// IF NODE IS LEAF: CHECK BITS A TO B INCLUSIVE AND RETURN
if (bits == 0) {
return (this->value[offset] & mask) == (value & mask);
}
uint32_t subMask = ~(0xFFFFFFFF << bits);
a &= subMask; b &= subMask;
// IF SUB-RANGE A IS MIXED AND PART OF IT NEEDS CHECKING: RECURSE AND RETURN IF FALSE
if ((this->mixed[offset] & maskA) && (a != 0 || posA == posB && b != subMask)) {
if (this->check(a, posA == posB ? b : subMask, value, bits - 4, offset * 16 + posA + 1)) {
mask ^= maskA;
}
else return false;
}
// IF SUB-RANGE B IS MIXED AND PART OF IT NEEDS CHECKING: RECURSE AND RETURN IF FALSE
if (posA != posB && (this->mixed[offset] & maskB) && b != subMask) {
if (this->check(0, b, value, bits - 4, offset * 16 + posB + 1)) {
mask ^= maskB;
}
else return false;
}
// CHECK INBETWEEN BITS (AND A AND/OR B IF NOT YET CHECKED) WHETHER SOLID AND CORRECT
return (this->mixed[offset] & mask) == 0 && (this->value[offset] & mask) == (value & mask);
}
public:
NaryTree32() { // CONSTRUCTOR: INITIALISES ROOT NODE
this->value[0] = 0x0000;
this->mixed[0] = 0x0000;
}
void addValue(uint32_t a) {this->set(a, a);}
void addRange(uint32_t a, uint32_t b) {this->set(a, b);}
void delValue(uint32_t a) {this->set(a, a, 0);}
void delRange(uint32_t a, uint32_t b) {this->set(a, b, 0);}
bool hasValue(uint32_t a) {return this->check(a, a);}
bool hasRange(uint32_t a, uint32_t b) {return this->check(a, b);}
bool notValue(uint32_t a) {return this->check(a, a, 0);}
bool notRange(uint32_t a, uint32_t b) {return this->check(a, b, 0);}
};
int main() {
NaryTree32 *tree = new NaryTree32();
tree->addRange(4294967280, 4294967295);
std::cout << tree->hasRange(4294967280, 4294967295) << "\n";
tree->delValue(4294967290);
std::cout << tree->hasRange(4294967280, 4294967295) << "\n";
tree->addRange(1000000000, 4294967295);
std::cout << tree->hasRange(4294967280, 4294967295) << "\n";
tree->delRange(2000000000, 4294967280);
std::cout << tree->hasRange(4294967280, 4294967295) << "\n";
return 0;
}
@Terminus huh, я не знал об этом обмене. Я не знаком с этим различием. Есть ли причина, по которой вы считаете, что этот вопрос более уместен? – Moodragonx
Что делать, если вы храните пары (левая конечная точка, правая конечная точка) в BST, заказывая только левую конечную точку. Рассматривайте одиночные целые числа 'n' как' (n, n) '.Вставка, которая создает интервал длины> = 1 по соглашению, регулирует правую конечную точку одного из узлов. Затем, похоже, что все ваши операции - «O (log n)», с пространственной сложностью «O (n)» наихудший случай. –
Я думаю, что ваш вопрос достаточно практичен для SO; алгоритм вопросов здесь, вот для чего используется тег [algorithm]. (Некоторые пользователи не согласны.) – m69