高效计算第二小值的方法

6

给定一个矩阵,计算最小值的值和索引很容易:

A = rand(10);
[value, index] = min(A(:));

然而,我也想恢复第二个最小值(最大值同理)。
当然,我可以采取以下两种方法之一:
1. 将A转换为向量并排序。 优点:然后我可以恢复第二、第三、第n个最小值。 缺点:如果A很大,则排序代价高昂。
2. 找到A的最小位置后,我可以将此值替换为一个大值(例如:Inf),然后再次运行min。 优点:比排序便宜。 缺点:我必须修改矩阵(并在辅助变量中保存修改后的值)。对于大矩阵来说,重新运行min的代价很高。
我想知道是否有更好的解决方案: 当计算min时,算法必须跟踪到目前为止找到的最小值,直到新值具有较低的值为止(然后我们更新该值)。 如果我们改为跟踪到目前为止找到的最后n个最小值,就可以恢复最小的n个值了。
我可以实现这个方法,但我想知道是否有更好的方法或者是否已经实现了这个方法。

如果使用 sort,你可以通过一次 昂贵的 sort 操作得到2倍最小值和2倍最大值。否则,你需要运行2个 min 和 2个 max。进行测试并找出哪种方法更加节省成本。 - Hoki
如果min能够跟踪n个较小的值,那么它就不会很昂贵。我认为这将是一个不错的功能。您可以将要恢复的最小值数量作为可选参数提供。 - user2261062
我们需要多大的规模呢?sort非常快。而且,如果您拥有GPU,您可以随时使用gpuarray。在GPU中,reduce操作非常快。 - Ander Biguri
请看 这里 - Luis Mendo
还要检查Bruno的解决方案(其中一个是mex):https://www.mathworks.com/matlabcentral/newsreader/view_thread/309300 - user5128199
4个回答

3

我不知道在什么情况下它比排序更便宜,但是一个简单但不太快的方法是使用以下代码。 我可能错了,但是如果您只想要第一和第二小,使用内置函数也无法更快。

A = rand(10);
[firstMin, firstMinIndex] = min(A(:));
secondMin = min(A(A~=firstMin));
secondMinIndex = find(A==secondMin); % slow, but use only if you need the index

在这里,您需要再次遍历矩阵两次,一次进行布尔运算,一次进行第二个最小值。

在对2000x2000和4000x4000的随机矩阵进行了一些测试后,似乎这段代码片段比应用于相同矩阵的排序函数快大约3.5倍。

如果您真的需要更高的效率,您将需要编写自己的mex例程,通过该例程,您可以在n+log n-2次比较中理论上获得两个值,如@luismendotomas提供的链接所述。

希望这可以帮助您!


如果你只想要第二小的值,你可以使用第一次调用时的索引而不必再次遍历矩阵,像这样:[2nd_min,2nd_ind]=min(A(setdiff(1:numel(A),1st_ind)));。但是,如果你想要n个最小值,那就更加复杂了,最好使用适当的算法... - Adiel
Adiel,原则上你是正确的。但在实践中,Matlab使用逻辑索引要快得多... setdiff也相对较慢。在我的电脑上,逻辑索引比你提出的代码快了大约6倍! - beesleep
好的,知道了。 - Adiel

0

如前所述,我认为最好(即“最有效”)的方法是实现@luismendotomas链接中的方法。

然而,如果您想避免过多的编程工作,那么您可以应用一些k最近邻算法,假设您对数据有一个下限,例如,如果所有数据点都是正数,则可以找到离0最近的2个邻居。虽然我不确定这是否比您最初的建议更快。

有关一个k最近邻算法,请参见this


0

在一次遍历中:

a = [53 53 49 49 97 75 4 22 4 37];

first = Inf;
second = Inf;

for i = 1:1:numel(a)
    if (a(i) < first)
        second = first;
        first = a(i);
    elseif (a(i) < second && a(i) ~= first)
        second = a(i);
    end
end

fprintf('First smallest %d\n', first);
fprintf('Second smallest %d\n', second);

如果你更喜欢将a(i) ~= first这个条件去掉,那么输出结果会变成4, 4而不是4, 23

另外,可以查看这个SO问题


1
是的,这就是我在问题结尾提出的建议,但我想避免使用循环,因为它们速度较慢。 - user2261062

0

beesleep已经指出方法2(通过两次计算最小值)比方法1(通过排序)更有效。然而,答案中提供的实现通过find计算第二个最小值的索引是非常低效的。

事实上,为了获得第二个最小值的索引,将第一个最小值设置为inf(如问题中建议的)然后从min函数中获取第二个最小值的索引(而不是使用find)大约快了10倍

[firstMin, firstMinIndex] = min(A(:));
A(firstMinIndex) = inf;
[secondMin, secondMinIndex] = min(A(:));

这是我用来比较此实现与beesleep建议的代码:

for i = 1:10

    A = rand(10000);

    tic
    [firstMin, firstMinIndex] = min(A(:));
    secondMin = min(A(A~=firstMin));
    secondMinIndex = find(A==secondMin); % slow, but use only if you need the index
    t1(i) = toc;

    tic
    [firstMin, firstMinIndex] = min(A(:));
    A(firstMinIndex) = inf;
    [secondMin, secondMinIndex] = min(A(:));
    t2(i) = toc;

end

disp(mean(t1) / mean(t2))

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接