数据结构与算法 引入概念 算法的五大特性 1. 输入: 算法具有0个或多个输入 2. 输出: 算法至少有1个或多个输出 3. 有穷性: 算法在有限的步骤之后会自动结束而不会无限循环,并且每一个步骤可以在可接受的时间内完成 4. 确定性:算法中的每一步都有确定的含义,不会出现二义性 5. 可行性:算法的每一步都是可行的,也就是说每一步都能够执行有限的次数完成 算法的概念 算法是计算机处理信息的本质 算法是独立存在的一种解决问题的方法和思想 算法效率衡量 算法运行时间可以衡量算法效率 * 算法中的基本操作的执行次数 * 基本操作:加减乘除等 * 函数:封装了一系列的基本操作 * 算法运行时间 = 算法的基本操作次数*计算机执行基本操作的时间单位 * 时间复杂度:算法中的基本操作的执行次数 * 最坏时间复杂度:算法最多的基本操作执行次数 * 最优时间复杂度:算法最少的基本操作执行次数 * 平均时间复杂度:算法平均基本操作执行次数 * 往往用最坏时间复杂度来衡量我们算法效率 * n算法规模 * T(n)= n^3 *(9+c) k=9+c * 为了计算方便把某些项忽略掉,保留与规模相关的最高次项,再忽略掉k c * 分支结构:取最大值 * 循环结构:乘操作 * 顺序结构:加操作 * 时间复杂度用来对同一问题,取最优算法 常见时间复杂度 所消耗的时间从小到大 O(1) < O(logn) < O(n) < O(nlogn) < O(n ^2) < O(n^3 ) < O(2^n ) < O(n!) < O(n^n ) * O(1)表示算法是一个固定值或常数 python内置类型性能分析 * class timeit.Timer(stmt='pass',setup='pass',timer=<timer function>) * # stmt 测试代码,注意:把测试代码封装成一个函数 * setup:保证测试代码可以正常运行,而设置的某些参数,import time * timer 定时器函数,和平台有关,默认不需要设置 * 启动定时器 timeit.Timer.timeit(number=1000000) number 测试代码测试次数,返回测试总时间 float 数据结构 * 数据结构:把数据以某种结构关系存储起来形成的集合 * 数据的存储结构影响数据的使用效率 * 抽象数据类型:把数据结构与其相关的操作封装在一起的类型 * 最常用的数据运算操作:插入、删除、修改、查找、排序 * int float bool 统称为基本数据类型 * 内存:内存条,看成一块连续的存储空间;最小的存储单位:Byte;每个最小的存储单元都有一个编号,用16进制表示 0x11... * int(占4B) * float(占8B) * bool(4B) * 数据在内存以二进制的形式存储;向操作系统申请一块空间返回空间的首地址 顺序表 * 顺序表:把相同类型的数据连续存储到一块连续的内存 顺序表的基本形式 * 三个数据存储使用特性: * 1、要内存;2、有首地址;3、通过首地址得到数据 * 存储真是数据元素:顺序表的内置结构 * 存储真实元素地址:顺序表的外置结构 顺序表的基本操作 * 向满的顺序表添加元素:1、返回已满 ,不能添加元素;2、扩展顺序表 * 顺序表扩展内存地址变更是否有影响 * 尾部添加:O(1) * 尾部删除:O(1) * 任意位置插入:O(n)伴随数据迁移,内存的拷贝 * 任意位置删除:O(n) 顺序表的扩展策略 * 顺序表的内存扩展策略:1、每次扩展固定值,大量的内存拷贝,效率低,节省内存;倍增的形式扩展,内存浪费,效率高 * python list扩展内存:开始的以2倍扩展,每扩展一次内存倍数减少,倍数趋近于1 * 用python是否可以实现顺序表:1、申请内存;2、通过首地址,用顺序表的方式管理这块空间 顺序表的两种基本实现方式 * 表头和数据区一体——一体式结构 * 表头和数据区分离——分离式结构 链表 单向链表 * 单向链表:存储数据的类型;节点;数据域和地址域(指向域) * 链表特性:以微小的内存减少大量内存的浪费 * python 中如何描述节点?list【data,next】可以描述节点,不可以描述链表;class可以描述 * 尾部添加:1、构造新节点;2、查找尾部节点、3、执行插入操作; 找一个游标:如何移动游标 cur = cur.next; 遍历终止条件:当前指向域是None; 执行插入操作:cur.next = new_node * 执行插入过程:(需要知道插入pos位置的前后两个节点) new_node.next = cur.next;cur.next = new_node; 1、先让新的节点有所指向; 2、让与新节点有关的其它节点有所指向 * 插入过程:1、构造新节点;2、找到pos位置前一个节点;3、执行插入操作 * 查找pos位置:游标、计数器; 游标和计数器的初始状态,游标正好是pos位置的前一个节点 * 删除操作:1、查找删除节点的前一个节点;2、执行删除操作 双向链表 有prev指向前一个元素 单向循环链表 最后一个元素的next指向头部元素 双向循环链表 最后一个元素的next指向头部元素,头部元素的prev指向最后一个元素 时间复杂度 * 复杂度 顺序表 链表 * 遍历: O(n) O(n) * 获取元素O(1) O(n) * 头部删除 O(n) O(1) * 尾部删除 O(1) O(n)双向链表O(1) * 任意位置:O(n) O(n) * 任意位置插入和删除时间复杂度n:顺序表,内存拷贝;链表,基本操作 * 内存拷贝相比较基本操作效率低 * 插入和删除,链表效率高;链表适合数据频繁插入和删除 * 顺序表访问效率高:适用于频繁访问数据,删除操作少 * while else:循环过程中没有被break出来,执行else语句块;如果break出来,则跳过else语句块 栈 * 怎么判断所有括弧都匹配成功? * 匹配:左右括弧配对 * 遇到左括弧,尾部添加,遇到右括弧提取尾部左括弧,查看左括弧和右括弧是否配对 * 数据流向:尾部进,尾部出 * 添加数据:压栈;取数据:出栈;栈顶:最后一个元素位置 栈的操作 Stack() 创建一个新的空栈 push(item) 添加一个新的元素item到栈顶 pop() 弹出栈顶元素 peek() 返回栈顶元素 is_empty() 判断栈是否为空 size() 返回栈的元素个数 队列 * 数据流向:先进先出 队列常用操作 * 进队操作 * 出队操作 * 判空操作 * size操作 * 银行大厅的机子(生产者) * 1、产生号码、存入队列 * 2、打印号码 * 窗口(消费者):从队列区号码,办理业务、消耗号码 * 网络通讯 * lock:生产者保证所有的消费者获取唯一的数据 * 双端队列:可以当栈使用,也可以当队列使用 * 撤销和反撤销? * 保存操作(队列操作) 保存固定的操作 撤销操作(出栈操作):撤销最近的一次操作; 保存撤销操作(压入新栈); 执行反撤销(新栈出栈操作,存入双端队列),返回撤销 排序 冒泡排序 * 核心:1、遍历整个序列,遍历过程比较相邻的两个元素大小,大的向后走2、遍历完成后,最大的值在序列的最后 * 控制序列进行冒泡排序,直至序列大小为2(最后一次循环比较) * 最坏时间复杂度:O(n^2) * 最优时间复杂度:O(n) * 稳定性:稳定 * 面试:设定flag = True 选择排序 * 核心:1、从当前序列中选择一个最小值,放置于当前序列的第一个位置 * 方法:用第一个位置元素依次和后面所有的元素进行比较 * 依次减小序列直至大小为1 * 最坏时间复杂度:O(n^2) * 最优时间复杂度:O(n^2) * 稳定性:不稳定 插入排序 * 核心:1、把整个序列看成两部分,一部分看成有序、另一部分看成无序;初始:把第一个元素看成一个有序序列,后面所有元素看成无序序列;关键点:从无序序列中,遍历获取数据插入到前面的有序序列 * 如何插入到前面的有序序列? * 无需序列中的取出来的数据和依次和前面的有序序列中的所有数据比较,比较的过程中直至遇到比取出的数据小 * 最坏时间复杂度:1+2+...n-1 次数 n-1 O(n^2) * 最优复杂度:n-1 O(n) * 稳定性:稳定 希尔排序 * 核心:gap=4或者gap = length//2;把整个序列每隔gap取一个元素,最终把整个序列分成gap个子序列,对我们的子序列进行插入排序,最终得到一个新的序列,减小gap取值,gap=2,再次对新的序列进行核心部分处理,直至gap取值到1 * 最坏时间复杂度:O(n^2) * 最优时间复杂度:和gap取值有关,O(n^1.3) * 稳定性:不稳定 二分查找 * 猜数字游戏:每次取中间值进行比较,猜的数字大了,去左半边处理;猜的数字小了,去右半边处理 * 二分查找:限制:序列有序,顺序表;优点:查找次数少 O(logn) * 找中间值:计算中间值下表;mid_index = (start+end)//2 * 什么是递归:自己调用自己;有终止条件可以退出调用;递归中的状态向终止条件靠近 快速排序 # 中心思想:把当前序列的第一元素作为基准元素,找到基准元素在当前序列的正确位置(排序后的位置) # 位置查找:最后左边的基准元素小,右边的比基准元素大 # 1、保存基准元素 # 2、定义两个游标 left = start right = end # 移动右边游标,如果大继续移动,如果小停止移动,把当前数据移动到左边 # 移动左边游标,如果小继续移动,如果大停止移动,把当前数据移动到右边 # 重复以上两个过程,直至左右两个游标相遇,此时的位置就是基准元素位置 # 3、基准元素把当前序列分成两部分,再次用步骤2对子序列进行处理 # 最坏时间复杂度:n-1+n-2+...+1 次数n O(n) # 最优时间复杂度:次数logn 每次执行n O(nlogn) # 稳定性:稳定 归并排序 # 中心思想:把整个序列对半拆分,再次对子序列进行对半拆分,直至所有序列长度都是1 # 接下来合并左右两个有序子序列,直至合并到起始 # 最坏时间复杂度:1+2+4+。。。+logn = 2logn-1 合并过程:次数logn 每次n nloghn # T(n) = nlogn+2logn-1=O(nlogh) # 最优时间复杂度:O(nlogn) 二叉树 有序树:任意节点左子树所有数据比此节点小,右子树比此节点大 数据存储:用节点存储 顺序表存储树有什么好处:优点:访问快速;缺点:对于不规则树浪费空间 先序遍历:根 左 右 中序遍历:左 根 右 后序遍历:左 右 根