剑指Offer——美团内推+校招笔试题+知识点总结
前言
美团9.9内推笔试、9.11校招笔试,反正就是各种虐,笔试内容如下:
知识点:图的遍历(DFS、BFS)、进程间通信、二叉查找树节点的删除及中序遍历、HTTP会话的4个过程、红黑树、1024!有多少个0,60块糖分给5个人,如何分等。编程题考察拿红包、多叉树(见下图)。
另外,更变态的是其IDE编程居然在多行输入问题上死翘翘了,基本上就是纠结于到底如何实现多行输入,结果就挂了!整理后的多行输入如下:
package cn.edu.ujn.practice; import java.util.Scanner; import java.util.regex.Pattern; import java.util.ArrayList; // 死在了多行输入上 public class meituan1 { static ArrayList<Integer> list = new ArrayList<Integer>(); public static void main(String [] args){ Scanner in = new Scanner(System.in); int lines = in.nextInt(); Pattern pattern = Pattern.compile("[,]+"); int [] arr = null; // 用于接收换行符 in.nextLine(); for(int i = 0; i < lines; i++){ String [] str = pattern.split(in.nextLine()); arr = new int[str.length]; for(int j = 0; j < str.length; j++){ //System.out.print(str[j]+" "); arr[j] = Integer.parseInt(str[j]); } // 调用方法 solution(arr); //System.out.println(); } for(int k = 0; k < arr.length; k++){ //System.out.println(arr[k]); } for(Integer a : list){ System.out.print(a + ""); } } private static void solution(int [] arr){ System.out.println(arr.length); list.add(1); } }
测试结果如下:
注 红黑树基本性质
性质1. 节点是红色或黑色。
性质2. 根是黑色。
性质3. 所有叶子都是黑色(叶子是NIL节点)。
性质4. 每个红色节点的两个子节点都是黑色。(从每个叶子到根的所有路径上不能有两个连续的红色节点)
性质5. 从任一节点到其每个叶子的所有简单路径 都包含相同数目的黑色节点。
注 质数 素数 合数
质数又称素数。指在一个大于1的自然数中,除了1和此整数自身外,没法被其他自然数整除的数。换句话说,只有两个正因数(1和自己)的自然数即为素数。比1大但不是素数的数称为合数。1和0既非素数也非合数。合数是由若干个质数相乘而得到的。所以,质数是合数的基础,没有质数就没有合数。这也说明了前面所提到的质数在数论中有着重要地位。历史上曾将1也包含在质数之内,但后来为了算术基本定理,最终1被数学家排除在质数之外,而从高等代数的角度来看,1是乘法单位元,也不能算在质数之内,并且,所有的合数都可由若干个质数相乘而得到。
注 1024!末尾有多少个0?
1024的阶乘末尾有多少个0,这个问题只要理清思想就很好解了。
有多少个0取决于有多少个10相乘,即1024拆成小单元后有多少个10。
由于10不是素数,所以直接用10进行计算的话会有很多问题,于是将10分解。 10可以分解成2*5,2和5都是素数,由于每2个相邻的数中一定包含2,所以只要计算出有多少个5就可以了(2会在5之后及时出现)。
于是解法如下:
是5的倍数的数有: 1024 / 5 = 204个
是25的倍数的数有:1024 / 25 = 40个
是125的倍数的数有:1024 / 125 = 8个
是625的倍数的数有:1024 / 625 = 1个
所以1024! 中总共有204+40+8+1=253个因子5。
即1024!后有253个0。
编程实现
package cn.edu.ujn.demo; public class Prime { public static void main(String[] args) { int num = 1024; System.out.println(sumOfZero(num)); } // 结算阶乘中包含0的个数 private static int sumOfZero(int num){ int cnt = 0; int a = 5; while(num > 0){ cnt += num / a; num /= a; } return cnt; } }
注 进程间通信机制
进程的通信机制主要有:管道、有名管道、消息队列、信号量、共享空间、信号、套接字。
管道
它传递数据是单向性的,只能从一方流向另一方,也就是一种半双工的通信方式;只用于有亲缘关系的进程间的通信,亲缘关系也就是父子进程或兄弟进程;没有名字并且大小受限,传输的是无格式的流,所以两进程通信时必须约定好数据通信的格式。管道它就像一个特殊的文件,但这个文件之存在于内存中,在创建管道时,系统为管道分配了一个页面作为数据缓冲区,进程对这个数据缓冲区进行读写,以此来完成通信。其中一个进程只能读一个只能写,所以叫半双工通信,为什么一个只能读一个只能写呢?因为写进程是在缓冲区的末尾写入,读进程是在缓冲区的头部读取,他们各自的数据结构不同,所以功能不同。
有名管道
看见这个名字就能知道个大概了,它与管道不同之处是它有名字。这就不同于管道只能在具有亲缘关系的进程间通信了。它提供了一个路径名与之关联,有了自己的传输格式。有名管道和管道的不同之处还有一点是,有名管道是个设备文件,存储在文件系统中,没有亲缘关系的进程也可以访问,但是它要按照先进先出的原则读取数据。同样也是单双工的。
消息队列
是存放在内核中的消息链表,每个消息队列由消息队列标识符标识,与管道不同的是,消息队列存放在内核中,只有在内核重启时才能删除一个消息队列,内核重启也就是系统重启,同样消息队列的大小也是受限制的。
信号量
也可以说是一个计数器,常用来处理进程或线程同步的问题,特别是对临界资源的访问同步问题。临界资源:为某一时刻只能由一个进程或线程操作的资源,当信号量的值大于或等于0时,表示可以供并发进程访问的临界资源数,当小于0时,表示正在等待使用临界资源的进程数。更重要的是,信号量的值仅能由PV操作来改变。
共享内存
就是分配一块能被其他进程访问的内存。共享内存可以说是最有用的进程间通信方式,也是最快的IPC形式。首先说下在使用共享内存区前,必须通过系统函数将其附加到进程的地址空间或说为映射到进程空间。两个不同进程A、B共享内存的意思是,同一块物理内存被映射到 进程A、B各自的进程地址空间。进程A可以即时看到进程B对共享内存中数据的更新,反之亦然。由于多个进程共享同一块内存区域,必然需要某种同步机制,互斥锁和信号量都可以。采用共享内存通信的一个显而易见的好处是效率高,因为进程可以直接读写内存,而不需要任何数据的拷贝。对于像管道和消息队列等通信方式,则需要在内核和用户空间进行四次的数据拷贝,而 共享内存则只拷贝两次数据[1]:一次从输入文件到共享内存区,另一次从共享内存区到输出文件。实际上,进程之间在共享内存时,并不总是读写少量数据后就 解除映射,有新的通信时,再重新建立共享内存区域。而是保持共享区域,直到通信完毕为止,这样,数据内容一直保存在共享内存中,并没有写回文件。共享内存 中的内容往往是在解除映射时才写回文件的。因此,采用共享内存的通信方式效率是非常高的。
信号
信号是在软件层次上对中断机制的一种模拟,在原理上,一个进程收到一个信号与处理器收到一个中断请求可以说是一样的。信号是异步的,一个进程不必通过任何操作来等待信号的到达,事实上,进程也不知道信号到底什么时候到达。信号是进程间通信机制中唯一的异步通信机制,可以看作是异步通知,通知接收信号的进程有哪些事情发生了。信号机制经过POSIX实时扩展后,功能更加强大,除了基本通知功能外,还可以传递附加信息。信号事件的发生有两个来源:硬件来源(比如我们按下了键盘或者其它硬件故障);软件来源。信号分为可靠信号和不可靠信号,实时信号和非实时信号。进程有三种方式响应信号1.忽略信号2.捕捉信号3.执行缺省操作。
套接字
这一块在网络编程那一块讲的 很多,在此就不在说拉。
注 next()、nextInt()、nextDouble()、nextLine()
当next()、nextInt()、nextDouble()等等这些之后,你如果不再加一条in.nextLine()的话,下面如果用到了类似str = in.nextLine(); 这条语句的话,会首先读取上面next()、nextInt()、nextDouble()等等这些语句的回车作为一条默认的字符串(为什么是这样的机制呢?还需要继续探索),因此,解决的办法就是在输入next()、nextInt()、nextDouble()等等这些之后,再用一个in.nextLine()这个来截取上面的一个回车操作,后面的nextLine()在第一次才能起作用。(这真是一个大坑!)
注 静态方法
声明为static的方法有以下几条限制(main也是??):
A.它们仅能调用其他的static 方法;
B.它们只能访问static数据;
C.它们不能以任何方式引用this 或super(this涉及到对象,super 与继承有关);
D.非静态可以调用静态。
注 HTTP会话过程包括四个步骤
连接(Connection);请求(Request);应答(Response);关闭(Close)
HTTP协议是基于TCP/IP之上的协议,它不仅保证正确传输超文本文档,还确定传输文档中的哪一部分,以及哪部分内容首先显示(如文本先于图形)等等。
HTTP协议定义了内容的格式,这是一个应用层的协议,应用层协议的内容需要通过传输层在浏览器和服务器直接传送,TCP/IP是网络模型的实现,其中传输层和应用层为主要层。
应用层用于在特定的应用程序之间传输数据。HTTP协议就是TCP/IP协议中专门用于浏览器与Web服务器之间通信的应用层协议。应用层协议依赖于传输层协议完成数据传输,传输层协议依赖于网络层协议完成数据传输。
ISO制定的OSI参考模型的过于庞大、复杂招致了许多批评。与此对照,由技术人员自己开发的TCP/IP协议栈获得了更为广泛的应用。如下图所示,是TCP/IP参考模型和OSI参考模型的对比示意图。
注 TCP/IP参考模型的层次结构及应用协议
TCP/IP参考模型分为四个层次:应用层、传输层、网络互连层和主机到网络层。如下图所示。(PS:DNS亦是应用层协议)
注 广度优先搜索&&深度优先搜索
广度优先搜索需要借助辅助队列记录访问顺序。
DFS_BFS.java
package cn.edu.ujn.graph; import java.util.ArrayList; import java.util.LinkedList; /** * @description 邻接矩阵模型类 * @author SHQ * @time 2016.09.12 */ public class DFS_BFS { private ArrayList<Object> vertexList;// 存储点的链表 private int[][] edges; // 邻接矩阵,用来存储边 private int numOfEdges; // 边的数目 boolean[] isVisited; // 遍历标志位 public DFS_BFS(int n) { //初始化矩阵,一维数组,和边的数目 edges = new int[n][n]; vertexList = new ArrayList<Object>(n); numOfEdges = 0; // 将所有节点访问标志位均置为未访问 isVisited = new boolean[n]; for(int i = 0; i < n; i++){ isVisited[i] = false; } } //得到结点的个数 public int getNumOfVertex() { return vertexList.size(); } //得到边的数目 public int getNumOfEdges() { return numOfEdges; } //返回结点i的数据 public Object getValueByIndex(int i) { return vertexList.get(i); } //返回v1,v2的权值 public int getWeight(int v1,int v2) { return edges[v1][v2]; } //插入结点 public void insertVertex(Object vertex) { vertexList.add(vertexList.size(),vertex); } //插入结点 public void insertEdge(int v1,int v2,int weight) { edges[v1][v2]=weight; numOfEdges++; } //删除结点 public void deleteEdge(int v1,int v2) { edges[v1][v2] = 0; numOfEdges--; } //得到第一个邻接结点的下标 public int getFirstNeighbor(int index) { for(int j = 0; j < vertexList.size(); j++) { if (edges[index][j] > 0) { return j; } } return -1; } //根据前一个邻接结点的下标来取得下一个邻接结点 public int getNextNeighbor(int v1, int v2) { for (int j = v2+1; j < vertexList.size(); j++) { if (edges[v1][j]>0) { return j; } } return -1; } //私有函数,深度优先遍历 private void depthFirstSearch(boolean[] isVisited,int i) { //首先访问该结点,在控制台打印出来 System.out.print(getValueByIndex(i) + " "); //置该结点为已访问 isVisited[i] = true; int w = getFirstNeighbor(i); while (w != -1) { if (!isVisited[w]) { depthFirstSearch(isVisited,w); } w = getNextNeighbor(i, w); } } //对外公开函数,深度优先遍历,与其同名私有函数属于方法重载 public void depthFirstSearch() { for(int i = 0; i < getNumOfVertex(); i++) { //因为对于非连通图来说,并不是通过一个结点就一定可以遍历所有结点的。 if (!isVisited[i]) { depthFirstSearch(isVisited,i); } } } /** * 私有函数,广度优先遍历 * 遍历步骤: * 访问初始结点v并标记结点v为已访问。 * 结点v入队列 * 当队列非空时,继续执行,否则算法结束。 * 出队列,取得队头结点u。 * 查找结点u的第一个邻接结点w。 * 若结点u的邻接结点w不存在,则转到步骤3;否则循环执行以下三个步骤: * 1)若结点w尚未被访问,则访问结点w并标记为已访问。 * 2)结点w入队列 * 3)查找结点u的继w邻接结点后的下一个邻接结点w,转到步骤6。 * @param isVisited * @param i */ private void broadFirstSearch(boolean[] isVisited, int i) { int u, w; // 借助辅助队列,记录访问顺序 LinkedList<Object> queue = new LinkedList<Object>(); // 访问结点i System.out.print(getValueByIndex(i) + " "); isVisited[i] = true; // 结点入队列 queue.addLast(i); while (!queue.isEmpty()) { u = ((Integer)queue.removeFirst()).intValue(); w = getFirstNeighbor(u); while(w != -1) { if(!isVisited[w]) { //访问该结点 System.out.print(getValueByIndex(w)+" "); //标记已被访问 isVisited[w] = true; //入队列 queue.addLast(w); } //寻找下一个邻接结点 w = getNextNeighbor(u, w); } } } //对外公开函数,广度优先遍历 public void broadFirstSearch() { for(int i = 0; i < getNumOfVertex(); i++) { if(!isVisited[i]) { broadFirstSearch(isVisited, i); } } } }
main.java
package cn.edu.ujn.graph; public class Main { public static void main(String args[]) { int n = 8, e = 9; // 分别代表结点个数和边的数目 String labels[]={"1","2","3","4","5","6","7","8"}; //结点的标识 DFS_BFS graph = new DFS_BFS(n); for(String label : labels) { graph.insertVertex(label); // 插入结点 } //插入九条边 graph.insertEdge(0, 1, 1); graph.insertEdge(0, 2, 1); graph.insertEdge(1, 3, 1); graph.insertEdge(1, 4, 1); graph.insertEdge(3, 7, 1); graph.insertEdge(4, 7, 1); graph.insertEdge(2, 5, 1); graph.insertEdge(2, 6, 1); graph.insertEdge(5, 6, 1); graph.insertEdge(1, 0, 1); graph.insertEdge(2, 0, 1); graph.insertEdge(3, 1, 1); graph.insertEdge(4, 1, 1); graph.insertEdge(7, 3, 1); graph.insertEdge(7, 4, 1); graph.insertEdge(6, 2, 1); graph.insertEdge(5, 2, 1); graph.insertEdge(6, 5, 1); /* System.out.println("深度优先搜索序列为:"); graph.depthFirstSearch(); System.out.println();*/ System.out.println("广度优先搜索序列为:"); graph.broadFirstSearch(); } }
附 阿里变态附加题



附 sort()方法在J6、J7中的区别
在Java 6中Arrays.sort()和Collections.sort()使用的是MergeSort,而在Java 7中,内部实现换成了TimSort,其对对象间比较的实现要求更加严格:
Comparator的实现必须保证以下几点(出自这儿):
a). sgn(compare(x, y)) == -sgn(compare(y, x))
b). (compare(x, y)>0) && (compare(y, z)>0) 意味着 compare(x, z)>0
c). compare(x, y)==0 意味着对于任意的z:sgn(compare(x, z))==sgn(compare(y, z)) 均成立
PS: TimSort不仅内置在各种JDK 7的版本,也存在于Android SDK中(尽管其并没有使用JDK 7)。