diff --git a/Algorithm_with_DataStructure/ReadMe.md b/Algorithm_with_DataStructure/ReadMe.md new file mode 100644 index 0000000..34fb9d7 --- /dev/null +++ b/Algorithm_with_DataStructure/ReadMe.md @@ -0,0 +1,21 @@ +# 프로젝트 파일 설명 + +| 파일 이름 | 설명 | +|-----------------------|------------------| +| `matching_bracket.cpp`| 괄호 매칭 알고리즘: 주어진 문자열의 모든 종류의 괄호가 올바르게 닫히고 중첩되었는지 확인합니다. | +| `mergesort.cpp` | 병합 정렬 알고리즘: 배열을 정렬하기 위한 병합 정렬 기술을 구현합니다. 병합 정렬에서 사용되는 분할 정복 방법을 설명합니다. | +| `nqueen.cpp` | N-퀸 문제 알고리즘: 백트래킹을 사용하여 N-퀸 문제를 해결합니다. 단계별 접근 방식과 복잡도 분석을 자세히 설명합니다. | +| `selectionsort.cpp` | 선택 정렬 알고리즘: 선택 정렬 방법을 시연하며 정렬 메커니즘과 효율성에 대한 자세한 설명이 포함되어 있습니다. | +| `adjacencyarray.cpp` | 인접 배열 표현: 그래프를 표현하기 위한 인접 배열 접근 방식을 설명합니다. 밀집 그래프에 적합합니다. | +| `adjacencylist.cpp` | 인접 리스트 표현: 그래프 표현을 위해 인접 리스트의 사용을 자세히 설명합니다. 희소 그래프에 이상적입니다. | +| `eratosthenes_sieve.cpp` | 에라토스테네스의 체 알고리즘: 주어진 한계까지 모든 소수를 찾는 고대 알고리즘을 구현합니다. | +| `heapsort.cpp` | 힙 정렬 알고리즘: 힙 정렬의 구현 세부사항을 제공하며, 최대 힙을 구성하고 요소들을 정렬하는 방법을 포함합니다. | +| `insertionsort.cpp` | 삽입 정렬 알고리즘: 삽입 정레의 기본 개념과 구현을 다루며, 그 과정을 반복적으로 집중적으로 설명합니다. | +| `kruskal.cpp` | 크루스칼 알고리즘: 그래프의 최소 신장 트리를 찾기 위해 크루스칼 알고리즘을 구현합니다. 유니온-파인드 자료 구조를 사용합니다. | +| `subsum.cpp` | 부분합 알고리즘: 배열에서 특정 합을 가지는 부분 배열을 찾는 백트래킹 알고리즘입니다. 최악의 경우 시간 복잡도는 O(2^n)입니다. | +| `tree_array.cpp` | 트리 배열 구조: 배열을 사용하여 트리 구조를 표현하고, 트리 연산을 수행하는 방법을 설명합니다. | +| `tree_travel.cpp` | 트리 순회 알고리즘: 전위 순회, 중위 순회, 후위 순회 등 다양한 트리 순회 방법을 구현합니다. | +| `unionfind.cpp` | 유니온-파인드 자료구조: 두 요소의 집합을 합치고, 요소가 속한 집합을 찾는 연산을 효율적으로 수행합니다. 경로 압축과 랭크를 사용하여 최적화된 버전을 제공합니다. | + +## 추가 정보 +- 각 파일에는 알고리즘의 목적과 기능을 설명하는 자세한 주석이 포함되어 있어 코드와 그 적용을 이해하기 쉽습니다. diff --git a/Algorithm_with_DataStructure/adjacencyarray.cpp b/Algorithm_with_DataStructure/adjacencyarray.cpp index bc0d179..57c3d84 100644 --- a/Algorithm_with_DataStructure/adjacencyarray.cpp +++ b/Algorithm_with_DataStructure/adjacencyarray.cpp @@ -4,63 +4,77 @@ // | business | ultrasuperrok@gmail.com | //############################################################ #include -#include - using namespace std; -/* -그래프의 최종 모습 (인접 행렬 형식): -Adjacency Matrix: -0 1 0 0 1 -1 0 1 1 1 -0 1 0 1 0 -0 1 1 0 1 -1 1 0 1 0 -이 구조는 무방향 그래프를 나타내며, 각 정점은 해당하는 다른 정점들과 1로 표시된 연결을 가집니다. -시간 복잡도 설명: -- addEdge 함수는 O(1) -- printGraph 함수는 O(V^2) -*/ +// 인접 행렬의 노드 개수 (고정) +const int numNodes = 2; + +// 인접 행렬 배열 +int adjMatrix[numNodes][numNodes]; +// 인접 행렬을 초기화하는 함수 +// 모든 값을 0으로 설정 +void initializeMatrix() { + for (int i = 0; i < numNodes; i++) { + for (int j = 0; j < numNodes; j++) { + adjMatrix[i][j] = 0; + } + } +} -// addEdge 함수는 그래프의 인접 행렬에 양방향 간선을 추가합니다. -// 'u'와 'v'는 연결될 두 정점입니다. 'adjMatrix'는 인접 행렬을 나타내는 2차원 벡터입니다. -// 이 함수의 시간 복잡도는 O(1), 특정 행렬 위치에 접근하여 값을 설정하는데 상수 시간이 걸립니다. -void addEdge(vector>& adjMatrix, int u, int v) { - adjMatrix[u][v] = 1; // 정점 u에서 v로의 간선을 추가 - adjMatrix[v][u] = 1; // 무방향 그래프이므로, 정점 v에서 u로도 간선을 추가 +// 간선을 추가하는 함수 +// u에서 v로 가는 간선의 가중치를 설정 +void addEdge(int u, int v, int weight) { + adjMatrix[u][v] = weight; } -// printGraph 함수는 그래프의 인접 행렬을 출력합니다. -// 각 행과 열을 순회하며, 각 정점간 연결 상태를 출력합니다. -// 시간 복잡도는 O(V^2) 입니다. 여기서 V는 정점의 수입니다. -void printGraph(const vector>& adjMatrix) { - cout << "Adjacency Matrix:\n"; - for (int i = 0; i < adjMatrix.size(); ++i) { - for (int j = 0; j < adjMatrix[i].size(); ++j) { - cout << adjMatrix[i][j] << " "; +// 인접 행렬을 출력하는 함수 +// 값이 0인 경우 '-'로 출력 +void printMatrix() { + for (int i = 0; i < numNodes; i++) { + for (int j = 0; j < numNodes; j++) { + if (adjMatrix[i][j] == 0) + cout << "- "; + else + cout << adjMatrix[i][j] << " "; } - cout << "\n"; + cout << endl; } } int main() { - int V = 5; // 그래프의 정점 개수 - vector> adjMatrix(V, vector(V, 0)); // 모든 값을 0으로 초기화된 VxV 크기의 2차원 벡터 생성 + // 인접 행렬 초기화 + initializeMatrix(); - // 간선 추가 - addEdge(adjMatrix, 0, 1); - addEdge(adjMatrix, 0, 4); - addEdge(adjMatrix, 1, 2); - addEdge(adjMatrix, 1, 3); - addEdge(adjMatrix, 1, 4); - addEdge(adjMatrix, 2, 3); - addEdge(adjMatrix, 3, 4); + // 엣지 추가: 서울(0)에서 부산(1)으로 가는 엣지, 가중치 400 + addEdge(0, 1, 400); - // 그래프 출력 - printGraph(adjMatrix); + // 인접 행렬 출력 + printMatrix(); + + /* + 최종적인 인접 행렬의 모습: + - 400 + - - + */ return 0; } +/* +인접 행렬 설명: +- 인접 행렬은 그래프를 행렬 형태로 표현하는 방법입니다. +- 행과 열의 인덱스는 각각의 노드를 나타내고, 행렬의 값은 간선의 가중치를 나타냅니다. +- 만약 간선이 없는 경우 0으로 표시되며, 이 코드에서는 '-'로 출력됩니다. + +인접 행렬의 시간 복잡도: +- 인접 행렬을 사용하여 그래프를 표현할 경우, 공간 복잡도는 O(V^2)입니다. 여기서 V는 노드의 개수입니다. +- 간선이 존재하는지 확인하는 작업은 O(1)의 시간 복잡도를 가집니다. +- 모든 간선을 탐색하는 작업은 O(V^2)의 시간 복잡도를 가집니다. + +최종적인 인접 행렬의 모습: +- 400 +- - +*/ + diff --git a/Algorithm_with_DataStructure/adjacencylist.cpp b/Algorithm_with_DataStructure/adjacencylist.cpp index a61e43f..54e0f7b 100644 --- a/Algorithm_with_DataStructure/adjacencylist.cpp +++ b/Algorithm_with_DataStructure/adjacencylist.cpp @@ -1,65 +1,64 @@ -//############################################################ -// | cafe | http://cafe.naver.com/dremdelover | -// | Q&A | https://open.kakao.com/o/gX0WnTCf | -// | business | ultrasuperrok@gmail.com | -//############################################################ +//############################################################# +//# | cafe | http://cafe.naver.com/dremdelover | +//# | Q&A | https://open.kakao.com/o/gX0WnTCf | +//# | business | ultrasuperrok@gmail.com | +//############################################################# #include #include using namespace std; +int main() { + // 정점의 수 + int V = 4; -/* -그래프의 최종 모습 (인접 리스트 형식): -Vertex 0: -> 1 -> 4 -Vertex 1: -> 0 -> 2 -> 3 -> 4 -Vertex 2: -> 1 -> 3 -Vertex 3: -> 1 -> 2 -> 4 -Vertex 4: -> 0 -> 1 -> 3 -이 구조는 무방향 그래프이며, 각 정점은 해당하는 연결된 정점들과 직접 연결됩니다. 각 정점의 리스트를 통해 모든 간선이 표현됩니다. - -시간 복잡도 설명: -- addEdge 함수는 O(1) -- printGraph 함수는 O(V + E) -*/ - + // 인접 리스트 생성 + vector> adjList[V + 1]; -// addEdge 함수는 그래프의 인접 리스트에 양방향 간선을 추가합니다. -// 이 함수의 시간 복잡도는 O(1), 간선을 두 리스트에 추가하는 데 상수 시간이 걸립니다. -void addEdge(vector>& adj, int u, int v) { - adj[u].push_back(v); // 정점 u에서 v로의 간선 추가 - adj[v].push_back(u); // 무방향 그래프이므로, 정점 v에서 u로의 간선도 추가 -} + // 그래프에 간선 추가 + adjList[1].push_back({2, 3}); + adjList[1].push_back({3, 5}); + adjList[2].push_back({1, 6}); + adjList[2].push_back({3, 5}); + adjList[3].push_back({2, 1}); + adjList[3].push_back({4, 13}); + adjList[4].push_back({4, 9}); + adjList[4].push_back({1, 42}); -// printGraph 함수는 그래프의 인접 리스트를 출력합니다. -// 각 정점마다 연결된 모든 정점을 순회하므로, 시간 복잡도는 O(V + E) 입니다. -// 여기서 V는 정점의 수, E는 간선의 수입니다. -void printGraph(const vector>& adj) { - for (int i = 0; i < adj.size(); ++i) { + // 인접 리스트 출력 + for (int i = 1; i <= V; ++i) { cout << "Vertex " << i << ":"; - for (int j = 0; j < adj[i].size(); ++j) { - cout << " -> " << adj[i][j]; + for (auto edge : adjList[i]) { + cout << " (" << edge.first << ", " << edge.second << ")"; } - cout << "\n"; + cout << endl; } -} - -int main() { - int V = 5; // 그래프의 정점 개수 설정 - vector> adjList(V); // 각 정점의 인접 리스트를 저장할 벡터 생성 - - // 간선 추가 - addEdge(adjList, 0, 1); - addEdge(adjList, 0, 4); - addEdge(adjList, 1, 2); - addEdge(adjList, 1, 3); - addEdge(adjList, 1, 4); - addEdge(adjList, 2, 3); - addEdge(adjList, 3, 4); - - // 그래프 출력 - printGraph(adjList); return 0; } +/* +인접리스트의 개념: +- 인접리스트는 그래프를 표현하는 방법 중 하나로, 각 정점에 연결된 다른 정점들을 리스트 형태로 저장합니다. + 이 리스트는 일반적으로 벡터, 배열 또는 링크드 리스트로 구현됩니다. + +인접리스트의 시간 복잡도: +- 그래프의 모든 간선을 순회하는데 걸리는 시간은 O(E)입니다. 여기서 E는 간선의 수를 의미합니다. +- 정점의 수를 V라 할 때, 모든 정점을 순회하는데 O(V)의 시간이 걸립니다. +- 특정 정점에 연결된 모든 정점을 순회하는 시간은 해당 정점에 연결된 간선의 수에 비례하며, + 평균적으로 한 정점에 연결된 간선의 수는 E/V이므로 O(E/V)입니다. +- 인접리스트를 사용하여 두 정점 사이에 간선이 존재하는지 확인하는 데는 최악의 경우 O(V)의 시간이 걸립니다. + 이는 특정 정점에 연결된 모든 간선을 순회해야 하기 때문입니다. +- 따라서, 그래프 전체를 순회하는 시간 복잡도는 O(V + E)입니다. 이는 각 정점과 각 간선을 한 번씩 방문하는 것을 의미합니다. + +인접리스트의 장점: +- 메모리 효율적: 인접 행렬에 비해 간선이 적은 희소 그래프에서 메모리를 더 효율적으로 사용합니다. +- 동적 그래프: 간선의 추가와 삭제가 상대적으로 용이합니다. + +완성된 인접리스트의 모습: +- 위 코드에서 출력되는 인접리스트는 다음과 같은 형식을 가집니다. + Vertex 1: (2, 3) (3, 5) + Vertex 2: (1, 6) (3, 5) + Vertex 3: (2, 1) (4, 13) + Vertex 4: (4, 9) (1, 42) +*/ diff --git a/Algorithm_with_DataStructure/bfs.cpp b/Algorithm_with_DataStructure/bfs.cpp new file mode 100644 index 0000000..216bfe3 --- /dev/null +++ b/Algorithm_with_DataStructure/bfs.cpp @@ -0,0 +1,86 @@ +//############################################################# +//# | cafe | http://cafe.naver.com/dremdelover | +//# | Q&A | https://open.kakao.com/o/gX0WnTCf | +//# | business | ultrasuperrok@gmail.com | +//############################################################# +#include +#include +#include + +using namespace std; + +// 전역 인접 리스트 및 방문 리스트 +vector> adj; +vector visited; + +// 그래프에 간선을 추가하는 함수 +// 인자: u (정점), v (연결된 정점) +void add_edge(int u, int v) { + adj[u].push_back(v); // 정점 u의 인접 리스트에 정점 v를 추가 +} + +// 너비 우선 탐색(BFS)을 수행하는 함수 +// 인자: start (시작 정점) +void bfs(int start) { + queue q; + visited[start] = true; + q.push(start); + + while (!q.empty()) { + int v = q.front(); + q.pop(); + cout << v << " "; + + for (int i = 0; i < adj[v].size(); ++i) { + if (!visited[adj[v][i]]) { + visited[adj[v][i]] = true; + q.push(adj[v][i]); + } + } + } +} + +// 메인 함수: 그래프 생성 및 BFS 수행 +int main() { + // 정점의 개수 + int V = 5; + adj.resize(V); + + // 그래프에 간선 추가 (주어진 다이어그램에 따라) + add_edge(0, 1); // A -> B + add_edge(0, 2); // A -> C + add_edge(1, 3); // B -> D + add_edge(1, 4); // B -> E + + cout << "너비 우선 탐색 (정점 0에서 시작):\n"; + bfs(0); + +/* + BFS 호출 과정 설명 (예: 정점 0에서 시작): + 1. bfs(0) 호출 -> 0 출력, 인접 정점 1과 2를 큐에 추가 + 2. 큐에서 1을 꺼내어 출력 -> 인접 정점 3과 4를 큐에 추가 + 3. 큐에서 2를 꺼내어 출력 -> 인접 정점 없음 + 4. 큐에서 3을 꺼내어 출력 -> 인접 정점 없음 + 5. 큐에서 4를 꺼내어 출력 -> 인접 정점 없음 + 6. 큐가 비어 있으므로 탐색 종료 +*/ + + return 0; +} + +/* + 너비 우선 탐색(BFS)의 개념: + - BFS는 그래프 탐색 알고리즘 중 하나로, 시작 정점에서 출발하여 인접한 모든 정점을 먼저 탐색한 후, + 다음 깊이의 정점들을 탐색하는 방식이다. + - BFS는 큐를 사용하여 구현되며, 시간 복잡도는 O(V + E)이다. 여기서 V는 정점의 수, E는 간선의 수를 의미한다. + + BFS를 사용해야 하는 경우: + - 최단 경로를 찾아야 하는 경우 (예: 최단 경로 문제) + - 레벨 순서 탐색을 해야 하는 경우 (예: 각 레벨별로 노드를 처리해야 하는 문제) + - 사이클 검출 (무방향 그래프에서) + + DFS 대신 BFS를 사용해야 하는 경우: + - 경로의 길이가 중요한 경우 (BFS는 최단 경로를 보장함) + - 큐를 사용하여 더 적은 메모리로 처리가 가능한 경우 (특히 넓은 그래프에서) + - 모든 정점을 같은 레벨에서 순서대로 처리해야 하는 경우 +*/ diff --git a/Algorithm_with_DataStructure/bst,cpp b/Algorithm_with_DataStructure/bst,cpp new file mode 100644 index 0000000..15c4e8a --- /dev/null +++ b/Algorithm_with_DataStructure/bst,cpp @@ -0,0 +1,114 @@ +//############################################################# +//# | cafe | http://cafe.naver.com/dremdelover | +//# | Q&A | https://open.kakao.com/o/gX0WnTCf | +//# | business | ultrasuperrok@gmail.com | +//############################################################# + +#include +using namespace std; + +/* +이진 탐색 트리(Binary Search Tree, BST)의 개념: +이진 탐색 트리는 각 노드가 최대 두 개의 자식 노드를 가지는 이진 트리의 일종입니다. +각 노드의 왼쪽 자식은 그 노드보다 작은 값을 가지고, 오른쪽 자식은 그 노드보다 큰 값을 가집니다. +이러한 구조를 통해 효율적인 탐색, 삽입, 삭제 연산이 가능합니다. + +시간 복잡도: +- 평균적으로, BST에서의 탐색, 삽입, 삭제 연산의 시간 복잡도는 O(log n)입니다. + 이는 트리가 균형잡혀 있을 때의 경우입니다. +- 최악의 경우, 즉 트리가 편향된 경우(한쪽으로 치우친 경우), 시간 복잡도는 O(n)입니다. + 이는 트리가 하나의 연속된 리스트와 같은 형태가 되었을 때입니다. + +배열과의 성능 비교: +- 배열: + - 탐색: O(n) (순차 탐색) + - 삽입/삭제: O(n) (삽입/삭제 시 원소를 이동해야 함) +- 이진 탐색 트리: + - 탐색: O(log n) (평균), O(n) (최악) + - 삽입/삭제: O(log n) (평균), O(n) (최악) + +BST는 정렬된 데이터를 빠르게 탐색할 수 있는 장점이 있습니다. 또한, 새로운 데이터를 삽입하거나 기존 데이터를 삭제하는 데에도 비교적 효율적입니다. 반면에, 배열은 특정 인덱스에 빠르게 접근할 수 있는 장점이 있지만, 삽입이나 삭제 시에는 많은 원소를 이동시켜야 하므로 비효율적일 수 있습니다. +*/ + +// 이진 트리 노드 정의 +struct Node { + int data; // 노드에 저장된 데이터 + Node* left; // 왼쪽 자식 노드 + Node* right; // 오른쪽 자식 노드 + + Node(int value) : data(value), left(nullptr), right(nullptr) {} // 생성자 +}; + +// 새로운 노드를 이진 탐색 트리에 삽입하는 함수 +Node* insert(Node* root, int data) { + if (root == nullptr) { + return new Node(data); // 루트가 없으면 새로운 노드를 반환 + } + + if (data < root->data) { + root->left = insert(root->left, data); // 데이터가 작으면 왼쪽 서브트리에 삽입 + } else { + root->right = insert(root->right, data); // 데이터가 크면 오른쪽 서브트리에 삽입 + } + + return root; +} + +// 이진 탐색 트리에서 특정 값을 찾는 함수 +Node* search(Node* root, int data) { + if (root == nullptr || root->data == data) { + return root; // 노드가 없거나 데이터를 찾은 경우 반환 + } + + if (data < root->data) { + return search(root->left, data); // 데이터가 작으면 왼쪽 서브트리에서 검색 + } else { + return search(root->right, data); // 데이터가 크면 오른쪽 서브트리에서 검색 + } +} + +// 이진 탐색 트리를 중위 순회로 출력하는 함수 +void inorder(Node* root) { + if (root != nullptr) { + inorder(root->left); + cout << root->data << " "; + inorder(root->right); + } +} + +int main() { + Node* root = nullptr; // 초기 트리는 비어 있음 + + // 노드 삽입 + root = insert(root, 3); + root = insert(root, 4); + root = insert(root, 2); + root = insert(root, 8); + root = insert(root, 9); + + cout << "이진 탐색 트리의 중위 순회 출력: "; + inorder(root); // 중위 순회 호출 + cout << endl; + // 출력: 2 3 4 8 9 + + // 특정 값 검색 + int valueToSearch = 4; + Node* result = search(root, valueToSearch); + if (result != nullptr) { + cout << valueToSearch << "를 찾았습니다: " << result->data << endl; + } else { + cout << valueToSearch << "를 찾을 수 없습니다." << endl; + } + + valueToSearch = 5; + result = search(root, valueToSearch); + if (result != nullptr) { + cout << valueToSearch << "를 찾았습니다: " << result->data << endl; + } else { + cout << valueToSearch << "를 찾을 수 없습니다." << endl; + } + + return 0; +} + + diff --git a/Algorithm_with_DataStructure/dfs_recursive.cpp b/Algorithm_with_DataStructure/dfs_recursive.cpp new file mode 100644 index 0000000..d93de08 --- /dev/null +++ b/Algorithm_with_DataStructure/dfs_recursive.cpp @@ -0,0 +1,78 @@ +#include +#include + +using namespace std; + +// 전역 인접 리스트 및 방문 리스트 +vector> adj; +vector visited; + +// 그래프에 간선을 추가하는 함수 +// 인자: v (정점), w (연결된 정점) +void add_edge(int v, int w) { + adj[v].push_back(w); // 정점 v의 인접 리스트에 정점 w를 추가 +} + +// 깊이 우선 탐색(DFS)을 수행하는 함수 +// 인자: v (현재 정점) +void dfs(int v) { + // 현재 정점을 방문했다고 표시하고 출력 + visited[v] = true; + cout << v << " "; + + // 현재 정점에 인접한 모든 정점을 순회 + for (int i = 0; i < adj[v].size(); ++i) { + if (!visited[adj[v][i]]) { + dfs(adj[v][i]); + } + } +} + + +// 메인 함수: 그래프 생성 및 DFS 수행 +int main() { + // 정점의 개수 + int V = 5; + adj.resize(V); + + // 그래프에 간선 추가 (주어진 다이어그램에 따라) + add_edge(0, 1); // A -> B + add_edge(0, 2); // A -> C + add_edge(1, 3); // B -> D + add_edge(1, 4); // B -> E + + cout << "깊이 우선 탐색 (정점 0에서 시작):\n"; + + visited.assign(V, false); + dfs(0); + /* + 재귀 호출 과정 설명 (예: 정점 0에서 시작): + dfs(0) 호출 -> 0 출력 + dfs(1) 호출 -> 1 출력 + dfs(3) 호출 -> 3 출력 + dfs(4) 호출 -> 4 출력 + dfs(2) 호출 -> 2 출력 + - dfs_traversal 종료 + */ + + return 0; +} + +/* + 깊이 우선 탐색(DFS)의 개념: + - DFS는 그래프 탐색 알고리즘 중 하나로, 시작 정점에서 출발하여 한 방향으로 가능한 깊이까지 탐색한 후, + 더 이상 갈 곳이 없으면 이전 정점으로 되돌아와 다른 방향으로 탐색을 계속하는 방식이다. + - 시간 복잡도는 O(V + E)이며, 여기서 V는 정점의 수, E는 간선의 수를 의미한다. + + DFS를 사용해야 하는 경우: + - 그래프의 모든 정점을 방문해야 하는 경우 + - 경로 찾기 (예: 미로 탐색) + - 사이클 검출 + - 위상 정렬 + - 강한 연결 요소 찾기 + + BFS를 사용할 수 없고 DFS를 사용해야 하는 경우: + - 재귀적 성질을 이용해야 할 때 (예: 백트래킹 문제) + - 공간 복잡도가 중요한 경우 (BFS는 큐를 사용하여 더 많은 메모리를 필요로 함) + +*/ diff --git a/Algorithm_with_DataStructure/dijkstra.cpp b/Algorithm_with_DataStructure/dijkstra.cpp new file mode 100644 index 0000000..9a7a60a --- /dev/null +++ b/Algorithm_with_DataStructure/dijkstra.cpp @@ -0,0 +1,174 @@ +//############################################################# +//# | cafe | http://cafe.naver.com/dremdelover | +//# | Q&A | https://open.kakao.com/o/gX0WnTCf | +//# | business | ultrasuperrok@gmail.com | +//############################################################# +#include +#include +#include +#include +#include + +using namespace std; + +// 무한대를 나타내기 위해 사용되는 상수 +const int INF = numeric_limits::max(); + +// 전역 변수 선언 +vector>> graph; // 그래프를 인접 리스트 형태로 저장 +vector dist; // 각 노드까지의 최단 거리를 저장 +vector prev; // 각 노드의 직전 노드를 저장 +vector visited; + +// 다익스트라 알고리즘 함수 +void dijkstra(int start) { + int n = graph.size(); // 그래프의 노드 개수 + dist.assign(n, INF); // 최단 거리 배열을 무한대로 초기화 + prev.assign(n, -1); // 직전 노드 배열을 -1로 초기화 + dist[start] = 0; // 시작 노드의 거리는 0으로 설정 + + // 최소 힙 (우선순위 큐)을 사용하여 최단 거리를 찾음 + priority_queue, vector>, greater>> pq; + pq.push({0, start}); // 시작 노드를 큐에 추가 + + // 초기 상태 + /* + Initial state: + Node: A B C D E + Dist: 0 INF INF INF INF + Prev: -1 -1 -1 -1 -1 + */ + + // 큐가 빌 때까지 반복 + while (!pq.empty()) { + int d = pq.top().first; // 현재 노드까지의 거리 + int u = pq.top().second; // 현재 노드 + pq.pop(); // 큐에서 제거 + + // 현재 노드까지의 거리가 이미 더 짧은 경로가 있으면 무시 + if (visited[u]) continue; + visitied[u] = true; + + // 현재 노드의 모든 인접 노드를 탐색 + for (const auto& edge : graph[u]) { + int v = edge.first; // 인접 노드 + int weight = edge.second; // 가중치 + + // 더 짧은 경로를 발견한 경우 + if (dist[u] + weight < dist[v]) { + dist[v] = dist[u] + weight; // 최단 거리 업데이트 + prev[v] = u; // 직전 노드 업데이트 + pq.push({dist[v], v}); // 큐에 추가 + } + } + } +} + +// 경로를 출력하는 함수 (재귀 호출 사용) +void printPath(int node) { + if (node == -1) return; // base case: 시작 노드에 도달 + printPath(prev[node]); // 직전 노드로 재귀 호출 + cout << node << " "; // 현재 노드 출력 +} + +// 다익스트라 알고리즘 수행 과정 주석 +/* +Initial state: +Node: A B C D E +Dist: 0 INF INF INF INF +Prev: -1 -1 -1 -1 -1 + +1. A 노드 선택 (현재 최단 거리: 0) +- 인접 노드 B, C, E 업데이트: +After visiting A: +Node: A B C D E +Dist: 0 4 4 INF 1 +Prev: -1 0 0 -1 0 + +2. E 노드 선택 (현재 최단 거리: 1) +- 인접 노드 C 업데이트: +After visiting E: +Node: A B C D E +Dist: 0 4 3 INF 1 +Prev: -1 0 4 -1 0 + +3. C 노드 선택 (현재 최단 거리: 3) +- 인접 노드 B, D 업데이트: +After visiting C: +Node: A B C D E +Dist: 0 4 3 11 1 +Prev: -1 0 4 2 0 + +4. B 노드 선택 (현재 최단 거리: 4) +- 인접 노드 업데이트 없음 +After visiting B: +Node: A B C D E +Dist: 0 4 3 11 1 +Prev: -1 0 4 2 0 + +5. D 노드 선택 (현재 최단 거리: 11) +- 인접 노드 B 업데이트: +After visiting D: +Node: A B C D E +Dist: 0 4 3 11 1 +Prev: -1 0 4 2 0 + +최종 상태: +Node: A B C D E +Dist: 0 4 3 11 1 +Prev: -1 0 4 2 0 +*/ + +/* +다익스트라 알고리즘에 대한 설명: +1. 다익스트라 알고리즘의 개념: + 다익스트라 알고리즘은 가중치가 있는 그래프에서 특정 시작 노드로부터 다른 모든 노드까지의 최단 경로를 찾는 알고리즘입니다. + 이 알고리즘은 그래프의 모든 간선의 가중치가 양수일 때 유효합니다. + +2. 다익스트라 알고리즘의 과정: + 1) 시작 노드의 거리를 0으로 설정하고, 나머지 노드의 거리는 무한대로 설정합니다. + 2) 모든 노드를 미방문 집합에 추가합니다. + 3) 현재 노드에서 인접한 모든 노드의 거리를 계산하여 더 짧은 경로가 발견되면 거리를 업데이트합니다. + 4) 현재 노드를 방문한 것으로 표시하고, 미방문 노드 중 최단 거리를 가진 노드를 선택하여 3) 과정을 반복합니다. + 5) 모든 노드를 방문할 때까지 3)과 4) 과정을 반복합니다. + +3. 다익스트라 알고리즘의 시간 복잡도: + - 인접 리스트를 사용한 구현: O((V+E)log V), 여기서 V는 노드의 개수, E는 간선의 개수입니다. + (우선순위 큐를 사용하여 최소 거리를 찾고 업데이트하는데 log V 시간이 걸립니다) + +4. 다익스트라 알고리즘의 한계: + - 음수 가중치를 가진 간선이 있는 그래프에서는 동작하지 않습니다. 음수 가중치가 있는 경우 벨만-포드 알고리즘을 사용해야 합니다. + +코드 실행 결과: +Node Distance Path +0 0 0 +1 4 0 1 +2 3 0 4 2 +3 11 0 4 2 3 +4 1 0 4 +*/ + +int main() { + // 그래프를 인접 리스트 형태로 정의 + // 각 pair는 (인접 노드, 가중치)를 의미 + graph = { + {{1, 4}, {2, 4}, {4, 1}}, // A (0) + {}, // B (1) + {{1, 6}, {3, 8}}, // C (2) + {{1, 2}}, // D (3) + {{2, 2}} // E (4) + }; + + int start = 0; // 시작 노드 (A) + dijkstra(start); // 다익스트라 알고리즘 실행 + + // 결과 출력 + cout << "Node\tDistance\tPath" << endl; + for (int i = 0; i < dist.size(); ++i) { + cout << i << "\t" << dist[i] << "\t\t"; + printPath(i); + cout << endl; + } + + return 0; +} diff --git a/Algorithm_with_DataStructure/inorder_tree_array.cpp b/Algorithm_with_DataStructure/inorder_tree_array.cpp new file mode 100644 index 0000000..9779528 --- /dev/null +++ b/Algorithm_with_DataStructure/inorder_tree_array.cpp @@ -0,0 +1,73 @@ +//############################################################ +// | cafe | http://cafe.naver.com/dremdelover | +// | Q&A | https://open.kakao.com/o/gX0WnTCf | +// | business | ultrasuperrok@gmail.com | +//############################################################ +#include +using namespace std; + +// 트리를 배열로 나타내기 위해서, 배열의 인덱스를 사용하는 중위 순회 함수 정의 +void inorderTraversal(int tree[], int index, int size) { + if (index >= size || tree[index] == -1) return; // 유효하지 않은 인덱스나 값이 -1이면 종료 + + // Step 1: 왼쪽 자식 노드로 이동 (2*index) + inorderTraversal(tree, 2 * index, size); + + // Step 2: 현재 노드 값 출력 + cout << tree[index] << " "; + + // Step 3: 오른쪽 자식 노드로 이동 (2*index + 1) + inorderTraversal(tree, 2 * index + 1, size); +} + +int main() { + // 트리를 배열로 나타내기 + // -1은 해당 위치에 노드가 없음을 나타냅니다. + int tree[] = {-1, 1, 4, 8, 3, 5, -1, 7, 2, -1, -1, -1, -1, -1, 6}; + int size = sizeof(tree) / sizeof(tree[0]); + + // 중위 순회 함수 호출 + inorderTraversal(tree, 1, size); + + return 0; +} + +/* +트리 도식화 (배열 인덱스 기반): + + 1 (1) + / \ + 4 (2) 8 (3) + / \ \ + 3 (4) 5 (5) 7 (7) + / / + 2 (8) 6 (14) + +중위 순회 과정: +1. 왼쪽 자식: 4 (index 2) + - 왼쪽 자식: 3 (index 4) + - 왼쪽 자식: 2 (index 8) + - 왼쪽 자식: 없음 (index 16, size 초과) + - 현재 노드 값 출력: 2 (index 8) + - 오른쪽 자식: 없음 (index 17, size 초과) + - 현재 노드 값 출력: 3 (index 4) + - 오른쪽 자식: 없음 (index 9, 값이 -1) + - 현재 노드 값 출력: 4 (index 2) + - 오른쪽 자식: 5 (index 5) + - 왼쪽 자식: 없음 (index 10, 값이 -1) + - 현재 노드 값 출력: 5 (index 5) + - 오른쪽 자식: 없음 (index 11, 값이 -1) +2. 현재 노드 값 출력: 1 (index 1) +3. 오른쪽 자식: 8 (index 3) + - 왼쪽 자식: 없음 (index 6, 값이 -1) + - 현재 노드 값 출력: 8 (index 3) + - 오른쪽 자식: 7 (index 7) + - 왼쪽 자식: 6 (index 14) + - 왼쪽 자식: 없음 (index 28, size 초과) + - 현재 노드 값 출력: 6 (index 14) + - 오른쪽 자식: 없음 (index 29, size 초과) + - 현재 노드 값 출력: 7 (index 7) + - 오른쪽 자식: 없음 (index 15, 값이 -1) + +중위 순회 순서: 2 3 4 5 1 8 6 7 +*/ diff --git a/Algorithm_with_DataStructure/insertionsort.cpp b/Algorithm_with_DataStructure/insertionsort.cpp index 153b91e..c6a835f 100644 --- a/Algorithm_with_DataStructure/insertionsort.cpp +++ b/Algorithm_with_DataStructure/insertionsort.cpp @@ -13,7 +13,7 @@ 2. 다음 원소를 정렬된 부분과 비교하여, 올바른 위치에 삽입합니다. 3. 모든 원소가 정렬된 부분에 삽입될 때까지 2의 과정을 반복합니다. -도식화 예: +도식화 예:(아래 코드 기준) 입력 배열: [5, 2, 9, 1, 5, 6] 첫 번째 패스: [5| 2, 9, 1, 5, 6] => |의 왼쪽은 정렬된 부분 두 번째 패스: [2, 5| 9, 1, 5, 6] => 2를 정렬된 부분의 올바른 위치에 삽입 @@ -24,6 +24,20 @@ 완료된 정렬: [1, 2, 5, 5, 6, 9] */ +// 삽입정렬의 슈도코드: +// for i = 1 to length(A) - 1 +// key = A[i] +// j = i - 1 +// while j >= 0 and A[j] > key +// A[j + 1] = A[j] +// j = j - 1 +// A[j + 1] = key + +// 삽입정렬의 시간 복잡도: +// 최선의 경우: O(n) - 이미 정렬된 배열의 경우 +// 평균의 경우: O(n^2) - 일반적인 경우 +// 최악의 경우: O(n^2) - 역순으로 정렬된 배열의 경우 + #include #include diff --git a/Algorithm_with_DataStructure/parenthesesBalanced.cpp b/Algorithm_with_DataStructure/parenthesesBalanced.cpp new file mode 100644 index 0000000..26e532c --- /dev/null +++ b/Algorithm_with_DataStructure/parenthesesBalanced.cpp @@ -0,0 +1,67 @@ +#include +#include +#include + +using namespace std; + +// 두 문자가 짝이 맞는 괄호인지 확인하는 함수 +bool isMatchingPair(char first, char second) { + // 각 괄호 쌍을 비교하여 일치하면 true 반환 + if (first == '(' && second == ')') return true; + else if (first == '{' && second == '}') return true; + else if (first == '[' && second == ']') return true; + // 일치하지 않으면 false 반환 + return false; + // 시간 복잡도: O(1) +} + +// 주어진 괄호 문자열이 올바르게 짝이 맞는지 확인하는 함수 +bool areParenthesesBalanced(string expression) { + stack stack; // 여는 괄호를 저장할 스택 + + // 문자열의 각 문자를 순회 + for (int i = 0; i < expression.length(); i++) { + // 여는 괄호는 스택에 푸시 + if (expression[i] == '{' || expression[i] == '(' || expression[i] == '[') { + stack.push(expression[i]); + } + + // 닫는 괄호가 나왔을 때 + if (expression[i] == '}' || expression[i] == ')' || expression[i] == ']') { + // 스택이 비어있으면 짝이 맞지 않음 + if (stack.empty()) { + return false; + } + // 스택의 top과 현재 닫는 괄호가 짝이 맞는지 확인 + else if (!isMatchingPair(stack.top(), expression[i])) { + return false; + } + // 짝이 맞으면 스택에서 여는 괄호를 팝 + else { + stack.pop(); + } + } + } + + // 스택이 비어있으면 모든 괄호가 짝이 맞음, 아니면 짝이 맞지 않음 + return stack.empty(); + // 시간 복잡도: O(n), n은 입력 문자열의 길이 +} + +// 주어진 괄호 문자열을 테스트하는 함수 +void do_test(string expression) { + if (areParenthesesBalanced(expression)) + std::cout << "괄호가 올바르게 짝이 맞습니다." << std::endl; + else + std::cout << "괄호가 올바르게 짝이 맞지 않습니다." << std::endl; +} + +int main() { + // 테스트 케이스 실행 + do_test("{}({})"); // 올바르게 짝이 맞는 경우 + do_test("{{({})"); // 여는 괄호가 더 많은 경우 + do_test("{}({))"); // 닫는 괄호가 더 많은 경우 + + return 0; + // 시간 복잡도: O(n) 각 테스트 케이스에 대해 +} diff --git a/Algorithm_with_DataStructure/postorder_tree_array.cpp b/Algorithm_with_DataStructure/postorder_tree_array.cpp new file mode 100644 index 0000000..4cbf2c0 --- /dev/null +++ b/Algorithm_with_DataStructure/postorder_tree_array.cpp @@ -0,0 +1,73 @@ +//############################################################ +// | cafe | http://cafe.naver.com/dremdelover | +// | Q&A | https://open.kakao.com/o/gX0WnTCf | +// | business | ultrasuperrok@gmail.com | +//############################################################ +#include +using namespace std; + +// 트리를 배열로 나타내기 위해서, 배열의 인덱스를 사용하는 후위 순회 함수 정의 +void postorderTraversal(int tree[], int index, int size) { + if (index >= size || tree[index] == -1) return; // 유효하지 않은 인덱스나 값이 -1이면 종료 + + // Step 1: 왼쪽 자식 노드로 이동 (2*index) + postorderTraversal(tree, 2 * index, size); + + // Step 2: 오른쪽 자식 노드로 이동 (2*index + 1) + postorderTraversal(tree, 2 * index + 1, size); + + // Step 3: 현재 노드 값 출력 + cout << tree[index] << " "; +} + +int main() { + // 트리를 배열로 나타내기 + // -1은 해당 위치에 노드가 없음을 나타냅니다. + int tree[] = {-1, 1, 4, 8, 3, 5, -1, 7, 2, -1, -1, -1, -1, -1, 6}; + int size = sizeof(tree) / sizeof(tree[0]); + + // 후위 순회 함수 호출 + postorderTraversal(tree, 1, size); + + return 0; +} + +/* +트리 도식화 (배열 인덱스 기반): + + 1 (1) + / \ + 4 (2) 8 (3) + / \ \ + 3 (4) 5 (5) 7 (7) + / / + 2 (8) 6 (14) + +후위 순회 과정: +1. 왼쪽 자식: 4 (index 2) + - 왼쪽 자식: 3 (index 4) + - 왼쪽 자식: 2 (index 8) + - 왼쪽 자식: 없음 (index 16, size 초과) + - 오른쪽 자식: 없음 (index 17, size 초과) + - 현재 노드 값 출력: 2 (index 8) + - 오른쪽 자식: 없음 (index 9, 값이 -1) + - 현재 노드 값 출력: 3 (index 4) + - 오른쪽 자식: 5 (index 5) + - 왼쪽 자식: 없음 (index 10, 값이 -1) + - 오른쪽 자식: 없음 (index 11, 값이 -1) + - 현재 노드 값 출력: 5 (index 5) + - 현재 노드 값 출력: 4 (index 2) +2. 오른쪽 자식: 8 (index 3) + - 왼쪽 자식: 없음 (index 6, 값이 -1) + - 오른쪽 자식: 7 (index 7) + - 왼쪽 자식: 6 (index 14) + - 왼쪽 자식: 없음 (index 28, size 초과) + - 오른쪽 자식: 없음 (index 29, size 초과) + - 현재 노드 값 출력: 6 (index 14) + - 오른쪽 자식: 없음 (index 15, 값이 -1) + - 현재 노드 값 출력: 7 (index 7) + - 현재 노드 값 출력: 8 (index 3) +3. 현재 노드 값 출력: 1 (index 1) + +후위 순회 순서: 2 3 5 4 6 7 8 1 +*/ diff --git a/Algorithm_with_DataStructure/preorder_tree_array.cpp b/Algorithm_with_DataStructure/preorder_tree_array.cpp new file mode 100644 index 0000000..548884d --- /dev/null +++ b/Algorithm_with_DataStructure/preorder_tree_array.cpp @@ -0,0 +1,66 @@ +//############################################################ +// | cafe | http://cafe.naver.com/dremdelover | +// | Q&A | https://open.kakao.com/o/gX0WnTCf | +// | business | ultrasuperrok@gmail.com | +//############################################################ +#include +using namespace std; + +// 트리를 배열로 나타내기 위해서, 배열의 인덱스를 사용하는 전위 순회 함수 정의 +void preorderTraversal(int tree[], int index, int size) { + if (index >= size || tree[index] == -1) return; // 유효하지 않은 인덱스나 값이 -1이면 종료 + + // Step 1: 현재 노드 값 출력 + cout << tree[index] << " "; + + // Step 2: 왼쪽 자식 노드로 이동 (2*index) + preorderTraversal(tree, 2 * index, size); + + // Step 3: 오른쪽 자식 노드로 이동 (2*index + 1) + preorderTraversal(tree, 2 * index + 1, size); +} + +int main() { + // 트리를 배열로 나타내기 + // -1은 해당 위치에 노드가 없음을 나타냅니다. + int tree[] = {-1, 1, 4, 8, 3, 5, -1, 7, 2, -1, -1, -1, -1, -1, 6}; + int size = sizeof(tree) / sizeof(tree[0]); + + // 전위 순회 함수 호출 + preorderTraversal(tree, 1, size); + + return 0; +} + +/* +트리 도식화 (배열 인덱스 기반): + + 1 (1) + / \ + 4 (2) 8 (3) + / \ \ + 3 (4) 5 (5) 7 (7) + / / + 2 (8) 6 (14) + +전위 순회 과정: +1. 현재 노드 값 출력: 1 (index 1) + - 왼쪽 자식: 4 (index 2) + - 왼쪽 자식: 3 (index 4) + - 왼쪽 자식: 2 (index 8) + - 왼쪽 자식: 없음 (index 16, size 초과) + - 오른쪽 자식: 없음 (index 17, size 초과) + - 오른쪽 자식: 없음 (index 9, 값이 -1) + - 오른쪽 자식: 5 (index 5) + - 왼쪽 자식: 없음 (index 10, 값이 -1) + - 오른쪽 자식: 없음 (index 11, 값이 -1) + - 오른쪽 자식: 8 (index 3) + - 왼쪽 자식: 없음 (index 6, 값이 -1) + - 오른쪽 자식: 7 (index 7) + - 왼쪽 자식: 6 (index 14) + - 왼쪽 자식: 없음 (index 28, size 초과) + - 오른쪽 자식: 없음 (index 29, size 초과) + - 오른쪽 자식: 없음 (index 15, 값이 -1) + +전위 순회 순서: 1 4 3 2 5 8 7 6 +*/ diff --git a/Algorithm_with_DataStructure/queue_josephuse.cpp b/Algorithm_with_DataStructure/queue_josephuse.cpp new file mode 100644 index 0000000..153d5f5 --- /dev/null +++ b/Algorithm_with_DataStructure/queue_josephuse.cpp @@ -0,0 +1,54 @@ +#include +#include + +using namespace std; + +int josephus(int N, int K) { + // Step 1: 큐 초기화 및 요소 삽입 + // 시간복잡도: O(N) + // 큐를 초기화하고 1부터 N까지의 요소를 삽입합니다. + // 예시: N = 5일 경우, 큐는 [1, 2, 3, 4, 5]로 초기화됩니다. + queue q; + for (int i = 1; i <= N; ++i) { + q.push(i); + } + + // Step 2: 제거 과정 시뮬레이션 + // 시간복잡도: O(N * K) + // 큐의 크기가 1이 될 때까지 반복합니다. + while (q.size() > 1) { + // 첫 번째 K-1개의 요소를 큐의 뒤로 이동시킵니다. + for (int i = 0; i < K - 1; ++i) { + q.push(q.front()); + q.pop(); + } + // K번째 요소를 제거합니다. + q.pop(); + } + + // 마지막으로 남은 요소가 생존자입니다. + return q.front(); +} + +int main() { + int N = 5; // 예시: 5명의 사람이 있을 때 + int K = 2; // 예시: 매 2번째 사람을 제거할 때 + + // 생존자를 출력합니다. + cout << "The survivor is: " << josephus(N, K) << endl; + return 0; +} + +/* +동작 과정 예시: +1. 큐 초기화: [1, 2, 3, 4, 5] +2. 첫 번째 반복: + - [1, 2, 3, 4, 5] -> [2, 3, 4, 5, 1] -> [3, 4, 5, 1] +3. 두 번째 반복: + - [3, 4, 5, 1] -> [4, 5, 1, 3] -> [5, 1, 3] +4. 세 번째 반복: + - [5, 1, 3] -> [1, 3, 5] -> [3, 5] +5. 네 번째 반복: + - [3, 5] -> [5, 3] -> [3] +6. 생존자: 3 +*/ diff --git a/Algorithm_with_DataStructure/unionfind.cpp b/Algorithm_with_DataStructure/unionfind.cpp index 60d8552..50990f4 100644 --- a/Algorithm_with_DataStructure/unionfind.cpp +++ b/Algorithm_with_DataStructure/unionfind.cpp @@ -6,22 +6,25 @@ #include -using namespace std; +const int MAX_SIZE = 10; // 예제에서는 최대 요소 수를 10으로 설정 -const int MAX_SIZE = 100; // 집합의 최대 크기, 필요에 따라 조정 가능 -int parent[MAX_SIZE]; // 각 원소의 부모 원소를 저장 -int nodeRank[MAX_SIZE]; // 각 트리의 높이를 저장 +int parent[MAX_SIZE]; +int rank[MAX_SIZE]; -// n개의 원소에 대해 각각을 독립적인 집합으로 초기화 +// 초기화 함수: n개의 요소로 초기화 +// 이 함수는 각 요소를 자신을 루트로 가리키도록 초기화합니다. +// 시간복잡도: O(n) void initialize(int n) { - for (int i = 0; i < n; i++) { - parent[i] = i; // 초기에 각 원소의 부모는 자기 자신 - nodeRank[i] = 0; // 초기 트리의 높이는 0 + for (int i = 0; i < n; ++i) { + parent[i] = i; // 초기에는 모든 요소가 자기 자신을 부모로 가짐 + rank[i] = 0; // 초기 랭크는 0으로 설정 } } -// Find 함수: 주어진 원소 x의 루트 원소를 찾는다. -// 경로 압축을 사용하여 효율성 증가: find를 실행하는 동안 만나는 모든 노드를 직접 루트에 연결 +// find 함수: 경로 압축 기법을 사용하여 루트 찾기 +// 이 함수는 경로 압축 기법을 사용하여 트리의 루트를 찾습니다. +// 경로 압축은 트리의 높이를 낮추어 이후 연산의 효율성을 높입니다. +// 시간복잡도: 거의 O(1)로 간주될 수 있습니다 (아커만 함수의 역함수에 비례). int find(int x) { if (parent[x] != x) { parent[x] = find(parent[x]); // 경로 압축 @@ -29,63 +32,118 @@ int find(int x) { return parent[x]; } -// Union 함수: 두 원소 x, y가 포함된 집합을 합친다. -// 랭크를 사용하여 두 트리를 균형있게 합침 +// union 함수: rank를 사용하여 두 집합 합치기 +// 이 함수는 두 집합을 합치기 위해 랭크를 사용하여 트리의 높이를 최적화합니다. +// 더 낮은 랭크의 트리를 더 높은 랭크의 트리 밑에 붙입니다. +// 시간복잡도: 거의 O(1)로 간주될 수 있습니다 (아커만 함수의 역함수에 비례). void unionSets(int x, int y) { int rootX = find(x); int rootY = find(y); - if (rootX != rootY) { // 두 원소가 다른 집합에 속해있는 경우 - if (nodeRank[rootX] > nodeRank[rootY]) { - parent[rootY] = rootX; // 더 높은 랭크의 트리 아래에 낮은 랭크의 트리를 합침 - } else if (nodeRank[rootX] < nodeRank[rootY]) { - parent[rootX] = rootY; // 반대 경우 + if (rootX != rootY) { + // rank가 더 낮은 트리를 더 높은 트리 밑에 붙임 + if (rank[rootX] > rank[rootY]) { + parent[rootY] = rootX; + } else if (rank[rootX] < rank[rootY]) { + parent[rootX] = rootY; } else { parent[rootY] = rootX; - nodeRank[rootX]++; // 같은 높이의 트리를 합치면 결과 트리의 높이가 증가 + ++rank[rootX]; } } } int main() { - int n = 10; // 예시로 10개의 원소로 집합을 생성 + int n = 10; // 예제용 요소 수 initialize(n); - // 연산 수행 및 각 스텝에서의 트리 변화 설명 - unionSets(0, 1); + // 초기 상태: + // 각 요소가 자신을 루트로 가짐 + // 0 1 2 3 4 5 6 7 8 9 + // | | | | | | | | | | + // 0 1 2 3 4 5 6 7 8 9 + + // unionSets(1, 2) 호출 후: unionSets(1, 2); + // 트리 구조: + // 1 + // | + // 2 + // + // 부모 배열: [0, 1, 1, 3, 4, 5, 6, 7, 8, 9] + // 랭크 배열: [0, 1, 0, 0, 0, 0, 0, 0, 0, 0] + + // unionSets(3, 4) 호출 후: unionSets(3, 4); - unionSets(0, 4); - - // 모든 원소에 대해 find를 호출하여 경로 압축 적용 - for (int i = 0; i < n; i++) { - find(i); - } - - // 최종 트리 구조 출력 - for (int i = 0; i < n; i++) { - cout << "Parent of " << i << ": " << parent[i] << ", nodeRank: " << nodeRank[i] << endl; - } + // 트리 구조: + // 3 + // | + // 4 + // + // 부모 배열: [0, 1, 1, 3, 3, 5, 6, 7, 8, 9] + // 랭크 배열: [0, 1, 0, 1, 0, 0, 0, 0, 0, 0] + + // unionSets(2, 4) 호출 후: + unionSets(2, 4); + // 트리 구조: + // 1 + // / \ + // 2 3 + // | + // 4 + // + // 부모 배열: [0, 1, 1, 1, 1, 5, 6, 7, 8, 9] + // 랭크 배열: [0, 2, 0, 1, 0, 0, 0, 0, 0, 0] + + // unionSets(5, 6) 호출 후: + unionSets(5, 6); + // 트리 구조: + // 5 + // | + // 6 + // + // 부모 배열: [0, 1, 1, 1, 1, 5, 5, 7, 8, 9] + // 랭크 배열: [0, 2, 0, 1, 0, 1, 0, 0, 0, 0] + + // find 연산 수행 및 결과 출력 + std::cout << "find(1): " << find(1) << std::endl; // 1 + // 트리 구조: 변화 없음 + // 1 + // / \ + // 2 3 + // | + // 4 + + std::cout << "find(2): " << find(2) << std::endl; // 1 + // 트리 구조: + // 1 + // /|\ + // 2 3 4 + // 경로 압축이 일어나 2와 4가 직접 1을 가리키게 됩니다. + + std::cout << "find(3): " << find(3) << std::endl; // 1 + // 트리 구조: 변화 없음 + // 1 + // /|\ + // 2 3 4 + + std::cout << "find(4): " << find(4) << std::endl; // 1 + // 트리 구조: 변화 없음 + // 1 + // /|\ + // 2 3 4 + + std::cout << "find(5): " << find(5) << std::endl; // 5 + // 트리 구조: 변화 없음 + // 5 + // | + // 6 + + std::cout << "find(6): " << find(6) << std::endl; // 5 + // 트리 구조: 변화 없음 + // 5 + // | + // 6 return 0; } - -/* -최종 트리 구조 (경로 압축 후): -최종 트리 구조 (경로 압축 후): - 0 (root) - /|\ - / | \ - 1 3 4 - | - 2 - -설명: -- 노드 0이 루트 노드입니다. -- 노드 1, 3, 4는 직접적으로 노드 0에 연결되어 있습니다. -- 노드 2는 노드 1에 연결되어 있었지만, 경로 압축을 통해 직접 노드 0에 연결되어 있습니다. -- 노드 5, 6, 7, 8, 9는 각각 자기 자신이 루트입니다 (독립적인 집합 그림에 표시하지 않음). - -이 구조는 경로 압축 기법이 적용된 후 모든 노드가 가능한 직접적으로 루트 노드에 연결되도록 최적화되어 있음을 보여줍니다. - -*/ diff --git a/Algorithm_with_DataStructure/unionfind_not_optimization.cpp b/Algorithm_with_DataStructure/unionfind_not_optimization.cpp new file mode 100644 index 0000000..43f6a66 --- /dev/null +++ b/Algorithm_with_DataStructure/unionfind_not_optimization.cpp @@ -0,0 +1,124 @@ +#include + +const int MAX_SIZE = 10; // 예제에서는 최대 요소 수를 10으로 설정 + +int parent[MAX_SIZE]; + +// 초기화 함수: n개의 요소로 초기화 +void initialize(int n) { + for (int i = 0; i < n; ++i) { + parent[i] = i; // 초기에는 모든 요소가 자기 자신을 부모로 가짐 + } +} + +// find 함수: 루트를 찾기 위한 기본적인 방법 +// 이 함수는 트리의 루트를 찾기 위해 부모 배열을 순회합니다. +// 시간복잡도: O(n) (최악의 경우, 트리가 한쪽으로 치우쳐 있을 때) +int find(int x) { + while (parent[x] != x) { + x = parent[x]; + } + return x; +} + +// union 함수: 두 집합을 합침 +// 이 함수는 두 집합을 합치는 작업을 수행합니다. +// 시간복잡도: O(n) (find 함수의 시간복잡도에 의해 결정됨) +void unionSets(int x, int y) { + int rootX = find(x); + int rootY = find(y); + + if (rootX != rootY) { + parent[rootY] = rootX; // 단순히 y의 루트를 x의 루트로 변경 + } +} + +int main() { + int n = 10; // 예제용 요소 수 + initialize(n); + + // 초기 상태: + // 각 요소가 자신을 루트로 가짐 + // 0 1 2 3 4 5 6 7 8 9 + // | | | | | | | | | | + // 0 1 2 3 4 5 6 7 8 9 + + // unionSets(1, 2) 호출 후: + unionSets(1, 2); + // 트리 구조: + // 1 + // | + // 2 + // + // 부모 배열: [0, 1, 1, 3, 4, 5, 6, 7, 8, 9] + + // unionSets(3, 4) 호출 후: + unionSets(3, 4); + // 트리 구조: + // 3 + // | + // 4 + // + // 부모 배열: [0, 1, 1, 3, 3, 5, 6, 7, 8, 9] + + // unionSets(2, 4) 호출 후: + unionSets(2, 4); + // 트리 구조: + // 1 + // / + // 2 + // | + // 3 + // | + // 4 + // + // 부모 배열: [0, 1, 1, 1, 1, 5, 6, 7, 8, 9] + + // unionSets(5, 6) 호출 후: + unionSets(5, 6); + // 트리 구조: + // 5 + // | + // 6 + // + // 부모 배열: [0, 1, 1, 1, 1, 5, 5, 7, 8, 9] + + // find 연산 수행 및 결과 출력 + std::cout << "find(1): " << find(1) << std::endl; // 1 + // 트리 구조: 변화 없음 + // 1 + // /|\ + // 2 3 4 + + std::cout << "find(2): " << find(2) << std::endl; // 1 + // 트리 구조: 변화 없음 + // 1 + // /|\ + // 2 3 4 + + std::cout << "find(3): " << find(3) << std::endl; // 1 + // 트리 구조: 변화 없음 + // 1 + // /|\ + // 2 3 4 + + std::cout << "find(4): " << find(4) << std::endl; // 1 + // 트리 구조: 변화 없음 + // 1 + // /|\ + // 2 3 4 + + std::cout << "find(5): " << find(5) << std::endl; // 5 + // 트리 구조: 변화 없음 + // 5 + // | + // 6 + + std::cout << "find(6): " << find(6) << std::endl; // 5 + // 트리 구조: 변화 없음 + // 5 + // | + // 6 + + return 0; +} diff --git a/Algorithm_with_DataStructure/unionfind_with_unordered_map.cpp b/Algorithm_with_DataStructure/unionfind_with_unordered_map.cpp new file mode 100644 index 0000000..a67a3c0 --- /dev/null +++ b/Algorithm_with_DataStructure/unionfind_with_unordered_map.cpp @@ -0,0 +1,74 @@ +//############################################################ +// | cafe | http://cafe.naver.com/dremdelover | +// | Q&A | https://open.kakao.com/o/gX0WnTCf | +// | business | ultrasuperrok@gmail.com | +//############################################################ +#include +#include + +using namespace std; + +// 노드 값을 키로 가지는 부모와 랭크를 저장하는 맵 +unordered_map parent; +unordered_map rank; + +// 초기화 함수: 주어진 노드를 초기화 +// 이 함수는 주어진 노드를 초기화하여 자기 자신을 루트로 가지도록 합니다. +// 시간복잡도: O(1) (각 노드의 초기화는 상수 시간에 이루어짐) +void initialize(long long x) { + parent[x] = x; // 초기에는 모든 노드가 자기 자신을 부모로 가짐 + rank[x] = 0; // 초기 랭크는 0으로 설정 +} + +// find 함수: 경로 압축 기법을 사용하여 루트 찾기 +// 이 함수는 경로 압축 기법을 사용하여 주어진 노드의 루트 노드를 찾습니다. +// 경로 압축을 통해 트리의 높이를 줄여 이후 연산의 효율성을 높입니다. +// 시간복잡도: 거의 O(1)로 간주될 수 있습니다 (아커만 함수의 역함수에 비례). +long long find(long long x) { + if (parent[x] != x) { + parent[x] = find(parent[x]); // 경로 압축 + } + return parent[x]; +} + +// union 함수: rank를 사용하여 두 집합 합치기 +// 이 함수는 두 집합을 합치는 작업을 수행합니다. +// 두 집합의 루트를 비교하여, 더 낮은 랭크의 트리를 더 높은 랭크의 트리 밑에 붙여 트리의 높이를 최소화합니다. +// 시간복잡도: 거의 O(1)로 간주될 수 있습니다 (아커만 함수의 역함수에 비례). +void unionSets(long long x, long long y) { + long long rootX = find(x); + long long rootY = find(y); + + if (rootX != rootY) { + // rank가 더 낮은 트리를 더 높은 트리 밑에 붙임 + if (rank[rootX] > rank[rootY]) { + parent[rootY] = rootX; + } else if (rank[rootX] < rank[rootY]) { + parent[rootX] = rootY; + } else { + parent[rootY] = rootX; + ++rank[rootX]; + } + } +} + +int main() { + // 노드 값이 매우 큰 경우를 가정하여 몇 가지 노드를 초기화 + initialize(1000000000); + initialize(1000000001); + initialize(2000000000); + initialize(2000000001); + + // unionSets 연산 수행 + unionSets(1000000000, 1000000001); + unionSets(2000000000, 2000000001); + unionSets(1000000001, 2000000001); + + // find 연산 수행 및 결과 출력 + cout << "find(1000000000): " << find(1000000000) << endl; + cout << "find(1000000001): " << find(1000000001) << endl; + cout << "find(2000000000): " << find(2000000000) << endl; + cout << "find(2000000001): " << find(2000000001) << endl; + + return 0; +} diff --git a/Example/Algorithm_with_DataStructure/bactracking_structure.cpp b/Example/Algorithm_with_DataStructure/bactracking_structure.cpp new file mode 100644 index 0000000..0b672b7 --- /dev/null +++ b/Example/Algorithm_with_DataStructure/bactracking_structure.cpp @@ -0,0 +1,36 @@ +//############################################################ +// | cafe | http://cafe.naver.com/dremdelover | +// | Q&A | https://open.kakao.com/o/gX0WnTCf | +// | business | ultrasuperrok@gmail.com | +//############################################################ + +/* +상태 정의 : 문제의 각 단계에서 가능한 상태를 정의 +유망 함수(isPromising) : 현재 상태가 유망한지 판단, 유망하지 않으면 더 이상 탐색 x +해결책 확인(isSolution) : 현재 상태가 문제의 해결책인지 판단 +재귀 호출 : 유망한 상태로 이동하면서 문제 해결 +*/ +// 유망성 판단 함수 +bool isPromising(int level, State state) { + // 현재 상태가 유망한지 판단하는 로직을 구현합니다. +} + +// 해결책 확인 함수 +bool isSolution(State state) { + // 현재 상태가 문제의 해결책인지 판단하는 로직을 구현합니다. +} + +// 백트래킹 함수 +void backtrack(int level, State state) { + // 현재 상태가 해결책이면 처리 + if (isSolution(state)) { + processSolution(state); + return; + } + // 다음 가능한 상태를 탐색 + for (State nextState : possibleStates(state)) { + if (isPromising(level, nextState)) { + backtrack(level + 1, nextState); + } + } +} diff --git a/Example/FindCombinationsSumToTarget.cpp b/Example/FindCombinationsSumToTarget.cpp new file mode 100644 index 0000000..8b1912a --- /dev/null +++ b/Example/FindCombinationsSumToTarget.cpp @@ -0,0 +1,81 @@ +//############################################################ +// | cafe | http://cafe.naver.com/dremdelover | +// | Q&A | https://open.kakao.com/o/gX0WnTCf | +// | business | ultrasuperrok@gmail.com | +//############################################################ + +#include +#include + +using namespace std; + +// 사용할 숫자들 +vector nums = {1, 2, 3, 4}; +// 목표 합 +int target = 5; +// 현재 조합 +vector current; + +void findCombinations(int index) { + int sum = 0; + for (int num : current) { + sum += num; + } + + // 조건 1: 현재 조합으로 합이 target이 되면 결과를 출력하고 더 탐색하지 않음 + if (sum == target) { + for (int num : current) { + cout << num << " "; + } + cout << endl; + return; + } + + // 조건 2: 합이 target을 초과하면 더 탐색하지 않음 + if (sum > target) { + return; + } + + // 유망한 경우에만 다음 숫자를 추가하여 탐색을 계속함 + for (int i = index; i < nums.size(); ++i) { + current.push_back(nums[i]); + findCombinations(i + 1); + current.pop_back(); // 현재 숫자를 조합에서 제외하여 다음 경우를 탐색 + } +} + +int main() { + findCombinations(0); + return 0; +} + +/* +출력값: +1 4 +2 3 +*/ + + +/* +Call trace: findCombinations({ 1 }, 1, 1) + Call trace: findCombinations({ 1 2 }, 2, 2) + Call trace: findCombinations({ 1 2 3 }, 3, 3) + Pruned branch: sum(6) > target(5) // Pruned branch: 현재 합이 목표 값보다 크므로 더 이상 탐색 하지 않음. + Call trace: findCombinations({ 1 2 4 }, 4, 3) + Pruned branch: sum(7) > target(5) // Pruned branch: 현재 합이 목표 값보다 크므로 더 이상 탐색하지 않음. + Call trace: findCombinations({ 1 3 }, 3, 2) + Call trace: findCombinations({ 1 3 4 }, 4, 3) + Pruned branch: sum(8) > target(5) // Pruned branch: 현재 합이 목표 값보다 크므로 더 이상 탐색하지 않음. + Call trace: findCombinations({ 1 4 }, 4, 2) + Found combination: { 1 4 } // Found combination: 현재 합이 목표 값과 일치하므로 조합을 출력하고 백트래킹함. +Call trace: findCombinations({ 2 }, 2, 1) + Call trace: findCombinations({ 2 3 }, 3, 2) + Found combination: { 2 3 } // Found combination: 현재 합이 목표 값과 일치하므로 이 조합을 출력하고 백트래킹함. + Call trace: findCombinations({ 2 4 }, 4, 2) + Pruned branch: sum(6) > target(5) // Pruned branch: 현재 합이 목표 값보다 크므로 더 이상 탐색하지 않음. +Call trace: findCombinations({ 3 }, 3, 1) + Call trace: findCombinations({ 3 4 }, 4, 2) + Pruned branch: sum(7) > target(5) // Pruned branch: 현재 합이 목표 값보다 크므로 더 이상 탐색하지 않음. +Call trace: findCombinations({ 4 }, 4, 1) + +*/ diff --git a/Example/MST(prim).cpp b/Example/MST(prim).cpp new file mode 100644 index 0000000..beaad00 --- /dev/null +++ b/Example/MST(prim).cpp @@ -0,0 +1,97 @@ +#include +#include +#include +#include + +using namespace std; + +typedef pair Edge; // (비용, 정점) + +vector> graph; // 그래프를 전역 변수로 선언 + +// 문제의 정의: +// 주어진 그래프에서 모든 정점을 최소 비용으로 연결하는 최소 신장 트리(MST)를 찾는 문제입니다. +// 정점은 1부터 6까지 번호가 매겨져 있으며, 간선에는 가중치가 부여되어 있습니다. + +// 프림 알고리즘 설명: +// 프림 알고리즘은 하나의 시작 정점에서 출발하여 MST를 단계적으로 확장해 나가는 방식입니다. +// 1. 시작 정점을 선택하고, 해당 정점을 MST 집합에 추가합니다. +// 2. MST 집합에 인접한 간선 중에서 가장 가중치가 작은 간선을 선택하여 연결된 정점을 MST 집합에 추가합니다. +// 3. 이 과정을 모든 정점이 MST 집합에 포함될 때까지 반복합니다. +// 우선순위 큐를 사용하여 현재 정점에서 가장 가중치가 작은 간선을 효율적으로 선택할 수 있습니다. + +// 왜 그리디로 풀리는지: +// 탐욕적 선택 속성: 매 단계에서 현재 MST에 인접한 간선 중에서 가장 비용이 적은 간선을 선택하여 MST를 확장합니다. +// 이렇게 하면 항상 부분 문제의 최적해가 전체 문제의 최적해로 이어집니다. +// 최적 부분구조: 현재 선택한 간선이 전체 최소 신장 트리의 일부라면, 남은 간선들로 구성된 부분 문제도 최소 신장 트리가 됩니다. +// 즉, 작은 문제를 해결하여 전체 문제를 해결할 수 있습니다. + +// greater 설명: +// priority_queue는 기본적으로 큰 값을 먼저 꺼내는 최대 힙입니다. 하지만 프림 알고리즘에서는 가장 작은 가중치를 먼저 선택해야 하므로 +// greater를 사용하여 최소 힙을 만듭니다. 이는 Edge의 첫 번째 요소(비용)를 기준으로 오름차순 정렬하여 작은 값이 먼저 나오도록 합니다. + +// 그래프를 인접 리스트 형태로 표현 +void addEdge(int u, int v, int weight) { + graph[u].push_back(make_pair(weight, v)); + graph[v].push_back(make_pair(weight, u)); +} + +// 프림 알고리즘으로 최소 신장 트리 구하기 +int primMST(int V) { + priority_queue, greater> pq; + vector key(V + 1, INT_MAX); // 최소 비용을 저장하는 배열 (정점 번호 1부터 사용) + vector inMST(V + 1, false); // MST에 포함 여부를 저장하는 배열 + int result = 0; // MST의 총 비용 + + // 시작 정점은 1로 설정하고 초기화 + key[1] = 0; + pq.push(make_pair(0, 1)); + + while (!pq.empty()) { + int u = pq.top().second; + pq.pop(); + + // 이미 MST에 포함된 정점은 무시 + if (inMST[u]) { + continue; + } + + // 현재 정점을 MST에 포함시키고 비용을 더함 + inMST[u] = true; + result += key[u]; + + // 인접한 정점의 비용을 업데이트 + for (auto& edge : graph[u]) { + int weight = edge.first; + int v = edge.second; + + // 아직 MST에 포함되지 않고, 현재 정점을 통해 더 저렴하게 갈 수 있는 경우 + if (!inMST[v] && key[v] > weight) { + key[v] = weight; + pq.push(make_pair(key[v], v)); + } + } + } + + return result; +} + +int main() { + int V = 6; // 정점의 개수 + graph.resize(V + 1); // 정점 번호 1부터 사용 + + // 그래프의 간선 추가 (이미지를 참고하여 작성) + addEdge(1, 2, 9); + addEdge(1, 4, 7); + addEdge(2, 4, 8); + addEdge(2, 3, 2); + addEdge(2, 6, 5); + addEdge(3, 6, 3); + addEdge(3, 4, 6); + addEdge(3, 5, 4); + addEdge(4, 5, 5); + + cout << "최소 신장 트리의 비용: " << primMST(V) << endl; + + return 0; +} diff --git a/Example/NQueen.cpp b/Example/NQueen.cpp new file mode 100644 index 0000000..b6aff6a --- /dev/null +++ b/Example/NQueen.cpp @@ -0,0 +1,95 @@ +//############################################################ +// | cafe | http://cafe.naver.com/dremdelover | +// | Q&A | https://open.kakao.com/o/gX0WnTCf | +// | business | ultrasuperrok@gmail.com | +//############################################################ +#include +#include +#include + +using namespace std; + +// N Queen 문제: n x n 체스판에 n개의 퀸을 서로 공격하지 못하게 배치하는 문제 +const int N = 4; // 퀸의 개수 +vector board(N, -1); // 보드 배열, 인덱스는 행을, 값은 열을 나타냄 + +// 보드의 현재 상태를 출력하는 함수 +void printBoard() { + // 각 행을 반복 + for (int i = 0; i < N; ++i) { + // 각 열을 반복 + for (int j = 0; j < N; ++j) { + // 현재 위치에 퀸이 있는지 확인 + if (board[i] == j) { + cout << "Q "; // 퀸이 있으면 "Q" 출력 + } else { + cout << ". "; // 퀸이 없으면 "." 출력 + } + } + cout << endl; // 한 행이 끝나면 줄바꿈 + } + cout << endl; // 보드 전체 출력 후 줄바꿈 +} + +// 현재 위치에 퀸을 놓아도 되는지 확인하는 유망함수 +bool isSafe(int row, int col) { + // 현재 행 이전의 모든 행을 검사 + for (int i = 0; i < row; ++i) { + // 1. 같은 열에 퀸이 있는지 확인 + if (board[i] == col) { + return false; // 같은 열에 퀸이 있으면 false 반환 + } + // 2. 같은 대각선에 퀸이 있는지 확인 + if (abs(board[i] - col) == abs(i - row)) { + return false; // 같은 대각선에 퀸이 있으면 false 반환 + } + } + return true; // 어떤 충돌도 없으면 true 반환 +} + +// N Queen 문제를 해결하기 위한 재귀 함수 +void solveNQueens(int row, int &solutions) { + // 모든 행에 퀸을 배치한 경우 (기저 조건) + if (row == N) { + // 해결책을 찾았으므로 해결책 수 증가 + solutions++; + // 현재 보드 상태를 출력 + printBoard(); + return; // 함수 종료 + } + + // 현재 행의 모든 열에 대해 반복 + for (int col = 0; col < N; ++col) { + // 현재 위치에 퀸을 놓을 수 있는지 확인 + if (isSafe(row, col)) { + board[row] = col; // 현재 행의 열에 퀸을 놓음 + solveNQueens(row + 1, solutions); // 다음 행에 대해 재귀 호출 + // 퀸을 제거할 필요 없음, 다음 반복에서 덮어쓰기 때문에 + } + } +} + +int main() { + int solutions = 0; // 해결책 수를 저장할 변수 + + solveNQueens(0, solutions); // 첫 번째 행부터 시작 + + cout << "총 해결책 수: " << solutions << endl; // 총 해결책 수 출력 + + return 0; +} + +/* + +. Q . . +. . . Q +Q . . . +. . Q . + +. . Q . +Q . . . +. . . Q +. Q . . + +총 해결책 수: 2 +*/ diff --git a/Example/backtracking_sturcure.cpp b/Example/backtracking_sturcure.cpp new file mode 100644 index 0000000..4ba14b5 --- /dev/null +++ b/Example/backtracking_sturcure.cpp @@ -0,0 +1,37 @@ +//############################################################ +// | cafe | http://cafe.naver.com/dremdelover | +// | Q&A | https://open.kakao.com/o/gX0WnTCf | +// | business | ultrasuperrok@gmail.com | +//############################################################ + +/* +상태 정의 : 문제의 각 단계에서 가능한 상태를 정의 +유망 함수(isPromising) : 현재 상태가 유망한지 판단, 유망하지 않으면 더 이상 탐색 x +해결책 확인(isSolution) : 현재 상태가 문제의 해결책인지 판단 +재귀 호출 : 유망한 상태로 이동하면서 문제 해결 +*/ + +// 유망성 판단 함수 +bool isPromising(int level, State state) { + // 현재 상태가 유망한지 판단하는 로직을 구현합니다. +} + +// 해결책 확인 함수 +bool isSolution(State state) { + // 현재 상태가 문제의 해결책인지 판단하는 로직을 구현합니다. +} + +// 백트래킹 함수 +void backtrack(int level, State state) { + // 현재 상태가 해결책이면 처리 + if (isSolution(state)) { + processSolution(state); + return; + } + // 다음 가능한 상태를 탐색 + for (State nextState : possibleStates(state)) { + if (isPromising(level, nextState)) { + backtrack(level + 1, nextState); + } + } +} diff --git a/Example/chageCoin.cpp b/Example/chageCoin.cpp new file mode 100644 index 0000000..8b9b8d2 --- /dev/null +++ b/Example/chageCoin.cpp @@ -0,0 +1,57 @@ +#include +#include + +using namespace std; + +// 문제의 정의: +// 주어진 거스름돈을 1원, 5원, 10원, 50원, 100원, 500원의 동전을 사용하여 +// 최소 개수의 동전으로 거슬러 주는 문제입니다. + +// 왜 그리디로 풀리는지: +// 탐욕적 선택 속성: 매 단계에서 가장 큰 단위의 동전을 선택하여, +// 나머지 금액을 거슬러 줍니다. 큰 단위의 동전을 선택하는 것이 항상 +// 최적의 해를 보장합니다. +// 최적 부분구조: 현재 금액을 가장 큰 단위로 거슬러 준 후, +// 남은 금액에 대해 같은 문제를 반복하여 해결할 수 있습니다. +// 즉, 부분 문제의 최적해가 전체 문제의 최적해로 이어집니다. + +// 왜 최적의 해를 보장하는가: +// 1. 탐욕적 선택 속성: 가장 큰 단위의 동전을 선택하는 것이 +// 최적의 해를 보장하는 이유는, 큰 단위의 동전이 작은 단위의 동전보다 +// 더 많은 금액을 커버할 수 있기 때문입니다. 예를 들어, 500원을 선택하면 +// 100원을 5개 사용하는 것보다 적은 개수의 동전으로 같은 금액을 커버할 수 있습니다. +// 2. 최적 부분구조: 주어진 문제를 작은 부분 문제로 나눌 수 있으며, +// 각 부분 문제의 최적해가 전체 문제의 최적해를 구성합니다. 예를 들어, +// 837원을 거슬러 줄 때, 500원을 선택하면 남은 337원을 거슬러 주는 문제로 +// 축소됩니다. 이때, 337원을 최적으로 거슬러 주는 방법이 전체 문제를 최적으로 +// 해결하는 방법이 됩니다. + +int main() { + // 거스름돈 변수 (예: 837원) + int change = 837; + + // 동전 단위 배열 (큰 단위부터) + vector coins = {500, 100, 50, 10, 5, 1}; + vector count(coins.size(), 0); + + // 코드 상세 동작: + // 큰 단위의 동전부터 차례로 거스름돈을 나누어 줍니다. + for (int i = 0; i < coins.size(); i++) { + if (change >= coins[i]) { + // 현재 동전 단위로 거슬러 줄 수 있는 최대 개수를 계산합니다. + count[i] = change / coins[i]; + // 거슬러 준 금액을 제외한 나머지 금액을 계산합니다. + change %= coins[i]; + } + } + + // 결과 출력: 각 동전 단위별로 사용된 개수를 출력합니다. + cout << "최소 동전 개수: " << endl; + for (int i = 0; i < coins.size(); i++) { + if (count[i] != 0) { + cout << coins[i] << "원: " << count[i] << "개" << endl; + } + } + + return 0; +} diff --git a/Example/factorial_iterative.cpp b/Example/factorial_iterative.cpp new file mode 100644 index 0000000..fd2ddc4 --- /dev/null +++ b/Example/factorial_iterative.cpp @@ -0,0 +1,74 @@ +//############################################################ +// | cafe | http://cafe.naver.com/dremdelover | +// | Q&A | https://open.kakao.com/o/gX0WnTCf | +// | business | ultrasuperrok@gmail.com | +//############################################################ +#include +#include + +using namespace std; + +// 동적 계획법을 사용하여 팩토리얼을 계산하는 함수 +int factorial(int n) { + // 크기 n+1의 벡터를 1로 초기화합니다. + vector dp(n + 1, 1); + + // 팩토리얼 계산: dp[i]는 i! (i 팩토리얼)을 저장합니다. + for (int i = 2; i <= n; ++i) { + dp[i] = dp[i - 1] * i; + } + + // 결과로 n!을 반환합니다. + return dp[n]; +} + +int main() { + int n = 5; // 예시로 5의 팩토리얼을 계산 + cout << n << "! = " << factorial(n) << endl; + + n = 7; // 예시로 7의 팩토리얼을 계산 + cout << n << "! = " << factorial(n) << endl; + + return 0; +} + +/* +동적 계획법(DP)와 팩토리얼 계산: + +1. 동적 계획법(DP) 기본 원리: + - 큰 문제를 작은 부분 문제로 분할하여 해결. + - 동일한 부분 문제의 반복 계산을 피하기 위해 결과를 저장(memoization). + - 시간 복잡도를 줄이고 효율성을 높이는 기법. + +2. 중복 부분 문제(Overlapping Subproblems): + - 동일한 작은 문제들이 여러 번 반복하여 계산되는 문제. + - 예: 피보나치 수열에서 F(n) = F(n-1) + F(n-2) 계산 시, F(n-1)과 F(n-2)가 여러 번 중복 계산됨. + - 동적 계획법은 이러한 중복 계산을 피하여 효율성을 높임. + +3. 최적 부분 구조(Optimal Substructure): + - 문제의 최적 해결 방법이 부분 문제의 최적 해결 방법으로 구성되는 성질. + - 팩토리얼 계산에서 n! = n * (n-1)!의 형태로 나타남. + - 예: 최단 경로 문제에서, 전체 경로의 최적해는 부분 경로의 최적해로 구성됨. + +4. 팩토리얼 계산의 기본 원리: + - 팩토리얼(n!)은 n * (n-1) * (n-2) * ... * 1의 곱셈으로 계산. + - 즉, n! = n * (n-1)! + +5. 팩토리얼 계산에 동적 계획법 적용: + - dp[i]에 i!의 값을 저장하여 중복 계산을 피함. + - 예: dp[3] = 3 * dp[2], dp[2] = 2 * dp[1] 등. + +6. 팩토리얼 계산의 시간 복잡도: + - 반복문을 사용하면 O(n) 시간 복잡도로 계산 가능. + - 동적 계획법을 사용해도 시간 복잡도는 여전히 O(n). + +7. 동적 계획법의 효율성 향상이 어려운 이유: + - 팩토리얼 문제는 중복 부분 문제가 존재하지 않음. + - 각 단계에서 이전 단계의 결과를 단순히 곱하는 형태. + - 추가적인 메모리 공간 사용(dp 배열)이 필요하지만 시간 복잡도 개선 효과는 없음. + +결론: + - 팩토리얼 계산 문제는 최적 부분 구조를 가지지만, 중복 부분 문제가 없기 때문에 + 동적 계획법의 이점(시간 복잡도 감소)을 크게 활용할 수 없음. + - 따라서, DP를 적용해도 효율성 개선은 미미함. +*/ diff --git a/Example/fibonacci_iterative.cpp b/Example/fibonacci_iterative.cpp new file mode 100644 index 0000000..94d3b1b --- /dev/null +++ b/Example/fibonacci_iterative.cpp @@ -0,0 +1,77 @@ +//############################################################ +// | cafe | http://cafe.naver.com/dremdelover | +// | Q&A | https://open.kakao.com/o/gX0WnTCf | +// | business | ultrasuperrok@gmail.com | +//############################################################ + +#include +#include + +using namespace std; + +// 동적 계획법을 사용하여 피보나치 수열을 계산하는 함수 +int fibonacci(int n) { + // 크기 n+1의 벡터를 0으로 초기화합니다. + vector dp(n + 1, 0); + + // 초기 조건 설정 + dp[0] = 0; + if (n > 0) { + dp[1] = 1; + } + + // 피보나치 수열 계산: dp[i]는 i번째 피보나치 수를 저장합니다. + for (int i = 2; i <= n; ++i) { + dp[i] = dp[i - 1] + dp[i - 2]; + } + + // 결과로 n번째 피보나치 수를 반환합니다. + return dp[n]; +} + +int main() { + int n = 5; // 예시로 5번째 피보나치 수를 계산 + cout << "Fibonacci(" << n << ") = " << fibonacci(n) << endl; + + n = 10; // 예시로 10번째 피보나치 수를 계산 + cout << "Fibonacci(" << n << ") = " << fibonacci(n) << endl; + + return 0; +} + +/* +동적 계획법(DP)와 피보나치 수열 계산: + +1. 동적 계획법(DP) 기본 원리: + - 큰 문제를 작은 부분 문제로 분할하여 해결. + - 동일한 부분 문제의 반복 계산을 피하기 위해 결과를 저장(memoization). + - 시간 복잡도를 줄이고 효율성을 높이는 기법. + +2. 중복 부분 문제(Overlapping Subproblems): + - 동일한 작은 문제들이 여러 번 반복하여 계산되는 문제. + - 피보나치 수열의 경우, F(n) = F(n-1) + F(n-2)에서 F(n-1)과 F(n-2)가 여러 번 계산됨. + - 예: F(5)를 계산할 때, F(4)와 F(3)를 계산하고, F(4)를 계산할 때 다시 F(3)와 F(2)를 계산함. + - 동적 계획법은 이러한 중복 계산을 피하여 효율성을 높임. + +3. 최적 부분 구조(Optimal Substructure): + - 문제의 최적 해결 방법이 부분 문제의 최적 해결 방법으로 구성되는 성질. + - 피보나치 수열 계산에서 F(n)은 F(n-1)과 F(n-2)의 결과로 구성됨. + - 즉, F(n)의 최적해는 F(n-1)과 F(n-2)의 최적해로 구성됨. + +4. 피보나치 수열 계산의 기본 원리: + - 피보나치 수열은 다음과 같이 정의됨: + F(0) = 0, F(1) = 1 + F(n) = F(n-1) + F(n-2) (n >= 2) + +5. 피보나치 수열 계산에 동적 계획법 적용: + - dp[i]에 i번째 피보나치 수를 저장하여 중복 계산을 피함. + - 예: dp[5] = dp[4] + dp[3], dp[4] = dp[3] + dp[2] 등. + +6. 피보나치 수열 계산의 시간 복잡도: + - 반복문을 사용하면 O(n) 시간 복잡도로 계산 가능. + - 동적 계획법을 사용하면 중복 계산을 피하여 O(n) 시간 복잡도로 효율적 계산. + +결론: + - 피보나치 수열 문제는 DP의 이점(시간 복잡도 감소)을 크게 활용할 수 있음. + - 중복 부분 문제와 최적 부분 구조를 가지므로 DP를 적용하면 효율성 개선이 큼. +*/ diff --git a/Example/frequency_word.cpp b/Example/frequency_word.cpp new file mode 100644 index 0000000..530f71f --- /dev/null +++ b/Example/frequency_word.cpp @@ -0,0 +1,40 @@ +//############################################################ +// | cafe | http://cafe.naver.com/dremdelover | +// | Q&A | https://open.kakao.com/o/gX0WnTCf | +// | business | ultrasuperrok@gmail.com | +//############################################################ +#include +#include +#include + +using namespace std; + +int main() { + // 문자열 내 각 문자의 빈도를 저장할 unordered_map 선언 + unordered_map frequency; + + // 분석할 문자열 + string str = "hello world"; + + // 문자열의 각 문자를 순회하면서 빈도를 계산 + for (char ch : str) { + frequency[ch]++; + } + + // 각 문자의 빈도 출력 + for (const auto& pair : frequency) { + cout << pair.first << ": " << pair.second << "\n"; + } + + // 출력값: + // h: 1 + // e: 1 + // l: 3 + // o: 2 + // : 1 + // w: 1 + // r: 1 + // d: 1 + + return 0; +} diff --git a/Example/lis_iterative.cpp b/Example/lis_iterative.cpp new file mode 100644 index 0000000..7791ac3 --- /dev/null +++ b/Example/lis_iterative.cpp @@ -0,0 +1,81 @@ +//############################################################ +// | cafe | http://cafe.naver.com/dremdelover | +// | Q&A | https://open.kakao.com/o/gX0WnTCf | +// | business | ultrasuperrok@gmail.com | +//############################################################ + +#include +#include +#include + +using namespace std; + +// 동적 계획법을 사용하여 최장 증가 부분 수열(LIS)을 계산하는 함수 +int lis(vector& arr) { + int n = arr.size(); + vector dp(n, 1); // 각 원소를 초기값 1로 초기화 + + // LIS 계산 + for (int i = 1; i < n; ++i) { // arr[i]를 마지막 원소로 하는 LIS 계산 + for (int j = 0; j < i; ++j) { // arr[j] (0 <= j < i)를 마지막 원소로 하는 LIS 고려 + if (arr[i] > arr[j] && dp[i] < dp[j] + 1) { // arr[i]가 arr[j]보다 크고 dp[i]가 dp[j] + 1보다 작으면 + dp[i] = dp[j] + 1; // dp[i] 갱신 + } + } + } + + // dp 배열 중 최댓값이 LIS 길이 + return *max_element(dp.begin(), dp.end()); +} + +int main() { + vector arr = {10, 22, 9, 33, 21, 50, 41, 60, 80}; // 예시 배열 + cout << "LIS 길이: " << lis(arr) << endl; + + return 0; +} + +/* +동적 계획법(DP)와 최장 증가 부분 수열(LIS) 계산: + +1. 최장 증가 부분 수열(LIS) 개념: + - LIS는 주어진 수열에서 각 원소들이 증가하는 형태의 가장 긴 부분 수열을 의미. + - 예: 수열 [10, 22, 9, 33, 21, 50, 41, 60, 80]에서 LIS는 [10, 22, 33, 50, 60, 80]으로 길이는 6. + - 부분 수열은 원본 수열의 원소 순서를 유지해야 함. + +2. 동적 계획법(DP) 기본 원리: + - 큰 문제를 작은 부분 문제로 분할하여 해결. + - 동일한 부분 문제의 반복 계산을 피하기 위해 결과를 저장(memoization). + - 시간 복잡도를 줄이고 효율성을 높이는 기법. + +3. 중복 부분 문제(Overlapping Subproblems): + - 동일한 작은 문제들이 여러 번 반복하여 계산되는 문제. + - LIS의 경우, 특정 위치까지의 LIS 길이를 여러 번 계산하게 됨. + - 동적 계획법은 이러한 중복 계산을 피하여 효율성을 높임. + +4. 최적 부분 구조(Optimal Substructure): + - 문제의 최적 해결 방법이 부분 문제의 최적 해결 방법으로 구성되는 성질. + - LIS의 경우, arr[i]까지의 최적해는 arr[j] (j < i)까지의 최적해를 이용하여 구성됨. + - 예: arr[i]를 포함한 LIS는 arr[j] (arr[j] < arr[i])까지의 LIS에 arr[i]를 추가한 것. + +5. LIS 계산의 기본 원리: + - dp[i]는 arr[i]를 마지막 원소로 가지는 LIS의 길이를 저장. + - 초기값으로 모든 dp[i]를 1로 설정 (각 원소 자체가 길이 1의 LIS이므로). + +6. LIS 계산에 동적 계획법 적용: + - 이중 반복문을 사용하여 dp 배열을 갱신. + - 외부 반복문(i): arr[i]를 마지막 원소로 하는 LIS를 계산. + - 내부 반복문(j): arr[j]를 마지막 원소로 하는 LIS를 고려. + - dp[i] 갱신 조건: + - arr[i] > arr[j]인 경우: arr[i]가 arr[j] 뒤에 올 수 있는 경우. + - dp[i] < dp[j] + 1인 경우: arr[j]를 포함하는 LIS에 arr[i]를 추가하여 더 긴 LIS를 만들 수 있는 경우. + - dp[i] = dp[j] + 1로 갱신: 더 긴 LIS를 만들 수 있는 경우 dp[i] 갱신. + +7. LIS 계산의 시간 복잡도: + - 이중 반복문을 사용하므로 O(n^2) 시간 복잡도. + - dp 배열을 사용하여 중복 계산을 피하고 효율적으로 계산. + +결론: + - LIS 문제는 DP의 이점(시간 복잡도 감소)을 크게 활용할 수 있음. + - 중복 부분 문제와 최적 부분 구조를 가지므로 DP를 적용하면 효율성 개선이 큼. +*/ diff --git a/Example/managing_stdudent_multi_score.cpp b/Example/managing_stdudent_multi_score.cpp new file mode 100644 index 0000000..c9198ac --- /dev/null +++ b/Example/managing_stdudent_multi_score.cpp @@ -0,0 +1,89 @@ +//############################################################ +// | cafe | http://cafe.naver.com/dremdelover | +// | Q&A | https://open.kakao.com/o/gX0WnTCf | +// | business | ultrasuperrok@gmail.com | +//############################################################ + +#include +#include +#include +#include +#include // std::accumulate를 사용하기 위해 필요 + +using namespace std; + +// 특정 학생의 점수를 출력하는 함수 +void print_scores(const unordered_map>& student_scores, const string& name) { + // 학생이 존재하는지 확인 + auto it = student_scores.find(name); + if (it != student_scores.end()) { + cout << name << "의 점수: "; + // 학생의 모든 점수를 출력 + for (int score : it->second) { + cout << score << " "; + } + cout << endl; + } else { + // 학생이 존재하지 않을 경우 메시지 출력 + cout << name << "의 점수를 찾을 수 없습니다." << endl; + } +} + +// 특정 학생의 평균 점수를 계산하고 출력하는 함수 +void print_average_score(const unordered_map>& student_scores, const string& name) { + // 학생이 존재하는지 확인 + auto it = student_scores.find(name); + if (it != student_scores.end()) { + const vector& scores = it->second; // 학생의 점수 목록을 참조 + + // 평균 점수 계산 + // accumulate 함수는 주어진 범위의 요소를 모두 더하는 함수입니다. + // 첫 번째 인자는 시작 반복자, 두 번째 인자는 끝 반복자, 세 번째 인자는 초기값입니다. + // 이 경우 scores 벡터의 모든 요소를 더하고 초기값 0.0에서 시작하여 결과를 double로 반환합니다. + // accumulate 함수의 시간 복잡도는 O(n)입니다. 여기서 n은 벡터의 요소 개수입니다. + double average = accumulate(scores.begin(), scores.end(), 0.0) / scores.size(); + cout << name << "의 평균 점수: " << average << endl; + } else { + // 학생이 존재하지 않을 경우 메시지 출력 + cout << name << "의 점수를 찾을 수 없습니다." << endl; + } +} + +int main() { + // 학생 이름을 키로 하고, 그 학생의 점수 목록을 벡터로 저장하는 unordered_map 선언 + unordered_map> student_scores; + + // 학생들의 점수를 추가합니다. + student_scores["Alice"].push_back(90); // Alice의 점수 목록에 90 추가 + student_scores["Alice"].push_back(85); // Alice의 점수 목록에 85 추가 + student_scores["Bob"].push_back(78); // Bob의 점수 목록에 78 추가 + student_scores["Bob"].push_back(82); // Bob의 점수 목록에 82 추가 + student_scores["Bob"].push_back(88); // Bob의 점수 목록에 88 추가 + + // Alice의 점수를 출력 + print_scores(student_scores, "Alice"); + // Alice의 평균 점수를 출력 + print_average_score(student_scores, "Alice"); + + // Bob의 점수를 출력 + print_scores(student_scores, "Bob"); + // Bob의 평균 점수를 출력 + print_average_score(student_scores, "Bob"); + + // 존재하지 않는 학생 Charlie의 점수를 출력 시도 + print_scores(student_scores, "Charlie"); + // 존재하지 않는 학생 Charlie의 평균 점수를 출력 시도 + print_average_score(student_scores, "Charlie"); + + return 0; +} + +/* +출력 결과: +Alice의 점수: 90 85 +Alice의 평균 점수: 87.5 +Bob의 점수: 78 82 88 +Bob의 평균 점수: 82.6667 +Charlie의 점수를 찾을 수 없습니다. +Charlie의 점수를 찾을 수 없습니다. +*/ diff --git a/Example/managing_stdudent_one_score.cpp b/Example/managing_stdudent_one_score.cpp new file mode 100644 index 0000000..8b21e20 --- /dev/null +++ b/Example/managing_stdudent_one_score.cpp @@ -0,0 +1,34 @@ +//############################################################ +// | cafe | http://cafe.naver.com/dremdelover | +// | Q&A | https://open.kakao.com/o/gX0WnTCf | +// | business | ultrasuperrok@gmail.com | +//############################################################ +#include +#include +#include + +using namespace std; + +int main() { + // unordered_map을 사용하여 학생 ID를 키로, 성적을 값으로 저장 + unordered_map students; + + // 학생 정보 추가 + students["12345"] = "A"; // 학생 ID "12345"의 성적은 "A" + students["67890"] = "B"; // 학생 ID "67890"의 성적은 "B" + students["54321"] = "A+"; // 학생 ID "54321"의 성적은 "A+" + + // 특정 학생 정보 검색 + string student_id = "12345"; // 검색할 학생 ID 설정 + if (students.find(student_id) != students.end()) { + // 학생 ID가 존재하면 성적을 출력 + cout << "학생 ID: " << student_id << "\n"; // 학생 ID 출력 + cout << "성적: " << students[student_id] << "\n"; // 해당 학생의 성적 출력 + // 출력값: 성적: A + } else { + // 학생 ID가 존재하지 않으면 에러 메시지 출력 + cout << "학생 정보를 찾을 수 없습니다.\n"; + } + + return 0; +} diff --git a/Example/network_connectivity_union_find.cpp b/Example/network_connectivity_union_find.cpp new file mode 100644 index 0000000..76a29ae --- /dev/null +++ b/Example/network_connectivity_union_find.cpp @@ -0,0 +1,145 @@ +//############################################################ +// | cafe | http://cafe.naver.com/dremdelover | +// | Q&A | https://open.kakao.com/o/gX0WnTCf | +// | business | ultrasuperrok@gmail.com | +//############################################################ + +#include +#include +using namespace std; + +// Find 연산: 특정 원소가 속한 집합의 대표(루트) 원소를 찾는 함수입니다. +// 경로 압축(Path Compression)을 통해 트리의 높이를 낮춰 탐색 속도를 향상시킵니다. +// 이는 재귀적으로 부모 노드를 찾아가며, 각 노드의 부모를 직접 루트 노드로 갱신합니다. +int find(int parent[], int x) { + if (parent[x] != x) { + parent[x] = find(parent, parent[x]); // 경로 압축 적용 + } + return parent[x]; +} + +// Union 연산: 두 집합을 하나로 합치는 함수입니다. +// 유니온 바이 랭크(Union by Rank)를 통해 트리의 높이를 최소화하여 성능을 최적화합니다. +// 랭크가 높은 트리의 루트를 낮은 트리의 루트로 설정하여 트리의 균형을 유지합니다. +void unite(int parent[], int rank[], int x, int y) { + int rootX = find(parent, x); + int rootY = find(parent, y); + if (rootX != rootY) { + // 랭크가 높은 트리를 다른 트리에 합침 + if (rank[rootX] > rank[rootY]) { + parent[rootY] = rootX; + } else if (rank[rootX] < rank[rootY]) { + parent[rootX] = rootY; + } else { + parent[rootY] = rootX; + rank[rootX]++; + } + } +} + +// 두 노드가 같은 집합에 속해 있는지 확인하는 함수입니다. +// 두 노드의 루트 노드를 비교하여 같은 집합에 속하는지 여부를 반환합니다. +// 같은 루트 노드를 갖고 있으면 연결된 것입니다. +bool connected(int parent[], int x, int y) { + return find(parent, x) == find(parent, y); +} + +// 주어진 노드와 간선 목록, 쿼리 목록을 바탕으로 두 노드가 연결되어 있는지 확인하는 함수입니다. +vector solution(int n, int m, const vector>& edges, int q, const vector>& queries) { + int parent[n]; // 각 노드의 부모 노드를 저장하는 배열입니다. + int rank[n] = {0}; // 각 노드의 트리 높이를 저장하는 배열입니다. + + // 초기화: 모든 노드를 각각의 집합으로 설정합니다. + for (int i = 0; i < n; ++i) { + parent[i] = i; + } + + // 간선 정보 입력 및 Union 연산 수행합니다. + // 각 간선에 대해 Union 연산을 호출하여 두 노드를 같은 집합으로 만듭니다. + for (int i = 0; i < m; ++i) { + unite(parent, rank, edges[i].first, edges[i].second); + } + + vector result(q); // 결과를 저장할 벡터입니다. + for (int i = 0; i < q; ++i) { + if (connected(parent, queries[i].first, queries[i].second)) { + result[i] = 1; // 연결됨 + } else { + result[i] = 0; // 연결되지 않음 + } + } + + return result; // 결과를 반환합니다. +} + +int main() { + // 예시 입력 + int n = 6; + int m = 3; + vector> edges = {{0, 1}, {1, 2}, {3, 4}}; + int q = 2; + vector> queries = {{0, 2}, {0, 3}}; + + // solution 함수 호출 + vector result = solution(n, m, edges, q, queries); + + // 결과 출력 + for (int i = 0; i < q; ++i) { + cout << result[i] << endl; + } + + return 0; +} + +/* +입력 예시: +6 +3 +0 1 +1 2 +3 4 +2 +0 2 +0 3 + +출력 예시: +1 +0 + +제약사항: +- 1 <= n <= 10^4 +- 0 <= edges.length <= 10^4 +- 0 <= queries.length <= 10^4 +- edges[i].length == 2 +- queries[i].length == 2 +- 0 <= edges[i][j] < n +- 0 <= queries[i][j] < n + +입력값 설명: +- 첫 번째 줄: 그래프의 노드 개수 n +- 두 번째 줄: 간선의 개수 m +- 세 번째 줄부터: m개의 줄에 간선 목록, 각 줄마다 두 개의 정수 u와 v가 주어지며 이는 노드 u와 노드 v가 연결되었음을 의미합니다. +- 그 다음 줄: 쿼리 개수 q +- 그 다음 줄부터: q개의 줄에 쿼리 목록, 각 줄마다 두 개의 정수 x와 y가 주어지며 이는 노드 x와 노드 y가 연결되어 있는지 확인하는 쿼리입니다. + +출력값 설명: +- 각 쿼리에 대해 두 노드가 연결되어 있으면 1, 그렇지 않으면 0을 출력합니다. + +시간 복잡도: +- Find 연산: 평균적으로 O(α(N)), 여기서 α는 아커만 함수의 역함수로 매우 느리게 증가하는 함수입니다. + - 경로 압축(Path Compression)을 사용하여 트리의 높이를 최소화합니다. + - 따라서 대부분의 경우 거의 상수 시간에 가깝게 동작합니다. +- Union 연산: 평균적으로 O(α(N)) + - 유니온 바이 랭크(Union by Rank)를 사용하여 트리의 높이를 최소화합니다. + - 두 트리를 합칠 때, 트리의 높이가 더 작은 트리를 큰 트리에 합쳐 트리의 높이를 낮춥니다. +- 전체 시간 복잡도: O(N + M + Q * α(N)) + - N: 노드의 개수 + - M: 간선의 개수 + - Q: 쿼리의 개수 + - α(N): 아커만 함수의 역함수로, 매우 느리게 증가하여 사실상 상수 시간으로 간주될 수 있습니다. + +시간 복잡도 근거: +- 경로 압축과 유니온 바이 랭크 기법을 사용함으로써 각 연산의 시간 복잡도가 매우 낮아집니다. +- 아커만 함수의 역함수는 대부분의 실용적인 문제 크기에서 상수 시간에 가깝기 때문에, 전체적인 시간 복잡도는 매우 효율적입니다. +*/ + diff --git a/Example/squarecount.cpp b/Example/squarecount.cpp new file mode 100644 index 0000000..74332da --- /dev/null +++ b/Example/squarecount.cpp @@ -0,0 +1,64 @@ +#include +#include +#include + +using namespace std; + +const int ROW = 3; +const int COL = 4; + +// 문제의 정의: +// ROW x COL 크기의 사각형에서 만들 수 있는 모든 정사각형의 개수를 구하는 문제입니다. + +// 최적 부분 구조: +// 현재 셀에서 만들 수 있는 정사각형의 크기는 +// 위쪽, 왼쪽, 왼쪽 위 대각선 셀에서 만들 수 있는 정사각형 크기의 최솟값에 1을 더한 값입니다. + +// 중복 부분 문제: +// 동일한 부분 문제(예: 위쪽, 왼쪽, 왼쪽 위 대각선 셀)가 여러 번 계산됩니다. + +// 점화식: +// dp[i][j] = min(dp[i-1][j], dp[i][j-1], dp[i-1][j-1]) + 1 + +int countSquares(); + +int main() { + int result = countSquares(); + cout << "만들 수 있는 정사각형의 총 개수: " << result << endl; + return 0; +} + +// ROW x COL 크기의 사각형에서 만들 수 있는 모든 정사각형의 개수를 계산하는 함수 +int countSquares() { + vector> dp(ROW, vector(COL, 1)); // 모든 값을 1로 초기화 + + // 동적 계획법을 이용한 계산 + for (int i = 1; i < ROW; ++i) { + for (int j = 1; j < COL; ++j) { + // dp[i][j]는 현재 셀에서 만들 수 있는 가장 큰 정사각형의 크기를 나타냄 + dp[i][j] = min({dp[i-1][j], dp[i][j-1], dp[i-1][j-1]}) + 1; + } + } + + int totalSquares = 0; + // 모든 dp 값 합산 + for (int i = 0; i < ROW; ++i) { + for (int j = 0; j < COL; ++j) { + totalSquares += dp[i][j]; + } + } + + return totalSquares; +} + +/* +예제 출력: +20 + +예제 설명: +3x4 행렬에서 만들 수 있는 정사각형의 개수는 다음과 같습니다: +- 1x1 크기의 정사각형: 12개 +- 2x2 크기의 정사각형: 6개 +- 3x3 크기의 정사각형: 2개 +총 합계: 12 + 6 + 2 = 20개 +*/ diff --git a/Example/stairproblem_dp.cpp b/Example/stairproblem_dp.cpp new file mode 100644 index 0000000..d0e6fa3 --- /dev/null +++ b/Example/stairproblem_dp.cpp @@ -0,0 +1,46 @@ +#include +#include + +// 문제의 정의: +// N개의 계단이 존재하고, 한 번에 1개 또는 2개의 계단을 오를 수 있을 때, +// 계단을 오르는 방법의 총 수를 구하는 문제입니다. + +// 최적 부분 구조: +// 현재 계단에 도달하는 방법은 이전 계단에서 한 계단 오르거나, +// 두 계단 아래에서 두 계단 오르는 두 가지 방법으로 나뉩니다. +// 즉, f(n) = f(n-1) + f(n-2)의 형태로 나타낼 수 있습니다. + +// 중복 부분 문제: +// 동일한 부분 문제(예: f(n-1), f(n-2))가 여러 번 반복되어 계산됩니다. +// 동적 계획법을 사용하면 이러한 중복 계산을 피할 수 있습니다. + +int countWays(int n); + +int main() { + int n; + std::cout << "계단의 수를 입력하세요: "; + std::cin >> n; + + int result = countWays(n); + std::cout << "계단을 오르는 방법의 총 수: " << result << std::endl; + + return 0; +} + +// 계단을 오르는 방법의 총 수를 계산하는 함수 +int countWays(int n) { + // 동적 계획법을 위한 배열 선언 + std::vector dp(n + 1); + + // 초기값 설정 + dp[1] = 1; // 계단 1개를 오르는 방법은 1가지 + dp[2] = 2; // 계단 2개를 오르는 방법은 2가지 (1+1 또는 2) + + // 동적 계획법을 이용한 계산 + // 점화식: dp[i] = dp[i - 1] + dp[i - 2] + for (int i = 3; i <= n; ++i) { + dp[i] = dp[i - 1] + dp[i - 2]; + } + + return dp[n]; +} diff --git a/ReadMe.md b/ReadMe.md index b51d63b..26c4e79 100644 --- a/ReadMe.md +++ b/ReadMe.md @@ -1,16 +1,16 @@ -# 코딩 테스트 합격자 되기(CPP 편) - CPP편 4월중 출간 예정(카톡방은 운영중 입니다.) +# 코딩 테스트 합격자 되기(CPP 편) - 프로그래머스에서 엄선한 기출문제에 대한 상세한 풀이 제공 - 들고 다닐수 있는 요약노트 제공 - 실전문제 위주로 출제왼 기출문제 5회본 제공 -![image](https://github.com/dremdeveloper/codingtest_cpp/assets/131899974/ecad59fa-d937-41e4-8f35-98a5f0df21bd) +![코딩 테스트 합격자 되기(C++ 편)_입체표지](https://github.com/dremdeveloper/codingtest_cpp/assets/131899974/380f732f-174d-4fc3-a552-bb74f8478b24) # 진행중인 이벤트 | Event | 세부내용 |기간 | | ---------- | ---------------------------------------------- |---------------------------------------------- | -|코딩테스트 스터디 |[참여방법 및 스터디 세부사항](https://cafe.naver.com/dremdeveloper/901) | 상시 | -|코테 정리노트 증정(100프로 당첨) |[참여하러가기](https://rabbit.prosell.kr/m/bbs/Jonbeo/1 ) | 9월27~ | +|코딩테스트 강의신청 |[세부사항](https://bit.ly/4aPfz5P) | 구매자 한 무료로 진행 | +|코테 정리노트 증정(100프로 당첨) |[참여하러가기](https://bit.ly/3VWUjGO) | 구매자 한 무료로 진행 | # 코딩테스트 소통공간(저자가 직접운영) @@ -35,4 +35,7 @@ | ---------- | ---------------------------------------------- | | STL | 코딩테스트에서 사용되는 STL 정리 | | solution | 책에 있는 문제에 대한 해설 | +| Algorithm DataStructure | 코테에 자주나오는 알고리즘과자료구조 설명 | +| reference | C++기본 문법설명 | +| performance | C++에서 유의해야할 성능비교 | diff --git a/STL/ReadMe.md b/STL/ReadMe.md new file mode 100644 index 0000000..b9b1423 --- /dev/null +++ b/STL/ReadMe.md @@ -0,0 +1,22 @@ +| 파일 이름 | 설명 | +|-----------------------|------------------| +| `sort.cpp` | 정렬 알고리즘: std::sort를 사용하여 배열을 오름차순과 내림차순으로 정렬하고, partial_sort를 사용한 부분 정렬을 설명합니다. | +| `stack.cpp` | 스택 구현: std::stack을 사용하여 LIFO 원칙에 따라 동작하는 스택의 기본 동작을 설명합니다. | +| `template.cpp` | 템플릿 사용 예제: C++ 템플릿을 사용하여 함수와 클래스의 템플릿 예제를 보여줍니다. | +| `unordered_map.cpp` | 언오더드 맵 사용법: std::unordered_map의 기본 사용법을 설명하며, 해시 테이블 기반으로 작동하는 방식을 설명합니다. | +| `unordered_set.cpp` | 언오더드 셋 사용법: std::unordered_set의 사용을 통해 중복 없이 요소를 저장하는 방법을 설명합니다. | +| `vector.cpp` | 벡터 사용법: std::vector의 기능과 사용법을 설명하며, 동적 배열로서의 장점을 강조합니다. | +| `priority_queue.cpp` | 우선순위 큐 사용법: std::priority_queue를 사용하여 요소를 우선순위에 따라 관리하는 방법을 설명합니다. | +| `queue.cpp` | 큐 구현: std::queue를 사용하여 FIFO 원칙에 따라 동작하는 큐의 기본 동작을 설명합니다. | +| `reverse.cpp` | 배열 뒤집기: std::reverse를 사용하여 배열의 요소 순서를 뒤집는 방법을 설명합니다. | +| `set.cpp` | 셋 사용법: std::set을 사용하여 요소를 정렬 상태로 유지하며 중복 없이 저장하는 방법을 설명합니다. | +| `map.cpp` | 맵 사용법: std::map을 사용하여 키-값 쌍을 저장하고 관리하는 예제를 보여줍니다. 키는 유일하며 정렬된 순서로 저장됩니다. | +| `max_element.cpp` | 최대 요소 찾기: 컨테이너에서 최대값을 찾는 std::max_element 사용법을 설명합니다. | +| `min_element.cpp` | 최소 요소 찾기: 컨테이너에서 최소값을 찾는 std::min_element 사용법을 설명합니다. | +| `next_permutation.cpp` | 다음 순열 찾기: 컨테이너의 가능한 순열을 생성하는 std::next_permutation 함수를 사용합니다. | +| `binary_search.cpp` | 이진 검색 알고리즘: 정렬된 데이터에 대한 이진 검색을 수행하는 방법을 설명합니다. | +| `call_by_ref.cpp` | 참조에 의한 호출: 함수에서 참조를 통해 변수를 전달하고 변경하는 방법을 보여줍니다. | +| `count.cpp` | 요소 개수 세기: std::count를 사용하여 특정 값의 개수를 세는 방법을 설명합니다. | +| `deque.cpp` | 덱 사용법: 양쪽 끝에서 요소를 추가하거나 제거할 수 있는 std::deque의 사용법을 설명합니다. | +| `iterator.cpp` | 이터레이터 사용법: STL 컨테이너를 순회하는 데 사용되는 이터레이터의 다양한 사용법을 보여줍니다. | + diff --git a/STL/binary_search.cpp b/STL/binary_search.cpp index 37f89ed..fab8cdd 100644 --- a/STL/binary_search.cpp +++ b/STL/binary_search.cpp @@ -5,36 +5,51 @@ //############################################################ #include #include -#include - +#include // for std::binary_search and std::sort using namespace std; -// 이진 탐색은 정렬된 배열이나 컨테이너에서 특정 값을 빠르게 찾는 알고리즘입니다. -// - 이진 탐색은 로그 시간 복잡도를 가지며, 정렬된 데이터에서 특정 값을 빠르게 찾을 때 사용됩니다. -// 좋은 사용 시기: -// - 큰 데이터셋에서 특정 값이 포함되어 있는지 빠르게 확인하거나, 특정 값의 위치를 찾을 때. - int main() { + // 예시 벡터 생성 + vector v = {1, 3, 4, 5, 7, 9, 10}; - vector v = {1, 3, 4, 5, 9, 10}; - - // binary_search: 범위 내에서 값의 존재 확인, O(log n) - bool found = binary_search(v.begin(), v.end(), 5); - cout << (found ? "5 is in the vector." : "5 is not in the vector.") << endl; // 출력: 5 is in the vector. + // 찾고자 하는 값 + int value1 = 5; + int value2 = 6; - // lower_bound: 값이 들어갈 수 있는 첫 번째 위치의 이터레이터 반환, O(log n) - auto lb = lower_bound(v.begin(), v.end(), 5); - cout << "5 would get inserted at index: " << (lb - v.begin()) << endl; // 출력: 5 would get inserted at index: 3 + // 이진 탐색 사용 예시 + bool found1 = binary_search(v.begin(), v.end(), value1); + bool found2 = binary_search(v.begin(), v.end(), value2); - // upper_bound: 값보다 큰 첫 번째 위치의 이터레이터 반환, O(log n) - auto ub = upper_bound(v.begin(), v.end(), 5); - cout << "First element greater than 5 is at index: " << (ub - v.begin()) << endl; // 출력: First element greater than 5 is at index: 4 - - // 성능 저하 예제: - // 데이터가 정렬되어 있지 않으면 이진 탐색은 올바른 결과를 반환하지 않을 수 있습니다. - vector unsorted = {10, 1, 9, 5, 3, 4}; - found = binary_search(unsorted.begin(), unsorted.end(), 5); // 이 경우에는 정확한 결과를 기대하기 어렵습니다. + // 결과 출력 + cout << "값 " << value1 << "를 찾는 중: " << (found1 ? "찾음" : "찾지 못함") << endl; + cout << "값 " << value2 << "를 찾는 중: " << (found2 ? "찾음" : "찾지 못함") << endl; return 0; } +/* +출력값: + +값 5를 찾는 중: 찾음 +값 6를 찾는 중: 찾지 못함 +*/ + +// binary_search에 대한 설명 +/* +std::binary_search 함수는 정렬된 범위에서 특정 값을 찾는 데 사용됩니다. +이진 탐색 알고리즘을 사용하여 값의 존재 여부를 확인합니다. + +주의해야 할 점: +1. 정렬된 상태에서 사용: binary_search 함수는 정렬된 범위에서만 올바르게 동작합니다. + 정렬되지 않은 범위에서 사용하면 결과가 올바르지 않습니다. +2. 반환값: 찾고자 하는 값이 존재하면 true를 반환하고, 존재하지 않으면 false를 반환합니다. + +시간복잡도: +- std::binary_search 함수의 시간복잡도는 O(log N)입니다. 여기서 N은 범위 내의 요소 수입니다. + +예제: +벡터가 {1, 3, 4, 5, 7, 9, 10}로 주어졌을 때, +binary_search(v.begin(), v.end(), 5)를 호출하면 true를 반환하고, +binary_search(v.begin(), v.end(), 6)를 호출하면 false를 반환합니다. +*/ + diff --git a/STL/count.cpp b/STL/count.cpp index 5eb8b9b..b69be61 100644 --- a/STL/count.cpp +++ b/STL/count.cpp @@ -5,33 +5,32 @@ //############################################################ #include #include -#include - +#include // for std::count using namespace std; -// std::count는 C++ STL의 algorithm 헤더에 포함된 요소 계산 함수입니다. -// - 주어진 범위 내에서 특정 값을 가진 요소의 개수를 반환합니다. -// 좋은 사용 시기: -// - 배열이나 컨테이너에서 특정 값의 개수를 알고 싶을 때. -// 성능 이슈: -// - count는 선형 시간 복잡도를 갖는 알고리즘입니다. 대규모 데이터에서는 성능 저하가 발생할 수 있습니다. +int main() { + // 예시 벡터 생성 + vector v = {1, 2, 3, 4, 5, 1, 2, 1}; -// 조건 함수: 짝수를 확인하는 함수 -bool isEven(int i) { - return i % 2 == 0; -} + // count 함수 사용 예시 + // std::count 함수는 주어진 범위에서 특정 값이 몇 번 나타나는지 센다. + // 여기서 v.begin()과 v.end()는 벡터의 시작과 끝을 나타내며, + // 1은 찾고자 하는 값이다. + int count_of_1 = count(v.begin(), v.end(), 1); -int main() { + // 결과 출력 + cout << "벡터에서 1의 개수: " << count_of_1 << endl; - vector v = {3, 1, 4, 1, 5, 9, 2, 6, 3, 3}; + // 출력값: + // 벡터에서 1의 개수: 3 - // 기본 동작: 범위 내에서 값 3의 개수 계산, O(n) - int cnt = count(v.begin(), v.end(), 3); - cout << "Number of 3s: " << cnt << endl; // 출력: Number of 3s: 3 + // 시간 복잡도: + // std::count 함수의 시간 복잡도는 O(N)이다. + // 여기서 N은 주어진 범위의 요소 수를 나타낸다. - // count_if: 조건에 맞는 요소의 개수 계산, O(n) - int evenCount = count_if(v.begin(), v.end(), isEven); - cout << "Number of even numbers: " << evenCount << endl; // 출력: Number of even numbers: 3 + // count 함수를 사용해야 하는 경우 + // 데이터 집합에서 특정 값의 출현 빈도를 알고 싶을 때 유용하다. + // 예를 들어, 설문조사 결과에서 특정 답변이 몇 번 나왔는지 확인할 때 사용할 수 있다. return 0; } diff --git a/STL/iterator.cpp b/STL/iterator.cpp index 679b3e7..5716cf6 100644 --- a/STL/iterator.cpp +++ b/STL/iterator.cpp @@ -1,45 +1,98 @@ -//############################################################ -// | cafe | http://cafe.naver.com/dremdelover | -// | Q&A | https://open.kakao.com/o/gX0WnTCf | -// | business | ultrasuperrok@gmail.com | -//############################################################ -#include -#include -#include - -using namespace std; - -// 이터레이터(iterator)는 STL 컨테이너의 원소들을 순회하기 위한 포인터와 유사한 객체입니다. -// - 다양한 STL 컨테이너들은 자신만의 이터레이터 타입을 제공합니다. -// 좋은 사용 시기: -// - 컨테이너의 원소들을 순회하거나, 특정 위치의 원소에 접근하고 싶을 때. -// 성능 이슈: -// - 일반적으로 이터레이터의 연산들은 상수 시간 복잡도를 가집니다. 하지만 이터레이터의 종류(예: forward iterator, bidirectional iterator, random access iterator)에 따라 가능한 연산이 다를 수 있습니다. - -int main() { - vector v = {1, 2, 3, 4, 5}; - - // begin, end: 컨테이너의 시작과 끝을 가리키는 이터레이터 반환, O(1) - vector::iterator it = v.begin(); - - // * 연산자: 이터레이터가 가리키는 원소에 접근, O(1) - cout << "First element: " << *it << endl; // 출력: First element: 1 - - // ++ 연산자: 이터레이터를 다음 원소로 이동, O(1) - ++it; - cout << "Second element: " << *it << endl; // 출력: Second element: 2 - - // += 연산자: 이터레이터를 지정된 수만큼 이동 (Random Access Iterator에만 가능), O(1) - it += 2; - cout << "Fourth element: " << *it << endl; // 출력: Fourth element: 4 - - // 이터레이터를 사용한 순회 예제 - for (vector::iterator iter = v.begin(); iter != v.end(); ++iter) { - cout << *iter << " "; // 출력: 1 2 3 4 5 - } - cout << endl; - - return 0; - -} - +//############################################################ +// | cafe | http://cafe.naver.com/dremdelover | +// | Q&A | https://open.kakao.com/o/gX0WnTCf | +// | business | ultrasuperrok@gmail.com | +//############################################################ +#include +#include +#include + +using namespace std; + +// 반복자 예시 +// 반복자는 컨테이너(예: 벡터, 리스트 등)의 요소를 순회하는 데 사용됩니다. +// 반복자는 포인터와 비슷하게 동작하며, STL에서 중요한 역할을 합니다. +// 반복자를 사용하면 컨테이너의 내부 구현에 독립적으로 요소를 처리할 수 있습니다. + +int main() { + // 벡터 선언 및 초기화 + vector vec = {1, 2, 3, 4, 5}; + + // 리스트 선언 및 초기화 + list lst = {10, 20, 30, 40, 50}; + + // 순방향 반복자를 사용하여 벡터의 요소를 순회하고 출력 + cout << "Vector elements: "; + for (vector::iterator it = vec.begin(); it != vec.end(); ++it) { + cout << *it << " "; // 반복자가 가리키는 요소를 출력 + } + cout << endl; // 출력: Vector elements: 1 2 3 4 5 + + // 순방향 반복자를 사용하여 리스트의 요소를 순회하고 출력 + cout << "List elements: "; + for (list::iterator it = lst.begin(); it != lst.end(); ++it) { + cout << *it << " "; // 반복자가 가리키는 요소를 출력 + } + cout << endl; // 출력: List elements: 10 20 30 40 50 + + // 역방향 반복자를 사용하여 벡터의 요소를 순회하고 출력 + cout << "Vector elements in reverse: "; + for (vector::reverse_iterator rit = vec.rbegin(); rit != vec.rend(); ++rit) { + cout << *rit << " "; // 역방향 반복자가 가리키는 요소를 출력 + } + cout << endl; // 출력: Vector elements in reverse: 5 4 3 2 1 + + // 역방향 반복자를 사용하여 리스트의 요소를 순회하고 출력 + cout << "List elements in reverse: "; + for (list::reverse_iterator rit = lst.rbegin(); rit != lst.rend(); ++rit) { + cout << *rit << " "; // 역방향 반복자가 가리키는 요소를 출력 + } + cout << endl; // 출력: List elements in reverse: 50 40 30 20 10 + + return 0; +} + +/* +반복자의 목적: +- 반복자는 컨테이너의 요소를 순차적으로 접근하고 조작하는 데 사용됩니다. +- 반복자를 통해 컨테이너의 내부 구현에 독립적으로 요소를 처리할 수 있습니다. + +반복자의 장점: +1. 유연성: 다양한 컨테이너에 대해 동일한 방식으로 요소를 순회할 수 있습니다. +2. 추상화: 컨테이너의 내부 구조를 몰라도 요소를 접근할 수 있습니다. +3. 범용성: 알고리즘 함수와 함께 사용하여 코드의 재사용성을 높입니다. + +순방향 반복자와 역방향 반복자: +1. 순방향 반복자: 컨테이너의 처음(begin)부터 끝(end)까지 순차적으로 요소를 접근합니다. + - 선언: vector::iterator it; + - 초기화: it = vec.begin(); + - 사용: *it를 통해 반복자가 가리키는 요소에 접근합니다. + +2. 역방향 반복자: 컨테이너의 끝(rbegin)부터 처음(rend)까지 역순으로 요소를 접근합니다. + - 선언: vector::reverse_iterator rit; + - 초기화: rit = vec.rbegin(); + - 사용: *rit를 통해 역방향 반복자가 가리키는 요소에 접근합니다. + +반복자의 사용방법: +1. 반복자 선언: 컨테이너 타입에 따라 반복자를 선언합니다. 예: vector::iterator. +2. 반복자 초기화: begin()과 end(), 또는 rbegin()과 rend() 함수를 사용하여 반복자를 초기화합니다. +3. 반복자 사용: 반복자를 사용하여 요소에 접근하고 조작합니다. + +예시: +- 벡터 요소 순회: for (it = vec.begin(); it != vec.end(); ++it). +- 리스트 요소 순회: for (it = lst.begin(); it != lst.end(); ++it). +- 역방향 요소 순회: for (rit = vec.rbegin(); rit != vec.rend(); ++rit). +- 요소 접근: *it 또는 *rit를 사용하여 반복자가 가리키는 요소에 접근합니다. + +코드 설명: +1. 벡터 선언 및 초기화: vector vec = {1, 2, 3, 4, 5}; 벡터를 선언하고 초기화합니다. +2. 리스트 선언 및 초기화: list lst = {10, 20, 30, 40, 50}; 리스트를 선언하고 초기화합니다. +3. 순방향 반복자 사용: + - for (vector::iterator it = vec.begin(); it != vec.end(); ++it): 순방향 반복자를 사용하여 벡터의 요소를 순회합니다. + - for (list::iterator it = lst.begin(); it != lst.end(); ++it): 순방향 반복자를 사용하여 리스트의 요소를 순회합니다. + - *it: 순방향 반복자가 가리키는 요소를 출력합니다. +4. 역방향 반복자 사용: + - for (vector::reverse_iterator rit = vec.rbegin(); rit != vec.rend(); ++rit): 역방향 반복자를 사용하여 벡터의 요소를 역순으로 순회합니다. + - for (list::reverse_iterator rit = lst.rbegin(); rit != lst.rend(); ++rit): 역방향 반복자를 사용하여 리스트의 요소를 역순으로 순회합니다. + - *rit: 역방향 반복자가 가리키는 요소를 출력합니다. +*/ diff --git a/STL/map.cpp b/STL/map.cpp index b3f57d0..db725d0 100644 --- a/STL/map.cpp +++ b/STL/map.cpp @@ -5,49 +5,129 @@ //############################################################ #include #include +#include using namespace std; -// map은 키-값 쌍을 저장하는 균형 이진 트리를 기반으로 한 STL 컨테이너입니다. -// - 키는 정렬된 순서로 저장됩니다. -// - 각 키는 유일합니다. -// 좋은 사용 시기: -// - 키를 기준으로 데이터를 빠르게 검색하고자 할 때. -// - 키-값 쌍의 삽입과 삭제가 정렬된 순서를 유지하면서 자주 발생할 때. -// 성능 이슈: -// - 연속된 메모리에 데이터를 저장하지 않기 때문에 cache locality가 떨어집니다. - int main() { + // map 컨테이너 설명 + // - map은 키와 값의 쌍으로 이루어진 순서가 있는 집합입니다. + // - 키는 중복을 허용하지 않으며, 삽입되는 원소는 자동으로 정렬됩니다. + // - 내부적으로 균형 이진 트리(일반적으로 Red-Black Tree)로 구현되어 있습니다. + // - 삽입, 삭제, 탐색 등의 주요 연산은 O(log N)의 시간복잡도를 가집니다. + + // map을 사용해야 하는 경우: + // - 키와 값의 쌍을 효율적으로 저장하고 관리해야 할 때. + // - 키를 기준으로 정렬된 순서로 데이터를 저장하고 싶을 때. + // - 삽입, 삭제, 탐색 연산의 시간복잡도가 O(log N)이면 충분히 빠른 경우. + + // map을 사용하지 말아야 하는 경우: + // - 키의 중복을 허용해야 할 때 (이 경우 multimap을 사용). + // - 키의 순서가 중요하지 않은 경우 (이 경우 unordered_map이 더 효율적일 수 있음). + // - 데이터의 크기가 매우 크고, 삽입 및 탐색 연산이 더 빠른 시간복잡도를 요구하는 경우. + + // map 컨테이너 선언 + // key: int (학생 ID), value: string (학생 이름) + map studentMap; + + // 삽입: 학생 ID와 이름을 맵에 추가 + // insert 함수 + // 인자: 삽입할 키와 값 쌍 (key, value) + // 동작: 키가 존재하지 않으면 삽입, 존재하면 값을 업데이트 + // 시간복잡도: O(log N) + studentMap.insert({101, "Alice"}); + studentMap.insert({102, "Bob"}); + studentMap.insert({103, "Charlie"}); + + // 맵의 모든 요소 출력 + cout << "Initial map content:\n"; + for (const auto& pair : studentMap) { + cout << "ID: " << pair.first << ", Name: " << pair.second << endl; + } + + // 출력값: + // Initial map content: + // ID: 101, Name: Alice + // ID: 102, Name: Bob + // ID: 103, Name: Charlie + + // 탐색: 특정 ID로 학생 이름 찾기 + // find 함수 + // 인자: 찾을 키 (key) + // 동작: 키가 존재하면 iterator 반환, 없으면 end() 반환 + // 시간복잡도: O(log N) + auto it = studentMap.find(102); + if (it != studentMap.end()) { + cout << "\nStudent with ID 102 found: " << it->second << endl; + } else { + cout << "\nStudent with ID 102 not found.\n"; + } - // map 선언 - map m; + // 출력값: + // Student with ID 102 found: Bob - // insert: O(log n) - m.insert(make_pair("apple", 100)); - m.insert({"banana", 200}); + // 업데이트: 이미 존재하는 ID의 이름 변경 + // insert 함수 + // 인자: 삽입할 키와 값 쌍 (key, value) + // 동작: 키가 존재하지 않으면 삽입, 존재하면 값을 업데이트 + // 시간복잡도: O(log N) + studentMap.insert({102, "Bobby"}); + cout << "\nAfter updating ID 102:\n"; + for (const auto& pair : studentMap) { + cout << "ID: " << pair.first << ", Name: " << pair.second << endl; + } - // operator[]: O(log n) - m["cherry"] = 300; + // 출력값: + // After updating ID 102: + // ID: 101, Name: Alice + // ID: 102, Name: Bobby + // ID: 103, Name: Charlie - // find: O(log n) - auto it = m.find("banana"); - if(it != m.end()) { - cout << "banana: " << it->second << endl; // 출력: banana: 200 + // 삭제: 특정 ID의 학생 정보 삭제 + // erase 함수 + // 인자: 삭제할 키 (key) + // 동작: 키가 존재하면 해당 키의 요소를 삭제 + // 시간복잡도: O(log N) + studentMap.erase(101); + cout << "\nAfter erasing ID 101:\n"; + for (const auto& pair : studentMap) { + cout << "ID: " << pair.first << ", Name: " << pair.second << endl; } - // erase: O(log n) - m.erase("apple"); + // 출력값: + // After erasing ID 101: + // ID: 102, Name: Bobby + // ID: 103, Name: Charlie + + // [] 연산자와 find의 차이점 + // [] 연산자 + // 인자: 접근할 키 (key) + // 동작: 키가 존재하면 해당 키의 값을 반환, 없으면 키를 생성하고 기본값을 설정 + // 시간복잡도: O(log N) + cout << "\nUsing [] operator:\n"; + cout << "Student with ID 103: " << studentMap[103] << endl; // 존재하는 키 + cout << "Student with ID 104: " << studentMap[104] << endl; // 존재하지 않는 키, 기본값 설정 - // size: O(1) - cout << m.size() << endl; // 출력: 2 + // 출력값: + // Using [] operator: + // Student with ID 103: Charlie + // Student with ID 104: - // begin, end를 사용한 순회: O(n) - for(auto it = m.begin(); it != m.end(); ++it) { - cout << it->first << ": " << it->second << endl; // 출력: banana: 200, cherry: 300 + // find 함수 + // 인자: 찾을 키 (key) + // 동작: 키가 존재하면 iterator 반환, 없으면 end() 반환 + // 시간복잡도: O(log N) + cout << "\nUsing find function:\n"; + it = studentMap.find(103); + if (it != studentMap.end()) { + cout << "Student with ID 103 found: " << it->second << endl; + } else { + cout << "Student with ID 103 not found.\n"; } - // clear: O(n) - m.clear(); + // 출력값: + // Using find function: + // Student with ID 103 found: Charlie return 0; } diff --git a/STL/max_element.cpp b/STL/max_element.cpp index abb8f25..9cde388 100644 --- a/STL/max_element.cpp +++ b/STL/max_element.cpp @@ -5,37 +5,44 @@ //############################################################ #include #include -#include - +#include // for std::max_element using namespace std; -// std::max_element는 C++ STL의 algorithm 헤더에 포함된 최대 요소 검색 함수입니다. -// - 주어진 범위 내에서 최대 값을 가진 요소의 반복자를 반환합니다. -// 좋은 사용 시기: -// - 배열이나 컨테이너에서 최대 값을 가진 요소를 찾고 싶을 때. -// 성능 이슈: -// - max_element는 선형 시간 복잡도를 갖는 알고리즘입니다. 대규모 데이터에서는 성능 저하가 발생할 수 있습니다. - int main() { + // 예시 벡터 생성 + vector v = {1, 3, 4, 5, 7, 9, 10}; + + // 최대값 찾기 + auto max_it = max_element(v.begin(), v.end()); - vector v = {1, 3, 7, 2, 5, 9, 4}; - - // 기본 동작: 범위 내 최대 값을 가진 요소 찾기, O(n) - auto it = max_element(v.begin(), v.end()); - if(it != v.end()) { - cout << "Max element: " << *it << endl; // 출력: Max element: 9 - } - - // 사용자 정의 비교 함수를 사용하여 최소 요소 찾기 - auto minIt = max_element(v.begin(), v.end(), [](int a, int b) { - return a > b; // 반대로 비교 - }); - if(minIt != v.end()) { - cout << "Min element using custom comparator: " << *minIt << endl; // 출력: Min element using custom comparator: 1 - } - - // 성능 저하 예제: - // max_element는 모든 요소를 확인하기 때문에 대규모 데이터에서는 시간이 오래 걸릴 수 있습니다. - // 필요한 경우, 데이터의 구조를 최적화하거나 다른 알고리즘/자료구조를 사용하여 성능을 향상시킬 수 있습니다. + // 결과 출력 + cout << "최대값: " << *max_it << " (위치: " << distance(v.begin(), max_it) << ")" << endl; + + return 0; } +/* +출력값: + +최대값: 10 (위치: 6) +*/ + +// max_element에 대한 설명 +/* +std::max_element 함수는 주어진 범위에서 가장 큰 요소를 찾는 데 사용됩니다. +이 함수는 선형 탐색 알고리즘을 사용하여 값을 찾습니다. + +설명: +1. 반환값: 가장 큰 값을 가리키는 반복자를 반환합니다. + 반환된 반복자를 사용하여 해당 값에 접근할 수 있습니다. +2. 범위: 시작 반복자와 끝 반복자를 인자로 받아, 이 범위 내에서 탐색을 수행합니다. + +시간복잡도: +- std::max_element 함수의 시간복잡도는 O(N)입니다. 여기서 N은 범위 내의 요소 수입니다. + +예제: +벡터가 {1, 3, 4, 5, 7, 9, 10}로 주어졌을 때, +max_element(v.begin(), v.end())를 호출하면 반복자가 10을 가리킵니다. +*/ + + diff --git a/STL/min_element.cpp b/STL/min_element.cpp index 874a9e7..e2ae9df 100644 --- a/STL/min_element.cpp +++ b/STL/min_element.cpp @@ -5,37 +5,43 @@ //############################################################ #include #include -#include - +#include // for std::min_element using namespace std; -// std::min_element는 C++ STL의 algorithm 헤더에 포함된 최소 요소 검색 함수입니다. -// - 주어진 범위 내에서 최소 값을 가진 요소의 반복자를 반환합니다. -// 좋은 사용 시기: -// - 배열이나 컨테이너에서 최소 값을 가진 요소를 찾고 싶을 때. -// 성능 이슈: -// - min_element는 선형 시간 복잡도를 갖는 알고리즘입니다. 대규모 데이터에서는 성능 저하가 발생할 수 있습니다. - int main() { + // 예시 벡터 생성 + vector v = {1, 3, 4, 5, 7, 9, 10}; + + // 최소값 찾기 + auto min_it = min_element(v.begin(), v.end()); - vector v = {1, 3, 7, 2, 5, 9, 4}; - - // 기본 동작: 범위 내 최소 값을 가진 요소 찾기, O(n) - auto it = min_element(v.begin(), v.end()); - if(it != v.end()) { - cout << "Min element: " << *it << endl; // 출력: Min element: 1 - } - - // 사용자 정의 비교 함수를 사용하여 최대 요소 찾기 - auto maxIt = min_element(v.begin(), v.end(), [](int a, int b) { - return a > b; // 반대로 비교 - }); - if(maxIt != v.end()) { - cout << "Max element using custom comparator: " << *maxIt << endl; // 출력: Max element using custom comparator: 9 - } - - // 성능 저하 예제: - // min_element는 모든 요소를 확인하기 때문에 대규모 데이터에서는 시간이 오래 걸릴 수 있습니다. - // 필요한 경우, 데이터의 구조를 최적화하거나 다른 알고리즘/자료구조를 사용하여 성능을 향상시킬 수 있습니다. + // 결과 출력 + cout << "최소값: " << *min_it << " (위치: " << distance(v.begin(), min_it) << ")" << endl; + + return 0; } +/* +출력값: + +최소값: 1 (위치: 0) +*/ + +// min_element에 대한 설명 +/* +std::min_element 함수는 주어진 범위에서 가장 작은 요소를 찾는 데 사용됩니다. +이 함수는 선형 탐색 알고리즘을 사용하여 값을 찾습니다. + +설명: +1. 반환값: 가장 작은 값을 가리키는 반복자를 반환합니다. + 반환된 반복자를 사용하여 해당 값에 접근할 수 있습니다. +2. 범위: 시작 반복자와 끝 반복자를 인자로 받아, 이 범위 내에서 탐색을 수행합니다. + +시간복잡도: +- std::min_element 함수의 시간복잡도는 O(N)입니다. 여기서 N은 범위 내의 요소 수입니다. + +예제: +벡터가 {1, 3, 4, 5, 7, 9, 10}로 주어졌을 때, +min_element(v.begin(), v.end())를 호출하면 반복자가 1을 가리킵니다. +*/ + diff --git a/STL/next_permutation.cpp b/STL/next_permutation.cpp index ceb4bff..8975651 100644 --- a/STL/next_permutation.cpp +++ b/STL/next_permutation.cpp @@ -5,37 +5,77 @@ //############################################################ #include #include -#include - +#include // for std::next_permutation and std::sort using namespace std; -// 순열은 가능한 모든 원소의 조합을 의미합니다. -// - STL의 next_permutation은 주어진 범위의 원소를 재배치하여 사전 순으로 다음 순열을 생성합니다. -// 좋은 사용 시기: -// - 가능한 모든 원소의 조합을 순차적으로 생성하고 싶을 때. 예를 들면, 완전 탐색 알고리즘에서 사용될 수 있습니다. -// 성능 이슈: -// - next_permutation은 원소의 수에 따라 많은 수의 순열을 생성할 수 있습니다. 원소의 수가 많아지면 가능한 모든 순열을 생성하는 데 많은 시간이 걸릴 수 있습니다. - -int main() { - - vector v = {1, 2, 3}; - - // next_permutation: 주어진 범위의 원소를 재배치하여 다음 순열을 생성, O(n) +// 벡터의 모든 순열을 출력하는 함수 +void print_permutations(vector v) { do { - for(int i : v) { - cout << i << " "; + // 현재 순열 출력 + for (int num : v) { + cout << num << " "; } cout << endl; } while (next_permutation(v.begin(), v.end())); +} + +int main() { + // 예시 벡터 생성 + vector v = {3, 2, 1}; + + // 정렬되지 않은 경우 + cout << "정렬되지 않은 경우:\n"; + vector v_unsorted = v; + print_permutations(v_unsorted); - // 출력: - // 1 2 3 - // 1 3 2 - // 2 1 3 - // 2 3 1 - // 3 1 2 - // 3 2 1 + // 벡터를 정렬된 상태로 만들기 + sort(v.begin(), v.end()); + + // 정렬된 경우 + cout << "\n정렬된 경우:\n"; + vector v_sorted = v; + print_permutations(v_sorted); return 0; } +/* +출력값: + +정렬되지 않은 경우: +3 2 1 + +정렬된 경우: +1 2 3 +1 3 2 +2 1 3 +2 3 1 +3 1 2 +3 2 1 +*/ + +// next_permutation에 대한 설명 +/* +std::next_permutation 함수는 주어진 범위에서 사전식 순서로 다음 순열을 생성합니다. +이 함수는 내부적으로 요소의 순서를 비교하여 다음 순열을 만듭니다. + +주의해야 할 점: +1. 정렬된 상태에서 사용: next_permutation 함수는 범위가 정렬된 상태에서 시작할 때 모든 순열을 + 올바르게 생성할 수 있습니다. 정렬되지 않은 상태에서 시작하면 모든 순열을 생성하지 못합니다. +2. 반환값: 다음 순열이 존재하면 true를 반환하고, 더 이상 다음 순열이 없으면 false를 반환하여 + 주어진 범위를 처음 순열(가장 작은 순열)로 재설정합니다. +3. 시간 복잡도: 함수 자체의 시간 복잡도는 O(N)입니다. 모든 순열을 생성하기 위해 반복적으로 + 호출하면 총 시간 복잡도는 O(N * N!)이 됩니다. 여기서 N은 요소의 수입니다. +*/ + +// next_permutation 사용 예: +/* +벡터가 {1, 2, 3}로 주어졌을 때, +next_permutation(v.begin(), v.end())를 호출하면, +벡터는 {1, 3, 2}로 변환됩니다. + +벡터가 {3, 2, 1}로 주어졌을 때, +next_permutation(v.begin(), v.end())를 호출하면, +벡터는 다시 {1, 2, 3}로 재설정됩니다. +*/ + diff --git a/STL/priority_queue.cpp b/STL/priority_queue.cpp index a79cc33..cc1d498 100644 --- a/STL/priority_queue.cpp +++ b/STL/priority_queue.cpp @@ -34,7 +34,7 @@ int main() { // size: O(1) cout << pq.size() << endl; // 출력: 2 - // 순회 (힙의 순서대로, 정렬된 순서가 아님): O(n) + // 힙의 우선순위에 따라 순회 : O(n) while(!pq.empty()) { cout << pq.top() << " "; // 출력: 3 1 pq.pop(); diff --git a/STL/priority_queue_with_priority.cpp b/STL/priority_queue_with_priority.cpp new file mode 100644 index 0000000..8646942 --- /dev/null +++ b/STL/priority_queue_with_priority.cpp @@ -0,0 +1,62 @@ +//############################################################ +// | cafe | http://cafe.naver.com/dremdelover | +// | Q&A | https://open.kakao.com/o/gX0WnTCf | +// | business | ultrasuperrok@gmail.com | +//############################################################ + +#include +#include +#include + +// 표준 라이브러리 사용을 위한 네임스페이스 선언 +using namespace std; + +// 좌표를 나타내는 구조체 정의 +struct Point { + int x, y; + + // 비교 연산자 정의 (내림차순) + // 우선순위 큐에서 가장 큰 값을 우선 처리하기 위해 사용 + bool operator<(const Point& other) const { + // 내림차순 정렬을 위해 x 좌표가 큰 순서대로, x 좌표가 같다면 y 좌표가 큰 순서대로 정렬 + if (x == other.x) { + return y < other.y; + } + return x < other.x; + } +}; + +int main() { + // 우선순위 큐 선언 (내림차순) + // priority_queue: + // - Point: 큐에 저장될 요소의 타입 (좌표 구조체) + // 기본적으로 가장 큰 값이 높은 우선순위를 가집니다. + priority_queue pq; + + // 좌표 삽입 + // push() 함수는 우선순위 큐에 요소를 삽입합니다. + pq.push({10, 20}); // (10, 20) 삽입 + pq.push({30, 40}); // (30, 40) 삽입 + pq.push({20, 30}); // (20, 30) 삽입 + pq.push({5, 10}); // (5, 10) 삽입 + + // 좌표 출력 (우선순위 순) + // while 루프는 큐가 비어있지 않을 때까지 반복됩니다. + // top() 함수는 우선순위 큐의 가장 위에 있는 요소(우선순위가 가장 높은 요소)를 반환합니다. + // pop() 함수는 우선순위 큐의 가장 위에 있는 요소를 제거합니다. + while (!pq.empty()) { + Point p = pq.top(); // 우선순위가 가장 높은 좌표를 반환 + cout << "(" << p.x << ", " << p.y << ") "; // 좌표를 출력 + pq.pop(); // 우선순위가 가장 높은 좌표를 큐에서 제거 + } + return 0; +} + +// 출력 설명: +// priority_queue는 기본적으로 최대 힙(Max-Heap) 구조로 동작하여 가장 큰 요소가 가장 높은 우선순위를 가집니다. +// 좌표 구조체(Point)의 비교 연산자(operator<)를 사용하여 x 좌표가 큰 순서대로, +// x 좌표가 같다면 y 좌표가 큰 순서대로 정렬됩니다. +// 따라서, 위의 코드는 (30, 40), (20, 30), (10, 20), (5, 10) 순으로 좌표를 출력합니다. +// pq.push() 함수를 통해 좌표를 삽입할 때마다 내부적으로 정렬이 이루어지며, +// pq.top() 함수는 가장 큰 x 값을 가진 좌표를 반환하고, pq.pop() 함수는 반환된 좌표를 큐에서 제거합니다. +// 결과적으로, 우선순위 큐에 삽입된 좌표들은 내림차순으로 정렬되어 출력됩니다. diff --git a/STL/set.cpp b/STL/set.cpp index c39e50f..8f919f7 100644 --- a/STL/set.cpp +++ b/STL/set.cpp @@ -5,68 +5,98 @@ //############################################################ #include #include - -using namespace std; - -// set은 균형 이진 트리(Red-Black Tree)를 기반으로 한 STL 컨테이너입니다. -// - 모든 요소는 유일하며 정렬된 순서로 저장됩니다. -// 좋은 사용 시기: -// - 중복 없이 요소를 저장하고 싶을 때. -// - 요소가 항상 정렬된 상태로 유지되어야 할 때. -// 성능 이슈: -// - 연속된 메모리에 저장되지 않아 cache locality가 떨어집니다. -// - 단순히 중복 제거만 원하면 unordered_set이 더 빠를 수 있습니다. +#include +#include int main() { + // 초기화 + std::set s; - // set 선언 - set s; - - // insert: O(log n) - s.insert(3); - s.insert(1); - s.insert(4); - s.insert(1); // 중복된 값은 추가되지 않습니다. + // insert: 원소 삽입 + // 시간복잡도: O(log N) + s.insert(10); // {10} + s.insert(20); // {10, 20} + s.insert(10); // {10, 20} (중복된 값은 삽입되지 않음) - // find: O(log n) - auto it = s.find(3); - if(it != s.end()) { - cout << "Found: " << *it << endl; // 출력: Found: 3 + // find: 원소 탐색 + // 시간복잡도: O(log N) + auto it = s.find(10); // 10을 가리키는 반복자 반환 + if (it != s.end()) { + std::cout << "Found: " << *it << std::endl; // 출력: Found: 10 + } else { + std::cout << "Not Found" << std::endl; } - // erase: O(log n) - s.erase(1); + // erase: 원소 삭제 + // 시간복잡도: O(log N) + s.erase(10); // {20} - // size: O(1) - cout << s.size() << endl; // 출력: 2 + // find 메서드를 사용하여 삭제된 원소를 찾으려 하면 + it = s.find(10); + if (it != s.end()) { + std::cout << "Found: " << *it << std::endl; + } else { + std::cout << "Not Found" << std::endl; // 출력: Not Found + } - // 순회 (정렬된 순서대로): O(n) - for(int num : s) { - cout << num << " "; // 출력: 3 4 + // set을 사용해야 하는 경우 + // 1. 중복을 허용하지 않는 경우 + // 예: 유일한 사용자 ID를 저장하는 경우 + std::set uniqueIds; + uniqueIds.insert(1); + uniqueIds.insert(2); + uniqueIds.insert(1); // 중복 삽입 무시 + for (int id : uniqueIds) { + std::cout << "User ID: " << id << std::endl; // 출력: User ID: 1, User ID: 2 } - cout << endl; - // lower_bound & upper_bound: O(log n) - // lower_bound는 주어진 값보다 크거나 같은 첫 번째 요소의 iterator를 반환합니다. - it = s.lower_bound(3); - if(it != s.end()) { - cout << "Lower bound of 3: " << *it << endl; // 출력: Lower bound of 3: 3 + // 2. 정렬된 순서가 필요한 경우 + // 예: 정렬된 데이터가 필요한 상황 + std::set sortedData; + sortedData.insert(5); + sortedData.insert(1); + sortedData.insert(3); + for (int val : sortedData) { + std::cout << "Sorted Value: " << val << std::endl; // 출력: Sorted Value: 1, 3, 5 } - // upper_bound는 주어진 값보다 큰 첫 번째 요소의 iterator를 반환합니다. - it = s.upper_bound(3); - if(it != s.end()) { - cout << "Upper bound of 3: " << *it << endl; // 출력: Upper bound of 3: 4 + // 3. 탐색, 삽입, 삭제의 성능이 중요한 경우 + // 예: 데이터의 존재 여부를 빈번히 검사해야 하는 경우 + std::set dataSet; + dataSet.insert(15); + if (dataSet.find(15) != dataSet.end()) { + std::cout << "15 is in the set" << std::endl; } - // clear: O(n) - s.clear(); + // set을 사용하지 말아야 하는 경우 + // 1. 중복된 원소를 허용해야 하는 경우 + // 예: 동일한 값을 여러 번 저장해야 하는 경우 + std::multiset multiSet; + multiSet.insert(10); + multiSet.insert(10); // 중복된 값도 저장됨 + std::cout << "Multiset contains " << multiSet.count(10) << " instances of 10" << std::endl; - // 성능 저하 예제: - // set은 원소의 삽입, 삭제, 검색에 log n의 시간이 걸립니다. - // 연속적인 메모리 접근이 필요하거나, 중복만을 피하려는 경우에는 unordered_set을 사용하는 것이 더 효율적일 수 있습니다. - - return 0; + // 2. 정렬이 필요 없는 경우 + // 예: 순서가 중요하지 않고 단순히 데이터를 저장하고자 하는 경우 + std::vector vec = {5, 3, 8, 1}; + vec.push_back(10); + std::cout << "Vector: "; + for (int v : vec) { + std::cout << v << " "; // 출력: Vector: 5 3 8 1 10 + } + std::cout << std::endl; + + // 3. O(1) 시간 복잡도가 필요한 경우 + // 예: 빈번한 삽입과 삭제가 필요한 경우 + std::unordered_set unorderedSet; + unorderedSet.insert(5); + unorderedSet.insert(10); + unorderedSet.erase(5); + if (unorderedSet.find(10) != unorderedSet.end()) { + std::cout << "10 is in the unordered set" << std::endl; + } + return 0; } + diff --git a/STL/sort.cpp b/STL/sort.cpp index 9041d98..487714f 100644 --- a/STL/sort.cpp +++ b/STL/sort.cpp @@ -5,37 +5,83 @@ //############################################################ #include #include -#include +#include // sort() 함수가 포함된 헤더 -using namespace std; +using namespace std; // std 네임스페이스를 사용 -// std::sort는 C++ STL의 algorithm 헤더에 포함된 정렬 함수입니다. -// - 일반적으로 빠른 정렬 알고리즘을 구현하고 있습니다. -// 좋은 사용 시기: -// - 배열이나 컨테이너의 요소를 빠르게 정렬할 때. +// 사용자 정의 비교 함수 +bool compare(int a, int b) { + // 내림차순 정렬을 위한 비교: a가 b보다 클 때 true를 반환 + // sort 함수가 비교 시 compare(a, b)가 true를 반환하면 a가 b보다 앞에 있어야 한다고 판단하여 a와 b의 위치를 교환하지 않음 + return a > b; +} int main() { + // 정렬할 벡터 생성 + vector v = {5, 2, 9, 1, 5, 6}; - vector v = {3, 1, 4, 1, 5, 9, 2, 6}; - - // 기본 정렬: 오름차순, O(n log n) + // 벡터를 오름차순으로 정렬 + // std::sort는 기본적으로 < 연산자를 사용하여 정렬 + // v.begin()은 첫 번째 요소, v.end()는 마지막 요소의 다음 위치를 가리킴 (v.end()는 정렬 대상에 포함되지 않음) sort(v.begin(), v.end()); - for(int i : v) cout << i << " "; // 출력: 1 1 2 3 4 5 6 9 + + // 오름차순으로 정렬된 벡터 출력 + cout << "오름차순 정렬: "; + for (int n : v) { + cout << n << ' '; + } cout << endl; + // 출력값: 1 2 5 5 6 9 + + // 벡터를 다시 섞어서 초기 상태로 되돌림 + v = {5, 2, 9, 1, 5, 6}; - // 사용자 정의 비교 함수를 사용하여 내림차순 정렬 - sort(v.begin(), v.end(), [](int a, int b) { - return a > b; - }); - for(int i : v) cout << i << " "; // 출력: 9 6 5 4 3 2 1 1 + // 벡터를 사용자 정의 비교 함수로 정렬 (내림차순) + // std::sort는 compare 함수가 true를 반환할 때 첫 번째 요소가 두 번째 요소보다 앞에 있어야 한다고 판단 + // compare 함수는 a > b일 때 true를 반환하므로, 큰 값이 작은 값 앞에 오게 되어 내림차순 정렬이 이루어짐 + sort(v.begin(), v.end(), compare); + + // 내림차순으로 정렬된 벡터 출력 + cout << "내림차순 정렬: "; + for (int n : v) { + cout << n << ' '; + } cout << endl; + // 출력값: 9 6 5 5 2 1 + + // 벡터를 다시 섞어서 초기 상태로 되돌림 + v = {5, 2, 9, 1, 5, 6}; + + // 벡터의 앞 3개 요소만 오름차순으로 정렬 + // v.begin()에서 v.begin() + 3까지 정렬 + // 정렬 범위: [5, 2, 9] (v.begin() + 3는 정렬 대상에 포함되지 않음) + sort(v.begin(), v.begin() + 3); - // partial_sort: 일부만 정렬, O(n log m) - // 여기서 m은 [v.begin(), v.begin() + 3) 범위의 길이 - partial_sort(v.begin(), v.begin() + 3, v.end()); - for(int i : v) cout << i << " "; // 출력: 1 1 2 9 6 5 4 3 + // 부분 정렬된 벡터 출력 + cout << "부분 정렬 (앞 3개 요소 오름차순): "; + for (int n : v) { + cout << n << ' '; + } cout << endl; + // 출력값: 2 5 9 1 5 6 + + // 벡터를 다시 섞어서 초기 상태로 되돌림 + v = {5, 2, 9, 1, 5, 6}; + + // 역방향 반복자를 사용하여 벡터를 내림차순으로 정렬 + // v.rbegin()은 마지막 요소, v.rend()는 첫 번째 요소의 이전 위치를 가리킴 (v.rend()는 정렬 대상에 포함되지 않음) + sort(v.rbegin(), v.rend()); + + // 역방향으로 정렬된 벡터 출력 + cout << "역방향 반복자로 정렬 (내림차순): "; + for (int n : v) { + cout << n << ' '; + } + cout << endl; + // 출력값: 9 6 5 5 2 1 return 0; } + + diff --git a/STL/stack.cpp b/STL/stack.cpp index 34d75a4..25260fd 100644 --- a/STL/stack.cpp +++ b/STL/stack.cpp @@ -8,39 +8,72 @@ using namespace std; -// std::stack은 C++ STL에 포함된 컨테이너 어댑터입니다. -// - LIFO(Last-In-First-Out) 원칙에 따라 동작하는 데이터 구조입니다. -// 좋은 사용 시기: -// - LIFO 원칙에 따라 데이터를 처리해야 할 때, 예를 들면, 재귀 알고리즘의 내부 스택, 실행 취소 기능 등. - int main() { - stack s; - - // push: 스택의 상단에 요소 추가, O(1) - s.push(1); - s.push(2); - s.push(3); - - // top: 스택의 상단 요소에 접근, O(1) - cout << "Top element: " << s.top() << endl; // 출력: Top element: 3 - - // pop: 스택의 상단 요소 제거, O(1) - s.pop(); - cout << "Top element after pop: " << s.top() << endl; // 출력: Top element after pop: 2 - - // empty: 스택이 비어 있는지 확인, O(1) + + // 원소 삽입 + s.push(1); // push: 스택의 맨 위에 원소를 추가합니다. 예를 들어, 1을 추가하면 스택의 상태는 [1]이 됩니다. 시간복잡도: O(1) + s.push(2); // push: 스택의 맨 위에 원소를 추가합니다. 예를 들어, 2를 추가하면 스택의 상태는 [1, 2]가 됩니다. 시간복잡도: O(1) + s.push(3); // push: 스택의 맨 위에 원소를 추가합니다. 예를 들어, 3을 추가하면 스택의 상태는 [1, 2, 3]이 됩니다. 시간복잡도: O(1) + + // 맨 위 원소 확인 + cout << "Top element: " << s.top() << endl; // 출력: 3 + // top: 스택의 맨 위 원소를 반환합니다. 스택이 비어 있있을 때 호출하는 것은 undefined behavior입니다. 예를 들어, 현재 스택의 상태는 [1, 2, 3]이므로 top()은 3을 반환합니다. 시간복잡도: O(1) + + // 원소 삭제 + s.pop(); // pop: 스택의 맨 위 원소를 제거합니다. 스택이 비어 있있을 때 호출하는 것은 undefined behavior입니다. 예를 들어, 현재 스택의 상태는 [1, 2, 3]이므로 pop()은 3을 제거하여 [1, 2]가 됩니다. 시간복잡도: O(1) + cout << "Top element after pop: " << s.top() << endl; // 출력: 2 + // top: 스택의 맨 위 원소를 반환합니다. 스택이 비어 있있을 때 호출하는 것은 undefined behavior입니다. 예를 들어, 현재 스택의 상태는 [1, 2]이므로 top()은 2를 반환합니다. 시간복잡도: O(1) + + // 스택이 비어있는지 확인 if (!s.empty()) { - cout << "Stack is not empty" << endl; // 출력: Stack is not empty + cout << "Stack is not empty" << endl; // 출력: Stack is not empty } + // empty: 스택이 비어 있는지 여부를 확인합니다. 비어 있으면 true, 아니면 false를 반환합니다. 예를 들어, 현재 스택의 상태는 [1, 2]이므로 empty()는 false를 반환합니다. 시간복잡도: O(1) + + // 스택의 크기 확인 + cout << "Stack size: " << s.size() << endl; // 출력: 2 + // size: 스택에 있는 원소의 개수를 반환합니다. 예를 들어, 현재 스택의 상태는 [1, 2]이므로 size()는 2를 반환합니다. 시간복잡도: O(1) - // size: 스택의 크기 확인, O(1) - cout << "Stack size: " << s.size() << endl; // 출력: Stack size: 2 + // 스택에서 모든 원소를 pop하여 출력 + while (!s.empty()) { + cout << "Popping element: " << s.top() << endl; + s.pop(); + } + // 반복문을 통해 모든 원소를 제거합니다. 예를 들어, 현재 스택의 상태는 [1, 2]이므로 pop()은 2와 1을 순서대로 제거합니다. 시간복잡도: O(1) - // 성능 저하 예제: - // stack은 중간 요소에 직접 접근할 수 없으므로, 중간 요소를 검색하거나 수정하는 연산이 필요한 경우 - // stack보다는 다른 자료 구조를 사용하는 것이 좋습니다. + // 스택이 비어있는지 확인 + if (s.empty()) { + cout << "Stack is empty after popping all elements" << endl; // 출력: Stack is empty after popping all elements + } + // empty: 스택이 비어 있는지 여부를 확인합니다. 비어 있으면 true, 아니면 false를 반환합니다. 예를 들어, 현재 스택의 상태는 빈 상태이므로 empty()는 true를 반환합니다. 시간복잡도: O(1) return 0; } +/* +* 각 메서드의 동작 및 시간복잡도: +* - push: 스택의 맨 위에 원소를 추가합니다. 삽입된 원소는 기존 원소 위에 쌓이게 됩니다. +* 예를 들어, push(3)를 호출하면 스택의 상태는 [1, 2, 3]이 됩니다. 시간복잡도: O(1) +* - pop: 스택의 맨 위 원소를 제거합니다. 가장 최근에 추가된 원소가 제거됩니다. +* 예를 들어, pop()을 호출하면 스택의 상태는 [1, 2]가 됩니다. 시간복잡도: O(1) +* - top: 스택의 맨 위 원소를 반환합니다. 스택이 비어 있있을 때 호출하는 것은 undefined behavior입니다. +* 예를 들어, top()을 호출하면 현재 스택의 맨 위 원소 2가 반환됩니다. 시간복잡도: O(1) +* - empty: 스택이 비어 있는지 여부를 확인합니다. 스택이 비어 있으면 true, 아니면 false를 반환합니다. +* 예를 들어, 스택이 비어 있지 않으면 empty()는 false를 반환합니다. 시간복잡도: O(1) +* - size: 스택에 있는 원소의 개수를 반환합니다. 예를 들어, size()는 현재 스택에 2개의 원소가 +* 있으므로 2를 반환합니다. 시간복잡도: O(1) +*/ + +/* +* 스택을 사용해야 하는 경우: +* - 함수 호출이나 재귀 호출의 관리를 위해 (콜 스택) +* - 괄호의 짝을 맞추거나 문자열의 역순 변환 등 LIFO 구조가 필요한 경우 +* - Depth-First Search(DFS)와 같은 알고리즘 구현 시 + +* 스택을 사용하지 말아야 하는 경우: +* - 임의 접근이 필요한 경우 (스택은 특정 위치의 원소 접근이 불가능) +* - 중간 위치의 삽입/삭제가 빈번한 경우 (스택은 맨 위에서만 삽입/삭제 가능) +* - FIFO(First In First Out) 구조가 필요한 경우 (큐 사용 권장) +*/ + diff --git a/STL/unique.cpp b/STL/unique.cpp new file mode 100644 index 0000000..aa40126 --- /dev/null +++ b/STL/unique.cpp @@ -0,0 +1,77 @@ +//############################################################ +// | cafe | http://cafe.naver.com/dremdelover | +// | Q&A | https://open.kakao.com/o/gX0WnTCf | +// | business | ultrasuperrok@gmail.com | +//############################################################ +#include +#include +#include // for std::unique and std::sort +using namespace std; + +/* +std::unique 함수는 주어진 범위에서 인접한 중복 요소를 재배치합니다. +이 함수는 정렬된 상태에서 사용하면 모든 중복 요소를 올바르게 처리할 수 있습니다. + +주의해야 할 점: +1. 정렬된 상태에서 사용: unique 함수는 인접한 중복 요소만 재배치하므로, + 중복 요소를 완전히 제거하기 위해서는 범위가 정렬된 상태여야 합니다. +2. 반환값: 새로운 끝 부분을 반환합니다. 실제로 중복 요소를 제거하는 것이 아니라, + 중복되지 않은 요소를 앞으로 재배치합니다. +3. 벡터 크기 조정: 중복 요소를 재배치한 후, 반환된 새로운 끝 부분까지 벡터를 잘라내어야 합니다. +4. 새로운 범위: 반환된 반복자는 중복되지 않은 요소로 이루어진 새로운 범위의 끝을 나타냅니다. + 이 범위는 벡터의 시작부터 반환된 반복자 전까지입니다. + +시간복잡도: +- std::unique 함수의 시간복잡도는 O(N)입니다. 여기서 N은 범위 내의 요소 수입니다. +- std::vector::erase 함수의 시간복잡도도 O(N)입니다. 범위의 요소들을 제거하고 나머지 요소들을 앞으로 이동시키기 때문입니다. + +예제: +벡터가 {1, 2, 2, 3, 3, 3, 4, 4, 5}로 주어졌을 때, +unique(v.begin(), v.end())를 호출하면, +벡터는 {1, 2, 3, 4, 5, 3, 4, 4, 5}로 변환되고, 반환된 new_end는 첫 번째 5를 가리킵니다. +v.erase(new_end, v.end())를 호출하면, 벡터는 {1, 2, 3, 4, 5}로 변환됩니다. +*/ + +int main() { + // 예시 벡터 생성 (정렬되지 않은 상태) + vector v1 = {3, 1, 2, 3, 2, 4, 1, 5, 3}; + + // 중복 요소 재배치 (정렬되지 않은 상태에서) + auto last1 = unique(v1.begin(), v1.end()); + + // 결과 출력 (정렬되지 않은 상태에서 unique 사용 후) + cout << "정렬되지 않은 상태에서 unique 사용 후:\n"; + for (auto it = v1.begin(); it != last1; ++it) { + cout << *it << " "; + } + cout << endl; + + // 벡터를 정렬된 상태로 만들기 + vector v2 = {3, 1, 2, 3, 2, 4, 1, 5, 3}; + sort(v2.begin(), v2.end()); + + // 중복 요소 재배치 (정렬된 상태에서) + auto last2 = unique(v2.begin(), v2.end()); + + // 새로운 끝 부분까지 벡터를 잘라내기 + v2.erase(last2, v2.end()); + + // 결과 출력 (정렬된 상태에서 unique 사용 후) + cout << "정렬된 상태에서 unique 사용 후:\n"; + for (int num : v2) { + cout << num << " "; + } + cout << endl; + + return 0; +} + +/* +출력값: + +정렬되지 않은 상태에서 unique 사용 후: +3 1 2 3 2 4 1 5 3 + +정렬된 상태에서 unique 사용 후: +1 2 3 4 5 +*/ diff --git a/STL/unordered_map.cpp b/STL/unordered_map.cpp index 2ba88cb..784cd60 100644 --- a/STL/unordered_map.cpp +++ b/STL/unordered_map.cpp @@ -5,51 +5,131 @@ //############################################################ #include #include +#include using namespace std; -// unordered_map은 해시 테이블을 기반으로 한 STL 컨테이너입니다. -// - 키는 정렬되지 않은 순서로 저장됩니다. -// - 각 키는 유일합니다. -// 좋은 사용 시기: -// - 키를 기준으로 데이터를 매우 빠르게 검색하고자 할 때. -// - 키의 정렬 순서가 중요하지 않을 때. -// 성능 이슈: -// - 해시 충돌이 발생하면 성능이 떨어질 수 있습니다. -// - 연속된 메모리에 데이터를 저장하지 않기 때문에 cache locality가 떨어집니다. - int main() { + // unordered_map 컨테이너 설명 + // - unordered_map은 키와 값의 쌍으로 이루어진 순서가 없는 집합입니다. + // - 키는 중복을 허용하지 않으며, 삽입되는 원소는 해시 테이블로 관리됩니다. + // - 삽입, 삭제, 탐색 등의 주요 연산은 평균 O(1)의 시간복잡도를 가집니다. + // - 최악의 경우, 해시 충돌로 인해 시간복잡도가 O(N)이 될 수 있습니다. + + // unordered_map을 사용해야 하는 경우: + // - 키와 값의 쌍을 효율적으로 저장하고 관리해야 할 때. + // - 키의 순서가 중요하지 않을 때. + // - 평균 O(1)의 시간복잡도를 갖는 빠른 삽입, 삭제, 탐색이 필요할 때. - // unordered_map 선언 - unordered_map um; + // unordered_map을 사용하지 말아야 하는 경우: + // - 키의 순서가 중요할 때 (이 경우 map을 사용). + // - 해시 함수가 비효율적으로 동작하여 충돌이 많이 발생할 경우. + // - 데이터의 크기가 매우 크고, 메모리 사용이 중요한 경우 (해시 테이블은 메모리 사용량이 많을 수 있음). - // insert: O(1) 평균, O(n) 최악의 경우 (해시 충돌 시) - um.insert(make_pair("apple", 100)); - um.insert({"banana", 200}); + // unordered_map 컨테이너 선언 + // key: int (학생 ID), value: string (학생 이름) + unordered_map studentMap; - // operator[]: O(1) 평균, O(n) 최악의 경우 - um["cherry"] = 300; + // 삽입: 학생 ID와 이름을 맵에 추가 + // insert 함수 + // 인자: 삽입할 키와 값 쌍 (key, value) + // 동작: 키가 존재하지 않으면 삽입, 존재하면 값을 업데이트 + // 시간복잡도: 평균 O(1), 최악 O(N) + studentMap.insert({101, "Alice"}); + studentMap.insert({102, "Bob"}); + studentMap.insert({103, "Charlie"}); - // find: O(1) 평균, O(n) 최악의 경우 - auto it = um.find("banana"); - if(it != um.end()) { - cout << "banana: " << it->second << endl; // 출력: banana: 200 + // 맵의 모든 요소 출력 + cout << "Initial unordered_map content:\n"; + for (const auto& pair : studentMap) { + cout << "ID: " << pair.first << ", Name: " << pair.second << endl; } - // erase: O(1) 평균, O(n) 최악의 경우 - um.erase("apple"); + // 출력값: + // Initial unordered_map content: + // ID: 101, Name: Alice + // ID: 102, Name: Bob + // ID: 103, Name: Charlie + + // 탐색: 특정 ID로 학생 이름 찾기 + // find 함수 + // 인자: 찾을 키 (key) + // 동작: 키가 존재하면 iterator 반환, 없으면 end() 반환 + // 시간복잡도: 평균 O(1), 최악 O(N) + auto it = studentMap.find(102); + if (it != studentMap.end()) { + cout << "\nStudent with ID 102 found: " << it->second << endl; + } else { + cout << "\nStudent with ID 102 not found.\n"; + } + + // 출력값: + // Student with ID 102 found: Bob + + // 업데이트: 이미 존재하는 ID의 이름 변경 + // insert 함수 + // 인자: 삽입할 키와 값 쌍 (key, value) + // 동작: 키가 존재하지 않으면 삽입, 존재하면 값을 업데이트 + // 시간복잡도: 평균 O(1), 최악 O(N) + studentMap.insert({102, "Bobby"}); + cout << "\nAfter updating ID 102:\n"; + for (const auto& pair : studentMap) { + cout << "ID: " << pair.first << ", Name: " << pair.second << endl; + } - // size: O(1) - cout << um.size() << endl; // 출력: 2 + // 출력값: + // After updating ID 102: + // ID: 101, Name: Alice + // ID: 102, Name: Bobby + // ID: 103, Name: Charlie - // 순회: O(n) - for(auto it = um.begin(); it != um.end(); ++it) { - cout << it->first << ": " << it->second << endl; // 출력: banana: 200, cherry: 300 + // 삭제: 특정 ID의 학생 정보 삭제 + // erase 함수 + // 인자: 삭제할 키 (key) + // 동작: 키가 존재하면 해당 키의 요소를 삭제 + // 시간복잡도: 평균 O(1), 최악 O(N) + studentMap.erase(101); + cout << "\nAfter erasing ID 101:\n"; + for (const auto& pair : studentMap) { + cout << "ID: " << pair.first << ", Name: " << pair.second << endl; } - // clear: O(n) - um.clear(); + // 출력값: + // After erasing ID 101: + // ID: 102, Name: Bobby + // ID: 103, Name: Charlie + + // [] 연산자와 find의 차이점 + // [] 연산자 + // 인자: 접근할 키 (key) + // 동작: 키가 존재하면 해당 키의 값을 반환, 없으면 키를 생성하고 기본값을 설정 + // 시간복잡도: 평균 O(1), 최악 O(N) + cout << "\nUsing [] operator:\n"; + cout << "Student with ID 103: " << studentMap[103] << endl; // 존재하는 키 + cout << "Student with ID 104: " << studentMap[104] << endl; // 존재하지 않는 키, 기본값 설정 + + // 출력값: + // Using [] operator: + // Student with ID 103: Charlie + // Student with ID 104: + + // find 함수 + // 인자: 찾을 키 (key) + // 동작: 키가 존재하면 iterator 반환, 없으면 end() 반환 + // 시간복잡도: 평균 O(1), 최악 O(N) + cout << "\nUsing find function:\n"; + it = studentMap.find(103); + if (it != studentMap.end()) { + cout << "Student with ID 103 found: " << it->second << endl; + } else { + cout << "Student with ID 103 not found.\n"; + } + + // 출력값: + // Using find function: + // Student with ID 103 found: Charlie return 0; } + diff --git a/STL/unordered_set.cpp b/STL/unordered_set.cpp index ac3af1b..bd2b8b1 100644 --- a/STL/unordered_set.cpp +++ b/STL/unordered_set.cpp @@ -8,46 +8,95 @@ using namespace std; -// unordered_set은 해시 테이블을 기반으로 한 STL 컨테이너입니다. -// - 모든 요소는 유일합니다. -// - 내부적인 순서가 정의되어 있지 않습니다. -// 좋은 사용 시기: -// - 중복 없이 요소를 저장하고 싶을 때. -// - 내부 정렬 순서가 중요하지 않을 때. +int main() { + // unordered_set 컨테이너 설명 + // - unordered_set은 중복을 허용하지 않는 순서가 없는 집합입니다. + // - 원소는 해시 테이블로 관리되며, 자동으로 정렬되지 않습니다. + // - 삽입, 삭제, 탐색 등의 주요 연산은 평균 O(1)의 시간복잡도를 가집니다. + // - 최악의 경우, 해시 충돌로 인해 시간복잡도가 O(N)이 될 수 있습니다. + // unordered_set을 사용해야 하는 경우: + // - 중복되지 않는 값의 집합을 효율적으로 저장하고 관리해야 할 때. + // - 원소의 순서가 중요하지 않을 때. + // - 평균 O(1)의 시간복잡도를 갖는 빠른 삽입, 삭제, 탐색이 필요할 때. -int main() { + // unordered_set을 사용하지 말아야 하는 경우: + // - 원소의 순서가 중요할 때 (이 경우 set을 사용). + // - 해시 함수가 비효율적으로 동작하여 충돌이 많이 발생할 경우. + // - 데이터의 크기가 매우 크고, 메모리 사용이 중요한 경우 (해시 테이블은 메모리 사용량이 많을 수 있음). + + // unordered_set 컨테이너 선언 + // value: int (학생 ID) + unordered_set studentSet; + + // 삽입: 학생 ID를 셋에 추가 + // insert 함수 + // 인자: 삽입할 값 (value) + // 동작: 값이 존재하지 않으면 삽입 + // 시간복잡도: 평균 O(1), 최악 O(N) + studentSet.insert(101); + studentSet.insert(102); + studentSet.insert(103); - // unordered_set 선언 - unordered_set us; + // 셋의 모든 요소 출력 + cout << "Initial unordered_set content:\n"; + for (const auto& value : studentSet) { + cout << "ID: " << value << endl; + } - // insert: O(1) 평균, O(n) 최악의 경우 (해시 충돌 시) - us.insert(3); - us.insert(1); - us.insert(4); - us.insert(1); // 중복된 값은 추가되지 않습니다. + // 출력값: + // Initial unordered_set content: + // ID: 101 + // ID: 102 + // ID: 103 - // find: O(1) 평균, O(n) 최악의 경우 - auto it = us.find(3); - if(it != us.end()) { - cout << "Found: " << *it << endl; // 출력: Found: 3 + // 탐색: 특정 ID가 셋에 있는지 찾기 + // find 함수 + // 인자: 찾을 값 (value) + // 동작: 값이 존재하면 iterator 반환, 없으면 end() 반환 + // 시간복잡도: 평균 O(1), 최악 O(N) + auto it = studentSet.find(102); + if (it != studentSet.end()) { + cout << "\nStudent with ID 102 found.\n"; + } else { + cout << "\nStudent with ID 102 not found.\n"; } - // erase: O(1) 평균, O(n) 최악의 경우 - us.erase(1); + // 출력값: + // Student with ID 102 found. + + // 삭제: 특정 ID의 학생 정보 삭제 + // erase 함수 + // 인자: 삭제할 값 (value) + // 동작: 값이 존재하면 해당 값을 삭제 + // 시간복잡도: 평균 O(1), 최악 O(N) + studentSet.erase(101); + cout << "\nAfter erasing ID 101:\n"; + for (const auto& value : studentSet) { + cout << "ID: " << value << endl; + } - // size: O(1) - cout << us.size() << endl; // 출력: 2 + // 출력값: + // After erasing ID 101: + // ID: 102 + // ID: 103 - // 순회 (정의된 순서가 없습니다): O(n) - for(int num : us) { - cout << num << " "; // 출력: 3 4 (또는 4 3, 순서는 보장되지 않습니다.) + // [] 연산자는 unordered_set에서는 사용할 수 없음. 대신 find를 사용해야 함. + // find 함수 + // 인자: 찾을 값 (value) + // 동작: 값이 존재하면 iterator 반환, 없으면 end() 반환 + // 시간복잡도: 평균 O(1), 최악 O(N) + it = studentSet.find(103); + if (it != studentSet.end()) { + cout << "\nStudent with ID 103 found: " << *it << endl; + } else { + cout << "\nStudent with ID 103 not found.\n"; } - cout << endl; - // clear: O(n) - us.clear(); + // 출력값: + // Student with ID 103 found: 103 return 0; } + diff --git a/STL/vector.cpp b/STL/vector.cpp index ad92398..f192f0c 100644 --- a/STL/vector.cpp +++ b/STL/vector.cpp @@ -6,64 +6,103 @@ #include #include +using namespace std; + int main() { - // C++의 vector는 동적 배열을 구현한 컨테이너입니다. - // 연속된 메모리 위치에 데이터를 저장하기 때문에 랜덤 액세스가 가능하며, O(1)의 시간복잡도를 갖습니다. - // 배열과 비슷하지만, 크기가 동적으로 변경될 수 있습니다. - // 주로 알려진 크기가 없고, 자주 수정되지 않는 데이터를 저장할 때 사용됩니다. - - // vector의 선언 - std::vector vec; - - // push_back: O(1) 평균, O(n) 최악의 경우 (재할당이 필요할 때) - // 맨 뒤에 요소 추가 - vec.push_back(1); - vec.push_back(2); - vec.push_back(3); - - // [] operator: O(1) - // 랜덤 액세스 - std::cout << vec[1] << std::endl; // 출력: 2 - - // at: O(1) - // 랜덤 액세스, 범위를 검사 - std::cout << vec.at(2) << std::endl; // 출력: 3 - - // front: O(1) - // 첫 번째 요소 액세스 - std::cout << vec.front() << std::endl; // 출력: 1 - - // back: O(1) - // 마지막 요소 액세스 - std::cout << vec.back() << std::endl; // 출력: 3 - - // size: O(1) - // 벡터의 크기 반환 - std::cout << vec.size() << std::endl; // 출력: 3 - - // pop_back: O(1) - // 마지막 요소 제거 - vec.pop_back(); - - // resize: O(n) - // 벡터 크기 조절. 필요한 경우 값을 채워 넣거나 기존 값 삭제 - vec.resize(5, 100); // {1, 2, 100, 100, 100} - - // clear: O(n) - // 모든 요소 제거 - vec.clear(); - - // 성능 이슈: - // 1. 벡터의 중간에 요소를 삽입하거나 제거할 경우, O(n)의 시간복잡도를 갖게 되며 성능 저하가 발생할 수 있습니다. - // 예: - // vec.insert(vec.begin() + 1, 100); // 두 번째 위치에 100 삽입 - // vec.erase(vec.begin()); // 첫 번째 요소 제거 - - // 2. 벡터가 자주 재할당되면 (즉, push_back을 자주 호출하여 벡터의 용량을 초과하면) 성능 저하가 발생할 수 있습니다. - // 이를 피하기 위해 reserve 메서드를 사용하여 미리 메모리를 할당해두는 것이 좋습니다. - // 예: - // vec.reserve(1000); + // 벡터 초기화 방법 1: 기본 생성자 + vector vec1; // 빈 벡터 선언: vec1 = {} + + // 벡터 초기화 방법 2: 크기 지정, 모든 원소 0으로 초기화 + vector vec2(5); // vec2 = {0, 0, 0, 0, 0} + + // 벡터 초기화 방법 3: 크기와 초기값 지정 + vector vec3(5, 1); // vec3 = {1, 1, 1, 1, 1} + + // 벡터 초기화 방법 4: 초기화 리스트 사용 + vector vec4 = {1, 2, 3, 4, 5}; // vec4 = {1, 2, 3, 4, 5} + + // 벡터 초기화 방법 5: 다른 벡터로부터 초기화 + vector vec5(vec4); // vec5 = {1, 2, 3, 4, 5} + + // 벡터 초기화 방법 6: 다른 벡터의 부분 범위로부터 초기화 + vector vec6(vec4.begin() + 1, vec4.end() - 1); // vec6 = {2, 3, 4} + + // 벡터 메서드 예시 + vector vec; + + // push_back: 벡터의 끝에 원소를 추가합니다. + // vec.push_back(값) + // 벡터의 맨 끝에 '값'을 추가합니다. + // 시간복잡도: 평균 O(1) + vec.push_back(10); // vec = {10} + vec.push_back(20); // vec = {10, 20} + vec.push_back(30); // vec = {10, 20, 30} + + // pop_back: 벡터의 마지막 원소를 제거합니다. + // vec.pop_back() + // 벡터의 맨 끝에 있는 원소를 제거합니다. + // 시간복잡도: O(1) + vec.pop_back(); // vec = {10, 20} + + // insert: 지정한 위치에 원소를 삽입합니다. + // vec.insert(위치, 값) + // '위치'에 '값'을 삽입합니다. '위치'는 반복자로 지정합니다. + // vec.begin()은 첫 번째 원소를 가리킵니다. + // vec.begin() + 1은 두 번째 원소를 가리킵니다. + // 시간복잡도: O(n) + vec.insert(vec.begin() + 1, 15); // vec = {10, 15, 20} + + // erase: 지정한 위치의 원소를 제거합니다. + // vec.erase(위치) + // '위치'의 원소를 제거합니다. '위치'는 반복자로 지정합니다. + // vec.begin()은 첫 번째 원소를 가리킵니다. + // 시간복잡도: O(n) + vec.erase(vec.begin()); // vec = {15, 20} + + // size: 벡터의 크기를 반환합니다. + // vec.size() + // 현재 벡터에 저장된 원소의 개수를 반환합니다. + // 시간복잡도: O(1) + cout << "Size of vector: " << vec.size() << endl; // 출력: Size of vector: 2 + + // vector를 사용해야 하는 경우 + // 1. 동적 배열이 필요한 경우 + // 예: 프로그램 실행 중에 배열의 크기를 변경해야 하는 경우 + vector dynamicArray; + for (int i = 0; i < 10; ++i) { + dynamicArray.push_back(i * 2); // {0, 2, 4, 6, 8, 10, 12, 14, 16, 18} + } + cout << "Dynamic array: "; + for (int v : dynamicArray) { + cout << v << " "; + } + cout << endl; + + // 2. 임의 접근이 필요한 경우 + // 예: 특정 인덱스에 빠르게 접근해야 하는 경우 + cout << "Third element: " << dynamicArray[2] << endl; // 출력: Third element: 4 + + // 3. 데이터의 크기가 자주 바뀌는 경우 + // 예: 데이터의 추가와 삭제가 빈번하게 발생하는 경우 + vector flexibleArray; + flexibleArray.push_back(1); // {1} + flexibleArray.push_back(2); // {1, 2} + flexibleArray.pop_back(); // {1} + + // vector를 사용하지 말아야 하는 경우 + // 1. 값을 자주 찾아야 할 때 + // 예: 원소의 존재 여부를 자주 검사해야 하는 경우에는 비효율적 + // 대안: std::set 또는 std::unordered_set 사용 + vector searchVector = {1, 2, 3, 4, 5}; + if (find(searchVector.begin(), searchVector.end(), 3) != searchVector.end()) { + cout << "3 is in the vector" << endl; + } + + // 2. 맨 앞에 원소를 추가해야 할 때 + // 예: 벡터의 맨 앞에 원소를 자주 삽입해야 하는 경우에는 비효율적 + // 대안: std::deque 또는 std::list 사용 + vector inefficientFrontInsert = {1, 2, 3, 4, 5}; + inefficientFrontInsert.insert(inefficientFrontInsert.begin(), 0); // {0, 1, 2, 3, 4, 5} return 0; } - diff --git a/algorithm_performance.md b/algorithm_performance.md new file mode 100644 index 0000000..32852cf --- /dev/null +++ b/algorithm_performance.md @@ -0,0 +1,62 @@ + +# 알고리즘 성능 측정 및 공간 복잡도 고려 + +## 시간 복잡도(Time Complexity) + +알고리즘의 성능을 측정하는 데 있어 중요한 척도는 시간 복잡도입니다. 시간 복잡도는 알고리즘이 실행되는 데 걸리는 시간을 입력값의 크기와 관련지어 표현한 것입니다. 입력값이 커질수록 알고리즘의 실행 시간이 어떻게 변하는지를 나타내며, 이를 통해 알고리즘의 효율성을 평가할 수 있습니다. + +## 문제 요구 성능 파악 + +문제에서 요구하는 성능을 파악하기 위해서는 주어진 입력값을 통해 시간 복잡도를 구하고 이를 기준으로 성능을 분석해야 합니다. 이는 주어진 문제를 해결하는 데 있어 적합한 알고리즘을 선택하는 데 중요한 역할을 합니다. + +## 재귀 함수와 공간 복잡도 + +재귀 함수를 사용할 때는 시간 복잡도뿐만 아니라 공간 복잡도도 고려해야 합니다. 재귀 함수는 호출될 때마다 함수 코드 영역, 함수에서 사용하는 변수 영역, 매개변수 영역이 메모리에 할당됩니다. 예를 들어, 10!을 구할 때는 메모리에 10개의 함수 호출이 쌓이게 됩니다. + +### 주의사항: +1. **공간 복잡도**: 재귀 함수는 메모리 사용이 많을 수 있습니다. +2. **재귀 깊이 제한**: 일부 언어는 재귀 깊이 제한이 있습니다. (예: 파이썬) + +## 자료구조와 시간 복잡도 + +각 언어에서 제공하는 자료구조의 메서드의 시간 복잡도를 이해하고 정리하는 것은 중요합니다. 단순히 GitHub에서 제공하는 코드를 복사해서 사용하는 것이 아니라, 직접 구현해보고 성능 차이를 이해해야 합니다. 이를 통해 문제 분석 후 요구하는 성능에 맞는 구현 전략을 세울 수 있습니다. + +## 코드 개선과 시간 복잡도 + +시간 제한 초과(TLE) 문제를 해결하기 위해서는 시간 복잡도를 고려하여 연산 횟수가 많은 부분을 찾아 개선해야 합니다. 전체 코드에서 시간이 많이 소요되는 부분을 찾아 효율적인 알고리즘으로 대체함으로써 성능을 개선할 수 있습니다. + +## 의사코드(Pseudocode) + +의사코드는 구현하기 전 데이터의 흐름을 정리하고, 로직을 명확히 하는 데 사용됩니다. 이는 문제 해결 과정에서 중요한 단계로, 전체 시간의 60~70%를 할애할 가치가 있습니다. 구현 단계는 30~40% 정도로 시간 배분을 하면 좋습니다. + +--- + +# 시간 복잡도 정리 + +자료구조를 배울 때 각 메서드의 시간 복잡도를 정리해보겠습니다. 이는 직접 구현해보고 성능 차이를 이해하는 데 도움이 됩니다. + +## 예시: 리스트(List) + +- **탐색**: O(n) +- **삽입/삭제 (중간)**: O(n) +- **삽입/삭제 (끝)**: O(1) + +## 예시: 해시맵(HashMap) + +- **탐색**: O(1) +- **삽입**: O(1) +- **삭제**: O(1) + +이해하고 직접 구현해보면서 성능 차이를 경험해보는 것이 중요합니다. 이를 통해 문제 분석 후 요구하는 성능에 맞는 구현 전략을 세울 수 있습니다. + +--- + +## 결론 + +- 시간 복잡도는 알고리즘 성능 측정의 중요한 척도입니다. +- 재귀 함수를 사용할 때는 공간 복잡도도 고려해야 합니다. +- 자료구조의 각 메서드의 시간 복잡도를 이해하고 정리하는 것이 중요합니다. +- 코드 개선 시 시간 복잡도를 고려하여 주요 연산 부분을 최적화해야 합니다. +- 의사코드를 통해 데이터 흐름을 정리하고 구현 전 로직을 명확히 해야 합니다. + +학습 과정에서 복사 붙여넣기가 아니라 직접 구현하고 성능 차이를 이해하는 것이 중요합니다. 이를 통해 문제 분석 후 적절한 구현 전략을 세울 수 있는 능력을 키워야 합니다. diff --git a/algorithm_performance_with_cpp_examples.md b/algorithm_performance_with_cpp_examples.md new file mode 100644 index 0000000..21c29d6 --- /dev/null +++ b/algorithm_performance_with_cpp_examples.md @@ -0,0 +1,124 @@ + +# 알고리즘 성능 측정 및 공간 복잡도 고려 + +## 시간 복잡도(Time Complexity) + +알고리즘의 성능을 측정하는 데 있어 중요한 척도는 시간 복잡도입니다. 시간 복잡도는 알고리즘이 실행되는 데 걸리는 시간을 입력값의 크기와 관련지어 표현한 것입니다. 입력값이 커질수록 알고리즘의 실행 시간이 어떻게 변하는지를 나타내며, 이를 통해 알고리즘의 효율성을 평가할 수 있습니다. + +## 문제 요구 성능 파악 + +문제에서 요구하는 성능을 파악하기 위해서는 주어진 입력값을 통해 시간 복잡도를 구하고 이를 기준으로 성능을 분석해야 합니다. 이는 주어진 문제를 해결하는 데 있어 적합한 알고리즘을 선택하는 데 중요한 역할을 합니다. + +## 재귀 함수와 공간 복잡도 + +재귀 함수를 사용할 때는 시간 복잡도뿐만 아니라 공간 복잡도도 고려해야 합니다. 재귀 함수는 호출될 때마다 함수 코드 영역, 함수에서 사용하는 변수 영역, 매개변수 영역이 메모리에 할당됩니다. 예를 들어, 10!을 구할 때는 메모리에 10개의 함수 호출이 쌓이게 됩니다. + +### 주의사항: +1. **공간 복잡도**: 재귀 함수는 메모리 사용이 많을 수 있습니다. +2. **재귀 깊이 제한**: 일부 언어는 재귀 깊이 제한이 있습니다. (예: 파이썬) + +### 예시: C++의 재귀 깊이 제한 +```cpp +#include +using namespace std; + +void recursiveFunction(int depth) { + if (depth == 0) return; + recursiveFunction(depth - 1); +} + +int main() { + recursiveFunction(1000); // 재귀 깊이 1000으로 호출 + return 0; +} +``` +재귀 깊이 제한을 넘어가는 경우 스택 오버플로우가 발생할 수 있습니다. + +## 자료구조와 시간 복잡도 + +각 언어에서 제공하는 자료구조의 메서드의 시간 복잡도를 이해하고 정리하는 것은 중요합니다. 단순히 GitHub에서 제공하는 코드를 복사해서 사용하는 것이 아니라, 직접 구현해보고 성능 차이를 이해해야 합니다. 이를 통해 문제 분석 후 요구하는 성능에 맞는 구현 전략을 세울 수 있습니다. + +### STL 컨테이너 및 알고리즘의 시간복잡도 + +| 자료구조/알고리즘 | 접근 | 탐색 | 삽입 | 삭제 | +|------------------|------|------|------|------| +| 배열(Array) | O(1) | O(n) | O(n) | O(n) | +| 연결 리스트(Linked List) | O(n) | O(n) | O(1) | O(1) | +| 스택(Stack) | O(n) | O(n) | O(1) | O(1) | +| 큐(Queue) | O(n) | O(n) | O(1) | O(1) | +| 해시맵(HashMap) | - | O(1) | O(1) | O(1) | +| 트리(Tree) | O(log n) | O(log n) | O(log n) | O(log n) | +| 힙(Heap) | O(n) | O(1) | O(log n) | O(log n) | + +## 코드 개선과 시간 복잡도 + +시간 제한 초과(TLE) 문제를 해결하기 위해서는 시간 복잡도를 고려하여 연산 횟수가 많은 부분을 찾아 개선해야 합니다. 전체 코드에서 시간이 많이 소요되는 부분을 찾아 효율적인 알고리즘으로 대체함으로써 성능을 개선할 수 있습니다. + +### 예시: 코드 개선 +아래는 배열에서 중복된 요소를 제거하는 코드의 예시입니다. + +**개선 전:** +```cpp +#include +using namespace std; + +vector removeDuplicates(const vector& arr) { + vector result; + for (int i : arr) { + if (find(result.begin(), result.end(), i) == result.end()) { + result.push_back(i); + } + } + return result; +} +``` +위 코드는 중복 검사를 위해 `find` 함수를 사용하므로 시간 복잡도는 O(n^2)입니다. + +**개선 후:** +```cpp +#include +#include +using namespace std; + +vector removeDuplicates(const vector& arr) { + unordered_set s(arr.begin(), arr.end()); + return vector(s.begin(), s.end()); +} +``` +`unordered_set` 자료형을 사용하면 시간 복잡도가 O(n)으로 개선됩니다. + +## 의사코드(Pseudocode) + +의사코드는 구현하기 전 데이터의 흐름을 정리하고, 로직을 명확히 하는 데 사용됩니다. 이는 문제 해결 과정에서 중요한 단계로, 전체 시간의 60~70%를 할애할 가치가 있습니다. 구현 단계는 30~40% 정도로 시간 배분을 하면 좋습니다. + +--- + +# 시간 복잡도 정리 + +자료구조를 배울 때 각 메서드의 시간 복잡도를 정리해보겠습니다. 이는 직접 구현해보고 성능 차이를 이해하는 데 도움이 됩니다. + +## 예시: 리스트(List) + +- **탐색**: O(n) +- **삽입/삭제 (중간)**: O(n) +- **삽입/삭제 (끝)**: O(1) + +## 예시: 해시맵(HashMap) + +- **탐색**: O(1) +- **삽입**: O(1) +- **삭제**: O(1) + +이해하고 직접 구현해보면서 성능 차이를 경험해보는 것이 중요합니다. 이를 통해 문제 분석 후 요구하는 성능에 맞는 구현 전략을 세울 수 있습니다. + +--- + +## 결론 + +- 시간 복잡도는 알고리즘 성능 측정의 중요한 척도입니다. +- 재귀 함수를 사용할 때는 공간 복잡도도 고려해야 합니다. +- 자료구조의 각 메서드의 시간 복잡도를 이해하고 정리하는 것이 중요합니다. +- 코드 개선 시 시간 복잡도를 고려하여 주요 연산 부분을 최적화해야 합니다. +- 의사코드를 통해 데이터 흐름을 정리하고 구현 전 로직을 명확히 해야 합니다. + +학습 과정에서 복사 붙여넣기가 아니라 직접 구현하고 성능 차이를 이해하는 것이 중요합니다. 이를 통해 문제 분석 후 적절한 구현 전략을 세울 수 있는 능력을 키워야 합니다. diff --git a/event.md b/event.md new file mode 100644 index 0000000..2f122a1 --- /dev/null +++ b/event.md @@ -0,0 +1,13 @@ +# 스터디 진행사항 + + +## 그룹 스터디 모집 +같이 공부할 스터디원을 모집해드립니다. 참석 누르고, 댓글 다시면 연락 드립니다.(디스코드로 연락 드립니다.) +| 스터디명 | 링크 | 기간 | +| --- | --- | ---| +| 모각코 | [참석](https://discord.com/invite/JhBB9wYgWw) | 상시 ( 오전 10시, 오후 11시 매일) | +|코딩 테스트 합격자 되기 - 자바스크립트 | [참석](https://discord.com/channels/1190334577248583791/1265644208203763713) | ~07/31까지 모집 | +|코테 문제 플랫폼(백준,codeleet,프로그래머스)에서 문제풀이 | [참석](https://discord.com/channels/1190334577248583791/1265644805057417217) |~07/31까지 모집 | +| 코딩 테스트 합격자 되기 - 자바 | [참석](https://discord.com/channels/1190334577248583791/1265644208203763713) |~07/31까지 모집 | +| 코딩 테스트 합격자 되기 - C++ | [참석](https://discord.com/channels/1190334577248583791/1265647941511680070) |~07/31까지 모집 | + diff --git a/performance/ReadMe.md b/performance/ReadMe.md new file mode 100644 index 0000000..ddc5ecd --- /dev/null +++ b/performance/ReadMe.md @@ -0,0 +1,5 @@ +| 파일 이름 | 설명 | +|-------------------------------|-----------------------------------------------------------------------------------------------------------------------------------| +| `set_vs_unordered_set.cpp` | `std::set`와 `std::unordered_set`의 성능 비교: 각 컨테이너의 특성과 성능 측정을 통한 삽입, 검색 시간 비교를 설명합니다. | +| `vector_vs_set.cpp` | `std::vector`와 `std::set`의 사용 시나리오와 성능 차이를 비교: 요소 접근, 삽입, 삭제 작업의 효율성을 분석합니다. | +| `map_vs_unordered_map.cpp` | `std::map`과 `std::unordered_map`의 성능 차이를 설명: 삽입 및 검색 작업에서의 시간 복잡도 및 실제 사용 예를 통해 각 컨테이너의 장단점을 비교합니다. | diff --git a/reference/ReadMe.md b/reference/ReadMe.md new file mode 100644 index 0000000..e04a0c4 --- /dev/null +++ b/reference/ReadMe.md @@ -0,0 +1,17 @@ +| 파일 이름 | 설명 | +|-------------------------|-----------------------------------------------------------------------------| +| `if_statement.cpp` | if문 사용법: 조건에 따라 다르게 실행되는 코드 블록을 만드는 if, if-else, 및 if-else if-else 문을 설명합니다. | +| `loop.cpp` | 반복문 사용법: for, while, do-while 반복문을 사용하여 코드를 반복 실행하는 방법을 설명합니다. | +| `reference.cpp` | 참조 사용법: C++에서 참조를 사용하여 변수의 별칭을 생성하고, 함수의 매개변수로 참조를 사용하는 예제를 보여줍니다. | +| `string.cpp` | 문자열 사용법: std::string 클래스를 사용하여 문자열을 생성, 수정, 조작하는 다양한 방법을 설명합니다. | +| `type_conversion.cpp` | 타입 변환: 암시적 및 명시적 타입 변환, 그리고 static_cast, dynamic_cast를 사용한 변환 예제를 제공합니다. | +| `variable.cpp` | 변수 정의: C++에서 변수를 정의하고 초기화하는 방법, 변수의 유효 범위와 생명주기를 설명합니다. | +| `variable_scope.cpp` | 변수 범위: 전역 변수, 지역 변수, 정적 변수의 범위와 사용 시 주의점을 다룹니다. | +| `decay.cpp` | 타입 감쇠: 함수 템플릿과 배열, 함수 포인터 매개변수에서의 타입 감쇠 현상을 설명합니다. | +| `dynamic_alloc.cpp` | 동적 할당: new와 delete를 사용하여 힙 메모리를 할당하고 해제하는 방법을 설명합니다. | +| `function.cpp` | 함수 사용법: 함수의 정의, 호출, 매개변수 전달, 반환 값과 함수 오버로딩에 대해 설명합니다. | +| `callback.cpp` | 콜백 함수 사용법: 함수를 다른 함수의 인자로 전달하고, 이벤트 발생 시 호출하는 방법을 설명합니다. | +| `variables.cpp` | 변수 선언과 초기화: 다양한 데이터 타입의 변수를 선언하고 초기화하는 방법을 설명합니다. | +| `array.cpp` | 배열 사용법: 동일한 타입의 여러 데이터를 연속적인 메모리 공간에 저장하는 배열의 선언, 접근 및 동적 할당 방법을 설명합니다. | + + diff --git a/reference/iterator.cpp b/reference/iterator.cpp deleted file mode 100644 index e5cc837..0000000 --- a/reference/iterator.cpp +++ /dev/null @@ -1,98 +0,0 @@ -//############################################################ -// | cafe | http://cafe.naver.com/dremdelover | -// | Q&A | https://open.kakao.com/o/gX0WnTCf | -// | business | ultrasuperrok@gmail.com | -//############################################################ -#include -#include -#include - -using namespace std; - -// 반복자 예시 -// 반복자는 컨테이너(예: 벡터, 리스트 등)의 요소를 순회하는 데 사용됩니다. -// 반복자는 포인터와 비슷하게 동작하며, STL에서 중요한 역할을 합니다. -// 반복자를 사용하면 컨테이너의 내부 구현에 독립적으로 요소를 처리할 수 있습니다. - -int main() { - // 벡터 선언 및 초기화 - vector vec = {1, 2, 3, 4, 5}; - - // 리스트 선언 및 초기화 - list lst = {10, 20, 30, 40, 50}; - - // 순방향 반복자를 사용하여 벡터의 요소를 순회하고 출력 - cout << "Vector elements: "; - for (vector::iterator it = vec.begin(); it != vec.end(); ++it) { - cout << *it << " "; // 반복자가 가리키는 요소를 출력 - } - cout << endl; // 출력: Vector elements: 1 2 3 4 5 - - // 순방향 반복자를 사용하여 리스트의 요소를 순회하고 출력 - cout << "List elements: "; - for (list::iterator it = lst.begin(); it != lst.end(); ++it) { - cout << *it << " "; // 반복자가 가리키는 요소를 출력 - } - cout << endl; // 출력: List elements: 10 20 30 40 50 - - // 역방향 반복자를 사용하여 벡터의 요소를 순회하고 출력 - cout << "Vector elements in reverse: "; - for (vector::reverse_iterator rit = vec.rbegin(); rit != vec.rend(); ++rit) { - cout << *rit << " "; // 역방향 반복자가 가리키는 요소를 출력 - } - cout << endl; // 출력: Vector elements in reverse: 5 4 3 2 1 - - // 역방향 반복자를 사용하여 리스트의 요소를 순회하고 출력 - cout << "List elements in reverse: "; - for (list::reverse_iterator rit = lst.rbegin(); rit != lst.rend(); ++rit) { - cout << *rit << " "; // 역방향 반복자가 가리키는 요소를 출력 - } - cout << endl; // 출력: List elements in reverse: 50 40 30 20 10 - - return 0; -} - -/* -반복자의 목적: -- 반복자는 컨테이너의 요소를 순차적으로 접근하고 조작하는 데 사용됩니다. -- 반복자를 통해 컨테이너의 내부 구현에 독립적으로 요소를 처리할 수 있습니다. - -반복자의 장점: -1. 유연성: 다양한 컨테이너에 대해 동일한 방식으로 요소를 순회할 수 있습니다. -2. 추상화: 컨테이너의 내부 구조를 몰라도 요소를 접근할 수 있습니다. -3. 범용성: 알고리즘 함수와 함께 사용하여 코드의 재사용성을 높입니다. - -순방향 반복자와 역방향 반복자: -1. 순방향 반복자: 컨테이너의 처음(begin)부터 끝(end)까지 순차적으로 요소를 접근합니다. - - 선언: `vector::iterator it;` - - 초기화: `it = vec.begin();` - - 사용: `*it`를 통해 반복자가 가리키는 요소에 접근합니다. - -2. 역방향 반복자: 컨테이너의 끝(rbegin)부터 처음(rend)까지 역순으로 요소를 접근합니다. - - 선언: `vector::reverse_iterator rit;` - - 초기화: `rit = vec.rbegin();` - - 사용: `*rit`를 통해 역방향 반복자가 가리키는 요소에 접근합니다. - -반복자의 사용방법: -1. 반복자 선언: 컨테이너 타입에 따라 반복자를 선언합니다. 예: `vector::iterator`. -2. 반복자 초기화: `begin()`과 `end()`, 또는 `rbegin()`과 `rend()` 함수를 사용하여 반복자를 초기화합니다. -3. 반복자 사용: 반복자를 사용하여 요소에 접근하고 조작합니다. - -예시: -- 벡터 요소 순회: `for (it = vec.begin(); it != vec.end(); ++it)`. -- 리스트 요소 순회: `for (it = lst.begin(); it != lst.end(); ++it)`. -- 역방향 요소 순회: `for (rit = vec.rbegin(); rit != vec.rend(); ++rit)`. -- 요소 접근: `*it` 또는 `*rit`를 사용하여 반복자가 가리키는 요소에 접근합니다. - -코드 설명: -1. 벡터 선언 및 초기화: `vector vec = {1, 2, 3, 4, 5};` 벡터를 선언하고 초기화합니다. -2. 리스트 선언 및 초기화: `list lst = {10, 20, 30, 40, 50};` 리스트를 선언하고 초기화합니다. -3. 순방향 반복자 사용: - - `for (vector::iterator it = vec.begin(); it != vec.end(); ++it)`: 순방향 반복자를 사용하여 벡터의 요소를 순회합니다. - - `for (list::iterator it = lst.begin(); it != lst.end(); ++it)`: 순방향 반복자를 사용하여 리스트의 요소를 순회합니다. - - `*it`: 순방향 반복자가 가리키는 요소를 출력합니다. -4. 역방향 반복자 사용: - - `for (vector::reverse_iterator rit = vec.rbegin(); rit != vec.rend(); ++rit)`: 역방향 반복자를 사용하여 벡터의 요소를 역순으로 순회합니다. - - `for (list::reverse_iterator rit = lst.rbegin(); rit != lst.rend(); ++rit)`: 역방향 반복자를 사용하여 리스트의 요소를 역순으로 순회합니다. - - `*rit`: 역방향 반복자가 가리키는 요소를 출력합니다. -*/ diff --git a/reference/string.cpp b/reference/string.cpp index 81baece..c5f193d 100644 --- a/reference/string.cpp +++ b/reference/string.cpp @@ -12,6 +12,42 @@ int main() { // 1. 기본적인 문자열 생성 및 출력 string str1 = "Hello, World!"; // 문자열 초기화 cout << str1 << endl; // 출력: Hello, World! + // 인덱스: | 0| 1| 2| 3| 4| 5| 6| 7| 8| 9|10|11|12| + // 값 : | H| e| l| l| o| ,| | W| o| r| l| d| !| + // 시간 복잡도: O(n) (n은 문자열의 길이) + // 주의할 점: 문자열의 길이가 길어질수록 초기화에 시간이 더 걸립니다. + + // 여러가지 문자열의 초기화 방법 + string str2("Hello"); // str2: H e l l o + cout << str2 << endl; // 출력: Hello + // 인덱스: | 0| 1| 2| 3| 4| + // 값 : | H| e| l| l| o| + // 시간 복잡도: O(n) + + string str3 = "World"; // str3: W o r l d + cout << str3 << endl; // 출력: World + // 인덱스: | 0| 1| 2| 3| 4| + // 값 : | W| o| r| l| d| + // 시간 복잡도: O(n) + + string str4(str2 + ", " + str3 + "!"); // str4: H e l l o , W o r l d ! + cout << str4 << endl; // 출력: Hello, World! + // 인덱스: | 0| 1| 2| 3| 4| 5| 6| 7| 8| 9|10|11|12| + // 값 : | H| e| l| l| o| ,| | W| o| r| l| d| !| + // 시간 복잡도: O(n + m) (n과 m은 각각 str2와 str3의 길이) + // 주의할 점: 문자열을 자주 연결하면 시간 복잡도가 커질 수 있습니다. + + string str5 = str1; // str5: H e l l o , W o r l d ! + cout << str5 << endl; // 출력: Hello, World! + // 인덱스: | 0| 1| 2| 3| 4| 5| 6| 7| 8| 9|10|11|12| + // 값 : | H| e| l| l| o| ,| | W| o| r| l| d| !| + // 시간 복잡도: O(n) + + string str6(5, 'A'); // str6: A A A A A + cout << str6 << endl; // 출력: AAAAA + // 인덱스: | 0| 1| 2| 3| 4| + // 값 : | A| A| A| A| A| + // 시간 복잡도: O(n) /* 문자열(string)이란? @@ -22,30 +58,76 @@ int main() { */ // 2. 문자열 연결 - string str2 = "Hello"; - string str3 = "World"; - string str4 = str2 + ", " + str3 + "!"; // 문자열 연결 - cout << str4 << endl; // 출력: Hello, World! + string str7 = "Hello"; + cout << str7 << endl; // 출력: Hello + // 인덱스: | 0| 1| 2| 3| 4| + // 값 : | H| e| l| l| o| + // 시간 복잡도: O(n) + + string str8 = "World"; + cout << str8 << endl; // 출력: World + // 인덱스: | 0| 1| 2| 3| 4| + // 값 : | W| o| r| l| d| + // 시간 복잡도: O(n) + + string str9 = str7 + ", " + str8 + "!"; + cout << str9 << endl; // 출력: Hello, World! + // 인덱스: | 0| 1| 2| 3| 4| 5| 6| 7| 8| 9|10|11|12| + // 값 : | H| e| l| l| o| ,| | W| o| r| l| d| !| + // 시간 복잡도: O(n + m) + // 주의할 점: 여러 문자열을 연결할 때는 효율성을 고려해야 합니다. // 3. 문자열 길이와 접근 cout << "Length of str1: " << str1.length() << endl; // 출력: 13 + // str1의 길이: 13 (H e l l o , W o r l d !) + // 시간 복잡도: O(1) + cout << "First character of str1: " << str1[0] << endl; // 출력: H + // str1의 첫 번째 문자: H (H e l l o , W o r l d !) + // 시간 복잡도: O(1) /* - 주의사항: - - 문자열 인덱스를 사용하여 문자에 접근할 때 인덱스 범위를 초과하지 않도록 주의해야 합니다. 초과할 경우 undefined behavior가 발생합니다. + length() 메서드를 사용하여 문자열의 길이를 알 수 있습니다. + 예: str1.length()는 13을 반환합니다. + + 대괄호([])를 사용하여 문자열의 특정 위치에 있는 문자에 접근할 수 있습니다. + 예: str1[0]은 'H'를 반환합니다. */ // 4. 문자열의 부분 문자열 및 찾기 - string str5 = str1.substr(7, 5); // 7번째 인덱스부터 5개의 문자를 가져옴 - cout << "Substring: " << str5 << endl; // 출력: World + string str10 = str1.substr(7, 5); + cout << "Substring: " << str10 << endl; // 출력: World + // 인덱스: | 0| 1| 2| 3| 4| + // 값 : | W| o| r| l| d| + // 시간 복잡도: O(m) (m은 부분 문자열의 길이) + // 주의할 점: substr의 인자는 시작 위치와 길이입니다. - size_t pos = str1.find("World"); // "World"의 시작 위치를 찾습니다. + size_t pos = str1.find("World"); if (pos != string::npos) { cout << "\"World\" starts at index " << pos << endl; // 출력: "World" starts at index 7 } else { cout << "\"World\" not found!" << endl; } + // str1: | H| e| l| l| o| ,| | W| o| r| l| d| !| + // 인덱스: | 0| 1| 2| 3| 4| 5| 6| 7| 8| 9|10|11|12| + // 시간 복잡도: O(n) + // 주의할 점: find 메서드는 찾는 문자열의 시작 위치를 반환하며, 없으면 string::npos를 반환합니다. + + // 5. 문자열 대체 + string str11 = "I like cats"; + cout << str11 << endl; // 출력: I like cats + // 인덱스: | 0| 1| 2| 3| 4| 5| 6| 7| 8| 9|10| + // 값 : | I| | l| i| k| e| | c| a| t| s| + + str11.replace(7, 4, "dogs"); + cout << str11 << endl; // 출력: I like dogs + // str11: | I| | l| i| k| e| | d| o| g| s| + // 인덱스: | 0| 1| 2| 3| 4| 5| 6| 7| 8| 9|10| + // 시간 복잡도: O(m) (m은 대체할 문자열의 길이) + // 주의할 점: replace의 인자는 시작 위치, 길이, 대체할 문자열입니다. return 0; } + + + diff --git a/solution/ReadMe.md b/solution/ReadMe.md index 5125d3d..f1934d6 100644 --- a/solution/ReadMe.md +++ b/solution/ReadMe.md @@ -44,13 +44,12 @@ |[23.cpp](https://github.com/dremdeveloper/codingtest_cpp/blob/main/solution/23.cpp)|할인 행사|⭐⭐|https://school.programmers.co.kr/learn/courses/30/lessons/131127| |[24.cpp](https://github.com/dremdeveloper/codingtest_cpp/blob/main/solution/24.cpp)|오픈 채팅방|⭐⭐|https://programmers.co.kr/learn/courses/30/lessons/42888| |[25.cpp](https://github.com/dremdeveloper/codingtest_cpp/blob/main/solution/25.cpp)|베스트 앨범|⭐⭐| https://programmers.co.kr/learn/courses/30/lessons/42579| - +|[26.cpp](https://github.com/dremdeveloper/codingtest_cpp/blob/main/solution/26.cpp)|신고 결과 받기|⭐⭐|https://programmers.co.kr/learn/courses/30/lessons/92334| +|[27.cpp](https://github.com/dremdeveloper/codingtest_cpp/blob/main/solution/27.cpp)|메뉴 리뉴얼|⭐⭐⭐|https://programmers.co.kr/learn/courses/30/lessons/72411| ## 09장 트리 | 파일명 | 문제 |난이도| 비고 | |------|------|------|------| -|[26.cpp](https://github.com/dremdeveloper/codingtest_cpp/blob/main/solution/26.cpp)|신고 결과 받기|⭐⭐|https://programmers.co.kr/learn/courses/30/lessons/92334| -|[27.cpp](https://github.com/dremdeveloper/codingtest_cpp/blob/main/solution/27.cpp)|메뉴 리뉴얼|⭐⭐⭐|https://programmers.co.kr/learn/courses/30/lessons/72411| |[28.cpp](https://github.com/dremdeveloper/codingtest_cpp/blob/main/solution/28.cpp)|트리 순회|⭐⭐|저자 출제| |[29.cpp](https://github.com/dremdeveloper/codingtest_cpp/blob/main/solution/29.cpp)|이진 탐색 트리 구현|⭐⭐⭐|저자 출제| |[30.cpp](https://github.com/dremdeveloper/codingtest_cpp/blob/main/solution/30.cpp)|예상 대진표|⭐|https://programmers.co.kr/learn/courses/30/lessons/12985| @@ -64,13 +63,14 @@ |[33.cpp](https://github.com/dremdeveloper/codingtest_cpp/blob/main/solution/33.cpp)|간단한 유니온-파인드 알고리즘 구현하기|⭐⭐|저자 출제| |[34.cpp](https://github.com/dremdeveloper/codingtest_cpp/blob/main/solution/34.cpp)|폰켓몬|⭐|https://programmers.co.kr/learn/courses/30/lessons/1845| |[35.cpp](https://github.com/dremdeveloper/codingtest_cpp/blob/main/solution/35.cpp)|섬 연결하기|⭐⭐⭐| https://school.programmers.co.kr/learn/courses/30/lessons/42861| -|[36.cpp](https://github.com/dremdeveloper/codingtest_cpp/blob/main/solution/36.cpp)|깊이 우선 탐색으로 모든 노드를 순회하는 함수 작성하기|⭐|저자 출제| -|[37.cpp](https://github.com/dremdeveloper/codingtest_cpp/blob/main/solution/37.cpp)|너비 우선 탐색으로 이용하여 모든 노드를 순회하는 함수 작성하기|⭐|저자 출제| + ## 11장 그래프 | 파일명 | 문제 |난이도| 비고 | |------|------|------|------| +|[36.cpp](https://github.com/dremdeveloper/codingtest_cpp/blob/main/solution/36.cpp)|깊이 우선 탐색으로 모든 노드를 순회하는 함수 작성하기|⭐|저자 출제| +|[37.cpp](https://github.com/dremdeveloper/codingtest_cpp/blob/main/solution/37.cpp)|너비 우선 탐색으로 이용하여 모든 노드를 순회하는 함수 작성하기|⭐|저자 출제| |[38.cpp](https://github.com/dremdeveloper/codingtest_cpp/blob/main/solution/38.cpp)|다익스트라 알고리즘 구현하기|⭐⭐⭐|저자 출제| |[39.cpp](https://github.com/dremdeveloper/codingtest_cpp/blob/main/solution/39.cpp)|벨만-포드 알고리즘 구현하기|⭐⭐⭐|저자 출제| |[40.cpp](https://github.com/dremdeveloper/codingtest_cpp/blob/main/solution/40.cpp)|미로 탈출|⭐⭐|https://school.programmers.co.kr/learn/courses/30/lessons/159993| diff --git a/summary/time_complexity.md b/summary/time_complexity.md new file mode 100644 index 0000000..8b8c7d0 --- /dev/null +++ b/summary/time_complexity.md @@ -0,0 +1,162 @@ + +# 알고리즘 성능 측정 및 공간 복잡도 고려 + +## 시간 복잡도(Time Complexity) + +알고리즘의 성능을 측정하는 데 있어 중요한 척도는 시간 복잡도입니다. 시간 복잡도는 알고리즘이 실행되는 데 걸리는 시간을 입력값의 크기와 관련지어 표현한 것입니다. 입력값이 커질수록 알고리즘의 실행 시간이 어떻게 변하는지를 나타내며, 이를 통해 알고리즘의 효율성을 평가할 수 있습니다. + +### 시간 복잡도 예시 + +간단한 예시로, 배열의 모든 요소를 더하는 함수의 시간 복잡도를 살펴보겠습니다. + +```cpp +int sumArray(const vector& arr) { + int sum = 0; + for (int num : arr) { + sum += num; + } + return sum; +} +``` + +위 함수의 시간 복잡도는 O(n)입니다. 배열의 길이가 n일 때, 모든 요소를 한 번씩 방문하여 더하기 때문입니다. + +## 문제 요구 성능 파악 + +문제에서 요구하는 성능을 파악하기 위해서는 주어진 입력값을 통해 시간 복잡도를 구하고 이를 기준으로 성능을 분석해야 합니다. 이는 주어진 문제를 해결하는 데 있어 적합한 알고리즘을 선택하는 데 중요한 역할을 합니다. +예를 들어, 입력값으로 주어진 배열의 길이가 100만 이상일 때 O(n^2) 시간 복잡도의 알고리즘은 현실적으로 실행 시간이 너무 오래 걸릴 수 있습니다. 이 경우 O(n) 또는 O(n log n) 시간 복잡도의 알고리즘을 찾아야 합니다. + +```cpp +#include +#include +#include // sort 함수 포함 +using namespace std; + +bool hasDuplicate(const vector& arr) { + vector sortedArr = arr; + sort(sortedArr.begin(), sortedArr.end()); // O(n log n) + for (size_t i = 1; i < sortedArr.size(); ++i) { + if (sortedArr[i] == sortedArr[i - 1]) { + return true; + } + } + return false; +} + +int main() { + vector arr = {1, 2, 3, 4, 5, 6}; + cout << (hasDuplicate(arr) ? "Duplicates found" : "No duplicates") << endl; + return 0; +} +``` +## 재귀 함수와 공간 복잡도 + +재귀 함수를 사용할 때는 시간 복잡도뿐만 아니라 공간 복잡도도 고려해야 합니다. 재귀 함수는 호출될 때마다 함수 코드 영역, 함수에서 사용하는 변수 영역, 매개변수 영역이 메모리에 할당됩니다. 예를 들어, 10!을 구할 때는 메모리에 10개의 함수 호출이 쌓이게 됩니다. + +### 예시: 재귀 함수의 메모리 사용 +```cpp +#include +using namespace std; + +int factorial(int n) { + if (n == 1) return 1; + return n * factorial(n - 1); // 각 호출마다 메모리에 함수 정보가 쌓임 +} + +int main() { + int result = factorial(10); // 10!을 계산 + cout << "10! = " << result << endl; + return 0; +} +``` +위 예시에서 `factorial` 함수는 재귀 호출될 때마다 스택에 함수 호출 정보가 쌓입니다. 이는 메모리를 많이 사용할 수 있다는 점에서 주의해야 합니다. + +## 자료구조와 시간 복잡도 + +각 언어에서 제공하는 자료구조의 메서드의 시간 복잡도를 이해하고 정리하는 것은 중요합니다. 단순히 GitHub에서 제공하는 코드를 복사해서 사용하는 것이 아니라, 직접 구현해보고 성능 차이를 이해해야 합니다. 이를 통해 문제 분석 후 요구하는 성능에 맞는 구현 전략을 세울 수 있습니다. + +### STL 컨테이너 및 알고리즘의 시간복잡도 + +| 자료구조/알고리즘 | 접근 | 탐색 | 삽입 | 삭제 | 설명 | +|---------------------|----------|----------|----------|----------|-------------------------------------| +| 벡터(vector) | O(1) | O(n) | O(n) | O(n) | 동적 배열, 크기 조절 가능 | +| 스택(stack) | O(n) | O(n) | O(1) | O(1) | LIFO 구조 | +| 큐(queue) | O(n) | O(n) | O(1) | O(1) | FIFO 구조 | +| 해시맵(unordered_map) | - | O(1) | O(1) | O(1) | 키-값 쌍, 순서 보장 안 함 | +| 해시셋(unordered_set) | - | O(1) | O(1) | O(1) | 고유한 값의 집합, 순서 보장 안 함 | +| 맵(map) | O(log n) | O(log n) | O(log n) | O(log n) | 키-값 쌍, 키 순서대로 정렬 | +| 셋(set) | - | O(log n) | O(log n) | O(log n) | 고유한 값의 집합, 값 순서대로 정렬 | +| 우선순위 큐(priority_queue) | - | - | O(log n) | O(log n) | 힙 기반, 우선순위가 높은 요소 먼저 처리 | +| `find` | O(n) | - | - | - | 범위 내 특정 값 찾기 | +| `erase` | O(n) | - | - | O(n) | 범위 내 요소 삭제 | +| `next_permutation` | O(n) | - | - | - | 다음 순열 생성 | +| `max_element` | O(n) | - | - | - | 범위 내 최대값 찾기 | +| `min_element` | O(n) | - | - | - | 범위 내 최소값 찾기 | + + +## 코드 개선과 시간 복잡도 + +시간 제한 초과(TLE) 문제를 해결하기 위해서는 시간 복잡도를 고려하여 연산 횟수가 많은 부분을 찾아 개선해야 합니다. 전체 코드에서 시간이 많이 소요되는 부분을 찾아 효율적인 알고리즘으로 대체함으로써 성능을 개선할 수 있습니다. + +### 예시: 코드 개선 +아래는 배열에서 중복된 요소를 제거하는 코드의 예시입니다. + +**개선 전:** +```cpp +#include +using namespace std; + +vector removeDuplicates(const vector& arr) { + vector result; + for (int i : arr) { + if (find(result.begin(), result.end(), i) == result.end()) { + result.push_back(i); + } + } + return result; +} +``` +위 코드는 중복 검사를 위해 `find` 함수를 사용하므로 시간 복잡도는 O(n^2)입니다. `find` 함수는 벡터에서 특정 값을 찾는 데 O(n)의 시간이 걸리고, 이를 모든 요소에 대해 반복하기 때문에 O(n^2)의 시간 복잡도가 됩니다. + +**개선 후:** +```cpp +#include +#include +using namespace std; + +vector removeDuplicates(const vector& arr) { + unordered_set s(arr.begin(), arr.end()); // O(n) + return vector(s.begin(), s.end()); // O(n) +} +``` +`unordered_set` 자료형을 사용하면 시간 복잡도가 O(n)으로 개선됩니다. `unordered_set`은 해시 기반의 자료구조로, 삽입과 검색이 평균적으로 O(1)의 시간 복잡도를 가지기 때문입니다. 따라서 전체 중복 제거 과정은 O(n)의 시간 복잡도를 갖습니다. + +## 의사코드(Pseudocode) + +의사코드는 구현하기 전 데이터의 흐름을 정리하고, 로직을 명확히 하는 데 사용됩니다. 이는 문제 해결 과정에서 중요한 단계로, 전체 시간의 60~70%를 할애할 가치가 있습니다. +구현 단계는 3 ~ 40% 정도로 시간 배분을 하면 좋습니다. + +문제: 주어진 배열에서 중복된 요소를 제거하고, 고유한 요소들로만 구성된 배열을 반환하세요 + +1. 입력 배열을 하나의 집합 자료구조에 삽입합니다. +2. 집합 자료구조는 중복된 요소를 허용하지 않으므로, 자동으로 중복 요소가 제거됩니다. +3. 집합을 다시 배열로 변환하여 반환합니다. + +```pseudocode +함수 removeDuplicates(arr): + s = 빈 set 생성 + 각 element in arr에 대해 반복: + set s에 element 추가 + set s의 요소들을 리스트로 변환하여 반환 +``` +--- + +## 결론 + +- 시간 복잡도는 알고리즘 성능 측정의 중요한 척도입니다. +- 재귀 함수를 사용할 때는 공간 복잡도도 고려해야 합니다. +- 자료구조의 각 메서드의 시간 복잡도를 이해하고 정리하는 것이 중요합니다. +- 코드 개선 시 시간 복잡도를 고려하여 주요 연산 부분을 최적화해야 합니다. +- 의사코드를 통해 데이터 흐름을 정리하고 구현 전 로직을 명확히 해야 합니다. + +학습 과정에서 복사 붙여넣기가 아니라 직접 구현하고 성능 차이를 이해하는 것이 중요합니다. 이를 통해 문제 분석 후 적절한 구현 전략을 세울 수 있는 능력을 키워야 합니다. diff --git "a/\354\240\225\354\230\244\355\221\234.md" "b/\354\240\225\354\230\244\355\221\234.md" new file mode 100644 index 0000000..81a690e --- /dev/null +++ "b/\354\240\225\354\230\244\355\221\234.md" @@ -0,0 +1,27 @@ +# 코딩테스트 합격자 되기 - C++편 정오표 + +이 문서는 코딩테스트 합격자 되기- C++ 편의 정오표입니다. 독자 여러분의 정확한 이해를 돕기 위해 오류를 정정하고 업데이트합니다. 발견된 오류에 대한 정정 사항이나 추가 설명을 아래에 기록합니다. + +## 정오표 업데이트 정보 + +- **최종 업데이트**: 2025-03-28 +- **문의 및 제보**: ultrasuperrok@gmail.com으로 제보해 주시거나 깃헙에 issue로 올려주시면 감사하겠습니다. + +## 정오표 내용 + +| 페이지 | 잘못된 내용 | 정정된 내용 | 비고 | +|--------|-------------|-------------|------| +| 240 | ![image](https://github.com/dremdeveloper/codingtest_cpp/assets/131899974/628081ed-f3ab-433c-8519-ab9c7cee8377) |![image](https://github.com/dremdeveloper/codingtest_cpp/assets/131899974/cfa0b24e-8ae1-4fce-9f03-94bc524358b3)| 좀 더 직관적인 표현으로 수정 | +|359|![image](https://github.com/dremdeveloper/codingtest_cpp/assets/131899974/095f7e28-e022-45b7-8c86-9c627985827d)|![image](https://github.com/dremdeveloper/codingtest_cpp/assets/131899974/4ca4d827-e4f8-44cf-b8d9-2b4988c0b011)|오탈자 수정| +|535|![image](https://github.com/user-attachments/assets/10dbc36f-49b5-4e0f-b79c-cc02605bb2a3)|![image](https://github.com/user-attachments/assets/42b678f7-472b-41db-92e4-b584ab7e9ace)|그림 수정| +| 587 | ![image](https://github.com/user-attachments/assets/5ec175a7-c610-4c45-8d65-c450fe0e928b)|![image](https://github.com/user-attachments/assets/481412e7-cb54-4e3b-9d8d-42e6e5c62e38)|전체 데이터를 밀어내는 것이 아닙니다. 정렬을 유치한 상태에서 삽입될 위치를 찾기위해 정렬된 영역의 원소들을 오른쪽으로 하나씩 미는것입니다.| +| 82 | ![image](https://github.com/user-attachments/assets/16a81d58-cb8b-4b34-96c6-1131b7f54c1c)|![image](https://github.com/user-attachments/assets/b6faa1b8-d76a-4a28-a5c8-fc19765901d8)|소문자를 대문자로 변경| +| 595 |![image](https://github.com/user-attachments/assets/31d632fb-b6c5-49d2-915b-0683000b15f9)|![image](https://github.com/user-attachments/assets/8c27831b-c8ee-4592-8b11-fdfd4ed9c12d)|좀 더 친절하게 문구 추가| +| 598 |![image](https://github.com/user-attachments/assets/e63606b3-3143-4d05-b080-1a07a5db199a)|![image](https://github.com/user-attachments/assets/0bd50177-549e-42f1-a5d1-f5cbc45be60f)|2번째 인덱스 값 수정| +|530|유효한 답의 집합을 정의합니다.|해가 될 수 있는 잠재적 후보를 정의합니다.|오해 소지가 있는 표현 변경| +|530|정의한 집합을 그래프로 표현 합니다.|정의한 후보를 상태-공간 트리로 표현 합니다.|오해 소지가 있는 표현 변경| +|531|유효한 답의 집합을 정의합니다.|해가 될 수 있는 잠재적 후보를 정의합니다.|오해 소지가 있는 표현 변경| +|531|정의한 답의 집합을 그래프로 표현 합니다.|정의한 후보를 상태-공간 트리로 표현 합니다.|오해 소지가 있는 표현 변경| +|538|유효한 해의 집합을 정의합니다. 4행 4열의 칸이 있고 여기에 퀸을 놓을 수 있으므로 집합은 다음과 같이 표시할 수 있습니다.|해가 될 수 있는 잠재적 후보를 정의합니다. 4행 4열의 칸이 있고 여기에 퀸을 놓을 수 있으므로 아래와 같이 나타낼 수 있습니다.|오해 소지가 있는 표현 변경| +|539|앞서 본 것처럼 해의 집합을 그래프로 표현합니다.|정의한 후보를 상태-공간 트리로 표현 합니다.|오해 소지가 있는 표현 변경| +|594|![image](https://github.com/user-attachments/assets/dde3ca17-5d45-4785-8d56-1d1f9d3d7741)|![image](https://github.com/user-attachments/assets/cef3da62-8b1d-42c9-a4b8-784d1e3f77b6)|틀린 설명은 아니나, 인덱스 4에 해당 되는 노드의 걍우, 8 값을 지우고 7이되는걸로 표현해야 이해하기 쉬움| diff --git "a/\354\275\224\353\224\251\355\205\214\354\212\244\355\212\270 \355\225\251\352\262\251\354\236\220 \353\220\230\352\270\260 C++\355\216\270 - \354\261\205 \354\206\214\352\260\234.pptx" "b/\354\275\224\353\224\251\355\205\214\354\212\244\355\212\270 \355\225\251\352\262\251\354\236\220 \353\220\230\352\270\260 C++\355\216\270 - \354\261\205 \354\206\214\352\260\234.pptx" new file mode 100644 index 0000000..d210daa Binary files /dev/null and "b/\354\275\224\353\224\251\355\205\214\354\212\244\355\212\270 \355\225\251\352\262\251\354\236\220 \353\220\230\352\270\260 C++\355\216\270 - \354\261\205 \354\206\214\352\260\234.pptx" differ