1 前言
1.1 所选教材
1.2 写作原因
1.3 一些约定
1.4 历史记录
1.5 联系方式
2 单链表
2.1 代码实现
2.2 效率问题
2.3 应用:一元多项式(加法和乘法)
2.3.1 基础知识
2.3.2 代码实现
2.3.3 说明
3 双链表
3.1 代码实现
3.2 说明
4 循环链表
4.1 基本概念
4.2 代码实现
4.3 说明
4.4 应用:约瑟夫问题
5 栈
5.1 基本概念
5.2 代码实现
5.3 说明
5.4 应用:中缀到后缀表达式的转换
5.4.1 代码实现
5.4.2 说明
6 队列
6.1 基本概念
6.2 代码实现
6.3 应用
7 递归
7.1 基本概念
7.2 应用
7.2.1 阶乘
7.2.2 斐波那契数列
7.2.3 汉诺塔
7.2.4 帕斯卡三角形(杨辉三角)
8 二叉树
8.1 基本概念
8.2 代码实现
8.3 说明
8.4 应用
9 二叉搜索树
9.1 基本概念
9.2 代码实现
9.3 说明
我所选择的教材是《数据结构与算法分析——C语言描述》(原书第2版),英文版的名称是《Data Structures and Algorithm Analysis in C》,作者是:(美)Mark Allen Weiss。原书曾被评为20世纪顶尖的30部计算机著作之一。之所以选这本书,还因为它的简体中文版翻译得相当不错,几乎没有给我的阅读带来什么障碍。^_^
这本教科书所使用的是C语言,也许很多人会说C语言已经过时了,但是,我认为在数据结构的学习中,应该用尽量简单的语言,以免进入了语言的细枝末节中,反而冲淡了主题。实际上在国外的许多大学中(甚至中学),数据结构和算法分析的课程是选用Scheme的,例如MIT麻省理工大学极其著名的SICP课程。呵呵,语言又能说明什么呢?
数据结构与算法分析是计算机专业的必修课——但遗憾的是,我在大学阶段并不是计算机专业的学生,以至于没有系统地跟着老师学习过这门课程。现在我已经工作了,在实际的工作中,我经常感到自己的基础知识不够,有很多问题无法解决。在经历了一段痛苦的斗争后,我选择了自学的道路,想把这门课程扎扎实实地学好。
教科书中已经给出了大部分的代码,因此,我基本上也只是重复敲入了一次而已(或者是改写成C++),但这并不是没有意义的。我们在看书的时候经常会觉得自己已经懂了,但如果真的要亲自动手去做了,却会感到无法下手。我认为,亲自输入一次代码并调试通过,比任何空谈都有效。
在具体的代码实现上,我可能会参考MFC、STL……但也可能会进行一定的修改。
我使用的是Visual C++ 6.0编译器,并将会用C/C++来撰写代码(我可能会用C++改写原书中的例子,以便能用在工作中,但一些地方还是会用C),不会使用任何与平台相关的特性(因此可以保证有比较好的移植性)。原书中的代码风格跟我平时的代码风格非常相近,但有一些地方我可能会进行一些改动。
我认为数据结构的代码不需要任何界面,因此,请您新建一个工程,类型为Win32 Console Application,即控制台工程。然后添加一个.h头文件和一个.c/.cpp文件。头文件中,我一般会写3行固定格式的预编译语句,如下:
#ifndef __LIST_H__ #define __LIST_H__ // TODO: Add header body code here #endif // __LIST_H__
表示这是一个list.h。
另外,C++操作符new的实现在不同的编译器中都不太一样,在VC6中,如果new失败,则会返回NULL,程序中我用检测返回值是否为NULL来判断new是否成功,但如果这个代码是用别的编译器编译的,则要特别注意别的编译器是否也是用NULL来表示new失败的,否则很可能会导致无法意料的结果。
为了方便调试内存泄漏,我会在一些地方写入这样的代码:
#include <assert.h> #include <crtdbg.h> #ifdef _DEBUG #define DEBUG_NEW new (_NORMAL_BLOCK, THIS_FILE, __LINE__) #endif #ifdef _DEBUG #define new DEBUG_NEW #undef THIS_FILE static char THIS_FILE[] = __FILE__; #endif #ifdef _DEBUG #ifndef ASSERT #define ASSERT assert #endif #else // not _DEBUG #ifndef ASSERT #define ASSERT #endif #endif // _DEBUG
以及:
#ifdef _DEBUG _CrtSetDbgFlag(_CRTDBG_ALLOC_MEM_DF | _CRTDBG_LEAK_CHECK_DF); #endif
在阅读时不用管它们,直接略过即可。
[2005-01-18] 二叉搜索树一章。
[2005-01-14] 二叉树一章。
[2005-01-09] 递归一章。
[2005-01-08] 队列一章。
[2005-01-07] 栈一章。
[2005-01-05] 循环链表一章。
[2005-01-04] 双链表一章。
[2004-12-30] 单链表一章。
作者:罗聪
主页:http://www.luocong.com
E-Mail:admin[AT]luocong.com
链表是最常用、最简单和最基本的数据结构之一。我们先来看看单链表的实现。
单链表的实现如下:
/////////////////////////////////////////////////////////////////////////////// // // FileName : slist.h // Version : 0.10 // Author : Luo Cong // Date : 2004-12-29 9:58:38 // Comment : // /////////////////////////////////////////////////////////////////////////////// #ifndef __SINGLE_LIST_H__ #define __SINGLE_LIST_H__ #include <assert.h> #include <crtdbg.h> #ifdef _DEBUG #define DEBUG_NEW new (_NORMAL_BLOCK, THIS_FILE, __LINE__) #endif #ifdef _DEBUG #define new DEBUG_NEW #undef THIS_FILE static char THIS_FILE[] = __FILE__; #endif #ifdef _DEBUG #ifndef ASSERT #define ASSERT assert #endif #else // not _DEBUG #ifndef ASSERT #define ASSERT #endif #endif // _DEBUG template<typename T> class CNode { public: T data; CNode<T> *next; CNode() : data(T()), next(NULL) {} CNode(const T &initdata) : data(initdata), next(NULL) {} CNode(const T &initdata, CNode<T> *p) : data(initdata), next(p) {} }; template<typename T> class CSList { protected: int m_nCount; CNode<T> *m_pNodeHead; public: CSList(); CSList(const T &initdata); ~CSList(); public: int IsEmpty() const; int GetCount() const; int InsertBefore(const int pos, const T data); int InsertAfter(const int pos, const T data); int AddHead(const T data); int AddTail(const T data); void RemoveAt(const int pos); void RemoveHead(); void RemoveTail(); void RemoveAll(); T& GetTail(); T GetTail() const; T& GetHead(); T GetHead() const; T& GetAt(const int pos); T GetAt(const int pos) const; void SetAt(const int pos, T data); int Find(const T data) const; }; template<typename T> inline CSList<T>::CSList() : m_nCount(0), m_pNodeHead(NULL) { } template<typename T> inline CSList<T>::CSList(const T &initdata) : m_nCount(0), m_pNodeHead(NULL) { AddHead(initdata); } template<typename T> inline CSList<T>::~CSList() { RemoveAll(); } template<typename T> inline int CSList<T>::IsEmpty() const { return 0 == m_nCount; } template<typename T> inline int CSList<T>::AddHead(const T data) { CNode<T> *pNewNode; pNewNode = new CNode<T>; if (NULL == pNewNode) return 0; pNewNode->data = data; pNewNode->next = m_pNodeHead; m_pNodeHead = pNewNode; ++m_nCount; return 1; } template<typename T> inline int CSList<T>::AddTail(const T data) { return InsertAfter(GetCount(), data); } // if success, return the position of the new node. // if fail, return 0. template<typename T> inline int CSList<T>::InsertBefore(const int pos, const T data) { int i; int nRetPos; CNode<T> *pTmpNode1; CNode<T> *pTmpNode2; CNode<T> *pNewNode; pNewNode = new CNode<T>; if (NULL == pNewNode) { nRetPos = 0; goto Exit0; } pNewNode->data = data; // if the list is empty, replace the head node with the new node. if (NULL == m_pNodeHead) { pNewNode->next = NULL; m_pNodeHead = pNewNode; nRetPos = 1; goto Exit1; } // is pos range valid? ASSERT(1 <= pos && pos <= m_nCount); // insert before head node? if (1 == pos) { pNewNode->next = m_pNodeHead; m_pNodeHead = pNewNode; nRetPos = 1; goto Exit1; } // if the list is not empty and is not inserted before head node, // seek to the pos of the list and insert the new node before it. pTmpNode1 = m_pNodeHead; for (i = 1; i < pos; ++i) { pTmpNode2 = pTmpNode1; pTmpNode1 = pTmpNode1->next; } pNewNode->next = pTmpNode1; pTmpNode2->next = pNewNode; nRetPos = pos; Exit1: ++m_nCount; Exit0: return nRetPos; } // if success, return the position of the new node. // if fail, return 0. template<typename T> inline int CSList<T>::InsertAfter(const int pos, const T data) { int i; int nRetPos; CNode<T> *pTmpNode; CNode<T> *pNewNode; pNewNode = new CNode<T>; if (NULL == pNewNode) { nRetPos = 0; goto Exit0; } pNewNode->data = data; // if the list is empty, replace the head node with the new node. if (NULL == m_pNodeHead) { pNewNode->next = NULL; m_pNodeHead = pNewNode; nRetPos = 1; goto Exit1; } // is pos range valid? ASSERT(1 <= pos && pos <= m_nCount); // if the list is not empty, // seek to the pos of the list and insert the new node after it. pTmpNode = m_pNodeHead; for (i = 1; i < pos; ++i) { pTmpNode = pTmpNode->next; } pNewNode->next = pTmpNode->next; pTmpNode->next = pNewNode; nRetPos = pos + 1; Exit1: ++m_nCount; Exit0: return nRetPos; } template<typename T> inline int CSList<T>::GetCount() const { return m_nCount; } template<typename T> inline void CSList<T>::RemoveAt(const int pos) { ASSERT(1 <= pos && pos <= m_nCount); int i; CNode<T> *pTmpNode1; CNode<T> *pTmpNode2; pTmpNode1 = m_pNodeHead; // head node? if (1 == pos) { m_pNodeHead = m_pNodeHead->next; goto Exit1; } for (i = 1; i < pos; ++i) { // we will get the previous node of the target node after // the for loop finished, and it would be stored into pTmpNode2 pTmpNode2 = pTmpNode1; pTmpNode1 = pTmpNode1->next; } pTmpNode2->next = pTmpNode1->next; Exit1: delete pTmpNode1; --m_nCount; } template<typename T> inline void CSList<T>::RemoveHead() { ASSERT(0 != m_nCount); RemoveAt(1); } template<typename T> inline void CSList<T>::RemoveTail() { ASSERT(0 != m_nCount); RemoveAt(m_nCount); } template<typename T> inline void CSList<T>::RemoveAll() { int i; int nCount; CNode<T> *pTmpNode; nCount = m_nCount; for (i = 0; i < nCount; ++i) { pTmpNode = m_pNodeHead->next; delete m_pNodeHead; m_pNodeHead = pTmpNode; } m_nCount = 0; } template<typename T> inline T& CSList<T>::GetTail() { ASSERT(0 != m_nCount); int i; int nCount; CNode<T> *pTmpNode = m_pNodeHead; nCount = m_nCount; for (i = 1; i < nCount; ++i) { pTmpNode = pTmpNode->next; } return pTmpNode->data; } template<typename T> inline T CSList<T>::GetTail() const { ASSERT(0 != m_nCount); int i; int nCount; CNode<T> *pTmpNode = m_pNodeHead; nCount = m_nCount; for (i = 1; i < nCount; ++i) { pTmpNode = pTmpNode->next; } return pTmpNode->data; } template<typename T> inline T& CSList<T>::GetHead() { ASSERT(0 != m_nCount); return m_pNodeHead->data; } template<typename T> inline T CSList<T>::GetHead() const { ASSERT(0 != m_nCount); return m_pNodeHead->data; } template<typename T> inline T& CSList<T>::GetAt(const int pos) { ASSERT(1 <= pos && pos <= m_nCount); int i; CNode<T> *pTmpNode = m_pNodeHead; for (i = 1; i < pos; ++i) { pTmpNode = pTmpNode->next; } return pTmpNode->data; } template<typename T> inline T CSList<T>::GetAt(const int pos) const { ASSERT(1 <= pos && pos <= m_nCount); int i; CNode<T> *pTmpNode = m_pNodeHead; for (i = 1; i < pos; ++i) { pTmpNode = pTmpNode->next; } return pTmpNode->data; } template<typename T> inline void CSList<T>::SetAt(const int pos, T data) { ASSERT(1 <= pos && pos <= m_nCount); int i; CNode<T> *pTmpNode = m_pNodeHead; for (i = 1; i < pos; ++i) { pTmpNode = pTmpNode->next; } pTmpNode->data = data; } template<typename T> inline int CSList<T>::Find(const T data) const { int i; int nCount; CNode<T> *pTmpNode = m_pNodeHead; nCount = m_nCount; for (i = 0; i < nCount; ++i) { if (data == pTmpNode->data) return i + 1; pTmpNode = pTmpNode->next; } return 0; } #endif // __SINGLE_LIST_H__
调用如下:
/////////////////////////////////////////////////////////////////////////////// // // FileName : slist.cpp // Version : 0.10 // Author : Luo Cong // Date : 2004-12-29 10:41:18 // Comment : // /////////////////////////////////////////////////////////////////////////////// #include <iostream> #include "slist.h" using namespace std; int main() { int i; int nCount; CSList<int> slist; #ifdef _DEBUG _CrtSetDbgFlag(_CRTDBG_ALLOC_MEM_DF | _CRTDBG_LEAK_CHECK_DF); #endif slist.InsertAfter(slist.InsertAfter(slist.AddHead(1), 2), 3); slist.InsertAfter(slist.InsertAfter(slist.GetCount(), 4), 5); slist.InsertAfter(slist.GetCount(), 6); slist.AddTail(10); slist.InsertAfter(slist.InsertBefore(slist.GetCount(), 7), 8); slist.SetAt(slist.GetCount(), 9); slist.RemoveHead(); slist.RemoveTail(); // print out elements nCount = slist.GetCount(); for (i = 0; i < nCount; ++i) cout << slist.GetAt(i + 1) << endl; }
代码比较简单,一看就明白,懒得解释了。如果有bug,请告诉我。
考虑到效率的问题,代码中声明了一个成员变量:m_nCount,用它来记录链表的结点个数。这样有什么好处呢?在某些情况下就不用遍历链表了,例如,至少在GetCount()时能提高速度。
原书中提到了一个“表头”(header)或“哑结点”(dummy node)的概念,这个结点作为第一个结点,位置在0,它是不用的,我个人认为这样做有点浪费空间,所以并没有采用这种做法。
单链表在效率上最大的问题在于,如果要插入一个结点到链表的末端或者删除末端的一个结点,则需要遍历整个链表,时间复杂度是O(N)。平均来说,要访问一个结点,时间复杂度也有O(N/2)。这是链表本身的性质所造成的,没办法解决。不过我们可以采用双链表和循环链表来改善这种情况。
我们使用一元多项式来说明单链表的应用。假设有两个一元多项式:
P1(X) = X^2 + 2X + 3
以及
P2(X) = 3X^3 + 10X + 6
现在运用中学的基础知识,计算它们的和:
P1(X) + P2(X) = (X^2 + 2X + 3) + (3X^3 + 10X + 6) = 3X^3 + 1X^2 + 12X^1 + 9
以及计算它们的乘积:
P1(X) * P2(X) = (X^2 + 2X + 3) * (3X^3 + 10X + 6) = 3X^5 + 6X^4 + 19X^3 + 26X^2 + 42X^1 + 18
怎么样,很容易吧?:) 但我们是灵长类动物,这么繁琐的计算怎么能用手工来完成呢?(试想一下,如果多项式非常大的话……)我们的目标是用计算机来完成这些计算任务,代码就在下面。
/////////////////////////////////////////////////////////////////////////////// // // FileName : poly.cpp // Version : 0.10 // Author : Luo Cong // Date : 2004-12-30 17:32:54 // Comment : // /////////////////////////////////////////////////////////////////////////////// #include <stdio.h> #include "slist.h" #define Max(x,y) (((x)>(y)) ? (x) : (y)) typedef struct tagPOLYNOMIAL { CSList<int> Coeff; int HighPower; } * Polynomial; static void AddPolynomial( Polynomial polysum, const Polynomial poly1, const Polynomial poly2 ) { int i; int sum; int tmp1; int tmp2; polysum->HighPower = Max(poly1->HighPower, poly2->HighPower); for (i = 1; i <= polysum->HighPower + 1; ++i) { tmp1 = poly1->Coeff.GetAt(i); tmp2 = poly2->Coeff.GetAt(i); sum = tmp1 + tmp2; polysum->Coeff.AddTail(sum); } } static void MulPolynomial( Polynomial polymul, const Polynomial poly1, const Polynomial poly2 ) { int i; int j; int tmp; int tmp1; int tmp2; polymul->HighPower = poly1->HighPower + poly2->HighPower; // initialize all elements to zero for (i = 0; i <= polymul->HighPower; ++i) polymul->Coeff.AddTail(0); for (i = 0; i <= poly1->HighPower; ++i) { tmp1 = poly1->Coeff.GetAt(i + 1); for (j = 0; j <= poly2->HighPower; ++j) { tmp = polymul->Coeff.GetAt(i + j + 1); tmp2 = poly2->Coeff.GetAt(j + 1); tmp += tmp1 * tmp2; polymul->Coeff.SetAt(i + j + 1, tmp); } } } static void PrintPoly(const Polynomial poly) { int i; for (i = poly->HighPower; i > 0; i-- ) printf( "%dX^%d + ", poly->Coeff.GetAt(i + 1), i); printf("%d/n", poly->Coeff.GetHead()); } int main() { Polynomial poly1 = NULL; Polynomial poly2 = NULL; Polynomial polyresult = NULL; #ifdef _DEBUG _CrtSetDbgFlag(_CRTDBG_ALLOC_MEM_DF | _CRTDBG_LEAK_CHECK_DF); #endif poly1 = new (struct tagPOLYNOMIAL); if (NULL == poly1) goto Exit0; poly2 = new (struct tagPOLYNOMIAL); if (NULL == poly2) goto Exit0; polyresult = new (struct tagPOLYNOMIAL); if (NULL == polyresult) goto Exit0; // P1(X) = X^2 + 2X + 3 poly1->HighPower = 2; poly1->Coeff.AddHead(0); poly1->Coeff.AddHead(1); poly1->Coeff.AddHead(2); poly1->Coeff.AddHead(3); // P2(X) = 3X^3 + 10X + 6 poly2->HighPower = 3; poly2->Coeff.AddHead(3); poly2->Coeff.AddHead(0); poly2->Coeff.AddHead(10); poly2->Coeff.AddHead(6); // add result = 3X^3 + 1X^2 + 12X^1 + 9 AddPolynomial(polyresult, poly1, poly2); PrintPoly(polyresult); // reset polyresult->Coeff.RemoveAll(); // mul result = 3X^5 + 6X^4 + 19X^3 + 26X^2 + 42X^1 + 18 MulPolynomial(polyresult, poly1, poly2); PrintPoly(polyresult); Exit0: if (poly1) { delete poly1; poly1 = NULL; } if (poly2) { delete poly2; poly2 = NULL; } if (polyresult) { delete polyresult; polyresult = NULL; } }
原书中只给出了一元多项式的数组实现,而没有给出单链表的代码。实际上用单链表最大的好处在于多项式的项数可以为任意大。(当然只是理论上的。什么?你的内存是无限大的?好吧,当我没说……)
我没有实现减法操作,实际上减法可以转换成加法来完成,例如 a - b 可以换算成 a + (-b),那么我们的目标就转变为做一个负号的运算了。至于除法,可以通过先换算“-”,然后再用原位加法来计算。(现在你明白加法有多重要了吧?^_^)有兴趣的话,不妨您试试完成它,我的目标只是掌握单链表的使用,因此不再继续深究。
单链表学完后,理所当然的就是轮到双链表了。
双链表的实现如下:
/////////////////////////////////////////////////////////////////////////////// // // FileName : dlist.h // Version : 0.10 // Author : Luo Cong // Date : 2005-1-4 10:33:21 // Comment : // /////////////////////////////////////////////////////////////////////////////// #ifndef __DOUBLE_LIST_H__ #define __DOUBLE_LIST_H__ #include <assert.h> #include <crtdbg.h> #ifdef _DEBUG #define DEBUG_NEW new (_NORMAL_BLOCK, THIS_FILE, __LINE__) #endif #ifdef _DEBUG #define new DEBUG_NEW #undef THIS_FILE static char THIS_FILE[] = __FILE__; #endif #ifdef _DEBUG #ifndef ASSERT #define ASSERT assert #endif #else // not _DEBUG #ifndef ASSERT #define ASSERT #endif #endif // _DEBUG template<typename T> class CNode { public: T data; CNode<T> *prior; CNode<T> *next; CNode() : data(T()), prior(NULL), next(NULL) {} CNode(const T &initdata) : data(initdata), prior(NULL), next(NULL) {} }; template<typename T> class CDList { protected: int m_nCount; CNode<T> *m_pNodeHead; CNode<T> *m_pNodeTail; public: CDList(); CDList(const T &initdata); ~CDList(); public: int IsEmpty() const; int GetCount() const; int InsertBefore(const int pos, const T data); int InsertAfter(const int pos, const T data); int AddHead(const T data); int AddTail(const T data); void RemoveAt(const int pos); void RemoveHead(); void RemoveTail(); void RemoveAll(); T& GetTail(); T GetTail() const; T& GetHead(); T GetHead() const; T& GetAt(const int pos); T GetAt(const int pos) const; void SetAt(const int pos, T data); int Find(const T data) const; T& GetPrev(int &pos); T& GetNext(int &pos); }; template<typename T> inline CDList<T>::CDList() : m_nCount(0), m_pNodeHead(NULL), m_pNodeTail(NULL) { } template<typename T> inline CDList<T>::CDList(const T &initdata) : m_nCount(0), m_pNodeHead(NULL), m_pNodeTail(NULL) { AddHead(initdata); } template<typename T> inline CDList<T>::~CDList() { RemoveAll(); } template<typename T> inline T& CDList<T>::GetNext(int &pos) { ASSERT(0 != m_nCount); ASSERT(1 <= pos && pos <= m_nCount); int i; CNode<T> *pTmpNode = m_pNodeHead; for (i = 1; i < pos; ++i) { pTmpNode = pTmpNode->next; } ++pos; return pTmpNode->data; } template<typename T> inline T& CDList<T>::GetPrev(int &pos) { ASSERT(0 != m_nCount); ASSERT(1 <= pos && pos <= m_nCount); int i; CNode<T> *pTmpNode = m_pNodeHead; for (i = 1; i < pos; ++i) { pTmpNode = pTmpNode->next; } --pos; return pTmpNode->data; } template<typename T> inline int CDList<T>::InsertBefore(const int pos, const T data) { int i; int nRetPos; CNode<T> *pTmpNode; CNode<T> *pNewNode; pNewNode = new CNode<T>; if (NULL == pNewNode) { nRetPos = 0; goto Exit0; } pNewNode->data = data; // if the list is empty, replace the head node with the new node. if (NULL == m_pNodeHead) { pNewNode->prior = NULL; pNewNode->next = NULL; m_pNodeHead = pNewNode; m_pNodeTail = pNewNode; nRetPos = 1; goto Exit1; } // is pos range valid? ASSERT(1 <= pos && pos <= m_nCount); // insert before head node? if (1 == pos) { pNewNode->prior = NULL; pNewNode->next = m_pNodeHead; m_pNodeHead->prior = pNewNode; m_pNodeHead = pNewNode; nRetPos = 1; goto Exit1; } // if the list is not empty and is not inserted before head node, // seek to the pos of the list and insert the new node before it. pTmpNode = m_pNodeHead; for (i = 1; i < pos; ++i) { pTmpNode = pTmpNode->next; } pNewNode->next = pTmpNode; pNewNode->prior = pTmpNode->prior; pTmpNode->prior->next = pNewNode; pTmpNode->prior = pNewNode; // if tail node, must update m_pNodeTail if (NULL == pNewNode->next) { m_pNodeTail = pNewNode; } nRetPos = pos; Exit1: ++m_nCount; Exit0: return nRetPos; } template<typename T> inline int CDList<T>::InsertAfter(const int pos, const T data) { int i; int nRetPos; CNode<T> *pNewNode; CNode<T> *pTmpNode; pNewNode = new CNode<T>; if (NULL == pNewNode) { nRetPos = 0; goto Exit0; } pNewNode->data = data; // if the list is empty, replace the head node with the new node. if (NULL == m_pNodeHead) { pNewNode->prior = NULL; pNewNode->next = NULL; m_pNodeHead = pNewNode; m_pNodeTail = pNewNode; nRetPos = 1; goto Exit1; } // is pos range valid? ASSERT(1 <= pos && pos <= m_nCount); // if the list is not empty, // seek to the pos of the list and insert the new node after it. pTmpNode = m_pNodeHead; for (i = 1; i < pos; ++i) { pTmpNode = pTmpNode->next; } pNewNode->next = pTmpNode->next; pNewNode->prior = pTmpNode; // if NewNode's position is m_pNodeTail, update m_pNodeTail if (pTmpNode->next == m_pNodeTail) { m_pNodeTail->prior = pNewNode; } pTmpNode->next = pNewNode; // if tail node, must update m_pNodeTail if (NULL == pNewNode->next) { m_pNodeTail = pNewNode; } nRetPos = pos + 1; Exit1: ++m_nCount; Exit0: return nRetPos; } template<typename T> inline T& CDList<T>::GetAt(const int pos) { ASSERT(1 <= pos && pos <= m_nCount); int i; CNode<T> *pTmpNode = m_pNodeHead; for (i = 1; i < pos; ++i) { pTmpNode = pTmpNode->next; } return pTmpNode->data; } template<typename T> inline T CDList<T>::GetAt(const int pos) const { ASSERT(1 <= pos && pos <= m_nCount); int i; CNode<T> *pTmpNode = m_pNodeHead; for (i = 1; i < pos; ++i) { pTmpNode = pTmpNode->next; } return pTmpNode->data; } template<typename T> inline int CDList<T>::AddHead(const T data) { return InsertBefore(1, data); } template<typename T> inline int CDList<T>::AddTail(const T data) { return InsertAfter(GetCount(), data); } template<typename T> inline CDList<T>::IsEmpty() const { return 0 == m_nCount; } template<typename T> inline CDList<T>::GetCount() const { return m_nCount; } template<typename T> inline T& CDList<T>::GetTail() { ASSERT(0 != m_nCount); return m_pNodeTail->data; } template<typename T> inline T CDList<T>::GetTail() const { ASSERT(0 != m_nCount); return m_pNodeTail->data; } template<typename T> inline T& CDList<T>::GetHead() { ASSERT(0 != m_nCount); return m_pNodeHead->data; } template<typename T> inline T CDList<T>::GetHead() const { ASSERT(0 != m_nCount); return m_pNodeHead->data; } template<typename T> inline void CDList<T>::RemoveAt(const int pos) { ASSERT(1 <= pos && pos <= m_nCount); int i; CNode<T> *pTmpNode = m_pNodeHead; // head node? if (1 == pos) { m_pNodeHead = m_pNodeHead->next; goto Exit1; } for (i = 1; i < pos; ++i) { pTmpNode = pTmpNode->next; } pTmpNode->prior->next = pTmpNode->next; Exit1: delete pTmpNode; --m_nCount; if (0 == m_nCount) { m_pNodeTail = NULL; } } template<typename T> inline void CDList<T>::RemoveHead() { ASSERT(0 != m_nCount); RemoveAt(1); } template<typename T> inline void CDList<T>::RemoveTail() { ASSERT(0 != m_nCount); RemoveAt(m_nCount); } template<typename T> inline void CDList<T>::RemoveAll() { int i; int nCount; CNode<T> *pTmpNode; nCount = m_nCount; for (i = 0; i < nCount; ++i) { pTmpNode = m_pNodeHead->next; delete m_pNodeHead; m_pNodeHead = pTmpNode; } m_nCount = 0; } template<typename T> inline void CDList<T>::SetAt(const int pos, T data) { ASSERT(1 <= pos && pos <= m_nCount); int i; CNode<T> *pTmpNode = m_pNodeHead; for (i = 1; i < pos; ++i) { pTmpNode = pTmpNode->next; } pTmpNode->data = data; } template<typename T> inline int CDList<T>::Find(const T data) const { int i; int nCount; CNode<T> *pTmpNode = m_pNodeHead; nCount = m_nCount; for (i = 0; i < nCount; ++i) { if (data == pTmpNode->data) return i + 1; pTmpNode = pTmpNode->next; } return 0; } #endif // __DOUBLE_LIST_H__
调用如下:
/////////////////////////////////////////////////////////////////////////////// // // FileName : dlist.cpp // Version : 0.10 // Author : Luo Cong // Date : 2005-1-4 10:58:22 // Comment : // /////////////////////////////////////////////////////////////////////////////// #include <iostream> #include "dlist.h" using namespace std; int main() { int i; int nCount; CDList<int> dlist; #ifdef _DEBUG _CrtSetDbgFlag(_CRTDBG_ALLOC_MEM_DF | _CRTDBG_LEAK_CHECK_DF); #endif dlist.AddTail(1); dlist.AddTail(3); dlist.InsertBefore(2, 2); dlist.AddHead(4); dlist.RemoveTail(); nCount = dlist.GetCount(); for (i = 1; i <= nCount;) { cout << dlist.GetNext(i) << endl; } }
单链表的结点中只有一个指向直接后继结点的指针,所以,从某个结点出发只能顺着指针往后查询其他的结点。靠,那如果我想访问某个结点的前一个结点,岂不只能重新从表头结点开始了?效率真低啊!换句话说,在单链表中,GetNext()的时间复杂度为O(1),而GetPrev()的时间复杂度则为O(N)。为克服单链表这种单向性的缺点,我们可以利用——“当当当当”,Only you,就是——双链表。
顾名思义,在双链表的结点中有两个指针,一个指向直接后继,另一个指向直接前驱,在C++语言中表示如下:
struct Node { struct Node *prior; struct Node *next; T data; };
大部分对双链表的操作(只涉及到向后方向的指针的操作)都与单链表的相同,但在插入、删除时有很大的不同,在双链表中需同时修改两个方向上的指针。因此,可以直接继承单链表的类来完成双链表,然后改改不一样的函数就行了。但我没有这样做,别问为什么,人品问题而已。
如果你已经熟练掌握了单链表的指针域,那么双链表的这部分应该难不倒你了。不多说了,请看代码吧。如果有bug,请告诉我。^_^
循环链表可以为单链表,也可以为双链表,但我不想把问题搞得那么复杂,姑且就做单链表的循环形式吧。
我们在实现了链表后,必然会提出一个问题:链表能不能首尾相连?怎样实现?
答案:能。其实实现的方法很简单,就是将表中最后一个结点的指针域指向头结点即可(P->next = head;)。这种形成环路的链表称为循环链表。
试想我们在学校的运动场上跑步锻炼身体(学校……好遥远的记忆啊),绕着400米跑道一直跑啊跑,好像永远没有尽头一样。这是因为跑道的首尾是相连的,跑完一圈后,“尾巴”突然就变成了“头”,这跟循环链表的原理是一样的。好了,明白了这个道理,实现起来就简单了,不过要注意的是,在循环链表里面如果要获得结点的个数,不能采用while()循环来遍历表,因为这个循环是永不会结束的,这就像无论有多长的长跑比赛都可以在400米的跑道上进行一样。我的做法还是通过增加一个m_nCount变量,每次新增或删除一个结点就对m_nCount进行相应的操作。
循环链表的特点:
从任一结点出发均可找到表中其他结点。
操作仅有一点与单链表不同:循环条件。
单链表:P = NULL 或 P->next = NULL
循环链表:P = head 或 P->next = head
循环链表的实现如下:
/////////////////////////////////////////////////////////////////////////////// // // FileName : clist.h // Version : 0.10 // Author : Luo Cong // Date : 2005-1-5 10:43:17 // Comment : // /////////////////////////////////////////////////////////////////////////////// #ifndef __CIRC_LIST_H__ #define __CIRC_LIST_H__ #include "../../slist/src/slist.h" template<typename T> class CCList : public CSList<T> { protected: CNode<T> *m_pNodeCurr; public: CCList(); public: T& GetNext(); void RemoveAt(const int pos); int GetCurrentIndex() const; }; template<typename T> inline T& CCList<T>::GetNext() { ASSERT(0 != m_nCount); if ((NULL == m_pNodeCurr) || (NULL == m_pNodeCurr->next)) m_pNodeCurr = m_pNodeHead; else m_pNodeCurr = m_pNodeCurr->next; return m_pNodeCurr->data; } template<typename T> inline int CCList<T>::GetCurrentIndex() const { ASSERT(0 != m_nCount); int i; CNode<T> *pTmpNode = m_pNodeHead; for (i = 1; i <= m_nCount; ++i) { if (pTmpNode == m_pNodeCurr) return i; else pTmpNode = pTmpNode->next; } return 0; } template<typename T> inline void CCList<T>::RemoveAt(const int pos) { ASSERT(1 <= pos && pos <= m_nCount); int i; CNode<T> *pTmpNode1; CNode<T> *pTmpNode2; pTmpNode1 = m_pNodeHead; // head node? if (1 == pos) { m_pNodeHead = m_pNodeHead->next; // added for loop list // m_pNodeCurr will be set to m_pNodeHead in function GetNext() m_pNodeCurr = NULL; goto Exit1; } for (i = 1; i < pos; ++i) { // we will get the previous node of the target node after // the for loop finished, and it would be stored into pTmpNode2 pTmpNode2 = pTmpNode1; pTmpNode1 = pTmpNode1->next; } pTmpNode2->next = pTmpNode1->next; // added for loop list m_pNodeCurr = pTmpNode2; Exit1: delete pTmpNode1; --m_nCount; } template<typename T> inline CCList<T>::CCList() : m_pNodeCurr(NULL) { } #endif // __CIRC_LIST_H__
由于循环链表的操作大部分是与非循环链表相同的,因此我的循环链表是直接从单链表继承来的,并且新增了表示当前结点的变量m_pNodeCurr,以及重载了几个函数。但还有两点是需要特别注意的:
在GetNext()函数中,必须有判断当前结点应该如何指向下一个结点的条件。
在RemoveAt()函数中,如果要删除一个结点,而该结点又恰好是头结点的话,那么当前结点必须指向NULL,这样才能在GetNext()中重新获得头结点的正确的值。
关于这两点应该毫无疑问吧?呵呵,那就让我们继续吧……什么?你不明白第二点是什么意思?我倒!
让我们来假定一下,如果当前结点指向了尾结点,然后这时我们调用了GetNext(),那么很显然,当前结点就应该指向头结点了。但问题是头结点已经被我们删除了,那么当前结点还能指向哪里呢?这时什么事情都可能发生,计算机可能会格式化了你的硬盘,也可能会把你的情书送给了班里的恐龙,更可能会告诉你的老板你愿意从此以后一分钱工资都不要一直做到over为止……但最有可能发生的事情是产生一个内存访问的异常,所以,咳咳,计算机是很笨的,必须由我们亲自告诉它:“头结点已经完蛋啦,所以当前结点就指向NULL吧,你在GetNext()函数中自个儿给我解决好下一步的问题。”
明白了吗?还不明白的话……我……
约瑟夫问题几乎是最经典的用来讲解循环链表的案例了。为什么呢?我们来看看这个问题的描述就会明白了:
有一队由n个冒险家组成的探险队深入到热带雨林中,但他们遭遇到了食人族,食人族的游戏规则是让他们围成一圈,然后选定一个数字m,从第1个人开始报数,报到m时,这个人就要被吃掉了,然后从下一个人开始又重新从1报数,重复这个过程,直到剩下最后一个人,这个人是幸运者,可以离开而不被吃掉。那么问题是,谁是这个幸运者呢?
我们来举个例子:
假设这个探险队有6个探险家,食人族选定的数字m是5,那么在第一轮中,5号会被吃掉,剩下的就是:1, 2, 3, 4, 6总共5个人,然后从6号开始,重新从1开始报5个数:6, 1, 2, 3, 4,所以在第二轮里面被吃掉的就是4号……一直重复这个过程,按顺序应该是:5, 4, 6, 2, 3被吃掉,剩下1号活下来。
解决这个问题并不是只能用循环链表的,但使用循环链表应该是最方便的。我写的代码如下:
/////////////////////////////////////////////////////////////////////////////// // // FileName : joseph.cpp // Version : 0.10 // Author : Luo Cong // Date : 2005-1-5 13:56:32 // Comment : // /////////////////////////////////////////////////////////////////////////////// #include <iostream> #include "clist.h" using namespace std; int main() { int i; int n; int m; int nNumber; int nCurIndex; CCList<int> clist; #ifdef _DEBUG _CrtSetDbgFlag(_CRTDBG_ALLOC_MEM_DF | _CRTDBG_LEAK_CHECK_DF); #endif cout << "请输入总的人数: "; cin >> n; cout << "请输入死亡号码: "; cin >> m; // 初始化序列号码列表: for (i = 1; i <= n; ++i) { clist.AddTail(i); } i = 0; do { ++i; nNumber = clist.GetNext(); if (i == m) { cout << "第 " << nNumber << " 个人被吃掉了!" << endl; // 这个人倒霉了 nCurIndex = clist.GetCurrentIndex(); clist.RemoveAt(nCurIndex); --n; // 剩下的人重新开始报数 i = 0; } } while (1 != n); cout << "最后活下来的是: " << clist.GetHead() << endl; }
为了解决约瑟夫问题,我在循环链表中加入了GetCurrentIndex()函数,用来获得当前结点的索引值,以便删除当前结点。整个代码应该不难理解,实际动手做做就明白了。 :)
栈(stack)是限制插入和删除只能在一个位置上进行的表,该位置是表的末端,叫做栈的顶(top),它是后进先出(LIFO)的。对栈的基本操作只有push(进栈)和pop(出栈)两种,前者相当于插入,后者相当于删除最后的元素。
由于栈在本质上是一种受限制的表,所以可以使用任何一种表的形式来实现它,我们最常使用的一般有两种:
链表
数组
它们在复杂度上的优缺点对比如下:
新增和删除元素时的时间复杂度
链表:在动态申请内存(new或者malloc)上的花销非常昂贵。
数组:几乎没有花销,以常数O(1)时间运行,在带有自增和自减寻址功能的寄存器上操作时,编译器会把整数的push和pop操作编译成一条机器指令。
空间复杂度
链表:由于空间是动态申请、释放的,因此不会浪费空间,而且只要物理存储器允许,理论上能够满足最大范围未知的情况。
数组:必须在初始化时指定栈的大小,有可能会浪费空间,也有可能不够空间用。
结论:
如果对运行时的效率要求非常高,并且能够在初始化时预知栈的大小,那么应该首选数组形式;否则就应该选用链表形式。
由于对栈的操作永远都是针对栈顶(top)进行的,因此数组的随机存取的优点就没有了,而且数组必须预先分配空间,空间大小也受到限制,所以一般情况下(对运行时效率的要求不是太高)链表应该是首选。
栈的实现如下:
/////////////////////////////////////////////////////////////////////////////// // // FileName : stack.h // Version : 0.10 // Author : Luo Cong // Date : 2005-1-6 11:42:17 // Comment : // /////////////////////////////////////////////////////////////////////////////// #ifndef __STACK_H__ #define __STACK_H__ #include "../../slist/src/slist.h" template<typename T> class CStack : public CSList<T> { public: int push(T data); int pop(T *data = NULL); int top(T *data) const; }; template<typename T> inline int CStack<T>::push(T data) { return AddTail(data); } template<typename T> inline int CStack<T>::pop(T *data) { if (IsEmpty()) return 0; if (data) top(data); RemoveTail(); return 1; } template<typename T> inline int CStack<T>::top(T *data) const { ASSERT(data); if (IsEmpty()) return 0; *data = GetTail(); return 1; } #endif // __STACK_H__
调用如下:
/////////////////////////////////////////////////////////////////////////////// // // FileName : stack.cpp // Version : 0.10 // Author : Luo Cong // Date : 2005-1-6 11:42:28 // Comment : // /////////////////////////////////////////////////////////////////////////////// #include <iostream> #include "stack.h" using namespace std; static void PrintValue(const int nRetCode, const int nValue) { if (nRetCode) cout << nValue << endl; else cout << "Error occured!" << endl; } int main() { CStack<int> stack; int nValue; int nRetCode; #ifdef _DEBUG _CrtSetDbgFlag(_CRTDBG_ALLOC_MEM_DF | _CRTDBG_LEAK_CHECK_DF); #endif stack.push(1); stack.push(2); stack.push(3); nRetCode = stack.top(&nValue); PrintValue(nRetCode, nValue); nRetCode = stack.pop(&nValue); PrintValue(nRetCode, nValue); nRetCode = stack.pop(&nValue); PrintValue(nRetCode, nValue); nRetCode = stack.pop(&nValue); PrintValue(nRetCode, nValue); }
上面的代码就是在单链表的基础上实现的栈,您会看到,在C++的继承机制下,栈的实现简单得可怕。 :)
一个影响栈的运行效率的问题是错误检测。我的栈实现中是仔细地检查了错误的——对空栈进行top和pop操作,以及当存储空间不够时进行push操作是会引起异常的,显然,我们不愿意出现这种情况,但是,如果把对这些条件的检测放到代码中,那就很可能要花费像实际栈操作那样多的时间。由于这个原因,除非在错误处理极其重要的场合(例如在操作系统中),一般在栈中省去错误检测就成了普通的惯用手法。
但我认为,一个良好的程序首先应该是健壮的,这比效率还要重要,特别是对于栈这种最基本的数据结构,它很可能会被作为基本的元素而被别的地方大量地使用。所以我并没有因为效率的问题而省去了错误检查机制。
引入错误检查机制的代价是:
对top和pop的操作变得有些繁琐。在代码中我是使用了返回值0或者1来表示成功或者失败,而实际的栈顶元素是通过参数来返回的。这样做必定会有人不满——太麻烦了!但这是我能想到的最好的解决方法,如果你有更好的方法,请告诉我。
运行时效率会降低。如果确实耗费了太多的时间,你可以把错误检查去掉,但前提条件是你能确保整个运行过程中不会出错——其实还是要有错误检查的,只不过这些错误检查会放在外围来做而已。
好了,就说那么多,下面我们来看看栈的应用。
对栈的应用实在是太广泛了(谁让栈是最基本的数据结构元素之一呢?),例如有平衡符号、表达式转换之类的,我们在这里就选择一个比较有实用价值的例子——中缀到后缀表达式的转换。(可以用在编译器等地方)
/////////////////////////////////////////////////////////////////////////////// // // FileName : postfix.cpp // Version : 0.10 // Author : Luo Cong // Date : 2005-1-6 16:00:54 // Comment : // /////////////////////////////////////////////////////////////////////////////// // 算法: // 1)检查输入的下一元素。 // 2)假如是个操作数,输出。 // 3)假如是个开括号,将其压栈。 // 4)假如是个运算符,则 // i) 假如栈为空,将此运算符压栈。 // ii) 假如栈顶是开括号,将此运算符压栈。 // iii) 假如此运算符比栈顶运算符优先级高,将此运算符压入栈中。 // iv) 否则栈顶运算符出栈并输出,重复步骤4。 // 5)假如是个闭括号,栈中运算符逐个出栈并输出,直到遇到开括号。开括号出栈并丢弃。 // 6)假如输入还未完毕,跳转到步骤1。 // 7)假如输入完毕,栈中剩余的所有操作符出栈并输出它们。 #include <stdio.h> #include "stack.h" // 返回操作符的优先级 // +和-的优先级是一样的,*和/的优先级也是一样的,但+和-的优先级要比*和/的低。 static int GetPRI(const char optr) { switch (optr) { case '+': return 1; case '-': return 1; case '*': return 2; case '/': return 2; default : return 0; } } // 在这个函数中完成对栈顶的操作符和当前操作符的优先级对比, // 并决定是输出当前的操作符还是对当前的操作符进行入栈处理。 static void ProcessStackPRI( CStack<char> &stack, const char optr, char **szPostfix ) { ASSERT(*szPostfix); int i; int nRetCode; char chStackOptr; int nCount = stack.GetCount(); for (i = 0; i <= nCount; ++i) { nRetCode = stack.top(&chStackOptr); if ( (0 == nRetCode) || // 栈顶为空,新操作符添加到栈顶 (GetPRI(chStackOptr) < GetPRI(optr))// 栈顶操作符优先级比当前的要低 ) { stack.push(optr); break; } else { // 如果栈顶操作符优先级不低于当前的,则栈顶元素出栈并输出: stack.pop(); *(*szPostfix)++ = chStackOptr; } } } static void Infix2Postfix( const char *szInfix, char *szPostfix ) { ASSERT(szPostfix); char chOptr; int nRetCode; CStack<char> stack; while (*szInfix) { switch (*szInfix) { // 忽略空格和TAB: case ' ': case '/t': break; // 对操作符进行优先级判断,以便决定是入栈还是输出: case '+': case '-': case '*': case '/': nRetCode = stack.IsEmpty(); if (!nRetCode) ProcessStackPRI(stack, *szInfix, &szPostfix); else stack.push(*szInfix); // 当栈为空时,毫无疑问操作符应该入栈 break; // 遇到左括号时,无条件入栈,因为它的优先级是最高的 case '(': stack.push(*szInfix); break; // 遇到右括号时,逐个把栈中的操作符出栈,直到遇到左括号为止 case ')': do { nRetCode = stack.pop(&chOptr); if (nRetCode && ('(' != chOptr)) // 左括号本身不输出 *szPostfix++ = chOptr; } while (!stack.IsEmpty() && ('(' != chOptr)); // 遇到左括号为止 break; // 其余的情况,直接输出即可 default: *szPostfix++ = *szInfix; break; } ++szInfix; } // 如果输入的内容已经分析完毕,那么就把栈中剩余的操作符全部出栈 while (!stack.IsEmpty()) { nRetCode = stack.pop(&chOptr); *szPostfix++ = chOptr; } *szPostfix = '/0'; } int main() { char *szInfix = "a+b*c+(d*e+f)*g"; char szPostfix[255]; #ifdef _DEBUG _CrtSetDbgFlag(_CRTDBG_ALLOC_MEM_DF | _CRTDBG_LEAK_CHECK_DF); #endif Infix2Postfix(szInfix, szPostfix); printf("Infix : %s/n", szInfix); printf("Postfix : %s/n", szPostfix); }
源代码里面已经有了详细的注释,我就不再罗嗦了。我只做了+、-、*、/四种操作符的转换,另外,如果括号不匹配,例如有左括号但是没有右括号,或者反过来,程序就可能会运行不正确,但这不是我写这个例子的重点,我写它只是为了掌握栈的用法,如果您有兴趣,可以试着完善它。
下面给出两个例子:
中缀表达式:a + b * c + (d * e + f) * g 后缀表达式:abc*+de*f+g*+
中缀表达式:2 * (x + y) / (1 - x) 后缀表达式:2xy+*1x-/
第六章
队列6.1 基本概念
像栈一样,队列(queue)也是表。然而,使用队列时插入在一端进行而删除则在另一端进行,也就是先进先出(FIFO)。队列的基本操作是EnQueue(入队),它是在表的末端(叫做队尾(rear))插入一个元素;还有DeQueue(出队),它是删除(或返回)在表的开头(叫做队头(front))的元素。
队列一般有链式队列和循环队列两种。链式队列相当于我们在银行中排队,后来的人排到队伍的最后,前面的人办理完业务后就会离开,让下一个人进去;循环队列则跟循环链表很相似。
我在此只写出链式队列的代码,循环队列其实也可以继承自循环链表,就不多罗嗦了。可以看到,队列的实现也是惊人的简单。
6.2 代码实现
队列的实现如下:
/////////////////////////////////////////////////////////////////////////////// // // FileName : lqueue.h // Version : 0.10 // Author : Luo Cong // Date : 2005-1-8 16:49:54 // Comment : // /////////////////////////////////////////////////////////////////////////////// #ifndef __LIST_QUEUE_H__ #define __LIST_QUEUE_H__ #include "../../slist/src/slist.h" template<typename T> class CLQueue : public CSList<T> { public: int EnQueue(const T data); T DeQueue(); T& GetFront(); T GetFront() const; T& GetRear(); T GetRear() const; }; template<typename T> inline int CLQueue<T>::EnQueue(const T data) { return AddTail(data); } template<typename T> inline T CLQueue<T>::DeQueue() { T data = GetHead(); RemoveHead(); return data; } template<typename T> inline T& CLQueue<T>::GetFront() { return GetHead(); } template<typename T> inline T CLQueue<T>::GetFront() const { return GetHead(); } template<typename T> inline T& CLQueue<T>::GetRear() { return GetTail(); } template<typename T> inline T CLQueue<T>::GetRear() const { return GetTail(); } #endif // __LIST_QUEUE_H__调用如下:
/////////////////////////////////////////////////////////////////////////////// // // FileName : queue.cpp // Version : 0.10 // Author : Luo Cong // Date : 2005-1-8 17:00:40 // Comment : // /////////////////////////////////////////////////////////////////////////////// #include <iostream> #include "lqueue.h" using namespace std; int main() { CLQueue<int> queue; #ifdef _DEBUG _CrtSetDbgFlag(_CRTDBG_ALLOC_MEM_DF | _CRTDBG_LEAK_CHECK_DF); #endif queue.EnQueue(1); queue.EnQueue(2); queue.EnQueue(3); while (!queue.IsEmpty()) cout << queue.DeQueue() << endl; }6.3 应用
队列的应用一般来说是模拟现实生活中的一些离散现象,例如银行排队、打印机任务、接线员工作等等。还有的就是使用队列来提高运行效率的算法,这些一般是在图算法中使用到。考虑到队列的应用要么是比较简单,要么是在特定的环境中进行,因此我就不给出应用的例子了,如果您有兴趣的话可以自行试试。
第七章
递归7.1 基本概念
按照原书的流程,现在应该讲到递归了。递归是一种有力的数学工具。不知道各位学过Lisp或者它的方言没有(例如Scheme),如果学过的话,一定会对递归非常熟悉,因为在Lisp和它的方言中,是没有循环语句的,如果您要构造一个循环,必须通过递归的形式来实现。当时我的脑袋怎么也转不过弯来,因为我已经习惯了在C/C++里面使用for、while等语句来循环了,在Lisp里面刚开始我几乎没有办法写出一个不出错的循环来。
例如,下面的代码:
for (int i = 0; i <= 10; ++i) { }可以被转换成递归:
void recursion_loop(int i) { if (i == 10) return; else recursion_loop(i + 1); } // 调用: recursion_loop(0);递归具有以下的性质:
递归就是在某个过程中重复调用它本身。例如在上面的例子中,就是在recursion_loop()这个函数中再调用它本身。
必须有停止条件。这很容易理解,因为如果没有停止条件的话,那么这个递归就会子子孙孙无穷溃也。例如在上面的例子中,
if (i == 10)
就是停止的条件。递归会受到现实中的限制,例如栈的大小不够而导致失败。这是因为在计算机中,栈的大小是有上限的,而每次递归调用函数本身,都需要在栈中保存返回地址、参数等信息,在经过N次递归之后,栈很可能就会满了,这样就会导致无法进行第(N+1)次递归。
根据上面的性质3我们可以知道,并不是所有的语言都支持递归的——如果某种语言能够支持递归,那么它必须是支持“栈”这种结构的。目前就我所知道的对递归的使用发挥得最淋漓尽致的语言,Lisp和它的方言是当之无愧的王者。
7.2 应用
唉,本来都不想写递归的例子了,因为这些例子已经被写过无数次。提到递归,就一定会说到阶乘、斐波那契数列和汉诺塔这三个例子,但本着把教科书过一遍的目的,我还是再进行一次重复劳动吧(但不再对这三个例子进行讲解了,随便找一本数据结构的书都会有这方面的内容)。最后增加一个帕斯卡三角形,在我国也就是著名的杨辉三角。
7.2.1 阶乘
/////////////////////////////////////////////////////////////////////////////// // // FileName : factorial.c // Version : 0.10 // Author : Luo Cong // Date : 2005-1-8 21:23:16 // Comment : // /////////////////////////////////////////////////////////////////////////////// #include <stdio.h> static long factorial(const long n) { return 0 == n || 1 == n ? 1 : n * factorial(n - 1); } int main() { long lResult = factorial(10); printf("%ld/n", lResult); }7.2.2 斐波那契数列
/////////////////////////////////////////////////////////////////////////////// // // FileName : fib.c // Version : 0.10 // Author : Luo Cong // Date : 2005-1-8 21:28:56 // Comment : // /////////////////////////////////////////////////////////////////////////////// #include <stdio.h> static long fib(const long n) { return 0 == n || 1 == n ? 1 : fib(n - 1) + fib(n - 2); } int main() { long lResult = fib(10); printf("%ld/n", lResult); }7.2.3 汉诺塔
/////////////////////////////////////////////////////////////////////////////// // // FileName : hanoi.c // Version : 0.10 // Author : Luo Cong // Date : 2005-1-8 21:40:44 // Comment : // /////////////////////////////////////////////////////////////////////////////// #include <stdio.h> static void move(const char x, const int n, const char z) { printf("把圆盘 %d 从柱子 %c 移动到 %c 上/n", n, x, z); } static void hanoi(const int n, const char x, const char y, const char z) { if (1 == n) move(x, 1, z); // 如果只有一个盘,则直接将它从x移动到z else { hanoi(n - 1, x, z, y); // 把1 ~ n - 1个盘从x移动到y,用z作为中转 move(x, n, z); // 把第n个盘从x移动到z hanoi(n - 1, y, x, z); // 把1 ~ n - 1个盘从y移动到z,用x作为中转 } } int main() { hanoi(1, 'X', 'Y', 'Z'); }7.2.4 帕斯卡三角形(杨辉三角)
下面的数值被称为帕斯卡三角形,在我国则是著名的杨辉三角:
1 1 1 1 2 1 1 3 3 1 1 4 6 4 1三角形边界上的数都是1,内部的每个数是位于它上面的两个数之和。
利用递归我们可以很容易地把问题转换为这个性质:
假设f(row, col)表示杨辉三角的第row行的第col个元素,那么:
f(row, col) = 1 (col = 1 或者 row = col),也就是递归的停止条件。
f(row, col) = f(row - 1, col - 1) + f(row - 1, col),也就是上一行的两个相邻元素的和。
有了这个性质,我们的递归程序就容易写了。^_^
/////////////////////////////////////////////////////////////////////////////// // // FileName : pascaltriangle.c // Version : 0.10 // Author : Luo Cong // Date : 2005-1-9 14:53:57 // Comment : // /////////////////////////////////////////////////////////////////////////////// #include <stdio.h> static long GetElement(const long row, const long col) { // 每行的外围两个元素为1 if ((1 == col) || (row == col)) return 1; else // 其余的部分为上一行的(col - 1)和(col)元素之和 return GetElement(row - 1, col - 1) + GetElement(row - 1, col); } static long PascalTriangle(const long n) { int row; int col; for (row = 1; row <= n; ++row) { for (col = 1; col <= row; ++col) printf(" %4ld", GetElement(row, col)); printf("/n"); } } int main() { PascalTriangle(5); }
第八章
二叉树8.1 基本概念
树是一种非线性的数据结构,它在客观世界中广泛存在,例如人类社会的族谱和各种社会组织机构都可以用树来表示。我们最常用到的是树和二叉树,其中又以二叉树更为实用。为什么这样说呢?因为大部分的操作都可以转变为一个父亲、一个左儿子和一个右儿子来实现,而且对二叉树的操作更为简单。
8.2 代码实现
二叉树的代码实现如下:
/////////////////////////////////////////////////////////////////////////////// // // FileName : btree.h // Version : 0.10 // Author : Luo Cong // Date : 2005-1-12 12:22:40 // Comment : // /////////////////////////////////////////////////////////////////////////////// #ifndef __BINARY_TREE_H__ #define __BINARY_TREE_H__ #include <assert.h> #include <crtdbg.h> #ifdef _DEBUG #define DEBUG_NEW new (_NORMAL_BLOCK, THIS_FILE, __LINE__) #endif #ifdef _DEBUG #define new DEBUG_NEW #undef THIS_FILE static char THIS_FILE[] = __FILE__; #endif #ifdef _DEBUG #ifndef ASSERT #define ASSERT assert #endif #else // not _DEBUG #ifndef ASSERT #define ASSERT #endif #endif // _DEBUG template<typename T> class CBTNode { public: T data; CBTNode<T> *parent; CBTNode<T> *left; CBTNode<T> *right; CBTNode( T data = T(), CBTNode<T> *parent = NULL, CBTNode<T> *left = NULL, CBTNode<T> *right = NULL ) : data(data), parent(parent), left(left), right(right) {} }; template<typename T> class CBTree { protected: CBTNode<T> *m_pNodeRoot; public: CBTree(CBTNode<T> *initroot = NULL); ~CBTree(); void AssignTo(CBTNode<T> *p); void Copy(CBTree<T> &p); private: CBTNode<T>* Copy(CBTNode<T> *p); void DestroyNode(CBTNode<T> *p); void PreOrderTraverse( const CBTNode<T> *p, void (*Visit)(const T &data) ) const; void InOrderTraverse( const CBTNode<T> *p, void (*Visit)(const T &data) ) const; void PostOrderTraverse( const CBTNode<T> *p, void (*Visit)(const T &data) ) const; void GetNodesCount(const CBTNode<T> *p, unsigned int *unCount) const; void GetLeafCount(const CBTNode<T> *p, unsigned int *unCount) const; unsigned int GetDepth(const CBTNode<T> *p) const; public: T& GetNodeData(CBTNode<T> *p); T GetNodeData(const CBTNode<T> *p) const; void SetNodeData(CBTNode<T> *p, const T &data); CBTNode<T>*& GetRoot(); CBTNode<T>* GetRoot() const; CBTNode<T>*& GetParent(CBTNode<T> *p); CBTNode<T>* GetParent(const CBTNode<T> *p) const; CBTNode<T>*& GetLeftChild(CBTNode<T> *p); CBTNode<T>* GetLeftChild(const CBTNode<T> *p) const; CBTNode<T>*& GetRightChild(CBTNode<T> *p); CBTNode<T>* GetRightChild(const CBTNode<T> *p) const; CBTNode<T>*& GetLeftSibling(CBTNode<T> *p); CBTNode<T>* GetLeftSiblig(const CBTNode<T> *p) const; CBTNode<T>*& GetRightSibling(CBTNode<T> *p); CBTNode<T>* GetRightSibling(const CBTNode<T> *p) const; public: int IsEmpty() const; void Destroy(); void PreOrderTraverse(void (*Visit)(const T &data)) const; void InOrderTraverse(void (*Visit)(const T &data)) const; void PostOrderTraverse(void (*Visit)(const T &data)) const; unsigned int GetNodesCount() const; // Get how many nodes unsigned int GetLeafCount() const; unsigned int GetDepth() const; }; template<typename T> inline CBTree<T>::CBTree(CBTNode<T> *initroot) : m_pNodeRoot(initroot) { } template<typename T> inline CBTree<T>::~CBTree() { Destroy(); } template<typename T> inline void CBTree<T>::AssignTo(CBTNode<T> *p) { ASSERT(p); m_pNodeRoot = p; } template<typename T> inline void CBTree<T>::Copy(CBTree<T> &p) { if (NULL != p.m_pNodeRoot) m_pNodeRoot = Copy(p.m_pNodeRoot); else m_pNodeRoot = NULL; } template<typename T> inline CBTNode<T>* CBTree<T>::Copy(CBTNode<T> *p) { CBTNode<T> *pNewNode; if (p) { pNewNode = new CBTNode<T>; if (NULL == pNewNode) return NULL; pNewNode->data = p->data; pNewNode->parent = p->parent; pNewNode->left = Copy(p->left); pNewNode->right = Copy(p->right); return pNewNode; } else return NULL; } template<typename T> inline CBTNode<T>*& CBTree<T>::GetLeftChild(CBTNode<T> *p) { ASSERT(p); return *(&(p->left)); } template<typename T> inline CBTNode<T>* CBTree<T>::GetLeftChild(const CBTNode<T> *p) const { ASSERT(p); return p->left; } template<typename T> inline CBTNode<T>*& CBTree<T>::GetRightChild(CBTNode<T> *p) { ASSERT(p); return *(&(p->right)); } template<typename T> inline CBTNode<T>* CBTree<T>::GetRightChild(const CBTNode<T> *p) const { ASSERT(p); return p->right; } template<typename T> inline CBTNode<T>*& CBTree<T>::GetLeftSibling(CBTNode<T> *p) { ASSERT(p); if (p->parent) return *(&(p->parent->left)); else return *(&(p->parent)); // return NULL; } template<typename T> inline CBTNode<T>* CBTree<T>::GetLeftSiblig(const CBTNode<T> *p) const { ASSERT(p); if (p->parent) return p->parent->left; else return p->parent; // return NULL; } template<typename T> inline CBTNode<T>*& CBTree<T>::GetRightSibling(CBTNode<T> *p) { ASSERT(p); if (p->parent) return *(&(p->parent->right)); else return *(&(p->parent)); // return NULL; } template<typename T> inline CBTNode<T>* CBTree<T>::GetRightSibling(const CBTNode<T> *p) const { ASSERT(p); if (p->parent) return p->parent->right; else return p->parent; // return NULL; } template<typename T> inline CBTNode<T>*& CBTree<T>::GetParent(CBTNode<T> *p) { ASSERT(p); return *(&(p->parent)); } template<typename T> inline CBTNode<T>* CBTree<T>::GetParent(const CBTNode<T> *p) const { ASSERT(p); return p->parent; } template<typename T> inline T& CBTree<T>::GetNodeData(CBTNode<T> *p) { ASSERT(p); return p->data; } template<typename T> inline T CBTree<T>::GetNodeData(const CBTNode<T> *p) const { ASSERT(p); return p->data; } template<typename T> inline void CBTree<T>::SetNodeData(CBTNode<T> *p, const T &data) { ASSERT(p); p->data = data; } template<typename T> inline int CBTree<T>::IsEmpty() const { return NULL == m_pNodeRoot; } template<typename T> inline CBTNode<T>*& CBTree<T>::GetRoot() { return *(&(m_pNodeRoot)); } template<typename T> inline CBTNode<T>* CBTree<T>::GetRoot() const { return m_pNodeRoot; } template<typename T> inline void CBTree<T>::DestroyNode(CBTNode<T> *p) { if (p) { DestroyNode(p->left); DestroyNode(p->right); delete p; } } template<typename T> inline void CBTree<T>::Destroy() { DestroyNode(m_pNodeRoot); m_pNodeRoot = NULL; } template<typename T> inline void CBTree<T>::PreOrderTraverse(void (*Visit)(const T &data)) const { PreOrderTraverse(m_pNodeRoot, Visit); } template<typename T> inline void CBTree<T>::PreOrderTraverse( const CBTNode<T> *p, void (*Visit)(const T &data) ) const { if (p) { Visit(p->data); PreOrderTraverse(p->left, Visit); PreOrderTraverse(p->right, Visit); } } template<typename T> inline void CBTree<T>::InOrderTraverse(void (*Visit)(const T &data)) const { InOrderTraverse(m_pNodeRoot, Visit); } template<typename T> inline void CBTree<T>::InOrderTraverse( const CBTNode<T> *p, void (*Visit)(const T &data) ) const { if (p) { InOrderTraverse(p->left, Visit); Visit(p->data); InOrderTraverse(p->right, Visit); } } template<typename T> inline void CBTree<T>::PostOrderTraverse(void (*Visit)(const T &data)) const { PostOrderTraverse(m_pNodeRoot, Visit); } template<typename T> inline void CBTree<T>::PostOrderTraverse( const CBTNode<T> *p, void (*Visit)(const T &data) ) const { if (p) { PostOrderTraverse(p->left, Visit); PostOrderTraverse(p->right, Visit); Visit(p->data); } } template<typename T> inline unsigned int CBTree<T>::GetNodesCount() const { unsigned int unCount; GetNodesCount(m_pNodeRoot, &unCount); return unCount; } template<typename T> inline void CBTree<T>::GetNodesCount( const CBTNode<T> *p, unsigned int *unCount ) const { ASSERT(unCount); unsigned int unLeftCount; unsigned int unRightCount; if (NULL == p) *unCount = 0; else if ((NULL == p->left) && (NULL == p->right)) *unCount = 1; else { GetNodesCount(p->left, &unLeftCount); GetNodesCount(p->right, &unRightCount); *unCount = 1 + unLeftCount + unRightCount; } } template<typename T> inline unsigned int CBTree<T>::GetLeafCount() const { unsigned int unCount = 0; GetLeafCount(m_pNodeRoot, &unCount); return unCount; } template<typename T> inline void CBTree<T>::GetLeafCount( const CBTNode<T> *p, unsigned int *unCount ) const { ASSERT(unCount); if (p) { // if the node's left & right children are both NULL, it must be a leaf if ((NULL == p->left) && (NULL == p->right)) ++(*unCount); GetLeafCount(p->left, unCount); GetLeafCount(p->right, unCount); } } template<typename T> inline unsigned int CBTree<T>::GetDepth() const { // I minus 1 here because I think the root node's depth should be 0. // So, if u think the root node's depth should be 1, then needn't minus. return GetDepth(m_pNodeRoot) - 1; } template<typename T> inline unsigned int CBTree<T>::GetDepth(const CBTNode<T> *p) const { unsigned int unDepthLeft; unsigned int unDepthRight; if (p) { unDepthLeft = GetDepth(p->left); unDepthRight = GetDepth(p->right); return 1 + // if don't plus 1 here, the tree's depth will be always 0 (unDepthLeft > unDepthRight ? unDepthLeft : unDepthRight); } else return 0; } #endif // __BINARY_TREE_H__测试代码:
/////////////////////////////////////////////////////////////////////////////// // // FileName : btree.cpp // Version : 0.10 // Author : Luo Cong // Date : 2005-1-12 13:17:07 // Comment : // /////////////////////////////////////////////////////////////////////////////// #include <iostream> #include "btree.h" using namespace std; // 结点的数据类型 typedef char ElementType; // 回调函数:Visit() = PrintElement() static void PrintElement(const ElementType &data) { cout << data; } int main() { CBTNode<ElementType> *pRoot; CBTNode<ElementType> *pLeftChild; CBTNode<ElementType> *pRightChild; CBTree<ElementType> btree; #ifdef _DEBUG _CrtSetDbgFlag(_CRTDBG_ALLOC_MEM_DF | _CRTDBG_LEAK_CHECK_DF); #endif pRoot = new CBTNode<ElementType>; if (NULL == pRoot) return EXIT_FAILURE; pLeftChild = new CBTNode<ElementType>; if (NULL == pLeftChild) return EXIT_FAILURE; pRightChild = new CBTNode<ElementType>; if (NULL == pRightChild) return EXIT_FAILURE; // 创建父亲结点 pRoot->data = '+'; pRoot->parent = NULL; pRoot->left = pLeftChild; pRoot->right = pRightChild; // 创建左儿子结点 pLeftChild->data = 'a'; pLeftChild->parent = pRoot; pLeftChild->left = NULL; pLeftChild->right = NULL; // 创建右儿子结点 pRightChild->data = 'b'; pRightChild->parent = pRoot; pRightChild->left = NULL; pRightChild->right = NULL; // 创建二叉树 btree.AssignTo(pRoot); // 输出这棵二叉树 cout << " (" << btree.GetNodeData(btree.GetRoot()) << ") " << endl; cout << " / // " << endl; cout << "(" << btree.GetNodeData(btree.GetLeftChild(btree.GetRoot())) << ") (" << btree.GetNodeData(btree.GetRightChild(btree.GetRoot())) << ")" << endl << endl; cout << "这棵树的叶子数:" << btree.GetLeafCount() << endl; cout << "这棵树的深度是:" << btree.GetDepth() << endl; cout << "先序遍历:"; btree.PreOrderTraverse(PrintElement); cout << endl << "中序遍历:"; btree.InOrderTraverse(PrintElement); cout << endl << "后序遍历:"; btree.PostOrderTraverse(PrintElement); cout << endl; return EXIT_SUCCESS; }8.3 说明
您也许已经注意到了一个“奇怪”的现象:在我的二叉树实现中,有各种对结点的访问操作(例如计算树的高、各种遍历),但就是没有插入和删除这两个操作的函数。其实这并不值得奇怪。因为二叉树基本上是一个最“底层”的类,将来我们在写二叉搜索树等更高级的类时,是要从二叉树开始继承的,而对于树这种非线性的数据结构来说,插入和删除是要根据它所处的环境来具体问题具体分析的——也就是说,没有一个特定的法则(这点不像链表,链表无论怎么变,它都是线性的)。所以,在具体的应用中,我才会给出具体的插入和删除代码。在这里,我用了一种很拙劣的方式来创建了一棵二叉树,请读者在这个问题上不要深究。
在结点类CBTNode中,我定义了4个成员变量:data、parent、left和right。data表示该结点的数据域,parent表示该结点的父亲结点,left和right分别表示该结点的左右儿子结点。这里要说明的是:
parent指针并不是必需的,但有了它之后,就会大大简化许多对父亲结点的操作。因此,在资源并不十分紧张的情况下应该考虑加入它。
二叉树的根结点(root)的parent应该赋值为
NULL
。在二叉树中还大量运用了前面所说的一个强大的工具——递归。例如对二叉树的遍历操作就都是通过递归来实现的(不递归也行,可以用栈来模拟,但速度会比较慢,同时也多占用了很多空间,也就是说,非递归的算法无论是时间复杂度还是空间复杂度都比递归要高——非递归的唯一好处只是节省了堆栈。因此到底选用哪个,就要看具体的应用环境了)。另外,我在先序、中序和后序遍历中用了Visit()这个回调函数,这是为了增加处理的自由度。除此之外,我还写了几个要使用到遍历技术的子函数,如:GetLeafCount(),就是用先序遍历来获得二叉树的叶子个数。在此不一一而足,如有不清楚的地方,请联系我。
8.4 应用
基本的二叉树还谈不上有什么应用,因此我的示例程序只是做了一个对表达式的转换……您是不是想说,对表达式的转换不是在栈那里已经做过了吗?
是的!但实际上二叉树这种数据结构才是对表达式的最直观的储存和表达方式,甚至可以说,它天生就是一棵表达式!我的例子代码是用一棵二叉树来表示一个表达式:a + b,执行完后,会得到这样的输出结果:
(+) / / (a) (b) 这棵树的叶子数:2 这棵树的深度是:1 先序遍历:+ab 中序遍历:a+b 后序遍历:ab+
第九章
二叉搜索树9.1 基本概念
二叉树的一个重要的应用是它们在查找中的使用。二叉搜索树的概念相当容易理解,即:对于树中的每个结点X,它的左子树中所有关键字的值都小于X的关键字值,而它的右子树中的所有关键字值都大于X的关键字值。这意味着该树所有的元素都可以用某种统一的方式排序。
例如下面就是一棵合法的二叉搜索树:
6 / / 2 8 / / 1 4 / 3二叉搜索树的性质决定了它在搜索方面有着非常出色的表现:要找到一棵树的最小结点,只需要从根结点开始,只要有左儿子就向左进行,终止结点就是最小的结点。找最大的结点则是往右进行。例如上面的例子中,最小的结点是1,在最左边;最大的结点是8,在最右边。
9.2 代码实现
二叉树的代码实现如下:
/////////////////////////////////////////////////////////////////////////////// // // FileName : bstree.h // Version : 0.10 // Author : Luo Cong // Date : 2005-1-17 22:53:52 // Comment : // /////////////////////////////////////////////////////////////////////////////// #ifndef __BINARY_SEARCH_TREE_H__ #define __BINARY_SEARCH_TREE_H__ #include "../../btree/src/btree.h" template<typename T> class CBSTree : public CBTree<T> { private: CBTNode<T>* Find(const T &data, CBTNode<T> *p) const; CBTNode<T>* FindMin(CBTNode<T> *p) const; CBTNode<T>* FindMax(CBTNode<T> *p) const; CBTNode<T>* Insert(const T &data, CBTNode<T> *p); CBTNode<T>* Delete(const T &data, CBTNode<T> *p); public: CBTNode<T>* Find(const T &data) const; CBTNode<T>* FindMin() const; CBTNode<T>* FindMax() const; CBTNode<T>* Insert(const T &data); CBTNode<T>* Delete(const T &data); }; template<typename T> inline CBTNode<T>* CBSTree<T>::Find(const T &data) const { return Find(data, m_pNodeRoot); } template<typename T> inline CBTNode<T>* CBSTree<T>::Find(const T &data, CBTNode<T> *p) const { if (NULL == p) return NULL; if (data < p->data) return Find(data, p->left); else if (data > p->data) return Find(data, p->right); else return p; } template<typename T> inline CBTNode<T>* CBSTree<T>::FindMin() const { return FindMin(m_pNodeRoot); } template<typename T> inline CBTNode<T>* CBSTree<T>::FindMin(CBTNode<T> *p) const { if (NULL == p) return NULL; else if (NULL == p->left) return p; else return FindMin(p->left); } template<typename T> inline CBTNode<T>* CBSTree<T>::FindMax() const { return FindMax(m_pNodeRoot); } template<typename T> inline CBTNode<T>* CBSTree<T>::FindMax(CBTNode<T> *p) const { if (NULL == p) return NULL; else if (NULL == p->right) return p; else return FindMax(p->right); } template<typename T> inline CBTNode<T>* CBSTree<T>::Insert(const T &data) { return Insert(data, m_pNodeRoot); } template<typename T> inline CBTNode<T>* CBSTree<T>::Insert(const T &data, CBTNode<T> *p) { if (NULL == p) { p = new CBTNode<T>; if (NULL == p) return NULL; else { p->data = data; p->left = NULL; p->right = NULL; if (NULL == m_pNodeRoot) { m_pNodeRoot = p; m_pNodeRoot->parent = NULL; } } } else if (data < p->data) { p->left = Insert(data, p->left); if (p->left) p->left->parent = p; } else if (data > p->data) { p->right = Insert(data, p->right); if (p->right) p->right->parent = p; } // else data is in the tree already, we'll do nothing! return p; } template<typename T> inline CBTNode<T>* CBSTree<T>::Delete(const T &data) { return Delete(data, m_pNodeRoot); } template<typename T> inline CBTNode<T>* CBSTree<T>::Delete(const T &data, CBTNode<T> *p) { if (NULL == p) { // Error! data not found! } else if (data < p->data) { p->left = Delete(data, p->left); } else if (data > p->data) { p->right = Delete(data, p->right); } else if (p->left && p->right) // found it, and it has two children { CBTNode<T> *pTmp = FindMin(p->right); p->data = pTmp->data; p->right = Delete(p->data, p->right); } else // found it, and it has one or zero children { CBTNode<T> *pTmp = p; if (NULL == p->left) p = p->right; else if (NULL == p->right) p = p->left; if (p) p->parent = pTmp->parent; if (m_pNodeRoot == pTmp) m_pNodeRoot = p; delete pTmp; } return p; } #endif // __BINARY_SEARCH_TREE_H__测试代码:
/////////////////////////////////////////////////////////////////////////////// // // FileName : bstree.cpp // Version : 0.10 // Author : Luo Cong // Date : 2005-1-17 22:55:12 // Comment : // /////////////////////////////////////////////////////////////////////////////// #include "bstree.h" int main() { CBSTree<int> bstree; #ifdef _DEBUG _CrtSetDbgFlag(_CRTDBG_ALLOC_MEM_DF | _CRTDBG_LEAK_CHECK_DF); #endif bstree.Insert(1); bstree.Insert(2); bstree.Insert(3); bstree.Delete(1); }9.3 说明
我的二叉搜索树是从二叉树继承而来的,我写了Find()、FindMin()、FindMax()、Insert()和Delete()一共5个成员函数。这里要说的是,对非线性数据结构的操作总是特别的不直观,因为一般来说我们会选择使用递归——而人脑一般不太容易“调试”递归的程序——如果递归的层数比较少(例如只有1、2次)那还好点,但一旦超过5、6次,恐怕人脑的“堆栈”就要溢出了。
好了,牢骚完毕,来解释一下:
Find():如果树为空,则返回NULL;如果根结点比它的左儿子要小,就往左进行,否则如果比右儿子小就往右进行,一直到既不大于也不小于它的儿子为止,那么这个结点就一定是我们要找的了。
FindMin():从根结点开始,只要有左儿子就向左进行,直到遇到终止结点为止。
FindMax():除分支朝右儿子进行外,其余过程与FindMin()相同。
Insert():如果找到了相同的元素,则什么都不做;否则,递归查找到遍历路径的最后一点上,然后执行Insert操作。
Delete():正如许多数据结构一样,最困难的操作是删除。删除的操作可以分成下面几种情况:
如果结点是一篇树叶,那么它可以被立即删除。
如果结点有一个儿子,那么该结点可以在其父结点调整指针绕过该结点后被删除。
最复杂的情况是处理具有两个儿子的结点。我们可以用其右子树的最小的数据(很容易找到)代替该结点的数据,并递归地删除那个结点。为什么?因为一个结点肯定比它的右子树的所有结点都小,同时又比它的左子树的所有结点都大,所以我们只要在其右子树中找到最小的那个结点来代替它,就能满足二叉树的性质了。(根据这个规则,我们还可以用其左子树的最大的数据来代替该结点的数据,道理是一样的,不再叙述)
说了那么多,估计我还是没有讲清楚(主要是有点抽象),请读者编译我的代码并亲自动手调试一下吧。我的测试代码没有输出结果,因为要写个打印二叉树的函数我觉得有点烦,您可以在相应的函数中下断点,我个人认为只要能弄懂Delete()函数,那别的应该都没问题了。:)