在线性时间中查找两个排序列表中的公共元素

时间:2009-09-25 20:00:49

标签: algorithm language-agnostic f#

我有一个已排序的输入列表:

let x = [2; 4; 6; 8; 8; 10; 12]
let y = [-8; -7; 2; 2; 3; 4; 4; 8; 8; 8;]

我想编写一个与SQL INNER JOIN类似的函数。换句话说,我想返回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 个答案:

答案 0 :(得分:8)

我无法帮助您使用F#,但基本思路是使用两个索引,每个列表一个。在该列表的当前索引处选择每个列表中的项目。如果这两个项的值相同,则将该值添加到结果集中并递增两个索引。如果项目具有不同的值,则仅增加包含两个值中较小值的列表的索引。重复比较,直到其中一个列表为空,然后返回结果集。

答案 1 :(得分:8)

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));;

(编辑:我太晚了)

因此,在最坏的情况下,O(n m)中的代码是最好的。但是,IIUIC它执行总是 n * m次操作,这不是最佳的。

我的方法是

1)写一个函数

组:'列表 - &gt; ('a * int)列表

计算相同元素的数量:

组[1,1,1,1,1,2,2,3] == [(1,5);(2,2);(3,1)]

2)使用它来使用与之前类似的代码合并两个列表(可以将这些系数相乘)

3)写一个函数

取消组合:('a * int)列表 - &gt; '列表

并撰写这三个。

这具有复杂度O(n + m + x),其中x是结果列表的长度。这是最好的常数。

编辑:你走了:

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]

答案 2 :(得分:2)

以下也是尾递归(据我所知),但输出列表因此反转:

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),仅仅因为两个重复相同元素列表的边缘情况需要在{1}中创建x.length * y.length值。输出列表,它本身就是一个O(m*n)操作。

答案 3 :(得分:2)

我不知道F#,但我认为它有阵列和数组的二进制搜索实现(也可以实现)

  1. 选择最小的列表
  2. 将其复制到数组(对于O(1)随机访问,如果F#已经为您提供,则可以跳过此步骤)
  3. 查看大名单并使用二进制搜索查找大列表中的小数组元素
  4. 如果找到则将其添加到结果列表
  5. 复杂度O(min + max * log min),其中min = sizeof small list和max - sizeof(big list)

答案 4 :(得分:1)

我不知道F#,但我可以根据tvanfosson概述的算法提供功能性的Haskell实现(由Lasse V. Karlsen进一步指定)。

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,一些文档的引用:

答案 5 :(得分:1)

我认为可以通过使用哈希表来完成。哈希表存储每个列表中元素的频率。然后将这些用于创建一个列表,其中每个元素e的频率是X中e的频率乘以Y中的e的频率。这具有O(n + m)的复杂度。

(编辑:刚看到这可能是最糟糕的情况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)

答案 6 :(得分:0)

他想要的问题是它显然必须重新遍历列表。

为了让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]);
    }
}

答案 7 :(得分:0)

我认为这是交叉/连接代码的O(n),尽管完整的东西遍历每个列表两次:

// 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]

编辑:我刚刚意识到这与sdcvvc的早期答案(编辑后)相同。

答案 8 :(得分:0)

你不能得到O(min(x.length,y.length)),因为输出可能大于那个。例如,假设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)
  }
}