LeetCode 剑指Offer 40.最小的K个数 堆,快排思想变形/easy


1.Description

输入整数数组 arr ,找出其中最小的 k 个数。例如,输入4、5、1、6、2、7、3、8这8个数字,则最小的4个数字是1、2、3、4。


2.Example

示例 1:
输入:arr = [3,2,1], k = 2
输出:[1,2] 或者 [2,1]


示例 2:
输入:arr = [0,1,2,1], k = 1
输出:[0]

3.Solution

1.堆

找出最小的k个数可以使用堆,建立一个大小为k的堆,遍历数组如果数组中的一个元素小于堆中最大的元素,就让这个元素进来,并移除堆中最大的元素,这样遍历完后就能得到最小的k个数(保存在堆中),最后再将堆中的元素输出到数组中。

class Solution {
    
    
    public int[] getLeastNumbers(int[] arr, int k) {
    
    
        if(k==0||arr.length==0){
    
    
            return new int[0];
        }
        
        // 使用一个最大堆(大顶堆)
        // Java 的 PriorityQueue 默认是小顶堆,添加 comparator 参数使其变成最大堆
        Queue<Integer> heap = new PriorityQueue<>(k, (i1, i2) -> Integer.compare(i2, i1));
        
        for(int i=0;i<arr.length;i++) {
    
    
        	// 当前数字小于堆顶元素才会入堆
        	//这里用isEmpty是因为peek方法在堆是空的时候无法返回
        	if(heap.isEmpty()||heap.size()<k||arr[i]<heap.peek()) {
    
    
        		//offer和add的区别:
        		heap.offer(arr[i]);
        	}
        	if(heap.size()>k) {
    
    
        		heap.poll();// 删除堆顶最大元素
        	}
        }
        
        // 将堆中的元素存入数组
        int[] res = new int[k];
        int j = 0;
	    //使用迭代器获得堆中的元素
	    for (int e : heap) {
    
    
	        res[j++] = e;
        }
        return res;
    }
}

空间复杂度:O(k) 使用了一个大小为k的堆
时间复杂度:O(nlogk)出入堆时间复杂度都是O(logk),每一个元素一次一共n次

2.快排变形

快速排序的过程是将小于基准数的数都放在基准数左边,大于基准数的数都放在基准数右边,然后对左边右边分别递归进行快排。
而解这道题的思想类似于快排:先将小于/大于基准数的数分别放到基准数的左/右边,
在这里插入图片描述
如果基准数的下标就是k的话,说明基准数左侧的那些数就是最小的k个数了;
如果基准数下标<k,说明左边的数都在最小的k个数中,剩下的数在当前基准数坐标的右边,需要递归地对右边的数再进行快速选择;
如果基准数下标>k,说明左边的数多于k个了,需要递归地对左边的数再进行快速选择;

public int[] getLeastNumbers(int[] arr, int k) {
    
    
    if (k == 0) {
    
    
        return new int[0];
    } else if (arr.length <= k) {
    
    
        return arr;
    }
    
    // 原地不断划分数组
    partitionArray(arr, 0, arr.length - 1, k);
    
    // 数组的前 k 个数此时就是最小的 k 个数,将其存入结果
    int[] res = new int[k];
    for (int i = 0; i < k; i++) {
    
    
        res[i] = arr[i];
    }
    return res;
}

void partitionArray(int[] arr, int lo, int hi, int k) {
    
    
    // 做一次 partition 操作
    int m = partition(arr, lo, hi);
    // 此时数组前 m 个数,就是最小的 m 个数
    if (k == m) {
    
    
        // 正好找到最小的 k(m) 个数
        return;
    } else if (k < m) {
    
    
        // 最小的 k 个数一定在前 m 个数中,递归划分
        partitionArray(arr, lo, m-1, k);
    } else {
    
    
        // 在右侧数组中寻找最小的 k-m 个数
        partitionArray(arr, m+1, hi, k);
    }
}

// partition 函数和快速排序中相同,具体可参考快速排序相关的资料
    public int partition(int[] arr,int lo,int hi) {
    
    
    	 int i=lo;
    	 int j=hi;
    	 int par = arr[lo];
    	 while(i!=j) {
    
    
    		 while(arr[j]>=par&&i<j) {
    
    
    			 j--;
    		 }
    		 while(arr[i]<=par&&i<j) {
    
    
    			 i++;
    		 }
    		 swap(i, j, arr);
    	 }
    	 arr[lo] = arr[i];
    	 arr[i] = par;
    	 return i;
     }

void swap(int[] a, int i, int j) {
    
    
    int temp = a[i];
    a[i] = a[j];
    a[j] = temp;
}

空间复杂度:O(1) 未使用额外空间。
时间复杂度:O(n) 期望为O(n),最坏时间复杂度为O(n^2)


两种算法优劣比较

看起来快速选择算法的时间空间复杂度都比堆要好,但是使用快速选择有几点局限性:
第一,算法需要修改原数组,如果原数组不能修改的话,还需要拷贝一份数组,空间复杂度就上去了。
第二,算法需要保存所有的数据。如果把数据看成输入流的话,使用堆的方法是来一个处理一个,不需要保存数据,只需要保存 k 个元素的最大堆。而快速选择的方法需要先保存下来所有的数据,再运行算法。当数据量非常大的时候,甚至内存都放不下的时候,就麻烦了。所以当数据量大的时候还是用基于堆的方法比较好。

猜你喜欢

转载自blog.csdn.net/weixin_45736160/article/details/113835614
今日推荐