剑指offer 最小的k个数

1.题目

输入n个整数,找出其中最小的K个数。例如输入4,5,1,6,2,7,3,88个数字,则最小的4个数字是1,2,3,4

来源:剑指offer
链接:https://www.nowcoder.com/practice/6a296eb82cf844ca8539b57c23e6e9bf?tpId=13&tqId=11182&rp=1&ru=/ta/coding-interviews&qru=/ta/coding-interviews/question-ranking

2.我的题解

2.1最小堆

使用最小堆实现:

  • 实现一个最小堆,借助vector,下标从1开始存储;
  • 实现删除最小元素的函数del,要求删除后保持堆有序,返回删除的元素;
  • 利用输入数据建堆;
  • 连续执行Kdel,并将函数返回值添加到输出结果。
  • 时间复杂度:O(n+klogn)
  • 空间复杂度:O(n)

下沉操作构建堆:

  • 确定堆的大小,开辟空间并填入数值;
  • 从第一个非叶节点开始做下沉操作,直到第一个节点。

构建堆的复杂度计算:n个节点,树高h的满二叉树(堆)为例,显然h=log(n+1)
各层节点关系: 设想一个满二叉树,下一层的节点数总是翻倍,而总结点个数约等于最后一层节点个数的两倍(实际上就少1);故每一层的节点数和都可以由n表示。

堆的层数下沉时比较次数最大节点数最大总操作数
1h1/2h * (n+1)h/2h * (n+1)
2h-11/2h-1 * (n+1)(h-1)/2h-1 * (n+1)
3h-21/2h-2 * (n+1)(h-2)/2h-2 * (n+1)
……………………
kh-k1/2h+1-k * (n+1)(h-k)/2h+1-k * (n+1)
……………………
h11/2 * (n+1)1/2 * (n+1)

S = ∑ k = 1 h k 2 k = 1 2 + 2 4 + ⋯ + h 2 h S 2 = ∑ k = 1 h k 2 k + 1 = 1 4 + 2 8 + ⋯ + h 2 h + 1 错 位 相 减 得 : S 2 = 1 2 + 1 4 + 1 8 + ⋯ + 1 2 h − h 2 h + 1 = ( 1 − 1 2 h + 1 ) − h 2 h + 1 < 1 T = h 2 h ( n + 1 ) + h − 1 2 h − 1 ( n + 1 ) + ⋯ + 1 2 ( n + 1 ) = ( n + 1 ) ∑ k = 1 h k 2 k < ( n + 1 ) ∗ 2 \begin{aligned} S=&\sum_{k=1}^{h}\frac{k}{2^k}=\frac{1}{2}+\frac{2}{4}+\cdots+\frac{h}{2^h}\\ \frac{S}{2}=&\sum_{k=1}^{h}\frac{k}{2^{k+1}}=\frac{1}{4}+\frac{2}{8}+\cdots+\frac{h}{2^{h+1}}\\ 错位相减&得:\\ \frac{S}{2}=&\frac{1}{2}+\frac{1}{4}+\frac{1}{8}+\cdots+\frac{1}{2^{h}}-\frac{h}{2^{h+1}}\\ =&(1-\frac{1}{2^{h+1}})-\frac{h}{2^{h+1}}\\ <&1\\ T=&\frac{h}{2^h}(n+1)+\frac{h-1}{2^{h-1}}(n+1)+\cdots+\frac{1}{2}(n+1) \\ =&(n+1)\sum_{k=1}^{h}\frac{k}{2^k} \\ <&(n+1)*2 \end{aligned} S=2S=2S==<T==<k=1h2kk=21+42++2hhk=1h2k+1k=41+82++2h+1h21+41+81++2h12h+1h(12h+11)2h+1h12hh(n+1)+2h1h1(n+1)++21(n+1)(n+1)k=1h2kk(n+1)2

因此T=O(n)

class Solution {
	vector<int> vec;//小顶堆
	int N = 0;
	void swap(int * a, int * b) {
		int tmp = *a;
		*a = *b;
		*b = tmp;
	}
	void sink(int k) {
		while ((k<<1) <= N) {
			int j = k << 1;
			if (j+1<=N && vec[j] > vec[j + 1])j++;
			if (vec[k] > vec[j])swap(&vec[k], &vec[j]);
			k = j;
		}
	}
	int del() {
		swap(&vec[1], &vec[N--]);
		sink(1);
		return vec[N+1];
	}
	void makeHeap(vector<int> &data) {
		N = data.size();
		vec.resize(N << 1);
        for(int i=0;i<data.size();i++)vec[i+1]=data[i];
		for(int i=N/2;i>=1;i--)
            sink(i);
	}
public:
	vector<int> GetLeastNumbers_Solution(vector<int> input, int k) {
		vector<int> res;
		if (k == 0 || input.empty() || k>input.size())return res;
		//make heap
		makeHeap(input);
		//find
		while (k-- > 0) {
			int tmp = del();
			//cout << tmp << " ";
			res.push_back(tmp);
		}
		return res;
	}
};

3.别人的题解

3.1 大顶堆

构造大小为K的大顶堆而不是小顶堆:

  • 遍历输入数据;
  • 如果堆的规模小于等于K:添加元素到大顶堆中;
  • 如果堆的规模大于K:如果当前数大于堆顶元素,跳过;如果当前元素小于堆顶,则用该元素代替堆顶元素,并进行操作保证堆有序。
  • 时间复杂度:O(nlogk)
  • 空间复杂度:O(k) ;该方法适合海量数据。
class Solution {
	vector<int> vec;//大顶堆
	int N = 0;
	void swap(int * a, int * b) {
		int tmp = *a;
		*a = *b;
		*b = tmp;
	}
	void sink(int k) {
		while ((k << 1) <= N) {
			int j = k << 1;
			if (j + 1 <= N && vec[j] < vec[j + 1])j++;
			if (vec[k] < vec[j])swap(&vec[k], &vec[j]);
			k = j;
		}
	}
	void makeHeap(vector<int> &data, int K) {
		vec.resize(K + 1);
		N = K;
		int i = 0;
		//1~K
		for (i = 0; i<K; i++) {
			vec[i + 1] = data[i];
		}
		for (i = K / 2; i >= 1; i--)sink(i);
		//K~n
		for (i = K; i<data.size(); i++) {
			if (data[i]<vec[1]) {
				vec[1] = data[i];
				sink(1);
			}
		}
	}
public:
	vector<int> GetLeastNumbers_Solution(vector<int> input, int k) {
		vector<int> res;
		if (k == 0 || input.empty() || k>input.size())return res;
		//make heap
		makeHeap(input, k);
		//find
		res.assign(vec.begin() + 1, vec.begin() + k + 1);
		return res;
	}
};

3.2 partition思想

利用快排中的partition思想对[low,high]进行操作,如果进行partition操作返回的值:

  • 初始化:low=0,high=len(input)-1
  • 小于Klow++;
  • 大于Khigh--
  • 等于K:返回输入数组的前K个元素;
  • 该方法改变输入数据,不需要额外空间;每次partition时间复杂度为O(n)n为输入数据的规模),最好情况下只需要执行1次,最坏情况下需要执行n次;
  • 时间复杂度:O(n)~O(n^2)
  • 空间复杂度:O(1)
class Solution {
	void swap(int * a, int * b) {
		int tmp = *a;
		*a = *b;
		*b = tmp;
	}
    int partition(vector<int> &s,int left,int right){
        int pivot=s[left];
        int i=left;
        for(int j=left+1;j<=right;j++){
            if(s[j]<=pivot){
                i++;
                if(i!=j)swap(&s[i],&s[j]);
            }
        }
        swap(&s[left],&s[i]);
        return i;
    }
public:
	vector<int> GetLeastNumbers_Solution(vector<int> input, int k) {
		vector<int> res;
		if (k == 0 || input.empty() || k>input.size())return res;
        //partition
        int low=0,high=input.size()-1;
        int index=partition(input,low,high);
        while(index!=k-1){
            if(index>k-1)high--;
            else low++;
            index=partition(input,low,high);
        }
        res.assign(input.begin(),input.begin()+k);
		return res;
	}
};

4.总结与反思

(1)Markdown数学公式。
(2)partition思想。
(3)大顶堆、小顶堆及建堆、用途。

4.1 大顶堆小顶堆的比较

建堆需要O(n)的复杂度,具体近似是2n,下沉操作复杂度是O(logn),故建堆加找最小的K个数:

  • 大顶堆:时间复杂度O(2n+klogn),空间复杂度O(n)
  • 小顶堆:时间复杂度O(2k+nlogk),空间复杂度O(k)

闲来无聊,我分析了一波本题中大顶堆实现和小顶堆实现的时间耗费,我直觉地觉着,大顶堆已经耗费更多的空间了,时间上应该会好一些吧……n,k都是可变的,三维曲面图画了没能清晰地看出变化趋势,于是固定了n,观察不同k下的时间耗费:

  • 如图可以看到,基本上k>8时,大顶堆的时间耗费将更低,k=n时,二者时间耗费一样。
  • 横坐标是k,纵坐标是时间耗费,计算公式如上。

在这里插入图片描述

4.2 partition的实现

pivot的选择:

  • 选取第一个元素;
  • 选取最后一个元素;
  • 选取第一个、中间、最后一个元素的中位数:该方法性能一般较好。
  • 随机选取数组中某个值:性能较好,但random()并不便宜。

4.2.1 单向扫描法实现partition

int partition1(vector<int> s, int left, int right) {
	int pivot = s[left];
	int i = left ;
	for (int j = left+1; j <= right; j++) {
		if (s[j] <= pivot) {
			i++;
			if (i != j)swap(&s[i], &s[j]);
		}
	}
	swap(&s[left], &s[i]);
	return i;
}

4.2.2 双向扫描法实现partition(哨兵法)

void swap(int *a, int *b) {
	int tmp = *a; *a = *b; *b = tmp;
}
int partition2(vector<int> s, int left, int right) {
	int pivot = s[left];
	int i = left, j = right;
	while (i < j) {
		while (i<j&&s[j]>pivot)j--;
		while (i < j&&s[i] <= pivot)i++;
		if (i < j)swap(&s[i], &s[j]);
	}
	swap(&s[left], &s[i]);
	return i;
}

4.2.3 双向扫描法实现partition(挖坑法)

int partition3(vector<int> s, int left, int right) {
	int pivot = s[left];
	int i = left, j = right;
	while (i < j) {
		while (i<j && s[j]>pivot)j--;
		if (i < j)s[i++] = s[j];
		while (i < j&&s[i] <= pivot)i++;
		if (i < j)s[j--] = s[i];
	}
	s[i] = pivot;
	return i;
}

4.2.4 partition三路切分

假设数组的值仅有0,1,2三种取值,需要将全部0放到最左边,全部2放最右边,全部1放中间,可通过以下步骤实现:

  • 获取数组长度len,初始化i=0,low=0,high=len-1
  • 从两端向中间扫描,同时从左向右扫描;
  • 可以理解为low的左边全是0i指向当前数值,high的右边全是2;只要把全部的0移到左边,全部的2移到右边,那么剩下的1就自然全在中间了;
  • i位置上是1i右移;
  • 位置上是0,交换ilow位置,同时都右移;
  • i位置上是2,交换ihigh位置,high左移,但i不右移,因为交换后i位置可能是0,就还需要一次交换;
  • 完成后low指向第一个1high指向第一个2,如需要确切的划分位置可利用。
#include <iostream>
#include <stack>
#include <string>
#include <vector>
#include <algorithm>
#include <map>
#include <queue>
#include <stdio.h>
#include <list>
#include <set>

using namespace std;

void partitionThreeWay(vector<int> &s, int left, int right) {
	int i = left , low = left, high = right;
	while (i <= high) {
		if (s[i] == 1)i++;
		else if (s[i] == 0)swap(&s[i++], &s[low++]);
		else swap(&s[i], &s[high--]);
	}
	//输出内容
	for (int t = left; t <= right; t++) {
		if (t == low)cout << low << " ";
		else if (t == i)cout << i << " ";
		else cout << "_ ";
	}
	cout << endl;
	for (int t = left; t <= right; t++) {
		cout << s[t] << " ";
	}
	cout << endl;
}
int main()
{
	vector<int> s = { 0,1,2,2,1,0,1,2,0 };
	partitionThreeWay(s,0, s.size() - 1);
	system("pause");
	return 0;
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值