数据结构与算法

第一章 数据结构

第1节:什么是数据结构?什么是算法?

  1. 程序(Program)= 数据结构(Data Structure) + 算法(Algorithm)
  2. 数据结构:是一些待处理的,要存到计算机里的,"数据"及之间的关系/组织形式。包含数据的逻辑结构 存储结构 运算/操作
  3. 算法:用于处理数据(解决某问题)的 一段指令集合
  4. 数据结构 + 算法  编程效率 (决定了)

第2节:数据结构和算法用于解决哪些问题?

路由转发问题,需要存储各种数据的问题,各种赛制pk的争冠问题等。。

例如:

第3节:数据结构和算法 在软件开发中 占哪一步?

一个"系统"的一生 : 需求分析 ->【设计】-> 实现 -> 测试 -> 部署运行 -> 维护

第4节:数据结构的基本概念:

先搞清楚什么是数据项,数据元素,数据?

  • 数据元素:是"数据"基本单位,在计算机程序中通常【作为一个整体】进行处理。也被称为元素、顶点、结点、记录
  • 数据项:构成数据元素的不可分割的最小单位(一个项一个项的)
  • 数据项 < 数据元素 < 数据对象 < 数据

例如:

第5节:数据结构的类型:

1. 逻辑结构(及数据元素之间的关系):

  • 线性结构:一对一(线性表、链表、栈、队列、数组等)【例如:电话号码簿】
  • 结构:一对多(二叉树、多叉树、红黑树等)
  • 结构:多对多(图、网等)
  • 集合结构:无特定关系(散列/哈希表)

2. 存储结构(物理):

  • 顺序存储结构:在硬盘里连着顺序,一个挨着一个的存放
  • 链式存储结构:在硬盘里有规则的乱放
  • 还有索引存储、散列存储(有些地方会有)

3. 逻辑结构和存储结构的关系:

每一种逻辑结构】都可以 使用【不同的存储结构】来实现

4. 区分逻辑结构与存储结构:

方法:给出一个结构,若能用多种(n>1) 种不同的方式存储,那就是逻辑结构;反之就是存储(物理)结构,已经采用了一种具体的存储方法。

例如

  • 线索二叉树: (这种加上了线索的二叉链表称为线索链表,相应的二叉树称为线索二叉树) 二叉树是一种逻辑结构,但线索二叉树是加上线索后的【链表结构】,因此是存储结构
  • 静态链表:是用【一片连续的空间(数组,顺序)】来存储实现的,故静态链表是存储结构(称为用数组实现的链式存储结构)。
  • 散列表:用的是【散列(哈希)】存储,即根据元素的关键字直接计算出该元素的存储地址,故是存储结构
  • 循环队列:用【数组】求余实现,故是存储结构
  • 顺序表:用【数组】实现,故是存储结构
  • 有向图:可以用【邻接表】或【邻接矩阵】两种实现,故有向图是逻辑结构
  • 二叉树:可以用【数组】或【指针(链式)】实现,故二叉树是逻辑结构

第6节:抽象数据类型ADT (Abstract Data Type)

1. 含义

通俗来说就是一个各种函数的模版,这些模版把每个操作都分成了不同的模块,有存放了各种数据类型的(数据对象),数据对象的各个元素之间的关系(数据关系),以及一些对数据对象的基本操作(基本操作

2. 特点:

  • 封装性:把数据操作都封装在一起了。
  • 抽象性只关注逻辑操作,不涉及实现的细节。
  • 独立性可移植到不同环境,和具体实现的高级语言无关。

3. 例如:

栈(Stack):

  1. 数据对象:栈中的元素集合。
  2. 数据关系:栈中的元素遵循 后进先出(Last In First Out,LIFO)的原则。
  3. 基本操作:
  • push:将一个元素压入栈顶。
  • pop:弹出栈顶元素。
  • top:获取栈顶元素但不弹出。
  • isEmpty:判断栈是否为空。

第7节:存储密度

  • 含义:一个结点数据本身所占的存储量和整个结点结构所占的存储量之比。
  • 存储密度 = (一个结点数据本身所占的存储量)/(一个结点结构所占的存储总量
  • 例如:一个结点,假设有数据区域data占8B,也会有指针区域next占2B,那么这个结点的存储密度=8/(8+2)

第一章 算法

第1节:算法的特性:

  • 有穷性/有限性:一段算法得有限行解决吧。我就解决个1+1=2,你非要用量子力学,微积1分,循环无限的去处理。
  • 确定性:写的每行代码得有自己的功能吧。别给我整一堆没用的代码。
  • 可行性:这段代码总不能有一堆错误吧。比如说条件失效的无限循环,你想干嘛,你是猴子请来的救兵吗?
  • 输入,输出:写代码是给我解决问题的,比如说数学里的函数,我得有题目的1+1输入,还得算结果=2输出。

【注意】:算法与【具体的"程序设计语言"(c,py,java)无关】,算法的具体实现就是程序

第2节:如何描述一个算法?

  1. 自然语言:用人话描述出来(描述的不够准确和严格)
  2. 流程图:画出来这个逻辑过程(辅助描述算法的方法)
  3. 伪代码:用具体计算机语言描述出来(辅助描述算法的方法)
  4. 程序设计语言:针对具体语言的实现(c,python,java)(称为算法语言***

1.自然语言

2.流程图

3.伪代码

第3节:如何评价一个算法?

  1. 高效性 (时间复杂度 + 空间复杂度)
  2. 正确性
  3. 可读性
  4. 简单性
  5. 健壮性
  6. 抽象性
  7. 可维护性
  8. 兼容性

第4节:如何衡量一个算法的效率?

  1. 时间复杂度(时间效率): 运行一段最深层次的代码,需要多少时间
  2. 空间复杂度(空间效率):运行一段代码,最多需要浪费多少存储空间

第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)
  • 例如:

  • 常见时间复杂度的量级
  1. 常数阶 O(1) :通常与n没有关系(代码中没有给n,或者与n关联不大)
  2. 对数阶 O(logn)
  3. 线性阶 O(n)
  4. 线性对数阶 O(nlogn)
  5. 平方阶 O(n²)
  6. 指数阶 O(2^n) 【问题规模n较大时,不可计算】***
  • 时间复杂度趋势比较:

第6节:空间复杂度涉及的定义:

  • 空间复杂度:【辅助空间的数量】

∙ 表示方式:O(n)

∙ 计算方法:O( 使用辅助空间最大的n项 )【与时间复杂度类似】

问题:数据结构与算法的经典问题:

  1. 地图染色问题:地图上相邻的国家使用不同的颜色标注,则最少使用多少种颜色就能全表示出来?

答: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) 

黄金分割

数列: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的组成部分

  1. 容器(Containers):一个存放数据的“盒子”,这个盒子的容量大小可以灵活变化,是一种特定类型对象的集合,比如一种类、数据结构、或者抽象数据类型
  2. 算法(Algorithms):提供了各种通用的算法。排序算法(如 sort)、查找算法(如 find)、遍历算法等。
  3. 迭代器(Iterators):连接“容器”和“算法”的桥梁。类似于指针(指向某个对象),用来遍历容器类中的对象。有不同类型的迭代器,如输入迭代器、输出迭代器、前向迭代器、双向迭代器和随机访问迭代器等,每种迭代器支持不同的操作。
  4. 仿函数/函数对象(Functors):类似函数一样,可以被调用的对象。可以自定义仿函数来实现特定的操作,例如自定义比较函数(用于排序算法)。
  5. 适配器(Adapters):除了容器适配器,还有函数适配器和迭代器适配器。函数适配器可以修改现有的函数对象的行为,迭代器适配器可以修改迭代器的行为或创建新的迭代器类型。
  6. 空间配置器(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节:学了线性表,在生活中能解决什么问题?解决目标是什么?

  1. 要解决的问题:
  • 通讯录
  • 图书管理系统(图书管理、借阅管理)
  • 电商(商品管理、订单管理、客户信息管理)
  • 选课系统(课程管理、选课管理、成绩管理)
  • 学籍管理系统等

2. 目标:

设计并实现一个通用类class,完成上述各种系统的增删改查,其中这个类包含以下要素:

  1. 数据类型T(任意) 【template】
  2. 数据长度L(任意) 【N】
  3. 通用操作(运算/算法):插入、删除、查找、输出 【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. 存储结构:

  1. 顺序存储结构:用一段地址连续的存储单元,依次存储数据元素(结点)
  2. 链式存储结构:
  • 单链表物理上不连续依赖后继指针定位下一个元素的位置。用结构类型描述结点,包含数据域指针域。实现包括构造函数(空链表、头插法、尾插法)、析构函数、按值查找(locate)、按位查找(Get)、插入(Insert)、删除(Delete)等操作。【带头结点first的单链表】解决了空链表非空链表的不同处理,以及首结点插入、删除处理特殊的问题。
  • 循环链表结点指针指向头结点,【可同时快速找到头尾结点】。
  • 双链表:每个结点有两个指针域,指向前驱和后继【可同时搜索前驱和后继结点】。
  • 静态链表:使用数组元素的下标来模拟单链表的指针,【可在无指针的语言中可实现链表

第3节:顺序存储结构

  1. 含义:用一段地址连续的存储单元,依次存储数据元素(结点)
  2. 实现:
  • 在 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的指针变量,它指向一个模板类型为TNode类型对象(结点)。这意味着可以根据不同的类型参数T来实例化这个指针所指向的对象,从而实现泛型编程,使代码可以适用于不同的数据类型。
  • new Node<T> 在内存上(动态)分配一个模板类型为TNode对象(结点)所需的内存空间。使用完动态分配的内存后,应该使用delete p; 来释放分配的内存,以避免内存泄漏。

4)p,q指针指向同一地址的结点:Node<T>* q = p; (前提p指针已经被创建了)

  • Node<T>* q 声明了一个名为q的指针变量,其类型为指向模板类型为TNode对象(结点)的指针。
  • q = p 将指针p的值(即它所指向的内存地址)赋给指针q。现在,qp都指向同一个Node<T>对象(结点)。
  • 如果对pq所指向的对象(结点)进行修改,会影响到另一个指针所指向的对象(结点),因为它们指向同一个内存位置。
  • 注意及时释放动态分配的内存

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. 静态链表的删除:

第二章 总结

  1. 链表:插入、删除不需要移动大量元素
  2. 链表:动态分配内存
  3. 链表:顺序查找,不能直接定位
  4. 链表:头插法尾插法的结果正好相反(可以反转)
  5. 顺序表适用于频繁查找的场景;链表适用于频繁插入和删除的场景。
  6. 表长为n的顺序表,插入/删除任何一个元素等概率,插入一个元素平均移动个数【n/2】删除一个元素平均移动个数【n-1/2】
  7. 静态链表是链式结构,但属于静态存储结构
  8. 长度为n的顺序表,等概率条件下,查找成功时,按序号查找的平均查找次数为【1】;按值查找的平均查找次数为【n+1/2】
  9. 顺序表是一种采用随机存取方式(结构)的顺序存储结构【keyword:存取or存储 】;链表是一种采用顺序存取方式的链式存储结构

第二章 练习

1. 已知数组A[n]中元素为整型,设计算法将其调整为左右两部分,左边所有元素为偶数,右边所有元素为奇数

  • 基本思想:将左边的奇数调整到右边,右边的偶数调整到左边,关键在于调整的新位置如何确定。最理想的情况:正好将左边的奇数和右边的偶数交换一下。

  • 代码实现:

2. 以单链表为存储结构,写出就地逆置的算法

  • 基本思想:
  1. 孤立出来头结点
  2. 按照头插法的原则建立链表
  • 代码实现:

3. (约瑟夫问题)设有编号为【1、2、3、…、n】的n个人围成一个圈。从【第一个人】开始报数,报到m的人出圈,再从出圈的人的下一个人起重新报数,报到m的人出圈,如此下去,直到所有人全部出圈为止。给定任意的总人数n和m项,设计算法求n个人出圈的次序。

  • 基本思想:
  1. 逻辑结构:线性表
  2. 存储结构:数组/链表
  • 代码实现:

第三章 线性表的扩展(栈、队列、串、多维数组)

1. 普通的线性表

  • 可以存储任意类型的数据
  • 可以在任意位置进行插入、删除操作。

2. 扩展线性表

  • : 后进先出(LIFO线性表
  • 队列先进先出(FIFO线性表
  • : 字符为结点的线性表
  • 多维数组类型相同的元素构成的集合

第三章 栈

1. 栈的逻辑结构:

  1. 只能在表的一端进行插入和删除的线性表
  2. (有数据的情况下)栈顶为 ( top ),栈底 ( bottom )
  3. 存取原则:后进先出 (LIFO)
  4. 主要操作:
  • 入栈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. 顺序栈和链栈的比较:

  1. 时间耗费:O(1)
  2. 空间耗费:
  • 顺序栈:预分配(STACKSIZE),有额外的空间浪费
  • 链栈:指针域(top),有额外开销

4.测试:设计算法,把十进制数转换为二进制输出。比如:77 = 1001101

第三章 队列

1. 队列的逻辑结构

  1. 特征:在表的一端进行插入,而在表的另一端进行删除
  2. 队头——允许删除的一端。
  3. 队尾——允许插入的一端。
  4. 存取原则:先进先出(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. 串的逻辑结构

  1. 串是字符串string,是由【零个 || 多个字符】组成的有限序列,每一个结点都是一个字符
  2. 记作:S=“ a1a2 …… an ”
  3. 空格串和空串的区别:
  • 空格串: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。
  • 失配函数(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数组:

  1. 令 next[0] = -1,next[1] = 0,
  2. 假设i:2 ~ len-1
  3. 令 j= next[i-1] , 即P[ 0, ... , j ] == P[ i-j-1 ,..., i-1 ] ,则:
  4. 当 j !=-1 且 Pj != Pi−1 【 P[j] != P[i-1] 】,则 j = next[j]
  5. 当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;
}
i01234
sababc
next[i]-10120

例如:

saabacaa
i0123456
next[i]-1001012

2.2 【链式存储结构】串

1. 常用两种串的结构:

第三章 多维数组

1. 多维数组的逻辑结构

  1. 类型相同的数据元素构成的有序集合,每一个元素受n个线性关系的约束,每个元素有一个序号 i1 、 i2 … in ,称为该元素的下标,并称该数组为n维数组
  2. 主要操作:存取 and 修改
  3. 两种下标的数组表示:

2. 多维数组的物理结构

  1. 本质操作:寻址
  2. 存储(物理)结构:顺序存储结构
  • 顺序存储结构的问题:
  1. 内存是一维的结构
  2. 多维数组是多维的结构
  • 解决方法:多维数组->一维数组【映射】
  1. 行优先存储【C++】
  2. 列优先存储(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开始】:
  1. 行号 row : 0~m-1
  2. 列号 col : 0~n-1
  3. 值 item/value : 具体是什么数
  4. data[t] : 存储行号,列号和值
  5. m:矩阵一共有m行
  6. n:矩阵一共有n列
  7. 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.【优化】方法:

  1. 扫描一遍A表,直接定位到A中的元素在B中的位置
  2. 如果知道: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 十字链表

  • 定义:表示稀疏矩阵(三元组)的另一种方式,用链式的结点存储一个元素
  • 存储方式:顺序结构【数组】+链式结构【链表】
  • 如何找到定位到一个准确元素:
  1. 行号 row : 该非零元素所在的行号[0~m-1]
  2. 列号 col : 该非零元素所在的列号[0~n-1]
  3. 值 item/value : 具体是什么数
  4. down:指向【同一列】的下一个非零元素的节点 ↓
  5. 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. 代表墙(阻碍物):-1
  2. 代表未走过的路径(空白区域):0
  3. 代表走不通的路径(已经走过了,但是走不通):1
  4. 代表路径(已经走过了,将该位置设置成路径的一部分):2

  • 如何规定搜索方向的顺序:东 -> 南 -> 西-> 北

  • 解决过程:

1. 【递归算法】:

【每走一步】的处理方法:

  1. 如果当前位置==出口(cur.x==end.x)&& (cur.y==end.y),结束。
  2. 否则:
  • 假设当前位置 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. 判断下一步是否可通:“可通”则返回步骤1 ; “不可通”,换方向继续探索;
  3. 若四周“均无通路”,则当前位置出栈,从【前一位置】换方向搜索。

关键代码:

例如2:八皇后问题——在一个 8×8 的国际象棋棋盘上,要放置八个皇后,使得任意两个皇后都不能在同一行、同一列或同一对角线上?【皇后可以攻击同一行、同一列和同一对角线上的除了自己的其他棋子】

  • 解决思路:由于皇后可以攻击同一行、同一列和同一对角线上的除了自己的其他棋子,所以放置皇后时需要确保它们之间不会相互攻击。通过不断尝试不同的位置放置皇后,当发现某个位置不满足条件时,就回退到上一步重新选择位置。这样逐步搜索,直到找到所有满足条件的放置方案或者确定不存在这样的方案。

【动态规划】:

  • 动态规划:
  1. 针对【不同子问题重叠】的情况——【具有公共的子问题】
  2. 对每个子问题【只求解一次】,效率更高。
  • 分治和动态规划对比:
分治动态规划
将问题划分为互不相交的子问题不同子问题重叠的情况(具有公共的子问题)
反复地递归求解公共子问题,再将它们的解组合起来,求出原问题的解。对每个子问题只求解一次
  • 回溯应用:求解最优化问题
  • 例如1:背包问题

  • 解决思想:
  1. 刻画一个最优解的结构特征;
  2. 【递归】地定义最优解的值;
  3. 计算最优解的值,通常采用【自底向下】的方法;
  4. 利用计算出的信息构造一个最优解。
  • 解决过程:

【设置数组】:

  • 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;
}

【非递归实现】

01234
000000
100000
203333
303444
403455
503777
603788
703799
8037910
90371212
100371213
i1234
物品abcd
w2345
v3456
#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;
}
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值