Как вычесть два списка (быстро)? [дубликат]

Pensé en investigar el comportamiento un poco más, así que aquí hay otra respuesta.

Internamente, rmusa FTS para recursivamente en jerarquías de archivos. fts_opentoma una matriz de rutas como parámetro y crea una estructura de árbol para cada ruta. Esto le permite al programador explorar varias ubicaciones sin problemas como si fueran parte de una jerarquía unificada.

Aquí hay un programa de prueba que puedes usar para jugar con FTS tú mismo.

#include 
#include 
#include 

int main(int argc, char* argv[])
{
    if(argc < 2) return EXIT_FAILURE;

    char* const* arr = argv + 1;
    FTS* hier = fts_open(arr, FTS_NOSTAT | FTS_PHYSICAL, NULL);

    FTSENT* ent;
    while((ent = fts_read(hier))) {
        printf("%s info=%d (D=%d DP=%d F=%d SL=%d)\n",
               ent->fts_accpath, ent->fts_info,
               ent->fts_info == FTS_D, ent->fts_info == FTS_DP,
               ent->fts_info == FTS_F || ent->fts_info == FTS_NSOK,
               ent->fts_info == FTS_SL);
    }

    fts_close(hier);
    return EXIT_SUCCESS;
}

Supongamos que hemos creado la siguiente estructura de directorios:

$ mkdir dir
$ touch dir/file
$ ln -s dir sym

Ahora, consideremos su primer caso y veamos cómo FTS lidera la exploración.

$ gcc fts.c 
$./a.out sym
sym info=12 (D=0 DP=0 F=0 SL=1)

Como puede ver, en este caso, symse ve como un archivo. Un enlace simbólico, para ser más exactos. Con esta información, rmlo trataría como un archivo y llamaría a unlinkat(AT_FDCWD, "sym", 0). El último parámetro (0 )hace que unlinkatse comporte como unlink. En otras palabras :simplemente borra un archivo. Como resultado, su enlace desaparece.

Ahora, echemos un vistazo a lo que sucede con sym/.

$./a.out sym/
sym/ info=1 (D=1 DP=0 F=0 SL=0)
file info=11 (D=0 DP=0 F=1 SL=0)
sym/ info=6 (D=0 DP=1 F=0 SL=0)

En este caso, symfue tratado como su directorio de destino. Primero iteramos a sym, luego sym/filey luego symnuevamente. Este último se debe a cómo funciona FTS :primero, itera sobre el contenido y luego regresa al nodo raíz. Esto es bastante conveniente para rm. En la primera pasada (D), puede borrar archivos, y en la segunda(DP)eliminar los directorios vacíos.

Como puede ver, en este caso, FTS informa sym/como un directorio en ambos casos. Esto se debe a que le dimos la ruta con una barra al final, lo que obliga al núcleo a interpretarla como un directorio. En el caso de un enlace, esto significa que lo seguirá sin importar nada. En términos más técnicos, las especificaciones dicen:

A pathname that contains at least one non-slash character and that ends with one or more trailing slashes shall be resolved as if a single dot character ( '.' ) were appended to the pathname.

Debido a que FTS informa sym/como un directorio, rmse comporta como si estuviera eliminando un directorio vacío. En consecuencia, llama unlinkat(AT_FDCWD, "sym/", AT_REMOVEDIR).Esto hace que unlinkatse comporte como rmdir.

Sin embargo, al resolver la ruta sym/, la llamada al sistema unlinkatse dará cuenta de que, de hecho, no se le está dando un directorio. Por lo tanto, informará ENOTDIR, lo que desencadena:

$ rm: cannot remove ‘sym/’: Not a directory

Y, de hecho, si elimina la marca -fde sus llamadas... Eso es exactamente lo que verá. Ahora, si esto es un error o una característica... No tengo idea.

8
14.05.2018, 20:46
3 ответа

Puede usar commpara eliminar todo lo que sea común a ambas listas:

listr=($(comm -3 <(printf "%s\n" "${list1[@]}" | sort) <(printf "%s\n" "${list2[@]}" | sort) | sort -n))

Esto ordena ambas listas en el orden commesperado, las compara, genera solo elementos que son exclusivos de cualquiera de las listas y los ordena nuevamente en orden numérico.

Si ambas listas están ordenadas lexicográficamente(según LC_COLLATE), puede evitar ordenarlas de nuevo:

listr=($(comm --nocheck-order -3 <(printf "%s\n" "${list1[@]}") <(printf "%s\n" "${list2[@]}")))

Esto también funciona muy bien si los valores que necesita comparar están almacenados en archivos.

11
27.01.2020, 20:09
#!/bin/zsh
list1=( 1 2 3 4 5 6 7 8 9 10 11 12 )
list2=( 1 2 3   5   7 8 9    11 12 )
listr=("${(@)list1:|list2}")
typeset -p listr
6
27.01.2020, 20:09

Resumen:

  • Para listas largas, si las listas ya están ordenadas, comm (alg7 )es el más rápido.

  • La solución zsh es (con mucho )la más rápida si no se lee ningún archivo, es decir, las listas se dan "en memoria". Sin embargo, esa no es una comparación justa con todas las demás soluciones que tienen que leer valores de archivos. Cambié el código original (que evitó el tiempo de lectura de archivos de prueba )a un código que también incluye el tiempo de lectura de archivos.


Esta es una respuesta de la comunidad, solo debe informar la hora de cada respuesta.

Por favor, HAGA editar y agregar su solución/opción para comparar todo.


Lista de algoritmos

  • alg1 :solución ingenua en bucle.
  • alg2 :usando externo sortyuniq -u
  • alg3 :procesando una cadena en bash.
  • alg4 :unirse -v en listas ordenadas (Gracias @Kusalananda)
  • alg5 :comunicación (Gracias @Stephen Kitt)
  • alg6 :zsh (Gracias @Llua)
  • alg7 :comm pero en archivos ya ordenados (gracias @Stephen Kitt)

Nota del manual zsh:

${name:|arrayname}
If arrayname is the name (N.B., not contents) of an array variable, then any elements contained in arrayname are removed from the substitution of name.

Prueba

Como hay varias formas de resolver esto,necesitamos un marco general para probar (con equidad )las respuestas.

Algunas pautas (cámbialas si las encuentras injustas):

  • Medir suficientes repeticiones para tener una precisión razonable.
  • Medir dentro de la carcasa utilizada (Evitar la carga/descarga de la carcasa ).
  • Evite la sobrecarga de salida al no imprimir o redirigir a /dev/null.

Código de prueba:

#!/bin/bash
alg1(){
         arr=( "${list1[@]}" )
         for i in "${list2[@]}"; do
             for j in "${!arr[@]}"; do
         if [[ "$i" == "${arr[j]}" ]]; then
             unset arr["$j"]
             break
         fi
             done
     done
     printf '%s ' "${arr[@]}"; echo
}

alg2(){
         arr=($({ printf '%s\n' "${list1[@]}" "${list2[@]}"; } | sort | uniq -u))
         printf '%s ' "${arr[@]}"; echo
}

alg3(){
         a=" $(printf '%s ' ${list1[@]})" # Watch the space at the start!!.
         for i in "${list2[@]}"; do
         a=${a/ "$i" / };
     done
     printf '%s\n' "$a"
}

alg4(){  join -v 1 list1.txt list2.txt ; }

alg5(){  #listr=$(
                    comm -3 <(printf "%s\n" "${list1[@]}" | sort) \
                            <(printf "%s\n" "${list2[@]}" | sort) |
                sort -n
     #)
      }

alg6(){ zsh -c '  alg6(){
                           list1=( $(cat list1.txt) )
                           list2=( $(cat list2.txt) )
                           listr=("${(@)list1:|list2}")
                           typeset -p listr
                        }
                  TIMEFMT="%E %U %S"
                  time ( for ((k=0;k<'"$1"';k++)); do alg6; done; )
                '
      }
#: <<-\_comment_
alg7(){ comm -23 list1.txt list2.txt; }

try(){ for ((k=0;k<$1;k++)); do "$2"; done; }

#list1=( 1 2 3 4 5 6 7 8 9 10 11 12 )
#list2=( 1 2 3   5   7 8 9    11 12 )

#list1=( a a b b b c     d d   )
#list2=(     b b   c c c d d e )

size=1000000
list1=( "0" $(seq 1 "$size") )
list2=( "${list1[@]}" ); unset "list2[123]" "list2[234]" "list2[345]"

printf '%s\n' "${list1[@]}" | sort >list1.txt
printf '%s\n' "${list2[@]}" | sort >list2.txt

repeats=${1:-10}; shift
out=${1:-no}    ; shift
(($#==0)) && set -- alg{1..7}

TIMEFORMAT='%R %U %S'
for   i
do    printf '%s ' "$i"
      if [[ $out == no ]]; then
      [[ $i != alg6 ]] &&
          time try "$repeats" "$i" >/dev/null ||
          time alg6 "$repeats" >/dev/null
      else
      [[ $i != alg6 ]] &&
          time try "$repeats" "$i"            ||
          time alg6 "$repeats"
      fi
done

Resultados:

Lista corta (como se presenta en el código):

$./script
alg1 2.056 0.806 1.237
alg2 3.478 3.126 1.756
alg3 0.999 0.728 0.304
alg4 1.186 0.780 0.434
alg5 5.234 1.926 1.722
alg6 2.71s 1.64s 1.26s
     2.758 1.637 1.265
alg7 1.156 0.799 0.422

Los tiempos para alg6 son los informados por zsh y después según lo informado por bash.
Además, el tiempo de ejecución de zsh es realmente menor (0.050 )si se quita la lectura de archivos de la función al exterior.

Lista más larga

Probar una lista de solo 500 valores (10 repeticiones )revela que alg1 es muy ineficiente. Eliminarlo de más pruebas:

alg1 4.149 3.471 0.657
alg2 0.145 0.055 0.063
alg3 0.219 0.180 0.009
alg4 0.039 0.015 0.003
alg5 0.149 0.018 0.027
alg6 0.06s 0.02s 0.02s
     0.087 0.030 0.018
alg7 0.033 0.008 0.008

Probar valores de 5k (10 repeticiones )revela que alg3 también es ineficiente:

alg2 0.590 0.526 0.187
alg3 12.957 12.888 0.044
alg4 0.098 0.047 0.008
alg5 0.654 0.028 0.036
alg6 0.16s 0.12s 0.04s
     0.211 0.118 0.044
alg7 0.038 0.022 0.014

Probando 50k valores (10 repeticiones):

alg2 6.487 5.838 1.611
alg4 0.488 0.469 0.019
alg5 5.073 0.250 0.056
alg6 1.42s 1.20s 0.21s
     1.467 1.206 0.216
alg7 0.271 0.247 0.014

Para 500k (10 repeticiones)

alg4 5.471 5.269 0.156
alg6 15.14s 13.33s 1.91s
     15.215 13.335 1.926
alg7 2.833 2.655 0.138

Para 1M (10 repeticiones)

alg4 11.127 10.804 0.251
alg7 5.772 5.525 0.230
4
27.01.2020, 20:09

Теги

Похожие вопросы