mirror of
https://github.com/krahets/hello-algo.git
synced 2025-07-29 13:23:09 +08:00
deploy
This commit is contained in:
@ -3527,69 +3527,47 @@
|
|||||||
<li>由于数组具有更高的缓存命中率,因此它通常比链表更高效。在选择数据结构时,应根据具体需求和场景做出恰当选择。</li>
|
<li>由于数组具有更高的缓存命中率,因此它通常比链表更高效。在选择数据结构时,应根据具体需求和场景做出恰当选择。</li>
|
||||||
</ul>
|
</ul>
|
||||||
<h3 id="2-q-a">2. Q & A<a class="headerlink" href="#2-q-a" title="Permanent link">¶</a></h3>
|
<h3 id="2-q-a">2. Q & A<a class="headerlink" href="#2-q-a" title="Permanent link">¶</a></h3>
|
||||||
<div class="admonition question">
|
<p><strong>Q</strong>:数组存储在栈上和存储在堆上,对时间效率和空间效率是否有影响?</p>
|
||||||
<p class="admonition-title">数组存储在栈上和存储在堆上,对时间效率和空间效率是否有影响?</p>
|
|
||||||
<p>存储在栈上和堆上的数组都被存储在连续内存空间内,数据操作效率基本一致。然而,栈和堆具有各自的特点,从而导致以下不同点。</p>
|
<p>存储在栈上和堆上的数组都被存储在连续内存空间内,数据操作效率基本一致。然而,栈和堆具有各自的特点,从而导致以下不同点。</p>
|
||||||
<ol>
|
<ol>
|
||||||
<li>分配和释放效率:栈是一块较小的内存,分配由编译器自动完成;而堆内存相对更大,可以在代码中动态分配,更容易碎片化。因此,堆上的分配和释放操作通常比栈上的慢。</li>
|
<li>分配和释放效率:栈是一块较小的内存,分配由编译器自动完成;而堆内存相对更大,可以在代码中动态分配,更容易碎片化。因此,堆上的分配和释放操作通常比栈上的慢。</li>
|
||||||
<li>大小限制:栈内存相对较小,堆的大小一般受限于可用内存。因此堆更加适合存储大型数组。</li>
|
<li>大小限制:栈内存相对较小,堆的大小一般受限于可用内存。因此堆更加适合存储大型数组。</li>
|
||||||
<li>灵活性:栈上的数组的大小需要在编译时确定,而堆上的数组的大小可以在运行时动态确定。</li>
|
<li>灵活性:栈上的数组的大小需要在编译时确定,而堆上的数组的大小可以在运行时动态确定。</li>
|
||||||
</ol>
|
</ol>
|
||||||
</div>
|
<p><strong>Q</strong>:为什么数组要求相同类型的元素,而在链表中却没有强调同类型呢?</p>
|
||||||
<div class="admonition question">
|
|
||||||
<p class="admonition-title">为什么数组要求相同类型的元素,而在链表中却没有强调同类型呢?</p>
|
|
||||||
<p>链表由节点组成,节点之间通过引用(指针)连接,各个节点可以存储不同类型的数据,例如 <code>int</code>、<code>double</code>、<code>string</code>、<code>object</code> 等。</p>
|
<p>链表由节点组成,节点之间通过引用(指针)连接,各个节点可以存储不同类型的数据,例如 <code>int</code>、<code>double</code>、<code>string</code>、<code>object</code> 等。</p>
|
||||||
<p>相对地,数组元素则必须是相同类型的,这样才能通过计算偏移量来获取对应元素位置。例如,数组同时包含 <code>int</code> 和 <code>long</code> 两种类型,单个元素分别占用 4 字节 和 8 字节 ,此时就不能用以下公式计算偏移量了,因为数组中包含了两种“元素长度”。</p>
|
<p>相对地,数组元素则必须是相同类型的,这样才能通过计算偏移量来获取对应元素位置。例如,数组同时包含 <code>int</code> 和 <code>long</code> 两种类型,单个元素分别占用 4 字节 和 8 字节 ,此时就不能用以下公式计算偏移量了,因为数组中包含了两种“元素长度”。</p>
|
||||||
<div class="highlight"><pre><span></span><code><a id="__codelineno-0-1" name="__codelineno-0-1" href="#__codelineno-0-1"></a><span class="c1"># 元素内存地址 = 数组内存地址 + 元素长度 * 元素索引</span>
|
<div class="highlight"><pre><span></span><code><a id="__codelineno-0-1" name="__codelineno-0-1" href="#__codelineno-0-1"></a><span class="c1"># 元素内存地址 = 数组内存地址 + 元素长度 * 元素索引</span>
|
||||||
</code></pre></div>
|
</code></pre></div>
|
||||||
</div>
|
<p><strong>Q</strong>:删除节点后,是否需要把 <code>P.next</code> 设为 <code>None</code> 呢?</p>
|
||||||
<div class="admonition question">
|
|
||||||
<p class="admonition-title">删除节点后,是否需要把 <code>P.next</code> 设为 <code>None</code> 呢?</p>
|
|
||||||
<p>不修改 <code>P.next</code> 也可以。从该链表的角度看,从头节点遍历到尾节点已经不会遇到 <code>P</code> 了。这意味着节点 <code>P</code> 已经从链表中删除了,此时节点 <code>P</code> 指向哪里都不会对该链表产生影响。</p>
|
<p>不修改 <code>P.next</code> 也可以。从该链表的角度看,从头节点遍历到尾节点已经不会遇到 <code>P</code> 了。这意味着节点 <code>P</code> 已经从链表中删除了,此时节点 <code>P</code> 指向哪里都不会对该链表产生影响。</p>
|
||||||
<p>从垃圾回收的角度看,对于 Java、Python、Go 等拥有自动垃圾回收机制的语言来说,节点 <code>P</code> 是否被回收取决于是否仍存在指向它的引用,而不是 <code>P.next</code> 的值。在 C 和 C++ 等语言中,我们需要手动释放节点内存。</p>
|
<p>从垃圾回收的角度看,对于 Java、Python、Go 等拥有自动垃圾回收机制的语言来说,节点 <code>P</code> 是否被回收取决于是否仍存在指向它的引用,而不是 <code>P.next</code> 的值。在 C 和 C++ 等语言中,我们需要手动释放节点内存。</p>
|
||||||
</div>
|
<p><strong>Q</strong>:在链表中插入和删除操作的时间复杂度是 <span class="arithmatex">\(O(1)\)</span> 。但是增删之前都需要 <span class="arithmatex">\(O(n)\)</span> 的时间查找元素,那为什么时间复杂度不是 <span class="arithmatex">\(O(n)\)</span> 呢?</p>
|
||||||
<div class="admonition question">
|
|
||||||
<p class="admonition-title">在链表中插入和删除操作的时间复杂度是 <span class="arithmatex">\(O(1)\)</span> 。但是增删之前都需要 <span class="arithmatex">\(O(n)\)</span> 的时间查找元素,那为什么时间复杂度不是 <span class="arithmatex">\(O(n)\)</span> 呢?</p>
|
|
||||||
<p>如果是先查找元素、再删除元素,时间复杂度确实是 <span class="arithmatex">\(O(n)\)</span> 。然而,链表的 <span class="arithmatex">\(O(1)\)</span> 增删的优势可以在其他应用上得到体现。例如,双向队列适合使用链表实现,我们维护一个指针变量始终指向头节点、尾节点,每次插入与删除操作都是 <span class="arithmatex">\(O(1)\)</span> 。</p>
|
<p>如果是先查找元素、再删除元素,时间复杂度确实是 <span class="arithmatex">\(O(n)\)</span> 。然而,链表的 <span class="arithmatex">\(O(1)\)</span> 增删的优势可以在其他应用上得到体现。例如,双向队列适合使用链表实现,我们维护一个指针变量始终指向头节点、尾节点,每次插入与删除操作都是 <span class="arithmatex">\(O(1)\)</span> 。</p>
|
||||||
</div>
|
<p><strong>Q</strong>:图“链表定义与存储方式”中,浅蓝色的存储节点指针是占用一块内存地址吗?还是和节点值各占一半呢?</p>
|
||||||
<div class="admonition question">
|
|
||||||
<p class="admonition-title">图“链表定义与存储方式”中,浅蓝色的存储节点指针是占用一块内存地址吗?还是和节点值各占一半呢?</p>
|
|
||||||
<p>该示意图只是定性表示,定量表示需要根据具体情况进行分析。</p>
|
<p>该示意图只是定性表示,定量表示需要根据具体情况进行分析。</p>
|
||||||
<ul>
|
<ul>
|
||||||
<li>不同类型的节点值占用的空间是不同的,比如 <code>int</code>、<code>long</code>、<code>double</code> 和实例对象等。</li>
|
<li>不同类型的节点值占用的空间是不同的,比如 <code>int</code>、<code>long</code>、<code>double</code> 和实例对象等。</li>
|
||||||
<li>指针变量占用的内存空间大小根据所使用的操作系统及编译环境而定,大多为 8 字节或 4 字节。</li>
|
<li>指针变量占用的内存空间大小根据所使用的操作系统及编译环境而定,大多为 8 字节或 4 字节。</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
<p><strong>Q</strong>:在列表末尾添加元素是否时时刻刻都为 <span class="arithmatex">\(O(1)\)</span> ?</p>
|
||||||
<div class="admonition question">
|
|
||||||
<p class="admonition-title">在列表末尾添加元素是否时时刻刻都为 <span class="arithmatex">\(O(1)\)</span> ?</p>
|
|
||||||
<p>如果添加元素时超出列表长度,则需要先扩容列表再添加。系统会申请一块新的内存,并将原列表的所有元素搬运过去,这时候时间复杂度就会是 <span class="arithmatex">\(O(n)\)</span> 。</p>
|
<p>如果添加元素时超出列表长度,则需要先扩容列表再添加。系统会申请一块新的内存,并将原列表的所有元素搬运过去,这时候时间复杂度就会是 <span class="arithmatex">\(O(n)\)</span> 。</p>
|
||||||
</div>
|
<p><strong>Q</strong>:“列表的出现极大地提高了数组的实用性,但可能导致部分内存空间浪费”,这里的空间浪费是指额外增加的变量如容量、长度、扩容倍数所占的内存吗?</p>
|
||||||
<div class="admonition question">
|
|
||||||
<p class="admonition-title">“列表的出现极大地提高了数组的实用性,但可能导致部分内存空间浪费”,这里的空间浪费是指额外增加的变量如容量、长度、扩容倍数所占的内存吗?</p>
|
|
||||||
<p>这里的空间浪费主要有两方面含义:一方面,列表都会设定一个初始长度,我们不一定需要用这么多;另一方面,为了防止频繁扩容,扩容一般会乘以一个系数,比如 <span class="arithmatex">\(\times 1.5\)</span> 。这样一来,也会出现很多空位,我们通常不能完全填满它们。</p>
|
<p>这里的空间浪费主要有两方面含义:一方面,列表都会设定一个初始长度,我们不一定需要用这么多;另一方面,为了防止频繁扩容,扩容一般会乘以一个系数,比如 <span class="arithmatex">\(\times 1.5\)</span> 。这样一来,也会出现很多空位,我们通常不能完全填满它们。</p>
|
||||||
</div>
|
<p><strong>Q</strong>:在 Python 中初始化 <code>n = [1, 2, 3]</code> 后,这 3 个元素的地址是相连的,但是初始化 <code>m = [2, 1, 3]</code> 会发现它们每个元素的 id 并不是连续的,而是分别跟 <code>n</code> 中的相同。这些元素的地址不连续,那么 <code>m</code> 还是数组吗?</p>
|
||||||
<div class="admonition question">
|
|
||||||
<p class="admonition-title">在 Python 中初始化 <code>n = [1, 2, 3]</code> 后,这 3 个元素的地址是相连的,但是初始化 <code>m = [2, 1, 3]</code> 会发现它们每个元素的 id 并不是连续的,而是分别跟 <code>n</code> 中的相同。这些元素的地址不连续,那么 <code>m</code> 还是数组吗?</p>
|
|
||||||
<p>假如把列表元素换成链表节点 <code>n = [n1, n2, n3, n4, n5]</code> ,通常情况下这 5 个节点对象也分散存储在内存各处。然而,给定一个列表索引,我们仍然可以在 <span class="arithmatex">\(O(1)\)</span> 时间内获取节点内存地址,从而访问到对应的节点。这是因为数组中存储的是节点的引用,而非节点本身。</p>
|
<p>假如把列表元素换成链表节点 <code>n = [n1, n2, n3, n4, n5]</code> ,通常情况下这 5 个节点对象也分散存储在内存各处。然而,给定一个列表索引,我们仍然可以在 <span class="arithmatex">\(O(1)\)</span> 时间内获取节点内存地址,从而访问到对应的节点。这是因为数组中存储的是节点的引用,而非节点本身。</p>
|
||||||
<p>与许多语言不同,Python 中的数字也被包装为对象,列表中存储的不是数字本身,而是对数字的引用。因此,我们会发现两个数组中的相同数字拥有同一个 id ,并且这些数字的内存地址无须连续。</p>
|
<p>与许多语言不同,Python 中的数字也被包装为对象,列表中存储的不是数字本身,而是对数字的引用。因此,我们会发现两个数组中的相同数字拥有同一个 id ,并且这些数字的内存地址无须连续。</p>
|
||||||
</div>
|
<p><strong>Q</strong>:C++ STL 里面的 <code>std::list</code> 已经实现了双向链表,但好像一些算法书上不怎么直接使用它,是不是因为有什么局限性呢?</p>
|
||||||
<div class="admonition question">
|
|
||||||
<p class="admonition-title">C++ STL 里面的 <code>std::list</code> 已经实现了双向链表,但好像一些算法书上不怎么直接使用它,是不是因为有什么局限性呢?</p>
|
|
||||||
<p>一方面,我们往往更青睐使用数组实现算法,而只在必要时才使用链表,主要有两个原因。</p>
|
<p>一方面,我们往往更青睐使用数组实现算法,而只在必要时才使用链表,主要有两个原因。</p>
|
||||||
<ul>
|
<ul>
|
||||||
<li>空间开销:由于每个元素需要两个额外的指针(一个用于前一个元素,一个用于后一个元素),所以 <code>std::list</code> 通常比 <code>std::vector</code> 更占用空间。</li>
|
<li>空间开销:由于每个元素需要两个额外的指针(一个用于前一个元素,一个用于后一个元素),所以 <code>std::list</code> 通常比 <code>std::vector</code> 更占用空间。</li>
|
||||||
<li>缓存不友好:由于数据不是连续存放的,因此 <code>std::list</code> 对缓存的利用率较低。一般情况下,<code>std::vector</code> 的性能会更好。</li>
|
<li>缓存不友好:由于数据不是连续存放的,因此 <code>std::list</code> 对缓存的利用率较低。一般情况下,<code>std::vector</code> 的性能会更好。</li>
|
||||||
</ul>
|
</ul>
|
||||||
<p>另一方面,必要使用链表的情况主要是二叉树和图。栈和队列往往会使用编程语言提供的 <code>stack</code> 和 <code>queue</code> ,而非链表。</p>
|
<p>另一方面,必要使用链表的情况主要是二叉树和图。栈和队列往往会使用编程语言提供的 <code>stack</code> 和 <code>queue</code> ,而非链表。</p>
|
||||||
</div>
|
<p><strong>Q</strong>:初始化列表 <code>res = [0] * self.size()</code> 操作,会导致 <code>res</code> 的每个元素引用相同的地址吗?</p>
|
||||||
<div class="admonition question">
|
|
||||||
<p class="admonition-title">初始化列表 <code>res = [0] * self.size()</code> 操作,会导致 <code>res</code> 的每个元素引用相同的地址吗?</p>
|
|
||||||
<p>不会。但二维数组会有这个问题,例如初始化二维列表 <code>res = [[0] * self.size()]</code> ,则多次引用了同一个列表 <code>[0]</code> 。</p>
|
<p>不会。但二维数组会有这个问题,例如初始化二维列表 <code>res = [[0] * self.size()]</code> ,则多次引用了同一个列表 <code>[0]</code> 。</p>
|
||||||
</div>
|
<p><strong>Q</strong>:在删除节点中,需要断开该节点与其后继节点之间的引用指向吗?</p>
|
||||||
<div class="admonition question">
|
|
||||||
<p class="admonition-title">在删除节点中,需要断开该节点与其后继节点之间的引用指向吗?</p>
|
|
||||||
<p>从数据结构与算法(做题)的角度看,不断开没有关系,只要保证程序的逻辑是正确的就行。从标准库的角度看,断开更加安全、逻辑更加清晰。如果不断开,假设被删除节点未被正常回收,那么它会影响后继节点的内存回收。</p>
|
<p>从数据结构与算法(做题)的角度看,不断开没有关系,只要保证程序的逻辑是正确的就行。从标准库的角度看,断开更加安全、逻辑更加清晰。如果不断开,假设被删除节点未被正常回收,那么它会影响后继节点的内存回收。</p>
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Source file information -->
|
<!-- Source file information -->
|
||||||
|
|
||||||
|
@ -3529,14 +3529,12 @@
|
|||||||
<li>列约束和对角线约束的处理方式类似。对于列约束,我们利用一个数组来记录每一列是否有皇后,从而指示选中的格子是否合法。对于对角线约束,我们借助两个数组来分别记录该主、次对角线上是否存在皇后;难点在于找处在到同一主(副)对角线上格子满足的行列索引规律。</li>
|
<li>列约束和对角线约束的处理方式类似。对于列约束,我们利用一个数组来记录每一列是否有皇后,从而指示选中的格子是否合法。对于对角线约束,我们借助两个数组来分别记录该主、次对角线上是否存在皇后;难点在于找处在到同一主(副)对角线上格子满足的行列索引规律。</li>
|
||||||
</ul>
|
</ul>
|
||||||
<h3 id="2-q-a">2. Q & A<a class="headerlink" href="#2-q-a" title="Permanent link">¶</a></h3>
|
<h3 id="2-q-a">2. Q & A<a class="headerlink" href="#2-q-a" title="Permanent link">¶</a></h3>
|
||||||
<div class="admonition question">
|
<p><strong>Q</strong>:怎么理解回溯和递归的关系?</p>
|
||||||
<p class="admonition-title">怎么理解回溯和递归的关系?</p>
|
|
||||||
<p>总的来看,回溯是一种“算法策略”,而递归更像是一个“工具”。</p>
|
<p>总的来看,回溯是一种“算法策略”,而递归更像是一个“工具”。</p>
|
||||||
<ul>
|
<ul>
|
||||||
<li>回溯算法通常基于递归实现。然而,回溯是递归的应用场景之一,是递归在搜索问题中的应用。</li>
|
<li>回溯算法通常基于递归实现。然而,回溯是递归的应用场景之一,是递归在搜索问题中的应用。</li>
|
||||||
<li>递归的结构体现了“子问题分解”的解题范式,常用于解决分治、回溯、动态规划(记忆化递归)等问题。</li>
|
<li>递归的结构体现了“子问题分解”的解题范式,常用于解决分治、回溯、动态规划(记忆化递归)等问题。</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Source file information -->
|
<!-- Source file information -->
|
||||||
|
|
||||||
|
@ -3539,12 +3539,9 @@
|
|||||||
<li>常见空间复杂度从低到高排列有 <span class="arithmatex">\(O(1)\)</span>、<span class="arithmatex">\(O(\log n)\)</span>、<span class="arithmatex">\(O(n)\)</span>、<span class="arithmatex">\(O(n^2)\)</span> 和 <span class="arithmatex">\(O(2^n)\)</span> 等。</li>
|
<li>常见空间复杂度从低到高排列有 <span class="arithmatex">\(O(1)\)</span>、<span class="arithmatex">\(O(\log n)\)</span>、<span class="arithmatex">\(O(n)\)</span>、<span class="arithmatex">\(O(n^2)\)</span> 和 <span class="arithmatex">\(O(2^n)\)</span> 等。</li>
|
||||||
</ul>
|
</ul>
|
||||||
<h3 id="2-q-a">2. Q & A<a class="headerlink" href="#2-q-a" title="Permanent link">¶</a></h3>
|
<h3 id="2-q-a">2. Q & A<a class="headerlink" href="#2-q-a" title="Permanent link">¶</a></h3>
|
||||||
<div class="admonition question">
|
<p><strong>Q</strong>:尾递归的空间复杂度是 <span class="arithmatex">\(O(1)\)</span> 吗?</p>
|
||||||
<p class="admonition-title">尾递归的空间复杂度是 <span class="arithmatex">\(O(1)\)</span> 吗?</p>
|
|
||||||
<p>理论上,尾递归函数的空间复杂度可以优化至 <span class="arithmatex">\(O(1)\)</span> 。不过绝大多数编程语言(例如 Java、Python、C++、Go、C# 等)不支持自动优化尾递归,因此通常认为空间复杂度是 <span class="arithmatex">\(O(n)\)</span> 。</p>
|
<p>理论上,尾递归函数的空间复杂度可以优化至 <span class="arithmatex">\(O(1)\)</span> 。不过绝大多数编程语言(例如 Java、Python、C++、Go、C# 等)不支持自动优化尾递归,因此通常认为空间复杂度是 <span class="arithmatex">\(O(n)\)</span> 。</p>
|
||||||
</div>
|
<p><strong>Q</strong>:函数和方法这两个术语的区别是什么?</p>
|
||||||
<div class="admonition question">
|
|
||||||
<p class="admonition-title">函数和方法这两个术语的区别是什么?</p>
|
|
||||||
<p>「函数 function」可以被独立执行,所有参数都以显式传递。「方法 method」与一个对象关联,被隐式传递给调用它的对象,能够对类的实例中包含的数据进行操作。</p>
|
<p>「函数 function」可以被独立执行,所有参数都以显式传递。「方法 method」与一个对象关联,被隐式传递给调用它的对象,能够对类的实例中包含的数据进行操作。</p>
|
||||||
<p>下面以几种常见的编程语言为例来说明。</p>
|
<p>下面以几种常见的编程语言为例来说明。</p>
|
||||||
<ul>
|
<ul>
|
||||||
@ -3552,13 +3549,10 @@
|
|||||||
<li>Java 和 C# 是面向对象的编程语言,代码块(方法)通常作为某个类的一部分。静态方法的行为类似于函数,因为它被绑定在类上,不能访问特定的实例变量。</li>
|
<li>Java 和 C# 是面向对象的编程语言,代码块(方法)通常作为某个类的一部分。静态方法的行为类似于函数,因为它被绑定在类上,不能访问特定的实例变量。</li>
|
||||||
<li>C++ 和 Python 既支持过程式编程(函数),也支持面向对象编程(方法)。</li>
|
<li>C++ 和 Python 既支持过程式编程(函数),也支持面向对象编程(方法)。</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
<p><strong>Q</strong>:图解“常见的空间复杂度类型”反映的是否是占用空间的绝对大小?</p>
|
||||||
<div class="admonition question">
|
|
||||||
<p class="admonition-title">图解“常见的空间复杂度类型”反映的是否是占用空间的绝对大小?</p>
|
|
||||||
<p>不是,该图展示的是空间复杂度,其反映的是增长趋势,而不是占用空间的绝对大小。</p>
|
<p>不是,该图展示的是空间复杂度,其反映的是增长趋势,而不是占用空间的绝对大小。</p>
|
||||||
<p>假设取 <span class="arithmatex">\(n = 8\)</span> ,你可能会发现每条曲线的值与函数对应不上。这是因为每条曲线都包含一个常数项,用于将取值范围压缩到一个视觉舒适的范围内。</p>
|
<p>假设取 <span class="arithmatex">\(n = 8\)</span> ,你可能会发现每条曲线的值与函数对应不上。这是因为每条曲线都包含一个常数项,用于将取值范围压缩到一个视觉舒适的范围内。</p>
|
||||||
<p>在实际中,因为我们通常不知道每个方法的“常数项”复杂度是多少,所以一般无法仅凭复杂度来选择 <span class="arithmatex">\(n = 8\)</span> 之下的最优解法。但对于 <span class="arithmatex">\(n = 8^5\)</span> 就很好选了,这时增长趋势已经占主导了。</p>
|
<p>在实际中,因为我们通常不知道每个方法的“常数项”复杂度是多少,所以一般无法仅凭复杂度来选择 <span class="arithmatex">\(n = 8\)</span> 之下的最优解法。但对于 <span class="arithmatex">\(n = 8^5\)</span> 就很好选了,这时增长趋势已经占主导了。</p>
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Source file information -->
|
<!-- Source file information -->
|
||||||
|
|
||||||
|
@ -3529,23 +3529,15 @@
|
|||||||
<li>UTF-8 是最受欢迎的 Unicode 编码方法,通用性非常好。它是一种变长的编码方法,具有很好的扩展性,有效提升了存储空间的使用效率。UTF-16 和 UTF-32 是等长的编码方法。在编码中文时,UTF-16 占用的空间比 UTF-8 更小。Java 和 C# 等编程语言默认使用 UTF-16 编码。</li>
|
<li>UTF-8 是最受欢迎的 Unicode 编码方法,通用性非常好。它是一种变长的编码方法,具有很好的扩展性,有效提升了存储空间的使用效率。UTF-16 和 UTF-32 是等长的编码方法。在编码中文时,UTF-16 占用的空间比 UTF-8 更小。Java 和 C# 等编程语言默认使用 UTF-16 编码。</li>
|
||||||
</ul>
|
</ul>
|
||||||
<h3 id="2-q-a">2. Q & A<a class="headerlink" href="#2-q-a" title="Permanent link">¶</a></h3>
|
<h3 id="2-q-a">2. Q & A<a class="headerlink" href="#2-q-a" title="Permanent link">¶</a></h3>
|
||||||
<div class="admonition question">
|
<p><strong>Q</strong>:为什么哈希表同时包含线性数据结构和非线性数据结构?</p>
|
||||||
<p class="admonition-title">为什么哈希表同时包含线性数据结构和非线性数据结构?</p>
|
<p>哈希表底层是数组,而为了解决哈希冲突,我们可能会使用“链式地址”(后续“哈希冲突”章节会讲):数组中每个桶指向一个链表,当链表长度超过一定阈值时,又可能被转化为树(通常为红黑树)。</p>
|
||||||
<p>哈希表底层是数组,而为了解决哈希冲突,我们可能会使用“链式地址”(后续“哈希冲突”章节会讲):数组中每个桶指向一个链表,当链表长度超过一定阈值时,又可能被转化为树(通常为红黑树)。
|
<p>从存储的角度来看,哈希表的底层是数组,其中每一个桶槽位可能包含一个值,也可能包含一个链表或一棵树。因此,哈希表可能同时包含线性数据结构(数组、链表)和非线性数据结构(树)。</p>
|
||||||
从存储的角度来看,哈希表的底层是数组,其中每一个桶槽位可能包含一个值,也可能包含一个链表或一棵树。因此,哈希表可能同时包含线性数据结构(数组、链表)和非线性数据结构(树)。</p>
|
<p><strong>Q</strong>:<code>char</code> 类型的长度是 1 字节吗?</p>
|
||||||
</div>
|
|
||||||
<div class="admonition question">
|
|
||||||
<p class="admonition-title"><code>char</code> 类型的长度是 1 字节吗?</p>
|
|
||||||
<p><code>char</code> 类型的长度由编程语言采用的编码方法决定。例如,Java、JavaScript、TypeScript、C# 都采用 UTF-16 编码(保存 Unicode 码点),因此 <code>char</code> 类型的长度为 2 字节。</p>
|
<p><code>char</code> 类型的长度由编程语言采用的编码方法决定。例如,Java、JavaScript、TypeScript、C# 都采用 UTF-16 编码(保存 Unicode 码点),因此 <code>char</code> 类型的长度为 2 字节。</p>
|
||||||
</div>
|
<p><strong>Q</strong>:基于数组实现的数据结构也称“静态数据结构” 是否有歧义?栈也可以进行出栈和入栈等操作,这些操作都是“动态”的。</p>
|
||||||
<div class="admonition question">
|
|
||||||
<p class="admonition-title">基于数组实现的数据结构也称“静态数据结构” 是否有歧义?栈也可以进行出栈和入栈等操作,这些操作都是“动态”的。</p>
|
|
||||||
<p>栈确实可以实现动态的数据操作,但数据结构仍然是“静态”(长度不可变)的。尽管基于数组的数据结构可以动态地添加或删除元素,但它们的容量是固定的。如果数据量超出了预分配的大小,就需要创建一个新的更大的数组,并将旧数组的内容复制到新数组中。</p>
|
<p>栈确实可以实现动态的数据操作,但数据结构仍然是“静态”(长度不可变)的。尽管基于数组的数据结构可以动态地添加或删除元素,但它们的容量是固定的。如果数据量超出了预分配的大小,就需要创建一个新的更大的数组,并将旧数组的内容复制到新数组中。</p>
|
||||||
</div>
|
<p><strong>Q</strong>:在构建栈(队列)的时候,未指定它的大小,为什么它们是“静态数据结构”呢?</p>
|
||||||
<div class="admonition question">
|
|
||||||
<p class="admonition-title">在构建栈(队列)的时候,未指定它的大小,为什么它们是“静态数据结构”呢?</p>
|
|
||||||
<p>在高级编程语言中,我们无须人工指定栈(队列)的初始容量,这个工作由类内部自动完成。例如,Java 的 <code>ArrayList</code> 的初始容量通常为 10。另外,扩容操作也是自动实现的。详见后续的“列表”章节。</p>
|
<p>在高级编程语言中,我们无须人工指定栈(队列)的初始容量,这个工作由类内部自动完成。例如,Java 的 <code>ArrayList</code> 的初始容量通常为 10。另外,扩容操作也是自动实现的。详见后续的“列表”章节。</p>
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Source file information -->
|
<!-- Source file information -->
|
||||||
|
|
||||||
|
@ -3530,19 +3530,13 @@
|
|||||||
<li>图的深度优先遍历是一种优先走到底、无路可走时再回溯的搜索方式,常基于递归来实现。</li>
|
<li>图的深度优先遍历是一种优先走到底、无路可走时再回溯的搜索方式,常基于递归来实现。</li>
|
||||||
</ul>
|
</ul>
|
||||||
<h3 id="2-q-a">2. Q & A<a class="headerlink" href="#2-q-a" title="Permanent link">¶</a></h3>
|
<h3 id="2-q-a">2. Q & A<a class="headerlink" href="#2-q-a" title="Permanent link">¶</a></h3>
|
||||||
<div class="admonition question">
|
<p><strong>Q</strong>:路径的定义是顶点序列还是边序列?</p>
|
||||||
<p class="admonition-title">路径的定义是顶点序列还是边序列?</p>
|
<p>维基百科上不同语言版本的定义不一致:英文版是“路径是一个边序列”,而中文版是“路径是一个顶点序列”。以下是英文版原文:In graph theory, a path in a graph is a finite or infinite sequence of edges which joins a sequence of vertices.</p>
|
||||||
<p>维基百科上不同语言版本的定义不一致:英文版是“路径是一个边序列”,而中文版是“路径是一个顶点序列”。以下是英文版原文:In graph theory, a path in a graph is a finite or infinite sequence of edges which joins a sequence of vertices.
|
<p>在本文中,路径被视为一个边序列,而不是一个顶点序列。这是因为两个顶点之间可能存在多条边连接,此时每条边都对应一条路径。</p>
|
||||||
在本文中,路径被视为一个边序列,而不是一个顶点序列。这是因为两个顶点之间可能存在多条边连接,此时每条边都对应一条路径。</p>
|
<p><strong>Q</strong>:非连通图中是否会有无法遍历到的点?</p>
|
||||||
</div>
|
|
||||||
<div class="admonition question">
|
|
||||||
<p class="admonition-title">非连通图中是否会有无法遍历到的点?</p>
|
|
||||||
<p>在非连通图中,从某个顶点出发,至少有一个顶点无法到达。遍历非连通图需要设置多个起点,以遍历到图的所有连通分量。</p>
|
<p>在非连通图中,从某个顶点出发,至少有一个顶点无法到达。遍历非连通图需要设置多个起点,以遍历到图的所有连通分量。</p>
|
||||||
</div>
|
<p><strong>Q</strong>:在邻接表中,“与该顶点相连的所有顶点”的顶点顺序是否有要求?</p>
|
||||||
<div class="admonition question">
|
|
||||||
<p class="admonition-title">在邻接表中,“与该顶点相连的所有顶点”的顶点顺序是否有要求?</p>
|
|
||||||
<p>可以是任意顺序。但在实际应用中,可能需要按照指定规则来排序,比如按照顶点添加的次序,或者按照顶点值大小的顺序等,这样有助于快速查找“带有某种极值”的顶点。</p>
|
<p>可以是任意顺序。但在实际应用中,可能需要按照指定规则来排序,比如按照顶点添加的次序,或者按照顶点值大小的顺序等,这样有助于快速查找“带有某种极值”的顶点。</p>
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Source file information -->
|
<!-- Source file information -->
|
||||||
|
|
||||||
|
@ -3532,32 +3532,20 @@
|
|||||||
<li>编程语言通常会为数据类型提供内置哈希算法,用于计算哈希表中的桶索引。通常情况下,只有不可变对象是可哈希的。</li>
|
<li>编程语言通常会为数据类型提供内置哈希算法,用于计算哈希表中的桶索引。通常情况下,只有不可变对象是可哈希的。</li>
|
||||||
</ul>
|
</ul>
|
||||||
<h3 id="2-q-a">2. Q & A<a class="headerlink" href="#2-q-a" title="Permanent link">¶</a></h3>
|
<h3 id="2-q-a">2. Q & A<a class="headerlink" href="#2-q-a" title="Permanent link">¶</a></h3>
|
||||||
<div class="admonition question">
|
<p><strong>Q</strong>:哈希表的时间复杂度在什么情况下是 <span class="arithmatex">\(O(n)\)</span> ?</p>
|
||||||
<p class="admonition-title">哈希表的时间复杂度在什么情况下是 <span class="arithmatex">\(O(n)\)</span> ?</p>
|
|
||||||
<p>当哈希冲突比较严重时,哈希表的时间复杂度会退化至 <span class="arithmatex">\(O(n)\)</span> 。当哈希函数设计得比较好、容量设置比较合理、冲突比较平均时,时间复杂度是 <span class="arithmatex">\(O(1)\)</span> 。我们使用编程语言内置的哈希表时,通常认为时间复杂度是 <span class="arithmatex">\(O(1)\)</span> 。</p>
|
<p>当哈希冲突比较严重时,哈希表的时间复杂度会退化至 <span class="arithmatex">\(O(n)\)</span> 。当哈希函数设计得比较好、容量设置比较合理、冲突比较平均时,时间复杂度是 <span class="arithmatex">\(O(1)\)</span> 。我们使用编程语言内置的哈希表时,通常认为时间复杂度是 <span class="arithmatex">\(O(1)\)</span> 。</p>
|
||||||
</div>
|
<p><strong>Q</strong>:为什么不使用哈希函数 <span class="arithmatex">\(f(x) = x\)</span> 呢?这样就不会有冲突了。</p>
|
||||||
<div class="admonition question">
|
|
||||||
<p class="admonition-title">为什么不使用哈希函数 <span class="arithmatex">\(f(x) = x\)</span> 呢?这样就不会有冲突了</p>
|
|
||||||
<p>在 <span class="arithmatex">\(f(x) = x\)</span> 哈希函数下,每个元素对应唯一的桶索引,这与数组等价。然而,输入空间通常远大于输出空间(数组长度),因此哈希函数的最后一步往往是对数组长度取模。换句话说,哈希表的目标是将一个较大的状态空间映射到一个较小的空间,并提供 <span class="arithmatex">\(O(1)\)</span> 的查询效率。</p>
|
<p>在 <span class="arithmatex">\(f(x) = x\)</span> 哈希函数下,每个元素对应唯一的桶索引,这与数组等价。然而,输入空间通常远大于输出空间(数组长度),因此哈希函数的最后一步往往是对数组长度取模。换句话说,哈希表的目标是将一个较大的状态空间映射到一个较小的空间,并提供 <span class="arithmatex">\(O(1)\)</span> 的查询效率。</p>
|
||||||
</div>
|
<p><strong>Q</strong>:哈希表底层实现是数组、链表、二叉树,但为什么效率可以比它们更高呢?</p>
|
||||||
<div class="admonition question">
|
|
||||||
<p class="admonition-title">哈希表底层实现是数组、链表、二叉树,但为什么效率可以比它们更高呢?</p>
|
|
||||||
<p>首先,哈希表的时间效率变高,但空间效率变低了。哈希表有相当一部分内存未使用。</p>
|
<p>首先,哈希表的时间效率变高,但空间效率变低了。哈希表有相当一部分内存未使用。</p>
|
||||||
<p>其次,只是在特定使用场景下时间效率变高了。如果一个功能能够在相同的时间复杂度下使用数组或链表实现,那么通常比哈希表更快。这是因为哈希函数计算需要开销,时间复杂度的常数项更大。</p>
|
<p>其次,只是在特定使用场景下时间效率变高了。如果一个功能能够在相同的时间复杂度下使用数组或链表实现,那么通常比哈希表更快。这是因为哈希函数计算需要开销,时间复杂度的常数项更大。</p>
|
||||||
<p>最后,哈希表的时间复杂度可能发生劣化。例如在链式地址中,我们采取在链表或红黑树中执行查找操作,仍然有退化至 <span class="arithmatex">\(O(n)\)</span> 时间的风险。</p>
|
<p>最后,哈希表的时间复杂度可能发生劣化。例如在链式地址中,我们采取在链表或红黑树中执行查找操作,仍然有退化至 <span class="arithmatex">\(O(n)\)</span> 时间的风险。</p>
|
||||||
</div>
|
<p><strong>Q</strong>:多次哈希有不能直接删除元素的缺陷吗?标记为已删除的空间还能再次使用吗?</p>
|
||||||
<div class="admonition question">
|
|
||||||
<p class="admonition-title">多次哈希有不能直接删除元素的缺陷吗?标记为已删除的空间还能再次使用吗?</p>
|
|
||||||
<p>多次哈希是开放寻址的一种,开放寻址法都有不能直接删除元素的缺陷,需要通过标记删除。标记为已删除的空间可以再次使用。当将新元素插入哈希表,并且通过哈希函数找到标记为已删除的位置时,该位置可以被新元素使用。这样做既能保持哈希表的探测序列不变,又能保证哈希表的空间使用率。</p>
|
<p>多次哈希是开放寻址的一种,开放寻址法都有不能直接删除元素的缺陷,需要通过标记删除。标记为已删除的空间可以再次使用。当将新元素插入哈希表,并且通过哈希函数找到标记为已删除的位置时,该位置可以被新元素使用。这样做既能保持哈希表的探测序列不变,又能保证哈希表的空间使用率。</p>
|
||||||
</div>
|
<p><strong>Q</strong>:为什么在线性探测中,查找元素的时候会出现哈希冲突呢?</p>
|
||||||
<div class="admonition question">
|
|
||||||
<p class="admonition-title">为什么在线性探测中,查找元素的时候会出现哈希冲突呢?</p>
|
|
||||||
<p>查找的时候通过哈希函数找到对应的桶和键值对,发现 <code>key</code> 不匹配,这就代表有哈希冲突。因此,线性探测法会根据预先设定的步长依次向下查找,直至找到正确的键值对或无法找到跳出为止。</p>
|
<p>查找的时候通过哈希函数找到对应的桶和键值对,发现 <code>key</code> 不匹配,这就代表有哈希冲突。因此,线性探测法会根据预先设定的步长依次向下查找,直至找到正确的键值对或无法找到跳出为止。</p>
|
||||||
</div>
|
<p><strong>Q</strong>:为什么哈希表扩容能够缓解哈希冲突?</p>
|
||||||
<div class="admonition question">
|
|
||||||
<p class="admonition-title">为什么哈希表扩容能够缓解哈希冲突?</p>
|
|
||||||
<p>哈希函数的最后一步往往是对数组长度 <span class="arithmatex">\(n\)</span> 取模(取余),让输出值落在数组索引范围内;在扩容后,数组长度 <span class="arithmatex">\(n\)</span> 发生变化,而 <code>key</code> 对应的索引也可能发生变化。原先落在同一个桶的多个 <code>key</code> ,在扩容后可能会被分配到多个桶中,从而实现哈希冲突的缓解。</p>
|
<p>哈希函数的最后一步往往是对数组长度 <span class="arithmatex">\(n\)</span> 取模(取余),让输出值落在数组索引范围内;在扩容后,数组长度 <span class="arithmatex">\(n\)</span> 发生变化,而 <code>key</code> 对应的索引也可能发生变化。原先落在同一个桶的多个 <code>key</code> ,在扩容后可能会被分配到多个桶中,从而实现哈希冲突的缓解。</p>
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Source file information -->
|
<!-- Source file information -->
|
||||||
|
|
||||||
|
@ -3526,10 +3526,8 @@
|
|||||||
<li>Top-k 是一个经典算法问题,可以使用堆数据结构高效解决,时间复杂度为 <span class="arithmatex">\(O(n \log k)\)</span> 。</li>
|
<li>Top-k 是一个经典算法问题,可以使用堆数据结构高效解决,时间复杂度为 <span class="arithmatex">\(O(n \log k)\)</span> 。</li>
|
||||||
</ul>
|
</ul>
|
||||||
<h3 id="2-q-a">2. Q & A<a class="headerlink" href="#2-q-a" title="Permanent link">¶</a></h3>
|
<h3 id="2-q-a">2. Q & A<a class="headerlink" href="#2-q-a" title="Permanent link">¶</a></h3>
|
||||||
<div class="admonition question">
|
<p><strong>Q</strong>:数据结构的“堆”与内存管理的“堆”是同一个概念吗?</p>
|
||||||
<p class="admonition-title">数据结构的“堆”与内存管理的“堆”是同一个概念吗?</p>
|
|
||||||
<p>两者不是同一个概念,只是碰巧都叫“堆”。计算机系统内存中的堆是动态内存分配的一部分,程序在运行时可以使用它来存储数据。程序可以请求一定量的堆内存,用于存储如对象和数组等复杂结构。当这些数据不再需要时,程序需要释放这些内存,以防止内存泄漏。相较于栈内存,堆内存的管理和使用需要更谨慎,使用不当可能会导致内存泄漏和野指针等问题。</p>
|
<p>两者不是同一个概念,只是碰巧都叫“堆”。计算机系统内存中的堆是动态内存分配的一部分,程序在运行时可以使用它来存储数据。程序可以请求一定量的堆内存,用于存储如对象和数组等复杂结构。当这些数据不再需要时,程序需要释放这些内存,以防止内存泄漏。相较于栈内存,堆内存的管理和使用需要更谨慎,使用不当可能会导致内存泄漏和野指针等问题。</p>
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Source file information -->
|
<!-- Source file information -->
|
||||||
|
|
||||||
|
@ -3531,31 +3531,21 @@
|
|||||||
<p align="center"> 图 11-19 排序算法对比 </p>
|
<p align="center"> 图 11-19 排序算法对比 </p>
|
||||||
|
|
||||||
<h3 id="2-q-a">2. Q & A<a class="headerlink" href="#2-q-a" title="Permanent link">¶</a></h3>
|
<h3 id="2-q-a">2. Q & A<a class="headerlink" href="#2-q-a" title="Permanent link">¶</a></h3>
|
||||||
<div class="admonition question">
|
<p><strong>Q</strong>:排序算法稳定性在什么情况下是必需的?</p>
|
||||||
<p class="admonition-title">排序算法稳定性在什么情况下是必需的?</p>
|
|
||||||
<p>在现实中,我们有可能基于对象的某个属性进行排序。例如,学生有姓名和身高两个属性,我们希望实现一个多级排序:先按照姓名进行排序,得到 <code>(A, 180) (B, 185) (C, 170) (D, 170)</code> ;再对身高进行排序。由于排序算法不稳定,因此可能得到 <code>(D, 170) (C, 170) (A, 180) (B, 185)</code> 。</p>
|
<p>在现实中,我们有可能基于对象的某个属性进行排序。例如,学生有姓名和身高两个属性,我们希望实现一个多级排序:先按照姓名进行排序,得到 <code>(A, 180) (B, 185) (C, 170) (D, 170)</code> ;再对身高进行排序。由于排序算法不稳定,因此可能得到 <code>(D, 170) (C, 170) (A, 180) (B, 185)</code> 。</p>
|
||||||
<p>可以发现,学生 D 和 C 的位置发生了交换,姓名的有序性被破坏了,而这是我们不希望看到的。</p>
|
<p>可以发现,学生 D 和 C 的位置发生了交换,姓名的有序性被破坏了,而这是我们不希望看到的。</p>
|
||||||
</div>
|
<p><strong>Q</strong>:哨兵划分中“从右往左查找”与“从左往右查找”的顺序可以交换吗?</p>
|
||||||
<div class="admonition question">
|
|
||||||
<p class="admonition-title">哨兵划分中“从右往左查找”与“从左往右查找”的顺序可以交换吗?</p>
|
|
||||||
<p>不行,当我们以最左端元素为基准数时,必须先“从右往左查找”再“从左往右查找”。这个结论有些反直觉,我们来剖析一下原因。</p>
|
<p>不行,当我们以最左端元素为基准数时,必须先“从右往左查找”再“从左往右查找”。这个结论有些反直觉,我们来剖析一下原因。</p>
|
||||||
<p>哨兵划分 <code>partition()</code> 的最后一步是交换 <code>nums[left]</code> 和 <code>nums[i]</code> 。完成交换后,基准数左边的元素都 <code><=</code> 基准数,<strong>这就要求最后一步交换前 <code>nums[left] >= nums[i]</code> 必须成立</strong>。假设我们先“从左往右查找”,那么如果找不到比基准数更大的元素,<strong>则会在 <code>i == j</code> 时跳出循环,此时可能 <code>nums[j] == nums[i] > nums[left]</code></strong>。也就是说,此时最后一步交换操作会把一个比基准数更大的元素交换至数组最左端,导致哨兵划分失败。</p>
|
<p>哨兵划分 <code>partition()</code> 的最后一步是交换 <code>nums[left]</code> 和 <code>nums[i]</code> 。完成交换后,基准数左边的元素都 <code><=</code> 基准数,<strong>这就要求最后一步交换前 <code>nums[left] >= nums[i]</code> 必须成立</strong>。假设我们先“从左往右查找”,那么如果找不到比基准数更大的元素,<strong>则会在 <code>i == j</code> 时跳出循环,此时可能 <code>nums[j] == nums[i] > nums[left]</code></strong>。也就是说,此时最后一步交换操作会把一个比基准数更大的元素交换至数组最左端,导致哨兵划分失败。</p>
|
||||||
<p>举个例子,给定数组 <code>[0, 0, 0, 0, 1]</code> ,如果先“从左向右查找”,哨兵划分后数组为 <code>[1, 0, 0, 0, 0]</code> ,这个结果是不正确的。</p>
|
<p>举个例子,给定数组 <code>[0, 0, 0, 0, 1]</code> ,如果先“从左向右查找”,哨兵划分后数组为 <code>[1, 0, 0, 0, 0]</code> ,这个结果是不正确的。</p>
|
||||||
<p>再深入思考一下,如果我们选择 <code>nums[right]</code> 为基准数,那么正好反过来,必须先“从左往右查找”。</p>
|
<p>再深入思考一下,如果我们选择 <code>nums[right]</code> 为基准数,那么正好反过来,必须先“从左往右查找”。</p>
|
||||||
</div>
|
<p><strong>Q</strong>:关于尾递归优化,为什么选短的数组能保证递归深度不超过 <span class="arithmatex">\(\log n\)</span> ?</p>
|
||||||
<div class="admonition question">
|
|
||||||
<p class="admonition-title">关于尾递归优化,为什么选短的数组能保证递归深度不超过 <span class="arithmatex">\(\log n\)</span> ?</p>
|
|
||||||
<p>递归深度就是当前未返回的递归方法的数量。每轮哨兵划分我们将原数组划分为两个子数组。在尾递归优化后,向下递归的子数组长度最大为原数组长度的一半。假设最差情况,一直为一半长度,那么最终的递归深度就是 <span class="arithmatex">\(\log n\)</span> 。</p>
|
<p>递归深度就是当前未返回的递归方法的数量。每轮哨兵划分我们将原数组划分为两个子数组。在尾递归优化后,向下递归的子数组长度最大为原数组长度的一半。假设最差情况,一直为一半长度,那么最终的递归深度就是 <span class="arithmatex">\(\log n\)</span> 。</p>
|
||||||
<p>回顾原始的快速排序,我们有可能会连续地递归长度较大的数组,最差情况下为 <span class="arithmatex">\(n\)</span>、<span class="arithmatex">\(n - 1\)</span>、<span class="arithmatex">\(\dots\)</span>、<span class="arithmatex">\(2\)</span>、<span class="arithmatex">\(1\)</span> ,递归深度为 <span class="arithmatex">\(n\)</span> 。尾递归优化可以避免这种情况出现。</p>
|
<p>回顾原始的快速排序,我们有可能会连续地递归长度较大的数组,最差情况下为 <span class="arithmatex">\(n\)</span>、<span class="arithmatex">\(n - 1\)</span>、<span class="arithmatex">\(\dots\)</span>、<span class="arithmatex">\(2\)</span>、<span class="arithmatex">\(1\)</span> ,递归深度为 <span class="arithmatex">\(n\)</span> 。尾递归优化可以避免这种情况出现。</p>
|
||||||
</div>
|
<p><strong>Q</strong>:当数组中所有元素都相等时,快速排序的时间复杂度是 <span class="arithmatex">\(O(n^2)\)</span> 吗?该如何处理这种退化情况?</p>
|
||||||
<div class="admonition question">
|
|
||||||
<p class="admonition-title">当数组中所有元素都相等时,快速排序的时间复杂度是 <span class="arithmatex">\(O(n^2)\)</span> 吗?该如何处理这种退化情况?</p>
|
|
||||||
<p>是的。对于这种情况,可以考虑通过哨兵划分将数组划分为三个部分:小于、等于、大于基准数。仅向下递归小于和大于的两部分。在该方法下,输入元素全部相等的数组,仅一轮哨兵划分即可完成排序。</p>
|
<p>是的。对于这种情况,可以考虑通过哨兵划分将数组划分为三个部分:小于、等于、大于基准数。仅向下递归小于和大于的两部分。在该方法下,输入元素全部相等的数组,仅一轮哨兵划分即可完成排序。</p>
|
||||||
</div>
|
<p><strong>Q</strong>:桶排序的最差时间复杂度为什么是 <span class="arithmatex">\(O(n^2)\)</span> ?</p>
|
||||||
<div class="admonition question">
|
|
||||||
<p class="admonition-title">桶排序的最差时间复杂度为什么是 <span class="arithmatex">\(O(n^2)\)</span> ?</p>
|
|
||||||
<p>最差情况下,所有元素被分至同一个桶中。如果我们采用一个 <span class="arithmatex">\(O(n^2)\)</span> 算法来排序这些元素,则时间复杂度为 <span class="arithmatex">\(O(n^2)\)</span> 。</p>
|
<p>最差情况下,所有元素被分至同一个桶中。如果我们采用一个 <span class="arithmatex">\(O(n^2)\)</span> 算法来排序这些元素,则时间复杂度为 <span class="arithmatex">\(O(n^2)\)</span> 。</p>
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Source file information -->
|
<!-- Source file information -->
|
||||||
|
|
||||||
|
@ -3524,27 +3524,19 @@
|
|||||||
<li>双向队列是一种具有更高自由度的队列,它允许在两端进行元素的添加和删除操作。</li>
|
<li>双向队列是一种具有更高自由度的队列,它允许在两端进行元素的添加和删除操作。</li>
|
||||||
</ul>
|
</ul>
|
||||||
<h3 id="2-q-a">2. Q & A<a class="headerlink" href="#2-q-a" title="Permanent link">¶</a></h3>
|
<h3 id="2-q-a">2. Q & A<a class="headerlink" href="#2-q-a" title="Permanent link">¶</a></h3>
|
||||||
<div class="admonition question">
|
<p><strong>Q</strong>:浏览器的前进后退是否是双向链表实现?</p>
|
||||||
<p class="admonition-title">浏览器的前进后退是否是双向链表实现?</p>
|
|
||||||
<p>浏览器的前进后退功能本质上是“栈”的体现。当用户访问一个新页面时,该页面会被添加到栈顶;当用户点击后退按钮时,该页面会从栈顶弹出。使用双向队列可以方便地实现一些额外操作,这个在“双向队列”章节有提到。</p>
|
<p>浏览器的前进后退功能本质上是“栈”的体现。当用户访问一个新页面时,该页面会被添加到栈顶;当用户点击后退按钮时,该页面会从栈顶弹出。使用双向队列可以方便地实现一些额外操作,这个在“双向队列”章节有提到。</p>
|
||||||
</div>
|
<p><strong>Q</strong>:在出栈后,是否需要释放出栈节点的内存?</p>
|
||||||
<div class="admonition question">
|
|
||||||
<p class="admonition-title">在出栈后,是否需要释放出栈节点的内存?</p>
|
|
||||||
<p>如果后续仍需要使用弹出节点,则不需要释放内存。若之后不需要用到,<code>Java</code> 和 <code>Python</code> 等语言拥有自动垃圾回收机制,因此不需要手动释放内存;在 <code>C</code> 和 <code>C++</code> 中需要手动释放内存。</p>
|
<p>如果后续仍需要使用弹出节点,则不需要释放内存。若之后不需要用到,<code>Java</code> 和 <code>Python</code> 等语言拥有自动垃圾回收机制,因此不需要手动释放内存;在 <code>C</code> 和 <code>C++</code> 中需要手动释放内存。</p>
|
||||||
</div>
|
<p><strong>Q</strong>:双向队列像是两个栈拼接在了一起,它的用途是什么?</p>
|
||||||
<div class="admonition question">
|
|
||||||
<p class="admonition-title">双向队列像是两个栈拼接在了一起,它的用途是什么?</p>
|
|
||||||
<p>双向队列就像是栈和队列的组合,或两个栈拼在了一起。它表现的是栈 + 队列的逻辑,因此可以实现栈与队列的所有应用,并且更加灵活。</p>
|
<p>双向队列就像是栈和队列的组合,或两个栈拼在了一起。它表现的是栈 + 队列的逻辑,因此可以实现栈与队列的所有应用,并且更加灵活。</p>
|
||||||
</div>
|
<p><strong>Q</strong>:撤销(undo)和反撤销(redo)具体是如何实现的?</p>
|
||||||
<div class="admonition question">
|
|
||||||
<p class="admonition-title">撤销(undo)和反撤销(redo)具体是如何实现的?</p>
|
|
||||||
<p>使用两个栈,栈 <code>A</code> 用于撤销,栈 <code>B</code> 用于反撤销。</p>
|
<p>使用两个栈,栈 <code>A</code> 用于撤销,栈 <code>B</code> 用于反撤销。</p>
|
||||||
<ol>
|
<ol>
|
||||||
<li>每当用户执行一个操作,将这个操作压入栈 <code>A</code> ,并清空栈 <code>B</code> 。</li>
|
<li>每当用户执行一个操作,将这个操作压入栈 <code>A</code> ,并清空栈 <code>B</code> 。</li>
|
||||||
<li>当用户执行“撤销”时,从栈 <code>A</code> 中弹出最近的操作,并将其压入栈 <code>B</code> 。</li>
|
<li>当用户执行“撤销”时,从栈 <code>A</code> 中弹出最近的操作,并将其压入栈 <code>B</code> 。</li>
|
||||||
<li>当用户执行“反撤销”时,从栈 <code>B</code> 中弹出最近的操作,并将其压入栈 <code>A</code> 。</li>
|
<li>当用户执行“反撤销”时,从栈 <code>B</code> 中弹出最近的操作,并将其压入栈 <code>A</code> 。</li>
|
||||||
</ol>
|
</ol>
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Source file information -->
|
<!-- Source file information -->
|
||||||
|
|
||||||
|
@ -3530,43 +3530,27 @@
|
|||||||
<li>AVL 树的旋转操作包括右旋、左旋、先右旋再左旋、先左旋再右旋。在插入或删除节点后,AVL 树会从底向顶执行旋转操作,使树重新恢复平衡。</li>
|
<li>AVL 树的旋转操作包括右旋、左旋、先右旋再左旋、先左旋再右旋。在插入或删除节点后,AVL 树会从底向顶执行旋转操作,使树重新恢复平衡。</li>
|
||||||
</ul>
|
</ul>
|
||||||
<h3 id="2-q-a">2. Q & A<a class="headerlink" href="#2-q-a" title="Permanent link">¶</a></h3>
|
<h3 id="2-q-a">2. Q & A<a class="headerlink" href="#2-q-a" title="Permanent link">¶</a></h3>
|
||||||
<div class="admonition question">
|
<p><strong>Q</strong>:对于只有一个节点的二叉树,树的高度和根节点的深度都是 <span class="arithmatex">\(0\)</span> 吗?</p>
|
||||||
<p class="admonition-title">对于只有一个节点的二叉树,树的高度和根节点的深度都是 <span class="arithmatex">\(0\)</span> 吗?</p>
|
|
||||||
<p>是的,因为高度和深度通常定义为“经过的边的数量”。</p>
|
<p>是的,因为高度和深度通常定义为“经过的边的数量”。</p>
|
||||||
</div>
|
<p><strong>Q</strong>:二叉树中的插入与删除一般由一套操作配合完成,这里的“一套操作”指什么呢?可以理解为资源的子节点的资源释放吗?</p>
|
||||||
<div class="admonition question">
|
|
||||||
<p class="admonition-title">二叉树中的插入与删除一般由一套操作配合完成,这里的“一套操作”指什么呢?可以理解为资源的子节点的资源释放吗?</p>
|
|
||||||
<p>拿二叉搜索树来举例,删除节点操作要分三种情况处理,其中每种情况都需要进行多个步骤的节点操作。</p>
|
<p>拿二叉搜索树来举例,删除节点操作要分三种情况处理,其中每种情况都需要进行多个步骤的节点操作。</p>
|
||||||
</div>
|
<p><strong>Q</strong>:为什么 DFS 遍历二叉树有前、中、后三种顺序,分别有什么用呢?</p>
|
||||||
<div class="admonition question">
|
<p>与顺序和逆序遍历数组类似,前序、中序、后序遍历是三种二叉树遍历方法,我们可以使用它们得到一个特定顺序的遍历结果。例如在二叉搜索树中,由于节点大小满足 <code>左子节点值 < 根节点值 < 右子节点值</code> ,因此我们只要按照“左 <span class="arithmatex">\(\rightarrow\)</span> 根 <span class="arithmatex">\(\rightarrow\)</span> 右”的优先级遍历树,就可以获得有序的节点序列。</p>
|
||||||
<p class="admonition-title">为什么 DFS 遍历二叉树有前、中、后三种顺序,分别有什么用呢?</p>
|
<p><strong>Q</strong>:右旋操作是处理失衡节点 <code>node</code>、<code>child</code>、<code>grand_child</code> 之间的关系,那 <code>node</code> 的父节点和 <code>node</code> 原来的连接不需要维护吗?右旋操作后岂不是断掉了?</p>
|
||||||
<p>与顺序和逆序遍历数组类似,前序、中序、后序遍历是三种二叉树遍历方法,我们可以使用它们得到一个特定顺序的遍历结果。例如在二叉搜索树中,由于节点大小满足 <code>左子节点值 < 根节点值 < 右子节点值</code> ,因此我们只要按照 <code>左 $\rightarrow$ 根 $\rightarrow$ 右</code> 的优先级遍历树,就可以获得有序的节点序列。</p>
|
|
||||||
</div>
|
|
||||||
<div class="admonition question">
|
|
||||||
<p class="admonition-title">右旋操作是处理失衡节点 <code>node</code>、<code>child</code>、<code>grand_child</code> 之间的关系,那 <code>node</code> 的父节点和 <code>node</code> 原来的连接不需要维护吗?右旋操作后岂不是断掉了?</p>
|
|
||||||
<p>我们需要从递归的视角来看这个问题。右旋操作 <code>right_rotate(root)</code> 传入的是子树的根节点,最终 <code>return child</code> 返回旋转之后的子树的根节点。子树的根节点和其父节点的连接是在该函数返回后完成的,不属于右旋操作的维护范围。</p>
|
<p>我们需要从递归的视角来看这个问题。右旋操作 <code>right_rotate(root)</code> 传入的是子树的根节点,最终 <code>return child</code> 返回旋转之后的子树的根节点。子树的根节点和其父节点的连接是在该函数返回后完成的,不属于右旋操作的维护范围。</p>
|
||||||
</div>
|
<p><strong>Q</strong>:在 C++ 中,函数被划分到 <code>private</code> 和 <code>public</code> 中,这方面有什么考量吗?为什么要将 <code>height()</code> 函数和 <code>updateHeight()</code> 函数分别放在 <code>public</code> 和 <code>private</code> 中呢?</p>
|
||||||
<div class="admonition question">
|
|
||||||
<p class="admonition-title">在 C++ 中,函数被划分到 <code>private</code> 和 <code>public</code> 中,这方面有什么考量吗?为什么要将 <code>height()</code> 函数和 <code>updateHeight()</code> 函数分别放在 <code>public</code> 和 <code>private</code> 中呢?</p>
|
|
||||||
<p>主要看方法的使用范围,如果方法只在类内部使用,那么就设计为 <code>private</code> 。例如,用户单独调用 <code>updateHeight()</code> 是没有意义的,它只是插入、删除操作中的一步。而 <code>height()</code> 是访问节点高度,类似于 <code>vector.size()</code> ,因此设置成 <code>public</code> 以便使用。</p>
|
<p>主要看方法的使用范围,如果方法只在类内部使用,那么就设计为 <code>private</code> 。例如,用户单独调用 <code>updateHeight()</code> 是没有意义的,它只是插入、删除操作中的一步。而 <code>height()</code> 是访问节点高度,类似于 <code>vector.size()</code> ,因此设置成 <code>public</code> 以便使用。</p>
|
||||||
</div>
|
<p><strong>Q</strong>:如何从一组输入数据构建一棵二叉搜索树?根节点的选择是不是很重要?</p>
|
||||||
<div class="admonition question">
|
|
||||||
<p class="admonition-title">如何从一组输入数据构建一棵二叉搜索树?根节点的选择是不是很重要?</p>
|
|
||||||
<p>是的,构建树的方法已在二叉搜索树代码中的 <code>build_tree()</code> 方法中给出。至于根节点的选择,我们通常会将输入数据排序,然后将中点元素作为根节点,再递归地构建左右子树。这样做可以最大程度保证树的平衡性。</p>
|
<p>是的,构建树的方法已在二叉搜索树代码中的 <code>build_tree()</code> 方法中给出。至于根节点的选择,我们通常会将输入数据排序,然后将中点元素作为根节点,再递归地构建左右子树。这样做可以最大程度保证树的平衡性。</p>
|
||||||
</div>
|
<p><strong>Q</strong>:在 Java 中,字符串对比是否一定要用 <code>equals()</code> 方法?</p>
|
||||||
<div class="admonition question">
|
|
||||||
<p class="admonition-title">在 Java 中,字符串对比是否一定要用 <code>equals()</code> 方法?</p>
|
|
||||||
<p>在 Java 中,对于基本数据类型,<code>==</code> 用于对比两个变量的值是否相等。对于引用类型,两种符号的工作原理是不同的。</p>
|
<p>在 Java 中,对于基本数据类型,<code>==</code> 用于对比两个变量的值是否相等。对于引用类型,两种符号的工作原理是不同的。</p>
|
||||||
<ul>
|
<ul>
|
||||||
<li><code>==</code> :用来比较两个变量是否指向同一个对象,即它们在内存中的位置是否相同。</li>
|
<li><code>==</code> :用来比较两个变量是否指向同一个对象,即它们在内存中的位置是否相同。</li>
|
||||||
<li><code>equals()</code>:用来对比两个对象的值是否相等。</li>
|
<li><code>equals()</code>:用来对比两个对象的值是否相等。</li>
|
||||||
</ul>
|
</ul>
|
||||||
<p>因此,如果要对比值,我们应该使用 <code>equals()</code> 。然而,通过 <code>String a = "hi"; String b = "hi";</code> 初始化的字符串都存储在字符串常量池中,它们指向同一个对象,因此也可以用 <code>a == b</code> 来比较两个字符串的内容。</p>
|
<p>因此,如果要对比值,我们应该使用 <code>equals()</code> 。然而,通过 <code>String a = "hi"; String b = "hi";</code> 初始化的字符串都存储在字符串常量池中,它们指向同一个对象,因此也可以用 <code>a == b</code> 来比较两个字符串的内容。</p>
|
||||||
</div>
|
<p><strong>Q</strong>:广度优先遍历到最底层之前,队列中的节点数量是 <span class="arithmatex">\(2^h\)</span> 吗?</p>
|
||||||
<div class="admonition question">
|
|
||||||
<p class="admonition-title">广度优先遍历到最底层之前,队列中的节点数量是 <span class="arithmatex">\(2^h\)</span> 吗?</p>
|
|
||||||
<p>是的,例如高度 <span class="arithmatex">\(h = 2\)</span> 的满二叉树,其节点总数 <span class="arithmatex">\(n = 7\)</span> ,则底层节点数量 <span class="arithmatex">\(4 = 2^h = (n + 1) / 2\)</span> 。</p>
|
<p>是的,例如高度 <span class="arithmatex">\(h = 2\)</span> 的满二叉树,其节点总数 <span class="arithmatex">\(n = 7\)</span> ,则底层节点数量 <span class="arithmatex">\(4 = 2^h = (n + 1) / 2\)</span> 。</p>
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Source file information -->
|
<!-- Source file information -->
|
||||||
|
|
||||||
|
Binary file not shown.
File diff suppressed because one or more lines are too long
BIN
sitemap.xml.gz
BIN
sitemap.xml.gz
Binary file not shown.
Reference in New Issue
Block a user