排序2.复杂排序

堆排序

堆排序是一种选择排序,遍历后选择数组中最大的值放到堆顶,可用数组实现,最好和最坏和平均复杂度都为O(nlogn),不稳定。
堆就是一种完全二叉树,排序特征是堆顶元素大于子元素(不管子元素之间的排列);完全二叉树的特征是层序遍历等于满二叉树的层序遍历(也就是底层可以没有右边的子树)
用数组来描述堆的定义有大顶堆和小顶堆,若为大顶堆,则i的父节点为i/2,子节点为2i和2i+1
arr[i]<arr[i/2] && arr[i]>arr[2i] && arr[i]>arr[2i+1]
但是放到数组中父节点变成了2(i-1) 子节点为2i+1,2i+2
升序使用大顶堆,就是先将待排序数组构造成一个大顶堆,此时堆顶为最大值,将它放到最后一个(对应数组中就是最后一个值),就找到了最大值,然后将剩下的值再次排序可以找到第二大的值,将它放到堆底,如此反复就可以将所有值排序
堆顶也就是数组0位置,堆尾就是数组的最后一个位置

步骤

1.构造大顶堆的时候就是从第一个非叶节点(也就是倒数第二层,因为从这开始才有子节点i/2-1)开始调整(调整就是将此元素下沉到适合它的位置),然后一直循环到0也就是堆顶,这时所有元素都完成下沉。
2.此时已经构造好大顶堆,所以交换堆顶元素到堆底,下沉堆顶元素,循环继续交换,下沉。此时循环的i就是未调整的长度可变的,每次都下沉堆顶元素0.
调整的操作是实质就是下沉,需要的是数组,下沉元素位置,还有最大的数组长度(也就是除去已经交换完的位置),目的是把arr[i]值下沉到它该去的位置。第一步就是保存arr[i]为temp,进入for循环,从i的左子节点2i+1开始一直到length结束,进入之后首先找到它的两个子节点中大的那一个,如果此时大的那个子节点大于i,就把子节点值赋值给父节点(不是交换,而是把arr[i]值和i值都改变成是arr[j],j),然后接着循环,一直循环到结束或者时子节点值不再大于父节点。for循环执行完之后将temp放回到arr[i]处。
边界条件一定注意

public static void main(String[] argc){
       int[] arr={0,6,3,2,14,6,7};
        sort(arr);
       for (int i=0;i<arr.length;i++){
           System.out.println(arr[i]);
       }
    }
    public static void sort(int[] arr) {
        for(int i=arr.length/2-1;i>=0;i--){
            sink(arr,i,arr.length-1);//传入的是适合数组的
        }
        for(int i=arr.length-1;i>0;i--){
            swap(arr,i,0);
            sink(arr,0,i-1);//已经交换了的最后就不用比较,减1的基础上再减1
        }
    }
    public static void sink(int[] arr,int i,int length){
        int temp=arr[i];
        for(int j=2*i+1;j<length;j++){//最后一个不用下沉了
            if(j+1<length && arr[j]<arr[j+1]){
                j=j+1;
            }
            if(arr[j]>temp){
                arr[i]=arr[j];
                i=j;
            }else{
                break;
            }
        }
        arr[i]=temp;
    }
    public static void swap(int[] arr,int lo,int hi){
        int temp=arr[lo];
        arr[lo]=arr[hi];
        arr[hi]=temp;
    }

归并排序

归并的操作就是把两个以及排序好的队列归并merge到一起,两个就是二路归并。
时间复杂度为O(nlogn),最好为O(nlogn)最差为O(nlogn),稳定。
所以它的复杂度和堆排序一样,但稳定,所以工程中对象的排序一般用归并排序
过程很简单,就是递归,然后归并操作
归并merge操作很重要,在归并排序中需要的是创建一个新数组,然后遍历旧数组中的两个部分,将值放到新数组中,最后全部遍历完成则将旧数组的值放到新数组中。
merge操作的应用有很多,比如说逆序对问题,比如说小和问题。

 static void MergeSortA(int[] a){
        if(a.length<=1 || a==null) return;
        MergeSort(a,0,a.length-1);
    }
    public static void MergeSort(int[] arr,int lo,int hi){
        if(lo==hi){
            return;
        }
        int mid=lo+((hi-lo)>>1);
        MergeSort(arr,lo,mid);
        MergeSort(arr,mid+1,hi);
        Merge(arr,lo,mid,hi);
    }
    public static void Merge(int[] arr,int lo,int mid,int hi){
        int[] res=new int[hi-lo+1];
        int p1=lo;
        int p2=mid+1;
        int cur=0;
        while(p1<=mid && p2<=hi){
            if(arr[p1]<=arr[p2]){
                res[cur++]=arr[p1++];
            }else{
                res[cur++]=arr[p2++];
            }
        }
        while(p1<=mid){
            res[cur++]=arr[p1++];
        }
        while(p2<=hi){
            res[cur++]=arr[p2++];
        }
        for(int i=0;i<res.length;i++){
            arr[lo+i]=res[i];
        }
    }

快速排序

快速排序利用的主要是partition操作和递归,每次都先将数组分割成两部分,小于a的放左边,大于a的放右边。然后递归分割这两部分。
与归并排序的区别是归并是先递归,然后归并排好序的两部分。快排是先分割,然后递归分割两部分。
平均复杂度为O(nlogn),最好O(n),最坏O(n2),不稳定,所以工程中排序基本数据类型用的就是快速排序。
比起普通的快速排序,改进的地方主要有借鉴了荷兰国旗问题将partition分成了三部分,第二个改进的地方就是切分的时候是随机一个数来切分的。
partiton操作非常重要,主要思想就是通过遍历把数组分成前后两部分或者是三部分,分成三部分需要两个分界点,分成两部分需要两个分界点。
只需要一个while操作遍历即可完成分割。

public static void main(String[] argc){
       int[] arr={0,6,3,2,14,6,7};
       sort(arr);
       for (int i=0;i<arr.length;i++){
           System.out.println(arr[i]);
       }
    }
    public static void sort(int[] arr) {
        if(arr==null || arr.length==0){
            return;
        }
        quickSort(arr,0,arr.length-1);
    }
    public static void quickSort(int[] arr,int lo,int hi){
        if(lo>hi){
            return;
        }
        int[] mid=partition(arr,lo,hi);
        quickSort(arr,lo,mid[0]-1);
        quickSort(arr,mid[1]+1,hi);
    }
    public static int[] partition(int[] arr,int lo,int hi){
        int ran=lo+(int)(Math.random()*(hi-lo+1));//注意这里要加lo
        swap(arr,ran,hi);
        int less=lo-1;
        int more=hi;
        int cur=lo;;
        while(cur<more){
            if(arr[cur]<arr[hi]){
                swap(arr,cur++,++less);
            }else if(arr[cur]>arr[hi]){
                swap(arr,cur,--more);//注意这里的cur是不加的,因为换过来的还没遍历
            }else{
                cur++;
            }
        }
        swap(arr,hi,more++);
        return new int[]{less+1,more-1};
    }
    public static void swap(int[] arr,int lo,int hi){
        int temp=arr[lo];
        arr[lo]=arr[hi];
        arr[hi]=temp;
    }

partition应用也非常多,就是在考虑分组成几部分或者要得到某部分的时候非常有用。
1.比如说第一个是返回超过数组中一半长度的数字,这个可以根据它的特点知道排好序的中位数肯定是这个数字。所以可以利用partition将随机一个数字进行分割,分为前面小于它,后面大于它,中间等于它,如果返回发现中间位置不在等于它的范围内,就缩小范围来循环partition,直到发现这样的数在中间了。再看这个数是否超过一半,超过则就是这个数,不超过则返回0。
2.返回数组中第k大的数字,其实就是一个partition,先随机选一个数字,左边比它小右边比它大,然后如果不在第k个接着partition,直到找到第k个数。
3.比如说找出数组中的前k个数,这个也是要把数组分成两半,先以一个数比如说第k个数partition,分好之后看它的左边有几个数,不管小于还是大于都循环partition直到分好之后下标为k

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值