Найти общие элементы в двух отсортированных списках в линейное время

у меня есть отсортированный список входов:

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#, однако я полагаю, что у него есть массивы и реализация двоичного поиска по массивам (также может быть реализована)

  1. выбрать наименьший список
  2. скопируйте его в массив (для O (1) random access, если F# уже дает вам это, вы можете пропустить этот шаг)
  3. перейти через большой список и с помощью двоичного поиска найти в малых элементов массива из большого списка,
  4. Если найдено, добавьте его в список результатов

сложность 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)
  }
}