정렬

송현준·2022년 11월 24일
0

기준에 따라 데이터를 정렬

정렬이란

  • 데이터를 특정한 기준에 따라서 순서대로 나열하는 것
  • 다양한 정렬 알고리즘 중 많이 사용하는 선택 정렬, 삽입 정렬, 퀵 정렬, 계수 정렬만 언급

1. 선택 정렬

  • 가장 작은 것을 선택한다는 의미

  • 데이터가 무작위로 여러 개 있을 때, 이 중에서 가장 작은 데이터를 선택해 맨 앞에 있는 데이터와 바꾸고, 그다음 작은 데이터를 선택해 앞에서 두 번째 데이터와 바꾸는 과정을 반복

  • 가장 작은 데이터를 앞으로 보내는 과정을 N - 1번 반복하면 정렬이 완료

  • 선택 정렬 소스 코드

    array = [7, 5, 9, 0, 3, 1, 6, 2, 4, 8]
    
    for i in range(len(array)) :
       min_index = i # 가장 작은 원소의 인덱스
       for j in range(i + 1, len(array)) :
               if array[min_index] > array[j] :
                   min_index = j
       array[i], array[min_index] = array[min_index], array[i] # 스왚

    [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]

선택 정렬의 시간 복잡도

  • 선택 정렬은 N1N - 1번 만큼 가장 작은 수를 찾아서 맨 앞으로 보내야 한다.
    또한 매번 가장 작은 수를 찾기 위해서 비교 연산이 필요하다.
    구현 방식에 따라 오차는 있을 수 있지만 연산 횟수는 N+(N1)+(N2)+...+2N + (N - 1) + (N - 2) + ... + 2로 표현
    따라서 근사치로 N(N+1)/2N * (N + 1) / 2번의 연산을 수행한다고 가정
    이는 (N2+N)/2(N^2 + N) / 2로 표현 가능
  • 빅오 표기법으로는 O(N2)O(N^2)

2. 삽입 정렬

  • 특정한 데이터를 적절한 위치에 '삽입'한다는 의미
    더불어 삽입 정렬은 특정한 데이터가 적절한 위치에 들어가기 이전에, 그 앞까지의 데이터는 이미 정렬되어 있다고 가정
  • 데이터를 하나씩 확인하며, 각 데이터를 적절한 위치에 삽입
  • 삽입 정렬은 두 번째 데이터부터 시작한다. 왜냐하면, 첫 번째 데이터는 그 자체로 정렬되어 있다고 판단하기 때문
  • 삽입 정렬은 현재 리스트의 데이터가 거의 정렬되어 있는 상태라면 매우 빠르게 동작한다.
  • 삽입 정렬 소스 코드
    array = [7, 5, 9, 0, 3, 1, 6, 2, 4, 8]
    
    for i in range(1, len(array)) :
    	for j in range(i, 0 -1) : # 인덱스 i부터 1까지 감소하며 반복하는 문법
       		if array[j] < array[j - 1] : # 한 칸씩 왼쪽으로 이동
               	array[j], array[j - 1] = array[j - 1], array[j]
            else : # 자기보다 작은 데이터를 만나면 그 위치에서 멈춤
            	break
    print(array)

    [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]

삽입 정렬의 시간 복잡도

  • O(N2)O(N^2)
  • 최선의 경우 O(N)O(N)
  • 보통은 삽입 정렬이 비효율적이나 정렬이 거의 되어 있는 상황에서는 퀵 정렬 알고리즘보다 더 강력

3. 퀵 정렬

  • 퀵 정렬은 지금까지 배운 정렬 알고리즘 중에 가장 많이 사용되는 알고리즘

  • 기준 데이터를 설정하고 그 기준보다 큰 데이터와 작은 데이터의 위치를 바꾼다.

  • 퀵 정렬에서는 피벗(Pivot)이 사용된다. 큰 숫자와 작은 숫자를 교환할 때, 교환하기 위한 '기준'을 바로 피벗이라고 표현

  • 퀵 정렬 소스코드

    array = [5, 7, 9, 0, 3, 1, 6, 2, 4, 8]
    
    def quick_sort(array, start, end) :
    	if start >= end : # 원소가 1개인 경우 종료
       		return
        pivot = start # 피벗은 첫 번째 원소
        left = start + 1
        right = end
        while left <= right :
        	# 피벗보다 큰 데이터를 찾을 때까지 반복
           while left <= end and array[left] <= array[pivot] :
           		left += 1
           # 피벗보다 작은 데이터를 찾을 때까지 반복
           while right > start and array[right] >= array[pivot] :
           		right -= 1
           if left > right : # 엇갈렸다면 작은 데이터와 피벗을 교체
           		array[right], array[pivot] = array[pivot], array[right]
           else : # 엇갈리지 않았다면 작은 데이터와 큰 데이터를 교체
           		array[left], array[right] = array[right], array[left]
       # 분할 이후 왼쪽 부분과 오른쪽 부분에서 각각 정렬 수행
       quick_sort(array, start, right -1)
       quick_sort(array, right + 1, end)
       
    quick_sort(array, 0, len(array) -1)
    print(array)

    [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]

  • 파이썬의 장점을 살린 퀵 정렬 소스코드

    array = [5, 7, 9, 0, 3, 1, 6, 2, 4, 8]
    
    def quick_sort(array) :
    	# 리스트가 하나 이하의 원소만을 담고 있다면 종료
       if len(array) <= 1 :
       		return array
       
       pivot = array[0] # 피벗은 첫 번째 원소
       tail = array[1:] # 피벗을 제외한 리스트
       
       left_side = [x for x in tail if x <= pivot] # 분할된 왼쪽 부분
       right_side = [x for x in tail if x > pivot] # 분할된 오른쪽 부분
       
       # 분할 이후 왼쪽 부분과 오른쪽 부분에서 각각 정렬을 수행하고, 전체 리스트를 반환
       return quick_sort(left_side) + [pivot] + quick_sort(right_side)
       
    print(quick_sort(array))

퀵 정렬의 시간 복잡도

  • 평균 시간 복잡도는 O(NlogN)O(NlogN)
  • 최악의 경우 O(N2)O(N^2)
  • 데이터가 무작위로 입력되는 경우 퀵 정렬은 빠르게 동작할 확률이 높다. 하지만 리스트의 가장 왼쪽 데이터를 피벗으로 삼을 때, '이미 데이터가 정렬되어 있는 경우'에는 매우 느리게 동작한다.

4. 계수 정렬

  • 특정한 조건이 부합할 때만 사용할 수 있지만 매우 빠른 정렬 알고리즘
  • 데이터의 크기 범위가 제한되어 정수 형태로 표현할 수 있을 때만 사용할 수 있다.
  • 일반적으로 가장 큰 데이터와 가장 작은 데이터의 차이가 1,000,000을 넘지 않을 때 효과적으로 사용
    이러한 특징을 가지는 이유는, 계수정렬을 이용할 때는 '모든 범위를 담을 수 있는 크기의 리스트를 선언'해야 하기 때문이다.
  1. 먼저 가장 큰 데이터와 가장 작은 데이터의 범위가 모두 담길 수 있도록 하나의 리스트를 생성
  2. 처음에는 리스트의 모든 데이터가 0이 되도록 초기화
  3. 그 다음 데이터를 하나씩 확인하며 데이터의 값과 동일한 인덱스의 데이터를 1씩 증가시키면 계수 정렬이 완료된다.
  4. 결과적으로 각 데이터가 몇 번 등장 했는지 그 횟수가 기록
  5. 리스트의 첫 번째 데이터부터 하나씩 그 값만큼 인덱스를 출력
  • 계수 정렬 소스코드

    
    # 모든 원소의 값이 0보다 크거나 같다고 가정
    array = [7, 5, 9, 0, 3, 1, 6, 2, 9, 1, 4, 8, 0, 5, 2]
    
    # 모든 범위를 포함하는 리스트 선언(모든 값은 0으로 초기화)
    count = [0] * (max(array) + 1)
    
    for i in range(len(array)) :
    	count[array[i]] += 1 # 각 데이터에 해당하는 인덱스의 값 증가
       
    for i in range(len(count)) : # 리스트에 기록된 정렬 정보 확인
    	 for j in range(count[i]) :
        	print(i, end=' ') # 띄어쓰기를 구분으로 등장한 횟수만큼 인덱스 출력

    0 0 1 1 2 2 3 4 5 5 6 7 8 9 9

계수 정렬의 시간 복잡도

  • O(N+K)O(N + K)
  • 계수 정렬은 앞에서부터 데이터를 하나씩 확인하면서 리스트에서 적절한 인덱스의 값을 11씩 증가시킬 뿐만 아니라, 추후에 리스트의 각 인덱스에 해당하는 값들을 확인할 때 데이터 중 최댓값의 크기만큼 반복을 수행해야 하기 때문이다.
    따라서 데이터의 범위만 한정되어 있다면 효과적으로 사용할 수 있으며 항상 빠르게 동작한다.

계수 정렬의 공간 복잡도

  • 계수 정렬은 때에 따라서 심각한 비효율성을 초래한다. 예를 들어 데이터가 0과 999,999 단 2개만 존재한다고 가정해 보면 이럴 때에도 리스트의 크기가 100만 개가 되도록 선언해야 한다.
    따라서 항상 사용할 수 있는 정렬 알고리즘은 아니며, 동일한 값을 가지는 데이터가 여러 개 등장할 때 적합하다.
  • 다시 말해 계수 정렬은 데이터의 크기가 한정되어 있고, 데이터의 크기가 많이 중복되어 있을수록 유리하며 항상 사용할 수는 없다.
  • 공간 복잡도는 O(N+K)O(N + K)

5. 파이썬 정렬 라이브러리

  • 퀵 정렬과 동작 방식이 비슷한 병합 정렬을 기반으로 만들어졌다.
    정확히는 병합 정렬과 삽입 정렬의 아이디어를 더한 하이브리드 방식의 정렬 알고리즘을 사용

  • 병합 정렬은 일반적으로 퀵 정렬보다 느리지만 최악의 경우에도 시간 복잡도 O(NlogN)O(NlogN)을 보장한다.

  • 리스트, 딕셔너리 자료형 등을 입력받아서 정렬된 결과를 출력한다.
    집합 자료형이나 딕셔너리 자료형을 입력 받아도 반환되는 결과는 리스트 자료형이다.

  • sorted 소스코드

    
    array = [7, 5, 9, 0, 3, 1, 6, 2, 4, 8]
    
    result = sorted(array)
    print(result)

    [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]

  • sort소스코드

    
    # 별도의 정렬된 리스트가 반환되지 않고 내부 원소가 바로 정렬
    array = [7, 5, 9, 0, 3, 1, 6, 2, 4, 8]
    
    array.sort()
    print(array)

    [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]

  • 정렬 라이브러리에서 key를 활용한 소스코드

    
    # key 매개변수를 입력으로 받을 수 있다.
    # key 값으로는 하나의 함수가 들어가야 하며 이는 정렬 기준이 된다.
    # 각 데이터의 두 번째 원소를 기준으로 설정
    array = [('바나나', 2), ('사과', 5), ('당근', 3)]
    
    def setting(data) :
    	return data[1]
       
    result = sorted(array, key=setting)
    print(result)
    

    [('바나나', 2), ('당근', 3), ('사과', 5)]

코딩 테스트에서 정렬 알고리즘이 사용되는 경우

  1. 정렬 라이브러리로 풀 수 있는 문제
    단순히 정렬 기법을 알고 있는지 물어보는 문제

  2. 정렬 알고리즘의 원리에 대해서 물어보는 문제
    선택, 삽입, 퀵 정렬 등의 원리를 알고 있어야 풀 수 있는 문제

  3. 더 빠른 정렬이 필요한 문제
    퀵 정렬 기반의 정렬 기법으로는 풀 수 없으며 계수 정렬 등의 다른 정렬 알고리즘을 이용하거나 문제에서 기존에 알려진 알고리즘의 구조적인 개선을 거쳐야 풀 수 있는 문제

6. 실전 문제

1. 위에서 아래로
2. 성적이 낮은 순서로 학생 출력하기(key를 이용한 정렬)
3. 두 배열의 원소 교체

출처 이것이 코딩테스트다 with 파이썬

profile
노력중

0개의 댓글