2.1线性表简介
线性表是一种线性结构,它是由零个或多个数据元素构成的有限序列。线性表的特征是在一个序列中,除了头尾元素,每个元素都有且只有一个直接前驱,有且只有一个直接后继,而序列头元素没有直接前驱,序列尾元素没有直接后继。
数据结构中常见的线性结构有数组、单链表、双链表、循环链表等。线性表中的元素为某种相同的抽象数据类型。可以是C语言的内置类型或结构体,也可以是C++自定义类型。
2.2数组
数组在实际的物理内存上也是连续存储的,数组有上界和下界。C/C++语言中定义一个数组:
数组下标是从0开始的,a[0]对应第一个元素。其中,a[0]称为数组a的下界,a[6]称为数组a的上届。超过这个范围的下标使用数组,将造成数组越界错误。
数组的特点是:数据连续,支持快速随机访问。
数组分为固定数组与动态数组。其中固定数组的大小必须在编译时就能够确认,动态数组允许在运行时申请数组内存。复杂点的数组是多维数组,多维数组实际上也是通过一维数组来实现的。在C语言中,可以通过malloc来分配动态数组,C++使用new。另外,C++的标准模板库提供了动态数组类型vector以及内置有固定数组类型array。笔者在这里就不再对普通数组进行论述,接下来对C++中的模板库中的动态数组vector进行讲解。
2.2.1 STL array
C++中的数组类型是继承了C语言的特性,在使用数组的时候要注意数组越界操作问题。为了更安全的对数组进行操作,C++提出了数组模板类array。
(1)array模板类的声明
template <class T,size_t N> class array;
数组类是固定大小的序列容器,它们包含以严格线性序列排序的特定数量的元素。数组类具有固定大小,并且不通过分配器管理其元素的分配,它们是封装固定大小元素数组的聚合类型。
(2)容器属性
序列容器中的元素按严格的线性顺序排序。各个元素按其顺序访问它们的位置。
元素存储在连续的存储器位置,允许对元素进行恒定时间随机访问。可以偏移元素的指针以访问其他元素。
容器使用隐式构造函数和析构函数静态分配所需的空间。它的大小是编译时常量。没有内存或时间开销。
(3)内存分配策略
在内存分配策略上,array也与C-style数组类似。编译器在为array分配内存,取决于array定义的位置和方式。
若作为函数的局部对象,则将从栈上获得内存,与之对比是的vector,vector底层数据结构是动态数组,从自由存储区上分配内存:
若使用new操作符分配内存,则是在自由存储区上分配内存。
若作为全局变量或局部静态变量,则是在全局/静态存储区上分配的内存。
(4)array模板类的说明
array模板类中T为包含元素的类型(std::array::value_type),N为元素个数。
(5)array模板类头文件
使用array模板类之前需要包含#include 头文件!
(6)array的优势
array比数组更安全。它提供了opeartor[]与at()成员函数,后者将进行数组越界检查。
与其他容器相似,array也有自己的迭代器,因此array能够更好地与标准算法库结合起来。
通过array::swap函数,可以实现线性时间内的两个数组内容的交换。
另外,不像C-style数组,array容器类型的名称不会自动转换为指针。对于C++程序员来说,array要比C-style数组更好用。
建议:多使用array数组容器代替C类型数组,使操作数组元素更加安全!
/**
******************************************************************************
* @file main.cpp
* @author BruceOu
* @version V1.0
* @date 2019.03.07
* @brief STL array
******************************************************************************
*/
/**Includes*********************************************************************/
#include <iostream>
#include <array>
/**namespace********************************************************************/
using namespace std;
/**
* @brief 主函数
* @param argc
argv
* @retval None
*/
int main(int argc, char *argv[])
{
//创建长度为5的array
array<int,5> arr = {1,2,3,4,5};
size_t i;
//普通数组方式访问
for(i = 0;i < arr.size();i ++)
{
cout <<arr[i] <<" ";
}
cout <<endl;
cout <<"==============================" << endl;
//迭代器方式访问
for (auto it = arr.begin(); it != arr.end(); ++it)
{
cout << *it << " ";
}
cout <<endl;
cout << "-----------------------------" << endl;
for (auto it:arr)
{
cout << it << " ";
}
cout <<endl;
cout <<"==============================" << endl;
cout << "sizeof(array) = " << sizeof(arr) << endl;
cout << "size of array = " << arr.size() << endl;
cout << "max_size of array = " << arr.max_size() << endl;
if (arr.empty())
{
cout << "array is empty!" << endl;
}
else
{
cout << "array is not empty!" << endl;
}
//Element access
cout <<"==============================" << endl;
cout << "array[0] = " << arr[0] << endl;
cout << "array.at(4) = " << arr.at(4) << endl;
cout << "array.front() = " << arr.front() << endl;
cout << "array.back() = " << arr.back() << endl;
cout << "&array: " << arr.data() << " = " << &arr << endl;
return 0;
}
2.2.2 STL vector
底层数据结构
vector的底层数据结构是动态数组,因此,vector的数据安排以及操作方式与std::array十很相似,它们间的唯一差别在于对空间的运用灵活性上。array为静态数组,有着静态数组最大的缺点:每次只能分配一定大小的存储空间,当有新元素插入时,要经历 “找到更大的内存空间”->“把数据复制到新空间” ->“销毁旧空间” 三部曲, 对于std::array而言,这种空间管理的任务压在使用它的用户身上,用户必须把握好数据的数量,尽量在第一次分配时就给数据分配合理的空间(这有时很难做到),以防止“三部曲”带来的代价,而数据溢出也是静态数组使用者需要注意的问题。而vector用户不需要亲自处理空间运用问题。vector是动态空间,随着新元素的插入,旧存储空间不够用时,vector内部机制会自行扩充空间以容纳新元素,当然,这种空间扩充大部分情况下(几乎是)也逃脱不了“三部曲”,只是不需要用户自己处理,而且vector处理得更加安全高效。vector的实现技术关键就在于对其大小的控制以及重新配置时数据移动效率。
迭代器类型
对于C_style数组,我们使用普通指针就可以对数组进行各种操作。vector维护的是一个连续线性空间,与数组一样,所以无论其元素型别为何,普通指针都可以作为vector的迭代器而满足所有必要的条件。vector所需要的迭代器操作,包括operator*,operator->,operator++,operator–,operator+=,operator-=等,普通指针都具有。因此,普通指针即可满足vector对迭代器的需求。所以,vector提供了Random Access Iterators。
内存分配策略
标准库的实现者使用了这样的内存分配策略:以最小的代价连续存储元素。为了使vector容器实现快速的内存分配,其实际分配的容量要比当前所需的空间多一些(预留空间),vector容器预留了这些额外的存储区用于存放添加的新元素,于是不必为每个新元素进行一次内存分配。当继续向容器中加入元素导致备用空间被用光(超过了容量 capacity),此时再加入元素时vector的内存管理机制便会扩充容量至两倍,如果两倍容量仍不足,就扩张至足够大的容量。容量扩张必须经历“重新配置、元素移动、释放原空间”这个浩大的工程。按照《STL源码剖析》中提供的vector源码,vector的内存配置原则为:
-
如果vector原大小为0,则配置1,也即一个元素的大小。
-
如果原大小不为0,则配置原大小的两倍。
当然,vector的每种实现都可以自由地选择自己的内存分配策略,分配多少内存取决于其实现方式,不同的库采用不同的分配策略。
迭代器失效问题
vector管理的是连续的内存空间,在容器中插入(或删除)元素时,插入(或删除)点后面的所有元素都需要向后(或向前)移动一个位置,指向发生移动的元素的迭代器都失效。这里以插入操作示例:
随着元素的插入,原来分配的连续内存空间已经不够且无法在原地拓展新的内存空间,整个容器会被copy到另外一块内存上,此时指向原来容器元素的所有迭代器通通失效。
删除元素后,指向被删除元素的迭代器失效,这是显而易见的。
总之,vector向量相当于一个不定长数组,在内存中分配一块连续的内存空间进行存储。支持不指定vector大小的存储。通常此默认的内存分配能完成大部分情况下的存储。不仅如此,它把一些常用操作“封装”在了vector类型内部。
例如,若a是一个vector,可以用a.size( )读取它的大小,a.resize( )改变大小,a.push_back( )向尾部添加元素,a.pop_back( )删除最后一个元素。
vector是一个模板类,所以需要用vectora或者vectorb这样的方式来声明一个vector。 Vector是一个类似于int a[]的整数数组,而vector就是一个类似于string a[ ]的字符串数组。 vector看上去像是“一等公民”,因为它们可以直接赋值,还可以作为函数的参数或者返回值,而无须像传递数组那样另外用一个变量指定元素个数。
优点:
可以不指定大小,使用push_back、pop_back来进行动态操作,随机访问方便,即支持[ ]操作符和vector.at();节省空间。
缺点:
在内部进行插入删除操作效率低;只能在vector的最后进行push和pop,不能在vector的头进行push和pop;当动态添加的数据超过vector默认分配的大小时要进行整体的重新分配、拷贝与释放。
stu.h
#ifndef STU_H_
#define STU_H_
class Stu {
public:
int age;
Stu(int age);
Stu();
~Stu();
};
#endif /* STU_H_ */
stu.cpp
#include "Stu.h"
#include <iostream>
using namespace std;
Stu::Stu(int age)
{
this->age = age;
}
Stu::Stu()
{
cout << "Stu()" << endl;
}
Stu::~Stu()
{
cout << "~Stu()" << endl;
}
main.cpp
/**
******************************************************************************
* @file main.cpp
* @author BruceOu
* @version V1.0
* @date 2019.03.04
* @brief vector
******************************************************************************
*/
/**Includes*********************************************************************/
#include <iostream>
#include <vector>
#include "Stu.h"
/**namespace********************************************************************/
using namespace std;
struct Teacher{
int age;
};
void fx()
{
Teacher t1;
t1.age = 10;
Teacher t2;
t2.age = 20;
vector<Teacher> v;
v.push_back(t1);
v.push_back(t2);
Teacher t = v.at(1);//t2
cout<<t.age<<endl; //20
}
void f()
{
Stu stu1(10);
Stu stu2(20);
Stu stu3(30);
vector<Stu> v;
v.push_back(stu1);//0
v.push_back(stu2);//1
v.push_back(stu3);//2
for (unsigned int i = 0; i < v.size(); i++) {
Stu stu = v.at(i);
cout<<stu.age<<endl;
}
}
void f0()
{
//创建长度为0的只装int数据的vector v1对象
vector<int> v1;
//装数据
v1.push_back(3);//0
v1.push_back(1);//1
v1.push_back(2);//2
//取出长度
int size = v1.size();//size==3
// cout<<size<<endl;
//取数数据
// cout<<v1.at(0)<<endl;//3
// cout<<v1[0]<<endl; //3
for (int i = 0; i < size; i++)
{
cout << v1[i] << endl;
cout << v1.at(i) << endl;
}
}
void f1()
{
//创建长度为2的只装int数据的vector v1对象,默认值为0
vector<int> v1(2);//[0][0]
// cout<<v1.size()<<endl;//2
// cout<<v1[0]<<":"<<v1[1]<<endl;//0:0
v1.push_back(1);
v1.push_back(2);
v1.push_back(3);//[0][0][1][2][3]
size_t size = v1.size();//5
for (size_t i = 0; i < size; i++) {
cout << "v1[" << i << "]=" << v1[i] << endl;
}
}
void f2()
{
//创建长度为3的只装string数据的vector v1对象,默认值为"好"
vector<string> v(3, "好");
v.push_back("你好");
size_t size = v.size();
for (size_t i = 0; i < size; i++) {
cout << "v1[" << i << "]=" << v[i] << endl;
}
}
void f3()
{
int ia[6] = { -2, -1, 0, 1, 2, 1024 };
//使用数据为新创建的vector对象赋值
vector<int> v1(&ia[2], &ia[5]);//[0][1][2]
for (size_t i = 0; i < v1.size(); i++) {
cout << "v1[" << i << "]=" << v1[i] << endl;
}
cout << "v2=================" << endl;
//克龙
vector<int> v2(v1);
for (size_t i = 0; i < v2.size(); i++) {
cout << "v2[" << i << "]=" << v2[i] << endl;
}
cout << "v3=================" << endl;
//克龙
vector<int> v3 = v2;
for (size_t i = 0; i < v3.size(); i++) {
cout << "v3[" << i << "]=" << v3[i] << endl;
}
}
void f4()
{
vector<int> v1;
v1.push_back(1);//[1]
// v1.back() == v1.at(v1.size()-1);
cout << v1.back() << endl;//1 back()取出最后一个无素的值
v1.push_back(2);//[1][2]
cout << v1.back() << endl;//2
v1.pop_back();// pop_back()删除最后一个元素 [1]
cout << v1.back() << endl;//1
}
//修改元素的值
void f5()
{
vector<int> v(3, 100);//[100][100][100]
vector<int>::iterator it = v.end();
int i = 200;
for (it = v.begin(); it != v.end(); ++it) {
i++;
*it = i;
cout << *it << endl;
}
}
/**
* @brief 主函数
* @param argc
argv
* @retval None
*/
int main(int argc, char *argv[])
{
f0();
return 0;
}
2.3单向链表
2.3.1单向链表C++模板实现
单向链表是链表的一种。链表由节点所构成,节点内含一个指向下一个节点的指针,节点依次链接成为链表。因此,链表这种数据结构通常在物理内存上是不连续的。链表的通常含有一个头节点,头节点不存放实际的值,它含有一个指针,指向存放元素的第一个节点。
单向链表的节点结构
//节点结构
template <typename T>
class Node
{
public :
T _value;
Node* _next;
public:
Node() = default;
Node(T value, Node * next)
: _value(value), _next(next){}
};
【注】
_value: 节点的值
_next: 指针,指向下一个节点
单向链表的抽象数据结构
//单链表
template <typename T>
class SingleLink
{
public:
typedef Node<T>* pointer;
SingleLink();
~SingleLink();
int size(); //获取长度
bool isEmpty(); //判空
Node<T>* insert(int index, T t); //在指定位置进行插入
Node<T>* insert_head(T t); //在链表头进行插入
Node<T>* insert_last(T t); //在链表尾进行插入
Node<T>* del(int index); //在指定位置进行删除
Node<T>* delete_head(); //删除链表头
Node<T>* delete_last(); //删除链表尾
T get(int index); //获取指定位置的元素
T get_head(); //获取链表头元素
T get_last(); //获取链表尾元素
Node<T>* getHead(); //获取链表头节点
private :
int count;
Node<T> * phead;
private :
Node<T> * getNode(int index); //获取指定位置的节点
};
【注】
- phead: 链表的头节点。
- count: 链表元素个数。
单链表添加节点
链表的插入元素操作时间复杂度O(1),只需要进行指针的指向修改操作。
【注】
在2之后添加7:
- 为元素7构建节点 。
- 将节点7的next指向节点3。(节点3 的位置要先保留起来)
- 将节点2 的next指针指向节点7。
/*
在指定位置插入新节点
*/
template <typename T>
Node<T>* SingleLink<T>::insert(int index, T t)
{
Node<T> * preNode = getNode(index);
if (preNode)
{
Node<T> *newNode = new Node<T>(t,preNode->_next);//指向下一个节点的地址保存在新节点中
preNode->_next = newNode;//上一个节点指向新节点
count++;
return newNode;
}
return nullptr;
};
/*
从头部插入
*/
template <typename T>
Node<T>* SingleLink<T>::insert_head(T t)
{
return insert(0, t);
};
/*
从尾部进行插入
*/
template <typename T>
Node<T>* SingleLink<T>::insert_last(T t)
{
return insert(count, t);
};
单链表删除节点
单链表的删除操作同样是一个时间复杂度O(1)的操作,它也只需要修改节点的指针指针后即可销毁被删除节点。
例如我们删除链表元素7:
【注】
删除2后的7:
- 获取元素7节点索引 。
- 将节点2的指向节点3。
- 将节点7删除。
/*
删除链表指定位置元素
*/
template <typename T>
Node<T>* SingleLink<T>::del(int index)
{
if (isEmpty())
return nullptr;
Node<T>* ptrNode = getNode(index);
Node<T>* delNode = ptrNode->_next;
ptrNode->_next = delNode->_next;
count--;
delete delNode;
return ptrNode->_next;
};
/*
删除头节点
*/
template<typename T>
Node<T>* SingleLink<T>::delete_head()
{
return del(0);
};
/*
删除尾节点
*/
template<typename T>
Node<T>*SingleLink<T>::delete_last()
{
return del(count);
};
单链表代码测试
int main()
{
SingleLink<int> link;
for (int i = 0; i < 10; i++)
{
link.insert(i, i);
}
cout << link.size() << endl;
link.insert_head(1111);
link.insert_last(2222);
SingleLink<int>::pointer ptr = link.getHead();
while (ptr != nullptr)
{
cout << ptr->_value << endl;
ptr = ptr->_next;
}
return 0;
}
完整代码如下所示:
SingleList.h
# ifndef SINGLE_LIST_HXX
# define SINGLE_LIST_HXX
//节点结构
template <typename T>
class Node
{
public :
T _value;
Node* _next;
public:
Node() = default;
Node(T value, Node * next): _value(value), _next(next){}
};
//单链表
template <typename T>
class SingleLink
{
public:
typedef Node<T>* pointer;
SingleLink();
~SingleLink();
int size(); //获取长度
bool isEmpty(); //判空
pointer insert(int index, T t); //在指定位置进行插入
pointer insert_head(T t); //在链表头进行插入
pointer insert_last(T t); //在链表尾进行插入
pointer del(int index); //在指定位置进行删除
pointer delete_head(); //删除链表头
pointer delete_last(); //删除链表尾
T get(int index); //获取指定位置的元素
T get_head(); //获取链表头元素
T get_last(); //获取链表尾元素
pointer getHead(); //获取链表头节点
private :
int count;
pointer phead;
private :
pointer getNode(int index); //获取指定位置的节点
};
//默认构造函数
template <typename T>
SingleLink<T>::SingleLink():count(0), phead(nullptr)
{
//创建头节点
phead = new Node<T>();
phead->_next = nullptr;
};
/*
返回指定索引的前一个位置节点,若链表为空,则返回头节点
*/
template <typename T>
Node<T>* SingleLink<T>::getNode(int index)
{
if (index > count||index < 0 )
return nullptr;
int temp = 0;
Node<T>* preNode = phead;
while (temp < index)
{
temp++;
preNode = preNode->_next;
}
return preNode;
}
/*
析构函数
*/
template <typename T>
SingleLink<T>::~SingleLink()
{
Node<T>* pNode = phead->_next;
while (nullptr != pNode)
{
Node<T>* temp = pNode;
pNode = pNode->_next;
delete temp;
}
//进行销毁
};
//返回链表的大小
template <typename T>
int SingleLink<T>::size()
{
return count;
};
//链表判空
template <typename T>
bool SingleLink<T>::isEmpty()
{
return count==0;
};
template<typename T>
Node<T>* SingleLink<T>::getHead()
{
return phead->_next;
}
/*
在指定位置插入新节点
*/
template <typename T>
Node<T>* SingleLink<T>::insert(int index, T t)
{
Node<T> * preNode = getNode(index);
if (preNode)
{
Node<T> *newNode = new Node<T>(t,preNode->_next); //构建新节点,构建时指明节点的next节点
preNode->_next = newNode;//上一个节点指向新节点
count++;
return newNode;
}
return nullptr;
};
/*
从头部插入
*/
template <typename T>
Node<T>* SingleLink<T>::insert_head(T t)
{
return insert(0, t);
};
/*
从尾部进行插入
*/
template <typename T>
Node<T>* SingleLink<T>::insert_last(T t)
{
return insert(count, t);
};
/*
删除链表指定位置元素
*/
template <typename T>
Node<T>* SingleLink<T>::del(int index)
{
if (isEmpty())
return nullptr;
Node<T>* ptrNode = getNode(index);
Node<T>* delNode = ptrNode->_next;
ptrNode->_next = delNode->_next;
count--;
delete delNode;
return ptrNode->_next;
};
/*
删除头节点
*/
template<typename T>
Node<T>* SingleLink<T>::delete_head()
{
return del(0);
};
/*
删除尾节点
*/
template<typename T>
Node<T>*SingleLink<T>::delete_last()
{
return del(count);
};
# endif
main.cpp
/**
******************************************************************************
* @file main.cpp
* @author BruceOu
* @version V1.0
* @date 2019.03.07
* @brief 单向链表
******************************************************************************
*/
/**Includes*********************************************************************/
#include <iostream>
#include "SingleList.h"
/**namespace********************************************************************/
using namespace std;
/**
* @brief 主函数
* @param argc
argv
* @retval None
*/
int main(int argc, char *argv[])
{
SingleLink<int> link;
for (int i = 0; i < 10; i++)
{
link.insert(i, i);
}
cout << link.size() << endl;
link.insert_head(1111);
link.insert_last(2222);
SingleLink<int>::pointer ptr = link.getHead();
while (ptr != nullptr)
{
cout << ptr->_value << endl;
ptr = ptr->_next;
}
return 0;
}
2.3.2 STL forward_list
底层数据结构
单向列表是一个容器,支持在其任何地方快速插入和删除元素,不支持快速的随机访问。它被实现为单向链表,和C中它的实现相比,基本上不会有任何开销。 当不需要双向迭代的时候, forward_list则是不错的选择,该容器具有更高的空间利用率。 插入和删除操作不会使指向其他元素指针、引用和迭代器失效。
forward_list的底层数据结构为单向链表。如C++标准所讲,forward_list容器支持前向遍历元素序列,允许常数时间内在任意位置的插入或删除操作并进行自动的内存管理。与list的主要区别是forward_list没有反方向的迭代器,不过也正因如此,forward_list的每个节点都节省了迭代器大小的开销,在元素众多的时候,将比list消耗少得多的内存。
受单向链表这种特殊结构的影响,forward_list在很多地方表现得和其他容器不同:
forward_list特殊之一:forward_list不提供返回其大小的操作。
在所有已知的STL容器中,forward_list是唯一一个不提供size()的容器。不提供的原因在于计算一个forward_list的长度需要线性的时间,库用户有时无法忍受这样的时间开销。其他容器提供的size()操作皆可以在常数时间内完成(在C++98时,list也是线性时间)。为了节省内存,forward_list甚至不跟踪序列的长度,要想获得某个forward_list对象的长度,用户需要通过distance()来计算。这带来了一些不便,但使得用户远离了size()带来的高消耗。每个容器类型都有三个与大小相关的操作:max_size(),empty(),size(),而forward_list只提供了前两个。
int main()
{
forward_list<int> flist;
for (int i = 0; i < 10; i++)
{
flist.push_front(i);
}
std::cout << std::distance(flist.begin(), flist.end()); //输出10
}
forward_list特殊之二:forward_list是唯一一个在给定位置之后插入新元素的容器。
为此,forward_list提供了如下的插入接口:
接口 | 描述 |
---|---|
insert_after | 在给定位置之后插入新元素 |
emplace_after | 在给定位置之后构造新元素 |
erase_after | 删除给定位置之后的元素 |
splice_after | 将另一个forward_list的元素移动到本forward_list的指定位置之后 |
其他所有STL容器都是在指定位置之前插入元素(除了std::array,它不允许插入)。forward_list的这种特殊处理,还是出于效率的考虑。对于单链表我们应该很熟悉,为了在某个指定节点之前插入插入节点,我们必须改变插入位置的前一个节点的指向。换句话说,为了在指定节点之前插入新元素,我们必须要先获得插入位置前一个位置的节点,为了获取前面这个节点,需要线性的操作时间。
而如果我们是在指定位置之后插入新元素,则无需线性时间的查找操作,这样可实现常数时间的插入:
同样的,处于性能的考虑,forward_list没有提供在尾部进行操作的接口,包括push_back(),pop_back()和emplace_back(),这些操作对单列表来说都至少要花费O(n)来完成。
迭代器失效问题
指向被删除元素的迭代器,在删除之后失效。
forward_list和array是新C++标准增加的类型。与内置数组相比,array是一个种更安全、更容易使用的数组类型。与内置数组类似,array对象的大小是固定的。因此,array不支持添加和删除元素以及改变容器大小的操作。forward_list的设计目标是达到与最好的手写的单向链表数据结构相当的性能。因此,forward_list没有size操作,因为保存或计算其大小就会比手写链表多出额外的开销。对其他容器而言,size保证是一个快速的常量时间的操作。
/**
******************************************************************************
* @file main.cpp
* @author BruceOu
* @version V1.0
* @date 2019.03.07
* @brief forward_list
******************************************************************************
*/
/**Includes*********************************************************************/
#include <forward_list>
#include <iostream>
#include <algorithm>
#include <iterator>
#include <string>
/**namespace********************************************************************/
using namespace std;
void printLists (const string& s, const forward_list<int>& l1,
const forward_list<int>& l2)
{
cout << s << endl;
cout << " list1: ";
copy (l1.cbegin(), l1.cend(), ostream_iterator<int>(cout," "));
cout << endl << " list2: ";
copy (l2.cbegin(), l2.cend(), ostream_iterator<int>(cout," "));
cout << endl;
}
/**
* @brief 主函数
* @param argc
argv
* @retval None
*/
int main(int argc, char *argv[])
{
//创建两个前向列表
forward_list<int> list1 = { 1, 2, 3, 4 };
forward_list<int> list2 = { 77, 88, 99 };
printLists ("initial:", list1, list2);
//在list2头部插入6个新元素
list2.insert_after(list2.before_begin(),99);
list2.push_front(10);
list2.insert_after(list2.before_begin(), {10,11,12,13} );
printLists ("6 new elems:", list1, list2);
//在list1头部插入list2全部元素
list1.insert_after(list1.before_begin(),
list2.begin(),list2.end());
printLists ("list2 into list1:", list1, list2);
//删除list2的第二个元素,删除list2中值为99的元素后面的所有元素
list2.erase_after(list2.begin());
list2.erase_after(find(list2.begin(),list2.end(),
99),
list2.end());
printLists ("delete 2nd and after 99:", list1, list2);
//对list1排序,并把list1赋值给list2,对list2去重
list1.sort();
list2 = list1;
list2.unique();
printLists ("sorted and unique:", list1, list2);
//把已序的list2合并到list1中
list1.merge(list2);
printLists ("merged:", list1, list2);
return 0;
}
2.4双向链表
2.4.1双向链表C++模板实现
单链表的节点链接是单方向的,要得到指定节点的前一个节点,必须从头遍历链表。
双向链表是链表的一种。与单链表一样,双向节点由节点链接而成,每个节点含有两个指针,分别指向直接前驱与直接后继。从双向链表的任何一个节点开始都能够遍历整个链表。
我们将双向链表实现为双向循环链表,也即是最后一个元素的后继将指向头节点,整个链表形成一个循环。
例如,我们为元素1,2,3,4,5 构建一个双向循环链表
双向链表节点结构
双向循环的节点中,比单向链表中多了一个指向直接前驱的指针。
/*
双向链表的节点结构
*/
template <typename T>
struct Node
{
public:
Node()= default;
Node(T value, Node<T>* preptr, Node<T>* nextptr)
:_value(value), pre_ptr(preptr), next_ptr(nextptr){}
public:
T _value;
Node<T>* pre_ptr;
Node<T>* next_ptr;
};
【注】
- _value: 节点元素的值
- pre_ptr:指向直接前驱的指针
- next_ptr:指向直接后继的指针
- 双向链表的抽象数据结构
双向链表类的定义与单链表相似。
/*
* 双向链表类
*/
template<typename T>
class DoubleLink
{
public:
typedef Node<T>* pointer;
public:
DoubleLink();
~DoubleLink(){};
public:
Node<T>* insert(int index, T value);
Node<T>* insert_front(T value);
Node<T>* insert_last(T value);
Node<T>* del(int index);
Node<T>* delete_front();
Node<T>* delete_last();
bool isEmpty();
int size();
T get(int index);
T get_front();
T get_last();
Node<T>* getHead();
private:
Node<T>* phead;
int count;
private :
Node<T>* getNode(int index);
};
双向链表添加节点
与单链表一样,双向链表添加节点的时间复杂度为O(1),它也只需要修改相关指针的指向。
/*
*将新节点插到第一个位置
*/
template <typename T>
Node<T>* DoubleLink<T>::insert_front(T value)
{
Node<T>* newNode = new Node<int>(value, phead, phead->next_ptr);
phead->next_ptr ->pre_ptr= newNode;
phead->next_ptr = newNode;
count++;
return newNode;
};
/*
*将新节点插到链表尾部
*/
template <typename T>
Node<T>* DoubleLink<T>::insert_last(T value)
{
Node<T> * newNode = new Node<int>(value, phead->pre_ptr, phead);
phead->pre_ptr->next_ptr = newNode;
phead->pre_ptr = newNode;
count++;
return newNode;
};
/*
*将节点位置插到index位置之前
*/
template <typename T>
Node<T>* DoubleLink<T>::insert(int index, T value)
{
if (index == 0)
return insert_front(value);
Node<T>* pNode = getNode(index);
if (pNode == nullptr)
return nullptr;
Node<T>* newNode = new Node<T>(value, pNode->pre_ptr, pNode);
pNode->pre_ptr->next_ptr = newNode;
pNode->pre_ptr = newNode;
count++;
return newNode;
};
双向链表删除节点
双向链表的删除操作时间复杂度为O(1).我们删除节点7:
/*
*删除链表第一个节点
*返回删除后链表第一个节点
*/
template<typename T>
Node<T>* DoubleLink<T>::delete_front()
{
if (count == 0)
{
return nullptr;
}
Node<T>* pnode = phead->next_ptr;
phead->next_ptr = pnode->next_ptr;
pnode->next_ptr->pre_ptr = phead;
delete pnode;
count--;
return phead->next_ptr;
};
/*
*删除链表的末尾节点
*返回删除后链表尾部元素
*/
template<typename T>
Node<T>* DoubleLink<T>::delete_last()
{
if (count == 0)
{
return nullptr;
}
Node<T>*pnode = phead->pre_ptr;
pnode->pre_ptr->next_ptr = phead;
phead->pre_ptr = pnode->pre_ptr;
delete pnode;
count--;
return phead->pre_ptr;
}
/*
*删除指定位置的元素
*
*/
template <typename T>
Node<T>* DoubleLink<T>::del(int index)
{
if (index == 0)
return delete_front();
if (index == count - 1)
return delete_last();
if (index >= count)
return nullptr;
Node<T>* pnode = getNode(index);
pnode->pre_ptr->next_ptr = pnode->next_ptr;
pnode->next_ptr->pre_ptr = pnode->pre_ptr;
Node<T>* ptemp = pnode->pre_ptr;
delete pnode;
count--;
return ptemp;
};
其他的接口实现都很简单,这里不再讲解。下面有提供完整的工程项目及源代码。
双向链表代码测试
int main()
{
DoubleLink<int> dlink;
//插入测试
for (int i = 0; i < 10; i++)
{
dlink.insert(0, i+10);
}
dlink.insert(0, 100);
dlink.insert_last(1000);
cout <<"链表长度:"<< dlink.size() << endl;
//删除测试
dlink.delete_front();
dlink.delete_last();
dlink.del(3);
DoubleLink<int>::pointer ptr = dlink.getHead();
ptr = ptr->next_ptr;
while (ptr != dlink.getHead())
{
cout << ptr->_value<<endl;
ptr = ptr->next_ptr;
}
getchar();
return 0;
}
【注】
- C++模板不支持分离编译,因此类定义与成员函数的实现都在.h文件中完成;
- 可以看到代码中new一个新节点之后,并没有使用(prt!=nullptr)来检查内存分配是否成功,这是因为new失败时直接抛出异常,不同于C语言malloc内存分配失败返回NULL。
完整代码如下所示:
DoubleLink.h
# ifndef DOUBLE_LINK_HPP
# define DOUBLE_LINK_HPP
/*
双向链表的节点结构
*/
template <typename T>
struct Node
{
public:
Node()= default;
Node(T value, Node<T>* preptr, Node<T>* nextptr)
:_value(value), pre_ptr(preptr), next_ptr(nextptr){}
public:
T _value;
Node<T>* pre_ptr;
Node<T>* next_ptr;
};
/*
* 双向链表类
*/
template<typename T>
class DoubleLink
{
public:
typedef Node<T>* pointer;
public:
DoubleLink();
~DoubleLink(){};
public:
Node<T>* insert(int index, T value);
Node<T>* insert_front(T value);
Node<T>* insert_last(T value);
Node<T>* del(int index);
Node<T>* delete_front();
Node<T>* delete_last();
bool isEmpty();
int size();
T get(int index);
T get_front();
T get_last();
Node<T>* getHead();
private:
Node<T>* phead;
int count;
private :
Node<T>* getNode(int index);
};
/*
* 构造函数
*
*/
template <typename T>
DoubleLink<T>::DoubleLink()
{
phead = new Node<T>(0, nullptr, nullptr);
phead->next_ptr = phead;
phead->pre_ptr = phead;
count = 0;
};
template<typename T>
Node<T>* DoubleLink<T>::getHead()
{
return phead;
}
/*
*返回链表长度
*/
template <typename T>
int DoubleLink<T>::size()
{
return count;
}
/*
获取指定下标的元素
*/
template <typename T>
Node<T>* DoubleLink<T>::getNode(int index)
{
if (index >= count || index < 0)
return nullptr;
if (index<=count / 2) //如果在前半部分
{
Node<T>* pnode = phead->next_ptr;
while (index)
{
pnode = pnode->next_ptr;
index--;
}
return pnode;
} //在后半部分
index = count - index-1;
Node<T>* pnode = phead->pre_ptr;
while (index)
{
pnode = pnode->pre_ptr;
index--;
}
return pnode;
};
/*
*将新节点插到第一个位置
*/
template <typename T>
Node<T>* DoubleLink<T>::insert_front(T value)
{
Node<T>* newNode = new Node<int>(value, phead, phead->next_ptr);
phead->next_ptr ->pre_ptr= newNode;
phead->next_ptr = newNode;
count++;
return newNode;
};
/*
*将新节点插到链表尾部
*/
template <typename T>
Node<T>* DoubleLink<T>::insert_last(T value)
{
Node<T> * newNode = new Node<int>(value, phead->pre_ptr, phead);
phead->pre_ptr->next_ptr = newNode;
phead->pre_ptr = newNode;
count++;
return newNode;
};
/*
*将节点位置插到index位置之前
*/
template <typename T>
Node<T>* DoubleLink<T>::insert(int index, T value)
{
if (index == 0)
return insert_front(value);
Node<T>* pNode = getNode(index);
if (pNode == nullptr)
return nullptr;
Node<T>* newNode = new Node<T>(value, pNode->pre_ptr, pNode);
pNode->pre_ptr->next_ptr = newNode;
pNode->pre_ptr = newNode;
count++;
return newNode;
};
/*
*删除链表第一个节点
*返回删除后链表第一个节点
*/
template<typename T>
Node<T>* DoubleLink<T>::delete_front()
{
if (count == 0) //空树,返回nullptr
{
return nullptr;
}
Node<T>* pnode = phead->next_ptr;
phead->next_ptr = pnode->next_ptr;
pnode->next_ptr->pre_ptr = phead;
delete pnode;
count--;
return phead->next_ptr;
};
/*
*删除链表的末尾节点
*返回删除后链表尾部元素
*/
template<typename T>
Node<T>* DoubleLink<T>::delete_last()
{
if (count == 0)
{
return nullptr;
}
Node<T>*pnode = phead->pre_ptr;
pnode->pre_ptr->next_ptr = phead;
phead->pre_ptr = pnode->pre_ptr;
delete pnode;
count--;
return phead->pre_ptr;
}
/*
*删除指定位置的元素
*
*/
template <typename T>
Node<T>* DoubleLink<T>::del(int index)
{
if (index == 0)
return delete_front();
if (index == count - 1)
return delete_last();
if (index >= count)
return nullptr;
Node<T>* pnode = getNode(index);
pnode->pre_ptr->next_ptr = pnode->next_ptr;
pnode->next_ptr->pre_ptr = pnode->pre_ptr;
Node<T>* ptemp = pnode->pre_ptr;
delete pnode;
count--;
return ptemp;
};
template <typename T>
bool DoubleLink<T>::isEmpty()
{
return count == 0;
};
/*
*获取第一个节点的值
*/
template<typename T>
T DoubleLink<T>::get_front()
{
return phead->next_ptr->_value;
};
/*
*获取最后一个节点的值
*/
template <typename T>
T DoubleLink<T>::get_last()
{
return phead->pre_ptr->_value;
};
/*
*获取指定位置节点的值
*/
template <typename T>
T DoubleLink<T>::get(int index)
{
Node<T> pnode = getNode(index);
return pnode->_value;
};
# endif
main.cpp
/**
******************************************************************************
* @file main.cpp
* @author BruceOu
* @version V1.0
* @date 2019.03.07
* @brief 双向链表
******************************************************************************
*/
/**Includes*********************************************************************/
#include <iostream>
#include"DoubleLink.h"
/**namespace********************************************************************/
using namespace std;
/**
* @brief 主函数
* @param argc
argv
* @retval None
*/
int main(int argc, char *argv[])
{
DoubleLink<int> dlink;
//插入测试
for (int i = 0; i < 10; i++)
{
dlink.insert(0, i+10);
}
dlink.insert(0, 100);
dlink.insert_last(1000);
cout <<"链表长度:"<< dlink.size() << endl;
//删除测试
dlink.delete_front();
dlink.delete_last();
dlink.del(3);
DoubleLink<int>::pointer ptr = dlink.getHead();
ptr = ptr->next_ptr;
while (ptr != dlink.getHead())
{
cout << ptr->_value<<endl;
ptr = ptr->next_ptr;
}
return 0;
}
2.4.2 STL list
底层数据结构
list同样是一个模板类,它底层数据结构为双向循环链表。因此,它支持任意位置常数时间的插入/删除操作,不支持快速随机访问。
迭代器类型
list的迭代器具备前移、后移的能力,所以list提供的是Bidirectional iterator(双向迭代器)。由于采用的是双向迭代器,自然也很方便在指定元素之前插入新节点,所以list很正常地提供了insert()操作与push_back()/pop_back()操作。在C++11中,list新增了三个接口,以支持在指定位置构造对象后插入容器中:
接口(C++11新增) | 描述 |
---|---|
emplace | 在指定位置之前插入新构造的元素 |
emplace_front | 在链表头插入新构造的元素 |
emplace_back | 在链表尾插入新构造的元素 |
内存分配策略
list的空间配置策略,自然是像我们普通双向链表那样,有多少元素申请多少内存。它不像vactor那样需要预留空间供新元素的分配,也不会因找不到连续的空间而引起整个容器的内存迁移。
迭代器失效问题
list 有一个重要性质:插入操作(insert)与接合操作(splice)都不会造成原有的list迭代器失效。这在vector是不成立的,因为vactor的插入可能引起空间的重新配置,导致原来的迭代器全部失效。list的迭代器失效,只会出现在删除的时候,指向删除元素的那个迭代器在删除后失效。
通常来说,forward_list在使用灵活度上比不上list,因为它只能单向迭代元素,且提供的接口没有list多。然而,在内存的使用上,它是比list占优势的。当对内存的要求占首要位置时,应该选择forward_list。
list 双向链表的每一个结点都包括一个信息快Info、一个前驱指针Pre、一个后驱指针Post。可以不分配必须的内存大小方便的进行添加和删除操作。使用的是非连续的内存空间进行存储。
优点:
不使用连续内存完成动态操作;在内部方便的进行插入和删除操作;可在两端进行push、pop。
缺点:
不能进行内部的随机访问,即不支持[ ]操作符和vector.at();相对于verctor占用内存多。
/**
******************************************************************************
* @file main.cpp
* @author BruceOu
* @version V1.0
* @date 2019.03.04
* @brief list
******************************************************************************
*/
/**Includes*********************************************************************/
#include <iostream>
#include <list>
/**namespace********************************************************************/
using namespace std;
//创建list对象
void f1()
{
list<int> l;
//在后面增加元素
l.push_back(10);
l.push_back(20);
l.push_back(30);// 10, 20, 30
//在前面增加元素
l.push_front(2);//2, 10, 20, 30
list<int>::iterator it;
for (it = l.begin(); it != l.end(); ++it)
{
cout << *it << endl;
}
cout << "----------------------------------" << endl;
it = l.begin();
it++;
l.erase(it);//根据指针删除元素 2, 20, 30
it = l.begin();
for (it = l.begin(); it != l.end(); ++it)
cout << *it << endl;
cout << "=================" << endl;
l.push_back(20);// 2, 20, 30, 20
l.remove(50); //根据元值删除元素 2, 20, 30, 20
// l.clear(); //全部删除
l.sort();
l.reverse();
it = l.begin();
for (it = l.begin(); it != l.end(); ++it)
cout << *it << endl;
}
/**
* @brief 主函数
* @param argc
argv
* @retval None
*/
int main(int argc, char *argv[])
{
f1();
return 0;
}