给数据结构初学者:跨过算法和程序之间的鸿沟

【摘要】学习数据结构时,将各种基本操作通过程序实现,可以加深对算法的理解,也是提高编程能力的一种有效手段。针对初学者在搭建算法和程序之间联系困难的问题,本文以线性表部分为例,介绍了如何从读算法中找出实现程序的线索,围绕算法和程序之间的联系、抽象的描述和具体的实现之间的关系,引导读者学到抽象算法的精髓,最后对实践的路线、方案等进行了总结,并给出一些建议。

【讲座和视频】见《讲座:跨过算法和程序之间的那道沟(带视频链接)

【正文】

  计算机是算法的科学。学习IT的童鞋,在算法中下多大的功夫都不为过。在学习《数据结构》课程的时候,将教材中给出的算法用程序代码描述出来,在实现的过程中,可以不断加深思考;在调试程序的过程中,对算法的细节能够进行精细的钻研,这些都是获得算法精髓的方法。算法往往用“伪代码”的形式给出,学生在学习过程中,将这种抽象的描述与能够执行的具体形态的代码之间建立联系,使得算法形象起来,这样一个学习过程,以及由此带来的体验,将会使学生在技术成长之路上受益菲浅。

  在我组织的“未来IT工程师协会/CSDN高校俱乐部”的活动中,结合同学们正在“算法与数据结构”课程,创办“算法达人修炼营”,组成合作学习团体,实践相关的各种算法,讨论在算法学习中遇到的问题,以此来提高驾驭算法的能力。为帮助同学们做好抽象的数据结构、算法与某种语言编写的程序之间的过渡,特撰写此文。

  结合我校大二同学已经有的知识结构,本文以严蔚敏老师的《数据结构(C语言版)》为基础说数据结构和算法,实现算法的语言用C和C++。(建议:读本文中,一边翻着教材才有感觉。)

  一、读算法中找出实现程序的线索

  要看懂算法,解决其中存在的障碍,需要同学们在读书时能够做到前后对照。

  以P23中的算法2.3为例讲读算法的方法,以及如何前后对照。

  算法2.3的顺序存储的线性表的初始化问题,伪代码是:

  为便于后续的说明,为算法加些行号:

1. Status InitList_sq(SqList &L){
2. 	//构造一个空的线性表L
3. 	L.elem =(ElemType *)malloc(LIST_INIT_SIZE * size(ElemType));
4.	if(!L.elem) exit(OVERFLOW);	//存储分配失败
5.	L.length = 0;	 //空表长度为0
6. 	L.listsize = LIST_INIT_SIZE;	//初始存储容量
7. 	return OK;
8. }

  这个算法要解决的问题非常显然,用思维导图表达出来是:

  算法中的逻辑非常简单,常有同学说,算法是能看懂。这得益于抽象(后面专门要说),使我们忽略了很多实现中要考虑的细节,所以容易看懂,这是抽象的好处。而恰好由于忽略了实现细节中的具体形态,使得在考虑如何实现算法时出现障碍。这不是一个大问题,却成为初学者起步的一个障碍,尤其是对程序设计的功底并不很深的同学。(程序设计功底的加强是必需的,但已经到了这个阶段,并不是一定要先补上那一课再能学数据结构,时候不等人。实际上,学数据结构,同时也促程序设计。)

  障碍主要来自于,算法描述中出现的“词汇”和曾经编程中用过的似乎并不相同。“字”都不认识,谈何理解,又何谈实现。实际上,会看书的同学应该发现,算法中出现的“词”,在教材前面都曾经出现过,我们找出来,将其联系到一起。

  说有些同学不会看书可能委屈,更多的是没有耐心,一门课程起步阶段,基础性的内容要看细了。

  算法第1行:Status InitList_sq(SqList &L)

  InitList_sq是函数名自不用说。Status 显然是函数InitList_sq()的返回值类型,但究竟是什么类型呢?C和C++中没有这种数据类型,其他语言中也没有,可以猜到是自定义类型。教材P10有解释:

  教材接着给出了在C语言实现算法时的建议:

//Status是函数的类型,其值是函数结果的代码
typedef int Status

  其实如果用PASCAL实现,需要按PASCAL语言的语法写作:

type Status=integer;

  一个函数执行结束后,函数结果的代码给出一些约定(如1是成功,0是失败)通过返回值通知调用函数执行的情况,这种设计很常见。那么,此处Status用整型表示,其具体取值与含义是什么?从算法第7行 return OK;可以看出,这个OK就是Status可取的值。同样在P10,有一些常定义(只列两行,ERROR在其他函数中用到):

#define OK 1
#define ERROR 0

  在PASCAL中,对应的定义是:

const OK=1;
const ERROR=0; 

  还没有说Java,不说不够意思。C/C++和PASCAL中利用自定义类型解决,而Java中没有提自定义类型一词,但实际就在不断地声明自定义类型(calss)。在此做自定义类实现涉嫌杀鸡用牛刀,一种合适的解决方法是用枚举类型(其实这种方法对C/C++也合适):

enum Status {ERROR, OK};

  理解:抽象的Status在各种语言中实现的途径不同,甚至在一种语言中也可以有不同的实现方案。算法这样的写法有两个方面的好处:(1)可以供使用不同语言编程的人使用;(2)对学习算法的人而言,可以忽略(用某语言实现的)细节,而将注意力集中到算法本身。这两点好处对于后面的复杂算法更加重要。再次强调,要习惯并喜欢上这种抽象的描述。

  接下来讲函数InitList_sq()的形式参数&L。

  形式参数&L的类型是对SqList类型的引用。SqList类型是何类型?自定义类型。SqList是一个结构体类型,其定义就在P22,算法2.3前的一点点:

  SqList结构体包括有三个数据成员,在函数中都会用到。Length和listsize成员的类型是整型int好理解,ElemType又是个什么类型?理解了前面Status抽象的意义后,可以猜到ElemType又是个抽象数据类型,对应的是顺序表中要存储的数据的类型。ElemType(见名知义,元素类型)在教材前面出现过,但放在不同应用背景下,可以给出不同的定义。这个数据可以是简单的整型(若干整数的序列构成一个线性表),也可以是浮点型,甚至ElemType是一个字符串、结构体。例如,可以是:

typedef int ElemType 

  还可以是:

typedef struct student{
  char num[10];
  char name[16];
  int age;
  float score;
}ElemType;

  在教材例2.4中,线性表中的数据是多项式中的一项,需要记录数据下系数和指数,使用了结构体:

typedef struct{
  float coef;   //系数
  int expn; 	 //指数
}ElemType;

  至于用其他语言和任务的实现,不再给出分析和示例,道理一样。

  下面的思维导图 给出一个直观些的总结:

  理解了上面的内容,从第2行开始就好办了。第2行注释说明了这个算法完成的操作,第3行分配内存空间:

L.elem =(ElemType *)malloc(LIST_INIT_SIZE * size(ElemType));

  malloc()函数是分配内存空间,相当于C++中的new运算符。查手册知malloc()的原型是:

void *malloc(unsigned int num_bytes); 

  由函数调用可以看出,分配的空间大小为LIST_INIT_SIZE * size(ElemType),足够存放LIST_INIT_SIZE(定义的常量,值为100,见P22)个ElemType型的值,返回的地址指针转换为指向ElemType型的指针。函数调用结束后,将指针赋值给L.elem。

  算法第4行:if(!L.elem) exit(OVERFLOW);是在存储分配失败时的处理,OVERFLOW是P10定义的常量,值为-2。

  第5行和第6行算法就不再多述。其实也都是C中的内容,不清楚的内容通过参考资料可以解决。

  从上面的描述可以看出,教材中说得是用伪代码描述算法,算法实际上已经就是程序了。以C语言实现为例,在实现时,需要把这段算法/程序所需要的其他内容写到一个文件中。需要写一些类型定义typedef、常量定义#define、包含文件#include等,这是语言的要求。另外,还要增加mian()函数作为程序运行的入口,在其中设置测试和调试程序的代码,如果必要,还应该增加一些专门用于输入、输出的函数。

  这样,为实践算法2.3写出的对应程序是:

#include<stdio.h> //printf()等
#include<malloc.h> // malloc()等
#include<process.h> //exit()
#define OK 1
#define OVERFLOW -2
typedef int Status;    //Status是函数的类型,其值是函数结果状态代码,如OK等
typedef int ElemType;  //ElemType是线性表中数据元素的类型,此处用int
#define LIST_INIT_SIZE 10 // 线性表存储空间的初始分配量,为方便测试,改为10
typedef struct
{
  ElemType *elem; // 存储空间基址
  int length; // 当前长度
  int listsize; // 当前分配的存储容量(以sizeof(ElemType)为单位)
}SqList;
Status InitList(SqList &L) // 算法2.3
{   // 操作结果:构造一个空的顺序线性表
  L.elem=(ElemType*)malloc(LIST_INIT_SIZE*sizeof(ElemType));
  if(!L.elem)
    exit(OVERFLOW); // 存储分配失败
 L.length=0; // 空表长度为0
 L.listsize=LIST_INIT_SIZE; // 初始存储容量
 return OK;
}
Status ListTraverse(SqList L)
{   	// 初始条件:顺序线性表L已存在
	// 操作结果:依次输出L中的每个数据元素,函数名没有用display,而是用了更专业的术语——遍历Traverse
	ElemType *p;
	int i;
	p=L.elem;
	printf("线性表当前容量为: %d,",	L.listsize);
	if (L.length>0)
	{
			printf("线性表中有 %d 个元素,分别是:",L.length);
			for(i=1;i<=L.length;i++)
						printf("%d ",*p++);
	}
	else
			printf("目前还是空线性表。");
	printf("\n");
	return OK;
}
int main()
{
	SqList La;
	Status i;
	i=InitList(La);
	ListTraverse(La);
	return 0;
}  

  对于上面的程序,运行结果为:

  上面的程序要用C++(或Java)实现时,将SqList定义为一个类,基本操作为成员函数(或称为方法)。

  二、理解算法和程序之间的“跨度”

  下面罗列网上收集来的几组说法,适当修改、补充,提供给读者从多个角度分别体会:

  说法1:

  算法是解决问题的步骤;程序是算法的代码实现。

  算法要依靠程序来完成功能;程序需要算法作为灵魂。

  说法2:

  算法与程序:(1)一个程序不一定满足有穷性,而算法需要在有限步骤内完成。(2)程序中的指令必须是机器可执行的,而算法中的指令则无此限制。(3)算法代表了对问题的解,而程序则是算法在计算机上的特定的实现。一个算法若用程序设计语言来描述,则它就是一个程序。

  说法3:

  从计算机的角度讲,程序是用一种计算机能理解并执行的计算机语言描述解决问题的方法步骤。算法是解决问题的方法步骤(不一定要让计算机能理解并执行)。程序设计是分析解决问题的方法步骤,并将其记录下来的过程,其关键就是将算法描述出来。

  数据结构课程里面的代码,都是伪代码,也就是说,用C编译器编译是通不过的,还要做很多的修改才可以。算法是编程的核心,算法出来了,我们就可以考虑用哪种语言实现比较简单,不一定要选C。学数据结构学的是算法思想,学会如何去解决问题,这才是最重要的,用C实现次之。在数据结构C语言版里面,我们只是将这种数据结构的操作用伪C代码描述出来而已。而在实现时,再考虑语言细节,将算法用计算机可以接受的方式重新描述即可。

  说法4:

  算法和程序都是指令的有限序列 ,但是:程序是算法,而算法不一定是程序。 

  算法和程序的区别主要在于:

  (1) 在语言描述上,程序必须是用规定的程序设计语言来写,而算法很随意;

  (2) 在执行时间上,算法所描述的步骤一定是有限的,而程序可以无限地执行下去。

  所以: 程序 = 数据结构 + 算法(这是面向过程程序设计的概念,在面向对象程序设计的语境下需要拓展。)

  三、理解算法与数据结构中的抽象

  首先谈一下计算科学中的抽象。

  抽象(Abstraction)是简化复杂的现实问题的途径,可以忽略一个主题中与当前目标无关的那些方面,以便更充分地注意与当前目标有关的方面。抽象并不打算了解全部问题,而只是选择其中的一部分,暂时不用部分细节。运用抽象,可以使我们的注意力集中到要解决问题的主要矛盾上来,主要矛盾解决了,再解决次要矛盾。例如,在用计算机解决问题时,先设计抽象的数据结构和算法,再考虑如何用程序设计语言表达出来。除了降低难度,还利于保证正确性、可靠性,也便于分工,便于工程组织。

  抽象是计算机科学与技术中最基本的思维模式之一。抽象包括两个方面,一是过程抽象,二是数据抽象。它侧重于相关的细节和忽略不相关的细节。抽象作为识别基本行为和消除不相关的和繁琐的细节的过程,允许设计师专注于解决一个问题的考虑有关细节而不考虑不相关的较低级别的细节。

  在学习数据结构时,要能看出抽象程序的高低。举一个例子,本文前面举例用的是P23的算法2.3,而不是p20的算法2.1,我为什么这样安排?

  线性表可以采取顺序表示和链式表示两种方法进行实现。算法2.3对线性表的初始化针对的是顺序表示。在链式表示时,针对初始化的操作,另有算法。算法2.3是和具体实现相关的算法。而算法2.1所涉及的两个线性表的合并,却是和采用的存储方式、数据结构的实现方法无关的,适合于上面提及的各种存储结构。从这个意义上讲,算法2.3的抽象程序比算法2.1的低,考虑细节,考虑实现更多。

  于是引出一个建议。在将教材中的算法转换为程序的过程中,不必依着教材的顺序实现,而是应该先实现抽象程度低的,再考虑抽象程度高的。算法2.1的描述中,调用了不少其他的基本操作,从这一点也可以看出其抽象程度高,实现的细节都“隐藏”到其他操作中了。当然在实现和测试时,这样的算法不应该优先安排。

  四、用编程语言实现算法的小结

  下面的思维导图给出了将算法写成程序的要点:

  五、实践指导

  以“第2章 线性表”为例,P19已经给出了抽象数据类型线性表的定义(ADT List)。无论采取什么样的数据结构,其中的基本操作都是要实现的。换句话说,在今后进行项目开发时,只要涉及线性表,用到的就是这样一组算法。在学习期间,建立起自己的算法库,还是一件非常有意义的事。

  在以实践为主的课外自主性学习中,可以采取下面的学习策略:

  • 阅读教材,注重看出教材编写的结构
  • 对某抽象数据类型
    • 钻研抽象数据类型ADT的定义,理由(要品出的味道)
      • 基本操作即要实现的算法
      • 基本操作中的形式参数经过提炼
      • 基本操作是具体数据结构的“总纲”、清单
    • 选定存储结构
      • 研习教材中结合出的算法
      • 自行设计教材中没有给出,但ADT中要求的算法
  • 编程并调试
    • 实现ADT中规定的基本操作
    • 必备:要实现的算法、这个算法的支撑算法、测试函数(main)

  以第2章线性表为例,依着这样的策略,在钻研P18的线性表的ADT基础上,可以安排出的实践任务是:

任务1(必做):用顺序表示(2.2节)实现线性表的基本操作

任务2(必做):用线性链表(2.3.1节)实现线性表的基本操作

任务3(选做):用循环链表(2.3.2节)实现线性表的基本操作

任务4(选做):用双向链表(2.3.3节),结合重新定义的基本操作(P37),实现线性表的基本操作

任务5(选做):项目实训,用采用顺序表或链表为存储结构

  在实践安排中,不必求多,每种数据结构

时间: 2024-08-03 18:35:51

给数据结构初学者:跨过算法和程序之间的鸿沟的相关文章

讲座:跨过算法和程序之间的那道沟(带视频链接)

讲座视频在我的土豆,链接<讲座:跨过算法和程序之间的那道沟> 相关博文链接:<给数据结构初学者:跨过算法和程序之间的鸿沟>,比看视频节约时间,得到的信息量大于看幻灯片. 以下是幻灯片: 在组织学生社团活动中,发现同学们有这个需求而想到做这个报告.未来IT工程协会的日常课外学习活动见<2012-2013-学生社团学习活动组织(链接)>,本讲座针对的学习活动见:C04-算法达人修炼营学习安排及方法指导      

十大编程算法助程序员走上大神之路

十大编程算法助程序员走上大神之路 算法一:快速排序算法 快速排序是由东尼·霍尔所发展的一种排序算法.在平均状况下,排序 n 个项目要Ο(n log n)次比较.在最坏状况下则需要Ο(n2)次比较,但这种状况并不常见.事实上,快速排序通常明显比其他Ο(n log n) 算法更快,因为它的内部循环(inner loop)可以在大部分的架构上很有效率地被实现出来. 快速排序使用分治法(Divide and conquer)策略来把一个串行(list)分为两个子串行(sub-lists). 算法步骤:

c语言数据结构,求算法

问题描述 c语言数据结构,求算法 把一个单链表LA中的奇数项和偶数项分开,分别放在两个单链表LB,LC中(要求利用原空间,头结点空间可另外开辟) 解决方案 (C语言-数据结构与算法)还原二叉树数据结构和算法系列 - c语言归并排序法 解决方案二: 对LA进行遍历,依次把LA中的项加入LB,LC中.依靠修改原LA中项的指针实现. 解决方案三: //输入时以-1结束#include <stdio.h>#include <stdlib.h>struct node{ int data; s

cc++ 数据结...-数据结构关于抢红包算法问题

问题描述 数据结构关于抢红包算法问题 老师叫用队列模仿写一个微信抢红包程序,但不知道具体思路和算法,有了解的求赐教,菜鸟在这跪谢了! 解决方案 不要想很复杂,其实老师的让你做的关键不是什么抢红包,而只是队列. 定义一个链表或者数组来表示队列,实现入队.出队操作.入队在这里就是提交红包请求,出队就是抢到红包,先到先得,数量达到为止. 解决方案二: 数据结构与算法问题 朋友圈

《C++ 开发从入门到精通》——2.5 算法是程序的灵魂

2.5 算法是程序的灵魂 图片 1 知识点讲解:光盘:视频\PPT讲解(知识点)\第2章\算法是程序的灵魂.mp4 任何程序语言都需要进行大量的运算,为达到某个目的以获取指定的结果,这就需要了解算法的基础知识.算法是对操作的描述,是编程语言实现一种功能的操作方法.任何一门语言都有自己的数据类型,通过数据类型,能够实现具体的功能. 2.5.1 算法的概念 一个程序应包括对数据的描述和对操作的描述2个部分,其中,"数据的描述"在程序中要指定数据的类型和数据的组织形式,即数据结构(data

基于Silverlight的本地应用程序之间的通信

通过本地消息传递,您可以在单台计算机上运行的多个 Silverlight 插件之间创建通信通道.您通常 在单个网页中承载这些插件,并且使用本地消息传递来协调其行为.这样,您可以创建将多个基于 Silverlight 的应用程序与基于其他技术的内容结合在一起的复杂布局. 例如,假定您要为基于 HTML 的网站提供基于 Silverlight 的导航系统.您可以使用 Silverlight 来创建菜单栏和侧栏,并且使用本地消息传递来保持其状态同步.另一个方案就是创建具有跨越侧栏广告 的动画效果的基于

Excel在应用程序之间拖动信息

有些应用程序可让我们选定数据或图形,并且把它们拖动到另一个活动的应用程序窗口中.不过,应用程序必须正在运行并且出现在屏幕上.注意:两个应用程序都必须支持对象链接和嵌入的第二版(OLE 2),才能在应用程序之间拖动信息. 在应用程序之间拖动数据时可用以下选项. 拖动数据如同复制和粘贴,也会在目标文档上建立相同的数据,拖动到目标文档内的数据格式取决于数据类型. 在拖动的同时会把数据从源文档处剪切下来,并且粘贴到目标文档上.数据会永远从源文档中移走. 使用拖动数据的操作步骤如下: (1)分别调整Wor

在Windows Phone 8与Windows 8应用程序之间共享代码

Visual Studio 2012 为构建 Windows 8 和 Windows Phone 8 应用程序提供了一套出色的工具.因此,可以进 行适当的探究,以了解可在应用程序的 Windows 应用商店版本与 Windows Phone 版本之间共享多少代 码. 您可以采用多种不同语言编写 Windows 应用商店应用程序:XAML 搭配 C#.Visual Basic .C++,甚至是 HTML5 搭配 JavaScript. 通常采用 XAML 搭配 C# 或 Visual Basic

Android是如何实现应用程序之间数据共享的?

一个应用程序可以将自己的数据完全暴露出去,外界更本看不到,也不用看到这个应用程序暴露的数据是如何存储的,或者是使用数据库还是使用文件,还是通过网上获得,这些一切都不重要,重要的是外界可以通过这一套标准及统一的接口和这个程序里的数据打交道,例如:添加(insert).删除(delete).查询(query).修改(update),当然需要一定的权限才可以. 如何将应用程序的数据暴露出去? Android提供了ContentProvider,一个程序可以通过实现一个Content provider的