Skip to content
评价维度
  • 就地性 在原数组上直接操作实现排序,无需借助额外辅助数组
  • 稳定性 在完成排序后,相等元素在数组中的相对顺序不发生变化
  • 自适应性 能够利用输入数据已有的顺序信息来减少计算量,达到更优的时间效率
  • 是否基于比较 基于比较的排序依赖比较运算符(<、=、>)来判断元素的相对顺序,理论最优时间复杂度为O(nlogn)
    非比较排序不使用比较运算符,时间复杂度可以达O(n),但通用性差
排序算法对比
Preview
package algorithm.排序算法;

import java.util.*;

public class AllSort {

    // 【选择排序】
    // 算法:每次从未排序的数据里选最小的,放在已排序的后面
    // O(n^2)、基于比较、不稳定、原地、非自适应
    public static void selectionSort(int[] nums) {
        int n = nums.length;
        // 外循环:未排序的区间为[i, n - 1]
        for (int i = 0; i < n - 1; i++) {
            // 内循环:找到未排序区间的最小元素
            int k = i;
            for (int j = i + 1; j < n; j++) {
                if (nums[j] < nums[k]) k = j;  // 记录最小元素的索引
            }
            // 将该最小元素与未排序区间的首个元素交换
            int temp = nums[i];
            nums[i] = nums[k];
            nums[k] = temp;
        }
    }

    // 【冒泡排序】
    // 算法:从左到右,依次比较相邻元素大小,如果“左元素>右元素”,就交换二者(最大的会冒泡到最后)
    // O(n^2)、基于比较、稳定、原地、自适应
    public static void bubbleSort(int[] nums) {
        // 外循环:未排序区间为[0,i]
        for(int i = nums.length - 1; i > 0; i--) {
            // 内循环:将未排序区间[0, i]中最大的元素交换到该区间最右端
            for (int j = 0; j < i; j++) {  // (注意这里不是<=i,因为里面是j+1)
                if (nums[j] > nums[j+1]) {
                    int tmp = nums[j];
                    nums[j] = nums[j+1];
                    nums[j+1] = tmp;
                }
            }
        }
    }

    // 【冒泡排序 - 优化】
    // 如果某轮“冒泡”中没有执行任何交换操作,说明数组已经完成排序,可直接返回结果
    // 增加一个标志位flag监测
    public static void bubbleSortWithFlag (int[] nums) {
        // 外循环:未排序区间为[0,i]
        for (int i = nums.length - 1; i > 0; i--) {
            boolean flag = false; // 初始化标志位
            // 内循环:将未排序区间[0,i]中最大元素交换到该区间的最右端
            for (int j = 0; j < i; j++) {
                if (nums[j] > nums[j+1]) {
                    int tmp = nums[j];
                    nums[j] = nums[j+1];
                    nums[j+1] = tmp;
                    flag = true;
                }
            }
            if (!flag) break;  // 此轮没有交换,退出
        }
    }

    // 【插入排序】
    // 算法:在未排序区间选一个基准元素,将该元素与已排序区间的元素逐一比较大小,并将该元素插入到正确的位置
    // O(n^2)、基于比较、稳定、原地、自适应
    // 相比于上面两种,插入排序是用的最多的,而且,如果数据量较小(30个元素以内)的情况下,插入排序比快速排序O(nlogn)还快
    public static void insertionSort(int[] nums) {
        // 外循环:已排序区间为[0, i-1]
        for (int i = 1; i < nums.length; i++) {   // (i最大到len-1)
            int base = nums[i], j = i - 1;        // (j指向i的前1个位置,j最大到len-2)
            // 内循环:将base插入到已排序区间[0, i-1]中的正确位置
            while (j >= 0 && nums[j] > base) {
                nums[j + 1] = nums[j];
                j--;
            }
            nums[j + 1] = base;
        }
    }

    // 【快速排序】
    // 算法:选择数组中某个元素作为基准数,小于基准数的移到其左侧,大于基准的元素移到其右侧
    // O(nlogn)、基于比较、不稳定、原地、非自适应
    public static void quickSort(int[] nums, int left, int right) {
        // 子数组长度为1时终止递归
        if (left >= right) return;
        // 哨兵划分, partition会返回一个基准位置,基准位置的左侧值均小于基准值,右侧值均大于基准值
        int pivot = partition(nums, left, right);
        // 递归左子数组,右子数组
        quickSort(nums, left, pivot - 1);
        quickSort(nums, pivot + 1, right);
    }

    // 哨兵划分
    private static int partition(int[] nums, int left, int right) {
        // 以nums[left]为基准数
        int i = left, j = right;
        while (i < j) {
            while (i < j && nums[j] >= nums[left]) j--;
            while (i < j && nums[i] <= nums[left]) i++;
            // 上面两个while不执行了,一定是j的位置小于基准值并且i的位置大于基准值
            // 因为如果是i>j跳出循环,就会跳出最外层的while
            swap(nums, i ,j);
        }
        // 思考一下,left表示的值是?i表示的值是?
        // 上面的while执行完毕,left处表示的是基准值,基准值后面,一部分是小于基准值的,一部分是大于基准值的,
        // 已经排好序了,除了基准值不在里面
        // 根据上面的代码,跳出循环,一定有i>=j
        // 因为先处理的j,也就是在i<j的情况下,j停止处理,必须有nums[j]<nums[left]所以j的位置一定小于基准值
        // 所以一定有i和j重合,且i在最后一个小于基准值的位置
        // 交换i和left,则满足基准值left左侧都是小于基准值的,右侧都是大于基准值的
        swap(nums, i, left);  // 将基准数交换到2个子数组的分界线
        return i;
    }

    // 元素交换(交换i索引和j索引的元素)
    private static void swap(int[] nums, int i, int j) {
        int tmp = nums[i];
        nums[i] = nums[j];
        nums[j] = tmp;
    }

    // 【快速排序 - 基准数优化】
    // 选3个数中间那个数
    private static int medianThree(int[] nums, int left, int mid, int right) {
        int l = nums[left], m = nums[mid], r = nums[right];
        if ((l <= m && m <= r) || (r<=m && m <= l)) return mid;
        if ((m<=l && l<=r) || (r <= l && l <= m)) return left;
        return right;
    }

    // 三数取中值的哨兵函数
    private static int partition2(int[] nums, int left, int right) {
        int med = medianThree(nums, left, (left + right) /2, right);
        swap(nums, left, med); // 将中位数交换到数组最左端
        // 以nums[left]为基准数
        int i = left, j = right;
        while (i < j) {
            while (i < j && nums[j] >= nums[left]) j--;
            while (i < j && nums[i] <= nums[left]) i++;
            swap(nums, i, j);
        }
        swap(nums, i, left);
        return i;
    }

    // 【快速排序 - 尾递归优化】Java不支持尾递归优化
    public static void quickSort2(int[] nums, int left, int right) {
        // 子数组长度为1时终止
        while (left < right) {
            // 哨兵划分操作
            int pivot = partition2(nums, left, right);
            // 对2个子数组中较短的那个执行快速排序
            if (pivot - left < right - pivot) {
                quickSort2(nums, left, pivot - 1); // 递归排序左子数组
                left = pivot + 1;  // 剩余未排序区间为[pivot + 1, right]
            } else {
                quickSort2(nums, pivot + 1, right);  // 递归排序右子树数组
                right = pivot - 1; // 剩余未排序空间为[left, pivot-1]
            }
        }
    }

    // 因为Java不支持尾递归优化,所以依然可能导致栈溢出
    // 显式栈(迭代法)
    public void quickSortStack(int[] nums, int left, int right) {
        Deque<Integer> stack = new ArrayDeque<>();
        stack.push(left);
        stack.push(right);

        while (!stack.isEmpty()) {
            right = stack.pop();
            left = stack.pop();

            if (left >= right) continue;

            int pivot = partition2(nums, left, right);
            // 先处理较短子数组,减少栈深度
            if (pivot - left < right - pivot) {
                stack.push(left);
                stack.push(pivot - 1);

                stack.push(pivot + 1);
                stack.push(right);
            } else {
                stack.push(pivot + 1);
                stack.push(right);

                stack.push(left);
                stack.push(pivot - 1);
            }
        }
    }

    // 【归并排序】
    // 算法:不断递归将数组从中点分开,当子数组长度为1时,停止划分,将2个较短的有序数组合并为一个较长的有序数组
    // O(nlogn)、基于比较、稳定、非原地、非自适应
    public static void mergeSort(int[] nums, int left, int right) {
        // 终止条件
        if (left >= right) return; // 当子数组长度为1时终止递归
        // 划分阶段
        int mid = left + (right - left) / 2;
        mergeSort(nums, left, mid);  // 递归左子数组
        mergeSort(nums, mid+1, right); // 递归右子数组
        // 合并
        merge(nums, left, mid, right);
    }

    // 归并排序的合并操作
    private static void merge(int[] nums, int left, int mid, int right) {
        // 左子数组区间为[left, mid], 右子数组区间为[mid+1, right]
        // 创建一个临时数组tmp,存放合并后的结果
        int[] tmp = new int[right - left + 1];
        int i = left, j = mid + 1, k = 0;
        while (i <= mid && j <= right) {
            if (nums[i] <= nums[j]) tmp[k++] = nums[i++];
            else tmp[k++] = nums[j++];
        }
        // 将左子数组和右子数组的剩余元素复制到临时数组中
        while (i <= mid) {
            tmp[k++] = nums[i++];
        }
        while (j<=right) {
            tmp[k++] = nums[j++];
        }
        // 将临时数组中的元素复制回原数组中
        for (k = 0; k < tmp.length; k++) {
            nums[left + k] = tmp[k];
        }
    }

    // 【堆排序】
    // 算法:
    // 1. 输入数组并建立大顶堆。完成后,最大元素位于堆顶
    // 2.将堆顶元素(第一个元素)与堆底元素(最后一个元素)交换。完成交换后,堆的长度减1,已排序元素数量加1
    // 3.从堆顶元素开始,从顶到底执行堆化操作。完整堆化后,堆的性质得到修复
    // 4.循转执行2,3,循环n-1轮后,完成数组排序
    // O(nlogn)、基于比较、不稳定、原地、非自适应
    public static void heapSort(int[] nums) {
        // 建堆操作:堆化除叶节点以外的其他所有节点
        for (int i = nums.length / 2 - 1; i >= 0; i--) {
            siftDown(nums, nums.length, i);
        }
        // 从堆中提取最大元素,循环 n-1 轮
        for (int i = nums.length - 1; i > 0; i--) {
            // 交换根节点与最右叶节点(交换首元素与尾元素)
            int temp = nums[0];
            nums[0] = nums[i];
            nums[i] = temp;
            // 以根节点为起点,从顶至底进行堆化
            siftDown(nums, i, 0);
        }
    }

    // 堆的长度为n,从节点i开始,从顶至底堆化
    private static void siftDown(int[] nums, int n, int i) {
        while (true) {
            // 判断节点i,l,r中值最大的节点,记为ma
            int l = 2 * i + 1;
            int r = 2 * i + 2;
            int ma = i;
            if (l < n && nums[l] > nums[ma]) ma = l;
            if (r < n && nums[r] > nums[ma]) ma = r;
            // 若节点i最大或索引l,r越界,则无法继续堆化,跳出
            if (ma == i) break;
            // 交换两节点
            int temp = nums[i];
            nums[i] = nums[ma];
            nums[ma] = temp;
            // 循环向下堆化
            i = ma;
        }
    }

    // 【桶排序】
    // 算法:设置一些具有大小顺序的桶,每个桶对应一个数据范围,把数据分配到桶中,每个桶内部进行排序,最终按照桶的顺序合并数据
    // O(n+k),不比较、稳定,非原地,非自适应
    public static void bucketSort(float[] nums) {
        // 初始化 k = n/2个桶,预期向每个桶分配2个元素
        int k = nums.length / 2;
        List<List<Float>> buckets = new ArrayList<>();
        for (int i = 0; i < k; i++) {
            buckets.add(new ArrayList<>());
        }

        // 1. 将数组元素分配到各个桶中
        for (float num : nums) {
            // 输入数据范围为[0, 1),使用num * k映射索引范围[0, k-1)
            int i = (int) (num * k);
            buckets.get(i).add(num);
        }
        // 2. 对各个桶执行排序
        for (List<Float> buckect : buckets) {
            Collections.sort(buckect);
        }
        // 3. 遍历桶合并结果
        int i = 0;
        for (List<Float> buckect : buckets) {
            for (float num : buckect) {
                nums[i++] = num;
            }
        }
    }

    // 【计数排序】(用于整数数组)
    // 算法:需要一个辅助数组统计数字出现的次数,辅助数组的索引是整数值,对应的数组值是这个数字出现的次数
    // 之后按照索引顺序生成数据
    // O(n+m),不比较、稳定,非原地,非自适应
    public static void countingSortNaive(int[] nums) {
        // 1. 统计数组最大元素m
        int m = 0;
        for (int num : nums) {
            m = Math.max(m, num);
        }
        // 2. 统计各数字出现次数
        int[] counter = new int[m+1];
        for (int num : nums) {
            counter[num]++;
        }
        // 3. 遍历counter,将各元素填入原数组nums
        int i = 0;
        for (int num = 0; num < m + 1; num++) {
            for (int j = 0;j<counter[num];j++, i++) {
                nums[i] = num;
            }
        }
    }

    // 完整实现
    public static void countingSort(int[] nums) {
        // 1. 统计数组最大元素m
        int m = 0;
        for (int num : nums) {
            m = Math.max(m, num);
        }
        // 2. 统计各数字的出现次数
        // counter[num] 代表 num 的出现次数
        int[] counter = new int[m+1];
        for (int num : nums) {
            counter[num]++;
        }
        // 3. 求counter的前缀和,将“出现次数”转换为“尾索引”
        // 即counter[num]-1是num在res中最后一次出现的索引
        for(int i=0;i<m;i++) {
            counter[i+1] += counter[i];
        }
        // 4. 倒序遍历nums,将各元素填入结果数组res
        // 初始化数组res用于记录结果
        int n = nums.length;
        int[] res = new int[n];
        for (int i = n-1;i>=0;i--) {
            int num = nums[i];
            res[counter[num] - 1] = num;
            counter[num]--;
        }
        // 使用结果数组 res 覆盖原数组nums
        for (int i=0;i<n;i++) {
            nums[i] = res[i];
        }
    }

    // 【基数排序】(适用于数据量大但是范围小)
    // 算法:
    // O(nk),不比较、稳定,非原地,非自适应
    public static void radixSort(int[] nums) {
        // 获取数组的最大元素,用于判断最大位数
        int m = Integer.MIN_VALUE;
        for (int num : nums) {
            if (num > m) m = num;
        }
        for (int exp = 1;exp <= m; exp *= 10) {
            countingSortDigit(nums, exp);
        }
    }

    private static void countingSortDigit(int[] nums, int exp) {
        // 十进制的位范围为0-9,因此需要长度为10的桶数组
        int[] counter = new int[10];
        int n = nums.length;
        // 统计 0-9 各数字的出现次数
        for (int i = 0; i < n;i++) {
            int d = digit(nums[i], exp);
            counter[d]++;
        }
        for (int i=1;i<10;i++) {
            counter[i] += counter[i-1];
        }
        int[] res = new int[n];
        for (int i = n-1;i>=0;i--) {
            int d = digit(nums[i], exp);
            int j = counter[d] - 1;
            res[j] = nums[i];
            counter[d]--;
        }
        // 使用结果覆盖原数组
        for (int i=0;i<n;i++) nums[i] = res[i];
    }

    private static int digit(int num, int exp) {
        return (num / exp) % 10;
    }

    public static void main(String[] args) {
        int[] nums = new int[]{3,2,1,5,6,4,9,7,8,6,8};
        radixSort(nums);
        System.out.println(Arrays.toString(nums));
    }


}