数据结构及算法系列 -- 核心解法积累

309 阅读21分钟

前言

个人认为编程这项基础功有个亘古不变的解决问题原则:【大而化小】

  1. 把大问题变成小问题:
  • 可以是流程先后的分割(核心)
  • 也可以是分治式分割(需要每一块都差不多,但是只是让问题规模变小,本质上没什么变化)
  1. 然后解决小问题的时候就要看有哪些方案,挑一个最佳的出来解决(有时候很难抉出最佳,需要积累,而且更何况有些方案都是有利有弊的)
  2. 如果当前的小问题解决起来还是很复杂(复杂是因为要顾及的细节还是很多)的话,我们就继续把这个小问题分割成更小的问题,再一个一个小问题去解决。
  3. 等到我们的经验积累到达一定程度时,我们解决问题细分的规模也就越来越大了(就不会像之前那样细分了很多次)

简单来说就是学会一招叫 问题拆解

  • 示例1:旋转链表的问题 → 可以拆解出 → 寻找倒数第 n 个结点的问题
  • 示例2:最长无重复字符的子串 → 可以拆解出 → 寻找各元素左边距离最近的相同元素的位置

而流程上的分割解决(核心)也遵循一个不变的原则:

往小了看就像中学时期做数学题、往大了看就像工作时候做项目:

  1. 每一道问题(无论规模大小)都需要明确 已有条件要求结果
  2. 问题的解决方案有哪些?(手头上有哪些工具?)最佳方案是哪个?可以做到更佳吗?
  3. 问题的先后顺序

解决方案的搜索和对比真的就需要去【积累】,很多东西、事情不可能一蹴而就,很大概率我们会走一些弯路,但有时要停下来反思和总结,才能不断的【积累】获得更好的办法

其实不止是编程,乃至整个社会的每一件事都是遵循整个原则。每一个大项目的完成,都会细化到每一个公司/组织、每一个团队、甚至细化到每一个人,我们都是大项目中的一个环节。

扯远了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. 会对变量的值进行重复更替
    • 例子:将树结构转换为列表结构(层序遍历):

image.png

  • 递归:
    • 思想:大问题拆解成小问题,小问题和大问题本质上看处理方式是一样的(递归基除外)
    • 特点:1. 函数里面调用自己;2. 进入函数之前设立递归基(递归在树、二分、分治等问题中常见)
    • 例子:将列表结构转换为树结构:

image.png

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)

image.png

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 的方式

  1. 使用 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
}

image.png

2. 从 URL 中提取域名

【思路】借助 indexOfslice 方法

  1. 使用 indexOf 方法检测出:所在位置
  2. 使用 slice 方法截取字符串获得新的字符串
  3. 使用 indexOf 方法检测出新的字符串中第一个/出现的位置
  4. 使用 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. 最长回文子串

【思路】每个字符都使用中心扩展法

  1. 自左至右遍历字符串,以 i 为中心向两周扩展(注意分奇偶两种情况)
  2. 借助子函数 fn 进行扩展,碰到边界两个字符一致 则停止
  3. 利用 slice 方法对字符串进行截取获取以 i 为中心的最长回文串
  4. 对当前获取到的最长回文串的长度与之前攒下去的 总的最长回文串 的长度进行对比,择出更长
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. 最长不重复子串

原题:3. 最长无重复字符的子串

示例:

  • "abcabcbb" 的最长不含重复字符的子字符串之一为 "abc",返回其长度 3;
  • "pwwkew" 的最长不含重复字符的子字符串为 "wke",返回其长度 3;
  • "bbbbb" 的最长不含重复字符的子字符串为 "b",返回其长度 1;

【思路1】暴力解法

  1. 遍历每个字符
  2. 统计以它为起点的字符的最长无重复子串
  3. 最终从中选出最长的那个
  • 时间复杂度:O(n²)
  • 空间复杂度:O(1)

【值得优化的点】"pwwkew"中已知以"p"为起点的最长无重复子串为"pw",那么以"w"为起点的最长无重复子串则为"w",不需要 O(n) 的时间去遍历了,只需要 O(n) 的空间存下 "pw",以 O(1) 的时间 shift 掉"p"即可

【思路2】双指针 + 滑动窗口 + 哈希表法

  1. 两根指针:指针 i 作为窗口起点,指针 j 寻找最大窗口的终点
  2. 指针到达最大窗口终点时停下,对窗口大小进行统计
  3. 接下来会进行 i++,所以指针 i 当前指向的元素 s[i] 就得退出滑动窗口了
  • 时间复杂度:O(n)
  • 空间复杂度:O(n)

未命名文件 (5).png

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. 可以使用额外空间对数组查重

【思路】哈希表法

  1. 遍历数组,把各个元素放入哈希表中
  2. 放入哈希表之前注意在哈希表中查看是否已经存在
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(缺点:区分不了数字和字符串)

【注意】适用于纯数字数组或纯字符串数组

【关键步骤】

  1. for...of循环遍历数组,把所有元素当作对象的属性,注意使用toString()方法才能使用obj[]方法添加属性及设置其值,也就是说obj[]里面必须放字符串;
  2. 利用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. 不用额外空间去掉数组中指定某一项

【思路】想要不用额外空间去掉指定项:就得把指定项放到数组尾部

  1. 自左至右寻找指定项后停下;自右至左寻找非指定项的项后停下
  2. 两个元素交换位置
  3. 设置数组长度 / 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. 不用额外空间筛选出数组中指定某一项

【思路】想要不用额外空间筛选出指定项:就得把指定项放到数组头部

  1. 自左至右寻找非指定项后停下;自右至左寻找指定项的项后停下
  2. 两个元素交换位置
  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

  1. 如果当前元素比 min 小则替换 min;
  2. 如果当前元素比 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. 买卖股票的最佳时机 —— 可多次买卖

【原题】122. 买卖股票的最佳时机 II

【思路】变量 flag 表示当前是否持有股票,变量 arr 记录每次股票的买入卖出,变量 sum 表示最大收益

  1. 如果股价呈反弹上升趋势,则在恰好反弹处买入股票
  2. 如果股价呈顶端下降趋势,则在恰好顶端出卖出股票
  3. 对数组 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)

【思路】首先排除两个数:左右两边第一个数不满足条件

  1. 以时间复杂度 O(n) 找到数组各个元素对应的其右边所有元素中的最小元素对应起来;
  2. 然后自左至右找所遍历过的所有元素的最大元素;
  3. 将该元素与其对应的最小元素比较,若小之则满足条件,若大之则不满足条件;
  4. 综上,时间复杂度为 O(n + n)

未命名文件 (2).png

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

  1. p1 指向最左边的非 0 元素
  2. p3 指向最右边的非 2 元素
  3. p2 在中间自左至右遍历数组
    • 若为 p2 指向元素 0 则与 p1 指向元素交换
    • 若为 p2 指向元素 2 则与 p3 指向元素交换 【注意】
  • p2 指针必须在 p1 的右侧,否则就是在做无用功
  • p2 指针不能在 p3 的右侧,否则也是在做无用功

未命名文件 (1).png

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. 它们之间的距离是最近的

【思路】哈希表法

  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. 栈实现队列

【思路】两个栈可以实现一个队列

  1. 将数组按序 push 入栈 A
  2. 将栈 A 的元素一个一个 pop 出压入栈 B
  3. 将栈 B 的元素一个一个 pop 出

image.png

232. 用栈实现队列 - 力扣题目

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. 队列实现栈

【思路】简单粗暴

  1. 将数组按序入队 A
  2. 将队 A 元素按序出队,只留下队尾那个元素
  3. 将该元素放入队 B
  4. 重复 2、3 直至队 A 为空

225. 队列实现栈 - 力扣题目

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. 有效括号的判断(只含有一种括号 {} 或 [] 或 ())

【思路】利用栈

  1. 将字符串拆成字符串数组
  2. 若当前字符为(则直接压入栈
  3. 若当前字符为)则对栈的长度进行判断
    • 若栈空,则直接返回 false
    • 若栈非空,则对栈顶元素进行判断
      • 若栈顶元素为)则直接返回 false
      • 若栈顶元素为(则将栈顶元素出栈
  4. 退出循环后若栈空则返回 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. 有效的括号

【思路】与上一题的区别在于有多种括号,所以需要借助哈希表

  1. 先构建一个哈希表,键为右括号)]},值为左括号([{
  2. 在判断括号是否匹配时就使用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. 升序数组去重

  1. 构造一个栈 stack
  2. 遍历数组
    • 如果【当前遍历的元素跟栈顶元素不同】则入栈
    • 否则不入栈

7. 找出各元素对应的下一个更大值,不存在则返回 -1

【原题】1019. 链表中的下一个更大节点

【思路】单调栈

  • 栈空则元素入栈,将入栈的元素比栈顶元素的小(或等于)也入栈
  • 将入栈的比栈顶元素大则不入栈,并将该要入栈的元素作为栈顶元素的下一个更大值,记录在另外的数组中,同时将栈顶元素 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. 反转链表

【思路】

  1. 保留 next 值
  2. 后结点指向前结点
  3. 指针指向之前保留的 next 值

2. 反转链表的一部分(从 x 到 y)

image.png 【思路】

  1. 反转 x + 1 到 y 所有结点的 next 值
  2. x - 1 指向 y
  3. 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. 合并两个已排序链表

image.png

【思路】五部曲

  1. 各自链表头结点出发,对比谁小,p1 和 p2 两个指针(如:1 比 2 小)
  2. 较小者顺着链表找最接近较大者的(如:1 往后走找最接近 2 但比 2 小的)
  3. 存 p1.next(把结点 1 的 next 值用变量存起来)
  4. 改 p1.next(将结点 1 指向结点 2)
  5. 指针 p1 指向之前存起来的 p1.next。重复回到第 1 步

普通树

数据结构图:

image.png

数据结构:

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);

控制台输出结果:

image.png

广度优先搜索——迭代

思路:两个数组 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);

image.png

二叉树

对于一棵树来讲,除了叶子节点以外的 所有 节点都有这样的特性:

  • 每个节点也都可以看作是一棵树的根节点
  • 并且该节点和其左子树右子树有一定的联系 而树的 叶子节点 不满足这样的特性,所以它们是【停止递归开始反弹回溯】的关键点

这就是递归思想的重要体现 —— 看似变化,实则不变

① 深度优先搜索——递归

二叉树的中序遍历

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. 二叉树的最大深度

image.png

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

image.png

【代码】

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 = [];
    }
};

判断是否为对称二叉树

【思路】迭代方法——层序遍历,得到每一行的所有节点,组成一个数组,判断数组是否为对称数组(左右指针向中间遍历)

image.png

计算二叉树的最大深度

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

  1. 初试状态:p 指向 left - 1 位置的结点;p1 指向 left 位置的结点;p2 指向 left + 1 位置的结点
  2. 规定循环次数为 right - left,反转链表
  3. 由于是反转链表的一部分,所以需要对反转部分的头尾进行处理:
    • 位置为 left 的结点的 next 需要指向位置为 right + 1 的结点
    • 位置为 left - 1 的结点的 next 需要指向位置为 right 的结点
  4. 注意最后返回头指针有两种情况:
    • 若 left 等于 1 时对应的新链表的头指针是 p1
    • 若 left 不等于 1 是对应的新链表的头指针仍是 head

未命名文件.png

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. 升序链表去重 —— 重复的项留一个

【思路】

  1. 通过两个指针遍历,检测重复,定义 p1 指针指向头结点,p2 指针指向第二个结点
  2. 不重复 —— 把 p2 所指交给 p1
  3. 重复 —— 把 p2 的 next 交给 p1 的 next
  4. 无论是否重复,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. 升序链表去重 —— 重复的项全部去掉

【思路】

  1. 设置哑结点指向头结点,防止头结点被删除,定义 p 指针指向哑结点
  2. 不重复 —— p 指针后移即可
  3. 重复 —— 将 p.next 及其后面相同值的所有结点删除
  4. 返回哑结点的 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. 容器装最多的水

【思路】左右指针向中夹击

  1. 两指针分别指向最左和最右,然后往中间遍历
  2. 每次只移动那个指向木板高度较低的指针

证明如下:【反证法】 如果移动指向较高木板的指针,会出现两种情况:

  1. 新木板比原来较高木板低,则容量必定减小;
  2. 新木板比原来较高木板高,则容量也是必定减小,因为容器容量由短板决定,短板高度并没有变化,而容器的横向跨度变小,所以容量减小 综上所述:如果移动指向较高木板的指针,则容器容量必定减小;相反,如果移动指向较低木板的指针,才有可能使容器容量增大
  1. 注意保存最大值:这样的话每次移动对应每次计算一次容量,对 max 值进行更替,最后输出 max 值即可解决问题

8. 判断回文数组/回文串(含有无效字符、忽略大小写)

【思路】

  1. 指针 i 指向数组头,指针 j 指向数组尾,各自向中间靠拢
  2. 使用 while 循环,退出循环条件为 i >= j,该过程中要跳过无效字符,同时要进行大小写转换

使用 正则表达式 来判断是否为有效字符,格式为 /[有效字符集]/.test(待检测字符)

  1. 如果有效字符大小写转换后相同则非回文串;如果顺利结束循环则为回文串
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. 判断回文链表(包含反转链表)

【思路】

  1. 统计链表总长度 len
  2. 遍历到链表中心处,该过程中顺便反转链表的前半段【核心】
  3. 对原链表的前半部分 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. 合并两个有序链表

【示例】

image.png

【思路】五部曲

  1. 各自链表头结点出发,对比谁小,p1 和 p2 两个指针(如:1 比 2 小)
  2. 较小者顺着链表找最接近较大者的(如:1 往后走找最接近 2 但比 2 小的)
  3. 存 p1.next(把结点 1 的 next 值用变量存起来)
  4. 改 p1.next(将结点 1 指向结点 2)
  5. 指针 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])
};

二叉树的中序遍历

二分查找

旋转数组的最小数字

【原题】剑指 Offer 11. 旋转数组的最小数字

【示例】

  • [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] 作为其替代品,因而通过此方法缩小搜查范围。

情况1

情况2

情况3

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
  • 两个乱序数组/字符串找交集 一个(长的)作哈希表,一个(短的)去遍历