En programación informática , una cuerda , o cordón , es una estructura de datos compuesta de cadenas más pequeñas que se utiliza para almacenar y manipular eficientemente una cadena muy larga. Por ejemplo, un programa de edición de texto puede utilizar una cuerda para representar el texto que se está editando, de modo que operaciones como inserción, eliminación y acceso aleatorio se puedan realizar de manera eficiente. [1]
Una cuerda es un tipo de árbol binario donde cada hoja (nodo final) contiene una cuerda y una longitud (también conocida como "peso"), y cada nodo más arriba en el árbol contiene la suma de las longitudes de todas las hojas en su subárbol izquierdo . Un nodo con dos hijos divide así toda la cadena en dos partes: el subárbol izquierdo almacena la primera parte de la cadena, el subárbol derecho almacena la segunda parte de la cadena y el peso de un nodo es la longitud de la primera parte.
Para las operaciones con cuerdas, se supone que las cadenas almacenadas en los nodos son objetos constantes e inmutables en el caso típico no destructivo, lo que permite cierto comportamiento de copia en escritura . Los nodos hoja generalmente se implementan como cadenas básicas de longitud fija con un recuento de referencia adjunto para desasignación cuando ya no se necesitan, aunque también se pueden usar otros métodos de recolección de basura .
En las siguientes definiciones, N es la longitud de la cuerda, es decir, el peso del nodo raíz.
La clase final InOrderRopeIterator implementa Iterator <RopeLike> { pila final privada Deque <RopeLike> ; InOrderRopeIterator ( @NonNull RopeLike root ) { pila = new ArrayDeque <> (); var c = raíz ; mientras ( c ! = nulo ) { pila . empujar ( c ); c = c . obtenerIzquierda (); } } @Override public boolean hasNext () { pila de retorno . tamaño () > 0 ; } @Override public RopeLike next () { val resultado = pila . estallido (); if ( ! pila . está vacío ()) { var padre = pila . estallido (); var derecha = padre . obtenerDerecha (); if ( correcto ! = nulo ) { pila . empujar ( derecha ); var hendidura = derecha . obtenerIzquierda (); while ( hendido ! = nulo ) { pila . empujar ( hendidura ); hendidura = hendidura . obtenerIzquierda (); } } } resultado de retorno ; } }
estático booleano isBalanced ( RopeLike r ) { val profundidad = r . profundidad (); if ( profundidad >= FIBONACCI_SEQUENCE . longitud - 2 ) { return false ; } devolver FIBONACCI_SEQUENCE [ profundidad + 2 ] <= r . peso (); } reequilibrio estático de RopeLike ( RopeLike r ) { if ( ! isBalanced ( r )) { val leaves = Ropes . recoger hojas ( r ); return merge ( hojas , 0 , hojas . tamaño ()); } devolver r ; } fusión estática RopeLike ( Lista < RopeLike > hojas ) { return merge ( hojas , 0 , hojas . tamaño ()); } fusión estática RopeLike ( List < RopeLike > hojas , int inicio , int fin ) { int rango = fin - inicio ; if ( rango == 1 ) { return hojas . obtener ( comenzar ); } if ( rango == 2 ) { return new RopeLikeTree ( hojas . get ( inicio ), hojas . get ( inicio + 1 )); } int mid = inicio + ( rango / 2 ); devolver nuevo RopeLikeTree ( fusionar ( hojas , inicio , mitad ), fusionar ( hojas , mitad , final )); }
Insert(i, S’)
inserte la cadena S' comenzando en la posición i en la cadena s , para formar una nueva cadena C 1 , ..., C i , S' , C i + 1 , ..., C m .Esta operación se puede realizar mediante una Split()
y dos Concat()
operaciones. El costo es la suma de los tres.
inserción de cuerda pública ( int idx , secuencia CharSequence ) { if ( idx == 0 ) { return anteponer ( secuencia ); } if ( idx == longitud ()) { return append ( secuencia ); } val lhs = base . dividir ( idx ); devolver nueva cuerda ( cuerdas . concat ( lhs . fst . append ( secuencia ), lhs . snd )); }
Index(i)
: devuelve el carácter en la posición iPara recuperar el i -ésimo carácter, comenzamos una búsqueda recursiva desde el nodo raíz:
@Override public int indexOf ( char ch , int startIndex ) { if ( startIndex > peso ) { retorno a la derecha . indexOf ( ch , startIndex - peso ); } regresar a la izquierda . indexOf ( ch , índiceInicio ); }
Por ejemplo, para encontrar el carácter en i=10
la Figura 2.1 que se muestra a la derecha, comience en el nodo raíz (A), encuentre que 22 es mayor que 10 y hay un hijo izquierdo, así que vaya al hijo izquierdo (B). 9 es menor que 10, así que resta 9 de 10 (dejando i=1
) y ve al hijo derecho (D). Luego, como 6 es mayor que 1 y queda un hijo izquierdo, vaya al hijo izquierdo (G). 2 es mayor que 1 y hay un hijo izquierdo, así que ve al hijo izquierdo nuevamente (J). Finalmente, 2 es mayor que 1 pero no queda ningún hijo, por lo que el carácter en el índice 1 de la cadena corta "na" (es decir, "n") es la respuesta. (índice basado en 1)
Concat(S1, S2)
concatenar dos cuerdas, S 1 y S 2 , en una sola cuerda.Se puede realizar una concatenación simplemente creando un nuevo nodo raíz con left = S1 y right = S2 , que es un tiempo constante. El peso del nodo padre se establece en la longitud del hijo izquierdo S 1 , lo que llevaría tiempo, si el árbol está equilibrado.
Como la mayoría de las operaciones con cuerdas requieren árboles equilibrados, es posible que sea necesario reequilibrar el árbol después de la concatenación.
Split (i, S)
: dividir la cadena S en dos nuevas cadenas S 1 y S 2 , S 1 = C 1 , ..., C i y S 2 = C i + 1 , ..., C m .Hay dos casos que deben abordarse:
El segundo caso se reduce al primero al dividir la cadena en el punto de división para crear dos nuevos nodos hoja y luego crear un nuevo nodo que sea el padre de las dos cadenas componentes.
Por ejemplo, para dividir la cuerda de 22 caracteres que se muestra en la Figura 2.3 en dos cuerdas componentes iguales de longitud 11, consulte el carácter 12 para ubicar el nodo K en el nivel inferior. Elimine el vínculo entre K y G. Vaya al padre de G y reste el peso de K del peso de D. Suba por el árbol y elimine los enlaces derechos a los subárboles que cubren los caracteres más allá de la posición 11, restando el peso de K de sus nodos principales (solo los nodos D y A , en este caso). Finalmente, construya los nodos K y H recién huérfanos concatenándolos y creando un nuevo padre P con un peso igual a la longitud del nodo izquierdo K.
Como la mayoría de las operaciones con cuerdas requieren árboles equilibrados, es posible que sea necesario reequilibrar el árbol después de dividirlo.
Par público < RopeLike , RopeLike > split ( int index ) { if ( índice < peso ) { val split = left . dividir ( índice ); volver Par . de ( reequilibrar ( split . fst ), reequilibrar ( new RopeLikeTree ( split . snd , right ))); } else if ( índice > peso ) { val split = right . dividir ( índice - peso ); volver Par . de ( reequilibrar ( nuevo RopeLikeTree ( izquierda , dividir . fst )), reequilibrar ( dividir . snd )); } else { return Par . de ( izquierda , derecha ); } }
Delete(i, j)
eliminar la subcadena C i , …, C i + j − 1 , de s para formar una nueva cadena C 1 , …, C i − 1 , C i + j , …, C m .Esta operación se puede realizar mediante dos Split()
y una Concat()
operación. Primero, divida la cuerda en tres, dividida por el carácter i -ésimo e i+j -ésimo respectivamente, lo que extrae la cadena para eliminar en un nodo separado. Luego concatene los otros dos nodos.
@Override public RopeLike eliminar ( int inicio , int longitud ) { val lhs = split ( inicio ); val rhs = dividir ( inicio + longitud ); reequilibrio de retorno ( nuevo RopeLikeTree ( lhs . fst , rhs . snd )); }
Report(i, j)
: genera la cadena C i , …, C i + j − 1 .Para informar la cadena C i ,…, C i + j − 1 , encuentre el nodo u que contiene C i y weight(u) >= j
, y luego recorra T comenzando en el nodo u . Salida C i , …, C i + j − 1 haciendo un recorrido en orden de T comenzando en el nodo u .
Ventajas:
Desventajas:
Esta tabla compara las características algorítmicas de las implementaciones de cuerdas y cuerdas, no su velocidad bruta . Las cadenas basadas en matrices tienen una sobrecarga menor, por lo que (por ejemplo) las operaciones de concatenación y división son más rápidas en conjuntos de datos pequeños. Sin embargo, cuando se utilizan cadenas basadas en matrices para cadenas más largas, la complejidad del tiempo y el uso de memoria para insertar y eliminar caracteres se vuelven inaceptablemente grandes. Por el contrario, una estructura de datos de cuerda tiene un rendimiento estable independientemente del tamaño de los datos. Además, la complejidad espacial de las cuerdas y las matrices es O(n). En resumen, las cuerdas son preferibles cuando los datos son grandes y se modifican con frecuencia.