【排序算法总结(1)】

排序算法总结(1)

文章目录

排序算法的定义

排序算法(Sorting Algorithm)是一种将一组数据元素按照特定顺序重新排列的算法。排序的目标是使数据从无序状态变为有序状态,以便于后续的查找、统计或其他处理。

核心要素:

输入:一个包含 n 个元素的序列(通常为数组或列表)。

输出:输入序列的一个排列(即相同元素的重新排列),满足指定的顺序规则。

顺序规则:通常为非递减(升序)或非递增(降序)顺序。对于复杂数据类型,排序基于一个或多个关键字(如对象的某个属性)。

示例:

text 复制代码
 输入: [5, 2, 9, 1, 5]
升序输出: [1, 2, 5, 5, 9]

排序算法稳定性的定义

排序算法的稳定性(Stability)是指:如果待排序序列中存在关键字相同的元素,在排序完成后,这些相同关键字元素的相对顺序是否保持不变。

稳定性的判断:

稳定排序:相同关键字的元素在排序后保持它们原始的相对顺序。

不稳定排序:相同关键字的元素在排序后可能改变原始的相对顺序。

输入数据如下:

text 复制代码
输入: [(90, "Alice"), (85, "Bob"), (90, "Charlie")]

稳定排序的结果:

text 复制代码
[(85, "Bob"), (90, "Alice"), (90, "Charlie")]

不稳定排序的结果:

text 复制代码
[(85, "Bob"), (90, "Charlie"), (90, "Alice")]

内部排序:排序元素都在内存中的排序
外部排序:数据元素太多不能放在内存之中,根据排序过程中要求不能在内外存之间移动数据。

插入排序

插入排序的算法思想如下:

将数组分为三部分:已排序元素,当前元素,待排序元素。从后往前遍历已经排好序的元素,拿当前元素和已经排好序的元素进行比较,如果待排序元素小于已经排序的元素,则让已经排序的元素往后挪动一个位置。直到待排序元素小于当前元素时停止遍历,然后让当前元素插入当前遍历到的待排序元素的后一个位置即可。代码实现如下:

java 复制代码
 //插入排序
    /**
     * 时间复杂度:O(N^2) 倒序
     * 空间复杂度:O(1)
     * 稳定性:稳定
     * @param array
     */
    public static void insertSort(int[] array) {
        //从第二个元素开始处理
        for (int i = 1; i < array.length; i++) {
            //保存当前待排序的元素
             int tmp = array[i];
             int j = i - 1;
             //遍历之前已经排好序的元素
             for(;j >= 0;j--) {
                 if(tmp < array[j]) {
                     //将比array[i]大的元素往后移
                     array[j + 1]  = array[j];
                 }else {
                     //找到了插入位置,结束循环
                     break;
                 }
             }
            //当循环走出来的时候,说明array[j]的元素小于tmp,二array[j+1]的元素大于等于tmp,因此应该将tmp插在array[j+1]的位置
             array[j+1] = tmp;
        }
    }

测试代码如下:

java 复制代码
//生成随机的数字
    public static void randomOrder(int[] array) {
        Random random = new Random();
        for (int i = 0; i < array.length; i++) {
            array[i] = random.nextInt(1_0000);
        }
    }

    public static void testInsertSort(int[] array) {
        array = Arrays.copyOf(array,array.length);
        long startTime = System.currentTimeMillis();
        //System.out.println(startTime);
        Sort.insertSort(array);
        long endTime = System.currentTimeMillis();
        //System.out.println(endTime);
        System.out.println("插入排序耗时:"+(endTime - startTime));
    }
    

测试结果如下:

时间复杂度分析:

  1. 最好情况(Best Case):数组已经有序
    比较次数:每次只需比较1次(与前一个元素比较)

第1个元素:1次比较

第2个元素:1次比较

...

第(n-1)个元素:1次比较

总比较次数:(n-1) 次

移动次数:0次(不需要移动元素)

时间复杂度:O(n)

  1. 最坏情况(Worst Case):数组完全逆序
    比较次数:

第1个元素:1次比较

第2个元素:2次比较

第3个元素:3次比较

...

第(n-1)个元素:(n-1)次比较

总比较次数:1+2+3+...+(n-1) = n(n-1)/2

移动次数:

每次比较都需要移动元素(将arr[j]向后移动)

总移动次数同样为:n(n-1)/2

时间复杂度:O(n²)

  1. 平均情况(Average Case):随机排列的数组
    对于第i个元素,平均需要比较 i/2 次

总比较次数 ≈ n(n-1)/4

时间复杂度:O(n²)

空间复杂度分析:

空间复杂度:O(1)

只需要常数级别的额外空间(用于存储key和索引变量)

是一种原地排序(in-place sorting)算法

稳定性:

插入排序是一种稳定的排序算法,当遇到相等元素时,插入排序会将新元素放在相等元素的后面,保持原始相对顺序。

希尔排序

在分析插入排序的时间复杂度的时候,我们发现插入排序对于有序的数组性能更好,那么我们有没有什么办法将一个数组变得接近有序之后然后再进行直接插入排序呢?有的,兄弟。包有的,这么强的算法还有9个。接下来我们介绍希尔排序。

希尔排序的基本思想是:先选定一个整数,把待排序文件中所有记录分成多个组,所有距离为的记录分在同一组内,并对每一组内的记录进行排序。然后重复上述分组和排序的工作。当到达分组间隔=1时,所有记录在统一组内排好序(即最后一趟的写入排序就是直接插入排序)。

用通俗易懂的话来解释就是:对数组中的元素进行分组,在每一个分组的内部使用直接插入排序让分组内部的数据有序。最后对整个数组使用直接插入排序的时候就会有相对较高的性能。

下面来看代码实现:

java 复制代码
private static void Shell(int[] array, int gap) {
        //记住直接插入排序是增量为1的希尔排序
        for (int i = gap; i < array.length; i++) {
            int tmp = array[i];
            int j = i-gap;
            for (; j >= 0; j-=gap ){
                if(array[j] > tmp) {
                    array[j+gap] = array[j];
                }else {
                    break;
                }
            }
            array[j+gap] = tmp;
        }
    }

    //希尔排序
    //先对数组中的元素进行分组,每个分组内的元素进行直接插入排序。设置一个间隔gap,初始时gap等于数组长度的一半,每次gap都减半
    public static void ShellSort(int[] array) {
        int gap = array.length / 2;
        while(gap >= 1) {
            Shell(array,gap);
            gap /= 2;
        }
    }
   public static void testShellSort(int[] array) {
        array = Arrays.copyOf(array,array.length);
        long startTime = System.currentTimeMillis();
        Sort.ShellSort(array);
        long endTime = System.currentTimeMillis();
        System.out.println("希尔排序耗时:"+(endTime - startTime));
    }

希尔排序的时间复杂度证明涉及到数学上无法解释的难题:我们只需要记住希尔排序的时间复杂度为O(n^1.6)。并且希尔排序是不稳定的排序算法就可以了~

简单选择排序

简单选择排序的算法思想是:将数组划分为两个区域,一个区域是已经排好序的元素,另一个是待排序的元素,如下图所示:

在待排序的元素中选择一个最小的元素,与待排序元素的第一个对换。当遍历完整个数组之后,整个数组有序。接下来来看算法实现:

java 复制代码
public static void selectSort(int[] array) {
        //处理空数组
        if(array == null || array.length == 0) {
            return;
        }
        //外层循环代表排序趟数
        for (int i = 0; i < array.length; i++) {
            int index = i;
            int min = array[i];
            //在后 n - i 个元素之中寻找一个最小的元素并记录下标
            for (int j = i; j < array.length; j++) {
                if(array[j] < min) {
                    min = array[j];
                    index = j;
                }
            }
            //交换i所指元素和最小的元素
            swap(array,i,index);
        }
    }

    public static void testSelectSort(int[] array) {
        array = Arrays.copyOf(array,array.length);
        long startTime = System.currentTimeMillis();
        Sort.selectSort(array);
        long endTime = System.currentTimeMillis();
        System.out.println("选择排序耗时:"+(endTime - startTime));
    }
   

时间复杂度分析:

平均时间复杂度:O(n²)

各种情况下的时间复杂度

最好情况(数组已排序):

比较次数:n(n-1)/2(必须比较,无法提前结束)

交换次数:0 次(最小元素就在当前位置,不需要交换)

时间复杂度:O(n²)

最坏情况(数组逆序):

比较次数:n(n-1)/2

交换次数:n-1 次(每趟都需要交换)

时间复杂度:O(n²)

平均情况(随机数组):

比较次数:n(n-1)/2

交换次数:约(n-1)/2 次(概率上,最小元素不在当前位置的概率为(n-1)/n)

时间复杂度:O(n²)

堆排序

一、堆排序核心思想

堆排序(Heap Sort)是一种基于二叉堆数据结构的比较排序算法。它利用堆的性质,通过将待排序数组构造成一个大顶堆(或小顶堆),然后逐步将堆顶元素与末尾元素交换并调整堆,实现排序。

二、基本概念

  1. 二叉堆(Binary Heap)
    完全二叉树(Complete Binary Tree)

满足堆性质:

大顶堆:每个节点的值都大于或等于其子节点的值

小顶堆:每个节点的值都小于或等于其子节点的值

  1. 数组表示
    对于节点i(索引从0开始):

父节点:(i-1)/2(向下取整)

左子节点:2*i + 1

右子节点:2*i + 2

三、堆排序算法过程(升序排序,使用大顶堆)

第一阶段:构建大顶堆

将无序数组构建成一个大顶堆。

步骤:

找到最后一个非叶子节点:

最后一个非叶子节点索引:n/2 - 1(n为数组长度)

从该节点开始,向前逐个调整每个节点

调整堆(Heapify):

对每个节点,确保其值大于等于子节点值

如果不满足,则与较大的子节点交换

递归调整被交换的子节点所在子树

第二阶段:排序

逐步将堆顶元素(最大值)移动到数组末尾。

步骤:

交换:将堆顶元素(arr[0])与当前堆的最后一个元素交换

堆大小减1:将堆的有效大小减1(排除已排序的最大值)

调整堆:从新的堆顶开始向下调整,恢复大顶堆性质

重复:重复上述步骤,直到堆大小为1

堆排序的代码如下:

首先写出构建将一个数组区间构建为大根堆的算法:

java 复制代码
/**
     * 将一个数组向下调整为大根堆
     * @param array 待处理数组
     * @param parent 根节点
     * @param usedSize 数组有效数据的长度
     */
    public static void siftDown(int[] array,int parent,int usedSize) {
        //向下调整为大根堆
        //根据公式计算出左孩子的下标
        int child = 2 * parent + 1;
        
        while (child < usedSize) {
            //判断parent是否有右孩子,选取左孩子和右孩子之间的较大者
            if(child + 1 < usedSize && array[child] < array[child + 1]) {
                child++;
            }
            //比较array[child]和array[parent]的大小
            //向下持续调整为大根堆
            if(array[child] > array[parent]) {
                swap(array,parent,child);
                parent = child;
                child = 2 * child + 1;
            }else {
                break;
            }
        }
    }

然后将待排序的数组构建为大根堆:

java 复制代码
/**
     *
     * @param array 将一个创建为大根堆
     */
    public static void createHeap(int[] array) {
        int usedSize = array.length;
        //遍历调整为大根堆

        for(int i = (usedSize - 1 - 1)/2;i >= 0;i--) {
           //从最后一个parent结点开始将数组调整为大根堆
            siftDown(array,i,usedSize);
        }

    }

最后进行堆排序:

java 复制代码
/**
     * 堆排序
     * 时间复杂度:O(n*log n)
     * 空间复杂度:O(1)
     * 稳定性:不稳定
     * @param array 待排序数组
     */
    public static void heapSort(int[] array) {
        //把array数组改造成为大根堆
        int usedSize = array.length;
        createHeap(array);

        //交换堆顶元素和堆底元素
        for (int i = 0;i < array.length;i++) {
            swap(array,0,usedSize - 1);
            usedSize--;
            //将剩下的元素向下调整成为大根堆
            siftDown(array,0,usedSize);
        }

    }

时间复杂度和空间复杂度分析:

时间复杂度:建堆的时间复杂度为:O(n),整个堆排序的时间复杂度为O(n log n)。堆排序的时间复杂度为 O(n log n),且在最坏、最好和平均情况下都保持这一复杂度。其空间复杂度为 O(1)(原地排序),但如果使用递归实现调整堆函数,递归调用栈的空间复杂度为 O(log n)。

堆排序的主要优势在于其时间复杂度的稳定性和空间效率,但缺点是常数因子较大且不稳定。在实际应用中,堆排序常被用于需要保证最坏情况性能的场景或作为优先级队列的基础。

快速排序

快速排序的基本思路如下:

  1. 选定一个枢轴元素,以这个枢轴元素为基准划分数组,划分之后的数组满足如下条件:小于枢轴元素的数组元素全部在枢轴元素左端,大于枢轴元素的数组元素全部在枢轴元素的右端。而我们选定的枢轴元素一定在其"最终位置"(即在后续的排序过程中这个枢轴元素的位置不会再发生变化)。
  2. 以相同的方式处理枢轴元素左端的数组元素。
  3. 以相同方式处理枢轴元素右端的数组元素。

快速排序算法的整体框架如下:

java 复制代码
/**
     * 快速排序
     * @param array
     * 时间复杂度:平均: O(n*log n)  (最坏:有序或者逆序) O(n^2)
     * 空间复杂度:平均: O (log n)    (最坏:有序或者是逆序) O(n^2)
     * 稳定性:不稳定
     */
     //快速排序父接口
     public static void quickSort(int[] array) {
          quickSortImpl(array,0,array.length - 1);
    }
    
    //真正的快速排序实现代码
    /**
     * 
     * @param array 待排序的数组
     * @param start 待排序区间的起点
     * @param end   待排序区间的终点
     */
     private static void quickSortImpl(int[] array,int start,int end) {
        //递归出口
        if(start >= end) {
            return;
        }
         int partition = Partition1(array,start,end);
         quickSortImpl(array,start,partition - 1);
         quickSortImpl(array,partition + 1,end);
    }

quickSort为公开入口方法:

作用:为外部调用提供简洁接口

参数:整个待排序数组

调用:启动递归排序过程,初始范围为整个数组

quickSortImpl为快速排序的递归实现:

递归终止条件:start >= end(子数组长度为0或1时结束)

递归逻辑:

执行划分操作,确定基准元素最终位置

递归排序基准元素左边的子数组

递归排序基准元素右边的子数组

快速排序的核心算法是如何实现对数组的划分,下面实现两种划分算法,首先是挖坑法:

java 复制代码
//挖坑法实现快速排序划分
    public static int Partition1(int[] array,int start,int end) {
        //选取最开始的元素,作为枢轴元素,并将枢轴元素保存下来
        int pivot = array[start];
        
        int left = start;
        int right = end;

        while(left < right) {

            //在排序区间的右端寻找小于枢轴元素的数组元素
            while(left < right && array[right] > pivot) {
                right--;
            }

            //将排序区间右端小于枢轴元素的数组元素,移动到在排序区间的左端挖好的"坑"中
            array[left] = array[right];

            //在排序区间的左端寻找大于枢轴元素的数组元素
            while(left < right &&array[left] <= pivot) {
                left++;
            }

            //将排序区间左端小于枢轴元素的数组元素,移动到排序区间的右端挖好的"坑"中
            array[right] = array[left];
        }

        //当左右指针相遇的时候,将枢轴元素填入该位置
        array[left] = pivot;

        //返回枢轴元素最后在数组中的下标
        return left;
    }

挖坑法的基本思想是:

选择基准元素,并"挖出"一个"坑"

从数组两端交替扫描,将不符合条件的元素填入"坑"中

当左右指针相遇时,将基准元素填入最后的"坑",并且将基准元素的下标返回。

然后是Hoare法,这种划分方法更好地体现了快速排序是交换类排序的思想。

Hoare法划分的算法思想:

  • 首先选取排序区间的起始元素作为基准元素
  • 定义一个left指针从数组待排序区间的左端开始从左向右遍历,直到找到一个元素大于基准元素,定义一个right指针从数组待排序区间的右端开始从右向左遍历,直到找到一个元素小于基准元素。然后交换此时left和right所指的元素,一直重复此过程
  • 直到left和right相遇,当left和right相遇之后,交换left和基准元素。
java 复制代码
//霍尔法实现快速排序划分
    /**
     *
     * @param array 待排序数组
     * @param start 待排序区间的起始下标
     * @param end   待排序区间的结束下标
     * @return      返回的是基准元素在数组中的最终位置
     */
    private static int Partition(int[] array,int start,int end) {
        int left = start;
        int right = end;

        //寻找一个分割元素
        int div = array[left];

        while(left < right) {

            while(array[right] > div && left < right) {
                right--;
            }

            while(array[left] <= div && left < right) {
                left++;
            }

            swap(array,left,right);
        }

        //循环结束之后,交换arrary[left]和枢轴元素的值
        int tmp = array[left];
        array[left] = div;
        array[start] = tmp;
        return left;
    }

快速排序的缺陷:当数据接近有序的时候,划分数组将会变得不均匀。此时快速排序将退化为冒泡排序,时间复杂度为O(n^2)。

可以对快速排序进行优化,比如;优化枢轴元素的选取策略,在划分数组的时候尽量让数组划分均匀。对长度较小的区间采用插入排序的策略。

以下是三数取中法的Java代码实现

java 复制代码
    //三数取中法选取枢轴元素
    /**
     *
     * @param array 待排序数组
     * @param left  待排序区间的起始下标
     * @param right 待排序区间的终止下标
     * @return      选取的枢轴元素的下标
     */
    private static int median(int[] array,int left,int right) {
        //取中间索引的元素
        int mid = (left + right)/2;

        //在array[left]、array[right]、array[mid]中寻找中位数
        if(array[left] == array[right]) {
            return left;
        }

        //三数取中
        if(array[left] > array[right]) {
            if(array[mid] < array[right]) {
                return right;
            } else if (array[mid] > array[left]) {
                return left;
            } else {
                return mid;
            }
        }else {
            if(array[mid] < array[left]) {
                return left;
            } else if (array[mid] > array[right]) {
                return right;
            } else {
                return mid;
            }
        }

    }

下图是三数取中法的示意图:

选取数组中更加接近中位数的元素作为枢轴元素,可以将数组划分的更加均匀。而且对与长度较小的区间,可以使用插入排序:

java 复制代码
//对指定区间的插入排序
    /**
     *
     * @param array 待排序数组
     * @param left  待排序区间开始下标
     * @param right 待排序区间结束下标
     */
    public static void insertSort_(int[] array,int left,int right) {
        //从left + 1位置开始处理
        for(int i = left+1;i <= right;i++) {

            int tmp = array[i];
            int j = i - 1;
            for(;j >= left;j--) {
                if(tmp < array[j]) {
                    //将比array[i]大的元素往后移
                    array[j + 1]  = array[j];
                }else {
                    //找到了插入位置,结束循环
                    break;
                }
            }

            //将array[i]插入指定的位置,思考:为什么是在j+1位置插入而不是j位置插入元素?
            //当循环走出来的时候,说明array[j]的元素小于tmp,二array[j+1]的元素大于等于tmp,因此应该将tmp插在array[j+1]的位置?
            array[j+1] = tmp;
        }
    }

理由:虽然插入排序的平均时间复杂度是O(n^2),但是插入排序在已经接近有序的数据上的时间复杂度接近O(n)。

以下是快速排序优化的Java代码实现

java 复制代码
 //快速排序优化:
    /**
     * 思路:对于较小的区间,使用对指定区间的插入排序
     * 划分元素的时候:采用三数取中法 选取枢轴元素
     *
     * @param array 排序数组
     * @param left  开始区间
     * @param right 结束区间
     */
    public static void quick(int[] array,int left,int right) {
        //判断区间是否合法,同时也是递归出口
        if(left > right) {
            return;
        }

        //判断区间大小
        //如果区间大小小于15则使用插入排序
        if(right - left + 1 <= 15) {
            insertSort_(array,left,right);
            return;
        }

        //三数取中法,选取一个中位数作为枢轴元素
        int mid = median(array,left,right);
        //将中位数和区间起点元素进行对换
        swap(array,mid,left);
        //以中位数为枢轴元素,对数组指定区间进行划分
        int partition = Partition(array,left,right);

        quick(array,left,partition - 1);
        quick(array,partition + 1,right);
    }

算法思路:

  • 首先设置递归出口,如果排序的开始区间的下标大于排序的结束区间的下标 。
  • 判断待排序区间的长度,如果待排序区间的长度小于15,则采用插入排序的策略,对该区间进行排序。
  • 采用三数取中法选取枢轴元素,选取完枢轴元素之后和待排序区间最开始的元素对换。
  • 采用Hoare法对数组进行划分,并返回枢轴元素在数组中的最终位置
  • 对划分之后的数组的两个区间[left,partition - 1],[partition + 1,right]进行排序
快速排序的非递归写法

以下是快速排序非递归的Java实现

java 复制代码
//挖坑法实现快速排序划分
    public static int Partition1(int[] array,int start,int end){
        //选取最开始的元素,作为枢轴元素,并将枢轴元素保存下来
        int pivot = array[start];

        int left = start;
        int right = end;

        while(left < right) {

            //在排序区间的右端寻找小于枢轴元素的数组元素
            while(left < right && array[right] > pivot) {
                right--;
            }

            //将排序区间右端小于枢轴元素的数组元素,移动到在排序区间的左端挖好的"坑"中
            array[left] = array[right];

            //在排序区间的左端寻找大于枢轴元素的数组元素
            while(left < right &&array[left] <= pivot) {
                left++;
            }

            //将排序区间左端小于枢轴元素的数组元素,移动到排序区间的右端挖好的"坑"中
            array[right] = array[left];
        }

        //当左右指针相遇的时候,将枢轴元素填入该位置
        array[left] = pivot;

        //返回枢轴元素最后在数组中的下标
        return left;
    }


//快速排序非递归算法
    public static void quickSortNor(int[] array) {
        quickSortNorImpl(array,0,array.length - 1);
    }

    //快速排序非递归算法,实现
    public static void quickSortNorImpl(int[] array,int left,int right) {
          //判断区间是否合法
        if(left > right) {
            return;
        }

        //利用挖坑法,首先对数组进行一个划分
        int mid = Partition1(array,left,right);

        //初始化一个栈
        Stack<Integer> stack = new Stack<>();

        //判断划分之后的子区间长度,如果大于等于2的话则将区间两端的下标压入栈
        if(left < mid - 1) {
            // 先压入mid - 1,再压入left
            // 因为排序的区间是[left,mid - 1]
            // 栈遵循先进后出的原则
            stack.push(mid - 1);
            stack.push(left);
        }

        if(right > mid + 1) {
            // 先压入right,再压入mid + 1
            // 因为排序的区间是[mid + 1,right]
            // 栈遵循先进后出的原则
            stack.push(right);
            stack.push(mid + 1);
        }

        //当栈不为空的时候
        while(!stack.isEmpty()) {

            //获取子区间两端的下标
            int start = stack.pop().intValue();
            int end = stack.pop().intValue();
            //对子区间做划分操作
            int partition = Partition1(array,start,end);

            //判断划分之后的子区间长度,如果大于2,则将子区间两端的下标压入栈
            if(start < partition - 1) {
                stack.push(partition - 1);
                stack.push(start);
            }

            //判断划分之后的子区间长度,如果大于2,则将子区间两端的下标压入栈
            if(end > partition + 1) {
                stack.push(end);
                stack.push(partition + 1);
            }
        }

        //当循环结束的时候,数组指定区间已经有序
    }

快速排序的非递归实现相对于递归实现有几个显著优点:
1.避免栈溢出风险

  • 递归版本:使用系统调用栈,深度受JVM栈大小限制(通常几MB)

  • 非递归版本:使用堆内存中的数据结构(Stack对象),受堆内存限制(通常几百MB到GB级)

示例:排序100万个已排序的元素

  • 递归版本:递归深度达到100万,几乎肯定栈溢出

  • 非递归版本:可以正常处理(虽然效率可能不高)

2. 可以手动控制栈深度

3. 更容易实现迭代深度优先或广度优先

4. 内存使用更可控

5. 避免尾递归优化问题

快速排序总结:

  1. 快速排序整体的综合性能和使用场景都是比较好的,所以才敢叫快速排序
  2. 时间复杂度:O(N*logN)
  3. 空间复杂度:O(logN)
  4. 稳定性:不稳定

归并排序

归并排序(MERGE-SORT)是建立在归并操作上的一种有效的排序算法,该算法是采用分治法(Divide and Conquer)的一个非常典型的应用。将已有序的子序列合并,得到完全有序的序列;即先使每个子序列有序,再使子序列段间有序。若将两个有序表合并成一个有序表,称为二路归并。

归并排序的过程如下:

以下是归并排序的Java代码实现:

java 复制代码
public static void mergeSort(int[] array) {
        mergeSortImpl(array,0,array.length - 1);
    }

    //真正实现
    public static void mergeSortImpl(int[] array,int start,int end) {
        //递归出口
        if(start >= end) {
            return;
        }
        //找到中间索引,断开
        int mid = (start + end)/2;

        mergeSortImpl(array,start,mid);
        mergeSortImpl(array,mid + 1,end);

        //归并
        merge(array,start,end,mid);
    }

    private static void merge(int[] array, int start, int end,int mid) {
           int l1 = start;
           int r1 = mid;
           int l2 = mid+1;
           int r2 = end;

           //创建一个辅助数组
           int[] tmpArray = new int[end - start + 1];

           //辅助指针
           int k = 0;
           //进行归并
           while(l1 <= r1 && l2 <= r2) {
               //从两个归并段之中选取较小的元素加入原数组
               if(array[l1] <= array[l2]) {
                   tmpArray[k++] = array[l1++];
               }

               if(array[l1] > array[l2]) {
                   tmpArray[k++] = array[l2++];
               }
           }
           //代码走到这里,如果归并段中还有元素剩余,那么剩余的元素一定大于临时数组的最后一个元素

           //处理归并段中有剩余元素的情况
           while(l1 <= r1) {
               tmpArray[k++] = array[l1++];
           }

           while(l2 <= r2) {
               tmpArray[k++] = array[l2++];
           }

           //代码走到这里,临时数组中已经是将两个归并段有序合并之后的序列
           //将临时数组中的元素,写回到待排序的数组中
        for (int i = 0; i < tmpArray.length; i++) {
             array[start + i] = tmpArray[i];
        }
    }

归并排序总结:

  1. 归并的缺点在于需要O(N)的空间复杂度,归并排序的思考更多的是解决在磁盘中的外排序问题。
  2. 时间复杂度:O(N*logN)
  3. 空间复杂度:O(N)
  4. 稳定性:稳定

海量数据的排序问题​

外部排序:排序过程需要在磁盘等外部存储进行的排序

前提:内存只有 1G,需要排序的数据有 100G​

因为内存中因为无法把所有数据全部放下,所以需要外部排序,而归并排序是最常用的外部排序1. 先把文件切分成 200 份,每个 512 M​

  1. 分别对 512 M 排序,因为内存已经可以放的下,所以任意排序方式都可以​

  2. 进行 2路归并,同时对 200 份有序文件做归并过程,最终结果就有序了​

相关推荐
小宋10212 小时前
Kafka 自动发送消息 Demo 实战:从配置到发送的完整流程(java)
java·分布式·kafka
Remember_9932 小时前
【数据结构】Java数据结构深度解析:栈(Stack)与队列(Queue)完全指南
java·开发语言·数据结构·算法·spring·leetcode·maven
鱼很腾apoc2 小时前
【实战篇】 第13期 算法竞赛_数据结构超详解(上)
c语言·开发语言·数据结构·学习·算法·青少年编程
期待のcode2 小时前
JVM 中对象进入老年代的时机
java·开发语言·jvm
派大鑫wink2 小时前
【Day37】MVC 设计模式:原理与手动实现简易 MVC 框架
java·设计模式·mvc
毕设源码-赖学姐2 小时前
【开题答辩全过程】以 基于java的医院床位管理系统的设计与开发 为例,包含答辩的问题和答案
java·开发语言
啊阿狸不会拉杆2 小时前
《数字图像处理》第 12 章 - 目标识别
图像处理·人工智能·算法·计算机视觉·数字图像处理
曹轲恒2 小时前
SpringBoot的热部署
java·spring boot·后端
Remember_9932 小时前
深入理解 Java String 类:从基础原理到高级应用
java·开发语言·spring·spring cloud·eclipse·tomcat