引言

最近笔者正在优化 Android 开源代码编辑器项目 TextWarrior 的一些算法,包括时间、空间两方面。TextWarroir 的文本编辑器算法采用经典的 GapBuffer,其基本思想是利用编辑时的局部性原理,在光标处维护一个缓冲区,实现高效替换。

但是笔者需要对其代码高亮、自动断行等功能用到的标记数组进行优化:

  • 原编辑器的代码高亮标记数组直接采用差分数组存储其文本下标,好处是文章内容频繁更新时差分数组可以只需要改动其中一两个元素的值便能导致后面整体的改动,缺点是查找某个下标时需要从头开始,时间复杂度为 \(O(N)\)。
  • 原编辑器的自动断行标记数组直接存储文本下标,好处是定位时可以采用二分查找,但是当文本改动时需要对整个数组光标处的后半段进行修改,时间复杂度 \(O(N)\)。

不管是差分数组还是直接存下标,貌似都有其缺陷。那有没有一种两全其美的方法呢?答案是有的。这要从 GapBuffer 说起。

GapBuffer 基本思想

GapBuffer 又叫间隙缓冲区,是一种文本编辑器算法,主要对编辑器中频繁的字符串插入、删除操作进行优化。

我们知道,对于中间插入,数组的时间复杂度为 \(O(N)\),而链表的时间复杂度为 \(O(1)\)。字符串常常用数组的方式存储,若采用链表,每个字符都会附带一个指针指向下一个字符结点,数据冗余度很高。而 GapBuffer 则实现了对于数组高效的中间插入删除。

GapBuffer 利用编辑时的局部性——编辑操作在一段时间内往往集中在最初光标附近,换位置的情况比较少这一事实——进行优化。GapBuffer 在原字符串数组光标附近维护一个缓冲区(即所谓间隙缓冲区,后文简称间隙),并通过双指针限定该间隙的范围。

由于间隙内的内容实际不可见,当我通过字符串索引获取字符时,需要跳过间隙,此时存在一个下标映射:将获取字符时的逻辑下标映射到所维护字符数组的实际下标。

基本操作

  • 局部性编辑:当光标位于间隙开始位置时,输入时直接将输入内容从间隙开始位置拷贝到间隙,并后移起始指针;删除时,直接前移起始指针。此时时间复杂度为 \(O(1)\)。

  • 跨域编辑:若出现跨域编辑,即此时光标位置不在间隙开始处,则需要进行整体复制,以将间隙移动到当前光标处,再进行相关操作。时间复杂度 \(O(m)\),其中 \(m\) 表示间隙区移动的距离。

  • 若插入时间隙所剩空间不足,则需要进行扩容,并把间隙后的字符全部向后整体复制。时间复杂度 \(O(k)\),其中 \(k\) 为间隙之后的部分数组长度。

插入操作示例:

插入前
|H|e|l|l|o| | | | |W|o|r|l|d|
^ Gap (size=4)
插入后
|H|e|l|l|o|!| | | |W|o|r|l|d|
^ Gap (size=3)

删除操作示例:

删除前
|H|e|l|l|o|!| | | |W|o|r|l|d|
^ Gap (size=3)
插入后
|H|e|l|l|o|!| | | |W|o|r|l|d|
^ Gap (size=4)

基于下标映射的标记记录法

既然 GapBuffer 采用下标映射实现实际下标和逻辑下标的转换,而在编辑的过程中,某个字符的逻辑下标往往是不断变动的,而其实际下标则要稳定得多,因此完全可以记录实际下标实现高效率的标记管理。

记录实际下标,即记录标记在原字符数组中的下标,当间隙发生变动时维护下标的映射关系。可以对比逻辑下标和差分下标,实际下标+映射方式兼具二者有点同时避免了各自的缺陷。

下标映射

在需要访问下标时,会用到 GapBuffer 的下标映射函数将记录的实际下标转为逻辑下标再返回,而增加记录时会把逻辑下标转为实际下标进行记录。

private ArrayList<Integer> records;

private int mapToReal(int i) {
return i < gapStart ? i : i + gapLength();
} private int mapToLogical(int i) {
return i < gapEnd ? i : i - gapLength();
} public void getMark(int i) {
return mapToLogical(records.get(i));
} public void addMark(int i) {
records.add(mapToReal(i));
}

搜索

在需要进行查找时,只需要将逻辑下标转为实际下标并应用二分查找即可,时间复杂度 \(O(\log N)\),继承了记录逻辑下标的优点,而记录差分下标则必须从头遍历累加。

public int findMark(int i) {
return Collections.binarySearch(records, mapToReal(i));
}

维护

由于记录为实际下标,因此维护需保证与 GapBuffer 的一致性。对于间隙维护的三种情况均需考虑,其时间复杂度也和三种情况基本对等:

  • 局部性编辑:在间隙开头插入时,如果间隙不需要扩容,则记录不变,如果是删除,检查并处理实际下标落入间隙区中的下标,移动或删除,平均时间复杂度 \(O(1)\)。由于间隙发生了改变,虽然实际下标没有改变,但映射函数的参数发生变化,因此映射到的逻辑下标会变化。

  • 跨域编辑:在间隙以外的地方插入或删除,此时只需检查移动区间内的下标并加或减去间隙长度,再同上处理插入删除情况,时间复杂度 \(O(m)\),此处 \(m\) 为移动区间内标记数量。

  • 间隙扩容:当间隙大小不足插入时需要进行扩容,此时需要将间隙之后的所有标记加上扩容量,时间复杂度 \(O(k)\),此处 \(k\) 为间隙之后的标记数量。

实际下标记录的维护在满足局部性的情况下时间复杂度为 \(O(1)\),与差分下标记录同级,同时避免了逻辑辑下标记录的不足。当然,实际下标记录的维护难度要比二者大一些。

对比

下标记录方法 逻辑下标 差分下标 实际下标
访问 直接读取O(1) 前缀和O(n) 线性映射O(1)
搜索 二分O(logN) 线性O(n) 二分O(logN)
维护 O(k) O(1) O(1),最坏O(k)

总结

本文讨论文本编辑器经典算法 GapBuffer 的标记记录优化方案,利用算法中的局部性思想提出配套的基于映射的下标记录算法,并对比了 TextWarrior 用到的两种记录方案,表现出该方法在时间复杂度上的优势。另外,局部性原理也是很多算法的依据,在计算机软硬件设计很多地方都有所体现,值得研究。希望本文为读者提供一些参考帮助。


原文链接:https://www.cnblogs.com/RainbowC0/p/18805061 ,未经作者许可禁止转载。

GapBuffer高效标记管理算法的更多相关文章

  1. 自动内存管理算法 —— 标记和复制法

    最近阅读了<垃圾回收算法手册>这本经典的书籍,借此机会打算写几篇内存管理算法方面的文章,也算是自己的总结吧.                                         ...

  2. JVM内存管理------GC算法精解(复制算法与标记/整理算法)

    本次LZ和各位分享GC最后两种算法,复制算法以及标记/整理算法.上一章在讲解标记/清除算法时已经提到过,这两种算法都是在此基础上演化而来的,究竟这两种算法优化了之前标记/清除算法的哪些问题呢? 复制算 ...

  3. JVM内存管理之GC算法精解(复制算法与标记/整理算法)

    本次LZ和各位分享GC最后两种算法,复制算法以及标记/整理算法.上一章在讲解标记/清除算法时已经提到过,这两种算法都是在此基础上演化而来的,究竟这两种算法优化了之前标记/清除算法的哪些问题呢? 复制算 ...

  4. JVM内存管理------GC算法精解(五分钟让你彻底明白标记/清除算法)

    相信不少猿友看到标题就认为LZ是标题党了,不过既然您已经被LZ忽悠进来了,那就好好的享受一顿算法大餐吧.不过LZ丑话说前面哦,这篇文章应该能让各位彻底理解标记/清除算法,不过倘若各位猿友不能在五分钟内 ...

  5. JVM内存管理之GC算法精解(五分钟让你彻底明白标记/清除算法)

    相信不少猿友看到标题就认为LZ是标题党了,不过既然您已经被LZ忽悠进来了,那就好好的享受一顿算法大餐吧.不过LZ丑话说前面哦,这篇文章应该能让各位彻底理解标记/清除算法,不过倘若各位猿友不能在五分钟内 ...

  6. (转)jvm具体gc算法介绍标记整理--标记清除算法

    转自:https://www.cnblogs.com/ityouknow/p/5614961.html GC算法 垃圾收集器 概述 垃圾收集 Garbage Collection 通常被称为“GC”, ...

  7. 1. GC标记-清除算法(Mark Sweep GC)

    世界上第一个GC算法,由 JohnMcCarthy 在1960年发布. 标记-清除算法由标记阶段和清除阶段构成. 标记阶段就是把所有的活动对象都做上标记的阶段. 标记阶段就是"遍历对象并标记 ...

  8. GC算法精解(五分钟让你彻底明白标记/清除算法)

    GC算法精解(五分钟让你彻底明白标记/清除算法) 相信不少猿友看到标题就认为LZ是标题党了,不过既然您已经被LZ忽悠进来了,那就好好的享受一顿算法大餐吧.不过LZ丑话说前面哦,这篇文章应该能让各位彻底 ...

  9. JVM之GC算法、垃圾收集算法——标记-清除算法、复制算法、标记-整理算法、分代收集算法

    标记-清除算法 此垃圾收集算法分为“标记”和“清除”两个阶段: 首先标记出所有需要回收的对象,在标记完成后统一回收所有被标记对象,它的标记过程前面已经说过——如何判断对象是否存活/死去 死去的对象就会 ...

  10. 《垃圾回收的算法与实现》——GC标记-清除算法

    基本算法 标记-清除算法由 ==标记阶段== 和 ==清除阶段== 构成. 标记即将所有活动的对象打上标记. 清除即将那些没有标记的对象进行回收. 标记与清除 遍历GC root引用,递归标记(设置对 ...

随机推荐

  1. FCC(Federal Communications Commission)授权许可及其FCC ID和FCC批文查询

    清晰可见FCC批准申请的商品的内部拍照,甚至是所用集成电路的型号: 以 FCC ID 2AMSUGSKBBT066 为例: 所用集成电路型号: 触控板: PXI 的 PCT1335QN BT芯片: C ...

  2. SciTech-Mathmatics-因式分解定理:UNIQUE FACTORIZATION THEOREM + Science Hackathon Prizes@Kaggle.com

    SciTech-Mathmatics-UNIQUE FACTORIZATION THEOREM 因式分解趣谈: 一元 多项式方程: 解一元N次方程,其实就是对 一元N次方程"因式分解&quo ...

  3. JAVA基础-5-类型转换--九五小庞

    代码示例: public class Demo2 { public static void main(String[] args) { //类型转换的练习 /** * @param 类型转换从低到高 ...

  4. 从零开始实现简易版Netty(五) MyNetty FastThreadLocal实现

    从零开始实现简易版Netty(五) MyNetty FastThreadLocal实现 1. ThreadLocal介绍 在上一篇博客中,lab4版本的MyNetty对事件循环中的IO写事件处理进行了 ...

  5. 利用Origin2022工具绘制一个2D饼图的方法

    饼图在科研中是应用比较多的一种图形,用来展示各个组别的比例情况,下面给大家分享一下使用Origin制作一个美观实用的2D饼图: origin2022中文版   操作步骤: 1.先打开Origin202 ...

  6. 个人主页V1.1

    简单的个人主页 参考了Github上一些开源项目资源 预览: live at https://yukirinll.github.io/Personal-Profile/ 预览图: 代码: https: ...

  7. selenium安装教程python

    安装Selenium的步骤主要包括准备Python环境.安装Selenium.安装浏览器驱动以及验证安装. 准备Python环境: 访问Python官网并下载适合你操作系统的Python版本. 安 ...

  8. [笔记]数位dp例题及详解-下

    [接上回]-数位dp例题及详解-上 共\(4\)道难度较高.较有思考性的题. 附上数位dp题单:https://www.luogu.com.cn/training/494976#problems 小小 ...

  9. LLM ,MCP协议,A2A协议,RAG,智能体(AI Agent) 图解详细讲解

    LLM ,MCP协议,A2A协议,RAG,智能体(AI Agent) 图解详细讲解 @ 目录 LLM ,MCP协议,A2A协议,RAG,智能体(AI Agent) 图解详细讲解 MCP 概述 如何理解 ...

  10. .NET周刊【8月第3期 2025-08-17】

    国内文章 精选 5 款 .NET 开源.功能强大的工作流系统,告别重复造轮子! https://www.cnblogs.com/Can-daydayup/p/19038600 本文推荐了5款适用于.N ...