经典数据结构沉思录(二):数组和链表
特别声明:此系列博文代码编译环境需为DEV-C++、G++或VS等支持C++99标准的,VC6.0不支持。
经典数据结构涵盖了多种抽象数据类型(ADT),其中包括栈、队列、有序列表、排序表、哈希表及分散表、树、优先队列、集合和图等。对于每种情况,都可以选用数组或某一种链表数据结构来实现其抽象数据类型(ADT)。由于数组和链表几乎是建立所有ADT的基础,所以称数组与链表为基本数据结构。
一、数组
也许聚集数据最常用的方法是利用数组,虽然C++提供了对数组的内嵌式支持,但是这种支持并非没有缺点。在C++中,数组并不是一级数据类型,也没有有关数组求值的表达式。结果,既不能把数组用途一个函数的实际值参数,也不能从一个函数返回一个数组值;还不能把一个数组赋给另一个数组(当然,利用指向数组的指针可以做到)。另外,数组下标是从0~N-1的范围(N为数组元素个数),对数组下标表达式也不提供越界检查。最后,在编译期间一个数组的规模大小是静态和固定的,除非程序员显式使用动态内存分配。
数组这些特性有一部分源于这样一个事实,在C++中,就指向某一T类型的指针T *ptr而言,仅仅从指针本身来说,也不可能说清它是指向一个T类型变量的单一实例还是指向一个T类型的数组。而且,即使知道指针指向一个数组,也无法确定该数组中元素的实际数目。
我们将用面向对象的设计模式来思考数据结构及相关和算法,在本系列后续的博文中也将基于面向对象来设计和实现。统一的设计思路将使得后续博文中出现的程序能够建立于前面博文的程序之上。
我们第一个基本的数组结构Array的设计包含了两个域:data、和length,其中data是一个指向数组数据的指针,length表示数组长度。考虑到Array存储的数据类型的不确定性,我们借助于C++的泛型特性进行设计,则Array的类定义如下:
template <class T> class Array { protected: /*指向的数据*/ T* data; /*数组长度*/ unsigned int length; public: /*构造函数及析构函数*/ Array(); Array(unsigned int); ~Array(); /*拷贝构造函数、等号操作符、下标操作符*/ Array(Array const&); Array& operator = (Array const&); T const& operator [] (unsigned int) const; T& operator [] (unsigned int); /*Get方法*/ T const* Data() const; unsigned int Length() const; /*Set方法*/ void SetLength(unsigned int); };
有了最基本的Array之后,我们就需要对其API函数进行逐个实现,并进行测试。在实际的编码过程中,我们设置了一个全局的.H文件Global.h用于定义一些公共的变量,随着编码工作的进行,我们将不断地更新和扩充Global.h文件。目前Global.h的定义如下,它对于我们本节的数据结构讲解已经够用了。
//Global.h #ifndef GLOBAL_H_ #define GLOBAL_H_ #include <iostream> #include <stdexcept> #define DEFAULT_ARRAY_SIZE 10 #define EXIT_INFO "Press Any Key To Continue!" using namespace std; namespace OP { template <class T> void swap(T& x, T& y) { T tmp; tmp = x; x = y; y = tmp; } } #endif
一份完整的Array类C++代码清单如下所示,为了避免与其他命名空间函数或变量重名,而造成不可用的情况,我们自定义了命名空间OP,当然你可以换成其他名字,之所以用OP是因为我比较喜欢看《海贼王》。代码比较清晰,只给出了简单的注释,并没有过多解释,大家都看的懂的~
//Array.h #ifndef ARRAY_H_ #define ARRAY_H_ #include "Global.h" namespace OP { /*数组对象*/ template <class T> class Array { protected: /*指向的数据*/ T* data; /*数组长度*/ unsigned int length; public: /*构造函数及析构函数*/ Array(); Array(unsigned int); ~Array(); /*拷贝构造函数、等号操作符、下标操作符*/ Array(Array const&); Array& operator = (Array const&); T const& operator [] (unsigned int) const; T& operator [] (unsigned int); /*Get方法*/ T const* Data() const; unsigned int Length() const; /*Set方法*/ void SetLength(unsigned int); }; /*默认构造函数*/ template <class T> Array<T>::Array() : data(new T[DEFAULT_ARRAY_SIZE]), length(0) {} /*带参数的构造函数*/ template <class T> Array<T>::Array(unsigned int n) : data(new T[n]), length(n) {} /*析构函数*/ template <class T> Array<T>::~Array() { delete [] data; } /*拷贝构造函数*/ template <class T> Array<T>::Array(Array<T> const& array) : data(new T[array.length]), length(array.length) { for (unsigned int i = 0; i < length; ++i) { data[i] = array.data[i]; } } /*重载=操作符*/ template <class T> Array<T>& Array<T>::operator = (Array const& array) { if (this != &array) { delete [] data; length = array.length; data = new T[length]; for (unsigned int i = 0; i < length; ++i) { data[i] = array.data[i]; } } return *this; } /*重载[]下标操作符,默认为只读类型,数据不可修改*/ template <class T> T const& Array<T>::operator [] (unsigned int position) const { if (position >= length) { throw out_of_range("Invalid Position!"); } return data[position]; } /*重载[]下标操作符,数据可修改*/ template <class T> T& Array<T>::operator [] (unsigned int position) { if (position >= length) { throw out_of_range("Invalid Position!"); } return data[position]; } /*Get方法*/ template <class T> T const* Array<T>::Data() const { return data; } template <class T> unsigned int Array<T>::Length() const { return length; } /*当length<newLength时,[length,newLength)区间的元素赋初值为0*/ template <class T> void Array<T>::SetLength(unsigned int newLength) { T* const newData = new T[newLength]; unsigned int const min = length < newLength ? length : newLength; for (unsigned int i = 0; i < min; ++i) { newData[i] = data[i]; } /*[length,newLength)区间赋初值*/ for (unsigned int i = min; i < newLength; ++i) { newData[i] = 0; } delete [] data; data = newData; length = newLength; } } #endif
完成了Array的类之后,我们对所有函数进行测试,以保证其能够正常工作。我们实现了一个Test的类,代码清单如下。
//ArrayTest.cpp #include "Array.h" #include "string" using namespace OP; /*遍历数组*/ void traverse(const Array<int>& array); int main() { /*构造函数测试*/ Array<int> array1(10); Array<int> array2; /*下标操作符测试*/ for (unsigned int i = 0; i < 10; ++i) { array1[i] = i*i; } traverse(array1); /*=操作符测试*/ array2 = array1; traverse(array2); /*拷贝构造函数测试*/ Array<int> array3(array1); traverse(array3); /*设置数组长度测试*/ array1.SetLength(20); traverse(array1); array1.SetLength(5); traverse(array1); getchar(); return 0; } /*遍历数组*/ void traverse(const Array<int>& array) { for (unsigned int i = 0; i < array.Length(); ++i) { cout << array[i] << " "; } cout << endl; }
二、单链表
单链表在所有的基于指针的数据结构中是最基本的,它由一系列动态分配的存储元素组成,且每个元素都含有一个指向后继元素的指针。虽然单链表的表达简洁明晰,但实现形式却可以是多种多样的:循环链表、头节点为空、头节点不为空等;链表插入的方式也可以不同:前插或后插。
我们的LinkedList设计中包含了两个重要的指针:head和tail,分别指向头节点和尾结点,其中头节点不为空,插入方式提供前插和后插两种。LinkedList的设计依赖于链表节点的数据结构ListElement。LinkedList的代码清单如下:
//LinkedList.h #ifndef LINKEDLIST_H_ #define LINKEDLIST_H_ #include "Global.h" namespace OP { template <class T> class LinkedList; /*链表节点*/ template <class T> class ListElement { private: /*链表节点数据元素*/ T datum; /*链表节点next指针*/ ListElement* next; /*链表构造函数*/ ListElement(T const&, ListElement*); public: /*Get方法*/ T const& Datum() const; ListElement const* Next() const; /*链表对象为友元*/ friend class LinkedList<T>; }; /*链表对象*/ template <class T> class LinkedList { private: /*头节点*/ ListElement<T>* head; /*尾节点*/ ListElement<T>* tail; public: /*构造函数和析构函数*/ LinkedList(); ~LinkedList(); /*拷贝构造函数和赋值函数*/ LinkedList(LinkedList const&); LinkedList& operator=(LinkedList const&); /*Get方法*/ ListElement<T> const* Head() const; ListElement<T> const* Tail() const; /*链表是否为空*/ bool IsEmpty() const; /*链表首元素*/ T const& First() const; /*链表尾元素*/ T const& Last() const; /*链表首前插一个元素*/ void Prepend(T const&); /*链表尾插入一个元素*/ void Append(T const&); /*删除链表中指定元素节点*/ void Extract(T const&); /*清空链表*/ void Purge(); /*后插*/ void InsertAfter(ListElement<T> const*, T const&); /*前插*/ void InsertBefore(ListElement<T> const*, T const&); }; /*构造函数*/ /*构造函数*/ template <class T> LinkedList<T>::LinkedList() : head(0), tail(0) {} template <class T> ListElement<T>::ListElement(T const& _datum, ListElement<T>* _next) : datum(_datum), next(_next) {} /*Get方法*/ template <class T> T const& ListElement<T>::Datum() const { return datum; } template <class T> ListElement<T> const* ListElement<T>::Next() const { return next; } /*销毁链表*/ template <class T> void LinkedList<T>::Purge() { while (head != 0) { ListElement<T>* const tmp = head; head = head->next; delete tmp; } tail = 0; } /*析构函数*/ template <class T> LinkedList<T>::~LinkedList() { Purge(); } /*Get方法*/ template <class T> ListElement<T> const* LinkedList<T>::Head() const { return head; } template <class T> ListElement<T> const* LinkedList<T>::Tail() const { return tail; } /*是否为空*/ template <class T> bool LinkedList<T>::IsEmpty() const { return head == 0; } /*链表首元素*/ template <class T> T const& LinkedList<T>::First() const { if (head == 0) { throw domain_error("List Is Empty!"); } return head->datum; } /*链表尾元素*/ template <class T> T const& LinkedList<T>::Last() const { if (tail == 0) { throw domain_error("List Is Empty!"); } return tail->datum; } /*前插*/ template <class T> void LinkedList<T>::Prepend(T const& item) { ListElement<T>* const tmp = new ListElement<T>(item, head); if (head == 0) { tail = tmp; } head = tmp; } /*后插*/ template <class T> void LinkedList<T>::Append(T const& item) { ListElement<T>* const tmp = new ListElement<T>(item, 0); if (head == 0) { head = tmp; } else { tail->next = tmp; } tail = tmp; } /*拷贝构造函数*/ template <class T> LinkedList<T>::LinkedList(LinkedList<T> const& linkedList) : head(0), tail(0) { ListElement<T> const* ptr; for (ptr = linkedList.head; ptr != 0; ptr = ptr->next) { Append(ptr->datum); } } /*赋值构造函数*/ template <class T> LinkedList<T>& LinkedList<T>::operator = (LinkedList<T> const& linkedList) { if (&linkedList != this) { Purge(); ListElement<T> const* ptr; for (ptr = linkedList.head; ptr != 0; ptr = ptr->next) { Append(ptr->datum); } } return *this; } /*删除该元素节点*/ template <class T> void LinkedList<T>::Extract(T const& item) { ListElement<T>* ptr = head; ListElement<T>* prevPtr = 0; while (ptr != 0 && ptr->datum != item) { prevPtr = ptr; ptr = ptr->next; } if (ptr == 0) { cout << "Item not Found!" << endl; return; } if (ptr == head) { head = ptr->next; } else { prevPtr->next = ptr->next; } if (ptr == tail) { tail = prevPtr; } delete ptr; } /*在节点arg处前插入一个节点*/ template <class T> void LinkedList<T>::InsertAfter(ListElement<T> const* arg, T const& item) { ListElement<T>* ptr = const_cast<ListElement<T>*>(arg); if (ptr == 0) { throw invalid_argument("Invalid Position!"); } ListElement<T>* const tmp = new ListElement<T>(item, ptr->next); ptr->next = tmp; if (tail == ptr) { tail = tmp; } } /*在节点arg处后插入一个节点,这里使用了一个技巧,先实现前插,后将节点数据进行交换*/ template <class T> void LinkedList<T>::InsertBefore(ListElement<T> const* arg, T const& item) { /*先实现前插,后将节点数据进行交换*/ ListElement<T>* ptr = const_cast<ListElement<T>*>(arg); if (ptr == 0) { throw invalid_argument("Invalid Position!"); } ListElement<T>* const tmp = new ListElement<T>(item, ptr->next); ptr->next = tmp; if (tail == ptr) { tail = tmp; } /*交换数据实现前插*/ swap(tmp->datum, ptr->datum); } } #endif
完成了LinkedList的类之后,我们对所有函数进行测试,以保证其能够正常工作。我们实现了一个Test的类,代码清单如下。
//LinkedListTest.cpp #include "Global.h" #include "LinkedList.h" using namespace OP; void traverse(LinkedList<int>& linkedList); int main() { /*构造函数测试*/ LinkedList<int> l1; /*后插函数测试*/ for (int i = 0; i < 10; ++i) { l1.Append(i + 1); } traverse(l1); /*拷贝构造函数测试*/ LinkedList<int> l2(l1); traverse(l2); /*前插函数测试*/ LinkedList<int> l3; for (int i = 0; i < 10; ++i) { l3.Prepend(i + 1); } traverse(l3); /*后插函数测试*/ l1.InsertAfter(l1.Head()->Next(), -1); traverse(l1); /*前插函数测试*/ l1.InsertBefore(l1.Head()->Next()->Next(), -2); traverse(l1); /*删除节点函数测试*/ l1.Extract(1); traverse(l1); l1.Extract(10); traverse(l1); l1.Extract(5); traverse(l1); l1.Extract(1); traverse(l1); getchar(); return 0; } /*遍历链表*/ void traverse(LinkedList<int>& linkedList) { ListElement<int> const* ptr; for (ptr = linkedList.Head(); ptr != 0; ptr = ptr->Next()) { cout << ptr->Datum() << " "; } cout << endl; }
博文的撰写参考了《数据结构与算法-面向对象的C++设计模式》一书,再此向该书作者表示致敬!本系列博文将继续基于面向对象对经典的数据结构进行设计和封装,并用C++实现,请持续关注本博客~