# (1.8w字)负重前行，前端工程师如何系统练习数据结构和算法？【上】

63,550 阅读57分钟

## 练习之前，你需要了解的

### 算法没有用？

``````if (cache[key]) {
vnode.componentInstance = cache[key].componentInstance
// make current key freshest
remove(keys, key)
keys.push(key)
}
``````

### 如何系统练习？

#### 刻意练习

1. 经常性地做你不会做的题

2. 多种解法方式轮流试，最大化地挖掘一道题的价值

# 链表篇

## 反转链表

### No.1 简单的反转链表

``````输入: 1->2->3->4->5->NULL

``````

#### 循环解决方案

``````function ListNode(val) {
this.val = val;
this.next = null;
}
``````

``````/**
* @return {ListNode}
*/
let reverseList =  (head) => {
return null;
let pre = null, cur = head;
while (cur) {
// 关键: 保存下一个节点的值
let next = cur.next;
cur.next = pre;
pre = cur;
cur = next;
}
return pre;
};
``````

• 当链表只包含`一个节点`时, 此时我们希望直接返回这个节点，对上述代码而言，进入循环后 `pre` 被赋值为`cur`，也就是`head`，没毛病，通过✅

#### 递归解决方案

``````let reverseList = (head) =>{
let reverse = (pre, cur) => {
if(!cur) return pre;
// 保存 next 节点
let next = cur.next;
cur.next = pre;
return reverse(cur, next);
}
}
``````

### No.2 区间反转

``````输入: 1->2->3->4->5->NULL, m = 2, n = 4

``````

#### 循环解法

``````/**
* @param {number} m
* @param {number} n
* @return {ListNode}
*/
var reverseBetween = function(head, m, n) {
let count = n - m;
let p = dummyHead = new ListNode();
let pre, cur, start, tail;
for(let i = 0; i < m - 1; i ++) {
p = p.next;
}
// 保存前节点
front = p;
// 同时保存区间首节点
pre = tail = p.next;
cur = pre.next;
// 区间反转
for(let i = 0; i < count; i++) {
let next = cur.next;
cur.next = pre;
pre = cur;
cur = next;
}
// 前节点的 next 指向区间末尾
front.next = pre;
// 区间首节点的 next 指向后节点(循环完后的cur就是区间后面第一个节点，即后节点)
tail.next = cur;
};
``````

#### 递归解法

``````var reverseBetween = function(head, m, n) {
// 递归反转函数
let reverse = (pre, cur) => {
if(!cur) return pre;
// 保存 next 节点
let next = cur.next;
cur.next = pre;
return reverse(cur, next);
}
let p = dummyHead = new ListNode();
let start, end; //区间首尾节点
let front, tail; //前节点和后节点
for(let i = 0; i < m - 1; i++) {
p = p.next;
}
front = p; //保存前节点
start = front.next;
for(let i = m - 1; i < n; i++) {
p = p.next;
}
end = p;
tail = end.next; //保存后节点
end.next = null;
// 开始穿针引线啦，前节点指向区间首，区间首指向后节点
front.next = reverse(null, start);
start.next = tail;
}
``````

### No.3 两个一组翻转链表

``````给定 1->2->3->4, 你应该返回 2->1->4->3.
``````

#### 循环解决

``````var swapPairs = function(head) {
let dummyHead = p = new ListNode();
let node1, node2;
while((node1 = p.next) && (node2 = p.next.next)) {
node1.next = node2.next;
node2.next = node1;
p.next = node2;
p = node1;
}
};
``````

#### 递归方式

``````var swapPairs = function(head) {
node1.next = swapPairs(node2.next);
node2.next = node1;
return node2;
};
``````

### No.4 K个一组翻转链表

k 是一个正整数，它的值小于或等于链表的长度。

``````给定这个链表：1->2->3->4->5

``````

• 你的算法只能使用常数的额外空间。
• 你不能只是单纯的改变节点内部的值，而是需要实际的进行节点交换。

#### 递归解法

``````/**
* @param {number} k
* @return {ListNode}
*/
var reverseKGroup = function(head, k) {
let pre = null, cur = head;
// 下面的循环用来检查后面的元素是否能组成一组
for(let i = 0; i < k; i++) {
p = p.next;
}
for(let i = 0; i < k; i++){
let next = cur.next;
cur.next = pre;
pre = cur;
cur = next;
}
// pre为本组最后一个节点，cur为下一组的起点
return pre;
};
``````

#### 循环解法

``````var reverseKGroup = function(head, k) {
let count = 0;
// 看是否能构成一组，同时统计链表元素个数
for(let p = head; p != null; p = p.next) {
if(p == null && i < k) return head;
count++;
}
let loopCount = Math.floor(count / k);
let p = dummyHead = new ListNode();
// 分成了 loopCount 组，对每一个组进行反转
for(let i = 0; i < loopCount; i++) {
let pre = null, cur = p.next;
for(let j = 0; j < k; j++) {
let next = cur.next;
cur.next = pre;
pre = cur;
cur = next;
}
// 当前 pre 为该组的尾结点，cur 为下一组首节点
let start = p.next;// start 是该组首节点
// 开始穿针引线！思路和2个一组的情况一模一样
p.next = pre;
start.next = cur;
p = start;
}
};
``````

## 环形链表

### No.1 如何检测链表形成环？

#### 方法一: Set 判重

``````/**
* @return {boolean}
*/
var hasCycle = (head) => {
let set = new Set();
while(p) {
// 同一个节点再次碰到，表示有环
if(set.has(p)) return true;
p = p.next;
}
return false;
}
``````

#### 方法二: 快慢指针

``````var hasCycle = function(head) {
let fast = slow = dummyHead;
// 零个结点或者一个结点，肯定无环
if(fast.next == null || fast.next.next == null)
return false;
while(fast && fast.next) {
fast = fast.next.next;
slow = slow.next;
// 两者相遇了
if(fast == slow) {
return true;
}
}
return false;
};
``````

### No.2 如何找到环的起点

**说明：**不允许修改给定的链表。

#### 思路分析

① - ② * 2 得:

L = (m - n) * S - Y-----③

`新指针``慢指针`都每次走一步，那么，当`新指针`走了 L 步之后到达环起点，而与此同时，我们看看`慢指针情况如何`

#### 编程实现

``````/**
* @return {ListNode}
*/
let fast = slow = dummyHead;
// 零个结点或者一个结点，肯定无环
if(fast.next == null || fast.next.next == null)
return null;
while(fast && fast.next) {
fast = fast.next.next;
slow = slow.next;
// 两者相遇了
if(fast == slow) {
while(p != slow) {
p = p.next;
slow = slow.next;
}
return p;
}
}
return null;
};
``````

## 链表合并

### No.1 合并两个有序链表

``````输入：1->2->4, 1->3->4

``````

#### 递归解法

``````/**
* @param {ListNode} l1
* @param {ListNode} l2
* @return {ListNode}
*/
var mergeTwoLists = function(l1, l2) {
const merge = (l1, l2) => {
if(l1 == null) return l2;
if(l2 == null) return l1;
if(l1.val > l2.val) {
l2.next = merge(l1, l2.next);
return l2;
}else {
l1.next = merge(l1.next, l2);
return l1;
}
}
return merge(l1, l2);
};
``````

#### 循环解法

``````var mergeTwoLists = function(l1, l2) {
if(l1 == null) return l2;
if(l2 == null) return l1;
let p = dummyHead = new ListNode();
let p1 = l1, p2 = l2;
while(p1 && p2) {
if(p1.val > p2.val) {
p.next = p2;
p = p.next;
p2 = p2.next;
}else {
p.next = p1;
p = p.next;
p1 = p1.next;
}
}
// 循环完成后务必检查剩下的部分
if(p1) p.next = p1;
else p.next = p2;
};
``````

### No.2 合并 K 个有序链表

``````输入:
[
1->4->5,
1->3->4,
2->6
]

``````

#### 自上而下(递归)实现

``````/**
* @param {ListNode[]} lists
* @return {ListNode}
*/
var mergeKLists = function(lists) {
// 上面已经实现
var mergeTwoLists = function(l1, l2) {/*上面已经实现*/};
const _mergeLists = (lists, start, end) => {
if(end - start < 0) return null;
if(end - start == 0)return lists[end];
let mid = Math.floor(start + (end - start) / 2);
return mergeTwoList(_mergeLists(lists, start, mid), _mergeLists(lists, mid + 1, end));
}
return _mergeLists(lists, 0, lists.length - 1);
};
``````

#### 自下而上实现

``````var mergeKLists = function(lists) {
var mergeTwoLists = function(l1, l2) {/*上面已经实现*/};
// 边界情况
if(!lists || !lists.length) return null;
// 虚拟头指针集合
// 初始化虚拟头指针
for(let i = 0; i < lists.length; i++) {
let node = new ListNode();
node.next = lists[i];
}
// 自底向上进行merge
for(let size = 1; size < lists.length; size += size){
for(let i = 0; i + size < lists.length;i += 2 * size) {
}
}
};
``````

## 求链表中间节点

### 判断回文链表

``````输入: 1->2

``````

``````输入: 1->2->2->1

``````

#### 代码实现

``````  let dummyHead = slow = fast = new ListNode();
// 注意注意，来找中点了
while(fast && fast.next) {
slow = slow.next;
fast = fast.next.next;
}
``````

• 当链表节点个数为奇数

• 当链表节点个数为偶数

``````/**
* @return {boolean}
*/
let reverse = (pre, cur) => {
if(!cur) return pre;
let next = cur.next;
cur.next = pre;
return reverse(cur, next);
}
let dummyHead = slow = fast = new ListNode();
// 注意注意，来找中点了, 黄金模板
while(fast && fast.next) {
slow = slow.next;
fast = fast.next.next;
}
let next = slow.next;
slow.next = null;
let newStart = reverse(null, next);
for(let p = head, newP = newStart; newP != null; p = p.next, newP = newP.next) {
if(p.val != newP.val) return false;
}
return true;
};
``````

# 栈和队列篇

## 栈&递归

### 有效括号

``````输入: "()"

``````

#### 代码实现

``````/**
* @param {string} s
* @return {boolean}
*/
var isValid = function(s) {
let stack = [];
for(let i = 0; i < s.length; i++) {
let ch = s.charAt(i);
if(ch == '(' || ch == '[' || ch == '{')
stack.push(ch);
if(!stack.length) return false;
if(ch == ')' && stack.pop() !== '(') return false;
if(ch == ']' && stack.pop() !== '[' ) return false;
if(ch == '}' && stack.pop() !== '{') return false;
}
return stack.length === 0;
};
``````

### 多维数组 flatten

``````[1, [2, [3, [4, 5]]], 6] -> [1, 2, 3, 4, 5, 6]
``````

#### 代码实现

``````/**
* @constructor
* @param {NestedInteger[]} nestedList
* @return {Integer[]}
*/
let flatten = (nestedList) => {
let result = [];
let fn = function (target, ary) {
for (let i = 0; i < ary.length; i++) {
let item = ary[i];
if (Array.isArray(ary[i])) {
fn(target, item);
} else {
target.push(item);
}
}
}
fn(result, nestedList)
return result;

``````

``````let flatten = (nestedList) =>  nestedList.reduce((pre, cur) => pre.concat(Array.isArray(cur) ? flatten(cur): cur), [])
``````

## 二叉树层序遍历

`二叉树的层序遍历`本是下一章的内容，但是其中队列的性质又体现得太明显，因此就以这样一类问题来让大家练习队列的相关操作。这里会不仅仅涉及到`普通的层序遍历`, 而且涉及到变形问题，让大家彻底掌握。

### 普通的层次遍历

``````  3
/ \
9  20
/  \
15   7
``````

``````[
[3],
[9,20],
[15,7]
]
``````

#### 实现

``````/**
* @param {TreeNode} root
* @return {number[][]}
*/
var levelOrder = function(root) {
if(!root) return [];
let queue = [];
let res = [];
let level = 0;
queue.push(root);
let temp;
while(queue.length) {
res.push([]);
let size = queue.length;
// 注意一下: size -- 在层次遍历中是一个非常重要的技巧
while(size --) {
// 出队
let front = queue.shift();
res[level].push(front.val);
// 入队
if(front.left) queue.push(front.left);
if(front.right) queue.push(front.right);
}
level++;
}
return res;
};
``````

### 二叉树的锯齿形层次遍历

``````    3
/ \
9  20
/  \
15   7
``````

``````[
[3],
[20,9],
[15,7]
]
``````

#### 代码实现

``````var zigzagLevelOrder = function(root) {
if(!root) return [];
let queue = [];
let res = [];
let level = 0;
queue.push(root);
let temp;
while(queue.length) {
res.push([]);
let size = queue.length;
while(size --) {
// 出队
let front = queue.shift();
res[level].push(front.val);
if(front.left) queue.push(front.left);
if(front.right) queue.push(front.right);
}
// 仅仅增加下面一行代码即可
if(level % 2) res[level].reverse();
level++;
}
return res;
};
``````

### 二叉树的右视图

``````输入: [1,2,3,null,5,null,4]

1            <---
/   \
2     3         <---
\     \
5     4       <---
``````

#### 代码实现

``````/**
* @param {TreeNode} root
* @return {number[]}
*/
var rightSideView = function(root) {
if(!root) return [];
let queue = [];
let res = [];
queue.push(root);
while(queue.length) {
res.push(queue[0].val);
let size = queue.length;
while(size --) {
// 一个size的循环就是一层的遍历，在这一层只拿最右边的结点
let front = queue.shift();
if(front.right) queue.push(front.right);
if(front.left) queue.push(front.left);
}
}
return res;
};
``````

## 无权图 BFS 遍历

### 完全平方数

``````输入: n = 12

``````

#### 思路

Talk is cheap, show me your code. 我们接下来来一步步实现这个寻找的过程。

#### 实现

``````/**
* @param {number} n
* @return {number}
*/
var numSquares = function(n) {
let queue = [];
queue.push([n, 0]);
while(queue.length) {
let [num, step] = queue.shift();
for(let i = 1; ; i ++) {
let nextNum = num - i * i;
if(nextNum < 0) break;
// 还差最后一步就到了，直接返回 step + 1
if(nextNum == 0) return step + 1;
queue.push([nextNum, step + 1]);
}
}
// 最后是不需要返回另外的值的，因为 1 也是完全平方数，所有的数都能用 1 来组合
};
``````

``````queue.push([nextNum, step + 1]);
``````

``````var numSquares = function(n) {
let map = new Map();
let queue = [];
queue.push([n, 0]);
map.set(n, true);
while(queue.length) {
let [num, step] = queue.shift();
for(let i = 1; ; i++) {
let nextNum = num - i * i;
if(nextNum < 0) break;
if(nextNum == 0) return step + 1;
// nextNum 未被访问过
if(!map.get(nextNum)){
queue.push([nextNum, step + 1]);
// 标记已经访问过
map.set(nextNum, true);
}
}
}
};
``````

### 单词接龙

• 每次转换只能改变一个字母。
• 转换过程中的中间单词必须是字典中的单词。

1. 如果不存在这样的转换序列，返回 0。
2. 所有单词具有相同的长度。
3. 所有单词只由小写字母组成。
4. 字典中不存在重复的单词。
5. 你可以假设 beginWord 和 endWord 是非空的，且二者不相同。

``````输入:
beginWord = "hit",
endWord = "cog",
wordList = ["hot","dot","dog","lot","log","cog"]

返回它的长度 5。
``````

#### 代码实现

``````/**
* @param {string} beginWord
* @param {string} endWord
* @param {string[]} wordList
* @return {number}
*/
var ladderLength = function(beginWord, endWord, wordList) {
// 两个单词在图中是否相邻
const isSimilar = (a, b) => {
let diff = 0
for(let i = 0; i < a.length; i++) {
if(a.charAt(i) !== b.charAt(i)) diff++;
if(diff > 1) return false;
}
return true;
}
let queue = [beginWord];
let index = wordList.indexOf(beginWord);
if(index !== -1) wordList.splice(index, 1);
let res = 2;
while(queue.length) {
let size = queue.length;
while(size --) {
let front = queue.shift();
for(let i = 0; i < wordList.length; i++) {
if(!isSimilar(front, wordList[i]))continue;
// 找到了
if(wordList[i] === endWord) {
return res;
}
else {
queue.push(wordList[i]);
}
// wordList[i]已经成功推入，现在不需要了，删除即可
// 这一步性能优化，相当关键，不然100%超时
wordList.splice(i, 1);
i --;
}
}
// 步数 +1
res += 1;
}
return 0;
};
``````

## 实现优先队列

### 关于堆的说明

siftDown是与其相反方向的操作，从上到下比较，原理相同，也是为了保证堆的正确结构。

### 实现一个最大堆

``````// 以最大堆为例来实现一波
/**
* @param {number[]} nums
* @param {number} k
* @return {number[]}
*/
class MaxHeap {
constructor(arr = [], compare = null) {
this.data = arr;
this.size = arr.length;
this.compare = compare;
}
getSize() {
return this.size;
}
isEmpty() {
return this.size === 0;
}
// 增加元素
this.data.push(value);
this.size++;
// 增加的时候把添加的元素进行 siftUp
this._siftUp(this.getSize() - 1);
}
// 找到优先级最高的元素
findMax() {
if (this.getSize() === 0)
return;
return this.data[0];
}
// 让优先级最高的元素(即队首元素)出队
extractMax() {
// 1.保存队首元素
let ret = this.findMax();
// 2.让队首和队尾元素交换位置
this._swap(0, this.getSize() - 1);
// 3. 把队尾踢出去，size--
this.data.pop();
this.size--;
// 4. 新的队首 siftDown
this._siftDown(0);
return ret;
}

toString() {
console.log(this.data);
}
_swap(i, j) {
[this.data[i], this.data[j]] = [this.data[j], this.data[i]];
}
_parent(index) {
return Math.floor((index - 1) / 2);
}
_leftChild(index) {
return 2 * index + 1;
}
_rightChild(index) {
return 2 * index + 2;
}
_siftUp(k) {
// 上浮操作，只要子元素优先级比父节点大，父子交换位置，一直向上直到根节点
while (k > 0 && this.compare(this.data[k], this.data[this._parent(k)])) {
this._swap(k, this._parent(k));
k = this._parent(k);
}
}
_siftDown(k) {
// 存在左孩子的时候
while (this._leftChild(k) < this.size) {
let j = this._leftChild(k);
// 存在右孩子而且右孩子比左孩子大
if (this._rightChild(k) < this.size &&
this.compare(this.data[this._rightChild(k)], this.data[j])) {
j++;
}
if (this.compare(this.data[k], this.data[j]))
return;
// 父节点比子节点小，交换位置
this._swap(k, j);
// 继续下沉
k = j;
}
}
}
``````

### 实现优先队列

``````class PriorityQueue {
// max 为优先队列的容量
constructor(max, compare) {
this.max = max;
this.compare = compare;
this.maxHeap = new MaxHeap([], compare);
}

getSize() {
return this.maxHeap.getSize();
}

isEmpty() {
return this.maxHeap.isEmpty();
}

getFront() {
return this.maxHeap.findMax();
}

enqueue(e) {
// 比当前最高的优先级的还要高，直接不处理
if (this.getSize() === this.max) {
if (this.compare(e, this.getFront())) return;
this.dequeue();
}
}

dequeue() {
if (this.getSize() === 0) return null;
return this.maxHeap.extractMax();
}
}
``````

``````let pq = new PriorityQueue(3);
pq.enqueue(1);
pq.enqueue(333);
console.log(pq.dequeue());
console.log(pq.dequeue());
pq.enqueue(3);
pq.enqueue(6);
pq.enqueue(62);
console.log(pq.dequeue());
console.log(pq.dequeue());
console.log(pq.dequeue());
``````

``````333
1
62
6
3
``````

## 优先队列应用

### 前 K 个高频元素

``````输入: nums = [1,1,1,2,2,3], k = 2

``````

• 你可以假设给定的 k 总是合理的，且 1 ≤ k ≤ 数组中不相同的元素的个数。
• 你的算法的时间复杂度必须优于 O(n log n) , n 是数组的大小。

#### 代码实现

``````var topKFrequent = function(nums, k) {
let map = {};
let pq = new PriorityQueue(k, (a, b) => map[a] - map[b] < 0);
for(let i = 0; i < nums.length; i++) {
if(!map[nums[i]]) map[nums[i]] = 1;
else map[nums[i]] = map[[nums[i]]] + 1;
}
let arr = Array.from(new Set(nums));
for(let i = 0; i < arr.length; i++) {
pq.enqueue(arr[i]);
}
return pq.maxHeap.data;
};
``````

### 合并 K 个排序链表

``````输入:
[
1->4->5,
1->3->4,
2->6
]

``````

``````/**
* @param {ListNode[]} lists
* @return {ListNode}
*/
var mergeKLists = function(lists) {
let dummyHead = p = new ListNode();
// 定义优先级的函数，重要！
let pq = new PriorityQueue(lists.length, (a, b) => a.val <= b.val);
// 将头结点推入优先队列
for(let i = 0; i < lists.length; i++)
if(lists[i]) pq.enqueue(lists[i]);
// 取出值最小的节点，如果 next 不为空，继续推入队列
while(pq.getSize()) {
let min = pq.dequeue();
p.next = min;
p = p.next;
if(min.next) pq.enqueue(min.next);
}
};
``````

## 双端队列及应用

### 什么是双端队列？

JS 中的数组就是一种典型的双端队列。push、pop 方法分别从尾部添加和删除元素，unshift、shift 方法分别从首部添加和删除元素。

### 滑动窗口最大值

``````输入: nums = [1,3,-1,-3,5,3,6,7], 和 k = 3

滑动窗口的位置                最大值
---------------               -----
[1  3  -1] -3  5  3  6  7       3
1 [3  -1  -3] 5  3  6  7       3
1  3 [-1  -3  5] 3  6  7       5
1  3  -1 [-3  5  3] 6  7       5
1  3  -1  -3 [5  3  6] 7       6
1  3  -1  -3  5 [3  6  7]      7
``````

#### 代码实现

``````var maxSlidingWindow = function(nums, k) {
// 异常处理
if(nums.length === 0 || !k) return [];
let window = [], res = [];
for(let i = 0; i < nums.length; i++) {
// 先把滑动窗口之外的踢出
if(window[0] !== undefined && window[0] <= i - k) window.shift();
// 保证队首是最大的
while(nums[window[window.length - 1]] <= nums[i])  window.pop();
window.push(i);
if(i >= k - 1) res.push(nums[window[0]])
}
return res;
};
``````

## 栈和队列的相互实现

### 栈实现队列

push(x) -- 将一个元素放入队列的尾部。 pop() -- 从队列首部移除元素。 peek() -- 返回队列首部的元素。 empty() -- 返回队列是否为空。

``````let queue = new MyQueue();

queue.push(1);
queue.push(2);
queue.peek();  // 返回 1
queue.pop();   // 返回 1
queue.empty(); // 返回 false
``````

#### 代码实现

``````var MyQueue = function() {
this.stack1 = [];
this.stack2 = [];
};

MyQueue.prototype.push = function(x) {
this.stack1.push(x);
};
// 将 stack1 的元素转移到 stack2
MyQueue.prototype.transform = function() {
while(this.stack1.length) {
this.stack2.push(this.stack1.pop());
}
}

MyQueue.prototype.pop = function() {
if(!this.stack2.length) this.transform();
return this.stack2.pop();
};

MyQueue.prototype.peek = function() {
if(!this.stack2.length) this.transform();
return this.stack2[this.stack2.length - 1];
};

MyQueue.prototype.empty = function() {
return !this.stack1.length && !this.stack2.length;
};
``````

### 队列实现栈

#### 代码实现

``````var MyStack = function() {
this.queue1 = [];
this.queue2 = [];
};
MyStack.prototype.push = function(x) {
if(!this.queue2.length) this.queue1.push(x);
else {
// queue2 已经有值
this.queue2.push(x);
// 旧的栈顶移到 queue1 中
this.queue1.push(this.queue2.shift());
}

};
MyStack.prototype.transform = function() {
while(this.queue1.length !== 1) {
this.queue2.push(this.queue1.shift())
}
// queue2 保存了前面的元素
// 让 queue1 和 queue2 交换
// 现在queue1 包含前面的元素，queue2 里面就只包含队尾的元素
let tmp = this.queue1;
this.queue1 = this.queue2;
this.queue2 = tmp;
}
MyStack.prototype.pop = function() {
if(!this.queue2.length) this.transform();
return this.queue2.shift();
};
MyStack.prototype.top = function() {
if(!this.queue2.length) this.transform();
return this.queue2[0];
};
MyStack.prototype.empty = function() {
return !this.queue1.length && !this.queue2.length;
};
``````

# 二叉树篇

## 二叉树的遍历

### 前序遍历

``````示例:

1
\
2
/
3

``````

#### 递归方式

``````/**
* @param {TreeNode} root
* @return {number[]}
*/
var preorderTraversal = function(root) {
let arr = [];
let traverse = (root) => {
if(root == null) return;
arr.push(root.val);
traverse(root.left);
traverse(root.right);
}
traverse(root);
return arr;
};
``````

#### 非递归方式

``````var preorderTraversal = function(root) {
if(root == null) return [];
let stack = [], res = [];
stack.push(root);
while(stack.length) {
let node = stack.pop();
res.push(node.val);
// 左孩子后进先出，进行先左后右的深度优先遍历
if(node.right) stack.push(node.right);
if(node.left) stack.push(node.left);
}
return res;
};
``````

### 中序遍历

``````输入: [1,null,2,3]
1
\
2
/
3

``````

#### 递归方式:

``````/**
* @param {TreeNode} root
* @return {number[]}
*/
var inorderTraversal = function(root) {
let arr = [];
let traverse = (root) => {
if(root == null) return;
traverse(root.left);
arr.push(root.val);
traverse(root.right);
}
traverse(root);
return arr;
};
``````

#### 非递归方式

``````var inorderTraversal = function(root) {
if(root == null) return [];
let stack = [], res = [];
let p = root;
while(stack.length || p) {
while(p) {
stack.push(p);
p = p.left;
}
let node = stack.pop();
res.push(node.val);
p = node.right;
}
return res;
};
``````

### 后序遍历

``````输入: [1,null,2,3]

1
\
2
/
3

``````

#### 递归方式

``````/**
* @param {TreeNode} root
* @return {number[]}
*/
var postorderTraversal = function(root) {
let arr = [];
let traverse = (root) => {
if(root == null) return;
traverse(root.left);
traverse(root.right);
arr.push(root.val);
}
traverse(root);
return arr
};
``````

#### 非递归方式

``````var postorderTraversal = function(root) {
if(root == null) return [];
let stack = [], res = [];
let visited = new Set();
let p = root;
while(stack.length || p) {
while(p) {
stack.push(p);
p = p.left;
}
let node = stack[stack.length - 1];
// 如果右孩子存在，而且右孩子未被访问
if(node.right && !visited.has(node.right)) {
p = node.right;
} else {
res.push(node.val);
stack.pop();
}
}
return res;
};
``````

## 最大/最小深度

### 最大深度

``````    3
/ \
9  20
/  \
15   7
``````

#### 递归实现

``````/**
* @param {TreeNode} root
* @return {number}
*/
var maxDepth = function(root) {
// 递归终止条件
if(root == null) return 0;
return Math.max(maxDepth(root.left) + 1, maxDepth(root.right) + 1);
};
``````

#### 非递归实现

``````var maxDepth = function(root) {
if(root == null) return 0;
let queue = [root];
let level = 0;
while(queue.length) {
let size = queue.length;
while(size --) {
let front = queue.shift();
if(front.left) queue.push(front.left);
if(front.right) queue.push(front.right);
}
// level ++ 后的值代表着现在已经处理完了几层节点
level ++;
}
return level;
};
``````

### 最小深度

``````    3
/ \
9  20
/  \
15   7
``````

#### 递归实现

``````/**
* @param {TreeNode} root
* @return {number}
*/
var minDepth = function(root) {
// 递归终止条件
if(root == null) return 0;
return Math.min(minDepth(root.left) + 1, minDepth(root.right)+1);
};
``````

``````var minDepth = function(root) {
if(root == null) return 0;
// 左右孩子都不为空才能像刚才那样调用
if(root.left && root.right)
return Math.min(minDepth(root.left), minDepth(root.right)) + 1;
// 右孩子为空了，直接忽略之
else if(root.left)
return minDepth(root.left) + 1;
// 左孩子为空，忽略
else if(root.right)
return minDepth(root.right) + 1;
// 两个孩子都为空，说明到达了叶子节点，返回 1
else return 1;
};
``````

#### 非递归实现

``````var minDepth = function(root) {
if(root == null) return 0;
let queue = [root];
let level = 0;
while(queue.length) {
let size = queue.length;
while(size --) {
let front = queue.shift();
// 找到叶子节点
if(!front.left && !front.right) return level + 1;
if(front.left) queue.push(front.left);
if(front.right) queue.push(front.right);
}
// level ++ 后的值代表着现在已经处理完了几层节点
level ++;
}
return level;
};
``````

## 对称二叉树

``````    1
/ \
2   2
/ \ / \
3  4 4  3
``````

``````    1
/ \
2   2
\   \
3    3
``````

### 递归实现

``````/**
* @param {TreeNode} root
* @return {boolean}
*/
var isSymmetric = function(root) {
let help = (node1, node2) => {
// 都为空
if(!node1 && !node2) return true;
// 一个为空一个不为空，或者两个节点值不相等
if(!node1 || !node2 || node1.val !== node2.val) return false;
return help(node1.left, node2.right) && help(node1.right, node2.left);
}
if(root == null) return true;
return help(root.left, root.right);
};
``````

### 非递归实现

``````var isSymmetric = function(root) {
if(root == null) return true;
let queue = [root.left, root.right];
let node1, node2;
while(queue.length) {
node1 = queue.shift();
node2 = queue.shift();
// 两节点均为空
if(!node1 && !node2)continue;
// 一个为空一个不为空，或者两个节点值不相等
if(!node1 || !node2 || node1.val !== node2.val) return false;
queue.push(node1.left);
queue.push(node2.right);
queue.push(node1.right);
queue.push(node2.left);
}
return true;
};
``````

## LCA 问题

LCA (Lowest Common Ancestor)即最近公共祖先问题。

### 二叉树的最近公共祖先

``````输入: root = [3,5,1,6,2,0,8,null,null,7,4], p = 5, q = 1

``````

#### 祖先节点集合法

``````/**
* @param {TreeNode} root
* @param {TreeNode} p
* @param {TreeNode} q
* @return {TreeNode}
*/
var lowestCommonAncestor = function(root, p, q) {
if(root == null || root == p || root == q) return root;
let set = new Set();
let map = new WeakMap();
let queue = [];
queue.push(root);
// 层序遍历
while(queue.length) {
let size = queue.length;
while(size --) {
let front = queue.shift();
if(front.left) {
queue.push(front.left);
// 记录父亲节点
map.set(front.left, front);
}
if(front.right) {
queue.push(front.right);
// 记录父亲节点
map.set(front.right, front);
}
}
}
// 构造 p 的上层节点集合
while(p) {
p = map.get(p);
}
while(q) {
// 一旦发现公共节点重合，直接返回
if(set.has(q))return q;
q = map.get(q);
}
};
``````

#### 深度优先遍历法

``````var lowestCommonAncestor = function(root, p, q) {
if (root == null || root == p || root == q) return root;
let left = lowestCommonAncestor(root.left, p, q);
let right = lowestCommonAncestor(root.right, p, q);
if(left == null) return right;
else if(right == null) return left;
return root;
};
``````

### 二叉搜索树的最近公共祖先

``````输入: root = [6,2,8,0,4,7,9,null,null,3,5], p = 2, q = 8

``````

#### 实现

``````/**
* @param {TreeNode} root
* @param {TreeNode} p
* @param {TreeNode} q
* @return {TreeNode}
*/
var lowestCommonAncestor = function(root, p, q) {
if(root == null || root == p || root == q) return root;
// root.val 比 p 和 q 都大，找左孩子
if(root.val > p.val && root.val > q.val)
return lowestCommonAncestor(root.left, p, q);
// root.val 比 p 和 q 都小，找右孩子
if(root.val < p.val && root.val < q.val)
return lowestCommonAncestor(root.right, p, q);
else
return root;
};
``````

``````var lowestCommonAncestor = function(root, p, q) {
let node = root;
while(node) {
if(p.val > node.val && q.val > node.val)
node = node.right;
else if(p.val < node.val && q.val < node.val)
node = node.left;
else return node;
}
};
``````

## 二叉树中的路径问题

### No.1 二叉树的直径

``````          1
/ \
2   3
/ \
4   5
``````

#### 思路

``````          1
/
2
/ \
4   5
/     \
8       6
\
7
``````

#### 初步求解

``````/**
* @param {TreeNode} root
* @return {number}
*/
var diameterOfBinaryTree = function(root) {
// 求最大深度
let maxDepth = (node) => {
if(node == null) return 0;
return Math.max(maxDepth(node.left) + 1, maxDepth(node.right) + 1);
}
let help = (node) => {
if(node == null) return 0;
let rootSum = maxDepth(node.left) + maxDepth(node.right);
let childSum = Math.max(help(node.left), help(node.right));
return Math.max(rootSum, childSum);
}
if(root == null) return 0;
return help(root);
};
``````

``````          1
/
2
/ \
4   5
/     \
8       6
\
7
``````

#### 优化解法

``````var diameterOfBinaryTree = function(root) {
let help = (node) => {
if(node == null) return 0;
let left = node.left ? help(node.left) + 1: 0;
let right = node.right ? help(node.right) + 1: 0;
let cur = left + right;
if(cur > max) max = cur;
// 这个返回的操作相当关键
return Math.max(left, right);
}
let max = 0;
if(root == null) return 0;
help(root);
return max;
};
``````

### No.2 二叉树的所有路径

``````输入:

1
/   \
2     3
\
5

``````

#### 递归解法

``````/**
* @param {TreeNode} root
* @return {string[]}
*/
var binaryTreePaths = function(root) {
let path = [];
let res = [];
let dfs = (node) => {
if(node == null) return;
path.push(node);
dfs(node.left);
dfs(node.right);
if(!node.left && !node.right)
res.push(path.map(item => item.val).join('->'));
// 注意每访问完一个节点记得把它从path中删除，达到回溯效果
path.pop();
}
dfs(root);
return res;
};
``````

#### 非递归解法

``````var binaryTreePaths = function(root) {
if(root == null) return [];
let stack = [];
let p = root;
let set = new Set();
res = [];
while(stack.length || p) {
while(p) {
stack.push(p);
p = p.left;
}
let node = stack[stack.length - 1];
// 叶子节点
if(!node.right && !node.left) {
res.push(stack.map(item => item.val).join('->'));
}
// 右孩子存在，且右孩子未被访问
if(node.right && !set.has(node.right)) {
p = node.right;
} else {
stack.pop();
}
}
return res;
};
``````

### No.3 二叉树的最大路径和

``````输入: [-10,9,20,null,null,15,7]

-10
/ \
9  20
/  \
15   7

``````

#### 递归解

``````/**
* @param {TreeNode} root
* @return {number}
*/
var maxPathSum = function(root) {
let help = (node) => {
if(node == null) return 0;
let left = Math.max(help(node.left), 0);
let right = Math.max(help(node.right), 0);
let cur = left + node.val + right;
// 如果发现某一个节点上的路径值比max还大，则更新max
if(cur > max) max = cur;
// left 和 right 永远是"一根筋"，中间不会有转折
return Math.max(left, right) + node.val;
}
let max = Number.MIN_SAFE_INTEGER;
help(root);
return max;
};
``````

## 二叉搜索树

### No.1 验证二叉搜索树

``````输入:
2
/ \
1   3

``````

#### 方法一: 中序遍历

``````/**
* @param {TreeNode} root
* @return {boolean}
*/
var isValidBST = function(root) {
let prev = null;
const help = (node) => {
if(node == null) return true;
if(!help(node.left)) return false;
if(prev !== null && prev >= node.val) return false;
// 保存当前节点，为下一个节点的遍历做准备
prev = node.val;
return help(node.right);
}
return help(root);
};
``````

#### 方法二: 限定上下界进行DFS

``````  parent
/    \
left   right
``````

...left, parent, right...

``````var isValidBST = function(root) {
const help = (node, max, min) => {
if(node == null) return true;
if(node.val >= max || node.val <= min) return false;
// 左孩子更新上界，右孩子更新下界，相当于边界要求越来越苛刻
return help(node.left, node.val, min)
&& help(node.right, max, node.val);
}
return help(root, Number.MAX_SAFE_INTEGER, Number.MIN_SAFE_INTEGER);
};
``````

``````var isValidBST = function(root) {
if(root == null) return true;
let stack = [root];
let min = Number.MIN_SAFE_INTEGER;
let max = Number.MAX_SAFE_INTEGER;
root.max = max; root.min = min;
while(stack.length) {
let node = stack.pop();
if(node.val <= node.min || node.val >= node.max)
return false;
if(node.left) {
stack.push(node.left);
// 更新上下界
node.left.max = node.val;
node.left.min = node.min;
}
if(node.right) {
stack.push(node.right);
// 更新上下界
node.right.max = node.max;
node.right.min = node.val;
}
}
return true;
};
``````

### No.2 将有序数组转换为二叉搜索树

``````给定有序数组: [-10,-3,0,5,9],

0
/ \
-3   9
/   /
-10  5
``````

#### 递归实现

``````/**
* @param {number[]} nums
* @return {TreeNode}
*/
var sortedArrayToBST = function(nums) {
let help = (start, end) => {
if(start > end) return null;
if(start === end) return new TreeNode(nums[start]);
let mid = Math.floor((start + end) / 2);
// 找出中点建立节点
let node = new TreeNode(nums[mid]);
node.left = help(start, mid - 1);
node.right = help(mid + 1, end);
return node;
}
return help(0, nums.length - 1);
};
``````

``````/**
* @param {number[]} nums
* @return {TreeNode}
*/
var sortedArrayToBST = function(nums) {
if(nums.length === 0) return null;
let mid = Math.floor(nums.length / 2);
let root = new TreeNode(nums[mid]);
// 说明: 1. index 指的是当前元素在数组中的索引
//      2. 每一个节点的值都是区间中点，那么 start 属性就是这个区间的起点，end 为其终点
root.index = mid; root.start = 0; root.end = nums.length - 1;
let stack = [root];
while(stack.length) {
let node = stack.pop();
// node 出来了，它本身包含了一个区间，[start, ..., index, ... end]
// 下面判断[node.start, node.index - 1]之间是否还有开发的余地
if(node.index - 1 >= node.start) {
let leftMid = Math.floor((node.start + node.index)/2);
let leftNode = new TreeNode(nums[leftMid]);
node.left = leftNode;
// 初始化新节点的区间起点、终点和索引
leftNode.start = node.start;
leftNode.end = node.index - 1;
leftNode.index = leftMid;
stack.push(leftNode);
}
// 中间夹着node.index, 已经有元素了，这个位置不能再开发
// 下面判断[node.index + 1, node.end]之间是否还有开发的余地
if(node.end >= node.index + 1) {
let rightMid = Math.floor((node.index + 1 + node.end)/2);
let rightNode = new TreeNode(nums[rightMid]);
node.right = rightNode;
// 初始化新节点的区间起点、终点和索引
rightNode.start = node.index + 1;
rightNode.end = node.end;
rightNode.index = rightMid;
stack.push(rightNode);
}
}
return root;
};
``````

### No.3 二叉树展开为链表

``````    1
/ \
2   5
/ \   \
3   4   6
``````

``````1
\
2
\
3
\
4
\
5
\
6
``````

#### 递归方式

``````/**
* @param {TreeNode} root
* @return {void} Do not return anything, modify root in-place instead.
*/
var flatten = function(root) {
if(root == null) return;
flatten(root.left);
flatten(root.right);
if(root.left) {
let p = root.left;
while(p.right) {
p = p.right;
}
p.right = root.right;
root.right = root.left;
root.left = null;
}
};
``````

#### 非递归方式

``````var flatten = function(root) {
if(root == null) return;
let stack = [];
let visited = new Set();
let p = root;
// 开始后序遍历
while(stack.length || p) {
while(p) {
stack.push(p);
p = p.left;
}
let node = stack[stack.length - 1];
// 如果右孩子存在，而且右孩子未被访问
if(node.right && !visited.has(node.right)) {
p = node.right;
} else {
// 以下为思路图中关键逻辑
if(node.left) {
let p = node.left;
while(p.right) {
p = p.right;
}
p.right = node.right;
node.right = node.left;
node.left = null;
}
stack.pop();
}
}
};
``````

### No.4 不同的二叉搜索树II

``````输入: 3

[
[1,null,3,2],
[3,2,null,1],
[3,1,null,null,2],
[2,1,3],
[1,null,2,null,3]
]

1         3     3      2      1
\       /     /      / \      \
3     2     1      1   3      2
/     /       \                 \
2     1         2                 3
``````

#### 递归解法

``````/**
* @param {number} n
* @return {TreeNode[]}
*/
var generateTrees = function(n) {
let help = (start, end) => {
if(start > end) return [null];
if(start === end) return [new TreeNode(start)];
let res = [];
for(let i = start; i <= end; i++) {
// 左孩子集
let leftNodes = help(start, i - 1);
// 右孩子集
let rightNodes = help(i + 1, end);
for(let j = 0; j < leftNodes.length; j++) {
for(let k = 0; k < rightNodes.length; k++) {
let root = new TreeNode(i);
root.left = leftNodes[j];
root.right = rightNodes[k];
res.push(root);
}
}
}
return res;
}
if(n == 0) return [];
return help(1, n);
};
``````

#### 非递归解法

``````var generateTrees = function(n) {
let clone = (node, offset) => {
if(node == null) return null;
let newnode = new TreeNode(node.val + offset);
newnode.left = clone(node.left, offset);
newnode.right = clone(node.right, offset);
return newnode;
}
if(n == 0) return [];
let dp = [];
dp[0] = [null];
// i 是子问题中的节点个数，子问题: [1], [1,2], [1,2,3]...逐步递增，直到[1,2,3...,n]
for(let i = 1; i <= n; i++) {
dp[i] = [];
for(let j = 1; j <= i; j++) {
// 左子树集
for(let leftNode of dp[j - 1]) {
// 右子树集
for(let rightNode of dp[i - j]) {
let node = new TreeNode(j);
// 左子树结构共享
node.left = leftNode;
// 右子树无法共享，但可以借用节点个数相同的树，每个节点增加一个偏移量
node.right = clone(rightNode, j);
dp[i].push(node);
}
}
}
}
return dp[n];
};
``````

github仓库