数据结构与算法分析

数据结构与算法分析

C 语言描述

引论

  • 从N个数中确定第k个最大值,称为选择问题(selection problem).
  • 不是所有的数学递归函数都能有效地(或正确地)由C的递归模拟来实现. 递归将反复进行直到基准情形出现.
  • 递归的四条基本法则:
    • 基准情形: 不需递归也能得到的解, 即终止条件.
    • 不断推进: 每次递归调用都要使解朝向基准情形推进.
    • 设计法则: 假设所有的递归调用都能运行.
    • 合法效益法则(compound interest rule): 在不同递归调用中不要做重复性的工作.

算法分析

  • 算法(algorithm)是为求解一个问题需要遵循的, 被清楚地指定的单一指令的集合.
  • 分治策略(divide and conquer):
    • 分: 把问题分成两个大致相等的子问题, 然后递归地求解.
    • 治: 将两个子问题的求解合并到一起并求解, 最后得到整个问题的解.

表, 栈和队列

  • 抽象数据类型(abstract data type, ADT)是一些操作的集合.
  • 后缀表达式: 计算后缀表达式花费的时间是O(n), 每一步处理是栈操作的常数时间.
  • 利用栈将一个标准形式的表达式转换为后缀式:
    • 当读到一个操作数的时候,立即把它放到输出中;
    • 遇到操作符, 用栈缓存;
    • 遇到左括号也入栈;
    • 遇到右括号,就一直弹出至一个左括号;
    • 遇到符号, 弹出至低优先级符号为止.

  • 二叉查找树(binary search tree).
  • 先序遍历(preoder traversal): 根左右.
  • 后序遍历(postorder traversal): 左右根.
  • 中序遍历(inorder traversal): 左根右.
  • 二叉树:每个节点不能多于两个儿子.
    • 二叉树的平均深度要比N小得多 --- 为(O(根号(N))).
    • 二叉查找树的平均深度为O(logN).
  • 表达式树: 表达式树的叶子是操作数(operand), 其他节点是操作符(operator).
    • 中序就是中缀表达式;
    • 后序就是后序表达式;
  • AVL树是带有平衡条件的二叉查找树, 通过旋转(rotation)来保证树的平衡.
    • 插入发生在外边(左-左或右-右) --- 通过一次单旋来调整.
    • 插入发生在内部(左-右或右-左) --- 通过双旋来处理.
  • 伸展树(splay tree): 保证从空树开始任意连续M次对树的操作最多花费O(MlogN)时间.
    • 一颗伸展树每次操作的摊还代价是O(logN).
    • 当一个节点被访问后, 要经过一系列AVL树的旋转被放到根上. (适用于一个节点被访问,不久后会再次被访问).

B- 树

  • B- 树是常用的查找树, 但不是二叉树.
  • 阶为M的B-树具有下列特性:
    • 树的根或者是一片树叶,或者儿子数在2和M之间.
    • 除根外, 所有非树叶节点的儿子数在[M/2]和M之间.
    • 所有的叶子都在相同的深度上.
  • 4阶的B- 树被称为2-3-4树, 3阶的B- 树被称为2-3树.
  • B- 树实际用于数据库系统, 在哪里树被存储在物理的磁盘上而不是主存中.

散列

  • 散列表的实现常常叫做散列(hashing).
  • 散列是一种用于常数平均时间执行插入, 删除和查找的技术.
  • 散列最主要的是解决冲突(collision) --- 两个关键字散列到同一个值的时候.
  • 散列函数: 通常保证散列表的大小为一个素数.
    • 取余法: Key mod TableSize;
    • 尽量让键值分布均匀.
  • 解决散列冲突的方法:
    • 分离链接法;
      • 将散列到同一值的所有元素保留到一个表中.
    • 开放定址法;
      • 如果发生冲突, hash尝试选择另外的单元, 直到找出空的单元为止.
      • 线性探测法;
      • 平方探测法;
      • 双散列;
      • 再hash; --- 表满一半就再散列; 途中策略(middle-of-the-road) --- 当表到达某一个装载因子的时候再进行再散列.
  • 处理数据大以至于装不进主存的情况:
    • 可扩散列(extendible hashing) --- 允许用两次磁盘访问执行一次Find操作.
    • 用D代表根所使用的比特数, 称其为目录(directory), 目录中的项数为2^D, dL为树叶L所有元素共有的最高为的位数, dL <= D.
    • 如果不够存放插入的新关键字, 那么树叶会被分成两片新的树叶, 目录大小(比特的位数)将加1.
    • 这种简单的方法提供了大型数据库Insert操作和Find操作的快速存取时间.
  • 假设位模式(bit pattern)是均匀分布的
  • 目录的大小(2^D)为O(N^(1+1/M)/M), 如果M很小. 那么目录可能过分大.
    • 树叶包含指向记录的指针而不是实际的记录, 这样可以增加M的值.

优先队列(堆)

  • 优先队列(priority queue) 也称之为堆. --- 使用二叉查找树实现优先级队列.
  • 二叉堆(binary heap)具有两个性质:
    • 结构性; --- 堆是一个完全二叉树, 树高O(logN). --- 完全二叉树可以用一个数组表示而不需要指针.
      • i位置的元素, 其左儿子在位置2i上, 右儿子在左儿子后的单元(2i+1)中, 父亲在[i/2]上.
    • 堆序性;
      • 使操作快速执行的性质是堆序性(heap order), 最小堆需要最小元素在根节点上.
    • 对堆的一次操作可能会破坏掉这两个性质.
  • 二叉堆插入节点时, 需要执行上虑策略(percolate up), 直到新元素找出正确的位置.
    • 将新元素插入到满二叉树的末尾, 然后检查是否父节点比其大, 如果大的话就与父节点交换位置.
  • 删除需要一种下滤的策略(percolate down):
    • 删除根节点后, 需要将满二叉树的最后一个元素正确的放到堆中, 先放到根节点, 再继续和左右孩子进行比较, 选择小的进行交换.
  • 优先队列的应用:
    • 选择问题;
    • 事件模拟;
  • d-堆是二叉堆的简单推广, 所有的节点都有d个儿子;
    • 当优先队列太大不能完全装入主存的时候, d-堆是一个不错的选择.
  • 左式堆:
    • 左式堆(leftist heap)也具有结构特性和有序性, 左式堆也是一个二叉树.
    • 左式堆不是理想平衡的, 倾向于非常不平衡.
    • 左式堆的左儿子的领路径长至少与右儿子的领路径长一样大.
    • 左式堆倾向于加深左路径, 所以有路径应该短.
    • 左式堆最主要的应用是在堆的合并上.
  • 斜堆:
    • 斜堆(skew heap)是左式堆的自调节形式, 实现起来极其简单.
    • 斜堆与左式堆的关系类似于伸展树与AVL树之间的关系.
    • 斜堆具有堆序的二叉树, 但是不对树的结构进行限制.
  • 二项队列结构:
    • 一个二项队列结构, 不是一棵堆序的树, 而是堆序树的集合, 称为森林(forest).
    • 二项树的每一个节点将包含数据, 第一个儿子以及右兄弟, 二项树中的各个儿子以递减次序排序.

排序

  • 插入排序(insertion sort);
  • 希尔排序(shell sort)也称为缩小增量排序(diminishing increment sort).
  • 堆排序(heap sort)在实践中慢于Sedgewick增量序列的希尔排序.
  • 归并排序(mergesort): 通过递归实现.典型的分治策略(divide-and-conquer).
    • 每个递归调用, 局部都会声明一个临时数组.
  • 快速排序(quicksort)是实践中最快的已知排序算法, 快排也是一种分治的递归算法.
    • 基本步骤:
      • 如果中元素个数是0或1, 则返回.
      • 取S中任一元素v, 称之为轴点(pivot);
      • 将S-v(S中剩余元素)分成两个不相交的集合, S1和S2.
      • 返回quicksort(S1), v, 和quicksort(S2).
    • 随机选取枢纽元, 三数中值分割法.
  • 小数组快排没有插入排序快(N <= 20);
  • 桶式排序(bucket sort).
  • 外部排序(external sorting): 用来处理输入很大的数据.
    • 基本的外部排序是基于归并排序中的Merge过程.
    • 多路合并.
    • 多相合并.
    • 替换选择.

不相交集ADT

  • 等价关系:
    • 自反性;
    • 对称性;
    • 传递性.
  • 保持不相交集合是非常简答的数据结构.
  • 路劲压缩是自调整(self-adjustment)的最早形式之一, (伸展树和斜堆).

图论算法

  • 深度优先搜索(depth-first search)是一个重要的技巧.
  • 一个邮箱无圈图有时被称为DAG.
  • 如果无向图中从每个顶点到其他顶点都存在至少一条路径, 则称该无向图是连通的(connected).
  • 如果有向图具有这样的性质称为强连通(strongly connected).
    • 如果一个有向图不是强连通, 但是是基础图(underlying graph) --- 去掉方向所形成的图是连通的, 则称该有向图为弱连通(weakly connected).
  • 完全图(complete graph)是指每一对顶点之间都存在一条边的图.
  • 图可以通过邻接矩阵和邻接表进行表示.

拓扑排序

  • 拓扑排序是对有向无圈图的顶点的一种排序, 如果存在一条vi到vj的路径, 那么在排序中vj出现在vi的后面.
  • 拓扑排序并不是唯一的, 任何合理的排序都是可以的.
  • 找出任意一个没有入边的顶点, 显示出该顶点, 然后将它和它的边一起从图中删除.

最短路径算法

  • 单源最短路劲问题: 找到给定顶点s到G中每个其他顶点的最短路径.
  • 广度优先搜索(breadth-first search): 按层处理顶点, 距开始点最近的那些顶点首先被访问, 最远的点最后被访问, 有点像树的层次遍历.
  • dijkstra算法:
    • 每个顶点都要保留一个临时距离dv = 源点s到v的最短路径长.
    • 采用的是一种贪婪的策略(greedy algorithm). 贪婪算法一般分阶段求解一个问题, 在每个阶段它把当前出现的当作最优解去处理.
      • 贪婪算法不是总能成功.
    • 已知的邻接点的临时距离需要不断调整直到最小的值.
    • 可以写一个递归程序跟踪p数组留下的踪迹.
    • 对于稀疏图, 使用优先级队列对dijkstra最短路径算法进行优化.
  • 具有负边值的图, Dijstra算法是行不通的, 也不能向每条边增加一个常量值delta进行去负边.
  • 无圈图, 可以通过拓扑排序来改进Dijkstra最短路径算法, 即选择和更新在拓扑排序执行的时候进行.
    • 无圈图可以模拟某种滑雪问题;
    • 无圈图的一个更重要的用途是关键路径分析法(critical path analysis).
  • 所有点对最短路径问题, 多源多汇最短路径.

网络流问题

  • 解决最大流问题, 最简单的思路是分阶段解决, 构建一个残余流的图(residual graph), 对应残余边(residual edge).
    • 从残余图Gr中寻找一条s到t的一条路径, 称这条路径为增广通路(augmenting path).
    • 一旦注满一条边(使饱和), 则这条边就要从残余图中去除.
    • 最小费用流问题, 每条边不仅有容量, 而且每个单位流还存在价格, 在最大流中找一个最小价格的流来.

最小生成树

  • 最小生成树(minimum spanning tree), 无向图中找最小生成树比较容易, 在有向图中比较困难.
    • 无向图中的最小生成树就是由该图的那些连接G的所有顶点的边构成的图, 其总价值最低.
    • 包含途中所有顶点的最小的树.
  • Prim算法: 计算最小生成树时, 使其连续地一步步生成, 在每一步都要把一个节点当作根往上加边.
    • 每一步添加一条边和一个顶点到最小生成树上.
    • 与dijkstra最短路径算法类似.
  • Kruskal算法; 连续地按照最小的权值选择边(贪心的策略).

深度优先搜索

  • 深度优先搜索(depth-first search)是对先序遍历(preorder traversal)的推广.
  • 无向图是连通的.
  • 如果无向图任一顶点删除后, 图仍是连通的, 那么这个无向连通图称为双向连通的.
  • 如果一个图不是连通的, 删除一点后图不再连通, 那么这个顶点就叫做割点.
  • 欧拉回路:
    • 终点必须终止在起点上的欧拉回路只有当图是连通的并且每个顶点的度(边的条数)是偶数才又节能存在有效解.
    • 所有顶点的度(边的条数)均为偶数的任何连通图必然有欧拉回路.
    • 哈密尔顿圈问题(Hamiltonian cvcle problem) 是一个NP难问题.
      • 哈密尔顿圈问题是要找一个圈, 该圈包含所有的顶点.
  • NP完全性:
    • 存在大量重要的问题, 他们在复杂性上大体是等价的, 这一类问题被叫做NP完全(NP-complete)问题.
    • 这些NP完全问题精确度的复杂性仍然需要确定并且在计算机理论科学方面仍然是最重要的开放性问题.
    • 要么NP完全问题都有多项式时间解法, 要么都没有多项式时间解法.
    • NP(Nondeterministic polynomial-time)非确定型多项式时间.
    • NP中的任意问题都能多项式归约为NP完全问题.
    • NP完全问题是NP问题中最难的问题.
  • NP完全问题的例子:
    • 哈密尔顿圈问题是NP完全问题.
    • 巡回售货员是NP完全问题. 完全图小于K值的简单圈.
    • 最长路径问题.
    • 装箱问题(bin packing).
    • 背包问题(knapsack).
    • 图的着色(graph coloring)问题.
    • 团的问题(clique).

算法设计技巧

  • 求解问题的五种通常类型的算法.

贪婪算法

  • 贪心算法分阶段工作, 在每一个阶段, 认为锁决定是好的, 不会考虑将来的后果.
    • 一般得到的解都是局部最优解, 即次优解.
  • 任务调度问题;
  • Huffman编码 --- 文件压缩问题.
    • 代表字母的二进制代码用二叉树来表示, 每个字符通过从根节点开始用0指示左分支用1指示, 右分支而以记录路径的方法表示出来. --- 这种树叫做trie数.
    • 总价值最小的满二叉树.
    • 哈夫曼算法:
      • 算法对一个由树组成的森林进行;
      • 一棵树的权等于它的树叶的频率和.
      • 任意选取最小权的两颗树T1和T2, 并任意形成新树, 进行C-1次;
      • 存在C棵单节点树 --- 每个字符一颗树;
      • 算法结束时得到一颗树, 这棵树就是哈夫曼编码树.
      • 最优前缀码.

近似装箱问题

  • 联机算法:
    • 下项适合算法(next fit): 检查刚刚装进物品的箱子是否还能装.
    • 首次适合算法(first fit): 依次扫描箱子, 把新的物品放入足够空间的箱子.
    • 最佳适合算法(best fit): 把一个物品放到所有箱子中能够容纳它的最满箱子中.
  • 脱机算法:
    • 首次适合递减算法(first fit decreasing);
    • 最佳适合递减算法(best fit decreasing).

分治算法

  • 分治算法(divide and conquer)由两部分组成:
    • 分(divide): 递归解决较小的问题.
    • 治(conquer): 用子问题的解构造原问题的解.

动态规划

  • 动态规划(dynamic programming)解决数学递推公式的一种技巧, 当前状态与之前的状态相关.
  • 最优二叉查找树.

随机化算法

  • 一个随机化算法的最坏运行时间几乎总是和非随机化算法的最坏运行时间相同.
  • x(i+1) = Ax(i) mod M x(0)叫做随机种子.
  • 跳跃表(skip list): 是在单链表上的扩展, 每个节点具有k个指针(每个节点的阶是随机确定的), 可以跳跃式地指向其他节点.
  • 跳跃表类似于散列表, 它们都需要估计表中的元素个数(从而阶的个数可以确定);
    • 跳跃表如许多平衡查找树实现方法一样有效.

回溯算法

  • 回溯算法(backtracking)相当于穷举搜索的巧妙实现.
  • 收费公路重建问题.
博弈
  • 极小极大策略, 利用置换表(散列实现)节省大量的计算.
  • a-b裁剪(a-b pruning): 不需要进行求值的叫做a裁剪, 不会影响到min层的结果, 叫b裁剪.

  • 摊还界比最坏情形界要弱, 但比等值的平均情形要强, 摊还要考虑整个操作序列而不是仅仅一次操作.
  • 红黑树:
    • 每一个节点或者着色为红色, 或者着色为黑色.
    • 根是黑色的.
    • 如果一个节点是红色的, 那其子节点都必须是黑色的.
    • 从一个节点到一个NULL指针的每条路径必须包含相同的黑色节点数.
  • 1-2-3确定性跳跃表.
  • BB- 树是带有一个附加条件的红黑树: 一个节点最多可以有一个红儿子.
  • AA结构要求从颜色转换成层次.
    • 水平连接是同一层次上的儿子之间的连接.
  • treap树是一种二叉查找树, 像跳跃表一样使用随机数并且对任意输入都能给出O(logN)的期望时间性能.
  • k-d树.
  • 匹配堆. 最实用的斐波那契堆的变种, 具有兄弟指针, 前向指针(不代表父节点).

posted @ 2019-04-23 00:02 coding-for-self 阅读(...) 评论(...) 编辑 收藏