【3.1】MySQL锁、动态规划、Redis缓存,过期删除与淘汰策略

news2024/10/2 20:23:28

5.4 MySQL死锁了,怎么办?

  • RR隔离级别下,会存在幻读的问题,InnoDB为了解决RR隔离级别下的幻读问题,就引出了next-key 锁,是记录锁和间隙锁的组合。

    • Record Lock,记录锁,锁的是记录本身;

    • Gap Lock,间隙锁,锁的就是两个值之间的空隙,以防止其他事务在这个空隙间插入新的数据,从而避免幻读现象。

  • 我们可以执行 select * from performance_schema.data_locks\G; 语句 ,确定事务加了什么类型的锁。

为什么会产生死锁?

  • 通过举例体现死锁问题:

    1. 建了一张订单表,其中 id 字段为主键索引,order_no 字段普通索引,也就是非唯一索引(二级索引):

      CREATE TABLE `t_order` (
        `id` int NOT NULL AUTO_INCREMENT,
        `order_no` int DEFAULT NULL,
        `create_date` datetime DEFAULT NULL,
        PRIMARY KEY (`id`),
        KEY `index_order` (`order_no`) USING BTREE
      ) ENGINE=InnoDB ;
      # 插入六条记录,id 1-6 、order_on 1001-1006
      
    2. 事务A执行:给订单做幂等性校验,目的是为了保证不会出现重复的订单。

      SELECT id FROM t_order WHERE `order_no` = 1007 for UPDATE;
      

      执行该语句,事务A在二级索引加了X型next-key锁,范围是(1006 , +∞]。

    3. 事务B执行:插入数据。

      Insert into t_order (order_no, create_date) values (1008, now());
      

      Insert 语句在正常执行时是不会生成锁结构的,它是靠聚簇索引记录自带的 trx_id 隐藏列来作为隐式锁来保护记录的。但此时记录之间加有间隙锁,隐式锁会转换为显示锁

      如果已加间隙锁,此时会生成一个插入意向锁,然后锁的状态设置为等待状态,现象就是Insert语句被阻塞。

      • 因为插入意向锁与间隙锁是冲突的,所以当其它事务持有该间隙的间隙锁时,需要等待其它事务释放间隙锁之后,才能获取到插入意向锁。
  • 间隙锁与间隙锁之间是兼容的吗?

    • 间隙锁的意义只在于阻止区间被插入,因此是可以共存的。一个事务获取的间隙锁不会阻止另一个事务获取同一个间隙范围的间隙锁,共享和排他的间隙锁是没有区别的,他们相互不冲突,且功能相同,即两个事务可以同时持有包含共同间隙的间隙锁。
    • next-key lock 是包含间隙锁+记录锁的,如果一个事务获取了 X 型的 next-key lock,那么另外一个事务再获取相同范围的 X 型的 next-key lock 时,是会被阻塞的。但是,对于这种范围为 (1006, +∞] 的 next-key lock,两个事务是可以同时持有的,不会冲突。因为 +∞ 并不是一个真实的记录,自然就不需要考虑 X 型与 S 型关系。
  • 插入意向锁是什么?

    • 插入意向锁是一种特殊的间隙锁,但不同于间隙锁的是,该锁只用于并发插入操作。如果说间隙锁锁住的是一个区间,那么「插入意向锁」锁住的就是一个点。因而从这个角度来说,插入意向锁确实是一种特殊的间隙锁。

Insert语句是怎么加行级锁的?

隐式锁就是在 Insert 过程中不加锁,只有在特殊情况下,才会将隐式锁转换为显示锁。

  • 如果记录之间加有间隙锁,为了避免幻读,此时是不能插入记录的,插入意向锁会被设置为等待状态

  • 如果 Insert 的记录和已有记录存在唯一键冲突,此时也不能插入记录,会对这条记录加上S型的锁

    • 至于是记录锁,还是 next-key 锁,跟是「主键冲突」还是「唯一二级索引冲突」有关系。
      • 如果主键冲突:(RR,RC级别)给已存在的主键索引记录添加S型记录锁
      • 如果唯一二级索引冲突:(RR,RC级别)给已存在的二级索引记录添加S型next-key锁

如何避免死锁?

死锁的四个必要条件:互斥、占有且等待、不可强占用、循环等待。只要系统发生死锁,这些条件必然成立,但是只要破坏任意一个条件就死锁就不会成立。

  • 在数据库层面,有两种策略通过「打破循环等待条件」来解除死锁状态:
    • 设置事务等待锁的超时时间。当一个事务的等待时间超过该值后,就对这个事务进行回滚,于是锁就释放了,另一个事务就可以继续执行了。在 InnoDB 中,参数 innodb_lock_wait_timeout 是用来设置超时时间的,默认值时 50 秒。
    • 开启主动死锁检测。主动死锁检测在发现死锁后,主动回滚死锁链条中的某一个事务,让其他事务得以继续执行。将参数 innodb_deadlock_detect 设置为 on,表示开启这个逻辑,默认就开启。

5.5 字节面试: 加了什么锁,导致死锁的?

创建一张学生表:其中id为主键索引,其他都是普通字段。

image-20230301141107054

执行事务:事务 A 和 事务 B 都在执行 insert 语句后,都陷入了等待状态(前提没有打开死锁检测),也就是发生了死锁,因为都在相互等待对方释放锁。

img
  • Time1阶段:此时事务 A 在主键索引(INDEX_NAME : PRIMARY)上加的是间隙锁,锁范围是(20, 30)。(唯一索引等值查询,查询id不在索引中,退化成间隙锁)
  • Time2阶段:此时事务B在主键索引上加的是也是间隙锁,和事务A相同。
  • Time3阶段:事务 A 的状态为等待状态(LOCK_STATUS: WAITING),因为向事务 B 生成的间隙锁(范围 (20, 30))中插入了一条记录,所以事务 A 的插入操作生成了一个插入意向锁(LOCK_MODE:INSERT_INTENTION)。
  • Time4阶段:与Time3阶段相同。

本次案例中,事务 A 和事务 B 在执行完后 update 语句后都持有范围为(20, 30)的间隙锁,而接下来的插入操作为了获取到插入意向锁,都在等待对方事务的间隙锁释放,于是就造成了循环等待,满足了死锁的四个条件:互斥、占有且等待、不可强占用、循环等待,因此发生了死锁。

Redis过期删除与内存淘汰

Redis使用的过期删除策略是什么?

Redis 是可以对 key 设置过期时间的,因此需要有相应的机制将已过期的键值对删除,而做这个工作的就是过期键值删除策略。

  • Redis的过期字典中,保存了数据库中所有key的过期时间。当我们查询一个key,Redis会首先检查key是否存在与过期字典中。
    • 如果不再,正常获取键值。
    • 如果存在,则会进行比较时间之后判断。
  • Redis 使用的过期删除策略是「惰性删除+定期删除」这两种策略配和使用。
    • 惰性删除策略的做法是,不主动删除过期键,每次从数据库访问 key 时,都检测 key 是否过期,如果过期则删除该 key。
      • 优点:此策略使用很少的系统资源,对CPU最友好。缺点:过期的key会一直占用系统资源,对内存不友好。
    • 定期删除策略的做法是,每隔一段时间「随机」从数据库中取出一定数量的 key 进行检查,并删除其中的过期key。
      • 优点:通过限制删除操作执行时长和频率,可以减少对CPU的影响,同时也能删除部分数据,避免对内存的影响。缺点:难以确定删除操作执行时长和频率。

Redis持久化时,对过期键会如何处理?

Redis持久化文件有两种格式:RDB(Redis Database)和 AOF(Append Only File)。

  • RDB分为两个阶段,为生成阶段和加载阶段。

    • RDB 文件生成阶段:从内存状态持久化成 RDB(文件)的时候,会对 key 进行过期检查,过期的键「不会」被保存到新的 RDB 文件中,因此 Redis 中的过期键不会对生成新 RDB 文件产生任何影响。
    • RDB 加载阶段:RDB 加载阶段时,要看服务器是主服务器还是从服务器,分别对应以下两种情况:
      • 如果 Redis 是「主服务器」运行模式的话,在载入 RDB 文件时,程序会对文件中保存的键进行检查,过期键「不会」被载入到数据库中
      • 如果 Redis 是「从服务器」运行模式的话,在载入 RDB 文件时,不论键是否过期都会被载入到数据库中。但由于主从服务器在进行数据同步时,从服务器的数据会被清空。所以一般来说,过期键对载入 RDB 文件的从服务器也不会造成影响。
  • AOF 文件分为两个阶段,为写入阶段和 AOF 重写阶段。

    • AOF 文件写入阶段:当 Redis 以 AOF 模式持久化时,如果数据库某个过期键还没被删除,那么 AOF 文件会保留此过期键,当此过期键被删除后,Redis 会向 AOF 文件追加一条 DEL 命令来显式地删除该键值
    • AOF 重写阶段:执行 AOF 重写时,会对 Redis 中的键值对进行检查,已过期的键不会被保存到重写后的 AOF 文件中,因此不会对 AOF 重写造成任何影响。

Redis 主从模式中,对过期键会如何处理?

当 Redis 运行在主从模式下时,从库不会进行过期扫描,从库对过期的处理是被动的。也就是即使从库中的 key 过期了,如果有客户端访问从库时,依然可以得到 key 对应的值,像未过期的键值对一样返回。

从库的过期键处理依靠主服务器控制,主库在 key 到期时,会在 AOF 文件里增加一条 del 指令,同步到所有的从库,从库通过执行这条 del 指令来删除过期的 key。

Redis内存淘汰策略

在 Redis 的运行内存达到了某个阀值,就会触发内存淘汰机制,这个阀值就是我们设置的最大运行内存,此值在 Redis 的配置文件中可以找到,配置项为 maxmemory。

Redis 内存淘汰策略共有八种,这八种策略大体分为「不进行数据淘汰」和「进行数据淘汰」两类策略。

  1. 不进行数据淘汰的策略
    • noeviction(不进行驱逐)(Redis3.0之后,默认的内存淘汰策略) :它表示当运行内存超过最大设置内存时,不淘汰任何数据,而是不再提供服务,直接返回错误。
  2. 进行数据淘汰的策略
    • 在设置了过期时间的数据中进行淘汰:
      • volatile-random随机淘汰设置了过期时间的任意键值;
      • volatile-ttl(Time-To-Live):优先淘汰更早过期的键值。
      • volatile-lru(Least recently used,最近最少使用)(Redis3.0 之前,默认的内存淘汰策略):淘汰所有设置了过期时间的键值中,优先淘汰最久未用的键值;
      • volatile-lfu(Least Frequently Used,最近最不常用)(Redis 4.0 后新增的内存淘汰策略):淘汰所有设置了过期时间的键值中,优先淘汰最近最不常用的键值;
    • 在所有数据范围内进行淘汰:
      • allkeys-random:随机淘汰任意键值;
      • allkeys-lru:淘汰整个键值中最久未用的键值;
      • allkeys-lfu(Redis 4.0 后新增的内存淘汰策略):淘汰整个键值中最不常用的键值。

LRU 算法和 LFU 算法有什么区别?

  • LRU(最近最少使用)算法根据时间淘汰,LFU(最近最不常用)算法根据使用次数淘汰。

    • 在 LRU 算法中,Redis 对象头的 24 bits 的 lru 字段是用来记录 key 的访问时间戳,因此在 LRU 模式下,Redis可以根据对象头中的 lru 字段记录的值,来比较最后一次 key 的访问时间长,从而淘汰最久未被使用的 key。

    • 在 LFU 算法中,Redis对象头的 24 bits 的 lru 字段被分成两段来存储,高 16bit 存储 ldt(Last Decrement Time),低 8bit 存储 logc(Logistic Counter)。

      • ldt 是用来记录 key 的访问时间戳;
      • logc 是用来记录 key 的访问频次,它的值越小表示使用频率越低,越容易淘汰,每个新加入的 key 的logc 初始值为 5。logc 并不是单纯的访问次数,而是访问频次(访问频率),因为 logc 会随时间推移而衰减的
  • LRU全称是 Least Recently Used 翻译为最近最少使用,会选择淘汰最近最少使用的数据。

    • 传统 LRU 算法的实现是基于「链表」结构,链表中的元素按照操作顺序从前往后排列,最新操作的键会被移动到表头,当需要内存淘汰时,只需要删除链表尾部的元素即可,因为链表尾部的元素就代表最久未被使用的元素。
    • Redis 并没有使用这样的方式实现 LRU 算法,因为传统的 LRU 算法存在两个问题:
      • 需要用链表管理所有的缓存数据,这会带来额外的空间开销;
      • 当有数据被访问时,需要在链表上把该数据移动到头端,如果有大量数据被访问,就会带来很多链表移动操作,会很耗时,进而会降低 Redis 缓存性能。
    • Redis 实现的是一种近似 LRU 算法,目的是为了更好的节约内存,它的实现方式是在 Redis 的对象结构体中添加一个额外的字段,用于记录此数据的最后一次访问时间
    • 当 Redis 进行内存淘汰时,会使用随机采样的方式来淘汰数据,它是随机取 5 个值(此值可配置),然后淘汰最久没有使用的那个
    • Redis 实现的 LRU 算法的优点:
      • 不用为所有的数据维护一个大链表,节省了空间占用;
      • 不用在每次数据访问时都移动链表项,提升了缓存的性能;
    • 但是 LRU 算法有一个问题,无法解决缓存污染问题,比如应用一次读取了大量的数据,而这些数据只会被读取这一次,那么这些数据会留存在 Redis 缓存中很长一段时间,造成缓存污染。引入LFU算法解决了这个问题。
  • LFU 全称是 Least Frequently Used 翻译为最近最不常用,LFU 算法是根据数据访问次数来淘汰数据的。

    • Redis 在访问 key 时,对于 logc 是这样变化的:
      1. 先按照上次访问距离当前的时长,来对 logc 进行衰减;
      2. 然后,再按照一定概率增加 logc 的值。

Redis缓存设计

如何避免缓存雪崩、缓存击穿、缓存穿透?

  • 缓存的作用

    通常我们为了保证缓存中的数据与数据库中的数据一致性,会给 Redis 里的数据设置过期时间,当缓存数据过期后,用户访问的数据如果不在缓存里,业务系统需要重新生成缓存,因此就会访问数据库,并将数据更新到 Redis 里,这样后续请求都可以直接命中缓存。

  • 缓存雪崩

    大量缓存数据在同一时间过期(失效)时,如果此时有大量的用户请求,都无法在 Redis 中处理,于是全部请求都直接访问数据库,从而导致数据库的压力骤增,严重的会造成数据库宕机,从而形成一系列连锁反应,造成整个系统崩溃,这就是缓存雪崩的问题。

    • 对于缓存雪崩,可以采用两种方案解决
      • 将缓存失效时间随机打散: 我们可以在原有的失效时间基础上增加一个随机值(比如 1 到 10 分钟)这样每个缓存的过期时间都不重复了,也就降低了缓存集体失效的概率。
      • 设置缓存不过期: 我们可以通过后台服务来更新缓存数据,从而避免因为缓存失效造成的缓存雪崩,也可以在一定程度上避免缓存并发问题。
  • 缓存击穿

    如果缓存中的某个热点数据过期了,此时大量的请求访问了该热点数据,就无法从缓存中读取,直接访问数据库,数据库很容易就被高并发的请求冲垮,这就是缓存击穿的问题。可以认为缓存击穿是缓存雪崩的一个子集。

    • 对于缓存击穿,可以采取两种方案:

      • 互斥锁方案(Redis 中使用 setNX 方法设置一个状态位,表示这是一种锁定状态),保证同一时间只有一个业务线程请求缓存,将所有的访问变成互斥操作,未能获取互斥锁的请求,要么等待锁释放后重新读取缓存,要么就返回空值或者默认值。

      • 不给热点数据设置过期时间,由后台异步更新缓存,或者在热点数据准备要过期前,提前通知后台线程更新缓存以及重新设置过期时间;

  • 缓存穿透

    当用户访问的数据,既不在缓存中,也不在数据库中,导致请求在访问缓存时,发现缓存缺失,再去访问数据库时,发现数据库中也没有要访问的数据,没办法构建缓存数据,来服务后续的请求。那么当有大量这样的请求到来时,数据库的压力骤增,这就是缓存穿透的问题。

    • 缓存穿透的发生一般有这两种情况:业务误操作;黑客恶意攻击。
    • 缓存穿透的方案,常见的方案有三种。
      • 非法请求的限制:当有大量恶意请求访问不存在的数据的时候,也会发生缓存穿透,因此在 API 入口处我们要判断求请求参数是否合理,请求参数是否含有非法值、请求字段是否存在,如果判断出是恶意请求就直接返回错误,避免进一步访问缓存和数据库。
      • 设置空值或者默认值:当我们线上业务发现缓存穿透的现象时,可以针对查询的数据,在缓存中设置一个空值或者默认值,这样后续请求就可以从缓存中读取到空值或者默认值,返回给应用,而不会继续查询数据库。
      • 使用布隆过滤器快速判断数据是否存在,避免通过查询数据库来判断数据是否存在:我们可以在写入数据库数据时,使用布隆过滤器做个标记,然后在用户请求到来时,业务线程确认缓存失效后,可以通过查询布隆过滤器快速判断数据是否存在,如果不存在,就不用通过查询数据库来判断数据是否存在,即使发生了缓存穿透,大量请求只会查询 Redis 和布隆过滤器,而不会查询数据库,保证了数据库能正常运行,Redis 自身也是支持布隆过滤器的。
        • 布隆过滤器可以用于检索一个元素是否在一个集合中。它的优点是空间效率和查询时间都比一般的算法要好的多。

如何设计一个缓存策略,可以动态缓存热点数据呢?

由于数据存储受限,系统并不是将所有数据都需要存放到缓存中的,而只是将其中一部分热点数据缓存起来,所以我们要设计一个热点数据动态缓存的策略。

  • 热点数据动态缓存的策略总体思路:通过数据最新访问时间来做排名,并过滤掉不常访问的数据,只留下经常访问的数据

以电商平台场景中的例子,现在要求只缓存用户经常访问的 Top 1000 的商品。具体细节如下:

  • 先通过缓存系统做一个排序队列(比如存放 1000 个商品),系统会根据商品的访问时间,更新队列信息,越是最近访问的商品排名越靠前;
  • 同时系统会定期过滤掉队列中排名最后的 200 个商品,然后再从数据库中随机读取出 200 个商品加入队列中;
  • 这样当请求每次到达的时候,会先从队列中获取商品 ID,如果命中,就根据 ID 再从另一个缓存数据结构中读取实际的商品信息,并返回。

在 Redis 中可以用 zadd 方法和 zrange 方法来完成排序队列和获取 200 个商品的操作。

说说常见的缓存更新策略?

常见的缓存更新策略共有3种:

  • Cache Aside(旁路缓存)策略:最常用,应用程序直接与「数据库、缓存」交互,并负责对缓存的维护,该策略又可以细分为「读策略」和「写策略」。

    • 写策略:先更新数据库中的数据,再删除缓存中的数据。
      • 不能颠倒,会出现缓存和数据库不一致的问题。
      • 不颠倒:因为缓存的写入通常要远远快于数据库的写入,所以在实际中很难出现 写策略A已经更新了数据库并且删除了缓存,读策略B才更新完缓存的情况。而一旦请求 A 早于请求 B 删除缓存之前更新了缓存,那么接下来的请求就会因为缓存不命中而从数据库中重新读取数据,所以不会出现这种不一致的情况。
    • 读策略:如果读取的数据命中了缓存,则直接返回数据;否则从数据库中读取数据,然后将数据写入到缓存,并且返回给用户。
    • Cache Aside 策略适合读多写少的场景,不适合写多的场景,因为当写入比较频繁时,缓存中的数据会被频繁地清理,这样会对缓存的命中率有一些影响。如果业务对缓存命中率有严格的要求,那么可以考虑两种解决方案:
      • 一种做法是在更新数据时也更新缓存,只是在更新缓存前先加一个分布式锁,同一时间只允许一个线程更新缓存,就不会产生并发问题。当然这么做对于写入的性能会有一些影响;
      • 另一种做法同样也是在更新数据时更新缓存,只是**给缓存加一个较短的过期时间,**这样即使出现缓存不一致的情况,缓存的数据也会很快过期,对业务的影响也是可以接受。
  • Read/Write Through(读穿 / 写穿)策略:该策略原则是应用程序只和缓存交互,不再和数据库交互,而是由缓存和数据库交互,相当于更新数据库的操作由缓存自己代理了。

    1、Read Through 策略

    先查询缓存中数据是否存在,如果存在则直接返回,如果不存在,则由缓存组件负责从数据库查询数据,并将结果写入到缓存组件,最后缓存组件将数据返回给应用。

    2、Write Through 策略

    当有数据更新的时候,先查询要写入的数据在缓存中是否已经存在:

    • 如果缓存中数据已经存在,则更新缓存中的数据,并且由缓存组件同步更新到数据库中,然后缓存组件告知应用程序更新完成。
    • 如果缓存中数据不存在,直接更新数据库,然后返回;

    我们经常使用的分布式缓存组件,无论是 Memcached 还是 Redis 都不提供写入数据库和自动加载数据库中的数据的功能,所以不常用。

  • Write Back(写回)策略:在更新数据的时候,只更新缓存,同时将缓存数据设置为脏的,然后立马返回,并不会更新数据库。对于数据库的更新,会通过批量异步更新的方式进行。缺点是数据不是强一致性的,而且会有数据丢失的风险

    Write Back(写回)策略也不能应用到我们常用的数据库和缓存的场景中,因为 Redis 并没有异步更新数据库的功能。

  • 583. 两个字符串的删除操作 - 力扣(LeetCode)

    1. dp[i][j]:以i - 1结尾的word1和以j - 1结尾的word2,删除字符后使两个单词相等的最小删除步数为dp[i][j]

    2. word1[i - 1] = word2[j - 1]:不需要删除:dp[i][j] = dp[i - 1][j - 1]

      word1[i - 1] != word2[j - 1]:需要删除:删除word1或删除word2dp[i][j] = Math.min(dp[i - 1][j] + 1, dp[i][j - 1] + 1)

    3. dp[i][0]:word2为空字符串,以i-1为结尾的字符串word1要删除多少个元素,才能和word2相同呢,很明显dp[i][0] = idp[0][j]的话同理。

    4. 遍历顺序从前往后,从上往下遍历。

    5. 举例推导dp

      class Solution {
          public int minDistance(String word1, String word2) {
              int m = word1.length();
              int n = word2.length();
              int [] [] dp = new int [m + 1][n + 1];
              for(int i = 0 ; i <= m ; i ++){
                  dp[i][0] = i;
              }
              for(int j = 0 ; j <= n ; j ++){
                  dp[0][j] = j;
              }
              for(int i = 1 ; i <= m ; i ++){
                  char w1 = word1.charAt(i - 1);
                  for(int j = 1 ; j <= n ; j ++){
                      char w2 = word2.charAt(j - 1);
                      if(w1 == w2) dp[i][j] = dp[i - 1][j - 1];
                      else dp[i][j] = Math.min(dp[i - 1][j] + 1 , dp[i][j - 1] + 1);
                      
                      //System.out.println("以word1[" + (i - 1) + "]和word[" + (j - 1) + "]结尾的单词,最少需要" + dp[i][j] + "步删除才能使word1与word2相等");
                  }
              }
              return dp[m][n];
          }
      }
      
  • 72. 编辑距离 - 力扣(LeetCode)

    1. dp[i][j]:以i - 1结尾的word1和以j - 1结尾的word2,转换所需的最小操作数为dp[i][j]

    2. word1[i] == word2[j] :不需要进行操作,dp[i][j] = dp[i - 1][j - 1]

      word1[i - 1] != word2[j - 1]:需要进行操作:

      删除(添加):word2删除一个元素,相当于word1添加一个元素。

      word1删除一个元素:dp[i][j] = dp[i - 1][j] + 1

      word2删除一个元素(word1添加元素):dp[i][j] = dp[i][j - 1] + 1

      替换:可以回顾一下,if (word1[i - 1] == word2[j - 1])的时候我们的操作 是 dp[i][j] = dp[i - 1][j - 1] 对吧。

      那么只需要一次替换的操作,就可以让 word1[i - 1] 和 word2[j - 1] 相同。所以 dp[i][j] = dp[i - 1][j - 1] + 1;

    3. dp数组初始化:dp[i][0] :以下标i-1为结尾的字符串word1,和空字符串word2,最近编辑距离为dp[i][0]

      那么dp[i][0]就应该是i,对word1里的元素全部做删除操作,即:dp[i][0] = i;

      同理dp[0][j] = j;

    4. 从上往下,从左往右遍历。

    5. 举例推导dp数组

      class Solution {
          public int minDistance(String word1, String word2) {
              int m = word1.length();
              int n = word2.length();
              int [] [] dp = new int [m + 1][n + 1];
              for(int i = 0 ; i <= m ; i ++){
                  dp[i][0] = i;
              }
              for(int j = 0 ; j <= n ; j ++){
                  dp[0][j] = j;
              }
              for(int i = 1 ; i <= m ; i ++){
                  char w1 = word1.charAt(i - 1);
                  for(int j = 1 ; j <= n ; j ++){
                      char w2 = word2.charAt(j - 1);
                      if(w1 == w2) dp[i][j] = dp[i - 1][j - 1];
                      else dp[i][j] = Math.min(dp[i - 1][j - 1] + 1 , Math.min(dp[i - 1][j] + 1 , dp[i][j - 1] + 1));
                      
                      //System.out.println("以word1[" + (i - 1) + "]和word2[" + (j - 1) + "]结尾的单词,word1最少需要" + dp[i][j] + "步操作才能使word1与word2相等");
                  }
              }
              return dp[m][n];
          }
      }
      

回文串问题

题目关键点
647. 回文子串 - 力扣(LeetCode)dp数组定义
516. 最长回文子序列 - 力扣(LeetCode)dp数组定义
  • 647. 回文子串 - 力扣(LeetCode)

    判断一个子字符串(字符串的下表范围[i,j])是否回文,依赖于,子字符串(下表范围[i + 1, j - 1])) 是否是回文。

    1. 所以定义一个boolean类型的dp[i][j],表示区间范围[i , j ]的子串是否回文。

    2. s[i] == s[j]时分三种情况:

      下标i == j(a): dp[i][j] = true

      下标i与j相差为1(aa):dp[i][j] = true

      下标i与j相差大于1(cabac):此时就看dp[i + 1][j - 1]是不是回文即可。

    3. dp[i][j]初始化为false。正好也解决了当s[i] != s[j]时的问题。

    4. 遍历顺序:

      dp[i + 1][j - 1] dp[i][j]的左下角,如图:

      所以应该从下往上,从左到右遍历

    5. 举例推导dp数组

      class Solution {
          public int countSubstrings(String s) {
              //记录回文串数量。
              int ans = 0;
              int m = s.length();
              boolean dp [][] = new boolean [m][m];
              for(int i = m - 1; i >=0 ; i --){
                  char si = s.charAt(i);
                  for(int j = i ; j < m ; j ++){
                      char sj = s.charAt(j);
                      if(si ==sj){
                          if( j - i <= 1){
                              ans ++;
                              dp[i][j] = true;
                          }else if(dp[i + 1][j - 1]){
                              ans ++;
                              dp[i][j] = true;
                          }
                      }
                     // System.out.println("以[" +  i  + ","  + j   + "]为区间的字符串" + dp[i][j]+"回文串");
                  }
              }
              return ans;
          }
      }
      
  • 516. 最长回文子序列 - 力扣(LeetCode)

    1. dp数组初始化:dp[i][j]:字符串s在[i, j]范围内最长的回文子序列的长度为dp[i][j]

    2. 递推公式:如果s[i] == s[j] ,那么dp[i][j] = dp[i + 1][j - 1] + 2

      如果s[i] != s[j] ,那么就是根据加上s[i]或者加上s[j]两种情况的不同长度来判断,dp[i][j] = Math.max(dp[i + 1][j] , dp[i][j - 1])

    3. 初始化:dp[i][j] = dp[i + 1][j - 1] + 2计算不到相同的情况,所以当i == j时,初始化为1。

    4. 遍历顺序与上一题一致

    5. 举例推导dp数组

      class Solution {
          public int longestPalindromeSubseq(String s) {
              int m = s.length();
              int [][] dp = new int [m][m];
              for(int i = 0 ; i < m ; i ++){
                  dp[i][i] = 1;
              }
              for(int i = m - 1 ; i >= 0 ; i --){
                  char si = s.charAt(i);
                  for(int j = i + 1; j < m ; j ++){
                      char sj = s.charAt(j);
                      if(si == sj){
                          dp[i][j] = dp[i + 1][j - 1] + 2;
                      }else {
                          dp[i][j] = Math.max(dp[i + 1][j] , dp[i][j - 1]);
                      }
                  }
              }
              return dp[0][m - 1];
          }
      }
      

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/380245.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

常用的“小脚本“-json数据处理

小背景&#xff1a; 我们公司项目中的小脚本是一些工具类&#xff0c;比如常用的是MapUtil工具类的一些方法 写公司的MapUtil工具类的方法要注意&#xff0c;方法名的命名&#xff0c;因为方法名&#xff0c;在公司的项目的某个业务流程有对方法名的进行String截取开头字符串然…

考研机试 | C++ | 王道复试班 | map专场

目录关于map查找学生信息&#xff08;清华上机&#xff09;题目代码&#xff1a;魔咒词典&#xff08;浙大机试&#xff09;题目&#xff1a;代码英语复试常用话题关于map map的一些基本操作&#xff1a; 判空 &#xff1a;map.empty()键值对的个数&#xff1a; map.size()插入…

进程、线程、协程详解

目录 前言&#xff1a; 一、进程 进程的概念 进程内存空间 二、线程 线程的定义 内核线程 用户线程 内核线程和用户线程的比较 线程的状态 三、协程 协程的定义 协程序相对于线程优势 运用场景 四、线程、协程、进程切换比较 前言&#xff1a; 有时候无法…

JPA之实体之间的关系

JPA之实体之间的关系 10.1.1实体类创建 注解的应用 Table&#xff0c;Entity IdGeneratedValue指定主键&#xff0c;Column P174 实体类编写规范 Table(name "t_user") Entity(name "User") public class User implements Serializable {IdGeneratedVa…

王道操作系统课代表 - 考研计算机 第二章 进程与线程 究极精华总结笔记

本篇博客是考研期间学习王道课程 传送门 的笔记&#xff0c;以及一整年里对 操作系统 知识点的理解的总结。希望对新一届的计算机考研人提供帮助&#xff01;&#xff01;&#xff01; 关于对 “进程与线程” 章节知识点总结的十分全面&#xff0c;涵括了《操作系统》课程里的全…

机器学习——线性学习

提及线性学习&#xff0c;我们首先会想到线性回归。回归跟分类的区别在于要预测的目标函数是连续值线性回归假定输入空间到输出空间的函数映射成线性关系&#xff0c;但现实应用中&#xff0c;很多问题都是非线性的。为拓展其应用场景&#xff0c;我们可以将线性回归的预测值做…

SQL的优化【面试工作】

SQL的优化 最近看到群友在讨论这块的优化,感觉不管工作和面试,都是用上的,记录下吧!(不然又记不住) 优化点: 处理和优化复杂的 SQL 查询可以有以下几个方向&#xff1a; 1.优化查询语句本身 首先&#xff0c;可以优化 SQL 查询语句本身&#xff0c;尽量让其更加简洁、高效。 …

Go程序当父进程被kill,子进程也自动退出的问题记录

平常我们启动一个后台进程&#xff0c;会通过nouhp &的方式启动&#xff0c;这样可以在退出终端会话的时候&#xff0c;进程仍然可以继续在后台执行(进程的父进程id会从原来的bash进程变成1) 在go程序中&#xff0c;通过nouhp &的方式启动子进程&#xff0c;预期是即使…

干货| Vue小程序开发技术原理

目前应用最广的三大前端框架分别是Vue、 React 和 Angular 。其中&#xff0c;不管是 BAT 大厂&#xff0c;还是创业公司&#xff0c;Vue 都有广泛的应用。如今&#xff0c;再随着移动开发小程序的蓬勃发展&#xff0c;Vue也广泛应用到了小程序开发当中。今天&#xff0c;就来详…

嵌入式 STM32 SHT31温湿度传感器

目录 简介 1、原理图 2、时序说明 数据传输 起始信号 结束信号 3、SHT31读写数据 SHT31指令集 读数据 温湿度转换 4、温湿度转换应用 sht3x初始化 读取温湿度 简介 什么是SHT31&#xff1f; 一主机多从机--通过寻址的方式--每个从机都有唯一的地址&…

linux--多线程(一)

文章目录Linux线程的概念线程的优点线程的缺点线程异常线程的控制创建线程线程ID以及进程地址空间终止线程线程等待线程分离线程互斥进程线程间的互斥相关概念互斥量mutex有线程安全问题的售票系统查看ticket--部分的汇编代码互斥量的接口互斥量实现原理探究可重入和线程安全常…

三重积分为何不能直接带入积分区域?搞懂这些,重积分基本可以了

积分的积分区域及被积表达式 重点&#xff1a;积分的结果均为数值&#xff0c;仅与被积表达式和积分区间有关&#xff01;&#xff01;&#xff01; 1.如何一下子区分一重积分&#xff0c;二重积分&#xff0c;三重积分&#xff1f; 看积分区间和被积表达式&#xff1a; 一重…

React教程详解一(props、state、refs、生命周期)

文章略长&#xff0c;耐心读完&#xff0c;受益匪浅哦~ 目录 前言 简介 JSX 面向组件编程 state props refs 组件生命周期 前言 简介 React框架由Facebook开发&#xff0c;和Vue框架一样&#xff0c;都是用于构建用户界面的JavaScript库&#xff1b; 它有如下三个特…

PHP - ChatGpt 学习 仅供参考

由于最近ChatGpt 大火&#xff0c;但是门槛来说是对于大家最头疼的环节&#xff0c; 由此ChatGpt 有一个API 可以仅供大伙对接 让我来说下资质&#xff1a; 1&#xff1a;首先要搞得到一个 ChatGpt 的账户&#xff0c; 会获得一个KEY&#xff0c;该key为访问API核心&#xff0…

jenkins漏洞集合

目录 CVE-2015-8103 反序列化远程代码执行 CVE-2016-0788 Jenkins CI和LTS 远程代码执行漏洞 CVE-2016-0792 低权限用户命令执行 CVE-2016-9299 代码执行 CVE-2017-1000353 Jenkins-CI 远程代码执行 CVE-2018-1000110 用户枚举 CVE-2018-1000861 远程命令执行 CVE-2018…

ChatGPT文章自动发布WordPress

WordPress可以用ChatGPT发文章吗&#xff1f;答案是肯定的&#xff0c;ChatGPT官方有提供api接口&#xff0c;多以目前有很多的SEO工具具有自动文章生成自动发布的功能&#xff0c;使用SEO工具&#xff0c;我们可以通过疑问词和关键词进行文章生成&#xff0c;并定时发布到我们…

软测入门(三)Selenium(Web自动化测试基础)

Selenium&#xff08;Web端自动测试&#xff09; Selenium是一个用于Web应用程序测试的工具&#xff1a;中文是硒 开源跨平台&#xff1a;linux、windows、mac核心&#xff1a;可以在多个浏览器上进行自动化测试多语言 Selenium WebDriver控制原理 Selenium Client Library…

Linux基础——连接Xshell7

个人简介&#xff1a;云计算网络运维专业人员&#xff0c;了解运维知识&#xff0c;掌握TCP/IP协议&#xff0c;每天分享网络运维知识与技能。座右铭&#xff1a;海不辞水&#xff0c;故能成其大&#xff1b;山不辞石&#xff0c;故能成其高。个人主页&#xff1a;小李会科技的…

复位理论基础

先收集资料&#xff0c;了解当前常用的基础理论和实现方式 复位 初始化微控制器内部电路 将所有寄存器恢复成默认值确认MCU的工作模式禁止全局中断关闭外设将IO设置为高阻输入状态等待时钟趋于稳定从固定地址取得复位向量并开始执行 造成复位的原因 有多种引起复位的因素&…

客户关系管理挑战:如何保持客户满意度并提高业绩?

当今&#xff0c;各行业市场竞争愈发激烈&#xff0c;对于保持客户满意度并提高业绩是每个企业都面临的挑战。而客户关系管理则是实现这一目标的关键&#xff0c;因为它涉及到与客户的互动和沟通&#xff0c;以及企业提供优质的产品和服务。在本文中&#xff0c;我们将探讨客户…