数据结构(一)—— 基本概念
作者:互联网
1. 什么是数据结构
1.1 关于数据结构的一些定义
- “数据结构是数据对象,以及存在于该对象的实例和组成实例的数据元素之间的各种联系。这些联系可以通过定义相关的函数来给出。” ——Sartaj Sahni,《数据结构、算法与应用》
- “数据结构是ADT(抽象数据类型Abstract Data Type)的物理实现。” ——Clifford A.Shaffer,《数据结构与算法分析》
- “数据结构(Data Structure)是计算机中存储、组织数据的方式。通常情况下,精心选择的数据结构可以带来最优效率的算法。” ——中文维基百科
1.2 解决问题方法的效率
解决问题方法的效率:1. 跟数据的组织方式有关;
2. 跟空间的利用效率有关;
3. 跟算法的巧妙程度有关。
1.2.1 跟数据的组织方式有关
例1:如何在书架上摆放图书?
- 方法1:随便放
操作1:新书怎么插入? 哪里有空放哪里,一步到位!
操作2:怎么找到某本指定的书? 一本一本挨着找,累死! - 方法2:按照书名的拼命字母顺序查找
操作1:新书怎么插入? 新进一本书《阿Q正传》,a开头,需要把后面的所有书往后挪
操作2:怎么找到某本指定的书? 二分查找! - 方法3:把书架划分为几块区域,每块区域指定摆放某种类别的图书,在每种类别内,按照书名的拼音字母顺序摆放
操作1:新书怎么插入?先定类别,二分查找确定位置,移出空位
操作2:怎么找到某本指定的书? 先定类别,再二分查找
问题:空间如何分配?类别应该分多细?
例1告诉我们的是,解决问题方法的效率,跟数据的组织方式有关。
1.2.2 跟空间的利用效率有关
例2:写程序实现一个函数PrintN,使得传入一个正整数为n的参数后,能顺序打印从1到n的全部正整数。
用循环和递归两种方法分别实现,实现代码如下所示。
#include<iostream>
using namespace std;
//循环实现
void PrintN_Iteration(int n)
{
for (int i = 1; i <= n; i++)
{
cout << i << endl;
}
}
//递归实现
void PrintN_Recursion(int n)
{
if(n)
{
PrintN_Recursion(n - 1);
cout << n << endl;
}
}
int main()
{
PrintN_Iteration(10000);
//PrintN_Recursion(10000);
system("pause");
return 0;
}
依次测试n=10、100、1000、10000…,当n=10000时,循环实现和递归实现的结果如下图所示。
- 循环实现:可以正常打印
- 递归实现:出现异常,无法正常打印(将自己的所有可使用的空间用完,还不够,所以出现异常)
例2告诉我们的是,解决问题方法的效率,跟空间的利用效率有关。
1.2.3 跟算法的巧妙程度有关
例3:写程序计算给定多项式 f ( x ) = a 0 + a 1 x + . . . + a n − 1 x n − 1 + a n x n f(x)=a_{0}+a_{1}x+...+a_{n-1}x^{n-1}+a_{n}x^{n} f(x)=a0+a1x+...+an−1xn−1+anxn在给定点x处的值。
最直接的方法实现代码如下所示。
#include<iostream>
using namespace std;
double f1(double a[], int n, double x)
{
double p = a[0];
for (int i = 1; i <= n; i++)
{
p += (a[i] * pow(x, i)); //f(x)=a[0]+a[1]*x+...+a[n−1]*x^(n−1)+a[n]*x^(n)
}
return p;
}
int main()
{
double a[] = { 0,1,2,3,4,5,6,7,8,9 };
cout << "f1(1.1) = " << f1(9, a, 1.1) << endl;
system("pause");
return 0;
}
f1(1.1) = 84.0626
但通常情况下,我们不会使用上述方法,简化 f ( x ) = a 0 + a 1 x + . . . + a n − 1 x n − 1 + a n x n = a 0 + x ( a 1 + x ( . . . ( a n − 1 + x ( a n ) ) . . . ) ) f(x)=a_{0}+a_{1}x+...+a_{n-1}x^{n-1}+a_{n}x^{n}=a_{0}+x(a_{1}+x(...(a_{n-1}+x(a_{n}))...)) f(x)=a0+a1x+...+an−1xn−1+anxn=a0+x(a1+x(...(an−1+x(an))...)),编写代码如下所示。
#include<iostream>
using namespace std;
double f2(double a[], int n, double x)
{
double p = a[n];
for (int i = n; i > 0; i--)
{
p = a[i - 1] + x * p; //f(x)=a[0]+x*(a[1]+x*(...(a[n-1]+x*(a[n]))...))
}
return p;
}
int main()
{
double a[] = { 0,1,2,3,4,5,6,7,8,9 };
cout << "f2(1.1) = " << f2(9, a, 1.1) << endl;
system("pause");
return 0;
}
f2(1.1) = 84.0626
常用第二种方法计算给定多项式
f
(
x
)
=
a
0
+
a
1
x
+
.
.
.
+
a
n
−
1
x
n
−
1
+
a
n
x
n
f(x)=a_{0}+a_{1}x+...+a_{n-1}x^{n-1}+a_{n}x^{n}
f(x)=a0+a1x+...+an−1xn−1+anxn在给定点x处的值,是因为第二种方法相比于第一种方法运行更快。
使用clock()计时函数记录程序运行时间,这个时间单位是clock tick,即“时间打点”。计算上述两种方法编写的程序运行时间的代码如下所示。
#include<iostream>
using namespace std;
#include<ctime>
double f1(double a[], int n, double x)
{
double p = a[0];
for (int i = 1; i <= n; i++)
{
p += (a[i] * pow(x, i)); //f(x)=a[0]+a[1]*x+...+a[n−1]*x^(n−1)+a[n]*x^(n)
}
return p;
}
double f2(double a[], int n, double x)
{
double p = a[n];
for (int i = n; i > 0; i--)
{
p = a[i - 1] + x * p; //f(x)=a[0]+x*(a[1]+x*(...(a[n-1]+x*(a[n]))...))
}
return p;
}
int main()
{
double a[] = { 0,1,2,3,4,5,6,7,8,9 };
clock_t startTime, endTime;
startTime = clock();//计时开始
f1(a, 9, 1.1);
endTime = clock();//计时结束
cout << "The run time of f1 is: " << (double)(endTime - startTime) / CLOCKS_PER_SEC << "s" << endl; //常数CLOCKS_PER_SEC为机器时钟每秒所走的时钟打点数
cout << "f1(1.1) = " << f1(a, 9, 1.1) << endl;
startTime = clock();//计时开始
f2(a, 9, 1.1);
endTime = clock();//计时结束
cout << "The run time of f2 is: " << (double)(endTime - startTime) / CLOCKS_PER_SEC << "s" << endl;
cout << "f2(1.1) = " << f2(a, 9, 1.1) << endl;
system("pause");
return 0;
}
代码运行结果如下图所示。
因为运行速度太快,所以结果均显示为0,无法区分快慢。解决方案: 让被测函数重复运行充分多次,使得测出的总的时钟打点间隔充分长,最后计算被测函数平均次数运行的时间即可。修改main函数部分的代码为如下所示。
int main()
{
double a[] = { 0,1,2,3,4,5,6,7,8,9 };
clock_t startTime, endTime;
startTime = clock();//计时开始
for (int i = 0; i < 1e7; i++)
{
f1(a, 9, 1.1);
}
//f1(a, 9, 1.1);
endTime = clock();//计时结束
//cout << "The run time of f1 is: " << (double)(endTime - startTime) / CLOCKS_PER_SEC << "s" << endl; //常数CLOCKS_PER_SEC为机器时钟每秒所走的时钟打点数
cout << "The run time of f1 is: " << (double)(endTime - startTime) / CLOCKS_PER_SEC / 1e7 << "s" << endl;
cout << "f1(1.1) = " << f1(a, 9, 1.1) << endl;
startTime = clock();//计时开始
for (int i = 0; i < 1e7; i++) //重复调用函数以获得充分多的时钟打点数
{
f2(a, 9, 1.1);
}
//f2(a, 9, 1.1);
endTime = clock();//计时结束
//cout << "The run time of f2 is: " << (double)(endTime - startTime) / CLOCKS_PER_SEC << "s" << endl;
cout << "The run time of f2 is: " << (double)(endTime - startTime) / CLOCKS_PER_SEC / 1e7 << "s" << endl; //计算函数单次运行时间
cout << "f2(1.1) = " << f2(a, 9, 1.1) << endl;
system("pause");
return 0;
}
代码运行结果如下图所示。
由上述结果可以看出,第二种方法编写的程序运行时间更快。
例3告诉我们的是,解决问题方法的效率,跟算法的巧妙程度有关。
1.3 到底什么是数据结构
- 数据结构是关于数据对象在计算机中的组织方式
⋄ \diamond ⋄ 逻辑结构
⋄ \diamond ⋄ 物理存储结构 - 数据对象必定与一系列加在其上的操作相关联
- 完成这些操作所用的方法就是算法
1.4 抽象数据类型
抽象数据类型(Abstract Data Type,ADT)是计算机科学中具有类似行为的特定类别的数据结构的数学模型;或者具有类似语义的一种或多种程序设计语言的数据类型。抽象数据类型是描述数据结构的一种理论工具,其目的是使人们能够独立于程序的实现细节来理解数据结构的特性。抽象数据类型的定义取决于它的一组逻辑特性,而与计算机内部如何表示无关。
- 数据类型
⋄ \diamond ⋄ 数据对象集
⋄ \diamond ⋄ 数据集合相关联的操作集 - 抽象: 描述数据类型的方法不依赖于具体实现
⋄ \diamond ⋄ 与存放数据的机器无关
⋄ \diamond ⋄ 与数据存储的物理结构无关
⋄ \diamond ⋄ 与实现操作的算法和编程语言均无关
只描述数据对象集和相关操作集“是什么”,并不涉及“如何做到”的问题。
例4:“矩阵”的抽象数据类型定义。
∙
\bullet
∙ 类型名称: 矩阵(Matrix)
∙
\bullet
∙ 数据对象集: 一个
M
×
N
M\times N
M×N的矩阵
A
M
×
N
=
(
a
i
j
)
(
i
=
1
,
.
.
.
,
M
;
j
=
1
,
.
.
.
,
N
)
A_{M\times N}=(a_{ij})(i=1,...,M; j=1,...,N )
AM×N=(aij)(i=1,...,M;j=1,...,N)由
M
×
N
M\times N
M×N个三元组
<
a
,
i
,
j
>
<a, i, j >
<a,i,j>构成,其中
a
a
a是矩阵元素的值,
i
i
i是元素所在的行号,
j
j
j是元素所在的列号。
∙
\bullet
∙ 操作集: 对于任意矩阵
A
A
A、
B
B
B、
C
C
C
∈
\in
∈ Matrix,以及整数
i
i
i、
j
j
j、
M
M
M、
N
N
N
∘
\circ
∘ Matrix Create( int M, int N )
:返回一个
M
×
N
M\times N
M×N的空矩阵;
∘
\circ
∘ int GetMaxRow ( Matrix A )
:返回矩阵A的总行数;
∘
\circ
∘ int GetMaxcol( Matrix A )
:返回矩阵A的总列数;
∘
\circ
∘ ElementType GetEntry (Matrix A, int i, int j)
:返回矩阵A的第i行、第j列的元素;
∘
\circ
∘ Matrix Add (Matrix A, Matrix B)
:如果A和B的行、列数一致,则返回矩阵C=A+B,否则返回错误标志;
∘
\circ
∘ Matrix Multiply (Matrix A, Matrix B)
:如果A的列数等于B的行数,则返回矩阵C=AB,否则返回错误标志;
∘
\circ
∘ … …
抽象表示在:1. 在数据对象集中,a只是矩阵元素的值,不关心其具体的数据类型;
2. 在操作集中,ElementType GetEntry (Matrix A, int i, int j)
返回的也是通用的数据类型ElementType
,并不是某个具体的数据类型。
2. 什么是算法
2.1 定义
∙
\bullet
∙ 算法(Algorithm)
∘
\circ
∘ 一个有限指令集
∘
\circ
∘ 接受一些输入(有些情况下不需要输入)
∘
\circ
∘ 产生输出
∘
\circ
∘ 一定在有限步骤之后终止
∘
\circ
∘ 每一条指令必须
⋄
\diamond
⋄ 有充分明确的目标,不可以有歧义
⋄
\diamond
⋄ 计算机能处理的范围之内
⋄
\diamond
⋄ 描述应不依赖于任何一种计算机语言以及具体的实现手段
例1:选择排序算法的伪码描述。
//将N个整数List[0]...List[N-1]进行非递减排序
void SelectionSort(int List[], int N)
{
for(int i = 0; i < N; i++)
{
MinPosition = ScanForMin(List, i ,N-1); //从List[i]到List[N-1]中找最小元,并将其位置赋给MinPosition
Swap(List[i], List[MinPosition]); //将未排序部分的最小元换到有序部分的最后位置
}
}
上述伪码描述一个很重要的特点是抽象,表现在以下几点:
- List到底是数组还是链表?
- Swap用函数还是宏去实现?
上面两点均为具体实现的细节,在描述算法的时候是不关心的。
2.2 衡量算法好坏的指标
什么是好的算法?一般可以使用以下两个指标来衡量:
- 空间复杂度S(n):根据算法写成的程序在执行时占用存储单元的长度。这个长度往往与输入数据的规模有关。空间复杂度过高的算法可能导致使用的内存超限,造成程序非正常中断。
- 时间复杂度T(n):根据算法写成的程序在执行时耗费时间的长度。这个长度往往也与输入数据的规模有关。时间复杂度过高的低效算法可能导致我们在有生之年都等不到运行结果。
2.2.1 空间复杂度S(n)
例2:之前编写的PrintN函数的递归实现,在n=10000时会出现异常,无法正常打印的原因分析。
//递归实现
void PrintN_Recursion(int n)
{
if(n)
{
PrintN_Recursion(n - 1);
cout << n << endl;
}
}
对于上述的递归实现代码,如果要运行PrintN(100000),调用PrintN(99999)之前,先需将PrintN(100000)函数的所有状态存储到系统内存中,再执行PrintN(99999),依次调用执行,直到调用PrintN(0)返回,如下图所示。
空间复杂度
S
(
N
)
=
C
⋅
N
S(N) = C \cdot N
S(N)=C⋅N与长度
N
N
N成正比,当
N
N
N非常大的时候,程序可以使用的空间是有限的,所以程序会爆掉。
//循环实现
void PrintN_Iteration(int n)
{
for (int i = 1; i <= n; i++)
{
cout << i << endl;
}
}
对于上述的循环实现,没有任何调用,所以不论 N N N有多大,占用的内存始终都是一个定值,不会随着 N N N的增长而增长,所以程序会正常运行。
2.2.2 时间复杂度T(n)
例3:之前编写的计算多项式在给定点x处的值的程序,第一种方法实现比第二种方法实现运行速度慢的原因分析。
在分析一个函数运行效率的时候,如果只有加减乘除,机器运行加减法的速度比乘除法要快很多,所以基本上就是在计算函数的所有乘除法次数,加减法可忽略不计。
double f1(double a[], int n, double x)
{
double p = a[0];
for (int i = 1; i <= n; i++)
{
p += (a[i] * pow(x, i)); //f(x)=a[0]+a[1]*x+...+a[n−1]*x^(n−1)+a[n]*x^(n)
}
return p;
}
对于上述第一种方法的实现代码,每次执行for
循环,a[i] * pow(x, i)
执行 1 次乘法,pow(x, i)
执行 i-1 次乘法,共执行 i 次乘法,for
循环一共执行了 n 次,所以总共执行
1
+
2
+
.
.
.
+
n
=
n
2
+
n
2
1+2+...+n=\frac{n^{2}+n}{2}
1+2+...+n=2n2+n次乘法,时间复杂度为
T
(
n
)
=
C
1
n
2
+
C
2
n
T(n)=C_{1}n^{2}+C_{2}n
T(n)=C1n2+C2n。
double f2(double a[], int n, double x)
{
double p = a[n];
for (int i = n; i > 0; i--)
{
p = a[i - 1] + x * p; //f(x)=a[0]+x*(a[1]+x*(...(a[n-1]+x*(a[n]))...))
}
return p;
}
对于上述第二种方法的实现代码,每次执行for
循环,p = a[i - 1] + x * p
执行 1 次乘法,for
循环一共执行了 n 次,所以总共执行
n
n
n次乘法,时间复杂度为
T
(
n
)
=
C
⋅
n
T(n)=C \cdot n
T(n)=C⋅n。
当n充分大的时候, T ( n ) = C 1 n 2 + C 2 n T(n)=C_{1}n^{2}+C_{2}n T(n)=C1n2+C2n的时间复杂度远远大于 T ( n ) = C ⋅ n T(n)=C \cdot n T(n)=C⋅n。
在分析一般算法效率时,经常关注以下两种复杂度:
- 最坏情况复杂度 T ( w o r s t ) ( n ) T_{(worst)}(n) T(worst)(n)
- 平均复杂度 T ( a v g ) ( n ) T_{(avg)}(n) T(avg)(n)
T ( a v g ) ( n ) < T ( w o r s t ) ( n ) T_{(avg)}(n)<T_{(worst)}(n) T(avg)(n)<T(worst)(n),一般情况下更多关注的是最坏情况复杂度。
2.3 复杂度的渐进表示法
当我们分析算法复杂度时,只关心随着要处理的数据规模增大,复杂度增长的性质,不需要精细的分析,所以需要复杂度的渐进表示法。
∙
\bullet
∙
T
(
n
)
=
O
(
f
(
n
)
)
T(n)=O(f(n))
T(n)=O(f(n))表示存在常数
C
>
0
C>0
C>0,
n
0
>
0
n_{0} >0
n0>0,使得当
n
≥
n
0
n ≥ n_{0}
n≥n0时,有
T
(
n
)
≤
C
⋅
f
(
n
)
T ( n ) ≤ C ⋅ f ( n )
T(n)≤C⋅f(n),即
O
(
f
(
n
)
)
O(f(n))
O(f(n))表示
f
(
n
)
f(n)
f(n)是
T
(
n
)
T(n)
T(n)的某种上界;
∙
\bullet
∙
T
(
n
)
=
Ω
(
g
(
n
)
)
T(n)=Ω(g(n))
T(n)=Ω(g(n))表示存在常数
C
>
0
C>0
C>0,
n
0
>
0
n_{0} >0
n0>0,使得当
n
≥
n
0
n ≥ n_{0}
n≥n0时,有
T
(
n
)
≤
C
⋅
g
(
n
)
T ( n ) ≤ C ⋅ g ( n )
T(n)≤C⋅g(n),即
Ω
(
g
(
n
)
)
Ω(g(n))
Ω(g(n))表示
g
(
n
)
g(n)
g(n)是
T
(
n
)
T(n)
T(n)的某种下界;
∙
\bullet
∙
T
(
n
)
=
Θ
(
h
(
n
)
)
T(n) = \Theta(h(n))
T(n)=Θ(h(n))表示同时有
T
(
n
)
=
O
(
h
(
n
)
)
T ( n ) = O ( h ( n ) )
T(n)=O(h(n))和
T
(
n
)
=
Ω
(
h
(
n
)
)
T(n) = Ω(h(n))
T(n)=Ω(h(n)),即
Θ
(
h
(
n
)
)
\Theta(h(n))
Θ(h(n))表示
h
(
n
)
h(n)
h(n)既是
T
(
n
)
T(n)
T(n)的上界也是下界。
下表显示了不同的函数随着n的增长,复杂度的增长速度,可以看出 n ! n! n!的复杂度增长的速度是最快的。
下图显示了不同的函数随着n的增长,复杂度的增长速度,可以看出
l
o
g
n
logn
logn是最好的函数。
2.4 复杂度分析小窍门
∙
\bullet
∙ 若两段算法分别有复杂度
T
1
(
n
)
=
O
(
f
1
(
n
)
)
T_{1} ( n ) = O ( f_{1} ( n ) )
T1(n)=O(f1(n)) 和
T
2
(
n
)
=
O
(
f
2
(
n
)
)
T_{2} ( n ) = O ( f_{2} ( n ) )
T2(n)=O(f2(n)),则
∘
\circ
∘
T
1
(
n
)
+
T
2
(
n
)
=
m
a
x
(
O
(
f
1
(
n
)
)
,
O
(
f
2
(
n
)
)
)
T_{1}( n ) + T_{2}( n ) = max ( O ( f_{1} ( n ) ), O ( f_{2} ( n ) ) )
T1(n)+T2(n)=max(O(f1(n)),O(f2(n)))
∘
\circ
∘
T
1
(
n
)
×
T
2
(
n
)
=
O
(
f
1
(
n
)
×
f
2
(
n
)
)
T_{1} ( n ) ×T_{2} ( n ) = O (f_{1}( n ) ×f_{2} ( n ) )
T1(n)×T2(n)=O(f1(n)×f2(n))
∙
\bullet
∙ 若
T
(
n
)
T(n)
T(n)是关于
n
n
n的
k
k
k阶多项式,那么
T
(
n
)
=
Θ
(
n
k
)
T ( n ) = \Theta ( n^{k} )
T(n)=Θ(nk)。
∙
\bullet
∙ 一个for
循环的时间复杂度等于循环次数乘以循环体代码的复杂度。
∙
\bullet
∙ if-else
结构的复杂度取决于 if
的条件判断复杂度和两个分枝部分的复杂度,总体复杂度取三者中最大.
3. 应用实例:最大子列和问题
给定N个整数的序列 { A 1 , A 2 , . . . , A N } \{ A_{1}, A_{2}, ..., A_{N} \} {A1,A2,...,AN},求函数 f ( i , j ) = m a x { 0 , ∑ k = i j A k } f(i,j)=max\{0, \sum ^{j}_{k=i}A_{k} \} f(i,j)=max{0,∑k=ijAk}的最大值。
分析:“连续子列”被定义为 { N i , N i + 1 , … , N j } \{ N_{i} , N_{i+1} , …, N_j \} {Ni,Ni+1,…,Nj},其中 1≤i≤j≤K。“最大子列和”则被定义为所有连续子列元素的和中最大者,例如给定序列{ 4, -3, 5, -2, -1, 2, 6, -2 },其连续子列{ 4, -3, 5, -2, -1, 2, 6 }有最大的和11。
- 算法1:确定子列的首部和尾部,再遍历累加,时间复杂度为 O ( n 3 ) O(n^3) O(n3)
//算法1:确定子列的首部和尾部,再遍历累加,时间复杂度为O(n^3)
#include<iostream>
using namespace std;
int MaxSubseqSum1(int a[], int n)
{
int ThisSum, MaxSum = 0;
for (int i = 0; i < n; i++) //i为子列左端位置
{
for (int j = 0; j < n; j++) //j是子列右端位置
{
ThisSum = 0; //ThisSum是从a[i]到a[j]的子列和
for (int k = i; k <= j; k++)
{
ThisSum += a[k];
}
if (ThisSum > MaxSum) //如果刚得到的这个子列和更大,则更新结果
{
MaxSum = ThisSum;
}
}
}
return MaxSum;
}
int main() {
int n;
int a[100000 + 5];
cout << "请输入子列的长度n:" << endl;
cin >> n; // 8
cout << "请输入子列的元素(元素之间以空格分隔):" << endl;
for (int i = 0; i < n; i++)
{
cin >> a[i]; // a[]={ 4, -3, 5, -2, -1, 2, 6, -2 };
}
cout << "最大子列和:" << endl;
int MaxSum1 = MaxSubseqSum1(a, n); // 11
cout << "算法1结果:" << MaxSum1 << endl;
system("pause");
return 0;
}
请输入子列的长度n:
8
请输入子列的元素(元素之间以空格分隔):
4 -3 5 -2 -1 2 6 -2
最大子列和:
算法1结果:11
- 算法2:确定子列的首部,逐个累加,时间复杂度为 O ( n 2 ) O(n^2) O(n2)
//算法2:确定子列的首部,逐个累加,时间复杂度为O(n^2)
#include<iostream>
using namespace std;
int MaxSubseqSum1(int a[], int n)
{
int ThisSum, MaxSum = 0;
for (int i = 0; i < n; i++) //i为子列左端位置
{
ThisSum = 0; //ThisSum是从a[i]到a[j]的子列和
for (int j = i; j < n; j++) //j是子列右端位置
{
ThisSum += a[j];
if (ThisSum > MaxSum) //如果刚得到的这个子列和更大,则更新结果
{
MaxSum = ThisSum;
}
}
}
return MaxSum;
}
int main() {
int n;
int a[100000 + 5];
cout << "请输入子列的长度n:" << endl;
cin >> n; // 8
cout << "请输入子列的元素(元素之间以空格分隔):" << endl;
for (int i = 0; i < n; i++)
{
cin >> a[i]; // a[]={ 4, -3, 5, -2, -1, 2, 6, -2 };
}
cout << "最大子列和:" << endl;
int MaxSum2 = MaxSubseqSum2(a, n); // 11
cout << "算法2结果:" << MaxSum2 << endl;
system("pause");
return 0;
}
请输入子列的长度n:
8
请输入子列的元素(元素之间以空格分隔):
4 -3 5 -2 -1 2 6 -2
最大子列和:
算法2结果:11
- 算法3:分而治之,简单来说就是把一个大的问题分解成多个小问题求解,再从所有小问题的解里面寻求最优解。对于此问题而言,可以把一个大的序列分为两个小的序列,再把小的序列分为更小的两个序列,…,直到每个小序列只有一个数,这就是分的过程。在每个小序列中,会得到:
1. 左边最大子列和(正数即本身,负数即0);
2. 右边最大子列和;
3. 横跨划分边界的最大子列和。
此时三者中最大的值就是该小序列的"最大子列和",以此再得到更大一级序列的"最大子列和",…,最终得到整个序列的最大子列和。
时间复杂度为 T ( n ) = 2 T ( n 2 ) + c ⋅ n , T ( 1 ) = O ( 1 ) = 2 [ 2 T ( n 2 2 ) + c ⋅ n 2 ] + c ⋅ n = 2 k O ( 1 ) + + c ⋅ k ⋅ n , 其 中 n 2 k = 1 ⇒ k = l o g n = O ( n l o g n ) \begin{matrix} T ( n ) = 2 T (\frac{n}{2}) + c ⋅ n , & T ( 1 ) = O ( 1 ) \ _{}\ _{}\ _{}\ _{}\ _{}\ _{}\ _{}\ _{}\ _{}\ _{}\ _{}\ _{}\ _{}\ _{}\ _{}\\ \ _{}\ _{}\ _{}\ _{}\ _{}\ _{}\ _{}\ _{}\ _{}\ _{}\ _{}\ _{}\ _{}\ _{}\ _{}\ _{}\ _{}\ _{}\ _{}\ _{}\ _{}=2[2T(\frac{n}{2^{2}})+ c ⋅\frac{ n}{2}] +c ⋅n& \\ \ _{}\ _{}\ _{}\ _{}\ _{}\ _{}\ _{}\ _{}\ _{}\ _{}\ _{}\ _{}\ _{}\ _{}\ _{}\ _{}\ _{}\ _{}\ _{}= 2^{k}O(1) ++ c ⋅ k⋅n,\ _{}\ _{}\ _{}& 其中\frac{n}{2^{k}}=1\Rightarrow k = logn\\ \ _{}\ _{}=O(nlogn) \end{matrix} T(n)=2T(2n)+c⋅n, =2[2T(22n)+c⋅2n]+c⋅n =2kO(1)++c⋅k⋅n, =O(nlogn)T(1)=O(1) 其中2kn=1⇒k=logn
//算法3:分而治之,递归分成两份,分别求每个分割后最大子列和,时间复杂度为O(nlogn)
//返回左边最大子列和、右边最大子列和、横跨划分边界的最大子列和三者中最大值
int MaxSum(int A, int B, int C)
{
return (A > B) ? ((A > C) ? A : C) : ((B > C) ? B : C);
}
//分治
int DivideAndConquer(int a[], int left, int right)
{
if (left == right) //递归结束条件:子列只有一个数字
{
if (a[left] > 0) // 当该数为正数时,最大子列和为其本身
{
return a[left];
}
return 0; // 当该数为负数时,最大子列和为 0
}
//分别递归找到左右最大子列和
int mid = left + (right - left) / 2; //利用left+(right - left)/2求mid是为了防止整数溢出问题
int MaxLeftSum = DivideAndConquer(a, left, mid);
int MaxRightSum = DivideAndConquer(a, mid + 1, right);
//再分别找左右跨界最大子列和
int MaxLeftBorderSum = 0;
int LeftBorderSum = 0;
for (int i = mid; i >= left; i--) //应该从边界出发向左边找
{
LeftBorderSum += a[i];
if (LeftBorderSum > MaxLeftBorderSum)
MaxLeftBorderSum = LeftBorderSum;
}
int MaXRightBorderSum = 0;
int RightBorderSum = 0;
for (int i = mid + 1; i <= right; i++) // 从边界出发向右边找
{
RightBorderSum += a[i];
if (RightBorderSum > MaXRightBorderSum)
MaXRightBorderSum = RightBorderSum;
}
//最后返回分解的左边最大子列和,右边最大子列和,和跨界最大子列和三者中最大的数
return MaxSum(MaxLeftSum, MaxRightSum, MaXRightBorderSum + MaxLeftBorderSum);
}
int MaxSubseqSum3(int a[], int n)
{
return DivideAndConquer(a, 0, n - 1);
}
int main() {
int n;
int a[100000 + 5];
cout << "请输入子列的长度n:" << endl;
cin >> n; // 8
cout << "请输入子列的元素(元素之间以空格分隔):" << endl;
for (int i = 0; i < n; i++)
{
cin >> a[i]; // a[]={ 4, -3, 5, -2, -1, 2, 6, -2 };
}
cout << "最大子列和:" << endl;
int MaxSum3 = MaxSubseqSum3(a, n); // 11
cout << "算法3结果:" << MaxSum3 << endl;
system("pause");
return 0;
}
请输入子列的长度n:
8
请输入子列的元素(元素之间以空格分隔):
4 -3 5 -2 -1 2 6 -2
最大子列和:
算法3结果:11
- 算法4:在线处理。“在线”的意思是指每输入一个数据就进行即时处理,在任何一个地方中止输入,算法都能正确给出当前的解。时间复杂度为 O ( n ) O(n) O(n)。
//算法4:在线处理,直接累加,如果累加到当前的和为负数,置当前值或0,时间复杂度为 O(n)
int MaxSubseqSum4(int a[], int n)
{
int ThisSum = 0;
int MaxSum = 0;
for (int i = 0; i < n; i++)
{
ThisSum += a[i];
if (ThisSum < 0)
{
ThisSum = 0;
}
else if (ThisSum > MaxSum)
{
MaxSum = ThisSum;
}
}
return MaxSum;
}
int main() {
int n;
int a[100000 + 5];
cout << "请输入子列的长度n:" << endl;
cin >> n; // 8
cout << "请输入子列的元素(元素之间以空格分隔):" << endl;
for (int i = 0; i < n; i++)
{
cin >> a[i]; // a[]={ 4, -3, 5, -2, -1, 2, 6, -2 };
}
cout << "最大子列和:" << endl;
int MaxSum4 = MaxSubseqSum4(a, n); // 11
cout << "算法4结果:" << MaxSum4 << endl;
system("pause");
return 0;
}
请输入子列的长度n:
8
请输入子列的元素(元素之间以空格分隔):
4 -3 5 -2 -1 2 6 -2
最大子列和:
算法4结果:11
在某台机器上四种算法在不同输入规模时的运行时间比较结果如下图所示。
可以看出第4种算法是最好的,当n=100,000时,仍可以在小于1秒的时间内得到运行结果。
标签:子列,int,double,复杂度,算法,数据结构,基本概念,cout 来源: https://blog.csdn.net/HUAI_BI_TONG/article/details/116785891