在带有 if 条件的 for 循环中强制按字母顺序排列

在带有 if 条件的 for 循环中强制按字母顺序排列

我想创建大量的文件夹并在其中进行一些操作。文件夹名称基于几种化学元素的排列,我将它们定义为for循环中的变量:

for Element in Cr Hf Mo Nb Ta Ti V W Zr

我想要一个文件夹,用于按字母顺序排列 4 个元素的所有排列,以便获得包含字母CrHfMoNbCrHfMoTa、 ... 等的子文件夹。我尝试使用 4 个嵌套for循环来完成此操作,但为了简单起见,我将在此处仅使用 2 个循环进行演示。我想出的代码是:

for Element in Cr Hf Mo Nb Ta Ti V W Zr; do
    for Elemen in Hf Mo Nb Ta Ti V W Zr; do
        mkdir "$Element""$Elemen"N     # the N at the end is intended
    done
done

这会产生我想要的文件夹,但也会产生很多不必要的文件夹,因为我还得到了像TiNbNZrVN这样的非字母组合以及像HfHfN.我可以通过在第三行添加 if 语句来消除重复项

do [ "$Element" != "$Elemen" ] && mkdir "$Element""$Elemen"N

尽管这些重复的文件夹并没有完全消失,而是成为我的目录中的“幻影”文件,这意味着它们被称为HfHfN等,但没有文件扩展名。然而真正的问题是其余的文件夹。我尝试添加更多 if 语句,例如

do [ "$Element" != "$Elemen" ] && [ "$Element" > "$Elemen" ] && mkdir "$Element""$Elemen"N

减少允许的排列数量,但这并不能消除任何东西。我还尝试将 if 语句分离到各自的 for 循环中,但这也不会改变任何内容:

for Element in Cr Hf Mo Nb Ta Ti V W Zr; do
    [ "$Element" != "$Elemen" ] && [ "$Element" > "$Elemen" ] &&
    for Elemen in Hf Mo Nb Ta Ti V W Zr;  do...

我不完全确定这是否>是正确的if命令,但从这个列表中http://tldp.org/LDP/Bash-Beginners-Guide/html/sect_07_01.html这似乎是最合理的。使用类似的命令-ne, -lt, -le, -gt也不起作用,因为它们需要整数,所以不接受字母。最后我想将4个循环组合在一起,这样就变得有点难以看透。我缺少什么?

答案1

#/bin/sh

# shellcheck disable=SC2046
# ^ word-splitting by the shell is intentional in this file

elems="Cr Hf Mo Nb Ta Ti V W Zr"
for a in $elems
do
    for b in $elems
    do
        for c in $elems
        do
            for d in $elems
            do
                # for a set of any four elements:
                #   string them together, separated by NUL-bytes
                #   sort them lexicographically ...
                #     ... with NUL separating the elements (-z)
                #     ... and eliminate duplicates (-u)
                #   then replace the NUL bytes with line breaks
                #   allow the shell to split on those line breaks
                #   and chuck the resulting chunks into $1, $2, etc
                set -- $(printf '%s\0' "$a" "$b" "$c" "$d" | sort -z -u | tr "\0" "\n")

                # only if the current selection of elements consisted of four
                # different ones (remember we eliminated duplicates):
                if [ $# -eq 4 ]
                then
                    # create a directory, don't error out if it already exists (-p)
                    mkdir -p "$(printf '%s' "$@")"
                fi
            done
        done
    done
done

效率不高(sort甚至调用明显的非候选者和多次mkdir调用同一目录名),但内部循环最多 9 4 = 6561 次迭代,而且它是一次性脚本,我认为不会这是值得花费大量时间进行优化的。


编辑:
Xeon E3-1231v3 的基准测试,没有mkdir

./elemdirs.sh > /dev/null  11.66s user 1.73s system 173% cpu 7.725 total

并随之而来:

./elemdirs.sh > /dev/null  13.80s user 2.16s system 156% cpu 10.215 total

它产生 126 个目录,预期数量组合其中 k = 4,n = 9。

答案2

使用 Perl 和Algorithm::Combinatorics模块:

perl -MAlgorithm::Combinatorics=combinations -e '$"=""; map { mkdir "@{$_}N" } combinations([qw(Cr Hf Mo Nb Ta Ti V W Zr)], 4)'

这将创建 126 个目录,您可以从包含的四个单词的所有组合中获得这些目录。每个目录的名称N末尾都有一个。由于代码中数组的初始排序,各个单词将始终按字母顺序出现在目录名称中。

作为一个正确的 Perl 脚本:

#!/usr/bin/perl

use strict;
use warnings;

use English;
use Algorithm::Combinatorics qw(combinations);

# When interpolating a list in a string (@{$ARG} below), don't use a delimiter
local $LIST_SEPARATOR = "";

# Get all combinations, and create a directory for each combination
map { mkdir "@{$ARG}N" } combinations( [qw(Cr Hf Mo Nb Ta Ti V W Zr)], 4 );

这几乎可以立即运行,并且可以轻松扩展以包含更多单词或组合长度。

你可能可以在 Python 中做一些非常类似的事情......


递归 shell 实现(只是为了好玩,递归 shell 函数很少非常有效):

#!/bin/sh

build_combinations () {
    set_size=$1
    shift

    if [ "$set_size" -eq 0 ]; then
        printf 'N'
    else
        for token do
            shift
            for reminder in $(build_combinations "$(( set_size - 1 ))" "$@")
            do
                printf '%s%s\n' "$token" "$reminder"
            done
        done
    fi
}

build_combinations 4 Cr Hf Mo Nb Ta Ti V W Zr | xargs mkdir

读过的想法斯图狗的回答以及来自各个方面的灵感StackOverflow 问题的答案

请注意,此解决方案的优点是目录名称始终以N.递归停止分支输出N而不是空字符串,这使得整个事情正常进行。如果没有它(打印空字符串或换行符),带有命令替换的循环将没有任何内容可循环,并且不会有输出(由于变量的默认值IFS)。

答案3

对 @n.st 答案的改进,利用了元素一开始就按排序顺序的事实。我认为这也更清楚一些。

#!/bin/bash

elements=(Cr Hf Mo Nb Ta Ti V W Zr)
len=${#elements[@]}

(( a_end = len - 3 ))
(( b_end = len - 2 ))
(( c_end = len - 1 ))
(( d_end = len - 0 ))

(( a = 0 ))
while (( a < a_end )); do
   (( b = a + 1 ))
   while (( b < b_end )); do
      (( c = b + 1 ))
      while (( c < c_end )); do
         (( d = c + 1 ))
         while (( d < d_end )); do
            mkdir "${elements[$a]}${elements[$b]}${elements[$c]}${elements[$d]}"
            (( d++ ))
         done
         (( c++ ))
      done
      (( b++ ))
   done
   (( a++ ))
done

每个内部循环的关键部分从封闭循环的下一个元素索引开始。这是生成项目列表的所有组合的非常常见的模式。

运行:

user@host:~/so$ time ./do.sh 

real    0m0.140s
user    0m0.085s
sys 0m0.044s

user@host:~/so$ ls -1d Cr* Hf* Mo* Nb* Ta* Ti* V* W* Zr* | wc -l
ls: cannot access 'V*': No such file or directory
ls: cannot access 'W*': No such file or directory
ls: cannot access 'Zr*': No such file or directory
126

答案4

花几个步骤来跳过冗余。它将加快整个过程。

declare -a lst=( Cr Hf Mo Nb Ta Ti V W Zr ) # make an array
for a in ${lst[@]}                          # for each element
do  for b in ${lst[@]:1}                    # for each but the 1st
    do [[ "$b" > "$a" ]] || continue        # keep them alphabetical and skip wasted work
        for c in ${lst[@]:2}                # for each but the first 2
        do  [[ "$c" > "$b" ]] || continue   # keep them alphabetical and skip wasted work
            for d in ${lst[@]:3}            # for each but the first 3
            do [[ "$d" > "$c" ]] || continue # keep them alphabetical and skip wasted work
                mkdir "$a$b$c$d" && echo "Made: $a$b$c$d" || echo "Fail: $a$b$c$d"
            done
        done
    done
done

冗余跳过适用于后面的循环开始时,例如当外部循环位于元素 4 上但第二个循环仍在元素 3 或 4 上时。它们会跳过这些,因为它们不是字母组合。这样做也保证不会重复。这在我的笔记本电脑上的 git bash 中生成了 126 个不同的目录,在 0m8.126s 中没有错误,除了mkdir.

相关内容