【问题标题】:How to remove lines from a list which can be found within other longer lines in the list?如何从列表中删除可以在列表中其他较长行中找到的行?
【发布时间】:2013-07-23 02:36:28
【问题描述】:

我有一个文件,list.txt,如下所示:

cat
bear
tree
catfish
fish
bear

我需要删除文档中其他地方已经完全找到的所有行,无论是作为重复行还是在另一个较长的行中找到。例如,“bear”和“bear”行相同,因此删除其中之一; "cat" 完全可以在 "catfish" 中找到,因此 "cat" 被删除。输出如下所示:

catfish
tree
bear

如何删除所有重复行,包括在列表中较长行中找到的行?

到目前为止,我有这个:

#!/bin/bash
touch list.tmp
while read -r line
do
    found="$(grep -c $line list.tmp)"
    if [ "$found" -eq "1" ]
    then
        echo $line >> list.tmp
        echo $line" added"
    else
        echo "Not added."
fi
done < list.txt

【问题讨论】:

  • 到目前为止你有什么?
  • 顺便说一句,如果一个单词出现多次,您的代码也会失败(想想“shellfish”、“catfish”中的“fish”)。正确的检查方法是if grep -q "$line" list.tmp; then ...

标签: perl bash sed


【解决方案1】:

如果 O(N^2) 不打扰你:

#!/usr/bin/env perl

use strict;
use warnings;
use List::MoreUtils qw{any};

my @words;
for my $word (
    sort {length $b <=> length $a}
    do {
        my %words;
        my @words = <>;
        chomp @words;
        @words{@words} = ();
        keys %words;
    }
)
{
    push @words, $word unless do {
        my $re = qr/\Q$word/;
        any {m/$re/} @words;
    };
}

print "$_\n" for @words;

如果你想要 O(NlogN),你必须使用某种 trie 方法。例如使用后缀树:

#!/usr/bin/env perl

use strict;
use warnings;
use Tree::Suffix;

my $tree = Tree::Suffix->new();

my @words;
for my $word (
    sort {length $b <=> length $a}
    do {
        my %words;
        my @words = <>;
        chomp @words;
        @words{@words} = ();
        keys %words;
    }
)
{
    unless ($tree->find($word)){
        push @words, $word;
        $tree->insert($word);
    };
}

print "$_\n" for @words;

【讨论】:

    【解决方案2】:

    我能想到一个相当不错的算法。我将在 Perl 中回答以保持结果足够有效。

    对于每个单词,测试它是否是较大单词集中的任何单词的子字符串。如果不是,则从集合中删除所有作为该单词子串的单词,并将该单词添加到集合中。

    因为这通常意味着遍历所有值,所以我们不妨使用数组。为了加快速度,我们保持数组以递减的方式排序。这允许我们对集合中已经存在的每个单词只测试一次。

    use strict; use warnings;
    
    my @words;
    INPUT:
    while (<>) {
      chomp;
      my $len = length;
      my $i = 0;
    
      # check larger words if they contain $_
      LARGER:
      for ( ; $i < @words ; $i++) {
        last LARGER if length $words[$i] < $len;
        next INPUT if 0 <= index $words[$i], $_; # the word was seen
      }
    
      # insert the new word
      splice @words, $i++, 0, $_;
    
      # remove words that are contained in new word
      for ( ; $i < @words ; $i++) {
        splice @words, $i--, 1 if 0 <= index $_, $words[$i]; # $i-- adjusts index for deletion
      }
    }
    print "$_\n" for @words;
    

    0 &lt;= index $a, $b 是编写$a =~ /\Q$b\E/ 的有效方式。

    这是对 David W. 算法的概括。如果输入按字长递减排序,则两种实现都会产生相同的输出。


    如果单词很短,但有很多不同的单词,最好记住所有可能的子字符串。这使我们能够快速检测到所见的单词,但将单词添加到已知列表中的成本很高。

    my %seen;  # used to detect seen words
    my %words; # used to remember real words
    while (<>) {
      chomp;
      next if exists $seen{$_};
      # so we didn't see it. Let's produce all substrings
      START: for (my $start = 0 ; $start < length() - 1 ; $start++) {
        LENGTH: for (my $length = length() - $start ; $length ; $length--) {
          my $substr = substr $_, $start, $length;
          delete $words{$substr};         # if this was a real word, it's now a substring
          last LENGTH if exists $seen{$substr};  # dont repeat yourself
          $seen{$substr} = undef;         # add the entry
        }
      }
      $words{$_} = undef;  # remember this word as a real word
    }
    undef %seen;  # free obscene amount of memory
    print "$_\n" for keys %words;
    

    【讨论】:

      【解决方案3】:

      这需要对文件进行两次传递,但应该可以:

      script.awk 的内容

      NR==FNR {
        words[$1]++
        next
      } 
      {
        for (word in words) { 
          if (index ($1,word) == 0) { 
            words[word] 
          } 
          else { 
            delete words[word]
            words[$1] 
          } 
        }
      }
      END {
        for (left in words)
          print left
      }
      

      测试:

      $ cat file
      cat
      bear
      tree
      catfish
      fish
      bear
      $ awk -f script.awk file file
      bear
      catfish
      tree
      

      【讨论】:

      • 您可以将所有处理移至END 块,并通过文件本身的单个I/O 传递来摆脱困境。处理仍然是两遍,你最终会在内存中得到整个文件,但如果输入真的很大,你可以通过按长度排序来解决这个问题。
      【解决方案4】:

      这可能对你有用(GNU sed):

      sed -r ':a;$!{N;ba};s/\b([^\n]+)\n(.*\1)/\2/;ta;s/(([^\n]+).*\n)(\2)\n?/\1/;ta' file
      

      在内存中读取文件,然后删除在整个文件中前后重复的单个单词。

      【讨论】:

        【解决方案5】:

        由于子字符串问题,这将非常困难。最初,我正在考虑对我的列表进行排序,而 catcatfish 之类的内容将彼此相邻,但请查看此列表::

        bug
        bear
        calf
        catbug
        catbear
        

        对此列表进行排序无济于事。另外,这个呢?

        concatenate
        cat
        bear
        bug
        

        我会忽略cat 吗?已经在concatenate这个词里了?

        这个呢:

        cat
        concatenate
        bear
        bug
        

        在这种情况下,单词 catconcatenate 都在列表中,因为 catconcatenate 之前是列表中的第一个。由于没有单词已经属于 concatenate,因此它进入了列表。

        除非我需要双向检查。我要添加到列表中的单词是否已经在列表中并且是列表中已经包含在我正在查看的单词中的单词。

        这不仅是一个定义不明确的问题,而且是一个代码混乱。编码实际上很简单,但最终会生成一个 O2 类型的算法。这意味着列表大小加倍会导致处理时间加倍。如果我能在一秒钟内处理 100 个单词,我需要 4 秒来完成 200 个单词,8 秒来完成 400 个单词,16 秒来完成 800 个单词。差不多 20 秒做 1000 个单词。

        这里使用您的定义,其中顺序很重要。也就是说,如果catcatbug 之前,则两者都在您的批准列表中,但如果catbugcat 之前,则cat 不会进入列表:

        #! /usr/bin/env perl
        #
        use strict;
        use warnings;
        use autodie;
        use feature qw(say);
        use Data::Dumper;
        
        use constant {
            LIST_FILE => "text.txt",
        };
        
        open my $list_fh, "<", LIST_FILE;
        my @approved_list;
        while ( my $new_word = <list_fh> ) {
            chomp $new_word;
            my $new_word_in_list = 0;
            for my $word_already_in_list ( @approved_list ) {
                if ( $word_already_in_list =~ /\Q$new_word\E/ ) {
                    # Word is already in the list or in a word in the list
                    $new_word_in_list = 1;
                    last;
                }
            }
            if ( not $new_word_in_list ) {
                push @approved_list, $new_word;
            }
        }
        say Dumper \@approved_list;
        

        深思

        我之前意识到我可以使用 grep 代替内部循环:

        #! /usr/bin/env perl
        #
        use strict;
        use warnings;
        use autodie;
        use feature qw(say);
        use Data::Dumper;
        
        use constant {
            LIST_FILE => "text.txt",
        };
        
        open my $list_fh, "<", LIST_FILE;
        my @approved_list;
        while ( my $new_word = <$list_fh> ) {
            chomp $new_word;
            if ( not grep { /\Q$new_word\E/ } @approved_list ) {
                push @approved_list, $new_word;
            }
        }
        say Dumper \@approved_list
        

        程序看起来更短,似乎只需要一个循环,但grep 隐藏了内部循环。为了使 grep 工作,它仍然需要遍历数组中的每一个条目。这就是为什么我决定不使用grep,而是让内部循环更加明确。

        但是,如果我可以使用字符串而不是数组来保存单词,并且我可以用某个字符分隔单词,我可以保证它不在单词中,该怎么办?也许我可以在字符串上使用正则表达式。这样会更有效率吗?

        #! /usr/bin/env perl
        #
        use strict;
        use warnings;
        use autodie;
        use feature qw(say);
        use Data::Dumper;
        
        use constant {
            LIST_FILE => "text.txt",
        };
        
        open my $list_fh, "<", LIST_FILE;
        my $approved_list = "";
        while ( my $new_word = <$list_fh> ) {
            chomp $new_word;
            if ( not $approved_list =~ /\Q$new_word\E/ ) {
                $approved_list = ( $approved_list ) ? "$approved_list\0$new_word" : $new_word;
            }
        }
        say Dumper split /\0/, $approved_list;
        

        在上面,我将批准的单词列表放在一个名为$approved_list 的标量中。我假设单词不包含NUL 字符,将单词与NUL character 分开。现在,我可以用新词 grep 标量。如果它还没有在$approved_list 中,我会在它前面加上NUL 字符(\0)。稍后我可以拆分 NUL 以再次返回列表。

        使用正则表达式会更快吗?如果我批准的列表包含 1000 个单词,平均每个单词 5 个字符(可能更长,因为较长的单词比较短的单词更有可能)。那是一个 6000 个字符的字符串,我正在做一个正则表达式。这样更有效率吗?很难说。

        有三种解决方案:

        1. 第一个使用隐式内部循环。
        2. 第二个使用grep 隐藏内部循环。
        3. 第三个将单词列表串成一个字符串,由一个字符分隔,我是 absolutely 肯定不在字符串中。 (我的钱在NUL)。

        唯一的判断方法是在所有三个上都使用Benchmark::Timer 之类的东西,看看哪个最有效——这可能会根据列表大小、单词等而改变。

        【讨论】:

        • 如果你按长度排序,就像@Amon 的回答一样,你可以避免比较两种方式的问题。
        • @tripleee 如果顺序不重要,按长度排序将避免顺序问题。
        【解决方案6】:

        只是为了好玩,这里是一个 shell 脚本版本。不过,我通过使用 Perl 打印行长来作弊。

        #!/bin/sh
        
        touch list.tmp
        
        # Schwartzian transform: add length as prefix for each line,
        perl -nle 'print length, "\t", $_' list.txt |
        # reverse sort by this prefix,
        sort -rn |
        # and discard the prefix
        cut -f2- |
        while read -r line; do
             grep -q "$line" list.tmp && continue
             echo "$line" >>list.tmp
        done
        

        【讨论】:

          猜你喜欢
          • 1970-01-01
          • 1970-01-01
          • 2021-03-17
          • 2021-03-12
          • 1970-01-01
          • 2021-05-26
          • 1970-01-01
          • 1970-01-01
          • 2016-04-23
          相关资源
          最近更新 更多