Java算法——排序

Posted 364.99°

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Java算法——排序相关的知识,希望对你有一定的参考价值。



1.冒泡排序

1.1.排序原理

  1. 相邻元素(arr[j]与arr[j+1]) 比较,前一个元素比后一个元素大,则交换两元素的位置
  2. 对每一对相邻元素做上述操作,从第一对元素到最后一对元素,最终实现最大的元素排在最后一位
  3. 重复上述操作,直到排序完成

1.2.代码实现

public class Bubble {
    public static void sort(int []arr){
        for (int i = arr.length-1;i > 0;i--){//每次
            for (int j = 0;j < i;j++){
                int temp;
                //比较,交换顺序
                if (arr[j] > arr[j+1]){
                    temp = arr[j];
                    arr[j] = arr[j+1];
                    arr[j+1] = temp;
                }
            }
        }
    }
}
import java.util.Arrays;

public class TestSort {
    public static void main(String[] args) {
        int[] a = {4,5,9,7,1,3,5};
        System.out.println("排序前数组:" + Arrays.toString(a));
        Bubble.sort(a);
        System.out.println("排序后数组:" + Arrays.toString(a));
    }
}

1.3.复杂度分析

双层for循环,内循环完成排序,故主要分析内循环

最坏情况: 数组完全逆序

  1. 元素比较次数:(N-1)+(N-2)+…+2+1 = N^2/2 - N/2
  2. 元素交换次数:(N-1)+(N-2)+…+2+1 = N^2/2 - N/2
  3. 总执行次数:N^2 - N

O(N^2)

2.选择排序

2.1.排序原理

  1. 找到最小值元素所在位置: 每一次遍历,都设定首位元素为最小值,与其他位的元素进行比较,如果其他位的元素更小,则将其他位设定为最小值,最后可以找到最小值所在的位置
  2. 交换位置: 交换首位元素与最小值元素的位置

2.2.代码实现

public class Selection {
    public static void sort(int[] arr){
        for (int i = 0;i < arr.length-2;i++){
            //设置arr[i]为最小值所在位置
            int minIdex = i;
            for (int j = i+1;j < arr.length;j++){
                if (arr[minIdex] > arr[j]){
                    //交换最小值下标
                    minIdex = j;
                }
            }
            //交换位置
            int temp = arr[i];
            arr[i] = arr[minIdex];
            arr[minIdex] = temp;
        }
    }
}

2.3.复杂度分析

双层for循环,外层实现数据交换,内层实现数据比较,分别统计交换次数与比较次数

最坏情况:

  1. 元素比较次数:(N-1)+(N-2)+…+2+1 = N^2/2 - N/2
  2. 元素交换次数:N-1
  3. 时间复杂度:N^2/2 - N/2 + (N-1) = N^2/2 + N/2 - 1;

O(N^2)

3.插入排序

3.1.排序原理

  1. 分组: 把所有数据分为两组,已经排序和未经排序(默认第一个元素为已排序元素)
  2. 选择插入元素: 将未排序组中的第一个元素,插入已排序组中
  3. 倒序比较插入: 倒序遍历已排序组的元素,依次和待插入的元素进行比较,直到找到一个元素<=待插入元素,就把待插入元素放置在此位置,其他元素向后移动一位

3.2.代码实现

public class Insertion {
    public static void sort(int[] a){
        for (int i=1;i < a.length;i++){
            //当前未排序首位元素为a[i],与之前的元素进行比较,直到找到小于等于a[i]的元素,跳出循环
            for (int j=i;j > 0;j--){
                if (a[j] < a[j-1]){
                    int temp = a[j-1];
                    a[j-1] = a[j];
                    a[j] = temp;
                }else{
                    //找到最终插入位置,跳出循环
                    break;
                }
            }
        }
    }
}

3.3.复杂度分析

双层for循环,内层完成排序,主要分析内层循环体执行次数

最坏情况

  1. 比较次数:(N-1)+(N-2)+…+2+1 = N^2/2 - N/2
  2. 交换次数:(N-1)+(N-2)+…+2+1 = N^2/2 - N/2
  3. 总执行次数:N^2 - N

O(N^2)

上述三种排序时间复杂度都是O(N^2),不适合大规模输入


以下为一些较为高级的算法,争取降低算法时间的最高次幂

4.希尔排序

又名“缩小增量排序”,插入排序的升级版

4.1.排序原理

  1. 选择定一个增长量h=5,将增长量作为元素分组的依据,对元素进行分组
  2. 对分好的每一组数据完成插入排序
  3. 减少增长量,重复第二步操作,直到增长量h减小为1,完成排序

增长量h的取值规则:

最大值:

int h = 1
while (h < n/2){//n排序数组长度
  h = 2h + 1;
}

减小:h=h/2

4.2.代码实现

public class Shell {
    public static void sort(int[] a){
        //数组长度为n
        int n = a.length;
        //确定增长量h的最大值
        int h = 1;
        while (h < n/2){
            h = 2*h + 1;
        }
        //开始排序
        while (h >= 1){
            for (int i=0;i < n;i++){
                //a[j]为待插入元素,依次和a[j-h],a[j-2h],a[j-3h]...比较,如果a[j]小,则交换位置,否则a[j]完成插入,跳出循环
                for (int j=i;j >= h;j -= h){
                    if (a[j] < a[j-h]){
                        int temp = a[j];
                        a[j] = a[j-h];
                        a[j-h] = temp;
                    }else {
                        break;//跳出循环
                    }
                }
            }
            h /= 2;//h缩减
        }
    }
}

4.3.复杂度分析

因为增长量h没有固定规则,故可以使用事后分析法及逆行分析
经过测试,在处理大量数据时,希尔排序的性能确实优于插入排序

5.归并排序

归并排序是建立在归并操作上的一种有效的排序方法,该算法是采用分治法的一个非常典型的例子。
将已有序的子序列合并,得到完全有序的序列(先使子序列有序,再使子序列间有序),将两个子序列表合并成一个有序表,称为二路归并

5.1.递归

在定义方法时,在方法内部调用方法本身,为递归

将一个大型复杂的问题,层层转换为一个与原问题相似的,规模较小的问题来求解,大大地减少了程序的代码量

注意: 在递归中不能无限制地调用自己,必须要有边界条件,能够让递归结束。每一次地柜都会在栈内开辟新的空间,重新执行方法,递归的层级太深,很容易造成栈内存溢出。

如:实现二叉树的数据填充方法
    通过递归调用add方法,实现左子节点与右子节点的数据插入

public class Node {
    //左子节点
    public Node leftNode;
    //右子节点
    public Node rightNode;
    //值
    public Object value;

    //插入数据的方法
    public void add(Object v){
        //当前节点没有值,就将v赋值给value
        if (null == value)
            value = v;
        else {
            if ((Integer) v - ((Integer) value) <= 0){
                if (null == leftNode)
                    leftNode = new Node();
                    leftNode.add(v);  //使用递归
                }
            else {
                if (null == rightNode)
                    rightNode = new Node();
                    rightNode.add(v);
                }
            }
    }

5.2.归并排序原理

  1. 尽可能地将一组数据拆分为两个元素数相等的子组,并对每一个子组继续拆分,直到拆分后的每个子组的元素个数都是1为止
  2. 将相邻的两个子组进行合并成一个有序的大组
  3. 不断地重复步骤2,直到最终只有一个组为止
    在这里插入图片描述

5.3.代码实现

package Test;

public class Merge {

    //创建辅助数组
    private static int[] assist;

    /*
         排序数组a的元素
     */
    public static void sort(int[] a){
        assist = new int[a.length];

        //创建两个原数组的指针,start:最小索引指针,end:最大索引指针
        int start = 0;
        int end = a.length - 1;

        //对数组a进行拆分,排序,合并
        sort(a,start,end);
    }

    /*
         先拆分,后排序合并数组a
     */
    private static void sort(int[] a,int start,int end){
       //判断,当,end <= start时,说明拆分到最小单位,结束方法
        if (end <= start){
            return;
        }

        //定义一个中间指针,实现从中拆分
        int middle = start + (end - start)/2;

        //递归调用sort进行拆分
        sort(a,start,middle);
        sort(a,middle+1,end);

        //调用merge函数进行归并
        merge(a,start,middle,end);
    }
    /*
         排序,合并数组(归并)
     */
    private static void merge(int[] a,int start,int middle,int end){
        //定义辅助函数插入数据的指针
        int i = start;
        //创建两个子数组的初始位置指针
        int pointer1 = start;
        int pointer2 = middle + 1;

        //当两个子数组指针都未越界时,两子数组进行比较排序插入辅助数组
        while (pointer1 <= middle && pointer2 <= end){
            if (a[pointer1] < a[pointer2]){
                assist[i++] = a[pointer1++];
            }else {
                assist[i++] = a[pointer2++];
            }
        }
        //当有一个子数组的指针到了边界,就只进行剩余的数组的数据插入,以下两循环只进行一个
        while (pointer1 <= middle){
            assist[i++] = a[pointer1++];
        }
        while (pointer2 <= end){
            assist[i++] = a[pointer2++];
        }
        //当assist数组中所有数据完成排序,就拷贝到原数组
        for (int j=start;j <= end;j++){
            a[j] = assist[j];
        }

    }
}

5.4.复杂度分析

归并排序是分治思想最典型的例子,先将数组进行拆分为(尽量)等长的两部分,分别通过递归调用将它们单独排序,最后将有序的数组归并为最终的排序结果。
该递归的出口在于:如果一个数组不能再被分成两个数组,就执行merge进行归并,在归并的时候比较元素的大小进行排序

复杂度分析:

输入元素个数为n,使用归并排序的拆分次数:log2(n),所以会有log2(n)层树,,故自顶向下第k层有2^k个子数组,每个数组长度 2^(log2(n)-k) ,归并最多需要 2^(log2(n)-k) 次比较,因此每层的比较次数为 2^k*2^(log2(n)-k) = 2^log2(n) ,n层就是 log2(n)*2^(log2(n)) = log2(n)*n

O(nlogn)

6.快速排序

冒泡排序法的升级版,基本思想:通过一次排序,将要排序的数据分成独立的两个部分,其中一部分的所有数据都比另一部分的所有数据要小,然后重复此步骤对两个部分数据分别进行排序。

6.1.切分原理

  1. 找一个基准值,用两个指针分别指向数组的头部和尾部
  2. 先从尾部向头部开始搜索一个比基准值小的元素,搜索到即停止,并记录指针的位置
  3. 再从头部向尾部开始搜索一个比基准值大的元素,搜索到即停止,并记录指针的位置
  4. 交换当前左边指针位置和右边指针位置的元素
  5. 重复2,3,4步骤,直到左边指针的值大于右边指针的值停止

6.2.排序原理

  1. 设定一个分界值,通过此分界值将数组分为左右两个部分
  2. 将>=分界值的数据放到数组右侧,<=分界值的数据放到数组左侧,
  3. 对左右两边的数据进行独立排序,两边数组的数据又可以取分界值,分别划分为两个子数组左边较小值,右边较大值
  4. 重复上述步骤,通过递归实现。通过递归先将左侧数据排序,再排序右侧数据,当两侧数据都排序好,整个数组也就排序好了

6.3.代码实现

public class Quick {
    /*
        排序总方法
     */
    public static void sort(int[] a){
        int start = 0;
        int end = a.length - 1;
        //排序素组所有元素
        sort(a,start,end);
    }

    /*
        切分排序
     */
    private static void sort(int[] a,int start,int end){
        //安全性检测
        if (end <= start){
            return;
        }
        //对数组a的start到end的元素进行切分
        int partition = partition(a,start,end);
        //对左子祖进行排序
        sort(a,start,partition - 1);
        //对右子组进行排序
        sort(a,partition + 1,end);
    }

    /*
        切分
     */
    public static int partition(int[] a,int start,int end){
        //基准值(切分中间值)
        int mid = a[start];
        //左指针
        int left = start;
        //右指针
        int right = end + 1;

        //进行切分
        while (true){
            //从右到左扫描,找到比mid小的数值,则停止
            while (a[--right] > mid){
                //当整个数组没有比mid小的数值时,跳出循环
                if (right == left){
                    break;
                }
            }
            //从左到右扫描,找到比mid大的数值,则停止
            while (a[++left] < mid){
                //当整个数组没有比mid大的数值时,跳出循环
                if (left == end){
                    break;
                }
            }
            //交换上述两个步骤扫描到的数值的位置
            if (left >= right){
                //扫描完所有数据
                break;
            }else {
                //交换数组left与right处的值
                exch(a,left,right);
            }
            //right就是切分界限

        }
        //交换最后right索引处和基准值所在的索引处的值
        exch(a,start,right);
        return right;
    }

    private static void exch(int[] a,int i,int j){
        int temp = a[i];
        a[i] = a[j];
        a[j] = temp;
    }
}

6.4.复杂度分析

快速排序的一次切分从两头开始交替搜索,直到left和right重合,因此,一次切分算法的时间复杂度为O(n),但整个快速排序的时间复杂度和切分次数有关

最优情况:

每一次切分选择的基准数字刚好将当前序列等分在这里插入图片描述
共切分logn
时间复杂度:O(nlogn)

最坏情况:

每一次切分选择的基准数字是当前序列中最大数或者最小数,这使得每次切分都会有一个子组,那么总
共就得切分n次
在这里插入图片描述
时间复杂度为O(n^2)

7.排序的稳定性

数组arr中有若干元素,其中A元素和B元素相等,并且A元素在B元素前面,如果使用某种排序算法排序后,能够保
证A元素依然在B元素的前面,可以说这个该算法是稳定的

7.1.稳定性的意义

如果一组数据只需要一次排序,则稳定性一般是没有意义的,如果一组数据需要多次排序,稳定性是有意义的。例
如要排序的内容是一组商品对象,第一次排序按照价格由低到高排序,第二次排序按照销量由高到低排序,如果第
二次排序使用稳定性算法,就可以使得相同销量的对象依旧保持着价格高低的顺序展现,只有销量不同的对象才需
要重新排序。这样既可以保持第一次排序的原有意义,而且可以减少系统开销

7.2.常见排序算法的稳定性

冒泡排序

只有当arr[i]>arr[i+1]的时候,才会交换元素的位置,而相等的时候并不交换位置,所以冒泡排序是一种稳定的排序
算法

选择排序

选择排序是给每个位置选择当前元素最小的,例如有数据{5(1),8 ,5(2), 2, 9 },第一遍选择到的最小元素为2,
所以5(1)会和2进行交换位置,此时5(1)到了5(2)后面,破坏了稳定性,所以选择排序是一种不稳定的排序算法

插入排序

比较是从有序序列的末尾开始,也就是想要插入的元素和已经有序的最大者开始比起,如果比它大则直接插入在其
后面,否则一直往前找直到找到它该插入的位置。如果碰见一个和插入元素相等的,那么把要插入的元素放在相等
元素的后面。所以,相等元素的前后顺序没有改变,从原无序序列出去的顺序就是排好序后的顺序,所以插入排序
稳定的

希尔排序

希尔排序是按照不同步长对元素进行插入排序 ,虽然一次插入排序是稳定的,不会改变相同元素的相对顺序,但在不同的插入排序过程中,相同的元素可能在各自的插入排序中移动,最后其稳定性就会被打乱,所以希尔排序是不稳定的

归并排序

归并排序在归并的过程中,只有arr[i]<arr[i+1]的时候才会交换位置,如果两个元素相等则不会交换位置,所以它
并不会破坏稳定性,归并排序是稳定的

快速排序

快速排序需要一个基准值,在基准值的右侧找一个比基准值小的元素,在基准值的左侧找一个比基准值大的元素,然后交换这两个元素,此时会破坏稳定性,所以快速排序是一种不稳定的算法


以上是关于Java算法——排序的主要内容,如果未能解决你的问题,请参考以下文章

片段(Java) | 机试题+算法思路+考点+代码解析 2023

算法排序之堆排序

java排序算法

快速排序-递归实现

排序算法之冒泡选择插入排序(Java)

排序算法之冒泡选择插入排序(Java)