查找算法、选择算法——LeetCode

查找算法

顺序查找
对于无序数组或者链表,我们只能利用顺序查找。平均时间复杂度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{//大顶堆,自定义
		@Override
		public 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{
		@Override
		public 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{
		@Override
		public 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
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值