欢迎指正
题解(01-10):link
题解(11-20):link
题解(21-30):link
题解(31-40):link
题解(41-50):link
题解(51-60): link
题解(61-67): link
51.构建乘积数组
题目描述: 给定一个数组
A[0,1,...,n-1]
,请构建一个数组B[0,1,...,n-1
], 其中 B 中的元素B[i]=A[0]*A[1]*...*A[i-1]*A[i+1]*...*A[n-1]
。不能使用除法。(注意:规定B[0] = A[1] * A[2] * ... * A[n-1]
,B[n-1] = A[0] * A[1] * ... * A[n-2];
)
1.解法一:暴力求解,时间复杂度O(n^2)
- 使用两次循环,外层循环代表要给
B
的第i
个位置赋值 - 内层循环记录
A
的乘积,乘积不包括A[i]
,题目要求
public class Solution {
public int[] multiply(int[] A) {
if (A == null) return A;
int len = A.length;
int[] B = new int[len];
if (len <= 1) return B;
for (int i = 0;i < len;i ++) {
int res = 1;
for (int j = 0;j < len;j ++) {
if (i != j)
res *= A[j];
}
B[i] = res;
}
return B;
}
}
2.解法二:优化一中有大量重复的子问题
根据A
与B
的关系B[i] = A[0] *...*A[i - 1] * A[i + 1] * ... * A[n - 1]
,我们可以把这个关系看成两个部分的乘积,其中C[i] = A[0] *...*A[i - 1]
,D[i] = A[i + 1] * ... * A[n - 1]
,则最终B[i] = C[i] * D[i]
,这个i
代表的是行数
那么我们可以得到如下一个矩阵来形象化,B[i]
为矩阵中第i
行的所有元素之积
根据上面的图,我们可以得出B[i] = C[i] * D[i]
,其中C[0] = 1,C[i] = C[i - 1] * A[i - 1]
,D[len - 1] = 1,D[i] = D[i + 1] * A[i + 1]
。
使用空间来换时间,将时间复杂度提升为O(n)
public class Solution {
public int[] multiply(int[] A) {
if (A == null || A.length < 1) return A;
int len = A.length;
int[] B = new int[len];
int[] C = new int[len]; // C 记录左半部分
int[] D = new int[len]; // D 记录右半部分
// 先计算 C
C[0] = 1;
for (int i = 1;i < len;i ++)
C[i] = C[i - 1] * A[i - 1];
// 再计算 D
D[len - 1] = 1;
// 从倒数第二行开始往上
for (int i = len - 2;i >= 0;i --)
D[i] = D[i + 1] * A[i + 1];
// 最后计算 B
for (int i = 0;i < len;i ++)
B[i] = C[i] * D[i];
return B;
}
}
3.解法三:对解法二一点点小改动,无需额外空间了,解法二比较直观易懂
public class Solution {
public int[] multiply(int[] A) {
if (A == null || A.length < 1) return A;
int len = A.length;
int[] B = new int[len]; // 让B先记录左边的值
B[0] = 1;
for (int i = 1;i < len;i ++)
B[i] = B[i - 1] * A[i - 1];
int temp = 1; // 用 temp 来记录右边的值
for (int i = len - 2;i >= 0;i --) { // 右边的值可以自下向上
temp *= A[i + 1]; // 边记录变更新
B[i] *= temp; // 并且一并更新 B
}
return B;
}
}
52.正则表达式匹配
描述见链接
1.解法一:递归
- 当用一个字符去和模式串中的字符进行匹配时,如果模式中的字符是
.
,那么任何字符都可以匹配;或者,如果两个字符相同,那么可以匹配,接着再去匹配下一个字符; - 当模式串的第二个字符不是
*
时, 问题就比较简单:若字符串的第一个字符和模式串的第一个匹配时,字符串和模式串指针都向后移动一个字符,然后匹配剩余的字符串和模式串。如果第一个字符不匹配,那么就直接返回false
; - 当模式串的第二个字符是
*
时, 可能就出现多种不同的匹配方式:- 无论第一个字符是否相等,模式串向后移动两个字符,相当于
*
和他前面的字符被忽略,因为*
可以代表前面一个字符出现0次; - 如果模式串中第一个字符和字符串中第一个字符匹配,则字符串向后移动一个字符,比较下一位,而此时模式串有两种情况:
- 模式串向后移动两个字符;
- 保持模式串不变(因为
*
代表前面的字符出现多次)
- 无论第一个字符是否相等,模式串向后移动两个字符,相当于
public class Solution {
public boolean match(char[] str, char[] pattern) {
if (str == null || pattern == null) return false;
return match(str, 0, pattern, 0);
}
private boolean match(char[] str, int i, char[] pattern, int j) {
if (i == str.length && j == pattern.length) return true; // 都为空
if (i < str.length && j == pattern.length) return false; // 模式串为空
// 以下情况中,j < pattern.length
if (j + 1 < pattern.length && pattern[j + 1] == '*') { // 第二个字符是 *
if ((i < str.length && str[i] == pattern[j])
|| (i < str.length && pattern[j] == '.')) {
// 第一个字符相等有三种情况,分别匹配0,1,或多个
return match(str, i, pattern, j + 2)
|| match(str, i + 1, pattern, j + 2)
|| match(str, i + 1, pattern, j);
} else { // 第一个字符不相等
return match(str, i, pattern, j + 2);
}
} else { // 第二个字符不是 *
if ((i < str.length && str[i] == pattern[j])
|| (i < str.length && pattern[j] == '.')) {
return match(str, i + 1, pattern, j + 1);
} else {
return false;
}
}
}
}
53.表示数值的字符串
描述见链接
首先要清楚数值的字符串模式包括什么?
- 第一种情况:
A [ . [ B ] ] [ e | E C]
- 第二种请况:
. B [ e | E C]
其中A
是数值的整数部分(可以没有,此时小数部分一定要有),B
为跟在小数点后的小数部分(可以没有),C
为跟在e
或E
之后的指数部分。并且A
和C
都可能以+ -
开头,B
前面不能有符号
1.解法一:正则表达式
public class Solution {
public boolean isNumeric(char[] str) {
String s = String.valueOf(str);
//+代表出现一次或多次,*代表出现0次或多次,?代表出现0次或者一次
String regexp = "[\\+-]?[0-9]*(\\.[0-9]+)?([eE][\\+-]?[0-9]+)?";
return s.matches(regexp);
}
}
2.解法二:遍历字符,挨个匹配(未做)
54.字符流中第一个不重复的字符
描述见链接
1.解法一:使用一个长度为256的字符数组来存储可能出现的字符
import java.lang.StringBuilder;
public class Solution {
private StringBuilder sb = new StringBuilder();
private int[] arrCount = new int[256];
//Insert one char from stringstream
public void Insert(char ch) {
sb.append(ch);
arrCount[ch] ++;
}
//return the first appearence once char in current stringstream
public char FirstAppearingOnce() {
for (int i = 0;i < sb.length();i ++) {
char c = sb.charAt(i);
// 去这个arrCount数组中查看这个字符出现的次数
if (arrCount[c] == 1)
return c;
}
return '#';
}
}
55.链表中环的入口节点
题目描述: 给一个链表,若其中包含环,请找出该链表的环的入口结点,否则,输出 null。
1.解法一:使用哈希集合来存储节点
import java.util.HashSet;
public class Solution {
public ListNode EntryNodeOfLoop(ListNode pHead) {
// 使用HashSet存储节点,出现过的那个节点就是环的入口
if (pHead == null || pHead.next == null) return null;
HashSet<ListNode> set = new HashSet<>();
while (pHead != null) {
if (set.contains(pHead))
return pHead;
set.add(pHead);
pHead = pHead.next;
}
return null;
}
}
2.解法二:Floyd 算法
public class Solution {
// 使用快慢指针,先判断有没有环,再判断环的入口
public ListNode EntryNodeOfLoop(ListNode pHead) {
if (pHead == null || pHead.next == null) return null;
ListNode slow = pHead;
ListNode fast = pHead;
ListNode meet = null;
// 先判断有没有环
while (fast != null && fast.next != null) {
slow = slow.next;
fast = fast.next.next;
if (slow == fast) {
meet = fast;
break;
}
}
// 如果为空,说明没有环
if (meet == null) return null;
// 重新从起点出发
slow = pHead;
while (meet != slow) {
meet = meet.next;
slow = slow.next;
}
return meet;
}
}
56.删除链表中的重复的节点
题目描述: 在一个排序的链表中,存在重复的结点,请删除该链表中重复的结点,重复的结点不保留,返回链表头指针。 例如,链表
1->2->3->3->4->4->5
处理后为1->2->5
1.解法一
public class Solution {
public ListNode deleteDuplication(ListNode pHead) {
if (pHead == null || pHead.next == null) return pHead;
ListNode dummy = new ListNode(-1);
dummy.next = pHead;
// prev 已经确保不会重复的最后一个
ListNode prev = dummy, curr = pHead;
boolean flag = false; // 标记是否重复
while (prev != null && curr != null) {
flag = false;
// 有重复的就前进,并把标记置为true
while (curr.next != null && curr.next.val == curr.val) {
curr = curr.next;
flag = true;
}
// 循环出来后还要再往后跳一个到未重复的元素去
if (flag && curr != null) {
curr = curr.next;
}
// 搞掉一个重复的还不能连接,因为下一个是否重复还要再接着判断
if (curr == null || curr.next == null || curr.next.val != curr.val) {
prev.next = curr;
prev = curr;
if (curr != null) // 防止空指针
curr = curr.next;
}
}
return dummy.next;
}
}
2.解法二
public class Solution {
public ListNode deleteDuplication(ListNode pHead) {
if (pHead == null || pHead.next == null) return pHead;
ListNode dummy = new ListNode(-1);
dummy.next = pHead;
ListNode prev = dummy, curr = pHead;
while (curr != null) {
if (curr.next != null && curr.val == curr.next.val) {
while (curr.next != null && curr.val == curr.next.val)
curr = curr.next; // 将curr移动到最后一个相等的元素
// 更新 prev 到下一个不重复的节点
prev.next = curr.next;
curr = curr.next;
} else {
prev = prev.next;
curr = curr.next;
}
}
return dummy.next;
}
}
57.二叉树的下一个节点
题目描述: 给定一个二叉树和其中的一个结点,请找出中序遍历顺序的下一个结点并且返回。注意,树中的结点不仅包含左右子结点,同时包含指向父结点的指针。
1.解法一:先找到根节点,进行中序遍历,再去找目标节点的下一个节点
- 时间复杂度
O(n)
- 空间复杂度
O(n)
public class Solution {
private ArrayList<TreeLinkNode> list = new ArrayList<>();
public TreeLinkNode GetNext(TreeLinkNode pNode) {
// 先找到父节点,中序遍历父节点,找到目标节点的下一个节点
if (pNode == null) return pNode;
TreeLinkNode par = pNode;
// 一直向上找到根节点
while (par.next != null)
par = par.next;
inOrder(par); // 中序遍历根节点
for (int i = 0;i < list.size();i ++) {
if (pNode == list.get(i))
// 如果是最后一个节点,返回空节点
return i == list.size() - 1 ? null : list.get(i + 1);
}
return null;
}
// 中序遍历这棵树
private void inOrder(TreeLinkNode node) {
if (node != null) {
inOrder(node.left);
list.add(node);
inOrder(node.right);
}
}
}
2.解法二:直接去找目标节点的下一个节点
直接去找目标节点中序遍历的下一个节点可以分为三种情况:
- 该节点有右子树,那么他的下一个节点就是右子树中的最左边的节点;如节点
B
- 该节点没有右子树,但是他是他的父节点的左节点,那么这个父节点就是他的下一个中序遍历节点。如节点
H
- 该节点没有右子树,并且还是他的父节点的右节点,那么我们就一直沿着父节点追溯,直到找到某个节点
N
是其父节点的左子树,如果存在这样的节点N
,那么这个节点N
的父节点就是我们要找的下一个节点。如节点I
,下个节点是A
,(I -> E -> B -> A)
public class Solution {
public TreeLinkNode GetNext(TreeLinkNode pNode) {
if (pNode == null) return pNode;
// 有右子树
if (pNode.right != null) {
TreeLinkNode pRight = pNode.right;
while (pRight.left != null)
pRight = pRight.left;
return pRight;
}
// 无右子树,且节点是该节点父节点的左子树
// 首先保证父节点要存在
if (pNode.next != null && pNode == pNode.next.left) {
return pNode.next;
}
// 无右子树且节点是该节点父节点的右子树
if (pNode.next != null) {
TreeLinkNode pNext = pNode.next;
while (pNext.next != null && pNext.next.right == pNext)
pNext = pNext.next;
return pNext.next;
}
return null;
}
}
58.对称的二叉树
题目描述: 请实现一个函数,用来判断一颗二叉树是不是对称的。注意,如果一个二叉树同此二叉树的镜像是同样的,定义其为对称的。
1.解法一:递归
对称二叉树: 根节点的左右子树相同,左子树的左子树和右子树的右子树相同,左子树的右子树和右子树的左子树相同
public class Solution {
boolean isSymmetrical(TreeNode pRoot) {
if (pRoot == null) return true;
return isSymmetrical(pRoot.left, pRoot.right);
}
private boolean isSymmetrical(TreeNode left, TreeNode right) {
// 左右孩子为空满足
if (left == null && right == null) return true;
// 左右孩子有一个不为空,另一个为空,不满足
if (left == null || right == null) return false;
// 左右孩子值不同,不满足
if (left.val != right.val) return false;
// 递归判断 左子树的左子树和右子树的右子树
// 递归判断 左子树的右子树和右子树的左子树
return isSymmetrical(left.left, right.right)
&& isSymmetrical(left.right, right.left);
}
}
59.按之字形顺序打印二叉树
题目描述: 请实现一个函数按照之字形打印二叉树,即第一行按照从左到右的顺序打印,第二层按照从右至左的顺序打印,第三行按照从左到右的顺序打印,其他行以此类推。
1.解法一:层序遍历时加一个正序还是反序打印的标志位
- 在偶数层进行
reverse
操作可能在海量数据下效率很低
public class Solution {
public ArrayList<ArrayList<Integer> > Print(TreeNode pRoot) {
ArrayList<ArrayList<Integer>> res = new ArrayList<>();
if (pRoot == null) return res;
// 使用一个LinkedList 充当队列
LinkedList<TreeNode> q = new LinkedList<>();
q.add(pRoot);
boolean flag = true; // 代表正序还是反序打印
while (!q.isEmpty()) {
int size = q.size();
ArrayList<Integer> level = new ArrayList<>();
for (int i = 0;i < size;i ++) {
// removeFirst() 取得队首元素
TreeNode top = q.removeFirst();
level.add(top.val);
if (top.left != null) q.add(top.left);
if (top.right != null) q.add(top.right);
}
if (flag == true) {
res.add(level);
} else {
// 如果是反向打印,先把层逆序一下
Collections.reverse(level);
res.add(level);
}
// 更改标志位
flag = !flag;
}
return res;
}
}
2.解法二:优化一,不使用reverse
操作。使用两个栈来操作
值得注意的地方:
14
行和29
行,先判断弹出来的节点node
是否为空,因为在压栈的过程中会把空节点null
给一起压入栈中17
和32
行,压栈的顺序可以体现出最后出栈是从左往右出栈,还是从右往左出栈23
行38
行,先判断层级链表temp
是否为空,可以避免结果集中出现空链表[]
public class Solution {
public ArrayList<ArrayList<Integer> > Print(TreeNode pRoot) {
if (pRoot == null) return new ArrayList<>();
ArrayList<ArrayList<Integer>> res = new ArrayList<>();
Stack<TreeNode> stack1 = new Stack<>(); // 存奇数层
Stack<TreeNode> stack2 = new Stack<>(); // 存偶数层
boolean flag = true;
stack1.push(pRoot); // 根节点在奇数层
while (!stack1.empty() || !stack2.empty()) {
if (flag) {
ArrayList<Integer> temp = new ArrayList<>();
while (!stack1.empty()) {
TreeNode node = stack1.pop();
// 因为可能压入栈的是null,所以先判断一下,避免空指针异常
if (node != null) {
temp.add(node.val);
// 从左往右推进去,从右往左弹出来
stack2.push(node.left);
stack2.push(node.right);
}
}
// 不是空链表才添加到结果集中
if (!temp.isEmpty())
res.add(temp);
} else {
ArrayList<Integer> temp = new ArrayList<>();
while (!stack2.empty()) {
TreeNode node = stack2.pop();
// 因为可能压入栈的是null,所以先判断一下,避免空指针异常
if (node != null) {
temp.add(node.val);
// 从右往左推进去,从左往右弹出来
stack1.push(node.right);
stack1.push(node.left);
}
}
// 不是空链表才添加到结果集中
if (!temp.isEmpty())
res.add(temp);
}
flag = !flag; // 标记下一次是正序还是反序
}
return res;
}
}
60.把二叉树打印成多行
题目描述: 从上到下按层打印二叉树,同一层结点从左至右输出。每一层输出一行。
1.解法一:层序遍历即可
public class Solution {
ArrayList<ArrayList<Integer> > Print(TreeNode pRoot) {
ArrayList<ArrayList<Integer>> res = new ArrayList<>();
// 首先跟面试官沟通好当根节点为空,返回 [] 还是 [[]]
if (pRoot == null) return res;
// 使用一个队列
LinkedList<TreeNode> q = new LinkedList<>();
q.add(pRoot);
while (!q.isEmpty()) {
int size = q.size();
ArrayList<Integer> level = new ArrayList<>();
for (int i = 0;i < size;i ++) {
TreeNode node = q.removeFirst();
level.add(node.val);
if (node.left != null) q.add(node.left);
if (node.right != null) q.add(node.right);
}
res.add(level);
}
return res;
}
}