1.简答题,给出一个单词,找出这个单词所有的变位词。例如army,mary就是一组变位词,pots stop tops这三个是一组变位词。
要求:写出用到的数据结构,查询流程。
2.简答题,线程和进程的区别,简述什么是“线程安全”。
3.简答题,c和c++如何动态分配和释放内存,以及他们的区别。
4.算法与程序设计题,网络爬虫从一个页面抓取到下一个页面的连接,比如从a.html可以连接到b.html,那么爬虫就能从a.html抓取到b.html,(为了使问题简便,假设每一个页面只存在一个url)。爬虫把抓取到的页面放入一个单链表中,当抓取到的页面不存在向下的url或者是以前已经访问过的页面时,爬虫抓取结束。现在有两个爬虫同时从x1.html和x2.html开始抓取,返回2个单链表请问你如何判断它们是否抓取到了相同的url(假设url有几百亿大小,无法使用hash)。
5.算法与程序设计题,有al[0,mid-1],al[mid,num-1]两个数组,这两个数组都是排序好的,将这两个数组merge成一个数据。
要求:空间复杂性O(1).
6.系统设计题,实现百度的suggestion功能。(研发类)
要求,写出算法,所用数据结构,尽可能的减少时间空间复杂度,提出优化方法。
感觉时间好紧,只有两个小时,答的也不是很靠谱。下面一个一个的讨论下,可能不是最合理答案~
1.给出一个单词,找出这个单词所有的变位词。例如army,mary就是一组变位词,pots stop tops这三个是一组变位词。
方案一:最直接想到的就是先找出所有可能的变位词,在在字典中判断了。变位词的话直接想到的应该是一个全排列,这样字典的数据结构trie树或者用hash来实现
PS:如果判断两个词是不是同一个词的变位词。
1)一种算法思想就是“对任意一组变位词内部排序后他们得到的单词是一样的”,例如,对pots stop tops这三个词排序后的结果都是opst。
2)还可以把每个字母都附上权值,用一个单词各个权值的乘积来判断,不过这样对权值的要求有点难,权值为不为1的素数的时候满足要求。
2.线程和进程的联系和区别,简述什么是“线程安全”。
a.线程和进程的联系和区别
1)最主要的就是进程是操作系统资源分配的最小单位,每个进程有自己的独立的地址空间(用户空间)。而一个进程中的所有信息对该进程所有的线程都是共享的包括进程的程序文本,程序的全局内存和堆内存、栈以及文件描述符(这是最根本的区别,也是其他区别的原因)
2)线程包含了表示进程内执行环境必须的信息,其中包括进程中标识线程的线程id 、一组寄存器值、栈、调度优先级和策略、信号屏蔽字、erron变量。
3)效率上来讲,创建(创建的时候时间、资源上线程都比进程较好,创建一个线程时候要分配给它独立的地址空间,建立众多的数据表来维护它的代码段、堆栈段和数据段,而而运行于一个进程中的多个线程,它们彼此之间使用相同的地址空间,共享大部分数据,启动一个线程所花费的空间远远小于启动一个进程所花费的空间),销毁,切换(进程与进程,线程与线程)的时候,线程的效率比进程的效率高,主要原因是因为第一点,一个进程的多个线程大部分的信息都是共享的。
4)多进程与多线程并行处理的时候,一般而言多线程的程序的效率较高,不仅是因为创建和切换快,多线程间通信也方便,可以直接通过共享的信息来通信,而多进程通信就稍麻烦些,有信号量、管道、消息队列、共享内存还可以通过网络socket来实现通信。多个线程间通信为了数据的一致性需要加锁处理。
5)一个线程死掉,可能拥有这个线程的进程都会死掉,但一个进程死掉保护模式下不会对其他的进程产生影响(进程有自己独立的地址空间,而线程没有)。所以多进程的程序会比多线程的程序健壮些,但是一些要求同时进行(并发性,线程的最大特点)又要求共享一些变量的操作,只能用多线程。
从函数调用上来说,进程创建使用fork()操作;线程创建使用clone()操作。Richard Stevens大师这样说过:
fork is expensive. Memory is copied from the parent to the child, all descriptors are duplicated in the child, and so on. Current implementations use a technique called copy-on-write, which avoids a copy of the parent's data space to the child until the child needs its own copy. But, regardless of this optimization, fork is expensive.
IPC is required to pass information between the parent and child after the fork. Passing information from the parent to the child before the fork is easy, since the child starts with a copy of the parent's data space and with a copy of all the parent's descriptors. But, returning information from the child to the parent takes more work.
Threads help with both problems. Threads are sometimes called lightweight processes since a thread is "lighter weight" than a process. That is, thread creation can be 10–100 times faster than process creation.
All threads within a process share the same global memory. This makes the sharing of information easy between the threads, but along with this simplicity comes the problem of synchronization.
PS:不和多进程比多线程还有两个优点,一、可以提高应用程序的响应速度,例如在android 开发中的下载这样耗时比较长的操作通常会启动一个线程来下载(idea)。二、可以使得多CPU更见有效,一个又长又复杂的进程可以分成多个独立的线程充分利用cpu 。
b.线程安全
线程安全指某个函数 (计算机科学)、函数库在多线程环境中被调用时,能够正确地处理各个线程的局部变量,使程序功能正确完成。一般来说,线程安全的函数应该为每个调用它的线程分配专门的空间,来储存需要单独保存的状态(如果需要的话),很多C库代码(比如某些strtok的实现,它将“多次调用中需要保持不变的状态”储存在静态变量中,导致不恰当的共享)不是线程安全的,在多线程环境中调用这些函数时,要进行特别的预防措施,或者寻找别的替代方案。(参考:http://zh.wikipedia.org/wiki/%E7%BA%BF%E7%A8%8B%E5%AE%89%E5%85%A8)。
“如果一个函数在同一时刻可以被多个线程安全的调用,就称这个函数是线程安全的”“很多函数不是线程安全的,因为它们返回的数据都是存放在静态的内存缓冲区中”(参考:Unix 环境高级编程)。
3.简答题,c和c++如何动态分配和释放内存,以及他们的区别。
ISO C说明了三个可以用于存储空间动态分配的函数
void *malloc(size_t size);
void *calloc(size_t nobj, size_t size);
void *realloc(void *ptr, size_t newsize);
其中malloc分配的空间的初始值不确定,而calloc的分配空间每一位都初始化为0,realloc可以更改以前分配空间的长度,当长度增加的时候,可能需要将以前的分配区的内容移到另一个大的空间,新增加的空间(向高地址增加,堆是向高地址的)的初始值是不确定的,需要注意的是因为可能会移动位置,不应当使任何指针指向该区,而应该只使用返回的指针。大多数的实现所分配的存储空间可能比所要求的稍大一些,额外的空间用过来记录管理信息,例如分配块的长度,指向下一个分配块的指针等。如果开辟了一块空间但是不使用的时候忘记free掉,那么可能会造成内存的泄露还可能会使得由于过的分页开销使性能下降。
C++中用的是new和delete操作符,还会调用构造函数和析构函数。
4.算法题一
审题上,两个链表在存储空间上不相交,而且不存在环,遇到链表上曾经出现的页面的就结束了。
分类讨论下结束的两种有相同的元素的情况
情况一:有相同的元素,且都是因为最后一个页面为空没有连接到别的页面,这个时候两个链表如果有相同的元素,那么最后一个元素肯定相同
情况二:因为出现重复的页面而结束,并且有相同的页面。那么,如果把链表最后的元素补充完整,则两个链表尾部都形成了环。又分两种情况:一、如果相同的元素在环外,那么这个相同的元素肯定是在环的前面,那么从这个相同的元素开始,后面的元素都是一样的了,最后一个元素肯定也相同。二、相同的元素在环内,那么够成这个环的元素则全部的相同,又因为这两个环都在尾部,两个链表最后一个元素肯定在环内,所以这时候每个链表最后一个元素都在另外一个链表中。
所以对于两种情况,只要判断其中一个链表的最后一个元素是不是在另外一个链表中就可以了。
5.算法题二
算法空间复杂度是1,当时一时没明白是什么意思,可能是要求对两个数组就地重排,排序完的结果还是放在两个数组的里面,这样就跟一般的合并排序(归并排序的算法复杂度为n)不一样了。而且有个特点,a[0,mid-1]和a[mid, num-1]在内存上是连续的,所以可以看成一个数组分成了两个部分,前后两个部分都是有序的,要求某种空间复杂度为1的算法把这个数组变成有序的。
那么简单的想,可以利用堆排序,冒泡,插入来解决这个为题,空间复杂度为O(1),但是完全没有利用两部分基本有序的特性,也没有满足题目中merge的要求。所以算法是要求既要用到两个数组有序进行合并,又要空间复杂度为1。
方案一:假如要从小到大的顺序,a[0,mid-1]取第一个元素,如果a[0,mid-1]中的大于a[mid, num-1]中的第一个元素,那么交换两个数,这时a[mid, num-1]可能第一个交换过来的元素比后面的大,失去了从小到大的顺序,重新调整下a[mid, num-1]失其有序,在用a[0, mid-1]中第二个元素和a[mid, num-1]的第一元素比较一直到a[0, mid-1]的最后一个元素数组就变成有序的了。
调整部分效率上考虑,可以首先找到a[mid]在a[mdi, num-1]中的位置,移动的时候可用内存拷贝函数void *memcpy(void *dest, const void *src, int n)而不用一个一个的元素拷贝。下面的例子只是简单的for循环来实现调整的。如果简单点写,可以直接用个for循环来实现调整部分,不过效率有点低如下
for(int k=mid;k<num-1;k++) { if(a[k] > a[k+1]) exchange(a, k, k+1); }
全部代码:
#include <iostream> using namespace std; void exchange(int *a, int m, int n); int main() { const int mid = 3; const int num = 6; int a[6] = {1, 4, 6, 1, 2, 3}; int i = 0; int j = mid; while(i < mid) { if(a[i] > a[j]) { exchange(a, i, j); int local = mid; //----调整部分,local为a[mid]应该在的数组下标 int key = a[mid]; for(int k=mid;k<num-1;k++) { if(key>=a[k] && key<a[k+1]) { local = k; break; } if(k == num-2 && key >= a[num-1]) { local = num -1; } } memcpy(&a[mid], &a[mid+1], (local-mid)*sizeof(int)); a[local] = key; //-----调整部分结束 i++; } else i++; } for(int m=0;m<6;m++) cout<<a[m]<<endl; return 0; } void exchange(int *a, int m, int n) { int temp = a[m]; a[m] = a[n]; a[n] = temp; }
方案二:思想有点类似于冒泡,也是从小到大的顺序,假如a[0,mid-1]中的第1个元素大于a[mid, num-1]的第一个元素,那么把a[mid]与a[0,mid-1]整体交换,代码如下
#include <iostream> using namespace std; int main() { const int mid = 3; const int num = 6; int a[6] = {1, 1, 1, 1, 8, 9}; int i = 0; int j = mid; while(i<j && j<num) { if(a[i]>a[j]) { int temp = a[j]; memcpy(&a[i+1], &a[i], (j-i)*sizeof(int)); a[i] = temp; j++; i++; } else i++; } for(int m=0;m<6;m++) cout<<a[m]<<endl; return 0; }
6.系统设计题
suggestion功能可以先根据搜索日志,作出搜索字典库,然后根据字典库生成trie树,统计出每个词搜索的次数,计算出每个词最大前串匹配的topK。