chapter_1 绪论

1. 什么是数据结构

数据:所有能够输入到计算机中,且能被计算机处理的符号的集合。
数据元素:是数据(集合)中的一个“个体”,它是数据的基本单位。
数据项:数据项是用来描述数据元素的,它是数据的最小单位。
数据对象:具有相同性质的若干个数据元素的集合,如整数数据对象是所有整数的集合。
数据结构:是指带结构的数据元素的集合。
数据元素之间的逻辑关系 -> 数据的逻辑结构。
数据元素及其关系在计算机存储器中的存储方式 -> 数据的存储结构(或物理结构)。
施加在该数据上的操作 -> 数据运算。

1、数据的逻辑结构表示:表格、二元组、图形
2、数据的存储结构表示:顺序存储结构、链式存储结构

逻辑结构用二元组表示为:B=(D, R)
其中,B是一种数据结构,它由数据元素的集合D和D上二元关系的集合R所组成。其中:
D={ di | 1≤i≤n, n≥0}:数据元素的集合
R={ rj | 1≤j≤m, m≥0}:关系的集合

序偶<x,y>(x,y∈D): x为第一元素,y为第二元素。
x 为 y 的前驱元素。
y 为 x 的后继元素。
若某个元素没有前驱元素,则称该元素为开始元素;
若某个元素没有后继元素,则称该元素为终端元素。
序偶<x,y>表示x、y是有向的,序偶(x,y)表示x、y是无向的

如下矩阵:
a[][4]={
2,  6, 3,  1,
8, 12, 7,  4,
5, 10, 9, 11};

对应的二元组表示为B=(D,R),其中:
 D={2, 6, 3, 1, 8, 12, 7, 4, 5, 10, 9, 11};
 R={r1, r2}     其中,r1表示行关系,r2表示列关系

 r1={<2,6 >, < 6,3>, <3,1 >,
     <8,12>, <12,7>, <7,4 >,
     <5,10>, <10,9>,<9,11>}

 r2={< 2,8 >, <8,5>, <6,12>,
     <12,10>, <3,7>, <7,9>,
     < 1,4 >, <4,11>}

逻辑结构类型:
1、集合
元素之间关系:无。
特点:数据元素之间除了“属于同一个集合”的关系外,别无其他逻辑关系。
是最松散的,不受任何制约的关系。

2、线性结构
元素之间关系:一对一。
特点:开始元素和终端元素都是唯一的,除此之外,其余元素都有且仅有一个前驱元素和一个后继元素。

3、树形结构
元素之间关系:一对多。
特点:开始元素唯一,终端元素不唯一。
除终端元素以外,每个元素有一个或多个后续元素;
除开始元素外,每个元素有且仅有一个前驱元素。

4、图形结构
元素之间关系:多对多。
特点:所有元素都可能有多个前驱元素和多个后继元素。

存储结构类型:
1、顺序存储结构
2、链式存储结构
3、索引存储结构
4、哈希(散列)存储结构

数据类型和抽象数据类型

在高级程序语言中提供了多种数据类型。不同数据类型的变量,其所能取的值的范围不同,所能进行的操作不同。
数据类型是一个值的集合和定义在此集合上的一组操作的总称。
数据类型和数据结构的关系:数据类型就是已经实现了的数据结构。

抽象数据类型(ADT)指的是从求解问题的数学模型中抽象出来的数据逻辑结构和运算(抽象运算),而不考虑计算机的具体实现。
抽象数据类型 = 逻辑结构 + 抽象运算

例如,定义复数抽象数据类型Complex
一个复数的形式:e1+e2i 或(e1, e2)

ADT Complex
{
数据对象:D={ e1, e2  | e1,e2均为实数 }
数据关系:R={<e1, e2> | e1是复数的实部, e2是复数的虚部 }
基本运算:
      AssignComplex(&z, v1, v2):构造复数Z。
      DestroyComplex(&z):复数z被销毁。
      GetReal(z, &real):返回复数z的实部值。
      GetImag(z, &Imag):返回复数z的虚部值。
      Add(z1, z2, &sum):返回两个复数z1、z2的和。
}  ADT Complex

2. 什么是算法

数据元素之间的关系有逻辑关系和物理关系,对应的运算有基于逻辑结构的运算描述和基于存储结构的运算实现。
通常把基于存储结构的运算实现的步骤或过程称为算法。

算法的五个重要的特性
(1)有穷性: 一个算法必须保证执行有限步之后结束;
(2)确切性: 算法的每一步骤必须有确切的定义,无二义性;
(3)输入:一个算法有0个或多个输入,以刻画运算对象的初始情况,所谓0个输入是指算法本身定除了初始条件;
(4)输出:一个算法有一个或多个输出,以反映对输入数据加工后的结果。没有输出的算法是毫无意义的;
(5)可行性:可通过基本运算有限次执行来实现,也就是算法中每一个动作能够被机械地执行。

【例】设计一个算法:求一元二次方程 ax2+bx+c=0 的根。
算法可以采用自然语言、流程图或者表格方式等来描述。

int solution(float a, float b, float c, float &x1, float &x2){
    float  d,x1,x2;
    d=b*b-4*a*c;
    if (d>0){
        x1=(-b+sqrt(d))/(2*a);
        x2=(-b-sqrt(d))/(2*a);
        return 2;     //2个实根
   }else if (d==0){
        x1=(-b)/(2*a);
        return 1;     //1个实根
   }else{             //d<0的情况
      return 0;       //不存在实根
   }
}

3. 算法分析

分析算法占用的资源 = CPU时间(时间性能分析) + 内存空间(空间性能分析)
算法分析目的:分析算法的时空效率以便改进算法性能。

一个算法是由控制结构(顺序、分支和循环三种)和原操作(指固有数据类型的操作,如+、-、*、/、++和--等)构成的。
算法执行时间取决于两者的综合效果。

void fun(int a[], int n){
    int i;                //原操作
    for(i=0; i<n; i++){
       a[i] = 2*i;        //原操作
    }
    for (i=0; i<n; i++){
       printf("%d", a[i]);//原操作
    }
    printf("\n");         //原操作
}

算法分析方式:

  1. 事后分析统计方法:编写算法对应程序,统计其执行时间。
    编写程序的语言不同、执行程序的环境不同、其他因素
    所以不能用绝对执行时间进行比较。

  2. 事前估算分析方法:撇开上述因素,认为算法的执行时间是问题规模n的函数。

求出算法所有原操作的执行次数(也称为频度),它是问题规模n的函数,用T(n)表示。
算法执行时间大致 = 原操作所需的时间×T(n)。
所以T(n)与算法的执行时间成正比,为此用T(n)表示算法的执行时间。
比较不同算法的T(n)大小得出算法执行时间的好坏。

【例】求两个n阶方阵的相加C=A+B的算法如下,分析其时间复杂度。

#define MAX 20    //定义最大的方阶
void matrixadd(int n, int A[MAX][MAX], int B[MAX][MAX], int C[MAX][MAX]){
    int i,j;
    for (i=0;i<n;i++){                //①,频度为 n+1,循环体执行 n次
        for (j=0;j<n;j++){            //②,频度为 n(n+1)
            C[i][j]=A[i][j]+B[i][j];  //③,频度为 n^2
        }
    }
}

解:除变量定义语句外,该算法包括3个可执行语句①、②和③。
T(n) = n+1 +n(n+1) +n^2
= 2n^2 +2n +1

算法中执行时间T(n)是问题规模n的某个函数f(n),记作:T(n) = O(f(n))
记号 "O" 读作 "大O",它表示随问题规模 n 的增大算法执行时间的增长率和f(n)的增长率相同。

"O"的形式定义为:
T(n) = O(f(n))表示存在一个正的常数M,使得当 n ≥n0 时都满足:|T(n)|≤M|f(n)|

f(n)是T(n)的上界,这种上界可能很多,通常取最接近的上界,即紧凑上界

也就是只求出T(n)的最高阶,忽略其低阶项和常系数,这样既可简化T(n)的计算,又能比较客观地反映出当n很大时算法的时间性能。

例如 :T(n) = 2n^2 + 2n + 1 = O(n^2)

一个没有循环的算法的执行时间与问题规模n无关,记作O(1),也称作常数阶。
一个只有一重循环的算法的执行时间与问题规模n的增长呈线性增大关系,记作O(n),也称线性阶。
其余常用的算法时间复杂度还有平方阶O(n2)、立方阶O(n3)、对数阶O(log2n)、指数阶O(2n)等。

各种不同算法时间复杂度的比较关系如下:
O(1)<O(log2n)<O(n)<O(nlog2n)<O(n2)<O(n3)<O(2n)<O(n!)

算法时间性能比较:假如求同一问题有两个算法:A和B,
如果算法A的平均时间复杂度为O(n),而算法B的平均时间复杂度为O(n2)。
一般情况下,认为算法A的时间性能好比算法B。

算法中的基本操作一般是最深层循环内的原操作。
算法执行时间大致 = 基本操作所需的时间 × 其运算次数。

在算法分析时,计算T(n)时仅仅考虑基本操作的运算次数。

【例】下列程序段的时间复杂度是( )。

count=0;
for(k=1;k<=n;k*=2){
      for(j=1;j<=n;j++){
         count++;
      }
}

答案:O(nlog2n)

【例】下列程序段的时间复杂度是( )。

void func(int n){
    int i=0,s=0;
    while (s<n){
        i++;
        s=s+i;
    }
}

答案:O(sqrt(n))

空间复杂度:用于量度一个算法在运行过程中临时占用的存储空间大小。
一般也作为问题规模n的函数,采用数量级形式描述,记作:S(n) = O(g(n))
若一个算法的空间复杂度为O(1),则称此算法为原地工作或就地工作算法。

【例】分析如下算法的空间复杂度。

int fun(int n){
    int i, j, k, s;
    s=0;
    for (i=0;i<=n;i++){
       for (j=0;j<=i;j++){
         for (k=0;k<=j;k++){
            s++;
         }
       }
    }
   return(s);
}

答案:算法中临时分配的变量个数与问题规模n无关,所以空间复杂度均为O(1)。

int max(int a[],int n){
    int i, maxi=0;
    for (i=1; i<=n; i++){
        if (a[i]>a[maxi]){
            maxi=i;
        }
   }
   return a[maxi];
}
void maxfun(){
    int b[]={1, 2, 3, 4, 5}, n=5;
    printf("Max=%d\n", max(b, n));
}

maxfun算法中为b数组分配了相应的内存空间,其空间复杂度为O(n)
如果max函数中再考虑形参a的空间,就重复累计了执行整个算法所需的空间。

最好、最坏和平均时间复杂度分析
...

4. 递归算法的时间复杂度与空间复杂度分析

【例】分析下列程序段的时间复杂度与空间赋复杂度。

void fun(int a[], int n, int k) {//数组a共有n个元素
    int i;
    if (k==n-1){
        for (i=0; i<n; i++){
            printf("%d\n", a[i]);//执行n次
        }
    }else {
        for (i=k; i<n; i++){
            a[i]=a[i]+i*i;       //执行n-k次
        }
        fun(a, n, k+1);
    }
}

分析调用fun(a, n, 0)的时间复杂度。

解:设 fun(a,n,0) 的执行时间为 T(n),fun(a,n,k) 的执行时间为 T1(n,k)
T(n)=T1(n,0)。

则有如下关系式:
T1(n,k) = n                 当k=n-1时
T1(n,k) = (n-k)+T1(n,k+1)   其他情况

则:
  T(n) = T1(n,0) = n+T1(n,1) = n+(n-1)+T1(n,2)
      = …= n+(n-1)+…+2+T1(n,n-1)
      = n+(n-1)+ …+2+n
      = O(n^2)

所以调用fun(a,n,0)的时间复杂度为O(n^2)。

分析调用fun(a, n, 0)的空间复杂度。

解:设fun(a,n,0)的空间为S(n),fun(a,n,k)的空间为S1(n,k)
S(n) = S1(n,0)。

S1(n,k) = 1             当k=n-1时
S1(k) = 1+S1(n,k+1)     其他情况

则:
   S(n) = S1(n,0) = 1+S1(n,1) = 1+1+S1(n,2)
        = … = 1 + 1 + … + 1
        = O(n)

所以调用fun(a,n,0)的空间复杂度为O(n)。

【例】分析下列程序段的时间复杂度。

int max(int a[],int i,int j){
    int mid=(i+j)/2,max1,max2;
    if (i<j){
        max1=max(a,i,mid);
        max2=max(a,mid+1,j);
        return (max1>max2)?max1:max2;
   }else return a[i];
}

分析调用max(a,0,n-1)的时间复杂度。

解:设调用max(a,0,n-1)的执行时间为T(n)
递归算法max(a,i,j)的执行时间为T1(n) (n=j-i+1)

有T(n)=T1(n)
T(n)=O(1)       当n=1(i=j的情况)
T(n)=2T(n/2)+1  当n>1(i<j的情况)


T(n) = 2T(n/2) + 1
     = 2[2T(n/2^2) + 1] + 1 = 2^2T(n/2^2) + 2 + 1
     = …
     = 2kT(n/2k) + 2k-1 + … + 2 + 1 (k=log2n)
     = 2k + 2k-1 + … + 2 + 1
     = 2*2k - 1
     = O(n)

调用max(a,0,n-1)的时间复杂度为O(n)
posted @ 2022-02-28 07:59  HelloHeBin  阅读(108)  评论(0编辑  收藏  举报