Overview

Java Collection Implementations[1]

이 장에서는 여러 Data Structure의 개념과 자바에서는 실제로 어떻게 구현되어 있는지 알아보도록 한다.

Linear Data Structure

Array

논리적 저장순서와 물리적 저장순서가 일치한다. 즉 메모리에서 연속적으로 데이터가 기록되는 자료구조이다. 이 특성 덕에, 특정 아이템을 인덱스를 알면 O(1)안에 접근이 가능하지만(random access), 아이템 삭제나 추가를 진행할 때, 연속성을 유지해야 하기 때문에 최대 O(n)의 시간복잡도가 발생한다.

보통 자바에서는 []를 통해서 Array를 생성하며, 아래와 같이 스택 영역에 Array의 레퍼런스를 저장하고 힙 영역의 메모리에 연속적으로 데이터를 할당하고 있다. 이 때, 메모리의 크기는 고정적이다. 또한 Array에서는 같은 클래스의 인스턴스를 가지도록 하며, 관련이 없는 클래스가 할당될 경우 IllegalArgumentException이 발생한다.

ArrayList in Java

위와 같이 []를 이용한 일반적인 Array는 선언과 동시에 배열의 크기가 정해진다. 따라서 만약 초기 선언된 크기보다 더 많은 데이터를 넣고 싶다면 문제가 발생한다. 이를 해결하기 위해서 Java에서는 배열의 크기를 변경할 수 있는 ArrayList 클래스가 존재한다.

ArrayList는 Array를 사용하며 만약 사용하던 Array의 capacity(용량)가 부족해지면 적절한 크기로 늘린 새로운 Array를 만들고 기존의 데이터를 복사한다. 다른 블로그에서는 대부분 2배로 Array를 늘린다고 되어 있지만, 실제로는 아래와 같이 ArraysSupport.newLength을 사용하여 최적화된 크기로 늘린다.

openJDK17에 사용된 ArrayList 클래스의 grow 메서드

  • Tip : Vector vs ArrayList결론부터 말하자면 Vector는 synchronized되어 있으며, ArrayList는 asynchronized되어 있다. 즉, Vector는 하나의 스레드만 접근이 가능하며 ArrayList는 여러 스레드가 동시에 접근이 가능하다. synchronized되어 있다는 것은 이를 위한 코드가 추가적으로 작성되어 있다는 뜻이기에, 일반적으로 ArrayList가 더 빠른 성능을 가지고 있다. 따라서 Thread-safe한 환경에서는 ArrayList를 사용하고 그렇지 않은 환경에서는 Vector나 Collection.synchronizedList를 사용하는 것이 옳다.
  • 자바에서 Vector클래스를 들어본적이 있는가? 아마 대부분 들어본적이 없을 것이다. Vector는 자바의 초창기에 지금의 ArrayList기능을 위한 레거시 클래스이다. 그러나 현재는 List 인터페이스를 구현하기 위해서 Collection 프레임워크에 맞게 재구성되어있기 때문에 사용할 수 있다. 그렇다면 Vector 와 ArrayList의 차이점은 무엇일까?

Linked List

Linked List의 경우 Array와 달리 메모리에 연속적으로 저장되지 않으며, 각 아이템은 자기 다음의 아이템의 위치만을 가지고 있는 구조이다. 이를 통해서 메모리에 연속적으로 저장하지 않아도 리스트의 요구사항을 만족할 수 있어 삽입과 삭제에 O(1)의 시간복잡도가 요구된다. 그러나 원하는 부분에서 삽입과 삭제를 진행하기 위한 search 과정에서 최악의 경우 O(n)의 시간복잡도가 발생할 수 있기 때문에, 전체적인 시간복잡도는 O(n)이 될 수 있다.

Doubly-Linked List in Java

Doubly-linked list implementation of the Listand Dequeinterfaces. Implements all optional list operations, and permits all elements (including null). All of the operations perform as could be expected for a doubly-linked list. Operations that index into the list will traverse the list from the beginning or the end, whichever is closer to the specified index. [2]

자바에서 어떻게 구현되어 있는지 살펴보자. 자바에서 사용하는 LinkeList는 기본적으로 Doubly-LinkeList이다. 이는 하나의 원소가 자기 다음의 원소 주소만 저장하는 것이 아니라 반대로 자기 이전의 주소 또한 저장하는 것이다. 이렇게 사용하는 이유는 효율성을 위해서이다. 예를 들어, 찾고자 하는 원소의 인덱스가 리스트의 마지막에 가깝다면, 처음에 가까이 있는 원소보다 시간이 많이 소요될 것이다. 자바에서는 이런 불균형을 방지하기 위해서 마지막에서부터 원소를 탐색을 시작할 수 있도록 하기 위해서 Doubly-Linked List를 사용하고 있다.

Stack

선형 자료구조의 일종으로 마지막으로 삽입된 원소가 처음 나오는 구조(LIFO, Last In First Out)를 가지고 있다. 대표적으로 사용자가 어떤 액션을 취하고 되돌아가기 위해 ctrl+z를 누르는데 이 때, 사용자의 액션이 저장되는 자료구조가 스택이다. 또한, 프로그램에서 스택은 함수의 호출 구조를 저장하는데 사용된다. 예를 들어, 사용자의 정보를 다운로드 하는 함수가 아래와 같이 수행된다고 하자. 그럼 getUserInfo → parse → download 순으로 메서드가 호출되고 이 순서대로 스택에 호출 정보가 저장된다. 그리고 메서드 처리가 완료될때마다, download → parse → getUserInfo 순으로 값이 반환된다. 이렇듯, 스택은 마지막으로 처리한 과정부터 역으로 진행이 필요한 프로세스에 사용되는 기본적인 자료구조이다.

function getUserInfo(){
	return parse(donwload())
}

Stack in Java

자바에서는 Stack 클래스를 이용해서 stack 자료구조를 구현하고 있다. 그 특성은 위에서 말한 Stack의 특성과 동일하며 재미있는 점은 아래와 같이 Vector를 super 클래스로 삼고 있다. Vector는 synchronized ArrayList라고 생각하면 된다(자세한 내용은 위의 “Tip : Vector vs ArrayList”를 참고). 따라서 Stack 또한 하나의 스레드만 접근가능한 synchronized가 적용되어 있어 Thread-safe하다.

//Stack's super class
java.lang.Object
	java.util.AbstractCollection<E>
		java.util.AbstractList<E>
			java.util.Vector<E>
				java.util.Stack<E>

Queue

Stack은 LIFO 구조라고 하였다. 그렇다면 반대로 처음 삽입된 원소가 마지막으로 나오는 (FIFO, First In First Out)구조도 필요하지 않을까? 그럴 때 사용되는 것이 Queue이다. 큐는 CS에서 사용하는 세마포어, 버퍼링 등 다양한 프로세스를 구현하기 위해서 사용된다.

Queue in Java

Stack과 달리 자바에서는 Queue를 위한 클래스가 따로 존재하지 않는다. 대신, LinkedList, PrioirtyQueue 등 Queue 인터페이스가 구현된 다른 클래스를 적절히 선택하는 것을 권장하고 있다.

Queue<String> queue= new LinkedList<String>()
Queue<String> queue= new PrioirtyQueue<String>()

예를 들어, 일반적인 순서를 가지는 큐를 구현하고 싶으면 LinkedList를 구현하면 되고, 데이터를 삽입할 때마다 정렬되는 큐를 만들고 싶다면 PrioirtyQueue를 사용하면 된다. 이외 에도, AbstractQueue, ArrayBlockingQueue, ArrayDeque, ConcurrentLinkedDeque, ConcurrentLinkedQueue, DelayQueue, LinkedBlockingDeque, 등등, 다양한 클래스를 자신의 적절한 환경에 사용할 수 있다.

Tree

지금까지 다룬, Array,List, Stack, Queue는 데이터를 순서대로 표현할 수 있는 선형 자료구조다. 이와 달리 Tree는 나뭇가지가 뻗어 나가듯 데이터가 나뉘어지는 비선형 구조를 가지고 있다.

Binary Tree

이진 트리는 각각의 노드가 최대 2개의 자식노드를 가질 수 있는 재귀적인 자료구조이다. 이진트리는 깊이에 따라 레벨이 정해져 있다. 루트노드가 레벨 0이며 깊이가 내려갈 수록 레벨이 증가한다. 또한 최고레벨을 가르켜 해당 트리의 height라고 한다.

Perfect Binary Tree(포화 이진 트리)

모든 레벨이 채워져있는 이진 트리

Complete Binary Tree(완전 이진 트리)

트리의 노드가 위에서 아래로, 왼쪽에서 오른쪽으로 차곡차곡 채워져있는 트리이다. 여러 알고리즘에서 자주 사용되는 트리 구조

Full Binary Tree(정 이진 트리)

모든 노드가 0 또는 2개의 자식 노드만을 가지고 있는 트리이다.

Binary Tree in Java(Array vs Recursive Class)

자바에서는 이진 트리를 위한 특정한 클래스가 존재하지 않는다. 대신, 이진트리는 Array를 이용해서 간단하게 구현할 수 있다. 루트노드는 1번째 노드라고 규정하고 노드의 내용을 1번째 인덱스에 저장 했을 때, n번째 노드의 왼쪽 자식노드는 n2 인덱스에 저장하며 오른쪽 자신 노드는 n2+1 인덱스에 저장하도록 하면 Array에 이진트리의 정보를 저장할 수 있다.

아래와 같이 Array 이외에도 노드가 노드를 참조하는 재귀적인 클래스를 생성하여 이진트리를 구현할 수 있다.

class Node {
    int value;
    Node left;
    Node right;

    Node(int value) {
        this.value = value;
        right = null;
        left = null;
    }
}

이제 Array로 작성한 이진 트리와 클래스를 사용한 이진트리의 장단점을 구별해보자. 첫 번째로, 노드의 데이터를 가져올 때 Array는 인덱스를 통해서 상수시간내에 접근이 가능하지만, 클래스에서는 O(logn)의 검색시간이 발생한다. 따라서 삽입, 삭제, 업데이트 시에도 클래스로 구현한 이진트리에서 더 많은 시간이 소요된다. 두 번째로, 메모리 관점에서 바라볼때 클래스는 내용 이외에 자식노드의 인스턴스까지 참조해야 하기 때문에, Array에 비해서 많은 메모리가 필요하다. 그러나, 완전 이진트리가 아닌 이진트리의 경우 Array는 저장해야할 내용보다 많은 메모리가 필요하지만 클래스는 저장해야만 하는 노드를 구현하기 때문에 메모리를 절감할 수 있다.

Binary Search Tree

이진 트리를 사용해서 데이터를 검색하기 효율적인 자료구조를 만들 수 있다. 아래와 같은 규칙을 정렬된 데이터를 트리 형식으로 구현해보자.

  • 규칙 1. 이진 탐색 트리의 노드에 저장된 키는 유일하다.
  • 규칙 2. 부모의 키가 왼쪽 자식 노드의 키보다 크다.
  • 규칙 3. 부모의 키가 오른쪽 자식 노드의 키보다 작다.
  • 규칙 4. 왼쪽과 오른쪽 서브트리도 이진 탐색 트리이다.

그리고 루트노드 부터 임의의 저장된 숫자를 검색한다고 했을 때, 마주치는 노드를 기준으로 왼쪽 자식노드로 검색할 지, 오른쪽 자식노드로 검색할 지 비교를 통해 결정할 수 있다. 트리가 완전 이진트리로 균형 잡혀 생성된 경우, 검색을 할 때마다, 비교해야 할 데이터가 절반씩 줄어들기 때문에 O(logN)=O(height)의 시간복잡도가 발생하여 큰 데이터 일 수록 매우 빠르게 검색할 수 있다.

그러나 만약 이진 탐색 트리가 왼쪽이나 오른쪽을 치우쳐 있을 경우, Worst Case 가 발생하며, 검색하는데 O(n)의 시간복잡도가 필요하다.

Red Black Tree

앞서 말했듯, 위와 같은 Binary Tree는 Worst Case의 경우 O(n)의 시간복잡도가 발생한다. 이런 경우를 대비하기 위해서 자료구조에는 Binary Tree가 균형잡인 트리로 유지하기 위한 Red Balck Tree가 존재한다. (구현하기 위한 알고리즘은 복잡하므로 생략한다.)

Red Balck Tree in Java

대표적으로 TreeMap이 Red Black Tree 구조를 사용하여 구현되어 있다. Map에서는 키의 유무 또는 위치를 검색하는게 중요한다. 이 때, 키를 저장하는 자료구조가 Red Black Tree이다.

Binary Heap

Complete Binary Tree 구조이며, 부모 노드의 값이 자식노드보다 항상 크다면 Max Heap, 부모 노드의 값이 자식노드보다 항상 작다면 Min Heap이라고 부른다. 이 자료구조는 저장된 데이터에서 최댓값이나 최솟값을 검색할 때 항상 O(1)의 시간복잡도를 보장하기에 최대,최소를 자주 찾는 프로세스에서 사용된다. 최대값을 pop할 경우 다시 저장된 데이터중 최댓값이나 최솟값이 루트노드에 존재해야 하기 때문에, heapify(맨 마지막 노드를 루트 노드에 대체시킨 후, 자식 노드와 비교하면서 데이터의 자리를 재위치 시키는 과정)를 진행하기에 O(logn)의 시간복잡도가 발생한다.

Binary Heap in Java

Priority queue represented as a balanced binary heap: the two children of queue[n] are queue[2n+1] and queue[2(n+1)]. The priority queue is ordered by comparator, or by the elements' natural ordering, if comparator is null: For each node n in the heap and each descendant d of n, n <= d. The element with the lowest value is in queue[0], assuming the queue is nonempty.

자바에서는 Heap을 구현하기 위해서 PriorityQueue 클래스를 사용한다. PriorityQueue는 Array를 이용해서 Complete Binary Tree를 구현하고 있으며, null을 포함한 non-Comparable한 데이터의 추가를 금지하고 있다. 또한, 동기성이 보장되고 있지 않아 여러 사용자가 동시에 접근하는 것은 안전하지 않으므로 이 경우, thread-safety가 보장된 PriorityBlockingQueue를 권장하고 있다.

Hash Table

Array에 데이터를 저장할 때 단순히 순서대로 저장하는 것이 아니라, 저장할 값을 기준으로 인덱스를 결정하여 저장하는 자료구조를 뜻한다. 이 때, 저장할 값을 기준으로 인덱스를 생성하는 방법을 hash method 또는 hash functiond이라고 하며 생성된 인덱스를 hashcode라고 한다. hash function이 좋지 못할 경우 서로 다른 값에서 동일한 인덱스가 생성될 수 있는데, 이 경우 동일한 인덱스에 여러개의 데이터가 존재할 수 있다. 이를 collision이라고 칭한다. hash function이 어떻게 작성되느냐에 따라, collision의 정도가 결정되는데, 항상 collision이 발생하지 않는것이 꼭 좋은 자료구조는 아니다. 오히려 적절한 collision이 발생하는 것이 좋은 성능의 Hash Table을 만들 수 있다.

HashTable in Java

자바에서는 HashTable을 사용할 때 기본적으로 키값과 저장할 내용, 두 가지를 입력한다. 키로 삽입되는 인스턴스는 Null이 아니며 hashCode()메서드가 구현되어 있어야 한다(HashMap은 Null값도 허용한다). 자바 개발자들은 Hashtable은 최근에 Java Collection에 포함된 대부분의 클래스와 달리 thread-safety가 보장되기 때문에 single programming에서 사용하는 경우 오버헤드가 발생하므로 thread-safety가 필요없는 경우 HashMap을 사용하는 것을 권장하고 있다. 또한 Hashtable은 레거시 클래스에 가깝기 때문에, thread-safety하고 고도의 동시성을 구현하기 위해서는 [ConcurrentHashMap](<https://docs.oracle.com/javase/8/docs/api/java/util/concurrent/ConcurrentHashMap.html>) 을 사용하는 것을 권장하고 있다.[5]

  1. Entry

이제 본격적인 구조에 대해서 알아보자. 해쉬 테이블의 데이터는 아래처럼 Entry 메서드에 저장된다. Entry는 Hashtable의 이너 클래스로, 이론적으론 하나의 버킷을 의미한다. [6]

private transient Entry<?,?>[] table;

내부 구조를 보면 알겠지만 LinkedList와 비슷한 구조를 가지고있다. 여기서 알 수 있는 점은, 같은 hashCode() 결과를 가져, 하나의 Entry에 포함된 원소들의 경우 검색하는데 선형적 시간복잡도가 걸린다는 것이다.

/**
 * Hashtable bucket collision list entry
 */
private static class Entry<K,V> implements Map.Entry<K,V> {
    final int hash;
    final K key;
    V value;
    Entry<K,V> next;
....

2. Put

이번엔 새로운 데이터를 추가하는 코드를 살펴보자. 우선 윈소의 값이 null인지 확인하고, 기존의 테이블의 길이와 hashCode()를 통해서 인덱스를 결정한다. 그리고 인덱스에 해당하는 엔트리를 가져와 링크드리스트를 탐색하듯 반복문을 진행하고 같은 키를 가지는 경우 기존의 원소값을 업데이트 한다. 같은 키가 없다면 addEntry를 통해서 새로운 값을 추가한다.

public synchronized V put(K key, V value) {
        // Make sure the value is not null
        if (value == null) {
            throw new NullPointerException();
        }

        // Makes sure the key is not already in the hashtable.
        Entry<?,?> tab[] = table;
        int hash = key.hashCode();
        int index = (hash & 0x7FFFFFFF) % tab.length;
        @SuppressWarnings("unchecked")
        Entry<K,V> entry = (Entry<K,V>)tab[index];
        for(; entry != null ; entry = entry.next) {
            if ((entry.hash == hash) && entry.key.equals(key)) {
                V old = entry.value;
                entry.value = value;
                return old;
            }
        }

        addEntry(hash, key, value, index);
        return null;
    }

Hashtable에서는 loadFactor라는 값이 있는데 기존의 테이블을 얼마나 채우는 것을 허용하는지 결정하는 변수이다. 보통 메모리와 처리시간을 고려하여 0.75로 정해져 있다. threshold는 loadFactor값에 현재의 엔트리 용량을 곱한 결과로 지금 테이블 용량에서 최대로 채울수 있는 엔트리의 길이를 의미한다. 그래서 테이블에 새로운 값을 추가할 때, 테이블의 갯수가 threshold를 넘을 경우 rehash()메서드를 호출하여 테이블의 크기가 두 배인 변수를 만들어 기존의 값을 전부 복사한다. 여기서 배울 수 있는 점은, 만약 Hashtable의 크기를 처음에 결정할 수 있다면, rehash()메서드의 호출을 줄이기 위해서 처음부터 크기를 알맞게 할당해야 한다는 것이다. 그 다음 과정은 새로운 엔트리를 생성하여 값을 추가하는 것이다.

private void addEntry(int hash, K key, V value, int index) {
        Entry<?,?> tab[] = table;
        if (count >= threshold) {
            // Rehash the table if the threshold is exceeded
            rehash();

            tab = table;
            hash = key.hashCode();
            index = (hash & 0x7FFFFFFF) % tab.length;
        }

        // Creates the new entry.
        @SuppressWarnings("unchecked")
        Entry<K,V> e = (Entry<K,V>) tab[index];
        tab[index] = new Entry<>(hash, key, value, e);
        count++;
        modCount++;
    }

3. get

Hashtable에서 put을 이해했다면 get은 매우 간단하다. hashCode를 통해서 인덱스를 구한 후, 이에 해당하는 엔트리를 가져와 LinkedList를 검색하듯 get을 진행하고 만약 찾지 못했다면 null을 반환한다.

public synchronized V get(Object key) {
        Entry<?,?> tab[] = table;
        int hash = key.hashCode();
        int index = (hash & 0x7FFFFFFF) % tab.length;
        for (Entry<?,?> e = tab[index] ; e != null ; e = e.next) {
            if ((e.hash == hash) && e.key.equals(key)) {
                return (V)e.value;
            }
        }
        return null;
    }

HashMap vs Hashtable in Java

Java Docs를 포함한 대부분의 포스트에서는 HashMap과 Hashtable의 차이를 Thread-safety와 키, 원소의 Null 허용 여부에 두고 있다. 그러나 내부 구조에서는 매우 큰 차이가 존재하는데 바로 Resolve Conflict관점에서 Hashtable은 Seperate Chaining방식에서 Linked List만을 사용하지만 Tree와 Linked List를 공용으로 사용한다. Linked List는 탐색을 위해서 선형적 시간복잡도가 발생하는데 만약 하나의 버킷에 많은 양의 링크드 리스트가 존재한다면 탐색시간이 크게 늘어난다. 따라서 크기가 8이상의 버킷에 대해선 Red-Black-Tree방식으로 자료구조를 변경하여 탐색에 O(logN)의 시간복잡도를 제공하도록 한다.[7]

Graph

그래프는 정점(Vertex,Node)와 두 개의 정점을 잇는 간선(Edge)의 집합이다. Edge그래프는 Edge의 종류에 따라 Undirected Graph와 Directed graph로 나뉘게 되는데, Undirected의 경우 Edge에 방향이 없고, Directed의 경우 하나의 노드에서 다른 노드를 가르키는 방향성이 존재한다. 이 때, Undirected Graph에서 정점 간 연결된 Edge의 갯수를 Degree라고 하며 Directed Graph에서는 방향성에 따라 Outdegree, Indegree 두 가지로 나누어 갯수를 센다. 또한 만약 Edge에 가중치가 존재하는 경우 Weight Graph라고 칭한다.

그래프는 보통 이해관계를 표현할 때 주로 사용된다. 예를 들어 장소와 장소의 관계를 연결된 도로나 다리로 표현하거나 사람과 사람간의 채무 관계 등을 표시할 때 사용된다. 이런 이해관계를 그래프 자료구조로 저장함으로써 장소간 최단거리를 구하는 등의 알고리즘을 구현할 수 있다.

트리 또한 그래프의 종류 중 하나이며, 트리는 그래프와 달리 사이클이 생성되지 않는 특징을 가지고 있다.

Implementation

그래프를 구현하는 방법에는 크게 두 가지가 존재한다.

Adjacent Matrix

인접 매트릭스의 경우 정점의 갯수만큼의 행과 열을 가진 배열을 만들고 row,col을 정점의 번호로 가정했을 때, A와 B를 잇는 간선이 존재하는 경우 matirx[A][B]에 간선의 여부 및 가중치의 값을 기록하여 그래프를 구현하는 방법이다.

Adjacent List

인접 리스트의 경우, 정점의 갯수만큼 Array를 생성한 다음 각 원소에는 LinkedList를 구현하여 정점 A와 B가 이어져 있는 경우 matrix[A].push(B)를 통해서 간선의 유무, 방향성을 표시한다. 이 때 가중치 그래프인 경우 가르키는 정점과 함께 가중치 값을 삽입하기도 한다(matrix[A].push([B,W])).

Search

그래프는 비정형 데이터이므로 인덱스를 통한 순차적 검색을 지원하지 않는다. 대신, Recursive based Class Tree처럼 정점과의 관계인 간선을 하나씩 이어나가면서 데이터를 탐색하는 방법이 있다. 여기에는 크게 깊이를 우선으로 탐색하냐, 너비를 우선으로 탐색하냐에 탐색법이 나뉘게 된다.

  1. DFS (Depth First Search)

DFS는 임의의 한 정점에서 다른 정점으로 나아가는 것을 반복하는 것이다. 예를 들자면, A 정점이 B,C와 연결되고 B 정점이 D,E와 연결된다면 A→B→D 순서로 연결된 간선 중 순서가 빠른 간선으로 계속 전진하는 것이다. 만약 더 이상 전진할 간선이 없다면 이전의 정점으로 돌아가 남아있는 간선으로 탐색을 다시 진행한다. 이 구조는 Stack을 이용한 미로찾기와 같은 구조인데, 역시 DFS에서는 Stack을 이용하여 이를 구현하다.

  1. BFS(Breath First Search)

BFS는 너비 우선 탐색으로, 주위에 있는 간선을 먼저 탐색하는 것이다. 위와 같은 예를 들때, BFS로 탐색하면 A→B→C→D→E 순서로 탐색하게 된다. 주위에 있는 정점을 모두 탐색했을 때, 이 후 탐색한 순서대로 또 다시 해당 정점에서 주위 탐색을 진행한다. “먼저 탐색한 정점이 또 먼저 탐색을 시작”하므로 FIFO 형태의 자료구조가 필요하므로 BFS에서는 Queue를 사용한다.

Reference

  1. https://docs.oracle.com/javase/7/docs/technotes/guides/collections/overview.html
  2. https://docs.oracle.com/javase/7/docs/api/java/util/LinkedList.html
  3. https://docs.oracle.com/javase/7/docs/api/java/util/Stack.html
  4. https://docs.oracle.com/javase/7/docs/api/java/util/PriorityQueue.html
  5. https://docs.oracle.com/javase/8/docs/api/java/util/Hashtable.html
  6. https://github.com/AdoptOpenJDK/openjdk-jdk11/blob/master/src/java.base/share/classes/java/util/Hashtable.java
  7. https://github.com/AdoptOpenJDK/openjdk-jdk11/blob/master/src/java.base/share/classes/java/util/HashMap.java


출처: http://t1.kakaocdn.net/codefestival/preliminary.pdf

이번에 풀어볼 문제는 다음과 같습니다.

이번 문제를 풀 때 DFS search를 써서 인접한 영역들을 찾아내고 그 크기를 계산하는 것을 시도했습니다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
class Solution {
    public static int m,n;
    public static int[][] picture;
    public static boolean visited[][];
    public static int size=0;
    public int[] solution(int m, int n, int[][] picture) {
        int numberOfArea = 0;
        int maxSizeOfOneArea = 0;
        //TODO 1: 다른 함수에서 데이터에 접근하기 위해 전역변수취급을 해줍니다. 
        this.m=m;
        this.n=n;
        this.picture=picture;
        this.visited=new boolean[m][n];
        /*
        TODO 2
        반복문을 통해서 새로 탐색할 부분을 찾고, 
        serch함수(영역의 크기계산 & 탐색한 부분은 visited배열에서 true로 처리합니다.
        그리고 최대 영역크기를 갱신합니다.
        */
        for(int r=0;r<m;r++){
            for(int l=0;l<n;l++){
                if(visited[r][l]==false && picture[r][l]!=0 ){
                    searchSize(r,l);
                    numberOfArea++;
                    if(size>maxSizeOfOneArea)   maxSizeOfOneArea=size;
                    size=0;
                }
            }
        }
        int[] answer = new int[2];
        answer[0= numberOfArea;
        answer[1= maxSizeOfOneArea;
        return answer;
    }
    public static int searchSize(int r,int l){
        // TODO 3: searchSize함수는 DFS search로 상하좌우 영역을 탐색합니다.
        size++;
        visited[r][l]=true;
        if(r>0 && visited[r-1][l]==false &&picture[r-1][l]==picture[r][l]){
            searchSize(r-1,l);
        }
        if(r<m-1 && visited[r+1][l]==false &&picture[r+1][l]==picture[r][l]){
            searchSize(r+1,l);
        }
        if(l>0 && visited[r][l-1]==false && picture[r][l-1]==picture[r][l]){
            searchSize(r,l-1);
        }
        if(l<n-1 && visited[r][l+1]==false &&picture[r][l+1]==picture[r][l]){
            searchSize(r,l+1);
        }
        return 0;
    }
}
cs


 Selection Sort(선택정렬) &  Bineary search(이진탐색)


목차

  1. 선택정렬 설명 & 코드
  2. 이진탐색 설명 & 코드
  3. 알고리즘 코딩을위한 팁

Selection Sort(선택정렬)

설명

선택정렬이란?: 선택 정렬은 전체배열에서 가장 작은 것(또는 큰것)들을 하나씩 선택하여  정렬을하는방법입니다.
이해는 모션으로
선택정렬을위한 단계
  1. 주어진 리스트 중에 최솟값을 찾는다.
  2. 그 값을 맨 앞에 위치한 값과 교체한다(패스(pass)).
  3. 맨 처음 위치를 뺀 나머지 리스트를 같은 방법으로 교체한다.

자 그럼 단계를 알았으니 이것을 C언어로 프로그래밍해보겠습니다.
프로그램은 배열의 크기배열을 입력받아서 이를 선택정렬후 출력하도록합니다.

시작하기전
어떻게 코딩을 할것인지 각주를 통해 구상을 해봅시다.

1. 배열을 입력받아야하니 우선 데이터를 받는부분을 구현합니다.
2. 선택정렬을합니다.
3. 정렬된 배열을 출력합니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
#include <stdio.h>
int main(void){
    //1.make array
        //1.get data
    
    //2.loop for n
    
        //2.loop for find min
    
        //2.exchange
    
    //3.print
}
cs


무턱대고 바로 코딩을 시작하는 것보단 전체적인 구상을 잡는게 실수를 줄이며 효율적인 코드를 만들 수 있습니다.

구상을 잡았으니 단계별 프로그래밍을 진행해보겠습니다.

1. 배열을 입력받아야하니 우선 데이터를 받는부분을 구현합니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
#include <stdio.h>
#include <stdlib.h>
int main(void){
    //make array
    int array_size,*array;
    scanf("%d",&array_size);
    array=(int*)malloc(sizeof(int)*array_size);
    //get data
    int i;
    for(i=0;i<array_size;i++)
        scanf("%d",&array[i]);    
    //loop for n
    
        //loop for find min
    
        //exchange
    
    //print
}
cs

배열의 크기를 받고 동적할당(malloc)이용해 크기만큼 정수형 배열을 생성합니다.
그리고 반복문과 scanf()함수를 통해서 데이터를 입력받습니다.


2. 선택정렬을합니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
#include <stdio.h>
#include <stdlib.h>
#define swap(a,b,temp) {temp=a;a=b;b=temp;}
int main(void) {
    //make array
    int array_size, *array;
    scanf("%d"&array_size);
    array = (int*)malloc(sizeof(int)*array_size);
    //get data
    int i;
    for (i = 0; i < array_size; i++)
        scanf("%d"&array[i]);
    //loop for n
    for (i = 0; i < array_size; i++) {
        //loop for find min
        int smallesti = i, j;
        for (j = i + 1; j < array_size; j++) {
            if (array[smallesti] > array[j]) {
                smallesti = j;
            }
        }
        //exchange
        int t;
        swap(array[smallesti], array[i], t)
    }
  //print
}
cs

구상했던데로 이중 반복문을 입력해서 범위를 줄여가며 가장작은 부분을 앞에오도록합니다.

3. 정렬된 배열을 출력합니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
#include <stdlib.h>
#define swap(a,b,temp) {temp=a;a=b;b=temp;}
int main(void) {
    //make array
    int array_size, *array;
    scanf("%d"&array_size);
    array = (int*)malloc(sizeof(int)*array_size);
    //get data
    int i;
    for (i = 0; i < array_size; i++)
        scanf("%d"&array[i]);
    //loop for n
    for (i = 0; i < array_size; i++) {
        //loop for find min
        int smallesti = i, j;
        for (j = i + 1; j < array_size; j++) {
            if (array[smallesti] > array[j]) {
                smallesti = j;
            }
        }
        //exchange
        int t;
        swap(array[smallesti], array[i], t)
    }
    //print
    for (i = 0; i < array_size; i++) {
        printf("%d ", array[i]);
    }
}
cs

 Bineary search(이진탐색)

설명

이진탐색이란?
 이진탐색은 배열중에서 어떤 수를 찾는 방법중 하나입니다.

 전체 범위에서 가운데 숫자를 기준으로 그 숫자보다 큰 지 작은지 범위를 좁혀가며 숫자를 찾습니다. 이 방식은 순서대로 찾는것보다 훨씬 효율적입니다.

 이진탐색은 오름차순으로 정렬된 배열에서만 사용이 가능합니다.

그럼이제 배열의 크기, 배열 그리고 찾고 싶은 수를 받아서 수의 위치를 반환하는 알고리즘을 작성해보도록 하겠습니다.

위와 마찬가지로 구상을 해봅시다.
1. 배열의크기, 배열 그리고 찾고 싶은 수를 입력받는다
2. 중간값이 찾고싶은 수인지 더 작은지, 큰지 비교하여 범위를 좁혀나가는걸 반복한다.
3. 찾고싶은 수가 있다면 위치를 출력하고 없다면 -1을 출력한다.

이번 구상은 위보다 조금 어려울수 있습니다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
#include <stdlib.h>
int main(void) {
    //make array
    //get data( array and target)
    
    // loop while range > 0 
        // find midle value
        
        //check value is target
        
        //else if value < target
        //else(target < value)
    //check find
}
cs

구상한걸 우선 각주로 표현하고 하나씩 진행해봅시다.


1. 배열의크기, 배열 그리고 찾고 싶은 수를 입력받는다

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
#include <stdlib.h>
int main(void) {
    //make array
    int array_size, *array, target;
    scanf("%d"&array_size);
    array = (int*)malloc(sizeof(int)*array_size);
    //get data( array and target)
    int i;
    for (i = 0; i < array_size; i++)
        scanf("%d"&array[i]);
    scanf("%d",&target);
    // loop while range > 0 
        // find midle value
        
        //check value is target
        
        //else if value < target
        //else(target < value)
    //check find
}
cs

이부분은 쉬우니 따로 설명하지 않습니다.


2. 중간값이 찾고싶은 수인지 더 작은지, 큰지 비교하여 범위를 좁혀나가는걸 반복한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
#include <stdlib.h>
int main(void) {
    //make array
    int array_size, *array, target;
    scanf("%d"&array_size);
    array = (int*)malloc(sizeof(int)*array_size);
    //get data( array and target)
    int i;
    for (i = 0; i < array_size; i++)
        scanf("%d"&array[i]);
    scanf("%d",&target);
    int start=0,end=array_size-1, target_loc=0;
    // loop while range >= 0 
    while(start<=end){
        // find midle value
        int mid=(start+end)/2
        //check value is target
        if(array[mid]==target){
            target_loc=mid;
            break;
        }
        //else if value < target
        else if(array[mid] < target){
            start=mid+1;
        }
        //else(target < value)
        else{
            end=mid-1;
        }
    }
    //check find
}
cs

이 부분에서 범위를 start변수로 첫번째 지점, end변수로 마지막 지점을 정합니다.
그리고 int mid=(start+end)/2  를 이용해서 중간지점을 찾아 target과의 값을 비교하고 결과에 따라 범위를 새로 지정합니다.

3. 찾고싶은 수가 있다면 위치를 출력하고 없다면 -1을 출력한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
#include <stdlib.h>
int main(void) {
    //make array
    int array_size, *array, target;
    scanf("%d"&array_size);
    array = (int*)malloc(sizeof(int)*array_size);
    //get data( array and target)
    int i;
    for (i = 0; i < array_size; i++)
        scanf("%d"&array[i]);
    scanf("%d",&target);
    int start=0,end=array_size-1, target_loc=0;
    // loop while range >= 0 
    while(start<=end){
        // find midle value
        int mid=(start+end)/2
        //check value is target
        if(array[mid]==target){
            target_loc=mid;
            break;
        }
        //else if value < target
        else if(array[mid] < target){
            start=mid+1;
        }
        //else(target < value)
        else{
            end=mid-1;
        }
    }
    //check find
    if (start > end) {
        printf("-1");
    }
    else {
        printf("%d", target_loc);
    }
}
cs

start > end인 경우가 못찾은 경우랑 같은이유는 다음과 같습니다.
위 반복문안에서는 범위가 점점줄어들고 있습니다. 범위가 줄어들어 start와 end가 같아질때의 위치에 있는 숫자가 없다면 end=mid-1; 이 실행되어 end는 start보다 작아지게 됩니다. 따라서 start > end == 못찾음이 성립됩니다.

알고리즘 코딩을위한 팁

두 개의 대표적인 알고리즘 코드를 짜보았습니다. 앞으로는 이보다 더 어렵고 복잡한 알고리즘을 짤 일이 많을 수 있습니다. 이 때 빠르고 정확하게 알고리즘을 짜는법은 절대 이해하지도 못하면서 빨리 시작부터하는것이 아닙니다.

1.복잡한 문제를 빠르게 짜기위해서는 항상 전체적인 구상을 생각해봐야 합니다. 
2. 각주를 사용해 미리 코딩을 한다는 생각으로 살짝 적어주고 맞는지 판단합니다.
3. 그리고 코드를 짜면 자잘한 에러를 제외하고 거의 정확합니다.

코드를 짜기전에는 항상 이해를 해야하기 때문에 무턱대고 코드를 짜는것은 나중에 누더기옷같은 코드를 만들수 있습니 꼭 이해와 구상을 하고 짜시기 바랍니다.

Transposing Sparse Matrix 

1. 희소행렬(Sparse Matrix)이란?

희소행렬은 행렬안의 값들 중에서 0이외의 값이 희소한 행렬을 의미합니다.

따라서 대부분 0이라면 모든 값들을 표현하지 않고 0이아닌 값과 그 위치만 알고 나머지는 0이다 라고 표현을 할 수 있습니다.

그러므로
 
평소에 그리던 행렬을
이런 방식으로 표현할 수 있습니다.(변환 방식은 생략)

2. 희소행렬의 대칭이동

위에서 봤던 간단하게 표현된 행렬을 통해서 대칭이동을 하겠습니다.
단순한 대칭이동은 그저 row와 col값을 교환해주면 간단합니다. 그러나 정렬을 유지 하고싶다면 어떻게 해야할까요?

단순 교환은 정렬이 유지되지 않습니다. 따라서 새로운 방법을 궁리해야합니다.

3. 대칭이동과 정렬


우선 기존행렬을 A, 바뀐 새로운 행렬을 B라고 합시다.
대칭이동을 하면서 정렬을 하기위해선 단순히 바꾸기보다는 적절한 위치에 A의 원소를 넣고 row,col을 바꿔주기
자 그럼 할일은 두가지 입니다.

1. row, col 바꿔주기

: 못하면 안됩니다;;

2. 적절한 위치에 넣어주기

네 이부분이 관건이 됩니다.

적절한 위치에 어떻게 넣지? 라고 생각하면 우리가 선택한 원소가 갈 장소가 적힌 배열(row_start[])을 만들어서 해결해봅시다.

그럼 자연스럽게 이 배열은 어떻게 만들지? 라고 생각 할 수 있습니다.

코딩이 아니라 머리로 넣을 장소를 생각해봅시다. 머리로 생각 할 줄 알아야 코드를 짤 수 있으니까요

같은 색은 A에서 같은 col을 의미합니다. 위 방식대로 우리는 쭈욱~ 채워나갈수 있습니다.
그리고 이걸 보면 A에서 같은 각col 종류의 갯수들을 알면 row_start[]을 알 수 있겠네? 라는 생각을 할 수 있습니다.

구체적으로는
row_start[i]의 값은 row_start[i-1]에서 그 i-1에 해당하는 A의 col갯수를 더하면 알 수 있습니다.
그리고row_start[0]은 0번째 즉 첫번째니까 바로 1을 넣을 수 있습니다.

예를 들어 우리가 A의 열 1이 들어갈 row_start[1]을 구하고 싶다면
row_start[0]에서 A의 열중 0의 갯수를 구하고 더하면 됩니다.

그 다음부터는 위의 규칙을 적용하면 차례로 row_start[i]를 구할수 있겠죠?

자 그렇다면 다음으로 구해야할 것들은 해당하는 A의 col갯수들이담긴 배열(col_count)입니다.
이건 A에 있는 col들을 i=1 ~ 8 까지 전체를 세어서 col_count[A[i].col]++을 하면 되겠죠?

이제 구상이 끝났으니 코드를 조져봅시다.

4. 코드

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
#include <stdio.h>
#include <stdlib.h>
#define size 9
#define MAX_COL 6
#define MAX_ROW 6
/*
term: 행렬의 행,열 그리고 해당 값을 저장
*/
typedef struct term {
    int row;
    int col;
    int value;
}term;
term *transposing(term A[]);
int main(void) {
    term A[9= { { 6,6,8 },{ 0,0,15 },{ 0,3,22 },{ 0,5,-15 },{ 1,1,11 },{ 1,2,3 },{ 2,3,-6 },{ 4,0,91 },{ 5,2,28 } };
    term *= transposing(A);
    system("pause");
}
term *transposing(term A[]) {
    //setting 
//처음 부분이니 여러가지를 선언해서 세팅을 해줍시다.
// 위의 col_Count는 여기서 rowTerms로 표현했습니다.
    int rowTerms[MAX_COL] = { 0 }, startPoint[MAX_COL] = { 0 };
    term B[size];
    B[0].col = A[0].row; B[0].row = A[0].col; B[0].value = A[0].value;
    //get rowTerm
    for (int i = 1; i < size; i++) {
        rowTerms[A[i].col]++;
    }
    //get startpoint
    startPoint[0= 1;
    for (int i = 1; i <MAX_COL; i++) {
        startPoint[i] = startPoint[i - 1+ rowTerms[i - 1];
    }
    //transport to B
    for (int i = 1; i < size; i++) {
        int j = startPoint[A[i].col]++;
        B[j].row = A[i].col;
        B[j].col = A[i].row;
        B[j].value = A[i].value;
    }
    return B;
}a
cs

+ Recent posts