(四)排序算法(Java实现)

本篇博客中总结了以下经典排序算法:

冒泡排序、快速排序、简单插入排序、希尔排序、简单选择排序、堆排序、二路归并排序、计数排序、桶排序、基数排序。

经典的排序算法分为下面两大类:

非线性时间比较类排序:通过比较来决定元素间的相对次序。时间复杂度不能突破O(nlogn)而得此名称。

线性时间非比较类排序:不能通过比较来决定元素间的相对次序。它可以突破比较排序的时间下限,以线性时间运行而得此称。

 


目录

一、交换排序

1.1、冒泡排序

1.2、快速排序

二、插入排序

2.1简单插入排序

2.2、希尔(shell)排序

三、选择排序

3.1、简单选择排序

3.2、堆排序

四、归并排序

4.1、二路归并排序

4.2、多路归并排序

五、线性时间非比较类排序

5.1、计数排序

5.2、桶排序

5.3、基数排序


 

一、交换排序

1.1、冒泡排序

   在深入学习更多排序算法后和在实际使用情况中,冒泡排序的使用还是极少的。它适合数据规模很小的时候,而且它的效率也比较低,但是作为入门的排序算法,还是值得学习的。

    冒泡排序是一种简单的排序算法。它重复地走访要排序的数列,一次比较两个元素,如果它们的顺序错误就把它们交换过来。走访数列的工作是重复地进行直到没有再需要交换,也就是说该数列已经排序完成。这个算法的名字由来是因为越小的元素会经由交换慢慢“浮”到数列的顶端。

算法步骤描述:

①比较两个相邻的元素。如果第一个比第二个大,就交换它们两个;

②对每一对相邻元素作同样的工作,从开始第一对到结尾的最后一对,这样在最后的元素应该会是最大的数;

③针对所有的元素重复以上的步骤,除了最后一个;

④重复步骤①~③,直到排序完成。

 

代码实现:

package com.review08.sort;

public class BubbleSort {
    public static  void bubblesort(int arr[]) {
        int temp = 0;
        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]) { //相邻两个数比较,前面的数大就交换
                    temp = arr[j];
                    arr[j] = arr[j+1];
                    arr[j+1] = temp;
                }
            }
            System.out.println("\n第"+(i+1)+"趟:");
            printarr(arr);
        }
    }

    public static void printarr(int [] arr) {
        for (int i=0; i<arr.length; i++) {
            System.out.print(arr[i] +"  ");
        }
    }

    public static void main(String[] args) {
        int[] arr = {9,8,7,6,5,4,3,2,1};
        System.out.println("排序前:");
        printarr(arr);

        bubblesort(arr);
    }
}

 

选择排序方法时,冒泡排序显然不够理想,看看下面改良后的快速排序。

 

 

1.2、快速排序

    所谓快速排序:基于分治的思想,是冒泡排序的改进型。首先在数组中选择一个基准点并把基准点放于序列的开头,这里就基准点就选取数组的第一个元素; 然后分别从数组的两端扫描数组,设两个指示标志(i 指向起始位置,j 指向末尾),首先从后半部分开始,如果发现有元素比该基准点的值小,就交换 i 和 j 位置的值,然后从前半部分开始扫秒,发现有元素大于基准点的值,就交换 i 和 j 位置的值,如此往复循环,直到 i >= j ,然后把基准点的值放到 i 这个位置,一次排序就完成了;之后再采用递归的方式分别对前半部分和后半部分排序,当前半部分和后半部分均有序时该数组自然也就有序了。

看下面的例子:给5,8,3,6,7,9,2用快速排序法排序

第一趟:

快速排序代码实现(用到了递归哦!):

package com.review08.sort;

import java.lang.reflect.Array;

public class QuickSort {

    /**寻找所标记数的每一趟排序后的最终位置(数组下标)
     * @param arr  要排序的数组
     * @param s  开始位置
     * @param e  结束位置
     * @return i  中间位置
     */
    public static int sort(int arr[], int s, int e) { //寻找中间位置
        int mark = arr[s];
        int i = s;
        int j = e;
        while(i<j) {  //直到i、j相遇
            while (i < j) { //j负责找小的,扔给i
                if(arr[j] < mark) {
                    arr[i] = arr[j];
                    break;  //扔给j后,退出里面的while循环,让i走
                }
                j--; //所在位置比mark的值大,就继续向前走
            }
            while (i < j) {
                if(arr[i] >= mark) { //i负责找大的,扔给j
                    arr[j] = arr[i];
                    break;  //扔给i后,退出里面的while循环,让i走
                }
                i++;  //所在位置比mark的值小,就继续向前走
            }
        }
        arr[i] = mark;  //把标记的那个数放在i位置上

        return i;//最后i所在位置就是中间位置(i所在位置的前面的数都比它小,后面的都比它大)
    }

    public static void quicksort(int arr[], int s, int e) {  //这里用到了递归算法!
        if(s<e) {
            int index = sort(arr, s, e); //中间那个位置
            quicksort(arr, s, index-1); //中间位置的左边
            quicksort(arr, index+1, e); //中间位置的右边
        }
    }

    public static void printarr(int arr[]) { //打印数组
        for (int i=0; i<arr.length; i++) {
            System.out.print(arr[i]+"  ");
        }
    }
    
    public static void main(String[] args) {
        int[] arr = {6,7,5,4,8,9,1,2};

        sort(arr, 0, arr.length-1);
        System.out.println("第一趟排序结果:");
        printarr(arr);

        quicksort(arr, 0, arr.length-1);
        System.out.println("\n最终排序结果:");
        printarr(arr);
    }
}

 

 

 

二、插入排序

2.1简单插入排序

 

把表分成两部分,前半部分已排序,后半部分未排序,后半部分元素依次一个一个插入前半部分适当的位置。

假设线性中前 j-1元素已经有序,现在要将线性表中第 j 个元素插入到前面的有序子表中,插入过程如下:

将第 j 个元素放到一个变量T中,然后从有序子表的最后一个元素(即线性表中第 j-1个元素)开始,往前逐个与T进行比较,将大于T的元素均依次向后移动一个位置,直到发现一个元素不大于T为止,此时就将T(即原线性表中的第 j 个元素)插入到刚移出的空位置上,有序子表的长度就变为 j 了。效率与冒泡排序法相同。

 

例:15,45,84,48,79,10,35,34,88

(15) |   45    84    48     79    10    35    34    88   

(15    45) |   84    48     79    10    35    34    88   

(15    45     84) |  48     79    10    35    34    88   

(15    45     48    84) |   79    10    35    34    88   

(15    45     48    79     84) |  10    35    34    88   

(10    15     45    48     79    84) |  35    34    88   

(10    15     35    45     48    79    84) 34    88   

(10    15     34    35     45    48    79    84) |  88  

(10    15     34    35     45    48    79    84    88) 

 

代码实现:

package com.review08.sort;

public class SimpleInsertSort {

    public static void simpleinsert(int[] arr) {
        if(arr.length<1) {
            return;
        }
        for(int i=1; i<arr.length; i++) {//注意,i从1开始,我们是从arr[1]开始与前面的元素比较的
            //因为0~i-1是有序的,如果arr[i]>arr[i-1],则0~i也是有序的
            if(arr[i]<arr[i-1]) {
                int temp = arr[i];//先保存i位置元素的值
                int j = i-1;//从i-1开始向前找,一直找到比i位置元素小的位置,然后插入
                for(; j>=0&&arr[j]>temp; j--) {
                    arr[j+1] = arr[j];//没有找到就将此位置的元素向后移一位,腾出位置
                }
                arr[j+1] = temp;//将i位置元素放在腾出的位置上面

            }

        }

    }

    public static void main(String[] args) {
        int[] arr = {41,12,8,65,32,75,11,29};
        System.out.println("简单插入排序前:");
        printArr(arr);
        simpleinsert(arr);
        System.out.println("\n简单插入排序后:");
        printArr(arr);
    }

    public static void printArr(int[] arr) {
        for(int i=0; i<arr.length; i++) {
            System.out.print(arr[i]+"  ");
        }
    }

}

 

 

 

2.2、希尔(shell)排序

 希尔排序严格来说是基于插入排序的思想,又被称为缩小增量排序。

 具体流程如下:

 1、将包含n个元素的数组,分成d=n/2个数组序列,第一个数据和第n/2+1个数据为一对...

 2、对每对数据进行比较和交换,排好顺序;

 3、然后分成d=n/4个数组序列,再次排序;

 4、不断重复以上过程,随着d减少并直至为1,排序完成。

下面举个例:

原始序列:18,35,94,47,61,28,17,98,共8个数

代码实现

public class ShellSort {
    //法一:
    public static void shellSort1(int[] arr) {
        int i,j,r,temp;
        r = arr.length/2;
        for( ; r>=1; r=r/2) { //划分组,间隔为r
            for(i=r;i<arr.length;i++) {
                temp = arr[i];
                j = i - r;
                //一轮排序
                while(j >=0 && temp <arr[j]) {
                    arr[j+r] = arr[j];
                    j -= r;
                }
                arr[j+r] = temp;
            }
        }
    }
    //法二:
    public static void shellSort2(int[] arr) {
        if(arr == null && arr.length <= 1) {
            return;
        }
        int increment = arr.length/2;//增量
        while(increment >= 1) {
            for(int i=0; i<arr.length-increment; i++) {
                for(int j=i; j<arr.length-increment; j=j+increment) {
                    if(arr[j]>arr[j+increment]) {
                        int temp = arr[j];
                        arr[j] = arr[j+increment];
                        arr[j+increment] = temp;
                    }
                }
            }
            increment = increment/2; //设置新的增量
        }
    }
}

 

 

三、选择排序

3.1、简单选择排序

基本思想(数组长度为n):

第一次遍历n-1个数,找到最小的数值与第一个元素交换; 

第二次遍历n-2个数,找到最小的数值与第二个元素交换;

...

第n-1次遍历,找到最小的数值与第n-1个元素交换,排序完成

看下面的例子:

代码实现简单交换排序

思路:第一个层循环遍历数组,第二层循环找到剩余元素中最小值的索引,内层循环结束,交换数据。 内层循环每结束一次,排好一位数据。两层循环结束,数据排好有序。

public static void simpleswap(int[] arr) {
    for(int i=0; i<arr.length; i++) {  //负责遍历索引
        int index =i ;//index负责找最小的索引
        for(int j=i+1; j<arr.length; j++) { //负责找剩余元素中的最小值
            if(arr[j]<arr[index]) {
                index = j;
            }
        }
        //每找到一次就与前面的交换一次
        if(i != index){
            int temp = arr[index];
            arr[index] = arr[i];
            arr[i] = temp;
        }
    }
}

 

3.2、堆排序

下一篇文章二叉树里讲的是小根堆实现的堆排序,大根堆实现原理也差不多!

    堆是一棵顺序存储的完全二叉树。完全二叉树中所有非终端节点的值均不大于(或不小于)其左、右孩子节点的值。

    其中每个结点的值小于等于其左、右孩子的值,这样的堆称为小根堆; 其中每个结点的值大于等于其左、右孩子的值,这样的堆称为大根堆。

 

堆排序的大概步骤如下:

  1. 构建大根堆;
  2. 选择顶,并与第0位置元素交换;
  3. 由于步骤2的的交换可能破环了大根堆的性质,第0不再是最大元素,需要调用maxHeap调整堆(沉降法),如果需要重复步骤2。

    堆排序中最重要的算法就是maxHeap,该函数假设一个元素的两个子结点都满足最大堆的性质(左右子树都是最大堆),只有跟元素可能违反最大堆性质,那么把该元素以及左右子结点的最大元素找出来,如果该元素已经最大,那么整棵树都是最大堆,程序退出,否则交换跟元素与最大元素的位置,继续调用maxHeap原最大元素所在的子树。该算法是分治法的典型应用。

 

 

四、归并排序

    归并排序 (merge sort) 是一类与插入排序、交换排序、选择排序不同的另一种排序方法。归并的含义是将两个或两个以上的有序表合并成一个新的有序表。归并排序有多路归并排序、两路归并排序 , 可用于内排序,也可以用于外排序。这里仅仅写到内排序的归并方法!

4.1、二路归并排序

 

代码实现二路归并排序:

package com.review08.sort;

public class TwoWaysMergeSort {
    private static int number = 0;

    public static void main(String[] args) {
        int[] arr = {10,15,33,8,48,11,95,87,38};
        printArr("排序前:",arr);
        twomergesort(arr);
        printArr("\n排序后:",arr);
    }

    public static void printArr(String pre, int[] a) {
        System.out.print(pre);
        for (int i = 0; i < a.length; i++) {
            System.out.print(a[i] + "  ");
        }
    }

    private static void twomergesort(int[] arr) {
        System.out.println("\n**************开始排序**************");
        sort(arr, 0, arr.length-1);
    }

    private static void sort(int[] arr, int left, int right) {
        if (left >= right) {
            return;
        }

        int middle = (left + right) / 2;

        //二路归并里面有两个sort,多路归并里面有多个sort
        sort(arr, left, middle);
        sort(arr, middle + 1, right);
        merge(arr, left, middle, right);
    }

    private static void merge(int[] arr, int left, int middle, int right) {
        int[] temp = new int[arr.length];
        int r1 = middle + 1;
        int left1 = left;
        int left2 = left;

        //逐个归并
        while (left <= middle && r1 <= right) {
            if (arr[left] <= arr[r1]) {
                temp[left1++] = arr[left++];
            } else {
                temp[left1++] = arr[r1++];
            }
        }
        //将左边剩余的归并
        while (left <= middle) {
            temp[left1++] = arr[left++];
        }
        //将右边剩余的归并
        while (r1 <= right) {
            temp[left1++] = arr[r1++];
        }

        System.out.print("第"+(++number)+"趟排序:");
        //从临时数组拷贝到原数组
        while(left2 <= right) {
            arr[left2] = temp[left2];
            //输出中间归并排序结果
            System.out.print(arr[left2]+"\t ");
            left2++;
        }
        System.out.println();
    }
}

 

 

 

 

 

4.2、多路归并排序

多路归并排序涉及到的情况较为复杂,这里略过!

————————————————————————————————————————————————

    常见的快速排序、归并排序、堆排序、冒泡排序等属于比较排序。在排序的最终结果里,元素之间的次序依赖于它们之间的比较。每个数都必须和其他数进行比较,才能确定自己的位置。在冒泡排序之类的排序中,问题规模为n,又因为需要比较n次,所以平均时间复杂度为O(n²)。在归并排序、快速排序之类的排序中,问题规模通过分治法消减为logN次,所以时间复杂度平均O(nlogn)。比较排序的优势是,适用于各种规模的数据,也不在乎数据的分布,都能进行排序。可以说,比较排序适用于一切需要排序的情况。

    计数排序、基数排序、桶排序则属于非比较排序。非比较排序是通过确定每个元素之前,应该有多少个元素来排序。针对数组arr,计算arr[i]之前有多少个元素,则唯一确定了arr[i]在排序后数组中的位置。非比较排序只要确定每个元素之前的已有的元素个数即可,所有一次遍历即可解决。算法时间复杂度O(n)。非比较排序时间复杂度底,但由于非比较排序需要占用空间来确定唯一位置。所以对数据规模和数据分布有一定的要求。

——————————————————————————————————————————————————

 

五、线性时间非比较类排序

5.1、计数排序

    计数排序需要占用大量的空间,仅仅只适用要比较的数据范围很集中的情况,比如0~1000,10000~20000,...这样在一定范围的数据。

需要三个数组:
待排序数组 int[] before= new int[]{4,3,6,3,5,1};
辅助计数数组 int[] arr= new int[max - min + 1]; //该数组大小为待排序数组中的最大值减最小值+1
输出数组 int[] res = new int[before.length];

1.求出待排序数组before的最大值max=6, 最小值min=1
2.实例化辅助计数数组arr,arr数组中每个下标对应before中的一个元素,arr用来记录每个元素出现的次数
3.计算 before中每个元素在arr中的位置 position = before[i] - min,此时 arr= [1,0,2,1,1,1]; (3出现了两次,2未出现)
4.根据 arr数组求得排序后的数组,此时 res = [1,3,3,4,5,6]

例:

原始数列:105    110    105    102    111    107    108    104    110    105

 

Java代码实现计数排序:

package com.review08.sort;

public class CountSort {

    public static int[] countSort(int[] before) {
        //找出数组中的最大值和最小值
        int max = Integer.MIN_VALUE;
        int min = Integer.MAX_VALUE;
        for(int i=0; i<before.length; i++) {
            max = Math.max(max,before[i]);
            min = Math.min(min,before[i]);
        }

        int[] arr = new int[max-min+1]; //记录出现次数的数组

        //找出每个数字出现的次数
        for (int i=0; i<before.length; i++) {
            int index = before[i]-min;
            arr[index] ++;
        }

        //计算每个数字在排序后数组中应该出现的位置
        for(int i=1; i<arr.length; i++) {
            arr[i] = arr[i] +arr[i-1];
        }

        //根据arr数组排序
        int res[] = new int[before.length];
        for(int i=0; i<before.length; i++) {
            int index = --arr[before[i]-min];
            res[index] = before[i];
        }
        return res;
    }
    //测试
    public static void main(String[] args) {
        int [] before = {105,110,105,102,111,107,108,104,110,105,105};
        printArr(countSort(before));
    }
    //打印数组元素
    public static void printArr(int[]arr) {
        for(int i=0; i<arr.length; i++) {
            System.out.print(arr[i] + "  ");
        }
    }
}

 

 

 

5.2、桶排序

    桶排序与计数排序恰恰相反,桶排序可用于最大值与最小值相差较大的情况,比如[106,1902,47867,83556,102456]。不过计数排序是桶排序的一种特殊情况,可以把计数排序当成每个桶里只有一个元素的情况。

    桶排序要求数据的分布必须均匀,否则可能导致数据都集中到一个桶中。比如[114,170,127,132,20000], 这种数据会导致前4个数都集中到同一个桶中。导致桶排序失效。

桶排序步骤:

1.找出待排序数组中的最大值max、最小值min
2.我们使用动态数组ArrayList 作为桶,桶里放的元素也用 ArrayList 存储。桶的数量为(max-min)/arr.length+1
3.遍历数组 arr,计算每个元素 arr[i] 放的桶
4.每个桶各自排序
5.遍历桶数组,把排序好的元素放进输出数组

简言之,就是把数组 arr 划分为n个大小相同子区间(桶),每个子区间各自排序,最后合并。

 

代码后期更新!

 

 

5.3、基数排序

    思想:把待排序的整数按位分,分为个位,十位.....从小到大依次将位数进行排序。实际上分为两个过程:分配和收集。 分配就是:从个位开始,按位数从小到大把数据排好,分别放进0--9这10个桶中; 收集就是:依次将0-9桶中的数据放进数组中,重复这两个过程直到最高位。

例:

代码实现:

package com.review08.sort;

import java.util.Arrays;

public class RadixSort {
    public static void main(String[] args) {
        int a[] = {400,52,61,84,96,195,26};
        radixSort(a,10,3);
        for(int i=0; i<a.length; i++) {
            System.out.print(a[i] + "  ");
        }
    }

    /**
     * 基数排序
     * @param arr 待排序数组
     * @param radix  基数(10,桶的个数)
     * @param distance  待排序中,最大的位数
     */
    public static void radixSort(int[] arr, int radix, int distance) {
        int length = arr.length;
        int[] temp = new int[length]; //用于暂存元素
        int[] count = new int[radix]; //用于计数排序的桶
        int divide = 1;//除数,用于取个位数数字、十位数数字、百位数数字...

        for(int i=0; i<distance; i++) {
            System.arraycopy(arr,0,temp,0,length);
            Arrays.fill(count,0); //桶清空

            for(int j=0; j<length; j++) { //这个循环用于吧每个数的个十百千分开,并使相对应号数的桶的个数增加1
                //divide:1  10  100  radix:10
                int tempKey = (temp[j]/divide) % radix;
                count[tempKey] ++;
            }

            for (int j=1; j<radix; j++) {
                count[j] = count[j] + count[j-1];
            }

            for(int j=length-1; j>=0; j--) {
                int tempKey = (temp[j]/divide) % radix;
                count[tempKey] --;
                arr[count[tempKey]] = temp[j];
            }
            divide = divide * radix;  //1  10  100
        }
    }
}

 


补充

最后附上最近看马士兵老师讲的视频中的对排序写的一首宋词。

画红圈圈的四种排序算法是一定要掌握的!时间复杂度、空间复杂度和稳定性(参看马士兵老师的宋词)也是要掌握的!

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值