八大排序算法

505 阅读5分钟

1. 冒泡排序

Bubble Sorting 基本思想是:通过对待排序序列从前向后(从下标较小的元素开始),依次比较相邻元素的值,若发现逆序则交换,使值较大的元素逐渐从前移向后部,就象水底下的气泡一样逐渐向上冒。

优化:因为排序的过程中,各元素不断接近自己的位置,如果一趟比较下来没有进行过交换,就说明序列有序,因此要在排序过程中设置一个标志flag判断元素是否进行过交换。从而减少不必要的比较。

  public static void bubbleAsc(int[] array) {
    int temp;
    for (int i = 0; i < array.length - 1; i++) {
      boolean flag = true;//是否发生过交换
      for (int j = 0; j < array.length - 1 - i; j++) {
        if (array[j] > array[j + 1]) {//如果逆序,则交换
          flag = false;
          temp = array[j];
          array[j] = array[j + 1];
          array[j + 1] = temp;
        }
      }
      if (flag) {//如果没有发生交换,说明排序完毕
        break;
      }
    }
  }

2. 选择排序

Select Sorting基本思想是:第一次从 arr[0] ~ arr[n-1] 中选取最小值,与 arr[0] 交换,第二次从 arr[1] ~ arr[n-1] 中选取最小值,与 arr[1] 交换,第三次从 arr[2] ~ arr[n-1] 中选取最小值,与 arr[2] 交换,…,第 i 次从 arr[i-1] ~ arr[n-1] 中选取最小值,与 arr[i-1] 交换,…,第 n-1 次从 arr[n-2] ~ arr[n-1] 中选取最小值,与 arr[n-2] 交换,总共通过 n-1 次,得到一个按排序码从小到大排列的有序序列。

  public static void selectAsc(int[] array) {
    int min, minIndex;
    for (int i = 0; i < array.length - 1; i++) {
      min = array[i];//假定最小值是第一个元素
      minIndex = i;//假定最小值索引是第一个
      for (int j = i + 1; j < array.length; j++) {
        if (min > array[j]) {//如果后面的元素比最小值还要小,交换
          min = array[j];
          minIndex = j;
        }
      }
      if (minIndex != i) {//小小的优化
        array[minIndex] = array[i];
        array[i] = min;
      }
    }
  }

3. 插入排序

Insertion Sorting 基本思想是:把 n 个待排序的元素看成为一个有序表和一个无序表,开始时有序表中只包含一个元素,无序表中包含有 n-1 个元素,排序过程中每次从无序表中取出第一个元素,把它的排序码依次与有序表元素的排序码进行比较,将它插入到有序表中的适当位置,使之成为新的有序表。

  public static void insertAsc(int[] array) {
    int insertValue, insertIndex;
    for (int i = 1; i < array.length; i++) {
      insertValue = array[i];//待插入值
      insertIndex = i - 1;//待插入位置
      while (insertIndex >= 0 && array[insertIndex] > insertValue) {//如果待插入位置元素比待插入值大,将值向后移动,继续向前搜索
        array[insertIndex + 1] = array[insertIndex];
        insertIndex--;
      }
      if (insertIndex != i - 1) {//小小的优化
        array[insertIndex + 1] = insertValue;
      }
    }
  }

4. 希尔排序

插入排序可能存在的问题:数组 arr = {2, 3, 4, 5, 6, 1} 这时需要插入的数 1 (最小),需要交换 5 次,以及 1 次赋最小值;当需要插入的数是较小的数时,后移的次数明显增多,对效率有影响。

希尔排序是希尔(Donald Shell)于1959年提出的一种排序算法。希尔排序也是一种插入排序,它是简单插入排序经过改进之后的一个更高效的版本,也称为缩小增量排序。

Shell Sorting 基本思想是:把记录按下标的一定增量分组,对每组使用插入排序算法排序;随着增量逐渐减少,每组包含的关键词越来越多,当增量减至1时,整个文件恰被分成一组,算法便终止。

只需要将插入排序稍微修改一下即可:在外部嵌套一层循环,该循环引入步长,然后将插入排序中 +1 或 -1 部分改为 +步长 或 -步长。

  public static void shellAsc(int[] array) {
    int insertValue, insertIndex;
    //只是加一层步长的循环嵌套,然后把 1 替换成步长
    for (int stepSize = array.length / 2; stepSize > 0; stepSize /= 2) {
      for (int i = stepSize; i < array.length; i++) {
        insertValue = array[i];
        insertIndex = i - stepSize;
        while (insertIndex >= 0 && array[insertIndex] > insertValue) {
          array[insertIndex + stepSize] = array[insertIndex];
          insertIndex -= stepSize;
        }
        if (insertIndex != i - stepSize) {
          array[insertIndex + stepSize] = insertValue;
        }
      }
    }
  }

5. 快速排序

Quick Sorting 是对冒泡排序的一种改进,其基本思想是:通过一趟排序将要排序的数据分割成独立的两部分,其中一部分的所有数据都比另外一部分的所有数据都要小,然后再按此方法对这两部分数据分别进行快速排序,整个排序过程递归进行,以此达到整个数据变成有序序列。

  public static void quickAsc(int[] array) {
    quickAsc(array, 0, array.length - 1);
  }

  private static void quickAsc(int[] array, int left, int right) {
    if (left >= right) {
      return;
    }
    //以左边元素为基准
    int key = array[left], l = left, r = right;
    while (l < r) {
      //先从右向左搜索(因为是以左边元素为基准)
      while (l < r && array[r] >= key) {
        r--;
      }
      //此时从右边找到比基准小的值
      array[l] = array[r];
      while (l < r && array[l] <= key) {
        l++;
      }
      //此时从左边找到比基准大的值
      array[r] = array[l];
    }
    //遍历完毕,此时 l = r,将基准赋值给 l 位置
    array[l] = key;
    //左边继续快排
    quickAsc(array, left, l - 1);
    //右边继续快排
    quickAsc(array, l + 1, right);
  }

6. 归并排序

Merge Sorting 利用归并的思想实现的排序方法,该算法采用经典的分治(divide-and-conquer)策略(分治法将问题分(divide)成一些小的问题然后递归求解,而治(conquer)的阶段则将分的阶段得到的各答案"修补"在一起,即分而治之)。

说明:可以看到这种结构很像一棵完全二叉树,本文的归并排序我们采用递归去实现(也可采用迭代的方式去实现)。分阶段可以理解为就是递归拆分子序列的过程。

分比较简单,不对数据进行任何操作;

再来看看治阶段,我们需要将两个已经有序的子序列合并成一个有序序列,比如上图中的最后一次合并,要将[4,5,7,8]和[1,2,3,6]两个已经有序的子序列,合并为最终序列[1,2,3,4,5,6,7,8],来看下实现步骤。

  public static void mergeAsc(int[] array) {
    mergeDivideAsc(array, 0, array.length - 1, new int[array.length]);
  }

  public static void mergeDivideAsc(int[] array, int left, int right, int[] tempArray) {
    if (left >= right) {
      return;
    }
    int mid = (left + right) / 2;
    //向左继续分
    mergeDivideAsc(array, left, mid, tempArray);
    //向右继续分
    mergeDivideAsc(array, mid + 1, right, tempArray);
    //分完后,开始合并
    mergeConquerAsc(array, left, mid, right, tempArray);
  }

  public static void mergeConquerAsc(int[] array, int left, int mid, int right, int[] tempArray) {
    int leftIndex = left, rightIndex = mid + 1, index = 0;
    //比较左边元素与右边元素大小,将小的放入临时数组
    while (leftIndex <= mid && rightIndex <= right) {
      if (array[leftIndex] > array[rightIndex]) {
        tempArray[index++] = array[rightIndex++];
      } else {
        tempArray[index++] = array[leftIndex++];
      }
    }
    //此时,左边和右边必定有一个已经遍历完毕
    //如果左边还有元素,将元素依次放入临时数组
    while (leftIndex <= mid) {
      tempArray[index++] = array[leftIndex++];
    }
    //如果右边还有元素,将元素依次放入临时数组
    while (rightIndex <= right) {
      tempArray[index++] = array[rightIndex++];
    }
    index = 0;
    //将临时数组数据放入目标数组中
    for (int i = left; i <= right; i++) {
      array[i] = tempArray[index++];
    }
  }

7. 基数排序

Radix Sorting 属于“分配式排序”,又称“桶子法”(Bucket Sorting或Bin Sorting),顾名思义,它是通过键值的各个位的值,将要排序的元素分配至某些“桶”中,达到排序的作用;

基数排序法是1887年赫尔曼 · 何乐礼发明的,是效率高的稳定性排序法,是桶排序的扩展。它是这样实现的:将整数按位数切割成不同的数字,然后按每个位数分别比较。

基数排序基本思想:将所有待比较数值统一为同样的数位长度,数位较短的数前面补零。然后,从最低位开始,依次进行一次排序。这样从最低位排序一直到最高位排序完成以后, 数列就变成一个有序序列,图文说明如下:

说明:

  • 基数排序是对传统桶排序的扩展,速度很快;

  • 基数排序是经典的空间换时间的方式,占用内存很大, 当对海量数据排序时,容易造成 OutOfMemoryError;

  • 基数排序时稳定的【注:假定在待排序的记录序列中,存在多个具有相同的关键字的记录,若经过排序,这些记录的相对次序保持不变,即在原序列中,r[i] = r[j],且 r[i] 在 r[j] 之前,而在排序后的序列中,r[i] 仍在 r[j] 之前,则称这种排序算法是稳定的;否则称为不稳定的】;

  • 有负数的数组,我们不用基数排序来进行排序, 如果要支持负数,参考:code.i-harness.com/zh-CN/q/e98…

    public static void radixAsc(int[] array) { //定义 10 个桶 int[][] bucket = new int[10][array.length]; //10 个桶内元素下一个位置的索引,或者说是桶内元素个数 int[] bucketSize = new int[10]; for (int div = 1; ; div *= 10) { boolean zeroFlag = true;//是否所有元素除以 div 后都等于 0 for (int i = 0; i < array.length; i++) { int d = array[i] / div; if (d > 0) { zeroFlag = false; } int mod = d % 10; bucket[mod][bucketSize[mod]++] = array[i];//放入桶的对应位置 } if (zeroFlag) {//都等于 0,则说明排序完毕 break; } int index = 0; //将桶中元素放回数组,一次排序完毕 for (int i = 0; i < bucketSize.length; i++) { for (int j = 0; j < bucketSize[i]; j++) { array[index++] = bucket[i][j]; } //注意需要将桶内元素个数清 0 bucketSize[i] = 0; } } }

8. 堆排序

堆排序是利用堆这种数据结构而设计的一种排序算法,堆排序是一种选择排序,它的最坏,最好,平均时间复杂度均为 O(nlogn),它也是不稳定排序。

堆是具有以下性质的完全二叉树:每个结点的值都大于或等于其左右孩子结点的值,称为大顶堆,注意:没有要求结点的左孩子的值和右孩子的值的大小关系;每个结点的值都小于或等于其左右孩子结点的值,称为小顶堆。

大顶堆举例说明:

小顶堆举例说明:

一般升序采用大顶堆,降序采用小顶堆。

Heap Sorting 基本思想是:将待排序序列构造成一个大顶堆;此时,整个序列的最大值就是堆顶的根结点。将其与末尾元素进行交换,此时末尾就为最大值。然后将剩余 n-1 个元素重新构造成一个大顶堆,这样会得到 n 个元素的次小值。如此反复执行,便能得到一个有序序列了。可以看到在构建大顶堆的过程中,元素的个数逐渐减少,最后就得到一个有序序列了。

  public static void heapAsc(int[] array) {
    //最后一个非叶子结点的计算公式:array.length / 2 - 1    //从后往前遍历,将堆变成大顶堆
    for (int i = array.length / 2 - 1; i >= 0; i--) {
      heapAsc(array, i, array.length);
    }
    int temp;
    for (int i = 0; i < array.length - 1; i++) {
      //此时第一个元素就是最大值,将其与最后一个位置交换
      temp = array[0];
      array[0] = array[array.length - 1 - i];
      array[array.length - 1 - i] = temp;
      //继续构造大顶堆,此时只有 0 位置不满足大顶堆条件
      heapAsc(array, 0, array.length - 1 - i);
    }
  }

  /**
   * 本方法对 index 位置的元素进行大顶堆排序。
   * 调用本方法有个前提:假定 index 位置的左右子树都是大顶堆,
   * 只有 index 位置根结点不满足大顶堆条件。
   * 如何保证该前提成立?
   * 只需要从后往前构建大顶堆就可以了。
   *
   * @param array  堆
   * @param index  待排序的根结点
   * @param length 待排序的长度
   */
  private static void heapAsc(int[] array, int index, int length) {
    int target = array[index];
    for (int i = index * 2 + 1; i < length; i = i * 2 + 1) {
      //如果右结点比左结点大,取大的那个位置
      if (i + 1 < length && array[i + 1] > array[i]) {
        i++;
      }
      if (array[i] > target) {//如果子结点比待排序的大,将子结点值赋给父结点
        array[index] = array[i];//此时 index 就是父结点,i 是子结点
        index = i;//继续向下遍历,当前位置赋值给 index,在下一次循环时,i 会变成左子结点位置,继续满足 index 是父结点 i 是子结点
      } else {//否则退出
        break;
      }
    }
    //循环完毕时,index 指向的位置要么已经替换到前面,要么就是原先的位置,再将值赋给 index
    array[index] = target;
  }

9. 常用排序算法总结和对比

  • 稳定:如果 a 原本在 b 前面,而 a=b,排序之后 a 仍然在 b 的前面;
  • 不稳定:如果 a 原本在 b 的前面,而 a=b,排序之后 a 可能会出现在 b 的后面;
  • 内排序:所有排序操作都在内存中完成;
  • 外排序:由于数据太大,因此把数据放在磁盘中,而排序通过磁盘和内存的数据传输才能进行;
  • 时间复杂度: 一个算法执行所耗费的时间;
  • 空间复杂度:运行完一个程序所需内存的大小;
  • n:数据规模;
  • k:“桶”的个数;
  • In-place:不占用额外内存;
  • Out-place:占用额外内存。