算法原型 -- 如何以o(nlogn^2)的方式来得到一个数组中的最长递增子序列长度的问题
问题 : 就是给你一串数组,让你来得到这个数组中的最长递增子序列长度,这个子序列中的数字不一定是要挨着的
思路 : 先讲如何以o(n^2)的时间复杂度来得到这个子序列。一般像这种要在数组中求什么什么条件的子序列的问题,都有一个通解,那就是先求某个位置下的什么什么条件的情况,比如这道题就是,我们可以求在原数组中i位置下的最长递增子序列长度,然后怎么求这个i位置下的最长递增子序列长度呢,你可以先试着求第一个和第二个位置下的递增子序列长度,用一个同等长度的辅助数组来装,然后第三个位置你就可以采用遍历原数组的值,若是比当前值要小,则当前的位置下的最长递增子序列的长度就等于遍历到的那个位置的子序列长度(储存在辅助数组中)加一,然后,再用个一层循环中的全局变量max来记录最大值,最后循环完了之后就可以得到最大的值了,这样外层循环是n,内层循环每次都要从头开始遍历到当前的位置,就是n-1,所以复杂度就是n^2。
上代码
public static int getTheMostLongSubArray_V1(int[] arr){ int res = 0; //最长的子序列的长度 int[] h = new int[arr.length]; //来一个辅助数组,典型的以空间换时间的做法 int max_i = 1; // 记录原数组下i位置的最大的递增长度 for(int i = 0;i<arr.length; ++i){ max_i = 1; h[i] = 1; for(int j=0; j<i; ++j){ if(arr[j] < arr[i]){ h[i] = h[j]+1; }else{ h[i] = 1; } max_i = Math.max(max_i, h[i]); } h[i] = max_i; } for(int i = 0; i<h.length; ++i){ res = Math.max(h[i], res); } return res; }
但是有一个比较巧妙的方式就是可以让我们的内层循环加速,当然外层的循环是跑不掉的,如何让内层循环加速呢?
这个就是比较牛的部分了,我整个过程来模拟一个用一个辅助数组来找到从0开始到某个位置的最小原数组的值,并且,要保证这个辅助数组一直是有序的,也就是,找到辅助数组i位置下的原数组的最小值,怎么理解这个想法呢,其实你想,最长的递增子序列在一个数组确定之后其实就是确定的,那其实我们可以做到把这个序列给找出来,怎么找,就得了解到这个序列的特点了,这个序列中的每一值应该都是当前位置下的最小的值,那我们就是要在依次遍历的过程中找到当前位置下的最小值,找到最小值,能够递增的可能性就比较大嘛,所以整个流程就是在外层依次遍历的情况下,用一个辅助的数组,分成有效区和无效区,有效区第一个位置默认为原数组的第一个值,后面就是无效区,然后从下标为1依次遍历原数组,然后用二分查找的方式找到辅助数组中第一个比当前遍历到的原数组的值大的值,两点注意的地方,我们内层循环,不再是依次遍历了,而是用的二分查找,因为辅助数组一直是一个递增的数组,其次要找到的是第一个比原数组大的值,然后找到之后就替换,这样就可以保证遍历原数组当前位置的值一定在辅助数组中是最小的那个了,但如果找不到,那就说明原数组的这个值比辅助数组中的值都要大,那就像无效区扩容。最后就能够找到最长的递增子序列的值了。
时间复杂度就是外层的n, 和内层中的用二分查找来找到第一个比当前原数组中的值大的值,logn,所以就是o(nlogn)
代码
// 第二种方法,首先如果我要用o(nlogn)的时间复杂度来求得最长递增子序列的话,应该在哪里做什么改变呢?外层循环n是跑不掉的,所以只能在内层的循环上加速 // 它主要是模拟了一个从第一个位置开始,每次找到第i位置的应该在的最小的值,也就是能够做到让这个辅助数组始终是有序的,而且是原数组中能够依次递增的,这样的话,就完全可以在内层的循环中使用二分查找,找到原数组中的第i位置的那个值 // 在辅助数组中的具体位置,最后就可以得到这个一次递增的最长子序列了 public static int[] getMostLongSuBArray_V2(int[] arr){ int[] h1 = new int[arr.length]; // 一个辅助的数组 int res = 0; // 最长的递增子序列值 // 二分查找 -- 由L和 R指针来确定第一个大于原数组当前的值 int L = 0; int R = 0; int right = 0; // 这个是有效区边界值 // 第一个值就自己定义为第一个值 h1[0] = arr[0]; for(int i = 1; i<arr.length; ++i){ // 每往下一个原数组的值进行比较的时候,都要再次重新整理左右边界的指针 L = 0; R = right; while(L <= R){ int m = (L+R)/2; if(arr[i] < h1[m]){ R = m-1; // 继续往左边找第一个大于arr[i]的值 }else{ L = m+1; // 继续往右边找第一个大于arr[i]的值 } } right = Math.max(right, L); h1[L] = arr[i]; } return h1; }
上面的算法原型已经看过了之后,就可以理解一下二元组如何来求这样的最大子序列问题,当然二元组的时候,参数变多了,就得有个先后顺序,但也是一个反自然的想法,主要的思想就是,假设这个二元组的两个参数a ,b ,先以a的大小升序排序,当在a一样的情况下,让b的大小来降序排序,注意是降序,最后,我们的辅助数组就是对b的值进行一个求最长子序列就可以了,所以为什么这么做是对的呢? 欸自己体会
hhhhhh,开玩笑的,其实就是首先对a的值进行升序排序,这个目的清楚吧,反正就是求两个二元组,a和b的值都要比前一个二元组大,才可以算子序列中的一组,所以,我们不妨先让a的值升序,然后在考虑b的值,b为什么要降序呢,因为还记得辅助数组的实质就是求辅助数组中第i位置上的最小的那个,如果b中的最小的这个都大于前面的值,那肯定就能摞上去
所以上香喷喷的代码吧
package arraySubOrderQuestionCollection; import java.util.Arrays; import java.util.Comparator; /* * question : 给定一个N*2的二维数组,看作是一个二维数组,例如[[a1,b1],[a2,b2],[a3,b3]],规定如果想把二元组甲放在二元组乙上,甲中的a值必须大于乙中的a值,甲中的 * b值必须大于乙中的b值,如果在二维数组中随意选择二元组,请问二元组最多可以网上摞多少个? * * example : [[5,4],[6,4],[6,7],[2,3]] ---> [2,3] ===> [5,4] ===> [6,7] 最多可以摞三层 * * required : 实现o(n*logn) * */ class gentleman{ int Property; int looking; public gentleman(int p, int l) { this.Property = p; this.looking = l; } @Override public String toString() { return "["+this.looking+","+this.Property+"]"; } } // 一个比较器 class menComperator implements Comparator<gentleman>{ @Override public int compare(gentleman o1, gentleman o2) { if(o1.looking == o2.looking){ if(o1.Property == o2.Property){ //如果两个值都是一样的话 return 0; }else if(o1.Property < o2.Property ){ // 第二参数值按照降序排序 return 1; // -1 代表前面的object放在前面 }else{ return -1; } }else if(o1.looking < o2.looking){ return -1; }else{ return 1; } } } public class MaxTwo_tuples { //我们把程序设定变得有意思些,现在我们要求的是寻找高富帅的过程,假如现在有两个参数,一个是财产,一个是颜值作为比较的基础,现在我们要求能够求出财产和颜值都很高的高富帅序列。就是票判定条件是如果一个高富帅在比另一个高富帅好的话 //那么这个高富帅的财产和颜值都要比另一个高富帅高 public static int getTheTwo_tuples(int[][] two_tuples){ gentleman[] gentlemen = new gentleman[two_tuples.length]; // 来存放二元组 for(int i = 0;i<gentlemen.length; ++i){ // 是要将two_tuples 的参数值放到gentlemen中 gentlemen[i] = new gentleman(two_tuples[i][1],two_tuples[i][0]); // 第一个是颜值, 第二个是properties } // 对二元组的gentleman进行一个排序 Arrays.sort(gentlemen, new menComperator()); for(int i=0; i<gentlemen.length; ++i){ System.out.println(gentlemen[i].toString()); } // 一样的需要一个辅助的加速数组 int[] helpSpeed = new int[gentlemen.length]; // 现在只对properties进行排序 // 在辅助数组的参数 int l = 0; int r = 0; int right = 0; helpSpeed[0] = gentlemen[0].Property; for(int i = 1; i<gentlemen.length; ++i){ // 记住每次找到最后一个大于当前值的数,后都要将辅助数组的左右和有效区进行更新 l = 0; r = right; // 二分查找最后一个大于当前的properties while(l <= r){ int mid = (l+r) / 2 ; if(gentlemen[i].Property > helpSpeed[mid]){ l = mid + 1; }else{ r = mid - 1; } } right = Math.max(right, l); helpSpeed[l] = gentlemen[i].Property; } return right+1; } public static void main(String[] args) { int[][] matrix = {{5,4},{6,4},{6,7},{2,3}}; System.out.println("the most number subArray"+getTheTwo_tuples(matrix)); } }
最后我想重点说两件事: 当如果不是很明显在考你排序的时候,一定要学会写比较器,特别是在多个参数合成的object进行比较时,第二件是对于二分查找,上面就是在查找,原数组的当前值,在辅助数组中第一个大于当前值的位置,然后你就可以进行替换了.还是比较简单的