7.排序算法

目录

1.排序算法的介绍
2.排序的分类
3.冒泡排序
4.选择排序
5.插入排序
6.希尔排序
7.快速排序
8.归并排序
9.基数排序
10.堆排序
11.常用排序算法总结和对比

1.排序算法的介绍

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

2.排序的分类

(1) 内部排序:指将需要处理的所有数据都加载到内部存储器(内存)中进行排序。
(2) 外部排序:数据量过大,无法全部加载到内存中,需要借助外部存储(文件等)进行排序。
(3)常见的排序算法分类如下图:
7.排序算法_第1张图片
下述排序算法默认均将无序序列排序为升序。

3.冒泡排序

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

示例演示冒泡的过程:
7.排序算法_第3张图片
小结上面的示例过程:

(1) 一共进行了序列的大小-1 趟排序;
(2) 每一趟排序比较的次数在逐渐的减少;
(3) (1)和(2)又可以总结为,第一趟后最大的数被放到了序列最后一个位置,第二趟后第二大的数放到了倒数第二个位置,以此类推,直到最小的数无需比较,就在序列第一个位置,实现了升序排序;
(4) 实际在上述示例中,第二趟结束后序列则有序了,后续的几趟只进行了比较,没有进行交换。所以如果我们发现在某趟排序中,没有发生一次交换,则表示 序列已经有序了,就可以提前结束冒泡排序,避免后续无效的冒泡。这样可以实现对冒泡排序的优化。

代码实现:

package sort;

import java.util.Arrays;

public class BubbleSort {
    
    public static void main(String[] args) {
        int[] array = {3, 9, -1, 10, 20};
        System.out.println("初始序列:"+ Arrays.toString(array));
        //冒泡排序的时间复杂度 O(n^2)
        for (int i = 0; i < array.length; i++) {
            boolean isExchange = false; //记录该趟冒泡是否有做交换
            //依次交换逆序的相邻两个元素
            for (int j = 0; j < array.length-i-1; j++) {
                int temp = 0;
                //满足相邻元素逆序,则交换位置
                if (array[j]>array[j+1]){
                    temp = array[j];
                    array[j] = array[j+1];
                    array[j+1] = temp;
                    isExchange = true;
                }
            }
            //该趟冒泡没有做交换,则序列已经有序,无需做无用循环了
            if (!isExchange){ 
                break;
            }
            System.out.println("第"+(i+1)+"趟排序后的序列:"+ Arrays.toString(array));
        }
    }
}

4.选择排序

选择排序介绍:

选择式排序也属于内部排序法,是从欲排序的数据中,按指定的规则选出某一元素,再依规定交换位置后达到排序的目的。

选择排序的思想:

选择排序(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 次,得到一个按排序码从小到大排列的有序序列。

示例演示选择排序的过程:
在这里插入图片描述

说明:

  1. 选择排序一共有 数组大小 - 1 轮排序;
  2. 每一轮排序中,又是一个循环, 循环的规则:
    2.1 先假定当前这个数是最小数;
    2.2 然后和后面的每个数进行比较,如果发现有比当前数更小的数,就重新确定最小数,并得到下标;
    2.3 当遍历到数组的最后时,就得到本轮最小数和下标;
    2.4 交换。

代码实现:

package sort;

import java.util.Arrays;

public class SelectSort {
    public static void main(String[] args) {
        int[] array = {101, 34 ,119 ,1};
        System.out.println("初始序列:"+ Arrays.toString(array));
        //选择排序的时间复杂度 O(n^2)
        for (int i = 0; i < array.length-1; i++) {
            int min = array[i];
            int minIndex = i;
            // 查找当前第i个元素后面的元素是否有比它小的
            for (int j = i+1; j < array.length; j++) {
                if (array[j] < min){
                    min = array[j];
                    minIndex = j;
                }
            }
            //如果当前即为最小值则不需要交换,否则将最小值交换到当前第i个元素的位置
            if (minIndex != i){
                array[minIndex] = array[i];
                array[i] = min;
            }
            System.out.println("第"+(i+1)+"轮选择排序后的序列:"+ Arrays.toString(array));
        }
    }
}

5.插入排序

插入式排序介绍:

插入式排序属于内部排序法,是对于欲排序的元素以插入的方式找寻该元素的适当位置,以达到排序的目的。

插入排序法思想:

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

示例演示选择排序的过程:
7.排序算法_第4张图片
代码实现:

package sort;

import java.util.Arrays;

public class InsertSort {
    public static void main(String[] args) {
        int[] array = {101, 34 ,119 ,1};
        System.out.println("初始序列:"+ Arrays.toString(array));
        //插入排序
        for (int i = 1; i < array.length; i++) {
            int insertVal = array[i];
            // 用待插入数与有序列表中元素从后往前比较,每比较一个如果比待插入数更大,则表示待插入数应该在比较数前面,将比较数后移,为插入数让出位置
            // 以此类推直到找到第一个比待插入数小的元素,表示应该插入到该元素后面,结束后移,插入带插入数。
            for (int j = i; j >= 0; j--) {
                if (j==0 || insertVal>=array[j-1]){//若没有比带插入数小的数,即下标为0时,则带插入数为最小数,直接插入到下标0处
                    array[j] = insertVal;
                    break;
                }
                array[j] = array[j-1];
            }
            System.out.println("插入第"+(i)+"个元素后的序列:"+ Arrays.toString(array));
        }
    }
}

6.希尔排序

在了解上述的简单插入排序后,我们容易发现:当需要插入的数是较小的数时,后移的次数明显增多,对效率有影响。 所以提出了希尔排序的方案,对简单插入排序进行了优化。

希尔排序介绍:

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

希尔排序基本思想:

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

希尔排序的示意图:
7.排序算法_第5张图片
7.排序算法_第6张图片
代码实现:

package sort;

import java.util.Arrays;

public class ShellSort {
    public static void main(String[] args) {
        int[] array = {8, 9, 1, 7, 2, 3, 5, 4, 6, 0};
        System.out.println("初始序列:"+ Arrays.toString(array));
        int count = 0;
        //希尔排序
        for (int gap = array.length/2; gap > 0; gap /= 2) {//分组
            //各个分组中第一个元素为插入排序时的初始有序序列,所以从gap开始,根据增量遍历
            for (int i = gap; i < array.length; i++) {
                int insertVal = array[i];
                int j = 0;
                //从待插入元素前一个元素开始判断,比待插入元素大则后移,为待插入元素留出空间。
                // 直到找到比待插入数小的数,结束后移,插入到该数后面
                for (j = i - gap; j >=0 && insertVal < array[j] ; j -= gap) {
                    array[j+gap] = array[j];
                }
                array[j+gap] = insertVal;
            }

            /* 下面的实现方式更容易理解,但代码没那么简洁。因为没必要非要在单独每个组中进行插入排序,
                可以直接在原序列中根据增量进项插入排序,同样能实现对各个组分别进行插入排序
            //对gap个组分别进行直接插入排序
            for (int i = 0; i < gap; i++) {
                //注意每一组并不是连续的,是按照gap增量跳跃的
                for (int j = i + gap; j < array.length; j += gap) {
                    int insertVal = array[j];
                    for (int k = j; k >= 0 ; k -= gap) {
                        if (k==i || insertVal>=array[k-gap]){
                            array[k] = insertVal;
                            break;
                        }
                        array[k] = array[k-gap];
                    }
                }
            } */
            System.out.println("希尔排序第"+ (++count) +"次分组插入排序后的序列:"+ Arrays.toString(array));
        }

    }
}

7.快速排序

快速排序基本思想:

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

实现原理:

快速排序,说白了就是给基准数据找其正确索引位置的过程,基准数随机指定。这样基准数左边都是小于它的数,基准数右边都是大于它的数,然后分别再对左右两部分进行上述操作,递归实现。

如何找基准数位置:

默认指定基准数为序列第一个元素,取出放到pivot中。然后分别从序列的两端扫描数据,设两个扫描指针:left指向起始位置,right指向末尾,扫描过程中,小于基准数的统一放到序列左边部分,大于基准数的放到右边部分。由于取出了基准数,所以为扫描时数据移动、交换提供了辅助空间,最后再将基准数放到这左右两部分数的中间,即找到基准数的位置。
7.排序算法_第7张图片
首先从右半部分开始,如果扫描到的值大于基准数据就让right减1,表示该数在合适的半区,继续扫描下一个值;如果发现有元素比该基准数据的值小(如上图中18<=pivot),就将right位置的值赋值给left位置(此时left指向基准数取出后的空闲位置) ,实现将较小值放到左半区(放到左半区后,right的位置的空间又空闲出来了),结果如下:
7.排序算法_第8张图片
然后开始从左半区向后扫描,如果扫描到的值小于基准数据就让left加1,表示该数在合适的半区,继续扫描下一个值;如果发现有元素大于基准数据的值(如上图46=>pivot),就再将left位置的值赋值到right位置,指针移动并且数据移动后的结果如下:
7.排序算法_第9张图片
然后再开始从右半部分开始扫描,以此类推:
7.排序算法_第10张图片
直到 left=right 时结束循环,此时left或right的下标就是基准数据23在该数组中的正确索引位置,将之前取出保存的基准数放到该位置。如下图所示:
7.排序算法_第11张图片
这样一遍走下来,就实现了找到基准数的位置。然后采用递归的方式分别对前半部分和后半部分用同样的方式定基准数,分左右两部分。当所有的前半部分和后半部分均有序时该数组就自然整体有序了。

代码实现:

package sort;

import java.util.Arrays;

public class QuickSort {
    public static void main(String[] args) {
        int[] array = { 49, 38, 65, 97, 23, 22, 76, 1, 5, 8, 2, 0, -1, 22 };
        System.out.println("初始序列:"+ Arrays.toString(array));

        //快速排序
        quickSort(array,0,array.length-1);
        System.out.println(Arrays.toString(array));
    }

    /**
     * 递归快速排序
     * @param array
     * @param left
     * @param right
     */
    public static void quickSort(int[] array, int left, int right){
        if (left < right){
            int index = getPivotIndex(array,left,right);
            //递归左右两部分
            quickSort(array,left,index-1);
            quickSort(array,index+1,right);
        }
    }

    /**
     * 查找基准数下标
     * @param array
     * @param left
     * @param right
     * @return
     */
    public static int getPivotIndex(int[] array, int left, int right){
        int pivot = array[left];//基准数
        while (left < right){
            //从序列最右边开始查找,如果比基准数大,其符合放在基准数右边的要求,则将右下标往前移,直到在右边发现一个比基准数小的数
            while (left < right && array[right] >= pivot){
                right--;
            }
            //将较小数交换到左边去,基准数提前保存到pivot,所以预留出了一个空间,
            // 将右边较小的数放到左边去后,右边的那个位置则空闲出来了,供左边较大的数放过来
            array[left] = array[right];
            //同理,从左边开始查找
            while (left < right && array[left] <= pivot){
                left++;
            }
            array[right] = array[left];
            //直到left=right时结束循环
        }
        // 跳出循环时left和right相等,此时的left或right就是基准数的正确索引位置
        array[left] = pivot;
        return left;
    }

}

8.归并排序

归并排序介绍:

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

归并排序基本思想示意图:

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

治阶段,我们需要将两个已经有序的子序列合并成一个有序序列,比如上图中的最后一次合并,要将[4,5,7,8]和[1,2,3,6]两个已经有序的子序列,合并为最终序列[1,2,3,4,5,6,7,8],实现步骤(类似将两个有序链表合并成一个新的有序链表的感觉):
7.排序算法_第13张图片
7.排序算法_第14张图片
可见合并时用到了一个辅助序列放置合并后的数据,然后再将包含合并后元素的辅助序列拷贝回原序列。

代码实现:

package sort;

import java.util.Arrays;

public class MergeSort {
    public static void main(String[] args) {
        int[] array = { 8, 4, 5, 7, 1, 3, 6, 2 };
        int[] tempArray = new int[array.length];
        System.out.println("初始序列:"+ Arrays.toString(array));
        //快速排序
        mergeSort(array,0,array.length-1, tempArray);
        System.out.println(Arrays.toString(array));
    }

    /**
     * 归并排序递归实现分和治
     * @param array
     * @param left
     * @param right
     * @param tempArray
     */
    public static void mergeSort(int[] array, int left, int right, int[] tempArray){
        if (left<right){
            int lEnd = (left+right)/2;
            //向左递归分解
            mergeSort(array, left, lEnd, tempArray);
            //向右递归分解
            mergeSort(array, lEnd+1, right, tempArray);
            //合并
            merge(array, left, lEnd, right, tempArray);
        }
    }

    /**
     * 递归排序合并(治)阶段
     * @param array 排序的原始数组
     * @param left 左边有序序列的初始索引
     * @param lEnd 边有序序列的末尾索引
     * @param right 右边有序序列的初始索引
     * @param tempArray 中转的辅助数组
     */
    public static void merge(int[] array, int left, int lEnd, int right,int[] tempArray){
        int i = left;//左边序列的当前下标,初始为最左边
        int j = lEnd+1;//右边序列的当前下标,初始为右边序列最左边
        int t = 0;//辅助序列的当前下标
        //将两个有序序列合并为一个新的有序序列
        while (i<=lEnd && j<=right){
            if (array[i] <= array[j]){
                tempArray[t] = array[i++];
            }else {
                tempArray[t] = array[j++];
            }
            t++;
        }
        //经过上面合并后,更长的序列直接插入到合并序列后面
        while (i<=lEnd){
            tempArray[t++] = array[i++];
        }
        while (j<=right){
            tempArray[t++] = array[j++];
        }
        //将合并在辅助数组中的序列拷贝回原数组
        t = 0;
        int arrayIndex = left;
        while (arrayIndex<=right){
            array[arrayIndex++] = tempArray[t++];
        }
    }
}

9.基数排序

基数排序(桶排序)介绍:

(1) 基数排序(radix sort)属于“分配式排序”(distribution sort),又称“桶子法”(bucket sort)或bin sort,顾名思义,它是通过键值的各个位的值,将要排序的元素分配至某些“桶”中,达到排序的作用;
(2) 基数排序法是属于稳定性的排序,基数排序法是效率高的稳定性排序法;
(3) 基数排序(Radix Sort)是桶排序的扩展;
(4) 基数排序是1887年赫尔曼·何乐礼发明的。它的基本实现思路是:将整数按位数切割成不同的数字,然后按每个位数分别比较。

基数排序基本思想:

将所有待比较数值统一为同样的数位长度,数位较短的数前面补零。然后,从最低位(个位)开始,依次进行一次排序。这样从最低位排序一直到最高位排序完成以后, 数列就变成一个有序序列。

图解基数排序思路:

在这里插入图片描述
准备10个桶(10个一维数组),分别存放元素数位(个、十、百…)为0~9的对应元素。
7.排序算法_第15张图片
7.排序算法_第16张图片
7.排序算法_第17张图片
最高数位放完并依次取出后,发现序列已经有序。

代码实现:

package sort;

import java.util.Arrays;

public class RadixSort {

    public static void main(String[] args) {
        int[] array = {53, 3, 542, 748, 14, 214};
        System.out.println("初始序列:"+ Arrays.toString(array));
        //初始10个桶,并且每个桶第一个空间存放的数据为桶中存放的数据个数,序列数据从下标1开始存放。
        int[][] buckets = new int[10][array.length+1];
        //首先找到序列中最大数的数位长度
        int max = array[0];
        for (int i = 1; i < array.length; i++) {
            if (array[i] > max){
                max = array[i];
            }
        }
        int maxLength = (""+max).length();
        //从个位开始对各个数位放入桶中,并依次取出排列成新序列
        for (int i = 0, n = 1; i < maxLength; i++ , n *= 10) {
            //取出每个元素的对应数位放入桶中
            for (int j = 0; j < array.length; j++) {
                int digitOfElement = array[j] / n % 10;
                buckets[digitOfElement][buckets[digitOfElement][0]+1] = array[j];
                buckets[digitOfElement][0] = buckets[digitOfElement][0]+1;
            }
            //按照10个桶的顺序,分别取出里面的所有元素放入到原数组,实现一轮重新排序
            int index = 0;//原数组当前下标
            for (int j = 0; j < buckets.length; j++) {
                for (int k = 0; k < buckets[j][0]; k++) {
                    array[index++] = buckets[j][k+1];
                }
                //取完该桶数据后将该桶的当前存放数量置零
                buckets[j][0] = 0;
            }
            System.out.println("第"+(i+1)+"轮放入桶中并依次取出排列后的序列为:"+Arrays.toString(array));
        }
    }


}

基数排序的说明:

(1) 基数排序是对传统桶排序的扩展,速度很快;
(2) 基数排序是经典的空间换时间的方式,占用内存很大, 当对海量数据排序时,容易造成OutOfMemoryError ;
(3) 基数排序时稳定的。(注:假定在待排序的记录序列中,存在多个具有相同的关键字的记录,若经过排序,这些记录的相对次序保持不变,即在原序列中,r[i]=r[j],且r[i]在r[j]之前,而在排序后的序列中,r[i]仍在r[j]之前,则称这种排序算法是稳定的;否则称为不稳定的);
(4) 有负数的数组,我们一般不用基数排序来进行排序, 如果非要支持负数,可以参考该如下方案:
方案一:将数组中的负数整数分开,使它们成为正数,然后使用基数排序为正值,然后反转并附加排序后的非负数组。
方案二:将所有的数加一个正数,使得所有的数变为正数进行基数排序;排序完之后再将所有数减去最初加的正数值。该方法还可以用来解决小数的情况,所有数乘以一个数后全部变成整数,排序完后再除以。

10.堆排序

堆排序用到二叉树的相关知识,请跳转到文章 11.树结构的应用 查看堆排序的详解。

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

7.排序算法_第18张图片
相关术语解释:

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

时间效率比较测试:
运行结果:
7.排序算法_第19张图片
实现代码:

package sort;

import java.util.Arrays;
import java.util.Collections;

public class SortUtils {
    /**
     * 以10万个数据的数组为例测试
     * 各种排序运行时间效率比较:冒泡排序 < 选择排序 < 插入排序 < 希尔排序 < 快速排序 <= 归并排序 < 基数排序
     * 冒泡排序思路最简单,但是效率最低
     *
     * @param args
     */
    public static void main(String[] args) {
        int num = 100000;//序列大小
        //初始化序列
        int[] array = new int[num];
        for(int i =0; i < num;i++) {
            array[i] = (int)(Math.random() * num*10); //生成一个[0, num*10) 的数,增大10倍增加序列元素的可选范围
        }
        //记录算法运行的起始时间
        long startTime = 0, endTime = 0;
        //冒泡排序
        int[] tempArray = array.clone();
        System.out.println("冒泡排序进行中...");
        startTime = System.currentTimeMillis();
        bubbleSort(tempArray);
        endTime = System.currentTimeMillis();
        System.out.println("冒泡排序运行时间:"+ (endTime-startTime) + "毫秒");
        //选择排序
        tempArray = array.clone();
        System.out.println("选择排序进行中...");
        startTime = System.currentTimeMillis();
        selectSort(tempArray);
        endTime = System.currentTimeMillis();
        System.out.println("选择排序运行时间:"+ (endTime-startTime) + "毫秒");
        //插入排序
        tempArray = array.clone();
        System.out.println("插入排序进行中...");
        startTime = System.currentTimeMillis();
        insertSort(tempArray);
        endTime = System.currentTimeMillis();
        System.out.println("插入排序运行时间:"+ (endTime-startTime) + "毫秒");
        //希尔排序
        tempArray = array.clone();
        System.out.println("希尔排序进行中...");
        startTime = System.currentTimeMillis();
        shellSort(tempArray);
        endTime = System.currentTimeMillis();
        System.out.println("希尔排序运行时间:"+ (endTime-startTime) + "毫秒");
        //快速排序
        tempArray = array.clone();
        System.out.println("快速排序进行中...");
        startTime = System.currentTimeMillis();
        quickSort(tempArray,0,tempArray.length-1);
        endTime = System.currentTimeMillis();
        System.out.println("快速排序运行时间:"+ (endTime-startTime) + "毫秒");
        //归并排序
        tempArray = array.clone();
        int[] temp = new int[array.length];
        System.out.println("归并排序进行中...");
        startTime = System.currentTimeMillis();
        mergeSort(tempArray,0,tempArray.length-1, temp);
        endTime = System.currentTimeMillis();
        System.out.println("归并排序运行时间:"+ (endTime-startTime) + "毫秒");
        //基数排序
        tempArray = array.clone();
        System.out.println("基数排序进行中...");
        startTime = System.currentTimeMillis();
        radixSort(tempArray);
        endTime = System.currentTimeMillis();
        System.out.println("基数排序运行时间:"+ (endTime-startTime) + "毫秒");
        //堆排序
        tempArray = array.clone();
        System.out.println("堆排序进行中...");
        startTime = System.currentTimeMillis();
        heapSort(tempArray);
        endTime = System.currentTimeMillis();
        System.out.println("堆排序运行时间:"+ (endTime-startTime) + "毫秒");
    }

    /**
     * 冒泡排序的时间复杂度 O(n^2)
     * @param array
     */
    public static void bubbleSort(int[] array){
        for (int i = 0; i < array.length; i++) {
            boolean isExchange = false; //记录该趟冒泡是否有做交换
            //依次交换逆序的相邻两个元素
            for (int j = 0; j < array.length-i-1; j++) {
                int temp = 0;
                //满足相邻元素逆序,则交换位置
                if (array[j]>array[j+1]){
                    temp = array[j];
                    array[j] = array[j+1];
                    array[j+1] = temp;
                    isExchange = true;
                }
            }
            //该趟冒泡没有做交换,则序列已经有序,无需做无用循环了
            if (!isExchange){
                break;
            }
        }
    }

    /**
     * 选择排序的时间复杂度 O(n^2)
     * @param array
     */
    public static void selectSort(int[] array){
        for (int i = 0; i < array.length-1; i++) {
            int min = array[i];
            int minIndex = i;
            // 查找当前第i个元素后面的元素是否有比它小的
            for (int j = i+1; j < array.length; j++) {
                if (array[j] < min){
                    min = array[j];
                    minIndex = j;
                }
            }
            //如果当前即为最小值则不需要交换,否则将最小值交换到当前第i个元素的位置
            if (minIndex != i){
                array[minIndex] = array[i];
                array[i] = min;
            }
        }
    }

    /**
     * 插入排序
     * @param array
     */
    public static void insertSort(int[] array){
        for (int i = 1; i < array.length; i++) {
            int insertVal = array[i];
            // 用待插入数与有序列表中元素从后往前比较,每比较一个如果比待插入数更大,则表示待插入数应该在比较数前面,将比较数后移,为插入数让出位置
            // 以此类推直到找到第一个比待插入数小的元素,表示应该插入到该元素后面,结束后移,插入带插入数。
            for (int j = i; j >= 0; j--) {
                if (j==0 || insertVal>=array[j-1]){//若没有比带插入数小的数,即下标为0时,则带插入数为最小数,直接插入到下标0处
                    array[j] = insertVal;
                    break;
                }
                array[j] = array[j-1];
            }
        }
    }

    /**
     * 希尔排序
     * @param array
     */
    public static void shellSort(int[] array){
        for (int gap = array.length/2; gap > 0; gap /= 2) {//分组
            //各个分组中第一个元素为插入排序时的初始有序序列,所以从gap开始,根据增量遍历
            for (int i = gap; i < array.length; i++) {
                int insertVal = array[i];
                int j = 0;
                //从待插入元素前一个元素开始判断,比待插入元素大则后移,为待插入元素留出空间。
                // 直到找到比待插入数小的数,结束后移,插入到该数后面
                for (j = i - gap; j >=0 && insertVal < array[j] ; j -= gap) {
                    array[j+gap] = array[j];
                }
                array[j+gap] = insertVal;
            }

            /* 下面的实现方式更容易理解,但代码没那么简洁。因为没必要非要在单独每个组中进行插入排序,
                可以直接在原序列中根据增量进项插入排序,同样能实现对各个组分别进行插入排序
            //对gap个组分别进行直接插入排序
            for (int i = 0; i < gap; i++) {
                //注意每一组并不是连续的,是按照gap增量跳跃的
                for (int j = i + gap; j < array.length; j += gap) {
                    int insertVal = array[j];
                    for (int k = j; k >= 0 ; k -= gap) {
                        if (k==i || insertVal>=array[k-gap]){
                            array[k] = insertVal;
                            break;
                        }
                        array[k] = array[k-gap];
                    }
                }
            } */
        }
    }

    /**
     * 快速排序
     * @param array
     * @param left
     * @param right
     */
    public static void quickSort(int[] array, int left, int right){
        if (left < right){
            int index = getPivotIndex(array,left,right);
            //递归左右两部分
            quickSort(array,left,index-1);
            quickSort(array,index+1,right);
        }
    }

    /**
     * 快速排序查找基准数下标
     * @param array
     * @param left
     * @param right
     * @return
     */
    public static int getPivotIndex(int[] array, int left, int right){
        int pivot = array[left];//基准数
        while (left < right){
            //从序列最右边开始查找,如果比基准数大,其符合放在基准数右边的要求,则将右下标往前移,直到在右边发现一个比基准数小的数
            while (left < right && array[right] >= pivot){
                right--;
            }
            //将较小数交换到左边去,基准数提前保存到pivot,所以预留出了一个空间,
            // 将右边较小的数放到左边去后,右边的那个位置则空闲出来了,供左边较大的数放过来
            array[left] = array[right];
            //同理,从左边开始查找
            while (left < right && array[left] <= pivot){
                left++;
            }
            array[right] = array[left];
            //直到left=right时结束循环
        }
        // 跳出循环时left和right相等,此时的left或right就是基准数的正确索引位置
        array[left] = pivot;
        return left;
    }

    /**
     * 归并排序递归实现分和治
     * @param array
     * @param left
     * @param right
     * @param tempArray
     */
    public static void mergeSort(int[] array, int left, int right, int[] tempArray){
        if (left<right){
            int lEnd = (left+right)/2;
            //向左递归分解
            mergeSort(array, left, lEnd, tempArray);
            //向右递归分解
            mergeSort(array, lEnd+1, right, tempArray);
            //合并
            merge(array, left, lEnd, right, tempArray);
        }
    }

    /**
     * 递归归并排序合并(治)阶段
     * @param array 排序的原始数组
     * @param left 左边有序序列的初始索引
     * @param lEnd 边有序序列的末尾索引
     * @param right 右边有序序列的初始索引
     * @param tempArray 中转的辅助数组
     */
    public static void merge(int[] array, int left, int lEnd, int right,int[] tempArray){
        int i = left;//左边序列的当前下标,初始为最左边
        int j = lEnd+1;//右边序列的当前下标,初始为右边序列最左边
        int t = 0;//辅助序列的当前下标
        //将两个有序序列合并为一个新的有序序列
        while (i<=lEnd && j<=right){
            if (array[i] <= array[j]){
                tempArray[t] = array[i++];
            }else {
                tempArray[t] = array[j++];
            }
            t++;
        }
        //经过上面合并后,更长的序列直接插入到合并序列后面
        while (i<=lEnd){
            tempArray[t++] = array[i++];
        }
        while (j<=right){
            tempArray[t++] = array[j++];
        }
        //将合并在辅助数组中的序列拷贝回原数组
        t = 0;
        int arrayIndex = left;
        while (arrayIndex<=right){
            array[arrayIndex++] = tempArray[t++];
        }
    }

    /**
     * 基数排序
     * @param array
     */
    public static void radixSort(int[] array){
        //初始10个桶,并且每个桶第一个空间存放的数据为桶中存放的数据个数,序列数据从下标1开始存放。
        int[][] buckets = new int[10][array.length+1];
        //首先找到序列中最大数的数位长度
        int max = array[0];
        for (int i = 1; i < array.length; i++) {
            if (array[i] > max){
                max = array[i];
            }
        }
        int maxLength = (""+max).length();
        //从个位开始对各个数位放入桶中,并依次取出排列成新序列
        for (int i = 0, n = 1; i < maxLength; i++ , n *= 10) {
            //取出每个元素的对应数位放入桶中
            for (int j = 0; j < array.length; j++) {
                int digitOfElement = array[j] / n % 10;
                buckets[digitOfElement][buckets[digitOfElement][0]+1] = array[j];
                buckets[digitOfElement][0] = buckets[digitOfElement][0]+1;
            }
            //按照10个桶的顺序,分别取出里面的所有元素放入到原数组,实现一轮重新排序
            int index = 0;//原数组当前下标
            for (int j = 0; j < buckets.length; j++) {
                for (int k = 0; k < buckets[j][0]; k++) {
                    array[index++] = buckets[j][k+1];
                }
                //取完该桶数据后将该桶的当前存放数量置零
                buckets[j][0] = 0;
            }
        }
    }

    public static void heapSort(int[] array){
        //初始i指向最底层的最右边的非叶子节点(最后一个非叶子节点),将完全二叉树构建成一个大顶堆
        for (int i = array.length/2-1; i >= 0; i--) {
            adjustToBigHeap(array,i,array.length);
        }
        //将堆顶节点交换到数组尾部,将去掉尾部的数组(二叉树)再次调整为大顶堆,循环进行该过程,直到只剩一个节点
        for (int j = array.length-1; j > 0; j--) {
            int temp = array[j];
            array[j] = array[0];
            array[0] = temp;
            //只变了堆顶一个元素,将该较小值不断往下层移即可
            adjustToBigHeap(array, 0, j);
        }
    }

    /**
     * 堆排序中将二叉树调整为大顶堆
     * @param array 待调整二叉树的顺序存储数组
     * @param i 非叶子节点下标
     * @param length 截取的二叉树的顺序存储数组的长度(堆顶元素交换到数组末尾后不再参与大顶堆的构建了)
     */
    public static void adjustToBigHeap(int[] array, int i, int length){
        //初始指向该非叶子节点的左子节点
        for (int k = i*2+1; k < length; k = k*2+1) {
            int nodeData = array[i];
            //以该非叶子节点为树根,将该树调整为大顶堆
            if (k+1<length && array[k]<array[k+1]){//右子节点存在,且左子节点的值小于右子节点,则k指向较大值
                k++;
            }
            if (array[k]>nodeData){//该非叶子节点的叶子节点值比它大,则交换
                array[i] = array[k];
                array[k] = nodeData;
                //该非叶子节点交换后,可能导致子树混乱,不再为大顶堆。所以需要将该较小值不断往下移,直到满足大顶堆条件。
                //所以循环进行,继续访问交换后的较小值的子节点,将子节点的值同这个较小值比较,比它大则较小值继续下移,比它小则已经是大顶堆。
                i = k;
            }else {//已经满足大顶堆条件则不需要交换,子树也不会混乱,直接退出循环。
                break;
            }
        }
    }
}

你可能感兴趣的:(数据结构与算法)