Более быстрая версия поиска для отсортированных векторов (MATLAB)
У меня есть код следующего вида в MATLAB:
indices = find([1 2 2 3 3 3 4 5 6 7 7] == 3)
Это возвращает 4,5,6 - индексы элементов в массиве, равном 3. Теперь. мой код делает такие вещи с очень длинными векторами. Векторы всегда сортируются.
Поэтому мне нужна функция, которая заменяет сложность O (n) на поиск с O (log n) за счет того, что массив нужно сортировать.
Я знаю об этом, но для того, что я знаю, он не возвращает индексы всех предметов, только последний (мне все они нужны).
По соображениям переносимости мне нужно, чтобы решение было только MATLAB (без скомпилированных файлов mex и т.д.)
Ответы
Ответ 1
Вот быстрая реализация с использованием бинарного поиска. Этот файл также доступен на github
function [b,c]=findInSorted(x,range)
%findInSorted fast binary search replacement for ismember(A,B) for the
%special case where the first input argument is sorted.
%
% [a,b] = findInSorted(x,s) returns the range which is equal to s.
% r=a:b and r=find(x == s) produce the same result
%
% [a,b] = findInSorted(x,[from,to]) returns the range which is between from and to
% r=a:b and r=find(x >= from & x <= to) return the same result
%
% For any sorted list x you can replace
% [lia] = ismember(x,from:to)
% with
% [a,b] = findInSorted(x,[from,to])
% lia=a:b
%
% Examples:
%
% x = 1:99
% s = 42
% r1 = find(x == s)
% [a,b] = myFind(x,s)
% r2 = a:b
% %r1 and r2 are equal
%
% See also FIND, ISMEMBER.
%
% Author Daniel Roeske <danielroeske.de>
A=range(1);
B=range(end);
a=1;
b=numel(x);
c=1;
d=numel(x);
if A<=x(1)
b=a;
end
if B>=x(end)
c=d;
end
while (a+1<b)
lw=(floor((a+b)/2));
if (x(lw)<A)
a=lw;
else
b=lw;
end
end
while (c+1<d)
lw=(floor((c+d)/2));
if (x(lw)<=B)
c=lw;
else
d=lw;
end
end
end
Ответ 2
Даниэль подход умный, и его функция myFind2 определенно быстро, но есть ошибки/ошибки, которые происходят вблизи граничных условий или в случае, если верхняя и нижняя границы производят диапазон вне переданного набора.
Кроме того, как он отметил в своем комментарии к его ответу, его реализация имела некоторые недостатки, которые можно было бы улучшить. Я реализовал улучшенную версию своего кода, который работает быстрее, а также правильно обрабатывает граничные условия. Кроме того, этот код содержит больше комментариев, чтобы объяснить, что происходит. Надеюсь, это поможет кому-то, как мне помогает код Дэниела!
function [lower_index,upper_index] = myFindDrGar(x,LowerBound,UpperBound)
% fast O(log2(N)) computation of the range of indices of x that satify the
% upper and lower bound values using the fact that the x vector is sorted
% from low to high values. Computation is done via a binary search.
%
% Input:
%
% x- A vector of sorted values from low to high.
%
% LowerBound- Lower boundary on the values of x in the search
%
% UpperBound- Upper boundary on the values of x in the search
%
% Output:
%
% lower_index- The smallest index such that
% LowerBound<=x(index)<=UpperBound
%
% upper_index- The largest index such that
% LowerBound<=x(index)<=UpperBound
if LowerBound>x(end) || UpperBound<x(1) || UpperBound<LowerBound
% no indices satify bounding conditions
lower_index = [];
upper_index = [];
return;
end
lower_index_a=1;
lower_index_b=length(x); % x(lower_index_b) will always satisfy lowerbound
upper_index_a=1; % x(upper_index_a) will always satisfy upperbound
upper_index_b=length(x);
%
% The following loop increases _a and decreases _b until they differ
% by at most 1. Because one of these index variables always satisfies the
% appropriate bound, this means the loop will terminate with either
% lower_index_a or lower_index_b having the minimum possible index that
% satifies the lower bound, and either upper_index_a or upper_index_b
% having the largest possible index that satisfies the upper bound.
%
while (lower_index_a+1<lower_index_b) || (upper_index_a+1<upper_index_b)
lw=floor((lower_index_a+lower_index_b)/2); % split the upper index
if x(lw) >= LowerBound
lower_index_b=lw; % decrease lower_index_b (whose x value remains \geq to lower bound)
else
lower_index_a=lw; % increase lower_index_a (whose x value remains less than lower bound)
if (lw>upper_index_a) && (lw<upper_index_b)
upper_index_a=lw;% increase upper_index_a (whose x value remains less than lower bound and thus upper bound)
end
end
up=ceil((upper_index_a+upper_index_b)/2);% split the lower index
if x(up) <= UpperBound
upper_index_a=up; % increase upper_index_a (whose x value remains \leq to upper bound)
else
upper_index_b=up; % decrease upper_index_b
if (up<lower_index_b) && (up>lower_index_a)
lower_index_b=up;%decrease lower_index_b (whose x value remains greater than upper bound and thus lower bound)
end
end
end
if x(lower_index_a)>=LowerBound
lower_index = lower_index_b;
else
lower_index = lower_index_a;
end
if x(upper_index_b)<=UpperBound
upper_index = upper_index_a;
else
upper_index = upper_index_b;
end
Обратите внимание, что улучшенная версия функции Daniels searchFor теперь просто:
function [lower_index,upper_index] = mySearchForDrGar(x,value)
[lower_index,upper_index] = myFindDrGar(x,value,value);
Ответ 3
ismember
предоставит вам все индексы, если вы посмотрите на первый вывод:
>> x = [1 2 2 3 3 3 4 5 6 7 7];
>> [tf,loc]=ismember(x,3);
>> inds = find(tf)
inds =
4 5 6
Вам просто нужно использовать правильный порядок входов.
Обратите внимание, что есть вспомогательная функция, используемая ismember
, которую вы можете вызвать напрямую:
% ISMEMBC - S must be sorted - Returns logical vector indicating which
% elements of A occur in S
tf = ismembc(x,3);
inds = find(tf);
Использование ismembc
будет сохранять время вычисления, так как ismember
вызывает issorted
, но это пропустит проверку.
Обратите внимание, что более новые версии matlab имеют встроенный вызов под именем builtin('_ismemberoneoutput',a,b)
с той же функциональностью.
Так как вышеупомянутые приложения ismember
и т.д. несколько назад (поиск второго элемента x
во втором аргументе, а не наоборот), код намного медленнее, чем необходимо. Как указывает OP, очень жаль, что [~,loc]=ismember(3,x)
обеспечивает только расположение первого вхождения 3 в x
, а не всех. Однако, если у вас есть последняя версия MATLAB (R2012b +, я думаю), вы можете использовать еще недокументированные встроенные функции, чтобы получить первые последние индексы! Это ismembc2
и builtin('_ismemberfirst',searchfor,x)
:
firstInd = builtin('_ismemberfirst',searchfor,x); % find first occurrence
lastInd = ismembc2(searchfor,x); % find last occurrence
% lastInd = ismembc2(searchfor,x(firstInd:end))+firstInd-1; % slower
inds = firstInd:lastInd;
Еще медленнее, чем у Daniel R. отличный код MATLAB, но там он (rntmX
добавлен в тест randomatlabuser) просто для удовольствия:
mean([rntm1 rntm2 rntm3 rntmX])
ans =
0.559204323050486 0.263756852283128 0.000017989974213 0.000153682125682
Ниже приведены биты документации для этих функций внутри ismember.m
:
% ISMEMBC2 - S must be sorted - Returns a vector of the locations of
% the elements of A occurring in S. If multiple instances occur,
% the last occurrence is returned
% ISMEMBERFIRST(A,B) - B must be sorted - Returns a vector of the
% locations of the elements of A occurring in B. If multiple
% instances occur, the first occurence is returned.
Фактически существует ссылка на встроенный ISMEMBERLAST
, но он, похоже, не существует (пока?).
Ответ 4
Это не ответ. Я просто сравниваю время работы трех решений, предложенных chappjc и Daniel R.
N = 5e7; % length of vector
p = 0.99; % probability
KK = 100; % number of instances
rntm1 = zeros(KK, 1); % runtime with ismember
rntm2 = zeros(KK, 1); % runtime with ismembc
rntm3 = zeros(KK, 1); % runtime with Daniel function
for kk = 1:KK
x = cumsum(rand(N, 1) > p);
searchfor = x(ceil(4*N/5));
tic
[tf,loc]=ismember(x, searchfor);
inds1 = find(tf);
rntm1(kk) = toc;
tic
tf = ismembc(x, searchfor);
inds2 = find(tf);
rntm2(kk) = toc;
tic
a=1;
b=numel(x);
c=1;
d=numel(x);
while (a+1<b||c+1<d)
lw=(floor((a+b)/2));
if (x(lw)<searchfor)
a=lw;
else
b=lw;
end
lw=(floor((c+d)/2));
if (x(lw)<=searchfor)
c=lw;
else
d=lw;
end
end
inds3 = (b:c)';
rntm3(kk) = toc;
end
Двойной поиск Daniel очень быстрый.
% Mean of running time
mean([rntm1 rntm2 rntm3])
% 0.631132275892504 0.295233981447746 0.000400786666188
% Percentiles of running time
prctile([rntm1 rntm2 rntm3], [0 25 50 75 100])
% 0.410663611685559 0.175298784336465 0.000012828868032
% 0.429120717937665 0.185935198821797 0.000014539383770
% 0.582281366154709 0.268931132925888 0.000019243302048
% 0.775917520641649 0.385297304740352 0.000026940622867
% 1.063753914942895 0.592429428396956 0.037773746662356
Ответ 5
Мне нужна была такая функция. Спасибо за сообщение @Дэниел!
Я немного поработал с этим, потому что мне нужно было найти несколько индексов в одном массиве. Я хотел избежать издержек arrayfun
(или подобного) или вызова функции несколько раз. Таким образом, вы можете передать несколько значений в range
и вы получите индексы в массиве.
function idx = findInSorted(x,range)
% Author Dídac Rodríguez Arbonès (May 2018)
% Based on Daniel Roeske solution:
% Daniel Roeske <danielroeske.de>
% https://github.com/danielroeske/danielsmatlabtools/blob/master/matlab/data/findinsorted.m
range = sort(range);
idx = nan(size(range));
for i=1:numel(range)
idx(i) = aux(x, range(i));
end
end
function b = aux(x, lim)
a=1;
b=numel(x);
if lim<=x(1)
b=a;
end
if lim>=x(end)
a=b;
end
while (a+1<b)
lw=(floor((a+b)/2));
if (x(lw)<lim)
a=lw;
else
b=lw;
end
end
end
Я думаю, вы можете использовать вместо этого parfor
или arrayfun
. Я не проверял себя на том, какой размер range
это окупается, все же.
Другим возможным улучшением будет использование предыдущих найденных индексов (если range
отсортирован), чтобы уменьшить пространство поиска. Я скептически отношусь к его возможности сэкономить процессор из-за времени выполнения O(log n)
.
Последняя функция закончилась чуть быстрее. Для этого я использовал фреймворк @randomatlabuser:
N = 5e6; % length of vector
p = 0.99; % probability
KK = 100; % number of instances
rntm1 = zeros(KK, 1); % runtime with ismember
rntm2 = zeros(KK, 1); % runtime with ismembc
rntm3 = zeros(KK, 1); % runtime with Daniel function
for kk = 1:KK
x = cumsum(rand(N, 1) > p);
searchfor = x(ceil(4*N/5));
tic
range = sort(searchfor);
idx = nan(size(range));
for i=1:numel(range)
idx(i) = aux(x, range(i));
end
rntm1(kk) = toc;
tic
a=1;
b=numel(x);
c=1;
d=numel(x);
while (a+1<b||c+1<d)
lw=(floor((a+b)/2));
if (x(lw)<searchfor)
a=lw;
else
b=lw;
end
lw=(floor((c+d)/2));
if (x(lw)<=searchfor)
c=lw;
else
d=lw;
end
end
inds3 = (b:c)';
rntm2(kk) = toc;
end
%%
function b = aux(x, lim)
a=1;
b=numel(x);
if lim<=x(1)
b=a;
end
if lim>=x(end)
a=b;
end
while (a+1<b)
lw=(floor((a+b)/2));
if (x(lw)<lim)
a=lw;
else
b=lw;
end
end
end
Это не большое улучшение, но оно помогает, потому что мне нужно выполнить несколько тысяч поисков.
% Mean of running time
mean([rntm1 rntm2])
% 9.9624e-05 5.6303e-05
% Percentiles of running time
prctile([rntm1 rntm2], [0 25 50 75 100])
% 3.0435e-05 1.0524e-05
% 3.4133e-05 1.2231e-05
% 3.7262e-05 1.3369e-05
% 3.9111e-05 1.4507e-05
% 0.0027426 0.0020301
Я надеюсь, что это может кому-то помочь.
РЕДАКТИРОВАТЬ
Если существует значительный шанс получить точное совпадение, то перед вызовом функции ismember
использовать очень быстрый встроенный ismember
:
[found, idx] = ismember(range, x);
idx(~found) = arrayfun(@(r) aux(x, r), range(~found));