C++学习笔记(四)指针实现的链表、堆栈、队列、二叉查找树

一、概述

本文将介绍在程序执行期间动态消长的动态数据结构,包括链表(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),表示链表的结束。结构可由下图所示。

C++学习笔记(四)指针实现的链表、堆栈、队列、二叉查找树_第1张图片

(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;
}

测试结果如下

C++学习笔记(四)指针实现的链表、堆栈、队列、二叉查找树_第2张图片

C++学习笔记(四)指针实现的链表、堆栈、队列、二叉查找树_第3张图片

(4)循环单向链表及双向链表

前面讨论的链表仅为单向链表,特点是只能做单向遍历;如果将上述链表末节点的指针指向首节点,形成单向的一个回路,这样就构成了循环单向链表;如果每个节点不但包含指向下一个节点的指针,还包含指向前一个节点的指针,并构成了一个你像逆向回路,那么则构成了双向链表,双向链表的特点是可向前和向后遍历。如下图所示,左边为单向循环链表,右边为双向循环链表

C++学习笔记(四)指针实现的链表、堆栈、队列、二叉查找树_第4张图片C++学习笔记(四)指针实现的链表、堆栈、队列、二叉查找树_第5张图片

三、栈

(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)测试结果
C++学习笔记(四)指针实现的链表、堆栈、队列、二叉查找树_第6张图片

四、队列

队伍是一种模拟了排队等候的数据结构,插入操作在队伍的后面(对尾)进行,删除操作则在队伍的前面(队列头)进行,也即是先进先出。队列也可以看成是有限制的链表结构,只是元素的增加只在链表头,而删除元素只在链表后。下面通过对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<
测试结果
C++学习笔记(四)指针实现的链表、堆栈、队列、二叉查找树_第7张图片

五、二叉树

(1)二叉树的结构

链表、栈和队列均为线性结构,而树是非线性的,树的节点可以可以包含两个或更多个指针链接。这里讨论的是二叉树,即所有的节点只包含两个链接。二叉树的结构可如下图所示。

C++学习笔记(四)指针实现的链表、堆栈、队列、二叉查找树_第8张图片

图中有A、B、C、D、E共5个节点,每个节点除了自身的数据变量还有两个指向下一节点的指针,根指针rootPtr指向了根节点,父节点的指针指向了子节点,例如图上B和C的父节点是A,D的父节点是B,E的父节点是C。插入节点是从根节点开始的,向下插入,这与树的生长方向相反。

(2)二叉查找树

这是一种特殊的二叉树,也称为二叉搜索树,二叉查找树没有值相同的节点,因为如果有相同值的节点话,这个节点会无法进行比较任何,进而无法确定是插在左边还是右边。左子树上的值都小于其父节点的值,而它的任何右子树上的值都大于其父节点的值。如下图所示。

C++学习笔记(四)指针实现的链表、堆栈、队列、二叉查找树_第9张图片

(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();
}

测试结果

C++学习笔记(四)指针实现的链表、堆栈、队列、二叉查找树_第10张图片

当依次输入50,25,75,12,33,67,88,6,13,68时,则按照二叉查找树的规则,会得到的结构如下所示:

C++学习笔记(四)指针实现的链表、堆栈、队列、二叉查找树_第11张图片

前序遍历、中序遍历和后序遍历的结果如测试结果所示,详细过程可看preOrderHelper

、inOrderHelper、postOrderHelper函数

(4)二叉查找树的优点

在二叉查找树中查找匹配的关键字是很迅速的,假如树为平衡的,那么每个分支包含树上一半数目的节点,为搜索关键值,每次在一个节点上的比较就能排除一半的节点,称为O(log n)算法,那么有n个元素的二叉查找树最多需要log2 n次的比较就可以找到匹配的值或确定不存在。


本文的学习资料参考自《Cpp大学教程(第五版)》第21章,阅读即可获得更详细的解释。



你可能感兴趣的:(C/C++)