数据结构与算法
- 基础
- 大O表示法
- 时间复杂度
- 大O表示法
- 时间复杂度排序:
- 最坏时间复杂度
- 时间复杂度的几条基本计算规则
- 内存工作原理
- 什么是内存
- 内存主要分为三种存储器
- 随机存储器(RAM)
- 只读存储器(ROM)
- 高速缓存(Cache)
- 工作原理
- 内存映射
- 虚拟内存空间分布
- 内存分配与回收
- 递归
- 什么是递归
- 求和(python)
- NP完全问题
- 数据结构
- 数组
- 链表
- 混合结构
- 栈
- 队列
- 散列表
- 图
- 集合
- 二叉树
- 算法
- 二分查找
- 选择排序
- 快速排序
- 广度优先&深度优先
- 狄克斯特拉算法
- 近似算法
- K最近邻算法
- 策略
- D&C分而治之
- 贪婪算法
- 动态规划
算法是为了解决实际问题而设计的,数据结构是算法需要处理的问题载体
基础
大O表示法
实现算法程序的执行时间可以反应出算法的效率,即算法的优劣。但是单纯依靠运行的时间来比较算法的优劣并不一定是客观准确的!
时间复杂度
时间复杂度,又称"渐进式时间复杂度",表示代码执行时间与数据规模之间的增长关系。
我们假定计算机执行算法每一个基本操作的时间是固定的一个时间单位,那么有多少个基本操作就代表会花费多少时间单位。算然对于不同的机器环境而言,确切的单位时间是不同的,但是对于算法进行多少个基本操作(即花费多少时间单位)在规模数量级上却是相同的,由此可以忽略机器环境的影响而客观的反应算法的时间效率。
大O表示法
对于算法的时间效率,我们可以用“大O记法”来表示。
“大O记法”:对于单调的整数函数f,如果存在一个整数函数g和实常数c>0,使得对于充分大的n总有f(n)<=c*g(n),就说函数g是f的一个渐近函数(忽略常数),记为f(n)=O(g(n))。也就是说,在趋向无穷的极限意义下,函数f的增长速度受到函数g的约束,亦即函数f与函数g的特征相似。
时间复杂度:假设存在函数g,使得算法A处理规模为n的问题示例所用时间为T(n)=O(g(n)),则称O(g(n))为算法A的渐近时间复杂度,简称时间复杂度,记为T(n)
对于算法进行特别具体的细致分析虽然很好,但在实践中的实际价值有限。对于算法的时间性质和空间性质,最重要的是其数量级和趋势,这些是分析算法效率的主要部分。而计量算法基本操作数量的规模函数中那些常量因子可以忽略不计。例如,可以认为
3
n
2
3n^2
3n2和
100
n
2
100n^2
100n2属于同一个量级,如果两个算法处理同样规模实例的代价分别为这两个函数,就认为它们的效率“差不多”,都为
n
2
n^2
n2级。
时间复杂度排序:
1、O(1) 为常数级的时间复杂度,算法是十分好。
2、O(log n) 为对数级的时间复杂度,算法也不错。
3、O(n) 为线性级的时间复杂度,算法也还行。
4、O(nlog n)线性对数级的时间复杂度,算法也还可以。
5、O(n^2) 二次方级的时间复杂度,算法有点差。
6、O(n^3)三次方级的时间复杂度,算法差。
7、O(2^n) 指数级的时间复杂度,算法很差。
8、O(n!)阶乘级的时间复杂度,算法极差。
O ( 1 ) < O ( l o g n ) < O ( n ) < O ( n l o g n ) < O ( n 2 ) < O ( n 3 ) < O ( 2 n ) < O ( n ! ) < O ( n n ) O(1) < O(logn) < O(n) < O(nlogn) < O(n^2) < O(n^3) < O(2^n) < O(n!) < O(n^n) O(1)<O(logn)<O(n)<O(nlogn)<O(n2)<O(n3)<O(2n)<O(n!)<O(nn)
最坏时间复杂度
常见的时间复杂度关系:
时间复杂度一般有以下几种种形式:
算法完成工作最少需要多少基本操作,即最优时间复杂度
算法完成工作最多需要多少基本操作,即最坏时间复杂度
算法完成工作平均需要多少基本操作,即平均时间复杂度
1、对于最优时间复杂度,其价值不大,因为它没有提供什么有用信息,其反映的只是最乐观最理想的情况,没有参考价值。
2、对于最坏时间复杂度,提供了一种保证,表明算法在此种程度的基本操作中一定能完成工作。
3、对于平均时间复杂度,是对算法的一个全面评价,因此它完整全面的反映了这个算法的性质。但另一方面,这种衡量并没有保证,不是每个计算都能在这个基本操作内完成。而且,对于平均情况的计算,也会因为应用算法的实例分布可能并不均匀而难以计算。
4、因此,我们主要关注算法的最坏情况,亦即最坏时间复杂度。
时间复杂度的几条基本计算规则
1、基本操作,即只有常数项,认为其时间复杂度为O(1)
2、顺序结构,时间复杂度按加法进行计算
3、循环结构,时间复杂度按乘法进行计算
4、分支结构,时间复杂度取最大值
5、判断一个算法的效率时,往往只需要关注操作数量的最高次项,其它次要项和常数项可以忽略
6、在没有特殊说明时,我们所分析的算法的时间复杂度都是指最坏时间复杂度
内存工作原理
什么是内存
内存(Memory)是计算机中最重要的部件之一,它是程序与CPU进行沟通的桥梁。计算机中所有程序的运行都是在内存中进行的,因此内存对计算机的影响非常大,内存又被称为主存,其作用是存放 CPU 中的运算数据,以及与硬盘等外部存储设备交换的数据。只要计算机在运行中,CPU 就会把需要运算的数据调到主存中进行运算,当运算完成后CPU再将结果传送出来,主存的运行也决定了计算机的稳定运行。
内存主要分为三种存储器
随机存储器(RAM)
随机存储器(RAM)内存中最重要的一种,表示既可以从中读取数据,也可以写入数据。当机器关闭时,内存中的信息会 丢失。
只读存储器(ROM)
只读存储器(ROM):ROM 一般只能用于数据的读取,不能写入数据,但是当机器停电时,这些数据不会丢失。
高速缓存(Cache)
高速缓存(Cache):Cache 也是我们经常见到的,它分为一级缓存(L1 Cache)、二级缓存(L2 Cache)、三级缓存(L3 Cache)这些数据,它位于内存和 CPU 之间,是一个读写速度比内存更快的存储器。当 CPU 向内存写入数据时,这些数据也会被写入高速缓存中。当 CPU 需要读取数据时,会直接从高速缓存中直接读取,当然,如需要的数据在Cache中没有,CPU会再去读取内存中的数据。
工作原理
内存映射
通常所说的内存容量,比如笔记本电脑的8GB内存,其实指的是物理内存。物理内存也称为主存,大多数计算机用的主存都是动态随机访问内存(DRAM)。只有内核才可以直接访问物理内存。
Linux 内核给每个进程都提供了一个独立的虚拟地址空间,并且这个地址空间是连续的。这样,进程就可以很方便地访问内存,更确切地说是访问虚拟内存。
虚拟地址空间的内部又被分为内核空间和用户空间两部分,不同字长(也就是单个CPU指令可以处理数据的最大长度)的处理器,地址空间的范围也不同。比如最常见的 32 位和 64 位系统,它们的虚拟地址空间。
如下所示:
通过这里可以看出,32位系统的内核空间占用 1G,位于最高处,剩下的3G是用户空间。而 64 位系统的内核空间和用户空间都是 128T,分别占据整个内存空间的最高和最低处,剩下的中间部分是未定义的。
进程在用户态时,只能访问用户空间内存;只有进入内核态后,才可以访问内核空间内存。虽然每个进程的地址空间都包含了内核空间,但这些内核空间,其实关联的都是相同的物理内存。这样,进程切换到内核态后,就可以很方便地访问内核空间内存。
既然每个进程都有一个这么大的地址空间,那么所有进程的虚拟内存加起来,自然要比实际的物理内存大得多。所以,并不是所有的虚拟内存都会分配物理内存,只有那些实际使用的虚拟内存才分配物理内存,并且分配后的物理内存,是通过内存映射来管理的。
内存映射,其实就是将虚拟内存地址映射到物理内存地址。为了完成内存映射,内核为每个进程都维护了一张页表,记录虚拟地址与物理地址的映射关系。
如下图所示:
页表实际上存储在 CPU 的内存管理单元 MMU中,这样,正常情况下,处理器就可以直接通过硬件,找出要访问的内存。
而当进程访问的虚拟地址在页表中查不到时,系统会产生一个缺页异常,进入内核空间分配物理内存、更新进程页表,最后再返回用户空间,恢复进程的运行。
另外,TLB(Translation Lookaside Buffer,转译后备缓冲器)会影响 CPU 的内存访问性能,TLB 其实就是 MMU 中页表的高速缓存。由于进程的虚拟地址空间是独立的,而 TLB 的访问速度又比 MMU 快得多,所以,通过减少进程的上下文切换,减少TLB的刷新次数,就可以提高TLB 缓存的使用率,进而提高CPU的内存访问性能。
不过要注意,MMU 并不以字节为单位来管理内存,而是规定了一个内存映射的最小单位,也就是页,通常是 4 KB大小。这样,每一次内存映射,都需要关联 4 KB 或者 4KB 整数倍的内存空间。
页的大小只有4 KB ,导致的另一个问题就是,整个页表会变得非常大。比方说,仅 32 位系统就需要 100 多万个页表项(4GB/4KB),才可以实现整个地址空间的映射。为了解决页表项过多的问题,Linux 提供了两种机制,也就是多级页表和大页(HugePage)。
多级页表就是把内存分成区块来管理,将原来的映射关系改成区块索引和区块内的偏移。由于虚拟内存空间通常只用了很少一部分,那么,多级页表就只保存这些使用中的区块,这样就可以大大地减少页表的项数。
Linux用的正是四级页表来管理内存页,如下图所示,虚拟地址被分为5个部分,前4个表项用于选择页,而最后一个索引表示页内偏移。
大页,就是比普通页更大的内存块,常见的大小有 2MB 和 1GB。大页通常用在使用大量内存的进程上,比如 Oracle、DPDK等。
通过这些机制,在页表的映射下,进程就可以通过虚拟地址来访问物理内存了。
虚拟内存空间分布
最上方的内核空间不用多讲,下方的用户空间内存,其实又被分成了多个不同的段。以32 位系统为例。
如下图:
通过这张图可以看到,用户空间内存,从低到高分别是五种不同的内存段。
1.只读段,包括代码和常量等。
2.数据段,包括全局变量等。
3.堆,包括动态分配的内存,从低地址开始向上增长。
4.文件映射段,包括动态库、共享内存等,从高地址开始向下增长。
5.栈,包括局部变量和函数调用的上下文等。栈的大小是固定的,一般是 8 MB。
在这五个内存段中,堆和文件映射段的内存是动态分配的。比如说,使用 C 标准库的 malloc() 或者 mmap() ,就可以分别在堆和文件映射段动态分配内存。
其实64位系统的内存分布也类似,只不过内存空间要大得多。
内存分配与回收
malloc() 是 C 标准库提供的内存分配函数,对应到系统调用上,有两种实现方式,即 brk() 和 mmap()。
- 对小块内存(小于128K),C 标准库使用 brk() 来分配,也就是通过移动堆顶的位置来分配内存。这些内存释放后并不会立刻归还系统,而是被缓存起来,这样就可以重复使用。
- 而大块内存(大于 128K),则直接使用内存映射 mmap() 来分配,也就是在文件映射段找一块空闲内存分配出去。
这两种方式,自然各有优缺点。
- brk() 方式的缓存,可以减少缺页异常的发生,提高内存访问效率。不过,由于这些内存没有归还系统,在内存工作繁忙时,频繁的内存分配和释放会造成内存碎片。
- mmap() 方式分配的内存,会在释放时直接归还系统,所以每次 mmap 都会发生缺页异常。在内存工作繁忙时,频繁的内存分配会导致大量的缺页异常,使内核的管理负担增大。这也是malloc 只对大块内存使用 mmap 的原因。
了解这两种调用方式后,还需要清楚一点,那就是,当这两种调用发生后,其实并没有真正分配内存。这些内存,都只在首次访问时才分配,也就是通过缺页异常进入内核中,再由内核来分配内存。
整体来说,Linux 使用伙伴系统来管理内存分配。这些内存在MMU中以页为单位进行管理,伙伴系统也一样,以页为单位来管理内存,并且会通过相邻页的合并,减少内存碎片化(比如brk方式造成的内存碎片)。
在用户空间,malloc 通过 brk() 分配的内存,在释放时并不立即归还系统,而是缓存起来重复利用。
在内核空间,Linux 则通过 slab 分配器来管理小内存。可以把slab 看成构建在伙伴系统上的一个缓存,主要作用就是分配并释放内核中的小对象。
对内存来说,如果只分配而不释放,就会造成内存泄漏,甚至会耗尽系统内存。所以,在应用程序用完内存后,还需要调用 free() 或 unmap(),来释放这些不用的内存。
当然,系统也不会任由某个进程用完所有内存。在发现内存紧张时,系统就会通过一系列机制来回收内存,比如下面这三种方式:
- 回收缓存,比如使用 LRU(Least Recently Used)算法,回收最近使用最少的内存页面;
- 回收不常访问的内存,把不常用的内存通过交换分区直接写到磁盘中;
- 杀死进程,内存紧张时系统还会通过 OOM(Out of Memory),直接杀掉占用大量内存的进程。
其中,第二种方式回收不常访问的内存时,会用到交换分区(以下简称 Swap)。Swap 其实就是把一块磁盘空间当成内存来用。它可以把进程暂时不用的数据存储到磁盘中(这个过程称为换出),当进程访问这些内存时,再从磁盘读取这些数据到内存中(这个过程称为换入)。
所以,可以发现,Swap 把系统的可用内存变大了。不过要注意,通常只在内存不足时,才会发生 Swap 交换。并且由于磁盘读写的速度远比内存慢,Swap 会导致严重的内存性能问题。
第三种方式提到的 OOM(Out of Memory),其实是内核的一种保护机制。它监控进程的内存使用情况,并且使用 oom_score 为每个进程的内存使用情况进行评分:
- 一个进程消耗的内存越大,oom_score 就越大;
- 一个进程运行占用的 CPU 越多,oom_score 就越小。
这样,进程的 oom_score 越大,代表消耗的内存越多,也就越容易被 OOM 杀死,从而可以更好保护系统。
当然,为了实际工作的需要,管理员可以通过 /proc 文件系统,手动设置进程的 oom_adj ,从而调整进程的 oom_score。
oom_adj 的范围是 [-17, 15],数值越大,表示进程越容易被 OOM 杀死;数值越小,表示进程越不容易被 OOM 杀死,其中 -17 表示禁止OOM。
比如用下面的命令,就可以把 sshd 进程的 oom_adj 调小为 -16,这样, sshd 进程就不容易被 OOM 杀死。
echo -16 > /proc/$(pidof sshd)/oom_adj
- 对普通进程来说,它能看到的其实是内核提供的虚拟内存,这些虚拟内存还需要通过页表,由系统映射为物理内存。
- 当进程通过 malloc() 申请内存后,内存并不会立即分配,而是在首次访问时,才通过缺页异常陷入内核中分配内存。
- 由于进程的虚拟地址空间比物理内存大很多,Linux 还提供了一系列的机制,应对内存不足的问题,比如缓存的回收、交换分区 Swap
以及OOM 等。 - 当需要了解系统或者进程的内存使用情况时,可以用 free 和 top 、ps 等性能工具。它们都是分析性能问题时最常用的性能工具。
递归
什么是递归
递归在程序语言中简单的理解是:方法自己调用自己
递归其实和循环是非常像的,循环都可以改写成递归,递归未必能改写成循环。
那么,有了循环,为什么还要用递归呢??在某些情况下(费波纳切数列,汉诺塔),使用递归会比循环简单很多很多
我们初学编程的时候肯定会做过类似的练习:
- 1+2+3+4+…+100(n)求和
- 给出一个数组,求该数组内部的最大值
我们要记住的是,想要用递归必须知道两个条件:
- 递归出口(终止递归的条件)
- 递归表达式(规律)
技巧:在递归中常常是将问题切割成两个部分(1和整体的思想),这能够让我们快速找到递归表达式(规律)
求和(python)
# 递归求和
def recursion(max_n,num):
'''
:param max_n = 100: 求0 - max_n之间的数的和
:param num = 0: 存放结果
:return: 最终返回的求和结果
'''
if max_n == 0: # 判断终止条件
return num # 返回最终的结果
else:
# 每次max_n减1 【100,99,98,97,...0 】直到 max_n为0
# 每次num 加 max_n 【0 + 100 + 99 + 98 + ... + 0】
return recursion(max_n - 1 ,num + max_n) # 如果没达到终止条件就再次调用自己
if __name__ == '__main__':
max_n = 100
num = 0
recursion(max_n,num))
NP完全问题
- 什么是N问题
- N问题(Nondeterministic Problem)是指其解的搜索空间极大,其解不能用确定性算法找到。例如:调度问题,一次性安排n个任务在m台机器上,求最优解。
- 什么是P问题
- P问题(Polynomial Problem)是指其可以在多项式时间内解决的问题,其解可以用确定性算法找到。例如:最短路径问题,给出n个城市间的距离,求从城市1出发到城市n的最短路径及最短距离。
- 什么是NP问题
- NP问题(Non-deterministic Polynomial Problem)是指其可以在多项式时间内用非确定性算法检验算法是否正确,但无法在多项式时间内求出其最优解的问题。例如:旅行商问题,给出n个城市之间的距离,求一条从城市1出发,经过n个城市后回到城市1的最短路径及最短距离。
数据结构
数组
- 数组(Array)是一种线性表数据结构。它用一组连续的内存空间,来存储一组具有相同类型的数据。数组能够快速的查找指定索引位置的数据,但是缺点是插入和删除数据的操作比较低效。
# 初始化一个数组
my_array = [1, 2, 3, 4, 5]
# 访问指定索引位置的数据
print(my_array[2]) # 3
# 插入元素到指定位置
my_array.insert(3, 6)
# 删除指定位置的元素
del my_array[3]
链表
-
链表是一种数据结构,它的每个元素都有一个指针指向下一个元素,最后一个元素的指针指向一个特殊的空值,形成一个链。
-
链表的作用是存储有序数据,可以实现非连续存储,节省空间;可以实现动态存储,可以根据需要添加或删除元素;可以实现高效的插入和删除操作;可以实现更复杂的抽象数据类型。
-
优点:
-
- 节省空间,可以实现非连续存储;
-
- 可以实现动态存储,可以根据需要添加或删除元素;
-
- 可以实现高效的插入和删除操作;
-
- 可以实现更复杂的抽象数据类型。
-
-
缺点
-
- 链表元素的查找时间复杂度较高;
-
- 链表元素插入和删除操作可能需要遍历整个链表;
-
- 实现链表需要额外的存储空间。
-
# python实现的链表
class Node:
# 初始化
def __init__(self, data):
self.data = data # 数据域
self.next = None # 指针域
# 定义链表类
class LinkedList:
# 初始化
def __init__(self):
self.head = None # 链表头部
# 链表头部插入新节点
def insertAtHead(self, data):
newNode = Node(data)
newNode.next = self.head
self.head = newNode
# 链表尾部插入新节点
def insertAtTail(self, data):
newNode = Node(data)
currentNode = self.head
while currentNode.next is not None:
currentNode = currentNode.next
currentNode.next = newNode
# 在指定位置插入新节点
def insertAtPosition(self, data, position):
newNode = Node(data)
currentNode = self.head
currentPosition = 0
while currentPosition < position-1:
currentNode = currentNode.next
currentPosition += 1
newNode.next = currentNode.next
currentNode.next = newNode
# 删除指定位置的节点
def deleteAtPosition(self, position):
currentNode = self.head
currentPosition = 0
while currentPosition < position-1:
currentNode = currentNode.next
currentPosition += 1
currentNode.next = currentNode.next.next
# 获取链表的长度
def getLength(self):
currentNode = self.head
count = 0
while currentNode is not None:
currentNode = currentNode.next
count += 1
return count
# 获取指定位置的节点
def getAtPosition(self, position):
currentNode = self.head
currentPosition = 0
while currentPosition < position:
currentNode = currentNode.next
currentPosition += 1
return currentNode.data
# 打印链表
def printLinkedList(self):
currentNode = self.head
while currentNode is not None:
print(currentNode.data, end = " ")
currentNode = currentNode.next
# Demo
linked_list = LinkedList()
linked_list.insertAtHead(5)
linked_list.insertAtHead(6)
linked_list.insertAtTail(7)
linked_list.insertAtTail(8)
linked_list.printLinkedList() # 6 5 7 8
混合结构
-
混合结构是指一种数据结构,它允许用户以不同的方式存储和访问数据。它通常由一组不同的数据结构组成,如链表、堆栈、散列表和树等,用于实现不同的功能。混合结构可以节省空间,提高查询效率,并使得算法更加灵活。
-
混合数据结构可以帮助开发人员更好地分解和操作保存在数据库中的数据。它们也可以用于提高程序的执行效率,减少存储开销,提高程序代码的可读性和可维护性。
-
优点
-
- 混合数据结构可以提高程序的执行效率;
-
- 它们可以减少存储开销;
-
- 它们可以提高程序代码的可读性和可维护性;
-
- 它们可以更有效地实现某些操作(例如搜索);
-
- 它们可以帮助开发人员更好地分解和操作保存在数据库中的数据。
-
-
缺点
-
- 混合数据结构的实现要求更高的技术技能;
-
- 它们的设计和实现比简单的数据结构要更加复杂;
-
- 它们的实现过程可能更加耗时;
-
- 它们的维护成本可能更高;
-
- 它们往往更难以理解和使用。
-
# 声明混合结构
mixed_structure = {
"name": "John",
"age": 25,
"skills": ["Python", "Java", "C++"],
"is_married": False
}
# 获取结构中的属性
print(mixed_structure["name"])
print(mixed_structure["skills"])
# 修改结构中的属性
mixed_structure["name"] = "Tom"
mixed_structure["is_married"] = True
# 增加结构中的属性
mixed_structure["salary"] = 70000
mixed_structure["skills"].append("JavaScript")
# 删除结构中的属性
del mixed_structure["age"]
mixed_structure["skills"].remove("C++")
# 打印结构
print(mixed_structure)
栈
-
栈(Stack)是一种特殊的线性表,只允许在表的一端进行插入和删除操作,这一端被称为栈顶,另一端称为栈底,遵循先进后出的原则,后进入的元素先出栈。
-
用来实现特定的程序结构,如临时存放数据,实现程序的子程序调用等。
-
优点:
-
- 实现简单,操作也简单,只允许在表的一端进行插入和删除操作;
-
- 时间复杂度简单,只需要按照先进后出的原则进行插入和删除操作;
-
- 可以节省空间,因为它不需要为表中的元素预先分配存储空间,只要有需要就可以动态分配;
-
- 可以实现复杂的程序结构,如临时存放数据,实现程序的子程序调用等。
-
-
缺点:
-
- 由于只能在一端进行操作,所以无法对整个栈进行操作;
-
- 栈的大小有限,不能无限制的增加元素;
-
- 只能操作栈顶的元素,无法访问其他元素。
-
# Python栈实例:
# 栈的概念:先进后出,后进先出
class Stack:
# 初始化栈
def __init__(self):
self.items = []
# 判断栈是否为空
def is_empty(self):
return self.items == []
# 入栈
def push(self, item):
self.items.append(item)
# 出栈
def pop(self):
return self.items.pop()
# 返回栈顶元素
def peek(self):
return self.items[-1]
# 返回栈的大小
def size(self):
return len(self.items)
# 使用栈
s = Stack()
# 入栈
s.push(1)
s.push(2)
s.push(3)
s.push(4)
# 出栈
print(s.pop())
print(s.pop())
print(s.pop())
# 栈顶元素
print(s.peek())
# 栈的大小
print(s.size())
队列
-
定义:队列是一种先进先出的数据结构,后进来的元素排在前面的元素之后。
-
作用:队列很有用,可以用来存储和管理数据,在许多场合中可以使用队列来排序数据,甚至可以来实现多线程程序。
-
优点
-
- 队列具有简单快速的插入和删除操作;
-
- 队列可以用于排序数据;
-
- 队列可以用于实现多线程程序;
-
- 队列具有良好的空间利用性;
-
- 队列可以用来实现任务调度。
-
-
缺点:
-
- 队列的插入和删除操作受到限制;
-
- 队列的插入和删除操作可能会导致数据变化;
-
- 队列有可能被填满,这时候插入操作就会失败;
-
- 队列有可能会空,这时候删除操作就会失败。
-
class Queue():
def __init__(self):
# 初始化队列
self.items = []
def isEmpty(self):
# 检查队列是否为空
return self.items == []
def enqueue(self, item):
# 将新元素添加到队尾
self.items.insert(0,item)
def dequeue(self):
# 从队头删除元素
return self.items.pop()
def size(self):
# 返回队列大小
return len(self.items)
# 使用队列
q = Queue()
q.enqueue(4)
q.enqueue('dog')
q.enqueue(True)
print(q.size()) # 3
print(q.isEmpty()) # False
q.enqueue(8.4)
print(q.dequeue()) # 4
print(q.dequeue()) # dog
print(q.size()) # 2
散列表
-
定义:散列表(Hash Table)是一种数据存储结构,它使用一个函数(Hash Function)将元素映射到一个位置上,从而快速查找和存储数据。
-
作用:散列表可以实现快速的插入、查找和删除操作,因此它常被用来实现类似映射(Mapping)和字典(Dictionary)这样的数据结构。
-
优点:
-
- 查找和插入操作的时间复杂度都是O(1),非常高效;
-
- 散列表可以节省空间,即使是大量的元素,也可以存储在一个很小的数组里;
-
- 散列表不需要排序,因为数据已经存储在一个位置上,不需要比较;
-
-
缺点:
-
- 散列表需要额外的存储空间;
-
- 散列函数的设计很重要,如果散列函数不好,会导致散列冲突;
-
- 散列表不能保证数据的有序性,因为它根据散列函数将元素映射到一个位置上。
-
# 使用python实现散列表
# 定义HashTable类
class HashTable:
def __init__(self, size):
self.size = size
self.slots = [None] * self.size # 申请一个指定大小的列表,用来存储哈希表中的元素
self.data = [None] * self.size # 申请一个指定大小的列表,用来存储哈希表中元素的值
# 实现哈希算法,将键映射到表中的位置
def put(self, key, data):
hashvalue = self.hashfunction(key, len(self.slots)) # 计算key的hash值
if self.slots[hashvalue] == None: # 没有冲突,key映射到未被占用的槽位
self.slots[hashvalue] = key
self.data[hashvalue] = data
else: # 发生冲突,此处需要处理冲突
if self.slots[hashvalue] == key: # 检查key是否存在
self.data[hashvalue] = data # 更新数据
else:
nextslot = self.rehash(hashvalue, len(self.slots)) # 寻找下一个槽位
while self.slots[nextslot] != None and self.slots[nextslot] != key:
nextslot = self.rehash(nextslot, len(self.slots)) # 继续寻找槽位
if self.slots[nextslot] == None:
self.slots[nextslot] = key
self.data[nextslot] = data
else:
self.data[nextslot] = data # 更新数据
# 实现rehash算法,当发生冲突时找到下一个槽位
def rehash(self, oldhash, size):
return (oldhash + 1) % size
# 实现哈希函数
def hashfunction(self, key, size):
return key % size
# 根据key获取值
def get(self, key):
startslot = self.hashfunction(key, len(self.slots))
data = None
stop = False
found = False
position = startslot
while self.slots[position] != None and not found and not stop:
if self.slots[position] == key:
found = True
data = self.data[position]
else:
position = self.rehash(position, len(self.slots))
if position == startslot:
stop = True
return data
# 使用方法:
# 初始化哈希表
ht = HashTable(5)
# 向哈希表添加键值对
ht.put(1, 'one')
ht.put(2, 'two')
ht.put(3, 'three')
# 获取键对应的值
print(ht.get(1)) # one
print(ht.get(2)) # two
print(ht.get(3)) # three
图
- 定义:图是由顶点(Vertex)和边(Edge)组成的数据结构。顶点是图中的基本单元,边是连接两个顶点的线段。
- 作用:图可以用来表示任意复杂的网络关系,它可以用来描述交通路线,节点之间的联系,或者是社会网络中人与人之间的关系等。
- 优点:
- 描述能力强:图可以用来描述网络中的任意复杂的关系,并且不存在层次关系的限制;
- 易于理解:图的形式比其他数据结构更加直观,更易于理解;
- 搜索能力强:图可以用来寻找任意两点之间的最短路径;
- 可以存储对象:图可以用来存储对象,对象可以是顶点、边或者是其他的信息;
- 缺点:
- 实现比较复杂:图的实现比较复杂,它需要一个非常复杂的数据结构来存储图中的信息;
- 存储空间大:图的存储空间比较大,如果图中的顶点数量很大,那么存储空间就会很大;
- 搜索效率低:因为图的实现比较复杂,所以搜索效率也比较低;
- 不能做分析:图无法用来做分析,只能用来表示网络结构。
# 使用python实现图类
# 使用python实现图类
class Graph:
def __init__(self):
# 在构造函数中初始化图的信息
self.vertices = {} # 保存顶点的字典
self.edges = {} # 保存边的字典
# 添加顶点
def add_vertex(self, vertex):
self.vertices[vertex] = [] # 保存顶点的相邻顶点列表
# 添加边
def add_edge(self, start, end, weight=1):
if start in self.vertices and end in self.vertices:
self.edges[(start, end)] = weight
self.vertices[start].append(end) # 保存start顶点的相邻顶点
else:
print("Error: vertex not in graph")
# 获取图的顶点列表
def get_vertices(self):
return list(self.vertices.keys())
# 获取边的列表
def get_edges(self):
return self.edges
# 使用示例
# 初始化一个图
graph = Graph()
# 添加顶点
graph.add_vertex('A')
graph.add_vertex('B')
graph.add_vertex('C')
# 添加边
graph.add_edge('A', 'B', 2)
graph.add_edge('A', 'C', 3)
graph.add_edge('B', 'C', 4)
# 获取图的顶点列表
vertices = graph.get_vertices()
print(vertices) # ['A', 'B', 'C']
# 获取边的列表
edges = graph.get_edges()
print(edges) # {('A', 'B'): 2, ('A', 'C'): 3, ('B', 'C'): 4}
集合
-
定义:集合是一种无序的、不重复的数据结构,可以存储任何类型的数据。
-
作用:集合用于存储不同类型的数据,可以有效地进行检索和操作。
-
优点:
-
- 集合中的元素不重复,因此可以有效地消除重复数据;
-
- 集合可以实现快速检索,因为其元素与索引无关;
-
- 集合中的元素无序,可以更容易地实现操作。
-
-
缺点:
-
- 集合中的元素无序,没有具体的索引,因此无法根据索引快速检索;
-
- 集合没有具体的顺序,因此没有办法根据顺序排序。
-
# 创建两个集合
s1 = set([1, 2, 3, 4, 5])
s2 = set([4, 5, 6, 7, 8])
# 计算并集
s3 = s1 | s2
print(s3) # 输出:{1, 2, 3, 4, 5, 6, 7, 8}
# 计算交集
s4 = s1 & s2
print(s4) # 输出:{4, 5}
# 计算差集
s5 = s1 - s2
print(s5) # 输出:{1, 2, 3}
# 计算对称差集
s6 = s1 ^ s2
print(s6) # 输出:{1, 2, 3, 6, 7, 8}
# 检查元素是否存在于集合中
if 3 in s1:
print('3在集合s1中')
else:
print('3不在集合s1中')
二叉树
-
定义:二叉树是每个节点最多有两个子树的树结构。
-
作用:二叉树用于存储和检索数据,在计算机科学中有着广泛的应用,如二叉搜索树、堆、红黑树等。
-
优点:
-
- 查找、插入和删除的时间复杂度都很低,平均为 O(logn);
-
- 对于有序的数据,二叉搜索树的查找效率比顺序表更高;
-
- 二叉树可以被用于实现排序算法,如快速排序。
-
-
缺点:
-
- 二叉树的高度可能很高,这会降低查找效率;
-
- 如果不平衡,查找效率会降低;
-
- 二叉树需要额外的内存空间来存储指向子节点的指针。
-
# 二叉树的实现
# 二叉树的节点
class TreeNode:
def __init__(self, data):
self.data = data
self.left = None
self.right = None
# 二叉树
class BinaryTree:
def __init__(self):
self.root = None
# 向二叉树添加节点
def add_node(self, data):
# 先创建一个节点
node = TreeNode(data)
# 如果根节点为空
if self.root is None:
self.root = node
else:
# 使用队列存储节点
q = []
q.append(self.root)
# 循环队列
while True:
# 出队
temp = q.pop(0)
# 如果左节点为空
if temp.left is None:
temp.left = node
break
# 如果右节点为空
elif temp.right is None:
temp.right = node
break
# 如果左右节点均不为空
else:
# 将左右节点加入队列
q.append(temp.left)
q.append(temp.right)
# 二叉树的先序遍历
def pre_order(self, node):
if node is None:
return
# 先访问根节点
print(node.data)
# 再访问左节点
self.pre_order(node.left)
# 最后访问右节点
self.pre_order(node.right)
# 二叉树的中序遍历
def in_order(self, node):
if node is None:
return
# 先访问左节点
self.in_order(node.left)
# 再访问根节点
print(node.data)
# 最后访问右节点
self.in_order(node.right)
# 二叉树的后序遍历
def post_order(self, node):
if node is None:
return
# 先访问左节点
self.post_order(node.left)
# 再访问右节点
self.post_order(node.right)
# 最后访问根节点
print(node.data)
# 使用示例
if __name__ == '__main__':
bt = BinaryTree()
bt.add_node(1)
bt.add_node(2)
bt.add_node(3)
bt.add_node(4)
bt.add_node(5)
bt.add_node(6)
bt.add_node(7)
print('先序遍历:')
bt.pre_order(bt.root)
print('中序遍历:')
bt.in_order(bt.root)
print('后序遍历:')
bt.post_order(bt.root)
算法
二分查找
-
定义:二分查找(Binary Search)又称折半查找,是比较容易想到的一种查找算法。它的基本思想是:将数组分为两部分,比较中间元素和目标元素,然后根据比较结果,将搜索空间缩小一半。
-
作用:二分查找可以用来快速查找一个排序数组中的元素。
-
优点:二分查找的查找时间复杂度为O(logn),比起顺序查找O(n)有很大的优势,所以在大数据量的情况下,二分查找更有效率。
-
缺点:二分查找需要满足一定的条件,即待查找的数组必须是有序的,如果是无序的,则需要先进行排序,这样会降低查找效率。
# 二分查找
def binary_search(list, item):
"""
二分查找给定的列表,如果找到值返回索引,否则返回None
"""
low = 0
high = len(list)-1
# 只要范围没有缩小到只包含一个元素,就持续查找
while low <= high:
# 计算中间位置
mid = (low + high) // 2
# 将中间位置的值与值进行比较
guess = list[mid]
# 如果猜测的值等于要查找的值,就返回索引
if guess == item:
return mid
# 如果猜测的值大于要查找的值,就将high设置为中间位置的前一个位置
if guess > item:
high = mid -1
# 如果猜测的值小于要查找的值,就将low设置为中间位置的后一个位置
else:
low = mid + 1
# 如果没有找到值,就返回None
return None
# 调用示例
my_list = [1, 3, 5, 7, 9]
print(binary_search(my_list, 3)) # 输出1
print(binary_search(my_list, -1)) # 输出None
选择排序
-
定义:选择排序(Selection Sort)是一种原地排序算法,它的工作原理是每一次从待排序的数据元素中选出最小(或最大)的一个元素,存放在序列的起始位置,直到全部待排序的数据元素排完。
-
作用:选择排序是一种简单直观的排序算法,它能够在不额外使用存储空间的情况下,对比较小的序列进行排序。
-
优点:
- ①时间复杂度为O(n2),算法简单;
- ②不占用额外的存储空间,移动元素的次数少;
- ③适用于少量数据的排序。
-
缺点:
- ①比较次数较多,时间复杂度较高;
- ②不稳定的排序方式;
- ③数据量大时,性能上不如其他快速排序算法。
# 选择排序算法
def selection_sort(data):
'''
选择排序算法:从第一个元素开始,比较它后面的元素,找出最小的元素,与第一个元素交换位置,
再从第二个元素开始,重复上述步骤,直到将所有元素排序完毕
:param data: 待排序的list
:return: 排序后的list
'''
length = len(data)
for i in range(length):
smallestIndex = i
for j in range(i+1, length):
if data[j] < data[smallestIndex]:
smallestIndex = j
data[i], data[smallestIndex] = data[smallestIndex], data[i]
return data
# 调用示例
data = [10, 20, 4, 45, 99, 40, 25, 60]
print(selection_sort(data)) # [4, 10, 20, 25, 40, 45, 60, 99]
快速排序
-
定义:快速排序(Quick Sort)是一种分治算法,由 Tony Hoare 在 1960 年提出,它的基本思想是:选择一个基准元素,通过一趟排序将要排序的数据分割成独立的两部分,其中一部分的所有数据都比另外一部分的所有数据都要小,然后再按此方法对这两部分数据分别进行快速排序,整个排序过程可以递归进行,以此达到整个数据变成有序序列。
-
- 作用:快速排序用于对数据进行排序。
-
优点
-
- 快速排序的时间复杂度为O(nlogn),运行效率高;
-
- 快速排序是不稳定排序算法,比较占用内存,但是它是原地排序,不占用额外内存;
-
- 由于其特殊的分治思想,使得快速排序比其他排序算法更有效。
-
-
缺点
-
- 快速排序在最坏的情况下,时间复杂度可能会退化成O(n2);
-
- 快速排序的最坏情况就是每次都要选取最大或者最小的元素作为基准;
-
- 对于部分有序的数据,快排的性能并不是很好。
-
def quick_sort(lists): # 快速排序
"""
快速排序是一种分而治之的算法,可以快速地对一个列表进行排序。
它的基本思想是:首先在数列中选择一个基准点,然后将数列中比该基准点小的放到它的左边,大的放到它的右边,
然后依次将该基准点左边和右边的数列进行这种操作,最终可以完成数列的排序。
"""
if not lists:
return []
pivot = lists[0]
left = [x for x in lists[1:] if x < pivot] # 将列表中比基准点小的数放到基准点的左边
right = [x for x in lists[1:] if x >= pivot] # 将列表中比基准点大的数放到基准点的右边
left = quick_sort(left) # 递归处理基准点左边的数列
right = quick_sort(right) # 递归处理基准点右边的数列
return left + [pivot] + right # 返回最终排序的结果
# 调用示例
lists = [3, 4, 1, 6, 7, 8, 9, 2, 5]
print(quick_sort(lists)) # [1, 2, 3, 4, 5, 6, 7, 8, 9]
广度优先&深度优先
-
广度优先(Breadth-First)是一种图遍历算法,它采用“一层一层”的方式依次访问图中的结点,先搜索离源结点最近的结点,再搜索离源结点更远的结点,直到搜索完整个图。
-
深度优先(Depth-First)是一种图遍历算法,它采用“一条路走到黑”的方式,从源结点开始,沿着一条路直走,直到走到死胡同,然后再回退,寻找其他的分支,直到整个图都搜索完毕。
-
广度优先和深度优先的作用是用来搜索图中的结点。
-
它们的优点是:
-
- 广度优先可以找到最短路径;
-
- 深度优先可以找到更深层次的结构;
-
-
它们的缺点是:
-
- 广度优先可能会浪费内存;
-
- 深度优先可能会进入死胡同,无法找到最优解。
-
# 广度优先
def bfs(graph, start):
visited = set()
queue = [start]
while queue:
vertex = queue.pop(0)
if vertex not in visited:
visited.add(vertex)
queue.extend(graph[vertex] - visited)
return visited
# 深度优先
def dfs(graph, start):
visited = set()
stack = [start]
while stack:
vertex = stack.pop()
if vertex not in visited:
visited.add(vertex)
stack.extend(graph[vertex] - visited)
return visited
# 调用示例
graph = {'A': set(['B', 'C']),
'B': set(['A', 'D', 'E']),
'C': set(['A', 'F']),
'D': set(['B']),
'E': set(['B', 'F']),
'F': set(['C', 'E'])}
print(bfs(graph, 'A')) # {'E', 'B', 'F', 'A', 'C', 'D'}
print(dfs(graph, 'A')) # {'E', 'D', 'F', 'B', 'C', 'A'}
狄克斯特拉算法
-
狄克斯特拉算法是一种用于求解最短路径的算法,它是一种贪婪算法,每一步都在求得最优解。
-
它的作用是根据给定的起点、终点和路径节点之间的距离来求解最短路径。
-
它的优点是:
- 算法简单易懂,实现容易;
- 速度快,可以在较短的时间内解决问题;
- 可以解决传统最短路径算法无法解决的复杂问题;
- 可以用于解决有向图和无向图问题。
-
它的缺点是:
- 只能计算最短路径,不能计算最优路径;
- 不能处理带有权重的边;
- 无法处理负权重的边;
- 算法复杂度较高。
# 定义一个狄克斯特拉算法类
class Dijkstra:
# 初始化函数,输入为图的节点和边
def __init__(self, nodes, edges):
self.nodes = nodes
self.edges = edges
# 主函数
def dijkstra(self, start, end):
# 先将图的节点存储在一个字典中,key为节点,value为当前节点的最短距离
path_dict = {node: float('inf') for node in self.nodes}
# 将起点的最短距离设为0
path_dict[start] = 0
# 将起点加入已经访问的节点
visited = set()
# 当起点不等于终点时
while start != end:
# 将起点加入已访问节点
visited.add(start)
# 将起点的相邻节点加入未访问节点
child_nodes = self.edges[start]
not_visited = set(child_nodes).difference(visited)
# 对于未访问节点,更新它们的最短距离
for node in not_visited:
if path_dict[start] + child_nodes[node] < path_dict[node]:
path_dict[node] = path_dict[start] + child_nodes[node]
# 将未访问节点中最短距离最小的节点作为新的起点
new_start = min(path_dict, key=path_dict.get)
start = new_start
# 返回终点的最短距离
return path_dict[end]
# 创建节点列表
nodes = ['A', 'B', 'C', 'D', 'E', 'F', 'G']
# 创建边列表
edges = {
'A': {'B': 5, 'C': 1},
'B': {'A': 5, 'C': 2, 'D': 1},
'C': {'A': 1, 'B': 2, 'D': 4, 'E': 8},
'D': {'B': 1, 'C': 4, 'E': 3, 'F': 6},
'E': {'C': 8, 'D': 3},
'F': {'D': 6},
'G': {}
}
# 实例化一个狄克斯特拉算法类
dijkstra = Dijkstra(nodes, edges)
# 调用dijkstra函数求解从‘A’到‘E’的最短距离
shortest_path = dijkstra.dijkstra('A', 'E')
# 输出最短距离
print(shortest_path) # 5
近似算法
-
近似算法是一种求解复杂问题的算法,它通过对原问题进行简化,将复杂问题转化为更容易求解的实际问题,以达到求解复杂问题的目的。
-
近似算法的作用是帮助人们求解复杂问题,减少计算量,提高求解效率。例如,最短路径问题是一个复杂的问题,人们可以通过近似算法来求解,而不用穷举所有可能的路径。
-
优点:
- 它可以求解复杂问题,大大减少计算量和时间,使求解复杂问题变得更加容易;
- 它可以产生更好的结果,更适合实际应用;
- 它可以提高求解效率,更有利于解决实际问题;
- 它也可以在保证结果精度的前提下,进行部分结果的实时计算。
-
缺点:
- 由于算法过程中简化了问题,因此可能会有一定的误差;
- 此外,近似算法的计算复杂度还是比较高的,因此能够计算的数据量也是相对较小的。
# 近似算法
# 找出字符串s中,最长的回文子串
# 回文子串是指从左到右读和从右到左读完全相同的子串
def approximate_algorithm(s):
# 定义一个字典,用来保存所有可能的回文子串
result = {}
# 遍历字符串s,找出所有可能的回文子串
for i in range(len(s)):
for j in range(i+1,len(s)+1):
# 若子串s[i:j]是一个回文子串
if s[i:j] == s[i:j][::-1]:
# 将子串s[i:j]以及它的长度加入字典result中
result[s[i:j]] = j-i
# 找出字典result中子串最长的那个
longest_str = max(result,key=result.get)
# 将最长的回文子串返回
return longest_str
# 调用示例
print(approximate_algorithm('abccbace')) # 输出结果为'bccb'
K最近邻算法
-
K最近邻算法(K-Nearest Neighbor,KNN)是一种基于实例的学习,它的基本思想是:如果一个样本在特征空间中的k个最相似(即特征空间中最邻近)的样本中的大多数属于某一个类别,则该样本也属于这个类别。KNN算法中,k是用户指定的正整数,也可以是自动选择的。
-
KNN的作用是:用于分类和回归。KNN分类是指,给定一个训练样本集,对新的输入实例,在训练样本集中找到与该实例最邻近的K个实例,则该新实例的类别就是K个实例中比例最大的类别。KNN回归是指,给定一个训练样本集,对新的输入实例,在训练样本集中找到与该实例最邻近的K个实例,则该新实例的输出就是K个实例中输出值的平均值。
-
KNN算法的优点是:
-
1、算法简单,易于理解,易于实现,对异常值不敏感;
-
2、计算复杂度低,训练时间仅与样本数量有关,不随特征数量增加而增加;
-
3、可以提供可解释性,由于KNN算法是基于实例的学习,它更容易被人理解。
-
-
KNN算法的缺点是:
-
1、计算量大,当样本量较大时,搜索最近邻实例需要花费大量的时间;
-
2、结果不稳定,有可能出现改变很小的输入实例,导致划分结果的显著变化;
-
3、对于类别不平衡的问题,KNN表现不佳,容易受到类别不平衡的影响。
-
#导入相关模块
import numpy as np
from math import sqrt
from collections import Counter
#创建KNN类
class KNN:
#初始化函数
def __init__(self, k):
self.k = k
#fit函数,用于训练模型
def fit(self, X_train, y_train):
self.X_train = X_train
self.y_train = y_train
#predict函数,用于预测
def predict(self, X_predict):
#存放最终结果
y_predict = [self._predict(x) for x in X_predict]
return np.array(y_predict)
#_predict函数,用于计算单个预测结果
def _predict(self, x):
#计算距离
distances = [sqrt(np.sum((x_train - x)**2)) for x_train in self.X_train]
#找到最近的K个点
nearest = np.argsort(distances)[:self.k]
#找到K个点对应的标签
topK_y = [self.y_train[i] for i in nearest]
#找到K个点中标签最多的类别
votes = Counter(topK_y)
#print(votes)
return votes.most_common(1)[0][0]
#调用示例
#导入要使用的模块
import numpy as np
from sklearn.model_selection import train_test_split
#创建KNN类
knn_clf = KNN(3)
#准备数据
X = np.array([[3,104],[2,100],[1,81],[101,10],[99,5],[98,2]])
y = np.array([0,0,0,1,1,1])
#将数据分为训练集和测试集
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.3)
#训练模型
knn_clf.fit(X_train, y_train)
#预测结果
y_predict = knn_clf.predict(X_test)
#打印预测结果
print(y_predict)
策略
D&C分而治之
-
D&C分而治之是一种解决问题的方法。它的作用是将一个大问题分解成若干个较小的子问题,并按照一定的顺序解决每个子问题,最终解决整个问题。
-
D&C分而治之的优点有:
-
1、减少计算量:将一个大问题分解成若干个小问题,通过对每个小问题的独立解决,可以大大减少计算量;
-
2、提高代码的可读性:通过将一个大问题分解成若干个小问题,将代码分成若干个函数,可以极大地提高代码的可读性;
-
3、可复用性强:由于每个小问题的解法都是独立的,所以,可以将其中某些小问题的解法复用到其他问题中,从而提高代码的可复用性。
-
-
D&C分而治之的缺点是:
-
1、需要花费更多的时间:将一个大问题分解成若干个小问题,解决每个小问题,最终求得整个问题的解,这需要花费更多的时间;
-
2、需要更多的空间:分解一个大问题,求解每个小问题,需要更多的空间来存储每个小问题的解;
-
3、调试困难:将一个大问题分解成若干个小问题,每个小问题的调试都是一件复杂的事情,很难保证每个小问题都正确。
-
# 以求和问题为例,使用python实现D&C分而治之如下:
def sum(list):
# 如果list只有一个元素,则返回该元素
if len(list) == 1:
return list[0]
# 否则,将list分成两个部分,分别求和
else:
return sum(list[:len(list)//2]) + sum(list[len(list)//2:])
# 调用示例
list = [1,2,3,4,5]
print(sum(list)) # 输出15
贪婪算法
-
贪婪算法是一种在每一步中都采取在当前状态下最好或最优(即最有利)的选择,从而希望导致结果是最好或最优的算法。
-
贪婪算法的作用是在搜索空间中找到最优解,它可以用来解决最优化问题,如最小路径、最大收益、最小花费等。
-
优点是贪婪算法的实现简单,它可以很快地找到一个近似最优解;它不需要进行大量的计算,只需要计算当前状态的最优解;它可以用来解决复杂的问题,例如动态规划和图论。
-
缺点是贪婪算法不能保证找到最优解,它只能找到一个近似最优解;它可能陷入局部最优解,而不是全局最优解;它不能处理复杂的约束条件。
# 定义一个列表,表示每次操作的收益
gains = [4, 8, 7, 6, 5]
# 定义一个函数来求出最大收益
def get_max_gains(gains):
# 假设最大收益为第一个收益
max_gains = gains[0]
# 逐一比较每次操作的收益
for i in range(1, len(gains)):
# 如果当前收益大于最大收益,则更新最大收益
if gains[i] > max_gains:
max_gains = gains[i]
# 返回最大收益
return max_gains
# 调用函数求出最大收益
max_gains = get_max_gains(gains)
# 输出最大收益
print(max_gains)
# 调用示例
get_max_gains(gains) # 输出:8
动态规划
-
动态规划是一种通过将大型复杂的问题分解成若干个小的子问题,逐一解决子问题,最终解决原问题的技术。它实际上是一种分析最优解的方法,求解多个子问题时,子问题会有重叠,动态规划可以将重叠子问题只求解一次,从而减少求解所需要的时间。
-
动态规划的作用是通过分解复杂的问题,将其转化为一系列子问题,然后从子问题中求解最优解,最终得到原问题的最优解。
-
动态规划的优点是可以解决复杂的问题,提高了计算效率,减少了计算量,它可以将大型复杂问题分解成若干个子问题,每个子问题可以独立求解,这样可以节省计算时间。
-
动态规划的缺点是实现起来比较复杂,需要考虑很多细节问题,而且要求解的问题本身必须具有最优子结构,才能使用动态规划求解。另外,由于需要存储大量的中间结果,空间复杂度也较高,有时会导致内存不足的问题。
# 动态规划实现
# 问题:求解最大路径和
# 这里定义一个二维数组
arr = [[1,2,3],
[4,5,6],
[7,8,9]]
#定义一个二维数组,用来存储到达每个点最大路径和
dp = [[0 for _ in range(len(arr[0]))] for _ in range(len(arr))]
# 将最上面一行和最左边一列的数据赋值为arr数组中的值
for i in range(len(arr)):
dp[i][0] = arr[i][0]
for j in range(len(arr[0])):
dp[0][j] = arr[0][j]
# 根据状态转移方程dp[i][j] = max(dp[i-1][j], dp[i][j-1]) + arr[i][j]
# 从上到下,从左到右计算每一个值
for i in range(1, len(arr)):
for j in range(1, len(arr[0])):
dp[i][j] = max(dp[i-1][j], dp[i][j-1]) + arr[i][j]
# 调用示例:
print(dp[len(arr)-1][len(arr[0])-1])