几种常见的排序

一、插入排序

1、插入排序:

先看代码

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

测试:

    public static void main(String[] args) {
        int[] arr1 = {12,5,9,34,6,8,33,56,89,0,7,4,22,55,77};
        System.out.println("排序前: " + Arrays.toString(arr1));
        insertSort(arr1);
        System.out.println("排序后: " + Arrays.toString(arr1));
    }

结果:在这里插入图片描述

思路:
1、循环嵌套,利用i和j双指针,i从1下标开始,到数组结束;
2、每次j循环能够保证的是i下标前面的元素是有序的。

时间复杂度: 最好情况下:O(n); 最坏情况下:O(n^2);

空间复杂度:O(1);

稳定性:插入排序是比较稳定的,它并没有跳跃式交换数值。

2、希尔排序

    public static void shellSort(int[] arr) {
        //将数组进行分组
        int gap = arr.length-1;
        while (gap > 1) {
        //这里gap >= 1的话会死循环 
        //所以需要在循环结束后,数组趋于有序时进行最后的排序。
            shell(arr,gap);
            gap = (gap/3)+1;
        }
        shell(arr,1);
    }

    public static void shell(int[] arr,int gap) {
        for (int i = gap; i < arr.length; i++) {
            int tmp = arr[i];
            int j = i-gap;
            for ( ;j >= 0 && arr[j] > tmp; j=j-gap) {
                if (arr[j] > tmp) {
                    arr[j+gap] = arr[j];
                }
            }
            arr[j+gap] = tmp;
        }
    }

测试:

    public static void main(String[] args) {
        int[] arr1 = {12,5,9,34,6,8,33,56,89,0,7,4,22,55,77};
        System.out.println("排序前: " + Arrays.toString(arr1));
        shellSort(arr1);
        System.out.println("排序后: " + Arrays.toString(arr1));
    }

结果:
在这里插入图片描述
希尔排序是对插入的排序的优化,当数据量很大的时候,我们先将数据分成若干个组,然后进行类似于插入排序的排序。当gap=1的时候,数组已经趋近于有序,这个时候就会很快。整体而言,达到了优化的效果。
我说优化可能你也不信,现在我们来利用System.currentTimeMillis();
方法来获取当前时间(毫秒级别),然后用时间来粗略对比一下。
我们来测试一下当数据量很大的时候,用插入排序和希尔排序来对比下:

插入排序测试:

    public static void main(String[] args) {
        int[] arr = new int[10_0000];
        Random random = new Random();
        for (int i = 0; i < arr.length; i++) {
            arr[i] = random.nextInt(100000);
        }
        long Start = System.currentTimeMillis();
        insertSort(arr);
        long End = System.currentTimeMillis();
        System.out.println("插入排序时间差: " + (End - Start));
    }

插入排序测试结果(进行了三次测试):
1483 1780 1494

希尔排序测试:

    public static void main(String[] args) {
        int[] arr = new int[10_0000];
        Random random = new Random();
        for (int i = 0; i < arr.length; i++) {
            arr[i] = random.nextInt(100000);
        }
        long Start = System.currentTimeMillis();
        shellSort(arr);
        long End = System.currentTimeMillis();
        System.out.println("希尔排序时间差: " + (End - Start));
    }

希尔排序测试结果(进行了三次测试):
17 16 19

不管你信不信,反正我信了!希尔排序做到了优化。

思路:
希尔排序的关键思路是和插入排序是一样的,但是需要将元素分组进行插入排序。

时间复杂度: 正常情况下:O(n^1.3-1.5); 最坏情况:O(n^2);

空间复杂度:O(1);

稳定性:不稳地,因为它拥有跳跃式交换数据。

二、选择排序

    public static void selectSort(int[] arr) {
        for (int i = 0; i < arr.length; i++) {
            for (int j = i+1; j < arr.length; j++) {
                if (arr[i] > arr[j]) {
                    int tmp = arr[j];
                    arr[j] = arr[i];
                    arr[i] = tmp;
                }
            }
        }
    }
	
	//另外一种写法
	    public static void selectSortII(int[] arr) {
        for (int i = 0; i < arr.length - 1; i++) {
            int max = 0;
            for (int j = 0; j < arr.length - i; j++) {
                if (arr[j] > arr[max]) {
                    max = j;
                }
            }
            int tmp = arr[arr.length-i-1];
            arr[arr.length-i-1] = arr[max];
            arr[max] = tmp;
        }
    }

选择排序是一个比较简单的排序。我们来理理它的思路:
1、利用 i 循环遍历数组;
2、每次进入j循环后,j 循环做的事就是让 arr[i] 的值从 arr[i]arr[arr.length-1] 的元素的值是最小的。

用更简单的话来说就是:(第二个写法更适合这种思路)

把数组分为有序区间和无序区间,每次从无序区间中找到最大的数放在无序区间的最后一位

测试:

    public static void main(String[] args) {
        int[] arr1 = {12,5,9,34,6,8,33,56,89,0,7,4,22,55,77};
        System.out.println("排序前: " + Arrays.toString(arr1));
        selectSort(arr1);
        System.out.println("排序后: " + Arrays.toString(arr1));
    }

结果:在这里插入图片描述

时间复杂度:O(n^2);
空间复杂度O(1);
稳定性:不稳地。

三、堆排序

我们先来说一下思路:
1、如果数组要升序,那么就是建立大堆;反之建立小堆;
2、每次将堆顶的元素与最后一个元素交换;交换后数组长度"减一";
3、在"减一"的这个数组中整体向下调整,保证堆顶的元素的最大的;
4、重复2-3步骤,直到"减一"到这个数组只有一个元素;
5、得到了升序数组。

又到了上代码环节:


    public static void heapSort(int[] arr) {
        //建大堆
        creatHeap(arr);
        //排序
        int end = arr.length-1;
        while (end > 0) {
            int tmp = arr[end];
            arr[end] = arr[0];
            arr[0] = tmp;
            //每次都向下调整,确保堆顶元素最大
            adjustDown(arr,0,end);
            //虽然我们传过去的时end,但是这个end代表的是数组的长度。
            //所以不会调整到当前位置的元素。
            end--;
        }
    }

    //建大堆
    public static void creatHeap(int[] arr) {
        for (int parent = (arr.length-1-1)/2; parent >= 0; parent--) {
            adjustDown(arr,parent,arr.length);
        }
    }

    //向下调整
    public static void adjustDown(int[] arr,int parent,int len) {
        int child = 2*parent+1;
        while (child < len) {
            //确保孩子节点的值是最大的
            if (child + 1 < len && arr[child] < arr[child + 1]) {
                child++;
            }
            if (arr[parent] < arr[child]) {
                int tmp = arr[parent];
                arr[parent] = arr[child];
                arr[child] = tmp;
                parent = child;
                child = 2*parent+1;
            } else {
                break;
            }
        }
    }

在堆排序的时候,你就把数组想成二叉树。虽然他没有left和right,毕竟堆就是用来存储完全二叉树的一种数据类型。
又到了测试环节:

    public static void main(String[] args) {
        int[] arr1 = {12,5,9,34,6,8,33,56,89,0,7,4,22,55,77};
        System.out.println("排序前: " + Arrays.toString(arr1));
        heapSort(arr1);
        System.out.println("排序后: " + Arrays.toString(arr1));
    }

虽然这个数组已经是老演员了,但不影响它来测试我们的排序方法。

结果:
在这里插入图片描述
现在来解释一下上面说的数组长度"减一",因为每次end>0循环中都会将堆顶元素最大的值与堆尾元素进行交换,第一次拿下来的一定是整个数组中的最大值,当我们数组长度"减一"后,即不需要调整这个刚得到的最大值,又可以使用adjustDown函数使得堆顶元素变为"减一"数组后的最大。

时间复杂度: O(nlogn);
空间复杂度:O(1);
稳定性:不稳地。

四、冒泡排序

上代码!上代码!

    public static void bubbleSort(int[] arr) {
        for (int i = 0; i < arr.length; i++) {
            boolean isSorted = true;//优化
            for (int j = 0; j < arr.length-i-1; j++) {
                if (arr[j] > arr[j+1]) {
                    int tmp = arr[j];
                    arr[j] = arr[j+1];
                    arr[j+1] = tmp;
                    isSorted = false;
                }
            }
            if (isSorted) {
                return;
            }
        }
    }

测试:

   public static void main(String[] args) {
        int[] arr1 = {12,5,9,34,6,8,33,56,89,0,7,4,22,55,77};
        System.out.println("排序前: " + Arrays.toString(arr1));
        bubbleSort(arr1);
        System.out.println("排序后: " + Arrays.toString(arr1));
    }

结果:
在这里插入图片描述
该看的都看了,现在我们进入正题,冒泡排序的思路:
1、定义i循环表示循环的次数;有几个元素就循环几次呗;
2、j循环中依次比较,从0到arr.length-i-1。都把大于的元素放在后面。
3、i的第一次循环一定能把最大的放在后面,第二次循环把第二大的放在倒数第二个位置,以此类推;最后数组有序。

来说说为啥j<arr.length-i-1;因为第一次i循环已经保证最后一个元素有序了,那么第二次i循环j就可以不用管最后一个值了,而i代表的是上依次i循环中i有序数字的个数。至于这个-1,如果不加当i=1的时候可能会造成数组越界异常,因为有个arr[j] > arr[j+1]
从另外一方面理解,j+1已经可以读取到数组最后一个元素。所以一定要-1,记得!

关于优化,当某次进入i循环,j循环走完都没有进入if语句,那么证明数组是有序的,不用再进行比较了。直接return跑路。

时间复杂度(没优化的情况下) :O(n^2);
空间复杂度:O(1);
稳定性:稳定。

五、快速排序

现在终于来到了传说中的快排,我将用递归和非递归的两种代码来表达快排。
我们先来聊聊递归:分治的思想,把大问题化解成小问题。还记得求斐波那契数第n项吗?还记得让人欲仙欲死的二叉树吗?算了,不为难我自己了。

快排使用递归,其实就是:

1、找基准,让基准的左边小于基准,基准的右边大于基准,现在定义一个基准par。
2、递归基准的左边,(其实递归哪边无所谓的,你高兴你把你自己的左边递归了都可以)。左边low到 par-1;
3、递归基准的右边;右边par+1到high;

每次start和end基本上是不一样的;

4、终止条件:这也是递归的要素,当par左边或者右边只有一个元素或者没有元素时,说明它的左边或者右边有序了,此时下一个递归的start 或者end
它们存在着start>=end的关系。所以这就是递归的终止条件。


    public static void quickSort(int[] arr) {
        //quick(arr,0,arr.length-1);
        quickImitate(arr,0,arr.length-1);
    }

    public static void quick(int[] arr,int low,int high) {
        if (low >= high) {
            return;
        }
        //优化二:使用insertSort优化 记得return
        if ((high-low)+1 <= 100){
            insertSort2(arr,low,high);
            return;
        }
        int mid = (low + high)/2;
        //优化一:三数取中 可以防止栈溢出情况  而栈溢出是因为数组原本有序
        minOfThree(arr,low,mid,high);
        int par = par(arr,low,high);
        quick(arr,low,par-1);
        quick(arr,par+1,high);
    }

    public static int par(int[] arr,int start,int end) {
        int tmp = arr[start];
        while (start < end) {
            //让end往前走,找到一个比tmp还小的数字放在start位置
            //end可能会走几步,所以写一个循环
            while (start < end && arr[end] >= tmp) {//一定要写= 不然可能会陷入死循环
                end--;
            }
            //此时end来到了比tmp还小的数字
            //不小于tmp也可以赋值,此时start=end
            if (arr[end] < tmp) {
                arr[start] = arr[end];
            }

            //让start往前走,找到一个比tmp还小的数字让在end位置
            //start可能会走几步,所以写一个循环
            while (start < end && arr[start] <= tmp) {
                start++;
            }
            //此时start来到了比tmp还大的数字              
            if (arr[start] > tmp) {
                arr[end] = arr[start];
            }
        }
        //此时end和start在同一个位置 将tmp的值放在这个位置上  此时start所在下标的左边比arr[start]小  start所在下标的右边比arr[start]大
        arr[start] = tmp;//也可以换成arr[end] = tmp;
        //此时start和end相遇的位置就是par
        return start;
    }

    /**
     * 优化一、三数取中
     */
    public static void minOfThree(int[] arr,int low,int mid,int high) {
        int x = arr[low];
        int y = arr[mid];
        int z = arr[high];
        int[] tmp = {x,y,z};
        for (int i = 1; i < tmp.length; i++) {
            int n = tmp[i];
            int j = i-1;
            for ( ;j >= 0 && tmp[j] > n; j--) {
                tmp[j+1] = tmp[j];
            }
            tmp[j+1] = n;
        }
        //会存在小问题  arr[mid]一定要放在中间赋值
        arr[low] = tmp[1];
        arr[mid] = tmp[0];
        arr[high] = tmp[2];
    }

    /**
     * 优化二:insertSort优化
     */
    public static void insertSort2(int[] arr,int low,int high) {
        for (int i = low; i <= high; i++) {
            int tmp = arr[i];
            int j = i-1;
            for (; j >= 0 && arr[j] > arr[j+1]; j--) {
                arr[j+1] = arr[j];
            }
            arr[j+1] = tmp;
        }
    }

测试:

    public static void main(String[] args) {
        int[] arr1 = {12,5,9,34,6,8,33,56,89,0,7,4,22,55,77};
        System.out.println("排序前: " + Arrays.toString(arr1));
        quickSort(arr1);
        System.out.println("排序后: " + Arrays.toString(arr1));
    }

结果:
在这里插入图片描述

现在我们来说一下非递归:利用栈模拟

其实思路都一样,我们要做的就是:
1、找基准!
2、再次利用par函数得到par;
分别得到将start和par-1入栈,再将par+1和end入栈。
3、每次栈pop()两次;两个元素分别等于新的start和新的end传入到par函数中.
4、重复2-3步骤,直到栈为空的时候数组就有序了。

现在来实现代码:

   public static void quickImitate(int[] arr,int low,int high) {
        Stack<Integer> stack = new Stack<>();
        //调用par函数的到第一次par
        int par = par(arr,low,high);
        //如果par左边有两个元素以上,那么入栈 先push end下标  再push  start下标
        if (par-1 > low) {
            stack.push(par-1);
            stack.push(low);
        }
        //如果par右边有两个元素以上,那么也入栈
        if (par+1 < high) {
            stack.push(high);//end
            stack.push(par+1);
        }

        while (!stack.isEmpty()) {
            int start = stack.pop();
            int end = stack.pop();
            int tmpPar = par(arr, start, end);
            if (tmpPar - 1 > start) {
                stack.push(tmpPar - 1);
                stack.push(start);
            }
            //如果par右边有两个元素以上,那么也入栈
            if (tmpPar + 1 < end) {
                stack.push(end);//end
                stack.push(tmpPar + 1);
            }
        }
    }

这次就不请我们的老演员数组了,赶紧下一集归并排序。

时间复杂度:最好情况下O(nlogn);最坏情况O(n^2);
空间复杂度:最好情况:O(logn);最坏情况O(n);
稳定性:不稳地。

六、归并排序

还是一样的,归并排序分为递归和非递归。

递归解法:

   public static void mergeSort(int[] arr) {
        mergeRec(arr,0,arr.length-1);
    }

    public static void mergeRec(int[] arr,int start,int end) {
        if (start >= end) {//此时只有一个元素
            return;
        }

        int mid = (start+end)/2;
        mergeRec(arr,start,mid);
        mergeRec(arr,mid+1,end);
		
        merge(arr,start,mid,end);
    }

    //相当于合并两个有序数组
    public static void merge(int[] arr,int start,int mid,int end) {
        int[] tmpArr = new int[end-start+1];//元素个数=下标相减+1
        int s1 = start;
        int e1 = mid;
        int s2 = mid+1;
        int e2 = end;
        int k = 0;
        while (s1 <= e1 && s2 <= e2) {
            if (arr[s1] <= arr[s2]){
                tmpArr[k] = arr[s1];
                k++;
                s1++;
            }else {
                tmpArr[k] = arr[s2];
                k++;
                s2++;
            }
        }
        //这个时候有一个数组已经走完了
        while (s1 <= e1) {
            tmpArr[k] = arr[s1];
            k++;
            s1++;
        }
        while (s2 <= e2) {
            tmpArr[k] = arr[s2];
            k++;
            s2++;
        }
        for (int i = 0; i < tmpArr.length; i++) {
            arr[start+i] = tmpArr[i];
        }
    }

先来看张图:
在这里插入图片描述
从递归整体思路来解释归并排序:
递:将数组分解成"小数组";
归:将数组排序并归。
粗鲁点说,先让元素一个一个有序,再让元素两个两个有序,再让元素四个四个有序,一直到数组结束。

测试:

    public static void main(String[] args) {
        int[] arr1 = {12,5,9,34,6,8,33,56,89,0,7,4,22,55,77};
        System.out.println("排序前: " + Arrays.toString(arr1));
        quickSort(arr1);
        System.out.println("排序后: " + Arrays.toString(arr1));
    }

结果:
在这里插入图片描述

非递归实现:


    public static void mergeSort1(int[] arr) {
        for (int i = 1; i <= arr.length; i *= 2) {
            merge1(arr,i);
        }
    }

    public static void merge1(int[] arr,int gap) {
        int[] tmpArr = new int[arr.length];
        int s1 = 0;
        int e1 = s1+gap-1;
        int s2 = e1+1;
        int e2 = s2+gap-1 >= arr.length ? arr.length-1 : s2+gap-1;
        int k = 0;
        while (s2 < arr.length) {
            while (s1 <= e1 && s2 <= e2) {
                if (arr[s1] <= arr[s2]) {
                    tmpArr[k++] = arr[s1++];
                }else {
                    tmpArr[k++] = arr[s2++];
                }
            }
            while (s1 <= e1) {
                tmpArr[k++] = arr[s1++];
            }
            while (s2 <= e2) {
                tmpArr[k++] = arr[s2++];
            }
            s1 = e2+1;
            e1 = s1+gap-1;
            s2 = e1+1;
            e2 = s2+gap-1 >= arr.length ? arr.length-1 : s2+gap-1;
        }
        while (s1 < arr.length) {
            tmpArr[k++] = arr[s1++];
        }
        for (int i = 0; i < tmpArr.length; i++) {
            arr[i] = tmpArr[i];
        }
    }

思路:
1、将数组从前往后分组,第一次分为1组,第二次分为2组,第三次分为4组,第四次分为八组。
2、每次分组都能保证相邻俩个组之间的元素是有序的。
3、当两个分组的长度>=数组长度时,数组就有序了!

时间复杂度:O(nlogn);
空间复杂度:O(n);
稳定性:稳定。

七、简单总结

这些常见的排序建议多画图,跟着代码画图,多画图就能理解了。

老规矩,初学小白。如有错误,多多指正。感谢!

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值