트리

  • 부모 노드 밑에 여러 자식 노드가 연결되고, 자식 노드 각각에 다시 자식 노드가 연결되는 재귀적 형태의 자료구조

  • 단, 자식 노드의 자식이 부모로 연결되는 경우는 보통 트리로 인정되지 않음

  • rooted tree에서는 여러가지 용어를 정의

    • 높이(height)는 루트의 높이를 0으로 하고, 자식의 높이를 원래 노드보다 1 큰 것으로 정의
    • 말단 노드(leaf node)의 정의는 자식이 없는 노드
  • unrooted tree에서는 차수가 1인 노드로 말단 노드를 정의

  • Tree 관련 용어

    • 노드(node): 트리를 구성하는 기본 원소
    • 루트 노드(root node/root): 트리에서 부모가 없는 최상위 노드, 트리의 시작점
    • 부모 노드(parent node): 루트 노드 방향으로 직접 연결된 노드
    • 자식 노드(child node): 루트 노드 반대방향으로 직접 연결된 노드
    • 형제 노드(siblings node): 같은 부모 노드를 갖는 노드들
    • 리프 노드(leaf node/leaf/terminal node): 자식이 없는 노드
    • 경로(path): 한 노드에서 다른 한 노드에 이르는 길 사이에 있는 노드들의 순서
    • 길이(length): 출발 노드에서 도착 노드까지 거치는 노드의 갯수
    • 깊이(depth): 루트 경로의 길이
    • 레벨(level): 루트 노드(1) 로 부터 노드까지 연결된 엣지의 수의 합
    • 높이(height): 가장 긴 루트 경로의 길이
    • 차수(degree): 각 노드의 자식의 갯수
    • 트리의 차수(degree of tree): 트리의 최대 차수
    • 크기(size): 노드의 개수
    • 너비(width): 가장 많은 노드를 갖고 있는 레벨의 크기
    • Tree의 응용
    • 이진트리(Binary Tree)
    • 이진탐색트리(Binary Search Tree, BST)
    • 스레드 이진 트리
    • 힙(Heap)
    • B-tree
    • 포레스트(Forest)
    • 트라이(Trie)

자료구조

  • Tree 인터페이스 작성
    • search 추상메서드와 insert 추상메서드 작성
    • Comparable 인터페이스 타입의 원소를 저장할 수 있도록 제네릭 생성
    • Node 클래스를 자식으로 갖는 또 다른 재귀 타입
public interface Tree<E extends Comparable> {
    boolean search(E toFind);
    void insert(E toInsert);
}
  • Node 클래스 작성

    • 데이터와 left, right Node 클래스를 재귀 타입으로 갖는 클래스
    • constructor, Tree 인터페이스로부터 search, insert 메서드를 구현
public class Node<E extends Comparable> implements Tree<E> {

    private E value;
    private Tree<E> left;
    private Tree<E> right;

    public Node(E value, Tree<E> left, Tree<E> right) {
        this.value = value;
        this.left = left;
        this.right = right;
    }

    public E getValue() { return value; }
    public Tree<E> getLeft() { return left; }
    public Tree<E> getRight() { return right; }
    public void setLeft(Tree<E> left) { this.left = left; }
    public void setRight(Tree<E> right) { this.right = right; }

    @Override
    public boolean search(E toFind) {
        // right element 검색
        if (toFind.equals(value)) return true;

        /* 
         * toFind를 value와 비교하여 같으면 0, '>' 는 양수, '<'는 음수
         * 음수일 경우 left Node를 검색
         * 같거나 양수일 경우 right Node를 검색
         */
        if (toFind.compareTo(value) < 0) { return left.search(toFind); }

        // 위 조건문에서 양수일 경우 right Node를 검색
        return right.search(toFind);
    }

    @Override
    public void insert(E toInsert) {
        /* 
         * toInsert 값을 value값과 비교하여 같으면 0, '>' 는 양수, '<'는 음수
         * 음수일 경우 left Node에 삽입
         * 같거나 양수일 경우 right Node에 삽입
         */
        if (toInsert.compareTo(value) < 0) { left.insert(toInsert);
        } else { right.insert(toInsert); }

    }
}
  • Leaf 클래스
public class Leaf<E extends Comparable> implements Tree<E> {

    private final Node<E> parent;

    public Leaf(Node<E> parent) {
        this.parent = parent;
    }

    @Override
    public boolean search(E toFind) {
        return false;
    }

    @Override
    public void insert(E toInsert) {
        /*
         * toInsert 값을 상위노드 값과 비교, 같으면 0, '>' 는 양수, '<'는 음수
         * 상위노드의 left 노드에 새로 Node 삽입
         */
        if (toInsert.compareTo(parent.getValue()) < 0)
            parent.setLeft(new Node<>(toInsert, new Leaf<>(parent), new Leaf<>(parent)));
        else
            parent.setRight(new Node<>(toInsert, new Leaf<>(parent), new Leaf<>(parent)));
    }
}

이진트리 검색 알고리즘

  • 단순 트리 구조

    • search를 통해 값이 있는지 확인
    • insert를 통해 상위노드와 비교하여 값이 작으면 left 노드에 삽입, 값이 같거나 클 경우 right 노드에 삽입
public class SimpleTree<E extends Comparable> {
    private E value;
    private SimpleTree<E> left;
    private SimpleTree<E> right;

    public SimpleTree(E value, SimpleTree<E> left, SimpleTree<E> right) {
        this.value = value;
        this.left = left;
        this.right = right;
    }

    public E getValue() { return value; }
    public SimpleTree<E> getLeft() { return left; }
    public SimpleTree<E> getRight() { return right; }

    public boolean search(final E toFind) {
        // 찾으려는 값이 Tree에 있는 것을 확인
        if (toFind.equals(value)) return true;

        /* 
         * toFind를 value와 비교하여 같으면 0, '>' 는 양수, '<'는 음수
         * left 노드가 존재하고, 음수일 경우 left Node를 검색
         * 같거나 양수일 경우 right Node를 검색
         */
        if (toFind.compareTo(value) < 0 && left != null) return left.search(toFind);

        return right != null && right.search(toFind);
    }

    public void insert(final E toInsert) {
        // 입력할 값을 비교, 같으면 0, '>' 는 양수, '<'는 음수
        if (toInsert.compareTo(value) < 0) {
            // 입력 값이 작고, left Node가 null일 경우 새로 생성
            if (left == null) left = new SimpleTree<>(toInsert, null, null);
            // 입력 값이 작고, left Node가 null이 아닐 경우 삽입
            else left.insert(toInsert);

        } else {
            // 입력 값이 같거나 크고, right Node 값이 null인 경우 새로 생성
            if (right == null) right = new SimpleTree<>(toInsert, null, null);
            // 입력 값이 같거나 크고, right Node 값이 null이 아닌 경우 새로 생성
            else right.insert(toInsert);

        }
    }
}
  • 단순트리 검색 테스트
    • 단순 트리 구조의 insert, search 테스트
public class SimpleTreeTest {

    private SimpleTree<Integer> root;

    @Before
    public void createSampleTree() {
        final SimpleTree<Integer> t1 = new SimpleTree<>(1, null, null);
        final SimpleTree<Integer> t5 = new SimpleTree<>(5, null, null);

        final SimpleTree<Integer> t3 = new SimpleTree<>(5, t1, t5);
        final SimpleTree<Integer> t9 = new SimpleTree<>(9, null, null);
        /*     7
         *   5   9
         * 1 5
         */
        root = new SimpleTree<>(7, t3, t9);

    }

    @Test
    public void search() {
        assertTrue(root.search(7));
        assertFalse(root.search(70));
    }

    @Test
    public void insert() {
        root.insert(10);
        assertTrue(root.search(10));
        /*     7
         *   5   9
         * 1 5     10
         */
        assertEquals(Integer.valueOf(10), root.getRight().getRight().getValue());
    }

    @Test
    public void createTree() {
        final SimpleTree<Integer> root = new SimpleTree<>(7, null, null);
        root.insert(3);
        root.insert(9);
        root.insert(10);
        assertTrue(root.search(10));
        /*     7
         *   3   9
         *            10
         */
        assertEquals(Integer.valueOf(10), root.getRight().getRight().getValue());
    }

    @Test
    public void linkedListTree() {
        final SimpleTree<Integer> root = new SimpleTree<>(1, null, null);
        root.insert(2);
        root.insert(3);
        root.insert(4);
        root.insert(5);

        /*     1
         *       2
         *            3
         *              4
         *                5
         */
        assertEquals(Integer.valueOf(1), root.getValue());
        assertEquals(Integer.valueOf(2), root.getRight().getValue());
        assertEquals(Integer.valueOf(3), root.getRight().getRight().getValue());
        assertEquals(Integer.valueOf(4), root.getRight().getRight().getRight().getValue());
        assertEquals(Integer.valueOf(5), root.getRight().getRight().getRight().getRight().getValue());
    }
}
  • Complex 트리 검색
public class ComplexTreeTest {

    @Test
    public void insert() {
        final Node<Integer> root = new Node<>(7, null, null);
        root.setLeft(new Leaf<>(root));
        root.setRight(new Leaf<>(root));

        root.insert(3);
        assertTrue(root.search(3));
        assertFalse(root.search(13));
    }
}

코드 참고 - 자바 프로그래밍 면접 이렇게 준비한다

  • 데이터가 들어오는 위치가 가장 뒤에 있고, 데이터가 나가는 위치가 가장 앞에 있는 자료구조
  • 먼저 들어온 데이터가 가장 먼저 삭제되는 선입선출(First In First Out, FIFO) 자료구조
  • 어떠한 작업 / 데이터를 순서대로 실행/사용하기 위해서 대기시킬 때 사용
  • 큐 응용
    • Queue, PriorityQueue, Deque

코드 설계

  • Queue의 인터페이스의 추상 메서드 확인
  • LinkedList에서 구현 코드 확인
public interface Queue<E> extends Collection<E> {
  boolean add(E paramE);

  boolean offer(E paramE);

  E remove();

  E poll();

  E element(); // 해당 큐의 맨 앞에 있는(제일 먼저 저장된) 요소를 반환

  E peek(); // 해당 큐의 맨 앞에 있는(제일 먼저 저장된) 요소를 반환
}

코드 구현

  • Queue 인터페이스의 핵심 메서드를 LinkedList에서 확인
public class LinkedList<E> extends AbstractSequentialList<E> implements List<E>, Deque<E>, Cloneable, Serializable {
    transient int size = 0;
    transient Node<E> first;
    transient Node<E> last; 
    private static final long serialVersionUID = 876323262645176354L;
    public LinkedList() {}

    // 새로운 Node를 만들어 기존의 Queue의 마지막 node의 next에 연결, Queue의 size + 1
    public boolean add(E paramE) {
        linkLast(paramE);
        return true;
    }
    // add와 같은 기능을 함
    public boolean offer(E paramE) { 
        return add(paramE); 
    }

    // Queue의 index확인 후에 link가 연결된 부분을 null을 대입하여 link 연결을 해제
    public E remove(int paramInt) {
        checkElementIndex(paramInt);
        return unlink(node(paramInt));
    }

    // Queue의 first값을 확인 후 link를 제거한 뒤에 해당 Node를 반환
    public E poll() {
        Node<E> node = this.first;
        return (node == null) ? null : unlinkFirst(node);
    }

    // Queue의 first값을 확인하여 값을 조회
    public E element() { 
        return getFirst(); 
    }

    // Queue의 first값을 확인하여 값을 조회
    public E peek() {
        Node<E> node = this.first;
        return (node == null) ? null : node.item;
    }

    public E getFirst() {
        Node<E> node = this.first;
        if (node == null)
            throw new NoSuchElementException(); 
        return node.item;
    }

    void linkLast(E paramE) {
        Node<E> node1 = this.last;
        Node<E> node2 = new Node<>(node1, paramE, null);
        this.last = node2;
        if (node1 == null) {
            this.first = node2;
        } else {
            node1.next = node2;
        } 
        this.size++;
        this.modCount++;
    }

    E unlink(Node<E> paramNode) {
        E e = paramNode.item;
        Node<E> node1 = paramNode.next;
        Node<E> node2 = paramNode.prev;
        if (node2 == null) {
            this.first = node1;
        } else {
            node2.next = node1;
            paramNode.prev = null;
        } 
        if (node1 == null) {
            this.last = node2;
        } else {
            node1.prev = node2;
            paramNode.next = null;
        } 
        paramNode.item = null;
        this.size--;
        this.modCount++;
        return e;
    }

    private E unlinkFirst(Node<E> paramNode) {
        E e = paramNode.item;
        Node<E> node = paramNode.next;
        paramNode.item = null;
        paramNode.next = null;
        this.first = node;
        if (node == null) {
            this.last = null;
        } else {
            node.prev = null;
        } 
        this.size--;
        this.modCount++;
        return e;
    }

    private void checkElementIndex(int paramInt) {
        if (!isElementIndex(paramInt))
            throw new IndexOutOfBoundsException(outOfBoundsMsg(paramInt)); 
    }
    ...
}

스택(Stack)

  • 스택은 리스트의 한쪽 끝으로만 자료의 삽입, 삭제 작업이 이루어지는 자료구조
  • 스택은 가장 나중에 삽입된 자료가 가장 먼저 삭제되는 후입선출(LIFO)방식으로 자료를 처리

코드 설계

  • Stack 클래스의 핵심 메서드 확인
public E push(E paramE) // 해당 스택의 제일 상단에 전달된 요소를 삽입

public synchronized E pop() // 해당 스택의 제일 상단에 있는(제일 마지막으로 저장된) 요소를 반환, 해당 요소를 스택에서 제거

public synchronized E peek() // 해당 스택의 제일 상단에 있는(제일 마지막으로 저장된) 요소를 반환

public boolean empty() // 해당 스택이 비어 있으면 true를, 비어 있지 않으면 false를 반환

// 해당 스택에서 전달된 객체가 존재하는 위치의 인덱스를 반환
// 이때 인덱스는 제일 상단에 있는(제일 마지막으로 저장된) 요소의 위치부터 0이 아닌 1부터 시작
public synchronized int search(Object paramObject) 

코드 구현

  • Stack 클래스 확인
public class Stack<E> extends Vector<E> {
    private static final long serialVersionUID = 1224463164541339165L;

    public E push(E paramE) {
        addElement(paramE);
        return paramE;
    }

    public synchronized E pop() {
        int i = size();
        E e = peek();
        removeElementAt(i - 1);
        return e;
    }

    public synchronized E peek() {
        int i = size();
        if (i == 0)
            throw new EmptyStackException(); 
        return elementAt(i - 1);
    }

    public boolean empty() { return (size() == 0); }

    public synchronized int search(Object paramObject) {
        int i = lastIndexOf(paramObject);
        return (i >= 0) ? (size() - i) : -1;
    }
}
// solution(new int[] { 6, 9, 5, 7, 4 });         
3    7    5    9    6    
// solution(new int[] { 3, 9, 9, 3, 5, 7, 2 });   
3    7    5    3    9    9    3    
// solution(new int[] { 1, 5, 3, 6, 7, 6, 5, 2 });
3    5    6    7    6    3    5    1    

계수정렬

  • 각 숫자가 몇개 있는지 갯수를 세어서 다른 한 배열에 저장한 후에 정렬을 하는 방식
  • 카운팅 정렬은 가장 큰 데이터에 따라 효율이 좌지우지된다.
  • 특정 데이터의 개수를 데이터의 값에 대응하는 위치에 저장
  • 자신의 위치에서 앞에 있던 값을 모두 더한 배열을 생성
  • 거기서 데이터가 들어가야 할 위치를 찾아내는 정렬 알고리즘이다.

코드 설계

  1. 각 요소의 범위가 1 ~ 7까지인 배열(A)을 읽기
  2. 요소의 값의 빈도수(frequency)를 저장
  3. 빈도 수를 기준으로 빈도수가 큰 순으로 정렬

코드 구현

    // int[] A = new int[] {1, 5, 4, 6, 5, 7, 1, 4, 4, 2};

    public int[] solution(int[] A) {
        int[] answer = {};

        // key : value로 저장할 수 있는 HashMap 생성
        Map<Integer, Integer> countMap = new HashMap<>();

        // 입력 값 출력
        for(int a : A) {
            countMap.put(a, countMap.getOrDefault(a, 0) + 1);
        }

        // key 값 가져와서 sorting
        List<Integer> sorted = new ArrayList<>(countMap.keySet());
        // 내림차순 정렬 (빈도수가 높은 순으로 정렬)
        Collections.sort(sorted, new Comparator<Integer>() {
            @Override
            public int compare(Integer o1, Integer o2) {
                return countMap.get(o2).compareTo(countMap.get(o1));
            }
        });

        answer = new int[sorted.size()];
        for(int i = 0 ; i < sorted.size() ; i++) {
            answer[i] = sorted.get(i);
        }

        return answer;
    }
  • 과정 값 및 결과 값
// key : frequency
4[3]    1[2]    5[2]    2[1]    6[1]    7[1]
// answer
4    1    5    2    6    7

분석

  • 모든 데이터의 크기 범위를 메모리 상에 표현할 수 있다면 복잡도 O(N)

힙 정렬(Heap Sort)

  • 병합 정렬, 퀵 정렬만큼 빠른 정렬 알고리즘

  • 가장 큰 원소를 찾는 데 최적화된 형태의 이진 트리

  • 힙의 가장 중요한 원칙

    • 부모 노드가 가진 원소는 항상 자식 노드가 가진 원소의 크기보다 크다.
    • 트리에서 가장 큰 원소는 항상 트리의 루트에 존재한다.
  • 힙의 모양 규칙

    • 마지막 레벨을 제외한 모든 레벨에 노드가 꽉 차 있어야 한다.
    • 마지막 레벨에 노드가 있을 때는 항상 가장 왼쪽부터 순서대로 채워져 있어야 한다.
  • 힙 트리 구조(Heap Tree Structure)를 이용하는 정렬 방식

    • 크기가 n인 완전 이진트리
    • 힙(Heap) 이란 최소값이나 최대값을 빠르게 찾아내기 위해 완전 이진트리를 기반으로 하는 자료구조
    • 최댓값이란 부모 노드의 키 값이 자식 노드의 키 값보다 항상 큰 힙
    • 최솟값이란 부모 노드의 키 값이 자식 노드의 키 값보다 항상 작은 힙
    • 키 값의 대소 관계는 오직 부모 노드와 자식 노드 간에만 성립하며, 형제 사이에는 대소 관계가 정해지지 않는다.
  • 힙을 사용할 경우 새 원소를 추가하는 연산과 가장 큰 원소를 꺼내는 연산을 모두 O(logN) 시간에 수행할 수 있다.

코드 설계

  • 힙 구조의 규칙
    • A[i]에 대응되는 노드의 왼쪽 자손은 A[2 * i + 1]에 대응된다.
    • A[i]에 대응되는 노드의 오른쪽 자손은 A[2 * i + 2]에 대응된다.
    • A[i]에 대응되는 노드의 부모는 A[(i-1)/2]에 대응된다.(나눗셈 결과는 내림한다.)
  • 정렬 과정
    • 마지막 노드의 부모 노드부터 시작하여 root 노드순으로 검색한다.
    • 정렬의 과정은 최댓값을 검증하여 정상적인 힙 구조인지 확인, 아닐 경우 child와 parent를 변경

코드 구현

    private void swap(int[] A, int pre, int post) {
        System.out.println("swap: \tA[" + pre + "]=" + A[pre] + " <> A[" + post + "]=" + A[post]);
        int tmp = A[pre];
        A[pre] = A[post];
        A[post] = tmp;
    }
    /**
     * 힙 정렬
     * @param A
     */
    private void heapSort(int[] A) {
        int aLength = A.length - 1; // 배열의 실 길이
        int parentIdx = aLength / 2;

        // 마지막 노드의 부모 노드부터 시작하여 root 노드 방향으로 거슬러 올라감
        for (int i = parentIdx; i >= 0; i--) maxHeapify(A, A.length, i);

        for(int i = aLength ; i > 0 ; i--) { // 원소가 하나 남을 때까지 반복
            swap(A, 0, i); // 배열의 마지막 요소 값을 루트로 변경
            maxHeapify(A, i, 0);
        }
    }

    /**
     * 배열 A를 힙으로 만드는 메서드
     * 루트에 있는 가장 큰 값을 빼내어 마지막 요소와 바꾸고,
     * 배열의 나머지 부분을 다시 힙으로 만드는 과정을 반복하여 정렬을 수행

     * @param A
     * @param length
     * @param i 
     */
    private void maxHeapify(int[] A, int length, int i) {
        int parent = i; // parent index
        int leftChild = (i * 2) + 1; // leftChild index
        int rightChild = (i * 2) + 2; // rightChild index


        // leftChild가 parent보다 클 경우
        if(leftChild < length && A[parent] < A[leftChild]) {
            parent = leftChild;
        }

        // rightChild가 parent보다 클 경우
        if(rightChild < length && A[parent] < A[rightChild]) {
            parent = rightChild;
        }

        if(i != parent) {
            swap(A, parent, i);
            maxHeapify(A, length, parent-1);
        }
    }
  • 과정
             01
            /\
     04              05
    /\            /\
 07      02      06      03
/            
08

A[7]=8 < swap > A[3]=7
             01
            /\
     04              05
    /\            /\
 08      02      06      03
/            
07

A[5]=6 < swap > A[2]=5
             01
            /\
     04              06
    /\            /\
 08      02      05      03
/            
07

A[3]=8 < swap > A[1]=4
             01
            /\
     08              06
    /\            /\
 04      02      05      03
/            
07

A[1]=8 < swap > A[0]=1
             08
            /\
     01              06
    /\            /\
 04      02      05      03
/            
07

A[0]=8 < swap > A[7]=7
             07
            /\
     01              06
    /\            /\
 04      02      05      03
/            
08

A[0]=7 < swap > A[6]=3
             03
            /\
     01              06
    /\            /\
 04      02      05      07
/            
08

A[2]=6 < swap > A[0]=3
             06
            /\
     01              03
    /\            /\
 04      02      05      07
/            
08

A[3]=4 < swap > A[1]=1
             06
            /\
     04              03
    /\            /\
 01      02      05      07
/            
08

A[5]=5 < swap > A[2]=3
             06
            /\
     04              05
    /\            /\
 01      02      03      07
/            
08

A[0]=6 < swap > A[5]=3
             03
            /\
     04              05
    /\            /\
 01      02      06      07
/            
08

A[2]=5 < swap > A[0]=3
             05
            /\
     04              03
    /\            /\
 01      02      06      07
/            
08

A[0]=5 < swap > A[4]=2
             02
            /\
     04              03
    /\            /\
 01      05      06      07
/            
08

A[1]=4 < swap > A[0]=2
             04
            /\
     02              03
    /\            /\
 01      05      06      07
/            
08

A[0]=4 < swap > A[3]=1
             01
            /\
     02              03
    /\            /\
 04      05      06      07
/            
08

A[2]=3 < swap > A[0]=1
             03
            /\
     02              01
    /\            /\
 04      05      06      07
/            
08

A[0]=3 < swap > A[2]=1
             01
            /\
     02              03
    /\            /\
 04      05      06      07
/            
08

A[1]=2 < swap > A[0]=1
             02
            /\
     01              03
    /\            /\
 04      05      06      07
/            
08

A[0]=2 < swap > A[1]=1
             01
            /\
     02              03
    /\            /\
 04      05      06      07
/            
08

분석

  • 힙 정렬은 선택 정렬을 응용한 알고리즘
  • 첫 요소를 꺼내는 것만으로 가장 큰 값이 구해지므로 첫 요소를 꺼낸 다음 나머지 요소를 다시 힙으로 만들어야 그 다음에 꺼낼 요소도 가장 큰 값을 유지한다.
  • 따라서 단순 선택 정렬에서 가장 큰 요소를 선택할 때의 시간 복잡도 O(n)의 값을 한 번에 선택할 수 있어 O(1)로 줄일 수 있다.
  • 복잡도 분석
    • 힙으로 만드는 작업의 시간 복잡도 O(logn)
    • 힙으로 만드는 작업을 요소의 개수 만큼 반복하므로 O(n logn)

퀵 정렬(Quick Sort)

  • Divide and Conquer 알고리즘
  • 하나의 문제를 작은 문제로 분할하여 해결해나가는 패러다임
  • 재귀 알고리즘을 사용
  • 안정적인 알고리즘은 아님

코드 설계

  • Pivot이라는 값을 기준으로 파티셔닝(피벗을 선택하고 재정렬하는 프로세스)작업을 한다.

  • Pivot을 기준으로 좌측에는 pivot보다 작은 값, 우측에는 보다 큰 값을 놓는다.

  • Pivot을 기준으로 양쪽의 두 배열(세그먼트)들은 하위 문제로 놓고 재귀를 통해 다시 pivot 작업을 수행

  • peudo-code

QuickSort(A, start, end) {
    if(start < end) {
        pIndex <- partition(A, start, end) // 파티션 작업 & pivot index 설정
        QuickSort(A, start, pIndex - 1) // 분할 작업
        QuickSort(A, pIndex + 1, end)
    }
}
partition(A, start, end) {
    pivot <- A[end]
    pIndex <- start
    for i <- start to end -1 {
        if (A[i] <= pivot) {
            swap(A[i], A[pIndex])
            pIndex <- pIndex + 1
        }
    }
    swap(A[pIndex], A[end])
    return pIndex
}

코드 구현

    /**
     * 1. pivot 설정
     * 2. partition 작업
     * 3. 분할 정복
     * @param A
     * @param start
     * @param end
     */
    private void quickSort(int[] A, int start, int end) {
        if (start < end) {
            int pIndex = partition(A, start, end);
            quickSort(A, start, pIndex - 1);
            quickSort(A, pIndex + 1, end);
        }
    }
    /**
     * 파티션 나누기
     * @param A
     * @param start
     * @param end
     * @return
     */
    private int partition(int[] A, int start, int end) {
        int pivot = A[end];
        int pIndex = start;

        for (int i = start; i < end; i++) {
            if (A[i] <= pivot) {
                // pivot 값보다 작거나 같은 경우 swap
                int tmp = A[i];
                A[i] = A[pIndex];
                A[pIndex] = tmp;

                pIndex++;
            }
        }
        // pivot 값을 센터로 이동
        int tmp = A[pIndex];
        A[pIndex] = A[end];
        A[end] = tmp;

        return pIndex;
    }
  • 과정 출력

Pivot( 4 ) 값 설정 후 파티션 나누기: 
7    2    1    6    8    5    3    4    
2    7    1    6    8    5    3    4    
2    1    7    6    8    5    3    4    
2    1    3    6    8    5    7    4    
파티션 과정 완료 후 Pivot(4) 이동
2    1    3    4    8    5    7    6    

Pivot( 3 ) 값 설정 후 파티션 나누기: 
2    1    3    4    8    5    7    6    
2    1    3    4    8    5    7    6    
2    1    3    4    8    5    7    6    
파티션 과정 완료 후 Pivot(3) 이동
2    1    3    4    8    5    7    6    

Pivot( 1 ) 값 설정 후 파티션 나누기: 
2    1    3    4    8    5    7    6    
파티션 과정 완료 후 Pivot(1) 이동
1    2    3    4    8    5    7    6    

Pivot( 6 ) 값 설정 후 파티션 나누기: 
1    2    3    4    8    5    7    6    
1    2    3    4    5    8    7    6    
파티션 과정 완료 후 Pivot(6) 이동
1    2    3    4    5    6    7    8    

Pivot( 8 ) 값 설정 후 파티션 나누기: 
1    2    3    4    5    6    7    8    
1    2    3    4    5    6    7    8    
파티션 과정 완료 후 Pivot(8) 이동
1    2    3    4    5    6    7    8    

분석

  • 복잡도 분석
    • Best & Average case O(nlogn)
    • Worst case O(n^2)
  • O(n^2) 임에도 적절하게 빠른 정렬

병합정렬(Merge Sort)

  • 선택, 버블, 삽입 정렬보다 빠른 알고리즘
  • Divide and Conquer 전략을 활용하는 알고리즘
  • 분할 할 때, 배열이 절반으로 줄어들어 시간 복잡도는 O(logN)
  • 합병 할 때, 모든 값을 비교해야 하기 때문에 시간 복잡도는 O(N)
  • 두 개의 배열을 병합할 때 병합 결과를 담을 배열이 추가적으로 필요 (Space complexity: Not in-place)
  • 최악의 경우 O(nlogn)인 알고리즘
  • 재귀에 대한 이해가 있어야 한다.

코드 설계

  • 하나의 큰 문제를 두 개의 작은 문제로 분할
  • 더 이상 나눌 수 없을 때 병합
  • 병합 시 정렬을 하면서 병합하여 정렬을 완성
0: 2 4 1 6 8 5 3 7

1: 2 4 1 6        8 5 3 7

2: 2 4        ||        1 6        ||        8 5        ||        3 7

3: 2    ||    4    ||    1    ||    6    ||    8    ||    5    ||    3    ||    7

4: 2 4        ||        1 6        ||        5 8     ||        3 7

5: 1 2 4 6        ||        3 5 7 8

6: 1 2 3 4 5 6 7 8
  • pseudo-code
Merge(L, R, A) {
    nL <- length(L)
    nR <- length(R)
    i <- 0, j <- 0, k <- 0
    while(i < nL && j < nR) {
        if( L[i] <= R[j] ) {
            A[k] <- L[i]
            i <- i + 1
        } else {
            A[k] <- R[j]
            j <- j + 1
        }
        k <- k + 1
    }
    while(i < nL) {
        A[k] <- L[i]
        i <- i + 1
        k <- k + 1
    }
    while(j < nR) {
        A[k] <- R[j]
        j <- j + 1
        k <- k + 1
    }
}    
MergeSort(A) {    
    n <- length(A)
    // 기저조건: 더 이상 나눌 수 없는 배열의 길이인 경우
    if (n < 2) return
    mid <- n / 2

    left <- array.size(mid)
    right <- array.size(n - mid)

    for i <- 0 to mid - 1
        left[i] <- A[i]
    for i <- mid to n - 1
        right[i - mid] <- A[i]

    MergeSort(left)
    MergeSort(right)
    Merge(left, right, A)    
}

코드 구현

    public void mergeSort(int[] A) {
        int n = A.length;
        if (n < 2) return; // 배열의 요소가 1개인 경우 리턴

        int mid = n / 2; // 배열의 중간 요소

        int[] left = new int[mid]; // 좌측 배열
        int[] right = new int[n - mid]; // 우측 배열

        for (int i = 0; i < mid; i++) {
            left[i] = A[i];
        }
        for (int i = mid; i < n; i++) {
            right[i - mid] = A[i];
        }
        mergeSort(left);
        mergeSort(right);
        merge(left, right, A);
    }

    private void merge(int[] L, int[] R, int[] A) {
        int nL = L.length;
        int nR = R.length;

        int i = 0 , j = 0 , k = 0;

        while(i < nL && j < nR) {
            if(L[i] <= R[j]) {
                A[k] = L[i];
                i++;
            } else {
                A[k] = R[j];
                j++;
            }
            k++;
        }
        while(i < nL) { // L 배열에 A에 넣지 않은 여분이 있는 경우 
            A[k] = L[i];
            i++;
            k++;
        }
        while(j < nR) { // R 배열에 A에 넣지 않은 여분이 있는 경우 
            A[k] = R[j];
            j++;
            k++;
        }
        // 과정 출력
    }
  • 과정 출력 (병합되는 과정에 출력)
2    4    
1    6    
1    2    4    6    
5    8    
3    7    
3    5    7    8    
1    2    3    4    5    6    7    8    

분석

  • 복잡도 분석
    • 순환 호출의 깊이: k = logN
    • 각 합병 단계의 비교 연산: 최대 N번
    • 순환 호출의 깊이 만큼 합병 단계 * 각 합병 단계의 비교연산: O(N * logN)
  • 공간 복잡도
    • 공간의 소비는 목록의 요소의 수에 비례한다.

삽입정렬(Insertion Sort)

  • 임의 값을 적절한 자리에 삽입하여 정렬하는 방식

코드 설계

  • 배열의 가장 왼쪽부터 정렬한다는 기준을 잡기
  • 변경 전 값을 기준 값과 비교하여 적절한 위치로 변경

코드 구현

    private int[] insertionSort(int[] A, int n) {
        int[] ret;

        for(int i = 1 ; i < n ; i++) {
            int value = A[i]; // 배열의 두 번째 값을 기준으로 시작
            int hole = i;

            while(hole > 0 && (A[hole-1] > value)) { // A[0] > A[1] 부터 비교하기를 시작
                // 과정 출력 not swap
                A[hole] = A[hole - 1];
                hole--;

            }
            A[hole] = value;
            // 과정 출력 swap
        }

        ret = A;
        return ret;
    }
  • 과정
1:    2    7    4    1    5    3    : 1    

2:    2    7    4    1    5    3    : 2    
2:    2    4    7    1    5    3    : 1    

3:    2    4    7    1    5    3    : 3    
3:    2    4    7    7    5    3    : 2    
3:    2    4    4    7    5    3    : 1    
3:    1    2    4    7    5    3    : 0    

4:    1    2    4    7    5    3    : 4    
4:    1    2    4    5    7    3    : 3    

5:    1    2    4    5    7    3    : 5    
5:    1    2    4    5    7    7    : 4    
5:    1    2    4    5    5    7    : 3    
5:    1    2    3    4    5    7    : 2    
  • 결과 확인
    int[] A = {2, 7, 4, 1, 5, 3};
    int[] except = {1, 2, 3, 4, 5, 7};
    int n = A.length;

    @Test
    public void testInsert() {
        assertThat(insertionSort(A, n), is(except));
    }

분석

  • 복잡도 분석
    • Bast-Case O(n)
    • Average-Case O(n^2)
    • Worst-Case O(n^2)
  • 삽입 정렬은 선택정렬, 버블정렬보다 효율적인 알고리즘이다.
  • 직관적인 알고리즘

+ Recent posts