---- 整理自狄泰软件唐佐林老师课程
文章目录
1. 线性表的本质和操作
1.1 线性表 List 的表现形式
- 线性表是具有相同类型的 n (>=0) 个数据元素的有限序列
(a0, a1, … , an-1)
ai 是表项(数据元素),n 是表长度
1.2 线性表的性质
- a0 为线性表的第一个元素,只有一个后继
- an-1 为线性表的最后一个元素,只有一个前驱
- 除 a0 和 an-1 外的其它元素 ai ,既有前驱又有后继
- 直接支持逐项访问和顺序存取
1.3 线性表的程序实现
#ifndef LIST_H
#define LIST_H
#include "Object.h"
namespace DTLib
{
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) = 0;
virtual int length() const = 0;
virtual void clear() = 0;
};
}
#endif // LIST_H
- List 个抽象类是用来被继承的
2. 线性表的顺序存储结构
- 线性表的顺序存储结构,指的是用一段地址连续的存储单元依次存储线性表中的数据元素。可以用一维数组来实现顺序存储结构。
3. 顺序存储结构的抽象实现
- SeqList 是一个抽象类,仅实现关键的操作,还不能生成具体的对象,因为顺序存储空间的指定不在 SeqList 中完成,需要在 StaticList 和 DynamicList 两个子类中完成。
- SeqList 的设计要点:
- 抽象类模板,存储空间的位置和大小由子类 StaticList 和 DynamicList 完成
- 实现顺序存储结构线性表的关键操作(增删查等)
- 提供数组操作符,方便获取元素
- SeqList 抽象类模板的声明如下:
#ifndef SEQLIST_H
#define SEQLIST_H
#include "List.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 remove(int i);
bool set(int i, const T &e);
bool get(int i, T &e) const;
int length() const;
void clear();
//顺序存储线性表的数组访问方式
T& operator[](int i);
T operator[](int i) const;
//顺序存储空间的容量
virtual int capacity() const = 0;
};
}
#endif // SEQLIST_H
4. StaticList 和 DynamicList
- 完成 StaticList 类的具体实现
- 完成 DynamicList 类的具体实现
- StaticList 是静态的,在 StaticList 的具体实现当中就是将某个固定的存储空间作为顺序存储空间,指定到父类 SeqList 的 m_array 上去。
- DynamicList 是动态的,可以动态得指定顺序存储空间,并将这个存储空间指定到父类 SeqList 的 m_array 上去。
4.1 StaticList
- StaticList 设计要点
- 类模板:
- 使用原生数组作为顺序存储空间
- 使用模板参数决定数组大小
- 在构造函数中将这个顺序存储空间挂接到父类 SeqList 的 m_array 上去
- 类模板:
template <typename T, int N>
class StaticList: public SeqList<T>
{
protected:
T m_space[N]; //顺序存储空间,N为模板参数
public:
StaticList(); //指定父类成员的具体值
int capacity() const;
};
#ifndef STATICLIST_H
#define STATICLIST_H
#include "SeqList.h"
namespace DTLib
{
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;
}
};
}
#endif // STATICLIST_H
- StaticList 的实现
4.2 DynamicList
- DynamicList 的设计
- 申请连续内存作为顺序存储空间。存储空间是new出来的,不同于StaticList使用原生数组
- 动态设置顺序存储空间的大小
- 保证重置顺序存储空间时的异常安全性
- 函数异常安全的概念
- 不泄漏任何资源
- 不允许破坏数据
- 函数异常安全的基本保证
如果异常被抛出,对象内的任何成员仍然能保持有效状态,没有数据破坏及资源泄漏 - DynamicList 中没有原生数组,只有一个 m_capacity 代表存储空间的大小。这个大小就不是通过模板参数来指定了,而是通过构造函数的参数来指定,在构造函数中申请堆空间,在析构函数中释放堆空间。
template <typename T>
class DynamicList : public SeqList<T>
{
protected:
int m_capicity;
public:
DynamicList(int capacity);
int capacity() const;
void resize(int capacity);
~DynamicList();
};
5. 顺序存储线性表的分析
5.1 效率分析
- 时间复杂度
template <typename T>
class SeqList : public List<T>
{
protected:
T* m_array;
int m_length;
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,插入和删除的平均耗时是否相同?
假设两个 SeqList,一个存的是 int 类型,一个是 string,他们的操作效率肯定不一样。特别是当存一个很复杂的类对象时,for 循环中的赋值操作会更耗时。
5.2 问题分析
5.2.1 问题一
s2 = s1 的赋值操作会变成如下,导致堆空间被释放两次。
5.2.2 问题二
- 用 d1 拷贝构造 d2,在循环中给 d1 和 d2 插入元素,会出现问题:执行完 d2 = d1,它们就指向了同一片内存,后面的插入操作会覆盖前面的操作,而且在析构时会将这片空间释放两次。
5.2.3 解决问题
- 考虑禁用拷贝构造和赋值操作
将拷贝构造函数和赋值操作符的重载定义为了 protected 的,也就相当于禁用了拷贝构造和赋值操作。定义了拷贝构造函数后,编译器就不会帮我们生成默认的构造函数了,因此,需要我们自己定义一个。
6. 思考
- 将线性表当做数组来使用,下面的代码正确吗?
上面的程序直接执行会产生越界异常,这是为什么呢?我们的顺序表 SeqList 中确实重载了 [ ] 操作符,因此,可以以数组的形式访问线性表中的元素,但是前提是表中必须提前插入了元素,也就是表中相应的位置必须已经有元素。
- 要想使用 [ ] 操作符,我们需要自己实现一个数组类来代替 C++ 中的原生数组