1.两数之和
1.1 自己所写方法(暴力枚举)
let result = [];
for(let i = 0; i < nums.length; i++){
for(let j = i + 1; j < nums.length; j++){
if(nums[i] + nums[j] == target){
result.push(i);
result.push(j)
}
}
}
return result;
1.2 别人的方法(ES6中的map方法):
Map对象保存键值对,任何值都可以作为一个键或一个值。这里需要注意Object中虽然也可以保存键值对,但是Object中的对象只能是字符串或者Symbol,而Map中的键可以是函数,对象或者基本类型。
Map.prototype.has(key): 返回一个布尔值,表示Map实例是否包含键对应的值
Map.prototype.get(key):返回指定键的值,如果不存在返回undefined
let map = new Map();
for(let i = 0; i < nums.length; i++){
x = target - nums[i];
if(map.has(x)){
return [map.get(x),i]
}
map.set(nums[i],i);
}
map.set() 不可以放在if语句前面 会有错误!
2. 两数相加
补充知识:NodeList是一种类数组对象,用于保存一组有序的节点,可以通过方括号来访问NodeList的值,它有item()方法与length属性,没有数组对象的方法
let head = null, tail = null;
let carry = 0;
while(l1 || l2){
const n1 = l1?l1.val : 0;
const n2 = l2?l2.val : 0;
const sum = n1 + n2 + carry;
if(!head){
head = tail = new ListNode(sum % 10);
}else{
tail.next = new ListNode(sum % 10);
tail = tail.next;
}
carry = Math.floor(sum/10)
if(l1){
l1 = l1.next
}
if(l2){
l2 = l2.next
}
}
if(carry > 0){
tail.next = new ListNode(carry)
}
return head
对上述代码的理解:先创建一个新的链表head 将l1与l2的第一个元素相加 作为head,tail的第一个元素,注意这里head 与 tail 用的应该是赋址,然后tail.next 指向下一个元素 将计算后的结果作为head第二个元素。
322.零钱兑换
这是一个 背包(DP)问题,给定一个背包容量target,再给定一个数组nums(物品),能否按一定方式选取nums中的元素得到target,可以分几种情况
0/1背包问题(每个元素最多选一次)、 完全背包问题(每个元素可以重复选择)、组合背包问题(背包中的物品要考虑顺序)、分组背包问题(不止一个背包,需要遍历每个背包)
let dp = new Array(amount + 1 ).fill(Infinity);
dp[0]=0;
for(let i = 1;i <= amount; i++){
for(let coin of coins){
if(i - coin >= 0){
dp[i] = Math.min(dp[i],dp[i - coin]+1)
}
}
}
return dp[amount] === Infinity?-1:dp[amount]
75、颜色分类
排序问题
let flag = 0;
for(let i = 0; i < nums.length -1; i++){
for(let j = i + 1 ; j < nums.length; j++){
if(nums[i] > nums[j]){
flag = nums[i];
nums[i] = nums[j];
nums[j] = flag
}
}
}
参考官方题解答案
let p0 = 0 ;
let i = 0 ;
let p2 = nums.length - 1;
while(i <= p2){
if(nums[i] == 0){
swap(i,p0);
p0++;
i++;
}else if(nums[i] == 1){
i++;
}else{
swap(i,p2)
p2--;
}
}
function swap(index1, index2){
let temp = nums[index1];
nums[index1] = nums[index2]
nums[index2] = temp
}
114、二叉树展开为链表
二叉树是一种数据结构,简单来说就是一个包含节点,以及它的左右孩子的一种数据结构
前序遍历:根节点--->左边节点--->右边节点,[1 2 4 5 3 6 7]
中序遍历:左边节点--->根节点--->右边节点,[4 2 5 1 6 3 7]
后序遍历:左边节点--->右边节点--->根节点,[4 5 2 6 7 3 1]
层次遍历:按照每一层从左向右的方式进行遍历,[1 2 3 4 5 6 7]
迭代方法
就像是先 前序遍历 然后将得到结果list遍历 左加上null 右加上当前值
const list = [];
const stack = [];
let node = root;
while(node !== null || stack.length){
while(node !== null){
list.push(node)
stack.push(node)
node = node.left
}
node = stack.pop();
node = node.right;
}
const size = list.length;
for(let i = 1; i < size; i++){
const prev = list[i-1], curr = list[i];
prev.left = null;
prev.right = curr
}
递归方法(与144题解思想类似)
const list = [];
preorder(root,list);
const size = list.length;
for(let i = 1; i < size; i++){
const prev = list[i-1], curr = list[i];
prev.left = null;
prev.right = curr;
}
function preorder(root, list){
if(root != null){
list.push(root);
preorder(root.left,list)
preorder(root.right,list)
}
}
144、二叉树的前序遍历
迭代方法
stack作为栈 res作为最后结果。先根节点入栈 在出栈添加到res中,在将右边节点入栈,然后左边节点入栈 判断栈是否为空,不是则开始处理左边节点 对左边节点 也进行一样的操作
if(!root) return [];
const stack = [], res = []
stack.push(root)
while(stack.length){
const curr = stack.pop();
res.push(curr.val)
if(curr.right) stack.push(curr.right)
if(curr.left) stack.push(curr.left)
}
return res
递归方法
条件:大问题拆成两个子问题、子问题求解方式和大问题一样、存在最小问题
res是结果集。递归终止条件node为空 不为空就添加到res中。先处理左边,在处理右边节点
if(!root) return [];
const res = [];
preorder(root,res)
return res
function preorder(node, res){
if(!node) return;
res.push(node.val)
preorder(node.left,res);
preorder(node.right,res)
}
128、最长连续序列
1、对数组去重。
2、遍历数组,如果当前数x的前驱x-1不存在,我们就以当前数x为起点向后枚举。
3、不断枚举更新答案。
const num_set = new Set(nums);
let longest_streak = 0;
for(let num of num_set){
if(num_set.has(num - 1)) continue
let current_streak = 1
while(num_set.has(++num)){
current_streak++
}
longest_streak = Math.max(longest_streak,current_streak)
}
return longest_streak
160.相交链表
先创建一个集合visited,遍历headA 当不为null值时添加到 visited中,然后遍历headB,当节点值不为null值 且在visited中有的时候 则是相交节点。
我的理解是 let temp = headA temp指向的是地址值 value值一样 并不代表地址值一样,所以结果不是1
const visited = new Set()
let temp = headA;
while(temp !== null){
visited.add(temp);
temp = temp.next
}
temp = headB;
while(temp !== null){
if(visited.has(temp)){
return temp
}
temp = temp.next
}
return null
双指针的方法(同时遍历)
当链表相交的时候, 长度一样 同时遍历,直到两个值一样; 长度不一样,遍历完之后也就是为null值的时候遍历另一个链表,让他们加起来的长度一样。不相交的情况也是如此。
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;
}
return pA
406、根据身高重建队列
这里需要用到js中数组的sort方法 里面可选参数是函数 function(a,b) 若返回a-b 则升序,若返回b-a则降序。若可选参数不写 则默认升序。
splice方法,有三个参数 第一个index(在什么位置添加/删除元素,若是负数则从末尾开始),howmany(删除几个元素),item...(要添加的新元素)
const res = [];
people.sort((a,b) => {
if(a[0] === b[0]) return a[1] - b[1]
return b[0] - a[0]
})
for(let i = 0; i<people.length; i++){
let k = people[i][1]
if(k < i){
res.splice(k,0,people[i])
}else{
res[i] = people[i]
}
}
return res
先按身高h降序排,以k为第二元素升序排。对于个子高的人先暂时排好。后面的每个个子比他矮的元素 再看k,往前面排好的个子高的那一列插入 因为个子矮 所以不会影响 排好的高个子那数组的k值,以此类推。
148、排序链表
归并排序采用了分治策略,将数组分成2个较小的数组,然后每个数组再分成两个更小的数组,直至每个数组里只包含一个元素,然后将小数组不断的合并成较大的数组,直至只剩下一个数组,就是排序完成后的数组序列。
const merge = (head1, head2) => {
const dummyHead = new ListNode(0);
let temp = dummyHead, temp1 = head1, temp2 = head2;
while(temp1 !== null && temp2 !== null){
if(temp1.val <= temp2.val){
temp.next = temp1;
temp1 = temp1.next;
}else{
temp.next = temp2;
temp2 = temp2.next;
}
temp = temp.next
}
if(temp1 !== null){
temp.next = temp1
}else if(temp2 !== null){
temp.next = temp2
}
return dummyHead.next
}
const toSortList = (head, tail) => {
if(head === null){
return head
}
if(head.next === tail){
head.next = null;
return head
}
let slow = head, fast = head;
while(fast !== tail){
slow = slow.next;
fast = fast.next;
if(fast !== tail){
fast = fast.next;
}
}
const mid =slow;
return merge(toSortList(head,mid), toSortList(mid,tail))
}
var sortList = function(head) {
return toSortList(head,null)
};
3、无重复字符串的最长子串
遍历原字符串 看是否在arr中,要是不在就添加到arr中,要是在就删除arr中重复的元素 以前位置在它前面的元素。
let arr = [], max = 0;
for(let i = 0; i < s.length; i++){
let index = arr.indexOf(s[i])
if(index !== -1){
arr.splice(0,index+1)
}
arr.push(s.charAt(i))
max = Math.max(arr.length, max)
}
return max
4、寻找两个正序数组的中位数
先分别判断nums1 与 nums2 是否为null 如果是则只用一个数组 进行中位数的计算,不是则拼接成新的数组,先排序,在计算。 但是时间复杂度 不满足要求
if(nums1.length === 0){
let n = (nums2.length - 1) / 2;
let res = (nums2[Math.ceil(n)] + nums2[Math.floor(n)]) / 2
return res
}else if(nums2.length === 0){
let n = (nums1.length - 1) / 2;
let res = (nums1[Math.ceil(n)] + nums1[Math.floor(n)]) / 2
return res
}else{
nums = new Array(...nums1,...nums2)
nums.sort((a,b) => {
return a-b
})
let n = (nums.length - 1) / 2;
let res = (nums[Math.ceil(n)] + nums[Math.floor(n)]) / 2
return res
}
二分查找方法:
// 把较小长度的数组作为nums1
if(nums1.length > nums2.length){
let temp = nums1;
nums1 = nums2;
nums2 = temp;
}
let m = nums1.length, n = nums2.length;
// 分割线左边所有元素需要满足的个数
let totalLeft = Math.floor((m + n + 1) / 2);
// 在num1的区间 [0, m] 里查找恰当的分割线
// 使得分割线左边的元素交叉小于等于分割线右边的元素
// 即nums1[i-1]<=nums2[j] && nums2[j-1]<=nums1[i]
let left = 0, right = m;
while(left < right){
let i = Math.floor(left + (right - left + 1) / 2);
let j = totalLeft - i;
if(nums1[i - 1] > nums2[j]){
// 下一轮搜索区间[left, i -1]
right = i - 1;
}else{
// 下一轮搜索区间[i, right]
left = i;
}
}
let i = left, j = totalLeft - i;
let nums1LeftMax = i === 0? -Infinity : nums1[i-1];
let nums1RightMin = i === m? Infinity : nums1[i];
let nums2LeftMax = j === 0? -Infinity : nums2[j-1];
let nums2RightMin = j === n? Infinity : nums2[j];
if((m + n) % 2 == 1){
return Math.max(nums1LeftMax, nums2LeftMax)
}else{
return (Math.max(nums1LeftMax,nums2LeftMax) + Math.min(nums1RightMin,nums2RightMin)) / 2
}
5、最长回文子串
字符串长度小于为2和3 就不需要检查是否为回文。
let len = s.length;
if(len < 2){
return s
}
let begin = 0, maxLen = 1;
for(let i = 0; i < len - 1; i++){
for(let j = i + 1; j < len; j++){
if(j - i + 1 > maxLen && validPalindromic(i,j)){
maxLen = j - i + 1;
begin = i;
}
}
}
return s.substring(begin, begin + maxLen)
function validPalindromic(left, right){
while(left < right){
if(s[left] !== s[right]){
return false
}
left++;
right--;
}
return true;
}
10、正则表达式匹配
先创建一个二维数组(下标长度分别是s 和 p 的长度 加上1)将二维数组的默认值全设置为false 将dp[0][0] 设置为true,先对s为空串的情况 进行考虑,座位后续dp[i][j] 进行转态转移的判断依据。
然后开始对字符串s 和 p 逐一匹配,匹配过程遇到三种情况,p[j]为普通字符,p[j]为'.'时,p[j]为'*' 这三种情况分别对应着不同的状态转移方程。
let dp = Array(s.length + 1);
for(let i = 0; i < dp.length; i++){
dp[i] = Array(p.length + 1).fill(false)
}
dp[0][0] = true;
for(let j = 1; j < p.length; j++){
if(p.charAt(j) === '*'){
dp[0][j+1] = dp[0][j-1]
}
}
for(let i = 0; i < s.length; i++){
for(let j = 0; j < p.length; j++){
if(p.charAt(j) === '.'){
dp[i+1][j+1] = dp[i][j];
}
if(p.charAt(j) === s.charAt(i)){
dp[i+1][j+1] = dp[i][j]
}
if(p.charAt(j) === '*'){
if(p.charAt(j-1) !== s.charAt(i) && p.charAt(j-1) !== '.'){
dp[i+1][j+1] = dp[i+1][j-1]
}else{
dp[i+1][j+1] = (dp[i+1][j-1] || dp[i+1][j] || dp[i][j+1])
}
}
}
}
return dp[s.length][p.length]
11、盛最多容器的水
双指针,一个指针指向第一个,一个指向最后一个,比较两者大小,谁小的就往里走。
let area = 0;
let l = 0, r = height.length - 1;
while(l < r){
area = Math.max(area, Math.min(height[l],height[r]) * (r - l));
if(height[l] <= height[r]){
++l;
}else{
--r;
}
}
return area
15、三数之和
先对数组进行排序,然后从左到右遍历,利用双指针左指针指向左边,右指针指向右边,目标值设为遍历的数的相反数,首先去重(因为题目说了,不包含重复的三元组),在判断 sum 与target的差值,分三种情况讨论。如果和小于目标值,左指针右移,反之右指针左移。
if(nums.length < 3) return [];
nums.sort((a,b) => a - b);
res = [];
for(let i = 0; i < nums.length - 2; i++){
if(i>0 && nums[i] === nums[i-1]) continue
const target = -nums[i];
let left=i+1, right=nums.length-1;
while(left < right){
const sum = nums[left] + nums[right];
if(sum === target){
res.push([nums[i], nums[left], nums[right]]);
while(left < right && nums[left] === nums[++left]);
while(left < right && nums[right] === nums[--right]);
}else if(sum < target){
left++;
}else{
right--;
}
}
}
return res
15、三数之和
创建一个map 把键值对设置好,如果字符串为空了 则将拼接好的字符加入数组。else语句里 先获得digits的第一个字符,然后进行递归。
if(digits.length === 0) return [];
let map = new Map()
map.set('2', 'abc');
map.set('3','def');
map.set('4','ghi');
map.set('5','jkl');
map.set('6','mno');
map.set('7','pqrs');
map.set('8','tuv');
map.set('9','wxyz');
let res = [];
dfs("", digits);
return res;
function dfs(str, digit){
if(digit.length === 0) res.push(str);
else{
let numstr = map.get(digit[0]);
for(let i = 0; i < numstr.length; i++){
str += numstr[i];
dfs(str, digit.slice(1));
str = str.slice(0, -1);
}
}
}
19、删除链表的倒数第N个节点
对链表进行操作时,常用的技巧就是添加一个哑节点(dummy node),它的next指针指向链表的头节点。删除倒数第n个节点 就是删除第Length-n+1个节点,因为ListNode没有length方法,所以还需要定义。到第Length-n的节点时 它的next指向下一个next。
let dummy = new ListNode(0, head);
let length = getLength(head);
let cur = dummy;
for(let i = 1; i < length - n + 1; ++i){
cur = cur.next;
}
cur.next = cur.next.next;
let res = dummy.next;
return res
function getLength(head){
let length = 0;
while(head !== null){
length++;
head = head.next;
}
return length
}
方法二(快慢指针):需要找到倒数第n个节点,因此可以使用两个指针fast和slow, 同时对链表进行遍历,并且fast比slow超前n个节点,当fast遍历到链表末尾时,slow就恰好处于倒数第n个节点。为了更好的删除第n个节点 slow节点指向哑节点。
let dummy = new ListNode(0, head);
let fast = head, slow = dummy;
for(let i = 0; i < n; i++){
fast = fast.next;
}
while(fast !== null){
fast = fast.next;
slow = slow.next;
}
slow.next = slow.next.next
return dummy.next
20、有效的括号
如果长度是奇数,则返回false。然后创建一个map,键对应 右括号, 值对应左括号,遍历s,将左括号加入栈中,遇到右括号 则取对应的值 是否与左括号相等,匹配一次出栈一次。
const n = s.length;
if(n % 2 === 1){
return false;
}
const pairs = new Map([
[')', '('],
[']', '['],
['}', '{']
])
const stk = [];
for(let ch of s){
if(pairs.has(ch)){
if(!stk.length || stk[stk.length - 1] !== pairs.get(ch)){
return false
}
stk.pop();
}else{
stk.push(ch)
}
}
return !stk.length;
21、合并两个有序链表
先创建一个哑节点,pre指向他,在判断l1、l2 大小,谁小pre.next就指向他。循环结束的条件是 l1 或者 l2 中有一个指向了null。
let prehead = new ListNode(-1);
let pre = prehead;
while(l1 !== null && l2 !== null){
if(l1.val <= l2.val){
pre.next = l1;
l1 = l1.next;
}else{
pre.next = l2;
l2 = l2.next;
}
pre = pre.next
}
pre.next = l1 === null? l2 : l1;
return prehead.next;
递归方法:
如果l1或者l2为空,直接返回另一个链表。否则 比较第一个节点的大小,谁小,那他的下一个节点与剩下的元素merge操作结果合并。
if(l1 === null){
return l2;
}else if(l2 === null){
return l1;
}else if(l1.val < l2.val){
l1.next = mergeTwoLists(l1.next, l2)
return l1;
}else{
l2.next = mergeTwoLists(l1, l2.next);
return l2;
}
22、括号生成
采用树的dfs方法,记录左右括号的数量,根据左右括号的数量来判断序列是否合法以及是否需要剪枝,当右括号数量大于左括号或者,左括号数量大于总数量一半时,进行剪枝。当str的长度等于总括号数量,且左括号等于右括号数量时,push到结果集中。然后不断进行dfs
const res = [];
if(n <= 0) return res;
const dfs = (str, left, right) => {
if(left > n || right > left) return;
if(str.length === 2 * n){
res.push(str);
return;
}
dfs(str + '(', left + 1, right);
dfs(str + ')', left, right + 1);
}
dfs('', 0, 0);
return res
23、合并k个升序链表
与合并两个有序链表类似,只不过加入了分治的方法。
const mergeTwoLists = (l1, l2) => {
if(l1 === null){
return l2
}else if(l2 === null){
return l1;
}else{
if(l1.val < l2.val){
l1.next = mergeTwoLists(l1.next, l2);
return l1;
}else{
l2.next = mergeTwoLists(l1, l2.next);
return l2;
}
}
}
const merge = (lists, left, right) => {
if(left === right){
return lists[left];
}
if(left > right){
return null
}
let mid = Math.floor((left + right) / 2);
return mergeTwoLists(merge(lists, left, mid), merge(lists, mid + 1, right ))
}
return merge(lists, 0, lists.length - 1)
31、下一个排列
我们需要将左边较小的数与右边一个较大的数交换,同时让较小的数尽量靠右,而较大的数尽量较小。当完成交换后,较大数的右边的数需要按照升序重新排列。
代码思路,第一步,从右边向左找,找到第一个左边数小于右边数的。第二步从当前位置开始,也是从右像左找(因为第一步就已经是降序排列了),找到第一个比它大的数,然后交换位置; 第三步 交换位置之后,较小数的右边升序排列。如果第一步没找到,则直接进入第三步。
let i = nums.length - 2;
while(i >= 0 && nums[i] >= nums[i+1]){
i--;
}
if(i >= 0){
let j = nums.length - 1;
while(j >= 0 && nums[i] >= nums[j]){
j--;
}
swap(i, j);
}
reverse(i+1);
function swap(i , j){
let temp = nums[i];
nums[i] = nums[j];
nums[j] = temp;
}
function reverse(start){
let left = start, right = nums.length - 1;
while(left < right){
swap(left, right);
left++;
right--
}
}
32、最长有效括号
方法一: 采用栈的思想,先将 -1 入栈。如果遇到左括号 则其索引入栈,如果遇到右括号,则栈顶元素出栈,此时更新最大长度maxres 等于 当前索引减去当前栈顶的数。直到循环结束。
let maxres = 0;
let stk = [];
stk.push(-1);
for(let i = 0; i < s.length; i++){
if(s[i] === '('){
stk.push(i);
}else{
stk.pop();
if(stk.length === 0){
stk.push(i)
}else{
maxres = Math.max(maxres, i - stk[stk.length - 1]);
}
}
}
return maxres
方法二:正向逆向结合法,利用两个计数器left和right,首先从左到右遍历 遇到左括号left++,遇到右括号right++,当left等于right时,更新maxlength。当right大于left时,将两个更新为0。在从右到左遍历,方法一样,只不过是当left大于right时,将left和right更新为0。
let left = 0 , right = 0, maxlength = 0;
for(let i = 0; i < s.length; i++){
if(s[i] === '('){
left++;
}else{
right++
}
if(left === right){
maxlength = Math.max(maxlength, 2 * right)
}else if(right > left){
left = right = 0;
}
}
left = right = 0;
for(let i = s.length - 1; i >= 0; i--){
if(s[i] === '('){
left++;
}else{
right++
}
if(left === right){
maxlength = Math.max(maxlength, 2 * right)
}else if(left > right){
left = right = 0;
}
}
return maxlength
33、搜索旋转排序数组
暴力解法:
let res = -1;
for(let i = 0; i < nums.length; i++){
if(nums[i] === target){
res = i;
}
}
return res
二分查找:对于有序数组可以采用二分查找,对于本题旋转后局部有序也可以进行二分查找。从中间分割数组 ,看哪部分有序,哪部分不是有序的,然后将目标值target与有序的那部分数组的左右边界进行比较,如果目标值在此区间内,则下一步在有序数组中查找 如果不在则在另一个区间查找。
let len = nums.length;
if(len === 0 ){
return -1;
}
if(len === 1){
return nums[0] === target? 0 : -1;
}
let l = 0, r = len - 1;
while(l <= r){
let mid = Math.floor((l + r) / 2);
if(nums[mid] === target){
return mid;
}
if(nums[l] <= nums[mid]){
if(nums[l] <= target && target < nums[mid]){
r = mid - 1;
}else{
l = mid + 1;
}
}else{
if(nums[mid] < target && target <= nums[r]){
l = mid + 1;
}else{
r = mid - 1;
}
}
}
return -1;
34、在排序数组中查找元素的第一个和最后一个位置
与上面一题方法类似,采用二分法解决,不同的是在于此题每个数可以出现多次。所以需要一个 i 和 j 记录 mid的前面与后面是否与目标值相等,注意返回的时候,索引需要加一或减一。
let len = nums.length;
if(len === 0){
return [-1, -1]
}
let l = 0, r = len - 1;
while(l <= r){
let mid = Math.floor((l + r) / 2);
if(nums[mid] === target){
let i = j = mid;
while(--i >= 0 && nums[i] === target);
while(++j < len && nums[j] === target );
return [i+1, j-1]
}
if(target < nums[mid]){
r = mid -1;
}
if(nums[mid] < target){
l = mid + 1;
}
}
return [-1, -1];
39、组合总和
对于这类寻找所有可行性的解,可以采用搜索回溯的方法。定义递归函数dfs(target,combine,idx),表示当前在数组candidates数组的第idx位,还剩target要组合,已经组合的列表为combine。递归终止的条件是 target<=0 或者candidates数组元素被全部用完。
const res = [];
const dfs = (target, combine, idx) => {
if(idx === candidates.length){
return;
}
if(target === 0){
res.push(combine);
return;
}
dfs(target, combine, idx + 1);
if(target - candidates[idx] >= 0 ){
dfs(target - candidates[idx], [...combine, candidates[idx]],idx)
}
}
dfs(target, [], 0);
return res;
42、接雨水
方法一(暴力解法):对于数组中每个元素,找出下雨后能到达的最高位置,等于两边最大高度的较小值减去当前高度的值,
let res = 0;
let len = height.length;
for(let i = 1; i < len - 1; i++){
let max_left = 0, max_right = 0;
for(let j = i; j >= 0; j--){
max_left = Math.max(max_left, height[j]);
}
for(let j = i; j < len; j++){
max_right = Math.max(max_right, height[j]);
}
res += Math.min(max_left, max_right) - height[i];
}
return res;
方法二(双指针法):此方法有个思想就是当左边最大值小于右边最大值时, 则积水高度由左边决定,反之则由右边的最大值决定。如果当前高度大于或等于最大值时,更新最大值
let l = 0, r = height.length - 1;
let res = 0;
let l_max = 0, r_max = 0;
while(l < r){
if(height[l] < height[r]){
if(height[l] >= l_max){
l_max = height[l];
}else{
res += (l_max - height[l]);
}
l++;
}else{
if(height[r] >= r_max){
r_max = height[r];
}else{
res += (r_max - height[r]);
}
r--
}
}
return res
46、全排列
采用dfs + 回溯的方法,used用来判断 重复使用的数字。为true则跳过,为false则将nums[i]加入到templist中,然后进行回溯。
let res = [], templist = [];
let used = new Array(nums.length).fill(false);
const dfs = () => {
if(templist.length === nums.length){
res.push([...templist])
return;
}
for(let i = 0; i < nums.length; i++){
if(used[i]) continue
templist.push(nums[i])
used[i] = true
dfs();
path.pop()
used[i] = false
}
}
dfs();
return res
48、旋转图像
方法一 (辅助数组),创建一个n行n列的全0数组,旋转之后发现,第一行的元素出现在倒数第一列的位置,第一行的第x列元素出现在倒数第一列的第x行。所以公式就是 new_matirx[j][n-i-1] = matrix[i][j] 。
const len = matrix.length;
const matrix_new = new Array(len).fill(0).map(() => new Array(len).fill(0));
for(let i = 0; i < len; i++){
for(let j = 0; j < len; j++){
matrix_new[j][len - i - 1] = matrix[i][j];
}
}
for(let i = 0; i < len; i++){
for(let j = 0; j < len; j++){
matrix[i][j] = matrix_new[i][j]
}
}
方法二(原地旋转):结婚方法一的思路, 在原地更改,matirx[j][n-i-1] = matrix[i][j]那么先用临时变量temp= matirx[j][n-i-1],考虑一下此时原本在matirx[j][n-i-1]位置的数跑到的matrix[n-i-1][n-j-1]上,那么我们就令temp=matrix[n-i-1][n-j-1], matrix[n-i-1][n-j-1]=matirx[j][n-i-1],matirx[j][n-i-1]=matrix[i][j],再往后考虑,最终回到matrix[i][j]上。
const len = matrix.length;
for(let i = 0; i < Math.floor(len / 2); i++){
for(let j = 0; j < Math.floor((len + 1) / 2); j++){
const temp = matrix[i][j];
matrix[i][j] = matrix[len-j-1][i];
matrix[len-j-1][i] = matrix[len-i-1][len-j-1];
matrix[len-i-1][len-j-1] = matrix[j][len-i-1];
matrix[j][len-i-1] = temp;
}
}
49、字母异位词分组
方法一(排序): 先对数组进行遍历,对于每个字符串得到该字符串所在的一组字母异位词的标志,因为异位词包含的字母相同,所以排序之后 字符串是相同的,因为可以将它设置为键,值对应的是原字符串构成的列表。
const map = new Map();
for(let str of strs){
let array = Array.from(str);
array.sort();
let key = array.toString();
let list = map.get(key)? map.get(key) : new Array();
list.push(str);
map.set(key, list);
}
return Array.from(map.values());
53、最大子序和
方法一(动态规划):其思想就是,若前一个元素大于0,就将其加到当前元素上,否则就舍弃前面数列,然后取列表中最大值。
let pre = 0, maxRes = nums[0];
nums.forEach((x) => {
pre = Math.max(pre + x, x);
maxRes = Math.max(maxRes, pre);
})
return maxRes;
54、 跳跃游戏
对于数组中任意一个位置y,只要存在一个x,他本身可以到达,并且它跳跃的最大长度是x+nums[x],这个值大于等于y,那么位置y也可以到达。
let len = nums.length;
let max_reach = 0;
for(let i = 0; i < len; i++){
if(i <= max_reach){
max_reach = Math.max(max_reach, i + nums[i]);
if(max_reach >= len - 1){
return true;
}
}
}
return false
56、合并区间
先对数组进行排序,按照区间的左端点进行排序,然后令第一个区间为pre,遍历数组,比较当前区间的左端点与pre区间的右端点,若当前区间左端点比pre区间右端点小,则更新pre区间的右端点,为当前区间右端点和pre区间右端点的最大值,否则将pre区间加入到结果列表中,更新pre为当前区间。
intervals.sort((a, b) => a[0] - b[0]);
let pre = intervals[0];
let res = [];
for(let i = 0; i < intervals.length; i++){
let cur = intervals[i];
if(cur[0] > pre[1]){
res.push(pre)
pre = cur;
}else{
pre[1] = Math.max(pre[1], cur[1]);
}
}
res.push(pre);
return res;
62、不同路径
用path[i][j]表示从左上角走到(i,j)的路径数量,由于得向下或者向右走,所以动态规划方程为 path(i, j) = path(i - 1)( j ) + path( i )( j - 1) 但是注意 path[0][j] 与 path[i][0]都设置为1。
const path = new Array(m).fill(0).map(() => new Array(n).fill(0));
for(let i = 0; i < m; i++){
path[i][0] = 1;
}
for(let j = 0; j < n; j++){
path[0][j] = 1;
}
for(let j = 1; j < n; j++){
for(let i = 1; i < m; i++){
path[i][j] = path[i-1][j] + path[i][j-1];
}
}
return path[m-1][n-1];
64、最小路径和
与上一题类似,改一下动态规划状态转移方程
let m = grid.length, n = grid[0].length;
let path = new Array(m).fill(0).map(() => new Array(n).fill(0));
path[0][0] = grid[0][0];
for(let i = 1; i < m; i++){
path[i][0] = path[i-1][0] + grid[i][0];
}
for(let j = 1; j < n; j++){
path[0][j] = path[0][j-1] + grid[0][j]
}
for(let i = 1; i < m; i++){
for(let j = 1; j < n; j++){
path[i][j] = Math.min(path[i-1][j], path[i][j-1])+grid[i][j];
}
}
return path[m-1][n-1];
70、爬楼梯
也可以考虑成动态规划的方法,思路同上面类似,就是要找出状态转移方程
let dp = new Array(n).fill(0);
dp[0] = 1, dp[1] = 2
for(let i = 2; i < n; i++){
dp[i] = dp[i-1] + dp[i-2];
}
return dp[n-1]
72、编辑距离
动态规划:dp[i][j]表示word1的第i的单词 到 word2的第j个单词的编辑距离。
如果word1的第i的单词 和 word2的第j个单词相等,那么dp[i][j] = dp[i-1][j-1]
否则 dp[i][j] = 1 + min(dp[i-1][j], dp[i][j-1], dp[i-1][j-1]) 分别对应着插入、删除、替换的操作。
let m = word1.length, n = word2.length;
if(m * n === 0) return m + n;
// dp数组
let dp = new Array(m+1).fill(0).map(() => new Array(n+1).fill(0));
// 边界初始化
for(let i = 0; i < m+1; i++){
dp[i][0] = i;
}
for(let j = 0; j < n+1; j++){
dp[0][j] = j;
}
// 计算所有dp值
for(let i = 1; i < m+1; i++){
for(let j = 1; j < n+1; j++){
let left = dp[i-1][j] + 1;
let down = dp[i][j-1] + 1;
let left_down = dp[i-1][j-1];
if(word1.charAt(i - 1) !== word2.charAt(j-1)){
left_down += 1;
}
dp[i][j] = Math.min(left, down, left_down);
}
}
return dp[m][n]
76、最下覆盖子串
先建立一个hash表,键为t的元素,值为t中每个元素的个数.。让needType等于需要的字符串个数,先动右指针,让 当前滑动窗口 恰好包含t中所有字符,比较当前字符串与原字符串长度,然后移动右指针,使得哈希表又出现新的需要的字符。
let map = new Map();
for(let i = 0; i < t.length; i++){
map.set(t[i], map.has(t[i])? map.get(t[i])+1 : 1);
}
let needType = map.size;
let l = 0, r = 0;
let res = '';
while(r < s.length){
const str = s[r];
if(map.has(str)){
map.set(str, map.get(str)-1);
if(map.get(str) === 0) needType-=1;
}
while(needType === 0){
const str2 = s[l];
let new_res = s.slice(l,r+1);
if(!res || new_res.length < res.length) res = new_res;
if(map.has(str2)){
map.set(str2,map.get(str2)+1);
if(map.get(str2) === 1) needType +=1
}
l++;
}
r++
}
return res;
78、子集
回溯思想,每种元素存在两种状态取/不取,分别递归每一层
const cur = [];
const res = [];
const dfs = (i) => {
if(i === nums.length){
res.push(cur.slice());
return;
}
cur.push(nums[i])
dfs(i+1);
cur.pop();
dfs(i+1)
}
dfs(0)
return res
79、单词搜索
const m = board.length;
const n = board[0].length;
// used 用来记录已经访问过的点
const used = new Array(m).fill(false).map(() => new Array(n).fill(false));
// canFind 判断当前点是否是目标路径上的点
const canFind = (row, col, i) => {
if(i === word.length){
return true; //递归的出口,word遍历完了
}
if(row < 0 || row >= m || col < 0 || col >= n){//越界了
return false;
}
if(used[row][col] || board[row][col] !== word[i]){
return false;
}
// 排除所有false情况,可以继续考察递归
used[row][col] = true
const canFindRest = canFind(row + 1, col, i+1) || canFind(row-1, col, i+1) || canFind(row, col+1, i+1) || canFind(row, col-1, i+1);
if(canFindRest) return true //基于当前点,可以为剩下字符找到路径
used[row][col] = false;
return false
}
for(let i = 0; i < m; i++){
for(let j = 0; j < n; j++){
if(canFind(i, j, 0)){
return true;
}
}
}
return false;
84、柱状图中最大的矩形、
方法一(暴力解法)超时 就是找到每个柱子,以他为高度所能构成的面积,找出最大值。对于每根柱子那就要往左往右寻找。比他高度高的。
const len = heights.length;
if(len === 0 ) return 0;
let res = 0
for(let i = 0; i < len; i++){
let left = i;
let curHeight = heights[i]
while(left>0 && heights[left-1] >= curHeight){
left--
}
let right = i;
while(right<len-1 && heights[right+1] >= curHeight){
right++
}
let width = right-left+1;
res = Math.max(res, width*curHeight)
}
return res
方法二:(栈)栈中保存下标
当栈不为空时,出栈条件是,看到的元素高度严格小于栈顶元素高度,这样也可以计算出栈顶元素的最大宽度
此时的宽度的定义就是 当前遍历到的下标与出栈后新栈的栈顶元素下标之差再减1。
let res = 0
let stack = [];
let new_heights = new Array(heights.length + 2).fill(0);
for(let i = 1; i < heights.length + 1; i++) new_heights[i] = heights[i-1];
for(let i = 0; i < new_heights.length; i++){
while(stack.length > 0 && new_heights[stack[stack.length-1]] > new_heights[i]){
let cur = stack.pop()
res = Math.max(res, (i-stack[stack.length-1]-1)*new_heights[cur])
}
stack.push(i)
}
return res
85、最大矩形
暴力解法:left保存每行的每个元素 左边是1的个数。然后在遍历数组 从下到上计算最大面积
const m = matrix.length;
if(m === 0) return 0
const n = matrix[0].length
const left = new Array(m).fill(0).map(() => new Array(n).fill(0))
for(let i =0;i < m; i++){
for(let j = 0; j < n; j++){
if(matrix[i][j] === '1'){
left[i][j] = (j===0? 0 : left[i][j-1]) + 1;
}
}
}
let res = 0
for(let i = 0; i < m; i++){
for(let j = 0; j < n; j++){
if(matrix[i][j] === '0'){
continue;
}
let width = left[i][j];
let area = width;
for(let k = i - 1; k >= 0; k--){
width = Math.min(width, left[k][j]);
area = Math.max(area, (i - k + 1)*width)
}
res = Math.max(res, area)
}
}
return res
方法二(单调栈)对每一列使用 基于柱状图法
const m = matrix.length;
if(m === 0) return 0
const n = matrix[0].length
const left = new Array(m).fill(0).map(() => new Array(n).fill(0))
for(let i =0;i < m; i++){
for(let j = 0; j < n; j++){
if(matrix[i][j] === '1'){
left[i][j] = (j===0? 0 : left[i][j-1]) + 1;
}
}
}
let res = 0;
for(let j = 0; j < n; j++){//对于每一列使用基于柱状图法
let new_col = new Array(m+2).fill(0)
for(let i=1; i<m+1; i++){
new_col[i] = left[i-1][j]
}
let stack = [];
for(let i = m + 1; i >= 0; i--){
while(stack.length && new_col[stack[stack.length-1]] > new_col[i]){
let cur = stack.pop();
res = Math.max(res, (stack[stack.length-1]-i-1)*new_col[cur])
}
stack.push(i)
}
}
return res
94、二叉树的中序遍历
方法一(递归)递归终止条件,遇到空节点。先遍历左边节点,加入到结果中,在遍历右边节点。
const res = [];
const inorder = (root) => {
if(!root){
return;
}
inorder(root.left);
res.push(root.val);
inorder(root.right);
}
inorder(root);
return res
方法二(迭代)
const res = [];
const stk = [];
while(root || stk.length){
while(root){
stk.push(root)
root = root.left;
}
root = stk.pop();
res.push(root.val);
root = root.right
}
return res
方法三(Morris遍历算法)
1、x无左孩子,x加入结果,x=x.right;
2、x有左孩子,找predecessor(即左子树中序遍历的最后一个节点)
①predecessor右孩子为空,右孩子指向x ,x = x.left
②predecessor右孩子不为空,x加入结果, x = x.right
const res = []
let predecessor = null;
while(root){
if(root.left){
predecessor = root.left;
while(predecessor.right && predecessor.right !== root){
predecessor = predecessor.right;
}
if(!predecessor.right){
predecessor.right = root;
root = root.left
}else{
res.push(root.val);
predecessor.right = null;
root = root.right
}
}else{
res.push(root.val);
root = root.right
}
}
return res
96、不同的二叉搜索树
G表示以n为长度的二叉搜索树的个数,F(i,n)表示以i为根节点的长度为n的二叉树的个数
可得公式
又可以进一步推导 F(i, n) = G(i-1) * G(n-i),所以可得
const G = new Array(n+1).fill(0);
G[0] = 1;
G[1] = 1;
for(let i = 2; i <=n; i++){
for(let j = 1; j <= i; j++){
G[i] += G[j-1] * G[i-j];
}
}
return G[n];
98、验证二叉搜索树
方法一(递归):定义一个递归函数,考虑以root为根的子树,判断子树的所有节点的值是否都在(lower, upper)范围内,不在则直接返回false。当调用左子树时,上界upper设置为root.val,当递归调用右子树时,下界设置为root.val。
const bst = (root, lower, upper) => {
if(root === null){
return true;
}
if(root.val <= lower || root.val >= upper){
return false;
}
return bst(root.left, lower, root.val) && bst(root.right, root.val, upper)
}
return bst(root, -Infinity, Infinity);
方法二(中序遍历):在中序遍历的时候检查当前节点的值是否大于前一个中序遍历得到的值。
let stack = [];
let lower = -Infinity;
while(stack.length || root !== null){
while(root !== null){
stack.push(root);
root = root.left;
}
root = stack.pop();
if(root.val <= lower){
return false;
}
lower = root.val;
root = root.right;
}
return true;
101、对称二叉树
(递归法)p 指针和 q 指针一开始都指向这棵树的根,随后 p 右移时,q 左移,p 左移时,q 右移。每次检查当前 p 和 q节点的值是否相等,如果相等再判断左右子树是否对称。
if(p === null && q === null){
return true;
}
if(p === null || q === null){
return false;
}
return p.val === q.val && check(p.left, q.right) && check(p.right, q.left)
}
return check(root, root)
102、二叉树的层序遍历
首先根元素入列,当队列不为空时候,求当前队列长度si,依次从队列中取si个元素,然后进入下一次迭代。
const res = []
if(!root){
return res;
}
const stack = [];
stack.push(root);
while(stack.length !== 0){
const currentLevelSize = stack.length;
res.push([]);
for(let i = 1; i <= currentLevelSize; i++){
const node = stack.shift();
res[res.length - 1].push(node.val);
if(node.left) stack.push(node.left);
if(node.right) stack.push(node.right);
}
}
return res
104、二叉树的最大深度
(递归法)
if(root === null){
return 0;
}else{
let leftHeight = maxDepth(root.left);
let rightHeight = maxDepth(root.right);
return Math.max(leftHeight, rightHeight) + 1;
}