Глава 12. Множества и деревья. 12.1. Представление множеств с помощью деревьев. Полное двоичное дерево. T-деревья. Нарисуем точку. Из нее проведем две стрелки (влево вверх и вправо вверх) в две другие точки. Из каждой из этих точек прове- дем по две стрелки и так далее. Полученную картинку (в n-ом слое будет (2 в степени (n - 1)) точек) называют полным двоичным де- ревом. Нижнюю точку называют корнем. У каждой вершины есть два сына (две вершины, в которые идут стрелки) - левый и правый. У всякой вершины, кроме корня, есть единственный отец. Пусть выбрано некоторое конечное множество вершин полного двоичного дерева, содержащее вместе с каждой вершиной и всех ее предков. Пусть на каждой вершине этого множества написано значе- ние фиксированного типа T (то есть задано отображение множества вершин в множество значений типа T). То, что получится, будем называть T-деревом. Множество всех T-деревьев обозначим Tree(T). Рекурсивное определение. Всякое непустое T-дерево разбива- ется на три части: корень (несущий пометку из T), левое и правое поддеревья (которые могут быть и пустыми). Это разбиение уста- навливает взаимно однозначное соответствие между множеством не- пустых T-деревьев и произведением T * Tree (T) * Tree (T). Обоз- начив через empty пустое дерево, можно написать Tree (T) = {empty} + T * Tree (T) * Tree (T). Поддеревья. Высота. Фиксируем некоторое T-дерево. Для каждой его вершины x оп- ределено ее левое поддерево (левый сын вершины x и все его по- томки), правое поддерево (правый сын вершины x и все его потом- ки) и поддерево с корнем в x (вершина x и все ее потомки). Левое и правое поддеревья вершины x могут быть пустыми, а поддерево с корнем в x всегда непусто (содержит по крайней мере x). Высотой поддерева будем считать максимальную длину цепи y[1]..y[n] его вершин, в которой y [i+1] - сын y [i] для всех i. (Высота пусто- го дерева равна нулю, высота дерева из одного корня - единице.) Упорядоченные T-деревья. Пусть на множестве значений типа T фиксирован порядок. На- зовем T-дерево упорядоченным, если выполнено такое свойство: для любой вершины x все пометки в ее левом поддереве меньше пометки в x, а все пометки в ее правом поддереве больше пометки в x. 12.1.1. Доказать, что в упорядоченном дереве все пометки различны. Указание. Индукция по высоте дерева. Представление множеств с помощью деревьев. Каждое дерево будем считать представлением множества всех пометок на его вершинах. При этом одно и то же множество может иметь различные представления. Благодаря упорядоченности каждый элемент легко может "найти свое место" в дереве: придя в какую-то вершину и сравнив себя с тем, кто там находится, элемент решает, идти ему налево или нап- раво. Начав с корня и двигаясь по этому правилу, он либо обнару- жит, что такой элемент уже есть, либо найдет место, в котором он должен быть. Всюду далее мы предполагаем, что на значениях типа T задан порядок, и рассматриваем только упорядоченные деревья. Хранение деревьев в программе. Можно было бы сопоставить вершины полного двоичного дерева с числами 1, 2, 3,... (считая, что левый сын (n) = 2n, правый сын (n) = 2n + 1) и хранить пометки в массиве val [1...]. Однако этот способ неэкономен, поскольку тратится место на хранение пустых вакансий в полном двоичном дереве. Более экономен такой способ. Введем три массива val: array [1..n] of T; left, right: array [1..n] of 0..n; (n - максимальное возможное число вершин дерева) и переменную root: 0..n. Каждая вершина хранимого T-дерева будет иметь номер - число от 1 до n. Разные вершины будут иметь разные номера. По- метка в вершине с номером x равна val [x]. Корень имеет номер root. Если вершина с номером i имеет сыновей, то их номера равны left [i] и right [i]. Отсутствующим сыновьям соответствует число 0. Аналогичным образом значение root = 0 соответствует пустому дереву. Для хранения дерева используется лишь часть массива; для тех i, которые свободны - т.е. не являются номерами вершин - значения val [i] безразличны. Нам будет удобно, чтобы все сво- бодные числа были "связаны в список": первое хранится в специ- альное переменной free: 0..n, а следующее за i свободное число хранится в left [i], так что свободны числа free, left [free], left [left[free]],... Для последнего свободного числа i значение left [i] = 0. Ра- венство free = 0 означает, что свободных чисел больше нет. (За- мечание. Мы использовали для связывания свободных вершин массив left, но, конечно, с тем же успехом можно было использовать мас- сив right.) Вместо значения 0 (обозначающего отсутствие вершины) можно было бы воспользоваться любым другим числом вне 1..n. Чтобы под- черкнуть это, будем вместо 0 использовать константу null = 0. 12.1.2. Составить программу, определяющую, содержится ли элемент t: T в упорядоченном дереве (хранимом так, как только что описано). Решение. if root = null then begin | ..не принадлежит end else begin | x := root; | {инвариант: остается проверить наличие t в непустом подде- | реве с корнем x} | while ((t < val [x]) and (left [x] <> null)) or | | ((t > val [x]) and (right [x] <> null)) do begin | | if t < val [x] then begin {left [x] <> null} | | | x := left [x]; | | end else begin {t > val [x], right [x] <> null} | | | x := right [x]; | | end; | end; | {либо t = val [x], либо t отсутствует в дереве} | ..ответ = (t = val [x]) end; 12.1.3. Упростить решение, используя следующий трюк. Расши- рим область определения массива val, добавив ячейку с номером null и положим val [null] = t. Решение. val [null] := t; x := root; while t <> val [x] do begin | if t < val [x] then begin | | x := left [x]; | end else begin | | x := right [x]; | end; end; ..ответ: (x <> null). 12.1.4. Составить программу добавления элемента t в мно- жество, представленное упорядоченным деревом (если элемент t уже есть, ничего делать не надо). Решение. Определим процедуру get_free (var i: integer), да- ющую свободное (не являющееся номером) число i и соответствующим образом корректирующую список свободных чисел. procedure get_free (var i: integer); begin | {free <> null} | i := free; | free := left [free]; end; С ее использованием программа приобретает вид: if root = null then begin | get_free (root); | left [root] := null; right [root] := null; | val [root] := t; end else begin | x := root; | {инвариант: осталось добавить t к непустому поддереву с | корнем в x} | while ((t < val [x]) and (left [x] <> null)) or | | ((t > val [x]) and (right [x] <> null)) do begin | | if t < val [x] then begin | | | x := left [x]; | | end else begin {t > val [x]} | | | x := right [x]; | | end; | end; | if t <> val [x] then begin {t нет в дереве} | | get_free (i); | | left [i] := null; right [i] := null; | | val [i] := t; | | if t < val [x] then begin | | | left [x] := i; | | end else begin {t > val [x]} | | | right [x] := i; | | end; | end; end; 12.1.5. Составить программу удаления элемента t из мно- жества, представленного упорядоченным деревом (если его там нет, ничего делать не надо). Решение. if root = null then begin | {дерево пусто, ничего делать не надо} end else begin | x := root; | {осталось удалить t из поддерева с корнем в x; поскольку | это может потребовать изменений в отце x, введем | переменные father: 1..n и direction: (l, r); | поддерживаем такой инвариант: если x не корень, то father | - его отец, а direction равно l или r в зависимости от | того, левым или правым сыном является x} | while ((t < val [x]) and (left [x] <> null)) or | | ((t > val [x]) and (right [x] <> null)) do begin | | if t < val [x] then begin | | | father := x; direction := l; | | | x := left [x]; | | end else begin {t > val [x]} | | | father := x; direction := r; | | | x := right [x]; | | end; | end; | {t = val [x] или t нет в дереве} | if t = val [x] then begin | | ..удаление вершины x с отцом father и направлением | | direction | end; end; Удаление вершины x происходит по-разному в разных случаях. При этом используется процедура procedure make_free (i: integer); begin | left [i] := free; | free := i; end; она включает число i в список свободных. Различаются 4 случая в зависимости от наличия или отсутствия сыновей у удаляемой верши- ны. if (left [x] = null) and (right [x] = null) then begin | {x - лист, т.е. не имеет сыновей} | make_free (x); | if x = root then begin | | root := null; | end else if direction = l then begin | | left [father] := null; | end else begin {direction = r} | | right [father] := null; | end; end else if (left[x]=null) and (right[x] <> null) then begin | {x удаляется, а right [x] занимает место x} | make_free (x); | if x = root then begin | | root := right [x]; | end else if direction = l then begin | | left [father] := right [x]; | end else begin {direction = r} | | right [father] := right [x]; | end; end else if (left[x] <> null) and (right[x]=null) then begin | ..симметрично end else begin {left [x] <> null, right [x] <> null} | ..удалить вершину с двумя сыновьями end; Удаление вершины с двумя сыновьями нельзя сделать просто так, но ее можно предварительно поменять с вершиной, пометка на которой является непосредственно следующим (в порядке возрастания) эле- ментом за пометкой на x. y := right [x]; father := x; direction := r; {теперь father и direction относятся к вершине y} while left [y] <> null do begin | father := y; direction := r; | y := left [y]; end; {val [y] - минимальная из пометок, больших val [x], y не имеет левого сына} val [x] := val [y]; ..удалить вершину y (как удалять вершину, у которой нет ле- вого сына, мы уже знаем) 12.1.6. Упростить программу удаления, заметив, что некото- рые случаи (например, первые два из четырех) можно объединить. 12.1.7. Использовать упорядоченные деревья для представле- ния функций, область определения которых - конечные множества значений типа T, а значения имеют некоторый тип U. Операции: вы- числение значения на данном аргументе, изменение значения на данном аргументе, доопределение функции на данном аргументе, исключение элемента из области определения функции. Решение. Делаем как раньше, добавив еще один массив func_val: array [1..n] of U; если val [x] = t, func_val [x] = u, то значение хранимой функции на t равно u. Оценка количества действий. Для каждой из операций (проверки, добавления и исключения) количество действий не превосходит C * (высота дерева). Для "ровно подстриженного" дерева (когда все листья на одной высоте) высота по порядку величины равна логарифму числа вершин. Однако для кривобокого дерева все может быть гораздо хуже: в наихудшем случае все вершины образуют цепь и высота равна числу вершин. Так случится, если элементы множества добавляются в возрастающем или убывающем порядке. Можно доказать, однако, что при добавле- нии элементов "в случайном порядке" средняя высота дерева будет не больше C * (логарифм числа вершин). Если этой оценки "в сред- нем" мало, необходимы дополнительные действия по поддержанию "сбалансированности" дерева. Об этом см. в следующем пункте. 12.1.8. Предположим, что необходимо уметь также отыскивать k-ый элемент множества (в порядке возрастания), причем коли- чество действий должно быть не более C*(высота дерева). Какую дополнительную информацию надо хранить в вершинах дерева? Решение. В каждой вершине будем хранить число всех ее по- томков. Добавление и исключение вершины требует коррекции лишь на пути от корня к этой вершине. В процессе поиска k-ой вершины поддерживается такой инвариант: искомая вершина является s-ой вершиной поддерева с корнем в x (здесь s и x - переменные).) 12.2. Сбалансированные деревья. Дерево называется сбалансированным (или АВЛ-деревом в честь изобретателей этого метода Г.М.Адельсона-Вельского и Е.М.Ланди- са), если для любой его вершины высоты левого и правого подде- ревьев этой вершины отличаются не более чем на 1. (В частности, когда одного из сыновей нет, другой - если он есть - обязан быть листом.) 12.2.1. Найти минимальное и максимальное возможное коли- чество вершин в сбалансированном дереве высоты n. Решение. Максимальное число вершин равно (2 в степени n) - 1. Если m (n) - минимальное число вершин, то, как легко видеть, m (n + 2) = 1 + m (n) + m (n+1), откуда m (n) = fib (n+1) - 1 (fib(n) - n-ое число Фибоначчи, fib(0)=1, fib(1)=1, fib(n+2) = fib(n) + fib(n+1)). 12.2.2. Доказать, что сбалансированное дерево с n вершинами имеет высоту не больше C * (log n) для некоторой константы C, не зависящей от n. Решение. Индукцией по n легко доказать, что fib [n+1] >= (a в степени n), где a - больший корень квадратного уравнения a*a = 1 + a, то есть a = (sqrt(5) + 1)/2. Остается воспользоваться предыдущей задачей. Вращения. Мы хотим восстанавливать сбалансированность дерева после включения и удаления элементов. Для этого необходимы какие-то преобразования дерева, не меняющие множества пометок на его вер- шинах и не нарушающие упорядоченности, но способствующие лучшей сбалансированности. Опишем несколько таких преобразований. Пусть вершина a имеет правого сына b. Обозначим через P ле- вое поддерево вершины a, через Q и R - левое и правое поддеревья вершины b. Упорядоченность дерева требует, чтобы P < a < Q < b < R (точнее следовало бы сказать "любая пометка на P меньше пометки на a", "пометка на a меньше любой пометки на Q" и т.д., но мы позволим себе этого не делать). Точно того же требует упорядо- ченность дерева с корнем b, его левым сыном a, в котором P и Q - левое и правое поддеревья a, R - правое поддерево b. Поэтому первое дерево можно преобразовать во второе, не нарушая упорядо- ченности. Такое преобразование назовем малым правым вращением (правым - поскольку существует симметричное, левое, малым - пос- кольку есть и большое, которое мы сейчас опишем). Пусть b - правый сын a, c - левый сын b, P -левое поддерево a, Q и R -левое и правое поддеревья c, S - правое поддерево b. Тогда P < a < Q < c < R < b < S. Такой же порядок соответствует дереву с корнем c, имеющим левого сына a и правого сына b, для которого P и Q - поддеревья вершины a, а R и S - поддеревья вершины b. Соответствующее преобразова- ние будем называть большим правым вращением. (Аналогично опреде- ляется симметричное ему большое левое вращение.) 12.2.3. Дано дерево, сбалансированное всюду, кроме корня, в котором разница высот равна 2 (т.е. левое и правое поддеревья корня сбалансированы и их высоты отличаются на 2). Доказать, что оно может быть превращено в сбалансированное одним из четырех описанных преобразований, причем высота его останется прежней или уменьшится на 1. Решение. Пусть более низким является, например, левое под- дерево, и его высота равна k. Тогда высота правого поддерева равна k+2. Обозначим корень через a, а его правого сына (он обя- зательно есть) через b. Рассмотрим левое и правое поддеревья вершины b. Одно из них обязательно имеет высоту k+1, а другое может иметь высоту k или k+1 (меньше k быть не может, так как поддеревья сбалансированы). Если высота левого поддерева равна k+1, а правого - k, до потребуется большое правое вращение; в остальных случаях помогает малое. ------------------------------------ ------------------------------------ ------------------------------------ высота уменьшилась на 1 ------------------------------------ ------------------------------------ ------------------------------------ высота не изменилась k-1 или k (в одном из случаев k) ------------------------------------ ------------------------------------ ------------------------------------ высота уменьшилась на 1 Три случая балансировки дерева. 12.2.4. В сбалансированное дерево добавили или из него уда- лили лист. Доказать, что можно восстановить сбалансированность с помощью нескольких вращений, причем их число не больше высоты дерева. Решение. Будем доказывать более общий факт: Лемма. Если в сбалансированном дереве X одно из его подде- ревьев Y заменили на сбалансированное дерево Z, причем высота Z отличается от высоты Y не более чем на 1, то полученное такой "прививкой" дерево можно превратить в сбалансированное вращени- ями (причем количество вращений не превосходит высоты, на кото- рой делается прививка). Частным случаем прививки является замена пустого поддерева на лист или наоборот, так что достаточно доказать эту лемму. Доказательство леммы. Индукция по высоте, на которой дела- ется прививка. Если она происходит в корне (заменяется все дере- во целиком), то все очевидно ("привой" сбалансирован по усло- вию). Пусть заменяется некоторое поддерево, например, левое под- дерево некоторой вершины x. Возможны два случая. (1) После прививки сбалансированность в вершине x не нару- шилась (хотя, возможно, нарушилась сбалансированность в предках x: высота поддерева с корнем в x могла измениться). Тогда можно сослаться на предположение индукции, считая, что мы прививали целиком поддерево с корнем в x. (2) Сбалансированность в x нарушилась. При этом разница вы- сот равна 2 (больше она быть не может, так как высота Z отлича- ется от высоты Y не более чем на 1). Разберем два варианта. (2а) Выше правое (не заменявшееся) поддерево вершины x. Пусть высота левого (т.е. Z) равна k, правого - k+2. Высота ста- рого левого поддерева вершины x (т.е. Y) была равна k+1. Подде- рево с корнем x имело в исходном дереве высоту k+3, и эта высота не изменилась после прививки. По предыдущей задаче вращение преобразует поддерево с кор- нем в x в сбалансированное поддерево высоты k+2 или k+3. То есть высота поддерева с корнем x - в сравнении с его прежней высотой - не изменилась или уменьшилась на 1, и мы можем воспользоваться предположением индукции. ------------- ---------------- ------------- ---------------- -------------k ----------------k 2а 2б (2б) Выше левое поддерево вершины x. Пусть высота левого (т.е. Z) равна k+2, правого - k. Высота старого левого поддерева (т.е. Y) была равна k+1. Поддерево с корнем x в исходном дереве X имело высоту k+2, после прививки она стала равна k+3. После подходящего вращения (см. предыдущую задачу) поддерево с корнем в x станет сбалансированным, его высота будет равна k+2 или k+3, так что изменение высоты по сравнению с высотой поддерева с кор- нем x в дереве X не превосходит 1 и можно сослаться на предполо- жение индукции. 12.2.5. Составить программы добавления и удаления элемен- тов, сохраняющие сбалансированность. Число действий не должно превосходить C*(высота дерева). Разрешается хранить в вершинах дерева дополнительную информацию, необходимую при балансировке. Решение. Будем хранить для каждой вершины разницу между высотой ее правого и левого поддеревьев: diff [i] = (высота правого поддерева вершины с номером i) - (высота левого поддерева вершины с номером i). Нам потребуются четыре процедуры, соответствующие большим и ма- лым правым и левым вращениями. Но вначале два замечания. (1) Нам нужно, чтобы при вращении поддерева номер его корня не менялся. (В противном случае потребовалось бы корректировать информацию в отце корня, что нежелательно.) Этого можно достичь, так как номера вершин дерева можно выбирать независимо от их значений. (На картинках номер указан сбоку от вершины, а значе- ние - внутри.) Малое правое вращение Большое правое вращение (2) После преобразований мы должны также изменить соот- ветственно значения в массиве diff. Для этого достаточно знать высоты деревьев P, Q, ... с точностью до константы, поэтому мож- но предполагать, что одна из высот равна нулю. Вот процедуры вращений: procedure SR (a:integer); {малое правое вращение с корнем a} | var b: 1..n; val_a,val_b: T; h_P,h_Q,h_R: integer; begin | b := right [a]; {b <> null} | val_a := val [a]; val_b := val [b]; | h_Q := 0; h_R := diff[b]; h_P := (max(h_Q,h_R)+1)-diff[a]; | val [a] := val_b; val [b] := val_a; | right [a] := right [b] {поддерево R} | right [b] := left [b] {поддерево Q} | left [b] := left [a] {поддерево P} | left [a] := b; | diff [b] := h_Q - h_P; | diff [a] := h_R - (max (h_P, h_Q) + 1); end; procedure BR (a:integer);{большое правое вращение с корнем a} | var b,c: 1..n; val_a,val_b,val_c: T; | h_P,h_Q,h_R,h_S: integer; begin | b := right [a]; c := left [b]; {b,c <> null} | val_a := val [a]; val_b := val [b]; val_c := val [c]; | h_Q := 0; h_R := diff[c]; h_S := (max(h_Q,h_R)+1)+diff[b]; | h_P := 1 + max (h_S, h_S-diff[b]) - diff [a]; | val [a] := val_c; val [c] := val_a; | left [b] := right [c] {поддерево R} | right [c] := left [c] {поддерево Q} | left [c] := left [a] {поддерево P} | left [a] := c; | diff [b] := h_S - h_R; | diff [c] := h_Q - h_P; | diff [a] := max (h_S, h_R) - max (h_P, h_Q); end; Левые вращения (большое и малое) записываются симметрично. Процедуры добавления и удаления элементов пишутся как раньше, но только добавление и удаление должно сопровождаться коррекцией массива diff и восстановлением сбалансированности. При этом используется процедура с такими свойствами: дано: левое и правое поддеревья вершины с номером a сбалан- сированы, в самой вершине разница высот не больше 2, в поддереве с корнем a массив diff заполнен правильно; надо: поддерево с корнем a сбалансировано и массив diff со- ответственно изменен, d - изменение его высоты (равно 0 или -1); в остальной части все осталось как было} procedure balance (a: integer; var d: integer); begin {-2 <= diff[a] <= 2} | if diff [a] = 2 then begin | | b := right [a]; | | if diff [b] = -1 then begin | | | BR (a); d := -1; | | end else if diff [b] = 0 then begin | | | SR (a); d := 0; | | end else begin {diff [b] = 1} | | | SR (a); d := - 1; | | end; | end else if diff [a] = -2 then begin | | b := left [a]; | | if diff [b] = 1 then begin | | | BL (a); d := -1; | | end else if diff [b] = 0 then begin | | | SL (a); d := 0; | | end else begin {diff [b] = -1} | | | SL (a); d := - 1; | | end; | end else begin {-2 < diff [a] < 2, ничего делать не надо} | | d := 0; | end; end; Восстановление сбалансированности требует движения от листьев к корню, поэтому будем хранить в стеке путь от корня к рассматриваемой в данный момент вершине. Элементами стека будут пары (вершина, направление движения из нее), т.е. значения типа record | vert: 1..n; {вершина} | direction : (l, r); {l - левое, r- правое} end; Программа добавления элемента t теперь выглядит так: if root = null then begin | get_free (root); | left [root] := null; right [root] := null; diff[root] := 0; | val [root] := t; end else begin | x := root; ..сделать стек пустым | {инвариант: осталось добавить t к непустому поддереву с | корнем в x; стек содержит путь к x} | while ((t < val [x]) and (left [x] <> null)) or | | ((t > val [x]) and (right [x] <> null)) do begin | | if t < val [x] then begin | | | ..добавить в стек пару | | | x := left [x]; | | end else begin {t > val [x]} | | | ..добавить в стек пару | | | x := right [x]; | | end; | end; | if t <> val [x] then begin {t нет в дереве} | | get_free (i); val [i] := t; | | left [i] := null; right [i] := null; diff [i] := 0; | | if t < val [x] then begin | | | ..добавить в стек пару | | | left [x] := i; | | end else begin {t > val [x]} | | | ..добавить в стек пару | | | right [x] := i; | | end; | | d := 1; | | {инвариант: стек содержит путь к изменившемуся поддереву, | | высота которого увеличилась по сравнению с высотой в | | исходном дереве на d (=0 или 1); это поддерево сбалан- | | сировано; значения diff для его вершин правильны; в ос- | | тальном дереве все осталось как было - в частности, | | значения diff} | | while (d <> 0) and ..стек непуст do begin {d = 1} | | | ..взять из стека пару в | | | if direct = l then begin | | | | if diff [v] = 1 then begin | | | | | c := 0; | | | | end else begin | | | | | c := 1; | | | | end; | | | | diff [v] := diff [v] - 1; | | | end else begin {direct = r} | | | | if diff [v] = -1 then begin | | | | | c := 0; | | | | end else begin | | | | | c := 1; | | | | end; | | | | diff [v] := diff [v] + 1; | | | end; | | | {c = изменение высоты поддерева с корнем в v по сравне- | | | нию с исходным деревом; массив diff содержит правиль- | | | ные значения для этого поддерева; возможно нарушение | | | сбалансированности в v} | | | balance (v, d1); d := c + d1; | | end; | end; end; Легко проверить, что значение d может быть равно только 0 или 1 (но не -1): если c = 0, то diff [v] = 0 и балансировка не произ- водится. Программа удаления строится аналогично. Ее основной фраг- мент таков: {инвариант: стек содержит путь к изменившемуся поддереву, высота которого изменилась по сравнению с высотой в исходном дереве на d (=0 или -1); это поддерево сбалансировано; значения diff для его вершин правильны; в остальном дереве все осталось как было - в частности, значения diff} while (d <> 0) and ..стек непуст do begin | {d = -1} | ..взять из стека пару в | if direct = l then begin | | if diff [v] = -1 then begin | | | c := -1; | | end else begin | | | c := 0; | | end; | | diff [v] := diff [v] + 1; | end else begin {direct = r} | | if diff [v] = 1 then begin | | | c := -1; | | end else begin | | | c := 0; | | end; | | diff [v] := diff [v] - 1; | end; | {c = изменение высоты поддерева с корнем в v по срав- | нению с исходным деревом; массив diff содержит | правильные значения для этого поддерева; | возможно нарушение сбалансированности в v} | balance (v, d1); | d := c + d1; end; Легко проверить, что значение d может быть равно только 0 или -1 (но не -2): если c = -1, то diff [v] = 0 и балансировка не про- изводится. Отметим также, что наличие стека делает излишними перемен- ные father и direction (их роль теперь играет вершина стека). 12.2.6. Доказать, что при добавлении элемента (а) второй из трех случаев балансировки (см. рисунок выше) невозможен; (б) полная балансировка требует не более одного вращения (после чего все дерево становится сбалансированным), в то время как при удалении элемента может понадобиться много вращений. Замечание. Мы старались записать программы добавления и удаления так, чтобы они были как можно более похожими друг на друга. Используя специфику каждой из них, можно многое упрос- тить. Существуют и другие способы представления множеств, гаран- тирующие число действий порядка log n на каждую операцию. Опишем один из них (называемый Б-деревьями). До сих пор каждая вершина содержала один элемент хранимого множества. Этот элемент служил границей между левым и правым поддеревом. Будем теперь хранить в вершине k >= 1 элементов мно- жества (число k может меняться от вершины к вершине, а также при добавлении и удалении новых элементов, см. далее). Эти k элемен- тов служат разделителями для k+1 поддерева. Пусть фиксировано некоторое число n >= 1. Будем рассматривать деревья, обладающие такими свойствами: (1) Каждая вершина содержит от n до 2n элементов (за исклю- чением корня, который может содержать любое число элементов от 0 до 2n). (2) Вершина с k элементами либо имеет k+1 сына, либо не имеет сыновей вообще (такие вершины называются листьями). (3) Все листья находятся на одной и той же высоте. Добавление элемента происходит так. Если лист, в который он попадает, неполон (т.е. содержит менее 2n элементов), то нет проблем. Если он полон, то 2n+1 элемент (все элементы листа и новый элемент) разбиваем на два листа по n элементов и разделя- ющий их серединный элемент. Этот серединный элемент надо доба- вить в вершину предыдущего уровня. Это возможно, если в ней ме- нее 2n элементов. Если и она полна, то ее разбивают на две, вы- деляют серединный элемент и т.д. Если в конце концов мы захотим добавить элемент в корень, а он окажется полным, то корень рас- щепляется на две вершины, а высота дерева увеличивается на 1. Удаление элемента. Удаление элемента, находящемся не в лис- те, сводится к удалению непосредственно следующего за ним, кото- рый находится в листе. Поэтому достаточно научиться удалять эле- мент из листа. Если лист при этом становится неполным, то его можно пополнить за счет соседнего листа - если только и он не имеет минимально возможный размер n. Если же оба листа имеют размер n, то на них вместе 2n элементов, вместе с разделителем - 2n+1. После удаления одного элемента остается 2n элементов - как раз на один лист. Если при этом вершина предыдущего уровня ста- новится меньше нормы, процесс повторяется и т.д. 12.2.7. Реализовать описанную схему хранения множеств, убе- дившись, что она также позволяет обойтись C*log(n) действий для операций включения, исключения и проверки принадлежности. 12.2.8. Можно определять сбалансированность дерева иначе: требовать, чтобы для каждой вершины ее левое и правое поддеревья имели не слишком сильно отличающиеся количества вершин. (Преиму- щество такого определения состоит в том, что при вращениях изме- няется сбалансированность только в одной вершине.) Реализовать на основе этой идеи способ хранения множеств, гарантирующий оценку в C*log(n) действий для включения, удаления и проверки принадлежности. (Указание. Он также использует большие и малые вращения. Подробности см. в книге Рейнгольда, Нивергельта и Део "Комбинаторные алгоритмы".)