병합 정렬의 재귀에 대한 미세한 관점

머리말

이번에는 병합 정렬 알고리즘을 자세히 다루지는 않고 병합 정렬 알고리즘을 사용하여 재귀에 대해 논의합니다. 병합 정렬의 특징은 연속해서 두 번의 재귀 호출을 사용한다는 점인데, 이번에는 전체 재귀 과정을 미시적인 관점에서 관찰하고 재귀의 본질을 이해해 보겠습니다. 읽어보면 분명 강해질 것입니다!

암호

먼저 코드로 바로 가보겠습니다!

using System.CodeDom.Compiler;

int _1 = 0;
int _2 = 0;

void __merge(int[] arr, int left, int mid, int right, string flag)
{
    
     
    Console.WriteLine($"__merge_{
      
      flag}: left={
      
      left+1}, mid={
      
      mid + 1}, right={
      
      right + 1}");
    int[] copy = new int[right - left + 1];
    //copy arr[left,right] to copy[]
    for (int ii = left; ii <= right; ii++)
    {
    
    
        copy[ii - left] = arr[ii];
    }
    int i = left;
    int j = mid + 1;
    for (int k = left; k <= right; k++)
    {
    
    
        if (i > mid)
        {
    
    
            arr[k] = copy[j-left];
            j++;
        }
        else if (j > right)
        {
    
    
            arr[k] = copy[i - left];
            i++;
        }
        else if (copy[i - left] < copy[j - left])
        {
    
    
            arr[k] = copy[i - left];
            i++;
        }
        else
        {
    
    
            arr[k] = copy[j - left];
            j++;
        }
    }
}

void __merge_sort(int[] arr, int left, int right, string flag)
{
    
    
    
    if (left >= right)
        return;

    if (flag.Contains("1"))
    {
    
    
        _1 += 1;
    }
    if (flag.Contains("2"))
    {
    
    
        _2 += 1;
    }

    int mid = (left + right) / 2;
    Console.WriteLine($"{
      
      flag}, left={
      
      left+1}, mid={
      
      mid+1}, right={
      
      right + 1}");
    __merge_sort(arr, left, mid, "第1个merge_sort");
    __merge_sort(arr, mid + 1, right, "第2个merge_sort");
    __merge(arr, left, mid, right, flag);
}


void merge_sort(int[] arr)
{
    
    
    __merge_sort(arr, 0, arr.Length - 1, "第0个merge_sort");
}

int[] arr = {
    
     1, 3, 5, 7, 8, 2, 4, 6};
merge_sort(arr);


Console.WriteLine($"_1:{
      
      _1}||_2:{
      
      _2}");
foreach (var item in arr)
{
    
    
    Console.Write(item + " ");
}

Console.ReadLine();

재귀 분석

이 코드의 특별한 점은 두 가지 재귀를 사용한다는 것입니다.

_1과 _2는 첫 번째와 두 번째 재귀 호출 횟수를 기록합니다.(알고리즘 로직과 무관) 여기에 추가된 플래그 매개변수는 주로 재귀 프로세스를 분석하기 위한 것입니다.

첫 번째 __merge_sort 재귀 함수는 주로 왼쪽 배열을 두 부분으로 연속적으로 나누는 것입니다.
두 번째 __merge_sort 재귀의 주요 기능은 오른쪽 배열을 두 부분으로 연속적으로 나누는 것입니다.

병합은 두 개의 분할된 배열을 크기 순서대로 하나로 결합합니다!

이 알고리즘을 구현하는 데 있어 어려운 점은 배열 경계를 재귀적으로 구성하고 파악하는 데 있습니다.

거시적으로

void __merge_sort(int[] arr, int left, int right)
{
    
    
    int mid = (left + right) / 2;
    __merge_sort(arr, left, mid);
    __merge_sort(arr, mid + 1, right);
    __merge(arr, left, mid, right, flag);
}

__merge_sort의 재귀를 통해 배열을 두 부분으로 나누고, 분할된 두 배열을 병합하는 과정입니다.
__merge를 사용하여 병합하는 전제는 병합할 두 배열이 이미 정렬된 배열이라는 것입니다!
그러나 이를 하나의 숫자로 나누면 그 숫자는 배열이고, 이 숫자도
순서가 있는 배열로 간주할 수 있습니다.
여기에 이미지 설명을 삽입하세요.
따라서 두 점이 "극단" 수준에 도달하면 __merge의 전제가 충족됩니다.

이분법이 완성된 후 Merge 작업은 다음과 같습니다.
병합 프로세스
이 그림을 보면 재귀 알고리즘을 생각하기 쉽지만 재귀 함수를 구성하는 방법은 무엇입니까? 그것은 약간 비슷합니다:
코끼리를 냉장고에 설치하려면 몇 단계가 필요합니까? 거시적 관점에서 보면 다음과 같습니다:
1 첫 번째 단계는 왼쪽으로 나뉩니다: __merge_sort(arr, left, mid);
2 두 번째 단계는 오른쪽으로 나뉩니다: __merge_sort(arr, mid + 1, right );
3 세 번째 단계는 함께 통합됩니다: __merge(arr, left, mid, right, flag);

미시적인 관점에서

먼저 전체 재귀 프로세스가 어떻게 실행되는지 미시적인 관점에서 살펴보겠습니다. (다음 두 그림을 참고하세요.)
여기에 이미지 설명을 삽입하세요.여기에 이미지 설명을 삽입하세요.
이것은 프로그램의 실행 결과이고, 0번째는 가장 바깥쪽의 __merge_sort가 호출되었음을 나타냅니다.
이때 가장 왼쪽이 1, 가운데가 4, 가장 오른쪽이 8이다
. 그러면 __merge_sort의 재귀 호출이 트리거되고 첫 번째 __merge_sort가 왼쪽을 담당한다.
따라서 가장 왼쪽이 1, 가운데가 2, 가장 오른쪽이 4입니다. 이때 재귀 종료 조건이 충족되지 않으므로
첫 번째 __merge_sort가 계속 호출됩니다. 이때 계속 왼쪽 부분을 담당합니다(1 2 3 4의 왼쪽 부분임을 참고하세요).
따라서 1 1 2가 있고 다음 번에 재귀가 발생할 때 왼쪽이 오른쪽과 동일하므로(왼쪽 >= 오른쪽) 다음 번에 재귀 종료 조건이 충족될 것임이 분명합니다.

다음 단락이 핵심입니다.

그럼 다음번에는 두 번째 재귀 호출이 시작됩니다! 그는 오른쪽의 두 지점을 담당합니다. 여기서 이상하게 생각하시는 분들도 계시겠지만, 오른쪽 통화에 책임이 있는 것 아닌가요? 왜 3 3 4가 인쇄되나요? 이것은 왼쪽입니다!
이것이 제가 이해하는 바입니다. 재귀는 여러 수준으로 구분됩니다. 각 재귀 수준은 계단을 내려가는 것과 같습니다. 재귀가 돌아올 때마다 한 단계 올라갑니다. 방금 종료했을 때 실제로는 두 지점에 있었습니다. 1 2 3 4 이 층의 사다리이므로 이때 전체 층에서 둘로 나누어야 할 것은 1 2 3 4의 오른쪽! 따라서 이분법은 3 3 4입니다.

이때 이 레이어의 __merge_sort도 이전 레이어로 돌아갑니다.
이때 출력되는 것은 5 6 8이고, 직접 나눗셈은 오른쪽이 5 6 7 8인데, 이는 이전 레이어 왼쪽의 1 2 3 4가 마지막 재귀에서 나누어졌기 때문입니다! (재귀의 각 수준에는 고유한 메모리가 있습니다. 실제로 각 수준의 매개변수는 저장을 위해 스택에 푸시됩니다.) 이때 우리는 재귀의 최상위 수준에 도달했으며 첫 번째 수준의 왼쪽과 오른쪽에 도달했습니다. 레벨이 나누어졌습니다.
다음 단계는 다음 단계로 재귀를 이어가는 것인데, 왼쪽의 1 2 3 4가 2개로 나누어졌으니 오른쪽도 5 6 7 8이 되고, 5 6 7 8도 56
| 78. 따라서 첫 번째 __merge_sort는 왼쪽의 56을 이등분하기 시작합니다.
그래서 이때 출력되는 것은 5 5 6이고, 마지막으로 오른쪽 두 번째 것이 7 7 8로 나누어집니다. 전체 이등분 과정은 끝났습니다.

두 개의 __merge_sort는 항상 같은 레벨에 있다는 점에 유의해야 합니다. 첫 번째 __merge_sort가 몇 계단 아래로 내려갈 때 두 번째 __merge_sort는 실제로 같은 수의 계단을 내려갑니다. (이에 대해서는 아래에서 자세히 설명하겠습니다)

병합된 부분

다음으로 두 분할 후 __merge 함수의 호출 프로세스를 살펴보겠습니다. 이는
여기에 이미지 설명을 삽입하세요.
병합 프로세스
완전히 예상한 대로입니다.
왼쪽을 표시하고 먼저 12를 병합한 다음 14를 병합하고 1234를 병합한 다음
오른쪽을 병합하고 먼저 56을 병합합니다. , 글쎄, 알았어, 78, 5678을 병합한 후 결과
는 148, 즉 12345678의 전체 병합입니다!

이제 재귀와 병합을 결합하여 시퀀스가 ​​무엇인지 살펴보겠습니다.
여기에 이미지 설명을 삽입하세요.
여기에 이미지 설명을 삽입하세요.

코드 검토

    int mid = (left + right) / 2;
    Console.WriteLine($"{
       
       flag}, left={
       
       left+1}, mid={
       
       mid+1}, right={
       
       right + 1}");
    __merge_sort(arr, left, mid);
    __merge_sort(arr, mid + 1, right);
    __merge(arr, left, mid, right, flag); } ```

우선, __merge_sort의 처음 세 번의 연속 재귀 이후 첫 번째 병합이 직접 시작되었습니다!
여기서 누군가는 다음과 같이 질문할 수 있습니다. 함수 호출 순서에 따르면 두 번째 __merge_sort는 이때 실행하면 안 되나요?
__merge 함수 로 직접 전달되나요 ? 두 번째 __merge_sort가 실행되지 않습니까?

여기서 다시 한 번 레벨의 문제를 강조합니다. 이제 마지막 레벨로 다시 돌아왔습니다. 이때 왼쪽 중간 오른쪽은
1 1 2에 해당합니다. 실제로 12는 두 부분으로 나누어집니다. 이는 2번째 __merge_sort에 해당합니다. 다음과 같이 말하세요:
__merge_sort(arr, mid + 1, right);
left = mid+1 그래서 이때 재귀 종료 조건 left >= right가 충족됩니다(사실 2개만 남았으므로 그렇게 하지 마세요). 오른쪽으로 나눌 필요는 없습니다!)
따라서 이때 두 번째 __merge_sort가 호출되지 않는 것이 아니라 바로 종료됩니다. (재귀의 종료 조건도 재귀의 가장 중요한 핵심 중 하나입니다.)
따라서 __merge가 실행되어 12개의 병합이 완료됩니다(병합 프로세스는 실제로 정렬 중입니다. 상위 __merge 코드를 참조할 수 있습니다).

이때 재귀는 바닥을 치고 이전 시간으로 돌아가기 시작했으며, 이전 레벨의 왼쪽이 재귀적으로 완료되었다(12가 2개로 나누어졌는데, 이 또한 재귀 종료 조건을 만족함). 오른쪽에서 사다리가 시작되고 34가 ​​두 개로 나누어지고( 참고: 124 주변의 모든 분할은 여기에 있습니다 ) 두 분할이 완료된 후 반환되므로
__merge를 실행하여 34의 병합을 완료합니다. 이번에는 __merge가 끝난 후 또 다른
__merge가 발생하여 1 2 4의 병합이 완료됩니다. 즉, 처음 두 개의 __merge_sort를 건너뜁니다!
왜 이런거야?

__merge가 실행된 후 재귀가 다른 수준으로 올라가기 때문인데, 이 수준에서는 실제로는 1 2 4의 두 나누기이고,
이전 재귀 과정에서는 1 2 4의 왼쪽과 오른쪽 나누기가 끝났습니다. , 그래서 합병이 즉시 시작되었습니다.

이때 남은 부분은
여기에 이미지 설명을 삽입하세요.
여기에 이미지 설명을 삽입하세요.
합병이 완료된 후 이번에는 재귀가 반환되어 재귀의 최상위 수준에 이르지만 왼쪽 부분은 실행이 완료되었으므로 오른쪽은 5 6 8의 나눗셈이다. , 나누어서 플레이한 후 두 번째 __merge_sort부터 다시 재귀에 들어갑니다(계단의 다음 단계).이 때 다음 단계의 첫 번째 __merge_sort를 만납니다 . 따라서 바닥에 도달한 5 5 6이 있으므로 돌아와서 이 수준의 두 번째 __merge_sort를 만나면 778이 됩니다. 이 시점에서 두 재귀는 모두 바닥을 치고 완료되었으며 다음 단계는 병합입니다!

여기 몇 가지 생각이 있습니다. 이것을 읽은 후에는 두 개의 재귀를 호출하는 특성을 깨달았을 것입니다. 첫 번째 재귀를 만나면 맨 아래 레이어로 재귀한 다음 다음과 같이 레이어별로 돌아갑니다. 반환 프로세스 여기에 이미지 설명을 삽입하세요.
중에 두 번째 레벨의 두 번째 __merge_sort가 호출되므로 계단을 재귀적으로 내려갈 때 첫 번째 __merge_sort가 호출되고, 계단을 올라갈 때 두 번째 재귀가 호출되며, 최상위 레벨로 올라갈 때는 두 번째 __merge_sort를 호출한 직후에 호출됩니다. , 다음 재귀 수준으로 들어가고 첫 번째 __merge_sort를 다시 만나고 다시 첫 번째 재귀 수준으로 들어갑니다! 다시 바닥을 쳤다!

시대의 문제

(재귀와 관련 없는) 또 다른 문제를 살펴보자, 배열을 10으로 확장하면,
여기에 이미지 설명을 삽입하세요.
여기에 이미지 설명을 삽입하세요.
이번에는 왼쪽을 담당하는 재귀는 5번 실행되고, 오른쪽을 담당하는 재귀는 3번만 실행된다. 이번에는 불균형인가?
이상하다고 생각하시나요?
홀수와 짝수의 문제 때문인데, 배열이 8이면 8은 2개로 나누어 4+4가 되고 최종적으로는 2+2+2+2가 된다.
단일이 될 때까지 짝수입니다. 10이면 두 포인트는 5가 됩니다. 숫자 5는 두 포인트일 때 왼쪽의 두 포인트 수가 더 많아지게 됩니다.
따라서 숫자가 8 16과 같이 2의 N승인 경우에만 배열 길이가 이 정도일 때 두 재귀에 대한 호출 횟수는 동일합니다!

재귀 요약

결국 __merge_sort가 어떻게 이등분을 달성했는지 기억할 수 있습니까?
이 과정은 매우 비밀스럽기 때문에 기억하지 못해도 문제가 되지 않지만, 재귀적 디자인의 핵심이기도 합니다.

void __merge_sort(int[] arr, int left, int right)
{
    
    
    int mid = (left + right) / 2;
    __merge_sort(arr, left, mid);
    __merge_sort(arr, mid + 1, right);
    __merge(arr, left, mid, right, flag);
}

먼저 배열을 전달하는 등의 재귀 함수를 직접 설계해야 합니다. 우리의 목적은 배열 내부 요소의 순서를 변경하는 것이지만 매번 그 일부를 고려합니다. 그래서 왼쪽과 오른쪽에 테두리가 필요합니다.
전체 배열의 경우 왼쪽은 0이고 오른쪽은 길이-1입니다.
이등분 후 왼쪽과 오른쪽은 각 이등분 후에 변경됩니다.
각 재귀 호출은 사다리를 타고 다음 단계로 들어가 왼쪽과 오른쪽이 다시 변경됩니다.
"다음 단계로의 진입"을 이해할 수 있는 것이 재귀를 이해하는 열쇠입니다. 각 재귀마다 이분법의 과정이 완성됩니다!
먼저 거시적 관점에서 아이디어를 디자인한 다음 미시적 관점에서 아이디어가 올바른지 확인할 수 있습니다.

오랫동안 이 글을 쓰고 있는데 기분이 좋네요. 여러분은 어떻게 생각하실지 모르겠습니다. 댓글란에 피드백을 환영합니다~~

첨부된 전체 Python 코드를 제공해 주세요.

원래는 이전에 테스트를 위해 Python을 사용했지만 여전히 vs를 사용하여 C#을 디버그하는 것이 편리하다는 것을 알았습니다.

def __merge(arr, left, mid, right):
    arr_copy = arr[left:right + 1][:]
    i = left
    j = mid+1
    for k in range(left, right+1):
        if i > mid:
            arr[k] = arr_copy[j-left]
            j = j + 1
        elif j > right:
            arr[k] = arr_copy[i-left]
            i = i + 1
        elif arr_copy[i-left] < arr_copy[j-left]:
            arr[k] = arr_copy[i-left]
            i = i + 1
        else:
            arr[k] = arr_copy[j-left]
            j = j + 1


def __merge_sort(arr, left, right):
    if left >= right:
        return
    mid = (left + right) // 2
    print(left, mid, right)
    __merge_sort(arr, left, mid)
    __merge_sort(arr, mid + 1, right)
    __merge(arr, left, mid, right)


def merge_sort(arr):
    __merge_sort(arr, 0, len(arr) - 1)


if __name__ == '__main__':
    arr0 = [1, 3, 5, 7, 9, 2, 4, 6, 8, 10]
    merge_sort(arr0)
    print(arr0)

Supongo que te gusta

Origin blog.csdn.net/songhuangong123/article/details/132432459
Recomendado
Clasificación