Written by
Poogle
on
on
자료구조 면접 질문 모음
참고
Books
Articles
f-lab - 자바 백엔드 기술 면접 대비하기 - 1편
Posts
[실무 면접 준비 - 5] 자료구조 & Java (Data Structure & Java)
📌 자료구조
선형 자료구조와 비선형 자료구조의 차이는 무엇인가요?
- 선형 자료구조
- 데이터 요소들이 저장되어 있는 모습을 표현했을 때 직선의 형태로 되어 있습니다.
- 대표적인 예: Array, Queue 등
- 비선형 자료구조
- 데이터 요소들이 저장되어 있는 모습을 표현했을 때 직선이 아닌 것을 의미합니다.
- 대표적인 예: Tree, Graph 등
📌 시간복잡도, 공간복잡도
🕰️ 시간복잡도
- 알고리즘의 수행시간을 의미합니다.
🫧 공간복잡도
- 알고리즘 수행에 필요한 메모리를 의미합니다.
📌 Big-O 표기법
- 알고리즘 실행에 있어 최악의 시나리오를 나타냅니다.
- 시, 공간의 상한선
- 점근식 표기법을 사용(영향력 없는 항, 상수계수 제거해 최고차항으로만 표기)합니다.
각 자료구조 연산 별 시간복잡도
출처: bigocheatsheet
📌 Collection Framework in Java
- 자바에서 널리 알려져 있는 자료구조를 바탕으로 객체, 데이터들을 효율적으로 관리 할 수 있는 자료구조들이 있는 라이브러리를 의미합니다.
- List, Set은 Collection 인터페이스을 상속받습니다.
- Map 인터페이스는 별도로 정의합니다.
📌 Array, LinkedList, ArrayList
Array
- Array는 인덱스와 인덱스에 대응하는 데이터들로 이루어진 자료 구조입니다.
- 논리적 저장 순서와 물리적 저장 순서가 일치하며 인덱스로 해당 원소에 접근합니다.
- 원소와 인덱스 값을 알고 있으면 검색에
O(1)
시간 복잡도 소요됩니다.- 배열은 데이터를 인덱스로 조회할 수 있기 때문에 인덱스 조회성능이 높습니다.
- 캐시의 지역성으로 인해 비교적 빠르게 탐색 수행 가능(메모리 상에 순서대로 데이터를 저장하기 때문)합니다.
- 삽입이나 삭제 등이 필요한 경우에는 원소들을 shift 해주어야 하기 때문에 O(n)의 시간이 걸립니다.
LinkedList
- LinkedList란 각 노드가 데이터와 포인터를 가지고 연결되어 있는 방식으로 데이터를 저장하는 자료 구조입니다.
- 종류: 단일 링크드리스트, 이중 링크드리스트, 원형 링크드리스트 등이 존재합니다.
- 링크드리스트의 경우 인덱스가 존재하지 않기 때문에 검색 및 수정시 첫 번째 노드부터 순차적으로 모든 노드를 검색합니다.
- 링크드리스트의 검색과 수정 시 시간 복잡도는 O(1)입니다.
- 링크드리스트의 경우 노드의 삭제와 삽입 시 O(1)만에 해결할 수 있지만, 원하는 위치에 원소를 삽입하거나 삭제하는 경우 위치를 검색하는 시간이 필요하므로 O(n)의 시간이 걸립니다.
❓ 왜 LinkedList를 사용할까요? (배열과 비교해서)
- 배열은 비슷한 유형의 선형 데이터를 저장하는데 사용할 수 있지만 제한 사항이 있습니다.
- 배열의 크기가 고정되어 있어 미리 요소의 수에 대해 할당을 받아야 합니다.
- 새로운 요소를 삽입하는 것은 비용이 많이 듭니다. (공간을 만들고, 기존 요소 전부 이동)
장점
- 동적 크기
- 삽입/삭제가 용이합니다.
단점
- 임의로 액세스를 허용할 수 없습니다. 즉, 첫 번째 노드부터 순차적으로 요소에 액세스 해야합니다. (이진 검색 수행 불가능)
- 포인터의 여분의 메모리 공간이 목록의 각 요소에 필요합니다.
ArrayList (Dynamic Array)
- 이름처럼 내부적으로 배열을 사용하여 데이터를 관리합니다.
- 인덱스를 가지고 있어 데이터 검색에 적합하고 속도가 빠릅니다. -> 시간 복잡도 : O(1)
- 데이터의 삽입, 삭제 시 해당 데이터를 제외한 모든 데이터를 임시 배열을 생성해 복사하므로 삽입, 삭제가 빈번할 경우 속도가 느리며 부적합합니다. -> 시간 복잡도 : O(n)
- 동기화를 지원하지 않아 Vector보다 빠릅니다.
❓ ArrayList는 내부적으로 어떻게 구현되어 있을까요?
- Q. 배열로 구현되어있다면 분명 크기가 꽉 차면 일반 배열처럼 예외가 발생할텐데 ArrayList 는 어떻게 무한히 데이터를 받을 수 있나요?
- A.
- 새로운 ArrayList가 생성될 떄 빈 Object 배열 객체가 elementData에 할당이 됩니다.
- 값을 추가할 때 현재 사이즈(capacity)가 배열의 길이(elementData)와 같으면
grow()
메소드로 크기를 확장(기존 용량 + 기존 용량 / 2)한 후 크기가 늘어난 배열에 기존 elementData를 복사합니다.
비교
- Array는 index로 빠르게 값을 찾는 것이 가능합니다.
- LinkedList는 데이터의 삽입 및 삭제가 빠릅니다.
- ArrayList는 데이터를 찾는데 빠르지만, 삽입 및 삭제가 느립니다.
- 데이터의 검색이 주가 되는 경우에는 Dynamic Array(ArrayList)를 사용하는 게 좋습니다.
- 데이터의 삽입, 삭제가 빈번하다면 Dynamic Array(ArrayList)보다는 LinkedList를 사용하는 편이 낫습니다.
- 삽입 및 삭제
- LinkedList:
O(1)
- ArrayList:
O(n)
- LinkedList:
- 인덱스를 통해서 검색
- ArrayList:
O(1)
- LinkedList:
O(n)
- ArrayList:
📌 List, Queue, Set 인터페이스
- List
- 순서가 존재합니다.
- 중복이 가능합니다.
- 구현체 ex. ArrayList
- Queue
- 중복이 가능합니다.
- FIFO
- 구현체 ex. LinkedList
- Set
- 순서가 없습니다.
- 중복이 불가능합니다.
- 구현체 ex. HashSet, TreeSet
- TreeSet: 순서를 유지하는 Set
📌 Hash
Hash Table
- 임의의 키를 Hash Code로 변환시켜서 저장하는 자료구조입니다.
- 시간 복잡도:
O(1)
Hash Function
- 임의의 길이를 가진 데이터를 고정된 길이를 가진 데이터로 매핑하는 함수입니다.
- 해시 충돌(Hash Collision): 서로 다른 두 데이터가 같은 인덱스로 해싱되는 경우를 의미합니다.
- 해시는 등호(=) 연산에만 특화되어 있기 때문에 부등호 연산(>, <)이 자주 사용되는 데이터베이스 검색을 위해서는 적절하지 않습니다. => 이런 경우 B+ Tree 알고리즘을 자주 사용합니다.
- hash function을 1:1 맵핑으로 만드는 것보다 Collision을 최소화하는 방향으로 설계하고 발생하는 Collision에 대비해 어떻게 대응할 것인가가 중요합니다.
Hash Collision Resolve 방식에는 어떠한 방법이 존재하나요?
Open Address 방식 (개방 주소법)
- 해시 충돌이 발생하면 다른 해시 버킷에 해당 자료를 삽입하는 방식입니다.
- 다른 해시 버킷을 찾기 위해 다양한 방법이 존재하는데 대표적인 방법으로는 순차적으로 비어있는 버킷을 찾을 때까지 계속 탐색하는 Linear Probing 방식이 존재합니다.
- 해시 버킷을 채운 밀도가 높아질수록 Worst Case 발생 빈도가 높아지기 때문에 시간 복잡도가 O(n)에 가까워집니다.
Separate Chaining 방식 (분리 연결법)
- Hash Table의 각 버킷이 여러 값을 저장할 수 있는 자료 구조를 가리키도록 만드는 방법을 의미합니다.
- 이를 구현할 수 있는 방법으로는 LinkedList를 사용하는 방식과 Tree를 사용하는 방식이 존재합니다.
- java util 에서는 HashMap을 Separate Chaining 방식을 사용하여 구현합니다.
- Separate Chaining Using LinkedList
- 각각의 버킷들을 LinkedList로 만들어 충돌(Collision)이 발생하면 해당 bucket의 list에 값을 추가하는 방식입니다.
- Separate Chaining Using Tree
- 각각의 버킷들을 Tree로 만들어 충돌(Collision)이 발생하면 해당 bucket의 tree에 값을 추가하는 방식입니다.
- Tree는 LinkedList에 비해 메모리 사용량이 많기 때문에 데이터 개수가 적을 때는 링크드리스트를 사용하는게 유리하지만, 개수가 많을 경우 Tree의 시간 복잡도가 LinkedList보다 빠르기 때문에 Tree를 사용하는게 더 유리합니다.
HashMap
- 해시 함수를 사용해 키를 해시값으로 바꾼 후 이를 사용해서 데이터를 관리하고 연산을 수행합니다.
- CRUD 시간 복잡도:
O(1)
- 그러나 충돌이 자주 나는 경우 시간 복잡도가
O(N)
까지 안 좋아질 수 있습니다. - Open Address나 Separate Chaining 방식 고려합니다.
- 그러나 충돌이 자주 나는 경우 시간 복잡도가
- 동기화 기능을 제공해주지 않기 때문에 멀티 스레드 환경에서 Thread-Safe하지 않습니다.
- HashMap의
put()
호출될 때 threshold 초과 시 HashMap의 멤버 변수인 table의 사이즈를 늘리는resize()
함수가 호출됩니다. resize()
는this.table
에 새로운 Array를 할당하고 새 테이블에 과거 데이터 복제하는 방식을 사용합니다.resize()
과정 중 다른 메서드에서get(key)
호출 시 새 테이블에 데이터가 온전히 복제되지 않으면 null을 리턴합니다.
- HashMap의
📌 Stack, Queue
Stack
- LIFO(Last In First Out): 마지막에 저장한 데이터를 가장 먼저 꺼냅니다.
- ex. 웹 브라우저의 방문기록(뒤로가기), 실행취소(undo)
Queue
- FIFO(First In First Out): 처음에 저장한 데이터를 가장 먼저 꺼냅니다.
- ex. 프린터 인쇄 대기, 콜센터 고객 대기 시간
📌 Tree, Binary Tree, BST, Red Black Tree, AVL Tree, 최대/최소 신장 트리, B Tree, B+ Tree
Tree
- 비선형 자료구조로 node(정점)들과 이를 연결하는 edge(간선)로 구성됩니다.
- 트리는 그래프의 한 종류입니다.
- 트리는 1개의 루트 노드를 가지고 있습니다.
- 루트 노드는 0 ~ N개 자식 노드를 갖고 있습니다.
- 자식 노드는 0 ~ N개 자식 노드를 갖고 있습니다. …
- 트리는 사이클을 가질 수 없습니다.
Tree vs. Binary Tree(이진 트리)
- 이진 트리: 각 노드가 최대 2개의 자식을 갖는 트리입니다.
Complete Binary Tree; 완전 이진 트리
- 마지막 레벨을 제외하고 모든 레벨이 완전히 채워져있고 마지막 레벨의 노드가 왼쪽에서 오른쪽으로 채워지는 이진 트리입니다.
Full Binary Tree; 전 이진 트리
- 모든 노드가 0개 또는 2개의 자식 노드를 갖는 이진 트리입니다.
Perfect Binary Tree; 포화 이진 트리
- 전 이진 트리이면서 완전 이진 트리입니다.
- 모든 리프 노드가 같은 레벨에 있고 이 레벨은 최대 개수의 노드를 가지고 있습니다.
- 항상 2^k - 1개의 노드를 가지고 있습니다.(k: 레벨 수)
Binary Tree(이진 트리) vs. Binary Search Tree(이진 탐색 트리)
- 이진 탐색 트리: 중복된 노드가 없다는 가정 하에 부모 노드가 왼쪽 자식보다 크고 오른쪽 자식보다는 작은 규칙을 만족하는 이진 트리입니다.
BST의 최악의 경우의 예와 시간복잡도
- Skewed Binary Tree(사향 이진 트리)
- 순차적으로 값을 저장했을 때 모양이 마치 연결리스트와 닮아있습니다. -> 탐색할 때 성능이 떨어집니다.
- BST는 평균적인 경우
O(log(N))
의 시간 복잡도를 갖지만 최악의 경우O(N)
이 됩니다. - 이런 문제를 해결하기 위해 ✅ 2-3 트리, AVL 트리같은 자가 균형 이진 탐색 트리(Self Balancing BST)를 사용합니다.
Red-Black Tree란 무엇인가요?
- 자가 균형 이진 탐색 트리의 한 종류입니다.
- 이진 탐색 트리(BST)의 단점인 편향성을 색깔을 통해서 보완하기 위한 자료구조입니다.
- 즉, 트리의 노드의 개수가 동일할 경우 depth를 최소화하여 시간 복잡도를 줄이는 것이 핵심 아이디어입니다.
- RBT의 CRUD 시간 복잡도: O(log n)
- Root node 부터 leaf node 까지의 모든 경로 중 최소 경로와 최대 경로의 크기 비율은 2보다 크지 않습니다. (balanced 상태)
- 노드의 child가 없을 경우 child를 가리키는 포인터는 NIL 값을 저장 -> 이러한 NIL 들을 leaf node로 간주합니다.
Red-Black Tree의 정의
- 각 노드는 Red or Black 색깔을 가집니다.
- Root node: Black
- 각 leaf node(NIL)의 색깔: Black
- 어떤 노드의 색깔이 red라면 두 개의 children의 색깔은 모두 black
- 어떤 노드의 색깔이 black이라면 두 개의 children 의 색깔은 모두 red
- 각 노드에 대해서 노드로부터 그 노드의 자손인 leaf node로 가는 경로들은 모두 같은 수의 Black 노드를 포함합니다. (-> Black-Height)
- Black-Height: 노드 x 로부터 노드 x 를 포함하지 않는 leaf node 까지의 simple path 상에 있는 black nodes 들의 개수를 의미합니다.
Red-Black Tree 노드 삽입
- BST의 특성을 유지하면서 색깔이 red인 노드를 삽입합니다.
- 삽입한 노드가 색깔 규칙에 어긋난다면 노드의 색깔을 조정하고, Black-Height에 위배된다면 rotation을 통해 height를 조정합니다.
Red-Black Tree 노드 삭제
- BST의 특성을 유지하면서 해당 노드를 삭제합니다.
- 지워진 노드의 색깔이 Black 이라면 Black-Height가 1 감소한 경로에 black node가 1개 추가되도록 rotation하고 노드의 색깔을 조정됩니다.
- 지워진 노드의 색깔이 red라면 어떠한 문제도 발생하지 않습니다.
AVL Tree
- BST가 편향되어 최악의 경우가 될 때 해당 단점을 상쇄하기 위해 사용합니다.
- 이진 탐색 트리의 속성을 가집니다.
- 왼쪽, 오른쪽 서브 트리의 높이 차이가 최대 1
- 높이 차이가 1보다 커지면 회전(Rotation)을 통해 균형을 맞춰 높이 차이를 줄입니다.
- 삽입, 검색, 삭제의 시간 복잡도가 O(log N)입니다. (N : 노드의 개수)
B Tree & B+ Tree
- 이진 트리는 하나의 부모가 두 개의 자식밖에 가지지 못하고, 균형이 맞지 않으면 검색 효율이 선형 검색 급으로 떨어집니다.
- 하지만 이진 트리 구조의 간결함과 균형만 맞다면 검색, 삽입, 삭제 모두 O(logN)의 성능을 보이는 장점이 있기 때문에 계속 개선시키기 위한 노력이 이루어지고 있습니다.
B Tree
- 데이터 베이스, 파일 시스템에서 널리 사용되는 트리 자료구조의 일종입니다.
- 이진 트리를 확장해서, 더 많은 수의 자식을 가질 수 있게 일반화 시킨 것: B Tree
- 자식 수에 대한 일반화를 진행하면서, 하나의 레벨에 더 저장되는 것 뿐만 아니라 트리의 균형을 자동으로 맞춰주는 로직까지 갖추었습니다. 단순하고 효율적이며, 레벨로만 따지면 완전히 균형을 맞춘 트리입니다.
- ex.
- 대량의 데이터를 처리해야 할 때, 검색 구조의 경우 하나의 노드에 많은 데이터를 가질 수 있다는 점은 상당히 큰 장점입니다.
- 대량의 데이터는 메모리보다 블럭 단위로 입출력하는 하드디스크 or SSD에 저장해야 하기 때문!
- 한 블럭이 1024 바이트이면, 2바이트를 읽으나 1024바이트를 읽으나 똑같은 입출력 비용이 발생합니다.
- 따라서 하나의 노드를 모두 1024바이트로 꽉 채워서 조절할 수 있으면 입출력에 있어서 효율적인 구성을 갖출 수 있습니다.
- -> B-Tree는 이러한 장점을 토대로 많은 데이터베이스 시스템의 인덱스 저장 방법으로 애용하고 있습니다.
- 규칙
- 노드의 자료수가 N이면, 자식 수는 N+1이어야 합니다.
- 각 노드의 자료는 정렬된 상태여야 합니다.
- 루트 노드는 적어도 2개 이상의 자식을 가져야 합니다.
- 루트 노드를 제외한 모든 노드는 적어도 M/2개의 자료를 가지고 있어야 합니다.
- 외부 노드로 가는 경로의 길이는 모두 같습니다.
- 입력 자료는 중복될 수 없습니다.
B+ Tree
- 데이터의 빠른 접근을 위한 인덱스 역할만 하는 비단말 노드가 추가로 있습니다.
- 기존의 B-Tree와 데이터의 연결리스트로 구현된 색인구조 입니다.
- B-Tree의 변형 구조로 index 부분과 leaf 노드로 구성된 순차 데이터 부분으로 이루어집니다. 인덱스 부분의 key 값은 leaf에 있는 key 값을 직접 찾아가는데 사용합니다.
- 장점
- 블럭 사이즈를 더 많이 이용할 수 있습니다. (Key 값에 대한 하드디스크 액세스 주소가 없기 때문)
- Leaf 노드끼리 연결 리스트로 연결되어 있어서 범위 탐색에 매우 유리합니다.
- 단점
- B-Tree의 경우 최상 케이스에서는 루트에서 끝날 수 있지만, B+Tree는 무조건 leaf 노드까지 내려가봐야 합니다.
- 비교
- B-Tree : 각 노드에 데이터가 저장됩니다.
- 각 노드에서 key와 data 모두 들어갈 수 있고, data는 disk block으로 포인터가 될 수 있습니다.
- B+ Tree : index 노드와 leaf 노드로 분리되어 저장됩니다.
- 각 노드에서 key만 들어간다. 따라서 data는 leaf 노드에만 존재합니다.
- add, delete 연산 모두 leaf 노드에서만 이루어집니다.
- 또한, leaf 노드는 서로 연결되어 있어서 임의 접근이나 순차 접근 모두 성능이 우수합니다.
- B-Tree : 각 노드에 데이터가 저장됩니다.
트라이(Trie)
- 문자열에서 검색을 빠르게 도와주는 자료구조입니다.
- 정수형에서 이진탐색트리를 이용하면 시간복잡도: O(logN)
- 하지만 문자열에서 적용했을 때, 문자열 최대 길이가 M이면: O(M*logN)
- 트라이를 활용하면? → O(M)으로 문자열 검색이 가능함!
Minimum Spanning Tree
- 그래프의 여러 Spanning Tree 중 edge의 가중치 합이 최소인 Spanning tree를 의미합니다.
- Spanning Tree란 그래프의 모든 정점이 Cycle 없이 연결된 형태를 의미합니다.
Minimum Spanning Tree를 구하는 알고리즘에는 어떤 방법이 존재하나요?
- Kruskal Algorithm: Edge없이 vertex로만 구성된 그래프를 만들어서 weight가 제일 작은 edge 부터 검토후 cycle이 생기지 않는 경우에만 edge를 추가하는 방법 -> O(ElogE)
- Prim Algorithm: 한 개의 vertex로 구성된 그래프 A를 만들어서 그래프 A 내부에 있는 vertex와 외부에 있는 vertex 사이의 edge를 연결했을 때 가장 작은 weight를 가진 vertex와 edge를 그래프 A에 추가하는 방법 -> O(ElogV)
📌 Heap, Priority Queue
Heap
- 완전 이진 트리의 일종
- 여러 값 중, 최대값과 최소값을 빠르게 찾아내도록 만들어진 자료구조입니다.
- 반정렬 상태입니다.
- 힙 트리는 중복된 값을 허용합니다. (이진 탐색 트리는 중복값 허용X)
- Binary Heap이란 최대 또는 최소를 빠르게 찾아내기 위해 만들어진 완전이진트리(마지막 레벨 빼고 모든 레벨 채워지고 왼 -> 오)로써 부모노드와 자식 노드간에 대소 관계가 성립합니다.
- 이러한 대소 관계를 기반 -> Max Heap / Min Heap
- 최대 힙(Max Heap): 부모 노드의 키 값이 자식 노드의 키 값보다 크거나 같은 완전 이진 트리
- 최대 힙(Max Heap): 부모 노드의 키 값이 자식 노드의 키 값보다 작거나 같은 완전 이진 트리
- Heap에 존재하는 노드들의 최대 및 최소를 검색하기 위한 시간 복잡도 -> O(1)
- 연산 후 heap의 대소 관계 구조를 계속 유지하기 위해서는 제거된 루트 노트를 대체할 다른 노드가 필요합니다.
- 여기서 heap은 맨 마지막 노드를 루트 노드로 대체시킨 후, 다시 heapify 과정을 거쳐 heap 구조를 유지합니다.
- heapify 연산을 수행하는데 걸리는 시간 복잡도는 O(log n)이기 때문에 -> 결국 최대 및 최소를 검색하는데 걸리는 시간은 O(log n)
- 최대 최소가 아닌 다른 노드들의 CRUD 시간 복잡도는 O(log n)
❓ Heap과 BST의 차이
- 힙은 최대 및 최소를 찾는데
O(1)
유리, heapify를 포함한다면O(log n))
- BST는 모든 원소들을 찾는데
O(log n)
유리합니다.
Priority Queue 우선순위 큐
- 우선순위의 개념을 큐에 도입한 자료구조입니다.
- 데이터들이 우선순위를 가지고 있습니다.
- 우선순위가 높은 데이터가 먼저 나갑니다.
- 시뮬레이션 시스템, 작업 스케줄링, 수치해석 계산에서 사용합니다.
- 우선순위 큐는 배열, 연결리스트, 힙으로 구현합니다. (힙으로 구현이 가장 효율적!)
- 힙 → 삽입: O(log n) / 삭제 : O(log n)
우선순위 큐는 일반적으로 힙(Heap)을 이용하여 구현합니다.
📌 피보나치 수열을 코드로 구현하는 방법 (코드로 구현할 수 있는지, 재귀로 구현할 때의 문제점, DP로 구현하는 이유 등)
재귀로 구현
- 중복된 연산이 계속해서 발생합니다.
- 시간 복잡도는 함수가 한 번 호출되면 다시 두 번 호출되기 때문에 지수적으로 증가해 O(2^N)이 됩니다.
반복문으로 구현
- n 이 2 이상일 때는 n-1 번만큼 반복문을 시행합니다.
- 재귀에 비해 매우 효율적인 방법이며 시간 복잡도는 O(n)
DP로 구현
- DP로 구현하는 이유: 기본적인 피보나치 수 알고리즘이 너무나 비효율적이기 때문입니다.
- 부분문제가 너무 많이 중복되는 것이 문제라면, 각 부분문제를 해결할 때마다 그것을 저장하고 필요할 때마다 가져다 쓰도록 합니다.
📌 그래프 - DFS, BFS
그래프
- Node들과 이를 연결하는 Edge들을 모아 놓은 자료구조입니다.
- 방향 및 비방향 그래프 모두 존재합니다.
- 사이클 및 self-loop 가 존재해도 상관 없습니다.
Graph 탐색에는 어떠한 방법이 존재하나요?
- 그래프는 따로 규칙이 존재하지 않기 때문에 모든 정점을 탐색합니다.
- 특정 정점을 기준으로 넓게 탐색하기 전에 깊게 탐색하는 방법 -> DFS(Depth First Search)
- 깊게 탐색하기 전에 넓게 탐색하는 방법 -> BFS(Breadth First Search)
깊이 우선 탐색 (DFS; Depth First Search)
- 스택을 이용해서 갈 수 있는 만큼 최대한 많이 가고, 갈 수 없으면 이전 정점으로 돌아갑니다.
- 실제로 구현할 때는 재귀를 이용합니다.
인접 행렬을 이용한 구현
void dfs(int x) {
check[x] = true; //방문함
for (int i = 1; i <= n; i++) {
if (a[x][i] == 1 && check[i] == false) { //1) x와 i 사이에 간선이 있는지 2) i를 방문한 적이 없는지 확인
dfs(i);
}
}
}
- 각 정점마다 dfs 한번씩 호출 * dfs 함수의 복잡도 = O(V^2)
인접 리스트를 이용한 구현
void dfs(int x) {
check[x] = true; //방문함
for (int i = 1; i < a[x].size; i++) { //a[x]= x와 연결된 모든 정점(간선)
int y = a[x][i];
if(check[y] == false) {
dfs(y); //O(V + E) = (V < E -> O(E))
}
}
}
- O(V + E)
너비 우선 탐색 (BFS; Breadth First Search)
- 다차원 배열에서 각 칸을 방문할 때 너비를 우선으로 방문하는 알고리즘입니다.
- 임의의 정점에서 시작해서, 모든 정점을 한 번씩 반복합니다.
- 최단 거리 알고리즘입니다.
- 모든 가중치가 1일 때 최단 거리를 구하는 알고리즘입니다.
- 가중치가 1이 아니라면? -> 다익스트라 알고리즘
BFS 조건
- 최소 비용 문제
- 간선의 가중치: 1
- 정점과 간선의 개수가 적어야 합니다.(적다는 것은 문제의 조건에 맞춰서 해결할 수 있다는 것을 의미)
- 간선의 가중치가 문제에서 구하라고 하는 최소 비용과 의미가 일치합니다.
- 즉, 거리의 최소값을 구하는 문제라면 가중치는 거리를 의미, 시간의 최소값을 구하는 문제라면 가중치는 시간을 의미해야 합니다.
풀이
- 시작하는 칸을 큐에 넣고 방문했다는 표시를 남깁니다.
- 큐에서 원소를 꺼내어 그 칸에 상하좌우로 인접한 칸에 대해 (아래)3번을 진행합니다.
- 해당 칸을 이전에 방문했다면 아무 것도 하지 않고, 처음으로 방문했다면 방문했다는 표시를 남기고 해당 칸을 큐에 삽입합니다.
- 큐가 빌 때까지 (위)2번을 반복합니다.
- 모든 칸이 큐에 한 번씩 들어가므로 -> 칸이 N개 일 때 O(N)
- 참고 - 구현 시 문제가 되는 경우들
- 시작점에 방문했다는 표시를 남기지 않았을 때
- 큐에 넣을 때 방문했다는 표시를 하는 대신 큐에서 빼낼 대 방문했다는 표시를 남길 때
- 이웃한 원소가 범위를 벗어났는지에 대한 체크를 잘못했을 때
인접 행렬을 이용한 구현
check[1] = true; queue.push(1);
while (!queue.empty()) {
int x = queue.front(); queue.pop();
for (int i = 1; i <= n; i++) {
if (a[x][i] == 1 && check[i] == false) {
check[i] = true;
queue.push(i);
}
}
}
- O(V^2)
인접 리스트를 이용한 구현
check[1] = true; queue.push(1);
while (!queue.empty()) {
int x = queue.front(); queue.pop();
for (int i = 0; i < a[x] .size(); i++) {
int y = a[x][i];
if (check[y] == false) {
check[y] = true;
queue.push(y);
}
}
}
- O(V + E)
📌 정렬, 탐색
Bubble Sort(버블 정렬)
- Bubble Sort는 Selection Sort와 유사한 알고리즘으로 서로 인접한 두 원소의 대소를 비교하고, 조건에 맞지 않다면 자리를 교환하며 정렬하는 알고리즘입니다.
- 시간복잡도
- (n-1) + (n-2) + (n-3) + …. + 2 + 1 => n(n-1)/2이므로, O(n^2)
- 정렬 되어있던 안 되어있던 2개의 원소를 비교 -> 최선, 평균, 최악의 경우 모두 시간복잡도가 O(n^2) 으로 동일합니다.
- 공간복잡도
- 주어진 배열 안에서 교환(swap)을 통해, 정렬이 수행되므로 O(n)
- 장점
- 구현이 매우 간단하고, 소스코드가 직관적입니다.
- 정렬하고자 하는 배열 안에서 교환하는 방식이므로, 다른 메모리 공간을 필요로 하지 않습니다. => 제자리 정렬(in-place sorting)
- 안정 정렬(Stable Sort) 입니다.
- 단점
- 시간복잡도가 최악, 최선, 평균 모두 O(n^2)으로, 굉장히 비효율적입니다.
- 정렬 되어있지 않은 원소가 정렬 됐을때의 자리로 가기 위해서, 교환 연산(swap)이 많이 일어나게 됩니다.
Insertion Sort(삽입 정렬)
- Insertion Sort는 Selection Sort와 유사하지만, 좀 더 효율적인 정렬 알고리즘입니다.
- Selection Sort와 Insertion Sort는 k번째 반복 이후, 첫번째 k 요소가 정렬된 순서로 온다는 점에서 유사합니다.
- 하지만, Selection Sort는 k+1번째 요소를 찾기 위해 나머지 모든 요소들을 탐색하지만 Insertion Sort는 k+1번째 요소를 배치하는 데 필요한 만큼의 요소만 탐색하기 때문에 훨씬 효율적으로 실행된다는 차이가 있습니다.
- Insertion Sort는 2번째 원소부터 시작하여 그 앞(왼쪽)의 원소들과 비교하여 삽입할 위치를 지정한 후, 원소를 뒤로 옮기고 지정된 자리에 자료를 삽입 하여 정렬하는 알고리즘입니다.
- 최선의 경우 O(N)이라는 엄청나게 빠른 효율성을 가지고 있어, 다른 정렬 알고리즘의 일부로 사용될 만큼 좋은 정렬 알고리즘입니다.
- 시간복잡도
- 최악의 경우(역으로 정렬되어 있을 경우) Selection Sort와 마찬가지로, (n-1) + (n-2) + …. + 2 + 1 => n(n-1)/2 즉, O(n^2) 입니다.
- 하지만, 모두 정렬이 되어있는 경우(Optimal)한 경우, 한번씩 밖에 비교를 안하므로 O(n) 의 시간복잡도를 가지게 됩니다.
- 또한, 이미 정렬되어 있는 배열에 자료를 하나씩 삽입/제거하는 경우에는, 현실적으로 최고의 정렬 알고리즘이 되는데, 탐색을 제외한 오버헤드가 매우 적기 때문입니다.
- 최선의 경우는 O(n) 의 시간복잡도를 갖고, 평균과 최악의 경우 O(n^2) 의 시간복잡도를 갖게 됩니다.
- 공간복잡도
- 주어진 배열 안에서 교환(swap)을 통해, 정렬이 수행되므로 O(n) 입니다.
- 장점
- 알고리즘이 단순합니다.
- 대부분의 원소가 이미 정렬되어 있는 경우, 매우 효율적일 수 있습니다.
- 정렬하고자 하는 배열 안에서 교환하는 방식이므로, 다른 메모리 공간을 필요로 하지 않습니다. => 제자리 정렬(in-place sorting)
- 안정 정렬(Stable Sort) 입니다.
- Selection Sort나 Bubble Sort과 같은 O(n^2) 알고리즘에 비교하여 상대적으로 빠릅니다.
- 단점
- 평균과 최악의 시간복잡도가 O(n^2)으로 비효율적입니다.
- Bubble Sort와 Selection Sort와 마찬가지로, 배열의 길이가 길어질수록 비효율적입니다.
Selection Sort(선택 정렬)
- Selection Sort는 Bubble Sort과 유사한 알고리즘으로, 해당 순서에 원소를 넣을 위치는 이미 정해져 있고, 어떤 원소를 넣을지 선택하는 알고리즘입니다.
- Selection Sort는 배열에서 해당 자리를 선택하고 그 자리에 오는 값을 찾는 것
- 시간복잡도
- 데이터의 개수가 n개라고 했을 때,
첫 번째 회전에서의 비교횟수 : 1 ~ (n-1) => n-1 두 번째 회전에서의 비교횟수 : 2 ~ (n-1) => n-2 ... (n-1) + (n-2) + .... + 2 + 1 => n(n-1)/2
- 비교하는 것이 상수 시간에 이루어진다는 가정 아래, n개의 주어진 배열을 정렬하는데 O(n^2) 만큼의 시간이 걸립니다.
- 최선, 평균, 최악의 경우 시간복잡도는 O(n^2) 으로 동일합니다.
- 데이터의 개수가 n개라고 했을 때,
- 공간복잡도
- 주어진 배열 안에서 교환(swap)을 통해, 정렬이 수행되므로 O(n)
- 장점
- Bubble sort와 마찬가지로 알고리즘이 단순합니다.
- 정렬을 위한 비교 횟수는 많지만, Bubble Sort에 비해 실제로 교환하는 횟수는 적기 때문에 많은 교환이 일어나야 하는 자료상태에서 비교적 효율적입니다.
- Bubble Sort와 마찬가지로 정렬하고자 하는 배열 안에서 교환하는 방식이므로, 다른 메모리 공간을 필요로 하지 않습니다. => 제자리 정렬(in-place sorting)
- 단점
- 시간복잡도가 O(n^2)으로, 비효율적입니다.
- 불안정 정렬(Unstable Sort) 입니다.
Quick Sort(퀵 정렬)
- Quick Sort은 분할 정복(divide and conquer) 방법을 통해 주어진 배열을 정렬
- 분할 정복(divide and conquer) 방법 : 문제를 작은 2개의 문제로 분리하고 각각을 해결한 다음, 결과를 모아서 원래의 문제를 해결하는 전략입니다.
- Quick Sort은 불안정 정렬에 속하며, 다른 원소와의 비교만으로 정렬을 수행하는 비교 정렬에 속합니다.
- 또한 Merge Sort와 달리 Quick Sort는 배열을 비균등하게 분할합니다.
- Quick Sort 개선
- partition() 함수에서 피벗 값이 최소나 최대값으로 지정되어 파티션이 나누어지지 않았을 때, O(n^2)에 대한 시간복잡도를 가집니다.
- 즉, 정렬하고자 하는 배열이 오름차순 정렬되어있거나 내림차순 정렬되어있으면 O(n^2)의 시간복잡도를 갖습니다.
- 이때, 배열에서 가장 앞에 있는 값과 중간값을 교환해준다면 확률적으로나마 시간복잡도 O(nlog₂n)으로 개선할 수 있습니다.
- 하지만, 이 방법으로 개선한다해도 Quick Sort의 최악의 시간복잡도가 O(nlog₂n)가 되는 것은 아닙니다.
- 시간복잡도
- 최선의 경우(Best cases) : T(n) = O(nlog₂n)
- 최악의 경우(Worst cases) : T(n) = O(n^2)
- 최악의 경우는 정렬하고자 하는 배열이 오름차순 정렬되어있거나 내림차순 정렬되어있는 경우입니다.
- 평균의 경우(Average cases) : T(n) = O(nlog₂n)
- 공간복잡도
- 주어진 배열 안에서 교환(swap)을 통해, 정렬이 수행되므로 O(n)
- 장점
- 불필요한 데이터의 이동을 줄이고 먼 거리의 데이터를 교환할 뿐만 아니라, 한 번 결정된 피벗들이 추후 연산에서 제외되는 특성 때문에, 시간 복잡도가 O(nlog₂n)를 가지는 다른 정렬 알고리즘과 비교했을 때도 가장 빠릅니다.
- 정렬하고자 하는 배열 안에서 교환하는 방식이므로, 다른 메모리 공간을 필요로 하지 않습니다.
- 단점
- 불안정 정렬(Unstable Sort) 입니다.
- 정렬된 배열에 대해서는 Quick Sort의 불균형 분할에 의해 오히려 수행시간이 더 많이 걸립니다.