数据结构笔记目录:
1. 绪论、时间复杂度
2. 线性表
3. 树
4. 图
5. 查找
6. 排序
0. 基本概念
0.1 数据类型
值+操作的集合
- 原子类型
- 结构类型
- 抽象数据类型(ADT):可用于定义一个完整数据类型 、 数据结构
ADT
一个数学模型及定义在该模型上的一组操作
ADT{
数据对象:<数据对象的定义> D
数据关系:<数据关系的定义> S
基本操作:<基本操作的定义> P
}
0.2 数据结构
存在一种或多种特定关系(逻辑结构)的数据元素集合
- 逻辑结构 独立于 存储结构
四种逻辑结构
- 集合
- 线性结构
- 树形结构
- 网状结构
两种存储结构
- 顺序存储
- 链式存储
- (哈希)
- (索引)
0.3 算法
特定问题的求解步骤
特性
- 有穷性
- 程序
≠
\neq
= 算法
- 程序不一定有穷:死循环,操作系统
- 程序
≠
\neq
= 算法
- 确定性
- 可行性
- 输入
- (至少一个)输出
目标
- 正确
- 可读
- 健壮
- 高效低存储
时间复杂度的简单计算方法
参考数据结构实现的黑书
1. 时间复杂度
1.1 概念
算法的渐进时间复杂度,简称时间复杂度
事前分析
- 算法采用的策略和方案
- 编译产生代码质量
- 问题的输入规模
- 机器执行指令的速度
将核心操作的次数与输入规模关联
1.1.1 函数渐进增长
给定两个函数 f ( n ) f(n) f(n) 和 g ( n ) g(n) g(n) ,如果存在一个整数 N N N 使得对于所有 n > N n >N n>N , f ( n ) f(n) f(n) 总是比 g ( n ) g(n) g(n) 大,那么称 f ( n ) f(n) f(n) 的渐进增长快于 g ( n ) g(n) g(n)
- 算法函数中的常数、系数可以忽略
- 算法函数中最高次幂越小,算法效率越高
1.1.2 大O记法
语句总的执行次数 T ( n ) T(n) T(n) 是关于问题规模 n n n 的函数,进而分析 T ( n ) T(n) T(n) 随着 n n n 的变化情况并确定 T ( n ) T(n) T(n) 的量级。记为 T ( n ) = O ( f ( n ) ) T(n) = O(f(n)) T(n)=O(f(n))
表示随着问题规模 n n n 的增大,算法执行时间的增长率和 f ( n ) f(n) f(n) 的增长率相同
-
f ( n ) f(n) f(n) 表示问题规模 n 的某个函数
-
执行次数 ⟺ \iff ⟺ 执行时间
规则
- 用1代替运行时间中的所有常数、系数
- 在修改后的次数中,只保留高阶项
1-100求和
# 累加
int main(){
int sum = 0;//执行一次
int n = 100;//执行一次
for(int i = 0; i < n;++i){//判断语句执行n+1次
sum += i;//执行n次数
}
printf("sum=%d",sum);
}
# 执行 2n+3 次
# 大O记法 O(n)
# -------------------------------------------
# 高斯公式
void main(){
int sum = 0;//执行1次
int n = 100;//执行1次
sum = (n+1)n/2;//执行1次
printf("sum=%d",sum);
}
# 执行3次
# 大O记法 O(1)
最坏情况
直接查找
int search(int num){
int arr[] = [11,10,8,9,7,22,23,0];
for(int i = 0;i < arr.length;++i){
if(num == arr[i])
return i;
}
return -1;
}
最好情况:
- 查找的第一个数字就是期望数字—— O ( 1 ) O(1) O(1)
最坏情况:
- 查找的最后一个数字,才是期望数字—— O ( n ) O(n) O(n)
平均情况:
- 任何数字查找的平均代价为 —— O ( n ) O(n) O(n)
1.1.3 时间复杂度排序
O ( 1 ) < O ( l o g n ) < O ( n l o g n ) < O ( n 2 ) < O ( n 3 ) < O ( 2 n ) < O ( n ! ) < O ( n n ) O(1) < O(logn) < O(nlogn) < O(n^2) < O(n^3) < O(2^n) < O(n!) < O(n^n) O(1)<O(logn)<O(nlogn)<O(n2)<O(n3)<O(2n)<O(n!)<O(nn)
1.2 简单时间复杂度分析
1.2.1 非函数调用
-
线性阶
非嵌套线性阶,随输入规模的扩大,呈 线性增长
int sum = 0;//执行一次 int n = 100;//执行一次 for(int i = 0; i <= n;++i){//执行n+1次 sum += i;//执行n次数 }
-
平方阶
二重循环嵌套
int sum = 0; int n = 100; for(int i = 1;i <= n;++i){//执行 n+1 次 for (j = 1;j <= n;++j){//执行 n*(n+1) 次 sum += i;//执行 n^2 次 } }
-
立方阶
三重循环嵌套
-
对数阶
对数阶,由于输入规模n的增大,不管底数多少,增长率都相同,所以忽略底数
int i=1,n=100; while(i < n){ i = i*2; }
循环次数: x = l o g ∗ 2 n x = log*2n x=log∗2n
时间复杂度: O ( l o g n ) O(logn) O(logn) -
常数阶
1.2.3 函数调用
void main(){
int n = 100;
for(int i = 0;i < n;++i){
show(i); // O(n)
}
}
void show(int i){
printf("%d",i);
}
void main(){
int n = 100;
for(int i = 0;i < n;++i){
show(i); //O(n^2)
}
}
void show(int i){
for(int j = 0;j < i;++j){
printf("%d",i);
}
}
1.3 计算方法
1.3.0 常规
for
循环- 一次
for
循环至多是该for
循环包含的 内语句运行次数 乘 迭代次数 - 嵌套的
for
循环 - 从里到外分析循环
- 顺序语句
- 各语句运行时间求和
if/else
语句- 两执行函数中运行时间长的
1.3.1 循环主体中的变量参与循环条件的判断
找出主体语句中与 T ( n ) T(n) T(n) 成正比的循环变量,将之代入条件运算
int i = 1;
while(i <= n){
i = i*2;
}
- 执行次数 t t t ,有 2 t ≤ n 2^t \leq n 2t≤n ,得 T ( n ) = O ( l o g ∗ 2 n ) T(n) = O(log*2n) T(n)=O(log∗2n)
int y = 5;
while((y+1)*(y+1)<=n){
y = y+1;
}
- 执行次数 t = y − 5 t = y-5 t=y−5 ,有 y = t + 5 y=t+5 y=t+5,代入得 t < n − 6 t < \sqrt{n}-6 t<n−6,即 T(n) = O( n \sqrt{n} n)
运算时间中的对数
- 如果一个算法用时间 O ( 1 ) O(1) O(1) 将问题的大小削减为原来的一部分,那么该算法就是 O ( l o g N ) O(logN) O(logN)
- 如果用常数时间把问题减少一个常数,那么这种算法就是 O ( N ) O(N) O(N)
折半查找
int BinarySearch(const ElementType A[],ElementType X,int N){
int Low,Mid,High;
Low = 0,High = N-1;
while(Low <= High){
Mid = (Low+High)/2;
if(A[Mid] < X){
Low = Mid + 1;
}else{
if(A[Mid] > X){
High = Mid-1;
else
return Mid; //Found
}
}
return NotFound; //NotFound is defined as -1
}
分析:提供了 O ( l o g N ) O(logN) O(logN) 以内的查找操作
欧几里得算法
定理:如果 M > N M > N M>N ,则 KaTeX parse error: Undefined control sequence: \mbox at position 3: M \̲m̲b̲o̲x̲{ mod N} < \fra…
unsigned int Gcd(unsigned int M,unsigned int N){
unSigned int Rem;
while(N > 0){
Rem = M % N;
M = N;
N = Rem;
}
return M;
}
幂运算 O ( l o g n ) O(logn) O(logn)
long int Pow(long int X,unsigned int N){
if(N == 0)
return 1;
if(N == 1)
return X;
if(isEven(N))
return Pow(X*X,N/2);
else
return Pow(X*X,N/2) * X;
}
1.3.2 循环主体中的变量与循环条件无关
采用 归纳法 或 累计循环次数
多层循环从内到外分析,忽略单步语句、条件判断语句,只关注主体语句的执行次数
递归程序——分治策略
递归程序四条准则
- 基准情形:有结束条件
- 不断推进:每次调用都朝向基准情形推进
- 设计法则:假设所有的递归调用都能进行
- 合成效益法则:避免重复计算
“分治”策略
- ”分“:将大问题大致分为两大致相等的 幂次阶 子问题,用递归求解
- “治”:将两个子问题的解合并到一起并可能再做些少量的附加工作(幂次阶),得到整个问题的解
主方法 :大小为
n
n
n 的原问题分成若干个大小为
n
b
\frac{n}{b}
bn 的子问题,其中
a
a
a 个子问题需要求解,而
c
n
k
cn^k
cnk 是合并各个子问题需要的工作量。此时问题可表示为:
T
(
N
)
=
{
c
n
=
1
a
T
(
N
b
)
+
c
n
k
n
>
1
T(N)=\begin{cases} c & n=1\\ aT(\frac{N}{b}) + cn^k & n>1 \end{cases}
T(N)={caT(bN)+cnkn=1n>1
则其时间复杂度可相应的表示为:
T ( N ) = { O ( n l o g b a ) a > b k O ( n k l o g b n ) a = b k O ( n k ) a < b k T(N)=\begin{cases} O(n^{log_ba}) &a>b^k \\ O(n^klog_bn) &a=b^k \\ O(n^k) &a<b^k \end{cases} T(N)=⎩ ⎨ ⎧O(nlogba)O(nklogbn)O(nk)a>bka=bka<bk
推导
已知
{ T ( 1 ) = 1 T ( N ) = 2 T ( N 2 ) + O ( N ) \begin{cases} T(1) = 1 \\ T(N) = 2T(\frac{N}{2}) + O(N) \end{cases} {T(1)=1T(N)=2T(2N)+O(N)
1. 等号右边连续代入递归关系
T
(
N
)
=
2
T
(
N
2
)
+
N
=
2
[
2
T
(
N
4
)
+
N
2
]
+
N
=
2
{
2
[
2
T
(
N
8
)
+
N
4
]
+
N
2
}
+
N
=
.
.
.
=
2
k
T
(
N
2
k
)
+
k
N
\begin{align} T(N) & = 2T(\frac{N}{2})+N \notag \\ &= 2[2T(\frac{N}{4})+\frac{N}{2}]+N \notag \\ & =2\{2[2T(\frac{N}{8})+\frac{N}{4}]+\frac{N}{2}\}+N \notag \\ & = ... \notag \\ & = 2^kT(\frac{N}{2^k}) + kN \notag \end{align}
T(N)=2T(2N)+N=2[2T(4N)+2N]+N=2{2[2T(8N)+4N]+2N}+N=...=2kT(2kN)+kN
由 k = l o g N k = logN k=logN 得
T ( n ) = N T ( 1 ) + N l o g N = N l o g ( N ) + N T(n) = NT(1)+NlogN = Nlog(N) + N T(n)=NT(1)+NlogN=Nlog(N)+N
2. 叠缩求和
用 N 去除递归关系中的两边,不断替换
T ( N ) N = T ( N 2 ) N 2 + 1 T ( N 2 ) N 2 = T ( N 4 ) N 4 + 1 T ( N 4 ) N 4 = T ( N 8 ) N 8 + 1 ⋮ ⋮ T ( 2 ) 2 = T ( 1 ) 1 + 1 \begin{align} \frac{T(N)}{N} &= \frac{T(\frac{N}{2})}{\frac{N}{2}} + 1 \notag \\ \notag \\ \frac{T(\frac{N}{2})}{\frac{N}{2}} &= \frac{T(\frac{N}{4})}{\frac{N}{4}} + 1 \notag \\ \notag \\ \frac{T(\frac{N}{4})}{\frac{N}{4}} &= \frac{T(\frac{N}{8})}{\frac{N}{8}} + 1 \notag \\ \notag \\ \vdots \notag \\ \vdots \notag \\ \notag \\ \frac{T(2)}{2} &= \frac{T(1)}{1} + 1\notag \\ \notag \\ \end{align} NT(N)2NT(2N)4NT(4N)⋮⋮2T(2)=2NT(2N)+1=4NT(4N)+1=8NT(8N)+1=1T(1)+1
将等号左边的所有相相加等于右边所有项的和,结果为
T ( N ) N = T ( 1 ) 1 + l o g N = N l o g ( N ) + N \begin{align} \frac{T(N)}{N} &= \frac{T(1)}{1} + logN \notag \\ \notag \\ &=Nlog(N) + N \notag \end{align} NT(N)=1T(1)+logN=Nlog(N)+N
1.4 常见算法时间复杂度总结
描述 | 增长的数量级 | 说明 | 举例 |
---|---|---|---|
常数级 | 1 | 普通语句 | 将两个数相加 |
对数级 | l o g N logN logN | 二分策略 | 二分查找 |
线性级 | N N N | 单层循环 | 找出最大元素 |
线性对数级 | N l o g N NlogN NlogN | 分治思想 | 归并排序 |
平方级 | N 2 N^2 N2 | 双层循环 | 检查所有元素对 |
立方级 | N 3 N^3 N3 | 三层循环 | 检查所有三元组 |
指数级 | 2 N 2^N 2N | 穷举查找 | 检查所有子集 |
1.5 最大子序列和问题的解
1.5.1 遍历所有子串,对子串的子序列依次求和
int MaxSubSequenceSum(const int A[],int N){
int ThisSum,MaxSum,i,j,k;
MaxSum = 0;
for(i = 0;i < N;++i){
for(j = i;j < N;++j){//遍历所有子串
ThisSum = 0;
for(k = i;k <= j;++k)//当前子串的所有子序列求和
ThisSum += A[k];
if(ThisSum > MaxSum)
MaxSum = ThisSum;
}
}
return MaxSum;
}
∑ i = 0 N − 1 ∑ j = i N − 1 ∑ k = i j 1 ∑ k = i j 1 = j − i + 1 ∑ j = i N − 1 j − i + 1 = ( N − i + 1 ) ( N − i ) 2 ∑ i = 0 N − 1 ( N − i + 1 ) ( N − i ) 2 = ∑ i = 1 N ( N − i + 1 ) ( N − i ) 2 = 1 2 ∑ i = 1 N i 2 − ( N + 3 2 ) ∑ i = 1 N i + 1 2 ( N 2 + 3 N + 2 ) ∑ i = 1 N 1 = N 3 + 3 N 2 + 2 N 6 \begin{align} \sum_{i = 0}^{N-1}{\sum_{j = i}^{N-1}{\sum_{k = i}^{j}{1}}} \notag \\ \sum_{k = i}^{j}{1} &= j-i+1 \notag \\ \sum_{j = i}^{N-1}{ j-i+1} &= \frac{(N-i+1)(N-i)}{2} \notag \\ \notag \\ \sum_{i = 0}^{N-1}{ \frac{(N-i+1)(N-i)}{2}} & = \sum_{i = 1}^{N}{ \frac{(N-i+1)(N-i)}{2}} \notag \\ \notag \\ &= \frac{1}{2}\sum_{i=1}^{N}i^2-(N+\frac{3}{2})\sum_{i=1}^{N}i + \frac{1}{2}(N^2+3N+2)\sum_{i = 1}^{N}1 \notag \\ \notag \\ &= \frac{N^3+3N^2+2N}{6} \notag \end{align} i=0∑N−1j=i∑N−1k=i∑j1k=i∑j1j=i∑N−1j−i+1i=0∑N−12(N−i+1)(N−i)=j−i+1=2(N−i+1)(N−i)=i=1∑N2(N−i+1)(N−i)=21i=1∑Ni2−(N+23)i=1∑Ni+21(N2+3N+2)i=1∑N1=6N3+3N2+2N
时间复杂度为 O ( N 3 ) O(N^3) O(N3)
1.5.2 记录中间累加量
∑ k = i j A k = A j + ∑ k = i j − 1 A k \sum_{k = i}^{j}{A_k} = A_j+\sum_{k = i}^{j-1}{A_k} k=i∑jAk=Aj+k=i∑j−1Ak
int MaxSubSequenceSum(const int A[],int N){
int ThisSum,MaxSum,i,j,k;
MaxSum = 0;
for(i = 0;i < N;++i){
ThisSum = 0;
for(j = i;j < N;++j){//遍历所有子串
ThisSum += A[k];
if(ThisSum > MaxSum){
MaxSum = ThisSum;
}
}
return MaxSum;
}
时间复杂度为 O ( N 2 ) O(N^2) O(N2)
1.5.3 分治法
将序列分成大致相等的两部分。
最大子序列和可能在三处出现:
- 数据的左半部分;
- 递归求解
- 数据的右半部分;
- 递归求解
- 中间部分
- 分别求出前、后部分的最大和,相加中间部分最大和
int MaxSubSum(const int A[],int Left,int Right){
int MaxLeftSum,MaxRightSum;
int MaxLeftBorderSum,MaxRightBorderSum;
int LeftBorderSum,RightBorderSum;
int Center,i;
if(Left == Right){//只有一个元素
if(A[Left] > 0)//该元素非负即为最大和
return A[Left];
else{
return 0;
}
}
Center = (Left+Right) / 2;
MaxLeftSum = MaxSubSum(A,Left,Center);
MaxRightSum = MaxSubSum(A,Center,Right);
MaxLeftBorderSum = 0,LeftBorderSum = 0;
for(i = Center;i >= Left;i--){
LeftBorderSum += A[i];
if(LeftBorderSum > MaxLeftBorderSum)
MaxLeftBorderSum = LeftBorderSum;
}
MaxRightBorderSum = 0,RightBorderSum = 0;
for(i = Center;i >= Left;i--){
RightBorderSum += A[i];
if(RightBorderSum > MaxRightBorderSum)
MaxRightBorderSum = RightBorderSum;
}
return Max3(MaxLeftSum,MaxRightSum,MaxLeftBorderSum+MaxRightBorderSum);
}
int MaxSubSequenceSum(Const A[],int N){
return MaxSubSum(A,0,N-1);
}
{ T ( 1 ) = 1 T ( N ) = 2 T ( N / 2 ) + O ( N ) \begin{cases} T(1) = 1 \\ T(N) = 2T(N/2) + O(N) \end{cases} {T(1)=1T(N)=2T(N/2)+O(N)
时间复杂度为 O ( N l o g N ) O(NlogN) O(NlogN)
分治法 时间复杂度一般都是若干个代价为 l o g N logN logN 的子问题与一个处理函数的代价和
1.5.4 最简
int MaxSubSequenceSum(const int A[],int N){
int ThisSum,MaxSum,j;
ThisSum = MaxSum = 0;
for(j = 0;j < N;++j){
ThisSum += A[k];
if(ThisSum > MaxSum){
MaxSum = ThisSum;
}else if(ThisSum < 0){
ThisSum = 0;
}
}
return MaxSum;
}
时间复杂度为 O ( N ) O(N) O(N)
空间复杂度
算法所需的辅助空间数量级
- 运行过程中变量占用的空间 辅助数组
- 递归工作栈 递归深度
原地工作
算法所需的辅助空间为常量