홈

PS 노트

6개 글필터: #cpp

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

3분#ps#leetcode#cpp단일 페이지

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

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));

n행 m열짜리 방문 배열을 만들고 전부 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분#ps#leetcode#cpp단일 페이지

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

#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분#ps#leetcode#cpp단일 페이지

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

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분#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분#ps#leetcode#cpp단일 페이지

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

나는 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분#ps#leetcode#cpp단일 페이지

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

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(log⁡N)O(\log N)
pop()최우선 요소 제거O(log⁡N)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>를 넣는 습관을 들이면 실수를 크게 줄일 수 있다.