第十八课 顺序存储线性表的分析

  这一节我们分析前面实现的线性表的功能和性能。

效率分析:

上图中给出了时间复杂度的分析,但是这还不是效率的分析。

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

 如下所示:

  insert中最耗时的操作是那个for循环,假设两个SeqList,一个存的是int型,一个存的是string,它们的操作效率当然是不一样的,特别是当存一个很复杂的类对象时,for循环中的赋值操作会更耗时。

  从上面的例子可以看出,评估一个算法的效率不能只看时间复杂度,时间复杂度只是一个参考指标,但并不是绝对的指标。我们还需要具体的分情况来考虑。

  效率问题需要根据具体情况来看,基于顺序存储结构的线性表,就不适合使用类类型的对象来作为数据元素存储。

考察下面的代码:

s2=s1的赋值操作会变成下面的情形:

导致在释放的时候堆空间释放两次。可能会出现bug。

继续考察下面的代码:

用d1拷贝构造d2,并且在for循环中给d1、d2插入元素,执行这个函数会产生如下的情形:

  执行完d2=d1,它们就指向了同一片内存,后面的插入操作会覆盖前面的操作,而且在析构时会将这片空间释放两次。

如何解决上面说到的这些问题呢?

对于容器类型的类,可以考虑禁用拷贝构造和赋值操作。这是一种简单粗暴的方法。容器类完全可以使用这种方法。

改造List.h如下:

 1 #ifndef LIST_H
 2 #define LIST_H
 3 
 4 #include "Object.h"
 5 
 6 namespace DTLib
 7 {
 8 
 9 template <typename T>
10 class List : public Object
11 {
12 protected:
13     List(const List&);
14     List& operator= (const List&);
15 public:
16     List() {}
17     virtual bool insert(const T& e) = 0;
18     virtual bool insert(int i, const T& e) = 0;
19     virtual bool remove(int i) = 0;
20     virtual bool set(int i, const T& e) = 0;
21     virtual bool get(int i, T& e) const = 0;
22     virtual int length() const = 0;
23     virtual void clear() = 0;
24 };
25 
26 }
27 
28 
29 #endif // LIST_H

13、14行我们将拷贝构造函数和赋值操作符的重载定义为了protected的,也就相当于禁用了拷贝构造和赋值操作。定义了拷贝构造函数后,编译器就不会帮我们生成默认的构造函数了,因此,16行我们自己定义一个。

此外,我们改造SeqList.h,添加一个insert函数的重载版本:

  1 #ifndef SEQLIST_H
  2 #define SEQLIST_H
  3 
  4 #include "List.h"
  5 #include "Exception.h"
  6 
  7 namespace DTLib
  8 {
  9 
 10 template <typename T>
 11 class SeqList : public List<T>
 12 {
 13 protected:
 14     T* m_array;   //顺序存储空间,具体在子类中指定
 15     int m_length; //当前线性表长度
 16 public:
 17     bool insert(int i, const T &e)
 18     {
 19         bool ret = ((0 <= i) && (i <= m_length));
 20 
 21         ret = ret && (m_length < capacity());
 22 
 23         if( ret )
 24         {
 25             for(int p = m_length -1; p >= i; p--)
 26             {
 27                 m_array[p + 1] = m_array[p];
 28             }
 29 
 30             m_array[i] = e;
 31 
 32             m_length++;
 33         }
 34 
 35         return ret;
 36     }
 37 
 38     bool insert(const T& e)
 39     {
 40         return insert(m_length, e);
 41     }
 42 
 43     bool remove(int i)
 44     {
 45         bool ret = ((0 <= i) && (i < m_length));
 46 
 47         if( ret )
 48         {
 49             for(int p = i; p < m_length - 1; p++)
 50             {
 51                 m_array[p] = m_array[p + 1];
 52             }
 53 
 54             m_length--;
 55         }
 56 
 57         return ret;
 58     }
 59 
 60     bool set(int i, const T &e)
 61     {
 62         bool ret = ((0 <= i) && (i < m_length));
 63 
 64         if( ret )
 65         {
 66             m_array[i] = e;
 67         }
 68 
 69         return ret;
 70     }
 71 
 72     bool get(int i, T &e) const
 73     {
 74         bool ret = ((0 <= i) && i < m_length);
 75 
 76         if( ret )
 77         {
 78             e = m_array[i];
 79         }
 80 
 81         return ret;
 82     }
 83 
 84     int length() const
 85     {
 86         return m_length;
 87     }
 88 
 89     void clear()
 90     {
 91         m_length = 0;
 92     }
 93     //顺序存储线性表的数组访问方式
 94     T& operator[](int i)
 95     {
 96         if((0 <= i) && (i < m_length))
 97         {
 98             return m_array[i];
 99         }
100         else
101         {
102             THROW_EXCEPTION(IndexOutOfBoundsException, "Parameter i is invalid ...");
103         }
104     }
105 
106     T operator[](int i) const
107     {
108         return (const_cast<SeqList<T>&>(*this))[i];
109     }
110 
111     //顺序存储空间的容量
112     virtual int capacity() const = 0;
113 };
114 
115 }
116 
117 #endif // SEQLIST_H

38-41行使我们的insert的重载版本。

main函数测试程序如下:

 1 #include <iostream>
 2 #include "List.h"
 3 #include "SeqList.h"
 4 #include "StaticList.h"
 5 #include "Exception.h"
 6 #include "DynamicList.h"
 7 
 8 using namespace std;
 9 using namespace DTLib;
10 
11 
12 int main()
13 {
14 
15     DynamicList<int> l(5);
16     DynamicList<int> n = l;
17 
18     return 0;
19 }

16行相当于拷贝构造,会出现编译错误,如下:

这说明我们禁用拷贝构造函数成功了。

 将线性表当做数组来使用,下面的代码正确吗?

上面的程序直接执行会产生越界异常,这是为什么呢?我们的顺序表SeqList中确实重载了[]操作符,因此,可以以数组的形式访问线性表中的元素,但是前提是表中必须提前插入了元素,也就是表中相应的位置必须已经有元素了。SeqList中我们重载的[]操作符如下:

 1 T& operator[](int i)
 2     {
 3         if((0 <= i) && (i < m_length))
 4         {
 5             return m_array[i];
 6         }
 7         else
 8         {
 9             THROW_EXCEPTION(IndexOutOfBoundsException, "Parameter i is invalid ...");
10         }
11     }

可以看到,当表中没有元素时,m_length是0,这时我们直接以数组下标的形式访问就会产生异常。

结论就是线性表有自己的使用方式,线性表不是数组。

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

要想使用[]操作符,我们需要自己实现一个数组类来代替C++中的原生数组,如下:

小结:

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

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

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

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

原文地址:https://www.cnblogs.com/wanmeishenghuo/p/9502312.html