[개념] Sort(정렬)
다양한 정렬 알고리즘의 개념과 구현 방법, 성능 특성을 비교 설명합니다. 교환, 버블, 삽입, 선택 정렬부터 기수, 버킷, 계수, 쉘 정렬, 그리고 합병, 퀵, 힙 정렬까지 각 알고리즘의 작동 원리, 시간복잡도, 공간복잡도, 안정성을 코드와 도식으로 상세히 다루고 있습니다.
다양한 정렬들이 있으며, 이들을 기본적으로 알아야 하기 때문에 공부한 것들을 정리해 보겠다.
우선 그전에 (비)오름차순, (비)내림차순, 분류기준(2가지), Stable, In-space(or not)
을 설명한다.
-
차순
- 모든 값이 다르면 - 오름차순, 내림차순
-
같은 값이 있다면 - 비오름차순, 비내림차순
- 비(Non)
- 예로 비내림차순은 같은 숫자값이 있는것, 비(Non)라서 내림차순 반대인 오름차순
-
분류기준
- Comparison(비교) - 교환, 버블, 삽입, 선택
- Distribution(분배) - radix(기수), bucket(버킷)
-
Stable : 같은 수 입력 순서를 지켜주는 것
- Stable : 삽입, 버블, 병합
- Non Stable : 교환, 선택, 퀵
-
In-space or not(제자리 정렬 or X)
- In-space : QuickSort처럼 메인 메모리에 데이터를 담아둔 공간에서만 정렬을 진행하는 것들
- Not In-space : Radix나 MergeSort(합병정렬)처럼 추가 메모리 공간을 사용하는 것들
Exchange, Bubble, Insertion, Selection Sort
코드를 바로 보고 해석해서 비교하는게 더 간단할 것이다.
-
순서 : 삽입(좌측상단), 버블(우측상단), 선택(좌측하단), 교환(우측하단)
-
삽입정렬 : 앞에서부터 하나씩 삽입하면서 삽입된것들 비교해가며 자기 자리를 찾아가는 구조
즉, 삽입했을때 바로앞과 비교하고 비교한거의 앞에것들도 다 비교해야한다는 것 -
버블정렬 : 2개씩 비교하면서 작은걸 앞으로 옮기면서 한바퀴 지나면 제일 큰게 제일 뒤로 감
이때, 제일 뒤로간 이 숫자를 제외하고 다시 반복하는 구조 -
선택정렬: 교환정렬과 차이는 조건에 맞으면 따로 기억을 했다가 마지막에 swap을 하는 구조
바퀴마다 맨 앞자리가 옳게 채워지므로 이 숫자를 제외하고 다음 바퀴를 반복하는 구조 -
교환정렬 : 선택정렬과 차이는 매번 조건에 맞으면 swap을 하는 구조
바퀴마다 맨 앞자리가 옳게 채워지므로 이 숫자를 제외하고 다음 바퀴를 반복하는 구조
-
삽입정렬 : 앞에서부터 하나씩 삽입하면서 삽입된것들 비교해가며 자기 자리를 찾아가는 구조
-
복잡도 : 전부 최악 N^2
- 삽입정렬은 최선의 복잡도가 N이란 점 참고
Radix, Bucket, Counting, Shell Sort
- 맨 끝자리 기준으로 순서대로 통에 나눠 적고, 0번 통부터 다시 순서대로 배열 정리(끝자리 정렬)
그다음은 10자리 기준으로 반복(십의자리 정렬), 그다음은 100자리 기준으로 반복(전체 정렬 끝)- 통에 갓다오는 횟수 = 자리수 * n번(n은 통 개수)
- 버킷들 각각이 알아서 정렬 후 이를 합하게 되는점이 기수(Radix)와의 차이점이다.
- 어떤 특정수 입장에서 나보다 작은게 몇개 있다라는걸 미리 구하는것
- 따라서 주어진 입력이 얼만지 모르는 그런경우는 미리 카운트를 구해두질 못하니까 사용불가
- 지금처럼 입력 배열이 저렇게 주어져야 구할수가 있음
- 비교간격(Gap)을 큰 것 -> 점차 작은 것으로 변환하여 서로 떨어진 수를 swap
- Gap이 3과 1인 상태를 수행하는 모습을 그림에서 볼 수 있음
MergeSort(합병정렬)
합병(Merge), 합병정렬(Mergesort) 함수를 이용해 재귀형태로 구현
합병(Merge)
두개의 정렬된 배열을 하나의 정렬된 배열로 합병
- 두 배열 차례로 비교해서 더 큰 숫자를 새 배열에 넣는 식이고, 나중에 배열 하나가 먼저 비면 남은 배열꺼를 다넣음
합병정렬(MergeSort)
정렬되지 않은 배열을 인수로 받아서 합병(Merge)도 이용해서 정렬
-
입력 배열을 절반으로 나눈 2개를 mergesort()로 왼쪽 반, 오른쪽 반 호출 후 merge()로 합병 방식
-
mergesort(h, U)나 mergesort(m, V) 재귀 반복해서 제일 마지막 단계까지 가면, 그곳에서 merge() 함수로 합쳐지기 시작
- U, V는 절반으로 나눈 2개 배열
- h, m은 U, V의 크기
- 참고로 오른쪽에 공간을 나타낸것을 보면, 추가로 2n만큼 메모리 공간이 더 필요한 것이 핵심
- 이는 제자리 정렬은 아니라는 의미
- 다만, 이를 개선이 가능하다 (2n -> n으로 개선가능)
공간복잡도 개선한 합병정렬
값을 복사하지않고 index만 가지고 해결
- 변형된 Mergesort 함수
-
변형된 Merge 함수
- 여기선 줄그은 부분이 기존에서 변형된 코드 부분
- 결론은 여기 코드만 봐도 추가로 사용하는 배열이 2개가 아닌 하나라는 점을 알 수 있다.
- 따라서 2n -> n으로 추가메모리 공간을 줄이는 개선을 보여준다.
- 다만, 제자리정렬은 여전히 아니라는 점
- 복잡도 : nlgn
QuickSort(퀵정렬)
quicksort()와 partition() 함수를 이용해 재귀형태로 구현
“분할교환정렬(partition exchange sort)”라 한다. - 분할 알고리즘
기준값이 무엇이든 상관없지만 예로 첫 번째 값을 기준값으로 선정해서,
그보다 작은 것은 왼쪽, 큰 것은 오른쪽으로 나누는게 핵심 아이디어
빠른정렬(QuickSort)
-
partition()를 통해 pivotpoint(=기준값)의 왼쪽, 오른쪽으로 나누고,
-
pivotpoint의 index를 반환 받아서 mid처럼 사용해서 quicksort() 함수를 다시 재귀
분할(Partition)
- partition으로 나눌 때 흐름?
-
15 22 13 27 12 10 20 25 -> 15 13 12 10 22 27 20 25 -> 10 13 12 15 22 27 20 25
- 처음에선 15가 pivotpoint로 시작한것이고,
pivotpoint는 index니까 pivotitem에 해당 index의 값을 저장해서 활용한다. - 두번째에선 아래 코드에서 for문을 다 수행한 순간의 모습이다.
- 세번째에선 아래 코드를 마지막까지 수행한 모습이며,
이때 pivotpoint가 중간인 mid위치의 index를 가지게 된다.
=> 이덕분에 quicksort함수에서 mid처럼 사용하는 것이다.
- 처음에선 15가 pivotpoint로 시작한것이고,
-
15 22 13 27 12 10 20 25 -> 15 13 12 10 22 27 20 25 -> 10 13 12 15 22 27 20 25
-
복잡도 : 최악의 복잡도는 n^2인데, 평균은 nlgn이다.
- 합병정렬이 최악이 nlgn이라서 좋긴 하지만, 메모리 측면에서는 제자리정렬인 퀵이 우수
- 또한, 퀵은 최악 복잡도를 피하는 방법들이 존재
최악 복잡도를 피할 수 있는 방법
최악을 피할 수 있는 방법은 무엇이 있을까?
- 기준값 선택 – 맨앞, 중간, 맨뒤를 구해 3개 중 중간 값 사용방법
- 미리 shuffle – 미리 n정도 복잡도를 사용해 섞어두면, 평균정도의 복잡도는 얻게 됨
HeapSort(힙정렬)
힙 개념
힙의 조건은 Complete Binary Tree, Key(v)>=Key(child(v))
- Complete Binary Tree : 레벨 순서로 채워나가고, 왼쪽부터 오른쪽으로 채워나감
- Key(v)>=Key(child(v)) : 자식보다 키값 같거나 크다는 성질
- 참고로 이것은
Max Heap
을 의미
- 참고로 이것은
힙 만드는 법
힙 만드는 2가지 방법 : Insertion, Adjust
- Insertion : 자식이 부모보다 크다면 자리 교환하면서 자기 자리 끝까지 찾아감(2k, 2k+1)
- 자식이 삽입되어 자기 자리 정해가는 거니까 자식이 주체인것
- Adjust : 부모가 왼쪽, 오른쪽 자식 중 큰 값과 자리 교환하면서 자기 자리 찾아가기
- 자식 있는 부모 노드들만 실행하는것임, 부모가 주체인것
참고로 k를 배열 인덱스로 보는게 핵심. 인덱스를 1부터!
또한, 배열로 봤을때 완전이진트리 자식을 찾아 가는 방식이 2k, 2k+1이라는 것
Insertion힙 만드는 과정만 소개하겠다.
- 참고로 원본 배열 값 : [10, 7, 3, 1, 6, 12, 13, 15, 19]
힙정렬
Heapsort(힙정렬) : 루트와 최하위 값을 바꿔서 최하위를 제외하고 다시 Heap(Adjust) 생성을 반복
-
따라서 힙을 만드는 방법을 아는것이 중요
-
복잡도 : Insertion, Adjust, Heapsort - nlgn
복잡도 상세 설명
해당 복잡도가 나온 이유를 좀 상세히 설명해보겠다.
- Heapsort는 n-1번 힙을 새로 만듬. 힙만드는 Adjust가 1번 동작하는 자체는 lgn걸리므로 힙정렬의 복잡도는 nlgn이라 할 수 있다.
- 당연히 초기에 힙도 아니었을땐 힙 만드는건 Adjust로 nlgn인데, 그 후 맨끝과 루트 값만 서로바꾸고 한번 Adjust로 힙동작하는(lgn)거기 때문에 nlgn이라 볼수있고, 총 nlgn+nlgn=nlgn으로 복잡도를 정의.
- Adjust가 nlgn이라 한것은, 그건 처음에 힙을 구성할때 쭉 뒤에부터접근해서 계속 힙만드는(lgn)을 반복해서 nlgn 이라는 것.
합병, 힙 vs 퀵 정렬 비교
퀵 정렬을 많이 사용하는 이유가 궁금해서 구글링을 해보았다.
퀵 vs 힙
퀵 정렬에서 최악의 경우가 O(n^2) 이고 힙은 최악이든 최선이든 O(NlogN)인데 퀵 정렬이 성능이 보통 더 좋다고 한다.
이유가 무엇일까?
- swap의 횟수 때문이라고 한다.
퀵 vs 합병
합병도 O(NlogN)이고 퀵은 최악이 O(n^2) 인데, 왜 또 퀵 정렬 성능이 보통 더 좋다고 하는것일까?
- 합병은 배열을 생성하는 시간이 있기 때문이라고 한다.
- 따라서 배열의 길이가 길수록 차이가 난다고 한다.
댓글남기기