五、顺序存储线性表分析

1、效率分析

采用大O表示法来进行效率分析

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),最好O(1)
    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;
};

最耗时的两个操作是:insertremove

长度相同的两个SeqList,插入和删除操作的平均耗时是否相同?

SeqList<int> s1 ==> 5
SeqList<string> s2 ==> 5
s1.insert(0, 1);
s2.insert(0, "string");

主要分析最耗时的for循环,对于int来说,就是整型的赋值操作

对于s2来说,就是两个字符串对象之间赋值操作,可能会发生strcpy,意味着再来一个for循环单个字符进行赋值

由于数据类型的不同,赋值操作的效率是不一样的,同样的插入操作,s2.insert的操作更耗时

结论:insert操作,具体耗时取决于线性表内存储的数据元素类型,如果是自定义类类型并且这个类非常强大,这时的插入操作就非常耗时

分析代码效率,不能单纯只看时间复杂度,这只是一个参考指标,不是绝对指标,还要分情况考虑

基于顺序存储结构的线性表不适合使用类类型的对象来作为数据元素存储了

2、顺序存储线性表存在的一些问题

  • 赋值操作
    StaticList<int*, 5> s1;
    StaticList<int*, 5> s2;

    for(int i = 0; i<s1.capacity();i++)
    {
        s1.insert(0, new int(i));
    }

    s2 = s1;    // 赋值之后, s2中的5个元素也会指向这个堆空间

    for(int i = 0; i < s1.length(); i++)
    {
        delete s1[i];
        delete s2[i];
        // 同一个堆空间会被释放两次,情况未定,有问题
    }
  • 拷贝操作
DynamicList<int> d1(5);
    DynamicList<int> d2 = d1; // copy construct
    // 是否合法?
    // d1和d2会指向同一片堆空间
    // d1和d2都会被析构,就是说同一片堆空间被释放两次

    for(int i=0; i<d1.capacity();i++)
    {
        d1.insert(i, i);
        d2.insert(i, i*i);
        // d2会将d1覆盖掉
    }

    for(int i = 0; i < d1.length(); i++)
    {
        cout << d1[i] << endl;
    }

分析:对于容器类型的类,可以考虑禁用拷贝构造和赋值操作

template <typname T>
class List : public Object
{
protected:
    List(const List&);
    List& operator= (const List&);
public:
    List(){}
    // ...
}

生活中常见的容器是水杯,假设有两只一样的水杯,向第一个水杯中倒水,想让第二个水杯也有和第一杯一样多的水,只需要往第二个水杯中倒定量的水,并不会对第一杯水进行复制之后放进第二杯水。即生活中没办法对容器的内容进行复制。

同样的,程序中的容器也不能进行拷贝或赋值。

3、线性表的数组访问形式

    StaticList<int, 5> list;
    for(int i = 0; i<list.capacity();i++)
    {
        list[i] = i * i;    // 异常
    }

// 重载的操作符
T& operator[] (int i)
    {
        if((0 <= i) && (i < m_length))
        {
            return m_array[i];  // 目标位置合法,返回对应的数据元素
        }
        else
        {
            // 不合法,抛异常
            // 意味着越界
            THROW_EXCEPTION(IndexOutOfBoundsException, "Parameter i is invalid...");
        }
    }

顺序存储结构的线性表可以通过数组操作符,方便快捷地访问对应位置处的数据元素,前提是线性表中得先有元素才能这样访问。

线性表必须先插入元素,才能使用数组访问操作符访问元素,不能直接将线性表当作数组来使用,线性表不是数组。

顺序存储结构线性表提供了数组操作符重载,通过重载能够快捷方便地获取目标位置处地数据元素,在具体的使用形式上类似数组,但是由于本质不同,不能代替数组使用。

需求:数组类的开发

提供安全可靠的原生数组类,

原生数组的劣势:无法提供数组的长度信息,无法在使用的时候自动检测当前操作是否合法,当前数据访问是否越界

4、小结

顺序存储线性表的插入和删除操作存在重大效率隐患

线性表作为容器类,应该避免拷贝构造和拷贝赋值

顺序存储线性表可能被当成数组误用

工程开发中可以考虑使用数组类代替原生数组使用

猜你喜欢

转载自www.cnblogs.com/chenke1731/p/9486167.html