해당 포스팅은 "코딩 테스트 합격자 되기 C++편" 의 책 및 강의를 보며 포스팅 한 내용입니다. 

 

 

 

[지금 무료] 코딩 테스트 합격자 되기 - C++ 강의 | dremdeveloper - 인프런

dremdeveloper | 코딩 테스트 합격을 위한 C++ 강의, 책 없이도 가능! 저자와 직접 소통 가능한 커뮤니티 제공!, [사진]여기에 문의 하세요https://open.kakao.com/o/gX0WnTCf📘 코딩 테스트 합격자 되기 - C++편

www.inflearn.com

 

 

📝내용

 

그래프(Graph)

1. 정의

노드와 간선을 이용한 비선형 자료구조

- 목적에 따라 간선의 가중치나 방향이 있을 수 있음.

 

1. 노드 : key와 value가 있음.

2. 가중치 : 노드와 노드 사이를 이어주는 무게

3. 방향 : 노드와 노드 사이의 방향 관계를 표시

4. 사이클 : 노드의 관계가 순환이 되는 현상

 

 

 

- 이차원 배열로 그래프 간의 관계, 가중치, 방향을 표시한다.

- 메모리 낭비가 심하지만, 노드사이의 간선 존재 여부를 한번에 확인할 수 있다(접근 용이)

 

- 인접리스트는 1개의 배열을 만들어 해당 key값과 관련된 노드들을 리스트로 연결한 것이다.

- 예를 들어 key값 1에 2와 3이 연결되어 있다면 1->2->3 으로 표현할 수 있다.

- 노드를 struct로 만들어 vertex, weight, next를 만들어 관리할 수 있다.

 - 간선의 개수를 Node의 개수만큼만 추가되기 때문에 인접리스트는 메모리 낭비가 없다.

 - vector<pair<int,int>> 로 구현을 할 수 있다.

 

- 특점 정점의 간선을 찾아 수정하는 경우는 인접행렬이 좋다.
- 노드의 개수가 많을 떄는 인접리스트를 써야 한다.(공간을 많이 차지하게 되므로)

 

 - 재귀로 구현, visit는 cout 출력을 통해 방문했다고 하고 출력

 

 - 깊이 우선 탐색의 동작원리, stack에 push할 때, 재귀는 함수를 호출하기 전 방문처리, 함수가 종료, 나갈때는 pop 

 

 - 루트노드부터 시작해서 엣지로 이어져있는 노드부터 방문하는 탐색으로, queue를 통해서 구현할 수 있다.

 

- 루트노드인 A를 넣고 B와 C를 바라본다.

 

 

 - B와 C를 Queue 컨테이너에 넣고, B와 C의 주변노드를 바라본다. 

 

 - B와 C를 방문하면서, B의 이웃노드인 D와 E를 Queue 컨테이너에 넣어준다.

 

 

 - D와 E를 방문하면서 이웃 노드가 있는지 확인하고 컨테이너에 더이상 노드가 없으면 너비우선탐색을 마친다.

 

 

 

 

 

- 그림과 같이 노드,엣지,가중치, 방향이 있는 그래프에서 최단경로를 찾는 알고리즘을 배운다.

- 알고리즘은 다익스트라, 벨만-포드가 있다.

- 여기서 우선순위 큐가 쓰인다고 한다.

 

 - A를 시작노드로 잡고, 다음과 같은 변수들을 선언하여 기록하며 다익스트라 알고리즘을 진행한다.

- 시작 노드를 A로 잡는다.

unordered_set<char> visited;
unordered_map<char, pair<int,char>> um; 
// um[방문노드].first == 최소비용
// um[방문노드].second == 직전노드

um[A] = {0, 'A'};

 

 - 시작노드 A 와 인접 노드인 B,C,E 노드와 해당 해당 노드간 연결되어 있는 엣지의 가중치를 확인하며 um을 수정한다.

 - 그리고, A를 방문했다고 visited 변수에 기록한다. visited.insert('A');

 - 그리고 방문하지 않은 BCDE 중에서 가장 낮은 가중치를 가진 노드를 선택한다.

 

 

- 해당 노드와 엣지로 연결되어있는 노드 C를 선택한 후에, E->C와 A->C가 어떤 게 더 작은 비용으로 갈 수 있는지 비교한다.

- 현재는 E->C 루트가 더 작은 형태가 되므로 아래와 같이 수정한다.

- 그리고 visited 변수에 E를 추가한다.

 

 

 

 - 다익스트라 알고리즘은 음의 가중치를 가지고 있는 그래프는 제대로 동작이 되질 않는다.

 - 그럴때는 벨먼-포드 알고리즘을 사용하자. 

 


 

2.  그래프의 ADT

그래프의 ADT는 자료구조의 형태가 아니기 때문에 생략하지만 나중에 찾아봤을때 알아보기 쉽게

인접리스트와 인접행렬로 표현의 형태만 저장을 해둔다.

 

 - 인접 행렬

vector<vector<int>> adjMat; //가중치가 없을때는 이진법을 이용해서 구현해준다.

//* adjMat[a][b] = c;
// a : 시작 노드
// b : 끝 노드
// c : 가중치
//
// 장점은 쉬운 구현 및 O(1)인효율적인 접근에 있고, 단점은 많은 메모리 낭비에 있다.

 

 

- 인접 리스트

unordered_map<int, vector<pair<int,int>> adjList; // 해시맵의 형태(가중치 있는 버전)
unordered_map<int, vector<int>> adjList2; // 가중치가 없을 때에는 pair가 아닌 일반 vector를 쓴다.

//* adjList[a] = b;
// a : 시작 노드
// b.first : 가중치
// c.second : 도착 노드

// 장점은 메모리낭비가 비교적 적다, 단점은 접근의 시간복잡도가 인접행렬보다 좀 더 길다.

3.  문제 풀이

   1) 깊이 우선 탐색 순회

더보기

a. 문제 설명

깊이 우선 탐색으로 모든 그래프의 노드를 순회하는 함수 Solution()을 작성하세요. 시작 노드는 문자형 start로 주어집니다. graph 배열은 [출발 노드, 도착 노드] 쌍들이 들어있는 배열입니다. 반환값은 그래프의 시작 노드부터 모든 노드를 깊이 우선 탐색으로 탐색한 경로가 순서대로 저장된 배열입니다.

 

ex) 

graph start result
(A,B),(B,C),(C,D),(D,E) 1 (A,B,C,D,E)
(A,B),(A,C),(B,D),(B,E),(C,F),(E,F) 1 (A,B,D,E,F,C)

 

b. 의사 코드

  1. 우선 주어진 graph를 인접리스트인 unordered_map 에 넣어서 관리하자.
  2. BFS는 stack(재귀)의 특성을 가졌으니, 재귀함수를 이용하자.

c. 문제 풀이

#include <vector>
#include <iostream>
#include <unordered_map>
#include <unordered_set>

using namespace std;

unordered_set<char> visit;
unordered_set<char> visit2;

void dfs(unordered_map<char, vector<char>>& graph, char start, unordered_set<char>& visit)
{
	cout << start << " ";
	visit.insert(start);

	for (auto& e : graph[start])
	{
		if (visit.find(e) == visit.end()) dfs(graph, e, visit);
	}
}

void solution(vector<pair<char, char>> graph, char start, unordered_set<char>& visit)
{
	unordered_map<char, vector<char>> um;

	for (auto& e : graph)
	{
		auto first = e.first;
		auto second = e.second;

		um[first].push_back(second);
	}

	dfs(um, start, visit);
}

int main()
{
	vector<pair<char, char>> graph = { {'A','B'},{'B','C'},{'C','D'},{'D','E'}};
	vector<pair<char, char>> graph2 = { {'A','B'},{'A','C'},{'B','D'},{'B','E'},{'C','F'} ,{'E','F'} };

	solution(graph, 'A', visit);
	cout << endl;
	solution(graph2, 'A', visit2);
}

 

d. 문제 후기

  • DFS의 기본 구현 문제이다. 보기보다 쉬워서 안보고 DFS를 구현할 수 있게 되었다.

 

   2) 너비 우선 탐색 순회

더보기

a. 문제 설명

너비 우선 탐색으로 모든 노드를 순회하는 함수 solution()을 작성하세요. 시작 노드는 정수형 start로 주어집니다. graph 배열은 [출발 노드, 도착 노드] 쌍이 들어 있는 배열입니다. 반환값은 그래프의 시작 노드부터 모든 노드를 너비 우선 탐색한 경로가 순서대로 저장된 배열입니다.

 

ex) 

graph start result
(1,2),(1,3),(2,4),(2,5),(3,6),(3,7),(4,8),(5,8),(6,9),(7,9) 1 (1,2,3,4,5,6,7,8,9)
(0,1),(1,2),(2,3),(3,4),(4,5),(5,0) 1 (1,2,3,4,5,0)

 

b. 의사 코드

  1. 우선 주어진 graph를 인접리스트인 unordered_map 에 넣어서 관리하자.
  2. DFS는 Queue의 특성을 가졌으니, Queue를 잘 이용하는데, 방문 시점을 push로 할지 pop으로 할지 잘 구분하자.

c. 문제 풀이

#include <bits/stdc++.h>

using namespace std;

void bfs(unordered_map<int, vector<int>>& um, int start)
{
	queue<int> q;
	unordered_set<int> visited;
	
	q.push(start);
	visited.insert(start);
	cout << start << " ";

	while (!q.empty())
	{
		auto front = q.front();
		q.pop();

		for (const auto& e : um[front])
		{
			if (visited.find(e) == visited.end())
			{
				q.push(e);
				visited.insert(e);
				cout << e << " ";
			}
		}
	}
}

void solution(vector<pair<int, int>>& graph, int start)
{
	unordered_map<int, vector<int>> um;

	for (const auto& e : graph)
	{
		int first = e.first;
		int second = e.second;

		um[first].push_back(second);
	}

	bfs(um, 1);
}

int main()
{
	vector<pair<int, int>> graph = { {1,2},{1,3},{2,4},{2,5},{3,6},{3,7},{4,8},{5,8},{6,9},{7,9} };
	vector<pair<int, int>> graph2 = { {0,1},{1,2},{2,3},{3,4},{4,5},{5,0}};

	solution(graph, 1);
	cout << endl;
	solution(graph2, 0);
	
}

 

d. 문제 후기

  • BFS의 기본 구현 문제이다. 여기서 가중치가 붙게된다면 우선순위 큐(최소 힙)를 이용하여 최단 경로를 구하는 알고리즘을 만들 수 있다.

 

  3) 다익스트라 알고리즘 구현

더보기

a. 문제 설명

주어진 그래프와 시작 노드를 이용하여 다익스트라 알고리즘을 구현하는 solution()함수를 작성하세요. 시작 노드 start, 노드의 개수 numNode, [시작 노드, 도착 노드, 가중치] 형태로 간선 정보를 담은 배열 edges가 인수로 주어집니다. edges에 [2,1,9] 가 있다면 시작 노드 2에서 도착 노드 1까지 가중치가 9인 간선이 있다는 뜻입니다. 시작 노드 start 부터 각 노드까지 최소 비용을 담은 벡터를 반환하는 solution() 함수를 구현하세요.

 

ex) 

start numNodes edges result
0 3 (0,1,9),(0,2,3),(1,0,5),(2,1,1) (0,4,3)
0 4 (0,1,1),(1,2,5),(2,3,1) (0,1,6,7)

 

b. 의사 코드

  1. 일단 각 노드를 연결하는 그림을 그려보자.

c. 문제 풀이

#include <vector>
#include <tuple>

using namespace std;

const int INF = 99999;
const int MAX_NODES = 100;
int graph[MAX_NODES][MAX_NODES];
bool visited[MAX_NODES];

vector<int> solution(int start, int numNodes, const vector<tuple<int, int, int>> edges) {
  //❶ 그래프 및 방문 여부 초기화
  for (int i = 0; i < MAX_NODES; ++i) {
    fill_n(graph[i], MAX_NODES, INF);
    visited[i] = false;
  }
  
  //❷ 입력받은 간선 정보를 그래프로 표현
  for (const auto& [from, to, weight] : edges) {
    graph[from][to] = weight;
  }

  //❸ 시작 노드를 제외한 모든 노드의 최소 비용을 INF로 초기화
  vector<int> distances(numNodes, INF);
  distances[start] = 0;

  for (int i = 0; i < numNodes - 1; ++i) {
    int minDistance = INF;
    int closestNode = -1;

    //❹ 최소 거리 노드 찾기
    for (int j = 0; j < numNodes; ++j) {
      if (!visited[j] && distances[j] < minDistance) {
        minDistance = distances[j];
        closestNode = j;
      }
    }

    //❺ 찾은 노드를 방문 처리
    visited[closestNode] = true;

    //❻ 인접 노드에 대한 거리 업데이트
    for (int j = 0; j < numNodes; ++j) {
      int newDistance = distances[closestNode] + graph[closestNode][j];
      if (!visited[j] && graph[closestNode][j] != INF && newDistance < distances[j]) {
        distances[j] = newDistance;
      }
    }
  }

  return distances;
}


//아래 코드는 테스트 코드 입니다.
#include <iterator>
#include <iostream>

using namespace std;


void print(vector<int> vec)
{
  copy(vec.begin(), vec.end(), std::ostream_iterator<int>(cout, " "));
  cout << endl;
    
}

int main()
{
  
  print(solution(0, 3, {{0, 1, 9},{0, 2, 3},{1, 0, 5},{2, 1, 1}})); //출력값 : 0 4 3
  print(solution(0, 4, {{0, 1, 1}, {1, 2, 5}, {2, 3, 1}})); //출력값 : 0 1 6 7
  return 0;
}

 

d. 문제 후기

  • BFS/DFS는 자세히 실습해보질 않아서 많이 어려움을 느끼는 것 같다.

 

 4) 미로 탈출 <-- 12.5% 오답 ⭐⭐⭐⭐⭐

 

더보기

a. 문제 설명

문제 참조

 

b. 의사 코드

  1. 처음에는 DFS를 이용해 문제를 풀려고 했으나 잘 풀리지 않았다.
  2. 이후로 BFS로 문제를 풀어볼려고 하였으나 잘 되지 않았다.

c. 문제 풀이

#include <string>
#include <vector>
#include <unordered_map>
#include <unordered_set>
#include <queue>

using namespace std;


unordered_map<int, vector<pair<char, int>>> um;
unordered_map<char, int> um2;
unordered_set<int> visit;


int bfs(int start, int end)
{
    int cnt = -1;
    queue<int> q;
    
    int stack = 0;
    q.push(start);
    while(!q.empty())
    {
        int front = q.front();
        q.pop();
        visit.insert(front);
        cnt++;
        
        if(front == end) break;
        
        int size = um[front].size();
        for(int i = 0; i < size; i++)
        {
            if(visit.find(um[front][i].second) == visit.end())
            {
                q.push(um[front][i].second);
            }
        }
    }    
    
    //visit.clear();
    
    return cnt;
}

int solution(vector<string> maps) {
    int answer = 0;
    
    int row = maps.size();
    int col = maps[0].size();
    
    for(int i = 0; i < row; i++)
    {
        for(int j = 0; j <col; j++)
        {
            if(maps[i][j] == 'X') continue;
            
            int cur = i*row+j;
            int up = (i-1)*row+j;
            int down = (i+1)*row+j;
            int left = i*row + j - 1;
            int right = i*row + j + 1;
            
            if(maps[i][j] == 'S') um2['S'] = cur;
            if(maps[i][j] == 'L') um2['L'] = cur;
            if(maps[i][j] == 'E') um2['E'] = cur;
            
            if(i-1 >= 0 && maps[i-1][j] != 'X') um[cur].push_back({maps[i-1][j],up});
            if(i+1 < row && maps[i+1][j] != 'X') um[cur].push_back({maps[i+1][j],down});
            if(j-1 >= 0 && maps[i][j-1] != 'X') um[cur].push_back({maps[i][j-1],left});
            if(j+1 < col && maps[i][j+1] != 'X') um[cur].push_back({maps[i][j+1],right});
        }
    }
    
    if(um[um2['S']].empty()) return -1;
    answer = bfs(um2['S'], um2['L']) + bfs(um2['L'], um2['E']);    
    
    
    return answer;
}

 

d. 문제 후기

  • 해시맵과 해시셋이 능사가 아니라는 것을 알았다. 그래프 유형의 문제는 약하다는것을 느꼈었고, 고민끝에 정답을 보고 한줄한줄 분석하여 내것으로 암기하여 풀어보려고 한다.
  • 정답 풀이
#include <queue>
#include <string>
#include <vector>

using namespace std;

struct Point //S나 L나 E의 특수한 공간의 위치를 반환
{
	int y, x, cnt;
};

int dy[4] = { -1,0,1,0 }; // 상하의 증감값을 표현
int dx[4] = { 0, 1, 0, -1 }; // 좌우의 증감값을 표현
int n, m; // row와 col을 표현

bool isWithinRange(int y, int x) { return 0 <= y && y < n && 0 <= x && x < m; }//21. 상하좌우로 이동했을 시, 배열의 range에 들어가는지 검사

Point findStartPoint(char start, vector<string>& maps)
{
	for (int i = 0; i < n; i++) // 7.maps의 row만큼 반복
	{
		for (int j = 0; j < m; j++) // 8.maps의 col 만큼 반복
		{
			if (maps[i][j] == start) // 9.start를 검출했다면
			{
				return { i,j,0 }; //10.start의 위치를 반환
			}
		}
	}
}

int bfs(char start, char end, vector<string>& maps)
{
	bool visited[101][101] = { false }; // 4. 방문한 곳들을 이력으로 남기는 배열
	queue<Point> q; // 5. bfs를 진행하기 위한 queue 선언

	q.push(findStartPoint(start, maps)); // 6. queue에 start 위치를 push해주기

	while (!q.empty()) //11. q가 다 빌때까지 반복
	{
		Point current = q.front(); //12. q의 front를 기준으로 최근 포인트를 저장
		q.pop(); //13. current로 저장을 했으니 빼주기

		if (maps[current.y][current.x] == end) //14. 현재 위치가 도착점이면
		{
			return current.cnt; //15. 현재 위치의 cnt를 반환
		}

		for (int i = 0; i < 4; i++) // 16. 4번 반복? 아...! 상하좌우를 for문으로 표현
		{
			int ny = current.y + dy[i]; // 17. 현재위치의 y좌표와 증분y축의 값을 더해 위와 아래를 ny에 넣기
			int nx = current.x + dx[i]; // 18. 현재위치의 x좌표와 증분x축의 값을 더해 좌측과 우측을 nx에 넣기
			// 19. i를 순서대로 하면 위, 오른쪽, 아래, 왼쪽

			if (isWithinRange(ny, nx) && !visited[ny][nx] && maps[ny][nx] != 'X') // 20. 배열 내부에 들어 있는 값인지, 방문한 적이 없는지,  X가 아닌지 다 조건에 적합하다면, q에 푸쉬한다.
			{
				q.push({ ny,nx,current.cnt + 1 }); // 22. count를 1 증가시킨 채 이동한 좌표를 푸쉬해준다.
				visited[ny][nx] = true; // 23. 방문했다는 위치를 갱신
			}
		}
	}
	return -1; // 24. E를 찾지 못했다면 마지막 위치 반환
}

int solution(vector<string> maps)
{
	n = maps.size(); // 1. row
	m = maps[0].size(); // 2. col

	int distanceToL = bfs('S', 'L', maps); // 3. Start 부터 End까지 가는 거리를 cnt로 환산했을 때, bfs 함수를 이용
	if (distanceToL == -1) return -1; // 25. bfs가 -1이 되어 돌아왔다면, 목표를 찾지 못했다는 뜻이므로 -1을 리턴

	int distanceToE = bfs('L', 'E', maps); // 26. bfs 함수를 다시 목표만 바꾸어 호출
	return distanceToE == -1 ? -1 : distanceToL + distanceToE; // 27. -1이 돌아왔다면 -1을, 제대로 카운팅 되었다면 두개의 변수를 합침
}

 

 5) 게임 맵 최단거리 <-- 효율성 전체 오답 ⭐⭐⭐⭐⭐

 

더보기

a. 문제 설명

문제 참조

 

b. 의사 코드

  1. 처음에 고민했던 경험을 바탕으로 BFS를 사용하여 풀기로 하였다.

c. 문제 풀이

#include <vector>
#include <queue>

using namespace std;

struct Node
{
    int x;
    int y;
    int cnt;
};

int visit[100][100];

int solution(vector<vector<int>> maps)
{
    int answer = -1;
    int row = maps.size();
    int col = maps[0].size();
    queue<Node> q;
    q.push({0,0,1}); //시작 위치
    
    while(!q.empty())
    {
        Node cur = q.front();
        q.pop();
        visit[cur.y][cur.x]++;
        
        if(cur.x == col-1 && cur.y == row-1)
        {
            answer = cur.cnt;
            return cur.cnt;
            break;
        }
        
        for(int i = 0; i < 4; i++) // 0은 up, 1은 down, 2는 left, 3은 right
        {
            if(i == 0 && cur.y-1 >= 0 && maps[cur.y-1][cur.x] && visit[cur.y-1][cur.x] == 0) q.push({cur.x,cur.y-1,cur.cnt+1});
            if(i == 1 && cur.y+1 < row && maps[cur.y+1][cur.x] && visit[cur.y+1][cur.x] == 0) q.push({cur.x,cur.y+1,cur.cnt+1});
            if(i == 2 && cur.x-1 >= 0 && maps[cur.y][cur.x-1] && visit[cur.y][cur.x-1] == 0) q.push({cur.x-1,cur.y,cur.cnt+1});
            if(i == 3 && cur.x+1 < col && maps[cur.y][cur.x+1] && visit[cur.y][cur.x+1] == 0) q.push({cur.x+1,cur.y,cur.cnt+1});
        }
    }
    
    return answer;
}

 

d. 문제 후기

  • 정답은 모두 맞았으나 효율성체크에서 모두 오답이 떴다.

 

 

 

 

 

 

해당 포스팅은 "코딩 테스트 합격자 되기 C++편" 의 책 및 강의를 보며 포스팅 한 내용입니다. 

 

 

 

[지금 무료] 코딩 테스트 합격자 되기 - C++ 강의 | dremdeveloper - 인프런

dremdeveloper | 코딩 테스트 합격을 위한 C++ 강의, 책 없이도 가능! 저자와 직접 소통 가능한 커뮤니티 제공!, [사진]여기에 문의 하세요https://open.kakao.com/o/gX0WnTCf📘 코딩 테스트 합격자 되기 - C++편

www.inflearn.com

 

 

 

 

💬주요 키워드

집합 : 코테에서의 집합이란, 중복되지 않은 요소를 트리구조로 묶어놓는 것을 말하며, 보통 배열로 많이 표현하는데 index를 자기 자신, element로 부모를 가리켜 노드간의 관계를 표현하였다.

 

상호배타적 집합 : 집합의 구조에서 서로 이어지지 않은 노드가 있다면 서로 다른 집합이라고 판단하여, 상호배타적 집합이라고 표현한다. 코테에서는 대부분 상호배타적 집합이 나온다고 한다.

 

루트노드 : 집합을 대표하는 원소를 부모노드라고 하며, index와 element가 같다면 해당 집합을 대표하는 루트노드 라고 표현한다.

 

유니온-파인드 알고리즘 : 유니온 - 파인드 알고리즘이란, 상호배타적 집합에서 서로 다른 집합들의 노드를 이어 같은 집합으로 만드는 것이 유니온, 하나의 노드에 이어져있는 루트 노드를 찾는 것을 파인드 알고리즘 이라고 한다.

 

경로 압축 알고리즘 : 파인드 알고리즘을 사용하는 와중, 시간복잡도가 최대 O(n)이기 때문에 이 문제를 방지하기 위하여, 모든 노드의 부모 노드를 루트노드로 만드는 것을 경로 압축 알고리즘이라고 한다.

 

랭크 기반 알고리즘 : 유니온 알고리즘을 사용 할 때, 말단노드와 루트노드의 깊이가 달라지는 현상을 방지하기 위하여 집합간의 레벨을 비교하여 레벨이 낮은 쪽이 높은 쪽에 붙을 수 있도록 만들어 주는 알고리즘이다.

 

📝내용

 

집합(DisjointSet)

1. 정의

집합이란 트리구조에서 노드간의 연결관계를 두고 집합이라고 표현한다.

수학에서의 집합과 비슷하면서 다른데, 서로 다른 트리에서 같은 노드의 값을 가지고 있다면 교집합으로 표현할 수 있고

합집합과 같이 서로 다른 두개의 트리에 연결관계를 이어줌으로서 각각 두개의 트리를 하나의 트리로 이어줄 수 있다.

 

일단, 코테에서의 집합은 교집합은 없다. 라고 생각하면 되고 이러한 교집합이 없는 관계를 상호-배타적 집합(disjointSet) 이라고 한다.

또한, 상호배타적 집합의 두 트리를 합치는 과정을 Union이라고 한다.

마지막으로, 두 개의 트리에는 각각 루트노드의 개념이 있는데 이러한 루트 노드를 찾는 과정을 Find라고 한다.

 

이정도만 알고 있으면, 어느정도 유니온-파인드 알고리즘을 구현할 수 있을듯 하다.

 

find함수에는 각 노드별 부모노드를 루트노드로 통일하는 경로 압축 알고리즘이 적용되고,

union함수에는 랭크(높이)가 낮은 트리가 높은 트리에 붙는 랭크 기반 알고리즘이 적용된다.


 

2.  집합의 ADT

   1) find : 트리의 루트노드를 확인

   2) merge : 원래는 union이나, std에 다른 내장함수가 있으므로 merge로 변경

   3) isCycle : 최단경로를 구하는데 필요한 요소로서, 트리 내부에서 노드가 순환하는 현상이 있는지 확인하는 용도이다.

 

 

   ADT를 바탕으로 구현한 코드

class DisjointSet
{
    private:
    vector<int> parent, rank;
    
    public:
    DisjointSet(int size) : parent(size, -1), rank(size, 0) {}
    
    int find(int node)
    {
        if(parent[node] == -1) return node;
        return parent[node] = find(parent[node]);
    }
    
    void merge(int node1, int node2)
    {
        int root1 = find(node1);
        int root2 = find(node2);
        
        if(root1 != root2)
        {
            if(rank[root1] > rank[root2])
            {
                parent[root2] = root1;
            }
            else if(rank[root1] < rank[root2])
            {
                parent[root1] = root2;
            }
            else
            {
                parent[root2] = root1;
                rank[root1]++;
            }
        }
    }
    
    bool isCycle(int node1, int node2) { return find(node1) == find(node2);}
};

 


3.  문제 풀이

   1) 유니온-파인드 알고리즘 구현하기(연습문제)

더보기
더보기

a. 문제 설명

상호배타적 집합을 표현하고 관리하는데 다음 두 연산이 필요합니다.

  • union(x, y) : x와 y가 속한 두 집합을 합칩니다.
  • find(x) : x가 속한 집합의 대표 원소를 찾습니다.

operations라는 배열은 수행할 연산을 의미합니다. 연산의 종류는 2개입니다.

  • ['u',1,2]는 노드1과 노드2에 대해 union 연산을 수행
  • ['f',1,2,]는 노드1과 노드2의 루트노드가 같은지 find연산으로 확인해서 같으면 true, 다르면 false를 반환

경로압축과 랭크 기반 합치기를 활용해서 유니온-파인드 알고리즘을 구현해주세요. operations배열에 있는 연산을 모두 수행한 후 find 연산 결과를 순서에 맞춰 벡터에 담아 반환하는 solution() 함수를 구현해주세요.

 

ex) 

k operations result
3 [u, 0, 1], [u,1,2], [f,0,2] true
4 [u,0,1] , [u,2,3],[f,0,1],[f,0,2] true,false

 

b. 의사 코드

  1. 우선 집합은 배열이라고 했으니, 배열을 통해 구현해보자.
  2. 모든 노드들은 index 가 element가 되게 하여 수많은 노드방울을 만든다.
  3. 각 2개의 노드를 받는 union함수를 만들어 트리를 구성할 수 있도록 한다.
  4. 트리를 구성하면 find()함수를 만들어 트리의 부모노드를 만든다.
  5. 경로 압축 알고리즘과 랭크기반 알고리즘을 적용시킨다.

c. 문제 풀이

#include <bits\stdc++.h>
#include <iostream>

using namespace std;

int find(int x, vector<int>& disjointset)
{
	if (disjointset[x] == x) return x;
	return disjointset[x] = find(disjointset[x], disjointset);
}

void merge(int x, int y, vector<int>& disjointset, vector<int>& rank)
{
	int rootX = find(x, disjointset);
	int rootY = find(y, disjointset);

	if (rootX == rootY) return; // 2개의 루트노드가 같을 경우는 1개의 트리라는 뜻으로 이미 합쳐져있음.


	if (rank[rootX] > rank[rootY]) disjointset[rootX] = rootY;
	else if (rank[rootX < rank[rootY]]) disjointset[rootY] = rootX;
	else
	{
		disjointset[rootY] = rootX;
		rank[rootX]++;
	}
}

vector<int> solution(int k, vector<vector<char>> operations)
{
	vector<int> answer;

	vector<int> disjointset;
	vector<int> rank;
	for (int i = 0; i < k; i++)
	{
		disjointset.push_back(i);
		rank.push_back(0);
	}

	for (const auto& e : operations)
	{
		if (e[0] == 'u') merge(e[1] - '0', e[2] - '0', disjointset, rank);
		else
		{
			int rootX = find(e[1] - '0', disjointset);
			int rootY = find(e[2] - '0', disjointset);
			answer.push_back(rootX == rootY);
		}
	}

	return answer;
}

int main()
{
	vector<vector<char>> operations;
	operations.push_back({ 'u', '0', '1' });
	operations.push_back({ 'u', '1', '2' });
	operations.push_back({ 'f', '0', '2' });
	vector<int> result = solution(3, operations);

	for (auto e : result) cout << e << " ";

	cout << endl;

	vector<vector<char>> operations2;
	operations2.push_back({ 'u', '0', '1' });
	operations2.push_back({ 'u', '2', '3' });
	operations2.push_back({ 'f', '0', '1' });
	operations2.push_back({ 'f', '0', '2' });
	vector<int> result2 = solution(4, operations2);

	for (auto f : result2) cout << f << " ";

	return 0;
}

 

d. 문제 후기

  • 시간복잡도 O(n) : operations에 주어진 명령만큼만 실행하기 때문에 1번 순회하는 n의 시간복잡도를 갖는다.
  • 이제는 구현에 꽤나 자신감이 붙어, 맨땅에서도 이론만 들어서 구현할 수 있게 된 것 같다.

 

   2) 폰켓몬(링크)

더보기
더보기

a. 문제 설명

문제 참조

 

b. 의사 코드

  1. 우선 unordered_map을 이용하여 종류가 몇개인지 세는 용도로 해시를 등록해준다.
  2. 그리고 maxnum이라는 함수를 만들어서, 문제에 주어진 폰켓몬 N/2를 가져가도록 변수를 넣어준다.
  3. 마지막으로 unordered_map과 maxnum중 작은 것을 return해준다.

c. 문제 풀이

#include <vector>
#include <bits/stdc++.h>
using namespace std;

int solution(vector<int> nums)
{
    int answer = 0;
    int maxnum = nums.size()/2;
    unordered_map<int,int> um;
    
    for(int i = 0; i < nums.size(); i++)
    {
        um[nums[i]] = i;
    }
    
    if(um.size() < maxnum)//최댓값과 숫자의 종류 중 누가 더 큰지
    {
        return um.size();
    }
    else
    {
        return maxnum;
    }
        
    
    return answer;
}

 

d. 문제 후기

  • 시간복잡도 O(N) : nums의 개수만큼만 중복되지 않도록 unordered_set이나 unordered_map에 넣어주기 때문에, nums를 넣어주는 순회의 수만큼 시간복잡도가 구성된다.
  • unordered_map이 시간복잡도상으로 좋은 성능을 보여주기 때문에 기회만 된다면 자주 써먹는 습관을 들여야 겠다.

 

   3) 섬 연결하기(링크) <-- 12.5% 오답 ⭐⭐⭐⭐⭐

더보기
더보기

a. 문제 설명

링크 참조

 

 

b. 의사 코드

  1. 문제를 읽어보면 연결할 수 없는 섬은 존재하지 않으므로, 작은 코스트 먼저 섬끼리 이을 수 있도록 코스트 오름차순으로 정렬한다. 
  2. 정렬된 costs를 순회하며 2개의 노드를 unordered_map으로 체크하여 n번 언급될 경우 break하여 그동안 순회하며 모은 cost를 더한 값을 리턴한다.

c. 문제 풀이

#include <bits/stdc++.h>

using namespace std;

bool rule(vector<int> a, vector<int> b)
{
    return a[2] < b[2];
}

int solution(int n, vector<vector<int>> costs) 
{
    int answer = 0;
    unordered_map<int,int> um_cnt;
    sort(costs.begin(), costs.end(), rule);

    for(const auto& e : costs)
    {
        um_cnt[e[0]]++;
        um_cnt[e[1]]++;
        answer += e[2];
        
        if(um_cnt.size() == n) break;
    }
    return answer;
}

 

d. 문제 후기

  • 결국에는 트리의 사이클 문제에 대해 확인해야 하므로, 사이클과 최대신장트리라는 개념을 알아야 풀 수 있는 문제였다. 사이클이 걸리는지 확인하기 위해 결국, disjointSet을 구현하며 풀어야 하는 문제로 보인다.
  • 정답
#include <algorithm>
#include <string>
#include <vector>

using namespace std;

class DisjointSet
{
    private:
    vector<int> parent, rank;
    
    public:
    DisjointSet(int size) : parent(size, -1), rank(size, 0) {}
    
    int find(int node)
    {
        if(parent[node] == -1) return node;
        return parent[node] = find(parent[node]);
    }
    
    void merge(int node1, int node2)
    {
        int root1 = find(node1);
        int root2 = find(node2);
        
        if(root1 != root2)
        {
            if(rank[root1] > rank[root2])
            {
                parent[root2] = root1;
            }
            else if(rank[root1] < rank[root2])
            {
                parent[root1] = root2;
            }
            else
            {
                parent[root2] = root1;
                rank[root1]++;
            }
        }
    }
    
    bool isCycle(int node1, int node2) { return find(node1) == find(node2);}
};

int solution(int n, vector<vector<int>> costs) 
{
    sort(costs.begin(), costs.end(), [](const vector<int> &a, const vector<int> &b) {return a[2] < b[2];});
    
    DisjointSet disjointSet(n);
    int totalCost = 0;
    
    for(const auto& e : costs)
    {
        int cost = e[2];
        int node1 = e[0];
        int node2 = e[1];
        
        if(!disjointSet.isCycle(node1,node2))
        {
            disjointSet.merge(node1,node2);
            totalCost += cost;
        }
    }
    
    return totalCost;
}

 

 

 

 

 

해당 포스팅은 "코딩 테스트 합격자 되기 C++편" 의 책 및 강의를 보며 포스팅 한 내용입니다. 

 

 

 

[지금 무료] 코딩 테스트 합격자 되기 - C++ 강의 | dremdeveloper - 인프런

dremdeveloper | 코딩 테스트 합격을 위한 C++ 강의, 책 없이도 가능! 저자와 직접 소통 가능한 커뮤니티 제공!, [사진]여기에 문의 하세요https://open.kakao.com/o/gX0WnTCf📘 코딩 테스트 합격자 되기 - C++편

www.inflearn.com

 

 

 

 

💬주요 키워드

이진 트리 : 트리는 뿌리와 잎을 가진 나무를 상상하며 만들어낸 자료구조이며, 이진 트리는 노드라는 Element가 있으며 최대 두 개의 또다른 자식개념의 노드를 가질 수 있는 것이 특징이다.

 

노드 : 노드는 배열의 Element와 비슷한 역할을 하지만 트리구조는 부모와 자식이라는 관계성이 추가되기 때문에 Element에서 부모, 자식의 주소정보를 추가로 가진다. 

 

엣지 : 엣지는 간선이라고도 불리고 노드와 노드 사이를 이어주는 선의 개념이며, 노드와 엣지를 이용해 노드의 관계를 표현한다. 보통 이진 트리에서는 부모관계를 표현한다.

 

차수 : 차수는 degree 라고도 불리며 자식 노드가 2개 있으면 2차수, 1개 있으면 1차수라고 불린다.

 

높이 : 높이는 레벨이라고도 하며 노드의 상하관계에 따라 형성되는 노드의 높이가 되며 루트노드와 리프노드 사이의 엣지가 몇개가 있는지에 따라 결정된다.

 

루트 노드 : 트리의 최상위 노드로서, 트리의 최상단에 있는 시작위치 라고 보면 된다.

 

리프 노드 : 말단 노드라고도 하며, 트리의 최하단에 있는 노드들이라고 보면 된다.

 

자식 노드  : 노드의 하단에 뻗어나가는 엣지를 통해 연결된 노드들을 말하며, 보통 이진트리에는 2개의 자식노드까지 있을 수 있다.

 

서브 트리 : 자식노드를 통해 연결되있는 또다른 자식노드들과 통틀어 서브 트리 라고 한다.

 

포화 이진트리 : 이진트리의 최대 노드의 개수는 높이n에 따라 2^n-1개까지 생성할 수 있는데, 이 때, 최대 노드의 개수까지 늘어난 트리를 포화이진트리라고 한다.

 

완전 이진트리 : 이진트리에서 노드가 새로 생성될 때 노드에 붙어있는 순서가 좌측부터 정렬되어 붙어있다면 완전 이진트리라고 한다.

 

📝내용

 

이진트리(Binary Tree)

1. 정의

이진트리는 요소(Element)를 가지고 있는 노드와 요소와 요소 사이를 이어주는 엣지로 이루어져 있는 자료구조를 의미하며, 이 때 하나의 노드에는 2개의 자식노드를 가질 수 있다는 것이 주요 규칙인 자료형을 의미한다.

또한, 이진트리의 구조는 배열로도 손쉽게 표현할 수 있다.

 

또한 노드를 이동, 방문(Visit)한다는 개념을 가지고 있어, 노드에서 다른노드로 이동-> 현재 머물고 있는 노드를 방문 이라는 개념을 가지고 있다.

 

동작 방식은 링크 참조

https://www.youtube.com/watch?v=_ITudD2Cz9M&ab_channel=VisualHow

 

또한, 이진트리에는 다른 자료구조와 같이 모든 요소(Element)에 접근할 수 있는 순회의 방식이 여러가지가 있는데 크게

 

 

전위 순회 : 현재노드 방문 -> 좌측 자식노드로 이동 -> 좌측 자식노드가 없다면 우측 자식노드로 이동 -> 우측 자식노드가 없다면 부모노드로 되돌아가기

중위 순회 : 좌측 자식노드로 이동 -> 현재노드 방문 -> 좌측 자식노드가 없다면 우측 자식노드로 이동 -> 우측 자식노드가 없다면 부모노드로 되돌아가기 

후위 순회 : 좌측 자식노드로 이동 -> 좌측 자식노드가 없다면 우측 자식노드로 이동 ->  현재노드 방문 ->  우측 자식노드가 없다면 부모노드로 되돌아가기

 

가 있다.


 

2.  트리의 ADT

   1) empty : 트리의 루트노드가 비어있는지 확인

   2) sum : 트리의 전체노드 합을 구하기

   3) height : 트리의 높이 구하기

   4) visit : 트리의 방문,  여기서는 간단한 cout << node->item; 으로 대체

   5) clear : 변수의 수명이 다해 소멸자가 발동할 시, 동적할당을 해제하기 위한 노드의 삭제 구현

   6) PreOrder : 전위순회

   7) InOrder : 중위순회

   8) PostOrder : 후위순회

   9) LevelOrder : 높이의 좌측 순서대로 순회

   

보통 배열로 많이 구현하지만, 시각적으로 보았을 때에는 양방향연결리스트(Double Linked List)의 형태로 설명하기 때문에, 해당 형태로 구현해보았습니다.

배열로 구현할 때에는

부모노드 접근 : 현재 노드 / 2
자식노드 접근 : 현재 노드 * 2 , 현재 노드 *2 + 1

이라는 규칙을 통해 부모와 자식노드에 접근할 수 있다. 

 

 

   ADT를 바탕으로 구현한 코드(양방향연결리스트)

#include<bits/stdc++.h>

using namespace std;

template<typename T>
class BT
{
public:
	struct Node
	{
		T item = T();
		Node* left = nullptr; // Left child
		Node* right = nullptr; // Right child

		//Node* parents = nullptr; // parents

	};

	BT() {}

	BT(Node* root)
	{
		root_ = root;
	}

	bool empty()
	{
		return root_ == nullptr;
	}

	void visit() { return visit(root_); }
	void visit(Node* node)
	{
		using namespace std;
		cout << node->item << " "; // 수행하고 싶은 작업 구현(여기서는 출력)
	}

	int sum()
	{
		return sum(root_);
	}

	int sum(Node* node)
	{
		//* Doing
		if (!node) return 0;
		return node->item + sum(node->left) + sum(node->right); // TODO:
	}
	
	int height()
	{
		return height(root_);
	}

	int height(Node* node)
	{
		//* Doing
		if (!node) return 0;
		return 1 + std::max(height(node->left), height(node->right)); // TODO:
	}

	~BT()
	{
		DeleteTree(root_);
	}

	void DeleteTree(Node* node)
	{
		if (node)
		{
			DeleteTree(node->left);
			DeleteTree(node->right);
			delete node;
		}
	}

	void PreOrder() { PreOrder(root_); }
	void PreOrder(Node* node)
	{
		// TODO:
		if (node)
		{
			visit(node);
			PreOrder(node->left);
			PreOrder(node->right);
		}
	};

	void InOrder() { InOrder(root_); }
	void InOrder(Node* node)
	{
		// TODO:
		if (node)
		{
			InOrder(node->left);
			visit(node);
			InOrder(node->right);
		}
	}

	void PostOrder() { PostOrder(root_); }
	void PostOrder(Node* node)
	{
		// TODO:
		if (node)
		{
			PostOrder(node->left);
			PostOrder(node->right);
			visit(node);
		}
	}

	void LevelOrder()
	{
		std::queue<Node*> q;
		Node* current = root_; //루트에서
		while (current)
		{
			visit(current); // 루트 방문
			if (current->left) q.push(current->left); // 왼쪽 자식노드 푸시
			if (current->right) q.push(current->right); // 오른쪽 자식노드 푸시
			if (q.empty()) return; //q가 비었다면 리턴
			current = q.front(); //왼쪽 자식노드를 커런트로 만들고
			q.pop(); // 큐에서 빼준다.
			// TODO:
		}
	}

protected:
	Node* root_ = nullptr;
	//Node* last_ = nullptr; // complete Binary Tree에서 필요한 요소
};

int main()
{

	using Node = BT<int>::Node;

	Node* n1 = new Node{ 1, nullptr, nullptr }; // 물결괄호 초기값 나열 (생성자 아님)
	Node* n2 = new Node{ 2, n1, nullptr };
	Node* n3 = new Node{ 3, nullptr, nullptr };
	Node* n4 = new Node{ 4, nullptr, nullptr };
	Node* n5 = new Node{ 5, nullptr, n4 };
	Node* n6 = new Node{ 6, n2, n5 };

	n1->right = n3; // <- 연결관계 변경

	BT<int> tree(n6); // <- n6의 주소를 root node로 

	cout << "Empty: " << tree.empty() << endl;//Empty: 1 
	cout << "Sum: " << tree.sum() << endl;//Sum: 21
	cout << "Height: " << tree.height() << endl;//Height : 4

	{	//Visit: 6
		cout << "Visit: ";
		tree.visit();
		cout << endl;
	}

	// Tree traversal methods

	cout << "Preorder" << endl; // 6 2 1 3 5 4
	tree.PreOrder();
	cout << endl;

	cout << "Inorder" << endl; // 1 3 2 6 5 4
	tree.InOrder();
	cout << endl;

	cout << "Postorder" << endl; // 3 1 2 4 5 6
	tree.PostOrder();
	cout << endl;

	cout << "LevelOrder" << endl; // 6 2 5 1 4 3
	tree.LevelOrder();
	cout << endl;
}

 

 


3.  문제 풀이

   1) 트리 순회(연습문제)

더보기
더보기

a. 문제 설명

이진트리를 표현한 배열 nodes를 인수로 받습니다. 예를 들어서 nodes가 [1,2,3,4,5,6,7]이면 다음과 같은 트리를 표현한 것 입니다. 해당 이진트리에 대하여 전위,중위,후위 순회 결과를 각각 순서대로 문자열로 담은 배열을 반환하는 solution()함수를 구현하세요.

 

ex)

arr result
[1,2,3,4,5,6,7] [1,2,4,5,3,6,7] , [4,2,5,1,6,3,7] , [4,5,2,6,7,3,1]

 

b. 의사 코드

  1. 전위 순회는 현재 노드를 방문, 좌측노드 이동,좌측노드가 없으면 우측노드 이동으로 구현한다.
  2. 중위 순회는 좌측노드 이동, 좌측노드 없다면 현재노드를 방문, 우측노드로 이동으로 구현한다.
  3. 후위 순회는 좌측노드로 이동, 좌측노드가 없다면 우측노드로 이동, 좌측/우측도 없다면 현재노드 방문, 으로 구현한다.

c. 문제 풀이

#include <bits/stdc++.h>

using namespace std;

void forwardtraversal(vector<int>& arr, int start)
{
    if(start < arr.size())
    {
        cout << arr[start] << " ";
        
        forwardtraversal(arr, start*2);
    
        forwardtraversal(arr, start*2+1);
    }
}

int main()
{
    vector<int> arr = {0,1,2,3,4,5,6,7};
    
    forwardtraversal(arr,1);
    
    return 0;
}

 

d. 문제 후기

  • 시간복잡도 O(3n) : 각 원소들을 한번씩 순회하는 과정이 3번 일 어나기 때문에 3n의 시간복잡도를 가짐.
  • 트리는 비교적 재귀를 이용하는 문제들이 많은듯 하다.

 

   2) 이진 탐색 트리(연습문제)

더보기
더보기

a. 문제 설명

lst 배열에 있는 값을 통해 이진 탐색 트리를 생성하고, 이 이진 탐색 트리로 search_lst 배열의 각 원소가 있는지 확인합니다. 각 원소가 이진 탐색 트리에 존재하면 true, 없으면 false를 반환하는 함수 solution()을 작성하세요.

 

ex)

lst search_lst result
[5,3,8,4,2,1,7,10] [1,2,5,6] [true, true, true, false]
[1,3,5,7,9] [2,4,6,8,10] [false, false, false, false, false]

 

b. 의사 코드

  1. 검색하려는 값을 현재 노드와 비교해서 같으면 검색을 완료한다.
  2. 검색하려는 값을 현재 노드와 비교했을 때, 값이 작으면 왼쪽 서브 트리로 이동한다.
  3. 검색하려는 값을 현재 노드와 비교했을 때, 값이 크면 오른쪽 서브 트리로 이동한다.

c. 문제 풀이

#include <vector>
using namespace std;

// ❶ 노드를 정의
class Node 
{
public:
  int val;
  Node *left, *right;

  Node(int key) : val(key), left(nullptr), right(nullptr) {}
};

//❷ 이진 탐색 트리 정의
class BST 
{
private:
  Node* root;

  
  Node* insertNode(Node* node, int key) 
  {
    if (!node) 
    {
      return new Node(key);
    }
    //❸ 키 값과 현재 노드의 값을 비교해서 이진탐색트리 규칙에 맞는 위치로 이동
    if (key < node->val) 
    {
      node->left = insertNode(node->left, key);
    } 
    else 
    {
      node->right = insertNode(node->right, key);
    }
    return node;
  }

  bool searchNode(Node* node, int key) 
  {
    //❹ 찾는 키 값이 없는 경우 
    if (!node) 
    {
      return false;
    }
    //❺ 이진탐색트리에서 키 값을 찾은 경우
    if (key == node->val) 
    {
      return true;
    }
    //❻ 아직 값을 찾지 못한 경우, 현재 노드값과 key 값을 비교해서, 어느 노드에서 탐색할지 결정
    return key < node->val ? 
           searchNode(node->left, key) : 
           searchNode(node->right, key);
  }

public:
  BST() : root(nullptr) {}

  void insert(int key) 
  {
    root = insertNode(root, key);
  }

  bool search(int key) 
  {
    return searchNode(root, key);
  }
};

vector<bool> solution(vector<int> lst, vector<int> search_lst) 
{
  BST bst;
  // 이진 탐색 트리에 노드 삽입
  for (int key : lst) 
  {
    bst.insert(key);
  }

  vector<bool> result;
  // 이진 탐색 트리에서 찾는 값이 있는지 확인하고 탐색결과를 result에 추가
  for (int search_val : search_lst) 
  {
    result.push_back(bst.search(search_val));
  }

  return result;
}

//아래 코드는 테스트 코드 입니다.
#include <iterator>
#include <iostream>
void print(vector<bool> vec)
{
    copy(vec.begin(), vec.end(), std::ostream_iterator<bool>(cout, " "));
    cout << endl;
}

int main()
{
    // bool을 출력할 때 true는 1 false는 0 입니다.
    print(solution({5, 3, 8, 4, 2, 1, 7, 10}, {1, 2, 5, 6})); //출력값 : 1 1 1 0
    print(solution({1, 3, 5, 7, 9}, {2, 4, 6, 8, 10})); //출력값 : 0 0 0 0 0    
    return 0;
}

 

d. 문제 후기

  • 시간복잡도 O(N) : 트리에 배열을 넣는 과정에서 최악의 경우 한쪽으로 치우친 트리의 케이스가 나올 수 있기 때문에 보통은 logN과 가깝겠지만 최악의 상황에서는 N이 나온다.
  • AVL트리나, 레드블랙트리를 구현하는 방법을 알아볼 필요가 있다.

 

   3) 예상 대진표(링크)

더보기
더보기

a. 문제 설명

링크 참조

 

 

b. 의사 코드

  1. 문제는 길지만 비교적 간단하게 풀 수 있다. 대진표가 트리의 구조와 동일하기 때문에, 트리의 구조를 생각하며 문제를 풀어보자.
  2. 주어진 a와 b노드를 부모를 타고 쭉쭉 올라가는 형태로 구해보자.

c. 문제 풀이

#include<iostream>
#include <bits/stdc++.h>

using namespace std;

int solution(int n, int a, int b)
{
    int answer = 0;
    
    while(a!=b)
    {
        a = (a + 1) / 2;
        b = (b + 1) / 2;
        answer++;
    }
    
    return answer;
}

 

d. 문제 후기

  • 시간복잡도 O(logN) : 트리로 이루어진 간선들을 탈 때마다 logN 만큼 시간을 절약한다.

 

   4) 다단계 칫솔 판매(링크)

더보기
더보기

a. 문제 설명

링크 참조

 

 

b. 의사 코드

  1. 우선 enroll과 referal은 해시맵으로 묶어서 이진트리 노드의 특성 중 노드의 부모는 하나밖에 없다는 것을 이용할 수 있을것 같다는 생각을 하여 묶었다.
  2. 또한, enroll과 int자료형을 통해 얼마나 이익금을 받는지 해시맵을 통해 묶어주기로 한다.
  3. seller의 부모를 끝없이 찾아야 하기 때문에 for문을 통해 seller를 선택하고, seller의 부모는 만들어둔 해시맵으로 묶어준다. while문을 이용하여 부모가 "-"가 나올때 까지 이익금의 10%를 나눠주는 형태로 반복문을 진행한다.
  4. 마지막으로 완성된 이익금의 해시맵을 배열에 넣어준다.

c. 문제 풀이

#include <bits/stdc++.h>

using namespace std;

vector<int> solution(vector<string> enroll, vector<string> referral, vector<string> seller, vector<int> amount) 
{
    vector<int> answer;
    
    unordered_map<string, string> umap_parent;
    for(int i = 0; i < enroll.size(); i++) umap_parent[enroll[i]] = referral[i];
    
    unordered_map<string, int> umap_money;
    for(const auto& e : enroll) umap_money[e] = 0;
    
    for(int i = 0; i < seller.size(); i++)
    {
        int money = amount[i] * 100;
        string str = seller[i];
        
        while(money > 0 && str != "-")
        {
            int vat = money / 10;
            umap_money[str] += money - vat;
            
            if(umap_parent.find(str) != umap_parent.end())
            {
                str = umap_parent[str];
            }
            else
            {
                break;
            }
            
            money = vat;
        }
    }
    
    for(const auto& e : enroll) answer.push_back(umap_money[e]);
    
    return answer;
}

 

d. 문제 후기

  • 시간복잡도 O(N^2+2N) : 우선 부모자식관계의 노드를 나타내는 해시맵을 만들고, 각 이름별 얼마만큼의 돈을 벌었는지의 해시맵도 만들어준 다음, 판매자와 판매자의 부모들이 이익금을 얼마나 받는지 계산해준다.

 

 

 

 

 

 

해당 포스팅은 "코딩 테스트 합격자 되기 C++편" 의 책 및 강의를 보며 포스팅 한 내용입니다. 

 

 

 

[지금 무료] 코딩 테스트 합격자 되기 - C++ 강의 | dremdeveloper - 인프런

dremdeveloper | 코딩 테스트 합격을 위한 C++ 강의, 책 없이도 가능! 저자와 직접 소통 가능한 커뮤니티 제공!, [사진]여기에 문의 하세요https://open.kakao.com/o/gX0WnTCf📘 코딩 테스트 합격자 되기 - C++편

www.inflearn.com

 

 

 

 

💬주요 키워드

해시 : 기존 배열의 index역할을 문자화 시킨것을 의미한다. 예를 들어, 전화번호부에서 "멍개발"을 인덱싱하면 010-XXXX-XXXX라는 번호를 Element로 얻을 수 있다.

 

여기서 "멍개발"이 Hash에 속한다. 기존 배열에서 인덱싱하던 0,1,2,3 와는 다르다. 기존 배열에 문자열을 저장하기 위해서는,pair 클래스를 이용하여 Element에 2개의 요소를 저장하여 관리할 수 있다. 하지만 이 경우에는 시간복잡도에 의한 단점이 발견된다.

그 이유는 전화번호를 찾기 위해서는 전체 배열을 순회  O(N) 해야 한다는 단점이 있다. 

 

해시를 사용한다면, 매우 큰 확률로 O(1)의 시간복잡도로 동작되어, 코테 문제를 효율적으로 풀 수 있다.

그렇다면 어떻게 메모리주소를 이름으로 변환시켜서 우리가 빠르게 찾을 수 있도록 하는 걸까? 또한, 내부에서는 어떻게 동작을 하는 걸까?

 

해시 함수 : 해시 함수란, 우리가 인덱싱하려는 임의의 이름들을 메모리주소로 변환하는 과정을 말한다. 예를 들어 "멍개발" 이라는 이름을 인덱싱 하면 
"멍개발" -> 0X001A2B3C 이러한 형태로 전환하여 배열에서 인덱싱하듯 값을 불러온다는 것이다.

 

주어진 해시를 변환하여 적절한 메모리주소를 찾아주는 과정을 해시 함수라고 한다.해시함수의 방법으로는 나눗셈법(소수), 곱셈법(황금비), 문자열 해싱(메르센 소수 31)이 있다.

 

충돌 처리 : 해시는 배열로 이루어져 있다. 해시 함수를 이용하여 숫자로 된 배열의 인덱스에 접근하는 것인데, 이름이 다른 해시를 입력하더라도 같은 배열의 인덱스에 접근할 수 있다. 이를 충돌처리라고 하며 이를 해결하기 위해 체이닝, 개방 주소법이라는 방법이 있다.

 

해시 테이블 : 해시도 배열로 이루어져 있기 때문에, 해시의 Element를 저장하는 곳을 해시 테이블이라고 한다. 

 

unordered_map : 해당 자료구조가 해시로 되어있으므로, 해시 관련 문제라면 이 자료구조를 사용하도록 하자. 

여담으로, hash_map이라는 동일한 라이브러리가 있는데 정식 STL이 된 것은 unordered_map이므로 unordered_map을 애용하자.

 

📝내용

 

해시(Hash)

1. 정의

해시는 데이터를 특정한 방식으로 변환하여 빠르게 검색할 수 있도록 하는 기술이다.
해시는 보통 해시테이블이라는 변수로 이루어져 있으며, 코테에서는 unordered_map과 unordered_set이 해시구조로 이루어져 있다.

 

동작 방식은 링크 참조

https://www.youtube.com/watch?v=VeYKEMY2F9k&ab_channel=VisualHow

 


 

2.  해시의 ADT

   1) Insert : 해시에 키-밸류를 넣는 동작

   2) Get: 해시에 키-밸류를 접근하는 동작

   3) HashFunc(int): 숫자로 된 키를 통해 밸류로 접근할 때, 키->인덱스로 변환하는 함수

   4) HashFunc(string): 문자로 된 키를 통해 밸류로 접근할 때, 키->인덱스로 변환하는 함수

   5) Print : 모든 해시의 키-밸류 값을 출력한다.

   

   ADT를 바탕으로 구현한 코드

#include <iostream>
#include <string> // std::string

using namespace std;

template<typename K, typename V>
class HashTable
{
public:
	struct Item
	{
		K key = K();
		V value = V();
	};

	HashTable(const int& cap = 8)
	{
		capacity_ = cap;
		table_ = new Item[capacity_];
	}

	~HashTable()
	{
		delete[] table_;
	}

	void Insert(const Item& item)
	{
		size_t index = HashFunc(item.key);
		// key가 int 자료형일때는 0이면 비어있는 것으로 가정
		// key가 문자열 자료형일때는 길이가 0 비어있는 것으로 가정

		if (table_[index].key != K())
			cout << "Collision!" << endl;

		for (int i = 0; i < capacity_; i++)
		{
			int temp = (index + i) % capacity_;
			if (table_[temp].key == K())
			{
				table_[temp] = item;
				return;
			}
		}
		cout << "Failed to insert" << endl;
	}

	V Get(const K& key)
	{
		// 풀이 후에 드래그
		size_t index = HashFunc(key);
		for (int i = 0; i < capacity_; i++)
		{
			// index = (index + i) % capacity;
			size_t temp = (index + i) % capacity_; // index를 덮어쓰는 것이 아니라 임시 변수 만들기 
			if (table_[temp].key == key)
				return table_[temp].value;
		}
		return V();
	}

	// 정수 -> 해시값
	size_t HashFunc(const int& key)
	{
		return key % capacity_;
	}

	// 문자열을 정수 인덱스(해시값)로 변환
	// Horner's method
	size_t HashFunc(const string& s)
	{
		size_t index = 0;
		for (const auto& c : s) index = 31 * index + int(c);
		return index % capacity_;
	}

	void Print()
	{
		for (int i = 0; i < capacity_; i++)
			cout << i << " : " << table_[i].key << " " << table_[i].value << endl;
		cout << endl;
	}

private:
	Item* table_ = nullptr;
	int capacity_ = 0;
};

int main()
{
	// 충돌 
	// - 개방 주소법: 선형 조사법
	// - 체이닝: 멤버 변수 Item* table_ 대신에 LinkedList<Item>* table_사용

	// 키: int, 값: int 인 경우
	// 키의 범위가 아주 크고 아이템의 개수는 적을 경우
	{
		using Item = HashTable<int, int>::Item;

		HashTable<int, int> h(8);

		h.Insert(Item{ 123, 456 });

		h.Print();

		cout << "Get 123 " << h.Get(123) << endl;

		h.Insert(Item{ 1000021, 9898 });

		h.Print();

		cout << "Get 1000021 " << h.Get(1000021) << endl;

		h.Insert(Item{ 1211, 999 }); // 충돌!

		h.Print();

		cout << "Get 123 " << h.Get(123) << endl;
		cout << "Get 1211 " << h.Get(1211) << endl;
	}

	// 키: std::string, 값: int
	{
		using Item = HashTable<string, int>::Item;

		HashTable<string, int> h(8);

		h.Insert(Item{ "apple", 1 });
		h.Insert(Item{ "orange", 2 });
		h.Insert(Item{ "mandarin", 4 });

		h.Print();

		cout << "apple " << h.Get("apple") << endl;
		cout << "orange " << h.Get("orange") << endl;
		cout << endl;

		h.Print();

		h.Insert(Item{ "tomato", 4 });

		h.Print(); 

		cout << "apple " << h.Get("apple") << endl;
		cout << "orange " << h.Get("orange") << endl;
		cout << "pineapple " << h.Get("pineapple") << endl;
		cout << endl;
	}

	return 0;
}

 

 


3.  문제 풀이

   1) 두 개의 수로 특정 값 만들기

더보기

a. 문제 설명

n개의 양의 정수로 이루어진 배열 arr와 정수 target이 주어졌을 때 이 중에서 합이 target인 두 수가 arr에 있는지 찾고, 있으면 true, 없으면 false를 반환하는 solution() 함수를 작성하세요.

 

  • n은 2 이상 10,000이하의 자연수 입니다.
  • arr의 각 원소는 1 이상 10,000 이하의 자연수 입니다.
  • arr의 원소 중 중복되는 원소는 없습니다.
  • target은 1 이상 20,000 이하의 자연수 입니다.

ex)

arr target result
[1,2,3,4,8] 6 true
[2,3,5,9] 10 false

 

b. 의사 코드

  1. 평소같으면 2중 for문을 써서 확인해보겠지만, hash를 활용하여 어떻게 동작하는지 확인해보자.
  2. Hash를 쓴다면, 1,2,3,4,8의 값을 키에 넣어 있는지 없는지를 확인하는 수단으로 사용할 수 있을듯 하다.
  3. target 숫자를 확인하고 첫번째 숫자와 같이 더하면 target이 되는 수를 찾아보는 식을 짠다.

c. 문제 풀이

#include <iostream>
#include <vector>
#include <unordered_map>

using namespace std;

bool solution(vector<int> arr, int target)
{
	bool answer = false;
	unordered_map<int, int> unmap;

	for (const auto& e : arr) unmap[e]++; //1. 각 숫자를 해시에 넣어준다.

	for (const auto& e : arr)
	{
    	//2. 해시의 키 값을 find 함수를 이용해 방정식의 해가 되는 값을 찾는다.
        //*  또한, 해가 자기 자신이 될 경우도 있기 때문에 자기 자신이 될 경우는 제외한다.
		if (unmap.find(target - e) != unmap.end() && (target - e) != e)
		{
			answer = true;
			break;
		}
	}

	return answer;
}

int main()
{
	vector<int> arr1 = { 1,2,3,4,8 };
	int target1 = 6;

	vector<int> arr2 = { 2,3,5,9 };
	int target2 = 10;

	cout << boolalpha << solution(arr1, target1) << endl;
	cout << boolalpha << solution(arr2, target2) << endl;
}

 

d. 문제 후기

  • 시간복잡도 O(2N) : 주어진 배열을 2번 순회한다.
  • 책에서는 vector를 2개 선언하여 하나는 해시의 key역할, 하나는 value을 하게 만들었다.
  • 해시의 ADT파트에서 스스로 구현해보았으므로 이 과정은 생략하고 STL을 이용하여 풀이해보았다.

 

 

   2) 문자열 해싱을 이용한 검색 함수 만들기

더보기

a. 문제 설명

문자열 배열 string_list 와 쿼리 배열 query_list가 있을 때 각 쿼리 배열에 있는 문자열이 string_list의 문자열 배열에 있는지 여부를 확인해야 합니다. 문자열이 있으면 true, 없으면 false가 됩니다. 각 문자열에 대해서 문자열의 존재 여부를 배열 형태로 반환하는 solution() 함수를 작성해주세요. 

 

  • 입력 문자열은 영어 소문자로만 이루어져 있습니다.
  • 문자열의 최대 길이는 10^6 입니다.
  • 해시 충돌은 고려하지 않습니다.
  • 아래와 같은 문자열 해싱 방법을 활영해서 해싱 함수를 구현하세요.
  • 다음 식에서 p는 31, m은 1,000,000,007 입니다.
    • hash(s) = (s[0] + s[1]*p + s[2]*p^2 ........ s[n-1]*p^n-1)mod m

 

ex)

string_list query_list result
apple, banana, cherry banana, kiwi, melon, apple true, false, false, true

 

b. 의사 코드

  1. 문자열 해싱을 이용해, 문자열을 숫자로 변환해준다.
  2. 변환한 숫자를 Key값으로 만들어 준 후에, 해싱화 시켜준다.
  3. 해싱화 시킨 값들을 찾는다.해싱화 시킨 값들을 찾는다.

 

c. 문제 풀이

#include <vector>
#include <string>
#include <unordered_set>

using namespace std;

// ❶ 다항 해시 함수 구현
long long polynomial_hash(const string& str) {
    const int p = 31;  // 소수
    const long long m = 1000000007;  // 버킷 크기
    long long hash_value = 0;

    for (char c : str) {
        hash_value = (hash_value * p + c) % m;
    }

    return hash_value;
}

vector<bool> solution(vector<string> string_list, vector<string> query_list) {
    unordered_set<long long> hash_set;

    //❷ string_list의 각 문자열에 대해 다항 해시값을 계산하고 저장
    for (const string& str : string_list) {
        long long hash = polynomial_hash(str);
        hash_set.insert(hash);
    }

    vector<bool> result;

    //❸  query_list의 각 문자열이 string_list에 있는지 확인하고 result에 추가
    for (const string& query : query_list) {
        long long query_hash = polynomial_hash(query);
        bool found = (hash_set.find(query_hash) != hash_set.end());
        result.push_back(found);
    }
    // ❹ query_list의 문자열이 hash에 있는지 결과가 저장된 result를 반환
    return result;
}


//아래 코드는 테스트 코드 입니다.
#include <iterator>
#include <iostream>
void print(vector<bool> vec)
{
    copy(vec.begin(), vec.end(), std::ostream_iterator<bool>(cout, " "));
    cout << endl;
}

int main()
{
    //true를 출력하면 1이되고 false를 출력하면 0
    print(solution({ "apple", "banana", "cherry" }, { "banana", "kiwi", "melon", "apple" })); // 1 0 0 1
    return 0;

}

d. 문제 후기

  • 시간복잡도 O(N^2) : 해싱할 문자열의 문자 하나하나를 검사하고, 그 문자열들을 숫자로 변환하는 과정이 있기 때문
  • 문자열 해싱은 메르센 소수 31과 1000000007만 기억하고 있어야겠다.
  • 또한, 이 문자열 해싱은 잘못쓰이면, 오버플로우가 일어나기 때문에 mod m 연산을 잘 해주는 것이 중요하다.

 

   3) 완주하지 못한 선수 (링크)

더보기

a. 문제 설명

링크 참조

 

 

b. 의사 코드

  1. 일단 참가자들을 다 해시에 넣는다.문자열 해싱을 이용해, 문자열을 숫자로 변환해준다.
  2. 완주자들을 해시에서 빼준다.
  3. 참가자 중 완주자에 들지 못한 사람을 answer에 넣어준다.

 

c. 문제 풀이

#include <string>
#include <iostream>
#include <vector>
#include <unordered_map>

using namespace std;

string solution(vector<string> participant, vector<string> completion) 
{
    string answer = "";
    unordered_map<string, int> unmap;
    
    for(auto e : participant) unmap[e]++; //1. 일단 참가자들을 다 해시에 넣는다.
    
    for(int i = 0; i < completion.size(); i++)
    {
        unmap[completion[i]]--; //2. 완주자들을 해시에서 빼준다.
    }
    
    for(int i = 0; i < participant.size(); i++)
    {
        if(unmap[participant[i]] > 0) //3. 참가자 중 완주자에 들지 못한 사람(i > 0)을 answer에 넣어준다.
        {
            answer += participant[i];
            break;
        }
    }
    
    return answer;
}

d. 문제 후기

  • 시간복잡도 O(3N) : 해시에 모든 참가자를 넣고, 완주자들을 해시에서 제외하고, 남은 참가자를 도출
  • 해시맵을 사용하는 첫 예제인 만큼 어떻게 해시맵을 사용해야 할 지 알게 되었던 문제였다.

 

 4) 영어 끝말잇기 (링크)<-- 이전에 나왔던 문제

더보기

a. 문제 설명

링크 참조

 

 

b. 의사 코드

  1. 주어진 끝말잇기 단어를 해시맵에 모두 담는 반복문을 만든다.
  2. 만약, 중복되거나 끝말이 아닌 단어가 온다면 count를 기록해둔다.
  3. count에 따라서 몇번째 사람이 걸렸는지, 몇번 돌았는지를 기록한다.

 

c. 문제 풀이

#include <string>
#include <vector>
#include <unordered_map>
#include <iostream>

using namespace std;

vector<int> solution(int n, vector<string> words) 
{
    vector<int> answer;
    unordered_map<string, int> hash_map;
    int cnt = 0;
    
    // !. 첫번째 해시는 무조건 담는다.
    hash_map[words[0]]++;
    // 1. 주어진 단어를 해시맵에 담는 반복문을 만든다.
    for(int i = 1; i < words.size(); i++)
    {
        // 2. 만약, 중복되거나 끝말이 아닌 단어가 온다면 count를 기록해둔다.
        // !. 한글자인 단어도 검출
        if(hash_map[words[i]] >= 1 || words[i-1][words[i-1].size()-1] != words[i][0] || words[i].size() == 1)
        {
            cnt = i;
            break;
        }
        else
        {
            hash_map[words[i]]++;
        }
    }

    // !. 만약 아무도 실패하지 않았다면, 0,0을 리턴한다.
    if(cnt == 0)
    {
        answer.push_back(0);
        answer.push_back(0);
        return answer; 
    }
    // 3. 카운트에 따라서 n과 조합하여 가장 먼저 탈락하는 사람의 번호와 몇번째 차례인지 도출해낸다.
    answer.push_back(cnt%n+1); // 가장 먼저 탈락하는 사람
    answer.push_back(cnt/n+1); // 몇번째 차례
    return answer;
}

d. 문제 후기

  • 시간복잡도 O(N) : 해시에 참가자들을 넣으면서, 단어들이 규칙에 맞는지 도출해낸다.
  • 이전에도 푼 문제지만, 중복인 단어나 끝말잇기의 규칙에 잘 들어맞는지를 판단하는것이 중요!

 

 5) 전화번호 목록 (링크)

더보기

a. 문제 설명

링크 참조

 

 

b. 의사 코드

  1. 전화번호를 모두 맵에 저장한다.
  2. 각 전화번호가 자기 자신이 아니라는 가정 하에 한 단어씩 글자마다 비교하여 맵에 있는지 확인한다.

 

c. 문제 풀이

#include <string>
#include <vector>
#include <unordered_map>
#include <algorithm>

using namespace std;

bool solution(vector<string> phone_book) 
{
    bool answer = true;
    unordered_map<string, int> unmap;
    
    // 1. 전화번호를 모두 맵에 저장한다.
    for(auto e : phone_book) unmap[e]++;
    
    // 2. 각 전화번호를 한번씩 순회하여 맵에 있는지 확인한다.
    for(auto e : phone_book)
    {
        string temp = "";
        
        for(auto c : e)
        {
            temp.push_back(c);
            if(temp != e && unmap.find(temp) != unmap.end())
            {
                return false;
            }
        }
        
    }
    return answer;
}

d. 문제 후기

  • 시간복잡도 O(N*M^2) : phone_book을 맵에 한번씩 넣고,  phone_book에서 글자 하나하나를 계속 비교한다.
  • 책을 보니 정렬로 푸는 매우 쉬운 방법이 있어 그 방법을 이용하면 될 것 같다. 
  • string끼리의 정렬과, int끼리의 정렬은 다르다는 것을 알았다.
  • 꼭 해시맵이 시간복잡도 상으로 이득을 보는건 아니라는 것을 알게 되었다.

 

 6) 할인행사 (링크)

더보기

a. 문제 설명

링크 참조

 

 

b. 의사 코드

  1. want 와 number를 해시맵에 초기화한다.
  2. 할인 행사가 진행되는 품목을 순회하는데, 10개씩 확인해봐야 하기 때문에 unmap_temp을 하나 더 선언하여 할인행사 하는 품목들을 하나하나 넣어준다.
  3. 마지막으로 unmap_temp와 unmap에 담긴 것들이 모두 같은지 비교한다.
  4. 가능한 날짜마다 answer에 1씩 더해준다.

 

c. 문제 풀이

#include <string>
#include <vector>
#include <unordered_map>

using namespace std;

int solution(vector<string> want, vector<int> number, vector<string> discount) 
{
    int answer = 0;
    unordered_map<string, int> unmap;
    
    for (int i = 0; i < want.size(); i++)
    {
        unmap[want[i]] = number[i];
    }
    
    for (int i = 0; i < discount.size()-9; i++)
    {
        unordered_map<string, int> unmap_temp;
        bool flag = false;
        
        for(int j = i; j < i + 10; j++)
        {
            unmap_temp[discount[j]]++;
        }
        
        for(const auto& e : unmap_temp)
        {
            if(unmap.find(e.first) == unmap.end() || e.second != unmap.find(e.first)->second)
            {
                flag = true;
                break;
            }
        }
        
        if(flag) continue;
        answer++;
    }
    return answer;
}

d. 문제 후기

  • 시간복잡도 O(20*N) : 일단 해시맵에 저장하고, 할인행사중인 10개의 품목을 또 저장한 다음, 10개의 품목 하나하나 검사하는 과정이 있으므로 20N이다.
  • 책에서는 시간복잡도를 더 개선할 수 있는 방법을 제시하였는데, 개발자의 입장에서는 계속 효율적인 방법을 찾아나가는 것이 유지보수가 아닐까? 라는 생각이 든다.

 

 

 7) 오픈채팅방 (링크) <-- (78.1% TLE 오답)

더보기

a. 문제 설명

링크 참조

 

 

b. 의사 코드

  1. 일단 record의 공백으로 나누어진 부분을 명령, uid, 이름으로 분석해야 하기 때문에 이것을 string 내장함수를 적절히 이용하여 분리한다
  2. 이름을 변경해야 하는 부분은 Enter와 Change이기 때문에, Enter와 Change에서는 for문을 다시한번 사용하여 이전에 입력했던 채팅안내문의 닉네임을 수정하였다. <-- 여기서부터는 코드를 작성하며 생각하였다.

 

c. 문제 풀이(오답)

#include <string>
#include <iostream>
#include <vector>
#include <unordered_map>

using namespace std;

vector<string> solution(vector<string> record) 
{
    vector<string> answer;
    unordered_map<string, string> unmap;
    
    for(auto e : record)
    {
        if(e.find("Enter") != string::npos)
        {
            int pos1 = e.find(' ');
            string cmd = e.substr(0, pos1);
            
            int pos2 = e.find(' ', e.find(' ') + 1);
            string uid = e.substr(pos1+1, pos2 - pos1 - 1);
            
            string name = e.substr(pos2+1, e.size() - pos2);
            
            unmap[uid] = name;
            
           for(int i = 0; i < answer.size() ; i++)
            {
                if(answer[i] == uid && answer[i+1].find("들어왔") != string::npos)
                {
                    answer[i+1] = name + "님이 들어왔습니다.";
                }
                else if (answer[i] == uid && answer[i+1].find("나갔") != string::npos)
                {
                    answer[i+1] = name + "님이 나갔습니다.";
                }
            }
            
            answer.push_back(uid);
            answer.push_back(unmap[uid] + "님이 들어왔습니다.");
        }
        else if(e.find("Leave") != string::npos)
        {
            int pos1 = e.find(' ');
            string cmd = e.substr(0, pos1);
            
            int pos2 = e.find(' ', e.find(' ') + 1);
            string uid = e.substr(pos1+1, pos2 - pos1 - 1);
            
            answer.push_back(uid);
            answer.push_back(unmap[uid] + "님이 나갔습니다.");
        }
        else if(e.find("Change") != string::npos)
        {
            int pos1 = e.find(' ');
            string cmd = e.substr(0, pos1);
            
            int pos2 = e.find(' ', e.find(' ') + 1);
            string uid = e.substr(pos1+1, pos2 - pos1 - 1);
            
            string name = e.substr(pos2+1, e.size() - pos2);
            
            unmap[uid] = name;
            
            for(int i = 0; i < answer.size() ; i++)
            {
                if(answer[i] == uid && answer[i+1].find("들어왔") != string::npos)
                {
                    answer[i+1] = name + "님이 들어왔습니다.";
                }
                else if (answer[i] == uid && answer[i+1].find("나갔") != string::npos)
                {
                    answer[i+1] = name + "님이 나갔습니다.";
                }
            }
        }
    }
    
    vector<string> answer2;
    
    for(int i = 1; i < answer.size(); i+=2) answer2.push_back(answer[i]);
    
    return answer2;
}

d. 문제 후기

  • 시간복잡도 O(N^2) : 명령어를 저장하고, 저장한 명령어를 수행 할 때, 이름을 바꾸기 위해서 이전 기록들을 뒤져 이름을 계속 바꾸어 나가는 작업을 수행하였다.
  • 정답을 보면 꽤나 간단하게 코드가 짜여져 있었다. 아래 코드를 확인하면 된다.
#include <vector>
#include <unordered_map>
#include <sstream>

using namespace std;

vector<string> solution(vector<string> record)
{
	vector<string> answer;
	unordered_map<string, string> uid;

	for (const auto& line : record)
	{
		stringstream ss(line);
		string cmd, id, name;
		ss >> cmd >> id;

		if (cmd != "Leave")
		{
			ss >> name;
			uid[id] = name;
		}
	}

	for (const auto& line : record)
	{
		stringstream ss(line);
		string cmd, id;
		ss >> cmd >> id;

		if (cmd == "Enter")
		{
			answer.push_back(uid[id] + "님이 들어왔습니다.");
		}
		else if (cmd == "Leave")
		{
			answer.push_back(uid[id] + "님이 나갔습니다.");
		}
	}

	return answer;
}

 

  •  새롭게 안 사실은 sstream헤더의 존재로 cmd, uid, name 변수에 깔끔하게 나눌 수 있었으며
  • 굳이 2중 for문을 쓰는게 아닌, 2번 for문을 써서 첫번째는 명령을 먼저 수행하여 uid에 따른 name만 변경하고 두번째는 명령어에 따라서  answer에 넣어주는 작업을 하였는데, 2중 for문을 경계하는법을 알게된 것 같다.



8) 베스트 앨범(링크) <-- (13.3% 완전 오답) ⭐⭐⭐⭐⭐ 

더보기

a. 문제 설명

링크 참조

 

 

b. 의사 코드

  1. 일단 속한 장르 중 가장 많이 재생된 장르를 내림차순으로 정렬해야 했기 때문에 hash_map<string, int> 를 이용하여 장르별 plays를 총합하여, sorting 하였다.
  2.  이후 각 장르 별 idx와 play횟수를 make_pair를 통해 한 DataType에 묶어두고, 이를 내림차순으로 정렬하고 같은 재생횟수는 idx기준으로 오름차순 정렬을 한번 더 한 다음 장르별 차트hash_map에 넣어준다.
  3. 장르별 차트hash_map의 second값을 2개 뽑아 순서대로 넣어주면 끝

 

c. 문제 풀이(오답)

#include <string>
#include <vector>
#include <unordered_map>
#include <algorithm>
#include <iostream>
#include <map>
#include <set>

using namespace std;

bool ByValue(const pair<string, int>& a, const pair<string, int>& b)
{
    return a.second > b.second;
}

vector<int> solution(vector<string> genres, vector<int> plays)
{
    vector<int> answer;

    unordered_map<string, int> umap;
    for (int i = 0; i < genres.size(); i++)
    {
        umap[genres[i]] += plays[i];
    }

    vector<pair<string, int>> vec_pair(umap.begin(), umap.end());
    sort(vec_pair.begin(), vec_pair.end(), ByValue);

    unordered_map<string, vector<pair<int, int>>> umap_chart;
    for (int i = 0; i < vec_pair.size(); i++)
    {
        vector<pair<int, int>> temp;
        for (int j = 0; j < plays.size(); j++)
        {
            if (vec_pair[i].first == genres[j])
            {
                temp.push_back(make_pair(plays[j], j));
            }
        }

        sort(temp.begin(), temp.end());
        reverse(temp.begin(), temp.end());

        for (int j = 1; j < temp.size(); j++)
        {
            if (temp[j - 1].first == temp[j].first && temp[j - 1].second > temp[j].second)
            {
                swap(temp[j - 1], temp[j]);
            }
        }

        umap_chart[genres[i]] = temp;
    }

    for (const auto& e : umap_chart)
    {
        int cnt = 0;
        for (int i = 0; i < e.second.size(); i++)
        {
            if (cnt >= 2) break;
            answer.push_back(e.second[i].second);
            cnt++;
        }
    }
    
    return answer;
}

d. 문제 후기

  • 시간복잡도 O(N^2) : 가장 많이 실행된 장르를 순회하고, 그 장르에서 가장 많이 실행된 2가지를 선별하는 과정이 있어 2중 for문을 쓰게 되었다.
  • 정답을 보면 꽤나 간단하게 코드가 짜여져 있었다. 아래 코드를 확인하면 된다.
#include <vector>
#include <string>
#include <algorithm>
#include <unordered_map>

using namespace std;

bool compareGenre(const pair<string, int>& a, const pair<string, int>& b) {
  return a.second > b.second;
}

bool compareSong(const pair<int, int>& a, const pair<int, int>& b) {
  if (a.second == b.second) return a.first < b.first;
  return a.second > b.second;
}

vector<int> solution(vector<string> genres, vector<int> plays) {
  vector<int> answer;
  unordered_map<string, vector<pair<int, int>>> genres_dict;
  unordered_map<string, int> play_dict;

  //❶ 장르별 총 재생 횟수와 각 곡의 재생 횟수 저장
  for (int i = 0; i < genres.size(); ++i) {
    genres_dict[genres[i]].push_back({i, plays[i]});
    play_dict[genres[i]] += plays[i];
  }

  //❷ 총 재생 횟수가 많은 장르순으로 정렬
  vector<pair<string, int>> sorted_genres(play_dict.begin(), play_dict.end());
  sort(sorted_genres.begin(), sorted_genres.end(), compareGenre);

  //❸ 각 장르 내에서 노래를 재생 횟수 순으로 정렬해 최대 2곡 까지 선택
  for (auto& genre : sorted_genres) {
    auto& songs = genres_dict[genre.first];
    sort(songs.begin(), songs.end(), compareSong);

    for (int i = 0; i < min(2, (int)songs.size()); ++i) {
      answer.push_back(songs[i].first);
    }
  }

  return answer;
}

//아래 코드는 테스트 코드 입니다.
#include <iterator>
#include <iostream>
void print(vector<int> vec)
{
    copy(vec.begin(), vec.end(), std::ostream_iterator<int>(cout, " "));
    cout << endl;
}

int main()
{
    print(solution({"classic", "pop", "classic", "classic", "pop"}, {500, 600, 150, 800, 2500})); //출력값 :  1 0 0 1
    return 0;

}

 

 

 9) 신고 결과 받기(링크

더보기

a. 문제 설명

링크 참조

 

 

b. 의사 코드

  1. 우선 신고자와 피신고자로 hash_map 변수를 만들어, 신고자는 내가 신고한 사람들을 모으고 / 피신고자는 신고를 받은 횟수를 카운팅해준다.
  2. 피신고자가 k라는 기준보다 많이 신고 당할 경우, id_list를 한번 순회하여 신고자들에게 메일을 전송한 횟수를 retrun 해준다.

 

c. 문제 풀이

#include<iostream>
#include<unordered_map>
#include<set>
#include<vector>
#include<algorithm>
#include<string>
#include<sstream>

using namespace std;

vector<int> solution (vector<string> id_list, vector<string> report, int k)
{
    vector<int> answer;
    
    sort(report.begin(), report.end());
    auto pos = unique(report.begin(), report.end());
    report.erase(pos, report.end());
    
    unordered_map<string, vector<string>> umap_report;
    unordered_map<string, int> umap_reported;
    
    for(const auto& str : report)
    {
        stringstream ss(str);
        string receiver;
        string reporter;
        
        ss >> reporter >> receiver;
        
        umap_report[reporter].push_back(receiver);
        umap_reported[receiver]++;
    }
    
    for(const auto& id : id_list)
    {
        int temp = 0;
        for(const auto& ban : umap_reported)
        {
            if(ban.second < k) continue;
            if(find(umap_report[id].begin(), umap_report[id].end(), ban.first) != umap_report[id].end())
            {
                temp++;
            }
        }
        answer.push_back(temp);
    }
    
    return answer;
}

d. 문제 후기

  • 시간복잡도 O(N^2) : id_list를 순회하며 피신고자hash_map을 k의 기준에 부합한지 1번 더 순회하므로 N^2의 시간복잡도를 가진다.
  • 이전에 stringstream에 대해서 배우지 않았다면, 역시나 어렵게 공백을 기준으로 쪼개는 알고리즘 코드를 작성하였을 것이다.

 

 


10) 메뉴 리뉴얼(링크) <--(오답) 순열 문제 ⭐⭐⭐⭐⭐ 

더보기

a. 문제 설명

링크 참조

 

 

b. 의사 코드

  1. 순열을 사용해야 한다는 것을 알았으나, next_permutation 함수의 사용법과 재귀로 조합함수를 어떻게 짜야하는지 감이 안잡혀서 다시한번 제대로 배워야 겠다는 생각이 들었다.

 

c. 문제 풀이(답지)

#include <algorithm>
#include <string>
#include <vector>
#include <map>

using namespace std;

map<string,int> combi; // 주문의 조합 - 조합의 빈도

// 실제 주문의 조합을 구하는 함수
void combination(string src, string dst, int depth) {
  if (dst.size() == depth) combi[dst]++;

  else for (int i = 0; i < src.size(); i++)
    combination(src.substr(i+1), dst+src[i], depth);
}

vector<string> solution(vector<string> orders, vector<int> course) {
  vector<string> answer;
  // ❶ 각 주문들을 오름차순으로 정렬
  for (string &order : orders)
    sort(order.begin(), order.end());
  
  for (int len : course) {
    for (string order : orders)
      // ❷ course의 길이에 해당되는 조합 생성
      combination(order, "", len);

    // ❸ 각 주문의 빈도수를 순회하면서 가장 많은 빈도수를 maxOrder에 저장
    int maxOrder = 0;
    for (auto it : combi)
      maxOrder = max(maxOrder, it.second);

    // ❹ 주문 횟수가 2회 이상이면서, 가장 많이 주문된 주문의 구성은 answer에 추가
    for (auto it : combi)
      if (maxOrder >= 2 && it.second == maxOrder)
        answer.push_back(it.first);
    
    combi.clear();
  }
  // ❺ 반환 전, 문제의 조건에 따라 주문의 구성들을 오름차순 정렬해서 반환 
  sort(answer.begin(), answer.end());
  return answer;
}


//아래 코드는 테스트 코드 입니다.
#include <iterator>
#include <iostream>
void print(vector<string> vec)
{
    copy(vec.begin(), vec.end(), std::ostream_iterator<string>(cout, " "));
    cout << endl;
}

int main()
{
    print(solution({"ABCFG", "AC", "CDE", "ACDE", "BCFG", "ACDEH"}, {2, 3, 4})); //출력값 :  AC ACDE BCFG CDE
    print(solution({"ABCDE", "AB", "CD", "ADE", "XYZ", "XYZ", "ACD"}, {2, 3, 5})); //출력값 : ACD AD ADE CD XYZ
    print(solution({"XYZ", "XWY", "WXA"}, {2, 3, 4})); //출력값 : WX XY 

    return 0;
}

d. 문제 후기

  • 의사코드에 있는 내용과 동일

 

 

 

 

 

해당 포스팅은 "코딩 테스트 합격자 되기 C++편" 의 책 및 강의를 보며 포스팅 한 내용입니다. 

 

 

 

[지금 무료] 코딩 테스트 합격자 되기 - C++ 강의 | dremdeveloper - 인프런

dremdeveloper | 코딩 테스트 합격을 위한 C++ 강의, 책 없이도 가능! 저자와 직접 소통 가능한 커뮤니티 제공!, [사진]여기에 문의 하세요https://open.kakao.com/o/gX0WnTCf📘 코딩 테스트 합격자 되기 - C++편

www.inflearn.com

 

 

 

💬주요 키워드

스택 : 가장 최근에 들어간 원소가 가장 먼저 나오는 데이터 구조를 말한다

LIFO : Last In First Out의 약자로 후입선출 이라고도 한다. 프링글스 통의 감자칩을 시뮬레이션 하면 이해하기 쉽다.

: 가장 최근에 들어간 원소가 가장 먼저 나오는 데이터 구조를 말한다.

FIFO : First In First Out의 약자로 선입선출 이라고도 한다. 버스를 기다리기 위해 선착순으로 줄을 서는 시뮬레이션을 생각하면 이해하기 쉽다.

ADT(Abstract Data Type) : 추상 자료형이라고도 불리며, ~ 할 것이다. 와 같은 인터페이스만 있고 실제 구현되지 않은 코드를 말한다.

 

 

📝내용(스택)

 

스택(stack)

1. 정의

스택(stack)은 쌓는다. 라는 뜻으로 먼저 입력한 데이터를 가장 나중에 꺼낼 수 있는 자료구조 입니다.

이때 스택에 삽입하는 연산을 push, 꺼내는 연산을 pop이라고 한다.

 


 

2.  스택의 ADT

   1) push : 스택에 데이터를 넣는 동작

   2) pop : 스택에 데이터를 빼는 동작

   3) top : 스택의 맨 위에 있는 element를 return 하는 동작

   4) empty : 스택이 비었는지 확인하는 동작

   5) size : 스택의 size를 반환한다.

   6) resize : 스택의 내부 공간을 재할당 하는 동작

   7) []연산자 적용 : 스택을 배열로 만들 때, index를 통해 내부 원소를 확인할 수 있음.

   

   ADT를 바탕으로 구현한 코드

   (구현하다보니 알게된 사실이지만, STL의 stack은 []연산자가 원래 없다.)

#include <iostream>

using namespace std;

template <typename T>
class MyStack
{
private:
	int size_ = 0;
	int cap_ = 0;
	T* stack_ = nullptr;

public:
	MyStack(int cap = 1)
	{
		if (cap > 0) resize(cap);
		else cout << "스택 만들기 실패 : " << endl;
	}

	~MyStack()
	{
		delete[] stack_; // 동적 할당 해제
	}

	void resize(const int& param_cap)
	{
		// 1. 새로운 공간의 배열을 만들어 준다.
		T* temp = new T[param_cap];

		// 2. 기존 배열에서 새로 생성된 배열의 element를 넣어준다. memcpy
		if (cap_ > param_cap) // 새로 생성될 배열이 더 작을 경우
		{
			for (int i = 0; i < param_cap; i++)
			{
				temp[i] = stack_[i]; // 그대로 복사한다.(내부 데이터 유실)
			}
		}
		else if (cap_ <= param_cap) // 새로 생성될 경우
		{
			for (int i = 0; i < param_cap; i++)
			{
				if (i < cap_) temp[i] = stack_[i];
				else temp[i] = 0; // 남는 공간을 0으로 초기화 해준다.
			}
		}

		if (stack_) delete[] stack_;
		stack_ = temp;
		cap_ = param_cap;
	}

	int& size()
	{
		return size_;
	}

	void push(const T& param)
	{
		if (size_ >= cap_) // push하면 메모리에 있는 공간이 초과될 경우
		{
			resize(cap_ * 2);
			stack_[size_] = param;
			size_++;
		}
		else
		{
			stack_[size_] = param;
			size_++;
		}
	}

	void pop()
	{
		size_--;
	}

	T& top()
	{
		return stack_[size_ - 1];
	}

	T& operator[] (int param)
	{
		return stack_[param];
	}

	bool empty()
	{
		return size_ == 0 ? true : false;
	}
};

int main()
{
	MyStack<int> test;

	test.push(1);
	test.push(2);
	test.push(3);

	cout << "[1]연산자 테스트 : " << test[1] << endl;

	cout << "size()테스트 및 원소 순회 : ";
	for (int i = 0; i < test.size(); i++)
	{
		cout << test[i] << " ";
	}
	cout << endl;

	test.pop();

	cout << "pop()&top() 테스트 : " << test.top() << endl;

	test.pop();
	cout << "empty()테스트 : " << test.empty() << endl;

	test.pop();
	cout << "empty()테스트2 : " << test.empty();
}

 

 


3.  문제 풀이

   1) 괄호 짝 맞추기 (교재 184P 참조)

더보기

a. 문제 설명

소괄호는 짝을 맞춘 열린 괄호 '(' 와 닫힌 괄호 ')'로 구성합니다. 문제에서는 열린 괄호나 닫힌 괄호가 마구 뒤섞인 문자열 s를 줍니다. 이 때 소괄호가 정상으로 열고 닫혔는지 판별하는 solution()함수를 구현하세요.

만약 소괄호가 정상적으로 열고 닫혔다면 true를, 그렇지 않다면 false를 반환하면 됩니다.

 

  • 열린 괄호는 자신과 가장 가까운 닫힌 괄호를 만나면 상쇄됩니다.
  • 모든 괄호가 상쇄되어 더이상 아무 괄호도 없어야만 정상으로 열고 닫혔다고 할 수 있습니다.
  • 더 상쇄할 괄호가 없을 때 까지 상쇄를 반복합니다.

ex)

s result
"( ( ) ) ( )" true
 "( ( ( ) ) ( )" false

 

b. 의사 코드

  1. 내부에 있는 '(' 와 ')'를 순회를 통해 확인한다.
  2. stack을 공간을 하나 만들고 순회하면서 '('를 만나면 push를, ')' 만나면 pop을 한다.
  3. 만약 ')'을 했을 때 stack이 비어있다면 bool을 false로 하고 solution을 return 한다.
  4. 만약 ')'을 했을 때 stack이 비어있지 않다면 계속 pop을 한다.

 

c. 문제 풀이

#include <iostream>
#include <stack>
#include <string>

using namespace std;

bool solution(const string& param)
{
	stack<char> stack;

	for (int i = 0; i < param.size(); i++)
	{
		if (param[i] == '(')
		{
			cout << "push : (" << endl;
			stack.push(param[i]);
		}
		else if (param[i] == ')' && !stack.empty())
		{
			cout << "pop : )" << endl;
			stack.pop();
		}
		else if (param[i] == ')' && stack.empty())
		{
			cout << "error" << endl;
			return false;
		}
	}

	return stack.empty() ? true : false;
}

int main()
{
	string s = "()())"; // 임의의 괄호를 입력하세요

	cout << boolalpha << solution(s) << endl;
}

 

d. 문제 후기

  • 시간복잡도 O(N) : 주어진 문자열을 1번 순회한다.
  • 코테에서의 stack 은 짝을 맞추거나, 재귀, 이전으로 되돌리는 동작에 특화되어 있다는 것을 알았다.

   2) 10진수를 2진수로 변환하기 (교재 189P 참조)

더보기

a. 문제 설명

10진수 decimal을 입력받아 2진수로 변환해서 문자열 형태로 반환하는 solution() 함수를 구현하세요.

 

ex)

decimal result
10 "1010"
 27 "11011"
12345 "11000000111001"

 

b. 의사 코드

  1. 일단 decimal(10진수) 와 문자열 형태를 받아야 하니 int 자료형을 선언하고 string을 return 한다.
  2. 입력받은 숫자를 0으로 나누어 떨어질 때 까지 계속 2로 나누어 준다.
  3. 2로 나누어주면서 나온 나머지에 따라 string 자료형에 1 또는 0을 push_back() 해준다.
  4. reverse()를 통해 문자열을 뒤집어준다.

 

c. 문제 풀이

#include <iostream>
#include <stack>
#include <string>
#include <algorithm>

using namespace std;

void print(const string& s)
{
	for (const auto& e : s)
	{
		cout << e;
	}

	cout << endl;
}

string solution(int& input)
{
	string s;
    
	if (input == 0)
	{
		s.push_back('0');
		return s;
	}

	while (input / 2 != 0)
	{
		if (input % 2 == 1)
		{
			s.push_back('1');
			input = input / 2;
		}
		else
		{
			s.push_back('0');
			input = input / 2;
		}
	}

	s.push_back('1');
	reverse(s.begin(), s.end());

	return s;
}

int main()
{
	int input = 0;
	cout << "10진수를 입력해주세요. : ";
	cin >> input;

	print(solution(input));
}

d. 문제 후기

  • 시간복잡도 O(logN) : 주어진 숫자에 2를 계속 나눈다.
  • string에 나머지를 계속 넣어주는데, reverse를 한번 해야한다는 사실을 놓쳤다.
  • 또한 0을 입력받았을 때와 같은 예외상황들을 코드로 일일히 추가하니 길어지는듯한 느낌을 받았다.
  • 이런 문제는 Queue로도 풀어보면 괜찮을것 같다는 생각도 든다.

   3) 괄호 회전하기(링크) <-- 오답

더보기

a. 문제 설명

다음 규칙을 지키는 문자열을 올바른 괄호 문자열이라고 정의합니다.

  • (), [], {} 는 모두 올바른 괄호 문자열입니다.
  • 만약 A가 올바른 괄호 문자열이라면, (A), [A], {A} 도 올바른 괄호 문자열입니다. 예를 들어, [] 가 올바른 괄호 문자열이므로, ([]) 도 올바른 괄호 문자열입니다.
  • 만약 A, B가 올바른 괄호 문자열이라면, AB 도 올바른 괄호 문자열입니다. 예를 들어, {} 와 ([]) 가 올바른 괄호 문자열이므로, {}([]) 도 올바른 괄호 문자열입니다.
  • 대괄호, 중괄호, 그리고 소괄호로 이루어진 문자열 s가 매개변수로 주어집니다. 이 s를 왼쪽으로 x (0 ≤ x < (s의 길이)) 칸만큼 회전시켰을 때 s가 올바른 괄호 문자열이 되게 하는 x의 개수를 return 하도록 solution 함수를 완성해주세요.

대괄호, 중괄호, 그리고 소괄호로 이루어진 문자열 s가 매개변수로 주어집니다. 이 s를 왼쪽으로 x (0 ≤ x < (s의 길이)) 칸만큼 회전시켰을 때 s가 올바른 괄호 문자열이 되게 하는 x의 개수를 return 하도록 solution 함수를 완성해주세요.

 

제약 조건

  • s의 길이는 1 이상 1000이하 입니다.

 

b. 의사 코드

  1. 처음에는 "회전" 이라는 의미를 잘 몰랐다. 회전의 의미는 각도를 활용한 회전이 아니라, "ABCD" -> "BCDA" 처럼 이동하는 회전이라는 것을 이해했다.
  2. 결국 올바른 괄호쌍의 개수를 맞추는 문제이므로 string을 한번 순회하여 괄호쌍의 개수가 맞는지 먼저 확인한다.
  3. 확인되었다면 여는 괄호 '(' '{' '[' 부터 시작하는 index를 찾아준다.
  4. 반복자를 통해 적용되는 괄호쌍이 몇개인지 확인 한 후 숫자로 return 해준다. 

c. 문제 풀이(오답)

#include <string>
#include <vector>
#include <stack>

using namespace std;

int solution(string s) 
{
    int answer = 0;
    stack<char> stack;
    
    // 1. 괄호쌍을 순회하여 올바른 괄호인지 찾기
    int open_count = 0, close_count = 0;
    for(const auto& e : s)
    {
        if(e == '(' || e == '{' || e == '[' )
        {
            open_count++;
        }
        else if(e == ')' || e == '}' || e == ']' )
        {
            close_count++;
        }
    }
    
    // 괄호쌍의 개수가 맞지 않는다면 맞는 문자열은 없다.
    if(open_count != close_count) return 0; 
    
    // 2. 여는 괄호의 index를 찾아 순회한다.
    int index = 0;
    for(int i = 0; i < s.size(); i++)
    {
        if(s[i] == '(' || s[i] == '{' || s[i] == '[' )
        {
            index = i;
            break;
        }
    }
    
    // 3. 순회하여 조건에 맞는 식들을 찾는다.
    for(auto it = s.begin() + index; it != s.end(); it++)
    {
        if(*it == '(' || *it == '{' || *it == '[' )
        {
            stack.push(*it);
        }
        else
        {
            if(*it == ')' && stack.top() == '(')
            {
                stack.pop();
            }
            else if(*it == '}' && stack.top() == '{')
            {
                stack.pop();
            }
            else if(*it == ']' && stack.top() == '[')
            {
                stack.pop();
            }
            
            if(stack.empty()) answer++;
        }
    }
    for(auto it = s.begin(); it != s.begin()+index; it++)
    {
        if(*it == '(' || *it == '{' || *it == '[' )
        {
            stack.push(*it);
        }
        else
        {
            if(*it == ')' && stack.top() == '(')
            {
                stack.pop();
            }
            else if(*it == '}' && stack.top() == '{')
            {
                stack.pop();
            }
            else if(*it == ']' && stack.top() == '[')
            {
                stack.pop();
            }
            
            if(stack.empty()) answer++;
        }
    }
    
    return answer;
}

 

 

d. 문제 후기

//////////////////////////////정답코드

#include <string>
#include <vector>
#include <unordered_map>
#include <stack>

using namespace std;

unordered_map<char, char> bracketPair = {{')', '('}, {'}', '{'}, {']', '['}};

bool isValid(string& s, int start)
{
    stack<char> stk;
    unsigned int sz = s.size();
    
    for(int i = 0; i < sz; i++)
    {
        char ch = s[(start + i) % sz];
        
        if(bracketPair.count(ch))
        {
            if(stk.empty() || stk.top() != bracketPair[ch]) return false;
            stk.pop();
        }
        else
        {
            stk.push(ch);
        }
    }
    
    return stk.empty();
}

int solution(string s) 
{
    int answer = 0;
    int n = s.size();
    
    for(int i = 0; i < n; i++)
    {
        answer += isValid(s,i);
    }
    return answer;
}
  • 내가 생각한 sudo코드에는 오류가 있어 책에 있는 정답을 보고 풀었다. (정답률 56.7%)
  • 왜 오답이 나왔는지는 3.에서 stack에서 empty()를 확인하는 과정에서 하나 빼먹은 것이 있는듯 하였다.
  • 책에서는 unordered_map을 이용하여 풀었는데, unordered_map의 key값과 value를 이용하여 열린,닫힌 괄호를 표현한다는 생각이 참신해보였다.
  • 다음부터는 하나의 값에 무조건 하나의 짝이 있을 때에는 map을 활용해야겠다.

   

   4) 짝지어 제거하기(링크

더보기

a. 문제 설명

짝지어 제거하기는, 알파벳 소문자로 이루어진 문자열을 가지고 시작합니다. 먼저 문자열에서 같은 알파벳이 2개 붙어 있는 짝을 찾습니다. 그다음, 그 둘을 제거한 뒤, 앞뒤로 문자열을 이어 붙입니다. 이 과정을 반복해서 문자열을 모두 제거한다면 짝지어 제거하기가 종료됩니다. 문자열 S가 주어졌을 때, 짝지어 제거하기를 성공적으로 수행할 수 있는지 반환하는 함수를 완성해 주세요. 성공적으로 수행할 수 있으면 1을, 아닐 경우 0을 리턴해주면 됩니다.

예를 들어, 문자열 S = baabaa 라면

b aa baa → bb aa → aa →

의 순서로 문자열을 모두 제거할 수 있으므로 1을 반환합니다.

 

 

b. 의사 코드

  1. 의사코드 작성하지 않고 직관적으로 풀었습니다 ㅠㅠ..

c. 문제 풀이

#include <iostream>
#include <string>
#include <stack>
using namespace std;

int solution(string s)
{
    int answer = 1;
    stack<char> stack;
    
    stack.push(s[0]);
    
    for (int i = 1; i < s.size(); i++)
    {
        if(!stack.empty() && s[i] == stack.top())
        {
            stack.pop();
        }
        else if(stack.empty())
        {
            stack.push(s[i]);
        }
        else if(!stack.empty() && s[i] != stack.top())
        {
            stack.push(s[i]);
        }
    }
    
    if(stack.empty()) return true;
    
    return false;
}

 

 

d. 문제 후기

  • 프로그래머스 Lv.2 문제지만 stack을 사용한다면 굉장히 쉬운 문제였다.
  • if문을 많이 사용하는 것은 직관적이지 못하니, if문과 for문을 자료구조로 간략하게 나타내는 것이 포인트 인 듯 하다.

   

  5) 주식 가격(링크) <-- ⭐⭐⭐⭐⭐ 

더보기

a. 문제 설명

초 단위로 기록된 주식가격이 담긴 배열 prices가 매개변수로 주어질 때, 가격이 떨어지지 않은 기간은 몇 초인지를 return 하도록 solution 함수를 완성하세요.

 

 

b. 의사 코드

  1. 의사코드 작성하지 않고 직관적으로 풀었습니다 ㅠㅠ..

c. 문제 풀이

#include <string>
#include <iostream>
#include <stack>
#include <vector>

using namespace std;

vector<int> solution(vector<int> a) 
{    
    vector<int> answer;
    
    for(int i = 0; i < a.size() ; i++)
    {
        int cnt = 0;
        for(int j = i+1; j < a.size(); j++)
        {
            if(a[i] <= a[j])
            {
                cnt++;
            }
            else
            {
                cnt++;
                break;
            }
        }
        answer.push_back(cnt);
    }
    return answer;
}

 

 

d. 문제 후기

  • 내가 푼 답지의 시간복잡도는 O(N^2)이었고, 다른 사람들이 문제 푼 답지를 보니 stack을 사용하여 시간복잡도를O(2N)으로 푼 답지가 있었다.
  • 정답은 맞추었지만, 시간복잡도 상으로 더 빠르게 풀 수 있는 답안이 있으므로 stack을 사용한 예제를 다시한번 복기하면서 한번 더 배웠다.

   


 
6) 크레인 인형 뽑기 게임(
링크

더보기

a. 문제 설명

문제 링크 참조

 

 

b. 의사 코드

  1. 우선 인형을 담을 stack을 하나 만들어주고 moves 컨테이너를 한번씩 순회한다.
  2. stack은 중간 요소 접근을 할 수 없으므로, 현재 것을 비교하는 grab과 prev 변수를 만들어 관리한다.
  3. grab과 prev가 같다면 answer에 2를 더하고 같은 숫자의 인형들을 없애준다.
  4. 점수를 리턴한다.

c. 문제 풀이

#include <string>
#include <stack>
#include <vector>

using namespace std;

int solution(vector<vector<int>> board, vector<int> moves) 
{
    int answer = 0;
    stack<int> stack;
    
    for(int i = 0; i < moves.size(); i++)
    {
        int grab = 0;
        int prev = stack.empty() ? false : stack.top();
        for(int j = 0; j < board.size(); j++)
        {
            if(board[j][moves[i]-1] != 0)
            {
               stack.push(board[j][moves[i]-1]);
               grab = board[j][moves[i]-1];
               board[j][moves[i]-1] = 0;
               break;
            }
        }
        
        if(!stack.empty() && grab == prev)
        {
            stack.pop();
            stack.pop();
            answer+=2;
        }
    }
    return answer;
}

 

 

d. 문제 후기

  • 문제 지문은 길지만 2차원 배열과 스택에 대한 개념만 알고 있다면 비교적 쉽고 재밌는 문제였던 것 같다.
  • 이번에도 2차원 배열을 사용했기 때문에, 2중 for문을 사용하여 순회하였지만 2중 for문은 O(N^2)의 시간복잡도를 가지기 때문에 항상 유의하여 사용하여야겠다.

 


 
7) 표 편집(
링크) <-- 오답(⭐⭐⭐⭐⭐ )

더보기

a. 문제 설명

문제 링크 참조

 

 

b. 의사 코드

  1. 우선 문제에 주어진 표와 동일하게 만들어줘야 하므로, n개의 size를 가진 vector를 만들어준다. 이 때, vector에는 처음 만들어두었던 행의 숫자를 넣어준다.
  2. 되돌리기 기능을 위한 stack 컨테이너를 만들어준다.
  3. 명령어를 담은 cmd를 한번씩 순회하며 조건문을 만들어 순회해준다.
  4. 순회를 마치고 처음에 만들어두었던 vector의 행 숫자와 비교하며 O와 X를 string 에 리턴해준다.

c. 문제 풀이(오답)

#include <string>
#include <stack>
#include <vector>

using namespace std;

string solution(int n, int k, vector<string> cmd) 
{
    string answer = "";
    vector<int> vec;
    stack<pair<int,int>> stk;
    
    for(int i = 0; i < n; i++) vec.push_back(1); //기존 것과 비교할 vector
    
    for(int i = 0; i < cmd.size(); i++) //명령을 수행하는 순회
    {
        if(cmd[i][0] == 'U')
        {
            k-=stoi(cmd[i].substr(2,cmd[i].size()-2)); // 주어진 숫자만큼 올라가기
            if(k < 0) k = 0;
        }
        else if(cmd[i][0] == 'D')
        {
            k+=stoi(cmd[i].substr(2,cmd[i].size()-2)); // 주어진 숫자만큼 내려가기
            if (k > vec.size() - 1) k = vec.size() - 1;
        }
        else if(cmd[i] == "C")
        {
            if(k == vec.size()-1)
            {
                stk.push(make_pair(k,vec[k]));
                vec.erase(vec.begin()+k);
                k-=1;
            }
            else
            {
                stk.push(make_pair(k,vec[k]));
                vec.erase(vec.begin()+k); 
            }
        }
        else if(cmd[i] == "Z")
        {
            vec.insert(vec.begin()+stk.top().first,stk.top().second);
            stk.pop();
        }
    }
    
    for(int i = 0; i < vec.size(); i++)
    {
        answer.push_back('O');
    }
    for(int i = 0; i < stk.size(); i++)
    {
        answer.insert(answer.begin()+stk.top().first,'X');
        stk.pop();
    }
    
    return answer;
}

 

 

d. 문제 후기(정답)

  • 이번 문제는 1시간동안 잡고 풀어보았으나, 틀리게 되어 어떤 곳이 잘못된 것인지 잘 몰랐다.
  • 프로그래머스에서 그대로 문제를 풀자니, 디버깅이 안되어 어디서부터 잘못되었는지 확인할 수 없는게 안좋은 듯 하다.
  • 오답에 있는 문제는 1~2주 후에 다시 풀어보는게 좋을듯 하다.
  • 풀이 정답
#include <string>
#include <vector>
#include <stack>

using namespace std;

string solution(int n, int k, vector<string> cmd) {
   
   //❶삭제 된 행의 인덱스를 저장 
    stack<int> deleted;
   //❷ 각 행의 위아래에 있는 행의 인덱스를 저장 
    vector<int> up;
    vector<int> down;

    for (int i = 0; i < n + 2; i++) {
        up.push_back(i - 1);
        down.push_back(i + 1);
    }
  //❸ 임시공간을 고려한 현재위치 
  k++;

  //❹ 주어진 명령어를 순회  
  for (int i = 0; i < cmd.size(); i++) {
        //❺ 현재 위치를 삭제하고 그 다음 위치로 이동
        if (cmd[i][0] == 'C') {
            deleted.push(k);
            down[up[k]] = down[k];
            up[down[k]] = up[k];

            if (down[k] == n + 1) k = up[k];
            else k = down[k];
        }

        //❻ 가장 최근에 삭제한 행을 복원
        else if (cmd[i][0] == 'Z') {
            int r = deleted.top();
            down[up[r]] = r;
            up[down[r]] = r;
            deleted.pop();
        } 

        //❼  현재 행을 주어진 값 만큼 위혹은 아래로 이동
        else { 
            int sz = stoi(cmd[i].substr(2));
            
            if (cmd[i][0] == 'U') {
                for (int j = 0; j < sz; j++) {
                    k = up[k];
                }
            }
                  
            else if (cmd[i][0] == 'D') {
                for (int j = 0; j < sz; j++) {
                    k = down[k];
                }
            }
        }
        
    }

    string answer;
   
    //❽ 삭제된 행의 위치에 “X”, 그렇지 않은 행의 위치에 “X” 로 표시한 문자열 반환
    answer.append(n, 'O');
    while (!deleted.empty()) {
        answer[deleted.top() - 1] = 'X';
        deleted.pop();
    }

    return answer;
}

//아래 코드는 테스트 코드 입니다.
#include <iostream>

int main()
{
    
    cout << solution(8, 2, {"D 2", "C", "U 3", "C", "D 4", "C", "U 2", "Z", "Z"}) << endl;              //OOOOXOOO
    cout << solution(8, 2, {"D 2", "C", "U 3", "C", "D 4", "C", "U 2", "Z", "Z", "U 1", "C"}) << endl;  //OOXOXOOO
    return 0;
}

   

 

더 많은 스택 문제

 

 

📝내용(큐)

 

큐(Queue)

1. 정의

 

큐(Queue)는 먼저 저장된 데이터가 가장 나중에 나오는 자료구조로 FIFO(First In, First Out) 구조를 띄고 있다.
이때 스택에 삽입하는 연산을 enqueue, 꺼내는 연산을 dequeue라고 한다.

 

그런데, STL에서는 push와 pop을 그대로 사용하므로 이를 염두해두자.

 

 


 

2.  큐의 ADT

   1) push : 큐에 데이터를 넣는 동작

   2) pop : 에 데이터를 빼는 동작

   3) front : 의 맨 앞에 있는 element를 return 하는 동작

   4) rear : 큐 의 맨 뒤에 있는 element를 return 하는 동작 

   5) empty : 큐가 비었는지 확인하는 동작

   6) isfull : 큐가 꽉 찼는지 확인하는 동작

   7) size : 큐의 size를 반환한다.

   8) resize : 큐의 내부 공간을 재할당 하는 동작

   

   ADT를 바탕으로 구현한 코드

#include <iostream>

using namespace std;

template <typename T>
class MyQueue //원형 큐
{
public:
	MyQueue(int param_cap = 4)
	{
		if (param_cap > 0) resize(param_cap);
		else cout << "큐 만들기 실패" << endl;
	}

	~MyQueue()
	{
		delete[] que_;
	}

	void resize(int cap)
	{
		T* temp = new T[cap];
		int rear = 0;
		
		if (cap_ < cap)
		{
			for (int i = 1; i < cap; i++)
			{
				if (i <= cap_)
				{
					temp[i] = que_[i];
					rear++;
				}
				else temp[i] = 0;
			}
		}
		else
		{
			for (int i = 1; i < cap; i++)
			{
				temp[i] = que_[i];
				rear++;
			}
		}

		if(que_) delete[] que_;
		que_ = temp;
		cap_ = cap;
		rear_ = rear;
	}

	T& front()
	{
		return que_[front_ + 1];
	}

	T& rear()
	{
		return que_[rear_];
	}

	bool isfull()
	{
		if(rear_ > front_) return (cap_ - 1) == rear_ ? true : false;
		else return front_ - 1 == rear_ ? true : false;
	}

	bool empty()
	{
		return rear_ == front_ ? true : false;
	}

	void push(const T& param)
	{
		if (this->isfull()) resize(cap_ * 2);
		rear_++;
		que_[rear_] = param;
	}

	void pop()
	{
		if (this->empty())
		{
			cout << "비었습니다!" << endl;

		}
		else
		{
			if (front_ == cap_ - 1) front_ = 0;
			else front_++;
		}
	}

	int size()
	{
		if (rear_ > front_)
		{
			return rear_ - front_;
		}
		else if (rear_ == front_)
		{
			return 0;
		}
		else if (rear_ < front_)
		{
			return rear + (cap_ - front_ - 1);
		}
	}

public:
	int front_ = 0;
	int rear_ = 0;
	int cap_ = 0; // Queue의 최대 사이즈는 Cap-1
	T* que_;
};

int main()
{
	MyQueue<int> que;

	que.push(1);
	que.push(2);
	que.push(3);

	cout << que.front() << endl;
	cout << que.rear() << endl;

	que.pop();

	cout << que.front() << endl;
}

 

 


3.  문제 풀이

   1) 요세푸스 문제 (교재 239P 참조)

더보기

a. 문제 설명

N명의 사람이 원 형태로 서 있습니다. 각 사람은 1부터 N까지 번호표를 갖고 있습니다. 그리고 임의의 숫자 K가 주어졌을 때, 다음과 같이 사람을 없앱니다.

 

  • 1번 번호표를 가진 사람 기준 시계 방향으로 K번째 사람을 없앱니다.
  • 없앤 사람 다음 사람을 기준으로 하고 다시 K번째 사람을 없앱니다.

N과 K가 주어질 때 마지막에 살아 있는 사람의 번호를 반환하는 solution() 함수를 구현해주세요.

 

 

 

b. 의사 코드

  1. 먼저 Queue를 만들어 1,2,3,4,5를 밀어넣는다.
  2. Queue의 내부에 있는 개수가 1이 될 때까지, 무언가를 한다.
  3. 그 무언가는 맨 앞에 있는 숫자를 1개씩 빼면서, 2번째가 아니라면 다시 밀어넣고 2번째가 되는 순간 뺀다.
  4. Queue가 단 1개가 되면 이 값을 리턴한다.

 

c. 문제 풀이

#include<iostream>
#include<queue>

using namespace std;

int solution(int n, int k)
{
    int answer = 0;
    queue<int> que;
    
    for(int i = 1; i <= n; i++)
    {
        que.push(i);
    }
    
    int cnt = 0;
    while(que.size() != 1)
    {
        cnt++;
        int temp = que.front();
        que.pop();
        if(cnt == k)
        {
            cnt = 0;
        }
        else
        {
            que.push(temp);
        }
    }

    return que.front();
}

int main()
{
    int n = 5, k = 2;
    
    cout << "마지막 남는 수는?" << endl;
    
    cout << solution(n,k);
}

 

d. 문제 후기

  • 시간복잡도 O(N) : 주어진 숫자를 반복 순회한다.
  • pop()과 front()를 잘 활용해야 한다. 내가 뺀 숫자를 다시 집어넣는다 라는 발상은 나는 push(pop())으로 하려 했으나, pop()은 리턴값이 없으므로 temporary 변수를 하나 만들어 따로 저장해두어야 하는걸 잊지말자.

 

 

 

   2) 기능 개발(링크

더보기

a. 문제 설명


문제 링크 참조

 

 

b. 의사 코드

  1. 먼저 progresses와 speeds가 어떤 동작을 하는지 이해한다.
  2. 주어진 작업들을 pair클래스를 이용하여 하나의 Queue에 밀어 넣어준다.
  3. Queue가 빌 때 까지, 무언가를 한다.
  4. 그 무언가는 cnt라는 변수를 이용하여, 작업 진행된지 몇 초가 지났는지 구현하고 첫번째 작업이 끝날때까지 계속 cnt변수를 올려 흘러가는 시간을 구현한다.
  5. 만약, 대기중인 작업(첫번째 작업)이 끝났으면 work라는 변수를 하나 만들어 이후 대기중인 작업도 이미 끝나있으면 work의 수 만큼 어딘가에 밀어 넣는다.
  6. 그 어딘가는 answer 벡터이다.

 

c. 문제 풀이

#include <iostream>
#include <vector>
#include <queue>

using namespace std;

vector<int> solution(vector<int> progresses, vector<int> speeds)
{
    vector<int> answer;
    queue<pair<int,int>> que;
    
    int cnt = 0;
    int work = 0;
    
    for(int i = 0; i < progresses.size(); i++)
    {
        que.push(make_pair(progresses[i],speeds[i]));
    }
    
    while(!que.empty())
    {
        if((que.front().first + que.front().second * cnt) >= 100 )
        {
            work++;
            que.pop();
            continue;
        }
        
        cnt++;
        
        if(work > 0)
        {
            answer.push_back(work);
            work = 0;
        }
    }
    
    if(work > 0) answer.push_back(work);
    work = 0;
    
    return answer;
}

 

d. 문제 후기

  • 시간복잡도 O(N) : 주어진 문자열을 무작위의 숫자동안 반복 순회한다.
  • 예제에 주어진  progresses 와 speed 처럼 쌍으로 주어지는 변수가 있다면, 일단 pair 클래스로 묶어 두는 것이 가독성이 참 좋은것 같다고 느꼈다.

 


   3) 카드 뭉치(링크

더보기

a. 문제 설명


문제 링크 참조

 

 

b. 의사 코드

  1. 일단 goal을 순회한다.
  2. card1과 card2는 순차적으로 처리해야 하므로, vector가 아닌 queue로 초기화 해준다.
  3. goal을 순회할 때, card1과 card2에 똑같은 문자열이 있는지 조건문을 걸어 비교한다.
  4. 계속 진행하다가 문장을 만들 수 없다면 no를 리턴한다.
  5. 계속 진행이 되어 문장을 만들었다면 yes를 리턴한다.

 

c. 문제 풀이

#include <string>
#include <queue>
#include <vector>

using namespace std;

string solution(vector<string> cards1, vector<string> cards2, vector<string> goal) 
{
    string answer = "";
    queue<string> que1;
    queue<string> que2;
    
    for(auto& e : cards1) que1.push(e);
    for(auto& e : cards2) que2.push(e);

    for(int i = 0; i < goal.size(); i++)
    {
        if(goal[i] == que1.front())
        {
            que1.pop();
        }
        else if(goal[i] == que2.front())
        {
            que2.pop();
        }
        else if(goal[i] != que1.front() && goal[i] != que2.front())
        {
            return "No";
        }
    }
    return "Yes";
}

 

d. 문제 후기

  • 시간복잡도 O(N) : 주어진 vector<string>을 Queue의 size만큼 순회한다.
  • vector<string> 을 queue<string>으로 복사하는 과정에서 순회를 한번씩 해야하기 때문에 이 과정이 뭔가 부자연스럽게 느껴졌습니다.

 

   4) 영어 끝말잇기(링크) <-- 오답(90%)

더보기


a. 문제 설명


문제 링크 참조

 

 

b. 의사 코드

  1. 먼저 words를 순회한다.
  2. 순회하면서, 이전 단어를 수집할 vector<string> 자료형을 만들어준다.
  3. 이전단어를 수집하며, 이전에 나왔던 단어인지, 끝말잇기의 조건에 부합하는지 확인한다.
  4. 만약 조건과 틀릴 경우 num 변수를 하나 선언하여 몇번째에 틀렸는지 기록해둔다.
  5. 기록된 숫자를 바탕으로, 번호와 차례수를 기록하거나 0을 리턴한다.

 

c. 문제 풀이

#include <string>
#include <vector>
#include <iostream>

using namespace std;

vector<int> solution(int n, vector<string> words)
{
    vector<int> answer;
    vector<string> prev_words;

    int num = 0;

    for (int i = 0; i < words.size(); i++)
    {
        for (int j = 0; j < prev_words.size(); j++) //이전 단어가 나왔는지 확인
        {
            if (words[i] == prev_words[j])
            {
                num = i;
                break;
            }
        }
        
        if(!prev_words.empty() && prev_words.back().back() != words[i].front()) //이전 맨 끝글자와, 첫번째 
        {
            num = i;
            break;
        }
        
        prev_words.push_back(words[i]);
    }

    if (num == 0)
    {
        answer.push_back(0);
        answer.push_back(0);
    }
    else
    {
        answer.push_back((num % n) + 1);
        answer.push_back(num / n + 1);
    }

    return answer;
}

 

d. 문제 후기

  • 시간복잡도 O(n(n-1)) : 주어진 문자열을 순회하며, 이전 것과 계속 비교한다.
  • 정답률 90%라서 굉장히 아깝다고 생각되는 문제이다. 아직 해결방법을 찾진 않았지만 아마 스택/큐를 사용하지 않아 발생한 문제라고 생각되며, 다른 분들의 풀이를 보면서 1주 뒤에 또 풀 계획이다.

 

 

 

 

해당 포스팅은 "코딩 테스트 합격자 되기 C++편" 의 책 및 강의를 보며 포스팅 한 내용입니다. 

 

 

 

[지금 무료] 코딩 테스트 합격자 되기 - C++ 강의 | dremdeveloper - 인프런

dremdeveloper | 코딩 테스트 합격을 위한 C++ 강의, 책 없이도 가능! 저자와 직접 소통 가능한 커뮤니티 제공!, [사진]여기에 문의 하세요https://open.kakao.com/o/gX0WnTCf📘 코딩 테스트 합격자 되기 - C++편

www.inflearn.com

 

 

 

💬키워드

알고리즘 : 입력값이 주어졌을 때, 출력값으로 도출하기 위한 모든 문제 해결 방법을 뜻함.정렬 : 주어진 배열을 알맞는 규칙에 맞게 재배치하는 행위 (주로 오름차순이 Default)이진,이분탐색 : 정렬된 배열에서 분할정복의 방법으로 특정 자료를 찾는 행위선형탐색 : 정렬되지 않은 배열에서 특정 자료를 찾는 행위

순열: 주어진 원소들을 모든 가능한 방법으로 배열한 것을 말한다. 방법의 수는 n!로 표현된다.

 

 

🪄목차

 

https://www.youtube.com/watch?v=xc0HZiqh8Fs

 

1. 알고리즘 -> count

2. 알고리즘 -> sort

3. 알고리즘 -> unique

4. 알고리즘 -> binary_search

5. 알고리즘 -> max_element(min_element)

6. 알고리즘 -> next(prev)_permutation

📝내용

 

 

알고리즘(Algorithm)

1. 정의

-  알고리즘은 문제를 해결하기 위한 방법을 명확하게 설계하고 순서대로 나열하는 것.

-  #include <algorithm> 헤더를 추가하면 사용할 수 있음.

-  STL에서 제공되는 알고리즘의 경우 주로 반복자(iterator)를 통하여 사용 가능하다.

 

2. 사용법

#include <iostream>
#include <vector>
#include <algorithm> // 카운트 라는 함수를 사용하기 위해서 헤더를 추가해 준다.
using namespace std;

int main() {
    vector<int> v = {1, 2, 1, 1, 1, 1, 2, 1};

    int count_of_1 = count(v.begin(), v.end(), 1);
    cout << "벡터에서 1의 개수: " << count_of_1 << endl;

    // 출력값:
    // 벡터에서 1의 개수: 6

    return 0;
}

 

count

1. 정의

- 데이터를 세는 함수

- 특정 값의 출현 횟수를 반환(return)

동작 예시코드 인자 설명 시간
복잡도
특정 값의 출현 횟수 count(v.begin(),v.end(),value) 정렬의 시작 반복자
정렬의 끝 반복자
찾고자 하는 값
O(N)

 

2. 사용예제

//############################################################
// | cafe       | http://cafe.naver.com/dremdelover          |
// | Q&A        | https://open.kakao.com/o/gX0WnTCf          |
// | business   | ultrasuperrok@gmail.com                    |
//############################################################
#include <iostream>
#include <vector>
#include <algorithm> // for std::count
using namespace std;

int main() {
    // 예시 벡터 생성
    vector<int> v = {1, 2, 3, 4, 5, 1, 2, 1};

    // count 함수 사용 예시
    // std::count 함수는 주어진 범위에서 특정 값이 몇 번 나타나는지 센다.
    // 여기서 v.begin()과 v.end()는 벡터의 시작과 끝을 나타내며,
    // 1은 찾고자 하는 값이다.
    int count_of_1 = count(v.begin(), v.end(), 1);

    // 결과 출력
    cout << "벡터에서 1의 개수: " << count_of_1 << endl;

    // 출력값:
    // 벡터에서 1의 개수: 3

    // 시간 복잡도:
    // std::count 함수의 시간 복잡도는 O(N)이다.
    // 여기서 N은 주어진 범위의 요소 수를 나타낸다.

    // count 함수를 사용해야 하는 경우
    // 데이터 집합에서 특정 값의 출현 빈도를 알고 싶을 때 유용하다.
    // 예를 들어, 설문조사 결과에서 특정 답변이 몇 번 나왔는지 확인할 때 사용할 수 있다.

    return 0;
}

 


 

sort

1. 정의

- 데이터를 정렬하는 함수

- 정렬 기준을 전달하지 않으면 오름차순으로 동작

- 사용자 정의형의 경우 무조건 정렬 기준을 전달 해야 함.

동작 예시코드 인자 설명 시간
복잡도
특정 범위를 오름차순으로 정렬 sort(v.begin(),v.end()) 정렬의 시작 반복자
정렬의 끝 반복자
O(NlogN)
특정 범위를 사용자가 정의한 기준으로 정렬 sort(v.begin(),v.end().compare) 정렬의 시작 반복자
정렬의 끝 반복자
정렬 기준
O(NlogN)

 

2. 사용예제

//############################################################
// | cafe       | http://cafe.naver.com/dremdelover          |
// | Q&A        | https://open.kakao.com/o/gX0WnTCf          |
// | business   | ultrasuperrok@gmail.com                    |
//############################################################
#include <iostream>
#include <vector>
#include <algorithm> // sort() 함수가 포함된 헤더

using namespace std; // std 네임스페이스를 사용

// 사용자 정의 비교 함수
bool compare(int a, int b) {
    // 내림차순 정렬을 위한 비교: a가 b보다 클 때 true를 반환
    // sort 함수가 비교 시 compare(a, b)가 true를 반환하면 a가 b보다 앞에 있어야 한다고 판단하여 a와 b의 위치를 교환하지 않음
    return a > b;
}

int main() {
    // 정렬할 벡터 생성
    vector<int> v = {5, 2, 9, 1, 5, 6};

    // 벡터를 오름차순으로 정렬
    // std::sort는 기본적으로 < 연산자를 사용하여 정렬
    // v.begin()은 첫 번째 요소, v.end()는 마지막 요소의 다음 위치를 가리킴 (v.end()는 정렬 대상에 포함되지 않음)
    sort(v.begin(), v.end());

    // 오름차순으로 정렬된 벡터 출력
    cout << "오름차순 정렬: ";
    for (int n : v) {
        cout << n << ' ';
    }
    cout << endl;
    // 출력값: 1 2 5 5 6 9

    // 벡터를 다시 섞어서 초기 상태로 되돌림
    v = {5, 2, 9, 1, 5, 6};

    // 벡터를 사용자 정의 비교 함수로 정렬 (내림차순)
    // 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);

    // 부분 정렬된 벡터 출력
    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;
}


unique

1. 정의

- 인접한 중복 요소를 뒤로 재배치하는 함수

- 중복되지 않는 범위의 끝을 나타내는 반복자를 반환함

- 함수 사용 후 "중복 요소가 제거된 것이 아님" 완전히 제거하려면 erase 함수 추가 사용

- 정렬된 상태에서만 모든 중복요소를 제거할 수 있음.

동작 예시코드 인자 설명 시간
복잡도
인접 중복 재배치 sort(v.begin(),v.end()) 정렬의 시작 반복자
정렬의 끝 반복자
O(N)
인접 중복 삭제 auto it = unique(v.begin(), v.end())
v.erase(it,v.end())
it은 unique에서 반환한 새로운 끝 반복자 O(N)

 

 

 

2. 사용예제

//############################################################
// | cafe       | http://cafe.naver.com/dremdelover          |
// | Q&A        | https://open.kakao.com/o/gX0WnTCf          |
// | business   | ultrasuperrok@gmail.com                    |
//############################################################
#include <iostream>
#include <vector>
#include <algorithm> // 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<int> 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<int> 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
*/

 


binary_search

1. 정의

- 정렬된 범위에서 특정 값을 찾는 함수

- 값이 존재하면 true를, 존재하지 않으면 false를 반환함

- 이진탐색을 사용하므로 O(logN)을 보장

- 정렬된 상태에서만 찾을 수 있음.

동작 예시코드 인자 설명 시간
복잡도
값 찾기 binary_search
(v.begin(),v.end(), value)
정렬의 시작 반복자
정렬의 끝 반복자
찾을 값
O(logN)

 

 

 

2. 사용예제

//############################################################
// | cafe       | http://cafe.naver.com/dremdelover          |
// | Q&A        | https://open.kakao.com/o/gX0WnTCf          |
// | business   | ultrasuperrok@gmail.com                    |
//############################################################
#include <iostream>
#include <vector>
#include <algorithm> // for std::binary_search and std::sort
using namespace std;

int main() {
    // 예시 벡터 생성
    vector<int> v = {1, 3, 4, 5, 7, 9, 10};

    // 찾고자 하는 값
    int value1 = 5;
    int value2 = 6;

    // 이진 탐색 사용 예시
    bool found1 = binary_search(v.begin(), v.end(), value1);
    bool found2 = binary_search(v.begin(), v.end(), value2);

    // 결과 출력
    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를 반환합니다.
*/

 

 


 


max(min)_element

1. 정의

- 범위 내에서 가장 큰(작은) 원소를 찾는 함수

- 반복자 범위에서 가장 큰 원소를 가리키는 반복자를 반환함

- 선형 탐색을 사용하므로 O(N)

동작 예시코드 인자 설명 시간
복잡도
최대값 찾기 max_element(v.begin(),v.end()) 정렬의 시작 반복자
정렬의 끝 반복자
O(N)
최소값 찾기  min_element(v.begin(),v.end())  정렬의 시작 반복자
정렬의 끝 반복자
O(N)

 

 

 

2. 사용예제

//############################################################
// | cafe       | http://cafe.naver.com/dremdelover          |
// | Q&A        | https://open.kakao.com/o/gX0WnTCf          |
// | business   | ultrasuperrok@gmail.com                    |
//############################################################
#include <iostream>
#include <vector>
#include <algorithm> // for std::max_element
using namespace std;

int main() {
    // 예시 벡터 생성
    vector<int> v = {1, 3, 4, 5, 7, 9, 10};

    // 최대값 찾기
    auto max_it = max_element(v.begin(), v.end());

    // 결과 출력
    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을 가리킵니다.
*/

 

//############################################################
// | cafe       | http://cafe.naver.com/dremdelover          |
// | Q&A        | https://open.kakao.com/o/gX0WnTCf          |
// | business   | ultrasuperrok@gmail.com                    |
//############################################################
#include <iostream>
#include <vector>
#include <algorithm> // for std::min_element
using namespace std;

int main() {
    // 예시 벡터 생성
    vector<int> v = {1, 3, 4, 5, 7, 9, 10};

    // 최소값 찾기
    auto min_it = min_element(v.begin(), v.end());

    // 결과 출력
    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을 가리킵니다.
*/

 

 

 



next(prev)_permutation

1. 정의

- 주어진 범위의 요소들에 대해 다음 순열을 생성

- 순열이 더이상 없으면 false, 그렇지 않으면 true

- 모든 순열을 생성하기 위해서는 정렬되어 있어야 함.

- 시간 복잡도는 O(N*N!)

동작 예시코드 인자 설명 시간
복잡도
다음 순열 생성 후
true/false 반환
next_permutation(v.begin(),v.end()) 정렬의 시작 반복자
정렬의 끝 반복자
O(N*N!)
이전 순열 생성 후
true/false 반환
prev_permutation(v.begin(),v.end()) 정렬의 시작 반복자
정렬의 끝 반복자
O(N*N!)

 

 

 

2. 사용예제

//############################################################
// | cafe       | http://cafe.naver.com/dremdelover          |
// | Q&A        | https://open.kakao.com/o/gX0WnTCf          |
// | business   | ultrasuperrok@gmail.com                    |
//############################################################
#include <iostream>
#include <vector>
#include <algorithm> // for std::next_permutation and std::sort
using namespace std;

// 벡터의 모든 순열을 출력하는 함수
void print_permutations(vector<int> v) {
    do {
        // 현재 순열 출력
        for (int num : v) {
            cout << num << " ";
        }
        cout << endl;
    } while (next_permutation(v.begin(), v.end()));
}

int main() {
    // 예시 벡터 생성
    vector<int> v = {3, 2, 1};

    // 정렬되지 않은 경우
    cout << "정렬되지 않은 경우:\n";
    vector<int> v_unsorted = v;
    print_permutations(v_unsorted);

    // 벡터를 정렬된 상태로 만들기
    sort(v.begin(), v.end());

    // 정렬된 경우
    cout << "\n정렬된 경우:\n";
    vector<int> 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}로 재설정됩니다.
*/

 


 

 

 

 

해당 포스팅은 "코딩 테스트 합격자 되기 C++편" 의 책 및 강의를 보며 포스팅 한 내용입니다. 

 

 

 

[지금 무료] 코딩 테스트 합격자 되기 - C++ 강의 | dremdeveloper - 인프런

dremdeveloper | 코딩 테스트 합격을 위한 C++ 강의, 책 없이도 가능! 저자와 직접 소통 가능한 커뮤니티 제공!, [사진]여기에 문의 하세요https://open.kakao.com/o/gX0WnTCf📘 코딩 테스트 합격자 되기 - C++편

www.inflearn.com

 

 

 

💬키워드

스택 : 가장 마지막에 추가한 항목이 가장 먼저 제거되는, 데이터가 쌓이는 구조의 컨테이너

큐 : 가장 마지막에 추가한 항목이 가장 마지막에 제거되는, 선착순 형태의 컨테이너

 

 

🪄목차

 

https://www.youtube.com/watch?v=xc0HZiqh8Fs

 

1. 스택(Stack)

2. 큐(Queue)

 

📝내용

 

스택(Stack)

1. 정의

- 중복을 허용하는 순서가 있는 컨테이너 자료구조

- LIFO(Last In First Out), 최근에 들어온 원소가 먼저 삭제됨, 후입선출

- 삽입/삭제 : O(1)

- 탐색 및 임의 접근은 불가능하다.

 

출처 :  https://www.youtube.com/watch?v=agQQyKd0HBQ



 

 

 

 

2. 사용법

//############################################################
// | cafe       | http://cafe.naver.com/dremdelover          |
// | Q&A        | https://open.kakao.com/o/gX0WnTCf          |
// | business   | ultrasuperrok@gmail.com                    |
//############################################################
#include <iostream>
#include <stack>

using namespace std;

int main() {
    stack<int> s;
    
    // 원소 삽입
    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: 스택의 맨 위 원소를 반환합니다. 스택이 비어 있으면 예외를 던집니다. 예를 들어, 현재 스택의 상태는 [1, 2, 3]이므로 top()은 3을 반환합니다. 시간복잡도: O(1)
    
    // 원소 삭제
    s.pop();  // pop: 스택의 맨 위 원소를 제거합니다. 스택이 비어 있으면 예외를 던집니다. 예를 들어, 현재 스택의 상태는 [1, 2, 3]이므로 pop()은 3을 제거하여 [1, 2]가 됩니다. 시간복잡도: O(1)
    cout << "Top element after pop: " << s.top() << endl; // 출력: 2
    // top: 스택의 맨 위 원소를 반환합니다. 스택이 비어 있으면 예외를 던집니다. 예를 들어, 현재 스택의 상태는 [1, 2]이므로 top()은 2를 반환합니다. 시간복잡도: O(1)
    
    // 스택이 비어있는지 확인
    if (!s.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)

    // 스택에서 모든 원소를 pop하여 출력
    while (!s.empty()) {
        cout << "Popping element: " << s.top() << endl;
        s.pop();
    }
    // 반복문을 통해 모든 원소를 제거합니다. 예를 들어, 현재 스택의 상태는 [1, 2]이므로 pop()은 2와 1을 순서대로 제거합니다. 시간복잡도: O(1)

    // 스택이 비어있는지 확인
    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: 스택의 맨 위 원소를 반환합니다. 스택이 비어 있으면 예외를 던집니다.
*   예를 들어, 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) 구조가 필요한 경우 (큐 사용 권장)
*/

 


 

큐(Queue)

1. 정의

- 중복을 허용하는 순서가 있는 선형 데이터 구조

- FIFO(First In First Out), 먼저 들어온 원소가 먼저 삭제됨(선착순)

- 삽입/삭제 : O(1)

- 탐색 및 임의 접근 불가능

 

출처 :  https://www.youtube.com/watch?v=agQQyKd0HBQ



2. 사용법

//############################################################
// | cafe       | http://cafe.naver.com/dremdelover          |
// | Q&A        | https://open.kakao.com/o/gX0WnTCf          |
// | business   | ultrasuperrok@gmail.com                    |
//############################################################
#include <iostream>
#include <queue>

using namespace std;

// std::queue는 C++ STL에 포함된 컨테이너 어댑터입니다.
// - FIFO(First-In-First-Out) 원칙에 따라 동작하는 데이터 구조입니다.
// 좋은 사용 시기:
// - 데이터를 순서대로 처리해야 할 때, 예를 들면, 너비 우선 탐색(BFS) 알고리즘, 대기열 구현 등.
// 성능 이슈:
// - STL queue는 일반적으로 빠른 연산을 제공합니다. 하지만, 중간 요소에 직접 접근할 수 없습니다. 중간 요소를 검색하거나 수정하려면 다른 자료구조를 사용하는 것이 좋습니다.

int main() {

    queue<int> q;

    // push: 큐의 끝에 요소 추가, O(1)
    q.push(1);
    q.push(2);
    q.push(3);

    // front: 큐의 첫 번째 요소에 접근, O(1)
    cout << "Front element: " << q.front() << endl;  // 출력: Front element: 1

    // pop: 큐의 첫 번째 요소 제거, O(1)
    q.pop();
    cout << "Front element after pop: " << q.front() << endl;  // 출력: Front element after pop: 2

    // empty: 큐가 비어 있는지 확인, O(1)
    if (!q.empty()) {
        cout << "Queue is not empty" << endl;  // 출력: Queue is not empty
    }

    // size: 큐의 크기 확인, O(1)
    cout << "Queue size: " << q.size() << endl;  // 출력: Queue size: 2

    // 성능 저하 예제:
    // queue는 중간 요소에 직접 접근할 수 없으므로, 중간 요소를 검색하거나 수정하는 연산이 필요한 경우 
    // queue보다는 다른 자료 구조를 사용하는 것이 좋습니다.
   
    return 0;

}

 

 

 

 

 

해당 포스팅은 "코딩 테스트 합격자 되기 C++편" 의 책 및 강의를 보며 포스팅 한 내용입니다. 

 

 

 

[지금 무료] 코딩 테스트 합격자 되기 - C++ 강의 | dremdeveloper - 인프런

dremdeveloper | 코딩 테스트 합격을 위한 C++ 강의, 책 없이도 가능! 저자와 직접 소통 가능한 커뮤니티 제공!, [사진]여기에 문의 하세요https://open.kakao.com/o/gX0WnTCf📘 코딩 테스트 합격자 되기 - C++편

www.inflearn.com

 

 

map 과 set에 대한 내용은 이전 포스팅을 참조해주세요

 

[코딩 테스트 합격자 되기] C++ - STL 반복자, 컨테이너(vector,set,map)

해당 포스팅은 "코딩 테스트 합격자 되기 C++편" 의 책 및 강의를 보며 포스팅 한 내용입니다.    [지금 무료] 코딩 테스트 합격자 되기 - C++ 강의 | dremdeveloper - 인프런dremdeveloper | 코딩 테스트 합

jungamedev.tistory.com

 

 

 

💬키워드

STL(Standard Template Library) : C++에서 제공하는 템플릿 기반의 표준 라이브러리

컨테이너(Container) : 데이터를 저장하고 관리하는 클래스 템플릿

반복자(iterator) : 컨테이너의 요소를 순회할 수 있는 객체로서, 일반적으로 포인터와 유사한 개념

알고리즘(Algorithm) : 컨테이너에 저장된 데이터를 처리하고 조작하는 일련의 기능을 제공하는 함수 객체

 

 

🪄목차

 

https://www.youtube.com/watch?v=xc0HZiqh8Fs

 

1. unordered_map

2. unordered_set

 

📝내용

 

unordered_map

1. 정의

- 자동으로 정렬해주지 않는 map

- 키는 중복을 허용하지 않음.

- 원소가 해시 테이블로 관리 됨(자동 정렬되지 않음)

- 삽입/삭제/탐색 : 평균적으로 O(1), 최악 O(N)  ... 보통의 경우에는 O(1)

 

 

     해시 테이블에 대해서 잘 모르겠다면?

더보기

https://www.youtube.com/watch?v=HraOg7W3VAM

개발자라면 꼭 알아야 할 Hash Table의 모든 것 - 노마드코더

 

https://www.youtube.com/watch?v=X9Ty-FmHWqY

C++ STL, 해시 맵, unordered map  -  코드없는 프로그래밍

 

 

      map과 unordered_map의 차이점

map unorder_map
균형이진탐색트리로 구성 해시 테이블로 구성
자동 정렬O 자동 정렬X
삽입/삭제/탐색 : O(logN) 삽입/삭제/탐색 : O(1), 최악 O(N)

 

2. 사용법

//############################################################
// | cafe       | http://cafe.naver.com/dremdelover          |
// | Q&A        | https://open.kakao.com/o/gX0WnTCf          |
// | business   | ultrasuperrok@gmail.com                    |
//############################################################
#include <iostream>
#include <unordered_map>
#include <string>

using namespace std;

int main() {
    // unordered_map 컨테이너 설명
    // - unordered_map은 키와 값의 쌍으로 이루어진 순서가 없는 집합입니다.
    // - 키는 중복을 허용하지 않으며, 삽입되는 원소는 해시 테이블로 관리됩니다.
    // - 삽입, 삭제, 탐색 등의 주요 연산은 평균 O(1)의 시간복잡도를 가집니다.
    // - 최악의 경우, 해시 충돌로 인해 시간복잡도가 O(N)이 될 수 있습니다.

    // unordered_map을 사용해야 하는 경우:
    // - 키와 값의 쌍을 효율적으로 저장하고 관리해야 할 때.
    // - 키의 순서가 중요하지 않을 때.
    // - 평균 O(1)의 시간복잡도를 갖는 빠른 삽입, 삭제, 탐색이 필요할 때.

    // unordered_map을 사용하지 말아야 하는 경우:
    // - 키의 순서가 중요할 때 (이 경우 map을 사용).
    // - 해시 함수가 비효율적으로 동작하여 충돌이 많이 발생할 경우.
    // - 데이터의 크기가 매우 크고, 메모리 사용이 중요한 경우 (해시 테이블은 메모리 사용량이 많을 수 있음).

    // unordered_map 컨테이너 선언
    // key: int (학생 ID), value: string (학생 이름)
    unordered_map<int, string> studentMap;

    // 삽입: 학생 ID와 이름을 맵에 추가
    // insert 함수
    // 인자: 삽입할 키와 값 쌍 (key, value)
    // 동작: 키가 존재하지 않으면 삽입, 존재하면 값을 업데이트
    // 시간복잡도: 평균 O(1), 최악 O(N)
    studentMap.insert({101, "Alice"});
    studentMap.insert({102, "Bob"});
    studentMap.insert({103, "Charlie"});

    // 맵의 모든 요소 출력
    cout << "Initial unordered_map content:\n";
    for (const auto& pair : studentMap) {
        cout << "ID: " << pair.first << ", Name: " << pair.second << endl;
    }

    // 출력값:
    // 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;
    }

    // 출력값:
    // After updating ID 102:
    // ID: 101, Name: Alice
    // ID: 102, Name: Bobby
    // ID: 103, Name: Charlie

    // 삭제: 특정 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;
    }

    // 출력값:
    // 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;
}

 


 

unordered_set

1. 정의

- 중복을 허용하지 않는 집합(자동 정렬을 안해줌)

- 원소가 해시 테이블로 관리 됨

- 삽입/삭제/탐색 : 평균적으로 O(1), 최악 O(N)

 

      unordered_set과 해시 테이블에 대해서 잘 모르겠다면?

더보기

https://www.youtube.com/watch?v=MsDz50o3jHY

STL 해시 셋, std::unordered_set  -  코드없는 프로그래밍

 

2. 사용법

//############################################################
// | cafe       | http://cafe.naver.com/dremdelover          |
// | Q&A        | https://open.kakao.com/o/gX0WnTCf          |
// | business   | ultrasuperrok@gmail.com                    |
//############################################################
#include <iostream>
#include <unordered_set>

using namespace std;

int main() {
    // unordered_set 컨테이너 설명
    // - unordered_set은 중복을 허용하지 않는 순서가 없는 집합입니다.
    // - 원소는 해시 테이블로 관리되며, 자동으로 정렬되지 않습니다.
    // - 삽입, 삭제, 탐색 등의 주요 연산은 평균 O(1)의 시간복잡도를 가집니다.
    // - 최악의 경우, 해시 충돌로 인해 시간복잡도가 O(N)이 될 수 있습니다.

    // unordered_set을 사용해야 하는 경우:
    // - 중복되지 않는 값의 집합을 효율적으로 저장하고 관리해야 할 때.
    // - 원소의 순서가 중요하지 않을 때.
    // - 평균 O(1)의 시간복잡도를 갖는 빠른 삽입, 삭제, 탐색이 필요할 때.

    // unordered_set을 사용하지 말아야 하는 경우:
    // - 원소의 순서가 중요할 때 (이 경우 set을 사용).
    // - 해시 함수가 비효율적으로 동작하여 충돌이 많이 발생할 경우.
    // - 데이터의 크기가 매우 크고, 메모리 사용이 중요한 경우 (해시 테이블은 메모리 사용량이 많을 수 있음).

    // unordered_set 컨테이너 선언
    // value: int (학생 ID)
    unordered_set<int> studentSet;

    // 삽입: 학생 ID를 셋에 추가
    // insert 함수
    // 인자: 삽입할 값 (value)
    // 동작: 값이 존재하지 않으면 삽입
    // 시간복잡도: 평균 O(1), 최악 O(N)
    studentSet.insert(101);
    studentSet.insert(102);
    studentSet.insert(103);

    // 셋의 모든 요소 출력
    cout << "Initial unordered_set content:\n";
    for (const auto& value : studentSet) {
        cout << "ID: " << value << endl;
    }

    // 출력값:
    // Initial unordered_set content:
    // ID: 101
    // ID: 102
    // ID: 103

    // 탐색: 특정 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";
    }

    // 출력값:
    // 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;
    }

    // 출력값:
    // After erasing ID 101:
    // ID: 102
    // ID: 103

    // [] 연산자는 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";
    }

    // 출력값:
    // Student with ID 103 found: 103

    return 0;
}

 

 

 

 

 

 

+ Recent posts