当前位置: 首页 > news >正文

数据结构与算法总结整理(超级全的哦!)

数据结构与算法

  • 基础
    • 大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)
        对于算法进行特别具体的细致分析虽然很好,但在实践中的实际价值有限。对于算法的时间性质和空间性质,最重要的是其数量级和趋势,这些是分析算法效率的主要部分。而计量算法基本操作数量的规模函数中那些常量因子可以忽略不计。例如,可以认为3n23n^23n2100n2100n^2100n2属于同一个量级,如果两个算法处理同样规模实例的代价分别为这两个函数,就认为它们的效率“差不多”,都为n2n^2n2级。

时间复杂度排序:

在这里插入图片描述
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(logn)<O(n)<O(nlogn)<O(n2)<O(n3)<O(2n)<O(n!)<O(nn)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
  1. 对普通进程来说,它能看到的其实是内核提供的虚拟内存,这些虚拟内存还需要通过页表,由系统映射为物理内存。
  2. 当进程通过 malloc() 申请内存后,内存并不会立即分配,而是在首次访问时,才通过缺页异常陷入内核中分配内存。
  3. 由于进程的虚拟地址空间比物理内存大很多,Linux 还提供了一系列的机制,应对内存不足的问题,比如缓存的回收、交换分区 Swap
    以及OOM 等。
  4. 当需要了解系统或者进程的内存使用情况时,可以用 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 = 100num = 0recursion(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]

链表

  • 链表是一种数据结构,它的每个元素都有一个指针指向下一个元素,最后一个元素的指针指向一个特殊的空值,形成一个链。

  • 链表的作用是存储有序数据,可以实现非连续存储,节省空间;可以实现动态存储,可以根据需要添加或删除元素;可以实现高效的插入和删除操作;可以实现更复杂的抽象数据类型。

  • 优点:

      1. 节省空间,可以实现非连续存储;
      1. 可以实现动态存储,可以根据需要添加或删除元素;
      1. 可以实现高效的插入和删除操作;
      1. 可以实现更复杂的抽象数据类型。
  • 缺点

      1. 链表元素的查找时间复杂度较高;
      1. 链表元素插入和删除操作可能需要遍历整个链表;
      1. 实现链表需要额外的存储空间。
# 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.headself.head = newNode# 链表尾部插入新节点def insertAtTail(self, data):newNode = Node(data)currentNode = self.headwhile currentNode.next is not None:currentNode = currentNode.nextcurrentNode.next = newNode# 在指定位置插入新节点def insertAtPosition(self, data, position):newNode = Node(data)currentNode = self.headcurrentPosition = 0while currentPosition < position-1:currentNode = currentNode.nextcurrentPosition += 1newNode.next = currentNode.nextcurrentNode.next = newNode# 删除指定位置的节点def deleteAtPosition(self, position):currentNode = self.headcurrentPosition = 0while currentPosition < position-1:currentNode = currentNode.nextcurrentPosition += 1currentNode.next = currentNode.next.next# 获取链表的长度def getLength(self):currentNode = self.headcount = 0while currentNode is not None:currentNode = currentNode.nextcount += 1return count# 获取指定位置的节点def getAtPosition(self, position):currentNode = self.headcurrentPosition = 0while currentPosition < position:currentNode = currentNode.nextcurrentPosition += 1return currentNode.data# 打印链表def printLinkedList(self):currentNode = self.headwhile 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

混合结构

  • 混合结构是指一种数据结构,它允许用户以不同的方式存储和访问数据。它通常由一组不同的数据结构组成,如链表、堆栈、散列表和树等,用于实现不同的功能。混合结构可以节省空间,提高查询效率,并使得算法更加灵活。

  • 混合数据结构可以帮助开发人员更好地分解和操作保存在数据库中的数据。它们也可以用于提高程序的执行效率,减少存储开销,提高程序代码的可读性和可维护性。

  • 优点

      1. 混合数据结构可以提高程序的执行效率;
      1. 它们可以减少存储开销;
      1. 它们可以提高程序代码的可读性和可维护性;
      1. 它们可以更有效地实现某些操作(例如搜索);
      1. 它们可以帮助开发人员更好地分解和操作保存在数据库中的数据。
  • 缺点

      1. 混合数据结构的实现要求更高的技术技能;
      1. 它们的设计和实现比简单的数据结构要更加复杂;
      1. 它们的实现过程可能更加耗时;
      1. 它们的维护成本可能更高;
      1. 它们往往更难以理解和使用。
# 声明混合结构
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)是一种特殊的线性表,只允许在表的一端进行插入和删除操作,这一端被称为栈顶,另一端称为栈底,遵循先进后出的原则,后进入的元素先出栈。

  • 用来实现特定的程序结构,如临时存放数据,实现程序的子程序调用等。

  • 优点:

      1. 实现简单,操作也简单,只允许在表的一端进行插入和删除操作;
      1. 时间复杂度简单,只需要按照先进后出的原则进行插入和删除操作;
      1. 可以节省空间,因为它不需要为表中的元素预先分配存储空间,只要有需要就可以动态分配;
      1. 可以实现复杂的程序结构,如临时存放数据,实现程序的子程序调用等。
  • 缺点:

      1. 由于只能在一端进行操作,所以无法对整个栈进行操作;
      1. 栈的大小有限,不能无限制的增加元素;
      1. 只能操作栈顶的元素,无法访问其他元素。
# 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())

队列

  • 定义:队列是一种先进先出的数据结构,后进来的元素排在前面的元素之后。

  • 作用:队列很有用,可以用来存储和管理数据,在许多场合中可以使用队列来排序数据,甚至可以来实现多线程程序。

  • 优点

      1. 队列具有简单快速的插入和删除操作;
      1. 队列可以用于排序数据;
      1. 队列可以用于实现多线程程序;
      1. 队列具有良好的空间利用性;
      1. 队列可以用来实现任务调度。
  • 缺点:

      1. 队列的插入和删除操作受到限制;
      1. 队列的插入和删除操作可能会导致数据变化;
      1. 队列有可能被填满,这时候插入操作就会失败;
      1. 队列有可能会空,这时候删除操作就会失败。
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)这样的数据结构。

  • 优点:

      1. 查找和插入操作的时间复杂度都是O(1),非常高效;
      1. 散列表可以节省空间,即使是大量的元素,也可以存储在一个很小的数组里;
      1. 散列表不需要排序,因为数据已经存储在一个位置上,不需要比较;
  • 缺点:

      1. 散列表需要额外的存储空间;
      1. 散列函数的设计很重要,如果散列函数不好,会导致散列冲突;
      1. 散列表不能保证数据的有序性,因为它根据散列函数将元素映射到一个位置上。
# 使用python实现散列表
# 定义HashTable类
class HashTable:def __init__(self, size):self.size = sizeself.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] = keyself.data[hashvalue] = dataelse:  # 发生冲突,此处需要处理冲突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] = keyself.data[nextslot] = dataelse: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 = Nonestop = Falsefound = Falseposition = startslotwhile self.slots[position] != None and not found and not stop:if self.slots[position] == key:found = Truedata = self.data[position]else:position = self.rehash(position, len(self.slots))if position == startslot:stop = Truereturn 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)] = weightself.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}

集合

  • 定义:集合是一种无序的、不重复的数据结构,可以存储任何类型的数据。

  • 作用:集合用于存储不同类型的数据,可以有效地进行检索和操作。

  • 优点:

      1. 集合中的元素不重复,因此可以有效地消除重复数据;
      1. 集合可以实现快速检索,因为其元素与索引无关;
      1. 集合中的元素无序,可以更容易地实现操作。
  • 缺点:

      1. 集合中的元素无序,没有具体的索引,因此无法根据索引快速检索;
      1. 集合没有具体的顺序,因此没有办法根据顺序排序。
# 创建两个集合
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中')

二叉树

  • 定义:二叉树是每个节点最多有两个子树的树结构。

  • 作用:二叉树用于存储和检索数据,在计算机科学中有着广泛的应用,如二叉搜索树、堆、红黑树等。

  • 优点:

      1. 查找、插入和删除的时间复杂度都很低,平均为 O(logn);
      1. 对于有序的数据,二叉搜索树的查找效率比顺序表更高;
      1. 二叉树可以被用于实现排序算法,如快速排序。
  • 缺点:

      1. 二叉树的高度可能很高,这会降低查找效率;
      1. 如果不平衡,查找效率会降低;
      1. 二叉树需要额外的内存空间来存储指向子节点的指针。

# 二叉树的实现# 二叉树的节点
class TreeNode:def __init__(self, data):self.data = dataself.left = Noneself.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 = nodeelse:# 使用队列存储节点q = []q.append(self.root)# 循环队列while True:# 出队temp = q.pop(0)# 如果左节点为空if temp.left is None:temp.left = nodebreak# 如果右节点为空elif temp.right is None:temp.right = nodebreak# 如果左右节点均不为空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 = 0high = 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# 如果没有找到值,就返回Nonereturn 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 = ifor j in range(i+1, length):if data[j] < data[smallestIndex]:smallestIndex = jdata[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 年提出,它的基本思想是:选择一个基准元素,通过一趟排序将要排序的数据分割成独立的两部分,其中一部分的所有数据都比另外一部分的所有数据都要小,然后再按此方法对这两部分数据分别进行快速排序,整个排序过程可以递归进行,以此达到整个数据变成有序序列。

    • 作用:快速排序用于对数据进行排序。
  • 优点

      1. 快速排序的时间复杂度为O(nlogn),运行效率高;
      1. 快速排序是不稳定排序算法,比较占用内存,但是它是原地排序,不占用额外内存;
      1. 由于其特殊的分治思想,使得快速排序比其他排序算法更有效。
  • 缺点

      1. 快速排序在最坏的情况下,时间复杂度可能会退化成O(n2);
      1. 快速排序的最坏情况就是每次都要选取最大或者最小的元素作为基准;
      1. 对于部分有序的数据,快排的性能并不是很好。
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)是一种图遍历算法,它采用“一条路走到黑”的方式,从源结点开始,沿着一条路直走,直到走到死胡同,然后再回退,寻找其他的分支,直到整个图都搜索完毕。

  • 广度优先和深度优先的作用是用来搜索图中的结点。

  • 它们的优点是:

      1. 广度优先可以找到最短路径;
      1. 深度优先可以找到更深层次的结构;
  • 它们的缺点是:

      1. 广度优先可能会浪费内存;
      1. 深度优先可能会进入死胡同,无法找到最优解。

# 广度优先
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 = nodesself.edges = edges# 主函数def dijkstra(self, start, end):# 先将图的节点存储在一个字典中,key为节点,value为当前节点的最短距离path_dict = {node: float('inf') for node in self.nodes}# 将起点的最短距离设为0path_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])

相关文章:

数据结构与算法总结整理(超级全的哦!)

数据结构与算法基础大O表示法时间复杂度大O表示法时间复杂度排序&#xff1a;最坏时间复杂度时间复杂度的几条基本计算规则内存工作原理什么是内存内存主要分为三种存储器随机存储器&#xff08;RAM&#xff09;只读存储器&#xff08;ROM&#xff09;高速缓存&#xff08;Cach…...

DPDK — MALLOC 堆内存管理组件

目录 文章目录 目录MALLOC 堆内存管理组件rte_malloc() 接口malloc_heap 结构体malloc_elem 结构体内存初始化流程内存申请流程内存释放流程MALLOC 堆内存管理组件 MALLOC(堆内存管理组件)基于 hugetlbfs 内核文件系统来实现,能够从 HugePage 中分配一块连续的物理大页内存…...

分享113个HTML艺术时尚模板,总有一款适合您

分享113个HTML艺术时尚模板&#xff0c;总有一款适合您 113个HTML艺术时尚模板下载链接&#xff1a;https://pan.baidu.com/s/1ReoPNIRjkYov-SjsPo0vhg?pwdjk4a 提取码&#xff1a;jk4a Python采集代码下载链接&#xff1a;采集代码.zip - 蓝奏云 女性化妆用品网页模板 粉…...

2023年美赛C题Wordle预测问题一建模及Python代码详细讲解

相关链接 &#xff08;1&#xff09;2023年美赛C题Wordle预测问题一建模及Python代码详细讲解 &#xff08;2&#xff09;2023年美赛C题Wordle预测问题二建模及Python代码详细讲解 &#xff08;3&#xff09;2023年美赛C题Wordle预测问题三、四建模及Python代码详细讲解 &…...

小米12s ultra,索尼xperia1 iv,数码相机 拍照对比

首先说明所有的测试结果和拍摄数据我放到百度网盘了(地址在结尾) 为什么做这个测试 我一直想知道现在的手机和相机差距有多大,到底差在哪儿? 先说结论: 1.1英寸的手机cmos(2022年) 6年前(2016)的入门款相机(m43画幅) 2.手机 不能换镜头,只能在特定的拍摄距离才能发挥出全…...

C++笔记 模板的进阶知识

目录 1. 非类型模板参数 2.模板的特化 2.1 函数模板的特化 2.2 类模板的特化 2.2.1 全特化 2.2.2 偏特化 3.模板的分离编译 3.1 什么是分离编译&#xff1f; 3.2 模板的分离编译 4.模板的总结 模板的初阶内容&#xff1a;(594条消息) C模板的原理和使用_全貌的博客-CSD…...

基于 Debain11 构建 asp.net core 6.x 的基础运行时镜像

基于 Debain11 构建 asp.net core 6.x 的基础运行时镜像Linux 环境说明Debian 简介Debian 发行版本关于 Debian 11Linux 常用基础工具Dockerfile 中 RUN 指令RUN 语法格式RUN 语义说明编写 Dockerfile 构建 Runtime 基础镜像ASP.NET Core Runtime 基础镜像Dockerfile 编写Windo…...

【无人机路径规划】基于IRM和RRTstar进行无人机路径规划(Matlab代码实现)

&#x1f4a5;&#x1f4a5;&#x1f49e;&#x1f49e;欢迎来到本博客❤️❤️&#x1f4a5;&#x1f4a5; &#x1f3c6;博主优势&#xff1a;&#x1f31e;&#x1f31e;&#x1f31e;博客内容尽量做到思维缜密&#xff0c;逻辑清晰&#xff0c;为了方便读者。 ⛳️座右铭&a…...

Spring Boot中使用@Autowire装配接口是怎么回事?

在学习使用Spring Boot框架时候&#xff0c;发现了一个特别的现象UserMapper是一个接口&#xff0c;在另一个类中好像直接使用Autowired装配了一个UserMapper对象&#xff1f;&#xff1f;&#xff1f;我纳闷了一会儿&#xff0c;接口居然可以直接实例对象吗&#xff1f;根据我…...

23种设计模式介绍(Python示例讲解)

文章目录一、概述二、设计模式七种原则三、设计模式示例讲解1&#xff09;创建型模式1、工厂模式&#xff08;Factory Method&#xff09;【1】简单工厂模式&#xff08;不属于GOF设计模式之一&#xff09;【2】工厂方法模式2、抽象工厂模式&#xff08;AbstractFactory&#x…...

初识Hadoop,走进大数据世界

文章目录数据&#xff01;数据&#xff01;遇到的问题Hadoop的出现相较于其他系统的优势关系型数据库网格计算本文章属于Hadoop系列文章&#xff0c;分享Hadoop相关知识。后续文章中会继续分享Hadoop的组件、MapReduce、HDFS、Hbase、Flume、Pig、Spark、Hadoop集群管理系统以及…...

加油站会员管理小程序实战开发教程14 会员充值

我们上篇介绍了会员开卡的业务,开卡是为了创建会员卡的信息。有了会员卡信息后我们就可以给会员进行充值。当然了充值这个业务是由会员自主发起的。 按照我们的产品原型,我们在我的页面以轮播图的形式循环展示当前会员的所有卡信息。这个会员卡信息需要先用变量从数据源读取…...

leetcode 1792. 最大平均通过率

一所学校里有一些班级&#xff0c;每个班级里有一些学生&#xff0c;现在每个班都会进行一场期末考试。给你一个二维数组 classes &#xff0c;其中 classes[i] [passi, totali] &#xff0c;表示你提前知道了第 i 个班级总共有 totali 个学生&#xff0c;其中只有 passi 个学…...

15-基础加强-2-xml(约束)枚举注解

文章目录1.xml1.1概述【理解】(不用看)1.2标签的规则【应用】1.3语法规则【应用】1.4xml解析【应用】1.5DTD约束【理解】1.5.1 引入DTD约束的三种方法1.5.2 DTD语法&#xff08;会阅读&#xff0c;然后根据约束来写&#xff09;1.6 schema约束【理解】1.6.1 编写schema约束1.6.…...

13:高级篇 - CTK 事件管理机制(signal/slot)

作者: 一去、二三里 个人微信号: iwaleon 微信公众号: 高效程序员 在《12:高级篇 - CTK 事件管理机制(sendEvent/postEvent)》一文中,我们介绍了如何进行插件间通信 - sendEvent()/postEvent() + ctkEventHandler。然而,除了这种方式之外,EventAdmin 还提供了另一种方…...

群晖-第1章-IPV6的DDNS

群晖-第1章-IPV6的DDNS 方案&#xff1a;腾讯云群晖DS920 本文参考群晖ipv6 DDNS-go教程-牧野狂歌&#xff0c;感谢原作者的分享。 这篇文章只记录了我需要的部分&#xff0c;其他的可以查看原文&#xff0c;原文还记录了更多的内容&#xff0c;可能帮到你。 一、购买域名 …...

centos7系统-kubeadm安装k8s集群(v1.26版本)亲测有效,解决各种坑可供参考

文章目录硬件要求可省略的步骤配置虚拟机ip设置阿里镜像源各服务器初始化配置配置主节点的主机名称配置从节点的主机名称配置各节点的Host文件关闭各节点的防火墙关闭selinux永久禁用各节点的交换分区同步各节点的时间将桥接的IPv4流量传递到iptables的链&#xff08;三台都执行…...

帮助指令 man ,help及文档常用管理指令

帮助指令 man&#xff0c;help 1. man 当我们想要了解某个命令如何使用&#xff0c;及选项的含义是什么以及配置文件的帮助信息时&#xff0c;可以使用 man [命令或配置文件]&#xff0c;这样便可以获得到帮助提示信息了。 语法格式&#xff1a;man [命令或者配置文件] 比如…...

电子科技大学操作系统期末复习笔记(五):文件管理

目录 前言 文件管理&#xff1a;基础 基本概念 文件 文件系统 文件系统的实现模型 文件的组成 文件名 文件分类 文件结构 逻辑结构 物理结构 练习题 文件管理&#xff1a;目录 文件控制块FCB FCB&#xff1a;File Control Block FCB信息 目录 基本概念 目…...

SpringBoot+ActiveMQ-发布订阅模式(生产端)

SpringBootActiveMQ-发布订阅模式&#xff08;生产端&#xff09;Topic 主题* 消息消费者&#xff08;订阅方式&#xff09;消费该消息* 消费生产者将发布到topic中&#xff0c;同时有多个消息消费者&#xff08;订阅&#xff09;消费该消息* 这种方式和点对点方式不同&#xf…...

手写Promise.all

前言 之前在看远方os大佬直播的时候看到有让手写的Promise.all的问题&#xff0c;然后心血来潮自己准备手写一个 开始 首先&#xff0c;我们需要明确原本js提供的Promise.all的特性 Promise.all返回的是一个Promise如果传入的数据中有一个reject即整个all返回的就是reject&…...

蓝桥杯单片机之通过实现同一个按键的短按与长按功能

实现按键的短按与长按的不同功能 问题分析 对于按键短按&#xff0c;通常是松开后实现其功能&#xff0c;而不会出现按下就进行后续的操作&#xff1b;而对于按键长按&#xff0c;则不太一样&#xff0c;按键长按可能分为两种情况&#xff0c;一是长按n秒后实现后续功能&…...

功能安全实战系列09-英飞凌TC3xx LBIST开发详解

本文框架 0. 前言1.What?1.1 基本原理1.1.1 检测范围1.1.2 LBIST与锁步核对比1.1.3 控制寄存器1.2 关联Alarm2. How?2.1 LBIST触发?2.1.1 SSW配置自动触发2.1.2 软件手动触发LBIST2.2 实现策略2.3 测试篇LBIST对启动时间的影响如何确定当前LBIST是否已使能?如何确定当前LBI…...

sqlsugar WhereIF条件的大于等于和等于查出来的坑

一、如下图所示&#xff0c;当我用 .WhereIF(input.Plancontroltype > 0, u > u.Plancontroltype (DnjqPlancontroltype)input.Plancontroltype) 这里面用等于的时候&#xff0c;返回结果一条数据都没有。 上图中生成的SQL如下&#xff1a; SELECT id AS Id ,code AS …...

二.单例模式‌

一.单例模式的定义 单例模式是一种‌创建型设计模式‌&#xff0c;确保一个类‌只有一个实例‌&#xff0c;并提供该实例的‌全局访问点‌。 1.1.核心目标 唯一实例‌&#xff1a;限制类的实例化次数仅一次。‌全局访问‌&#xff1a;提供统一的访问入口&#xff08;通常是静…...

多模型协同:基于 SAM 分割 + YOLO 检测 + ResNet 分类的工业开关状态实时监控方案

一、技术优势与适配性分析 1. 任务分工的合理性 YOLO&#xff08;目标检测&#xff09; 核心价值&#xff1a;快速定位工业开关在图像中的位置&#xff08;边界框&#xff09;&#xff0c;为后续分割和分类提供ROI&#xff08;感兴趣区域&#xff09;。工业场景适配性&#xf…...

鸿蒙图片缓存(一)

移动端开发过程中图片缓存功能是必备&#xff0c;iOS和安卓都有相关工具库&#xff0c;鸿蒙系统组件本身也自带缓存功能&#xff0c;但是遇到复杂得逻辑功能还是需要封装图片缓存工具。 系统组件Image 1. Image的缓存策略 Image模块提供了三级Cache机制&#xff0c;解码后内…...

AI大模型在测试领域应用案例拆解:AI赋能的软件测试效能跃迁的四大核心引擎(顺丰科技)

导语 5月份QECon深圳大会已经结束&#xff0c;继续更新一下案例拆解&#xff0c;本期是来自顺丰科技。 文末附完整版材料获取方式。 首先来看一下这个案例的核心内容&#xff0c;涵盖了测四用例设计、CI/CD辅助、测试执行、监控预警四大方面&#xff0c;也是算大家比较熟悉的…...

Podman 和 Docker

Podman 和 Docker 都是容器化工具&#xff0c;用于创建、运行和管理容器。它们有很多相似之处&#xff0c;但也存在关键区别。下面从多个维度对比它们&#xff0c;并给出适用场景建议。 1. 核心区别 特性DockerPodman守护进程&#xff08;Daemon&#xff09;必须运行 dockerd …...

大语言模型提示词(LLM Prompt)工程系统性学习指南:从理论基础到实战应用的完整体系

文章目录 前言&#xff1a;为什么提示词工程成为AI时代的核心技能一、提示词的本质探源&#xff1a;认知科学与逻辑学的理论基础1.1 认知科学视角下的提示词本质信息处理理论的深层机制图式理论的实际应用认知负荷理论的优化策略 1.2 逻辑学框架下的提示词架构形式逻辑的三段论…...