一、栈
leetcode题目:
//方法一:栈
var isValid = function (s) {
if (s.length % 2 === 1) return false;
const stack = [];
for (var i = 0; i < s.length; i++) {
var c = s[i];
if (c === '(' || c === '[' || c === '{') {
stack.push(c);
} else {
var top = stack[stack.length - 1];
if (
(top === '(' && c === ')') ||
(top === '[' && c === ']') ||
(top === '{' && c === '}')
) {
stack.pop();
}
else {
return false;
}
}
}
return stack.length === 0;
};
二、队列
leetcode题目:
三、链表
js中没有链表,但可以用对象object来模拟链表:
const a = {
val: 'a'
}
const b = {
val: 'b'
}
const c = {
val: 'c'
}
const d = {
val: 'd'
}
a.next = b;
b.next = c;
c.next = d;
// 遍历链表
let p = a;
while (p) {
console.log(p.val);
p = p.next;
}
//插入链表
const e = {
val: 'e'
};
c.next = e;
e.next = d;
// 刪除链表
c.next = d;
- leecode题目:203. 移除链表元素
- leecode题目:24. 两两交换链表中的节点
- leecode题目:19. 删除链表的倒数第 N 个结点
- leecode题目:面试题 02.07. 链表相交
- leecode 题目:142. 环形链表 II
总结:给链表添加一个虚拟头结点
const ret = new ListNode(0, head);
return ret;
leecode题目:
解题思路:
- 无法直接获取被删除节点的上个节点。
- 将被删除的节点转移到下个节点。
解题步骤:
- 将被删除节点的值改为下个节点的值。
- 删除下个节点。
本题时间复杂度和空间复杂度均为:O(1)
leecode题目:206. 反转链表
思路:
- 反转两个节点:将n+1的next指向n.
- 反转多个节点:双指针遍历链表,重复上述操作。
步骤:
- 双指针一前一后遍历链表。
- 反转双指针。
时间复杂度:O(n) 空间复杂度:O(1)
var reverseList = function (head) {
let p1 = null;
let p2 = head;
while (p2) {
let temp = p2.next; //提前保存p2的下一个节点
p2.next = p1; //反转
p1 = p2; //把p2的值给p1,相当于p1往前移动一位
p2 = temp; //把p2.next赋值给p2,相当于p2往前移动一位
}
return p1; //循环结束后,p2指向null,返回p1即可
};
leecode题目:2. 两数相加【较难】
解题步骤:
- 新建一个空链表。
- 遍历相加的两个链表,模拟相加操作,将个位数追加到新链表上,将十位数留到下一位去相加。
时间/空间复杂度:O(max(m,n)),其中 m 和 n 分别为两个链表的长度.
var addTwoNumbers = function (l1, l2) {
const l3 = new ListNode();
let p1 = l1;
let p2 = l2;
let p3 = l3;
let carry = 0;
while (p1 || p2) {
const v1 = p1 ? p1.val : 0;
const v2 = p2 ? p2.val : 0;
const v3 = v1 + v2 + carry;
carry = Math.floor(v3 / 10);
p3.next = new ListNode(v3 % 10);
if (p1) p1 = p1.next;
if (p2) p2 = p2.next;
p3 = p3.next;
}
if (carry) {
p3.next = new ListNode(carry);
}
return l3.next;
};
leecode题目:141. 环形链表
思路:
用一快一慢两个指针遍历链表,如果指针能够相逢,说明链表有环。时间复杂度:O(n)
空间复杂度:O(1)
var hasCycle = function (head) {
let p1 = head;
let p2 = head;
while (p1 && p2 && p2.next) {
p1 = p1.next;
p2 = p2.next.next;
if(p1===p2){
return true;
}
}
return false;
};
leecode 题目:21. 合并两个有序链表
思路:双指针
- 新建一个链表,作为返回的结果。
- 用指针遍历两个有序链表,并比较两个链表的当前节点,较小者先接入新链表,并将指针后移一步。
- 时间复杂度:O(n+m),其中 n 和 m 分别为两个链表的长度。
- 空间复杂度:O(1)
var mergeTwoLists = function (l1, l2) {
let res = new ListNode(0);
let p = res;
let p1 = l1;
let p2 = l2;
while (p1 && p2) {
if (p1.val < p2.val) {
p.next = p1;
p1 = p1.next;
} else {
p.next = p2;
p2 = p2.next;
}
p = p.next;
}
if (p1) {
p.next = p1;
}
if (p2) {
p.next = p2;
}
return res.next;
};
四、集合
- 一种无序且唯一的数据结构。
- ES6中有集合,名为set。
- 集合的常用操作:去重、判断某元素是否在集合中、求交集…
// 查看集合
const arr = [1, 1, 2, 2, 2];
const set = new Set(arr)//Set(2) {1, 2}
//查看集合的长度
console.log(set.size);//2
// 去重
const arr = [1, 1, 2, 2, 2];
const arr2 = [...new Set(arr)];
console.log(arr2); //[ 1, 2 ]
// 判断元素是否在集合中
const set = new Set(arr);
const has = set.has(3);
console.log(has); //false
//求交集
const set2 = new Set([2, 3]);
const set3 = new Set([...set].filter(item => set2.has(item)));
console.log(set3); //{ 2 }
ES6中的set:
let mySet = new Set();
mySet.add(1);
mySet.add(5);
mySet.add(5);
mySet.add('some text');
let o = { a: 1, b: 2 };
mySet.add(o);
mySet.add({ a: 1, b: 2 });
const has = mySet.has(o);
mySet.delete(5);
for(let [key, value] of mySet.entries()) console.log(key, value);
const myArr = Array.from(mySet);
const mySet2 = new Set([1,2,3,4]);
const intersection = new Set([...mySet].filter(x => mySet2.has(x)));
const difference = new Set([...mySet].filter(x => !mySet2.has(x)));
leecode题目:
var intersection = function (nums1, nums2) {
nums1 = new Set(nums1);
nums2 = new Set(nums2);
let res = [...nums1].filter(item=>nums2.has(item));
return res;
};
//时间复杂度:O(m*n)
//空间复杂度:O(m), m为去重后的数组长度
五、字典
- 与集合类似,字典也是一种存储唯一值的数据结构,但它是以键值对的形式来存储的。
- ES6中有字典,名为Map。
- 字典的常用操作:键值对的增删改查。
const m = new Map();
// 增
m.set('a', 'aa');
m.set('b', 'bb');
//查看字典m:
//Map(2) {"a" => "aa", "b" => "bb"}
//字典的长度
console.log(m.size);//2
// 删
m.delete('b');
// m.clear();
// 改
m.set('a', 'aaa');
//查
m.get('a'); //'aaa'
//判断a是否存在字典中
m.has('a'); //true
leecode题目:
var intersection = function (nums1, nums2) {
let map = new Map(),
arr = [];
nums1.forEach(n => {
map.set(n, true);
});
nums2.forEach(n => {
if (map.get(n)) {
map.delete(n);
arr.push(n);
};
});
return arr;
};
//时间复杂度:O(m+n)
//空间复杂度:O(m)
leecode题目:
var isValid = function (s) {
if (s.length % 2 === 1) return false;
const stack = [];
const map = new Map();
map.set('(',')');
map.set('{','}');
map.set('[',']');
for (var i = 0; i < s.length; i++) {
var c = s[i];
if (map.has(c)) {
stack.push(c);
} else {
var top = stack[stack.length - 1];
if (map.get(top) === c) {
stack.pop();
}
else {
return false;
}
}
}
return stack.length === 0;
};
//时间复杂度:O(n)
//空间复杂度:O(n)
leecode题目:
思路:
- 先找出所有的不包含重复字符的子串;
- 找出长度最大那个子串,返回其长度即可。
步骤:
- 用双指针维护一个滑动窗口,用来剪切子串。
- 不断移动右指针,遇到重复字符,就把左指针移动到重复字符的下一位。
- 过程中,记录所有窗口的长度,并返回最大值。
var lengthOfLongestSubstring = function (s) {
let l = 0;
let res = 0;
let map = new Map();
for (let r = 0; r < s.length; r++) {
if (map.has(s[r]) && map.get(s[r]) >= l) {
l = map.get(s[r]) + 1;
}
res = Math.max(res, r - l + 1);
map.set(s[r], r);
}
return res;
};
//时间复杂度:O(n)
//空间复杂度:O(m), m为字符串中不重复字符的个数
六、树
- 树是一种分层数据的抽象模型。
- 常见的树包括:DOM树,级联选择,树形控件。
- JS中没有树,但是可以用Object和Array来构建树。
- 树的常用操作:深度、广度优先遍历,先中后序遍历。
(一)深度、广度优先遍历
1.深度优先遍历:
js实现深度优先遍历:
const tree = {
val: 'a',
children: [
{
val: 'b',
children: [
{
val: 'd',
children: [],
},
{
val: 'e',
children: [],
}
],
},
{
val: 'c',
children: [
{
val: 'f',
children: [],
},
{
val: 'g',
children: [],
}
],
}
],
};
const dfs = (root) => {
console.log(root.val);
root.children.forEach(children=>dfs(children));
// root.children.forEach(dfs);//上面代码简写
};
dfs(tree);
结果:
a b d e c f g
2.广度优先遍历:
js实现广度优先遍历:
const tree = {
val: 'a',
children: [{
val: 'b',
children: [{
val: 'd',
children: [],
},
{
val: 'e',
children: [],
}
],
},
{
val: 'c',
children: [{
val: 'f',
children: [],
},
{
val: 'g',
children: [],
}
],
}
],
};
const bfs = (root) => {
const q = [root];
while (q.length > 0) {
const n = q.shift();
console.log(n.val);
// for (let i = 0; i < n.children.length; i++) {
// let item = n.children[i];
// q.push(item);
// }
n.children.forEach(children=>{
q.push(children);
});
}
};
bfs(tree);
输出结果:
a b c d e f g
(二)二叉树
- 树中每个节点最多只能有两个子节点。
- 在js中通常用Object来模拟二叉树
例如:
const bt = {
val: 1,
left: {
val: 2,
left: {
val: 4,
left: null,
right: null,
},
right: {
val: 5,
left: null,
right: null,
},
},
right: {
val: 3,
left: {
val: 6,
left: null,
right: null,
},
right: {
val: 7,
left: null,
right: null,
},
},
};
图示:
(三)二叉树的先序遍历
【重点】:这里前中后序遍历,其实指的就是中间节点的遍历顺序。
思路:
- 访问根节点
- 对根节点的左子树进行先序遍历
- 对根节点的右子树进行先序遍历
方法1:递归
const preorder = (root) => {
if (!root) { return; }
console.log(root.val);
preorder(root.left);
preorder(root.right);
};
preorder(bt); //1 2 4 5 3 6 7
方法2:利用栈结构
const preorder = (root) => {
if (!root) { return; }
const stack = [root];
while (stack.length) {
const n = stack.pop();
console.log(n.val);
if (n.right) stack.push(n.right);
if (n.left) stack.push(n.left);
}
};
preorder(bt); //1 2 4 5 3 6 7
(四)二叉树的中序遍历
思路:
- 对根节点的左子树进行中序遍历
- 访问根节点
- 对根节点的右子树进行中序遍历
方法1:递归
const inorder = (root) => {
if (!root) { return; }
inorder(root.left);
console.log(root.val);
inorder(root.right);
};
inorder(bt);// 4 2 5 1 6 3 7
方法2:利用栈结构
const inorder = (root) => {
if (!root) { return; }
const stack = [];
let p = root;
while (stack.length || p) {
while (p) {
stack.push(p);
p = p.left;
}
const n = stack.pop();
console.log(n.val);
p = n.right;
}
};
inorder(bt);// 4 2 5 1 6 3 7
//另一个种写法:
const inorder = (root) => {
if (!root) { return; }
const stack = [];
let p = root;
while (stack.length || p) {
if (p) {
stack.push(p);
p = p.left;
} else {
const n = stack.pop();
console.log(n.val);
p = n.right;
}
}
};
inorder(bt);// 4 2 5 1 6 3 7
(五)二叉树的后续遍历
思路:
- 对根节点的左子树进行后序遍历
- 对根节点的右子树进行后序遍历
- 访问根节点
方法1:递归
const postorder = (root) => {
if (!root) { return; }
postorder(root.left);
postorder(root.right);
console.log(root.val);
};
postorder(bt);// 4 5 2 6 7 3 1
方法2:利用栈结构
const postorder = (root) => {
if (!root) { return; }
const outputStack = [];
const stack = [root];
while (stack.length) {
const n = stack.pop();
outputStack.push(n);
if (n.left) stack.push(n.left);
if (n.right) stack.push(n.right);
}
while(outputStack.length){
const n = outputStack.pop();
console.log(n.val);
}
};
postorder(bt);// 4 5 2 6 7 3 1
七、图
图示网络结构的抽象模型,是一组由边连接的节点。
- 图可以表示任何二元关系,比如道路、航班。。。
- JS中没有图,但是可以用Object和Array来构建图。
- 图的表示法:邻接矩阵、邻接表。
- 图的常用操作:深度、广度优先遍历
(一)图的深度优先遍历
const graph = {
0: [1, 2],
1: [2],
2: [0, 3],
3: [3]
};
const res = [];
const dfs = (n) => {
console.log(n); //2 0 1 3
res.push(n);
graph[n].forEach(item => {
// 如果找不到该元素,它将返回-1
if (res.indexOf(item) === -1) {
dfs(item);
}
});
}
dfs(2);
(二)图的广度优先遍历
const graph = {
0: [1, 2],
1: [2],
2: [0, 3],
3: [3]
};
const res = [];
res.push(2);
const queue = [2];
while (queue.length > 0) {
const n = queue.shift();
console.log(n); // 2 0 3 1
graph[n].forEach(item => {
// 如果找不到该元素,它将返回-1
if (res.indexOf(item) === -1) {
queue.push(item);
res.push(item);
}
});
}
八、堆
(一)堆是什么?
(二)js实现最小堆类
class MinHeap {
constructor() {
this.heap = [];
}
getParentIndex(i) {
return Math.floor((i - 1) / 2);
}
getLeftIndex(i) {
return i * 2 + 1;
}
getRightIndex(i) {
return i * 2 + 2;
}
shiftUp(index) {
if (index === 0) return;
const parentIndex = this.getParentIndex(index);
if (this.heap[parentIndex] > this.heap[index]) {
this.swap(parentIndex, index);
this.shiftUp(parentIndex);
}
}
shiftDown(index) {
const leftIndex = this.getLeftIndex(index);
const rightIndex = this.getRightIndex(index);
if (this.heap[leftIndex] < this.heap[index]) {
this.swap(leftIndex, index);
this.shiftDown(leftIndex);
}
if (this.heap[rightIndex] < this.heap[index]) {
this.swap(rightIndex, index);
this.shiftDown(rightIndex);
}
}
swap(i1, i2) {
const temp = this.heap[i1];
this.heap[i1] = this.heap[i2];
this.heap[i2] = temp;
}
insert(value) {
this.heap.push(value);
this.shiftUp(this.heap.length - 1);
}
pop() {
this.heap[0] = this.heap.pop();
this.shiftDown(0);
}
peek() {
return this.heap[0];
}
size() {
return this.heap.length;
}
}
const h = new MinHeap();
h.insert(3);
h.insert(2);
h.insert(1);
// console.log(h); // [1, 3, 2]
h.pop();
// console.log(h); // [2, 3]
console.log(h.peek()); //2
console.log(h.size()); //2
leetcode题目:
解法1:暴力法
- 思路:先对数组进行排序,然后找出对应的索引即可。
var findKthLargest = function(nums, k) {
let res = nums.sort(function(a,b){
return a-b;
});
return res[nums.length-k];
};
解法2:利用堆结构
思路:
- 1.构建一个最小堆,并依次把数组的值插入堆中。
- 2.当堆的容量超过K,就删除堆顶。
- 3.插入结束后,堆顶就是第K个最大的元素。
var findKthLargest = function (nums, k) {
const h = new MinHeap();
nums.forEach(c => {
h.insert(c);
if (h.size() > k) {
h.pop();
}
});
return h.peek();
};
// 自己构建一个最小堆类
class MinHeap {
constructor() {
this.heap = [];
}
getParentIndex(i) {
return Math.floor((i - 1) / 2);
}
getLeftIndex(i) {
return i * 2 + 1;
}
getRightIndex(i) {
return i * 2 + 2;
}
shiftUp(index) {
if (index === 0) return;
const parentIndex = this.getParentIndex(index);
if (this.heap[parentIndex] > this.heap[index]) {
this.swap(parentIndex, index);
this.shiftUp(parentIndex);
}
}
shiftDown(index) {
const leftIndex = this.getLeftIndex(index);
const rightIndex = this.getRightIndex(index);
if (this.heap[leftIndex] < this.heap[index]) {
this.swap(leftIndex, index);
this.shiftDown(leftIndex);
}
if (this.heap[rightIndex] < this.heap[index]) {
this.swap(rightIndex, index);
this.shiftDown(rightIndex);
}
}
swap(i1, i2) {
const temp = this.heap[i1];
this.heap[i1] = this.heap[i2];
this.heap[i2] = temp;
}
insert(value) {
this.heap.push(value);
this.shiftUp(this.heap.length - 1);
}
pop() {
this.heap[0] = this.heap.pop();
this.shiftDown(0);
}
peek() {
return this.heap[0];
}
size() {
return this.heap.length;
}
}
九、排序和搜索
【注】:https://visualgo.net/此网站用来演示各种排序的动画。
(一)冒泡排序*
思路:
- 比较所有相邻的元素,如果第一个比第二个大,则交换他们。
- 一轮下来,可以保证最后一个数是最大的。
- 以此类推,执行n-1轮,就可以完成排序。
复杂度分析:
- 时间复杂度:O(n^2)
Array.prototype.bubbleSort = function () {
for (let i = 0; i < this.length - 1; i++) {
for (let j = 0; j < this.length - 1 - i; j++) {
if (this[j] > this[j + 1]) {
const temp = this[j];
this[j] = this[j + 1];
this[j + 1] = temp;
}
}
}
}
const array = [2, 5, 4, 3, 1];
array.bubbleSort();
(二)选择排序*
思路:
- 找到数组中的最小值,选中它并将其放置在第一位。
- 接着找到第二小的值,选中它并将其放置在第二位。
- 以此类推,执行n-1轮,就可以完成排序。
复杂度分析:
- 时间复杂度:O(n^2)
Array.prototype.selectionSort = function () {
for (let i = 0; i < this.length - 1; i++) {
let indexMin = i;
for (let j = i; j < this.length; j++) {
if (this[indexMin] > this[j]) {
this.swap(indexMin, j);
}
}
}
}
Array.prototype.swap = function (m, n) {
const temp = this[m];
this[m] = this[n];
this[n] = temp;
}
const array = [2, 5, 4, 3, 1];
array.selectionSort();
(三)插入排序
思路:
- 从第二个数开始往前比。
- 比他大就往后排。
- 以此类推,进行到最后一个数。
复杂度分析:
- 时间复杂度:O(n^2)
Array.prototype.insertionSort = function () {
for (let i = 1; i < this.length; i++) {
const temp = this[i];
let j = i;
while (j > 0) {
if (this[j - 1] > temp) {
this[j] = this[j - 1];
j--;
this[j] = temp;
} else {
break;
}
}
}
}
const array = [2, 5, 4, 3, 1];
array.insertionSort();
(四)归并排序
思路:
- 分:把数组分成两半,再递归地对子数组进行“分”操作,直到分成一个个单独的数。
- 合:把两个数合并为有序数组,再对有序数组进行合并,直到全部子数组合并为一个完整的数组。
具体操作:
- 新建一个空数组res,用于存放最终排序后的数组。
- 比较两个有序数组的头部,较小者出队并推入res中。
- 如果两个数组还有值,就重复第二步。
复杂度分析:
- 分的时间复杂度:O(logn)
- 合的时间复杂度:O(n)
- 所以总的时间复杂度:O(n* logn)
Array.prototype.mergeSort = function () {
const rec = (array) => {
if (array.length === 1) return array;
const mid = Math.floor(array.length / 2);
const left = array.slice(0, mid);
const right = array.slice(mid, array.length);
const orderLeft = rec(left);
const orderRight = rec(right);
const res = [];
while (orderLeft.length || orderRight.length) {
if (orderLeft.length && orderRight.length) {
res.push(orderLeft[0] < orderRight[0] ? orderLeft.shift() : orderRight.shift());
} else if (orderLeft.length) {
res.push(orderLeft.shift());
} else if (orderRight.length) {
res.push(orderRight.shift());
}
}
return res;
};
const res = rec(this);
// 把数组拷贝一份给原来的数组
res.forEach((n, i) => {
this[i] = n;
})
}
const array = [2, 5, 4, 3, 1];
array.mergeSort();
(五)快速排序*
- 第一种思路:
- 分区:从数组中任意选择一个“基准”,所有比基准小的元素放在基准前面,比基准大的元素放在基准的后面。
- 递归:递归地对基准前后的子数组进行分区。
复杂度分析:
- 递归的时间复杂度:O(logn)
- 分区操作的时间复杂度:O(n)
- 所以总的时间复杂度:O(n* logn)
代码实现:
Array.prototype.quickSort = function () {
const rec = (array) => {
if (array.length === 0 || array.length === 1) return array;
const left = [];
const right = [];
const mid = array[0];
for (let i = 1; i < array.length; i++) {
if (mid > array[i]) {
left.push(array[i]);
} else {
right.push(array[i]);
}
}
return [...rec(left), mid, ...rec(right)];
};
const res = rec(this);
// 把数组拷贝一份给原来的数组
res.forEach((n, i) => {
this[i] = n;
})
}
const array = [2, 5, 4, 3, 1];
array.quickSort();
- 第二种思路:
- 选择一个元素作为基数(通常是第一个元素),把比基数小的元素放到它左边,比基数大的元素放到它右边(相当于二分),再不断递归基数左右两边的序列。
- 视频讲解:B站
复杂度分析:
- 最好: O(n * logn),所有数均匀分布在基数的两边,此时的递归就是不断地二分左右序列。
- 最坏: O(n²),所有数都分布在基数的一边,此时划分左右序列就相当于是插入排序。
- 平均: O(n * logn)
代码实现:需加强记忆
var sortArray = function (nums) {
if (nums.length <= 1) return nums;
// 递归排序基数左右两边的序列
function quickSort(arr, left, right) {
if (left >= right) return;
let index = partition(arr, left, right);
quickSort(arr, left, index - 1);
quickSort(arr, index + 1, right);
return arr;
}
// 将小于基数的数放到基数左边,大于基数的数放到基数右边,并返回基数的位置
function partition(arr, left, right) {
// 取第一个数为基数
let temp = arr[left];
while (left < right) {
while (left < right && arr[right] >= temp) {
right--;
}
arr[left] = arr[right];
while (left < right && arr[left] < temp) {
left++;
}
arr[right] = arr[left];
}
// 修改基数的位置
arr[left] = temp;
return left;
}
quickSort(nums, 0, nums.length - 1);
}
const nums = [19, 97, 9, 17, 1, 8];
sortArray(nums);
console.log(nums); //[ 1, 8, 9, 17, 19, 97 ]
(六)顺序搜索
思路:
- 遍历数组。
- 找到跟目标值相等的元素,就返回他的下标。
- 遍历结束后,如果没有搜索到目标值,就返回-1。
复杂度分析:
- 时间复杂度:O(n)
Array.prototype.sequentialSearch = function (item) {
for (let i = 0; i < this.length; i++) {
if (item === this[i]) return i;
}
return -1;
}
const array = [2, 5, 4, 3, 1];
array.sequentialSearch(1); //4
(七)二分搜索
【注意】:二分搜索的前提是数组是排序好的。
思路:
- 从数组的中间元素开始,如果中间元素正好是目标值,则搜索结束。
- 如果目标值大于或者小于中间元素,则在大于或小于中间元素的那一半数组中搜索。
复杂度分析:
- 每一次比较都使得搜索范围缩小一半
- 时间复杂度:O(logn)
Array.prototype.binarySearch = function (item) {
let low = 0;
let high = this.length - 1;
while (low <= high) {
const mid = Math.floor((low + high) / 2);
const element = this[mid];
if (element < item) {
low = mid + 1;
} else if (element > item) {
high = mid - 1;
} else {
return mid;
}
}
return -1;
};
const res = [1, 2, 3, 4, 5].binarySearch(4);
console.log(res); //3
十、分而治之
- 分而治之是算法设计中的一种方法。
- 它将一个问题分成多个和原问题相似的小问题,递归解决小问题,再将结果合并以解决原来的问题。
- 举例:
- 1.归并排序
- 2.快速排序
- 3.二分搜索
- 4.翻转二叉树
LeetCode第 374题:
// 1.二分搜索
// 时间复杂度:O(logn)
// 空间复杂度:O(1)
/**
var guessNumber = function (n) {
let low = 1;
let high = n;
while (low <= high) {
let mid = Math.floor((low + high) / 2);
const res = guess(mid);
if (res === 1) {
low = mid + 1;
} else if (res === -1) {
high = mid - 1;
} else {
return mid;
}
}
};
*/
// 2.分而治之版二分搜索
// 时间复杂度:O(logn)
// 空间复杂度:O(logn) --- 递归
var guessNumber = function (n) {
const rec = (low, high) => {
if (low > high) return;
const mid = Math.floor((low + high) / 2);
const res = guess(mid);
if (res === 1) {
return rec(mid + 1, high);
} else if (res === -1) {
return rec(low, mid - 1);
} else {
return mid;
}
}
return rec(1, n);
};
LeetCode第 226 题:
方法1:分而治之
思路:
- 分:获取左右子树。
- 解:递归地翻转左右子树。
- 合:将翻转后的左右子树换个位置放到根节点上。 时间复杂度:O(n) 空间复杂度:O(h):h为树的高度
方法2:利用树的广度优先遍历
思路:
- 根节点先入列,然后出列,出列就 “做事”,交换它的左右子节点(左右子树)。
- 让左右子节点入列,往后,这些子节点出列,也被翻转。
- 直到队列为空,就遍历完所有的节点,翻转了所有子树。
//1.分而治之
var invertTree = function(root) {
if (root === null) {
return null;
}
const left = invertTree(root.left);
const right = invertTree(root.right);
root.left = right;
root.right = left;
return root;
};
// 方法1----另一种简单写法:
var invertTree = function(root) {
if (!root) return null;
return {
val:root.val,
left:invertTree(root.right),
right:invertTree(root.left)
}
};
// 2.二叉树的广度优先遍历---队列
var invertTree = function (root) {
if(root===null) return null;
const queue = [root];
while (queue.length > 0) {
let node = queue.shift();
[node.left, node.right] = [node.right, node.left]; // 交换左右子树
if (node.left !== null) {
queue.push(node.left);
}
if (node.right !== null) {
queue.push(node.right);
}
}
return root;
};
// 3.二叉树的深度优先遍历(先序遍历)---栈
var mirrorTree = function (root) {
if (root == null) return null;
const stack = [root];
while (stack.length > 0) {
let node = stack.pop();
if (node.left !== null) stack.push(node.left);
if (node.right !== null) stack.push(node.right);
if (node.left !== null || node.right !== null) {
let temp = node.left;
node.left = node.right;
node.right = temp;
}
}
return root;
};
LeetCode第 100 题:
方法三:分而治之 思路:
- 分:获取两个树的左子树和右子树。
- 解:递归地判断两个树的左子树是否相同,右子树是否相同。
- 合:将上述结果合并,如果根节点的值也相同,树就相同。
时间复杂度:O(n)
空间复杂度:O(n)
//1.投机取巧:直接转为字符串进行比较
/**
var isSameTree = function(p, q) {
return JSON.stringify(p)===JSON.stringify(q);
};
*/
// 2.深度优先遍历
/**
var isSameTree = function(p, q) {
if(p === null && q === null)
return true;
if(p === null || q === null)
return false;
if(p.val !== q.val)
return false;
return isSameTree(p.left, q.left) && isSameTree(p.right, q.right);
};
*/
// 3.分而治之
var isSameTree = function (p, q) {
if (p === null && q === null)
return true;
if (p === null || q === null)
return false;
const left = isSameTree(p.left,q.left);
const right = isSameTree(p.right,q.right);
if (left && right && p.val === q.val)
return true;
return false;
};
LeetCode第 101 题:
方法2:分而治之
思路:
- 转化为:判断左右子树是否镜像。
- 分解为:树1的左子树和树2的右子树是否镜像,树1的右子树和树2的左子树是否镜像。
解题步骤:
- 分:获取两个树的左子树和右子树。
- 解:递归地判断树1的左子树和树2的右子树是否镜像,树1的右子树和树2的左子树是否镜像。
- 合:如果上述都成立,且根节点的值也相同,则两个树就镜像。
时间复杂度:O(n)
空间复杂度:O(n) —最坏
// 1.正常思路
/**
const check = (p, q) => {
if (!p && !q) return true;
if (!p || !q) return false;
return p.val === q.val && check(p.left, q.right) && check(p.right, q.left);
}
var isSymmetric = function(root) {
return check(root, root);
};
*/
// 2.分而治之
var isSymmetric = function (root) {
const check = (p, q) => {
if (!p && !q) return true;
if (!p || !q) return false;
const left = check(p.left, q.right);
const right = check(p.right, q.left);
if (left && right && p.val === q.val && p && q) return true;
return false;
}
return check(root, root);
};
十一、动态规划
-
动态规划是算法设计中的一种方法。
-
他将一个问题分解为相互重叠的子问题,通过反复求解子问题,来解决原来的问题。
-
举例:
- 斐波那契数列:0 1 1 2 3 5 8 13…
- 定义子问题:F(n) = F(n-1)+F(n-2); n>=2
- 反复执行:从2循环到n,执行上述公式。
- 斐波那契数列:0 1 1 2 3 5 8 13…
-
LeetCode第70题
代码:
// 时间复杂度、空间复杂度:O(n)
// var climbStairs = function (n) {
// if (n === 1) return 1;
// const dp = [1, 1];
// for (let i = 2; i <= n; i++) {
// dp[i] = dp[i - 1] + dp[i - 2];
// }
// return dp[n];
// };
// 优化以上代码
//时间复杂度:O(n),空间复杂度:O(1)
var climbStairs = function(n) {
let dp0 = 0, dp1 = 1, sum = 1;
for (let i = 2; i <= n; ++i) {
dp0 = dp1;
dp1 = sum;
sum = dp0 + dp1;
}
return sum;
};
- LeetCode第198题
代码:
// 时间复杂度:O(n)、空间复杂度:O(n)
// var rob = function (nums) {
// if (nums.length === 0) return 0;
// const dp = [0, nums[0]];
// for (let i = 2; i <= nums.length; i++) {
// dp[i] = Math.max(dp[i - 2] + nums[i - 1], dp[i - 1]);
// }
// return dp[nums.length];
// };
// 优化以上代码
// 时间复杂度:O(n)、空间复杂度:O(1)
var rob = function (nums) {
if (nums.length === 0) return 0;
let dp0 = 0;
let dp1 = nums[0];
for (let i = 2; i <= nums.length; i++) {
const dp2 = Math.max(dp0 + nums[i - 1], dp1);
dp0 = dp1;
dp1 = dp2;
}
return dp1;
};
- LeetCode第53题
动态规划 基本思路:
f(i)=max{f(i−1)+nums[i],nums[i]}
// 方法一:动态规划
var maxSubArray = function (nums) {
for (let i = 1; i < nums.length; i++) {
if (nums[i - 1] > 0) {
nums[i] += nums[i - 1];
}
}
// console.log(nums);
return Math.max(...nums);
};
// 方法二:贪心算法
// var maxSubArray = function (nums) {
// let pre = 0, maxAns = nums[0];
// nums.forEach((x) => {
// pre = Math.max(pre + x, x);
// maxAns = Math.max(maxAns, pre);
// });
// return maxAns;
// };
- LeetCode题目
/**
// 动态规划:
// 时间复杂度、空间复杂度:O(n)
var fib = function (n) {
if (n === 0) return 0;
const dp = [0, 1]
for (let i = 2; i <= n; i++) {
dp[i] = (dp[i - 1] + dp[i - 2]) % 1000000007;
}
// console.log(dp);
return dp[dp.length - 1];
};
*/
// 优化:
// 时间复杂度:O(n),空间复杂度:O(1)
var fib = function (n) {
if (n === 0) return 0;
if (n === 1) return 1;
let dp0 = 0;
let dp1 = 1;
let dp2 = null;
for (let i = 2; i <= n; i++) {
dp2 = (dp0 + dp1) % 1000000007;
dp0 = dp1;
dp1 = dp2;
}
return dp2;
};
- 剑指 Offer 63. 股票的最大利润【经典】
//暴力法求解【必会】
var maxProfit = function (prices) {
let max = 0;
for (var i = 1; i < prices.length; i++) {
for (var j = 0; j < i; j++) {
if (prices[i] > prices[j]) {
max = Math.max(prices[i] - prices[j],max);
}
}
}
return max;
};
//动态规划解决:
var maxProfit = function (prices) {
let min = prices[0];
let dp = [0];
for (var i = 1; i < prices.length; i++) {
dp[i] = Math.max(dp[i - 1], prices[i] - min);
min = Math.min(min, prices[i]);
}
// console.log(dp);
return dp[dp.length - 1];
};
【总结】:如果子问题是独立的,则用分而治之;如果子问题是相互重叠的,则用动态规划。
十二、贪心算法
- 贪心算法是算法设计中的一种方法。
- 期盼通过每个阶段的局部最优选择,从而达到全局的最优。
- 结果并不一定是最优。
- 举例:
- 零钱兑换
- 输入:coin = [1, 2, 5], amount = 11;
- 输出:3
- 解释:11 = 5 + 5 + 1
===========================================================- 输入:coin = [1, 3, 4], amount = 6;
- 输出:3
- 解释:6 = 4 + 1 + 1(这种情况下贪心算法不是最优解,最优解是6 = 3 + 3)
- LeetCode第455题
代码:
var findContentChildren = function (g, s) {
g.sort((a, b) => a - b);
s.sort((a, b) => a - b);
let i = 0;
for (let j = 0; j < s.length; j++) {
if (s[j] >= g[i]) i++;
}
return i;
};
- LeetCode第122题
- 解题思路:
1.前提:上帝视角,知道未来的价格。
2.局部最优:见好就收,见差不动。- 解题步骤:
1.新建一个变量,用来统计总利润。
2.遍历价格数组,如果当前价格比昨天高,就在昨天买,今天卖,否则就不做交易。
3.遍历结束后,返回所有利润之和。
代码:
// 时间复杂度:O(n)
// 空间复杂度:O(1)
var maxProfit = function (prices) {
let profit = 0;
for (let i = 1; i < prices.length; i++) {
if (prices[i] > prices[i - 1]) {
profit += prices[i] - prices[i - 1];
}
}
return profit;
};
十三、回溯算法【待重刷】
- 回溯算法
回溯的本质是穷举,穷举所有可能,然后选出我们想要的答案,如果想让回溯法高效一些,可以加一些剪枝的操作,但也改不了回溯法就是穷举的本质。
- 回溯法,一般可以解决如下几种问题:
- 组合问题:N个数里面按一定规则找出k个数的集合
- 切割问题:一个字符串按一定规则有几种切割方式
- 子集问题:一个N个数的集合里有多少符合条件的子集
- 排列问题:N个数按一定规则全排列,有几种排列方式
- 棋盘问题:N皇后,解数独等等
- 如何理解回溯法
- 回溯法解决的问题都可以抽象为树形结构!
- 因为回溯法解决的都是在集合中递归查找子集,集合的大小就构成了树的宽度,递归的深度,都构成的树的深度。
- 回溯算法题解模板
function backtracking(参数) {
if (终止条件) {
存放结果;
return;
}
for (选择:本层集合中元素(树中节点孩子的数量就是集合的大小)) {
处理节点;
backtracking(路径,选择列表); // 递归
回溯,撤销处理结果
}
}
-
LeetCode第46题
解题步骤:
- 用递归模拟出所有的情况。
- 遇到包含重复元素的情况,就回溯。
- 收集所有到达递归终点的情况,并返回。
代码:
// 时间复杂度:O(n!)
// 空间复杂度:O(n)
var permute = function (nums) {
const res = [];
const backtrack = (path) => {
if (path.length === nums.length) {
res.push(path);
return;
}
nums.forEach(n => {
if (path.includes(n)) return;
backtrack(path.concat(n));
});
}
backtrack([]);
return res;
};
- LeetCode第78题
- 解题思路:
- 要求:(1)所有子集;(2)没有重复元素
- 有出路,有死路。
- 考虑使用回溯算法
- 解题步骤:
- 用递归模拟出所有的情况。
- 保证接的数字都是后面的数字。
- 收集所有到达递归终点的情况,并返回。
代码:
// 时间复杂度:O(2^n)
// 空间复杂度:O(n)
var subsets = function (nums) {
const res = [];
const backtrack = (path, length, start) => {
if (path.length === length) {
res.push(path);
return;
}
for (let i = start; i < nums.length; i++) {
backtrack(path.concat(nums[i]), length, i + 1);
}
}
for (let i = 0; i <= nums.length; i++) {
backtrack([], i, 0);
}
return res;
};