This commit is contained in:
krahets
2023-08-13 19:36:16 +08:00
parent 90c45dd8a0
commit 48b32c4488
38 changed files with 1871 additions and 281 deletions
@@ -3444,12 +3444,12 @@
<p>对于上表,需要注意以下几点:</p>
<ul>
<li>C, C++ 未明确规定基本数据类型大小,而因实现和平台各异。上表遵循 LP64 <a href="https://en.cppreference.com/w/cpp/language/types#Properties">数据模型</a>,其用于 Unix 64 位操作系统(例如 Linux , macOS)。</li>
<li>字符 <code>char</code> 的大小在 C, C++ 中为 1 字节,在大多数编程语言中取决于特定的字符编码方法,详见字符编码章节。</li>
<li>现代计算机 CPU 通常将 1 字节作为最小寻址内存单元。因此即使表示布尔量仅需 1 位(<span class="arithmatex">\(0\)</span><span class="arithmatex">\(1\)</span>),它在内存中通常被存储为 1 字节。</li>
<li>字符 <code>char</code> 的大小在 C, C++ 中为 1 字节,在大多数编程语言中取决于特定的字符编码方法,详见字符编码章节。</li>
<li>即使表示布尔量仅需 1 位(<span class="arithmatex">\(0\)</span><span class="arithmatex">\(1\)</span>),它在内存中通常被存储为 1 字节。这是因为现代计算机 CPU 通常将 1 字节作为最小寻址内存单元。</li>
</ul>
<p>那么,基本数据类型与数据结构之间有什么联系呢?我们知道,数据结构是在计算机中组织与存储数据的方式。它的主语是“结构”而非“数据”。</p>
<p>那么,基本数据类型与数据结构之间有什么联系呢?我们知道,数据结构是在计算机中组织与存储数据的方式。它的主语是“结构”而非“数据”。</p>
<p>如果想要表示“一排数字”,我们自然会想到使用数组。这是因为数组的线性结构可以表示数字的相邻关系和顺序关系,但至于存储的内容是整数 <code>int</code> 、小数 <code>float</code> 、还是字符 <code>char</code> ,则与“数据结构”无关。</p>
<p>换句话说,<strong>基本数据类型提供了数据的“内容类型”,而数据结构提供了数据的“组织方式”</strong>。例如以下代码,我们用相同的数据结构(数组)来记录与表示不同的基本数据类型(<code>int</code> , <code>float</code> , <code>chat</code>, <code>bool</code>)。</p>
<p>换句话说,<strong>基本数据类型提供了数据的“内容类型”,而数据结构提供了数据的“组织方式”</strong>。例如以下代码,我们用相同的数据结构(数组)来存储与表示不同的基本数据类型(<code>int</code> , <code>float</code> , <code>chat</code>, <code>bool</code>)。</p>
<div class="tabbed-set tabbed-alternate" data-tabs="1:12"><input checked="checked" id="__tabbed_1_1" name="__tabbed_1" type="radio" /><input id="__tabbed_1_2" name="__tabbed_1" type="radio" /><input id="__tabbed_1_3" name="__tabbed_1" type="radio" /><input id="__tabbed_1_4" name="__tabbed_1" type="radio" /><input id="__tabbed_1_5" name="__tabbed_1" type="radio" /><input id="__tabbed_1_6" name="__tabbed_1" type="radio" /><input id="__tabbed_1_7" name="__tabbed_1" type="radio" /><input id="__tabbed_1_8" name="__tabbed_1" type="radio" /><input id="__tabbed_1_9" name="__tabbed_1" type="radio" /><input id="__tabbed_1_10" name="__tabbed_1" type="radio" /><input id="__tabbed_1_11" name="__tabbed_1" type="radio" /><input id="__tabbed_1_12" name="__tabbed_1" type="radio" /><div class="tabbed-labels"><label for="__tabbed_1_1">Java</label><label for="__tabbed_1_2">C++</label><label for="__tabbed_1_3">Python</label><label for="__tabbed_1_4">Go</label><label for="__tabbed_1_5">JS</label><label for="__tabbed_1_6">TS</label><label for="__tabbed_1_7">C</label><label for="__tabbed_1_8">C#</label><label for="__tabbed_1_9">Swift</label><label for="__tabbed_1_10">Zig</label><label for="__tabbed_1_11">Dart</label><label for="__tabbed_1_12">Rust</label></div>
<div class="tabbed-content">
<div class="tabbed-block">
@@ -3454,65 +3454,69 @@
<h1 id="34">3.4. &nbsp; 字符编码 *<a class="headerlink" href="#34" title="Permanent link">&para;</a></h1>
<p>在计算机中,所有数据都是以二进制数的形式存储的,字符 <code>char</code> 也不例外。为了表示字符,我们需要建立一套字符集,规定每个字符和二进制数之间的一一对应关系。有了字符集之后,计算机就可以通过查表完成二进制数到字符的转换。</p>
<p>在计算机中,所有数据都是以二进制数的形式存储的,字符 <code>char</code> 也不例外。为了表示字符,我们需要建立一套字符集,规定每个字符和二进制数之间的一一对应关系。有了字符集之后,计算机就可以通过查表完成二进制数到字符的转换。</p>
<h2 id="341-ascii">3.4.1. &nbsp; ASCII 字符集<a class="headerlink" href="#341-ascii" title="Permanent link">&para;</a></h2>
<p>「ASCII 码」是最早出现的字符集,全称为“美国标准信息交换代码”。它使用 7 位二进制数(即一个字节的低 7 位)表示一个字符,最多能够表示 128 个不同的字符。这包括英文字母的大小写、数字 0-9 、一些标点符号,以及一些控制字符(如换行符和制表符)。</p>
<p><img alt="ASCII 码" src="../character_encoding.assets/ascii_table.png" /></p>
<p align="center"> Fig. ASCII 码 </p>
<p>然而,<strong>ASCII 码仅能够表示英文</strong>。随着计算机的全球化,诞生了一种能够表示更多语言的字符集「EASCII」。它在 ASCII 的 7 位基础上扩展到 8 位,能够表示 256 个不同的字符。在世界范围内,陆续出现了一批适用于不同地区的 EASCII 字符集。这些字符集的前 128 个字符统一为 ASCII 码,后 128 个字符定义不同,以适应不同语言的需求。</p>
<p>然而,<strong>ASCII 码仅能够表示英文</strong>。随着计算机的全球化,诞生了一种能够表示更多语言的字符集「EASCII」。它在 ASCII 的 7 位基础上扩展到 8 位,能够表示 256 个不同的字符。</p>
<p>在世界范围内,陆续出现了一批适用于不同地区的 EASCII 字符集。这些字符集的前 128 个字符统一为 ASCII 码,后 128 个字符定义不同,以适应不同语言的需求。</p>
<h2 id="342-gbk">3.4.2. &nbsp; GBK 字符集<a class="headerlink" href="#342-gbk" title="Permanent link">&para;</a></h2>
<p>后来人们发现,<strong>EASCII 码仍然无法满足许多语言的字符数量要求</strong>例如,汉字大约有近十万个,光日常使用的就有几千个。中国国家标准总局于 1980 年发布了「GB2312」字符集,其收录了 6763 个汉字,基本满足了汉字的计算机处理需要。</p>
<p>然而,GB2312 无法处理部分的罕见字和繁体字。之后在 GB2312 的基础上扩展得到了「GBK」字符集,它共收录了 21886 个汉字。在 GBK 编码方案中,ASCII 字符使用一个字节表示,汉字使用两个字节表示。</p>
<p>后来人们发现,<strong>EASCII 码仍然无法满足许多语言的字符数量要求</strong>比如汉字大约有近十万个,光日常使用的就有几千个。中国国家标准总局于 1980 年发布了「GB2312」字符集,其收录了 6763 个汉字,基本满足了汉字的计算机处理需要。</p>
<p>然而,GB2312 无法处理部分的罕见字和繁体字。「GBK」字符集是在 GB2312 的基础上扩展得到,它共收录了 21886 个汉字。在 GBK 编码方案中,ASCII 字符使用一个字节表示,汉字使用两个字节表示。</p>
<h2 id="343-unicode">3.4.3. &nbsp; Unicode 字符集<a class="headerlink" href="#343-unicode" title="Permanent link">&para;</a></h2>
<p>随着计算机的蓬勃发展,字符集与编码标准百花齐放,而这带来了许多问题。一方面,这些字符集一般只定义了特定语言的字符,无法在多语言环境下正常工作另一方面,同一种语言也存在多种字符集标准,如果两台电脑安装的是不同的编码标准,则在信息传递时就会出现乱码。</p>
<p>那个时代的人们就在想:<strong>如果推出一个足够完整的字符集,将世界范围内的所有语言和符号都收录其中,不就可以解决跨语言环境和乱码问题了吗</strong>?在这种想法的驱动下,一个大而全的字符集 Unicode 应运而生。</p>
<p>随着计算机的蓬勃发展,字符集与编码标准百花齐放,而这带来了许多问题。一方面,这些字符集一般只定义了特定语言的字符,无法在多语言环境下正常工作另一方面,同一种语言也存在多种字符集标准,如果两台电脑安装的是不同的编码标准,则在信息传递时就会出现乱码。</p>
<p>那个时代的研究人员就在想:<strong>如果推出一个足够完整的字符集,将世界范围内的所有语言和符号都收录其中,不就可以解决跨语言环境和乱码问题了吗</strong>?在这种想法的驱动下,一个大而全的字符集 Unicode 应运而生。</p>
<p>「Unicode」的全称为“统一字符编码”,理论上能容纳一百多万个字符。它致力于将全球范围内的字符纳入到统一的字符集之中,提供一种通用的字符集来处理和显示各种语言文字,减少因为编码标准不同而产生的乱码问题。</p>
<p>自 1991 年发布以来,Unicode 不断扩充新的语言与字符。截止 2022 年 9 月,Unicode 已经包含 149186 个字符,包括各种语言的字符、符号、甚至是表情符号等。在庞大的 Unicode 字符集中,常用的字符占用 2 字节,有些生僻的字符占 3 字节甚至 4 字节。</p>
<p>Unicode 是一种字符集标准,本质上是给每个字符分配一个编号(称为“码点”),<strong>但它并没有规定在计算机中如何存储这些字符码点</strong>。我们不禁会问:当多种长度的 Unicode 码点同时出现在同一个文本中时,系统如何解析字符?例如给定一个长度为 2 字节的编码,系统如何确认它是一个 2 字节的字符还是两个 1 字节的字符?</p>
<p>Unicode 是一种字符集标准,本质上是给每个字符分配一个编号(称为“码点”),<strong>但它并没有规定在计算机中如何存储这些字符码点</strong>。我们不禁会问:当多种长度的 Unicode 码点同时出现在同一个文本中时,系统如何解析字符?例如给定一个长度为 2 字节的编码,系统如何确认它是一个 2 字节的字符还是两个 1 字节的字符?</p>
<p>对于以上问题,<strong>一种直接的解决方案是将所有字符存储为等长的编码</strong>。如下图所示,“Hello”中的每个字符占用 1 字节,“算法”中的每个字符占用 2 字节。我们可以通过高位填 0 ,将“Hello 算法”中的所有字符都编码为 2 字节长度。这样系统就可以每隔 2 字节解析一个字符,恢复出这个短语的内容了。</p>
<p><img alt="Unicode 编码示例" src="../character_encoding.assets/unicode_hello_algo.png" /></p>
<p align="center"> Fig. Unicode 编码示例 </p>
<p>然而ASCII 码已经向我们证明,编码英文只需要 1 字节。若采用上述方案,英文文本占用空间的大小将会是 ASCII 编码下大小的 2 倍,非常浪费内存空间。因此,我们需要一种更加高效的 Unicode 编码方法。</p>
<p>然而 ASCII 码已经向我们证明,编码英文只需要 1 字节。若采用上述方案,英文文本占用空间的大小将会是 ASCII 编码下大小的倍,非常浪费内存空间。因此,我们需要一种更加高效的 Unicode 编码方法。</p>
<h2 id="344-utf-8">3.4.4. &nbsp; UTF-8 编码<a class="headerlink" href="#344-utf-8" title="Permanent link">&para;</a></h2>
<p>目前,UTF-8 已成为国际上使用最广泛的 Unicode 编码方法。<strong>它是一种可变长的编码</strong>,使用 1 到 4 个字节来表示一个字符,根据字符的复杂性而变。ASCII 字符只需要 1 个字节,拉丁字母和希腊字母需要 2 个字节,常用的中文字符需要 3 个字节,其他的一些生僻字符需要 4 个字节。</p>
<p>UTF-8 的编码规则并不复杂,分为两种情况:</p>
<ul>
<ol>
<li>对于长度为 1 字节的字符,将最高位设置为 <span class="arithmatex">\(0\)</span> 、其余 7 位设置为 Unicode 码点。值得注意的是,ASCII 字符在 Unicode 字符集中占据了前 128 个码点。也就是说,<strong>UTF-8 编码可以向下兼容 ASCII 码</strong>。这意味着我们可以使用 UTF-8 来解析年代久远的 ASCII 码文本。</li>
<li>对于长度为 <span class="arithmatex">\(n\)</span> 字节的字符(其中 <span class="arithmatex">\(n &gt; 1\)</span>),将首个字节的高 <span class="arithmatex">\(n\)</span> 位都设置为 <span class="arithmatex">\(1\)</span> 、第 <span class="arithmatex">\(n + 1\)</span> 位设置为 <span class="arithmatex">\(0\)</span> ;从第二个字节开始,将每个字节的高 2 位都设置为 <span class="arithmatex">\(10\)</span> ;其余所有位用于填充字符的 Unicode 码点。</li>
</ul>
<p>下图展示了“Hello算法”对应的 UTF-8 编码。最高 <span class="arithmatex">\(n\)</span>设置为 <span class="arithmatex">\(1\)</span> 比较容易理解,可以向系统指出字符的长度为 <span class="arithmatex">\(n\)</span> 。那么,为什么要将其余所有字节的高 2 位都设置为 <span class="arithmatex">\(10\)</span> 呢?实际上,这个 <span class="arithmatex">\(10\)</span> 能够起到校验符的作用,因为在 UTF-8 编码规则下,不可能有字符的最高两位是 <span class="arithmatex">\(10\)</span> 。这是因为长度为 1 字节的字符的最高一位是 <span class="arithmatex">\(0\)</span> 。假设系统从一个错误的字节开始解析文本,字节头部的 <span class="arithmatex">\(10\)</span> 能够帮助系统快速的判断出异常</p>
</ol>
<p>下图展示了“Hello算法”对应的 UTF-8 编码。观察发现,由于最高 <span class="arithmatex">\(n\)</span> 位都设置为 <span class="arithmatex">\(1\)</span> ,因此系统可以通过读取最高位 <span class="arithmatex">\(1\)</span> 的个数来解析出字符的长度为 <span class="arithmatex">\(n\)</span></p>
<p>但为什么要将其余所有字节的高 2 位都设置为 <span class="arithmatex">\(10\)</span> 呢?实际上,这个 <span class="arithmatex">\(10\)</span> 能够起到校验符的作用。假设系统从一个错误的字节开始解析文本,字节头部的 <span class="arithmatex">\(10\)</span> 能够帮助系统快速的判断出异常。</p>
<p>之所以将 <span class="arithmatex">\(10\)</span> 当作校验符,是因为在 UTF-8 编码规则下,不可能有字符的最高两位是 <span class="arithmatex">\(10\)</span> 。这个结论可以用反证法来证明:假设一个字符的最高两位是 <span class="arithmatex">\(10\)</span> ,说明该字符的长度为 <span class="arithmatex">\(1\)</span> ,对应 ASCII 码。而 ASCII 码的最高位应该是 <span class="arithmatex">\(0\)</span> ,与假设矛盾。</p>
<p><img alt="UTF-8 编码示例" src="../character_encoding.assets/utf-8_hello_algo.png" /></p>
<p align="center"> Fig. UTF-8 编码示例 </p>
<p>除了 UTF-8 之外,常见的编码方式还包括 UTF-16 和 UTF-32 。它们为 Unicode 字符集提供了不同的编码方法。</p>
<p>除了 UTF-8 之外,常见的编码方式还包括</p>
<ul>
<li><strong>UTF-16 编码</strong>:使用 2 或 4 个字节来表示一个字符。所有的 ASCII 字符和很多常用的非英文字符,都用 2 个字节表示;少数字符需要用到 4 个字节表示。对于 2 字节的字符,UTF-16 编码与 Unicode 码点相等。</li>
<li><strong>UTF-32 编码</strong>:每个字符都使用 4 个字节。这意味着 UTF-32 会比 UTF-8 和 UTF-16 更占用空间,特别是对于主要使用 ASCII 字符的文本。</li>
<li><strong>UTF-16 编码</strong>:使用 2 或 4 个字节来表示一个字符。所有的 ASCII 字符和常用的非英文字符,都用 2 个字节表示;少数字符需要用到 4 个字节表示。对于 2 字节的字符,UTF-16 编码与 Unicode 码点相等。</li>
<li><strong>UTF-32 编码</strong>:每个字符都使用 4 个字节。这意味着 UTF-32 会比 UTF-8 和 UTF-16 更占用空间,特别是对于 ASCII 字符占比较高的文本。</li>
</ul>
<p>从存储空间的角度看,使用 UTF-8 表示英文字符非常高效,因为它仅需 1 个字节;使用 UTF-16 编码某些非英文字符(例如中文)会更加高效,因为它只需要 2 个字节,而 UTF-8 可能需要 3 个字节。从兼容性的角度看,UTF-8 的通用性最佳,许多工具和库都优先支持 UTF-8 。</p>
<p>从存储空间的角度看,使用 UTF-8 表示英文字符非常高效,因为它仅需 1 个字节;使用 UTF-16 编码某些非英文字符(例如中文)会更加高效,因为它只需要 2 个字节,而 UTF-8 可能需要 3 个字节。</p>
<p>从兼容性的角度看,UTF-8 的通用性最佳,许多工具和库都优先支持 UTF-8 。</p>
<h2 id="345">3.4.5. &nbsp; 编程语言的字符编码<a class="headerlink" href="#345" title="Permanent link">&para;</a></h2>
<p>对于以往的大多数编程语言,程序运行中的字符串都采用 UTF-16 或 UTF-32 这类等长的编码。这是因为在等长编码下,我们可以将字符串看作数组来处理,具体来说</p>
<p>对于以往的大多数编程语言,程序运行中的字符串都采用 UTF-16 或 UTF-32 这类等长的编码。这是因为在等长编码下,我们可以将字符串看作数组来处理,其优点包括</p>
<ul>
<li><strong>随机访问</strong>: UTF-16 编码的字符串可以很容易地进行随机访问。UTF-8 是一种变长编码,要找到第 <span class="arithmatex">\(i\)</span> 个字符,我们需要从字符串的开始处遍历到第 <span class="arithmatex">\(i\)</span> 个字符,这需要 <span class="arithmatex">\(O(n)\)</span> 的时间。</li>
<li><strong>字符计数</strong>: 与随机访问类似,计算 UTF-16 字符串的长度也是 <span class="arithmatex">\(O(1)\)</span> 的操作。但是,计算 UTF-8 编码的字符串的长度需要遍历整个字符串。</li>
<li><strong>字符串操作</strong>: 在 UTF-16 编码的字符串中,很多字符串操作(如分割、连接、插入、删除等)都更容易进行。在 UTF-8 编码的字符串上进行这些操作通常需要额外的计算,以确保不会产生无效的 UTF-8 编码。</li>
</ul>
<p>编程语言的字符编码方案设计是一个很有趣的话题,涉及到许多因素:</p>
<p>实际上,编程语言的字符编码方案设计是一个很有趣的话题,涉及到许多因素:</p>
<ul>
<li>Java 的 <code>String</code> 类型使用 UTF-16 编码,每个字符占用 2 字节。这是因为 Java 语言设计之初,人们认为 16 位足以表示所有可能的字符。然而,这是一个不正确的判断。后来 Unicode 规范扩展到了超过 16 位,所以 Java 中的字符现在可能由一对 16 位的值(称为“代理对”)表示。</li>
<li>JavaScript 和 TypeScript 的字符串使用 UTF-16 编码的原因与 Java 类似。当 JavaScript 语言在 1995 年被 Netscape 公司首次引入时,Unicode 还处于相对早期的阶段,那时候使用 16 位的编码就足够表示所有的 Unicode 字符了。</li>
<li>C# 使用 UTF-16 编码,主要因为 .NET 平台是由 Microsoft 设计的,而 Microsoft 的很多技术,包括 Windows 操作系统,都广泛地使用 UTF-16 编码。</li>
</ul>
<p>由于以上编程语言对字符数量的低估,它们不得不采取“代理对”的方式来表示超过 16 位长度的 Unicode 字符。这是一个不得已为之的无奈之举。一方面,包含代理对的字符串中,一个字符可能占用 2 字节或 4 字节,因此丧失了等长编码的优势。另一方面,处理代理对需要增加额外代码,这增加了编程的复杂性和 Debug 难度。</p>
<p>由于以上编程语言对字符数量的低估,它们不得不采取“代理对”的方式来表示超过 16 位长度的 Unicode 字符。这是一个不得已为之的无奈之举。一方面,包含代理对的字符串中,一个字符可能占用 2 字节或 4 字节,从而丧失了等长编码的优势。另一方面,处理代理对需要增加额外代码,这增加了编程的复杂性和 Debug 难度。</p>
<p>出于以上原因,部分编程语言提出了不同的编码方案:</p>
<ul>
<li>Python 3 使用一种灵活的字符串表示,存储的字符长度取决于字符串中最大的 Unicode 码点。对于全部是 ASCII 字符的字符串,每个字符占用 1 个字节;如果字符串中包含的字符超出了 ASCII 范围,但全部在基本多语言平面(BMP)内,每个字符占用 2 个字节;如果字符串中有超出 BMP 的字符,那么每个字符占用 4 个字节。</li>
<li>Go 语言的 <code>string</code> 类型在内部使用 UTF-8 编码。Go 语言还提供了 <code>rune</code> 类型,它用于表示单个 Unicode 码点。</li>
<li>Rust 语言的 str 和 String 类型在内部使用 UTF-8 编码。Rust 也提供了 char 类型,用于表示单个 Unicode 码点。</li>
</ul>
<p>需要注意的是,以上讨论的都是字符串在编程语言中的存储方式,<strong>这和字符串如何在文件中存储或在网络中传输是两个不同的问题</strong>。在文件存储或网络传输中,我们一般会将字符串编码为 UTF-8 格式,以达到最优的兼容性和空间效率。</p>
<p>需要注意的是,以上讨论的都是字符串在编程语言中的存储方式,<strong>这和字符串如何在文件中存储或在网络中传输是两个不同的问题</strong>。在文件存储或网络传输中,我们通常会将字符串编码为 UTF-8 格式,以达到最优的兼容性和空间效率。</p>
@@ -3412,10 +3412,10 @@
<h1 id="31">3.1. &nbsp; 数据结构分类<a class="headerlink" href="#31" title="Permanent link">&para;</a></h1>
<p>数据结构可以从逻辑结构物理结构两个维度进行分类。</p>
<p>常见的数据结构包括数组、链表、栈、队列、哈希表、树、堆、图,它们可以从逻辑结构”和“物理结构两个维度进行分类。</p>
<h2 id="311">3.1.1. &nbsp; 逻辑结构:线性与非线性<a class="headerlink" href="#311" title="Permanent link">&para;</a></h2>
<p><strong>「逻辑结构」揭示了数据元素之间的逻辑关系</strong>。在数组和链表中,数据按照顺序依次排列,体现了数据之间的线性关系;而在树中,数据从顶部向下按层次排列,表现出祖先与后代之间的派生关系;图则由节点和边构成,反映了复杂的网络关系。</p>
<p>逻辑结构通常分为“线性”和“非线性”两类。线性结构比较直观,指数据在逻辑关系上呈线性排列;非线性结构则相反,呈非线性排列。</p>
<p>逻辑结构可被分为“线性”和“非线性”两类。线性结构比较直观,指数据在逻辑关系上呈线性排列;非线性结构则相反,呈非线性排列。</p>
<ul>
<li><strong>线性数据结构</strong>:数组、链表、栈、队列、哈希表。</li>
<li><strong>非线性数据结构</strong>:树、堆、图、哈希表。</li>
@@ -3425,23 +3425,23 @@
<p>非线性数据结构可以进一步被划分为树形结构和网状结构。</p>
<ul>
<li><strong>线性结构</strong>:数组、链表、队列、栈、哈希表,元素存在一对一的顺序关系。</li>
<li><strong>树形结构</strong>:树、堆、哈希表,元素存在一对多的关系。</li>
<li><strong>网状结构</strong>:图,元素存在多对多的关系。</li>
<li><strong>线性结构</strong>:数组、链表、队列、栈、哈希表,元素之间是一对一的顺序关系。</li>
<li><strong>树形结构</strong>:树、堆、哈希表,元素之间是一对多的关系。</li>
<li><strong>网状结构</strong>:图,元素之间是多对多的关系。</li>
</ul>
<h2 id="312">3.1.2. &nbsp; 物理结构:连续与离散<a class="headerlink" href="#312" title="Permanent link">&para;</a></h2>
<p>在计算机中,内存和硬盘是两种主要的存储硬件设备。硬盘主要用于长期存储数据,容量较大(通常可达到 TB 级别)、速度较慢。内存用于运行程序时暂存数据,速度较快,但容量较小(通常为 GB 级别)。</p>
<p><strong>在算法运行过程中,相关数据都存储在内存中</strong>。下图展示了一个计算机内存条,其中每个黑色方块都包含一块内存空间。我们可以将内存想象成一个巨大的 Excel 表格,其中每个单元格都可以存储 1 byte 的数据,在算法运行时,所有数据都被存储在这些单元格中。</p>
<p><strong>系统通过内存地址 Memory Location」来访问目标内存位置的数据</strong>。计算机根据特定规则为表格中的每个单元格分配编号,确保每个内存空间都有唯一的内存地址。有了这些地址,程序便可以访问内存中的数据。</p>
<p><strong>在算法运行过程中,相关数据都存储在内存中</strong>。下图展示了一个计算机内存条,其中每个黑色方块都包含一块内存空间。我们可以将内存想象成一个巨大的 Excel 表格,其中每个单元格都可以存储一定大小的数据,在算法运行时,所有数据都被存储在这些单元格中。</p>
<p><strong>系统通过内存地址来访问目标位置的数据</strong>。计算机根据特定规则为表格中的每个单元格分配编号,确保每个内存空间都有唯一的内存地址。有了这些地址,程序便可以访问内存中的数据。</p>
<p><img alt="内存条、内存空间、内存地址" src="../classification_of_data_structure.assets/computer_memory_location.png" /></p>
<p align="center"> Fig. 内存条、内存空间、内存地址 </p>
<p>内存是所有程序的共享资源,当内存被某个程序占用时,其他程序无法同时使用。<strong>因此在数据结构与算法的设计中,内存资源是一个重要的考虑因素</strong>如,算法所占用的内存峰值不应超过系统剩余空闲内存;如果运行的程序很多并且缺少大量连续的内存空间,那么所选用的数据结构必须能够存储在离散的内存空间内。</p>
<p><strong>「物理结构」反映了数据在计算机内存中的存储方式</strong>,可分为数组的连续空间存储和链表的离散空间存储。物理结构从底层决定了数据的访问、更新、增删等操作方法,同时在时间效率和空间效率方面呈现出互补的特点。</p>
<p>内存是所有程序的共享资源,当某块内存被某个程序占用时,则无法被其他程序同时使用<strong>因此在数据结构与算法的设计中,内存资源是一个重要的考虑因素</strong>如,算法所占用的内存峰值不应超过系统剩余空闲内存;如果缺少连续大块的内存空间,那么所选用的数据结构必须能够存储在离散的内存空间内。</p>
<p><strong>「物理结构」反映了数据在计算机内存中的存储方式</strong>,可分为连续空间存储(数组)和离散空间存储(链表)。物理结构从底层决定了数据的访问、更新、增删等操作方法,同时在时间效率和空间效率方面呈现出互补的特点。</p>
<p><img alt="连续空间存储与离散空间存储" src="../classification_of_data_structure.assets/classification_phisical_structure.png" /></p>
<p align="center"> Fig. 连续空间存储与离散空间存储 </p>
<p><strong>所有数据结构都是基于数组、链表或二者的组合实现的</strong>。例如,栈和队列既可以使用数组实现,也可以使用链表实现;而哈希表的实现可能同时包含数组和链表。</p>
<p>值得说明的是,<strong>所有数据结构都是基于数组、链表或二者的组合实现的</strong>。例如,栈和队列既可以使用数组实现,也可以使用链表实现;而哈希表的实现可能同时包含数组和链表。</p>
<ul>
<li><strong>基于数组可实现</strong>:栈、队列、哈希表、树、堆、图、矩阵、张量(维度 <span class="arithmatex">\(\geq 3\)</span> 的数组)等。</li>
<li><strong>基于链表可实现</strong>:栈、队列、哈希表、树、堆、图等。</li>
@@ -3449,7 +3449,7 @@
<p>基于数组实现的数据结构也被称为“静态数据结构”,这意味着此类数据结构在初始化后长度不可变。相对应地,基于链表实现的数据结构被称为“动态数据结构”,这类数据结构在初始化后,仍可以在程序运行过程中对其长度进行调整。</p>
<div class="admonition tip">
<p class="admonition-title">Tip</p>
<p>如若感觉理解物理结构有困难,建议先阅读下一章“数组与链表”,然后再回头理解物理结构的含义。数组与链表是其他所有数据结构的基石,建议你投入更多时间深入了解这两种基本数据结构</p>
<p>如若感觉理解物理结构有困难,建议先阅读下一章“数组与链表”,然后再回顾本节内容</p>
</div>
@@ -3414,10 +3414,11 @@
<h1 id="33">3.3. &nbsp; 数字编码 *<a class="headerlink" href="#33" title="Permanent link">&para;</a></h1>
<div class="admonition note">
<p class="admonition-title">Note</p>
<p>在本书中,标题带有的 * 符号的是选读章节。如果你时间有限或感到理解困难,建议先跳过,等学完必读章节后再单独攻克。</p>
<p>在本书中,标题带有的 * 符号的是选读章节。如果你时间有限或感到理解困难,可以先跳过,等学完必读章节后再单独攻克。</p>
</div>
<h2 id="331">3.3.1. &nbsp; 原码、反码和补码<a class="headerlink" href="#331" title="Permanent link">&para;</a></h2>
<p>从上一节的表格中我们发现,所有整数类型能够表示的负数都比正数多一个。例如,<code>byte</code> 的取值范围是 <span class="arithmatex">\([-128, 127]\)</span> 。这个现象比较反直觉,它的内在原因涉及到原码、反码、补码的相关知识。在展开分析之前,我们首先给出三者的定义:</p>
<p>从上一节的表格中我们发现,所有整数类型能够表示的负数都比正数多一个。例如,<code>byte</code> 的取值范围是 <span class="arithmatex">\([-128, 127]\)</span> 。这个现象比较反直觉,它的内在原因涉及到原码、反码、补码的相关知识。</p>
<p>在展开分析之前,我们首先给出三者的定义:</p>
<ul>
<li><strong>原码</strong>:我们将数字的二进制表示的最高位视为符号位,其中 <span class="arithmatex">\(0\)</span> 表示正数,<span class="arithmatex">\(1\)</span> 表示负数,其余位表示数字的值。</li>
<li><strong>反码</strong>:正数的反码与其原码相同,负数的反码是对其原码除符号位外的所有位取反。</li>
@@ -3426,7 +3427,7 @@
<p><img alt="原码、反码与补码之间的相互转换" src="../number_encoding.assets/1s_2s_complement.png" /></p>
<p align="center"> Fig. 原码、反码与补码之间的相互转换 </p>
<p>显然「原码」最为直观,<strong>然而数字是以「补码」的形式存储在计算机中的</strong>。这是因为原码存在一些局限性。</p>
<p>显然「原码」最为直观。但实际上<strong>数字是以「补码」的形式存储在计算机中的</strong>。这是因为原码存在一些局限性。</p>
<p>一方面,<strong>负数的原码不能直接用于运算</strong>。例如,我们在原码下计算 <span class="arithmatex">\(1 + (-2)\)</span> ,得到的结果是 <span class="arithmatex">\(-3\)</span> ,这显然是不对的。</p>
<div class="arithmatex">\[
\begin{aligned}
@@ -3436,25 +3437,25 @@
&amp; = -3
\end{aligned}
\]</div>
<p>为了解决此问题,计算机引入了「反码」。例如,我们先将原码转换为反码,并在反码下计算 <span class="arithmatex">\(1 + (-2)\)</span> 将结果从反码转化回原码,则可得到正确结果 <span class="arithmatex">\(-1\)</span></p>
<p>为了解决此问题,计算机引入了「反码」。如果我们先将原码转换为反码,并在反码下计算 <span class="arithmatex">\(1 + (-2)\)</span> 最后将结果从反码转化回原码,则可得到正确结果 <span class="arithmatex">\(-1\)</span></p>
<div class="arithmatex">\[
\begin{aligned}
&amp; 1 + (-2) \newline
&amp; = 0000 \space 0001 \space \text{(原码)} + 1000 \space 0010 \space \text{(原码)} \newline
&amp; \rightarrow 0000 \space 0001 \space \text{(原码)} + 1000 \space 0010 \space \text{(原码)} \newline
&amp; = 0000 \space 0001 \space \text{(反码)} + 1111 \space 1101 \space \text{(反码)} \newline
&amp; = 1111 \space 1110 \space \text{(反码)} \newline
&amp; = 1000 \space 0001 \space \text{(原码)} \newline
&amp; = -1
&amp; \rightarrow -1
\end{aligned}
\]</div>
<p>另一方面,<strong>数字零的原码有 <span class="arithmatex">\(+0\)</span><span class="arithmatex">\(-0\)</span> 两种表示方式</strong>。这意味着数字零对应着两个不同的二进制编码,而这可能会带来歧义问题。例如,在条件判断中,如果没有区分正零和负零,可能会导致错误的判断结果如果我们想要处理正零和负零歧义,则需要引入额外的判断操作,其可能会降低计算机的运算效率。</p>
<p>另一方面,<strong>数字零的原码有 <span class="arithmatex">\(+0\)</span><span class="arithmatex">\(-0\)</span> 两种表示方式</strong>。这意味着数字零对应着两个不同的二进制编码,可能会带来歧义。比如在条件判断中,如果没有区分正零和负零,可能会导致判断结果出错。而如果我们想要处理正零和负零歧义,则需要引入额外的判断操作,其可能会降低计算机的运算效率。</p>
<div class="arithmatex">\[
\begin{aligned}
+0 &amp; = 0000 \space 0000 \newline
-0 &amp; = 1000 \space 0000
\end{aligned}
\]</div>
<p>与原码一样,反码也存在正负零歧义问题。为此,计算机进一步引入了「补码」。那么,补码有什么作用呢?我们先来分析一下负零的补码的计算过程:</p>
<p>与原码一样,反码也存在正负零歧义问题,因此计算机进一步引入了「补码」。我们先来观察一下负零的原码、反码、补码的转换过程:</p>
<div class="arithmatex">\[
\begin{aligned}
-0 = \space &amp; 1000 \space 0000 \space \text{(原码)} \newline
@@ -3462,23 +3463,23 @@
= 1 \space &amp; 0000 \space 0000 \space \text{(补码)} \newline
\end{aligned}
\]</div>
<p>在负零的反码基础上加 <span class="arithmatex">\(1\)</span> 会产生进位,而由于 byte 的长度只有 8 位,因此溢出到第 9 位的 <span class="arithmatex">\(1\)</span> 会被舍弃。<strong>从而得到负零的补码为 <span class="arithmatex">\(0000 \space 0000\)</span> ,与正零的补码相同</strong>。这意味着在补码表示中只存在一个零,从而解决了正负零歧义问题</p>
<p>还剩余最后一个疑惑:byte 的取值范围是 <span class="arithmatex">\([-128, 127]\)</span> ,多出来的一个负数 <span class="arithmatex">\(-128\)</span> 是如何得到的呢?我们注意到,区间 <span class="arithmatex">\([-127, +127]\)</span> 内的所有整数都有对应的原码、反码和补码,并且原码和补码之间是可以互相转换的。</p>
<p>在负零的反码基础上加 <span class="arithmatex">\(1\)</span> 会产生进位,<code>byte</code> 类型的长度只有 8 位,因此溢出到第 9 位的 <span class="arithmatex">\(1\)</span> 会被舍弃。也就是说,<strong>负零的补码为 <span class="arithmatex">\(0000 \space 0000\)</span> ,与正零的补码相同</strong>。这意味着在补码表示中只存在一个零,正负零歧义从而得到解决</p>
<p>还剩余最后一个疑惑:<code>byte</code> 类型的取值范围是 <span class="arithmatex">\([-128, 127]\)</span> ,多出来的一个负数 <span class="arithmatex">\(-128\)</span> 是如何得到的呢?我们注意到,区间 <span class="arithmatex">\([-127, +127]\)</span> 内的所有整数都有对应的原码、反码和补码,并且原码和补码之间是可以互相转换的。</p>
<p>然而,<strong>补码 <span class="arithmatex">\(1000 \space 0000\)</span> 是一个例外,它并没有对应的原码</strong>。根据转换方法,我们得到该补码的原码为 <span class="arithmatex">\(0000 \space 0000\)</span> 。这显然是矛盾的,因为该原码表示数字 <span class="arithmatex">\(0\)</span> ,它的补码应该是自身。计算机规定这个特殊的补码 <span class="arithmatex">\(1000 \space 0000\)</span> 代表 <span class="arithmatex">\(-128\)</span> 。实际上,<span class="arithmatex">\((-1) + (-127)\)</span> 在补码下的计算结果就是 <span class="arithmatex">\(-128\)</span></p>
<div class="arithmatex">\[
\begin{aligned}
&amp; (-127) + (-1) \newline
&amp; = 1111 \space 1111 \space \text{(原码)} + 1000 \space 0001 \space \text{(原码)} \newline
&amp; \rightarrow 1111 \space 1111 \space \text{(原码)} + 1000 \space 0001 \space \text{(原码)} \newline
&amp; = 1000 \space 0000 \space \text{(反码)} + 1111 \space 1110 \space \text{(反码)} \newline
&amp; = 1000 \space 0001 \space \text{(补码)} + 1111 \space 1111 \space \text{(补码)} \newline
&amp; = 1000 \space 0000 \space \text{(补码)} \newline
&amp; = -128
&amp; \rightarrow -128
\end{aligned}
\]</div>
<p>你可能已经发现,上述的所有计算都是加法运算。这暗示着一个重要事实:<strong>计算机内部的硬件电路主要是基于加法运算设计的</strong>。这是因为加法运算相对于其他运算(比如乘法、除法和减法)来说,硬件实现起来更简单,更容易进行并行化处理,从而提高运算速度。</p>
<p>然而,这并不意味着计算机只能做加法。<strong>通过将加法与一些基本逻辑运算结合,计算机能够实现各种其他的数学运算</strong>。例如,计算减法 <span class="arithmatex">\(a - b\)</span> 可以转换为计算加法 <span class="arithmatex">\(a + (-b)\)</span> ;计算乘法和除法可以转换为计算多次加法或减法。</p>
<p>现在我们可以总结出计算机使用补码的原因:基于补码表示,计算机可以用同样的电路和操作来处理正数和负数的加法,不需要设计特殊的硬件电路来处理减法,并且无需特别处理正负零的歧义问题。这大大简化了硬件设计,提高了运算效率。</p>
<p>补码的设计非常精妙,由于篇幅关系我们先介绍到这里建议有兴趣的读者进一步深度了解。</p>
<p>你可能已经发现,上述的所有计算都是加法运算。这暗示着一个重要事实:<strong>计算机内部的硬件电路主要是基于加法运算设计的</strong>。这是因为加法运算相对于其他运算(比如乘法、除法和减法)来说,硬件实现起来更简单,更容易进行并行化处理,运算速度更快</p>
<p>请注意,这并不意味着计算机只能做加法。<strong>通过将加法与一些基本逻辑运算结合,计算机能够实现各种其他的数学运算</strong>。例如,计算减法 <span class="arithmatex">\(a - b\)</span> 可以转换为计算加法 <span class="arithmatex">\(a + (-b)\)</span> ;计算乘法和除法可以转换为计算多次加法或减法。</p>
<p>现在我们可以总结出计算机使用补码的原因:基于补码表示,计算机可以用同样的电路和操作来处理正数和负数的加法,不需要设计特殊的硬件电路来处理减法,并且无需特别处理正负零的歧义问题。这大大简化了硬件设计,提高了运算效率。</p>
<p>补码的设计非常精妙,篇幅关系我们先介绍到这里建议有兴趣的读者进一步深度了解。</p>
<h2 id="332">3.3.2. &nbsp; 浮点数编码<a class="headerlink" href="#332" title="Permanent link">&para;</a></h2>
<p>细心的你可能会发现:<code>int</code><code>float</code> 长度相同,都是 4 bytes,但为什么 <code>float</code> 的取值范围远大于 <code>int</code> ?这非常反直觉,因为按理说 <code>float</code> 需要表示小数,取值范围应该变小才对。</p>
<p>实际上,<strong>这是因为浮点数 <code>float</code> 采用了不同的表示方式</strong>。记一个 32-bit 长度的二进制数为:</p>
@@ -3509,7 +3510,7 @@ b_{31} b_{30} b_{29} \ldots b_2 b_1 b_0
<p><img alt="IEEE 754 标准下的 float 表示方式" src="../number_encoding.assets/ieee_754_float.png" /></p>
<p align="center"> Fig. IEEE 754 标准下的 float 表示方式 </p>
<p>以上图为例,<span class="arithmatex">\(\mathrm{S} = 0\)</span> <span class="arithmatex">\(\mathrm{E} = 124\)</span> <span class="arithmatex">\(\mathrm{N} = 2^{-2} + 2^{-3} = 0.375\)</span> 易得</p>
<p>给定一个示例数据 <span class="arithmatex">\(\mathrm{S} = 0\)</span> <span class="arithmatex">\(\mathrm{E} = 124\)</span> <span class="arithmatex">\(\mathrm{N} = 2^{-2} + 2^{-3} = 0.375\)</span> 则有:</p>
<div class="arithmatex">\[
\text { val } = (-1)^0 \times 2^{124 - 127} \times (1 + 0.375) = 0.171875
\]</div>
+4 -8
View File
@@ -3398,16 +3398,12 @@
<h1 id="35">3.5. &nbsp; 小结<a class="headerlink" href="#35" title="Permanent link">&para;</a></h1>
<p><strong>数据结构分类</strong></p>
<ul>
<li>数据结构可以从逻辑结构和物理结构两个角度进行分类。逻辑结构描述了数据元素之间的逻辑关系,而物理结构描述了数据在计算机内存中的存储方式。</li>
<li>常见的逻辑结构包括线性、树状和网状等。通常我们根据逻辑结构将数据结构分为线性(数组、链表、栈、队列)和非线性(树、图、堆)两种。哈希表的实现可能同时包含线性和非线性结构。</li>
<li>当程序运行时,数据被存储在计算机内存中。每个内存空间都拥有对应的内存地址,程序通过这些内存地址访问数据。</li>
<li>物理结构主要分为连续空间存储(数组)和离散空间存储(链表)。所有数据结构都是由数组、链表或两者的组合实现的。</li>
</ul>
<p><strong>数据类型与编码</strong></p>
<ul>
<li>计算机中的基本数据类型包括整数 byte, short, int, long 、浮点数 float, double 、字符 char 和布尔 boolean 。它们的取值范围取决于占用空间大小和表示方式。</li>
<li>计算机中的基本数据类型包括整数 <code>byte</code> , <code>short</code> , <code>int</code> , <code>long</code> 、浮点数 <code>float</code> , <code>double</code> 、字符 <code>char</code> 和布尔 <code>boolean</code> 。它们的取值范围取决于占用空间大小和表示方式。</li>
<li>原码、反码和补码是在计算机中编码数字的三种方法,它们之间是可以相互转换的。整数的原码的最高位是符号位,其余位是数字的值。</li>
<li>整数在计算机中是以补码的形式存储的。在补码表示下,计算机可以对正数和负数的加法一视同仁,不需要为减法操作单独设计特殊的硬件电路,并且不存在正负零歧义的问题。</li>
<li>浮点数的编码由 1 位符号位、8 位指数位和 23 位分数位构成。由于存在指数位,浮点数的取值范围远大于整数,代价是牺牲了精度。</li>
@@ -3417,11 +3413,11 @@
<h2 id="351-q-a">3.5.1. &nbsp; Q &amp; A<a class="headerlink" href="#351-q-a" title="Permanent link">&para;</a></h2>
<div class="admonition question">
<p class="admonition-title">为什么哈希表同时包含线性数据结构和非线性数据结构?</p>
<p>哈希表底层是数组,而为了解决哈希冲突,我们可能会使用“拉链法”(后续散列表章节会讲)。在拉链法中,数组中每个地址(桶)指向一个链表;当这个链表长度超过一定阈值时,又可能被转化为树(通常为红黑树)。因此,哈希表可能同时包含线性(数组、链表)和非线性(树)数据结构。</p>
<p>哈希表底层是数组,而为了解决哈希冲突,我们可能会使用“链式地址”(后续散列表章节会讲)。在拉链法中,数组中每个地址(桶)指向一个链表;当这个链表长度超过一定阈值时,又可能被转化为树(通常为红黑树)。因此,哈希表可能同时包含线性(数组、链表)和非线性(树)数据结构。</p>
</div>
<div class="admonition question">
<p class="admonition-title">char 类型的长度是 1 byte 吗?</p>
<p>char 类型的长度由编程语言采用的编码方法决定。例如,Java, JS, TS, C# 都采用 UTF-16 编码(保存 Unicode 码点),因此 char 类型的长度为 2 bytes 。</p>
<p class="admonition-title"><code>char</code> 类型的长度是 1 byte 吗?</p>
<p><code>char</code> 类型的长度由编程语言采用的编码方法决定。例如,Java, JS, TS, C# 都采用 UTF-16 编码(保存 Unicode 码点),因此 char 类型的长度为 2 bytes 。</p>
</div>