- 数据结构是存储,组织数据的方式
- 算法是完成一个目标的方法和˼·
- 公司的核心价值点起始与数据,数据可以预判趋势,指导方向,解决实际问题,掌握了公司的数据,就掌握了公司运营和发展的命脉
- 是做技术的基础中的基础,是高技术人才的必备能力
- 装逼利器
什么是数据结构,什么是算法,他们之间的关系,抽象数据类型
数据结构:
- 数据组织的方式
- 数据的种类有很多种:整型,浮点型,字符串。。。
- 数据的组织方式:字典,列表,元组。。。
- 举例子:数据:‘老王’ 10 ‘男’ 组织方式:列表:[‘老王’,10, ‘男’], 字典{name:’老王’,age:18,gender:’男’}
- 物理形式
- 顺序表
- 链表
逻辑形式
集合,线性,树形,图型
算法
- 解决问题的方法和思路
- 公司的核心是数据,数据的组织方式是数据结构,怎么利用这些数据结构完成公司的业务需求,就用到了算法
- 评判算法的好坏的标准:算法复杂度
- 时间复杂度:完成一个目标所花费的时间
- 空间复杂度:完成一个目标所花费的内存空间
数据结构+算法 == 程序,也就是业务需求
- 程序 + 语言 == 编程
- 抽象数据类型就是将数据和在这个数据上的运算捆绑在一起。
- 如魂斗罗中的人物是一种数据类型,打子弹是动作,点击空格键使人物发子弹,那么按一下空格键就生成了一个 抽象数据类型:人物打子弹
- 为什么引入抽象数据类型的概念
- 为了将数据类型的表示和运算从程序中单独隔离开来,使得他们有自己的独立作用。简单的说,就是将数据代入场景,使数据场景化。
- 数据结构:数据的组织方式
- 算法:解决问题的方法和思路
- 评判算法的好坏:算法复杂度:事件复杂度,空间复杂度
- 关系:数据结构+算法==程序,程序+语言==编程
- 抽象数据结构: 数据场景化
- 算法的特性,评判标准
- 算法复杂度的定义,分类及计算规则
- 性能分析模块timeit的使用
如果a+b+c=1000,且a^2+b^2=c^2(a,b,c为自然数),求出所有a,b,c的组合?
思路:
- 1.全试,a遍历0-1000,b遍历0-1000,c遍历0-1000,满足两个条件的结果输出
- 2.试两个,a遍历0-1000,b遍历0-1000,然后输出满足,c=1000-(a+b), a^2+b^2=c^2的所有结果
代码实现:
算法1
import time start_time = time.time() for a in range(1001): for b in range(1001): for c in range(1001): if a+b+c==1000 and a**2+b**2==c**2: print("满足条件的a,b,c=%d,%d,%d" % (a,b,c)) end_time = time.time() time_cost = end_time-start_time print('算法1消耗的时间为:%f'%time_cost) print('complete') 结果:
算法2
import time # 猜两个数字 start_time = time.time() for a in range(1001): for b in range(1001): c = 1000-a-b if a**2+b**2==c**2: print("满足条件的a,b,c=%d,%d,%d" % (a, b, c)) end_time = time.time() time_cost = end_time-start_time print('算法2消耗的时间为:%f'%time_cost) print('complete') 结果: 满足条件的a,b,c=0,500,500 满足条件的a,b,c=200,375,425 满足条件的a,b,c=375,200,425 满足条件的a,b,c=500,0,500 算法2消耗的时间为:1.842306 complete
- 算法的特性:
- 量入出,题题能解 ,行有意步步可为
- 五个特性:
- 入,输入:要有输入
- 出,输出:要有输出
- 题题能解,有穷性:代码要在有限的步骤内完成,不能无限循环,且时间可接受
- 行有意,确定型:算法的每一步都有确定的含义,不会出现二义性
- 步步可为,可行性:算法的每一步都是可执行的,都是在有限的执行次数内完成的
- 算法复杂度
- 时间复杂度:算法完成所耗费的时间
- 空间复杂度:算法完成所耗费的计算机内存资源
- 做一件事需要耗费的时间: 抬手,看一集电视剧,跑完整个马拉松
- 大O记法: O(规律)
- 以之前的例子的算法2为例,执行的次数规律为:n*n*10
- 若g(n)表示计算次数的规律,则g(n)=n*n*10
- 使用T(n)表示算法的时间算法复杂度,即T(n)=O(n^2)
- T(n)=O(n^2)为算法2的渐近时间复杂度,简称为时间复杂度
-
工作中跟老板谈的时候,要谈最坏时间复杂度
- 常数结构
- T(n)=O(1):可见的有限的步骤,例子:22岁结婚
- 顺序结构
- T(n)=O(n):顺序结构,步骤不可见,但终归是顺序的,可执行完的,例子:22岁前所做的事情
- 循环结构
- 简单循环
- T(n)=O(n^数字):批量执行多次,如上面的例子
- 递归循环
- T(n)=O(logn):同样的步骤重复多次:切棍子,一次切一半
分支结构
多分支if语句,找一个时间最长的作为标准时间
- 判断一个算法的效率时,往往只需要关注操作数量的最高次项,其他的次要项和常数项可以忽略。
- 没有特殊说明,我们所说的算法时间复杂度就是最坏时间复杂度
- 是一个算法在运行过程中临时占用存储空间大小的量度
- 在代码上的表现形式就是,同一个功能的实现,我是用两行代码,还是三行代码,两行代码的空间复杂度就低
- 只有在代码量非常大的时候,才会考虑空间复杂度
- 用空间换时间:一个无差别劳动的计件工作,用一个人干,和用8个人干,8个人肯定要快一点,但是用的钱就多一点。有时候,空间复杂度大一点,会节约时间,用空间换时间,也是值得的
- 上面的例子中
算法1的时间复杂度:T(n)1 = n^3 算法2的时间复杂度:T(n)2 = n^2 n^2<n^3 算法2的时间复杂度更低一点,则算法2的效率高一点
- 例子(执行步骤) 阶 非正式术语
- 12 O(1) 常数阶
- 2n+1 O(n) 线性阶
- 3n^3+2n^2+3 O(n^3) 指数阶
5log2^n+4 O(logn) 对数阶
时间复杂度函数的图形的趋势越陡,时间复杂度越高
- 则 :O(1)< O(n) < O(logn) < O(n^3)
- 时间复杂度越高,执行步越多,耗费时间时间越多效率越低
- 掌握timeit模块的使用规则
- 使用timeit模块测试代码
timeit模块是一个用来测试一小段代码的执行速度的模块
核心代码:
代码1:
class timeit.Timer(stmt='pass', setup='pass', timer=<timer function>)
代码2:
timeit.Time.timeit(number=10000)
代码1详解:
- Timer是测量小段代码执行速度的类
- stmt(statement):要测试代码语句块,如果是函数,调用函数的形式
- 如: 测试函数”fun1“ 就写”fun1()“
- setup:运行代码时需要的设置
- 如:测试当前文档的func1语句块 ,就写”from _main_ import func1“
- timer:一般省略
示例:
time1 = timeit.Time("fun1", "from __main__ import func1")
- 代码2详解:
- timeit.Timer.timeit()是Timer类中用来测试语句执行速度的实例方法
- number表示执行代码的次数
返回执行次数的平均时间,是一个float的秒数
示例:
timeit.Timer.timeit(number=10000)
需求:给一个列表里添加1000个数字
方法(算法)
- 列表的内置属性:list.append()
- 列表拼接:list1 + list2
- 列表推导式:[i for i in range()]
- 列表属性转换:list(range())
代码:
列表的内置属性:list.append()
import timeit def T1(): li = [] for i in range(1000): li.append(i) timer1 = timeit.Timer("T1()","from __main__ import T1") print("内置属性: %f seconds" % timer1.timeit(number=1000)) 内置属性: 0.234965 seconds
列表拼接:list1 + list2
import timeit def T2(): li = [] for i in range(1000): li = li + [i] timer2 = timeit.Timer("T2()","from __main__ import T2") print("列表拼接: %f seconds" % timer2.timeit(number=1000)) 列表拼接: 4.817975 seconds
列表推导式:[i for i in range()]
import timeit def T3(): li = [i for i in range(1000)] timer3 = timeit.Timer("T3()","from __main__ import T3") print("列表推导: %f seconds" % timer3.timeit(number=1000)) 列表推导: 0.085915 seconds
列表属性转换:list(range())
import timeit def T4(): list=list(range(1000)) timer3 = timeit.Timer("T4()", "from __main__ import T4") print("列表属性:%f second"%timer3.timeit(1000)) 属性转换: 0.042624 seconds
- 可见列表属性的效率最高,因为list()是用C写的
- 测试代码块的执行速度模块:timeit
- timeit.Timer(“fun1()”,”from _main_ import fun1”)
- time = timeit.Timer.timeit(1000)
- 线性表的特性和分类
- 顺序表形式,结构,示例
- 三种链表的特性,示例
- 将一大堆同类型的数据,按照某种关系,一次排列出来,形成的一条线
- 示例: 100 200 300 400 。。。 n
- 顺序表
- 将元素顺序的存放在一块连续的存储区里,元素之间的存储关系由他们的存储顺序自然展示
- ”排排坐吃果果“
- 链表
- 将元素存放在通过链接构造起来的一系列存储块中。也就是说,这种表不但有自己的数据信息,也有下一个数据节点的地址信息。
- ”吃套餐“
- 基本布局
- 存储同类数据
- 元素外置
- 存储不同类数据
- 简单来说,就是存储多个同类型的数据,每个数据占用一个固定存储空间,多个数据组合在一起的时候,物理地址就会连续起来
- 逻辑地址:m = 起始值 + 偏移量*(元素总数-1)
- 适用于存储不同类型的数据。因为不同类型数据占用存储的空间不一样,不能按照基本布局的方式来存储。
- 虽然他们的数据占用的空间不一样,但是他们的逻辑地址的数据是相同的数据,可以把他们的逻辑地址存储在一个固定的空间里,然后通过调用新生成的相同类型数据的逻辑地址来找到数据数据的逻辑地址,再通过逻辑地址找到数据本身真正的逻辑地址,通过真正的逻辑地址找到数据数据内容
- 基本信息+存储元素
- 基本信息
- 地址容量
- 元素个数
- 因为顺序表有两种表现形式,所以有两种结构
- 基本布局:一体式结构
- 元素外置:分离式结构
存储表信息与元素存储区以连续的方式安排在一块存储块里,形成了一个完整的顺序表对象。
特点:
- 因为整体,所以易于管理,顺序表创建后,元素存储区就固定了
顺序表对象里保存的基本信息(地址容量+元素个数)和元素存储空间的一个逻辑地址,通过逻辑地址找到响应数据的真正地址。
特点:灵活,可以随意调整
我们在设定数据表的时候,会首先申请我要使用的存储空间。而存储空间不是一成不变的。接下来,我们来介绍一下元素存储内容变化时顺序表的变化。
存储内容的变化,无非是少或者多,少会造成存储空间的浪费,多的话,存储空间发生变化。 我们接下来主要是对存储内容更改或者存储内容大于计划空间的这两种情况进行分析。
一体式:
- 因为一体式,基本信息和存储元素是一个整体的对象,而且一旦定义了,就不会再更改了
存储空间一旦大于计划容量,若要存储更多内容,只能整体搬迁,即整个顺序表对象(指存储书序表的结构信息的区域)改变了
示例:
分离式:
- 因为分离式,基本信息和存储元素是两个独立的对象,它们彼此间使用基本信息后面的逻辑地址来链接,所以存储数据变动,只不过存储的逻辑地址变动,而存储对象中的基本信息没有发生变动,所以,对于顺序表对象来说,没有发生变动
- 通过上面的两种顺序表结构的内容更新实践案分析,数据发生变动,我们推荐使用分离式顺序表结构,为什么?因为对象没有变动,一体式相当于重新开辟了一片空间,产生了一个新的顺序表对象,没有分离式简单省事。
- 接下来分析数据内容增加的情况,只分析分离式顺序表结构
- 采用分离式结构的顺序表,可以在不改变对象的前提下,将数据存储区更换为存储空间更大的区域。所有使用这个顺序表的地方都不必修改。只要程序的运行环境(计算机系统)还有空闲存储, 这种表结构就不会因为数据存储区空间不足满了而导致操作无法进行。人们把采用这种技术实现的顺序表称为动态顺序表,因为其容量可以在使用中动态变化。
- 目前动态顺序表的库容策略分为两大类:线性增长,倍数增长
- 数据存储区容量一旦发生不足,每次扩充增加固定数目的存储区容量,如每次扩充增加4个元素位置。
- 特点:
- 确定性:因为增加数量有限制,所以节省空间
- 不确定性:扩充操作可能频繁操作,随时发生
- 数据存储区容量一旦发现不足,每次扩容加倍,如:
- 当前容量8,容量不足的时候,就扩容8,最终容量为16
- 当前容量为16,容量不足的时候,就扩容16,最终容量为32
- 。。。
- 如此循环往复,每次扩充的容量都是相当于当前容量的大小
- 确定性:每次增加容量会很大,所以扩充操作的执行此说不会频繁发生
- 不确定性:容量扩容后,可能会造成空间的资源浪费
可以看到倍数增长的方式是空间换时间,因为目前的空间代价越来越小,所以我们工作中推荐使用倍数增长的方式来调整容量空间
单一元素的值就是顺序表
- 增删改查
- 插头,插尾,乱插
- 插头插尾:保序增加
乱插:非保序增加,不推荐使用,不容易控制
时间复杂度:
- 乱插,插尾:O(1)
- 插头:O(n)
ɾ
- 删头,删尾,乱删
- 删头和删尾为保序删除
- 乱删为非保序删除,一般不用
- 事件复杂度
- 删尾,乱删:O(1)
- 删头:O(n)
- 顺序表形式:基本布局(同类元素) + 元素外置(不同类元素)
- 顺序表结构:一体式 + 分离式(灵活)
- 顺序表的容量扩容策略:线性(频繁)+倍增(工作推荐)
- 顺序表元素的增加:头[保序,O(n)] + 尾[保序,O(1)] + 位置 [非保序、O(1)]
- 删除:头[保序,O(n)] + 尾[保序,O(1)] + 位置[非保序,O(1)]