新秀系列C/C++经典问题(四)

时间:2022-07-04 22:52:07

一个主题:查找最小的k个元素

输入n个整数。输出当中最小的k个。

比如输入1。2,3,4,5,6。7和8这8个数字,则最小的4个数字为1,2,3和4。

分析:这道题最简单的思路莫过于把输入的n个整数排序,这样排在最前面的k个数就是最小的k个数。

仅仅是这样的思路的时间复杂度为O(nlogn)。

我们试着寻找更快的解决思路。

我们能够先创建一个大小为k的数据容器来存储最小的k个数字。接下来我们每次从输入的n个整数中读入一个数。假设容器中已有的数字少于k个,则直接把这次读入的整数放入容器之中。假设容器中已有k个数字了,也就是容器已满。此时我们不能再插入新的数字而仅仅能替换已有的数字。我们找出这已有的k个数中最大值。然和拿这次待插入的整数和这个最大值进行比較。假设待插入的值比当前已有的最大值小,则用这个数替换替换当前已有的最大值;假设带插入的值比当前已有的最大值还要大。那么这个数不可能是最小的k个整数之中的一个,由于我们容器内已经有k个数字比它小了,于是我们能够抛弃这个整数。

因此当容器满了之后。我们要做三件事情:一是在k个整数中找到最大数。二是有可能在这个容器中删除最大数,三是可能要插入一个新的数字,并保证k个整数依旧是排序的。假设我们用一个二叉树来实现这个数据容器,那么我们能在O(logk)时间内实现这三步操作。因此对于n个输入数字而言,总的时间效率就是O(nlogk)。

我们能够选择用不同的二叉树来实现这个数据容器。因为我们每次都须要找到k个整数中的最大数字。我们非常easy想到用最大堆。在最大堆中,根结点的值总是大于它的子树中随意结点的值。于是我们每次能够在O(1)得到已有的k个数字中的最大值。但须要O(logk)时间完毕删除以及插入操作。

我们自己从头实现一个最大堆须要一定的代码。我们还能够採用红黑树来实现我们的容器。红黑树通过把结点分为红、黑两种颜色并依据一些规则确保树是平衡的,从而保证在红黑树中查找、删除和插入操作都仅仅须要O(logk)。在STL中set和multiset都是基于红黑树实现的。

假设面试官不反对我们用STL中的数据容器。我们就直接拿过来用吧。以下是基于STL中的multiset的參考代码:

typedef multiset<int, greater<int> >  IntHeap;

///////////////////////////////////////////////////////////////////////
// find k least numbers in a vector
///////////////////////////////////////////////////////////////////////
void FindKLeastNumbers(
const vector<int>& data, // a vector of data
IntHeap& leastNumbers, // k least numbers, output
unsigned int k)
{
leastNumbers.clear(); if (k == 0 || data.size() < k)
{
return;
} vector<int>::const_iterator iter = data.begin();
for (; iter != data.end(); ++iter)
{
// if less than k numbers was inserted into leastNumbers
if ((leastNumbers.size()) < k)
leastNumbers.insert(*iter); // leastNumbers contains k numbers and it's full now
else
{
// first number in leastNumbers is the greatest one
IntHeap::iterator iterFirst = leastNumbers.begin(); // if is less than the previous greatest number
if (*iter < *(leastNumbers.begin()))
{
// replace the previous greatest number
leastNumbers.erase(iterFirst);
leastNumbers.insert(*iter);
}
}
}
}

题目二:二元树中和为某一值的全部路径

输入一个整数和一棵二元树。

从树的根结点開始往下訪问一直到叶结点所经过的全部结点形成一条路径。打印出和与输入整数相等的全部路径。

比如输入整数22和例如以下二元树

10

                                          /   \

                                         5     12

                                      /   \   

                                    4    7

则打印出两条路径:10, 12和10, 5, 7。

二元树结点的数据结构定义为:

struct BinaryTreeNode // a node in the binary tree
{
int m_nValue; // value of node
BinaryTreeNode *m_pLeft; // left child of node
BinaryTreeNode *m_pRight; // right child of node
};

分析:这是百度的一道笔试题,考查对树这样的基本数据结构以及递归函数的理解。

当訪问到某一结点时。把该结点加入到路径上。并累加当前结点的值。

假设当前结点为叶结点而且当前路径的和刚好等于输入的整数。则当前的路径符合要求,我们把它打印出来。假设当前结点不是叶结点。则继续訪问它的子结点。

当前结点訪问结束后。递归函数将自己主动回到父结点。因此我们在函数退出之前要在路径上删除当前结点并减去当前结点的值,以确保返回父结点时路径刚好是根结点到父结点的路径。我们不难看出保存路径的数据结构实际上是一个栈结构。由于路径要与递归调用状态一致,而递归调用本质就是一个压栈和出栈的过程。

參考代码:

///////////////////////////////////////////////////////////////////////
// Find paths whose sum equal to expected sum
///////////////////////////////////////////////////////////////////////
void FindPath(
BinaryTreeNode* pTreeNode, // a node of binary tree
int expectedSum, // the expected sum
std::vector<int>& path, // a path from root to current node
int& currentSum // the sum of path
)
{
if (!pTreeNode)
return; currentSum += pTreeNode->m_nValue;
path.push_back(pTreeNode->m_nValue); // if the node is a leaf, and the sum is same as pre-defined,
// the path is what we want. print the path
bool isLeaf = (!pTreeNode->m_pLeft && !pTreeNode->m_pRight);
if (currentSum == expectedSum && isLeaf)
{
std::vector<int>::iterator iter = path.begin();
for (; iter != path.end(); ++iter)
std::cout << *iter << '\t';
std::cout << std::endl;
} // if the node is not a leaf, goto its children
if (pTreeNode->m_pLeft)
FindPath(pTreeNode->m_pLeft, expectedSum, path, currentSum);
if (pTreeNode->m_pRight)
FindPath(pTreeNode->m_pRight, expectedSum, path, currentSum); // when we finish visiting a node and return to its parent node,
// we should delete this node from the path and
// minus the node's value from the current sum
currentSum -= pTreeNode->m_nValue;
path.pop_back();
}

今天看到了两个冠军, 一定要指出一个错误, 共享注明出处, 谢谢!