赞
踩
一般来说,笔试面试中常考的知识点有:
- 数据结构:字符串、链表、数组、堆、哈希表、树(Trie树、后缀树、红黑树、B树、R树)、图(遍历:BFS、DFS、Dijkstra);
- 算法:基于各个数据结构的查找(二分、二叉树)、排序、遍历,分治、递归、回溯,贪心算法、动态规划、海量数据,外加字符串匹配和资源调优;
- 数学:排列组合概率;
- 操作系统、网络协议、数据库等等。
1. 旋转字符串 给定一个字符串,要求把字符串前面的若干个字符移动到字符串的尾部,要求对长度为n的字符串操作的时间复杂度为 O(n),空间复杂度为 O(1)
解法一:
暴力移位:把需要移动的字符一个一个地移动到字符串的尾部,时间复杂度为O(m * n),空间复杂度为O(1)
解法二:
三步翻转(字符串abcdef -> defabc)时间复杂度为O(n),空间复杂度为O(1)
l X:abc,Y:def;
l X->X^T,得:abc->cba;Y->Y^T,得:def->fed (这里是整个字符串反转,题目是要求若干个字符移动到尾部)
voidReverseString(char* s,int from,int to)
{
while (from < to)
{
char t = s[from];
s[from++] = s[to];
s[to--] = t;
}
}
l X^TY^T,得到:cbafed->defabc,即(X^TY^T)^T=YX
2. 字符串包含 给定两个分别由字母组成的字符串A和字符串B,字符串B的长度比字符串A短。请问,如何最快地判断字符串B中所有字母是否都在字符串A里?
解法一:暴力轮询:针对string2中每一个字符,一一与string1中每个字符依次轮询比较,看它是否在String1中,O(n*m)
解法二:先对这两个字符串的字母进行排序,然后同时对两个字串依次轮询。两个字串的排序需要(常规情况)O(m log m) + O(n log n)次操作,之后的线性扫描需要O(m+n)次操作。
解法三:hashtable,空间换时间。时间复杂度还是O(n + m),空间复杂度为O(26个字母)
解法四:素数相乘,缺点:可能导致溢出
最好解法五:位运算,字符串A,用位运算(26bit整数表示)计算出一个“签名”,再用B中的字符到A里面进行查找。空间O(1)。时间复杂度还是O(n + m)。
3. 字符串转换成整数
```c
int StrToInt(const char *str)
{
int n = 0;
while (*str != 0)
{
int c = *str -'0';
n = n * 10 + c;
++str;
}
return n;
}
```
上述代码忽略了以下细节:
1. 空指针输入:输入的是指针,在访问空指针时程序会崩溃,因此在使用指针之前需要先判断指针是否为空。
2. 正负符号:整数不仅包含数字,还有可能是以'+'或'-'开头表示正负整数,因此如果第一个字符是'-'号,则要把得到的整数转换成负整数。
3. 非法字符:输入的字符串中可能含有不是数字的字符。因此,每当碰到这些非法的字符,程序应停止转换。
4. 整型溢出:输入的数字是以字符串的形式输入,因此输入一个很长的字符串将可能导致溢出。
4. 字符串的全排列
1.这是典型的递归求解问题,递归算法有四个特性:
必须有可达到的终止条件,否则程序陷入死循环
子问题在规模上比原问题小
子问题可通过再次递归调用求解
子问题的解应能组合成整个问题的解
2.对于字符串的排列问题:
如果能生成n-1个元素的全排列,就能生成n个元素的全排列。对于只有一个元素的集合,可以直接生成全排列。所以全排列的递归终止条件很明确,只有一个元素时。我们可以分析一下全排列的过程:
(1)首先,我们固定第一个字符a,求后面两个字符bc的排列
(2)当两个字符bc排列求好之后,我们把第一个字符a和后面的b交换,得到bac,接着我们固定第一个字符b,求后面两个字符ac的排列
(3)现在是把c放在第一个位置的时候了,但是记住前面我们已经把原先的第一个字符a和后面的b做了交换,为了保证这次c仍是和原先处在第一个位置的a交换,我们在拿c和第一个字符交换之前,先要把b和a交换回来。在交换b和a之后,再拿c和处于第一位置的a进行交换,得到cba。我们再次固定第一个字符c,求后面两个字符b、a的排列
(4)既然我们已经知道怎么求三个字符的排列,那么固定第一个字符之后求后面两个字符的排列,就是典型的递归思路了
5. 字符串组合
输入一个字符串,输出该字符串中字符的所有组合。举个例子,如果输入abc,它的组合有a、b、c、ab、ac、bc、abc。
假设我们想在长度为n的字符串中求m个字符的组合。我们先从头扫描字符串的第一个字符。针对第一个字符,我们有两种选择:一是把这个字符放到组合中去,接下来我们需要在剩下的n-1个字符中选取m-1个字符;二是不把这个字符放到组合中去,接下来我们需要在剩下的n-1个字符中选择m个字符。这两种选择都很容易用递归实现。
6. 寻找最小的k个数 和 TOP K
解法一:咱们先简单的理解,要求一个序列中最小的k个数,按照惯有的思维方式,很简单,先对这个序列从小到大排序,然后输出前面的最小的k个数即可。
解法二: 至于选取什么的排序方法,我想你可能会第一时间想到快速排序,我们知道,快速排序平均所费时间为n*logn,然后再遍历序列中前k个元素输出,即可,总的时间复杂度为O(n*logn+k)=O(n*logn)。
解法三:咱们再进一步想想,题目并没有要求要查找的k个数,甚至后n-k个数是有序的,既然如此,咱们又何必对所有的n个数都进行排序列?
这时,咱们想到了用选择或交换排序,即遍历n个数,先把最先遍历到得k个数存入大小为k的数组之中,对这k个数,利用选择或交换排序,找到k个数中的最大数kmax(kmax设为k个元素的数组中最大元素),用时O(k)(你应该知道,插入或选择排序查找操作需要O(k)的时间),后再继续遍历后n-k个数,x与kmax比较:如果x<kmax,则x代替kmax,并再次重新找出k个元素的数组中最大元素kmax‘(多谢kk791159796 提醒修正);如果x>kmax,则不更新数组。这样,每次更新或不更新数组的所用的时间为O(k)或O(0),整趟下来,总的时间复杂度平均下来为:n*O(k)=O(n*k)。
解法四:当然,更好的办法是维护k个元素的最大堆,原理与上述第2个方案一致,即用容量为k的最大堆存储最先遍历到的k个数,并假设它们即是最小的k个数,建堆费时O(k)后,有k1<k2<...<kmax(kmax设为大顶堆中最大元素)。继续遍历数列,每次遍历一个元素x,与堆顶元素比较,x<kmax,更新堆(用时logk),否则不更新堆。这样下来,总费时O(k+(n-k)*logk)=O(n*logk)。此方法得益于在堆中,查找等各项操作时间复杂度均为logk。
二叉树顺序结构:i的父节点为i/2,左右子节点为2i,2i+1
堆排序使用元素较多时
建立堆:时间复杂度O(n)
给定一个整形数组a[]={16,7,3,20,17,8},对其进行堆排序。
首先根据该数组元素构建一个完全二叉树,得到
然后需要构造初始堆,则从最后一个非叶节点开始调整,调整过程如下:
20和16交换后导致16不满足堆的性质,因此需重新调整
这样就得到了初始堆。
即每次调整都是从父节点、左孩子节点、右孩子节点三者中选择最大者跟父节点进行交换(交换之后可能造成被交换的孩子节点不满足堆的性质,因此每次交换之后要重新对被交换的孩子节点进行调整)。
7. 寻找和为定值的两个数 输入一个数组和一个数字,在数组中查找两个数,使得它们的和正好是输入的那个数字。要求时间复杂度是O(N)
不论原序列是有序还是无序,解决这类题有以下三种办法:
- 1、二分(二分查找时间复杂度O(logN),若无序,先排序后二分),时间复杂度总为O(N log N),空间复杂度为O(1);
- 2、扫描一遍X-S[i] 映射到一个数组或构造hash表,时间复杂度为O(N),空间复杂度为O(N);
- 3、两个指针两端扫描(如果无序,先排序,排完序后,i j前后两个指针往中间扫),时间复杂度最后为:有序O(N),无序O(N log N + N)=O(N log N),空间复杂度都为O(1)。
所以,要想达到时间O(N),空间O(1)的目标,除非原数组是有序的(指针扫描法),不然,当数组无序的话,就只能先排序,后指针扫描法或二分(时间 O(Nlog N),空间O(1)),或映射或hash(时间O(N),空间O(N))。时间或空间,必须牺牲一个,达到平衡。
综上,若是数组有序的情况下,优先考虑两个指针两端扫描法,以达到最佳的时O(N),空O(1)效应。否则,如果要排序的话,时间复杂度最快当然是只能达到O(N log N),空间O(1)则不在话下。
8. 二分查找 针对已排序数组
int i = 0;
int j =a.length-1;
while(i <= j){
intmiddle = i + ((j - i) >> 1);
if(a[middle] > val){
j = middle - 1;
}elseif( a[middle] < val){
i= middle + 1;
}else{
returnmiddle;
}
}
return -1;
9. 海量数据处理
1. 分而治之/hash映射 + hash统计 + 堆/快速/归并排序;
2. 双层桶划分
3. Bloom filter/Bitmap;
4. Trie树/数据库/倒排索引;
5. 外排序;
6. 分布式处理之Hadoop/Mapreduce。
密匙一、分而治之/Hash映射 + Hash_map统计 + 堆/快速/归并排序
1. 海量日志数据,提取出某日访问百度次数最多的那个IP
分而治之/hash映射,比如模1000,把整个大文件映射为1000个小文件
hash_map(ip,value)来进行频率统计(hash_map对那1000个文件中的所有IP进行频率统计,然后依次找出1000个文件中频率最大的那个IP)
再在这1000个最大的IP中,找出那个频率最大的IP
2. 寻找热门查询,300万个查询字符串中统计最热门的10个查询
只有300万的Query,每个Query255Byte,因此我们可以考虑把他们都放进内存中去(300万个字符串假设没有重复,都是最大长度,那么最多占用内存3M*1K/4=0.75G。
由于能直接放进内存所以不用hash映射,直接hash统计
So,针对此类典型的TOP K问题,采取的对策往往是:hashmap + 堆。如下所示:
hash_map统计:先对这批海量数据预处理。具体方法是:维护一个Key为Query字串,Value为该Query出现次数的HashTable,即hash_map(Query,Value),每次读取一个Query,如果该字串不在Table中,那么加入该字串,并且将Value值设为1;如果该字串在Table中,那么将该字串的计数加一即可。最终我们在O(N)的时间复杂度内用Hash表完成了统计;
堆排序:第二步、借助堆这个数据结构,找出Top K,时间复杂度为N‘logK。即借助堆结构,我们可以在log量级的时间内查找和调整/移动。因此,维护一个K(该题目中是10)大小的小根堆,然后遍历300万的Query,分别和根元素进行对比。所以,我们最终的时间复杂度是:O(N) + N' * O(logK),(N为1000万,N’为300万)。
密匙三:Bloom filter/Bitmap 适用范围:可以用来实现数据字典,进行数据的判重,或者集合求交集
密匙四、Trie树/数据库/倒排索引
好比假设有b,abc,abd,bcd,abcd,efg,hii 这6个单词,我们构建的树就是如下图这样的:
使用trie:因为当查询如字符串abc是否为某个字符串的前缀时,显然以b,c,d....等不是以a开头的字符串就不用查找了。所以建立trie的复杂度为O(n*len),而建立+查询在trie中是可以同时执行的,建立的过程也就可以成为查询的过程,hash就不能实现这个功能。所以总的复杂度为O(n*len),实际查询的复杂度也只是O(len)。(说白了,就是Trie树的平均高度h为len,所以Trie树的查询复杂度为O(h)=O(len)。
9. 位操作
例:在一个数组中除两个数字只出现1次外,其它数字都出现了2次,要求尽快找出这两个数字。
比如int a[] ={1, 1, 3, 5, 2, 2}
整个数组异或的结果为3^5即 0x0011 ^0x0101 = 0x0110
对0x0110,第1位(由低向高,从0开始)就是1。因此整个数组根据第1位是0还是1分成两组。
a[0] =1 0x0001 第一组
a[1] =1 0x0001 第一组
a[2] =3 0x0011 第二组
a[3] =5 0x0101 第一组
a[4] =2 0x0010 第二组
a[5] =2 0x0010 第二组
第一组有{1, 1, 5},第二组有{3, 2,3},明显对这二组分别执行“异或”解法就可以得到5和3了。
位操作的一些常见应用。
l 判断奇偶: if ((a& 1) == 0)代替if (a % 2 == 0)来判断a是不是偶数
l 交换两数可以用位操作来实现交换两数而不用第三方变量:
第一步 a^=b 即a=(a^b);
第二步 b^=a 即b=b^(a^b),由于^运算满足交换律,b^(a^b)=b^b^a。由于一个数和自己异或的结果为0并且任何数与0异或都会不变的,所以此时b被赋上了a的值。
第三步 a^=b 就是a=a^b,由于前面二步可知a=(a^b),b=a,所以a=a^b即a=(a^b)^a。故a会被赋上b的值。
void Swap(int &a, int&b)
{
if (a != b)
{
a ^= b;
b ^= a;
a ^= b;
}
}
l 变换符号
变换符号就是正数变成负数,负数变成正数。
如对于-11和11,可以通过下面的变换方法将-11变成11
1111 0101(二进制) –取反-> 00001010(二进制) –加1-> 0000 1011(二进制)
同样可以这样的将11变成-11
0000 1011(二进制) –取反-> 00000100(二进制) –加1-> 1111 0101(二进制)
因此变换符号只需要取反后加1即可。
l 求绝对值。
先移位来取符号位,int i = a>> 31;要注意如果a为正数,i等于0,为负数,i等于-1。然后对i进行判断——如果i等于0,直接返回。否之,返回~a+1。完整代码如下:
[cpp] viewplaincopy
//by MoreWindows(http://blog.csdn.net/MoreWindows )
int my_abs(int a)
{
int i = a >> 31;
return i == 0 ? a : (~a + 1);
}
10. 找出数组中出现次数超过一半的数
解法一:hash表
解法二:设数A出现次数超过一半。每次删除两个不同的数,在剩余的数中,数A出现的次数仍超过一半。(有个数字出现的次数比其他所有数字出现次数的和还要多)通过重复这个过程,求出最后的结果。
//size为数组A的大小
//返回数组中出现超过一半的数
intsearch(int *A,int size)
{
int count=0;
int current;
for(int i=0;i<size;i++)
{
if(count==0)
{
current=A[i];
count=1;
}
else
{
if(A[i]==current)
count++;
else
count--;
}
}
return current;
}
11. 链表倒置
/**
* @author luochengcheng
* 两种方式实现单链表的反转(递归、普通)
* 新手强烈建议旁边拿着纸和笔跟着代码画图(便于理解)
*/
publicclass ReverseSingleList {
/**
* 递归,在反转当前节点之前先反转后续节点
*/
public static Node reverse(Node head){
if (null == head || null ==head.getNextNode()) {
return head;
}
Node reversedHead =reverse(head.getNextNode());
head.getNextNode().setNextNode(head);
head.setNextNode(null);
return reversedHead;
}
/**
* 遍历,将当前节点的下一个节点缓存后更改当前节点指针
*
*/
public static Node reverse2(Node head){
if (null == head) {
return head;
}
Node pre = head;
Node cur = head.getNextNode();
Node next;
while (null != cur) {
next = cur.getNextNode();
cur.setNextNode(pre);
pre = cur;
cur = next;
}
//将原链表的头节点的下一个节点置为null,再将反转后的头节点赋给head
head.setNextNode(null);
head = pre;
return head;
}
12. 连续子数组最大和 O(n)
intmaxSum(int* a, int n)
{
intsum=0;
//其实要处理全是负数的情况,很简单,如稍后下面第3 点所见,直接把这句改成:"int sum=a[0]"
即可
//也可以不改,当全是负数的情况,直接返回0,也不见得不行。
int b=0;
for(inti=0; i<n; i++)
{
if(b<0) //...
b=a[i];
else
b+=a[i];
if(sum<b)
sum=b;
}
returnsum;
}
13. 判断俩个链表是否相交
将其中一个首尾相接,遍历另一个链表,如果能到达第一个链表的首部,则表明相交。O(list1.len + list2.len),空间复杂度O(1)。
找到第一个相同节点:链表一从第L1-L2个节点开始遍历,链表二从第一个节点遍历,每次前进一步,直到找到第一个相同的节点
14. 输入一个单向链表,输出该链表中倒数第k个结点。链表的倒数第0个结点为链表的尾指针。
在遍历时维持两个指针,第一个指针从链表的头指针开始遍历,在第k-1步之前,第二个指针保持不动;在第k-1步开始,第二个指针也开始从链表的头指针开始遍历。由于两个指针的距离保持在k-1,当第一个(走在前面的)指针到达链表的尾结点时,第二个指针(走在后面的)指针正好是倒数第k个结点。
15. 求1+2+…+n,要求不能使用乘除法、for、while、if、else、switch、case等关键字以及条件判断语句(A?B:C)。
用构造函数和静态变量 Temp() { ++N; Sum+=N; } Temp[] t = new Temp[n]
16. 输入一颗二元树,从上往下按层打印树的每个结点,同一层中按照从左往右的顺序打印。
public voidPrintTree<T>(Tree<T> tree)
{
Node<T> root=tree.Root;
Queue<Node<T>> queue =new Queue<Node<T>>();
queue.Enqueue(root);
while (queue.Count != 0)
{
Node<T> node =queue.Dequeue();
Console.WriteLine(node.Value);
if (node.Left != null)
queue.Enqueue(node.Left);
if (node.Right != null)
queue.Enqueue(node.Right);
}
}
17. 单链表题
题一、给定单链表,检测是否有环。
使用两个指针p1,p2从链表头开始遍历,p1每次前进一步,p2每次前进两步。如果p2到达链表尾部,说明无环,否则p1、p2必然会在某个时刻相遇(p1==p2),从而检测到链表中有环。
题二、给定两个单链表(head1, head2),检测两个链表是否有交点,如果有返回第一个交点。
如果head1==head2,那么显然相交,直接返回head1。
否则,分别从head1,head2开始遍历两个链表获得其长度len1与len2。假设len1>=len2,那么指针p1由head1开始向后移动len1-len2步。指针p2=head2,下面p1、p2每次向后前进一步并比较p1p2是否相等,如果相等即返回该结点,否则说明两个链表没有交点。
题三、给定单链表(head),如果有环的话请返回从头结点进入环的第一个节点。
运用题一,我们可以检查链表中是否有环。
如果有环,那么p1p2重合点p必然在环中。从p点断开环,方法为:p1=p, p2=p->next, p->next=NULL。此时,原单链表可以看作两条单链表,一条从head开始,另一条从p2开始,于是运用题二的方法,我们找到它们的第一个 交点即为所求。
18. 排列组合
public staticvoid pailie(String[] str, int i, int j){
if(i == j){
for (int o = 0; o < str.length;o++) {
print(str[o]);
}
System.out.println();
}
for(int k=i;k<=j;k++){
swap(str,i,k);
pailie(str,i+1,j);
swap(str,i,k);
}
}
publicstaticvoid combination(String[] s,int begin,int count,List l){
if(count == 0) {
System.out.println(l.toString());
return;
}
if(begin == s.length) return;
l.add(s[begin]);
combination(s, begin+1, count-1, l);
l.remove(s[begin]);
combination(s, begin+1, count, l);
}
19. 输入两个整数n和m,从数列1,2,3,……n中随意取几个数,使其和等于m。要求将所有的可能组合列出来(背包问题求解)
publicstatic void findSum(Integer sum, Integer n)
{
if ( n < 1 || sum < 1){
return;
}
if (sum > n)
{
list.add(n);
findSum(sum - n, n - 1);// n加入,取n=n-1,m=m-n
list.remove(n);
findSum(sum, n - 1); // n没有加入,取n=n-1,m=m
}
else//sum<=n 先加入SUM,再移除从SUM-1中找SUM的和
{
list.add(sum);
for (int i = 0; i < list.size(); i ++)
System.out.print(" "+ list.get(i));
System.out.println();
list.remove(sum);
findSum(sum,sum-1);
}
}
20. 回文:所谓回文数就是顺着读和倒着读一样的数(例如:11,121,1991…)
public static boolean isPalindromic(intn) {
int temp = n;
int sum = 0;
while(temp > 0) {
sum= sum * 10 + temp % 10;
temp/= 10;
}
return sum == n;
}
21. 约瑟夫环:15个基督教徒和15个非教徒在海上遇险,必须将其中一半的人投入海中,其余的人才能幸免于难,于是30个人围成一圈,从某一个人开始从1报数,报到9的人就扔进大海,
查找
1. 顺序查找(n+1)/2, 其中n为表长
2. 二分查找 O(logn)
3. 索引查找
4. 二叉查找树:时间复杂度O(h)。avl树(任何结点的左右子树高度差不超过1):O(logn)
5. Hash O(1)
6. Trie树的平均高度h为len,所以Trie树的查询复杂度为O(h)=O(len)。
需要准备的:
手写插入、冒泡、选择、快速、归并、堆排序,同时敲入了实际代码,其中快排和归并排序练习了几次,已经做到信手拈来,可惜面试中还没碰到直接写排序的,当然了各种排序算法的时空复杂度以及特点都是要理解好的;
编写链表、队列、栈、堆、哈希表数据结构,一开始没有写总是觉得思路比较简单,到实际去实现就会发现没那么简单了,后来面试中确实要直接手写一个栈的实现代码,有所准备了;
要保证自己对自己做过的项目烂熟于心,这是非常重要的,很多面试官都会问你做过的项目,然后从你做过的项目里面集中问你某些知识点
哈夫曼树构建 哈夫曼树:带权路径最小和最小。 方法:权值小的离跟远
图的深度优先遍历类似于树的先跟遍历 用递归
图的广度优先遍历类似于树的层次遍历 用队列
最小生成树Prim算法
选择排序、快速排序、希尔排序、堆排序不是稳定的排序算法,而冒泡排序、插入排序、归并排序和基数排序是稳定的排序算法。
计算中间位置时不应该使用(high+low) / 2的方式,因为加法运算可能导致整数越界,这里应该使用一下三种方式之一:low+ (high – low) / 2或low + (high – low) >> 1或(low + high) >>> 1
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。