为什么建议程序员一定要进大厂?
那年十八 母校舞会 站着如喽罗
那时候 我含泪 发誓各位 必须看到我
第一题:从1到n整数中1出现的次数
求出1~13的整数中 1 出现的次数,并算出 100~1300 的整数中1出现的次数?为此他特别数了一下 1~13 中包含1的数字有 1、10、11、12、13 因此共出现 6 次,但是对于后面问题他就没辙了。ACMer 希望你们帮帮他,并把问题更加普遍化,可以很快的求出任意非负整数区间中1出现的次数(从1 到 n 中1出现的次数)。
解题思路
- 假定
$$n=21345$$
将数字分为首位和非首位两个部分 - 对于首位为 1 的情况,如果首位
$$>1$$
那么$$sum=sum+10^{len(n)-1}$$
,如果首位$$=1$$
那么$$sum=sum+1$$
- 对于非首位 1,指定其中一位为 1,根据排列组合有
$$10^{len(n)-2}\times(len(n)-1)$$
个。那么非首位 1 总共有$$2\times10^{len(n)-2}\times(len(n)-1)$$
public int NumberOf1Between1AndN_Solution(int n) {
int[] res = {0};
NumberOf1Between1AndN(res, n);
return res[0];
}
private void NumberOf1Between1AndN(int[] res, int n) {
//假设 num=21345
String num = String.valueOf(n);
int firstNum = num.charAt(0) - '0';
if (num.length() == 1) {
if (firstNum > 0) res[0]++;
return;
}
String nextNum = num.substring(1);
int nextN = Integer.valueOf(nextNum);
//数字 10000 ~ 19999 的第一位中的个数
if (firstNum > 1) {
res[0] += Math.pow(10, num.length() - 1);
} else if (firstNum == 1) {
res[0] += nextN + 1;
}
//1346 ~ 21345 除第一位之外的数的个数
res[0] += firstNum * (num.length() - 1) * Math.pow(10, num.length() - 2);
NumberOf1Between1AndN(res, nextN);
}
第二题:把数组排成最小的数
输入一个正整数数组,把数组里所有数字拼接起来排成一个数,打印能拼接出的所有数字中最小的一个。例如输入数组{3,32,321},则打印出这三个数字能排成的最小数字为321323。
解题思路
- 最直接的办法就是,找到数组中数字的所有排列组合,找到最小的
- 对于
$$m
,n$$
,可以组成$$mn
,nm$$
这两个数,如果$$mn < nm$$
那么,$$m$$
应该在$$n$$
之前 - 对于一组数,可以通过上述规则进行排序,依次打印出来就是最小的数
- 由于组合之后的数可能超出 int 的表示范围,注意使用字符串来处理大数问题
public String PrintMinNumber(int[] numbers) {
List nums = new ArrayList<>();
for (int number : numbers) {
nums.add(String.valueOf(number));
}
nums.sort(Comparator.comparing(s -> s, (o1, o2) -> (o1 + o2).compareTo(o2 + o1)));
StringJoiner joiner = new StringJoiner("");
nums.forEach(joiner::add);
return joiner.toString();
}
第三题:丑数
把只包含质因子2、3和5的数称作丑数(Ugly Number)。例如6、8都是丑数,但14不是,因为它包含质因子7。 习惯上我们把1当做是第一个丑数。求按从小到大的顺序的第N个丑数。
解题思路
- 通过保存已有丑数的方式,用空间换时间
- 对于已有丑数
$$M$$
,那么下一个丑数$$M=\min(M{2}\times2,M{3}\times3,M_{5}\times5)$$
3.$$M{max}$$
是目前最大的丑数,那么$$M{2}$$
是已有丑数中$$M{2}\times2$$
第一个大于$$M{max}$$
的丑数
public int GetUglyNumber_Solution(int index) {
if (index == 0) {
return 0;
}
if (index == 1) {
return 1;
}
ArrayList list = new ArrayList<>(index);
list.add(1);
int preIndex2 = 0;
int preIndex3 = 0;
int preIndex5 = 0;
for (int i = 0; i < index; i++) {
int next2 = list.get(preIndex2) * 2;
int next3 = list.get(preIndex3) * 3;
int next5 = list.get(preIndex5) * 5;
int nextV = Math.min(Math.min(next2, next3), next5);
list.add(nextV);
while (preIndex2 < list.size() - 1 && list.get(preIndex2) * 2 <= nextV) preIndex2++;
while (preIndex3 < list.size() - 1 && list.get(preIndex3) * 3 <= nextV) preIndex3++;
while (preIndex5 < list.size() - 1 && list.get(preIndex5) * 5 <= nextV) preIndex5++;
}
return list.get(index - 1);
}
第四题:第一个只出现一次的字符
在一个字符串(0<=字符串长度<=10000,全部由字母组成)中找到第一个只出现一次的字符,并返回它的位置, 如果没有则返回 -1(需要区分大小写).
解题思路
- 通过 LinkedHashMap 记录数组顺序,然后计算字符出现的次数
- 遍历找到第一个只出现 1次 的字符
public int FirstNotRepeatingchar(String str) {
LinkedHashMap data = new LinkedHashMap<>();
char[] chars = str.toCharArray();
for (char c : chars) {
Integer count = data.getOrDefault(c, 0);
data.put(c, count + 1);
}
Character res = null;
for (Character c : data.keySet()) {
if (data.get(c) == 1) {
res = c;
break;
}
}
if (res == null) {
return -1;
}
for (int i = 0; i < chars.length; i++) {
if (chars[i] == res) {
return i;
}
}
return -1;
}
第五题:数组中的逆序对
在数组中的两个数字,如果前面一个数字大于后面的数字,则这两个数字组成一个逆序对。输入一个数组,求出这个数组中的逆序对的总数P。并将P对1000000007取模的结果输出。 即输出P%1000000007
输入描述: 题目保证输入的数组中没有的相同的数字
数据范围:
对于%50的数据,size<=10^4
对于%75的数据,size<=10^5
对于%100的数据,size<=2*10^5
解题思路
- 使用归并排序的方式,划分子数组
- 两个子数组进行对比,有两个分别指向两个数组末尾的指针
f,s
,数组分割下标为mid
,如果array[f] > array[s]
那么,就有s - mid
个array[f]
的逆序 - 依此类推,最终将数组排序,并且获得结果
public int InversePairs(int[] array) {
long[] sum = {0};
if (array == null || array.length == 0) {
return (int) sum[0];
}
int[] temp = new int[array.length];
mergeSort(array, 0, array.length - 1, temp, sum);
return (int) (sum[0] % 1000000007);
}
private void mergeSort(int[] array, int start, int end, int[] temp, long[] sum) {
if (start == end) {
return;
}
int mid = (start + end) / 2;
mergeSort(array, start, mid, temp, sum);
mergeSort(array, mid + 1, end, temp, sum);
int f = mid, s = end;
int t = end;
while (f >= start && s >= mid + 1) {
if (array[f] > array[s]) {
temp[t--] = array[f--];
sum[0] += s - mid;
} else {
temp[t--] = array[s--];
}
}
while (f >= start) {
temp[t--] = array[f--];
}
while (s >= mid + 1) {
temp[t--] = array[s--];
}
for (int i = end, j = end; i >= start; ) {
array[j--] = temp[i--];
}
}
第六题:两个链表的第一个公共结点
输入两个链表,找出它们的第一个公共结点。
解决思路
空间复杂度 O(n) 的算法
- 使用辅助容器,保存第一个链表的所有元素
- 遍历第二个链表,并对比当前节点是否在辅助容器中
/**
* 空间 O(n)
*
* @param pHead1
* @param pHead2
* @return
*/
public ListNode FindFirstCommonNode_1(ListNode pHead1, ListNode pHead2) {
Set node1s = new HashSet<>();
while (pHead1 != null) {
node1s.add(pHead1);
pHead1 = pHead1.next;
}
while (pHead2 != null) {
if (node1s.contains(pHead2)) {
return pHead2;
}
pHead2 = pHead2.next;
}
return null;
}
空间复杂度 O(1) 的算法
- 由于两个链表有可能不一样长,首先通过遍历找到他们的长度
- 移动较长的那个链表,使得两个链表长度一致
- 同步遍历两个链表
原理:如果两个链表相交,那么它们一定有相同的尾节点
/**
* 空间 O(1)
*
* @param pHead1
* @param pHead2
* @return
*/
public ListNode FindFirstCommonNode_2(ListNode pHead1, ListNode pHead2) {
int len1 = 0, len2 = 0;
ListNode cursor1 = pHead1, cursor2 = pHead2;
while (cursor1 != null) {
cursor1 = cursor1.next;
len1++;
}
while (cursor2 != null) {
cursor2 = cursor2.next;
len2++;
}
cursor1 = pHead1;
cursor2 = pHead2;
if (len1 > len2) {
int i = len1;
while (i != len2) {
cursor1 = cursor1.next;
i--;
}
} else if (len1 < len2) {
int i = len2;
while (i != len1) {
cursor2 = cursor2.next;
i--;
}
}
while (cursor1 != null && cursor2 != null) {
if (cursor1 == cursor2) {
return cursor1;
}
cursor1 = cursor1.next;
cursor2 = cursor2.next;
}
return null;
}
第七题:数字在排序数组中出现的次数
统计一个数字在排序数组中出现的次数。
解题思路
- 利用二分查找,找到任意一个 k
- 由于 k 有多个,并且当前找到的 k 可能在任意位置。所以,在当前 k 的前后进行遍历查找
public int GetNumberOfK(int[] array, int k) {
if (array == null || array.length == 0) {
return 0;
}
//二分查找
int start = 0, end = array.length - 1;
int t = -1;
while (start < end) {
int mid = (start + end) / 2;
if (array[mid] == k) {
t = mid;
break;
} else if (array[mid] > k) {
end = mid - 1;
} else {
start = mid + 1;
}
}
if (array[start] == k) {
t = start;
}
if (t == -1) {
return 0;
}
//左侧
int sum = 0;
int a = t;
while (a >= 0 && array[a] == k) {
sum++;
a--;
}
//右侧
a = t + 1;
while (a < array.length && array[a] == k) {
sum++;
a++;
}
return sum;
}
第八题:二叉树的深度
输入一棵二叉树,求该树的深度。从根结点到叶结点依次经过的结点(含根、叶结点)形成树的一条路径,最长路径的长度为树的深度。
解题思路
- 深度优先遍历
public int TreeDepth(TreeNode root) {
int[] max = {0};
depth(root, max, 1);
return max[0];
}
private void depth(TreeNode root, int[] max, int curDepth) {
if (root == null) return;
if (curDepth > max[0]) max[0] = curDepth;
depth(root.left, max, curDepth + 1);
depth(root.right, max, curDepth + 1);
}
第九题:数组中只出现一次的数字
一个整型数组里除了两个数字之外,其他的数字都出现了两次。请写程序找出这两个只出现一次的数字。
解题思路
- 两个相等的数字进行异或的结果为0
- 在这个特殊的数组中,重复出现的数字只能为2次,那么如果将所有数字异或 就等价与将两个不同的数字进行异或
- 异或的结果肯定有一位为1,那么这两个不同的数字,在这一位上不同。
- 找到第一个为1的位,并将第一位为1的位是否为1作为分组条件,相同的数字一定在同一个分组里,整个数组分组异或
- 得到两个结果,即为两个不同的数
/**
* num1,num2分别为长度为1的数组。传出参数。将num1[0],num2[0]设置为返回结果
* @param array
* @param num1
* @param num2
*/
public void FindNumsAppearOnce(int[] array, int num1[], int num2[]) {
if (array == null || array.length < 3) {
return;
}
int result = array[0];
for (int i = 1; i < array.length; i++) {
result ^= array[i];
}
//找到第一个为1的位
int indexOfFirstBit1 = 0;
int temp = result;
while (temp != 0) {
indexOfFirstBit1++;
temp >>>= 1;
}
int mask = 1;
for (int i = 1; i < indexOfFirstBit1; i++) {
mask <<= 1;
}
//将第一位为1的位是否为1作为分组条件,分组异或
int n1 = -1, n2 = -1;
for (int i : array) {
if ((i & mask) == mask) {
if (n1 == -1) n1 = i; else n1 ^= i;
} else {
if (n2 == -1) n2 = i; else n2 ^= i;
}
}
num1[0] = n1;
num2[0] = n2;
}
第十题:和为S的两个数字
输入一个递增排序的数组和一个数字S,在数组中查找两个数,使得他们的和正好是S,如果有多对数字的和等于S,输出两个数的乘积最小的。
对应每个测试案例,输出两个数,小的先输出。
解题思路
- 利用二分查找的思想,由于是排序数组,通过两个指针来进行遍历
public ArrayList FindNumbersWithSum(int[] array, int sum) {
ArrayList res = new ArrayList<>();
if (array == null || array.length == 1) {
return res;
}
int start = 0, end = array.length - 1;
int minMulti = Integer.MAX_VALUE;
int a = -1, b = -1;
while (start < end) {
int t = array[start] + array[end];
if (t == sum) {
int multi = array[start] * array[end];
if (multi < minMulti) {
a = array[start];
b = array[end];
minMulti = multi;
}
start++;
end--;
} else if (t > sum) end--; else start++;
}
if (a == -1 || b == -1) {
return res;
}
res.add(a);
res.add(b);
return res;
}
第十一题:和为S的连续正数序列
输出所有和为S的连续正数序列。序列内按照从小至大的顺序,序列间按照开始数字从小到大的顺序
解题思路
- 与上一个题目类似,需要确定的是序列的最大值,不超过 sum
- 使用窗口模式,两个指针定义一个窗口,和为 t
public ArrayList> FindContinuousSequence(int sum) {
ArrayList> res = new ArrayList<>();
if (sum == 1) {
return res;
}
int start = 1, end = 2;
int t = start + end;
while (start < end) {
if (t == sum) {
ArrayList ints = new ArrayList<>();
for (int i = start; i <= end; i++) {
ints.add(i);
}
res.add(ints);
t -= start;
start++;
} else if (t > sum) {
t -= start;
start++;
} else {
if (end >= sum) break;
end++;
t += end;
}
}
return res;
}
第十二题:翻转单词顺序列
牛客最近来了一个新员工 Fish ,每天早晨总是会拿着一本英文杂志,写些句子在本子上。同事Cat对Fish写的内容颇感兴趣,有一天他向Fish借来翻看,但却读不懂它的意思。例如,“student. a am I”。后来才意识到,这家伙原来把句子单词的顺序翻转了,正确的句子应该是“I am a student.”。Cat对一一的翻转这些单词顺序可不在行,你能帮助他么?
解题思路
public String ReverseSentence(String str) {
if(str == null || str.trim().equals("")) return str;
String[] split = str.split(" ");
StringBuilder builder = new StringBuilder();
for (int i = split.length - 1; i >= 0; i--) {
builder.append(split[i]);
if (i != 0) builder.append(" ");
}
return builder.toString();
}
第十三题:左旋转字符串
汇编语言中有一种移位指令叫做循环左移(ROL),现在有个简单的任务,就是用字符串模拟这个指令的运算结果。对于一个给定的字符序列S,请你把其循环左移K位后的序列输出。例如,字符序列S=”abcXYZdef”,要求输出循环左移3位后的结果,即“XYZdefabc”。是不是很简单?OK,搞定它!
解题思路
- 对于 abcXYZdef 左移 3位,可以将字符串分为两个部分:abc & XYZdef
- 分别将两个部分进行反转得到:cba & fedZYX
- 将两部分和在一起再进行反转:XYZdefabc
public String LeftRotateString(String str, int n) {
if (str == null || str.trim().equals("")) return str;
String res = revert(str, 0, n - 1);
res = revert(res, n, str.length() - 1);
res = revert(res, 0, str.length() - 1);
return res;
}
private String revert(String str, int start, int end) {
char[] chars = str.toCharArray();
while (start < end) {
char t = chars[start];
chars[start] = chars[end];
chars[end] = t;
start++;
end--;
}
return new String(chars);
}
第十四题:n个骰子的点数
把 n 个骰子扔在地上,所有骰子朝上一面的和为 s,输入 n,打印 s 所有可能值的概率
解题思路
- 首先考虑一个骰子的情况,那么有 1~6 出现的次数均为 1
- 再增加一个骰子时,由于各个点数出现的概率一致。用
$$f(n,s)=f(n-1,s-1)+f(n-1,s-2)+f(n-1,s-3)+f(n-1,s-4)+f(n-1,s-5)+f(n-1,s-6)$$
- 使用两个数组循环求解
public void SumOfNDice(int n) {
if (n < 1) {
return;
}
int[][] nums = new int[2][n * 6 + 1];
int flag = 0;
//初始化第一个骰子各总和出现的次数
int maxLen = nums[0].length;
for (int i = 1; i < maxLen; i++) {
nums[flag][i] = 1;
}
for (int i = 2; i <= n; i++) {
int newFlag = flag ^ 0x01;
Arrays.fill(nums[newFlag], 0);
for (int j = i; j < maxLen; j++) {
int sum = 0;
for (int k = 1; k <= 6 && (j - k >= 0); k++) {
sum += nums[flag][j - k];
}
nums[newFlag][j] = sum;
}
flag = newFlag;
}
//debug out
System.out.println(Arrays.toString(nums[flag]));
int sum = 0;
for (int i : nums[flag]) {
sum += i;
}
for (int i = 0; i < nums[flag].length; i++) {
System.out.println(i + ":" + nums[flag][i] * 1.0 / sum);
}
}
第十五题:扑克牌顺子
LL今天心情特别好,因为他去买了一副扑克牌,发现里面居然有 2 个大王, 2 个小王(一副牌原本是 54 张)…他随机从中抽出了 5 张牌,想测测自己的手气,看看能不能抽到顺子,如果抽到的话,他决定去买体育彩票,嘿嘿!!“红心A,黑桃3,小王,大王,方片5”,“Oh My God!”不是顺子…..LL不高兴了,他想了想,决定大\小 王可以看成任何数字,并且A看作1,J为11,Q为12,K为13。上面的5张牌就可以变成“1,2,3,4,5”(大小王分别看作2和4),“So Lucky!”。LL决定去买体育彩票啦。 现在,要求你使用这幅牌模拟上面的过程,然后告诉我们 LL 的运气如何, 如果牌能组成顺子就输出 true,否则就输出 false。为了方便起见,你可以认为大小王是0。
解题思路
- 对数组进行排序
- 计算非0元素之间的间隔总和
- 如果有相同元素则直接认为失败
- 如果间隔大于0,那么间隔的总个数等于0的总个数,即为成功
public Boolean isContinuous(int[] numbers) {
if (numbers == null || numbers.length < 5) return false;
Arrays.sort(numbers);
int count = 0;
int zeroCount = 0;
int pre = -1;
for (int number : numbers) {
if (number == 0) {
zeroCount++;
continue;
}
if (pre == -1) pre = number; else {
int t = number - pre - 1;
if (t > 0) {
count += t;
} else if (t < 0) return false;
pre = number;
}
}
if (count == 0) return true; else return count == zeroCount;
}
写在最后
文章限于篇幅,还有很多面试题,我就不在这里一一讲解,面试题我已经整理成一份完整的PDF文件,需要的朋友,请加VX->"Angel_CoCc
",免费领取
部分面试题截图,总有你想要的的或者你需要的!