在一列数字中,找到与某个目标值最接近的值

时间:2013-07-25 08:39:25

标签: bash awk

假设我在列中有一些数字数据,比如

11.100000 36.829657 6.101642
11.400000 36.402069 5.731998
11.700000 35.953025 5.372652
12.000000 35.482082 5.023737
12.300000 34.988528 4.685519
12.600000 34.471490 4.358360
12.900000 33.930061 4.042693
13.200000 33.363428 3.738985
13.500000 32.770990 3.447709
13.800000 32.152473 3.169312

我还有一个目标值和一个列索引。给定这组数据,我想在具有指定索引的列中找到与目标值最接近的值。

例如,如果我的目标值在11.6列中为1,则脚本应输出11.7。如果有两个与目标值等距的数字,则应输出较高的值。

我觉得awk具有执行此操作的必要功能,但欢迎任何在bash脚本中运行的解决方案。

3 个答案:

答案 0 :(得分:7)

试试这个:

awk -v c=2 -v t=35 'NR==1{d=$c-t;d=d<0?-d:d;v=$c;next}{m=$c-t;m=m<0?-m:m}m<d{d=m;v=$c}END{print v}' file

-v c=2-v t=35可以是动态值。它们是列idx(c)和您的目标值(t)。在上面的行中,参数是第2列和目标25.它们可以是shell变量。

基于给定输入数据的上述行的输出是:

kent$  awk -v c=2 -v t=35 'NR==1{d=$c-t;d=d<0?-d:d;v=$c;next}{m=$c-t;m=m<0?-m:m}m<d{d=m;v=$c}END{print v}' f
34.988528

kent$  awk -v c=1 -v t=11.6 'NR==1{d=$c-t;d=d<0?-d:d;v=$c;next}{m=$c-t;m=m<0?-m:m}m<d{d=m;v=$c}END{print v}' f
11.700000

修改

  

如果有两个与目标值等距的数字,则应输出较高的值

以上代码未检查此要求....下面的代码应该有效:

awk -v c=1 -v t=11.6 '{a[NR]=$c}END{
        asort(a);d=a[NR]-t;d=d<0?-d:d;v = a[NR]
        for(i=NR-1;i>=1;i--){
                m=a[i]-t;m=m<0?-m:m
                if(m<d){
                    d=m;v=a[i]
                }
        }
        print v
}' file

试验:

kent$  awk -v c=1 -v t=11.6 '{a[NR]=$c}END{
        asort(a);d=a[NR]-t;d=d<0?-d:d;v = a[NR]
        for(i=NR-1;i>=1;i--){
                m=a[i]-t;m=m<0?-m:m
                if(m<d){
                    d=m;v=a[i]
                }
        }
        print v
}' f
11.700000

简短说明。

我不会解释每行代码,它的作用。告诉我们做这项工作的想法。

  • 首先读取给定列中的所有元素,保存在数组中
  • 对数组进行排序。
  • 从数组中取出最后一个元素(最大数字)。将其分配给var v,并计算它与给定目标之间的差异,将其保存(绝对值)d
  • 从数组循环的第二个最后一个元素到第一个元素。如果元素和目标之间的差异(绝对值)小于d,则用diff覆盖d,同时将当前元素保存到v
  • print v,循环后,v就是答案。

注意事项

  • 有优化逻辑的空间。例如我们不必遍历整个数组。只需比较d(abs),如果新差异&gt; d,我们可以停止循环。
  • 由于排序,此算法 O(nlogn) 。事实上,这个问题可以通过 O(n) 来解决。如果您的输入数据很大,并且情况最糟糕(例如,您的列的值在500-99999999999范围内,但您的目标是1.),您可能希望避免排序。但我认为你的表现不是问题。

答案 1 :(得分:2)

让我们尝试另一种方式,虽然肯特的回答必须更短更清晰:)

awk -vc=1 -vv=13.6 '
    BEGIN{l=$c; ld=99}
    {d=($c-v>=0) ? ($c-v) : v-$c; if (d <= ld) {ld=d; l=$c}}
    END{print l}' file

我们在开头提供c(=列)和v(=值)参数。

然后我们会跟踪较低的值l和最低的距离ld。对于每个值,我们计算距离值d,如果它低于上一个ld,我们会在l中交换并保存新的最小值。最后,我们打印l

d=($c-v>=0) ? ($c-v) : v-$c是一种将距离保存为绝对值的方法:如果c-v为负数,则将其保存为正数。它基于value=(condition) ? if yes : else结构。

测试

$ awk -vc=2 -vv=13.6 'BEGIN{l=$c; ld=99} {d=($c-v>=0) ? ($c-v) : v-$c; if (d <= ld) {ld=d; l=$c}} END{print l}' file
32.152473
$ awk -vc=3 -vv=10.6 'BEGIN{l=$c; ld=99} {d=($c-v>=0) ? ($c-v) : v-$c; if (d <= ld) {ld=d; l=$c}} END{print l}' file
3.169312

答案 2 :(得分:2)

Perl解决方案:

#!/usr/bin/perl
use warnings;
use strict;

@ARGV == 2 or die "Usage: closest column value < input\n";
my ($column, $target) = (shift, shift);
my $closest;
while (<>) {
    my $value = (split)[$column - 1];
    if ($. == 1
        or abs($closest - $target) >  abs($target - $value)
        or abs($closest - $target) == abs($target - $value)
           && $value > $closest) {
        $closest = $value;
    }
}
print $closest, "\n";

请注意,使用 float == float 可能不起作用(What Every Computer Scientist Should Know About Floatin-Point Arithmetic)。您可能需要abs(abs($closest - $target) - abs($target - $value)) < 1e-14

之类的内容