浅谈七种排序算法

排序算法

*以此谨记自己学习Java心得
这几天在学习JavaWeb阶段,落了一点排序的算法,今天重新回顾。
以下排序算法,都是从小到大排。这七种排序算法分别是冒泡排序、选择排序、插入排序、希尔排序、快速排序、归并排序以及桶排序。

时间复杂度

每个算法都有时间复杂度,用bigO来表示。先引入一个比较简单的程序来计算一下时间复杂度
上课老师所讲的
这个是C++的一个程序,算时间复杂度都一样,根据每一步程序执行了多少时间单位来计算,最终得到O(N)。
下面在引入一个比较难的
在这里插入图片描述
答案是
i表达为:f(x) = x
k表达为:g(x) = g(x-1) + f(x)
则有:g(x) = g(0) + f(1) + f(2) + … + f(x) = 1 + 2 + … + x
令 y = g(x),则 y = x/2 * (1 + x)
求得 x = (2y + 1/4)^1/2 - 1/2
x即循环轮次,y即该循环轮的k具体值。
n为我们给定的循环条件值,所以n约等于y。

所以有近似精确值: O(√2 * n^1/2)。
忽略系数得到时间复杂度为O(n^1/2)。

下面张贴一张常用算法的时间复杂度
常用算法时间复杂度
由此可见,冒泡、选择、插入排序,这三种是复杂度高,且代码容易理解的,为初级算法。后面的几种是高级算法。

冒泡算法

好,接下来讲一讲最基础的冒泡算法。
冒泡排序(Bubble Sorting)的基本思想是:通过对待
排序序列从前向后(从下标较小的元素开始),依次比较
相邻元素的值,若发现逆序则交换,使值较大
的元素逐渐从前移向后部,就象水底下的气泡一样逐渐
向上冒。
用下图来直观表示
在这里插入图片描述
这里我们可以得到,每一次冒泡排序完成后,最后一个索引位永远是最大的,所以我们在进行第二次冒泡排序后,就可以让索引从0到倒数第二个,这样就又可以把倒数第二个大的的值放在倒数第二个索引位置上。这里注明一下,在进行循环的时候,是两个指针一起往右边移动。(我曾经以为是一个指针不变,另一个一直往后走,然而这样效率过低)在这里插入图片描述
接下来张贴一下代码
代码中有一处对冒泡排序做了优化,如果在这一次循环中,每个数字的位置都没有变化,那我们就可以提前结束整个程序

 /**
     * 优化了的冒泡算法
     * 两个指针一起往数列末尾移动,并比较两指针的值
     * 每一次执行完一次后,都是把最大的值放在了最后
     * 然后第二次循环,忽略最后一位
     * 第二次 从J=0开始到J=length-1-1;
     * 再从J=0开始到J=length-1-1-1;
     */
    public void sort1() {

        for (int i = 1; i < array.length; i++) {
            int j;
            int c = 0;
            for (j = 0; j < array.length - i; j++) {

                if (array[j] > array[j + 1]) {
                    temp = array[j];
                    array[j] = array[j + 1];
                    array[j + 1] = temp;
                    c++;
                }
                count++;
            }
            //这里就是我代码的优化地方
            if ((j == array.length - i) && c == 0) {
                flag = true;
                break;
            }

        }
    }

选择排序

选择排序就是将数组里的最小的元素放在0号索引位,也就是先假定说0号位的值是最小的,然后让它和后面的一个个去比,如果有比0号位小的,就让这个值到0号位,然后再进行循环,比较倒数第二个小的值,赋给1号位。
下面一张图,用来解释过程。
在这里插入图片描述

下面张贴一下代码,同样也对代码进行了优化,如果在此次循环,没有位置发生改变,就证明数组已经从小到大排,提前结束程序

/**
     * 选择排序
     * 规则:先找到一个最小值,赋给0号位,再在剩余的值中,找到最小值,赋给1号位。。。。。。。
     */
    public void sort2() {

        for (int i=0;i<array.length-1;i++) {
            int c=0;
            int j=0;
            for ( j = i; j < array.length; j++) {
                int min = array[i];
                if (array[j] < min) {
                    temp=array[i];
                    array[i]=array[j];
                    array[j]=temp;
                    c++;
                }

                count++;
            }
            if ((j==array.length)&&(c==0)){
                break;
            }
        }

    }

插入排序

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

下面张贴一张图片用来说明程序过程
其中括号代表左侧的有序表
在这里插入图片描述
下面我在根据自己的程序来做一个图片说明
先张贴代码

public void sort3(){
        for (int i=1;i<array.length;i++) {
            int value = array[i];
            int index=i-1;
            int parse=i;
            while (index>=0&&value<array[index]){
                array[parse]=array[index];
               // array[index]=value;
               // 改进后应该写在while外面,因为每次都赋值了,其实只要赋值最后一次就行
                index--;
                parse--;
                count++;
            }
            array[index+1]=value;
        }
    }

下面是我对程序思路的概述,我为了简单理解起见,初始数组定位{3,9,1,4}
在这里插入图片描述
程序中,我们索引先从i=1开始,也就是9开始,比较3与9,发现3小于9,则退出while循环。
再从i=2开始,也就是1,发现1小于9,那我们就把当前有序表的数组设置位{3,9,9}。然后while仍然继续,再拿着这个1的值 与索引位0号位的值去比较,也就是1与3比较,发现1小于3,那我们此时更新有序表数组为{3,3,9}。while循环退出,然后执行array[index+1]=value,也就是将有序表数组更新为{1,3,9},这里重点讲一下这个array[index+1]=value,我们没必要每次都让这个1直接赋给比它大的位置,比如说1与9比完后,直接更新数组为{3,1,9}。我们可以提前拿出这个1的值,让他是一个不变的常量,这样我们也就节省了交换的次数,只需要在最后退出while循环的时候,进行一次赋值就行。这个算法是老师讲的,的确很精髓,这个操作很秀。

希尔排序(shell)

首先,先指出直接插入算法存在的效率问题,
在这里插入图片描述
现在来介绍希尔排序,能解决上面的方法。
希尔排序是希尔(Donald Shell)于1959年提出的一种排序算法。希尔排序也是一种插入排序,它是简单插入排序经过改进之后的一个更高效的版本,也称为缩小增量排序。
希尔排序是把记录按下标的一定增量分组,对每组使用直接插入排序算法排序;随着增量逐渐减少,每组包含的关键词越来越多,当增量减至1时,整个文件恰被分成一组,算法便终止
用图片来解析过程
在这里插入图片描述
在这里插入图片描述
下面再用自己手画的图解过程来解释
在这里插入图片描述
这个希尔排序底层其实就是直接插入,采用移动法(效率高),他先把整个数组划分为几组几组(generation),这个数量是通过length/2,length/2/2…这样得到的,知道分为一组,此时的一组基本上已经快完成了,再进行微调,操作的时间较少。也就达到了高效的排序效果。
代码如下

public void sort4() {

        int gap;
        int c;
        for (gap = (array.length / 2); gap > 0; gap = (gap / 2)) {
            for (int j = gap; j < array.length; j++) {
                if (array[j] < array[j - gap]) {
                    int a = j;
                    c = array[j];
                    while (a - gap >= 0 && c < array[a - gap]) {
//                        temp = array[a];
//                        array[a] = array[a - gap];
                        array[a]=array[a-gap];
                        a = a - gap;
                    }
                    array[a]=c;//和直接插入的那个while循环一样,都是最后一次才赋值,减少交换次数
                }

            }
            count++;

        }

    }

快速排序

介绍一下快速排序的定义
快速排序(Quicksort)是对冒泡排序的一种改进。基本思想是:通过一趟排序将要排序的数据分割成独立的两部分,其中一部分的所有数据都比另外一部分的所有数据都要小,然后再按此方法对这两部分数据分别进行快速排序,整个排序过程可以递归进行,以此达到整个数据变成有序序列。
用老师上课的图片来解释一下过程
在这里插入图片描述
再用我自己的图片来解释一下
在这里插入图片描述
快速排序线,就是先确定一个中间值,我们这里用左右索引的一半来确定,也就是中间那个数字。这里我举简单的数组{8,6,7,4,1,3,5,2} 那我们一开始也就是找到4,然后将4左侧大于4的都移动到4的右侧,将4右侧小于4的 都移动到4的左侧。也就得到了{2,3,1,4,7,6,5,8} 然后再对4的左侧进行同样的左递归,4的右侧进行同样的右递归。
代码经过调试,有一些小细节问题需要处理,在代码中已经注释清楚。
代码如下:

/**
     * 快速排序
     *
     * @param left  左边界
     * @param right 右边界
     */
    public void sort5(int left, int right) {
        int l = left;
        int r = right;
        int avg = array[(left + right) / 2];
        while (l < r) {
            while (array[l] < avg) {//找到左侧小于中轴值的下标,并弹出
                l = l + 1;
            }
            while (array[r] > avg) {//找到右侧小于中轴值的下标,并弹出
                r = r - 1;
            }
            if (l == r) {
                break;
            }
            temp = array[r];
            array[r] = array[l];
            array[l] = temp;

            if (array[l] == avg) {//与下面的同理
                r--;
            }
            if (array[r] == avg) {//不然的话。比如说{8,6,7,4,8,6,9,10} l=0,r=3,如果l一直不动的话,会一直循环下去
                l++;
            }

        }
        if (l == r) {//不写的话会栈溢出,导致该程序无限递归
            l++;
            r--;
        }
        if (left < r) {
            sort5(left, r);//左递归 left是传进来的参数 0
        }
        if (l < right) {
            sort5(l, right);//右递归 left是传进来的参数 array.length-1
        }
    }

归并排序

归并排序也是分治算法,归并排序(MERGE-SORT)是利用归并的思想实现的排序方法,该算法采用经典的分治(divide-and-conquer)策略(分治法将问题分(divide)成一些小的问题然后递归求解,而治(conquer)的阶段则将分的阶段得到的各答案"修补"在一起,即分而治之)。
用下图来说明过程:
在这里插入图片描述
说明:
可以看到这种结构很像一棵完全二叉树,本文的归并排序我们采用递归去实现(也可采用迭代的方式去实现)。分阶段可以理解为就是递归拆分子序列的过程。

归并排序思想示意图2-合并相邻有序子序列:
再来看看治阶段,我们需要将两个已经有序的子序列合并成一个有序序列,比如上图中的最后一次合并,要将[4,5,7,8]和[1,2,3,6]两个已经有序的子序列,合并为最终序列[1,2,3,4,5,6,7,8],来看下实现步骤
在这里插入图片描述
在这里插入图片描述
上面的图已经很清晰了,我再稍微画一点图
在这里插入图片描述
因为这个是个递归,我首先讲一下最后的重点过程,也就是我上面画五角星的那个过程。
此时我们已经得到左边数组{4,5,7,8}(经过操作都已经有序了),右边的数组{1,2,3,6}.。然后我们要放到最后一个新数组里,也就是治的过程。我们首先要从左侧数组取出第一个数,右侧数组取出第一个数,然后比较这两个数谁小,谁小就放在新数组的0号索引位,发现是右侧的第一位小,所以右侧的第一位 1就放在新数组的0号位上。那我们再继续比较左侧数组第一位与右侧数组第二位,同理得到新数组。
然后当我们把某一侧的数组都放进新数组后,那我们就把另一个数组的值按照原先有序的顺序,放入新数组中。然后新数组中的值再根据left 、right传入到原来的数组里(通过代码调试可以理解)
下面张贴代码(分的代码和治的代码)

public static void sort_break(int []array,int left,int right,int []temparray) {
        int mid=(left+right)/2;
        if (left<right){

            sort_break(array,left,mid,temparray);
            sort_break(array,mid+1,right,temparray);
            sort_gorvernance(array,left,mid,right,temparray);
        }

    }
public static void sort_gorvernance(int []array,int left, int mid, int right, int[] temparray) {
            int i=left;
            int j=mid+1;
            int t=0;
            while (i<=mid&&j<=right){
                if (array[i]<array[j]){
                    temparray[t]=array[i];
                    t++;
                    i++;
                }
                else {
                    temparray[t]=array[j];
                    t++;
                    j++;
                }
            }
            //某一个数组已经全部递归完
            while (j<=right){
                temparray[t]=array[j];
                t++;
                j++;
            }
        //某一个数组已经全部递归完
        while (i<=mid){
            temparray[t]=array[i];
            t++;
            i++;
        }
             t=0;
            for (int k=left;k<=right;k++){
                array[k]=temparray[t];//temparray[t]是新数组,然后按照左右侧放入到老的数组里
                t++;
            }
    }

桶排序

介绍一下桶排序的定义
堆排序基本介绍
堆排序是利用堆这种数据结构而设计的一种排序算法,堆排序是一种选择排序,它的最坏,最好,平均时间复杂度均为O(nlogn),它也是不稳定排序。
堆是具有以下性质的完全二叉树:每个结点的值都大于或等于其左右孩子结点的值,称为大顶堆, 注意 : 没有要求结点的左孩子的值和右孩子的值的大小关系。
每个结点的值都小于或等于其左右孩子结点的值,称为小顶堆
大顶堆举例说明
在这里插入图片描述
在这里插入图片描述
一般升序采用大顶堆,降序采用小顶堆
堆排序的基本思想是:
将待排序序列构造成一个大顶堆
此时,整个序列的最大值就是堆顶的根节点。
将其与末尾元素进行交换,此时末尾就为最大值。
然后将剩余n-1个元素重新构造成一个堆,这样会得到n个元素的次小值。如此反复执行,便能得到一个有序序列了。

可以看到在构建大顶堆的过程中,元素的个数逐渐减少,最后就得到一个有序序列了.
其中先说一下如何使数组变成一个大顶堆,以{4,6,8,5,9}为例
原来的二叉树为
在这里插入图片描述
在这里插入图片描述
下面张贴代码,其中一些代码解析也在注释中写清楚了

int [] array=new  int[]{4,6,8,5,9};
        //k从最后一个叶子结点开始找
        for (int k = array.length/2-1; k >=0 ; k--) {
            barrelSort(array,k,array.length);
        }
        System.out.println(Arrays.toString(array));
/**
     * 数组变换成大顶堆
     * @param arr 一个数组
     * @param i  最后一个非叶子结点 算法为,array.length/2-1
     * @param leng 数组的长度
     */
    public static void barrelSort(int []arr,int i,int leng){

        for (int j = i*2+1; j <leng ; j=j*2+1) {
            int temp=arr[i];

            //i是最后一个叶子结点,然后j和j+1是最后一个叶子结点的子结点,比较子结点中的最大值与叶子结点的值
            if (j+1<leng&&arr[j]<arr[j+1]){
                j++;
            }
            //将最大的子结点与叶子结点比较
            if (arr[i]<arr[j]){
                arr[i]=arr[j];
                arr[j]=temp;
                i=j;
            }
        }
//        System.out.println(Arrays.toString(arr));
    }

最上面的for循环,是从最后一个叶子结点往前面循环,作为参数传入函数中。
接下来获取到数组也就是大顶堆为[9, 6, 8, 5, 4]。
然后将0号索引位放在数组最后一个位置,得到[4, 6, 8, 5, 9]。
然后下次计算的时候忽略掉最后一位(因为最后一位肯定是最大的数字了,这里和冒泡有点像)。也就是对[4,6,8,5]进行大顶堆排序,继续递归下去
下面张贴代码

public static void main(String[] args) {
        int [] array=new  int[]{4,6,8,5,9};
        //k从最后一个叶子结点开始找
        for (int k = array.length/2-1; k >=0 ; k--) {
            barrelSort(array,k,array.length);
        }
        System.out.println(Arrays.toString(array));

        for (int i = array.length-1; i >1 ; i--) {//i要大于1,因为经过调试,如果i=1会和0号位调换,使本来正常的排序错误
//            for (int j = (i+1)/2-1; j >=0 ; j--) {//先使数组变成大顶堆
//                barrelSort(array,j,i);
//            }
               int temp= array[i];
               array[i]=array[0];//最大值赋给数组最后一位
               array[0]=temp;
            //执行到此 数组的0号位原本是最大的值,现在已经放在了最后面,再将数组最后一位去掉(不进行交换)
                barrelSort(array,0,i);//前面的for循环不要了,否则时间复杂度会变n的平方,违背桶排序原理,
            //接上一行代码注释  在这里因为该数组已经是一个大顶堆,所以只需要对0,1,2号位置进行比较获取第一次的大顶堆
            //因为只动了根结点0号,所以只要对0这个叶子结点进行操作

        }
        System.out.println(Arrays.toString(array));



    }

到此为止,我所学的七种算法也已经分析完了,这七种也仅仅是对排序而言。世界上算法太多了,单单一个小问题就可以用多种算法来解析,还是那句话,”无他,唯手熟尔“。我的代码分析肯定不到位,相信自己,自己去动手去写一下,才会真正的理解。屏幕前的你,加油!

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值