一、什么是数据结构
1.定义
1)数据结构是一门怎样的学科?
研究非数值计算的程序设计问题中计算机的操作对象以及他们之间的关系和操作等的学科。
2)数据结构如何定义?
对于数据结构是什么没有一个完全的定义,在Sartaj Sahni的《数据结构、算法与应用》中,给出了这样的定义:
“数据结构是数据对象,以及存在于该对象的实例和组成实例的数据元素之间的各种联系。这些联系可以通过定义相关函数给出。”
可以说,数据结构是数据对象在计算机中的组织方式,包括逻辑结构和物理存储结构;另外这些数据对象必定与一系列加在其上的操作相关联,这些操作所用的方法就是算法。
3)
数据:对客观事物的符号表示,在计算机科学中是指所有能输入到计算机中并被计算机程序处理的符号的总称。
数据元素:数据的基本单位,在计算机程序中通常作为一个整体进行考虑和处理。
数据对象:性质相同的数据元素的集合,是数据的一个子集。
数据结构:相互之间存在一种或多种特定关系的数据元素的集合。
数据类型:一个值的集合和定义在这个值集上的一组操作的总称。
2.抽象数据类型
数据类型包括:
- 数据对象集
- 数据集合相关联的操作
抽象,描述数据类型的的方法不依赖于具体实现:
- 与存放数据的机器无关
- 与数据存储的物理结构无关
- 与实现操作的算法和编程语言无关
3.数据结构与算法
(1)解决问题方法的效率与数据的组织方式有关。——数据结构
数据结构与算法是紧密相关的,说到数据结构就离不开算法,或者说有了合适的数据结构之后,才能更好的执行算法,处理数据。
(2)解决问题方法的效率与空间利用效率有关。——空间复杂度
一个简单的例子:两种PrintN
函数
循环打印:
void PrintN_Loop(int N)
{
int i;
for (i=1;i<=N;i++){
cout<< i << endl;
}
return;
}
递归打印:
void PrintN_Recursive(int N)
{
if(N){
PrintN_Recursive(N-1);
cout<<N<<endl;
}
return ;
}
对于循环打印的函数中,不管N多少,始终只有占用一个变量i的空间;而对于递归打印的函数,在执行时,不断地递归调用自身,因为递归外层的函数还没用执行完,所以需要不断的需要内存空间储存未执行完的函数,当N特别大的时候,内存空间不足就会导致程序异常终止。
(3)解决问题方法的效率与算法设计的是否巧妙有关。——时间复杂度
一个简单的例子:计算多项式在x
处的值
直接循环,每次循环中都调用pow()
:
double Fx_pow(int n, double a[], double x)
{
int i;
double p =a[0];
for (i=1;i<=n;i++){
p += (a[i]*pow(x,i));
}
return p;
}
采用上一次计算的值,再乘x
:
double Fx_nonepow(int n, double a[], double x)
{
int i;
double p =a[n];
for (i=n;i>0;i--){
p = x*p +a[i-1];
}
return p;
}
写一个测函数运行时间的函数,测一下,可以发现这两种算法运算时间基本上差一个数量级:
#include <iostream>
#include <math.h>
#include <time.h>
#define MaxN 10
#define X 1.1
#define MaxR 1e5
double Fx_pow(int n, double a[], double x);
double Fx_nonepow(int n, double a[], double x);
double Runtime(int n, double a[], double x,double(*func)(int n, double a[], double x))
{
clock_t start,stop;
double duration;
start = clock();
int j;
for (j=0; j<MaxR; j++){
func(n, a, x);
}
stop = clock();
duration = (double)(stop - start)/CLK_TCK/MaxR;
return duration;
}
int main()
{
int i;
double a[MaxN];
for (i=0;i<MaxN;i++) a[i] = (double)i;
cout<<Fx_pow(MaxN-1, a, X)<<endl;
cout<<Runtime(MaxN-1, a, X,Fx_pow)<<endl;
cout<<Fx_nonepow(MaxN-1, a, X)<<endl;
cout<<Runtime(MaxN-1, a, X,Fx_nonepow)<<endl;
return 0;
}
运行结果:
84.0626
2.9e-07
84.0626
2e-08
二、什么是算法
1.算法定义
算法五个特性:有穷性,确定性,可行性,输入,输出。
- 一个有限指令集
- 接收输入(部分情况下可以没有输入)
- 产生输出
- 一定是有限步骤之后终止
- 每一条指令必须有明确的目标,没有歧义;且在计算机可以处理的范围内;描述抽象(伪码表示)
2.好算法的定义
从上面的两个例子中可以看出,算法好不好,主要是有两个复杂度决定的:
- 空间复杂度S(N)
- 时间复杂度T(N)
在分析时,我们一般关注的是最坏情况的复杂度。
3.复杂度渐进表示
1)大O记号:
假设f(n)和g(n)的定义域是非负整数,存在两个正整数c和n0,使得n>n0的时候,f(n)≤c * g(n),则f(n)=O(g(n))。可见O(g(n))可以表示算法运行时间的上界。O(g(n))表示的函数集合的函数是阶数不超过g(n)的函数。
2)Ω记号:
Ω记号与大O记号相反,他可以表示算法运行时间的下界。Ω(g(n))表示的函数集合的函数是所有阶数超过g(n)的函数。
3)Θ记号:
Θ记号介于大O记号和Ω记号之间。他表示,存在正常数c1,c2,n0,当n>n0的时候,c1 * g(n)≤f(n)≤c2 * g(n),则f(n)=Θ(g(n))。他表示所有阶数与g(n)相同的函数集合。
4.复杂度分析小知识
1)算法复杂度之和,取较大的;相乘即最高次相乘。
2)for循环复杂度=循环次数×循环体复杂度;if分支语句复杂度是各分支和判断语句中最大的。
3)1 logn n nlogn n 2 … x n n!,一般不能超过n2,见到n2就要想想能不能优化为nlogn。
5.一个例子:连续子列和问题
1)三重循环:时间复杂度时间复杂度n3
template<typename T>
int MaxSubseqSum_1(T& A)
{
int N, ThisSum, MaxSum = 0;
N = sizeof(A)/ sizeof(A[0]);
int i, j, k;
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;
}
2)二重循环:时间复杂度n2
template<typename T>
int MaxSubseqSum_2(T& A)
{
int N, ThisSum, MaxSum = 0;
N = sizeof(A)/ sizeof(A[0]);
N = 8;
int i, j;
for (i = 0; i < N; i++){
ThisSum = 0;
for (j = i; j < N; j++){
ThisSum += A[j];
if(ThisSum>MaxSum)
MaxSum = ThisSum;
}
}
return MaxSum;
}
3)类似于二分法的迭代——分而治之:时间复杂度nlogn
int Max3(int A,int B,int C)
{
return (A > B) ? (A > C ? A : C) : (B > C ? B : C);
}
int DivideSum(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 = DivideSum ( A, left, center ); //递归求左子列和。
MaxRightSum = DivideSum ( A, center+1, 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+1; i <= right; i++ ) {
RightBorderSum += A[i];
if ( RightBorderSum > MaxRightBorderSum )
MaxRightBorderSum = RightBorderSum;
}//右边扫描结束。
return Max3 ( MaxLeftSum, MaxRightSum, MaxLeftBorderSum + MaxRightBorderSum );
}
template<typename T>
int MaxSubseqSum_3(T& A)
{
int N, ThisSum, MaxSum = 0;
N = sizeof(A)/ sizeof(A[0]);
return DivideSum(A, 0, N-1);
}
4)在线处理:时间复杂度n,已经是最小的时间复杂度了,每个数至少都有遍历一遍
template<typename T>
int MaxSubseqSum_4(T& A)
{
int i,N, ThisSum =0, MaxSum = 0;
N = sizeof(A)/ sizeof(A[0]);
for (i = 0; i < N; i++){
ThisSum += A[i];
if (ThisSum>MaxSum)//实时记录当前已经遍历的子列和的最大值
MaxSum = ThisSum;
else if (ThisSum<0)//如果子列和小于0就舍弃
ThisSum = 0;
}
return MaxSum;
}
随机生成数组,计算运行时间:
#include<iostream>
#include<cstdlib>
#include<time.h>
using namespace std;
#define MaxR 1e5
template<typename T>
int MaxSubseqSum_1(T& A);
template<typename T>
int MaxSubseqSum_2(T& A);
template <typename T>
int MaxSubseqSum_3(T &A);
template <typename T>
int MaxSubseqSum_4(T &A);
int Max3(int A,int B,int C)
{
return (A > B) ? (A > C ? A : C) : (B > C ? B : C);
}
template <typename T>
double Runtime(T& A,int(*func)(T& A),int times)
{
clock_t start,stop;
double duration;
start = clock();
int j;
for (j=0; j<times; j++){
func(A);
}
stop = clock();
duration = (double)(stop - start)/CLK_TCK/times;
return duration;
}
void Random(int *a,int n,int l,int r)//生成范围在l~r的随机数
{
srand(time(0)); //设置时间种子
for(int i=0;i<n;i++){
a[i]=rand()%(r-l+1)+l;//生成区间r~l的随机数
}
}
int main()
{
int A[1000];
Random(A,1000,-10,10);
cout << MaxSubseqSum_1(A) << endl;
cout << Runtime(A,MaxSubseqSum_1,1) << endl;
cout << MaxSubseqSum_2(A) << endl;
cout << Runtime(A,MaxSubseqSum_2,1) << endl;
cout << MaxSubseqSum_3(A) << endl;
cout << Runtime(A,MaxSubseqSum_3,1) << endl;
cout << MaxSubseqSum_4(A) << endl;
cout << Runtime(A,MaxSubseqSum_4,MaxR) << endl;
return 0;
}
运行结果:
203
0.268
203
0.0008
203
3.031e-05
203
1.71e-06
301
0.268
301
0.0008
301
3.148e-05
301
1.64e-06