Полное внешнее соединение в памяти для списков - PullRequest
4 голосов
/ 06 марта 2020

Как мне go записать SQL -подобную операцию полного объединения для списков с подписью ниже?

fullJoin :: [a] -> [b] -> (a -> b -> Bool) -> [(Maybe a, Maybe b)]
fullJoin xs [] _ = map (\x -> (Just x, Nothing)) xs
fullJoin [] ys _ = map (\y -> (Nothing, Just y)) ys
fullJoin xs@(xh:xt) ys@(yh:yt) on = 
  if on xh yh
    then (Just xh, Just yh) : fullJoin xt yt
    else ???

Условие в том виде, в котором оно написано, предоставляется a -> b -> Bool. здесь, в примере, он установлен как (\n i -> length n == i), что означает записи из names, длина которых равна числам в nums.

Пример:

names = ["Foo","Bar", "John" , "Emily", "Connor"]
nums = [1,2,3,4,5]

fullJoin names nums (\n i -> length n == i)
  == [ (Just "Foo", Just 3)
     , (Just "Bar", Just 3)
     , (Just "John", Just 4)
     , (Just "Emily", Just 5)
     , (Just "Connor", Nothing)
     , (Nothing, Just 1)
     , (Nothing, Just 2)
     ]

Чтобы уточнить точный SQL эквивалент указанной функции, вот как это будет записано в PostgreSQL:

create table names as
select * from (values 
               ('Foo'),
               ('Bar'),
               ('John'),
               ('Emily'),
               ('Connor')
              ) 
              as z(name);

create table nums as
select * from (values 
               (1),
               (2),
               (3),
               (4),
               (5)
              ) 
              as z(num);

select * from names
full join nums
on char_length(name) = num

И выполнение этого приведет к:

name    num
(null)  1
(null)  2
Foo     3
Bar     3
John    4
Emily   5
Connor  (null)

Ссылка Fiddle: sqlfiddle

Ответы [ 3 ]

2 голосов
/ 06 марта 2020

Полное внешнее соединение между таблицами X и Y при условии rel(x, y) может рассматриваться как объединение трех (непересекающихся) частей:

  • Набор пар (x, y) где rel(x,y)
  • Набор пар (x0, null), где для всех y in Y, not (rel(x0, y))
  • Набор пар (null, y0), где для всех x in X, not (rel(x, y0))

Мы можем структурировать нашу Haskell программу аналогичным образом:

fullJoin :: [a] -> [b] -> (a -> b -> Bool) -> [(Maybe a, Maybe b)]
fullJoin xs ys rel = concat $
  [[(Just x, Just y) | y <- ys, x `rel` y] | x <- xs] -- for each x, find all the related ys
  <> [[(Just x, Nothing)] | x <- xs, all (\y -> not (x `rel` y)) ys] -- find all xs that have no related ys
  <> [[(Nothing, Just y)] | y <- ys, all (\x -> not (x `rel` y)) xs] -- find all ys that have no related xs

При постановке проблемы вы не можете получить ее быстрее, чем O(n^2). Тем не менее, мое решение, хотя оно и составляет O(n^2), оно не оптимально: оно пересекает xs дважды. Вы можете подумать о том, что вам нужно сделать, чтобы пройти только 1028 раз. Это связано с поиском способа отслеживать, какой xs вы уже видели.

2 голосов
/ 06 марта 2020

Просто тупо реализуй то, что ты хочешь, чтобы он делал. Супер неэффективно:

fullJoin :: [a] -> [b] -> (a -> b -> Bool) 
         -> [(Maybe a, Maybe b)]
fullJoin xs ys p = concatMap (map (Just *** Just) . snd) a
                    ++ [(Just x, Nothing) | (x, []) <- a]
                    ++ [(Nothing, Just y) | (y, []) <- b]
  where
    a = [(x, [(x, y) | y <- ys, p x y]) | x <- xs]
    b = [(y, [(x, y) | x <- xs, p x y]) | y <- ys]

Если бы нам было разрешено добавить ограничения Eq для типов a и b, мы могли бы использовать Data.List.\\, чтобы найти несопоставленные элементы вместо создания второго развертка.

Тестирование:

> mapM_ print $ fullJoin names nums (\n i -> length n == i)
(Just "Foo",Just 3)
(Just "Bar",Just 3)
(Just "John",Just 4)
(Just "Emily",Just 5)
(Just "Connor",Nothing)
(Nothing,Just 1)
(Nothing,Just 2)
1 голос
/ 06 марта 2020

Вот наивная Python реализация. Это O(n^2).

#! /usr/bin/env python

def full_join_cond (list1, list2, condition_fn):
    answer = []
    for x in list1:
        matches = []
        for y in list2:
            if condition_fn(x, y):
                matches.append(y)
        if len(matches):
            answer.extend([[x, y] for y in matches])
        else:
            answer.append([x, None])

    for y in list2:
        matched = False
        for x in list1:
            if condition_fn(x, y):
                matched = True
                break
        if not matched:
            answer.append([None, y])

    return answer


names = ["Foo","Bar", "John" , "Emily", "Connor"]
nums = [1,2,3,4,5]
cond = lambda x, y: len(x) == y

print(full_join_cond(names, nums, cond))

А вот реализация, которая более точно соответствует тому, как база данных будет выполнять это объединение. Это O(size_of_output), что часто O(n).

def list_map_to_dict (l, m):
    d = {}
    for x in l:
        mx = m(x)
        if mx in d:
            d[mx].append(x)
        else:
            d[mx] = [x]
    return d

def full_join_map (list1, map1, list2, map2):
    dict1 = list_map_to_dict(list1, map1)
    dict2 = list_map_to_dict(list2, map2)

    answer = []
    for mx, xs in dict1.iteritems():
        if mx in dict2:
            for y in dict2[mx]:
                answer.extend([[x, y] for x in xs])
            dict2.pop(mx)
        else:
            answer.extend([[x, None] for x in xs])

    for my, ys in dict2.iteritems():
        answer.extend([[None, y] for y in ys])
    return answer

print(full_join_map(names, lambda x: len(x), nums, lambda y: y))

. И для хихиканья и усмешки оба могут быть объединены в функцию, которая является общей и может быстро выполняться. (Я не пытался сделать подпись функции разумной - просто пытаюсь показать идею.)

def full_join (list1, map1, list2, map2, cond=None):
    if map1 is None:
        map1 = lambda x: None

    if map2 is None:
        map2 = lambda y: None

    if cond is None:
        cond = lambda x, y: True

    dict1 = list_map_to_dict(list1, map1)
    dict2 = list_map_to_dict(list2, map2)

    answer = []
    for mx, xs in dict1.iteritems():
        if mx in dict2:
            answer.extend(full_join_cond(xs, dict2[mx], cond))
            dict2.pop(mx)
        else:
            answer.extend([[x, None] for x in xs])

    for my, ys in dict2.iteritems():
        answer.extend([[None, y] for y in ys])
    return answer
...