数据结构——排序的超级详解(Java版)

排序

一、排序的概念及引用

1、排序的概念

排序 :所谓排序,就是使一串记录,按照其中的某个或某些关键字的大小,递增或递减的排列起来的操作。(默认都是从小到大排的)

稳定性:假定在待排序的记录序列中,存在多个具有相同的关键字的记录,若经过排序,这些记录的相对次序保持不变,即在原序列中,ri=rj,且ri在rj之前,而在排序后的序列中,ri仍在rj之前,则称这种排序算法是稳 定的;否则称为不稳定的。

内部排序:数据元素全部放在内存中的排序。

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

2.排序的应用

RAM:8G (运行内存)内部排序

磁盘:1T 外部排序

二、常见排序算法的实现

1 插入排序

1.1直接插入排序

直接插入排序是一种简单的插入排序法,其基本思想是:
把待排序的记录按其关键码值的大小逐个插入到一个已经排好序的有序序列中 ,直到所有的记录插入完为止,得到 一个新的有序序列 。实际中我们玩扑克牌时,就用了插入排序的思想。

java 复制代码
**   /**
     * 插入排序
     * 时间复杂度
     * 最坏的时间复杂度(n^2)
     * 最好的时间复杂度(n)
     * 得出结论:越有序,越快
     * 空间复杂度:n(1)
     * 稳定性:
     * @param array
     */
    public static void insertSort(int[] array) {
        for (int i = 1; i < array.length; i++) {
            int j = i - 1;
            int temp = array[i]; // 暂存当前要插入的元素
            // 从已排序区间的末尾向前遍历,寻找插入位置
            for (; j >= 0; j--) {
                //执行第一次如果是的话覆盖array[i],array[j]往后移动1,空出来一个
                if (array[j] > temp) {
                    //这里有先后顺序,因为是要比你大的时候才会往后移动,是稳定的
                    // 若已排序元素大于temp,将其向后移动一位
                    array[j + 1] = array[j];
                } else {
                    // 找到插入位置,退出循环
                    break;
                }
            }
            // 此时array[j]<temp,将temp插入到正确位置(j+1)
            array[j + 1] = temp;
        }
    }

在游览器找的动态图我觉得非常形象,大家看一看

该图是游览器借鉴这个博主的,在此声明https://blog.csdn.net/wenjiahui123/article/details/127660742

1.2希尔排序(缩小增量排序)

希尔排序法又称缩小增量法。希尔排序法的基本思想是:**先选定一个整数,把待排序文件中所有记录分成多个组, 所有距离为的记录分在同一组内,并对每一组内的记录进行排序。然后,去重复上述分组和排序的工作。**当到达gap =1时,所有记录在统一组内排好序。

希尔排序(Shell Sort)是插入排序的优化版本,通过引入 "间隔(gap)" 概念,先将数组分成多个子数组进行局部排序,再逐步缩小间隔直至为 1,最终完成全局排序。其核心思想是让数组先接近有序,再用插入排序收尾,可以大幅提升效率。

按着上面图的分法:首先这样分5组,然后就是排序

排序的结果是:这样排序的话,就是小的部分基本就在前半部分,更加有序了

java 复制代码
 /**
     * 时间复杂度:O(n^1.3   -  n^1.5)
     * 空间复杂度:O(1)
     * 不稳定的排序
     * @param array
     */
    public static void shellSort(int[] array) {
        int gap = array.length;
        while (gap > 1) {
            gap /= 2;
            shell(array,gap);
        }
        
    }
/*public static void shellSort(int[] array) {
    int gap = array.length;
    while (gap > 1) {
        gap = gap / 2 + 1; // 保证gap最终能减到1
        shell(array, gap);
    }
    shell(array, 1); // 最后一次gap=1的插入排序
}
*/
    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(array[j] > tmp) {
                    array[j+gap] = array[j];
                }else {
                    break;
                }
            }
            array[j+gap] = tmp;
        }
    }

另一种分法看下图:这里就是在分组和合并组的过程中(预排序)趋向于有序,n的奇数虽然逐渐变大,但是它逐渐趋向与有序,运行的速度也会得到提升

代码如下:

java 复制代码
public class ShellSortGroup {
    public static void shellSortWithGap2(int[] array) {
        if (array == null || array.length <= 1) {
            return;
        }
        int n = array.length;
        int gap = 2; // 固定间隔为2,实现[0,1]、[2,3]...分组
        
        // 按gap=2分组并排序
        for (int i = gap; i < n; i++) {
            // 这里i从gap开始,但为了严格按[0,1]、[2,3]...分组,调整遍历方式
            // 每组内执行插入排序(组内元素下标为i和i-1,间隔1,但gap=2控制组间间隔)
            int temp = array[i];
            int j = i - 1; // 组内相邻元素(因为每组内只有两个元素,间隔1)
            
            // 组内排序(对两个元素比较交换)
            while (j >= i - gap && j >= 0 && array[j] > temp) {
                array[j + 1] = array[j];
                j--;
            }
            array[j + 1] = temp;
        }
        
        // 最后用gap=1完成全局排序(标准插入排序)
        for (int i = 1; i < n; i++) {
            int temp = array[i];
            int j = i - 1;
            while (j >= 0 && array[j] > temp) {
                array[j + 1] = array[j];
                j--;
            }
            array[j + 1] = temp;
        }
    }

    public static void main(String[] args) {
        int[] arr = {3, 1, 4, 2, 7, 5, 8, 6}; // 初始数组
        System.out.println("排序前:");
        for (int num : arr) {
            System.out.print(num + " "); // 输出:3 1 4 2 7 5 8 6
        }

        shellSortWithGap2(arr);

        System.out.println("\n排序后:");
        for (int num : arr) {
            System.out.print(num + " "); // 输出:1 2 3 4 5 6 7 8
        }
    }
}

希尔排序特性总结

(1)希尔排序是对直接插入排序的优化。

(2)当gap > 1时都是预排序,目的是让数组更接近于有序。当gap == 1时,数组已经接近有序的了,这样就会很快。这样整体而言,可以达到优化的效果。我们实现后可以进行性能测试的对比。

(3)希尔排序的时间复杂度不好计算,因为gap的取值方法很多,导致很难去计算,因此在好些树中给出的希尔排序的时间复杂度都不固定:

希尔排序时间复杂度的计算

因此,希尔排序在最初和最后的排序次数都为 n(n 为数组长度):前一阶段排序次数呈逐渐上升的状态,当到达某一顶点后,排序次数会逐渐下降至 n,而该顶点的具体计算过程暂时无法给出。希尔排序的时间复杂度难以精确计算,核心原因是 间隔(gap)的取值方式多样(如 "减半法""Knuth 序列" 等),不同 gap 序列对排序次数的影响差异较大,导致时间复杂度没有固定统一的计算结果。在严蔚敏所著的《数据结构(C 语言版)》中,给出的希尔排序时间复杂度范围为 (O(n^{1.3}) \sim O(n^2))(注:书中未指定单一固定值,而是基于常见 gap 序列的复杂度区间)。


2选择排序

2.1 直接选择排序

(1)在元素集合arrayi--arrayn-1中选择关键码最大(小)的数据元素

(2)若它不是这组元素中的最后一个(第一个)元素,则将它与这组元素中的最后一个(第一个)元素交换

(3)在剩余的arrayi--arrayn-2(arrayi+1--arrayn-1)集合中,重复上述步骤,直到集合剩余1个元素

该图在声明游览器找的,借鉴了https://blog.csdn.net/qq_45667680/article/details/107400977的博主图片

java 复制代码
 /**
     * 选择排序 :
     * 时间复杂度:O(N^2)
     *    没有最好情况 和 最坏情况
     * 空间复杂度:O(1)
     * 稳定性:不稳定
     * @param array
     */
    public static void selectSort(int[] array) {
        for (int i = 0; i < array.length; i++) {
            int minIndex = i;
            for (int j = i+1; j < array.length; j++) {
                if(array[j] < array[minIndex]) {
                    minIndex = j;
                }
            }
            swap(array,i,minIndex);
        }
    }
    private static void swap(int[] array,int i,int j) {
        int tmp = array[i];
        array[i] = array[j];
        array[j] = tmp;
    }
    //System.currentTimeMillis(); 属于 java.lang.System 类,是 Java 核心库中的一个工具类,用于获取当前系统时间与 UTC 1970 年 1 月 1 日 00:00:00 之间的毫秒差值(即时间戳)。
        public static void main(String[] args) {
        // 准备测试数组(可根据需要调整大小和内容)
        int[] array = new int[10000];
        for (int i = 0; i < array.length; i++) {
            array[i] = (int) (Math.random() * 100000); // 生成随机数填充数组
        }

        // 测试选择排序耗时
        long startTime = System.currentTimeMillis();
        Sort.selectSort(array); // 执行选择排序
        long endTime = System.currentTimeMillis();

        // 输出耗时(修正描述为"选择排序")
        System.out.println("选择排序耗时:" + (endTime - startTime) + " 毫秒");
    }
}

2.2.堆排序

堆排序(Heapsort)是指利用堆积树(堆)这种数据结构所设计的一种排序算法,它是选择排序的一种。它是通过堆来进行选择数据。需要注意的是排升序要建大堆,排降序建小堆。

java 复制代码
/**
     * 堆排序
     * 时间复杂度:O(n * logN)  对数据不敏感
     * 空间复杂度:O(1)
     * 稳定性:不稳定
     * @param array
     */
    public static void heapSort(int[] array) {
        //O(n)
        createHeap(array);
        //O(n * logN)
        int end = array.length-1;
        while (end > 0) {
            swap(array,0,end);
            siftDown(array,0,end);
            end--;
        }

    }

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

    private static void siftDown(int[] array, int parent, int length) {
        int child = 2 * parent + 1;
        while (child < length) {
            if(child+1 < length && array[child] < array[child+1] ) {
                child++;
            }
            if(array[child] > array[parent]) {
                swap(array,child,parent);
                //这里是因为加入说换位置了,那我的程序咋直到还完后在这个跟节点和它交换的子节点,这个字节点下面的值是大(小)跟堆,所以又重新往下调
                parent = child;
                child = 2 * parent+1;
            }else {
                break;
            }
        }
    }

3. 交换排序

基本思想:所谓交换,就是根据序列中两个记录键值的⽐较结果来对换这两个记录在序列中的位置,交换排序的特点是:将键值较⼤的记录向序列的尾部移动,键值较⼩的记录向序列的前部移动。

3.1 冒泡排序

java 复制代码
 /**
     * 冒泡排序:
     * 时间复杂度:O(N^2)
     *          加上优化之后,最好情况下-》O(N)
     * 空间复杂度:O(1)
     * 稳定性:稳定
     * @param array
     */
    public static void bubbleSort(int[] array) {
        //i趟数
        for (int i = 0; i < array.length-1; i++) {
            //
            boolean flg = false;
            for (int j = 0; j < array.length-1-i; j++) {
                if(array[j] > array[j+1]){
                    swap(array,j,j+1);
                    flg = true;
                }
            }
            if(!flg){
                break;
            }
        }
    }
private static void swap(int[] array,int i,int j) {
        int tmp = array[i];
        array[i] = array[j];
        array[j] = tmp;
    }
  1. 冒泡排序是⼀种非常容易理解的排序
  2. 时间复杂度:O(N^2)
  3. 空间复杂度:O(1)
  4. 稳定性:稳定
    2.3.2 快速排序
    快速排序是Hoare于1962年提出的⼀种⼆叉树结构的交换排序方法,其基本思想为:任取待排序元素
    序列中的某元素作为基准值,按照该排序码将待排序集合分割成两子序列,左⼦序列中所有元素均小
    于基准值,右子序列中所有元素均大于基准值,然后最左右子序列重复该过程,直到

3.2.快速排序

快速排序是Hoare于1962年提出的⼀种⼆叉树结构的交换排序方法,其基本思想为:任取待排序元素序列中的某元素作为基准值,按照该排序码将待排序集合分割成两子序列,左子序列中所有元素均小于基准值,右子序列中所有元素均大于基准值,然后最左右子序列重复该过程,直到所有元素都排列在相应位置上为止。

java 复制代码
/**
     * 快速排序
     * 时间复杂度:  最坏(有序/逆序):O(n^2)   最好:O(N*logN)
     * 空间复杂度:   最坏:O(N)   最好: O(logN)
     * 稳定性:不稳定
     * @param array
     */
    public static void quickSort(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) {
            insertSort(array,start,end);
            return;
        }

        //三数取中
        int index = mid_three(array,start,end);
        swap(array,index,start);

        int pivot = partition(array,start,end);

        quick(array,start,pivot-1);
        quick(array,pivot+1,end);
    }
java 复制代码
// 假设按照升序对array数组中[left, right)区间中的元素进⾏排序
 
void QuickSort(int[] array, int left, int right) {
 if(right - left <= 1)
 return;
 // 按照基准值对array数组的[left, right)区间中的元素进⾏划分
 
int div = partion(array, left, right);
 QuickSort(array, left, div);
 }
 // 划分成功后以div为边界形成了左右两部分 [left, div) 和[div+1, right) 
// 递归排[left, div) 
// 递归排[div+1, right) 
QuickSort(array, div+1, right);

1.Hoare 版

左哨兵:是找到比第一个的值(6)也就做基准值 小的数,就停到这个数字这(7),然后与右哨兵找到的交换位置,左哨兵接着走,继续找到比第一个大的数,一直按着这个规则,直到俩人相遇结束。相遇点记作:pivot基准值跟相遇点的值换 ,但是看这个划分的话不是很有序,从这个相遇的这个点(3),就把3的左边的接着按着这个规则寻找,又相遇了接着按着这个规则,直到左右哨兵起点是同一个结束。

右哨兵:是从后往前找 是找到比基准值(6)小的数,就停到这个数字这(3),然后与左哨兵找到的交换位置(左右交换一次就行,我只是写下来了),右哨兵接着走,继续找到比6小的数,一直按着这个规则,直到俩人相遇结束。后面跟左子树是一样的
简洁来说:

  • 选择基准(Pivot):从数组中选取一个元素作为 "基准"(通常选第一个、最后一个或中间元素)。

分区(Partition):通过 Hoare 划分法,将数组重新排列为两部分:

  • 左侧元素均小于等于基准;
  • 右侧元素均大于等于基准;
  • 基准元素最终落在正确的位置(左右分区的分界点)。
  • 左侧≤基准,右侧≥基准,把这个交换到最前面然后在他的左边在进行排序

递归排序:对基准左侧和右侧的子数组分别重复上述步骤,直至子数组长度为 1 或 0(自然有序)。

这个有点像二叉树,左边只有左子树,右边只有右子树

下标分为两种情况:

第一次相遇的左边:

L:0 起点

R:pivot-1

第一次相遇的右边:

L:pivot +1

R : array.length -1
那为什么从后往前?

准值的初始位置(第一个或最后一个元素)会决定指针的初始移动方向,其核心逻辑是一致的:通过调整指针移动顺序,确保最终相遇点的元素性质与基准值的初始位置匹配,从而在交换后将基准值正确放置在分区的分界点。

说白了就是我们是以第一个数为准基值,我们先找比他小的,如果相遇这个值一定是班比他小的,然后他俩交换位置,

反之,我们将最后一个数为准基值,我们就从前往后,找比它大的,交换,最后找到的值是比这个准基值大的,然后进行交换

再用更直白的话补充两句:

  • 当基准值在左边(第一个元素)时,我们的目标是 "在右边找一个比它小的元素",让右指针先出发去 "捞"这个小元素。当指针相遇时,这个位置一定藏着一个 "该在左边" 的小元素,和基准值交换后,基准值左边就都是小的,右边都是大的。
  • 当基准值在右边(最后一个元素)时,目标就变成 "在左边找一个比它大的元素",让左指针先出发去 "捞" 这个大元素。相遇时的位置一定藏着一个 "该在右边" 的大元素,交换后,基准值右边就都是大的,左边都是小的。

为什么 arrayright >= tmp是>= 与 arrayleft <= tmp要写< >?

  • 右指针(right):从后往前移,寻找 "应该放在左侧" 的元素(即 ≤ 基准值 tmp 的元素),找左侧的元素就是比准基值小的,所以arrayright 找比准基值小的停下来,进行交换换来的就是比准基值大的数
  • 左指针(left):从前往后移,寻找 "应该放在右侧" 的元素(即 ≥ 基准值 tmp 的元素)找右侧的元素就是比准基值大的,所以arrayleft 找比准基值大的停下来,进行交换换来的就是比准基值小的数
    = 存在的原因是:
    如果左右两边的值是一样的,那就是啥也没有交换

既然他是不完整的子树,我们需不需要限制它呢?

比如左边的子树的子树没有右边的子树"(即某个左分区的子分区只有左子数组、没有右子数组)是正常现象,本质上是由数组元素的分布和基准值的选择决定的,并不需要刻意 "限制"------ 因为快速排序的递归逻辑本身就通过 "终止条件" 自然处理了这种情况。

终止条件是:当子数组长度≤1 时,不再对其进行分区

java 复制代码
    /**
     * 划分待排序的序列
     * @param array
     * @param left
     * @param right
     * @return
     */
    private static int partition1(int[] array, int left, int right) {
        int i = left;
        int tmp = array[left];
        while (left < right) {
        //= 存在的原因是:**
 //如果左右两边的值是一样的,那就是啥也没有交换
            while (left < right && array[right] >= tmp) {
                right--;
            }

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

            swap(array,left,right);
        }
//将基准值与相遇点交换,让基准值回到他应该在的位置
        swap(array,left,i);
        return left;
    }

2.挖坑法

挖坑法单趟排序动图

注:此图借鉴博主是此链接的https://blog.csdn.net/gfdxx/article/details/126826128
选择基准值

以数组左侧第一个元素为基准值(tmp = arrayleft),目标是将数组中所有 ≤ tmp 的元素移到左侧,≥ tmp 的元素移到右侧。
2. 双向指针移动与元素覆盖

使用 left(左指针)和 right(右指针)从数组两端向中间逼近,通过覆盖操作逐步划分区域:

  • 第一步:右指针左移(找≤基准的元素)
java 复制代码
while (left < right && array[right] >= tmp) {
    right--; // 右指针左移,跳过所有≥基准的元素
}
array[left] = array[right]; // 将找到的≤基准的元素放到左指针位置

右指针从最右侧出发,不断左移,直到找到第一个小于等于基准值的元素,然后将该元素 "搬运" 到左指针当前位置(此时左指针位置的原始值已被存为 tmp,不会丢失)。

  • 第二步:左指针右移(找≥基准的元素)
java 复制代码
while (left < right && array[left] <= tmp) {
    left++; // 左指针右移,跳过所有≤基准的元素
}
array[right] = array[left]; // 将找到的≥基准的元素放到右指针位置

左指针从左侧出发(已被覆盖为右指针找到的元素),不断右移,直到找到第一个大于等于基准值的元素,然后将该元素 "搬运" 到右指针当前位置。
循环重复 :上述两步不断交替,直到 left == right(左右指针相遇),此时数组已被划分为两部分(左侧≤基准,右侧≥基准)。
3. 基准值归位

当 left == right 时,相遇位置就是基准值 tmp 在排序后应处的位置,将 tmp 放入该位置:

java 复制代码
array[left] = tmp;
return left; // 返回基准值的最终位置,用于后续递归划分左右子数组

代码如下:

java 复制代码
* 快速排序
     * 时间复杂度:  最坏(有序/逆序):O(n^2)   最好:O(N*logN)
     * 空间复杂度:   最坏:O(N)   最好: O(logN)
     * 稳定性:不稳定
  private static int partition(int[] array, int left, int right) {
        int tmp = array[left];
        while (left < right) {
            while (left < right && array[right] >= tmp) {
                right--;
            }
            array[left] = array[right];

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

该方法存在缺陷

1、递归层数过多有爆栈风险 2、面对有序或者接近有序的待排序数据,时间复杂度就变成了O()

所以需要作如下优化:

1.4三数取中,优化选key

1、随机选key(听着很随机,虽然不靠谱,但有的场景还是可以使用随即选key的方法)

2、针对有序情况,选正中间数据做key(前提是知道有序)

3、三数取中(选出左中右三数中间大小的做key)(三数取中后,对于缺陷2,直接由最坏情况变成最好情况)

3.前后指针法

注:声明这个图还是借鉴博主是此链接的https://blog.csdn.net/gfdxx/article/details/126826128

  • 判断cur 指针指向的数据是否小于key,若小于,则prev 指针后移一位,并则cur 指向的内容与prev 指向的内容交换,然后cur的指针++
  • 此时cur 指针指向的数据大于key, 则cur指针继续++
  • 在比较,cur 指针指向的数据还是小于key prev 先后移动一位,然后与cur 指向的数据交换
  • 又一次比较,cur 指针指向的数据还是小于key,prev 先后移动一位,然后与cur指向的数据交换,cur 再++
java 复制代码
 // 递归排序方法
    private static void quickSortRecursive(int[] array, int left, int right) {
        // 终止条件:当子数组范围无效(left >= right)时,停止递归
        if (left >= right) {
            return;
        }
        // 调用 partition3 进行分区,获取基准值的最终位置
        int pivotPos = partition3(array, left, right);
        // 递归排序基准值左侧子数组(left 到 pivotPos - 1)
        quickSortRecursive(array, left, pivotPos - 1);
        // 递归排序基准值右侧子数组(pivotPos + 1 到 right)
        quickSortRecursive(array, pivotPos + 1, right);
    }

 private static int partition3(int[] array, int left, int right) {
        int prev = left ;
        int cur = left+1;
        //防止cur越界超出范围
        while (cur <= right) {
          //cur 寻找比基准值小的数停下来
        //prev找到了prev+1 
        //prev 与 cur 之间一定都是比他大的数,然后交换即可
      
        //array[++prev] != array[cur]   然后交换
//array[++prev] != array[cur])相等的话有可能就是循环了
            if(array[cur] < array[left] && array[++prev] != array[cur]) {
                swap(array,cur,prev);
            }
            cur++;
        }
        swap(array,prev,left);
        //此时prev 在与left交换
        return prev;
    }

那这三种方法搞出来的排序一般都是不一样的,如果出选择题优先用挖坑法 ,Hoare , 前后指针法,一般会以挖坑法去考察你,

4.优化快速排序

  1. 三数取中法选key
  2. 递归到小的子区间时,可以考虑使用插入排序
    可以直接在idea help里直接改栈大小:-Xss

优化快速排序的核心目的是提升其在实际场景中的稳定性和效率

1.避免极端情况导致性能退化

  • 理想情况:基准元素能将数组均匀分割为两半,递归层数为 log n,时间复杂度为 O(n log n)。

  • 极端情况:若基准元素是当前区间的最大值或最小值(例如对已排序数组选择第一个元素作为基准),会导致分割极不均匀(一边为空,另一边为原区间长度

  • 1),递归层数退化为 n,时间复杂度退化到 O(n²)。

    2.提升对小规模数据的处理效率

    快速排序的递归特性在处理小规模数据(如长度小于 10~20 的数组) 时,效率不如插入排序等简单排序算法:

  • 快速排序的递归调用本身有额外开销(函数调用、栈操作等)。

  • 小规模数据的分割收益有限,反而可能被递归开销抵消。

  • 处理重复元素较多的数组

    当数组中存在大量重复元素时,未优化的快速排序可能出现以下问题:

  • 若基准元素是重复值,可能导致分割失衡(例如所有元素都等于基准,左右区间几乎未分割)。

  • 递归层数增加,时间复杂度接近 O(n²)。

    4 减少递归栈溢出风险

  • 快速排序的递归深度在最坏情况下为 O(n)(如极端基准选择),可能导致栈溢出(尤其是对大数组)。

  • 适应现代计算机体系结构

    5.未优化的快速排序可能存在缓存利用率低的问题:

  • 递归过程中对数组的随机访问可能导致缓存未命中(cache miss),增加内存访问开销。

快速排序的 "优化" 本质上是扬长避短:保留其平均 O(n log n) 时间复杂度、原地排序(空间效率高)的优点,同时通过针对性改进,解决基准选择不当、小规模数据低效、重复元素处理差等问题

1. 三数取中法选key

快速排序优化核心思路:

  • 选好基准:用三数取中或随机法,避免选到最值导致分割失衡。

  • 小数据换算法:区间长度较小时(如 < 15),改用插入排序,减少递归开销。

  • 处理重复值:用三路分割(小于 / 等于 / 大于基准),避免重复元素拖累效率。

  • 控制递归:用尾递归或手动栈,防止栈溢出,降低递归成本。

  • 混合策略:如内省排序,快速排序 + 堆排序 + 插入排序结合,兼顾效率与稳定性。

java 复制代码
private static void quick(int[] array, int start, int end) {
    if (start >= end) {
        return;
    }
    // 三数取中
    int index = mid_three(array, start, end);
    swap(array, index, start);

    int pivot = partition(array, start, end);

    quick(array, start, pivot - 1);
    quick(array, pivot + 1, end);
}
 private static int mid_three(int[] array, int left, int right) {
        int mid = (left+right)/2;
        if(array[left] < array[right]) {
            if(array[mid] < array[left]) {
                return left;
            }else if(array[mid] > array[right]) {
                return right;
            }else {
                return mid;
            }
        }else {
            if(array[mid] > array[left]) {
                return left;
            }else if(array[mid] < array[right]) {
                return right;
            }else {
                return mid;
            }
        }
    }

但是这个优化的深度还是不够,如果他是9,8,7,6,5,你在交换的完,右指针会进行很多的交换,还是有一定的问题的

2. 递归到小的子区间时,可以考虑使用插入排序

1.对外接口 quickSort(int\[\] array)

作为公共方法,接收待排序数组,调用内部递归方法 quick 并传入数组的全区间(从索引 0 到 array.length-1),隐藏了排序的细节实现。

2.核心递归方法 quick(int\[\] array, int start, int end)负责快速排序的主要逻辑,处理数组中 start, end 区间的排序:

  • **终止条件:**若 start >= end(区间为空或只有一个元素),直接返回(已有序)。
  • 小规模数据优化:若区间长度 end - start + 1 <= 15(阈值可调整),调用 insertSort 用插入排序处理,避免快速排序的递归开销。
  • 基准选择优化:通过 mid_three 方法(三数取中)选择区间内的 "中间值" 作为基准,交换到区间起始位置(便于后续分区)。
  • **分区与递归:**调用 partition 方法(需自行实现)将区间按基准分为 "小于基准" 和 "大于基准" 两部分,返回基准的最终位置 pivot,再递归处理左右子区间 start, pivot-1pivot+1, end

3.插入排序辅助方法 insertSort(int\[\] array, int left, int end)专门处理小规模区间 left, end 的排序:

通过 "逐个将元素插入到已排序部分的正确位置" 实现排序,适合小规模数据(因逻辑简单、无递归开销,效率高于快速排序)。

java 复制代码
    public static void quickSort(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) {
            insertSort(array,start,end);
            return;
        }

        //三数取中
        int index = mid_three(array,start,end);
        swap(array,index,start);

        int pivot = partition(array,start,end);

        quick(array,start,pivot-1);
        quick(array,pivot+1,end);
    }
//插入排序
    public static void insertSort(int[] array,int left,int end) {
        for (int i = left+1; i <= end; i++) {
            int tmp = array[i];
            int j = i-1;
            for (; j >= left; j--) {
                if(array[j] > tmp) {
                    array[j+1] = array[j];
                }else {
                    break;
                }
            }
            array[j+1] = tmp;
        }
    }

3.快速排序非递归

点击这里,进行把栈扩大

出现这个界面

java 复制代码
if(pivot > start + 1){
	左边有两个及以上的数据
	}
	if(pivot < end - 1){
	右边有两个及以上的数据
}
java 复制代码
    private static int partition(int[] array, int left, int right) {
        int tmp = array[left];
        while (left < right) {
            while (left < right && array[right] >= tmp) {
                right--;
            }
            array[left] = array[right];

            while (left < right && array[left] <= tmp) {
                left++;
            }
            array[right] = array[left];
        }
        array[left] = tmp;
        return left;
    }
  public static void quickSortNonR(int[] array) {
        int start = 0;
        int end = array.length-1;
        Deque<Integer> stack = new LinkedList<>();
        int pivot = partition(array,start,end);
        if(pivot > start + 1) {
            stack.push(start);
            stack.push(pivot-1);
        }
        if(pivot < end - 1) {
            stack.push(pivot+1);
            stack.push(end);
        }
        while (!stack.isEmpty()) {
            end = stack.pop();
            start = stack.pop();
            pivot = partition(array,start,end);
            if(pivot > start + 1) {
                stack.push(start);
                stack.push(pivot-1);
            }
            if(pivot < end - 1) {
                stack.push(pivot+1);
                stack.push(end);
            }
        }
    }

4.快速排序的总结

  1. 快速排序整体的综合性能和使用场景都是比较好的,所以才敢叫快速排序
  2. 时间复杂度:O(N*logN)
  3. 空间复杂度:O(logN)
  4. 稳定性:不稳定
    局部优化并不能从根上解决问题,对于快排,可以看一下尾递归的实现

归并排序

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

思路:

1.不断的分割数据,让数据的每一段都有序(一个数据相当于有序)

2.当所有子序列有序的时候,在把子序列归并,形成更大的子序列,最终整个数组有序。

该图借鉴:https://www.cnblogs.com/MarisaMagic/p/16908457.html

java 复制代码
 /**
     * 时间复杂度:O(N*logN) 和数据是否有序无序没有关系
     * 空间复杂度:O(N)
     * 稳定性:稳定的
     *       直接插入   冒泡排序  归并排序
     * @param array
     */
public static void mergeSort(int[] array) {
    mergeChild(array, 0, array.length - 1);
}

private static void mergeChild(int[] array, int left, int right) {
    if (left >= right) {
        return;
    }
    int mid = (left + right) / 2;
    mergeChild(array, left, mid);      // 递归排序左半部分
    mergeChild(array, mid + 1, right); // 递归排序右半部分
    merge(array, left, mid, right);    // 合并两个有序子数组
}

// 合并两个有序子数组:array[left..mid] 和 array[mid+1..right]
private static void merge(int[] array, int left, int mid, int right) {
    int[] temp = new int[right - left + 1]; // 临时数组,存储合并后的结果
    int i = left;   // 左子数组的起始指针
    int j = mid + 1; // 右子数组的起始指针
    int k = 0;      // 临时数组的指针

    // 比较左右子数组的元素,按从小到大放入临时数组
    while (i <= mid && j <= right) {
        if (array[i] <= array[j]) {
        //左右两个数组都是前边小,后边大,所以两边比较,谁小谁先存
            temp[k++] = array[i++];
        } else {
            temp[k++] = array[j++];
        }
    }
//因为两边的数不一定相等,需要我们判断到底是那一边长,就执行哪边

    // 处理左子数组剩余的元素
    while (i <= mid) {
        temp[k++] = array[i++];
    }

    // 处理右子数组剩余的元素
    while (j <= right) {
        temp[k++] = array[j++];
    }

    // 将临时数组的结果拷贝回原数组
    for (k = 0; k < temp.length; k++) {
        array[left + k] = temp[k];
    }
}

非递归的归并

java 复制代码
  /**
     * 非递归的归并排序
     * @param array
     */
    public static void mergeSortNor(int[] array) {
        int gap = 1;
        while (gap < array.length) {
        // 用于遍历每个分组的起始位置
        //i + 2*gap 表示跳到 "下一个同类型分组的起始位置"(因为每个分组内部的元素间隔为 gap,相邻分组的起始位置间隔为 2*gap)
//        每一对 "待合并的相邻子数组" 的第一个子数组的起始下标
//这一步就是把每两个小数组合并成大一点的大数组,所以中间大数组的起点间隔就是i+* gap,小数组是2 * gap
            for (int i = 0; i < array.length; i = i + 2*gap) {
                int left = i;
                int mid = left+gap - 1;
                //因为他们会越界 mid ,right .所以在他们要越界的时候,放他们等于数组最后一个
                if(mid >= array.length) {
                    mid = array.length-1;
                }
                int right = mid+gap;
                if(right >= array.length) {
                    right = array.length-1;
                }
                merge(array,left,mid,right);
            }
            gap *= 2;
        }
    }

2.4.2 归并排序总结

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

2.4.3 海量数据的排序问题

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

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

因为内存中因为无法把所有数据全部放下,所以需要外部排序,而归并排序是最常用的外部排序

  1. 先把文件切分成200份,每个512M
  2. 分别对512M排序,因为内存已经可以放的下,所以任意排序方式都可以
  3. 进行2路归并,同时对200份有序文件做归并过程,最终结果就有序了
  4. 排序算法复杂度及稳定性分析

    希尔排序写的不是很准确,上面有介绍

4. 其他非基于比较排序(了解)

1. 计数排序

注意:图片来源https://blog.csdn.net/k1234hxh/article/details/134631590

思想:计数排序又称为鸽巢原理,是对哈希直接定址法的变形应用。操作步骤:

a. 统计相同元素出现次数

b. 根据统计的结果将序列回收到原来的序列中

【计数排序的特性总结】

a. 计数排序在数据范围集中时,效率很高,但是使用范围及场景有限。

b. 时间复杂度:O(MAX(N,范围))

c. 空间复杂度:O(范围)

d. 稳定性:稳定

2. 基数排序

注意:该图借鉴https://blog.csdn.net/m0_46975599/article/details/112174826

核心原理

确定最大位数 d:找到数组中最大的数,确定其位数(如最大数是 123,则 d=3)。

按位排序:从最低位(个位)到最高位(百位),对每一位进行 "分配 - 收集" 操作:

  • 分配:将所有数按当前位的值(0-9)放入对应的 "桶"(共 10 个桶,对应 0-9)。
  • 收集:按桶的顺序(0→9)将元素依次取出,组成新的数组,此时数组按当前位有序。
  • 重复:完成所有位数的排序后,数组整体有序。
java 复制代码
import java.util.Arrays;

public class RadixSort {

    // 基数排序入口
    public static void radixSort(int[] array) {
        if (array == null || array.length <= 1) {
            return;
        }

        // 1. 找到数组中的最大值,确定最大位数 d
        int max = array[0];
        for (int num : array) {
            if (num > max) {
                max = num;
            }
        }
        int d = 0; // 最大位数
        while (max > 0) {
            max /= 10;
            d++;
        }

        // 2. 按每位进行排序(从个位到高位)
        int radix = 1; // 用于提取当前位(1:个位,10:十位,100:百位...)
        int[][] buckets = new int[10][array.length]; // 10个桶,每个桶最多存放array.length个元素
        int[] bucketCounts = new int[10]; // 记录每个桶中元素的数量

        for (int i = 0; i < d; i++) { // 循环 d 次,处理每一位
            // 清空桶计数
            Arrays.fill(bucketCounts, 0);

            // 分配:将元素按当前位放入对应桶中
            for (int num : array) {
                int digit = (num / radix) % 10; // 提取当前位的值(0-9)
                buckets[digit][bucketCounts[digit]++] = num;
            }

            // 收集:按桶顺序(0-9)将元素取出,重新组成数组
            int index = 0;
            for (int j = 0; j < 10; j++) { // 遍历每个桶
                for (int k = 0; k < bucketCounts[j]; k++) { // 取出桶中所有元素
                    array[index++] = buckets[j][k];
                }
            }

            radix *= 10; // 处理下一位(个位→十位→百位...)
        }
    }

    // 测试
    public static void main(String[] args) {
        int[] array = {170, 45, 75, 90, 802, 24, 2, 66};
        System.out.println("排序前:" + Arrays.toString(array));
        radixSort(array);
        System.out.println("排序后:" + Arrays.toString(array));
    }
}

特点与适用场景

优点:效率高(非比较排序),适合大规模整数排序。

缺点:依赖数据的位数,不适用于浮点数(需特殊处理)或长度差异大的字符串,且需要额外空间(桶)。

适用场景:整数排序、固定长度的字符串排序(如手机号、邮编)等。

3. 桶排序

核心原理

  • 创建桶:根据数据范围和分布,创建 k 个空桶(通常是数组或链表)。
  • 分配数据:遍历待排序数组,将每个元素放入对应的桶中(例如:0-9 分放桶 0,10-19 分放桶 1...)。
  • 桶内排序:对每个非空桶内的元素单独排序(可使用快速排序、插入排序等)。
  • 合并结果:按桶的顺序依次取出所有元素,拼接成最终的有序数组。
java 复制代码
import java.util.*;

public class BucketSort {

    // 桶排序入口
    public static void bucketSort(int[] array) {
        if (array == null || array.length <= 1) {
            return;
        }

        // 1. 确定数据范围(假设数据在 [0, 100) 之间)
        int min = 0;
        int max = 100;
        int bucketCount = 10; // 创建10个桶,每个桶存放10个范围的数据(0-9, 10-19, ..., 90-99)
        int bucketSize = (max - min) / bucketCount; // 每个桶的范围大小

        // 2. 初始化桶(使用链表存储桶内元素,方便动态添加)
        List<List<Integer>> buckets = new ArrayList<>();
        for (int i = 0; i < bucketCount; i++) {
            buckets.add(new LinkedList<>());
        }

        // 3. 将元素分配到对应的桶中
        for (int num : array) {
            // 计算元素应放入的桶索引(确保索引在0~bucketCount-1之间)
            int bucketIndex = Math.min((num - min) / bucketSize, bucketCount - 1);
            buckets.get(bucketIndex).add(num);
        }

        // 4. 对每个桶内的元素排序,并合并结果
        int index = 0;
        for (List<Integer> bucket : buckets) {
            if (!bucket.isEmpty()) {
                // 桶内排序(使用Collections.sort,底层是归并排序的变种)
                Collections.sort(bucket);
                // 将排序后的桶元素放入原数组
                for (int num : bucket) {
                    array[index++] = num;
                }
            }
        }
    }

    // 测试
    public static void main(String[] args) {
        int[] array = {45, 22, 88, 35, 56, 12, 77, 9, 63};
        System.out.println("排序前:" + Arrays.toString(array));
        bucketSort(array);
        System.out.println("排序后:" + Arrays.toString(array));
    }
}

特点与适用场景

优点:效率高(数据均匀时接近线性时间),可并行处理(每个桶独立排序)。

缺点:依赖数据分布(分布不均时效率下降),需要额外空间存储桶。

适用场景:数据范围明确且分布均匀的场景(如学生成绩、用户年龄、商品价格等)。

桶排序是 "分治思想" 的典型应用,通过将大问题拆分为小问题(每个桶的排序),再合并结果,实现高效排序。

总结:基数排序是 "按位拆分,多轮排序",桶是固定的,不依赖数据分布;桶排序是 "按范围分桶,一次排序",桶是灵活的,依赖数据均匀分布。

相关推荐
Jerry5 小时前
LeetCode 28. 找出字符串中第一个匹配项的下标
算法
Jerry7 小时前
LeetCode 459. 重复的子字符串
算法
闲猫10 小时前
Spring AI 对接Deepseek ChatModel 聊天对话
java·前端·spring
海石10 小时前
1500分的题目,确实有实力,不过还是我略胜一筹
算法·leetcode
海石10 小时前
【记忆化搜索】条条大路通AC,走好适合你的那一条,走到后再考虑走得快
算法·leetcode
AI云海10 小时前
利用好手机这个工具-碎片化学习
学习
自信的未来11 小时前
JSON 工具|Web Worker 工程化打包 + 语法自动修复 + 多语言代码生成实战
java·前端·json
Brookty11 小时前
【JavaEE】线程安全(一).4:写块串行保安全、CAS
java·开发语言·java-ee·多线程·线程安全
Jerry12 小时前
LeetCode 151. 反转字符串中的单词
算法