100负5减去8等于多少100、等于0写在前面还是后面

更多选车参考:
miniholder
综述:法国的萨拉是一辆好车。这辆车在法国的售价不高,1.6I的配置合人民币12.3万元。
1.6I在英国的售价在15.17万元。
0至100KM/H加速在13.1秒。
而且标配六个,正因为配置了六个气囊,所以在欧洲的撞击试验中得到了宝贵的四星(按百分制算,正面撞击得分为69分,侧向撞击得了89分的高分)。
这辆车外型美观,内部空间大。
而且储物格多,在后座的踏脚处也都有储物格,后座是三个独立的座位,可以各自单独翻折。
在前座的靠背后而装了航空椅上的折板,可以支起作小餐桌用。
五个座位全部装有三点式安全带,在前排座位的还是预紧限力式安全带,是一种新式的更安全的品种。
在追求舒适性方面,法国人永远是走在世界前列的。
可是,这款车在引进国内后对安全配置方面有所减少。四个侧气囊被减了。
这样,国内组装的车只相当于法国10.3万人民币价格的车?
不知和的配置有什么问题,据说加速性能差了一点?
即使降低了配置,在国内也还是大受欢迎,这个月准备产一千辆,现在据说已被订购一空。
我们对照一下在国内的定价,觉得萨拉毕加索的定价偏高。
参考宝来和萨拉毕加索在英国的销售价,(今日英镑对人民币比价1:11.8775)我们看到:
价格范围 :
宝二奶Bora:£1;相当于人民币154823元至241113元之间。
萨拉毕加索:£1;相当于人民币151972元至183685元之间。
以下是在英国的详细售价(已按今日汇率折成人民币售价)
萨拉毕加索:
规格 售价(镑)售价(人民币)
XSARA PICASSO 5 Door 1.6i LX & 12795 ¥151972
XSARA PICASSO 5 Door 1.6i SX & 13495 ¥160286
XSARA PICASSO 5 Door 2.0HDi 90hp LX & 13765 ¥163493
XSARA PICASSO 5 Door 1.8i 16V SX & 13795 ¥163850
XSARA PICASSO 5 Door 2.0HDi 90hp SX &
XSARA PICASSO 5 Door 1.8i 16V Exclusive & 14795 (带天窗) ¥175727
XSARA PICASSO 5 Door 2.0 HDi 90hp Exclusive & 15465 (带天窗)¥183685
规格 售价(镑) 售价(人民币)
S 1.6 105bhp 5 spd manual &13,035.00 ¥154823
S 2.0 115bhp 5spd manual &13,615.00 ¥161712
S TDI 1.9 90bhp 5spd manual &14,035.00 ¥166700
S TDI PD 1.9 100bhp 5spd manual &14,270.00 ¥169491
S TDI 1.9 110bhp 5spd manual &14,335.00 170263
S 2.0 115bhp 4spd auto &14,440.00 ¥171511
ST 1.8T 150bhp 5spd manual &15,795.00 ¥187605
ST TDI PD 1.9 130bhp 6spd manual &16,310.00 ¥193722
ST TDI PD 1.9 130bhp 5spd auto tiptronic &17,275.00 ¥205183
V5 2.3 170bhp 5spd manual &18,000.00 ¥213795
V5 2.3 170bhp 5spd auto tiptronic &19,150.00 ¥227454
选配方案:
宝二奶Bora:
在英国有六种规格:S、SE、ST、Sport、V5,各种规格均可选配多种的发动机及变速器,共有31种配置。
萨拉毕加索:
在英国有三种规格:Exclusive、LX、SX,有三种发动机可以选配,1.6i、1.8i16V、2.0HDi90HP,其中2.0的是柴油机。Exclusive可配1.8和2.0,LX可配1.6和2.0两种,而SX则三种发动机均可配。但没有自动档可选。
安全配置:
宝二奶:标配有四个汽囊,两个前,两个侧。帘式气囊要另配,得花430英镑,合5100元人民币。前座安全带是预紧式的,后座是两点式的,换三点式安全带得另加50英镑。
萨拉毕加索:标配有六个气囊:两个前,两个侧,两个帘。
全部安全带是三点式的,前座是预紧限力式的。
现在神龙和一汽分别将上述两种车引进中国;
神龙的将两侧的四个气囊减去,最低配置要卖20.8万;而我们看到,在英国的最低配置售价为15.19万元;减去四个气囊大约相当于不到十四万元吧!
一汽不知减了配置没有?最低的配置据说是19.3万。而我们看到,在英国的最低配置售价是15.48万元。从中我们可以看到,神龙赶不上一汽的真正原因:定价偏高。
11:52:14回复(0)|支持(0)
上一条口碑:
下一条口碑:
车型评分:*
做出个总体评价吧
评价标题:*
必填,3-20个汉字
您的评价会对其他人有很大的帮助
填写个综述吧10-500汉字
您还需要输入10个汉字
选择口碑分类:
外观内饰操控动力售后保养
请输入验证码:
验证码有误
同步到微博&&
指导价:8.28-11.93万(待定)
市场价: 4.89-14.88万
类型:紧凑型
擅长领域:
解答问题:个
被提问:次程序员面试题精选100题与解法_甜梦文库
程序员面试题精选100题与解法
程序员面试题精选 100 题(01)-把二元查找树转变成排序的双向链表 题目:输入一棵二元查找树,将该二元查找树转换成一个排序的双向链表。要求 不能创建任何新的结点,只调整指针的指向。 比如将二元查找树 10 / 6 / 4 转换成双向链表 4=6=8=10=12=14=16。 分析:本题是微软的面试题。很多与树相关的题目都是用递归的思路来 解决,本题也不例外。下面我们用两种不同的递归思路来分析。 思路一:当我们到达某一结点准备调整以该结点为根结点的子树时,先 调整其左子树将左子树转换成一个排好序的左子链表,再调整其右子树转换右子 链表。最近链接左子链表的最右结点(左子树的最大结点)、当前结点和右子链 表的最左结点(右子树的最小结点)。从树的根结点开始递归调整所有结点。 思路二:我们可以中序遍历整棵树。按照这个方式遍历树,比较小的结 点先访问。如果我们每访问一个结点,假设之前访问过的结点已经调整成一个排 序双向链表,我们再把调整当前结点的指针将其链接到链表的末尾。当所有结点 都访问过之后,整棵树也就转换成一个排序双向链表了。 参考代码: 首先我们定义二元查找树结点的数据结构如下: struct BSTreeNode // a node in the binary search tree { int m_nV // value of node BSTreeNode *m_pL // left child of node BSTreeNode *m_pR // right child of node }; 思路一对应的代码: ///////////////////////////////////////////////////////// ////////////// // Covert a sub binary-search-tree into a sorted double-linked list \ 8 / 12 \ 14 \ 16 // Input: pNode - the head of the sub tree // asRight - whether pNode is the right child of its parent // Output: if asRight is true, return the least node in the sub-tree // else return the greatest node in the sub-tree ///////////////////////////////////////////////////////// ////////////// BSTreeNode* ConvertNode(BSTreeNode* pNode, bool asRight) { if(!pNode) return NULL; BSTreeNode *pLeft = NULL; BSTreeNode *pRight = NULL; // Convert the left sub-tree if(pNode-&m_pLeft) pLeft = ConvertNode(pNode-&m_pLeft, false); // Connect the greatest node in the left sub-tree to the current node if(pLeft) { pLeft-&m_pRight = pN pNode-&m_pLeft = pL } // Convert the right sub-tree if(pNode-&m_pRight) pRight = ConvertNode(pNode-&m_pRight, true); // Connect the least node in the right sub-tree to the current node if(pRight) { pNode-&m_pRight = pR pRight-&m_pLeft = pN } BSTreeNode *pTemp = pN // If the current node is the right child of its parent, // return the least node in the tree whose root is the current node if(asRight) { while(pTemp-&m_pLeft) pTemp = pTemp-&m_pL } // If the current node is the left child of its parent, // return the greatest node in the tree whose root is the current node else { while(pTemp-&m_pRight) pTemp = pTemp-&m_pR } return pT } ///////////////////////////////////////////////////////// ////////////// // Covert a binary search tree into a sorted double-linked list // Input: the head of tree // Output: the head of sorted double-linked list ///////////////////////////////////////////////////////// ////////////// BSTreeNode* Convert(BSTreeNode* pHeadOfTree) { // As we want to return the head of the sorted double-linked list, // we set the second parameter to be true return ConvertNode(pHeadOfTree, true); } 思路二对应的代码: ///////////////////////////////////////////////////////// ////////////// // Covert a sub binary-search-tree into a sorted double-linked list // Input: pNode the head of the sub tree // pLastNodeInList - the tail of the double-linked list ///////////////////////////////////////////////////////// ////////////// void ConvertNode(BSTreeNode* pNode, BSTreeNode*& pLastNodeInList) { if(pNode == NULL) BSTreeNode *pCurrent = pN // Convert the left sub-tree if (pCurrent-&m_pLeft != NULL) ConvertNode(pCurrent-&m_pLeft, pLastNodeInList); // Put the current node into the double-linked list pCurrent-&m_pLeft = pLastNodeInL if(pLastNodeInList != NULL) pLastNodeInList-&m_pRight = pC pLastNodeInList = pC // Convert the right sub-tree if (pCurrent-&m_pRight != NULL) ConvertNode(pCurrent-&m_pRight, pLastNodeInList); } ///////////////////////////////////////////////////////// ////////////// // Covert a binary search tree into a sorted double-linked list // Input: pHeadOfTree - the head of tree // Output: the head of sorted double-linked list ///////////////////////////////////////////////////////// ////////////// BSTreeNode* Convert_Solution1(BSTreeNode* pHeadOfTree) { BSTreeNode *pLastNodeInList = NULL; ConvertNode(pHeadOfTree, pLastNodeInList); // Get the head of the double-linked list BSTreeNode *pHeadOfList = pLastNodeInL while(pHeadOfList && pHeadOfList-&m_pLeft) pHeadOfList = pHeadOfList-&m_pL return pHeadOfL } 程序员面试题精选 100 题(02)-设计包含 min 函数的栈 题目:定义栈的数据结构,要求添加一个 min 函数,能够得到栈的最小元素。 要求函数 min、push 以及 pop 的时间复杂度都是 O(1)。 分析:这是去年 google 的一道面试题。 我看到这道题目时,第一反应就是每次 push 一个新元素时,将栈里所有逆序元 素排序。这样栈顶元素将是最小元素。但由于不能保证最后 push 进栈的元素最 先出栈,这种思路设计的数据结构已经不是一个栈了。 在栈里添加一个成员变量存放最小元素(或最小元素的位置)。每次 push 一个 新元素进栈的时候,如果该元素比当前的最小元素还要小,则更新最小元素。 乍一看这样思路挺好的。但仔细一想,该思路存在一个重要的问题:如果当前最 小元素被 pop 出去,如何才能得到下一个最小元素? 因此仅仅只添加一个成员变量存放最小元素(或最小元素的位置)是不够的。我 们需要一个辅助栈。每次 push 一个新元素的时候,同时将最小元素(或最小元 素的位置。考虑到栈元素的类型可能是复杂的数据结构,用最小元素的位置将能 减少空间消耗)push 到辅助栈中;每次 pop 一个元素出栈的时候,同时 pop 辅 助栈。 参考代码: #include &deque& #include &assert.h& template &typename T& class CStackWithMin { public: CStackWithMin(void) {} virtual ~CStackWithMin(void) {} T& top(void); const T& top(void) void push(const T& value); void pop(void); const T& min(void) private: T&m_// theelements of stack size_t&m_minI// the indicesof minimum elements }; // get the last element of mutable stack template &typename T& T& CStackWithMin&T&::top() { return m_data.back(); } // get the last element of non-mutable stack template &typename T& const T& CStackWithMin&T&::top() const { return m_data.back(); } // insert an elment at the end of stack template &typename T& void CStackWithMin&T&::push(const T& value) { // append the data into the end of m_data m_data.push_back(value); // set the index of minimum elment in m_data at the end of m_minIndex if(m_minIndex.size() == 0) m_minIndex.push_back(0); else { if(value & m_data[m_minIndex.back()]) m_minIndex.push_back(m_data.size() - 1); else m_minIndex.push_back(m_minIndex.back()); } } // erease the element at the end of stack template &typename T& void CStackWithMin&T&::pop() { // pop m_data m_data.pop_back(); // pop m_minIndex m_minIndex.pop_back(); } // get the minimum element of stack template &typename T& const T& CStackWithMin&T&::min() const { assert(m_data.size() & 0); assert(m_minIndex.size() & 0); return m_data[m_minIndex.back()]; } 举个例子演示上述代码的运行过程: 步骤 1.push 2.push 3.push 4.push 5.pop 6.pop 7.push 数据栈 3 3,4 3,4,2 3,4,2,1 3,4,2 3,4 3,4,0 辅助栈 0 0,0 0,0,2 0,0,2,3 0,0,2 0,0 0,0,2 最小值 3 3 2 1 2 3 03 4 2 10讨论:如果思路正确,编写上述代码不是一件很难的事情。但如果能注意一些细 节无疑能在面试中加分。比如我在上面的代码中做了如下的工作: ? 用模板类实现。如果别人的元素类型只是 int 类型,模板将能给面试官带 来好印象; ? 两个版本的 top 函数。在很多类中,都需要提供 const 和非 const 版本的 成员访问函数; ? min 函数中 assert 把代码写的尽量安全是每个软件公司对程序员的要求 。 ;? 添加一些注释。注释既能提高代码的可读性,又能增加代码量,何乐而不 为? 总之,在面试时如果时间允许,尽量把代码写的漂亮一些。说不定代码中的几个 小亮点就能让自己轻松拿到心仪的 Offer。 PS: 每当 push 进一个新元素, 进一个新元素, 若比当前最小元素小, 则将它进栈, PS: 若比当前最小元素小, 则将它进栈, 并将它的 index 进最小辅助栈;若大于当前最小元素,则将它进栈, 进最小辅助栈;若大于当前最小元素,则将它进栈,并将当前最小元素 index 进最小辅助栈(可以重复进栈多次)! 进最小辅助栈(可以重复进栈多次)!程序员面试题精选 100 题(03)-求子数组的最大和 题目:输入一个整形数组,数组里有正数也有负数。数组中连续的一个或多个整 数组成一个子数组,每个子数组都有一个和。求所有子数组的和的最大值。要求 时间复杂度为 O(n)。 例如输入的数组为 1, -2, 3, 10, -4, 7, 2, -5,和最大的子数组为 3, 10, -4, 7, 2, 因此输出为该子数组的和 18。 分析:本题最初为 2005 年浙江大学计算机系的考研题的最后一道程序设计题, 在 2006 年里包括 google 在内的很多知名公司都把本题当作面试题。由于本题 在网络中广为流传,本题也顺利成为 2006 年程序员面试题中经典中的经典。 如果不考虑时间复杂度,我们可以枚举出所有子数组并求出他们的和。不过非常 遗憾的是,由于长度为 n 的数组有 O(n2)个子数组;而且求一个长度为 n 的数组 的和的时间复杂度为 O(n)。因此这种思路的时间是 O(n3)。 很容易理解,当我们加上一个正数时,和会增加;当我们加上一个负数时,和会 减少。如果当前得到的和是个负数,那么这个和在接下来的累加中应该抛弃并重 新清零,不然的话这个负数将会减少接下来的和。基于这样的思路,我们可以写 出如下代码。 参考代码: ///////////////////////////////////////////////////////// //////////////////// // Find the greatest sum of all sub-arrays // Return value: if the input is valid, return true, otherwise return false ///////////////////////////////////////////////////////// //////////////////// bool FindGreatestSumOfSubArray ( int *pData, // an array unsigned int nLength, // the length of array int &nGreatestSum ) {// the greatest sum of all sub-arrays// if the input is invalid, return false if((pData == NULL) || (nLength == 0)) int nCurSum = nGreatestSum = 0; for(unsigned int i = 0; i & nL ++i) { nCurSum += pData[i]; // if the current sum is negative, discard it if(nCurSum & 0) nCurSum = 0; // if a greater sum is found, update the greatest sum if(nCurSum & nGreatestSum) nGreatestSum = nCurS }// if all data are negative, find the greatest element in the array if(nGreatestSum == 0) { nGreatestSum = pData[0]; for(unsigned int i = 1; i & nL ++i) { if(pData[i] & nGreatestSum) nGreatestSum = pData[i]; } } } 讨论:上述代码中有两点值得和大家讨论一下: ? 函数的返回值不是子数组和的最大值,而是一个判断输入是否有效的标 志。如果函数返回值的是子数组和的最大值,那么当输入一个空指针是应该返回 什么呢?返回 0?那这个函数的用户怎么区分输入无效和子数组和的最大值刚 好是 0 这两中情况呢?基于这个考虑 本人认为把子数组和的最大值以引用的方 , 式放到参数列表中,同时让函数返回一个函数是否正常执行的标志。 ? 输入有一类特殊情况需要特殊处理。当输入数组中所有整数都是负数时, 子数组和的最大值就是数组中的最大元素。 扫描算法。 《编程珠机》第八章,8.4 扫描算法。 编程珠机》第八章, 采用类似分治算法的道理: 个元素中, 采用类似分治算法的道理:前 i 个元素中,最大综合子数组要么在 i-1 个元素 (maxsofar), i(maxendinghere)。 中(maxsofar),要么截止到位置 i(maxendinghere)。程序员面试题精选 100 题(04)-在二元树中找出和为某一值的所有路径 题目:输入一个整数和一棵二元树。从树的根结点开始往下访问一直到叶结点所 经过的所有结点形成一条路径。打印出和与输入整数相等的所有路径。 例如输入整数 22 和如下二元树 10 / \ 5 12 / \ 4 7 则打印出两条路径:10, 12 和 10, 5, 7。 二元树结点的数据结构定义为: struct BinaryTreeNode // a node in the binary tree { int m_nV // value of node BinaryTreeNode *m_pL // left child of node BinaryTreeNode *m_pR // 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 pathfrom root to current node int& currentSum // the sum of path ) { if(!pTreeNode) currentSum += pTreeNode-&m_nV 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:: } // if the node is not a leaf, goto its children if(pTreeNode-&m_pLeft) FindPath(pTreeNode-&m_pLeft, expectedSum, currentSum); if(pTreeNode-&m_pRight) FindPath(pTreeNode-&m_pRight, expectedSum, currentSum);path,path, // 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_nV path.pop_back(); }程序员面试题精选 100 题(05)-查找最小的 k 个元素 题目:输入 n 个整数,输出其中最小的 k 个。 例如输入 1,2,3,4,5,6,7 和 8 这 8 个数字,则最小的 4 个数字为 1,2, 3 和 4。 分析:这道题最简单的思路莫过于把输入的 n 个整数排序,这样排在最前面的 k 个数就是最小的 k 个数。只是这种思路的时间复杂度为 O(nlogn)。我们试着寻 找更快的解决思路。 我们可以开辟一个长度为 k 的数组。每次从输入的 n 个整数中读入一个数。如果 数组中已经插入的元素少于 k 个,则将读入的整数直接放到数组中。否则长度为 k 的数组已经满了,不能再往数组里插入元素,只能替换了。如果读入的这个整 数比数组中已有 k 个整数的最大值要小,则用读入的这个整数替换这个最大值; 如果读入的整数比数组中已有 k 个整数的最大值还要大,则读入的这个整数不可 能是最小的 k 个整数之一,抛弃这个整数。这种思路相当于只要排序 k 个整数, 因此时间复杂可以降到 O(n+nlogk)。通常情况下 k 要远小于 n,所以这种办法要 优于前面的思路。 这是我能够想出来的最快的解决方案。不过从给面试官留下更好印象的角度出 发,我们可以进一步把代码写得更漂亮一些。从上面的分析,当长度为 k 的数组 已经满了之后,如果需要替换,每次替换的都是数组中的最大值。在常用的数据 结构中,能够在 O(1)时间里得到最大值的数据结构为最大堆。因此我们可以用 堆(heap)来代替数组。 另外,自己重头开始写一个最大堆需要一定量的代码。我们现在不需要重新去发 明车轮,因为前人早就发明出来了。同样,STL 中的 set 和 multiset 为我们做了 很好的堆的实现,我们可以拿过来用。既偷了懒,又给面试官留下熟悉 STL 的 好印象,何乐而不为之? 参考代码: #include &set& #include &vector& #include &iostream& typedef multiset&int, greater&int& & IntH///////////////////////////////////////////////////////// ////////////// // 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) 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); } } } }程序员面试题精选 100 题(06)-判断整数序列是不是二元查找树的后序遍历结 果 题目:输入一个整数数组,判断该数组是不是某二元查找树的后序遍历的结果。 如果是返回 true,否则返回 false。 例如输入 5、7、6、9、11、10、8,由于这一整数序列是如下树的后序遍历结 果: 8 / \ 6 10 /\ /\ 5 7 9 11 因此返回 true。 如果输入 7、4、6、5,没有哪棵树的后序遍历的结果是这个序列,因此返回 false。 分析:这是一道 trilogy 的笔试题,主要考查对二元查找树的理解。 在后续遍历得到的序列中,最后一个元素为树的根结点。从头开始扫描这个序 列,比根结点小的元素都应该位于序列的左半部分;从第一个大于跟结点开始到 跟结点前面的一个元素为止,所有元素都应该大于跟结点,因为这部分元素对应 的是树的右子树。根据这样的划分,把序列划分为左右两部分,我们递归地确认 序列的左、右两部分是不是都是二元查找树。 参考代码:
///////////////////////////////////////////////////////// ////////////// // Verify whether a squence of integers are the post order traversal // of a binary search tree (BST) // Input: squence - the squence of integers // length - the length of squence // Return: return ture if the squence is traversal result of a BST, // otherwise, return false ///////////////////////////////////////////////////////// ////////////// bool verifySquenceOfBST(int squence[], int length) { if(squence == NULL || length &= 0) // root of a BST is at the end of post order traversal squence int root = squence[length - 1]; // the nodes in left sub-tree are less than the root int i = 0; for(; i & length - 1; ++ i) { if(squence[i] & root) } // the nodes in the right sub-tree are greater than the root int j = for(; j & length - 1; ++ j) { if(squence[j] & root) } // verify whether the left sub-tree is a BST bool left = if(i & 0) left = verifySquenceOfBST(squence, i); // verify whether the right sub-tree is a BST bool right = if(i & length - 1) right = verifySquenceOfBST(squence + i, length - i 1); return (left && right); }程序员面试题精选 100 题(07)-翻转句子中单词的顺序 题目:输入一个英文句子,翻转句子中单词的顺序,但单词内字符的顺序不变。 句子中单词以空格符隔开。为简单起见,标点符号和普通字母一样处理。 例如输入“I am a student.”,则输出“student. a am I”。 分析:由于编写字符串相关代码能够反映程序员的编程能力和编程习惯,与字符 串相关的问题一直是程序员笔试、面试题的热门题目。本题也曾多次受到包括微 软在内的大量公司的青睐。 由于本题需要翻转句子,我们先颠倒句子中的所有字符。这时,不但翻转了句子 中单词的顺序,而且单词内字符也被翻转了。我们再颠倒每个单词内的字符。由 于单词内的字符被翻转两次,因此顺序仍然和输入时的顺序保持一致。 还是以上面的输入为例子。翻转“I am a student.”中所有字符得到“.tneduts a ma I”,再翻转每个单词中字符的顺序得到“students. a am I”,正是符合要求 的输出。 参考代码: ///////////////////////////////////////////////////////// ////////////// // Reverse a string between two pointers // Input: pBegin - the begin pointer in a string // pEnd - the end pointer in a string ///////////////////////////////////////////////////////// ////////////// void Reverse(char *pBegin, char *pEnd) { if(pBegin == NULL || pEnd == NULL) while(pBegin & pEnd) { char temp = *pB *pBegin = *pE *pEnd = pBegin ++, pEnd --; } } ///////////////////////////////////////////////////////// ////////////// // Reverse the word order in a sentence, but maintain the character // order inside a word // Input: pData - the sentence to be reversed ///////////////////////////////////////////////////////// ////////////// char* ReverseSentence(char *pData) { if(pData == NULL) return NULL; char *pBegin = pD char *pEnd = pD while(*pEnd != '\0') pEnd ++; pEnd--; // Reverse the whole sentence Reverse(pBegin, pEnd); // Reverse every word in the sentence pBegin = pEnd = pD while(*pBegin != '\0') { if(*pBegin == ' ') { pBegin ++; pEnd ++;
} // A word is between with pBegin and pEnd, reverse it else if(*pEnd == ' ' || *pEnd == '\0') { Reverse(pBegin, --pEnd); pBegin = ++pE } else { pEnd ++; } } return pD }程序员面试题精选 100 题(08)-求 1+2+...+n 题目:求 1+2+…+n,要求不能使用乘除法、for、while、if、else、switch、case 等关键字以及条件判断语句(A?B:C)。 分析:这道题没有多少实际意义,因为在软件开发中不会有这么变态的限制。但 这道题却能有效地考查发散思维能力,而发散思维能力能反映出对编程相关技术 理解的深刻程度。 通常求 1+2+…+n 除了用公式 n(n+1)/2 之外,无外乎循环和递归两种思路。由 于已经明确限制 for 和 while 的使用,循环已经不能再用了。同样,递归函数也 需要用 if 语句或者条件判断语句来判断是继续递归下去还是终止递归,但现在题 目已经不允许使用这两种语句了。 我们仍然围绕循环做文章。循环只是让相同的代码执行 n 遍而已,我们完全可以 不用 for 和 while 达到这个效果。比如定义一个类,我们 new 一含有 n 个这种类 型元素的数组,那么该类的构造函数将确定会被调用 n 次。我们可以将需要执行 的代码放到构造函数里。如下代码正是基于这个思路: class Temp { public: Temp() { ++ N; Sum += N; } static void Reset() { N = 0; Sum = 0; } static int GetSum() { return S } private: static int N; static int S }; int Temp::N = 0; int Temp::Sum = 0; int solution1_Sum(int n) { Temp::Reset(); Temp *a = new Temp[n]; delete []a; a = 0; return Temp::GetSum(); } 我们同样也可以围绕递归做文章。既然不能判断是不是应该终止递归,我们不妨 定义两个函数。一个函数充当递归函数的角色,另一个函数处理终止递归的情 况,我们需要做的就是在两个函数里二选一。从二选一我们很自然的想到布尔变 量,比如 ture(1)的时候调用第一个函数,false(0)的时候调用第二个函数。 那现在的问题是如和把数值变量 n 转换成布尔值。如果对 n 连续做两次反运算, 即!!n,那么非零的 n 转换为 true,0 转换为 false。有了上述分析,我们再来看 下面的代码: class A; A* Array[2]; class A { public: virtual int Sum (int n) { return 0; } }; class B: public A { public: virtual int Sum (int n) { return Array[!!n]-&Sum(n-1)+n; } }; int solution2_Sum(int n) { A B Array[0] = &a; Array[1] = &b; int value = Array[1]-&Sum(n); } 这种方法是用虚函数来实现函数的选择。当 n 不为零时,执行函数 B::Sum;当 n 为 0 时,执行 A::Sum。我们也可以直接用函数指针数组,这样可能还更直接 一些: typedef int (*fun)(int); int solution3_f1(int i) { return 0; } int solution3_f2(int i) { fun f[2]={solution3_f1, solution3_f2}; return i+f[!!i](i-1); } 另外我们还可以让编译器帮我们来完成类似于递归的运算,比如如下代码: template &int n& struct solution4_Sum { enum Value { N = solution4_Sum&n - 1&::N + n}; }; template && struct solution4_Sum&1& { enum Value { N = 1}; }; solution4_Sum&100&::N 就 是 1+2+...+100 的 结 果 。 当 编 译 器 看 到 solution4_Sum&100&时,就是为模板类 solution4_Sum 以参数 100 生成 该类型的代码。但以 100 为参数的类型需要得到以 99 为参数的类型,因为 solution4_Sum&100&::N=solution4_Sum&99&::N+100。这个过程会递 归一直到参数为 1 的类型,由于该类型已经显式定义,编译器无需生成,递归编 译到此结束。由于这个过程是在编译过程中完成的,因此要求输入 n 必须是在编 译期间就能确定,不能动态输入。这是该方法最大的缺点。而且编译器对递归编 译代码的递归深度是有限制的,也就是要求 n 不能太大。 大家还有更多、更巧妙的思路吗?欢迎讨论^_^ PS:递归解决 PS:递归解决 int func(int n) { int i=1; (n&1)&&(i=func(n(n&1)&&(i=func(n-1)+n); }程序员面试题精选 100 题(09)-查找链表中倒数第 k 个结点 题目:输入一个单向链表,输出该链表中倒数第 k 个结点。链表的倒数第 0 个结 点为链表的尾指针。链表结点定义如下: struct ListNode { int m_nK ListNode* m_pN }; 分析:为了得到倒数第 k 个结点,很自然的想法是先走到链表的尾端,再从尾端 回溯 k 步。可是输入的是单向链表,只有从前往后的指针而没有从后往前的指 针。因此我们需要打开我们的思路。 既然不能从尾结点开始遍历这个链表,我们还是把思路回到头结点上来。假设整 个链表有 n 个结点,那么倒数第 k 个结点是从头结点开始的第 n-k-1 个结点(从 0 开始计数)。如果我们能够得到链表中结点的个数 n,那我们只要从头结点开 始往后走 n-k-1 步就可以了。如何得到结点数 n?这个不难,只需要从头开始遍 历链表,每经过一个结点,计数器加一就行了。 这种思路的时间复杂度是 O(n),但需要遍历链表两次。第一次得到链表中结点 个数 n,第二次得到从头结点开始的第 n-k-1 个结点即倒数第 k 个结点。 如果链表的结点数不多,这是一种很好的方法。但如果输入的链表的结点个数很 多,有可能不能一次性把整个链表都从硬盘读入物理内存,那么遍历两遍意味着 一个结点需要两次从硬盘读入到物理内存。我们知道把数据从硬盘读入到内存是 非常耗时间的操作。我们能不能把链表遍历的次数减少到 1?如果可以,将能有 效地提高代码执行的时间效率。 如果我们在遍历时维持两个指针,第一个指针从链表的头指针开始遍历,在第 k-1 步之前,第二个指针保持不动;在第 k-1 步开始,第二个指针也开始从链表 的头指针开始遍历。由于两个指针的距离保持在 k-1,当第一个(走在前面的) 指针到达链表的尾结点时,第二个指针(走在后面的)指针正好是倒数第 k 个结 点。 这种思路只需要遍历链表一次。对于很长的链表,只需要把每个结点从硬盘导入 到内存一次。因此这一方法的时间效率前面的方法要高。 思路一的参考代码: ///////////////////////////////////////////////////////// ////////////// // Find the kth node from the tail of a list // Input: pListHead - the head of list // k - the distance to the tail // Output: the kth node from the tail of a list ///////////////////////////////////////////////////////// ////////////// ListNode* FindKthToTail_Solution1(ListNode* pListHead, unsigned int k) { if(pListHead == NULL) return NULL; // count the nodes number in the list ListNode *pCur = pListH unsigned int nNum = 0; while(pCur-&m_pNext != NULL) { pCur = pCur-&m_pN nNum ++; } // if the number of nodes in the list is less than k // do nothing if(nNum & k) return NULL; // the kth node from the tail of a list // is the (n - k)th node from the head pCur = pListH for(unsigned int i = 0; i & nNum - ++ i) pCur = pCur-&m_pN return pC } 思路二的参考代码: ///////////////////////////////////////////////////////// ////////////// // Find the kth node from the tail of a list // Input: pListHead - the head of list // k - the distance to the tail // Output: the kth node from the tail of a list ///////////////////////////////////////////////////////// ////////////// ListNode* FindKthToTail_Solution2(ListNode* pListHead, unsigned int k) { if(pListHead == NULL) return NULL; ListNode *pAhead = pListH ListNode *pBehind = NULL; for(unsigned int i = 0; i & ++ i) { if(pAhead-&m_pNext != NULL) pAhead = pAhead-&m_pN else { // if the number of nodes in the list is less than k, // do nothing return NULL; } } pBehind = pListH // the distance between pAhead and pBehind is k // when pAhead arrives at the tail, p // Behind is at the kth node from the tail while(pAhead-&m_pNext != NULL) { pAhead = pAhead-&m_pN pBehind = pBehind-&m_pN } return pB } 讨论:这道题的代码有大量的指针操作。在软件开发中,错误的指针操作是大部 分问题的根源。因此每个公司都希望程序员在操作指针时有良好的习惯,比如使 用指针之前判断是不是空指针。这些都是编程的细节,但如果这些细节把握得不 好,很有可能就会和心仪的公司失之交臂。 另外,这两种思路对应的代码都含有循环。含有循环的代码经常出的问题是在循 环结束条件的判断。是该用小于还是小于等于?是该用 k 还是该用 k-1?由于题 目要求的是从 0 开始计数,而我们的习惯思维是从 1 开始计数,因此首先要想 好这些边界条件再开始编写代码,再者要在编写完代码之后再用边界值、边界值 减 1、边界值加 1 都运行一次(在纸上写代码就只能在心里运行了)。 扩展:和这道题类似的题目还有:输入一个单向链表。如果该链表的结点数为奇 数,输出中间的结点;如果链表结点数为偶数,输出中间两个结点前面的一个。 如果各位感兴趣,请自己分析并编写代码。 解扩展题的思路和思路二类似吧,也用到两个指针。节点数为奇数时, 解扩展题的思路和思路二类似吧,也用到两个指针。节点数为奇数时,两个指 针初始都指向头结点,然后一个每次跳两个节点,一个每次跳一个节点。 针初始都指向头结点,然后一个每次跳两个节点,一个每次跳一个节点。当第 一个指针到达尾端时后一个指针指向需要的节点。 一个指针到达尾端时后一个指针指向需要的节点。 节点数为偶数时情况基本一样,只是两个指针初始一个指向头结点,一个指向 节点数为偶数时情况基本一样,只是两个指针初始一个指向头结点,一个指向 1 号节点。后一个指针每次跳两个节点,前一个每次只跳一个节点。 号节点。后一个指针每次跳两个节点,前一个每次只跳一个节点。 程序员面试题精选 100 题(10)-在排序数组中查找和为给定值的两个数字 题目:输入一个已经按升序排序过的数组和一个数字,在数组中查找两个数,使 得它们的和正好是输入的那个数字。要求时间复杂度是 O(n)。如果有多对数字 的和等于输入的数字,输出任意一对即可。 例如输入数组 1、2、4、7、11、15 和数字 15。由于 4+11=15,因此输出 4 和 11。 分析:如果我们不考虑时间复杂度,最简单想法的莫过去先在数组中固定一个数 字,再依次判断数组中剩下的 n-1 个数字与它的和是不是等于输入的数字。可惜 这种思路需要的时间复杂度是 O(n2)。 我们假设现在随便在数组中找到两个数。如果它们的和等于输入的数字,那太好 了,我们找到了要找的两个数字;如果小于输入的数字呢?我们希望两个数字的 和再大一点。由于数组已经排好序了,我们是不是可以把较小的数字的往后面移 动一个数字?因为排在后面的数字要大一些,那么两个数字的和也要大一些,就 有可能等于输入的数字了;同样,当两个数字的和大于输入的数字的时候,我们 把较大的数字往前移动,因为排在数组前面的数字要小一些,它们的和就有可能 等于输入的数字了。 我们把前面的思路整理一下:最初我们找到数组的第一个数字和最后一个数字。 当两个数字的和大于输入的数字时,把较大的数字往前移动;当两个数字的和小 于数字时,把较小的数字往后移动;当相等时,打完收工。这样扫描的顺序是从 数组的两端向数组的中间扫描。 问题是这样的思路是不是正确的呢?这需要严格的数学证明。感兴趣的读者可以 自行证明一下。 参考代码: ///////////////////////////////////////////////////////// ////////////// // Find two numbers with a sum in a sorted array // Output: ture is found such two numbers, otherwise false ///////////////////////////////////////////////////////// ////////////// bool FindTwoNumbersWithSum ( int data[], // a sorted array unsigned int length, // the length of the sorted array int sum, int& num1, int& num2 ) { bool found = if(length & 1)// the sum // the first number, output // the second number, outputint ahead = length - 1; int behind = 0; while(ahead & behind) { long long curSum = data[ahead] + data[behind]; // if the sum of two numbers is equal to the input // we have found them if(curSum == sum) { num1 = data[behind]; num2 = data[ahead]; found = } // if the sum of two numbers is greater than the input // decrease the greater number else if(curSum & sum) ahead --; // if the sum of two numbers is less than the input // increase the less number else behind ++; } } 扩展:如果输入的数组是没有排序的,但知道里面数字的范围,其他条件不变, 如和在 O(n)时间里找到这两个数字? =============== 扩展问题是不是先记数排序再用原来的方法? 扩展问题是不是先记数排序再用原来的方法? 如果是这样的话, 如果是这样的话,设数字范围是 d,则时间复杂度应该是 O(max(d,n))是这个思路。 min, max。 max是这个思路。如果记最小值为 min,最大值为 max。新建一个长度为 max-min+1 的数组。 的数组。初始化这个数组的每个元素为 0。扫描原数组每个元素 k,在新数组中 O(n)的时间内 的时间内把原数组转换为一个排好序的 下标为 k-min 的位置加 1,这样在 O(n)的时间内把原数组转换为一个排好序的 数组。接下来的做法一样。 数组。接下来的做法一样。 当然, 更准确一些。谢谢指出。 当然,时间复杂度标记为 O(max(d,n)) 更准确一些。谢谢指出。程序员面试题精选 100 题(11)-求二元查找树的镜像 题目:输入一颗二元查找树,将该树转换为它的镜像,即在转换后的二元查找树 中,左子树的结点都大于右子树的结点。用递归和循环两种方法完成树的镜像转 换。 例如输入: 8 / 6 /\ 5 7 \ 10 /\ 9 11输出: 8 / \ 10 6 /\ /\ 11 9 7 5 定义二元查找树的结点为: struct BSTreeNode // a node in the binary search tree (BST) { int m_nV // value of node BSTreeNode *m_pL // left child of node BSTreeNode *m_pR // right child of node }; 分析:尽管我们可能一下子不能理解镜像是什么意思,但上面的例子给我们的直 观感觉,就是交换结点的左右子树。我们试着在遍历例子中的二元查找树的同时 来交换每个结点的左右子树。遍历时首先访问头结点 8,我们交换它的左右子树 得到: 8 / \ 10 6 /\ /\ 9 11 5 7 我们发现两个结点 6 和 10 的左右子树仍然是左结点的值小于右结点的值,我们 再试着交换他们的左右子树,得到: 8 / \ 10 6 /\ /\ 11 9 7 5 刚好就是要求的输出。 上面的分析印证了我们的直觉:在遍历二元查找树时每访问到一个结点,交换它 的左右子树。这种思路用递归不难实现,将遍历二元查找树的代码稍作修改就可 以了。参考代码如下: ///////////////////////////////////////////////////////// ////////////// // Mirror a BST (swap the left right child of each node) recursively // the head of BST in initial call ///////////////////////////////////////////////////////// ////////////// void MirrorRecursively(BSTreeNode *pNode) { if(!pNode) // swap the right and left child sub-tree BSTreeNode *pTemp = pNode-&m_pL pNode-&m_pLeft = pNode-&m_pR pNode-&m_pRight = pT // mirror left child sub-tree if not null if(pNode-&m_pLeft) MirrorRecursively(pNode-&m_pLeft); // mirror right child sub-tree if not null if(pNode-&m_pRight) MirrorRecursively(pNode-&m_pRight); } 由于递归的本质是编译器生成了一个函数调用的栈,因此用循环来完成同样任务 时最简单的办法就是用一个辅助栈来模拟递归。首先我们把树的头结点放入栈 中。在循环中,只要栈不为空,弹出栈的栈顶结点,交换它的左右子树。如果它 有左子树,把它的左子树压入栈中;如果它有右子树,把它的右子树压入栈中。 这样在下次循环中就能交换它儿子结点的左右子树了。参考代码如下: ///////////////////////////////////////////////////////// ////////////// // Mirror a BST (swap the left right child of each node) Iteratively // Input: pTreeHead: the head of BST ///////////////////////////////////////////////////////// ////////////// void MirrorIteratively(BSTreeNode *pTreeHead) { if(!pTreeHead) std::stack&BSTreeNode*&stackTreeN stackTreeNode.push(pTreeHead); while(stackTreeNode.size()) { BSTreeNode *pNode = stackTreeNode.top(); stackTreeNode.pop(); // swap the right and left child sub-tree BSTreeNode *pTemp = pNode-&m_pL pNode-&m_pLeft = pNode-&m_pR pNode-&m_pRight = pT // push left child sub-tree into stack if not null if(pNode-&m_pLeft) stackTreeNode.push(pNode-&m_pLeft); // push right child sub-tree into stack if not null if(pNode-&m_pRight) stackTreeNode.push(pNode-&m_pRight); } }程序员面试题精选 100 题(12)-从上往下遍历二元树 题目:输入一颗二元树,从上往下按层打印树的每个结点,同一层中按照从左 往右的顺序打印。 例如输入 8 / \ 6 10 /\ /\ 5 7 9 11 输出 8 6 10 5 7 9 11。分析:这曾是微软的一道面试题。这道题实质上是要求遍历一棵二元树,只不过 不是我们熟悉的前序、中序或者后序遍历。 我们从树的根结点开始分析。自然先应该打印根结点 8,同时为了下次能够打印 8 的两个子结点,我们应该在遍历到 8 时把子结点 6 和 10 保存到一个数据容器 中。现在数据容器中就有两个元素 6 和 10 了。按照从左往右的要求,我们先取 出 6 访问。打印 6 的同时要把 6 的两个子结点 5 和 7 放入数据容器中,此时数 据容器中有三个元素 10、5 和 7。接下来我们应该从数据容器中取出结点 10 访 问了。注意 10 比 5 和 7 先放入容器,此时又比 5 和 7 先取出,就是我们通常说 的先入先出。因此不难看出这个数据容器的类型应该是个队列。 既然已经确定数据容器是一个队列,现在的问题变成怎么实现队列了。实际上我 们无需自己动手实现一个,因为 STL 已经为我们实现了一个很好的 deque(两 端都可以进出的队列),我们只需要拿过来用就可以了。 我们知道树是图的一种特殊退化形式。同时如果对图的深度优先遍历和广度优先 遍历有比较深刻的理解,将不难看出这种遍历方式实际上是一种广度优先遍历。 因此这道题的本质是在二元树上实现广度优先遍历。 参考代码: #include &deque& #include &iostream& struct BTreeNode // a node in the binary tree { int m_nV // value of node BTreeNode *m_pL // left child of node BTreeNode *m_pR // right child of node }; ///////////////////////////////////////////////////////// ////////////// // Print a binary tree from top level to bottom level // Input: pTreeRoot - the root of binary tree ///////////////////////////////////////////////////////// ////////////// void PrintFromTopToBottom(BTreeNode *pTreeRoot) { if(!pTreeRoot) // get a empty queue deque&BTreeNode *& dequeTreeN // insert the root at the tail of queue dequeTreeNode.push_back(pTreeRoot); while(dequeTreeNode.size()) { // get a node from the head of queue BTreeNode *pNode = dequeTreeNode.front(); dequeTreeNode.pop_front(); // print the node cout && pNode-&m_nValue && ' '; // print its left child sub-tree if it has if(pNode-&m_pLeft) dequeTreeNode.push_back(pNode-&m_pLeft); // print its right child sub-tree if it has if(pNode-&m_pRight) dequeTreeNode.push_back(pNode-&m_pRight); } } PS:层序二叉树,用队列实现! PS:层序二叉树,用队列实现!程序员面试题精选 100 题(13)-第一个只出现一次的字符 题目:在一个字符串中找到第一个只出现一次的字符。如输入 abaccdeff,则输 出 b。 分析:这道题是 2006 年 google 的一道笔试题。 看到这道题时,最直观的想法是从头开始扫描这个字符串中的每个字符。当访问 到某字符时拿这个字符和后面的每个字符相比较,如果在后面没有发现重复的字 符,则该字符就是只出现一次的字符。如果字符串有 n 个字符,每个字符可能与 后面的 O(n)个字符相比较,因此这种思路时间复杂度是 O(n2)。我们试着去找一 个更快的方法。 由于题目与字符出现的次数相关,我们是不是可以统计每个字符在该字符串中出 现的次数?要达到这个目的,我们需要一个数据容器来存放每个字符的出现次 数。在这个数据容器中可以根据字符来查找它出现的次数,也就是说这个容器的 作用是把一个字符映射成一个数字 在常用的数据容器中 哈希表正是这个用途 。 , 。 哈希表是一种比较复杂的数据结构。由于比较复杂,STL 中没有实现哈希表,因 此需要我们自己实现一个。但由于本题的特殊性,我们只需要一个非常简单的哈 希表就能满足要求。由于字符(char)是一个长度为 8 的数据类型,因此总共有 可能 256 种可能 于是我们创建一个长度为 256 的数组 每个字母根据其 ASCII 。 , 码值作为数组的下标对应数组的对应项,而数组中存储的是每个字符对应的次 数。这样我们就创建了一个大小为 256,以字符 ASCII 码为键值的哈希表。 我们第一遍扫描这个数组时,每碰到一个字符,在哈希表中找到对应的项并把出 现的次数增加一次。这样在进行第二次扫描时,就能直接从哈希表中得到每个字 符出现的次数了。 参考代码如下: ///////////////////////////////////////////////////////// ////////////// // Find the first char which appears only once in a string // Input: pString - the string // Output: the first not repeating char if the string has, otherwise 0 ///////////////////////////////////////////////////////// ////////////// char FirstNotRepeatingChar(char* pString) { // invalid input if(!pString) return 0; // get a hash table, and initialize it constinttableSize =256; unsignedinthashTable[tableSize]; for(unsignedinti = 0; i&tableS ++ i) hashTable[i] = 0; // get the how many times each char appears in the string char* pHashKey = pS while(*(pHashKey) != '\0') hashTable[*(pHashKey++)] ++; // find the first char which appears only once in a string pHashKey = pS while(*pHashKey != '\0') { if(hashTable[*pHashKey] == 1) return *pHashK pHashKey++; } // if the string is empty // or every char in the string appears at least twice return 0; } 程序员面试题精选 100 题(14)-圆圈中最后剩下的数字 题目:n 个数字(0,1,…,n-1)形成一个圆圈,从数字 0 开始,每次从这个圆圈 中删除第 m 个数字 (第一个为当前数字本身 第二个为当前数字的下一个数字) , 。 当一个数字删除后,从被删除数字的下一个继续删除第 m 个数字。求出在这个 圆圈中剩下的最后一个数字。 分析:既然题目有一个数字圆圈,很自然的想法是我们用一个数据结构来模拟这 个圆圈。在常用的数据结构中,我们很容易想到用环形列表。我们可以创建一个 总共有 m 个数字的环形列表,然后每次从这个列表中删除第 m 个元素。 在参考代码中,我们用 STL 中 std::list 来模拟这个环形列表。由于 list 并不是一 个环形的结构,因此每次跌代器扫描到列表末尾的时候,要记得把跌代器移到列 表的头部。这样就是按照一个圆圈的顺序来遍历这个列表了。 这种思路需要一个有 n 个结点的环形列表来模拟这个删除的过程 因此内存开销 , 为 O(n)。而且这种方法每删除一个数字需要 m 步运算,总共有 n 个数字,因此 总的时间复杂度是 O(mn)。当 m 和 n 都很大的时候,这种方法是很慢的。 接下来我们试着从数学上分析出一些规律。首先定义最初的 n 个数字 (0,1,…,n-1)中最后剩下的数字是关于 n 和 m 的方程为 f(n,m)。 在这 n 个数字中,第一个被删除的数字是 m%n-1,为简单起见记为 k。那么删 除 k 之后的剩下 n-1 的数字为 0,1,…,k-1,k+1,…,n-1,并且下一个开始计数的数 字 是 k+1 。 相 当 于 在 剩 下 的 序 列 中 , k+1 排 到 最 前 面 , 从 而 形 成 序 列 k+1,…,n-1,0,…k-1。该序列最后剩下的数字也应该是关于 n 和 m 的函数。由于 这个序列的规律和前面最初的序列不一样(最初的序列是从 0 开始的连续序 列)因此该函数不同于前面函数 记为 f’(n-1,m) 最初序列最后剩下的数字 f(n,m) , , 。 一定是剩下序列的最后剩下数字 f’(n-1,m),所以 f(n,m)=f’(n-1,m)。 接下来我们把剩下的的这 n-1 个数字的序列 k+1,…,n-1,0,…k-1 作一个映射,映 射的结果是形成一个从 0 到 n-2 的序列: k+1 -& 0 k+2 -& 1 … n-1 -& n-k-2 0 -& n-k-1 … k-1 -& n-2 把映射定义为 p,则 p(x)= (x-k-1)%n,即如果映射前的数字是 x,则映射后的数 字是(x-k-1)%n。对应的逆映射是 p-1(x)=(x+k+1)%n。 由于映射之后的序列和最初的序列有同样的形式,都是从 0 开始的连续序列,因 此仍然可以用函数 f 来表示,记为 f(n-1,m)。根据我们的映射规则,映射之前的 序列最后剩下的数字 f’(n-1,m)= p-1 [f(n-1,m)]=[f(n-1,m)+k+1]%n。把 k=m%n-1 代入得到 f(n,m)=f’(n-1,m)=[f(n-1,m)+m]%n。 经过上面复杂的分析,我们终于找到一个递归的公式。要得到 n 个数字的序列的 最后剩下的数字,只需要得到 n-1 个数字的序列的最后剩下的数字,并可以依此 类推。当 n=1 时,也就是序列中开始只有一个数字 0,那么很显然最后剩下的数 字就是 0。我们把这种关系表示为: 0 f(n,m)={ [f(n-1,m)+m]%n n&1 n=1尽管得到这个公式的分析过程非常复杂,但它用递归或者循环都很容易实现。最 重要的是,这是一种时间复杂度为 O(n),空间复杂度为 O(1)的方法,因此无论 在时间上还是空间上都优于前面的思路。 思路一的参考代码: ///////////////////////////////////////////////////////// ////////////// // n integers (0, 1, ... n - 1) form a circle. Remove the mth from // the circle at every time. Find the last number remaining // Input: n - the number of integers in the circle initially // m - remove the mth number at every time // Output: the last number remaining when the input is valid, // otherwise -1 ///////////////////////////////////////////////////////// ////////////// int LastRemaining_Solution1(unsigned int n, unsigned int m) { // invalid input if(n & 1 || m & 1) return -1; unsigned int i = 0; // initiate a list with n integers (0, 1, ... n - 1) list&int& for(i = 0; i & ++ i) integers.push_back(i); list&int&::iterator curinteger = integers.begin(); while(integers.size() & 1) { // find the mth integer. Note that std::list is not a circle // so we should handle it manually for(int i = 1; i & ++ i) { curinteger ++; if(curinteger == integers.end()) curinteger = integers.begin(); } // remove the mth integer. Note that std::list is not a circle // so we should handle it manually list&int&::iterator nextinteger = ++ if(nextinteger == integers.end()) nextinteger = integers.begin(); -- integers.erase(curinteger); curinteger = } return *(curinteger); } 思路二的参考代码: ///////////////////////////////////////////////////////// ////////////// // n integers (0, 1, ... n - 1) form a circle. Remove the mth from // the circle at every time. Find the last number remaining // Input: n - the number of integers in the circle initially // m - remove the mth number at every time // Output: the last number remaining when the input is valid, // otherwise -1 ///////////////////////////////////////////////////////// ////////////// int LastRemaining_Solution2(int n, unsigned int m) { // invalid input if(n &= 0 || m & 0) return -1; // if there are only one integer in the circle initially, // of course the last remaining one is 0 int lastinteger = 0; // find the last remaining one in the circle with n integers for (int i = 2; i &= i ++) lastinteger = (lastinteger + m) % } 如果对两种思路的时间复杂度感兴趣的读者可以把 n 和 m 的值设的稍微大一 点,比如十万这个数量级的数字,运行的时候就能明显感觉出这两种思路写出来 的代码时间效率大不一样。程序员面试题精选 100 题(15)-含有指针成员的类的拷贝 题目:下面是一个数组类的声明与实现。请分析这个类有什么问题,并针对存在 的问题提出几种解决方案。 template&typename T& class Array { public: Array(unsigned arraySize):data(0), size(arraySize) { if(size & 0) data = new T[size]; } ~Array() { if(data) delete[] } void setValue(unsigned index, const T& value) { if(index & size) data[index] = } T getValue(unsigned index) const { if(index & size) return data[index]; else return T(); } private: T* }; 分析:我们注意在类的内部封装了用来存储数组数据的指针。软件存在的大部分 问题通常都可以归结指针的不正确处理。 这个类只提供了一个构造函数,而没有定义构造拷贝函数和重载拷贝运算符函 数。当这个类的用户按照下面的方式声明并实例化该类的一个实例 Array A(10); Array B(A); 或者按照下面的方式把该类的一个实例赋值给另外一个实例 Array A(10); Array B(10); B=A; 编译器将调用其自动生成的构造拷贝函数或者拷贝运算符的重载函数。在编译器 生成的缺省的构造拷贝函数和拷贝运算符的重载函数,对指针实行的是按位拷 贝,仅仅只是拷贝指针的地址,而不会拷贝指针的内容。因此在执行完前面的代 码之后,A.data 和 B.data 指向的同一地址。当 A 或者 B 中任意一个结束其生命 周期调用析构函数时,会删除 data。由于他们的 data 指向的是同一个地方,两 个实例的 data 都被删除了。但另外一个实例并不知道它的 data 已经被删除了, 当企图再次用它的 data 的时候,程序就会不可避免地崩溃。 由于问题出现的根源是调用了编译器生成的缺省构造拷贝函数和拷贝运算符的 重载函数。一个最简单的办法就是禁止使用这两个函数。于是我们可以把这两个 函数声明为私有函数,如果类的用户企图调用这两个函数,将不能通过编译。实 现的代码如下: private: Array(const Array& copy); const Array& operator = (const Array& copy); 最初的代码存在问题是因为不同实例的 data 指向的同一地址,删除一个实例的 data 会把另外一个实例的 data 也同时删除。因此我们还可以让构造拷贝函数或 者拷贝运算符的重载函数拷贝的不只是地址,而是数据。由于我们重新存储了一 份数据,这样一个实例删除的时候,对另外一个实例没有影响。这种思路我们称 之为深度拷贝。实现的代码如下: public: Array(const Array& copy):data(0), size(copy.size) { if(size & 0) { data = new T[size]; for(int i = 0; i & ++ i) setValue(i, copy.getValue(i)); } } const Array& operator = (const Array& copy) { if(this == &copy) return * if(data != NULL) { delete [] data = NULL; } size = copy. if(size & 0) { data = new T[size]; for(int i = 0; i & ++ i) setValue(i, copy.getValue(i)); } } 为了防止有多个指针指向的数据被多次删除,我们还可以保存究竟有多少个指针 指向该数据。只有当没有任何指针指向该数据的时候才可以被删除。这种思路通 常被称之为引用计数技术。在构造函数中,引用计数初始化为 1;每当把这个实 例赋值给其他实例或者以参数传给其他实例的构造拷贝函数的时候,引用计数加 1,因为这意味着又多了一个实例指向它的 data;每次需要调用析构函数或者需 要把 data 赋值为其他数据的时候 引用计数要减 1 因为这意味着指向它的 data , , 的指针少了一个。当引用计数减少到 0 的时候,data 已经没有任何实例指向它 了,这个时候就可以安全地删除。实现的代码如下: public: Array(unsigned arraySize) :data(0), size(arraySize), count(new unsigned int) { *count = 1; if(size & 0) data = new T[size]; } Array(const Array& copy) : size(copy.size), data(copy.data), count(copy.count) { ++ (*count); } ~Array() { Release(); } const Array& operator = (const Array& copy) { if(data == copy.data) return * Release(); data = copy. size = copy. count = copy. ++(*count); } private: void Release() { --(*count); if(*count == 0) { if(data) { delete [] data = NULL; } count = 0; } } unsigned int * PS:拷贝构造函数和重载拷贝运算符函数,深复制,避免调用缺省(浅复制), PS:拷贝构造函数和重载拷贝运算符函数,深复制,避免调用缺省(浅复制), 两对象指向同一地址,删除一个时出现错误! 两对象指向同一地址,删除一个时出现错误!程序员面试题精选 100 题(16)-O(logn)求 Fibonacci 数列 题目:定义 Fibonacci 数列如下: / f(n)= \ 0 1 f(n-1)+f(n-2) n=0 n=1 n=2输入 n,用最快的方法求该数列的第 n 项。 分析 在很多 C 语言教科书中讲到递归函数的时候 都会用 Fibonacci 作为例子 : , 。 因此很多程序员对这道题的递归解法非常熟悉,看到题目就能写出如下的递归求 解的代码。 ///////////////////////////////////////////////////////// ////////////// // Calculate the nth item of Fibonacci Series recursively ///////////////////////////////////////////////////////// ////////////// long long Fibonacci_Solution1(unsigned int n) { int result[2] = {0, 1}; if(n & 2) return result[n]; return Fibonacci_Solution1(n - 1) + Fibonacci_Solution1(n - 2); } 但是,教科书上反复用这个题目来讲解递归函数,并不能说明递归解法最适合这 道题目。我们以求解 f(10)作为例子来分析递归求解的过程。要求得 f(10),需要 求得 f(9)和 f(8)。同样,要求得 f(9),要先求得 f(8)和 f(7)……我们用树形结构来 表示这种依赖关系 f(10) / \ f(9) f(8) / \ / \ f(8) f(7) f(6) f(5) / \ / \ f(7) f(6) f(6) f(5) 我们不难发现在这棵树中有很多结点会重复的 而且重复的结点数会随着 n 的增 , 大而急剧增加。这意味这计算量会随着 n 的增大而急剧增大。事实上,用递归方 法计算的时间复杂度是以 n 的指数的方式递增的。大家可以求 Fibonacci 的第 100 项试试,感受一下这样递归会慢到什么程度。在我的机器上,连续运行了一 个多小时也没有出来结果。 其实改进的方法并不复杂。上述方法之所以慢是因为重复的计算太多,只要避免 重复计算就行了。比如我们可以把已经得到的数列中间项保存起来,如果下次需 要计算的时候我们先查找一下,如果前面已经计算过了就不用再次计算了。 更简单的办法是从下往上计算,首先根据 f(0)和 f(1)算出 f(2),在根据 f(1)和 f(2) 算出 f(3)……依此类推就可以算出第 n 项了。很容易理解,这种思路的时间复杂 度是 O(n)。 ///////////////////////////////////////////////////////// ////////////// // Calculate the nth item of Fibonacci Series iteratively ///////////////////////////////////////////////////////// ////////////// long long Fibonacci_Solution2(unsigned n) { int result[2] = {0, 1}; if(n & 2) return result[n]; long long fibNMinusOne = 1; long long fibNMinusTwo = 0; long long fibN = 0; for(unsigned int i = 2; i &= ++ i) { fibN = fibNMinusOne + fibNMinusT fibNMinusTwo = fibNMinusO fibNMinusOne = fibN; } return fibN; } 这还不是最快的方法。下面介绍一种时间复杂度是 O(logn)的方法。在介绍这种 方法之前,先介绍一个数学公式: {f(n), f(n-1), f(n-1), f(n-2)} ={1, 1, 1,0}n-1 (注:{f(n+1), f(n), f(n), f(n-1)}表示一个矩阵。在矩阵中第一行第一列是 f(n+1), 第一行第二列是 f(n),第二行第一列是 f(n),第二行第二列是 f(n-1)。) 有了这个公式,要求得 f(n),我们只需要求得矩阵{1, 1, 1,0}的 n-1 次方,因为矩 阵{1, 1, 1,0}的 n-1 次方的结果的第一行第一列就是 f(n)。这个数学公式用数学归 纳法不难证明。感兴趣的朋友不妨自己证明一下。 现在的问题转换为求矩阵{1, 1, 1, 0}的乘方。如果简单第从 0 开始循环,n 次方 将需要 n 次运算,并不比前面的方法要快。但我们可以考虑乘方的如下性质: / an/2*an/2 a= \ a(n-1)/2*a(n-1)/2 n 为奇数时nn 为偶数时要求得 n 次方,我们先求得 n/2 次方,再把 n/2 的结果平方一下。如果把求 n 次 方的问题看成一个大问题,把求 n/2 看成一个较小的问题。这种把大问题分解成 一个或多个小问题的思路我们称之为分治法。这样求 n 次方就只需要 logn 次运 算了。 实现这种方式时,首先需要定义一个 2×2 的矩阵,并且定义好矩阵的乘法以及 乘方运算。当这些运算定义好了之后,剩下的事情就变得非常简单。完整的实现 代码如下所示。 #include &cassert& ///////////////////////////////////////////////////////// ////////////// // A 2 by 2 matrix ///////////////////////////////////////////////////////// ////////////// struct Matrix2By2 { Matrix2By2 ( long long m00 = 0, long long m01 = 0, long long m10 = 0, long long m11 = 0 ) :m_00(m00), m_01(m01), m_10(m10), m_11(m11) { } long long long long }; ///////////////////////////////////////////////////////// ////////////// // Multiply two matrices // Input: matrix1 - the first matrix // matrix2 - the second matrix //Output: the production of two matrices ///////////////////////////////////////////////////////// ////////////// Matrix2By2 MatrixMultiply ( const Matrix2By2& matrix1, const Matrix2By2& matrix2 ) { return Matrix2By2( matrix1.m_00 * matrix2.m_00 + matrix1.m_01 * matrix2.m_10, matrix1.m_00 * matrix2.m_01 + matrix1.m_01 * matrix2.m_11, matrix1.m_10 * matrix2.m_00 + matrix1.m_11 * matrix2.m_10, matrix1.m_10 * matrix2.m_01 + matrix1.m_11 * long long long long m_00; m_01; m_10; m_11; matrix2.m_11); } ///////////////////////////////////////////////////////// ////////////// // The nth power of matrix // 1 1 // 1 0 ///////////////////////////////////////////////////////// ////////////// Matrix2By2 MatrixPower(unsigned int n) { assert(n & 0); Matrix2By2 if(n == 1) { matrix = Matrix2By2(1, 1, 1, 0); } else if(n % 2 == 0) { matrix = MatrixPower(n / 2); matrix = MatrixMultiply(matrix, matrix); } else if(n % 2 == 1) { matrix = MatrixPower((n - 1) / 2); matrix = MatrixMultiply(matrix, matrix); matrix = MatrixMultiply(matrix, Matrix2By2(1, 1, 1, 0)); } } ///////////////////////////////////////////////////////// ////////////// // Calculate the nth item of Fibonacci Series using devide and conquer ///////////////////////////////////////////////////////// ////////////// long long Fibonacci_Solution3(unsigned int n) { int result[2] = {0, 1}; if(n & 2) return result[n]; Matrix2By2 PowerNMinus2 = MatrixPower(n - 1); return PowerNMinus2.m_00; }程序员面试题精选 100 题(17)-把字符串转换成整数 题目:输入一个表示整数的字符串,把该字符串转换成整数并输出。例如输入 字符串&345&,则输出整数 345。 分析:这道题尽管不是很难,学过 C/C++语言一般都能实现基本功能,但不同 程序员就这道题写出的代码有很大区别,可以说这道题能够很好地反应出程序员 的思维和编程习惯,因此已经被包括微软在内的多家公司用作面试题。建议读者 在往下看之前自己先编写代码,再比较自己写的代码和下面的参考代码有哪些不 同。 首先我们分析如何完成基本功能,即如何把表示整数的字符串正确地转换成整 数。还是以&345&作为例子。当我们扫描到字符串的第一个字符'3'时,我们不知 道后面还有多少位,仅仅知道这是第一位,因此此时得到的数字是 3。当扫描到 第二个数字'4'时,此时我们已经知道前面已经一个 3 了,再在后面加上一个数 字 4,那前面的 3 相当于 30,因此得到的数字是 3*10+4=34。接着我们又扫描 到字符'5',我们已经知道了'5'的前面已经有了 34,由于后面要加上一个 5,前 面的 34 就相当于 340 了,因此得到的数字就是 34*10+5=345。 分析到这里,我们不能得出一个转换的思路:每扫描到一个字符,我们把在之前 得到的数字乘以 10 再加上当前字符表示的数字。这个思路用循环不难实现。 由于整数可能不仅仅之含有数字,还有可能以'+'或者'-'开头,表示整数的正负。 因此我们需要把这个字符串的第一个字符做特殊处理。如果第一个字符是'+' 号,则不需要做任何操作;如果第一个字符是'-'号,则表明这个整数是个负数, 在最后的时候我们要把得到的数值变成负数。 接着我们试着处理非法输入。由于输入的是指针,在使用指针之前,我们要做的 第一件是判断这个指针是不是为空。如果试着去访问空指针,将不可避免地导致 程序崩溃。另外,输入的字符串中可能含有不是数字的字符。每当碰到这些非法 的字符,我们就没有必要再继续转换。最后一个需要考虑的问题是溢出问题。由 于输入的数字是以字符串的形式输入,因此有可能输入一个很大的数字转换之后 会超过能够表示的最大的整数而溢出。 现在已经分析的差不多了,开始考虑编写代码。首先我们考虑如何声明这个函 数。由于是把字符串转换成整数,很自然我们想到: int StrToInt(const char* str); 这样声明看起来没有问题。但当输入的字符串是一个空指针或者含有非法的字符 时,应该返回什么值呢?0 怎么样?那怎么区分非法输入和字符串本身就是”0” 这两种情况呢? 接下来我们考虑另外一种思路。我们可以返回一个布尔值来指示输入是否有效, 而把转换后的整数放到参数列表中以引用或者指针的形式传入。于是我们就可以 声明如下: bool StrToInt(const char *str, int& num); 这种思路解决了前面的问题。但是这个函数的用户使用这个函数的时候会觉得不 是很方便,因为他不能直接把得到的整数赋值给其他整形变脸,显得不够直观。 前面的第一种声明就很直观。如何在保证直观的前提下当碰到非法输入的时候通 知用户呢?一种解决方案就是定义一个全局变量,每当碰到非法输入的时候,就 标记该全局变量。用户在调用这个函数之后,就可以检验该全局变量来判断转换 是不是成功。 下面我们写出完整的实现代码。参考代码: enum Status {kValid = 0, kInvalid}; int g_nStatus = kV ///////////////////////////////////////////////////////// ////////////// // Convert a string into an integer ///////////////////////////////////////////////////////// ////////////// int StrToInt(const char* str) { g_nStatus = kI longlongnum = 0; if(str != NULL) { const char* digit = // the first char in the string maybe '+' or '-' bool minus = if(*digit == '+') digit ++; else if(*digit == '-') { digit ++; minus = } // the remaining chars in the string while(*digit != '\0') { if(*digit &= '0' && *digit &= '9') { num = num * 10 + (*digit - '0'); // overflow if(num&std::numeric_limits&int&::max()) { num = 0; } digit++; } // if the char is not a digit, invalid input else { num = 0; } } if(*digit == '\0') { g_nStatus = kV if(minus) num = 0 - } } return static_cast&int&(num); } 讨论:在参考代码中,我选用的是第一种声明方式。不过在面试时,我们可以选 用任意一种声明方式进行实现。但当面试官问我们选择的理由时,我们要对两者 的优缺点进行评价。第一种声明方式对用户而言非常直观,但使用了全局变量, 不够优雅;而第二种思路是用返回值来表明输入是否合法,在很多 API 中都用 这种方法,但该方法声明的函数使用起来不够直观。 最后值得一提的是,在 C 语言提供的库函数中,函数 atoi 能够把字符串转换整 数。它的声明是 int atoi(const char *str)。该函数就是用一个全局变 量来标志输入是否合法的。程序员面试题精选 100 题(18)-用两个栈实现队列 题目:某队列的声明如下: template&typename T& class CQueue { public: CQueue() {} ~CQueue() {} void appendTail(const T& node); // append a element to tail void deleteHead(); // remove a element from head private: T&m_stack1; T&m_stack2; };分析:从上面的类的声明中,我们发现在队列中有两个栈。因此这道题实质上是 要求我们用两个栈来实现一个队列。相信大家对栈和队列的基本性质都非常了解 了:栈是一种后入先出的数据容器,因此对队列进行的插入和删除操作都是在栈 顶上进行;队列是一种先入先出的数据容器,我们总是把新元素插入到队列的尾 部,而从队列的头部删除元素。 我们通过一个具体的例子来分析往该队列插入和删除元素的过程。首先插入一个 元素 a,不妨把先它插入到 m_stack1。这个时候 m_stack1 中的元素有{a}, m_stack2 为空 再插入两个元素 b 和 c 还是插入到 m_stack1 中 此时 m_stack1 。 , , 中的元素有{a,b,c},m_stack2 中仍然是空的。 这个时候我们试着从队列中删除一个元素。按照队列先入先出的规则,由于 a 比 b、c 先插入到队列中,这次被删除的元素应该是 a。元素 a 存储在 m_stack1 中,但并不在栈顶上,因此不能直接进行删除。注意到 m_stack2 我们还一直没 有使用过,现在是让 m_stack2 起作用的时候了。如果我们把 m_stack1 中的元 素逐个 pop 出来并 push 进入 m_stack2,元素在 m_stack2 中的顺序正好和原 来在 m_stack1 中的顺序相反。因此经过两次 pop 和 push 之后,m_stack1 为 空,而 m_stack2 中的元素是{c,b,a}。这个时候就可以 pop 出 m_stack2 的栈顶 a 了。pop 之后的 m_stack1 为空,而 m_stack2 的元素为{c,b},其中 b 在栈顶。 这个时候如果我们还想继续删除应该怎么办呢?在剩下的两个元素中 b 和 c,b 比 c 先进入队列,因此 b 应该先删除。而此时 b 恰好又在栈顶上,因此可以直 接 pop 出去。这次 pop 之后,m_stack1 中仍然为空,而 m_stack2 为{c}。 从上面的分析我们可以总结出删除一个元素的步骤:当 m_stack2 中不为空时, 在 m_stack2 中的栈顶元素是最先进入队列的元素,可以 pop 出去。如果 m_stack2 为空时,我们把 m_stack1 中的元素逐个 pop 出来并 push 进入 m_stack2。由于先进入队列的元素被压到 m_stack1 的底端,经过 pop 和 push 之后就处于 m_stack2 的顶端了,又可以直接 pop 出去。 接下来我们再插入一个元素 d。我们是不是还可以把它 push 进 m_stack1?这样 会不会有问题呢?我们说不会有问题。因为在删除元素的时候,如果 m_stack2 中不为空,处于 m_stack2 中的栈顶元素是最先进入队列的,可以直接 pop;如 果 m_stack2 为空 我们把 m_stack1 中的元素 pop 出来并 push 进入 m_stack2 , 。 由于 m_stack2 中元素的顺序和 m_stack1 相反,最先进入队列的元素还是处于 m_stack2 的栈顶,仍然可以直接 pop。不会出现任何矛盾。 我们用一个表来总结一下前面的例子执行的步骤: 操作 append a append b append c delete head delete head append d delete head m_stack1 {a} {a,b} {a,b,c} {} {} {d} {d} m_stack2 {} {} {} {b,c} {c} {c} {}总结完 push 和 pop 对应的过程之后,我们可以开始动手写代码了。参考代码如 下: ///////////////////////////////////////////////////////// ////////////// // Append a element at the tail of the queue ///////////////////////////////////////////////////////// ////////////// template&typename T& void CQueue&T&::appendTail(const T& element) { // push the new element into m_stack1 m_stack1.push(element); } ///////////////////////////////////////////////////////// ////////////// // Delete the head from the queue ///////////////////////////////////////////////////////// ////////////// template&typename T& void CQueue&T&::deleteHead() { // if m_stack2is empty,and there are some //elements inm_stack1, push them in m_stack2 if(m_stack2.size()&= 0) { while(m_stack1.size()&0) { T&data =m_stack1.top(); m_stack1.pop(); m_stack2.push(data); } } // push theelement into m_stack2 assert(m_stack2.size()&0); m_stack2.pop(); } 扩展:这道题是用两个栈实现一个队列。反过来能不能用两个队列实现一个栈? 如果可以,该如何实现? 程序员面试题精选 100 题(19)-反转链表 题目:输入一个链表的头结点,反转该链表,并返回反转后链表的头结点。链表 结点定义如下: struct ListNode { int m_nK ListNode* m_pN }; 分析:这是一道广为流传的微软面试题。由于这道题能够很好的反应出程序员思 维是否严密,在微软之后已经有很多公司在面试时采用了这道题。 为了正确地反转一个链表,需要调整指针的指向。与指针操作相关代码总是容易 出错的,因此最好在动手写程序之前作全面的分析。在面试的时候不急于动手而 是一开始做仔细的分析和设计,将会给面试官留下很好的印象,因为在实际的软 件开发中,设计的时间总是比写代码的时间长。与其很快地写出一段漏洞百出的 代码,远不如用较多的时间写出一段健壮的代码。 为了将调整指针这个复杂的过程分析清楚,我们可以借助图形来直观地分析。假 设下图中 l、m 和 n 是三个相邻的结点: a b … l m n …假设经过若干操作,我们已经把结点 l 之前的指针调整完毕,这些结点的 m_pNext 指针都指向前面一个结点。现在我们遍历到结点 m。当然,我们需要 把调整结点的 m_pNext 指针让它指向结点 l。但注意一旦调整了指针的指向, 链表就断开了,如下图所示: a b …l m n …因为已经没有指针指向结点 n,我们没有办法再遍历到结点 n 了。因此为了避免 链表断开,我们需要在调整 m 的 m_pNext 之前要把 n 保存下来。 接下来我们试着找到反转后链表的头结点。不难分析出反转后链表的头结点是原 始链表的尾位结点。什么结点是尾结点?就是 m_pNext 为空指针的结点。 基于上述分析,我们不难写出如下代码: ///////////////////////////////////////////////////////// ////////////// // Reverse a list iteratively // Input: pHead - the head of the original list // Output: the head of the reversed head ///////////////////////////////////////////////////////// ////////////// ListNode* ReverseIteratively(ListNode* pHead) { ListNode* pReversedHead = NULL; ListNode* pNode = pH ListNode* pPrev = NULL; while(pNode != NULL) { // get the next node, and save it at pNext ListNode* pNext = pNode-&m_pN // if the next node is null, the currect is the end of original // list, and it's the head of the reversed list if(pNext == NULL) pReversedHead = pN // reverse the linkage between nodes pNode-&m_pNext = pP // move forward on the the list pPrev = pN pNode = pN } return pReversedH } 扩展:本题也可以递归实现。感兴趣的读者请自己编写递归代码。程序员面试题精选 100 题(20)-最长公共子串 题目:如果字符串一的所有字符按其在字符串中的顺序出现在另外一个字符串二 中,则字符串一称之为字符串二的子串。注意,并不要求子串(字符串一)的字 符必须连续出现在字符串二中。请编写一个函数,输入两个字符串,求它们的最 长公共子串,并打印出最长公共子串。 例如:输入两个字符串 BDCABA 和 ABCBDAB,字符串 BCBA 和 BDAB 都是是 它们的最长公共子串,则输出它们的长度 4,并打印任意一个子串。 分析:求最长公共子串(Longest Common Subsequence, LCS)是一道非常经 典的动态规划题 因此一些重视算法的公司像 MicroStrategy 都把它当作面试题 , 。 完整介绍动态规划将需要很长的篇幅,因此我不打算在此全面讨论动态规划相关 的概念,只集中对 LCS 直接相关内容作讨论。如果对动态规划不是很熟悉,请 参考相关算法书比如算法讨论。 先介绍 LCS 问题的性质:记 Xm={x0, x1,…xm-1}和 Yn={y0,y1,…,yn-1}为两个字符 串,而 Zk={z0,z1,…zk-1}是它们的 LCS,则: 1. 2. 3. 如果 xm-1=yn-1,那么 zk-1=xm-1=yn-1,并且 Zk-1 是 Xm-1 和 Yn-1 的 LCS; 如果 xm-1≠yn-1,那么当 zk-1≠xm-1 时 Z 是 Xm-1 和 Y 的 LCS; 如果 xm-1≠yn-1,那么当 zk-1≠yn-1 时 Z 是 Yn-1 和 X 的 LCS;下面简单证明一下这些性质: 1. 如果 zk-1≠xm-1,那么我们可以把 xm-1(yn-1)加到 Z 中得到 Z’,这样就得 到 X 和 Y 的一个长度为 k+1 的公共子串 Z’。这就与长度为 k 的 Z}

我要回帖

更多关于 收入减去支出等于什么 的文章

更多推荐

版权声明:文章内容来源于网络,版权归原作者所有,如有侵权请点击这里与我们联系,我们将及时删除。

点击添加站长微信