파이썬에서는 이미 sort() 함수를 제공하고 있기 때문에, 사실 정렬을 직접 구현해야 하는 일은 많지 않습니다. 오히려 코딩 테스트에서는 정렬을 구현하는 방법보다도 정렬 알고리즘이 어떤 방식으로 정렬을 수행하는지를 이해하는 것이 더 중요할 수 있습니다. 따라서, 문제 풀이보다는 정렬 알고리즘의 핵심 개념과 구현 방법에 초점을 맞추어 설명하도록 하겠습니다.
특히, 이번 포스팅에서는 이중 for문을 이용해 정렬을 수행하는 O(N^2) 정렬 알고리즘에 대해 알아보기로 하겠습니다.
버블 정렬은 가장 큰 값을 맨 뒤로 보내는 과정을 반복하며 정렬을 수행한다. 버블 정렬 구현에 사용되는 변수는 아래와 같다. (배열의 Size를 N, 이중 for문의 외부 루프와 내부 루프에 사용되는 변수를 각각 i, j라고 하자.)
① i
② j
arr = [3, 2, 4, 5, 1]
N = len(arr)
for i in range(N-1):
for j in range(N-i-1):
if arr[j] > arr[j+1]:
arr[j], arr[j+1] = arr[j+1], arr[j]
위 코드에서 부등호 방향만 변경하면, 내림차순으로 정렬하는 것도 가능하다.
위 문제는 버블 정렬에서 Swap이 한번도 일어나지 않은 루프가 언제인지를 알아내는 문제이다. 당연히 위 코드를 적절히 파이썬 코드로 변경만 해주면, 쉽게 해결될 문제이기에 문제에서 시간 제약을 걸어두었다.
시간 제약은 2초인데 N의 범위는 500,000이므로, O(N^2)인 버블 정렬을 그대로 이용할 경우, 시간 초과가 발생할 것이다. 그러므로, Swap이 한번도 일어나지 않은 루프를 찾기 위한, 다른 방법을 모색해 보아야 할 것이다.
사실 버블 정렬의 내부 루프(2차 루프)에서 Swap이 한번도 일어나지 않았다는 것은, 이미 정렬이 완료된 상태임을 의미한다. 따라서, 위 문제의 출력 결과는 곧 버블 정렬이 몇번째 루프에서 완료되는지에 1을 더한 수치가 될 것이다. 여기서 1을 더하는 이유는, 정렬이 완료된 이후에 Swap이 한번도 일어나지 않았다는 것을 확인하기 위해 한번의 루프를 추가로 수행해야 하기 때문이다.
그렇다면 버블 정렬이 완료되기 위해 필요한 루프의 횟수는 어떻게 구할 수 있을까? 버블 정렬에는 한 가지 특징이 있는데, 이는 한 루프에서 각 원소가 왼쪽(앞쪽)으로 이동하는 경우는 최대 한번밖에 발생하지 않는다는 것이다. 즉, 한 원소가 한 루프 내에서 2번 이상 왼쪽으로 이동하는 일은 발생하지 않는다.
버블 정렬이 완료되려면, 더 이상 왼쪽으로 이동하는 원소가 없어야 한다. 그러므로, 각 원소가 왼쪽으로(앞으로) 이동한 횟수의 최대 값과, 정렬이 완료되기 위해 필요한 루프의 횟수가 같다. 위 그림에서도, 왼쪽으로 이동하는 횟수의 최대 값이 2이므로, 두번째 루프에서 정렬이 완료되는 것을 확인할 수 있다.
왼쪽으로 이동한 횟수는 정렬 전 Index에서 정렬 후 Index를 빼는 것으로 구할 수 있다. 이렇게 해서 구해진 값 중 최대 값을 찾고, 그 값에 1을 더하면 문제 풀이가 완료될 것이다.
import sys
input = sys.stdin.readline
N = int(input())
lst = []
for i in range(N):
lst.append([int(input()), i]) # 원소와 정렬 전 index를 모두 저장
lst.sort() # 배열의 첫번째 요소를 기준으로 정렬
Max = 0
for i in range(N): # i는 정렬 후 index에 해당
Max = max(Max, lst[i][1] - i) # 정렬 전 index - 정렬 후 index의 최대 값
print(Max + 1)
선택 정렬은 최소 값이 위치한 인덱스를 탐색한 후, 해당 인덱스의 원소와 맨 앞의 원소를 swap하는 방식으로 정렬을 수행한다. 선택 정렬 구현에 사용되는 변수는 아래와 같다.
① i
② j
③ min_idx
arr = [3, 2, 4, 5, 1]
N = len(arr)
for i in range(N-1):
min_idx = i
for j in range(i+1, N):
if arr[min_idx] > arr[j]:
min_idx = j
arr[min_idx], arr[i] = arr[i], arr[min_idx]
위 코드에서 min_idx를 max_idx로 변경하고 부등호 방향만 바꿔주면, 내림차순 정렬을 수행할 수 있다.
삽입 정렬은 먼저 배열을 정렬부와 비정렬부로 구분한다. 이 때, 0번 인덱스의 원소는 이미 정렬되어 있다고 가정한다. 비정렬부에 존재하는 원소를 순차적으로 정렬부로 삽입하며, 정렬이 유지될 수 있도록 적절한 위치에 배치하는 과정을 통해 정렬을 수행한다.
① i
② j
arr = [3, 2, 4, 5, 1]
N = len(arr)
for i in range(1, N):
for j in range(i, 0, -1):
if arr[j-1] > arr[j]:
arr[j-1], arr[j] = arr[j], arr[j-1]
else:
break
마찬가지로 부등호 방향만 바꾸면, 내림차순으로 정렬할 수 있다.
각 정렬이 어떤 방식으로 정렬을 수행하는지 헷갈릴 때가 많다. 그러므로, 3가지 정렬 방식에 대해서 확실히 구분하는 방법에 대해 알아보자.
① Bubble Sort
② Selection Sort
③ Insertion Sort