중첩 리스트 풀기

오픈마루에서 대학생 인턴 구인 면접에서 나온 문제라고 합니다.

Quote:

리스트는 유한개의 원소가 정해진 순서로 들어가 있는 자료구조입니다. 리스트에 포함된 원소는 하나의 숫자일수도 있지만 또다른 리스트일수도 있습니다. 이렇게 리스트 속에 리스트가 들어 있는 것을 중첩 리스트(nested list)라고 합시다.

리스트를 격자괄호로 둘러쌓고, 각 원소는 쉼표로 구분해 표기하도록 하죠. 다음은 중첩 리스트의 예입니다.

[1, 2, [3, 4, [5]], 6, [[7,8]]]

예를 들어, 이런 리스트가 주어져 있을 때, 그걸 평평하게 만들어주는 프로그램 F를 작성해야 합니다.

즉, 결과가 [1, 2, 3, 4, 5, 6, 7, 8]로 나오면 됩니다.

면접의 실제 문제는 이걸 구현하는 것이 아니라 이 프로그램이 정상적으로 작동한다는 것을 보증하기 위해 무엇을 하겠느냐입니다. 그냥 보증하는 것도 아니고 목숨을 걸고 보증한다면 말이죠. 자세한 내용은 위의 링크를 참조하세요.

이 문제의 경우 여러가지 해법이 있을텐데요 일단 쉽게 푸는 걸 최우선 과제로 합시다. 그래야 틀리지도 않을테니까요. 그러니까 중첩리스트라고 생각하지도 말고 그냥 문자열에서 숫자만 순서대로 꺼내는 것이라고 생각하면 되겠지요. 하스켈로 한 번 해볼까요. 일단 문자열에서 숫자를 제외하면 모두 스페이스로 바꿉니다. 그러면 원래의 중첩리스트는 빈칸으로 구분된 숫자만 남습니다.

> map (\x -> if elem x "0123456789" then x else ' ') "[1, 2, [3, 4, [5]], 6, [[7,8]]]"
" 1  2   3  4   5    6    7 8   "

그 다음은 간단하죠. 스페이스로 끊어서 읽고

> words it
["1","2","3","4","5","6","7","8"]

수로 읽어주면 됩니다.

> map read it :: [Integer]
[1,2,3,4,5,6,7,8]

합쳐서 하나의 함수로 만들면 간단하죠?

flatten lst = map read $ words $ map (\x -> if elem x "0123456789" then x else ' ') lst :: [Integer]

이번에는 중복리스트를 중복리스트로 보고 합쳐봅시다. 코드는 스킴으로 짰습니다. 설명은 주석으로 대체하겠습니다.

(define (flatten nested)
  (define (flat unnest nest)                 ; nest를 풀어서 unnest의 뒤에 붙임
    (if
      (null? nest)                           ; 빈 리스트이면
      unnest                                 ; unnest 반환
      (let* 
        (
          (x (car nest))
          (y (if 
            (number? x)                      ; 첫 원소가 수이면
            (append unnest (list x))         ; unnest에 추가
            (flat unnest x))))               ;수가 아니면 풀어서 추가
        (flat y (cdr nest)))))               ; 나머지 원소를 풀어서 붙임
  (flat '() nested))                         ; 빈 리스트에 풀어 붙임

(flatten '(1 2 (3 4 (5)) 6 ((7 8))))

역시 목숨이 걸렸다면 그냥 문자열로 보는 쪽이 낫겠습니다. 여러분이라면 어떻게 푸시겠습니까?

댓글 보기 옵션

원하시는 댓글 전시 방법을 선택한 다음 "설정 저장"을 누르셔서 적용하십시오.

스킴 구현은...

저라면 꼬리 재귀(tail recursion)로 하겠어요. 아, 우선은 가장 간단한 재귀로 하고 수학적 증명을 한 다음, pre/post-condition등을 확인해 가면서 규칙적으로 "변형"해 나가겠어요.

꼬리 재귀 버전

역시 하스켈을 이용합니다. 일단 스킴과 달리 하스켈은 중첩 리스트를 언어 자체에서 지원하지 않습니다. 왜냐하면 리스트의 원소는 모두 똑같은 자료형이어야 하는 데 수와 리스트는 자료형이 서로 다르기 때문이지요. 그래서 일단 자료형부터 만듭니다.

data Nested a = Item a | Nest [Nested a] deriving (Eq, Show)

수는 Item, 리스트는 Nest 생성자를 붙여서 똑같이 Nested 형으로 만들어줍니다. 이제 예를 들어 [1,[2,3]]은 Nest [Item 1, Nest [Item 2, Item 3]]이 됩니다. 입력하기가 번거로우면 에디터에서 찾아바꾸기로 [는 모두 Nest [로 바꿔주고 수 앞에 Item을 붙여주세요.

이제 꼬리 재귀 버전으로 만들어보겠습니다. 맨 앞에서부터 수를 하나씩 꺼내고 나머지는 꼬리 재귀를 하면 됩니다.

리스트의 가능한 조합은 빈 리스트, 수:리스트와 리스트:리스트 세 가지 입니다. 이 중에 리스트:리스트는 다시 빈 리스트:리스트와 비지 않은 리스트:리스트 두 가지가 있습니다. 따라서 모두 네 종류의 조합이 있고 각각에 대해 패턴 매칭을 해주면 됩니다. 먼저 빈 리스트일 경우에는 간단합니다.

flatten (Nest []) = []

그 다음으로 수:리스트인 경우도 간단합니다.

flatten (Nest ((Item x):xs)) = x:flatten (Nest xs)

빈 리스트 : 리스트인 경우에는 앞에 빈 리스트를 무시하면 되겠습니다.

flatten (Nest (Nest []:xs)) = flatten $ Nest xs

끝으로 비지 않은 리스트:리스트인 경우는 수:리스트 꼴로 바꿔줍니다. 예를 들어 [[1,2],3] 이런 경우에는 [1,[2],3]으로 바꿉니다. 그러면 수:리스트가 됩니다.

flatten (Nest ((Nest (x:xs)):xss)) = flatten $ Nest (x:Nest xs:xss)

문제에 있는 코드로 테스트를 해보겠습니다.

> flatten (Nest [Item 1, Item 2, Nest [Item 3, Item 4, Nest [Item 5]], Item 6, Nest [Nest [Item 7,Item 8]]])
[1,2,3,4,5,6,7,8]

하스켈은 기본적으로 지연평가(lazy evaluation)을 하기 때문에 x:xs라고 되어 있으면 x만 처리하고 xs는 처리하지 않습니다. 그래서 x:flatten (Nest xs)에서 x:를 평가하고 그 다음에 flatten (Nest xs)를 평가하여 꼬리 재귀가 됩니다.

스킴으로 똑같이 짜면 아래와 같습니다.

(define (flatten lst)
  (cond
    ((null? lst) lst)
    ((not (list? (car lst))) (cons (car lst) (flatten (cdr lst))))
    ((null? (car lst)) (flatten (cdr lst)))
    (else (flatten (cons (caar lst) (cons (cdar lst) (cdr lst)))))))

스킴은 즉시 평가(eager evaluation)을 하기 때문에 (cons (car lst) (flatten (cdr lst)))를 하려면 flatten이 cons보다 먼저 실행되서 꼬리 재귀가 안됩니다. 스킴에서 꼬리 재귀가 되게 하려면 delay와 force를 이용하여 지연 평가를 하도록 코드를 수정해야 합니다.

Erlang, Tail-Recursive

Erlang 배우기 시작할 때 짰던 코드입니다. Erlang은 꼬리 재귀 최적화가 지원되는 언어입니다.

-module(flat).
-export([flat3/3,flatten/1]).

flat3([]   ,[]  ,Done) -> Done;
flat3([]   ,Todo,Done) -> flat3(Todo,[],Done);
flat3([H|T],Todo,Done) when list(H) -> flat3(H,T++Todo,Done);
flat3([H|T],Todo,Done) -> flat3(T,Todo,Done++[H]).

flatten(L) -> flat3(L,[],[]).

더 간단한 방법?

더 간단합니다만 더 효율적이진 않습니다.

data Nested a = Item a | Nest [Nested a] deriving (Show, Eq)

flatten (Item x) = [x]
flatten (Nest xs) = concatMap flatten xs

이렇게 할수도 있겠죠...

flatList (Item a) = [a] -- 이 경우 최초 입력값이 리스트가 아닌 원소일 경우에도 이렇게 처리하겠다는 약속이 필요하겠죠...
flatList (Nest []) = []
flatList (Nest (x:xs)) = (flatList x) ++ (flatList $ Nest xs)

Ocaml version~

알고리즘은 똑같은 같아요. tail-recursion 은 아니구요.

type nested = Item of int| Nest of nested list

let temp = [Item 1; Item 2; Nest [Item 3; Item 4; Nest [Item 5]]; Item 6; Nest[Nest[Item 7;Item 8]]]

(* test : nested list -> int list *)
let rec flatten nl =
  match nl with
      [] -> []
    | x::xs -> (
          match x with
              Item y -> [y]@(flatten xs)
            | Nest ys -> (flatten ys)@(flatten xs))

결과

# flatten temp;;
- : int list = [1; 2; 3; 4; 5; 6; 7; 8]

I wanna know you~!

코드가 깨져서

코드가 깨져서 태그를 붙여 편집했습니다. ^^

아 그랬군요~

아 그랬군요~ 감사합니다~

근데 왜 덧글이 위로 가는지 모르겠어요...-_-;

맨 아래 있어야 할꺼같은데...
---
I wanna know you~!

설정 바꿨습니다

기본 설정을 시간순으로 바꿨습니다. 그리고 댓글 목록 맨 위에 자신이 원하는 대로 설정을 할 수 있도록 메뉴를 추가했습니다.

아 다시 한번

아 다시 한번 감사합니다~ ^^

---
I wanna know you~!

우연찮게 발견한

우연찮게 발견한 사이트가 너무 예쁘네요~ 가입 기념으로 하나 풀어봤습니다.
해답은 아래 링크합니다.

http://blueiur.tistory.com/entry/중첩-리스트-풀기-오픈마루-대학생-인턴-구인면접

scheme 한 줄 더 줄여봤습니다.

(define (flat x)
  (cond ((null? x) '())
        ((not (pair? x)) (list x))        
        (else (append (flat (car x)) (flat (cdr x))))))

음.

p [1, 2, [3, 4, [5]], 6, [[7,8]]].flatten