通過上篇的學習,我們可以知道作爲爲編輯器buffer,他們可以快速的找到編輯器的第一位和最後一位,但是插入和刪除某個元素的時候,卻是可能很慢。因此爲了改進這個問題,我們有必要了解爲什麼以前的方法會使得插入和刪除變得這麼慢(因爲通常使用編輯器的時候,插入和刪除的操作往往更頻繁)。
在數組實現的情況下,答案是顯而易見的:因爲當我們在buffer中插入一些新文本時,必須移動大量字符。例如,假設你嘗試補全插入字母表:
當你發現你漏寫了字母B的時候,你必須將下一個24個字符的每一個字符向右移動一個位置,以便爲丟失的字母留出空間。只要buffer沒有太長,現代電腦就可以相對較快地處理這種移動操作;但是即使如此,如果緩衝器中的字符數足夠大(假如有100w個),則延遲最終將變得非常明顯。
然而,假設在發明現代電腦之前,我們遇到這樣的情況,會怎麼處理呢?通常爲了避免給人的印象是你已經忽略了字母表中一個字母,你可以簡單地拿出一支筆,並使用以下符號:
這種編輯符號的一個優點是,它允許你暫停現有的排序規則,說明所有字母按照它們在打印頁面上出現的形式按順序排列。線下方的編輯符號告訴我們,閱讀A後,你必須向上移動,讀取B,回來,讀取C,然後繼續按順序。使用此插入方式的另一個優點也很重要。那就是不管行是多長,你只需要繪製的是新的字符和編輯符號,不需要把數據改動。只是使用鉛筆和紙張而已,插入時間不變。
linked list的概念
通過上述的例子,我們可以採用類似的方法來設計編輯buffer的表示,看看是否可以減少數據插入造成的花費。實際上,我們甚至可以推廣這個想法,也就是我們的元素不一定就要跟正常序列的數組一樣排,我們也可以你只需將緩衝區中每個字母的箭頭繪製到跟隨它的字母上即可。
我們對比一下,
- 基於數組原始緩衝區內容可以表示如下:
- 基於箭頭原始的緩衝區內容可以表示如下:
如果我們需要在字符A之後添加字符B,需要做的就是:
- 將B寫到某處
- 從B繪製一個箭頭指向到A當前指向的字母(當前是 C)
- 更改從A指向的箭頭,指向新加的B,如下所示:
因此要實現這一結構,我們能想到的底層往往就是使用指針實現,因爲這樣的表示跟指針是很相近的,指針的一大優點在於,它們使一個數據對象能夠包含指向第二個對象的指針。因此可以使用此指針來表示排序關係,這與上圖中箭頭所暗示的順序關係非常相似。以這種方式使用的指針通常被稱爲鏈接(links)。當這樣的鏈接用於創建線性排序的數據結構,其中每個元素指向其後繼者時,該結構稱爲鏈表(linked list)。
設計linked list結構
如果要將此基於指針的策略應用於編輯器緩衝區的設計,我們需要的是一個鏈接的字符列表(即一個節點)。因此我們必須將緩衝區中的每個字符與指示列表中下一個字符的指針相關聯。 然而,該指針不能是簡單的字符指針; 你需要的是指向下一個字符/鏈接組合的指針。
要使linked-list能工作,必須創建一個單一的結構,其中包含與應用程序相關的數據(在本例中爲字符)和指向同一類型的另一個結構的指針,然後用於指示內部排序。應用數據和指針的這種組合成爲鏈表的基本構建塊,其通常被稱爲單元(cell)。 設計的過程如下
- 爲了使單元的想法更容易可視化,我們可以從結構圖開始。 在編輯器buffer中,單元格有兩個組件:一個字符和鏈接到以下的單元格。 因此,單個單元格可以如下圖所示:
- 然後,你可以通過將幾個這些單元格鏈接在一起來表示一系列的字符。例如,字符序列ABC可以被表示爲包含以下小單元集合的鏈表:
- 如果C是序列中的最後一個字符,則需要通過在該單元格的鏈接字段中添加特殊值來指示該事實,以指示列表中沒有其他字符。當用C ++編程時,爲此目的通常使用特殊的指針值NULL。然而,在列表結構圖中,通常在框中指定帶有對角線的NULL值,如前面的示例所示。
爲了在C ++中表示這些單元格結構圖,你需要定義一個結構類型來保存數據。單元格結構必須包含字符的ch字段和指向下一個單元格的link成員。 這個定義有點不尋常,因爲Cell類型是根據自身定義的,因此是一種遞歸類型(recursive type)。 以下類型定義正確表示單元格的結構:
struct Cell {
char ch;
Cell *link;
};
buffer的鏈表表示
現在我們考慮用鏈表來表示我們之前的buffer,首先是將鏈表中的初始指針設置爲buffer,其次我們還要有個指針指向光標的位置。因此EditorBuffer類的數據成員應該包含兩個指向Cell對象的指針:一個指針,指示緩衝區啓動的位置。另一個指針,指示當前光標位置。
如果有一個包含三個字符的buffer,我們通常會想到用3個cell來表示這三個元素,而此時,cursor字段存放指向下一個單元格的指針(注意最後一個cell元素的指針值爲null),也就是說此時只有3個cursor字段,然而情況卻是下圖所示:
三個字母有四個遊標位置,我們只能表示後面的三種情況,無法表示第一種。於是我們考慮加多一個虛擬的cell,使其指向首元素A:
當表示第四種情況的時候,cursor的情況應該是這樣的(注意,A下面的指針,對應的是第二種情況下游標的位置):
於是將之前的buffer的私有文件改動一下:
/*
*說明:buffer 數據結構的實現
*--------------------------
*在緩衝區的鏈表實現中,緩衝區中的字符存儲在單元結構中,
*每個單元格結構都包含一個字符和指向鏈中下一個單元格的指針 。
*爲了簡化用於維護光標的代碼,此實現在列表的開頭添加了一個額外的“dummy”單元格。
*該單元格中的字符不被使用,但是在數據結構中提供了一個單元格,
*當遊標位於緩衝區的開頭時,該值指向遊標。
*/
private:
/*
*結構類型:Cell
*--------------------------
*該結構類型在實現中本地使用以將每個單元格存儲在鏈表表示中。
*每個單元格包含一個字符和指向鏈中下一個單元格的指針
*/
struct Cell{
char ch;
Cell *link;
}
/*實例化變量*/
Cell *start; //指向虛擬的單元格
Cell *cursor; // 指向光標之前的單元格
/* 使得對象複製不合法 */
EditorBuffer(const EditorBuffer & value) { }
const EditorBuffer & operator=(const EditorBuffer & rhs) { return *this; }
鏈表的插入操作
無論光標位於何處,鏈表的插入操作都包括以下步驟:
- 爲新單元格分配空間,並將指針存儲在此單元格中的臨時變量cp中的。
- 將要插入的字符複製到新單元格的 ch 成員中。
- 轉到緩衝區光標字段指示的單元格,並將其link成員複製到新單元格的link成員。 此操作確保不會丟失超出當前光標位置的字符。
- 更改光標所在單元格中的link成員,使其指向新單元格。
- 更改緩衝區中的光標字段,以便它也指向新的單元格。此操作確保在重複插入操作之後插入下一個字符。
爲了更加可視化的說明這個問題,我們舉個實際的例子(假設我們想將B插入到下面的序列中):
光標在A和C之間,如圖所示。 插入之前的情況如下所示:
第一步,分配一個新單元格,並在變量cp中存儲一個指針,指向這個新的單元格,如下所示
第二步,將字符B存儲到新單元格的ch中,這將留下以下配置:
第三步,將出現在cursor中的link地址(A下面的小黑點)複製到新單元格的link中。該link字段指向C單元格的指針,因此生成的圖形如下所示:
第四步:更改當前單元格中由cusor的link成員,使其指向新分配的單元格的ch值,如下所示:
第五步:更改緩衝區結構中的cursor字段,以便它也指向新的單元格:
第六步:buffer現在具有正確的內容。如果你從緩衝區開始處的dummy單元格中按箭頭讀取,則沿路徑順序遇到包含A,B,C和D的單元格。此時,函數返回,並釋放掉臨時指針cp:
函數返回後,顯示的結果爲:
將上述的步驟轉換成C++代碼,結果爲:
void EditorBuffer::insertCharacter(char ch) {
Cell *cp = new Cell; //創建一個新的單元格,並創建一個指針指向該單元格
cp->ch = ch; //向單元格中賦值
cp->link = cursor->link;// 將遊標指向的節點的link值,複製到新的單元格中
cursor->link = cp; //將cp的值,複製到當前遊標的link字段中
cursor = cp; //重定位遊標
}
鏈表的刪除操作
同樣我們圖解刪除鏈表中的節點的過程。要刪除鏈接列表中的單元格,只需將其從指針鏈中刪除即可。 我們假設緩衝區的當前內容是:
用表示就是:
假如我們要刪除B節點,那麼我們只需要改變A單元的指向即可,如下:
用一行代碼表示就是:
cursor->link = cursor->link->link;//好好理解,cursor->link就是B單元,B->link就是C單元
完整的刪除的C++代碼就是:
void EditorBuffer::deleteCharacter() {
if (cursor->link != NULL) {
Cell *oldCell = cursor->link; //定義一個指針,指向要被刪除的節點
cursor->link = cursor->link->link;//改變節點的指向
delete oldCell; //釋放節點
}
}
通常我們需要一個像oldCell這樣的變量,以便在調整link指針時保存指向要釋放的單元格的指針的副本。 如果你不保存此值,則再調用delete時將無法引用該單元格。
鏈表中指針的移動
我們此時拿cursor爲實例來演示。其中的兩個操作(moveCursorForward和moveCursorToStart)在鏈表模型中很容易執行。例如,要向前移動光標,你只需要從當前單元格中取出link,並將該指針存儲在緩衝區的cursor字段中,使其成爲新的當前單元格。完成此操作所需的聲明很簡單。(實際就是指針往前或者往後移動了一下)
前移
假設,buffer的光標位置如下,我們需要將光標位置往前挪動一位 :
於是我們改變cursor指針的指向:
cursor = cursor->link;
當然,當到達緩衝區的末尾時,我們將將無法向前移動。 moveCursorForward的實現必須檢查這種情況,所以完整的方法定義如下所示:
void EditorBuffer::moveCursorForward() {
if (cursor->link != NULL) {
cursor = cursor->link;
}
}
後移
剛剛看了指針的前移,實際上是很簡單的,只需要往前挪動指針的方向即可。那麼是否可以參考着來往後移動一位呢?很遺憾,答案是否定的。理由就是,我們之所以可以那麼輕易的向前移動,是因爲我們的單元中已經包含了指向下一個單元的指針。然而我們並沒有存放指向前一位的單元的指針(當然也可以這麼設計,但是需要增加額外的空間開銷)。因此對於上述的指針圖,這種限制的效果是你可以從箭頭底部的點移動到箭頭指向的單元格,但是你永遠不能從箭頭返回到其上一個。
所以,唯一的辦法就是,從start開始,從頭開始遍歷鏈表,然後找到當前cursor指向的位置,再進行轉換。
要遍歷表示緩衝區的列表,首先聲明一個指針變量並將其初始化爲列表的開頭。 也就是;
Cell *cp = start;
要找到光標前面的字符,只要cp的link與cursor的link不匹配,你就繼續遍歷鏈表,按照每個link字段將cp從單元移動到單元格。 因此,可以通過在循環中添加以下代碼繼續執行代碼:
Cell *cp = start;
while (cp->link != cursor) {
cp = cp->link;
}
當while循環退出時,cp被設置爲光標之前的單元格。 與向前移動一樣,你需要保護此循環,避免超過緩衝區的限制,因此moveCursorBackward的完整代碼將爲:
void EditorBuffer::moveCursorBackward() {
Cell *cp = start; //定義一個指針,指向鏈表的開始
if (cursor != start) {
while (cp->link != cursor) {//當其指向的link不是當前cursor指向的單元格時
cp = cp->link; //更新cp的值
}
cursor = cp; //最終找到cursor指向的元素之前的位置,複製給當前的cursor
}
}
同樣的原理,我們可以利用遍歷操作直接將鏈表重頭遍歷到尾:
void EditorBuffer::moveCursorToEnd() {
while (cursor->link != NULL) {
cursor = cursor->link;
}
}
回到開頭
直接賦指針值即可
void EditorBuffer::moveCursorToStart() {
cursor = start;
}