Initial commit

This commit is contained in:
lifei
2020-11-28 23:53:03 +08:00
commit c81f767014
8 changed files with 662 additions and 0 deletions

36
.gitattributes vendored Normal file
View File

@@ -0,0 +1,36 @@
# Sources
*.c text diff=c
*.cc text diff=cpp
*.cxx text diff=cpp
*.cpp text diff=cpp
*.c++ text diff=cpp
*.hpp text diff=cpp
*.h text diff=c
*.h++ text diff=cpp
*.hh text diff=cpp
# Compiled Object files
*.slo binary
*.lo binary
*.o binary
*.obj binary
# Precompiled Headers
*.gch binary
*.pch binary
# Compiled Dynamic libraries
*.so binary
*.dylib binary
*.dll binary
# Compiled Static libraries
*.lai binary
*.la binary
*.a binary
*.lib binary
# Executables
*.exe binary
*.out binary
*.app binary

32
.gitignore vendored Normal file
View File

@@ -0,0 +1,32 @@
# Prerequisites
*.d
# Compiled Object files
*.slo
*.lo
*.o
*.obj
# Precompiled Headers
*.gch
*.pch
# Compiled Dynamic libraries
*.so
*.dylib
*.dll
# Fortran module files
*.mod
*.smod
# Compiled Static libraries
*.lai
*.la
*.a
*.lib
# Executables
*.exe
*.out
*.app

246
README.md Normal file
View File

@@ -0,0 +1,246 @@
# [王道考研 数据结构](https://www.bilibili.com/video/BV1b7411N798)
## 1. 基本概念
- 基本概念
- 数据
- 数据元素、数据项
- 数据对象、数据结构
- 数据类型、抽象数据类型ADT
- 三要素
- **逻辑结构**
- 集合
- 线性结构
- 树形结构
- 图状结构(网状结构)
- **物理结构**(存储结构)
- 顺序存储
- 链式存储
- 索引存储
- 散列存储
- **数据的运算**
逻辑结构:
- 线性结构
- 非线性结构
存储结构:
- 顺序存储
- 非顺序存储
数据的存储结构:
- 如果采用顺序存储,则各个数据元素在物理上必须是联系的;若采用非顺序存储,则各个数据元素在物理上可以是离散的。
- 数据的存储结构会影响存储空间分配的方便程度。
- 数据的存储结构会影响对数据运算的速度。
数据的运算:
- 运算的定义是针对逻辑结构的。
- 运算的实现是针对存储结构的。
数据类型是一个值的集合和定义在此几何上的一组操作的总称。
- 原子类型。其值不可再分的数据类型。
- 结构类型。其值可以再分解为若干成分(分量)的数据类型
抽象数据类型Abstract Data TypeADT是抽象数据组织及与之相关的操作。
抽象数据类型只关心:
- 逻辑结构
- 数据的运算
在讨论一种数据结构时:
1. 定义逻辑结构,数据元素之间的关系
2. 定义数据的运算,针对现实需求,应该对这种逻辑结构进行什么样的运算
3. 确定某种存储结构,实现数据结构,并实现一些对数据结构的基本运算
## 2. 算法
程序 = 数据结构 + 算法
- 数据结构:如何把现实世界的问题信息化,将信息存进计算机。同时还要实现对数据结构的基本操作。
- 算法:如何处理这些信息,以解决解决实际问题。
算法的特性:
- 有穷性:一个算法必须总在执行有穷步之后结束,且每一步都可在有穷时间内完成。
- 确定性:算法中每条指令必须有明确的含义,对于相同的输入只能得到相同输出。
- 可行性:算法中描述的操作都可以通过已经实现的基本运算执行有限次来实现。
- 输入:一个算法有零个或多个输入,这些输入取自于某个特定的对象的集合。
- 输出:一个算法有一个或多个输出,这些输出是与输入有着某种也行关系的量。
算法必须是有穷的,而程序可以是无穷的。
“好”算法的特质:
- 正确性:能正确解决问题。
- 可读性:无歧义地描述每步操作。
- 健壮性:输入非法数据时,算法能够适当地做出反应或进行处理,而不会产生莫名其妙的输出结果。
- 高效率和低存储量需求:执行速度快,时间复杂度低;不费内存,空间复杂度低。
### 2.1. 时间复杂度
时间开销 $T(n)$ 与问题规模 $n$ 的关系。
1. 找到一个基本操作(最深层循环、递归型循环)
2. 分析改基本操作的执行次数 $x$ 与问题规模 $n$ 的关系 $x=f(n)$
3. $x$ 的数量级 $O(x)$ 就是算法时间复杂度 $T(n)$
用 $O$ 表示同阶,同等数量级。即当 $n\to+\infty$ 时,二者之比为常数。
- $T_1(n)=O(n)$
- $T_2(n)=O(n^2)$
- $T_3(n)=O(n^3)$
$$
T(n)=O(f(n))
\iff
\lim \limits_{n\to+\infty} \frac{T(n)}{f(n)}=k
$$
**加法规则**(只保留更高阶的项):
$$
T(n)
=T_1(n)+T_2(n)
=O(f(n)) + O(g(n))
=O(max(f(n),g(n)))
$$
**乘法规则**
$$
T(n)
=T_1(n) \times T_2(n)
=O(f(n)) \times O(g(n))
=O(f(n) \times g(n))
$$
举例:
$$
\begin{aligned}
T_3(n)
&=n^3+n^2log_2n \\
&=O(n^3)+O(n^2log_2n) \\
&=O(n^3)
\end{aligned}
$$
算法的时间复杂度(**常对幂指阶**
$$
O(1)
\lt
O(log_2n)
\lt
O(n)
\lt
O(nlog_2n)
\lt
O(n^2)
\lt
O(n^3)
\lt
O(2^n)
\lt
O(n!)
\lt
O(n^n)
$$
- 结论一:顺序执行的代码只会影响常数项,可以忽略。
- 结论二:只需挑循环中的一个基本操作,分析它的执行次数与 $n$ 的关系即可。
- 结论三:如果有多层嵌套循环,只需关注最深层循环循环了几次。
查找长度为 $n$ 的数组中的一个元素:
最好情况
$$
T(n)=O(1)
$$
最坏情况
$$
T(n)=O(n)
$$
平均情况:被查找的元素出现在任意位置的概率相同,都为 $\frac{1}{n}$
$$
T(n)
=(1+2+3+...+n) \times \frac{1}{n}
=\frac{1+n}{2}
=O(n)
$$
计算时间复杂度情况:
- 最好时间复杂度
- **最坏时间复杂度**
- **平均时间复杂度**
> 算法的性能问题只有在 $n$ 很大时($n\to+\infty$)才会暴露出来。
### 2.2. 空间复杂度
空间开销(内存开销) $S(n)$ 与问题规模 $n$ 的关系。
普通程序只需要关注存储空间大小与问题规模相关的变量:
1. 找到所占空间大小与问题规模相关的变量。
2. 分析所占空间 $x$ 与问题规模 $n$ 的关系 $x=f(n)$。
3. $x$ 的数量级 $O(x)$ 就是算法时间复杂度 $S(n)$。
函数递归调用带来的内存开销。(函数调用栈)
空间复杂度 = 递归调用的深度。也有的情况,每一层递归调用的空间大小不一样。
递归程序:
1. 找到递归调用的深度 $x$ 与问题规模 $n$ 的关系 $x=f(n)$。
2. $x$ 的数量级 $O(x)$ 就是算法时间复杂度 $S(n)$。
3. 有的算法各层函数所需的存储空间不同,分析方法略有差别。
加法规则:
$$
O(f(n)) + O(g(n))
=O(max(f(n),g(n)))
$$
乘法规则:
$$
O(f(n)) \times O(g(n))
=O(f(n) \times g(n))
$$
常对幂指阶:
$$
O(1)
\lt
O(log_2n)
\lt
O(n)
\lt
O(nlog_2n)
\lt
O(n^2)
\lt
O(n^3)
\lt
O(2^n)
\lt
O(n!)
\lt
O(n^n)
$$

189
ch2.md Normal file
View File

@@ -0,0 +1,189 @@
# 线性表
## 1. 定义
线性表Linear List是具有相同数据类型的 $n$ $(n \geq 0)$ 个数据元素的有序序列。
- **相同数据类型**
- **有序**
- **序列**
其中 $n$ 为**表长**。当 $n=0$ 时线性表是一个**空表**。
$$
L=(\alpha_1,\alpha_2,...,\alpha_i,\alpha_{i+1},...,\alpha_n)
$$
- $\alpha_i$ 是线性表中的 “第 $i$ 个” 元素线性表中的**位序**。
- $\alpha_1$ 是**表头元素**$\alpha_n$ 是**表尾元素**。
- 出第一个元素外,每个元素有且仅有一个**直接前驱**;除最后一个元素外,每个元素有且仅有一个**直接后继**。
> 注意:位序是从 $1$ 开始的,而数组下标是从 $0$ 开始的。
## 2. 基本操作
- `InitList(&L)`:初始化表。构造一个空的线性表 $L$,分配内存空间。
- `DestroyList(&L)`:销毁操作。销毁线性表,并释放线性表 $L$ 所占用的内存。
- `ListInsert(&L,i,e)`:插入操作。在表 $L$ 中的第 $i$ 个位置插入指定元素 $e$。
- `ListDelete(&L,i,&e)`:插入操作。删除表 $L$ 中的第 $i$ 个位置的元素,并用 $e$ 返回删除元素的值。
- `LocateElem(L,e)`:按值查找操作。在表 $L$ 中查找查找具有给行关键字值的元素。
- `GetElem(L,i)`:按位查找操作。获取表 $L$ 中的第 $i$ 个位置的元素的值。
- `Length(L)`:求表长。返回线性表 $L$ 的长度,即 $L$ 中所有元素的个数。
- `PrintList(L)`:输出操作。按前后顺粗输出线性表 $L$ 的所有元素值。
- `Empty(L)`:判空操作。若 $L$ 为空表,则返回 `true`,否则返回 `false`
Tips
- 对数据的操作(记忆思路):创销、增删改查。
- C 语言函数的定义:<返回值类型> 函数名(<参数 1 类型> 参数 1, <参数 2 类型> 参数 2, ...)。
- 实际开发中,可根据实际需求定义其他的基本操作。
- 函数名和参数的形式、命名都可改变(参考:严蔚敏《数据结构》)。(可读性)
- 什么时候要传入引用 `&`:对参数的修改结果需要 “带回来”。C++ 语法)
为什么要实现对数据结构的基本操作:
- 团队合作编程,你定义的数据结构要让别人能够很方便的使用(封装)。
- 将常用的操作、运算封装成函数,避免重复工作,降低出错风险。
存储/物理结构:
- 顺序表(顺序存储)
- 链表(链式存储)
## 3. 顺序表
顺序表:用顺序存储方式实现的线性表。
顺序存储:把逻辑上相邻的元素存储在物理位置上也相邻的存储单元中,元素之间的关系由存储单元的邻接关系来体现。
- $LOC(L)$
- $LOC(L)+sizeof(ElemType)$
- $LOC(L)+2*sizeof(ElemType)$
- $...$
### 3.1. 静态分配
```c
#define MaxSize 10
typedef struct
{
ElemType data[MaxSize];
int length;
} SqList;
```
$MaxSize*sizeof(ElemType)$
- 使用“静态数组”实现
- 大小一旦确定就无法改变
### 3.2. 动态分配
```c
#define InitSize 10
typedef struct
{
ElemType *data;
int MaxSize;
int length;
} SeqList;
```
- `malloc`:动态申请一整片连续的内存空间
- `free`:释放内存空间
```c
L.data = (ElemType *) malloc(sizeof(ElemType) * InitSize)
```
C++ 可以使用 `new``delete` 关键字。
- 使用“动态数组”实现
- `L.data = (ElemType *) malloc(sizeof(ElemType) * InitSize)`
- 顺序表存满时,可再用 `malloc` 动态扩展顺序表的最大容量
- 需要将数据元素复制到新的存储区域,并用 `free` 函数释放原区域
### 3.3. 顺序表的特点
- **随机访问**:即可以在 $O(1)$ 时间内找到第 $i$ 个元素。
- 存储密度高:每个节点只存储数据元素。
- 扩展容量不方便:静态分配不能扩展容量,动态分配可以扩展但时间复杂度高。
- 插入、删除元素操作不方便,需要移动大量元素。
### 3.4. 插入操作
$$
L=(\alpha_1,\alpha_2,\alpha_3,\alpha_4,\alpha_5)
$$
$$
[a_0,a_1,a_2,a_3,a_4]
$$
```cpp
bool ListInsert(SqList &L, int i, int e)
{
// i 的值必须是合法的位序
if (i < 1 || i > L.length + 1)
{
return false;
}
// 如果存储空间已满,则不能插入
if (L.length >= MaxSize)
{
return false;
}
// 插入操作
for (int j = L.length; j >= i; j--)
{
L.data[j] = L.data[j - 1];
}
L.data[i - 1] = e;
L.length++;
return true;
}
```
插入位置之后的元素都要后移。
- 最好时间复杂度:$O(1)$
- 最坏时间复杂度:$O(n)$
- 平均时间复杂度:$O(n)$
### 3.5. 删除操作
```cpp
bool ListDelete(SqList &L, int i, int &e)
{
// i 的值必须是合法的位序
if (i < 1 || i > L.length)
{
return false;
}
// 删除操作
e = L.data[i - 1];
for (int j = i; j < L.length; j++)
{
L.data[j - 1] = L.data[j];
}
L.length--;
return true;
}
```
删除位置之后的元素都要前移。
- 最好时间复杂度:$O(1)$
- 最坏时间复杂度:$O(n)$
- 平均时间复杂度:$O(n)$
代码要点:
- 代码中注意位序 $i$ 与数组下标的区别。
- 算法要有健壮性,注意判断 $i$ 的合法性。
- 移动元素时,从靠前的元素开始?还是从表尾元素开始?
- 分析代码,理解为什么有的参数需要加 `&`C++ 语法中的引用)。
### 3.6. 按位查找
### 3.7. 按值查找

39
ch2/sequence/dynamic.c Normal file
View File

@@ -0,0 +1,39 @@
#include <stdlib.h>
#define InitSize 10
typedef struct
{
int *data;
int MaxSize;
int length;
} SeqList;
// 初始化顺序表,动态分配
void InitList(SeqList &L)
{
// 用 malloc 函数申请一篇连续的存储空间
L.data = (int *)malloc(InitSize * sizeof(int));
L.length = 0;
L.MaxSize = InitSize;
}
// 增加动态数组的长度
void IncreaseSize(SeqList &L, int len)
{
int *p = L.data;
L.data = (int *)malloc((L.MaxSize + len) * sizeof(int));
for (int i = 0; i < L.length; i++)
{
L.data[i] = p[i]; // 时间复杂度高
}
L.MaxSize += len;
free(p)
}
int main()
{
SeqList L;
InitList(L);
// 往顺序表里面插入元素
IncreaseSize(L, 5);
return 0;
}

90
ch2/sequence/static.c Normal file
View File

@@ -0,0 +1,90 @@
#include <stdio.h>
#define MaxSize 10
typedef struct
{
int data[MaxSize];
int length;
} SqList;
// 初始化顺序表,静态分配
void InitList(SqList &L)
{
// for (int i = 0; i < MaxSize; i++)
// {
// L.data[i] = 0;
// }
L.length = 0;
}
// 插入元素操作,要将元素 e 放到数组 L 下标为 i-1 的位置
bool ListInsert(SqList &L, int i, int e)
{
// i 的值必须是合法的位序
if (i < 1 || i > L.length + 1)
{
return false;
}
// 如果存储空间已满,则不能插入
if (L.length >= MaxSize)
{
return false;
}
// 插入操作
for (int j = L.length; j >= i; j--)
{
L.data[j] = L.data[j - 1];
}
L.data[i - 1] = e;
L.length++;
return true;
}
bool ListDelete(SqList &L, int i, int &e)
{
// i 的值必须是合法的位序
if (i < 1 || i > L.length)
{
return false;
}
// 删除操作
e = L.data[i - 1];
for (int j = i; j < L.length; j++)
{
L.data[j - 1] = L.data[j];
}
L.length--;
return true;
}
int main()
{
SqList L;
InitList(L);
ListInsert(L, 1, 1);
ListInsert(L, 2, 2);
ListInsert(L, 3, 3);
ListInsert(L, 4, 4);
ListInsert(L, 5, 5);
ListInsert(L, 3, 100);
// 打印操作
for (int i = 0; i < L.length; i++)
{
printf("data[%d]=%d\n", i, L.data[i]);
}
// 测试删除操作
int e = -1;
if (ListDelete(L, 7, e))
{
printf("已删除的元素值为=%d\n", e);
}
else
{
printf("位序不合法,删除失败\n");
}
// 打印操作
for (int i = 0; i < L.length; i++)
{
printf("data[%d]=%d\n", i, L.data[i]);
}
return 0;
}

15
ch2/tips.c Normal file
View File

@@ -0,0 +1,15 @@
#include <stdio.h>
void test(int x)
{
x = 1024;
printf("test 函数内部x = %d\n", x);
}
int main()
{
int x = 1;
printf("调用 test 函数前x = %d\n", x);
test(x);
printf("调用 test 函数后x = %d\n", x);
}

15
ch2/tips2.c Normal file
View File

@@ -0,0 +1,15 @@
#include <stdio.h>
void test(int &x)
{
x = 1024;
printf("test 函数内部x = %d\n", x);
}
int main()
{
int x = 1;
printf("调用 test 函数前x = %d\n", x);
test(x);
printf("调用 test 函数后x = %d\n", x);
}