《算法设计手册》面试题解答 第三章:数据结构
3-18.
你查字典时用的是什么方法?
解析:
既然要和算法相关,除了随机尝试,明显应该用二分查找。
不要嫌它简单,其实这是道MS和Google用过的面试题。
3-19.
假设你有一个装满T恤衫的衣柜,如何组织这些T恤衫以便以后取衣服?
解析:
由于衣柜是线性存储,肯定要用到排序从而应用二分查找。具体到T恤衫,可以按颜色排序。
当然,如果这是个商场中的衣柜,那么还应该在同样式内部按尺码排序。
3-20.
写一个函数,找出单链表的中间结点。
解析:
比较常见,最简单的解法是用两个指针,一个每次后移1个,另一个每次后移2个,速度快的达到末尾时,慢的正好在中间。实际编写的时候注意边界条件(最好把NULL做第0个结点,考虑结点数奇偶性不要把快的指针移动出界),略。
3-21.
写一个函数判断两个二叉树是否全等。树全等包括结构相同和对应结点数据相同。
解析:
二叉树遍历的变形,只是写函数返回值时可能迷惑。可以这样写:
int compare(struct node* a, struct node* b) { // 1. both empty -> true if (a==NULL && b==NULL) return(true); // 2. both non-empty -> compare them else if (a!=NULL && b!=NULL) { return( a->data == b->data && compare(a->left, b->left) && compare(a->right, b->right) ); } // 3. one empty, one not -> false else return 0; }
3-22.
写一个把二叉搜索树转化为链表的程序。
解析:
如果允许使用额外的辅助数据结构,可以遍历树时构造链表,也可以遍历树时把结点压入队列最后出队构造。
如果不允许使用额外的存储空间,把二叉搜索树原地转化为双链表,思想还是树的递归遍历:把根的左子树和右子树分别转化为双链表,再与根相连接。只不过连接时右子树,要注意是把原先的根作为双链表末尾来连接。下面贴的代码来自于何海涛《剑指Offer》面试题27:二叉搜索树与双向链表:
void ConvertNode(BinaryTreeNode* pNode, BinaryTreeNode** pLastNodeInList); BinaryTreeNode* Convert(BinaryTreeNode* pRootOfTree) { BinaryTreeNode *pLastNodeInList = NULL; ConvertNode(pRootOfTree, &pLastNodeInList); // pLastNodeInList指向双向链表的尾结点, // 我们需要返回头结点 BinaryTreeNode *pHeadOfList = pLastNodeInList; while(pHeadOfList != NULL && pHeadOfList->m_pLeft != NULL) pHeadOfList = pHeadOfList->m_pLeft; return pHeadOfList; } void ConvertNode(BinaryTreeNode* pNode, BinaryTreeNode** pLastNodeInList) { if(pNode == NULL) return; BinaryTreeNode *pCurrent = pNode; if (pCurrent->m_pLeft != NULL) ConvertNode(pCurrent->m_pLeft, pLastNodeInList); pCurrent->m_pLeft = *pLastNodeInList; if(*pLastNodeInList != NULL) (*pLastNodeInList)->m_pRight = pCurrent; *pLastNodeInList = pCurrent; if (pCurrent->m_pRight != NULL) ConvertNode(pCurrent->m_pRight, pLastNodeInList); }
既然来自于《剑指Offer》,这里也附注下好了,设计的测试用例为:
- 功能测试:完全二叉树、所有节点都没有左/右子树、只有一个结点的二叉树。
- 特殊输入测试:指向根节点指针为NULL。
3-23.
写一个逆置链表的函数。再写一个非递归实现。
解析:
递归实现只要将当前结点作为已处理部分的最后一个结点附加上去即可。
//http://nbl.cewit.stonybrook.edu:60128/mediawiki/index.php/TADM2E_3.23 Node* Reverse(Node*head) { Node* temp=NULL; if(head==NULL) { return NULL; } if(head->next==NULL) return head; temp=Reverse(head->next); head->next->next=head; head->next=NULL; return temp; }
非递归实现需要保存当前操纵结点的下一个结点以免断链,一次遍历即可。下面的代码来自于何海涛《剑指Offer》面试题16:反转链表:
ListNode* ReverseList(ListNode* pHead) { ListNode* pReversedHead = NULL; ListNode* pNode = pHead; ListNode* pPrev = NULL; while(pNode != NULL) { ListNode* pNext = pNode->m_pNext; if(pNext == NULL) pReversedHead = pNode; pNode->m_pNext = pPrev; pPrev = pNode; pNode = pNext; } return pReversedHead; }
测试用例:
- 链表头指针是NULL。
- 输入链表只有一个头结点。
- 输入链表有多个结点。
3-24.
为网页爬虫设计一个数据结构,来判断URL是否被访问过。要求时间空间都最优。
解析:
我一开始想到的是URL分段进行hash,有人说用前缀树完成匹配,http://nbl.cewit.stonybrook.edu:60128/mediawiki/index.php/TADM2E_3.24提到使用布隆过滤器。简单了解下吧,看上去和分段hash的思路很类似。
布隆过滤器的原理是,当一个元素被加入集合时,通过K个Hash函数将这个元素映射成一个位阵列(Bit array)中的K个点,把它们置为1。检索时,我们只要看看这些点是不是都是1就(大约)知道集合中有没有它了:如果这些点有任何一个0,则被检索元素一定不在;如果都是1,则被检索元素很可能在。这就是布隆过滤器的基本思想。
优点
相比于其它的数据结构,布隆过滤器在空间和时间方面都有巨大的优势。布隆过滤器存储空间和插入/查询时间都是常数()。另外, Hash函数相互之间没有关系,方便由硬件并行实现。布隆过滤器不需要存储元素本身,在某些对保密要求非常严格的场合有优势。
布隆过滤器可以表示全集,其它任何数据结构都不能;
和相同,使用同一组Hash函数的两个布隆过滤器的交并差运算可以使用位操作进行。
缺点
但是布隆过滤器的缺点和优点一样明显。误算率是其中之一。随着存入的元素数量增加,误算率随之增加。但是如果元素数量太少,则使用散列表足矣。
另外,一般情况下不能从布隆过滤器中删除元素. 我们很容易想到把位列阵变成整数数组,每插入一个元素相应的计数器加1, 这样删除元素时将计数器减掉就可以了。然而要保证安全地删除元素并非如此简单。首先我们必须保证删除的元素的确在布隆过滤器里面. 这一点单凭这个过滤器是无法保证的。另外计数器回绕也会造成问题。
在降低误算率方面,有不少工作,使得出现了很多布隆过滤器的变种。
3.25
给定一个字符串和一本杂志,需要你从杂志上剪下来字母从而拼成这个字符串。给出高效率的算法来判断这本杂志是否能拼成这个字符串。
解析:
对杂志逐个字母进行统计,当种类和个数都达到给定字符串中对应字母的个数时结束,否则判断不能拼成。即将字符串按字母序hash,其值是在字符串的出现次数;检索杂志时,每个字母与hash表进行判断。如果值非0,则减1。
为加速确定是否结束,还可以存储字符串内各不相同的字母数,每当hash表中某项由1变0,则计数减1,减到0时表示可以拼成。遍历结束时计数仍为正数则表示不能拼成。
3.26
翻转句子。"My name is Chris" 变成"Chris is name My"。要求最优化时间和空间复杂度。
解析:
常见的序列旋转问题。可以参考:http://www.cnblogs.com/wuyuegb2312/p/3139925.html#title013
3.27
判断一个链表是否有环,并找出环的入口。要求不使用额外存储空间。
解析:
链表找环问题。解法和原理请见:http://www.cnblogs.com/wuyuegb2312/p/3183214.html
3.28
给定一个n元数组X,求出n元数组M,满足M[i] = X[1]*X[2]* ... *X[i-1]*X[i+1]*...*X[n]。不允许用除法,可以用额外的存储空间。(提示:可以快于O(n^2))
解析:
很常见的题目,《编程之美》2.13"子数组最大乘积"利用到了这个算法。
求M[i]需要左右两段X[1...i-1]和X[i+1...n]的积,那么分别构造数组保存即可。即L[i] = X[1]*X[2]* ... *X[i],R[i] = X[i]*...*X[n]。
构造L时从1开始构造,总时间O(n);R从n开始倒序构造,同样是O(n)。构造X[i]只需L[i-1]和R[i+1]的乘积即可(为便于计算令L[0]=R[n+1]=1),也只需O(n)。算法整体复杂度是O(n)。
3.29
写出算法来获取一个页面上出现频率最高的英文词组(仅限两个单词组成的词组,如New York)。使用什么数据结构?要求时间和空间最优。
解析:
在《程序设计实践》(Practise of Programming)上有一段二元马尔科夫链文本生成器做了类似的工作:读入所有词对构成哈希表。解决这个问题也是一样:从头至尾读入每个词对(一次后移一个单词)并放入hash表,记录词对出现的次数,然后求得值最大的词对。
作者:五岳
出处:http://www.cnblogs.com/wuyuegb2312
对于标题未标注为“转载”的文章均为原创,其版权归作者所有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文连接,否则保留追究法律责任的权利。