【软件设计师】——2.数据结构与算法

目录

2.1 基本概念

2.2 数据结构

2.2.1 数组

2.2.2 线性表

2.2.3 栈和队列

2.2.4 稀疏矩阵

2.2.5 广义表

2.2.6 树和二叉树

2.2.7 图

2.3 查找

2.4 排序

2.5 算法

2.5.1 算法的特性

2.5.2 算法的复杂度

2.5.3 分治法一分而治之

2.5.4 动态规划法一全局最优

2.5.5 贪心法一局部最优

2.5.6 回溯法一深度优先搜索

2.5.7 分支限界法一广度优先搜索

2.5.8 其他


2.1 基本概念

  • 数据元素、数据项:数据元素是数据的基本单位,通常作为一个整体进行考虑和处理。一个数据元素可由若干数据项组成,数据项是构成数据元素的不可分割的最小单位

  • 物理存储结构

    1. 顺序存储:把逻辑上相邻的元素存储在物理位置上也相邻的存储单元中,通过元素在存储空间中的相对位置来表示数据元素间的逻辑关系

    2. 链式存储:逻辑上相邻的元素在物理位置上可以不相邻

    3. 索引存储:在存储元素信息的同时,还建立附加的索引表

    4. 散列存储:根据元素的关键字直接计算出该元素的存储地址,又称哈希Hash存储;元素的存储地址与其关键字之间存在某种映射关系

2.2 数据结构

2.2.1 数组
  • 一维数组a[n]:a[i]的存储地址为:a + i * len

  • 二维数组a[m] [n] :a[i] [j] 的存储地址:a + ( i * n + j)* len (按行存储)a + ( j * m + i)* len (按列存储)

2.2.2 线性表
  • 基本概念

    链表:最灵活的存储结构,允许同一个表中的结点类型可以不一致,适用于广义表

    散列表:元素的存储位置与其关键字值相关

    静态数组:固定长度的数组

    动态数组:允许扩容,数组中的元素类型必须相同,不适用于广义表

  • 分类

    • 线性表常见两种存储结构:顺序存储-顺序表、链式存储-链表

    • 单链表:从链表的第一个表元开始,将线性表的节点依次存储在链表的各表元中。链表的每个表元除要存储线性表节点信息外,还要一个成分用来存储其后继节点的指针

    • 循环链表:表中最后一个节点的指针域指向头节点,整个链表形成一个环。从表中任意一个节点出发都可以找到表中的其他节点。从头指针开始遍历的结束条件不是节点的指针是否为空,而是是否等于头指针。为简化操作,循环链表中往往加入表头节点

    • 双向链表:节点中有两个指针域,其一指向直接后继,另一指向直接前驱,克服了单链表的单向性的缺点

  • 应用

    • 若采用结点大小相同的链表存储串,在串比较、求子串、串连接、串替换等基本运算中,串替换会改变串中的内容,最不方便

    • 若对一个链表最常用的操作是在末尾插入结点和删除尾结点,则采用仅设尾指针的单向循环链表(不含头结点)时,插入操作的时间复杂度为 O(1),删除操作的时间复杂度为O(n)

    • 若某线性表中最常用的操作是在最后一个元素之前插入和删除元素,则采用( 双链表 )最节省运算时间

性能类别具体项目顺序存储链式存储
空间性能存储密度=1,更优<1,有指针开销
容量分配事先确定动态改变,更优
时间性能查找运算O(n/2)O(n/2)
读运算O(1),更优O([n+1]/2),最好情况为1,最坏情况为n
插入运算O(n/2),最好情况为0,最坏情况为nO(1),更优
删除运算O([n-1]/2),存在元素移动O(1),更优
2.2.3 栈和队列
  • 栈和队列

    1. 都是限制存取点的线性结构,操作受限的线性表

    2. 栈:

      ① 后进先出,仅在表尾插入、删除元素;经一个栈结构后可得到多种元素序列

      ② 用两个栈可模拟一个队列入队/出队;

      ③ 应用:表达式求值、括号匹配、递归

    3. 队列:

      ① 先入先出,仅在表头删除元素、表尾插入元素;经队列结构后只能得到与原序列相同的元素序列

      ② 应用:打印队列、图的广度优先遍历

      ③ 入队、出队操作均与队列长度无关,时间复杂度都为O(1)

      ④ 若队列数据规模n可以确定,则采用顺序存储结构比链式效率更高

  • 优先队列

    1. 常用二叉堆实现,对应于大/小顶堆,存在最大/小优先队列。元素被赋予优先级,访问元素时,最先删除具有最高优先级的元素

    2. 以最大优先队列为例,优先队列除具有堆上的一些操作(调整堆、构建堆)外,还有获得优先队列最大元素、抽取出优先队列最大元素、向优先队列插入一个元素、增大优先队列中某个元素的值

    3. 除获得优先队列最大元素的时间复杂度为O(1),其他操作时间复杂度均为二叉树高度 O(lgn)

  • 循环队列

    1. 将顺序队列形成一个环状结构,元素入队时修改尾指针,元素出队时修改头指针,入队/出队都不需要移动队列中其他元素

    2. 用循环单链表表示队列,入队是在表尾插入元素、出队是在表头删除元素。将队尾指针设置在表尾,可以快速得到表头信息,入队/出队都不需要遍历链表,入队/出队时间复杂度都为O(1)

    3. 队空条件:head = tail;队满条件:(tail+1) % size = head

2.2.4 稀疏矩阵
  • 理解

    1. 一般用二维数组存储矩阵,优点:可随机访问每个元素,较容易地实现矩阵的各种运算

    2. 用二维数组表示稀疏矩阵,会重复存储多个0浪费空间,且要花费时间进行0的无效计算

    3. 对稀疏矩阵进行压缩存储,稀疏矩阵的三元组表的顺序存储为三元组顺序表,链式存储为十字链表

  • 上三角矩阵:在矩阵下标分别为i、j的元素,对应的一维数组下标计算公式:(2n - i + 1) x i/ 2 + j(代入法)

  • 下三角矩阵:在矩阵下标分别为i、j的元素,对应的一维数组下标计算公式为:(i + 1) x i/2 + j(代入法)

2.2.5 广义表
  • 理解

    是n个表元素组成的有限序列,是线性表的推广。通常用递归形式定义,记做:LS = (a0,a1…,an)

    LS:表名;ai:表元素,可以是表-子表/数据元素-原子;n:广义表长度/最外层包含的元素个数

    空表:n=0 的广义表;广义表深度:递归定义的重数,定义中所含括号的重数(原子深度0,空表深度1)

  • 基本运算:取表头 head(Ls)、取表尾 tail(Ls)

    若LS1 = (a,(b,c),(d,e) );head(Ls1) = a ;tail(Ls1) = ( ( b,c),(d,e) ),长度为3,深度为2,b字母取出的操作为 head( head( tail( LS1 ) ) )

2.2.6 树和二叉树
  • 基本概念

    1. 满二叉树:每一层上的节点数均达到最大值

    2. 完全二叉树:除最后一层外,每一层的节点数均达到最大值;最后一层上只缺少右边的若干结点

    3. 平衡二叉树 AVL:空树或任意结点的左右子树深度之差绝对值不超过1,每结点的平衡度只能为 -1、0或1,且它的左、右子树都是一颗平衡二叉树,时间复杂度为树的高 log2n

    4. 树转二叉树:孩子结点 => 左子树结点;兄弟结点 => 右孩子结点

    5. 线索二叉树

  • 二叉树重要特性

    1. 在二叉树的第 i 层上最多有 2^(i - 1)个结点( i ≥ 1)

    2. 深度为 k 的二叉树最多有 2^k - 1 个结点( k ≥ 1)

    3. 任何一棵二叉树,如果其叶子结点数为n0,度为2的结点数为n2,则 n0 = n2 + 1

    4. 有3个结点的二叉树有5种,有4个结点的二叉树有14种

    5. 若对一棵n结点完全二叉树的结点按层序编号,从第1层到 (log2n) +1层,则对任一结点i(1 ≤ i ≤ n),有:

      若 i = 1,该节点为根节点;如果 i > 1,则父结点是 i / 2

      若 2i > n,则结点 i 为叶子结点,无左子结点;否则,其左子结点是结点 2i

      若 2i+1 > n,则结点 i 无右子叶点;否则,其右子结点是结点 2i + 1

    6. n节点完全二叉树的深度为 [log2n]+1,完全二叉树适合采用顺序存储结构

    7. 任何一颗二叉树的叶子结点在先/中/后序遍历序列中的相对次序不发生改变,根据三个遍历的次序和特点:前序-根左右、中序-左根右、后-左右根,相对次序发生变化的都是子树的根,也就是分支结点

  • 二叉查找/排序树

    1. 左孩子小于根,右孩子大于根

      ①若左子树非空,则其左子树上所有节点的关键字均小于根节点;

      ②若右子树非空,则其右子树上所有节点的关键字均大于根节点;

      ③左、右子树本身就是两棵二叉排序树

      综上,二叉排序树是一个有序表,中序遍历可得到关键字递增的序列

    2. 插入结点

      ① 若该键值结点已存在,则不再插入,如48

      ② 若查找二叉树为空树,则以新结点为查找二叉树

      ③ 将要插入结点键值与插入后父结点键值比较,就能确定新结点是父结点的左/右子结点

    3. 删除结点

      ① 若待删除结点是叶子结点,则直接删除

      ② 若待删除结点只有一个子结点,则将这个子结点与待删除结点的父结点直接连接,如56

      ③ 若待删除的结点p有两个子结点,则在其左子结点用中序遍历寻找关键值最大的结点s,用结点s代替结点p的值,然后删除节点s,节点s必属于①,②情况之一,如89

    4. 特点

      ① 若关键字初始序列有序,则构造出的二叉排序树一定是单校树(每个节点只有一个孩子)

      二叉排序树中结点在左、右子树上的分布并不均匀,极端情况下,n结点二叉排序树的高度为n

      ③ 为了使二叉排序树查找操作性能最优,构造二叉排序树需进行平衡化处理,使每个节点左、右子树的高度差的绝对值不超过1

      从左到右排列同层次的结点,其关键字呈现有序排列的特点

      ⑤ 查找过程:若二叉查找树非空,将给定值与根结点的关键字值比较,若相等,则查找成功;若不等,则当根节点的关键字值大于给定值时,到根的左子树中进行查找。否则到根的右子树中进行查找。若找到,则查找过程是走了一条从树根到所找到结点的路径;否则,查找过程终止于一棵空树,因此,在具有n个结点的二叉查找树上进行查找的算法复杂度与树的高度同阶

  • 最优二叉树——哈夫曼树

    1. 哈夫曼树定义:指权值为w1,w2,...,wn的n个叶子节点的二叉树中带权路径长度最小的二叉树

    2. 树的带权路径长度:树中所有叶子节点的带权路径长度之和

    3. 构造最优二叉树的哈夫曼算法:

      ① 根据给定的n个权值(w1,w2,...,wn),构成n棵二叉树的集合F = {T1,T2,...,Tn},每棵二叉树Ti中只有一个带权为wi的根结点,其左右子树均空

      ② 在F中选取两棵权值最小的二叉树作为左、右子树,构造一棵新的二叉树,新构造二叉树的根节点的权值为其左、右子树根节点的权值之和

      ③ 从F中删除这两棵树,同时将新得到的二叉树加入到F中

      ④ 重复②、③,直到F中只含有一棵树时为止,这棵树便是最优二叉树/哈夫曼树

    4. 权值最小的两节点为兄弟节点;权值越大的叶子离根越近;不存在只有一个子树的结点;结点总数为奇数

    5. 若有n个字符/叶子结点构造哈夫曼数,则多出n-1个节点,哈夫曼树的节点数 = 2n-1

  • B树

    1. m阶的B树:① 树中的每个节点至多有M棵子树,所有子结点都出现在同一层次 ② 有k颗子树的非叶子结点有k-1个键,键按照递增顺序排列 ③ 叶子-叶子结点间不相连,若根节点不是叶子节点,则至少有两棵子树 ③ 除根之外的所有非终端节点至少有[M/2]棵子树

    2. B+树:① 有k颗子树的非叶节点有k个键,键按照递增顺序排列 ② 所有叶子结点中包含了完整的索引信息,包括指向含有这些关键字记录的指针,中间节点每个元素不保存数据,只用来索引 ③ 叶子结点本身依关键码的大小自小而大的顺序链接

2.2.7 图
  • 顶点的度:定义为与该顶点相关联的边的数目。无向图中就是与该顶点相邻接的顶点数

  • 有向图:图G的邻接表中共有奇数个表示边的表结点,则图G(是有向图

    对于任意一个图G,其最小割数入(G)不超过最小顶点度数6(G),而最小顶点度数不超过最小顶点割数K(G)。那么对于一个包含奇数个表示边的表结点的邻接表,由于每个表结点表示一条边,因此这意味着对应的图G中必定存在奇数个顶点的度数为奇数,因为整个图的所有顶点的度数之和是偶数。如果一个有向图中不存在奇数长度的环,则其最小割数入(G)等于最小顶点割数K(G)。而一个度数为奇数的顶点必然会形成长度为奇数的环

  • 无向连通图:

    G中任意两个顶点之间存在路径;从G中任意顶点出发可遍历图中所有顶点

  • 完全图:完全图适合采用邻接矩阵存储

    无向图中,若每对顶点之间都有一条边相连,则称该图为完全图

    有向图中,若每对顶点之间都有二条有向边相互连接,则称该图为完全图

  • 邻接矩阵

    1. 若采用邻接矩阵存储简单有向图,则其某一个顶点i的入度等于该矩阵 第i列中值为1的元素个数

    2. 设一个包含N个顶点、E条边的简单无向图采用邻接矩阵存储结构,无向图的邻接矩阵是一个对称矩阵,每条边会表示两次,因此矩阵中的非零元素数目为2E

    3. 一个图的邻接矩阵表示唯一,邻接表表示不唯一

    4. 用一个n阶方阵R来存放图中各结点的关联信息,其矩阵元素Rij定义为:

  • 邻接表

    把每个顶点的邻接顶点用链表示出来,然后用一个一维数组来顺序存储上面每个链表的头指针

  • 图的遍历

    1. 图的遍历:对图中所有顶点进行访问且只访问一次的过程。因为图的任一个结点都可能与其余顶点相邻接,所以在访问了某个顶点之后,可能沿着某路径又回到该结点上。因此为了避免顶点的重复访问,在图的遍历过程中,必须对已访问过的顶点进行标记。图中有回路时也可进行遍历

    2. 深度优先遍历:图进行深度遍历类似于二叉树的先序遍历

    3. 广度优先遍历:

      ① 从图中某个顶点V出发,在访问了v之后依次访问v的各个未被访问过的邻接点,然后分别从这些邻接点出发依次访问它们的邻接点,并使“先被访问的顶点的邻接点”先于“后被访问的顶点的邻接点”被访问,直至图中所有己被访问的顶点的邻接点都被访问到。若此时还有未被访问的顶点,则另选图中的一个未被访问的顶点作为起点,重复上述过程,直至图中所有的顶点都被访问到为止

      ② 尽可能先进行横向搜索,即最先访问的顶点的邻接点也先被访问。类似于对二叉树进行层次遍历,需要借助队列实现。引入队列来保存已访问过的顶点序列,即每当一个顶点被访问后,就将其放入队中,当队头顶点出队时,就访问其未被访问的邻接点并令这些邻接顶点入队

    4. 时间复杂度(具有 n个顶点、e条边的图)

      ① 深/广度优先遍历图的过程实质上是对某个顶点查找其邻接点的过程,耗费时间取决于采用的存储结构

      ② 采用邻接表存储结构:需要O(e)查找所有顶点的邻接点,深度/广度优先遍历的时间复杂度均为O(n+e)

      ③ 采用邻接矩阵/数组:对每个顶点都扫描矩阵的一个行向量,以确定其邻接顶点,时间复杂度为O(n2)

遍历方法说明示例
深度优先1.首先访问出发顶点V;2.依次从V出发搜索V的任意一个邻接点W;3.若W未访问过,则从该点出发继续深度优先遍历,类似于树的前序遍历V1,V2,V4,V8,V5,V3,V6,V7
广度优先1.首先访问出发顶点V;2.然后访问与顶点V邻接的全部未访问顶点W、X、Y;3.然后再依次访问W、X、Y...邻接的未访问的顶点V1,V2,V3,V4,V5,V6,V7,V8
  • 拓朴

    1. AOV网:以顶点表示活动,用有向边表示活动之间的优先关系,称这样的有向图为以顶点表示活动的网。下图的拓朴序列有:02143567、01243657、02143657、01243567

    2. AOE网:若在带权有向图中以顶点表示事件,以有向边表示活动,边上的权值表示该活动持续的时间,这种带权有向图称为用边表示活动的网

    3. 通常在AOE网中列出了完成预定工程计划所需进行的活动、每项活动的计划完成时间、要发生哪些事件以及这些事件和活动间的关系从而可以分析该项工程是否实际可行并估计工程完成的最短时间,分析出哪些活动是影响工程进度的关键。进一步可以进行人力、物力的调度和分配,以达到缩短工期的目的

  • 图的最小生成树(贪心法)

    1. 连通无向图的最小生成树中,顶点数恰好比边数多1;n个结点,连接n-1条线,不能形成环路

    2. Prim:普利姆算法,从扩展顶点开始,每次总是贪心选择与当前顶点集合中距离最短的顶点。时间复杂度为O(n2),n为图的顶点数,计算时间与边数无关,因此适合于求边稠密的图的最小生成树

    3. Kruscal:克鲁斯卡尔算法,从扩展边开始,每次总是贪心的选择剩余的边中最小权重的边。时间复杂度为O(nlgn),n为图的边数,计算时间与顶点数无关,因此适合于求边稀疏的图的最小生成树

    4. 两个算法都基于贪心策略,但若事先没关于图的拓扑特征信息时,无法判断两者优劣。由于一个图的最小生成树可能有多棵,因此不能保证用这两种算法得到的是同一棵最小生成树

2.3 查找

  • 动态查找表:二叉排序树、平衡二叉树、B-树

  • 静态查找表-分块查找:块内无序、块间有序。① 在索引表中确定待查记录所在的块 ② 在块内顺序查找

  • 静态查找表-顺序查找

    1. 思想:将待查找关键字为key的元素从头到尾与表中元素比较,若中间存在关键字为key的元素,返回成功;否则查找失败

    2. 时间复杂度:等概率情况下平均查找长度为 (n+1)/2,时间复杂度为O(n)

  • 静态查找表-二分查找

    1. 思想:前提为有序序列,设 R[low,….,high] 是当前查找区间

      ① 确定该区间中点位置:mid = [(low + high) / 2]

      ② 将待查的k值与R[mid].key比较,若相等,查找成功返回此位置,否则确定新的查找区间继续二分查找

      若R[mid].key > k,则要查找的k必在mid左子表R[low,…,mid-1]中

      若R[mid].key < k,则要查找的k必在mid的右子表R[mid+1,…,high]中

      若R[mid].key = k,则查找成功,算法结束

      ③ 下一次查找是针对新的查找区间进行,重复步骤① ②

      ④ 查找过程中,low逐步增加,high逐步减少。若high < low,则查找失败,算法结束

    2. 时间复杂度:查找成功时关键字比较次数最多为|log₂n|+1次,时间复杂度为O(log₂n)

    3. n结点二叉查找树上查找,最坏情况下的算法复杂度为O(n)

  • 哈希/散列表

    1. 思想:

      ① 已知关键字集合U,最大关键字为m,设计一个函数Hash,它以关键字为自变量,关键字的存储地址为因变量,将关键字映射到一个有限的、地址连续的区间T[ 0..n-1] (n << m)中,这个区间就称为散列表,散列查找中使用的转换函数称为散列函数

      ② 哈希算法是使用给定数据构造哈希表,然后在哈希表上进行查找,是通过数据元素的存储地址进行查找的一种算法。先给定一个值,然后根据哈希函数求得哈希地址,再根据哈希地址查找到要找的元素

      ③ 在计算机程序中,若需要在一秒种内查找上干条记录通常使用哈希表(拼写检查器)。哈希表编程实现相对容易,速度明显比树快,树的操作通常需要O(n)的时间级

    2. 特点:

      ① 数据元素的关键字与其存储地址直接相关。散列表中进行查找时,需要与哈希桶中所有的关键字进行比较,直到找到与目标关键字相等的元素或者确定目标元素不存在于哈希桶中

      ② 同义词:不同的关键字通过散列函数映射到同一个值

      ③ 冲突:通过散列函数确定的位置已经存放了其他元素,可通过开放地址法、链地址法、再哈希法、线性探测法、伪随机数法建立一个公共溢出区

      ④ 装填因子a:表示哈希表装满程度,a越大发生冲突可能性就越大;a越小发生冲突可能性就越小,过小时会浪费大量空间

      线性探测法:可能会产生聚集问题,即出现大量密集的冲突,使得哈希桶中的元素出现聚集。可采用二次探测、双重散列等避免

      ⑥ 链地址法:可以保证每个桶中只有一个元素时,平均查找长度为1,但是当一个桶中有多个元素时,依然需要遍历链表中的所有元素才能找到目标元素,平均查找长度就会增加

      ⑦ 构造哈希函数:某哈希表长度为n,设散列函数为H(Key)=Key mod p,采用线性探测法解决冲突,p的值一般为不大于n且最接近n的质数。构造哈希函数时应尽量使关键字的所有组成部分都能起作用

2.4 排序

  • 排序:重新排列表中的元素,使表中的元素满足按关键字有序的过程

  • 直接插入排序:数据基本有序时采用;排序完后才能确保全部序列及前k个元素的最终排列

    每次将一个待排序的记录按关键字大小插入到前面已排好序的子序列中直到全部记录插入完成;即当插入第i个记录时,R1,R2,…,Ri-1均已排好序,将第i个记录Ri依次与Ri-1,…,R2,R1,比较,找到合适位置插入。简单明了,但速度很慢

  • 希尔排序:

    1. 实质:分组插入。先将待排序表分割成若干形如 L[i, i + d, i + 2d..i + kd]的“特殊”子表,对各个子表分别进行直接插入排序。缩小增量d,重复上述过程,直到 d = 1为止

    2. 先取一个小于n的整数d1作为第一个增量,把文件的全部记录分成d1个组。所有距离为d1的倍数的记录放在同一个组中。先在各组内进行直接插入排序;然后,取第二个增量d2 < d1,重复上述分组和排序,直至所取的增量dt = 1(dt < dt-1 < O < d2 < d1),即所有记录放在同一组中进行直接插入排序为止

  • 简单/直接选择排序:

    1. 每一趟在待排序元素中选取关键字最小的元素加入有序子序列

    2. 首先在所有记录中选出排序码最小的记录,把它与第1个记录交换,然后在其余的记录内选出排序码最小的记录,与第2个记录交换。依次类推,直到所有记录排完为止

  • 堆排序:k部分排序

    1. 堆:设有n个元素的序列{K1,K2,…,Kn},当且仅当满足下述关系之一时,称之为堆:小顶堆:ki ≤ k2i 且 ki ≤ k2i + 1,孩子节点大于父节点;大顶堆: ki ≥ k2i 且 k i≥ k2i + 1,孩子节点小于父节点

    2. 思想:先将序列建立堆,输出堆顶元素,再将剩下序列建立堆,再输出堆顶元素,依此类推直到所有元素输出为止,此时元素输出的序列就是一个有序序列

    3. 利用堆这一特殊的树形结构进行的选择排序,有效改进了直接选择排序,提高了算法效率,每次重建堆的时间复杂度是log₂n,n个元素总时间复杂度是O(nlog2n)

    4. 初建堆过程:假设有数组A = {1,3,4,5,7,2,6,8,0}

    5. 堆排序过程:将顺序表R = {80,60,16,50,45,10,15,30,40,20}进行堆排序(以大顶堆为例)

      ① 初始时将顺序表R[1..n]中元素建立为一个大顶堆,堆顶位于R[1],待序区为R[1..n]

      ② 循环执行步骤③ ④ ,共n - 1次

      ③ 假设为第i次运行,则待序区为R[1..n-i+1],将堆顶元素R[1]与待序区尾元素R[n-i+1]交换,此时顶点元素被输出,新的待序区为R[1..n-i]

      ④ 待序区对应的堆已经被破坏,将之重新调整为大顶堆

  • 冒泡排序:通过相邻元素间比较和交换,将排序码较小元素逐渐从底部移向顶部。整个排序过程就像水底下的气泡一样逐渐向上冒,称为冒泡算法

  • 快速排序:分治法,数据有序使其效率最低

    1. 思想:在待排序表L[1...n]中任取一个元素pivot作为枢轴/基准(常取首元素),通过一趟排序将待排序表划分为独立两部分L[1...k-1]、L[k+1...n],使得L[1...k-1]中的所有元素小于pivot,L[k+1...n]中的所有元素大于等于 pivot,则pivot放在了最终位置L(k)上,该过程称为一次“划分”。然后分别递归地对两个子表重复上述过程,直到每部分内只有一个元索或空为止,即所有元素放在了其最终位置上

    2. 采用分治法,将原问题分解成若干规模更小但结构与原问题相似的子问题。通过递归解决这些子问题,再将这些子问题的解组合成原问题的解。进行一趟划分需要遍历一遍数组,一趟的时间复杂度为O(n)

    3. 步骤

      ① 在待排序的n个记录中任取一个记录,以该记录的排序码为准,将所有记录都分成两组,第1组都小于该数,第2组都大于该数

      ② 采用相同的方法对左、右两组分别进行排序,直到所有记录都排到相应的位置为止

  • 归并/合并排序:分治法

    1. 思想:将两个或两个以上有序子表合并成一个新有序表。将两个有序表合并成一个有序表,称为二路合并

    2. 过程:比较A[i]和A[j]的排序码大小,若A[i]排序码小于等于A[j]排序码,则将第一个有序表中的元素A[i]复制到R[k]中,并令i、k分别加1;如此循环下去,直到其中一个有序表比较和复制完,然后再将另一个有序表剩余元素复制到R中

  • 基数排序:关键字取值范围较小

    借助多关键字排序思想对单逻辑关键字进行排序。基数排序不是基于关键字比较的排序方法,它适合于元素很多而关键字较少的序列。基数选择和关键字分解是根据关键字类型来决定的,例如关键字是十进制数,则按个位、十位分解

  • 评价指标

    1. 对一个初始无序序列, 简单选择排序、堆排序、冒泡排序、快速排序第一趟排序结束后,一定能将序列中的某个元素在最终有序序列中的位置确定下来

    2. 数据基本有序

      对插入排序:可以在近似线性时间内完成排序,时间复杂度O(n)

      对快速排序:最坏情况,需要二次时间才能完成排序,时间复杂度O(n2)

类别排序方法时间复杂度/最好时间复杂度/平均时间复杂度/最坏空间复杂度稳定性
插入排序直接插入O(n),基本有序 O(n^2)O(n^2)O(1)稳定
希尔排序O(n)O(n^1.3)O(n^2)O(1)不稳定
选择排序直接/简单选择O(n^2)O(n^2)O(n^2)O(1)不稳定
堆排序O(nlog₂n)O(nlog₂n)O(nlog₂n)O(1)不稳定
交换排序冒泡排序O(n)O(n^2)O(n^2)O(1)稳定
快速排序O(nlog₂n)O(nlog₂n)O(n^2);基准元素是中位数O(nlgn)O(log₂n)不稳定
归并排序O(nlog₂n)O(nlog₂n)O(nlog₂n)O(n)占用辅助存储空间最多稳定
基数排序O(d(r+n))O(d(rd+n))O(r+n)稳定

2.5 算法

2.5.1 算法的特性
  • 算法:对某类给定问题求解过程的精确描述,是为解决一个/一类问题给出的一个确定的、有限长的操作序列

  • 算法设计原则:

    ① 首先说设计的算法必须是"正确的",其次应有很好的"可读性",还必须具有"健壮性",最后应考虑所设计的算法具有"高效率与低存储量"

    ② 在算法是正确的前提下,算法的可读性是摆在第一位的。算法的效率是指算法的执行时间,算法的存储量是指算法执行过程中所需最大存储空间

  • 基本特性

    ① 有穷性:任意一组合法输入值,在执行有穷步骤后一定能结束,即算法中每个步骤都能在有限时间内完成

    ② 确定性:对于每种情况下所应执行的操作,在算法中都有确切的规定,使算法的执行者或阅读者都能明确其含义及如何执行,并且在任何条件下,算法都只有一条执行路径。算法中每一条指令都必须有确切的含义,不能含糊不清;算法的每个步骤都能有效执行并能得到确定的结果。如a =0,b/a就无效

    ③ 可行性:算法中的所有操作都必须足够基本,都可以通过已经实现的基本操作运算有限次实现

2.5.2 算法的复杂度
  • 时间复杂度:

    1. 指程序运行从开始到结束所需要的时间。分析时间复杂度的方法是从算法中选取一种对于所研究的问题来说是基本运算的操作,以该操作重复执行的次数作为算法的时间度量。算法中原操作重复执行的次数是规模n的某个函数T(n)。由于许多情况下要精确计算T(n)是困难的,因此引入了渐进时间复杂度在数量上估计一个算法的执行时间

    2. 如果存在两个常数c和m,对于所有的n,当n ≥ m时有f(n) ≤ cg(n),则有f(n)=O(g(n))。随着n的增大,f(n)渐进地不大于g(n)。例如,一个程序的实际执行时间为T(n)=3n^3+2n^2+n,则T(n)=O(n^3)

    3. 设某算法计算时间表示为递推关系式 T(n)= T(n-1) + n (n>0) 及 T(0)=1,则该算法时间复杂度为 O(n^2 )

      设某算法计算时间表示为递推关系式 T(n)=2T(n/2) + O(n),则该算法时间复杂度为(nlgn )

    4. 常见的对算法执行所需时间的度量:O(1) < O(log₂n ) < O(n) < O(nlog₂n) < O(n^2) < O(n³) < O(2^n) < O(n!) < O(n^n)

  • 空间复杂度:指对一个算法在运行过程中临时占用存储空间大小的度量。一个算法的空间复杂度只考虑在运行过程中为局部变量分配的存储空间的大小

2.5.3 分治法一分而治之
  • 思想:对于一个规模为n的问题,若该问题容易解决(如规模n较小)则直接解决;否则将其分解为k个规模较小的子问题,子问题互相独立且与原问题形式相同递归地解各子问题,将各子问题的解合并得到原问题的解

  • 特征:把一个问题拆分成多个小规模的相同子问题,一般可用递归解决

  • 经典问题:斐波那契数列、归并排序、快速排序、矩阵乘法、二分搜索、大整数乘法、汉诺塔

  • 步骤和要求

    求解:该问题规模缩小到一定程度就可以容易解决;递归(在运行过程中调用自己)地求解各子问题

    分解:该问题可以分解为若干规模较小的相同问题

    合并:利用该问题分解出的子问题的解可以合并为该问题的解

    独立:该问题所分解出的各个子问题相互独立

2.5.4 动态规划法一全局最优
  • 思想:

    ① 在求解问题中,对于每一步决策,列出各种可能的局部解,再依据某种判定条件舍弃那些肯定不能得到最优解的局部解,在每一步都经过筛选,以每一步都是最优解来保证全局是最优解

    ② 动态规划算法的第一步通常是刻画最优解结构。当问题的最优解包含了子问题的最优解时,称该问题具有最优子结构性质。问题的最优子结构性质提供了该问题可用动态规划算法求解的重要线索,以自底向上的方式递归地从子问题的最优解逐步构造出整个问题的最优解

  • 特征:划分子问题,并把子问题结果使用数组存储,利用查询子问题结果构造最终问题结果

  • 经典问题:斐波那契数列、LCS最长公共子序列、矩阵乘法、背包问题

  • 区别分治法——子问题不独立

    基本将待求解问题分解成若干个子问题,先求解子问题,然后从这些子问题的解得到原问题的解

    与分治法不同的是,适合于用动态规划法求解的问题,经分解得到的子问题往往不是独立的

    动态规划算法通常用于求解具有某种最优性质的问题。在这类问题中,可能会有许多可行解,每个解都对应于一个值,我们希望找到具有最优值的那个解。最优解可能会有多个,动态规划算法能找出其中的一个最优解

  • 区别贪心法——整体最优

    动态规划法来求解问题具有的性质:

    1. 最优子结构。当一个问题的最优解中包含其子问题的最优解,称该问题具有最优子结构。问题的最优子结构是该问题可以采用动态规划法或者贪心法求解的关键性质

    2. 重叠子问题。指用来解原问题的递归算法可反复地解同样的子问题,而不是总在产生新的子问题。即当一个递归算法不断地调用同一个问题时,就说该问题包含重叠子问题。此时若用分治法递归求解,则每次遇到子问题都会视为新问题,会极大地降低算法的效率,而动态规划法总是充分利用重叠子问题,对每个子问题仅计算一次,把解保存在一个在需要时就可以查看的表中,而每次査表的时间为常数

2.5.5 贪心法一局部最优
  • 思想:总是做出在当前来说是最好的选择,而并不从整体上加以考虑,它所做的每步选择只是当前步骤的局部最优选择,但从整体来说不一定是最优的选择。由于它不必为了寻找最优解而穷尽所有可能解,因此其耗费时间少,一般可以快速得到满意的解,但得不到最优解

  • 特征:局部最优,但整体不见得最优。每步有明确的,既定的策略

  • 经典问题:部分背包(保证能求得最优解)、多机调度、找零钱问题。例如:由于每次选择下一个要访问的城市时都是基于与当前最近的城市来进行,是一种贪心的选择策略。而货车从中央仓库出发,第一个要到达的目的地是在n个目的地中选择一个,第二个要到达的目的地是在n-1个目的地中选择一个,. . . . .第n个要到达的目的地是在1个目的地中选择一个,因此时间复杂度为n+(n-1)+...+1= n*(n-1)/2=n^2

  • 贪心法求解的问题一般具有两个重要的性质

    1. 最优子结构。当一个问题的最优解包含其子问题的最优解,称此问题具有最优子结构。问题的最优子结构是该问题可以采用动态规划法或者贪心法求解的关键性质

    2. 贪心选择性质。指问题的整体最优解可以通过一系列局部最优的选择,即贪心选择来得到。这是贪心法和动态规划法的主要区别

2.5.6 回溯法一深度优先搜索
  • 思想:回溯法是一种选优搜索法,按选优条件向前搜索,以达到目标。但当搜索到某一步时,发现原先选择并不优或达不到目标,就退回一步重新选择。这种走不通就退回再走的技术

  • 特征:系统的搜索一个问题的所有解或任一解

  • 经典问题:N皇后问题、迷宫、背包问题

  • 试探部分扩大规模:满足除规模之外的所有条件

  • 回溯部分缩小规模:① 当前规模解不是合法解时回溯(不满足约束条件)② 求完一个解要求下一个解时回溯

2.5.7 分支限界法一广度优先搜索
  • 思想:类似于回溯法,也是一种在问题的解空间树T 上搜索问题解的算法,但在一般情况下,分支限界法与回溯法的求解目标不同。分支限界法的求解目标是找出满足约束条件的一个解即可。由于求解目标不同,其探索方式与回溯法也不同,分支限界法以广度优先或以最小耗费优先的方式搜索解空间树

2.5.8 其他
  • 地杰斯特拉(Dijkstra)算法:求单源点最短路径,按路径长度递增的顺序求源点到各定点的最短路径

  • 最长公共子序列:求解两个长度为n的序列X和Y的一个最长公共序列的一个最长公共子序列

    ① 蛮力法:对X的每一个子序列,判断其是否也是Y的子序列,最后求出最长的即可。长度为n的序列X的子序列数是2^n,而判断一个子序列是否也是Y的子序列的时间是n,时间复杂度为O(n2^n)

    ② 动态规划:经分析发现该问题具有最优子序列,可以定义序列程度分别为i、j的两个序列X、Y的最长公共子序列的长度为C[i,j],采用自底向上的方法实现该算法,则时间复杂度为O(n^2)

  • 矩阵链乘问题:

    ① 矩阵链乘是一个最优化问题,求解n个矩阵相乘的最优加括号方式,可用动态规划求解,程序中有问题规模的循环、i的循环、k的循环,时间复杂度是O(n^3)

    ② 需要申请二维数组m来存储每个子问题的标量乘法次数,空间复杂度为O(n^2)

  • LC-检索

    ① 在状态空间树中,定义c(.)为结点的成本函数,g(X)为从结点向X到达一个答案结点所需做的附加工作的估计函数,h(X)为从根结点到结点X的成本,则用成本估计函数c(X) = f(h(X) + g(X))选择下一个E结点的检索策略总是选取c(.)值最小的活结点作为下一个E-结点,因此这种检索策略称为最小成本检索,简称LC-检索

    ② 在状态空间树中找出最优的答案结点,就可以利用LC-检索快速找到一个答案结点。根据定义在进行LC-检索时,为避免算法过分偏向于做纵深检查,应该在成本估算函数C(.)中考虑根结点到当前结点的成本(距离)

  • 16
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值