百度2012实习生校园招聘笔试题

原文地址:http://blog.csdn.net/hackbuteer1/article/details/7542774

1、给一个单词a,如果通过交换单词中字母的顺序可以得到另外的单词b,那么b是a的兄弟单词,比如的单词army和mary互为兄弟单词。
现在要给出一种解决方案,对于用户输入的单词,根据给定的字典找出输入单词有哪些兄弟单词。请具体说明数据结构和查询流程,要求时间和空间效率尽可能地高。
字典树的典型应用,一般情况下,字典树的结构都是采用26叉树进行组织的,每个节点对应一个字母,查找的时候,就是一个字母一个字母的进行匹配,算法的时间复杂度就是单词的长度n,效率很高。因此这个题目可以定义一个字典树作为数据结构来查询的,时间效率会很高,这样就转化为在一棵字典树中查找兄弟单词,只要在字典树中的前缀中在存储一个vector结构的容器,这样查找起来就是常数级的时间复杂度了,效率很高的。。
数据结构可以定义如下:

  1. struct word  
  2. {    
  3.     vector<string> brother;    // 用于保存每个单词的兄弟单词   
  4.     word *next[26];            // 字典树中每个节点代表一个字符,并指向下一个字符   
  5. };  
struct word
{  
    vector<string> brother;    // 用于保存每个单词的兄弟单词
    word *next[26];            // 字典树中每个节点代表一个字符,并指向下一个字符
};

如上述数据结构所示,字典树的建立是在预处理阶段完成的,首先根据字典中的单词来建立字典树,建立的时候,需要稍微特殊处理一下,就是比如pots、stop和tops互为兄弟单词,那么在字典中按照首字母顺序的话,应该先遇到pots单词,那么我首先对其进行排序,结果是opts,那么字典树中就分别建立4个节点,分别为o->p->t->s,当然这个是不同层次的,在节点s处的vector容器brother中添加单词pots,遇到stop的时候,同样的方法,排序是opts,此时发现这4个节点已经建立了,那么只需要在第四个节点s处的vector容器brother中添加单词stop,tops单词的处理方法是同样的。
     这样建立完字典树后,查询兄弟单词的效率就会很高了,比哈希的效率还要高;查到tops的兄弟的单词的时候,首先排序,那么就是opts,然后在字典树中查找opts,在s处将其vector容器brother中的的单词输出就是tops的所有兄弟单词。
2、系统中维护了若干数据项,我们对数据项的分类可以分为三级,首先我们按照一级分类方法将数据项分为A、B、C......若干类别,每个一级分类方法产生的类别又可以按照二级分类方法分为a、b、c......若干子类别,同样,二级分类方法产生的类别又可以按照是三级分类方法分为i、ii、iii......若干子类别,每个三级分类方法产生的子类别中的数据项从1开始编号。我们需要对每个数据项输出日志,日志的形式是key_value对,写入日志的时候,用户提供三级类别名称、数据项编号和日志的key,共五个key值,例如,write_log(A,a,i,1,key1),获取日志的时候,用户提供三级类别名称、数据项编号,共四个key值,返回对应的所有的key_value对,例如get_log(A,a,i,1,key1),
请描述一种数据结构来存储这些日志,并计算出写入日志和读出日志的时间复杂度。

我的思路:

 

存储数据结构,查找速度最快的无疑使Hash Table或Hash Map,换句话说就是多为数组。如果用五维数组的话,我们假设一级分类为a个,二级为b个,以此类推,那么需要的总内存就是a*b*c*d*e。

 

我们看到数组查找的时间复杂度几乎为O(1),时间复杂度理论上都为O(1)。

4、数组al[0,mid-1]和al[mid,num-1]是各自有序的,对数组al[0,num-1]的两个子有序段进行merge,得到al[0,num-1]整体有序。要求空间复杂度为O(1)。注:al[i]元素是支持'<'运算符的。

 

  1. /* 
  2. 数组a[begin, mid] 和 a[mid+1, end]是各自有序的,对两个子段进行Merge得到a[begin , end]的有序数组。 要求空间复杂度为O(1) 
  3. 方案: 
  4. 1、两个有序段位A和B,A在前,B紧接在A后面,找到A的第一个大于B[0]的数A[i], A[0...i-1]相当于merge后的有效段,在B中找到第一个大于A[i]的数B[j], 
  5. 对数组A[i...j-1]循环右移j-k位,使A[i...j-1]数组的前面部分有序 
  6. 2、如此循环merge 
  7. 3、循环右移通过先子段反转再整体反转的方式进行,复杂度是O(L), L是需要循环移动的子段的长度 
  8. */  
  9. #include<iostream>   
  10. using namespace std;  
  11.   
  12. void Reverse(int *a , int begin , int end )   //反转   
  13. {  
  14.     for(; begin < end; begin++ , end--)  
  15.         swap(a[begin] , a[end]);  
  16. }  
  17. void RotateRight(int *a , int begin , int end , int k)    //循环右移   
  18. {  
  19.     int len = end - begin + 1;  //数组的长度   
  20.     k %= len;  
  21.     Reverse(a , begin , end - k);  
  22.     Reverse(a , end - k + 1 , end);  
  23.     Reverse(a , begin , end);  
  24. }  
  25.   
  26. // 将有序数组a[begin...mid] 和 a[mid+1...end] 进行归并排序   
  27. void Merge(int *a , int begin , int end )  
  28. {  
  29.     int i , j , k;  
  30.     i = begin;  
  31.     j = 1 + ((begin + end)>>1);    //位运算的优先级比较低,外面需要加一个括号,刚开始忘记添加括号,导致错了很多次   
  32.     while(i <= end && j <= end && i<j)  
  33.     {  
  34.         while(i <= end && a[i] < a[j])  
  35.             i++;  
  36.         k = j;   //暂时保存指针j的位置   
  37.         while(j <= end && a[j] < a[i])  
  38.             j++;  
  39.         if(j > k)  
  40.             RotateRight(a , i , j-1 , j-k);   //数组a[i...j-1]循环右移j-k次   
  41.         i += (j-k+1);  //第一个指针往后移动,因为循环右移后,数组a[i....i+j-k]是有序的   
  42.     }  
  43. }  
  44.   
  45. void MergeSort(int *a , int begin , int end )  
  46. {  
  47.     if(begin == end)  
  48.         return ;  
  49.     int mid = (begin + end)>>1;  
  50.     MergeSort(a , begin , mid);   //递归地将a[begin...mid] 归并为有序的数组   
  51.     MergeSort(a , mid+1 , end);   //递归地将a[mid+1...end] 归并为有序的数组   
  52.     Merge(a , begin , end);       //将有序数组a[begin...mid] 和 a[mid+1...end] 进行归并排序   
  53. }  
  54.   
  55. int main(void)  
  56. {  
  57.     int n , i , a[20];  
  58.     while(cin>>n)  
  59.     {  
  60.         for(i = 0 ; i < n ; ++i)  
  61.             cin>>a[i];  
  62.         MergeSort(a , 0 , n - 1);  
  63.         for(i = 0 ; i < n ; ++i)  
  64.             cout<<a[i]<<" ";  
  65.         cout<<endl;  
  66.     }  
  67.     return 0;  
  68. }  
/*
数组a[begin, mid] 和 a[mid+1, end]是各自有序的,对两个子段进行Merge得到a[begin , end]的有序数组。 要求空间复杂度为O(1)
方案:
1、两个有序段位A和B,A在前,B紧接在A后面,找到A的第一个大于B[0]的数A[i], A[0...i-1]相当于merge后的有效段,在B中找到第一个大于A[i]的数B[j],
对数组A[i...j-1]循环右移j-k位,使A[i...j-1]数组的前面部分有序
2、如此循环merge
3、循环右移通过先子段反转再整体反转的方式进行,复杂度是O(L), L是需要循环移动的子段的长度
*/
#include<iostream>
using namespace std;

void Reverse(int *a , int begin , int end )   //反转
{
	for(; begin < end; begin++ , end--)
		swap(a[begin] , a[end]);
}
void RotateRight(int *a , int begin , int end , int k)    //循环右移
{
	int len = end - begin + 1;  //数组的长度
	k %= len;
	Reverse(a , begin , end - k);
	Reverse(a , end - k + 1 , end);
	Reverse(a , begin , end);
}

// 将有序数组a[begin...mid] 和 a[mid+1...end] 进行归并排序
void Merge(int *a , int begin , int end )
{
	int i , j , k;
	i = begin;
	j = 1 + ((begin + end)>>1);    //位运算的优先级比较低,外面需要加一个括号,刚开始忘记添加括号,导致错了很多次
	while(i <= end && j <= end && i<j)
	{
		while(i <= end && a[i] < a[j])
			i++;
		k = j;   //暂时保存指针j的位置
		while(j <= end && a[j] < a[i])
			j++;
		if(j > k)
			RotateRight(a , i , j-1 , j-k);   //数组a[i...j-1]循环右移j-k次
		i += (j-k+1);  //第一个指针往后移动,因为循环右移后,数组a[i....i+j-k]是有序的
	}
}

void MergeSort(int *a , int begin , int end )
{
	if(begin == end)
		return ;
	int mid = (begin + end)>>1;
	MergeSort(a , begin , mid);   //递归地将a[begin...mid] 归并为有序的数组
	MergeSort(a , mid+1 , end);   //递归地将a[mid+1...end] 归并为有序的数组
	Merge(a , begin , end);       //将有序数组a[begin...mid] 和 a[mid+1...end] 进行归并排序
}

int main(void)
{
	int n , i , a[20];
	while(cin>>n)
	{
		for(i = 0 ; i < n ; ++i)
			cin>>a[i];
		MergeSort(a , 0 , n - 1);
		for(i = 0 ; i < n ; ++i)
			cout<<a[i]<<" ";
		cout<<endl;
	}
	return 0;
}

5、线程和进程的区别及联系?如何理解“线程安全”问题?

 

答案:进程和线程都是由操作系统所体会的程序运行的基本单元,系统利用该基本单元实现系统对应用的并发性。
1、简而言之,一个程序至少有一个进程,一个进程至少有一个线程.
2、线程的划分尺度小于进程,使得多线程程序的并发性高。
3、另外,进程在执行过程中拥有独立的内存单元,而多个线程共享内存,从而极大地提高了程序的运行效率。
4、线程在执行过程中与进程还是有区别的。每个独立的线程有一个程序运行的入口、顺序执行序列和程序的出口。但是线程不能够独立执行,必须依存在应用程序中,由应用程序提供多个线程执行控制。
5、从逻辑角度来看,多线程的意义在于一个应用程序中,有多个执行部分可以同时执行。但操作系统并没有将多个线程看做多个独立的应用,来实现进程的调度和管理以及资源分配。这就是进程和线程的重要区别。

 

区别和联系

         进程由进程控制块PCB和地址空间两部分组成。进程,是并发执行的程序在执行过程中分配和管理资源的基本单位,是一个动态概念,竟争计算机系统资源的基本单位。每一个进程都有一个自己的地址空间,即进程空间或(虚空间)。进程空间的大小只与处理机的位数有关,一个16位长处理机的进程空间大小为216,而32位处理机的进程空间大小为232。进程至少有5种基本状态,它们是:初始态,执行态,等待状态,就绪状态,终止状态。
        线程由线程控制块TCB和线程栈两部分组成。线程,在网络或多用户环境下,一个服务器通常需要接收大量且不确定数量用户的并发请求,为每一个请求都创建一个进程显然是行不通的,无论是从系统资源开销方面或是响应用户请求的效率方面来看。因此,操作系统中线程的概念便被引进了。线程,是进程的一部分,一个没有线程的进程可以被看作是单线程的。线程有时又被称为轻型进程或轻量级进程,也是CPU 调度的一个基本单位。

二者大致的区别:
        进程的执行过程是线状的,尽管中间会发生中断或暂停,但该进程所拥有的资源只为该线状执行过程服务。一旦发生进程上下文切换,这些资源都是要被保护起来的。这是进程宏观上的执行过程。而进程又可有单线程进程与多线程进程两种。我们知道,进程有一个进程控制块PCB ,相关程序段和该程序段对其进行操作的数据结构集这三部分,单线程进程的执行过程在宏观上是线性的,微观上也只有单一的执行过程;而多线程进程在宏观上的执行过程同样为线性的,但微观上却可以有多个执行操作(线程),如不同代码片段以及相关的数据结构集。线程的改变只代表了CPU 执行过程的改变,而没有发生进程所拥有的资源变化。除了 CPU之外,计算机内的软硬件资源的分配与线程无关,线程只能共享它所属进程的资源。与进程控制表和 PCB 相似,每个线程也有自己的线程控制表TCB ,而这个 TCB 中所保存的线程状态信息则要比 PCB表少得多,这些信息主要是相关指针用堆栈(系统栈和用户栈),寄存器中的状态数据。进程拥有一个完整的虚拟地址空间,不依赖于线程而独立存在;反之,线程是进程的一部分,没有自己的地址空间,与进程内的其他线程一起共享分配给该进程的所有资源。
        线程可以有效地提高系统的执行效率,但并不是在所有计算机系统中都是适用的,如某些很少做进程调度和切换的实时系统。使用线程的好处是有多个任务需要处理机处理时,减少处理机的切换时间;而且,线程的创建和结束所需要的系统开销也比进程的创建和结束要小得多。最适用使用线程的系统是多处理机系统和网络系统或分布式系统。

(1). 线程的执行特性
        线程只有 3 个基本状态:就绪,执行,阻塞。
       线程存在 5 种基本操作来切换线程的状态:派生,阻塞,激活,调度,结束。

线程安全

       如果代码所在的进程中有多个线程在同时运行,而这些线程可能会同时运行这段代码。如果每次运行结果和单线程运行的结果是一样的,而且其他的变量的值也和预期的是一样的,就是线程安全的。或者说:一个类或者程序所提供的接口对于线程来说是原子操作或者多个线程之间的切换不会导致该接口的执行结果存在二义性,也就是说我们不用考虑同步的问题。线程安全问题都是由全局变量及静态变量引起的。若每个进程中对全局变量、静态变量只有读操作,而无写操作,一般来说,这个全局变量是线程安全的;若有多个线程同时执行写操作,一般都需要考虑线程同步,否则就可能影响线程安全。

 结合singleton,解释线程安全问题。

public class Singleton { private Singleton() { } private static Singleton _instance = null; public static Singleton getInstance() { if(_instance == null) _instance = new Singleton(); return _instance; } }

这样的代码在单线程下能正常工作,但是多线程下就可能出问题。如果线程A判断_instance为null,但还没创建实例时,这时线程A被打断,线程B开始执行,线程B也会判断_instance为NULL,创建了一份Singleton实例,当A继续执行时,又创建了一份Singleton实例。这时Singleton就不满足单例模式要求了。

在Java里可以通过加锁或synchronized关键字来避免代码块受并发访问的干扰。

算法与程序设计一、
网页爬虫在抓取网页时,从指定的URL站点入口开始爬取这个站点上的所有URL link,抓取到下一级link对应的页面后,同样对页面上的link进行抓取从而完成深度遍历。为简化问题,我们假设每个页面上至多只有一个link,如从www.baidu.com/a.html链接到
www.baidu.com/b.html再链到www.baidu.com/x.html,当爬虫抓取到某个页面时,有可能再链回www.baidu.com/b.html,也有可能爬取到一个不带任何link的终极页面。当抓取到相同的URL或不包含任何link的终极页面时即完成爬取。爬虫在抓取到这些页面后建立一个单向链表,用来记录抓取到的页面,如:a.html->b.html->x.html...->NULL。
问:对于爬虫分别从www.baidu.com/x1.html和www.baidu.com/x2.html两个入口开始获得两个单向链表,得到这两个单向链表后,如何判断他们是否抓取到了相同的URL?(假设页面URL上百亿,存储资源有限,无法用hash方法判断是否包含相同的URL)
请先描述相应的算法,再给出相应的代码实现。(只需给出判断方法代码,无需爬虫代码)

方法一:(海量信息的处理方法)
        将两个单项链表进行hash映射来划分,如取模为1000,即hash(url)00,则将一个单项链表划分为1000个小文件(a0,a1,a2,a3,....,a999),其中相同的url会在同一个小文件中。如果感觉划分后的文件仍太大,可以将模取得更大一些,比如10000。采取同样的hash函数,以同样的方式处理第二个单项链表,得到小文件(b0,b1,b2,b3,...,b999)。
然后处理每个对应的小文件(ai和bi),即求ai和bi中相同的url。此时可以采用hash统计的方法,求二者的交集;或者采用bloomfilter求交集

两个单向链表的相交问题。
算法与程序设计二、
4、有一种结构如下图所示,它由层的嵌套组成,一个父层中只能包含垂直方向上或者是水平方向上并列的层,例如,层1可以包含2、3、4三个垂直方向上的层,层2可以包含5和6两个水平方向的层,在空层中可以包含数据节点,所谓的空层是指不包含子层的层,每个空层可以包含若干个数据节点,也可以一个都不包含。
在这种结构上面,我们从垂直方向上划一条线,我们约定每一个子层中我们只能经过一个数据节点,在这种情况下,每条线可以经过多个数据节点,也可以不经过任何数据节点,例如,线1经过了3、5、8三个数据节点,线2只经过了14个数据节点。
(1)给出函数,实现判断两个数据节点,是否可能同时被线划中,给出具体的代码。

(2)给出函数,输出所有一条线可以划中的数据节点序列, 可以给出伪代码实现。

 

思路:(1)判断两个数所属的同一层次的相同矩形框的下一层次矩形框是水平排列的还是垂直排列的,垂直排列在能在一条线上,水平排列则不能。
(2)用回溯算法求出所有在一条直线上的字符串,用两字符串是否在同一直线上进行剪枝操作。

题目描述:百度搜索框的suggestion,比如输入“北京”,搜索框下面会以北京为前缀,展示“北京爱情故事”、“北京公交”、“北京医院”等等搜索词,输入“结构之”,会提示“结构之法”,“结构之法 算法之道”等搜索词。请问,如何设计此系统,使得空间和时间复杂度尽量低。

 

解法:用Trie树统计每个前缀是“北京”的词出现的次数,然后给出Top N。



 

posted @ 2013-08-27 11:43  sandyhit  阅读(134)  评论(0)    收藏  举报