💻
신입 개발자 인터뷰 대비 - 1. 자료구조
April 30, 2024
신입 개발자 인터뷰 대비 : 1. 자료구조
자료구조
- Array, ArrayList, LinkedList의 차이를 알고, 직무 언어로 모듈 없이 구현할 수 있다.
- Array : 배열은 크기가 고정되고 데이터가 연속적으로 메모리 상에 들어가게 된다. 이는 인덱스를 통해 접근하므로 어떤 위치라도 O(1)의 시간 복잡도로 접근이 가능하다는 의미를 가지며, 일반적인 배열이므로 구현은 생략 가능
- ArrayList : 해당 데이터 구조는 Array 를 기반으로 하지만, 동적으로 크기를 변경할 수 있다는 차이를 가지며, 접근 시간은 Array와 동일하나, 크기 조정이나 데이터 이동, 중간 삽입 등에선 O(n)의 시간이 소요될 수 있다.
public class MyArrayList<T> { private Object[] elements; private int size = 0; public MyArrayList() { this.elements = new Object[10]; } public void add(T element) { if (size == elements.length) { increaseCapacity(); } elements[size++] = element; } private void increaseCapacity() { Object[] newElements = new Object[elements.length * 2]; System.arraycopy(elements, 0, newElements, 0, elements.length); elements = newElements; } public int getSize() { return size; } public T get(int index) { if (index < 0 || index >= size) { throw new IndexOutOfBoundsException("Index: " + index + ", Size: " + size); } return (T) elements[index]; } } package array; public class MyArrayListMain { public static void main(String[] args) { MyArrayList<Integer> list = new MyArrayList<>(); list.add(1); list.add(2); list.add(3); list.add(4); for (int i = 0; i < list.getSize(); i++){ System.out.println(list.get(i)); } } }
3. LinkedList : 링크드리스트는 각 요소가 노드라는 형태로 구성되어, 다음 노드를 가리키는 방식으로 데이터를 저장한다. 이 구조는 삽입과 삭제가 O(1)이라는 장점을 가지나, 특정 인덱스에 접근하는데에는 O(n)의 시간 복잡도가 소요된다. 이러한 특징은 노드의 구조 상 연결이나 분할에선 연결 부분만 수정하면 되지만, 탐색의 경우 전체를 탐색해서 그 중에 원하는 노드를 발견해야 하기 때문이다. ```java public class MyLinkedList<T> { private Node<T> head; private int size; private static class Node<T> { T data; Node<T> next; Node(T data) { this.data = data; } } public void add(T data) { Node<T> newNode = new Node<>(data); if (head == null) { head = newNode; } else { Node<T> current = head; while (current.next != null) { current = current.next; } current.next = newNode; } size++; } public T get(int index) { if (index < 0 || index >= size) { throw new IndexOutOfBoundsException("Index: " + index + ", Size: " + size); } Node<T> current = head; for (int i = 0; i < index; i++) { current = current.next; } return current.data; }
} ```
- Stack과 Queue에 대해 알고, 차이를 설명할 수 있다. 직무 언어로 모듈 없이 구현할 수 있다.
- stack :
- 특징 : LIFO 후입 선출의 구조를 가지며, 마지막에 삽입된 데이터가 가장 먼저 추출되고, push, pop, peek 등의 연산을 갖춘다. 호출된 순서대처리해야할 것을 먼저 처리하는, 함수 실행구조와 동일한 구조를 가진다.
- 구현 :
public class MyStack<T> { private Node<T> top; private static class Node<T> { private T data; private Node<T> next; Node(T data) { this.data = data; } } public void push(T data) { Node<T> node = new Node<>(data); node.next = top; top = node; } public T pop() { if (top == null) throw new EmptyStackException(); T data = top.data; top = top.next; return data; } public T peek() { if (top == null) throw new EmptyStackException(); return top.data; } public boolean isEmpty() { return top == null; }
- queue :
- 특징 : 선입선출의 구조를 갖추고 있으며, 삽입된 데이터가 가장 먼저 추출되는 형태를 가진다. 데이터의 특성 상 줄세워지게 되며, 일괄 요청을 받는 경우 해당 구조로 줄을 세워 처리하는 등에 사용될 수 있다.
- 구현 :
public class MyQueue<T> { private Node<T> head; private Node<T> tail; private static class Node<T> { private T data; private Node<T> next; Node(T data) { this.data = data; } } public void enqueue(T data) { Node<T> node = new Node<>(data); if (tail != null) { tail.next = node; } tail = node; if (head == null) { head = node; } } public T dequeue() { if (head == null) throw new NoSuchElementException(); T data = head.data; head = head.next; if (head == null) { tail = null; } return data; } public T peek() { if (head == null) throw new NoSuchElementException(); return head.data; } public boolean isEmpty() { return head == null; }
- stack :
- Tree와 Heap의 구조에 대해 알고, 설명할 수 있다. 직무 언어로 모듈 없이 구현할 수 있다.
-
Tree :
- 특징 : 트리는 계층적인 자료구조를 가지며, 노드가 구성요소고, 노드는 자식 노드를 가리키는 구조로 되어있고, 가장 최상 위에 있는 노드를 루트 노드라고 부르며, 노드 간의 상하 관계를 부모-자식 관계로 부른다.
- 구현 :
public class TreeNode { int value; TreeNode left; TreeNode right; public TreeNode(int value) { this.value = value; this.left = null; this.right = null; } public void add(int value) { if (value < this.value) { if (this.left != null) { this.left.add(value); } else { this.left = new TreeNode(value); } } else { if (this.right != null) { this.right.add(value); } else { this.right = new TreeNode(value); } } }
} ```
-
Heap :
- 특징 : Heap 은 완전 이진 트리를 기반으로 하는 자료구조이며, 최대 힙과, 최소 힙의 두 종류가 있다. 최대 힙은 부모 노드의 값이 자식 노드의 값보다 항상 크거나 같으며, 최소 힙은 부모 노드의 값이 자식 노드의 값보다 항상 작거나 같다. 중복된 값도 허용되며, 힙은 일종의 반 정렬 상태(느슨한 정렬 상태)를 가진다. 목적은 여러 개의 값들 중에서 최댓값이나 최솟값을 빠르게 찾아내기 위한 자료구조다.
- 구현 :
// MinHeap 버전은, 부모가 더 작거나 같아야 한다. public class MinHeap { private int[] Heap; private int size; private int maxsize;
public MinHeap(int maxsize) { this.maxsize = maxsize; this.size = 0; Heap = new int[this.maxsize + 1]; Heap[0] = Integer.MIN_VALUE; } private int parent(int pos) { return pos / 2; } private int leftChild(int pos) { return (2 * pos); } private int rightChild(int pos) { return (2 * pos) + 1; } private void swap(int fpos, int spos) { int tmp; tmp = Heap[fpos]; Heap[fpos] = Heap[spos]; Heap[spos] = tmp; } public void insert(int element) { Heap[++size] = element; int current = size; while (Heap[current] < Heap[parent(current)]) { swap(current, parent(current)); current = parent(current); } } public void minHeapify(int pos) { if (!isLeaf(pos)) { if (Heap[pos] > Heap[leftChild(pos)] || Heap[pos] > Heap[rightChild(pos)]) { if (Heap[leftChild(pos)] < Heap[rightChild(pos)]) { swap(pos, leftChild(pos)); minHeapify(leftChild(pos)); } else { swap(pos, rightChild(pos)); minHeapify(rightChild(pos)); } } } } private boolean isLeaf(int pos) { if (pos > (size / 2) && pos <= size) { return true; } return false; }
}
-
- Tree의 전위, 중위, 후위 순회 시 출력되는 노드 순서를 말할 수 있다. 트리 구현부터 순회 메소드까지 직무 언어로 모듈 없이 구현할 수 있다.
- 순회라는 것은 트리를 어떤 순서로 탐색하는 지를 말한다.
- 전위 순회(Pre-order Traversal): 루트를 먼저 방문하고 왼쪽 자식을 간 뒤 오른쪽 자식을 방문하는 구조
- 중위 순회(In-order Traversal): 왼쪽 자식을 먼저 방문 후, 루트를 거쳐 마지막으로 오른쪽 자식을 방문 하는 구조
- 후위 순회(Post-order Traversal): 왼쪽 자식, 오른쪽 자식을 방문 후에 루트를 방문하는 구조
public class TreeNode { int value; TreeNode left; TreeNode right; public TreeNode(int value) { this.value = value; this.left = null; this.right = null; } // 전위 순회 public void preOrder() { System.out.print(this.value + " "); if (this.left != null) { this.left.preOrder(); } if (this.right != null) { this.right.preOrder(); } } // 중위 순회 public void inOrder() { if (this.left != null) { this.left.inOrder(); } System.out.print(this.value + " "); if (this.right != null) { this.right.inOrder(); } } // 후위 순회 public void postOrder() { if (this.left != null) { this.left.postOrder(); } if (this.right != null) { this.right.postOrder(); } System.out.print(this.value + " "); } } public class BinaryTree { TreeNode root; public BinaryTree(int value) { root = new TreeNode(value); } public void add(int value) { addRecursive(root, value); } private TreeNode addRecursive(TreeNode current, int value) { if (current == null) { return new TreeNode(value); } if (value < current.value) { current.left = addRecursive(current.left, value); } else if (value > current.value) { current.right = addRecursive(current.right, value); } else { // 값이 같은 경우는 무시 return current; } return current; } } public class MainForBinaryTree { public static void main(String[] args) { BinaryTree tree = new BinaryTree(40); tree.add(20); tree.add(60); tree.add(10); tree.add(30); tree.add(50); tree.add(70); System.out.println("Pre-order traversal:"); tree.root.preOrder(); System.out.println(); System.out.println("In-order traversal:"); tree.root.inOrder(); System.out.println(); System.out.println("Post-order traversal:"); tree.root.postOrder(); System.out.println(); } }
- Tree, Binary Tree, BST, AVL Tree, RB Tree에 대해 설명하시오
- 이 모든 데이터 구조는 계층적 자료구조인 트리를 확장하여 여러 방식으로 특정 문제를 해결하기 위해 고안 되었다.
- Tree
- 특징 : 기본적인 노드와 노드를 연결하는 구조로, 간선을 가지며 계층적인 구조를 갖고 있다. 어떠한 두 노드도 정확히 하나의 경로로만 연결되어야 한다.
- 용도 : Tree 구조는 디렉토리 구조, 조직도 등 다양하게 계층적 모델링에 적용 가능하다.
- Binary Tree(이진 트리)
- 특징 : 각 노드가 최대 2개의 자식을 가지게 되는 트리 구조이며, 자식을 무조건 두개로 제한하기 때문에 구현과 관리에서 장점이 있는 데이터 구조이다.
- 용도 : 이진 탐색 트리(BST), 이진 힙(Heap) 등을 구현하는 기반으로 사용된다.
- BST(Bianary Search Tree)
- 특징 : 이진트리의 종류 중 하나이며, 모든 노드에 대해서 왼쪽 자식보다 부모노드(자기 자신)가 크며, 오른쪽 자식이 자기 자신 보다 큰 구조로 구성되어 있다. 이렇게 노드가 기본적으로 정렬되어 있으므로, 효율적인 검색, 삽입, 삭제를 가능하게 한다.
- 용도 : 데이터 베이스 관리 시스템에서 인덱스 구조로 사용되거나, 동적 데이터 세트의 관리 용도로 내부 매커니즘으로 사용되는 경우가 많다.
- AVL Tree
- 특징 : BST의 한 종류로, 자동 균형 조정을 수행하는 트리이다. 각 노드는 균형 인수(balance factor)를 유지하여서 양쪽 자식의 트리 높이 차이가 최대 1이 되도록 유지하는 형태를 만들며, 이러한 균형 조정은 회전 연산을 통해 이루어진다. 해당 구성을 갖고 있기 때문에 탐색 시 노드를 향해 들어가는 깊이를 적절하게 유지 한다는 점에서, 검색 속도의 일관성을 제공해준다고 볼 수 있다.
- 용도 : 검색이 많이 이루어지는 시스템에서 데이터의 빠른 검색을 보장하기 위해 사용한다.
- RB Tree
- 특징 : Red Black 트리라고도 부르며, 노드마다 색을 입힌 트리로, AVL 트리와 마찬가지로 자동 균형 이진 검색 트리이고, 삽입과 삭제 연산 후에도 트리의 균형을 바로 재조정하는 규칙이 있다. 특히 핵심으로 루트는 검은색, 모든 경로에는 동일 수의 검은색 노드가 있어야 하고, 빨간색 노드는 검은색 자식만을 가질 수 있다. AVL 트리보다도 훨씬 강력한 자동 조정 기준을 갖고 있다보니 노드들 사이의 밸런스를 맞춰주고, 그만큼 성능적 이점이 있다.
- 용도 : C++ 의 STL, Java의 TreeMap, TreeSet 등의 내부 데이터 구조로 사용되기도 하며, 프로세스들의 노드 관리 등 OS 단에서도 사용되는 구조이다.
- 우선순위 큐에 대해 설명할 수 있다.
- 기본적으로 우선순위 큐는 말 그대로 우선 순위가 될 요소들을 지정하고, 이를 통해 큐를 구현하더라도 우선순위라는 추상적 요소를 함께 고려하는 구조다.
- 우선순위 큐의 장점은 다음과 같다.
- 효율적인 데이터 관리 : 우선순위 요소를 배정하고 이를 통해 데이터를 관리하므로, 긴급한 작업을 먼저 처리하길 요구되는 상황에서 대응이 용이하다. 즉 시스템의 반응성 측면에서 긍정적이다.
- 데이터 처리의 최적화 : 처리 해야할 데이터 중 중요한 데이터를 먼저 처리함으로, 시스템 자체의 성능 향상에 용이하다. 즉 시스템 전체의 운용이 효율적이다.
- 구현 방식에 제약이 적다 : 배열, 연결 리스트, 힙 등 다양한 구조를 활용해 작성이 가능하고, 특히 힙을 이용한 구현은 삽입과 삭제를 O(NlogN) 시간 안에 처리 할 수 있어서 효과적이다.
- 우선순위 큐의 단점은 다음과 같다.
- 구현 복잡성 : 우선순위를 관리하기 위한 로직을 구현해야하는데, 힙 기반의 구현은 상성이 좋은 것에 비해 복잡해질 수 있다. (특히 우선순위 요소가 복수일 경우)
- 메모리 사용 : 기본적으로 데이터를 저장만 할 뿐이 아니라 우선순위 관리를 위한 추가 메모리 공간을 사용해야 하므로, 그만큼 간단한 구조에 비해 메모리 사용량이 커진다.
- 구현 : Java 기준 PriorityQueue 클래스를 제공해주긴 한다.
public class MinHeapPriorityQueue { private int[] heap; private int size; private int capacity; public MinHeapPriorityQueue(int capacity) { this.capacity = capacity; this.heap = new int[capacity + 1]; this.size = 0; heap[0] = Integer.MIN_VALUE; // 힙의 가장 앞에 최소값 설정 } private int parent(int pos) { return pos / 2; } private int leftChild(int pos) { return (2 * pos); } private int rightChild(int pos) { return (2 * pos + 1); } private boolean isLeaf(int pos) { return pos > (size / 2) && pos <= size; } private void swap(int fpos, int spos) { int tmp = heap[fpos]; heap[fpos] = heap[spos]; heap[spos] = tmp; } public void insert(int element) { if (size >= capacity) { return; // 힙이 꽉 찼을 경우 } heap[++size] = element; int current = size; while (heap[current] < heap[parent(current)]) { swap(current, parent(current)); current = parent(current); } } public int remove() { int popped = heap[1]; heap[1] = heap[size--]; minHeapify(1); return popped; } private void minHeapify(int pos) { if (!isLeaf(pos)) { if (heap[pos] > heap[leftChild(pos)] || heap[pos] > heap[rightChild(pos)]) { if (heap[leftChild(pos)] < heap[rightChild(pos)]) { swap(pos, leftChild(pos)); minHeapify(leftChild(pos)); } else { swap(pos, rightChild(pos)); minHeapify(rightChild(pos)); } } } } public static void main(String[] args) { MinHeapPriorityQueue minHeap = new MinHeapPriorityQueue(15); minHeap.insert(5); minHeap.insert(3); minHeap.insert(17); minHeap.insert(10); minHeap.insert(84); minHeap.insert(19); minHeap.insert(6); minHeap.insert(22); minHeap.insert(9); System.out.println("The Min-Val is: " + minHeap.remove()); }
- List, Set의 차이점을 설명하시오
- Java 언어를 기준으로 이야기하면 컬렉션 프레임워크에서 매우 중요한 인터페이스로, 데이터의 저장과 관리에 사용한다.
- List 의 경우 요소의 순서가 유지되는 컬렉션을 나타낸다. 요소는 정확한 위치에 의해 접근되며, 같은 요소의 중복 저장도 가능하다.
- List의 특징은, 기본적으로 추가된 그대로 순서가 저장되며, 인덱스를 통해 접근이 가능하고, 결정적으로 중복이 허용된다는 점이 Set과 차이점이다
- Set의 경우 집합의 개념으로 전체 컬렉션에서 요소들에게 유일함을 강조하는 구조이다. 요소 간의 순서는 일반적으로 보장되지 않고, 보장되는 확장 버전이 존재하고, 요소 자체는 유일무의 하다. 단 이와중에도 순서성을 함께 보장해주는 데이터 구조로 LinkedHashSet 같은 것이 존재한다.
- Set의 특징은, 구성요소의 중복이 허용되지 않아, 각 요소가 고유해야하며, 순서성을 보장하지 않는다는 점이다.
- 그렇기에 위의 두 자료구조의 가장 큰 차이는 중복과 순서성에 대한 영역이며, 추가로 성능적인 면에서 set은 중복검사를 진행하는 만큼 처리가 필요하고, 검색도 진행되므로 복잡도가 증가된다. List의 경우 인덱스 기반 접근 시에는 당연히 빠른 검색과 업데이트를 제공한다.
- Hash Function 과 Hash Table이란?
- 해시 : 해시는 데이터를 다른 데이터로 매핑하는 프로세스를 설명할 때 사용되는 용어다. 이 과정에서 생성되는 결과를 해시값, 또는 해시 코드라고 하며, 해시 함수를 통해 얻어 진다.
- 해시를 사용함은 해시 함수로 키를 생성하고, 값을 변화 시킨 뒤 이를 인덱스로 사용하여 빠른 검색을 도울 수 있다. 또한 암호학에서 해시 함수를 통해 데이터의 무결성을 확인하는 용도이자 원본 대신 갖고 있는 형태로 보안을 유지하는데 도움을 주고, 해시 값을 일종의 데이터 검증용 도구로도 사용이 가능해서 데이터 무결성 검증에도 도움이 되는 방식이다.
- 이러한 해시 함수의 성질은 결정성, 고속처리, 균일 분포, 충돌 최소화 라는 성질을 갖춰야 효율성, 범용성을 둘 다 잡을 수 있고, 현재는 많은 영역에서 굉장히 중요한 역할로 해당 소프트웨어가 사용된다.
- 결정성 : 동일한 입력에는 항상 같은 출력(해시값)을 반환해야한다.
- 고속처리 : 해시 함수는 그 계산이 빨라야 하며, 해당 영역이 데이터 처리 속도에 직접적인 영향을 미친다.
- 균일 분포 : 입력 값들이 해시 테이블 상에 다양한 위치로 적절히 배치되고, 충돌 가능성을 최소화 할 수 있어야 한다.
- 충돌 최소화 : 이상적인 해시 함수는 서로 다른 입력값에 대해 동일한 해시값을 반환하지 않아야 하나, 실제로는 충돌이 발생할 수 있기에 이를 어떻게 관리할 지를 효율적으로 대처해야한다.
- Hash Function : 위의 개념에 근거하여 해시 함수는 임의의 크기의 데이터를 고정된 크기의 유니크 값으로 매핑하는 함수 역할을 하는 함수다. 보통 그 값이 해시 값, 내지는 해시 코드라고 부른다.
- Hash Table : 해시 테이블은 키와 값을 매핑하는 데이터 구조로, 해시 함수를 사용해 키 값을 인덱스로 변화시킨다. 그렇기에 보통 O(1)의 시간복잡도로 삽입, 삭제, 검색이 수행되어야 마
- 사용 예 : 해시의 영역은 데이터베이스, 캐싱 시스템, 메모리 관리 등 다양한 분야에서 적용되어 사용된다.
- 해쉬 테이블과 시간 복잡도에 대해 설명할 수 있다. 내부 구조에 대해 설명할 수 있다.
- 해시 테이블의 내부 구조
- 해시 함수 : 키를 입력 받아 정수 형태의 해시 코드를 생성해 낸다. 이 해시 코드는 배열의 인덱스로 변환되어 사용된다. 이때 서로 다른 키가 동일한 인덱스로 매핑되는 경우 ‘충돌’이라고 부르며 대응이 되어야 한다.
- 버킷 배열 : 해시 테이블의 주요 데이터 구조로, 배열을 사용하여 데이터를 저장한다.
- 충돌 해결 메커니즘 : 1번에서 언급한 문제를 위한 해결 방법을 갖추고 있어야 하며, 대표적으로 체이닝, 오픈 어드레싱 두 방식이다.
- 체이닝(Chaining) : 각 배열 인덱스에 연결리스트로 여러개의 동일 키-값의 쌍을 저장할 수 있다.
- 오픈 어드레싱(Open Addressing) : 충돌이 발생하면 다음 비어 있는 빈 배열 인덱스를 찾아 데이터를 저장한다. 이 과정은 선형탐사, 제곱탐사, 이중 해싱 등의 방법으로 수행할 수 있다.
- 위의 구조를 갖춘 해시 테이블은 다음의 시간 복잡도를 가진다.
- 평균 시나리오(Average case):
- 삽입(Insertion): O(1)
- 삭제(Deletion): O(1)
- 검색(Search): O(1)
- 해시 테이블의 내부 구조
- 최악의 경우(Worst case):
- 모든 연산: O(n) - 이는 모든 키가 동일한 인덱스로 해시되어, 체이닝을 사용하는 경우 연결 리스트를 전체 탐색해야 하거나, 오픈 어드레싱에서 적절한 빈 슬롯을 찾기 위해 전체 배열을 탐색해야 하는 경우 발생할 수 있다.
- 특정한 상황이 주어졌을 때, 어떤 자료구조를 선택해야 할 지 논리적으로 설명할 수 있다.
- 개발 과정에서 어떤 자료구조를 사용하는 지는 성능, 안정성 등 모든 면에서 개발하는 어플리케이션에게 영향을 준다. 그렇기에 여러가지 조건들을 펼쳐놓고 적절한 자료구조를 선택하면 좋다.
- 우선 검색이 많이 필요한 경우라면, 검색에 특화된 해시 테이블, BST, AVL Tree, RB Tree 등 탐색 자체에 특화된 자료구조를 우선적으로 검토하면 좋다. 해시는 키를 통해 빠른 시간 복잡도를 가지며, 대신 고유 키를 제대로 도출 가능한 Hash Function 구현이 가능한지 검토해보면 좋다. 이진 검색 트리의 경우 평균적인 로그 시간 복잡도로 검색, 삽입, 삭제 처리의 시간을 보여주는데 그러나 문제는 최악의 경우 선형 시간이 필요할 수 있다는 점을 고려해봐야 한다. 균형 이진 트리의 경우 최악의 경우에도 선형보다 나은 로그 시간 복잡도 연산을 보장하지만, 구현이 어렵거나 구조적으로 내부 자원을 많이 쓰고, 연산 자원을 꾸준하게 쓴다는 점을 고려해야할 것이다.
- 두 번째로 데이터의 순서 유지가 중요한 경우에는 당연히 배열, 연결리스트, 큐와 같이 순서성을 보장하는 타입을 고민해볼 필요가 있고, 이러한 순간에도 데이터의 접근을 어떻게 접근하는지를 고민해볼 필요가 있다.
- 세 번째 상황은 중복 없는 데이터 관리가 필요한 경우도 있는데, 이럴 때는 당연히 집합 세트와 같은 구조를 보장하고, 거기서 다른 데이터의 특성을 감안하여 자료구조를 확정지으면 된다.
- 최종적으로 자료구조를 선택하는 것은 결국 데이터의 본질적인 특성과 요구되는 기본적인 작업이 무엇인지를 파악해볼 필요가 있다. 검색인지, 접근인지, 아니면 어떤 특수한 특성을 유지해줘야 하는지 등을 통해 애플리케이션의 성능을 최적화 시키는 것이 요구된다.