前言
本篇文章围绕刚开始学习数据结构时接触到的术语具象化解释,涵盖其他学习分支的部分已在文章中给出了系列文章链接。在此过程中,博主会尽量用科学准确的文字去解释术语,同时结合生活中的案例分析,穿插部分代码讲解以方便大家能更好理解;学识不精,内容上如有错误的地方还请大家指正🙏。
一、相关术语
首先,让我们着眼“数据结构”这个词, 依据不同词义将其分为“数据”、“结构”和“数据结构”三个词,分别从三个角度对这个概念进行解释。
👉什么是数据?
先看一个场景:假设张三是某公司的人事部职员,某天经理发给张三一个Excel文件并要求“统计各部门员工的办公软件考核成绩”,此时,这份文件就是张三要处理的数据(如下图)。有时,我们也会说,这份文件包含了多种数据,例如员工编号、姓名、年龄等,此时,数据又代表着该文件中的列标签。
张三接到任务后立即绘制了一张数据透视图和一张折线图,将其打包后发给经理,经理看完后称赞说“你这个数据分析做得还可以”。此时,这里的数据指的是散点图
从上述场景中我们可以得知,数据可以是一个文件、一个中文标签(在处理文本数据时称为字符串)、一张图片甚至其他格式,它有多重代号但始终是对客观事物的描述,可以说,数据是客观事物的符号表示。
计算机可接收多种数据类型作为输入(如.png/.jpg等图片格式的文件、.rtf/.csv/.text等文本编辑格式文件、二进制文件等等),为了方便描述,定义数据元素作为数据的基本单元。以图1为例,在整个文件中,所有数据都可以按第一行的标签分成十二类(共有十二列),每一列下有多个不同的实例,如“等级”标签下有“优”和“良”,张三要做的工作就是要对不同标签下的数据对象进行求和统计;每一列标签就是数据元素,在该数据元素下有多个数据项
👉什么是数据结构?
如图2就是一个表格型文件,由数据元素按照分层关系共同构成,可以说结构就是数据元素之间的关系,而数据结构是相互之间存在一种或多种特定关系的数据元素的集合。
数据类型是一个值的集合和定义在这个值集上的一组操作的总称,而我们常说的抽象数据类型(Abstract Data Type,ADT)是指一个数学模型以及定义在该模型上的一组操作,并且对抽象数据类型的规格说明应该独立于值的存储表示和操作实现。
👉接口函数、模块、包、库
本篇文章侧重于基本概念的介绍,模块、包接口设计以及相关说明文档的编写请参考系列文章。
- 接口(Application Programming Interface,API),类比USB(通用串行总线)——提供了一种标准接口,允许用户把计算机连接到数码相机、智能手机等其他设备,实现跨平台的资源共享。接口在软件中和它在硬件中一样常见, 通常代表函数,隐藏和封装着底层的算法代码和数据结构,从而:
- 降低了用户在初次接触软件时学习的难度
- 让用户有机会在相同的资源不同实现中做选择
- 允许开发者对资源的实现作出修改,而不影响用户的代码
🌰举个例子,让我们来看一段超简单的超市买水果程序(一次只购买一种水果):
def BuyFruit():
fruit = input("请输入要购买的水果:")
single_price, weight = map(float, input('请分别输入单价和重量:').split())
print('您购买了%.2f斤的%s,单价%.2f元/斤,合计消费【%.4f】元' %
(weight, fruit, single_price, single_price*weight))
BuyFruit()
""" 输出为:
您购买了4.70斤的苹果,单价3.00元/斤,合计消费【14.1000】元
"""
在这个程序中,我们首先定义了一个函数,通过接收用户在键盘上的输入、对数据进行简单的计算后,将结果打印到屏幕上。当有客人来购买水果时,售卖员会先称重水果,然后输入数字到机器中等待计算结果,类似于我们所编写的这段简单的程序,机器中也会有相应的程序去实现这一过程;而对于用户(售卖员)来说,不需要关心机器是如何实现,只需要学习如何操作机器为客户服务。
当然,超市里的机器程序肯定不只有这一简单的函数构成,比如开发员可以写一个功能函数专门去处理计算部分、一个UI设计函数去为用户选择界面增添更多功能······如果计算部分出现多算或少算的问题,那么可以停用UI设计函数界面的计算选项卡,正常使用其他功能,这样就为开发者在后期维护过程中,在不影响用户代码的前提下解决功能函数接口中的bug。
- 模块(Module),封装着许多功能函数,在使用某个模块中的功能函数时要先
import
关键字导入模块,否则会报错。导入模块语句如下:
from <module_name> import *
from <module_name> import <func_name>
import <module_name>.<func _name> as <func_name>
可以将模块类比工具包,函数就是工具, 不同工具包中的函数功能不相同、但名字可能会相同,所以Python官方规定在使用功能函数前必须要先导入其所在的模块。比如剪刀(类比函数)这个工具,有不同款式、大小之分,如果它是在厨师的工具包(类比模块)里,那它可能较大、会更锋利,如果它是在理发师的工具包里,那它可能较小轻便。
Python中其实有许多“同名”的函数,例如机器学习里的Dropout算法,在两个模块中都有不同的实现函数:
import torch.nn as nn
nn.Dropout(p:float,
inplace:bool)
import torch
torch.dropout(input:Tensor,
p:_float,
train:_bool)
因此,再一次强调⚠️在调用函数前,一定要先导入该函数所在模块!
3.包(package),将模块有序地组织在一起,也就是说在日常开发中,需要从不同模块中调用多个功能函数时,在每个文件开头一遍遍地敲模块名.函数名
既繁琐又增加了代码量,from <module_name> import *
会过多地占用命名空间,因此,用一个可调大小的容器来装这些互相联系的模块会更加高效。
- 库,是比包、模块、函数更大的概念,例如Python的数据科学分析库Pandas或NumPy等库中封装了很多个包,每个包中又包含许多模块。
二、基本数据类型
👉学习干货请参考文章 <Python基本数据类型及其函数>
👉练习题请看 <编程入门基本题目(Python、C语言),持续更新中…>
三、复杂度分析
算法(Algorithm)是计算机程序的一个基本的构建模块(另一个是数据结构),它描述了最终能够解决一个问题的计算过程,实质上是指令的有限序列。由于计算机的硬件资源有限,在运行算法时就不得不考虑它的开销——在有限内存空间中运行算法、需要耗费大量时间等待,而为了节省时间牺牲内存设计出来的算法往往不适用于日常生产生活,因此,我们要在此问题上找到一个比较好的平衡点,就需要分两方面进行评估:时间和空间。
【注:经本人对数据结构的学习,我认为在C语言系列文章的数据结构部分介绍更详细的、有数学证明的复杂度分析,更为合适;因此,在Python数据结构部分侧重于如何写代码实现复杂度分析】
现在来看一段循环嵌套程序:
step = 0 # 程序步
for i in range(15):
for j in range(15):
step += 1
print("%d*%d = %d" % (i, j, i*j), end=' ')
step += 1
print('\n')
print(step)
在这个程序中,定义了两个变量用以存储[0, 15]区间内的所有整数,实现了计算[0, 15]区间内所有整数相互乘积的结果。估算算法在内存上的表现效果的技术之一就是统计执行指令的数量,而计算机中的指令可分为:
- 不管问题规模多大,总是要执行相同数目的指令
- 根据问题规模,执行不同次数的指令
忽略第一类指令,可以通过计算程序步的值以分析这个程序的执行指令数。输出为“240”,很容易就可以知道:15*15+15=240。让我们再以斐波那契数列为例:根据迭代(iteration)次数的增加,那么需要的内存空间是以指数形式(下列是以2的幂次)增长:
def fib(n, table):
"""Fibonacci function with a table for memoization."""
if n < 3:
return 1
else:
# Attempt to get values for n - 1 and n - 2
# from the table
# If unsuccessful, recurse and add results to
# the table
result1 = table.get(n - 1, None)
if result1 is None:
result1 = fib(n - 1, table)
table[n - 1] = result1
result2 = table.get(n - 2, None)
if result2 is None:
result2 = fib(n - 2, table)
table[n - 2] = result2
return result1 + result2
def main():
"""Tests the function with some powers of 2."""
problemSize = 2
print("%4s%12s" % ("n", "fib"))
for count in range(5):
print("%4d%12d" % (problemSize, fib(problemSize, {})))
problemSize *= 2
if __name__ == "__main__":
main()
输出结果为:
n fib
2 1
4 3
8 21
16 987
32 2178309
那么如何计算时间上的开销呢?我们可以使用 time
或 datetime
时间模块:
- 关于时间模块请见文章 <Python入门之异常处理、文本进度条更新和日期时间操作>
import time
def main():
start = time.time()
"""程序执行块"""
end = time.time()
print('Timing: %.4f' % (end-start))
if __name__ == "__main__":
main()
将上述代码套用进斐波那契数列计算程序中,输出为:
n fib
2 1
4 3
8 21
16 987
32 2178309
Timing: 0.0103
本文结合代码简单地对斐波那契数列做了复杂度分析,在往后更复杂的项目中会利用到SciPy等数据科学库里的工具进行更科学准确的复杂度分析、用到Matplotlib等可视化结果进行数据分析。
- 如果您有意愿想要学习用Python进行数据分析的知识,请见系列文章 <Python数据分析>
四、面向对象编程基础概念
在后续开发包接口以及模块制作过程中会用到“类”(class)的概念,以下是类知识的部分说明,具体见文章 < 面向对象编程>
术语 | 说明 |
---|---|
类 | 用来描述具有相同的属性和方法的对象的集合,定义了该集合中每个对象所共有的属性和方法,对象是类的实例 |
类变量 | 在整个实例化的对象中是公用的,可在类的内部和外部通过成员运算符. 来访问属性,类变量定义在类中且在函数体之外,类变量通常不作为实例变量使用 |
数据变量 | 类变量或实例变量,用于处理类及其实例对象的相关数据 |
方法重写 | 若从父类继承的方法不能满足子类的需求,可以对其进行改写,有点像“覆盖” |
局部变量 | 定义在方法中的变量,只作用于当前的实例 |
实例变量 | 在类的声明中,属性使用变量来表示的,该变量就是实例变量,是在类声明的内部但是在类的其他成员之外方法之外声明的 |
继承 | 即一个派生类(derived class)继承基类(base class)的字段和方法。继承允许把一个派生类的对象当做一个基类来看待 |
实例化 | 创建一个类的实例,类的具体对象 |
方法 | 类中定义的函数 |
对象 | 通过类定义的数据结构实例;对象包括两个成员(类变量和实例变量)和方法 |
抽象 | 将具有相同特征的对象抽取共同特征的过程 |