ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • 5장 자료구조
    Books/면접을 위한 CS 전공지식 노트 2023. 2. 3. 22:21

    - 자료 구조(data structure)은 효율적으로 데이터를 관리하고 수정, 삭제, 탐색, 저장할 수 있는 데이터 집합을 말한다.

    - C++는 STL을 기반으로 전반적인 자료 구조를 가장 잘 설명할 수 있는 언어이며, 이를 기반으로 자료 구조에 대한 참고 코드를 제공한다.

     

    * STL : C++의 표준 템플릿 라이브러리이자 스택, 배열 등의 데이터 구조의 함수 등을 제공하는 라이브러리 묶음

     

    - C++는 어려운 언어이지만 이 책에서 설명하는 C++의 수준은 '매우 쉬운 난이도'이며,

       책의 실린 모든 C++ 코드는 별도의 프로그램 설치 없이 다음 링크에서 실행할 수 있다.

       https://www.onlinegdb.com/online_c++_compiler

     

    5.1 복잡도

    - 복잡도는 시간 복잡도와 공간 복잡도로 나뉜다.

     

    5.1.1 시간 복잡도

    C++의 기본

    - 시간 복잡도를 알아보기 전, 잠시 C++의 기본을 살펴보고 가자!

    - 먼저 입력받은 문자열을 출력하는 프로그램을 하나 만들어보자!

    #include <bits/stdc++.h>	// --- (1) 
    using namespace std;		// --- (2)
    string a;			// --- (3)
    int main() {
        cin >>> a;			// --- (4)
        cout << a << "\n";		// --- (5)
        return 0;			// --- (6)
    }

    - 실행시킨 이후 wow라고 입력하면 wow가 출력된다.

    - C++는 main 함수를 중심으로 돌아가므로 main 함수 하나를 무조건 만들어야 한다.

    - 이후 컴파일이 시작되면 전역 변수 초기화, 라이브러리 import 등의 작업이 일어나고, main 함수에 얽혀있는 함수들이 작동된다.

    - 그러고 나서 main 함수가 0을 리턴하며 프로세스가 종료된다.

    1. 헤더 파일이다. STL 라이브러리를 import한다.

        ㆍ이 중 bits/stdc++.h 는 모든 표준 라이브러리가 포함된 헤더이다.

    2. std라는 네임스페이스를 사용한다는 뜻이다. 

        ㆍcin이나 cout 등을 사용할 때 원래는 std::cin처럼 네임스페이스를 달아서 호출해야 하는데,

            이를 기본으로 설정한다는 뜻이다. 

        ㆍ참고로 네임스페이스는 같은 클래스 이름 구별, 모듈화에 쓰이는 이름을 말한다.

    3. 문자열을 선언했다. <타입> <변수명> 이렇게 선언한다.

        ㆍstring이라는 타입을 가진 a라는 변수를 정의했다.

        ㆍ예를 들어 string a = "큰돌"이라고 해보자.

        ㆍ이때 a를 lvalue라고 하며 큰돌을 rvalue라고 한다.

        ㆍlvalue는 추후 다시 사용될 수 있는 변수이며, rvalue는 한 번 쓰고 다시 사용되지 않는 변수를 말한다.

    4. 입력이다. 대표적으로 cin, scanf가 있다.

    5. 출력이다. 대표적으로 cout와 printf가 있다.

    6. return 0이다. 프로세스가 정상적으로 마무리됨을 뜻한다.

     

    빅오 표기법

    - 시간 복잡도란 '문제를 해결하는 데 걸리는 시간과 입력의 함수 관계'를 가리킨다.

    - 어떠한 알고리즘의 로직이 '얼마나 오래 시간'이 걸리는지를 나타내는 데 쓰이며, 보통 빅오 표기법으로 나타낸다.

    - 예를 들어 '입력 크기 n'의 모든 입력에 대한 알고리즘에 필요한 시간이 10n^2 + n이라고 하면 다음과 같은 코드를 상상할 수 있다.

    for (int i = 0; i < 10 ; i++) {
        for (int j = 0; j < n ; j++) {
            for(int k = 0; k < n ; k++) {
                if (true) cout << k << '\n';
            }
        }
    }
    
    for (int i = 0; i < n ; i++) {
        if (true) cout << i << '\n';
    }

    - 빅오 표기법이란 입력 범위 n을 기준으로 해서 로직이 몇 번 반복되는지 나타내는 것인데, 앞서 말한 코드의 시간 복잡도를 빅오 표기법으로 나타내면 O(n^2)이 된다.

    - '가장 영향을 많이 끼치는' 항의 상수 인자를 빼고 나머지 항을 없앤 것이다.

    - 다른 항들이 신경 쓰일 수도 있지만 증가 속도를 고려한다면 그렇지 않다.

    - 입력 크기가 커질수록 연산량이 가장 많이 커지는 항은 n의 제곱항이고, 다른 것은 그에 비해 미미하기 때문에 이것만 신경쓰면 된다는 이론이다.

     

    시간 복잡도의 존재 이유

    - 시간 복잡도는 왜 필요할까?

        ㆍ바로 효율적인 코드로 개선하는 데 쓰이는 척도가 된다.

    - 버튼을 누르고 화면이 나타나는데 이 로직이 O(n^2)의 시간 복잡도를 가지고 9초가 걸린다고 해보자!

        ㆍ이를 O(n)의 시간 복잡도를 가지는 알고리즘으로 개선한다면 3초가 걸릴 것이다.

     

    시간 복잡도의 속도 비교

    - O(1)과 O(n)은 입력 크기가 커질수록 차이가 많이 나는 것을 볼 수 있다. (p235)

    - O(n^2)는 말할 것도 없이 차이가 크다.

    - 즉, O(n^2)보다는 O(n), O(n)보다는  O(1)을 지향해야 한다.

     

    5.1.2 공간 복잡도

    - 공간 복잡도는 프로그램을 실행시켰을 때 필요로 하는 자원 공간의 양을 말한다.

    - 정적 변수로 선언된 것 말고도 동적으로 재귀적인 함수로 인해 공간을 계속해서 필요로 할 경우도 포함한다.

    int a[1004];

    - 예를들어 앞의 코드처럼 되어 있는 배열이 있다고 하면 a 배열은 1004x4바이트의 크기를 가지게 된다.

    - 이런 공간을 의미한다.

     

    5.1.3 자료 구조에서의 시간 복잡도

    - 자료 구조를 쓸 때는 이러한 시간 복잡도를 잘 생각해야 한다.

    - 다음은 자주 쓰는 자료 구조의 시간 복잡도를 나타낸 모습이다.

    - 보통 시간 복잡도를 생각할 때 평균, 그리고 최악의 시간 복잡도를 고려하면서 쓴다.

    - 자료 구조의 평균 시간 복잡도

    자료 구조 접근 탐색 삽입 삭제
    배열(array) O(1) O(n) O(n) O(n)
    스택(stack) O(n) O(n) O(1) O(1)
    큐(queue) O(n) O(n) O(1) O(1)
    이중 연결 리스트(doubly linked list) O(n) O(n) O(1) O(1)
    해시 테이블(hash table) O(1) O(1) O(1) O(1)
    이진 탐색 트리(BST) O(logn) O(logn) O(logn) O(logn)
    AVL 트리 O(logn) O(logn) O(logn) O(logn)
    레드 블랙트리 O(logn) O(logn) O(logn) O(logn)

    - 자료 구조 최악의 시간 복잡도

    자료 구조 접근 탐색 삽입 삭제
    배열(array) O(1) O(n) O(n) O(n)
    스택(stack) O(n) O(n) O(1) O(1)
    큐(queue) O(n) O(n) O(1) O(1)
    이중 연결 리스트(doubly linked list) O(n) O(n) O(1) O(1)
    해시 테이블(hash table) O(n) O(n) O(n) O(n)
    이진 탐색 트리(BST) O(n) O(n) O(n) O(n)
    AVL 트리 O(logn) O(logn) O(logn) O(logn)
    레드 블랙 트리 O(logn) O(logn) O(logn) O(logn)

     

    5.2 선형 자료 구조

    - 선형 자료 구조란 요소가 일렬로 나열되어 있는 자료 구조를 말한다.

     

    5.2.1 연결 리스트

    - 연결 리스트는 데이터를 감싼 노드를 포인터로 연결해서 공간적인 효율성을 극대화시킨 자료 구조이다.

    - 삽입과 삭제가 O(1)이 걸리며 탐색에는 O(n)이 걸린다.

    - prev 포인터와 next 포인터로 앞과 뒤의 노드를 연결시킨 것이 연결 리스트이며, 연결 리스트는 싱글 연결 리스트, 이중 연결 리스트, 원형 이중 연결 리스트가 있다.

    - 참고로 맨 앞에 있는 노드를 헤드(head)라고 한다.

    ㆍ싱글 연결 리스트 : next 포인터만 가진다.

    ㆍ이중 연결 리스트 : next 포인터와 prev 포인터를 가진다.

    ㆍ원형 이중 연결 리스트 : 이중 연결 리스트와 같지만 마지막 노드의 next 포인터가 헤드 노드를 가리키는 것을 말한다.

    - 이 책에서는 이중 연결 리스트를 기반으로 설명하겠다.

    - 이중 연결 리스트는 앞에서부터 요소를 넣는 push_front(), 뒤에서부터 요소를 넣는 push_back(), 중간에 요소를 넣는 insert() 등의 함수가 있다.

    #include <bits/stdc++.h>
    using namespace std;
    int main() {
        list<int> a;
        for(int i = 0; i < 10 ; i++) a.push_back(i);
        for(int i = 0; i < 10 ; i++) a.push_front(i);
        auto it = a.begin(); it++;
        a.insert(it, 1000);
        for (auto it: a) cout << it << " ";
        cout << '\n';
        a.pop_front();
        a.pop_back();
        for(auto it : a) cout << it << " ";
        cout << '\n';
        return 0;
    }
    /*
    9 1000 8 7 6 5 4 3 2 1 0 0 1 2 3 4 5 6 7 8 9
    1000 8 7 6 5 4 3 2 1 0 0 1 2 3 4 5 6 7 8
    */

     

    5.2.2 배열

    - 배열(array)은 같은 타입의 변수들로 이루어져 있고, 크기가 정해져 있으며, 인접한 메모리 위치에 있는 데이터를 모아놓은 집합이다.

    - 또한, 중복을 허용하고 순서가 있다.

    - 여기서 설명하는 배열은 '정적 배열'을 기반으로 설명한다.

    - 탐색에 O(1)이 되어 랜덤 접근(random access)이 가능하다.

    - 삽입과 삭제에는 O(n)이 걸린다.

    - 따라서 데이터 추가와 삭제를 많이 하는 것은 연결 리스트, 탐색을 많이 하는 것은 배열로 하는 것이 좋다.

    - 배열은 인덱스에 해당하는 원소를 빠르게 접근해야 하거나 간단하게 데이터를 쌓고 싶을 때 사용한다.

     

    랜덤 접근과 순차적 접근

    - 직접 접근이라고 하는 랜덤 접근은 동일한 시간에 배열과 같은 순차적인 데이터가 있을 때 임의의 인덱스에 해당하는 데이터에 접근할 수 있는 기능이다.

    - 이는 데이터를 저장된 순서대로 검색해야 하는 순차적 접근과는 반대이다.

     

    배열과 연결 리스트 비교

    - 배열은 상자를 순서대로 나열한 데이터 구조이며 몇 번째 상자인지만 알면 해당 상자의 요소를 끄집어낼 수 있다.

    - 연결 리스트는 상자를 선으로 연결한 형태의 데이터 구조이며, 상자 안의 요소를 알기 위해서는 하나씩 상자 내부를 확인해봐야 한다는 점이 다르다.

    - 배열 : 랜덤 접근이 가능하다. O(1)

       연결 리스트 : 랜덤 접근이 불가능하다. O(n)

    - 탐색은 배열이 빠르고 연결 리스트는 느리다.

    - 배열의 경우 그저 상자 위에 있는 요소를 탐색하면 되는 반면에, 연결 리스트는 상자를 열어야 하고 주어진 선을 기반으로 순차적으로 열어야 한다.

    - 하지만 데이터 추가 및 삭제는 연결 리스트가 더 빠르고 배열은 느리다.

    - 배열은 모든 상자를 앞으로 옮겨야 추가가 가능하지만, 연결 리스트는 선을 바꿔서 연결해주기만 하면 되기 때문이다.

    #include <bits/stdc++.h>
    using namespace std;
    int a[10];
    int main() {
        for (int i = 0; i < 10; i++) a[i]=i;
        for (auto it : a) cout << it << " ";
        cout << '\n';
        return 0;
    }
    /*
    0 1 2 3 4 5 6 7 8 9
    */

     

    5.2.3 벡터

    - 벡터(vector)는 동적으로 요소를 할당할 수 있는 동적 배열이다.

    - 컴파일 시점에 개수를 모른다면 벡터를 써야 한다.

    - 또한, 중복을 허용하고 순서가 있고 랜덤 접근이 가능하다.

    - 탐색과 맨 뒤의 요소를 삭제하거나 삽입하는 데 O(1)이 걸리며, 맨 뒤나 맨 앞이 아닌 요소를 삭제하고 삽입하는 데 O(n)의 시간이 걸린다.

    - 참고로 뒤에서부터 삽입하는 push_back()의 경우 O(1)의 시간이 걸리는데, 벡터의 크기가 증가되는 시간 복잡도가 amortized 복잡도, 즉 상수 시간 복잡도 O(1)과 유사한 시간 복잡도를 가지기 때문이다.

    - push_back()을 한다고 해서 매번 크기가 증가하는 것이 아니라 2의 제곱승 +1 마다 크기를 2배로 늘리는 것을 볼 수 있다.

    - amortized 복잡도를 가지기에 push_back()은 O(1)의 시간 복잡도를 가진다고 할 수 있다.

    #include <bits/stdc++.h>
    using namespace std;
    vector<int> v;
    int main() {
        for (int i=1;i<=10;i++) v.push_back(i);
        for (int a : v) cout << a << " ";
        cout << "\n";
        v.pop_back();
     
        for (int a : v) cout << a << " ";
        cout << "\n";
     
        v.erase(v.begin(), v.begin()+1);
     
        for (int a : v) count << a << " ";
        cout << "\n";
     
        auto a = find(v.begin(), v.end(), 100);
        if (a == v.end()) cout << "not found" << "\n";
     
        fill(v.begin(), v.end(), 10);
        for (int a : v) cout << a << " ";
        cout << "\n";
        v.clear();
        for (int a : v) cout << a << " ";
        cout << "\n";
     
        return 0;
    }
    /*
    1 2 3 4 5 6 7 8 9 10
    1 2 3 4 5 6 7 8 9
    2 3 4 5 6 7 8 9
    not found
    10 10 10 10 10 10 10 10
    */

    - 뒤부터 요소를 더하는 push_back(), 맨 뒤부터 지우는 pop_back(), 지우는 erase(), 요소를 찾는 find(), 배열을 초기화하는 clear() 함수가 있다.

    for (int a : v) cout << a << '\n';
    for (int i = 0; i < v.size(); i++) cout << v[i] << '\n';
    // 위의 두 코드는 같은 뜻

    - 앞의 코드는 "벡터의 요소를 순차적으로 탐색한다."라는 뜻이며, 전자와 후자 코드는 같다.

    - 예를 들어 벡터 v에 pair라는 값이 들어간다면 어떻게 해야할까?

        ㆍfor ( pair<int,int> a : v ) 방식으로 순회해야 한다.

     

    5.2.4 스택

    - 스택은 가장 마지막으로 들어간 데이터가 가장 첫 번째로 나오는 성질(LIFO, Last In First Out)을 가진 자료 구조이다.

    - 재귀적인 함수, 알고리즘에 사용되며 웹 브라우저 방문 기록 등에 쓰인다.

    - 삽입 및 삭제에 O(1), 탐색에 O(n)이 걸린다.

    #include <bits/stdc++.h>
    using namespace std;
    stack<int> stk;
    int main() {
        ios_base::sync_with_stdio(false);
        cin.tie(NULL);
        for (int i = 0; i < 10; i++) stk.push(i);
        while (stk.size()) {
            cout << stk.top() << " ";
            stk.pop();
        }
    }
    /* 
    9 8 7 6 5 4 3 2 2 1 0
    */

     

    5.2.5 큐

    - 큐(queue)는 먼저 집어넣은 데이터가 먼저 나오는 성질(FIFO, First In First Out)을 지닌 자료 구조이며, 나중에 집어넣은 데이터가 먼저 나오는 스택과는 반대되는 개념을 가졌다.

    - 삽입 및 삭제에 O(1), 탐색에 O(n)이 걸린다.

    - CPU 작업을 기다리는 프로세스, 스레드 행렬 또는 네트워크 접속을 기다리는 행렬, 너비 우선 탐색, 캐시 등에 사용된다

    #include <bits/stdc++.h>
    using namespace std;
    int main() {
        queue<int> q;
        q.push(1);
        cout << q.front() << "\n";
        q.pop();
        cout << q.size() << "\n";
        return 0;
    }
    /*
    1
    0
    */

    - 참고로 C++에서 enqueue()는 push(), dequeue()는 pop()으로 구현되어있다.

     

    5.3 비선형 자료 구조

    - 비선형 자료 구조란 일렬로 나열하지 않고 자료 순서나 관계가 복잡한 구조를 말한다.

    - 일반적으로 트리나 그래프를 말한다.

     

    5.3.1 그래프

    - 그래프는 정점과 간선으로 이루어진 자료 구조를 말한다.

     

    정점과 간선

    - 어떠한 곳에서 어떠한 곳으로 무언가를 통해 간다고 했을 때 '어떠한 곳'은 정점(vertex)이 되고 '무언가'는 간선(edge)이 된다.

    - 예를 들어 사람이 어떤 아파트로 간다고 해보자

        ㆍ사람과 아파트는 하나의 정점이고, 거기로 가는 길이 간선이 된다.

        ㆍ단방향 간선과 양방향 간선이 있다.

    - 정점으로 나가는 간선을 해당 정점의 outdegree라고 하며, 들어오는 간선을 해당 정점의 indegree라고 한다.

    - 정점은 약자로 V 또는 U라고 하며, 보통 어떤 정점으로부터 시작해서 어떤 정점까지 간다를 "U에서부터 V로 간다."라고 표현한다.

    - 지금까지 설명한 정점과 간선으로 이루어진 집합을 '그래프(graph)'라고 한다.

     

    가중치

    - 가중치는 간선과 정점 사이에 드는 비용을 뜻한다.

    - 1번 노드와 2번 노드까지 가는 비용이 한 칸이라면 1번 노드에서 2번 노드까지의 가중치는 한 칸이다.

    - 예를 들어 제가 성남이라는 정점에서 네이터라는 정점까지 가는 데 걸리는 택시비가 13000원이면 성남에서 네이버까지 가는 가중치는 13000원이 된다.

     

    5.3.2 트리

    - 트리는 그래프 중 하나로 그래프의 특징처럼 정점과 간선으로 이루어져 있고, 트리 구조로 배열된 일종의 계층적 데이터의 집합이다.

    - 루트 노드, 내부 노드, 리프 노드 등으로 구성된다.

    - 참고로 트리로 이루어진 집합을 숲이라고 한다.

     

    트리의 특징

    - 트리는 그래프의 일종이며 다음 특징을 가진다는 점이 다르다.

    1. 부모, 자식 계층 구조를 가진다. 같은 경로 상에서 어떤 노드보다 위에 있으면 부모, 아래에 있으면 자식 노드가 된다.

    2. V - 1 = E 라는 특징이 있다. 간선 수는 노드 수 -1 이다.

    3. 임의의 두 노드 사이의 경로를 '유일무이'하게  '존재'한다. 즉, 트리 내의 어떤 노드와 어떤 노드까지의 경로는 반드시 있다.

     

    트리의 구성

    - 트리는 루트 노드, 내부 노드, 리프 노드로 이루어져 있다.

     

    루트 노드

    - 가장 위에 있는 노드를 뜻한다.

    - 보통 트리 문제가 나오고 트리를 탐색할 때 루트 노드를 중심으로 탐색하면 문제가 쉽게 풀리는 경우가 많다.

     

    내부 노드

    - 루트 노드와 내부 노드 사이에 있는 노드를 뜻한다.

     

    리프 노드

    - 리프 노드는 자식 노드가 없는 노드를 뜻한다.

     

    트리의 높이와 레벨

    ㆍ깊이 : 트리에서의 깊이는 각 노드마다 다르며, 루트 노드부터 특정 노드까지 최단 거리로 갔을 때의 거리를 말한다.

    ㆍ높이 : 트리의 높이는 루트 노드부터 리프 노드까지 거리 중 가장 긴 거리를 의미한다.

    ㆍ레벨 : 트리의 레벨이 주어지는 문제마다 조금씩 다르지만 보통 깊이와 같은 의미를 지닌다.

    ㆍ서브트리 : 트리 내의 하위 집합을 서브트리라고 한다. 트리 내에 있는 부분집합이라고도 보면 된다.

     

    이진 트리

    - 이진 트리는 자식의 노드 수가 두 개 이하인 트리를 의미하며, 이를 다음과 같이 분류한다.

    ㆍ정이진 트리(full binary tree): 자식 노드가 0 또는 두 개인 이진 트리를 의미한다.

    ㆍ완전 이진 트리(complete binary tree): 왼쪽에서부터 채워져 있는 이진 트리를 의미한다. 

         마지막 레벨을 제외하고는 모든 레벨이 완전히 채워져 있으며, 마지막 레벨의 경우 왼쪽부터 채워져 있다.

    ㆍ변질 이진 트리(degenerate binary tree): 자식 노드가 하나밖에 없는 이진 트리를 의미한다.

    ㆍ포화 이진 트리(perfect binary tree): 모든 노드가 꽉 차 있는 이진 트리를 의미한다.

    ㆍ균형 이진 트리(balanced binary tree): 왼쪽과 오른쪽 노드의 높이 차이가 1 이하인 이진 트리를 의미한다.

        map, set을 구성하는 레드 블랙 트리는 균형 이진 트리 중 하나이다.

     

    이진 탐색 트리

    - 이진 탐색 트리(BST)는 노드의 오른쪽 하위 트리에서는 '노드 값보다 큰 값'이 있는 노드만 포함되고, 왼쪽 하위 트리에는 '노드 값보다 작은 값'이 들어 있는 트리를 말한다.

    - 이때 왼쪽 및 오른쪽 하위 트리도 해당 특성을 가진다.

    - 이렇게 두면 '검색'을 하기에 용이하다.

    - 왼쪽에는 작은 값, 오른쪽에는 큰 값이 이미 정해져 있기 때문에 10을 찾을려고 한다면 25의 왼쪽 노드들만 찾으면 된다는 것을 자명한다.

    - 보통 요소를 찾을 때 이진 탐색 트리의 경우 O(logn)이 걸린다.

    - 하지만 최악의 경우 O(n)이 걸린다.

    - 그 이유는 이진 탐색 트리는 삽입 순서에 따라 선형적일 수 있기 때문이다.

     

    AVL 트리

    - AVL 트리(Adelson-Velsky and Landis tree)는 앞서 설명한 최악의 경우 선형적인 트리가 되는 것을 방지하고 스스로 균형을 잡는 이진 탐색 트리이다.

    - 두 자식 서브트리의 높이는 항상 최대 1만큼 차이 난다는 특징이 있다.

    - 이진 탐색 트리는 선형적인 트리 형태를 가질 때 최악의 경우 O(n)의 시간 복잡도를 가진다.

    - "이러한 최악의 경우를 배제하고 항상 균형 잡힌 트리로 만들자"라는 개념을 가진 트리가 바로 AVL 트리이다.

    - 탐색, 삽입, 삭제 모두 시간 복잡도가 O(logn)이며 삽입, 삭제를 할 때마다 균형이 안 맞는 것을 맞추기 위해 트리 일부를 왼쪽 혹은 오른쪽으로 회전시키며 균형을 잡는다.

     

    레드 블랙 트리

    - 레드 블랙 트리는 균형 이진 탐색 트리로 탐색, 삽입, 삭제 모두 시간 복잡도가 O(logn)이다.

    - 각 노드는 빨간색 또는 검은색의 색상을 나타내는 추가 비트를 저장하며, 삽입 및 삭제 중에 트리가 균형을 유지하도록 하는 데 사용된다.

    - C++ STL의 set, multiset, map, and multimap이 이 레드 블랙 트리를 이용하여 구현되어 있다.

    - 참고로 "모든 리프 노드와 루트 노드는 블랙이고 어떤 노드가 레드이면 그 노드의 자식은 반드시 블랙이다." 등의 규칙을 기반으로 균형을 잡는 트리이다.

     

    5.3.3 힙

    - 힙은 완전 이진 트리 기반의 자료 구조이며, 최소힙과 최대힙 두 가지가 있고 해당 힙에 따라 특정한 특징을 지킨 트리를 말한다

    ㆍ최대힙 : 루트 노드에 있는 키는 모든 자식에 있는 키 중에서 가장 커야한다.

                    또한, 각 노드의 자식 노드와의 관계도 이와 같은 특징이 재귀적으로 이루어져야 한다.

    ㆍ최소힙 : 최소힙에서 루트 노드에 있는 키는 모든 자식에 있는 키 중에서 최솟값이어야 한다.

                    또한, 각 노드의 자식 노드와의 관계도 이와 같은 특징이 재귀적으로 이루어져야 한다.

    - 힙에는 어떠한 값이 들어와도 특정 힙의 규칙을 지키게 만들어져 있다.

    - 예를 들어 최대힙을 기반으로 설명하면 다음과 같다.

     

    최대힙의 삽입

    - 힙에 새로운 요소가 들어오면, 일단 새로운 노드를 힙의 마지막 노드에 이어서 삽입한다.

    - 이 새로운 노드를 부모 노드들과의 크기를 비교하며 교환해서 힙의 성질을 만족시킨다.

    - 예를 들어 8이라는 값을 가진 노드 밑에 15라는 값을 가진 노드를 삽입한다고 하면, 이 노드가 점차 올라가면서 해당 노드 위에 있는 노드와 스왑하는 과정을 거쳐 최대힙 조건을 만족하게 된다.

     

    최대힙의 삭제

    - 최대힙에서 최댓값은 루트 노드이므로 루트 노드가 삭제되고, 그 이후 마지막 노드와 루트 노드를 스왑하여 또다시 스왑 등의 과정을 거쳐 재구성된다.

     

    5.3.4 우선순위 큐

    - 우선순위 큐는 우선순위 대기열이라고도 하며, 대기열에서 우선순위가 높은 요소가 우선순위가 낮은 요소보다 먼저 제공되는 자료 구조이다.

    - 우선순위 큐는 힙을 기반으로 구현된다.

    - 다음 코드처럼 greater를 써서 오름차순, less를 써서 내림차순으로 바꿀 수 있다.

    - int뿐만 아니라 다른 자료 구조를 넣어서 할 수 있다.

    #include <bits/stdc++.h>
    using namespace std;
    priority_queue<int, vector<int>, greater<int> > pq;	//오름차순
    // priority_queue<int, vector<int>, less<int> > pq;	//내림차순
    int main() {
        pq.push(5);
        pq.push(4);
        pq.push(3);
        pq.push(2);
        pq.push(1);
        cout << pq.top() << "\n";
        return 0;
    }
    /*
    1
    */

    - 오름차순으로 정렬하게 했고 5,4,3,2,1,이 입력되었음에도 우선순위가 높은 1이 출력되는 것을 볼 수 있다.

     

    5.3.5 맵

    - 맵(map)은 특정 순서에 따라 키와 매핑된 값의 조합으로 형성된 자료구조이다.

    - 예를 들어 "이승철" : 1, "박동영" : 2 같은 방식으로 string : int 형태로 값을 할당해야 할 때 map을 쓴다.

    - 레드 블랙 트리 자료 구조를 기반으로 형성되고, 삽입하면 자동으로 정렬된다.

    - 맵을 쓸 때는 map<string, int> 형태로 구현한다.

    - 배열과 비슷하게 clear() 함수로 맵에 있는 모든 요소를 삭제할 수 있으며, size()로 map 크기를 구할 수 있다.

    - 또한, erase()로 해당 키와 키에 매핑된 값을 지울 수 있다.

    - 참고로 map은 해시 테이블을 구현할 때 쓰며 정렬을 보장하지 않는 unordered_map과 정렬을 보장하는 map 두 가지가 있다.

    #include <bits/stdc++.h>
    using namespace std;
    int v[10];
    int main() {
        unordered_map<string, int> umap;
        // 다음과 같이 넣기도 가능하고
        umap.insert({"test1",1});
        // 이렇게 넣을 수도 있다.
        umap.emplace("test5",5);
        // 또한, 이렇게 변경도 가능, 추가할 수도 있다. 다음 형태를 권장한다.
        umap["test1"] = 4;
        
        for (auto element : umap) {
            cout << element.first << " :: " << element.second << '\n';
        }
        // map의 find 메서드는 찾지 못하면 end() 이터레이터를 반환한다.
        auto search = umap.find("test4");
        if (search != umap.end()) {
            cout << "found :" << search -> first << " " << (*search).second << '\n';
        }else {
            cout << "not found.." << '\n';
        }
        // 다음과 같이 ++를 통해 test1이라는 키에 매핑된 int 값을 증가한다.
        umap["test1"]++;
        cout << umap["test1"] << "\n";
        
        cout << umap.size() << "\n";
        umap.erase("test1");
        cout << umap.size() << "\n";
        
        return 0;
    }
    /*
    test5 :: 5
    test1 :: 4
    not found..
    5
    2
    1
    */

    - map을 순회할 때는 키에 해당하는 값(key)을 first, 키에 매핑된 값(value)에 해당하는 값을 second로 탐색 가능하다.

    #include <bits/stdc++.h>
    using namespace std;
    int main() {
        map<string, int> _map;
        _map["큰돌"]++;
        _map["큰돌"]++;
        for (auto c : _map) {
            cout << c.first << " : " << c.second << "\n";
        }
        return 0;
    }
    /*
    큰돌 : 2
    */

     

    5.3.6 셋

    - 셋(set)은 특정 순서에 따라 고유한 요소를 저장하는 컨테이너이며, 중복되는 요소는 없고 오로지 희소한(unique) 값만 저장하는 자료구조 이다.

    #include <bits/stdc++.h>
    using namespace std;
    int main() {
        set<pair<string, int>> _set;
        _set.insert({"test",1});
        _set.insert({"test",1});
        _set.insert({"test",1});
        _set.insert({"test",1});
        cout << _set.size() << "\n";
        return 0;
    }
    /*
    1
    */

    - 여기서 pair 는 두 가지 형을 담을 수 있는 구조이며 first, second로 그 인자에 접근이 가능하다.

    - 이렇게 똑같은 값을 넣었더니 1이 나오는 것을 볼 수 있다.

    - 나머지 사항은 map과 비슷하다.

     

    5.3.7 해시 테이블

    - 해시 테이블은 무한에 가까운 데이터들을 유한한 개수의 해시 값으로 매핑한 테이블이다.

    - 삽입, 삭제, 탐색 시 평균적으로 O(1)의 시간 복잡도를 가지며 unordered_map으로 구현한다.

     


    예상질문

    Q. 해시 테이블을 설명하시오.

    A. 해시 테입르은 무한에 가까운 데이터(키)들을 유한한 개수의 해시 값으로 매핑한 테이블이다.

    - 이를 통해 작은 크기의 캐시 메모리로도 프로세스를 관리하도록 할 수 있다.

     

    Q. 그래프와 트리의 차이점은 무엇인가?

    A. 그래프는 정점과 간선으로 이루어진 자료 구조를 말하며, 트리는 그래프 중 하나로 그래프의 특징처럼 정점과 간선으로 이루어져 있고 트리 구조로 배열된 일종의 계층적 데이터의 집합이다.

    - 루트 노드, 내부 노드, 리프 노드 등으로 구성되며 V-1=E 등의 특징이 있다.

     

    Q. 이진 탐색 트리는 어떤 문제점이 있고 이를 해결하기 위한 트리 중 한 가지를 설명해보세요.

    A. 이진 탐색 트리는 선형적으로 구성될 때 시간 복잡도가 O(n)으로 커지는 문제점이 있다.

    - 선형적으로 구성하지 않고 균형 잡힌 트리로 구성하기 위해 나온 트리로 AVL 트리와 레드 블랙 트리가 있으며, 이 중 AVL트리를 설명하겠다.

    - AVL 트리(Adelson-Velsky and Landis tree)는 스르로 균형을 잡는 이진 탐색 트리이다.

    - 두 자식 서브트리의 높이는 항상 최대 1만큼 차이 난다는 특징이 있다.

    - 탐색, 삽입, 삭제 모두 시간 복잡도가 O(logn)이며 삽입, 삭제를 할 때마다 균형이 맞지 않는 것을 맞추기 위해 트리 일부를 왼쪽 혹은 오른쪽으로 회전시키며 균형을 잡는다.

    'Books > 면접을 위한 CS 전공지식 노트' 카테고리의 다른 글

    4장 데이터베이스  (0) 2023.02.02
    3장 운영체제  (0) 2023.01.31
    2장 네트워크  (0) 2023.01.25
    1장 디자인 패턴과 프로그래밍 패러다임  (0) 2022.12.31

    댓글

Designed by Tistory.