六大排序——(插入、希尔、选择、交换、归并、计数)

目录

一、插入排序

二、希尔排序

 三、选择排序

1)直接选择排序:

2)堆排序

四、交换排序 

1)冒泡排序

2)快速排序

1、Hoare版

2、挖坑法

3、前后指针

快排优化

快速排序非递归来实现

快排总结

五、归并排序

递归实现

非递归实现

六、计数排序


一、插入排序

步骤:

1、从第一个元素开始,该元素可以被认你为已经被排序了

2、取下一个元素tmp,从已经排列的序列从后往前扫描

3、如果该元素大于tmp,则将它移动到下一位

4、重复步骤三,直到找到元素小于等于tmp结束

5、将tmp插入到该元素的后面,如果已排序的序列都大于tmp,则将tmp插入到下标为0位置

6、重复步骤2-5

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

样例测试:

 思路:

在待排序的序列中,假设前n-1个元素都已经有序,现将第n个元素插入到前面已经拍好的序列中,使得前n个元素有序,按照这个方法对所有元素进行插入,直到整个序列有序,但是我们不能确定待排序的序列中哪一部分是有序的,所以我们一开始认为第一个元素是有序的,依次将其后面的元素插入到这个有序的序列中来,直到整个序列有序为止。

时间复杂度:

最坏的情况是:5 4 3 2 1 每一个元素都需要移动,时间复杂度是O(n*n)

最好的情况是:1 2 3 4 5每一个元素都是有序的,时间复杂度O(n)

空间复杂度:

O(1)

所以插入排序适用于,序列接近于有序,而序列过于复杂不适合使用插入排序。

二、希尔排序

希尔排序又称缩小增量法。希尔排序的主要思想就是:先选定一个整数,把待排序的文件中的所有记录分成多个组,所有距离为记录分在同一组内,并且对每一个组进行排序,然后取,重复上述分组排序的过程,当分的组大小为1的时候,所记录在同一组内排好序.

上面的概念可能难以理解我们可以看一下图示:

其中分组完成之后就需要排序,这里我们选择插入排序对每一个组内的数据进行排序。

public 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 ; j-=gap) {
                if(tmp<arr[j]){
                    arr[j+gap]=arr[j];
                }
                else{
                    break;
                }
            }
            arr[j+gap]=tmp;
        }
    }
    public void shellSort(int[] arr){
        int gap=arr.length;
        while(gap>1){
            gap/=2;
            shell(arr,gap);
        }
        shell(arr,1);;
    }

 希尔排序特性总结:

1、希尔排序是插入排序的优化

2、当gap>1的时候都是预排序,目的是让数组曲玉有序。当gap==1的时候,由于数组趋于有序所以排序的速度会大幅增加。

3、希尔排序时间复杂度不好计算,因为gap的取值不是固定的,在不同书中1时间复杂度都是不同的

 

所以希尔排序是不稳定的。

 三、选择排序

基本思想:

每一次从待排序的数据元素中选出最大或者最小的一个元素,存放在序列的起始位置,知道全部待排序的数据元素都已经排完。

1)直接选择排序:

1、在元素1集合array[i]——arrayy[n-1]中选择关键码最大(小)的数据元素

2、若他们不是最后一个或者第一个元素,则将它与这组元素中的最后一个(第一个)元素交换

3、在剩余的array[i]——array[n-2](array[i+1]——array[n-1])集合中,重复上述步骤,直到集合中只剩于一个元素。

选择排序的写法有很多种,并且写法也是很好理解,就是找最大致值,最小值,最小值移动到左端

最大值移动到右端

public void selectSort(int[] arr){
        for (int i = 0; i < arr.length; i++) {
            int minIndex=i;
            for (int j = i+1; j < arr.length ; j++) {
                if(arr[j]<arr[minIndex]){
                    minIndex=j;
                }
            }
            swap(arr,i,minIndex);
        }
    }
    public void selectSort2(int[] arr){
        int left=0;
        int right=arr.length-1;
        while(left<right){
            int minIndex=left;
            int maxIndex=right;
            for (int i = left; i <= right; i++) {
                if(arr[i]<arr[minIndex]){
                    minIndex=i;
                }
                if(arr[i]>arr[maxIndex]){
                    maxIndex=i;
                }
            }
            swap(arr,minIndex,left);
            if(left==maxIndex){
                maxIndex=minIndex;
            }
            swap(arr,maxIndex,right);
            left++;
            right--;
        }
    }

直接选择排序效率比较低,时间复杂度在O(n*n)

2)堆排序

堆排序是指利用二叉堆这种数据结构所设计的一种排序算法,它是通过堆来选择数据。需要注意的是要是排升序用大根堆,降序用小根堆。

我们想要用堆排序的话,就必须要建造堆,所以我们需要根据二叉堆的堆顶元素一定大于所有元素来将最大元素与最后一个元素进行交换,接着在进行一次向下调整将次大元素找出来交换,依次来实现元素排序。

public void createHeap(int[] arr){
        for (int parent=(arr.length-1-1)/2; parent>=0 ; parent--) {
            shiftDown(parent,arr.length,arr);
        }
    }
    public void shiftDown(int parent,int len,int[] arr){
        int child=parent*2+1;
        while(child<len){
            if(child<len-1&&arr[child]<arr[child+1]){
                child=child+1;
            }
            if(arr[parent]<arr[child]){
                swap(arr,parent,child);
                parent=child;
                child=parent*2+1;
            }
            else{
                break;
            }
        }
    }
    public void heapSort(int[] arr){
        createHeap(arr);
        int end=arr.length-1;
        while(end>0){
            swap(arr,0,end);
            shiftDown(0,end,arr);
            end--;
        }
    }

堆排的时间复杂对是:O(n*logn)

四、交换排序 

1)冒泡排序

冒泡排序我们都耳熟能详了,这里就不过多的介绍了,直接上代码。

public void bubbleSort(int[] arr){
        for (int i = 0; i < arr.length-1; i++) {
            boolean flg=false;
            for (int j = 0; j < arr.length-1-i; j++) {
                if(arr[j]>arr[j+1]){
                    swap(arr,j,j+1);
                    flg=true;
                }
            }
            if(flg==false){
                break;
            }
        }
    }

这里我们用到了一些优化,如果我们1的序列已经有序列,那么我们就不需要继续排列了。

时间复杂度:O(n*n)

2)快速排序

快速排序有很多种方法,这里我们慢慢介绍:

1、Hoare版

任取待排序元素序列中的某个元素为基准值,按照该基准值将怕挨徐集合分成左右两部分,小于该基准值放在左边,大于该基准值的放在右边,接着左右序列重复该过程,直到有序。

public int parttion(int[] arr,int left,int right){
        int k=left;//记录初始下标
        int tmp = arr[left];
        while(left<right){
            while(left<right&&arr[right]>=tmp){
                right--;
            }
            while(left<right&&arr[left]<=tmp){
                left++;
            }

            swap(arr,left,right);
        }
        swap(arr,k,left);
        return left;
    }
    public void quick(int[] arr,int start,int end){
        if(start>=end){
            return;
        }
        int pivot = parttion(arr,start,end);
        quick(arr,start,pivot-1);
        quick(arr,pivot+1,end);
    }

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

2、挖坑法

挖坑法其实和Hoare的方法大差不差都是将左面都小于这个基准值,右边都大于这个基准值,只不过Heare是双向查找,而挖坑法是找到右边比tmp小的值就将其与left进行交换,接着进行左边的查找。

public int parttion2(int arr[],int left,int right){
        int tmp=arr[left];
        while(left<right){
            while(left<right&&arr[right]>=tmp){
                right--;
            }
            arr[left]=arr[right];
            while(left<right&&arr[left]<=tmp){
                left++;
            }
            arr[right]=arr[right];
        }
        arr[left]=tmp;
        return left;
    }

3、前后指针

我们直接看代码实现更加会好理解一点:

    public int parttion3(int[] arr,int left,int right){
        int prev=left;
        int cur=prev+1;
        while(cur<=right){
            if(arr[cur]<arr[left]&&arr[++prev]!=arr[cur]){
                swap(arr,prev,cur);
            }
            cur++;
        }
        swap(arr,prev,left);
        return prev;
    }

我们可以看一下图示来理解一下这个代码:

这段代码不好理解的是arr[++prev]!=arr[cur]它的作用是将数组arr分区,可以这段图示来理解一下   

双指针还有一种写的方法,与这种方法差别不大

private static int partition(int[] array, int left, int right) {
    int d = left + 1;
    int pivot = array[left];
    for (int i = left + 1; i <= right; i++) {
        if (array[i] < pivot) {
            swap(array, i, d);
            d++;
        }
    }
    swap(array, d - 1, left);
    return d - 1;
}

快排优化

1、三数取中

当序列为1,2,3,4,5的时候,序列递归会成一条线性

所以我们需要打乱这种顺序,就需要每次取的基准值是三个数当中第二大的:

2、递归到小的子区间时可以考虑使用插入排序

快排是以类似二叉树的模型来遍历的,所以当遍历到最后一层的时候,每一个叶子节点都还需要遍历两边,所以会使时间变长,所以我们需要使用插入1排序来减小时间。

    private static int threeNum(int[] array,int left,int right) {
        int mid = (left+right) / 2;
        if(array[left] < array[right]) {
            if(array[mid] < array[left]) {
                return left;
            }else if(array[mid] > array[right]) {
                return right;
            }else {
                return mid;
            }
        }else {
            if(array[mid] < array[right]) {
                return right;
            }else if(array[mid] > array[left]) {
                return left;
            }else {
                return mid;
            }
        }
    }
        private static void quick(int[] array,int start,int end) {
        if(start >= end) {
            return;
        }
        count++;
        //System.out.println("start: "+start);
        //System.out.println("end: "+end);
        if(end - start +1 <= 20) {
            //直接插入排序
            insertSort2(array,start,end);
            return;
        }
        //三数取中
        int mid = threeNum(array,start,end);
        //交换
        swap(array,mid,start);

        int pivot = parttion(array,start,end);

        quick(array,start,pivot-1);//左树

        quick(array,pivot+1,end);//右树
    }

快速排序非递归来实现

由于使用递归双子树的方法来实现数据的排序当数据量过于大的时候会出现栈溢出的情况所以我们应该会用非递归来实现快速排序。

void quickSortNonR(int[] a, int left, int right) {
    Stack<Integer> st = new Stack<>();
    st.push(left);
    st.push(right);
    while (!st.empty()) {
        right = st.pop();
        left = st.pop();
    if(right - left <= 1)
        continue;
    int div = PartSort1(a, left, right);
    // 以基准值为分割点,形成左右两部分:[left, div) 和 [div+1, right)
    st.push(div+1);
    st.push(right);
    st.push(left);
    st.push(div);
    }
}

快排总结

1、快速排序综合性能和使用场景都是比较好的,所以才叫快速排序。

2、时间复杂度:O(n*logn)

3、空间复杂度:O(logn)

4、稳定性:不稳定

五、归并排序

归并排序是建立在归并操作的一种有效的排序算法,采用分治排序,分为分解、合并两个操作。

 分解:将数组分割成两个数组,在将两个数组分割成四个数组,直到不能分割为止

合并:将分割的有序数组进行排序,将其合并,一个成两个两个成四个,直到数组元素个数与原来相同即可。

递归实现

public void merge(int[] arr,int left,int mid,int right){
        int[] tmpArr = new int[right-left+1];
        int l1=left;
        int r1=mid;
        int l2=mid+1;
        int r2=right;
        int k=0;
        while(l1<=r1&&l2<=r2){
            if(arr[l1]<arr[l2]){
                tmpArr[k++]=arr[l1++];
            }
            else{
                tmpArr[k++]=arr[l2++];
            }
        }
        while(l1<=r1){
            tmpArr[k++]=arr[l1++];
        }
        while(l2<=r2){
            tmpArr[k++]=arr[l2++];
        }
        for (int i = 0; i < k; i++) {
            arr[i+left]=tmpArr[i];
        }
    }
    public void mergeSortFunc(int[] arr,int left,int right){
        if(left>=right){
            return ;
        }
        int mid  = (left+right)/2;
        mergeSortFunc(arr,left,mid);
        mergeSortFunc(arr,mid+1,right);
        merge(arr,left,mid,right);
    }
    public void mergeSort(int[] arr){
        mergeSortFunc(arr,0,arr.length-1);
    }

我们可以根据上诉思路来用递归实现分解,分解完成之后继续通过merge方法来将其合并,而merge方法我们就是同构两个有序数组合并的思路来完成的。

非递归实现

由于数组总是以一半的方式进行分割,分割的终点是数组元素只有一个,所以我们定义一个变量gap作为分割后的数组长度,遍历时一次跳过gap*2个元素,刚好是两个数组的长度,gap从1开始对两个有序数组进行排序,直到作为数组长度一半的时候结束

public void mergeSort1(int[] arr){
        int gap=1;
        while(gap<arr.length){
            for (int i = 0; i < arr.length; i+=gap*2) {
                int left=i;
                int mid=left+gap-1;
                int right=mid+gap;
                //防止mid和right越界
                if(mid >= arr.length) {
                    mid = arr.length-1;
                }
                if(right >= arr.length) {
                    right = arr.length-1;
                }
                merge(arr,left,mid,right);
            }
            gap*=2;
        }
    }

六、计数排序

计数排序是一个比较简单的排序,是通过记录每一个元素出现的次数,来进行的排序。

先假设 20 个数列为:{9, 3, 5, 4, 9, 1, 2, 7, 8,1,3, 6, 5, 3, 4, 0, 10, 9, 7, 9}。

让我们先遍历这个无序的随机数组,找出最大值为 10 和最小值为 0。这样我们对应的计数范围将是 0 ~ 10。然后每一个整数按照其值对号入座,对应数组下标的元素进行加1操作。

比如第一个整数是 9,那么数组下标为 9 的元素加 1。

第二个整数是 3,那么数组下标为 3 的元素加 1。

继续遍历数列并修改数组......。最终,数列遍历完毕。

数组中的每一个值,代表了数列中对应整数的出现次数。

有了这个统计结果,排序就很简单了,直接遍历数组,输出数组元素的下标值,元素的值是几,就输出几次。比如统计结果中的 1 为 2,就是数列中有 2 个 1 的意思。这样我们就得到最终排序好的结果。

0, 1, 1, 2, 3, 3, 3, 4, 4, 5, 5, 6, 7, 7, 8, 9, 9, 9, 9, 10

代码实现:

public void countArray(int[] arr){
        //找到数组的最大值 最小值 确定数组开辟的大小
        int maxVal=arr[0];
        int minVal=arr[0];
        for (int i = 1; i < arr.length; i++) {
            if(arr[i] < minVal) {
                minVal = arr[i];
            }
            if(arr[i] > maxVal) {
                maxVal = arr[i];
            }
        }
        int range=maxVal-minVal+1;
        int[] count=new int[range];
        for (int i = 0; i < arr.length; i++) {
            count[arr[i]-minVal]++;
        }
        int index = 0;//记录重新写会array数组的下标
        for (int i = 0; i < count.length; i++) {
            int val = count[i];
            while (val != 0) {
                arr[index] = i + minVal;
                val--;
                index++;
            }
        }
    }

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

lose_rose777

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值