admin 2025-12-08 06:20:40 世界杯专用足球

抽象数据类型精讲

最近笔者通过哈工大的软件构造课程,学习了抽象数据类型.

我们,将通过打比方的形式,通俗易懂的给大家讲明白ADT是谁,为什么要有这个东西,以及,怎么构建它

ADT:抽象数据类型

定义:抽象数据类型(Abstract Data Type,ADT)是计算机科学中具有类似行为的特定类别的数据结构的数学模型;或者具有类似语义的一种或多种程序设计语言的数据类型。抽象数据类型是间接定义的,通过其上的可执行的操作以及这些操作的效果的数学约束(与可能的代价)。

按照MIT的说法,ADT可以用下面的这幅图来进行描述:

直观的看,ADT就是一道墙,将内在的表示与外部的使用进行隔离这种隔离,通过接口进行的间接访问,降低了代码块与块之间的耦合性.

下面的两个比喻,形象生动的说明了ADT是干什么的,为什么这么重要:

就像盖房子,我们写的软件代码,java里面一个个的Class,就像很多的***普通的砖头***,我们并不需要他们每一个都那么的独领风骚,我们需要每一块砖头都长得差不多,标准一致,这样可以进行将砖头进行千百次的堆砌.同时,我们需要一块砖头如果觉得它存在质量问题,我们可以马上用另一块相似的砖头进行代替.

就像军舰里面的***水密隔舱***,通过一层一层的将船与外面的大海进行隔离,那么如果一侧因为中弹而进水,将防水门一关,进水将不会影响到军舰的其余部分.

为了能够造出更规则统一的砖头(水隔舱),无数计算机领域的巨佬,研究出ADT这样的指导思想,帮助外面更好的,造出统一而又安全的砖头.

可变与不可变数据类型

还是使用砖头来作比方:

如果我们想要自己用砖头搭建起来的房子是可以平稳存在的,那么,我们就首先得要求这个砖头,它是稳定的.

不可变数据类型就像是一块死砖头,它不会因为外界的操作而改变自己的属性,可以说这个砖头,它是死的.我们就可以依靠这个砖头来作房子的地基.从而使房子四平八稳.

比如说,上图的字符串String,就是不可变的,它如果对其进行操作,会直接返回新的一个对象

那么,如果我们的代码里面用了可变的数据类型,它会带来什么后果呢?

直接给外界你内部的属性的指针,让外界直接对内部属性进行修改.导致一些很阴间的bug.

比如:神不知鬼不觉的,数据结构内部属性的值一下子就变了…

/**

* @return the first day of spring this year

*/

public static Date startOfSpring() {

return askGroundhog();

}

********************************************

// somewhere else in the code...

public static void partyPlanning() {

Date partyDate = startOfSpring();

// ...

}

/**

* @return the first day of spring this year

*/

public static Date startOfSpring() {

if (groundhogAnswer == null) {

groundhogAnswer = askGroundhog();

}

return groundhogAnswer;

}

private static Date groundhogAnswer = null;

**********************************************

// somewhere else in the code...

public static void partyPlanning() {

// let's have a party one month after spring starts!

Date partyDate = startOfSpring();

partyDate.setMonth(partyDate.getMonth() + 1);

// ... uh-oh. what just happened?

}

所以,正如大厦的地基必须要用混凝土和坚实的砖石来打,程序内部所依靠的数据类型,最好是采用不可变的数据类型

Spec 规约

假如我们是来给别人家房子的施工队,在出发之前,一般都是要和雇主甲方爸爸签一个文书的,这个就是规约Spec

这个spec,它通过规定双方的责任,也就是产品问题,找谁说理去.

细分为两大类:

前置条件: 如果产品坏了,这个责任在用户.

后置条件: 如果产品坏了,这个责任在施工队.

在我们写Java程序时,其实,也有这样的一个spec要去完成

它长这样:

/**

* Find a value in an array.

* @param arr array to search, requires that val occurs exactly once

* in arr

* @param val value to search for

* @return index i such that arr[i] = val

*/

static int find(int[] arr, int val)

不难发现里面的前置与后置可以大致分下:

前置条件

@param arr array to search, requires that val occurs exactly once in arr

@param val value to search for

find(int[] arr, int val)

后置

Find a value in an array.

@return index i such that arr[i] = val

它就像一个防火墙,隔绝开了用户与程序内部的表示.

换一种视角 数据->操作

如果我们对之前学的数据结构与算法换一种视角来看,我们的数据结构其实可以被一组操作进行刻画.

比如说,以我手里的茶杯为例

它作为一名普普通通的茶杯,它可以用以下操作来刻画

class TeaCup{

/**

* 喝茶

*/

public Drink(){};

/**

* 倒茶

*/

public Fill(Water Tea){};

}

我们作为21世纪的消费者,是不需要了解这个茶杯它是用硅酸盐组成的.然后里面添加了什么样子的化学物质来进行塑性等待的细节.我们使用的,只是它给我们的两个方法:喝茶,倒茶.

ADT也一样,我们给外界的 是一组操作,而非内部的表示.

四大er,四种方法

具体来说,这些操作可以细分为四大方法:

以杯具的一生为例:

构造器 : 凭空直接给你造出一个茶杯

生产器 : 放入一个白色的茶杯,与一些颜料,给你返回一个黑色的茶杯

观察器 : 看下茶杯是否为空的,里面的茶叶是什么品种的.

变值器 : 放入一个空茶杯,返回一个打满了茶水的茶杯.

来一点数学: 抽象函数AbstractFunction

我们上面说了这么多,其实可以用一个简单的数学映射加以描述

AF(内在表示) ==> 抽象值

程序员需要利用号内在的表示值,同时编写程序,来进行映射,向外界提供观测,修改抽象值的接口.

这便是抽象.

通过抽象,隐藏底层复杂的实现细节,将间接,可靠一致的抽象操作提供给外界,从而达到降低系统整体的耦合度的效果.

底层的实现细节就是R空间.而用户关注的空间,或者说,我们给用户呈现的空间,就是A空间,即抽象空间.

下面就是一个例子,通过抽象函数,我们将字符串映射到了集合空间A里面.

左边的R,是Representation,即数据表示.这个,便如我们施工队的比方,是我们房子下面的地基.

这个地基,必须要稳,否则房子本身就是不稳定的,有害于人民群众的财产安全.

所以,我们作为ADT的编写者,有责任去做点事情,来确保这个房子是稳定的.

体现为:

RI 表示不变量

还是以集合为例,我们用按顺序排列的字母来队这个集合进行表示,所有串里面的字符排列,必须是升序的.

这个升序,就是RI,它对表示空间进行了划分,将我们用到的合法的值,映射为true,其余为false

用函数的角度来说

RI(表示)==>{true,false}

我们还可以在代码里面对这个进行检查,因为只要我们的表示什么时候被RI给映射到了false,那么就意味着,我们的代码出bug了…

上述内容便是ADT的一些介绍,谢谢大家.