【数据结构】用Java实现七大排序算法(万字详解)

排序的概念

排序:所谓排序,就是使一串记录,按照其中的某个或某些关键字的大小,递增或递减的排列起来的操作。
稳定性:假定在待排序的记录序列中,存在多个具有相同的关键字的记录,若经过排序,这些记录的相对次序保持不变,即在原序列中,r[i]=r[j],且r[i]在r[j]之前,而在排序后的序列中,r[i]仍在r[j]之前,则称这种排序算法是稳定的;否则称为不稳定的。

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

常见的排序算法

代码实现排序算法

插入排序

直接插入排序

原理:

直接插入排序是一种简单直观的排序算法,其核心思想是将待排序的数组分为已排序和未排序两个部分,然后通过逐步将未排序部分的元素插入到已排序部分的合适位置,从而实现排序。

直接插入排序的基本步骤如下:

  1. 初始化:将数组的第一个元素视为已排序部分,其余元素为未排序部分。

  2. 遍历未排序部分

    • 从未排序部分中取出一个元素(通常称为“关键值”)。
    • 将这个关键值与已排序部分的元素进行比较,找到合适的位置。
  3. 插入关键值

    • 将已排序部分中大于关键值的元素向后移动一位,为关键值腾出位置。
    • 将关键值插入到找到的合适位置。
  4. 重复:重复步骤2和3,直到未排序部分的所有元素都被插入到已排序部分中。

时间复杂度

  • 最坏情况:O(n^2),当输入数组是反序的时候,比较和移动次数最多。
  • 最好情况:O(n),当输入数组已经是有序时,仅需进行n-1次比较而不需要移动元素。
  • 平均情况:O(n^2)。

空间复杂度:O(1),因为只需常量级的额外空间。

总结: 直接插入排序适合小规模数据的排序,且具有稳定性,简单易懂,尤其在部分有序的情况下性能优越。通过不断将未排序的元素插入到已排序的部分,逐步构建一个完整的有序序列。

代码实现:

public class InsertSort {
    public static void sort(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[j+1] = array[j];
                }else{
                    break;
                }
            }
            array[j+1] = tmp;
        }
    }
}
 希尔排序( 缩小增量排序 )

原理:

希尔排序(Shell Sort)是一种基于插入排序的更高效的排序算法,它通过将待排序数组分成若干个子序列来实现。希尔排序通过对这些子序列进行插入排序,使得整体上比直接插入排序更快,特别是在数组较大时。

希尔排序的基本原理如下:

  1. 选择增量:首先,选择一个增量(也称“步长”),通常选取某个初始值,然后缩小这个值,直至增量为1。增量的选择方式影响排序的效率,常见的选择方式包括逐步减半、使用特定的增量序列等。

  2. 分组排序:根据增量将待排序数组分成若干个子序列,例如:

    • 对于增量d,将数组中相隔d个元素分为一个组。
    • 每组内的元素使用插入排序进行排序。
  3. 重复过程:在对所有组进行排序后,缩小增量(通常为原来的某个比例),重复步骤2,直到增量为1,此时整个数组就只有一个组,再进行一次插入排序,完成排序。

  4. 完成排序:经过多次分组和排序后,整个数组将变得有序。

时间复杂度

  • 最坏情况:O(n^(3/2))到O(n^2),具体取决于增量序列的选择。
  • 平均情况:O(n^(5/4))到O(n log^2 n)。
  • 最好情况:O(n)。

空间复杂度:O(1),因为只需要常量级的额外空间。

总结: 希尔排序通过分组的方式对元素进行排序,逐步减少增量,使数组有序。其优点是适用于较大数据集,并且比简单的插入排序快得多,特别是在数组部分有序的情况下,能够更有效地提升排序效率。

 

代码实现:

public class ShellSort {
    public static void sort(int[] array){
        int gap = array.length;
        while(gap != 1){
            gap /= 2;
            shell(array,gap);
        }
        shell(array,gap);
    }
    private static void shell(int[] array,int gap){
        for (int i = gap; i < array.length; i++) {
            int tmp = array[i];
            int j = i - gap;
            for (; j >= 0; j -= gap) {
                if(tmp < array[j]){
                    array[j+gap] = array[j];
                }else{
                    break;
                }
            }
            array[j+gap] = tmp;
        }
    }
}

选择排序:

直接选择排序

原理:

直接选择排序(Selection Sort)是一种简单直观的排序算法,其基本思想是每一次从待排序的数列中选出最小(或最大)的元素,将其放到已排序序列的末尾。具体原理如下:

直接选择排序的基本步骤:

  1. 初始化:将整个数组视为未排序部分。

  2. 遍历未排序部分

    • 在未排序部分中找到最小(或最大)元素的下标。
  3. 交换元素

    • 将找到的最小元素与未排序部分的第一个元素交换位置,从而将该最小元素移至已排序部分的末尾。
  4. 重复

    • 缩小未排序部分的范围,继续从未排序部分中选择最小元素,并进行交换,直到整个数组都被排序完成。

示例:

假设待排序数组为 [64, 25, 12, 22, 11],过程如下:

  1. 第一轮:找到最小元素11,并与第一个元素64交换,得到 [11, 25, 12, 22, 64]
  2. 第二轮:在剩下的未排序部分 [25, 12, 22, 64] 中找到最小元素12,并将其与25交换,得到 [11, 12, 25, 22, 64]
  3. 第三轮:找到最小元素22,将其与25交换,得到 [11, 12, 22, 25, 64]
  4. 第四轮:在最后的未排序部分中,25和64已经是有序的,因此最终结果为 [11, 12, 22, 25, 64]

时间复杂度:

  • 最坏情况:O(n^2)
  • 最好情况:O(n^2)
  • 平均情况:O(n^2)

空间复杂度:

  • O(1),因为直接选择排序是原地排序算法,只需常量级的额外空间。

总结:

直接选择排序是一种简单且易于理解的排序算法,但它的效率较低,特别是在大数据集时,通常不适用于实际应用中。它适合小规模数据的排序,且在元素比较时的稳定性较好。

代码实现: 

public class SelectSort {

    public static void sort(int[] array){
        for (int i = 0; i < array.length - 1; i++) {
            int minIndex = i;
            for (int j = i+1; j < array.length; j++) {
                if(array[j] < array[minIndex]){
                    minIndex = j;
                }
            }
            int tmp = array[i];
            array[i] = array[minIndex];
            array[minIndex] = tmp;
        }
    }

}

对于该代码我们可以进行优化,我们使用 left 指针指向最左边,用 right 指针指向最右边,如何在数组中找最小值和最大值分别与 left 和 right 交换,这样可以提高效率。

代码实现:

public class SelectSort {

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

    public static void sort(int[] array){
        int left = 0;
        int right = array.length - 1;
        while(left < right){
            int minValue = left;
            int maxValue = left;
            for (int i = left+1; i <= right; i++) {
                if(array[i] < array[minValue]){
                    minValue = i;
                }
                if(array[i] > array[maxValue]){
                    maxValue = i;
                }
            }
            swap(array,left,minValue);
            if(maxValue == left){
                maxValue = minValue;
            }
            swap(array,right,maxValue);
            left++;
            right--;
        }
    }
}
堆排序

原理:

堆排序(Heap Sort)是一种基于堆数据结构的排序算法,利用堆的性质来实现高效排序。堆是一种完全二叉树,具有以下特点:对于每个节点,值总是大于或等于(最大堆)或小于或等于(最小堆)其子节点的值。

堆排序的基本原理:

  1. 构建最大堆:首先,将待排序的数组构建成一个最大堆(或最小堆,具体取决于排序顺序)。在最大堆中,父节点的值大于等于子节点的值。

  2. 交换与调整

    • 将堆顶元素(最大元素)与堆的最后一个元素交换,这样最大元素就移到了数组的末尾。
    • 然后减少堆的大小(即排除已排序的最大元素),并对堆顶元素进行“下沉”操作,调整堆的结构,恢复最大堆的性质。
  3. 重复步骤:重复步骤2,直到堆的大小减小到1,此时所有元素都已排序。

示例:

假设待排序数组为 [3, 5, 1, 10, 2],过程如下:

  1. 构建最大堆

    • 首先将数组调整为最大堆: [10, 5, 1, 3, 2]
  2. 堆排序过程

    • 交换堆顶和最后一个元素,得到 [2, 5, 1, 3, 10]。减小堆的大小,调整堆,变为 [5, 3, 1, 2, 10]
    • 继续交换堆顶和最后未排序元素,得到 [2, 3, 1, 5, 10],再调整,变为 [3, 2, 1, 5, 10]
    • 重复这个过程,最终得到有序数组 [1, 2, 3, 5, 10]

时间复杂度:

  • 最坏情况:O(n log n)
  • 最好情况:O(n log n)
  • 平均情况:O(n log n)

空间复杂度:

  • O(1),堆排序是原地排序算法,仅需常量级的额外空间。

总结:

堆排序是一种高效的排序算法,适用于大规模数据的排序。它的时间复杂度稳定且较优,且是原地排序,不需要额外的存储空间。但由于堆排序不是稳定排序,因此在需要保持相同元素相对顺序的情况下可能不适用。

代码实现:

public class HeapSort {

    private static void createHeap(int[] array){
        for (int parent = (array.length-1-1)/2; parent >= 0; parent--) {
            shiftDown(array,parent,array.length);
        }
    }

    private static void shiftDown(int[] array,int parent, int usedSize) {
        int child = parent*2+1;
        while(child < usedSize) {
            if (child+1 < usedSize && array[child + 1] > array[child]) {
                child = child + 1;
            }
            if (array[parent] < array[child]) {
                int tmp = array[child];
                array[child] = array[parent];
                array[parent] = tmp;
                parent = child;
                child = parent*2+1;
            }else{
                break;
            }
        }
    }
    public static void sort(int[] array) {
        createHeap(array);
        int end = array.length - 1;
        while(end > 0){
            int tmp = array[0];
            array[0] = array[end];
            array[end] = tmp;
            shiftDown(array,0,end);
            end--;
        }
    }
}

交换排序

冒泡排序

原理:

冒泡排序(Bubble Sort)是一种简单的基于比较的排序算法,其核心思想是通过重复遍历数组,比较相邻元素并交换它们的位置,以将最大的元素“冒泡”到数组的末尾。具体原理如下:

冒泡排序的基本步骤:

  1. 初始化:将待排序数组视为未排序部分。

  2. 遍历数组

    • 从数组的开始元素开始,依次比较相邻的两个元素。
    • 如果前一个元素大于后一个元素,则交换它们的位置。
    • 每一轮遍历将确定一个最大元素(或最小元素)的位置,把它放到已排序部分的末尾。
  3. 重复过程:对整个数组重复上述步骤,每次遍历后,最大的元素会“冒泡”到未排序部分的末尾。随着遍历次数的增加,未排序部分的元素会逐渐减少。

  4. 终止条件:当一次遍历中没有发生交换时,表示数组已经有序,可以提前终止排序过程。

示例:

假设待排序数组为 [5, 3, 8, 4, 2],过程如下:

  1. 第一轮遍历

    • 比较5和3,交换得到 [3, 5, 8, 4, 2]
    • 比较5和8,不交换
    • 比较8和4,交换得到 [3, 5, 4, 8, 2]
    • 比较8和2,交换得到 [3, 5, 4, 2, 8]
    • 第一轮结束,最大元素8已在正确位置。
  2. 第二轮遍历

    • 比较3和5,不交换
    • 比较5和4,交换得到 [3, 4, 5, 2, 8]
    • 比较5和2,交换得到 [3, 4, 2, 5, 8]
    • 第二轮结束,5已在正确位置。
  3. 第三轮遍历

    • 比较3和4,不交换
    • 比较4和2,交换得到 [3, 2, 4, 5, 8]
    • 第三轮结束,4已在正确位置。
  4. 第四轮遍历

    • 比较3和2,交换得到 [2, 3, 4, 5, 8]
    • 第四轮结束,2已在正确位置。

最终得到的有序数组为 [2, 3, 4, 5, 8]

时间复杂度:

  • 最坏情况:O(n^2),当数组是反序时,比较和交换次数最多。
  • 最好情况:O(n),当数组已经有序时,仅需进行一次遍历。
  • 平均情况:O(n^2)。

空间复杂度:

  • O(1),因为冒泡排序是原地排序算法,仅需常量级的额外空间。

总结:

冒泡排序是一种基本的排序算法,简单易懂,适合于小规模数据的排序。由于其较低的效率,在实际应用中通常不推荐使用,特别是对于大规模数据。尽管如此,冒泡排序有助于理解排序算法的基本概念和实现原理。

代码实现:

public class BubbleSort {
    public static void Sort(int[] array){
        for (int i = 0; i < array.length-1; i++) {
            boolean flag = false;
            for (int j = 0; j < array.length-1-i; j++) {
                if(array[j+1] < array[j]){
                    int tmp = array[j];
                    array[j] = array[j+1];
                    array[j+1] = tmp;
                    flag = true;
                }
            }
            if(flag == false){
                return;
            }
        }
    }
}
快速排序

原理:

快速排序(Quick Sort)是一种高效的排序算法,它采用分治法(Divide and Conquer)策略来排序。快速排序的核心思想是通过一个称为“基准”(Pivot)的元素,将数组分成左右两部分,将小于基准的元素放在左侧,大于基准的元素放在右侧,随后递归地对这两个部分进行排序。以下是快速排序的基本原理:

快速排序的基本步骤:

  1. 选择基准元素:从数组中选择一个元素作为基准(常见的方法包括选择第一个元素、最后一个元素、随机选择或者三数取中等)。

  2. 分区操作

    • 重新排列数组,使得所有小于基准的元素放在基准的左侧,所有大于基准的元素放在基准的右侧。
    • 经过这一轮分区,基准元素就处于其最终位置。
  3. 递归排序

    • 对基准左侧和右侧的两个子数组递归执行快速排序,直到子数组的长度为1或0,此时数组已经有序。

示例:

假设待排序数组为 [10, 7, 8, 9, 1, 5],过程如下:

  1. 选择基准

    • 选取最后一个元素5作为基准。
  2. 分区操作

    • 通过遍历数组,将小于5的元素分到左边,得到 [1, 5, 8, 9, 7, 10],此时基准元素5在正确的位置。
  3. 递归排序

    • 对子数组 [1](左侧)和 [8, 9, 7, 10](右侧)进行递归。
    • 子数组 [1] 已经有序,递归结束。
    • 对子数组 [8, 9, 7, 10]继续进行快速排序,选择基准元素10进行分区,得到 [8, 7, 9, 10],然后为子数组 [8, 7, 9]递归排序。
  4. 继续递归

    • 对 [8, 7, 9]选择基准元素9进行分区,得到 [7, 8, 9]。此时这个子数组也是有序。
  5. 汇总结果

    • 最终组合得到: [1, 5, 7, 8, 9, 10]

时间复杂度:

  • 最坏情况:O(n^2),发生在每次选择的基准元素都是当前分区中的最大或最小元素(如已经有序或逆序时)。
  • 最好情况:O(n log n),每次分割都能将数组平均分成两部分。
  • 平均情况:O(n log n)。

空间复杂度:

  • O(log n)至O(n),具体取决于递归的深度。通常情况下,由于递归是使用栈来实现的,因此需要额外的空间。

总结:

快速排序是一种高效且广泛使用的排序算法,适合大规模数据的排序。在实际应用中,快速排序通常比其他O(n log n)复杂度的排序算法(如归并排序)更快,因为它可以充分利用缓存和数据局部性原则。尽管快速排序不稳定,但其高效性使其在多种排序任务中依然被广泛应用。

代码实现:

public class QuickSort {
    
    //在递归比较底的时候,改为直接插入排序来降低递归次数
    public static void insertSortRange(int[] array,int start,int end){
        for (int i = start+1; i <= end; i++) {
            int tmp = array[i];
            int j = i - 1;
            for (; j >= start; j--) {
                if(tmp < array[j]){
                    array[j+1] = array[j];
                }else{
                    break;
                }
            }
            array[j+1] = tmp;
        }
    }

    private static void swap(int[] array,int j,int k){
        int tmp = array[j];
        array[j] =  array[k];
        array[k] = tmp;
    }
    public static void sort(int[] array){
        quick(array,0,array.length-1);
    }

    //递归法
    private static void quick(int[] array,int start,int end){
        if(start >= end) return;
        if(end-start+1 <= 15) {
            insertSortRange(array,start,end);
            return;
        }
        int mid = midOfThree(array,start,end);
        swap(array,mid,start);
        int pivot = partition(array,start,end);
        quick(array,start,pivot-1);
        quick(array,pivot+1,end);
    }

    
    //三数取中,优化方案,降低递归次数
    private static int midOfThree(int[] array, int start, int end) {
        int mid = (start+end)/2;
        if(array[start] < array[end]) {
            if(array[mid] < array[start]){
                return start;
            }else if(array[mid] > array[end]) {
                return end;
            }else {
                return mid;
            }
        }else {
            if(array[mid] > array[start]) {
                return start;
            }else if(array[mid] < array[end]) {
                return end;
            }else {
                return mid;
            }
        }
    }


    //填坑法  找标记
    private static int  partition(int[] array,int left,int right){
        int key = array[left];
        while(left < right){
            while(left < right && array[right] >= key){
                right--;
            }
            array[left] = array[right];
            while(left < right && array[left] <= key){
                left++;
            }
            array[right] = array[left];
        }
        array[left] = key;
        return left;
    }
}

归并排序

原理:

归并排序(Merge Sort)是一种基于分治法(Divide and Conquer)的高效排序算法,其基本思想是将待排序的数组分为两个子数组,分别对这两个子数组进行排序,最后将排序后的子数组合并成一个有序的数组。归并排序具有稳定性和良好的时间复杂度表现,适合处理大数据集。以下是归并排序的基本原理:

归并排序的基本步骤:

  1. 分割过程

    • 将待排序的数组从中间分成两个子数组,递归地对这两个子数组进行归并排序,直到每个子数组的长度为1或0,此时这些子数组自然是有序的。
  2. 合并过程

    • 将两个已排序的子数组合并成一个新的有序数组。合并时,通过比较两个子数组中的元素,将较小的元素依次添加到合并后的数组中。
  3. 递归调用

    • 重复上述过程,直到整个数组合并成一个有序的数组。

示例:

假设待排序数组为 [38, 27, 43, 3, 9, 82, 10],过程如下:

  1. 分割过程

    • 将数组分为 [38, 27, 43] 和 [3, 9, 82, 10]
    • 继续分割,第一个子数组分为 [38] 和 [27, 43],第二个子数组分为 [3, 9] 和 [82, 10]
    • 然后继续对 [27, 43] 和 [82, 10] 分割,直到所有子数组长度为1。
  2. 合并过程

    • 开始合并:将单个元素的子数组合并成有序数组。
    • 合并 [27] 和 [43] 得到 [27, 43]
    • 合并 [3] 和 [9] 得到 [3, 9]
    • 合并 [82] 和 [10] 得到 [10, 82]
    • 接着合并 [38] 和 [27, 43] 得到 [27, 38, 43]
    • 最后合并 [3, 9] 和 [10, 82] 得到 [3, 9, 10, 82]
    • 最终合并 [27, 38, 43] 和 [3, 9, 10, 82] 得到完整的有序数组 [3, 9, 10, 27, 38, 43, 82]

时间复杂度:

  • 最坏情况:O(n log n)
  • 最好情况:O(n log n)
  • 平均情况:O(n log n)

空间复杂度:

  • O(n),由于合并过程需要一个额外的数组来存放合并的结果。

总结:

归并排序是一种稳定的排序算法,时间复杂度较好,适用于大规模数据的排序。尽管归并排序的空间复杂度较高,但它的稳定性和效率,使其在许多应用中仍然非常流行。在实际应用中,归并排序常常用于外部排序和链表排序等场景。

代码实现: 

public class MergeSort {

    
    //递归思路
    public static void sort(int[] array) {
        int left = 0;
        int right = array.length-1;
         recursion(array,left,right);
    }

    private static void recursion(int[] array, int left, int right) {
        int mid = (left+right) / 2;
        if(left >= right) return;
        recursion(array,left,mid);
        recursion(array,mid+1,right);
        merge(array,left,right,mid);
    }

    //归并操作
    private static void merge(int[] array, int left, int right, int mid) {
        int tmp = left;
        int s1 = left;
        int s2 = mid+1;
        int[] tmpArr = new int[right-left+1];
        int k = 0;

        //证明两个区间都有元素
        while (s1 <= mid && s2 <= right) {
            if(array[s2] <= array[s1]) {
                tmpArr[k++] = array[s2++];
            }else {
                tmpArr[k++] = array[s1++];
            }
        }
        while (s1 <= mid) {
            tmpArr[k++] = array[s1++];
        }
        while (s2 <= right) {
            tmpArr[k++] = array[s2++];
        }
        //走到这里这个区间的数组已经有序
        for (int i = 0; i < tmpArr.length; i++) {
            array[tmp++] = tmpArr[i];
        }
    }
}

排序算法复杂度及稳定性总结

 

海量数据的排序问题


外部排序:排序过程需要在磁盘等外部存储进行的排序。
前提:内存只有 1G,需要排序的数据有 100G
因为内存中因为无法把所有数据全部放下,所以需要外部排序,而归并排序是最常用的外部排序
1. 先把文件切分成 200 份,每个 512 M
2. 分别对 512 M 排序,因为内存已经可以放的下,所以任意排序方式都可以
3. 进行 2路归并,同时对 200 份有序文件做归并过程,最终结果就有序了 

  • 11
    点赞
  • 16
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值