【贰】 数据结构leetcode刷题系列--详解+例题

文章目录

贰 数据结构系列

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该元素未最近使用
  3. 哈希查找快,双链表插入和删除快------> 哈希链表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; }
            
    }
    
  2. 同时维护一个双链表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。
  2. Java :
    • LinkedHashSet 是链表和哈希结合的集合体。
    • 链表查找慢,插入元素具有时序
    • 哈希表元素无序,可以对于元素进行快速访问和删除
  3. JS : map有序,哈哈~!!!!
  4. 两个map && 链表 && 三个Map - LFU 缓存 - 力扣(LeetCode) (leetcode-cn.com))
    • 这题解详细

2-2 二叉搜索树合集

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

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

100. 相同的树

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

五、 98. 验证二叉搜索树

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

  1. 思路如下 :

    • 树为空,返回true
    • 根节点 < 左子节点 || 根节点 > 右子节点,返回false
      • 出现如下问题: image-20220217112104779
    • 需要约束root的左子树 小于 root 以及…
  2. 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);
        
        
    }
    
  2. 一套针对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有两个子结点

    image-20220321220654517

2-3 完全二叉树的节点

222. 完全二叉树的节点个数
  1. 普通二叉树的遍历:

    const countNodes(root){
        if (root === null) return 0;
        return 1 + countNodes(root.left) + countNodes(root.right);
    }
    
  2. 满二叉树的遍历: 节点数和树的高度呈指数关系: 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. 遇到任何递归型的问题,灵魂三问 :
    • 这个函数是干什么的
    • 这个函数参数中的变量是什么
    • 得到参数的递归结果,应该做什么
  2. image-20220403115726848
  3. 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)]
  2. image-20220405092234057

  1. 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 循环数组

  1. /**
     * @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)

  2. 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个,就保持不变
  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值