本文将介绍在程序执行期间动态消长的动态数据结构,包括链表(linked list)、栈(stack)、队列(queue)、二叉树(binary tree)。这些动态数据结构与定长数据结构(数组)的区别在于前者的长度是动态分配的,而后者为固定长度。
链表是多个数据节点(node)的线性集合,这些节点通过指针链(link)链接起来,是一种线性数据结构。在链表的任何一项数据项上都可以进行插入和删除操作。
(1)自引用类
链表的节点就是自用类对象,所以先理解自引用类的概念。一个自引用类包含一个指向它同类的对象的指针成员,以下即为一个自引用类,如下所示。
class Node { public: Node(int); private: int element; Node *nextPtr; //指向同类对象的指针 };
一个Node类对象的nextPtr指针可以指向下一个Node类对象,这样自引用类对象可以由自己的指针链接成有用的数据结构,除了链表,还有队列、栈、树等。
(2)链表的结构
链表为线性结构,可以想象为一些数据节点排列成一行,然后链表具有指向头节点的指针firstPtr和指向尾节点的指针lastPtr,头节点的指针指向下一个节点,下一个节点的指针指向再下一个节点,直至最后一个节点,而尾节点的指针则置为空(0),表示链表的结束。结构可由下图所示。
(3)下面由模板类list类和模板类NodeInList类构成链表结构,NodeInList类的对象作为节点这一数据结构,而list类对象用指针将多个节点链接起来。
//NodeInList.h #ifndef NODEINLIST_H #define NODEINLIST_H template<typename T> class List;//List类存在便于在下面类中声明为友元类 template<typename T> class NodeInList { friend class List<T>; //声明List类为其友元类 public: NodeInList(const T &); T getElement() const; private: T element; NodeInList<T> *nextPtr; //指向下一个节点的指针 }; template<typename T> NodeInList<T>::NodeInList(const T &data) :element(data),nextPtr(0) { } template<typename T> T NodeInList<T>::getElement()const { return element; } #endif
//List.h #ifndef LIST_H #define LIST_H #include <iostream> using std::cout; #include "NodeInList.h" //包含NodeInList类定义的头文件 template<typename T> class List { public: List(); ~List(); void inSertAtFront(const T &); //链表头插入数据 void insertAtBack(const T &); //链表末插入数据 bool removeFromFront(T &); //从链表头删除数据 bool removeFromBack(T &); //从链表末删除数据 bool isEmpty()const; //检测是否为空 void print() const; //打印链表中的数据 private: NodeInList<T> *firstPtr; //指向链表头节点的指针 NodeInList<T> *lastPtr; //指向链表末节点的指针 NodeInList<T> *createNewNode(const T &); //开辟节点空间 }; template<typename T> List<T>::List() :firstPtr(0),lastPtr(0) { } template<typename T> List<T>::~List() { if(!isEmpty()) { cout<<"Destroying nodes!\n"; NodeInList<T> *currentPtr = firstPtr; NodeInList<T> *tempPtr; while( currentPtr != 0) //如果当前指针不为空 { tempPtr =currentPtr; cout<<tempPtr->element<<'\n'; //输出当前指针的指向节点的数据 currentPtr =currentPtr->nextPtr; //当前指针指向再下一个节点 delete tempPtr; //删除之前的指针 } } cout<<"All nodes destroyed\n\n"; } template<typename T> void List<T>::inSertAtFront( const T& value) { NodeInList<T> *newPtr = createNewNode( value); if(isEmpty()) { firstPtr = lastPtr =newPtr; } else { newPtr->nextPtr = firstPtr; //新节点的指针指向头节点 firstPtr = newPtr; //firstPtr指针指向新节点 } } template<typename T> void List<T>::insertAtBack(const T &value ) { NodeInList<T> *newPtr = createNewNode( value); if(isEmpty()) { firstPtr = lastPtr =newPtr; } else { lastPtr->nextPtr =newPtr; //新节点的指针指向末节点 lastPtr = newPtr; //lastPtr指向新节点 } } template<typename T> bool List<T>::removeFromFront(T &value) { if(isEmpty()) return false; else { NodeInList<T> *tempPtr =firstPtr; if(firstPtr == lastPtr) firstPtr = lastPtr =0; else firstPtr = firstPtr->nextPtr; value = tempPtr->element; delete tempPtr; return true; } } template<typename T> bool List<T>::removeFromBack(T &value) { if(isEmpty()) return false; else { NodeInList<T> *tempPtr =lastPtr; if(firstPtr == lastPtr) firstPtr = lastPtr =0; else { NodeInList<T> *currentPtr =firstPtr; while(currentPtr->nextPtr != tempPtr) currentPtr =currentPtr->nextPtr; lastPtr = currentPtr; currentPtr->nextPtr =0 ; } value =tempPtr->element; delete tempPtr; return true; } } template<typename T> bool List<T>::isEmpty()const { return firstPtr == 0; } template<typename T> NodeInList<T>* List<T>::createNewNode(const T &value) { return new NodeInList<T>(value); } template<typename T> void List<T>::print()const { if(isEmpty()) { cout<<"The list is empty\n\n"; return; } NodeInList<T> *currentPtr = firstPtr; cout<<"The list is: "; while(currentPtr != 0) { cout<<currentPtr->element<<" "; currentPtr = currentPtr->nextPtr; } cout<<"\n\n"; } #endif
///////////////testList////////////////// #include <iostream> using std::cin; using std::cout; using std::endl; #include <string> using std::string; #include "List.h" template<typename T> void testList( List<T> &listObject) { cout<<"Enter one of the following:\n" <<" 1 to insert at beginning of list\n" <<" 2 to insert at end of list\n" <<" 3 to delete from beginning of list\n" <<" 4 to delete from end of list\n" <<" 5 to end list processing\n"; int choice; T value; do { cout<<"?"; cin>>choice; switch(choice) { case 1: cout<<"input value:"; cin>>value; listObject.inSertAtFront(value); listObject.print(); break; case 2: cout<<"input value:"; cin>>value; listObject.insertAtBack(value); listObject.print(); break; case 3: if(listObject.removeFromFront(value)) cout<<value<<" removed from list\n"; listObject.print(); break; case 4: if(listObject.removeFromBack(value)) cout<<value<<" removed from list\n"; listObject.print(); break; } } while ( choice != 5); cout<<"End list test\n\n"; } int main() { List<int> intList; testList(intList ); List<double> doubleList; testList(doubleList); return 0; }
测试结果如下
(4)循环单向链表及双向链表
前面讨论的链表仅为单向链表,特点是只能做单向遍历;如果将上述链表末节点的指针指向首节点,形成单向的一个回路,这样就构成了循环单向链表;如果每个节点不但包含指向下一个节点的指针,还包含指向前一个节点的指针,并构成了一个你像逆向回路,那么则构成了双向链表,双向链表的特点是可向前和向后遍历。如下图所示,左边为单向循环链表,右边为双向循环链表
(1)栈是编译器和操作系统中重要的数据结构,元素的插入和删除只能在栈栈顶进行。栈也可以看成是有限制的链表结构,只是元素的增加和删除只能在链表头进行,也即是后进先出的结构。下面模板类Stack类对模板类List类private继承(即基类所有数据成员和成员函数均为派生类的私有成员),然后使Stack类的成员函数适当调用List类的成员函数,push调用insertAtFront,pop调用removeFromFront,isStackEmpty调用isEmpty,而printStack调用print,这种方式称为委托。
<span style="font-size:12px;">//Stack.h #ifndef STACK_H #define STACK_H #include "List.h" template<typename STACKTYPE> class Stack:private List<STACKTYPE> { public: void push(const STACKTYPE &value) { inSertAtFront(value); } bool pop(STACKTYPE &value) { return removeFromFront(value); } bool isStackEmpty()const { return isEmpty(); } void printStack()const { print(); } }; #endif</span>
///////////////testStack////////////////// #include <iostream> using std::cout; using std::endl; #include "Stack.h" int main() { Stack<int > intStack; //存放整型变量的栈 for(int i=0;i<3 ;i++) { intStack.push(i); intStack.printStack(); } int popIntValue; while( !intStack.isStackEmpty()) { intStack.pop(popIntValue); cout<<popIntValue<<" popped from stack"<<endl; intStack.printStack(); } Stack<double> doubleStack; //存放双浮点型型变量的栈 double value =1.1; for (int j=0;j<3;j++) { doubleStack.push(value); doubleStack.printStack(); value +=1.1; } double popDoubleValue; while(!doubleStack.isStackEmpty()) { doubleStack.pop(popDoubleValue); cout<<popDoubleValue<<" popped from stack"<<endl; doubleStack.printStack(); } return 0; }(2)测试结果
队伍是一种模拟了排队等候的数据结构,插入操作在队伍的后面(对尾)进行,删除操作则在队伍的前面(队列头)进行,也即是先进先出。队列也可以看成是有限制的链表结构,只是元素的增加只在链表头,而删除元素只在链表后。下面通过对List类模板private继承得到Queue类,使Queue类的成员函数适当调用List类的成员函数,enqueue调用insertAtBack,dequeue调用removeFromFront,isQueueEmpty调用isEmpty,而printQueue调用print函数。
//Queue.h #ifndef QUEUE_H #define QUEUE_H #include "List.h" template <typename QUEUETYPE> class Queue:private List<QUEUETYPE> { public: void enqueue(const QUEUETYPE&value) { insertAtBack(value); } bool deququ(QUEUETYPE &value) { return removeFromFront(value); } bool isQueueEmpty()const { return isEmpty(); } void printQueue()const { print(); } }; #endif
//////////////////////testQueue//////////////////// #include <iostream> using std::cout; using std::endl; #include "Queue.h" int main() { Queue<int > intQueue; for (int i=0;i<3;i++) { intQueue.enqueue(i); intQueue.printQueue(); } int dequeueIntValue; while(!intQueue.isQueueEmpty()) { intQueue.deququ(dequeueIntValue); cout<<dequeueIntValue<<" dequeued from Queue"<<endl; intQueue.printQueue(); } Queue<double> doubleQueue; double value = 1.1; for (int j=0;j<3;j++) { doubleQueue.enqueue(value); doubleQueue.printQueue(); value +=1.1; } double deququDoubleValue; while(!doubleQueue.isQueueEmpty()) { doubleQueue.deququ(deququDoubleValue); cout<<deququDoubleValue<<" dequeued form Queue"<<endl; doubleQueue.printQueue(); } return 0; }测试结果
(1)二叉树的结构
链表、栈和队列均为线性结构,而树是非线性的,树的节点可以可以包含两个或更多个指针链接。这里讨论的是二叉树,即所有的节点只包含两个链接。二叉树的结构可如下图所示。
图中有A、B、C、D、E共5个节点,每个节点除了自身的数据变量还有两个指向下一节点的指针,根指针rootPtr指向了根节点,父节点的指针指向了子节点,例如图上B和C的父节点是A,D的父节点是B,E的父节点是C。插入节点是从根节点开始的,向下插入,这与树的生长方向相反。
(2)二叉查找树
这是一种特殊的二叉树,也称为二叉搜索树,二叉查找树没有值相同的节点,因为如果有相同值的节点话,这个节点会无法进行比较任何,进而无法确定是插在左边还是右边。左子树上的值都小于其父节点的值,而它的任何右子树上的值都大于其父节点的值。如下图所示。
(3)二叉查找树程序
实现二叉查找树的代码如下,并且使用了三种方法遍历,前序遍历、中序遍历、后序遍历。
//TreeNode.h #ifndef TREENODE_H #define TREENODE_H template<typename T> class Tree; template<typename T> class TreeNode { friend class Tree<T>; public: TreeNode(const T &d) :leftPtr(0),data(d),rightPtr(0) { } T getData()const { return data; } private: TreeNode<T> *leftPtr; T data; TreeNode<T> *rightPtr; }; #endif
//Tree.h #ifndef TREE_H #define TREE_H #include <iostream> using std::cout; using std::endl; #include "TreeNode.h" template<typename T> class Tree { public: Tree(); void insertNode(const T &); void preOrderTraversal() const; void inOrderTraversal()const; void postOrderTraversal()const; private: TreeNode<T> *rootPtr; void insertNodeHelper(TreeNode<T> **,const T &); void preOrderHelper(TreeNode<T> *)const; void inOrderHelper(TreeNode<T> *)const; void postOrderHelper(TreeNode<T> *)const; }; template < typename T> Tree<T>::Tree() { rootPtr =0; } template < typename T> void Tree<T>::insertNode(const T &value) { insertNodeHelper(&rootPtr ,value); } template < typename T> void Tree<T>::insertNodeHelper(TreeNode<T> **ptr ,const T &value) { if(*ptr == 0) *ptr = new TreeNode<T> (value); else { if(value<(*ptr)->data) insertNodeHelper(&((*ptr)->leftPtr ) ,value); else { if (value >(*ptr)->data ) insertNodeHelper(&((*ptr)->rightPtr ) ,value); else cout<<value<<" dup"<<endl; } } } template < typename T> void Tree<T>::preOrderTraversal()const { preOrderHelper( rootPtr); } template < typename T> void Tree<T>::preOrderHelper(TreeNode<T> *ptr)const { if(ptr != 0) { cout<<ptr->data<<" "; preOrderHelper(ptr->leftPtr); preOrderHelper(ptr->rightPtr); } } template < typename T> void Tree<T>::inOrderTraversal()const { inOrderHelper(rootPtr); } template < typename T> void Tree<T>::inOrderHelper(TreeNode<T> *ptr )const { if(ptr != 0) { inOrderHelper(ptr->leftPtr); cout<<ptr->data<<" "; inOrderHelper(ptr->rightPtr); } } template < typename T> void Tree<T>::postOrderTraversal()const { postOrderHelper(rootPtr); } template < typename T> void Tree<T>::postOrderHelper(TreeNode<T> *ptr )const { if(ptr != 0) { postOrderHelper(ptr->leftPtr); postOrderHelper(ptr->rightPtr); cout<<ptr->data<<" "; } } #endif
///////////////////testTreenode////////////////////////// #include <iostream> using std::cout; using std::cin; using std::fixed; #include <iomanip> using std::setprecision; #include "Tree.h" int main() { Tree<int> intTree; int intValue; cout<<"Enter 10 integer values:\n"; for(int i=0;i <10 ;i++) { cin>>intValue; intTree.insertNode( intValue); } cout<<"前序遍历:\n"; intTree.preOrderTraversal(); cout<<"中序遍历:\n"; intTree.inOrderTraversal(); cout<<"后序遍历:\n"; intTree.postOrderTraversal(); }
测试结果
当依次输入50,25,75,12,33,67,88,6,13,68时,则按照二叉查找树的规则,会得到的结构如下所示:
前序遍历、中序遍历和后序遍历的结果如测试结果所示,详细过程可看preOrderHelper
、inOrderHelper、postOrderHelper函数
(4)二叉查找树的优点
在二叉查找树中查找匹配的关键字是很迅速的,假如树为平衡的,那么每个分支包含树上一半数目的节点,为搜索关键值,每次在一个节点上的比较就能排除一半的节点,称为O(log n)算法,那么有n个元素的二叉查找树最多需要log2 n次的比较就可以找到匹配的值或确定不存在。
本文的学习资料参考自《Cpp大学教程(第五版)》第21章,阅读即可获得更详细的解释。