数据结构和算法能起到什么作用
数据结构是对在计算机内存中(有时在磁盘中)的数据的一种安排。数据结构包括数组,链表,栈,二叉树,哈希表等等。算法对这些结构中的数据进行各种处理,例如,查找一条特殊的数据项或对数据进行排序。
掌握这些知识以后可以解决哪些问题呢?粗略的估计一下,上述知识可以用于下面三类情况:
1.现实世界数据存储
2.程序员的工具
3.建模
这些并不是必须遵循的分类,但它们可以体现出数据结构和算法的重要性。
现实世界数据存储
现实世界数据指的是那些描述处于计算机外部的物理实体的数据。看几个例子:一条人事档案记录描述了一位真实人的信息,一条存货记录描述了一个真实存在的汽车部件或杂货店里的一种商品,一条财务交易记录描述了一笔支付电费的实际填写的支票。
举一个非计算机的现实世界数据存储的例子,有一叠3X5的索引卡片。这些卡片可以被用在不同的场合。如果每张卡片上写有某人的姓名,地址和电话号码,那么这叠卡片一定是一本地址薄。如果每一张卡片上写有家庭拥有物的名称,位置和价值,那么这一定是一本家庭财产清单。
当然,索引卡片并不能代表现在的科技发展水平。几乎所有以前用索引卡片处理的事务现在都可以用计算机来处理。如果想将旧式的索引卡片系统更新为计算机程序,便有可能发现回被如下问题所困扰:
- 如何在计算机内存中安放数据?
- 所用方法适用于100张卡片吗?1000张呢?100000张呢?
- 所用方法能够快速的插入新卡片和删除老卡片吗?
- 它能快速的查找一张特定的卡片吗?
- 若想将卡片按照字母的顺序排列,又应该如何去排呢?
然而,大多数程序比索引卡片要复杂的多。想象一下机动车管理局的数据库,这个库被用来记录驾驶员的执照的情况;或者看一个航班预定系统,这个系统存储了旅客和航班的各种信息。这些系统由许多数据结构组成,设计这些复杂的系统还需要应用软件工程的技术。
程序员的工具
并不是所有的存储结构都用来存储现实世界的数据。通常情况下,现实世界的数据或多或少会由程序的用户直接存取。但是有些数据存储结构并不打算让用户接触,他们仅被程序本身所使用。程序员经常将诸如栈,队列和优先级队列等结构当作工具来简化另一些操作。
现实世界的建模
有些数据结构能直接对真实世界的情况构造模型。其中最重要的数据结构是图。图可以用来表示城市之间的航线,电路中的连接,或是某一工程中的任务安排关系。其他诸如栈和队列等数据结构也会应用在时间模拟中。例如,一个队列可以模拟顾客在银行中排队等待的模型,还可以模拟汽车在收费站前等待交费的模型。
数据结构的概述
还可以通过另一个方面来看数据结构,那就是从它们的强项和弱项来看。下面的表格大致给出了它们的特性,就像从天空中鸟瞰地面的风景一样。如果对其中的术语不太清楚的话,请不要太着急。后续的学习过程中会陆续解释。
数据结构的特性
数据结构 | 优点 | 缺点 |
---|---|---|
数组 | 插入快,如果知道下标,可以非常快的存取 | 查找慢,删除慢,大小固定 |
有序数组 | 比无序的数组查找快 | 删除和插入慢,大小固定 |
栈 | 提供后进先出方式的存取 | 存取其他项很慢 |
队列 | 提供先进先出方式的存取 | 存取其他项很慢 |
链表 | 插入快,删除快 | 查找慢 |
二叉树 | 查找,插入,删除都快(如果树保持平衡) | 删除算法复杂 |
红--黑树 | 查找,插入,删除都快。树总是平衡的 | 算法复杂 |
2--3--4树 | 查找 ,插入,删除都快。树总是平衡的。类似的树对磁盘存储有用 | 算法复杂 |
哈希表 | 如果关键字已知则存取极快,插入快 | 删除慢,如果不知道关键字则存取很慢,对存储空间使用不充分 |
堆图 | 插入,删除快,对最大数据项的存取很快,对现实世界建模 | 对其他数据项存取慢,有些算法慢且复杂 |
算法的概述
许多将要讨论到的算法直接适用于某些特殊的数据结构,对于大多数数据结构来说,都需要知道如何:
- 插入一条新的数据项
- 寻找某一特定的数据项
- 删除某一特定的数据项
还需要知道如何迭代的访问某一数据结构中的各数据项,以便进行显示或其他操作。
另一种重要的算法范畴是排序,排序有许多种算法,我们后续讨论。
递归的概念在设计某些算法十分重要。递归意味着一个方法调用它自身。我们后续讨论(方法是Java的术语,在其他语言中,它被称为函数,过程或子例程)
一些定义
下面看一些术语
数据库(database)
我们将会使用数据库这个术语来表示在某一特定情况下所有要查阅的数据,数据库中的每一条数据都被认为是同样格式的。例如,如果使用索引卡片来做一本地址薄,其中所有的卡片便构成了一个数据库。文件这个术语有时也代表同样的意思。
记录(record)
记录是指数据库中划分成的单元。它们为存储信息提供了一个结构格式。在索引卡片的模拟系统中,每一张卡片就代表一条记录。当有许多类似的实体时,一条记录包含了某一个实体的所有信息。一条记录可能对应于人事档案中的某一个人,汽车供应存货目录中的某一个零部件,或是烹调书中的某一道菜谱。
字段(field)
一条记录经常被划分为几个字段。一个字段保存某一种特定的数据。在地址薄中的一张索引卡片上,一个人的名字,地址或电话号码都是一个独立的字段。
更复杂的数据库程序使用带有更多字段的记录,下图显示了一条记录,其中每一行代表了一个不同的字段。
关键字
在数据库中查找一条记录,需要指定记录的某一个字段为关键字(或查找关键字)通过这个特定的关键字来进行查找。例如,在一个地址薄的程序中,可以在每条记录的姓名字段中查找关键字“Brown”。当找到具有该关键字的记录时,便可以访问它的所有字段,而不仅仅是关键字了,可以说是关键字释放了整个记录。还可以通过电话号码字段或地址字段在整个文件中再次查找。在上图中的任何字段都可以被用作查找关键字。
面向对象编程
一、先来两小段通俗解释
面向对象(Object Oriented简称OO :如C++,JAVA等语言):
看名字它是注重对象的。当解决一个问题的时候,面向对象会把事物抽象成对象的概念,就是说这个问题里面有哪些对象,然后给对象赋一些属性和方法,然后让每个对象去执行自己的方法,问题得到解决。
面向过程(Procedure Oriented 简称PO :如C语言):
从名字可以看出它是注重过程的。当解决一个问题的时候,面向过程会把事情拆分成: 一个个函数和数据(用于方法的参数) 。然后按照一定的顺序,执行完这些方法(每个方法看作一个过程),等方法执行完了,事情就搞定了。
二、用3个例子说明两者的区别
例子一:
问题: 洗衣机里面放有脏衣服,怎么洗干净?
面向过程的解决方法:
1、执行加洗衣粉方法;
2、执行加水方法;
3、执行洗衣服方法;
4、执行清洗方法;
5、 执行烘干方法;
以上就是将解决这个问题的过程拆成一个个方法(是没有对象去调用的),通过一个个方法的执行来解决问题。
面向对象的解决方法:
1、我先弄出两个对象:“洗衣机”对象和“人”对象
2、针对对象“洗衣机”加入一些属性和方法:“洗衣服方法”“清洗方法”、“烘干方法”
3、针对对象“人”加入属性和方法:“加洗衣粉方法”、“加水方法”
4、然后执行
人.加洗衣粉
人.加水
洗衣机.洗衣服
洗衣机.清洗
洗衣机.烘干
解决同一个问题 ,面向对象编程就是先抽象出对象,然后用对象执行方法的方式解决问题。
例子二 : 打麻将例子 你 我 他
面向过程: 打麻将 (你,我,他)
---------解决问题 拆成一个动作,把数据丢进去
面向对象: 我.打麻将(你,他) or 你.打麻将(我,他) or 他.打麻将(我,你)
---------解决问题 抽象出对象,对象执行动作 。
例子三:
最后在用一个网上常见的五子棋例子来说明一下:
面向过程的设计思路就是首先分析问题的步骤:
1、开始游戏,2、黑子先走,3、绘制画面,4、判断输赢,5、轮到白子,6、绘制画面,7、判断输赢,8、返回步骤2,9、输出最后结果。把上面每个步骤用不同的方法来实现。
面向对象的设计则是从另外的思路来解决问题。
整个五子棋可以分为
1、黑白双方,这两方的行为是一模一样的,
2、棋盘系统,负责绘制画面,
3、规则系统,负责判定诸如犯规、输赢等。
第一类对象(玩家对象)负责接受用户输入,并告知第二类对象(棋盘对象)棋子布局的变化,棋盘对象接收到了棋子的变化就要负责在屏幕上面显示出这种变化,同时利用第三类对象(规则系统)来对棋局进行判定。
可以明显地看出,面向对象是以功能来划分问题,而不是步骤。同样是绘制棋局,这样的行为在面向过程的设计中分散在了多个步骤中,很可能出现不同的绘制版本,因为通常设计人员会考虑到实际情况进行各种各样的简化。而面向对象的设计中,绘图只可能在棋盘对象中出现,从而保证了绘图的统一。
三:面向过程与面向对象的优缺点:
用面向过程的方法写出来的程序是一份蛋炒饭,而用面向对象写出来的程序是一份盖浇饭。
蛋炒饭制作的细节不说了,最后的一道工序肯定是把米饭和鸡蛋混在一起炒匀。盖浇饭呢,则是把米饭和盖菜分别做好,你如果要一份红烧肉盖饭呢,就给你浇一份红烧肉;如果要一份青椒土豆盖浇饭,就给浇一份青椒土豆丝。
蛋炒饭的好处就是入味均匀,吃起来香。如果恰巧你不爱吃鸡蛋,只爱吃青菜的话,那么唯一的办法就是全部倒掉,重新做一份青菜炒饭了。盖浇饭就没这么多麻烦,你只需要把上面的盖菜拨掉,更换一份盖菜就可以了。盖浇饭的缺点是入味不均,可能没有蛋炒饭那么香。
到底是蛋炒饭好还是盖浇饭好呢?其实这类问题都很难回答,非要比个上下高低的话,就必须设定一个场景,否则只能说是各有所长。如果大家都不是美食家,没那么多讲究,那么从饭馆角度来讲的话,做盖浇饭显然比蛋炒饭更有优势,他可以组合出来任意多的组合,而且不会浪费。
盖浇饭的好处就是‘’菜”“饭”分离,从而提高了制作盖浇饭的灵活性。饭不满意就换饭,菜不满意换菜。用软件工程的专业术语就是”可维护性“比较好,”饭” 和”菜”的耦合度比较低。蛋炒饭将”蛋”“饭”搅和在一起,想换”蛋”“饭”中任何一种都很困难,耦合度很高,以至于”可维护性”比较差。软件工程追求的目标之一就是可维护性,可维护性主要表现在3个方面:可理解性、可测试性和可修改性。面向对象的好处之一就是显著的改善了软件系统的可维护性。
总结来说:
面向过程
优点:性能比面向对象高,因为类调用时需要实例化,开销比较大,比较消耗资源;比如单片机、嵌入式开发、 Linux/Unix等一般采用面向过程开发,性能是最重要的因素。
缺点:没有面向对象易维护、易复用、易扩展
面向对象
优点:易维护、易复用、易扩展,由于面向对象有封装、继承、多态性的特性,可以设计出低耦合的系统,使系统 更加灵活、更加易于维护
缺点:性能比面向过程低
四、面向对象编程的特性
三大基本特性:封装,继承,多态
封装
封装,就是把客观事物封装成抽象的类,并且类可以把自己的数据和方法只让可信的类或者对象操作,对不可信的进行信息隐藏。一个类就是一个封装了数据以及操作这些数据的代码的逻辑实体。在一个对象内部,某些代码或某些数据可以是私有的,不能被外界访问。通过这种方式,对象对内部数据提供了不同级别的保护,以防止程序中无关的部分意外的改变或错误的使用了对象的私有部分。
继承
继承,指可以让某个类型的对象获得另一个类型的对象的属性的方法。它支持按级分类的概念。继承是指这样一种能力:它可以使用现有类的所有功能,并在无需重新编写原来的类的情况下对这些功能进行扩展。 通过继承创建的新类称为“子类”或“派生类”,被继承的类称为“基类”、“父类”或“超类”。继承的过程,就是从一般到特殊的过程。要实现继承,可以通过 “继承”(Inheritance)和“组合”(Composition)来实现。继承概念的实现方式有二类:实现继承与接口继承。实现继承是指直接使用父类的属性和方法而无需额外编码的能力;接口继承是指仅使用属性和方法的名称、但是子类必须提供实现的能力。
多态
多态,是指一个类实例的相同方法在不同情形有不同表现形式。多态机制使具有不同内部结构的对象可以共享相同的外部接口。这意味着,虽然针对不同对象的具体操作不同,但通过一个公共的类,它们(那些操作)可以通过相同的方式予以调用。
五、五大基本原则:SPR, OCP, LSP, DIP, ISP
单一职责原则SRP(Single Responsibility Principle)
是指一个类的功能要单一,不能包罗万象。如同一个人一样,分配的工作不能太多,否则一天到晚虽然忙忙碌碌的,但效率却高不起来。
开放封闭原则OCP(Open-Close Principle)
一个模块在扩展性方面应该是开放的而在更改性方面应该是封闭的。比如:一个网络模块,原来只有服务端功能,而现在要加入客户端功能,那么应当在不用修改服务端功能代码的前提下,就能够增加客户端功能的实现代码,这要求在设计之初,就应当将服务端和客户端分开,公共部分抽象出来。
里式替换原则LSP(the Liskov Substitution Principle LSP)
子类应当可以替换父类并出现在父类能够出现的任何地方。比如:公司搞年度晚会,所有员工可以参加抽奖,那么不管是老员工还是新员工,也不管是总部员工还是外派员工,都应当可以参加抽奖,否则这公司就不和谐了。
依赖倒置原则DIP(the Dependency Inversion Principle DIP)
具体依赖抽象,上层依赖下层。假设B是较A低的模块,但B需要使用到A的功能,这个时候,B不应当直接使用A中的具体类: 而应当由B定义一个抽象接口,并由A来实现这个抽象接口,B只使用这个抽象接口:这样就达到了依赖倒置的目的,B也解除了对A的依赖,反过来是A依赖于B定义的抽象接口。通过上层模块难以避免依赖下层模块,假如B也直接依赖A的实现,那么就可能造成循环依赖。一个常见的问题就是编译A模块时需要直接包含到B模块的cpp文件,而编译B时同样要直接包含到A的cpp文件。
接口分离原则ISP(the Interface Segregation Principle ISP)
模块间要通过抽象接口隔离开,而不是通过具体的类强耦合起来
过程性语言的问题
面向对象编程语言的产生是由于发现过程性语言(诸如C,Pascal,和早期的basic)在处理大型的复杂问题时有些力不从心。为什么会这样呢?
有两类问题:一是程序与现实世界缺乏对应关系,二是程序内部的结构出现了问题。
对现实世界建模的无能为力
使用过程语言对现实世界问题进行抽象及概念化十分困难:方法执行任务,而数据存储信息,但是现实世界中的事物是对二者同时进行操作的。例如,路子上的自动调温器执行任务(炉子的开/关)但同时也存储信息(现在的温度和所希望的温度)
如果用过程语言来写一个自动调温器控制程序,可能会以两个方法furnace_on()和furnace_off(),即炉开和炉闭来完成,但是还会有两个全局变量currentTemp和desiredTemp,即现在的温度(由调温器提供)和希望的温度(由用户设置)。然而这些方法和变量并没有形成任何编程对象,在程序中不会出现任何可以称之为自动调温器的单元。这个单元的唯一概念仅存于程序员的脑海中。
对于大型的程序,有可能包括上百个类似调温器的实体,过程语言对这种情况会将程序搞得极为混乱,错误频繁出现,有时还完全不可能实现。因此需要一种可以更好的将程序中的事物与现实世界中的事物相匹配的语言。
粗糙的组织结构
解决程序的内部组织结构是一个更微妙而且事关重大的问题。面向过程的程序被划分为一个一个的方法。这种基于方法组织形式的一个巨大问题是它仅仅考虑了方法,而没有重视数据。当不得不面对数据时,它没有太多的选择。简言之,数据可以是一个特定的方法的局部变量,也可以是所有方法都可以存取的全局变量,就是无法(至少没有灵活的方法)规定一个变量只允许某些方法存取而不允许另一些方法存取。
当几个方法都要存取同一个数据时,这种不灵活性会产生问题。如果一个变量要想被一个以上的方法存取到,它必须是全局变量。但是全局变量会在不经意的情况下被程序中的任何一个方法存取,这就导致了频繁的编程错误。因此需要一种可以精调数据的课访问性的办法;使数据对应该存取它的方法是可用的,而对其他方法来说是隐藏的。
对象简述
对象的概念在编程社团中渐渐传播开来,它被当作解决过程语言所面临问题的途径之一。
对象
面向对象编程思想的关键性突破就是:一个对象同时包括方法和变量。例如,一个自动调温器对象不仅包括furnace_on()和furnace_off()两个方法,还包括currentTemp和desiredTemp两个变量。在Java中,这些变量被称为字段。
这个新的实体——对象,同时解决了许多问题。它不仅将计算机中的事物与现实世界中的事物联系的更加紧密,而且解决了在过程语言中由全局变量造成的麻烦。furnace_on()和furnace_off()两个方法可以访问currentTemp和desiredTemp,这两个变量对那些不属于自动调温器的方法是隐藏的,以防止它们被一些不可靠的方法所修改。
类
有些人任务对于一次编程技术的革命来说,一个全新的对象的概念就已经足够了,但世界上这并不足够。人们认识到在编程中由可能希望得到术语同一类型的好几个对象。例如,整幢大楼编写一个温度控制器的程序,在程序中必然会出现许多控制器对象。如果为每一个都指定一段程序的话会狠麻烦。类的概念便由此而生。
类是针对一个或多个对象的说明(或蓝图)。例如,下面是一个温度控制器类在Java中的形式:
class thermostat{
private float currentTemp();
private float desiredTemp();
public void furnace_on(){
//方法主体
}
public void furnace_off(){
//方法主体
}
}
Java语言中的关键字class引出了整个类的说明,随后是为这个而起的名字,在这个例子中,用thermostat(温度控制器)作为类名。大括号中是组成这个类的字段和方法。我们在这个例子中略去了方法体(方法的内容),通常每个方法都由若干行代码。
C语言的程序员会认为上面的语法规则同一个结构类似,而C++程序员会意识到除了在最后没有分号之外,它同C++中的类十分一致(既然如此,我们究竟为什么要在C++的类似情况下使用分号呢?)
创建对象
类的声明并没有创建这个类的任何对象(同样的,在C语言中生命一个结构体并没有创建任何变量),要想在Java中真正创建对象,必须使用关键字new。在创建对象的同时,需要将一个引用存储到一个具有合适的类型的变量中。
什么是引用?会在后面的部分更加详细的介绍。目前,先将引用认为是一个对象的名字。(实际上,它是一个对象的地址,但现在并不需要知道它真正是什么)
下面的例子说明了我们如何创建两个温度控制器类的引用,创建两个新的控制器对象,并将对它的引用存储在如下两个变量中:
thermostat therm1,therm2;//创建两个变量 引用
therm1 = new thermostat();
therm2 = new thermostat();
顺便提一下,创建对象也被称作实例化对象,常把对象叫做类的实例。
访问对象方法
声明一个类并创建了几个对象后,程序就有可能需要让这些对象相互作用。这是如何做的呢?
一般来说,程序的其他部分通过调用方法与这些对象相互作用,而不是通过它的数据(字段)。例如,若想使therm2(二号控制器)打开炉子,会写如下语句:
therm2.furnace_on();
点运算符(.)将一个对象同它的某一个方法(或有时同它的某个字段)连接起来。
至此我们(相当简单的)讨论了面向对象的几个最重要的特性。总结如下:
- 对象同时包括方法和字段(数据)
- 类是任意数目的对象的说明
- 创建一个对象,要将关键字new和类的名称连用
- 调用一个对象的方法,要使用点运算符
这些概念都很深奥难懂,第一次见到它们的时候更是不可能完全掌握,所有如果你现在感到有一些头晕的话,请不要着急。等见过更多的类和明白它们的功能之后,眼前的迷雾便会慢慢散去。
一个能运行的面向对象的程序
让我们来看一个能运行并真正有输出的面向对象程序。它描述了一个被称为BankAccount的类,该类模拟了银行中的账户操作。程序创建了一个开会金额,显示余额,存款,取款并显示新余额。
public class BankAccount {
private double balance;
//构造器
public BankAccount(double openingBanlance){
this.balance = openingBanlance;
}
//存款
public void deposit(double amount){
balance += amount;
}
//取款
public void withdraw(double amount){
balance -=amount;
}
//显示
public void display(){
System.out.println("余额 = " + balance);
}
}
public class BankApp {
public static void main(String[] args) {
//创建账户
BankAccount ba1 = new BankAccount(100);
System.out.println("交易前");
ba1.display();//显示账户
//存款
ba1.deposit(74.35);
ba1.display();
//取款
ba1.withdraw(20);
ba1.display();
}
}
BankApp类中的main方法通过 BankAccount ba1 = new BankAccount(100);创建了一个BankAccount类的对象,初始化开户金额为100, System.out.println();将用作其参数的字符串"交易前"显示出来,账户通过下面的语句显示它的余额
ba1.display();
随后程序从账户中存了一笔款,又取出一笔钱:
//存款
ba1.deposit(74.35);
ba1.display();
//取款
ba1.withdraw(20);
ba1.display();
程序将新的账户余额显示在屏幕上并退出。
BankAccount类
在BankAccount类中,唯一的数据字段是账户中的存钱余额balance。它有三个方法deposit()方法将余额加上一个数,withdrawal()将余额减去一个数,display()显示余额。
构造函数
BankAccount类还拥有一个构造函数,这是一个特殊的方法,在每个对象创建时都会被自动调用。构造函数总是鱼类的名称相同,在本例中它便叫做BankAccount()。这个构造函数有一个参数,它被用来在创建对象时对开户金额进行设置。
构造函数可以很方便的对一个新的对象进行初始化。如果程序中没有构造函数的话,则只能在创建时增加对deposit()的调用,以便将开户金额注入到账户里。
公有和私有
请注意在BankAccount类中的关键字public(公有)和private(私有)。这些关键字是访问修饰符,他们决定了某些方法是否能够访问另一个类的字段或方法。balance字段前面有一个private。当一个字段活方法为私有(private)时,它仅能被同一个类中的方法访问。因为main()不是BankAccount类中的方法,所以balance不能被main()中的语句访问。
在BankAccount类中每个方法都有访问修饰符public,因此它们可以被外部的类所访问。这就是为什么main()函数中的语句可以调用deposit(),withdrawal()和display()的原因。
一个类中的数据字段经常被设为私有的,而方法经常是公有的。这样可以保护数据,使之不会被其他类的方法所修改。所有外界的实体想要访问一个类中的数据,必须使用那个类自己的方法。数据就像是蜂后,它被隐藏在蜂巢里面,被工蜂(就是方法)精心地喂养、照料。