2 注:2025 DSA 的期末考试格外困难。但是我仍然认为这份笔记对我还是有所帮助的。总之也就看个乐子吧。

注:只是一个笔记,只看这个无法帮助你在 DSA 中获得正常的成绩,可能还需要一些往年题的 fine-tune.

misc:

  • 左旋的定义是把东西往左旋,右旋的定义是把东西往右旋
  • 二次探测法:对于哈希表,假如选得的位置是 \(x\),那么就看 \(x\)\(x+1\)\(x+4\)\(x+9\) 这样下去,对总 size 取模.
  • 双散列函数:对于哈希表,假如选的位置是 \(x\),并且第二个散列函数告诉你是 \(y\),那么就看 \(x,x+y,x+2y,\dots\) 下去,对总 size 取模.

内排序

Iterative style

  • 插入排序:从前往后,然后看 \(a_i\) 应该插入到 \(a'[1,i-1]\) 的哪里然后插进去,为稳定排序

    具体来说就是对于 \(i\),然后 \(j=i-1\to 1\) 然后每次如果逆序了就 swap,否则 break

  • 冒泡排序:不断(从前往后交换相邻逆序数),直到有序,为稳定排序

  • 选择排序:从前往后,找出后面的最小的放在这里,为不稳定排序

  • 堆排序:建大根堆,然后每次去除堆顶,为不稳定排序

  • 希尔排序:每次选一个 gap 然后将模 gap 同余的项做插入排序. 选 \(2^p3^q\) 为 gap 可以达到 \(O(n\log^2n)\),其他一些也可以做到 \(O(n^{<2})\). 为不稳定排序.

不需要访问已经排好序的记录:冒泡 & 选择

d&c style

  • 归并排序:为稳定排序
  • 快速排序:为不稳定排序

indexing

  • index array: \(Index_i=\) 第 i 小的值的位置

外排序

如果要排序的东西 RAM 塞不下那么就倒闭了,于是就需要外排序,大体就是说看看先能不能在有限的 RAM 范围内能够将整个序列给分成尽量多的有序段(run,顺串),然后再对这些段做 merge.

分段:

  • 假设我们最多能 load \(n\) 个元素
  • 那么就每次先 load \(n\) 个元素进堆,然后每次将最小值写入之后再读一个元素,如果元素比最小值大那么就可以塞入堆,否则就把这里面的元素给从小到大写完之后,结束这个 run.

merge:

  • two-way merge:直接 huffman tree

  • k-way merge:每次要找 \(k\) 个元素中的最小值.

    赢者树:一棵线段树,然后线段树每个节点记录最小值的位置

    败者树:一棵线段树,每个节点记录在合并儿子的时候更大的那个元素的位置。好处是替换掉最小值之后,更新的时候从下往上不需要读兄弟节点的信息.

    用这个执行 k-way merge 的话一次 merge 的复杂度是点数乘上 \(\log k\).

    然后再做类似 huffman 的贪心合并即可

查找 / 索引

散列表:

  • open hashing: 使用分离链表,每个哈希值维护一个链表
  • close hashing: 撞了之后按一个固定的方式找下一个. 注意这个不能支持直接删除,只能隐式删除.

B 树

考虑要维护动态插入/删除的集合,每个元素是一个 (key, address) 的 pair,然后需要支持给 key 查 address.

结构:

  • 每个节点存储 \(k_i\) 个元素 \(a_{i,1}\dots a_{i,k_i}\),然后有 \(\le k_{i}+1\) 个儿子,其中第 \(i\) 个儿子的子树记录所有 key 在 \((a_{i-1},a_i)\) 间的元素.
  • 大概可以用一个 [儿子指针,元素,儿子指针,...,儿子指针] 的一个 list 来表示一个点.
  • \(m=\max k_i+1\). \(m=3\) 的时候称为 \(2-3\) 树,因为每个节点只有 \(2\)\(3\) 个儿子.
  • 非退化情况下,根至少要有两个儿子. 并且每个点至少有 \(\lceil\frac{m}{2}\rceil-1\) 个元素. 并且每个叶节点深度相同.

插入:

  1. 先索引到自己适合插入的那个叶子. 如果叶子再多一个 item 还是 \(k_i\le m-1\) 那么就不用管了.
  2. 否则取出节点的中位元素,然后把这个节点按中位元素 split 成两个. 然后把这个中位元素给插到父亲节点里面.
  3. 如果插到父亲节点里面之后父亲节点元素数量没爆那么就 ok 了. 如果爆了,那么就需要对这个节点跑第二步,然后不断这样做.
  4. 最终如果跑到根节点然后根节点爆了,那么就需要建一个新的根就可以了. 新的根的儿子数量为 \(2\).

删除:

  1. 对于 \(x\),找到自己的后继 \(x'\),然后把 \(x'\) 换到自己的位置上即可删除.
  2. 带来的一个问题是 \(x'\) 所属叶子可能会元素数不够 . 此时我们看它的兄弟.
    • 如果兄弟的元素数 \(\ge \lceil\frac{m}{2}\rceil\) 那么就直接把兄弟的最值移到父亲,然后再把父亲的前/后驱给挪过来这样.
    • 如果兄弟的元素数小,那么就可以直接带上父亲的那个划分元素给合并到一个节点,这个节点的元素显然满足要求. 但是这可能导致父亲元素数不够,于是就跳到父亲节点然后执行 2.

B+ 树

和 B 树的区别是只有叶节点存元素 (leafy),设儿子数量为 \(d_i\),则每个节点需要存 \(d_i\) 个数,每个数表示该儿子的 key 的最大值.

插入:

  1. 同样,索引到自己应该放的叶节点然后丢进去.

  2. 如果没问题的话就一路往上走更新(如果需要更新的话)每个节点存的那个表示最大值的数.

  3. 如果爆了那就分成两个节点,这会使得父亲节点需要存储的值多一个. 然后就对父亲回到 2.

  4. 同样如果到了根那么可能需要建新根.

删除:

  1. 这里删就直接删,然后没问题的话还是需要一路往上更新.
  2. 同样如果个数少了就从兄弟拉一个过来. 如果兄弟没法再拉东西过来了就可以合并.
  3. 需要注意任意节点发生变动都要去 pushup.

一个变种是使用 B+ 树的 leafy 的思想,但是在非叶节点采用 B 树的方式,这样可以少记录很多冗余值,并且也不需要那么频繁地 pushup.

由于 B+ 树的叶节点实际上是不需要存储的,所以这个 leafy 结构带来的优势就是空间小. 另一方面 B 树的优势是的深度稍微小一些.

高级数据结构

广义表

考虑链表的每个元素,可以是一个原子元素,也可以是一个链表.

这样的链表可以当作邻接表看待.

  • 线性表:链.
  • 纯表:树.
  • 可重入表:图.
  • 循环表:有环的图.

AVL

定义:每个点的左子树和右子树高度最多差 1.

balance factor \(bf(x)\) 为右子树 - 左子树高度.

插入:

  • 正常查找然后插入一个叶子

  • 从下往上 check 每个节点符合要求. 三种情况:

    1. 当前节点新 \(bf=\pm 1\),那么继续 check 父亲

    2. 当前节点新 \(bf=0\),那么就不需要管父亲了.

    3. 当前节点新 \(bf=\pm 2\),那么就需要在这里旋转一下. 由于肯定是新加的点来自于那个多的子树,不妨设来源于右子树.

      如果来源于右子树 \(r\) 的右儿子 \(r_r\),那么直接把 \(r\) 旋转上去即可. 如果来源于右子树的左儿子 \(r_l\),那么就需要把 \(r_l\) 从下往上转两步. 然后就不需要再 check 父亲了.

红黑树

大家都去学 fhq-treap 好不好啊.

结构:

  • 每个点被染成红色和黑色
  • 不存在相邻红点
  • 对于每个点,到子树内所有外部节点所经过的黑点数量一致,称为这个点的 rank

插入:

  1. 直接做 BST 插入,然后将新的点染成红色.
  2. 由于直接插入后不存在子节点需要考虑. 只需要考虑和父节点的关系.
  3. 如果父节点是黑色就什么事都没有. 如果父节点是红色的话需要进入到一个分类讨论:
    • 如果父亲的兄弟是红的,那么直接将祖父变成红,父&父兄弟变成黑,然后对祖父继续做 3.
    • 如果父亲的兄弟是黑的,由于此时父亲的兄弟&自己的兄弟都是黑,而 (自己,父亲,祖父) 形成一条红红黑的链,于是考虑旋转,把这三者的中位数转成这三者的根,然后根为黑,根的两子为红. 由于剩下的周围节点都是黑所以这样做没有问题.

删除:

  1. 删除的话首先对于节点 \(x\),如果度数 <2 直接删,否则和右子树最小的点交换然后删
  2. 如果删除节点是红色那么 p 事没有,但是如果是黑色那么就需要调整,使得删除之前其唯一子树的 rank 要增大一然后才能放心删.
  3. 然后我们就关注这个“让rank保持平衡的操作”怎么弄(并且不会让自己变成红色). 分若干种情况:
    1. \(x\) 儿子是红色的:那么直接把这个儿子染成黑色.
    2. \(x\) 无红色儿子,且兄弟是红色的:把兄弟转上去,并且将兄弟染成红,转下来的父亲染成黑. 此时归约到兄弟是黑色的情况.
    3. \(x\) 无红色儿子,兄弟是黑色的,没有红色侄子:直接把兄弟染成红色. 此时,如果父亲本来就是红色那么染成黑色,如果本来就是黑色那么就要对父亲重复 3.2 / 3.3 / 3.4 了.
    4. \(x\) 无红色儿子,兄弟是黑色的,有红色侄子. 如果存在父亲-黑色兄弟-红色侄子的一条顺着的链,那直接把黑色兄弟给转上去就行了. 否则就需要把红色侄子转上去. 注意在赋颜色时,父亲位置的颜色要保持不变. 这种情况下也都不需要对父亲重复修复.

建议根据上述规则手搓一下.

AI 产物:

  • :叔红变色,叔黑旋转
  • :兄红转黑;兄黑侄黑则父债子偿;兄黑侄红则借位填坑