• <ins id="pjuwb"></ins>
    <blockquote id="pjuwb"><pre id="pjuwb"></pre></blockquote>
    <noscript id="pjuwb"></noscript>
          <sup id="pjuwb"><pre id="pjuwb"></pre></sup>
            <dd id="pjuwb"></dd>
            <abbr id="pjuwb"></abbr>
            【1】

            這些天參與了CSDN論壇的討論,改變了我以前的一些看法。回頭看我以前的東西,我雖對這本書很不滿,但我還是按照它的安排在一點點的寫;這樣就導致了,我過多的在意書中的偏漏,我寫的更多是說“這本書怎樣”,而偏離了我寫這些的初衷——給正在學習數(shù)據(jù)結(jié)構(gòu)的人一些幫助。正像我在前面所說的,雖然現(xiàn)有的教科書都不是很合理,但如果僅僅是抱怨這點,那無異于潑婦罵街。雖然本人的水平連初級都夠不上,但至少先從我做一點嘗試,以后這門課的教授方法必將一點點趨于合理。<?xml:namespace prefix = o ns = "urn:schemas-microsoft-com:office:office" />

            因此,后面不在按照書上的次序,將本著“實際應用(算法)決定數(shù)據(jù)結(jié)構(gòu)”的思想來講解,常見教科書上有的,基本不再重點敘述(除了重點,例如AVL樹的平衡旋轉(zhuǎn)),——因此,在看本文的同時,一定要有一本教科書。這只是一個嘗試,希望大家多提寶貴意見。

            因為現(xiàn)實世界中存在這“樹”這種結(jié)構(gòu)——族譜、等級制度、目錄分類等等,而為了研究這類問題,必須能夠?qū)鋬Υ妫绾蝺Υ鎸⑷Q于所需要的操作。這里有個問題,是否允許存在空樹。有些書認為樹都是非空的,因為樹表示的是一種現(xiàn)實結(jié)構(gòu),而0不是自然數(shù);我用過的教科書都是說可以有空樹,當然是為了和二叉樹統(tǒng)一。這個沒有什么原則上的差別,反正就是一種習慣。

            二叉樹

            二叉樹可以說是人們假想的一個模型,因此,允許有空的二叉樹是無爭議的。二叉樹是有序的,左邊有一個孩子和右邊有一個的二叉樹是不同的兩棵樹。做這個規(guī)定,是因為人們賦予了左孩子和右孩子不同的意義,在二叉樹的各種應用中,你將會清楚的看到。下面只講解鏈式結(jié)構(gòu)。看各種講數(shù)據(jù)結(jié)構(gòu)的書,你會發(fā)現(xiàn)一個有趣的現(xiàn)象:在二叉樹這里,基本操作有計算樹高、各種遍歷,就是沒有插入、刪除——那樹是怎么建立起來的?其實這很好理解,對于非線性的樹結(jié)構(gòu),插入刪除操作不在一定的法則規(guī)定下,是毫無意義的。因此,只有在具體的應用中,才會有插入刪除操作。

            節(jié)點結(jié)構(gòu)

            數(shù)據(jù)域、左指針、右指針肯定是必須的。除非很少用到節(jié)點的雙親,或者是資源緊張,建議附加一個雙親指針,這將會給很多算法帶來方便,尤其是在這個“空間換時間”的時代。

            template <class T>

            struct BTNode

            {

                BTNode(T data = T(), BTNode<T>* left = NULL, BTNode<T>* right = NULL, BTNode<T>* parent = NULL)

                    : data(data), left(left), right(right), parent(parent) {}

                BTNode<T> *left, *right, *parent;

                T data;

            };

            基本的二叉樹類

            template <class T>

            class BTree

            {

            public:

                BTree(BTNode<T> *root = NULL) : root(root) {}

                ~BTree() { MakeEmpty(); }

                void MakeEmpty() { destroy(root); root = NULL; }

            protected:

                BTNode<T> *root;

            private:

                void destroy(BTNode<T>* p)

                {

                    if (p)

                    {

                        destroy(p->left);

                        destroy(p->right);

                        delete p;

                    }

                }

            }

            二叉樹的遍歷

            基本上有4種遍歷方法,先、中、后根,逐層。當初我對這個很迷惑,搞這么多干什么?到了后面才明白,這是不同的應用需要的。例如,判斷兩個二叉樹是否相等,只要子樹根節(jié)點不同,那么就不等,顯然這時要用先序遍歷;而刪除二叉樹,必須先刪除左右子樹,然后才能刪除根節(jié)點,這時就要用后序遍歷。

            實際上,搞這么多遍歷方法,根本原因是在內(nèi)存中儲存的樹是非線性結(jié)構(gòu)。對于用數(shù)組儲存的二叉樹,這些名目繁多的方法都是沒有必要的。利用C++的封裝和重載特性,這些遍歷方法能很清晰的表達。

            1.         前序遍歷

            public:

            void PreOrder(void (*visit)(T &data) = print) { PreOrder(root, visit); }

            private:

            void PreOrder(BTNode<T>* p, void (*visit)(T &data))

            {

            if (p){ visit(p->data); PreOrder(p->left, visit); PreOrder(p->right, visit); }

            }

            2.         中序遍歷

            public:

                   void InOrder(void (*visit)(T &data) = print) { InOrder(root, visit); }

            private:

            void InOrder(BTNode<T>* p, void (*visit)(T &data))

            {

                   if (p){ InOrder(p->left, visit);       visit(p->data);       InOrder(p->right, visit); }

                   }

            3.         后序遍歷

            public:

                   void PostOrder(void (*visit)(T &data) = print) { PostOrder(root, visit); }

            private:

            void PostOrder(BTNode<T>* p, void (*visit)(T &data))

            {

                   if (p){ PostOrder(p->left, visit); PostOrder(p->right, visit); visit(p->data); }

                   }

            4.         層次遍歷

            void LevelOrder(void (*visit)(T &data) = print)

            {

                   queue< BTNode<T>* > a; BTNode<T>* p = root;//記得#include<queue>

                   while (p)

                   {

                          visit(p->data);

                          if (p->left) a.push(p->left); if (p->right) a.push(p->right);

                          if (a.empty()) break; p = a.front(); a.pop();

                   }

                   }

            附注:缺省的visit函數(shù)print如下

            private:

                   static void print(T &data) { cout << data << ' '; }

            5.         不用棧的非遞歸中序遍歷

            當有parent指針時,可以不用棧實現(xiàn)非遞歸的中序遍歷,書上提到了有這種方法,但沒給出例程。

            public:

            BTNode<T>* next()

            {

                   if(!current) return NULL;

                   if (current->right) { current = current->right; while (current->left) current = current->left; }

                   else

                   {

                          BTNode<T>* y = current->parent;

                          while (y && current == y->right) {current = y; y = y->parent; }

                          current = y;

                   }

                   return current;

            }

            private:

            BTNode<T>* current;

            上面的函數(shù)能使current指針向前移動一個位置,如果要遍歷整棵二叉樹,需要使current指向中序序列的第一個節(jié)點,例如下面的成員函數(shù):

            public:

            void first() { current = root; while (current->left) current = current->left; }

            【2】 

            線索化二叉樹

            這是數(shù)據(jù)結(jié)構(gòu)課程里第一個碰到的難點,不知道你是不是這樣看,反正我當初是費了不少腦細胞——當然,惱人的矩陣壓縮和相關(guān)的加法乘法運算不在考慮之列。我費了不少腦細胞是因為思考:他們干什么呢?很欣喜的看到在這本黃皮書上,這章被打了*號,雖然我不確定作者是不是跟我一個想法——線索化二叉樹在現(xiàn)在的PC上是毫無用處的!——不知我做了這個結(jié)論是不是會被人罵死,^_^

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

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

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

            節(jié)省內(nèi)存。添加了兩個標志位,問題是這兩個位怎么儲存?即使是在支持位存儲的CPU上,也是不能拿位存儲器來存的,第一是因為結(jié)構(gòu)體成員的地址是在一起的,第二是位存儲器的數(shù)目是有限的。因此,最少需要1個字節(jié)來儲存這兩個標志位。而為了速度和移植,一般來說,內(nèi)存是要對齊的,實際上根本就沒節(jié)省內(nèi)存!然而,當這個空間用來儲存雙親指針時,帶來的方便絕對不是線索化所能比擬的,前面已經(jīng)給出了無棧的非遞歸遍歷。并且,在線索化二叉樹上插入刪除操作附加的代價太大。

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

            由于對其他體系不太了解,以下觀點姑妄聽之。在內(nèi)存空間非常充裕的現(xiàn)在,一個節(jié)點省23個字節(jié)實在是沒什么意思(實際上由于對齊還省不出來);而在內(nèi)存非常寶貴的地方(比如單片機),會盡量避免使用樹結(jié)構(gòu)——利用其他的方法。所以,現(xiàn)在看來,線索化二叉樹真的是毫無用處了。

            二叉搜索樹

            這恐怕是二叉樹最重要的一個應用了。它的構(gòu)想實際是個很自然的事情——查找值比當前節(jié)點小轉(zhuǎn)左,大轉(zhuǎn)右,等則查到,到頭了就是沒找著。越自然的東西越好理解,也就越不需要我廢話。在給出BST的實現(xiàn)之前,我們要在二叉樹的類中添加一個打印樹狀結(jié)構(gòu)的成員函數(shù),這樣,就能清楚的看出插入和刪除過程。

            public:

            void print()

            {

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

                   BTNode<T>* p = root; BTNode<T> zero; bool v = true;

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

                   while (i < 2<<h)

                   {

                          if (i == 1<<level)

                          {

                                 cout << endl << setw(2 <<(h - level)); level++;

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

                                 else cout << ' ';

                          }

                          else

                          {

                                 cout << setw(4 <<(h - level + 1));

                                 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;

            }

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

            對于輸出格式,注意的是到了第1248號節(jié)點要換行,并且在同一行中,第一個節(jié)點的域?qū)捠呛笮蚬?jié)點的一半。上面的函數(shù)在樹的層次少于等于5height<=4)的時候能正常顯示,再多的話就必須輸出到文件中去ofstream outfile("out.txt");——如果層次再多的話,打印出來也沒什么意義了。

            二叉搜索樹的實現(xiàn)

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

            #include "BaseTree.h"

            template <class T>

            class BSTree : public BTree<T>

            {

            public:

                   BTNode<T>* &find(const T &data)

                   {

                          BTNode<T>** 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<T>* &p = find(data); if (p) return false;

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

                   }

                   bool remove(const T &data)

                   {

                          return remove(find(data));

                   }

            private:

            bool remove(BTNode<T>* &p)

            {

                   if (!p) return false; BTNode<T>* 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);

                   }

            };

            以上代碼有點費解,有必要說明一下——非線性鏈式結(jié)構(gòu)操作的實現(xiàn)都是很讓人費神。insertremove都是以find為基礎的,因此必須讓find能最大限度的被這兩個操作利用。

            l         對于insert,需要修改查找失敗時的指針內(nèi)容,顯然這是個內(nèi)部指針(在雙親節(jié)點的內(nèi)部,而不是象rootcurrent那樣在節(jié)點外面指向節(jié)點),這就要求find返回一個內(nèi)部指針的引用。但是C++的引用綁定到一個對象之后,就不能再改變了,因此在find內(nèi)部的實現(xiàn)是一個二重指針。insert操作還需要修改插入的新節(jié)點的parent指針域,因此在find中要產(chǎn)生一個能被insert訪問的指向find返回值所在節(jié)點的指針,這里用的是current。實際上find返回的指針引用不是current->left就是current->right。這樣一來,insert的實現(xiàn)就非常簡單了。

            l         對于remove,需要修改查找成功時的指針內(nèi)容,同樣是個內(nèi)部指針。在find的基礎上,很容易就能得到這個內(nèi)部指針的引用(BTNode<T>* &p = find(data)

            ?         p->leftp->right中至少有一個為NULL

            數(shù)據(jù)結(jié)構(gòu)學習(C++)二叉樹

            Posted on 2005-12-15 12:57 艾凡赫 閱讀(513) 評論(0)  編輯 收藏 引用 所屬分類: C++
            国产农村妇女毛片精品久久| 婷婷久久五月天| 狠狠色丁香久久婷婷综合五月| 久久国产精品波多野结衣AV| 久久精品国产清高在天天线| 久久久噜噜噜久久中文字幕色伊伊| 亚洲精品白浆高清久久久久久 | 久久精品这里只有精99品| 久久伊人精品一区二区三区| 国内精品久久久久久久久| 伊人久久综在合线亚洲2019| 久久无码av三级| 国产精品久久久久久久app| 久久影院久久香蕉国产线看观看| 久久国产精品免费| 精品久久人人妻人人做精品| 亚洲国产精品综合久久网络| 久久久这里只有精品加勒比| 九九精品久久久久久噜噜| 久久久这里有精品| 久久久久人妻一区二区三区vr| 无码人妻少妇久久中文字幕| 国产高潮久久免费观看| 久久久九九有精品国产| 久久这里只有精品首页| 欧美午夜A∨大片久久| 青青青国产成人久久111网站| 久久99久久99精品免视看动漫| 国产高潮国产高潮久久久91| 狠狠色丁香婷婷久久综合不卡| 久久久久99精品成人片试看| 色8久久人人97超碰香蕉987| 久久精品国产亚洲7777| 亚洲一区中文字幕久久| …久久精品99久久香蕉国产| 国产美女久久精品香蕉69| 欧美va久久久噜噜噜久久| 国产精品免费福利久久| 国产免费久久精品丫丫| 精品乱码久久久久久夜夜嗨| 99久久精品九九亚洲精品|