Estou interessado em escrever uma função Haskell eficiente triangularize :: [a] -> [[a]]
que pega uma lista (talvez infinita) e a "triangulariza" em uma lista de listas. Por exemplo, triangularize [1..19]
deve retornar
[[1, 3, 6, 10, 15]
,[2, 5, 9, 14]
,[4, 8, 13, 19]
,[7, 12, 18]
,[11, 17]
,[16]]
Por eficiente, quero dizer que quero que ele seja executado no O(n)
tempo em que n
está o comprimento da lista.
Observe que isso é bastante fácil de fazer em uma linguagem como Python, porque anexar ao final de uma lista (matriz) é uma operação de tempo constante. Uma função Python muito imperativa que realiza isso é:
def triangularize(elements):
row_index = 0
column_index = 0
diagonal_array = []
for a in elements:
if row_index == len(diagonal_array):
diagonal_array.append([a])
else:
diagonal_array[row_index].append(a)
if row_index == 0:
(row_index, column_index) = (column_index + 1, 0)
else:
row_index -= 1
column_index += 1
return diagonal_array
Isso ocorreu porque eu tenho usado Haskell para escrever algumas seqüências "tabl" na Enciclopédia On-line de Sequências Inteiras (OEIS) e quero poder transformar uma sequência comum (unidimensional) em uma (2- dimensional) sequência de seqüências exatamente dessa maneira.
Talvez exista uma maneira inteligente (ou não tão inteligente) de foldr
ultrapassar a lista de entradas, mas não consegui resolver isso.
fonte
foldr
você pode gostarunfoldr (Just . combWith comb)
para listas infinitas. Infelizmente, como eu mencionei em minha resposta,combWith
é O (n), portanto, a resposta aceitasplitAt
é significativamente mais eficiente.Respostas:
Faça pedaços de tamanho crescente:
Transponha apenas duas vezes:
Experimente em ghci:
fonte
transpose
como O (n). Também não estou super confiante de que não - sua implementação é meio complicada!take 3 . map (take 3) . diagonalize $ [1..]
dá[[1,3,6],[2,5,9],[4,8,13]]
, o que parece bem.take 10 $ map (take 10) $ diagonalize [1..]
de fato, fornece os dez primeiros elementos das dez primeiras linhas.Isso parece estar diretamente relacionado ao argumento da teoria dos conjuntos, provando que o conjunto de pares inteiros está em correspondência um-para-um com o conjunto de números inteiros ( denumerável ). O argumento envolve a chamada função de emparelhamento Cantor .
Então, por curiosidade, vamos ver se conseguimos uma
diagonalize
função dessa maneira. Defina a lista infinita de pares Cantor recursivamente em Haskell:E tente isso dentro do ghci:
Podemos numerar os pares e, por exemplo, extrair os números dos pares que possuem uma coordenada zero x:
Reconhecemos que esta é a linha superior do resultado do OP no texto da pergunta. Da mesma forma, para as próximas duas linhas:
A partir daí, podemos escrever nosso primeiro rascunho de uma
diagonalize
função:EDIT: atualização de desempenho
Para uma lista de 1 milhão de itens, o tempo de execução é de 18 segundos e 145 segundos para 4 milhões de itens. Como mencionado por Redu, isso parece com a complexidade de O (n√n).
Distribuir os pares entre as várias sublistas de destino é ineficiente, pois a maioria das operações de filtro falha.
Para melhorar o desempenho, podemos usar uma estrutura Data.Map para as sublistas de destino.
Com essa segunda versão, o desempenho parece ser muito melhor: 568 ms para a lista de 1 milhão de itens, 2669 ms para a lista de 4 milhões de itens. Portanto, é próximo à complexidade O (n * Log (n)) que poderíamos esperar.
fonte
Pode ser uma boa ideia criar um
comb
filtro.Então, o que o
comb
filtro faz ..? É comosplitAt
mas em vez de divisão em um único índice que tipo de zips a determinada lista infinita com o pente dado para separar os itens coressponding paraTrue
eFalse
no pente. De tal modo que;Agora, tudo o que precisamos fazer é pentear nossa lista infinita, pegar
fst
a primeira linha e continuar penteandosnd
a mesmacomb
.Vamos fazer isso;
também parece ser preguiçoso também :)
Eu acho que a complexidade pode ser como O (n√n), mas não posso ter certeza. Alguma ideia..?
fonte
:set +s
. Fazendo isso A resposta aceita de @Daniel Wagner parece estar correndo muito rápido com o tipo de lista. Você poderia verificar como ele se compara aos seus? Eu esperava alcançar um desempenho semelhante, mascombWith
não é tão rápido quantospilitAt
.transpose
parecem infundadas. Além disso, parece mais preguiça do que o mapa de Cantor. Bem feito !snd
dosplitAt
's valor de retorno fica obtidos em O (1), mas ofst
é ainda deve ser O (n). De alguma forma, isso se reflete no desempenho geral como O (nlogn).splitAt
vez de ligardrop
etake
separadamente.