首页 > 代码库 > 剑指offer (30) 最小的K个数
剑指offer (30) 最小的K个数
题目:输入n个整数,找出其中最小的K个数
方法一:直接std::sort,T(n) = O(nlgn)
方法二:直接std::nth_element T(n) = O(n) 但是修改了原数组
void MinKth(std::vector<int>& num, int kth, std::vector<int>& result){ if (num.size() == 0 || || kth <= 0 || kth > num.size()) { throw std::invalid_argument("invalid_argument"); } std::nth_element(num.begin(), num.begin() + kth - 1, num.end()); for (int i = 0; i < kth; ++i) { result.at(i) = num.at(i); }}
自己实现 nth_element:
一次划分:
int Partition(std::vector<int>& num, int first, int last){ if (first < 0 || last >= num.size() || first > last) { throw std::invalid_argument("invalid_argument"); } int index = rand() % (last - first + 1) + first; int key = num.at(index); std::swap(num.at(first), key); int midIndex = first; for (int i = first + 1; i <= last; ++i) { if (num.at(i) < num.at(first)) { std::swap(num.at(++midIndex), num.at(i)); } } std::swap(num.at(midIndex), num.at(first)); return midIndex;}
void GetKth(std::vector<int>& num, int kth, std::vector<int>& result){ if (num.size() == 0 || kth > num.size()) { throw std::invalid_argument("invalid_argument"); } int first = 0; int last = num.size() - 1; int index = Partition(num, first, last); while (index != kth - 1) { if (index > kth - 1) { last = index - 1; index = Partition(num, first, last); } else { first = index + 1; index = Partition(num, first, last); } } for (int i = 0; i < kth; ++i) { result.at(i) = num.at(i); }}
方法三:O(nlgk)解法
先创建一个大小为K的数据容器来存储最小的k个数,然后顺序遍历原数组:
如果容器中已有数字少于K个,则直接将原数组元素读入到容器中
如果容器中已有K个数字,表示已满,这时 我们找出容器中K个数的最大值,将这个最大值与待插入元素比较:
如果待插入元素比最大值还要大,那么直接忽略
如果待插入元素比最大值要小,那么就用这个元素替换掉最大值
我们该选取什么数据结构来表示这个数据容器呢?
如果是vector数组,那么每次找最大值需要O(k)时间,总时间为O(nk)
注意到我们每次仅仅需要的是最大值,很自然地想到最大堆,每次调整堆需要O(lgk),总时间为O(nlgk)
(1) 数据容器使用vector
void GetKth(const std::vector<int>& num, int kth, std::vector<int>& result){ if (num.size() == 0 || kth <= 0 || kth > num.size()) { throw std::invalid_argument("invalid_argument"); } for (int i = 0; i < num.size(); ++i) { if (result.size() < kth) { result.push_back(num.at(i)); } else { auto maxIter = std::max_element(result.begin(), result.end()); if (num.at(i) < *maxIter) { *maxIter = num.at(i); } } }}
(2)数据容器使用 堆
首先 堆是一棵完全二叉树,根据完全二叉树的性质,我们可以非常方便的由根节点定位到左子节点和右子节点
一般使用数组来表示完全二叉树,结点之间的关系依靠 结点编号来维持
假设父节点的编号为 index (1<= index <= num.size() ) 其左子节点编号为 2*index, 其右子节点编号为 2*index + 1
(注:图片来自于:http://www.cnblogs.com/Anker/archive/2013/01/23/2873422.html)
(1) 堆调整
堆调整操作是对编号为index的结点进行“下降”,使以index为根的子树成为最大堆
(注:图片来自于:http://www.cnblogs.com/Anker/archive/2013/01/23/2873422.html)
void MaxHeapAdjust(std::vector<int>& num, int index) { int leftIndex = 2 * index; int rightIndex = 2 * index + 1; int largest = 0; if (leftIndex <= num.size()) { if (num.at(leftIndex - 1) > num.at(index - 1)) { largest = leftIndex; } else { largest = index; } } if (rightIndex <= num.size()) { if (num.at(rightIndex - 1) > num.at(largest - 1)) { largest = rightIndex; } } if (largest == 0) return; if (largest != index) { std::swap(num.at(index - 1), num.at(largest - 1)); MaxHeapAdjust(num, largest); }}
(2) 建堆
建立最大堆的过程是自底向上地调用最大堆调整程序将一个数组A[1.....N]变成一个最大堆。将数组视为一颗完全二叉树,从其最后一个非叶子节点(n/2)开始进行堆调整
for (int i = num.size() / 2; i >= 1 ; --i) { MaxHeapAdjust(num, i);}
堆调整和建堆操作都已经说清楚了,再回到本题中来
我们首先取K个数建堆,然后对其后的数字不断与堆顶元素(即最大值)做比较,如果比堆顶元素要小,即更新堆顶元素,并以堆顶元素为根结点进行堆调整操作
void GetKth(const std::vector<int>& num, int kth, std::vector<int>& heap) { if (num.size() == 0 || kth <= 0 || kth > num.size()) { throw std::invalid_argument("invalid_argument"); } for (int i = 0; i < kth; ++i) { heap.push_back(num.at(i)); } for (int i = heap.size() / 2; i >= 1 ; --i) { MaxHeapAdjust(heap, i); } for (int i = kth; i < num.size(); ++i) { if (num.at(i) < heap.at(0)) { heap.at(0) = num.at(i); MaxHeapAdjust(heap, 1); } }}
最后上一张图总结一下两种方法的优缺点: