刷题笔记
时间复杂度
时间使用的上界 主要是要看操作元素多少次 不是有双重循环就是O(n2)
递归
看的算法图解,里面写的递归只是一种让程序看起来更好理解的写法。不用太害怕。
算法图解里面写的分而治之的思想,
数组
滑动窗口
思想:
- 当不满足条件时,拓展右边界,当满足条件时,缩短左边界,最后得到一个解并暂存
- 循环第一步,又得到一个解,将其与第一个解相对比,得到最优解并暂存,以此类推。
题解
var minSubArrayLen = function(target, nums) {
let right=0
let sum=0
let left=0
//len为子数组长度,这里加1是为了在判断的时候可以比原来数组要大 然后返回
let len=nums.length+1
while(right<=nums.length){
sum+=nums[right]
right++
while(sum>=target){
if(right-left<len){
len=right-left
}
sum-=nums[left]
left++
}
}
return len>nums.length?0:len
};
做这题的时候注意边界的处理。
注意 这样写的时间复杂度是O(n)
不要看到双循环就无脑说O(n2)
,只对每个操作数操作了两次所以是O(2n)
!
螺旋数组
其实这题就是寻找不变量找规律 仔细找找就好了
使用四个循环,模拟螺旋数组生成的过程,注意不变量和边界。
题解:
递归
链表具有天然的递归性。
可以将原链表看成头节点 1 后挂接一个更短的链表:
继续拆解,直到无法继续拆分:
var removeElements = function(head, val) {
if (head === null) {
return head;
}
head.next = removeElements(head.next, val);
return head.val === val ? head.next : head;
};
迭代
/**
* @param {number} n
* @return {number[][]}
*/
var generateMatrix = function(n) {
let i=0,j=0,r=Math.floor(n/2),t=0,num=1
let arr=new Array(n).fill(0).map(() => new Array(n).fill(0));
if(n%2!==0){
arr[r][r]=n**2
}
while(t<r){
for(i=t;i<n-t-1;i++){
arr[t][i]=num++
}
for(j=t;j<n-t-1;j++){
arr[j][n-t-1]=num++
}
for(;i>t;i--){
arr[n-t-1][i]=num++
}
for(;j>t;j--){
arr[j][t]=num++
}
t++
}
return arr
};
栈
有效的括号 20
题解:
这题主要是使用map存储,使用栈的特性 先进后出
看栈顶元素能不能跟数组的元素匹配上能匹配上就弹出,不符合就将数组元素shift压入栈中
var isValid = function(s) {
let stack=[]
let arr=s.split('')
let map=new Map()
map.set('{','}')
map.set('[',']')
map.set('(',')')
let len=arr.length
for(let i=0;i<len;i++){
let el=arr.pop()
let stackEl=stack[stack.length-1]
if(stackEl&&stackEl===map.get(el)){
stack.pop()
}else{
stack.push(el)
}
}
if(stack.length===0) return true
return false
};
链表
链表常用方法技巧
- 双指针
- 虚拟头结点
- 递归
移除链表 题目203
做这题的时候一定要注意第一个元素的处理。第一个元素有个头指针指着太麻烦了!!
我看了别人的解答,多加了一个充当头结点,这样就将原来的节点变成普通节点了,不需要再对头指针另外处理了。
题解:
/**
* Definition for singly-linked list.
* function ListNode(val, next) {
* this.val = (val===undefined ? 0 : val)
* this.next = (next===undefined ? null : next)
* }
*/
/**
* @param {ListNode} head
* @param {number} val
* @return {ListNode}
*/
var removeElements = function(head, val) {
if(head===null) return head
head=new ListNode(0,head)
let cur=head
while(cur.next){
if(cur.next.val===val){
cur.next=cur.next.next
continue
}
cur=cur.next
}
return head.next
};
设计链表 题目707
做这题的时候要注意链表的遍历的边界条件。当迭代条件为cur===null
时,最后一个节点会进入循环,cur.next===null
最后一个节点不进入循环。
题解:
var MyLinkedList = function () {
this.head = null;
};
MyLinkedList.prototype.node = function (val = undefined, next = null) {
this.val = val;
this.next = next;
};
/**
* @param {number} index
* @return {number}
*/
MyLinkedList.prototype.get = function (index) {
let cur = this.head;
let i = 0;
while (cur) {
if (i === index) {
return cur.val;
}
cur = cur.next;
i++;
}
return -1;
};
/**
* @param {number} val
* @return {void}
*/
MyLinkedList.prototype.addAtHead = function (val) {
let newNode = new this.node(val);
newNode.next = this.head;
this.head = newNode;
};
/**
* @param {number} val
* @return {void}
*/
MyLinkedList.prototype.addAtTail = function (val) {
if (this.head === null) {
this.head = new this.node(val);
return
}
let cur = this.head;
while (cur.next) {
cur = cur.next;
}
cur.next = new this.node(val);
return
};
/**
* @param {number} index
* @param {number} val
* @return {void}
*/
MyLinkedList.prototype.addAtIndex = function (index, val) {
let cur = this.head;
let i = 1;
let newNode = new this.node(val);
// let prev=this.head
if (index <= 0) {
newNode.next = this.head;
this.head = newNode;
} else {
while (cur){
if (i === index) {
newNode.next = cur.next;
cur.next = newNode;
return true;
}
// prev=cur
cur = cur.next;
i++;
}
}
return false;
};
/**
* @param {number} index
* @return {void}
*/
MyLinkedList.prototype.deleteAtIndex = function (index) {
if (index < 0 || this.head === null) return;
let i = 1;
let cur = this.head;
if(index===0){
this.head=this.head.next
}
while (cur.next) {
if (i === index) {
cur.next = cur.next.next;
return
}
i++;
cur = cur.next;
}
};
翻转链表 题目206
题解:
递归
-
递归要注意返回边界条件,我之前的边界条件就没写好 只处理了
head===null
的特殊情况 没有真正的考虑递归 -
之前写的
reverseList(head.next).next
报underfined
是因为返回值 不写返回值在期待什么、、、老笨比了、、 -
递归什么是变的 什么是不变的 在这个递归里面 其实就是在栈里面遍历head 栈:1-》2-》3-》4-》5
然后从后往前遍历 head指针移动 5-》4-》3-》2-》1
其实newHead没变 变的是head
var reverseList = function(head) {
if(head===null||head.next===null) return head
let newHead=reverseList(head.next)
head.next.next=head
head.next=null
//注意 非边界情况的返回 怎么可以不写返回值啊!!
return newHead
};
====2022.9.7
今天重新写了一遍 记忆出现偏差了 然后导致没写出来 对递归是明白的 但是没完全明白 我发现我有点恐惧递归返回什么这个东西
递归返回什么其实就是执行栈这一层退出 给下一层(也就是下一个即将成为栈顶的栈格带回什么) 也可以从最后要返回什么考虑
其实我大概知道要返回一个什么,就是要返回指向最后一个节点的head指针 本来想着是给返回newHead=head
但是我会在每次递归都会执行一遍 最后head就指向1,就相当于没有这个newHead 还是原来的头指针的指向 想要能透传的newHead 看题解是 newHead=reverseList(head.next)
有点像vue的父子组件 一层一层传过去 也是用到了 我也想要退出栈格时给下一层返回什么的思路
===
迭代
迭代还是边界设定 最好带入一下最后一次循环试试看 看看行不行
重写了一下 一定要自己做一下循环 不然老卡住
其实就是快慢指针 但是注意链表的特殊性 next改变之后后面的会找不到 所以要用别的东西存一下
var reverseList = function(head) {
if(head===null) return head
let cur=head
let prev=null
while(cur){
head=head.next
cur.next=prev
prev=cur
cur=head
}
head=prev
return head
};
删除链表的倒数第 N 个结点 题目19
题解:
迭代
/**
* Definition for singly-linked list.
* function ListNode(val, next) {
* this.val = (val===undefined ? 0 : val)
* this.next = (next===undefined ? null : next)
* }
*/
/**
* @param {ListNode} head
* @param {number} n
* @return {ListNode}
*/
var removeNthFromEnd = function(head, n) {
if(head===null) return head
let ret=new ListNode(0,head)
let cur=head
let num=0
while(cur){
cur=cur.next
num++
}
if(num<n) return head
cur=ret
for(let i=0;i<num-n;i++){
cur=cur.next
}
cur.next=cur.next.next
return ret.next
};
快慢指针
fast指针比slow走快n+1个 当fast指向null时,slow指向倒数n+1个 然后再移除链表节点
var removeNthFromEnd = function(head, n) {
if(head===null) return head
let ret=new ListNode(0,head)
let slow=ret
let fast=null
let cur=ret
let i=0
while(cur){
if(i===n+1) break
cur=cur.next
i++
}
fast=cur
cur=ret
while(fast){
fast=fast.next
slow=slow.next
}
slow.next=slow.next.next
return ret.next
};
相交链表 160
题解
var getIntersectionNode = function(headA, headB) {
if(headB===null||headA===null) return null
let p1=headA
let p2=headB
while(p1!==p2){
p1=p1===null?headB:p1.next
p2=p2===null?headA:p2.next
}
return p1
};
哈希表
哈希表问题常用的三种数据结构:
- 数组
- set
- map
有效的字母异位词 题目242
题解:
排序
根据题目意思 异位次其实就是顺序不一样,但是数量一样的字母组合
先看长度是不是一样 然后直接排序如果排序之后字符串是一样的那肯定是一样的
var isAnagram = function(s, t) {
return s.length == t.length && [...s].sort().join('') === [...t].sort().join('')
};
哈希表
维护一个字母-出现次数的哈希表 然后比较两个字符串的表
下面是官方题解 维护了一个26字母的数组 先存s的字母次数 然后遍历t
var isAnagram = function(s, t) {
if (s.length !== t.length) {
return false;
}
const table = new Array(26).fill(0);
for (let i = 0; i < s.length; ++i) {
table[s.codePointAt(i) - 'a'.codePointAt(0)]++;
}
for (let i = 0; i < t.length; ++i) {
table[t.codePointAt(i) - 'a'.codePointAt(0)]--;
if (table[t.codePointAt(i) - 'a'.codePointAt(0)] < 0) {
return false;
}
}
return true;
};
两个数组的交集 题目349
题解:
排序+双指针
初始时,两个指针分别指向两个数组的头部。每次比较两个指针指向的两个数组中的数字,如果两个数字不相等,则将指向较小数字的指针右移一位,如果两个数字相等,且该数字不等于 pre ,将该数字添加到答案并更新 pre变量,同时将两个指针都右移一位。当至少有一个指针超出数组范围时,遍历结束
var intersection = function(nums1, nums2) {
nums1.sort((x, y) => x - y);
nums2.sort((x, y) => x - y);
const length1 = nums1.length, length2 = nums2.length;
let index1 = 0, index2 = 0;
const intersection = [];
while (index1 < length1 && index2 < length2) {
const num1 = nums1[index1], num2 = nums2[index2];
if (num1 === num2) {
// 保证加入元素的唯一性
if (!intersection.length || num1 !== intersection[intersection.length - 1]) {
intersection.push(num1);
}
index1++;
index2++;
} else if (num1 < num2) {
index1++;
} else {
index2++;
}
}
return intersection;
};
复杂度分析
时间复杂度:O(mlogm+nlogn),其中 m和 n 分别是两个数组的长度。
- 对两个数组排序的时间复杂度分别是 O(mlogm+nlogn) (sort是快排
- 双指针寻找交集元素的时间复杂度是 O(m+n)
我自己写的是利用js的set
遍历短的集合,求交集(交谁谁小
var intersection = function(nums1, nums2) {
let set1=new Set(nums1)
let set2=new Set(nums2)
let set=set1.size>set2.size?set1:set2
let result=new Set()
for(let i of set){
if(set1.has(i)===set2.has(i)) result.add(i)
}
return Array.from(result)
};
两数之和 题目1
题解:
之前用暴力双循环也做出来了,这次写一下哈希的。
哈希的主要想法就是将数组里面的数存起来,然后遍历到target-nums[i]
的时候返回这两个数的索引
遍历完整个数组都没有找到target-nums[i]
的话就直接返回空数组
var twoSum = function(nums, target) {
let set=new Set()
for(let i=0;i<nums.length;i++){
if(set.has(target-nums[i])) return [i,nums.indexOf(target-nums[i])]
set.add(nums[i])
}
return []
};
- 时间复杂度从O(n2)降到O(n)。
- 空间复杂度O(n),主要是哈希表的空间开销。
快乐数 题目202
题解:
找规律
其实返回真的很好想,但是返回为假的很难。
从题目的示例2
中找到规律,4
->16
->37
->58
->89
->145
->42
->20
->4
。最后循环会循环到重复,像这种找数的可以设置一个哈希表来找。
补充:其实这里除了得到1
,一直循环到重复,还有可能是值会越来越大,最后接近无穷大。
Digits | Largest | Next |
---|---|---|
1 | 9 | 81 |
2 | 99 | 162 |
3 | 999 | 243 |
4 | 9999 | 324 |
13 | 9999999999999 | 1053 |
所以第三种情况不会出现。
var isHappy = function(n) {
let str=new String(n)
let strArr=str.split('')
let sumSet=new Set()
while(true){
let sum=0
for(let i of strArr){
sum+=i**2
}
if(sum===1) return true
if(sumSet.has(sum)) return false
sumSet.add(sum)
strArr=new String(sum).split('')
}
};
链表成环 快慢指针
将这些和值看成链表,无限循环就是因为链表成环了。那么问题就转化成查看链表是否成环。
这里用到的是快慢指针,快指针比慢指针先走一步,并且每次走两步,慢指针从第0个开始走,然后每次只走一步。
如果链表成环的话,快指针会比慢指针多走n圈,然后追上慢指针。如果没有,快指针会一直在前面,并且等于1.
var isHappy = function(n) {
function getNext(n){
let sum=0
let str=new String(n).split('')
for(i of str){
sum+=i**2
}
return sum
}
let slow=n
let fast=getNext(n)
while(fast!==slow&&fast!==1){
slow=getNext(slow)
fast=getNext(getNext(fast))
}
return fast===1
};
赎金信 题目383
题解:
哈希表
统计字符
其实这个题目可以理解成是比对两个字符串的字母出现次数。
var canConstruct = function(ransomNote, magazine) {
if (ransomNote.length > magazine.length) {
return false;
}
const cnt = new Array(26).fill(0);
for (const c of magazine) {
cnt[c.charCodeAt() - 'a'.charCodeAt()]++;
}
for (const c of ransomNote) {
cnt[c.charCodeAt() - 'a'.charCodeAt()]--;
if(cnt[c.charCodeAt() - 'a'.charCodeAt()] < 0) {
return false;
}
}
return true;
};
双指针
先排序 然后遍历ransomNote
如果一样就一起跳下一个 一个不一样就magazine
跳下一个
var canConstruct = function(ransomNote, magazine) {
let rArr=ransomNote.split('').sort()
let mArr=magazine.split('').sort()
for(let r=0,m=0;r<rArr.length;){
if(rArr[r]===mArr[m]){
r++
m++
}else{
m++
}
if(m>mArr.length) return false
}
return true
};
四数相加 题目454
题解:
分组+哈希表
这题用暴力法不行,不能用四重循环,超时了。
我看官方的题解是采用分组+哈希表
先分成2组,然后将第1组的和和次数存起来,然后遍历第二组。
var fourSumCount = function(A, B, C, D) {
const countAB = new Map();
//在A和B中取出两个数的组合,将这两个数的和作为键,出现次数作为值加入哈希表中,
A.forEach(u => B.forEach(v => countAB.set(u + v, (countAB.get(u + v) || 0) + 1)));
let ans = 0;
for (let u of C) {//循环C、D
for (let v of D) {
if (countAB.has(-u - v)) {//判断C和D中是否存在两个数的和 加 AB中的俩元素的和正好是0
ans += countAB.get(-u - v);//累加组合数
}
}
}
return ans;
};
树
二叉树的最大深度 104
题解:
递归就是将树放到栈里面遍历,每个栈格都是函数环境的快照.
var maxDepth = function(root) {
if(root===null) return 0
return Math.max(maxDepth(root.left),maxDepth(root.right))+1
};
时间复杂度:O(n)
空间复杂度:O(height),其中 height表示二叉树的高度。递归函数需要栈空间,而栈空间取决于递归的深度,因此空间复杂度等价于二叉树的高度。
平衡二叉树 110
题解:
感觉还是局部到整体,一棵树是平衡二叉树,那么它的子树都是平衡二叉树.
var isBalanced = function(root) {
if(root===null) return true
let result=true
if(Math.abs(maxDepth(root.left)-maxDepth(root.right))<=1&&isBalanced(root.left)&&isBalanced(root.right)){
result=true
}
else{
result=false
}
function maxDepth(root){
if(root===null) return 0
let left=maxDepth(root.left)
let right=maxDepth(root.right)
return Math.max(left,right)+1
}
return result
};
时间复杂度:O(n2)
空间复杂度:O(n)
二叉树的直径 543
题解:
根据二叉树的直径这个概念,其实可以理解成左边子树跟右边子树的加起来.然后找到它的最大值
还是用递归的想法,使用递归遍历每一个子节点,找到其左子树和右子树的深度的最大值.
var diameterOfBinaryTree = function(root) {
if(root===null) return 0
let max=0
maxDepth(root)
function maxDepth(root){
if(root===null) return 0
let left=maxDepth(root.left)
let right=maxDepth(root.right)
max=Math.max(max,left+right)
return Math.max(left,right)+1
}
return max
};
时间复杂度:O(n)
空间复杂度:O(height),其中 height表示二叉树的高度。递归函数需要栈空间,而栈空间取决于递归的深度,因此空间复杂度等价于二叉树的高度。
合并二叉树 617
题解:
深度优先,其实跟之前是一样的.一次遍历两棵树,然后将节点相加
这一题,我主要是没搞明白 返回什么 但是要注意后面返回的位置就是在后序遍历的位置 在遍历完左右子树之后干什么 也是做完这个节点干什么事情 返回什么
var mergeTrees = function(root1, root2) {
if(root1===null) return root2
if(root2===null) return root1
let node=new TreeNode(root1.val+root2.val)
node.left=mergeTrees(root1.left,root2.left)
node.right=mergeTrees(root1.right,root2.right)
return node
};
路径总和 112
题解:
递归
首先这道题的路径已经确定就是根节点到叶子节点 然后确定现在是不是遍历到叶子节点了 注意往父节点回跳的时候要减掉子节点的值
var hasPathSum = function(root, targetSum) {
if(root===null) return false
let sum=0
let flag=false
trans(root)
function trans(root){
if(root===null) return root
sum+=root.val
if(sum===targetSum&&!root.left&&!root.right) flag=true
trans(root.left)
if(root.left) sum-=root.left.val
trans(root.right)
if(root.right) sum-=root.right.val
return sum
}
return flag
};
时间复杂度:O(N)
空间复杂度:O(树的高度)
另一棵树的子树 572
题解:
一棵树是另一个树的子树可以分为三种情况:
- 这棵树是另外一棵树的左子树
- 这棵树是另外一棵树的右子树
- 两棵树相同
将函数分成了两层 第一层函数用于遍历root树,然后深度遍历每个节点的子树是不是跟subRoot相同(isSameTree
)
其实可以将封装好的函数看成是一个黑盒 只要符合条件就往里面丢就ok
还是部分=>整体的一个思路 不用去思考怎么遍历 去思考针对一个小的树结构怎么去做才能解决问题
var isSubtree = function(root, subRoot) {
if(subRoot===null) return true
if(root===null) return false
//其实还是用到了递归框架
//这样使用短路与的写法还是很常见的
return isSubtree(root.left,subRoot)||isSubtree(root.right,subRoot)||isSameTree(root,subRoot)
function isSameTree(root1,root2){
if(root2===null&&root1===null) return true
if(root1===null||root2===null) return false
if(root1.val!==root2.val) return false
return isSameTree(root1.left,root2.left)&& isSameTree(root1.right,root2.right)
}
};
时间复杂度:对于每一个 s 上的点,都需要做一次深度优先搜索来和 t 匹配,匹配一次的时间代价是 O(∣t∣),那么总的时间代价就是 O(∣s∣×∣t∣)。
空间复杂度:假设 s 深度为 ds,t的深度为 dt,任意时刻栈空间的最大使用代价是 O(max{ds,dt})
对称二叉树 101
题解:
轴对称的话就是跟上面的一样 改下逻辑 就行 还是之前 部分=>整体的思路
var isSymmetric = function(root) {
if(root===null) return true
return isSymmetricTree(root.left,root.right)
function isSymmetricTree(leftRoot,rightRoot){
if(leftRoot===null&&rightRoot===null) return true
if(leftRoot===null||rightRoot===null) return false
if(leftRoot.val!==rightRoot.val) return false
return isSymmetricTree(leftRoot.left,rightRoot.right)&&isSymmetricTree(leftRoot.right,rightRoot.left)
}
};
左叶子之和 404
题解:
深度遍历:
深度遍历,等遍历到子节点在将其值加起来
关键点在于:怎么判断是遍历到子节点(),怎么判断是左子节点
使用了flag标志,在叶子节点中加上判断是不是从左边进入的(因为二叉树就两个节点嘛(
var sumOfLeftLeaves = function(root) {
let flag=false
let sum=0
return isLeftLeaves(root)
function isLeftLeaves(root){
if(root===null) return root
if(root.left===null&root.right===null) {
if(flag) sum+=root.val
return sum
}
flag=true
isLeftLeaves(root.left)
flag=false
isLeftLeaves(root.right)
return sum
}
};
时间复杂度:O(n)
空间复杂度:O(n) 在最坏条件下 可以成链式的 这样深度就是树的节点数
二叉树的最小深度 111
题解:
深度遍历
思路:深度遍历,然后等到子节点的时候判断一下是不是min的
注意:做这题的时候,将叶子节点直接返回了 没有做depth-- 导致出错
要考虑正确depth的处理 当进入节点的时候depth++ 即将离开节点前depth–(之前那么写离开叶子节点就没有返回 疏漏乐
var minDepth = function(root) {
let depth=0
let min=Number.MAX_VALUE
return mini(root)
function mini(root){
if(root===null) return 0
depth++
if(root.left===null&&root.right===null) {
min=depth<min?depth:min
}
mini(root.left)
mini(root.right)
depth--
return min
}
};
二叉树中第二小的节点 671
题解:
深度遍历 将所有节点存到数组,然后去重,再将这个数组进行排序.拿到第二个值
var findSecondMinimumValue = function(root) {
if(root===null) return root
let treeArr=[]
traverse(root)
function traverse(root){
if(root===null) return
treeArr.push(root.val)
traverse(root.left)
traverse(root.right)
return
}
treeArr=Array.from(new Set(treeArr))
treeArr.sort((a,b)=>a-b)
if(treeArr.length>=2) return treeArr[1]
return -1
};
深度遍历 利用这个树特殊的属性 root.val = min(root.left.val, root.right.val)
每一棵子树的根节点都是这棵子树最小的值.第二小的节点所在子树,必定是这样的情况
root.value(最小的)
/ \
第二小的 (a) 最小的
不然没办法传上去
因此可以剪掉子树根比root.val大,不需要再往下遍历 下面子树是一定不会有第二小的
然后看这样树里面a节点哪个最小
var findSecondMinimumValue = function(root) {
let rootValue=root.val
let result=-1
let nodeValue=Number.MAX_VALUE
return tranverse(root)
function tranverse(root){
if(root===null) return
if(root.val>rootValue) return
if(root.left&&root.left.val!==root.right.val){
//选择两个中大一点的点(就是比根节点大的点
let temp=root.left.val>rootValue?root.left.val:root.right.val
nodeValue=temp<nodeValue?temp:nodeValue
result=nodeValue
}
tranverse(root.left)
tranverse(root.right)
return result
}
};
时间复杂度:O(n)
空间复杂度:O(n)
二叉树的层平均值 637
题解:
使用层序遍历,使用数组模拟队列.最外面的队列看是不是到最后一层
每层队列出去 然后放到nextArr tempArr空的说明这层的已经遍历完了
var averageOfLevels = function(root) {
if(root===null) return 0
let queue=[[root]]
let resultArr=[]
while(queue.length){
let tempArr=queue.shift()
resultArr.push(Array.from(tempArr))
let nextArr=[]
while(tempArr.length){
let temp=tempArr.shift()
if(temp.left) nextArr.push(temp.left)
if(temp.right) nextArr.push(temp.right)
}
if(nextArr.length) {
queue.push(nextArr)
}
}
//求和
let result=resultArr.map((itemArr)=>{
let sum=0
itemArr.map((item)=>{
sum+=item.val
})
return sum/itemArr.length
})
return result
};
优化版:
这个就少用了一个数组 只使用了一个数组去模拟队列
就是使用层序遍历去做,用size
确定层数.遍历完一层之后,队列里面就是下一层的所有节点
var averageOfLevels = function(root) {
if(root===null) return 0
let queue=[root]
let result=[]
while(queue.length!==0){
//现在队列里面只有这层树的节点
let size=queue.length
let sum=0
for(let i=0;i<size;i++){
let ptr=queue.shift()
sum+=ptr.val
if(ptr.left) queue.push(ptr.left)
if(ptr.right) queue.push(ptr.right)
}
result.push(sum/size)
}
return result
};
时间复杂度:只对节点做了一次操作 => O(n)
空间复杂度:O(n),其中 n 是二叉树中的节点个数。空间复杂度取决于队列开销,队列中的节点个数不会超过 n
找树左下角的值 513
题解:
使用层序遍历 每层都用一个数组存起来 双重数组最后一个数组的第一个数就是树左下角的值
var findBottomLeftValue = function(root) {
if(root===null) return 0
let queue=[[root]]
let resultArr=[]
while(queue.length){
let tempArr=queue.shift()
resultArr.push(Array.from(tempArr))
let nextArr=[]
while(tempArr.length){
let temp=tempArr.shift()
if(temp.left) nextArr.push(temp.left)
if(temp.right) nextArr.push(temp.right)
}
if(nextArr.length) {
queue.push(nextArr)
}
}
return resultArr[resultArr.length-1][0].val
};
二叉树非递归遍历
二叉树的前序遍历 144
题解:
非递归遍历:
用栈模拟二叉树的递归遍历时栈的变化(总体差不多
var preorderTraversal = function(root) {
let stack=[]
let result=[]
let currentNode=root
let visitNode=root
while(currentNode||stack.length!==0){
while(currentNode&&visitNode!==currentNode.left&&visitNode!==currentNode.right){
result.push(currentNode.val)
stack.push(currentNode)
currentNode=currentNode.left
}
currentNode=stack[stack.length-1]
if(currentNode.right&&visitNode!==currentNode.right){
currentNode=currentNode.right
}else{
visitNode=stack.pop()
currentNode=stack[stack.length-1]
}
}
return result
};
时间复杂度:O(n)
空间复杂度:O(n) 最坏情况树是链式的,栈的高度是树的高度
二叉树的中序遍历 94
题解:
var inorderTraversal = function(root) {
let currentNode=root
let stack=[]
let result=[]
while(currentNode||stack.length!==0){
while(currentNode){
stack.push(currentNode)
currentNode=currentNode.left
}
currentNode=stack[stack.length-1].right
let visitNode=stack.pop()
result.push(visitNode.val)
}
return result
};
时间复杂度:O(n)
空间复杂度:O(n) 最坏情况树是链式的,栈的高度是树的高度
二叉树的后序遍历 145
题解:
首先二叉树的后序遍历是LRD(x序遍历就是根节点在前/中/后
使用栈模拟二叉树的后序遍历的变化:
- 对于树的每个节点都先遍历树的左子树,再遍历树的右子树
- 等到遍历到叶子节点的时候,即左右子树都为null,将其pop出栈
- 一个节点pop出栈主要看两点:
- 看是不是叶子节点
- 右子树是不是已经遍历完成(刚刚pop的节点是不是其右节点 是则说明其右子树已经遍历完成
var postorderTraversal = function(root) {
let currentNode=root
let stack=[]
let visitedNode=root
let result=[]
while(currentNode||stack.length!==0){
while(currentNode){
stack.push(currentNode)
if(currentNode) currentNode=currentNode.left
}
currentNode=stack[stack.length-1]
if(currentNode.right&&visitedNode!==currentNode.right){
currentNode=currentNode.right
}else{
visitedNode=stack.pop()
currentNode=null
result.push(visitedNode.val)
}
}
return result
};
时间复杂度:O(n)
空间复杂度:O(n) 最坏情况树是链式的,栈的高度是树的高度
二叉排序树
修剪二叉搜索树 669
题解:
这题利用了二叉排序树的性质,left<root<right
当根节点的值比范围小的时候,其左子树都不会在其范围内,所以可以直接遍历其右子树,找到看右子树是不是符合范围,不符合再往下递归直到找到符合的/递归到null
,大的时候同理
var trimBST = function(root, low, high) {
if(root===null) return root
//小了 找右子树
if(root.val<low) return trimBST(root.right,low,high)
//大了找左子树
if(root.val>high) return trimBST(root.left,low,high)
//根节点ok 检查左右节点是否符合规范
root.left= trimBST(root.left,low,high)
root.right= trimBST(root.right,low,high)
return root
};
我从这题学到了不同的出口(不是递归结束出口,只是说递归可以从不同的方向离开
而且要想好递归的意义,比如这题,递归函数的意义就是返回符合范围的节点.所以用的时候只要想着,需要符合正确范围内的节点就可以使用递归,不必拘泥于递归框架.
可以从分析一棵小树进行分析,分析这棵树要怎么操作来写才正确.树的递归就是使用分治的思想.
二叉搜索树的最近公共祖先 235
题解:
这题的思路是运用二叉搜索树的性质,left<root<right
则p
,q
公共祖先一定是卡在两者中间(比min大,比max小),也可以相等,如果比p
,q
中最大还大应该遍历其左子树,同理比p
,q
中最小的还小应该遍历其右子树
var lowestCommonAncestor = function(root, p, q) {
let x=root
let max=p.val>q.val?p:q
let min=p.val>q.val?q:p
while(x){
if(x.val>max.val){
x=x.left
}else if(x.val<min.val){
x=x.right
}else if(x.val<=max.val&&x.val>=min.val){
return x
}
}
return x
};
时间复杂度:O(n)
空间复杂度:O(1)
把二叉搜索树转换为累加树 538
题解:
根据示例,这题的想法其实是从最右边开始遍历.然后遵循这样的遍历方式:R-D-L
然后其实就是将中序遍历调换的一下方向
var convertBST = function(root) {
let sum=0
sumBst(root)
function sumBst(root){
if(root===null) return
sumBst(root.right)
sum+=root.val
root.val=sum
sumBst(root.left)
}
return root
};
时间复杂度:O(n)
空间复杂度:最坏情况为O(n), 树以链表形式存储
思考:
其实我从这题里面更明白一件事 递归返回的到底有什么用. 第5行是一定要返回的因为第5行返回代表的是递归的结束条件. 但是第9行之后, 由于没有使用到递归返回的值, 所以不写, 直接使用返回的underfinded
也可以
有时候很纠结返回什么东西, 可以思考最后要返回什么 因为当递归栈都弹出, 递归函数结束, 最后如果有用到值的话, 就直接那个返回就ok, 也是一种思考返回值的方式
将有序数组转换为二叉搜索树 108
题解:
这题就是使用递归,本质上还是树的中序遍历.
题目要求:**每个节点的左右两个子树的高度差的绝对值不超过 1 ** 而且要是一棵二叉搜索树 而且给出的数组是升序排列的
很容易想到从最中间开始作为树的根节点,左边是左子树,右边是右子树(可以看做是中序遍历的结果
var sortedArrayToBST = function(nums) {
if(nums.length<1) return null
let mid=Math.floor(nums.length/2)
let root=new TreeNode(nums[mid])
root.left=sortedArrayToBST(nums.slice(0,mid))
root.right=sortedArrayToBST(nums.slice(mid+1,nums.length))
//即将离开这个节点
return root
};
思考:
第5,6行用到的递归返回的数据 最后一定要return值
在写递归函数的时候一定先思考好:
-
这个函数的意义是什么 然后把它当成一个黑箱(. 用到就调用就ok 意义很重要
-
离开这个节点要返回什么吗(可以看看 最后整个函数返回值要返回什么 如果没有用到返回值可以直接返回undefined
双指针
合并两个有序数组88
题解
双指针
var merge = function(nums1, m, nums2, n) {
let sorted=new Array(m+n).fill(0)
let p1=0,p2=0
let cur=0
while(p1<m||p2<n){
if(p1===m){
sorted[cur]=nums2[p2++]
}else if(p2===n){
sorted[cur]=nums1[p1++]
}else if(nums1[p1]<=nums2[p2]){
sorted[cur]=nums1[p1]
p1++
}
else{
sorted[cur]=nums2[p2]
p2++
}
cur++
}
for(let i=0;i<n+m;i++){
nums1[i]=sorted[i]
}
};
反转字符串中的元音字母 345
var reverseVowels = function(s) {
let first=0,last=s.length-1
let reg=/[aeiou]/i
let arr=s.split('')
while(first<last){
if(reg.test(arr[first])&®.test(arr[last])){
let temp=null
temp=arr[last]
arr[last]=arr[first]
arr[first]=temp
last--
first++
}else if(reg.test(arr[first])){
last--
}else if(reg.test(arr[last])){
first++
}else{
last--
first++
}
}
let str=arr.reduce((pre,cur)=>pre+=cur)
return str
};
时间复杂度:o(n) 使用双指针将暴力的双循环降成一个乐
空间复杂度:o(n) 因为字符串不能改 所以要用额外的空间来储存字符串
环形链表 141
思路一(双指针):
想法就是如果链表成环的话,没有尽头,就一定不会没有slow.next
和fast.next
的
fast在slow前面,而且走的更快一点,如果链表成环,fast会领先slow一个整圈追上slow
var hasCycle = function(head) {
if(head===null||head.next===null) return false
let slow=head,fast=head.next
while(slow!==fast){
if(fast===null||fast.next===null) return false
slow=slow.next
fast=fast.next.next
}
return true
};
要注意的点:
- 注意边界条件比如:
[]
[1,2]
时间复杂度:o(n)
空间复杂度:o(1) 只使用了两个额外的指针空间
思路二(哈希表):
边遍历边存进哈希表里,如果哈希表里面有走过的值,就说明重复走了
var hasCycle = function(head) {
if(head===null||head.next===null) return false
let set=new Set()
let p=head
while(p!==null){
if(set.has(p)) return true
set.add(p)
p=p.next
}
return false
};
时间复杂度:o(n)
空间复杂度:o(n)
删除有序数组中的重复项 26
题解:
直接遇到一样的就删除
var removeDuplicates = function(nums) {
let left=0,right=1
while(right<nums.length){
if(nums[left]===nums[right]){
nums.splice(right,1)
}else{
left++
right++
}
}
return nums.length
};
题解2:
其实就是原地填充一个数组 如果相同就right++ 不相同就把数字填过来
然后这道题只要返回前面数组的长度就行 看评论是逻辑删除
var removeDuplicates = function(nums) {
let left=0,right=1
while(right<nums.length){
if(nums[left]===nums[right]){
right++
}else{
left++
nums[left]=nums[right]
}
}
return left+1
};
删除排序链表中的重复元素 83
题解:
这个题目想法蛮有趣的。写起来其实很简单
var deleteDuplicates = function(head) {
if(head===null||head.next===null) return head
let left=head,right=head.next
while(right){
if(left.val!==right.val){
left.next=right
left=left.next
}
right=right.next
}
left.next=null
return head
};
最长回文子串 5
题解:
var longestPalindrome = function(s) {
let n=s.length
if(n===0||n===1) return s
let arr=s.split("")
let range=[0,0]
for(let i=0;i<n;i++){
helper(arr,n,i,i)
helper(arr,n,i-1,i)
}
return s.substring(range[0],range[1])
function helper(arr,n,start,end){
while(start>=0&&end<=n){
if(arr[start]===arr[end]){
start--
end++
}else{
break
}
}
if(end-(start+1)>range[1]-range[0]){
range[0]=start+1
range[1]=end
}
}
};
滑动窗口
本质
var minWindow = function(s, t) {
const need=new Map(),window=new Map()
for(const c of t){
if(!need.has(c)){
need.set(c,0)
}
need.set(c,need.get(c)+1)
window.set(c,0)
}
let left=0,right=0
let vaild=0//窗口中有效的字符个数
let start=0,len=Number.MAX_VALUE
//往不往右扩
while(right<s.length){
let c=s[right]
right++
if(need.has(c)){
window.set(c,window.get(c)+1)
if(window.get(c)===need.get(c)) vaild++
}
while(vaild===need.size){
if(right-left<len){
start=left
len=right-left
}
let d=s[left]
left++
if(need.has(d)){
if(window.get(d)===need.get(d)) vaild--
window.set(d,window.get(d)-1)
}
}
}
return len===Number.MAX_VALUE?"":s.substring(start,start+len)
};
排序
排序链表 148
题解:(归并排序
归并排序的思想就是将有序的两个数组进行合并.使用分治的思想,不断递归,到left
,right
两个数组只有一个数,可以将其视为已经排序完成的链表.然后进行两个有序链表的合并(不是简单的合并,合并之后得到的链表还是降序排列的链表.
var sortList = function(head) {
return sort(head,null)
function sort(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
}
}
let mid=slow
let left=sort(head,mid)
let right=sort(mid,tail)
return merge(left,right)
}
function merge(head1,head2){
let resultDummy=new ListNode()
let temp=resultDummy,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 resultDummy.next
}
};
时间复杂度:O(NlogN)
空间复杂度:O(logN)
怎么写的???
排序数组 912
题解:
快排
var sortArray = function (nums) {
sort(nums,0,nums.length-1)
return nums
};
function sort(nums,left,right){
if(left>=right) return
let p=randomPartion(nums,left,right)
sort(nums,left,p-1)
sort(nums,p+1,right)
}
function randomPartion(nums,left,right){
let i=Math.floor(Math.random()*(right-left)+left)
swap(nums,right,i)
return partion(nums,left,right)
}
function partion(nums,left,right){
let j=left-1
let p=nums[right]
for(let i=left;i<right;i++){
if(nums[i]<=p){
j++
swap(nums,i,j)
}
}
swap(nums,j+1,right)
return j+1
}
function swap(nums,left,right){
let temp=nums[left]
nums[left]=nums[right]
nums[right]=temp
}
堆排序
/**
* @param {number[]} nums
* @return {number[]}
*/
var sortArray = function (nums) {
heapSort(nums)
return nums
function heapSort(arr) {
let n = arr.length
// 建堆
for (let i =Math.floor(n/2-1); i >= 0; i--) {
heapify(arr, n, i)
}
// 检查是不是所有元素都遵循堆的性质
for (let i = n - 1; i > 0; i--) {
swap(arr, i, 0)
heapify(arr, i, 0)
}
return arr
}
function heapify(arr, n, i) {
let lson = 2 * i + 1
let rson = 2 * i + 2
let largest = i
if (lson < n && arr[largest]< arr[lson]) {
largest = lson
}
if (rson < n && arr[largest] < arr[rson]) {
largest = rson
}
if (largest !== i) {
swap(arr, i, largest)
heapify(arr, n, largest)
}
}
function swap(arr, i, j) {
let temp = arr[i]
arr[i] = arr[j]
arr[j] = temp
}
};
动态规划
var climbStairs = function(n) {
let map=new Map()
function dp(n){
if(map.has(n)) return map.get(n)
if(n<=0) return 0
if(n===1||n===2) return n
let sum=0
for(let item of [1,2]){
sum+= climbStairs(n-item)
}
map.set(n,sum)
return sum
}
return dp(n)
};
使用最小花费爬楼梯 746
题解:
这题主要是要找对状态和理解状态转移方程.
这题的状态是到n级楼梯所需要的最少耗费.不是每级最少,以每级最少为状态,在遇到两级要耗费时间最少的时候没有办法选择
所以以每级最少为子问题是不对的,子问题应该是到n级所需要的最少耗费
掌握好子问题就可以写出状态转移方程:
dp[i]=min(dp[i−1]+cost[i−1],dp[i−2]+cost[i−2])
dp[n]
是状态