本文将介绍在程序执行期间动态消长的动态数据结构,包括链表(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 class List;//List类存在便于在下面类中声明为友元类
template
class NodeInList
{
friend class List; //声明List类为其友元类
public:
NodeInList(const T &);
T getElement() const;
private:
T element;
NodeInList *nextPtr; //指向下一个节点的指针
};
template
NodeInList::NodeInList(const T &data)
:element(data),nextPtr(0)
{
}
template
T NodeInList::getElement()const
{
return element;
}
#endif
//List.h
#ifndef LIST_H
#define LIST_H
#include
using std::cout;
#include "NodeInList.h" //包含NodeInList类定义的头文件
template
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 *firstPtr; //指向链表头节点的指针
NodeInList *lastPtr; //指向链表末节点的指针
NodeInList *createNewNode(const T &); //开辟节点空间
};
template
List::List()
:firstPtr(0),lastPtr(0)
{
}
template
List::~List()
{
if(!isEmpty())
{
cout<<"Destroying nodes!\n";
NodeInList *currentPtr = firstPtr;
NodeInList *tempPtr;
while( currentPtr != 0) //如果当前指针不为空
{
tempPtr =currentPtr;
cout<element<<'\n'; //输出当前指针的指向节点的数据
currentPtr =currentPtr->nextPtr; //当前指针指向再下一个节点
delete tempPtr; //删除之前的指针
}
}
cout<<"All nodes destroyed\n\n";
}
template
void List::inSertAtFront( const T& value)
{
NodeInList *newPtr = createNewNode( value);
if(isEmpty())
{
firstPtr = lastPtr =newPtr;
}
else
{
newPtr->nextPtr = firstPtr; //新节点的指针指向头节点
firstPtr = newPtr; //firstPtr指针指向新节点
}
}
template
void List::insertAtBack(const T &value )
{
NodeInList *newPtr = createNewNode( value);
if(isEmpty())
{
firstPtr = lastPtr =newPtr;
}
else
{
lastPtr->nextPtr =newPtr; //新节点的指针指向末节点
lastPtr = newPtr; //lastPtr指向新节点
}
}
template
bool List::removeFromFront(T &value)
{
if(isEmpty())
return false;
else
{
NodeInList *tempPtr =firstPtr;
if(firstPtr == lastPtr)
firstPtr = lastPtr =0;
else
firstPtr = firstPtr->nextPtr;
value = tempPtr->element;
delete tempPtr;
return true;
}
}
template
bool List::removeFromBack(T &value)
{
if(isEmpty())
return false;
else
{
NodeInList *tempPtr =lastPtr;
if(firstPtr == lastPtr)
firstPtr = lastPtr =0;
else
{
NodeInList *currentPtr =firstPtr;
while(currentPtr->nextPtr != tempPtr)
currentPtr =currentPtr->nextPtr;
lastPtr = currentPtr;
currentPtr->nextPtr =0 ;
}
value =tempPtr->element;
delete tempPtr;
return true;
}
}
template
bool List::isEmpty()const
{
return firstPtr == 0;
}
template
NodeInList* List::createNewNode(const T &value)
{
return new NodeInList(value);
}
template
void List::print()const
{
if(isEmpty())
{
cout<<"The list is empty\n\n";
return;
}
NodeInList *currentPtr = firstPtr;
cout<<"The list is: ";
while(currentPtr != 0)
{
cout<element<<" ";
currentPtr = currentPtr->nextPtr;
}
cout<<"\n\n";
}
#endif
///testList//
#include
using std::cin;
using std::cout;
using std::endl;
#include
using std::string;
#include "List.h"
template
void testList( List &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< intList;
testList(intList );
List doubleList;
testList(doubleList);
return 0;
}
测试结果如下
(4)循环单向链表及双向链表
前面讨论的链表仅为单向链表,特点是只能做单向遍历;如果将上述链表末节点的指针指向首节点,形成单向的一个回路,这样就构成了循环单向链表;如果每个节点不但包含指向下一个节点的指针,还包含指向前一个节点的指针,并构成了一个你像逆向回路,那么则构成了双向链表,双向链表的特点是可向前和向后遍历。如下图所示,左边为单向循环链表,右边为双向循环链表
(1)栈是编译器和操作系统中重要的数据结构,元素的插入和删除只能在栈栈顶进行。栈也可以看成是有限制的链表结构,只是元素的增加和删除只能在链表头进行,也即是后进先出的结构。下面模板类Stack类对模板类List类private继承(即基类所有数据成员和成员函数均为派生类的私有成员),然后使Stack类的成员函数适当调用List类的成员函数,push调用insertAtFront,pop调用removeFromFront,isStackEmpty调用isEmpty,而printStack调用print,这种方式称为委托。
//Stack.h
#ifndef STACK_H
#define STACK_H
#include "List.h"
template
class Stack:private List
{
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
///testStack//
#include
using std::cout;
using std::endl;
#include "Stack.h"
int main()
{
Stack intStack; //存放整型变量的栈
for(int i=0;i<3 ;i++)
{
intStack.push(i);
intStack.printStack();
}
int popIntValue;
while( !intStack.isStackEmpty())
{
intStack.pop(popIntValue);
cout< 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<
(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
class Queue:private List
{
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
using std::cout;
using std::endl;
#include "Queue.h"
int main()
{
Queue intQueue;
for (int i=0;i<3;i++)
{
intQueue.enqueue(i);
intQueue.printQueue();
}
int dequeueIntValue;
while(!intQueue.isQueueEmpty())
{
intQueue.deququ(dequeueIntValue);
cout< 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<
测试结果
(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 class Tree;
template
class TreeNode
{
friend class Tree;
public:
TreeNode(const T &d)
:leftPtr(0),data(d),rightPtr(0)
{
}
T getData()const
{
return data;
}
private:
TreeNode *leftPtr;
T data;
TreeNode *rightPtr;
};
#endif
//Tree.h
#ifndef TREE_H
#define TREE_H
#include
using std::cout;
using std::endl;
#include "TreeNode.h"
template
class Tree
{
public:
Tree();
void insertNode(const T &);
void preOrderTraversal() const;
void inOrderTraversal()const;
void postOrderTraversal()const;
private:
TreeNode *rootPtr;
void insertNodeHelper(TreeNode **,const T &);
void preOrderHelper(TreeNode *)const;
void inOrderHelper(TreeNode *)const;
void postOrderHelper(TreeNode *)const;
};
template < typename T>
Tree::Tree()
{
rootPtr =0;
}
template < typename T>
void Tree::insertNode(const T &value)
{
insertNodeHelper(&rootPtr ,value);
}
template < typename T>
void Tree::insertNodeHelper(TreeNode **ptr ,const T &value)
{
if(*ptr == 0)
*ptr = new TreeNode (value);
else
{
if(value<(*ptr)->data)
insertNodeHelper(&((*ptr)->leftPtr ) ,value);
else
{
if (value >(*ptr)->data )
insertNodeHelper(&((*ptr)->rightPtr ) ,value);
else
cout<
void Tree::preOrderTraversal()const
{
preOrderHelper( rootPtr);
}
template < typename T>
void Tree::preOrderHelper(TreeNode *ptr)const
{
if(ptr != 0)
{
cout<data<<" ";
preOrderHelper(ptr->leftPtr);
preOrderHelper(ptr->rightPtr);
}
}
template < typename T>
void Tree::inOrderTraversal()const
{
inOrderHelper(rootPtr);
}
template < typename T>
void Tree::inOrderHelper(TreeNode *ptr )const
{
if(ptr != 0)
{
inOrderHelper(ptr->leftPtr);
cout<data<<" ";
inOrderHelper(ptr->rightPtr);
}
}
template < typename T>
void Tree::postOrderTraversal()const
{
postOrderHelper(rootPtr);
}
template < typename T>
void Tree::postOrderHelper(TreeNode *ptr )const
{
if(ptr != 0)
{
postOrderHelper(ptr->leftPtr);
postOrderHelper(ptr->rightPtr);
cout<data<<" ";
}
}
#endif
///testTreenode//
#include
using std::cout;
using std::cin;
using std::fixed;
#include
using std::setprecision;
#include "Tree.h"
int main()
{
Tree 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章,阅读即可获得更详细的解释。