mirror of
https://github.com/krahets/hello-algo.git
synced 2024-12-25 12:36:30 +08:00
Polish the chapter of heap, introduction, preface.
Replace "其它" with "其他"
This commit is contained in:
parent
10e2180013
commit
0bec52d7cc
29 changed files with 185 additions and 161 deletions
|
@ -7,23 +7,24 @@
|
||||||
#include "../include/include.h"
|
#include "../include/include.h"
|
||||||
|
|
||||||
/* 冒泡排序 */
|
/* 冒泡排序 */
|
||||||
void bucketSort(double nums[], int size) {
|
void bucketSort(double nums[], int size)
|
||||||
|
{
|
||||||
// 初始化 k = n/2 个桶,预期向每个桶分配 2 个元素
|
// 初始化 k = n/2 个桶,预期向每个桶分配 2 个元素
|
||||||
int k = size / 2;
|
int k = size / 2;
|
||||||
// 1. 将数组元素分配到各个桶中
|
// 1. 将数组元素分配到各个桶中
|
||||||
// 输入数据范围 [0, 1),使用 num * k 映射到索引范围 [0, k-1]
|
// 输入数据范围 [0, 1),使用 num * k 映射到索引范围 [0, k-1]
|
||||||
// 将 num 添加进桶 i
|
// 将 num 添加进桶 i
|
||||||
|
|
||||||
// 2. 对各个桶执行排序
|
// 2. 对各个桶执行排序
|
||||||
|
|
||||||
// 使用内置切片排序函数,也可以替换成其它排序算法
|
// 使用内置切片排序函数,也可以替换成其他排序算法
|
||||||
|
|
||||||
// 3. 遍历桶合并结果
|
|
||||||
|
|
||||||
|
// 3. 遍历桶合并结果
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Driver Code */
|
/* Driver Code */
|
||||||
int main() {
|
int main()
|
||||||
|
{
|
||||||
// 设输入数据为浮点数,范围为 [0, 1)
|
// 设输入数据为浮点数,范围为 [0, 1)
|
||||||
double nums[] = {0.49, 0.96, 0.82, 0.09, 0.57, 0.43, 0.91, 0.75, 0.15, 0.37};
|
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);
|
int size = sizeof(nums) / sizeof(double);
|
||||||
|
@ -31,7 +32,8 @@ int main() {
|
||||||
|
|
||||||
printf("桶排序完成后 nums = ");
|
printf("桶排序完成后 nums = ");
|
||||||
printf("[");
|
printf("[");
|
||||||
for (int i = 0; i < size - 1; i++) {
|
for (int i = 0; i < size - 1; i++)
|
||||||
|
{
|
||||||
printf("%g, ", nums[i]);
|
printf("%g, ", nums[i]);
|
||||||
}
|
}
|
||||||
printf("]");
|
printf("]");
|
||||||
|
|
|
@ -7,15 +7,19 @@
|
||||||
#include "../include/include.hpp"
|
#include "../include/include.hpp"
|
||||||
|
|
||||||
/* 基于邻接表实现的无向图类 */
|
/* 基于邻接表实现的无向图类 */
|
||||||
class GraphAdjList {
|
class GraphAdjList
|
||||||
|
{
|
||||||
public:
|
public:
|
||||||
// 邻接表,key: 顶点,value:该顶点的所有邻接顶点
|
// 邻接表,key: 顶点,value:该顶点的所有邻接顶点
|
||||||
unordered_map<Vertex*, vector<Vertex*>> adjList;
|
unordered_map<Vertex *, vector<Vertex *>> adjList;
|
||||||
|
|
||||||
/* 在 vector 中删除指定节点 */
|
/* 在 vector 中删除指定节点 */
|
||||||
void remove(vector<Vertex*> &vec, Vertex *vet) {
|
void remove(vector<Vertex *> &vec, Vertex *vet)
|
||||||
for (int i = 0; i < vec.size(); i++) {
|
{
|
||||||
if (vec[i] == vet) {
|
for (int i = 0; i < vec.size(); i++)
|
||||||
|
{
|
||||||
|
if (vec[i] == vet)
|
||||||
|
{
|
||||||
vec.erase(vec.begin() + i);
|
vec.erase(vec.begin() + i);
|
||||||
break;
|
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[0]);
|
||||||
addVertex(edge[1]);
|
addVertex(edge[1]);
|
||||||
addEdge(edge[0], edge[1]);
|
addEdge(edge[0], edge[1]);
|
||||||
|
@ -36,7 +42,8 @@ public:
|
||||||
int size() { return adjList.size(); }
|
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)
|
if (!adjList.count(vet1) || !adjList.count(vet2) || vet1 == vet2)
|
||||||
throw invalid_argument("不存在顶点");
|
throw invalid_argument("不存在顶点");
|
||||||
// 添加边 vet1 - vet2
|
// 添加边 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)
|
if (!adjList.count(vet1) || !adjList.count(vet2) || vet1 == vet2)
|
||||||
throw invalid_argument("不存在顶点");
|
throw invalid_argument("不存在顶点");
|
||||||
// 删除边 vet1 - vet2
|
// 删除边 vet1 - vet2
|
||||||
|
@ -54,30 +62,36 @@ public:
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 添加顶点 */
|
/* 添加顶点 */
|
||||||
void addVertex(Vertex* vet) {
|
void addVertex(Vertex *vet)
|
||||||
if (adjList.count(vet)) return;
|
{
|
||||||
|
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))
|
if (!adjList.count(vet))
|
||||||
throw invalid_argument("不存在顶点");
|
throw invalid_argument("不存在顶点");
|
||||||
// 在邻接表中删除顶点 vet 对应的链表
|
// 在邻接表中删除顶点 vet 对应的链表
|
||||||
adjList.erase(vet);
|
adjList.erase(vet);
|
||||||
// 遍历其它顶点的链表,删除所有包含 vet 的边
|
// 遍历其他顶点的链表,删除所有包含 vet 的边
|
||||||
for (auto& [key, vec] : adjList) {
|
for (auto &[key, vec] : adjList)
|
||||||
|
{
|
||||||
remove(vec, vet);
|
remove(vec, vet);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 打印邻接表 */
|
/* 打印邻接表 */
|
||||||
void print() {
|
void print()
|
||||||
|
{
|
||||||
cout << "邻接表 =" << endl;
|
cout << "邻接表 =" << endl;
|
||||||
for (auto& adj : adjList) {
|
for (auto &adj : adjList)
|
||||||
const auto& key= adj.first;
|
{
|
||||||
const auto& vec = adj.second;
|
const auto &key = adj.first;
|
||||||
|
const auto &vec = adj.second;
|
||||||
cout << key->val << ": ";
|
cout << key->val << ": ";
|
||||||
PrintUtil::printVector(vetsToVals(vec));
|
PrintUtil::printVector(vetsToVals(vec));
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,33 +7,39 @@
|
||||||
#include "../include/include.hpp"
|
#include "../include/include.hpp"
|
||||||
|
|
||||||
/* 桶排序 */
|
/* 桶排序 */
|
||||||
void bucketSort(vector<float> &nums) {
|
void bucketSort(vector<float> &nums)
|
||||||
|
{
|
||||||
// 初始化 k = n/2 个桶,预期向每个桶分配 2 个元素
|
// 初始化 k = n/2 个桶,预期向每个桶分配 2 个元素
|
||||||
int k = nums.size() / 2;
|
int k = nums.size() / 2;
|
||||||
vector<vector<float>> buckets(k);
|
vector<vector<float>> buckets(k);
|
||||||
// 1. 将数组元素分配到各个桶中
|
// 1. 将数组元素分配到各个桶中
|
||||||
for (float num : nums) {
|
for (float num : nums)
|
||||||
|
{
|
||||||
// 输入数据范围 [0, 1),使用 num * k 映射到索引范围 [0, k-1]
|
// 输入数据范围 [0, 1),使用 num * k 映射到索引范围 [0, k-1]
|
||||||
int i = num * k;
|
int i = num * k;
|
||||||
// 将 num 添加进桶 bucket_idx
|
// 将 num 添加进桶 bucket_idx
|
||||||
buckets[i].push_back(num);
|
buckets[i].push_back(num);
|
||||||
}
|
}
|
||||||
// 2. 对各个桶执行排序
|
// 2. 对各个桶执行排序
|
||||||
for (vector<float> &bucket : buckets) {
|
for (vector<float> &bucket : buckets)
|
||||||
// 使用内置排序函数,也可以替换成其它排序算法
|
{
|
||||||
|
// 使用内置排序函数,也可以替换成其他排序算法
|
||||||
sort(bucket.begin(), bucket.end());
|
sort(bucket.begin(), bucket.end());
|
||||||
}
|
}
|
||||||
// 3. 遍历桶合并结果
|
// 3. 遍历桶合并结果
|
||||||
int i = 0;
|
int i = 0;
|
||||||
for (vector<float> &bucket : buckets) {
|
for (vector<float> &bucket : buckets)
|
||||||
for (float num : bucket) {
|
{
|
||||||
|
for (float num : bucket)
|
||||||
|
{
|
||||||
nums[i++] = num;
|
nums[i++] = num;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Driver Code */
|
/* Driver Code */
|
||||||
int main() {
|
int main()
|
||||||
|
{
|
||||||
// 设输入数据为浮点数,范围为 [0, 1)
|
// 设输入数据为浮点数,范围为 [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};
|
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);
|
bucketSort(nums);
|
||||||
|
|
|
@ -70,7 +70,7 @@ public class GraphAdjList
|
||||||
throw new InvalidOperationException();
|
throw new InvalidOperationException();
|
||||||
// 在邻接表中删除顶点 vet 对应的链表
|
// 在邻接表中删除顶点 vet 对应的链表
|
||||||
adjList.Remove(vet);
|
adjList.Remove(vet);
|
||||||
// 遍历其它顶点的链表,删除所有包含 vet 的边
|
// 遍历其他顶点的链表,删除所有包含 vet 的边
|
||||||
foreach (List<Vertex> list in adjList.Values)
|
foreach (List<Vertex> list in adjList.Values)
|
||||||
{
|
{
|
||||||
list.Remove(vet);
|
list.Remove(vet);
|
||||||
|
|
|
@ -79,7 +79,7 @@ func (g *graphAdjList) removeVertex(vet Vertex) {
|
||||||
}
|
}
|
||||||
// 在邻接表中删除顶点 vet 对应的链表
|
// 在邻接表中删除顶点 vet 对应的链表
|
||||||
delete(g.adjList, vet)
|
delete(g.adjList, vet)
|
||||||
// 遍历其它顶点的链表,删除所有包含 vet 的边
|
// 遍历其他顶点的链表,删除所有包含 vet 的边
|
||||||
for _, list := range g.adjList {
|
for _, list := range g.adjList {
|
||||||
DeleteSliceElms(list, vet)
|
DeleteSliceElms(list, vet)
|
||||||
}
|
}
|
||||||
|
|
|
@ -23,7 +23,7 @@ func bucketSort(nums []float64) {
|
||||||
}
|
}
|
||||||
// 2. 对各个桶执行排序
|
// 2. 对各个桶执行排序
|
||||||
for i := 0; i < k; i++ {
|
for i := 0; i < k; i++ {
|
||||||
// 使用内置切片排序函数,也可以替换成其它排序算法
|
// 使用内置切片排序函数,也可以替换成其他排序算法
|
||||||
sort.Float64s(buckets[i])
|
sort.Float64s(buckets[i])
|
||||||
}
|
}
|
||||||
// 3. 遍历桶合并结果
|
// 3. 遍历桶合并结果
|
||||||
|
|
|
@ -62,7 +62,7 @@ class GraphAdjList {
|
||||||
throw new IllegalArgumentException();
|
throw new IllegalArgumentException();
|
||||||
// 在邻接表中删除顶点 vet 对应的链表
|
// 在邻接表中删除顶点 vet 对应的链表
|
||||||
adjList.remove(vet);
|
adjList.remove(vet);
|
||||||
// 遍历其它顶点的链表,删除所有包含 vet 的边
|
// 遍历其他顶点的链表,删除所有包含 vet 的边
|
||||||
for (List<Vertex> list : adjList.values()) {
|
for (List<Vertex> list : adjList.values()) {
|
||||||
list.remove(vet);
|
list.remove(vet);
|
||||||
}
|
}
|
||||||
|
|
|
@ -26,7 +26,7 @@ public class bucket_sort {
|
||||||
}
|
}
|
||||||
// 2. 对各个桶执行排序
|
// 2. 对各个桶执行排序
|
||||||
for (List<Float> bucket : buckets) {
|
for (List<Float> bucket : buckets) {
|
||||||
// 使用内置排序函数,也可以替换成其它排序算法
|
// 使用内置排序函数,也可以替换成其他排序算法
|
||||||
Collections.sort(bucket);
|
Collections.sort(bucket);
|
||||||
}
|
}
|
||||||
// 3. 遍历桶合并结果
|
// 3. 遍历桶合并结果
|
||||||
|
|
|
@ -61,7 +61,7 @@ class GraphAdjList {
|
||||||
}
|
}
|
||||||
// 在邻接表中删除顶点 vet 对应的链表
|
// 在邻接表中删除顶点 vet 对应的链表
|
||||||
this.adjList.delete(vet);
|
this.adjList.delete(vet);
|
||||||
// 遍历其它顶点的链表,删除所有包含 vet 的边
|
// 遍历其他顶点的链表,删除所有包含 vet 的边
|
||||||
for (let set of this.adjList.values()) {
|
for (let set of this.adjList.values()) {
|
||||||
const index = set.indexOf(vet);
|
const index = set.indexOf(vet);
|
||||||
if (index > -1) {
|
if (index > -1) {
|
||||||
|
|
|
@ -21,7 +21,7 @@ function bucketSort(nums) {
|
||||||
}
|
}
|
||||||
// 2. 对各个桶执行排序
|
// 2. 对各个桶执行排序
|
||||||
for (const bucket of buckets) {
|
for (const bucket of buckets) {
|
||||||
// 使用内置排序函数,也可以替换成其它排序算法
|
// 使用内置排序函数,也可以替换成其他排序算法
|
||||||
bucket.sort((a, b) => a - b);
|
bucket.sort((a, b) => a - b);
|
||||||
}
|
}
|
||||||
// 3. 遍历桶合并结果
|
// 3. 遍历桶合并结果
|
||||||
|
|
|
@ -56,7 +56,7 @@ class GraphAdjList:
|
||||||
raise ValueError
|
raise ValueError
|
||||||
# 在邻接表中删除顶点 vet 对应的链表
|
# 在邻接表中删除顶点 vet 对应的链表
|
||||||
self.adj_list.pop(vet)
|
self.adj_list.pop(vet)
|
||||||
# 遍历其它顶点的链表,删除所有包含 vet 的边
|
# 遍历其他顶点的链表,删除所有包含 vet 的边
|
||||||
for vertex in self.adj_list:
|
for vertex in self.adj_list:
|
||||||
if vet in self.adj_list[vertex]:
|
if vet in self.adj_list[vertex]:
|
||||||
self.adj_list[vertex].remove(vet)
|
self.adj_list[vertex].remove(vet)
|
||||||
|
|
|
@ -18,7 +18,7 @@ def bucket_sort(nums: list[float]) -> None:
|
||||||
buckets[i].append(num)
|
buckets[i].append(num)
|
||||||
# 2. 对各个桶执行排序5
|
# 2. 对各个桶执行排序5
|
||||||
for bucket in buckets:
|
for bucket in buckets:
|
||||||
# 使用内置排序函数,也可以替换成其它排序算法
|
# 使用内置排序函数,也可以替换成其他排序算法
|
||||||
bucket.sort()
|
bucket.sort()
|
||||||
# 3. 遍历桶合并结果
|
# 3. 遍历桶合并结果
|
||||||
i = 0
|
i = 0
|
||||||
|
|
|
@ -63,7 +63,7 @@ public class GraphAdjList {
|
||||||
}
|
}
|
||||||
// 在邻接表中删除顶点 vet 对应的链表
|
// 在邻接表中删除顶点 vet 对应的链表
|
||||||
adjList.removeValue(forKey: vet)
|
adjList.removeValue(forKey: vet)
|
||||||
// 遍历其它顶点的链表,删除所有包含 vet 的边
|
// 遍历其他顶点的链表,删除所有包含 vet 的边
|
||||||
for key in adjList.keys {
|
for key in adjList.keys {
|
||||||
adjList[key]?.removeAll(where: { $0 == vet })
|
adjList[key]?.removeAll(where: { $0 == vet })
|
||||||
}
|
}
|
||||||
|
|
|
@ -18,7 +18,7 @@ func bucketSort(nums: inout [Double]) {
|
||||||
}
|
}
|
||||||
// 2. 对各个桶执行排序
|
// 2. 对各个桶执行排序
|
||||||
for i in buckets.indices {
|
for i in buckets.indices {
|
||||||
// 使用内置排序函数,也可以替换成其它排序算法
|
// 使用内置排序函数,也可以替换成其他排序算法
|
||||||
buckets[i].sort()
|
buckets[i].sort()
|
||||||
}
|
}
|
||||||
// 3. 遍历桶合并结果
|
// 3. 遍历桶合并结果
|
||||||
|
|
|
@ -61,7 +61,7 @@ class GraphAdjList {
|
||||||
}
|
}
|
||||||
// 在邻接表中删除顶点 vet 对应的链表
|
// 在邻接表中删除顶点 vet 对应的链表
|
||||||
this.adjList.delete(vet);
|
this.adjList.delete(vet);
|
||||||
// 遍历其它顶点的链表,删除所有包含 vet 的边
|
// 遍历其他顶点的链表,删除所有包含 vet 的边
|
||||||
for (let set of this.adjList.values()) {
|
for (let set of this.adjList.values()) {
|
||||||
const index: number = set.indexOf(vet);
|
const index: number = set.indexOf(vet);
|
||||||
if (index > -1) {
|
if (index > -1) {
|
||||||
|
|
|
@ -21,7 +21,7 @@ function bucketSort(nums: number[]): void {
|
||||||
}
|
}
|
||||||
// 2. 对各个桶执行排序
|
// 2. 对各个桶执行排序
|
||||||
for (const bucket of buckets) {
|
for (const bucket of buckets) {
|
||||||
// 使用内置排序函数,也可以替换成其它排序算法
|
// 使用内置排序函数,也可以替换成其他排序算法
|
||||||
bucket.sort((a, b) => a - b);
|
bucket.sort((a, b) => a - b);
|
||||||
}
|
}
|
||||||
// 3. 遍历桶合并结果
|
// 3. 遍历桶合并结果
|
||||||
|
|
|
@ -19,7 +19,7 @@
|
||||||
|
|
||||||
!!! note "缓存局部性"
|
!!! note "缓存局部性"
|
||||||
|
|
||||||
在计算机中,数据读写速度排序是“硬盘 < 内存 < CPU 缓存”。当我们访问数组元素时,计算机不仅会加载它,还会缓存其周围的其它数据,从而借助高速缓存来提升后续操作的执行速度。链表则不然,计算机只能挨个地缓存各个节点,这样的多次“搬运”降低了整体效率。
|
在计算机中,数据读写速度排序是“硬盘 < 内存 < CPU 缓存”。当我们访问数组元素时,计算机不仅会加载它,还会缓存其周围的其他数据,从而借助高速缓存来提升后续操作的执行速度。链表则不然,计算机只能挨个地缓存各个节点,这样的多次“搬运”降低了整体效率。
|
||||||
|
|
||||||
- 下表对比了数组与链表在各种操作上的效率。
|
- 下表对比了数组与链表在各种操作上的效率。
|
||||||
|
|
||||||
|
|
|
@ -741,7 +741,7 @@ $$
|
||||||
|
|
||||||
### 2) 判断渐近上界
|
### 2) 判断渐近上界
|
||||||
|
|
||||||
**时间复杂度由多项式 $T(n)$ 中最高阶的项来决定**。这是因为在 $n$ 趋于无穷大时,最高阶的项将发挥主导作用,其它项的影响都可以被忽略。
|
**时间复杂度由多项式 $T(n)$ 中最高阶的项来决定**。这是因为在 $n$ 趋于无穷大时,最高阶的项将发挥主导作用,其他项的影响都可以被忽略。
|
||||||
|
|
||||||
以下表格展示了一些例子,其中一些夸张的值是为了强调“系数无法撼动阶数”这一结论。当 $n$ 趋于无穷大时,这些常数变得无足轻重。
|
以下表格展示了一些例子,其中一些夸张的值是为了强调“系数无法撼动阶数”这一结论。当 $n$ 趋于无穷大时,这些常数变得无足轻重。
|
||||||
|
|
||||||
|
|
|
@ -37,7 +37,7 @@
|
||||||
3. 当所有 bits 为 0 时代表数字 $0$ ,从零开始增大,可得最大正数为 $2^{31} - 1$;
|
3. 当所有 bits 为 0 时代表数字 $0$ ,从零开始增大,可得最大正数为 $2^{31} - 1$;
|
||||||
4. 剩余 $2^{31}$ 个数字全部用来表示负数,因此最小负数为 $-2^{31}$ ;具体细节涉及“源码、反码、补码”的相关知识,有兴趣的同学可以查阅学习;
|
4. 剩余 $2^{31}$ 个数字全部用来表示负数,因此最小负数为 $-2^{31}$ ;具体细节涉及“源码、反码、补码”的相关知识,有兴趣的同学可以查阅学习;
|
||||||
|
|
||||||
其它整数类型 byte, short, long 的取值范围的计算方法与 int 类似,在此不再赘述。
|
其他整数类型 byte, short, long 的取值范围的计算方法与 int 类似,在此不再赘述。
|
||||||
|
|
||||||
### 浮点数表示方式 *
|
### 浮点数表示方式 *
|
||||||
|
|
||||||
|
|
|
@ -1,25 +1,25 @@
|
||||||
# 堆
|
# 堆
|
||||||
|
|
||||||
「堆 Heap」是一棵限定条件下的「完全二叉树」。根据成立条件,堆主要分为两种类型:
|
「堆 Heap」是一种满足特定条件的完全二叉树,可分为两种类型:
|
||||||
|
|
||||||
- 「大顶堆 Max Heap」,任意节点的值 $\geq$ 其子节点的值;
|
- 「大顶堆 Max Heap」,任意节点的值 $\geq$ 其子节点的值;
|
||||||
- 「小顶堆 Min Heap」,任意节点的值 $\leq$ 其子节点的值;
|
- 「小顶堆 Min Heap」,任意节点的值 $\leq$ 其子节点的值;
|
||||||
|
|
||||||
![小顶堆与大顶堆](heap.assets/min_heap_and_max_heap.png)
|
![小顶堆与大顶堆](heap.assets/min_heap_and_max_heap.png)
|
||||||
|
|
||||||
## 堆术语与性质
|
堆作为完全二叉树的一个特例,具有以下特性:
|
||||||
|
|
||||||
- 由于堆是完全二叉树,因此最底层节点靠左填充,其它层节点皆被填满。
|
- 最底层节点靠左填充,其他层的节点都被填满。
|
||||||
- 二叉树中的根节点对应「堆顶」,底层最靠右节点对应「堆底」。
|
- 我们将二叉树的根节点称为「堆顶」,将底层最靠右的节点称为「堆底」。
|
||||||
- 对于大顶堆 / 小顶堆,其堆顶元素(即根节点)的值最大 / 最小。
|
- 对于大顶堆(小顶堆),堆顶元素(即根节点)的值分别是最大(最小)的。
|
||||||
|
|
||||||
## 堆常用操作
|
## 堆常用操作
|
||||||
|
|
||||||
值得说明的是,多数编程语言提供的是「优先队列 Priority Queue」,其是一种抽象数据结构,**定义为具有出队优先级的队列**。
|
需要指出的是,许多编程语言提供的是「优先队列 Priority Queue」,这是一种抽象数据结构,定义为具有优先级排序的队列。
|
||||||
|
|
||||||
而恰好,**堆的定义与优先队列的操作逻辑完全吻合**,大顶堆就是一个元素从大到小出队的优先队列。从使用角度看,我们可以将「优先队列」和「堆」理解为等价的数据结构。因此,本文与代码对两者不做特别区分,统一使用「堆」来命名。
|
实际上,**堆通常用作实现优先队列,大顶堆相当于元素按从大到小顺序出队的优先队列**。从使用角度来看,我们可以将「优先队列」和「堆」看作等价的数据结构。因此,本书对两者不做特别区分,统一使用「堆」来命名。
|
||||||
|
|
||||||
堆的常用操作见下表,方法名需根据编程语言确定。
|
堆的常用操作见下表,方法名需要根据编程语言来确定。
|
||||||
|
|
||||||
<div class="center-table" markdown>
|
<div class="center-table" markdown>
|
||||||
|
|
||||||
|
@ -33,11 +33,11 @@
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
我们可以直接使用编程语言提供的堆类(或优先队列类)。
|
在实际应用中,我们可以直接使用编程语言提供的堆类(或优先队列类)。
|
||||||
|
|
||||||
!!! tip
|
!!! tip
|
||||||
|
|
||||||
类似于排序中“从小到大排列”和“从大到小排列”,“大顶堆”和“小顶堆”可仅通过修改 Comparator 来互相转换。
|
类似于排序算法中的“从小到大排列”和“从大到小排列”,我们可以通过修改 Comparator 来实现“小顶堆”与“大顶堆”之间的转换。
|
||||||
|
|
||||||
=== "Java"
|
=== "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)
|
![堆的表示与存储](heap.assets/representation_of_heap.png)
|
||||||
|
|
||||||
我们将索引映射公式封装成函数,以便后续使用。
|
我们可以将索引映射公式封装成函数,方便后续使用。
|
||||||
|
|
||||||
=== "Java"
|
=== "Java"
|
||||||
|
|
||||||
|
@ -419,7 +419,7 @@
|
||||||
|
|
||||||
### 访问堆顶元素
|
### 访问堆顶元素
|
||||||
|
|
||||||
堆顶元素是二叉树的根节点,即列表首元素。
|
堆顶元素即为二叉树的根节点,也就是列表的首个元素。
|
||||||
|
|
||||||
=== "Java"
|
=== "Java"
|
||||||
|
|
||||||
|
@ -483,9 +483,9 @@
|
||||||
|
|
||||||
### 元素入堆
|
### 元素入堆
|
||||||
|
|
||||||
给定元素 `val` ,我们先将其添加到堆底。添加后,由于 `val` 可能大于堆中其它元素,此时堆的成立条件可能已经被破坏,**因此需要修复从插入节点到根节点这条路径上的各个节点**,该操作被称为「堆化 Heapify」。
|
给定元素 `val` ,我们首先将其添加到堆底。添加之后,由于 val 可能大于堆中其他元素,堆的成立条件可能已被破坏。因此,**需要修复从插入节点到根节点的路径上的各个节点**,这个操作被称为「堆化 Heapify」。
|
||||||
|
|
||||||
考虑从入堆节点开始,**从底至顶执行堆化**。具体地,比较插入节点与其父节点的值,若插入节点更大则将它们交换;并循环以上操作,从底至顶地修复堆中的各个节点;直至越过根节点时结束,或当遇到无需交换的节点时提前结束。
|
考虑从入堆节点开始,**从底至顶执行堆化**。具体来说,我们比较插入节点与其父节点的值,如果插入节点更大,则将它们交换。然后继续执行此操作,从底至顶修复堆中的各个节点,直至越过根节点或遇到无需交换的节点时结束。
|
||||||
|
|
||||||
=== "<1>"
|
=== "<1>"
|
||||||
![元素入堆步骤](heap.assets/heap_push_step1.png)
|
![元素入堆步骤](heap.assets/heap_push_step1.png)
|
||||||
|
@ -505,7 +505,7 @@
|
||||||
=== "<6>"
|
=== "<6>"
|
||||||
![heap_push_step6](heap.assets/heap_push_step6.png)
|
![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"
|
=== "Java"
|
||||||
|
|
||||||
|
@ -589,13 +589,13 @@
|
||||||
|
|
||||||
### 堆顶元素出堆
|
### 堆顶元素出堆
|
||||||
|
|
||||||
堆顶元素是二叉树根节点,即列表首元素,如果我们直接将首元素从列表中删除,则二叉树中所有节点都会随之发生移位(索引发生变化),这样后续使用堆化修复就很麻烦了。为了尽量减少元素索引变动,采取以下操作步骤:
|
堆顶元素是二叉树的根节点,即列表首元素。如果我们直接从列表中删除首元素,那么二叉树中所有节点的索引都会发生变化,这将使得后续使用堆化修复变得困难。为了尽量减少元素索引的变动,我们采取以下操作步骤:
|
||||||
|
|
||||||
1. 交换堆顶元素与堆底元素(即交换根节点与最右叶节点);
|
1. 交换堆顶元素与堆底元素(即交换根节点与最右叶节点);
|
||||||
2. 交换完成后,将堆底从列表中删除(注意,因为已经交换,实际上删除的是原来的堆顶元素);
|
2. 交换完成后,将堆底从列表中删除(注意,由于已经交换,实际上删除的是原来的堆顶元素);
|
||||||
3. 从根节点开始,**从顶至底执行堆化**;
|
3. 从根节点开始,**从顶至底执行堆化**;
|
||||||
|
|
||||||
顾名思义,**从顶至底堆化的操作方向与从底至顶堆化相反**,我们比较根节点的值与其两个子节点的值,将最大的子节点与根节点执行交换,并循环以上操作,直到越过叶节点时结束,或当遇到无需交换的节点时提前结束。
|
顾名思义,**从顶至底堆化的操作方向与从底至顶堆化相反**,我们将根节点的值与其两个子节点的值进行比较,将最大的子节点与根节点交换;然后循环执行此操作,直到越过叶节点或遇到无需交换的节点时结束。
|
||||||
|
|
||||||
=== "<1>"
|
=== "<1>"
|
||||||
![堆顶元素出堆步骤](heap.assets/heap_pop_step1.png)
|
![堆顶元素出堆步骤](heap.assets/heap_pop_step1.png)
|
||||||
|
@ -627,7 +627,7 @@
|
||||||
=== "<10>"
|
=== "<10>"
|
||||||
![heap_pop_step10](heap.assets/heap_pop_step10.png)
|
![heap_pop_step10](heap.assets/heap_pop_step10.png)
|
||||||
|
|
||||||
与元素入堆操作类似,**堆顶元素出堆操作的时间复杂度为 $O(\log n)$** 。
|
与元素入堆操作相似,堆顶元素出堆操作的时间复杂度也为 $O(\log n)$。
|
||||||
|
|
||||||
=== "Java"
|
=== "Java"
|
||||||
|
|
||||||
|
@ -711,6 +711,6 @@
|
||||||
|
|
||||||
## 堆常见应用
|
## 堆常见应用
|
||||||
|
|
||||||
- **优先队列**。堆常作为实现优先队列的首选数据结构,入队和出队操作时间复杂度为 $O(\log n)$ ,建队操作为 $O(n)$ ,皆非常高效。
|
- **优先队列**:堆通常作为实现优先队列的首选数据结构,其入队和出队操作的时间复杂度均为 $O(\log n)$,而建队操作为 $O(n)$,这些操作都非常高效。
|
||||||
- **堆排序**。给定一组数据,我们使用其建堆,并依次全部弹出,则可以得到有序的序列。当然,堆排序一般无需弹出元素,仅需每轮将堆顶元素交换至数组尾部并减小堆的长度即可。
|
- **堆排序**:给定一组数据,我们可以用它们建立一个堆,然后依次将所有元素弹出,从而得到一个有序序列。当然,堆排序的实现方法并不需要弹出元素,而是每轮将堆顶元素交换至数组尾部并缩小堆的长度。
|
||||||
- **获取最大的 $k$ 个元素**。这既是一道经典算法题目,也是一种常见应用,例如选取热度前 10 的新闻作为微博热搜,选取前 10 销量的商品等。
|
- **获取最大的 $k$ 个元素**:这是一个经典的算法问题,同时也是一种典型应用,例如选择热度前 10 的新闻作为微博热搜,选取销量前 10 的商品等。
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
# 小结
|
# 小结
|
||||||
|
|
||||||
- 堆是一棵限定条件下的完全二叉树,根据成立条件可分为大顶堆和小顶堆。大(小)顶堆的堆顶元素最大(小)。
|
- 堆是一棵完全二叉树,根据成立条件可分为大顶堆和小顶堆。大(小)顶堆的堆顶元素是最大(小)的。
|
||||||
- 优先队列定义为一种具有出队优先级的队列。堆是实现优先队列的最常用数据结构。
|
- 优先队列的定义是具有出队优先级的队列,通常使用堆来实现。
|
||||||
- 堆的常用操作和对应时间复杂度为元素入堆 $O(\log n)$ 、堆顶元素出堆 $O(\log n)$ 、访问堆顶元素 $O(1)$ 等。
|
- 堆的常用操作及其对应的时间复杂度包括:元素入堆 $O(\log n)$ 、堆顶元素出堆 $O(\log n)$ 和访问堆顶元素 $O(1)$ 等。
|
||||||
- 完全二叉树非常适合用数组来表示,因此我们一般用数组来存储堆。
|
- 完全二叉树非常适合用数组表示,因此我们通常使用数组来存储堆。
|
||||||
- 堆化操作用于修复堆的特性,在入堆和出堆操作中都会使用到。
|
- 堆化操作用于维护堆的性质,在入堆和出堆操作中都会用到。
|
||||||
- 输入 $n$ 个元素并建堆的时间复杂度可以被优化至 $O(n)$ ,非常高效。
|
- 输入 $n$ 个元素并建堆的时间复杂度可以优化至 $O(n)$,非常高效。
|
||||||
|
|
|
@ -1,18 +1,18 @@
|
||||||
# 算法无处不在
|
# 算法无处不在
|
||||||
|
|
||||||
听到“算法”这个词,我们一般会联想到数学。但实际上,大多数算法并不包含复杂的数学,而更像是在考察基本逻辑,而这些逻辑在我们日常生活中处处可见。
|
当我们听到“算法”这个词时,很自然地会想到数学。然而实际上,许多算法并不涉及复杂数学,而是更多地依赖于基本逻辑,这些逻辑在我们的日常生活中处处可见。
|
||||||
|
|
||||||
在正式介绍算法之前,我想告诉你一件有趣的事:**其实,你在过去已经学会了很多算法,并且已经习惯将它们应用到日常生活中**。接下来,我将介绍两个具体例子来佐证。
|
在正式探讨算法之前,有一个有趣的事实值得分享:**实际上,你已经学会了许多算法,并习惯将他们应用到日常生活中了**。下面,我将举两个具体例子来证实这一点。
|
||||||
|
|
||||||
**例一:拼积木**。一套积木,除了有许多部件之外,还会附送详细的拼装说明书。我们按照说明书上一步步操作,即可拼出复杂的积木模型。
|
**例一:组装积木**。一套积木,除了包含许多零件之外,还附有详细的组装说明书。我们按照说明书一步步操作,就能组装出精美的积木模型。
|
||||||
|
|
||||||
如果从数据结构与算法的角度看,大大小小的「积木」就是数据结构,而「拼装说明书」上的一系列步骤就是算法。
|
从数据结构与算法的角度来看,积木的各种形状和连接方式代表数据结构,而组装说明书上的一系列步骤则是算法。
|
||||||
|
|
||||||
**例二:查字典**。在字典中,每个汉字都有一个对应的拼音,而字典是按照拼音的英文字母表顺序排列的。假设需要在字典中查询任意一个拼音首字母为 $r$ 的字,一般我们会这样做:
|
**例二:查阅字典**。在字典里,每个汉字都对应一个拼音,而字典是按照拼音的英文字母顺序排列的。假设我们需要查找一个拼音首字母为 $r$ 的字,通常会这样操作:
|
||||||
|
|
||||||
1. 打开字典大致一半页数的位置,查看此页的首字母是什么(假设为 $m$ );
|
1. 翻开字典约一半的页数,查看该页首字母是什么(假设为 $m$ );
|
||||||
2. 由于在英文字母表中 $r$ 在 $m$ 的后面,因此应排除字典前半部分,查找范围仅剩后半部分;
|
2. 由于在英文字母表中 $r$ 位于 $m$ 之后,所以排除字典前半部分,查找范围缩小到后半部分;
|
||||||
3. 循环执行步骤 1-2 ,直到找到拼音首字母为 $r$ 的页码时终止。
|
3. 不断重复步骤 1-2 ,直至找到拼音首字母为 $r$ 的页码为止。
|
||||||
|
|
||||||
=== "<1>"
|
=== "<1>"
|
||||||
![查字典步骤](algorithms_are_everywhere.assets/look_up_dictionary_step_1.png)
|
![查字典步骤](algorithms_are_everywhere.assets/look_up_dictionary_step_1.png)
|
||||||
|
@ -29,10 +29,10 @@
|
||||||
=== "<5>"
|
=== "<5>"
|
||||||
![look_up_dictionary_step_5](algorithms_are_everywhere.assets/look_up_dictionary_step_5.png)
|
![look_up_dictionary_step_5](algorithms_are_everywhere.assets/look_up_dictionary_step_5.png)
|
||||||
|
|
||||||
查字典这个小学生的标配技能,实际上就是大名鼎鼎的「二分查找」。从数据结构角度,我们可以将字典看作是一个已排序的「数组」;而从算法角度,我们可将上述查字典的一系列指令看作是「二分查找」算法。
|
查阅字典这个小学生必备技能,实际上就是著名的「二分查找」。从数据结构的角度,我们可以把字典视为一个已排序的「数组」;从算法的角度,我们可以将上述查字典的一系列操作看作是「二分查找」算法。
|
||||||
|
|
||||||
小到烹饪一道菜、大到星际航行,几乎所有问题的解决都离不开算法。计算机的出现,使我们可以通过编程将数据结构存储在内存中,也可以编写代码来调用 CPU, GPU 执行算法,从而将生活中的问题搬运到计算机中,更加高效地解决各式各样的复杂问题。
|
小到烹饪一道菜,大到星际航行,几乎所有问题的解决都离不开算法。计算机的出现使我们能够通过编程将数据结构存储在内存中,同时编写代码调用 CPU 和 GPU 执行算法。这样一来,我们就能把生活中的问题转移到计算机上,以更高效的方式解决各种复杂问题。
|
||||||
|
|
||||||
!!! tip
|
!!! tip
|
||||||
|
|
||||||
读到这里,如果你感到对数据结构、算法、数组、二分查找等此类概念一知半解,那么就太好了!因为这正是本书存在的价值,接下来,本书将会一步步地引导你进入数据结构与算法的知识殿堂。
|
阅读至此,如果你对数据结构、算法、数组和二分查找等概念仍感到一知半解,那么太好了!因为这正是本书存在的意义。接下来,这本书将一步步引导你深入数据结构与算法的知识殿堂。
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
# 小结
|
# 小结
|
||||||
|
|
||||||
- 算法在生活中随处可见,并不高深莫测。我们已经不知不觉地学习到许多“算法”,用于解决生活中大大小小的问题。
|
- 算法在日常生活中无处不在,并不是遥不可及的高深知识。实际上,我们已经在不知不觉中学习了许多“算法”,用以解决生活中的大小问题。
|
||||||
- “查字典”的原理和二分查找算法一致。二分体现分而治之的重要算法思想。
|
- 查阅字典的原理与二分查找算法相一致。二分查找体现了分而治之的重要算法思想。
|
||||||
- 算法是在有限时间内解决特定问题的一组指令或操作步骤,数据结构是在计算机中组织与存储数据的方式。
|
- 算法是在有限时间内解决特定问题的一组指令或操作步骤,而数据结构是计算机中组织和存储数据的方式。
|
||||||
- 数据结构与算法两者紧密联系。数据结构是算法的底座,算法是发挥数据结构的舞台。
|
- 数据结构与算法紧密相连。数据结构是算法的基石,而算法则是发挥数据结构作用的舞台。
|
||||||
- 乐高积木对应数据,积木形状和连接形式对应数据结构,拼装积木的流程步骤对应算法。
|
- 乐高积木对应于数据,积木形状和连接方式代表数据结构,拼装积木的步骤则对应算法。
|
||||||
|
|
|
@ -4,32 +4,31 @@
|
||||||
|
|
||||||
「算法 Algorithm」是在有限时间内解决特定问题的一组指令或操作步骤。算法具有以下特性:
|
「算法 Algorithm」是在有限时间内解决特定问题的一组指令或操作步骤。算法具有以下特性:
|
||||||
|
|
||||||
- 问题是明确的,需要拥有明确的输入和输出定义。
|
- 问题是明确的,具有清晰的输入和输出定义。
|
||||||
- 解具有确定性,即给定相同输入时,输出一定相同。
|
- 解具有确定性,即给定相同的输入时,输出始终相同。
|
||||||
- 具有可行性,可在有限步骤、有限时间、有限内存空间下完成。
|
- 具有可行性,在有限步骤、时间和内存空间下可完成。
|
||||||
- 独立于编程语言,即可用多种语言实现。
|
|
||||||
|
|
||||||
## 数据结构定义
|
## 数据结构定义
|
||||||
|
|
||||||
「数据结构 Data Structure」是在计算机中组织与存储数据的方式。为了提高数据存储和操作性能,数据结构的设计原则有:
|
「数据结构 Data Structure」是计算机中组织和存储数据的方式。为了提高数据存储和操作性能,数据结构的设计目标包括:
|
||||||
|
|
||||||
- 空间占用尽可能小,节省计算机内存。
|
- 空间占用尽量减少,节省计算机内存。
|
||||||
- 数据操作尽量快,包括数据访问、添加、删除、更新等。
|
- 数据操作尽可能快速,涵盖数据访问、添加、删除、更新等。
|
||||||
- 提供简洁的数据表示和逻辑信息,以便算法高效运行。
|
- 提供简洁的数据表示和逻辑信息,以利于算法高效运行。
|
||||||
|
|
||||||
数据结构的设计是一个充满权衡的过程,这意味着如果获得某方面的优势,则往往需要在另一方面做出妥协。例如,链表相对于数组,数据添加删除操作更加方便,但牺牲了数据的访问速度;图相对于链表,提供了更多的逻辑信息,但需要占用更多的内存空间。
|
数据结构设计是一个充满权衡的过程,这意味着要在某方面取得优势,往往需要在另一方面作出妥协。例如,链表相较于数组,在数据添加和删除操作上更加便捷,但牺牲了数据访问速度;图相较于链表,提供了更丰富的逻辑信息,但需要占用更大的内存空间。
|
||||||
|
|
||||||
## 数据结构与算法的关系
|
## 数据结构与算法的关系
|
||||||
|
|
||||||
「数据结构」与「算法」是高度相关、紧密嵌合的,体现在:
|
「数据结构」与「算法」高度相关且紧密结合,具体表现在:
|
||||||
|
|
||||||
- 数据结构是算法的底座。数据结构为算法提供结构化存储的数据,以及操作数据的对应方法。
|
- 数据结构是算法的基石。数据结构为算法提供了结构化存储的数据,以及用于操作数据的方法。
|
||||||
- 算法是数据结构发挥的舞台。数据结构仅存储数据信息,结合算法才可解决特定问题。
|
- 算法是数据结构发挥的舞台。数据结构本身仅存储数据信息,通过结合算法才能解决特定问题。
|
||||||
- 算法有对应最优的数据结构。给定算法,一般可基于不同的数据结构实现,而最终执行效率往往相差很大。
|
- 特定算法通常有对应最优的数据结构。算法通常可以基于不同的数据结构进行实现,但最终执行效率可能相差很大。
|
||||||
|
|
||||||
![数据结构与算法的关系](what_is_dsa.assets/relationship_between_data_structure_and_algorithm.png)
|
![数据结构与算法的关系](what_is_dsa.assets/relationship_between_data_structure_and_algorithm.png)
|
||||||
|
|
||||||
如果将「LEGO 乐高」类比到「数据结构与算法」,那么可以得到下表所示的对应关系。
|
类比「LEGO 乐高」和「数据结构与算法」,则对应关系如下表所示。
|
||||||
|
|
||||||
<div class="center-table" markdown>
|
<div class="center-table" markdown>
|
||||||
|
|
||||||
|
@ -42,6 +41,8 @@
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
值得注意的是,数据结构与算法独立于编程语言。正因如此,本书得以提供多种编程语言的实现。
|
||||||
|
|
||||||
!!! tip "约定俗成的简称"
|
!!! tip "约定俗成的简称"
|
||||||
|
|
||||||
在实际讨论中,我们通常会将「数据结构与算法」直接简称为「算法」。例如,我们熟称的 LeetCode 算法题目,实际上同时考察了数据结构和算法两部分知识。
|
在实际讨论时,我们通常会将「数据结构与算法」简称为「算法」。例如,众所周知的 LeetCode 算法题目,实际上同时考察了数据结构和算法两方面的知识。
|
||||||
|
|
|
@ -1,18 +1,18 @@
|
||||||
# 关于本书
|
# 关于本书
|
||||||
|
|
||||||
本项目致力于构建一本开源免费、新手友好的数据结构与算法入门书。
|
本项目旨在创建一本开源免费、新手友好的数据结构与算法入门教程。
|
||||||
|
|
||||||
- 全书采用动画图解,结构化地讲解数据结构与算法知识,内容清晰易懂、学习曲线平滑;
|
- 全书采用动画图解,结构化地讲解数据结构与算法知识,内容清晰易懂、学习曲线平滑;
|
||||||
- 算法源代码皆可一键运行,支持 Java, C++, Python, Go, JS, TS, C#, Swift, Zig 等语言;
|
- 算法源代码皆可一键运行,支持 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 "前置条件"
|
!!! success "前置条件"
|
||||||
|
|
||||||
|
@ -20,26 +20,26 @@
|
||||||
|
|
||||||
## 内容结构
|
## 内容结构
|
||||||
|
|
||||||
本书主要内容有:
|
本书主要内容包括:
|
||||||
|
|
||||||
- **复杂度分析**:数据结构与算法的评价维度、算法效率的评估方法。时间复杂度、空间复杂度,包括推算方法、常见类型、示例等。
|
- **复杂度分析**:数据结构与算法的评价维度、算法效率的评估方法。时间复杂度、空间复杂度,包括推算方法、常见类型、示例等。
|
||||||
- **数据结构**:常用的基本数据类型,数据在内存中的存储方式、数据结构分类方法。数组、链表、栈、队列、散列表、树、堆、图等数据结构,内容包括定义、优劣势、常用操作、常见类型、典型应用、实现方法等。
|
- **数据结构**:常见基本数据类型,数据在内存中的存储形式、数据结构的分类方法。涉及数组、链表、栈、队列、散列表、树、堆、图等数据结构,内容包括定义、优缺点、常用操作、常见类型、典型应用、实现方法等。
|
||||||
- **算法**:查找算法、排序算法、搜索与回溯、动态规划、分治算法,内容包括定义、使用场景、优劣势、时空效率、实现方法、示例题目等。
|
- **算法**:查找算法、排序算法、搜索与回溯、动态规划、分治算法等,内容涵盖定义、应用场景、优缺点、时空效率、实现方法、示例题目等。
|
||||||
|
|
||||||
![Hello 算法内容结构](about_the_book.assets/hello_algo_mindmap.png)
|
![Hello 算法内容结构](about_the_book.assets/hello_algo_mindmap.png)
|
||||||
|
|
||||||
## 致谢
|
## 致谢
|
||||||
|
|
||||||
本书的成书过程中,我获得了许多人的帮助,包括但不限于:
|
在本书的创作过程中,我得到了许多人的帮助,包括但不限于:
|
||||||
|
|
||||||
- 感谢我在公司的导师李汐博士,在一次畅谈时您告诉我“觉得应该做就去做”,坚定了我写这本书的决心。
|
- 感谢我在公司的导师李汐博士,在深入交谈中您鼓励我“行动起来”,坚定了我写这本书的决心。
|
||||||
- 感谢我的女朋友泡泡担任本书的首位读者,从算法小白的视角提出了许多建议,使这本书更加适合初学者来阅读。
|
- 感谢我的女朋友泡泡作为本书的首位读者,从算法小白的角度提出许多宝贵建议,使得本书更适合新手阅读。
|
||||||
- 感谢腾宝、琦宝、飞宝为本书起了个好听又有梗名字,直接唤起我最初敲下第一行代码 "Hello World!" 的回忆。
|
- 感谢腾宝、琦宝、飞宝为本书起了一个富有创意的名字,唤起大家写下第一行代码 "Hello World!" 的美好回忆。
|
||||||
- 感谢苏潼为本书设计了封面和 LOGO ,在我的强迫症下前后多次帮忙修改,谢谢你的耐心。
|
- 感谢苏潼为本书设计了精美的封面和 LOGO,并在我的强迫症下多次耐心修改。
|
||||||
- 感谢 @squidfunk 给出的写作排版建议,以及优秀开源项目 [Material-for-MkDocs](https://github.com/squidfunk/mkdocs-material/tree/master) 。
|
- 感谢 @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)。
|
||||||
|
|
||||||
感谢父母,你们一贯的支持与鼓励给了我自由度来做这些有趣的事。
|
衷心感谢我的父母,正是你们一直以来的支持与鼓励,让我有机会做这些富有趣味的事。
|
||||||
|
|
|
@ -1,26 +1,28 @@
|
||||||
# 如何使用本书
|
# 如何使用本书
|
||||||
|
|
||||||
建议通读本节内容,以获取最佳阅读体验。
|
!!! tip
|
||||||
|
|
||||||
|
为了获得最佳的阅读体验,建议您通读本节内容。
|
||||||
|
|
||||||
## 算法学习路线
|
## 算法学习路线
|
||||||
|
|
||||||
总体上看,我认为可将学习数据结构与算法的过程分为三个阶段。
|
从总体上看,我们可以将学习数据结构与算法的过程划分为三个阶段:
|
||||||
|
|
||||||
1. **算法入门**。熟悉各种数据结构的特点、用法,学习各种算法的原理、流程、用途、效率等。
|
1. **算法入门**。我们需要熟悉各种数据结构的特点和用法,学习不同算法的原理、流程、用途和效率等方面内容。
|
||||||
2. **刷算法题**。可以先从热门题单开刷,推荐[剑指 Offer](https://leetcode.cn/problem-list/xb9nqhhg/)、[LeetCode Hot 100](https://leetcode.cn/problem-list/2cktkvj/),先积累至少 100 道题量,熟悉大多数的算法问题。刚开始刷题时,“遗忘”是最大的困扰点,但这是很正常的,请不要担心。学习中有一种概念叫“周期性回顾”,同一道题隔段时间做一次,在重复 3 轮以上后,往往就能牢记于心了。
|
2. **刷算法题**。建议从热门题目开刷,如[剑指 Offer](https://leetcode.cn/problem-list/xb9nqhhg/)和[LeetCode Hot 100](https://leetcode.cn/problem-list/2cktkvj/),先积累至少 100 道题目,熟悉主流的算法问题。初次刷题时,“知识遗忘”可能是一个挑战,但请放心,这是很正常的。我们可以按照“艾宾浩斯遗忘曲线”来复习题目,通常在进行 3-5 轮的重复后,就能将其牢记在心。
|
||||||
3. **搭建知识体系**。在学习方面,可以阅读算法专栏文章、解题框架、算法教材,不断地丰富知识体系。在刷题方面,可以开始采用进阶刷题方案,例如按专题分类、一题多解、一解多题等,相关刷题心得可以在各个社区中找到。
|
3. **搭建知识体系**。在学习方面,我们可以阅读算法专栏文章、解题框架和算法教材,以不断丰富知识体系。在刷题方面,可以尝试采用进阶刷题策略,如按专题分类、一题多解、一解多题等,相关的刷题心得可以在各个社区找到。
|
||||||
|
|
||||||
作为一本入门教程,**本书内容主要对应“第一阶段”**,致力于帮助你更高效地开展第二、三阶段的学习。
|
作为一本入门教程,本书内容主要涵盖“第一阶段”,旨在帮助你更高效地展开第二和第三阶段的学习。
|
||||||
|
|
||||||
![算法学习路线](suggestions.assets/learning_route.png)
|
![算法学习路线](suggestions.assets/learning_route.png)
|
||||||
|
|
||||||
## 行文风格约定
|
## 行文风格约定
|
||||||
|
|
||||||
标题后标注 `*` 的是选读章节,内容相对较难。如果你的时间有限,建议可以先跳过。
|
标题后标注 `*` 的是选读章节,内容相对困难。如果你的时间有限,建议可以先跳过。
|
||||||
|
|
||||||
文章中的重要名词会用 `「括号」` 标注,例如 `「数组 Array」` 。建议记住这些名词,包括英文翻译,以便后续阅读文献时使用。
|
文章中的重要名词会用 `「 」` 括号标注,例如 `「数组 Array」` 。请务必记住这些名词,包括英文翻译,以便后续阅读文献时使用。
|
||||||
|
|
||||||
重点内容、总起句、总结句会被 **加粗** ,此类文字值得特别关注。
|
**加粗的文字** 表示重点内容或总结性语句,这类文字值得特别关注。
|
||||||
|
|
||||||
专有名词和有特指含义的词句会使用 `“双引号”` 标注,以避免歧义。
|
专有名词和有特指含义的词句会使用 `“双引号”` 标注,以避免歧义。
|
||||||
|
|
||||||
|
@ -156,41 +158,40 @@
|
||||||
|
|
||||||
## 在动画图解中高效学习
|
## 在动画图解中高效学习
|
||||||
|
|
||||||
视频和图片相比于文字的信息密度和结构化程度更高,更容易理解。在本书中,**知识重难点会主要以动画、图解的形式呈现**,而文字的作用则是作为动画和图的解释与补充。
|
相较于文字,视频和图片具有更高的信息密度和结构化程度,因此更易于理解。在本书中,**重点和难点知识将主要通过动画和图解形式展示**,而文字则作为动画和图片的解释与补充。
|
||||||
|
|
||||||
阅读本书时,若发现某段内容提供了动画或图解,**建议你以图为主线**,将文字内容(一般在图的上方)对齐到图中内容,综合来理解。
|
在阅读本书时,如果发现某段内容提供了动画或图解,**建议以图为主线**,以文字(通常位于图像上方)为辅,综合两者来理解内容。
|
||||||
|
|
||||||
![动画图解示例](suggestions.assets/animation.gif)
|
![动画图解示例](suggestions.assets/animation.gif)
|
||||||
|
|
||||||
## 在代码实践中加深理解
|
## 在代码实践中加深理解
|
||||||
|
|
||||||
本书的配套代码托管在[GitHub 仓库](https://github.com/krahets/hello-algo),**源代码包含详细注释,配有测试样例,可以直接运行**。
|
本书的配套代码托管在[GitHub 仓库](https://github.com/krahets/hello-algo),**源代码包含详细注释,并附有测试样例,可直接运行**。
|
||||||
|
|
||||||
- 若学习时间紧张,**建议至少将所有代码通读并运行一遍**。
|
如果学习时间有限,建议你至少通读并运行所有代码。如果时间充裕,**建议参照代码自行敲一遍**。与仅阅读代码相比,编写代码的过程往往能带来更多收获。
|
||||||
- 若时间允许,**强烈建议对照着代码自己敲一遍**。相比于读代码,写代码的过程往往能带来新的收获。
|
|
||||||
|
|
||||||
![运行代码示例](suggestions.assets/running_code.gif)
|
![运行代码示例](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
|
```shell
|
||||||
git clone https://github.com/krahets/hello-algo.git
|
git clone https://github.com/krahets/hello-algo.git
|
||||||
```
|
```
|
||||||
|
|
||||||
当然,你也可以点击“Download ZIP”直接下载代码压缩包,本地解压即可。
|
当然,你也可以点击“Download ZIP”直接下载代码压缩包,然后在本地解压即可。
|
||||||
|
|
||||||
![克隆仓库与下载代码](suggestions.assets/download_code.png)
|
![克隆仓库与下载代码](suggestions.assets/download_code.png)
|
||||||
|
|
||||||
**第三步:运行源代码**。若代码块的顶部标有文件名称,则可在仓库 `codes` 文件夹中找到对应的 **源代码文件**。源代码文件可以帮助你省去不必要的调试时间,将精力集中在学习内容上。
|
**第三步:运行源代码**。如果代码块顶部标有文件名称,则可以在仓库的 `codes` 文件夹中找到相应的源代码文件。源代码文件将帮助你节省不必要的调试时间,让你能够专注于学习内容。
|
||||||
|
|
||||||
![代码块与对应的源代码文件](suggestions.assets/code_md_to_repo.png)
|
![代码块与对应的源代码文件](suggestions.assets/code_md_to_repo.png)
|
||||||
|
|
||||||
## 在提问讨论中共同成长
|
## 在提问讨论中共同成长
|
||||||
|
|
||||||
阅读本书时,请不要“惯着”那些弄不明白的知识点。**欢迎在评论区留下你的问题**,小伙伴们和我都会给予解答,您一般 2 日内会得到回复。
|
阅读本书时,请不要“惯着”那些没学明白的知识点。**欢迎在评论区提出你的问题**,我和其他小伙伴们将竭诚为你解答,一般情况下可在两天内得到回复。
|
||||||
|
|
||||||
同时,也希望你可以多花时间逛逛评论区。一方面,可以看看大家遇到了什么问题,反过来查漏补缺,这往往可以引起更加深度的思考。另一方面,也希望你可以慷慨地解答小伙伴们的问题、分享自己的见解,大家互相学习与进步!
|
同时,也希望您能在评论区多花些时间。一方面,您可以了解大家遇到的问题,从而查漏补缺,这将有助于激发更深入的思考。另一方面,希望您能慷慨地回答其他小伙伴的问题、分享您的见解,让大家共同学习和进步。
|
||||||
|
|
||||||
![评论区示例](suggestions.assets/comment.gif)
|
![评论区示例](suggestions.assets/comment.gif)
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
# 小结
|
# 小结
|
||||||
|
|
||||||
- 本书主要面向算法初学者。对于已经有一定积累的同学,这本书可以帮助你系统回顾算法知识,源代码可被当作“刷题工具库”来使用。
|
- 本书的主要受众是算法初学者。对于已具备一定积累的同学,本书能帮助系统回顾算法知识,同时源代码可作为“刷题工具库”使用。
|
||||||
- 书中内容主要分为复杂度分析、数据结构、算法三部分,覆盖了该领域的大部分主题。
|
- 书中内容主要包括复杂度分析、数据结构、算法三部分,涵盖了该领域的绝大部分主题。
|
||||||
- 对于算法小白,在初学阶段阅读一本入门书是非常有必要的,可以少走许多弯路。
|
- 对于算法新手,在初学阶段阅读一本入门书籍至关重要,有助于避免走弯路。
|
||||||
- 书内的动画和图解往往介绍的是重点和难点知识,在阅读时应该多加关注。
|
- 书内的动画和图解通常用于介绍重点和难点知识,阅读时应给予更多关注。
|
||||||
- 实践是学习编程的最佳方式,强烈推荐运行源代码,动手敲代码。
|
- 实践乃学习编程之最佳途径,强烈建议运行源代码并亲自敲打代码。
|
||||||
- 本书提供了讨论区,遇到疑惑可以随时提问。
|
- 本书设有讨论区,欢迎随时分享你的疑惑。
|
||||||
|
|
|
@ -4,11 +4,11 @@
|
||||||
|
|
||||||
[2] Aditya Bhargava. Grokking Algorithms: An Illustrated Guide for Programmers and Other Curious People (1st Edition).
|
[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] 程杰. 大话数据结构.
|
[6] 程杰. 大话数据结构.
|
||||||
|
|
||||||
|
|
|
@ -10,4 +10,4 @@
|
||||||
|
|
||||||
![排序算法对比](summary.assets/sorting_algorithms_comparison.png)
|
![排序算法对比](summary.assets/sorting_algorithms_comparison.png)
|
||||||
|
|
||||||
- 总体来看,我们追求运行快、稳定、原地、正向自适应性的排序。显然,如同其它数据结构与算法一样,同时满足这些条件的排序算法并不存在,我们需要根据问题特点来选择排序算法。
|
- 总体来看,我们追求运行快、稳定、原地、正向自适应性的排序。显然,如同其他数据结构与算法一样,同时满足这些条件的排序算法并不存在,我们需要根据问题特点来选择排序算法。
|
||||||
|
|
Loading…
Reference in a new issue