面试笔记

一般来说,笔试面试中常考的知识点有:

 

 - 数据结构:字符串、链表、数组、堆、哈希表、树(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

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值