快排和归并排序--快排处理第k大元素

冒泡、插入、排序这些排序时间复杂度都是 O(n2),时间复杂度高,适合小数据规模处理。
对于大数据规模排序,用归并排序和快速排序比较适合。
归并排序和快速排序都使用了分而治之的思想。

归并排序

归并排序的思想就是将数组分成前后两部分,然后再对分开的两部分再分成两部分,以此类推,分到不可再分为止,再合并一起,如此数组便可成有序了。
这个先分两半、再把两半分成两半、再分成两半,这就是前面所讲述的递归思想。所以可以用递归完成归并排序。
在这里插入图片描述
归并排序思想是分治,分而治之,将大问题分为小问题,小问题解决了,大问题也解决了。
分治思想是一种解决问题的思想,递归是一种编程技巧,两者并不冲突。
下面用递归处理归并排序:

1.递归公式
merge_sort(p…r) = merge(merge_sort(p…q),merge_sort(q+1…r))这里的q是中间元素下标,代表对半
2.终止条件:
p>=r的时候终止

p、r是无序数组的初始坐标和末元素坐标,将一整个排序问题化为了两个子问题,一个是merge_sort(p…q)和merge_sort(q+1…r)。用递归处理子问题,当子问题处理好了,再进行合并,这样p到r的数据大问题也处理好了。
代码如下:

    public static void merge(int[] arr, int start, int mid,int end){
        int i = start; //左边遍历起点 
        int j = mid+1; //右边遍历起点
        int[] tmp = new int[end-start+1];  //临时数组
        int tmp_i = 0; //临时数组起点坐标

        while (i<=mid&&j<=end){  //左边从起点到mid,右边mid+1到end遍历,两边一起开始遍历
            if (arr[i]<arr[j]){ //把小的放入数组如果右边比左边大,就先把小的左边放入临时数组
                tmp[tmp_i++] = arr[i++];//把小的,左边放入数组
            }else {
                tmp[tmp_i++] = arr[j++]; //把小的右边放入数组
            }
        }
        while (i<=mid){ 上面遍历完后,左边还有值没放入数组,直接放
            tmp[tmp_i++] = arr[i++];
        }
        while (j<=end){上面遍历完后,右边还有值没放入数组,直接放
            tmp[tmp_i++] = arr[j++];
        }
        for (i=0; i<tmp_i;++i){ //遍历临时数组,替换原数组
            arr[start++] = tmp[i];
        }
    }
    public static void merger_up_todown(int[] arr, int start, int end){
        if (start<end){//递归结束条件
            int mid= (start+end)/2;
            merger_up_todown(arr,start,mid); //递归左边
            merger_up_todown(arr,mid+1,end);//递归右边
            merge(arr,start,mid,end);//合并
        }
    }


先从merger_up_todown这个方法看,里面递归了三个merge,
前面两个merger_up_todown将数组的两半分别进行了拆分然后合并成有序的子数组
最后的一个merge将前面的两个有序子数组进行最后的合并成一个有序的数组.
整体过程如下:

1.申请一个tmp临时数组,大小和无序数组相同arr。
2.然后申请两个坐标i和j代表了了初始元素i和中间元素+1。代表了将数组分为两半的起始点元素,即A[p…q]和 A[q+1…r]
3.比较两个元素A[i]和 A[j],哪个小就放入临时数组tmp,且i或者j后移一位。
4.上面第三步操作进行循环,直到一个子数组的所有数组都放进了临时数组,再把另一个数组的剩余元素全部依次放入临时数组末尾。这个时候临时数组存放的就是两个子数组合并的最后结果了,再把临时数组的数据拷贝到无序数组arr中。
在这里插入图片描述

归并排序性能分析

归并排序是稳定的排序算法吗

合并过程中,如果A[p…q]和 A[q+1…r]之间有值相同的元素,先将A[p…q]的元素放入临时数组中,这样就没有改变相同元素的位置,所以是稳定的。

归并排序时间复杂度

假设归并排序中,问题a分解为求解问题b和问题c,等b和c解决后再合并b和c
求解a问题的时间是T(a),求解b和c的时间是 T(b) 和 T( c)
那么

T(1) =C ;C代表常量时间
(a) = T(b) + T© + K
K是合并两个问题bc的时间
假设对n个元素归并排序时间的T(n),
那么两个分解子问题的时间是T(n/2),merge合并两个有序子数组的时间复杂度是O(n)
所以归并时间复杂度的计算公式是
T(n) = 2T(n/2) + n; n>1

T(n) = 2
T(n/2) + n
= 2*(2T(n/4) + n/2) + n = 4T(n/4) + 2n
= 4
(2T(n/8) + n/4) + 2n = 8T(n/8) + 3n
= 8*(2T(n/16) + n/8) + 3n = 16T(n/16) + 4n

= 2^k * T(n/2^k) + k * n

T(n) =2^k * T(n/2^k) + k * n。
当T(n/2^k)=T(1) 时
即n/2^k=1 ,所以k=log2n ,将K带入公式,得到T(n)=Cn+nlog2n,换成O(n),T(n) 就等于 O(nlogn)

所以归并排序的时间复杂度是O(nlogn)。这个时间复杂度是稳定的,包括了最好、最坏、平均时间复杂度都是O(nlogn)。

空间复杂度

这是归并排序的弱点,归并排序不是原地排序。
归并排序在排序的时候需要借助一个临时数组tmp,这个数组大小跟原无序数组一样是n,所以空间复杂度是O(n)。

快排

快速排序的核心核心思想是分治,例如要排序下坐标p到r数组的数据,然后规定p-r之间一个点叫分区点pivot,使得分区点左边的数是比分区点上的数小的,即arr[p]<arr[pivot] ,分区点右边的数比分区点上的数大,即arr[pivot] < arr[r],然后通过递归、分治思想取缩减p到pivot的距离、pivot到r的距离,使得区间变为1 ,此时数据有序。

如何编排数据使得左边的数比分区点小,右边的数比分区点大,通常油三种方法,如下:

package com.company;

import java.util.Arrays;



public class MyquickSort {
    public static void main(String[] args) {
        int[] arr1 = {0,11111,103,100,1,3,2,6,4,5,8,7,10,9};
        quecksort3(arr1,0,arr1.length-1);
        System.out.println(Arrays.toString(arr1));
    }
    public static void quicksort1(int[] arr, int begin, int end){
        //左右指针法
        /**
         * 1.选出一个key,一般是最左或者最右
         * 2.定义begin,end,begin从左往右走,end从右往左走
         * 3.key在左边的话,end先走。key在右边的话end先走
         * 4.走的过程中,如果end遇到的数小于key遇到的数,停下此时为arr[end]
         * 5.end停下后,begin也往前走,如果遇到大于key的数, 也停下,此时为arr[begin]
         * 6.arr[end]和arr[begin]进行交换
         * 7.交换后end继续往左,如果遇到比key的数小,停下
         * 8.begin往右走,如果遇到比key大的数,停下,交换。
         * 9.如果在第8的时候没有遇到比key大的数,begin就会遇到end。就是begin=end
         * 10.这个时候,将key跟begin对应的数进行交换,交换后,新key左边是比key小的数,右边的数是比key大的数
         * **/
        if (begin >= end){
            return;
        }
        /*
        * begin,0, end arr.length-1
        * */
        int left = begin;  //定义一个左变量,留着做递归
        int right = end;  //定义一个右变量,留着做递归
        int keyi = begin;  //定义key所在的下坐标。
        while (begin < end){  //当begin<end的时候遍历,当begin=end的时候跳出循环,跟arr[begin]跟arr[keyi]交换
            while (arr[end] >= arr[keyi] && begin <end){ --end;} //找出当begin<end的时候,arr[end]<arr[keyi]的下坐标,即在右边找出比key小的值
            while (arr[begin] <= arr[keyi] && begin < end){ ++begin;}//跟上同理,在左边找出比key大的值
            int tmp = arr[end]; // 此三行是将上面找出的右边大值arr[end]、左边小值arr[begin]进行交换
            arr[end] = arr[begin];
            arr[begin]=tmp;
        }
        //经过上面的while循环后,这时候begin跟end指针已经相遇,即begin = end,所以这时候需要将begin坐标对应的值跟key值进行交换
        int tmp = arr[end];
        arr[end] = arr[keyi];
        arr[keyi] = tmp;
        keyi = end;  //将begin和end相遇的下坐标赋值给keyi,作为数据切割点,切割左边的数据做下一次的递归,切割左边的数据做递归
        quicksort1(arr,left,keyi-1);
        quicksort1(arr,keyi+1,right);
    }
    public static void quicksort2(int[] arr, int begin, int end){
        //挖坑法
        /**
        * 1.定义一个单独变量key保存key值(是key值,不是下坐标),一般是数组最左或者最右的的一个元素
        * 2.1完成后,key值对应的坐标可以看成是一个坑位,可以随意用别的数填取(即赋值给该下坐标)
        * 3.定义两个指针begin\end对应数组的0坐标和最末尾坐标
        * 4.如果是选最左(即arr[begin],数组的下坐标0)作为key值,即坑位是arr[begin],因为arr[begin]的值已经赋值给了key,不受数组影响了,先从end--遍历
        * 5.如果end--遍历,找到比key值小的arr[end],这时候需要arr[end]赋值给4说的坑位,arr[begin] = arr[end],俗称填坑。赋值填坑完成后,此时的arr[end]就变成新的坑位
        * 6.end填坑后,开始begin++遍历,找出比key大的值arr[begin],然后将该值赋值给5的arr[end]坑位进行填坑,此时的arr[begin]就变成了新坑位。
        * 7.begin填坑后,再次循环做5、6步骤,直至begin和end在一个坑位相遇(end/begin填坑后,end/begin位置留下坑位,然后另一个指针begin/end遍历++/--找不到比key大或者小的值,一直遍历到begin=end)
        * 8.相遇后,下坐标就是坑位,将key值赋值到坑位中,
        * 9.将该坑位定义给keyi,进行分割数据分别做递归
        * */
        if (begin>=end) {return;}
        int left = begin;//定义一个左变量,留着做递归
        int right = end;//定义一个右变量,留着做递归
        int key = arr[begin]; //将数组最左位arr[0]定义并赋值给变量key,留下坑位
        while (begin<end){//左右指针遍历
            while (arr[end] >=key && begin < end){ --end; } //end找出指针比key小的值,
            arr[begin] = arr[end]; //将end找出的比key小的值赋值给坑位arr[begin]进行填坑,留下坑位arr[begin]
            while (arr[begin] <= key && begin < end){++begin;}//end填坑后,begin++找出比key大的值,
            arr[end] = arr[begin];//将begin找出的值赋值给arr[begin]进行填坑,留下坑位
        }
        //begin跟end相遇在某个坑位
        arr[begin] = key; //用key填坑
        int keyi = begin; //将此次处理的最后坑位做数据切割点,切分数据
        quicksort2(arr,left,keyi-1);//切分数据,左边递归
        quicksort2(arr,keyi+1,right);//切分数据,右边递归
    }
    public static void quecksort3(int[] arr, int begin, int end){
        //前后指针
        /**
        * 1.选出一个key,用keyi记录它的下坐标,一般最右或者最左。
        * 2.定义pre之前指向数组开头(在一开始的时候,pre指向begin-1),cur指针指向pre+1(begin)
        * 3.首先arr[cur]会跟key值arr[keyi]比较,如果arr[cur]比key值小,pre指针则会往后移动一位,即pre+1,然后arr[pre+1]跟arr[cur]交换数据,交换完后cur继续往后cur++
        * 4.如果arr[cur]比key值大,则cur一直往后走,一直cur++
        * 5.cur++一直往后走, 直到cur到end位置,即cur=end
        * 6.cur=end后将key值arr[keyi]和++pre值arr[++pre]交换,
        * 7.然后现在pre左边是比它小的数,右边是比他大的数,所以将此时的pre赋值给keyi,切分数据,递归
        * */
        if (begin >= end){return;}
        int pre = begin - 1,cur = begin;
        int keyi = end;
        while (cur != end){
            System.out.println("pre:"+pre);
            System.out.println("cur:"+cur);
            System.out.println("stop");
            if (arr[cur] < arr[keyi] && ++pre !=cur){
                int tmp = arr[pre];
                arr[pre] = arr[cur];
                arr[cur] = tmp;
            }
            ++cur;
        }
        int tmp1 = arr[keyi];
        arr[keyi] = arr[++pre];
        arr[pre] = tmp1;
        keyi = pre;
        quecksort3(arr,begin,keyi-1);
        quecksort3(arr,keyi+1,end);
    }
}

归并排序对比快排

归并排序和快排都是使用了分治、递归的思想去进行实现。理解归并排序重要的是理解递归公式和merge()合并函数,理解快排重要的是理解递归公式和partition()方式。

归并排序是一种时间复杂度比较稳定的排序方法,缺点是不是原地排序,空间复杂度高,O(N)

快排最坏时间复杂度是O(n2),但是平均复杂度是O(nlogn)。而且很小概率演变为最坏时间复杂度。

快排处理第k大元素

package main.java.java_test;

import java.util.Arrays;

/**
 * @discreption:
 * @author: Chen
 * @date: 2022年01月15日 22:13
 * @version: 2022年01月15日 admin
 */
public class 第k大元素 {
    public static void main(String[] args) {
        int[] arr1 = {0,11111,103,100,1,3,2,6,4,5,8,7,10,9};
        int result = quickfindLarge(arr1,3);
        System.out.println(Arrays.toString(arr1));
        System.out.println(result);
    }
    public static int quickfindLarge(int[] arr, int k){
        k = arr.length-k; //在有序序列中找序号为arr.length-k大的数
        int begin = 0,end = arr.length-1;  //定义遍历数组
        while (begin < end){  //左右指针便利
            int keyi = quickPartition(arr,begin,end); //找出每次的分区点
            if (keyi == k){  //当分区点==k的时候,为第几大元素,如k=1,为最大的数
                break;
            }else if (keyi < k){ //当分区点在k左边,这个时候arr[keyi]的数不够大,需要左指针右移再分区一次,
                begin = keyi+1;
            }else {  //分区点在k右边,这个时候arr[keyi]太大,需要右指针左移一次
                end = keyi-1;
            }
        }
        return arr[k];
    }
    //快排分区方法,切分数据,使得分区点左边数据小于分区点上的数据,分区点右边的数据大于分区点数据
    public static int quickPartition(int[] arr, int begin, int end){
        int   keyi = begin; // 设置数组最左边为分区点
        while (begin < end){ // 左右指针便利
            while (arr[end] >= arr[keyi] && begin < end){ --end;} //找出右指针比分区点小的数
            while (arr[begin] <= arr[keyi] && begin < end){ ++begin;} //找出左指针比分区点大的数
            int tmp = arr[end]; //  交换数据
            arr[end] = arr[begin];
            arr[begin] = tmp;
        }
        int temp = arr[end];  //左右指针相遇,移动分区点到相遇点
        arr[end] = arr[keyi];
        arr[keyi] = temp;
        keyi = end; //找出最新的分区点
        return keyi;
    }
}

在处理第k大元素的时候,需要将之前快排的代码稍微改一下,将原本合并递归的代码修改为两个方法,一个是分区方法,即将数据左右分区,左分区的数小于分区点,右分区的数大于分区点, 另一个方法是寻找第k大分区点的方法,将分区方法求出来的分区点所在的位置与k相比,当分区点=k的时候,此时的arr[分区点]为第k大的数

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值