复习

MySQL数据库时间类型数据存储建议

转载自https://github.com/Snailclimb/JavaGuide(添加小部分笔记)感谢作者!

不要用字符串存储日期 #

  • 优点:简单直白
  • 缺点
    1. 字符串占有的空间更大
    2. 字符串存储的日期效率比较低(逐个字符进行比较),无法用日期相关的API进行计算和比较

Datetime和Timestamp之间抉择 #

Datetime 和 Timestamp 是 MySQL 提供的两种比较相似的保存时间的数据类型。他们两者究竟该如何选择呢?

通常我们都会首选 Timestamp

Datetime类型没有时区信息 #

  1. DateTime 类型是没有时区信息的(时区无关) ,DateTime 类型保存的时间都是当前会话所设置的时区对应的时间。这样就会有什么问题呢?当你的时区更换之后,比如你的服务器更换地址或者更换客户端连接时区设置的话,就会导致你从数据库中读出的时间错误。不要小看这个问题,很多系统就是因为这个问题闹出了很多笑话。
  2. Timestamp 和时区有关。Timestamp 类型字段的值会随着服务器时区的变化而变化,自动换算成相应的时间,说简单点就是在不同时区查询到同一个条记录此字段的值会不一样

案例

-- 建表
CREATE TABLE `time_zone_test` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT,
  `date_time` datetime DEFAULT NULL,
  `time_stamp` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8; 

-- 插入数据
INSERT INTO time_zone_test(date_time,time_stamp) VALUES(NOW(),NOW());
-- 查看数据
select date_time,time_stamp from time_zone_test;
-- 结果
/*
 +---------------------+---------------------+
| date_time           | time_stamp          |
+---------------------+---------------------+
| 2020-01-11 09:53:32 | 2020-01-11 09:53:32 |
+---------------------+---------------------+
------ 
*/

修改时区并查看数据

...

SQL语句在MySQL中的执行过程

转载自https://github.com/Snailclimb/JavaGuide(添加小部分笔记)感谢作者!

原文 https://github.com/kinglaw1204 感谢作者

  • 本篇文章会分析一个SQL语句在MySQL的执行流程,包括SQL的查询在MySQL内部会怎么流转SQL语句的更新是怎么完成的
  • 分析之前先看看MySQL的基础架构,知道了MySQL由哪些组件组成以及这些组件的作用是什么,可以帮助我们理解解决这些问题

MySQL基础架构分析 #

ly-20241212141908611

MySQL基本架构概览 #

  • 下图是MySQL的简要架构图,从下图可以看到用户的SQL语句在MySQL内部是如何执行的
  • 先简单介绍一个下图涉及的一些组件的基本作用 ly-20241212141908907
    1. 连接器身份认证权限相关(登录MySQL的时候)
    2. 查询缓存:执行查询语句的时候,会先查询缓存(MySQL8.0版本后移除,因为这个功能不太实用)
    3. 分析器没有命中缓存的话,SQL语句就会经过分析器,分析器说白了就是要先看你的SQL语句干嘛,再检查你的SQL语句语法是否正确
    4. 优化器:按照MySQL认为最优的方案去执行
    5. 执行器执行语句,然后从存储引擎返回数据
  • 简单来说 MySQL 主要分为 Server 层和存储引擎层:
    • Server 层:主要包括连接器查询缓存分析器优化器执行器等,所有跨存储引擎的功能都在这一层实现,比如存储过程触发器视图函数等,还有一个通用的日志模块 binlog 日志模块
    • 存储引擎: 主要负责数据的存储读取,采用可以替换的插件式架构,支持 InnoDB、MyISAM、Memory 等多个存储引擎,其中 InnoDB 引擎有自有的日志模块 redolog 模块现在最常用的存储引擎是 InnoDB,它从 MySQL 5.5 版本开始就被当做默认存储引擎了

Server层基本组件介绍 #

ly-20241212141909073

  1. 连接器 连接器主要和身份认证权限相关的功能相关,就好比一个级别很高的门卫一样

    主要负责用户登录数据库,进行用户的身份认证,包括校验账户密码,权限等操作,如果用户账户密码已通过,连接器会到权限表中查询该用户的所有权限,之后在这个连接里的权限逻辑判断都是会依赖此时读取到的权限数据,也就是说,后续只要这个连接不断开即使管理员修改了该用户的权限,该用户也是不受影响的。

  2. 查询缓存(MySQL8.0 版本后移除)
    查询缓存主要用来缓存我们所执行的 SELECT 语句以及该语句的结果集

    • 连接建立后,执行查询语句的时候,会先查询缓存,MySQL 会先校验这个 SQL 是否执行过,以 Key-Value 的形式缓存在内存中,Key 是查询预计,Value 是结果集。如果缓存 key 被命中,就会直接返回给客户端,如果没有命中,就会执行后续的操作,完成后也会把结果缓存起来,方便下一次调用。当然在真正执行缓存查询的时候还是会校验用户的权限,是否有该表的查询条件。

    • MySQL 查询不建议使用缓存,因为查询缓存失效在实际业务场景中可能会非常频繁,假如你对一个表更新的话,这个表上的所有的查询缓存都会被清空。对于不经常更新的数据来说,使用缓存还是可以的。所以,一般在大多数情况下我们都是不推荐去使用查询缓存的。

    MySQL 8.0 版本后删除了缓存的功能,官方也是认为该功能在实际的应用场景比较少,所以干脆直接删掉了

    ...

innodb引擎对MVCC的实现

转载自https://github.com/Snailclimb/JavaGuide(添加小部分笔记)感谢作者!

一致性非锁定读和锁定读 #

一致性非锁定读 #

★★非锁定★★

  • 对于一致性非锁定读(Consistent Nonlocking Reads)的实现,通常做法是加一个版本号或者时间戳字段,在更新数据的同时版本号+1或者更新时间戳。查询时,将当前可见的版本号对应记录的版本号进行比对,如果记录的版本小于可见版本,则表示该记录可见
  • InnoDB存储引擎中,多版本控制(multi versioning)即是非锁定读的实现。如果读取的行正在执行DELETEUPDATE操作,这时读取操作不会去等待行上 锁的释放.相反地,Inn哦DB存储引擎会去读取行的一个快照数据,对于这种读取历史数据的方式,我们叫它快照读(snapshot read)
  • Repeatable ReadRead Committed 两个隔离级别下,如果是执行普通的 select 语句(不包括 select ... lock in share mode ,select ... for update)则会使用 一致性非锁定读(MVCC)。并且在 Repeatable ReadMVCC 实现了可重复读和防止部分幻读

锁定读 #

  • 如果执行的是下列语句,就是锁定读(Locking Reads)

    1. select ... lock in share

    2. select ... for update

    3. insert upatedelete

    4. 锁定读下,读取的是数据的最新版本,这种读也被称为当前读current read锁定读会对读取到的记录加锁

    5. select ... lock in share mode :对(读取到的)记录加S锁,其他事务也可以加S锁,如果加X锁则会被阻塞

    6. select ... for updateinsertupdatedelete:对记录加X锁,且其他事务不能加任何锁

  • 在一致性非锁定读下,即使读取的记录已被其他事务加上X锁,这时记录也是可以被读取的,即读取的快照数据

    ...

MySQL事务隔离级别详解

转载自https://github.com/Snailclimb/JavaGuide(添加小部分笔记)感谢作者!

事务隔离级别总结 #

  • SQL标准定义了四个隔离级别

    1. READ-UNCOMMITTED(读取未提交)最低的隔离级别,允许读取尚未提交的数据变更,可能会导致脏读、幻读或不可重复读
    2. READ-COMMITED(读取已提交):允许读取并发事务 已经提交的数据,可以阻止脏读,但是幻读不可重复读仍有可能发生
    3. REPEATABLE-READ(可重复读):对同一字段的多次读取结果都是一致的,除非数据是被本身事务自己所修改,可以阻止脏读不可重复读,但幻读仍有可能发生
    4. SERIALIZABLE(可串行化)最高的隔离级别,完全服从ACID的隔离级别。所有的事务依次逐个执行,这样事务之间就完全不可能产生干扰,也就是说,该级别可以防止脏读不可重复读以及幻读
    隔离级别脏读不可重复读幻读
    READ-UNCOMMITTED
    READ-COMMITTED×
    REPEATABLE-READ××
    SERIALIZABLE×××
  • MySQL InnoDB 存储引擎的默认支持的隔离级别是 REPEATABLE-READ(可重读)

    使用命令查看,通过SELECT @@tx_isolation;
    MySQL 8.0 该命令改为SELECT @@transaction_isolation;

    MySQL> SELECT @@tx_isolation;
    +-----------------+
    | @@tx_isolation  |
    +-----------------+
    | REPEATABLE-READ |
    +-----------------+ 
    
  • 从上面对SQL标准定义了四个隔离级别的介绍可以看出,标准的SQL隔离级别里,REPEATABLE-READ(可重复读)是不可以防止幻读的。但是,InnoDB实现的REPEATABLE-READ 隔离级别其实是可以解决幻读问题发生的,分两种情况

    1. 快照读:由MVCC机制来保证不出现幻读
    2. 当前读:使用Next-Key Lock进行加锁来保证不出现幻读,Next-Key Lock是行锁(Record Lock )和间隙锁(Gap Lock)的结合,行锁只能锁住已经存在的行,为了避免插入新行,需要依赖间隙锁 (只用间隙锁不行,因为间隙锁是 > 或 < ,不包括等于,所以再可重复读下原记录可能会被删掉)

    因为隔离级别越低,事务请求的锁越少,所以大部分数据库系统的隔离级别都是 READ-COMMITTED ,但是你要知道的是 InnoDB 存储引擎默认使用 REPEATABLE-READ 并不会有任何性能损失。

    ...

日志

转载自https://github.com/Snailclimb/JavaGuide(添加小部分笔记)感谢作者!

前言 #

  • 首先要了解一个东西 :WAL,全称 Write-Ahead Logging,它的关键点就是先写日志,再写磁盘

    在概念上,innodb通过***force log at commit***机制实现事务的持久性,即在事务提交的时候,必须先将该事务的所有事务日志写入到磁盘上的redo log file和undo log file中进行持久化

    1. WAL 机制的原理也很简单:修改并不直接写入到数据库文件中,而是写入到另外一个称为 WAL 的文件中;如果事务失败,WAL 中的记录会被忽略,撤销修改;如果事务成功,它将在随后的某个时间被写回到数据库文件中,提交修改

    2. 使用 WAL 的数据库系统不会再每新增一条 WAL 日志就将其刷入数据库文件中,一般积累一定的量然后批量写入,通常使用页为单位,这是磁盘的写入单位。 同步 WAL 文件和数据库文件的行为被称为 checkpoint(检查点),一般在 WAL 文件积累到一定页数修改的时候;当然,有些系统也可以手动执行 checkpoint。执行 checkpoint 之后,WAL 文件可以被清空,这样可以保证 WAL 文件不会因为太大而性能下降。

      有些数据库系统读取请求也可以使用 WAL,通过读取 WAL 最新日志就可以获取到数据的最新状态

      关于checkpoint:https://www.cnblogs.com/chenpingzhao/p/5107480.html思考一下这个场景:如果重做日志可以无限地增大,同时缓冲池也足够大 ,那么是不需要将缓冲池中页的新版本刷新回磁盘。因为当发生宕机时,完全可以通过重做日志来恢复整个数据库系统中的数据到宕机发生的时刻。但是这需要两个前提条件:1、缓冲池可以缓存数据库中所有的数据;2、重做日志可以无限增大

      因此Checkpoint(检查点)技术就诞生了,目的是解决以下几个问题:1、缩短数据库的恢复时间;2、缓冲池不够用时,将脏页刷新到磁盘;3、重做日志不可用时,刷新脏页

      • 当数据库发生宕机时,数据库不需要重做所有的日志,因为Checkpoint之前的页都已经刷新回磁盘。数据库只需对Checkpoint后的重做日志进行恢复,这样就大大缩短了恢复的时间。
      • 当缓冲池不够用时,根据LRU算法会溢出最近最少使用的页,若此页为脏页,那么需要强制执行Checkpoint,将脏页也就是页的新版本刷回磁盘。
      • 当重做日志出现不可用时,因为当前事务数据库系统对重做日志的设计都是循环使用的,并不是让其无限增大的,重做日志可以被重用的部分是指这些重做日志已经不再需要,当数据库发生宕机时,数据库恢复操作不需要这部分的重做日志,因此这部分就可以被覆盖重用。如果重做日志还需要使用,那么必须强制Checkpoint,将缓冲池中的页至少刷新到当前重做日志的位置。
    3. mysql 的 WAL,大家可能都比较熟悉。mysql 通过 redo、undo 日志实现 WAL。redo log 称为重做日志,每当有操作时,在数据变更之前将操作写入 redo log,这样当发生掉电之类的情况时系统可以在重启后继续操作。undo log 称为撤销日志,当一些变更执行到一半无法完成时,可以根据撤销日志恢复到变更之间的状态。mysql 中用 redo log 来在系统 Crash 重启之类的情况时修复数据(事务的持久性),而 undo log 来保证事务的原子性。

      ...

索引

转载自https://github.com/Snailclimb/JavaGuide(添加小部分笔记)感谢作者!

补充索引基础知识(引自b站sgg视频) #

  1. 存储引擎,数据的基本单位是,如果数据很少,只有一页,那就简单,是直接二分查找(不涉及磁盘IO);如果数据很多,有好几个页,那么需要对页建立一种数据结构,能够最快定位到哪一页,然后减少磁盘IO

索引介绍 #

  • 索引是一种用于快速查询检索数据的数据结构,其本质可以看成是一种排序好的数据结构

    索引的作用就相当于书的目录。打个比方: 我们在查字典的时候,如果没有目录,那我们就只能一页一页的去找我们需要查的那个字,速度很慢。如果有目录了,我们只需要先去目录里查找字的位置,然后直接翻到那一页就行了

  • 索引底层数据结构存在很多种类型,常见的索引结构有:B树B+树Hash红黑树。在MySQL中,无论是Innodb还是MyIsam,都使用了B+树作为索引结构

索引的优缺点 #

优点:

  • 使用索引可以大大加快 数据的检索速度(大大减少检索的数据量), 这也是创建索引的最主要的原因。
  • 通过创建唯一性索引,可以保证数据库表中每一行数据的唯一性

缺点:

  • 创建索引维护索引需要耗费许多时间。当对表中的数据进行增删改的时候,如果数据有索引,那么索引也需要动态的修改,会降低 SQL 执行效率
  • 索引需要使用物理文件存储,也会耗费一定空间

索引一定会提高查询性能吗

  • 多数情况下,索引查询都是比全表扫描要快的。但是如果数据库的数据量不大,那么使用索引也不一定能够带来很大提升

索引的底层数据结构 #

Hash表 #

  • 哈希表是键值对的集合,通过键(key)即可快速取出对应的值(value),因此哈希表可以快速检索数据(接近O(1))

  • 为何能够通过key快速取出value呢?原因在于哈希算法(也叫散列算法)。通过哈希算法,我们可以快速找到key对应的index,找到了index也就找到了对应的value

    hash = hashfunc(key)
    index = hash % array_size
    

    注意,图中keys[天蓝色]是字符串不是什么莫名其妙的人 ly-20241212141858665

  • 哈希算法有个 Hash 冲突 问题,也就是说多个不同的 key 最后得到的 index 相同。通常情况下,我们常用的解决办法是 链地址法。链地址法就是将哈希冲突数据存放在链表中。就比如 JDK1.8 之前 HashMap 就是通过链地址法来解决哈希冲突的。不过,JDK1.8 以后HashMap为了减少链表过长的时候搜索时间过长引入了红黑树

  • 为了减少 Hash 冲突的发生,一个好的哈希函数应该**“均匀地”将数据分布**在整个可能的哈希值集合中

  • 由于Hash索引不支持顺序范围查询,假如要对表中的数据进行排序或者进行范围查询,那Hash索引就不行了,并且,每次IO只能取一个

    例如: SELECT * FROM tb1 WHERE id < 500 ;

    ...

字符集详解

转载自https://github.com/Snailclimb/JavaGuide(添加小部分笔记)感谢作者!

图示总结
ly-20241212141855666

  • MySQL字符编码集有两套UTF-8编码实现:utf-8utf8mb4
    而其中,utf-8 不支持存储emoji符号和一些比较复杂的汉字、繁体字,会出错

何为字符集 #

  • 字符是各种文字符号的统称,包括各个国家文字标点符号表情数字等等

    • 字符集就是一系列字符的集合,字符集的种类较多,每个字符集可以表示的字符范围通常不同,就比如说有些字符集无法表示汉字
  • 计算机只能存储二进制的数据,那英文汉字表情等字符应该如何存储呢

    • 我们要将这些字符和二进制的数据一一对应起来,比如说字符“a”对应“01100001”,反之,“01100001”对应 “a”。我们将字符对应二进制数据的过程称为"字符编码",反之,二进制数据解析成字符的过程称为“字符解码”。

      ly-20241212141855958

有哪些常见的字符集 #

  • 常见的字符集有ASCLLGB2312GBKUTF-8
  • 不同的字符集的主要区别在于
    1. 可以表示的字符范围
    2. 编码方式

ASCLL #

  • ASCII (American Standard Code for Information Interchange,美国信息交换标准代码) 是一套主要用于现代美国英语的字符集(这也是 ASCII 字符集的局限性所在)

    为什么 ASCII 字符集没有考虑到中文等其他字符呢? 因为计算机是美国人发明的,当时,计算机的发展还处于比较雏形的时代,还未在其他国家大规模使用。因此,美国发布 ASCII 字符集的时候没有考虑兼容其他国家的语言

  • ASCII 字符集至今为止共定义了 128 个字符,其中有 33 个控制字符(比如回车、删除)无法显示

  • 一个 ASCII 码长度是一个字节也就是 8 个 bit,比如“a”对应的 ASCII 码是“01100001”。不过,最高位是 0 仅仅作为校验位,其余 7 位使用 0 和 1 进行组合,所以,ASCII 字符集可以定义 128(2^7)个字符

    由于,ASCII 码可以表示的字符实在是太少了。后来,人们对其进行了扩展得到了 ASCII 扩展字符集 。ASCII 扩展字符集使用 8 位(bits)表示一个字符,所以,ASCII 扩展字符集可以定义 256(2^8)个字符

    ...

转载自https://github.com/Snailclimb/JavaGuide(添加小部分笔记)感谢作者!

  • 树是一种类似现实生活中的树的数据结构(倒置的树

  • 任何一颗非空树只有一个根节点

  • 一棵树具有以下特点:

    1. 一棵树中的任何两个节点有且仅有唯一的一条路相通 (因为每个结点只会有一个父节点)
    2. 一棵树如果有n个节点,那么它一定恰好有n-1条边
    3. 一棵树不包括回路
  • 下面是一颗二叉树 ly-20241212141852140 深度和高度是对应的;根节点所在层为1层

  • 常用概念

    1. 节点:树中每个元素都可以统称为节点

    2. 根节点:顶层节点,或者说没有父节点的节点。上图中A节点就是根节点

    3. 父节点:若一个节点含有子节点,则这个节点称为其子节点的父节点。上图中的 B 节点是 D 节点、E 节点的父节点

    4. 兄弟节点:具有相同父节点的节点互称为兄弟节点。上图中 D 节点、E 节点的共同父节点是 B 节点,故 D 和 E 为兄弟节点。

    5. 叶子节点:没有子节点的节点。上图中的 D、F、H、I 都是叶子节点

    6. 节点的高度**(跟叶子节点有关,同一层不一定一样)该节点到叶子节点最长路径所包含的边数

    7. 节点的深度**(跟根节点有关,同一层是一样的)根节点到该节点的路径所包含的边数**

    8. 节点的层数:节点的深度+1

    9. 树的高度:根节点的高度

二叉树的分类 #

  • **二叉树(Binary tree)**是每个节点最多只有两个分支(即不存在分支度大于2的节点)的树结构
  • 二叉树的分支,通常被称为左子树右子树,并且,二叉树的分支具有左右次序,不能随意颠倒
  • 二叉树的第i层至多拥有2^(i-1) 个节点
    深度为k的二叉树至多总共有 2^(k+1) -1 个节点 (深度为k,最多k + 1 层,最多为满二叉树的情况)
    至少有2^(k) 个节点,即 深度为k-1的二叉树的最多的节点再加1

(关于节点的深度的定义国内争议比较多,我个人比较认可维基百科对 节点深度的定义open in new window)。 ly-20241212141852434

满二叉树 #

一个二叉树,如果每一个的结点数都达到最大值,则这个二叉树就是 满二叉树。也就是说,如果一个二叉树的层数为 K,且结点总数是(2^k) -1 ,则它就是 满二叉树ly-20241212141852640

...

转载自https://github.com/Snailclimb/JavaGuide(添加小部分笔记)感谢作者!

什么是堆 #

  • 堆是满足以下条件的树 堆中每一个节点值都大于等于(或小于等于)子树中所有节点。或者说,任意一个节点的值**都大于等于(或小于等于)**所有子节点的值

    大家可以把堆(最大堆)理解为一个公司,这个公司很公平,谁能力强谁就当老大,不存在弱的人当老大,老大手底下的人一定不会比他强。这样有助于理解后续堆的操作。

    • 堆不一定是完全二叉树,为了方便存储索引,我们通常用完全二叉树的形式来表示堆
      广为人知的斐波那契堆二项堆就不是完全二叉树,它们甚至都不是二叉树
    • (二叉)堆是一个数组,它可以被看成是一个近似的完全二叉树
  • 下面给出的图是否是堆(通过定义)

    1,2是。 3不是。 ly-20241212141845610

堆的用途 #

  • 当我们只关心所有数据中的最大值或者最小值,存在多次获取最大值或者最小值,多次插入或删除数据时,就可以使用堆。

    有小伙伴可能会想到用有序数组,初始化一个有序数组时间复杂度是 O(nlog(n))[也就是将一堆数字乱序排序,最快是O(nlog(n))],查找最大值或者最小值时间复杂度都是 O(1),但是,涉及到更新(插入或删除)数据时,时间复杂度为 O(n),即使是使用复杂度为 O(log(n)) 的二分法找到要插入或者删除的数据,在移动数据时也需要 O(n) 的时间复杂度。

  • 相对于有序数组而言,堆的主要优势在于更新数据效率较高

    • 堆的初始化时间复杂度为O(nlog(n)),堆可以做到O(1)的时间复杂度取出最大值或者最小值O(log(n))的时间复杂度插入或者删除数据

堆的分类 #

  • 堆分为最大堆最小堆,二者的区别在于节点的排序方式
    • 最大堆:堆中的每一个节点的值都大于子树中所有节点的值
    • 最小堆:堆中的每一个节点的值都小于子树中所有节点的值
  • 如图,图1是最大堆,图2是最小堆 ly-20241212141845906

堆的存储 #

  • 由于完全二叉树的优秀性质利用数组存储二叉树即节省空间,又方便索引(若根结点的序号为1,那么对于树中任意节点i,其左子节点序号为 2*i,右子节点序号为 2*i+1)。
  • 为了方便存储索引(二叉)堆可以用完全二叉树的形式进行存储。存储的方式如下图所示 ly-20241212141846083

堆的操作 #

  • 堆的更新操作主要包括两种:插入元素删除堆顶元素

    堆是一个公平的公司,有能力的人自然会走到与他能力所匹配的位置

插入元素 #

  1. 将要插入的元素放到最后 ly-20241212141846259
  2. 从底向上,如果父节点该元素小,则该节点和父节点交换(其实就是一棵树有3个(最多)节点,与树上最大的节点比较) 直到无法交换(已经与根节点比较过) ly-20241212141846433 ly-20241212141846614

删除堆顶元素 #

  • 根据堆的性质可知,最大堆的堆盯元素为所有元素中最大的,最小堆的堆顶元素是所有元素中最小的

  • 当我们需要多次查找最大元素或者最小元素的时候,可以利用堆来实现

  • 删除堆顶元素后,为了保持堆的性质,需要对堆的结构进行调整,我们可以将这个过程称之为堆化

    1. 自底向上的堆化,上述的插入元素所使用的,就是自顶向上的堆化,元素从最底部向上移动
    2. 自顶向下的堆化,元素由顶部向下移动。在讲解删除堆顶元素的方法时,我将阐述这两种操作的过程
  • 自底向上堆化

    在堆这个公司中,会出现老大离职的现象,老大离职之后,它的位置就空出来了

    1. 首先删除堆顶元素,使得数组中下标为1的位置空出 ly-20241212141846789

      ...

转载自https://github.com/Snailclimb/JavaGuide(添加小部分笔记)感谢作者!

  • 图是一种较为复杂的非线性结构
  • 线性数据结构的元素满足唯一的线性关系,每个元素(除第一个和最后一个外)只有一个直接前驱和一个直接后继
  • 树形数据结构的元素之间有着明显的层级关系
  • 图形结构的元素之间的关系是任意的
    • 图就是由顶点有穷非空集合和顶点之间的组成的集合,通常表示为:G(V,E),其中,G表示一个图,V表示顶点的集合,E表示边的集合
    • 下面显示的即这种数据结构,而且还是一张有向图 ly-20241212141841748

图的基本概念 #

顶点 #

  • 图中的数据元素,我们称之为顶点,图至少有一个顶点有穷非空集合)
  • 对应到好友关系图,每一个用户就代表一个顶点

#

  • 顶点之间的关系表示
  • 对应到好友关系图,两个用户是好友的话,那两者之间就存在一条

#

  • 度表示一个顶点包含多少条边
  • 有向图中,分为出度入度,出度表示从该顶点出去的边的条数,入度表示从进入该顶点的边的条数
  • 对应到好友关系图,度就代表了某个人的好友数量

无向图和有向图 #

边表示的是顶点之间的关系,有的关系是双向的,比如同学关系,A是B的同学,那么B也肯定是A的同学,那么在表示A和B的关系时,就不用关注方向,用不带箭头的边表示,这样的图就是无向图

有的关系是有方向的,比如父子关系,师生关系,微博的关注关系,A是B的爸爸,但B肯定不是A的爸爸,A关注B,B不一定关注A。在这种情况下,我们就用带箭头的边表示二者的关系,这样的图就是有向图

无权图和带权图 #

对于一个关系,如果我们只关心关系的有无,而不关心关系有多强,那么就可以用无权图表示二者的关系。

对于一个关系,如果我们既关心关系的有无,也关心关系的强度,比如描述地图上两个城市的关系,需要用到距离,那么就用带权图来表示,带权图中的每一条边一个数值表示权值,代表关系的强度

下图就是一个带权有向图

ly-20241212141842057

图的存储 #

邻接矩阵存储 #

  • 邻接矩阵将图用二维矩阵存储,是一种比较直观的表示方式
  • 如果第i个顶点和第j个顶点有关系,且关系权值为n,则A[i] [j] = n
  • 在无向图中,我们只关心关系的有无,所以当顶点i顶点j有关系时,A[i] [j]=1 ; 当顶点i和顶点j没有关系时,A[i] [j] = 0 ,如下图所示
    ly-20241212141842233 无向图的邻接矩阵是一个对称矩阵,因为在无向图中,顶点i顶点j有关系,则顶点j顶点i必有关系
  • 有向图的邻接矩阵存储 ly-20241212141842414 邻接矩阵存储的方式优点是简单直接(直接使用一个二维数组即可),并且在获取两个顶点之间的关系的时候也非常高效*直接获取指定位置的数组元素。但是这种存储方式的确定啊也比较明显即 比较浪费空间

邻接表存储 #

  • 针对上面邻接矩阵比较浪费内存空间的问题,诞生了图的另一种存储方法–邻接表

  • 邻接链表使用一个链表来存储某个顶点的所有后继相邻顶点。对于图中每个顶点Vi ,把所有邻接于Vi 的顶点Vj 链接成一个单链表

    ...