前言
个人认为编程这项基础功有个亘古不变的解决问题原则:【大而化小】
- 把大问题变成小问题:
- 可以是流程先后的分割(核心)
- 也可以是分治式分割(需要每一块都差不多,但是只是让问题规模变小,本质上没什么变化)
- 然后解决小问题的时候就要看有哪些方案,挑一个最佳的出来解决(有时候很难抉出最佳,需要积累,而且更何况有些方案都是有利有弊的)
- 如果当前的小问题解决起来还是很复杂(复杂是因为要顾及的细节还是很多)的话,我们就继续把这个小问题分割成更小的问题,再一个一个小问题去解决。
- 等到我们的经验积累到达一定程度时,我们解决问题细分的规模也就越来越大了(就不会像之前那样细分了很多次)
简单来说就是学会一招叫 问题拆解 :
- 示例1:旋转链表的问题 → 可以拆解出 → 寻找倒数第 n 个结点的问题
- 示例2:最长无重复字符的子串 → 可以拆解出 → 寻找各元素左边距离最近的相同元素的位置
而流程上的分割解决(核心)也遵循一个不变的原则:
往小了看就像中学时期做数学题、往大了看就像工作时候做项目:
- 每一道问题(无论规模大小)都需要明确 已有条件 和 要求结果
- 问题的解决方案有哪些?(手头上有哪些工具?)最佳方案是哪个?可以做到更佳吗?
- 问题的先后顺序
解决方案的搜索和对比真的就需要去【积累】,很多东西、事情不可能一蹴而就,很大概率我们会走一些弯路,但有时要停下来反思和总结,才能不断的【积累】获得更好的办法
其实不止是编程,乃至整个社会的每一件事都是遵循整个原则。每一个大项目的完成,都会细化到每一个公司/组织、每一个团队、甚至细化到每一个人,我们都是大项目中的一个环节。
扯远了hhh,下面是个人整理的一些算法题的笔记,个人认为算法是比较难的,但是积累一些示例算法题的核心解决方案还是不错滴~
总结法:集合各个示例然后【找规律】,得到它们通用的算法思想,争取做到【以不变应万变】
类比法:借助一些核心算法的【核心思想】去类比其他算法,从而轻易得到解法,这比从无到有要简单很多
小技巧
时间复杂度上的优化
O(n) → O(1):判断数组是否含有某一指定项时,使用 哈希表 可以实现将时间复杂度由 O(n) 降为 O(1)
var m = new Map()
m.set(item1 , undefined)
m.set(item2 , undefined)
...
m.has(item) // true 还是 false
实际通常情况下是将 O(n²) 降为 O(n + n)。因为我们需要 O(n) 的时间去构造哈希表,但后面的每次循环里的查找可以省去每次以 O(n) 时间的遍历去判断有无该元素,当然代价是需要 O(n) 的额外空间。【空间换时间】
O(n) → O(1):寻找数组的指定项并返回其下标时,使用 哈希表 可以实现将时间复杂度由 O(n) 降为 O(1)
var m = new Map()
m.set(item1 , index1)
m.set(item2 , index2)
...
m.get(item) // index 还是 undefined
如果我们在循环中直接使用 indexOf 方法获取指定项的下标的话,时间复杂度为 O(n²),但如果事先哈希表,把元素及其下标对应起来的话,后面循环中通过 m.get(item) 就能以 O(1) 的时间快速获取 item 的下标
空间复杂度上的优化
O(n) → O(1):需要求出数组中的最值时,可以借助临时变量 max 存储当前的最值,在每次遍历中进行更新即可
var max = 0
Math.max(item , max) // 更加简洁
或
if(item > max) max = item
时间复杂度和空间复杂度的优化意义在于:当处理的数据是亿量级的时候,时间复杂度太高会很慢,空间复杂度太高会很占运行内存
迭代和递归的选择
- 迭代:
- 思想:下一步和这一步从本质上看处理方式是一样的
- 特点:1. for 或 while 循环;2. 会对变量的值进行重复更替
- 例子:将树结构转换为列表结构(层序遍历):
- 递归:
- 思想:大问题拆解成小问题,小问题和大问题本质上看处理方式是一样的(递归基除外)
- 特点:1. 函数里面调用自己;2. 进入函数之前设立递归基(递归在树、二分、分治等问题中常见)
- 例子:将列表结构转换为树结构:
function fn(node){
node.children = list.filter((item)=>{ // 递归基和处理体
return item.parentId === node.id
})
node.children.forEach((item)=>{ // 递归体
fn(item);
})
}
fn(list.find((item)=>{return item.id === -1})) // 调用根节点起步
时间复杂度:O(n²);空间复杂度:O(1)
缺陷:
// 观察这一模块:【每次都去遍历 list,换句话说是不是一直在做重复工作】
node.children = list.filter((item)=>{ // 递归基和处理体
return item.parentId === node.id
})
如果能
提前锁定父子对应关系,那就不用每次都去遍历 list —— 以空间换时间的通用套路
优化:以空间换时间,事先遍历 list 构建映射表 Map (key: parentId ; value: 所有 parentId 旗下的 id)
let map = new Map()
map.set(-1,[])
list.forEach((item)=>{
if(map.has(item.parentId)) map.get(item.parentId).push(item)
else map.set(item.parentId,[])
})
function fn(node){
node.children = map.get(node.id) // 找 node 的孩子
node.children.forEach((item)=>{ // 找 node 的孩子们的孩子
fn(item);
})
}
fn(map.get(-1)) // 调用根节点起步
时间复杂度:O(n);空间复杂度:O(n)
写递归的宗旨是:写一个能够解决
90%以上问题的方法,然后再加上递归基,就能解决100%的问题了
按数据结构分类
字符串
1. 从 URL 中提取传输的参数
从 URL 中提取传输的参数,返回 Object 类型数据。示例:从下面 URL 中提取出所有参数
https://www.baidu.com/s?ie=utf-8&f=8&rsv_bp=1&tn=54093922_1&wd=star&oq=star&rsv_pq=c365793c17a6&
rsv_t=890e3mC5fCtyQfabjwepkhmFk4r5VKRlWpz93U1I1jQsRlG&rqlang=cn&rsv_enter=0&rsv_dl=tb&rsv_btype=t
【思路】借助 String.prototype 的 indexOf、slice、split 方法 和 obj [ prop ] = value 的方式
- 使用 indexOf 方法判断
?所处位置,然后截取参数
var fn = function(str){
var i = str.indexOf("?")
str = str.slice(i + 1)
var arr = str.split("&")
var obj = {}
for(let item of arr){
var a = item.split("=")
obj[a[0]] = a[1]
}
return obj
}
2. 从 URL 中提取域名
【思路】借助 indexOf、slice 方法
- 使用 indexOf 方法检测出
:所在位置 - 使用 slice 方法截取字符串获得新的字符串
- 使用 indexOf 方法检测出新的字符串中第一个
/出现的位置 - 使用 slice 方法截取字符串获得域名
var fn = function(str){
var i = str.indexOf(":")
str = str.slice(i+3)
var j = str.indexOf("/")
str = str.slice(0,j)
return str
}
3. 求最长回文子串(需要借助子函数来判断)
【原题】5. 最长回文子串
【思路】每个字符都使用中心扩展法
- 自左至右遍历字符串,以 i 为中心向两周扩展(注意分奇偶两种情况)
- 借助子函数 fn 进行扩展,
碰到边界或两个字符一致则停止 - 利用 slice 方法对字符串进行截取获取以 i 为中心的最长回文串
- 对当前获取到的最长回文串的长度与之前攒下去的 总的最长回文串 的长度进行对比,择出更长
var longestPalindrome = function(s) {
if (s.length < 2) return s
let res = ''
for (let i = 0; i < s.length; i++) {
fn(i, i) // 回文子串长度是奇数
fn(i, i + 1) // 回文子串长度是偶数
}
function fn(m, n) {
while (m >= 0 && n < s.length && s[m] == s[n]) {
m--
n++
} // 循环结束时,恰好是不满足循环条件的状态
// 注意:m、n 两个边界不能取 所以回文串应该取 m+1 到 n-1 的区间,即回文串的长度应该是 n-m-1
if (n - m - 1 > res.length) {
res = s.slice(m + 1, n) // slice 也要取 [m+1,n-1] 这个区间
}
}
return res
};
时间复杂度:O(n²);空间复杂度:O(1) 4. 最长不重复子串
示例:
- "abcabcbb" 的最长不含重复字符的子字符串之一为 "abc",返回其长度 3;
- "pwwkew" 的最长不含重复字符的子字符串为 "wke",返回其长度 3;
- "bbbbb" 的最长不含重复字符的子字符串为 "b",返回其长度 1;
【思路1】暴力解法
- 遍历每个字符
- 统计以它为起点的字符的最长无重复子串
- 最终从中选出最长的那个
- 时间复杂度:O(n²)
- 空间复杂度:O(1)
【值得优化的点】"pwwkew"中已知以"p"为起点的最长无重复子串为"pw",那么以"w"为起点的最长无重复子串则为"w",不需要 O(n) 的时间去遍历了,只需要 O(n) 的空间存下 "pw",以 O(1) 的时间 shift 掉"p"即可
【思路2】双指针 + 滑动窗口 + 哈希表法
- 两根指针:指针 i 作为窗口起点,指针 j 寻找最大窗口的终点
- 指针到达最大窗口终点时停下,对窗口大小进行统计
- 接下来会进行 i++,所以指针 i 当前指向的元素 s[i] 就得退出滑动窗口了
- 时间复杂度:O(n)
- 空间复杂度:O(n)
var lengthOfLongestSubstring = function(s) {
if(s.length < 2) return s.length
var m = new Map()
var j = 0
var max = 0
var len = s.length
for(let i = 0; i < len; i++){ // 指针 i 指向窗口起点
while(j < len){ // 指针 j 寻找最大窗口终点
if(m.has(s[j])) break
else m.set(s[j])
j++
}
max = Math.max(max , m.size) // 每轮更新 max 值
m.delete(s[i]) // 元素 s[i] 退出窗口
}
return max
};
数组
1. 可以使用额外空间对数组查重
【思路】哈希表法
- 遍历数组,把各个元素放入哈希表中
- 放入哈希表之前注意在哈希表中查看是否已经存在
var fn = function(arr){
if(arr.length < 2) return false
var m = new Map()
for(let item of arr){
if(m.has(item)) return true
else m.set(item)
}
return false
}
2. 可以使用额外空间对数组去重
- ES5:使用 Object(缺点:区分不了数字和字符串)
【注意】适用于纯数字数组或纯字符串数组
【关键步骤】
for...of循环遍历数组,把所有元素当作对象的属性,注意使用toString()方法才能使用obj[]方法添加属性及设置其值,也就是说obj[]里面必须放字符串;- 利用
Object.keys()方法返回该对象的所有属性
var fn = function(arr){
if(arr.length < 2) return false
var obj = {}
for(let item of arr){
obj[item.toString()] = true
}
return Object.keys(obj)
}
- ES6:使用 Set / Map(优点:可以区分数字和字符串)
var fn = function(arr){
var s = new Set(arr)
return Array.from(s)
}
在这里插入 Set 结构的实例的属性 / 方法:
- var s = new Set()
- s.size 属性:返回 Set 实例的成员总数
- s.add(value) 方法:添加某个值,返回 Set 结构本身
- s.delete(value) 方法:删除某个值,返回一个布尔值,表示删除是否成功
- s.has(value) 方法:返回一个布尔值,表示该值是否为Set的成员
- s.clear() 方法:清除所有成员,没有返回值
- s.values() 方法:返回键值的遍历器
- s.forEach() 方法:遍历 Set 的所有成员
Set <=> Array:
- var set = new Set(arr)
- var arr = Array.from(set)
var fn = function(arr){
if(arr.length < 2) return false
var m = new Map()
for(let item of arr){
m.set(item)
}
return Array.from(m.keys())
}
在这里插入 Map 结构的实例的属性 / 方法:
- var m = new Map()
- m.size 属性:返回 Map 实例的成员总数
- m.set(key , value) 方法:设置键名 key 对应的键值为 value,如果 key 已有值,则键值会被更新
- m.get(key) 方法:读取 key 对应的键值,如果找不到 key,返回 undefined
- m.has(key) 方法:返回一个布尔值,表示某个键是否在当前 Map 对象之中
- m.delete(key) 方法:删除某个键,返回 true。如果删除失败,返回 false
- m.clear() 方法:清除所有成员,没有返回值(用于释放空间)
- m.keys() 方法:返回键名的遍历器
- m.values() 方法:返回键值的遍历器
- m.entires() 方法:返回所有成员的遍历器
- m.forEach() 方法:遍历 Map 的所有成员
- Map <=> Array
- Map <=> Object
- Map <=> JSON 2. 不用额外空间去掉数组中指定某一项
【思路】想要不用额外空间去掉指定项:就得把指定项放到数组尾部
- 自左至右寻找指定项后停下;自右至左寻找非指定项的项后停下
- 两个元素交换位置
- 设置数组长度 / slice / splice 截取数组
const MyFilter1 = function(arr,item){
var len = arr.length
var i = 0
var j = len - 1
while(i < j){ // 双指针法,注意退出循环条件是 i === j
if(arr[i] !== item) { // 自左至右找是 item 的元素
i++
continue
}
if(arr[j] === item){ // 自右至左找非 item 的元素
j--
continue
}
var t = 0 // 满足以上两个条件,则进行交换
t = arr[j]
arr[j] = arr[i]
arr[i] = t
}
arr.length = i // 最终指向为 item 的第一项,所以 arr.length = i
return arr
}
const arr = [5,3,3,4,6,3,8,9]
const element = 3
MyFilter1(arr,element) // [5,4,6,8,9]
3. 不用额外空间筛选出数组中指定某一项
【思路】想要不用额外空间筛选出指定项:就得把指定项放到数组头部
- 自左至右寻找非指定项后停下;自右至左寻找指定项的项后停下
- 两个元素交换位置
- 设置数组长度 / slice / splice 截取数组
const MyFilter2 = function(arr,item){
var len = arr.length
var i = 0
var j = 0
while(j < len){ // 双指针法,注意退出循环条件是 j 指针到达尾部
if(arr[i] === item) { // 自左至右找非 item 的元素
i++
continue
}
if(arr[j] !== item){ // 自右至左找是 item 的元素
j++
continue
}
var t = 0 // 满足以上两个条件,则进行交换
t = arr[j]
arr[j] = arr[i]
arr[i] = t
}
arr.length = i // 最终指向所有非 item 元素中的第一项,所以 arr.length = i
return arr
}
const arr = [5,3,3,4,6,3,8,9]
const element = 3
MyFilter2(arr,element)
4. 买卖股票的最佳时机 —— 买卖各一次
【原题】121. 买卖股票的最佳时机
【思路】(从第二个元素开始)遍历数组,min 初始值设为数组第一个元素,max 初始值设为 0
- 如果当前元素比 min 小则替换 min;
- 如果当前元素比 min 大则做减法,得出一个 t 值,再将 t 与 max 值进行比较,若大于 max 值则替换 max;
var fn = function(prices) {
var min = prices[0]
var max = 0
var t = 0
for(let i = 1; i < prices.length; i++){
if(prices[i] < min) min = prices[i]
else{
t = prices[i] - min
if(t > max) max = t
}
}
return max
};
注意:该题是求出最大收益,若需要求出买卖对应的天数,则只需在交换操作中对 i 的值作一下备份即可(如用变量 a、b 分别存储买卖天数)
5. 买卖股票的最佳时机 —— 可多次买卖
【思路】变量 flag 表示当前是否持有股票,变量 arr 记录每次股票的买入卖出,变量 sum 表示最大收益
- 如果股价呈反弹上升趋势,则在恰好反弹处买入股票
- 如果股价呈顶端下降趋势,则在恰好顶端出卖出股票
- 对数组 arr 的记录进行统计,得出最大收益
var maxProfit = function(prices) {
var flag = 0 // 未持股票
var len = prices.length
var arr = [] // 记录每次的买入卖出
var sum = 0 // 总获利
for(let i = 0; i < len; i++){
if(!flag){ // 寻找最佳买入机会
if(prices[i+1] >= prices[i]){
arr.push(prices[i])
flag = 1 // 已持有股票
}
}
else{ // 寻找最佳卖出机会
if(prices[i+1] <= prices[i] || prices[i+1] == undefined){
arr.push(prices[i])
flag = 0 // 未持有股票
}
}
}
var len1 = arr.length
for(let j = 0; j < len1 - 1; j = j+2){
sum += (arr[j+1] - arr[j])
}
return sum
};
6. 寻找两个数组的交集
【思路】
- 暴力法:遍历数组 A 各个元素的同时遍历数组 B 所有元素查看是否存在相同项。时间复杂度为 O(n²)
- 哈希表法:时间复杂度为 O(n)
- 如果两个数组的长度数量级差不多的情况下:把其中任意一个数组转化为哈希表
- 如果两个数组的长度数量级不同:选择数量级较大的数组转化为哈希表
- 然后对另一个数组进行遍历
var fn = function(arr1 , arr2){
var m = new Map()
for(let item of arr1){
m.set(item)
}
var res = []
for(let item of arr2){
if(m.has(item)) res.push(item)
}
return res
}
fn([5,2,1,4,6,3,,9,8],[1,2,13,15]) // [1,2]
7. 从数组中找出所有比左边所有元素大 (条件1) 且比右边所有元素小 (条件2) 的元素,要求时间复杂度为 O(n) (条件3)
【思路】首先排除两个数:左右两边第一个数不满足条件
- 以时间复杂度 O(n) 找到数组各个元素对应的其右边所有元素中的最小元素对应起来;
- 然后自左至右找所遍历过的所有元素的最大元素;
- 将该元素与其对应的最小元素比较,若小之则满足条件,若大之则不满足条件;
- 综上,时间复杂度为 O(n + n)
var fn = function(arr){
// 【第一步:部署出各个元素对应的其右最小值】
var len = arr.length
var min = arr[len - 1]
var arr1 = []
for(let i = len - 2; i > 0 ; i--){
arr1[i - 1] = min
if(arr[i] < min) min = arr[i]
}
console.log(arr1) // 此时获得的 arr1 便是 arr 中(除首尾两个元素外)的各元素其右边所有元素中的最小值
arr1.unshift(0) // 填补空位,否则因混乱而出错
arr1.push(0)
// 【第二步:自左至右找当前最大值】
var max = arr[0]
var res = []
for(let j = 1; j < len - 1; j++){
if(arr[j] > max){ // 满足条件(1)
max = arr[j]
if(arr[j] < arr1[j]) res.push(arr[j]) // 满足条件(2)
}
}
return res
}
fn([1,8,6,9,10,15,12,20]) // arr1: [6, 9, 10, 12, 12, 20] ; res: [9, 10]
8. 一个元素只有 0,1,2 的数组怎么排序
【思路】三个指针 p1,p2,p3
- p1 指向最左边的非 0 元素
- p3 指向最右边的非 2 元素
- p2 在中间自左至右遍历数组
- 若为 p2 指向元素 0 则与 p1 指向元素交换
- 若为 p2 指向元素 2 则与 p3 指向元素交换 【注意】
- p2 指针必须在 p1 的右侧,否则就是在做无用功
- p2 指针不能在 p3 的右侧,否则也是在做无用功
var fn = function(arr){
var len = arr.length
var p1 = 0
var p2 = 1
var p3 = len - 1
var t = 0
while(true){
while(arr[p1] === 0) p1++ // p1 指向最左边的非 0 元素
while(arr[p3] === 2) p3-- // p3 指向最右边的非 2 元素
if(p2 < p1) p2 = p1 + 1 // 注意 p2 指针必须在 p1 的右侧,否则就是在做无用功
if(p2 > p3) return arr // 注意退出循环条件是:p2 指针在 p3 的右侧,否则也是在做无用功
if(arr[p2] === 0){ // 若为 p2 指向元素 0 则与 p1 指向元素交换
t = arr[p2]
arr[p2] = arr[p1]
arr[p1] = t
p1++
}
else if(arr[p2] === 2){ // 若为 p2 指向元素 2 则与 p3 指向元素交换
t = arr[p2]
arr[p2] = arr[p3]
arr[p3] = t
p3--
}
else p2++ // 若为 p2 指向元素 1 则向右移动即可
}
}
9. 寻找两个距离最近的相同元素
【注意】两个条件:1. 两个元素是相同的;2. 它们之间的距离是最近的
【思路】哈希表法
- 遍历每个元素,构造一个哈希表,键为数组元素,值为该元素最后一次出现的索引
- 遍历过程中,先通过
m.get(item)来 判断之前是否出现过 以及 获取相同的 ta 的下标是多少
var fn = function(arr){
var m = new Map()
var a = 0 , b = 0 , min = arr.length
for(let i = 0; i < arr.length; i++){
if(m.get(arr[i]) === undefined) m.set(arr[i] , i)
else {
if(i - m.get(arr[i]) < min) {
a = arr[i]
b = i
min = i - m.get(arr[i])
}
}
}
return [a , b]
}
栈
1. 栈实现队列
【思路】两个栈可以实现一个队列
- 将数组按序 push 入栈 A
- 将栈 A 的元素一个一个 pop 出压入栈 B
- 将栈 B 的元素一个一个 pop 出
var MyQueue = function() {
this.inStack = [];
this.outStack = [];
};
MyQueue.prototype.push = function(x) {
this.inStack.push(x);
};
MyQueue.prototype.pop = function() {
if (!this.outStack.length) {
this.in2out();
}
return this.outStack.pop();
};
MyQueue.prototype.peek = function() {
if (!this.outStack.length) {
this.in2out();
}
return this.outStack[this.outStack.length - 1];
};
MyQueue.prototype.empty = function() {
return this.outStack.length === 0 && this.inStack.length === 0;
};
MyQueue.prototype.in2out = function() {
while (this.inStack.length) {
this.outStack.push(this.inStack.pop());
}
}
2. 队列实现栈
【思路】简单粗暴
- 将数组按序入队 A
- 将队 A 元素按序出队,只留下队尾那个元素
- 将该元素放入队 B
- 重复 2、3 直至队 A 为空
var MyStack = function() {
this.queue = [];
this._queue = [];
};
MyStack.prototype.push = function(x) {
this.queue.push(x);
};
MyStack.prototype.pop = function() {
while(this.queue.length > 1){
this._queue.push(this.queue.shift());
}
let ans = this.queue.shift();
while(this._queue.length){
this.queue.push(this._queue.shift());
}
return ans;
};
MyStack.prototype.top = function() {
return this.queue.slice(-1)[0];
};
MyStack.prototype.empty = function() {
return !this.queue.length;
};
3. 有效括号的判断(只含有一种括号 {} 或 [] 或 ())
【思路】利用栈
- 将字符串拆成字符串数组
- 若当前字符为
(则直接压入栈 - 若当前字符为
)则对栈的长度进行判断- 若栈空,则直接返回 false
- 若栈非空,则对栈顶元素进行判断
- 若栈顶元素为
)则直接返回 false - 若栈顶元素为
(则将栈顶元素出栈
- 若栈顶元素为
- 退出循环后若栈空则返回 true【表示全部匹配成功】,否则返回 false
var fn = function(s){
if(s.length < 2) return false
var stack = []
var arr = s.split("")
for(let item of arr){
if(item === "(") stack.push(item)
else {
if(stack.length === 0) return false
else {
if(stack[stack.length - 1] === "(") stack.pop()
else return false
}
}
}
if(stack.length !== 0) return false
else return true
}
4. 有效括号的判断(包含多种括号 {} 和 [] 和 (),不要求高低级)
【原题】20. 有效的括号
【思路】与上一题的区别在于有多种括号,所以需要借助哈希表
- 先构建一个哈希表,键为右括号
)、]、},值为左括号(、[、{ - 在判断括号是否匹配时就使用
m.get(右括号)得到 其对应的左括号,再查看是否与 栈顶元素 相等即可
var isValid = function(s) {
if(s.length < 2) return false
var stack = []
var arr = s.split("")
var m = new Map()
m.set(")","(")
m.set("]","[")
m.set("}","{")
for(let item of arr){
if(item === "(" || item === "[" || item === "{") stack.push(item)
else {
if(stack.length === 0) return false
else {
var l = stack.length
if(stack[l - 1] === m.get(item)) stack.pop()
else return false
}
}
}
if(stack.length !== 0) return false
else return true
};
5. 有效括号的判断(包含多种括号 {} > [] > (),要求高低级)
【思路】这道题与上一题的区别在于:([])、[{}]是无效的
6. 升序数组去重
- 构造一个栈 stack
- 遍历数组
- 如果【当前遍历的元素跟栈顶元素不同】则入栈
- 否则不入栈
7. 找出各元素对应的下一个更大值,不存在则返回 -1
【思路】单调栈
- 栈空则元素入栈,将入栈的元素比栈顶元素的小(或等于)也入栈
- 将入栈的比栈顶元素大则不入栈,并将该要入栈的元素作为栈顶元素的下一个更大值,记录在另外的数组中,同时将栈顶元素 pop 出来
var nextLargerNum = function(nums) {
var stack = [] // 创建栈
var i = -1 // i 用来指向栈顶
var j = 0 // j 用来记录数组下标
var len = nums.length
var res = [] // res 用来存放结果
while(j < len){
if(stack.length === 0 || nums[j] <= nums[stack[i]]) {
stack.push(j) // 栈里面放的是数组下标
i++
j++
res[stack[i]] = -1
}
else {
res[stack[i]] = nums[j] // 栈顶元素的下个较大值就是当前遍历的元素
stack.pop()
i--
}
}
return res
};
nextLargerNum([20,6,15,9,10,3,8,12]) // [-1, 15, -1, 10, 12, 8, 12, -1]
8. 二叉树的前序遍历、后序遍历(非递归遍历)
二叉树的前序遍历、后序遍历的迭代法都需要借助栈实现
- 前序遍历的方法是
根结点出栈,其右结点入栈后其左结点紧随入栈,无子结点则把栈顶元素(根结点)出栈; - 后序遍历跟前序遍历的方法相似,不同的是
根结点出栈,其左结点入栈后其右结点紧随入栈,然后最后得到的数组记得 reverse()
// 前序遍历
function fn(root) {
var arr = []
if(root==null) return arr
var stack = []
stack.push(root) // 先把整棵树放进去
while(stack.length!=0){
var node = stack.pop() // 弹出节点
arr.push(node.val) // 把弹出的节点的值放入数组
if(node.right!=null) stack.push(node.right) // 右先入栈后弹出
if(node.left!=null) stack.push(node.left) // 左后入栈后弹出
}
return arr
};
// 后序遍历
function fn(root) {
let arr = []
if(root==null) return arr
var stack = []
stack.push(root)
while(stack.length!=0){
var node = stack.pop() // 弹出节点
arr.push(node.val) // 把弹出节点的值写入数组
if(node.left!=null) stack.push(node.left) // 左先入栈后弹出
if(node.right!=null) stack.push(node.right) // 右后入栈先弹出
}
return arr.reverse() // 注意结果数组相反
};
链表
1. 反转链表
【思路】
- 保留 next 值
- 后结点指向前结点
- 指针指向之前保留的 next 值
2. 反转链表的一部分(从 x 到 y)
【思路】
- 反转 x + 1 到 y 所有结点的 next 值
- x - 1 指向 y
- x 指向 y + 1
3. 判断链表是否存在环状
-
维护哈希表法 每走一步保存结点到映射表,走到的节点有重复则含有环状
-
快慢指针法 快指针追到慢指针则含有环状
4. 判断链表进入环状的第一个结点
-
维护哈希表法 每走一步保存结点到映射表,走到的节点有重复则映射出结点出来
-
快慢指针法
假设链表头结点走到目标结点的距离为 a 格,目标结点走到相遇结点的距离为 b 格,相遇结点走到目标结点的距离为 c 格
【紧紧抓住相遇的点】相遇的时候快指针走了 a 格和 n 圈环,即 a + n(b+c) + b 步,如果往后退 b 格 / 往前走 c 格就可以到达目标结点,由于无法回退,所以只能选择往前走 c 格,所以此时在头结点设立一个指针,两根指针一起走,相遇时即在目标结点
5. 找出两个链表的公共结点
【思路1】走长链表得长链表长度,走短链表得短链表长度,作差,重走一遍,相遇结点则为公共结点(时间为 O(l短 + l长 + l短))
【思路2】长链表、短链表一起走,短链表到末尾瞬间跳回来短链表头结点,继续往前走,长链表上的指针到末尾时短链表所处位置就是公共结点(时间为 O(l长))
【本质】得到长度差
6. 合并两个已排序链表
【思路】五部曲
- 各自链表头结点出发,对比谁小,p1 和 p2 两个指针(如:1 比 2 小)
- 较小者顺着链表找最接近较大者的(如:1 往后走找最接近 2 但比 2 小的)
- 存 p1.next(把结点 1 的 next 值用变量存起来)
- 改 p1.next(将结点 1 指向结点 2)
- 指针 p1 指向之前存起来的 p1.next。重复回到第 1 步
普通树
数据结构图:
数据结构:
const tree = {
id: 0,
name: '目录-0',
children: [
{
id: 1,
name: '目录-1',
children: [
{
id: 4,
name: '目录-4',
children: [],
},
{
id: 5,
name: '目录-5',
children: [],
},
{
id: 6,
name: '目录-6',
children: [],
}
]
},
{
id: 2,
name: '目录-2',
children: [],
},
{
id: 3,
name: '目录-3',
children: [
{
id: 7,
name: '目录-7',
children: [],
},
{
id: 8,
name: '目录-8',
children: [],
}
]
}
]
}
深度优先搜索——递归
const fn = function(treeNode) {
console.log(treeNode);
if (treeNode.children) {
treeNode.children.forEach((item) => {
fn(item);
});
}
};
fn(tree);
控制台输出结果:
广度优先搜索——迭代
思路:两个数组 q、next_q + 两层循环
const fn = function(treeNode) {
let q = [treeNode];
let next_q = [];
while(q.length){
next_q = []; // 清空数组
q.forEach((item)=>{
if(item.children) {
console.log(item);
next_q = next_q.concat(item.children);
}
})
q = next_q;
}
};
fn(tree);
二叉树
对于一棵树来讲,除了叶子节点以外的 所有 节点都有这样的特性:
- 每个节点也都可以看作是
一棵树的根节点- 并且该节点和其
左子树和右子树有一定的联系 而树的 叶子节点 不满足这样的特性,所以它们是【停止递归开始反弹回溯】的关键点这就是递归思想的重要体现 —— 看似变化,实则不变
① 深度优先搜索——递归
二叉树的中序遍历
function fn(){
let arr = []
function treeEach(node){
if(node === null) return null
treeEach(node.left)
arr.push(node.value)
treeEach(node.right)
}
treeEach(root)
return arr
}
求某个节点的最大深度
【原题】104. 二叉树的最大深度
function maxDepth(node){
if(node === null) return 0
return Math.max(maxDepth(node.left),maxDepth(node.right)) + 1
}
求某个节点的最小深度
判断两棵树是否完全一样 —— 采用深度优先搜索的递归方式
递归的退出条件是至少一个节点为null(两个节点均为 null 要返回 true,只有一个节点为 null 要返回 false)
反过来的动态规划:第n次的结果和第n-1次的结果的关系是 —— 当前节点各自的值相同且各自的子节点也要都相同
function fn(p,q){
if(p==null&&q==null) return true
if(p==null||q==null) return false
return p.val==q.val && fn(p.left,q.left) && fn(p.right,q.right)
}
判断是否为对称二叉树
【思路】递归方式:针对一对 “对称节点” 的左右节点进行各自的比较
用颜色标出的,为一对对的 “对称节点”,对于 “对称节点” 我们要对比的是:
- node1.value ?== node2.value
- node1.left ?== node2.right
- node1.right ?== node2.left
【代码】
var isSymmetric = function(root) {
var res = true;
var isChildSymmetric = function(node1,node2){ // 一对对称结点,不一定是兄弟结点
if(!node1 && !node2) return; // 没有必要再比较下去了
if(node1 && !node2) { // 只有一个是 null
res = false;
return; // 没有必要再比较下去了
}
if(!node1 && node2) {
res = false;
return; // 没有必要再比较下去了
}
// 两个都是 null,不做处理
if(node1 && node2) { // 两个都不是null
if(node1.val !== node2.val) {
res = false;
return; // 没有必要再比较下去了
}
isChildSymmetric(node1.left,node2.right);
isChildSymmetric(node1.right,node2.left);
}
}
isChildSymmetric(root.left,root.right);
return res;
};
② 广度优先搜索——迭代
层序遍历【广度优先搜索(迭代)遍历树的核心武器】
var fn = function(root) {
let q = [root];
let next_q = [];
while(q.length){
for(let i = 0,len = q.length;i < len;i++){
if(q[i].left) next_q.push(q[i].left);
if(q[i].right) next_q.push(q[i].right);
}
q = next_q;
next_q = [];
}
};
判断是否为对称二叉树
【思路】迭代方法——层序遍历,得到每一行的所有节点,组成一个数组,判断数组是否为对称数组(左右指针向中间遍历)
计算二叉树的最大深度
var maxDepth = function(root) {
if(!root) return 0;
let q = [root];
let next_q = [];
let num = 1;
while(q.length){
for(let i = 0,len = q.length;i < len;i++){
if(q[i].left) next_q.push(q[i].left);
if(q[i].right) next_q.push(q[i].right);
}
if(next_q.length) num++; // 如果下队列不为空,则表示这一整行不都是叶子节点,把这一行算上去
q = next_q;
next_q = [];
}
return num;
};
计算二叉树的最小深度
var minDepth = function(root) {
if(!root) return 0;
let q = [root];
let next_q = [];
let num = 1;
let flag = false;
while(q.length){
for(let i = 0,len = q.length;i < len;i++){
if(!q[i].left && !q[i].right) { // 如果当前行存在叶子节点,则就此跳出两层循环
flag = true;
break;
};
if(q[i].left) next_q.push(q[i].left);
if(q[i].right) next_q.push(q[i].right);
}
if(flag) break; // 再跳一次
num++;
q = next_q;
next_q = [];
}
return num;
};
按算法思维分类
双指针法
总共就三种情况:
- 快慢指针:追及方式查看
是否有闭环- 先后指针:固定两针距离,一次遍历到达链表
倒数第N个节点- 相遇指针:左指针在头右指针在尾,左指针右移和右指针左移会产生
两种不同的趋势,看要到达的结果去决定移动哪根指针 1. 反转链表
【思路】采用迭代的方式,先保存前指针next值,然后指向后指针,后指针指向前指针,前指针指向之前保存的 next
var reverseList = function(head) {
if(head === null || head.next === null) return head
var p1 = head
var p2 = head.next
var p = head // 保存原先的头指针
var tmp = {} // 将当前 p2 所指的结点的 next 暂存起来
while(p2 !== null){
tmp = p2.next
p2.next = p1
p1 = p2
p2 = tmp
}
p.next = null // 原先的头指针变成了尾指针,将其 next 值设置为 null,链表才能顺利断开
return p1 // p1 所指便是新的头指针
};
2. 反转链表的一部分
【思路】需要三根指针 p,p1,p2
- 初试状态:p 指向 left - 1 位置的结点;p1 指向 left 位置的结点;p2 指向 left + 1 位置的结点
- 规定循环次数为 right - left,反转链表
- 由于是反转链表的一部分,所以需要对反转部分的头尾进行处理:
- 位置为 left 的结点的 next 需要指向位置为 right + 1 的结点
- 位置为 left - 1 的结点的 next 需要指向位置为 right 的结点
- 注意最后返回头指针有两种情况:
- 若 left 等于 1 时对应的新链表的头指针是 p1
- 若 left 不等于 1 是对应的新链表的头指针仍是 head
var reverseBetween = function(head, left, right) {
if(head === null || head.next === null || left === right) return head
var p = new ListNode(null,head) // p1 的前一个结点
var p1 = head
for(let i = 1; i < left; i++){ // 让 p1 指针指向位置为 left 的结点,p 指针跟随移动
p1 = p1.next
p = p.next
}
var p2 = p1.next
var tmp = {} // 将当前 p2 所指的结点的 next 暂存起来
for(let j = 0; j < right - left; j++){
tmp = p2.next
p2.next = p1
p1 = p2
p2 = tmp
} // 退出循环时 p1 指向位置 right,p2指向位置 right + 1
p.next.next = p2 // 位置为 left 的结点的 next 需要指向位置为 right + 1 的结点
p.next = p1 // 位置为 left - 1 的结点的 next 需要指向位置为 right 的结点
if(left === 1) return p1 // left 为 1 时新链表的头指针是 p1
else return head // 新链表的头指针仍是 head
};
3. 升序链表去重 —— 重复的项留一个
【思路】
- 通过两个指针遍历,检测重复,定义 p1 指针指向头结点,p2 指针指向第二个结点
- 不重复 —— 把 p2 所指交给 p1
- 重复 —— 把 p2 的 next 交给 p1 的 next
- 无论是否重复,p2 指针都得往前移动
var fn = function(head) {
if(head === null || head.next === null) return head
var p1 = head
var p2 = head.next
while(p2 !== null){
if(p2.val === p1.val) p1.next = p2.next
else p1 = p2
p2 = p2.next
}
return head
};
4. 升序链表去重 —— 重复的项全部去掉
【思路】
- 设置哑结点指向头结点,防止头结点被删除,定义 p 指针指向哑结点
- 不重复 —— p 指针后移即可
- 重复 —— 将 p.next 及其后面相同值的所有结点删除
- 返回哑结点的 next 结点,即头结点
var fn = function(head) {
if (!head) return head;
const node = new ListNode(0, head); // 设置哑结点并将其指向头结点:防止头结点被删除
let p = node;
while (p.next && p.next.next) {
if (p.next.val === p.next.next.val) {
const e = p.next.val;
while (p.next && p.next.val === e) {
p.next = p.next.next;
}
}
else p = p.next;
}
return node.next;
};
5. 寻找链表倒数第 n 个结点
【思路】一次遍历:前指针和后指针相隔 n 个结点,当前指针到达链表尾时后指针即指向倒数第 n 个结点
var fn = function(head, k) {
var p1 = head
var p2 = head
if(head === null) return head
for(let i = 0; i < k; i++){
p2 = p2.next
}
while(p2 !== null){
p2 = p2.next
p1 = p1.next
}
return p1
};
6. 旋转链表(包含寻找链表倒数第 n 个结点)
【思路】现在需要让链表旋转 n 格,其实就是 先找出链表倒数第 n 个结点,此时前指针 p1 指向链表尾,后指针 p2 指向倒数第 n 个结点,将 p1 的 next 值设为head(指向头结点),将 p2 的 next 值设为null(作为链表尾部)
7. 容器装最多的水
【思路】左右指针向中夹击
- 两指针分别指向最左和最右,然后往中间遍历
每次只移动那个指向木板高度较低的指针
证明如下:【反证法】 如果移动指向较高木板的指针,会出现两种情况:
- 新木板比原来较高木板低,则容量必定减小;
- 新木板比原来较高木板高,则容量也是必定减小,因为容器容量由短板决定,短板高度并没有变化,而容器的横向跨度变小,所以容量减小 综上所述:如果移动指向较高木板的指针,则容器容量必定减小;相反,如果移动指向较低木板的指针,才有可能使容器容量增大
- 注意保存最大值:这样的话每次移动对应每次计算一次容量,
对 max 值进行更替,最后输出 max 值即可解决问题
8. 判断回文数组/回文串(含有无效字符、忽略大小写)
【思路】
- 指针 i 指向数组头,指针 j 指向数组尾,各自向中间靠拢
- 使用 while 循环,退出循环条件为 i >= j,该过程中要跳过无效字符,同时要进行大小写转换
使用 正则表达式 来判断是否为有效字符,格式为
/[有效字符集]/.test(待检测字符)
- 如果有效字符大小写转换后相同则非回文串;如果顺利结束循环则为回文串
var isPalindrome = function(s) {
var i = 0
var j = s.length - 1
while(i < j){
while(/[^0-9A-Za-z]/.test(s[i])) i++
while(/[^0-9A-Za-z]/.test(s[j])) j--
if(i < j && s[i].toLocaleLowerCase() !== s[j].toLocaleLowerCase()) return false
i++
j--
}
return true
};
9. 判断回文链表(包含反转链表)
【思路】
- 统计链表总长度 len
- 遍历到链表中心处,该过程中顺便反转链表的前半段【核心】
- 对原链表的前半部分 p1 和后半部分 p2 进行逐个校验
var isPalindrome = function(head) {
if(head === null || head.next === null) return true
var p = head
var len = 0
while(p !== null){ // 1. 统计链表长度
p = p.next
len++
}
var p1 = null
var p2 = head
var next = {}
if(len % 2 === 0) var n = Math.floor(len/2) - 1 // 链表长度为偶数
else var n = Math.floor(len/2) // 链表长度为奇数
for(let i = 0; i < n + 1; i++){ // 2. 核心:边走边反转,走到链表中央停下
next = p2.next
p2.next = p1
p1 = p2
p2 = next
}
if(len % 2 !== 0) p1 = p1.next
while(p1 !== null && p2 !== null){ // 3. 对两个链表进行逐个检验
if(p1.val !== p2.val) return false
p1 = p1.next
p2 = p2.next
}
return true
};
10. 寻找链表的公共结点
【思路】首先遍历两条链表,分别获取两条链表的长度,然后再根据两条链表的长度差N让前指针领先后指针N格,两指针同时遍历,next值首次相同时表示来到了公共结点
11. 升序数组/升序链表找交集
【思路】一条指针遍历一个数组,每次如果两指针指向的数不同则移动那根指向较小数的指针,如果遇到相同的则放入新数组记录起来
12. 三数之和
【问题】数组 nums 长度为 n,找出 nums 中三个元素 a,b,c,满足 a + b + c = 0 且不重复的所有情况
【示例】输入: nums = [-1,0,1,2,-1,-4] ;输出: [[-1,-1,2],[-1,0,1]]
【初期思路】O(n³) 的时间列出所有可能,然后用 O(n) 的空间作哈希表去重
【改进思路1】在循环中去重,方法是排序后跳数,如 [0,1,2,2,2,3] 找出 [0,1,2] 之后不要继续找 2 了,直接跳到 3 就好
【改进思路2】利用进行第三重循环时结果的唯一性,可以把第三重循环放在第二重循环中去做,在第二重循环中使用双指针的方式(p1 向右走,p2 向左走)的方式完成挑选
简易版
const fn = function(arr){
arr.sort((a,b)=>{return a - b}); // 1. 排序
const res = [];
for(let i = 0;i < arr.length; i++){
for(let p1 = i + 1,p2 = arr.length - 1;p1 < p2;){ // 2. 双指针找出 和为 -arr[i] 的两个数
if(arr[i] + arr[p1] + arr[p2] < 0) p1++;
else if(arr[i] + arr[p1] + arr[p2] > 0) p2--;
else {
res.push([arr[i] , arr[p1] , arr[p2]]); // 3. 放入结果并退出循环
break;
}
}
}
return res;
}
console.log(fn([-1,0,1,2,-1,-4])) // [[-1,-1,2],[-1,0,1]]
完整版
/**
* @param {number[]} nums
* @return {number[][]}
*/
var threeSum = function(nums) {
if(nums.length < 3) return [];
if(nums[0] === nums[nums.length - 1] && nums[0] === 0) return [[0,0,0]];
nums = nums.sort((a,b)=>a - b);
const res = [];
for(let i = 0;i < nums.length;i++){
if(nums[i] === nums[i-1]) continue; // 首位数有重复,跳过循环
if(nums[i] > 0) break; // 如果这个数超过 0,后面的循环就没必要了
let p1 = i + 1;
let p2 = nums.length - 1;
while(p1 < p2){
if(nums[i] + nums[p1] + nums[p2] < 0) {
let p1_oldValue = nums[p1];
while(nums[p1] === p1_oldValue) p1++; // 有重复,跳过
}
else if(nums[i] + nums[p1] + nums[p2] > 0) {
let p2_oldValue = nums[p2];
while(nums[p2] === p2_oldValue) p2--; // 有重复,跳过
}
else {
res.push([nums[i],nums[p1],nums[p2]]);
let p1_oldValue = nums[p1];
while(nums[p1] === p1_oldValue) p1++; // 有重复,跳过。这里不能 break,因为还有其他情况
};
}
}
return res;
};
【复杂度】时间复杂度:O(n²);空间复杂度:O(log n)(因为有快速排序)
13. 合并两个有序链表
【示例】
【思路】五部曲
- 各自链表头结点出发,对比谁小,p1 和 p2 两个指针(如:1 比 2 小)
- 较小者顺着链表找最接近较大者的(如:1 往后走找最接近 2 但比 2 小的)
- 存 p1.next(把结点 1 的 next 值用变量存起来)
- 改 p1.next(将结点 1 指向结点 2)
- 指针 p1 指向之前存起来的 p1.next。重复回到第 1 步
/**
* @param {ListNode} l1
* @param {ListNode} l2
* @return {ListNode}
*/
var mergeTwoLists = function(l1, l2) {
let p = p1 = l1,p2 = l2;
while(p1.next&&p2.next){
if(p1.value > p2.value){ // 1. 控制 p1 始终指向较小者
let t = p2;
p1 = p2;
p2 = t;
}
while(p1.next){
if(p1.value < p2.value) p1 = p1.next; // 2. 较小者顺着链表找最接近较大者的
}
let temp = p1.next; // 3. 存 p1.next
p1.next = p2; // 4. 改 p1.next
p1 = temp; // 5. 指针 p1 指向之前存起来的 p1.next。重复回到第 1 步
}
return p; // 返回较小头结点
};
// 未测试,不知对不对
动态规划 —— 思路反着走,找规律,用递归
- 秘诀1:
从结果出发,思路反着走,找规律- 秘诀2:每一步的结果都跟
上一步的结果有直接关系
解法:动态规划方程 → 递归函数
1. n 阶楼梯求爬梯方案数(每次只能爬1格或2格)
思路:设 n 阶楼梯对应的爬梯方案数为f(n),每次只能爬 1 格或 2 格这句话表明当前处在n阶的位置一定是 <从n-1阶爬1格过来的> 或 <从n-2阶爬2格过来的>,所以这道题的动态规划方程是 f(n)=f(n-1)+f(n-2)
// 这里使用迭代法,递归法见下方
let fn = function(n) {
if(n==0) return 0
if(n==1) return 1
if(n==2) return 2
return fn(n-1) + fn(n-2)
}
2. 走迷宫,得到总和的最小值
有个 (m + 1) × (n + 1) 的二维数组,从(0,0)走到(m,n),只能往下走和往右走,要使路线上的数字总和最小。
思路:设走到(m,n)的时候的最小和的值为f(m,n),只能往下走和往右走这句话表明 f(m,n) 的值只能由 f(m-1,n) 和 f(m,n-1) 两个值加上 arr[m][n] 得来的,要哪个取决于哪个比较小,所以这道题的动态规划方程是 f(m,n) = Math.min(f(m-1,n) + f(m,n-1)) + a[m][n]
let f = function(m,n){
if(m<=0 && n<=0) return arr[0][0] // 到达左上角
if(m<=0) return f(m,n-1) + arr[m][n-1] // 上侧碰壁,只能由左向右走
if(n<=0) return f(m-1,n) + arr[m-1][n] // 左侧碰壁,只能由上往下走
return Math.min(f(m,n-1),f(m-1,n)) + a[m][n]
}
const arr = [
[1,2,6,0],
[3,0,5,0],
[4,5,6,2]
]
f(arr.length - 1,arr[0].length - 1) // 结果是 10,路线是 1 + 2 + 0 + 5 + 0 + 2
3. 连续子数组的最大和
思路:最终的结果(连续子数组)一定是以数组中的某一个元素结尾的,先求出以每一个元素结尾的连续子数组的最大和。设以第 n 个元素结尾的连续子数组的最大和是 f(n)。f(n) 的结果只能有两种可能:1. a[n] 单独做一个;2. a[n] 接上之前的结果 f(n - 1)。所以动态规划方程是 f(n) = Math.max(f(n - 1) + a[n],a[n])。再加一步:Math.max(f(0),f(1),f(),...,f(n))
const fn = function(a){
let f = function(n){
if(n <= 0) return a[0]
return Math.max(f(n - 1) + a[n],a[n])
}
let max = 0
for(let i in a){
max = Math.max(max,f(i))
}
return max
}
fn([-8,-2,5,0,3,-1,2,9,-10]) // 18 = sum([5,0,3,-1,2,9])
递归 —— 动态规划的解答方式
两个关键主体:
- 递归基:退出递归的条件
- 递归函数:本质上是动态规划方程
- 求 n!
function fn(n){
if(n == 1) return 1
return n + fn(n-1) // 关键在于理解:n + n-1 + n-2 + ... + 1
}
从尾到头打印链表
// 输入为[1,2,3]
// 回溯过程是 ([3].concat([2].concat([1]))
function fn(head) {
if(head==null) return []
if(head.next==null) return [head.val]
return fn(head.next).concat([head.val])
};
二叉树的中序遍历
二分查找
旋转数组的最小数字
【示例】
- [3,4,5,1,2] // 返回 1
- [6,7,1,2,3,4,5] // 返回 0
【要求】时间复杂度为 O(logn)
【思路】既然要求时间复杂度为 O(logn),那么查找方式势必为 二分查找
- 定义 left、right、得出 center,left 初始值为 0,right 初始值为 length - 1
- 有三种情况:
- nums[center] > nums[right],说明最小值在 nums[center] 的右侧;
- nums[center] < nums[right],说明最小值在 nums[center] 的左侧;
- nums[center] == nums[right],无法判断最小值在 nums[center] 的左侧还是右侧,但我们去掉 nums[right],即
right--,即使 nums[right] 是最小值,我们也还保留着 nums[center] 作为其替代品,因而通过此方法缩小搜查范围。
var findMin = function(numbers) {
if(numbers.length < 2) return numbers[0]
var left = 0
var right = numbers.length - 1
var center = 0
while(left < right){
if(left + 1 === right) return Math.min(numbers[left] , numbers[right])
center = Math.floor((left + right) / 2)
if(numbers[center] > numbers[right]) {
left = center
}
else if(numbers[center] < numbers[right]) {
right = center
}
else {
right--
}
}
};
字典树
哈希表辅助
- 乱序数组去重
- ES6:(时间复杂度O(n)、空间复杂度O(n))借助Set数据类型来实现去重(
Array.from(new Set(arr)) - ES6:(时间复杂度O(n)、空间复杂度O(n))遍历数组,压入哈希表,若哈希表已存在当前key值也会覆盖该key值,最后得到的map里的元素没有重复,最后利用Array.from(map.keys())将Map数据结构转回数组。
- ES5:(时间复杂度O(n²)、空间复杂度O(1))指针p1自左至右遍历数组,指针p2指向最后一个元素,若跟已遍历的重复则与p2指向的元素交换位置,然后p2向左移动一格
- ES5:(时间复杂度O(n)、空间复杂度O(n))自左至右遍历数组,维护一个Object,不断的把当前元素放入Object中(若元素重复则会被覆盖掉),最后将object转回array
- ES6:(时间复杂度O(n)、空间复杂度O(n))借助Set数据类型来实现去重(
- 两个乱序数组/字符串找交集 一个(长的)作哈希表,一个(短的)去遍历