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):母问题的时间复杂度,N代表的是母问题的数据量是N级别的;
a:子问题调用的次数(等量的);
T(N/b):子问题是N/b规模的(等量的);
O():除去子问题调用过程的时间复杂度
情况a:① >d —— 复杂度O(N^
)
情况b:② =d —— 复杂度O(N^d*logN)
情况c:③ <d —— 复杂度O(N^d)
1.8归并排序
整体思路:将一个数组拆分成左右数组,在再其基础上进行左右分组,利用递归拆成一个一个数,之后进行排序后一层一层返上来。
之前的时间复杂度都是O(),是因为每次比较后都浪费了很多比较行为没有用到下次比较上,而归并排序没有浪费比较行为,每次的比较行为都变成了有序的东西去跟更大范围的去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]);
}
}