밍경송의 E.B

선택 정렬(Selection sort)와 삽입 정렬(Insertion sort) 본문

CSE/자료구조&알고리즘

선택 정렬(Selection sort)와 삽입 정렬(Insertion sort)

m_gyxxmi 2023. 4. 22. 21:09

오늘은 길었던 정통공과 이별하고,, 알고리즘의 아주 기초적이지만 나는 모르는 .. 개념들에 대해서 정리해보겠습니다. 아자

함 넣어봄요

이전에 버블정렬(bubble sort) 알고리즘에 대해서 간략하게 정리했었는데요. 그때 버블 정렬의 시간 복잡도를 O(N^2)로 나타낼 수 있다고 배웠습니다. 오늘은 선택정렬과 삽입정렬에 대해서 다뤄볼텐데, 버블 정렬까지 세 알고리즘이 어떻게 다른지! 시간 복잡도는 어떤지! 이해해봅시다.

 

   선택정렬(Selection sort)    

선택 정렬의 키워드는 최솟값입니다. 간단하게 말하자면 배열의 왼쪽부터 시작해서 맨 마지막 셀까지 쭉 돌면서, 최솟값을 저장하고 순회가 끝나면 이를 맨 왼쪽 인덱스 값(인덱스 0이 아닐 수 있음)과 교환해주고,,그 과정을 정렬될 때까지 계속 반복하는 그런 알고리즘입니다.

이전에 배웠던 버블 정렬은 두 개의 포인터를 이용해 값을 비교하고 순서가 바뀌어 있으면 둘을 교환해줘서 패스스루가 끝날 때마다 그 당시의 맨 오른쪽 셀에 정렬된 값이 들어갔었는데요. 그러니까 최댓값이 먼저 정렬되는 것이죠.

그런데, 선택 정렬의 경우, 패스스루마다 최솟값을 저장하고 그것을 그 당시의 맨 왼쪽 셀에 저장하기 때문에 최솟값이 먼저 정렬된다는 중요한 차이가 있습니다. 

여러 정렬 알고리즘을 배우다보면 뭐가 뭔지 헷갈릴 수 있으니 처음 배울 때 잘 익혀두면 좋을 것 같아요..

 

그럼 선택 정렬의 과정을 한 번 들여다봅시다.

 

1) 배열의 각 셀을 왼쪽부터 오른쪽 방향으로 확인하면서 어떤 값이 최솟값인지 결정한다. 한  셀씩 이동하면서 현재까지 가장 작은 값을 기록한다(변수에 인덱스를 저장함). 변수에 들어 있는 값보다 작은 값이 들어 있는 셀을 만나면 변수가 새 인덱스를 가리키도록 값을 대체한다.

2) 이제 최솟값이 어느 인덱스에 들어 있는지 알았으므로 그 인덱스의 값과 패스스루를 처음 시작했을 때의 값을 교환한다. 첫 패스스루를 시작했을 때 인덱스=0, 그 다음 패스스루에서 인덱스=1, ... 

3) 매 패스스루는 1), 2) 단계로 이루어지고, 배열 끝에서 시작하는 패스스루에 도달할 때까지 패스스루를 반복한다. 마지막 패스스루에서는 배열이 완벽하게 정렬!

 

한 패스스루만 연습해봅시다!

 

[ 2  ,  6  ,  1  ,  3 ]

            (현재 최솟값:2)       

[ 2  ,  6  ,  1  ,  3 ]

                        (현재 최솟값:2)     

[ 2  ,  6  ,  1  ,  3 ]

                                      (현재 최솟값:1)   

[ 2  ,  6  ,  1  ,  3 ]

                                                     (현재 최솟값:1)     

[ 1  ,  6  ,  2  ,  3 ]

                       최솟값인 1을 맨 왼쪽 셀과 교환 

--> 첫번째 패스스루 끝

 


쉽쬬.. 이제 인덱스0(현재 1)에는 정렬된 값(최솟값)이 들어간 상태니까 다음 패스스루는 인덱스1(현재 6)부터 시작하면 됩니다. 그렇다면! 선택정렬을 한 번 코드로 구현해보겠습니다.

function selectionSort(array) {
	for(let i =0; i < array.length - 1 ; i++) { //각 패스스루를 나타내는 루프(0부터 마지막인덱스-1)
    											//마지막 인덱스는 안 봐도 됨
    	let lowestNumberIndex = i; //최솟값 넣어줄 변수 선언
        for (let j = i+1; j < array.length; j++) { //패스스루 안에서, 배열의 나머지값 확인
        	if (array[j] < array[lowestNumberIndex]) {//현재 최솟값보다 더 작은 값이 있다면 교환(인덱스넘버를)
        		lowestNumberIndex =j;
        	}
    	}
    
    	if(lowestNumberIndex != i) { //최솟값이 맨 왼쪽 값이 아니라면, 현재 최솟값과 맨 왼쪽값 교환
    		let temp = array[i];
    		array[i] = array[lowestNumberIndex];
    		array[lowestNumberIndex] = temp;
    	}
     }
     return array; //정렬된 배열 출력
}

 


            

선택정렬의 효율성

 선택정렬은 비교와 교환을 거칩니다. 여기서 교환은 패스스루당 최대 한 번(맨 왼쪽값과 최솟값의 불일치시) 일어나게 됩니다. 교환의 과정 때문에, 선택정렬은 버블정렬과 비교했을 때, 최악의 경우에도 2배 빠르게 정렬을 할 수 있는데요. 

이를 빅 오 표기법으로 나타내면 O(N^2 / 2)가 되겠죠? 

라고 생각했다면 땡! 선택정렬을 빅 오 표기법으로 나타내면 버블정렬과 동일하게 O(N^2)이라고 합니다. 이유가 뭘까요?

 

"빅 오 표기법은 상수를 무시한다"

 

 빅오의 본질에서 살펴봤듯, 빅 오 표기법은 데이터가 늘어날 때 알고리즘 단계 수가 장기적으로 어떤 궤적을 그리는지를 중요하게 생각합니다. 그러니까 다시 말하면, 빅  오 표기법은 단지 알고리즘의 카테고리를 분류해주는 것. 그 뿐이라는 뜻입니다. O(N)은 직선 성작을, O(N^2)은 지수 성장을 보여주는데 이 둘 완전 다른 카테고리에 속한다는 것을 알 수 있죠. 하지만, 10N이든 100N 빅오 표기법으로 나타내면 모두 O(N)이 되는 거죠. 둘은 O(N)이라는 같은 카테고리에 속하니까요.

뭐 어쨌든 최악의 경우, 선택 정렬이 버블 정렬보다 2배 빠른 알고리즘이라는 것은 알아둡시다 :)

 

 

 


 

 

자 그렇다면 삽입 정렬에 대해서 알아봅시다.

  삽입 정렬(Insertion Sort)  

 삽입정렬은 간단히 말하자면 특정 인덱스의 값을 빼서 공백으로 두고, 본인의 왼쪽 인덱스들을 돌면서 알맞은 자리를 찾아 삽입해주는 방법인데요. 일부로 배열 중 한 인덱스의 값을 다른 곳으로 옮겨 공백을 만들고, 이 값과 공백의 왼쪽 값들을 비교하여 왼쪽 값들 중 더 큰 값이 있으면 오른쪽으로 shift해주어 공백을 옮겨주고, 나중에 다른 곳으로 옮긴 값을 다시 공백에 넣어.. 배열을 정렬해나가는 알고리즘입니다. 

 

과정은 이러합니다.

 

1) 첫번째 패스스루에서 임시로 인덱스1(두번째 셀)의 값을 삭제하고 이 값을 임시 변수에 저장한다. 인덱스 1에 값이 없으므로 공백이 생긴다. 이후 각 패스스루마다 다음 인덱스의 값을 삭제한다(두번째 패스스루에서는 인덱스 2의 값 삭제).

 

2) 다음으로 공백 왼쪽에 있는 각 값을 가져와 임시 변수에 있는 값과 비교하는 시프트 단계를 시작한다. 공백 왼쪽에 있는 값이 임시 변수에 있는 값(삭제된 값)보다 크면 그 값을 오른쪽으로 시프트한다. 시프트-> 자연히 공백이 왼쪽으로 옮겨짐! 임시로 삭제한 값보다 작은 값을 만나거나 배열의 왼쪽 끝에 도달해야 시프트 단계가 끝난다.

 

3) 이제 임시로 제거한 값을 현재 공백에 삽입한다. 

 

4) 1) - 3)단계까지가 하나의 패스스루다. 배열의 마지막 인덱스에서 패스스루를 시작할 때까지 패스스루를 반복한다. 이때가 되면 배열은 완전히 정렬된다.

 

한 패스스루만 연습해봅시다.

 

[ 4 , 2 , 7 , 1 ,3 ]

                   ↑ 삭제(변수에 저장)

[ 4 , 2 , 7 , 1 ,3 ]

       ↑와 2(임시값) 비교

[   , 4 , 7 , 1 ,3  ]

                                                                  ↑  4가 2보다 크기 때문에 오른쪽(공백자리)로 shift

[   , 4 , 7 , 1 ,3  ]

                                                          ↑ 공백이 배열의 왼쪽 끝에 있으므로 비교/이동 불가

[ 2 , 4 , 7 , 1 ,3  ]

                2(임시값)을 공백에 삽입

 

   첫번째 패스스루 종료!

 

이제 두번째 패스스루에서는 다음 인덱스의 값(7) 삭제하여 공백을 만들고 시작하는데, 이때 공백의 왼쪽은 나? 정렬이 된 상태기 때문에!  비교해나가다가 임시값보다 더 작은 값을 만나면 더 왼쪽으로 갈 필요는 없습니다.


 

그렇다면 삽입 정렬(Insertion sort)를 코드로 구현해봅시다.

def insertion_sort(array):
	for index in range(1 , len(array)): #배열 순회 시작
		
        temp_value = array[index]; #삭제된 값을 변수에 저장
        position = index -1 #position이라는 변수는 삭제값의 왼쪽부터 시작
        
        while position > =0: #position값이 0보다 크거나 같은 경우동안 패스스루 진행
        	if array[position] > temp_value: #비교하는 값이 임시값보다 크다면
            	array[position+1] = array[position] #비교값을 오른쪽으로 시프트
                position = position -1 #position은 왼쪽으로 쭉쭉 가
            else: #position에 있는 값이 temp_value보다 같거나 작을때
            	break #패스스루 종료
                
        array[position + 1] = temp_value #임시값을 현 공백에 삽입
        
     return array

 

삽입정렬의 효율성

삽입정렬에 포함된 단계는 삭제, 비교, 시프트, 삽입 . 이렇게 총 4단계입니다. 중간 과정은 생략하고 결론부터 말하면, 삽입정렬은 최악의 경우 N^2의 비교와 시프트(합쳐서), N-1번의 삭제와 삽입(각각) 과정을 거칩니다. 그러니까 N^2 +2N -2 단계가 걸리는 알고리즘인 거죠. 그런데 우리는 위에서 빅 오 표기법이 상수를 무시한다는 것을 배웠습니다. 

그렇다면 삽입정렬의 효율성은 O(N^2 + 2N)으로 나타낼 수 있을까요?

땡! ... 이유는요. 우리가 빅오 표기법에 대해 알아야 할 한가지 정보가 더 있기 때문입니다.

 

"다양한 차수가 한데 섞여 있을 때 빅 오 표기법은 가장 높은 차수의 N만 고려한다."

 

이유는.. 데이터의 개수인 N이 증가할 수록 높은 차수가 차지하는 비중이 더 커지기 때문에 낮은 차수는 무시하기로 했기 때문입니다. 그러니까 결국 삽입정렬의 효율성을 빅 오 표기법으로 나타내면 앞에서 배운 버블, 선택 정렬과 동일하게 O(N^2)이 되는거죠.

 

그런데, 우리는 여기서 한 가지 추가로 짚고 넘어가야 할 부분이 있습니다. 거두절미하고, 삽입 정렬은 시나리오에 따라 성능이 크게 좌우되는 알고리즘이라는 것인데요. 

  최선의 시나리오 평균 시나리오 최악의 시나리오
선택정렬 N^2/2 N^2 /2 N^2 /2
삽입정렬 N N^2 /2 N^2

그러니까, 보통 빅 오 표기법은 최악의 시나리오일 때를 생각하기 때문에, 그 경우에는 선택정렬이 삽입정렬보다 2배나 빠른 알고리즘이라고 할 수 있습니다. 하지만, 대부분의 알고리즘은 평균적인 경우가 많다는 점을 생각하면.. 선택정렬과 삽입정렬의 효율성이 동일하다고도 할 수 있겠습니다. 헷

 

 

 

그럼 이상으로 선택정렬과 삽입정렬에 대한 포스팅을 마치겠습니다 땡큐~

 

 

이 글은 <누구나 자료구조와 알고리즘 개정 2판>, 제이 웬그로우 지음, 심지현 옮김 의 내용을 바탕으로 작성하였습니다.