八种经典的排序算法

排序算法

排序的分类

1) 内部排序:
指将需要处理的所有数据都加载到内部存储器(内存)中进行排序
2) 外部排序法:
数据量过大,无法全部加载到内存中,需要借助外部存储(文件等)进行排序。
3)常见的内部排序算法有:插入排序、希尔排序、选择排序、冒泡排序、归并排序、快速排序、堆排序、基数排序等。

冒泡排序

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

在这里插入图片描述

​ 代码实现:

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;
            }
        }
    }
}

​ 优化:因为排序的过程中,各元素不断接近自己的位置,如果一趟比较下来没有进行过交换,就说明序列有序,因此要在排序过程中设置一个flag判断元素是否进行过交换,一次冒泡中没有发生一次交换,可以提前结束排序,从而减少不必要的比较。

代码:

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;
            }
        }
        if (!flag){
            break;
        }else {
            flag=false;
        }
    }
}

选择排序

​ 选择排序(select sorting)的基本思想是:每一次遍历的过程中,都假定第一个索引处的元素是最小值,和其他索引处的值依次进行比较,如果当前索引处 的值大于其他某个索引处的值,则假定其他某个索引出的值为最小值,最后可以找到最小值所在的索引,最后交换第一个索引处和最小值所在的索引处的值。

在这里插入图片描述

总共通过 n-1次排序(n为待排序的个数)

代码:

public static void selectSort(int[] arr){
    int temp = 0;
    for (int i=0;i<arr.length-1;i++){
        for (int j=i+1;j<arr.length;j++){
            if (arr[i]>arr[j]){
                temp=arr[i];
                arr[i]=arr[j];
                arr[j]=temp;
            }
        }
    }
}

插入排序

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

在这里插入图片描述

代码1:

public static void insertSort1(int[] a) {
    int temp = 0;
    for (int i = 1; i < a.length; i++) {
    //当前元素为a[i],依次和i前面的元素比较,找到一个小于等于a[i]的元素
        for (int j = i; j > 0; j--) {
            if (a[j - 1] > a[j]) {
                //交换元素
                temp = a[j - 1];
                a[j - 1] = a[j];
                a[j] = temp;

            } else {
                //找到了该元素,结束
                break;
            }
        }
    }
}

代码2:

public static void insertSort2(int[] arr){
    int insertVal=0;
    int insertIndex=0;
    for (int i=1;i<arr.length;i++){
        //定义待插入的数
        insertVal=arr[i];
        insertIndex=i-1; //即arr[1]的前面这个数的下标

        //给insertVal找到插入的位置
        //1.insertIndex>=0 保证在给insertVal找插入位置,不越界
        //2.insertVal<arr[insertIndex] 待插入的数,找到插入的位置
        //3.就需要将arr[insertIndex]后移
        while (insertIndex>=0 && insertVal<arr[insertIndex]){
            arr[insertIndex+1]=arr[insertIndex];
            insertIndex--;
        }
        //当退出while循环时,说明插入的位置找到,insertIndex+1

        //这里我们判断是否需要赋值
        arr[insertIndex+1]=insertVal;
    }
}

希尔排序

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

​ 前面学习插入排序的时候,我们会发现一个很不友好的事儿,如果已排序的分组元素为{2,5,7,9,10},未排序的分组 元素为{1,8},那么下一个待插入元素为1,我们需要拿着1从后往前,依次和10,9,7,5,2进行交换位置,才能完成真 正的插入,每次交换只能和相邻的元素交换位置。那如果我们要提高效率,直观的想法就是一次交换,能把1放到 更前面的位置,比如一次交换就能把1插到2和5之间,这样一次交换1就向前走了5个位置,可以减少交换的次数, 这样的需求如何实现呢?接下来我们来看看希尔排序的原理。

希尔排序法基本思想:

  • 希尔排序是把数据序列按下标的一定增量分组,对每组使用直接插入排序算法排序;
  • 随着增量逐渐减少,每组包含的数据个数越来越多,当增量减至1时,整个文件恰被分成一组,整个数据序列就已经排序好了,算法便终止。

在这里插入图片描述

希尔排序时,对有序序列在插入时采用交换法

代码:

public static void shellSort(int[] arr) {
    int temp = 0;
    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;
                }
            }
        }
    }
}

​ 对交换式的希尔排序进行优化,移位法:

代码:

public static void shellSort1(int[] arr) {
    //增量gap,并逐步的缩小增量
    for (int gap = arr.length / 2; gap > 0; gap /= 2) {
        //从第gap个元素,逐个对其所在的组进行直接插入排序
        for (int i = gap; i < arr.length; i++) {
            int j = i;
            int temp = arr[j];
           // if(arr[j]<arr[j-gap]){
                while (j - gap >= 0 && temp < arr[j - gap]) {
                    //移动
                    arr[j] = arr[j - gap];
                    j -= gap;
                }
            //当退出while后,就给temp找到插入的位置
            arr[j] = temp;
        //  }
        }
    }
}

快速排序

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

具体步骤:

1.找一个基准值,用两个指针分别指向数组的头部和尾部;

2.先从尾部向头部开始搜索一个比基准值小的元素,搜索到即停止,并记录指针的位置;

3.再从头部向尾部开始搜索一个比基准值大的元素,搜索到即停止,并记录指针的位置;

4.交换当前左边指针位置和右边指针位置的元素;

5.重复2,3,4步骤,直到左边指针的值大于右边指针的值停止;

  1. 然后交换基准值和两指针交汇处的值,为了使基准值靠近待排序数据中间的位置,基准值左边边的数都小于它,基准值右边的数都大于它;

  2. 然后对基准值两边的数据再重复1,2,3,4,5,6步骤,直到左指针大于右指针结束递归

    注意:递归时基准值左边的数传入参数时右指针应-1,基准值右边的数传入参数时左指针应-1,因为不包含基准值
    在这里插入图片描述

代码:

public static void quickSort(int[] arr,int start,int end){
    if(start > end)
        return;
    //定义一个temp用于交换左指针和右指针指向的数
    int temp = 0;
    int base=arr[start];    //基准值
    int i = start; //左下标
    int j = end;   //右下标
    while(i != j){
        while(i<j && arr[j] >= base)
            j--;
        while(i<j && arr[i] <= base)
            i++;
        if(j > i){
            //swap(arr,i,j);
            temp=arr[i];
            arr[i] = arr[j];
            arr[j] = temp;
        }
    }
    //交换基准值和i,j交汇处的值
    //swap(nums,start,i);
    arr[start] = arr[i];
    arr[i] = base;
    quickSort(arr, start, i-1);
    quickSort(arr, i+1, end);
}

其中交换两数的地方可以封装成一个方法:

public static void swap(int[] nums,int left,int right){
    int temp=nums[left];
    nums[left]=nums[right];
    nums[right]=temp;
}

归并排序

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

思想示意图1-基本思想:

在这里插入图片描述

思想示意图2-合并相邻有序子序列

​ 再来看看治阶段,我们需要将两个已经有序的子序列合并成一个有序序列,比如上图中的最后一次合并,要将 [4,5,7,8]和[1,2,3,6]两个已经有序的子序列,合并为最终序列[1,2,3,4,5,6,7,8],来看下实现步骤
在这里插入图片描述

代码:合并的方法

public static void merge(int[] arr, int left, int mid, int right, int[] temp) {

    int i = left; // 初始化 i, 左边有序序列的初始索引
    int j = mid + 1; //初始化 j, 右边有序序列的初始索引
    int t = 0; // 指向 temp 数组的当前索引

    //(一)
    //先把左右两边(有序)的数据按照规则填充到 temp 数组
    //直到左右两边的有序序列,有一边处理完毕为止
    while (i <= mid && j <= right) {//继续
    //如果左边的有序序列的当前元素,小于等于右边有序序列的当前元素
    //即将左边的当前元素,填充到 temp 数组
    //然后 t++, i++
        if (arr[i] <= arr[j]) {
            temp[t] = arr[i];
            t += 1;
            i += 1;
        } else { //反之,将右边有序序列的当前元素,填充到 temp 数组
            temp[t] = arr[j];
            t += 1;
            j += 1;
        }

    }

    //(二)
    //把有剩余数据的一边的数据依次全部填充到 temp
    while (i <= mid) { //左边的有序序列还有剩余的元素,就全部填充到 temp
        temp[t] = arr[i];
        t += 1;
        i += 1;
    }
    while (j <= right) { //右边的有序序列还有剩余的元素,就全部填充到 temp
        temp[t] = arr[j];
        t += 1;
        j += 1;
    }

    //(三)
    //将 temp 数组的元素拷贝到 arr
    //注意,并不是每次都拷贝所有
    t = 0;
    int tempLeft = left; //
    //第一次合并 tempLeft = 0 , right = 1 // tempLeft = 2 right = 3 // tL=0 ri=3
    //最后一次 tempLeft = 0 ,right =7
    while (tempLeft <= right) {
        arr[tempLeft] = temp[t];
        t += 1;
        tempLeft += 1;
    }
}

分+合方法:

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);
    }
}

基数排序

介绍:

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

基本思想:

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

在这里插入图片描述

代码:

public static void radixSort(int[] arr) {
    //1. 得到数组中最大的数的位数
    int max = arr[0]; //假设第一数就是最大数
    for (int i = 1; i < arr.length; i++) {
        if (arr[i] > max) {
            max = arr[i];
        }
    }
    //得到最大数是几位数
    int maxLength = (max + "").length();
    //定义一个二维数组,表示 10 个桶, 每个桶就是一个一维数组
    //说明
    // 1. 二维数组包含 10 个一维数组
    //2. 为了防止在放入数的时候,数据溢出,则每个一维数组(桶),大小定为 arr.length
    //3. 名明确,基数排序是使用空间换时间的经典算法
    int[][] bucket = new int[10][arr.length];
    //为了记录每个桶中,实际存放了多少个数据,我们定义一个一维数组来记录各个桶的每次放入的数据个数
    //可以这里理解
    //比如:bucketElementCounts[0] , 记录的就是 bucket[0] 桶的放入数据个数
    int[] bucketElementCounts = new int[10];

    //这里我们使用循环将代码处理
    for (int i = 0, n = 1; i < maxLength; i++, n *= 10) {
        //(针对每个元素的对应位进行排序处理), 第一次是个位,第二次是十位,第三次是百位..
        for (int j = 0; j < arr.length; j++) {
            //取出每个元素的对应位的值
            int digitOfElement = arr[j] / n % 10;
            //放入到对应的桶中
            bucket[digitOfElement][bucketElementCounts[digitOfElement]] = arr[j];
            bucketElementCounts[digitOfElement]++;
        }
        //按照这个桶的顺序(一维数组的下标依次取出数据,放入原来数组)
        int index = 0;
        //遍历每一桶,并将桶中是数据,放入到原数组
        for (int k = 0; k < bucketElementCounts.length; k++) {
            //如果桶中,有数据,我们才放入到原数组
            if (bucketElementCounts[k] != 0) {
                //循环该桶即第 k 个桶(即第 k 个一维数组), 放入
                for (int l = 0; l < bucketElementCounts[k]; l++) {
                    //取出元素放入到 arr
                    arr[index++] = bucket[k][l];
                }
            }
            //第 i+1 轮处理后,需要将每个 bucketElementCounts[k] = 0 !!!!
            bucketElementCounts[k] = 0;
        }
      //  System.out.println("第"+(i+1)+"轮,对个位的排序处理 arr =" + Arrays.toString(arr));
    }
}

堆排序

堆排序基本介绍:
1) 堆排序是利用堆这种数据结构而设计的一种排序算法,堆排序是一种选择排序,它的最坏,最好,平均 时间复杂度均为 O(nlogn),它也是不稳定排序。
2) 堆是具有以下性质的完全二叉树:每个结点的值都大于或等于其左右孩子结点的值,称为大顶堆, 注意 : 没有要求结点的左孩子的值和右孩子的值的大小关系。
3) 每个结点的值都小于或等于其左右孩子结点的值,称为小顶堆

堆排序的基本思想是:

  1. 将待排序序列构造成一个大顶堆
  2. 此时,整个序列的最大值就是堆顶的根节点。
  3. 将其与末尾元素进行交换,此时末尾就为最大值。
  4. 然后将剩余 n-1 个元素重新构造成一个堆,这样会得到 n 个元素的次小值。如此反复执行,便能得到一个有序序列了。

可以看到在构建大顶堆的过程中,元素的个数逐渐减少,最后就得到一个有序序列了.

排序算法的比较图

在这里插入图片描述

相关术语解释:

  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: 占用额外内存
  • 2
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 4
    评论
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值