정렬(Sort)
정렬이란?
- 일련의 자료들을 자료 중에 포함된 몇 가지 항목을 우선 순서대로 지정하여 그 순서에 따라 모든 자료를 배열(나열)하는 방법
- 컴퓨터 공학분야에서 가장 기본적이고 중요한 알고리즘 중의 하나로 자료 탐색에 있어서 필수적
정렬 알고리즘의 종류
- 버블 정렬 (Bubble Sort)
- 선택 정렬 (Selection Sort)
- 삽입 정렬 (Insertion Sort)
- 쉘 정렬 (Shell Sort)
- 퀵 정렬 (Quick Sort)
- 2원 합병 정렬 (Two-way merge Sort)
- 기수 정렬 (Radix Sort)
- 힙 정렬 (Heap Sort)
정렬 알고리즘
- 단순하지만 비효율적인 방법: 삽입 정렬, 선택 정렬, 버블 정렬
- 복잡하지만 효율적인 정렬: 퀵 정렬, 힙 정렬, 합병 정렬, 기수 정렬
정렬 알고리즘의 평가
- 비교 횟수
- 이동 횟수
버블 정렬(Bubble Sort)
- 레코드가 순서대로 되어있지 않으면 인접한 항목 교환
- 이웃하는 숫자를 비교하여 작은 수를 앞쪽으로 이동시키는 과정을 반복하여 정렬
방법
- a[0]과 a[1]을 비교하여 정렬 순서에 맞도록 교환
- a[1]과 a[2]을 비교하여 정렬 순서에 맞도록 교환
- …
- a[n-2] 와 a[n-1]을 비교하여 정렬 순서에 맞도록 교환
- 제일 큰 원소가 배열의 n-1 위치로 이동 (1라운드)
- 배열 처음부터 다시 비교 정렬
버블 정렬 실행 예
버블 정렬 비교 횟수
- 항목이 4개인 경우
- 3회 + 2회 + 1회 = 6회
- 항목의 개수가 N이면 (N-1) + (N-1) + ... +1
N(N-1) / 2
BubbleSort.java
package sort;
public class BubbleSort {
public static void main(String[] args) {
int[] arr = {50, 40, 90, 10};
bubbleSort(arr);
for(int i = 0; i < arr.length; i++){
System.out.println(arr[i] + " ");
}
}
static void bubbleSort(int[] arr){
// 인접한 2개 요소 교환
// 2개의 값을 교환할 경우 변수에 값을 저장하는 순간 이전 값 없어짐
// 이전 값 보존 위해 임시 장소 필요
int temp;
// 총 패스: 배열 크기 -1번 진행
for(int i = 0; i<arr.length; i++){
// 각 패스별 비교 횟수: 배열 크기 - 패스 횟수
for(int j = 0; j<arr.length-i; j++){
if(arr[j] > arr[j+1]){
temp = arr[j];
arr[j] = arr[j+1];
arr[j+1] = temp;
}
}
}
}
}
선택 정렬(Selection Sort)
- 잘못된 위치에 들어가 있는 원소를 찾아서 그것을 올바른 위치에 넣는 원소 교환 방식으로 정렬
방법
- 가장 작은 원소를 찾아 첫 번재 위치의 원소와 교환
- 두 번째로 작은 원소를 찾아 두 번째 위치의 원소와 교환
- 첫 번째를 키로 지정하고 뒤 원소와 각각 비교해서 작은 값을 맨 앞으로 보내면 1라운드 종료
- 두 번째를 키로 지정하고 ~~ (반복)
선택 정렬 실행 예
SelectionSort.java
package sort;
public class SelectionSort {
public static void main(String[] args) {
int[] arr = {9, 6, 8, 7, 5};
selectionSort(arr);
for(int i = 0; i<arr.length; i++) {
System.out.print(arr[i] + " ");
}
}
static void selectionSort(int[] arr){
int temp;
// 총 라운드 : 배열크기 -1 번 진행
for(int i = 0; i<arr.length-1; i++) {
// i 다음부터 끝가지 비교
for(int j=i+1; j<arr.length; j++){
if(arr[i] > arr[j]){
temp = arr[i];
arr[i] = arr[j];
arr[j] = temp;
}
}
}
}
}
삽입 정렬 (Insertion Sort)
- 배열을 정렬된 부분(앞부분)과 정렬 안 된 부분(뒷 부분)으로 나누고, 정렬 안 된 부분의 가장 왼쪽 원소를 정렬된 부분의 적절한 위치에 삽입하여 정렬되도록 하는 과정을 반복하는 것
방법
- 정렬되어 있지 않은 부분의 왼쪽에서 삽입할 원소 k를 선택
- k를 꺼내서 임시 장소에 넣어 두고 (이동하기 위한 빈자리 확보)
- 정렬되어 있는 부분에서 k보다 큰 원소들을 오른쪽으로 이동하여 k가 들어갈 빈자리 확보
- 확보된 빈자리에 k를 삽입
- 모든 원소들이 정렬될 때까지 반복
정렬 과정
- k를 선택해서 이전 위치에 있는 원소들과 비교
- 첫 번째 위치 원소는 정렬되어 있다고 보고 k는 두 번째 원소부터 시작
- k가 이전 위치에 있는 원소보다 작으면 위치 서로 교환
특징
- 안정된 정렬 방법
- 많은 이동 횟수: 레코드가 클 경우 불리함
- 이미 정렬되어 있으면 효율적
삽입 정렬 시간 복잡도
- 최상의 경우: 이미 정렬되어 있는 경우
- 비교: n-1번
- 이동: 2(n-1) 번
- 최악의 경우: 역순으로 정렬되어 있는 경우(모든 단계에서 앞에 놓은 자료 전부 이동)
- 비교: n(n-1)/2
- 이동: n(n-1)/2 + 2(n-1)
삽입 정렬 실행 예
InsertionSort.java
package sort;
public class InsertionSort {
public static void main(String[] args) {
int[] arr = {40, 60, 70, 50, 10, 20, 30};
insertionSort(arr);
for(int i = 0; i<arr.length; i++) {
System.out.print(arr[i] + " ");
}
}
static void insertionSort(int[] arr){
for(int i = 1; i<arr.length; i++) {
// 선택한 값 (k에 해당)
int temp = arr[i];
// 현재 선택한 값의 이전 원소의 인덱스 저장
int index = i-1;
// 현재 값이 이전 원소보다 작으면
while(index>= 0 && temp < arr[index]) {
// 이전 원소를 한 칸씩 뒤로 이동
arr[index+1] = arr[index];
index--;
}
// 앞의 원소가 현재 값보다 작으면 반복ㅁ누 종료
// 현재 값은 index 번째 원소 뒤로 와야함
// 따라서 index=1위치에 현재 값 저장
arr[index+1] = temp;
}
}
}
쉘 정렬 (Shell Sort)
- 원소의 비교 연산과 먼 거리의 이동을 줄이기 위해 몇 개의 서브 리스트로 나누어 삽입 정렬을 반복 수행
- 삽입 정렬의 개념을 확대하여 일반화시킨 정렬 방법
- 삽입 정렬의 최대 문제점은 요소들이 이동할 때, 이웃한 위치로만 이동한 것
- 쉘 정렬에서는 요소들이 멀리 떨어진 위치로도 이동할 수 있기 때문에 쉘 정렬이 삽입 정렬보다 정렬 속도가 빠름
방법
- 정렬에서 사용할 간격(interval) 결정
- 전체 원소 수의 n의 1/3, 다시 이것의 1/3, ...
- 간격이 작아질수록 짧은 거리를 이동하고 원소 이동이 적음
- 첫 번째 간격에 따라 서브 리스트로 분할
- 각 서브 리스트에 대해 삽입 정렬 수행
- 두 번째 간격에 따라 서브 리스트로 분할
- 각 서브 리스트에 대해 삽입 정렬 수행
- 리스트 전체에 대해 삽입 정렬 수행(마지막 간격은 1)
쉘 정렬 실행 예
ShellSort.java
package sort;
public class ShellSort {
public static void main(String[] args) {
int[] arr = {30, 60, 90, 10, 40, 80, 40, 20, 10, 60, 50, 30, 40, 90, 80};
shellSort(arr);
for(int i = 0; i<arr.length; i++) {
System.out.print(arr[i] + " ");
}
}
static void shellSort(int[] arr){
int N = arr.length;
for(int h=N/2; h > 0; h/=2){
// 삽입 정렬을 하기 위해 서브 리스트의 두 번째 값을 가지고 비교
for(int i=h; i<N; i++){
int idx = i-h;
int temp = arr[i];
// 삽입 정렬 수행
while(idx >= 0 && arr[idx] > temp) {
arr[idx+h] = arr[idx];
idx -= h;
}
arr[idx + h] = temp;
}
// 각 간격마다 정렬 결과 확인
for(int a : arr) {
System.out.print(a + " ");
}
System.out.println();
}
}
}
퀵 정렬
- 분할 정복 정렬 방법의 하나
- 전체 리스트를 2개의 부분 리스트로 분할하고
- 각각의 서브 리스트를 다시 퀵 정렬로 정렬
- 기준 값보다 큰 값들은 오른쪽으로, 작은 값들은 왼쪽으로 재배열
방법
- 전체 리스트에서 한 원소를 기준 값(pivot)으로 선정
- 피벗을 기준으로 두 개의 서브 리스트로 분할
- 왼쪽 서브 리스트: 피벗보다 작은 값의 원소들로 구성
- 오른쪽 서브 리스트: 피벗보다 크거나 같은 값의 원소들로 구성
- 각각의 파티션(서브 리스트)에 대해 다시 퀵 정렬을 순환 적용
- 피벗 선정 및 파티션 분할
퀵 정렬 수행 순서
- 피벗(pivot): 가장 왼쪽 숫자를 피벗으로 선정
- 두 개의 변수: low와 high(이동)
- low는 왼쪽에 위치
- high는 오른쪽에 위치
- low: 오른쪽으로 이동하면서 피벗보다 큰 숫자를 찾음
- 피벗보다 작으면 통과, 큰 숫자를 찾으면 정지
- high: 왼쪽으로 이동하면서 pivot보다 작은 숫자를 찾음
- 피벗보다 크면 통과, 작은 숫자를 찾으면 정지
- 정지된 위치의 숫자들을 서로 교환
- low와 high가 교차하면 종료, high와 pivot 교환
퀵 정렬 실행 예
QuickSort.java
package sort;
public class QuickSort {
public static void main(String[] args) {
int[] arr = {3, 7, 8, 5, 2, 1, 9, 5, 4};
quickSort(arr, 0, arr.length-1);
for(int i = 0; i<arr.length; i++) {
System.out.print(arr[i] + " ");
}
}
static void quickSort(int[] arr, int low, int high){
if(low >= high) return;
int pivot = low;
int i = low + 1;
int j = high;
int temp;
// 교차할 때까지 반복. j가 i보다 크면 while문 종료
while(i <= j){
// 피벗보다 큰 값을 만날 때가지 반복
while(i<=high && arr[i] < arr[pivot]){
i++;
}
// 피벗보다 작은 값을 만날 때까지 반복
while(i>low && arr[j] >= arr[pivot]) {
j--;
}
// 교차한 상태가 되면 -> 피벗과 j값 교환
if(i > j){
temp = arr[j];
arr[j] = arr[pivot];
arr[pivot] = temp;
}
// i와 j 교환
else {
temp = arr[i];
arr[i] = arr[j];
arr[j] = temp;
}
}
quickSort(arr, low, j-1);
quickSort(arr, j+1, high);
}
}
2원 합병 정렬(Two-way merge)
- 정렬된 2개의 리스트를 혼합하여 완전히 정렬된 하나의 리스트로 합하는 정렬 방식
2원 합병 정렬 실행 예
기수 정렬(Radix Sort)
- 정렬할 원소의 키 값을 나타내는 숫자의 자릿수(radix)를 기초로 정렬하는 기법(사전식 정렬 방법)
- 정렬하려고 하는 값을 버킷에 넣어 정렬
- 버킷으로 큐 사용
- 이터가 입력되는 곳, 출력되는 곳: 먼저 들어온 것이 먼저 출력
- 프린터, 은행 번호표
- 버킷의 개수는 키의 표현 방법과 밀접한 관계
- 이진법을 사용한다면 버킷은 2개
- 알파벳 문자를 사용한다면 버킷은 26개
- 십진법을 사용한다면 버킷은 10개
방법
- 첫 번째 패스 (1라운드)
- 정렬할 키의 가장 작은 자릿수를 기준으로 분배 정렬
- 두 번째 패스
- 두 번째 작은 자리수를 기준으로 분배 정렬
- 키 값이 같은 경우 이전 정렬에서의 순서를 그대로 유지
- 키의 가장 큰 자릿수까지 반복 수행
1자리 수의 기수 정렬 실행 예
- 단순히 자리 수에 따라 버킷에 넣었다가 꺼내면 정렬됨
두 자리 수의 기수 정렬 실행 예
- 일의 자리 수
- 십의 자리 수
- 초기 리스트:
a = [35, 81, 12, 67, 93, 46, 23, 26, 21]
힙 정렬 (Heap Sort)
- 정렬할 원소를 힙(heap)이라는 자료 구조를 이용하여 정렬하는 방법
- 힙 구조
- 완전 이진트리로서 각 노드의 키 값이 자식 노드들의 키 값보다 작지 않은 트리
- 힙 루트는 트리 중 가장 큰 값을 나타냄
방법
- 주어진 데이터로 이진트리 구성
- 힙 구조 알고리즘과 힙 정렬 알고리즘을 수행
- 힙 구조 알고리즘
- 제일 늦게 구성된 트리 선택
- 부모 노드와 자식 노드 중 가장 큰 값 선택
- 부모 노드와 자식 노드 중 가장 큰 값을 비교하여 자식 노드가 크면 서로 위치 교환(큰 값을 위로 보냄)
- 힙 정렬 알고리즘
- 근 노드의 값을 배열의 맨 마지막에 저장(근 노드 삭제에 해당)
- 빈 근 노드의 자리에선 자식 노드 가장자리 가장 큰 값 위치
- 빈 자식 노드의 자리에는 서브 트리의 자식 노드 중 맨 마지막 노드 값으로 채움
- 맨 마지막 노드부터 차례로 제거됨
힙 정렬 실행 예
- 초기 상태: 6, 5, 3, 1, 8, 7, 2, 4
힙 정렬 수행 과정
- 1단계: 이진 트리 구성
- 2단계: 힙 구조 알고리즘 수행
- 3단계: 힙 정렬 알고리즘 수행
'프로그래밍 언어 > 자료구조 & 알고리즘' 카테고리의 다른 글
자료구조 - 해싱 (0) | 2021.12.01 |
---|---|
자료구조 - 탐색 (0) | 2021.12.01 |
자료구조 - 트리 (0) | 2021.11.30 |
자료구조 - 연결리스트(1) 연결 리스트 (0) | 2021.11.29 |
자료구조 - 순차리스트(2) 큐(Queue), 데크(Deque) (0) | 2021.11.29 |