美团点评2018实习生研发工程师编程题
寻找二叉树两个节点的最低公共祖先(LCA)
给定一棵树,同时给出树中的两个结点(p和q),求它们的最低公共祖先。也就是常见的LCA(Lowest Common Ancestor )问题。
public class TreeNode {
int val = 0;
TreeNode left = null;
TreeNode right = null;
public TreeNode(int val) {
this.val = val;
}
}
如果是一个二分查找树
在二分查找树中,寻找两个节点的最低公共祖先。
1、如果p、q都比根节点小,则在左子树中递归查找公共节点。
2、如果p、q都比根节点大,则在右子树中查找公共祖先节点。
3、如果p、q一个比根节点大,一个比根节点小,或者有一个等于根节点,则根节点即为最低公共祖先。
public TreeNode lowestCommonAncestor(TreeNode root, TreeNode p, TreeNode q) {
if (root == null || q == null || p == null)
return null;
// 如果root.val 比左右子树大,说明在root的左子树
if (root.val > Math.max(q.val, p.val))
return lowestCommonAncestor(root.left, p, q);
// 如果root.val 比左右子树小,说明在root的右子树
if (root.val < Math.min(q.val, p.val))
return lowestCommonAncestor(root.right, p, q);
return root;
如果是一个普通的二叉树
从root开始遍历,如果p和q中的任一个和root匹配,那么root就是LCA。
如果都不匹配,则分别递归左、右子树,
如果有一个 key(p或q)出现在左子树,并且另一个key(p或q)出现在右子树,则root就是LCA.
如果两个key都出现在左子树,则说明LCA在左子树中,否则在右子树。
public TreeNode lowestCommonAncestor(TreeNode root, TreeNode p, TreeNode q) {
if (root == null || q == null || p == null)
return null;
// 注意:如果 一个节点是另一个祖先,则返回的是祖先节点
if (q == root || p == root)
return root;
TreeNode left = lowestCommonAncestor(root.left, p, q);
TreeNode right = lowestCommonAncestor(root.right, p, q);
// 如果都返回非空指针, 则说明两个节点分别出现了在两个子树中
// 则当前节点肯定为LCA
if (left != null && right != null)
return root;
// 如果一个为空,在说明LCA在另一个子树
return (left == null) ? right : left;
}
非递归实现
简单的复杂度为 O(n) 的算法,解决LCA问题
1)找到从根到p的路径,并存储在一个向量或数组中。
2)找到从根到q的路径,并存储在一个向量或数组中。
3) 遍历这两条路径,直到遇到一个不同的节点,则前面的那个即为最低公共祖先.
public TreeNode lowestCommonAncestor(TreeNode root, TreeNode p, TreeNode q) {
if (root == null || q == null || p == null)
return null;
List<TreeNode> listP = new ArrayList<>();
boolean isFindPathToP = findPathFromRoot(root, listP, p);
List<TreeNode> listQ = new ArrayList<>();
boolean isFindPathToQ = findPathFromRoot(root, listQ, q);
if (isFindPathToP && isFindPathToQ) {
TreeNode ans = null;
for (int i = 0; i < Math.min(listP.size(), listQ.size()); i++) {
if (listP.get(i) != listQ.get(i))
continue;
ans = listP.get(i);
}
return ans;
}
return null;
}
private boolean findPathFromRoot(TreeNode root, List<TreeNode> list, TreeNode node) {
if (root == null)
return false;
list.add(root);
// 注意不是root.val==node.val!!!!
if (root == node)
return true;
//左子树或右子树 是否找到,找到的话当前节点就在路径中了
boolean find = findPathFromRoot(root.left, list, node) || findPathFromRoot(root.right, list, node);
if (find)
return true;
// 该节点下未找到就弹出,并返回false
list.remove(list.size() - 1);
return false;
}
计算超大数的阶乘
补充计算阶乘的位数:我们知道整数n的位数的计算方法为:log10(n)+1
故n!的位数为log10(n!)+1
如果要求出n!的具体值,对很大的n(例如n=1000000)来说,计算会很慢,如果仅仅是求阶乘的位数,可以用斯特林(Stirling)公式求解
斯特林(Stirling)公式:
于是求n!的位数就是求log10((2*PI*n)^1/2*(n/e)^n)+1
即 1/2*log10(2*PI*n)+n*log10(n/e)+1
例如:n!位数为m, 则m=〔lgn+lg(n-1)+...+lg3+lg2+lg1]+1 其中〔lga〕表示a取常用对数后的整数部分
import java.util.Arrays;
public class BigInteger {
/**
* 计算进位
*
* @param bit
* 数组
* @param pos
* 用于判断是否是数组的最高位
*/
private void carry(int[] bit, int pos) {
int i, carray = 0;
for (i = 0; i <= pos; i++)// 从0到pos逐位检查是否需要进位
{
bit[i] += carray;// 累加进位
if (bit[i] <= 9) // 小于9不进位
{
carray = 0;
} else if (bit[i] > 9 && i < pos)// 大于9,但不是最高位
{
carray = bit[i] / 10;// 保存进位值
bit[i] = bit[i] % 10;// 得到该位的一位数
} else if (bit[i] > 9 && i >= pos)// 大于9,且是最高位
{
while (bit[i] > 9)// 循环向前进位
{
carray = bit[i] / 10;// 计算进位值
bit[i] = bit[i] % 10;// 当前的第一位数
i++;
bit[i] = carray;// 在下一位保存进位值
}
}
}
}
/**
* 大整数阶乘
*
* @param bigInteger
* 所计算的大整数
*/
private void bigFactorial(int bigInteger) {
int pos = 0;//
int digit;// 数据长度
int a, b;
int m = 0;// 统计输出位数
int n = 0;// 统计输出行数
double sum = 0;// 阶乘位数
for (a = 1; a <= bigInteger; a++)// 计算阶乘位数
{
sum += Math.log10(a);
}
digit = (int) sum + 1;// 数据长度
int[] fact = new int[digit];// 初始化一个数组
fact[0] = 1;// 设个位为 1,也是1 的阶乘
for (a = 2; a <= bigInteger; a++)// 将2~bigInteger逐个与原来的积相乘
{
for (b = digit - 1; b >= 0; b--)// 查找最高位{}
{
// fact[0]存储的是个位
if (fact[b] != 0) {
pos = b;// 记录最高位
break;
}
}
for (b = 0; b <= pos; b++) {
fact[b] *= a;// 每一位与a乘,不明白可以想像一下小学乘法公式
}
carry(fact, pos);
}
for (b = digit - 1; b >= 0; b--) {
if (fact[b] != 0) {
pos = b;// 记录最高位
break;
}
}
System.out.println(bigInteger + "阶乘结果为:");
for (a = pos; a >= 0; a--)// 输出计算结果
{
System.out.print(fact[a]);
m++;
if (m % 5 == 0) {
System.out.print(" ");
}
if (40 == m) {
System.out.println("");
m = 0;
n++;
if (10 == n) {
System.out.print("\n");
n = 0;
}
}
}
System.out.println("\n" + "阶乘共有: " + (pos + 1) + " 位");
}
public void doBigFactorial(int bigInteger) {
int timeBegin = (int) System.currentTimeMillis();
this.bigFactorial(bigInteger);
int timeFinishi = (int) System.currentTimeMillis();
int time = timeFinishi - timeBegin;
System.out.println("计算耗时: " + time + "毫秒");
}
public static void main(String[] args) {
BigInteger bi = new BigInteger();
bi.doBigFactorial(10);
}
}
大整数阶乘参考于:http://www.open-open.com/home/space-135360-do-blog-id-9620.html
网易2016实习研发工程师编程题
比较重量
小明陪小红去看钻石,他们从一堆钻石中随机抽取两颗并比较她们的重量。这些钻石的重量各不相同。在他们们比较了一段时间后,它们看中了两颗钻石g1和g2。现在请你根据之前比较的信息判断这两颗钻石的哪颗更重。给定两颗钻石的编号g1,g2,编号从1开始,同时给定关系数组vector,其中元素为一些二元组,第一个元素为一次比较中较重的钻石的编号,第二个元素为较轻的钻石的编号。最后给定之前的比较次数n。请返回这两颗钻石的关系,若g1更重返回1,g2更重返回-1,无法判断返回0。输入数据保证合法,不会有矛盾情况出现。
测试样例:
2,3,[[1,2],[2,4],[1,3],[4,3]],4
返回: 1
//使用这个集合构建一个有向图,比如 [1,2]表示成一条边1->2,同时,
//弧尾表示较大的一个数;
//然后,我们在这个图中寻找2,3之间有没有通路,
//注意这里,因为是有向图,路径可能为2到3,或者3到2,或者不存在。
//采用邻接矩阵+传递性实现
public int cmp(int g1, int g2, int[][] records, int n) {
{
int maxNum = Integer.MIN_VALUE;
// 使用最大值构造一个map
for (int i = 0; i < n; i++) {
maxNum = Math.max(maxNum, records[i][0]);
maxNum = Math.max(maxNum, records[i][1]);
}
// 构造有向图,map存储从1 开始(map[i][0]或者map[0][j])不存储,所以maxNum+1;
int[][] map = new int[maxNum + 1][maxNum + 1];
// 初始化对角线为1,其他为0
for (int i = 1; i <= maxNum; i++) {
for (int j = 1; j <= maxNum; j++) {
if (i == j)
map[i][j] = 1;
else
map[i][j] = 0;
}
}
// 把二维数组中出现的存储为1,如果map从0开始,此处计算需要与1做运算
for (int i = 0; i < n; i++) {
map[records[i][0]][records[i][1]] = 1;
}
// 传递性,如果(i,k),(k,j)为1,那么(i,j)设为1
for (int k = 1; k <= maxNum; k++) {
for (int i = 1; i <= maxNum; i++) {
for (int j = 1; j <= maxNum; j++) {
if (map[i][k] == 1 && map[k][j] == 1) {
map[i][j] = 1;
}
}
}
}
// 如果map值为1 ,则g1大
if (map[g1][g2] == 1)
return 1;
else if (map[g2][g1] == 1)
return -1;
else
return 0;
}
}
采用邻接矩阵+深度优先遍历实现
public int cmp(int g1, int g2, int[][] records, int n) {
{
int maxNum = Integer.MIN_VALUE;
// 使用最大值构造一个map
for (int i = 0; i < n; i++) {
maxNum = Math.max(maxNum, records[i][0]);
maxNum = Math.max(maxNum, records[i][1]);
}
// 构造有向图,map存储从1 开始(map[i][0]或者map[0][j])不存储,所以maxNum+1;
boolean[][] map = new boolean[maxNum + 1][maxNum + 1];
// 把二维数组中出现的存储为1,如果map从0开始,此处计算需要与1做运算
for (int i = 0; i < n; i++) {
map[records[i][0]][records[i][1]] = true;
}
// 构造结点是否访问的集合
boolean[] isVisited = new boolean[maxNum + 1];
if (dfs(isVisited, g1, g2, map))
return 1;
if (dfs(isVisited, g2, g1, map))
return -1;
return 0;
}
}
// 采用深度优先遍历
private boolean dfs(boolean[] isVisited, int g1, int g2, boolean[][] map) {
if (map[g1][g2])
return true;
for (int i = 1; i < isVisited.length; i++) {
if (map[g1][i] && !isVisited[i]) {
isVisited[i] = true;
// 继续遍历以i为开始结点的深度遍历
if (dfs(isVisited, i, g2, map))
return true;
}
}
return false;
}
采用邻接表+深度优先遍历
public boolean isRechable = false;
public static void main(String[] args) {
int re[][] = { { 1, 2 }, { 2, 4 }, { 1, 3 }, { 4, 3 } };
System.out.println(new Cmp().cmp(3, 2, re, 4));
}
public int cmp(int g1, int g2, int[][] records, int n) {
// 产生一个邻接表,map+list
Map<Integer, List<Integer>> map = new HashMap<>();
for (int i = 0; i < n; i++) {
int key = records[i][0];
int value = records[i][1];
if (map.containsKey(key)) {
map.get(key).add(value);
} else {
List<Integer> list = new ArrayList<>();
list.add(value);
map.put(key, list);
}
}
if (map.isEmpty()) {
return 0;
}
DFS(map, g1, g2);
if (isRechable)
return 1;
DFS(map, g2, g1);
if (isRechable)
return -1;
return 0;
}
// 深度优先遍历
private void DFS(Map<Integer, List<Integer>> map, int g1, int g2) {
if (!map.containsKey(g1))
isRechable = false;
// 获取key为g1的value
else {
List<Integer> values = map.get(g1);
for (Integer value : values) {
if (value == g2) {
isRechable = true;
return;
} else
DFS(map, value, g2);
}
}
}
下面是一个简单的复杂度为 O(n) 的算法,解决LCA问题
1) 找到从根到q的路径,并存储在一个向量或数组中。
2)找到从根到p的路径,并存储在一个向量或数组中。
3) 遍历这两条路径,直到遇到一个不同的节点,则前面的那个即为最低公共祖先.
网易2017秋招编程题集合
回文序列
如果一个数字序列逆置之后跟原序列是一样的就称这样的数字序列为回文序列。例如:
{1, 2, 1}, {15, 78, 78, 15} , {112} 是回文序列,
{1, 2, 2}, {15, 78, 87, 51} ,{112, 2, 11} 不是回文序列。
现在给出一个数字序列,允许使用一种转换操作:
选择任意两个相邻的数,然后从序列移除这两个数,并用这两个数字的和插入到这两个数之前的位置(只插入一个和)。
现在对于所给序列要求出最少需要多少次操作可以将其变成回文序列。
输入描述:
输入为两行,第一行为序列长度n ( 1 ≤ n ≤ 50)
第二行为序列中的n个整数item[i] (1 ≤ iteam[i] ≤ 1000),以空格分隔。
输出描述:
输出一个数,表示最少需要的转换次数
输入例子:
4
1 1 1 3
输出例子:
2
思路:分别使用两个指针,从两头向中间靠拢。如果两个指针处的数值相等,两个指针同时向中间移动。?如果left指针处的值较小,让left指针处的数值不断地与后面的元素相加,直至>=right处的数值,然后将得到的和存放在left指针处,在加的过程中,加的次数就合并的次数。?如果right指针处的数值较小,让right不断地与前面的数值相加,直至和>=left处的数值,然后将和保存在right指针处,在加的过程中,加的次数就是合并的次数。
import java.util.ArrayList;
import java.util.Scanner;
public class Main {
public static void main(String[] args) {
Scanner scanner = new Scanner(System.in);
while (scanner.hasNext()) {
int n = scanner.nextInt();
int[] item = new int[n];
for (int i = 0; i < n; i++) {
item[i] = scanner.nextInt();
}
System.out.println(leastTimeToHuiwen(n, item));
}
}
public static int leastTimeToHuiwen(int n, int[] item) {
// 比较第一个和最后一个数,如果第一个大,则前两个相加替换原来位置。
// 如果最后一个数大,则最后两个相加替换原来位置。
// 如果首尾元素相等,则删除首尾元素。
int count = 0;
ArrayList<Integer> list = new ArrayList<Integer>();
for (int i = 0; i < n; i++) {
list.add(item[i]);
}
while (list.size() > 1) {
if (list.get(0) < list.get(list.size() - 1)) {
int a = list.get(0);
int b = list.get(1);
list.set(0, a + b);
list.remove(1);
count++;
} else if (list.get(0) > list.get(list.size() - 1)) {
int a = list.get(list.size() - 1);
int b = list.get(list.size() - 2);
list.set(list.size() - 2, a + b);
list.remove(list.size() - 1);
count++;
} else {
list.remove(0);
list.remove(list.size() - 1);
}
}
return count;
}
}
第二种方法:
private static int leastTimeToHuiwen(int n, int[] item) {
int start = 0;
int end = n - 1;
int count = 0;
while (start < end) {
if (item[start] < item[end]) {
// 头指针位置的值小于尾,头指针+1,并将新指针的值加上旧指针的值
item[start + 1] = item[start] + item[start + 1];
start++;
count++;
} else if (item[start] > item[end]) {
// 尾指针位置的值小于头,尾指针-1,并将新指针的值加上旧指针的值
item[end - 1] = item[end] + item[end - 1];
end--;
count++;
} else {
// 相等,两边都往中间移动
start++;
end--;
}
}
return count;
}
网易有道2017内推选择题
洗牌
洗牌在生活中十分常见,现在需要写一个程序模拟洗牌的过程。现在需要洗2n张牌,从上到下依次是第1张,第2张,第3张一直到第2n张。首先,我们把这2n张牌分成两堆,左手拿着第1张到第n张(上半堆),右手拿着第n+1张到第2n张(下半堆)。接着就开始洗牌的过程,先放下右手的最后一张牌,再放下左手的最后一张牌,接着放下右手的倒数第二张牌,再放下左手的倒数第二张牌,直到最后放下左手的第一张牌。接着把牌合并起来就可以了。例如有6张牌,最开始牌的序列是1,2,3,4,5,6。首先分成两组,左手拿着1,2,3;右手拿着4,5,6。在洗牌过程中按顺序放下了6,3,5,2,4,1。把这六张牌再次合成一组牌之后,我们按照从上往下的顺序看这组牌,就变成了序列1,4,2,5,3,6。 现在给出一个原始牌组,请输出这副牌洗牌k次之后从上往下的序列。
输入描述:
第一行一个数T(T ≤ 100),表示数据组数。对于每组数据,第一行两个数n,k(1 ≤ n,k ≤ 100),
接下来一行有2n个数a1,a2,...,a2n(1 ≤ ai ≤ 1000000000)。表示原始牌组从上到下的序列。
输出描述:
对于每组数据,输出一行,最终的序列。数字之间用空格隔开,不要在行末输出多余的空格。
输入例子:
3
3 1
1 2 3 4 5 6
3 2
1 2 3 4 5 6
2 2
1 1 1 1
输出例子:
1 4 2 5 3 6
1 5 4 3 2 6
1 1 1 1
import java.util.*;
public class Main {
// 表示左手边的牌
private Stack<Integer> left = new Stack<Integer>();
// 表示右手边的牌
private Stack<Integer> right = new Stack<Integer>();
public static void main(String[] args) {
Main mm = new Main();
Scanner sc = new Scanner(System.in);
// 每次洗牌后的结果
List<Integer> tempRes = new ArrayList<Integer>();
// 存储所有组数的结果
List<String> result = new ArrayList<String>();
int m = sc.nextInt();
int n = 0, k = 0;
while (m != 0) {
n = sc.nextInt();
k = sc.nextInt();
for (int i = 0; i < 2 * n; i++) {
tempRes.add(sc.nextInt());
}
while (k != 0) {// k次洗牌
mm.pokeSort(tempRes);
k--;
} // 存储每组结果
result.add(tempRes.toString().replace("[", "").replace("]", "").replace(",", ""));
m--;
tempRes.clear();
}
for (String str : result)// 打印结果
System.out.println(str);
}
private void pokeSort(List<Integer> tempRes) {
for (int i = 0; i < tempRes.size() / 2; i++)
left.push(tempRes.get(i));
for (int i = tempRes.size() / 2; i < tempRes.size(); i++)
right.push(tempRes.get(i));
// 注意清空
tempRes.clear();
while (!left.isEmpty()) {
tempRes.add(right.pop());
tempRes.add(left.pop());
}
Collections.reverse(tempRes);
}
}
构造队列
小明同学把1到n这n个数字按照一定的顺序放入了一个队列Q中。现在他对队列Q执行了如下程序:
while(!Q.empty()) //队列不空,执行循环
{
int x=Q.front(); //取出当前队头的值x
Q.pop(); //弹出当前队头
Q.push(x); //把x放入队尾
x = Q.front(); //取出这时候队头的值
printf("%d\n",x); //输出x
Q.pop(); //弹出这时候的队头
}
做取出队头的值操作的时候,并不弹出当前队头。
小明同学发现,这段程序恰好按顺序输出了1,2,3,...,n。现在小明想让你构造出原始的队列,你能做到吗?[注:原题样例第三行5有错,应该为3,以下已修正]
输入描述:
第一行一个整数T(T ≤ 100)表示数据组数,每组数据输入一个数n(1 ≤ n ≤ 100000),输入的所有n之和不超过200000。
输出描述:
对于每组数据,输出一行,表示原始的队列。数字之间用一个空格隔开,不要在行末输出多余的空格.
输入例子:
4
1
2
3
10
输出例子:
1
2 1
2 1 3
8 1 6 2 10 3 7 4 9 5
import java.util.*;
public class Main{
public static void main(String[] args) {
//第一种方法,模拟相反操作
solution1();
//第二种方法
solution2();
}
public static void solution1() {
//双端队列
Deque<Integer> q = new LinkedList<>();
Scanner sc = new Scanner(System.in);
int k = sc.nextInt();
while (k-- > 0) {
int n = sc.nextInt();
for (int i = n; i > 0; i--) {
q.offerFirst(i);
int t = q.peekLast();
q.pollLast();
q.offerFirst(t);
}
for (int i = 1; i < n; i++)
System.out.print(q.pollFirst() + " ");
System.out.println(q.pollFirst());
}
}
/* * 思想:先使用一个1到n的数组模拟小明的操作,
* * 然后会得到一组输出,例如:3,5,10,7....
* * 按题意是应该输出 1,2,3,4....
* * 这样,我们就可以反推出
* * 1应该在第3个位置
* * 2应该在第5个位置
* * 3应该在第10个位置
* * 4应该在第7个位置 * ....
*/
public static void solution2() {
Queue<Integer> q = new LinkedList<>();
Scanner sc = new Scanner(System.in);
int k = sc.nextInt();
while (k-- > 0) {
int n = sc.nextInt();
for (int i = 1; i <= n; i++) {
q.offer(i);
}
int cnt = 1;
int num[] = new int[n + 1];
while (!q.isEmpty()) {
int x = q.peek();
q.poll();
q.offer(x);
num[q.peek()] = cnt++;
q.poll();
}
for (int i = 1; i < n; i++)
System.out.print(num[i] + " ");
System.out.println(num[n]);
}
}
}