();// Виділяємо сторінку 1 рівня p>
_pageCount = 1; p>
) p>
Дворівневий масив дозволяє здійснювати навігацію
по знаходилися в ньому елементів. При цьому виникає поняття поточного елемента, у
якого можна зчитувати або встановлювати значення, і читати значення ключа.
Для позиціонування на запис з деяким ключем використовується функція
NavigateKey. P>
Алгоритм роботи цієї функції такий. Оскільки
інформація в масиві завжди упорядкована, то пошук можна здійснювати за допомогою
алгоритму бінарного пошуку (тобто половинного поділу). Єдина
проблема, що не дозволяє використовувати класичний алгоритм прямо - це те,
що, що масив складається з двох рівнів. Тому алгоритм пошуку розділяється на
два етапи. На першому етапі перевіряється, чи є масив верхнього рівня. Якщо
є, то в ньому шукається сторінка, на якій може знаходитися шуканий елемент.
Якщо масиву верхнього рівня немає, як сторінки, на якій буде
проводитися подальший пошук, використовується єдина існуюча
сторінка. p>
На другому етапі проводиться класичний бінарний
пошук по ключу в сортованого масиві. p>
public bool NavigateKey (K key) p>
( p>
// Встановлюємо індекс елемента в 0. p>
_currentElementIndex = 0; p>
// Якщо є другий рівень ... p>
if (_pageCount> 1) p>
( p>
// перебираємо межі p>
int hi = _pageCount - 1; p>
int lo = 0; p>
while (lo <= hi) p>
( p>
int i = (lo + hi)>> 1; p>
int result =
_comparer.Compare (NodeArray [i]. Key, key); p>
if (result <0) p>
lo = i + 1; p>
else p>
( p>
hi = i - 1; p>
if (result == 0) p>
( p>
// Ключ знайдений на 2 рівні.
Встановлюємо поточну p>
// сторінку CurrentLeafPage. p>
_currentPageIndex = i; p>
CurrentLeafPage =
NodeArray [_currentPageIndex]. ChildPage; p>
_selected = true; p>
return true; p>
) p>
) p>
) p>
// Ключ не знайдено на 2 рівні. p>
// Перевіряємо на можливість того, що
шуканий ключ - p>
// найменший з наявних в об'єкті. p>
if (hi <0) p>
( p>
// Даний ключ менше найменшого
зберігається ключа. p>
// Встаємо на самий перший елемент
дворівневого масиву p>
_currentPageIndex = 0; p>
CurrentLeafPage =
NodeArray [_currentPageIndex]. ChildPage; p>
_selected = false; p>
// Повертаємо інформацію про те, що
ключ не знайдено. p>
return false; p>
) p>
else p>
( p>
// Даний ключ потрапляє в діапазон
ключів нижележащий сторінки. p>
// Змінюємо поточну сторінку CurrentLeafPage
на знайдену дочірню p>
// сторінку p>
CurrentLeafPage =
NodeArray [_currentPageIndex]. ChildPage; p>
// Встановлюємо поточний індекс ключа на листовий
сторінці в 1, p>
// тому що 0 вже перевіряли. P>
_currentElementIndex = 1; p>
) p>
) p>
// Намагаємося знайти індекс шуканого ключа або
індекс, в якому він повинен p>
// був знаходитися. p>
hi = CurrentLeafPage.Count - 1; p>
lo = _currentElementIndex; p>
while (lo <= hi) p>
( p>
int i = (lo + hi)>>
1; p>
int result =
_comparer.Compare (CurrentLeafPage.PageItems [i]. Key, key); p>
if (result <0) p>
lo = i + 1; p>
else p>
( p>
hi = i - 1; p>
if (result == 0) p>
( p>
// Знайшли! p>
_currentElementIndex =
i; p>
_selected = true; p>
return true; p>
) p>
) p>
) p>
// Не знайшли ... p>
_selected = false; p>
// Поміщаємо в
_currentElementIndex позицію в яку p>
// можна додати елемент з потрібним ключем. p>
_currentElementIndex = lo; p>
return false; p>
) p>
// Процедура вставки в поточну
позицію p>
private void Insert (K Key) p>
( p>
// Вставляємо ключ в поточну позицію,
розширюючи тим самим масив на 1 елемент. p>
// Зрушуємо елементи, щоб звільнити
місце для вставляється. p>
Array.Copy (CurrentLeafPage.PageItems,
_currentElementIndex, p>
CurrentLeafPage.PageItems,
_currentElementIndex + 1, p>
CurrentLeafPage.Count --
_currentElementIndex); p>
// Збільшуємо кількість збережених елементів на сторінці та вставляємо
ключ. p>
CurrentLeafPage.Count ++; p>
CurrentLeafPage.PageItems [_currentElementIndex]. Key = Key; p>
if (_currentElementIndex == 0) p>
NodeArray [_currentPageIndex]. Key = key; p>
version + +;// Відбулися зміни, збільшуємо поточну версію. p>
// p>
// Якщо поточна сторінка листова повністю
заповнена, p>
// то є 2 варіанти. Можна або
перенести елемент з поточною p>
// сторінки на сусідню, або розбити
сторінку на 2. p>
// p>
if (CurrentLeafPage.Count ==
BTConst.MaxCount) p>
( p>
// Сторінку повністю заповнена. p>
// Якщо другого рівня немає ... p>
if (_pageCount == 1) p>
( p>
// ... то створюємо другий рівень. p>
// p>
// Для цього ділимо поточну сторінку
навпіл ... p>
LeafPage NewPage
= New LeafPage (); p>
// ... виправляємо посилання в полях NextPage і PriorPage p>
// щоб можна було здійснювати наскрізну
навігацію p>
// по листовим сторінок. p>
CurrentLeafPage.NextPage = NewPage; p>
NewPage.PriorPage = CurrentLeafPage; p>
// переміщаємо половину елементів
вихідного масиву в новий. p>
Array.Copy (CurrentLeafPage.PageItems,
BTConst.MidlCount, p>
NewPage.PageItems, 0,
BTConst.MidlCount); p>
Array.Clear (CurrentLeafPage.PageItems, BTConst.MidlCount,
BTConst.MidlCount); p>
// Створюємо масив другого рівня і поміщаємо в нього
посилання p>
// на листові сторінки. p>
NodeArray = new
NodeItem [BTConst.MaxCount]; p>
_pageCount = 2;// Тепер сторінок два. p>
NodeArray [0]. Key =
CurrentLeafPage.PageItems [0]. Key; p>
NodeArray [0]. ChildPage =
CurrentLeafPage; p>
NodeArray [1]. Key =
NewPage.PageItems [0]. Key; p>
NodeArray [1]. ChildPage = NewPage; p>
// Задаємо кількість елементів на
сторінках. p>
CurrentLeafPage.Count =
BTConst.MidlCount; p>
NewPage.Count =
BTConst.MidlCount; p>
// Якщо поточний елемент перемістився на нову
сторінку ... p>
if (_currentElementIndex> =
BTConst.MidlCount) p>
( p>
// Змінюємо значення поточної сторінки
на нове ... p>
CurrentLeafPage = NewPage; p>
// ... та поточного індексу на ній. p>
_currentElementIndex -=
BTConst.MidlCount; p>
) p>
) p>
else p>
( p>
// Якщо другий рівень вже існує. p>
// Якщо є сторінка ліворуч від
поточної ... p>
LeafPage LeftPage
= CurrentLeafPage.PriorPage; p>
if (LeftPage! = null) p>
( p>
// ... і вона заповнена менш, ніж на
MaxFill (3/4 )... p>
if (LeftPage.Count <= BTConst.MaxFill) p>
( p>
// можна перекинути значення на
ліву сторінку. p>
// Знаходимо потрібне колічесво
елементів для перекидання. p>
int MoveCount =
(BTConst.MaxCount - LeftPage.Count)/2; p>
// переміщаємо початкові елементи з поточної сторінки
p>
// в кінець лівої сторінки ... p>
Array.Copy (CurrentLeafPage.PageItems, 0, p>
LeftPage.PageItems,
LeftPage.Count, MoveCount); p>
// І Зрушуємо залишилися елементи сторінки в початок. p>
Array.Copy (CurrentLeafPage.PageItems,
MoveCount, p>
CurrentLeafPage.PageItems, 0, CurrentLeafPage.Count - MoveCount); p>
// затираємо переміщені елементи. p>
Array.Clear (CurrentLeafPage.PageItems, p>
CurrentLeafPage.Count - MoveCount, MoveCount); p>
// Так як нульовий елемент на
сторінці змінився, необхідно p>
// відкоригувати значення ключа,
що посилається на цю сторінку p>
// в масиві верхнього рівня. p>
// Виправляємо значення ключа у верхньому
рівні так, щоб його p>
// значення було рівним значенню
ключа нульового елементу p>
// відповідної листовий
сторінки. p>
NodeArray [_currentPageIndex]. Key =
CurrentLeafPage.PageItems [0]. Key; p>
// Поточний ключ був переміщений. p>
// Якщо він перемістився на ліву
сторінку, змінюємо значення p>
// поточної сторінки і поточного
індексу на неї так, щоб вони p>
// вказували на вставлений ключ. p>
if
(_currentElementIndex
( p>
_currentElementIndex
+ = LeftPage.Count; p>
CurrentLeafPage.Count -= MoveCount; p>
LeftPage.Count + =
MoveCount; p>
CurrentLeafPage =
LeftPage; p>
) p>
else p>
( p>
_currentElementIndex
-= MoveCount; p>
CurrentLeafPage.Count -= MoveCount; p>
LeftPage.Count + =
MoveCount; p>
) p>
return; p>
) p>
) p>
// Якщо з лівого сторінкою не
вийшло, спробуємо з правого. p>
// Код цього кроку аналогічний
вищенаведеного. p>
// Його можна знайти у файлі,
супроводжує статтю. p>
... p>
// Не вийшло перекинути елементи
на сусідні сторінки, p>
// так як вони заповнені. p>
// Виділяємо нову сторінку аналогічно
тому, як це робилося вище, p>
// При виділенні верхнього рівня, за
винятком того, що потрібно p>
// скорегувати посилання на
прилеглі листові сторінки. p>
// Цей код пропущений для стислості. p>
... p>
// Вставляємо посилання на нову сторінку в
масив верхнього рівня p>
Array.Copy (NodeArray,
_currentPageIndex + 1, NodeArray, p>
_currentPageIndex + 2,
_pageCount - _currentPageIndex - 1); p>
NodeArray [_currentPageIndex + 1]. Key = NewPage.PageItems [0]. Key; p>
NodeArray [_currentPageIndex + 1]. ChildPage = NewPage; p>
CurrentLeafPage.Count =
BTConst.MidlCount; p>
NewPage.Count = BTConst.MidlCount; p>
// Перевіряємо поточну позицію
вставляється елементу (код пропущено) p>
... p>
// Якщо масив верхнього рівня
повністю заповнений просто збільшуємо p>
// його ємність у 2 рази (в Б + деревах у
цьому випадку вибудовується p>
// новий рівень). p>
if (_pageCount ==
NodeArray.Length) p>
( p>
NodeItem [] NewNodeArray
= New NodeItem [2 * _pageCount]; p>
Array.Copy (NodeArray, 0,
NewNodeArray, 0, _pageCount); p>
NodeArray = NewNodeArray; p>
) p>
) p>
) p>
) p>
Процедуру видалення елемента з дворівневого масиву
я тут наводити не буду. Її код можна знайти в файлі, доданому до статті.
Більший інтерес представляє функція пошуку індексу в дворівневому масиві. P>
Існують ситуації, коли потрібно знайти значення,
асоційоване з ключем, і якщо його немає, то вставити деяке значення,
асоційоване з цим ключем. Якщо для вирішення цього завдання скористатися
окремими процедурами пошуку і вставки, то загальний час подвоїться, так як
процедура вставки (перед безпосереднім вставкою значення) здійснює пошук
місця вставки, тобто має місце повторне (непродуктивний) виконання по суті
однієї і тієї ж операції (пошуку). У принципі, якщо при пошуку запам'ятовувати
знайдену позицію, то процедура вставки могла б скористатися результатами
функції пошуку (за умови, що між пошуком і вставкою не було ніяких
дій). Однак це призвело б до того, що користувача класу довелося
допустити в нетрі реалізації даної колекції, і надійність роботи алгоритмів,
побудованих за таким принципом, була б украй низка. З метою оптимізації я
вирішив створити метод, який намагався б знайти ключ і, якщо не знайшов, вставляв
б деяке значення «за замовчуванням» і позиціонувався на нього (а якщо знайшов,
то просто позиціонувався). Це дозволяє позбутися від зайвої операції пошуку
в описаних вище випадках, тому що після цієї операції користувач отримує
можливість працювати з поточним значенням (значенням, на яке відбулося
позиціонування). При цьому для читання чи модифікації значення не потрібно
проводити повторний пошук. Функцію, що виробляє позиціонування (або
вставку і позиціонування), я вирішив назвати NavigateOrInsertDefault. Ось її
код: p>
public bool NavigateOrInsertDefault (K Key) p>
( p>
bool result =
this.NavigateKey (Key); p>
if (! result) p>
( p>
// Немає такого елемента. Вставляємо в
поточну позицію. p>
Insert (Key); p>
// Поміщаємо в поточну позицію значення по
замовчуванням. p>
CurrentLeafPage.PageItems [_currentElementIndex]. Value
= V.default; p>
_count ++; p>
) p>
p>
// Стоїмо на потрібній позиції. p>
_selected = true; p>
return result; p>
) p>
Крім точного позиціонування, можна виробляти
позиціювання на елемент, ключ якого більше, менше, більше або дорівнює і
менше або дорівнює деякому значенню. Цим займається функція Navigate. У
Як параметри вона отримує значення ключа і варіант пошуку. Тип пошуку
задається наступним перерахуванням: p>
public
enum NavigateFlag: byte p>
( p>
Eqality,// == p>
LessThan,// < p>
GreaterThan,//> p>
LessThanOrEqval,// <= p>
GreaterThanOrEqval//> = p>
) p>
А от реалізація цієї функції: p>
public bool Navigate (K Key, NavigateFlag flag) p>
( p>
bool result =
this.NavigateKey (Key); p>
switch (flag) ( p>
case NavigateFlag.Eqality: p>
return result; p>
case NavigateFlag.GreaterThanOrEqval: p>
if (result) p>
return true; p>
goto case
NavigateFlag.GreaterThan; p>
case NavigateFlag.GreaterThan: p>
if (result) p>
_currentElementIndex ++; p>
if (CurrentLeafPage.Count
== _currentElementIndex) P>
( p>
if
(CurrentLeafPage.NextPage == null) p>
( p>
_selected = false; p>
return false; p>
) p>
else p>
( p>
CurrentLeafPage =
CurrentLeafPage.NextPage; p>
_currentElementIndex =
0; p>
) p>
) p>
p>
_selected = true; p>
return true; p>
case
NavigateFlag.LessThanOrEqval: p>
if (result) p>
return true; p>
goto case
NavigateFlag.LessThan; p>
case NavigateFlag.LessThan: p>
return
this.GetPriorRecord (); p>
) p>
return result; p>
) p>
Ось, загалом-то, і все. За швидкістю пошуку
дворівневий масив перевершує свого більш могутнього побратима (Б +-дерево), але за
вставці починає програвати десь в районі мільйона елементів (а може, і
раніше, якщо зберігаються значення або ключі мають великий розмір). Якщо порівнювати
за тестами пошуку кількості однакових слів у тексті, то виходить така
картина: p>
Кількість слів у тексті = 528124 p>
Кількість слів 20359 p>
Заповнення
SortedDictionary
1.53828656994115 p>
Заповнення QuickDictionary
(через індексатор) 0.189289700227264 p>
Заповнення Dictionary 0.309536826607851 p>
Заповнення TLSD (через індексатор) 0.860960541074354 p>
Заповнення QuickDictionary
(прямий доступ) 0.08363381379477 p>
Заповнення TLSD (прямий
доступ)
0.368364694395517 p>
Заповнення Б +-дерева
(прямий доступ)
0.461643030049909 p>
Заповнення MySortedDictionary 0.984015566224199 p>
«SortedDictionary» - це generic-реалізація абстракції
Dictionary на базі масиву. Входить в стандартну бібліотеку. NET Framework.
Більш докладно див статтю «Колекції ст. NET Framework
Class Library ». p>
«MySortedDictionary»
- Аналог SortedDictionary, написаний мною. Відрізняється від оригіналу тим, що доступ до масиву
здійснюється безпосередньо. Я привів її, щоб порівняти швидкість її роботи з
тестами «TLSD (прямий доступ)» і «Б +-дерева (прямий доступ)», тому що в цих
тестах також здійснюється прямий доступ до вмісту колекцій. Ці
колекції відрізняються тільки використовуваними алгоритмами, і їх порівняння дозволить
побачити чистий різницю між алгоритмами. p>
«QuickDictionary (через індексатор)» - це моя
generic-реалізація хеш-таблиці. Доступ до даних здійснюється через індексатор
(реалізацію інтерфейсу IDictionary ). p>
«QuickDictionary (прямий доступ)» - те ж, що й
попереднє, але з прямим доступом до вмісту хеш-таблиці. p>
«Dictionary» - це generic-реалізація абстракції
Dictionary на базі хеш-таблиці. Входить в стандартну бібліотеку. NET Framework. P>
«TLSD (через індексатор)» - TwoLevelSortedDictionary,
generic-реалізація дворівневого сортованого масиву. Доступ до даних
здійснюється через індексатор (реалізацію інтерфейсу IDictionary ).
При вставці проводиться повторний пошук. P>
«TLSD (прямий доступ)» - те ж, що і попереднє, але
вставка проводиться у позицію. знайдену в процесі пошуку та доступ до вмісту
колекції здійснюється прямо. p>
«Б +-дерево (прямий доступ)» - generic-реалізація
Б +-дерева. P>
З цих тестів видно, що якщо у Влада Чистякова в
статті «Колекції
в. NET Framework Class Library »(з цього самого номера журналу) хеш-таблиця і
Сортувати список розрізняються за швидкістю приблизно в 4 рази, то тут - аж у
16 разів. Якщо ж порівнювати Dictionary і TwoLevelSortedDictionary, то їх
швидкість розрізняється менш ніж у 3 рази. p>
Дерева виграють у MySortedDictionary тільки за рахунок
часу вставки, тому що при вставці в MySortedDictionary здійснюється
сдвіжка пам'яті, тоді як час, що витрачається на вставку в Б +-дерево і
дворівневий масив, дуже мале. p>
Алгоритм Б +-дерев цікавий ще й тим, що при
великих обсягах швидкість пошуку в них стає навіть більше, ніж у масиві, за
рахунок використання кешу процесора, куди потрапляють елементи вищих рівнів. Всі
залежить від об'єму. Чим більше обсяг збережених даних, тим більші переваги
дають Б +-дерева. Ну а явне їх перевага починається з обсягів, що перевищують
мільйон елементів. p>
Список літератури h2>
Для підготовки даної роботи були використані
матеріали з сайту http://www.rsdn.ru/
p>