查找算法、选择算法——LeetCode
来源:互联网 发布:上海心动网络 校招 编辑:程序博客网 时间:2024/06/06 14:29
查找算法
顺序查找
对于无序数组或者链表,我们只能利用顺序查找。平均时间复杂度O(n)
二分查找
对于有序数组或者平衡二叉查找树、红黑树。平均时间复杂度O(log n)。
哈希查找
利用哈希函数映射实现O(1)时间复杂度的查找,是一种以空间换取时间的做法。平均时间复杂度O(1)。
索引查找
相当于一种多路查找技术。特别是对于大量有序数据的查找,需要将数据组织到磁盘上的时候比较适用,比如数据库索引。查找的时间消耗主要在于I/O,而I/O的时间消耗取决于索引的层数。
选择算法
最大值和最小值
一趟遍历,用两个变量实时记录最大值和最小值。时间复杂度O(n)
第K大或第K小(K-th),中位数
1、期望线性时间选择
如果我们对输入数据进行排序,那么基于比较的排序算法,时间复杂度的下界是O(n log n)。
除此之外,我们还可以利用分治的思想,基于快速排序中的划分算法。每次划分,我们就确定了第K大元素在哪一半中。平均情况下,每次划分问题规模减半,因此平均时间复杂为O(n),最坏时间复杂度为O(n log)。
2、最坏情况线性时间选择
首先,对于期望线性时间选择,导致最坏情况的原因就是划分不均匀,我们可以通过随机取样或中值取样来进行优化,但是仍然无法避免最坏情况的发生。
分组内部排序(从上到下递减),分组间按照中位数的中位数划分(不是排序)。那么左上角和右下角的元素我们就可以淘汰,问题规模就缩小了一半。平均、最坏时间复杂度为O(n)。
Top K
首先,我们可以使用求K-th的方法求解。此外,对于K比较小,数组元素比较多的情况(特别是对于海量数据的处理,不能一次装入内存),我们可以利用大顶堆或小顶堆维护当前Top K,并遍历所有元素,并实时更新Top K。
算法题
时间复杂度O(m+n),空间复杂度O(m+n)。
public class Solution { public int[] intersection(int[] nums1, int[] nums2) { HashSet<Integer> existSet=new HashSet<Integer>(); for(int i:nums1){ existSet.add(i); } HashSet<Integer> founded=new HashSet<Integer>(); for(int i:nums2){ if(existSet.contains(i)){ founded.add(i);//利用Set自动去重复 } } int[] res=new int[founded.size()]; int index=0; for(Integer i:founded){ res[index++]=i; }return res; }}
分析
public class Solution { public int[] intersect(int[] nums1, int[] nums2) { HashMap<Integer,Integer> countMap=new HashMap<Integer,Integer>();//统计出现次数 for(int i:nums1){ if(countMap.get(i)==null){ countMap.put(i, 1); }else{ countMap.put(i,1+countMap.get(i)); } } int size=0;//记录结果的个数 HashMap<Integer,Integer> exist=new HashMap<Integer,Integer>(); for(int i:nums2){ if(countMap.containsKey(i)){ int preCount=countMap.get(i); if(exist.get(i)==null){ exist.put(i, 1); size++; }else{ if(exist.get(i)<preCount){ exist.put(i, 1+exist.get(i)); size++; } } } } int[] res=new int[size]; int index=0; //生成结果集 for(java.util.Map.Entry<Integer, Integer> entry:exist.entrySet()){ for(int i=0;i<entry.getValue();i++){ res[index++]=entry.getKey(); } }return res; }}扩展
上面两个例子都是使用哈希查找,利用空间换取时间,达到O(1)的查找效率。
方案一:线程查找
我们首先计算出元素的总个数m+n,中位数的问题实质上就是kth的问题,最简单的我们可以使用线性查找,每次过滤掉一个元素。
时间复杂度为O(m+n),空间复杂度O(1)。
递归版
public class Solution { public double findMedianSortedArrays(int[] nums1, int[] nums2) { int m=nums1.length,n=nums2.length; int index=(m+n)/2;//第一个中位数的位置 if((m+n)%2==0){ double first=(double)findIndex(nums1,0,nums2,0,index); double second=(double)findIndex(nums1,0,nums2,0,index+1); return (first+second)/2; }else{ return (double)findIndex(nums1,0,nums2,0,index+1); } } private int findIndex(int[] nums1,int start1, int[] nums2,int start2,int index){ if(index==1){//出口 if(start1<nums1.length&&(start2==nums2.length||nums1[start1]<=nums2[start2])){ return nums1[start1]; }else{ return nums2[start2]; } } if(start1<nums1.length&&(start2==nums2.length||nums1[start1]<=nums2[start2])){ return findIndex(nums1,start1+1,nums2,start2,index-1); }else{ return findIndex(nums1,start1,nums2,start2+1,index-1); } }}迭代版
public class Solution { public double findMedianSortedArrays(int[] nums1, int[] nums2) { int m=nums1.length,n=nums2.length; int index=(m+n)/2;//第一个中位数的位置 if((m+n)%2==0){ double first=(double)findIndex(nums1,0,nums2,0,index); double second=(double)findIndex(nums1,0,nums2,0,index+1); return (first+second)/2; }else{ return (double)findIndex(nums1,0,nums2,0,index+1); } } private int findIndex(int[] nums1,int start1, int[] nums2,int start2,int index){ int p=start1,q=start2; while(p!=nums1.length&&q!=nums2.length&&index>1){//两个数组都有数据处理 if(nums1[p]<=nums2[q]){ p++; }else{ q++; } index--; } if(q==nums2.length||(p!=nums1.length&&nums1[p]<nums2[q])){ return nums1[p+index-1]; }else{ return nums2[q+index-1]; } }}方案二:二分查找
时间复杂度为O(log(m+n)),空间复杂度为O(1)。
递归版
public class Solution { public double findMedianSortedArrays(int[] nums1, int[] nums2) { int m=nums1.length,n=nums2.length; int index=(m+n)/2; if((m+n)%2==0){ double first=(double)findIndex(nums1,0,nums2,0,index); double second=(double)findIndex(nums1,0,nums2,0,index+1); return (first+second)/2; }else{ return (double)findIndex(nums1,0,nums2,0,index+1); } } private int findIndex(int[] nums1,int start1, int[] nums2,int start2,int index){ if(index==1){//出口 if(start1<nums1.length&&(start2==nums2.length||nums1[start1]<=nums2[start2])){ return nums1[start1]; }else{ return nums2[start2]; } } if(nums1.length-start1>nums2.length-start2){//保证前面数组的短,简化处理 int[] nt=nums1;nums1=nums2;nums2=nt; int it=start1;start1=start2;start2=it; } if(start1==nums1.length){//第一个数组为空 return nums2[start2+index-1]; } int firstIndex=Math.min(start1+index/2-1, nums1.length-1);//每次考虑过滤掉大约k/2个元素,并考虑数组越界的情况 int secondIndex=start2+(firstIndex-start1+1)-1; if(nums1[firstIndex]<=nums2[secondIndex]){ return findIndex(nums1,firstIndex+1,nums2,start2,index-(firstIndex-start1+1)); }else{ return findIndex(nums1,start1,nums2,secondIndex+1,index-(secondIndex-start2+1)); } }}跌代版:略
分析
基本思路:我们用减法来累积倍数,为了提高效率,每次用被除数减去除数的“最大的2的指数倍数”(可以利用位移操作实现)。
注:被除数和除数为Integer.MIN_VALUE的特殊情况需要单独处理,防止溢出。
public class Solution {private int getCount(int dividend, int divisor){//传入正整数 int maxpow=-1;//最大2的指数倍数 int temp=divisor; while(temp>0&&temp<=dividend){ maxpow++;; temp<<=1; } int count=0;//倍数 int pow=maxpow; while(dividend>=divisor){ if(dividend>=divisor<<pow){ count+=1<<pow; dividend-=divisor<<pow; } pow--; }return count; } public int divide(int dividend, int divisor) { int sign=((dividend>0&&divisor>0)||(dividend<0&&divisor<0))?1:-1;//结果的符号 if(divisor==Integer.MIN_VALUE){ return dividend==Integer.MIN_VALUE?1:0; } if(dividend==Integer.MIN_VALUE&&divisor==-1){ return Integer.MAX_VALUE;//溢出 } int count=0; divisor=Math.abs(divisor); if(dividend==Integer.MIN_VALUE){//对于Integer.MIN_VALUE特殊处理 dividend+=divisor; count=1; } //先求绝对值 dividend=Math.abs(dividend); count+=getCount(dividend,divisor);return count*sign; }}
为了简化边界处理,我们可以先将输入转化为long,求出结果后再转化为int。
public class Solution {private long getCount(long dividend, long divisor){//传入正整数 long maxpow=-1;//最大2的指数倍long temp=divisor; while(temp>0&&temp<=dividend){ maxpow++;; temp<<=1; } long count=0;//倍数 long pow=maxpow; while(dividend>=divisor){ if(dividend>=divisor<<pow){ count+=(long)1<<pow;//注意这里的强制转换 dividend-=divisor<<pow; } pow--; }return count; } public int divide(int dividend, int divisor) { long sign=((dividend>0&&divisor>0)||(dividend<0&&divisor<0))?1:-1;//结果的符号 long dividendLong=dividend>=0?dividend:(-1)*(long)dividend;//注意这里的强制转换 long divisorLong=divisor>=0?divisor:(-1)*(long)divisor;//注意这里的强制转换 if(dividendLong<divisorLong) return 0; long count=getCount(dividendLong,divisorLong); count*=sign; if(count>Integer.MAX_VALUE)count=Integer.MAX_VALUE;return (int) (count); }}
分析
充分利用元素的有序性,采用二分查找算法,逐渐缩小查找范围,因为存在旋转,我们需要全面考虑如何缩小查找范围。
我们可以分析在[3,4,5,6,7,1,2]中查找1、4和在[6,7,1,2,3,4,5]中查找1、4的区别,探索如何去缩小查找范围。
public class Solution { public int search(int[] nums, int target) { int begin=0,end=nums.length-1; while(begin<=end){ if(begin==end) return nums[begin]==target?begin:-1; int mid=begin+(end-begin)/2; if(nums[mid]==target){ return mid; }else{ if(nums[mid]>=nums[begin]){//左边递增,考虑begin==mid的情况 if(target>nums[mid]){//目标值比中间值大,只可能在右边 begin=mid+1; }else{//两边都有可能 if(nums[begin]<=target){//与起点比较 end=mid-1; }else{ begin=mid+1; } } }else{//右边递增 if(target>nums[mid]){//目标值比中间值大,两边都有可能 if(nums[end]>=target){//与终点比较 begin=mid+1; }else{ end=mid-1; } }else{//目标值比中间值小,只可能在左边 end=mid-1; } } } }return -1; }}
如果我们按照常规的二分查找思路,先找到元素出现的位置,然后往两边线性扩展,平均时间复杂度为O(log n),但是极端情况下,例如:A=[1,2,2,2,.....,2,2,2,3],target=2时,算法复杂度为O(n),显然不符合要求。
因此,我们对常规的二分查找算法稍做调整,首先找到target第一次出现的位置,再找到target最后一次出现的位置。
这样,任意输入,最坏时间复杂度为O(log n),满足题目要求。
public class Solution {public int searchLeft(int[] nums, int target){//查找第一次出现的位置int begin=0,end=nums.length-1;while(begin<=end){if(begin==end) return nums[begin]==target?begin:-1;int mid=begin+(end-begin)/2;if(nums[mid]==target){if(mid==begin){return mid;}else{if(nums[mid-1]==target){end=mid-1;}else{return mid;}}}else{if(nums[mid]<target){begin=mid+1;}else{end=mid-1;}}}return -1; }public int searchRight(int[] nums, int target){//查找最后一次出现的位置int begin=0,end=nums.length-1;while(begin<=end){if(begin==end) return nums[begin]==target?begin:-1;int mid=begin+(end-begin)/2;if(nums[mid]==target){if(mid==end){return mid;}else{if(nums[mid+1]==target){begin=mid+1;}else{return mid;}}}else{if(nums[mid]<target){begin=mid+1;}else{end=mid-1;}}}return -1; } public int[] searchRange(int[] nums, int target) { int[] res=new int[2]; int left=searchLeft(nums,target); if(left==-1){ res[0]=-1; res[1]=-1; }else{ res[0]=left; res[1]=searchRight(nums,target); }return res; }}
分析
依然是二分查找。
public class Solution { public int searchInsert(int[] nums, int target) {int begin=0,end=nums.length-1;while(begin<=end){int mid=begin+(end-begin)/2;if(nums[mid]==target){return mid;}else{if(nums[mid]<target){begin=mid+1;}else{end=mid-1;}}}return begin; }}
暴力算法的复杂度为O(n),显然我们可以进一步优化,与LeetCode 29思路类似。我们不需要一次只乘以一个x,我们一次可以乘以x的4次方、8次方等等。
如果我们采用分治算法,并没有减少做乘法的次数,因为涉及到很多重复的计算。我们可以利用动态规划的思想,记录中间结果,避免重复计算。
注:对于x=0,x为负数,n为0,n为负数的特殊情况应该做全面考虑。
public class Solution { public double myPow(double x, int n) { if(x==0)return 0.0; if(n==0)return 1.0; boolean reverse=n>0?false:true; long ln=n>0?n:(long)-1*(long)n;//处理n=Integer.MIN_VALUE的情况 double res=1.0; double[] values=new double[33];//先求所以2的指数倍 values[0]=1.0; values[1]=x; for(int i=2;i<33;i++){ values[i]=values[i-1]*values[i-1]; } for(int i=31;i>=0;i--){ if((((long)1<<i)&ln)!=0){ res*=values[i+1]; } } if(reverse){//倒置 res=1.0/res; } return res; }}
分析
依然采用二分查找,我们可以把二维数组看做是一维的,只是取值时需要将下标再转换为二维。
public class Solution { public boolean searchMatrix(int[][] matrix, int target) { if(matrix.length==0||matrix[0].length==0)return false; int m=matrix.length,n=matrix[0].length; int begin=0,end=m*n-1; while(begin<=end){ int mid=begin+(end-begin)/2; int row=mid/n,col=mid%n;//将下标转换为二维 if(matrix[row][col]==target){ return true; }else{ if(matrix[row][col]<target){//目标元素在后面 begin=(mid+1); }else{ end=mid-1; } } }return false; }}
分析
如果旋转排序数组中的元素允许重复,那么如何去缩小查询范围呢?
例如在[1,1,1,2,0,1,1,1,1,1,1,1,1,1]和[1,1,1,1,1,1,1,1,1,2,0,1,1,1]分别查找2,对于这样的特殊情况,我们不能通过边界值来缩小范围,因此只能都考虑。虽然说最坏时间复杂度为O(n),但是平均时间复杂度仍然是O(log (n))。
public class Solution {private boolean doSearch(int[] nums,int target,int begin,int end){if(begin>end)return false;int mid=begin+(end-begin)/2;if(nums[mid]==target){return true;}else{if(nums[mid]>nums[begin]){//左边递增if(target>nums[mid]){//目标值比中间值大,只可能在右边return doSearch(nums,target,mid+1,end);}else{//两边都有可能if(nums[begin]<=target){//与起点比较return doSearch(nums,target,begin,mid-1); }else{return doSearch(nums,target,mid+1,end); }}}else{if(nums[mid]<nums[begin]){//右边递增 if(target>nums[mid]){//目标值比中间值大,两边都有可能 if(nums[end]>=target){//与终点比较 return doSearch(nums,target,mid+1,end); }else{ return doSearch(nums,target,begin,mid-1); } }else{//目标值比中间值小,只可能在左边 return doSearch(nums,target,begin,mid-1); }}else{//nums[mid]==nums[begin],都有可能return doSearch(nums,target,begin,mid-1)||doSearch(nums,target,mid+1,end); }}} }public boolean search(int[] nums, int target) {return doSearch(nums,target,0,nums.length-1);}}
分析
依然是二分查找,逐渐缩小查找范围。
public class Solution { public int findMin(int[] nums) { int begin=0,end=nums.length-1; while(true){ if(begin==end){ return nums[begin]; } if(begin+1==end){ return Math.min(nums[begin], nums[end]); }//长度>=3,便于边界处理 int mid=begin+(end-begin)/2; if(nums[mid]>nums[begin]){//左边递增 if(nums[end]>nums[mid]){//右边也递增 return nums[begin]; }else{//右边不是递增,最小值在右边 begin=mid+1; } }else{//右侧递增,最小值可能是当前元素,也可能在左边 if(nums[mid-1]>nums[mid]){ return nums[mid]; }else{ end=mid-1; } } } }}
分析
对特殊情况进行特殊处理。最坏时间复杂度为O(n),平均时间复杂度为O(log(n))。
public class Solution {private int doFindMin(int[] nums,int begin,int end){if(begin>end)return Integer.MAX_VALUE;if(begin==end)return nums[begin];if(begin+1==end) return Math.min(nums[begin], nums[end]);int mid=begin+(end-begin)/2;if(nums[mid]>nums[begin]){//左边递增return Math.min(nums[begin], doFindMin(nums,mid+1,end));}else{if(nums[mid]<nums[begin]){//右边递增return Math.min(nums[mid], doFindMin(nums,begin,mid-1));}else{//都有可能return Math.min(doFindMin(nums,mid+1,end), doFindMin(nums,begin,mid-1));}} } public int findMin(int[] nums) { return doFindMin(nums,0,nums.length-1); }}
分析
对边界情况特殊处理后,剩下的只是线性查找了。
public class Solution { public int findPeakElement(int[] nums) { int n=nums.length; if(nums.length==1)return 0; if(nums[0]>nums[1])return 0; if(nums[n-1]>nums[n-2])return n-1; for(int i=1;i<n-1;i++){ if(nums[i]>nums[i-1]&&nums[i]>nums[i+1]){ return i; } } return 0; }}
分析
经典的两个指针前后夹逼,逐渐缩小查找范围。时间复杂度O(n)。
public class Solution { public int[] twoSum(int[] numbers, int target) { int begin=0,end=numbers.length-1;//两个指针夹逼 int[] res=new int[2]; while(true){ if(numbers[begin]+numbers[end]==target){ res[0]=begin+1; res[1]=end+1; break; }else{ if(numbers[begin]+numbers[end]>target){ end--; }else{ begin++; } } }return res; }}
分析
方案一
类似求最长连续字段和的思路。记录前后两个指针,不断调整后移指针,从而遍历到所有的情况。时间复杂度O(n^2)。
public class Solution { public int minSubArrayLen(int s, int[] nums) { boolean finded=false; int minSize=Integer.MAX_VALUE; int begin=0,sum=0;//sum表示以nums[i]结尾的和,并始终保持sum<s且“最接近s” for(int i=0;i<nums.length;i++){ sum+=nums[i]; if(sum>=s){ finded=true; while(sum>=s){//更换起点 sum-=nums[begin++]; } minSize=Math.min(minSize, i-begin+1+1); } } if(!finded)return 0;return minSize; }}运行超时。我们在更新begin时,我们每次只能更新一个位置,这样导致时间复杂度为O(n^2)。我们该如何优化呢?我们可以以空间换取时间,我们首先计算所有前n项和,这样在更新begin时,就可以利用二分查找来优化begin的更新,降低时间复杂度。
方案二
有了前面的基本思路之后,首先得到所有前n项和,问题就转换为:寻找满足数据之差大于等于给定值s的最小下标距离。
遍历起始位置i,由于元素都为正数,前n项和递增,寻找合适的end时就可以采用二分查找算法O(log n),这样的end至多只有一个,这样就降低了时间复杂度为O(n log(n))。
public class Solution { public int minSubArrayLen(int s, int[] nums) { int[] sums = new int[nums.length + 1]; for (int i = 1; i < sums.length; i++) sums[i] = sums[i - 1] + nums[i - 1]; int minLen = Integer.MAX_VALUE; for (int i = 0; i < sums.length; i++) { int end = binarySearch(i + 1, sums.length - 1, sums[i] + s, sums);//将问题稍做转换 if (end == sums.length) break; if (end - i < minLen) minLen = end - i; } return minLen == Integer.MAX_VALUE ? 0 : minLen; } private int binarySearch(int lo, int hi, int key, int[] sums) { while (lo <= hi) { int mid = (lo + hi) / 2; if (sums[mid] >= key){ hi = mid - 1; } else { lo = mid + 1; } } return lo; }}
分析
如果采用前序、中序和后序的方式遍历数组,显然时间复杂度都为O(log(n)),但是这显然没有充分利用完全二叉树的特性。
采用分治算法,每次将问题规模缩小一半。每次划分的时间复杂度为O(log(n)),共log(n)次划分,时间复杂度为O(log(n))。
public class Solution {private static int completeHeight(TreeNode root){//求完全二叉树的高度int height=0;TreeNode p=root;while(p!=null){height++;p=p.left;}return height;} public int countNodes(TreeNode root) { if(root==null)return 0; int leftHeight=completeHeight(root.left); int rightHeight=completeHeight(root.right); if(leftHeight==rightHeight){//两边一样高,说明左边一个都不缺(满树) return countNodes(root.right)+1+((1<<leftHeight)-1); }else{ return countNodes(root.left)+1+((1<<rightHeight)-1); } }}
分析
方案一
二叉查找树的中序遍历是递增的,我们可以依靠这个特性查找第k小的元素。时间复杂度O(k)。
public class Solution { public int kthSmallest(TreeNode root, int k) { TreeNode p=root; int count=0; Stack<TreeNode> stack=new Stack<TreeNode>(); while(p!=null||!stack.isEmpty()){ while(p!=null){ stack.push(p); p=p.left; } p=stack.pop(); count++; if(count==k)return p.val; p=p.right; }return 0; }}方案二
public class Solution { public int kthSmallest(TreeNode root, int k) { int count = countNodes(root.left); if (k <= count) { return kthSmallest(root.left, k); } else if (k > count + 1) { return kthSmallest(root.right, k-1-count); // 1 is counted as current node } return root.val; } public int countNodes(TreeNode n) { if (n == null) return 0; return 1 + countNodes(n.left) + countNodes(n.right); }}扩展
如果,二叉查找树中有频繁的增删改操作,并且我们需要频繁的进行第k小元素的查找,该如何优化呢?
方案二虽然效率地下,每次都需要计算左右子树节点的个数,但是却给我们启发。如果我们在节点中实时维护左右子树节点个数,那么时间复杂度将降低为O(log(n)),为了使得子树节点个数的变化能够方便快速的反映到上层树,我们可以维护指向父节点的指针。
分析
因为每行、每列都有序,我们应该充分利用元素的有序性。因为数据不具有全局有序性,显然在整个数组上进行二分查找不可行。
那么,在每个行上(或者列上)进行二分查找呢,时间复杂度为O(n log(m))或者O(m log(n))。但是,这样显然没有充分利用元素在列上(或者行上)的有序性。
我们可以先分析一下示例。
1、如果从左上角开始往右下角查找,小于1的元素不存在,大于1的范围为剩余的所有元素,我们将问题划分为从2和4开始往右下角查找,显然,从2和4开始的搜索空间有重叠,并且有大量重叠,问题规模并没有缩小,复杂度依然很高。
2、如果从右下角开始往左上角查找,情况与上面类似。
3、如果从右上角开始往左下角查找,小于15的元素只可能在左边列,大于15的元素只可能在下边行,因此每次搜索问题规模会缩小(缩小规模与n相关),最多只需要比较m+n次就可以结束搜索。算法复杂度为O(m+n)
4、如果从左下角开始往右上角查找,情况与上面类似。
public class Solution { public boolean searchMatrix(int[][] matrix, int target) { if(matrix.length==0||matrix[0].length==0)return false; int m=matrix.length,n=matrix[0].length; int row=0,col=n-1;//从右上角开始搜索 while(row<=m-1&&col>=0){ if(matrix[row][col]==target){ return true; }else{ if(matrix[row][col]<target){ row++; }else{ col--; } } }return false; }}
分析
方案一
如果我们允许额外的存储空间,显然可以利用长度为n的数组(哈希表)记录元素出现的次数,时间复杂度和空间复杂度都为O(n)。
方案二
此外,如果可以修改数组的话,我们将元素 i 放置在下标 i 处,这样当遇到元素 k,如果元素 k 的下标不是 k 且下标为 k 的位置已经存放了元素 k,那么元素 k 即为所求,时间复杂度为O(n)。
public class Solution { public int findDuplicate(int[] nums) { int n=nums.length-1; for(int i=1;i<=n;i++){ //将nums[i]放置在下标为nums[i]处 while(nums[i]!=i){//如果当前下标i不是存放的元素i,交换处理 int index=nums[i]; if(nums[index]==nums[i]){//nums[i]已经存放了nums[i],即发现重复 return nums[i]; } int t=nums[i];nums[i]=nums[index];nums[index]=t; } } return nums[0];//如果下标1-n恰好放置完成,元素nums[0]必定是那个重复的元素 }}但是,我们这里数组只读,并且要求空间复杂度为O(1),因此上述方法不可行。
扩展
此方法还可以求解查找第一个缺失的整数。例如[3,0,233,233,1,-8,4,5,2,2222,-1],第一个缺失的整数为6。
方案三
如果我们采用暴力破解,两两比较,那么时间复杂度为O(n^2),显然也不满足题目要求。
方案四
初始化元素的范围为[1,n],我们计算中间值mid,然后分别统计[1,mid-1]、[mid,mid]、[mid+1,n]的元素个数,smaller、count、bigger。如果count>1直接返回mid,如果smaller>mid-1那么重复元素在[1,mid]中,否则重复元素在[mid+1,n]中。
总共log(n)次处理,每次处理耗时O(n)。时间复杂度为O(n log(n)),空间复杂度为O(1),且没有修改数组,满足。
public class Solution { public int findDuplicate(int[] nums) { int n=nums.length-1; int begin=1,end=n; while(true){ int mid=begin+(end-begin)/2; int smaller=0,count=0,bigger=0; for(int i=0;i<=n;i++){ if(nums[i]==mid){ count++; }else{ if(nums[i]>=begin&&nums[i]<mid){ smaller++; }else{ if(nums[i]>mid&&nums[i]<=end){ bigger++; } //忽略 } } } if(count>1){ return mid; }else{ if(smaller>mid-1-begin+1){ end=mid-1; }else{ begin=mid+1; } } } }}
方案五
O(n)的算法:https://discuss.leetcode.com/topic/25913/my-easy-understood-solution-with-o-n-time-and-o-1-space-without-modifying-the-array-with-clear-explanation
扩展
如果只有一个元素重复,且仅仅重复一次呢?那就说明,其余元素都出现一次。我们就可以利用异或操作,算法如下:
public class Solution { public int findDuplicate(int[] nums) { int res=0; int n=nums.length-1; for(int i=0;i<=n;i++){ res=(res^i); res=(res^nums[i]); }return res; }}
分析
定义:d[i]表示以a[i]结尾的最长上升子序列长度。
public class Solution {public int lengthOfLIS(int[] nums) {if (nums.length == 0)return 0;return maxLengthIncreasing(nums).size();}public List<Integer> maxLengthIncreasing(int[] a) {int[] d = new int[a.length];// 初始化for (int i = 0; i < d.length; i++)d[i] = 1;// 迭代for (int i = 0; i < a.length; i++) {for (int j = 0; j < i; j++) {if (a[j] < a[i]) {d[i] = Math.max(d[i], d[j] + 1);}}}// 遍历迭代结果int maxIndex = 0;for (int i = 0; i < d.length; i++) {if (d[i] > d[maxIndex]) {maxIndex = i;}}// 解析结果List<Integer> res = new ArrayList<Integer>();res.add(a[maxIndex]);int nextIndex = maxIndex;for (int i = maxIndex - 1; i >= 0; i--) {if (d[i] + 1 == d[nextIndex] && a[i] < a[nextIndex]) {res.add(a[i]);nextIndex = i;}}Collections.reverse(res);return res;}}
分析
二分查找。
public class Solution { public boolean isPerfectSquare(int num) { if(num==1)return true; int begin=1,end=num/2; while(begin<=end){ int mid=begin+(end-begin)/2; if(mid*mid==num){ return true; }else{ if(mid*mid<0||mid*mid>num){ end=mid-1; }else{ begin=mid+1; } } }return false; }}
K-th和Top K
分析
利用大顶堆求解kth问题。显然不是最优解,没有充分利用元素的有序性。如有清晰的思路还望指教!!
public class Solution {private static class MyComparator implements Comparator{//大顶堆,自定义@Overridepublic int compare(Object o1, Object o2) { Integer first=(Integer)o1; Integer second=(Integer)o2;return -1*first.compareTo(second);} } public int kthSmallest(int[][] matrix, int k) { int n = matrix.length; PriorityQueue<Integer> pq = new PriorityQueue<Integer>(k,new MyComparator() ); for(int i= 0; i < n*n; i++) { int row=i/n,col=i%n; if(pq.size()<k){ pq.add(matrix[row][col]); }else{ int top=pq.peek(); if(matrix[row][col]<top){ pq.poll(); pq.add(matrix[row][col]); } } } return pq.poll().intValue(); }}
分析
方案一
先对数组直接排序,排序后直接取结果。
空间复杂度IO(1),时间复杂度O(n log(n)),会修改数组。
public class Solution { public int findKthLargest(int[] nums, int k) { final int N = nums.length; Arrays.sort(nums); return nums[N - k]; }}方案二
用一个大顶堆保存前k小元素,遍历数组并实时更新堆,最终堆顶元素即为所求。
空间复杂度O(k),时间复杂度O(n log(k)),不会修改数组。
public class Solution {public int findKthLargest(int[] nums, int k) { final PriorityQueue<Integer> pq = new PriorityQueue<>(); for(int val : nums) { pq.offer(val); if(pq.size() > k) { pq.poll(); } } return pq.peek();}}方案三
利用快速排序的划分算法,逐渐缩小查找范围。
空间复杂度为O(1),平均时间复杂度为O(n),最坏时间复杂度为O(n^2),会修改数组。
public class Solution {public int findKthLargest(int[] nums, int k) { k = nums.length - k; int lo = 0; int hi = nums.length - 1; while (lo < hi) { final int j = partition(nums, lo, hi); if(j < k) { lo = j + 1; } else if (j > k) { hi = j - 1; } else { break; } } return nums[k]; } private int partition(int[] a, int lo, int hi) { int i = lo; int j = hi + 1; while(true) { while(i < hi && less(a[++i], a[lo])); while(j > lo && less(a[lo], a[--j])); if(i >= j) { break; } exch(a, i, j); } exch(a, lo, j); return j; } private void exch(int[] a, int i, int j) { final int tmp = a[i]; a[i] = a[j]; a[j] = tmp; } private boolean less(int v, int w) { return v < w; }}
分析
方案一
先利用HashMap统计各个元素出现的频率,然后利用大顶堆维护top k的频率,遍历所有元素及其频率并实时更新小顶堆。最后大顶堆中的结果即为所求。
时间复杂度O(n)+O(n log(k)),空间复杂度O(n)+O(log( k)),不会修改数组。
public class Solution {private static class MyComparator implements Comparator{@Overridepublic int compare(Object o1, Object o2) { Map.Entry<Integer, Integer> first=(Map.Entry<Integer, Integer>)o1;Map.Entry<Integer, Integer> second=(Map.Entry<Integer, Integer>)o2;return -1*first.getValue().compareTo(second.getValue());}}public List<Integer> topKFrequent(int[] nums, int k) {//统计频率Map<Integer, Integer> countMap = new HashMap<>();for (int n : nums) {if (countMap.containsKey(n)) {countMap.put(n, countMap.get(n) + 1);} else {countMap.put(n, 1);}}PriorityQueue<Map.Entry<Integer, Integer>> pq = new PriorityQueue<Map.Entry<Integer, Integer>>(countMap.size(),new MyComparator());pq.addAll(countMap.entrySet()); List<Integer> ret = new ArrayList<>();for (int i = 0; i < k; i++) {ret.add(pq.poll().getKey());}return ret;}}方案二
当我们统计完频率后,我们可以不利用堆来寻找Top k,因为频率的范围在限定范围内,我们可以采用桶排序的思想寻找Top K。
时间复杂度O(n),空间复杂度O(n),不修改数组。
public class Solution {public List<Integer> topKFrequent(int[] nums, int k) {List<Integer>[] bucket = new List[nums.length + 1];Map<Integer, Integer> frequencyMap = new HashMap<Integer, Integer>();for (int n : nums) {if (frequencyMap.containsKey(n)) {frequencyMap.put(n, frequencyMap.get(n) + 1);} else {frequencyMap.put(n, 1);}} for (int key : frequencyMap.keySet()) {int frequency = frequencyMap.get(key);if (bucket[frequency] == null) {bucket[frequency] = new ArrayList<>();}bucket[frequency].add(key);}List<Integer> res = new ArrayList<>();//这里没有使用堆来求Top K,而是利用桶排序的思想,因为频率的限定范围内。for (int pos = bucket.length - 1; pos >= 0 && res.size() < k; pos--) {if (bucket[pos] != null) {res.addAll(bucket[pos]);}}return res;}}
分析
直接利用大顶堆。
public class Solution {private static class MyComparator implements Comparator{@Overridepublic int compare(Object o1, Object o2) { Integer first=(Integer)o1; Integer second=(Integer)o2;return -1*first.compareTo(second);} } public int kthSmallest(int[][] matrix, int k) { int n = matrix.length; PriorityQueue<Integer> pq = new PriorityQueue<Integer>(k,new MyComparator() ); for(int i= 0; i < n*n; i++) { int row=i/n,col=i%n; if(pq.size()<k){ pq.add(matrix[row][col]); }else{ int top=pq.peek(); if(matrix[row][col]<top){ pq.poll(); pq.add(matrix[row][col]); } } } return pq.poll().intValue(); }}
- 查找算法、选择算法——LeetCode
- 查找算法---快速选择算法
- 选择查找算法
- Leetcode二分查找算法
- 冒泡,选择,顺序查找 算法
- 查找算法·快速选择
- java选择算法、java查找算法汇总
- 查找算法——javascript算法实现
- 算法——二分查找算法
- 算法之—二分查找算法
- 排序算法浅析——选择算法
- 查找算法之——二分查找
- 查找算法——二叉树查找
- C查找算法——二分查找
- c查找算法——斐波拉契查找
- 算法——查找之二分查找
- 查找算法——顺序查找
- 查找算法——折半查找
- Service Intent must be explicit: Intent
- \n 刷新缓冲区问题
- C#学习
- 【EF映射】EF原理及延迟加载
- Codevs 1536 海战 (DFS || BFS)+判断
- 查找算法、选择算法——LeetCode
- String,StringBuffer与StringBuilder的区别|线程安全与线程不安全
- 内存的五大分区
- sizeof()
- centos7 oracle11g吐血安装
- TimeWalker游戏开发过程中的问题,经验与反思
- php之验证码
- Hust oj 1388 Knight(BFS)
- hud 2084 数塔 dp