HTTP 헤더 개요 

HTTP 헤더는 양이 많다. 크게 '일반 헤더'와 '캐시와 조건부 요청 관련 헤더'로 분류했다. 

이번 시간은 헤더의 모양, 용도, 스펙에 따른 변화를 알아보자. 

HTTP 헤더의 모양 

헤더필드 

필드이름:필드값 

필드 이름은 대소문자 구분이 없다. 필드값은 띄어 쓰기를 허용한다. 

HTTP 메시지 

스타트 라인 다음에 헤더 내용이 오고, 다음에 메시지 바디가 온다. 

헤더의 용도

HTTP 전송에 필요한 모든 부가정보. 

표준 헤더 내용이 매우 많다. 필요 시 임의의 헤더 필드를 추가할 수 있다. 

 

RFC 2616 과거의 헤더와 메시지 바디

과거의 헤더 분류는 아래와 같이 4가지다. 

General 헤더, Request 헤더, Response 헤더, Entity 헤더 

 

과거의 메시지 바디란? 

메시지 본문은 Entity 본문을 전달하는데 사용한다. 

Entity 본문은 요청이나 응답에서 전달할 실제 데이터를 의미한다. 

Entity 헤더는 Entity 본문의 데이터를 해석할 수 있는 정보를 제공한다. 

Entity 를 안쓰고, 표현(Representation) 이라는 용어를 쓰기 시작 

표현 = 표현 메타데이터 + 표현 데이터 

1991년 RFC 2616이 폐기되고, 2015년 RFC7230~7235가 나오면서 많이 변경됬다. 

RFC 7280(최신) 메시지 본문 

메시지 본문(== payload)을 통해 표현 데이터를 전달한다. 

표현은 요청이나 응답에서 잔달할 실제 데이터를 의미한다. 

표현 헤더는 표현 데이터를 해석할 수 있는 정보를 제공한다. 

 

회원 이라는 리소스를 html 형태로 전달할 수 있고, json 형태로 전달할 수도 있다. 

그래서 실제 전달하는 형태를 '표현'이라는 용어로 정의했다. 

 


1. 표현과 관련된 헤더 

표현 헤더는 요청, 응답 두 경우 모두 사용한다.

표현 헤더 종류 4가지

1. Content-Type 표현 데이터의 형식 

ex) text/html, application/json, image/png, etc.. 

 

2. Content-Encoding 표현 데이터의 압축 방식 

ex) gzip, deflate, identity(압축 안함)

데이터를 보내는 쪽에서 압축 후 인코딩 헤더를 추가한다. 데이터를 읽는 쪽에서 인코딩 정보를 기반으로 압축을 해제한다. 

 

3. Content-Language 표현 데이터의 자연 언어 

ex) ko, en, en-us

한국어, 중국어, 영어 모두 제공하는 웹페이지를 떠올려보자. 

 

4. Content-Length 표현 데이터의 길이(byte 단위)

참고) Transfer-Encoding(전송 인코딩)을 사용하면 Content-Length를 쓰면 안된다.

전송 코딩은 chunk로 나뉘기 때문에 전체 길이가 아니라  chunk 마다의 길이를 포함하기 때문이다. (아래에서 배움)


2. 협상 헤더 ( == 콘텐츠 네고시에이션)

클라이언트가 원하는 표현 우선순위를 서버에 요청한다.  서버는 이것에 가능한 맞춰서 응답을 준다. 

협상 헤더는 요청 시에 만 사용한다. 

 

1. Accept: 클라이언트가 선호하는 미디어 타입 전달 
2. Accept-Charset: 클라이언트가 선호하는 문자 인코딩 
3. Accept-Encoding: 클라이언트가 선호하는 압축 인코딩
4. Accept-Language: 클라이언트가 선호하는 자연 언어 

Accept-Language 적용 전 시나리오 

클라이언트가 한국에서 브라우저를 이용하는데 외국 사이트에 접속했다. 

다중 언어를 지원하는 서버는 응답을 줄 때 기본으로 영어를 선택한다. 

Accept-Language 적용 후 시나리오 

클라이언트가 Accept-Lanuage: ko 협상 헤더를 담아서 요청한다. 

서버가 한국어를 지원할 수 있다면 한국어를 선택하여 응답을 준다.

협상과 우선순위 (Quality Values) 

협상 헤더의 우선순위 시나리오 예시 

클라이언트가 Accept-Language: ko 협상 헤더를 실어서 요청했다. 
서버는 ko를 지원하지 않는 상황이다. 그리고 서버의 1순위(기본)독일어이고 2순위 영어 를 지원한다. 
클라이언트는 1순위 한국어 2순위 영어 로 응답 받고 싶다. 

 

1. Quality Values(q)를 사용한다.

Quality Values(q)는 0에서 1까지의 값을 갖는다. 생략하면 1이다. 

클라이언트의 협상 헤더 Accept-Lanuage:ko-KR, ko; 1=0.9, en-US; 1=0.8, en;q=0.7

-> 한국어 1순위로 선호. 2순위로 영어를 선호. 

따라서 서버는 2순위 영어 페이지를 응답해준다. 

 

2. 구체적인 것이 우선한다.

아래 중에 어떤 것이 우선 적용될까? 우선순위대로 나열해보자. 

text/* , text/plain , text/plain;format=flowed, */* 

 

[ 우선순위 대로 나열 ]

1순위 text/plain;format=flowed 

2순위 text/plain

3순위 text/* 

4순위 */*


3. 전송 방식 헤더 4가지 

1. 단순 전송 

contents 의 길이를 알고 있을 때 사용한다. 

 

2. 압축 전송 

content-Encoding: gzip

서버에서 gzip으로 압축했을 때, content-Encoding: gzip 전송 헤더 필드를 넣어서 gzip임을 클라이언트에 알려야한다. 

 

3. 분할 전송 

Transfer-Encoding: chunked 

큰 데이터를 "길이와 데이터'로 구성된 덩어리(chunk)들로 분할해서 보낸다.

이 때는 content-length 헤더를 넣으면 안된다. 덩어리가 여러개라서 content-length를 모르기 때문이다. 

 

4. 범위 전송

Range, Content-Range 

클라이언트: "1001부터 2000까지 주세요" 라고 요청 

서버 : "1002부터 2000까지 보냅니다" 라고 응답 


김영한님의 모든 개발자를 위한 HTTP 웹 기본 지식을 공부하고 요약했습니다. 

728x90

'프로그래밍 > HTTP' 카테고리의 다른 글

HTTP 헤더 - 일반 헤더(2)  (0) 2021.12.14
HTTP 4xx 클라이언트 오류 5xx 서버 오류  (0) 2021.12.14
HTTP 리다이렉션  (0) 2021.12.14

4xx 클라이언트 오류 

클라이언트 요청에 잘못된 문법 등으로 서버가 요청을 수행할 수 없음 

오류의 원인이 클라이언트에 있다! 

[중요] 4xx 오류인 경우, 재요청해도 실패한다.

이미 요청이 틀렸기 때문이다. 

하지만 서버 문제(5xx)인 경우, 재시도 하면 성공할 가능성이 있다.

서버 또는 DB 문제가 복구되면 받은 요청에 응답할 수 있게 되기 때문이다. 

400 Bad Request 

요청 구문, 파라미터, API 스펙이 맞지 않을 때 클라이언트 오류. 

백엔드 개발자는 모든 입력의 경우의 수를 생각해서 검증로직 validation을 철저히 해야 한다. 

401 Unauthorized

인증 실패. WWW-Authenticate 헤더와 함께 인증 방법을 설명

참고

인증 : 본인이 누구인지 확인(로그인)

인가 : 리소스 접근 권한 확인(일반 유저? 관리자?)

403 Forbidden

인가 실패. 

인증은 됬는데(로그인은 됬지만) 접근할 수 없는 리소스를 요청한 경우.

404 Not Found 

요청 리소스를 찾을 수 없음

클라이언트 요청 url에 오타 있을 수 있다. 

클라이언트가 권한이 없는 리소스에 접근할 때 403임을 숨기고 싶을 때, 일부러 404를 쓰기도 한다. 


5xx 서버 오류 

서버 문제 (NPE, DB 등의 문제)

서버 문제라서 클라이언트가 재시도 몇 번 하면 응답을 받을 수도 있다. 

500 Internal Server Error

서버 내부 문제 전반적으로 500 코드로 처리 

503 Service Unavailable

서비스 이용 불가. 

503보다는 거의 500을 내려준다. 

서버 개발자는 사용자에게 500에러를 보여주면 안된다 

비즈니스 로직에서 예외 처리는 전부 4xx 또는 200으로 해결해서 쌩 500 에러 화면을 보여주지 않도록 하자.  


김영한님의 모든 개발자를 위한 HTTP 웹 기본 지식을 공부하고 요약했습니다. 

728x90

'프로그래밍 > HTTP' 카테고리의 다른 글

HTTP 헤더 - 일반 헤더(2)  (0) 2021.12.14
HTTP 헤더 - 일반 헤더(1)  (0) 2021.12.14
HTTP 리다이렉션  (0) 2021.12.14

리다이렉션이란? 

요청을 완료하기 위해 클라이언트 프로그램(주로 웹 브라우저)에게 다시 보내는 것

3xx 응답 결과에 Location헤더가 포함되어 있으면 거기에 명시된 URL로 이동한다.

 

자동 리다이렉트 흐름 예시 

1) 클라이언트 : 이벤트 페이지 /event 요청보냄

2) 서버 : /event 이젠 안씀! 헤더의 Location 필드 실어서 301 코드 응답보냄  Location :/new-event

3) 웹 브라우저가 301코드의 Location 필드를 읽고 URL을  /new-event로 인식. 서버에 /new-event 로 요청보냄 

 

리다이렉션 3가지 종류 

1. 영구 리다이렉션 : 특정 리소스의 URI가 영구적으로 이동 

2. 일시 리다이렉션 : PRG 패턴. 일시적인 변경 ex) 주문 완료 후 주문 내역 화면으로 리다이렉트 

3. 특수 리다이렉션 : 캐시 만료 여부를 서버에 요청. 캐시를 사용하니까 메시지 바디 쓰면 안됨.

 

영구 리다이렉션 

301, 308 둘 다 경로가 완전히 바뀌었음을 알려준다. 

리소스의 URI가 영구적으로 이동한다. 검색 엔진 등에서도 변경을 인지한다. 

 

301: 리다이렉트 요청 메서드가 GET으로 변하고, 본문이 제거될 수도(MAY)있다. 

308: 리다이렉트 시 요청 메서드와 body의 데이터를 유지한다. 

 

실무에서는 post로 와도 get으로 돌리는 형태 301로 구현한다. 

새로운 페이지로 가면 페이지의 내용이나 요구하는 데이터가 달라지는 경우가 많기 때문에 post를 유지할 필요가 거의 없기 때문이다. 

일시 리다이렉션 

302를 실무에서 디폴트로 많이 쓴다. 

302는 리다이렉트 시 요청 메서드가 GET으로 변하고, 본문이 제거될 수도(MAY)있다. 

307 리다이렉트 시 요청 메서드와 본문을 유지한다. 

303 리다이렉트 시 요청 메서드가 GET으로 변경된다. 

 

[중요] PRG패턴 : Post / Redirect / Get 

Post 로 주문 후에 웹 브라우저를 새로고침 하면 어떻게 될까? 

'새로고침'은 '재요청'과 같다. 따라서 이 경우 새로고침 하면 post를 2번 요청한 것이 되니까 중복 주문이 발생하는 문제가 있다. 

post로 주문 요청 후에, 주문 결과 화면을 get메서드로 리다이렉트 한다. 

이렇게 하면 '새로고침' 해도 post가 아니라 get요청이 되니까 중복 주문을 피할 수 있다. 

 

그래서 뭘 써야 하나요?

처음 302 스펙의 의도는 HTTP 메서드를 유지하는 것이었다. 하지만 대부분의 웹브라우저들이 GET으로 바꿔버린다. 

모호한 302를 보완하기 위해 303, 307이 등장한다. 

303, 307을 권장하지만 이미 현실의 많은 애플리케이션 라이브러리들이 302를 디폴트로 사용중이다. 

자동 리다이렉션 시에 GET으로 변해도 되면 302를 써도 문제가 없다. 

 

기타 리다이렉션 

304 Not Modified 

캐시로 리다이렉트 한다. 엄청 자주 쓴다. 

클라이언트에게 "리소스가 수정되지 않았으니 로컬PC에 저장된 캐시를 재사용하세요" 라고 알리면서 캐시로 리다이렉트 한다. 

캐시로 리다이렉트 하니까 응답에 메시지 바디를 포함하면 안된다. 

 


김영한님의 모든 개발자를 위한 HTTP 웹 기본 지식을 공부하고 요약했습니다. 

728x90

'프로그래밍 > HTTP' 카테고리의 다른 글

HTTP 헤더 - 일반 헤더(2)  (0) 2021.12.14
HTTP 헤더 - 일반 헤더(1)  (0) 2021.12.14
HTTP 4xx 클라이언트 오류 5xx 서버 오류  (0) 2021.12.14

문제

소문난 칠공주 백준 1941

"맞았습니다" 코드

#include <bits/stdc++.h>
#define X first
#define Y second
using namespace std;

// 1941 소문난 칠공주

int room[5][5]; // 입력받음
bool check[25];  // 조합
int temproom[5][5];
bool vis[5][5]; // 방문 체크
int answer;
int dx[] = {0, -1, 0, 1};
int dy[] = {-1, 0, 1, 0};

void checkad(int startx, int starty){

  fill(vis[0], vis[5], 0);

  queue<pair<int,int>> qu;
  qu.push({startx, starty});
  vis[startx][starty] = true;

  int checkcnt = 0;

  while(!qu.empty()){
    int cx = qu.front().X;
    int cy = qu.front().Y;
    qu.pop();
    checkcnt++;

    if(checkcnt==7) break;

    for(int i = 0; i < 4; i++){
      int nx = dx[i] + cx;
      int ny = dy[i] + cy;

      if(nx < 0 || nx >= 5 || ny < 0 || ny >= 5) continue;
      if(!vis[nx][ny] && temproom[nx][ny] == 1) {
        vis[nx][ny] = true;
        qu.push({nx, ny});
      }
    }
  }

  if(checkcnt == 7) answer++;
}

void permu(){

  do{
    fill(temproom[0], temproom[5], 0);

    int cnts = 0, startx =0 , starty =0 ;

    for(int i = 0; i < 25; i++){
      if(!check[i]){
        int x = i/5; // 행
        int y = i%5; // 열
        temproom[x][y] = 1; // 선택한 7칸 표시.
        startx = x, starty = y;
        if(1 == room[x][y]) cnts++; // 이다솜파 카운트
      }
    }

    if(cnts >= 4) checkad(startx, starty); // 이다솜파 4명 이상이면 인접 체크 호출

  }while(next_permutation(check, check+25));
}

int main(void) {
  ios::sync_with_stdio(0);
  cin.tie(0);

  string st;

  for(int i = 0; i < 5; i++){
    cin >> st;
    for(int j = 0; j < 5; j++){
      if(st[j] == 'Y') room[i][j] = 2;
      else room[i][j] = 1; // S 이다솜파
    }
  }

  for(int i = 7; i < 25; i++) check[i] = true;

  permu();
  cout << answer;

  return 0;
}

리뷰

조합과 bfs가 혼합된 유형이었다.
2차원 배열 fill 함수를 틀리게 구현해서 2시간 동안 고생했다.

로직의 순서는 아래와 같다.

 

 

  1. 주어진 학생들위치 배열 room에 입력받기
  2. false가 7개, true가 18개로 구성된 check 배열 만들기.
  3. next_permutation 으로 check배열의 순열을 돌린다. check 배열 false index로 25칸 중에 7칸을 고르는 조합 만들기.
  4. 현재 조합 중에서 (선택된 7칸에서) 이다솜파가 4명 이상 포함되어 있는지 검사하기
  5. 이다솜파가 4명이상 포함되어 있는 경우에, 선택된 7칸이 서로 모두 인접한지 검사하기.
  6. BFS를 돌릴때, 선택된 7개의 칸에 포함되면서도 처음으로 방문한 칸인지 검사해야 한다.
728x90

목차 

0x00 기초정렬

0x01 Merge Sort 

0x02 Quick Sort


0x00 기초정렬

1. 선택정렬 

책꽂이에 꽂힌 책을 높이 순으로 정렬하려면 어떻게 해야 할까? 

제일 큰 숫자를 찾아서 뒤로 보낸다.

그 다음 큰 것을 찾아서 뒤로 보낸다 ... 이렇게 이어질 테니 계산해보면 시간복잡도는 O(N^2) 이다. 

 

선택정렬 코드

int arr[10] = {3, 2, 7, 116, 62, 235, 1, 23, 55, 77};
int n = 10;
for(int i = n-1; i > 0; i--){ // 최대값을 찾으면 i위치로 보낼것이다. i:맨 마지막부터 앞에서 두번째 까지.
  int mxidx = 0;
  for(int j = 1; j <= i; j++){
    if(arr[mxidx] < arr[j]) mxidx = j;
  }
  swap(arr[mxidx], arr[i]);
}

선택 정렬은 가장 큰 수를 찾으면, 인덱스 i로 이동시킨다. 
내부 반복문에서 mxidx 인덱스와 j인덱스 자리의 숫자를 비교한다. 이 반복문이 종료되면 가장 큰 숫자의 인덱스가 mxidx에 저장된다. 
가장 큰 숫자 인덱스 mxidx와 뒤쪽 인덱스 i를 swap한다. ( == 가장 큰 숫자를 뒤로 보낸다)

max_element 함수로 줄여본 코드 

max_element(arr, arr+k) 
// arr[0], arr[1], arr[2], .. , arr[k-1] 에서 최댓값인 원소의 주소를 반환

arr[0]부터 arr[k-1] 까지 탐색한 후 최댓값인 원소 주소를 반환한다. 

arr[k]가 아니라 arr[k-1]임을 주의하자. 

int arr[10] = {3, 2, 7, 116, 62, 235, 1, 23, 55, 77};
int n = 10;
for(int i = n-1; i > 0; i--){ // 맨 마지막부터 앞에서 두번째 까지.
  swap(*max_element(arr, arr+i+1), arr[i]);
}

최댓값을 찾으면 인덱스 9와 swap, 최댓값을 찾으면 인덱스 8과 swap, 최댓값을 찾으면 인덱스 7과 swap..을 반복한다. 

max_element함수는 범위 내의 원소를 모두 살피니까 O(N^2)의 시간복잡도라는 점을 인지하자. 

2. 버블정렬 

인접한 원소가 자기보다 크면 자리를 바꾼다. 보글보글 인접한 원소끼리만 swap하는 모양 때문에 이름이 버블정렬이다. 

이중 반복문이라서 시간복잡도가 O(N^2)다. 

int arr[5] = {-2, 2, 4, 6, 13};
int n = 5;
for(int i = 0; i < n; i++){
  for(int j = 0; j < n-i-1; j++){
    if(arr[j]> arr[j+1]) swap(arr[j], arr[j+1]);
  }
}

정렬 1회전이 끝나면, 가장 큰 숫자가 맨 뒤에 위치한다. 13이 arr[4]에 가 있을 것이다. 

내부 반복문 j에서는 (인덱스 0과 1) (인덱스 1과 2) 이렇게 인접한 숫자를 비교하니까. j를 0부터 n-1까지 순회해야 한다.

그래야 없는 인덱스를 참조하는 런타임 에러가 안난다. 

내부 반복문 종료 조건을 j < n-i-1 로 해도 정상 동작하는 이유는 뭘까?

버블 정렬에서 정렬 1회전이 끝나면, 가장 큰 숫자가 맨 뒤에 위치한다.

 

정렬 1회전이 끝나면 13이 arr[4]에 있다. 인덱스 4에 있는 숫자를 이제 비교할 필요 없다.

정렬 2회전에서는 인덱스 0, 1, 2, 3 까지만 비교하면 된다! 정렬 2회전이 끝나면 6이 arr[3]에 있다. 인덱스 3부터는 비교할 필요 없다.

이렇게 이미 맨 뒤에 정렬이 끝난 숫자는 비교를 안해도 올바른 자리에 가 있기 때문이다. 


0x01 Merge Sort

재귀적으로 수열을 나눠 정렬한 후 합치는 정렬이다. 시간복잡도는 O(NlogN)이다. 

N이 10만 정도라면, O(N^2) 알고리즘의 경우, 10초~1분 소요된다. O(NlogN)알고리즘이면 눈 감았다 뜨기 전에 종료된다. 

1. 두 정렬된 리스트를 합친다는건 어떻게 할까? 

병합정렬을 배우기 전에, 길이가 N, M인 두 정렬된 리스트를 합쳐서 길이 N+M의 정렬된 리스트를 만드는 방법을 알아야 한다. 

그림을 보고 제일 앞에 와야하는 수(가장 작은 수)는 어떻게 택해야 할지 생각해보자.

빨강 리스트와 파랑리스트의 모든 원소를 확인해야 할까?

아니다. 

-> 두 리스트의 가장 앞에 있는 원소만 비교하면 된다! 

왜냐하면 이미 정렬된 리스트이기 때문에 가장 작은 수가 이미 각 리스트의 맨 앞에 있다. 

"-9와 -7만 비교하면 -9가 제일 작으니까 리스트를 합쳤을 때 -9가 제일 앞에 오게 된다" 는 것을 O(1)에 알 수 있다. 

 

그러면 -9 다음에 오는 수는 어떻게 알까?

두 리스트의 맨 앞을 비교하면 1과 -7이 있다. -7을 선택한다.  

다음은? 두 리스트의 맨 앞을 비교하면 6과 1이 있다. 1을 선택한다.  

이렇게 비교 1번 당 1개의 원소를 제자리에 보낼 수 있으니까 정렬된 두 리스트를 합치는건 O(N+M)에 수행 가능하다. 

 

혼자 힘으로 '정렬된 배열 합치기'를 구현해보자.  백준 11782번 배열 합치기 문제를 풀고 오자.

11782번 문제 내 코드  아래는 바킹독님의 코드 

// http://boj.kr/eff70b5b64974fdebbcd7d2a1fdf29a5
#include <bits/stdc++.h>
using namespace std;

int n, m;
int a[1000005], b[1000005], c[2000005];

int main(void) {
  ios::sync_with_stdio(0);
  cin.tie(0);
  
  cin >> n >> m;
  for(int i = 0; i < n; i++) cin >> a[i];
  for(int i = 0; i < m; i++) cin >> b[i];
  int aidx = 0, bidx = 0;
  for(int i = 0; i < n+m; i++){
    if(bidx == m) c[i] = a[aidx++];
    else if(aidx == n) c[i] = b[bidx++];
    else if(a[aidx] <= b[bidx]) c[i] = a[aidx++];
    else c[i] = b[bidx++]; 
  }
  for(int i = 0; i < n+m; i++) cout << c[i] << ' ';
}

2. Merge Sort 는 어떻게 구현할까? 

병합 정렬은 3단계로 요약할 수 있다.

step1. 주어진 리스트를 2개로 나눈다.
step2. 각각의 리스트를 정렬한다. 
step3. 정렬된 두 리스트를 합친다. 

"나눈 리스트는 어떤 방법으로 정렬할까?"

-> "재귀"에서 아이디어를 얻을 수 있다. 

재귀를 배울 때 도미노가 쓰러지는 것에 비유하여 귀납법을 이해했다. 

"1번 도미노가 쓰러지면 100번 도미노가 쓰러진다." 는 것을 납득시키려면? 

100번 도미노가 쓰러지려면, 99번 도미노가 쓰러져야하고, 99번 도미노가 쓰러지려면 98번 도미노가 쓰러져야 하고, ... 

2번 도미노가 쓰러지려면 1번 도미노가 쓰러져야 한다. 

일단 1번 도미노는 확실히 쓰러지게 되어 있으니까. 따라서 100번 도미노가 쓰러지면 1번 도미노가 쓰러진다.

이러한 귀납법을 병합 정렬에 적용하면. 

길이가 8인 리스트를 정렬하려면, 길이가 4인 리스트를 정렬할 수 있어야 하고. 

길이가 4인 리스트를 정렬하려면 길이가 2인 리스트를 정렬할 수 있어야 하고. 

길이가 2인 리스트를 정렬하려면 길이가 1인 리스트를 정렬할 수 있어야 하는데. 

길이가 1인 리스트는 아주 쉽게 정렬할 수 있으니까. 결국 우리는 {6, -8, 1, 12, 8, 15, 7, -7}를 정렬할 수 있다! 

3. 구현하기 전에 병합 정렬의 시간복잡도를 확인하자

병합 정렬의 전체 과정을 살펴보면, 리스트가 분할하는 과정과 합쳐나가는 과정으로 구분된다. 

1) 분할하는 과정 

분할한다는 것은 관념적으로 분할 되었다고 생각하는 것이다. 

함수 호출의 횟수는 각 줄별로 1, 2, 4, ... , 2의 k승 번이다. 이 횟수는 각 줄에 리스트가 몇 개 있는지 보면 된다. 

예를들어, 3번째 줄을 보자.

리스트가 {6, -8}, {1, 12}, {8, 15}, {7, -7} 총 4개가 있다. 각각에 대해 함수가 호출되니까 3번째 줄은 4번 호출된다. 

 

분할하는 과정에서 함수 호출은 1+2+4+ ... + 2의 k승 == 2N-1 = O(N)번 발생한다

즉, 분할하는 과정의 시간복잡도는 O(N)이다. 

 

2) 합쳐나가는 과정 

합쳐나가는 과정의 시간복잡도는 각 줄에서 모두 N번의 연산이 필요하다

아까 두 정렬된 리스트를 O(A+B)에 구하는 방법을 배웠다. 

그러면 각 줄에서 둘씩 짝지어 합쳐서 다음 단계로 넘어가려면 그냥 전체 원소의 갯수만큼 필요하다는 것을 알 수 있다. 

예를 들어, 5번째 줄을 보자. 

{-8, 6}, {1, 12}, {8,15}, {-7, 7} 길이가 N/4인 4개의 정렬된 리스트들을 2개씩 짝지어 합치는 상황이다. 

필요한 연산의 횟수는 N/4 + N/4 + N/4 + N/4 == N이 된다. 

분할이 완료됬을 때, 리스트의 원소는 1개 였다가 2의 k승 개가 될때까지 매번 2배씩 커지니까 줄은 k개다. 

(1개 -> 2개 -> 4개 -> 8개)

그렇기 때문에 합쳐나가는 과정은 O(Nk) == O(NlogN) 이다. 

분할하는 과정은 O(N)이고 합쳐나가는 과정은 O(NlogN)인데. O(N)보다 O(NlogN)이 더 크다. 

최종적으로 병합 정렬은 O(NlogN)의 시간복잡도를 가진다. 

4. [중요] 이제 직접 Merge Sort를 구현해보자. 

기본 틀 코드를 받아서 스스로 빈칸을 채워보자.

아래는 바킹독님의 정답 코드다. 

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

int n = 10;
int arr[1000001] = {15, 25, 22, 357, 16, 23, -53, 12, 46, 3};
int tmp[1000001]; // merge 함수에서 리스트 2개를 합친 결과를 임시로 저장하고 있을 변수

// mid = (st+en)/2라고 할 때 arr[st:mid], arr[mid:en]은 이미 정렬이 되어있는 상태일 때 arr[st:mid]와 arr[mid:en]을 합친다.
void merge(int st, int en){
  int mid = (st+en)/2;
  int lidx = st; // arr[st:mid]에서 값을 보기 위해 사용하는 index
  int ridx = mid; // arr[mid:en]에서 값을 보기 위해 사용하는 index
  for(int i = st; i < en; i++){
    if(ridx == en) tmp[i] = arr[lidx++];
    else if(lidx == mid) tmp[i] = arr[ridx++];
    else if(arr[lidx] <= arr[ridx]) tmp[i] = arr[lidx++];
    else tmp[i] = arr[ridx++];
  }
  for(int i = st; i < en; i++) arr[i] = tmp[i]; // tmp에 임시저장해둔 값을 a로 다시 옮김
}

// a[st:en]을 정렬하고 싶다.
void merge_sort(int st, int en){
  if(en == st+1) return; // 리스트의 길이가 1인 경우
  int mid = (st+en)/2;
  merge_sort(st, mid); // arr[st:mid]을 정렬한다.
  merge_sort(mid, en); // arr[mid:en]을 정렬한다.
  merge(st, en); // arr[st:mid]와 arr[mid:en]을 합친다.
}

int main(void){
  ios::sync_with_stdio(0);
  cin.tie(0);
  merge_sort(0, n);
  for(int i = 0; i < n; i++) cout << arr[i] << ' '; // -53 3 12 15 16 22 23 25 46 357
}

 

merge함수에서 두 리스트를 합친 결과를 임시롤 저장할 곳이 필요해서 tmp 변수가 필요하다. 

merge_sort함수에서 배열의 크기가 1일 때는 아무것도 안해도 정렬이 완료된다.

이후에는 재귀적으로 절반을 나눠 각각 정렬이 되게 한다. 

 

아직 헷갈리더라도 이건 어려운 내용인것이 맞고 시간과 반복시도가 해결해주는 문제니까 계속 가면 된다.

백준 2751번 문제도 풀어보자. (바킹독님 2751번 정답코드

5. [중요] 병합 정렬의 Stable Sort 성질 

사람들을 나이 순으로 정렬하는 상황이 있다. 

검정셔츠 입은 사람만 38살이고, 나머지 색깔의 사람들은 21살이다. 나이순으로 생각하면 3가지가 모두 올바른 정렬이 맞다.

이렇게 우선 순위가 같은 원소가 여러 개일 땐 정렬 결과가 유일하지 않다!

그런데 그 중에서 우선순위가 같은 원소들 끼리는 원래의 순서를 따라가도록 하는 정렬이 Stable Sort 이다.

즉, 정렬 전에 나이가 21살인 사람의 순서가 파랑, 빨강, 초록이니까. 정렬 후에도 이 순서를 같게 두는 것이다. 

 

병합정렬은 Stable Sort인데. 이 성질을 만족시키려면, a와 b의 우선순위가 같을 때, a를 먼저 선택해줘야 한다. 

아까 병합 정렬은 구현했을 때 Stable Sort성질이 드러난 코드다.

  for(int i = st; i < en; i++){
    else if(arr[lidx] <= arr[ridx]) tmp[i] = arr[lidx++]; 
    // <-- arr[lidx] == arr[ridx]인 경우처럼 우선순위가 같다면? 앞에 있는 lidx를 먼저 선택해준다!
  }

Stable Sort를 응용해보자.

1) 파일을 정렬하는 경우 

조건: 파일을 크기 순으로, 크기가 동일하다면 먼저 생성된 순으로 정렬하자. 

병합 정렬은 Stable Sort임을 기억하자!

따라서 생성 시간은 신경쓰지 않고, 파일을 크기를 기준으로 정렬하면 조건을 만족시킬 수 있다. 

 

2) 2차원 좌표를 정렬하는 경우 

조건: x가 작은 순으로, x가 동일하다면 y가 작은 순으로 정렬하자. 

먼저 y 크기를 기준으로 병합 정렬을 수행하고, 그 다음에 x크기를 기준으로 병합 정렬을 수행하면 된다. 

이렇게 하면 Stable Sort성질 때문에 일단 x가 작은게 앞으로 오게 되기 때문이다. 

그리고 이 상태에서 x가 같다면, x를 정렬하기 전에 앞에있었을 원소, 즉 y가 작은 원소가 앞에 온다. 

 

다만, 2차원 좌표의 정렬은 병합 정렬2번이 아니라 a[aidx]와 b[bidx]를 비교하는 부분을 살짝 바꾸면 1번 정렬로도 해결된다. 


0x02 Quick Sort

퀵 소트는 거의 모든 정렬 알고리즘 보다 빠르다. 

[참고]

만약, 코딩테스트에서 STL을 못 쓰고 직접 정렬을 구현해야 한다면, 퀵 소트를 쓰지 말고, 머지 소트를 써야 한다

1. 퀵 소트는 머지 소트 처럼 재귀적으로 구현되는 정렬이다. 

매 단계마다 pivot이라는 원소를 알맞은 자리로 보내는 작업을 반복한다. 

아래 조건을 만족하면 pivot은 올바른 자리에 있는 것이다. 
1) pivot 왼쪽은 전부 pivot 보다 작은 원소들 
2) pivot 오른쪽은 전부 pivot보다 큰 원소들 

퀵 소트의 장점은 추가적인 공간이 필요 없다는 점이다.

배열 안에서 포인트 변수2개를 이용해 자리 바꿈만으로 처리할 수 있다. (그래서 cache hit rate가 높아서 속도 빠름)

참고로 이렇게 추가적인 공간이 필요 없는 정렬을 In-Place Sort라고 부른다. 

pivot을 제외하고, 양쪽 끝에 포인터를 두고 적절하게 스왑하자. 

우선 l, r 이라는 포인터 2개를 둔다. pivot은 처음에 0번째 원소로 정한다. 

l은 1번째에서 오른쪽으로 이동하고, r은 맨 마지막 인덱스에서 왼쪽으로 이동한다. 

l은 pivot보다 큰 값이 나올때 까지 오른쪽으로 이동한다. r은 pivot보다 작은 값이 나올 때 까지 왼쪽으로 이동한다. 

그 다음 두 포인터가 가리키는 원소의 값을 스왑한다. l은 12에 도달했다. r은 -7에 도달했다. 

또 다시 l을 이동시키면 6보다 큰 8에 도달한다. r은 6보다 작은 3에 도달한다. 

그러면 3과 8을 스왑한다. 

l을 옮기려고 보니 r이 l보다 왼쪽에 와있다!  이렇게 r이 l보다 작아진 순간에는 멈춰서 pivot을 r과 스왑하면 된다. 

여기까지가 pivot하나를 올바른 자리에 두는 과정이다. 

이 방법이 왜 pivot을 제자리로 보내는지 이해하려면

"모든 순간에 l의 왼쪽에는 pivot보다 작거나 같은 원소들만 있고, r의 오른쪽에는 pivot보다 큰 원소들만 있다"는 점만 주목하면 된다. 

2. 퀵 소트를 구현한 코드 

인덱스 l 은 pivot보다 큰 원소를 찾을 때 까지 이동한다. 

인덱스 r 은 pivot보다 작은 원소를 찾을 때 까지 이동한다. 

만약 l 과 r이 타겟을 못찾으면 l > r 이 된다. 인덱스 l과 r이 역전되버린다. 이 때는 break 로 탐색을 멈춘다. 

l 과 r이 타겟을 찾으면, swap(arr[l], arr[r]) 스왑 한다! 

스왑이 종료되면, 인덱스 r과 인덱스 pivot을 스왑한다. 

그리고  pivot을 기준으로 왼쪽과 오른쪽을 보면 pivot이 올바른 자리에 있음을 확인할 수 있다. 

나머지 부분은 재귀호출해서 정렬시킨다. 

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

int n = 10;
int arr[1000001] = {15, 25, 22, 357, 16, 23, -53, 12, 46, 3};

void quick_sort(int st, int en) { // arr[st to en-1]을 정렬할 예정
  if(en <= st+1) return; // 수열의 길이가 1 이하이면 함수 종료.(base condition)
  int pivot = arr[st]; // 제일 앞의 것을 pivot으로 잡는다. 임의의 값을 잡고 arr[st]와 swap해도 상관없음.
  int l = st+1; // 포인터 l
  int r = en-1; // 포인터 r
  while(1){
    while(l <= r && arr[l] <= pivot) l++;
    while(l <= r && arr[r] >= pivot) r--;
    if(l > r) break; // l과 r이 역전되는 그 즉시 탈출
    swap(arr[l], arr[r]);
  }
  swap(arr[st], arr[r]);
  quick_sort(st, r);
  quick_sort(r+1, en);
}

int main() {
  ios_base::sync_with_stdio(0);
  cin.tie(0);
  quick_sort(0, n);
  for(int i = 0; i < n; i++) cout << arr[i] << ' ';
}

 

3. 퀵 소트의 시간복잡도 

pivot 하나를 올바른 자리에 보낼 때 마다 상수번의 연산으로 l과 r이 한 칸씩 가까워진다.

따라서 한 세트의 시간복잡도는 O(N)이다

재귀 종료 조건인 base condition은 구간의 길이가 1 이하일 때다. pivot을 제자리로 보낸 뒤에, 재귀적으로 자기 자신을 호출한다.

pivot 하나를 올바른 자리에 보내는 세트가  O(N)이 걸리는데. 

pivot 을 하나씩 제외하면 N, N-1, N-2 ... 번의 연산이 필요하다. 

4. 퀵 소트의 단점? 

만약 pivot이 매번 완벽하게 중앙에 위치한다면?

리스트를 균등하게 반으로 쪼개서 재귀를 호출하게 된다. 

이 때는 n, n/2, n/4 ... 번의 연산이 필요할 것이다. 그러면 머지 소트때와 같이 세트마다 O(logN)이 걸린다. 

한 세트의 연산횟수 N과 재귀 호출 연산횟수 logN을 곱하면 된다. 

 

물론 항상 pivot이 완벽하게 중앙에 오지 않겠지만 대략 시간복잡도는 O(NlogN)이 된다. 

그리고 앞서 말한 cache hit rate가 높아서 퀵 소트는 속도가 굉장히 빠르다.

 

{1,2,3,4,5,6,7,8} 을 퀵 소트로 정렬하면 시간 복잡도가 얼마일까? 

pivot은 항상 중앙에 있는게 아니라. 제일 왼쪽에 위치하게 된다. 

그래서 한 단계마다 longN개가 아니라 N개를 확인해야 한다. -> 시간복잡도는 O(N^2)이 된다. 

퀵 소트는 평균적으로 O(NlogN)이지만 최악의 경우 O(N^2)이다. 

단순히 제일 왼쪽 값을 pivot으로 선택해보면 지금처럼 리스트가 오름차순 이거나 내림차순일 때 O(N^2)이 된다. 

 

[참고]

STL을 못 쓰는 경우, 정렬을 직접 구현해야 한다면 퀵 소트를 쓰는게 아니라 머지 소트를 구현하라고 한 이유가 여기에 있다. 

최악의 경우 O(N^2)의 시간복잡도인 퀵 소트를 쓸 필요가 없다. 

하지만 대부분의 정렬 라이브러리는 퀵 소트를 쓰는 것도 사실이다. 

pivot을 '잘'고르는 것이 관건인데. 랜덤으로 선택하거나. 후보 3개를 정해서 중앙값을 선택하기도 한다. 

최악의 경우에도 O(NlogN)을 보장하기 위해 일정 깊이 이상의 재귀가 호출되면 힙 소트로 정렬한다.

재귀가 과도하게 호출되면 오버헤드 때문에 성능저하가 오는 데 이를 피하기 위함이다. 이러한 정렬을 Introspective sort라고 한다. 


5. 머지 소트 VS 퀵 소트 

머지소트와 퀵 소트는 둘 다 재귀적 구조로 구현된다. 

먼저 시간복잡도를 보면 머지소트는 무조건 O(NlogN)이고. 퀵 소트는 최악에 O(N^2), 평균적으로 O(NlogN)이다. 

평균적으로 (이름값하는) 퀵 소트가 빠르다. 

퀵 소트는 In-Place Sort 인 점을 기억하자. 

그리고 머지 소트는 우선 순위가 같은 원소들끼리의 원래 순서가 유지되는 Stable Sort이지만 퀵 소트는 아니다. 


정렬1 문제집

백준 문제 내 코드 
2750 수 정렬하기  2750 내 코드
2751 수 정렬하기 2 2751 내 코드(머지소트 구현)
10989 수 정렬하기 3 10989 내 코드 (메모리 제한 주의 )
11931 수 정렬하기 4 11931 내 코드(머지소트 구현)
15688 수 정렬하기 5 15688 내 코드 (머지소트 구현)
10814 나이순 정렬 10814 내 코드
11650 좌표 정렬하기 11650 내 코드
11651 좌표 정렬하기 2 11651 내 코드

 


다음 시간에는 정렬2(Counting Sort, Radix Sort)를 배운다. 

공부 자료 출처 : 바킹독 알고리즘 정렬1

728x90

'알고리즘 > 실전 알고리즘' 카테고리의 다른 글

0x10강 - 다이나믹 프로그래밍  (0) 2021.12.19
0x0F강 - 정렬2  (0) 2021.12.18
0x0C강 - 백트래킹  (0) 2021.12.10
0x0B강 - 재귀  (0) 2021.12.08
0x0A강 - DFS  (0) 2021.12.07

문제

계란으로 계란치기 백준 16987

"맞았습니다" 코드

#include <bits/stdc++.h>
#define X first
#define Y second
using namespace std;
// 16987 계란으로 계란치기

int n, a, b, answer;
vector<pair<int, int>> v;

void dfs(int depth){ // depth == 집어든 계란의 인덱스.

  if(depth == n){
    int crashcnt = 0;
    for(int i = 0; i < n; i++){
      if(v[i].X <= 0) crashcnt++;
    }
    answer = max(answer, crashcnt);
    return;
  }

  // 현재 집어든 계란이 깨졌으면, 그 옆에 다른 계란을 집어든다.
  if(v[depth].X <= 0) dfs(depth+1);
  else {
    bool crashflag = false;
    for (int i = 0; i < n; i++) {
      // i가 현재 집은 계란이거나 이미 깨졌으면 지나감
      if (i == depth || v[i].X <= 0) continue;

      v[i].X -= v[depth].Y;
      v[depth].X -= v[i].Y;

      crashflag = true; // 계란 깼음
      dfs(depth + 1); // 다음 계란을 집는다.

      v[i].X += v[depth].Y;
      v[depth].X += v[i].Y;
    }
    if (!crashflag) dfs(n); // 더이상 깰 계란이 없으면? 개수를 센다.
  }
}
int main(void) {
  ios::sync_with_stdio(0);
  cin.tie(0);

  cin >> n;
  for(int i = 0; i <n; i++){
    cin >> a >> b;
    v.push_back({a,b});
  }

  dfs(0); // 가장 왼쪽(0번째) 계란부터 시작한다.
  cout << answer;
  return 0;
}

리뷰

다시 풀어봐야 할 문제.
백트래킹 카테고리에 있었는데. 문제를 이해 못해서 헤맸다.

집어든 계란을 depth라고 두고, 집어든 계란으로 가능한 많은 계란을 깨면 된다. (공격하면 된다.)
만약 공격할 계란이 남아있지 않거나 내가 집어든 계란이 깨진다면, 집어든 계란을 바꾼다.
집어든 계란을 바꾼다는 의미가 depth를 +1 한다는 의미다.

내가 집어든 계란과 그 계란으로 깰(공격할)계란들이 헷갈려서 갈피를 못잡았다.

728x90

문제

암호 만들기 백준 1759

"맞았습니다" 코드

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

int L, C;
char ch;
vector<int> v;
vector<char> vo = {'a', 'e', 'i', 'o', 'u'};

bool checkvo(string st){
  int mo = 0, ja = 0;
  for(int i = 0; i < L; i++){
    auto it = find(vo.begin(), vo.end(), st[i]);
    if((it - vo.begin()) == vo.size()) {
      ja++;
    }else{
      mo++;
    }
  }

  return (mo > 0 && ja >= 2);
}

void permu(){

  vector<int> tempv(C, 1); // C개 만큼 1 으로 초기화.
  for(int i = 0; i < L; i++){
    tempv[i] = 0;
  } // 선택할 L개만 0으로 초기화

  do{
    string st = "";
    for(int i = 0; i < C; i++){
      if(tempv[i]==0){
        st += v[i];
      }
    }
    if(checkvo(st)){
      cout << st << '\n';
    }
  }while(next_permutation(tempv.begin(), tempv.end()));
}

int main(void) {
  ios::sync_with_stdio(0);
  cin.tie(0);

  cin >> L >> C; // 선택할 개수, 총 개수

  for(int i = 0; i < C; i++){
    cin >> ch;
    v.push_back(ch);
  }
  sort(v.begin(), v.end());
  permu();

  return 0;
}

리뷰

C개 중에 L개를 선택하도록
{0, 0, 0, 0, 1, 1} 배열을 만들어서 next_permutation() 으로 돌렸다.
길이는 L, 0의 개수는 C개 여야 한다.

0인 자리의 문자를 모아서 string 으로 붙였다.
string 에 모음과 자음 개수를 세서 반환하게 했다.

728x90

+ Recent posts