데이터 구조 - 정렬

1. 삽입 정렬

1.1 직접 삽입 정렬 - 원리:

        전체 구간은 1. 정렬된 구간, 2. 정렬되지 않은 구간, 정렬되지 않은 구간의 첫 번째 요소가 선택될 때마다 정렬된 구간에 삽입할 적절한 위치가 선택됩니다.

        

1.2 구현:

public class InsertSort {

    public static void sort(int[] array){
        // 一共要取多少个元素来进行插入过程(无序区间里有多少个元素)
        for (int i = 0; i < array.length - 1; i++) {
            // 有序区间 [0, i]  至少在 i == 0 的时候得有一个元素
            // 无序区间 [i + 1, n)

            // 先取出无序区间的第一个元素,记为 k
            int k = array[i + 1];

            // 从后往前,遍历有序区间[0,i]
            // 找到属于无序第一个元素,即k的位置。
            int j = i;
            for (; j >= 0 && k < array[j]; j--) {
                array[j + 1] = array[j];        // 将不符合条件的数据往后般一格
            }

            array[j + 1] = k;
        }
    }
}

1.3 성능 분석: 데이터를 안정적으로 유지할 수 있습니다. 삽입 정렬은 초기 데이터가 순서에 가까울수록 시간 효율성이 높아집니다.

1.4 반접기 삽입 정렬(이해)

정렬된 간격으로 데이터를 삽입해야 하는 위치를 선택할 때 간격의 질서성 때문에 절반 검색이라는 아이디어를 사용할 수 있습니다.

    public static void bsInsertSort(int[] array) {

        for (int i = 1; i < array.length; i++) {
            
            int v = array[i];
            int left = 0;
            int right = i;
            // [left, right)
            // 需要考虑稳定性
            while (left < right) {

                int m = (left + right) / 2;
                if (v >= array[m]) {
                    left = m + 1;
                } else {
                    right = m; 
                }
            }
            // 搬移
            for (int j = i; j > left; j--) {
                array[j] = array[j - 1];
            }
            array[left] = v;
        }
    }

2. 힐 정렬

2.1 원리: [알고리즘 아이디어] 먼저, 정렬할 레코드의 시퀀스를 여러 "희소한" 하위 시퀀스로 나누고 각각 직접 삽입 정렬을 수행합니다. 위의 대략적인 조정을 거쳐 기본적으로 전체 시퀀스의 레코드가 순서대로 정렬되며 마지막으로 모든 레코드가 직접 삽입 및 정렬됩니다.
        ① 먼저 레코드 사이의 거리를 di(i=1)로 선택하고 정렬할 레코드의 전체 순서에서 간격이 d1인 모든 레코드를 그룹화하고 그룹을 직접 삽입하여 정렬한다.
        ②그런 다음 i=i+1을 취하면 레코드 사이의 거리는 di( di < d(i-1) )이고 정렬할 레코드의 전체 시퀀스에서 di 간격의 모든 레코드를 그룹으로 그룹화하여 직접 삽입 그룹 정렬에.
        레코드 사이의 거리가 di=1이 될 때까지 ②의 과정을 여러 번 반복하는데 이때 부분수열은 하나만 존재하며, 순차를 직접 삽입하여 정렬하면 전체 정렬 과정이 완료된다.

2.2 시행

public class ShellSort {
    public static void sort(int[] a){
        //1.根据a的长度确定增长量h
        int h = 1;
        while (h < a.length/2){
            h = 2*h+1;
        }
        //2.希尔排序
        while ( h>=1 ){
           //排序:找到待插入的元素,
            for (int i = h; i < a.length; i++) {
                for (int j = i; j >= h ; j-=h) {
                    if (a[j-h]>a[j]){
                        //交换元素
                        swap(a,j-h,j);
                    }else {//j-h 比 j 小,不用交换。
                        break;
                    }
                }
            }
            h=h/2;
        }
    }
    private static void swap(int[] a, int i, int j){
        int emp;
        emp = a[i];
        a[i] = a[j];
        a[j] = emp;
    }
}

2.3 안정성: 불안정

3. 선택 정렬

3.1 직접 선택 정렬 - 원리

[알고리즘 아이디어]
첫 번째 단순 선택 정렬에서는 첫 번째 레코드부터 시작하여 n-1개의 키워드 비교를 통해 n개의 레코드 중 키워드가 가장 작은 레코드를 선택하고 첫 번째 레코드와 교환합니다.
두 번째 단순 선택 정렬에서는 두 번째 레코드부터 n-2개의 키워드 비교를 거쳐 n-1개의 레코드 중 키워드가 가장 작은 레코드를 선택하여 두 번째 레코드와 교환합니다.
i번째 단순 선택 정렬에서는 i번째 레코드부터 ni개의 키워드 비교를 거쳐 n-i+1개의 레코드 중에서 가장 작은 키워드를 가진 레코드를 선택하여 i번째 레코드와 교환한다.
이와 같이 n-1회의 단순 선택 정렬 후 n-1개의 레코드가 제자리에 놓이고 가장 작은 레코드가 바로 끝에 남겨지기 때문에 총 n-1회의 단순 선택 정렬이 필요합니다.

3.2 시행

public class SelectSort {

 public static void sort(int[] array){

     for (int i = 0; i < array.length-1; i++) {
         int k = i;
         for (int j = i+1; j < array.length; j++) {
             if (array[j] < array[k]){
                 k = j;
             }
         }
         swap(array,k,i);
     }
 }

    private static void swap(int[] a, int i, int j){
        int emp;
        emp = a[i];
        a[i] = a[j];
        a[j] = emp;
    }
}

3.3 안정성: 불안정

3.4 양방향 선택 정렬(이해)

매회 무순 구간에서 가장 작은 + 가장 큰 원소를 선택하여 정렬할 데이터 원소가 모두 소진될 때까지 정렬되지 않은 구간의 앞과 끝에 저장한다.

public static void selectSort(int[] array) {
    
    for (int i = 0; i < array.length - 1; i++) {

        // 无序区间: [0, array.length - i)
        // 有序区间: [array.length - i, array.length)
        int max = 0;
        for (int j = 1; j < array.length - i; j++) {
        
            if (array[j] > array[max]) {
            max = j;
        }
    }

    int t = array[max];
    array[max] = array[array.length - i - 1];
    array[array.length - i - 1] = t;

    }
}

4. 힙 정렬

4.1 원리 기본 원리도 선택 정렬이지만 무질서한 구간에서 가장 큰 수를 찾기 위해 순회를 사용하는 대신 무질서한 구간에서 가장 큰 수를 선택하기 위해 힙을 사용합니다.

[알고리즘 아이디어]
① heap 정의(알고리즘 9.9)에 따라 정렬할 레코드의 초기 heap을 빌드하고, heap의 최상위 요소를 출력한다.
② 남은 레코드 순서를 조정하고, 스크리닝 방법을 사용하여 처음 ni 요소를 새 힙으로 다시 스크리닝한 다음 힙의 맨 위 요소를 출력합니다.
③ ②단계를 반복하여 n-1번 필터링하면 새로 필터링된 힙이 점점 작아지고
새 힙 뒤에 있는 정렬된 키워드 가 점점 더 많아지며 결국 정렬할 레코드의 시퀀스가 ​​정렬된 시퀀스가 ​​됩니다. , 이 프로세스를 힙 정렬이라고 합니다.
그리는 과정이 다소 번거롭고 아이디어와 코드에 따라 직접 그리고 이해하시면 됩니다.

4.2 시행

public class HeapSort {
    
    public static void sort(int[] array){
        //建初堆:升序建大堆,降序建小堆。
        for (int i = (array.length-2)/2; i >=0 ; i--) {
            shiftDown(array,array.length,i);
        }
        //维护堆:堆顶元素与最后一个元素交换后,堆顶的“堆性质”被破环,需要维护。此时维护的堆大小应该是依次减小的。
        for (int i = 0; i < array.length-1; i++) {
            swap(array,0,array.length-i-1);
            shiftDown(array,array.length-i-1,0);
        }
    }

    private static void shiftDown(int[] array, int length, int index) {

        while (index*2+1 < length){
            int left = index*2+1;
            int right = left+1;
            int max = left;

            if (right < length && array[left] < array[right]){
                max = right;
            }

            if (array[index] >= array[max]){
                return;
            }
            swap(array,index,max);
            index = max;
        }
    }

    private static void swap(int[] a, int i, int j){
        int emp;
        emp = a[i];
        a[i] = a[j];
        a[j] = emp;
    }
}

4.3 안정성: 불안정

5. 버블 정렬

5.1 원리: 무순구간에서는 인접한 수의 비교를 통해 무순구간이 끝날 때까지 가장 큰 수를 버블링하고, 이 과정은 배열이 전체적으로 정렬될 때까지 계속된다.

5.2 구현:

public class BubbleSort{

    public void sort(long[] array) {
        for (int i = 0; i < array.length - 1; i++) {
            boolean sorted = true;
            for (int j = 0; j < array.length - i - 1; j++) {
                if (array[j] > array[j + 1]) {
                    SortUtil.swap(array, j, j + 1);
                    sorted = false;
                }
            }
            if (sorted) {
                return;
            }
        }
    }
}

5.3 안정성: 안정적

6. 빠른 정렬(중요)

6.1 원칙 - 개요

        1. 범위에서 피벗 값으로 정렬할 숫자를 선택합니다.

        2. 파티션: 정렬할 전체 구간을 순회하여 기준 값보다 작은 값(같음 포함)을 기준 값 왼쪽에 놓고 기준 값보다 큰 값(같음 포함)을 참조 값의 하단

오른쪽;

        3. 분할 정복 아이디어를 사용하여 왼쪽 및 오른쪽 셀은 동일한 방식으로 처리됩니다. 셀의 길이 == 1(순서가 있음을 의미함) 또는 셀의 길이 == 0(즉, 데이터가 없습니다.

6.2 원리 - 분할: 빠른 정렬의 본질은 분할 연산입니다.이 연산을 수행하는 방법은 여러 가지가 있습니다.일반적인 포인트는 피벗에 따라 데이터를 나누는 것입니다.

6.3 안정성: 불안정

6.4 원칙 - 기준값 선택

        1. 가장자리 선택(왼쪽 또는 오른쪽)

        2. 무작위 선택

        3. 몇 개의 숫자의 중간을 취한다(예를 들어, 세 숫자의 중간을 취한다): array[left], array[mid], array[right] 중간의 크기는 기준값이다.

6.5 코드 구현:



public class QuickSort {
    public static void sort(int[] array) {
        
        quickSortRange(array,0,array.length-1);
    }

    // 为了代码书写方便,我们选择使用左闭右闭的区间表示形式
    // from,to 下标的元素都算在区间的元素中
    // 左闭右闭的情况下,区间内的元素个数 = to - from + 1;
    private static void quickSortRange(int[] array, int from, int to) {

        if (to - from +1 <= 1) {
            // 区间中元素个数 <= 1 个
            return;
        }

        // 挑选中区间最右边的元素 array[to],
        //int pi = partitionMethodA(array, from, to);
        //经过该步处理后数组array中的数据呈现: [from,pi)的元素是小于 pivot ;(pi,array.length-1]元素是大于 pivot ;
        //pivot == array[pi];
        // 按照分治算法的思路,使用相同的方式,处理相同性质的问题,只是问题的规模在变小
        int[] index = partitionD(array,from,to);
        int left = index[0];
        int right = index[1];
        quickSortRange(array, from, left);    // 针对小于等于 pivot 的区间做处理
        quickSortRange(array, right, to);   // 针对大于等于 pivot 的区间做处理
    }

    /**
     * 以区间最右边的元素 array[to] 最为 pivot,遍历整个区间,从 from 到 to,移动必要的元素
     * 进行分区
     * @param array
     * @param from
     * @param to
     * @return 最终 pivot 所在的下标
     */

    /*
        <= pivot: [from,left];
        > pivot : [right,to];
        未比较 :   (left,right);
     */
    private static int partitionA(int[] array, int from, int to) {
        int left = from;
        int right = to;
        int pivot = array[to];
        while (left < right){
            while (left < right && array[left] <= pivot){
                left++;
            }
            while(left < right && array[right] >= pivot){
                right--;
            }
            swap(array,left,right);
        }
        swap(array,left,to);
        return left;

    }

    /*
        <= pivot: [from,left];
        > pivot : [right,to];
        未比较 :   (left,right);

     */
    public static int partitionB(int[] array, int from, int to){
        int pivot = array[to];
        int left = from;
        int right = to;

        while(left < right){
            while(left < right && array[left] < pivot){
                left++;
            }
            array[right] = array[left];
            while(left < right && array[right] > pivot){
                right--;
            }
            array[left] = array[right];
        }
        array[left] = pivot;
        return left;
    }

    /**
     * 对 array 的 [from, to] 区间进行分区
     * 分区完成之后,区间被分割为 [<= pivot] pivot [>= pivot]
     * 分区过程中,始终保持
     * [from, s)    小于 pivot
     * [s, i)       大于等于 pivot
     * [i, to)      未比较过的元素
     * [to, to]     pivot
     * @param array
     * @param from
     * @param to
     * @return pivot 最终所在下标
     */
    public static int partitionC(int[] array,int from,int to){

        int s = from;
        int pivot = array[to];
        for (int i = from; i < to; i++) {   // 遍历 [from, to)
            // 这里加 == 号也保证不了稳定性,有交换操作
            if (array[i] < pivot) {
                // TODO: 可以进行简单的优化:如果 i == s,就不交换
                swap(array,i,s);
                s++;
            }
        }

        array[to] = array[s];
        array[s] = pivot;

        return s;
    }

    public static int[] partitionD(int[] array,int from,int to){

        int s = from;
        int i = from;
        int g = to;

        int pivot = array[to];
        while (g-i+1 > 0){
            if (array[i] == pivot){
                i++;
            }else if (array[i] < pivot){
                swap(array,s,i);
                s++;i++;
            }else {
                swap(array,g,i);
                g--;
            }
        }
        return new int[] {s-1,i};
    }

    public static int partitionE(int[]array,int left,int right){
        int d = left + 1;
        int pivot = array[left];

        for (int i = left+1; i <=right ; i++) {
            if(array[i] < pivot) {
                swap(array,i,d);
                d++;
            }
        }
        swap(array,d,left);
        return d;
    }


    private static void swap(int[] a, int i, int j){
        int emp;
        emp = a[i];
        a[i] = a[j];
        a[j] = emp;
    }
}

6.7 최적화 요약

        1. 벤치마크 값을 선택하는 것이 매우 중요합니다. 일반적으로 몇 개의 숫자를 사용하여 중간 방법을 사용합니다.

        2. 분할 과정에서 기준 값과 동일한 숫자도 선택됩니다.

        3. 정렬할 범위가 임계값보다 작을 때 직접 삽입 정렬을 사용합니다.

6.8 요약

        1. 정렬할 범위에서 벤치마크 값을 선택합니다.

                1. 왼쪽 또는 오른쪽 선택

                2. 무작위 선정

                3. 소수의 중국어와 프랑스어

        2. 작은 숫자가 왼쪽에, 큰 숫자가 오른쪽에 오도록 파티션을 만드세요.

                1. 헐

                2. 구덩이를 파다

                3. 전진 및 후진

                4. 기준값이 같은 것도 선택(이해)

        3. 왼쪽과 오른쪽 셀을 나누어 셀 수가 임계값 미만이 될 때까지 정복하고 삽입 정렬을 사용합니다.

7. 병합 정렬(중요)

7.1 원리 - 개요: 병합 정렬(MERGE-SORT)은 분할 및 정복 방법의 매우 일반적인 응용 프로그램인 병합 작업을 기반으로 하는 효율적인 정렬 알고리즘입니다. 순서가 지정된 하위 시퀀스를 병합하여 완전히 정렬된 시퀀스를 얻습니다. 즉, 먼저 각 하위 시퀀스를 정렬한 다음 하위 시퀀스 세그먼트를 정렬합니다. 두 개의 정렬된 목록을 하나의 정렬된 목록으로 병합하는 것을 양방향 병합이라고 합니다.

7.2 원리 - 두 개의 정렬된 배열 병합

7.3 구현:

public class MergeSort {
    private static int[] assist;
    
    public static void sort(int[] array) {
        assist = new int[array.length];
        int lo = 0, hi = array.length - 1;
        sort(array, lo, hi);
    }

    private static void sort(int[] array, int lo, int hi) {
        if (hi <= lo) return;
        int mid = lo + (hi - lo) / 2;
        sort(array, lo, mid);
        sort(array, mid + 1, hi);//以上两步均为分组,
        merge(array, lo, mid, hi);//将array中从 lo 到 hi 的元素合并为有序数组。
    }

    private static void merge(int[] array, int lo, int mid, int hi) {
        int i = lo, p1 = lo, p2 = mid + 1;//三个指针
        while (p1 <= mid && p2 <= hi) {
            if (array[p1] < array[p2]) {
                assist[i++] = array[p1++];
            } else {
                assist[i++] = array[p2++];
            }
        }
        while (p1 <= mid) {
            assist[i++] = array[p1++];
        }
        while (p2 <= hi) {
            assist[i++] = array[p2++];
        }
        System.arraycopy(assist, lo, array, lo, hi - lo + 1);
    }

    private static void exchange(int[] a, int i, int j) {
        int emp;
        emp = a[i];
        a[i] = a[j];
        a[j] = emp;
    }
}

7.4 안정성: 안정적

7.5 최적화 요약

        정렬 과정에서 두 배열을 재사용하여 요소의 복사 과정을 줄입니다.

8. 외부 정렬

         위에서 설명한 다양한 정렬 방식에서 정렬할 레코드 및 관련 정보는 메모리에 저장되고 전체 정렬 프로세스는 메모리에서 모두 완료되며 데이터의 내부 및 외부 메모리 교환이 포함되지 않습니다
. 내부 정렬이라고 합니다. 정렬할 레코드의 수가 너무 많아 한 번에 메모리를 이전할 수 없는 경우 외부 메모리를 사용하여 일괄적으로 사람을 전송하여 전체 정렬 프로세스를 완료해야 하며, 이러한 정렬을 외부 정렬 이라고 합니다
. 이 섹션에서는 직접 액세스 장치(디스크 저장소)와 순차 액세스 장치(테이프 저장소)를 기반으로 하는 외부 정렬 방식의 기본 개념을 주로 소개합니다.

8.1 외부 분류의 기본 방법

        가장 일반적으로 사용되는 외부 정렬 방법은 병합 정렬입니다. 이 방법은 두 단계로 구성됩니다. 첫 번째 단계에서는 정렬할 레코드를 메모리에 일괄적으로 읽고, 파일을 세그먼트별로 콘텐츠 세그먼트에 입력하고, 파일의 각 세그먼트를 효과적인 내부 정렬
방법 정렬된 파일 세그먼트를 시퀀스(또는 병합 세그먼트)라고 하며, 생성될 때 외부 메모리에 하위 파일 형태로 기록되므로 외부 메모리에 많은 초기
시퀀스가 ​​형성됩니다. 하위 파일의 다중 병합 특정 병합 방법(예: 양방향 병합 방법)은 다중 병합을 수행하므로 시퀀스의 길이가 시퀀스가 ​​될 때까지 작은 것에서 큰 것으로 점차 증가합니다. 즉, 전체 파일이 주문됩니다. 외부 정렬은 테이프, 디스크와 같은 외부 메모리를 사용할 수 있습니다. 초기에 생성되는 직렬 파일의 길이는
메모리가 제공하는 정렬 영역의 크기와 초기 정렬 전략에 따라 다릅니다. 병합 경로의 수는 파일의 개수에 따라 다릅니다. 제공할 수 있는 외부 장치.

9. 요약 정렬:

9.1은 일반적으로 다음과 같은 종류로 나뉩니다.

9.2 알고리즘의 평균 시간 복잡도, 최악의 시간 복잡도 및 알고리즘에 필요한 보조 저장 공간의 세 가지 측면에서 다양한 정렬 방법을 비교합니다.

정렬 방법 평균 시간 복잡도 최악의 금 복잡성 보조 저장 공간
간단한 정렬 방법 오(n2) 오(n2)         오(1)
빠른 정렬       O(n 로그 2n) 오(n2) O(log2n)
힙 정렬 O(n 로그 2n) O(n 로그 2n) 오(1)
병합 정렬 O(n 로그 2n) O(n 로그 2n) 에)

9.3 다양한 분류 방법을 종합적으로 분석하고 비교하여 다음과 같은 결론을 얻을 수 있다.
        ① 단순 분류 방법은 일반적으로 n이 작은 경우(예: n<30)에만 사용된다. 시퀀스의 레코드가 "기본적으로 정렬된" 경우 직접 삽입 정렬이 가장 좋은 정렬 방법입니다. 레코드의 데이터가 크면 이동 수가 적은 단순 선택 정렬 방법을 사용해야 합니다.
        ② 퀵정렬, 힙정렬, 병합정렬의 평균 시간복잡도는 모두 O(nlog_n)이지만 실험 결과 평균 시간 성능 면에서 퀵정렬이 모든 정렬 방법 중 가장 우수함을 알 수 있다. 불행히도 퀵정렬의 최악의 시간 성능은 O(㎡)입니다. 힙정렬과 병합정렬의 최악의 시간복잡도는 여전히 O(nlogan)이며, n이 크면 병합정렬이 힙정렬보다 시간성능은 좋지만 가장 많은 보조공간을 필요로 한다.
        ③간단한 소팅 방식과 더 나은 성능을 가진 소팅 방식을 조합하여 사용할 수 있습니다. 예를 들어, 퀵소트에서 분할된 부분간격의 길이가 일정 값보다 작을 때 직접 삽입 정렬 방식을 대신 호출하거나 정렬할 시퀀스를 여러 개의 하위 시퀀스로 나누어 직접 삽입 정렬을 수행할 수 있다. 순서가 지정된 하위 시퀀스를 완전한 순서의 시퀀스로 병합하는 병합 정렬 방법이 사용됩니다.
        ④기수 정렬의 시간 복잡도는 O(dn)로 쓸 수 있습니다. 따라서 n의 값이 크고 키 d의 자릿수가 적은 시퀀스에 가장 적합합니다. d가 n보다 훨씬 작으면 시간복잡도는 O(n)에
        가깝다. 그러나 시간 성능이 더 좋은 정렬 방법 중 힐 정렬, 퀵 정렬, 힙 정렬은 모두 불안정하고 병합 정렬과 기수 정렬만 안정적입니다.
        요약하자면, 각 정렬 방법에는 고유한 특성이 있으며 어떤 방법도 절대적으로 최적일 수는 없습니다. 특정 상황에 따라 적절한 분류 방법을 선택하거나 여러 방법을 조합하여 사용할 수 있습니다.

추천

출처blog.csdn.net/weixin_52575498/article/details/124063085