2017년 1월 25일 수요일

스택 뒤집기 (Reverse stack)

스택(stack)이 주어졌을 때, 명시적인 별도의 스택 및 데이터 저장소를 사용하지 않고 스택 내부의 저장 순서를 거꾸로 만들어 보자.
또 다른 스택을 사용할 수 있다면, 원래의 스택에서 pop하여 새로 생성한 스택에 push하면 원하는 결과를 얻을 수 있다. 이 원리를 이용하되 명시적인 데이터 저장소를 사용하지 않는 방법을 고안하면 된다.
답은 간단한데, 함수 호출이 스택을 사용한다는 점에 착안하여 함수의 재귀호출을 사용하여 구현하면 되겠다.


func main() {
    data := []int{1, 2, 3, 4, 5}
    S := stack.Stack{}
    for _, item := range data {
        S.Push(item)
    }
    S.Reverse()
    for !S.IsEmpty() {
        item := S.Top().(int)
        S.Pop()
        fmt.Println(item)
    }

}

func (s *Stack) InsertAtBottom(item interface{}) {
    if s.IsEmpty() {
        s.Push(item)
        return
    }
    t := s.Top()
    s.Pop()
    s.InsertAtBottom(item)
    s.Push(t)
}

func (s *Stack) Reverse() {
    if s.IsEmpty() {
        return
    }
    t := s.Top()
    s.Pop()
    s.Reverse()
    s.InsertAtBottom(t)
}

2017년 1월 23일 월요일

스택 입출력 (Stack input/output)

Go는 generic을 자연스럽게 지원하지는 않아서, 다양한 타입의 데이터를 저장하는 몇 가지 trick을 사용한다. 이 포스트에서 stack을 구현할 때는 C/C++의 void pointer와 유사하게 Go의 interface{} 형태로 데이터를 저장하고, 사용하는 쪽에서 적절한 형변환(assertion)을 수행하는 방식으로 스택을 구현한다.
실제 데이터는 slice에 저장하고, slice operation을 사용하여 스택의 push, pop, top, empty여부 확인 구현할 수 있다.
Stack API의 디자인은 empty시 동작을 어떻게 나타낼 것인가에 답해야 하는데, 이 포스트에서는 C++의 standard template library에서처럼 empty시 stack 연산 수행의 동작을 보장하지 않는 형태로 디자인하였다.

type Stack struct {
    S []interface{}
}

func (s *Stack) Push(item interface{}) {
    s.S = append(s.S, item)
}

func (s *Stack) Top() interface{} {
    return s.S[len(s.S) - 1]
}

func (s *Stack) IsEmpty() bool {
    return len(s.S) == 0
}

func (s *Stack) Pop() {
    s.S = s.S[:len(s.S) - 1]

}

이진 검색 트리 (binary search tree)의 Inorder Successor

이진 검색 트리에서 중위 순회의 순서상 다음 노드를 찾아 보자.
찾으려는 노드는 주어진 노드 key값보다 큰 것중 가장 작은 key 값을 갖는다. 별도의 공간을 사용할 수 있다면, inorder traversal을 수행 하여 정렬된 key 배열을 얻은 후, 주어진 노드의 바로 오른쪽 노드를 찾는다.
아래에서는 별도의 공간을 사용하지 않고 찾는 방법을 구현한다. 이 문제는 주어진 노드가 오른쪽 sub-tree를 갖을 때와 그렇지 않을 때로 구분하여 처리할 수 있다. 오른쪽 sub-tree가 있는 경우에는 오른쪽 sub-tree에 있는 노드 중 가장 작은 값을 선택하면 된다. 오른쪽 sub-tree가 없는 경우에는 root로부터 주어진 노드로 탐색을 하면서 successor 값을 갱신하고, 주어진 노드에 도달했을 때의 successor가 찾는 노드이다. 탐색 시 successor 값은 왼쪽 sub-tree로 이동할 때만 갱신을 한다.


func InorderSuccessor(root, node *Node) *Node {
    if root == nil || node == nil {
        return nil
    }
    if node.Right != nil {
        successor := node.Right
        for successor.Left != nil {
            successor = successor.Left
        }
        return successor
    }
    var successor *Node
    indicator := root
    for indicator != node && indicator != nil {
        switch {
        case node.Val < indicator.Val:
            successor = indicator
            indicator = indicator.Left
        case node.Val > indicator.Val:
            indicator = indicator.Right
        case node.Val == indicator.Val:
            return successor
        }
    }
    return nil
}

오름차순으로 정렬된 배열로 부터 가장 높이가 낮은 이진 검색 트리 생성 (binary search tree)

오른차순으로 정렬된 배열이 주어졌을 때, 이를 가장 높이가 낮은 이진 검색 트리로 변환하는 코드를 작성한다.
정렬된 배열의 임의의 원소를 선택하더라도, 그 왼쪽 sub-array와 오른쪽 sub-array이 모두 정렬된 상태이므로, 선택한 원소를 key로 갖는 노드를 생성하고 노드의 왼쪽 자식과 노드의 오른쪽 자식은 재귀 호출을 사용하여 이진 검색 트리를 생성할 수 있다. 이 작업의 반복을 최소화 하기 위해서는 왼쪽과 오른쪽의 sub-array의 크기가 동일하거나 1만큼의 차이가 나도록 원소를 선택하면 된다.


type NodeVal int

type Node struct {
    Val   NodeVal
    Left  *Node
    Right *Node
}

func CreateShortestBST(sortedData []NodeVal) *Node {
    return CreateShortestBSTUtil(sortedData, 0, len(sortedData) - 1)
}

func CreateShortestBSTUtil(sortedData []NodeVal, from, to int) *Node {
    if to < from {
        return nil
    }
    mid := from + (to - from) / 2
    node := &Node{
        Val: sortedData[mid],
        Left: CreateShortestBSTUtil(sortedData, from, mid - 1),
        Right: CreateShortestBSTUtil(sortedData, mid + 1, to),
    }
    return node
}


2017년 1월 18일 수요일

이진 검색 트리 (binary search tree)의 삽입, 삭제, 검색

이진 검색 트리는 트리의 모든 노드에서 아래의 성질을 만족한다.
  • 노드는 nil이거나, key값을 갖는다
  • 노드의 왼쪽 노드는 nil이거나 노드의 key 보다 작은 key 값을 갖고,
  • 노드의 오른쪽 노드는 nil이거나 노드의 key 보다 큰 key 값을 갖는다
이러한 성질때문에, in-order traversal은 오름차순으로 정렬한 결과를 생성한다.

이진 검색 트리에 key를 삽입(insert)하고 삭제(remove)하고 존재 유무를 검색하는 코드를 작성한다.
삽입은 주어진 key와 노드의 key를 비교하여, 주어진 key가 노드의 key보다 큰 경우 오른쪽 sub-tree에 삽입하고, 값이 작은 경우 왼쪽 sub-tree에 삽입한다.
삭제는 주어진 key를 갖는 노드(target node)를 찾았을 때, 오른쪽 자식 노드가 있는 경우 오른쪽 자식 노드와 그 하위 tree에서 가장 작은 값 (replace node)을 해당 노드(target node)의 새로운 key로 변경하고, 재귀적으로 replace node를 삭제한다. 오른쪽 자식 노드가 없는 경우에는 왼쪽 자식노드로 해당 노드(target node)를 대신한다. 

type NodeVal int

type Node struct {
    Val   NodeVal
    Left  *Node
    Right *Node
}


func AddNode(node *Node, val NodeVal) *Node {
    if node == nil {
        return &Node {
            Val: val,
            Left: nil,
            Right: nil,
        }
    }
    if val < node.Val {
        node.Left = AddNode(node.Left, val)
    } else {
        node.Right = AddNode(node.Right, val)
    }
    return node
}

func FindNode(node *Node, val NodeVal) *Node {
    if node == nil {
        return nil
    }
    switch {
    case val < node.Val:
        return FindNode(node.Left, val)
    case val > node.Val:
        return FindNode(node.Right, val)
    }
    return node
}

func RemoveNode(node *Node, val NodeVal) *Node {
    if node == nil {
        return nil
    }
    switch {
    case val < node.Val:
        node.Left = RemoveNode(node.Left, val)
        return node
    case val > node.Val:
        node.Right = RemoveNode(node.Right, val)
        return node
    }
    if node.Right != nil {
        replaceNode := node.Right
        for replaceNode.Left != nil {
            replaceNode = replaceNode.Left
        }
        node.Val = replaceNode.Val
        node.Right = RemoveNode(node.Right, replaceNode.Val)
        return node
    }
    return node.Left
}

2017년 1월 16일 월요일

k번째로 큰 수 구하기 (kth largest number)

정수 배열에서 k번째로 큰 수를 찾는 방법을 알아 보자 (중복된 데이터는 없다고 가정).

1) 주어진 전체 배열을 정렬한 후 data[n-k]가 원하는 값
  • 데이터 정렬에 O(nlog(n))의 시간 소요하고 data[n-k] 접근에는 O(1)이 필요하므로, 전체적으로 O(nlog(n))의 시간복잡도를 갖음
2) max heap을 사용
  • 데이터 전체를 max-heap으로 구성한 후, max값을 k번 extract한 값이 원하는 값
  • max-heap 구성에 O(n), max값 extraction에 O(klog(n))이 필요하므로, 전체적으로 O(n + klog(n))의 시간복잡도를 갖음
3) min heap을 사용
  • [0, k-1]구간의 데이터를 min-heap으로 구성. [k, n-1]데이터가 min-heap의 최소값보다 큰 경우 min-heap의 최소값을 제거하고 방금 비교한 데이터를 heap에 넣은 후, min-heap을 구성. 결국 heap의 최소값이 원하는 값
  • min-heap 구성에 O(k), 나머지 데이터와의 비교에 O((n-k)log(k))의 시간이 소요되어, 전체적으로 O(k + (n-k)log(k))이 시간복잡도를 갖음

아래 코드는 min heap을 사용하여 k번째 큰 수를 찾는다.

type Comparer func (a, b int) bool

func buildHeap(data []int, size int, compFunc Comparer) {
  for index := (size - 2) / 2; index >= 0; index-- {
    heapify(data, size, index, compFunc)
  }
}

func heapify(data []int, size, index int, compFunc Comparer) {
  left := index*2 + 1
  right := left + 1
  sel := right

  if right < size {
    if compFunc(data[left], data[right]) {
      sel = left
    }
  } else if left < size {
    sel = left
  } else {
    return
  }
  if compFunc(data[sel], data[index]) {
    data[index], data[sel] = data[sel], data[index]
    heapify(data, size, sel, compFunc)
  }
}

func getKthLargestNumber(data []int, kth int) int {
  size := len(data)
  if kth > size {
    panic("can't get an element beyond data range")
  }
  minHeapFunc := func(a, b int) bool {
    return a < b
  }
  buildHeap(data, kth, minHeapFunc)
  for i := kth; i < size; i++ {
    if data[i] > data[0] {
      data[0] = data[i]
      heapify(data, kth, 0, minHeapFunc)
    }
  }
  return data[0]
}

2017년 1월 13일 금요일

힙 정렬 (heap sort)

힙(heap)을 이용하여 데이터를 정렬해보자. Heap은 complete binary tree이면서 모든 부모-자식 노드 간의 순서 관계가 일정한 자료구조이다. Heap을 이용하여 오름차순 정렬을 하는 경우,
  1. 전체 데이터를 max-heap으로 재구성
  2. 첫 데이터와 heap의 마지막 데이터를 교환
  3. heap의 크기를 하나 감소시킴
  4. heap 재구성
  5. heap크기가 1보다 큰 경우 2~4 단계 반복
의 과정을 수행한다. Max heap을 구성하였으므로, heap의 첫번째 원소는 heap에 있는 나머지 원소보다 크거나 같다. 이 원소를 heap의 마지막 원소와 교환을하고, heap의 크기를 하나 줄이면, heap에 있던 데이터중 가장 큰 값이 heap 영역의 바로 다음에 위치하게 된다. 이 작업을 나머지 데이터에 반복하여 데이터를 정렬한다.


package main

import "fmt”

func main() {
    data := []int{5, 7, 1, 2, 10, 6, 4, 9, 8, 3}
    heapSort(data)
    fmt.Println(data)
}

type Comparer func(a, b int) bool

func heapSort(data []int) {
    compFunc := func(a, b int) bool {
        return a > b
    }
    buildHeap(data, len(data), compFunc)
    for size := len(data); size > 1; {
        data[0], data[size-1] = data[size-1], data[0]
        size--
        heapify(data, size, 0, compFunc)
    }
}

func buildHeap(data []int, size int, compFunc Comparer) {
    for index := (size - 2) / 2; index >= 0; index-- {
        heapify(data, size, index, compFunc)
    }
}

func heapify(data []int, size, index int, compFunc Comparer) {
    left := index*2 + 1
    right := left + 1
    sel := right

    if right < size {
        if compFunc(data[left], data[right]) {
            sel = left
        }
    } else if left < size {
        sel = left
    } else {
        return
    }
    if compFunc(data[sel], data[index]) {
        data[index], data[sel] = data[sel], data[index]
        heapify(data, size, sel, compFunc)
    }
}


2017년 1월 11일 수요일

이진트리의 순차적 전위 순회 (iterative pre-order traversal)

이진트리에서 전위 순회(pre-order traversal)을 재귀호출을 사용하지 않고 구현하자.

전위 순회는 방문한 노드를 우선 출력하고 왼쪽 자식노드와 오른쪽 자식노드의 순서로 방문한다. 그렇다 보니, 왼쪽 자식노드의 하위에 있는 노드들이 오른쪽 자식노드 보다 먼저 방문된다. 이러한 방문 순서는 스택(stack)을 사용하여 구현할 수 있다 (아래 구현에서 stack은 Go의 slice를 이용하여 간단히 구현하였다).


type NodeVal int

type Node struct {
  Val   NodeVal
  Left  *Node
  Right *Node
}

func preorderTraverseIterative(root *Node) {
  S := []*Node{}
  S = append(S, root)
  for size := len(S); size > 0; size = len(S) {
    node := S[size-1]
    fmt.Printf("%d ", node.Val)
    S = S[:size-1]
    if node.Right != nil {
      S = append(S, node.Right)
    }
    if node.Left != nil {
      S = append(S, node.Left)
    }
  }
}

이진트리의 순차적 중위 순회 (iterative in-order traversal)

이진 트리의 in-order traversal을 재귀호출(recursion)을 사용하지 않고 구현해보자.



탐색할 때 주어진 노드의 왼쪽 subtree를 우선적으로 탐색해야 하고, 먼저 방문된 parent node보다 나중에 방문된 left child node가 우선적으로 처리가 되는 특성(LIFO)을 보이므로, stack을 사용하여 구현한다.
알고리즘을 기술해보면,

  1. Root node를 시작으로 node의 왼쪽 자식 노드를 따라가며 stack에 push
  2. Stack에 노드가 있는 동안
    1. Node를 pop하고
    2. Node를 출력
    3. 이 node의 오른쪽 자식 node를 시작으로 node의 왼쪽 자식 노드를 따라가며 stack에 push
가 된다.
(아래 구현에서 stack은 slice를 이용하여 간단히 흉내만 낸다)


func inorderTraverseIterative(root *Node) {
S := []*Node{}
node := root
for node != nil {
S = append(S, node)
node = node.Left
}

for size := len(S); size > 0; size = len(S) {
node = S[size-1]
S = S[:size-1]
fmt.Printf("%d ", node.Val)
node = node.Right
for node != nil {
S = append(S, node)
node = node.Left
}
}
}

2017년 1월 10일 화요일

In-order traversal과 pre-order traversal 결과로부터 이진트리(binary tree) 생성하기

In-order traversal과 pre-order traversal 결과가 주어졌을 때, 이진트리를 생성하는 방법에 대해서 알아보자.

Pre-order traversal
1
2
4
7
8
10
13
14
5
9
11
12
15
3
6

In-order traversal
7
4
13
10
14
8
2
5
11
9
12
15
1
3
6

가 주어졌을 때, 아래와 같은 이진트리를 생성하는 것이다.


Pre-order 순회의 특성상 root node가 먼저 출력되고, in-order 순회의 특성으로는 root node를 기준으로 left sub-tree와 right sub-tree가 나누어진다.

Pre-order traversal
1
2
4
7
8
10
13
14
5
9
11
12
15
3
6

In-order traversal
7
4
13
10
14
8
2
5
11
9
12
15
1
3
6

위 표에서 노란색으로 표시한 것이, pre-order traversal의 첫번째 element이자 root node, 분홍색으로 표시한 것은 in-order traversal에서 root node의 left subtree,  연두색으로 표시한 것은 in-order traversal에서 root node의 right subtree이다. In-order traversal에서 left subtree와 right subtree의 크기를 알 수 있으므로, 이를 근거로 pre-order traversal에서도 left subtree와 right subtree에 해당하는 부분을 구할 수 있다 (위 표에서 각각 파란색과 회색으로 표시됨)
Pre-order traversal과 in-order traversal 결과가 주어졌을 때, root node, left subtree, right subtree를 구분할 수 있고, 동일한 방법을 left subtree와 right subtree에 재귀적으로 적용하면, 전체 이진트리를 구성할 수 있다.


func buildTreeFromPreInorderTraversal(preorder, inorder []NodeVal) *Node {
return buildTreeFromPreInorderTraversalUtil(preorder, 0, len(preorder)-1, inorder, 0, len(inorder)-1)
}

func buildTreeFromPreInorderTraversalUtil(preorder []NodeVal, preS, preE int, inorder []NodeVal, inS, inE int) *Node {
if preS > preE {
return nil
}
index := inS - 1
for i := inS; i <= inE; i++ {
if inorder[i] == preorder[preS] {
index = i
break
}
}
node := &Node{
Val:   preorder[preS],
Left:  buildTreeFromPreInorderTraversalUtil(preorder, preS+1, preS+index-inS, inorder, inS, index-1),
Right: buildTreeFromPreInorderTraversalUtil(preorder, preS+index-inS+1, preE, inorder, index+1, inE),
}
return node
}

잔돈 만드는 방법 (Coin change)

숫자(N)와 숫자의 집합(C = {C1, C2, C3, ...}이 주어졌을 때, C의 숫자들을 중복으로 사용하여 N을 만드는 방법의 수를 구해보자.  잔돈 문제로 바꾸어서 표현하면, 각각 값어치가 C1, C2, C3, ... 인 동전이 무한대로 주어질...