본문 바로가기
2️⃣ Study/⚙ 알고리즘

[자료구조] 정렬

by isdawell 2024. 2. 25.
728x90

 

1.  정렬 라이브러리로 풀 수 있는 문제 : 단순히 정렬 기법을 알고있는지 물어보는 문제 
2.  정렬 알고리즘의 원리에 대해서 물어보는 문제 : 선택 정렬, 삽입 정렬, 퀵 정렬 등의 원리를 알고 있어야 함
3. 더 빠른 정렬이 필요한 문제 : 계수 정렬 등 다른 알고리즘을 이용하거나 구조적인 개선을 거쳐야 풀 수 있는 문제 

 

 

 

 

1️⃣   정렬  


 

•   데이터를 특정한 기준에 따라서 순서대로 나열하는 것 

   데이터를 정렬하면 이진탐색이 가능해진다. 

   선택정렬, 삽입 정렬, 퀵정렬, 계수정렬 

   알고리즘 효율성과 관련되어 있음 

 

※  책에서는 모두 오름차순 정렬을 기준으로 설명한다. 내림차순은 오름차순 결과에서 인덱스 Reverse 해서 출력하면 된다. 

 

 

 

 

2️⃣   선택정렬  


 

   원시적인 방법으로 매번 가장 작은 것을 선택하는 방식 

   인덱스 접근 , 모든 pair 비교 

   시간 복잡도 : O(N^2)  , 다소 비효율적이다. 

 

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]

 

 

↪  차례대로 인덱스를 순회하면서, 기준 인덱스와 나머지 인덱스에 해당하는 실제 값을 비교하면서, 최소값에 해당하는 인덱스를 할당한다. 가장 작은 값을 최종적으로 나서 값을 실제로 스와프 한다. 

 

 

 

 

 

 

3️⃣   삽입정렬 


 

   삽입정렬은 필요할 때만 위치를 바꾸기 때문에 데이터가 거의 정렬되어 있을 때 훨씬 효율적이다. 

   특정한 데이터가 적절한 위치에 들어가기 전에 그 앞까지의 데이터는 이미 정렬되어 있다고 가정한다. 정렬되어 있는 데이터 리스트에서 적절한 위치를 찾은 뒤 그 위치에 삽입된다는 것이 특징이다. 정렬이 이루어진 원소는 항상 오름차순을 유지한다. 

   삽입정렬은 두번째 데이터부터 시작한다. 첫번째 데이터는 그 자체로 정렬되어 있다고 판단하기 때문이다. 

   시간 복잡도 : O(N^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

 

↪  range(i,0,-1) : i 부터 0까지 1씩 감소. 가령 range(5, 0, -1)은 5, 4, 3, 2, 1과 같은 숫자를 생성

 

 

 

 

 

4️⃣  퀵 정렬 


 

   기준을 설정한 다음 큰 수와 작은 수를 교환한 후 리스트를 반으로 나누는 방식으로 동작 

   기준을 "pivot" 피벗이라 한다. 피벗을 어떻게 설정하느냐에 따라 퀵 정렬 방식이 여러가지인데, 책에서는 호어 분할 방식을 다룬다. 

 

(1)  리스트에서 첫번째 데이터를 피벗으로 정한다.  (호어 분할 방식) 

(2)  피벗을 설정한 뒤에는 왼쪽에서부터 피벗보다 큰 데이터를 찾고 오른쪽에서부터 피벗보다 작은 데이터를 찾는다. 그 다음 큰 데이터와 작은 데이터의 위치를 서로 교환한다. left, right

(3)  찾는 값의 위치가 엇갈린 경우 (왼쪽에 작은 데이터, 오른쪽에 큰 데이터) 에는 작은 데이터와 피벗의 위치를 서로 변경한다. 

(4)  분할이 완료되면, 피벗이 이동한 상태에서 왼쪽에는 모두 피벗보다 작고, 오른쪽에는 피벗보다 큰 데이터가 위치하게 된다. 

(5)  이후 왼쪽, 오른쪽 각각에 대해 피벗을 설정해 동일한 방식으로 정렬을 수행한다. 재귀함수 원리와 같다. 퀵정렬이 끝나는 조건은 현재 리스트의 데이터 개수가 1개인 경우다. 

 

 

   원리 이해 버전

 

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)

 

 

   간단 버전 

 

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]  # pivot 보다 작은 값들 
  print(left_side)

  # 분할된 오른쪽 부분 
  right_side = [x for x in tail if x > pivot] # pivot 보다 큰 값들 
  print(right_side)

  # 분할 이후 왼쪽 부분과 오른쪽 부분에서 각자 정렬을 수행, 전체 리스트 반환 
  return quick_sort(left_side) + [pivot] + quick_sort(right_side)  # 🟡
  
  
print(quick_sort(array))

 

 

   시간 복잡도 : O(NlogN) 

↪ 그러나 최악의 경우는 O(N^2) 이다. 데이터가 무작위로 입력되는 경우 퀵정렬은 빠르게 동작할 확률이 높으나, 리스트의 가장 왼쪽 데이터를 피벗으로 삼게되면 '이미 데이터가 정렬되어 있는 경우' 에는 매우 느리게 동작한다. 

 

 

 

 

5️⃣  계수 정렬 


 

   특정 조건이 부합할 때만 사용할 수 있지만 매우 빠른 정렬 알고리즘

   데이터 개수가 N, 최대값이 K 일 때, 계수정렬은 최악의 경우에도 수행시간 O(N+K) 를 보장한다. 

   계수정렬은 '데이터의 크기 범위가 제한되어 정수 형태로 표현할 수 있을 때' 만 사용할 수 있다. 가장 큰 데이터와 가장 작은 데이터 차이가 1,000,000 을 넘지 않을 때 효과적으로 사용할 수 있다. 

   계수정렬은 비교 기반의 정렬 알고리즘이 아니다. 

 

(1) 가장 큰 데이터와 가장 작은 데이터의 범위가 모두 담길 수 있도록 하나의 리스트를 생성한다. 리스트의 인덱스가 값의 범위를 모두 포함할 수 있도록 생성한다. 가령 가장 작은 값이 0이고 큰 값이 9이면 0부터 9까지 정수 인덱스 차례로 리스트를 생성한다. 

(2) 데이터를 하나씩 확인하며 데이터의 값과 동일한 인덱스의 데이터를 1씩 증가시키면 정렬이 완료된다. 

(3) 결과적으로 리스트에는 각 데이터가 몇 번 등장했는지 그 횟수가 기록된다. 

(4) 정렬된 데이터를 확인하고 싶다면 리스트의 첫 번째 데이터부터 하나씩 그 값 만큼 인덱스를 출력하면 된다. 

 

 

# 모든 원소의 값이 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 과 999,999 단 2개만 존재한다면 이럴 때에도 리스트 크기가 100만개가 되도록 선언해야 한다. 

   동일한 값을 가지는 데이턱 여러 개 등장할 때 적합하다. 

 

 

 

 

6️⃣  파이썬 정렬 알고리즘 


 

   sorted()  : 퀵 정렬과 동작 방식이 비슷한 병합 정렬을 기반으로 만들어졌다. 최악의 경우에도 시간 복잡도 O(NlogN) 을 보장한다. 

 

sorted(array)

 

 

   sort()  : 리스트를 바로 정렬하는 메소드

 

array.sort()

 

 

 

   key 매개변수 : key 값으로는 하나의 함수가 들어가야 하며 이는 정렬 기준이 된다. 

 

 

 

 

 

 

연습문제 + 코드 


 

   정수 리스트 입력 받기 

 

n = int(input()) # n 입력 받기 

# N 개의 정수를 입력받아 리스트에 저장 
array = [] 

for i in range(n) : 
  array.append(int(input()))

 

 

   (key, value) 형식으로 리스트 입력 받기 

 

n = int(input())  # 2 

array = []

for i in range(n) : 
  input_data = input().split() 
  array.append((input_data[0], int(input_data[1])))

 

 

   숫자 리스트 만들기 

 

a = list(map(int, input().split())) 
b = list(map(int, input().split())) # 숫자 리스트 만드는 방법

 

 

 

728x90

댓글