Gap Buffer--简洁有效的文本编辑算法

Gap buffer

在对jetpack compose原理进行分析过程中,了解到它的状态存储使用的一个叫Gap buffer的数据结构,开始的时候不太清楚这个是什么算法,准备进行深入了解下,了解后才发现这个算法很早就接触过,之前在做过一道文本编辑器的算法题的时候,使用的就是这种算法,今天就来讲解下这个算法以及分析jetpack compose为何使用这种算法进行状态存储。

算法讲解

实现文本编辑器的时候我们会考虑,如果使用链表的话,插入可以在o(1)的复杂度完成,但是如果光标随机移动的话,查找的话则需要o(n)的复杂度,如果使用数组的话,查找可以在o(1)的复杂度快速完成,但是插入的话则需要o(n)的复杂度。两者就像鱼和熊掌一样,不可兼得。如何解决这种矛盾,是实现文本编辑器的关键,其实有很多算法都可以取得不错的效果,比如块状链表(可以在o(n^1/2)复杂度完成插入和查找)、Rope树(一种平衡查找树)、piece table(改进版本的gap buffer,微软doc就是使用的这个算法,同时可以快速实现撤销和重做)。不过今天我们的重点是gap buffer算法,接下来我们就主要分析下gap buffer(gap buffer是notepad++采用的算法)。
我们来分析下文本编辑器的特点,我们的插入都是在光标处进行的,如果我们在数组的光标处预留一段空白位置,我们把它叫做buffer,这样插入的话我们就可以在o(1)的复杂度来完成,如果光标移动到其他位置的话,我们也可以在o(1)的复杂度完成(只是进行移动,不在光标处进行插入),如果光标移动到新的位置并且需要进行插入的时候,我们则需要将空的buffer移动到新的光标位置,这个操作的话需要o(n)的复杂度才能完成(不过对于实际的文本编辑过程,连续的插入远远大于移动光标的频率),如果在某个位置空白的buffer位置不够了,我们需要对其进行扩容,这个操作同样需要o(n)的复杂度,因此buffer的size选取也要根据实际情况灵活确定。因此我们就得到了一个能够在大多数情况下都能在o(1)的复杂度完成文本编辑器所需要操作的算法,并且这个算法还是如此简洁便于实现。
我们来简单的模拟上诉算法流程,假如我们目前文本编辑器内容为"hello,world,光标所在位置为5,buffer size 我们选取5,此时数组内容如下(“|”代表光标位置,“_”代表空白buffer)

hello,|_____world

我们此时连续输入my,数组将变成这样

hello,my|___world

这时我们移动光标到w位置处,

hello,my___w|orld

由于还没有执行插入操作,因此此时buffer位置没有变化,这时我们插入m

hello,mywm|____orld

我们在插入的之前,将buffer移动到w后面,同时更新buffer size 为5(这个可以根据具体情况进行处理,也可以不更新),然后插入m
经过上诉分析,我们发现实现gap buffer需要以下数据结构,一个数组,一个指针表示光标即(gap开始的位置),同时还需要gapsize 表示buffer的大小,我们还需要实现四个操作,插入删除字符、查找移动光标、移动buffer位置、扩容,简单的代码如下(没法编译,请不要直接拷贝)

int GAP_SIZE = 5;
struct GapBuffer{
	char array[size];
	int gapStart;
	int gapSize;
	int len;
} gapBuffer;
//插入字符
void insert(char c){
	if(gapBuffer.gapSize<0){
		expanison();
	}
	gapBuffer.array[++gapBuffer.gapStart]=c;
	--gapBuffer.gapSize;
	++len;
}
//扩容
void expanison(){
	GAP_SIZE = GAP_SZIE*2;//每次扩容两倍
	gapBuffer.gapSize = GAP_SIZE;
	//将后半部分数组往后拷贝腾出GAP_SIZE大小的位置
	arraycopy(gapBuffer.array,gapBuffer.gapStart,gapBuffer.gapStart+gapBuffer.gapSize,len-gapBuffer.start);
	
}
//移动gap,这个实现不会扩容buffer
void moveGap(int pos){
	if(gapBuffer.gapStart == pos)return;//相同位置不做移动
	//copy数组
	//如果pos小于当前gap
	if(pos<gapBuffer.gapStart)
		arraycopy(gapBuffer.array,pos,pos+gapBuffer.gapSize,gapBuffer.gapStart-pos);
	else
		arraycopy(gapBuffer.array,gapBuffer.gapStart+gapBuffer.gapSize,gapBuffer.gapStart,gapBuffer.gapSize);
	
}
void arraycopy(char array[],int srcSatrt,int dstStart,int len){
	for(int i = 0;i<len;++i)
		array[dstStart+i]=array[srcStart+i];
}

代码如果直接看的话可能不好理解,但是自己在脑子里过一遍,自己想下数组拷贝的位置和大小,应该就能理解了

jetpack compose和gap buffer

了解了gap buffer 之后,我们现在再来看jetpack compose为何使用gap buffer来存储状态,我们知道ui本质是一颗树形结构,ui的布局测量渲染都是对树进行深度遍历,对于每个组件的状态保存,我们可以看成是插入字符,而对于每个组件的遍历,我们可以看成是光标移动,因此jetpack compose使用此算法来保存状态(不过这里我比较疑惑的是,为何jetpack compose不采用其他框架使用的树形结构)。

  • 2
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 5
    评论
评论 5
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值