14-18 线性表的本质和操作 线性表的顺序存储结构 顺序存储结构的抽象实现 StaticList 和 DynamicList 顺序存储线性表的分析

本文详细探讨了线性表的本质、顺序存储结构的实现(StaticList和DynamicList),以及它们在插入、删除等操作中的效率分析。文中指出并解决了赋值操作和拷贝构造可能导致的问题,强调了正确使用线性表数组访问的条件。
摘要由CSDN通过智能技术生成

---- 整理自狄泰软件唐佐林老师课程

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 个抽象类是用来被继承的

【14 - 线性表的本质和操作】

2. 线性表的顺序存储结构

  • 线性表的顺序存储结构,指的是用一段地址连续的存储单元依次存储线性表中的数据元素。可以用一维数组来实现顺序存储结构。

【15 - 线性表的顺序存储结构】

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

【16 - 顺序存储结构的抽象实现】

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 的实现

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

【17 - 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++ 中的原生数组

在这里插入图片描述

  • 3
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

uuxiang

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值