В компьютерном программировании веревка или шнур — это структура данных , состоящая из меньших строк , которая используется для эффективного хранения и обработки более длинных строк или целых текстов. Например, программа редактирования текста может использовать веревку для представления редактируемого текста, чтобы такие операции, как вставка, удаление и произвольный доступ, могли выполняться эффективно. [1]
Веревка — это тип бинарного дерева , в котором каждый лист (конечный узел) содержит строку и длину (также известную как вес ), а каждый узел выше по дереву содержит сумму длин всех листьев в своем левом поддереве . Таким образом, узел с двумя дочерними узлами делит всю строку на две части: левое поддерево хранит первую часть строки, правое поддерево хранит вторую часть строки, а вес узла — это длина первой части.
Для операций с веревками строки, хранящиеся в узлах, предполагаются постоянными неизменяемыми объектами в типичном неразрушающем случае, что допускает некоторое поведение копирования при записи . Листовые узлы обычно реализуются как базовые строки фиксированной длины с прикрепленным счетчиком ссылок для освобождения, когда они больше не нужны, хотя можно использовать и другие методы сбора мусора .
В следующих определениях N — длина веревки, то есть вес корневого узла.
окончательный класс InOrderRopeIterator реализует Iterator < RopeLike > { частный финальный стек Deque < RopeLike > ; InOrderRopeIterator ( @NonNull RopeLike root ) { stack = new ArrayDeque <> ( ); var c = root ; while ( c ! = null ) { stack.push ( c ); c = c.getLeft ( ) ; } } @Override public boolean hasNext ( ) { return stack.size ( ) > 0 ; } @Override public RopeLike next () { val result = stack . pop (); если ( ! стек . isEmpty ()) { var parent = stack . pop (); var right = parent . getRight (); если ( right != null ) { стек . push ( right ); var cleft = right . getLeft (); while ( cleft != null ) { стек . push ( cleft ); cleft = cleft . getLeft (); } } } вернуть результат ; } }
static boolean isBalanced ( RopeLike r ) { val depth = r.deep ( ) ; if ( deep > = FIBONACCI_SEQUENCE.length - 2 ) { return false ; } return FIBONACCI_SEQUENCE [ deep + 2 ] < = r.deep ( ) ; } static RopeLike rebalance ( RopeLike r ) { if ( ! isBalanced ( r )) { val leaves = Ropes . collectLeaves ( r ); return merge ( leaves , 0 , leaves . size ()); } return r ; } статическое слияние RopeLike ( List < RopeLike > leaves ) { return merge ( leaves , 0 , leaves . size ()); } static RopeLike merge ( List <RopeLike> leaves , int start , int end ) { int range = end - start ; if ( range == 1 ) { return leaves.get ( start ) ; } if ( range == 2 ) { return new RopeLikeTree ( leaves.get ( start ) , leaves.get ( start + 1 ) ) ; } int mid = start + ( range / 2 ) ; return new RopeLikeTree ( merge ( leaves , start , mid ) , merge ( leaves , mid , end ) ) ; }
Insert(i, S’)
вставить строку S', начиная с позиции i, в строку s , чтобы образовать новую строку C 1 , ..., C i , S' , C i + 1 , ..., C m .Эту операцию можно выполнить с помощью одной Split()
и двух Concat()
операций. Стоимость равна сумме трех.
public Rope insert ( int idx , CharSequence sequence ) { if ( idx == 0 ) { return prepend ( sequence ); } if ( idx == length ()) { return append ( sequence ); } val lhs = base . split ( idx ); return new Rope ( Ropes . concat ( lhs . fst . append ( sequence ), lhs . snd )); }
Index(i)
: вернуть символ в позиции iЧтобы извлечь i -й символ, мы начинаем рекурсивный поиск с корневого узла:
@Override public int indexOf ( char ch , int startIndex ) { if ( startIndex > weight ) { return right.indexOf ( ch , startIndex - weight ) ; } return left.indexOf ( ch , startIndex ) ; }
Например, чтобы найти символ на i=10
рисунке 2.1, показанном справа, начните с корневого узла (A), найдите, что 22 больше 10 и есть левый потомок, поэтому перейдите к левому потомку (B). 9 меньше 10, поэтому вычтите 9 из 10 (оставив i=1
) и перейдите к правому потомку (D). Затем, поскольку 6 больше 1 и есть левый потомок, перейдите к левому потомку (G). 2 больше 1 и есть левый потомок, поэтому снова перейдите к левому потомку (J). Наконец, 2 больше 1, но левого потомка нет, поэтому ответом является символ с индексом 1 короткой строки "na" (т. е. "n"). (индекс на основе 1)
Concat(S1, S2)
: соединить две веревки, S 1 и S 2 , в одну.Конкатенацию можно выполнить просто путем создания нового корневого узла с левым = S1 и правым = S2 , что является постоянным временем. Вес родительского узла устанавливается равным длине левого потомка S 1 , что займет время, если дерево сбалансировано.
Поскольку для большинства операций с канатами требуются сбалансированные деревья, после соединения может потребоваться повторная балансировка дерева.
Split (i, S)
разделить строку S на две новые строки S 1 и S 2 , S 1 = C 1 , ..., Ci и S 2 = Ci + 1 , ..., C m .Необходимо рассмотреть два случая:
Второй случай сводится к первому путем разделения строки в точке разделения для создания двух новых конечных узлов, а затем создания нового узла, который является родительским для двух составляющих строк.
Например, чтобы разделить 22-символьную связку, изображенную на рисунке 2.3, на две равные компонентные связку длиной 11, запросите 12-й символ, чтобы найти узел K на нижнем уровне. Удалите связь между K и G . Перейдите к родителю G и вычтите вес K из веса D . Поднимитесь по дереву и удалите все правые связи с поддеревьями, охватывающими символы после позиции 11, вычитая вес K из их родительских узлов (в данном случае только узлы D и A ). Наконец, создайте новые осиротевшие узлы K и H, объединив их вместе и создав нового родителя P с весом, равным длине левого узла K .
Поскольку для большинства операций с канатами требуются сбалансированные деревья, после раскалывания может потребоваться повторная балансировка дерева.
public Pair < RopeLike , RopeLike > split ( int index ) { if ( index < weight ) { val split = left . split ( index ); return Pair . of ( rebalance ( split . fst ), rebalance ( new RopeLikeTree ( split . snd , right ))); } else if ( index > weight ) { val split = right . split ( index - weight ); return Pair . of ( rebalance ( new RopeLikeTree ( left , split . fst )), rebalance ( split . snd )); } else { return Pair . of ( left , right ); } }
Delete(i, j)
удалить подстроку C i , …, C i + j − 1 , из s, чтобы сформировать новую строку C 1 , …, C i − 1 , C i + j , …, C m .Эту операцию можно выполнить двумя Split()
и одной Concat()
операцией. Сначала разделите веревку на три, разделенные на i -й и i+j -й символ соответственно, что извлечет строку для удаления в отдельный узел. Затем соедините два других узла.
@Override public RopeLike delete ( int start , int length ) { val lhs = split ( start ) ; val rhs = split ( start + length ) ; return rebalance ( new RopeLikeTree ( lhs.fst , rhs.snd ) ) ; }
Report(i, j)
вывести строку C i , …, C i + j − 1 .Чтобы сообщить строку C i , …, C i + j − 1 , найдите узел u , содержащий C i и weight(u) >= j
, а затем пройдите T , начиная с узла u . Выведите C i , …, C i + j − 1 , выполнив упорядоченный обход T , начиная с узла u .
Преимущества:
Недостатки:
В этой таблице сравниваются алгоритмические черты реализаций строк и веревок, а не их чистая скорость . Строки на основе массивов имеют меньшие накладные расходы, поэтому (например) операции конкатенации и разделения выполняются быстрее на небольших наборах данных. Однако, когда строки на основе массивов используются для более длинных строк, временная сложность и использование памяти для вставки и удаления символов становятся неприемлемо большими. Напротив, структура данных веревок имеет стабильную производительность независимо от размера данных. Кроме того, пространственная сложность для веревок и массивов составляет O(n). Подводя итог, веревки предпочтительны, когда данные большие и часто изменяются.