If the path be beautiful, let us not ask where it leads.
fe-sadhu / data-structures-and-algorithms Goto Github PK
View Code? Open in Web Editor NEW🤷🏿♂️
🤷🏿♂️
If the path be beautiful, let us not ask where it leads.
思路:
可以维护一个虚拟头节点指向需要返回的链表。
因为每两个数的相加要关心上两个数相加和是否大于等于 10 ,所以可以维护一个存储进数的变量,每次两数相加再加上进数就是我们想要的和。
题目给了俩链表,我们可以维护俩指针,在迭代中指针同步前进,每次步进计算相加值,若和超过 10 ,记录超出数值且把进数变量置 1,否则置进数为 0,然后以(超出数值 + 进数)的和作为 Value 创建新的节点,再让虚拟头节点指向新节点并同步往前步进即可。
最后注意迭代结束处理进数即可。
实现:
/**
* @param {ListNode} l1
* @param {ListNode} l2
* @return {ListNode}
*/
var addTwoNumbers = function(l1, l2) {
const dummyHead = new ListNode(null);
let currentPoint = dummyHead;
let l1Point = l1;
let l2Point = l2;
let prevPlus = 0;
while(l1Point || l2Point) {
l1Point = l1Point || new ListNode(0);
l2Point = l2Point || new ListNode(0);
const l1Val = l1Point.val;
const l2Val = l2Point.val;
let sum = l1Val + l2Val + prevPlus;
prevPlus = 0;
if (sum >= 10) {
sum = sum - 10;
prevPlus = 1;
}
currentPoint.next = new ListNode(sum);
currentPoint = currentPoint.next;
l1Point = l1Point.next;
l2Point = l2Point.next;
}
prevPlus && (currentPoint.next = new ListNode(prevPlus));
return dummyHead.next;
};
// 时间复杂度: O(n) n 为 l1、l2 的更长度
// 空间复杂度: 若返回值算复杂度的话 O(n)
思路一:
遍历 haystack,对每个元素后 needle 的长度位筛选出来与 needle 作匹配。
代码实现:
var strStr = function(haystack, needle) {
if(!needle) return 0;
let hayS = 0,
len = needle.length;
while(hayS < haystack.length) {
if (haystack.substr(hayS, len) === needle) {
return hayS;
}
hayS++;
}
return -1;
};
时间复杂度: O(n^2)
空间复杂度: O(1)
思路一:
关键点就在于 BST 的性质,对于左子树的所有节点,max 值就是我,对于右子树的所有节点,最小值就是我。
写个递归函数:
定义: 接收一个节点、节点应该大于的最小值、节点应该小于的最大值,返回验证二叉搜索树的结果。
出口: 空节点的时,就是 BST; 或者不满足 BST 的性质。
代码实现:
var isValidBST = function(root) {
function helper(root, min, max) {
if (!root) return true;
if(root.val <= min || root.val >= max) return false;
return helper(root.left, min, root.val) && helper(root.right, root.val, max)
}
return helper(root, -Infinity, Infinity);
};
时间复杂度 : O(n),其中 n 为二叉树的节点个数。在递归调用的时候二叉树的每个节点最多被访问一次。
空间复杂度 : O(n),其中 n 为二叉树的节点个数。递归函数在递归过程中需要为每一层递归函数分配栈空间,所以这里需要额外的空间且该空间取决于递归的深度,即二叉树的高度。最坏情况则是 O(n),一条链。
思路二:中序遍历
由于 BST 的性质,中序后是升序的数值,所以可以边中序遍历,边比较上一个的值。
代码实现:
var isValidBST = function(root) {
let stack = [];
let pre = -Infinity;
while(stack.length || root!==null){
while(root!==null){
stack.push(root);
root = root.left
};
root = stack.pop();
if(root.val<=pre){
return false;
}
pre = root.val;
root = root.right;
}
return true;
};
时间复杂度: O(n), 一个节点走一次
空间复杂度: O(n)
可以试试递归写中序。
代码实现:
/**
* Definition for a binary tree node.
* function TreeNode(val, left, right) {
* this.val = (val===undefined ? 0 : val)
* this.left = (left===undefined ? null : left)
* this.right = (right===undefined ? null : right)
* }
*/
/**
* @param {TreeNode} root
* @return {boolean}
*/
var isValidBST = function(root) {
if (!root) return false;
let prev = -Infinity;
let isValid = true;
function dfs(node) {
if(!node) return;
if (!isValid) return;
dfs(node.left);
if (node.val > prev) {
prev = node.val;
} else {
isValid = false;
}
dfs(node.right);
}
dfs(root);
return isValid;
};
思路
把待删除节点的 val 改为后一个节点 val,把待删除节点的 next 执行下下个节点。
代码实现:
var deleteNode = function(node) {
node.val = node.next.val;
node.next = node.next.next;
};
时间复杂度: O(1);
空间复杂度: O(1);
思路:
自然层序遍历是第一反应,也能解题,只不过不符合题目进阶要求。
二叉树是天然的递归结构,题目的进阶表示可以用递归来解,我们试试递归。
递归函数定义:输入一个根节点,返回填充好每个节点下一个右侧节点指针的根节点。
出口:输入根节点为空时返回 null。
有了定义和出口,我们就可以先把关注点放在当前根节点,当前根节点的逻辑自然简单:「左孩子.next = 右孩子」。
意味着我们只要把填充好指针的左子树根节点的 next 指向填充好指针的右子树根节点就行。
但这样还没有解决问题,我们只是分别把左子树和右子树分别给填充好了,但联系左右子树之间的指针还没做处理,最后我们再对这部分做下处理就好了,也很简单: 「左孩子.right.next = 右孩子.left 」。
代码实现:
var connect = function(root) {
if (!root) return null;
let l = connect(root.left);
let r = connect(root.right);
while(l) {
l.next = r;
l = l.right;
r = r.left;
}
return root;
};
思路一:
核心就是前一个节点的 next 指向后一个节点即删除了本节点。
一次遍历找出链表长度,一次遍历做删除目标节点操作。
代码实现:
var removeNthFromEnd = function(head, n) {
const dummyHead = new ListNode(null, head);
const len = getLengthByHead(head);
if (len - n <= 0) { // 第一个节点
return dummyHead.next.next;
}
let a = dummyHead.next;
let count = 0;
while(a) {
if (len - count === n + 1) { // 找到前一个节点
a.next = a.next.next;
break;
}
count++;
a = a.next;
}
return dummyHead.next;
};
function getLengthByHead(head) {
let len = 0;
while(head) {
len++;
head = head.next;
}
return len;
}
时间复杂度: O(L);
空间复杂度: O(1);
思路二: 快慢指针 最优
可以画画图,快指针比慢指针先走 n 步,当一次遍历中,快指针指向 null 时,慢指针就指向待删除节点。
引入 dummy head 方法,方便删除操作。
代码实现:
var removeNthFromEnd = function(head, n) {
let ret = new ListNode(0, head),
slow = fast = ret;
while(n--) fast = fast.next;
if(!fast) return ret.next;
while (fast.next) {
fast = fast.next;
slow = slow.next
};
slow.next = slow.next.next;
return ret.next;
};
时间复杂度: O(n);
空间复杂度: O(1);
思路三: 栈
利用栈先进后出的特性,一次遍历全部入栈,然后出栈 n 次。找到待删除节点的前一个节点即可进行操作。代码就不演示了。
思路:
思路和 105. 从前序与中序遍历序列构造二叉树 一样,都是找到左子树和右子树的中、后序节点,然后递归。
后序遍历的值的结果 === [[左子树的前序遍历结果], [右子树的前序遍历结果],根节点]
中序遍历的值的结果 === [[左子树的中序遍历结果],根节点,[右子树的中序遍历结果]]
我们方法的定义是,输入一个二叉树的后序遍历结果、中序遍历结果,返回构建好的二叉树的根节点。
所以我们可以先根据 左子树的后序/中序遍历结果 求出左子树,同样再求出右子树,最终拼接上根节点就是我们想要的结果了。
代码实现:
var buildTree = function(inorder, postorder) {
if(!inorder.length) return null;
const root = postorder[postorder.length -1];
const rootIndexInInorder = inorder.findIndex(i => i === root);
const leftTreeInorder = inorder.slice(0, rootIndexInInorder);
const rightTreeInorder = inorder.slice(rootIndexInInorder + 1);
const leftTreePostorder = postorder.slice(0, leftTreeInorder.length);
const rightTreePostorder = postorder.slice(leftTreeInorder.length, -1);
const node = new TreeNode(root);
node.left = buildTree(leftTreeInorder, leftTreePostorder);
node.right = buildTree(rightTreeInorder, rightTreePostorder);
return node;
};
思路:
借助队列,在每一层的末尾加上特殊标志位,每遍历一个节点就把其左孩子右孩子加入队列。
代码实现:
var levelOrder = function(root) {
if (!root) return [];
let res = [],
item = [];
let specil = 'specil';
let queue = [root, specil],
node;
while((node = queue.shift()) !== undefined) {
if (node === specil) {
res.push(item);
item = [];
if (!queue.length) return res;
queue.push(specil);
continue;
}
item.push(node.val);
node.left && queue.push(node.left);
node.right && queue.push(node.right);
}
};
时间复杂度: O(n)
空间复杂度: O(n)
思路一
维护一个映射表,此处就用 Set ,每次指针移动,判断表内有没有该节点,无则进入表,有则返回该节点。
代码实现:
var detectCycle = function(head) {
const visited = new Set();
while (head !== null) {
if (visited.has(head)) {
return head;
}
visited.add(head);
head = head.next;
}
return null;
};
时间复杂度: O(n)
空间复杂度: O(n)
思路二:
其实链表是一个环的话,我们不要被双指针的思维给禁锢了,其实可以很简单的用一个指针去走,每走一个节点,给节点打个标记后继续走,走着走着遇到有标记的一个节点了不就是入口第一个节点? 如果没遇到标记的节点就代表没环嘛。
代码实现:
var detectCycle = function(head) {
while(head) {
if (head.flag) {
return head
} else {
head.flag = true;
head = head.next
}
}
return null
};
时间复杂度: O(n)
空间复杂度: O(1)
思路三
发现数学公式,还是快慢指针,从第一个相遇点起,快指针从头开始走,再次相遇则是入口点。 怎么找出这规律的可以去力扣看官方题解,此处不叙述了,个人认为复杂化了。
思路一:递归(DFS)
二叉树都是典型的递归结构,我们可以每条 road 从根到叶走一遍,记录最高深度。
递归定义: f(root),返回这个根节点的二叉树的最大深度
出口: root 不存在的时候,返回 0。
首先当 root 存在的时候,那么证明此时二叉树的深度至少为 1 。1 + Math.max(f(root.left), f(root.right))
代码实现:
var maxDepth = function(root) {
if(!root) {
return 0;
}
return 1 + Math.max(maxDepth(root.left), maxDepth(root.right));
};
时间复杂度: O(n), n 为二叉树节点的个数,每个节点在递归中只被遍历一次。
空间复杂度: O(height), height 表示二叉树的高度。递归函数需要栈空间,而栈空间取决于递归的深度,因此空间复杂度等价于二叉树的高度。
思路二: BFS
一层一层地遍历二叉树,遍历到最后一层时,层数就是最大深度。
代码实现:
var maxDepth = function(root) {
const flag = 'special';
const queue = [root, flag]
let node = null;
let depth = 0;
while ((node = queue.shift())) {
if (node === flag) {
depth++;
if (!queue.length) {
return depth;
}
queue.push(flag);
continue;
}
node.left && queue.push(node.left)
node.right && queue.push(node.right);
}
return depth;
};
时间复杂度: O(n), n = 节点数 + flag 数量
空间复杂度: O(n)
思路一:
有序的前提下,既然要去除重复项,且原地修改数组, JS 的话应该首先会想到 replace
方法。那么可以在一轮遍历中,遇到后一个元素与前一个元素相同,则直接 replace 掉后一个元素,动态维护数组长度和索引即可。
代码实现:
var removeDuplicates = function(nums) {
let len = nums.length;
for (let i = 0; i < len;) {
if (i + 1 === len) break;
let cur = nums[i];
let next = nums[i + 1];
if (cur === next) {
nums.splice(i + 1, 1);
len--;
continue;
}
i++
}
return nums.length;
};
时间复杂度: 最好情况一个重复都没有 O(n), n 为数组长度。 最坏情况每个重复,O(n * splice 复杂度)
空间复杂度: O(1)
思路二:
如果不让你用 replace 方法咋办? (注意此题可以单纯返回数组长度)
不用 replace 方法杂改变数组自身呢? 动态改变其元素。 (如:arr[2] = 'xxx';)
那么我们就可以维护一个慢指针来表示该慢指针之前的所有元素就是去重后的元素,至于往后找重复项的任务就交给一个快指针来做。
代码实现:
var removeDuplicates = function(nums) {
if(!nums.length) return 0;
let slow = 0;
let fast = 1;
const len = nums.length;
while(fast < len) {
if (nums[fast] !== nums[slow]) {
slow++;
nums[slow] = nums[fast];
}
fast++;
}
return slow + 1;
}
时间复杂度: O(n),n 为数组长度
空间复杂度: O(1)
思路一: 栈
利用栈先进后出的特性,先在一遍迭代中存每个 node 到栈里,再迭代栈,依次构建新链表。
代码实现:
var reverseList = function(head) {
const stack = [];
while(head) {
stack.push(head);
head = head.next;
}
const dummy = new ListNode(null);
let helper = dummy;
while(stack.length) {
helper.next = stack.pop();
helper = helper.next;
}
helper.next = null; // 记得赋 null,不然循环引用
return dummy.next;
let helper = null;
while (head) {
const next = head.next;
head.next = helper;
helper = head;
head = next;
}
return helper;
};
时间复杂度: O(n);
空间复杂度: O(n);
思路二:递归
定义: reverseList 的返回值就是反转后的链表的头节点。
出口: 当 reverseList 的参数传入的节点指向 null 时,证明链表只有一个节点,反转与否都是它,直接返回这个节点即可。
代码实现:
var reverseList = function(head) {
if (!head || !head.next) {
return head;
}
const reversed = reverseList(head.next);
head.next.next = head;
head.next = null; // 记得赋 null,不然循环引用
return reversed;
};
时间复杂度: O(n) n 取决于递归的层数
空间复杂度: O(n);
思路三:
一次遍历中动态修改原本的链表以及构建新链表。
代码实现:
var reverseList = function(head) {
let helper = null;
while (head) {
const next = head.next; // 缓存旧链表的下一个节点
head.next = helper; // 当前节点指向新链表的头节点
helper = head; // 改变新链表的头节点
head = next; // 改变旧链表为下一个节点
}
return helper; // 返回新链表
};
时间复杂度: O(n);
空间复杂度: O(1);
思路:
核心就在于分隔。
如果这题,你想直接在原链表上修改,你会步入 trap,问题复杂度增加很多。
但是若你及时止步,想想是否遍历一遍链表,把 < x 的节点和 >= x 的节点分别收集在两条不同的链表中,然后把 < x 的链表尾节点连上 >= x 链表的头节点就行了?
代码实现中,small 为 < x 的节点链表,large 为 >= x 的节点链表
实现:
var partition = function(head, x) {
if (!head) return head;
const dummySmallHead = new ListNode(0);
const dummyLargeHead = new ListNode(0);
let small = dummySmallHead;
let large = dummyLargeHead;
while(head) {
if (head.val < x) {
small.next = head;
small = small.next;
} else {
large.next = head;
large = large.next;
}
head = head.next;
}
small.next = dummyLargeHead.next;
large.next = null; // 断环
return dummySmallHead.next;
};
// 时间复杂度: O(n) 只遍历了一遍链表
// 空间复杂度: O(1) small、large 链表的节点都是复用原链表的,无创建额外空间
思路:
利用栈的**,旧数出栈,新数进栈。 但是题目要求不能要额外空间,出栈进栈的操作就由数学方法来代替:
出栈: old % 10
进栈: new * 10 + (old % 10)
代码实现:
var reverse = function(x) {
let rev = 0;
while (x !== 0) {
const digit = x % 10;
x = ~~(x / 10); // 双重位运算取反,因为位运算的结果是整数,这里的作用就是去掉小数
rev = rev * 10 + digit;
if (rev < Math.pow(-2, 31) || rev > Math.pow(2, 31) - 1) {
return 0;
}
}
return rev;
};
时间复杂度: O(logx) x 的十位数量
空间复杂度: O(1)
其余思路: 转换成字符串后双指针翻转,调用 API ,用栈 or 数组实现等。
思路:
动态维护外观值,下一次的外观值的构建依赖于前一次外观值。构建关系主要是: 遍历上一次的外观值,若遇到重复元素,动态记录重复数量,若前后不重复了,更新本次外观值、重复数量,继续遍历。
代码实现:
var countAndSay = function(n) {
if (n < 1 || n > 30) return;
let prev = '1';
while(--n) {
let res = '';
let repeatTime = 1;
for (let i = 0; i < prev.length; i++) {
if (!prev[i + 1] || prev[i + 1] !== prev[i]) {
res += repeatTime + prev[i];
repeatTime = 1;
} else {
repeatTime++;
}
}
prev = res;
}
return prev;
};
时间复杂度: O(n^2)
空间复杂度: O(1)
思路一:
到达 Left 位置的节点,一定是反转过后的那段链表中最后一个节点。 (1)
并且随着迭代的过程,那段待反转链表中的节点越靠后,反转后越往前。 (2)
所以我们可以在一次迭代中动态维护这个链表,在迭代过程中相对地保证迭代的迄今为止得到的就是反转完成后的链表。
设置三个指针:
pre -> 永远是 left 位置的前一个节点
cur -> 永远反转过后的那段链表中最后一个节点,也就是未迭代前的 left 节点
next -> 相对的 cur.next 节点
迭代中的关系就遵循三步:
cur.next = next.next; // 最后一个节点动态指向下下个节点。(因为 (1))
next.next =pre.next; // 下个节点指向 pre.next 节点 (因为 (2))
pre.next = next; // 因为 (2)
可以在草稿纸上演练一下。
代码实现:
var reverseBetween = function(head, left, right) {
const dummy = new ListNode(-1);
dummy.next = head;
let pre = dummy;
for (let i = 0; i < left - 1; ++i) {
pre = pre.next;
}
let cur = pre.next;
for (let i = 0; i < right - left; ++i) {
const next = cur.next;
cur.next = next.next;
next.next = pre.next;
pre.next = next;
}
return dummy.next;
};
时间复杂度: O(n)
空间复杂度: O(1)
思路二:
挑选出待反转链表,将链表交由 #38 的解法来处理,处理好后拼接链表就完事。
代码实现:
var reverseBetween = function(head, left, right) {
// 因为头节点有可能发生变化,使用虚拟头节点可以避免复杂的分类讨论
const dummyNode = new ListNode(-1);
dummyNode.next = head;
let pre = dummyNode;
// 从虚拟头节点走 left - 1 步,来到 left 节点的前一个节点
for (let i = 0; i < left - 1; i++) {
pre = pre.next;
}
// 从 pre 再走 right - left + 1 步,来到 right 节点
let rightNode = pre;
for (let i = 0; i < right - left + 1; i++) {
rightNode = rightNode.next;
}
//切断出一个子链表(截取链表)
let leftNode = pre.next;
let curr = rightNode.next;
// 切断链接
pre.next = null;
rightNode.next = null;
// 反转链表的子区间
reverseLinkedList(leftNode);
// 接回到原来的链表中
pre.next = rightNode;
leftNode.next = curr;
return dummyNode.next;
};
const reverseLinkedList = (head) => {
let pre = null;
let cur = head;
while (cur) {
const next = cur.next;
cur.next = pre;
pre = cur;
cur = next;
}
};
时间复杂度: O(n)
空间复杂度: O(1)
思路一:
一层遍历,从最后一位开始 + 1 ,如果结果为 10,则表示需要进位,后一位数就应该 + 1,以此循环。 如果计算到不需要进位了,后续的值不会变,直接 return 就好。如果整个数组的元素一直在进位,则把处理过后的最终数组 unshift 一个 1 表示最高位即可。
代码实现:
var plusOne = function(digits) {
let len = digits.length - 1;
while(len >= 0) {
digits[len]++;
if (digits[len] >= 10) {
digits[len] = 0;
} else {
return digits;
}
len--;
}
digits.unshift(1)
return digits;
};
时间复杂度: O(n) 最好的情况是 O(1)
空间复杂度: O(1)
思路二:
利用 js 语言特性,转换成数字++ 后再转换成数组。可以利用字符串达到这一效果。
代码实现:
var plusOne = function(digits) {
return String((Number(digits.join('')) + 1)).split('');
};
虽然只有一行代码就可以实现,但是如果 digits.join('')
数字字符串的数字大于 js 中最大安全整数(Number.MAX_SAFE_INTEGER)的话,Number(xxx)
/+'xxx'
/parseInt('xxx', 10)
等方法无法正确转换成数字。了解更多
思路:
直看看不出来的话可以把图像例子画成二维数组来分析比较直观。
最后一行代表 l1,第一行代表 f1,倒数第二行代表 l2, 依次类推...
可以发现,其实 (l1 1 表示倒数第一行第一个数):
l1 1 -> f1 1,l2 1 -> f2 1, l3 1 -> f3 1 ...
l1 2 -> f2 1, l2 2 -> f2 2, l3 2 -> f2 3 ...
...
找到规律后,就很简单了,动态维护这个数组,从后往前遍历,在一层遍历中,动态改变当前 item 往对应元素中符合规律地加值。
代码实现:
var rotate = function(matrix) {
for (let i = matrix.length - 1; i >= 0; i--) {
let j = 0;
while(j < matrix.length) {
const item = matrix[i].shift();
matrix[j].push(item);
j++
}
}
};
时间复杂度: O(n^2)
空间复杂度: O(1)
其余思路还有"翻转"等
思路一: 双指针
由于链表不好定义尾指针,所以可以先把链表转换成数组,再对数组使用头尾双指针,依次判断是否相等。
代码实现:
var isPalindrome = function(head) {
const vals = [];
while (head !== null) {
vals.push(head.val);
head = head.next;
}
for (let i = 0, j = vals.length - 1; i < j; ++i, --j) {
if (vals[i] !== vals[j]) {
return false;
}
}
return true;
};
时间复杂度: O(n);
空间复杂度: O(n);
思路二: 快慢指针
为了使空间复杂度为 O(1),我们可以定义快慢指针,都从起点出发,一轮迭代中,慢指针一次走一步,快指针走两步,当快指针到达末尾时,慢指针指向中间节点。
以中间节点为分隔,反转后半部分链表,再与前半部分链表在一轮循环中一个节点一个节点对比。
如果是奇数队列,则中间节点归属前半部分。
代码实现
const reverseList = (head) => {
let prev = null;
let curr = head;
while (curr !== null) {
let nextTemp = curr.next;
curr.next = prev;
prev = curr;
curr = nextTemp;
}
return prev;
}
const endOfFirstHalf = (head) => {
let fast = head;
let slow = head;
while (fast.next !== null && fast.next.next !== null) {
fast = fast.next.next;
slow = slow.next;
}
return slow;
}
var isPalindrome = function(head) {
if (head == null) return true;
// 找到前半部分链表的尾节点并反转后半部分链表
const firstHalfEnd = endOfFirstHalf(head);
const secondHalfStart = reverseList(firstHalfEnd.next);
// 判断是否回文
let p1 = head;
let p2 = secondHalfStart;
let result = true;
while (result && p2 != null) {
if (p1.val != p2.val) result = false;
p1 = p1.next;
p2 = p2.next;
}
// 还原链表并返回结果
firstHalfEnd.next = reverseList(secondHalfStart);
return result;
};
时间复杂度: O(n)
空间复杂度: O(1)
思路:
遍历一遍链表,维护一个指针指向当前节点的前一个节点,如果遇到当前节点的值与 val 相同,就让指针指向当前节点的下一个节点,继续往下走。
代码实现:
var removeElements = function(head, val) {;
const dummyHead = new ListNode(-1, head);
let prev = dummyHead;
while(head) {
if (head.val === val) {
prev.next = head.next;
} else {
prev = head;
}
head = head.next;
}
return dummyHead.next;
};
时间复杂度: O(n)
空间复杂度: O(1)
思路:
设置快慢指针,慢指针指向头节点,快指针指向头节点.next
当慢指针.val === 快指针.val 时,快指针移动,同时改变慢指针指向,直到快指针在非重复的节点位置。
否则,快慢指针一起步进。
代码实现:
var deleteDuplicates = function(head) {
if (!head || !head.next) return head;
let slow = head;
let fast = head.next;
while(fast) {
if (slow.val === fast.val) {
slow.next = fast.next;
} else {
slow = slow.next;
}
fast = fast.next;
}
return head;
};
// 时间复杂度: O(n) n 为链表长度
// 空间复杂度: O(1)
思路一:
遍历 k 次,每轮循环一边 pop
,一边 unshift
。 很简单,不放代码了,可惜这种方式力扣会超时。
思路二:
找出要移到头部的 k
个元素以及要移到尾部的 length - k
个元素,然后组合成一个新数组。
代码实现:
const len = nums.length
const left = nums.slice(-k);
const right = nums.slice(0, len - k);
return [...left, ...right];
时间复杂度: slice 方法的时间复杂度
空间复杂度: O(n)
思路三:
先反转整个数组,然后反转前 k
个元素,再反转尾部元素。
这个方法需要注意的就是要处理 k
输入比 length
多的情况,这种情况下其实按题意,移动 k
次的结果和移动 k - length
次的结果是一样的。
代码实现:
var rotate = function(nums, k) {
k %= nums.length;
reverse(nums, 0, nums.length - 1);
reverse(nums, 0, k - 1);
reverse(nums, k, nums.length - 1);
function reverse (nums, start, end) {
while(start < end) {
const tmp = nums[start];
nums[start] = nums[end];
nums[end] = tmp;
start++;
end--;
}
}
return nums;
};
时间复杂度: O(n)
空间复杂度: O(1)
思路一: 归并(双指针)
定义两个指针 first, second:first 指向 l1 链表的头节点,second 指向 l2 链表的头节点。
按题目要求的升序,动态比较两个指针的 val 大小,让新链表的尾节点指向比较结果较小的节点,然后较小的那条链表的头节点动态往后挪,直至一个链表挪成空,然后新链表接上非空的链表就行。
代码实现:
var mergeTwoLists = function(l1, l2) {
const dummy = new ListNode(null);
let helper = dummy;
while(l1 && l2) {
if (l1.val < l2.val) {
helper.next = l1;
l1 = l1.next;
} else {
helper.next = l2;
l2 = l2.next;
}
helper = helper.next;
}
l1 && (helper.next = l1);
l2 && (helper.next = l2);
return dummy.next;
};
时间复杂度: O(n) n = Math.min(L1, L2)
空间复杂度: O(1) 最长开辟内存为两个链表节点总数
思路二: 递归
定义: 定义个递归函数 f(l1, l2),返回合并后的新链表。
出口: !l1, !l2 分别返回 l2, l1;
代码实现:
var mergeTwoLists = function(l1, l2) {
if (!l1) return l2;
if (!l2) return l1;
if (l1.val <= l2.val) {
l1.next = mergeTwoLists(l1.next, l2);
return l1;
} else {
l2.next = mergeTwoLists(l1, l2.next);
return l2;
}
};
时间复杂度: O(n) n 为递归层数
空间复杂度: O(1) 最长开辟内存为两个链表节点总数
思路:
构建左指针和右指针,如果左右指针指向的都是英文或数字,则比较是否相等,不等则 false,如果左右指向不为英文或数字的话,则 -- 或 ++。
代码实现:
var isPalindrome = function(s) {
const reg = /[A-Za-z0-9]/
let i = 0;
let j = s.length - 1;
while(i < j) {
const left = s[i];
const right = s[j];
if (reg.test(left) && reg.test(right)) {
if (left.toUpperCase() === right.toUpperCase()) {
i++;
j--;
} else {
return false;
}
} else if (reg.test(left)) {
j--;
} else {
i++;
}
}
return true;
};
时间复杂度: O(n);
空间复杂度: O(1);
思路:
假如交易次数限 1 次,想要利益最大化,涨值波峰与波谷的差为最大。 但这题不限交易次数,那感情好,每次一有利益就揽入怀中,每次的利益累计总和就是利益最大化。
对于每次交易拆分来看的话:
单独交易日(低-高-中):上帝视角知道今天买后一天卖能赚,那我就这样做。 高-低
连续交易日(低-中-高): (中-低) + (高-中) === 高 - 低 ,连续上涨我就波峰时抛,利益最大。
不交易(高-中-低): 又不能后天买今天卖是吧。
所谓贪心,就是这样了。
代码:
var maxProfit = function(prices) {
let profit = 0;
for (let i = 0; i < (prices.length - 1); i++) {
if (prices[i + 1] > prices[i]) {
profit += prices[i + 1] - prices[i];
}
}
return profit
};
时间复杂度: O(n)
空间复杂度: O(1)
思路:
经过演算一下,会发现在一次嵌套迭代中,3*3 格子的索引可以通过这个表达式得到: Math.floor(r / 3) * 3 + Math.floor(c / 3)
,而行、列的索引,很容易在每次迭代中拿到,So, 我们可以维护每一行、每一列、每一个 box 区域的 hash 表,在一次遍历中,如果迭代的 item 未在三个 hash 表中,则加入三个 hash 表,如果在任意一个 hash 表中,代表有重复了。
代码实现:
var isValidSudoku = function(board) {
const isNumber = (val) => val !== '.';
const rowHash = [];
const columnHash = [];
const boxHash = [];
for (let i = 0; i < 9; i++) {
rowHash[i] = [];
columnHash[i] = [];
boxHash[i] = [];
}
for (let r = 0; r < 9; r++) {
for (let c = 0; c < 9; c++) {
const boxIndex = Math.floor(r / 3) * 3 + Math.floor(c / 3); // box 的索引
const item = board[r][c];
if (isNumber(item)) {
if (rowHash[r].includes(item)) return false;
if (columnHash[c].includes(item)) return false;
if (boxHash[boxIndex].includes(item)) return false;
rowHash[r].push(item);
columnHash[c].push(item);
boxHash[boxIndex].push(item);
}
}
}
return true;
};
时间复杂度: O(1); // 一次迭代遍历完 81 次
空间复杂度: O(1); // 开辟固定空间
思路:
前序遍历的值的结果 === [根节点,[左子树的前序遍历结果], [右子树的前序遍历结果]]
中序遍历的值的结果 === [[左子树的中序遍历结果],根节点,[右子树的中序遍历结果]]
我们方法的定义是,输入一个二叉树的前序遍历结果、中序遍历结果,返回构建好的二叉树的根节点。
所以我们可以先根据 左子树的前序/中序遍历结果 求出左子树,同样再求出右子树,最终拼接上根节点就是我们想要的结果了。
代码实现:
var buildTree = function(preorder, inorder) {
if (!preorder.length) {
return null;
}
const index = inorder.findIndex(v => v === preorder[0]); // 可以用 hash 表优化,不用每次都遍历查找
const leftInorder = inorder.slice(0, index);
const rightInorder = inorder.slice(index + 1);
const restPreorder = preorder.slice(1);
const leftPreorder = restPreorder.slice(0, leftInorder.length);
const rightPreorder = restPreorder.slice(index2);
const root = new TreeNode(preorder[0]);
root.left = buildTree(leftPreorder, leftInorder);
root.right = buildTree(rightPreorder, rightInorder);
return root;
};
时间复杂度: O(m * n) m 为递归的次数、n 为每次在中序数组中遍历找根节点位置的操作
空间复杂度: O(n) n 为函数调用栈的栈深
思路一:
一层循环,每轮迭代 shift
一个值 A,维护一个 A 应该插入的合适位置的指针。
代码实现:
var reverseString = function(s) {
let i = s.length - 1;
while(i > 0) {
const f = s.shift();
s.splice(i, 0, f);
i--
}
};
时间复杂度: O(n)
空间复杂度: O(1)
思路二: 双指针
维护两个指针,从数组左右开始,互相交换位置,直到左指针 >= 右指针
代码实现
var reverseString = function(s) {
let i = 0, x = s.length -1;
while (i < x) {
[s[i], s[x]] = [ s[x], s[i] ]
i++
x--
}
};
时间复杂度: O(n); 比思路一的优势是少迭代一半
空间复杂度: O(1)
思路三:
reverse
api
思路:
两两交换节点,可以设置两个指针,前指针和后指针。
在一轮迭代中交换节点位置就行了。
交换逻辑也简单:
① 前指针.next = 后指针.next
② 后指针.next = 前指针
按以上逻辑写个反转链表就够了..
但是我们要的是两两反转,比如 [1, 2, 3, 4] 转成 [2, 1, 4, 3]
所以我们的 ① 中,前指针就不能直接指向后指针的下个节点,因为后指针的下个节点在下一次迭代中就被换位了,我们的前指针应该指向下一次迭代中换位后的头节点。
但是在当前迭代中,我们并不知道下一次迭代换位后的头节点呀,那在本轮迭代,我的前指针应该指向谁?
可以用一个 Fix 标志位来修正前指针的指向。
在当前迭代,保存前指针在 Fix 里,在下轮迭代两两换位后,Fix 指向换位后的头节点,这就修正指针了。
所以逻辑变更为
修复指针
① Fix.next = 后指针
交换逻辑
② 前指针.next = 后指针.next
③ 后指针.next = 前指针
保存 Fix
④ Fix = 前指针
前后指针步进 2
⑤ 前指针 = 前指针.next
⑥ 后指针 = 前指针.next
此时代码实现为:
var swapPairs = function(head) {
if (!head) return null;
if (!head.next) return head;
let p = head;
let n = head.next;
let fix = new ListNode(0);
const newHead = n;
while(true) {
fix.next = n;
p.next = n.next;
n.next = p;
fix = p;
p = p.next;
if (!p) {
// 代表链表节点数是双数,我们已经处理完了所有节点,直接返回
return newHead
}
n = p.next;
if (!n) {
// 代表链表节点数是单数,最后一个节点不用处理,直接返回
return newHead;
}
}
}
这代码可以 AC 啦,但是有点丑陋,其实写到上面这种地步已经很容易想到写出递归版本的了。但我们就以指针的来做,来改善这个代码。
首先为什么出口写在 while 内部,因为 ⑤ ⑥ 步都可能为空,访问空的 next 属性会崩,但其实我们完全可以不放在内部,放到条件语句里就行了,如果第 ⑤ 步为空,直接不走第 ⑥ 步,所以第 ⑥ 步 就得提前到 while 的第一句,条件语句就判空第 ⑤ 步的结果。又因为第 ⑤ 步的结果为空或第 ⑥ 步的结果为空都代表我们处理完所有节点了,所以条件语句可以再保证下第 ⑥ 步一定有值。
改良后的迭代法:
var swapPairs = function(head) {
if (!head) return null;
if (!head.next) return head;
let p = head;
let fix = new ListNode(0);
const newHead = n;
while(p && p.next) {
const n = p.next;
fix.next = n;
p.next = n.next;
n.next = p;
fix = p;
p = p.next;
}
return newHead;
};
时间复杂度: O(n)
空间复杂度: O(1)
递归法:
var swapPairs = function(head) {
if(!head || !head.next) return head;
const dummy = new ListNode(0, head.next);
head.next = swapPairs(dummy.next.next); // 后指针指向的是已经两两交换过后的链表
dummy.next.next = head;
return dummy.next;
};
时间复杂度: O(n)
空间复杂度: O(n) 做不到尾递归
思路一:
利用哈希表,Key 是数组的 item, Value 是该 item 出现的次数。
代码实现:
var singleNumber = function(nums) {
const hash = {}
for (let i = 0; i < nums.length; i++) {
if (nums[i] in hash) {
hash[nums[i]]++
continue;
}
hash[nums[i]] = 1;
}
for (let k in hash) {
if (hash[k] === 1) {
return k;
}
}
};
时间复杂度: O(n)
空间复杂度: O(n)
思路二:
因为题目写死了两两相等且必有一个为单,所以可以先排序,然后对比 奇数位置 和 + 1 位置,如果不同,奇数位置就为单,如果找完了都没找到,那就是最后一个元素为单。
代码实现:
var singleNumber = function(nums) {
nums.sort();
for (let i = 0; i < nums.length - 1;) {
if (nums[i] !== nums[i + 1]) {
return nums[i];
}
i += 2;
}
return nums[nums.length - 1];
};
时间复杂度: O(nlogn)
空间复杂度: O(1)
思路三:
利用异或的三个特性:
a^b^a === a^a^b
代码实现:
var singleNumber = function(nums) {
let xor = 0;
for (let i = 0; i < nums.length; i++) {
xor = xor ^ nums[i];
}
return xor;
};
时间复杂度: O(n);
空间复杂度: O(1);
思路一:
k 是代表右移几位,如果设两个指针,后指针始终比前指针快走 k 个节点,是否就意味着,当后指针走到最后一个节点时,前指针指向的就是旋转后的链表的尾节点。前指针的下一个节点就是旋转后链表的头节点。
接下来我们要做的就是拼接出新链表就行了。
这题还要注意下优化,比如对于一个有 3 个节点的链表,我们右移 3 位,其实等于没移动。右移 4 位的结果等于右移 1 位。右移 3 * 1000000 的结果等没移动。 所以其实我们需要移动的次数是: k % 链表节点数。
代码实现
var rotateRight = function(head, k) {
if (!k) {
return head;
}
if (!head) {
return head;
}
const forOptimizing = k;
let fast = new ListNode(0, head);
let slow = new ListNode(0, head);
while(fast.next && k--) {
fast = fast.next;
}
if (k !== -1) {
// 代表 k 还没结束,链表就走到头了,也就是链表向右移动了链表总数个位置。也就相当于没移动,我们接着移动剩余的 properK 个。
const properK = forOptimizing % (forOptimizing - k);
return rotateRight(head, properK);
}
while(fast.next) {
// 指针步进直到 fast 指向最后一个节点
fast = fast.next;
slow = slow.next;
}
if (slow.next === head) {
// 链表往右移动链表总数,其实就等于自身
return head;
}
const newHead = slow.next;
slow.next = null;
fast.next = head;
return newHead;
};
时间复杂度: O(n) 最坏遍历两个链表长度
空间复杂度: O(1)
思路二:
把链表头尾相连形成环,在 「链表节点数 - (k % 链表节点数)」 处截断,返回截断后的节点即可。
为什么是 「链表节点数 - (k % 链表节点数)」 处,自己画个图去找规律吧~
代码实现:
var rotateRight = function(head, k) {
if (!k || !head || !head.next) {
return head;
}
let lastNode = head;
let len = 1;
while(lastNode.next) {
lastNode = lastNode.next;
len++;
}
lastNode.next = head; // 成环
let cut = len - (k % len);
while(--cut) {
head = head.next;
}
const newHead = head.next;
head.next = null // 切断
return newHead;
};
// 时间复杂度: O(n) 最坏情况为遍历 链表节点数 * 2 - 1 次
// 空间复杂度: O(1)
思路一:
暴力搜索:两层循环,一层循环找对应 item, 二层循环找后续有无重复。 就不给代码实现了。
思路二:
空间换时间: 建立一个目标是无重复元素的 helper 数组 A,一层遍历,每轮的循环中判断 item 是否在 A 中,若没在则 push 进去,若在则证明有重复。
代码实现:
var containsDuplicate = function(nums) {
const already = [];
let len = nums.length
while(len--) {
if (already.includes(nums[len])) {
return true;
}
already.push(nums[len])
}
return false;
};
时间复杂度: O(n);
空间复杂度: O(n);
思路三:
先排序,然后比较相邻元素是否重复。
代码实现:
var containsDuplicate = function(nums) {
nums.sort() // O(nlogn)
for (let i = 0; i < nums.length - 1; i++) {
if (nums[i] === nums[i + 1]) {
return true;
}
}
return false;
};
时间复杂度: O(nlogn) >>> sort 快排复杂度,其实每个引擎厂商都不一样,这里取 v8 实现
空间复杂度: O(1)
思路:
深度优先遍历比较适用于找路径的问题,此处我们用前序遍历。
在遍历中维护一个数组保存路径,相加已遍历路径上的值,若是叶节点且等于 target 则存进结果数组,否则继续遍历、回溯。
代码实现:
var pathSum = function(root, target) {
const ret = [];
const path = [];
function dfs(node, target) {
if (!node) return;
// 节点存在,入路径数组进行处理
path.push(node.val);
// 叶子节点且值相等
if (!node.left && !node.right && node.val === target) {
ret.push(path.slice());
}
dfs(node.left, target - node.val);
dfs(node.right, target - node.val);
// 左右节点处理完了,证明该节点处理完了,出路径数组回溯
path.pop();
}
dfs(root, target);
return ret;
};
```js
时间复杂度:O(n) n 为节点数
空间复杂度:O(n) 非尾递归,n 为树的深度
思路一
动态维护公共前缀,默认公共前缀就是第一个元素,然后在一轮遍历中,比较每个元素和动态公共前缀的功能前缀,遍历完即得到最长公共前缀。
代码实现
var longestCommonPrefix = function(strs) {
let res = strs[0];
for (let i = 1; i < strs.length; i++) {
res = findCommonPrefix(res, strs[i]);
if (res === '') return res;
}
return res;
};
function findCommonPrefix (str1, str2) {
let common = '';
for (let i = 0; i < str1.length; i++) {
if (str1[i] !== str2[i]) {
return common;
}
common += str1[i]
}
return common;
}
时间复杂度: O(m*n) m 是所有元素的平均长度、n 是数组长度
空间复杂度: O(1)
这是性能最优的实现。 其他思路的实现时空间复杂度都更高,都是耍流氓。
但我还是要说...
思路二:
先按照元素长度排序,然后找出第一个元素和最后一个元素的公共前缀就好。
思路三:
一位一位地纵向对比,然后递归下一位。
递归的定义:接收一个字符串数组,返回一个公共前缀的字符或空字符。
若第一个字符是公共前缀则返回第一个字符 + 其余字符串组成的新数组的公共前缀,否则返回 ""。
思路四五: 强行分治、强行二分。
思路一:
如果两个字符串长度不相等,肯定 false。
在长度相等的情况下,首先遍历第一个字符串,把各个字母的 ASCII 码值与 'a' 的 ASCII 码值相减作为索引,把出现次数作为值,放在一个 26 位数组中(因为英文字母就 26 位),然后遍历第二个字符串,遇到相同索引就把值减一,如果两个值符合异位词,那么数组最终各个索引的值应该都为 0。
代码实现:
var isAnagram = function(s, t) {
if (s.length !== t.length) {
return false;
}
// 26 个英文字母
const arr = new Array(26).fill(0);
for (let i = 0; i < s.length; i++) {
arr[s.charCodeAt(i) - 'a'.charCodeAt(0)]++;
}
for (let j = 0; j < t.length; j++) {
const index = t.charCodeAt(j) - 'a'.charCodeAt(0);
arr[index]--;
if (arr[index] < 0) {
return false;
}
}
return true;
};
时间复杂度: O(n)
空间复杂度: O(1) // 26 个字母
思路二: 哈希表(当字符串包含 Unicode 字符时)
ASCII 码只能够表示英文字母与二进制位的关系,Unicode 编码集的话可以表示所有字符与二进制位的关系,包括德文、汉字等。
又因为 javaScript 是使用 Unicode 字符集的,但它的编码方式是UCS-2(历史原因),并且UCS-2的编码与Unicode的编号几乎一样。所以,我们可以在JS中直接使用转义后 Unicode 的编号就能获取对应字符,比如 u597d => 好。
这部分可以参考阮一峰老师的 Unicode与JavaScript详解
所以我们可以创建个哈希表,Unicode 字符作为 key,出现次数作为 value,一次遍历一字符串收集次数,二次遍历二字符串判断异位性。
代码实现:
var isAnagram = function(s, t) {
if (s.length !== t.length) {
return false;
}
// 26 个英文字母
const hash = {};
for (let i = 0; i < s.length; i++) {
if (hash.hasOwnProperty(s[i])) {
hash[s[i]]++;
continue;
};
hash[s[i]] = 1;
}
for (let j = 0; j < t.length; j++) {
if (!hash.hasOwnProperty(t[j])) {
return false;
}
hash[t[j]]--;
if (hash[t[j]] < 0) {
return false;
}
}
return true;
};
时间复杂度: O(n)
空间复杂度: O(n)
思路三: 排序
代码实现:
var isAnagram = function(s, t) {
return s.length == t.length && [...s].sort().join('') === [...t].sort().join('')
};
时间复杂度: O(nlogn) sort 的时间复杂度
空间复杂度: O(n) 视字符的长度而定,并且 sort 方法根据不同厂商的实现也有空间复杂度
思路:
由思考过程易得: 大问题拆分成小问题也适用相同的逻辑,可以采用递归编写。
代码实现:
var sortedArrayToBST = function(nums) {
if (!nums.length) return null;
if (nums.length === 1) return new TreeNode(nums[0]);
const mid = Math.floor(nums.length / 2);
const root = new TreeNode(nums[mid])
root.left = sortedArrayToBST(nums.slice(0, mid))
root.right = sortedArrayToBST(nums.slice(mid + 1));
return root;
};
时间复杂度: O(n) 每个节点只会遍历到一次
空间复杂度: O(n) 递归栈带来的线性内存消耗
思路一: 递归
判断二叉树是否是对称,需要从子节点开始比较,两个子节点的值必须相同,并且左子节点的右子节点(如果有)必须等于右子节点的左子节点,左子节点的左子节点必须等于右子节点的右子节点。并且这个规律对所有子树都满足。
代码实现:
var isSymmetric = function(root) {
// 定义: 判断两棵树根节点值是否相等 且 左子节点的右子节点(如果有)必须等于右子节点的左子节点,左子节点的左子节点必须等于右子节点的右子节点。 如果不符合则返回 false。
// 出口: 除了定义的 false 条件外,左右节点为空返回 true。
function helper(left, right) {
if (!left && !right) return true;
else if (!left || !right) return false;
if (left.val !== right.val) {
return false
}
return helper(left.left, right.right) && helper(left.right, right.left)
}
return helper(root.left, root.right)
};
时间复杂度: O(n) 每个节点遍历一次
空间复杂度: O(H) 每层节点占一个递归栈
思路二:
利用队列,遍历到每个节点则按 left.left, right.right, right.left, left.right 的顺序推进节点,然后从队列中动态取前两个节点比较,判断 val 是否相等。
代码实现:
var isSymmetric = function(root) {
if(!root) return true;
const queue = [root.left, root.right];
while(queue.length) {
let l = queue.shift();
let r = queue.shift();
if(!l && !r) continue;
if(!l || !r) return false;
if(l.val !== r.val) return false;
queue.push(l.left);
queue.push(r.right);
queue.push(r.left);
queue.push(l.right);
}
return true;
}
时间复杂度: O(n);
空间复杂度: O(n);
思路三:
广度优先遍历,每层节点判断是否是镜像的:每一层设个双指针判断是否左右相等,相等则左指针++ 右指针--,不等则 false,等则继续判断下去,最终返回 true。 就不实现了。
思路一:
先遍历其中一个链表,用一个 Map 保存所有节点,再遍历另一个链表,第一个遇到 Map 内的节点则返回,否则返回 null。
代码很简单,不贴出来了。
思路二:
可以动手画一画,维护两个指针,分别从两个链表的头部开始,每次步进 1,步进总长度为两个链表的节点数之和,步进的顺序是走完一个链表后再走另一条链表,最终两个指针都会走相同的长度。 如果有相交的节点,两个指针会在步进相同长度后,指向同一节点,否则永远不会指向相同节点。
代码实现:
var getIntersectionNode = function(headA, headB) {
if (headA === null || headB === null) {
return null;
}
let pA = headA, pB = headB;
while (pA !== pB) {
pA = pA === null ? headB : pA.next;
pB = pB === null ? headA : pB.next;
}
// 要么同时指向相交的第一个几点,要么同时指向 null
return pA;
};
思路:
一层循环,哈希表缓存 key 为 target 与 item 的差,value 为 item 索引数组, 若之后循环 item in hashTable,则表明该两个 item 和为 target,返回其索引数组。
代码实现:
var twoSum = function(nums, target) {
const cache = [];
for(let i = 0; i < nums.length; i++) {
let diff = target - nums[i]
if (diff in cache) {
return [i, cache[diff]];
}
cache[nums[i]] = i;
}
};
时间复杂度: O(n)
空间复杂度: O(n)
思路:
其实题意把递归的思路都说清楚了。。
递归定义: 给定一个数组,构建一个最大二叉树
递归出口: 数组为空时返回 null
代码实现:
/**
* Definition for a binary tree node.
* function TreeNode(val, left, right) {
* this.val = (val===undefined ? 0 : val)
* this.left = (left===undefined ? null : left)
* this.right = (right===undefined ? null : right)
* }
*/
/**
* @param {number[]} nums
* @return {TreeNode}
*/
var constructMaximumBinaryTree = function(nums) {
if (!nums.length) {
return null;
}
let max = -Infinity, maxIndex;
nums.forEach((item, index) => {
if (item > max) {
max = item;
maxIndex = index;
}
})
const root = new TreeNode(max);
root.left = constructMaximumBinaryTree(nums.slice(0, maxIndex));
root.right = constructMaximumBinaryTree(nums.slice(maxIndex + 1));
return root;
};
思路一:
双指针:设计指针 L 动态标识数组中第一个 0 的位置,指针 R 找出距离 L 最近的非 0 数,然后在一层遍历中动态交换 L 、R 的值。
代码实现:
var moveZeroes = function(nums) {
let l = nums.indexOf(0);
if (l === -1) return nums;
for (let r = l + 1; r < nums.length; r++) {
if (nums[r] !== 0) {
nums[l] = nums[r];
nums[r] = 0;
l++;
r = l;
}
}
};
时间复杂度: O(n);
空间复杂度: O(1);
思路二:
利用 js 特性,从后往前遍历一次,遇到 0 了就删除,然后往屁股补 0 就行。
代码实现:
var moveZeroes = function(nums) {
for(let i = nums.length - 1; i >= 0; i--) {
if (nums[i] === 0) {
nums.splice(i, 1);
nums.push(0);
}
}
};
思路:递归
树是一个典型的递归结构。
对于一个树的前序遍历,其实就是想要先访问到树的根节点、再访问左节点、再访问右节点。
假如 root 根节点只有左右孩子,那么我们要的结果就是 [根节点,左节点,右节点]
对吧?
如果左右孩子又有其自己的节点,要以前序去访问节点是否也是先访问孩子节点,再访问孩子的左节点,最后访问孩子的右节点,我们想要的结果就是 [孩子节点,孩子左,孩子右]
。
所以我们完全可以把左右孩子分别作为根节点拆成两个树,我们最终想要的结果就是 [根节点,左孩子树的前序遍历结果,右孩子树的前序遍历结果]
。
然而 preorderTraversal
方法的定义(目的)就是输入一个根节点,返回该树的前序遍历结果,所以我们想要的结果就是 [根节点, ...preorderTraversal(左孩子), ...preorderTraversal(右孩子)]
。
中序、后续的遍历结果其实就只是顺序不同嘛。
以前序为例的代码实现:
if (!root) return []; // 出口条件,传个空节点,返回空数组
// [根节点, ...preorderTraversal(左孩子), ...preorderTraversal(右孩子)]
return [root.val, ...preorderTraversal(root.left), ...preorderTraversal(root.right)];
时间复杂度、空间复杂度都是 O(n)
思路二:迭代
递归的方式无非就是函数入执行栈、出执行栈的过程。
要想使用迭代方法,我们可以自己维护一个执行栈,模拟递归的过程。
著名的颜色标记法可以来解决迭代树的遍历问题。
时间复杂度、空间复杂度都是 O(n)
代码实现:
if (!root) return [];
const stack = [{
color: 'white', // 未遍历到就是 'white',遍历到了该节点就变颜色为 'gray'
node: root
}];
const res = [];
while(stack.length) {
const {color, node} = stack.pop(); // 节点出栈、待处理
if (color === 'gray') {
// 遍历到了该节点,则对其做出处理,此处是保存进返回值里
res.push(node.val);
} else {
// 节点入栈
// 因为栈是先进后出,假如我们想前序遍历 根左右 的话,入栈顺序就得反过来。
// 同理,想要中序遍历、后续遍历,改变入栈顺序就行。
node.right && stack.push({color: 'white', node: node.right});
node.left && stack.push({color: 'white', node: node.left});
stack.push({color: 'gray', node}); // 本轮走到了该节点,颜色改变
}
}
return res;
思路:
可以利用层序遍历来序列化收集树节点,然后同样用层序遍历的思路来反序列出树,在纸上画画,可以找到在反序列化层序遍历每个收集的节点时,其左、右孩子的位置在该节点的后两位、三位,所以我们维护个指针即可动态构建每个遍历的节点的左右孩子。
代码实现:
/**
* Encodes a tree to a single string.
*
* @param {TreeNode} root
* @return {string}
*/
var serialize = function(root) {
const Layer_Sign = 'layer';
const queue = [root, Layer_Sign];
const ret = root ? [root.val] : [];
let node = null;
while(node = queue.shift()) {
if (node === Layer_Sign) {
if(!queue.length) {
break;
}
queue.push(Layer_Sign);
continue;
}
ret.push(node.left ? node.left.val : null);
ret.push(node.right ? node.right.val : null);
node.left && queue.push(node.left);
node.right && queue.push(node.right);
}
return JSON.stringify(ret);
};
/**
* Decodes your encoded data to tree.
*
* @param {string} data
* @return {TreeNode}
*/
var deserialize = function(data) {
const arr = JSON.parse(data);
if (!arr.length) return null;
const root = new TreeNode(arr[0]);
const queue = [root];
let pointer = 1; // 指向当前节点的左孩子
let node;
while(node = queue.shift()) {
if (arr[pointer] !== null && arr[pointer] !== undefined) {
const leftNode = new TreeNode(arr[pointer]);
node.left = leftNode;
queue.push(leftNode);
}
if (arr[pointer + 1] !== null && arr[pointer + 1] !== undefined) {
const rightNode = new TreeNode(arr[pointer + 1]);
node.right = rightNode;
queue.push(rightNode);
}
pointer += 2;
}
return root;
};
/**
* Your functions will be called as such:
* deserialize(serialize(root));
*/
思路一:
熟知 BST 的特性的话,解这道题思路其实很简单:先找到待删除节点与待删除节点的父节点,再让父节点指向待删除节点右子树的最小值,最后删除最小值父节点的指向。
难点在思考边界情况,具体看代码。
代码实现:
/**
* Definition for a binary tree node.
* function TreeNode(val, left, right) {
* this.val = (val===undefined ? 0 : val)
* this.left = (left===undefined ? null : left)
* this.right = (right===undefined ? null : right)
* }
*/
/**
* @param {TreeNode} root
* @param {number} key
* @return {TreeNode}
*/
var deleteNode = function(root, key) {
let cache = null;
function dfs(node) {
if (!node || cache) return;
if(node.left && node.left.val === key) {
cache = [node.left, node, 'left'];
return;
}
if (node.right && node.right.val === key) {
cache = [node.right, node, 'right']
return;
}
dfs(node.left);
dfs(node.right);
}
if (root && root.val === key) {
// 根节点就是待删除节点
const dummyRoot = new TreeNode(-1);
dummyRoot.left = root;
cache = [root, dummyRoot, 'left'];
} else {
dfs(root)
}
const [target, parent, sign] = cache ?? [];
if (!target) {
// 如果没找到删除的,就返回树本身
return root;
}
function findMinNode(node) {
if (!node) return;
let parent = node;
let left = node.left;
while(left && left.left) {
parent = left;
left = left.left;
}
return [parent, left];
}
const [father, node] = findMinNode(target.right) ?? [];
// 删除节点没有右子树
if (!node && !father) {
parent[sign] = target.left;
}
// 删除节点的右子树没有左孩子
if (!node && father) {
parent[sign] = father;
father.left = target.left;
}
// 找到最小节点和其父节点
if(node && father) {
parent[sign] = node;
let r = node;
while(r && r.right) {
r = r.right;
}
r.right = target.right;
node.left = target.left;
father.left = null;
}
return target === root ? parent[sign] : root;
};
思路二:
递归去解,思路大概这四步:
代码实现:
var deleteNode = function(root, key) {
// 如果根节点
if(!root) return null;
if(root.val > key) {
root.left = deleteNode(root.left, key);
} else if(root.val < key) {
root.right = deleteNode(root.right, key);
} else {
if(!root.left && !root.right) return null;
if(!root.left || !root.right) {
return root.left ? root.left : root.right;
}
// 右子树最小的节点来替换
let node = root.right;
while(node.left) {
node = node.left;
}
root.val = node.val;
root.right = deleteNode(root.right, node.val);
}
return root;
};
思路一:
利用一个容器,一轮迭代链表的过程中,判断每个节点是否在容器内,如果没有则加入容器,否则证明有环。
(可以不用容器也达到这个思路的效果,改造链表,访问过则加一个标志位,再次遇到标志位则证明有环。这样的话时间复杂度就 O(n),空间复杂度就 O(1))
代码实现:
var hasCycle = function(head) {
const hash = [];
while (head) {
if (hash.includes(head)) {
return true;
}
hash.push(head);
head = head.next;
}
return false;
};
时间复杂度: O(n^2) includes API 有 O(n) 复杂度
空间复杂度: O(n)
思路二:
一般环形的数据结构都可利用快慢指针,同时出发,慢指针每次走一步,快指针每次走两步,如果有环的话,两个指针终会相遇。
代码实现:
var hasCycle = function(head) {
let fast = head;
let slow = head;
// 只有没环的时候才会不符合条件
while (fast != null && fast.next != null) {
fast = fast.next.next;
slow = slow.next;
if (fast === slow) {
return true;
}
}
return false;
};
时间复杂度: O(n);
空间复杂度: O(1);
思路:
设置快慢指针,慢指针指向虚拟头节点,快指针指向头节点。
当快指针.val === 快指针.next.val 时,快指针移动,直到找到非重复的节点。
然后慢指针指向非重复快指针,慢指针步进,快指针重复上述逻辑找非重复节点。
实现:
var deleteDuplicates = function(head) {
if (!head || !head.next) return head;
const dummy = new ListNode(0, head);
let slow = dummy;
let fast = head;
while(fast) {
if (fast.next && fast.val === fast.next.val) {
while(fast && fast.val === slow.next.val) {
fast = fast.next;
}
slow.next = fast; // 删除重复节点
continue;
}
slow = slow.next;
fast = fast.next;
}
return dummy.next;
};
// 时间复杂度: O(n) n 为链表长度
// 空间复杂度: O(1)
思路一:
两层遍历,一层遍历循环 nums1 数组每个 Item,针对每个 item 遍历 nums2 数组,找到相同的就往结果数组里 push,然后把 nums2 的该元素给删除了。 这样可以找到重复的交集。 可以找出元素较少的数组减少时间复杂度。
代码实现:
const len1 = nums1.length;
const len2 = nums2.length;
let less = null;
let more = null;
if (len1 < len2) {
less = nums1;
more = nums2;
} else {
less = nums2;
more = nums1;
}
const res = [];
for (let i = 0; i < less.length; i++) {
let j = 0;
while(j < more.length) {
if (more[j] === less[i]) {
res.push(more[j]);
more.splice(j, 1); // 底层是链表,单纯删除 O(1)
break;
}
j++
}
}
return res;
时间复杂度: O(n^2)
空间复杂度: O(n)
思路二:
维护一个哈希表,遍历一个数组,把 item 作为 key,出现次数作为 value。 然后再遍历另一个数组,遇到相同的 key 且 value >= 1,则推进结果数组,然后把 value--。
代码实现:
const map = {};
for (let i = 0; i < nums1.length; i++) {
if (!map[nums1[i]]) {
map[nums1[i]] = 1;
continue;
}
map[nums1[i]]++
}
const res = [];
for (let i = 0; i < nums2.length; i++) {
if (nums2[i] in map && map[nums2[i]]) {
res.push(nums2[i]);
map[nums2[i]]--;
}
}
return res;
时间复杂度: O(n)
空间复杂度: O(n)
思路三:
先将数组排序,然后用双指针,一开始指向最小,对比两指针指向的值,小的那方挪动,直到遍历完一方。若遇到相同的值,同时往后挪一位,并且往结果数组 push 。
代码实现
var intersect = function(nums1, nums2) {
nums1.sort((a, b) => a - b);
nums2.sort((a, b) => a - b);
let l = 0, r = 0, ans = [];
while (l < nums1.length && r < nums2.length) {
if (nums1[l] === nums2[r]) {
ans.push(nums1[l]);
l++;
r++;
} else nums1[l] < nums2[r] ? l++ : r++;
}
return ans;
时间复杂度: O(nlogn) sort 的时间复杂度
空间复杂度: O(m) m 代表交集数量
};
力扣进阶解答:
如果给定的数组已经排好序呢?你将如何优化你的算法? -> 思路三
如果 nums1 的大小比 nums2 小很多,哪种方法更优? -> 思路三
如果 nums2 的元素存储在磁盘上,内存是有限的,并且你不能一次加载所有的元素到内存中,你该怎么办?
答: 如果 nums2 的元素存储在磁盘上,磁盘内存是有限的,并且你不能一次加载所有的元素到内存中。那么就无法高效地 对 nums2 进行排序,反而思路二的查询操作更优。
思路一:
遍历数组的时候,假设当前 item 为最小值,遍历后续元素找出最大值,动态维护最大值。
代码实现:
var maxProfit = function(prices) {
let res = 0;
for(let i = 0; i < prices.length - 1; i++) { // 第一层遍历表示第几天买入股票(最后一天没必要买了,卖不出去)
let max = 0;
for(let j = i+1; j < prices.length; j++) { // 这层遍历表示第几天卖出
let getMoney = prices[j] - prices[i];
if (getMoney > max) {
max = getMoney;
}
}
max > res && (res = max);
}
return res;
};
时间复杂度: 0(n^2)
空间复杂度: O(1)
思路二: 动态维护最大利益值和最小值,当我们每遍历到一个 item 的时候,假设该 item 是最大值,动态找出之前所有 item 的最小值,然后让该 item - 动态维护的最小值,再与之前求出的最大值作比较,以此动态维护个最大利益值。
代码实现:
var maxProfit = function(prices) {
let min = prices[0];
let max = 0;
for(let i = 0; i < prices.length; i++) {
max = Math.max(max, prices[i] - min);
min = Math.min(min, prices[i]);
}
return max;
};
时间复杂度: O(n)
空间复杂度: O(1)
思路一:
对字符串一次完整的遍历肯定少不了的,在这一次的遍历中,可以存每个元素的索引到哈希表中,如果遇见哈希表中已经有这个 key 了,就把 value 设为一个特殊值(如下 -1),然后遍历哈希表,找到第一个不为特殊值的值即可,找不到就返回 -1。
这个思路有个大前提就是遍历哈希表的时候,要确定遍历元素的顺序就是插入哈希表的顺序,基于此,对象肯定不能用,因为对象的遍历会输出排序后的 key ,可以用 Map
构建哈希表, 遍历 Map
的顺序就是按照插入顺序的。
代码实现:
var firstUniqChar = function(s) {
const hash = new Map();
for (let i = 0; i < s.length; i++) {
if (!hash.has(s[i])) {
hash.set(s[i], i);
continue;
}
hash.set(s[i], -1);
}
for (let value of hash.values()) {
if (value !== -1) {
return value;
}
}
return -1;
};
时间复杂度: O(n);
空间复杂度: O(n);
思路二:
利用 js API String.prototype.lastIndexOf()
与 indexOf()
,在一次遍历中,判断每个元素的第一个索引是否和最后一个索引一样,一旦为 true,返回即可,否则返回 -1。
代码实现:
var firstUniqChar = function(s) {
for(let i = 0; i < s.length; i++)
if (s.indexOf(s[i]) === s.lastIndexOf(s[i]))
return i
return -1
};
时间复杂度: O(n*indexOf 时间复杂度)
空间复杂度: O(1);
思路:
这题其实相较 105. 从前序与中序遍历序列构造二叉树 106. 从中序与后序遍历序列构造二叉树 而言,思路是一模一样的,不过在这题中当我们找到根节点后,怎么确定左子树和右子树的数量呢?
我们可以找前序的第二个节点,是左子树的根节点,又因为后序是左右根,所以找左子树的根节点在后序的数组内是多少索引,这个索引之前的就是所有左子树的节点了,之后的就是所有右子树的节点了(除了最后一个根节点)。找出左子树、右子树的前序和后序后,递归构造二叉树就解了。
代码实现:
var constructFromPrePost = function(preorder, postorder) {
if (!preorder.length) return null;
const root = preorder[0];
const node = new TreeNode(root);
const leftLen = postorder.indexOf(preorder[1]) + 1;
const leftPreTree = preorder.slice(1, leftLen + 1);
const rightPreTree = preorder.slice(leftLen + 1);
const leftPostTree = postorder.slice(0, leftLen);
const rightPostTree = postorder.slice(leftLen, -1);
node.left = constructFromPrePost(leftPreTree, leftPostTree);
node.right = constructFromPrePost(rightPreTree, rightPostTree);
return node;
};
A declarative, efficient, and flexible JavaScript library for building user interfaces.
🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
An Open Source Machine Learning Framework for Everyone
The Web framework for perfectionists with deadlines.
A PHP framework for web artisans
Bring data to life with SVG, Canvas and HTML. 📊📈🎉
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
Some thing interesting about web. New door for the world.
A server is a program made to process requests and deliver data to clients.
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
Some thing interesting about visualization, use data art
Some thing interesting about game, make everyone happy.
We are working to build community through open source technology. NB: members must have two-factor auth.
Open source projects and samples from Microsoft.
Google ❤️ Open Source for everyone.
Alibaba Open Source for everyone
Data-Driven Documents codes.
China tencent open source team.