經典數據結構沉思錄(二):數組和鏈表
特別聲明:此係列博文代碼編譯環境需爲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++實現,請持續關注本博客~