堆排序

简单选择排序的优化
结合了插入排序和归并排序的优点
原地排序
不稳定

存储结构:数组
堆的顺序表示法:二叉树的层序遍历
二叉堆,完全二叉树
大顶堆:每个节点的值 ≥ 其子节点的值

下标从0开始
父节点:(i-1)/2
左子节点:2i+1
右子节点:2i+2

  1. 建堆,简化/最简单
    根节点的左右子树都是堆
    只有根节点不满足堆

  2. 下沉
    将根节点与左右孩子比较
    若不满足堆
    则交换根与较大的结点
    一直到叶子结点
    或所有子树均为堆为止

  3. 倒数第一层一定是堆
    倒数第二层不一定满足堆
    从下向上,下沉,建堆

  4. 建堆
    从最后一个分支结点(n / 2 - 1)开始建堆
    直到根节点为止
    是 O (n) 而不是 O (n logn)

  5. 排序
    根节点和最后一个元素交换(原地排序)
    队尾减1(排序n-1次)
    根节点的左右子树都是堆
    只有根节点不满足堆
    下沉(调整成堆)

  6. 上浮
    当前节点与父节点比较
    若不满足堆
    则交换当前节点与父节点
    一直到堆顶
    或恢复成堆

  7. 对比
    上浮
    新元素插入堆尾
    或大根堆中某个元素的值增大
    下沉
    初始建堆
    或堆顶被移除
    或大根堆中某个元素的值减小

1→3,3→4
2→4
2→5

每一趟选择最大元素
由简单选择排序的 O(n)降为 O(logn)

上浮和下沉都是 O (logn)

二路归并排序(非递归,自底向上)

分治法
稳定
缓冲区

  1. 两个有序序列合并成一个有序序列C
    逐一比较
    较小者放入 C
    如果相等,则取靠前的序列(稳定)
    取出较小者的下一个元素再次比较
    直到其中一个序列的元素全部放入 C
    再将另一个序列的元素放在 C 的尾部

  2. 一趟归并排序
    合并相邻序列
    三种子序列合并情况
    i 为相邻序列中的第一个元素的下标

    1. i <= n - 2h
      至少还有两个长度为 h 的序列

    2. i > n - 2h && i < n - h
      只有两个子序列
      右子序列长度小于 h

    3. i >= n - h
      只有一个子序列

  3. 完整的归并
    两个数组相互归并
    每进行一趟归并排序后
    子序列长度 h 翻倍
    直到子序列长度 h >= 总长度n
    (临时数组归并到到原数组时
    h >= n 可以
    但原数组归并到临时数组时不行)

logn 趟排序
每趟排序都要划分成两半
二叉树高度 logn取整 + 1

每趟排序 O(n)
每趟排序都要归并所有元素

二路归并排序(递归,自顶向下)

  1. 合并
    参考非递归的合并

  2. 划分
    int middle = left + (right - left) /2
    与数组【middle + 1......right】相比
    数组【left......middle】多0到1个元素

  3. 排序 r,结果 r1
    r 划分成左右子列
    分别排序左右子列到 r2(递归)
    将 r2 的左右子列 合并 到 r1

  4. 递归终止
    当区间长度为 1 时
    left == right
    数组本身已有序
    直接将 r[left] 复制到 r1[left]

  5. 调用
    待排数组和结果数组都是r
    (结果存回 r)
    (最终排序结果直接覆盖原始数组r)

栈深 logn
每趟排序都要划分成两半

每层的归并 O (n)
每层都要遍历所有元素

快速排序(Hoare 分区)

起泡排序的优化(相邻 → 两端向中间)
不稳定
递归树

交换较少
随机数据性能更优
对已排序数组性能较差

  1. 基准
    left 作为基准
    分区
    将基准放在左右指针上
    返回基准元素的位置

  2. 分区
    左指针初始化:left
    右指针初始化:right
    重复以下操作直至左右指针相等

    • 右指针向左找小于基准的元素
      赋值覆盖左指针指向的元素
    • 左指针向右找大于基准的元素
      赋值覆盖右指针指向的元素
  3. 递归
    参考lomuto分区的递归

快速排序(Lomuto 分区)

交换较多
减少有序数组的最坏情况概率

  1. 基准
    middle 作为基准
    int middle = left + (left - right) / 2
    分区前 交换 right 与 middle
    分区后 交换 right 与 A 的下一个位置
    返回基准值的索引

  2. 分区
    小于等于区域:A
    i 为 A 的最后一个元素的索引
    初始为 left - 1(升序排序)
    遍历 [ left, right - 1 ]
    若当前元素小于等于基准值
    扩展 A
    交换 A 的最后一个元素 与 当前元素

  3. 排序 [ left, right ]
    分区,获取基准索引 pivot
    排序 [ left, pivot - 1 ]
    排序 [ pivot + 1, right ]

  4. 递归终止
    当区间长度为 1 时
    left == right 时直接返回

递归深度 O(log n)
分区 O(n)

希尔排序

直接插入排序的改进

  1. 增量序列

    • 原始序列
      n/2, n/4, ..., 1

    • Hibbard 序列
      2^k - 1

    • Knuth序列
      递归:h = 3h + 1
      迭代:(3^k - 1) / 2

    • Sedgewick序列
      9×4^k - 9×2^k + 1
      4^k - 3×2^k + 1
      1, 5, 19, 41, 109, 209, 505……

    • 斐波那契序列

  2. 划分子集
    数组被分为 gap 个独立的子序列
    子序列的元素索引间隔为 gap
    对每个子序列执行插入排序

  3. 插入排序

    1. 法一
      对一个子序列插入排序后
      再对下一个子序列插入排序

    2. 法二
      先依次对每一个子序列的第二个元素插入排序
      再依次对每一个子序列的第三个元素插入排序
      接着是第四个,第五个……

直接插入排序

稳定
适合待排序列基本有序
正序时最好,O(n),不用移动
逆序时最差

  1. 整体思路
    无序区 pop_front
    有序区 upper_bound
    有序区 vector 的 insert 单个元素
    重复以上操作直至无序区为空

  2. n - 1 趟循环
    从 r[1] 开始插入
    逐步扩展为 r[n - 1]

  3. 边查找边后移
    从后向前(稳定)
    顺序查找

折半插入排序/折半查找

有序表
判定树是平衡二叉树

简单选择排序

  1. n - 1 趟循环

    • 第 i 趟循环选择出第 i 小的
      A:已排序区
      B:待排序区
  2. 每趟循环的整体思路
    B 找最小值的索引 min
    swap ( B[min], B.front() )
    B.pop_front()
    A.push_back()

  3. 找第 i 小的元素
    设 B 中第一个元素最小
    依次与 B 中的剩余元素比较
    查找 B 中最小的元素的位置

  4. 交换无序区最小元素与无序区第一个元素
    当 min != i 时
    r [ min ] 与 r [ i ] 交换
    相当于从 B 中取出最小的元素
    加入 A 中

O(n²)
不稳定

改进的冒泡排序

稳定

  1. 改进
    bound:有序区第一个元素
    pos:找本趟排序的 bound
    本趟排序前,将上一趟的 pos 赋值给 bound
    firstpos
    前一趟排序,交换开始位置的前一个位置
    本趟排序,交换开始位置

  2. 改进前后对比

    • 改进前:每次都将无序区最大的元素加入有序区
      改进后:每次都将至少一个元素加入有序区
      包含无序区最大的元素

    • bound 防止后部排好的元素重复比较
      firstpos 减少前部的比较次数

    • 改进前:循环 n - 1 次
      改进后:循环 直至本趟排序结束后 pos == 0

  3. 相邻交换(稳定)
    如果 pos == 0,则更新 firstpos
    更新 pos

双向冒泡排序

计数排序

O(n + k)
稳定

待排序数组A
2,3操作对应 计数数组B
结果数组C

  1. min, max
    O (n)

  2. 各个数值的个数
    O (n)
    创建大小为 max - min + 1 的 B
    元素大小 x,则 B 的索引 x - min

  3. 不大于 x 的个数
    O (k)

  4. 反向遍历
    O (n)
    从后向前遍历A(保证稳定性)
    A [ i ] 对应 C 的索引 j
    C [ j ] 对应 B 的索引 k
    B [ k ] = A [ i ]
    C [ j ]--

桶排序(ai版本)

O (n + k + nlog(n/k))

  1. min, max
    O (n)

  2. 桶数
    如果元素数量 <= 100,桶数量 = 元素数量
    如果元素数量 > 100,桶数量 = 元素数量开平方

  3. 每个桶的范围
    range = (max - min) / bucketCount

  4. 创建桶
    vector<vector<T>> 类型

  5. 分配
    O (n)
    元素大小 x,则桶索引 (x - min) / range
    若为最大值,桶索引要减一

  6. 桶内排序
    O (n log(n/k))
    决定了是否稳定

  7. 遍历,覆盖
    O (n + k)
    遍历桶,如果桶不空
    将元素覆盖在原数组

桶排序(课本版本)

O (n + k)
适合整数,连续
稳定

  1. min, max
    O (n)

  2. 桶数
    最大值 - 最小值 + 1

  3. 创建桶
    vector<list<T>> 类型

  4. 分配
    O (n)
    元素大小 x,则桶索引 x - min
    插入桶的尾部(保证稳定)

  5. 遍历,拼接
    O (k)
    从前向后遍历桶,如果桶不空
    将其 splice 到 result 的末尾

    • 从后向前遍历桶,如果桶不空
      将其 splice 到 result 的头部 也行

    • result 是 list<T> 类型

基数排序(LSD)

O(d*(n + k))
k 一般是 10
稳定

  1. 找最大值 和 最小值

    • 如果事先能确定一定是非负数
      则不需要找最小值
  2. 最大位数

  3. 从最低位到最高位
    逐位进行 计数排序

    1. 不需要找最值

    2. B 的大小为 10

    3. 第 d 位为 x ,则 B 的索引 x

各种排序的比较和选择

简单排序

  1. 分治:冒泡,直接插入,简单选择
    分为已排序区和待排序区

  2. 原地排序:冒泡,直接插入,简单选择

  3. 基本有序:直接插入,O(n)时间

  4. 大型对象:简单选择,O(n)次移动

复杂排序


  1. 快速排序(基本有序)

  2. 原地
    堆排序
    希尔排序
    归并排序(链表)

  3. 稳定
    归并排序

  4. 最大/最小的 n 个数
    堆排序

  5. 合并两个有序数组
    归并排序

非比较排序

  1. 都是非原地排序

  2. 都要找最值

  3. 分布均匀
    桶排序(ai 版本)

  4. 小数
    桶排序(ai 版本)
    基数排序

    • 只适合小数位数固定
      要转换成整数再排序
      不如桶排序
  5. 负数
    计数排序
    桶排序
    基数排序

    • 正负分离 + 负数子数组反转
  6. 极差较小
    计数排序

  7. 极差较大
    基数排序

  8. 最大/最小的 n 个数
    桶排序
    计数排序(稍作修改)

  9. 尾部的 n 个数的排名
    计数排序

  10. 某个元素的个数
    计数排序

Floyd

多源
任意两个顶点间的最短路径

dijkstra

无负权
有向图
单源
求一点到各点的最短路径和长度

性质

  1. 子路径最优性(最优子结构)
    最短路径上任意两点之间的路径都最短

  2. 最优子结构体现为
    从源点到某个顶点的最短路径
    一定包含路径上其他顶点到源点的最短路径

  3. prev数组
    以源点为根的最短路径树

  4. Q 为已确定最短路径的顶点集合
    迭代中,顶点 a 暂未加入 Q
    则 dist[a] 满足:

    • 该路径仅由两部分组成
      源点到 Q 中某顶点 u 的最短路径(已确定)
      边(u,a)

    • 在当前集合 Q 下
      源点到 a 的最短可能路径

  5. 每次确定的顶点 u 的距离 d[u]
    一定是最短路径长度

    • 若存在更短路径
      该路径必然经过某个未确定的顶点 v
      但 v 的距离 d[v] 至少为 d[u]
      (否则 v 会在 u 之前被选择),
      而无负权边无法让路径变短,故矛盾
  6. 按照顶点到源点的最短路径长度
    从小到大的顺序,
    依次确定顶点

算法流程

  1. 初始
    邻接矩阵
    顶点数n
    源点s:dist[s] = 0
    其他顶点v:dist[v] = MAX
    (表示不可达)
    数组 visited 初始均为 false

  2. 迭代
    重复以下操作 n 次
    (每次确定 1 个顶点
    总共 n 个顶点)

    • 选顶点
      从所有未标记的顶点中
      选择 dist[v] 最小的顶点 u
      此时 u 的最短路径已确定

    • 更新距离
      遍历 u 的所有邻接顶点 v
      若通过 u 到 v 的距离小于 dist[v]
      则更新 dist[v]

  3. 长度
    数组 dist: 源点到各顶点的最短路径长度

    • 长度为0:源点到源点,跳过
    • 长度为MAX:不可达
    • 长度∈(0, MAX):源点到可达顶点
  4. 路径

Kruskal

无向图
最小生成树

  • 包含所有顶点
    顶点数 - 1 条边
    权重之和最小

离散数学知识

  1. 等价关系
    自反,对称,传递
    集合S上的二元关系R

  2. 等价类
    两个等价类要么相等
    要么没有交集
    所有等价类的并集为集合S

  3. 商集S/R
    等价类的集合
    集合的集合
    集合S的一个划分

  4. 规范映射/投影映射
    π: S→S/R, π(x)=[x]

数学与编程

  1. 一个连通分量:一个等价类

  2. 并查集初始化
    每个元素独立成一个集合:每个元素都是一个等价类
    每个元素的根节点是自己:自反性

  3. find函数
    根节点:等价类代表元素
    返回根节点:规范映射
    路径压缩:传递性

  4. unite函数
    根是否相同:是否在同一个等价类
    合并集合:合并等价类

算法流程

  1. 初始化
    边的数量
    顶点数量
    三元组表(顶点1,顶点2,权重)
    初始化并查集数组

  2. 排序
    edge结构体,重载比较运算符
    所有边按权重从小到大排序

  3. find函数(迭代)

  4. find函数(递归)

    • 查找结点 a 的根节点
    • 如果结点 a 不是根节点
      • 递归查找其父节点的根节点
      • 路径压缩
        • 结点 a 的父节点变成根节点
        • 后续查询效率接近 O (1)
    • 如果结点 a 是根节点
      • 递归终止
      • 直接返回 a
  5. unite函数

    • 查找 a 和 b 的根节点 ra 和 rb
    • 若根节点相同
      • 则两集合合并失败
    • 若根节点不同
      • 将 ra 的父节点设为 rb
        • 反过来也行
        • 并查集:通过树结构表示集合
      • 则两集合合并成功
  6. 遍历边
    若添加的边不会形成环
    则依次添加边到生成树中,
    直到添加 n-1 条边
    或所有边处理完毕

判断是否成环

  1. 并查集
    每个集合由一棵树表示
    无向图,若两个节点属于同一集合
    则添加连接它们的边会形成环

判断图是否连通

  1. 是否为n - 1 条边

  2. 并查集
    任选一个节点的根作为基准
    判断剩下的结点的根是否与基准相同
    若不相同,则图不连通

  3. 任选一个节点作为起点
    BFS 或 DFS 遍历所有可达节点
    若访问的节点数等于总节点数
    则图连通;否则不连通。

时间复杂度由排序决定
O (ElogE),E 是边的数量

Kruskal与其他算法

  1. find函数(迭代)
    • 找到根节点
      int root = x;
      while (parent[root] != root) {
        root = parent[root];
      }
      
    • 将路径上所有节点的父节点直接设为根
      • 保存当前节点的父节点
        将当前节点的父节点设为根
        处理原父节点

  1. dijkstra的路径
    int pre = i;
    string path = G.name[pre];
    while (Path[pre] != -1) {
        pre = Path[pre];
        path = G.name[pre] + "->" + path;
    }
    cout << path << endl;
    

Kahn,CPM

有向图

数学知识补充

  1. 拓扑序列:
    对于有向边 u → v
    u 在排序结果中一定位于 v 之前

  2. 有向图存在拓扑序列,
    ←→图是有向无环图

算法流程

  1. 初始化
  • 节点数
  • 边数
  • 邻接表
    • 任务开始的交接点编号小者优先
      起点编号相同时
      与输入时任务的顺序相反
    • 邻接表保持边的插入顺序
      逆序遍历即可
    • 邻接矩阵不存储边插入顺序
      需额外记录边顺序
  • 入度数组
  • 最早发生时间ve
  • 最晚发生时间vl
  • 输入边(顶点1,顶点2,权重)
  • 计算所有顶点的入度
    • 输入一条边
      顶点2的入度 +1
  1. 拓扑排序
    所有入度为 0 的顶点入队
    每次从队列中取出一个顶点
    加入拓扑序列
    邻接顶点的入度减 1
    若邻接顶点入度变为 0,入队
    若拓扑序列的顶点数等于图的总顶点数,则无环
    否则存在环

    • 未被排序的顶点形成环
  2. 最早发生时间ve
    初始化ve数组全为0
    ve[j] = 所有前驱节点中 max(ve[i] + weight(i→j))

    • 翻译成代码
      遍历 i 的所有后继节点
      对于某一个后继节点 j
      ve[j] = max(ve[j], ve[i] + weight(i→j))

    • 思想类似于
      稀疏矩阵的乘法
      向量的加法和数乘

  3. 最晚发生时间vl
    初始化vl数组为工程总时间T
    (所有ve的最大值)
    vl[i] = 所有后继节点中 min(vl[j] - weight(i→j))

    • 翻译成代码
      遍历 i 的所有后继节点
      对于某一个后继节点 j
      vl[i] = min(vl[i], vl[j] - weight(i→j))
  4. 计算方向
    ve:拓扑正序
    vl:拓扑逆序

    • 要计算节点i的vl
      必须先知道所有后继节点j的vl值
      拓扑逆序保证在计算节点i时
      其所有后继节点都已计算完成

    • ve数组的计算
      拓扑排序
      可以放在一起/同时进行

  5. 关键活动
    判定:对每条边(i→j):
    ve[i] == vl[j] - weight

    • 活动a:边(i→j)
      最早开始时间 e (a) = ve[i]
      最迟开始时间 l (a) = vl[j] - weight(i→j)

    • 关键活动:e(a) = l(a)
      活动 a 没有缓冲时间
      必须按时完成
      否则会延误整个项目

kahn:
有向图拓扑序列
有向无环图DAG

  • 并查集:无向无环图

dfs

bfs

单向静态链表

    • 将节点归还至空闲区
      nodes[nodeIndex].next = freeList;
      freeList = nodeIndex;
      
    • 头插法
      nodes[newNode].next = head;
      head = newNode;
      
    • 从空闲区获取节点
      int newNode = freeList;
      freeList = nodes[freeList].next;
      return newNode;
      
    • 删除
      int temp = head;
      head = nodes[head].next;
      releaseNode(temp);
      
posted on 2025-06-20 20:09  2024211826  阅读(11)  评论(0)    收藏  举报