八大排序算法

39 阅读10分钟

八大排序算法

常用的排序算法的对比

排序算法时间复杂度空间复杂度稳定性第i趟结束后的特点适用场景
插入排序O(N^2)O(1)稳定前i+1个元素是相对有序序列大部分已排序较好
希尔排序O(N^2)O(1)不稳定对于任意x保证A[x] <= A[x+d]N大时较好
冒泡排序O(N^2)O(1)稳定保证i个元素放入了最终位置N小时较好
快速排序N*O(logN)O(N)不稳定确定i个枢值放在最终位置N大时较好
选择排序O(N^2)O(1)稳定保证i个元素放入了最终位置N小时较好
堆排序N*O(logN)O(1)不稳定保证i个元素放入了最终位置N大时较好
归并排序N*O(logN)O(N)稳定每2^i个元素为一组,组内相对有序N大时较好
基数排序O(d(n+r))O(d)稳定只看最低的i位,已完成排序

冒泡排序:只有当 arr[i] > arr[i+1] 的时候,才会交换元素的位置,而相等的时候并不交换位置,所以冒泡排序是一种稳定排序算法。

选择排序:选择排序是给每个位置选择当前元素最小的,在一趟选择,如果当前元素比一个元素小,而该小的元素又出现在一个和当前元素相等的元素后面,那么交换后稳定性就被破坏了,所以选择排序是一种不稳定的排序算法。

插入排序:比较是从有序序列的末尾开始,也就是想要插入的元素和已经有序的最大者开始比起,如果比它大则直接插入在其后面,否则一直往前找直到找到它该插入的位置。如果碰见一个和插入元素相等的,那么把要插入的元素放在相等元素的后面。所以,相等元素的前后顺序没有改变,从原无序序列出去的顺序就是排好序后的顺序,所以插入排序是稳定的。

希尔排序:希尔排序是按照不同步长对元素进行插入排序 ,虽然一次插入排序是稳定的,不会改变相同元素的相对顺序,但在不同的插入排序过程中,相同的元素可能在各自的插入排序中移动,最后其稳定性就会被打乱,所以希尔排序是不稳定的。

归并排序:归并排序在归并的过程中,只有 arr[i] < arr[i+1] 的时候才会交换位置,如果两个元素相等则不会交换位置,所以它并不会破坏稳定性,归并排序是稳定的。

快速排序:快速排序需要一个基准值,在基准值的右侧找一个比基准值小的元素,在基准值的左侧找一个比基准值大的元素,然后交换这两个元素,此时会破坏稳定性,所以快速排序是一种不稳定的算法。

冒泡排序

排序原理:最值往一端移动

  • 比较相邻的元素。如果前一个元素比后一个元素大,就交换这两个元素的位置。
  • 对每一对相邻元素做同样的工作,从开始第一对元素到结尾的最后一对元素。最终最后位置的元素就是最大值。

冒泡排序示意图.png

/**  
* 冒泡排序  
*  
* @author 文轩  
* @create 2023-12-30 17:53  
*/
public class BubbleSort {
    public static void sortArray(int[] nums) {
        // 数据校验
        if(nums == null || nums.length == 0 || nums.length == 1) return;

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

选择排序

排序原理:找出各个位置上合适的值

  • 每一次遍历的过程中,都假定第一个索引处的元素是最小值,和其他索引处的值依次进行比较,如果当前索引处的值大于其他某个索引处的值,则假定其他某个索引处的值为最小值,最后可以找到最小值所在的索引。
  • 交换第一个索引处和最小值所在的索引处的值。

选择排序示意图.png

/**
 * 选择排序
 *
 * @author 文轩
 * @create 2023-12-30 18:01
 */
public class ChoiceSort {

    public static void  sortArray(int[] nums) {
        if(nums == null || nums.length == 0 || nums.length == 1) return;

        for (int i = 0; i < nums.length - 1; i++) {
            int minIndex = i;
            for (int j = i; j < nums.length; j++) {
                if(nums[j] < nums[minIndex]) {
                    minIndex = j;
                }
            }
            int temp = nums[minIndex];
            nums[minIndex] = nums[i];
            nums[i] = temp;
        }

    }
}

插入排序

排序原理:无序插入有序

  • 把所有的元素分为两组,已经排序的和未排序的;
  • 找到未排序的组中的第一个元素,向已经排序的组中进行插入;
  • 倒叙遍历已经排序的元素,依次和待插入的元素进行比较,直到找到一个元素小于等于待插入元素,那么就把待插入元素放到这个位置,其他的元素向后移动一位。

插入排序示意图.png

/**
 * 插入排序
 *
 * @author 文轩
 * @create 2024-01-02 9:13
 */
public class InsertSort {
    
    public static void sortArray(int[] nums) {
        for (int i = 0; i < nums.length; i++) {
            int curValue = nums[i];
            int curIndex = i - 1;
            while(curIndex >= 0 && curValue < nums[curIndex]) {
                nums[curIndex + 1] = nums[curIndex];
                curIndex--;
            }
            nums[curIndex + 1] = curValue;
        }
    }
}

希尔排序

排序原理:分组插入

  • 选定一个增长量 gap ,按照增长量 gap 作为数据分组的依据,对数据进行分组;
  • 对分好组的每一组数据完成插入排序;
  • 减小增长量,直到增量减为 1,重复第二步操作

希尔排序示意图.png

/**
 * 希尔排序
 *
 * @author 文轩
 * @create 2024-01-02 11:21
 */
public class ShellSort {
    public static void sortArray(int[] nums) {
        int gap = nums.length / 2;
        while (gap > 0) {
            for (int i = 0; i < nums.length; i++) {
                int curIndex = i - gap;
                int curValue = nums[i];
                while (curIndex >= 0 && nums[curIndex] > curValue) {
                    nums[curIndex + gap] = nums[curIndex];
                    curIndex -= gap;
                }
                nums[curIndex + gap] = curValue;
            }
            gap /= 2;
        }
    }
}

归并排序

排序原理:分治法,先分后治

  • 尽可能的一组数据拆分成两个元素相同的子组,并对每一个子组继续拆分,直到拆分后的每个子组的元素个数是 1 为止。
  • 将相邻的两个子组进行合并成一个有序的大组;
  • 不断的重复步骤2,直到最终只有一个组为止。

归并排序示意图.png

/**
 * 归并排序
 *
 * @author 文轩
 * @create 2024-01-03 9:11
 */
public class MergeSort {

    public static void sortArray(int[] nums) {
        int[] temp = new int[nums.length];
        mergeSort(nums, 0, nums.length - 1, temp);
    }

    public static void mergeSort(int[] nums, int start, int end, int[] temp) {
        if(start >= end) return;

        int mid = (start + end) / 2;

        // 将数组分成两份
        mergeSort(nums, start, mid, temp);
        mergeSort(nums, mid + 1, end, temp);

        // 将两个有序数组合并
        int i = start;
        int j = mid + 1;
        int index = start;
        while(i <= mid && j <= end) {
            temp[index++] =  nums[i] <= nums[j] ? nums[i++] : nums[j++];
        }
        while(i <= mid) {
            temp[index++] = nums[i++];
        }
        while(j <= end) {
            temp[index++] = nums[j++];
        }
        for (int k = start; k <= end; k++) {
            nums[k] = temp[k];
        }
    }
}

快速排序

排序原理:冒泡排序的改进,多次比较和排序

  • 首先设定一个分界值,通过该分界值将数组分成左右两部分;
  • 将大于或等于分界值的数据放到到数组右边,小于分界值的数据放到数组的左边。此时左边部分中各元素都小于或等于分界值,而右边部分中各元素都大于或等于分界值;
  • 然后,左边和右边的数据可以独立排序。对于左侧的数组数据,又可以取一个分界值,将该部分数据分成左右两部分,同样在左边放置较小值,右边放置较大值。右侧的数组数据也可以做类似处理。
  • 重复上述过程,可以看出,这是一个递归定义。通过递归将左侧部分排好序后,再递归排好右侧部分的顺序。当左侧和右侧两个部分的数据排完序后,整个数组的排序也就完成了。

快速排序示意图.png

/**
 * 快速排序
 *
 * @author 文轩
 * @create 2024-01-02 10:40
 */
public class QuickSort {

    public static void sortArray(int[] nums, int start, int end) {
        if(start >= end) return;
        int pointValue = nums[start];
        int left = start, right = end;
        while(left != right) {
            while(left < right && nums[right] >= pointValue) right--;
            swap(nums, left, right);
            while(left < right && nums[left] <= pointValue) left++;
            swap(nums, left, right);
        }
        // 递归实现左右分组
        sortArray(nums, start, left - 1);
        sortArray(nums, right + 1, end);
    }

    public static void swap(int[] nums, int i, int j) {
        int temp = nums[i];
        nums[i] = nums[j];
        nums[j] = temp;
    }
}

堆排序

堆排序的基本思想:

  • 将待排序的序列构造成一个大顶堆
    • 从最后一个非叶子节点开始,以该节点为根节点形成的二叉树,将子节点和该节点中的最大值放到该节点处,这样子就形成了一个小的大顶堆;
    • 一直到二叉树的根节点循环第1步操作,这样子大值一直在往上走,小值一直在往下走
    • 等根节点也完成了第1步操作后,就形成了一个大顶堆
  • 让第一个值【根节点的值】和最后一个元素的值交换,这样子就能让最大的值放到最后
  • 将剩余 n - 1 个元素重新构造成一个大顶堆,重复 1、2步骤,就能得到一个升序的序列

注意:升序采用大顶堆,降序采用小顶堆

大顶堆.png

/**
 * 堆排序
 *
 * @author 文轩
 * @create 2024-01-03 14:52
 */
public class HeapSort {

    /**
     * 堆排序
     * @param nums 要排序的数组
     */
    public static void sortArray(int[] nums) {
        // 构建大顶堆
        build(nums, nums.length - 1);
        for (int i = nums.length - 1; i > 0; i--) {
            // 交换堆顶元素和末尾元素
            swap(nums, 0, i);
            // 重新调整对结构
            adjust(nums, 0, i - 1);
        }
    }

    /**
     * 将数组构建成一个大顶堆
     * @param nums 数组
     * @param end 末尾范围
     */
    public static void build(int[] nums, int end) {
        // 最后一个非叶子节点开始从上往下进行构建
        for (int i = (end - 1) / 2; i >= 0 ; i--) {
            adjust(nums, i, end);
        }
    }

    /**
     * 调整指定范围构成的二叉树为大顶堆,前提是只有根元素不满足大顶堆的定义
     * @param nums 原始数组
     * @param start 开始范围
     * @param end 结束范围
     */
    public static void adjust(int[] nums, int start, int end) {
        int maxIndex, left, right;
        // 最后一个非叶子节点下标:(end - 1) / 2
        for (int i = start; i <= (end - 1) / 2; i++) {
            maxIndex = i;
            // 找出i节点的左右子节点的下标
            left = maxIndex * 2 + 1;
            right = maxIndex * 2 + 2;
            // 将左右子节点中较大值和父节点值交换
            if(left <= end && nums[maxIndex] < nums[left]) maxIndex = left;
            if(right <= end && nums[maxIndex] < nums[right]) maxIndex = right;
            if(maxIndex != i) swap(nums, maxIndex, i);
        }
    }

    /**
     * 交换数组两个下标的值
     * @param nums 数组
     * @param i 下标
     * @param j 下标
     */
    public static void swap(int[] nums, int i, int j) {
        int temp = nums[i];
        nums[i] = nums[j];
        nums[j] = temp;
    }
}

基数排序

基数排序原理:非比较型整数排序算法

  • 准备好十个桶【二维数组】,用于存放数组,按各个位的进行存放;
  • 计算数组中各个数的个位,按个位【0~9】放到十个桶对应的桶中;
  • 按桶的顺序将桶中的数字依次放回数组中;
  • 重复 2~3 步骤,直到数组中最多数组的最多位数为止。

基数排序示意图.png

/**
 * 基数排序
 *
 * @author 文轩
 * @create 2024-01-04 10:59
 */
public class RadixSort {

    public static void sortArray(int[] nums) {

        if(nums == null || nums.length == 0 || nums.length == 1) return;
        int length = nums.length;

        // 计算所有数中最大值的长度
        int maxValue = Arrays.stream(nums).max().getAsInt();
        int maxLength = String.valueOf(maxValue).length();

        // 准备十个桶
        int[][] buckets = new int[10][length];
        // 用于记录各个桶的数字数量
        int[] bucketLen = new int[10];

        for (int i = 0; i < maxLength; i++) {
            int divisor = (int) Math.pow(10, i);
            for (int j = 0; j < length; j++) {
                // 求余数
                int bucketIndex = (nums[j] / divisor) % 10;
                // 将数字根据余数放到对应桶中
                buckets[bucketIndex][bucketLen[bucketIndex]++] = nums[j];
            }

            // 将十个桶中的数据放回数组中
            int index = 0;
            for (int j = 0; j < buckets.length; j++) {
                while(bucketLen[j] > 0) {
                    nums[index++] = buckets[j][--bucketLen[j]];
                }
            }

            // 将桶长度清0
            for (int j = 0; j < bucketLen.length; j++) {
                bucketLen[j] = 0;
            }

        }
    }
}