基于数组实现的buffer算法复杂度
在提这部分内容的时候,我们先回顾一下我们前面的基于数组实现的文件:C++算法学习——经典的抽象设计——buffer(3)
为了建立与其他表示法进行比较的基准,最重要的是确定基于数组的实现的编辑器的计算复杂度。和以前一样,复杂性分析的目标是了解编辑操作所需的执行时间如何随着问题大小的不同而变化。 在编辑器示例中,缓冲区中的字符数是衡量问题大小的最佳方法。 因此,对于编辑器的buffer,你需要确定缓冲区的大小如何影响每个编辑操作的运行时间。
对于基于数组的实现,要理解的最简单的操作是移动光标的操作。 例如,moveCursorForward的方法具有以下实现:
void EditorBuffer::moveCursorForward() {
if (cursor < length) cursor++;
}
即使该方法检查缓冲区的长度,我们也可以很容易意识到该方法的执行时间与缓冲区长度无关。无论缓冲区有多长,该方法都执行完全相同的操作: 一个测试,在几乎所有情况下,都进行一个增量操作。因为执行时间与N无关,所以moveCursorForward操作在O(1)时间运行。相同的分析适用于移动光标的其他操作,其中任何一个都不涉及依赖于缓冲区长度的任何操作都是O(1)。
但是insertCharacter呢? 在基于数组的EditorBuffer类的实现中,insertCharacter方法包含以下for循环:
for (int i = length; i > cursor; i--) {
array[i] = array[i - 1];
}
如果在buffer的末尾插入一个字符,这个方法运行得很快,因为没有必要移动字符为新的字符腾出空间。另一方面,如果在缓冲区的开头插入一个字符,缓冲区中的每个字符都必须向右移位一个位置。因此,在最坏的情况下,insertCharacter的运行时间与缓冲区中的字符数成正比,因此为O(N)。因为deleteCharacter操作具有相似的结构,其复杂度也是O(N)。每个编辑器操作的计算复杂度见下表
表中最后两个操作需要线性时间的事实对编辑器程序具有重要的性能影响。如果编辑器使用数组来表示其内部缓冲区,当缓冲区中的字符数变大,程序开始运行得更慢。因为这个问题似乎很严重,所以探索其他代表性的可能性是有道理的。
基于栈的实现
编辑器缓冲区的数组实现的问题是插入和删除在buffer的开头附近发生时缓慢运行。当这些相同的操作应用在缓冲区的末尾时,它们运行得相对较快,因为不需要移动内部数组中的字符。这个属性提出了一种使事情更快的方法:强制所有插入和删除发生在缓冲区的末尾。虽然这种方法从用户的角度来看是完全不切实际的,但它确实包含了可行的想法。
使插入和删除更快的关键是你要意识到你可以在游标边界划分缓冲区,并将游标前后的字符存储在单独的结构中。因为缓冲区的所有更改都发生在光标位置,所以这些结构中的每一个都像堆栈一样,可以由我们的的CharStack类来表示。在游标之前的字符被push到一个栈上,以便开始缓冲区位于底部,而游标前面的字符位于顶部。光标后的字符以相反的方向存储,缓冲区的结尾位于堆栈的底部,而字符恰好在顶部的指针之后。
说明这种结构的最好方法是使用图表。假设缓冲区包含字符串:
光标位于两个L字符之间,如图所示。 缓冲区的两个堆栈表示形式如下所示:
要读取缓冲区的内容,先读取栈中的字符,然后在后面的栈中读取字符,如箭头所示。
下面为了区分基于数组的实现,我把文件重新命名为stackBuffer,头文件的内容不变,只是名字变成了 stackBuffer.h
定义私有数据结构
使用此策略,缓冲对象的实例变量只是一对栈,一个用于保存在光标之前的字符,另一个用于保存其后的字符。 对于基于栈的buffer,bufferpriv.h文件只声明两个实例变量,如下所示。注意,游标在这个模型中没有明确的表示,而是简单的两个堆栈之间的边界。
/*
*说明: buffer数据结构
*----------------------
*在基于堆栈的缓冲区模型中,字符存储在两个栈中。
*光标前的字符存储在名为“before”的堆栈中;
*光标后的字符存储在名为“after”的堆栈中。 在每种情况下,
*最接近光标的字符更接近堆栈的顶部。
*该表示的优点在于当前光标位置的插入和删除发生在固定时间内。
*/
private:
/*实例化变量*/
CharStack before; //定义一个栈用于存放游标之前的字符
CharStack after; //定义一个栈用于存放右边之后的字符
/* 使得对象复制不合法 */
EditorBuffer(const EditorBuffer & value) { }
const EditorBuffer & operator=(const EditorBuffer & rhs) { return *this; }
实现缓冲操作
在堆栈模型中,为编辑器实现大部分操作非常容易。 例如,向后移动包括从前一个堆栈中弹出一个字符并将其推回到堆栈后面。前进是完全对称的。 插入一个字符包括在前一个堆栈上推送该字符。 删除一个字符包括从后一个堆栈中弹出一个字符并将其丢弃。
这个概念使得编写基于堆栈的编辑器的代码变得容易,如下面的代码所示。 命令中的四个命令 - insertCharacter,deleteCharacter,moveCursorForward和moveCursorBackward - 不包含循环,因此清楚地运行,因为它们调用的堆栈操作本身就是我们常用的操作。
/*
*这个文件实现stackbuffer.h中的文件
*/
#include <iostream>
#include "stackBuffer.h"
#include "charstack.h"
using namespace std;
/*
*构造函数和析构函数
*-------------------
*在这个实现中,所有动态分配由CharStack类来管理,
*这意味着EditorBuffer类没有任何工作。
*/
EditorBuffer::EditorBuffer() {
/* Empty */
}
EditorBuffer::~EditorBuffer() {
/* Empty */
}
/*
* 说明: moveCursor 方法
* ----------------------------------------
* 这四个移动游标的方法使用push和pop在两个堆栈之间传递值。
*/
void EditorBuffer::moveCursorForward() {
if (!after.isEmpty()) {
before.push(after.pop());
}
}
void EditorBuffer::moveCursorBackward() {
if (!before.isEmpty()) {
after.push(before.pop());
}
}
void EditorBuffer::moveCursorToStart() {
while (!before.isEmpty()) {
after.push(before.pop());
}
}
void EditorBuffer::moveCursorToEnd() {
while (!after.isEmpty()) {
before.push(after.pop());
}
}
/*
* 说明: 插入和删除字符
* ---------------------------------------------------------
* 插入或删除字符的每个功能都可以通过单次push或pop操作进行。
*/
void EditorBuffer::insertCharacter(char ch) {
before.push(ch);
}
void EditorBuffer::deleteCharacter() {
if (!after.isEmpty()) {
after.pop();
}
}
/*
* 说明: showContents()
* ------------------------------------
* 在基于堆栈的版本中,showContents操作符是复杂的,
* 但它不是基本操作符集的一部分。
*/
void EditorBuffer::showContents() {
int nBefore = before.size();
moveCursorToStart();
while (!after.isEmpty()) {
cout << ' ' << after.peek();
moveCursorForward();
}
cout << endl;
moveCursorToStart();
for (int i = 0; i < nBefore; i++) {
cout << " ";
moveCursorForward();
}
cout << '^' << endl;
}
这样子,插入删除是方便了,但是剩下的两个操作呢? moveCursorToStart和moveCursorToEnd方法都需要程序将整个内容从一个堆栈传输到另一个堆栈。 给定CharStack类提供的操作,完成此操作的唯一方法是从一个堆栈弹出值,并将其推回到另一个堆栈上,一次一个值,直到原始堆栈变为空。 例如,moveCursorToEnd操作具有以下实现:
void EditorBuffer::moveCursorToEnd() {
while (!after.isEmpty()) {
before.push(after.pop());
}
}
这些实现具有期望的效果,但是在最坏的情况下需要O(N)个时间。下一篇我们来讨论它的算法复杂度。