查找算法
顺序查找
对于无序数组或者链表,我们只能利用顺序查找。平均时间复杂度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{//大顶堆,自定义
@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();
}
}