算法学习系列(三):排序算法

65 阅读6分钟

工具类

快速测试,不用自己造数据,就可以验证算法是否正确

/**
 * 对数器,自动测试排序算法是否正确
 */
public class SortUtils {
    /**
     * 生成随机大小+随机数字的数组
     * @param maxSize 数组最大值
     * @param maxValue 数字最大值
     * @return
     */
    public static int[] generateRandomArray(int maxSize, int maxValue){
        //Math.random() -> [0,1) 所有小数,等概率返回一个
        //Math.random() * N -> [0,N) 所有小数,等概率返回一个
        //(int)(Math.random() * N) -> [0,N-1] 所有整数,等概率返回一个
        int arrLength = 0;
        //避免生成空数组
        while(arrLength == 0){
            arrLength = (int)((maxSize + 1) * Math.random());
        }
        int[] arr = new int[arrLength];
        for (int i = 0; i < arr.length; i++) {
            //无所谓正数还是负数
            arr[i] = (int)((maxValue + 1) * Math.random()) - (int)(maxValue * Math.random());
        }
        return arr;
    }

    /**
     * 复制一份数组
     * @param arr
     * @return
     */
    public static int[] copyArray(int arr[]){
        if(arr == null){
            return null;
        }
        int[] res = new int[arr.length];
        for (int i = 0; i < res.length; i++) {
            res[i] = arr[i];
        }
        return res;
    }

    /**
     * 判断两个数组是否相等
     * @param arr
     * @param res
     * @return
     */
    public static boolean isEquals(int[] arr, int[] res){
        boolean equals = true;
        for (int i = 0; i < arr.length; i++) {
            if(arr[i] != res[i]){
                equals = false;
                break;
            }
        }
        return equals;
    }

    /**
     * 使用异或运算,交换数字位置
     * @param arr
     * @param i
     * @param j
     */
    public static void swap(int[] arr, int i, int j){
        if(i == j){
            return;
        }
        arr[i] = arr[i]^arr[j];
        arr[j] = arr[i]^arr[j];
        arr[i] = arr[i]^arr[j];
    }

    /**
     * 打印数组
     * @param arr
     */
    public static void printArray(int[] arr){
        for (int i = 0; i < arr.length; i++) {
            System.out.print(arr[i]);
        }
    }
}

选择排序

遍历0~N项,选取第i大的数,放在i的位置上
时间复杂度:最差:O(n2):O(n^2)

import java.util.Arrays;

/**
 * 选择排序
 * 遍历0~N项,选取第i大的数,放在i的位置上
 */
public class SelectSort{

    public static void main(String[] args) {
        //测试比较次数
        int compareTimes = 1000;
        boolean isSuccess = true;
        for (int i = 0; i < compareTimes; i++) {
            //生成随机数组
            int[] arr = SortUtils.generateRandomArray(100, 100);
            //复制一份
            int[] arrCopy = SortUtils.copyArray(arr);
            SortUtils.printArray(arr);
            //使用选择排序
            sort(arr);
            SortUtils.printArray(arr);
            //系统自带排序
            Arrays.sort(arrCopy);
            SortUtils.printArray(arrCopy);
            if(!SortUtils.isEquals(arr, arrCopy)){
                isSuccess = false;
                break;
            }
        }
        System.out.println(isSuccess ? "success" : "fail");
    }

    /**
     * 选择排序
     */
    public static void sort(int[] arr){
        for(int i = 0; i < arr.length; i++){
            //当前最大值的位置
            int maxIndex = i;
            for(int j = i + 1; j < arr.length; j++){
                //大于小于号用来控制是正序还是倒序
                maxIndex = arr[maxIndex] < arr[j] ? maxIndex : j;
            }
            //交换
            SortUtils.swap(arr, maxIndex, i);
        }
    }

}

冒泡排序

时间复杂度:最差:O(n2):O(n^2)

import java.util.Arrays;

public class BubbleSort {
    public static void main(String[] args) {
        //测试比较次数
        int compareTimes = 1000;
        boolean isSuccess = true;
        for (int i = 0; i < compareTimes; i++) {
            //生成随机数组
            int[] arr = SortUtils.generateRandomArray(100, 100);
            //复制一份
            int[] arrCopy = SortUtils.copyArray(arr);
            SortUtils.printArray(arr);
            //使用选择排序
            sort(arr);
            SortUtils.printArray(arr);
            //系统自带排序
            Arrays.sort(arrCopy);
            SortUtils.printArray(arrCopy);
            if(!SortUtils.isEquals(arr, arrCopy)){
                isSuccess = false;
                break;
            }
        }
        System.out.println(isSuccess ? "success" : "fail");
    }
    
    /**
     * 冒泡排序
     * @param arr
     */
    public static void sort(int[] arr){
        for(int i = 0; i < arr.length; i++){
            for (int j = i+1; j < arr.length; j++) {
                if(arr[i] > arr[j]){
                    SortUtils.swap(arr, i, j);
                }
            }
        }
    }
}

插入排序

从后往前比较,到后面的数比前面的数大/小时结束这一轮比较。前面一定是有序的
时间复杂度:最差:O(n2):O(n^2),最好的时候是:O(n):O(n)
估计时间复杂度时,是去估计这个算法最差的情况

import java.util.Arrays;

/**
 * 插入排序
 * 从后往前比较,到后面的数比前面的数大/小时结束这一轮比较。前面一定是有序的
 * 时间复杂度:最差O(n^2),最好的时候是O(n)
 * 估计时间复杂度时,是去估计这个算法最差的情况
 */
public class insertSort {
    public static void main(String[] args) {
        //测试比较次数
        int compareTimes = 1000;
        boolean isSuccess = true;
        for (int i = 0; i < compareTimes; i++) {
            //生成随机数组
            int[] arr = SortUtils.generateRandomArray(100, 100);
            //复制一份
            int[] arrCopy = SortUtils.copyArray(arr);
            SortUtils.printArray(arr);
            //使用选择排序
            sort(arr);
            SortUtils.printArray(arr);
            //系统自带排序
            Arrays.sort(arrCopy);
            SortUtils.printArray(arrCopy);
            if(!SortUtils.isEquals(arr, arrCopy)){
                isSuccess = false;
                break;
            }
        }
        System.out.println(isSuccess ? "success" : "fail");
    }

    /**
     * 插入排序
     * @param arr
     */
    public static void sort(int[] arr){
        for(int i = 1; i < arr.length ; i++){
            //j > 0为不越界
            //arr[j-1] < arr[j]结束条件,再前面的数就已经符合这个条件
            for(int j = i; j > 0 && arr[j-1] > arr[j]; j--){
                SortUtils.swap(arr, j, j-1);
            }
        }
    }
}

归并排序

使用递归算法,保证L-midmid-R有序,然后使用使用两个指针,将两个有序数组合并

import java.util.Arrays;

public class MergeSort{

    public static void main(String[] args) {
        //测试比较次数
        int compareTimes = 1000;
        boolean isSuccess = true;
        for (int i = 0; i < compareTimes; i++) {
            //生成随机数组
            int[] arr = SortUtils.generateRandomArray(100, 100);
            //复制一份
            int[] arrCopy = SortUtils.copyArray(arr);
            SortUtils.printArray(arr);
            //使用归并排序
            process(arr, 0 , arr.length -1);
            SortUtils.printArray(arr);
            //系统自带排序
            Arrays.sort(arrCopy);
            SortUtils.printArray(arrCopy);
            if(!SortUtils.isEquals(arr, arrCopy)){
                isSuccess = false;
                break;
            }
        }
        System.out.println(isSuccess ? "success" : "fail");
    }

    /**
     * 递归调用
     * @param arr
     * @param L
     * @param R
     */
    public static void process(int[] arr, int L, int R){
        if(L == R){
            return;
        }
        //计算中点
        int mid = L + ((R - L) >>1);
        //调用两次
        process(arr, L, mid);
        process(arr, mid + 1, R);
        merge(arr, L , mid, R);
    }

    /**
     * 合并两个数组
     * @param arr
     * @param L
     * @param M
     * @param R
     */
    public static void merge(int[] arr, int L, int M, int R){
        int[] help = new int[R - L + 1];
        int i = 0;
        int p1 = L;
        int p2 = M+1;
        while(p1 <= M && p2 <= R){
            help[i++] = arr[p1] <= arr[p2]? arr[p1++] : arr[p2++];
        }
        while(p1 <= M){
            help[i++] = arr[p1++];
        }
        while(p2 <= R){
            help[i++] = arr[p2++];
        }
        for (int j = 0; j < help.length; j++) {
            arr[L+j] = help[j];
        }
    }
}

时间复杂度,符合master公式:
a = 2, b= 2 //调用两次递归,将数据等分为两份
d = 1 //除了调用以外的时间复杂度为 O(N)
符合 logba=d\log_{b}{a} = d 时间复杂度为 O(NlogN)O(N{\log_{}{N}})

快排

1、荷兰国旗问题1

给定一个数组,把小于等于num的数放在左边,把大于num的数放在右边
分析:
1.如果当前指针指向的值小于等于5,与minPart的后一位交换,minPart++,指针向前
2.如果当前指针指向的值大于5,指针向前i++

/**
 * 荷兰国旗问题1 
 * @param arr 给定数组
 * @param num 划分值
*/
public static void heLanGuoQi1(int[] arr, int num) {
    //小于等于num的区域
    int minPart = -1;
    //指针
    int i = 0;
    //1.如果当前指针指向的值小于等于5,与minPart的后一位交换,minPart++,指针向前
    //2.如果当前指针指向的值大于5,指针向前i++
    while(i < arr.length){
        if(arr[i] <= num){
            SortUtils.swap(arr, minPart+1, i);
            minPart++;
        }
        i++;
    }
    SortUtils.printArray(arr);
}

2、快排1.0

根据上面的荷兰国旗问题,取数组最后一个值作为num,进行递归处理,得到快排1.0版本

import java.util.Arrays;

public class QuickSort {
    
    public static void main(String[] args) {
        //测试比较次数
        int compareTimes = 1000;
        boolean isSuccess = true;
        for (int i = 0; i < compareTimes; i++) {
            //生成随机数组
            int[] arr = SortUtils.generateRandomArray(100, 100);
            //复制一份
            int[] arrCopy = SortUtils.copyArray(arr);
            SortUtils.printArray(arr);
            //使用快速排序
            quickSort(arr, 0, arr.length-1);
            SortUtils.printArray(arr);
            //系统自带排序
            Arrays.sort(arrCopy);
            SortUtils.printArray(arrCopy);
            if(!SortUtils.isEquals(arr, arrCopy)){
                isSuccess = false;
                break;
            }
        }
        System.out.println(isSuccess ? "success" : "fail");
    }

    public static void quickSort(int[] arr, int L, int R) {
        if(L < R){
            int minPart = partition(arr, L, R);
            quickSort(arr, L, minPart - 1);
            quickSort(arr, minPart + 1, R);
        }
    }

    //按荷兰国旗问题1处理数据
    public static int partition(int[] arr, int L, int R) {
        int num = arr[R];
        int minPart= L - 1;
        for (int i = L; i <= R ; i++) {
            if(arr[i] <= num){
                SortUtils.swap(arr, minPart + 1, i);
                minPart++;
            }
        }
        //最终停留为是:小于num的最后一个值
        return minPart;
    }
}

3、荷兰国旗问题2

给定一个数组,把小于num的数放在左边,把等于num的数放在中间,把大于num的数放在右边
1.如果当前指针指向的值小于5,与minPart的后一位交换,minPart++,i++
2.如果当前指针指向的值等于5,i++
3.如果当前指针指向的值大于5,与maxPart的前一位交换,maxPart--,i不变

public static void heLanGuoQi2(int[] arr, int num) {
    //小于等于num的区域
    int minPart = -1;
    int maxPart = arr.length;
    //指针
    int i = 0;
    //1.如果当前指针指向的值小于5,与minPart的后一位交换,minPart++,i++
    //2.如果当前指针指向的值等于5,i++
    //3.如果当前指针指向的值大于5,与maxPart的前一位交换,maxPart--,i不变
    while(i < maxPart){
        if(arr[i] < num){
            SortUtils.swap(arr, minPart+1, i);
            minPart++;
            i++;
        }else if(arr[i] == num){
            i++;
        }else{
            SortUtils.swap(arr, maxPart-1, i);
            maxPart--;
        }
    }
    SortUtils.printArray(arr);
}

4、快排2.0

根据上面的荷兰国旗问题,取数组最后一个值作为num,进行递归处理,得到快排2.0版本。会比1.0效率高一些,因为一次会排好一批数据

import java.util.Arrays;

public class QuickSort {
    
    public static void main(String[] args) {
        //测试比较次数
        int compareTimes = 1000;
        boolean isSuccess = true;
        for (int i = 0; i < compareTimes; i++) {
            //生成随机数组
            int[] arr = SortUtils.generateRandomArray(100, 100);
            //复制一份
            int[] arrCopy = SortUtils.copyArray(arr);
            SortUtils.printArray(arr);
            //使用快速排序
            quickSort(arr, 0, arr.length-1);
            SortUtils.printArray(arr);
            //系统自带排序
            Arrays.sort(arrCopy);
            SortUtils.printArray(arrCopy);
            if(!SortUtils.isEquals(arr, arrCopy)){
                isSuccess = false;
                break;
            }
        }
        System.out.println(isSuccess ? "success" : "fail");
    }

    public static void quickSort(int[] arr, int L, int R) {
        if(L < R){
            int[] part = partition(arr, L, R);
            quickSort(arr, L, part[0]);
            quickSort(arr, part[1], R);
        }
    }

    //按荷兰国旗问题2处理数据
    public static int[] partition(int[] arr, int L, int R) {
        int num = arr[R];
        int minPart= L - 1;
        int maxPart= R + 1;
        int i = L;
        while (i < maxPart) {
            if(arr[i] < num){
                SortUtils.swap(arr, ++minPart, i++);
            }else if(arr[i] == num){
                i++;
            }else{
                SortUtils.swap(arr, --maxPart, i);
            }
        }
        return new int[]{minPart, maxPart};
    }
}

5、快排3.0

不管是1.0还是2.0版本,在遇到最差的情况的时候,时间复杂度都是O(N2)O(N^2)
例如对{1,2,3,4,5,6,7,8,9}进行倒序排序。
快排3.0的优化方式为,随机选取一个值,放到最后,作为num值,然后进行排序

import java.util.Arrays;

public class QuickSort {
    
    public static void main(String[] args) {
        //测试比较次数
        int compareTimes = 1000;
        boolean isSuccess = true;
        for (int i = 0; i < compareTimes; i++) {
            //生成随机数组
            int[] arr = SortUtils.generateRandomArray(100, 100);
            //复制一份
            int[] arrCopy = SortUtils.copyArray(arr);
            SortUtils.printArray(arr);
            //使用快速排序
            quickSort(arr, 0, arr.length-1);
            SortUtils.printArray(arr);
            //系统自带排序
            Arrays.sort(arrCopy);
            SortUtils.printArray(arrCopy);
            if(!SortUtils.isEquals(arr, arrCopy)){
                isSuccess = false;
                break;
            }
        }
        System.out.println(isSuccess ? "success" : "fail");
    }

    public static void quickSort(int[] arr, int L, int R) {
        if(L < R){
            int[] part = partition(arr, L, R);
            quickSort(arr, L, part[0]);
            quickSort(arr, part[1], R);
        }
    }

    //按荷兰国旗问题2处理数据
    public static int[] partition(int[] arr, int L, int R) {
        //随机选取一个值,作为num值,使时间复杂度为O(nlogn)
        int point = L + (int)((R-L+1) * Math.random());
        SortUtils.swap(arr, point, R);
        int num = arr[R];
        int minPart= L - 1;
        int maxPart= R + 1;
        int i = L;
        while (i < maxPart) {
            if(arr[i] < num){
                SortUtils.swap(arr, ++minPart, i++);
            }else if(arr[i] == num){
                i++;
            }else{
                SortUtils.swap(arr, --maxPart, i);
            }
        }
        return new int[]{minPart, maxPart};
    }
}

注:因为多了下面两行代码,所以时间复杂度是O(NlogN)O(N{\log_{}{N}})
int point = L + (int)((R-L+1) * Math.random());
SortUtils.swap(arr, point, R);

堆排序

1、完全二叉树

一棵深度为k的有n个结点的二叉树,对树中的结点按从上至下、从左到右的顺序进行编号,如果编号为i(1≤i≤n)的结点与满二叉树中编号为i的结点在二叉树中的位置相同,则这棵二叉树称为完全二叉树。

2、大根堆

任意一颗子树中,头结点的值,比子节点的值大

  • 确定i位置的左孩子:2 * i + 1
  • 确定i位置的右孩子:2 * i + 1
  • 确定i位置的父:( i - 1 ) / 2

heapInsert:往大根堆末尾插入一个值,或者把index位置的值替换成一个比原值大的数,并重建的过程。 比较index上的值,是否比父大。如果比父小,结束。否则交换并重复执行

/**
 * 向大根堆插入值后,构建新的大根堆
 * @param arr
 * @param index
 */
public static void heapInsert(int[] arr, int index){
    //如果当前节点大于父节点
    while(arr[index] > arr[(index - 1) / 2]){
        //交换
        SortUtils.swap(arr, index, (index - 1) / 2);
        //当前节点等于父节点
        index = (index - 1) / 2;
    }
}

heapify:把index位置的值替换成一个比原值小的数,并重建的过程 左右孩子比较,获得最大值; index的值与最大值比较,如果index的值比较大,结束。否则交换并重复执行

/**
 * 
 * @param arr
 * @param index 任意节点
 * @param heapSize 堆大小
 */
public static void heapify(int[] arr, int index, int heapSize){
    //左孩子
    int left = 2 * index + 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){
            //找到了index应该在的位置,结束循环
            break;
        }
        //交换
        SortUtils.swap(arr, index, largest);
        index = largest;
        left = 2 * index + 1;
    }
}

堆排序:

  • 使用heapInsert构建一个大根堆
  • 把最大值(根节点),与最后一位交换。执行heapify
import java.util.Arrays;

public class HeapSort {
    
    public static void main(String[] args) {
        //测试比较次数
        int compareTimes = 1000;
        boolean isSuccess = true;
        for (int i = 0; i < compareTimes; i++) {
            //生成随机数组
            int[] arr = SortUtils.generateRandomArray(100, 100);
            //复制一份
            int[] arrCopy = SortUtils.copyArray(arr);
            SortUtils.printArray(arr);
            //使用快速排序
            heapSort(arr);
            SortUtils.printArray(arr);
            //系统自带排序
            Arrays.sort(arrCopy);
            SortUtils.printArray(arrCopy);
            if(!SortUtils.isEquals(arr, arrCopy)){
                isSuccess = false;
                break;
            }
        }
        System.out.println(isSuccess ? "success" : "fail");
    }

    public static void heapSort(int[] arr){
        int heapSize = 1;
        while(heapSize < arr.length){
            heapInsert(arr, heapSize++);
        }
        while(heapSize > 0){
            SortUtils.swap(arr, 0, heapSize - 1);
            heapify(arr, 0, --heapSize);
        }
    }

     /**
      * 向大根堆插入值后,构建新的大根堆
      * @param arr
      * @param index
      */
     public static void heapInsert(int[] arr, int index){
        //如果当前节点大于父节点
        while(arr[index] > arr[(index - 1) / 2]){
            //交换
            SortUtils.swap(arr, index, (index - 1) / 2);
            //当前节点等于父节点
            index = (index - 1) / 2;
        }
     }

     /**
      * 
      * @param arr
      * @param index 任意节点
      * @param heapSize 堆大小
      */
     public static void heapify(int[] arr, int index, int heapSize){
        //左孩子
        int left = 2 * index + 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){
                //找到了index应该在的位置,结束循环
                break;
            }
            //交换
            SortUtils.swap(arr, index, largest);
            index = largest;
            left = 2 * index + 1;
        }
     }
}