文章目录

  • 贰 数据结构系列
  • 2-1 手写LRU(`Least Recently Used`)缓存淘汰算法
  • 2-1-1 讲解
  • 2-1-2 相关例题
  • 一、 [146. LRU 缓存](https://leetcode-cn.com/problems/lru-cache/)
  • 2-2 手写LFU缓存淘汰算法
  • 2-2-1 思路分析
  • 2-2 二叉搜索树合集
  • 2-2-1 判断二叉搜索树的合法性
  • 2-2-2 在BST中查找一个数是否存在
  • 2-2-3 在BST中插入一个数
  • 2-2-4 在BST中删除一个数
  • 2-3 完全二叉树的节点
  • [222. 完全二叉树的节点个数](https://leetcode-cn.com/problems/count-complete-tree-nodes/)
  • 2-4 各种遍历框架序列化和反序列化 二叉树
  • 2-4-1 前序二叉树
  • 2-4-2 后序二叉树
  • 2-4-3 层序二叉树
  • 2-5 Git 原理之二叉树 最近公共祖先
  • 2-6 单调栈
  • 2-6-1 单调栈的解题模板
  • 2-6-2 循环数组
  • 2-6-3 [556. 下一个更大元素 III](https://leetcode-cn.com/problems/next-greater-element-iii/)
  • 2-7 特殊数据结构 --- 单调队列
  • 2-7-1框架 :
  • 2-7-2
  • 一、 [239. 滑动窗口最大值](https://leetcode-cn.com/problems/sliding-window-maximum/)
  • 二、 [480. 滑动窗口中位数](https://leetcode-cn.com/problems/sliding-window-median/)
  • 三、 [剑指 Offer 59 - I. 滑动窗口的最大值](https://leetcode-cn.com/problems/hua-dong-chuang-kou-de-zui-da-zhi-lcof/)
  • 四、 [剑指 Offer II 041. 滑动窗口的平均值](https://leetcode-cn.com/problems/qIsx9U/)
  • 2-7-3 [232. 用栈实现队列](https://leetcode-cn.com/problems/implement-queue-using-stacks/)
  • 2-8 判断回文链表
  • 2-8-1
  • 2-8-2 双指针技巧【我不当独秀,不学了不学了】
  • 2-9 【秀操作】递归反转链表
  • 2-9-1
  • 2-10 k个一组反转链表


贰 数据结构系列

2-1 手写LRU(Least Recently Used)缓存淘汰算法

JS中的Map是有序的!!!!

2-1-1 讲解

  1. LRU : least Recently Used
  2. put和get方法的时间复杂度为O(1),cache必备 【任意位置快速插入和删除元素】:
  • cache有时序,区分最近使用的数据,满了之后删除最久未使用的数据
  • cache中快速找某个key是否存在并得到对应的val
  • 每次访问cache中的某个key,需要get该元素未最近使用
  1. 哈希查找快,双链表插入和删除快------> 哈希链表LinkedHaspMap

2-1-2 相关例题

一、 146. LRU 缓存

  1. 自己实现LRU算法
class LRUCache {
    private HaspMap<Integer, Node> map;
    private DoubleList cache;
    // 最大容量
    private int cap;
    
    public LRUCache(int capacity) {
        this.cap = capacity;
        map = new HashMap<>();
        cache = new DoubleList();
    }
}

// ---------------------实现put和get方法------------
public int get(int key) {
    if(!map.containsKey(key)) {
        return -1;
    }
    // 将该数据提升为最近使用的
    makeRecently(key);
    return map.get(key).val;
}
public ivoid put(int key, int val) {
    if (map.containsKey(key)) {
        
    }
}
// -----------------抽象一层API-----------------
// 将某个key提升为最近使用
private void addRecently(int key, int val) {
    Node x = map.get(x);
    // 先从链表中删除这个节点
    cache.addLast(x);
    // 重新插入队尾
    cache.addLast(x);
}
// 添加最近使用的元素
private void addRecently(int key, int val) {
    Node x = new Node(key, val);
    // 链表尾部就是最近使用的元素
    cache.addLast(x);
    // map中添加key的映射
    map.put(key, x);
}

// 删除某一个key
private void deleteKey(int key) {
    Node x = map.get(key);
    // 从链表中删除
    cache.remove(x);
    // 从map中删除
    map.remove(key);
}

// 删除最旧未使用的元素
private void removeLeastRecently() {
    // 链表头部的第一个元素最久未使用
    Node deletedNode = cache.removeFirst();
    // 别忘了从map中删除key
    int deletedKey = deleteNode.key;
    map.remove(deletedKey);
}

// -------------以下是双向链表的实现-----------------

// 双链表的节点类
class Node {
    public int key,val;
    public Node next,prev;
    public Node(int k, int v) {
        this.key = k;
        this.val = v;
    }
}

// 依靠Node类型构建一个双链表
class DoubleList {
    // 头尾虚节点
    private Node head, tail;
    // 链表元素数
    private int size;
    
    public DoubleList() {
        /// 初始化双向链表的数据
        head = new  Node(0,0);
        tail = new Node(0,0);
        head.next = tail;
        tail.pre = head;
    }
    
    // 链表尾部添节点x
    public void addLast(Node x) {
        x.prev = tail.prev;
        x.next = tail;
        tail.prev.next = x;
        tail.prev = head;
        size = 0;
    }
    
    // 删除链表中的x节点---》 使用双向链表,完成删除操作,需要得到前驱节点的指针
   // 尾部的元素最近使用
    public void remove(Node x) {
        x.prev.next = x.next;
        x.next.prev = x.prev;
        size---;
    }
    
    // 删除链表第一个节点,返回该节点
    public Node removeFirst() {
        if(head.next === tail)
            return null;
       	Node first = head.next;
        remove(first);
        return first;
    }
    
    // 返回链表长度
    public int size() { return size; }
        
}
  1. 同时维护一个双链表cache和哈希表map, 遇到问题 :
  • 删除某个key时,在cache中删除对应的Node,忘记在map中删除key
  • 解决方法 : 在两种数据结构上抽象出一层抽象API

2-2 手写LFU缓存淘汰算法

460. LFU 缓存

2-2-1 思路分析

  • 调用get (key)时: 返回该key对应val
  • getput方法访问一次某个key,该key的freq加一
  • 如果在容量满时插入,需要将freq最小的key删除
  • 最小的freq对应多个key时,删除最旧的
  1. 希望在O(1)时间复杂度解决,使用基本数据结构
  • HashMap存储key到val的映射,快速计算get(key)
  • HashMap<Integer, Integer> keyToVal;
  • 使用一个HashMap存储key到freq的映射,快速操作key对应freq
  • HashMap<Integer, Interger> keyToFreq;
  • 核心需求 :
  • 需要Freq到key的映射
  • 将freq最小的key删除,快速得到当前所有key最小的freq是多少。O(1)-----> minFreq
  • 多个key拥有相同的freq,freq对key是一对多的关系
  • freq对应的key的列表存在时序
  • 快速删除 key列表片中的任何一个key,频次为freq的某个key被访问,频次就会变成freq+1。
  1. Java :
  • LinkedHashSet 是链表和哈希结合的集合体。
  • 链表查找慢,插入元素具有时序
  • 哈希表元素无序,可以对于元素进行快速访问和删除
  1. JS : map有序,哈哈~!!!!
  2. 两个map && 链表 && 三个Map - LFU 缓存 - 力扣(LeetCode) (leetcode-cn.com))
  • 这题解详细

2-2 二叉搜索树合集

二叉树的设计总路线:明确一个节点要做的事情,剩下的抛给框架

2-2-1 判断二叉搜索树的合法性

100. 相同的树

面试题 04.05. 合法二叉搜索树

五、 98. 验证二叉搜索树

纳闷了就,做以前以为自己学会了,结果 n次bug通不过,回去看题解,给我整不会了????

  1. 思路如下 :
  • 树为空,返回true
  • 根节点 < 左子节点 || 根节点 > 右子节点,返回false
  • 出现如下问题:
  • 需要约束root的左子树 小于 root 以及…
var isValidBST = function(root) {
    return isValidBST2(root,null,null);
};

let isValidBST2 = (root,min,max) => {
    if(root === null) return true;
    if(min != null && root.val <= min.val) return false;
    if(max != null && root.val >= max.val) return false;
    // 确保右子树小于 最大值root,确保左子树大于最小值root
    return isValidBST2(root.left, min, root) && isValidBST2(root.right, root, max);
}

2-2-2 在BST中查找一个数是否存在

700. 二叉搜索树中的搜索

  1. 二分搜索思想:
const isBST(TreeNode root, int target) {
    // root 该做的事
    if (root === null) return false;
    if (root.val === target) return true;
    if(root.val < target) 
        return isInBST(root.right, target);
    if(root.val > target) 
        return isInBST(root.left, target);
    
    
}
  1. 一套针对BST的遍历框架
const BST(TreeNode root,int target) {
    if(root.val === target)
        // 找到目标,做事情
    if(root.val < target)
        BST(root.right, target);
    if (root.val > target) 
        BST(root.left, target);
}

2-2-3 在BST中插入一个数

TreeNode insertIntoBST(TreeNode root, int val) {
    // 找到空位置 插入新节点
    if (root === null) return new TreeNode(val);
    // 如果已经存在,不要再重复插入,直接返回
    if (root.val === val){
        return root;
    }
    if(root.val < val){
        root.right = insertIntoBST(root.right, val);
    if(root.val >val)
        root.left = insertIntoBST(root.left, val);
    return root;
        
    }
}

2-2-4 在BST中删除一个数

450. 删除二叉搜索树中的节点

  1. 框架 :
TreeNode deleteNode(TreeNode root, int key) {
    if (root.val === key) {
        // 找到删除
    } else if (root.val > key){
        root.left = deleteNode(root.left, key);
    } else if (root.val < key){
        // 去右子树寻找key
        root.right = deleteNode(root.right, key);
    }
    return root;
}
  1. 删除方法实现
  • A恰好是末端节点
  • A只有一个非空子结点
  • A有两个子结点

leetcode java 题解 leetcode例题_二叉树

2-3 完全二叉树的节点

222. 完全二叉树的节点个数

  1. 普通二叉树的遍历:
const countNodes(root){
    if (root === null) return 0;
    return 1 + countNodes(root.left) + countNodes(root.right);
}
  1. 满二叉树的遍历: 节点数和树的高度呈指数关系: 2h-1
const countNodes(TreeNode root){
    let h =0;
    while(root !== null) {
        root = root.left;
        h++;
    }
    // 节点总数为
    return Math.pow(2,h) - 1;
}

2-4 各种遍历框架序列化和反序列化 二叉树

序列化和反序列化 :

  • 以某种固定格式组织字符串,使得数据独立于编程语言
  • --------------> 序列化--------->接收JSON字符串-----> 反序列化----------------> 原始数据

331. 验证二叉树的前序序列化

剑指 Offer II 048. 序列化与反序列化二叉树

剑指 Offer 37. 序列化二叉树

449. 序列化和反序列化二叉搜索树

297. 二叉树的序列化与反序列化

2-4-1 前序二叉树

  1. 反序列化 : 序列化列表的第一个元素就是一棵树的根节点,只要将列表的第一个元素取出作为根节点,剩下的交给递归函数

2-4-2 后序二叉树

  1. 反序列化时,找列表的最后一个元素

2-4-3 层序二叉树

2-5 Git 原理之二叉树 最近公共祖先

Git的rebase 引出一个经典的算法问题:最近公共祖先(Lowest Common Ancestor,简称LCA)

  • git pull
  • git pull -r

剑指 Offer 68 - II. 二叉树的最近公共祖先

剑指 Offer 68 - I. 二叉搜索树的最近公共祖先

236. 二叉树的最近公共祖先

235. 二叉搜索树的最近公共祖先

  1. 遇到任何递归型的问题,灵魂三问 :
  • 这个函数是干什么的
  • 这个函数参数中的变量是什么
  • 得到参数的递归结果,应该做什么
  1. lowestCommonAncestor(TreeNode root, TreeNode p, TreeNode q)
  • p,q都在以root为根的树中,函数返回的即是p和q的最近公共祖先节点
  • 如果p/q都不在以root为根的树中 : 函数返回null
  • 如果p和q只有一个存在于以root为根的树中

2-6 单调栈

先进后出

  • 单调栈:使得每次新元素入栈后,栈内的元素保持单调
  • 只解决Next Greater Element

739. 每日温度

496. 下一个更大元素 I : 两个数组

556. 下一个更大元素 III

503. 下一个更大元素 II

2-6-1 单调栈的解题模板

  1. [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-TZH7Uh9l-1650208185247)(https://gitee.com/hannah_bingo/yyy/raw/master/image-20220405092146085.png)]
var nextGreaterElement = function (nums1, nums2) {
  let stack = [];
  let map = new Map();
  for (let i = 0; i < nums2.length; i++) {
    while (stack.length && nums2[i] > nums2[stack[stack.length - 1]]) {
      let index = stack.pop();
      map.set(nums2[index], nums2[i]);
    }
    stack.push(i);
  }

  let res = [];
  for (let j = 0; j < nums1.length; j++) {
    res[j] = map.get(nums1[j]) || -1;
  }

  return res;
};

2-6-2 循环数组

/**
 * @param {number[]} nums
 * @return {number[]}
 */
var nextGreaterElements = function(nums) {
    const m = nums.length;
    const res = new Array(m).fill(0);
    const stack = [];
    // 将数组长度翻倍
    for (let i = 2*m-1; i >= 0; i--){  // 反向入栈
        while (stack.length  && nums[i%m] >= stack[stack.length -1]) {  // 大于栈顶元素
            stack.pop();
        }
        // 利用 % 求模 防止索引越界
        res[i%m] = stack.length ? stack[stack.length -1] : -1;
        stack.push(nums[i%m]);
    }
    return res;
};

2-6-3 556. 下一个更大元素 III

/**
 * @param {number} n
 * @return {number}
 */
// function nextGreaterElement(n) {
//   let res = 0
//   let q = []
//   let str = Array.from(String(n))//字符串数组

//   for (let i = str.length - 1; i >= 0; i--) {
//     if (q.length === 0 || str[i] >= q[q.length - 1]) q.push(str[i]) ;
//     else {
//       let count = 0
//       // 出栈,记录出栈的位数
//       while (q.length !== 0 && str[i] < q[q.length - 1]) {
//         q.pop()
//         count++
//       }
//       [str[i], str[i + count]] = [str[i + count], str[i]]  // swap元素
//       res = parseInt(
//         str.slice(0, i + 1).join('') +
//         str.slice(i + 1).reverse().join('')
//       ) // 反转右边
//       return res >= 2 ** 31 - 1 ? -1 : res
//     }
//   }

//   return -1
// }

/**
 * @param {number} n
 * @return {number}
 */
// var nextGreaterElement = function(n) {
//     let s = ('' + n).split('');
//     let i = s.length - 2;
//     while (s[i] >= s[i + 1]) i--; // 从右开始,找第一个严格降序的数字
//     if (i < 0) return -1; // 不存在,返回-1
//     let j = s.length - 1;
//     while (s[j] <= s[i]) j--; // 从右开始,找到第一个比上一步找到数字大的
//     [ s[i], s[j] ] = [ s[j], s[i] ]; // 换位
//     let res = parseInt(s.slice(0, i + 1).join('') + s.slice(i + 1).reverse().join('')); // 反转右边
//     return res >= 2 ** 31 - 1 ? -1 : res;
// };

/**
 * @param {number} n
 * @return {number}
 */
var nextGreaterElement = function(n) {
    // 把 n 字符串化
    const nStr = n + ''
    const len = nStr.length
    // 我们从后往前遍历,找到第一个比他后面的最大数字小的一个数字,这个位置的数字就是我们要更换的数字
    let max = nStr[len - 1]

    // 已经遍历过的数字组成的数字,当我们已经更换的数字更换了之后,这个数组就保存更换数字之后的所有数字,我们取得这个数组能组成的数字的最小值拼接在后面即可
    const after = [max]

    // 需要更换的数字的位置
    let before = -1
    for (let i = len - 2; i >= 0; i--) {
        if (nStr[i] < max) {
            before = i
            break;
        } else {
            max = nStr[i]
            after.unshift(nStr[i])
        }
    }

    // 当没有需要更换的数字,表示重排找不到比 n 最大的数字了,返回 -1
    if (before < 0) return -1

    // 保存需要更换的数字
    let num = nStr[before]

    // after数组从小到大排序,到时候直接拼接就是能组合的最小数字
    after.sort((a, b) => a - b)

    // 遍历after数组,找到第一个比 num 大的数,来和 num 做交换
    for (let i = 0, len = after.length; i < len; i++) {
        if (after[i] > num) {
            const temp = after[i]
            after[i] = num
            num = temp
            break
        }
    }

    // 拼接数字
    // 需要更换的数字之前 + 更换的后数字 + 更换的数字位置之后的所有数字能组成的最小数字
    const ret = +(nStr.slice(0, before) + num + after.join(''))
    
    return ret <= (2 ** 31 - 1) ? ret : -1
};

2-7 特殊数据结构 — 单调队列

2-7-1框架 :

int[] maxSlidingWindow(int[] nums, int k) {
    MonotonicQueue window = new MonotonicQueue();
    List<Integer> res = new ArrayList<>();
    
    for(int i = 0; i<nums.length; i++){
        if(i <k-1){
            // 先把 窗口的前 k-1 填满
            window.push(nums[i]);
        } else {
            // 窗口开始向前滑动
            // 移入新元素
            window.push(nums[i]);
            // 将当前窗口中的最大元素计入结果
            res.add(window.max());
            // 移出最后的元素
            window.pop(nums[i- k + 1]);
        }
    }
    // 将List 类型转换为int[]数组作为返回值
    int[] arr = new int[res.size()];
    for (int i = 0; i< res.size(); i++){
        arr[i] = res.get(i);
    }
    return arr;
}

2-7-2

一、 239. 滑动窗口最大值

队列中只有一个,队头持续是最大的

  1. 若队列不为空,且当前元素大于等于队尾所存下标的元素,则弹出队尾
  2. 入队当前元素下标
  3. 判断当前最大值(即队首元素)是否在窗口中,如不在便将其出队
  4. 当达到窗口大小时 开始 向结果中添加数据
var maxSlidingWindow = function (nums, k) {
  // 队列数组(存放的是元素下标,为了取值方便)
  const q = [];
  // 结果数组
  const ans = [];
  for (let i = 0; i < nums.length; i++) {
    // 若队列不为空,且当前元素大于等于队尾所存下标的元素,则弹出队尾
    while (q.length && nums[i] >= nums[q[q.length - 1]]) {
      q.pop();
    }
    // 入队当前元素下标
    q.push(i);
    // 判断当前最大值(即队首元素)是否在窗口中,若不在便将其出队
    while (q[0] <= i - k) {
      q.shift();
    }
    // 当达到窗口大小时便开始向结果中添加数据
    if (i >= k - 1) ans.push(nums[q[0]]);
  }
  return ans;
};

。
二、 480. 滑动窗口中位数
三、 剑指 Offer 59 - I. 滑动窗口的最大值
四、 剑指 Offer II 041. 滑动窗口的平均值

2-7-3 232. 用栈实现队列

  1. 看看这里的动画,会好很多 : 232. 用栈实现队列:【两个栈来模拟队列】详解 - 用栈实现队列 - 力扣(LeetCode) (leetcode-cn.com)
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-8 判断回文链表

2-8-1

234. 回文链表

剑指 Offer II 027. 回文链表

剑指 Offer II 018. 有效的回文

/**
 * Definition for singly-linked list.
 * function ListNode(val, next) {
 *     this.val = (val===undefined ? 0 : val)
 *     this.next = (next===undefined ? null : next)
 * }
 */
/**
 * @param {ListNode} head
 * @return {boolean}
 */
var isPalindrome = function(head) {
    let left = 0;
    const res = [];
    let sign = true;
    while(head !== null){
        res.push(head.val);
        head = head.next;
    }
    // console.log(res)
    let right = res.length-1;
    while(left < right) {
        if(res[left]!== res[right]) {
            sign = false
        }
        left++;
        right--;
    }
    return sign
};
/**
 * @param {string} s
 * @return {boolean}
 */
var isPalindrome = function(s) {
    let str = s.toLowerCase()
    let len = str.length
    let arr = []
    for(let i = 0;i < len;i++){
        if(str.charCodeAt(i) >= 97 && str.charCodeAt(i) <= 122) arr.push(str[i])
        if(str.charCodeAt(i) >= 48 && str.charCodeAt(i) <= 57) arr.push(str[i])
    }
    if(arr === []) return true
    str = arr.join('')
    str2 = arr.reverse().join('')
    if(str === str2) return true
    return false
};

2-8-2 双指针技巧【我不当独秀,不学了不学了】

2-9 【秀操作】递归反转链表

2-9-1

  1. 92. 反转链表 II

2-10 k个一组反转链表

  1. base case : 如果最后的元素不足k个,就保持不变