안녕하세요. 우주신 입니다.


이번 포스팅에서는 계수정렬(Count Sort)에 대해 알아보겠습니다.


앞선 포스팅에서의 삽입, 퀵, 힙 정렬 등의 방법은 정렬할 때 두 값을 비교하며 정렬을 했다.

이를 Comparison Sort라고도 부르는데, 이와 반대로 계수정렬은 비교를 하지 않고 정렬을 하기 때문에 

Non-Comparison Sort의 방법 중 하나이다.


[4 1 3 4 3] 리스트를 정렬하면서 자세히 알아보자.



우선, 두 개의 리스트 B, C를 만들어준다. 

B는 A 리스트를 정렬하여 결과물을 넣어줄 리스트이다.

C(Counting List)의 Index는 곧 A의 요소이고, C의 값은 A의 요소들의 갯수이다.

예를 들어 A에 4가 총 몇 개 나온지는 C의 4번째 Index를 보면 알 수 있다. 

그러므로, C 리스트의 길이는 A 리스트의 값들 중 가장 큰 값이 될 것 이다.



처음에는 C를 0으로 다 초기화 해준다.



이제 A의 첫번째 요소부터 Count하며 C를 업데이트 해준다.

A의 첫번째 요소는 4이므로 C의 4번째 칸에 1을 증가 시켜준다.



A의 다음 요소는 1이므로 C의 1번째 칸에 1을 증가 시켜준다.


.

.

.



이런식으로 A의 마지막 요소까지 순회하며 C를 업데이트하면,

1이 총 1번 나왔고, 2는 0번, 3은 2번, 4는 2번 나온 것을 알 수 있다.


아직 C는 완성되지 않았다. Counting 한 C의 각 값들을 직전 값을 더해 업데이트 해야 한다. 



C의 2번째 칸은 기존 값 0과 1번째 칸의 1을 더한 1이 된다.



마찬가지로, C의 3번째 칸은 기존 값 2에서 2번째 칸의 1을 더한 3이 된다.



C의 4번째 칸은 기존 값 2에서 3번째 칸의 3을 더한 5가 된다.

드디어 C 리스트가 완성이 되었다.


이제 이 C를 가지고 정렬 해보자.


A의 역순으로 훑으며 정렬을 한다.

A의 5번째 칸은 3이 들어있고, C의 3번째 칸으로 가면 3이 있다.



C의 3번째 칸의 3이 A의 5번째 칸의 3이 위치해야할 인덱스 값이다. 

즉, 3은 B 리스트의 3번째 자리에 위치한다. 그리고 위 그림처럼 C의 3번째 값을 1 감소 시킨다.



이번에는 A의 4번째 칸의 요소를 정렬 해야 한다.

4가 들어가 있다. C의 4번째 칸을 보면 5가 있었을 것이고 (위 그림은 1 감소 시킨 그림이다)

B의 5번째 인덱스에 4를 위치 시켜준다. 마찬가지로 C의 4번째 칸의 값을 1 감소 시킨다.



A의 3번째 칸의 요소를 정렬 한다. C의 3번째 칸으로 가면 2가 있었을 것이고

B의 2번째 인덱스에 3을 위치 시키고 C의 3번째 칸의 값을 1 감소 시킨다. 



반복해서, 2번째 값을 정렬 시키고



마지막으로 첫번째 값을 정렬 시키면 정렬이 완성 되었다.



이제 코드를 보며 한번 더 복습해보자. 


여기서  A = arr  //  B = sorted_arr  //  C = count_arr 이다

03:: 정렬되지 않은 리스트 A와 A의 size를 인자로 받는다

05:: 정렬된 결과를 받을 리스트 B를 생성 한다

07:: 리스트 A의 최대값을 찾는다

14:: 리스트 A의 최대값의 크기만큼 리스트 C를 생성 하고 0으로 모두 초기화 한다

20:: A를 돌면서 A 요소의 빈도수 만큼 C의 해당 인덱스에 채워넣는다

26:: C를 업데이트 한다

32:: A를 역순으로 훑으며 C를 참고하여 정렬한다

38:: 정렬된 리스트 B를 A에 복사 했다. (선택사항)



왜 Counting Sort이고, Non-Comparison 방식인지 이해 했을 것이다.

정렬되지 않은 리스트의 값들을 Count한 후에, 이 Count된 결과를 가지고 정렬을 하기 때문에 비교가 필요 없다.


Count Sort의 시간복잡도는 O(n + k)이다.

데이터가 n일 때, A를 Count하며 C에 갯수를 업데이트 하는 데 O(n)이 걸린다.

C를 가지고 A를 역순으로 훑으며 B를 만들 때도 O(n)이 걸린다.

k는 A 리스트 중 가장 큰 값을 가리킨다. 

업데이트 된 C를 바탕으로 직전 요소 값을 현재 요소 값에 더하는 데 k번 만큼 반복해야 하며 이는 O(k)가 걸린다.


그래서 만약 A의 최대값이 매우 큰 값이라면 Count Sort는 비효율적인 알고리즘인 것을 알 수 있다.


수고하셨습니다.





# 참고 # 

 - "Introduction to Algorithm" Cormen, Charles, Ronald, Clifford

- 한양대학교, 정형수 교수님, 알고리즘 수업



'Algorithm > Sorting' 카테고리의 다른 글

[C] Countsort:: 계수정렬  (2) 2018.07.28
[C] Quicksort:: 퀵정렬  (2) 2018.06.24
[C] Heap Sort:: 힙정렬  (0) 2018.06.23
[Python] Merge Sort: 병합 정렬  (0) 2018.03.30
[Python] Insertion Sort: 삽입정렬  (1) 2018.03.29
  1. 지나가는사람 2019.07.08 17:32

    1. A를 뒤에서부터 훑으며 정렬하는 이유는 C를 카운트다운하며 B를 완성하는 상황에서 stable한 정렬을 하기 위함이다.
    2. (초보적이지만)C라는 설계도를 보고 바로 B를 완성하는 것이 아니라 번거롭게 C'를 만들어 A의 요소를 움직여 갖다 꽂는 방식으로 B를 완성하는 이유는, 정렬의 실제 목적이 숫자 정렬이 아닌 key(데이터의 색인에 사용되는 실마리) 정렬이기 때문이다.

    이 설명이 더해지면 조금 더 좋을 것 같아서 그냥 써봅니다. 저같은 초보는 이런 게 궁금했거든요ㅎㅎ
    좋은 글 고맙습니다. 덕분에 계수 정렬이 무엇인지 잘 알고 갑니다!

안녕하세요. 우주신 입니다.


오늘은 퀵정렬(Quick Sort)에 대해 포스팅 하겠습니다.

퀵정렬은 시간복잡도가 O(nlogn)으로 실제로 매우 효율적인 알고리즘이라 자주 사용된다.


우선, 위키피디아의 퀵정렬 정의를 참고해보자.


퀵 정렬은 분할 정복(divide and conquer) 방법을 통해 리스트를 정렬한다.

  1. 리스트 가운데서 하나의 원소를 고른다. 이렇게 고른 원소를 피벗(Pivot)이라고 한다.
  2. 피벗 앞에는 피벗보다 값이 작은 모든 원소들이 오고, 피벗 뒤에는 피벗보다 값이 큰 모든 원소들이 오도록 피벗을 기준으로 리스트를 둘로 나눈다. 이렇게 리스트를 둘로 나누는 것을 분할이라고 한다. 분할을 마친 뒤에 피벗은 더 이상 움직이지 않는다.
  3. 분할된 두 개의 작은 리스트에 대해 재귀(Recursion)적으로 이 과정을 반복한다. 재귀는 리스트의 크기가 0이나 1이 될 때까지 반복된다.

재귀 호출이 한번 진행될 때마다 최소한 하나의 원소는 최종적으로 위치가 정해지므로, 이 알고리즘은 반드시 끝난다는 것을 보장할 수 있다.


그림을 보며 1번, 2번 과정인 분할을 따라가보자.


1. 먼저 피벗(pivot)을 고른다. 여기서는 첫번째 인덱스인 6이 피벗이다.

피벗 자리에 i를 그리고 i 바로 뒤에 j를 초기화 한다.



2. 피벗을 기준으로 앞에는 피벗보다 값이 작은 모든 원소들로, 뒤에는 피벗보다 값이 큰 원소들로 나누자.

원리는 매우 단순하다. j를 1씩 증가시키며 j의 자리에 있는 값이 피벗 값보다 크면 아무 조치를 하지 않고 

j의 자리에 있는 값이 피벗 값보다 작으면 i를 1 증가 시키고 swap 한다.


j 자리의 10은 피벗 값인 6보다 크므로 아무것도 하지 않고 j를 바로 다음 칸으로 옮긴다.


13 또한 피벗 값인 6보다 크므로 j를 다음 칸으로 옮긴다.


이번에는 j 자리의 5가 피벗 값인 6보다 작다. 

이 때, i를 1 증가시키고, j의 자리의 값과 Swap 한다.

밑의 그림처럼 5와 10이 Swap된 것을 알 수 있다.


j를 다음칸으로 이동한다. 8은 피벗 값보다 크므로 아무 것도 하지 않는다.


j를 다음 칸으로 이동한다. 3은 피벗 값보다 작다.


그러므로 i를 1 증가 시킨 후, j의 자리에 있는 3과 i의 자리에 있는 13을 Swap 한다. 


또 다시 j를 다음 칸으로 이동한다.


이번에도 마찬가지로 j의 자리에 있는 2는 피벗 값보다 작으므로 

i를 다음 칸으로 이동 시킨 후와 j의 자리에 있는 2와 i의 자리에 있는 10을 서로 Swap한다.


j를 다음 칸으로 이동한다. 11은 피벗 값보다 크므로 아무 것도 안한다.


j를 다음 칸으로 이동 시켰을 때 배열의 끝이면, i의 자리에 있는 값과 피벗의 값을 Swap 한다.

즉, 피벗인 6과 i의 자리에 있는 2와 Swap 한다.


여기까지가 위 위키피디아 정의에서 설명한 1번과 2번의 과정이다.

이제 3번에서 설명한 것처럼 피벗을 기준으로 분할된 두 개의 리스트를 재귀적으로 1번, 2번 과정을 반복하면 된다.


위 예시에서는 [2, 5, 3]과 [8, 13, 10, 11]의 리스트에서 위와 같이 분할을 하고 

거기서 나눠지는 각각의 리스트들을 대상으로 리스트의 길이가 1이 될 때까지 

분할 과정을 반복하면 정렬이 완성되는 것을 확인할 수 있다. 

코드를 보며 복습해보자.


38:: 인자로 배열 arr과 배열의 시작, 끝 인덱스인 start, end를 받는다.

40:: 피벗의 자리인 p를 선언한다.

43:: 배열의 크기가 1일 때는 멈춰야 한다.

45:: 배열을 분할하고 (1번 2번 과정), 분할이 완성된 후의 피벗을 p에 저장한다.

47:: 피벗을 기준으로 왼쪽 배열을 재귀적으로 퀵정렬 한다. (3번 과정)

48:: 피벗을 기준으로 오른쪽 배열을 재귀적으로 퀵정렬 한다. (3번 과정)



1번과 2번의 분할에 대한 코드이다.

 

13:: 인자로 배열 arr와 start, end 인덱스를 받는다.

15:: 배열의 첫번째 자리의 값을 pivot 변수에 저장한다.

16:: 그리고 i를 첫번째 자리인 start로 초기화 한다.

19:: j를 i+1로 초기화하고 j를 1씩 증가시키며 배열의 끝인 end 자리까지 반복한다.

21:: j 자리의 값이 피벗의 값보다 작다면 i를 다음 칸으로 이동한 후 Swap 한다.

26:: j가 마지막 인덱스까지 이동한 후, 반복문을 나온 뒤 i의 자리의 값과 피벗의 값을 Swap 한다.

27:: pivot의 자리를 반환한다.



다음 시간에는 퀵정렬의 최악의 경우를 방지하기 위한 Randomized Quicksort에 대해 정리해보겠습니다.


고생하셨습니다~






# 참고 # 

 - "Introduction to Algorithm" Cormen, Charles, Ronald, Clifford

- 한양대학교, 정형수 교수님, 알고리즘 수업

'Algorithm > Sorting' 카테고리의 다른 글

[C] Countsort:: 계수정렬  (2) 2018.07.28
[C] Quicksort:: 퀵정렬  (2) 2018.06.24
[C] Heap Sort:: 힙정렬  (0) 2018.06.23
[Python] Merge Sort: 병합 정렬  (0) 2018.03.30
[Python] Insertion Sort: 삽입정렬  (1) 2018.03.29
  1. 쿠케 2019.07.03 22:26

    감사합니다 퀵정렬 코드 이해 안대서 이것저것 찾아보다가 본 코드중에 제일 간단하고 이해잘되네요

  2. CRZ 2021.05.13 04:29

    감사합니다 선생님 많은 도움 됐습니다 ㅠㅠ

안녕하세요. 우주신 입니다. 


오늘은 힙 정렬(Heap Sort)에 대해 알아보겠습니다. 


힙 정렬(Heap Sort)은 힙(Heap) 성질을 이용하여 정렬(Sort)하는 방식이다. 


은 최대값 및 최솟값을 찾아내는 연산을 빠르게 하기 위해 

고안된 완전이진트리(Complete binary tree)를 기본으로 하는 자료구조이다. 


힙은 두 가지가 종류가 있는데, 최대 힙(Max Heaps), 최소 힙(Min Heaps)이다. 

최대 힙은 부모가 자식 노드의 값보다 더 큰 노드 값들로 구성된 트리이다.

밑의 그림은 최대 힙의 예시이다.


[용어 정리]

- 노드, 인덱스 = 주소

- 노드 값 = 주소 안에 있는 값



그리고 트리는 당연히 배열로 표현할 수 있다. 

이 때, 부모 인덱스가 i라면 왼쪽 자식은 2 * i, 오른쪽 자식은 2 * i + 1이 된다. 

**여기서 i가 0 or 1, 어디서 부터 시작하는지 고려해서 코드를 짜야한다.



자, 이제 힙에 대해 알았으니 이를 활용하여 어떻게 정렬하는지 알아보겠다.


힙 정렬을 위해선 크게 두 가지 과정이 필요하다.

 

주어진 데이터를 바탕으로 힙 트리를 구성하고

이 힙 트리를 활용하여 정렬하면 된다.


우리는 여기서 최대 힙을 사용하여 정렬하겠다.



1. 힙 트리 구성 


(1) 주어진 데이터로 구성된 배열을 받아온 후에 중간 지점을 선택한다. 

(2) 중간 인덱스부터 제일 앞 인덱스까지 Heapify 하며 트리를 재구성 한다.


이 부분이 헷갈릴 수 있는데, 정확히 이해하고 넘어가자.

우선, Heapify는 특정 노드를 중심으로 그 밑의 트리들이 힙 성질을 만족하게끔 만드는 작업이다.


밑의 예시를 보면 2번 노드가 Heapify 할 차례라고 하자.

이 때, 2번 노드의 4는 자식 노드 값보다 작으므로 최대 힙 성질을 만족하지 않는다.



힙 성질을 만족하기 위해 밑의 자식 중 더 큰 자식의 4번 노드의 14와 자리를 바꾼다.



그리고 자리가 바뀌었기 때문에 4번 노드를 중심으로 또 Heapify를 한다.

4번 노드의 값 4는 9번 노드의 값 8보다 작기 때문에 최대 힙 성질을 만족하지 못한다. 

4번과 9번 자리를 바꾸면 최종적으로 2번 노드를 중심으로 Heapify가 완성된다. 



중간 노드부터 루트 노드로 Heapify를 하는 이유는 항상 중간 노드부터 자식이 존재하기 때문이다.

예를 들어, 위 그림을 보면 전체 크기가 10인 트리 중에 5번 인덱스 부터 자식이 존재하는 것을 알 수 있다. 


코드를 보자.



먼저, arr 배열과 배열의 사이즈를 인자로 받은 후 배열의 중간 지점을 저장하였다.

int mid = size/2 - 1

여기서 배열의 index가 0부터 시작하는 것을 고려하여 

중간을 (배열 전체 사이즈 / 2 - 1)을 하였다. 


그리고 반복문을 돌며 mid부터 0까지 heapify를 하면 최대 힙이 완성된다.

추상화하여 이해부터 하고 넘어가는 것이 좋을 것 같아서 heapify 코드는 제일 밑에서 설명 했습니다.




2. 정렬


힙 트리를 만들었으면 정렬은 식은 죽 먹기다.

최대 힙의 루트 노드(첫번째 인덱스)는 전체 노드 중 가장 큰 값이므로 이를 활용하면 된다.


(1) 루트 노드의 값을 뽑아 마지막 노드의 값과 바꿉니다. 

(2) 전체 트리의 사이즈를 하나 감소 시킨다. ((1)번에서 뽑은 노드를 가장 큰 수로 마지막 자리에 고정 시켰기 때문에)

(3) 루트 노드를 중심으로 Heapify 하여 힙 트리를 재구성 한다.


그리고 트리의 사이즈가 1이 될 때까지 (1) ~ (3)을 반복하면 정렬이 완성된 것을 알 수 있다.


코드를 보겠습니다.


buildMaxHeap()을 통해 최대 힙을 만들고,

루트 노드, arr[0]을 뽑아 마지막 노드 arr[size-1]와 Swap 한다.

size-- 를 감소 시키며,size가 1이 될 때까지 힙 트리를 heapify 한다.


* Heapify 함수 설명 *


마지막으로 heapify 코드를 보자

힙정렬 중 heapify 코드가 상대적으로 복잡한데, 밑의 코드를 보며 확실히 이해 해보자



여기서 heapify 함수는 인자로, 배열, 배열의 크기, 배열의 중간 인덱스를 받았다.

그리고 그 중간 인덱스를 parent_node에 저장한 후, 왼쪽 자식과 오른쪽 자식의 인덱스도 초기화 한다.

가장 큰 노드를 알아야 하므로 우선은 parent_node를 largest_node로 초기화 한다.


첫번째 if 구문에서 왼쪽 자식 노드가 있는지 확인을 하고 있다면 왼쪽 자식 노드와 부모 노드의 값을 비교한다.

만약에 왼쪽 자식 노드의 값이 부모 노드의 값보다 크다면 largest_node에 부모 노드의 인덱스를 저장한다.


그 후 오른쪽 자식 노드와도 똑같은 방법으로 비교를 한다.


마지막으로 parent_node와 largest_node가 다른지를 확인 하고 다르다면 해당 자식 노드와 Swap을 한다.

만약 Swap이 일어났다면 노드 간의 자리가 바꼈음을 의미하므로 바뀐 노드를 대상으로 재귀적으로 heapify를 한다.





꼭 직접 구현해 보셔서 완벽하게 이해하시길 바랍니다.

Full code는 https://github.com/jjkyun/sorting 참고하시면 됩니다.


고생하셨습니다.




# 참고 # 

 - "Introduction to Algorithm" Cormen, Charles, Ronald, Clifford

- 한양대학교, 정형수 교수님, 알고리즘 수업



'Algorithm > Sorting' 카테고리의 다른 글

[C] Countsort:: 계수정렬  (2) 2018.07.28
[C] Quicksort:: 퀵정렬  (2) 2018.06.24
[C] Heap Sort:: 힙정렬  (0) 2018.06.23
[Python] Merge Sort: 병합 정렬  (0) 2018.03.30
[Python] Insertion Sort: 삽입정렬  (1) 2018.03.29

안녕하세요. 우주신 입니다.


오늘은 병합 정렬(Merge Sort)에 대해 포스팅하겠습니다.


병합 정렬은 정렬되지 않은 전체 데이터를 하나의 단위로 분할한 후에 

분할한 데이터들을 다시 병합하며 정렬하는 방식 입니다.


즉, Divide: n개의 데이터를 n/2개 데이터로 나누고 2개의 리스트에 넣습니다.

Conquer: 2개의 리스트들의 데이터가 하나가 될 때까지 재귀적으로 나눕니다.

Combine: 각각의 2개의 리스트를 병합 합니다.


시간 복잡도는 최악의 경우 O(nlogn)로 이전 포스팅에서 소개했던 insertion sort보다 효율적인 알고리즘 입니다.



코드를 통해 자세히 보겠습니다.


우선, 데이터를 리스트로 입력 받겠습니다.

입력 받은 데이터를 띄어쓰기 기준으로 구분하여 이를 정수형으로 리스트에 저장하는 코드 입니다.



위의 코드가 헷갈리시는 분들은 똑같은 기능을 하는 밑의 코드를 참고해주세요. 



병합 정렬 알고리즘은 merge_sort 함수와 merge 함수를 이용합니다.


1. merge_sort


8 7 6 5 4 3 2 1


8 7 6 5        4 3 2 1


8 7        6 5            4 3        2 1


8     7      6    5          4    3      2    1


2. merge


8     7      6    5          4    3      2    1


7 8        5 6            3 4         1 2


5 6 7 8           1 2 3 4


1 2 3 4 5 6 7 8



merge_sort 함수는 정렬되지 않은 리스트를 인자로 받습니다.



if 구문을 통해 정렬되지 않은 리스트의 데이터가 하나 이하이면 그대로 반환 해줍니다.

그렇지 않은 경우에는 우선, 정중앙을 기준으로 리스트를 나눠야 하는데, 이 때 정중앙의 위치를 mid라는 변수에 저장합니다.

mid를 기준으로 left와 right를 나누고, 다시 각각의 left와 right를 merge_sort의 인자로 전달해 줍니다. (재귀)


하나의 요소로 다 나눴으면 merge 함수에 left1, right1 인자를 넣어 병합 과정으로 넘어갑니다.



정렬된 데이터들을 넣을 sorted_list를 만들어주고,

left, right 길이 안에서 각각 0번째 자리부터 비교를 하고 작은 값을 sorted_list에 넣습니다.

left, right를 비교가 다 끝난 후, 남아 있는 값들을 다 넣어줍니다.



출력 결과를 확인해보면,



깔끔하게 정렬된 것을 확인할 수 있습니다.




'Algorithm > Sorting' 카테고리의 다른 글

[C] Countsort:: 계수정렬  (2) 2018.07.28
[C] Quicksort:: 퀵정렬  (2) 2018.06.24
[C] Heap Sort:: 힙정렬  (0) 2018.06.23
[Python] Merge Sort: 병합 정렬  (0) 2018.03.30
[Python] Insertion Sort: 삽입정렬  (1) 2018.03.29

안녕하세요. 우주신 입니다.


오늘은 삽입 정렬(Insertion Sort)에 대해 포스팅하겠습니다.


알고리즘을 배울 때, 가장 먼저 접하게 되는 친숙한 삽입 정렬입니다.

삽입 정렬은 말 그대로 데이터의 삽입을 통해 정렬을 완성하는 알고리즘 입니다.

현재 위치에서, 이미 정렬된 이전 배열들의 데이터를 차례대로 비교하여 자신의 위치를 찾아 그 위치에 삽입하는 방식 입니다.


시간 복잡도는 최악의 경우 O(n^2)이고, 최선의 경우(이미 정렬되어 있는 경우)에는 O(n) 이므로 Big-O (n^2) 입니다.


5 3 4 6 1 2


5 3 4 6 1 2


3 5 4 6 1 2


3 4 5 6 1 2


3 4 5 6 1 2


1 3 4 5 6 2


1 2 3 4 5 6



파이썬 코드를 통해 자세히 알아보겠습니다.



우선, 데이터를 리스트로 입력 받겠습니다.

입력 받은 데이터를 띄어쓰기 기준으로 구분하여 이를 정수형으로 리스트에 저장하는 코드 입니다.

위의 코드가 헷갈리시는 분들은 똑같은 기능을 하는 밑의 코드를 참고해주세요. 



다음으로는 삽입 정렬 알고리즘 입니다.



j가 비교하려는 값의 위치를 담고 있고,

key는 그 위치에 있는 값을 담고 있습니다.


i는 j의 바로 이전 값을 가리키는 위치 입니다. i의 값을 하나씩 감소시키며 반복문을 도는데,

이 때 i의 위치에 있는 값이 key보다 더 크면 i+1 자리로 값을 옮기고 key보다 값이 작으면 반복문을 빠져나와 오른쪽 자리에 위치합니다.


코드는 간단해서 한줄한줄 보시면 쉽게 이해가실겁니다.



마지막으로 결과 값을 출력 했고 깔끔하게 정렬되서 나오는 것을 확인할 수 있습니다.






'Algorithm > Sorting' 카테고리의 다른 글

[C] Countsort:: 계수정렬  (2) 2018.07.28
[C] Quicksort:: 퀵정렬  (2) 2018.06.24
[C] Heap Sort:: 힙정렬  (0) 2018.06.23
[Python] Merge Sort: 병합 정렬  (0) 2018.03.30
[Python] Insertion Sort: 삽입정렬  (1) 2018.03.29
  1. 캥거루개발자 2020.12.08 10:35

    깔끔한 주석과 쉽게 설명해주셔서 감사합니다!:)

+ Recent posts