目录
一、什么是线性表
- 线性表:由同种数据类型的数据元素组成的有序序列。
说人话:将有一堆同样类型的数据按顺序放在一起的数据结构。
举例子:现在我有英语书(序号0)、语文书(序号1)和数学书(序号2);现在我想把他们按语文书、数学书、英语书的顺序放成一叠,那么用序号表示就是1、2、0。在程序中可以用一个数组来表示:book[3]={1,2,0}.这里,这个数组book就是一个线性表。
- 线性表的作用:线性表一般用于存储有顺序关系的数据序列。最典型的例子为队列(先进先出)、栈(先进后出)。
- 线性表支持操作:
一般而言,线性表至少支持以下三种操作:
- 插入元素:在线性表的指定位置插入一个元素(insert、push_back、push_front等函数)
- 删除元素:在线性表的指定位置删除一个元素(delete、pop_back、pop_front等函数)
- 访问元素:访问线性表中指定位置的一个元素(下表操作符[]等)
二、线性表有哪些种类
- 数组(array):数组是有序的元素序列,其特点是相邻的两个元素在物理内存上也是相邻的,整个数组是一段连续的内存空间。数组使用很方便,但由于其内存空间是连续的,因此使用数组之前要预设其大小;插入和删除操作要移动大量元素,操作的平均时间复杂度较链表高。
- 链表(lined list):链表不同于数组,链表是一种链式的数据结构(如图所示,图为双向链表)。链表由若干个节点相连而成,每个节点内部分为数据域和链接域,数据域即该节点保存的具体数据,链接域是指向其他节点的指针。
队列(queue):队列是一种特殊的线性表。数组和链表都应支持随机访问,而队列不支持。队列必须遵守“先进先出”的原则。举个例子:我往队列里按顺序存了3(第一个元素)、2、4、5、1,那么从队列中取出数据时,取出的顺序也只能为3、2、4、5、1,在这个例子中,不能跳过3而取出其他元素。
栈(stack):栈跟队列一样,也是一种特殊的线性表。不同的是,队列遵守的是“先进先出”的原则,栈遵守的是“后进先出”的原则。如图所示:
三、线性表的实现
上面提到线性表有数组、链表、队列以及栈四种。事实上无论是队列还是栈都可以用数组或链表来实现,因此下面介绍怎么用数组或链表来实现线性表。
1.用数组实现的线性表
现在先列出这里实现的主要操作类型:
- find:找到第一个与x相同的第一个元素位置并返回
- insert:在指定位置插入目标元素,时间复杂度O(n)
- deleteElement:删除指定位置的元素,时间复杂度O(n)
- operator[]:返回第K个元素的非const引用
find:在数组中一个一个对比,判断当前数组元素是否目标元素,找到了就返回这个引索,找不到就返回-1.
int find(T x){
for(rangeType i = 0; i < length_; i++){
if(x == array_[i])return i;
}
return -1;
}
insert:前面说过,用数组实现的线性表的插入和删除操作是比较麻烦的,主要原因是要把目标位置之后的元素都往前挪一个位置或往后挪一个位置(插入操作示意如下)。
核心代码:
for(rangeType j = length_; j > i; j++){
array_[j] = array_[j-1];
}
array_[i] = element;
deleteElement:跟insert差不多,不过insert是向后挪的,deleteElement是向前挪的。核心代码:
length_--;
for (rangeType j = i; j < length_; j++) {
array_[j] = array_[j+1];
}
operator[]:由于数组本身就支持下表操作,这里加个条件判断(引索是否有效)就完事了。核心代码:
if(k >= length_) throw std::runtime_error("CHArray::findKTh: k>=length_.");
else return array_[k];
2.用链表(双向链表)实现的线性表
主要操作类型跟用数组实现的差不多:
- find:找到第一个与x相同的第一个元素位置并返回
- insert:在指定位置插入目标元素,时间复杂度O(n)
- deleteElement:删除指定位置的元素,时间复杂度O(n)
- operator[]:返回第K个元素的非const引用
除了上面几个以外,还多出四个操作:
- push_back:在链表尾部插入目标元素,时间复杂度O(1)
- push_front:在链头尾部插入目标元素,时间复杂度O(1)
- pop_back:返回尾部元素后删除该元素,时间复杂度O(1)
- pop_front:返回头部元素后删除该元素,时间复杂度O(1)
在介绍实现这几个操作的思路之前,先详细介绍一下链表的构成。
上面提到过,链表由一个个节点组成,节点代码如下::
struct node{
node(T element):pre(NULL), next(NULL), data(element){}
node* pre; //上一个节点
node* next; //后一个节点
T data; //数据
};
链表就是由节点相连而成的链状数据结构,而节点得以连接的关键在于pre指针和next指针,这两个指针分别指向连接该节点的上一个节点、连接该节点的下一个节点(把上面的图再拿下来看看就懂了)。
虽然说链表是由这些节点组成的,但这不意味着实现链表类(class LinkedList)的时候,要把这些节点都保存到一个地方去(存到一个数组里之类的);由于节点本身就保存了与之相连的节点的信息,因此我们只需把头节点的指针保存下来(双向链表还要把尾节点指针保存),就可以通过节点间的连接关系访问链表中的每个节点了。链表类保存的数据:
node<T> *head_, *tail_; //链表表头、表尾
CH_list_size length_; //链表长度
接下来介绍具体的操作。
find:find的基本思想跟数组实现的版本差不多,不同的是这里涉及到链表的遍历。先上代码:
CH_list_size find(T x){
auto tmp = head_;
CH_list_size i = 0;
while(tmp != NULL){
if(tmp->data == x)return i;
tmp = tmp->next;
i++;
}
return -1;
}
分步解说:
(1)先创建一个临时的节点指针tmp,这个指针的初始值是链表头节点。
(2)先判断tmp节点的元素是不是要找的那个元素:tmp(这时等于head_,头节点指针)->data == x,如果是就返回当前的引索;如果不是,就转到下个节点(tmp=tmp->next,next是指向下个节点的指针);
(3)循环(2),直到tmp指向NULL为止(指向NULL即表明上一个节点已经是尾节点,链表已经遍历完了)
insert:链表版本的元素插入跟数组版本有很大区别,因为链表不要求内存空间上的连续,只要每个节点存储了与之相连的节点的信息就可以。因此,实现链表版本的关键在于新加入节点时,怎么修改节点之间的关系。
插入分三种情况:(1)在头部插入(push_front);(2)在尾部插入(push_back);(3)在元素中间插入。
(1)在头部插入(push_front):
a.首先创建一个节点:
b.然后让这个节点的next指向头节点:
c.最后让头节点的pre指向新节点,新节点称为头节点:
上代码:
bool push_front(T element){
node<T>* tmp;
try {
tmp = new node<T>(element); //创建新节点
} catch ( const std::bad_alloc& e ) {
return -1;
}
if(length_++ == 0) {
head_ = tail_ = tmp;
return true;
}else{
head_->pre = tmp; //让头节点的pre指向新节点
tmp->next = head_; //让新节点的next指向头节点
head_ = tmp; //让新节点称为头节点
}
}
(2)在尾部插入(push_back):(直接上代码吧...)
bool push_back(T element){
node<T>* tmp;
try {
tmp = new node<T>(element); //创建一个新节点
} catch ( const std::bad_alloc& e ) {
return -1;
}
if(length_++ == 0){
head_ = tail_ = tmp;
return true;
}else{
tail_->next = tmp; //让尾节点的next指向新节点
tmp->pre = tail_; //新节点的pre指向尾节点
tail_ = tmp; //让新节点称为新的尾节点
return true;
}
}
(3)在元素中间插入(上个图帮助理解)
代码:
node<T>* tmp_pre = tmp->pre;
tmp_pre->next = new node<T>(element);
tmp_pre->next->next = tmp;
tmp_pre->next->pre = tmp_pre;
tmp->pre = tmp_pre->next;
length_++;
偷懒了....上个github,大家觉得有兴趣的话去那看看吧...