본문 바로가기
멀티캠퍼스 풀스택 과정/알고리즘

알고리즘1-6. 정렬(Sorting): 버블정렬, 선택정렬, 삽입정렬, 퀵정렬

by 이쟝 2022. 1. 26.

정렬이란?

  • 데이터를 특정한 기준에 따라 순서대로 나열하는 것
  • 키 값이 작은 작은 데이터를 앞쪽에 넣으면 오름차순(ascending order)정렬, 그 반대로 놓으면 내림차순(descending order)정렬
  • 정렬 알고리즘의 핵심요소는 교환, 선택, 삽입이다. 

 

내부정렬 정렬할 모든 데이터를 하나의 배열에 저장할 수 있는 경우에 사용하는 알고리즘
외부정렬 정렬할 데이터가 너무 많아서 하나의 배열에 저장할 수 없는 경우에 사용하는 알고리즘

1. 버블정렬(Bubble Sort)

  • 오름차순 또는 내림차순으로 정렬할 때 인접한 데이터 두개의 대소 관계를 비교해서 크기 순서대로 교환하는 작업
  • 오름차순으로 정렬할 경우 왼쪽값이 오른쪽 값보다 작아야 한다.

 

 

예제) 버블정렬을 구현한 코드 

 

1) 버블정렬을 구현하는 mySort()메서드 생성, 2) 데이터를 교환하는 swap()메서드 생성

 

public class ArrayBubbleSort {

	public static void swap(int a[], int idx1, int idx2) {
		int tmp = a[idx1];  
		a[idx1] = a[idx2];  
		a[idx2] = tmp;       
	}
	
	// 버블정렬: 오름차순   데이터배열, 데이터수
	public static void mySort(int a[], int n) { // 제일 작은 값이 제일 앞에
		// 이중 for문
		for (int i = 0; i < n-1; i++) {  // 배열의 크기만큼 for문 실행
			for (int j = n-1; j>i; j--) { // 배열안의 인접한 숫자 비교(뒤에서부터 비교)
				if (a[j-1] > a[j]) { 	// 부등호 표시만 바꾸면 내림차순
					swap(a, j-1, j);   // 배열, 앞의 값, 뒤의 값
				}
			}
		}
	}

 

3) 전체 코드

 

package al03_sort;

import java.util.Arrays;
import java.util.Random;

public class ArrayBubbleSort {
	public static void swap(int a[], int idx1, int idx2) {
		int tmp = a[idx1];
		a[idx1] = a[idx2];
		a[idx2] = tmp;
	}
    
	// 버블정렬: 오름차순   데이터배열, 데이터수
	public static void mySort(int a[], int n) { // 제일 작은 값이 제일 앞에
		// 이중 for문
		for (int i = 0; i < n-1; i++) {  // 회차
			for (int j = n - 1; j > i; j--) { 
				if (a[j-1] > a[j]) { 
					swap(a, j-1, j);
				}
			}
		}
	}
	
	public static void main(String[] args) {
		// 데이터 준비
		// 1~100까지의 난수를 생성해 크기순으로 정렬(오름차순) 
		
		Random ran = new Random();
		int arr[]  = new int[10];  // 10개의 정수를 저장할 수 있는 배열 생성
		
		for(int i=0; i<arr.length; i++) {
			arr[i] = ran.nextInt(100)+1;  // 1~100까지
		}
		
		System.out.println("정렬전 -> " + Arrays.toString(arr));
        
		mySort(arr, arr.length);
		System.out.println("정렬후 -> " + Arrays.toString(arr));
	}
}

 

더보기

<출력결과> 

정렬전 -> [71, 1, 93, 96, 95, 3, 15, 38, 52, 93]
정렬후 -> [1, 3, 15, 38, 52, 71, 93, 93, 95, 96]

2. 선택정렬(Selection Sort)

  • 처리되지 않은 데이터 중에서 가장 작은 데이터를 선택해 맨 앞에 있는 데이터와 바꾸는 것을 반복
  • 선택 정렬은 N번 만큼 가장 작은 수를 찾아서 맨 앞으로 보내야 해서 시간복잡도가 O(N^2)로 된다. 

 

 

*의사코드*

 

for i = 0 to n:
    a[i]부터 a[n - 1]까지 차례로 비교하여 가장 작은 값이 a[j]에 있다고 하자.
    a[i]와 a[j]의 값을 서로 맞바꾼다.

 

예제) 선택 정렬 소스 코드

 

public class Main {

	public static void main(String[] args) {
		
		// 선택 정렬
		int n = 10;
		int[] arr = {7,5,9,0,3,1,6,4,2,8};
		
		for(int i=0; i<n; i++) {
			int min_index = i;
			for(int j=i+1; j<arr.length; j++) {
				if(arr[min_index]>arr[j]) {
					min_index = j;
				}
			}
			int tmp = arr[i];
			arr[i] = arr[min_index];
			arr[min_index] = tmp;
		}
		
		for(int i=0;i<n;i++) {
			System.out.print(arr[i] + " ");
		}
	}
}

3. 삽입정렬(Insertion Sort)

  • 선택한 요소를 그보다 더 앞쪽의 알맞은 위치에 삽입하는 작업을 반복해 정렬하는 알고리즘
  • 처리되지 않은 데이터를 하나씩 골라 적절한 위치삽입한다.
  • 선택 정렬에 비해 구현 난이도가 높은 편이지만, 일반적으로 더 효율적으로 동작한다.
  • 삽입 정렬의 시간 복잡도는 O(N^2)이며, 선택 정렬과 마찬가지로 반복문이 두 번 중첩되어 사용된다.
  • 삽입정렬은 현재 리스트의 데이터가 거의 정렬되어 있는 상태라면 매우 빠르게 동작한다.(최선의 경우 O(N)의 시간복잡도를 가짐)

 

예제) 삽입정렬을 소스 코드

 

public class Main2775 {

	public static void main(String[] args) {
		
		// 삽입 정렬
		int n = 10;
		int[] arr = {7,5,9,0,3,1,6,4,2,8};
		
		for(int i=1; i<n; i++) { // 인덱스 i부터 1까지 감소하며 반복하는 문법
			for(int j=i; j>0; j--) { 
				if(arr[j]<arr[j-1]) {  // 인덱스의 앞과비교해서 앞이 크면 바꾸기
					int tmp = arr[j];
					arr[j] = arr[j-1];
					arr[j-1] = tmp;
				}
				else break; // 자기보다 작은 데이터를 만나면 그 위치에서 멈춤
			}
		}
		for(int i=0; i<arr.length;i++)
			System.out.print(arr[i] + " ");
	}
}

 

1) 삽입정렬을 구현하는 mySort()메서드 생성

 

static void mySort(int a[],int n) {
	for(int i=1; i<n; i++) { // 1,2,3,4,5,6,7,8,9
		int temp = a[i];  // 1번째 인덱스 (0번째 인덱스와 비교해야 함)
		
		int j;
		for(j=i; j>0 && a[j-1] > temp; j--) {  // 검색하려는 수가 클 때만 비교
				a[j] =  a[j-1];
		}
		a[j] = temp;
	}
}

 

2) 전체 코드

 

package al03_sort;

import java.util.Arrays;
import java.util.Random;

public class ArrayInsertionSort {
	// 삽입정렬(Insertion Sort) 
	static void mySort(int a[],int n) {
		for(int i=1; i<n; i++) { // 1,2,3,4,5,6,7,8,9
			int temp = a[i];  // 1번째 인덱스 (0번째 인덱스와 비교해야 함)
			
			int j;
			for(j=i; j>0 && a[j-1] > temp; j--) {  // 검색하려는 수가 클 때만 비교
					a[j] =  a[j-1];
			}
			a[j] = temp;
		}
	}
	
	public static void main(String[] args) {

		Random r = new Random();
		
		int a[] = new int[10];
		
		for(int i=0;i<a.length;i++) {
			a[i] = r.nextInt(100);  // 0~99
		}
		
		System.out.println("정렬전 -> " + Arrays.toString(a));

		mySort(a,a.length);
		System.out.println("정렬후 -> " + Arrays.toString(a));
	}
}

4. 퀵 정렬(Quick Sort)

  • 일반적으로 사용되고 있는 빠른 정렬 알고리즘
  • 기준 데이터를 설정하고, 그 기준보다 큰 데이터와 작은 데이터의 위치를 바꾸는 방법
  • 병합 정렬과 더불어 대부분의 프로그래밍 언어의 정렬 라이브러리의 근간이 되는 알고리즘
  • 가장 기본적인 퀵 정렬은 첫 번째 데이터로  기준 데이터(Pivot)로 설정한다. 
  • 퀵정렬은 대표적인 '분할 정복' 알고리즘으로 평균속도가 O(N*log N)이다. (하지만 항상 N*logN은 아니고 pivot값에 따라 최악 시간 복잡도는 O(N^2)가 될 수 있다.예)이미 정렬이 되어 있는 경우)

 

 


배열을 두 그룹으로 나누기

  • 왼쪽 끝 요소의 인덱스 pl / 피벗 X / 오른쪽 끝 요소의 인덱스 pr
  • 그룹을 나누려면 피벗 이하의 요소를 배열 왼쪽으로, 이상의 배열을 오른쪽으로 옮겨야 한다. 
a[pl] >= X 가 성립하는 요소를 찾을 때까지 pl을 오른쪽으로 스캔 X보다 큰 값을 찾으면 오른쪽으로 보내야 한다. 
a[pr] <= X가 가 성립하는 요소를 찾을 때까지 pr을 왼쪽으로 스캔 X보다 작은 값을 찾으면 왼쪽으로 보내야 한다.
  • pl이 위치한 지점은 피벗값 이상의 요소가 있는 지점이고, pr이 위치한 지점은 피벗 값 이하의 요소가 있는 지점이다.
  • 여기서 왼쪽(pl)과 오른쪽(pr)이 가리키는 인덱스 a[pl] <-> a[pr]의 값을 교환한다.
  • 그러면 피벗 이하의 값은 왼쪽으로 이동하고, 피벗 이상의 값은 오른쪽으로 이동한다.

 

-> 3이 7이 있는 인덱스[1]으로 가고, 7이 3이 있는 인덱스[6]으로 간다.

 

 

-> 계속해서 스캔하면 결국 두 그룹으로 나뉘어 지게 된다. 

 

예제)  퀵 정렬의 소스 코드

 

package al03_sort;

import java.util.Arrays;

public class QuickSort {

	// 배열요소 a[idx1]과 a[idx2]의 값을 바꾼다. 
	public static void swap(int a[], int idx1, int idx2) { 
		int tmp = a[idx1];  // 무엇을 먼저 하든 순서는 상관없음  
		a[idx1] = a[idx2]; 
		a[idx2] = tmp;
	}
	//                     		배열, 시작 인덱스, 끝인덱스 
	public static void myQuick(int[] a, int left, int right) {

		int pl = left;                     // 왼쪽부터 검색할 index위치
		int pr = right;                    // 오른쪽부터 검색할 index위치 (인덱스 중의 제일 큰 값)
		int pivot = a[(left + right) / 2]; // 피벗위치의 값을 구하기(기준) ((0+2)/7))

		// p1과 pr이 만날때까지, 배열 나누기 
		do {
			while (a[pl] < pivot)
				pl++; 
			while (a[pr] > pivot)
				pr--;
			if (pl <= pr)   
				swap(a, pl++, pr--);  
		} while (pl <= pr);
        
                // 정렬 후 왼쪽을 재정렬할 재귀호출
                if(left<pr) 
            	        myQuick(a,left,pr);
		// 정렬 후 오른쪽을 재정렬할 재귀호출
		if(pl<right) 
			myQuick(a,pl,right);
		
	}

 

더보기

while (a[pl] < pivot) pl++; 

- 피벗기준 왼쪽에서 오른쪽으로 피벗의 값보다 큰 데이터가 있는 index 찾기
- pl 인덱스에 들어있는 데이터가 피벗보다 커지면 pl++ 이 실행되지 않고 false가 되어, while문을 빠져나온다. 

 

while (a[pr] > pivot) pr--; 

- 피벗기준 오른쪽에서 왼쪽으로 이동하며 피벗의 값보다 작은 데이터가 있는 index 찾기
- pr 인덱스에 들어있는 데이터가 피벗보다 작으면 pr-- 이 실행되지 않고 false가 되어, while문을 빠져나온다.

 

if (pl <= pr)  swap(a, pl++, pr--);  (재귀적함수이용)

- pl의 위치값과, pr위치의 값을 교환한다. 현재 pl의 값은 pivot보다 크고, pr의 값은 pivot보다 작기 때문에 교환을 하게 되면 pivot보다 큰 값은 오른쪽으로, pivot보다 작은 값은 왼쪽으로 이동한다.

- 교환한 다음에 다시 돌아와서 p1++, pr--을 해준다. 다음 정렬을 하기 위해서

 

while (pl <= pr);

- pl이 계속해서 ++되고, pr이 계속해서 --가 되면서 pl이 pr보다 커지면 while문의 조건이 false가 되면서 종료된다.

 

if(left<pr) myQuick(a,left,pr);
- 요소가 두 개 이상 있을 때 성립함(왼쪽에 있는 조건이 하나만 있다면 실행되지 않음)

- 정렬 후  pivot을 기준으로 왼쪽(pivot보다 작은 값들)을 재정렬할 재귀호출

- 왼쪽 정렬이어서 left가 기준이 되어 새로운 오른쪽 기준이 되는 pr과 둘의 pivot이 새로 생기면서 정렬이 됨

 

if(pl<right) myQuick(a,pl,right);

- 요소가 두 개 이상 있을 때 성립함(오른쪽에 있는 조건이 하나만 있다면 실행되지 않음)

- 정렬 후 pivot을 기준으로 오른쪽(pivot보다 큰 값들)을 재정렬할 재귀호출

- 오른쪽 정렬이어서 right가 기준이 되어 새로운 왼쪽기준이 되는 pl과 둘의 pivot이 새로 생기면서 정렬이 됨

 

예제) 퀵정렬을 내림차순으로 하는 프로그램

 

package al03_sort;

import java.util.Arrays;
import java.util.Scanner;

public class QuickSort2 {

		public static void swap(int a[], int idx1, int idx2) {
			int tmp = a[idx1];  // 무엇을 먼저 하든 순서는 상관없음  
			a[idx1] = a[idx2]; 
			a[idx2] = tmp;
		}
		//                                시작 인덱스 + 끝인덱스 / 2 => 피벗
		public static void myQuick(int[] a, int left, int right) {

			int pl = left;
			int pr = right;
			int pivot = a[(left + right) / 2]; 

			do {
				while (a[pl] > pivot)
					pl++; 
				while (a[pr] < pivot)
					pr--; 
				if (pl <= pr)
					swap(a, pl++, pr--); 
			} while (pl <= pr);

			System.out.println("정렬 후: " + Arrays.toString(a));
		

			if(left<pr)
				myQuick(a,left,pr);
			if(pl<right) 
				myQuick(a,pl,right);
	
		}
		
		public static void main(String[] args) {
			
			Scanner sc = new Scanner(System.in);
			System.out.print("배열 수 입력: ");
			int n = sc.nextInt();
			int[] arr = new int[n];
			
			for(int i=0; i<arr.length; i++) {
				System.out.print("배열의 값을 입력하세요: ");
				arr[i] = sc.nextInt();
			}
			
			System.out.println("정렬 전 배열의 값: " + Arrays.toString(arr));
		
			myQuick(arr,0,arr.length-1);

		}
	}

 

더보기

배열 수 입력: 6
배열의 값을 입력하세요: 79
배열의 값을 입력하세요: 85
배열의 값을 입력하세요: 97
배열의 값을 입력하세요: 88
배열의 값을 입력하세요: 56
배열의 값을 입력하세요: 68
정렬 전 배열의 값: [79, 85, 97, 88, 56, 68]
정렬 후: [97, 85, 79, 88, 56, 68]
정렬 후: [97, 88, 79, 85, 56, 68]
정렬 후: [97, 88, 8579, 56, 68]
정렬 후: [97, 88, 85, 79, 68, 56]
정렬 후: [97, 88, 85, 79, 68, 56]

 

내림차순으로 하기 위해서는 

while (a[pl] > pivot) pl++; 
while (a[pr] < pivot) pr--; 

-> 화살표 방향만 바꾸면 된다. 

 


5. 정렬 알고리즘 비교하기

  • 대부분의 프로그래밍 언어에서 지원하는 표준정렬 라이브러리는 최악의 경우에도 O(NlogN)을 보장하도록 설계되어 있다. 

 

 

<문제> 두 배열의 원소 교체>

-  두 개의 배열 A와 B가 있다. 두 배열은 N개의 원소로 구성되어 있으며, 배열의 원소는 모두 자연수이다.

- 최대 K번의 바꿔치기 연산을 수행할 수 있는데, 바꿔치기 연산이란 배열 A에 있는 원소 하나와 배열 B에 있는 원소 하나를 골라서 두 원소를 서로 바꾸는 것을 말한다.

- 최종 목표는 배열 A의 모든 원소의 합이 최대가 되도록 하는 것이며, N, K, 그리고 배열 A와 B의 정보가 주어졌을 때, 최대 K번의 바꿔치기 연산을 수행하여 만들 수 있는 배열 A의 모든 원소의 합의 최대값을 출력하는 프로그램 작성

 

 

 

더보기

핵심 아이디어: 매번  A에서 가장 작은 원소를 골라서, B에서 가장 큰 원소와 교체 

1) A를 오름차순 정렬, 2) B를 내림차순 정렬

2) 두 배열의 원소를 첫 번째 인덱스부터 차례로 확인하면서 A의 원소가 B의 원소보다 작을 때만 교체 수행 

- 이 문제에서 두 배열의 원소가 최대 100,000개까지 입력될 수 있으므로, 최악의 경우 O(NlogN)을 보장하는 정렬 알고리즘 이용해야 한다. 

 

import java.io.*;
import java.util.*;

public class Main2775 {

	public static void swap(Integer[]a, Integer[]b, int i) {
		int tmp = a[i]; // 두 원소를 교체
		a[i] = b[i];
		b[i] = tmp;
	}
	
	public static void main(String[] args) throws Exception {
		
		BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
		
		// N과 K를 입력받기
		StringTokenizer st = new StringTokenizer(br.readLine());
		int n = Integer.parseInt(st.nextToken());
		int k = Integer.parseInt(st.nextToken());
		
		// 배열 A의 모든 원소를 입력받기
		Integer[] a = new Integer[n];
		for(int i=0;i<n;i++) {
			System.out.print("배열 A의 원소: ");
			a[i] = Integer.parseInt(br.readLine());
		}
		
		// 배열 B의 모든 원소를 입력받기
		Integer[] b = new Integer[n];
		for(int i=0;i<n;i++) {
			System.out.print("배열 B의 원소: ");
			b[i] = Integer.parseInt(br.readLine());
		}
		
		// 배열 A는 오름차순 정렬, 배열 B는 내림차순 정렬
		Arrays.sort(a);
		Arrays.sort(b, Collections.reverseOrder());
		
		// 첫 번째 인덱스부터 확인하며, 두 배열의 원소를 최대 K번 비교
		for(int i=0; i<k; i++) {
			if(a[i]<b[i]) {     // A의 원소가 B의 원소보다 작은 경우
				swap(a,b,i);
			}else break;        // A의 원소가 B의 원소보다 크거나 같을 때 반복무 탈출
		}
		
		// 배열 A의 모든 원소의 합을 출력
		for(int i=0; i<n; i++) {
			System.out.print(a[i] + " ");
		}
	}
}