인터넷 프로토콜 스택의 4계층 (TCP/IP 4계층)

계층 프로토콜 종류
애플리케이션 HTTP, FTP
전송 TCP, UDP
인터넷 IP
네트워크
인터페이스 
LAN 드라이버, LAN 장비

송신시 동작 순서

각 Layer에서 저장되는 정보

3-way-handshake

   - 데이터 전송전 연결을 확인한다.

  1. 접속요청 : SYN   ->
  2.                            <- ACK+SYN : 요청 수락 + 접속요청
  3. 요청수락 : ACK   ->
  4. 데이터 전송 (참고 : 3.ACK와 함께 데이터 전송 가능)

TCP와 UDP 차이

  TCP UDP
연결지향  연결지향 - 3 way handshake (가상 연결) x, 기능이 거의 없음
데이터 전달 보증 데이터 전달 보증 x
순서 보장 (packet 1, 2, 3 -> 1, 2, 3) 순서 보장 x
신뢰성 신뢰할 수 있다 x
속도 느림 빠름
     
결론 현재는 대부분 TCP 사용 IP와 거의 같다. + port + 체크섬 정도만 추가
애플리케이션에서 추가작업 필요.

PORT

  • 같은 IP 내 프로세스 구분 ( 서버에 두개의 애플리케이션이 띄워져 있고 두개의 애플리케이션에 접근하고자 한다면 Port를 이용해 구분하여 붙을 수 있다 )
  •  따라서 TCP 세그먼트에 출발지, 목적지 PORT 추가.

DNS

  • Domain Name System
  • IP 기억하기 어려우니 사용

 

 

 

<참고> 

해당 포스팅은 인프런의 김영한님이 강의하신 HTTP 강의 를 수강하고, 이해를 정리한 기록들이며 모든 자료의 출처는 강의의 강의자료이다.

'HTTP' 카테고리의 다른 글

HTTP 상태코드  (0) 2022.03.13
HTTP 메서드 활용  (0) 2022.03.06
HTTP 메서드  (0) 2022.03.05
HTTP 기본  (0) 2022.03.01
URI와 웹 브라우저 요청 흐름  (0) 2022.02.24

문제

편의점 주인인 동빈이는 N개의 동전을 가지고 있다.
N개의 동전을 이용하여 만들 수 없는 양의 정수 금액 중 최솟값을 구하라.

예를 들어,

N = 5이고, 각 동전이 각각 3원, 2원, 1원, 1원, 9원짜리 동전이라고 가정하자. 이때 동빈이가 만들 수 없는 양의 정수 금액 중 최솟값은 8원이다.

N = 3이고 각 동전이 각각 3원, 5원, 7원이면, 만들 수 없는 양의 정수 금액 중 최솟값은 1원이다.

입력 

  • 첫째 줄에는 동전의 개수를 나타내는 양의 정수 N이 주어진다. ( 1 <= N 1,000)
  • 둘째 줄에는 각 동전의 화폐 단위를 나타내는 N개의 자연수가 주어지며 각 자연수는 공백으로 구분된다. 각 화폐 단위는 1,000,000 이하의 자연수이다.

출력 

  • 첫째 줄에 주어진 동전들로 만들 수 없는 양의 정수 금액 중 최솟값을 출력한다.

입력 예시

입력


3 2 1 1 9
8

느낀점

이 문제는 처음 보면 그리디 알고리즘이라고 생각하기 힘든 문제였다.

하지만 알고나면 너무 쉬운 문제다.

하나의 유형이라고 생각하고 외워두면 좋을 것 같다.

 

문제를 풀어내는 사고방식은 다음과 같다.

작은 동전부터 탐색하면서, 만들고자 하는 금액(target)을 아직 이용하지 않고 가장 작은 금액의 동전이 만들고자 하는 금액 이하이면 해당 금액을 그 전에 이용한 동전과 조합해 만들 수 있다.

그 전에 이용했던 동전들로 1~target-1의 금액을 만들 수 있고 이용하지 않은 가장 작은 금액의 동전이 target 보다 작다면
     이용하지 않은 가장 작은 금액의 동전 ( <= target) + (1~target-1)  target

이 성립한다.

따라서 동전들을 정렬한 후, 하나씩 뽑으면서 다음 target을 업데이트해주면 된다.

 

#include <iostream>
#include <vector>

using namespace std;

int n;

vector<int> v;
int main(void) {
    cin >> n;
    for (int i = 0; i < n; i++) {
        int x;
        cin >> x;
        v.push_back(x);
    }

    // 1. 정렬
    sort(v.begin(), v.end());

    // 2. 첫번째 target은 1, 이용하기 전의 가장 작은 동전이 target이하여야 target을 만들 수 있다.
    int target = 1;
    for (int i = 0; i < n; i++) {
        if (v[i] <= target) {
            target += v[i];
        }
        else {
            // 만들 수 없는 금액 종료
            break;
        }
    }

    cout << target << "\n";

    return 0 ;
}

정렬에는 여러가지 방식이 있고 다음 4가지 정렬만 우선 다루려고 한다.

설명은 오름차순 기준으로 진행하므로 내림차순은 반대로 생각하면 된다.

 

1. 선택정렬(Selection Sort)

    - O(N^2)

    - k번째 작은 데이터를 가져와 k번째 위치에 놓는다.

#include <iostream>
#include <vector>

using namespace std;

vector<int> arr {7, 5, 9, 0, 3, 1, 6, 2, 4, 8};

int main(void) {

    // 삽입정렬
    for (int i = 0; i < arr.size(); i++) {
        for (int j = i; j > 0; j--) {
            if (arr[j] < arr[j-1]) {
                swap(arr[j], arr[j-1]);
            }
        }
    }

    for (int i = 0; i < arr.size(); i++) {
        cout << arr[i] << " ";
    }
    cout << "\n";
    return 0 ;
}

2. 삽입정렬(Insertion Sort)

    - O(N^2)

    - 데이터를 하나씩 확인하며, 각 데이터를 적절한 위치에 삽입한다.

#include <iostream>
#include <vector>

using namespace std;

vector<int> arr {7, 5, 9, 0, 3, 1, 6, 2, 4, 8};

int main(void) {

    // 선택정렬
    for (int i = 0; i < arr.size(); i++) {
        int min_index = i;
        for (int j = i+1; j < arr.size(); j++) {
            if (arr[min_index] > arr[j]) {
                min_index = j;
            }
        }
        swap(arr[min_index], arr[i]);
    }

    for (int i = 0; i < arr.size(); i++) {
        cout << arr[i] << " ";
    }
    cout << "\n";
    return 0 ;
}

3. 퀵정렬(Quick Sort)

    - O(N^2)

    - 호어 분할 방식 : 리스트에서 첫번째 데이터를 pivot으로 정한다. 이외에도 많은 pivot의 기준을 잡는 방식은 많다.

    1) pivot을 기준으로 왼쪽에서부터 pivot보다 큰 데이터를 찾고  오른쪽부터 pivot보다 작은 데이터를 찾아 교환한다.

    2) 1) 을 반복하다 작은 데이터와 큰 데이터가 바뀌면 작은 데이터와 pivot을 바꾼다.

    3) pivot으로 분할된 왼쪽 리스트와 오른쪽 리스트에 대해 각각 1), 2)를 반복수행한다.

   *C++ sort의 경우 퀵 정렬을 기반으로 하지만 피벗값을 설정할 때 추가적인 로직을 더해 O(NlogN)을 보장한다.

#include <iostream>
#include <vector>

using namespace std;

vector<int> arr {7, 5, 9, 0, 3, 1, 6, 2, 4, 8};

void quickSort(int start, int end) {
    // 원소가 한개인 경우 종료
    if (start >= end) {
        return;
    }

    // 호어 분할 방식 : pivot은 첫번째 원소
    int pivot = start;
    int left = pivot+1;
    int right = end;

    while (left <= right) {
        // pivot보다 큰 데이터를 찾을 때까지 반복 (배열을 초과하지 않을 정도로만)
        while (left <= end && arr[left] <= arr[pivot]) {
            left++;
        }
        // pivot보다 작은 데이터를 찾을 때까지 반복 (pivot index보다 커야함)
        while (right > start && arr[right] >= arr[pivot]) {
            right--;
        }
        // 엇갈렸다면 작은 데이터와 pivot을 교체
        if (left > right) {
            swap(arr[right], arr[pivot]);
        }
        // 엇갈리지 않았다면 큰 데이터와 작은 데이터를 교체
        else {
            swap(arr[left], arr[right]);
        }
    }

    // 분할 후, 왼쪽 리스트와 오른쪽 리스트에서 각각 반복수행
    quickSort(start, right-1);
    quickSort(right+1, end);

}

int main(void) {

    quickSort(0, arr.size()-1);

    for (int i = 0; i < arr.size(); i++) {
        cout << arr[i] << " ";
    }
    cout << "\n";
    return 0 ;
}

계수 정렬(Counting Sort)

  • O(N+K)
  • 정렬된 리스트에 가장 작은 데이터부터 큰 데이터까지의 빈도를 세서 저장해주는 것이다. 말 그대로 count다.
  • 가장 큰 데이터와 가장 작은 데이터의 차이가 1,000,000을 넘지 않을 때 효과적이다. (이를 넘어가면 공간 복잡도를 초과한다.)
  • 즉, 시간 복잡도를 위해 공간 복잡도를 희생한다.
  • 말도 안되게 빠른 정렬을 요하는 문제는 무조건 계수정렬이다.
#include <iostream>
#include <vector>

#define MAX_VALUE 9

using namespace std;

int n = 15;
// 모든 원소의 값이 0보다 크거나 같다고 가정
int arr[15] = {7, 5, 9, 0, 3, 1, 6, 2, 9, 1, 4, 8, 0, 5, 2};
// 모든 범위를 포함하는 배열 선언(모든 값은 0으로 초기화)
int cnt[MAX_VALUE + 1];

int main(void) {
    for (int i = 0; i < n; i++) {
        cnt[arr[i]] += 1; // 각 데이터에 해당하는 인덱스의 값 증가
    }
    for (int i = 0; i <= MAX_VALUE; i++) { // 배열에 기록된 정렬 정보 확인
        for (int j = 0; j < cnt[i]; j++) {
            cout << i << ' '; // 띄어쓰기를 기준으로 등장한 횟수만큼 인덱스 출력
        }
    }
}

'알고리즘' 카테고리의 다른 글

[Algorithm] 그리디 알고리즘  (0) 2022.02.05
  DFS BFS
동작원리 스택
구현방법 *재귀함수 이용 큐 자료구조 이용
시간복잡도 O(N) O(N)

*재귀함수 : 자기 자신을 다시 호출하는 함수, 종료조건을 반드시 명시해야 한다.

 

DFS 동작과정

  1. 탐색 시작 노드를 스택에 삽입하고 방문 처리를 한다.
  2. 스택의 최상단 노드에 방문하지 않은 인접 노드가 있으면 그 인접 노드를 스택에 넣고 방문 처리를 한다. 방문하지 않은 인접 노드가 없으면 스택에서 최상단 노드를 꺼낸다. (방문처리 순서가 중요하다. 스택에 삽입되면서 준비상태가 되면 방문처리가 되어야 한다.)
  3. 2번 과정을 더 이상 수행할 수 없을 때까지 반복한다.

    - 재귀함수 자체가 스택형식으로 작동한다. (함수호출 - 스택에 삽입, 함수 종료 - 스택에서 제거)

    - 즉, DFS는 재귀함수를 이용하면 스택을 따로 선언하여 사용할 필요가 없다. 따라서 BFS보다 구현이 간편하다.

 

#include <iostream>
#include <vector>

using namespace std;

vector<vector<int> > graph = {
    vector<int> {},
    vector<int> {2, 3, 8},
    vector<int> {1, 7},
    vector<int> {1, 4, 5},
    vector<int> {3, 5},
    vector<int> {3, 4},
    vector<int> {7},
    vector<int> {2, 6, 8},
    vector<int> {1, 7}
};
bool visited[101];

// DFS 함수 정의
// 탐색 시작 노드 스택 삽입
void dfs(int now) {
    // 현재 노드를 방문 처리
    visited[now] = true;
    cout << now << " ";
    // 현재 노드의 인접 노드 중 방문하지 않은 노드 재귀적으로 방문
    for (int next : graph[now]) {
        if (!visited[next]) {
            dfs(next);
        }
    }
// 현재(최상단) 노드 스택에서 꺼내기
}
int main(void) {

    dfs(1);
    cout << "\n";

    return 0 ;
}

 

BFS 동작과정

  1. 탐색 시작 노드를 큐에 삽입하고 방문 처리를 한다.
  2. 큐에서 노드를 꺼내 해당 노드의 인접 노드 중에서 방문하지 않은 노드를 전부 큐에 삽입하고 방문처리를 한다. (방문처리 순서가 중요하다. 큐에 삽입되면서 준비상태가 되면 방문처리가 되어야 한다.)
  3. 2번 과정을 더 이상 수행할 수 없을 때까지 반복한다.

    - 일반적인 경우 BFS가 DFS보다 성능이 좋다.

 

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

using namespace std;

vector<vector<int> > graph = {
    vector<int> {},
    vector<int> {2, 3, 8},
    vector<int> {1, 7},
    vector<int> {1, 4, 5},
    vector<int> {3, 5},
    vector<int> {3, 4},
    vector<int> {7},
    vector<int> {2, 6, 8},
    vector<int> {1, 7}
};
bool visited[101];

// BFS 함수 정
void bfs(int start) {
    queue<int> q;

    // 탐색 시작 노드 큐에 삽입
    q.push(start);
    // 현재 노드를 방문 처리
    visited[start] = true;
    
    // 큐가 빌때까지 반복
    while(!q.empty()) {
        // 큐에서 원소 하나 뽑기
        int now = q.front();
        cout << now << " ";
        q.pop();

        // 해당 원소와 연결된, 아직 방문하지 않은 노 큐에 삽입 후 방문처리
        for (int next : graph[now]){
            if (!visited[next]) {
                q.push(next);
                visited[next] = true;
            }
        }
    }
}
int main(void) {

    bfs(1);
    cout << "\n";

    return 0 ;
}

 

결론

      트리의 깊이를 묻는 문제는 BFS를 사용하자. 둘 다 사용할 수 있어도 BFS를 쓰자.

 

그리디 알고리즘(Greedy algorithm) : 탐욕법, 현재 상황에서 지금 당장 좋은 것만 고르는 방법

 

그리디 알고리즘의 대표적인 예제는 거스름돈이다.

마트에 가서 현금으로 결제하면 직원은 우리에게 '가장 큰 화폐단위'부터 돈을 거슬러 준다.

그것이 가장 적은 화폐수로 거스름돈을 줄 수 있는 방법이기 때문이다.

 

정당성을 어떻게 보장하는가?

 

그리디 알고리즘의 정당성 : 그리디 알고리즘을 이용해도 '최적의 해'를 찾을 수 없는 경우가 많다.

 

거스름돈의 예에서, 어떤 화폐는 그것보다 작은 단위의 배수이므로 작은 단위의 동전들을 종합해 다른 해가 나올 수 없나는 사실이 정당성을 증명한다. 

우리나라 화폐는 10, 50, 100, 500, 1000, 5000, 10000, 50000이다. 즉 배수로 증가함을 확인할 수 있다.

반면에, 600원짜리 화폐가 등장한다면 그리디 알고리즘의 정당성을 충족하지 못한다.

 

이처럼 그리디 알고리즘의 문제에서는 문제풀이 아이디어를 그리디로 접근했다면 이것이 정당한지를 검토할 수 있어야 한다.

'알고리즘' 카테고리의 다른 글

[Algorithm] 정렬  (0) 2022.02.07

객체지향 프로그래밍이란?

  • 객체 지향 프로그래밍은 컴퓨터 프로그램을 명령어의 목록으로 보는 시각에서 벗어나 여러 개의 독립된 단위, 즉 "객체"들의 모임으로 파악하고자 하는 것이다. 각각의 객체는 메시지 를 주고받고, 데이터를 처리할 수 있다. (협력)
  • 객체 지향 프로그래밍은 프로그램을 유연하고 변경이 용이하게 만들기 때문에 대규모 소프트웨어 개발에 많이 사용된다.

객체지향의 특징 4가지

  • 추상화
  • 캡슐화
  • 상속
  • 다형성

역할과 구현으로 분리(다형성)

  • 자바 언어의 다형성을 활용
  •  역할 = 인터페이스
  •  구현 =  클래스, 객체
  • 단순해져 변경에 유연해진다.
  • 클라이언트를 변경하지 않고, 서버의 유현기능만을 유연하게 변경 가능하다.
  • 확장에 유연
  • 인터페이스를 안정적으로 잘 설계하는 것이 중요하다

객체지향에선 다형성이 가장 중요한다.

스프링은 다형성을 극대화하여 사용할 수 있게 도와준다. ex) IoC, DI

 

SOLID
클린코드로 유명한 로버트 마틴이 좋은 객체 지향 설계의 5가지 원칙을 정리

  • SRP: 단일 책임 원칙(single responsibility principle)

         - 한 클래스는 하나의 책임만 가져야 한다.

         - 변경이 있을 때 파급효과가 적어야 한다.

  • OCP: 개방-폐쇄 원칙 (Open/closed principle)

         - 확장에는 열려 있으나 변경에는 닫혀 있어야 한다.

         - 다음과 같이 다형성을 활용해 역할과 구현을 분리해도 서버단의 구현객체를 변경하려면 클라이언트 코드를 변경해야 한다.

MemberRepository m = new MemoryMemberRepository(); //기존 코드
MemberRepository m = new JdbcMemberRepository(); //변경 코드

 

         - 객체를 생성하고, 연관관계를 맺어주는 별도의 조립, 설정자가 필요하다.

  • LSP: 리스코프 치환 원칙 (Liskov substitution principle)

      - 프로그램의 객체는 프로그램의 정확성을 깨뜨리지 않으면서 하위 타입의 인스턴스로 바꿀 수 있어야 한다.
      -
    부모 클래스의 행동규약을 자식 클래스가 위반하지 말아야 한다.

    ex) 오버라이딩 잘 못 하는 경우 :

      1. 자식 클래스가 부모 클래스의 변수의 타입을 바꾸거나 메소드의 파라미터 또는 리턴값의 타입 or 갯수를 바꾸는 경우
      2. 자식 클래스가 부모 클래스의 의도와 다르게 메소드를 오버라이딩 하는 경우

     

  • ISP: 인터페이스 분리 원칙 (Interface segregation principle)

          - 특정 클라이언트를 위한 인터페이스 여러 개가 범용 인터페이스 하나보다 낫다.

             즉, 역할을 여러 개로 구별하여 쪼개자.

  • DIP: 의존관계 역전 원칙 (Dependency inversion principle)

         - 추상화에 의존해야지 구체화에 의존하면 안된다.

            즉, 인터페이스에 의존하게 만들자.

         - 위의 OCP 예제 코드도 구현에 의존되어 있다.

 

결론 : 다형성만으로는 OCP, DIP를 지킬 수 없다.

          이를 spring에서 지원해준다.

'Spring > core' 카테고리의 다른 글

의존관계 자동 주입  (0) 2022.04.23
컴포넌트 스캔  (0) 2022.04.03
싱글톤 컨테이너  (0) 2022.04.02
스프링 컨테이너와 스프링 빈  (0) 2022.04.02
스프링 핵심 원리 이해 - 객체 지향 원리 적용  (0) 2022.04.02

지도(그래프)에서 동서남북으로 움직이는 문제는 자주 출제되는 문제유형인 것 같다.

2차원 배열에서 상하좌우로 1칸씩 움직일 수 있고 배열의 범위를 초과하는 것을 다뤄야 할 경우가 많다.

그래서 2차원 배열 동서남북 움직이기를 템플릿으로 만들어 두면 좋을 것 같다.

 

다음은 R, L, U, D를 입력 받아 동서남북으로 움직이는 전형적인 문제의 답안이다.

움직이는 좌표를 설정하고 공간을 벗어나는 경우를 continue로 무시하여 깔끔하게 코드를 작성하는 법이라 생각한다.

#include <iostream>
#include <vector>

using namespace std;

int r = 1, c = 1;
int n;
string plans;

// 동, 서, 남, 북 방향
int dr[4] = {0, 0, 1, -1};
int dc[4] = {1, -1, 0, 0};
char move_types[4] = {'R', 'L', 'D', 'U'};

int main(void) {
    cin >> n;
    cin.ignore();
    getline(cin, plans);

    // 이동 계획을 하나씩 확인
    for (int i = 0; i < plans.size(); i++) {
        char plan = plans[i];

        // 이동 후 좌표 구하기
        int tr = r, tc = c;
        for (int j = 0; j < 4; j++) {
            if (plan == move_types[j]) {
                tr += dr[j];
                tc += dc[j];
            }
        }

        // 공간을 벗어나는 경우 무시
        if (tr < 1 || tr > n || tc < 1 || tc > n) {
            continue;
        }

        r = tr;
        c = tc;
    }

    cout << r << " " << c << "\n";

    return 0;
}

 

C++에서는 순열과 조합을 각각 next_permutation, prev_permutation을 이용해 쉽게 구할 수 있다.

next_permutation

C++의 algorithm 헤더에는 n개의 원소의 순열을 구할 수 있는 next_permutation이라는 함수가 있습니다. 기본적 문법은 다음과 같습니다. 

 

1
2
3
4
5
6
7
// default
bool next_permutation (BidirectionalIterator first,
                         BidirectionalIterator last);
 
// custom
bool next_permutation (BidirectionalIterator first,
                         BidirectionalIterator last, Compare comp);
cs

 

next_permutation은 순열을 구할 컨테이너(쉽게 말해 배열)의 시작과 끝 iterator를 인자로 받습니다. 만약 해당 컨테이너에 다음 순열이 존재하면 그 컨테이너의 원소를 해당 순열 순서로 바꾸고 true를 반환하고, 다음 순열이 없다면 false를 반환합니다. 위 코드의 custom에서 볼 수 있듯이 비교 함수 comp를 인자로 넘기는 것도 가능합니다. 

 

 

 

next_permutation을 사용할 때는 몇 가지 주의할 점이 있습니다. 

  1. 오름차순으로 정렬된 값을 가진 컨테이너로만 사용가능합니다.
  2. default로 operator < 를 사용해 순열을 생성합니다. 즉 오름차순으로 순열을 생성합니다. 
  3. 중복이 있는 원소들은 중복을 제외하고 순열을 만들어줍니다. 

3번의 예시를 들자면 {0, 0, 1}과 같은 배열의 순열을 구한다면 중복을 제외한

{0, 0, 1}, {0, 1, 0}, {1, 0, 0}이 됩니다. 이를 이용해 조합(Combination)을 구할 수 있습니다. 


출처: https://mjmjmj98.tistory.com/38 [Live passionate😎]

 

<순열 - 중복 포함하지 않음> 

#include <iostream>
#include <vector>
#include <algorithm>

using namespace std;

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

int main(void) {

    // 1. next_permutation 사용을 위한 v 오름차순 정렬
    sort(v.begin(), v.end());

    // 2. 오름차순으로 정렬된 순열 출력
    do {
        for (auto it = v.begin(); it != v.end(); it++) {
            cout << *it << " ";
        }
        cout << endl;
    }
    while (next_permutation(v.begin(), v.end()));

    return 0 ;
}

<조합 - 중복 포함하지 않음>

#include <iostream>
#include <vector>
#include <algorithm>
#include <functional>

using namespace std;

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

int main(void) {

    // 1. prev_permutation 사용을 위한 t 내림차순 정렬
    sort(t.begin(), t.end(), greater<int>());

    // 2. 내림차순으로 정렬된 데이터를 받아서 순열로 뽑고 그 부분만 v에서 뽑아내 조합으로 만들기
    do {
        for (int i = 0; i < v.size(); i++) {
            if (t[i]) {
                cout << v[i] << " ";
            }
        }
        cout << endl;
    }
    while (prev_permutation(t.begin(), t.end()));

    return 0 ;
}

do-while은 쓰면 안 좋다지만, 위의 경우는 예외로 쓰도록 하자.

참고로, next_permutation, prev_permutation 함수는 터미널에서는 기본 제공되지만, 백준과 같은 사이트에서 사용하려면

#include <algorithm>을 해줘야 한다.

 

시간복잡도

 - next_permutation 자체가 O(n)이고 nPr 자체는 O(n!) 이므로 위 템플릿처럼 순열 조합을 구현하면 O(n*n!)이 된다.

+ Recent posts