1.递归
在一个数组中找最大值,最直观的方式的是遍历,当然可以,但我们这次要用递归的方式。怎么做哪?
假设我们有一个数组,最左边元素我们记为L,第右边元素我们记为R,那么(L+R)/2即为中间元素,我们记为mid。
那么以中间元素mid为分界线,我们只要求出两边最大元素,两边最大元素进行比较,最大的就是这个数组最大元素。至于怎么求两边最大元素,和求原数组最大元素,寻找中间元素切分即可。当L=R,不能切分的时候,再逆序返回即可。
java代码如下:
public class test222 {
public static int getMax(int[] arr,int L,int R){
if(L==R){
return arr[L];
}
int mid = (L+R)/2;
int maxLeft = getMax(arr,L,mid);
int maxRight = getMax(arr,mid+1,R);
return Math.max(maxLeft,maxRight);
}
public static void main(String[] args) {
int[] arr = {
4,3,2,1};
System.out.println(getMax(arr,0,arr.length-1));
}
}
也可以这样理解这代码,相信大家都看过动漫,动漫一开始是主线情节,发展发展就开始支线情节,看似与主线没啥关系,但都是为了给主线情节打下铺垫。最后再回归主线情节,因为有支线情节的存在,所以主线情节才显得如此合乎常理。如果没有支线情节的过度,火影忍者鸣人一开始各种丸子,显得就非常牵强了。当然,即使如此,忍界大战那些回忆和博人传的支线,也是不能忍的。
回到主题,这段代码同样如此,先主线情节,把数组分成两段,保存全部主线情节后,进入支线情节发展,两段分别最大值,支线情节结束后,再回归主线情节,主线和支线贯通,直到排序完成,结束代码运行。
那么问题来了:递归不像我们平常的循环,那我们要怎么判断它的时间复杂度呢?
首先,我们需要认识一个公式:master公式
它的概念是这样的:
T(N) = a*T(N/b) + O(N^d)
- log(b,a) > d -> 复杂度为O(N^log(b,a))
- log(b,a) = d -> 复杂度为O(N^d *logN)
- log(b,a) < d -> 复杂度为O(N^d)
这里的a表示的是代表代码被分成几段,b表示子过程样本量,d表示去子过程调用之外,剩下的时间复杂度是多少
以上面的代码为例,int[] arr = {4,3,2,1};
寻找这个数组的最大值。首先判断a,我们把这个数组中间为界,平分成两断,即a=2(注:只看主线被切分成多少支线,不看支线被切分成支线的部分),再判断b,这个数组有4个元素,被切分后,每一部分有两个元素,即b=2,再判断d,子过程调用之位,只剩下了常数项操作,常数项操作一般忽略,记为0。
根据a,b,d,可以得到T(N) = 2*T(N/2)+O(1)
因为log(2,2)=1>d,故时间复杂度为O(N^log(2,2)),化简为 O(N)
总结
T(N):样本量为 N 的情况下,时间复杂度 N:父问题的样本量
a:子问题发生的次数(父问题被拆分成了几个子问题,不需要考虑递归调用,只考虑单层的父子关系)
b:被拆成子问题,子问题的样本量(子问题所需要处理的样本量),比如 N 被拆分成两半,所以子问题样本量为 N/2
O(N^d):剩余操作的时间复杂度,除去调用子过程之外,剩下问题所需要的代价(常规操作则为 O(1))
注意:master公式,只有在子问题划分规模一样才能用,如果划分规模不一样,则需要用相对应的数学公式计算。
2.归并排序
明白了上面的求最大值的递归原理,其实归并排序就很简单了。
先把右边的排好序,再把左边的排好顺,按照外排,再整体排好序,即可。
怎么理解这句话。举个例子:
我们有一个数组,里面元素是3,7,5,2,6,4
首先,我们中间在中间切分,左边为3,7,5,右边为2,6,4.
左边排好序为3,5,7,右边为2,4,6.
两边都设置一个指针,初始都指向第一个元素,再设置一个辅助数组。
指针指向的数进行比较,小的记入辅助数组,指针向右移一位,继续比较,当有一方到头,另一方按顺序去不计入辅助数组,再把辅助数组copy给原数组覆盖即可
演示:
3>2,2计入辅助数组,指向向右移动3<4,3计入辅助数组,指针向右移动一位,剩下 的以此类推,不做演示。
那归并排序的时间复杂度是多少?
一个数组被切分成两份,一个为N的样本,单个样本量为N/2,外排的时间复杂度为O(N),我们可以得到,T(N) = 2T(N/2)+O(N),套用master公式,可得时间复杂度为O(N*logN)
java代码(归并排序+对数器):
package basic_class_01;
import java.util.Arrays;
public class Code_05_MergeSort {
public static void mergeSort(int[] arr) {
if (arr == null || arr.length < 2) {
return;
}
mergeSort(arr, 0, arr.length - 1);
}
public static void mergeSort(int[] arr, int l, int r) {
if (l == r) {
return;
}
int mid = l + ((r - l) >> 1);
mergeSort(arr, l, mid);
mergeSort(arr, mid + 1, r);
merge(arr, l, mid, r);
}
public static void merge(int[] arr, int l, int m, int r) {
int[] help = new int[r - l + 1];
int i = 0;
int p1 = l;
int p2 = m + 1;
while (p1 <= m && p2 <= r) {
help[i++] = arr[p1] < arr[p2] ? arr[p1++] : arr[p2++];
}
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];
}
}
// for test
public static void comparator(int[] arr) {
Arrays.sort(arr);
}
// for test
public static int[] generateRandomArray(int maxSize, int maxValue) {
int[] arr = new int[(int) ((maxSize + 1) * Math.random())];
for (int i = 0; i < arr.length; i++) {
arr[i] = (int) ((maxValue + 1) * Math.random()) - (int) (maxValue * Math.random());
}
return arr;
}
// for test
public static int[] copyArray(int[] arr) {
if (arr == null) {
return null;
}
int[] res = new int[arr.length];
for (int i = 0; i < arr.length; i++) {
res[i] = arr[i];
}
return res;
}
// for test
public static boolean isEqual(int[] arr1, int[] arr2) {
if ((arr1 == null && arr2 != null) || (arr1 != null && arr2 == null)) {
return false;
}
if (arr1 == null && arr2 == null) {
return true;
}
if (arr1.length != arr2.length) {
return false;
}
for (int i = 0; i < arr1.length; i++) {
if (arr1[i] != arr2[i]) {
return false;
}
}
return true;
}
// for test
public static void printArray(int[] arr) {
if (arr == null) {
return;
}
for (int i = 0; i < arr.length; i++) {
System.out.print(arr[i] + " ");
}
System.out.println();
}
// for test
public static void main(String[] args) {
int testTime = 500000;
int maxSize = 100;
int maxValue = 100;
boolean succeed = true;
for (int i = 0; i < testTime; i++) {
int[] arr1 = generateRandomArray(maxSize, maxValue);
int[] arr2 = copyArray(arr1);
mergeSort(arr1);
comparator(arr2);
if (!isEqual(arr1, arr2)) {
succeed = false;
printArray(arr1);
printArray(arr2);
break;
}
}
System.out.println(succeed ? "Nice!" : "Fucking fucked!");
int[] arr = generateRandomArray(maxSize, maxValue);
printArray(arr);
mergeSort(arr);
printArray(arr);
}
}
小结:
一个排序的重要性,绝对不局限于它的功能,更重要的是理解它的思路。归并排序。其实就是让你理解分治这件事情。
在后面的文章中,我将写快排和堆排等排序算法,功能是占一边的,更重要的是他们实现的思路。 那怎么体现出,它的思路有用呢?
下面我们看两个具体题目:
3.小和问题
在一个数组中,每一个数左边比当前数小的数累加起来,叫做这个数组的小和。求一个数组的小和。
例子:
[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*N),复杂度太高,写对数器应该这样写。一个特别好想,特别容易实现的方法。
这个题目可以用归并排序的思路,做出题目。
拿例子说事:[1,3,4,2,5]
把这个数组平分成两份,由于是奇数,所以无所谓哪边多一个,少一个。
然后继续下分,看下图:
如上图所示,1先和3比较,有1个小和,4在和1,3比较,有两个小和,左边的数组就找出所有小和。同理可得出,右侧小和。(注意:如果在找某一次小和时,乱序,那么找完小和后,将这段数组顺序)此时两侧数组都是顺序的,设置两个指针,让指针分别指向两侧数组第一个元素,然后进行比较,如果右边指针指向的数字大于左侧指针指向的数字,那么就有右侧指针后面有多少元素+1个左侧指针指向的数字的小和。这样说有点抽象,还是这个题目,2>1,所以1是2的小和,也是2后面元素5的小和,然后左侧指针右移,循环往复,直到结束为止。
java代码(小河问题+对数器):
package basic_class_01;
public class Code_12_SmallSum {
public static int smallSum(int[] arr) {
if (arr == null || arr.length < 2) {
return 0;
}
return mergeSort(arr, 0, arr.length - 1);
}
public static int mergeSort(int[] arr, int l, int r) {
if (l == r) {
return 0;
}
int mid = l + ((r - l) >> 1);
return mergeSort(arr, l, mid) + mergeSort(arr, mid + 1, r) + merge(arr, l, mid, r);
}
public static int merge(int[] arr, int l, int m, int r) {
int[] help = new int[r - l + 1];
int i = 0;
int p1 = l;
int p2 = m + 1;
int res = 0;
while (p1 <= m && p2 <= r) {
res += arr[p1] < arr[p2] ? (r - p2 + 1) * arr[p1] : 0;
help[i++] = arr[p1] < arr[p2] ? arr[p1++] : arr[p2++];
}
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 res;
}
// for test
public static int comparator(int[] arr) {
if (arr == null || arr.length < 2) {
return 0;
}
int res = 0;
for (int i = 1; i < arr.length; i++) {
for (int j = 0; j < i; j++) {
res += arr[j] < arr[i] ? arr[j] : 0;
}
}
return res;
}
// for test
public static int[] generateRandomArray(int maxSize, int maxValue) {
int[] arr = new int[(int) ((maxSize + 1) * Math.random())];
for (int i = 0; i < arr.length; i++) {
arr[i] = (int) ((maxValue + 1) * Math.random()) - (int) (maxValue * Math.random());
}
return arr;
}
// for test
public static int[] copyArray(int[] arr) {
if (arr == null) {
return null;
}
int[] res = new int[arr.length];
for (int i = 0; i < arr.length; i++) {
res[i] = arr[i];
}
return res;
}
// for test
public static boolean isEqual(int[] arr1, int[] arr2) {
if ((arr1 == null && arr2 != null) || (arr1 != null && arr2 == null)) {
return false;
}
if (arr1 == null && arr2 == null) {
return true;
}
if (arr1.length != arr2.length) {
return false;
}
for (int i = 0; i < arr1.length; i++) {
if (arr1[i] != arr2[i]) {
return false;
}
}
return true;
}
// for test
public static void printArray(int[] arr) {
if (arr == null) {
return;
}
for (int i = 0; i < arr.length; i++) {
System.out.print(arr[i] + " ");
}
System.out.println();
}
// for test
public static void main(String[] args) {
int testTime = 500000;
int maxSize = 100;
int maxValue = 100;
boolean succeed = true;
for (int i = 0; i < testTime; i++) {
int[] arr1 = generateRandomArray(maxSize, maxValue);
int[] arr2 = copyArray(arr1);
if (smallSum(arr1) != comparator(arr2)) {
succeed = false;
printArray(arr1);
printArray(arr2);
break;
}
}
System.out.println(succeed ? "Nice!" : "Fucking fucked!");
}
}
4.逆序对问题
原理和归并排序,小和问题原理相同,不做做解释,上代码:
public class 逆序对问题 {
public static void main(String[] args) {
int[] arr = {
5,78,36,32,87,777,5};
s(arr,0,arr.length-1);
}
private static void s(int[] arr, int left, int right) {
if(left==right){
return;
}
int mid = (left+right)/2;
s(arr,left,mid);
s(arr,mid+1,right);
w(arr,left,mid,right);
}
private static void w(int[] arr, int left, int mid, int right) {
int[] help = new int[right-left+1];
int i=0;
int p1 = left;
int p2 = mid+1;
while (p1<=mid&&p2<=right){
int s = arr[p1]<arr[p2]?1:0;
int x = p2;
if(s==1){
//(right-p2+1)
for(int p=0;p<right-p2+1;p++){
System.out.println(arr[p1]+"和"+arr[x++]+"为逆序对");
}
}
help[i++] = arr[p1]>arr[p2]?arr[p2++]:arr[p1++];
}
while (p1<=mid){
help[i++] = arr[p1++];
}
while (p2<=right){
help[i++] = arr[p2++];
}
for(i=0;i<help.length;i++){
arr[i+left] = help[i];
}
}
}