6种基础排序算法

  • 插入排序

    • 步骤描述

      逐个将数据从后往前的与已排序范围内所有数据进行比较,找到正确位置后插入:
      1、处理 i 位置数据时,保证 [0...i-1] 范围内数据全部有序(本文中全部以从小到大排序为例);
      2、从有序范围结尾开始比较,如果值比当前处理的数要大就将大的数值向后移动;
      3、遇到小于当前数的位置就可以确认当前数该插入的位置。

    • 数据示例

      处理数据位置 数组的值
      0 [5, 3, 6, 1, 2]
      1 [3, 5, 6, 1, 2]
      2 [3, 5, 6, 1, 2]
      3 [1, 3, 5, 6, 2]
      4 [1, 2, 3, 5, 6]
    • 代码示例

      for (int i = 1; i < arr.length; i++) {
          for (int j = i; j > 0; j--) {
              if (arr[j - 1] > arr[j]) {
                  // 前一个数值比当前数值大, 交换后移
                  swap(arr, j, j - 1);
              }
              else {
                  // 继续往前的数不会再大于当前数, 可以确定插入位置
                  break;
              }
          }
      }
      

      上述代码还能稍微优化一下

      for (int i = 1; i < arr.length; i++) {
          int tmp = arr[i], j = i;
          while (j > 0 && arr[j - 1] > arr[j]) {
              arr[j--] = arr[j - 1];// 将大于当前数后移一位
          }
          // 循环结束时j指向的就是该插入的位置
          arr[j] = tmp;
      }
      
  • 选择排序

    • 步骤描述

      每轮遍历都选取未排序范围内的最小值从而确定该值在排序后的正确位置:
      1、处理 i 位置数据时,保证 [0...i-1] 范围内数据已经有序;
      2、从 i 位置开始往后遍历,选取最小的数放到排序后应该位于的正确位置。

    • 数据示例

      处理数据的位置 数组的值 处理后的数组
      0 [5, 3, 6, 1, 2] [1, 3, 6, 5, 2]
      1 [1, 3, 6, 5, 2] [1, 2, 6, 5, 3]
      2 [1, 2, 6, 5, 3] [1, 2, 3, 5, 6]
      3 [1, 2, 3, 5, 6] [1, 2, 3, 5, 6]
    • 代码示例

      for (int i = 0; i < arr.length; i++) {
          int minIdx = i;
          for (int j = i + 1; j < arr.length; j++) {
              if (arr[j] < arr[minIdx]) {
                  minIdx = j;
              }
          }
          if (minIdx != i) {
              swap(arr, i, minIdx);
          }
      }
      
  • 冒泡排序

    • 步骤描述

      每次遍历未排序范围的相邻数据,如果顺序与排序需求不符就进行交换,从而将每次遍历的最值移到正确位置:
      1、处理 i 位置数据时,保证 [i...] 范围内数据有序;
      2、从 0 位置开始遍历到 i 位置,依次比较相邻两个数;
      3、如果前一个数大于它的下一个数,交换这两个数。

    • 数据示例

      处理数据的位置 数组的值 交换的位置 处理后的数组
      4 [5, 3, 6, 1, 2] (0,1), (2, 3), (3, 4) [3, 5, 1, 2, 6]
      3 [3, 5, 1, 2, 6] (1,2), (2, 3) [3, 1, 2, 5, 6]
      2 [3, 1, 2, 5, 6] (0,1), (1, 2) [1, 2, 3, 5, 6]
      1 [1, 2, 3, 5, 6] [1, 2, 3, 5, 6]
    • 代码示例

      for (int i = arr.length - 1; i > 0; i--) {
          boolean noSwap = true;
          for (int j = 0; j < i; j++) {
              if (arr[j] > arr[j + 1]) {
                  swap(arr, j, j + 1);
                  noSwap = false;
              }
          }
          if (noSwap) {
              // 当前遍历没有发生任何交换,说明数组全部有序,直接结束
              return;
          }
      }
      
  • 快速排序

    • 步骤描述

      从未排序数组中选取一个数将区间内的数按照大于或小于该数划分为两个部分,再依次按照同一逻辑分治处理更小的这两个区间:
      1、处理 [left, right] 区间时选取一个数,为了防止原数组有序导致性能最差的情况,采用随机选取的方式;
      2、将区间内大于随机元素的数据移到右半部分,其它数据移到左半部分;
      3、递归处理每一次划分得到的两个更小区间,直至全部处理完毕。

    • 数据示例

      处理的区间 数组的值 处理后的数组
      [0,4] [5, 3, 6, 1, 2] [1, 2, 3, 5, 6]
      [0,0], [2,4] [1, 2, 3, 5, 6] [1, 2, 3, 5, 6]
      [2,3] [1, 2, 3, 5, 6] [1, 2, 3, 5, 6]
      [2,2] [1, 2, 3, 5, 6] [1, 2, 3, 5, 6]
    • 代码示例

        public void quickSort(int[] arr) {
          if (arr == null || arr.length < 2) {
            return;
          }
          quickSort(arr, 0, arr.length - 1);
        }
      
        private void quickSort(int[] arr, int left, int right) {
          if (left >= right) {
            return;
          }
          int p = partition(arr, left, right);
          quickSort(arr, left, p - 1);
          quickSort(arr, p + 1, right);
        }
      
        /**
         * 将[left, right]区间的数据按照是否大于arr[right]划分为两部分,返回划分后arr[right]对应的下标值
         */
        private int partition(int[] arr, int left, int right) {
          // big - 1 指向下一个遍历到的大于arr[right]的数要交换到的位置
          int i = left, big = right;
          while (i < big) {// [i...big) 区间的数与arr[right]进行比较
            if (arr[i] > arr[right]) {
              swap(arr, --big, i);
            }
            else {
              i++;
            }
          }
          // 将用于比较的arr[right]交换到两个区间临界位置
          swap(arr, big, right);
          return big;
        }
      

      快排代码中两个重要的优化技巧:随机抽样和三路排序,改进后代码如下

        private void quickSort(int[] arr, int L, int R) {
          if (L >= R) {
            return;
          }
          swap(arr, L + (int) (Math.random() * (R - L + 1)), R);
          int[] p = partition(arr, L, R);
          quickSort(arr, L, p[0] - 1);
          quickSort(arr, p[1] + 1, R);
        }
      
        private int[] partition(int[] arr, int L, int R) {
          // 定义两个边界下标变量,始终保持 [L...small]区间数据都小于arr[R],[big...R-1]区间数据都大于arr[R]
          int small = L - 1, big = R;
          while (L < big) {
            if (arr[L] < arr[R]) {
              // 小于的数移到左边,small边界右扩,遍历下标右移
              swap(arr, ++small, L++);
            }
            else if (arr[L] > arr[R]) {
              // 大于的数移到右边,big边界左扩,遍历下标不变(因为交换过来的数还未比较过)
              swap(arr, --big, L);
            }
            else {
              // 相等的数不需要改变位置,下标右移即可
              L++;
            }
          }
          // 将大数区间边界的值移到最右
          swap(arr, big, R);
          // 返回数组为相等的数区间的左右边界下标值
          return new int[]{small + 1, big};
        }
      
  • 归并排序

    • 步骤描述

      将数组对半划分为两个部分,按照顺序将两个区间的数合并到辅助数组中实现排序:

      1、处理 [left, right] 区间数据时,从中间位置划分成两个子区间;

      2、双指针分别遍历两个子区间数据,按顺序复制到辅助数组,再复制回原数组区间;

      3、递归处理,直至区间不可再划分为二。

    • 数据示例

      处理的区间 数组的值 合并前的数组状态 处理后的数组
      [0,4] [5, 3, 6, 1, 2] [3, 5, 6, 1, 2] [1, 2, 3, 5, 6]
      [0,2]
      [3,4]
      [5, 3, 6]
      [1, 2]
      [3, 5, 6]
      [1, 2]
      [3, 5, 6, 1, 2]
      [0,1], [2,2]
      [3,3], [4,4]
      [5, 3], [6]
      [1], [2]
      [3, 5], [6]
      [1], [2]
      [3, 5, 6]
      [1,2]
    • 代码示例

        public void mergeSort(int[] arr) {
          if (arr == null || arr.length < 2) {
              return;
          }
          partition(arr, 0, arr.length - 1);
        }
      
        private void partition(int[] arr, int L, int R) {
            if (L == R) {
                return;
            }
            int mid = L + ((R - L) >> 1);// 防止数字溢出
            partition(arr, L, mid);
            partition(arr, mid + 1, R);
            merge(arr, L, mid, R);
        }
      
        private void merge(int[] arr, int L, int mid, int R) {
            int[] tmp = new int[R - L + 1];
            int i = 0, j = L, k = mid + 1;
            while (j <= mid && k <= R) {
                // 右边大于左边时才复制右边,保证稳定性
                tmp[i++] = arr[j] <= arr[k] ? arr[j++] : arr[k++];
            }
            while (j <= mid) {
                tmp[i++] = arr[j++];
            }
            while (k <= R) {
                tmp[i++] = arr[k++];
            }
            for (i = 0; i < tmp.length; i++) {// 排好序的数组依次复制回arr
                arr[L + i] = tmp[i];
            }
        }
      
  • 堆排序

    • 前置知识:堆

      堆简单来说就是除根节点外的所有节点值必然不大于或不小于父节点值的完全二叉树结构,按照根节点是最大值或者最小值分为大根堆或小根堆,一般直接用数组实现堆并解决排序或者TopK等问题。

      1、下标0代表根节点,按照层序遍历的顺序,下标 i 节点的左右孩子节点下标依次为 2 x i + 1 和 2 x i + 2;


      2、以构建小根堆为例,每次将数据插入最后一个节点,再逐层与父节点值比较,如果小于父节点的值就与父节点交换,直到不再小于父节点或者已经位于根节点为止;


      3、堆还提供移除堆顶元素的操作,具体实现逻辑是先将堆顶元素与最后一个节点值交换,堆可用下标范围减1代表将末尾节点移除,然后从根节点开始逐层与孩子节点中的较小值进行交换,直到不再大于孩子结点值或者已经位于最后一层;

    • 步骤描述

      1、数组原地构建 大根堆 ,每次插入新元素时[0...i]就是堆有效范围;

      2、依次从构建好的堆中将堆顶最大值交换到堆末尾并且将有效范围减一再重新调整堆,最后实现数组元素从小到大排序。

    • 代码示例

        public void heapSort(int[] arr) {
            if (arr == null || arr.length < 2) {
                return;
            }
            for (int i = 0; i < arr.length; i++) {// 构造大根堆
                heapInsert(arr, i);
            }
            int heapSize = arr.length;
            while (heapSize > 0) {
                swap(arr, 0, heapSize - 1);// 堆的根节点与最后一个节点互换
                heapSize--;// 移除已经排好序的堆最后一个节点,准备重新调整堆
                heapify(arr, 0, heapSize);
            }
        }
      
        /**
        * 堆化:调整后的index位置元素是否需要重新调整恢复为堆
        */
        public void heapify(int[] arr, int index, int heapSize) {
            int left = index * 2 + 1;
            while (left < heapSize) {
                int largest = left + 1 < heapSize && arr[left + 1] > arr[left] ?
                        (left + 1) : left;// 左右孩子中最大值的节点下标
                largest = arr[largest] > arr[index] ? largest : index;// 最大孩子节点与父节点之间更大的下标
                if (largest == index) {// 孩子不大于父节点,结束
                    break;
                }
                // 父节点下移,继续向下判断
                swap(arr, index, largest);
                index = largest;
                left = index * 2 + 1;
            }
        }
      
        public void heapInsert(int[] arr, int index) {
            int parent = (index - 1) / 2;
            while (arr[index] > arr[parent]) {
                swap(arr, index, parent);
                index = parent;
                parent = (index - 1) / 2;
            }
        }
      

总结

算法 时空复杂度 稳定性 其它说明
插入排序 时间复杂度O(\(n^2\))
空间复杂度O(1)
稳定 原数组倒序时性能最差,原数组有序时性能最好能达到O(n)
选择排序 时间复杂度O(\(n^2\))
空间复杂度O(1)
稳定 最好或最坏情况下都是O(\(n^2\)),最朴素暴力的排序方式
冒泡排序 时间复杂度O(\(n^2\))
空间复杂度O(1)
稳定 中途某轮遍历不存在交换时可以提前结束,最好情况下能达到O(n)
快速排序 时间复杂度O(n\(log_2\)n)
空间复杂度O(\(log_2\)n)
不稳定 性能提升两个关键点在于随机抽样和三路快排
归并排序 时间复杂度O(n\(log_2\)n)
空间复杂度O(n\(log_2\)n)
稳定
堆排序 时间复杂度O(n\(log_2\)n)
空间复杂度O(1)
不稳定 理解堆结构的构建与调整过程是关键

posted on 2024-01-28 22:32  真不如烂笔头  阅读(10)  评论(0编辑  收藏  举报

导航