[Swift 알고리즘] 정렬 알고리즘

2022. 5. 23. 15:32Swift 코딩테스트/Swift 알고리즘

728x90

//공부 기록용 포스팅입니다. 틀린 부분이 있을 경우 댓글로 알려주시면 감사합니다! 😎

대부분 O()과 O(nlogn) 사이의 시간 복잡도

 

 

정렬 알고리즘 정의 시간복잡도
버블(bubble) 데이터의 인접 요소기리 비교하고, swap 연산을 수행하며 정렬하는 방식 O(n²)
선택(selection) 대상에서 가장 크거나 작은 데이터를 찾아가 선택을 반복하면서 정렬하는 방식 O(n²)
삽입(insertion) 대상을 선택해 정렬된 영역에서 선택 데이터의 적절한 위치를 찾아 삽입하면서 정렬하는 방식 O(n²)
퀵(quick) pivot 값을 선정해 해당 값을 기준으로 정렬하는 방식 O(nlogn)
병합(merge) 이미 정렬된 부분 집합들을 효율적으로 병합해 전체를 정렬하는 방식 O(nlogn)
힙(heap) 힙이라는 자료구조를 사용해서 최소힙 또는 최대힙으로 정렬하는 방식 O(nlogn)
기수(radix) 데이터의 자릿수를 바탕으로 비교해 데이터를 정렬하는 방식 O(kn)
계수(counting) 데이터가 몇 번 나타나는지를 새어서 정렬하는 방식 O(n)

 

1. 버블 정렬(bubble): 두 인접한 데이터를 비교해 정렬

  • 루프를 돌면서 인접한 데이터 간의 swap 연산으로 정렬
    • 원본 데이터의 순서가 훼손된다.
  • 시간 복잡도 O(n²): 다른 정렬 알고리즘보다 속도가 느린 편
    • 수행시간: (n-1) + (n-2) + ... + 2 + 1 = O(n²)
      • worst 케이스와 average 케이스 모두 O(n²)
      • 배열이 운 좋게도 이미 정렬되어 있는경우에도 의미 없이 순환함

 

[버블정렬 코드 개요]

A: 정렬할 배열
N: 정렬할 수 개수

func bubbleSort(A[], n){
	for i in 0..<(n - 1){
		for j in 0..<(n - 1 - i){    //i를 빼는 것은 정렬된 범위를 제외하는 것
			if A[j] > A[j + 1]{
				let temp = A[j]
				A[j] = A[j + 1]
				A[j + 1] = temp
			}
		}
	}
}

[백준 2750 버블정렬]

func bubbleSort(a: [Int], n: Int) -> [Int]{
    var arr = a
    for i in 0..<n - 1{
        for j in 0..<n - 1 - i{
            if arr[j] > arr[j + 1]{
                let temp = arr[j]
                arr[j] = arr[j + 1]
                arr[j + 1] = temp
            }
        }
    }
    return arr
}

 

2. 선택정렬: 최대나 최소 데이터를 데이터가 나열된 순으로 찾아가며 선택

 

  • 최솟값 또는 최댓값을 찾고, 남은 정렬 부분의 가장 앞에 있는 데이터와 swap 하는 것
    • 맨 오른 쪽 원소를 제외하고 루프 반복
      • 하나의 원소만 남을 때 까지 루프를 반복
    • 원본 데이터의 순서가 훼손된다.
  • 시간 복잡도 O(n²): 다른 정렬 알고리즘보다 속도가 느린 편
    • 수행시간: (n-1) + (n-2) + ... + 2 + 1 = O(n²)
      • worst 케이스와 average 케이스 모두 O(n²)

 

[선택정렬 코드 개요]

A: 정렬할 배열
N: 정렬할 수 개수

//오름차순
func selectionSort(A[], N){
	for i in 0..<N-1{    //1개가 남을 때 까지 반복
			for j in (i+1)..<N{    //i는 0부터 시작 -> j는 그 다음 1부터 시작
			Min의 index찾기
		}
	Min과 현재 i 값 교환
}

[백준 2750 선택정렬]

func selectionSort(a: [Int], n: Int) -> [Int]{
    var arr = a
    for i in 0..<n-1{
        var min = i
        for j in i+1..<n{
            if arr[j] < arr[min]{
                min = j
            }
        }
        arr.swapAt(min, i)
    }
    return arr
}

 

3. 삽입정렬: 이미 정렬된 데이터 범위에서 정렬되지 않은 데이터를 적절한 위치에 삽입시켜 정렬

  • 선택 데이터를 현재 정렬된 데이터 범위 내에서 적절한 위치에 삽입하는 것이 삽입 정렬의 핵심
    • A[1...i]의 적당한 자리에 A[i]를 삽입한다.
      • A[i]가 A[i-1]보다 크면 제자리에 두고
      • 그렇지 않다면 A[i-1]부터 왼쪽으로 돌며 A[i]의 위치를 찾는다.
      • A[i]가 삽입될 자리부터 시작해서 그 이후(오른쪽)의 원소들은 모두 한 칸씩 오른쪽으로 shift한다.
  • 시간 복잡도 O(n²): 다른 정렬 알고리즘보다 속도가 느린 편
    • 수행시간: (n-1) + (n-2) + ... + 2 + 1 = O(n²)
      • worst case: 1 + 2 + ... + (n -2) + (n-1)
      • average case: 1/2 * ( 1 + 2 + ... + (n-2) + (n-1))
      • best case: O(n): 다 정렬되어 있는 경우
        • 삽입 정렬은 수행시간이 O(n²)인 비효율적인 정렬 알고리즘에 속하지만,배열이 모두 정렬되어 있는 상태에서는 가장 매력적인 알고리즘이다. 정렬되어 있다면 shift할 필요 없이 외부의 for 루프만 돌면 되기 때문에 O(n) 수행시간이 소요된다.
  • 정렬을 기다리고 있는 부분의 값들의 상대적 위치가 원래 그대로 유지되고 있는, 상당히 안정적인 정렬이다.
    • 선택정렬과 버블정렬은 정렬되지 않은 배열의 크기를 하나씩 줄여나간다.
    • 삽입정렬은 한 개짜리 배열에서 시작해서 이미 정렬된 배열의 크기를 하나씩 늘려나가는 과정에서 차이가 있다.

 

[삽입정렬 코드 개요]

A: 정렬할 배열
N: 정렬할 수 개수

func insertionSort(A[], N){
	for i in 0..<N{
		var temp = A[i]                    //선택 데이터
		var insertIndex = 0                  //삽입할 인덱스를 저장할 변수
		//이전의 정렬된 범위 내에서 역순으로 들어갈 위치를 검색
		while insertIndex >= 0 && A[insertIndex] > temp { 
			//정렬된 범위내에서 선택 데이터보다 크면 오른쪽으로 shift
			A[insertIndex + 1] = A[insertIndex]    
			insertIndex -= 1
		}
		A[insertIndex + 1] = temp                        //루프를 빠져나와서 선택 데이터를 삽입
	}
}

[백준 2750 삽입정렬]

func insertionSort(a: [Int], n: Int) -> [Int]{
    var arr = a
    for i in 1..<n{
        var temp = arr[i]
        var j = i - 1
        while j >= 0 && arr[j] > temp {
            arr[j+1] = arr[j]
            j -= 1
        }
        arr[j+1] = temp
    }
    return arr
}

 

4. 퀵정렬: 기준값(pivot)을 선정해 해당 값보다 작은 데이터와 큰 데이터로 분류하는 것을 반복해 정렬

  • 기준값(pivot)을 중심으로 계속 데이터를 2개의 집합으로 나누면서 정렬하는 것
    • 데이터를 분할할 pivot을 설정한다.
      • pivot이 제일 작은 값 or 큰 값인지 알 수 없기 때문에(=상관 없기 때문에) 중간에 위치한 값으로 설정
    • pivot을 기준으로 데이터를 2개의 집합으로 분리한다.
      • pivot을 기준으로 가장 왼쪽에 있는 데이터를 start
        • start가 pivot보다 작으면 → start += 1
        • start가 pivot보다 크면 → start 멈춤
      • pivot을 기준으로 가장 오른쪽에 있는 데이터를 end
        • end가 pivot보다 크면 → end -= 1
        • end가 pivot보다 작으면 → end 멈춤
      • start와 end이 둘 다 멈췄을 때 start와 end를 swap한 후 start +=1 , end -= 1
      • start == end 지점의 데이터가 pivot보다 크면 pivot의 위치는 -1, 작으면 +1
      • start ≥ end가 되면 종료 → pivot을 기준으로 분할된 집합에서 다시 반복
        • 분리 집합의 요소가 1개 이하가 될 때 까지 재귀
  • 시간 복잡도: O(nlogn)
    • 기준값이 어떻게 선정되는지가 시간 복잡도에 많은 영향을 미친다.
    • worst case: O(n²): 기준값이 항상 끝값일 경우

 

[퀵정렬 코드 개요] - 중간 값 Pivot은 swap시에 업데이트가 필요하다

func quickSort(start: Int, end: Int){
    if start >= end { return }
    var left = start
    var right = end
    var pivot = (left + right) / 2
    
    while left <= right{
        while left < end && arr[left] < arr[pivot]{
            left += 1
        }
        while right > start && arr[right] > arr[pivot]{
            right -= 1
        }
        
        if left <= right {
			//pivot도 swap되면 인덱스 개념인 pivot도 업데이트 되어야 한다
            if pivot == left {
                pivot = right
            }else if pivot == right{
                pivot = left
            }
            arr.swapAt(left, right)
            left += 1
            right -= 1
        }
    }

    if start < right {
        quickSort(start: start, end: right)
    }
    if left < end {
        quickSort(start: left, end: end)
    }
}

[퀵정렬 코드 개요] - 고차함수 filter 사용 - pivot이 가운데 있는 경우

A: 정렬할 배열

quickSort(A[]) -> [Int]{
	guard A.count > 1 else {return A}    //집합의 요소가 1개 이하라면 종료
	
	let pivot = A[A.count / 2]           //pivot이 가운데 있는 경우
	let left = A.filter {$0 < pivot}
	let right = A.filter {$0 > pivot}
	let equal = A.filter {$0 == pivot}    //equal을 따로 처리하지 않으면 배열의 개수가 달라짐
	
	return quickSort(left) + equal + quickSort(right)
}

filter 함수 참고 바람: https://ksk9820.tistory.com/127

[백준 2750 퀵정렬]

func quickSort(a: [Int]) -> [Int]{
    var arr = a
    guard arr.count > 1 else {return arr}
    
    let pivot = arr[arr.count / 2]
    let left = arr.filter {$0 < pivot}
    let right = arr.filter {$0 > pivot}
    let equal = arr.filter {$0 == pivot}
    
    return quickSort(a: left) + equal + quickSort(a: right)
}

 

퀵정렬의 경우 

1. pivot값이 중간값인 상태에서(제일 왼쪽 값과 swap하지 않은 상태에서)

2. right, left로 quickSort를 호출하지 않고 pivot - 1, pivot + 1로 quickSort를 호출하여 정렬하는 방법

을 구현하고 싶었으나 아직 구현하지 못했다. 만약 위의 상황으로 퀵정렬을 구현하는 방법을 알고 계신분이 있다면 댓글 꼭 부탁드립니다.

1. pivot = left + right / 2 인 상황에서 pivot -1, pivot + 1 의 재귀함수를 호출 하면?
2. pivot이 가운데에 위치한 상황에서 pivot도 swap될 수 있기 때문에 pivot의 index를 업데이트하는데 맞는지
3. left와 right이 엇갈리는 경우 right과 pivot을 swap했는데도 정렬되지 않음

 

5. 병합정렬: 분할정복 방식을 사용해 데이터를 분할하고 분할한 집합을 정렬하며 합치는 알고리즘

  • 투 포인터 개념을 사용하여 왼쪽, 오른쪽 그룹을 병합한다.
    • 배열 요소의 개수가 1개 → 2개 → 4개 → 8개 → ... → n개가 되도록 분할한 후 정렬해서 병합 재귀
      • 왼쪽 포인터와 오른쪽 포인터의 값을 비교하여 작은 값을 결과 배열에 추가하고 포인터를 오른쪽으로 1칸 이동
      • 두 배열 중(left or right) 모든 값을 결과 배열에 넣은 배열이 생긴다면
        • 아직 요소가 남아 있는 배열의 값을 그대로 결과 배열에 추가한다.
          • 이미 정렬되어 있는 배열이기 때문에 그대로 추가해도 무방
  • 시간 복잡도: O(nlogn)

 

[병합정렬 코드 개요]

A: 정렬할 배열
B: 정렬된 배열
func divide(A[]) -> [Int] {
	//배열 요소의 개수가 1개이면 값을 비교할 수 없기 때문에 재귀 종료 지점 설정
	if A.count <= 1 { return }
	let mid = A.count / 2
	let left = [0..<mid]
	let right = [mid..<A.count]
	
	return mergeSort(divide(left), divide(right))
}

func mergeSort(Left[], Right[]) -> [Int]{
	//Left와 Right을 비교할 수 있을 때 == Left와 Right에 비교할 요소가 남아 있을 떄 까지
	while !Left.isEmpty && !Right.isEmpty{

		//Left의 첫 요소와 Right의 첫 요소를 비교해서 작은값을 Result에 넣고 해당 값 삭제
		if Left[0] > Right[0]{
			result.append(Left.removeFirst())

		}else{
			//반대
		}
	}
	
	if !empty{
		//어느 한 배열에 요소가 하나도 없을 때 남은 배열(정렬된 상태)를 result에 추가하고 리턴
		B.append()
	}
	return B
		
}

[백준 2750 병합정렬]

func divide(a: [Int]) -> [Int]{
    if a.count <= 1 {return a}
    let mid = a.count / 2
    let left = Array(a[0..<mid])    //Array()를 하지 않으면 ArraySlice<Int>이기 때문
    let right = Array(a[mid..<a.count])
    return mergeSort(left: divide(a: left), right: divide(a: right))
}

func mergeSort(left: [Int], right: [Int]) -> [Int]{
    var result: [Int] = []
    var l = left
    var r = right
    
    while !l.isEmpty && !r.isEmpty{
        if l.first! > r.first! {
            result.append(r.removeFirst())
            
        }else{
            result.append(l.removeFirst())
        }
    }
    
    //left와 right중 하나라도 empty가 되면
    if !l.isEmpty{
        result.append(contentsOf: l)
    }
    if !r.isEmpty{
        result.append(contentsOf: r)
    }
    return result
}

 

6. 힙정렬: 힙이라는 특수한 자료구조를 사용하는 정렬 알고리즘

  • 힙(Heap)

  • 이진 트리로서 맨 아래 층을 제외하고는 완전히 채워져 있고 맨 아래층은 왼쪽 부터 꽉 채워져 있다.
    • 최소힙의 각 노드의 값은 자신의 자식 값보다 작다. → 루트 노드에는 최소값
    • 최대힙의 각 노드의 값은 자신의 자식 값보다 크다. → 루트 노드에는 최대값
  • 힙의 모든 노드는 하나씩의 값을 가지고 있다.
  • 각 노드의 값은 자신의 자식 값보다 작다.(최소힙일 때)

 

  • 힙정렬은 먼저 주어진 배열을 힙으로 만든다.
    • (루트 노드에 있는 원소를 제거하여 다른 곳에 저장한다.)
      • 맨 끝에 있는 원소와 루트 노드를 교환한다.
        • 루트 노드와 자식들간에 힙성질이 깨짐
          • heapify()를 이용해 힙성질을 만족하도록 수선한다.
            • heapify(A[], k, n): A[k]를 루트로 하는 트리를 힙성질을 만족하도록 수선
              • A[k]에 매달린 두 서브트리가 힙성질을 만족하는 상태에서 A[k]를 루트로 하는 서브트리 전체가 힙성질을 만족하도록 수선
              • 힙은 완전이진트리이기 때문에 인덱스의 절반(1/2)만 확인해줘도 된다.
                • 리프 노드는 그 자체로 힙성질을 만족 → 리프 노드가 아닌 노드들을 맨뒤에서 부터 수선
    • 힙에 요소가 하나만 남을 때 까지 반복
      • 마지막 요소가 제일 크거나 제일 작은 요소로 자동 정렬
  • 시간복잡도: O(nlogn)
    • heapify:힙을 만들 때는 힙의 높이 만큼만 계산: logn
    • heapify를 호출하는 횟수: n

 

[힙정렬 코드 개요]

func buildHeap(){
//힙 만들기
//인덱스 절반만 heapify호출하기
}

func heapify(A[], k){
//힙 수선하는 과정
	//root가 0부터 시작
	let left = 2 * k + 1
	let right = 2 * k + 2

	//자식노드가 2개일 때
	//자식노드가 1개일 때
	//자식노드가 0개일 때
	
	//자식노드중 제일 작은 노드의 값과 루트노드(k)의 값과 비교해서 swap
	//swap하고 그 자식 노드의 자식노드와 다시 heapify
}

[백준 2750 힙정렬]

func buildHeap<T: Comparable>(a: inout [T], n: Int){
    var i = n / 2
    while i >= 0 {
        heapify(a: &a, k: i, n: input)
        i -= 1
    }
}

//최소힙정렬
func heapify<T: Comparable>(a: inout [T], k: Int, n: Int){

    let left = 2 * k + 1
    let right = 2 * k + 2
    var smaller: Int!
    
    //k가 두 자식을 가지는 경우
    if right < n {
        if a[left] < a[right]{
            smaller = left
        }else{
            smaller = right
        }
        
    }
    //k가 왼쪽 자식만 가지는 경우
    else if left < n {
        smaller = left
    }
    //k가 리프노드인 경우
    else{
        smaller = nil
        return
    }
    
    //최소힙
    //부모노드보다 자식노드가 더 작은 경우 swap
    //swap한 후 자식노드의 자식노드도 확인해야하기 때문에 heapify
    
    if smaller != nil && a[smaller] < a[k]{
        a.swapAt(smaller, k)
        heapify(a: &a, k: smaller, n: n)
    }
}

func heapSort<T: Comparable>(a: inout [T], n: Int){

    buildHeap(a: &a, n: n)
    
    var size = n
    var i  = size - 1
    
    //하나 남을 때 까지
    for j in (1..<n).reversed(){
        //마지막 리프노드와 루트 노드 교환
        a.swapAt(j, 0)
        heapify(a: &a, k: 0, n: j)
    }
}

제너릭 참고: https://ksk9820.tistory.com/107

 

7. 기수정렬: 값을 놓고 비교할 자릿수를 정한 다음 해당 자릿수만 비교하는 정렬

  • 입력이 모두 k 이하의 자리수를 가진 자연수인 특수한 경우에(자연수가 아닌 제한된 종류를 가진 알파벳 등도 해당) 사용할 수 있는 방법

  • 값을 비교하지 않는 특이한 정렬
    • 해당 자릿수를 비교한다는 뜻은
      • 432 와 123 → 4와 1, 3과 2, 2와 3을 비교
    • 10개의 공간이 필요하고, 각 공간은 0부터 9까지 값을 대표
      • 일의 자릿수를 기준으로 데이터를 저장 → 십의 자릿수를 기준으로 데이터를 저장 → 백 → 천 → ...
      • 자릿수를 얻는 방법 예) 432
        • 432에서 2는
          • 432 / 1 = 432
          • 432 % 10 = 2 - 나머지 연산 활용
        • 432에서 3은
          • 432 / 10 = 43
          • 43 % 10 = 3
  • 안정성을 유지하는 정렬
    • 값이 같은 원소끼리는 정렬 후에 원래의 순서가 바뀌지 않는 성질
      • 데이터를 담는 자료구조가 FIFO(First In First Out) 형태를 취하기 때문에 원래의 순서를 유지할 수 있다.
  • 시간복잡도: O(kn)
    • k는 데이터의 자릿수
    • 기수정렬은 시간복잡도가 가장 짧은 정렬이다. 정렬해야하는 데이터의 개수가 너무 많으면 기수정렬 알고리즘을 활용

 

[기수정렬 코드 개요] - 백준 2750에서는 런타임에러

func radixSort(arr: inout [Int]){
    var done = false //루프종료를 알릴 벼
    var index: Int   //각 자릿수의 숫자
    var digit = 1    //자릿수 - 매 루프마다 * 10

    while !done {
        done = true
        var buckets: [[Int]] = [[Int]](repeating: [], count: 10)

        for num in arr{
            index = num / digit
            buckets[index % 10].append(num)
            //done은 루프를 돌 때 마다 true,
            //index > 0은 digit의 자릿수보다 num의 자릿수가 클 경우로
            //계속해서 비교할 수 있는 경우를 말함
            //done이 계속해서 true이고 index가 계속해서 0이하가 나오고 끝나는 경우에 종료
            if done && index > 0 {
                done = false
            }
        }
        //정렬된 buckets을 arr로 copy
        var i = 0
        for j in 0..<10{
            let bucket = buckets[j]
            for k in bucket{
                arr[i] = k
                i += 1
            }
        }
        //그 다음 자릿수
        digit *= 10
    }
}

[백준 10814 기수정렬]

func radixSort(arr: inout [Int], name: inout [String]){
    var done = false
    var index: Int
    var digit = 1

    while !done {
        done = true
        var buckets: [[Int]] = [[Int]](repeating: [], count: 10)
        var buckets_name: [[String]] = [[String]](repeating: [], count: 10)
        for num in 0..<arr.count{
            index = arr[num] / digit
            buckets[index % 10].append(arr[num])
            buckets_name[index % 10].append(name[num])
            if done && index > 0 {
                done = false
            }
        }

        var i = 0
        for j in 0..<10{
            let bucket = buckets[j]
            let bucket_name = buckets_name[j]
            for k in 0..<bucket.count{
                arr[i] = bucket[k]
                name[i] = bucket_name[k]
                i += 1
            }
        }
        digit *= 10
    }
}

 

8. 계수정렬: 한정된 범위에서 갯수를 세어 정렬하는 알고리즘

  • 정렬하고자 하는 원소들의 값이 O(n)을 넘지 않는 경우에(= 한정된 범위를 갖는 경우) 사용할 수 있다.
  • 배열 A[1...n]의 원소들이 k를 넘지 않는 자연수일 경우
    • 1부터 k까지의 자연수가 각각 몇 번 나타나는지를 샌다
      • A[1...n]의 각 원소가 몇 번째에 놓이면 되는지를 계산할 수 있다.
  • 정렬할 원소가 꼭 양수일 필요가 없다. 원소들이 모두 -k와 k사이의 정수이고 k가 O(n)일 경우에도 여전히 계수정렬을 사용하여 선형시간에 정렬할 수 있다.
  • 시간복잡도: O(n)
    • k가 O(n)을 초과하면 시간은 O(k)가 된다.
    • 만약 k가 O(nlogn)을 초과하면 계수정렬은 병합, 퀵, 힙정렬보다 매력이 없어진다.
    • 그래서 일반적으로 계수정렬은 k가 O(n)을 초과하지 않는 경우에 선형시간에 정렬하기 위해 사용하는 것

 

[계수정렬 코드 개요]

func countingSort(arr: inout [Int]){
    var max = arr.max()
    var count = [Int](repeating: 0, count: max! + 1)
    
    for i in 0..<input{
        count[arr[i]] += 1
    }
    
    for i in 0...max!{
        if count[i] != 0{
            for j in 0..<count[i] {
                print(i)
            }
        }       
    }
}

 

 

 


참고하였습니다. 감사합니다.

 

https://m.blog.naver.com/ndb796/221228361368

https://m.blog.naver.com/PostView.naver?blogId=ndb796&logNo=221228342808

https://joycestudios.tistory.com/61

Do it! 알고리즘 코딩 테스트 자바 편 

728x90