从零开始的数据结构之旅
MY-DATA-STRUCTURE
实现方案和规划
我会使用 python
、C
以及 Cpp
来实现这些数据结构,加深对数据结构的理解,以及我的编程能力
对于
Cpp
的实现,我尽量参考Eigen
库的代码规范,仅仅使用头文件来实现数据结构、并且学习使用一些特性(模板元编程\表达式模板\内联函数)来进行优化
数据结构一览
数据的结构有两种,一种是逻辑结构,一种是存储结构,它们相互独立。同一种逻辑结构,可以有不同的存储结构,不同的存储结构对算法的实现有不同的影响。在设计算法的时候要考虑两者的关系。
数据的逻辑结构表示数据元素之间的相互关系,由数学中的二元关系定义
- 集合、线性表、树、图
- 主要是面向问题时,思考选用哪种逻辑结构
数据的存储结构表示数据元素在计算机中的存储方式
- 顺序存储、链式存储
- 主要是面向在计算机实现算法时,思考选用哪种存储结构
顺序存储
- 用一段地址连续的存储单元依次存储数据元素
- 逻辑上相邻的元素在物理上也相邻,所以逻辑关系和物理关系一致
链式存储
- 用一组任意的存储单元存储数据元素
- 逻辑上相邻的元素在物理上不一定相邻,所以逻辑关系和物理关系不一致
- 需要用存放元素地址的指针来表示元素之间的逻辑关系
算法设计一览
算法都可通过已实现的基本操作运算的有限次执行来实现,必须在有限步后结束,且每步必须在有限时间内完成
枚举法、分治法、贪心法、动态规划、回溯法、分支定界法
使用循环不变式来证明算法的正确性,寻找一个循环中不变的式子进行判断
- 初始化:循环开始第一次迭代前,循环不变式为真
- 保持:如果循环开始前循环不变式为真,那么每次迭代后循环不变式仍为真
- 终止:循环结束时,循环终止时,这个不变式的性质可以帮助我们证明算法的正确性
- 类似数学归纳法的思想
如何构建循环不变式
- 观察循环目标,比如排序和查找极值
- 找出循环中保持不变的属性,比如取出的部分序列是有序的、最大值位置
- 用数学语言描述这个属性,比如「每次迭代之后,第 个元素是最大值 」
- 验证三要素,初始化、保持、终止
复杂度分析思路
特定算法的执行工作量、计算成本,受问题规模(输入数据规模 )的影响,可以是一种函数关系
于是标记为 ,表示问题规模为 时,算法的执行工作量或者说计算成本
实际上,计算成本 的具体形式可能受算法的实现往往很复杂,是一个复杂的函数,并且我们更关心问题规模 足够大之后, 的增长趋势
所以引入了渐进符号 ,用 来在当 很大时,用 简单表示 ,并且在工程中我们是更关心算法最坏(也就是计算成本最大)的情况,所以这个 通常是 的渐进上界
用数学一点的话说,就是存在一个常数 ,使得
对于 ,最高阶项的系数可以忽略、次高阶项和常数项可以忽略
-->
常数、对数、多项式复杂度的算法,都是可以接受的,但是指数、阶乘复杂度的算法是不可接受的
首先确定基本操作,然后看基本操作因为控制结构执行多少次
- 基本操作,比如普通的赋值、比较和数值运算语句操作数是
- 控制结构,比如循环、递归、分支等,是控制基本操作执行次数的关键
- 单纯的分支结构,复杂度也是
- 如果循环次数和 无关,复杂度也是
有循环先看循环,多层循环先看内层循环
- 结合循环条件看循环的循环次数(使用极限法判断,取两端边界值)
- 看循环内部的操作数是否和循环条件有关
- 如果没关,就操作数直接乘循环次数
- 如果有关就累加 n 个这样的特殊操作数
- 判断级数类型(记忆)
常见循环内部的操作数和循环条件有关的情况(还有一个循环次数和 无关的假情况)
1 | for (i=0; i<n; i++){ |
对一些数据结构进行操作,也会涉及到复杂度的分析,这和它们物理存储的结构相关
顺序结构,连续存储,插入时、移动时要复制已经有的元素,复杂度是
但是访问元素的复杂度是
链式结构,要访问未知位置的元素要用指针遍历,复杂度是
但是插入、删除元素只涉及到指针赋值和交换操作,复杂度是
线性表
语法学习记录
template<typename T>
的作用域
在C++中,template<typename T>
的作用域仅限于紧随其后的类或函数定义。每个模板类或模板函数都需要单独的template<typename T>
声明,因为它们是独立的实体。
如果你在一个文件中定义多个模板类或模板函数,每个模板类或模板函数都需要自己的template<typename T>
声明。它们不能共享一个template<typename T>
声明,因为每个模板类或模板函数的定义需要知道它们将处理的数据类型。
例如,在你的代码中:
1 | template<typename T> |
每个模板类都有自己的template<typename T>
声明。如果你省略其中一个template<typename T>
声明,编译器将无法解析该模板类的定义,并会产生编译错误。
如果template<typename T>
不紧挨着接下来的结构定义,它将不会影响该结构。例如:
1 | template<typename T> |
如果在template<typename T>
和类定义之间插入其他代码,编译器将无法将模板参数与类关联起来,并会产生编译错误。
总之,每个模板类或模板函数都需要自己的template<typename T>
声明,并且该声明必须紧挨着类或函数定义。这样编译器才能正确解析模板参数并生成相应的代码。