我的博客
欢迎来到我的博客
bunny.icu

八大经典排序算法(Java实现)

八大经典排序算法(Java实现)

简单选择排序

每次循环找到未排序的最小的元素,与未排序的第一个元素交换,重复此过程直到整体有序。
所有情况下的时间复杂度均为O(n^2),空间复杂度为O(1)

public static void selectionSort(int[] nums) {
    for (int i = 0; i < nums.length; i++) {
        int minIndex = i;
        for (int j = i + 1; j < nums.length; j++) {
            if (nums[minIndex] > nums[j]) {
                minIndex = j;
            }
        }
        int temp = nums[i];
        nums[i] = nums[minIndex];
        nums[minIndex] = temp;
    }
}

直接插入排序

每次处理未排序的第一个元素,将其向左移动到已排序元素的适当位置,重复此过程直到整体有序。
序列基本有序的情况下适合用直接插入排序。
最好情况是序列已经有序,时间复杂度为O(n)
平均和最坏情况的时间复杂为是O(n^2)
空间复杂度为O(1)

public static void insertionSort(int[] nums) {
    for (int i = 1; i < nums.length; i++) {
        for (int j = i; j > 0; j--) {
            if (nums[j] < nums[j - 1]) {
                int temp = nums[j];
                nums[j] = nums[j - 1];
                nums[j - 1] = temp;
            }
        }
    }
}

希尔排序

每次循环选取一个步长(例如数组长度的一半),将距离等于步长的元素作为一组,各组组内进行直接插入排序。下次循环步长-1,重复此过程直到步长为1,即可完成排序。
最好情况与直接插入排序相同,即序列已经有序,时间复杂度为O(n)
平均情况下时间复杂度约为O(n^{1.3})
最坏情况为逆序,时间复杂度为O(n^2)
空间复杂度为O(1)

public static void shellSort(int[] nums) {
    // 以nums.length/2作为初始步长
    for (int step = nums.length / 2; step > 0; step--) {
        for (int offset = 0; offset < step; offset++) {
            for (int i = offset + step; i < nums.length; i += step) {
                for (int j = i; j > offset; j -= step) {
                    if (nums[j] < nums[j - step]) {
                        int temp = nums[j];
                        nums[j] = nums[j - step];
                        nums[j - step] = temp;
                    }
                }
            }
        }
    }
}

冒泡排序

从右往左比较相邻的两个元素,如果右边的元素小于左边,则交换位置,每次循环会把未排序的最小元素换到最左边,重复此过程直到整体有序。
有一种优化方式,是用flag记录此轮循环中是否发生过交换,如果一次都没交换,则说明排序已经完成,可以直接结束。考研408默认采用这种方式。
优化后的冒泡排序最好情况为序列已经有序,时间复杂度为O(n)
平均和最坏情况时间复杂度为O(n^2)
空间复杂度为O(1)

public static void bubbleSort(int[] nums) {
    for (int i = 0; i < nums.length; i++) {
        for (int j = nums.length - 1; j > i; j--) {
            if (nums[j] < nums[j - 1]) {
                int temp = nums[j];
                nums[j] = nums[j - 1];
                nums[j - 1] = temp;
            }
        }
    }
}

public static void bubbleSort2(int[] nums) {
    for (int i = 0; i < nums.length; i++) {
        boolean swapped = false;
        for (int j = nums.length - 1; j > i; j--) {
            if (nums[j] < nums[j - 1]) {
                swapped = true;
                int temp = nums[j];
                nums[j] = nums[j - 1];
                nums[j - 1] = temp;
            }
        }
        if (!swapped) {
            return;
        }
    }
}

快速排序

每次选取一个元素作为枢轴pivot(例如最左边的元素),将比枢轴小的元素换到左边,比枢轴大的元素换到右边,得到左右两个子序列,对两个子序列分别递归调用此方法,直到每个序列只有一个元素。
快速排序的辅助空间用于记录递归树,因此空间复杂度等于递归深度。
最好和平均情况时间复杂度为O(n\log_2n),递归深度为\log_2n,空间复杂度为O(\log_2n)
最坏情况为序列已经按顺序或逆序排列,时间复杂度为O(n^2),因为此时的枢轴不能将序列分为两个子序列,递归深度为n,空间复杂度为空间复杂度为O(n)
有一种优化的方式是取随机位置的元素作为枢轴,而不是第一个元素,这样可以避免最坏情况时间和空间复杂度过大的问题。考研408默认不采用这种方式。

public static void quickSort(int[] nums, int left, int right) {
    if (left >= right) {
        return;
    }
    int i = left;
    int j = right;
    int pivot = nums[left];
    while (i < j) {
        while (i < j && nums[j] >= pivot) j--;
        if (i < j) nums[i++] = nums[j];
        while (i < j && nums[i] < pivot) i++;
        if (i < j) nums[j--] = nums[i];
    }
    nums[i] = pivot;
    quickSort(nums, left, i - 1);
    quickSort(nums, j + 1, right);
}

二路归并排序

归并排序的思想是“分而治之”。
“分”的过程是如果元素数量大于1,则将序列分为尽可能等长的两个子序列。
“治”的过程是将子序列两两合并,合并后的序列内部是有序的,重复合并过程直到全部完成。
所有情况的时间复杂度均为O(n\log_2n),空间复杂度为O(n),辅助空间用于记录子序列。
归并排序可用于外部排序,数据占用的存储空间大于内存也能使用,因为子序列合并的过程不需要把全部数据读取到内存中。
归并排序是稳定的。

public static int[] mergeSort(int[] nums) {
    if (nums.length == 1) {
        return nums;
    }

    // 分
    int middle = nums.length / 2;
    int[] left = new int[middle];
    int[] right = new int[nums.length - middle];
    for (int i = 0; i < nums.length; i++) {
        if (i < middle) {
            left[i] = nums[i];
        } else {
            right[i - middle] = nums[i];
        }
    }

    // 治
    return merge(mergeSort(left), mergeSort(right));

}

public static int[] merge(int[] left, int[] right) {
    // i, j分别为left, right的索引
    int i = 0, j = 0;
    int[] result = new int[left.length + right.length];
    while (i < left.length && j < right.length) {
        if (left[i] < right[j]) {
            result[i + j] = left[i++];
        } else {
            result[i + j] = right[j++];
        }
    }
    while (i < left.length) {
        result[i + j] = left[i++];
    }
    while (j < right.length) {
        result[i + j] = right[j++];
    }
    return result;
}

基数排序

基数排序主要用于整数。
对于十进制正整数,先按个位数字排序,再按十位数排序,以此类推,直到完成最大数字最高位的排序。
每一位的排序不是基于比较的,而是直接根据值分配到对应的桶。
基数排序是稳定的,因为桶中的数可以按放入的顺序取出。
时间复杂度为O(d\cdot (n+r)),其中n是排序元素个数,d是数字的最大位数,r是进制,加号表示取最大值。
从下面的代码可以看出外层循环次数是d,内层循环次数是2n+r,通常n远大于r,所以时间复杂度也能简写为O(d\cdot n)
空间复杂度为O(rn),也能简写为O(n)

public static int getMaxDigit(int[] nums) {
    int maxValue = Integer.MIN_VALUE;
    for (int num : nums) {
        maxValue = Math.max(maxValue, num);
    }
    int maxDigit = 0;
    while (maxValue > 0) {
        maxValue /= 10;
        maxDigit++;
    }
    return maxDigit;
}


public static void radixSort(int[] nums) {
    int maxDigit = getMaxDigit(nums);
    // d表示倒数第几位
    for (int d = 0; d < maxDigit; d++) {
        // 初始化10个桶
        List<List<Integer>> buckets = new ArrayList<>();
        for (int i = 0; i < 10; i++) {
            buckets.add(new ArrayList<>());
        }

        // 获取当前位的值,放在对应的桶中
        for (int num : nums) {
            int n = num / (int) Math.pow(10, d) % 10;
            buckets.get(n).add(num);
        }

        // 依次从桶中取出来
        // 虽然有两层循环,但复杂度为O(n),因为取出n个数
        int j = 0;
        for (int i = 0; i < 10; i++) {
            for (int num : buckets.get(i)) {
                nums[j++] = num;
            }
        }
    }
}

堆排序

构造大根堆,然后将堆顶元素与未排序的最后一个元素交换,堆的长度减1,然后重新调整为大根堆,重复此过程直到堆为空。
时间复杂度为O(n\log n),建堆时间复杂度为O(n)
空间复杂度为O(1),因为堆是完全二叉树,可以用数组来表示
不稳定。

public static void heapify(int[] nums, int i, int length) {
    int left = i * 2;
    int right = i * 2 + 1;
    int maxIndex = i;
    if (left < length && nums[left] > nums[maxIndex]) {
        maxIndex = left;
    }
    if (right < length && nums[right] > nums[maxIndex]) {
        maxIndex = right;
    }
    if (i != maxIndex) {
        int temp = nums[i];
        nums[i] = nums[maxIndex];
        nums[maxIndex] = temp;
        heapify(nums, maxIndex, length);
    }
}

private static void buildMaxHeap(int[] nums, int length) {
    for (int i = length / 2; i >= 0; i--) {
        heapify(nums, i, length);
    }
}

private static void heapSort(int[] nums) {
    buildMaxHeap(nums, nums.length);
    int length = nums.length;
    while (length > 0) {
        int temp = nums[0];
        nums[0] = nums[length - 1];
        nums[length - 1] = temp;
        length--;
        heapify(nums, 0, length);
    }
}

总结

以上八种排序算法中,
稳定的有:冒泡排序、归并排序、基数排序
不稳定的有:简单选择排序、直接插入排序、希尔排序、快速排序、二路归并排序

平均情况下时间复杂度为O(n\log n)的有:快速排序、二路归并排序、堆排序
平均情况下时间复杂度为O(n^2)的有:简单选择排序、冒泡排序、插入排序

平均情况下空间复杂度为O(1)的有:堆排序、直接插入排序、希尔排序、简单选择排序、冒泡排序
平均情况下空间复杂度为O(n)的有:归并排序
平均情况下空间复杂度为O(n\log n)的有:快速排序

与初始状态有关的有:冒泡排序、快速排序、直接插入排序、希尔排序
与初始状态无关的有:简单选择排序、二路归并排序、堆排序、基数排序

基数排序是基于分配的排序,其他七种都是基于比较的排序

归并排序既能用于内部排序,也能用于外部排序,其他七种都只能用于内部排序

直接插入排序、希尔排序都属于插入排序
简单选择排序、堆排序都属于选择排序
冒泡排序、快速排序都属于交换排序

基本有序时最适合用直接插入排序,虽然复杂度相同,但比较次数更少
有序或逆序时最不适合用快速排序,此时快速排序时间复杂度为O(n^2),空间复杂度为O(n)

数据量较小时适合用时间复杂度为O(n^2)的排序算法,因为代码简洁便于实现
数据量较大时适合用时间复杂度为O(n\log n)的排序算法,因为性能更好

Reference

堆排序 – 菜鸟教程
冒泡排序 – 菜鸟教程
归并排序 – 菜鸟教程
基数排序 – 菜鸟教程
快速排序 – 菜鸟教程
快速排序 – 维基百科
基数排序 – 维基百科
快速排序算法 – bilibili
Python实现基数排序 – CSDN
【算法】排序算法之归并排序 – 知乎
深入理解快速排序(quciksort) – 知乎
排序算法之 基数排序 及其时间复杂度和空间复杂度 – CSDN

版权声明


本作品系原创, 转载须遵循 CC BY-NC-ND 4.0 许可协议
本文标题:八大经典排序算法(Java实现)
本文链接:https://www.bunny.icu/archives/852

推荐文章

发表评论

textsms
account_circle
email

bunny.icu

八大经典排序算法(Java实现)
常用的排序算法
扫描二维码继续阅读
2022-06-14