|
地板
楼主 |
发表于 2011-10-22 16:31:36
|
只看该作者
6.22 真正的ADT
在讲解算法与数据结构的教科书,有一种语言抽象机制屡屡被谈到,这就是ADT。
类是真正的数据,,比类的成员数据更能代表数据的意义,我们的程序就是一个一个的数据,类是用用户的观点来表达现实生活中出现的各种各样的数据的最好方式,因此会有面向对象数据库的出现(数据库技术中,关系模式的下表达的数据给人的引象似乎是它是为专门的数值数据而建立的)
ADT就是指封装了事物属性跟作用的指代物本身,它的属性和作用是ADT之所以为ADT的意义所在。即数据类型不再是基本类型,而是结合了数据跟代码的抽象了的模型(是一种数据类型)
人脑往往不适于长辐记忆,因此也需要对象来表达一些数据(类型)或事物.(抽象抽取对象的可用部分),再在这里抽象上构建更为高层的抽象。也即,从问题到解决不是一步而就的,而是一层一层通过OO来建立抽象达成的,这就是多范型的“方案领域”和“应用领域”的概念。
多范型开发让你在高于面向对象的范畴里看待软件工程,所以真正的编程学习过程是要明白设计在先,这是基础的思想,然后去学习编码,,(以上二点都是方案领域)然后再去研究编程领域对现实问题的解法(这就是应用领域)
而且如果你知道可复用和可扩展对于软件工业来说是多么重要的一件事情,,,你就会知道面向对象是多么好的一种机制了(面向对象和面向构件,面向构件可以不用对象的方法,,但是很明显面向对象和面向构件都有一个共同点那就是它们都提供了可扩展,而这就促进了可复用)
数据接口是另一个很重要的概念
接口实际上就是将要用的部分作为某个接口提取出来(就是定制要使用的实现的某个子集而已),而所有实现就是接口下的所有代码,即数据本身,故其实数据与数据接口是分离的
设计模式不致于使我们声明对象的过程变为硬编码,,,这样就使得整个软件的对象生成是动态的,这个过程就如同动态分配内存,寻求一种好的方式来组织类的过程称为设计模式或策略
接口这个概念其实无比自然,无论是为了隐藏实现的安全考虑,还是为了更好地让这些实现为用户所用这个方面来说,接口都是必需的,
接口几乎改变了现今我们开发软件的方式
举个例子,在现实生活中,经理人可以卖票,,,个人可以卖票,国家也可以卖票,,在面向对象的范畴里(注意这里并非指某个具体的面向对象设计),我们一般是把国家,个人,经理人都各声明为一个对象,再为它们各自添加一个“卖票”服务,,但是事实上,我们需要的仅仅是卖票这个服务,(如果这个服务被包含在一个发布的第三方代码库中为我们所用),我们只需提取这个可用部分,,而不是要知道提供这个服务的各个提供者的细节(还好上面只是列举了三个对象提供了这个服务,,然而现实生活是复杂的,还存在成千上万个对象也可以提供这个服务),,因此,对于复用者我们来说,我们需要的仅仅是卖票这个服务,而不需要知道卖票服务这个背后的情况(而且,对于发布这个第三方代码库的第三方来说,这可以更有效地隐藏它的实现细节)
接口就是把我们所需要的对象部分封装起来提供给我们,而把我们压根不需要知道的关于对象的细节隐藏,,,也就是说,,接口是关于一个对象如何能被使用的形式的封装者(一个对象可以有多种形式被使用,因此可以一个对象提供多个接口),是真正的对象(实现)和外界复用的包装器和桥梁,也称适配器(即接口就是直接面向使用的中间件,或封装了给二个不同架构提供适配作用使它们能协作运转的中间逻辑)
.
6.23 Vector的观点
vector就是数组的数组,我们知道逻辑上的多维数组必须转成一维数组的方式被存储,C中的一维数组是地地道道的数组,,这个意义上的数组是实实在在的,,因为它们是按内存线性地址存储的,,所以明显地,,C语言中的其它泛数组的adt都是抽象的,,从C用一维数组实现多维数组这个意思上,可以看出C语言的确是面向底层的,,它企图用底层解释一切。由于它的这种作为,导致了表示方式上的一些多义性,比如多维数组的指针跟元素的表示混淆不错。
C中泛数组有普通多维数组,动态数组和vector,其中又以vector最为典型,我们知道,多维数组要转为逻辑等价的一维数组,,有三种方式,,1,row列优先,这就是C的方式,,比如foo[3][4],它有三行4列row,它先每一行排3个,再4列,,2,行优先,这也就是pascal的方式,但是这二种方式都有局限,因为它们只能实现为方形规则的数组,要突然这种局限就是vector的事情了,
3,,就是vector方式,,C标准库中倒是没有一个vector,我们讲c++的stl中的vector,一门语言要模拟多维数组,必须要达到一种“多维数组就是数组的数组”这样的抽象,而vector就是这种思想的本质,,因为3维数组是2维的,这就是说,3维数组是2维数组的数组,而由3到2,就是把3维中的其中一维标准化了,,缩为1了,,这正是vector的思想,不要把线性代数中的矢量跟这里的混淆了,但在线性代数中这样的比较有巧合性,比如矩阵(对应多维数组)是由矢量(某维缩为1的矩阵)所组成的,,矢量就是数组中的数组了。。比如foo[3][4][5],可以是:
(1)一个下标为3的一维数组,,每个元素都是[4][5]的二维数组;
(2)..
(3)..
6.24 真正的数据结构
数据结构是什么?它是组织内存中对象或基本类型数值(primtive types)的形式,为了更好地组织和使用这些对象而慢慢发展起来的固有形式,惯用法(idioms),
数据分类与ADT
基本类型数值是没有构造函数的,而Java中对于数值类型的封装形式就有构造函数,这是它们二者本质的不同,像C++中用new动态声明一个对象,它一定用到了这个对象所属类的某个构造函数,可以说C与C++的最大区别就是C++用到了对象ADT,而C没用到(因此C压根不需要构造函数来着),STL可以说是一种总数据结构.
很多地方都用到到向量,,在学汇编时,,中断向量表就是一种向量,为什么STL没有arrary而只有vector呢?因为数组一般都用来实现vector,而且数组是语言的内含类型,一定程序上不提供太多的接口(根本因为是作为数值形式的数值没有被wrapper成一个first class因此不能作为函数的参数—因为没有复制构造函数,只能作为指针形式间接来操作它,也因此不能成为一个函数的返回值),而Vector可以提供跟数组类似的结构(也有多维数组)和比数组更高级的使用接口(一般数组类型只能是静态的不可伸缩的,而Vector可作为动态数组动态改变大小)
实际上多维数组并不是在内存中是一个标准的矩阵(学过线代就知道,任何一种矩阵都可以化为它的等价三角矩阵),比如C或C++就用一种行或列优先的方式来索引其元素。
数组是一个静态配置的空间,在命名方面JAVA做得比C++好(个人看法),比如INT MyVar[10];作为数组名的MyVar带了序列,而JAVA中INT[] MyVar;这就有点相当于type *是指向某种type的指针类型,而type[]是某种大小的数组类型一样好记
下面来阐述几个易混淆的概念
顺序线性表,,有序线性表(orderd list),hasp map(可用关联容器来表达)都是列表,比如字典顺序,也即字母顺序的一种关联机制
线性表即通俗意义上的“线性数据结构”,线性的意思是什么呢,它一定要满足几个规则(1,有唯一的第一个元素和最后一个元素,2除了第一个元素都有一个前驱,,3,除了最后一个元素,都有一个后缀)
上述的元素就是数据结点的意思,数据和数据结点之间的关系可表达为B=(K,R)的二元组,满足关系的二个元素一个称为前驱一个为后继
计算机存储数据的方式只有二种,顺序存储与离散存储(又有链式,索引式,Hash式),这是面向计算机端的存储结构,本向用户端的逻辑结构有集合(set),,哈希(hash map是一种map),,表(table),数组(array),,向量(vector),,图,树,map映射,线性表,节点链式表linked list,多维数据结构(matrix)等等,这种关系就有点像数据库的内外模式之分。逻辑结构间也有高级的演变方式,比如用vector来实现矩阵和table map是映射,,,set是集合,,都是关联型的数据结构,因此可用关联容器来表达 集合这种数据结构是用位集来描述数据集(可能以一个数组的形式存在),通过移位和位运算
线性表就是不能随机存储的表,像数组就是线性的,堆栈和队列也是线性的,有双向链表的存在(list),,dequece(double end quece),
表table就对应关系数据中的模式,,也即一个记录是一个表,它的各个字段就是表的竖维聚合数据类型就是像图(map)啊,树啊之类的,树是一种特别的图(每二个节点都有通路)而且是简单图
图的存储结构主要由它的邻接矩阵来表示,或邻接表,而树的存储表示主要由以下三种表示:结点表示法,兄弟子女表示法,等
这就是关联,,关联一定有key和一个value,,它们一同被存入数据结构,然后运用某种机制(可能是hash)通过key来索引value图是一种离散结构而非一种数据结构
图的边就是顶点之间的关系,这种关系是单向的或者双向的
像MFC的消息系统用的就是表驱动方式(它的消息映射表就是一个静态表)
我们知道,栈是一端开口的,往往从高端压和出栈(称为栈顶)后进先出的(但是后出先进这种说法是不存的,因为当一个栈没有数据时,它就不能出任何东西,因此只能说后进先出,先假设它有至少一个数据存在)因此它有当前指针和栈顶指针这二个元素来表示(当前指针指),栈往往用数组来模拟(++p,p--这样的形式),栈其实是一种跟它的实现形式无关的思想而已(是一种逻辑结构),因此可以说是数组(指针数组)来摸拟(顺序存储的存储结构),可以用数组的一端,当,而队列是一种先进先出的,它二端开口,有三个描述元素(尾,头和当前指针),它往往被实现作为一个管道(因为队列从意义上来讲它也其实就是一端进而从另一端出的数据结构啊)作为缓冲,或forward给其它处理list是列表的意思,有序列表orderdlist,链表(linked list是一种orderd list),堆栈,队列都是一种orderd list就像链表和数组都可以仿真堆栈一样,,树啊,图啊都是思想模型,链表和数组才是实际存储的机制.对数据结构的讨论中经常用到递归,特别是树中,因为树本质就是一个递归结构,在回溯节点时就是回溯同一种节点(因此这个节点可用递归描述 )
递归跟回溯,栈
递推与迭代还是有区别的,递归就是用自己来定义自己,,一般不需要一个循环,,而迭代需要从1开始,将这个循环变量一直自加到最大值(循环不变量?),,需要一个循环,一般来说,迭代比递归更有效率(在某些专门对递归进行了优化的环境中除外)
对一种数据结构的讨论常常不但要明白它们的工作原理,还要明白它们的操作,如查找,排序等,数据结构存储结构和这些操作就构成了ADT
《数据结构C语言版》
前言:这是我在2005.7月 - 2005.9月署假看《数据结构C语言版 - 清华大学出版社 黄国瑜叶艿菁著》时写的读书笔记,现在把它发布出来,希望对大家有用,也算是作个备忘录吧,不科学之处,还望高手斧正.
6.25 堆栈与队列
堆栈与队列都是数据结构(更复杂的还有树,图)在关系上是平等的数据结构,实际上堆栈与队列都是"内含在空间里的数据块",堆栈,队列的本质就是"数据块",不过它们都是包含在特定内存空间里的"有序数据块",如下图(4.bmp,用数组模拟"特定内存空间")
"特定内存空间"可以用数组表示,也可以用链表表示,数组是内存中的线形空间,也就是说,数组可以在内存中开辟一段空间,该空间是线性连续的,对该空间里任一元素的存取要根据"索引值"来进行,这些索引取从0~MS-1(如定义一个int queue[ms]或int stack[ms])是线性递增的,与数组空间从低地到高地的排列一一对应,数组的每一个空间都可有数据,也可无数据,每个空间的大小为一个int,整个数组大小就是ms个int,但是无论如何,对其中任何一个元素的存取(存是往数组任意位置里存入一个大小为int的空间,或在某个无数据内容的数组空单元空间里赋值,显然,这个数组空间是本来就存在于数组内的,而不同于链表要动态开辟一个新空间,而如果是前一种情况,数组就不再是静态的空间了,因为它的大小由ms变成了ms+1,而这是不可行的,同理,取是释放一个空间单元,这就使数组总空间大小由ms变为ms-1,或在某个空间里赋值0,术语称置0,而事先不管这个空间里有无值,如果有值,有的是什么值),其实,对数组索引的描述不但可用0~MS-1,当然也可用1~MS,但是为了方便考虑,把前一种看作为常用的,专业的方式,当然还可用2~ms+1(3种方式在定义了一个大小为ms的数组的情况下都可行可用),因为数组空间是一定的,对其的表示方式当然可以自由地使用不同的方法,一切的一切,只要保证能正确存取到所需的数据为程序所用为准,因为这是数组这种数据结构要最终达到的作用和总则,另外还要保持易用(像0~ms-1就显得专业并且简单易用,1~ms就人性化,而2~ms+1就什么都不是),上图中的数组示意图都开了"口",是表示只能从数组的开口的那一端存取数据(数组每个元素都是空间里包含的内容值,即数据,请搞清"元素","数组每个空间单元","内容值"等的说法),是一种形象的表示方法,而标准的数组图示方法可如下表示5.bmp)哪为什么要开口呢?一个数组为什么能开口呢?这是因为前面提到,这个"数组开辟的空间"将作"堆栈空间"使用,也就是"用数组模拟堆栈",因此标准数组示意图就要经过一些变形以适应能正确表示堆栈(空间)的要求,首先第一点变化就是"开了口",第二个变化就是还加入了一个top指针,这二个变化适应"能正确地表示堆栈(空间)"而生,其实要说top指针,它还不是标准意义上的指针,指针是一个32位的整型,而这里的top本质还是索引值,而非内存或外存地址单元名称代号,只是因为它发挥了类似指针"寻址"的作用,因此将其看作为"索引型"的指针,数组"堆栈"相比链表"堆栈",数组"堆栈"中的top是索引,而链表"堆栈"中的top才是真正的指针,因此数组的索引本质上是一种线性循序查找,而链表"堆栈"的top才是指针,分散在内存空间非线性不密集,只能由指针指定查找,这里就比较了数组与链表的本质(数组是内存中一段紧密的空间块,各个空间单元的连接是线性紧密的,这是对数组的低层讨论,反应到数组中就是其中的各个元素,将上一个元素的索引加1就可定向到下一个元素的索引位置,将上一个元素的内存空间地址加一个空间单元长度可定向到下一个元素在内存空间的位置,用0~ms-1或1~ms这样的递增性的索引就可表示数组的各个空间并引用它们,存取其中的空间或元素),有高地址和低地址之分,索引由小到大递增的方向就是内存地址低地到高地的线性递增方向,而堆栈是内存中各个分散的节点数据,各个节点数据就是元素或者更确切地讲,各个节点数据中的内容值,或称数据值而非指针值,就是链表的各个元素,相比起数组的空间单元的"连接",各个链表单元空间,也即节点空间的链表使用一种指定式的数值指定法而非数组采用的循序式的查找定位法,各个无素之间分散的链接由内含在各个元素(节点数据)中的指针字段而非内容字段,数据字段来完成,因此对其每个元素的存取前首先确定元素位置时是不能像数组中循序进行的,而是已被指定的,当前元素的内存位置就在上一个节点数据(元素)的指针字段里,而链表结构里,显然就没有高地与低地这种数组里才有的特征.
另外,要注意堆栈和队列中所说,它们都是有序列表,上面讲到,4.bmp中各个空间里的"有序数据块"才是确切意义上的"有序列表",即堆栈,队列这二个词语作为概念所指的实体所在,那么,它们的有序性是靠什么来体现的呢?堆栈靠的是游离于0~ms-1之间的索引值top,那么堆栈就是指0到top的数据块,top有一个特殊情况,top也可等于-1,显然,此时它指向序列为0的空间的更低地址的一个空间,表示堆栈为空(即堆栈中没有元素再供出栈了,出栈<=>从栈中输出一个元素<=>从堆栈中释放一个元素<=>删除一个元素),前面谈到,出口和top的引入都为用一个标准数组变为"堆栈"数组提供了可能,堆栈数据块是出入有序的,因此称为有序列表,那么,堆栈是如何依靠top来实现其空间里数据块元素出入的有序性的呢?在堆栈里,出口是唯一的数据输入输出(压入即输入push)通道,而队列有2个数据出入口,准确的说法是一个出口,一个入口,而堆栈的出口和入口集中在数组空间的一端而已,示意图就是只能从数组空间的高地址方向端输入输出数据(而堆栈有2端前端后端或称头端尾端,即front,end与head,rear),从rear端入栈,从f端出栈,堆栈数据实体就是从f到r的数据块,在数组表示的图示中f在下,r在上,在链表表堆栈中,f在前,r在后(f此时也可称为头,r也可称作后端),在数组表示中,各个数据实体是各个数组空间里的内容值,而在链表表堆栈中,各个数据实体(f到r),堆栈就是各个节点的内容字段链接而成的,这些内容字段链接起来构成的一串数据值就是堆栈数据块实体,从f到r,可见f到r不是从低地到高地,也不是从高地到低地,因为链表整个空间是分散于内存中的"节点空间"链接起来的,各个节点空间是分散布列在内存中的,因此,各个节点空间的value字段也是分散于内存里的,链表的各个空间间实际上没有一条一条的链,这只是示意图中的形象表示,链表的各个空间的链接桥梁就是内含在各个节点空间中的指针字段,链表与数组的一个重要区别与优点就是链表使用指针而非索引来寻址,这样就可以通过改变指针的值来重新形成链表空间或增删元素(实际上也是重新形成链表空间),这样链表就是一种动态配置的空间,而数组就是一种静态配置的空间,数组中的每个空间在数组被定义后就存在了,其中的任一个空间都不能被增删,只能向其赋值内容值0,表示置空此空间单元,这就是静态配置的空间.
.
6.26 真正的递归
递归是一种思想,只要问题本身满足递归你才能,递归涉及到三个概念,回溯
递归定义
即用递归来定义一些离散结构(集合,函数),3.4递归算法,用递归思想来解决问题的一般化步骤(算法即解决特定问题的一般化步骤,”停机问题”证明不存在解决所有问题的算法)
这种关系就如同二叉树的定义和查找,因为二叉权的定义就是指明它的左右节点存在次序关系,所以可以用这种定义作为思想来对二叉树进行查找,这并不难理解
用对象本身来定义对象就是递归,,这是递归的描述性定义,,是不确定的,,,非形式的,
集合的定义和算法的定义只有在形式语言里面才有形式定义,,(特别是图灵机证明不存在所有问题的一般化算法时用到的集合和算法的概念)
递归是用自身来定义自身这种说法成立吗,先来看一个问题
你能描述以上一副画吗??(如果它的中心是无限循环的)
你可以这样描述,,,一副画的中心区域的内容是它自身(也是一副画,而且具有跟前面描述的一样的性质),这样一副画就是如上的画的定义
显然递归的说法在这个例子是成立的
序列函数的定义比较容易理解,,集合的递归定义常常用来产生一些合式公式
迭代与递归在不同的情况下各有其优势
兔子和斐波那契数
例4 免子和斐波那契数 考虑如下问题,一对刚出生的免子(一公一母)被放到岛上,每对兔子出生后两个月后才开始繁殖后代,如下表所示,在出生两个月后,每对兔子在每个月都将繁殖一对新的兔子,假定兔子不会死去,找出n个月后关于岛上兔子对数的递推关系。
如何理解该月新生兔子即为距该月二个月前的兔子总对数?如6月新生的兔子为4月总兔数,4月新生的兔子为2月总兔数,3月新生兔数即为1月总兔数?
对某个月的讨论要追到与它的前二个月的情况(题目意思如此:每对兔子出生后二月才开始生育),这里是某个月的新生兔子与它二月前兔子总数“相等”的情况(注意这是一种递推说法,在任意差为二的月份间都存在这种联系,比如3-1,4-2,5-3,6-4,而6-2则不能考虑,因为它超出了递推为2阶的阶数2),从第三个月开始,放上岛的第一对兔子A+A-生下了一对兔子B+B-(假设每生下来的一对兔子都是一对公母可生育),在B+B-被生育下来的这个三月份,B+B-并不生育,这对A+A-在第四个月继续生育C+C-,而B+B-在第四个月也不生育,第五个月A+A-继续生育,B+B-终于开始生育出一对兔子D+D-,,
上面描述的可以作为典型,因为从第三个月开始,它就满足“每对兔子出生后二个月才开始繁殖后代”“任意出生在a月份的兔子在二个月后的b月后才生育,b-a=2”的题目要求,作为典型的3-1,5-3的情况被提出,后来处理任意相差二个月的兔子情况都可参照以上的描述了.
这样问题便被缩小到相邻二个月之间的情况,,
(为什么这是对的呢,作为典型的3-1,5-3为什么可代表一切相邻二月的情况,上面说了,这是题目意思告诉我们的,而不是递推,n个月后的兔子跟n-1总数与n-2总数才是递推,,这个递推也是在假设不知道存在这个递推的情况下列出的一个式子,列出之后才发现它是一个线性组合的递归,列出这个式子之间设了an,这并不表明事先就知道an一定是个满足某种递推的通项,只是在列出式子之后才明白是一个递推,而列出式子的过程仅仅依赖于题目意思)
既然已经得知,5月兔子与3月兔子间存在联系,那么这种联系是什么呢?重要的是知道这个联系本身,这个联系可用于任意相邻二月之间(题目意思)
从以上3-1,5-3的描述中容易看出,
汉诺塔问题:
图1 初始状态
图2 柱1 n-1个盘移到柱3后的情形
我们的目标是计数移动步数,,,并不是如题目所说得出移动的具体方法和每个书面步骤
因为等我们得出步数这个结论后,就会发现如果具体去得出移动步骤会多傻
而且,尊照规则(一次只能移动一块盘子,最后全部原样移动到柱2而不是柱3,并且最重要的大在下小在上)把n个盘子成功转移,移动的方法也可以千千万万,,所以我们要求的移动步数是最少的移动步数,那么对这个问题(求出最少移动步数)该如何建模呢?
首先,我们设想这样一个步骤:第一步,把柱1的n-1个盘子移到柱3,保持小在上大在下的顺序(如图2所示),保留最下面一个底盘,第二步,把底盘移到柱2,第三步,再把从柱1移过来的n-1个盘按步骤1的方法和顺序移动柱2,至此完成(注意方法和顺序的说法,方法:步骤1是怎么样移盘的这次也怎么移,顺序:保持小在上大在下)
容易看出,使用更小的步数是不可能求解这个难题的
下面具体建模
按照上面设想的所产生最少移动步数的移法,首先,设Sn是把n个盘子从一根柱子移动到另一根柱子的需要的总步数,(问题是什么我们就设什么,,虽然我们并不知道这其实是一个为了满足递推关系的设法,虽然我们也不知道有了以上设法,再用递推关系就可以很好地解此题,因为从形式来看Sn像一个序列的通项,它指明Sn就是把n个盘子从柱1到柱3所需要的Step,S2就是2个,S10就是10所需要的Step,这个设法假定Sn与n之间存在序列关系),按照步骤1,移动次数可用Sn-1来表示,步骤2可用1来表示,步骤3亦为Sn-1,故有
Sn=2Sn-1+1 (Sn为把n盘从柱1移到柱3所需步数或直接就是)
注意Sn为什么不直接说是“设Sn是把n盘从柱1移到柱2所需步数”呢,这样说当然没有错,不过为了严紧性和通用性考虑还是这样设(本题当然是从柱1移到柱3,如果没有规定其实移到柱2作为中转也可,最后求移动到柱3上的步数同样满足Sn=2Sn-1+1,,况且还有很多同类的题目,移奶酪到盘子里什么的,所以为了通用性考虑还是如上设)
最后的问题:我们用迭代方法来求解这个递推关系 |
|