0%

一、索引

B+ Tree 原理

1. 数据结构

B Tree 指的是 Balance Tree,也就是平衡树。平衡树是一颗查找树,并且所有叶子节点位于同一层。

B+ Tree 是基于 B Tree 和叶子节点顺序访问指针进行实现,它具有 B Tree 的平衡性,并且通过顺序访问指针来提高区间查询的性能。

在 B+ Tree 中,一个节点中的 key 从左到右非递减排列,如果某个指针的左右相邻 key 分别是 keyi 和 keyi+1,且不为 null,则该指针指向节点的所有 key 大于等于 keyi 且小于等于 keyi+1


2. 操作

进行查找操作时,首先在根节点进行二分查找,找到一个 key 所在的指针,然后递归地在指针所指向的节点进行查找。直到查找到叶子节点,然后在叶子节点上进行二分查找,找出 key 所对应的 data。

插入删除操作会破坏平衡树的平衡性,因此在进行插入删除操作之后,需要对树进行分裂、合并、旋转等操作来维护平衡性。

3. 与红黑树的比较

红黑树等平衡树也可以用来实现索引,但是文件系统及数据库系统普遍采用 B+ Tree 作为索引结构,这是因为使用 B+ 树访问磁盘数据有更高的性能。

(一)B+ 树有更低的树高

平衡树的树高 O(h)=O(logdN),其中 d 为每个节点的出度。红黑树的出度为 2,而 B+ Tree 的出度一般都非常大,所以红黑树的树高 h 很明显比 B+ Tree 大非常多。

(二)磁盘访问原理

操作系统一般将内存和磁盘分割成固定大小的块,每一块称为一页,内存与磁盘以页为单位交换数据。数据库系统将索引的一个节点的大小设置为页的大小,使得一次 I/O 就能完全载入一个节点。

如果数据不在同一个磁盘块上,那么通常需要移动制动手臂进行寻道,而制动手臂因为其物理结构导致了移动效率低下,从而增加磁盘数据读取时间。B+ 树相对于红黑树有更低的树高,进行寻道的次数与树高成正比,在同一个磁盘块上进行访问只需要很短的磁盘旋转时间,所以 B+ 树更适合磁盘数据的读取。

(三)磁盘预读特性

为了减少磁盘 I/O 操作,磁盘往往不是严格按需读取,而是每次都会预读。预读过程中,磁盘进行顺序读取,顺序读取不需要进行磁盘寻道,并且只需要很短的磁盘旋转时间,速度会非常快。并且可以利用预读特性,相邻的节点也能够被预先载入。

MySQL 索引

索引是在存储引擎层实现的,而不是在服务器层实现的,所以不同存储引擎具有不同的索引类型和实现。

1. B+Tree 索引

是大多数 MySQL 存储引擎的默认索引类型。

因为不再需要进行全表扫描,只需要对树进行搜索即可,所以查找速度快很多。

因为 B+ Tree 的有序性,所以除了用于查找,还可以用于排序和分组。

可以指定多个列作为索引列,多个索引列共同组成键。

适用于全键值、键值范围和键前缀查找,其中键前缀查找只适用于最左前缀查找。如果不是按照索引列的顺序进行查找,则无法使用索引。

InnoDB 的 B+Tree 索引分为主索引和辅助索引。主索引的叶子节点 data 域记录着完整的数据记录,这种索引方式被称为聚簇索引。因为无法把数据行存放在两个不同的地方,所以一个表只能有一个聚簇索引。


辅助索引的叶子节点的 data 域记录着主键的值,因此在使用辅助索引进行查找时,需要先查找到主键值,然后再到主索引中进行查找。


2. 哈希索引

哈希索引能以 O(1) 时间进行查找,但是失去了有序性:

  • 无法用于排序与分组;
  • 只支持精确查找,无法用于部分查找和范围查找。

InnoDB 存储引擎有一个特殊的功能叫“自适应哈希索引”,当某个索引值被使用的非常频繁时,会在 B+Tree 索引之上再创建一个哈希索引,这样就让 B+Tree 索引具有哈希索引的一些优点,比如快速的哈希查找。

3. 全文索引

MyISAM 存储引擎支持全文索引,用于查找文本中的关键词,而不是直接比较是否相等。

查找条件使用 MATCH AGAINST,而不是普通的 WHERE。

全文索引使用倒排索引实现,它记录着关键词到其所在文档的映射。

InnoDB 存储引擎在 MySQL 5.6.4 版本中也开始支持全文索引。

4. 空间数据索引

MyISAM 存储引擎支持空间数据索引(R-Tree),可以用于地理数据存储。空间数据索引会从所有维度来索引数据,可以有效地使用任意维度来进行组合查询。

必须使用 GIS 相关的函数来维护数据。

索引优化

1. 独立的列

在进行查询时,索引列不能是表达式的一部分,也不能是函数的参数,否则无法使用索引。

例如下面的查询不能使用 actor_id 列的索引:

1
SELECT actor_id FROM sakila.actor WHERE actor_id + 1 = 5;

2. 多列索引

在需要使用多个列作为条件进行查询时,使用多列索引比使用多个单列索引性能更好。例如下面的语句中,最好把 actor_id 和 film_id 设置为多列索引。

1
2
SELECT film_id, actor_ id FROM sakila.film_actor
WHERE actor_id = 1 AND film_id = 1;

3. 索引列的顺序

让选择性最强的索引列放在前面。

索引的选择性是指:不重复的索引值和记录总数的比值。最大值为 1,此时每个记录都有唯一的索引与其对应。选择性越高,每个记录的区分度越高,查询效率也越高。

例如下面显示的结果中 customer_id 的选择性比 staff_id 更高,因此最好把 customer_id 列放在多列索引的前面。

1
2
3
4
SELECT COUNT(DISTINCT staff_id)/COUNT(*) AS staff_id_selectivity,
COUNT(DISTINCT customer_id)/COUNT(*) AS customer_id_selectivity,
COUNT(*)
FROM payment;
1
2
3
   staff_id_selectivity: 0.0001
customer_id_selectivity: 0.0373
COUNT(*): 16049

4. 前缀索引

对于 BLOB、TEXT 和 VARCHAR 类型的列,必须使用前缀索引,只索引开始的部分字符。

前缀长度的选取需要根据索引选择性来确定。

5. 覆盖索引

索引包含所有需要查询的字段的值。

具有以下优点:

  • 索引通常远小于数据行的大小,只读取索引能大大减少数据访问量。
  • 一些存储引擎(例如 MyISAM)在内存中只缓存索引,而数据依赖于操作系统来缓存。因此,只访问索引可以不使用系统调用(通常比较费时)。
  • 对于 InnoDB 引擎,若辅助索引能够覆盖查询,则无需访问主索引。

索引的优点

  • 大大减少了服务器需要扫描的数据行数。

  • 帮助服务器避免进行排序和分组,以及避免创建临时表(B+Tree 索引是有序的,可以用于 ORDER BY 和 GROUP BY 操作。临时表主要是在排序和分组过程中创建,不需要排序和分组,也就不需要创建临时表)。

  • 将随机 I/O 变为顺序 I/O(B+Tree 索引是有序的,会将相邻的数据都存储在一起)。

索引的使用条件

  • 对于非常小的表、大部分情况下简单的全表扫描比建立索引更高效;

  • 对于中到大型的表,索引就非常有效;

  • 但是对于特大型的表,建立和维护索引的代价将会随之增长。这种情况下,需要用到一种技术可以直接区分出需要查询的一组数据,而不是一条记录一条记录地匹配,例如可以使用分区技术。

二、查询性能优化

使用 Explain 进行分析

Explain 用来分析 SELECT 查询语句,开发人员可以通过分析 Explain 结果来优化查询语句。

比较重要的字段有:

  • select_type : 查询类型,有简单查询、联合查询、子查询等
  • key : 使用的索引
  • rows : 扫描的行数

优化数据访问

1. 减少请求的数据量

  • 只返回必要的列:最好不要使用 SELECT * 语句。
  • 只返回必要的行:使用 LIMIT 语句来限制返回的数据。
  • 缓存重复查询的数据:使用缓存可以避免在数据库中进行查询,特别在要查询的数据经常被重复查询时,缓存带来的查询性能提升将会是非常明显的。

2. 减少服务器端扫描的行数

最有效的方式是使用索引来覆盖查询。

重构查询方式

1. 切分大查询

一个大查询如果一次性执行的话,可能一次锁住很多数据、占满整个事务日志、耗尽系统资源、阻塞很多小的但重要的查询。

1
DELETE FROM messages WHERE create < DATE_SUB(NOW(), INTERVAL 3 MONTH);
1
2
3
4
5
rows_affected = 0
do {
rows_affected = do_query(
"DELETE FROM messages WHERE create < DATE_SUB(NOW(), INTERVAL 3 MONTH) LIMIT 10000")
} while rows_affected > 0

2. 分解大连接查询

将一个大连接查询分解成对每一个表进行一次单表查询,然后在应用程序中进行关联,这样做的好处有:

  • 让缓存更高效。对于连接查询,如果其中一个表发生变化,那么整个查询缓存就无法使用。而分解后的多个查询,即使其中一个表发生变化,对其它表的查询缓存依然可以使用。
  • 分解成多个单表查询,这些单表查询的缓存结果更可能被其它查询使用到,从而减少冗余记录的查询。
  • 减少锁竞争;
  • 在应用层进行连接,可以更容易对数据库进行拆分,从而更容易做到高性能和可伸缩。
  • 查询本身效率也可能会有所提升。例如下面的例子中,使用 IN() 代替连接查询,可以让 MySQL 按照 ID 顺序进行查询,这可能比随机的连接要更高效。
1
2
3
4
SELECT * FROM tag
JOIN tag_post ON tag_post.tag_id=tag.id
JOIN post ON tag_post.post_id=post.id
WHERE tag.tag='mysql';
1
2
3
SELECT * FROM tag WHERE tag='mysql';
SELECT * FROM tag_post WHERE tag_id=1234;
SELECT * FROM post WHERE post.id IN (123,456,567,9098,8904);

三、存储引擎

InnoDB

是 MySQL 默认的事务型存储引擎,只有在需要它不支持的特性时,才考虑使用其它存储引擎。

实现了四个标准的隔离级别,默认级别是可重复读(REPEATABLE READ)。在可重复读隔离级别下,通过多版本并发控制(MVCC)+ Next-Key Locking 防止幻影读。

主索引是聚簇索引,在索引中保存了数据,从而避免直接读取磁盘,因此对查询性能有很大的提升。

内部做了很多优化,包括从磁盘读取数据时采用的可预测性读、能够加快读操作并且自动创建的自适应哈希索引、能够加速插入操作的插入缓冲区等。

支持真正的在线热备份。其它存储引擎不支持在线热备份,要获取一致性视图需要停止对所有表的写入,而在读写混合场景中,停止写入可能也意味着停止读取。

MyISAM

设计简单,数据以紧密格式存储。对于只读数据,或者表比较小、可以容忍修复操作,则依然可以使用它。

提供了大量的特性,包括压缩表、空间数据索引等。

不支持事务。

不支持行级锁,只能对整张表加锁,读取时会对需要读到的所有表加共享锁,写入时则对表加排它锁。但在表有读取操作的同时,也可以往表中插入新的记录,这被称为并发插入(CONCURRENT INSERT)。

可以手工或者自动执行检查和修复操作,但是和事务恢复以及崩溃恢复不同,可能导致一些数据丢失,而且修复操作是非常慢的。

如果指定了 DELAY_KEY_WRITE 选项,在每次修改执行完成时,不会立即将修改的索引数据写入磁盘,而是会写到内存中的键缓冲区,只有在清理键缓冲区或者关闭表的时候才会将对应的索引块写入磁盘。这种方式可以极大的提升写入性能,但是在数据库或者主机崩溃时会造成索引损坏,需要执行修复操作。

比较

  • 事务:InnoDB 是事务型的,可以使用 Commit 和 Rollback 语句。

  • 并发:MyISAM 只支持表级锁,而 InnoDB 还支持行级锁。

  • 外键:InnoDB 支持外键。

  • 备份:InnoDB 支持在线热备份。

  • 崩溃恢复:MyISAM 崩溃后发生损坏的概率比 InnoDB 高很多,而且恢复的速度也更慢。

  • 其它特性:MyISAM 支持压缩表和空间数据索引。

四、数据类型

整型

TINYINT, SMALLINT, MEDIUMINT, INT, BIGINT 分别使用 8, 16, 24, 32, 64 位存储空间,一般情况下越小的列越好。

INT(11) 中的数字只是规定了交互工具显示字符的个数,对于存储和计算来说是没有意义的。

浮点数

FLOAT 和 DOUBLE 为浮点类型,DECIMAL 为高精度小数类型。CPU 原生支持浮点运算,但是不支持 DECIMAl 类型的计算,因此 DECIMAL 的计算比浮点类型需要更高的代价。

FLOAT、DOUBLE 和 DECIMAL 都可以指定列宽,例如 DECIMAL(18, 9) 表示总共 18 位,取 9 位存储小数部分,剩下 9 位存储整数部分。

字符串

主要有 CHAR 和 VARCHAR 两种类型,一种是定长的,一种是变长的。

VARCHAR 这种变长类型能够节省空间,因为只需要存储必要的内容。但是在执行 UPDATE 时可能会使行变得比原来长,当超出一个页所能容纳的大小时,就要执行额外的操作。MyISAM 会将行拆成不同的片段存储,而 InnoDB 则需要分裂页来使行放进页内。

在进行存储和检索时,会保留 VARCHAR 末尾的空格,而会删除 CHAR 末尾的空格。

时间和日期

MySQL 提供了两种相似的日期时间类型:DATETIME 和 TIMESTAMP。

1. DATETIME

能够保存从 1000 年到 9999 年的日期和时间,精度为秒,使用 8 字节的存储空间。

它与时区无关。

默认情况下,MySQL 以一种可排序的、无歧义的格式显示 DATETIME 值,例如“2008-01-16 22:37:08”,这是 ANSI 标准定义的日期和时间表示方法。

2. TIMESTAMP

和 UNIX 时间戳相同,保存从 1970 年 1 月 1 日午夜(格林威治时间)以来的秒数,使用 4 个字节,只能表示从 1970 年到 2038 年。

它和时区有关,也就是说一个时间戳在不同的时区所代表的具体时间是不同的。

MySQL 提供了 FROM_UNIXTIME() 函数把 UNIX 时间戳转换为日期,并提供了 UNIX_TIMESTAMP() 函数把日期转换为 UNIX 时间戳。

默认情况下,如果插入时没有指定 TIMESTAMP 列的值,会将这个值设置为当前时间。

应该尽量使用 TIMESTAMP,因为它比 DATETIME 空间效率更高。

五、切分

水平切分

水平切分又称为 Sharding,它是将同一个表中的记录拆分到多个结构相同的表中。

当一个表的数据不断增多时,Sharding 是必然的选择,它可以将数据分布到集群的不同节点上,从而缓存单个数据库的压力。


垂直切分

垂直切分是将一张表按列切分成多个表,通常是按照列的关系密集程度进行切分,也可以利用垂直切分将经常被使用的列和不经常被使用的列切分到不同的表中。

在数据库的层面使用垂直切分将按数据库中表的密集程度部署到不同的库中,例如将原来的电商数据库垂直切分成商品数据库、用户数据库等。


Sharding 策略

  • 哈希取模:hash(key) % N;
  • 范围:可以是 ID 范围也可以是时间范围;
  • 映射表:使用单独的一个数据库来存储映射关系。

Sharding 存在的问题

1. 事务问题

使用分布式事务来解决,比如 XA 接口。

2. 连接

可以将原来的连接分解成多个单表查询,然后在用户程序中进行连接。

3. ID 唯一性

  • 使用全局唯一 ID(GUID)
  • 为每个分片指定一个 ID 范围
  • 分布式 ID 生成器 (如 Twitter 的 Snowflake 算法)

六、复制

主从复制

主要涉及三个线程:binlog 线程、I/O 线程和 SQL 线程。

  • binlog 线程 :负责将主服务器上的数据更改写入二进制日志(Binary log)中。
  • I/O 线程 :负责从主服务器上读取二进制日志,并写入从服务器的中继日志(Relay log)。
  • SQL 线程 :负责读取中继日志,解析出主服务器已经执行的数据更改并在从服务器中重放(Replay)。

读写分离

主服务器处理写操作以及实时性要求比较高的读操作,而从服务器处理读操作。

读写分离能提高性能的原因在于:

  • 主从服务器负责各自的读和写,极大程度缓解了锁的争用;
  • 从服务器可以使用 MyISAM,提升查询性能以及节约系统开销;
  • 增加冗余,提高可用性。

读写分离常用代理方式来实现,代理服务器接收应用层传来的读写请求,然后决定转发到哪个服务器。


参考资料

一、事务

概念

事务指的是满足 ACID 特性的一组操作,可以通过 Commit 提交一个事务,也可以使用 Rollback 进行回滚。


ACID

1. 原子性(Atomicity)

事务被视为不可分割的最小单元,事务的所有操作要么全部提交成功,要么全部失败回滚。

回滚可以用回滚日志(Undo Log)来实现,回滚日志记录着事务所执行的修改操作,在回滚时反向执行这些修改操作即可。

2. 一致性(Consistency)

数据库在事务执行前后都保持一致性状态。在一致性状态下,所有事务对同一个数据的读取结果都是相同的。

3. 隔离性(Isolation)

一个事务所做的修改在最终提交以前,对其它事务是不可见的。

4. 持久性(Durability)

一旦事务提交,则其所做的修改将会永远保存到数据库中。即使系统发生崩溃,事务执行的结果也不能丢失。

系统发生奔溃可以用重做日志(Redo Log)进行恢复,从而实现持久性。与回滚日志记录数据的逻辑修改不同,重做日志记录的是数据页的物理修改。

Read more »

读写分离

基本架构

读写分离基于数据的主从复制的架构,数据库写的操作都在主库执行,读取的操作都在从库执行

架构图如下:

img

从该系统架构中,可以看出: 数据库从之前的单节点变为多节点提供服务 主节点数据,同步到从节点数据 应用程序需要连接到2个数据库节点,并且在程序内部实现判断读写操作

这种架构存在2个问题: 应用程序需要连接到多个节点,对应用程序而言开发变得复杂 这个问题,可以通过中间件解决

主从之间的同步,是异步完成,也就意味着这是 弱一致性

可能会导致,数据写入主库后,应用程序读取从库获取不到数据,或者可能会丢失数据,对于数据安全性 要求比较高的应用是不合适的 该问题可以通过PXC集群解决

考虑负载均衡的架构

在应用程序和中间件之间增加proxy代理,由代理来完成负载均衡的功 能,应用程序只需要对接到proxy即可。

img

PXC集群架构

在前面的架构中,都是基于MySQL主从的架构,那么在主从架构中,弱一致性问题依然没有解决,如果在需要强一 致性的需求中,显然这种架构是不能应对的,比如:交易数据。 PXC提供了读写强一致性的功能,可以保证数据在任何一个节点写入的同时可以同步到其它节点,也就意味着可以存 其它的任何节点进行读取操作,无延迟。 架构如下:

img

混合架构

在前面的PXC架构中,虽然可以实现了事务的强一致性,但是它是通过牺牲了性能换来的一致性,如果在某些业务场 景下,如果没有强一致性的需求,那么使用PXC就不合适了。所以,在我们的系统架构中,需要将这两种方式综合起 来,这样才是一个较为完善的架构

img

主从复制架构

img

mysql主(称master)从(称slave)复制的原理:

1、master将数据改变记录到二进制日志(binary log)中,也即是配置文件log-bin指定的文件(这些记录叫做二进制日 志事件,binary log events)

2、slave将master的binary log events拷贝到它的中继日志(relay log)

3、slave重做中继日志中的事件,将改变反映它自己的数据(数据重演)

主从复制架构存在的问题

从服务器未及时同步、主机宕机 – 主从同步之间异步未完成,数据丢失等问题,属于 弱一致性 – 解决方案:PXC集群,2PC(半同步的方式)

参考文章

https://github.com/xinlelee/E-book/blob/master/MySQL%E9%9B%86%E7%BE%A4%E8%A7%A3%E5%86%B3%E6%96%B9%E6%A1%88%EF%BC%88%E4%B8%BB%E4%BB%8E%E5%A4%8D%E5%88%B6%E3%80%81PXC%E9%9B%86%E7%BE%A4%E3%80%81MyCat%E3%80%81HAProxy.pdf

分库分表的基本原理

分库分表的两种方式:垂直切分 和 水平切分

垂直切分: 垂直分表 和 垂直分库
水平切分: 水平分表 和 水平分库

垂直分表: 操作数据库中的某张表,把这张表一部分的字段存到一张新表中,再把这张表的另一部分字段存到另外一张表中
垂直分库: 数据库按照业务划分,不同的业务使用不同的数据库

水平分表: 根据key将数据存放到不同的表(一张表类型 对应了 多个表(表A 表B…))
水平分库: 根据key将数据存放到不同的数据库(一张表类型 对应了 多个数据库)

Read more »

ClassLoader类加载机制

类加载器的命名空间

类加载器的命名空间-是由类加载器本身及其所有父加载器所加载出来的binary name(full class name 组成)
1、在同一个命名空间里,不允许出现两个完全一样的binary name
2、在不同的命名空间中,可以出现两个相同的binary name,但是两者对应的class 对象是相互不能感知到的,也就是class对象的类型是不一样的
3、子加载器的命名空间中的binary name对应的类中可以访问 父加载器命名空间中的binary name对应的类,反之则不行

Read more »

51. N 皇后

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
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
class Solution {
/**
* 优化方法 -- check 同一列、主对角线、副对角线的状态检查
* 主对角线 -- 横坐标 - 纵坐标 (在同一主对角线上的点,两坐标相减总相等 ,为了保证数组不越界,加上偏移量)
* 副对角线 -- 横坐标 + 纵坐标 (在同一副对角线上的点,两坐标相加总相等)
*/

/**
* 回溯算法(按行存放 皇后 ,backTracing(help, 0);)
* backTracing(String[][] help, int index) 当index大于等于n时,结束递归,加入结果集
* <p>
* 当准备向 第 index 行 选择第 j 列,进行存放 Q 时,先进行竖向检查、左上检查、以及右上检查,都符合条件时,再修改数组,进行递归,
* 最后 修改回 原先临时数组状态
*
* @param n
* @return
*/

static List<List<String>> result;

static boolean[] shuXiangHelp;//j为坐标 size = length i - j 的取值范围为:[-(length-1) , length -1]
static boolean[] zhuDuiJiaoHelp;// i -j + length -1为坐标 size = 2*length-1;
static boolean[] fuDuiJiaoHelp;// i -j + length -1为坐标 size = 2*length-1;


public static List<List<String>> solveNQueens(int n) {
char[][] help = new char[n][n];
shuXiangHelp = new boolean[n];
zhuDuiJiaoHelp = new boolean[2 * n - 1];
fuDuiJiaoHelp = new boolean[2 * n - 1];
result = new ArrayList<>();
for (int i = 0; i < n; i++) {
Arrays.fill(help[i], '.');
}
backTracing(help, 0);
return result;
}

private static void backTracing(char[][] help, int index) {
//backTracing(String[][] help, int index) 当index大于等于n时,结束递归,加入结果集
if (index >= help.length) {
ArrayList<String> list = new ArrayList<>();
for (int i = 0; i < help.length; i++) {
StringBuilder builder = new StringBuilder();
for (int j = 0; j < help[i].length; j++) {
builder.append(help[i][j]);
}
list.add(builder.toString());
}
result.add(list);
return;
}
// 当准备向 第 index 行 选择第 j 列,进行存放 Q 时,先进行竖向检查、左上检查、以及右上检查,都符合条件时,再修改数组,进行递归,
// 最后 修改回 原先临时数组状态
for (int j = 0; j < help.length; j++) {
if (!checkZhuDuiJiao(index, j, help.length)) {
continue;
}
if (!checkFuDuiJiao(index, j, help.length)) {
continue;
}
if (!checkShuxiang(j)) {
continue;
}
zhuDuiJiaoHelp[index - j + help.length - 1] = true;
fuDuiJiaoHelp[index + j] = true;
shuXiangHelp[j] = true;
help[index][j] = 'Q';
backTracing(help, index + 1);
help[index][j] = '.';
zhuDuiJiaoHelp[index - j + help.length - 1] = false;
fuDuiJiaoHelp[index + j] = false;
shuXiangHelp[j] = false;
}
}

/**
* 主对角线 -- 横坐标 - 纵坐标 (在同一主对角线上的点,两坐标相减总相等 ,为了保证数组不越界,加上偏移量)
*
* @param index
* @param j
* @param length
* @return
*/
private static boolean checkZhuDuiJiao(int index, int j, int length) {
return !zhuDuiJiaoHelp[index - j + length - 1];
}

/**
* 副对角线 -- 横坐标 + 纵坐标 (在同一副对角线上的点,两坐标相加总相等)
*
* @param index
* @param j
* @param length
* @return
*/
private static boolean checkFuDuiJiao(int index, int j, int length) {
return !fuDuiJiaoHelp[index + j];
}

private static boolean checkShuxiang(int j) {
return !shuXiangHelp[j];
}


private static boolean checkXiexiang1(char[][] help, int index, int j) {
while (index - 1 >= 0 && j - 1 >= 0) {
if (help[index - 1][j - 1] == 'Q') {
return false;
}
index--;
j--;
}
return true;
}

private static boolean checkXiexiang2(char[][] help, int index, int j) {
while (index - 1 >= 0 && j + 1 < help.length) {
if (help[index - 1][j + 1] == 'Q') {
return false;
}
index--;
j++;
}
return true;
}

private static boolean checkShuxiang2(char[][] help, int index, int j) {
for (int i = 0; i <= index; i++) {
if (help[i][j] == 'Q') {
return false;
}
}
return true;
}
}

486. 预测赢家

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
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
class Solution {
/**
* 递归版本 2
*
* @param nums
* @return
*/
public static boolean PredictTheWinner(int[] nums) {
int sum = 0;
for (int num : nums) {
sum += num;
}
int n = dfs(nums, 0, nums.length - 1);
return n >= (sum - n);
}

//表示,先手时,在begin - end 范围内能取的最大值
private static int dfs(int[] nums, int begin, int end) {
if (begin == end) {
return nums[begin];
}
if (begin + 1 == end) {
return Math.max(nums[begin], nums[end]);
}
// 当前选择begin位置上的数时,则下次选择的范围只能是[begin+1,end-1](后手选择了end)、或者是[bengin+2,end](后手选择了begin+1)
// Math.min(dfs(nums, begin + 1, end - 1), dfs(nums, begin + 2, end));后手一定会留最小的取值范围
int beginValue = nums[begin] + Math.min(dfs(nums, begin + 1, end - 1), dfs(nums, begin + 2, end));
int endValue = nums[end] + Math.min(dfs(nums, begin + 1, end - 1), dfs(nums, begin, end - 2));
return Math.max(beginValue, endValue);
}


/**
* 递归版本一
*
* @param nums
* @return
*/
public static boolean PredictTheWinner4(int[] nums) {
return dfs2(nums, 0, nums.length - 1, 1) >= 0;
}

private static int dfs2(int[] nums, int begin, int end, int flag) {
if (begin == end) {
return nums[begin] * flag;
}
int socreBegin = nums[begin] * flag + dfs2(nums, begin + 1, end, -flag);
int socreEnd = nums[end] * flag + dfs2(nums, begin, end - 1, -flag);
if (flag == 1) {
return Math.max(socreBegin, socreEnd);
} else {
return Math.min(socreBegin, socreEnd);
}
}


/**
* 优化的 DP -- dp[i][j] 与 另一方法的 表意不同
* 定义二维数组 dp,其行数和列数都等于数组的长度,dp[i][j] 表示当数组剩下的部分为下标 i 到下标 j时,当前玩家领先数值大小
* 当前玩家与另一个玩家的分数之差的最大值,注意当前玩家不一定是先手。
* <p>
* 只有当 i≤j 时,数组剩下的部分才有意义,因此当 i>j 时,dp[i][j]=0。
*
* @param nums
* @return
*/
public static boolean PredictTheWinner2(int[] nums) {
int length = nums.length;
int[][] dp = new int[length][length];
for (int i = 0; i < length; i++) {
dp[i][i] = nums[i];
}
for (int end = 1; end < length; end++) {
for (int begin = end - 1; begin >= 0; begin--) {
//begin -end 范围内,player1先手,取begin位置上的数, nums[begin] - dp[begin + 1][end]
//dp[begin + 1][end] 表示 此时 player2 的领先数值大小
dp[begin][end] = Math.max(nums[begin] - dp[begin + 1][end], nums[end] - dp[begin][end - 1]);
}
}
return dp[0][length - 1] >= 0;
}

static int[] temp;

/**
* dp 动态规划 int[][] dp = new int[length][length];
* dp[begin][end] 表示: 在nums数组中的begin - end 范围内,先手 所能获取到的最大值
* 就是计算出player1可能得到的最大分数,然后用数组总和减去player1的得分就是player2的得分,然后两者比较一下就可以了。
*
* @param nums
* @return
*/
public static boolean PredictTheWinner3(int[] nums) {
int length = nums.length;
int[][] dp = new int[length][length];
temp = new int[length];
int pre = 0;
for (int i = 0; i < length; i++) {
temp[i] = pre + nums[i];
pre = temp[i];
}
for (int end = 0; end < length; end++) {
for (int begin = end; begin >= 0; begin--) {
if (begin == end) {
// 如果当前 begin 和 end 相等,则dp[begin][end] = nums[end];
dp[begin][end] = nums[end];
} else {
//当前先手取 begin位置上的值,则 num1 = nums[begin] + (begin-1,end)范围内的最小值
//(begin-1,end)范围内的最小值 的获取: sum(begin + 1, end, nums) - dp[begin + 1][end];
//(begin-1,end)所有数据之和 减去 (begin-1,end)范围内先手的最大值
int num1 = nums[begin] + sum(begin + 1, end, nums) - dp[begin + 1][end];
//当前先手取 end 位置上的值
int num2 = nums[end] + sum(begin, end - 1, nums) - dp[begin][end - 1];
dp[begin][end] = Math.max(num1, num2);
}
}
}
int sum = sum(0, length - 1, nums);
int leftNum = sum - dp[0][length - 1];
return dp[0][length - 1] >= leftNum;
}

//求取begin - end 之间的和
private static int sum(int begin, int end, int[] nums) {
return temp[end] - temp[begin] + nums[begin];
}
}

332. 重新安排行程

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
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
/**
* tags: 欧拉路径 dfs 回溯 图
* <p>
* <p>
* 什么是欧拉路径?欧拉路径就是一条能够不重不漏地经过图上的每一条边的路径,即小学奥数中的一笔画问题。而若这条路径的起点和终点相同,则将这条路径称为欧拉回路。
* <p>
* 如何判断一个图是否有欧拉路径呢?显然,与一笔画问题相同,一个图有欧拉路径需要以下几个条件:
* <p>
* 首先,这是一个连通图
* 若是无向图,则这个图的度数为奇数的点的个数必须是0或2;若是有向图,则要么所有点的入度和出度相等,要么有且只有两个点的入度分别比出度大1和少1
* 上面这两个条件很好证明。查找欧拉路径前,必须先保证该图满足以上两个条件,否则直接判误即可。
* <p>
* 查找欧拉路径的算法有Fluery算法和Hierholzer算法。下面介绍一下Hierholzer算法。
*
* @author l00511002
* @since 2020-08-27
*/
class Solution {
/**
* 测试代码
* 特殊用例--[["JFK","KUL"],["JFK","NRT"],["NRT","JFK"]]
* String[][] array = {{"MUC", "LHR"}, {"JFK", "MUC"}, {"SFO", "SJC"}, {"LHR", "SFO"}};
* String[][] array = {{"JFK", "KUL"}, {"JFK", "NRT"}, {"NRT", "JFK"}};
* String[][] array = {{"JFK", "SFO"}, {"JFK", "ATL"}, {"SFO", "ATL"}, {"ATL", "JFK"}, {"ATL", "SFO"}};
* List<List<String>> tickets = new ArrayList<>();
* for (int i = 0; i < array.length; i++) {
* String[] strs = array[i];
* List<String> strings = Arrays.asList(strs);
* tickets.add(strings);
* }
*
* @param args
*/
public static void main(String[] args) {
String[][] array = {{"JFK", "KUL"}, {"JFK", "NRT"}, {"NRT", "JFK"}};
List<List<String>> tickets = new ArrayList<>();
for (int i = 0; i < array.length; i++) {
String[] strs = array[i];
List<String> strings = Arrays.asList(strs);
tickets.add(strings);
}
System.out.println(findItinerary(tickets));
}

/**
* 新建一个 HashMap<String, ArrayList<String>> map , key 表示 from , value 表示 to 的 list
* map 填充完毕后,对 list 进行排序,目的在于保证找到result-size长度的第一个集合就是按字符自然排序的
* (result的size一定等于tickets.length+1)
* 接着 DFS + 回溯 ,这里注意,从list中获取一个to时,先判断是否为空串,若不是,加入到tempList中,继续调用dfs方法
* 若是空串,表明,该 to 已经访问过,直接continue!!!
* -- 注意,dfs处理完当前string后,一定要将其从空串复原, 且 复原 tempResult !!!
* --- 定义全局 Boolean变量,剪枝 的同时,保证全局变量result 只会赋值一次 !!
*
* @param tickets
* @return
*/
static List<String> result;

static boolean hasFind;

public static List<String> findItinerary2(List<List<String>> tickets) {
result = new ArrayList<>();
hasFind = false;
HashMap<String, ArrayList<String>> map = new HashMap<>();
for (List<String> ticket : tickets) {
String from = ticket.get(0);
String to = ticket.get(1);
if (map.containsKey(from)) {
map.get(from).add(to);
} else {
ArrayList<String> list = new ArrayList<>();
list.add(to);
map.put(from, list);
}
}
//对list进行排序
for (Map.Entry<String, ArrayList<String>> entry : map.entrySet()) {
ArrayList<String> value = entry.getValue();
value.sort((x, y) -> x.compareTo(y));
}
String from = "JFK";
int targetLength = tickets.size() + 1;
ArrayList<String> tempResult = new ArrayList<>();
tempResult.add(from);
dfs2(tempResult, map, from, targetLength);
return result;
}

private static void dfs2(ArrayList<String> tempResult, HashMap<String, ArrayList<String>> map, String
from, int targetLength) {
// 定义全局变量,剪枝 的同时,保证全局变量result 只会赋值一次
if (hasFind) {
return;
}
if (tempResult.size() == targetLength) {
result = new ArrayList<>(tempResult);
hasFind = true;
return;
}
if (!map.containsKey(from)) {
return;
}
ArrayList<String> list = map.get(from);
for (int i = 0; i < list.size(); i++) {
String to = list.get(i);
if (to.length() == 0) {
continue;
}
// 修改为空串,表明 改 string 已经访问过了
list.set(i, "");
tempResult.add(to);
dfs2(tempResult, map, to, targetLength);
// 复原 tempResult
tempResult.remove(tempResult.size() - 1);
//注意,dfs处理完当前string后,一定要将其从空串复原!!!
list.set(i, to);
}
}

/******************* 代码优化 *****************************************/
/**
* Hierholzer 算法用于在连通图中寻找欧拉路径,其流程如下:
* 1、从起点出发,进行深度优先搜索。
* 2、每次沿着某条边从某个顶点移动到另外一个顶点的时候,都需要删除这条边。
* 3、如果没有可移动的路径,则将所在节点加入到栈中,并返回。
* <p>
* 当我们顺序地考虑该问题时,我们也许很难解决该问题,因为我们无法判断当前节点的哪一个分支是「死胡同」分支。
* 不妨倒过来思考。我们注意到只有那个入度与出度差为 11 的节点会导致死胡同。而该节点必然是最后一个遍历到的节点。
* 我们可以改变入栈的规则,当我们遍历完一个节点所连的所有节点后,我们才将该节点入栈(即逆序入栈)。
* 对于当前节点而言,从它的每一个非「死胡同」分支出发进行深度优先搜索,都将会搜回到当前节点。
* 而从它的「死胡同」分支出发进行深度优先搜索将不会搜回到当前节点。
* 也就是说当前节点的死胡同分支将会优先于其他非「死胡同」分支入栈。
* 这样就能保证我们可以「一笔画」地走完所有边,最终的栈中逆序地保存了「一笔画」的结果。我们只要将栈中的内容反转,即可得到答案。
*/
static List<String> list;
//定义一个优先级队列,用来存放to集合
static HashMap<String, PriorityQueue<String>> map;

public static List<String> findItinerary(List<List<String>> tickets) {
list = new ArrayList<>();
map = new HashMap<>();
for (List<String> ticket : tickets) {
String from = ticket.get(0);
String to = ticket.get(1);
if (!map.containsKey(from)) {
map.put(from, new PriorityQueue<>());
}
map.get(from).offer(to);
}
dfs("JFK");
return list;
}

private static void dfs(String from) {
while (map.containsKey(from) && !map.get(from).isEmpty()) {
dfs(map.get(from).poll());
}
//头插法
list.add(0, from);
}
}