数据结构C语言 Part1 引入篇

什么是数据结构呢?没有官方定义,不过数据结构+算法==程序。而很好的Data Structure(本学期以后简称DS)可以带来最优效率的算法。解决问题的效率,和数据的组织方式、空间的利用效率以及算法的巧妙程度有关。应该说,算法和数据结构都很重要。按学校的安排,我们先学DS再学Algorithm。我们先看几个例子引入一下:

例子一:写程序计算给定多项式在某一个给定的点的值:

介绍与一些知识储备:

#include <stdio.h>
#include <time.h>

//这个多项式是f(x)=a0+a1x+a2x^2+...+an*x^n

//我们用最直接的想法来计算:
double f1(int n,double a[],double x) //我们要尽可能避免浮点数和整型的四则运算
{
    int i;
    double p=a[0];  //a[i] represents the value of i(th) term in sequence
    for(i=1;i<=n;i++)
        p+=a[i-1]+x*p;
    return p;
}


//当n足够大了,我们看得出来这个计算量还是很大的,不如我们改进一下算法:
double f2(int n,double a[],double x)
{
    int i;
    double p=a[n];
    for(i=n;i>0;i--)
        p=a[i-1]+x*p;
    return p;
}

//这个思路是基于f(x)=a0+x(a1+x(...(an-1 + x(an))...))

/*接下来我们再介绍一下clock()这个函数,这个函数是捕捉从程序开始运行 到clock()被调用所消耗时间,这个时间单位时clock tick,即“时钟打点”。常数CLOCKS_PER_SEC代表机器时钟每秒走过的时钟打点数,即1000ms*/

clock_t start,stop; //clock_t 时clock()函数返回的变量类型
double duration; //记录被测函数的运行时间

int main()
{
    start=clock();
    function();               //这里代表一个抽象的函数模块,你懂我的意思吧
    stop=clock();
    duration =((double)(stop-start))/CLOCKS_PER_SEC; //计算function的运行时间
    exit(0);   //啊哈,题外话,exit是退出当前进程,return只是退出当前的函数
}

这里我们简化一下问题,就是求f(x)=x+2x^2+...+n*x^n在x=1.1处的值f(1.1),按顺序结构编写代码,如下:

我们再谈谈ADT--抽象数据类型:

一、数据类型:数据对象集与 数据集合相关联的操作集

二、抽象:描述数据类型的方法不依赖于具体实现,于存放数据的机器无关,与数据存储的物理结构无关,与实现操作的算法和编程语言无关。也就是说,我们只关注数据对象集和操作集是什么,而不关注其如何做到的实现机理。

你譬如说:矩阵,的抽象数据类型定义,我们可以给出如下的定义:

类型名称:矩阵(Matrix)

数据对象集:一个m×n的矩阵Am×n=(aij),其中i从1到m,j从1到n,由m*n哥三元组(a,i,j)构成,a代表这个位置元素的大小,i,j代表行号和列号。

扫描二维码关注公众号,回复: 3476880 查看本文章

操作集:对于任意矩阵...和整数...,有:

Matrix Create(int M,int N);//返回一个M*N的空矩阵

int GetMaxRow(Matrix A);//返回A的总行数

Matrix Add(Matrix A,Matrix B);//如果A,B可加,则返回他们的和矩阵,否则返回错误提示信息

...(etc)。

我们接下来看看算法,第一章都是很浅的东西,这个我们瞅瞅浙大的ppt就好。

可见,算法需要的时间规模是需要我们比较严格把控的,比如当基数比较大,我们就不推荐用选择排序,而用快速排序等更好的排序方式。

另外在分析复杂度的时候,别的我们不说了,if-else结构的复杂度取决于if的条件判断复杂度和两个分支部分的复杂度,总体复杂度取三者中最大者。

接下来我们应用一下我们前面所学的知识,来解决一个最大子列和的问题,顺便再看我的代码的时候,想想复杂度该是多少。

应用实例:最大连续子列和问题(进一步可以引申到背包问题)。

给定N个整数的序列{A1,A2,A3。。。An}

求函数f(i,j)=max{0,Σ Ak},其中求和符号下界是k=i,上界是j

在这里我们提供四种算法,复杂度不同。

//算法一
int MaxSubsequenceSum1(int A[],int N)
{
    int ThisSum,MaxSum=0;
    int i,j,k;
    for(i=0;i<N;i++)             //i是子列左端位置
    {     
        for(j=i;j<N;j++)         //j是子列右端位置
        {
         ThisSum=0;              //ThisSum代表下标从i到j的子列和
         for(k=i;k<=j;k++)
            ThisSum+=A[k];       //当i,j固定了就可以求出一个ThisSum了
         if(ThisSum>MaxSum)
            MaxSum=ThisSum;       //若得到的子列和更大,就更新MaxSum结果
        }
    }
    return MaxSum;
}

这个思路是最直接的思路,由于i,j的位置不确定性,所以我们需要先后遍历i、j,再对i到j位号的元素求和,来求出这样一个sum。可见,这个思路的时间复杂度是T(N)=O(N^3).

//算法二
int MaxSubsequenceSum2(int A[],int N)
{
    int ThisSum,MaxSum=0;
    int i,j;
    for(i=0;i<N;i++)          //i是子列左端
    {
        ThisSum=0;            //ThisSum代表位号为i~j的子列和
        for(j=i;j<N;j++)      //j是子列右端
        {
            ThisSum += A[j];
            /*对于相同的i,不同的j,只需要再在j-1次循环的基础上累加一项就可以了*/         
            if(ThisSum>MaxSum)
            MaxSum=ThisSum;
        }
    }
    return MaxSum;
}

这个思路是在第一个思路上进行的改编,算法复杂度是T(N)=O(N^2).

算法三:分治法,虽然暂时不是很懂,算法复杂度是O(NlogN)

int Max3( int A, int B, int C ) 
{
 /* 返回3个整数中的最大值 */   
  return A > B ? A > C ? A : C : B > C ? B : C; 
} 


int DivideAndConquer( int List[], int left, int right ) 
{ 
/* 分治法求List[left]到List[right]的最大子列和 */    
 int MaxLeftSum, MaxRightSum; /* 存放左右子问题的解 */    
 int MaxLeftBorderSum, MaxRightBorderSum; /*存放跨分界线的结果*/  
   int LeftBorderSum, RightBorderSum;  
   int center, i;   
   if( left == right ) 
   { /* 递归的终止条件,子列只有1个数字 */    
     if( List[left] > 0 )
     return List[left];   
     else return 0;    
   }   
  /* 下面是"分"的过程 */   
  center = ( left + right ) / 2; 
  /* 找到中分点 */  
   /* 递归求得两边子列的最大和 */     
   MaxLeftSum = DivideAndConquer( List, left, center );   
  MaxRightSum = DivideAndConquer( List, center+1, right );   
  /* 下面求跨分界线的最大子列和 */  
   MaxLeftBorderSum = 0;
 LeftBorderSum = 0;   
  for( i=center; i>=left; i-- ) 
  { 
  /* 从中线向左扫描 */       
  LeftBorderSum += List[i];       
  if( LeftBorderSum > MaxLeftBorderSum )         
    MaxLeftBorderSum = LeftBorderSum;    
  }
 /* 左边扫描结束 */   
  MaxRightBorderSum = 0;
  RightBorderSum = 0;  
   for( i=center+1; i<=right; i++ ) 
   { /* 从中线向右扫描 */      
   RightBorderSum += List[i];       
  if( RightBorderSum > MaxRightBorderSum )      
       MaxRightBorderSum = RightBorderSum;    
   } 
  /* 右边扫描结束 */   
  /* 下面返回"治"的结果 */    
 return Max3( MaxLeftSum, MaxRightSum, MaxLeftBorderSum + MaxRightBorderSum );
 } 
int MaxSubseqSum3( int List[], int N ) { /* 保持与前2种算法相同的函数接口 */    
 return DivideAndConquer( List, 0, N-1 ); }
//算法四:在线处理
int MaxSubsequenceSum4(int A[],int N)
{
    int ThisSum,MaxSum;
    int i;
    ThisSum=MaxSum=0;
    for(i=0;i<N;i++)
    {
        ThisSum +=A[i];       //向右累加
        if(ThisSum>MaxSum)    
            MaxSum=ThisSum;   //发现更大的子列和就更新当前结果
        else if(ThisSum<0)    //如果当前子列和为负
            ThisSum=0;        //就不可能使后面的部分和增大,抛弃之
    }
    return MaxSum;
}

T(N)=O(N),在线的意思是指每输入一个数据都能得到即时处理,在任何一个地方中止输入,算法都能正确给出当前的答案。一般来说,效率如此之高,会带来副作用,这个副作用,就是...不太容易理解。不过,当你举个例子,就能懂为什么要这么写了。这个算法的核心思想是,当前面的子列和为负数,我们就把它清零,抛弃不要,从后面重新开始,因为对于最优解而言,前面的不可能再提供积极的作用,只可能提供消极的作用。这个思路我也是今天才见到,觉得很巧妙,因为不论如何你把这个序列看一遍,复杂度都是N,而这个算法竟然可以控制复杂度在O(N),理解机理之后,真的很妙,发现了编程之美。

我们给出了以上四种算法,可见输入规模较大的时候,前两个算法就吃不消了。

我们在数据结构作的铺垫就到这里了,国庆节假期也正式完结了,明天第一堂课就是数电、数据结构,我也要加油鸭!

线性表、链表的知识我们随后再讲吧。

猜你喜欢

转载自blog.csdn.net/qq_42229034/article/details/82953520
今日推荐