Note
- 题解汇总:剑指offer题解汇总
- 代码地址:Github 剑指offer Java实现汇总
- 点击目录中的题名链接可直接食用题解~
- 有些解法博文中未实现,不代表一定很难,可能只是因为博主太懒```(Orz)
- 如果博文中有明显错误或者某些题目有更加优雅的解法请指出,谢谢~
目录
题号 | 题目名称 |
---|---|
51 | 构建乘积数组 |
52 | 正则表达式匹配 |
53 | 表示数值的字符串 |
54 | 字符流中第一个不重复的字符 |
55 | 链表中环的入口节点 |
56 | 删除链表中重复的节点 |
57 | 二叉树的下一个结点 |
58 | 对称的二叉树 |
59 | 按之字形顺序打印二叉树 |
60 | 把二叉树打印成多行 |
正文
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]。不能使用除法。
题目分析
解法一: 使用两个数组mul1和mul2,经过一次遍历分别存储前n个数字的乘积和后n个数字的乘积,生成结果数组时,只需要分别取出mul1和mul2中对应的数字相乘即可。时间复杂度:O(n) 空间复杂度:O(n)
解法二: 暴力法,两层for循环强行算出乘积。未实现。时间复杂度:O(n²) 空间复杂度:O(n)
代码实现
解法一: O(n)
public int[] multiply(int[] A) {
if (A == null || A.length < 2) return null;
int[] mul1 = new int[A.length];
int[] mul2 = new int[A.length];
mul1[0] = A[0];
mul2[A.length - 1] = A[A.length - 1];
for (int i = 1, j = A.length - 2; i < A.length; i++, j--) {
mul1[i] = mul1[i - 1] * A[i];
mul2[j] = mul2[j + 1] * A[j];
}
int[] B = new int[A.length];
for (int i = 0; i < B.length; i++) {
if (i == 0) {
B[i] = mul2[i + 1];
} else if (i == B.length - 1) {
B[i] = mul1[i - 1];
} else {
B[i] = mul1[i - 1] * mul2[i + 1];
}
}
return B;
}
52、正则表达式匹配
题目描述
请实现一个函数用来匹配包括’.‘和’*‘的正则表达式。模式中的字符’.‘表示任意一个字符,而’’*'表示它前面的字符可以出现任意次(包含0次)。在本题中,匹配是指字符串的所有字符匹配整个模式。例如,字符串"aaa"与模式"a.a"和"abaca"匹配,但是与"aa.a"和"ab*a"均不匹配
题目分析
解法一: 递归。先看*再看匹配。首先,当pattern遍历完时,如果str恰好遍历完则返回true,否则返回false。如果pattern的当前字符存在下一个字符,则看下一个字符是否是*,如果是,有两种情况:
1、当前str和pattern匹配。pattern的下一个字符*可能用来匹配多个str的当前字符,此时str后移;或者pattern的下一个字符*不用来匹配str的任何字符,此时pattern后移两位;
2、当前str和pattern不匹配,pattern后移两位即可。
如果下一个字符不是*,则str和pattern都后移一位。在比较过程中需要考虑pattern为’.‘的情况,但是’.‘只可能占一位比较字符,所以在判断匹配时多加一个pattern[j]==‘.’的条件即可。
解法二: 动态规划。dp[i][j]设置为boolean数组,它代表着str[i]及其以后的字符和pattern[j]及其以后的字符的匹配情况。我们对两个字符串从后往前进行遍历,判断pattern当前字符的下一个是否是’*’,如果下一个是’*‘且当前字符相等,则dp[i][j]=dp[i][j+2] || dp[i+1][j];如果下一个是’*'且当前字符不等,则dp[i][j]=dp[i][j+2] ;如果下一个不是‘*’且当前字符相等,则d[i][j]= dp[i + 1][j + 1]。
代码实现
解法一:
public boolean match(char[] str, char[] pattern) {
if (str == null || pattern == null) {
return false;
}
return helper(str, 0, pattern, 0);
}
public boolean helper(char[] str, int i, char[] pattern, int j) {
if (j == pattern.length) {
return i == str.length;
}
//pattern下一个字符是*
if ((j < pattern.length - 1) && (pattern[j + 1] == '*')) {
//当前字符匹配
if (i < str.length && (str[i] == pattern[j] || pattern[j] == '.')) {
return helper(str, i + 1, pattern, j) || helper(str, i, pattern, j + 2);
} else {
return helper(str, i, pattern, j + 2);
}
} else {
if (i < str.length && (str[i] == pattern[j] || pattern[j] == '.')) {
return helper(str, i + 1, pattern, j + 1);
}
}
return false;
}
解法二: O(mn)
public boolean match(char[] str, char[] pattern) {
if(str == null || pattern == null)
return false;
boolean [][] dp = new boolean[str.length + 1][pattern.length + 1];
dp[str.length][pattern.length] = true;
for (int i = str.length; i >= 0; i--) {
for (int j = pattern.length - 1; j >= 0; j--) {
if (j < pattern.length - 1 && pattern[j + 1] == '*') {
//下一个是'*'且当前字符相等
if (i < str.length && (str[i] == pattern[j] || pattern[j] == '.')) {
dp[i][j] = dp[i][j + 2] || dp[i + 1][j];
} else { //下一个是'*'且当前字符不等
dp[i][j] = dp[i][j + 2];
}
} else { //下一个不是'*'且当前字符相等
if (i < str.length && (str[i] == pattern[j] || pattern[j] == '.')) {
dp[i][j] = dp[i + 1][j + 1];
}
}
}
}
return dp[0][0];
}
53、表示数值的字符串
题目描述
请实现一个函数用来判断字符串是否表示数值(包括整数和小数)。例如,字符串"+100",“5e2”,"-123",“3.1416"和”-1E-16"都表示数值。 但是"12e",“1a3.14”,“1.2.3”,"±5"和"12e+4.3"都不是。
题目分析
解法一: 分类讨论。
±号后面必定为数字或后面为.(-.123 = -0.123)
±号只出现在第一位或在eE的后一位
.后面必定为数字或为最后一位(233. = 233.0)
eE后面必定为数字或±号
解法二: 正则表达式。
代码实现
解法一: O(n)
public boolean isNumeric(char[] str) {
boolean point = false;
boolean exp = false; //标识小数点和指数,只能出现一次
for (int i = 0; i < str.length; i++) {
if (str[i] == '+' || str[i] == '-') {
if (i + 1 == str.length || !(str[i + 1] >= '0' && str[i + 1] <= '9' || str[i + 1] == '.')) {
return false;
}
if (!(i == 0 || str[i-1] == 'e' || str[i-1] == 'E')) {
return false;
}
} else if (str[i] == '.') {
if (point || exp || !(i + 1 < str.length && str[i + 1] >= '0' && str[i + 1] <= '9')) {
return false;
}
point = true;
} else if (str[i] == 'e' || str[i] == 'E') {
if (exp || i + 1 == str.length || !(str[i + 1] >= '0' && str[i + 1] <= '9' || str[i + 1] == '+' || str[i + 1] == '-')) {
return false;
}
exp = true;
} else if (str[i] < '0' || str[i] > '9') {
return false;
}
}
return true;
}
解法二:
public boolean isNumeric1(char[] str) {
String pattern = "^[-+]?\\d*(?:\\.\\d*)?(?:[eE][+\\-]?\\d+)?$";
return Pattern.matches(pattern, new String(str));
}
54、字符流中第一个不重复的字符
题目描述
请实现一个函数用来找出字符流中第一个只出现一次的字符。例如,当从字符流中只读出前两个字符"go"时,第一个只出现一次的字符是"g"。当从该字符流中读出前六个字符“google"时,第一个只出现一次的字符是"l"。
输出描述
如果当前字符流没有存在出现一次的字符,返回#字符。
题目分析
解法一: 使用两个长度为256的数组模拟哈希表,一个用于流中字符的出现顺序,一个用于存储字符的出现次数。获得第一个不重复字符时,对记录出现次数的数组进行遍历,找到出现次数为1且出现顺序最小的那个字符并返回。
解法二: 使用两个哈希表。LinkedHashSet是输入有序的,用于按出现顺序保存不重复的字符;另一个HashSet用来保存出现次数大于1的字符。在Insert时对LinkedHashSet中的字符进行去重操作,需要取数据时,只需要取出LinkedHashSet中的第一个字符即可。
代码实现
解法一: O(n)
int[] count = new int[256]; // 字符出现的次数
int[] index = new int[256]; // 字符出现的次数
int number = 0;
public void Insert1(char ch) {
count[ch]++;
index[ch] = number++;
}
public char FirstAppearingOnce1() {
int minIndex = number;
char ch = '#';
for (int i = 0; i < 256; i++) {
if (count[i] == 1 && index[i] < minIndex) {
ch = (char) i;
minIndex = index[i];
}
}
return ch;
}
解法二: O(n)
Set<Character> set1 = new LinkedHashSet<>();
Set<Character> set2 = new HashSet<>();
public void Insert(char ch) {
if (!set1.contains(ch) && !set2.contains(ch)) {
set1.add(ch);
} else {
set1.remove(ch);
set2.add(ch);
}
}
public char FirstAppearingOnce() {
if (set1.isEmpty()) {
return '#';
} else {
char t = set1.iterator().next();
return t;
}
}
55、链表中环的入口节点
题目描述
给一个链表,若其中包含环,请找出该链表的环的入口结点,否则,输出null。
题目分析
解法一: 快慢指针法。快指针每次走两步,慢指针每次走一步,如果快指针能够走到null说明无环。若有环,快慢指针必定在某个节点meetNode相遇。和‘求倒数第k个节点’问题类似,此时将一个指针指向链表头部,一个指针指向meetNode,两个指针每次都只走一步,最终它们相遇的点即为环的入口节点。
代码实现
解法一: O(n)
public ListNode EntryNodeOfLoop(ListNode pHead) {
ListNode meetNode = meetNode(pHead);
if (meetNode == null) return null;
ListNode p1 = pHead;
ListNode p2 = meetNode;
while (p1 != p2) {
p1 = p1.next;
p2 = p2.next;
}
return p1;
}
public ListNode meetNode(ListNode pHead) {
ListNode pSlow = pHead;
ListNode pFast = pHead;
while (pSlow != null && pFast != null) {
pSlow = pSlow.next;
pFast = pFast.next;
if (pFast!= null) {
pFast = pFast.next;
}
if (pSlow != null && pFast != null && pSlow == pFast) {
return pSlow;
}
}
return null;
}
56、删除链表中重复的节点
题目描述
在一个排序的链表中,存在重复的结点,请删除该链表中重复的结点,重复的结点不保留,返回链表头指针。 例如,链表1->2->3->3->4->4->5 处理后为 1->2->5。
题目分析
解法一: 遍历链表。由于头结点也可能会重复,所以需要新建一个头结点。然后对链表进行遍历,pre指针永远指向cur的前一个节点,用于跳过重复节点。cur指针不断往后遍历,判断后续节点是否重复,重复则cur后移。最后判断cur是否移动过,如果没有移动过说明中间没有重复节点,则pre指向cur,cur后移;如果移动过说明中间有重复节点,pre的next指向cur的next。一定要记住:出现重复的话,pre指针一定不能直接后移!不然出现像“2->2->3->3”这种连续重复的情况,就无法通过pre删除重复节点了。
代码实现
解法一: O(n)
public static ListNode deleteDuplication(ListNode pHead) {
if (pHead == null) return pHead;
ListNode dummy = new ListNode(-1);
dummy.next = pHead;
ListNode pre = dummy, cur = pHead;
while (cur != null) {
while (cur.next != null && cur.val == cur.next.val) {
cur = cur.next;
}
if (pre.next == cur) {
pre = cur;
} else {
pre.next = cur.next;
}
cur = cur.next;
}
return dummy.next;
}
57、二叉树的下一个结点
题目描述
给定一个二叉树和其中的一个结点,请找出中序遍历顺序的下一个结点并且返回。注意,树中的结点不仅包含左右子结点,同时包含指向父结点的指针。
题目分析
解法一: 中序遍历特性。对于中序遍历而言。如果某个结点有右孩子,则遍历的下一个结点是它的右孩子的最左结点;如果某个节点没有右孩子,则遍历的下一个结点是它的某个作为左孩子的祖先结点。根据以上逻辑便能够写出代码,需要注意的是遍历过程中的最后一个结点,它的下一个结点为null。
代码实现
解法一: O(n)
public TreeLinkNode GetNext(TreeLinkNode pNode) {
if (pNode == null) return null;
if (pNode.right != null) {
pNode = pNode.right;
while (pNode.left != null) {
pNode = pNode.left;
}
return pNode;
} else {
while (pNode.next != null && pNode.next.right == pNode) {
pNode = pNode.next;
}
return pNode.next;
}
}
58、对称的二叉树
题目描述
请实现一个函数,用来判断一颗二叉树是不是对称的。注意,如果一个二叉树同此二叉树的镜像是同样的,定义其为对称的。
题目分析
解法一: 递归。子问题其实就是左子树的孩子和右子树的孩子是否对称,对称的实质就是左子树的左孩子等于右子树的右孩,左子树的右孩子等于右子树的左孩子。每次递归的pLeft和pRight都会指向同一层级的两个对称节点,然后对其左右子树进行递归的对称判断。
代码实现
解法一:
boolean isSymmetrical(TreeNode pRoot) {
if (pRoot == null) return true;
return helper(pRoot.left, pRoot.right);
}
boolean helper(TreeNode pLeft, TreeNode pRight) {
if (pLeft == null && pRight == null) return true;
if (pLeft == null || pRight == null) return false;
if (pLeft.val == pRight.val) {
return helper(pLeft.left, pRight.right) && helper(pLeft.right, pRight.left);
} else {
return false;
}
}
59、按之字形顺序打印二叉树
题目描述
请实现一个函数按照之字形打印二叉树,即第一行按照从左到右的顺序打印,第二层按照从右至左的顺序打印,第三行按照从左到右的顺序打印,其他行以此类推。
题目分析
解法一: 双栈法。本题的关键点有两个:如何判断当前层遍历完成;如何逆序打印。本解法使用两个栈,每个栈只存一层的节点,通过栈空来判断当前层的遍历是否完成;使用栈来完成逆序打印。
具体做法:根据层序遍历的思路,使用两个栈,轮流压栈和出栈。例如遍历第一行时,将第二行压入栈2,此时左右孩子压入的顺序是先左后右,则第二行遍历时会从右往左打印;遍历第二行时,将第三行压入栈1,此时左右孩子压入的顺序是先右后左,则第三行遍历时会从左往右打印。使用一个bool变量来标识此时的打印方向,用于判断此时应该压入哪个栈以及从哪个栈弹出。
解法二: BFS。依然是层序遍历思想,根据解法一中所提及的两个关键点。本解法通过队列长度来判断当前层是否遍历完成;通过list.add(T)完成顺序插入,通过list.add(0, T)将结果插入至表头,完成逆序插入。
具体做法:先得到队列长度size,该size就是当前层的节点个数,然后通过for循环将该层的所有节点poll出来,然后通过list.add(T)和list.add(0, T)完成list的插入。也可以不使用list.add(0, T),而是顺序插入,直到当前层插入完毕后使用Collection.reverse()对数组进行倒序。
代码实现
解法一:
public ArrayList<ArrayList<Integer>> Print(TreeNode pRoot) {
ArrayList<ArrayList<Integer>> list = new ArrayList<>();
Stack<TreeNode> stack1 = new Stack<>();
Stack<TreeNode> stack2 = new Stack<>();
boolean isRight = true;
if (pRoot == null) return list;
stack1.push(pRoot);
ArrayList<Integer> tList = new ArrayList<>();
while (!stack1.isEmpty() || !stack2.isEmpty()) {
if (isRight && !stack1.isEmpty()) {
TreeNode t = stack1.pop();
tList.add(t.val);
if (t.left != null) stack2.push(t.left);
if (t.right != null) stack2.push(t.right);
if (stack1.isEmpty()) {
list.add(tList);
tList = new ArrayList<>();
isRight = false;
}
continue;
}
if (!isRight && !stack2.isEmpty()) {
TreeNode t = stack2.pop();
tList.add(t.val);
if (t.right != null) stack1.push(t.right);
if (t.left != null) stack1.push(t.left);
if (stack2.isEmpty()) {
list.add(tList);
tList = new ArrayList<>();
isRight = true;
}
continue;
}
}
return list;
}
解法二:
public ArrayList<ArrayList<Integer> > Print1(TreeNode pRoot) {
LinkedList<TreeNode> linkedList = new LinkedList<>();
ArrayList<ArrayList<Integer>> res = new ArrayList<>();
boolean isRight = true;
linkedList.add(pRoot);
while (!linkedList.isEmpty()) {
int size = linkedList.size();
ArrayList<Integer> list = new ArrayList<>();
for (int i = 0; i < size; i++) {
TreeNode node = linkedList.poll();
if (node == null) continue;
if (isRight) {
list.add(node.val);
} else {
list.add(0, node.val);
}
linkedList.offer(node.left);
linkedList.offer(node.right);
}
if (list.size() != 0) {
res.add(list);
}
isRight = !isRight;
}
return res;
}
60、把二叉树打印成多行
题目描述
从上到下按层打印二叉树,同一层结点从左至右输出。每一层输出一行。
题目分析
解法一: 层序遍历。使用队列进行层序遍历,每次记录当前层的size,使用for循环将队列出队,并加入下一层节点,当for循环结束代表当前层的遍历已经结束,将当前数组加入结果数组中。
代码实现
解法一:
ArrayList<ArrayList<Integer>> Print(TreeNode pRoot) {
ArrayList<ArrayList<Integer>> list = new ArrayList<>();
if (pRoot == null) return list;
Queue<TreeNode> queue = new LinkedList<>();
queue.add(pRoot);
while (!queue.isEmpty()) {
int size = queue.size();
TreeNode t;
ArrayList<Integer> tList = new ArrayList<>();
for (int i = 0; i < size; i++) {
t = queue.poll();
tList.add(t.val);
if (t.left != null) {
queue.offer(t.left);
}
if (t.right != null) {
queue.offer(t.right);
}
}
list.add(tList);
}
return list;
}