排序算法

衡量排序算法的几个概念

衡量排序算法的几个概念,如下:

执行效率(时间复杂度)

内存消耗(空间复杂度)

原地排序/Sorted in place (空间复杂度为 O(1) )

原地排序算法,就是特指空间复杂度是 O(1) 的排序算法。(即,在排序时,不产生新的数列,只在原数列对数列中的元素进行交换位置)

属于原地排序算法有:冒泡排序、选择排序、插入排序(希尔排序)、快排

稳定性

简单的说,两个值相等的元素在排序后前后顺序不变,称为稳定;反之(即,不一定不变),称为不稳定。

一、冒泡排序

每一次遍历,较大数都要向右(or 向左)移动(像冒泡效果)。

时间复杂度为:

  • 最优时间复杂度 -- O(n),即只遍历一遍
  • 最坏时间复杂度 -- O(n^2),跑完所有循环

稳定性:稳定

public static void main(String[] args) {
    int[] disorderArray = {54, 26, 93, 17, 77, 31, 44, 55, 20};   // 常规打乱
    int[] disorderArray2 = {17, 20, 26, 31, 44, 54, 55, 77, 93};  // 正序
    int[] disorderArray3 = {93, 77, 55, 54, 44, 31, 26, 20, 17};  // 倒序
    bubbleSort(disorderArray);  
    bubbleSort(disorderArray2);
    bubbleSort(disorderArray3);
}

public static void bubbleSort(int[] disorderArray) {
    int k = 0;  // 循环的次数
    for (int i = disorderArray.length - 1; i > 0; i--) {
        // i 为下面循环的终点。下面每次循环后,最大的数就放在最后了,所以终点减1
        boolean flag = true;
        for (int j = 0; j < i; j++) {  // 使用小于号,所以 j 只到 i - 1, 而最后的 j + 1 刚好是 i
            k++;
            if (disorderArray[j] > disorderArray[j + 1]) {
                // 每一次将较大的数后移,即冒泡效果
                int temp = disorderArray[j];
                disorderArray[j] = disorderArray[j + 1];
                disorderArray[j + 1] = temp;
                flag = false;
            }
        }
        if (flag) {  // 优化排序
            // 内层没有发生交换即这一次的循环是顺的,所以整个已经顺了,则退出循环;
            break;
        }
    }
    System.out.println("循环的次数:" + k);
}

二、选择排序

移动次数最少。

选择一个最小(or 最大)数与首位(or 末位)交换位置,放好的数在下一次内存层循环时剔除。

时间复杂度:

  • 最优时间复杂度-- O(n^2)
  • 最坏时间复杂度-- O(n^2)

稳定性:不稳定

public static void main(String[] args) {
    int[] disorderArray = {54, 26, 93, 17, 77, 31, 44, 55, 20};   // 常规打乱
    int[] disorderArray2 = {17, 20, 26, 31, 44, 54, 55, 77, 93};  // 正序
    int[] disorderArray3 = {93, 77, 55, 54, 44, 31, 26, 20, 17};  // 倒序
    selectionSort(disorderArray);  
    selectionSort(disorderArray2);
    selectionSort(disorderArray3);
}

public static void selectionSort(int[] disorderArray) {
    // 其中外层循环中的 i 表示:待排数列的 首位(or 末位)
    int k = 0;
    int n = disorderArray.length;
    for (int i = 0; i < n - 1; i++) { // 需要进行 n - 1 次交换位置
        int minNumIndex = i;
        // 每完成内层循环,都找出当前情况下最小数所在的位置
        for (int j = i + 1; j < n; j++) {  // j 的最大值为 n - 1
            k++;
            if (disorderArray[j] < disorderArray[minNumIndex]) {
                minNumIndex = j;
            }
        }
        // 将最小数所在的位置放置在第 i 位
        if (minNumIndex != i) {
            int temp = disorderArray[i];
            disorderArray[i] = disorderArray[minNumIndex];
            disorderArray[minNumIndex] = temp;
        }
    }
    System.out.println("循环的次数:" + k);
}

三、插入排序

选择一个数,插入到有序数列中

思路步骤:

  1. 先将第一个位置的元素是为有序序列,
  2. 从第二个位置(即下标为1的元素)开始取出(即,待插入的数),
  3. 向前比较插入有序序列里,此时有序序列的长度加一。

其中

边界条件:待插入的数,比较完有序序列的所有数后,仍然没找到位置,那说明它的位置应该是放在首位

时间复杂度:和冒泡一样

  • 最优时间复杂度:O(n)
  • 最坏时间复杂度:O(n2)

稳定性:稳定

public static void main(String[] args) {
    int[] disorderArray = {54, 26, 93, 17, 77, 31, 44, 55, 20};   // 常规打乱
    int[] disorderArray2 = {17, 20, 26, 31, 44, 54, 55, 77, 93};  // 正序
    int[] disorderArray3 = {93, 77, 55, 54, 44, 31, 26, 20, 17};  // 倒序
    insertSort(disorderArray);  
    insertSort(disorderArray2);
    insertSort(disorderArray3);
}

public static void insertSort(int[] disorderArray) {
    int k = 0;
    for (int i = 1; i < disorderArray.length; i++) {
        int temp = disorderArray[i];  // 待插入的数为:disorderArray[i],并将其取出,取出后该位置可用,后续较大的数可往此方向移动 或 为插入使用
        for (int j = i - 1; j >= 0; j--) {  // 往前插入,往前算,所以使用 --
            k++;
            if (temp < disorderArray[j]) {
                disorderArray[j + 1] = disorderArray[j];
                if (j == 0) {  // 已经没得比较了,就把它放在首位。(边界条件)
                    disorderArray[0] = temp;
                }
            } else {
                disorderArray[j + 1] = temp;
                break;
            }
        }
    }
    System.out.println("循环的次数:" + k);
}

四、快速排序

这里举例是从小到大排序。快排使用的递归实现,体现分治思想。

思路步骤:

  1. (分治思想的执行方式之一)选择一个基准(一般选择数列中的第一元素),然后遍历数列,

    ​ 将不小于基准的数放右边(称为--较大数列),小于基准的放左边(较小数列),基准放中间。

  2. 然后再分别对 较大数列 和 较小数列 执行上面一步(即,递归的方式)。

  3. 递归结束的条件:数列只有1个数或0个数时,即数列 的 开始位置start >= 结束位置end

问题1(步骤2的细节):如何切出 较大(小)数列---从原数列开始位置start(or 结束位置end) 到 基准位置来切

问题2(步骤1的细节):如何将较小数抛到左边,较大数抛到右边 --- 定义两个游标,轮流从数列的左右两边遍历数列

  1. 将最左边的数(即,数列的开始位置)取出,作为基准数。此时左边该位置可用。
  2. 从右边遍历,直到找到一个较小数抛到左边可用的位置。此时右边便有一个可用位置(找到就较小数就停止遍历)
  3. 接着从左边遍历,直到找到一个较小数抛到右边可用的位置。此时左边便有一个可用位置。(找到就较大数就停止遍历)
  4. 2 和 3 循环轮流,直到 左右两边的游标重合时,结束所有循环(2、3的遍历,以及2、3之间的循环轮流)

问题3(步骤1的细节):基准数放在哪个位置 --- 当两个游标重合时,该位置就是基准数的位置。

时间复杂度:

  • 最优时间复杂度:O(nlogn)
  • 最坏时间复杂度:O(n2)

稳定性:不稳定

public static void main(String[] args) {
    int[] disorderArray = {54, 26, 93, 17, 77, 31, 44, 55, 20};   // 常规打乱
    int[] disorderArray2 = {17, 20, 26, 31, 44, 54, 55, 77, 93};  // 正序
    int[] disorderArray3 = {93, 77, 55, 54, 44, 31, 26, 20, 17};  // 倒序
    quickSort(disorderArray, 0, disorderArray.length-1);
    quickSort(disorderArray2, 0, disorderArray.length-1);
    quickSort(disorderArray3, 0, disorderArray.length-1);
}

public static void quickSort(int[] disorderArray, int start, int end) {
    if (start >= end) {  // 步骤3
        return;
    }
    int low = start;  // 问题2
    int high = end;  // 问题2
    int mid = disorderArray[start];  // 问题2.1

    while (low < high) {  // 问题2.4
        while (low < high && disorderArray[high] >= mid) {  // 问题2.2
            times++;
            high--;
        }
        disorderArray[low] = disorderArray[high];  // 问题2.2

        while (low < high && disorderArray[low] < mid) {  // 问题2.3
            times++;
            low++;
        }
        disorderArray[high] = disorderArray[low];  // 问题2.3
    }
    disorderArray[low] = mid;  // 问题3

    // 步骤2、问题1
    quickSort(disorderArray, start, low - 1);
    quickSort(disorderArray, low + 1, end);
}

合理选择分区点

如果数据原来就是有序的或者接近有序的,每次分区点都选择最后一个数据,那快速排序算法就会变得非常糟糕,时间复杂度就会退化为 O(n2)。 最理想的分区点是:被分区点分开的两个分区中,数据的数量差不多。

三数取中法
从区间的首、尾、中间,分别取出一个数,然后对比大小,取这 3 个数的中间值作为分区点。这样每间隔某个固定的长度,取数据出来比较,将中间值作为分区点的分区算法,肯定要比单纯取某一个数据更好。 但是,如果要排序的数组比较大,那“三数取中”可能就不够了,可能要“五数取中”或者“十数取中”。

随机法
每次从要排序的区间中,随机选择一个元素作为分区点。这种方法并不能保证每次分区点都选的比较好,但是从概率的角度来看,也不大可能会出现每次分区点都选的很差的情况,所以平均情况下,这样选的分区点是比较好的。时间复杂度退化为最糟糕的 O(n2) 的情况,出现的可能性不大。

警惕堆栈溢出

快排是用递归实现的,递归要警惕堆栈溢出。为了避免快速排序里,递归过深而堆栈过小,导致堆栈溢出,我们有两种解决办法:
第一种是限制递归深度。一旦递归过深,超过了我们事先设定的阈值,就停止递归。 第二种是通过在堆上模拟实现一个函数调用栈,手动模拟递归压栈、出栈的过程,这样就没有了系统栈大小的限制。

五、归并排序

也是分治的思想(二分法来分)。将数列递归分成最小数列(一个元素的数列,视为有序数列),再有序两两合并成有序数列(递归到将所有数列合成为一个)

思路步骤: 先分后合

  1. 先将数列分为两个子数列,再将子数列分为两个子子数列(即,递归),直到细分后的数列元素小于等于1个。

    1-1. 方式一、将拆分的数列放在新的数列里

    1-2.方式二:用游标来表示查分,(没有新的数列产生)---- 代码采用次方式

  2. 将切分后的数列排序 并且向上合并。(关键点:最小的元素为1个,视为有序。合并操作要保证 合并后的数列也要有序)

时间复杂度:

  • 最优时间复杂度:O(nlogn)
  • 最坏时间复杂度:O(nlogn)

稳定性:稳定

public static void main(String[] args) {
    int[] disorderArray = {54, 26, 93, 17, 77, 31, 44, 55, 20};   // 常规打乱
    int[] disorderArray2 = {17, 20, 26, 31, 44, 54, 55, 77, 93};  // 正序
    int[] disorderArray3 = {93, 77, 55, 54, 44, 31, 26, 20, 17};  // 倒序
    mergeSort(disorderArray, 0, disorderArray.length-1);
    mergeSort(disorderArray2, 0, disorderArray.length-1);
    mergeSort(disorderArray3, 0, disorderArray.length-1);
}

public static void mergeSort(int[] disorderArray, int leftIndex, int rightIndex) {

    int mid = leftIndex + (rightIndex - leftIndex) / 2;
    if (leftIndex < rightIndex) {  //
        mergeSort(disorderArray, leftIndex, mid);  // 步骤1
        mergeSort(disorderArray, mid + 1, rightIndex);  // 步骤1
        merge(disorderArray, leftIndex, mid, rightIndex);  // 步骤2
    }
}

public static void merge(int[] disorderArray, int leftIndex, int mid, int rightIndex) {
    int i = leftIndex;
    int j = mid + 1;
    int k = 0;
    int[] tmp = new int[rightIndex - leftIndex + 1];
    while (i <= mid && j <= rightIndex) {
        tmp[k++] = disorderArray[i] < disorderArray[j] ? disorderArray[i++] : disorderArray[j++];
    }
    while (i <= mid) {
        tmp[k++] = disorderArray[i++];
    }
    while (j <= rightIndex) {
        tmp[k++] = disorderArray[j++];
    }
    // 将tmp中的数据拷贝到a[low...high]
    for (int x = 0; x < tmp.length; ++x) {
        disorderArray[x + leftIndex] = tmp[x];
    }
}

补充:

合并后的数列是新的数列。也可将新数列拷贝到原数列上,但本质还是产生了新数列。所以空间复杂度高(是 O(n))(即,不是原地排序)

总结:常见排序算法效率比较

排序算法 平均情况 最好情况 最差情况 稳定性 备注
冒泡排序 O(n^2) O(n) O(n^2) 稳定 适合数据量小
选择排序 O(n^2) O(n^2) O(n^2) 不稳定 适合数据量小
插入排序 O(n^2) O(n) O(n^2) 稳定 适合数列原时序较好的
希尔排序 不稳定 特殊的插入排序,与步长有关
快速排序 O(nlogn) O(nlogn) O(n^2) 不稳定 适合数据量大的
归并排序 O(nlogn) O(nlogn) O(nlogn) 稳定 适合数据量大的
posted @ 2020-07-19 17:00  roronoa_wang  阅读(158)  评论(0编辑  收藏  举报