算法-排序算法(冒泡排序,选择算法,插入排序,希尔排序,快速排序,归并排序,基数排序,堆排序)

排序算法概述

  • 排序算法(Sort Algorithm)是将一组数据, 按指定的顺序进行排列的过程

排序的分类

  • 内部排序: 将数据加载到内部存储器中进行排序
  1. 选择排序: 简单选择排序, 堆排序
  2. 插入排序: 直接插入排序, 希尔排序
  3. 交换排序: 冒泡排序, 快速排序
  4. 归并排序
  5. 基数排序(桶排序)
  • 外部排序: 当数据量过大, 导致无法将数据全部加载到内存中时, 需借助外部存储进行排序

冒泡排序(Bubble Sort)

  • 排序过程: 从数组下标 0开始比较相邻元素, 当发现逆序时交换
  1. 共进行数组的大小-1次的循环
  2. 内循环的循环次数是跟着外循环递减
  3. 如果内循环中, 没有发生一次交换, 可以提现结束排序

public class BubbleSortApp {
    public static int[] setArray(int capacity) {
        int[] arr = new int[capacity];
        for (int i =0; i < capacity;i++) {
            arr[i] = (int) (Math.random() * capacity);
        }
        return arr;
    }

    /** 冒泡排序方法
     * 时间复杂度为平方阶 O(n2)*/
    public static void bubbleSort(int[] arr) {
        /** 用于转移值的临时变量*/
        int temp = 0;
        /** 用于标识交换过的循环*/
        boolean flag = false;
        for (int i = 0; i < arr.length - 1; i++) {
            for (int j = 0; j < arr.length - 1 - i; j++) {
                /** 循环判断邻着的元素*/
                if (arr[j] > arr[j + 1]) {
                    flag = true;
                    /** 转移值(交换)*/
                    temp = arr[j];
                    arr[j] = arr[j + 1];
                    arr[j + 1] = temp;
                }
            }

            System.out.println("i=" + i + ": " + Arrays.toString(arr));

            if (flag) {
                /** 有发生过交换, 则重置为 false*/
                flag = false;
            } else {
                /** 内部 for循环中未发生一次交换, 则结束排序*/
                break;
            }
        }
    }
    
    public static void main(String[] args) {
        int arr[] = {3, 9, 1, 10, 2};
        System.out.println("排序前 " + Arrays.toString(arr));
        /** 开始冒泡排序*/
        bubbleSort(arr);
    }
}

输出:
> 排序前 [3, 9, 1, 10, 2]
* 3>9=false	39 保持不动
* 9>1=true	19 交换
* 9>10=false	910保持不动
* 10>2=true	210交换
> i=0: [3, 1, 9, 2, 10]
* 3比较1
* 3比较9
* 9比较2
> i=1: [1, 3, 2, 9, 10]
* 1比较3
* 3比较2
> i=2: [1, 2, 3, 9, 10]
* 1比较2
> i=3: [1, 2, 3, 9, 10]


        /** 生成长度为 100000的随机值数组, 用于与其它算法对比排序效率*/
        int[] arr = setArray(100000);
        System.out.println("开始时间 " + DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss").format(LocalDateTime.now()));
        /** 开始冒泡排序*/
        bubbleSort(arr);
        System.out.println("结束时间 " + DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss").format(LocalDateTime.now()));

输出:
> 开始时间 2020-10-14 13:33:26
> 结束时间 2020-10-14 13:33:44

选择排序(Selection Sort)

  • 排序过程:
  1. 共进行数组的大小-1次的循环
  2. 每次外循环开始时假定当前值为最小值: min
  3. 内循环的开始下标1+i; 跟着外循环递增
  4. 在内循环中, 通过上面定义的 min, 依次往后的每个元素进行比较, 如发现有更小的值, 就重新确定最小值, 并得到下标
  5. 每次内循环结束后, 意味着得到本轮的最小值和下标
  6. 每次内循环结束后, 本轮最小值下标如有被改动过, 那么就进行原数组的数据交换

public class SelectionSortApp {
    public static int[] setArray(int capacity) {
        int[] arr = new int[capacity];
        for (int i =0; i < capacity;i++) {
            arr[i] = (int) (Math.random() * capacity);
        }
        return arr;
    }

    /** 选择排序方法
     * 时间复杂度为平方阶 O(n2)*/
    public static void selectionSort(int[] arr) {
        /** 假定的最小值*/
        int min = 0;
        /** 假定的最小值的元素下标*/
        int minIndex = 0;
        for (int i = 0; i < arr.length - 1; i++) {
            min = arr[i];
            minIndex = i;
            for (int j = i + 1; j < arr.length; j++) {
                /** 假定的最小值, 并不是最小*/
                if (min > arr[j]) {
                    /** 重置最小值和指定下标*/
                    min = arr[j];
                    minIndex = j;
                }
            }

            /** 内部 for循环的(j = i + 1)范围内, 再发生一次重新锁定的最小值的情况时*/
            if (minIndex != i) {
                /** 通过临时变量 min, 转移最小值的临时变量*/
                arr[minIndex] = arr[i];
                arr[i] = min;
            }

            System.out.println("i=" + i + ": " + Arrays.toString(arr));
        }
    }

    public static void main(String[] args) {
        int arr[] = {3, 9, 1, 10, 2};
        System.out.println("排序前 " + Arrays.toString(arr));
        /** 开始选择排序*/
        selectionSort(arr);
    }
}

输出:
> 排序前 [3, 9, 1, 10, 2]
* 3>9=false	此次循环中, 3依然是最小值, 继续
* 3>1=true	将假定的最小值改成1设置, 继续
* 1>10=false	1依然是最小值, 继续
* 1>2=false	1依然是最小值. 此次有过交换, 所以在 if (minIndex != i)中交换元素值
> i=0: [1, 9, 3, 10, 2]
* 9比较3
* 3比较10
* 3比较2
> i=1: [1, 2, 3, 10, 9]
* 3比较10
* 3比较9
> i=2: [1, 2, 3, 10, 9]
* 10比较9
> i=3: [1, 2, 3, 9, 10]


        /** 生成长度为 100000的随机值数组, 用于与其它算法对比排序效率*/
        int[] arr = setArray(100000);
        System.out.println("开始时间 " + DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss").format(LocalDateTime.now()));
        /** 开始选择排序*/
        selectionSort(arr);
        System.out.println("结束时间 " + DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss").format(LocalDateTime.now()));

输出:
> 开始时间 2020-10-14 14:04:09
> 结束时间 2020-10-14 14:04:12

插入排序(Insertion Sort)

  • 排序过程: n个待排序的元素看成是一个有序表和一个无序表
  1. 排序开始时, 有序表中只包含一个下标为0的元素, 而其它元素(n-1)都属于无序表
  2. 每次外循环获取的元素是无序表的数值: insertValue
  3. 内循环(while)中, 将 insertValue依次与有序表的所有元素比较
  4. 每次内循环结束后, 本轮 insertIndex如有被改动过, 那么就将 insertValue插入到有序表的相应位置

public class InsertionSortApp {
    public static int[] setArray(int capacity) {
        int[] arr = new int[capacity];
        for (int i =0; i < capacity;i++) {
            arr[i] = (int) (Math.random() * capacity);
        }
        return arr;
    }

    /** 插入排序方法
     * - 从无序表到有序表的转移过程*/
    public static void insertionSort(int[] arr) {
        /** 定义待插入的数值& 指定下标 -1, 也就是前一个元素下标
         * - 从第2个(1)元素开始与第一个元素(0)比较交换, 依次判断
         * */
        int insertValue = 0;
        int insertIndex = 0;
        for (int i = 1; i < arr.length; i++) {
            /** 定义待插入的数*/
            insertValue = arr[i];
            /** 待插入元素的下标 -1*/
            insertIndex = i - 1;

            /**
             * 1. 为了下标不越界 insertIndex >= 0
             * 2. 待插入的数(当前数: insertValue), 与原数组前一个数(下标-1的元素: arr[insertIndex])比较 insertValue < arr[insertIndex]
             * 3. 将 arr[insertIndex]值覆盖当前数的原数组位(arr[insertIndex + 1]), 也就是后移
             * */
            while (insertIndex >= 0 && insertValue < arr[insertIndex]) {
                arr[insertIndex + 1] = arr[insertIndex];
                insertIndex--;
            }

            /** 有在 while循环中处理过, 意味着 insertIndex下标递减过, 同时已锁定元素位, 所以可以将待插入的数(insertValue)插入到指定位置* */
            if (insertIndex + 1 != i) {
                arr[insertIndex + 1] = insertValue;
            }

            System.out.println("i=" + i + ": " + Arrays.toString(arr));
        }
    }

    public static void main(String[] args) {
        int arr[] = {9, 8, 7, 10, 2};
        System.out.println("排序前 " + Arrays.toString(arr));
        /** 开始插入排序*/
        insertionSort(arr);
    }

输出:
> 排序前 [9, 8, 7, 10, 2]
* 8<9=true; arr[insertIndex + 1] = arr[insertIndex]; 89 交换
i=1: [8, 9, 7, 10, 2]
* 7比较9
* 7比较8
i=2: [7, 8, 9, 10, 2]
i=3: [7, 8, 9, 10, 2]
* 2比较10
* 2比较9
* 2比较8
* 2比较7
i=4: [2, 7, 8, 9, 10]


        /** 生成长度为 100000的随机值数组, 用于与其它算法对比排序效率*/
        int[] arr = setArray(100000);
        System.out.println("开始时间 " + DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss").format(LocalDateTime.now()));
        /** 开始插入排序*/
        insertionSort(arr);
        System.out.println("结束时间 " + DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss").format(LocalDateTime.now()));

输出:
> 开始时间 2020-10-14 15:36:44
> 结束时间 2020-10-14 15:36:45

* 插入排序的问题: 当需要排序的数组中小值越靠后, 就会循环判断次数越多

希尔排序(Shell`s Sort)

  • 希尔排序是插入排序的优化版本, 又称缩小增量排序
  • 排序过程(交换方式): 原数组 arr={8,9,1,7,2,3,5,4,6,0}
  1. 第1&2个 for, 初始增量值是 gap=arr.length/2=5, 也就是最初开始是会分5组[8,9][1,7][2,3][5,4][6,0]
  2. 第2个 for, (i=5; i<10(arr.length); i++)循环5次进行直接插入排序, 结果为 [3,5,1,6,0,8,9,4,7,2]. 此轮循环结束后, 缩小增量 gap/=2(gap=5/2=2), 也就是这次会分两组[3,5,1,6,0][8,9,4,7,2]
  3. 第2个 for, (i=2; i<10(arr.length); i++)循环8次进行直接插入排序, 结果为 [0,2,1,4,3,5,7,6,9,8]. 此轮循环结束后, 缩小增量 gap/=2(gap=2/2=1)
  4. 最后第2个 for循环9次, 得到最终结果[0,1,2,3,4,5,6,7,8,9]

public class ShellSortApp {
    public static int[] setArray(int capacity) {
        int[] arr = new int[capacity];
        for (int i =0; i < capacity;i++) {
            arr[i] = (int) (Math.random() * capacity);
        }
        return arr;
    }

    /**
     * 希尔排序方法:
     * - 通过交换方式实现*/
    public static void shellSort(int[] arr) {
        /** 用于转移值的临时变量*/
        int temp = 0;
        /** 增量gap; 逐步缩小增量 gap /= 2*/
        for (int gap = arr.length / 2; gap > 0; gap /= 2) {
            for (int i = gap; i < arr.length; i++) {
                for (int j = i - gap; j >= 0; j -= gap) {
                    /** 如果当前元素大于加上步长后的元素就交换*/
                    if (arr[j] > arr[j + gap]) {
                        temp = arr[j];
                        arr[j] = arr[j + gap];
                        arr[j + gap] = temp;
                    }
                    System.out.println("i=" + i + ":" + arr[j] + "比较" + arr[j + gap]);
                }
            }
            System.out.println("i=" + gap + ": " + Arrays.toString(arr));
        }
    }

    public static void main(String[] args) {
        int arr[] = {8,9,1,7,2,3,5,4,6,0};
        System.out.println("排序前 " + Arrays.toString(arr));
        /** 开始希尔排序(交换方式)*/
        shellSort(arr);
    }
}

输出:
> 排序前 [8, 9, 1, 7, 2, 3, 5, 4, 6, 0]
* 8>3=true	38 交换 arr[j] > arr[j + gap]
* 9>5=true	59 交换
* 1>4=false	14 保持不动
* 7>6=true	67 交换
* 2>0=true	02 交换
> i=5: [3, 5, 1, 6, 0, 8, 9, 4, 7, 2]
i=2:1比较3
i=3:5比较6
i=4:0比较3
i=4:0比较1
i=5:6比较8
i=5:5比较6
i=6:3比较9
i=6:1比较3
i=6:0比较1
i=7:4比较8
i=7:4比较6
i=7:4比较5
i=8:7比较9
i=8:3比较7
i=8:1比较3
i=8:0比较1
i=9:2比较8
i=9:2比较6
i=9:2比较5
i=9:2比较4
> i=2: [0, 2, 1, 4, 3, 5, 7, 6, 9, 8]
i=1:0比较2
i=2:1比较2
i=2:0比较1
i=3:2比较4
i=3:1比较2
i=3:0比较1
i=4:3比较4
i=4:2比较3
i=4:1比较2
i=4:0比较1
i=5:4比较5
i=5:3比较4
i=5:2比较3
i=5:1比较2
i=5:0比较1
i=6:5比较7
i=6:4比较5
i=6:3比较4
i=6:2比较3
i=6:1比较2
i=6:0比较1
i=7:6比较7
i=7:5比较6
i=7:4比较5
i=7:3比较4
i=7:2比较3
i=7:1比较2
i=7:0比较1
i=8:7比较9
i=8:6比较7
i=8:5比较6
i=8:4比较5
i=8:3比较4
i=8:2比较3
i=8:1比较2
i=8:0比较1
i=9:8比较9
i=9:7比较8
i=9:6比较7
i=9:5比较6
i=9:4比较5
i=9:3比较4
i=9:2比较3
i=9:1比较2
i=9:0比较1
> i=1: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]


        /** 生成长度为 100000的随机值数组, 用于与其它算法对比排序效率*/
        int[] arr = setArray(100000);
        System.out.println("开始时间 " + DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss").format(LocalDateTime.now()));
        /** 开始希尔排序(交换方式)*/
        shellSort(arr);
        System.out.println("结束时间 " + DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss").format(LocalDateTime.now()));

输出:
> 开始时间 2020-10-15 22:18:27
> 结束时间 2020-10-15 22:18:37

  • 排序过程(移位方式): 原数组 arr={8,9,1,7,2,3,5,4,6,0}

public class ShellSortApp {
    public static int[] setArray(int capacity) {
        int[] arr = new int[capacity];
        for (int i =0; i < capacity;i++) {
            arr[i] = (int) (Math.random() * capacity);
        }
        return arr;
    }

    /**
     * 希尔排序方法:
     * - 通过移位方式实现*/
    public static void shellSort(int[] arr) {
        /** 增量gap; 逐步缩小增量 gap /= 2*/
        for (int gap = arr.length / 2; gap > 0; gap /= 2) {
            for (int i = gap; i < arr.length; i++) {
                /** 每次循环开始尝试比较的下标*/
                int j = i;
                /** 用于转移值的临时变量*/
                int temp = arr[j];
                /**
                 * 首轮 j=5; gap=5; j-gap=0;
                 *      - (j - gap): 待插入元素的下标
                 *      - arr[j] < arr[j - gap] -> 3 < 8
                 * */
                if (arr[j] < arr[j - gap]) {
                    /** 依次往下(缩小增量)比较移动*/
                    while (j - gap >= 0 && temp < arr[j - gap]) {
                        System.out.println("i=" + i + ":" + temp + "比较" + arr[j - gap]);
                        /** 移动*/
                        arr[j] = arr[j - gap];
                        /**
                         * 首轮 j=5; gap=5; j=0=j-gap;
                         * */
                        j -= gap;
                    }
                    /** 指定下标插入*/
                    arr[j] = temp;
                }
            }
            System.out.println("i=" + gap + ": " + Arrays.toString(arr));
        }
    }

    public static void main(String[] args) {
        int arr[] = {8,9,1,7,2,3,5,4,6,0};
        System.out.println("排序前 " + Arrays.toString(arr));
        /** 开始希尔排序(移位方式)*/
        shellSort(arr);
    }
}

输出:
> 排序前 [8, 9, 1, 7, 2, 3, 5, 4, 6, 0]
i=5:3<8 交换
i=6:5<9 交换
i=8:6<7 交换
i=9:0<2 保持不动
> i=5: [3, 5, 1, 6, 0, 8, 9, 4, 7, 2]
i=2:1比较3
i=4:0比较3
i=4:0比较1
i=7:4比较8
i=7:4比较6
i=7:4比较5
i=8:7比较9
i=9:2比较8
i=9:2比较6
i=9:2比较5
i=9:2比较4
> i=2: [0, 2, 1, 4, 3, 5, 7, 6, 9, 8]
i=2:1比较2
i=4:3比较4
i=7:6比较7
i=9:8比较9
> i=1: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]


        /** 生成长度为 100000的随机值数组, 用于与其它算法对比排序效率*/
        int[] arr = setArray(100000);
        System.out.println("开始时间 " + DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss").format(LocalDateTime.now()));
        /** 开始希尔排序(移位方式)*/
        shellSort(arr);
        System.out.println("结束时间 " + DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss").format(LocalDateTime.now()));

输出:
> 开始时间 2020-10-16 10:50:10
> 结束时间 2020-10-16 10:50:10

快速排序(Quick Sort)

  • 快速排序是冒泡排序的优化版
  • 快速排序的时间复杂度为线性对数阶 O(nlog2n)
  1. 原数组分割成左右两部分, 将左右值与中轴值一一比较, 然后所有的小值放左边大值放右边
  2. 退出外循环后, 如果左右下标不相等的话, 意为还未排序完, 此时会通过递归继续进行排序

public class QuickSortApp {
    public static int[] setArray(int capacity) {
        int[] arr = new int[capacity];
        for (int i =0; i < capacity;i++) {
            arr[i] = (int) (Math.random() * capacity);
        }
        return arr;
    }

    /** 开始快速排序*/
    public static void quickSort(int[] arr,int left, int right) {
        /** 左下标*/
        int l = left;
        /** 右下标*/
        int r = right;
        /** 中轴值*/
        int pivot = arr[(left + right) / 2];
        /** 用于转移值的临时变量*/
        int temp = 0;
        /** 循环判断左右元素值与中轴值(pivot)比较, 将小值放左, 大值放右*/
        while(l < r) {
            /** 从左边所有元素中, 找出相比中轴值大或等于的元素下标, 直到退出
             * 外部 while的第二轮: 由于 arr[0]=2; 以下条件成立 arr[0] < pivot=7, 也就是小于 pivot; 因此 l++;0 -> 1
             * */
            while(arr[l] < pivot) {
                l++;
            }

            /** 将所有右值下标递减的方式一一与 pivot比较, 如果小于或等于 pivot时退出, 并继续走流程
             * 外部 while的第二轮: 由于 arr[4]=9; 以下条件成立 arr[4] > pivot=7, 也就是大于 pivot; 因此 r--;4 -> 3 继续
             *                   - arr[3]=10; 条件又成立 arr[3] > 7; r--;3 -> 2, 此时 arr[r] == 7, 因此条件不成立. 退出以下循环, 并继续走流程
             * */
            while(arr[r] > pivot) {
                r--;
            }

            /** l >= r通过时, 意味着当前所有左值小于或等于(<=)中轴值, 所有右值大于等于(>=)中轴值
             * 外部 while的第三轮: l >= r(2 >= 1), 退出 while
             * */
            if(l >= r) {
                break;
            }

            System.out.println("l=" + l + ", r=" + r + ": arr[l]=" + arr[l] + ", arr[r]=" + arr[r]);

            /** 左右交换值*/
            temp = arr[l];
            arr[l] = arr[r];
            arr[r] = temp;
            /**
             * 外部 while的首轮: l=0, r=4: arr[l]=9, arr[r]=2, 交换后 arr[0]=2, arr[4]=9, 此时原数据 {2, 8, 7, 10, 9}
             * 外部 while的第二轮: l=1, r=2: arr[l]=8, arr[r]=7, 交换后 arr[1]=7, arr[2]=8, 此时原数据 {2, 7, 8, 10, 9}
             * */

            /** 交换后, 如果指定的左值与中轴值相等, 则将右下标往前移
             * 外部 while的第二轮: 此时由于 arr[1]==7, 因此与 pivot相等; r--; 2 -> 1
             * */
            if(arr[l] == pivot) {
                r--;
            }

            /** 交换后, 如果指定的右值与中轴值相等, 则将左下标往后移
             * 外部 while的第二轮: 此时由于 arr[1]==7, 因此与 pivot相等; l++; 1 -> 2
             * */
            if(arr[r] == pivot) {
                l++;
            }
        }

        /** 左右下标相等时, 必须 l++; r--; 否则死循环*/
        if (l == r) {
            l++;
            r--;
        }

        /** 向左递归*/
        if(left < r) {
            quickSort(arr, left, r);
        }

        /** 向右递归
         * - 首次执行时 right(4) > l(2), 因此进入递归
         * */
        if(right > l) {
            quickSort(arr, l, right);
        }
    }

    public static void main(String[] args) {
        int arr[] = {9, 8, 7, 10, 2};
        System.out.println("排序前 " + Arrays.toString(arr));
        /** 开始快速排序*/
        quickSort(arr, 0, arr.length - 1);
        System.out.println("排序后 " + Arrays.toString(arr));
    }
}

输出:
> 排序前 [9, 8, 7, 10, 2]
l=0, r=4: arr[l]=9, arr[r]=2
l=1, r=2: arr[l]=8, arr[r]=7
l=3, r=4: arr[l]=10, arr[r]=9 此行是递归中执行 quickSort(arr, l, right)后排序的部分
> 排序后 [2, 7, 8, 9, 10]


        /** 生成长度为 100000的随机值数组, 用于与其它算法对比排序效率*/
        int[] arr = setArray(100000);
        System.out.println("开始时间 " + DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss").format(LocalDateTime.now()));
        /** 开始快速排序*/
        quickSort(arr, 0, arr.length - 1);
        System.out.println("结束时间 " + DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss").format(LocalDateTime.now()));

输出:
> 开始时间 2020-10-17 00:30:49
> 结束时间 2020-10-17 00:30:49

归并排序(Merge Sort)

  • 该算法采用了经典的分治(divide-and-conquer)策略, 就是先将原数据拆分成很多小块, 然后将各小块按序合并.
  • 归并排序的时间复杂度为线性对数阶 O(nlog2n)

public class MergeSortApp {
    public static int[] setArray(int capacity) {
        int[] arr = new int[capacity];
        for (int i =0; i < capacity;i++) {
            arr[i] = (int) (Math.random() * capacity);
        }
        return arr;
    }

    /** 分治方法
     * 时间复杂度为线性对数阶 O(nlog2n)*/
    public static void mergeSort(int[] arr, int left, int right, int[] temp) {
        if(left < right) {
            /** 中间索引*/
            int mid = (left + right) / 2;
            /** 递归向左分解*/
            mergeSort(arr, left, mid, temp);
            /** 递归向右分解*/
            mergeSort(arr, mid + 1, right, temp);
            /** 合并*/
            merge(arr, left, mid, right, temp);
        }
    }

    /** 合并方法
     * @param arr 原始数组
     * @param left 左边有序序列的初始下标
     * @param mid 中间下标
     * @param right 右边下标
     * @param temp 做中转的数组*/
    public static void merge(int[] arr, int left, int mid, int right, int[] temp) {
        /** 左边有序序列的初始索引*/
        int i = left;
        /** 右边有序序列的初始索引*/
        int j = mid + 1;
        /** 指向 temp数组的当前下标*/
        int t = 0;

        /** 1)
         * 将左右两边(有序)的数据, 按照规则加到 temp数组, 直到其中一边处理完毕为止*/
        while (i <= mid && j <= right) {
            /** 如果左边的当前元素, 小于等于右边的当前元素*/
            if(arr[i] <= arr[j]) {
                /** 将左边有序序列的当前元素, 加到 temp*/
                temp[t] = arr[i];
                /** temp的下标往右移一位*/
                t++;
                /** 左边有序序列的下标往右移一位*/
                i++;
            } else {
                /** 将右边有序序列的当前元素, 加到 temp*/
                temp[t] = arr[j];
                /** temp的下标往右移一位*/
                t++;
                /** 右边有序序列的下标往右移一位*/
                j++;
            }
        }
        /** 2)
         * 将左边有序序列的剩余数据依次加到 temp*/
        while(i <= mid) {
            temp[t] = arr[i];
            t++;
            i++;
        }

        /** 将右边有序序列的剩余数据依次加到 temp*/
        while(j <= right) {
            temp[t] = arr[j];
            t++;
            j++;
        }

        t = 0;
        int tempLeft = left;
        /** 3)
         * 将 temp数组的元素拷贝到 arr*/
        System.out.println("right=" + right + ", mid=" + mid + ", tempLeft=" + tempLeft);
        while(tempLeft <= right) {
            arr[tempLeft] = temp[t];
            System.out.println(" temp=" + Arrays.toString(temp) + ", arr[" + tempLeft + "]=" + arr[tempLeft] + ", arr=" + Arrays.toString(arr));
            t += 1;
            tempLeft += 1;
        }
    }

    public static void main(String[] args) {
        int arr[] = {8, 4, 5, 7, 1, 3, 6, 2};
        System.out.println("排序前 " + Arrays.toString(arr));
        /** 开始归并排序*/
        int[] temp = new int[arr.length];
        mergeSort(arr, 0, arr.length - 1, temp);
        System.out.println("排序后 " + Arrays.toString(arr));
    }
}

输出:
> 排序前 [8, 4, 5, 7, 1, 3, 6, 2]
> right=1, mid=0, tempLeft=0
>  temp=[4, 8, 0, 0, 0, 0, 0, 0], arr[0]=4, arr=[4, 4, 5, 7, 1, 3, 6, 2]
>  temp=[4, 8, 0, 0, 0, 0, 0, 0], arr[1]=8, arr=[4, 8, 5, 7, 1, 3, 6, 2]
> right=3, mid=2, tempLeft=2
>  temp=[5, 7, 0, 0, 0, 0, 0, 0], arr[2]=5, arr=[4, 8, 5, 7, 1, 3, 6, 2]
>  temp=[5, 7, 0, 0, 0, 0, 0, 0], arr[3]=7, arr=[4, 8, 5, 7, 1, 3, 6, 2]
> right=3, mid=1, tempLeft=0
>  temp=[4, 5, 7, 8, 0, 0, 0, 0], arr[0]=4, arr=[4, 8, 5, 7, 1, 3, 6, 2]
>  temp=[4, 5, 7, 8, 0, 0, 0, 0], arr[1]=5, arr=[4, 5, 5, 7, 1, 3, 6, 2]
>  temp=[4, 5, 7, 8, 0, 0, 0, 0], arr[2]=7, arr=[4, 5, 7, 7, 1, 3, 6, 2]
>  temp=[4, 5, 7, 8, 0, 0, 0, 0], arr[3]=8, arr=[4, 5, 7, 8, 1, 3, 6, 2]
> right=5, mid=4, tempLeft=4
>  temp=[1, 3, 7, 8, 0, 0, 0, 0], arr[4]=1, arr=[4, 5, 7, 8, 1, 3, 6, 2]
>  temp=[1, 3, 7, 8, 0, 0, 0, 0], arr[5]=3, arr=[4, 5, 7, 8, 1, 3, 6, 2]
> right=7, mid=6, tempLeft=6
>  temp=[2, 6, 7, 8, 0, 0, 0, 0], arr[6]=2, arr=[4, 5, 7, 8, 1, 3, 2, 2]
>  temp=[2, 6, 7, 8, 0, 0, 0, 0], arr[7]=6, arr=[4, 5, 7, 8, 1, 3, 2, 6]
> right=7, mid=5, tempLeft=4
>  temp=[1, 2, 3, 6, 0, 0, 0, 0], arr[4]=1, arr=[4, 5, 7, 8, 1, 3, 2, 6]
>  temp=[1, 2, 3, 6, 0, 0, 0, 0], arr[5]=2, arr=[4, 5, 7, 8, 1, 2, 2, 6]
>  temp=[1, 2, 3, 6, 0, 0, 0, 0], arr[6]=3, arr=[4, 5, 7, 8, 1, 2, 3, 6]
>  temp=[1, 2, 3, 6, 0, 0, 0, 0], arr[7]=6, arr=[4, 5, 7, 8, 1, 2, 3, 6]
> right=7, mid=3, tempLeft=0
>  temp=[1, 2, 3, 4, 5, 6, 7, 8], arr[0]=1, arr=[1, 5, 7, 8, 1, 2, 3, 6]
>  temp=[1, 2, 3, 4, 5, 6, 7, 8], arr[1]=2, arr=[1, 2, 7, 8, 1, 2, 3, 6]
>  temp=[1, 2, 3, 4, 5, 6, 7, 8], arr[2]=3, arr=[1, 2, 3, 8, 1, 2, 3, 6]
>  temp=[1, 2, 3, 4, 5, 6, 7, 8], arr[3]=4, arr=[1, 2, 3, 4, 1, 2, 3, 6]
>  temp=[1, 2, 3, 4, 5, 6, 7, 8], arr[4]=5, arr=[1, 2, 3, 4, 5, 2, 3, 6]
>  temp=[1, 2, 3, 4, 5, 6, 7, 8], arr[5]=6, arr=[1, 2, 3, 4, 5, 6, 3, 6]
>  temp=[1, 2, 3, 4, 5, 6, 7, 8], arr[6]=7, arr=[1, 2, 3, 4, 5, 6, 7, 6]
>  temp=[1, 2, 3, 4, 5, 6, 7, 8], arr[7]=8, arr=[1, 2, 3, 4, 5, 6, 7, 8]
> 排序后 [1, 2, 3, 4, 5, 6, 7, 8]


        /** 生成长度为 100000的随机值数组, 用于与其它算法对比排序效率*/
        int[] arr = setArray(100000);
        System.out.println("开始时间 " + DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss").format(LocalDateTime.now()));
        /** 开始归并排序*/
        int temp[] = new int[arr.length];
        mergeSort(arr, 0, arr.length - 1, temp);
        System.out.println("结束时间 " + DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss").format(LocalDateTime.now()));

输出:
> 开始时间 2020-10-17 21:59:26
> 结束时间 2020-10-17 21:59:26

基数排序(Radix Sort)

  • 基数排序属于分配式排序(Distribution Sort)又称桶子法(Bucket Sort)或 Bin Sort. 它是桶排序的扩展
  • 基数排序属于稳定性的排序, 其时间复杂度为 O(nlog®m), 其中 r为所采取的基数, 而 m为堆数
  • 基数排序是经典的空间换取时间的排序算法, 如果数据量过多, 容易造成 OutOfMemoryError
  • 排序过程: 将所有待比较数值统一为同样的数位长度, 数位未达到的数前面默认补零. 然后从最低位(1位)开始到最高位加到桶进行排序

public class RadixSortApp {
    public static int[] setArray(int capacity) {
        int[] arr = new int[capacity];
        for (int i =0; i < capacity;i++) {
            arr[i] = (int) (Math.random() * capacity);
        }
        return arr;
    }

    /** 基数排序方法
     * 时间复杂度为 O(n*k)*/
    public static void radixSort(int[] arr) {
        /** 筛选出最大数; 用于获取最大数的位数*/
        int max = 0;
        for(int i = 0; i < arr.length; i++) {
            if (arr[i] > max) {
                max = arr[i];
            }
        }
        /** 获取最大数的位数*/
        int maxLength = String.valueOf(max).length();

        /**
         * 定义10个桶, 每个桶用深度为 arr.length
         * - 由于无法预知桶深度的, 所以大小设定为原数组长度(就是空间换取时间)*/
        int[][] bucket = new int[10][arr.length];

        /**
         * 用于记录每个桶中, 存放的数值个数, 如 bucket[0][0]=11; bucket[0][1]=22; 此时 bucketElementCounts[0] == 2*/
        int[] bucketElementCounts = new int[10];

        for(int i = 0 , n = 1; i < maxLength; i++, n *= 10) {
            /** 每个元素, 按各自对应的位加到 bucket
             * - 第一次外循环是1位, 第二10位, 第三100位, 依次递增. 取决于最大数的位数*/
            for(int j = 0; j < arr.length; j++) {
                /** 计算出每个元素值的对应位 如 (arr[j]=971 / 10 % 10)=7 */
                int digitOfElement = arr[j] / n % 10;
                /** 加到对应的桶中 如 bucket[7][bucketElementCounts[7]=0]=971, 也就是首轮, 将971加到 bucket[7][0]*/
                bucket[digitOfElement][bucketElementCounts[digitOfElement]] = arr[j];
                bucketElementCounts[digitOfElement]++;
            }

            int index = 0;
            /** 将桶的数据, 按照下标顺序取出, 再放入原来数组*/
            for(int k = 0; k < bucketElementCounts.length; k++) {
                /** 只处理, 有加数值的桶*/
                if(bucketElementCounts[k] != 0) {
                    for(int l = 0; l < bucketElementCounts[k]; l++) {
                        /** 按顺序将桶数据取出, 加到原数组中*/
                        arr[index++] = bucket[k][l];
                    }
                }

                /** 每个位的有效元素个数, 用完后需要清零, 防止下一次外循环后, 影响重新记录不同位的有效数值个数*/
                bucketElementCounts[k] = 0;
            }

            System.out.println("i=" + i + ": " + Arrays.toString(arr));
        }
    }

    public static void main(String[] args) {
        int arr[] = {971, 78, 7, 17, 2};
        System.out.println("排序前 " + Arrays.toString(arr));
        /** 开始基数排序*/
        radixSort(arr);
    }
}

输出:
> 排序前 [971, 78, 7, 17, 2]
> i=0: [971, 2, 7, 17, 78]
> i=1: [2, 7, 17, 971, 78]
> i=2: [2, 7, 17, 78, 971]
> 排序后 [2, 7, 17, 78, 971]


        /** 生成长度为 100000的随机值数组, 用于与其它算法对比排序效率*/
        int[] arr = setArray(100000);
        System.out.println("开始时间 " + DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss").format(LocalDateTime.now()));
        /** 开始基数排序*/
        radixSort(arr);
        System.out.println("结束时间 " + DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss").format(LocalDateTime.now()));

输出:
> 开始时间 2020-10-18 15:14:22
> 结束时间 2020-10-18 15:14:22

  • 以上例子不支持含有负数的数组排序, 如要支持参考: https://code.i-harness.com/zh-CN/q/e98fa9

堆排序(Heap Sort)

  • 堆排序是利用堆数据结构排序的算法, 它属于选择排序, 它的最坏,最好,平均时间复杂度为 O(nlogn), 还有就是属不稳定的排序
  • 堆有两种性质的完全二叉树:
  1. 每个节点的值都大于或等于其左右子节点的值, 称之为大顶堆(注: 没有限制左右子节点的值大小关系)
  • 大顶堆的规则: arr[i]>=arr[2i+1] && arr[i]>=arr[2i+2], 其中i对应第几节点, 从0开始编号
  1. 每个节点的值都小于或等于其左右子节点的值, 称之为小顶堆
  • 小顶堆的规则: arr[i]<=arr[2i+1] && arr[i]<=arr[2i+2], 其中i对应第几节点, 从0开始编号
    * 排升序需采用大顶堆, 降序需采用小顶堆
  • 排序(升序)过程:
  1. 将待排序的数组构建成一个堆, 根据升序或降序的需求选择大顶堆或小顶堆
  2. 将堆顶元素与尾元素交换, 将最大元素替换到数组的末端
  3. 重新调整结构, 继续交换堆顶元素与当前末尾元素, 反复调整+交换步骤, 直到整个序列有序
  • 排序图示:
  1. 准备无序数组: {4,6,8,5,9}, 构成一个堆
    在这里插入图片描述

  2. 从最后一个非叶子节点开始(叶子结点不用调整, 第一个非叶子节点 arr.length/2-1=5/2-1=1, 也就是6节点), 从左至右, 从下至上进行调整
    在这里插入图片描述

  3. 找到第二个非叶子节点4, 由于4,9,8中9最大, 因此4和9交换
    在这里插入图片描述

  4. 交换后发现, 4与它的新左右子节点5,6, 存在更大的值, 将最大的6与4交换

  • 此时, 已构成第一个大顶堆
    在这里插入图片描述
  1. 将堆顶元素9和末尾元素4交换
    在这里插入图片描述

  2. 将剩余长度 n-1元素, 再继续构建大顶堆
    在这里插入图片描述

  3. 再次将堆顶元素8与末尾元素5进行交换, 得到第二大元素8
    在这里插入图片描述

  4. 依次反复, 直到排序完
    在这里插入图片描述


public class HeapSortApp {
    public static int[] setArray(int capacity) {
        int[] arr = new int[capacity];
        for (int i =0; i < capacity;i++) {
            arr[i] = (int) (Math.random() * capacity);
        }
        return arr;
    }

    /** 堆排序方法(升序)*/
    public static void heapSort(int arr[]) {
        /** 1. 将待排序的数组构建成一个堆, 根据升序或降序的需求选择大顶堆或小顶堆*/
        for(int i = arr.length/2-1; i >=0; i--) {
            adjustHeap(arr, i, arr.length);
        }

		/** 2. 将堆顶元素与尾元素交换, 将最大元素替换到数组的末端
         *  3. 重新调整结构, 继续交换堆顶元素与当前末尾元素, 反复调整+交换步骤, 直到整个序列有序*/
        int temp = 0;
        for(int j = arr.length-1; j>0; j--) {
            temp = arr[j];
            arr[j] = arr[0];
            arr[0] = temp;
            adjustHeap(arr, 0, j);
        }
    }

    /** 将数组构建成一个大顶堆
     * 从非叶子结点 i开始, 从左至右, 从下至上进行调整
     * 第一轮: i=1(最后一个非叶子节点6): {4,6,8,5,9} => {4,9,8,5,6}
     * 第二轮: i=0(第二个非叶子节点4): {4,9,8,5,6} => {9,4,8,5,6}, 此时发现4与它的新左右子节点5,6, 存在更大的值,
     *          - 将其中最大的6与4交换, {9,4,8,5,6} => {9,6,8,5,4}, 完成第一个大顶堆
     * 第三轮: i=0: {9,6,8,5,4} => {4,6,8,5,9, 堆顶元素与末尾元素调换, 将剩余长度 n-1元素, 再继续构建大顶堆, 依次反复, 直到排序完
     * @param arr 待排序的数组
     * @param i 非叶子结点在数组中的下标
     * @param length 剩余待排序的元素长度, length会逐渐的减少*/
    public  static void adjustHeap(int arr[], int i, int length) {
        /** 临时保存当前元素的值*/
        int temp = arr[i];
        /** k=i*2+1, k是 i结点的左子节点*/
        for(int k=i*2+1; k<length; k=k*2+1) {
            /** 首轮: k3+1=4 < 5 && arr[k3]=5 < arr[k+1]=9*/
            if(k+1 < length && arr[k] < arr[k+1]) { /** 说明左子结点的值小于右子结点的值*/
                k++; /** k指向右子结点*/
            }
            /** 首轮: arr[k4]=9 > temp6*/
            if(arr[k] > temp) { /** 当子结点大于父结点*/
                arr[i] = arr[k]; /** 把较大的值赋给当前结点*/
                i = k; /** 非叶子节点 i=0改为 i=k4*/
            } else {
                break;
            }
        }

        /** 将更小的元素赋值到 i上(这个 i如果在以上 for循环中处理过了, 就是 k, 意思为不是形参上的 i, 而是形参 i的左右子节点)
         * 将 temp值放到调整后的位置 arr[i4] = temp6*/
        arr[i] = temp;
    }

    public static void main(String[] args) {
        int arr[] = {4,6,8,5,9};
        System.out.println("排序前 " + Arrays.toString(arr));
        /** 开始堆排序*/
        heapSort(arr);
        System.out.println("排序后 " + Arrays.toString(arr));
    }
}

输出:
> 排序前 [4, 6, 8, 5, 9]
> 排序后 [4, 5, 6, 8, 9]

        /** 生成长度为 10000000的随机值数组, 用于与其它算法对比排序效率*/
        int[] arr = setArray(10000000);
        System.out.println("开始时间 " + DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss").format(LocalDateTime.now()));
        /** 开始堆排序*/
        heapSort(arr);
        System.out.println("结束时间 " + DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss").format(LocalDateTime.now()));

输出:
> 开始时间 2020-10-30 16:45:11
> 结束时间 2020-10-30 16:45:14

如果您觉得有帮助,欢迎点赞哦 ~ 谢谢!!

©️2020 CSDN 皮肤主题: 创作都市 设计师:CSDN官方博客 返回首页