持续创作,加速成长!这是我参与「掘金日新计划 · 10 月更文挑战」的第3天,点击查看活动详情
最近开始接触与学习数据结构与算法,一方面为了解决课内数据结构课解题思路不清晰的问题,另一方面也是听说左神的大名,故开始跟着左神一起学习数据结构与算法。同时以写博客的形式作为输出,也算是为了对所学的知识能掌握的更深吧
递归行为以及递归行为时间复杂度的估算
master公式的使用
前提:子问题等规模
T(N) = a * T(N/b) + O(N^d) N为母问题的规模,N/b为子问题的规模,O(N^d)为除了递归部分的规模以外,剩下部分的时间复杂度 a为执行子问题递归的次数
如:求一个数组{3,6,1,4,2,5,8}的最大值,采用递归二分的方法 代码实现如下:
public static void int process(int[] arr,int L,int R){
if(L==R) return arr[L];
int mid = L+((R-L)>>1);//相当于L+(R-L)/2
int leftMax = process(arr,L,mid);
int rightMax = process(arr,mid+1,R);
return Math.max(leftMax,rightMax);
}
母问题的规模是R-L,在process中,母问题被分解为两个规模一样的子问题来求解,并且分别调用一次process,因此子问题的规模是N/2,a是2。在process中,除去递归部分,执行语句的次数为常数次,因此O(N^d)是O(1); 综上:此递归方法的时间复杂度为 T(N) = 2 * T(N/2) + O(1)
当a,b,d都确定了,时间复杂度T(N)也就确定了
- log(b,a)>d -> 时间复杂度为O(N^log(b,a))
- log(b,a) = d -> 时间复杂度为O(N^d * log(2,N))
- log(b,a) < d -> 时间复杂度为O(N^d)
归并排序
基本思想:分治思想 每层递归的步骤:
- 将序列分解成两个子序列
- 按顺序合并两个子序列
代码实现: L为左侧索引,R为右侧索引
public static void process(int[] arr ,int L,int R){
if(L == R) return;
int mid = L+((R-L)>>1);
process(arr,L,mid);
process(arr,mid+1,R);
merge(arr,L,mid,R);
}
public static void merge(int[] arr,int mid,int L,int R){
int help[R-L+1];
int i = 0;
int p1 = L;
int p2 = mid + 1;
while(p1<=mid && p2<=R){
help[i++] = arr[p1] <= arr[p2] ? arr[p1++] : arr[p2++];
}
while(p1<=mid){
help[i++] = arr[p1++];
}
while(p2<=R){
help[i++] = arr[p2++];
}
//把help数组的数据复制到arr数组上
for(i = 0;i<help.length;i++){
arr[L + i] = help[i];
}
}
时间复杂度:O(N * log(2,N)) 额外空间复杂度O(N)
归并排序为什么在时间复杂度上优于选择排序和冒泡排序
选择排序和冒泡排序的时间复杂度都是O(N^2),而归并排序的时间复杂度是 O(N * logN) 因为选择排序和冒泡排序在一轮循环中只能确定一个元素的位置,且做了很多次无用的比较,而归并排序在比较的同时也大致确定了比较的其中一个元素应有的位置。在最后一次迭代中变成两个有序子数组进行归并操作,在比较的同时也将元素排入了正确的位置,减少了无用的比较次数。
归并排序的拓展
小和问题
对于数组中的每一个数i,在i右边有n个大于它的数,i的小和就是 n * i, 所有数的小和加起来就是数组的小和。
现有有一个数组{1,5,3,7,4,2,8,9,6},试求该数组的小和是多少?
思路:介于归并操作会通过迭代的方式将原数组分成若干个,不断进行两个子数组之间的比较和排序,因此每次进行merge的两个数组都会是有序的状态。基于小和的特点,我们可以将左子数组的元素依次与右子数组的元素进行比较,当找到左子数组的元素小于右组数组的某一个元素的时候,就可以根据右子数组的下标求得左子数组元素对应的最小和。
[[归并排序——小和问题图解.png]]
以右边的子数组集为例: 在第一组左右子数组①中,1<5,所以小和为1;
在第二组左右子数组②中,1<3,小和为1,5>3,小和为0;
在第三组左右子数组③中,1<7 && 1<4,小和为2,5<7 && 5>4,小和为5,3<7 && 3<4,小和为6;
在第四组左右子数组④中,1小于右侧4个元素,小和为1 * 4 = 4,5小于右侧三个元素,小和为5 * 3 = 15,3小于右侧三个元素,小和为3 * 3 = 9...依次类推
最后只需要将所有的小和加在一起即为数组的小和。 所以我们只需要在迭代函数中归并子数组的时候将子数组产生的小和返回即可。
代码实现:
public static int process(int arr[],int L,int R){
if(L == R) return;
int mid = L + ((R-L)>>1);
return process(arr,L,mid) + process(arr,mid,R) +
merge(arr,L,mid,R);
//返回 左若干个子数组产生的小和 + 右若干个子数组产生的小和 + 当前左右两个子数组产生的小和(左子数组:L-mid,右子数组:mid-R)
}
public static int merge(int arr[],int L,int mid,int R){
int help[R-L+1];
int i = 0;
int p1 = L;
int p2 = R;
int res = 0;//小和
while(p1<=mid && p2<=R){
res += arr[p1]<arr[p2] ? arr[p1]*(R-p2+1):0;//统计小和
//进行归并操作
help[i++] = arr[p1] < arr[p2] ? arr[p1++] : arr[p2++];
}
while(p1<=mid){
help[i++] = arr[p1++];
}
while(p2<=R){
help[i++] = arr[p2++];
}
//将help数组结果拷贝到arr数组中
for(i = 0;i<arr.length;i++){
arr[L+i] = help[i];
}
return res;//返回小和
}
逆序对问题
与小和问题同理
快速排序
荷兰国旗问题
给定一个数组和一个数num,把小于等于num的数放在数组的左边,大于num的数放在数组的右边。要求额外空间复杂度O(1),时间复杂度O(N)
关于快速排序有三种思路
快排1.0
设定一个用于记录小于等于区间最右边的位置的变量x,用变量i记录当前需要比较的元素,当i遍历数组时会遇到3种情况
- arr[i] <= num:arr[i]和arr[x+1]的元素交换,<=区右扩,i++
- arr[i] > num:i++
图示
通过不断的比较和交换扩大<=区的范围直到i越界为止,时间复杂度为O(N),每一次排序只能确定一个num的位置
快排2.0
在1.0的基础上增加一个大于区,并将小于等于区改为小于区,通过不断地比较和交换扩大>区和<区的范围,而=区就在两个区间的中间,这种快排稍微优于1.0版本,每一次排序都能确定一批num的位置
注意:在比较中会遇到三种情况:
- arr[i] < num,arr[i]和arr[x+1]交换,i++,x++
- arr[i] == num,i++
- arr[i] > num,arr[i]与大于区的前一个元素交换,但是i不需要改变。因为此时i是刚换过来的大于区的前一个元素,尚未进行比较。
快排3.0
在2.0的基础上,不再以数组的最右边元素作为num,而是随机选取数组中一个元素作为num,然后进行2.0的排序,时间复杂度为O(N * logN)
快排3.0代码实现
L为左侧索引,R为右侧索引
public static void quickSort(int[] arr,int L,int R){
if(L < R){
swap(arr,L + (int)(Math.random()*(R-L+1)),R);//交换数组中随机一个数字与数组最右侧元素的位置,最右侧的元素即为num
int[] p = partition(arr,L,R);//p数组长度为2,p[0]是<区的右边界,p[1]是大于区的左边界
quickSort(arr,L,p[0]-1);//小于区
quickSort(arr,p[1]+1,R);//大于区
}
}
public static int[] partition(int[] arr, int L,int R){
//用于处理L-R的数组区间的函数,将该区间处理成 <num,==num, >num 的格式,并返回==num区间的左右边界
int less = L-1;
int more = R;
int i = L;
while(i<more){
if(arr[i] < arr[R]){//arr[R]是num
swap(arr,arr[i++],arr[++less]);//交换位置,小于区右扩
}else if(arr[i] > arr[R]){
swap(arr,arr[i],arr[--more]);//注意,i保持不变,大于区左扩
}else{
i++;
}
}
swap(arr,more,R);//把R位置上的num元素与大于区间的左边第一个元素交换
return new int[] {less+1,more};//返回等于区的左右两边的边界索引
}
今天的学习就到这里啦!国庆节学习的动力真的很低。。磨蹭到现在才学完今日任务QAQ。
研发后台摸鱼冠军在这里祝大家国庆节快乐啦!(话说是不是已经过了国庆节来着)