arts week15

arts

Posted by woncz on March 14, 2025

Algorithm

LeetCode算法题

17. 电话号码的字母组合 78. 子集 131. 分割回文串

Java 程序实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
class Solution_LC17 {
    String[] alpha = new String[] {"", "", "abc", "def", "ghi", "jkl", "mno", "pqrs", "tuv", "wxyz"};
    List<String> ans = new ArrayList<>();
    char[] path = null;
    String digits = null;

    public List<String> letterCombinations(String digits) {
        if (digits == null || digits.length() == 0) return new ArrayList<>();
        this.digits = digits;
        path = new char[digits.length()];
        dfs(0);
        return ans;
    }

    void dfs(int i) {
        if (i == digits.length()) {
            ans.add(new String(path));
            return;
        }

        int d = digits.charAt(i) - '0';
        for (char c : alpha[d].toCharArray()) {
            path[i] = c;
            dfs(i + 1);
        }
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
class Solution_LC78 {

    List<List<Integer>> ans = new ArrayList<>();
    List<Integer> path = new ArrayList<>();
    int[] nums = null;

    public List<List<Integer>> subsets(int[] nums) {
        this.nums = nums;
        //dfs(0);
        _dfs(0);
        return ans;
    }

    // input-view
    void dfs(int i) {
        // 1.terminator
        if (i == nums.length) {
            ans.add(new ArrayList<>(path));
            return;
        }

        // 2.drill down

        dfs(i + 1); // not select

        path.add(nums[i]); // select
        dfs(i + 1);
        path.remove(path.size() - 1); // recover
    }

    // answer-view
    void _dfs(int i) {
        ans.add(path);

        for (int j = i; j < nums.length; j++) {
            path.add(nums[j]);
            dfs(j + 1);
            path.remove(path.size() - 1);
        }
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
class Solution_LC131 {
    List<List<String>> ans = new ArrayList<>();
    List<String> path = new ArrayList<>();
    String s = null;

    public List<List<String>> partition(String s) {
        this.s = s;
        //dfs(0);
        _dfs(0, 0);
        return ans;
    }

    // answer-view
    void dfs(int i) {
        // terminator
        if (i == s.length()) {
            ans.add(new ArrayList<>(path));
            return;
        }

        // drill down

        for (int j = i; j < s.length(); j++) {
            if (isPalindrome(i, j)) {
                path.add(s.substring(i, j + 1));
                dfs(j + 1);
                path.remove(path.size() - 1);
            }
        }
    }

    // input-view
    void _dfs(int i, int start) {
        if (i == s.length()) {
            ans.add(new ArrayList<>(path));
            return;
        }

        if (i < s.length() - 1) {
            _dfs(i + 1, start);
        }

        if (isPalindrome(start, i)) {
            path.add(s.substring(start, i + 1));
            // why second param is i+1 not start+1?
            _dfs(i + 1, i + 1);
            path.remove(path.size() - 1);
        }
    }

    boolean isPalindrome(int left, int right) {
        while (left < right) {
            if (s.charAt(left++) != s.charAt(right--)) {
                return false;
            }
        }
        return true;
    }
}

Review

梁宁-产品思维30讲

  • 只是一个脑图,印象不很深刻

Bloom Filter

  • 损失精度换取速度
  • 搜索快、插入快,不支持删除
  • 经典使用场景:1 判断URL是否高危需要额外检查(chrome), 2 判断密码太弱提示用户更改
  • B[h1(x)] = ::: = B[hk(x)] = 1, K个哈希函数
  • 发现其中BUG(Example 4.1), 给作者发了Email

Tip

奇怪的缓存一致性问题

缓存穿透

{不存在的Key, 请求打到数据库}

question

  • 定义 缓存穿透是指查询一个数据库中不存在的数据,由于缓存不命中(因为数据根本就不存在),请求便会穿过缓存,直接请求数据库。如果有大量此类请求,数据库压力会突然增大,严重时可能会拖垮数据库。

  • 应对策略

    • 1)布隆过滤器:在缓存之前使用布隆过滤器,一种空间效率高的数据结构,用来检测一个元素是否在一个集合中。如果布隆过滤器说不存在,那么就直接返回,不再查询数据库。
    • 2)缓存空结果:如果查不到数据,也将这个“空”结果缓存起来,并设置一个合理的过期时间。

solution

缓存击穿

{A Hot Key失效, 请求打到数据库}

question

  • 定义 缓存击穿指一个热点 key 在缓存中有效期过期的瞬间,大量请求同时涌入数据库去查询这个数据,因为缓存过期这些请求不能被缓存拦截,直接请求到数据库,导致数据库瞬间压力过大。

  • 应对策略

    • 1)设置热点数据永远不过期:这要求系统能准确判断哪些是热点数据。
    • 2)加锁或队列:当热点 key 过期时,不是所有请求都去数据库查询,而是让某一个请求去数据库查询并更新缓存,其他请求等待缓存更新后再访问缓存。

缓存雪崩

{大量的Key同时失效, 请求打到数据库}

question

  • 定义 缓存雪崩是指在某一个时间段内,大量的缓存键集中过期失效,导致所有的请求都落到数据库上,造成数据库瞬间压力过大可能到达崩溃的状态。

  • 应对策略

    • 1)缓存数据的过期时间设置为随机,防止很多缓存同时过期。
    • 2)使用高可用的缓存架构,即使缓存服务出现问题,也能通过备份机制快速恢复。
    • 3)设置热点数据静态化,即把访问量较大的数据做静态化处理,减少数据库的访问。

数据一致性问题

  • case 数据一致性问题是指当数据在多个地方(如缓存和数据库)存储时,这些地方的数据可能会出现不一致的情况。这种不一致可能是由于缓存更新滞后、系统故障或其他原因引起的。数据一致性是分布式系统设计中的一项挑战,尤其是在读写非常频繁的系统中。当数据被更新时,如果缓存中的相应数据没有立即更新,那么缓存系统将向应用程序提供旧数据。这会导致应用程序得到不一致的结果,影响用户体验和数据的准确性。

  • solution 在分布式系统中,数据一致性是一个核心问题。根据系统的设计与需求,可以选择实时强一致性(Strong Consistency)或最终一致性(Eventual Consistency)。

实时强一致性 CP

  • 定义 实时强一致性保证了任何时刻,所有的客户端看到的数据都是一样的。在分布式系统中实现强一致性意味着,一个操作一旦完成,所有的客户端立即都能看到这个操作的结果。

  • 适用场景 事务性强、对数据一致性要求高的系统,如银行系统或任何财务系统。

  • 保障策略 1)三阶段提交(3PC)等分布式事务协议:在分布式系统中保证操作要么全部成功,要么全部失败。 2)分布式锁:通过在操作前获取全局锁,保证同一时刻只有一个操作可以修改数据,从而保障数据一致性 3)强一致性算法:如 Paxos 或 Raft 算法,通过一系列严格的消息传递和确认机制,确保分布式系统中的多个副本能够达到一致状态。

最终一致性 AP

  • 定义 最终一致性是指系统会保证在没有新的更新操作的情况下,经过足够的时间后,数据将达到一致的状态。在这种模型下,数据的副本之间可能会暂时存在不一致。

  • 适用场景 对实时性要求不高,可以容忍短时间内数据不一致的场景,如社交网络、推荐系统等。

  • 保障策略 1)异步复制:当数据更新发生时,首先更新主副本,然后异步地将更新同步到其他副本。 2)读取修复(Read Repair):在读取数据的时候检测副本之间的不一致,并在后台异步修复不一致的数据。 3)后台一致性修复进程:定期在后台运行的进程检查和同步数据副本之间的差异,以达到最终一致性。 4)版本控制:每次更新数据时附加一个时间戳或版本号,用于解决更新冲突和保持数据的最终一致性。

常见缓存更新/失效策略

缓存更新策略

  • Write through cache(直写缓存):首先将数据写入缓存,然后立即将新的缓存数据复制到数据库。这种方式可以保证写操作的一致性,但可能会影响写操作的性能。

  • Write back cache(写回缓存):数据首先写入缓存,然后由缓存异步写入数据库。这种方式可以提高写操作的性能,但增加了数据丢失的风险。

  • Write around cache(绕写缓存):绕过缓存,直接写数据库,然后依据需要更新缓存或使缓存失效。这适用于更频繁读取操作的场景。

缓存失效策略

  • 主动更新:当数据库数据变化时,主动更新缓存中的数据。这可以保持缓存数据的实时性,但可能会增加系统的复杂性。
  • 定时失效:为缓存数据设置一个过期时间。定期从数据库中重新加载数据,以保持数据的新鲜度。但这无法解决数据在两次加载之间变化导致的一致性问题。
  • 惰性加载:只有在请求特定数据且发现缓存失效或缓存中没有该数据时,才去数据库加载该数据。这种策略简单,但在高并发场景下可能会导致缓存击穿。

数据一致性解决方案

使用缓存一致性协议

  • 基于订阅的更新:使用消息队列(如Kafka,RabbitMQ)来发布数据库更新,然后相关服务订阅这些更新消息来同步更新缓存。
  • 最终一致性:采用最终一致性模型,允许系统在一段时间内是不一致的,但保证经过足够的时间后,系统中的所有复制数据最终将达到一致的状态。

分布式缓存系统

  • 使用如Redis Cluster、Apache Ignite、Tair等分布式缓存系统,这些系统内置了处理缓存一致性的机制,(但是无法解决缓存和数据库之间的数据一致性问题)。

各方案及优缺点

  • 先更新缓存
  • 先更新数据库
  • 先删缓存
  • 延迟双删

diff

曾经落地方案(某信Med)

  • 本地缓存带有失效时间
  • 本地缓存标准情况下不缓存空值
  • 数据库更新完成,事务未提交时,在事务内发布删除缓存消息

Share

回溯算法

回溯算法是一种通过穷举来解决问题的方法,其核心思想是从一个初始状态出发,暴力搜索所有可能的解决方案。当遇到正确的解时,将其记录,直到找到解或尝试了所有可能的选择都无法找到解为止。回溯算法通常采用“深度优先搜索”来遍历解空间,适用于组合问题、排列问题、子集问题等。在实际应用中,回溯算法通过逐步构建问题的解,并在发现当前解不可行时返回上一状态进行调整,直到找到一个满足要求的解。

解题思路

  • 1.从多重循环到回溯 LC17.电话号码的字母组合
  • 2.回溯三问: 思考回溯问题的套路 {一:当前操作or每一步操作? 二:子问题? 三:下一个子问题?}
  • 3.子集型回溯 LC78.子集 LC131.分割回文串 -> {①子集型回溯, ②组合型回溯, ③排列型回溯}
  • 4.两种思路 输入的视角 答案的视角

子集型回溯习题集

  • LC17
  • LC78
  • LC131

组合型回溯习题集

  • LC77
  • LC216
  • LC22

排列型回溯习题集

  • LC46
  • LC51

个人心得

  • 子集型 组合型 : 1) 不选或者选[no select / select, 输入视角 input-view] 2) 枚举选哪个[1->K, 答案视角 answer-view], 组合型可以通过约束条件剪枝[pruning]
  • 排列型 : 每一步操作都需要选择,只有[select]
  • 树形结构 {dfs:遍历} {bfs:最短路径}
  • 一个视角是指[树]的视角,两种思维模式是指[遍历:全排列:递归函数没有返回值,通过全局变量]和[分解问题:斐波那契数列:递归函数有返回值,向上传递]两种思维模式。
  • 与动态规划的联系