现在到了数学抽象中最关键的一步:让我们忘记这些符号所表示的对象。不应该在这里停滞不前,有许多操作可以应用于这些符号,而根本不必考虑它们到底代表着什么东西。

--Hermann Weyi 《思维的数学方式》

构造数据抽象

现在考虑一个完成有理数算术的系统,可以设想一个运算add-rat,以两个有理数为参数,产生它们的和。从基本数据出发,一个有理数可以看作两个整数的组合--分子和分母,其过程可以用add-rat实现,一个产生和数的分子,另一个产生和数的分母,在使用这些数据时,有很多有理数时,其对应的分子和分母成对出现,如果不加规则约束,将会使得代码越来越难读懂,所以希望考虑一种复合数据的方法,将数据“粘合”起来。

处理复合数据中的一个关键性思想是闭包的概念--也就是说,用于组合对象数据的粘合剂不但能用于组合基本的数据对象,同样也可以用于复合的数据对象;另一个关键思想是复合对象能够成为以混合与匹配的方式组合程序模块。

数据抽象导引

1)抽象数据的基本思想,就是设法构造出一些使用复合数据对象的程序,使他们就像是在“抽象数据”上操作一样。

2)一种“具体”数据表示的定义与程序中使用数据的方式无关。

[注]  与面向对象的思想相同,“类的写法”,在作者写书的那个年代,面向对象编程的概念还未成为主流,那个年代的思路为数理逻辑的方法,定义符号变量,并进行推导。上面提到的思想就是面向对象编程中类的属性和方法,只不过是用函数式编程实现。

有理数类

今天我们可能会这样构造一个Point类,从数据和操作的思路来看,面向过程和面向对象实现的功能一样。

  1. class RationalNumber(object):
  2. def __init__(self, x, y):
  3. #有序对(x,y)分别表示分子和分母
  4. self.x = x
  5. self.y = y
  6. def numer(self):
  7. return self.x
  8. def denom(self):
  9. return self.y
  10. def get(self):
  11. return self.x/self.y
  12. if __name__ =='__main__':
  13. a = RationalNumber(4,5)
  14. print(a.get())
有理数的运算

如果此时从顶层到底层的思路设计程序,考虑三个函数

def make_rat(n,d): 返回一个有理数:

def numer(x)返回有理数x的分子,def denom(x) 返回有理数x的分母。

两个有理数的加减乘除可以写成:

  1. def add_rat(x,y):
  2.    return make_rat(x,y)
  3. def make_rat(x,y):
  4.    return (numer(x)*denom(y) +numer(y)*denom(x))/(denom(x) *denom(y) )

然而numerdenom未定义,如果定义一个有序对,numer返回有序对的第一个位置(分子),denom返回有序对的第二个位置(分母),然后再来一次套娃:

  1. x = [1,2]
  2. y = [3,4]
  3. z = [x,y]
  4. def car(x):
  5.    return x[0]
  6. def cdr(x):
  7.    return x[1]
  8. def numer(x):
  9.    return car(x)
  10. def denom(x):
  11.    return cdr(x)
  12. print(numer(cdr(z)))

上述例子中的def car(x):相当于numer(x)def cdr(x):相当于denom(x)

  1. x=[6,9]
  2. def print_rat(x):
  3.    print(str(numer(x))+'/'+str(denom(x)))
  4. print_rat(x)
  5. 此时输出为:6/9,没有约分到最简形式,为了化简,考虑最大公约数
  6. def gcd(a,b):
  7.    if b==0:
  8.        return a
  9.    else:
  10.        return gcd(b,a%b)
  11. def print_rat1(x):
  12.    g = gcd(denom(x),numer(x))
  13.    #一定是整除的,但是python语言会自动类型转换,保留小数位,为了显示好看->int()
  14.    print(str(int(numer(x)/g))+'/'+str(int(denom(x)/g)))
  15. print_rat1(x)

[注]: car 和cdr就是寄存器的概念Contents of Address part of Register”和 “Contents of Decrement part of Register“。

前面给出的所有有理数操作,都是基于构造函数make_rat和选择函数numer(x)denom(x)定义出来的,数据抽象的思想就是为每一类数据对象标识出一组操作,使得对这类数据对象的所有操作都可以用这些操作来表述,而且在对这些数据进行增删改查的时候也能只能用这些操作。

(从功能上而言,make_rat拆解了一个数使其写成其他函数的组合形式,numer(x)denom(x)这两个函数选择了有序对中数据的位置,在计算理论中,认为只要实现功能相同,即可认为是等同的)

数据意味着什么

有理数x能够写成两个整数的除法:

\[\frac{number(x)}{denom(x)}= \frac{n}{d}
\]

前面提到了选择函数和构造函数的思路,有理数x在程序中也用一个有序对来表示,在python中我们使用了List来实现:

  1. x = [a,b]

由给定的构造函数和选择函数所实现的东西,如果要严格形式化这一思想非常困难。目前存在着两种完成这一形式化的途径。

1)抽象模型的方法,它形式化了如上面有理数实例中所勾勒出的“过程加条件”的规范描述,而抽象模型方法总是基于某些已经有定义的数据对象类型,定义出一类新的数据对象。

2)代数规范。这一方式将“过程”看作是抽象代数系统的元素,系统的行为由一些对应于我们的“条件”的公理刻画,并通过抽象代数的技术去检查有关数据对象的断言。

层次性数据和闭包性质

前面在构造有理数时,序对提供了一种用于构造符合数据基本“粘合剂”,并且我们在使用时,规定了第一个位置为分子,第二个位置为分母,调用numer(x)denom(x)时,实现的功能也只是将对应位置为数据取出而已。(注意指针的思想)

例如有理数 1/2,carcdr分别取出第一和第二个位置的元素

此外前面提到的有序对的进一步抽象也能表示为:

用今天的思想来看就是指针的概念,标记地址,指向数据元素,以及数据结构中提到的线性表。根据规则,组合序对建立出来的元素仍然是序对,那么就是该规则的闭包性质--通过它组合数据对象得到的结果本身还可以通过同样的操作再进行组合,通过这种性质能够建立起层次性的结构。

序列

按照SICP的思路,抽象的时候考虑两个重要的因素:

1)基本的表达式 --> 数据的基本表示

2)方法的组合和抽象 --> 怎么操作这些数据

如果回忆一下二年级学过的数据结构,写一个线性表的代码为(以增删改查中的增和遍历为例):

  1. class Node(object):
  2. def __init__(self, val):
  3. # 定义当前结点值
  4. self.val = val
  5. self.next_node = None
  6. class MyList(object):
  7. def __init__(self, node):
  8. #定义当前结点值
  9. self.node = node
  10. def add_node(self,node):
  11. self.node.next_node = node
  12. def print_all_node(self):
  13. p_node = self.node
  14. while p_node!=None:
  15. print(p_node.val)
  16. p_node = p_node.next_node
  17. if __name__ =='__main__':
  18. firstNode = Node(1)
  19. secondNode = Node(2)
  20. #列表的第一个结点
  21. print('新建列表并插入第一个结点')
  22. myList = MyList(firstNode)
  23. myList.print_all_node()
  24. #列表的第二个结点
  25. print('列表插入第二个结点')
  26. myList.add_node(secondNode)
  27. myList.print_all_node()

而如果学了Python可能这样去写,并且python的List内置了appendpop方法用于元素的插入和删除

  1. if __name__ =='__main__':
  2. lst = [1, 2, 3]
  3. for i in iter(lst):
  4. print(i)
SICP中的序列和操作

在SICP中,数据组合抽象的思路,由于语言的限制,python虽然能实现和Lisp一样的功能,但是不建议这样做:

  1. def car(z):
  2. return z[0]
  3. def cdr(z):
  4. return z[1]
  5. if __name__ =='__main__':
  6. a = [1,[2,[3,[4,[5,None]]]]]
  7. node1 = car(a)
  8. print(node1)
  9. next_iter = cdr(a)
  10. node2 = car(next_iter)
  11. print(node2)

序列的操作

如果是遍历序列的第n个位置,假设序列的数据表示为a=[1,2,3,4,5],那么直接返回下标即可。而如果序列的数据表示为a = [1,[2,[3,[4,[5,None]]]]],可以利用构造的car()cdr()函数遍历:

  1. #返回第n个位置的元素(从0开始)
  2. def list_ref(a,n):
  3. if n==0:
  4. return car(n)
  5. else:
  6. return cdr(a,n-1)
  7. #返回list的长度
  8. def length_recursive(a):
  9. if a==None:
  10. return 0
  11. else:
  12. return 1 + length(cdr(a))
  13. #返回list的长度
  14. def length_iterative(a):
  15. count = 0
  16. def length_iter(a,count):
  17. if a == None:
  18. return count
  19. else:
  20. return length_iter(cdr(a),count+1)
  21. return length_iter(a,count)
  22. #list的拼接
  23. # a=[1,2,3],b=[4,5]==>c=[1,2,3,4,5]
  24. def list_append(list1,list2):
  25. if list1==None:
  26. return list2
  27. else:
  28. return [car(list1),list_append(cdr(list1),list2)]
  29. #list的mapping
  30. # a = [1,2,3] ==>f(a)=[2,4,6]
  31. def scale_list(items, factors):
  32. if items==None:
  33. return None
  34. else:
  35. return [car(items)*factors, scale_list(cdr(items),factors)]
  36. #mapping操作的高阶抽象
  37. def map_higher_order(f,items):
  38. if items==None:
  39. return None
  40. else:
  41. return [f(car(items)),map_higher_order(f,cdr(items))]
  42. if __name__ =='__main__':
  43. a = [1,[2,[3,[4,[5,None]]]]]

如果用数据结构的思路来审视:a=[1,2,3,4,5]a = [1,[2,[3,[4,[5,None]]]]]区别,前者可以理解为线性表,而后者为树:

序列的常规接口定义

借助数据抽象的思路来复合数据(基本表达式),其中屏蔽了程序实现的细节。有了抽象数据之后,就要考虑数据操作的抽象,即现在面向对象的软件设计中,经常提到的一些接口的设计,考虑如下两个程序:

example:以一棵树为参数,计算出奇数叶子节点的平方和


  1. #f为函数的高阶抽象
  2. def sum_odd_square(f,tree):
  3.     def tree_iter(tree):
  4.         if tree== None:
  5.             return 0
  6.         else:
  7.             if car(tree)%2==0:
  8.                 return 0 + tree_iter(cdr(tree))
  9.             else:
  10.                 return f(car(tree)) + tree_iter(cdr(tree))
  11.     return tree_iter(tree)
  12. if __name__ == '__main__':
  13.     
  14.     tree = [1, [2, [3, [4, [5, [6,None]]]]]]
  15.     print(sum_odd_square(lambda x:x*x,tree))

example:构造所有偶数的斐波那契Fib(k)的一个表,其中的k小于某个给定的整数n:

  1. def fib(n):
  2. if n==1:
  3. return 1
  4. elif n==0:
  5. return 0
  6. else:
  7. return fib(n-1) + fib(n-2)
  8. def even_fibs(n):
  9. def next_iter(k):
  10. if k>n:
  11. return None
  12. else:
  13. val = fib(k)
  14. if val%2==0:
  15. return [val, next_iter(k+1)]
  16. else:
  17. return next_iter(k+1)
  18. return next_iter(0)

对比两个函数的功能

def sum_odd_square(f,tree):

1)枚举树的所有叶子

2)过滤叶子结点,选择奇数

3)求平方

4)求和

def even_fibs(n):

1)枚举整数0到n

2)计算斐波那契数

3)过滤结果,取出偶数

4)将结果进行组合

filter

  1. def filter(predicate, sequence):
  2. if sequence ==None:
  3. return None
  4. else:
  5. if predicate(car(sequence)):
  6. return [car(sequence), filter(predicate,cdr(sequence))]
  7. else:
  8. return filter(predicate, cdr(sequence))
  9. def f(z):
  10. if z%2==0:
  11. return True
  12. else:
  13. return False
  14. print(filter(f,a))

SCIP:构造数据抽象--数据结构中队列与树的解释的更多相关文章

  1. 数据结构中常见的树(BST二叉搜索树、AVL平衡二叉树、RBT红黑树、B-树、B+树、B*树)

    树 即二叉搜索树: 1.所有非叶子结点至多拥有两个儿子(Left和Right): 2.所有结点存储一个关键字: 非叶子结点的左指针指向小于其关键字的子树,右指针指向大于其关键字的子树: 如: BST树 ...

  2. 数据结构中很常见的各种树(BST二叉搜索树、AVL平衡二叉树、RBT红黑树、B-树、B+树、B*树)

    数据结构中常见的树(BST二叉搜索树.AVL平衡二叉树.RBT红黑树.B-树.B+树.B*树) 二叉排序树.平衡树.红黑树 红黑树----第四篇:一步一图一代码,一定要让你真正彻底明白红黑树 --- ...

  3. 使用JavaScript的数组实现数据结构中的队列与堆栈

    今天在项目中要使用JavaScript实现数据结构中的队列和堆栈,这里做一下总结. 一.队列和堆栈的简单介绍 1.1.队列的基本概念 队列:是一种支持先进先出(FIFO)的集合,即先被插入的数据,先被 ...

  4. JavaScript学习总结(二十一)——使用JavaScript的数组实现数据结构中的队列与堆栈

    今天在项目中要使用JavaScript实现数据结构中的队列和堆栈,这里做一下总结. 一.队列和堆栈的简单介绍 1.1.队列的基本概念 队列:是一种支持先进先出(FIFO)的集合,即先被插入的数据,先被 ...

  5. [Data Structure] 数据结构中各种树

    数据结构中有很多树的结构,其中包括二叉树.二叉搜索树.2-3树.红黑树等等.本文中对数据结构中常见的几种树的概念和用途进行了汇总,不求严格精准,但求简单易懂. 1. 二叉树 二叉树是数据结构中一种重要 ...

  6. 浅析数据结构中栈与C实现

    最近在搞摄像头驱动,o()︿︶)o 唉,别提有多烦,一堆寄存器就有人受的了--特么这不是单片机的开发,这是内核驱动开发-- 今天放松一下,我们来看看数据结构中的栈,这节的知识点可以说是数据结构中最容易 ...

  7. jvm 中内存的栈和数据结构中的栈的区别

    1.常见的数据结构:栈.队列.数组.链表和红黑树,java内存划分 2.JYM中的栈是先进先出,先入栈的先执行: 2.数据结构中的栈是先进后出,类似手枪的弹夹,先进入的子弹最后才发射: 3.数据结构中 ...

  8. python爬取股票最新数据并用excel绘制树状图

    大家好,最近大A的白马股们简直 跌妈不认,作为重仓了抱团白马股基金的养鸡少年,每日那是一个以泪洗面啊. 不过从金融界最近一个交易日的大盘云图来看,其实很多中小股还是红色滴,绿的都是白马股们. 以下截图 ...

  9. 数据结构中的棧在C#中的实现

    数据结构中的棧在C#中的实现 一.大致学习 棧是一种面向表的数据结构,棧中的数据只能在标的某一短进行添加和删除操作,是一种典型的(LIFO)数据结构. 现实生活中的理解:自助餐厅的盘子堆,人们总是从顶 ...

随机推荐

  1. Apache 低版本不支持 WebSocket

    Apache 低版本不支持 WebSocket Apache HTTP Server Version 2.4 Apache Module mod_proxy_wstunnel https://http ...

  2. NGK公链如何构建区块链数字经济商业帝国?

    2020年对于区块链市场来说,重大的利好消息莫过于NGK公链的上线了.NGK公链其广泛的市场前景.顶尖的技术,一直备受众多大型机构以及投资者所看好.同时,NGK公链也不负众望,在上线以后,就开始落地到 ...

  3. 如何吃透Python的面向对象(OOP)

    ​本篇可乐和大家一起来吃透 Python 的面向对象,类和实例. 面向对象(OOP) 解释:面向对象它是一种编程的思想,将现实事物抽象化为编程对象. 举例说明:喝可乐 ① 选择自己根据配方买对应的材料 ...

  4. 如何用Eggjs从零开始开发一个项目(3)

    上一篇中我们编写了用户注册登录.登录的代码,学习了如何进行用户的认证(JWT),如何安全地存储用的密码(hash).这一篇我们有以下2个任务: 获取token中的数据: 通过model来同步数据库. ...

  5. 《C++ Primer》笔记 第8章 IO库

    iostream定义了用于读写流的基本类型,fstream定义了读写命名文件的类型,sstream定义了读写内存string对象的类型. 标准库使我们能忽略这些不同类型的流之间的差异,这是通过继承机制 ...

  6. 剑指 Offer 64. 求1+2+…+n + 递归

    剑指 Offer 64. 求1+2+-+n Offer_64 题目描述 题解分析 使用&&逻辑短路规则来终止循环 package com.walegarrett.offer; /** ...

  7. 剑指 Offer 57 - II. 和为s的连续正数序列 + 双指针 + 数论

    剑指 Offer 57 - II. 和为s的连续正数序列 Offer_57_2 题目描述 方法一:暴力枚举 package com.walegarrett.offer; /** * @Author W ...

  8. CentOS7安装 xmlsec1 编译并运行官方示例

    1. 自动安装下列软件和依赖(默认已安装libxml2和libxslt) yum install xmlsec1-openssl xmlsec1-openssl-devel 2. 查看官网 www.a ...

  9. golang操作mysql2

    目录 Go操作MySQL 连接 下载依赖 使用MySQL驱动 初始化连接 SetMaxOpenConns SetMaxIdleConns CRUD 建库建表 查询 单行查询 多行查询 插入数据 更新数 ...

  10. 测试平台系列(4) 使用Flask蓝图(blueprint)

    使用Flask蓝图(blueprint) 回顾 先来看一下上一篇的作业吧,使用「logbook」的时候,遇到了时区不对的情况.那么我们怎么去解决这个问题呢? 实际上logbook默认采用的是世界标准时 ...