数据结构

84 阅读7分钟

数据结构

www.hello-algo.com/chapter_dat…

所有数据结构都是基于数组、链表或二者的组合实现的。

  • 基于数组可实现:栈、队列、哈希表、树、堆、图、矩阵、张量(维度 ≥3 的数组)等。
  • 基于链表可实现:栈、队列、哈希表、树、堆、图等。

1. 数据结构分类

1.1. 逻辑结构

逻辑结构揭示了数据元素之间的逻辑关系。逻辑结构可分为“线性”和“非线性”两大类。 线性结构中元素之间是一对一的顺序关系。 image.png

1.2. 物理结构

物理结构分为连续和分散。当算法程序运行时,正在处理的数据主要存储在内存中系统通过内存地址来访问目标位置的数据image.png

当某块内存被某个程序占用时,则通常无法被其他程序同时使用了。因此在数据结构与算法的设计中,内存资源是一个重要的考虑因素物理结构反映了数据在计算机内存中的存储方式,可分为连续空间存储(数组)和分散空间存储(链表)。 image.png

2. 基本数据类型

基本数据类型是 CPU 可以直接进行运算的内容的类型,在算法中直接被使用,主要包括以下几种。

  • 整数类型 byteshortintlong 。
  • 浮点数类型 floatdouble ,用于表示小数。
  • 字符类型 char ,用于表示各种语言的字母、标点符号甚至表情符号等。
  • 布尔类型 bool ,用于表示“是”与“否”判断。

基本数据类型以二进制的形式存储在计算机中。字节(byte)由 8 比特(bit)组成。每种编程语言都有各自的数据类型定义,它们的占用空间、取值范围和默认值可能会有所不同。

基本数据类型提供了数据的“内容类型”,而数据结构提供了数据的“组织方式”

3. 数字编码

3.1. 原码、反码和补码

  • 原码:我们将数字的二进制表示的最高位视为符号位,其中 0 表示正数,1 表示负数,其余位表示数字的值。
  • 反码:正数的反码与其原码相同,负数的反码是对其原码除符号位外的所有位取反。
  • 补码:正数的补码与其原码相同,负数的补码是在其反码的基础上加 1 。 image.png

tips:

  1. 负数的原码无法直接用于运算
  2. 进行与负数相关的运算时,将负数原码转为反码之后进行运算
  3. 数字零的原码有 +0 和 −0 两种表示方式,因此引入补码进行修正歧义。
  4. 基于补码表示,计算机可以用同样的电路和操作来处理正数和负数的加法,不需要设计特殊的硬件电路来处理减法,并且无须特别处理正负零的歧义问题。这大大简化了硬件设计,提高了运算效率。

3.2. 浮点数编码

浮点数 float 采用了不同的表示方式。记一个 32 比特长度的二进制数为:

b31b30b29…b2b1b0

根据 IEEE 754 标准,32-bit 长度的 float 由以下三个部分构成。

  • 符号位 S :占 1 位 ,对应 b31 。
  • 指数位 E :占 8 位 ,对应 b30b29…b23 。
  • 分数位 N :占 23 位 ,对应 b22b21…b0 。

float 的表示方式包含指数位,导致其取值范围远大于 int但其副作用是牺牲了精度。整数类型 int 将全部 32 比特用于表示数字,数字是均匀分布的;而由于指数位的存在,浮点数 float 的数值越大,相邻两个数字之间的差值就会趋向越大。

二进制数 float 对应值的计算方法为: val=(1)b31×2(b30b29b23)2127×(1.b22b21b0)2\mathrm{val}=(-1)^{b_{31}}\times2^{(b_{30}b_{29}\ldots b_{23})_{2}{-127}}\times(1.b_{22}b_{21}\ldots b_{0})_{2} 转化到十进制下的计算公式为: val=(1)S×2E127×(1+N)\mathrm{val}=(-1)^{{\mathrm{S}}}\times2^{{\mathrm{E}-127}}\times(1+\mathrm{N}) 其中各项的取值范围为: S{0,1},E{1,2,,254}(1+N)=(1+i=123b23i2i)[1,2223]\mathrm{S\in}\{0,1\},\quad\mathrm{E}\in\{1,2,\ldots,254\}\\(1+\mathrm{N})=(1+\sum_{i=1}^{23}b_{23-i}2^{-i})\subset[1,2-2^{-23}]

4. 字符编码

4.1. ASCII 字符集

image.png

4.2. Unicode 字符集

Unicode 的中文名称为“统一码”,理论上能容纳 100 多万个字符。它致力于将全球范围内的字符纳入统一的字符集之中,提供一种通用的字符集来处理和显示各种语言文字,减少因为编码标准不同而产生的乱码问题。

Unicode 是一种通用字符集,本质上是给每个字符分配一个编号(称为“码点”),但它并没有规定在计算机中如何存储这些字符码点

当多种长度的 Unicode 码点同时出现在一个文本中时,系统如何解析字符?一种直接的解决方案是将所有字符存储为等长的编码image.png 若采用上述方案,英文文本占用空间的大小将会是 ASCII 编码下的两倍,非常浪费内存空间。

4.3. UTF-8 编码

UTF-8 编码是一种可变长度的编码,使用 1 到 4 字节来表示一个字符,根据字符的复杂性而变。

UTF-8 的编码规则并不复杂,分为以下两种情况。

  • 对于长度为 1 字节的字符,将最高位设置为 0 ,其余 7 位设置为 Unicode 码点。值得注意的是,ASCII 字符在 Unicode 字符集中占据了前 128 个码点。也就是说,UTF-8 编码可以向下兼容 ASCII 码。这意味着我们可以使用 UTF-8 来解析年代久远的 ASCII 码文本。

  • 对于长度为 n 字节的字符(其中 n>1),将首个字节的高 n 位都设置为 1 ,第高 n+1 位设置为 0 ;从第二个字节开始,将每个字节的高 2 位都设置为 10 ,这个 10 能够起到校验符的作用。;其余所有位用于填充字符的 Unicode 码点。 image.png

除了 UTF-8 之外,常见的编码方式还包括以下两种。

  • UTF-16 编码:使用 2 或 4 字节来表示一个字符。所有的 ASCII 字符和常用的非英文字符,都用 2 字节表示;少数字符需要用到 4 字节表示。对于 2 字节的字符,UTF-16 编码与 Unicode 码点相等。
  • UTF-32 编码:每个字符都使用 4 字节。这意味着 UTF-32 比 UTF-8 和 UTF-16 更占用空间,特别是对于 ASCII 字符占比较高的文本。

从存储空间占用的角度看,使用 UTF-8 表示英文字符非常高效,因为它仅需 1 字节;使用 UTF-16 编码某些非英文字符(例如中文)会更加高效,因为它仅需 2 字节,而 UTF-8 可能需要 3 字节。 从兼容性的角度看,UTF-8 的通用性最佳,许多工具和库优先支持 UTF-8 。

4.4. 其他编码方式

  1. EASCII 字符集:这些字符集的前 128 个字符统一为 ASCII 码,后 128 个字符定义不同,以适应不同语言的需求。
  2. GBK 字符集:在 GBK 的编码方案中,ASCII 字符使用一个字节表示,汉字使用两个字节表示。

4.5. 编程语言中的字符编码

对于以往的大多数编程语言,程序运行中的字符串都采用 UTF-16 或 UTF-32 这类等长编码。在等长编码下,我们可以将字符串看作数组来处理。

  • 随机访问:UTF-16 编码的字符串可以很容易地进行随机访问。UTF-8 是一种变长编码,要想找到第 i 个字符,我们需要从字符串的开始处遍历到第 i 个字符,这需要 O(n) 的时间。
  • 字符计数:与随机访问类似,计算 UTF-16 编码的字符串的长度也是 O(1) 的操作。但是,计算 UTF-8 编码的字符串的长度需要遍历整个字符串。
  • 字符串操作:在 UTF-16 编码的字符串上,很多字符串操作(如分割、连接、插入、删除等)更容易进行。在 UTF-8 编码的字符串上,进行这些操作通常需要额外的计算,以确保不会产生无效的 UTF-8 编码。