Polish the chapter of heap, introduction, preface.

Replace "其它" with "其他"
This commit is contained in:
krahets 2023-04-09 19:12:37 +08:00
parent 10e2180013
commit 0bec52d7cc
29 changed files with 185 additions and 161 deletions

View file

@ -7,23 +7,24 @@
#include "../include/include.h"
/* 冒泡排序 */
void bucketSort(double nums[], int size) {
void bucketSort(double nums[], int size)
{
// 初始化 k = n/2 个桶,预期向每个桶分配 2 个元素
int k = size / 2;
// 1. 将数组元素分配到各个桶中
// 1. 将数组元素分配到各个桶中
// 输入数据范围 [0, 1),使用 num * k 映射到索引范围 [0, k-1]
// 将 num 添加进桶 i
// 2. 对各个桶执行排序
// 2. 对各个桶执行排序
// 使用内置切片排序函数,也可以替换成其它排序算法
// 3. 遍历桶合并结果
// 使用内置切片排序函数,也可以替换成其他排序算法
// 3. 遍历桶合并结果
}
/* Driver Code */
int main() {
int main()
{
// 设输入数据为浮点数,范围为 [0, 1)
double nums[] = {0.49, 0.96, 0.82, 0.09, 0.57, 0.43, 0.91, 0.75, 0.15, 0.37};
int size = sizeof(nums) / sizeof(double);
@ -31,7 +32,8 @@ int main() {
printf("桶排序完成后 nums = ");
printf("[");
for (int i = 0; i < size - 1; i++) {
for (int i = 0; i < size - 1; i++)
{
printf("%g, ", nums[i]);
}
printf("]");

View file

@ -7,15 +7,19 @@
#include "../include/include.hpp"
/* 基于邻接表实现的无向图类 */
class GraphAdjList {
class GraphAdjList
{
public:
// 邻接表key: 顶点value该顶点的所有邻接顶点
unordered_map<Vertex*, vector<Vertex*>> adjList;
unordered_map<Vertex *, vector<Vertex *>> adjList;
/* 在 vector 中删除指定节点 */
void remove(vector<Vertex*> &vec, Vertex *vet) {
for (int i = 0; i < vec.size(); i++) {
if (vec[i] == vet) {
void remove(vector<Vertex *> &vec, Vertex *vet)
{
for (int i = 0; i < vec.size(); i++)
{
if (vec[i] == vet)
{
vec.erase(vec.begin() + i);
break;
}
@ -23,9 +27,11 @@ public:
}
/* 构造方法 */
GraphAdjList(const vector<vector<Vertex*>>& edges) {
GraphAdjList(const vector<vector<Vertex *>> &edges)
{
// 添加所有顶点和边
for (const vector<Vertex*>& edge : edges) {
for (const vector<Vertex *> &edge : edges)
{
addVertex(edge[0]);
addVertex(edge[1]);
addEdge(edge[0], edge[1]);
@ -36,7 +42,8 @@ public:
int size() { return adjList.size(); }
/* 添加边 */
void addEdge(Vertex* vet1, Vertex* vet2) {
void addEdge(Vertex *vet1, Vertex *vet2)
{
if (!adjList.count(vet1) || !adjList.count(vet2) || vet1 == vet2)
throw invalid_argument("不存在顶点");
// 添加边 vet1 - vet2
@ -45,7 +52,8 @@ public:
}
/* 删除边 */
void removeEdge(Vertex* vet1, Vertex* vet2) {
void removeEdge(Vertex *vet1, Vertex *vet2)
{
if (!adjList.count(vet1) || !adjList.count(vet2) || vet1 == vet2)
throw invalid_argument("不存在顶点");
// 删除边 vet1 - vet2
@ -54,30 +62,36 @@ public:
}
/* 添加顶点 */
void addVertex(Vertex* vet) {
if (adjList.count(vet)) return;
void addVertex(Vertex *vet)
{
if (adjList.count(vet))
return;
// 在邻接表中添加一个新链表
adjList[vet] = vector<Vertex*>();
adjList[vet] = vector<Vertex *>();
}
/* 删除顶点 */
void removeVertex(Vertex* vet) {
void removeVertex(Vertex *vet)
{
if (!adjList.count(vet))
throw invalid_argument("不存在顶点");
// 在邻接表中删除顶点 vet 对应的链表
adjList.erase(vet);
// 遍历其它顶点的链表,删除所有包含 vet 的边
for (auto& [key, vec] : adjList) {
// 遍历其他顶点的链表,删除所有包含 vet 的边
for (auto &[key, vec] : adjList)
{
remove(vec, vet);
}
}
/* 打印邻接表 */
void print() {
void print()
{
cout << "邻接表 =" << endl;
for (auto& adj : adjList) {
const auto& key= adj.first;
const auto& vec = adj.second;
for (auto &adj : adjList)
{
const auto &key = adj.first;
const auto &vec = adj.second;
cout << key->val << ": ";
PrintUtil::printVector(vetsToVals(vec));
}

View file

@ -7,33 +7,39 @@
#include "../include/include.hpp"
/* 桶排序 */
void bucketSort(vector<float> &nums) {
void bucketSort(vector<float> &nums)
{
// 初始化 k = n/2 个桶,预期向每个桶分配 2 个元素
int k = nums.size() / 2;
vector<vector<float>> buckets(k);
// 1. 将数组元素分配到各个桶中
for (float num : nums) {
for (float num : nums)
{
// 输入数据范围 [0, 1),使用 num * k 映射到索引范围 [0, k-1]
int i = num * k;
// 将 num 添加进桶 bucket_idx
buckets[i].push_back(num);
}
// 2. 对各个桶执行排序
for (vector<float> &bucket : buckets) {
// 使用内置排序函数,也可以替换成其它排序算法
for (vector<float> &bucket : buckets)
{
// 使用内置排序函数,也可以替换成其他排序算法
sort(bucket.begin(), bucket.end());
}
// 3. 遍历桶合并结果
int i = 0;
for (vector<float> &bucket : buckets) {
for (float num : bucket) {
for (vector<float> &bucket : buckets)
{
for (float num : bucket)
{
nums[i++] = num;
}
}
}
/* Driver Code */
int main() {
int main()
{
// 设输入数据为浮点数,范围为 [0, 1)
vector<float> nums = {0.49f, 0.96f, 0.82f, 0.09f, 0.57f, 0.43f, 0.91f, 0.75f, 0.15f, 0.37f};
bucketSort(nums);

View file

@ -70,7 +70,7 @@ public class GraphAdjList
throw new InvalidOperationException();
// 在邻接表中删除顶点 vet 对应的链表
adjList.Remove(vet);
// 遍历其顶点的链表删除所有包含 vet 的边
// 遍历其顶点的链表删除所有包含 vet 的边
foreach (List<Vertex> list in adjList.Values)
{
list.Remove(vet);

View file

@ -79,7 +79,7 @@ func (g *graphAdjList) removeVertex(vet Vertex) {
}
// 在邻接表中删除顶点 vet 对应的链表
delete(g.adjList, vet)
// 遍历其顶点的链表,删除所有包含 vet 的边
// 遍历其顶点的链表,删除所有包含 vet 的边
for _, list := range g.adjList {
DeleteSliceElms(list, vet)
}

View file

@ -23,7 +23,7 @@ func bucketSort(nums []float64) {
}
// 2. 对各个桶执行排序
for i := 0; i < k; i++ {
// 使用内置切片排序函数,也可以替换成其排序算法
// 使用内置切片排序函数,也可以替换成其排序算法
sort.Float64s(buckets[i])
}
// 3. 遍历桶合并结果

View file

@ -62,7 +62,7 @@ class GraphAdjList {
throw new IllegalArgumentException();
// 在邻接表中删除顶点 vet 对应的链表
adjList.remove(vet);
// 遍历其顶点的链表删除所有包含 vet 的边
// 遍历其顶点的链表删除所有包含 vet 的边
for (List<Vertex> list : adjList.values()) {
list.remove(vet);
}

View file

@ -26,7 +26,7 @@ public class bucket_sort {
}
// 2. 对各个桶执行排序
for (List<Float> bucket : buckets) {
// 使用内置排序函数也可以替换成其排序算法
// 使用内置排序函数也可以替换成其排序算法
Collections.sort(bucket);
}
// 3. 遍历桶合并结果

View file

@ -61,7 +61,7 @@ class GraphAdjList {
}
// 在邻接表中删除顶点 vet 对应的链表
this.adjList.delete(vet);
// 遍历其顶点的链表,删除所有包含 vet 的边
// 遍历其顶点的链表,删除所有包含 vet 的边
for (let set of this.adjList.values()) {
const index = set.indexOf(vet);
if (index > -1) {

View file

@ -21,7 +21,7 @@ function bucketSort(nums) {
}
// 2. 对各个桶执行排序
for (const bucket of buckets) {
// 使用内置排序函数,也可以替换成其排序算法
// 使用内置排序函数,也可以替换成其排序算法
bucket.sort((a, b) => a - b);
}
// 3. 遍历桶合并结果

View file

@ -56,7 +56,7 @@ class GraphAdjList:
raise ValueError
# 在邻接表中删除顶点 vet 对应的链表
self.adj_list.pop(vet)
# 遍历其顶点的链表,删除所有包含 vet 的边
# 遍历其顶点的链表,删除所有包含 vet 的边
for vertex in self.adj_list:
if vet in self.adj_list[vertex]:
self.adj_list[vertex].remove(vet)

View file

@ -18,7 +18,7 @@ def bucket_sort(nums: list[float]) -> None:
buckets[i].append(num)
# 2. 对各个桶执行排序5
for bucket in buckets:
# 使用内置排序函数,也可以替换成其排序算法
# 使用内置排序函数,也可以替换成其排序算法
bucket.sort()
# 3. 遍历桶合并结果
i = 0

View file

@ -63,7 +63,7 @@ public class GraphAdjList {
}
// vet
adjList.removeValue(forKey: vet)
// vet
// vet
for key in adjList.keys {
adjList[key]?.removeAll(where: { $0 == vet })
}

View file

@ -18,7 +18,7 @@ func bucketSort(nums: inout [Double]) {
}
// 2.
for i in buckets.indices {
// 使
// 使
buckets[i].sort()
}
// 3.

View file

@ -61,7 +61,7 @@ class GraphAdjList {
}
// 在邻接表中删除顶点 vet 对应的链表
this.adjList.delete(vet);
// 遍历其顶点的链表,删除所有包含 vet 的边
// 遍历其顶点的链表,删除所有包含 vet 的边
for (let set of this.adjList.values()) {
const index: number = set.indexOf(vet);
if (index > -1) {

View file

@ -21,7 +21,7 @@ function bucketSort(nums: number[]): void {
}
// 2. 对各个桶执行排序
for (const bucket of buckets) {
// 使用内置排序函数,也可以替换成其排序算法
// 使用内置排序函数,也可以替换成其排序算法
bucket.sort((a, b) => a - b);
}
// 3. 遍历桶合并结果

View file

@ -19,7 +19,7 @@
!!! note "缓存局部性"
在计算机中,数据读写速度排序是“硬盘 < 内存 < CPU 缓存当我们访问数组元素时计算机不仅会加载它还会缓存其周围的其数据从而借助高速缓存来提升后续操作的执行速度链表则不然计算机只能挨个地缓存各个节点这样的多次搬运降低了整体效率
在计算机中,数据读写速度排序是“硬盘 < 内存 < CPU 缓存当我们访问数组元素时计算机不仅会加载它还会缓存其周围的其数据从而借助高速缓存来提升后续操作的执行速度链表则不然计算机只能挨个地缓存各个节点这样的多次搬运降低了整体效率
- 下表对比了数组与链表在各种操作上的效率。

View file

@ -741,7 +741,7 @@ $$
### 2) 判断渐近上界
**时间复杂度由多项式 $T(n)$ 中最高阶的项来决定**。这是因为在 $n$ 趋于无穷大时,最高阶的项将发挥主导作用,其项的影响都可以被忽略。
**时间复杂度由多项式 $T(n)$ 中最高阶的项来决定**。这是因为在 $n$ 趋于无穷大时,最高阶的项将发挥主导作用,其项的影响都可以被忽略。
以下表格展示了一些例子,其中一些夸张的值是为了强调“系数无法撼动阶数”这一结论。当 $n$ 趋于无穷大时,这些常数变得无足轻重。

View file

@ -37,7 +37,7 @@
3. 当所有 bits 为 0 时代表数字 $0$ ,从零开始增大,可得最大正数为 $2^{31} - 1$
4. 剩余 $2^{31}$ 个数字全部用来表示负数,因此最小负数为 $-2^{31}$ ;具体细节涉及“源码、反码、补码”的相关知识,有兴趣的同学可以查阅学习;
整数类型 byte, short, long 的取值范围的计算方法与 int 类似,在此不再赘述。
整数类型 byte, short, long 的取值范围的计算方法与 int 类似,在此不再赘述。
### 浮点数表示方式 *

View file

@ -1,25 +1,25 @@
# 堆
「堆 Heap」是一棵限定条件下的「完全二叉树」。根据成立条件,堆主要分为两种类型:
「堆 Heap」是一种满足特定条件的完全二叉树,可分为两种类型:
- 「大顶堆 Max Heap」任意节点的值 $\geq$ 其子节点的值;
- 「小顶堆 Min Heap」任意节点的值 $\leq$ 其子节点的值;
![小顶堆与大顶堆](heap.assets/min_heap_and_max_heap.png)
## 堆术语与性质
堆作为完全二叉树的一个特例,具有以下特性:
- 由于堆是完全二叉树,因此最底层节点靠左填充,其它层节点皆被填满。
- 二叉树中的根节点对应「堆顶」,底层最靠右节点对应「堆底」。
- 对于大顶堆 / 小顶堆,其堆顶元素(即根节点)的值最大 / 最小
- 最底层节点靠左填充,其他层的节点都被填满。
- 我们将二叉树的根节点称为「堆顶」,将底层最靠右的节点称为「堆底」。
- 对于大顶堆(小顶堆),堆顶元素(即根节点)的值分别是最大(最小)的
## 堆常用操作
值得说明的是,多数编程语言提供的是「优先队列 Priority Queue」其是一种抽象数据结构**定义为具有出队优先级的队列**
需要指出的是,许多编程语言提供的是「优先队列 Priority Queue」这是一种抽象数据结构定义为具有优先级排序的队列
而恰好,**堆的定义与优先队列的操作逻辑完全吻合**,大顶堆就是一个元素从大到小出队的优先队列。从使用角度看,我们可以将「优先队列」和「堆」理解为等价的数据结构。因此,本文与代码对两者不做特别区分,统一使用「堆」来命名。
实际上,**堆通常用作实现优先队列,大顶堆相当于元素按从大到小顺序出队的优先队列**。从使用角度来看,我们可以将「优先队列」和「堆」看作等价的数据结构。因此,本书对两者不做特别区分,统一使用「堆」来命名。
堆的常用操作见下表,方法名需根据编程语言确定。
堆的常用操作见下表,方法名需根据编程语言确定。
<div class="center-table" markdown>
@ -33,11 +33,11 @@
</div>
我们可以直接使用编程语言提供的堆类(或优先队列类)。
在实际应用中,我们可以直接使用编程语言提供的堆类(或优先队列类)。
!!! tip
类似于排序中“从小到大排列”和“从大到小排列”,“大顶堆”和“小顶堆”可仅通过修改 Comparator 来互相转换。
类似于排序算法“从小到大排列”和“从大到小排列”,我们可以通过修改 Comparator 来实现“小顶堆”与“大顶堆”之间的转换。
=== "Java"
@ -303,19 +303,19 @@
## 堆的实现
下文实现的是「大顶堆」,若想转换为「小顶堆」,将所有大小逻辑判断取逆(例如将 $\geq$ 替换为 $\leq$ )即可,有兴趣的同学可自行实现。
下文实现的是大顶堆。若要将其转换为小顶堆,只需将所有大小逻辑判断取逆(例如,将 $\geq$ 替换为 $\leq$ )。感兴趣的读者可以自行实现。
### 堆的存储与表示
在二叉树章节我们学过,「完全二叉树」非常适合使用「数组」来表示,而堆恰好是一棵完全二叉树,**因而我们采用「数组」来存储「堆」**。
我们在二叉树章节中学习到,完全二叉树非常适合用数组来表示。由于堆正是一种完全二叉树,**我们将采用数组来存储堆**。
**二叉树指针**。使用数组表示二叉树时,元素代表节点值,索引代表节点在二叉树中的位置,**而节点指针通过索引映射公式来实现**。
当使用数组表示二叉树时,元素代表节点值,索引代表节点在二叉树中的位置。**节点指针通过索引映射公式来实现**。
具体地,给定索引 $i$ ,那么其左子节点索引为 $2i + 1$ 、右子节点索引为 $2i + 2$ 、父节点索引为 $(i - 1) / 2$ (向下整除)。当索引越界时,代表空节点或节点不存在。
具体而言,给定索引 $i$ ,其左子节点索引为 $2i + 1$ ,右子节点索引为 $2i + 2$ ,父节点索引为 $(i - 1) / 2$(向下取整)。当索引越界时,表示空节点或节点不存在。
![堆的表示与存储](heap.assets/representation_of_heap.png)
我们将索引映射公式封装成函数,以便后续使用。
我们可以将索引映射公式封装成函数,方便后续使用。
=== "Java"
@ -419,7 +419,7 @@
### 访问堆顶元素
堆顶元素是二叉树的根节点,即列表首元素。
堆顶元素即为二叉树的根节点,也就是列表的首个元素。
=== "Java"
@ -483,9 +483,9 @@
### 元素入堆
给定元素 `val` ,我们先将其添加到堆底。添加后,由于 `val` 可能大于堆中其它元素,此时堆的成立条件可能已经被破坏,**因此需要修复从插入节点到根节点这条路径上的各个节点**,该操作被称为「堆化 Heapify」。
给定元素 `val` ,我们首先将其添加到堆底。添加之后,由于 val 可能大于堆中其他元素,堆的成立条件可能已被破坏。因此,**需要修复从插入节点到根节点的路径上的各个节点**,这个操作被称为「堆化 Heapify」。
考虑从入堆节点开始,**从底至顶执行堆化**。具体地,比较插入节点与其父节点的值,若插入节点更大则将它们交换;并循环以上操作,从底至顶地修复堆中的各个节点;直至越过根节点时结束,或当遇到无需交换的节点时提前结束。
考虑从入堆节点开始,**从底至顶执行堆化**。具体来说,我们比较插入节点与其父节点的值,如果插入节点更大,则将它们交换。然后继续执行此操作,从底至顶修复堆中的各个节点,直至越过根节点或遇到无需交换的节点时结束。
=== "<1>"
![元素入堆步骤](heap.assets/heap_push_step1.png)
@ -505,7 +505,7 @@
=== "<6>"
![heap_push_step6](heap.assets/heap_push_step6.png)
设节点总数为 $n$ ,则树的高度为 $O(\log n)$ 易得堆化操作的循环轮数最多为 $O(\log n)$ **因而元素入堆操作的时间复杂度为 $O(\log n)$** 。
设节点总数为 $n$ ,则树的高度为 $O(\log n)$ 。由此可知,堆化操作的循环轮数最多为 $O(\log n)$ **元素入堆操作的时间复杂度为 $O(\log n)$** 。
=== "Java"
@ -589,13 +589,13 @@
### 堆顶元素出堆
堆顶元素是二叉树根节点,即列表首元素,如果我们直接将首元素从列表中删除,则二叉树中所有节点都会随之发生移位(索引发生变化),这样后续使用堆化修复就很麻烦了。为了尽量减少元素索引变动,采取以下操作步骤:
堆顶元素是二叉树的根节点,即列表首元素。如果我们直接从列表中删除首元素,那么二叉树中所有节点的索引都会发生变化,这将使得后续使用堆化修复变得困难。为了尽量减少元素索引的变动,我们采取以下操作步骤:
1. 交换堆顶元素与堆底元素(即交换根节点与最右叶节点);
2. 交换完成后,将堆底从列表中删除(注意,因为已经交换,实际上删除的是原来的堆顶元素);
2. 交换完成后,将堆底从列表中删除(注意,由于已经交换,实际上删除的是原来的堆顶元素);
3. 从根节点开始,**从顶至底执行堆化**
顾名思义,**从顶至底堆化的操作方向与从底至顶堆化相反**,我们比较根节点的值与其两个子节点的值,将最大的子节点与根节点执行交换,并循环以上操作,直到越过叶节点时结束,或当遇到无需交换的节点时提前结束。
顾名思义,**从顶至底堆化的操作方向与从底至顶堆化相反**,我们将根节点的值与其两个子节点的值进行比较,将最大的子节点与根节点交换;然后循环执行此操作,直到越过叶节点或遇到无需交换的节点时结束。
=== "<1>"
![堆顶元素出堆步骤](heap.assets/heap_pop_step1.png)
@ -627,7 +627,7 @@
=== "<10>"
![heap_pop_step10](heap.assets/heap_pop_step10.png)
与元素入堆操作类似,**堆顶元素出堆操作的时间复杂度为 $O(\log n)$**
与元素入堆操作相似,堆顶元素出堆操作的时间复杂度也为 $O(\log n)$
=== "Java"
@ -711,6 +711,6 @@
## 堆常见应用
- **优先队列**。堆常作为实现优先队列的首选数据结构,入队和出队操作时间复杂度为 $O(\log n)$ ,建队操作为 $O(n)$ ,皆非常高效。
- **堆排序**。给定一组数据,我们使用其建堆,并依次全部弹出,则可以得到有序的序列。当然,堆排序一般无需弹出元素,仅需每轮将堆顶元素交换至数组尾部并减小堆的长度即可
- **获取最大的 $k$ 个元素**。这既是一道经典算法题目,也是一种常见应用,例如选取热度前 10 的新闻作为微博热搜,选取前 10 销量的商品等。
- **优先队列**:堆通常作为实现优先队列的首选数据结构,其入队和出队操作的时间复杂度均为 $O(\log n)$,而建队操作为 $O(n)$,这些操作都非常高效。
- **堆排序**:给定一组数据,我们可以用它们建立一个堆,然后依次将所有元素弹出,从而得到一个有序序列。当然,堆排序的实现方法并不需要弹出元素,而是每轮将堆顶元素交换至数组尾部并缩小堆的长度
- **获取最大的 $k$ 个元素**:这是一个经典的算法问题,同时也是一种典型应用,例如选择热度前 10 的新闻作为微博热搜,选取销量前 10 的商品等。

View file

@ -1,8 +1,8 @@
# 小结
- 堆是一棵限定条件下的完全二叉树,根据成立条件可分为大顶堆和小顶堆。大(小)顶堆的堆顶元素最大(小)。
- 优先队列定义为一种具有出队优先级的队列。堆是实现优先队列的最常用数据结构
- 堆的常用操作和对应时间复杂度为元素入堆 $O(\log n)$ 、堆顶元素出堆 $O(\log n)$ 、访问堆顶元素 $O(1)$ 等。
- 完全二叉树非常适合用数组来表示,因此我们一般用数组来存储堆。
- 堆化操作用于修复堆的特性,在入堆和出堆操作中都会使用到。
- 输入 $n$ 个元素并建堆的时间复杂度可以优化至 $O(n)$ ,非常高效。
- 堆是一棵完全二叉树,根据成立条件可分为大顶堆和小顶堆。大(小)顶堆的堆顶元素最大(小)
- 优先队列的定义是具有出队优先级的队列,通常使用堆来实现
- 堆的常用操作及其对应的时间复杂度包括:元素入堆 $O(\log n)$ 、堆顶元素出堆 $O(\log n)$ 和访问堆顶元素 $O(1)$ 等。
- 完全二叉树非常适合用数组表示,因此我们通常使用数组来存储堆。
- 堆化操作用于维护堆的性质,在入堆和出堆操作中都会用到。
- 输入 $n$ 个元素并建堆的时间复杂度可以优化至 $O(n)$,非常高效。

View file

@ -1,18 +1,18 @@
# 算法无处不在
听到“算法”这个词,我们一般会联想到数学。但实际上,大多数算法并不包含复杂的数学,而更像是在考察基本逻辑,而这些逻辑在我们日常生活中处处可见。
当我们听到“算法”这个词时,很自然地会想到数学。然而实际上,许多算法并不涉及复杂数学,而是更多地依赖于基本逻辑,这些逻辑在我们的日常生活中处处可见。
在正式介绍算法之前,我想告诉你一件有趣的事:**其实,你在过去已经学会了很多算法,并且已经习惯将它们应用到日常生活中**。接下来,我将介绍两个具体例子来佐证
在正式探讨算法之前,有一个有趣的事实值得分享:**实际上,你已经学会了许多算法,并习惯将他们应用到日常生活中了**。下面,我将举两个具体例子来证实这一点
**例一:拼积木**。一套积木,除了有许多部件之外,还会附送详细的拼装说明书。我们按照说明书上一步步操作,即可拼出复杂的积木模型。
**例一:组装积木**。一套积木,除了包含许多零件之外,还附有详细的组装说明书。我们按照说明书一步步操作,就能组装出精美的积木模型。
如果从数据结构与算法的角度看,大大小小的「积木」就是数据结构,而「拼装说明书」上的一系列步骤就是算法。
从数据结构与算法的角度来看,积木的各种形状和连接方式代表数据结构,而组装说明书上的一系列步骤则是算法。
**例二:查字典**。在字典中,每个汉字都有一个对应的拼音,而字典是按照拼音的英文字母表顺序排列的。假设需要在字典中查询任意一个拼音首字母为 $r$ 的字,一般我们会这样做
**例二:查阅字典**。在字典里,每个汉字都对应一个拼音,而字典是按照拼音的英文字母顺序排列的。假设我们需要查找一个拼音首字母为 $r$ 的字,通常会这样操作
1. 打开字典大致一半页数的位置,查看此页的首字母是什么(假设为 $m$
2. 由于在英文字母表中 $r$ 在 $m$ 的后面,因此应排除字典前半部分,查找范围仅剩后半部分;
3. 循环执行步骤 1-2 ,直到找到拼音首字母为 $r$ 的页码时终止。
1. 翻开字典约一半的页数,查看该页首字母是什么(假设为 $m$
2. 由于在英文字母表中 $r$ 位于 $m$ 之后,所以排除字典前半部分,查找范围缩小到后半部分;
3. 不断重复步骤 1-2 ,直至找到拼音首字母为 $r$ 的页码为止。
=== "<1>"
![查字典步骤](algorithms_are_everywhere.assets/look_up_dictionary_step_1.png)
@ -29,10 +29,10 @@
=== "<5>"
![look_up_dictionary_step_5](algorithms_are_everywhere.assets/look_up_dictionary_step_5.png)
字典这个小学生的标配技能,实际上就是大名鼎鼎的「二分查找」。从数据结构角度,我们可以将字典看作是一个已排序的「数组」;而从算法角度,我们可将上述查字典的一系列指令看作是「二分查找」算法。
阅字典这个小学生必备技能,实际上就是著名的「二分查找」。从数据结构的角度,我们可以把字典视为一个已排序的「数组」;从算法的角度,我们可以将上述查字典的一系列操作看作是「二分查找」算法。
小到烹饪一道菜、大到星际航行,几乎所有问题的解决都离不开算法。计算机的出现,使我们可以通过编程将数据结构存储在内存中,也可以编写代码来调用 CPU, GPU 执行算法,从而将生活中的问题搬运到计算机中,更加高效地解决各式各样的复杂问题。
小到烹饪一道菜,大到星际航行,几乎所有问题的解决都离不开算法。计算机的出现使我们能够通过编程将数据结构存储在内存中,同时编写代码调用 CPU 和 GPU 执行算法。这样一来,我们就能把生活中的问题转移到计算机上,以更高效的方式解决各种复杂问题。
!!! tip
读到这里,如果你感到对数据结构、算法、数组、二分查找等此类概念一知半解,那么就太好了!因为这正是本书存在的价值,接下来,本书将会一步步地引导你进入数据结构与算法的知识殿堂。
阅读至此,如果你对数据结构、算法、数组和二分查找等概念仍感到一知半解,那么太好了!因为这正是本书存在的意义。接下来,这本书将一步步引导你深入数据结构与算法的知识殿堂。

View file

@ -1,7 +1,7 @@
# 小结
- 算法在生活中随处可见,并不高深莫测。我们已经不知不觉地学习到许多“算法”,用于解决生活中大大小小的问题。
- “查字典”的原理和二分查找算法一致。二分体现分而治之的重要算法思想。
- 算法是在有限时间内解决特定问题的一组指令或操作步骤,数据结构是在计算机中组织与存储数据的方式。
- 数据结构与算法两者紧密联系。数据结构是算法的底座,算法是发挥数据结构的舞台。
- 乐高积木对应数据,积木形状和连接形式对应数据结构,拼装积木的流程步骤对应算法。
- 算法在日常生活中无处不在,并不是遥不可及的高深知识。实际上,我们已经在不知不觉中学习了许多“算法”,用以解决生活中的大小问题。
- 查阅字典的原理与二分查找算法相一致。二分查找体现了分而治之的重要算法思想。
- 算法是在有限时间内解决特定问题的一组指令或操作步骤,而数据结构是计算机中组织和存储数据的方式。
- 数据结构与算法紧密相连。数据结构是算法的基石,而算法则是发挥数据结构作用的舞台。
- 乐高积木对应于数据,积木形状和连接方式代表数据结构,拼装积木的步骤则对应算法。

View file

@ -4,32 +4,31 @@
「算法 Algorithm」是在有限时间内解决特定问题的一组指令或操作步骤。算法具有以下特性
- 问题是明确的,需要拥有明确的输入和输出定义。
- 解具有确定性,即给定相同输入时,输出一定相同。
- 具有可行性,可在有限步骤、有限时间、有限内存空间下完成。
- 独立于编程语言,即可用多种语言实现。
- 问题是明确的,具有清晰的输入和输出定义。
- 解具有确定性,即给定相同的输入时,输出始终相同。
- 具有可行性,在有限步骤、时间和内存空间下可完成。
## 数据结构定义
「数据结构 Data Structure」是在计算机中组织与存储数据的方式。为了提高数据存储和操作性能,数据结构的设计原则有
「数据结构 Data Structure」是计算机中组织和存储数据的方式。为了提高数据存储和操作性能,数据结构的设计目标包括
- 空间占用尽可能小,节省计算机内存。
- 数据操作尽量快,包括数据访问、添加、删除、更新等。
- 提供简洁的数据表示和逻辑信息,以便算法高效运行。
- 空间占用尽量减少,节省计算机内存。
- 数据操作尽可能快速,涵盖数据访问、添加、删除、更新等。
- 提供简洁的数据表示和逻辑信息,以利于算法高效运行。
数据结构设计是一个充满权衡的过程,这意味着如果获得某方面的优势,则往往需要在另一方面做出妥协。例如,链表相对于数组,数据添加删除操作更加方便,但牺牲了数据的访问速度;图相对于链表,提供了更多的逻辑信息,但需要占用更多的内存空间。
数据结构设计是一个充满权衡的过程,这意味着要在某方面取得优势,往往需要在另一方面作出妥协。例如,链表相较于数组,在数据添加和删除操作上更加便捷,但牺牲了数据访问速度;图相较于链表,提供了更丰富的逻辑信息,但需要占用更大的内存空间。
## 数据结构与算法的关系
「数据结构」与「算法」是高度相关、紧密嵌合的,体现在:
「数据结构」与「算法」高度相关且紧密结合,具体表现在:
- 数据结构是算法的底座。数据结构为算法提供结构化存储的数据,以及操作数据的对应方法。
- 算法是数据结构发挥的舞台。数据结构仅存储数据信息,结合算法才可解决特定问题。
- 算法有对应最优的数据结构。给定算法,一般可基于不同的数据结构实现,而最终执行效率往往相差很大。
- 数据结构是算法的基石。数据结构为算法提供了结构化存储的数据,以及用于操作数据的方法。
- 算法是数据结构发挥的舞台。数据结构本身仅存储数据信息,通过结合算法才能解决特定问题。
- 特定算法通常有对应最优的数据结构。算法通常可以基于不同的数据结构进行实现,但最终执行效率可能相差很大。
![数据结构与算法的关系](what_is_dsa.assets/relationship_between_data_structure_and_algorithm.png)
如果将「LEGO 乐高」类比到「数据结构与算法」,那么可以得到下表所示的对应关系
类比「LEGO 乐高」和「数据结构与算法」,则对应关系如下表所示
<div class="center-table" markdown>
@ -42,6 +41,8 @@
</div>
值得注意的是,数据结构与算法独立于编程语言。正因如此,本书得以提供多种编程语言的实现。
!!! tip "约定俗成的简称"
在实际讨论中,我们通常会将「数据结构与算法」直接简称为「算法」。例如,我们熟称的 LeetCode 算法题目,实际上同时考察了数据结构和算法两部分知识。
在实际讨论时,我们通常会将「数据结构与算法」简称为「算法」。例如,众所周知的 LeetCode 算法题目,实际上同时考察了数据结构和算法两方面的知识。

View file

@ -1,18 +1,18 @@
# 关于本书
本项目致力于构建一本开源免费、新手友好的数据结构与算法入门书
本项目旨在创建一本开源免费、新手友好的数据结构与算法入门教程
- 全书采用动画图解,结构化地讲解数据结构与算法知识,内容清晰易懂、学习曲线平滑;
- 算法源代码皆可一键运行,支持 Java, C++, Python, Go, JS, TS, C#, Swift, Zig 等语言;
- 鼓励读者在章节讨论区互帮互助、共同进步,提问与评论一般能在两日内得到回复;
- 鼓励读者在章节讨论区互帮互助、共同进步,提问与评论通常可在两日内得到回复;
## 读者对象
如果您是「算法初学者」,完全没有接触过算法,或者已经有少量刷题,对数据结构与算法有朦胧的理解,在会与不会之间反复横跳,那么这本书就是为你而写
若您是「算法初学者」,从未接触过算法,或者已经有一些刷题经验,对数据结构与算法有模糊的认识,在会与不会之间反复横跳,那么这本书正是为您量身定制
如果您是「算法老手」,已经积累一定刷题量,接触过大多数题型,那么本书可以帮助你回顾与梳理算法知识体系,仓库源代码可以被当作“刷题工具库”或“算法字典”来使用。
如果您是「算法老手」,已经积累一定刷题量,熟悉大部分题型,那么本书可助您回顾与梳理算法知识体系,仓库源代码可以被当作“刷题工具库”或“算法字典”来使用。
如果您是「算法大佬」,希望可以得到你的宝贵意见建议,或者[一起参与创作](https://www.hello-algo.com/chapter_appendix/contribution/)。
若您是「算法专家」,我们期待收到您的宝贵建议,或者[一起参与创作](https://www.hello-algo.com/chapter_appendix/contribution/)。
!!! success "前置条件"
@ -20,26 +20,26 @@
## 内容结构
本书主要内容
本书主要内容包括
- **复杂度分析**:数据结构与算法的评价维度、算法效率的评估方法。时间复杂度、空间复杂度,包括推算方法、常见类型、示例等。
- **数据结构**:常用的基本数据类型,数据在内存中的存储方式、数据结构分类方法。数组、链表、栈、队列、散列表、树、堆、图等数据结构,内容包括定义、优劣势、常用操作、常见类型、典型应用、实现方法等。
- **算法**:查找算法、排序算法、搜索与回溯、动态规划、分治算法,内容包括定义、使用场景、优劣势、时空效率、实现方法、示例题目等。
- **数据结构**:常见基本数据类型,数据在内存中的存储形式、数据结构的分类方法。涉及数组、链表、栈、队列、散列表、树、堆、图等数据结构,内容包括定义、优缺点、常用操作、常见类型、典型应用、实现方法等。
- **算法**:查找算法、排序算法、搜索与回溯、动态规划、分治算法等,内容涵盖定义、应用场景、优缺点、时空效率、实现方法、示例题目等。
![Hello 算法内容结构](about_the_book.assets/hello_algo_mindmap.png)
## 致谢
本书的成书过程中,我获得了许多人的帮助,包括但不限于:
在本书的创作过程中,我得到了许多人的帮助,包括但不限于:
- 感谢我在公司的导师李汐博士,在一次畅谈时您告诉我“觉得应该做就去做”,坚定了我写这本书的决心。
- 感谢我的女朋友泡泡担任本书的首位读者,从算法小白的视角提出了许多建议,使这本书更加适合初学者来阅读。
- 感谢腾宝、琦宝、飞宝为本书起了个好听又有梗名字,直接唤起我最初敲下第一行代码 "Hello World!" 的回忆。
- 感谢苏潼为本书设计了封面和 LOGO ,在我的强迫症下前后多次帮忙修改,谢谢你的耐心
- 感谢 @squidfunk 给出的写作排版建议,以及优秀开源项目 [Material-for-MkDocs](https://github.com/squidfunk/mkdocs-material/tree/master) 。
- 感谢我在公司的导师李汐博士,在深入交谈中您鼓励我“行动起来”,坚定了我写这本书的决心。
- 感谢我的女朋友泡泡作为本书的首位读者,从算法小白的角度提出许多宝贵建议,使得本书更适合新手阅读。
- 感谢腾宝、琦宝、飞宝为本书起了一个富有创意的名字,唤起大家写下第一行代码 "Hello World!" 的美好回忆。
- 感谢苏潼为本书设计了精美的封面和 LOGO并在我的强迫症下多次耐心修改
- 感谢 @squidfunk 提供的写作排版建议,以及杰出的开源项目 [Material-for-MkDocs](https://github.com/squidfunk/mkdocs-material/tree/master) 。
本书鼓励“手脑并用”的学习方式,在这点上受到了《动手学深度学习》很大影响,也在此向各位同学强烈推荐这本著作,包括[中文版](https://github.com/d2l-ai/d2l-zh)、[英文版](https://github.com/d2l-ai/d2l-en)、[李沐老师 bilibili 主页](https://space.bilibili.com/1567748478)。
在写作过程中,我阅读了许多关于数据结构与算法的教材和文章。这些作品为本书提供了优秀的范本,确保了本书内容的准确性与品质。在此感谢所有老师和前辈们的杰出贡献!
在写作过程中,我阅读了许多数据结构与算法的教材与文章,这些著作为本书作出了很好的榜样,保证了本书内容的正确性与质量,感谢各位老师与前辈的精彩创作!
本书倡导“手脑并用”的学习方法,在此方面深受《动手学深度学习》的启发。在此向各位读者强烈推荐这本优秀著作,包括[中文版](https://github.com/d2l-ai/d2l-zh)、[英文版](https://github.com/d2l-ai/d2l-en)、[李沐老师 bilibili 主页](https://space.bilibili.com/1567748478)。
感谢父母,你们一贯的支持与鼓励给了我自由度来做这些有趣的事。
衷心感谢我的父母,正是你们一直以来的支持与鼓励,让我有机会做这些富有趣味的事。

View file

@ -1,26 +1,28 @@
# 如何使用本书
建议通读本节内容,以获取最佳阅读体验。
!!! tip
为了获得最佳的阅读体验,建议您通读本节内容。
## 算法学习路线
总体上看,我认为可将学习数据结构与算法的过程分为三个阶段。
从总体上看,我们可以将学习数据结构与算法的过程划分为三个阶段:
1. **算法入门**熟悉各种数据结构的特点、用法,学习各种算法的原理、流程、用途、效率等
2. **刷算法题**可以先从热门题单开刷,推荐[剑指 Offer](https://leetcode.cn/problem-list/xb9nqhhg/)、[LeetCode Hot 100](https://leetcode.cn/problem-list/2cktkvj/),先积累至少 100 道题量,熟悉大多数的算法问题。刚开始刷题时,“遗忘”是最大的困扰点,但这是很正常的,请不要担心。学习中有一种概念叫“周期性回顾”,同一道题隔段时间做一次,在重复 3 轮以上后,往往就能牢记于心了
3. **搭建知识体系**。在学习方面,可以阅读算法专栏文章、解题框架、算法教材,不断地丰富知识体系。在刷题方面,可以开始采用进阶刷题方案,例如按专题分类、一题多解、一解多题等,相关刷题心得可以在各个社区找到。
1. **算法入门**我们需要熟悉各种数据结构的特点和用法,学习不同算法的原理、流程、用途和效率等方面内容
2. **刷算法题**建议从热门题目开刷,如[剑指 Offer](https://leetcode.cn/problem-list/xb9nqhhg/)和[LeetCode Hot 100](https://leetcode.cn/problem-list/2cktkvj/),先积累至少 100 道题目,熟悉主流的算法问题。初次刷题时,“知识遗忘”可能是一个挑战,但请放心,这是很正常的。我们可以按照“艾宾浩斯遗忘曲线”来复习题目,通常在进行 3-5 轮的重复后,就能将其牢记在心
3. **搭建知识体系**。在学习方面,我们可以阅读算法专栏文章、解题框架和算法教材,以不断丰富知识体系。在刷题方面,可以尝试采用进阶刷题策略,如按专题分类、一题多解、一解多题等,相关刷题心得可以在各个社区找到。
作为一本入门教程,**本书内容主要对应“第一阶段”**,致力于帮助你更高效地开展第二、三阶段的学习。
作为一本入门教程,本书内容主要涵盖“第一阶段”,旨在帮助你更高效地展开第二和第三阶段的学习。
![算法学习路线](suggestions.assets/learning_route.png)
## 行文风格约定
标题后标注 `*` 的是选读章节,内容相对难。如果你的时间有限,建议可以先跳过。
标题后标注 `*` 的是选读章节,内容相对难。如果你的时间有限,建议可以先跳过。
文章中的重要名词会用 `「括号」` 标注,例如 `「数组 Array」` 。建议记住这些名词,包括英文翻译,以便后续阅读文献时使用。
文章中的重要名词会用 `「 」` 括号标注,例如 `「数组 Array」` 。请务必记住这些名词,包括英文翻译,以便后续阅读文献时使用。
重点内容、总起句、总结句会被 **加粗** ,此类文字值得特别关注。
**加粗的文字** 表示重点内容或总结性语句,这类文字值得特别关注。
专有名词和有特指含义的词句会使用 `“双引号”` 标注,以避免歧义。
@ -156,41 +158,40 @@
## 在动画图解中高效学习
视频和图片相比于文字的信息密度和结构化程度更高,更容易理解。在本书中,**知识重难点会主要以动画、图解的形式呈现**,而文字的作用则是作为动画和图的解释与补充。
相较于文字,视频和图片具有更高的信息密度和结构化程度,因此更易于理解。在本书中,**重点和难点知识将主要通过动画和图解形式展示**,而文字则作为动画和图片的解释与补充。
阅读本书时,若发现某段内容提供了动画或图解,**建议你以图为主线**,将文字内容(一般在图的上方)对齐到图中内容,综合来理解
在阅读本书时,如果发现某段内容提供了动画或图解,**建议以图为主线**,以文字(通常位于图像上方)为辅,综合两者来理解内容
![动画图解示例](suggestions.assets/animation.gif)
## 在代码实践中加深理解
本书的配套代码托管在[GitHub 仓库](https://github.com/krahets/hello-algo)**源代码包含详细注释,配有测试样例,可以直接运行**。
本书的配套代码托管在[GitHub 仓库](https://github.com/krahets/hello-algo)**源代码包含详细注释,并附有测试样例,可直接运行**。
- 若学习时间紧张,**建议至少将所有代码通读并运行一遍**。
- 若时间允许,**强烈建议对照着代码自己敲一遍**。相比于读代码,写代码的过程往往能带来新的收获。
如果学习时间有限,建议你至少通读并运行所有代码。如果时间充裕,**建议参照代码自行敲一遍**。与仅阅读代码相比,编写代码的过程往往能带来更多收获。
![运行代码示例](suggestions.assets/running_code.gif)
**第一步:安装本地编程环境**。参照[附录教程](https://www.hello-algo.com/chapter_appendix/installation/),如果已有可直接跳过
**第一步:安装本地编程环境**。参照[附录教程](https://www.hello-algo.com/chapter_appendix/installation/)进行安装,如果已安装则可跳过此步骤
**第二步:下载代码仓**。如果已经安装 [Git](https://git-scm.com/downloads) ,可以通过命令行来克隆代码仓
**第二步:下载代码仓**。如果已经安装 [Git](https://git-scm.com/downloads) ,可以通过以下命令克隆本仓库
```shell
git clone https://github.com/krahets/hello-algo.git
```
当然你也可以点击“Download ZIP”直接下载代码压缩包本地解压即可。
当然你也可以点击“Download ZIP”直接下载代码压缩包然后在本地解压即可。
![克隆仓库与下载代码](suggestions.assets/download_code.png)
**第三步:运行源代码**。若代码块的顶部标有文件名称,则可在仓库 `codes` 文件夹中找到对应的 **源代码文件**。源代码文件可以帮助你省去不必要的调试时间,将精力集中在学习内容上
**第三步:运行源代码**。如果代码块顶部标有文件名称,则可以在仓库的 `codes` 文件夹中找到相应的源代码文件。源代码文件将帮助你节省不必要的调试时间,让你能够专注于学习内容
![代码块与对应的源代码文件](suggestions.assets/code_md_to_repo.png)
## 在提问讨论中共同成长
阅读本书时,请不要“惯着”那些弄不明白的知识点。**欢迎在评论区留下你的问题**,小伙伴们和我都会给予解答,您一般 2 日内会得到回复。
阅读本书时,请不要“惯着”那些没学明白的知识点。**欢迎在评论区提出你的问题**,我和其他小伙伴们将竭诚为你解答,一般情况下可在两天内得到回复。
同时,也希望你可以多花时间逛逛评论区。一方面,可以看看大家遇到了什么问题,反过来查漏补缺,这往往可以引起更加深度的思考。另一方面,也希望你可以慷慨地解答小伙伴们的问题、分享自己的见解,大家互相学习与进步!
同时,也希望您能在评论区多花些时间。一方面,您可以了解大家遇到的问题,从而查漏补缺,这将有助于激发更深入的思考。另一方面,希望您能慷慨地回答其他小伙伴的问题、分享您的见解,让大家共同学习和进步。
![评论区示例](suggestions.assets/comment.gif)

View file

@ -1,8 +1,8 @@
# 小结
- 本书主要面向算法初学者。对于已经有一定积累的同学,这本书可以帮助你系统回顾算法知识,源代码可被当作“刷题工具库”来使用。
- 书中内容主要分为复杂度分析、数据结构、算法三部分,覆盖了该领域的大部分主题。
- 对于算法小白,在初学阶段阅读一本入门书是非常有必要的,可以少走许多弯路。
- 书内的动画和图解往往介绍的是重点和难点知识,在阅读时应该多加关注。
- 实践是学习编程的最佳方式,强烈推荐运行源代码,动手敲代码。
- 本书提供了讨论区,遇到疑惑可以随时提问
- 本书的主要受众是算法初学者。对于已具备一定积累的同学,本书能帮助系统回顾算法知识,同时源代码可作为“刷题工具库”使用。
- 书中内容主要包括复杂度分析、数据结构、算法三部分,涵盖了该领域的绝大部分主题。
- 对于算法新手,在初学阶段阅读一本入门书籍至关重要,有助于避免走弯路。
- 书内的动画和图解通常用于介绍重点和难点知识,阅读时应给予更多关注。
- 实践乃学习编程之最佳途径,强烈建议运行源代码并亲自敲打代码。
- 本书设有讨论区,欢迎随时分享你的疑惑

View file

@ -4,11 +4,11 @@
[2] Aditya Bhargava. Grokking Algorithms: An Illustrated Guide for Programmers and Other Curious People (1st Edition).
[3] 严蔚敏. 数据结构( C 语言版).
[3] 严蔚敏. 数据结构C 语言版).
[4] 邓俊辉. 数据结构( C++ 语言版,第三版).
[4] 邓俊辉. 数据结构C++ 语言版,第三版).
[5] 马克·艾伦·维斯著,陈越译. 数据结构与算法分析Java语言描述第三版.
[5] 马克 艾伦 维斯著,陈越译. 数据结构与算法分析Java语言描述第三版.
[6] 程杰. 大话数据结构.

View file

@ -10,4 +10,4 @@
![排序算法对比](summary.assets/sorting_algorithms_comparison.png)
- 总体来看,我们追求运行快、稳定、原地、正向自适应性的排序。显然,如同其数据结构与算法一样,同时满足这些条件的排序算法并不存在,我们需要根据问题特点来选择排序算法。
- 总体来看,我们追求运行快、稳定、原地、正向自适应性的排序。显然,如同其数据结构与算法一样,同时满足这些条件的排序算法并不存在,我们需要根据问题特点来选择排序算法。