[알고리즘] 정렬 알고리즘
Updated: Categories: CS정렬 알고리즘에 대해 알아보자 (feat. 선택정렬, 삽입정렬, 버블정렬, 합병정렬, 퀵정렬, 힙정렬)
정렬 정리
시간복잡도
안정정렬
- 중복된 값이 존재할때 입력순서를 반영하여 동일하게 정렬함
- 삽입정렬, 버블정렬, 병합정렬
불안정정렬
- 중복된 값이 존재할때 입력순서와 동일하지 않게 정렬됨
- 선택정렬, 퀵정렬, 힙정렬
선택정렬 (Selection sort)
- 변경할 인덱스를 고정하고, 뒷부분에서 최소값을 찾아 교체하는 방식
- i=1 부터 최소값을 찾고 i=0 값과 바꿈
- i=2 부터 최소값을 찾고 i=1 값과 바꿈
- 반복
- 장점
- 제자리정렬이라 추가 메모리 사용 X
- 비교는 많고 교환은 적음
- 단점
- 불안정 정렬임
- 모든 상황에서 시간 복잡도가
O(N^2)
public static int[] selectionSort(int[] arr){
int len = arr.length;
for(int i=0; i<len; i++){
int min = i;
for(int j=i+1; j<len; j++){
if(arr[j] < arr[min]){
min = j;
}
}
int temp = arr[i];
arr[i] = arr[min];
arr[min] = temp;
}
return arr;
}
삽입정렬 (Insertion sort)
- 앞쪽의 정렬된 배열 부분과 그 뒤 원소를 비교해서 앞 배열에 삽입하는 방식
- i=0~0 배열과 i=1을 비교하여 i=1이 작다면 앞 배열에 삽입
- i=0~1 배열과 i=2를 비교하여 i=2가 작다면 앞 배열에 삽입
- 반복
- 장점
- 제자리정렬이라 추가 메모리 사용 X
- 안정 정렬임
- 이미 정렬되어있는 경우 시간복잡도
O(N)
- 단점
- 삽입시 배열을 시프트하기 때문에 변경이 많음
public static int[] insertionSort(int[] arr){
int len = arr.length;
for(int i=1; i<len; i++){
int key = arr[i];
int j = i- 1;
while(j >= 0 && key<arr[j]){
arr[j+1] = arr[j];
j--;
}
arr[j+1] = key;
}
return arr;
}
버블정렬 (Bubble sort)
- 인접한 두 원소를 비교하여 큰 원소를 뒤로 보내는 방식
- i=0과 i=1 원소를 비교 -> i=0>i=1 이라면 서로 교체, 아니면 멈춤
- 반복
- 장점
- 제자리정렬이라 추가 메모리 사용 X
- 안정 정렬임
- 단점
- 비교 + 교환이 매우 많이 일어남
- 모든 상황에서 시간 복잡도가
O(N^2)
public static int[] bubbleSort(int[] arr){
int len = arr.length;
for(int i=0; i<len; i++){
for(int j=0; j<len-i-1; j++){
if(arr[j] > arr[j+1]){
int temp = arr[j];
arr[j] = arr[j+1];
arr[j+1] = temp;
}
}
}
return arr;
}
합병 정렬 (Merge sort)
- 분할-정복 알고리즘 사용
- 재귀호출을 통해 구현함
- 분할(Divide): 입력 배열을 같은 크기의 2개의 부분 배열로 분할한다.
- 정복(Conquer): 부분 배열을 정렬한다. 부분 배열의 크기가 충분히 작지 않으면 순환 호출을 이용하여 다시 분할 정복 방법을 적용한다.
- 결합(Combine): 정렬된 부분 배열들을 하나의 배열에 합병한다
- 이 때 두 배열을 비교하여 정렬하는 작업이 일어난다.
- 장점
- 안정 정렬임
- 데이터 분포에 영향을 받지 않음
- 단점
- 분할된 배열을 합치며 정렬해야 할때 추가 메모리가 필요
- 단 연결리스트 사용시 제자리 정렬로 구현가능
- 분할된 배열을 합치며 정렬해야 할때 추가 메모리가 필요
public static int[] mergeSort(int[] arr){
reculsive(arr, 0, arr.length - 1);
return arr;
}
public static void reculsive(int[] arr, int left, int right){
if(left < right){
int mid = (left + right)/2;
// 분할
reculsive(arr, left, mid);
reculsive(arr, mid+1, right);
// 정복 - 합치면서 정렬
int[] sorted = new int[right-left+1];
int index = 0;
int L = left;
int R = mid+1;
while(L <= mid && R <= right){
if(arr[L] < arr[R]){
sorted[index] = arr[L];
L++;
}
else{
sorted[index] = arr[R];
R++;
}
index++;
}
// 남아있다면 추가해줌
for(int i=L; i<mid+1; i++){
sorted[index] = arr[i];
index++;
}
for(int i=R; i<=right; i++){
sorted[index] = arr[i];
index++;
}
// 정렬된것 원본에 반영
for(int i=0; i<sorted.length; i++){
arr[i+left] = sorted[i];
}
}
}
퀵 정렬 (Quick sort)
- 분할-정복 알고리즘 사용
- i=0의 원소를 pivot으로 설정
- i=1을 left 포인터, i=len-1을 right 포인터로 설정
- left 포인터 원소가 pivot보다 작을땐 건너 뛰고, right 포인터가 pivot보다 클땐 건너뜀
- left 포인터 원소가 pivot보다 크다면 잠시 멈추고 right 포인터가 pivot보다 작을때 서로 원소를 바꾼다
- left > right가 되면 i=0의 원소를 left 위치와 바꿈
- 장점
- 제자리정렬이라 추가 메모리 사용 X
- 단점
- 불안정 정렬임
- pivot이 랜덤하게 선정되기에 데이터 분포에 영향받음
public static int[] quickSort(int[] arr){
reculsive(arr, 0, arr.length-1);
return arr;
}
public static void reculsive(int[] arr, int left, int right){
if(left < right){
// 피봇정하고 피봇 기준으로 정렬
int pivot = left;
int pivotNum = arr[pivot];
int L = left;
int R = right;
while(L < R){
while(arr[L] < pivotNum){
L++;
}
while(arr[R] > pivotNum){
R--;
}
if(L < R){
int temp = arr[L];
arr[L] = arr[R];
arr[R] = temp;
}
}
// 분할하여 반복
reculsive(arr, left, R-1);
reculsive(arr, R+1, right);
}
}
힙 정렬 (Heap sort)
- 불안정 + 제자리정렬
public static void swap(int[] arr, int a, int b) {
int temp = arr[a];
arr[a] = arr[b];
arr[b] = temp;
}
public static void heapify(int arr[], int n, int i) {
int p = i;
int l = i * 2 + 1;
int r = i * 2 + 2;
if (l < n && arr[p] < arr[l]) {
p = l;
}
if (r < n && arr[p] < arr[r]) {
p = r;
}
if (i != p) {
swap(arr, p, i);
heapify(arr, n, p);
}
}
public static int[] heapSort(int[] arr) {
int n = arr.length;
// 최대힙 만들기
for (int i = n / 2 - 1; i >= 0; i--) {
heapify(arr, n, i);
}
// 최대힙으로 배열 정렬하기
for (int i = n - 1; i > 0; i--) {
swap(arr, 0, i);
heapify(arr, i, 0);
}
return arr;
}