第一章 数据结构
第1节:什么是数据结构?什么是算法?
- 程序(Program)= 数据结构(Data Structure) + 算法(Algorithm)
- 数据结构:是一些待处理的,要存到计算机里的,"数据"及之间的关系/组织形式。包含数据的逻辑结构 + 存储结构 + 运算/操作。
- 算法:用于处理数据(解决某问题)的 一段指令集合
- 数据结构 + 算法 → 编程效率 (决定了)
第2节:数据结构和算法用于解决哪些问题?
路由转发问题,需要存储各种数据的问题,各种赛制pk的争冠问题等。。
例如:
第3节:数据结构和算法 在软件开发中 占哪一步?
一个"系统"的一生 : 需求分析 ->【设计】-> 实现 -> 测试 -> 部署运行 -> 维护
第4节:数据结构的基本概念:
先搞清楚什么是数据项,数据元素,数据?
- 数据元素:是"数据"的基本单位,在计算机程序中通常【作为一个整体】进行处理。也被称为元素、顶点、结点、记录
- 数据项:构成数据元素的不可分割的最小单位(一个项一个项的)
- 数据项 < 数据元素 < 数据对象 < 数据
例如:
第5节:数据结构的类型:
1. 逻辑结构(及数据元素之间的关系):
- 线性结构:一对一(线性表、链表、栈、队列、数组等)【例如:电话号码簿】
- 树结构:一对多(二叉树、多叉树、红黑树等)
- 图结构:多对多(图、网等)
- 集合结构:无特定关系(散列/哈希表)
2. 存储结构(物理):
- 顺序存储结构:在硬盘里连着顺序,一个挨着一个的存放
- 链式存储结构:在硬盘里有规则的乱放
- 还有索引存储、散列存储(有些地方会有)
3. 逻辑结构和存储结构的关系:
【每一种逻辑结构】都可以 使用【不同的存储结构】来实现
4. 区分逻辑结构与存储结构:
方法:给出一个结构,若能用多种(n>1) 种不同的方式存储,那就是逻辑结构;反之就是存储(物理)结构,已经采用了一种具体的存储方法。
例如:
- 线索二叉树: (这种加上了线索的二叉链表称为线索链表,相应的二叉树称为线索二叉树) 二叉树是一种逻辑结构,但线索二叉树是加上线索后的【链表结构】,因此是存储结构。
- 静态链表:是用【一片连续的空间(数组,顺序)】来存储实现的,故静态链表是存储结构(称为用数组实现的链式存储结构)。
- 散列表:用的是【散列(哈希)】存储,即根据元素的关键字直接计算出该元素的存储地址,故是存储结构。
- 循环队列:用【数组】求余实现,故是存储结构。
- 顺序表:用【数组】实现,故是存储结构。
- 有向图:可以用【邻接表】或【邻接矩阵】两种实现,故有向图是逻辑结构。
- 二叉树:可以用【数组】或【指针(链式)】实现,故二叉树是逻辑结构。
第6节:抽象数据类型ADT (Abstract Data Type):
1. 含义:
通俗来说就是一个各种函数的模版,这些模版把每个操作都分成了不同的模块,有存放了各种数据类型的(数据对象),数据对象的各个元素之间的关系(数据关系),以及一些对数据对象的基本操作(基本操作)
2. 特点:
- 封装性:把数据和操作都封装在一起了。
- 抽象性:只关注逻辑操作,不涉及实现的细节。
- 独立性:可移植到不同环境,和具体实现的高级语言无关。
3. 例如:
栈(Stack):
- 数据对象:栈中的元素集合。
- 数据关系:栈中的元素遵循 后进先出(Last In First Out,LIFO)的原则。
- 基本操作:
push
:将一个元素压入栈顶。pop
:弹出栈顶元素。top
:获取栈顶元素但不弹出。isEmpty
:判断栈是否为空。
第7节:存储密度:
- 含义:一个结点数据本身所占的存储量和整个结点结构所占的存储量之比。
- 存储密度 = (一个结点数据本身所占的存储量)/(一个结点结构所占的存储总量)
- 例如:一个结点,假设有数据区域data占8B,也会有指针区域next占2B,那么这个结点的存储密度=8/(8+2)
第一章 算法
第1节:算法的特性:
- 有穷性/有限性:一段算法得有限行解决吧。我就解决个1+1=2,你非要用量子力学,微积1分,循环无限的去处理。
- 确定性:写的每行代码得有自己的功能吧。别给我整一堆没用的代码。
- 可行性:这段代码总不能有一堆错误吧。比如说条件失效的无限循环,你想干嘛,你是猴子请来的救兵吗?
- 输入,输出:写代码是给我解决问题的,比如说数学里的函数,我得有题目的1+1输入,还得算结果=2输出。
【注意】:算法与【具体的"程序设计语言"(c,py,java)无关】,算法的具体实现就是程序
第2节:如何描述一个算法?
- 自然语言:用人话描述出来(描述的不够准确和严格)
- 流程图:画出来这个逻辑过程(辅助描述算法的方法)
- 伪代码:用具体计算机语言描述出来(辅助描述算法的方法)
- 程序设计语言:针对具体语言的实现(c,python,java)(称为算法语言)***
1.自然语言
2.流程图
3.伪代码
第3节:如何评价一个算法?
- 高效性 (时间复杂度 + 空间复杂度)
- 正确性
- 可读性
- 简单性
- 健壮性
- 抽象性
- 可维护性
- 兼容性
第4节:如何衡量一个算法的效率?
- 时间复杂度(时间效率): 运行一段最深层次的代码,需要多少时间
- 空间复杂度(空间效率):运行一段代码,最多需要浪费多少存储空间
第5节:时间复杂度涉及的定义和计算方法:
- 问题规模:这个算法涉及到的 最大的数据数量
表示方式:n
- 频度:【每一条】语句 所运行的 次数
表示方式:与n有关 = 1 , 2, .. , n, n+1, n*(n+3)/2, n*(n+1)/2
- 时间耗费:【所有】 语句执行次数的 总和(求和)
表示方式:T(n) = 1+2+..+n = n*(n+1)/2
例如:
- 时间复杂度:【时间耗费的趋势】
∙ 表示方式:O(n)
∙ 计算方法:limn→∞T(n)=O( n的最大项 )
【计算频度太麻烦了,忽略无穷小项,保留最大项(最耗时的),也就是代码最深的】
- 例如:T(n) = n2+3n+1 => O(n2)
- 例如:T(n) = n2+nlogn+1 => O(n2)
- 例如:T(n) = n3+n2logn+1 => O(n3)
- 例如:T(n) = nlogn+2n+1 => O(nlogn)
- 例如:
- 常见时间复杂度的量级
- 常数阶 O(1) :通常与n没有关系(代码中没有给n,或者与n关联不大)
- 对数阶 O(logn)
- 线性阶 O(n)
- 线性对数阶 O(nlogn)
- 平方阶 O(n²)
- 指数阶 O(2^n) 【问题规模n较大时,不可计算】***
- 时间复杂度趋势比较:
第6节:空间复杂度涉及的定义:
- 空间复杂度:【辅助空间的数量】
∙ 表示方式:O(n)
∙ 计算方法:O( 使用辅助空间最大的n项 )【与时间复杂度类似】
问题:数据结构与算法的经典问题:
- 地图染色问题:地图上相邻的国家使用不同的颜色标注,则最少使用多少种颜色就能全表示出来?
答:4种颜色
2. NP问题(Non-deterministic Polynomial time problem):属于什么问题?
答:非确定性,多项式时间问题
- 多项式时间:在问题规模n前提下,能够用各种公式表示出来的时间:比如 n2 ,n3 , 有限次方n有限次方 ,这些既然能表示出来,就都能算出来。
- 非确定性:这个时间,表示不出来,基本算不出来,不确定性因素太多了,只能猜出来是什么结果。
- NP问题:在多项式时间内验证这个猜测的结果是否正确。
- NP问题经典案例:
- 1.旅行商问题(TSP):通俗理解就是我从北京出发,要去上海,成都,广州,回北京,该怎么走这个路线最划算
- 2.背包问题(KP):通俗理解就是我要出门,我的背包容量有限,但我要带的东西很多,怎么装才能尽可能的合理装下我最想要的东西。
3. 计算时间复杂度:
i=1;
while(i<=n) i*=2; //时间复杂度为O(log2n)
k=100,i=10;
do {
if(i<n)
break;
i++
} while(i<k); //时间复杂度为 O(1) 或者是 O(k-i)
- 斐波那契数列(Fibonacci sequence)的时间复杂度:
黄金分割
数列:1、1、2、3、5、8、13、21、34……(忽略0项)
通俗理解:我的前两项和,就是当前的值
推导公示:F(0)=F(1)=1, F(n)=F(n - 1)+F(n - 2)(n≥ 2,n∈ N*)
代码实现:
#include <iostream>
int Fibonacci(int n) { // 时间复杂度为:O(1.6^n) / O(2^n) 指数级别了
if (n <= 1) return 1;
return Fibonacci(n - 1) + Fibonacci(n - 2); // n^2=n^1+n^0 求n
}
int Fibonacci(int n) { // 时间复杂度为:O(n)
if (n <= 1) return 1;
int a = b = 1, c;
for (int i = 2; i <= n; i++) { // b=a+b; a=b
c = a + b;
a = b;
b = c;
}
return c;
}
int main() {
int n;
std::cout << "输入要计算的斐波那契数列的项数:";
std::cin >> n;
std::cout << "斐波那契数列的第 " << n << " 项是:" << Fibonacci(n) << std::endl;
return 0;
}
第一章 STL与数据结构
第1节:STL是什么?
STL(Standard Template Library):标准模板类
- 拥有各种常用的,用于存储数据的,模板类+相应的操作函数(为开发人员提供了一种通用的、高效的算法和数据结构)
- 是一个集合{}
第2节:STL的组成部分?
- 容器(Containers):一个存放数据的“盒子”,这个盒子的容量大小可以灵活变化,是一种特定类型对象的集合,比如一种类、数据结构、或者抽象数据类型
- 算法(Algorithms):提供了各种通用的算法。排序算法(如 sort)、查找算法(如 find)、遍历算法等。
- 迭代器(Iterators):连接“容器”和“算法”的桥梁。类似于指针(指向某个对象),用来遍历容器类中的对象。有不同类型的迭代器,如输入迭代器、输出迭代器、前向迭代器、双向迭代器和随机访问迭代器等,每种迭代器支持不同的操作。
- 仿函数/函数对象(Functors):类似函数一样,可以被调用的对象。可以自定义仿函数来实现特定的操作,例如自定义比较函数(用于排序算法)。
- 适配器(Adapters):除了容器适配器,还有函数适配器和迭代器适配器。函数适配器可以修改现有的函数对象的行为,迭代器适配器可以修改迭代器的行为或创建新的迭代器类型。
- 空间配置器(Allocators):负责管理内存的分配和释放。
∙ 【注意:后面四个都是为“容器”和“算法”服务的】
第3节:容器(标准容器)都包含什么?
1. 顺序容器(Sequential Containers)|| 序列式容器
- 数组容器(array):静态数组,表示可以存储N个,T类型的元素,数组长度固定。
- 向量(vector):动态数组,可快速在尾部插入删除,随机访问高效。
- 列表(list):双向链表,任意位置插入删除高效,随机访问慢。
- ***双端队列(deque):双端队列,两端插入删除高效。
2. 排序容器(Sorted Containers)|| 关联式容器(Associative Containers)
- 集合(set):存储唯一元素,自动升序排列。(abc在字典顺序排序,只有一个索引)
- 多重集合(multiset):允许存储重复元素,升序排列。(通讯录里有好几个都备注了“姐”)
- 映射(map):存储键值对,键唯一,自动按键排序。(一个身份证号只对应一个人)
- 多重映射(multimap):键可重复的映射。(一个单词可能对应不同的意思)
3. 哈希容器(Hash Containers)|| 容器适配器(Container Adapters)|| 无序关联式容器
- 栈(stack):后进先出(LIFO)的数据结构。
- ***队列(queue):先进先出(FIFO)的数据结构。
- 优先队列(priority_queue):按优先级排序的队列。
【 有时候 排序容器 + 哈希容器 统称为 关联容器 】
第4节:STL与数据结构的关系
- STL 来源于数据结构(需要了解数据结构的知识)
- STL提供简单的储存数据的模版类
- 复杂的数据处理,需要自己设计
- 算法可认为是 STL 的精髓,所有算法都是采用 函数模板 的形式提供的。
练习:将整数设计为一个类,将整数相关的常见函数运算设计为类的接口并进行实现,如求定值的最大公约数,最小公倍数,因式分解等。
整数类
class Integer {
private:
int value;
public:
Integer(int val) : value(val) {}
// 求最大公约数
int gcd(int a, int b) {
if (b == 0) return a;
return gcd(b, a % b); //只要能出的
}
int findGCD() {
return gcd(value, value - 1);
}
// 求最小公倍数
int lcm(int a, int b) {
return a * b / gcd(a, b);
}
int findLCM() {
return lcm(value, value + 1);
}
// 因式分解
void factorize() {
int n = value;
for (int i = 2; i * i <= n; i++) { //从2开始整除,3,4.....
while (n % i == 0) { //说明n还能被2(3,4....)整除
std::cout << i << " ";
n /= i;
}
}
if (n > 1) {
std::cout << n;
}
std::cout << std::endl;
}
};
主函数
int main() {
Integer num(24);
std::cout << "最大公约数:" << num.findGCD() << std::endl;
std::cout << "最小公倍数:" << num.findLCM() << std::endl;
std::cout << "因式分解:";
num.factorize();
return 0;
}
第二章 线性表(Linear List)
第1节:学了线性表,在生活中能解决什么问题?解决目标是什么?
- 要解决的问题:
- 通讯录
- 图书管理系统(图书管理、借阅管理)
- 电商(商品管理、订单管理、客户信息管理)
- 选课系统(课程管理、选课管理、成绩管理)
- 学籍管理系统等
2. 目标:
设计并实现一个通用类class,完成上述各种系统的增删改查,其中这个类包含以下要素:
- 数据类型T(任意) 【template】
- 数据长度L(任意) 【N】
- 通用操作(运算/算法):插入、删除、查找、输出 【new delete】
第2节:认识线性表(什么是线性表)?
1. 逻辑结构(定义):由n(n≥0)个相同类型的,数据元素(结点)组成的,有限序列
2. 数学表示方式:
- L = (a1, a2, …, an)
- a[0],a[1],a[2]…,a[n-1]
3. 逻辑表示方式:具有前驱和后继的逻辑关系
4. 操作/运算:包括插入(Insert)、删除(Delete)、求长度(Length)、按位置查找(Get)、按值查找(Locate)、打印线性表(Print)等。
5. 存储结构:
- 顺序存储结构:用一段地址连续的存储单元,依次存储数据元素(结点)
- 链式存储结构:
- 单链表:物理上不连续,依赖后继指针定位下一个元素的位置。用结构类型描述结点,包含数据域和指针域。实现包括构造函数(空链表、头插法、尾插法)、析构函数、按值查找(locate)、按位查找(Get)、插入(Insert)、删除(Delete)等操作。【带头结点first的单链表】解决了空链表和非空链表的不同处理,以及首结点插入、删除处理特殊的问题。
- 循环链表:尾结点指针指向头结点,【可同时快速找到头尾结点】。
- 双链表:每个结点有两个指针域,指向前驱和后继,【可同时搜索前驱和后继结点】。
- 静态链表:使用数组元素的下标来模拟单链表的指针,【可在无指针的语言中可实现链表】。
第3节:顺序存储结构
- 含义:用一段地址连续的存储单元,依次存储数据元素(结点)
- 实现:
- 在 C++ 中,用【数组】存储顺序表,。
- 线性表中的元素类型不同,需要使用【模板机制】
第4节:链式存储结构
1. 单链表
- 带头指针(head)的单链表
- 【带头结点(first)】的单链表:解决了空链表和非空链表的不同处理,以及首结点插入、删除操作的问题。从而最终【方便运算的实现】***。
2. 循环链表:【同时快速找到头尾结点】
这个是错误的【 × 】:
这个是正确的【 √ 】:
3. 双链表:【同时搜索前驱和后继结点】
- 空链表(只有一个头结点):
- 非空双链表:
4. 静态链表:【无指针也能实现链表】
第二章 第5节:(顺序表)线性表的实现
1. 顺序表的初始化:
- Length:有效长度
- N: 最大存储空间
2. 顺序表的插入:
- 插入过程
- 代码实现:
- 时间耗费:O(n)
3. 顺序表的删除:
- 删除过程
- 代码实现:
- 时间耗费:O(n)
4. 顺序表的查找:
(1)按【位置i】查找【值x】
(2)按【值x】查找【位置i】
5. 顺序表的应用:
顺序表是一个通用容器
第二章 第6节:(单链表)线性表的实现
1. 存储空间分配的特点:
- 物理上不连续
- 依赖后继指针【next】,来定位下一个元素的位置
2. 单链表的结点构成:
【理解指针和结点】:
(1)将p指向的Node类型的结点为空:Node* p=NULL;
- 这里声明了一个名为
p
的指针变量,其类型为Node*
,表示它可以存储一个Node类型的对象(结点)地址,并指向一个Node
类型的对象(结点)。 Node
可能是一个自定义的数据结构struct或者类class的类型。通过这个指针p,可以访问和操作Node
类型的对象(结点)。- 在后续的代码中,可以使用这个指针,来指向动态分配的
Node
对象(结点),或者指向已存在的Node
对象(结点),从而进行各种操作,如遍历链表、访问对象成员(结点数据)等。
(2)为p结点开辟一块内存:p = new Node<T>; (前提p指针已经被创建了)
- 动态分配内存:
new Node<T>
表示在内存上(动态)分配一个Node
类型对象(结点)所需的内存空间。 - 让指针指向新分配的对象:把新分配的
Node
对象(结点)的地址赋值给指针p
。通过指针p
就可以访问和操作这个新创建的Node
对象(结点)。 new Node
分配的内存,不会在函数调用结束等情况下自动释放,需要程序员手动分配和释放内存delete p;
。
(3)将p指向的Node类型的结点,为其开辟一块内存:Node<T>* p= new Node<T>
Node<T>* p
声明了一个名为p
的指针变量,它指向一个模板类型为T
的Node
类型对象(结点)。这意味着可以根据不同的类型参数T
来实例化这个指针所指向的对象,从而实现泛型编程,使代码可以适用于不同的数据类型。new Node<T>
在内存上(动态)分配一个模板类型为T
的Node
对象(结点)所需的内存空间。使用完动态分配的内存后,应该使用delete p;
来释放分配的内存,以避免内存泄漏。
(4)p,q指针指向同一地址的结点:Node<T>* q = p; (前提p指针已经被创建了)
Node<T>* q
声明了一个名为q
的指针变量,其类型为指向模板类型为T
的Node
对象(结点)的指针。q = p
将指针p
的值(即它所指向的内存地址)赋给指针q
。现在,q
和p
都指向同一个Node<T>
对象(结点)。- 如果对
p
或q
所指向的对象(结点)进行修改,会影响到另一个指针所指向的对象(结点),因为它们指向同一个内存位置。 - 注意及时释放动态分配的内存
(5)为结点p赋值:【通过“->”去调用指针指向的结点数据data,next】
p->data=‘我是数据x’;
p->next = NULL; (最后是空了,是个尾结点) || p->next = q; (其他结点的地址)
(6)释放结点p:delete p;
3. 单链表的初始化:
4. 单链表的构造:
5. 单链表的插入:
(1)头插法:
- 插入过程:a3->a2->a1【从前面插入】【都是一开始的头位置插入】
- 代码实现:
(2)尾插法:
- 插入过程:a1->a2->a3【从后面插入】【一直往下走到屁股后面插入】
- 代码实现:
(3)普通插入:
- 插入过程:
- 代码实现:
(4)三种插入位置:
6. 单链表的删除:
(1)析构函数(自动销毁所有结点,释放内存):
- 析构过程:
- 代码实现:
函数名与类名相同,在前面加上波浪线(~)。例如,如果类名为MyClass
,那么析构函数的名称就是~MyClass
。
通常是自动调用的,但也可以显式调用 (不建议自行调用)
(2)普通删除:
- 删除过程:
- 代码实现:
(3)三种删除位置:
7. 单链表的查找:
(1)按【位置i】查找:
- 查找过程
- 代码实现:
(2)按【值x】查找
- 查找过程 & 代码实现
8. 求单链表的长度:
- 代码实现:
第二章 第7节:(循环链表)线性表的实现
- 尾结点指针指向头结点
- 实现方法同单链表
第二章 第8节:(双向链表)线性表的实现
1. 双向链表的结点构成:
- 每一个结点有两个指针域,指向前驱和后继
5. 单链表的插入:
2. 双向链表的插入:【后前前后】
【3,4步注意:可以灵活转换位置,但要注意指针的指向】
3. 双向链表的删除:
第二章 第9节:(静态链表)线性表的实现
- 无指针实现链表
- 使用数组元素的下标来模拟单链表的指针
1. 静态链表的插入:
2. 静态链表的删除:
第二章 总结:
- 链表:插入、删除不需要移动大量元素
- 链表:动态分配内存
- 链表:顺序查找,不能直接定位
- 链表:头插法和尾插法的结果正好相反(可以反转)
- 顺序表适用于频繁查找的场景;链表适用于频繁插入和删除的场景。
- 表长为n的顺序表,插入/删除任何一个元素等概率,插入一个元素平均移动个数【n/2】;删除一个元素平均移动个数【n-1/2】
- 静态链表是链式结构,但属于静态存储结构
- 长度为n的顺序表,等概率条件下,查找成功时,按序号查找的平均查找次数为【1】;按值查找的平均查找次数为【n+1/2】
- 顺序表是一种采用随机存取方式(结构)的顺序存储结构【keyword:存取or存储 】;链表是一种采用顺序存取方式的链式存储结构
第二章 练习:
1. 已知数组A[n]中元素为整型,设计算法将其调整为左右两部分,左边所有元素为偶数,右边所有元素为奇数。
- 基本思想:将左边的奇数调整到右边,右边的偶数调整到左边,关键在于调整的新位置如何确定。最理想的情况:正好将左边的奇数和右边的偶数交换一下。
- 代码实现:
2. 以单链表为存储结构,写出就地逆置的算法
- 基本思想:
- 孤立出来头结点
- 按照头插法的原则建立链表
- 代码实现:
3. (约瑟夫问题)设有编号为【1、2、3、…、n】的n个人围成一个圈。从【第一个人】开始报数,报到m的人出圈,再从出圈的人的下一个人起重新报数,报到m的人出圈,如此下去,直到所有人全部出圈为止。给定任意的总人数n和m项,设计算法求n个人出圈的次序。
- 基本思想:
- 逻辑结构:线性表
- 存储结构:数组/链表
- 代码实现:
第三章 线性表的扩展(栈、队列、串、多维数组)
1. 普通的线性表
- 可以存储任意类型的数据
- 可以在任意位置进行插入、删除操作。
2. 扩展线性表
- 栈: 后进先出(LIFO)线性表
- 队列:先进先出(FIFO)线性表
- 串: 字符为结点的线性表
- 多维数组:类型相同的元素构成的集合
第三章 栈
1. 栈的逻辑结构:
- 只能在表的一端进行插入和删除的线性表
- (有数据的情况下)栈顶为 ( top ),栈底 ( bottom )
- 存取原则:后进先出 (LIFO)
- 主要操作:
- 入栈push
- 出栈pop
2. 栈的实现:
2.1 【顺序存储结构】栈
1. 栈的初始化:
- 栈底:top为0的一端【空栈时 top = -1 】
- 栈顶:top指针所指 【满栈时 top = -1 + STACKSIZE 】
2. 栈的插入【入栈】:
- 当栈满了的时候还要插入,考虑上溢:top == STACKSIZE - 1
3. 栈的删除【出栈】:
- 当栈空了到底了的时候还要删除,考虑下溢:top == - 1
4. 共享栈的实现:
- 定义:多个栈(2个)共享同一数组空间
4.1 共享栈的初始化:
4.2 共享栈的插入:
4.3 共享栈的删除:
2.2 【链式存储结构】栈
1. 栈的初始化:
- 栈底:第一个插入的结点
- 栈顶:【单链表的头部】最容易操作,栈顶设在头部(因为头部有top指针,方便操作,不需要头结点)
2. 栈的插入【入栈】:
3. 栈的删除【出栈】:
- 当链空了没有结点的时候还要删除,考虑下溢:top == NULL
3. 顺序栈和链栈的比较:
- 时间耗费:O(1)
- 空间耗费:
- 顺序栈:预分配(STACKSIZE),有额外的空间浪费
- 链栈:指针域(top),有额外开销
4.测试:设计算法,把十进制数转换为二进制输出。比如:77 = 1001101
第三章 队列
1. 队列的逻辑结构
- 特征:在表的一端进行插入,而在表的另一端进行删除。
- 队头——允许删除的一端。
- 队尾——允许插入的一端。
- 存取原则:先进先出(FIFO)
2. 队列的实现
2.1 【顺序存储结构】循环队列
1. 循环队列的初始化:
- 队尾 rear :尾指针,用来插入【入队】
- 队头front:【队头的前一个位置】,用来删除【出队】
2. 判断循环队列的空和满:
- 第一种情况:f 指向队头的前一个位置【推荐】
(1) 空:f == r
(2) 满:f == (r+1) % MAXSIZE
- 第二种情况:f 指向队头
(1) 空:冲突了
(2) 满:f == (r+1) % MAXSIZE
3. 循环队列的插入【入队】:
- 【队尾】指针移动:r = (r+1) % Queuesize;
4. 循环队列的删除【出队】:
- 【队头】指针移动:f =(f+1)% Queuesize;
5. 循环队列的长度:
2.2 【链式存储结构】队列
1. 队列的初始化:
- 队尾 rear :尾指针,用来插入【入队】
- 队头front:头结点(不是第一个元素),用来删除【出队】
2. 队列的插入【入队】:
3. 队列的删除【出队】:
- 特殊:当链空了没有结点的时候,rear随之删除了,需要还原:front->next == NULL
- 队列的析构函数【销毁整个队列的函数~】:
4.测试:一般银行排队机、路由器数据转发队列等都是优先级队列,即同时有多个队列,每个队列优先级不同,请根据自己所学设计一个优先级队列的结构。
其中3是优先级最高的任务,排在队列头,先进先出,优先处理
第三章 串
1. 串的逻辑结构
- 串是字符串string,是由【零个 || 多个字符】组成的有限序列,每一个结点都是一个字符
- 记作:S=“ a1a2 …… an ”
- 空格串和空串的区别:
- 空格串:S=" " (长度为1)【有个空格】
- 空串: S="" (长度为0)【啥也没有】
4. 子串:一个串中,任意连续的字符组成的子序列。例如:B是A的子串,B在A中的序号为9
- 主串A=“This is a string”;
- A的子串B =“a”;
2. 串的实现:
2.1 【顺序存储结构】串
1. 常用三种串的结构:
2. 串的基本操作:
【*s】指的是 *s这个指针指到了当前元素的位置了,可以直接调用s串的内容,类似调用了s位置的数组的值
【s】 指的是 s串的当前位置,类似下标
使用了const 就是说:这个s串的内容不能被修改,我只提供我的数据,你只能参考
- 复制 StrAssigh ()
- 求串长 StrLength()
- 比较 StrCmp ()
- 求子串在主串的位置StrIndex ()
【模式匹配技术】
- 存储(物理)结构:顺序结构
- 模式匹配:在主串S中,寻找子串T的过程。如果匹配成功,返回T在S中的位置;如果匹配失败,返回0
- 模式串:又称为子串T
【朴素的模式匹配算法】
什么是回溯:这条路走不通了,只能从原来的位置的下一步走了(或者其他方向)
每次回溯的位置:
- 主串S:i - j +1
【注意下标从“0”开始,而 i-j 就是为了计算出,从本轮开始已经从主串S中对比过的元素的个数,+1 是为了从这些对比过的元素的下一个开始新的一轮对比】
- 模式串T:0
朴素的模式匹配过程:
代码实现:
【KMP算法】(Knuth - Morris - Pratt)
解决问题:解决“朴素的模式匹配算法”每次回溯都要从下一个位置开始,提高了匹配效率【即不回溯算法】
关键概念:
- 部分匹配表(PMT - Partial Match Table)
- 对于模式字符串,部分匹配表记录了字符串的前缀(从第一个开始到最后一个前)和后缀(从最后一个开始到第一个后)的最长公共元素长度(即相同的字符串最长的)。例如,对于模式串 “ABABC”,其部分匹配表如下:
- 字符串:“A”,前缀为空;后缀为空,最长公共元素长度为 0。
- 字符串:“AB”,前缀为 “A”;后缀为 “B”,最长公共元素长度为 0。
- 字符串:“ABA”,前缀为 “A”、“AB”;后缀为 “A”、“BA”,最长公共元素长度为 1。
- 字符串:“ABAB”,前缀为 “A”、“AB”、“ABA”;后缀为 “B”、“AB”、“BAB”,最长公共元素长度为 2。
- 字符串:“ABABC”,前缀为 “A”、“AB”、“ABA”、“ABAB”;后缀为 “C”、“BC”、“ABC”、“BABC”,最长公共元素长度为 0。
- 对于模式字符串,部分匹配表记录了字符串的前缀(从第一个开始到最后一个前)和后缀(从最后一个开始到第一个后)的最长公共元素长度(即相同的字符串最长的)。例如,对于模式串 “ABABC”,其部分匹配表如下:
- 失配函数(Next 数组)
- 在实际应用中,通常会对“部分匹配表PMT”进行一定的变形得到 Next 数组(失配函数)。Next 数组的第i个元素表示当模式串中第i个字符失配时,模式串应该回溯到的位置。一般来说,Next 数组的计算方式为:next[0]=-1 , next[1]=0 , 对于i>1,next[i]=j,其中j是满足模式串T从0到 j-1 个字符与从 i-j 到 i-1 个字符匹配的最大j值。【主串的 i 指针不回溯】
算法过程:
如何求next数组:
- 令 next[0] = -1,next[1] = 0,
- 假设i:2 ~ len-1
- 令 j= next[i-1] , 即P[ 0, ... , j ] == P[ i-j-1 ,..., i-1 ] ,则:
- 当 j !=-1 且 Pj != Pi−1 【 P[j] != P[i-1] 】,则 j = next[j]
- 当Pj ==Pi−1【 P[j] == P[i-1] 】 ,则 next[i] = j+1 ,否则next[i]=0
int next[1000];
void getNext(char* t,int len)//这里next是从0开始的,与字符串坐标一一对应,表示该0~下标处的最大长度。-1表示没有,0表示1,1表示2,以此类推!
{
next[0] = -1;
int i = 1, j = next[0]; //i为当前值的指针,j为上一个能够匹配的值的指针
while (i <= len - 1) {
if (j == -1 || T[i] == T[j]) { //找到头了,或者说,两个相等了
i++; //当前位置向下走
j++; //上一个能够匹配的位置向下走
next[i] = j;
} else {
j = next[j]; //一直向前回溯寻找
}
}
}
int kmp(char *T, char* S){ //T子串,S总串
int len_T = strLength(T);
int len_S = strLength(S);
int i = 0,j = 0;
while(i<len_T && j<len_S)
{
if(i == -1 || T[i] == S[j]) {i++; j++;}
else i = next[i];
}
if(i == len_T) return i-len_T;
else return -1;
}
i | 0 | 1 | 2 | 3 | 4 |
---|---|---|---|---|---|
s | a | b | a | b | c |
next[i] | -1 | 0 | 1 | 2 | 0 |
例如:
s | a | a | b | a | c | a | a |
---|---|---|---|---|---|---|---|
i | 0 | 1 | 2 | 3 | 4 | 5 | 6 |
next[i] | -1 | 0 | 0 | 1 | 0 | 1 | 2 |
2.2 【链式存储结构】串
1. 常用两种串的结构:
第三章 多维数组
1. 多维数组的逻辑结构
- 由类型相同的数据元素构成的有序集合,每一个元素受n个线性关系的约束,每个元素有一个序号 i1 、 i2 … in ,称为该元素的下标,并称该数组为n维数组。
- 主要操作:存取 and 修改
- 两种下标的数组表示:
2. 多维数组的物理结构
- 本质操作:寻址
- 存储(物理)结构:顺序存储结构
- 顺序存储结构的问题:
- 内存是一维的结构
- 多维数组是多维的结构
- 解决方法:多维数组->一维数组【映射】
- 行优先存储【C++】
- 列优先存储(FORTRAN)
3. 多维数组的实现:
3.1 二维数组(m行n列)
- 注意起始下标:从 a[0][0] 开始 , 还是从 a[1][1] 开始 。默认:a[0][0]
- 行:0~m-1 【一共有m行】
- 列:0~n-1【一共有n列】
- 某数组项 a[i][j] (按照行优先)的存储地址为:Loc(a[i][j])= Loc(a[0][0])+ [i*n+ j] * d
- 【已知a[0][0]的起始物理地址】【每个元素占用d字节的空间】
3.2 三维数组(m张n行p列)
- 注意起始下标:从 a[0][0][0] 开始 , 还是从 a[1][1][1] 开始 。默认:a[0][0][0]
- 张:0~m-1【一共有m张】
- 行:0~n-1 【一共有m行】
- 列:0~p-1 【一共有n列】
- 某数组项 a[i][j][k] (按照行优先)的存储地址为:Loc(a[i][j][k])= Loc(a[0][0][0])+[i*n*p + j*p + k]*d
- 【已知a[0][0][0]的起始物理地址】【每个元素占用d字节的空间】
3.3 n维数组(k1维,k2维,...,kn维)
- 注意起始下标:从 a[0][0]...[0] 开始 , 还是从 a[1][1]...[1] 开始 。默认:a[0][0]...[0]
- 行:0~i1-1 【一共有m行】
- 列:0~i2-1 【一共有n列】
- ...
- 某数组项 a[i1][i2]...[in] (按照行优先)的存储地址为:Loc(ak1k2k3…kn) = Loc(a00…0)+ [ i1*(k2*k3…*kn) + i2*(k3*k4…*kn)+… in-2*(kn-1*kn) +in-1*kn + in ] * d
- 【已知a[0][0]...[0]的起始物理地址】【每个元素占用d字节的空间】
4. 特殊的多维数组:
4.1 稀疏矩阵【三元组】:
- 定义:非零元素的个数 << 矩阵中元素的总数(偌大的矩阵,只有寥寥几个 ≠0 的元素)(<< 远小于的意思)
- 产生问题:浪费大量的空闲空间,占内存。
- 解决方案:压缩策略——【仅存储非零元素】 零元素不存储。
- 存储方式:顺序存储【数组】
- 如何找到定位到一个准确元素:【采用三元组方式】【按行优先】【下标从0开始】:
- 行号 row : 0~m-1
- 列号 col : 0~n-1
- 值 item/value : 具体是什么数
- data[t] : 存储行号,列号和值
- m:矩阵一共有m行
- n:矩阵一共有n列
- t:矩阵一共有t个非零元素
- 代码描述三元组顺序表:
- 三元组的转置:
关键步骤:转置,相乘
- 转置定义:对一个m*n的矩阵M,经过转置后为一个n*m的矩阵T,且 T(i,j) = M(j,i),1≤ i≤ n,1≤ j≤ m
- 如何将【原矩阵M】转置得到【转置矩阵T】:
1.【普通】方法:
代码实现:
- 时间复杂度:O(n*t) 【原因:在A中依次寻找第0列、第1列……,第n-1列,每次都需要反复扫描一遍A表】
- 空间复杂度:O(1)
2.【优化】方法:
- 扫描一遍A表,直接定位到A中的元素在B中的位置
- 如果知道:A中每一列的元素的第一个位置,知道这一列有几个元素,就可以很好的解决每次都要遍历一次的时间耗费,但需要额外声明两个数组cnum[]和cpot[] :
- cpot[]:A中每一列的第一个元素的位置【初始位置(第一个元素)设置为0:cpot[0]=0】
- num[]:这一列共有几个元素
例如5行6列矩阵A:
- 第1列(下标为0):起始位置为0,共有2个数
- 第2列(下标为1):起始位置为0+2,共有3个数
- 【通过前一列的初始位置+前一列的个数就能算出来第二列的初始位置了,以此类推】
- 第3列(下标为2):起始位置为2+3,共有1个数
- 第4列(下标为3):起始位置为5+1,共有0个数
- 第5列(下标为4):起始位置为6+0,共有2个数
- 第6列(下标为5):起始位置为6+2,共有1个数
代码实现:
- 时间复杂度:O(n+t) 【n是列项,t是非零元素的个数】
- 空间复杂度:O(n)
4.2 十字链表
- 定义:表示稀疏矩阵(三元组)的另一种方式,用链式的结点存储一个元素
- 存储方式:顺序结构【数组】+链式结构【链表】
- 如何找到定位到一个准确元素:
- 行号 row : 该非零元素所在的行号[0~m-1]
- 列号 col : 该非零元素所在的列号[0~n-1]
- 值 item/value : 具体是什么数
- down:指向【同一列】的下一个非零元素的节点 ↓
- right:指向【同一行】的下一个非零元素的节点→
- 十字链表的结构(有数据的表):
头结点:例如:5行6列内容为空——表明了这个矩阵的5行,6列情况【但是要注意每一行,列的起始下标,是从0开始的】。而它的down列项和right行项,分别看作一组只是指向行,列头结点的循环链表。down列项:就先不要关注right行的,单独看down列(串起来每一行的头结点);right行项原理一样。例如:
- 【行】的头结点的循环列表:第一行row(下标0)的元素有3个(这个3存储在列col项中)。
- 只关注行项right指向的第一个数节点,而列项down是行头节点的循环链表
- 【列】的头结点的循环列表:第一列col(下标0)的元素有2个(这个3存储在行row项中)
- 只关注列项down指向的第一个数节点,而行项right是行头节点的循环链表
每个数结点:自己在矩阵的行row,列col,具体数值item,down列项指向自己所属的列数的下个数结点,right行项指向自己所属的行数的下个数结点。
注意:如果down列项和right行项有空的,都指回初始的【行/列】的头结点。
5. 基于【栈】的经典算法(先进后出):
5.1 递归算法:
递归的基本规律:
- 函数调用==进栈
- 函数返回==出栈
- 递归终止 == 栈空
递归的两个要素:
- 边界条件:确定递归何时终止,也称为递归出口
- 递归模式:大问题如何分解为小问题
程序设计中递归的思想一般可以分两类:【分治】和【回溯】,目标都是解决结构自相似的问题。
【分治】:
- 分治思想:将一个规模为n的大问题(难以解决的问题)分解为k个规模较小的小问题(容易解决的问题),这些小问题相互独立,且与大问题的求解方法相同。递归地解这些小问题,然后将各小问题的解合并,得到大问题的解。
- 分治应用:Fibonacci数列,阶乘,汉诺(Hanoi)塔,二分法搜索、归并排序等
- 例如:汉诺塔问题——有一座塔A,上有64个碟。所有碟子按从大到小的次序从塔底堆放至塔顶。紧挨着塔A有另外两个塔B和塔C。问题是:如何借助塔B,将塔A上的碟子移动到塔C上去,每次只能移动一个碟子,任何时候都不能把一个碟子放在比它小的碟子上面?
- 解决思想:
- 解决过程:
关键代码:
- hanoi(n,A,B,C)函数,表明一共要把n个数,从A移动到C,期间借助了B【因为一下子移动不过去】
- move (A, n, C)函数,表明要把第n个数,从A移动到C
- 整体思路:一下子无法将n个数直接从A移动到C。那只能将A【上面的n-1个数】看做一个整体,给第n个数挪位置,先移动到B上,期间借助到了C【hanoi(n-1, A, C, B)】,这样第n个数就可以直接从A移动到C【move (A, n, C)】,然后在将【这n-1个数】从B移动到C上,期间借助到了A【hanoi(n-1, B, A, C)】,这样反复的执行这个过程,直到最后只有一个元素了,直接将他从A移动到C即可【move(A,1,C)】。
【回溯】:
- 回溯思想:把问题的解,通过空间转化,变成了【图 or 树】的搜索形式,然后使用【某种搜索策略】进行遍历,遍历的过程中,记录和寻找所有可行解 or 最优解。
- 回溯应用:迷宫求解、深度优先遍历、八皇后问题等
例如1:迷宫问题——如何确认从起点到终点有可走通的路径?
- 解决思路:由于计算机很傻,只能通过穷举方式(一个方向一个方向)找出口,怎么找法?沿着一个方向走下去,如果走不通,则换个方向走;四个方向都走不通,则回到上一步的地方,换个方向走;依次走下去,直到走到出口。
- 用什么数据类型记录迷宫:二维数组arr[][10]
- 二维数组的值都表示什么:
- 代表墙(阻碍物):-1
- 代表未走过的路径(空白区域):0
- 代表走不通的路径(已经走过了,但是走不通):1
- 代表路径(已经走过了,将该位置设置成路径的一部分):2
- 如何规定搜索方向的顺序:东 -> 南 -> 西-> 北
- 解决过程:
1. 【递归算法】:
【每走一步】的处理方法:
- 如果当前位置==出口(cur.x==end.x)&& (cur.y==end.y),结束。
- 否则:
- 假设当前位置 arr[cur.x][cur.y] 为路径=2;
- 如果东面 arr[cur.x+1][cur.y] 未走过==0:向东走一步,继续递归这个点去探索
- 如果南面 arr[cur.x][cur.y+1] 未走过==0:向南走一步,继续递归这个点去探索
- 如果西面 arr[cur.x-1][cur.y] 未走过==0:向西走一步,继续递归这个点去探索
- 如果北面 arr[cur.x][cur.y-1] 未走过==0:向北走一步,继续递归这个点去探索
- 说明当前位置arr[cur.x][cur.y] 四个方向都完蛋了,走不通=1,回溯
关键代码:
2. 【非递归算法】:
- 解决过程 :
- [当前位置]入栈
- 判断下一步是否可通:“可通”则返回步骤1 ; “不可通”,换方向继续探索;
- 若四周“均无通路”,则当前位置出栈,从【前一位置】换方向搜索。
关键代码:
例如2:八皇后问题——在一个 8×8 的国际象棋棋盘上,要放置八个皇后,使得任意两个皇后都不能在同一行、同一列或同一对角线上?【皇后可以攻击同一行、同一列和同一对角线上的除了自己的其他棋子】
- 解决思路:由于皇后可以攻击同一行、同一列和同一对角线上的除了自己的其他棋子,所以放置皇后时需要确保它们之间不会相互攻击。通过不断尝试不同的位置放置皇后,当发现某个位置不满足条件时,就回退到上一步重新选择位置。这样逐步搜索,直到找到所有满足条件的放置方案或者确定不存在这样的方案。
【动态规划】:
- 动态规划:
- 针对【不同子问题重叠】的情况——【具有公共的子问题】
- 对每个子问题【只求解一次】,效率更高。
- 分治和动态规划对比:
分治 | 动态规划 |
---|---|
将问题划分为互不相交的子问题 | 不同子问题重叠的情况(具有公共的子问题) |
反复地递归求解公共子问题,再将它们的解组合起来,求出原问题的解。 | 对每个子问题只求解一次 |
- 回溯应用:求解最优化问题
- 例如1:背包问题
- 解决思想:
- 刻画一个最优解的结构特征;
- 【递归】地定义最优解的值;
- 计算最优解的值,通常采用【自底向下】的方法;
- 利用计算出的信息构造一个最优解。
- 解决过程:
【设置数组】:
- w[i]:第i件物品的重量weight
- v[i]:第i件物品的价值value
- f[i][j]:将【第i件物品】,放入【容纳重量为 j】的背包里,所获得的最大价值v。【越放,总价值越大】+v[i]
- j:背包能容纳的重量【越放,容纳重量越小】j-w[i]
- m:背包能容纳的总数量【越放,容纳数量越少】i-1
【找递推公式】:对于第i件物品,有两种选择,放 or 不放? ( 注意:放入第i件物品时,我不知道是否能加入,所以根据放不放入来考虑物品i放入后的价值:不放入只考虑前 i-1个物品的价值 ,放入加上i的价值v[i])
- 若放入第i件物品, f[i][j] = f[i-1][ j-w[i] ]+v[i] (背包容量够,不考虑物品
i
时,背包容量为j - w[i]
的最大价值,再加上物品i
的价值v[i]
) - 若不放入第i件物品,f[i][j] = f[i-1][j] (背包容量不够了,不考虑物品
i
时,背包容量为j
的最大价值 )
此时:f[i][j] = max{ f[i-1][j], f[i-1][ j-w[i] ]+v[i] } (比较这两种选择的价值,取较大值赋给f[i][j]
)
- 关键代码:
【递归实现】
#include <stdio.h>
#include <limits.h>
// 假设物品数量为 m,背包总容量为 totalCapacity
int make(int i, int j, int w[], int value[], int m) {
if (i == 0) return 0;
if (j >= w[i]) {
int r1 = value[i] + make(i - 1, j - w[i], w, value, m);
int r2 = make(i - 1, j, w, value, m);
return r1 > r2? r1 : r2;
}
return make(i - 1, j, w, value, m);
}
int main() {
int m = 4; // 物品数量
int w[] = {0, 2, 3, 4, 5}; // 物品重量数组,第一个元素为占位,实际从下标 1 开始
int value[] = {0, 3, 4, 5, 6}; // 物品价值数组,第一个元素为占位,实际从下标 1 开始
int totalCapacity = 8;
int maxValue = make(m, totalCapacity, w, value, m);
printf("最大价值为:%d\n", maxValue);
return 0;
}
【非递归实现】
0 | 1 | 2 | 3 | 4 | |
---|---|---|---|---|---|
0 | 0 | 0 | 0 | 0 | 0 |
1 | 0 | 0 | 0 | 0 | 0 |
2 | 0 | 3 | 3 | 3 | 3 |
3 | 0 | 3 | 4 | 4 | 4 |
4 | 0 | 3 | 4 | 5 | 5 |
5 | 0 | 3 | 7 | 7 | 7 |
6 | 0 | 3 | 7 | 8 | 8 |
7 | 0 | 3 | 7 | 9 | 9 |
8 | 0 | 3 | 7 | 9 | 10 |
9 | 0 | 3 | 7 | 12 | 12 |
10 | 0 | 3 | 7 | 12 | 13 |
i | 1 | 2 | 3 | 4 |
---|---|---|---|---|
物品 | a | b | c | d |
w | 2 | 3 | 4 | 5 |
v | 3 | 4 | 5 | 6 |
#include <stdio.h>
#define N 5 // 假设物品数量
#define V 10 // 假设背包容量
int main() {
int w[] = {0, 2, 3, 4, 5}; // 物品重量数组,第一个元素为占位,实际从下标 1 开始
int value[] = {0, 3, 4, 5, 6}; // 物品价值数组,第一个元素为占位,实际从下标 1 开始
int f[N + 1][V + 1] = {0}; //直接将整个f数组初始化为 0
for (int i = 1; i <= N; i++) {
for (int j = 1; j <= V; j++) {
if (j < w[i]) {
f[i][j] = f[i - 1][j]; //没加入i,还是前i-1个物品
} else {
int x = f[i - 1][j - w[i]] + value[i]; //加入了i,重新计算价值
int y = f[i - 1][j]; //没加入i,还是前i-1个物品
f[i][j] = x > y? x : y;
}
}
}
printf("最大价值为:%d\n", f[N][V]);
return 0;
}