本篇是记录本人在刷leetcode过程中的题解及经验总结,望读者善用Ctrl + F
。
数组篇
删除排序数组中的重复项
题目链接:https://leetcode-cn.com/problems/remove-duplicates-from-sorted-array
解题核心思想:双指针。
注意本题目的数组是有序的,这就说明重复的元素必然相邻。
题目要求不使用额外空间删除重复项,那么就说明这一题我们可以把不重复的元素都挪到数组的左边,最后返回不重复元素的个数即可。
解法:
- 定义一个指针
p
指向数组第一个元素,定义一个指针q
指向数组的第二个元素; - 比较
nums[p]
和nums[q]
:- 如果
nums[p] == nums[q]
:q
向后移动一位; - 如果
nums[p] != nums[q]
:把nums[q]
的值赋给nums[p + 1]
,p
和q
都向后移动一位;
- 如果
- 返回
p + 1
。
优化:
考虑如下数组:
此时数组中没有重复元素,按照上面的方法,每次比较时 nums[p]
都不等于 nums[q]
,因此就会将 q
指向的元素原地复制一遍,这个操作其实是不必要的。
因此我们可以添加一个小判断,当 q - p > 1
时,才进行复制。
题解:
int length = nums.length;
int p = 0, q = 1;
while(q < length){
if(nums[p] != nums[q]){
if(q - p > 1){
nums[p + 1] = nums[q];
}
p++;
}
q++;
}
return p + 1;
二分查找篇
搜索旋转排序数组
题目链接:https://leetcode-cn.com/problems/search-in-rotated-sorted-array
本题的难点在于我们并不知道这个数组旋转了几次,由传统的二分查找模板我们知道需要比较 nums[mid]
和 target
的值的大小来判断区间,但在此旋转数组中我们无法直接这样比较,因为二分查找的前提是数组有序。因此,我们需要讨论 nums[mid]
和 target
所处的区间位置。即:
先根据 nums[mid]
与 nums[low]
的关系判断 mid
是在左段还是右段,接下来再判断 target
是在 mid
的左边还是右边,从而来调整左右边界 low
和 high
。
题解:
class Solution {
public int search(int[] nums, int target) {
int low = 0, high = nums.length - 1;
while(low <= high){
int mid = low + (high - low) / 2;
if(nums[mid] == target){
return mid;
}
//说明 mid 在左段
if(nums[mid] >= nums[low]){
//说明 target 在左段
if(target < nums[mid] && target >= nums[low]){
high = mid - 1;
}else{
low = mid + 1;
}
}else{
if(target <= nums[high] && target > nums[mid]){
low = mid + 1;
}else{
high = mid - 1;
}
}
}
return -1;
}
}
搜索旋转排序数组 II
题目链接:https://leetcode-cn.com/problems/search-in-rotated-sorted-array-ii
这一题在上一题的基础上添加了一个条件:有重复元素,我们只需要加上这段代码就行了:
if(nums[low] == nums[mid] && nums[high] == nums[mid]) {
low++;
high--;
continue;
}
题解:
class Solution {
public boolean search(int[] nums, int target) {
int low = 0;
int high = nums.length - 1;
while(low <= high){
int mid = low + (high - low) / 2;
if(nums[mid] == target){
return true;
}
if(nums[low] == nums[mid] && nums[high] == nums[mid]) {
low++;
high--;
continue;
}
if(nums[mid] >= nums[low]){
if(target >= nums[low] && target < nums[mid]){
high = mid - 1;
}else{
low = mid + 1;
}
}else{
if(target <= nums[high] && target > nums[mid]){
low = mid + 1;
}else{
high = mid - 1;
}
}
}
return false;
}
}
总结:
这一题难就难在我们无法直接判断区间,所以需要分段讨论 nums[mid]
和 target
所在的区间是左还是右。
还有一个非常相似的题目:https://leetcode-cn.com/problems/search-rotate-array-lcci
这一题是在本题的基础上考虑了重复元素,要求返回重复元素的最小下标。由于本人精力有限,这题暂且先搁置一段时间。
链表篇
两数相加
题目地址:https://leetcode-cn.com/problems/add-two-numbers/
对于这类两数相加问题,我们需要考虑进位和补零,在两个节点的value相加大于等于10时,我们就需要进位,当一个节点为空而另一个节点不为空时,我们就要对空节点的value值进行补零操作。
题解:
/**
* Definition for singly-linked list.
* public class ListNode {
* int val;
* ListNode next;
* ListNode(int x) { val = x; }
* }
*/
class Solution {
public ListNode addTwoNumbers(ListNode l1, ListNode l2) {
ListNode dummy = new ListNode(0);
ListNode cur = dummy;
int carry = 0;
while(l1 != null || l2 != null){
int x = l1 == null ? 0 : l1.val;
int y = l2 == null ? 0 : l2.val;
int sum = x + y + carry;
carry = sum / 10;
sum = sum % 10;
cur.next = new ListNode(sum);
cur = cur.next;
if(l1 != null){
l1 = l1.next;
}
if(l2 != null){
l2 = l2.next;
}
}
if(carry != 0){
cur.next = new ListNode(carry);
}
return dummy.next;
}
}
其中,carry表示进位。
如果这道题能够理解,那么两数相加Ⅱ也就迎刃而解了。这道题主要是需要把链表翻转过来再进行上面那道题的操作,我们先来熟悉一下链表的反转:
private ListNode reverse1(ListNode head){
ListNode pre = null;
ListNode cur = head;
while(cur != null){
ListNode tmp = cur.next;
cur.next = pre;
pre = cur;
cur = tmp;
}
return pre;
}
如果是还不太熟悉链表的反转,建议先去做一下这道题目:206. 反转链表。
下面是完整题解:
/**
* Definition for singly-linked list.
* public class ListNode {
* int val;
* ListNode next;
* ListNode(int x) { val = x; }
* }
*/
class Solution {
public ListNode addTwoNumbers(ListNode l1, ListNode l2){
ListNode dummy = new ListNode(0);
ListNode cur = dummy;
ListNode r1 = reverse(l1);
ListNode r2 = reverse(l2);
int carry = 0;
while(r1 != null || r2 != null){
int x = r1 == null ? 0 : r1.val;
int y = r2 == null ? 0 : r2.val;
int sum = x + y + carry;
carry = sum / 10;
sum = sum % 10;
cur.next = new ListNode(sum);
cur = cur.next;
if(r1 != null){
r1 = r1.next;
}
if(r2 != null){
r2 = r2.next;
}
}
if(carry != 0){
cur.next = new ListNode(carry);
}
ListNode rr = reverse(dummy.next);
return rr;
}
private ListNode reverse(ListNode l){
ListNode pre = null;
ListNode cur = l;
while(cur != null){
ListNode tmp = cur.next;
cur.next = pre;
pre = cur;
cur = tmp;
}
return pre;
}
}
对于两数相加Ⅱ,我们还可以用栈来实现,这里推荐去看一下力扣的评论区,在此不再赘述。
删除链表的倒数第N个节点
题目地址:https://leetcode-cn.com/problems/remove-nth-node-from-end-of-list/
本题实在是经典,不得不拿出来说说了。
鉴于两次遍历大家都会,我就不在此细说了,想看详细解说可以去本题的讨论区逛一逛,本篇博客主要讲使用双指针一次遍历的解法。
思路:
提到删除,我们自然而然的就会想到找到要删除的节点的前一个节点,将它的指针域指向要删除节点的后一个节点,也就是 p.next = p.next.next
,而这题也不例外,我们需要找到该节点的前一个节点。
我们可以设想假设设定了双指针 p
和 q
的话,当 q
指向末尾的 null
,p
与 q
之间相隔的元素个数为 n
时,那么删除掉 p
的下一个指针就完成了要求。
流程:
- 设置一个虚拟节点
dummy
指向head
; - 定义两个指针
p
和q
同时指向dummy
; - 让指针
q
移动n + 1
个位置,使p
和q
的间距(不包含两头)为n
; - 再使
q
和p
同时移动,直至q
为null
,此时,p
正好指向要被删除的元素的前一个元素; - 接下来该干什么就不用我说了吧?懂得都懂,芜湖,起飞!
为了方便理解,在此,引用一下程序员吴师兄精心制作的动图:
题解:
/**
* Definition for singly-linked list.
* public class ListNode {
* int val;
* ListNode next;
* ListNode(int x) { val = x; }
* }
*/
class Solution {
public ListNode removeNthFromEnd(ListNode head, int n) {
ListNode dummy = new ListNode(0);
dummy.next = head;
ListNode p = dummy;
ListNode q = dummy;
int length = 0;
while(length < n + 1){
q = q.next;
length++;
}
while(q != null){
q = q.next;
p = p.next;
}
p.next = p.next.next;
return dummy.next;
}
}
合并两个有序链表
题目地址:https://leetcode-cn.com/problems/merge-two-sorted-lists/
本题有两种解法,一种迭代,一种递归。
迭代
我们可以用迭代的方法来实现上述算法。当 l1
和 l2
都不是空链表时,判断 l1
和 l2
哪一个链表的头节点的值更小,将较小值的节点添加到结果里,当一个节点被添加到结果里之后,将对应链表中的节点向后移一位。
流程:
- 先创建一个哑节点
dummy
,定义一个指针pre
指向dummy
; - 比较两链表各节点的值
val
,使pre
和l1
orl2
同时向后移动一位; - 到其中一个链表为空时,将另一个链表拼接至结果链表就行了。
题解:
/**
* Definition for singly-linked list.
* public class ListNode {
* int val;
* ListNode next;
* ListNode() {}
* ListNode(int val) { this.val = val; }
* ListNode(int val, ListNode next) { this.val = val; this.next = next; }
* }
*/
class Solution {
public ListNode mergeTwoLists(ListNode l1, ListNode l2) {
ListNode dummy = new ListNode(0);
ListNode pre = dummy;
while(l1 != null && l2 != null){
if(l1.val <= l2.val){
pre.next = l1;
l1 = l1.next;
}else{
pre.next = l2;
l2 = l2.next;
}
pre = pre.next;
}
pre.next = (l1 == null ? l2 : l1);
return dummy.next;
}
}
递归
递归解法我也不是很明白,就暂且把代码放上来,以后慢慢理解吧。
题解:
/**
* Definition for singly-linked list.
* public class ListNode {
* int val;
* ListNode next;
* ListNode(int x) { val = x; }
* }
*/
class Solution {
public ListNode mergeTwoLists(ListNode l1, ListNode l2) {
if(l1 == null) {
return l2;
}
if(l2 == null) {
return l1;
}
if(l1.val < l2.val) {
l1.next = mergeTwoLists(l1.next, l2);
return l1;
} else {
l2.next = mergeTwoLists(l1, l2.next);
return l2;
}
}
}
反转部分链表
题目地址:https://leetcode-cn.com/problems/reverse-linked-list-ii/
本题是反转链表的进阶版,不熟悉反转链表的可以看我的这篇博客。
正如反转链表一样,这一题我们依然可以使用双指针来解决。
流程:
- 定义一个哑节点
dummy
; - 定义两个指针
p
和q
,一前一后; - 使指针
q
指向反转的起始位置,p
指向q
的前一个节点; - 将
q
后面的节点依次删除,并利用头插法插入到p
指向节点的后面。
题解:
/**
* Definition for singly-linked list.
* public class ListNode {
* int val;
* ListNode next;
* ListNode(int x) { val = x; }
* }
*/
class Solution {
public ListNode reverseBetween(ListNode head, int m, int n) {
ListNode dummy = new ListNode(0);
dummy.next = head;
ListNode p = dummy;
ListNode q = dummy.next;
int length = 0;
while(length < m - 1){
p = p.next;
q = q.next;
length++;
}
for(int i = 0; i < n - m; ++i){
ListNode tmp = q.next;
q.next = q.next.next;
tmp.next = p.next;
p.next = tmp;
}
return dummy.next;
}
}
环形链表
题目地址:https://leetcode-cn.com/problems/linked-list-cycle/
本题有两种解法,一种是使用哈希表的暴力解法,还有一种是使用快慢指针的巧妙解法。
下面将着重介绍快慢指针解法。
快慢指针
当一个链表有环时,快慢指针都会陷入环中进行无限次移动,然后变成了追及问题。想象一下在操场跑步的场景,只要一直跑下去,快的总会追上慢的。当两个指针都进入环后,每轮移动使得慢指针到快指针的距离增加一,同时快指针到慢指针的距离也减少一,只要一直移动下去,快指针总会追上慢指针。
根据上面的原理得出,如果一个链表存在环,那么快慢指针必然会相遇。
题解:
/**
* Definition for singly-linked list.
* class ListNode {
* int val;
* ListNode next;
* ListNode(int x) {
* val = x;
* next = null;
* }
* }
*/
public class Solution {
public boolean hasCycle(ListNode head) {
if(head == null) return false;
ListNode fast = head;
ListNode slow = head;
while(fast != null && fast.next != null){
slow = slow.next;
fast = fast.next.next;
if(slow == fast){
fast = head;
while(slow != fast){
slow = slow.next;
fast = fast.next;
}
return true;
}
}
return false;
}
}
哈希表
如果一个链表是环形链表,那么必然有若干个节点会被遍历多次。
我们遍历所有结点并在哈希表中存储每个结点的引用(或内存地址)。
- 如果当前结点为空结点
null
(即已检测到链表尾部的下一个结点),那么我们已经遍历完整个链表,并且该链表不是环形链表。- 如果当前结点的引用已经存在于哈希表中,那么返回
true
(即该链表为环形链表)。
题解:
/**
* Definition for singly-linked list.
* class ListNode {
* int val;
* ListNode next;
* ListNode(int x) {
* val = x;
* next = null;
* }
* }
*/
public boolean hasCycle(ListNode head) {
Set<ListNode> nodesSeen = new HashSet<>();
while (head != null) {
if (nodesSeen.contains(head)) {
return true;
} else {
nodesSeen.add(head);
}
head = head.next;
}
return false;
}
总结:在环形链表问题上,使用快慢指针解法非常方便。
回文链表
题目地址:https://leetcode-cn.com/problems/palindrome-linked-list/
这一题我采用的是暴力解法:
将链表转换为数组,然后利用双指针从数组的两端比较值是否相等。
题解:
/**
* Definition for singly-linked list.
* public class ListNode {
* int val;
* ListNode next;
* ListNode(int x) { val = x; }
* }
*/
class Solution {
public boolean isPalindrome(ListNode head) {
ArrayList<Integer> list = new ArrayList<>();
ListNode tmp = head;
while(tmp != null){
list.add(tmp.val);
tmp = tmp.next;
}
int slow = 0;
int fast = list.size() - 1;
while(slow < fast){
if(!list.get(slow).equals(list.get(fast))){
return false;
}
slow++;
fast--;
}
return true;
}
}
相交链表
题目地址:
- https://leetcode-cn.com/problems/intersection-of-two-linked-lists/
- https://leetcode-cn.com/problems/intersection-of-two-linked-lists-lcci/
这两题都是相交链表问题。
本题仅用文字可能说不太明白,建议大家看这里的视频讲解。
题解:
/**
* Definition for singly-linked list.
* public class ListNode {
* int val;
* ListNode next;
* ListNode(int x) {
* val = x;
* next = null;
* }
* }
*/
public class Solution {
public ListNode getIntersectionNode(ListNode headA, ListNode headB) {
if(headA == null || headB == null) return null;
ListNode pA = headA;
ListNode pB = headB;
while(pA != pB){
pA = (pA == null ? headB : pA.next);
pB = (pB == null ? headA : pB.next);
}
return pA;
}
}
旋转链表
题目地址:https://leetcode-cn.com/problems/rotate-list
本题在面试中比较常考,应好好掌握。
思路:既然是每次将链表向右移动 k 个长度,那么我们可以联想到环,可以先把链表成环,然后再根据题目的要求从环的某一处断开不就可以了吗?
为了方便理解,在此我引用大佬liweiwei1419的图:
题解:
/**
* Definition for singly-linked list.
* public class ListNode {
* int val;
* ListNode next;
* ListNode(int x) { val = x; }
* }
*/
class Solution {
public ListNode rotateRight(ListNode head, int k) {
if(head == null || head.next == null || k == 0) return head;
ListNode fast = head;
int length = 1;
while(fast.next != null){
fast = fast.next;
length++;
}
k = k % length;
if(k == 0) return head;
ListNode slow = head;
for(int i = 0; i < length - k - 1; ++i){
slow = slow.next;
}
ListNode newHead = slow.next;
slow.next = null;
fast.next = head;
return newHead;
}
}
小结:
- 如果当前指针
p
在head
节点上,且head
节点的值是有效的,那么指针p
只需要移动length - k - 1
次就可以移动到链表的倒数第 k 个节点的前一个结点了。 - 判断链表长度可以用
while
循环:定义一个指针q
在头节点,循环条件为q.next != null
。 - 向右旋转链表是从链表的倒数第
k
个节点前断环,向左旋转链表则是从链表的正数第k
个节点后断环。
复杂链表的复制
题目地址:
本题是复制一个带随机指针的链表,我们使用HashMap。
因为是复制链表,所以我们需要先复制每一个节点,由于HashMap可以存储任意类型的值,这里我们就可以想到使用HashMap来复制节点:
HashMap<Node, Node> map = new HashMap<>();
复制完节点后,就需要复制指针,这里我们就需要用到 map 的 put 方法和 get 方法,具体如下图:
具体代码如下:
/*
// Definition for a Node.
class Node {
int val;
Node next;
Node random;
public Node(int val) {
this.val = val;
this.next = null;
this.random = null;
}
}
*/
class Solution {
public Node copyRandomList(Node head) {
HashMap<Node, Node> map = new HashMap<>();
Node cur = head;
while(cur != null){
map.put(cur, new Node(cur.val));
cur = cur.next;
}
cur = head;
while(cur != null){
map.get(cur).next = map.get(cur.next);
map.get(cur).random = map.get(cur.random);
cur = cur.next;
}
return map.get(head);
}
}
对于复制指针,我们一定要注意不能写成这样:
map.get(cur).next = cur.next;
map.get(cur).random = cur.random;
**这样是错误的!**因为我们是要返回一个新的链表,如果是上面这种情况,新的链表就会指向旧的链表,从而出错!
栈篇
有效的括号
题目地址:https://leetcode-cn.com/problems/valid-parentheses
思路:
此题是栈的一个常见应用,即判断括号的有效性。根据栈后进先出的特性,我们可以将括号push进栈中,待需要对比时pop出来即可。
题解:
class Solution {
public boolean isValid(String s) {
Stack<Character> stack = new Stack<Character>();
for(char c : s.toCharArray()){
if(c == '('){
stack.push(')');
}else if(c == '['){
stack.push(']');
}else if(c == '{'){
stack.push('}');
}else if(stack.isEmpty() || c != stack.pop()){
return false;
}
}
return stack.isEmpty();
}
}
二叉树篇
590. N叉树的后序遍历
题目链接:https://leetcode-cn.com/problems/n-ary-tree-postorder-traversal/
思路:
后序遍历即根节点在最后,我们可以使用LinkedList的addFirst()方法每次都将元素插入链表的第一个节点中,这样最先插入的根节点就会移动到链表的最后去了。
使用 LinkedList 创建两个链表,初次判断根节点是否为空,如果为空,则返回空链表。如果不为空,将根节点插入链表2的第一个节点中,然后判断链表2是否为空,若不为空,将链表2中的唯一一个节点插入链表1的头一个节点中,遍历根节点的子节点,若不为空,插入链表2中,反复循环直至链表2为空。
题解:
/*
// Definition for a Node.
class Node {
public int val;
public List<Node> children;
public Node() {}
public Node(int _val) {
val = _val;
}
public Node(int _val, List<Node> _children) {
val = _val;
children = _children;
}
};
*/
class Solution {
public List<Integer> postorder(Node root) {
LinkedList<Integer> res = new LinkedList<Integer>();
LinkedList<Node> stack = new LinkedList<Node>();
if(root == null) return res;
stack.push(root);
while(!stack.isEmpty()){
Node node = stack.pop();
res.addFirst(node.val);
for(Node child:node.children){
if(child != null){
stack.push(child);
}
}
}
return res;
}
}
补充:
Java 集合 LinkedList push()
和pop()
方法
public void push(E e)
:将元素插入列表的前面。
public E pop()
:删除并返回列表的第一个元素。
101. 对称二叉树
题目链接:https://leetcode-cn.com/problems/symmetric-tree/
思路:
1.判断根节点是否为空——–空返回true
2.判断左右子树是否对称———对称就返回true
2.1.当左子树的左节点等于右子树的右节点,且左子树的右节点等于右子树的左节点时对称
3.递归终止条件:
3.1.左子树右子树均为null——–true
3.2.左子树右子树只有一个为null——–false
3.3.左子树的值不等于右子树的值——–false
递归流程图
题解:
/**
* Definition for a binary tree node.
* public class TreeNode {
* int val;
* TreeNode left;
* TreeNode right;
* TreeNode(int x) { val = x; }
* }
*/
class Solution{
public boolean isSymmetric(TreeNode root){
if(root == null) return true;
return LR(root.left,root.right);
}
public boolean LR(TreeNode leftChild,TreeNode rightChild){
if(leftChild == null && rightChild == null)
return true;
if(leftChild == null || rightChild == null)
return false;
if(leftChild.val != rightChild.val)
return false;
return LR(leftChild.left,rightChild.right)&&LR(leftChild.right,rightChild.left);
}
}
面试题55 - I. 二叉树的深度
题目链接:https://leetcode-cn.com/problems/er-cha-shu-de-shen-du-lcof/
思路:本题可以使用递归的方法解决。二叉树的深度就是左右子树最大深度 + 1。
1.递归的终止条件:root为空。
2.递归工作: 本质上是对树做后序遍历。
- 计算节点 root 的 左子树的深度 ,即调用
maxDepth(root.left)
- 计算节点 root 的 右子树的深度 ,即调用
maxDepth(root.right)
3.返回值: 返回 此树的深度 ,即 max(maxDepth(root.left), maxDepth(root.right)) + 1
题解:
/**
* Definition for a binary tree node.
* public class TreeNode {
* int val;
* TreeNode left;
* TreeNode right;
* TreeNode(int x) { val = x; }
* }
*/
class Solution {
public int maxDepth(TreeNode root) {
if(root == null) return 0;
return Math.max(maxDepth(root.left),maxDepth(root.right)) + 1;
}
}
本题进阶:
面试题55 - II. 平衡二叉树
题目链接:https://leetcode-cn.com/problems/ping-heng-er-cha-shu-lcof/
本题思路:
后序遍历 + 剪枝 (从底至顶)
此方法为本题的最优解法,但剪枝的方法不易第一时间想到。
思路是对二叉树做后序遍历,从底至顶返回子树深度,若判定某子树不是平衡树则 “剪枝” ,直接向上返回。
算法流程:
recur(root)
函数:
返回值:
- 当节点root 左 / 右子树的深度差 ≤1 :则返回当前子树的深度,即节点 root 的左 / 右子树的深度最大值+1(
max(left, right) + 1
); - 当节点root 左 / 右子树的深度差>2 :则返回−1,代表此子树不是平衡树 。
终止条件:
- 当 root 为空:说明越过叶节点,因此返回高度0 ;
- 当左(右)子树深度为−1 :代表此树的左(右)子树 不是平衡树,因此剪枝,直接返回 −1 ;
isBalanced(root)
函数:
返回值: 若 recur(root) != -1
,则说明此树平衡,返回true ; 否则返回false 。
题解:
/**
* Definition for a binary tree node.
* public class TreeNode {
* int val;
* TreeNode left;
* TreeNode right;
* TreeNode(int x) { val = x; }
* }
*/
class Solution {
public boolean isBalanced(TreeNode root) {
return recur(root) != -1;
}
public int recur(TreeNode root){
if(root == null) return 0;
int left = recur(root.left);
if(left == -1) return -1;
int right = recur(root.right);
if(right == -1) return -1;
return Math.abs(left - right) < 2 ? Math.max(left,right) + 1 : -1;
}
}
重建二叉树
题目地址:https://leetcode-cn.com/problems/zhong-jian-er-cha-shu-lcof/
本题是一道比较经典的题目,核心思想其实还是递归。(好像二叉树大多都是使用递归……)
我们知道前序遍历的第一个节点是根节点root,中序遍历root两边的分别是左子树和右子树,根据这一特性,我们就可以求解本题,下面给出某位大佬的解题思路(原地址请点击这里):
题目分析:
前序遍历特点: 节点按照 [ 根节点 | 左子树 | 右子树 ] 排序,以题目示例为例:[ 3 | 9 | 20 15 7 ]
中序遍历特点: 节点按照 [ 左子树 | 根节点 | 右子树 ] 排序,以题目示例为例:[ 9 | 3 | 15 20 7 ]
根据题目描述输入的前序遍历和中序遍历的结果中都不含重复的数字,其表明树中每个节点值都是唯一的。
根据以上特点,可以按顺序完成以下工作:
- 前序遍历的首个元素即为根节点 root 的值;
- 在中序遍历中搜索根节点 root 的索引 ,可将中序遍历划分为
[ 左子树 | 根节点 | 右子树 ]
。 - 根据中序遍历中的左(右)子树的节点数量,可将前序遍历划分为
[ 根节点 | 左子树 | 右子树 ]
。
自此可确定 三个节点的关系 :1.树的根节点、2.左子树根节点、3.右子树根节点(即前序遍历中左(右)子树的首个元素)。
子树特点: 子树的前序和中序遍历仍符合以上特点,以题目示例的右子树为例:前序遍历:[20 | 15 | 7],中序遍历 [ 15 | 20 | 7 ] 。
根据子树特点,我们可以通过同样的方法对左(右)子树进行划分,每轮可确认三个节点的关系 。此递推性质让我们联想到用 递归方法 处理。
递归解析:
递推参数: 前序遍历中根节点的索引pre_root
、中序遍历左边界in_left
、中序遍历右边界in_right
。
终止条件: 当in_left
>in_right
,子树中序遍历为空,说明已经越过叶子节点,此时返回 null 。
递推工作:
建立根节点root
: 值为前序遍历中索引为pre_root
的节点值。
搜索根节点root
在中序遍历的索引i
: 为了提升搜索效率,本题解使用哈希表 dic 预存储中序遍历的值与索引的映射关系,每次搜索的时间复杂度为 O(1)。
构建根节点root
的左子树和右子树: 通过调用 recur()
方法开启下一层递归。
左子树: 根节点索引为pre_root + 1
,中序遍历的左右边界分别为in_left
和i - 1
。
右子树: 根节点索引为i - in_left + pre_root + 1
(即:根节点索引 + 左子树长度 + 1),中序遍历的左右边界分别为 i + 1
和in_right
。
返回值: 返回 root
,含义是当前递归层级建立的根节点 root
为上一递归层级的根节点的左或右子节点。
/**
* Definition for a binary tree node.
* public class TreeNode {
* int val;
* TreeNode left;
* TreeNode right;
* TreeNode(int x) { val = x; }
* }
*/
class Solution {
HashMap<Integer, Integer> map = new HashMap<>();
int[] preorder;
public TreeNode buildTree(int[] preorder, int[] inorder) {
this.preorder = preorder;
for(int i = 0;i < inorder.length;++i){
map.put(inorder[i],i);
}
return recur(0,0,inorder.length -1);
}
TreeNode recur(int pre_root, int in_left, int in_right) {
if(in_left > in_right) return null;
TreeNode root = new TreeNode(preorder[pre_root]);
int i = map.get(preorder[pre_root]);
root.left = recur(pre_root + 1,in_left,i - 1);
root.right = recur(pre_root + (i - 1 - in_left + 1) + 1,i + 1,in_right);
return root;
}
}
关于上面的root.left
和root.right
有些朋友可能不是很理解,这里给出解释:
root.left:
root.right:
剑指 Offer 32 - I. 从上到下打印二叉树
题目链接:https://leetcode-cn.com/problems/cong-shang-dao-xia-da-yin-er-cha-shu-lcof/
思路:
看到按层序遍历二叉树,我们就该想到广度优先搜索(BFS),而一提到广度优先搜索,我们就可以想到用**队列(Queue)**来解决。
算法流程:
- 特例处理:当根节点为空时,返回一个空数组
[]
; - 初始化:创建一个临时的动态数组存放
val
值,再创建一个带有根节点的队列; - BFS循环:
- 终止条件:队列为空
- 出队:将队首元素出队,记为
node
- 存值:将
node
的值存放在动态数组中 - 入队:若
node
的左右节点不为空,将node
的左右节点加入到队列中(注意:由于队列是FIFO结构,题目要求从左到右打印,所以必须左节点先入队)
- 将动态数组的值转移到结果数组
res
,返回res
。
题解:
/**
* Definition for a binary tree node.
* public class TreeNode {
* int val;
* TreeNode left;
* TreeNode right;
* TreeNode(int x) { val = x; }
* }
*/
class Solution {
public int[] levelOrder(TreeNode root) {
if(root == null) return new int[0];
Queue<TreeNode> queue = new LinkedList<>();
ArrayList<Integer> tmp = new ArrayList<>();
queue.offer(root);
//直至队列为空时终止循环
while(queue.size() != 0){
TreeNode node = queue.poll();
tmp.add(node.val);
if(node.left != null) queue.offer(node.left);
if(node.right != null) queue.offer(node.right);
}
//将动态数组中的值转移给要返回的结果数组
int[] res = new int[tmp.size()];
for(int i = 0;i < res.length;++i){
res[i] = tmp.get(i);
}
return res;
}
}
剑指 Offer 32 - II. 从上到下打印二叉树 II
题目链接:https://leetcode-cn.com/problems/cong-shang-dao-xia-da-yin-er-cha-shu-ii-lcof/
思路:
这一题与上一题只有很微小的差别,个人觉得这一题还比较难一些,然而它却是简单难度的题目😀。废话少说,我们开始这一题的解析:
这一题与上一题最大的不同就是我们需要把每一层单独打印出来,返回的结果是嵌套的List,然而思路是不会变的,我们依旧使用队列解决。
题解:
/**
* Definition for a binary tree node.
* public class TreeNode {
* int val;
* TreeNode left;
* TreeNode right;
* TreeNode(int x) { val = x; }
* }
*/
class Solution {
public List<List<Integer>> levelOrder(TreeNode root) {
Queue<TreeNode> queue = new LinkedList<>();
List<List<Integer>> res = new ArrayList<>();
if(root != null) queue.offer(root);
while(queue.size() != 0){
List<Integer> tmp = new ArrayList<>();
//这里因为queue.size()是变化的,所以我们采用递减的方法
for(int i = queue.size(); i > 0; i--){
TreeNode node = queue.poll();
tmp.add(node.val);
if(node.left != null) queue.offer(node.left);
if(node.right != null) queue.offer(node.right);
}
res.add(tmp);
}
return res;
}
}
剑指 Offer 32 - III. 从上到下打印二叉树 III
题目链接:https://leetcode-cn.com/problems/cong-shang-dao-xia-da-yin-er-cha-shu-iii-lcof/
思路:
本题目在上一题的基础上又增加了奇偶层打印次序的问题,我们可以使用一个双端队列,奇数层插入尾部,偶数层时插入头部。
题解:
/**
* Definition for a binary tree node.
* public class TreeNode {
* int val;
* TreeNode left;
* TreeNode right;
* TreeNode(int x) { val = x; }
* }
*/
class Solution {
public List<List<Integer>> levelOrder(TreeNode root) {
Queue<TreeNode> queue = new LinkedList<>();
List<List<Integer>> res = new ArrayList<>();
if(root != null) queue.offer(root);
while(queue.size() != 0){
//模拟双端队列
LinkedList<Integer> tmp = new LinkedList<>();
for(int i = queue.size();i > 0;--i){
TreeNode node = queue.poll();
//这里res.size()初始为0,所以在打印第二层时res.size()为1,第三层时res.size()为2,依此类推
if(res.size() % 2 == 0){
tmp.addLast(node.val);
}else{
tmp.addFirst(node.val);
}
if(node.left != null) queue.offer(node.left);
if(node.right != null) queue.offer(node.right);
}
res.add(tmp);
}
return res;
}
}
199. 二叉树的右视图
题目链接:https://leetcode-cn.com/problems/binary-tree-right-side-view/
BFS
思路: 利用 BFS 进行层次遍历,记录下每层的最后一个元素。
时间复杂度: O(N),每个节点都入队出队了 1 次。
空间复杂度: O(N),使用了额外的队列空间。
题解:
/**
* Definition for a binary tree node.
* public class TreeNode {
* int val;
* TreeNode left;
* TreeNode right;
* TreeNode(int x) { val = x; }
* }
*/
class Solution {
public List<Integer> rightSideView(TreeNode root) {
List<Integer> res = new ArrayList<>();
Queue<TreeNode> queue = new LinkedList<>();
if(root == null) return res;
queue.offer(root);
while(queue.size() != 0){
int length = queue.size();
for(int i = 0;i < length;++i){
TreeNode node = queue.poll();
if(node.left != null) queue.offer(node.left);
if(node.right != null) queue.offer(node.right);
if(i == length - 1) res.add(node.val);
}
}
return res;
}
}
DFS
思路: 我们按照 「根结点 -> 右子树 -> 左子树」
的顺序访问,就可以保证每层都是最先访问最右边的节点的。
(与先序遍历 「根结点 -> 左子树 -> 右子树」
正好相反,先序遍历每层最先访问的是最左边的节点)
时间复杂度: O(N),每个节点都访问了 1 次。
空间复杂度: O(N),因为这不是一棵平衡二叉树,二叉树的深度最少是 logN , 最坏的情况下会退化成一条链表,深度就是 N ,因此递归时使用的栈空间是 O(N) 的。
题解:
/**
* Definition for a binary tree node.
* public class TreeNode {
* int val;
* TreeNode left;
* TreeNode right;
* TreeNode(int x) { val = x; }
* }
*/
class Solution {
List<Integer> res = new ArrayList<>();
public List<Integer> rightSideView(TreeNode root) {
dfs(root, 0); // 从根节点开始访问,根节点深度是0
return res;
}
private void dfs(TreeNode root, int depth) {
if (root == null) {
return;
}
// 先访问 当前节点,再递归地访问 右子树 和 左子树。
if (depth == res.size()) { // 如果当前节点所在深度还没有出现在res里,说明在该深度下当前节点是第一个被访问的节点,因此将当前节点加入res中。
res.add(root.val);
}
depth++;
dfs(root.right, depth);
dfs(root.left, depth);
}
}
二叉树的最近公共祖先
题目链接:https://leetcode-cn.com/problems/lowest-common-ancestor-of-a-binary-tree/
本题解析请看这里
题解:
/**
* Definition for a binary tree node.
* public class TreeNode {
* int val;
* TreeNode left;
* TreeNode right;
* TreeNode(int x) { val = x; }
* }
*/
class Solution {
public TreeNode lowestCommonAncestor(TreeNode root, TreeNode p, TreeNode q) {
if(root == null || root == p || root == q) return root;
TreeNode left = lowestCommonAncestor(root.left,p,q);
TreeNode right = lowestCommonAncestor(root.right,p,q);
if(left == null && right == null) return null;
if(left == null) return right;
if(right == null) return left;
return root;
}
}
验证二叉搜索树
题目链接:https://leetcode-cn.com/problems/validate-binary-search-tree/
思路:
本题为验证二叉搜索树(BST),那么我们应该联想到二叉搜索树的特性,那就是:
- 若它的左子树不为空,那么左子树上所有节点的 key 都小于根节点的 key。
- 若它的右子树不为空,那么右子树上所有节点的 key 都大于根节点的 key。
- 它的左右子树也分别为二叉搜索树。
发现了吗,BST 的节点大小顺序为:left
< root
< right
。
利用这一特性,很自然的我们就会想到 BST 的中序遍历其实就是一个递增的序列。
那么我们该如何验证某棵树是不是 BST,采用中序遍历,只需要看当前节点是否大于已经遍历的上一个节点就行了。
题解:
/**
* Definition for a binary tree node.
* public class TreeNode {
* int val;
* TreeNode left;
* TreeNode right;
* TreeNode(int x) { val = x; }
* }
*/
class Solution {
long pre = Long.MIN_VALUE;
public boolean isValidBST(TreeNode root) {
if(root == null) return true;
if(!isValidBST(root.left)) return false;
if(root.val <= pre) return false;
pre = root.val;
return isValidBST(root.right);
}
}
总结:在我们看到***二叉搜索树***这几个字的时候,脑子里应该自然想到它的几个特点:
- 节点值大小顺序:
left
<root
<right
; - 二叉搜索树的中序遍历的结果是递增的。
将有序数组转换为二叉搜索树
题目链接:https://leetcode-cn.com/problems/convert-sorted-array-to-binary-search-tree/
这一题不算太难,但结合了二分查找的一些特性,所以单独拿出来讲讲。
题目描述:
将一个按照升序排列的有序数组,转换为一棵高度平衡二叉搜索树。
我们知道二叉搜索树的中序遍历结果正是一个按照升序排列的序列,那么这一题的意思显然就是让我们由一个按照升序排列的有序数组还原出一个二叉搜索树,因此我们可以以升序序列中的任一个元素作为根节点,以该元素左边的升序序列构建左子树,以该元素右边的升序序列构建右子树,这样得到的树就是一棵二叉搜索树。又因为本题要求高度平衡,因此我们需要选择升序序列的中间元素作为根节点。这就又结合了上面提到的二分查找的特性了,即不断地找中间元素作为根节点。
题解:
/**
* Definition for a binary tree node.
* public class TreeNode {
* int val;
* TreeNode left;
* TreeNode right;
* TreeNode(int x) { val = x; }
* }
*/
class Solution {
public TreeNode sortedArrayToBST(int[] nums) {
return recur(nums, 0, nums.length - 1);
}
public TreeNode recur(int[] nums, int lo, int hi){
if(lo > hi) return null;
int mid = (lo + hi)/2;
TreeNode root = new TreeNode(nums[mid]);
root.left = recur(nums, lo, mid - 1);
root.right = recur(nums, mid + 1, hi);
return root;
}
}