排序算法(CompareTo 的实现)

  • 对应算法(P153 开始)

    一、选择排序法:

    遍历数组,每次把数组中最小的放到前面,最后完成排序。

    (1)在使用泛型时,要说明泛型实现 Comparable 接口

    1. public static <E extends Comparable> sort(E[] arr)

对于 compareTo() 返回 int 类型, -1(<) , 0 (=) ,1 (>)

(2)类在实现接口时,需要说明类型

  1. public class Student implements Comparable<Student>

(3)ToString 方法

  1. String.format(); // 将 String 格式化
  2. // 也可以使用 StringBuilder
  3. @Override
  4. public String toString(){
  5. StringBuilder res = new StringBuilder();
  6. res.append(anotherString);
  7. return res.toString();
  8. }

(4)实现接口以及泛型后的选择排序算法

  1. public static <E extends Comparable<E>> void sort(E[] arr) {
  2. for (int i = 0; i < arr.length; i++) {
  3. int minIndex = i;
  4. for (int j=i;j<arr.length;j++){
  5. if(arr[j].compareTo(arr[minIndex])<0){
  6. minIndex = j;
  7. }
  8. }
  9. swap(arr, i , minIndex);
  10. }
  11. }
  12. private static <E> void swap(E[] arr, int i , int j){
  13. E t = arr[i];
  14. arr[i] = arr[j];
  15. arr[j] = t;
  16. }

1.复杂度分析:

对于二重循环,复杂度为 O((n+1)*n) => O(n^2)

2.在进行排序时,需要验证排序是否成功,需要写一个 Helper 进行验证

  1. public static <E extends Comparable<E>> Boolean isSorted(E[] arr) {
  2. for (int i = 1; i < arr.length; i++) {
  3. if (arr[i - 1].compareTo(arr[i]) > 0) {
  4. return false;
  5. }
  6. }
  7. return true;
  8. } // 测试是否正常排序

3.将测试方法进行封装,可以利用反射来对应不同的排序方法

  1. public static <E extends Comparable<E>> void sortTest(String sortName, E[] arr) {
  2. long startTime = System.nanoTime();
  3. if (sortName.equals("SelectionSort")) // 可以用反射来写
  4. SelectionSort.sort(arr);
  5. long endTime = System.nanoTime();
  6. double time = (endTime - startTime)*1.0 / 1000000000;
  7. if (!SortingHelper.isSorted(arr)) { // throw Exception
  8. System.out.println("failed");
  9. throw new RuntimeException("未正确排序");
  10. }
  11. System.out.println(String.format("%s ,n = %d : %f s", sortName, arr.length, time));
  12. }

二、插入排序

插入排序,每次把现在的那个数与前面的数进行比较,然后交换,最后遍历完所有index, 即可完成排序。

  1. eg:
  2. /// 1 4 3 5 7
  3. //第一次: 1 < 3 1 4 3 5 7
  4. // 第二次: 4 < 3 1 3 4 5 7 然后再第二次时,还要比较 3 与 1 若 更小, 则再次交换

未优化

  1. public static <E extends Comparable<E>> void sort(E[] arr) {
  2. for (int i = 0; i < arr.length; i++) {
  3. // 选择 index = i 上的数
  4. // 再遍历 j = 0 到 j = i 处的数,假如更小,则交换
  5. for (int j = i; j > 0 && arr[j].compareTo(arr[j - 1]) < 0; j--) {
  6. swap(arr, j, j - 1);
  7. }
  8. }
  9. }
  10. public static <E extends Comparable<E>> void swap(E[] arr, int i, int j) {
  11. E temp = arr[i];
  12. arr[i] = arr[j];
  13. arr[j] = temp;
  14. }

优化(空间复杂度)

swap 的 赋值所需的空间复杂度比 CompareTo(o) 所需要的高

  1. public static <E extends Comparable<E>> void sort2(E[] arr) {
  2. for (int i = 0; i < arr.length; i++) {
  3. // 获取 index = i 处的值
  4. E t = arr[i];
  5. int j ;
  6. //假如 arr[j] < arr[j-1] , 将 arr[j-1] 后移给 arr[j],j--;
  7. // 因为每次排序,index = i - 1 时都已经排好
  8. // 所以 若 t.compare(arr[j-1]) > 0, 在 就之前的数已经完成排序
  9. // j 位置的数字就是t;
  10. for(j = i; j> 0 && t.compareTo(arr[j-1]) < 0;j--){
  11. arr[j] = arr[j-1];
  12. }
  13. arr[j] = t;
  14. }
  15. }

重要特性

  • 循环终止条件,内层循环提前终止的机制。
  • 对于有序数组,插入排序法的复杂度为O(n)。
  • 对于插入算法,平均状态下仅需要 n^2/4 ,但是赋值过程比比较过程运行速度慢,因此可能性能更好。
  • 比较二者,对于大部分排序比较好的情况,比如银行,使用插入排序比选择的遍历次数要更少,运行速度就更快。

因为选择排序始终需要遍历n^2次,而假如是插入排序,最好情况下仅需要O(n) 次。

三、归并排序(分治思想)

  • 分治思想就是把 较大规模的问题,不断分为更小规模的问题进行处理,再将处理结果进行合并。从而获得大规模问题的结果。

image.png

  • 排序过程:将两个区间进行排序 [l,mid] 和 [mid, r]区间

image.png

  • 归并过程:使用两个 index , i 与 j ,分别对[l, mid] 与 [mid + 1, r] 进行排序。从而将[l,r]数组进行合并的过程,称为合并过程。
  • 为非原地排序

    1.实现代码

    ```java // 分别对 [l, mid] 和 [mid ,r] 进行排序 public E[] sort(E[] arr){ E[] temp = Arrays.copyOfRange(arr,0,arr.length); return mergeSort(arr, 0 , arr.length - 1 , temp); }

public E[] mergeSort(E[] arr, int l, int r, E[] temp){ // 当 l >= r 时,数组中[l,r]仅一个元素,无需排序 int mid = l + (r - l) /2; // 防止整型溢出 if(l < r){
mergeSort(arr, l, mid,temp); mergeSort(arr, mid+1, r,temp); merge(arr, l, mid, r,temp); } return arr; }

public void merge(E[] arr, int l, int mid ,int r, E[] temp){ System.arraycopy(arr,low,temp,low,r-l+1); //复制arr[l,r]因为每次进行合并后,arr都会变化 // 创建 i, j 下标,对[l,mid] [mid+1,r] 进行遍历 // 创建 k 下标,遍历 temp[k] 数组 int i = l, j = mid + 1; for(int k = l; k <= r; k++){ // 当 i > mid 时,直接将 arr[j] 放入temp 中 // 要先判断,是因为如果不在前面判断的话,容易和后面发生冲突 if(i > mid){ temp[k] = arr[j]; j++; }else if(j > r){ temp[k] = arr[i]; i++; }else if(arr[i].compareTo(arr[j]) < 0){ temp[k] = arr[i]; i++ }else if(arr[i].compareTo(arr[j]) >= 0){ temp[k] = arr[j]; j++; } } // 将 temp中排好的序,赋值给 arr for(int k = l; k <= r; k++){ arr[k] = temp[k]; } }

  1. <a name="OMPje"></a>
  2. ### 2.使用插入排序对较小的数组进行优化
  3. 当 r - l < 16 使用 插入排序进行排序可能会更快
  4. ```java
  5. // merge 部分不变
  6. // 更改 mergeSort() 方法
  7. public E[] mergeSort(E[] arr, int l, int r){
  8. // 当 l >= r 时,数组中[l,r]仅一个元素,无需排序
  9. int mid = l + (r - l) /2; // 防止整型溢出
  10. if(r - l < 16){
  11. inseration(arr, l ,r);
  12. return arr;
  13. }
  14. mergeSort(arr, l, mid);
  15. mergeSort(arr, mid+1, r);
  16. merge(arr, l, mid, r);
  17. }

3.使用递归条件进行优化

  • 当 arr[mid] < arr[mid + 1] 时, 由于两侧均是有序的,所以如果成立,则不需要再次进行 merge 合并排序

    public E[] mergeSort(E[] arr, int l, int r){
      // 当 l >= r 时,数组中[l,r]仅一个元素,无需排序
      int mid = l + (r - l) /2; // 防止整型溢出
      if(r - l < 16){
          inseration(arr, l ,r);
          return arr;
      }
      mergeSort(arr, l, mid); 
      mergeSort(arr, mid+1, r);
      if(arr[mid].compareTo(arr[mid+1]) < 0){
          merge(arr, l, mid, r);
      }
    }
    

    4.复杂度分析

    image.png
    越往下一层 n 的规模就降低为 n / 2, 进行 比较需要 n / 2 次,进行赋值需要 n / 2次
    O = O( n ) + 2O( n / 2) + 4O( n / 4 ) 进行等比数列相加
    最终获得时间复杂度为 O(nlogn);

  • 对于有序数组,由于不需要 merge , 故 时间复杂度为 O(n);

    5.自底向上实现归并排序

    同样使用 以上的三种优化方式

    // 自底向上进行排序
    // 非递归实现
    public void mergeSort(E[] arr, int l , int r){
      // 对 size 不断增加,从而规定 [l,mid] [mid+1,r]的范围,再进行 merge
      // 将所有数组, 划分为 [left, left + 15] [left+16,left+31]这样的数组,再进行merge
      for(int i = left; i < r ; i+=16){
          // 这里要要判断是否越界
          // 假如取 i = r, 则 inseration(i,i) 没有必要
          inserationSort(arr, i , Math.min(i + 15, r));
          return;
      }
    
      // 每次 size 都需要翻倍
      for(int size = 16; size < r - l + 1; size +=size){
          // 每次 merge 范围 [i , i + size, i +size + size - 1];
          for(int i = left; i + size < r; i += size + size)
              if(arr[i + size - 1].compareTo(arr[i + size]) < 0){
                  // i + size < r 因为如果 >= r 则 merge 无法继续进行
                  // 同理 size < r-l +1 ,这样 r 作为右边界进行 merge, mid 不会等于 r
                  merge(arr, i, i + size -1, Math(i+size+size -1 ,right));
              }
      }
    }
    

    归并相关问题:leetcode Offer.51
    https://leetcode-cn.com/problems/shu-zu-zhong-de-ni-xu-dui-lcof/ 数组中的逆序对
    image.png

    class Solution{
      // 每次进行排序时,由于 两部分的数组都是有序的,所以只需要考虑 arr[mid+1, r] 部分
      // arr[mid+1]每在 arr[i] 前排入一个,就产生 mid - i + 1 个逆序数对
      public int reversePairs(int[] nums){
          int[] temp = new int[nums.length];
          return sort(nums, 0 , nums.length - 1);
      }
    
      private int sort(int[] nums, int l, int r, int[] temp){
          if(l < r){
              int mid = l + (r - l) / 2;
              int res = 0;
              res += sort(nums, l, mid,temp);
              res +=sort(nums, mid + 1, r,temp);
              if(nums[mid] > nums[mid + 1]){
                  res +=merge(nums, l, mid, r, temp); 
              }
              return res;
          }
          return 0;
      }
    
      private int merge(int[] nums, int l, int mid, int r,int[] temp){
          int res = 0;
          int i = l, j = mid + 1;
          for(int k = l; k <= r; k++){
              if(i > mid){
                  temp[k] = nums[j];
                  j++;
              }
              else if(j > r){
                  temp[k] = nums[i];
                  i++;
              }
              else if(nums[j].compareTo(nums[i]) < 0){
                  temp[k] = nums[j]
                  res += mid - i + 1;
                  j++;
              }else{
                  temp[k] = nums[i];
                  i++;
              }
          }
          return res;
      }
    }
    

    image.png
    这里我们使用递归进行实现 ```java // 合并 arr[l, mid) 和 arr[mid, r) private void merge(int[] arr, int l ,int mid, int r, int[] aux){ System.arraycopy(arr,l , aux, l , r - l); // 因为取不到 r, 所以长度为 r - l int i = l, j = mid; int k = l; for(; k < r; k++){

      if( i >= mid ){
          aux[k] = arr[j];
          j++;
      }else if( j >= r ){
          aux[k] = arr[i];
          i++;
      }else if(arr[j] < arr[i]){
          aux[k] = arr[j];
          j++;
      }else{
          aux[k] = arr[i];
          i++;
      }
    

    } for(k = l; k < r; k++){

      arr[k] = aux[k];
    

    } }

// 对arr[l,r) 进行归并排序 // 说明 l < r - 1 // 因为当 l = r - 1 时,循环结束 private void sort(int[] arr, int l, int r, int[] temp){ if(l >= r -1 ){ return ; } int mid = l + (r - l) / 2; sort(arr, l ,mid, temp); sort(arr, mid, r, temp); if(arr[mid - 1] > arr[mid]){ merge(arr, l , mid , r, temp); } }

<a name="aUI3Y"></a>
## (**)四、快速排序:

- 快速排序的基本思想也是分治法

![image.png](https://cdn.nlark.com/yuque/0/2021/png/190166/1635522791144-bcff63e6-c8a1-4b86-88b9-e86e2854acfc.png#clientId=u2386c5ce-2df8-4&crop=0&crop=0&crop=1&crop=1&from=paste&height=687&id=u59aba581&margin=%5Bobject%20Object%5D&name=image.png&originHeight=502&originWidth=849&originalType=binary&ratio=1&rotation=0&showTitle=false&size=99834&status=done&style=none&taskId=u607a76b2-f475-4c00-af70-c51d0b6c698&title=&width=1162.5)

- **target 为 头部第一个元素**
- **遍历将 小于 target 放到前面, 将大于 target 的放到后面**
- **把最后的 头部元素 和 小于 target 的最后元素互换,完成排序**
- **然后再次分别对前后的元素进行排序**
<a name="GnFEU"></a>
#### 一、partition 基本实现:
```java
public void QuickSort(int[] arr, int l, int r){
    if(l < r){
        int p = partition(arr, l ,r);
        QuickSort(arr,l,p-1);
        QuickSort(arr,p+1,r);
    }
    return;
}

public int partition(int[] arr, int l, int r){
    int target = arr[l];
    int i = l, int j = l + 1;
    while(j <= r){
        if(arr[j] < target){
            i++;
            swap(arr, i, j);
        }
        j++;
    }
       swap(arr,l,i);
    return i;
}

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

- 随机化优化:

当数组大部分为有序的数组时,算法退化为o(n^2) 级别

// 1  2  3  4  5  6 = arr
int p = partition(0,5,arr);
p = 0;
sort(arr,0,0); = > return arr[0,0] = > 1
sort(arr, 1,5); => int p = partition(2, 5, arr) => 1; => sort(arr, 0, 0)
                                                    = > sort(arr,2,5)
// 可以发现每个数都需要进行依次遍历, 而且是按从左到右依次排序
// 每进行一次 partition 都需要进行 n 次比较,一共有 n 个数, 故退化为 O(n^2) 级别算法

添加随机化,让 p 不是每次都是从左向右,从而防止退化为 O(n^2) 级别

// 将 random 对象传入,获取一个随机数,与 l 处的数进行交换
public int partition(int[] arr, int l, int r, Random rnd){
    // 添加随机化
    int p = rnd.nextInt(r- l + 1) + l; // .nextInt() 表示取 0 ~ r - l  内的数, 加上 l 就包括了整个范围 
    swap(arr, p, l); //进行交换

    int target = arr[l];
    int i = l, int j = l + 1;
    while(j <= r){
        if(arr[j] < target){
            i++;
            swap(arr, i, j);
        }
        j++;
    }
       swap(arr,l,i);
    return i;
}

- 对于较小的数组,使用插入排序进行优化

就像之前的归并排序一样,对于较小的数组,使用插入排序速度可能会更快

// 更改调用递归的部分
public void QuickSort(int[] arr, int l, int r){
    if( r - l <=16)
        inserationSort(arr, l , r);
        return;
    }else if( l < r )  {     
        int p = partition(arr, l ,r);
        QuickSort(arr,l,p-1);
        QuickSort(arr,p+1,r);
    }
}

-使用双路排序

对于数组中所有元素都相同的情况,算法将会退化为 O ( n ^ 2) 级别

// 0 0 0 0 0 0  arr
// 假如使用原来的算法 
int p = (arr, l ,r) = > int p = rand; swap(arr, p, l); 
// 对于相同的元素,负责对元素最终位置进行记录的 i 不会移动, 即数组的元素不会互换
//  p = 0;
QuickSort(arr, l , -1) => if( l > r) return ;
QuickSort(arr, 1, r) ==>  int p = 1 => QuickSort(arr,1,0) => return
                                    => QuickSort(arr,2,r)
每次都返回一个 arr[0,0] 与 arr[i,r]的数组Sort
O ( n ) = n + (n-1) + ... + 1 = O(n(n+1)/2) = O(n^2)


// 故使用双路排序,减少循环
// 循环下标,分别从两端过来,当相等时,结束循环
public int partition(int[] arr, int l, int r){
    // 添加随机化
    int p = rnd.nextInt(r- l + 1) + l; // .nextInt() 表示取 0 ~ r - l  内的数, 加上 l 就包括了整个范围 
    swap(arr, p, l); //进行交换

    int i = l, j = r;
    while(i < j){
        if(arr[j] < arr[l]){
            i++;
            swap(arr,i,j);
        }else{ // arr[j] >= arr[l]
            j--;
        }
    }
    swap(arr, i, l);
    return i;
}

-使用三路排序

对于上面所有元素都相同的情况,实际上经过第一次排序后,就能确定都为完全一致的元素,不需要再次排序

// 注意不要从后往前进行循环, 因为 对于 当 arr[j] == target; 需要进行 swap(),但是此时无法确定 arr[k] 是否 == target 或者小于 target 
public int partition(int[] arr, int l, int r){ 
    // 添加随机化
    int p = rnd.nextInt(r - l + 1) + l; // .nextInt() 表示取 0 ~ r - l  内的数, 加上 l 就包括了整个范围 
    swap(arr, p, l); //进行交换

       // 数组分为 arr[l, i - 1] arr[i , j] arr[j+1, r]
    // 当 k == j时,循环结束
    // 开始情况下, 应该为三个空数组
    int i = l , k = l + 1 , j = r;
    while( k < j ){
        if(arr[k] < arr[l]){
            i++;
            swap(arr, i, k);
            k++
        }else if( arr[k] == arr[l]){
            k++;
        }else{ // arr[k] > arr[l]
            swap(arr,j,k);
            j--;
        }
    }

image.png

  • 确定好区间以及循环不变量,要分别考虑多种情况

    二、复杂度分析;

    对于快速排序算法,实际上为一种不稳定算法,需要不断的进行优化,最后优化为三路排序算法。
    对于这类将问题拆解为更小范围的问题,可以用数来理解,当一侧没有元素,而另一侧元素较多时,算法就会退化。
    ////        Sort[ arr, l , r ]
    ///  Sort[arr, l, l]    Sort[arr, l+1, r]
    
    此时算法时间复杂度为: 每次进行一次操作 都需要对 n - 1 个数进行遍历
    O( n ) = O ( n ^ 2 / 2 ) = O( n ^2)

在理想状况下 , 每次都分为 n / 2 的理想状况

///              Sort[arr, l, r]
///    Sort[arr, l, l + (r - l) / 2]   Sort[arr, mid + 1, r]

O ( n ) = 2O(n/2) + n / 2 = 2 ( 2 O( n /4) + 4/ n ) + n/2 == O(n logn) 级别
但是一般情况下,还是复杂度接近 O ( n log n ) 级别

三、相关的 leetcode 问题

image.png

// 可以使用 快速排序的三路排序, 但是此时 target 为固定值
// 注意原地排序
// [l, i - 1] [i , j - 1] [j, r]
class Solution {
    public void sortColors(int[] nums) {
        int i = -1, j = nums.length, k = 0;
        while( k < j ){
          if(nums[k] < 1){
              i++;
            swap(nums, i, k);
            k++;
          }else if (nums[k] == 1){
              k++;
          }else{
               j--;
              swap(nums, j, k);
          }   
        }
    }

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

}

https://leetcode-cn.com/problems/zui-xiao-de-kge-shu-lcof/ 最小的k个数
image.png

// 每次进行排序时, partition 所返回的索引,就是该数在其中的位置
// 前k个数,则为 0 ~ k - 1, 即当 p = k - 1 时,返回 arr, 取前 0 ~ k-1 即可
class Solution{
    public int[] getLeastNumber(int[] arr, int k){
        return Arrays.copyOfRange(arr, 0, k - 1);
    }


    public void sort (int[] arr, int l, int r, Random rnd, int target){
        if(l >= r) return ;
        int p = l + rnd.nextInt(r - l + 1);
        swap(arr, p , l);

        int j = l, i = l + 1, k = r + 1;
        while( i < k ){
            if( arr[i] > arr[l]){
                j++;
                swap(arr, j, i);
                i++;
            }else if(arr[i] == arr[l]){
                i++;
            }else{
                k--;
                swap(arr, i ,k);
            }
        }
        swap(arr, l , i)
        if( i == target - 1){
            return ;
        }
        sort(arr, l, i - 1, rnd ,target);
        sort(arr, j+1,r, rnd ,target);
    }

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

}