Home

PS Thread

6 postsfilter: #cpp

leetcode에서 cpp 사용 기초 - vector활용

3 min#ps#leetcode#cpp단일 페이지
class Solution {
public:
    int di[4] = {-1, 0, 1, 0};
    int dj[4] = {0, 1, 0, -1};
    int n, m;
    
    vector<vector<bool>> visited;

    bool containsCycle(vector<vector<char>>& grid) {
        n = grid.size();
        m = grid[0].size();
        visited.assign(n, vector<bool>(m, false)); // Arrays.fill이랑 똑같음

        for (int i = 0; i < n; i++) {
            for (int j = 0; j < m; j++) {
                if (!visited[i][j]) {
                    if (go(i, j, -1, -1, grid[i][j], grid)) {
                        return true;
                    }
                }
            }
        }
        return false;
    }

    bool go(int i, int j, int pi, int pj, char color, vector<vector<char>>& grid) {
        visited[i][j] = true;

        for (int k = 0; k < 4; k++) {
            int ni = i + di[k];
            int nj = j + dj[k];

            if (isValid(ni, nj, color, grid)) {
                // 만약 다음 칸이 이미 방문한 곳인데, 직전 칸이 아니면
                if (visited[ni][nj]) {
                    if (ni != pi || nj != pj) {
                        return true;
                    }
                } else {
                    if (go(ni, nj, i, j, color, grid)) {
                        return true;
                    }
                }
            }
        }
        return false;
    }

    bool isValid(int ni, int nj, char c, vector<vector<char>>& grid) {
        return ni >= 0 && ni < n && nj >= 0 && nj < m && grid[ni][nj] == c;
    }
};

vector를 좀 더 잘 활용할 필요가있다.

자바의 ArrayList쓰듯이 해보자.

vector는 뭐냐

vector는 C++ STL에서 제공하는 동적 배열이다.

자바로 치면 ArrayList랑 제일 비슷하다. 크기가 고정된 배열이 아니라, 원소를 뒤에 계속 추가할 수 있고 인덱스로 바로 접근할 수 있다.

#include <vector>
using namespace std;

vector<int> v;
v.push_back(10);
v.push_back(20);

cout << v[0]; // 10
cout << v.size(); // 2

자바로 쓰면 이런 느낌이다.

ArrayList<Integer> list = new ArrayList<>();
list.add(10);
list.add(20);

System.out.println(list.get(0)); // 10
System.out.println(list.size()); // 2

대충 대응시키면 이렇게 보면 된다.

C++ vectorJava ArrayList의미
vector<int> v;ArrayList<Integer> list = new ArrayList<>();빈 리스트 만들기
v.push_back(x);list.add(x);뒤에 원소 추가
v[i]list.get(i)i번째 원소 읽기
v[i] = x;list.set(i, x)i번째 원소 수정
v.size()list.size()크기 확인
v.clear()list.clear()전체 삭제

차이점도 있다. C++의 vector<int>int를 그대로 담는다. 자바의 ArrayList<Integer>는 primitive int가 아니라 wrapper인 Integer를 담는다. 그래서 PS에서는 C++ vector<int>가 훨씬 가볍고 자주 쓰인다.

vector 선언 방식

가장 기본은 이거다.

vector<int> arr;

처음부터 크기를 정해서 만들 수도 있다.

vector<int> arr(5);

이러면 크기 5짜리 vector가 만들어지고, int는 기본값인 0으로 채워진다.

// [0, 0, 0, 0, 0]

초기값까지 같이 주고 싶으면 이렇게 한다.

vector<int> arr(5, -1);

자바로 치면 아래랑 비슷하다.

int[] arr = new int[5];
Arrays.fill(arr, -1);

다만 자바의 int[]는 길이가 고정이고, C++의 vector<int>는 뒤에 push_back으로 더 붙일 수 있다.

2차원 vector

위 코드에서는 이 문법이 나온다.

vector<vector<bool>> visited;

처음 보면 꺾쇠가 두 번 나와서 좀 무서운데, 그냥 vector 안에 vector가 들어간 것이다.

자바로 치면 이런 느낌이다.

ArrayList<ArrayList<Boolean>> visited;

그런데 알고리즘 문제에서는 보통 2차원 배열처럼 쓴다.

visited[i][j] = true;

if (visited[i][j]) {
    // 이미 방문함
}

자바의 2차원 배열이랑 비교하면 거의 똑같이 읽으면 된다.

boolean[][] visited = new boolean[n][m];
visited[i][j] = true;

vector<vector<bool>> visited;boolean[][] visited;랑 비슷한 역할을 한다.

assign으로 크기와 초기값 정하기

코드에서 제일 중요한 부분은 이거다.

visited.assign(n, vector<bool>(m, false));

뜻은 다음과 같다.

  • visited를 크기 n짜리 vector로 만든다.
  • 각 칸에는 vector<bool>(m, false)를 넣는다.
  • 즉 길이 m짜리 bool vector를 n개 만든다.
  • 결과적으로 n * m 크기의 2차원 방문 배열이 된다.

그림으로 보면 이런 구조다.

visited = [
  [false, false, false],
  [false, false, false],
  [false, false, false]
]

만약 n = 3, m = 3이면 위처럼 된다.

자바로 쓰면 이 느낌이다.

boolean[][] visited = new boolean[n][m];

for (int i = 0; i < n; i++) {
    Arrays.fill(visited[i], false);
}

사실 자바의 boolean[][]은 기본값이 false라서 Arrays.fill을 안 해도 되긴 한다. 그래도 C++의 assign(n, vector<bool>(m, false))를 자바식으로 이해하면 "n행 m열짜리 배열을 만들고 전부 false로 채운다"가 된다.

grid는 왜 &가 붙어있나

함수 파라미터에 이런 코드가 나온다.

bool containsCycle(vector<vector<char>>& grid)

여기서 vector<vector<char>>는 2차원 char 배열이다.

자바로 치면 문제에 따라 이런 것과 비슷하다.

char[][] grid

그런데 C++에는 뒤에 &가 붙어 있다.

vector<vector<char>>& grid

&참조(reference) 라는 뜻이다. 쉽게 말하면 원본을 그대로 받아온다는 뜻이다.

만약 & 없이 이렇게 쓰면,

bool containsCycle(vector<vector<char>> grid)

함수를 호출할 때 grid 전체가 복사될 수 있다. 2차원 배열이 크면 당연히 느리다.

그래서 알고리즘 문제에서는 보통 이렇게 쓴다.

bool containsCycle(vector<vector<char>>& grid)

자바는 객체를 넘길 때 기본적으로 참조값이 넘어가니까, ArrayList나 배열을 함수에 넘길 때 매번 전체 복사가 일어나지 않는다. C++에서는 복사를 피하려고 &를 직접 붙여주는 느낌으로 이해하면 된다.

그리고 함수 안에서 grid를 수정하지 않을 거라면 더 엄격하게 이렇게도 많이 쓴다.

bool containsCycle(const vector<vector<char>>& grid)

const는 "읽기만 할 거고 수정하지 않겠다"는 뜻이다. 다만 LeetCode의 함수 시그니처가 정해져 있으면 거기에 맞춰야 한다.

size()와 인덱스 접근

위 코드에서는 행과 열 크기를 이렇게 구한다.

n = grid.size();
m = grid[0].size();

grid.size()는 바깥 vector의 크기다. 즉 행의 개수다.

grid[0].size()는 0번째 행의 크기다. 즉 열의 개수다.

자바로 치면 이런 느낌이다.

n = grid.length;
m = grid[0].length;

원소 접근은 []를 두 번 쓰면 된다.

grid[i][j]
visited[i][j]

자바의 2차원 배열 접근이랑 똑같다.

grid[i][j]
visited[i][j]

참고로 C++ vector에는 v.at(i)도 있다.

v.at(i)

at()은 범위를 벗어나면 예외를 던져준다. 대신 PS에서는 보통 속도와 간결함 때문에 v[i]를 많이 쓴다. 범위 체크는 직접 isValid 같은 함수로 해주는 식이다.

이 코드에서도 먼저 범위를 확인하고,

bool isValid(int ni, int nj, char c, vector<vector<char>>& grid) {
    return ni >= 0 && ni < n && nj >= 0 && nj < m && grid[ni][nj] == c;
}

그 다음에 grid[ni][nj]에 접근한다. 이 순서가 중요하다. 범위 밖인데 grid[ni][nj]를 먼저 하면 터진다.

이 코드의 vector 문법 정리

이 코드에서 vector 관련해서 봐야 할 문법은 이 정도다.

vector<vector<bool>> visited;

2차원 bool vector를 멤버 변수로 선언한다. DFS 중에 여러 함수에서 같이 쓰려고 클래스 안에 빼둔 것이다.

bool containsCycle(vector<vector<char>>& grid)

2차원 char vector를 참조로 받는다. 복사를 피하려고 &를 붙인다.

n = grid.size();
m = grid[0].size();

행 개수와 열 개수를 구한다.

visited.assign(n, vector<bool>(m, false));

nm열짜리 방문 배열을 만들고 전부 false로 초기화한다.

visited[i][j] = true;
if (visited[ni][nj]) { ... }

2차원 배열처럼 인덱스로 접근한다.

grid[ni][nj] == c

현재 칸의 문자가 내가 찾는 색깔과 같은지 확인한다.

Java에서 C++로 옮길 때 감각

자바에서 이렇게 쓰던 사람이라면,

boolean[][] visited = new boolean[n][m];
char[][] grid;

visited[i][j] = true;
if (grid[ni][nj] == color) {
    ...
}

C++에서는 이렇게 옮기면 된다.

vector<vector<bool>> visited(n, vector<bool>(m, false));
vector<vector<char>> grid;

visited[i][j] = true;
if (grid[ni][nj] == color) {
    ...
}

또 자바의 ArrayList<Integer>처럼 1차원 리스트가 필요하면,

ArrayList<Integer> arr = new ArrayList<>();
arr.add(3);
arr.add(5);

C++에서는 이렇게 쓴다.

vector<int> arr;
arr.push_back(3);
arr.push_back(5);

정리하면, PS에서 vector는 거의 기본 장비다. 1차원 배열, 2차원 배열, 그래프 인접 리스트, 좌표 목록 전부 vector로 만들 수 있다. 자바에서 ArrayList와 배열을 섞어 쓰던 감각을 C++에서는 대부분 vector 하나로 가져갈 수 있다고 보면 된다.

leetcode에서 cpp 사용 기초 - map 잘써보기

1 min#ps#leetcode#cpp단일 페이지
#include <vector>
#include <unordered_map>
#include <cmath>

using namespace std;

#define ll long long // 세미콜론안붙인다

class Solution {
public:
    vector<ll> distance(vector<int>& nums) {
        int n = nums.size();
        // 각 숫자의 모든 인덱스와 총합을 미리 저장
        unordered_map<int, vector<int>> groups;
        unordered_map<int, ll> total_sum_map;
        
        for (int i = 0; i < n; ++i) {
            groups[nums[i]].push_back(i);
            total_sum_map[nums[i]] += i;
        }

        // 각 숫자가 몇 번째로 등장했는지 지금까지의 인덱스 합은 얼마인지 추적
        unordered_map<int, int> count_map;
        unordered_map<int, ll> prefix_sum_map;
        vector<ll> arr;

        for (int i = 0; i < n; ++i) {
            int val = nums[i];
            ll curIdx = i;
            
            ll total_sum = total_sum_map[val]; // 현재 값에 대한 인덱스 총합
            int total_count = groups[val].size(); // 현재 값이 몇개인지?
            ll prefix_sum = prefix_sum_map[val]; // 현재 값의 누적합
            int count_so_far = count_map[val]; // 현재 인덱스 이전의 그 값 개수

            ll left = (ll)count_so_far * curIdx - prefix_sum;
            
            ll right_sum = total_sum - (prefix_sum + curIdx);
            int right_count = total_count - 1 - count_so_far;
            ll right = right_sum - (ll)right_count * curIdx;

            arr.push_back(left + right);

            prefix_sum_map[val] += curIdx;
            count_map[val]++;
        }

        return arr;
    }
};

처음엔 deque써서 회전큐같이 풀었다가 TLE나서 prefix로 선회했다. 먼저 각 숫자 nums[i]가 등장하는 모든 인덱스들의 총합 total_sum_map과 인덱스 목록 groups을 미리 저장한다.

그 다음 배열을 한 번 더 순회하며, 현재 인덱스 curIdx를 기준으로 왼쪽에 있는 같은 값들의 거리 합이랑 오른쪽 거리합을 나눠서 계산한다.

  • 왼쪽 구간: (현재까지 그 숫자가 나온 횟수) * 현재 인덱스 - (현재까지의 인덱스 누적합)
  • 오른쪽 구간: (나머지 인덱스의 합) - (나머지 등장 횟수) * 현재 인덱스

이거 나도 처음에 뇌가 돌아버릴뻔했는데, 알고보니 분배법칙이었다.

[2, 3, 7]이 있고, 현재 idx가 7이라고 해보겠다.

왼쪽에 있는 2,3과의 거리를 구하는 법?

7-2 + 7-3 이다. 보이는 지?

(왼쪽 개수 * 내 위치) - (왼쪽 인덱스들의 합) 이렇게 된다.


C++ PS 필수 문법: deque, #define, typedef

알고리즘 문제를 풀 때 코딩 속도를 높여주고, 코드를 간결하게 작성할 수 있게 돕는 유용한 문법들입니다.

1. 매크로 정의 #define

단축어를 만들 때 사용합니다. 코드 길이를 획기적으로 줄일 수 있어 C++로 PS(Problem Solving)를 할 때 즐겨 사용됩니다. 끝에 세미콜론(;)을 붙이지 않는 것에 주의하세요.

#define ll long long
#define pb push_back
#define all(v) (v).begin(), (v).end()

// 사용 예시
ll big_number = 10000000000;
vector<int> v;
v.pb(1); // v.push_back(1)과 동일

2. 별칭 지정 typedef & using

기존 자료형에 새로운 이름을 부여하는 기능입니다. 구조체나 긴 템플릿 타입을 타이핑하기 편하게 만듭니다. 요즘 Modern C++(C++11 이후)에서는 좀 더 직관적인 using을 자주 사용합니다.

// typedef 방식
typedef long long ll;
typedef pair<int, int> pii;

// using 방식 (Modern C++ 권장)
using ll = long long;
using pii = pair<int, int>;

// 사용 예시
pii my_pair = {1, 2};

3. 양방향 큐 std::deque (Double Ended Queue)

std::deque는 양쪽 끝에서 데이터 삽입과 삭제가 모두 O(1)O(1)의 시간 복잡도로 빠르게 이루어지는 자료구조입니다. 게다가 배열(Vector)처럼 중간 요소에 인덱스([])로 접근하는 Random Access도 가능해 다재다능하게 쓰입니다. (Vector는 뒤에서만 삽입/삭제 효율이 좋습니다.)

#include <deque>
#include <iostream>

using namespace std;

int main() {
    deque<int> dq;

    // 양 끝 삽입
    dq.push_back(2);    // 뒤에 2 삽입: [2]
    dq.push_back(3);    // 뒤에 3 삽입: [2, 3]
    dq.push_front(1);   // 앞에 1 삽입: [1, 2, 3]

    // 인덱스 접근 
    cout << "인덱스 1 참조: " << dq[1] << endl; // 출력: 2

    // 양 끝 삭제
    dq.pop_back();      // 뒤에서 꺼냄: [1, 2]
    dq.pop_front();     // 앞에서 꺼냄: [2]
    
    return 0;
}

leetcode에서 cpp 사용 기초 - vector에서 값 찾기

1 min#ps#leetcode#cpp단일 페이지

std::vector에서 특정 값이 포함되어 있는지 확인하는 방법을 빠르게 알아보자.

  1. std::find 사용 (가장 일반적인 방법)

<algorithm> 헤더에 포함된 std::find를 사용하는 것이 가장 표준적인 방식. 처음부터 끝까지 하나씩 찾는 방식(선형 탐색).

#include <vector>
#include <algorithm> // find를 위해 필요
#include <iostream>

int main() {
    std::vector<int> v = {10, 20, 30, 40, 50};
    int target = 30;

    // v.begin()부터 v.end()까지 target이 있는지 찾음
    auto it = std::find(v.begin(), v.end(), target);

    if (it != v.end()) {
        std::cout << "값을 찾았습니다!" << std::endl;
    } else {
        std::cout << "값이 없습니다." << std::endl;
    }
}
  1. std::binary_search 사용 (정렬된 경우) 만약 벡터가 이미 오름차순으로 정렬되어 있다면, binary_search를 쓰는 것이 훨씬 빠르다. (이진 탐색 활용)
#include <vector>
#include <algorithm>
#include <iostream>

int main() {
    std::vector<int> v = {10, 20, 30, 40, 50}; // 정렬된 상태
    
    if (std::binary_search(v.begin(), v.end(), 30)) {
        std::cout << "값이 존재합니다." << std::endl;
    }
}
  1. std::count 사용 (단순 존재 여부만) count 함수는 해당 값의 개수를 반환.

0보다 크면 존재하는 것으로 간주하는데 그냥 find 써라.

if (std::count(v.begin(), v.end(), target) > 0) {
    // 존재함
}

v.begin()v.end()의 의미

C++의 <algorithm> 함수들은 대부분 탐색 범위를 지정하기 위해 시작점과 끝점을 인자로 받는다. 이때 범위는 반개구간(Half-open interval) [begin, end) 형태임.

  • v.begin(): 벡터의 첫 번째 원소를 가리키는 반복자(iterator).
  • v.end(): 벡터의 마지막 원소가 아니라, 마지막 원소의 다음(past-the-end) 공간을 가리키는 반복자.

그니까 0..n쯤으로 생각하면 된단 얘기.

따라서 std::find 같은 함수는 v.end()에 도달할 때까지 탐색을 진행하며, 값을 찾지 못하면 v.end()를 그대로 반환한다. 우리가 값을 찾았는지 확인할 때 if (it != v.end())를 사용하는 이유가 바로 이거때문..

leetcode에서 cpp 사용 기초 - hashmap 사용법 추가

2 min#ps#leetcode#cpp단일 페이지
#include <vector>
#include <unordered_map>
#include <numeric>

using namespace std;

class Solution {
public:
    vector<int> p;

    int find(int a) {
        if (p[a] == a) return a;
        return p[a] = find(p[a]);
    }

    void union_group(int a, int b) {
        int pa = find(a);
        int pb = find(b);
        if (pa != pb) {
            p[pa] = pb;
        }
    }

    int minimumHammingDistance(vector<int>& source, vector<int>& target, vector<vector<int>>& allowedSwaps) {
        int n = source.size();
        p.resize(n);
        iota(p.begin(), p.end(), 0);

        for (const auto& swap : allowedSwaps) {
            union_group(swap[0], swap[1]);
        }

        unordered_map<int, unordered_map<int, int>> group_counts;
        for (int i = 0; i < n; ++i) {
            int root = find(i);
            group_counts[root][source[i]]++;
        }

        int matches = 0;
        for (int i = 0; i < n; ++i) {
            int root = find(i);
            if (group_counts[root][target[i]] > 0) {
                matches++;
                group_counts[root][target[i]]--; // 사용한 숫자의 개수 차감
            }
        }

        return n - matches;
    }
};

이 문제 코드 안에서 사용된 문법을 보자.

#include <vector>
#include <unordered_map>
#include <numeric>
  • #include는 필요한 표준 라이브러리를 가져오는 전처리 지시문이다.
  • <vector>는 동적 배열 vector를 사용하기 위해 필요하다.
  • <unordered_map>은 해시맵 unordered_map을 사용하기 위해 필요하다.
  • <numeric>iota 같은 수열 관련 함수를 사용하기 위해 필요하다.

이 코드에서는:

  • vector<int>로 부모 배열 p를 만든다.
  • unordered_map으로 그룹별 숫자 개수를 센다.
  • iota로 부모 배열을 0, 1, 2, ...로 초기화한다.

예시:

#include <vector>
#include <iostream>

using namespace std;

int main() {
    vector<int> nums = {1, 2, 3};
    cout << nums[0] << endl; // 1
}

using namespace std;: std:: 생략

using namespace std;
  • C++ 표준 라이브러리의 대부분은 std 네임스페이스 안에 있다.
  • 원래는 std::vector, std::unordered_map, std::cout처럼 써야 한다.
  • using namespace std;를 쓰면 std::를 생략할 수 있다.

예시:

#include <iostream>

using namespace std;

int main() {
    cout << "hello" << endl;
}

class: 사용자 정의 타입

class Solution {
public:
    vector<int> p;
    ...
};
  • class는 변수와 함수를 하나로 묶는 사용자 정의 타입이다.
  • LeetCode에서는 보통 Solution 클래스를 만들고 그 안에 풀이 함수를 넣는다.
  • { ... } 안에 멤버 변수와 멤버 함수를 정의한다.
  • 마지막 ;까지 있어야 클래스 정의가 끝난다.

예시:

class Person {
public:
    int age;

    void say_age() {
        cout << age << endl;
    }
};

public:: 접근 지정자

public:
  • public 아래의 멤버는 클래스 바깥에서도 접근할 수 있다.
  • class는 기본 접근 제한자가 private이기 때문에, 외부에서 호출할 함수는 보통 public: 아래에 둔다.

예시:

class A {
public:
    int x;
};

int main() {
    A a;
    a.x = 10;
}

vector<int> p;: 템플릿 자료형과 멤버 변수 선언

vector<int> p;
  • vector<int>int를 담는 가변 길이 배열이다.
  • <int>는 템플릿 인자로, 벡터가 어떤 타입을 저장하는지 뜻한다.
  • p는 클래스의 멤버 변수다.

일단 나는 처음에 배열을 선언해서 쓱려고했는데 벡터가 맞나보다.

예시:

vector<int> arr;
arr.push_back(5);
arr.push_back(9);
cout << arr[1] << endl; // 9

함수 정의: 반환형, 함수명, 매개변수

int find(int a) {
  • int는 반환형이다.
  • find는 함수 이름이다.
  • (int a)int 타입 매개변수 a를 받는다는 뜻이다.

예시:

int add(int a, int b) {
    return a + b;
}

void: 반환값이 없는 함수

void union_group(int a, int b) {
  • void는 반환값이 없다는 뜻이다.
  • union_group은 두 그룹을 합치는 역할만 하므로 값을 돌려줄 필요가 없다.

예시:

void print_hello() {
    cout << "hello" << endl;
}

지역 변수 선언과 초기화

int pa = find(a);
int pb = find(b);
int n = source.size();
int matches = 0;
  • 함수 안에서 선언한 변수는 지역 변수다.
  • 선언과 동시에 =로 초기화할 수 있다.

예시:

int x = 10;
int y = x + 5;

인덱싱 []

p[a]
swap[0]
source[i]
target[i]
group_counts[root][source[i]]
  • []는 배열, vector, unordered_map 등에서 원소에 접근할 때 쓴다.
  • vector에서는 인덱스로 접근한다.
  • unordered_map에서는 키로 접근한다.

예시 1:

vector<int> v = {10, 20, 30};
cout << v[1] << endl; // 20

예시 2:

unordered_map<string, int> mp;
mp["apple"] = 3;
cout << mp["apple"] << endl; // 3

참고:

  • unordered_map에서 mp[key]를 쓰면 key가 없을 때 기본값으로 새 원소가 만들어질 수 있다.
  • 그래서 group_counts[root][target[i]]도 키가 없으면 0부터 시작한다.

참조자 &: 원본을 직접 가리키기

vector<int>& source
vector<int>& target
vector<vector<int>>& allowedSwaps
  • &는 참조자다.
  • 복사본이 아니라 원본을 직접 가리킨다.
  • vector를 함수 인자로 넘길 때 복사를 피할 수 있어서 효율적이다.

예시:

void add_one(int& x) {
    x++;
}

int main() {
    int n = 3;
    add_one(n);
    cout << n << endl; // 4
}

중첩 템플릿

vector<vector<int>>& allowedSwaps
unordered_map<int, unordered_map<int, int>> group_counts;
  • 템플릿 안에 또 다른 템플릿을 넣을 수 있다.
  • vector<vector<int>>는 2차원 배열처럼 볼 수 있다.
  • unordered_map<int, unordered_map<int, int>>는 "정수 -> 또 다른 해시맵" 구조다.

이 코드에서는:

  • 바깥 키: 그룹 루트
  • 안쪽 키: 숫자 값
  • 안쪽 값: 해당 숫자의 개수

예시:

vector<vector<int>> grid = {
    {1, 2},
    {3, 4}
};

cout << grid[1][0] << endl; // 3

멤버 함수 호출과 . 연산자

source.size()
p.resize(n)
p.begin()
p.end()
  • .은 객체의 멤버에 접근할 때 쓰는 연산자다.
  • size(), resize(), begin(), end()vector의 멤버 함수다.

각 함수의 의미:

  • size(): 원소 개수 반환
  • resize(n): 크기를 n으로 변경
  • begin(): 시작 위치 반복자
  • end(): 마지막 다음 위치 반복자

예시:

vector<int> v = {1, 2, 3};

cout << v.size() << endl; // 3
v.resize(5);
cout << v.size() << endl; // 5

iota(...): 연속된 값 채우기

iota(p.begin(), p.end(), 0);
  • iota(first, last, start)[first, last) 구간을 start, start + 1, start + 2... 로 채운다.
  • 여기서는 p{0, 1, 2, ..., n - 1}로 초기화한다.
  • Union-Find에서 "처음에는 모든 원소의 부모가 자기 자신"이어야 해서 자주 쓰인다.

예시:

vector<int> v(5);
iota(v.begin(), v.end(), 10);

// 결과: {10, 11, 12, 13, 14}

const auto&: 타입 추론 + 읽기 전용 참조

const auto& swap

이 표현은 세 부분으로 나눠서 보면 쉽다.

  • auto: 타입을 컴파일러가 자동으로 추론
  • &: 복사하지 않고 참조로 받음
  • const: 읽기만 하고 수정은 못 함

즉, allowedSwaps의 각 원소를 "복사 없이, 수정하지 않는 참조"로 받는 문법이다.

예시:

vector<string> words = {"aa", "bb", "cc"};

for (const auto& word : words) {
    cout << word << endl;
}

위 예시에서 word의 실제 타입은 const string&로 추론된다.

unordered_map

unordered_map<int, unordered_map<int, int>> group_counts;
  • unordered_map<Key, Value>는 해시 기반 딕셔너리다.
  • 평균적으로 삽입과 조회가 빠르다.
  • 파이썬의 dict와 비슷하게 생각하면 된다.

이 코드에서는:

  • group_counts[root]는 특정 그룹의 숫자 개수표다.
  • group_counts[root][value]는 그 그룹에서 value가 몇 번 나왔는지 의미한다.

예시:

unordered_map<int, int> count;

count[5]++;
count[5]++;
count[2]++;

cout << count[5] << endl; // 2
cout << count[2] << endl; // 1

C++ 입문노트

5 min#ps#leetcode#cpp단일 페이지

나는 Java, Kotlin이 주력 언어다. 업무에서도 이 두 가지를 쓰지만 C++를 알아두면 선택지가 넓어질 것 같아 입문해보려고 한다. LeetCode를 풀면서 C++ 숙련도를 올려보자.

오늘 배운 것

1. std::vector (동적 배열) 선언법

C++에서는 객체를 생성할 때 new를 무분별하게 사용하지 않는다.

잘못된 예: vector<int> res = new vector(); (자바 스타일)

C++에서 new를 쓰면 메모리 주소(포인터)를 반환하기 때문에 vector<int>* res처럼 선언해야 하고, 나중에 직접 delete로 메모리를 해제해야 한다.

올바른 예: vector<int> res(크기);

이렇게 선언하면 함수가 끝날 때 자동으로 메모리가 정리되는 지역 변수가 된다.

2. .size() 메서드

배열이나 리스트의 길이를 구할 때 언어마다 이름이 다르지만, C++에서는 size()를 표준으로 사용한다.

3. 참조자 (Reference, &)

코드의 vector<int>& nums에서 & 기호는 원본을 가져다 쓰겠다는 뜻이다.

  • vector<int> nums: 함수를 호출할 때 배열 전체를 복사 (메모리 낭비가 큼)
  • vector<int>& nums: 원본 배열의 별명만 지어줌 (메모리 사용이 거의 없고 빠름)

4. 벡터의 초기화와 인덱스 접근

C++ 벡터는 선언할 때 크기를 미리 정해두면 일반 배열처럼 []를 사용해 접근할 수 있다.

vector<int> v(10);: 크기가 10이고 0으로 초기화된 벡터 생성. v[0]부터 v[9]까지 사용 가능.

크기를 정하지 않고 생성(vector<int> v;)했다면, v[i] = 10;처럼 대입할 수 없고 반드시 v.push_back(10);을 써야 한다.

5. 스택(Stack) vs 힙(Heap) - new의 의미가 다르다

Java에서 new는 일상적이지만, C++에서 new이 객체를 수동으로 관리하겠다는 선언에 가깝다.

Java: Solution sol = new Solution(); // GC(Garbage Collector)가 알아서 치워준다.

  • C++ (Stack): Solution sol; // 함수가 종료되면 메모리에서 자동으로 사라진다.
  • C++ (Heap): Solution* sol = new Solution(); // 사용 후 delete sol;을 하지 않으면 메모리 누수가 발생한다.

6. 네임스페이스 (std::) - 가문 밝히기

C++의 표준 라이브러리(STL)는 모두 std 네임스페이스 안에 있다.

Java는 import 후 클래스 이름만 쓰면 되지만, C++은 std::vector, std::sort처럼 출처를 밝히는 것이 원칙이다.

코드 상단에 using namespace std;를 선언하면 생략할 수 있으나, 규모가 큰 프로젝트에서는 이름 충돌 방지를 위해 std::를 명시하는 습관이 좋다.

7. 범위 기반 for 루프 (Range-based for loop)

Kotlin의 for (num in nums)나 Java의 for (int num : nums)와 같은 문법을 C++11부터 지원한다.

for (int n : nums) { 
    // n은 nums 요소를 '복사'해서 가져온다.
}

for (int& n : nums) { 
    // &를 붙이면 '참조'로 가져온다.
    // n을 수정하면 원본 nums도 수정되고, 복사 비용이 들지 않아 효율적이다.
}

Java/Kotlin -> C++ 대응표

Java / KotlinC++ STL핵심 특징
ArrayList<T>vector<T>연속 메모리, 인덱스 접근 O(1)
LinkedList<T>list<T>중간 삽입/삭제 O(1), 인덱스 접근 느림
Stack<T>stack<T>LIFO
Queue<T>queue<T>FIFO
Deque<T>deque<T>양쪽 삽입/삭제 O(1)
PriorityQueue<T>priority_queue<T>기본 최대 힙
HashMap<K,V>unordered_map<K,V>평균 O(1), 정렬 없음
TreeMap<K,V>map<K,V>자동 정렬, O(logN)
HashSet<T>unordered_set<T>평균 O(1), 중복 없음
TreeSet<T>set<T>정렬 유지, 중복 없음

코테에서 자주 쓰는 선택 기준

  • "순서대로 담고 인덱스로 빠르게 접근" -> vector
  • "키 기준 정렬된 순회 필요" -> map / set
  • "정렬 필요 없고 존재 여부만 빠르게" -> unordered_map / unordered_set
  • "최솟값/최댓값을 계속 꺼내야 함" -> priority_queue
  • "BFS 레벨 탐색" -> queue
  • "슬라이딩 윈도우에서 앞뒤 pop/push" -> deque

꼭 알아둘 기본 시간복잡도 (암기용)

  • vector: 뒤 push_back 평균 O(1), 중간 삽입/삭제 O(N), 인덱스 접근 O(1)
  • map/set: 삽입/삭제/탐색 O(logN)
  • unordered_map/unordered_set: 평균 O(1), 최악 O(N)
  • priority_queue: 삽입 O(logN), top 조회 O(1), pop O(logN)

실전 코드 패턴

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

// 1) 최소 힙 (Java PriorityQueue 기본 동작과 유사)
priority_queue<int, vector<int>, greater<int>> minHeap;

// 2) 빈도수 카운트
unordered_map<int, int> freq;
for (int x : nums) freq[x]++;

// 3) key 정렬 순회가 필요할 때
map<int, int> ordered;
for (int x : nums) ordered[x]++;

// 4) 중복 제거 + 존재 여부 확인
unordered_set<int> seen;
if (seen.count(x)) {
    // already exists
}
seen.insert(x);

bits/stdc++.h 없이 필요한 헤더만 include 하기

온라인 저지에서는 #include <bits/stdc++.h>가 편하지만, 표준 C++ 헤더는 아니기 때문에 환경에 따라 동작하지 않을 수 있다. 실무/이식성을 생각하면 필요한 헤더를 명시적으로 include하는 습관이 좋다.

#include <iostream>      // cin, cout
#include <vector>        // vector
#include <string>        // string
#include <algorithm>     // sort, max, min, lower_bound
#include <unordered_map> // unordered_map
#include <unordered_set> // unordered_set
#include <map>           // map
#include <set>           // set
#include <queue>         // queue, priority_queue
#include <stack>         // stack
#include <deque>         // deque
#include <utility>       // pair
#include <limits>        // numeric_limits

using namespace std;

자주 쓰는 알고리즘 유틸까지 넣고 싶다면 다음도 많이 사용한다.

  • <functional>: greater<>, less<>, 함수 객체
  • <numeric>: accumulate, gcd(환경에 따라)
  • <tuple>: tuple, tie

C++ STL 사용 시 자주 하는 실수

  • unordered_map은 순서를 보장하지 않는다. 출력 순서 기대하면 안 됨
  • priority_queue는 기본이 최대 힙이다 (최소 힙은 comparator 필요)
  • vector에 크기 없이 v[i] = ... 하면 런타임 에러 가능성 있음
  • map[key]++는 key가 없으면 자동으로 0 생성 후 증가한다

정리하면, 코테에서는 대부분 vector + unordered_map/set + priority_queue 조합으로 시작하고, "정렬된 상태 유지"가 필요할 때만 map/set로 바꾸면 판단이 빨라진다.

priority_queue<T> 사용법

2 min#ps#leetcode#cpp단일 페이지

C++ priority_queue 정리. 핵심은 **기본이 최대 힙(Max-Heap)**이라는 점이다.

1) 기본 사용법 (최대 힙)

가장 큰 값이 top()에 온다.

#include <queue>
#include <iostream>

using namespace std;

int main() {
    // 기본: 큰 값 우선
    priority_queue<int> pq;

    pq.push(10);
    pq.push(30);
    pq.push(20);

    cout << "Top element: " << pq.top() << endl; // 30

    pq.pop(); // 30 제거
    cout << "Next top: " << pq.top() << endl; // 20
}

2) 최소 힙 (Min-Heap)

가장 작은 값을 top()으로 두려면 세 번째 템플릿 인자로 greater<T>를 쓴다.

// priority_queue<자료형, 구현체, 비교연산자>
priority_queue<int, vector<int>, greater<int>> pq;

pq.push(10);
pq.push(30);
pq.push(20);

cout << pq.top() << endl; // 10

3) pair와 함께 사용하기

pair<int, int>는 기본 비교가 사전식(lexicographical)이다.

  • 먼저 first를 비교
  • first가 같으면 second를 비교

기본 priority_queue<pair<int, int>>에서는 큰 값이 우선이므로, first/second 모두 내림차순 우선순위를 가진다.

priority_queue<pair<int, int>> pq;

pq.push({5, 100}); // 거리 5, 인덱스 100
pq.push({5, 200}); // 거리 5, 인덱스 200
pq.push({10, 50}); // 거리 10, 인덱스 50

// 결과: {10, 50} -> {5, 200} -> {5, 100} 순으로 나옴

4) 핵심 멤버 함수

함수설명시간 복잡도
push(val)요소 추가O(logN)O(\log N)
pop()최우선 요소 제거O(logN)O(\log N)
top()최우선 요소 조회 (제거 X)O(1)O(1)
empty()비어있는지 확인O(1)O(1)
size()요소 개수O(1)O(1)

5) 커스텀 정렬 (구조체 comparator)

정렬 기준이 복잡하면 comparator를 직접 정의하는 것이 가장 안전하다.

예: 거리(first)는 큰 값 우선, 거리가 같으면 인덱스(second)는 작은 값 우선

struct Compare {
    bool operator()(pair<int, int> a, pair<int, int> b) {
        if (a.first == b.first) {
            return a.second > b.second; // 두 번째 인자는 작은 게 위로 (오름차순)
        }
        return a.first < b.first; // 첫 번째 인자는 큰 게 위로 (내림차순)
    }
};

priority_queue<pair<int, int>, vector<pair<int, int>>, Compare> pq;

주의: pop()은 반환값이 없다. 값을 사용하려면 top()을 먼저 읽고 pop()해야 한다.

6) greater<T> / less<T> 정리

greater<int><functional>의 함수 객체다. 내부적으로 a > b 비교를 수행한다.

template <typename T>
struct greater {
    bool operator()(const T& a, const T& b) const {
        return a > b;
    }
};

사용 예시

  • sort(..., greater<int>()) -> 내림차순 정렬
  • priority_queue<int, vector<int>, greater<int>> -> 최소 힙
vector<int> v = {1, 4, 2, 5, 3};
sort(v.begin(), v.end(), greater<int>()); 
// 결과: {5, 4, 3, 2, 1}
priority_queue<int, vector<int>, greater<int>> pq;

pq.push(10);
pq.push(30);
pq.push(20);

cout << pq.top() << endl; // 10

less vs greater 비교

기능less<T> (기본값)greater<T>
비교 연산a < ba > b
std::sort 결과오름차순내림차순
priority_queue 결과최대 힙최소 힙

7) C++14 이후 간결한 표기

C++14부터는 greater<int>() 대신 greater<>()를 쓸 수 있다.

sort(v.begin(), v.end(), greater<>());

정리: 코테에서 우선순위 큐는 "기본 최대 힙"을 먼저 기억하고, 최소 힙이 필요하면 greater<T>를 넣는 습관을 들이면 실수를 크게 줄일 수 있다.