資料結構學習(C++)——二叉樹【2】 (轉)

amyz發表於2007-11-12
資料結構學習(C++)——二叉樹【2】 (轉)[@more@]

線索化二叉樹

這是資料結構課程裡第一個碰到的難點,不知道你是不是這樣看,反正我當初是費了不少腦細胞——當然,惱人的矩陣和相關的加法乘法運算不在考慮之列。我費了不少腦細胞是因為思考:他們幹什麼呢?很欣喜的看到在這本黃皮書上,這章被打了*號,雖然我不確定作者是不是跟我一個想法——線索化二叉樹在現在的PC上是毫無用處的!——不知我做了這個結論是不是會被人罵死,^_^。

為了證明這個結論,我們來看看線索化二叉樹提出的緣由:第一,我們想用比較少的時間,尋找二叉樹某一個遍歷線性序列的前驅或者後繼。當然,這樣的操作很頻繁的時候,做這方面的改善才是有意義的。第二,二叉樹的葉子節點還有兩個指標域沒有用,可以節省。說真的,提出線索化二叉樹這樣的構思真的很精巧,完全做到了“廢物利用”——這個人真應該投身環保事業。但在這個死板的東西身上,人們的精巧構思往往都是不能實現的——為了速度,計算機的各個部件都是整齊劃一的,而構思的精巧往往都是建立在組成的複雜上的。

我們來看看線索化二叉樹究竟能不能達到上面的兩個目標。

求遍歷後的線性序列的前驅和後繼。前序線索化能依次找到後繼,但是前驅需要求雙親;中序線索化前驅和後繼都不需要求雙親,但是都不很直接;後序線索化能依次找到前驅,但是後繼需要求雙親。可以看出,線索化成中序是最佳的選擇,基本上算是達到了要求。

節省記憶體。新增了兩個標誌位,問題是這兩個位怎麼儲存?即使是在支援位的上,也是不能拿位儲存器來存的,第一是因為結構體成員的地址是在一起的,第二是位儲存器的數目是有限的。因此,最少需要1個位元組來儲存這兩個標誌位。而為了速度和移植,一般來說,記憶體是要對齊的,實際上根本就沒節省記憶體!然而,當這個空間用來儲存雙親指標時,帶來的方便絕對不是線索化所能比擬的,前面已經給出了無棧的非遞迴遍歷。並且,線上索化二叉樹上插入刪除操作附加的代價太大。

綜上,線索化最好是中序線索化(前序後序線索化後還得用棧,何必要線索化呢),附加的標誌域空間至少1個位元組,在32位的CPU會要求對齊到4位元組,還不如儲存一個雙親指標,同樣能達到中序線索化的目的,並且能帶來其他的好處。所以,線索化二叉樹在現在的PC上是毫無用處的!

由於對其他體系不太瞭解,以下觀點姑妄聽之。在記憶體空間非常充裕的現在,一個節點省2~3個位元組實在是沒什麼意思(實際上由於對齊還省不出來);而在記憶體非常寶貴的地方(比如微控制器),會盡量避免使用樹結構——利用其他的方法。所以,現在看來,線索化二叉樹真的是毫無用處了。

二叉搜尋樹

這恐怕是二叉樹最重要的一個應用了。它的構想實際是個很自然的事情——查詢值比當前節點小轉左,大轉右,等則查到,到頭了就是沒找著。越自然的東西越好理解,也就越不需要我廢話。在給出BST的實現之前,我們要在二叉樹的類中新增一個列印樹狀結構的成員,這樣,就能清楚的看出插入和刪除過程。

public:

void print()

{

  queue< BTNode* > a; queue flag; ofstream outfile("out.txt");

  BTNode* p = ; BTNode zero; bool v = true;

  int i = 1, level = 0, h = height();

  while (i < 2<

  {

  if (i == 1<

  {

    cout << endl << setw(2 <

  if (v) cout << p->data;

    else cout << ;

  }

  else

  {

    cout << setw(4 <

  if (v) cout << p->data;

    else cout << "  ";

  }

  if (p->left) { a.push(p->left); flag.push(true); }

  else { a.push(&zero); flag.push(false); }

  if (p->right) { a.push(p->right); flag.push(true); }

  else { a.push(&zero); flag.push(false); }

  p = a.front(); a.pop(); v = flag.front(); flag.pop(); i++;

  }

  cout << endl;

}

列印樹狀結構的核心是按層次遍歷二叉樹,但是,二叉樹有許多節點缺左或右子樹,連帶的越到下面空隙越大。為了按照樹的結構列印,必須把二叉樹補成完全二叉樹,這樣下面的節點就知道放在什麼位置了——a.push(&zero);但是這樣的節點不能讓它列印出來,所以對應每個節點,有一個是否列印的標誌,按理說pair結構很合適,為了簡單我用了並列的兩個佇列,一個放節點指標——a,一個放列印標誌——flag。這樣一來,迴圈結束的標誌就不能是佇列空——永遠都不可能空,碰到NULL就補一個節點——而是變成了到了滿二叉樹的最後一個節點2^(height+1)-1。——黃皮書對於樹高的定義是,空樹為的高度為-1。

對於輸出格式,注意的是到了第1、2、4、8號節點要換行,並且在同一行中,第一個節點的域寬是後序節點的一半。上面的函式在樹的層次少於等於5(height<=4)的時候能正常顯示,再多的話就必須輸出到中去ofstream outfile("out.txt");——如果層次再多的話,列印出來也沒什麼意義了。

二叉搜尋樹的實現

實際上就是在二叉樹的基礎上增加了插入、刪除、查詢。

#include "BaseTree.h"

template

class BSTree : public BTree

{

public:

  BTNode* &find(const T &data)

  {

    BTNode** p = &root; current = NULL;

    while(*p)

  {

  if ((*p)->data == data) break;

  if ((*p)->data < data) { current = *p; p = &((*p)->right); }

    else { current = *p; p = &((*p)->left); }

  }

  return *p;

  }

  bool insert(const T &data)

  {

    BTNode* &p = find(data); if (p) return false;

  p = new BTNode(data, NULL, NULL, current); return true;

  }

  bool remove(const T &data)

  {

  return remove(find(data));

  }

private:

bool remove(BTNode* &p)

{

  if (!p) return false; BTNode* t = p;

  if (!p->left || !p->right)

  {

  if (!p->left) p = p->right; else p = p->left;

  if (p) p->parent = current;

    delete t; return true;

  }

  t=p->right;while(t->left) t=t->left;p->data=t->data;current=t->parent;

  return remove(current->left==t?current->left:current->right);

  }

};

以上程式碼有點費解,有必要說明一下——非線性鏈式結構操作的實現都是很讓人費神。insert和remove都是以find為基礎的,因此必須讓find能最大限度的被這兩個操作利用。

l  對於insert,需要修改查詢失敗時的指標內容,顯然這是個內部指標(在雙親節點的內部,而不是象root和current那樣在節點外面指向節點),這就要求find返回一個內部指標的引用。但是C++的引用繫結到一個之後,就不能再改變了,因此在find內部的實現是一個二重指標。insert操作還需要修改插入的新節點的parent指標域,因此在find中要產生一個能被insert訪問的指向find返回值所在節點的指標,這裡用的是current。實際上find返回的指標引用不是current->left就是current->right。這樣一來,insert的實現就非常簡單了。

l  對於remove,需要修改查詢成功時的指標內容,同樣是個內部指標。在find的基礎上,很容易就能得到這個內部指標的引用(BTNode* &p = find(data)。

Ø  在p->left和p->right中至少有一個為NULL的情況下,如果p->left ==NULL,那麼就重連右子樹p = p->right,反之,重連左子樹p = p->left。注意,左右子樹全空的情況也包含在這兩個操作中了——在p->left ==NULL的時候重連右子樹,而這時p->right也是NULL——因此不必列出來。如果重連後p不為空,需要修改p->parent = current。

Ø  若p->left和p->right都不為空,可以轉化為有一個為空。例如一箇中序有序序列[1,2,3,4,5],假設3既有左子樹又有右子樹,那麼它的前驅2一定缺右子樹,後繼4一定缺少左子樹。【注1】這樣一來刪除節點3就等效成從[1,2,3(4),4,5]刪除節點4。這樣就可以利用上面的在p->left和p->right中至少有一個為NULL的情況下的方法了。還是由於C++的引用不能改變繫結物件,這裡是用利用遞迴來解決的,還好最多隻遞迴一次。如果用二重指標又是滿天星星了,這就是明明是尾遞迴卻沒有消去的原因。

【注1】這是因為,如果3既有左子樹又有右子樹,那麼2一定在3的左子樹上,4一定在3的右子樹上;如果2有右子樹,那麼在2和3之間還應該有一個節點;如果4有左子樹,那麼3和4之間也應該還有一個節點。

【閒話】上面關於remove操作p->left和p->right都不為空的處理方法的講解,源於嚴蔚敏老師的課件,看完後我豁然開朗,真不知道為什麼她自己那本《資料結構(C語言版)》這裡寫的那麼難懂,我是死活沒看明白。

 


來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/10752019/viewspace-981877/,如需轉載,請註明出處,否則將追究法律責任。

相關文章