Найти общие элементы в двух отсортированных списках в линейное время
у меня есть отсортированный список входов:
let x = [2; 4; 6; 8; 8; 10; 12]
let y = [-8; -7; 2; 2; 3; 4; 4; 8; 8; 8;]
Я хочу написать функцию, которая ведет себя подобно внутреннему соединению SQL. Другими словами, Я хочу вернуть декартовое произведение x и y, которое содержит только элементы, общие в обоих списках:
join(x, y) = [2; 2; 4; 4; 8; 8; 8; 8; 8; 8]
я написал наивную версию следующим образом:
let join x y =
[for x' in x do
for y' in y do
yield (x', y')]
|> List.choose (fun (x, y) -> if x = y then Some x else None)
это работает, но это работает в O(x.length * y.length)
. Поскольку оба моих списка отсортированы, я думаю, что можно получить результаты, которые я хочу O(min(x.length, y.length))
.
как я могу найти общие элементы в двух отсортированных списков в линейном времени?
9 ответов
O(min (n,m)) время невозможно: возьмите два списка [x;x;...;x; y] и [x; x;...; x; z]. Вы должны просмотреть оба списка до конца, чтобы сравнить y и z.
даже O (n+m) невозможно. Брать [1,1,...,1] - n раз и [1,1,...,1] - m раз Тогда результирующий список должен иметь n * m элементов. Вам нужно хотя бы O(n m) (правильно Omega(n m)) время, чтобы создать такой список.
без декартового произведения (простое слияние) это довольно легко. Код Ocaml (я не знаю F#, должен быть достаточно близко; скомпилировано, но не протестировано):
let rec merge a b = match (a,b) with
([], xs) -> xs
| (xs, []) -> xs
| (x::xs, y::ys) -> if x <= y then x::(merge xs (y::ys))
else y::(merge (x::xs) (y::ys));;
(Edit: я опоздал)
таким образом, ваш код в O(n m) является наилучшим в худшем случае. Тем не менее, IIUIC он выполняет всегда Н*M операций, который не является оптимальным.
мой подход будет
1) написать функцию
группа: 'A list - > ('a * int) list
это подсчитывает количество одинаковых элементов:
группа [1,1,1,1,1,2,2,3] == [(1,5);(2,2);(3,1)]
2) Используйте его для объединения обоих списков, используя аналогичный код, как и раньше (там вы можете умножить эти коэффициенты)
3) написать функцию
ungroup : ('a * int) list - >' a list
и составьте эти три.
это имеет сложность O (n+m+x), где x-длина результирующего списка. Это самое лучшее возможное до константы.
Edit: вот вы идете:
let group x =
let rec group2 l m =
match l with
| [] -> []
| a1::a2::r when a1 == a2 -> group2 (a2::r) (m+1)
| x::r -> (x, m+1)::(group2 r 0)
in group2 x 0;;
let rec merge a b = match (a,b) with
([], xs) -> []
| (xs, []) -> []
| ((x, xm)::xs, (y, ym)::ys) -> if x == y then (x, xm*ym)::(merge xs ys)
else if x < y then merge xs ((y, ym)::ys)
else merge ((x, xm)::xs) ys;;
let rec ungroup a =
match a with
[] -> []
| (x, 0)::l -> ungroup l
| (x, m)::l -> x::(ungroup ((x,m-1)::l));;
let crossjoin x y = ungroup (merge (group x) (group y));;
# crossjoin [2; 4; 6; 8; 8; 10; 12] [-7; -8; 2; 2; 3; 4; 4; 8; 8; 8;];;
- : int list = [2; 2; 4; 4; 8; 8; 8; 8; 8; 8]
Я не могу помочь вам с F#, но основная идея заключается в использовании двух индексов, по одному для каждого списка. Выберите элемент в каждом списке по текущему индексу для этого списка. Если два элемента имеют одинаковое значение, добавьте это значение в результирующий набор и увеличьте оба индекса. Если элементы имеют разные значения, увеличьте только индекс для списка, содержащего меньшее из двух значений. Повторите сравнение до тех пор, пока один из ваших списков не опустеет, а затем верните результирующий набор.
следующее также является хвост-рекурсивным (насколько я могу судить), но список вывода, следовательно, отменен:
let rec merge xs ys acc =
match (xs, ys) with
| ((x :: xt), (y :: yt)) ->
if x = y then
let rec count_and_remove_leading zs acc =
match zs with
| z :: zt when z = x -> count_and_remove_leading zt (acc + 1)
| _ -> (acc, zs)
let rec replicate_and_prepend zs n =
if n = 0 then
zs
else
replicate_and_prepend (x :: zs) (n - 1)
let xn, xt = count_and_remove_leading xs 0
let yn, yt = count_and_remove_leading ys 0
merge xt yt (replicate_and_prepend acc (xn * yn))
else if x < y then
merge xt ys acc
else
merge xs yt acc
| _ -> acc
let xs = [2; 4; 6; 8; 8; 10; 12]
let ys = [-7; -8; 2; 2; 3; 4; 4; 8; 8; 8;]
printf "%A" (merge xs ys [])
выход:
[8; 8; 8; 8; 8; 8; 4; 4; 2; 2]
обратите внимание, что, как говорит sdcvvc в своем ответе, это все еще O(x.length * y.length)
в худшем случае, просто потому, что крайний случай двух списков повторяющихся одинаковых элементов потребует создания x.length * y.length
значения в выходном списке, который сам по себе является O(m*n)
операция.
Я не знаю F#, однако я полагаю, что у него есть массивы и реализация двоичного поиска по массивам (также может быть реализована)
- выбрать наименьший список
- скопируйте его в массив (для O (1) random access, если F# уже дает вам это, вы можете пропустить этот шаг)
- перейти через большой список и с помощью двоичного поиска найти в малых элементов массива из большого списка,
- Если найдено, добавьте его в список результатов
сложность O(min + max * log min), где min = sizeof малый список и max - sizeof(большой список)
Я не знаю F#, но я могу предоставить функциональную реализацию Haskell, основанную на алгоритме, описанном tvanfosson (далее указано Лассе против Карлсена).
import Data.List
join :: (Ord a) => [a] -> [a] -> [a]
join l r = gjoin (group l) (group r)
where
gjoin [] _ = []
gjoin _ [] = []
gjoin l@(lh@(x:_):xs) r@(rh@(y:_):ys)
| x == y = replicate (length lh * length rh) x ++ gjoin xs ys
| x < y = gjoin xs r
| otherwise = gjoin l ys
main :: IO ()
main = print $ join [2, 4, 6, 8, 8, 10, 12] [-7, -8, 2, 2, 3, 4, 4, 8, 8, 8]
это выводит [2,2,4,4,8,8,8,8,8,8]
. Если вы не знакомы с Haskell, некоторые ссылки на документацию:
Я думаю, что это можно сделать просто с помощью хэш-таблиц. Хэш-таблицы хранят частоты элементов в каждом списке. Затем они используются для создания списка, где частота каждого элемента e-частота e в X, умноженная на частоту e в Y. Это имеет сложность O(n+m).
(EDIT: просто заметил, что это может быть худший случай O(n^2), после прочтения комментариев к другим сообщениям. Что-то очень похожее уже было опубликовано. Извините за дубликат. Я сохранение поста на случай, если код поможет.)
Я не знаю F#, поэтому я прикрепляю код Python. Я надеюсь, что код достаточно читаем, чтобы его легко преобразовать в F#.
def join(x,y):
x_count=dict()
y_count=dict()
for elem in x:
x_count[elem]=x_count.get(elem,0)+1
for elem in y:
y_count[elem]=y_count.get(elem,0)+1
answer=[]
for elem in x_count:
if elem in y_count:
answer.extend( [elem]*(x_count[elem]*y_count[elem] ) )
return answer
A=[2, 4, 6, 8, 8, 10, 12]
B=[-8, -7, 2, 2, 3, 4, 4, 8, 8, 8]
print join(A,B)
проблема с тем, что он хочет, заключается в том, что он, очевидно, должен пересечь список.
чтобы получить 8,8,8, чтобы появиться дважды, функция должна немного пройти через второй список. Худший сценарий (два одинаковых списка) все равно даст O (x * y)
как Примечание, это не использует внешние функции, которые петляют сами по себе.
for (int i = 0; i < shorterList.Length; i++)
{
if (shorterList[i] > longerList[longerList.Length - 1])
break;
for (int j = i; j < longerList.Length && longerList[j] <= shorterList[i]; j++)
{
if (shorterList[i] == longerList[j])
retList.Add(shorterList[i]);
}
}
Я думаю, что это O (n) в коде intersect/join, хотя полная вещь пересекает каждый список дважды:
// list unique elements and their multiplicity (also reverses sorting)
// e.g. pack y = [(8, 3); (4, 2); (3, 1); (2, 2); (-8, 1); (-7, 1)]
// we assume xs is ordered
let pack xs = Seq.fold (fun acc x ->
match acc with
| (y,ny) :: tl -> if y=x then (x,ny+1) :: tl else (x,1) :: acc
| [] -> [(x,1)]) [] xs
let unpack px = [ for (x,nx) in px do for i in 1 .. nx do yield x ]
// for lists of (x,nx) and (y,ny), returns list of (x,nx*ny) when x=y
// assumes inputs are sorted descending (from pack function)
// and returns results sorted ascending
let intersect_mult xs ys =
let rec aux rx ry acc =
match (rx,ry) with
| (x,nx)::xtl, (y,ny)::ytl ->
if x = y then aux xtl ytl ((x,nx*ny) :: acc)
elif x < y then aux rx ytl acc
else aux xtl ry acc
| _,_ -> acc
aux xs ys []
let inner_join x y = intersect_mult (pack x) (pack y) |> unpack
теперь мы испытываем его на ваших данных по образца
let x = [2; 4; 6; 8; 8; 10; 12]
let y = [-7; -8; 2; 2; 3; 4; 4; 8; 8; 8;]
> inner_join x y;;
val it : int list = [2; 2; 4; 4; 8; 8; 8; 8; 8; 8]
EDIT: я только что понял, что это та же идея, что и предыдущий ответ sdcvvc (после редактирования).
вы не можете получить O (min (x.длина, y.длина)), потому что выход может быть больше, чем это. Supppose все элементы X и y равны, например. Тогда выходной размер является произведением размера x и y, что дает нижнюю границу эффективности алгоритма.
вот алгоритм в F#. Это не хвост-рекурсивный, который можно легко исправить. Трюк делает взаимная рекурсия. Также обратите внимание, что я могу инвертировать порядок списка, заданного prod
чтобы избежать ненужная работа.
let rec prod xs ys =
match xs with
| [] -> []
| z :: zs -> reps xs ys ys
and reps xs ys zs =
match zs with
| [] -> []
| w :: ws -> if xs.Head = w then w :: reps xs ys ws
else if xs.Head > w then reps xs ys ws
else match ys with
| [] -> []
| y :: yss -> if y < xs.Head then prod ys xs.Tail else prod xs.Tail ys
оригинальный алгоритм в Scala:
def prod(x: List[Int], y: List[Int]): List[Int] = x match {
case Nil => Nil
case z :: zs => reps(x, y, y)
}
def reps(x: List[Int], y: List[Int], z: List[Int]): List[Int] = z match {
case w :: ws if x.head == w => w :: reps(x, y, ws)
case w :: ws if x.head > w => reps(x, y, ws)
case _ => y match {
case Nil => Nil
case y1 :: ys if y1 < x.head => prod(y, x.tail)
case _ => prod(x.tail, y)
}
}