查找算法、选择算法——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;     }}



分析
与上一个题相比,我们不仅需要记录元素的存在性,还需要记录元素的次数,我们只需将哈希表的value用来存储元素出现次数。
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;     }}
扩展
1、如果元素已经有序,我们能够如何优化算法呢?
我们可以对两个有序数组各自维护一个指针,初始化指向第一个元素。如果两个指针指向的元素值相等,那么将该元素添加到结果集中,否则将元素值较小的那个指针后移,直到某个指针达到数组末尾为止。
2、如果数组2的元素个数足够多,因为内存受限,只能存储在磁盘上,该怎么办呢?
很显然,对数组1建立哈希表,然后逐一处理数组2中的元素,这样数组2中的元素并不要一次性放入内存。
上面两个例子都是使用哈希查找,利用空间换取时间,达到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];    }      }}
方案二:二分查找
充分利用数组的有序性,每次过滤掉接近k/2个元素。
时间复杂度为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]结尾的最长上升子序列长度。
初始化:d[i]=1,每个元素构成一个序列
递推表达式:d[i]=MAX{d[i],d[j]+1},其中j<i且a[j]<=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();    }}

1 0