线性表
线性表的基础
线性表的之间定义我就不再多说了,直接来看一下它的基本运算:
(1)创建一个空的线性表(create)
(2)删除数据表中的所有线性结构(clear) (3)求线性表的长度(length)
(4)在第i
个位置插图一个元素(insert)
(5)删除第i
个位置的元素(remove)
(6)搜索元素,检查某个元素x
在线性表中是否出现,并返回x
的位置(search)
(7)返回线性表中第i
个数据元素的值(visit)
(8)按序访问线性表的每一个数据元素(traverse)
我们可以直接产生面向对象的程序设计
1 2 3 4 5 6 7 8 9 10 11 12 template <class T >class linearlist { public : virtual void create () = 0 ; virtual int length () const = 0 ; virtual void insert (int i) = 0 ; virtual void remove (int i) = 0 ; virtual int search (const T &x) const = 0 ; virtual T visit (int i) b const = 0 ; virtual void traverse () const = 0 ; virtual ~list (){}; };
在上面的代码里除了析构函数以外,其他基本上都是纯虚函数,这样相当于勒令衍生类解决这些问题,这样一个基本的线性表的就构建好了,我们为什么在上面只是设置成员函数,但是没有去设置数据成员?因为线性表有多种表示方式,既可以是顺序表,也可以是链表,链表又可以分为单双链表,顺序表也有多种表达方式,不同的表示方式我们要设置的私有数据成员也不同,我们只能够放到衍生类里面去实现。
顺序表类
顺序列表基础申明
顺序实现是我们存放数据最自然的一种方式,基本上所有事务我要把它归类,首先都是这么干的,它的基础逻辑就是数组,不过在C++中,数组确实不太好用(主要是在申明的时候就要给定最大容量,这对于经常要使用变量的我们来说还是十分复杂的),但是通过指针,我们就可以很清爽地规避掉这个问题:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 template <class T >class seqlist : public linearlist<T>{ private : T *data; int curlength; int maxsize; void doublespace () ; public : seqlist (int startsize); ~seqlist (); void clear () ; int length () const ; void insert (int i, const T &x) ; void remove (int i) ; int search (const T &x) const ; T visit (int i) const ; void traverse () const ; };
基本上还是跟上面一样的说明,但是这次就不能再打太极了,必须解决,不过一个习惯就是不要放在类里面解决(除非真的很短)
析构函数,clear,length,visit,traverse函数的实现
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 template <class T > seqlist<T>::~seqlist (){ delete [] data; }template <class T >void seqlist<T>::clear (){ curlength = 0 ; maxsize = 0 ; }template <class T >int seqlist<T>::length ()const { return curlength; }template <class T > T seqlist<T>::visit (int i)const { return data[i]; }template <class T >void seqlist<T>::traverse ()const { for (int i = 0 ; i <= curlength; i++) cout << data[i] << " " ; cout << endl; }
上面是最简单的几个功能的实现,基本上所有学习过C++的读者都可以轻松自己编写,除了traverse
函数的时间复杂度是O (n ) ,其他的时间复杂度都是O (1) ,接下来几个比较能够体现顺序类列表的特点,我分开来列:
构造函数的实现
1 2 3 4 5 6 7 template <class T > seqlist<T>::seqlist (int startsize){ data = new T [startsize]; maxsize = startsize; currentlength = 0 ; }
这个其实也非常简单(或许我应该把它列在上面的,算了,懒得动)
search的实现
1 2 3 4 5 6 7 template <class T >int seqlist<T>::search (const T &x)const { int i; for (i = 0 ; i < curlength && data[i] != x; i++); return ((i == curlength)?-1 :i); }
其实讲真的,基本上所有人都能看得出来这块挺麻烦的,但是搜索就是这样,
除非储存下标,不然就是需要遍历,时间复杂度就是O (n ) 级别的。
doublespace和search函数的实现
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 template <class T >void seqlist<T>::doublespace (){ T *tmp = data; maxsize *= 2 ; data = new T [maxsize]; for (int i = 0 ; i < curlength; i++) data[i] = tmp[i]; delete [] tmp; }template <class T >void seqlist<T>::insert (int i,const T &x){ if (curlength == maxsize) doublespace (); for (int j = i; j < curlength; j++) data[j + 1 ] = data[j]; data[i] = x; curlength++; }
基本上十分明显了,很简单就是我要插入在第i
的位置,那么我把自从第i
往后的每一位都向后挪一个位置,再把第i
位变成我要的x
这样子就完成了,这里就是顺序表和链表最大的不同,不过两者基本上都是O (n ) 的时间复杂度,因为顺序表知道下标但是挪动需要花一定的时间,而链表是挪动不用花时间但是不知道下表(但是实际上可以解决)
remove函数的实现
1 2 3 4 5 6 7 template <class T >void seqlist<T>::remove (int i){ for (int j = i; j < curlength; j++) data[j] = data[j + 1 ]; curlength--; }
顺序表就这么多内容了,基本上没有什么例外,确实比较简单,链表在move
函数那边比较具有迷惑性,需要好好解释一下的我们接下来探讨
单链表
单链表类的定义
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 template <class T >class slinklist : public linearlist<T>{ private : struct node { T data; node *next; node (const T &x, node *n = NULL ){data = x; next = n;} node ():next (NULL ){} ~node (){} }; node *head; int curlength; node *move (int i) const ; public : slinklist (); void clear () ; ~slinklist (){clear ();delete head;} int length () const {return curlength}; void insert (int i,const T &x) ; void remove (int i) ; int search (const T &x) T visit (int i) const ; void traverse () const ; };
这里里面的定义基本上都跟上面一样,不过这里需要注意的就是node
作为结构体,也具有构造函数和析构函数,形式跟类差不多
move函数的实现
1 2 3 4 5 6 7 template <class T >typename slinklist<T>::node *slinklist<T>::move (int i) const { node *p = head; while (i-- >= 0 ) p = p -> next; return p; }
这里面为什么是i + 1
次循环?而不是i
我们的头节点并不是我们的第0个节点,我们要从头节点移动到第0个节点需要移动1次,第一个节点需要2次……以此类推,你要到第i
个节点,就需要i + 1
次,那既然如此,我们为什么不直接去除掉head
节点,来考虑剩下来的问题?这个问题我们下文会考虑
构造函数的实现
1 2 3 4 5 6 template <class T > slinklist<T>::slinklist (){ head = new node; curlength = 0 ; }
非常简单,没有什么好讲的(所以你干嘛写它呢?)
clear函数的实现
1 2 3 4 5 6 7 8 9 10 11 12 template <class T >void slinklist <T> ::clear (){ node *p = head->next ,*q; head->next = NULL ; while (p != NULL ){ q = p->next; delete p; p = q; } curlength = 0 ; }
也是非常的简单,不多说了
insert和remove函数的实现
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 template <class T >void slinklist<T>::insert (int i, const T &x){ node *pos; pos = move (i - 1 ); pos->next = new node (x, pos->next); curlength++; }template <class T >void slinklist<T>::remove (int i){ node *pos, *delp; pos = move (i - 1 ); delp = pos->next; pos->next = delp->next; delete delp; curlength--; }
终于来了点有意思的东西了,第一个问题:为什么move
函数的参数是i - 1
?而不是i
呢?我们要去除第i
个节点,自然就是要将i - 1
和i + 1
链接起来,所以实际上,我们将pos
移到i - 1
的位置是便于操作的,因为我们这边是单链表,你找你的下家容易,但是你去找上家比较的困难
另外再说,为什么我们需要将node
后面写上x, pos->next
的参数,可以参考我在定义里面写的node(const T &x, node *n = NULL){data = x; next = n;}
我们将这里面的x
变为一个新的节点的data
然后将原来pos->next
变作这个新节点的next
,最后将post->next
改为这个新节点的地址,如果我们不要求代码的美观度这么高,我们可以写得明显一些:
1 2 3 4 node *tmp = new node; tmp->data = x; tmp->next = pos->next; pos->next = tmp;
上面这四行代码可以达到同样的效果
在remove
函数的实现中,注意delp
函数的使用,因为delp
函数其实是记下来给用来释放空间的,不然会产生内存泄漏。
search,visit,traverse函数的实现
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 template <class T >int slinklist<T>::search (const T &x)const { node *p = head->next; int i = 0 ; while (p != NULL && p->data != x){ p = p->next; i++; } if (p == NULL ) return -1 ; else return i; }template <class T > T slinklist<T>::visit (int i)const { return move (i)->data; }template <class T >void slinklist<T>::traverse ()const { node *p = head->next; while (p != NULL ){ cout << p->data << " " ; p = p->next; } cout << endl; }
这三个函数还是十分简单的,基本上就是遍历了,没有什么太过困难的。
无头结点的函数实现
但是
你觉得,这就结束了吗?
不!
我们还要尝试无头结点的情况下面的单链表,判断为什么拥有头节点是一件更好的事情,我们来尝试一下无头结点情况下的insert
和remove
函数
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 template <class T >void slinklist<T>::insert (int i, const T &x){ if (i == 0 ){ p = head; head = new node (x,p); } else if (i > 0 ){ p = move (i - 2 ); p->next = new node (x,p->next); } curlength++; }template <class T >void slinklist<T>::remove (int i){ node *pos, *delp; if (i == 0 ){ delp = head; head = delp->next; } else if (i > 0 ){ p = move (i - 2 ); delp = p->next; p->next = delp->next; } delete delp; curlength--; }
明显可以看出来复杂得多了,因为我们需要对于有i == 0
的情况进行专门的判断,我建议有精力的读者可以自己用笔来画画图,来理解一下为什么这里面i == 0
需要专门判断,笔者本来也想写一写然后贴图片上去的,但是作业还没有写完(悲),就不能继续写了
总结