
链表
反转链表
原地单链表的反转、两个一组反转链表和K个一组反转链表
1. 简单的反转链表
反转一个单链表。【206】
/**
* @param {ListNode} head
* @return {ListNode}
*/
let reverseList = (head) => {
if (!head) return null
let pre = null, cur = head
while (cur) {
// 关键: 保存下一个节点的值
let next = cur.next
cur.next = pre
pre = cur
cur = next
}
return pre
}
2. 区间反转
反转从位置 m 到 n 的链表。请使用一趟扫描完成反转。说明: 1 ≤ m ≤ n ≤ 链表长度。【92】
/**
* @param {ListNode} head
* @param {number} m
* @param {number} n
* @return {ListNode}
*/
var reverseBetween = function(head, m, n) {
let p = dummyHead = new ListNode()
let pre, cur, front, end
p.next = head
for(let i = 0; i < m - 1; i ++) {
p = p.next
}
front = p
pre = end = p.next
cur = pre.next
for(let i = 0; i < n - m; i++) {
let next = cur.next
cur.next = pre
pre = cur
cur = next
}
front.next = pre
end.next = cur
return dummyHead.next
}
3. 两个一组翻转链表
给定一个链表,两两交换其中相邻的节点,并返回交换后的链表。不能只是单纯的改变节点内部的值,而是需要实际的进行节点交换。给定 1->2->3->4, 你应该返回 2->1->4->3.【24】
var swapPairs = function(head) {
if(head === null || head.next === null) return head
let node1 = head, node2 = head.next
node1.next = swapPairs(node2.next)
node2.next = node1
return node2
}
4. K个一组反转链表
给你一个链表,每 k 个节点一组进行翻转,请你返回翻转后的链表。k 是一个正整数,它的值小于或等于链表的长度。如果节点总数不是 k 的整数倍,那么请将最后剩余的节点保持原有顺序。你的算法只能使用常数的额外空间。你不能只是单纯的改变节点内部的值,而是需要实际的进行节点交换。【25】
递归解法:
/**
* @param {ListNode} head
* @param {number} k
* @return {ListNode}
*/
var reverseKGroup = function(head, k) {
let pre = null, cur = head
let p = head
// 下面的循环用来检查后面的元素是否能组成一组
for(let i = 0; i < k; i++) {
if(p == null) return head
p = p.next
}
for(let i = 0; i < k; i++){
let next = cur.next
cur.next = pre
pre = cur
cur = next
}
// pre为本组最后一个节点,cur为下一组的起点
head.next = reverseKGroup(cur, k)
return pre
}
环形链表:
1. 如何检测链表形成环
var hasCycle = function(head) {
let dummyHead = new ListNode()
dummyHead.next = 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
}
2. 如何找到环的起点?
给定一个链表,返回链表开始入环的第一个节点。 如果链表无环,则返回 null。
**说明:**不允许修改给定的链表。
/**
* @param {ListNode} head
* @return {ListNode}
*/
var detectCycle = function(head) {
let dummyHead = new ListNode()
dummyHead.next = head
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) {
let p = dummyHead
while(p !== slow) {
p = p.next
slow = slow.next
}
return p
}
}
return null
}
链表合并
1. 合并两个有序链表
将两个有序链表合并为一个新的有序链表并返回。新链表是通过拼接给定的两个链表的所有节点组成的。【21】
/**
* @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)
}
2. 合并K个有序链表
合并 k 个排序链表,返回合并后的排序链表。请分析和描述算法的复杂度。【23】
自上而下递归实现:
/**
* @param {ListNode[]} lists
* @return {ListNode}
*/
var mergeKLists = function(lists) {
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);
};
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) {
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);
};
// 边界情况
if(!lists || !lists.length) return null;
// 虚拟头指针集合
let dummyHeads = [];
// 初始化虚拟头指针
for(let i = 0; i < lists.length; i++) {
let node = new ListNode();
node.next = lists[i];
dummyHeads[i] = node;
}
// 自底向上进行merge
for(let size = 1; size < lists.length; size += size){
for(let i = 0; i + size < lists.length;i += 2 * size) {
dummyHeads[i].next = mergeTwoLists(dummyHeads[i].next, dummyHeads[i + size].next);
}
}
return dummyHeads[0].next;
};
求链表中间节点
1. 判断回文链表
判断一个单链表是否为回文链表。你能否用 O(n) 时间复杂度和 O(1) 空间复杂度解决此题?【234】
/**
* @param {ListNode} head
* @return {boolean}
*/
var isPalindrome = function(head) {
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()
dummyHead.next = head
// 找中点, 黄金模板
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
}
栈和队列
栈&递归
1. 有效括号
给定一个只包括 '(',')','{','}','[',']' 的字符串,判断字符串是否有效。有效字符串需满足:左括号必须用相同类型的右括号闭合。 左括号必须以正确的顺序闭合。 注意空字符串可被认为是有效字符串。【20】
/**
* @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
}
2. 多维数组 flatten
将多维数组转化为一维数组。
/**
* @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
reduce方法:
let flatten = (nestedList) => nestedList.reduce((pre, cur) => pre.concat(Array.isArray(cur) ? flatten(cur): cur), [])
二叉树层序遍历
1. 普通的层次遍历
给定一个二叉树,返回其按层次遍历的节点值。 (即逐层地,从左到右访问所有节点)。【102】
/**
* @param {TreeNode} root
* @return {number[][]}
*/
var levelOrder = function(root) {
if(!root) return []
let queue = []
let res = []
let level = 0
queue.push(root)
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
}
2. 二叉树的锯齿形层次遍历
给定一个二叉树,返回其节点值的锯齿形层次遍历。(即先从左往右,再从右往左进行下一层遍历,以此类推,层与层之间交替进行)。【103】
var zigzagLevelOrder = function(root) {
if(!root) return []
let queue = []
let res = []
let level = 0
queue.push(root)
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
};
3. 二叉树的右视图
给定一棵二叉树,想象自己站在它的右侧,按照从顶部到底部的顺序,返回从右侧所能看到的节点值。【199】
使用广度优先的思想,即层序遍历:
/**
* @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遍历
1. 完全平方数
给定正整数 n,找到若干个完全平方数(比如 1, 4, 9, 16, ...)使得它们的和等于 n。你需要让组成和的完全平方数的个数最少。【279】
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)
}
}
}
}
2. 单词接龙
给定两个单词(beginWord 和 endWord)和一个字典,找到从 beginWord 到 endWord 的最短转换序列的长度。转换需遵循如下规则:
- 每次转换只能改变一个字母。
- 转换过程中的中间单词必须是字典中的单词。
说明:
- 如果不存在这样的转换序列,返回 0。
- 所有单词具有相同的长度。
- 所有单词只由小写字母组成。
- 字典中不存在重复的单词。
- 你可以假设 beginWord 和 endWord 是非空的,且二者不相同。
【127】
/**
* @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;
};
优先队列
1. 实现一个最大堆
// 以最大堆为例来实现一波
/**
* @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;
}
// 增加元素
add(value) {
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;
}
}
}
2. 实现优先队列
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();
}
return this.maxHeap.add(e);
}
dequeue() {
if (this.getSize() === 0) return null;
return this.maxHeap.extractMax();
}
}
优先队列应用
1. 前K个高频元素
给定一个非空的整数数组,返回其中出现频率前 k 高的元素。
说明:
- 你可以假设给定的 k 总是合理的,且 1 ≤ k ≤ 数组中不相同的元素的个数。
- 你的算法的时间复杂度必须优于 O(n log n) , n 是数组的大小。
【347】
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;
};
2. 合并 K 个排序链表
合并 k 个排序链表,返回合并后的排序链表。请分析和描述算法的复杂度。【23】
/**
* @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);
}
return dummyHead.next;
};
双端队列及应用
1. 滑动窗口最大值
给定一个数组 nums,有一个大小为 k 的滑动窗口从数组的最左侧移动到数组的最右侧。你只可以看到在滑动窗口内的 k 个数字。滑动窗口每次只向右移动一位。返回滑动窗口中的最大值。要求: 时间复杂度应为线性。【239】
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;
};
栈和队列的相互实现
1. 栈实现队列
使用栈实现队列的下列操作:push(x) -- 将一个元素放入队列的尾部。 pop() -- 从队列首部移除元素。 peek() -- 返回队列首部的元素。 empty() -- 返回队列是否为空。【232】
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;
};
2. 队列实现栈【225】
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. 前序遍历【144】
递归方法:
/**
* @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;
};
2. 中序遍历【94】
递归方法:
/**
* @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;
};
3. 后序遍历【145】
递归方法:
/**
* @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;
visited.add(node.right);
} else {
res.push(node.val);
stack.pop();
}
}
return res;
};
最大/最小深度
1. 最大深度
二叉树的深度为根节点到最远叶子节点的最长路径上的节点数。【104】
递归方法:
/**
* @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;
};
2. 最小深度【111】
递归方法:
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;
};
对称二叉树
给定一个二叉树,检查它是否是镜像对称的。【101】
递归方法:
/**
* @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)即最近公共祖先问题。“对于有根树 T 的两个结点 p、q,最近公共祖先表示为一个结点 x,满足 x 是 p、q 的祖先且 x 的深度尽可能大(一个节点也可以是它自己的祖先)。” 【236】
祖先节点集合法:
/**
* @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) {
set.add(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;
};
二叉搜索树的最近公共祖先 【235】
/**
* @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;
}
};
二叉树中的路径问题
1. 二叉树的直径【543】
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;
};
2. 二叉树的所有路径
给定一个二叉树,返回所有从根节点到叶子节点的路径。 【257】
递归方法:
/**
* @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;
set.add(node.right);
} else {
stack.pop();
}
}
return res;
};
3. 二叉树的最大路径和 【124】
递归方法:
/**
* @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;
};
二叉搜索树
1. 验证二叉搜索树
二叉搜索树具有如下特征:节点的左子树只包含小于当前节点的数。 节点的右子树只包含大于当前节点的数。 所有左子树和右子树自身必须也是二叉搜索树。【98】
中序遍历方法:
/**
* @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:
递归:
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);
};
2. 将有序数组转换为二叉搜索树
将一个按照升序排列的有序数组,转换为一棵高度平衡二叉搜索树。一个高度平衡二叉树是指一个二叉树每个节点 的左右两个子树的高度差的绝对值不超过 1。【108】
/**
* @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);
};
3. 二叉树展开为链表
给定一个二叉(搜索)树,原地将它展开为链表。【114】
/**
* @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
}
};
4. 不同的二叉搜索树II
给定一个整数 n,生成所有由 1 ... n 为节点所组成的二叉搜索树。【95】
递归方法:
/**
* @param {number} n
* @return {TreeNode[]}
*/
var generateTrees = function(n) {
if(!n) return []
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
}
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];
};
这篇博客详细探讨了前端开发中常见的数据结构和算法问题,包括链表的反转、环形链表检测、链表合并、求链表中间节点、栈和队列的应用以及二叉树的各种遍历和操作。通过递归和广度优先搜索等方法解决实际问题,如合并排序链表、判断回文链表、实现最大堆和优先队列等。
969

被折叠的 条评论
为什么被折叠?



