1 线性表(List)
-
线性表(
List
)是具有相同类型的 n(n ≥ 0)个数据元素的有限序列 -
线性表的表现形式
- 0 个或多个数据元素组成的集合
- 数据元素在位置上是有序排列的
- 数据元素的个数是有限的
- 数据元素的类型必须相同
-
线性表的常用操作
- 将元素插入线性表
- 将元素从线性表中删除
- 获取目标位置处元素的值
- 设置目标位置处元素的值
- 获取线性表的长度
- 清空线性表
2 线性表抽象类:List
-
线性表在 C++ 中表现为一个抽象类,用来被继承
-
List
抽象类实现//List.h template <typename T> class List : public Object { public: virtual bool insert(int i,const T& e) = 0; virtual bool remove(int i) = 0; virtual bool set(int i,const T& e) = 0; virtual bool get(int i,T& e) const = 0; virtual int length() const = 0; virtual void clear() = 0; };
3 线性表的顺序存储结构:SeqList
-
线性表的顺序存储结构(
SeqList
),指的是用一段地址连续的存储单元依次存储线性表中的数据元素 -
SeqList
是一个抽象类 -
设计思路:用一维数组实现顺序存储结构
- 存储空间:
T* m_array;
- 当前长度:
int m_length;
template <typename T> class SeqList : public List<T> { protected: T* m_array; int m_length; ////// };
- 存储空间:
-
顺序存储结构的元素获取操作
- 判断目标位置是否合法
- 将目标位置作为数组下标获取元素
bool SeqList<T>::get(int i,T& e) const { bool ret = ((0 <= i) && (i < m_length)); if(ret) { e = m_array[i]; } return ret; }
-
顺序存储结构的元素插入操作
- 判断目标位置是否合法
- 将目标位置之后的与所有元素后移一个位置
- 将新元素插入目标位置
- 线性表长度加 1
bool SeqList<T>::inert(int i,const T& e) { //i可以是最后一个元素的下一个位置:i <= m_length bool ret = ((0 <= i) && (i <= m_length)); ret = ret && ((m_length + 1) <= capacity()); if(ret) { //将要移动的元素从后往前依次往后移动一个位置 for(int p = m_length - 1; p >= i; p--) { m_array[p+1] = m_array[p]; } m_array[i] = e; m_length++; } return ret; }
-
顺序存储结构的元素删除操作
- 判断目标位置是否合法
- 将目标位置后的所有元素前移一个位置
- 线性表长度减 1
bool SeqList<T>::remove(int i) { bool ret = ((0 <= i) && (i < m_length)); if(ret){ //将要移动的元素从前往后依次往前移动一个位置 for(int p = i; p < m_length - 1; p++){ m_array[p] = m_array[p+1]; } m_length--; } return ret; }
4 顺序存储结构(SeqList)的抽象实现
-
顺序存储结构线性表的抽象实现:实现
SeqList
抽象类 -
SeqList
类设计要点- 抽象类模板,存储空间的位置和大小由子类(
StaticList
,DynamicList
)完成,不能生成具体的对象 - 实现顺序存储结构线性表的关键操作(增,删,查等)
- 提供数组操作符,方便快速获取元素
- 抽象类模板,存储空间的位置和大小由子类(
-
SeqList.h
#include "Exception.h" namespace DTLib { template <typename T> class SeqList : public List<T> { protected: T* m_array; //顺序存储空间 int m_length; //当前线性表长度 public: bool insert(int i,const T& e){ bool ret = ((0 <= i) && (i <= m_length)); //这一步的判断很必要 ret = ret && ((m_length + 1) <= capacity()); if(ret) { for(int p = m_length - 1; p >= i; p--) { m_array[p+1] = m_array[p]; } m_array[i] = e; m_length++; } return ret; } bool remove(int i) { bool ret = ((0 <= i) && (i < m_length)); if(ret) { for(int p = i; p < m_length - 1; p++) { m_array[p] = m_array[p+1]; } m_length--; } return ret; } bool set(int i,const T& e) { bool ret = ((0 <= i) && (i < m_length)); if(ret) { m_array[i] = e; } return ret; } bool get(int i,T& e) const { bool ret = ((0 <= i) && (i < m_length)); if(ret) { e = m_array[i]; } return ret; } int length() const { return m_length; } void clear() { m_length = 0; } //顺序存储线性表的数组访问方式 //非const对象 T& operator[] (int i) { if((0 <= i) && (i < m_length)) { return m_array[i]; } //抛出越界异常 else { THROW_EXCEPTION(IndexOutOfBoundsException,"Parameter i is invalid ..."); } } //const对象:const对象只能调用const成员函数 T operator[] (int i) const { return (const_cast<SeqList<T>&>(*this))[i]; } //顺序存储空间的容量 virtual int capacity() const = 0; //在子类中完成 }; }
-
使用
#include "SeqList.h" using namespace std; int main() { //SeqList<int> l; //error: cannot declare variable 'l' to be of abstract type 'DTLib::SeqList<int>' SeqList<int>* l; //可以声明一个指针 return 0; }
5 StaticList 类和 DynamicList 类
5.1 实现 Staticlist 类
-
Staticlist
类设计要点- 类模板
- 使用原生数组作为顺序存储空间
-
使用模板参数决定数组大小
-
StaticList.h
#include "SeqList.h" namespace DTLib { //模板参数T,N:T决定线性表元素类型,N决定线性表长度 template <typename T,int N> class StaticList : public SeqList<T> { protected: T m_space[N]; //顺序存储空间,N为模板参数 public: //构造函数:指定父类成员的具体值 StaticList(){ this->m_array = m_space; this->m_length = 0; } int capacity() const{ return N; } }; }
-
使用
int main() { StaticList<int,5> l; for(int i = 0; i < l.capacity(); i++){ //每次都往线性表的头部插入 l.insert(0,i); } for(int i = 0; i < l.length(); i++){ cout << l[i] << endl; } l[0] *= l[0]; for(int i = 0; i < l.length(); i++){ cout << l[i] << endl; } try { l[5] = 5; } catch(Exception& e) { cout << e.message() << endl; cout << e.location() << endl; } return 0; } //输出结果 4 3 2 1 0 16 3 2 1 0 Parameter i is invalid ... ..DTLibSeqList.h:78
5.2 实现 DynamicList 类
-
DynamicList
类设计要点- 类模板
- 申请连续堆空间作为顺序存储空间
- 动态设置顺序存储空间的大小
- 保证重置顺序存储空间时的异常安全性
-
函数异常安全的概念
- 不泄露任何资源
- 不允许破坏数据
-
函数异常安全的基本保证
- 如果异常被抛出
- 对象内的任何成员仍然能保持有效状态
- 没有数据的破坏及资源泄露
- 如果异常被抛出
-
DynamicList.h
#include "SeqList.h" namespace DTLib { template <typename T> class DynamicList : public SeqList<T> { protected: int m_capcaity; //记录顺序存储空间的大小,通过构造函数的参数来指定 public: //构造函数:动态申请堆内存空间 DynamicList(int capacity) { this->m_array = new T[capacity]; if(this->m_array != NULL) { this->m_length = 0; this->m_capcaity = capacity; } else { THROW_EXCEPTION(NoEnoughMemoryException,"No memory to create DynamicList object ..."); } } int capacity() const { return m_capcaity; } //重新设置顺序存储空间的大小:需要考虑异常安全 void resize(int capacity) { if(capacity != m_capcaity) { //为什么不直接操作成员变量指针m_array:防止下面的复制操作出现问题 T* array = new T[capacity]; // 新申请的堆空间 if(array != NULL) { int length = this->m_length < capacity ? this->m_length : capacity; // 判断需要复制的数据元素个数 //进行复制数据元素操作 for(int i=0;i<length;i++) { array[i] = this->m_array[i]; } T* temp = this->m_array; // 利用temp指向重置前的存储空间 this->m_array = array; this->m_length = length; this->m_capcaity = capacity; delete[] temp; // 此处才释放重置前的存储空间,防止之前释放发生异常抛出 } else { THROW_EXCEPTION(NoEnoughMemoryException,"No memory to resize DynamicList object ..."); } } } //析构函数:归还空间 ~DynamicList() { delete[] this->m_array; } }; }
-
使用
int main() { DynamicList<int> l(5); for(int i = 0; i < l.capacity(); i++){ l.insert(0,i); } for(int i = 0; i < l.length(); i++){ cout << l[i] << endl; } l[0] *= l[0]; for(int i = 0; i < l.length(); i++){ cout << l[i] << endl; } try{ l[5] = 5; } catch(Exception& e){ cout << e.message() << endl; cout << e.location() << endl; l.resize(10); l.insert(5,50); } l[5] = 5; for(int i = 0; i < l.length(); i++){ cout << l[i] << endl; } l.resize(3); for(int i = 0; i < l.length(); i++){ cout << l[i] << endl; } return 0; } //输出
6 顺序存储线性表的分析
6.1 时间分析
-
SeqList.h
效率分析template <typename T> class SeqList: public List<T> { public: bool insert(int i,const T& e); //O(n) bool remove(int i); //O(n) bool set(int i,const T& e); //O(1) bool get(int i,T& e) const; //O(1) int length() const; //O(1) void clear(); //O(1) T& operator[] (int i); //O(1) T operator[] (int i) const; //O(1) virtual int capacity() const = 0; }
-
问题:长度相同的两个
SeqList
,插入和删除操作的平均耗时是否相同?- 不一定相同,需要看存储的对象的类型。
- 如都含有 5 个元素的线性表
SeqList<int> s1;
和SeqList<string> s2
; - 在插入和删除操作中,
for
循环的赋值操作,整型的赋值操作比字符串的赋值操作(strcpy
是一个字符一个字符赋值的)耗时要小得多
6.2 功能分析
-
代码分析1
-
Demo
StaticList<int*,5> s1; StaticList<int*,5> s2; for(int i = 0; i < s1.capacity(); ++i){ s1.insert(0,new int(i)); } //赋值操作 s2 = s1; for(int i = 0; i < s1.length(); ++i){ delete s1[i]; delete s2[i]; }
-
分析:对于赋值操作
s2 = s1;
,会发生情况:使得链表s1
和s2
中的相对应的 5 个元素都指向同一块内存。那么在下一个for
循环中,每一个内存空间都会被释放两次!
-
-
代码分析2
-
Demo
void func() { DynamicList<int> d1(5); //拷贝构造 DynamicList<int> d2 = d1; for(int i=0;i<d1.capacity();++i){ d1.insert(i,i); d2.insert(i,i*i); } for(int i=0;i<d1.length();++i){ cout << d1[i] << endl; } }
-
分析:拷贝构造时发生了什么:构造
d1
时,m_array
会指向内存中一块空间,构造d2
时,d2
的m_array
同样会指向相同的一块内存,那么在随后的insert
操作中,后进行的insert
操作会覆盖之前的insert
操作,同样也会有同一块内存多次释放的问题
-
-
结论:对于容器类型的类,可以考虑禁用拷贝构造和赋值操作
线性表作为容器类,应该避免拷贝构造和拷贝赋值
template<typename T> class List: public Object { protected: List(const List&); List& operator= (const List&); public: List(){} //... }
-
代码优化
-
禁用拷贝构造和赋值操作
//List.h #include "Object.h" namespace DTLib { template <typename T> class List : public Object { protected: //禁用 List(const List&); List& operator= (const List&); public: List() { } //这里在添加了拷贝构造函数后,需要手动添加一个默认构造函数 virtual bool insert(int i,const T& e) = 0; virtual bool remove(int i) = 0; virtual bool set(int i,const T& e) = 0; virtual bool get(int i,T& e) const = 0; virtual int length() const = 0; virtual void clear() = 0; }; }
-
以重载的方式添加一个默认的插入操作:往线性表尾部插入数据
//List.h #ifndef LIST_H #define LIST_H #include "Object.h" namespace DTLib { template <typename T> class List : public Object { protected: List(const List&); List& operator= (const List&); public: List() { } //这里在添加了拷贝构造函数后,需要手动添加一个默认构造函数 virtual bool insert(const T& e) = 0; virtual bool insert(int i,const T& e) = 0; virtual bool remove(int i) = 0; virtual bool set(int i,const T& e) = 0; virtual bool get(int i,T& e) const = 0; virtual int length() const = 0; virtual void clear() = 0; }; } //SeqList.h #ifndef SEQLIST_H #define SEQLIST_H #include "Exception.h" namespace DTLib { template <typename T> class SeqList : public List<T> { protected: T* m_array; //顺序存储空间 int m_length; //当前线性表长度 public: bool insert(int i,const T& e){ bool ret = ((0 <= i) && (i <= m_length)); ret = ret && ((m_length + 1) <= capacity()); if(ret){ for(int p = m_length - 1;p >= i;p--){ m_array[p+1] = m_array[p]; } m_array[i] = e; m_length++; } return ret; } bool insert(const T& e){ return insert(m_length,e); } }; }
-
-
代码分析3
-
Demo
int main() { StaticList<int,5> list; for(int i = 0; i < list.capacity(); ++i){ //将线性表当作数组使用 list[i] = i * i; //报越界异常:Parameter i is invalid ... //if((0 <= i) && (i < m_length))中,i < m_length 是不满足的:m_lenght = 0 } return 0; }
-
-
问题:将线性表当作数组使用
-
分析:线性表必须先插入元素(使用
inert
),才能使用操作符[] 访问元素顺序存储结构线性表提供了数组操作符重载,通过重载能够快捷方便地获取目标位置处的数据元素,在具体的使用形式上类似数组 ,但是由于本质不同,不能代替数组使用
7 数组类 Array 的创建
- 基于顺序存储的线性表的主要问题
- 功能上的问题:链表误当作数组来使用(重载了
[]
操作符)——利用数组类解决 - 效率上的问题:基于顺序存储的线性表插入和删除的效率较低——利用线性表的链式存储解决
- 功能上的问题:链表误当作数组来使用(重载了
7.1 抽象类 Array 设计
-
需求分析
- 创建数组类代替原生数组的使用
- 数组类包含长度信息
- 数组类能够主动发现越界访问,例如上一节的代码分析3,数组发生越界却没有报告异常
-
Array
设计要点- 抽象模板类,存储空间的位置和大小由子类完成
- 重载数组操作符,判断访问下标是否合法
- 提供数组长度的抽象访问函数
- 提供数组对象间的复制操作
-
Array
类的声明template <typename T> class Array: public Object { protected: T* m_array; public: virtual bool set(int i,const T& e); virtual bool get(int i,T& e) const; virtual int length() const = 0; //数组访问操作符 T& operator[] (int i); T operator[] (int i) const; }
-
数组抽象类的实现
//Array.h #include "Object.h" #include "Exception.h" namespace DTLib { template <typename T> class Array : public Object { protected: T* m_array; public: virtual bool set(int i,const T& e){ //O(1) bool ret = ((0 <= i) && (i < length())); if(ret){ m_array[i] = e; } return ret; } virtual bool get(int i,T& e) const{ //O(1) bool ret = ((0 <= i) && (i < length())); if(ret){ e = m_array[i]; } return ret; } //数组访问操作符 T& operator[] (int i){ //O(1) if((0 <= i) && (i < length())){ return m_array[i]; } else{ THROW_EXCEPTION(IndexOutOfBoundsException,"Parameter i is invalid ..."); } } T operator[] (int i) const{ //O(1) return (const_cast<Array<T>&>(*this)[i]); } virtual int length() const = 0; }; }
7.2 子类 StaticArray 类设计
-
StaticArray
类设计要点- 类模板
- 封装原生数组
- 使用模板参数决定数组大小
- 实现函数返回数组长度
- 拷贝构造和赋值操作
-
StaticArray
类的声明template <typename T,int N> class StaticArray : public Array<T> { protected: T m_space[N]; public: StaticArray(); //拷贝和赋值操作 StaticArray(const StaticArray<T,N>& obj); StaticArray<T,N>& operator= (const StaticArray<T,N>& obj); int length() const; };
-
静态数组类的实现
//StaticArray.h #include "Array.h" namespace DTLib { template <typename T,int N> class StaticArray : public Array<T> { protected: T m_space[N]; public: StaticArray(){ //O(1) this->m_array = m_space; } //拷贝和赋值操作 StaticArray(const StaticArray<T,N>& obj){ //O(n) this->m_array = m_space; for(int i=0;i<N;++i){ m_space[i] = obj.m_space[i]; } } StaticArray<T,N>& operator= (const StaticArray<T,N>& obj){ //O(n) //判断是否自赋值 if(this != &obj){ for(int i=0;i<N;++i){ m_space[i] = obj.m_space[i]; } } return *this; } int length() const{ //O(1) return N; } }; }
-
使用
#include "StaticArray.h" using namespace std; using namespace DTLib; int main() { StaticArray<int,5> s1; for(int i=0;i<s1.length();++i){ s1[i] = i * i; } for(int i=0;i<s1.length();++i){ cout << s1[i] << endl; } StaticArray<int,5> s2; //支持数组的相互赋值 s2 = s1; for(int i=0;i<s2.length();++i){ cout << s2[i] << endl; } s1[6] = 100; //Array类会抛出异常 : 'DTLib::IndexOutOfBoundsException' int s3[5]; //原生数组 s3[6] = 100; //越界,但不报异常 return 0; }
7.3 子类 DynamicArray 类设计
-
DynamicArray
类设计要点- 类模板
- 动态确定内部数组空间的大小
- 实现函数返回数组长度
- 拷贝构造和赋值操作
-
DynamicArray
类的声明template <typename T> class DynamicArray : public Array<T> { protected: int m_length; public: DynamicArray(int length); DynamicArray(const DynamicArray<T>& obj); DynamicArray<T>& operator= (const DynamicArray<T>& obj); int length() const; void resize(int length); //动态重置数组的长度 ~DynamicArray(); };
-
动态数组类的实现
//DynamicArray.h #include "Array.h" #include "Exception.h" namespace DTLib { template <typename T> class DynamicArray : public Array<T> { protected: int m_length; public: DynamicArray(int length){ this->m_array = new T[length]; if(this->m_array != NULL){ this->m_length = length; } else{ THROW_EXCEPTION(NoEnoughMemoryException,"No memory to create DynamicArray object ..."); } } DynamicArray(const DynamicArray<T>& obj){ this->m_array = new T[obj.m_length]; if(this->m_array != NULL){ this->m_length = obj.length; for(int i=0;i<obj.m_length;++i){ this->m_array[i] = obj.m_array[i]; } } else{ THROW_EXCEPTION(NoEnoughMemoryException,"No memory to create DynamicArray object ..."); } } DynamicArray<T>& operator= (const DynamicArray<T>& obj){ if(this != &obj){ T* array = new T[obj.m_length]; if(array != NULL){ for(int i=0;i<obj.m_length;++i){ array[i] = obj.m_array[i]; } T* temp = this->m_array; this->m_array = array; this->m_length = obj.m_length; delete[] temp; } else{ THROW_EXCEPTION(NoEnoughMemoryException,"No memory to copy object ..."); } } return *this; } int length() const{ return m_length; } //动态重置数组的长度 void resize(int length){ if(length != m_length){ T* array = new T[length]; if(array != NULL){ int size = (length < m_length) ? length : m_length; for(int i=0;i<size;++i){ array[i] = this->m_array[i]; } T* temp = this->m_array; this->m_array = array; this->m_length = length; delete[] temp; } else { THROW_EXCEPTION(NoEnoughMemoryException,"No memory to resize object ..."); } } } ~DynamicArray(){ delete[] this->m_array; } }; }
-
使用
#include "DynamicArray.h" using namespace std; using namespace DTLib; int main() { DynamicArray<int> s1(5); for(int i=0;i<s1.length();++i){ s1[i] = i * i; } for(int i=0;i<s1.length();++i){ cout << s1[i] << endl; } DynamicArray<int> s2(10); s2 = s1; s2.resize(8); for(int i=0;i<s2.length();++i){ cout << s2[i] << endl; } s2.resize(3); for(int i=0;i<s2.length();++i){ cout << s2[i] << endl; } s2[4] = 100; return 0; }
-
DynamicArray
类中的函数实现存在重复的逻辑,进行代码优化init
:对象构造时的初始化操作copy
:在堆空间中申请新的内存,并执行拷贝操作update
:将指定的堆空间作为内部存储数组使用
//DynamicArray.h #include "Array.h" #include "Exception.h" namespace DTLib { template <typename T> class DynamicArray : public Array<T> { protected: int m_length; //在堆空间中申请一个新的数组ret,大小为newLen,再将传入的array数组中的元素拷贝到新的数组中 T* copy(T* array,int len,int newLen){ T* ret = new T[newLen]; if(ret != NULL){ //数据元素的拷贝 int size = (len < newLen) ? len : newLen; for(int i=0;i<size;++i){ ret[i] = array[i]; } } return ret; } void update(T* array,int length){ if(array != NULL){ //异常安全 T* temp = this->m_array; this->m_array = array; this->m_length = length; delete[] temp; } else{ THROW_EXCEPTION(NoEnoughMemoryException,"No memory to update DynamicArray object ..."); } } void init(T* array,int length){ if(array != NULL){ this->m_array = array; this->m_length = length; } else{ THROW_EXCEPTION(NoEnoughMemoryException,"No memory to create DynamicArray object ..."); } } public: DynamicArray(int length){ init(new T[length],length); } DynamicArray(const DynamicArray<T>& obj){ T* array = copy(obj.m_array,obj.m_length,obj.m_length); init(array,obj.m_length); } DynamicArray<T>& operator= (const DynamicArray<T>& obj){ if(this != &obj){ T* array = copy(obj.m_array,obj.m_length,obj.m_length); update(array,obj.m_length); } return *this; } int length() const{ return m_length; } //动态重置数组的长度 void resize(int length){ if(length != m_length){ T* array = copy(this->m_array,m_length,length); update(array,length); } } ~DynamicArray(){ delete[] this->m_array; } }; }