题目16 -- 数值的整数次方
题目:实现函数 double Power(double base, int exponent)。求 base 的 exponent 次方。不得使用库函数,同时不需要考虑大数问题。
这一章看起来都是关注代码质量的题,即题目本身不难解决,但是边界条件、异常情况等处处埋雷。
例如这道题,
-
首先应该考虑的就是大数问题,如果题目中没有提到可以忽略大数问题,那么我们还需要实现一个 大数类型的乘法。
-
其次需要考虑的就是0^2,0^(-2),5^(-2),这些情况,即base=0时、exponent是负数时的情况。综合考虑这些后得到代码。
-
最后,在求解乘方运算时,可以想到用 二分法 降低时间复杂度。
public class Solution {
public double power(double base, int exponent) {
boolean negative = exponent < 0;
if (exponent == 0) {
return 1; // note: include 0^0
}
if (base == 0) {
return 0; // note: include 0^(-2)
}
int unsignedExponent = Math.abs(exponent);
double rawResult = powerWithUnsignedExponent(base, unsignedExponent);
return negative ? (1 / rawResult) : rawResult;
}
private double powerWithUnsignedExponent(double base, int unsignedExponent) {
if (unsignedExponent == 1) {
return base;
}
int halfExponent = unsignedExponent >> 1;
double halfResult = powerWithUnsignedExponent(base, halfExponent);
if ((unsignedExponent & 1) == 1) {
return base * halfResult * halfResult;
} else {
return halfResult * halfResult;
}
}
}
题目17 -- 打印从1到最大的n位数
题目:输入数字n,按顺序打印从1到最大的n位十进制数。比如输入3,则打印出1、2、3一直到最大的三位数999。
和上面的题类似,此题的坑在于边界条件的考虑,即考虑大数问题,如果数字n足够大,那么不论是int、long还是long long,都无法抗住这么大的数字。所以需要实现 用数组模拟数字的加法 ,或者 用数组的全排列
用数组模拟数字的加法
进位不溢出
进位溢出
public class Solution {
public void printToMaxOfDigits(int n) {
if (n <= 0) {
return;
}
int[] number = new int[n];
while (increment(number)) {
printNumber(number);
}
}
private void printNumber(int[] number) {
boolean isBeginning0 = true;
for (int i = 0; i < number.length; i++) {
if (isBeginning0 && number[i] != 0) {
isBeginning0 = false;
}
if (!isBeginning0) {
System.out.print(number[i]);
}
}
System.out.println();
}
private boolean increment(int[] number) {
for (int i = number.length - 1; i >= 0; i--) {
if (number[i] + 1 == 10) {
number[i] = 0;
} else {
number[i]++;
return true;
}
}
return false;
}
}
数组的全排列(递归)
public class Solution2 {
public void printToMaxOfDigits(int n) {
if (n <= 0) {
return;
}
int[] number = new int[n];
printToMaxOfDigitsRecursively(number, 0);
}
private void printToMaxOfDigitsRecursively(int[] number, int index) {
if (index == number.length) {
printNumber(number);
return;
}
for (int i = 0; i < 10; i++) {
number[index] = i;
printToMaxOfDigitsRecursively(number, index+1);
}
}
private void printNumber(int[] number) {
boolean isBeginningZero = true;
for (int i = 0; i < number.length; i++) {
if (isBeginningZero && number[i] != 0) {
isBeginningZero = false;
}
if (!isBeginningZero) {
System.out.print(number[i]);
}
}
if (!isBeginningZero) {
System.out.println();
}
}
}
面试题18 -- 删除链表的节点
题目一:在O(1)的时间内删除链表节点
题目一:在O(1)的时间内删除链表节点。
给定单向链表的头指针和一个节点指针,定义一个函数在O(1)的时间内删除该节点。
public class Solution {
private static class ListNode {
int value;
ListNode next;
ListNode(int value) {
this.value = value;
}
}
// require caller to ensure nodeToBeDeleted is in listHead.
public void deleteNode(ListNode listHead, ListNode nodeToBeDeleted) {
if (listHead == null || nodeToBeDeleted == null || listHead.next == null) {
return;
}
if (nodeToBeDeleted.next == null) {
ListNode node = listHead;
// if nodeToBeDeleted is tail, we have to iterate whole list
while(node.next != null) {
if (node.next == nodeToBeDeleted) {
node.next = null;
return;
}
node = node.next;
}
throw new IllegalStateException("nodeToBeDeleted is not in list!");
} else {
ListNode nextNode = nodeToBeDeleted.next;
nodeToBeDeleted.value = nextNode.value;
nodeToBeDeleted.next = nextNode.next;
nextNode.next = null; // not important
}
}
private static void printNode(ListNode head) {
ListNode node = head.next;
while (node != null) {
System.out.print(node.value + " --> ");
node = node.next;
}
System.out.println("null");
}
public static void main(String[] args) {
ListNode head = new ListNode(0);
head.next = new ListNode(1);
head.next.next = new ListNode(2);
head.next.next.next = new ListNode(3);
printNode(head);
Solution solution = new Solution();
solution.deleteNode(head, head.next.next);
printNode(head);
solution.deleteNode(head, head.next.next);
printNode(head);
solution.deleteNode(head, head.next);
printNode(head);
}
}
题目二:删除链表中重复的节点
题目二:删除链表中的重复节点
在一个排序的链表中,如何删除重复的节点?例如,在图中重复的节点被删除后,链表如下。
- 动手前想好测试用例,写在注释上
- 标注好特殊入参todo
- 构思实现方式并写出代码原型
- 补充细节
- 回写todo,脑内编译执行测试用例,脑内debug
本题重点在于控制好三个指针
public void deleteDuplicateNode(ListNode head) {
if (head == null || head.next == null) {
return;
}
ListNode preNode = head;
ListNode node = head.next;
while(node != null) {
boolean findDuplicate = false;
ListNode nextNode = node.next;
while(nextNode != null && nextNode.value == node.value) {
findDuplicate = true;
nextNode = nextNode.next;
}
// conjunct node and nextNode if find duplicated
if (findDuplicate) {
node.next = nextNode;
}
node = node.next;
preNode = preNode.next;
}
}
面试题19 -- 正则表达式匹配
题目:请实现一个函数用来匹配包含'.'和'*'的正则表达式。模式中的字符'.'表示任意一个字符,而'*'表示它前面的字符可以出现任意次(含0次)。在本题中,匹配是指字符串的所有字符匹配整个模式。例如,字符串"aaa"与模式"a.a"和"ab*ac*a"匹配,但与"aa.a"和"ab*a"均不匹配。
此题是一个状态机(State Machine)问题,以字符串"aaa"匹配正则表达式"ab*ac*a"的过程为例。
正则表达式ab*ac*a
的状态机可以表示为如下:
在正则匹配的过程中可能会遇到这几种情况:
#
匹配到#*
- 如果字符串input的
#
与pattern的#
相等- 当做匹配了一次
#*
,字符串input向前+1,模式串pattern向前+2(跳过#*
) ==> 状态2转向状态3 - 匹配了一次
#*
后,还要继续匹配多次,字符串input向前+1,pattern不动 ==> 状态2原地踏步 #*
匹配失败,即匹配0次,字符串input不动,模式串pattern向前+2 ==> 状态2转向状态3
- 当做匹配了一次
- 如果字符串input的
#
与pattern的#
不相等- 只能当做pattern的
#*
匹配了0次,字符串input不动,模式串pattern向前+2 ==> 状态2转向状态3
- 只能当做pattern的
- 如果字符串input的
#
匹配#
比较是否相等
public class Solution {
public boolean matchPattern(String input, String pattern) {
if (input == null || pattern == null) {
return false;
}
return matchPatternCore(input, 0, pattern, 0);
}
private boolean matchPatternCore(String input, int inputIndex, String pattern, int patternIndex) {
if (inputIndex == input.length() && patternIndex == pattern.length()) {
return true;
}
if (inputIndex < input.length() && patternIndex == pattern.length()) {
return false;
}
if (pattern.charAt(patternIndex + 1) == '*') {
if (inputIndex < input.length() && isEqual(input.charAt(inputIndex), pattern.charAt(patternIndex))) {
// 1. move on the next state
// 2. stay on the current state
// 3. ignore 'a*'
return matchPatternCore(input, inputIndex + 1, pattern, patternIndex + 2)
|| matchPatternCore(input, inputIndex + 1, pattern, patternIndex)
|| matchPatternCore(input, inputIndex, pattern, patternIndex + 2);
} else {
// not equal(include input is over), we can only try ignore
return matchPatternCore(input, inputIndex, pattern, patternIndex + 2);
}
} else if (isEqual(input.charAt(inputIndex), pattern.charAt(patternIndex))) {
return matchPatternCore(input, inputIndex + 1, pattern, patternIndex + 1);
} else {
return false;
}
}
private boolean isEqual(char ch, char patternChar) {
return ch == patternChar || patternChar == '.';
}
}
此外还有一种动态规划的解法,是将递归的过程反过来,构建一个二维数组dp,dp[i][j]表示input直到i为止与pattern直到j为止是否匹配,通过各种情况的判断(包括遇到*后的处理逻辑),逐渐补完dp数组,计算出结果。
leetcode题解 - 逐行详细讲解,由浅入深,dp和递归两种思路
面试题20 -- 表示数值的字符串
题目:请实现一个函数用来判断字符串是否表示数值(包括整数和小数)。例如,字符串"+100"、"5e2"、"-123"、"3.1416"及"-1E-16"都表示数值,但"12e"、”1a3.14“、"1.2.3"、"+-5"及"12e+5.4"都不是。
书中的解法思路如下,以'.'和'e'为分隔符,将字符串分割为如下A、B、C三部分,分别判断各个部分是否为合法数字。
具体的实现细节不一定要真的执行split方法切割字符串,而是通过遍历字符串,在遇到非法字符之前都是A的部分,在遇到'.'或者'E'后,之后的合法字符都作为B或C的部分,最终通过index是否好好遍历完整个字符串作为判断条件。
public class Solution {
private int index = 0;
public boolean isNumber(String s) {
if (s == null || s.length() == 0) {
return false;
}
s = s.trim();
boolean numeric = scanInteger(s);
if (index < s.length() && s.charAt(index) == '.') {
index++;
numeric = scanUnsignedInteger(s) || numeric;
}
if (index < s.length() && isExponent(s.charAt(index))) {
index++;
numeric = numeric && scanInteger(s);
}
return numeric && index == s.length();
}
private boolean scanInteger(String s) {
if (index < s.length() && isPlusMinus(s.charAt(index))) {
index++;
}
return scanUnsignedInteger(s);
}
private boolean scanUnsignedInteger(String s) {
int before = index;
while(index < s.length() && isDigit(s.charAt(index))) {
index++;
}
return index > before;
}
private boolean isDigit(char c) {
return c >= '0' && c <= '9';
}
private boolean isPlusMinus(char c) {
return c == '+' || c == '-';
}
private boolean isExponent(char c) {
return c == 'e' || c == 'E';
}
}
leetcode题解中提到了一种基于 状态机 的解决办法,个人感觉更利于理解,即无脑列举出所有可能的状态转移就完事了。
面试题21 -- 调整数组顺序使奇数位于偶数前面
题目:输入一个整数数组,实现一个函数来调整该数组中数字的顺序,使得所有奇数位于数组的前半部分,所有偶数位于数组的后半部分。
通过经验很容易联想到快排的partition方法,把奇数与偶数进行分堆,通过先驱节点i遍历nums数组,把奇数的扔左边,把偶数的扔右边。(与书中的左右两个指针向中间靠拢的本质原理是类似的,书中提到将 (nums[i] & 1) == 1
抽象出来 函数指针 便于扩展,这点在Java中可以使用lambda表达式实现)
public class Solution {
// normal: 1 2 3 4 5
// all odd: 1 3 5
// all even: 2 4 6
public int[] exchange(int[] nums) {
if (nums == null || nums.length == 0) {
return nums;
}
int firstEven = 0;
for (int i = 0; i < nums.length; i++) {
if ((nums[i] & 1) == 1) { // odd
swap(nums, i, firstEven);
firstEven++;
}
}
return nums;
}
private void swap(int[] nums, int i, int j) {
int temp = nums[i];
nums[i] = nums[j];
nums[j] = temp;
}
}
面试题22 -- 链表中倒数第k个节点
题目:输入一个链表,输出该链表中倒数第k个节点。为了符合大多数人的习惯,本题从1开始计数,即链表的尾节点是倒数第1个节点。例如,一个链表有6个节点,从头节点开始,它们的值依次是1、2、3、4、5、6。这个链表的倒数第三个节点是值为4的节点。
很简单的一道题,正如书中所述,重点在于考虑鲁棒性
/**
* Definition for singly-linked list.
* public class ListNode {
* int val;
* ListNode next;
* ListNode(int x) { val = x; }
* }
*/
class Solution {
// 1 -> 2 -> 3 -> 4 -> 5 -> 6, k=3
// 1, k=3
public ListNode getKthFromEnd(ListNode head, int k) {
if (head == null || k <= 0) {
return null;
}
ListNode node = head;
for (int i = 0; i < k; i++) {
if (node == null) {
return null; // size of list is less than k.
}
node = node.next;
}
ListNode kthNode = head;
while(node != null) {
node = node.next;
kthNode = kthNode.next;
}
return kthNode;
}
}
面试题23 -- 链表中环的入口节点
题目:如果一个链表中包含环,如何找出环的入口节点?例如,在如图所示的链表中,环的入口节点是节点3 。
public class Solution {
private static class ListNode {
int val;
ListNode next;
ListNode(int x) { val = x; }
}
public ListNode entryNodeOfLoop(ListNode head) {
if (head == null) {
return null;
}
ListNode meetingNode = findMeetingNode(head);
if (meetingNode == null) {
return null; // means there is no loop in list
}
int nodeCountOfLoop = getNodeCountOfLoop(meetingNode);
ListNode foreNode = head;
for (int i = 0; i < nodeCountOfLoop; i++) {
foreNode = foreNode.next;
}
ListNode behindNode = head;
while(foreNode != behindNode) {
foreNode = foreNode.next;
behindNode = behindNode.next;
}
return behindNode;
}
private ListNode findMeetingNode(ListNode head) {
// assert head is not null
ListNode slow = head;
ListNode fast = head;
while(slow != null && fast.next != null) {
slow = slow.next;
fast = fast.next.next;
if (slow == fast) {
return slow;
}
}
return null;
}
private int getNodeCountOfLoop(ListNode meetingNode) {
// assert meetingNode is a loop which means there's no null in list
ListNode node = meetingNode.next;
int i = 1;
while(node != meetingNode) {
i++;
node = node.next;
}
return i;
}
}
面试题24 -- 反转链表
题目:定义一个函数,输入一个链表的头节点,反转该链表并输出反转后链表的头节点。
本题的重点在控制好before、node、after这三个指针,node用于遍历,before用于在反转的时候设置node.next=before,after用于在node调整之后还能找到下一个节点,使遍历继续下去。
public ListNode reverseList(ListNode head) {
if (head == null || head.next == null) {
return head;
}
ListNode before = null;
ListNode node = head;
ListNode after = head.next;
while(node != null) {
node.next = before;
if (after == null) {
return node;
}
before = node;
after = after.next;
node = after;
}
throw new IllegalStateException("not supposed to be here.");
}
面试题25 -- 合并两个排序的链表
题目:输入两个递增排序的链表,合并这两个链表并使新链表中的节点仍然是递增排序的。
同样是一道题目并不难,但是需要控制好指针的调整,防止出现指针指向不符合预期的bug。用循环和递归都可以解决。
public ListNode mergeTwoLists(ListNode l1, ListNode l2) {
if (l1 == null || l2 == null) {
return l1 == null ? l2 : l1;
}
ListNode baseList = l1.val < l2.val ? l1 : l2;
ListNode branchList = baseList == l1 ? l2 : l1;
ListNode baseNode = baseList.next;
ListNode foreNode = baseList;
ListNode branchNode = branchList;
while(baseNode != null && branchNode != null) {
if (baseNode.val <= branchNode.val) {
foreNode = foreNode.next; // baseNode & foreNode walk forward
baseNode = baseNode.next;
} else {
foreNode.next = branchNode; // merge branchNode into baseList and walk forward
branchNode = branchNode.next;
foreNode.next.next = baseNode;
foreNode = foreNode.next;
}
}
if (branchNode != null) {
foreNode.next = branchNode;
}
return baseList;
}
public ListNode mergeTwoListsInBook(ListNode l1, ListNode l2) {
if (l1 == null || l2 == null) {
return l1 == null ? l2 : l1;
}
ListNode mergedHead;
if (l1.val < l2.val) {
mergedHead = l1;
mergedHead.next = mergeTwoListsInBook(l1.next, l2);
} else {
mergedHead = l2;
mergedHead.next = mergeTwoListsInBook(l1, l2.next);
}
return mergedHead;
}
面试题26 -- 树的子结构
题目:输入两颗二叉树A和B,判断B是不是A的子结构。
此题理解的重点在于理解题意,B是A的子结构=B的结构能够在A的任意一个位置对上
public boolean isSubStructure(TreeNode A, TreeNode B) {
if (B == null) {
return false;
} else if (A == null) {
return false;
}
// pre-order traverse to find if B is-root-substructure of A
return isRootSubStructure(A, B) || isSubStructure(A.left, B) || isSubStructure(A.right, B);
}
private boolean isRootSubStructure(TreeNode A, TreeNode B) {
if (B == null) {
return true;
} else if (A == null) {
return false;
}
return A.val == B.val && isRootSubStructure(A.left, B.left) && isRootSubStructure(A.right, B.right);
}