ger_kar писал(а):В примере обнаружен маленький глючек, связанный с тем, что прогресс бар не сбрасывается.
Подтверждаю.
ger_kar писал(а):Хотя бы пояснительные наименования процедур/функций, для чего они нужны и общее словесное описание. А то что то в голове набор фрагментов, который не хочет складываться в общую картинку.
А что с функциями не так, они на китайском что-ли названы?
Ход мыслей был простым.
Нам нужно либо найти существующий элемент, либо добавить новый, а затем у этого элемента увеличить счётчик. Проблема была с тем, как топикстартер решал задачу «найти существующий».
У него, если элементов миллион, и нужного элемента нет, приходилось каждый раз обходить массив из миллиона уже существующих. Это никуда не годится. Нам нужно увеличить производительность поиска нужного элемента в тысячи (или сотни тысяч раз). Поиск нужного элемента должен требовать не обхода миллиона элементов массива, а требовать обхода где-то примерно 10 — причём в любом (вернее худшем) случае.
Как этого можно добиться? Добиться этого можно применением древовидной структуры вместо плоского списка уже существующих элементов, как я писал:
Хакер писал(а):А правильный подход — использовать другую структуру данных, где выборка элемента в худшем случае осуществляется как-то на несколько порядков эффективнее, нежели перебор всех значений. Например деревья. Могу сделать примерчик.
В плоском списке нужно идти от начала списка до конца, и если в списке миллион элементов — придётся проверить весь миллион.
В дереве нужно идти от корня до конечного узла, и максимальное (худшее возможное) число шагов определяется уже глубиной вложенности дерева, а не количеством элементов в нём.
Собственно, по этой причине деревья поиска и являются популярным решением для быстрого поиска среди множества пар ключ—значение.
Деревья бывают
двоичными, бывают сбалансированными (
B-дерево,
B+-дерево,
B*-дерево), бывают двоичными и сбалансированными (
АВЛ-дерево,
красно-чёрное дерево).
В моём же случае дерево ни сбалансированное, ни двоичное.
Идея состоит в том, каждая пара {ключ→значение} представлена определённым узлом в дереве. При этом в отношении индекса (будь то число или строка) определяется некое правило конвертирования самого индекса в путь-в-дереве. Способов получения пути из первоначального индекса можно придумать очень много.
Когда нам нужно по ключу получить значение (исходя из предположения, что для этого ключа в дереве уже есть узел), мы конвертируем ключ в путь, а затем используем этот путь для обхода дерева: каждый элемент пути отвечает на вопрос о том, куда перейти на определённом шаге. Первый элемент пути говорит, на какой узел перейти с корневого, второй элемент — куда дальше.
Теперь что касается получения пути или, иными словами, получения маршрута обхода дерева. Можно по разному получать это маршрут.
В моём коде числовой индекс конвертируется в строковое десятичное представление, а дальше каждая цифра начиная с самой левой определяет индекс дочернего узла относительно родительского.
123 — берём корневой узел, переходим к 1-му дочернему, у дочернего берём 2-й подузел, а у него самого — третью дочку, и у этого узла должно храниться значение для ключа «123».
Можно было бы придумать множество других способов, особенно для числовых индексов. Например, можно использовать байты числа, и тогда для 32-битных чисел обход дерева будет в худшем случае осуществляться за 4 шага, но у каждого узла было бы до потенциальных 256 потомков.
Можно было был использовать нибблы — 4-битные кусочки числа, и тогда ветвистость узлов составляла бы 16 ( как 2
4). Решение с нибблами аналогично решению конверировать числовой индекс не в десятичную строчку, а в 16-ричную и использовать вместо десятичных цифр шестнадцатеричные (0—F).
Можно было было бы использовать трёх-битные кусочки числа: сдвигать биты числа влево или вправо на 3 бита, брать крайние 3 бита и использовать их, чтобы определить номер дочернего узла.
Наконец, можно было бы каждый отдельно-взятый бит числа использовать как элемент пути: в таком случае дерево превратилось бы в двоичное (у каждого узла максимум два дочерних), но это было слишком неэффективно с точки зрения расхода памяти на поддержание дерева, да и обход числа сулил бы максимум 32 шага.
Решение в качества маршрута обхода дерева использовать путь, образованный десятичными цифрами десятичного представления — не самое эффективное (использовать нибблы или байты было бы быстрее), но зато оно имеет два плюса:
- Это не так далеко от полностью текстовых ключей/индексов, то есть не придётся сильно менять код при миграции на текстовые ключи.
- Полный обход дерева позволяет получить отсортированный по ключу набор пар ключ→значение (именно в текстовой интерпретации сортировки: 1, 12, 2, 25, 34, 54, 5, 51).
Дальше — конкретика.
Во-первых, у нас возможна ситуация, когда будут два ключа, причём такие, что путь одного явлчется начальной частью пути другого.
Например
12 и
12095. Поэтому каждый узел дерева у нас может одновременно и отождествлять собственно саму пару ключ→значение, и при этом быть просто промежуточным узлом на пути к конечным узлам других пар ключ→значение.
Так что структура узла дерева видится так: это массив указателей на дочершние узлы и указатель на собственно сами данные (если они есть для данного узла). Поскольку в VB у нас нет указателей в чистом виде (и слава богу
), вместо указателей на дочершние узлы (ноды) и указателя на данные я все ноды кладу в большой массив, и все данные кладу в большой массив, а взамен указателей на ноды и данные использую их индексы в этих массивах. При этом массивы я сознательно веду от единицы, чтобы нулевой индекс был аналогом нулевого указателя.
Поэтому вот структура для представления ноды (узла) дерева:
- Код: Выделить всё
Public Type ARCH_TREE_NODE
ChildrenIndecies() As Long
OwnDataEntryIndex As Long
End Type
А вот структура для представления самого дерева (вместе с двумя массивами):
- Код: Выделить всё
Public Type ARCH_TREE_OBJECT
cNodes As Long
cEntries As Long
Nodes() As ARCH_TREE_NODE
Entries() As ARCH_DATA_ENTRY
End Type
Ну и структура, отождествлющая в паре ключ—значение само
значение:
- Код: Выделить всё
Public Type ARCH_DATA_ENTRY
Key As String
Quantity As Long
'
' Можно добавлять другие поля
'
End Type
Экземпляры
ARCH_TREE_NODE и
ARCH_DATA_ENTRY живут в массивах внутри
ARCH_TREE_OBJECT, а чтобы ссылаться друг на друга используют индексы.
Если в дереве есть пара с ключём «123», то в дереве будет ветка \1\2\3, и конечный узел этой ветки будет иметь поле
OwnDataEntryIndex, ссылающуееся на соответствующее значение. Если отдельно ключей «1» и «12» — нет, то узлы \1 и \1\2 будут иметь это поле ссылающееся вникуда (0), а если есть отдельно пары с такими ключами, то у каждой ноды поле OwnDataEntryIndex будет ссылаться на своё значение.
Если в дереве будет храниться 100 000 пар ключ—значение, то в худшем случае как минимум там будет 100 000 конечных узлов, не считая приличное число промежуточных узлов. У конечных узлов никогда не будет дочерних — именно поэтому я сделал массив индексов дочерних узлов динамическим. Пока у узла нет дочершних, массив не создан (SA-дескриптор и блок данных под элементы не размещены), и это даёт экономию памяти. К тому же, Redim массива узлов проходит легче: каждая нота это указатель на SAFEARRAY, а не сам массив, так что копирование увеличиваемого массива узлов в случае, если старый блок памяти уже некуда расширять, проходит с меньшей кровью.
В добавок к этому, изначально пустое дерево я создаю с предварительно созданным корневым узлом. Этот корневой узел такой же, как и другие узлы, — может держать таблицы ссылок на дочерние узлы и ссылку на собственные данные.
В случае с числовым индексами корневой узел никогда не будет иметь собственных данных, но, например, при миграции на строчные индексы/ключи, если мы захотим оставить возможность в качестве ключа использовать пустую строку (""), то именно корневой узел будет своим полей OwnDataEntryIndex ссылаться на данные, ассоциированные с ключом "".
________
Собственно говоря, дальше всё вообще просто.
Я сделал функцию EntryTreeAddOrGet которая берёт дерево и нужный ключ и возвращает
указатель на данные, ассоциированные с этим ключом, ссылку на
ARCH_DATA_ENTRY, ассоциированный с этим ключом.
Для этого она конвертирует ключ в путь (маршрут обхода дерева) и пытается пройтись по дереву в направлении от корня до конечного узла, используя полученный путь. При этом, прямо во время обхода дерева все недостающие узлы достраиваются, если их нет.
Ключ в путь генерируется функцией
InitPathElms. Дальше идёт обход.
Обход начинается с корневого узла:
iCurNode = 1 ' Начинаем с корневого узлаитеративно, до того момента, пока не пройдём через весь путь (не достигнем конца пути):
Loop Until iPathElm = cPathElmsНа каждой итерации мы смотрим в таблицу дочерних элементов:
- Код: Выделить всё
On Error GoTo AllocateChildrenList
iNextNode = .Nodes(iCurNode).ChildrenIndecies(PathElms(iPathElm))
On Error GoTo 0
(которая, если её нет, создаётся при первом обращении)
И если в ней ссылка на соответствующий дочершний узел не установлена (то есть узла нет), то дочершний узел создаётся:
- Код: Выделить всё
If iNextNode = 0 Then
iNextNode = EntryTreeAllocateNode(tree, iCurNode, PathElms(iPathElm))
End If
И этот дочершний узел становится новым текущим:
iCurNode = iNextNodeа мы двигаемся к следующему шагу маршрута (пути):
iPathElm = iPathElm + 1И это всё повторяется до тех пор, пока нам есть куда идти далше, то есть пока остались шаги в «маршрутном листе»:
Loop Until iPathElm = cPathElmsКогда весь путь (от корневого узла до текущего) пройден, текущий узел объявляется конечным.
И если в с конечным узлом уже были ассоциированы какие-то данные, то возвращается их индекс:
- Код: Выделить всё
If .Nodes(iCurNode).OwnDataEntryIndex <> 0 Then
EntryTreeAddOrGet = .Nodes(iCurNode).OwnDataEntryIndex
А если не были (узел был создан только что), то создаётся соответствующая структура
ARCH_DATA_ENTRY и, опять таки, возвращается её индекс:
- Код: Выделить всё
Else
EntryTreeAddOrGet = EntryTreeAllocateEntry(tree, iCurNode, sIndexInternal)
End If
Вот и всё.
Для миграции на текстовые индексы надо переделать
InitPathElms и поменять код создания массива дочершних узлов:
ReDim tree.Nodes(iCurNode).ChildrenIndecies(0 To 9)