26년1학기/알고리즘

알고리즘) 26.3.16

kimchangmin02 2026. 3. 16. 12:09

 

의문점

 

수업중, 문제 풀었던것(라의 형태)[  ]

 

 

구현, 

 

문제풀기

 

 

 

bottom up에서

왜 a!=src일수있늕; ㅇ해[  ]

(참조변수랑 실제 배열이 다르다

swap할때 서로 가르키던걸 엇바꾸니깐


1

run의 크기, run의 갯수 다름

 

2

bottom-up에서 왜 홀/짝 의미있나

외부반복문이 그만큼 도는거니깐 (swap은 외부반복이 될떄마다 하고있음)

(내부반복문은 어떻다 했더라)[  ]

 

 

 

 

 

 

 

 

 


(갑자기 왜 곱하느냐_)

 

B:

블록(덩어리)의 갯수이지,

블록안의 데이터수가 아님

 

 

 

 

 

1. 바텀업 합병 정렬 (Bottom-up Merge Sort)

지난 시간의 탑다운(Top-down) 방식에 이어, 재귀를 사용하지 않는 바텀업 방식을 설명합니다.

  • 동작 과정:
    • 처음에는 1개씩 묶어서 정렬하고, 그 다음은 2개, 4개, 8개 순으로 사이즈를 2배씩 늘려가며 병합합니다.
    • 최종적으로 전체가 한 덩어리가 될 때까지 이 과정을 반복합니다.
  • Merge 로직:
    • 두 개의 덩어리(Low~Middle / Middle+1~High)를 비교하여 더 작은 값을 새로운 배열(out)에 복사합니다.
  • 구현 시 주의점 (김밥 자르기 비유):
    • 데이터를 2개씩 짝지어 병합하다 보면, 마지막 부분은 딱 맞아떨어지지 않고 자투리가 남을 수 있습니다.
    • 마지막 덩어리가 n개보다 적거나 아예 없을 경우를 대비해 Math.min(원래 끝 위치, 실제 배열의 끝)을 사용하여 인덱스 범위를 초과하지 않도록 처리해야 합니다.
  • 성능 최적화:
    • 매번 배열을 복사하면 느려지므로, **소스(Source) 배열과 목적지(Destination) 배열의 포인터만 스왑(Swap)**하며 병합을 진행합니다.
    • 최종 결과가 원래 배열 a가 아닌 다른 임시 배열에 들어있을 경우, 마지막에 System.arraycopy()를 이용해 복사합니다. (자바에서 포문보다 arraycopy가 가장 빠름)

2. 외부 정렬 (External Sorting)

메모리에 한꺼번에 올릴 수 없을 정도로 큰 파일(예: 수 테라바이트)을 정렬하는 방법입니다.

  • 기본 아이디어:
    1. 정렬 단계 (Sorting Phase): 파일을 메모리 크기만큼 잘라서 읽어 들인 후, 내부 정렬을 거쳐 **'런(Run)'**이라는 중간 결과 파일들을 만듭니다.
    2. 병합 단계 (Merging Phase): 생성된 여러 개의 런들을 합쳐서 최종적으로 하나의 정렬된 파일을 만듭니다.

 

 


4. 현대의 외부 정렬: TPMMS (Two-Phase Multi-way Merge Sort)

현대 컴퓨터 환경(랜덤 액세스 가능, 큰 메모리)에서 주로 사용하는 방식입니다.

  • 핵심 목표: '정렬 한 번 + 병합 한 번', 즉 딱 2단계(Two-Phase) 만에 정렬을 끝내는 것입니다.
  • 블록(Block) 단위 I/O:
    • 디스크에서 데이터를 읽을 때 한 바이트씩 읽는 게 아니라, 운영체제가 정한 블록 단위(예: NTFS의 경우 4KB)로 통째로 읽어 버퍼에 넣습니다. 이것이 하드웨어와 OS 수준에서 입출력 횟수를 줄이는 방법입니다.
  • TPMMS의 동작:
    1. 메모리 크기만큼 런을 만듭니다.
    2. 병합 단계에서 메모리를 쪼개어, 각 런마다 1블록씩 들어갈 공간(입력 버퍼)을 확보하고, 출력용으로 1블록 공간을 둡니다.
    3. 각 블록에서 데이터를 비교하며 병합하다가, 한 블록이 소진되면 즉시 디스크에서 다음 블록을 읽어옵니다.

 

 

 

 

 

 

 

 

 

 

 

 

M이 전체 데이터수인거네 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

'한 번에 병합 가능한 런의 개수'

는 쉽게 말해 "내가 동시에 몇 개의 주머니(런)에서 숫자를 꺼내서 비교할 수 있는가?" 하는 **'동시 처리 능력

 

 

1. 상황: 5성급 호텔 요리사의 소스 섞기

요리사인 당신은 각기 다른 재료가 담긴 거대한 솥(런) 10개에서 재료를 조금씩 꺼내서 **하나의 큰 통(최종 파일)**에 섞으려고 합니다.

  • 솥(런): 너무 커서 주방 안으로 들고 올 수 없습니다. (디스크에 있음)
  • 주방 조리대(메모리): 공간이 한정되어 있습니다.
  • 국자(블록): 솥에서 재료를 한 번에 퍼올 수 있는 양입니다.

2. 왜 개수의 제한이 생길까요?

당신이 솥 10개를 동시에 섞으려면, **조리대 위에 최소한 10개의 그릇(입력 버퍼)**을 놓아야 합니다. 각 솥에서 재료를 한 국자씩 퍼서 그릇에 담아놔야 서로 비교하며 섞을 수 있으니까요.

그런데 만약 당신의 **조리대 공간(메모리)**이 그릇 5개만 놓을 수 있는 크기라면 어떻게 될까요?

  1. 솥 10개를 한꺼번에 섞는 것은 불가능합니다.
  2. 당신은 한 번에 최대 4개의 그릇만 올려둘 수 있습니다. (나머지 1개 공간은 완성된 소스를 담아낼 출력용 그릇으로 써야 하니까요.)

여기서 '한 번에 병합 가능한 런의 개수'가 바로 4개가 되는 것입니다.


 

 

 

 

 

 

 

 

 

 

(한번에 처리가 아니네)

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

아까는 메모리(M)만큼만 처리할수있다면서 그러면 무조건 f<m인거 아닌가 뭐지

 

 

 

 

 

M을 넘을 수 없다"는 말의 진짜 의미

이 말은 **[한 번에(내부적으로) 정렬할 수 있는 조각의 크기]**에 대한 제한입니다.

 

 

 

 

 

(동시에 비교하고싶기떄문에 )

 

 

 

 

 

 

 

 


이렇게 바꿔서 호출하면 왜 복사가 필요없다는거지[  ]

 

 

 

 

// 1. 함수 정의: a를 원본으로 써서 정렬한 뒤, 결과를 aux에 담아라!
private static void sort(Comparable[] a, Comparable[] aux, int lo, int hi) {
    ...
    // 2. 재귀 호출: 이번엔 역할을 바꾼다! 
    // "aux를 원본으로 써서 정렬한 뒤, 결과를 a에 담아와라"
    sort(aux, a, lo, mid); 
    sort(aux, a, mid + 1, hi); 


}

 

 

 

 

 

merge의 매개변수 순서가 호출-정의가 같은 이유

sort 재귀 함수이기 때문에 자기 자신을 호출할 때 순서를 바꿔야 핑퐁(역할 교대)이 일어납니다. 하지만 merge는 재귀 함수가 아니라, sort가 시키는 일을 수행하는 도우미 함수입니다.

 

 

 

 

  • 기본형 merge(a, aux): a aux 복사한 뒤, aux를 보고 다시 a를 채웁니다. (결국 a에서 시작해서 a로 끝남)
  • 개선형 merge(aux, a): 복사 과정 없이, 그냥 aux에 있는 걸 바로 a에 넣습니다. (시작은 aux, 끝은 a)

이 개선된 코드에서 merge 함수는 **"재료와 결과 그릇이 서로 다른 배열"**이라는 것을 명확히 하기 위해 매개변수 이름을 아예 src(재료), dst(결과) 처럼 취급하고 있습니다. 이미지에서는 재료가 되는 첫 번째 자리에 aux를 쓰고, 결과 그릇인 두 번째 자리에 a를 쓴 것입니다.

 

 

 

 

2단계까지하면 aux가 일종의 a(정렬잘된 상태가 되는거고)

a가 쓰레기 정렬상태인건가, 그러니깐, 바로 a에 대입하면 되는 ?

// 정의: 1번 칸(a)을 결과 저장소로, 2번 칸(aux)을 보조 재료로 쓰기로 함
private static void sort(Comparable[] a, Comparable[] aux, ...) {
    ...
    // 하위 작업들에게 시킵니다: "결과를 aux에 만들어와!"
    sort(aux, a, lo, mid); 
    sort(aux, a, mid + 1, hi); 

    // 하위 작업이 끝나면, 결과물은 어디에 있을까요? 
    // 위에서 sort의 1번 칸에 aux를 넣었으니, 결과는 aux에 들어있습니다.

    // 마지막으로 merge를 호출합니다:
    merge(aux, a, lo, mid, hi); 
}

 

 

 

 

 

 

 

 

코드 안에 a = ... 같은 대입 연산자가 없는데 왜 "첫 번째 게 결과 저장용"이라고 하는지

 

 

코드에 명시적으로 써 있지는 않지만, 개발자가 그렇게 설계를 한 것입니다.

 

 

 

 

 

 Bottom-up(상향식) 방식에서 왜 처음부터 이런 '스왑(핑퐁)' 방식을 기본처럼 사용하는지, 그리고 **Top-down(하향식)**과의 차이점이 무엇인지 핵심을 짚어드릴게요.


1. 구현의 난이도 차이 (왜 Bottom-up은 이게 기본인가?)

  • Top-down: 재귀 호출을 타고 내려가면서 매개변수 순서를 바꿔야 합니다. 호출 스택마다 "누가 원본이고 누가 보조인가"가 계속 바뀌기 때문에 머릿속으로 그리기가 꽤 복잡합니다. 그래서 처음 배울 때는 "복사"하는 쉬운 버전부터 배웁니다.
  • Bottom-up: 반복문(for문)을 사용합니다. 한 단계(예: 크기 1→2 병합)가 모두 끝나면, 결과물은 전부 보조 배열(dst)에 들어가 있습니다. 이때 포인터(주소값)만 딱 바꿔주면 다음 단계(크기 2→4 병합)에서 바로 재료로 쓸 수 있습니다.
    • 이미지의 tmp = src; src = dst; dst = tmp; 부분이 바로 그겁니다. 데이터 수만 개를 옮기는 게 아니라, "배열의 이름표"만 바꾸는 것이라서 구현이 매우 쉽고 강력합니다.

2. "복사" 비용의 차이

  • Top-down: 최적화를 안 하면 매번 merge를 할 때마다 복사를 합니다.
  • Bottom-up: 만약 스왑을 안 쓰면, 바깥쪽 루프가 한 번 돌 때마다(크기가 1, 2, 4, 8...로 커질 때마다) 배열 전체를 다시 복사해야 합니다.
    • Bottom-up 구조 자체가 "전체 데이터를 한 번에 훑으며 다음 단계로 넘기는" 방식이라, 한 층이 끝날 때 포인터를 슥 바꿔주는 게 너무나 자연스럽고 효율적입니다.

3. 구조적 차이 (결과물의 위치)

  • Top-down: 재귀가 끝나면 결국 최종 결과는 원래 배열 a에 돌아오도록 설계하기가 상대적으로 쉽습니다.
  • Bottom-up: 루프가 몇 번 도느냐에 따라 최종 정렬된 데이터가 src에 있을 수도 있고, dst에 있을 수도 있습니다.
    • 그래서 이미지 마지막 줄에 if (src != a) System.arraycopy(...) 코드가 있는 것입니다.
    • 마지막에 이름표를 확인해보고, 혹시 결과가 보조 배열에 들어있다면 그때 딱 한 번만 통째로 복사해 주는 방식입니다.

요약하자면

  1. Bottom-up은 구조상 스왑이 훨씬 편하다: 루프가 한 번 끝날 때마다 주소값(포인터)만 교체하면 되므로, 굳이 복사하는 비효율적인 방식을 쓸 이유가 없습니다.
  2. 재료 관리의 용이성: Bottom-up은 "작은 조각들을 모아 큰 덩어리를 만드는" 과정이 매우 선형적입니다. 따라서 src에서 읽어 dst에 쓰고, 다 쓰면 둘의 역할을 바꾸는 '핑퐁' 전략이 알고리즘의 흐름과 완벽하게 일치합니다.

 

 

 

 

 

 

 

 

'정렬이 얼마나 되었느냐'**가 아니라, **'데이터가 물리적으로 어느 배열에 가 있느냐'*

(정렬이 덩어리만큼씩 되는건 동일하지만 )

 

 

 

 

 

 

 

 

 

 

1. 상향식 (Bottom-up): "이중 for문의 안쪽 루프"

for (int n = 1; n < N; n *= 2) {      // (1) 바깥 루프 (덩어리 크기)
    for (int i = 0; i < N; i += 2*n) { // (2) 안쪽 루프 (중요!!)
        merge(src, dst, i, i+n-1, ...); // (3) 여기서 실제 데이터 이동 발생
    }
    tmp = src; src = dst; dst = tmp;   // (4) 여기서 이름표 교체
}
  • 물리적 이동의 증거: (2)번 for문을 보세요. i 0부터 N까지 갑니다.
  • 즉, 이 루프 안에서 (3)번 merge가 여러 번 실행되면서 0번~끝번까지의 모든 데이터 src[ ]에서 dst[ ]로 자리를 옮기게 됩니다.
  • 시점: (2)번 루프가 딱 끝나는 찰나, 배열 전체가 이사를 완료한 상태입니다. 그래서 (4)번에서 전체 이름표를 바꿔도 데이터가 꼬이지 않습니다.

2. 하향식 (Top-down): "재귀 호출의 개별성"

void sort(a, aux, lo, hi) {
    sort(aux, a, lo, mid);      // (A) 왼쪽 절반만 정렬 (이사)
    sort(aux, a, mid + 1, hi);  // (B) 오른쪽 절반만 정렬 (이사)
    merge(aux, a, lo, mid, hi); // (C) 두 절반을 합침
}
  • 물리적 이동의 특징: (A)가 실행될 때, 데이터 이동은 lo에서 mid까지만 일어납니다.
  • 차이점: 상향식처럼 0부터 N까지 한꺼번에 훑는 루프가 없습니다.
  • (A)가 끝나도 (B) 구역은 아직 제자리에 있습니다. 즉, 배열 전체가 동시에 이사를 마친 시점이 중간 단계에서는 절대 오지 않습니다.
  • 그래서 상향식처럼 tmp = a; a = aux; 같은 전체 이름표 바꾸기를 쓸 수가 없는 것입니다.

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

아아아ㅏㅏ 10이라는 숫자는 1부터시작하니깐

 

(0부터 10까지를 10이라고 하는게 아니라

1부터 10까지를 10이라고 하잖아 

 

(걍 갯수라서 그런가)[  ]

 

 

 

 

 

 

1

left >= right  

return조건 있던거: 퀵소트임(피봇있던거)

 

 

 

 

 

2

 

 

 

//bottomup은 재귀가 아니라 반복문이엇네 

 

 

//근데 merge는 합치는것만이잖아, 아 시작이 1인깐, 이미 정렬되어있다고 가정하고 시작하는?
//top-down에서 lo>=hi return<여기부터 dwon은 시작하는건가
//정렬되어있다고 가정하고(2부터는 이전단계에서 잘 정렬햇다고 믿고)<합치는건가

 

 

merge(a,arr,j, j+n-1 ,min(j+2*n-1,N-1));
//j+2*n-1이유[  ]
//j+n-1
//n은 갯수니깐, 갯수가 중간에 들어가있으면 -1하는건가

 

//뭐를 증가시켜주는거지, 뭐를 몇번 반복하는거지
//n,N 뭐가 외부반복문인건지
//small-n이 외부 ?
int n=1;
for (;n<N; n*=2) {
    for (int j = 0; j < N; j+=2*n) {
        //내부반복문의 범위
        //이렇게 하면 어케합치지,
        //합치는건 merge역할이긴한데, 재귀가 업승면 어케
        //아 그래서 +=2n인가, 한번의 반복마다, 딱 두개씩만 할거니깐,
        //어디에다가 합치는가<무조건 a인가 ??[  ]

(내부반복문이나 과정에서 a에 바로 정렬하지않고 src,des이용함)

a에 할당은 마지막에 

(어차피 src는 a가르키고있음) 

(마지막 분기점 왜필요한지)>핑퐁하면 어디를 가르키는지가 계속 바뀌던가,

 

 

 

        Comparable[] dst = new Comparable[N]; //저장할(DES만 새로운 배열로 만들면 끝이 아니라)
        
        a랑 
        a를 참조하는 src변수도 만들어야하네 
        Comparable[] tmp;//temp는 선언만(new로 생성ㄴㄴ

 

 

 

merge(src, dst, i, i+n-1, Math.min(i+2*n-1, N-1)); // (4,4,3)이 남았을 때?
tmp = src; src = dst; dst = tmp;

두번째 매개변수가 결괏값 저장하는거니깐

항상 src만 최신 정렬상태인데

우리는 이 src가 a이길바라니깐 

 

 

근데 왜 src변수가 필요하지, 그냥 a랑 des핑퐁하면 안되나 

 

 

 

 

 

 

 

 

근데 src=a
src,de스왑, <이 스왑이 항상 최신데이터가 src있게 만들어주지만,

어떤 배열을 가르키는지(젤처음 arr배열인지, a인지의 순서를 바꾸기떄문이라는건가,
src가 최신이 아니라는 의미가 아니라, >

 

 

swap helper메소드가 있는게 아니라 

구현해야해서 

tmp = src; src = dst; dst = tmp;

temp 참조변수필요한거고 

 

 

 

 

 

⚠️ 수정해야 할 부분: merge 호출 시 매개변수

사용자님은 merge(a, des, ...)라고 쓰셨는데, 이렇게 하면 항상 원본 a에서만 데이터를 읽어오게 됩니다.

우리가 앞에서 열심히 이야기했던 **'핑퐁(Swap)'**의 의미는 **"지난 단계에서 정렬된 결과(src)를 읽어서 새로운 결과(des)를 만든다"**는 것입니다. 따라서 첫 번째 매개변수는 a가 아니라 **src**가 되어야 합니다.

 

 

 

 

 

 

 

//mid매개변수는 merge에만 있구나
sort(a,aux,mid+1,hi);