数据结构学习(基础)——简单排序法——Day02

1.7递归行为及递归时间复杂度

用系统栈把整个过程压栈。每次全而未绝的东西就压到栈里,算完的东西从栈里弹出释放到上级(我们可以在递归中进行排序,递归本身不是排序)。整个递归过程是一个多叉树,计算所有树节点的过程,就是利用栈做一次遍历,每个节点通过自己的子节点汇集信息后才能往上面返回。

求解arr[L,,R]范围内的最大值问题

在一个数组中,我们找到中间索引将数组分成左右两部分,之后再将左右两部分按照相同的方式进行分组,分到最后将会是一个一个小部分,每一个小部分就是原数组中的一个数,类似一个树型结构。最底下一级进行比较,返回大的之后进入上一级再进行比较,一层一层上来,到第二层,两个数分别为左右两部分的最大值,最后再进行比较,即可选择出整个数组最大值。

public static int getMax(int[] arr){
    return process(arr,0,arr.length-1);
}

//如果mid=(L+R)/2  很小情况下会报错,因为L+R可能溢出,所以通常用mid = L+(R-L)/2 也可以用位运算符右移一位代表除2
public static int process(int[] arr,int L,int R){
    if(L == R){
        //代表只有一个数
        return arr[L];
    }
    
    int mid = L + (R-L)>>1;
    int leftMax = process(arr,L,mid);
    int rightMax = process(arr,mid+1,R);
    return Math.max(leftMax,rightMax);
}

master公式:用于求解子问题规模相同的母问题时间复杂度;三个系数确定了时间复杂度就确定了

T(N)=a*T(N/b)+O(N^{d})

T(N):母问题的时间复杂度,N代表的是母问题的数据量是N级别的;

a:子问题调用的次数(等量的);

T(N/b):子问题是N/b规模的(等量的);

O(N^{d}):除去子问题调用过程的时间复杂度

情况a:① logb^{a}>d —— 复杂度O(N^logb^{a}

情况b:② logb^{a}=d —— 复杂度O(N^d*logN) 

情况c:③ logb^{a}<d —— 复杂度O(N^d) 

1.8归并排序

整体思路:将一个数组拆分成左右数组,在再其基础上进行左右分组,利用递归拆成一个一个数,之后进行排序后一层一层返上来。

之前的时间复杂度都是O(N^{2}),是因为每次比较后都浪费了很多比较行为没有用到下次比较上,而归并排序没有浪费比较行为,每次的比较行为都变成了有序的东西去跟更大范围的去merge,比较行为信息就会往下传递,所以时间复杂度低。

①从中间分开,左边先排序,右边再排序,再整体排序

②整体排序的时候利用merge过程,用了外排序方法,单独开辟出来一个空间,左边第一个跟右边第一个比较,谁小先放谁,放哪边哪边的指针右移之后再去比较和放,直到一边的指针越界后,另一边就全放进去。

③在开辟出来的空间有序后,再拷贝到原来的数组即可

④由于子问题都是等量的,所以可以用master公式求解母问题的时间复杂度

        T(N)=2*T(N/2)+O(N) a=2 b=2 d=1 所以时间复杂度为O(N^d*logN)

        额外空间复杂度O(N)因为每次merge一次我们都会创建一个临时数组,用完自动释放

public class processSort(int[] arr){
    public static void processSort(){
        //如果数组长度小于1或者数组不存在
        if(arr == null || arr.length < 1){
            return;
        }
    }
}

例1:小和问题(在一个数组中,每一个数左边比当前数小的数累加起来,叫做数组的小和),求一个数组的小和。

例子:[1,3,4,2,5]1左边比1小的数,没有;3左边比3小的数,1;4左边比4小的数,1,3;2左边比2小的数,1;5左边比5小的数,1,3,4,2;所以小和为:1+1+3+1+1+3+4+2=16。

不重复不遗漏

暴力解法,我们可以从第一个数开始遍历之后看看左边有几个比他小之后相加,但是这样做时间复杂度O(N方);由于递归排序,我们是把一个数组先按照树的形式拆成一小块一小块之后再merge进行排序后往上返,在merge的过程中,是左组指针所指的数跟右组指针所指的数进行比较,如果小的话放进临时数组,直到有一方指针溢出另一组数全放进去后再往上返。那么我们就可以把小和问题想成从左到右开始数,某个数右边有几个比他大的数,之后这个个数*本身的数再相加也是总和。所以我们可以利用递归,在merge的过程中去判断,每一次merge,这个数右方有几个比他大的数,这样做不会重复也不会遗漏,比如这个4在1、3一组的右边,就不会产生小和;但是在和2、5merge的过程当中又是在左边,才会去看右边有几个数比4大才会产生小和。

在merge过程中排序的过程不能省略,因为我们要看右组中有几个数比左组中这个数大,不是去遍历查找,而是通过下标去查找

与经典的merge过程有一点不一样,当左组指针的值跟右组指针的值相同的时候,拷贝下来右组的数,并且不产生小和。因为如果先拷贝左组的数,我们也就不知道右组有多少个数比他大。

public class xiaoheTest {
    public static int xiaohe(int[] arr){
        //如果这个数组长度<1或者不存在
        if(arr == null && arr.length < 1){
            return -1;
        }else if(arr.length == 1){//如果长度为1就返回当前数
            return arr[0];
        }

        //除了以上情况,就会出现小和问题
        //我们可以讲小和问题转化成从左到右,每个数右侧有几个比自己大的数,因为我们可以利用递归拆成左侧和右侧之后利用右侧的指针求出,便捷
        //先调用递归,再递归里面进行求和
        return digui(arr,0,arr.length-1);
    }

    /**
     * 递归方法
     * 我们在这里即要排序也要求小和
     */
    public static int digui(int[] arr,int L,int R){
        if(L==R){//当L==R的时候就代表该组只有一个数,所以也就不存在比这个数大的了
            return 0;//我们不需要排序也不用求小和
        }
        int mid = L + ((R-L)>>2);
        digui(arr,L,mid);
        digui(arr,mid+1,R);
        //求和
        return digui(arr,L,mid) + digui(arr,mid+1,R)+qiuhe(arr,0,mid,arr.length-1);//返回左侧小和+右侧小和+整体小和给上一级
    }

    /**
     * 求和:
     * 因为在归并排序中,我们左侧指针和右侧指针比较数值时,谁小就放里面,而大的指针不动
     * 所以当左侧放进去一个数时,右侧指针和指针右侧的数都是大于这个数的,我们求出来有几个再喝左侧那个数相乘就是和
     */
    public static int qiuhe(int[] arr,int L,int M,int R){
        //定义临时数组
        int[] help = new int[R-L+1];
        int i = 0;
        int sum = 0;//用于存放和

        //定义左侧指针和右侧指针用于比较
        int p1 = L;
        int p2 = M+1;
        //进行求和同时排序(如果不排序就毫无作用)
        while(p1 <= M && p2 <= R){
            help[i++] = arr[p1] < arr[p2] ? arr[p1++] : arr[p2++];
            sum += arr[p1] < arr[p2] ? (R-p1+1)*arr[p1] : 0;
        }
        while (p1 <= M){
            help[i++] = arr[p1++];
        }
        while (p2 <= R){
            help[i++] = arr[p2++];
        }

        for(i = 0;i < help.length;i++){
            arr[L+i] = help[i];
        }
        return sum;
    }
}

 例2:逆序对问题:在一个数组中,左边的数如果比右边的数大,则这两个数构成一个逆序对,请打印所有逆序对。

例3:

一、给定一个数组arr,和一个数num,请把小于等于num的数放在数组的左边,大于num的数放在数组的右边。要求额外空间复杂度O(1),时间复杂度O(n)。

//荷兰国旗上一个问题,<某个数都在左侧,右侧都是>=这个数
    public static void shu(int[] arr,int target){
        //定义左边界
        int L = -1;
        int i = 0;
        while (L < arr.length-1){
            if(arr[i] < target){
                swap(arr,++L,i++);
            }else{
                i++;
            }
        }
    }

二、(荷兰国旗问题)给定一个数组arr,和一个数num,请把小于num的数放在数组的左边,等于num的数放在数组的中间,大于num的数放在数组的右边。要求额外空间复杂度O(1),时间复杂度O(n)。

public class helanguoqiTest {
    /**
     * 荷兰国旗问题
     * 将数组分成<target =target >target三部分
     * 定义数组左边界=-1和右边界=数组长度,之后从头开始遍历,如果这个数比目标值小的话,左边界下一位数跟i位置的数交换并且右移同时i++,如果大于目标值的话,右边界前一个数跟i位置的数交换并且左移,如果相等的话i增大
     * @param arr
     * @param target
     */
    public static void helanguoqi(int[] arr,int target){
        //定义左边界和右边界
        int L = -1;
        int R = arr.length;
        int i = 0;//定义一个在数组中遍历的指针
        //当L<R的时候进行交换,如果L=R则结束
        while(L<R && i<R){//i>=R的时候就代表已经排好了
            if(arr[i] < target){
                //左边界下一个位置的数和i位置数进行调换,同时都往下移动
                swap(arr,++L,i++);
            }
            if(arr[i] > target){
                //右边界前一个位置的数和i位置数进行调换,同时右边界前移,但是i不变,因为还要去判断移动过来的数和target比较
                swap(arr,--R,i);
            }
            if(arr[i] == target){
                //如果相同的话,左右边界都不变,i位置右移
                i++;
            }
        }
    }
    //两数交换的方法
    public static void swap(int[] arr,int a,int b){
        int temp = arr[a];
        arr[a] = arr[b];
        arr[b] = temp;
    }
}

1.9快速排序(时间复杂度O(nlogn)~ O(n^2) 3.0和1.0/2.0区别)(空间复杂度O(logN))

1.0版本,先挑选出数组中最后一个数,之后用这个数当做参考,把数组前面的数分成<=这个数和>这个数两个区域后,将这个数放到>这个数区域的第一个数做交换,那么当前数组就会变成三个区域<=这个数 这个数 >这个数 ,之后这样一直递归下去,让左侧和右侧重复这个行为。

2.0版本,利用荷兰国旗问题,利用数组最后一个数把数组分成<这个数 =这个数 >这个数三个区域,之后把数组最后一个数和>这个数区域第一个数进行交换,那么数组就分成了三个区域,之后<这个数和>这个数继续按照这个方式递归下去。

public class quickTest {
    public static void swap(int[] arr,int a,int b){
        int temp = arr[a];
        arr[a] = arr[b];
        arr[b] = temp;
    }
    /**
     * 快排2.0
     * 快排3.0 跟2.0的区别就是在数组中随机挑选出来个数放在数组最后之后进行2.0
     */
    public static void quickSort02(int[] arr,int L,int R){//因为我们还要在左侧和右侧进行递归下去再排序,所以需要定义左右区间
        if(L>=R){//L==R代表数组长度为1即可返回 L>R代表越界了
            return;
        }
        //将小于最后一个数的都放在左边大于等于这个数的都放在右边,之后最后一个数和右边区域第一个数进行交换
        int i = L;//不能将i定义为0因为在后续递归的时候如右分组是从i+1到R,这是0根本不在里面,会溢出,所以这个i应该是递归分组的数组中的第0个坐标也就是L
        int target = arr[R];//先找出来一个数,我们先选择数组中的最后一个
        int less = L-1;//定义左区域边界
        int more = R;//定义右区域边界,因为我们是利用最后一个数对前面进行划分,最后将他与i位置进行兑换
        while (less < more-1){//less<more代表左区域和右区域还没有碰到,可以进行划分
            if(arr[i] < target){//将左边界下一个数与数组最后一个数交换,并且都右移
                swap(arr,++less,i++);
            }else if(arr[i] > target){//将右边界前一个数和最后一个数进行交换,并且右边界前移,但是i不变,因为交换回来的数还没进行判断
                swap(arr,--more,i);
            }else{//如果跟最后一个数相同,i后移,左右边界不变,那么中间夹着的,就是最后一个数
                i++;
            }
        }
        swap(arr,more,R);//将target和>=target的区域的第一个数进行调换,这样中间就都是target了
        quickSort02(arr,L,i-1);
        quickSort02(arr,i+1,R);
    }

    public static void main(String[] args) {
        int[] arr = new int[5];
        arr[0] = 4;
        arr[1] = 8;
        arr[2] = 10;
        arr[3] = 1;
        arr[4] = 3;
        quickSort(arr,0,4);
        for(int i = 0;i < arr.length;i++){
            System.out.println(arr[i]);
        }
    }
}

3.0版本,在2.0基础上,所选的数不是数组的最后一个数,而是随机挑选出来一个数之后跟数组最后一个数进行交换后再继续2.0的算法,这样所选的每个数都是相同概率的,所以时间复杂度为O(nlogn)。

/**
     * 3.0
     * 所选目标值是随机的,再与最后一个交换重复2.0
     * @param
     */
    public static void quickSort03(int[] arr,int L,int R){//因为我们还要在左侧和右侧进行递归下去再排序,所以需要定义左右区间
        if(L>=R){//L==R代表数组长度为1即可返回 L>R代表越界了
            return;
        }
        //将小于最后一个数的都放在左边大于等于这个数的都放在右边,之后最后一个数和右边区域第一个数进行交换
        //降低时间复杂度,就是随机选取一个数与最后一个数进行交换
        int targetIndex = L+(int)(Math.random()*(R-L+1));//在数组中随机找一个坐标
        swap(arr,targetIndex,R);//用这个数和最后一个数进行交换,后续进行荷兰国旗问题,重复2.0操作
        int target = arr[R];//由于已经对换好了,所以target就是R位置上的数

        int i = L;//不能将i定义为0因为在后续递归的时候如右分组是从i+1到R,这是0根本不在里面,会溢出,所以这个i应该是递归分组的数组中的第0个坐标也就是L
        int less = L-1;//定义左区域边界
        int more = R;//定义右区域边界,因为我们是利用最后一个数对前面进行划分,最后将他与i位置进行兑换
        while (less < more-1){//less<more代表左区域和右区域还没有碰到,可以进行划分
            if(arr[i] < target){//将左边界下一个数与数组最后一个数交换,并且都右移
                swap(arr,++less,i++);
            }else if(arr[i] > target){//将右边界前一个数和最后一个数进行交换,并且右边界前移,但是i不变,因为交换回来的数还没进行判断
                swap(arr,--more,i);
            }else{//如果跟最后一个数相同,i后移,左右边界不变,那么中间夹着的,就是最后一个数
                i++;
            }
        }
        swap(arr,more,R);//将target和>=target的区域的第一个数进行调换,这样中间就都是target了
        quickSort03(arr,L,i-1);
        quickSort03(arr,i+1,R);
    }

    public static void main(String[] args) {
        int[] arr = new int[5];
        arr[0] = 4;
        arr[1] = 8;
        arr[2] = 10;
        arr[3] = 1;
        arr[4] = 3;
        quickSort03(arr,0,4);
        for(int i = 0;i < arr.length;i++){
            System.out.println(arr[i]);
        }
    }

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值