一、有关list的介绍
- list是可以在常数范围内在任意位置进行插入和删除的序列式容器,并且该容器可以前后双向迭代
- list的底层是双向链表结构,双向链表中每个元素存储在互不相关的独立节点中,在节点中通过指针指向其前一个元素和后一个元素。
- Iist与forward_list非常相似,最主要的不同在于forward_list是单链表,只能朝前迭代,以让其更简单高效。
- 与其他的序列式容器相比(array,vector,deque),Iist通常在任意位置进行插入、移除元素的执行效率更好。
- 与其他序列式容器相比,Iist和forward_list最大的缺陷是不支持任意位置的随机访问,比如:要访问list的第6个元素,必须从已知的位置(比如头部或者尾部)迭代到该位置,在这段位置上迭代需要线性的时间开销;list还需要一些额外的空间,以保存每个节点的相关联信息(对于存储类型较小元素的大list来说这可能是一个重要的因素)
- list中的sort和算法库中sort的区别
在C++中,有两种不同的 sort
函数:一种是 list
容器的成员函数 sort
,另一种是算法库中的 std::sort
。它们在功能和应用上有一些区别。
-
list 中的 sort:
list
是 C++ 标准库中的双向链表容器,它具有自己的成员函数sort
,可以用于对链表中的元素进行排序。list::sort
使用的是链表特有的插入、删除操作,因此适用于链表这种数据结构。它的时间复杂度为 O(N*logN)。- 由于链表的特性,其插入、删除操作的时间复杂度为 O(1),因此
list::sort
可以高效地对链表进行排序。
-
算法库中的 std::sort:
std::sort
是 C++ 标准库中的算法,可以用于对各种容器(如数组、向量等)中的元素进行排序。std::sort
使用的是比较和交换操作,适用于随机访问迭代器所表示的数据结构(比如数组、向量等)。它的时间复杂度也为 O(N*logN)。- 对于随机访问迭代器所表示的数据结构,
std::sort
是一种通用的排序算法,可以适用于各种数据结构。
总的来说,list::sort
适用于对链表进行排序,而 std::sort
适用于对各种支持随机访问的数据结构进行排序,例如数组、向量等。选择哪种排序方式取决于你使用的数据结构以及具体的排序需求。
迭代器的类型:
虽然不管是算法库中的sort还是list中的sort,在传参时都使用了模板,理论上是可以传递任意类型的迭代器,但是它们在实现函数时,可能使用了 只有特定迭代器 才能使用的操作,导致如果传递的迭代器不支持这个功能,该函数就无法使用。
例如,在库sort实现时,使用了指针相减来确定元素距离,而对于链表来说,它的指针的指向不是顺序的而是随机的,用指针相减是毫无意义的。
那我们在使用时,如果确定传递怎样的迭代器呢?
实际上,在文档中命名迭代器时会有提示。
例如在算法库中sort迭代器命名为 RandomAccessIterator - 随机访问迭代器。所以在使用前可以查看一下文档。
迭代器从功能分类上可以分为五种主要类型:
-
输入迭代器(Input Iterator):
只能用于读取数据,一般用于单向遍历容器中的元素,例如std::vector
、std::list
等。 -
输出迭代器(Output Iterator):
只能用于写入数据,也是单向的,只能逐个元素地写入数据。 -
单向迭代器(Forward Iterator):
可以读取和写入数据,支持在序列中向前移动,例如单向链表。 -
双向迭代器(Bidirectional Iterator):
除了具备前向迭代器的功能外,还支持在序列中向后移动,例如std::list
。 -
随机访问迭代器(Random Access Iterator):
提供了对迭代器进行算术操作的功能,可以在 O(1) 的时间复杂度内实现随机访问和跳跃式遍历,例如数组。
需要注意的是,我们传递的迭代器只要能够满足该函数所需的功能即可,不是一定要传特定的迭代器才行。例如,双向迭代器和随机访问迭代器含有单向迭代器的功能,所以这两个迭代器是可以传递给所使用的函数的。
二、list的模拟实现
list的数据结构:
// 带头双向循环链表的一个节点
template<class T>
struct list_node
{// 成员变量list_node<T>* _prev;list_node<T>* _next;T _data;// 构造函数list_node(const T& x = T()):_data(x), _prev(nullptr), _next(nullptr){}
};template<class T>
class list
{typedef list_node<T> node;public:// 迭代器typedef __list_iterator<T, T&, T*> iterator;typedef __list_iterator<T, const T&, const T*> const_iterator;
private:// 头节点的指针 node* _head;
};
1、迭代器
这里迭代器的底层一定是一个Node*的指针,但是不能直接将Node*定义为iterator,因为Node*是一个指针,属于内置类型,当我们使用++iterator时,将相当于将Node*指针向后移动一个Node大小的空间,而list每一个节点存放的地址并不是连续的,所以这时候的++操作是不行的。
注:对于string和vector来说,他俩的底层是使用一个可变空间的数组,每一个元素存放的地址是连续的,不需要对++操作符进行重载,所以他俩可以直接使用原生指针作为迭代器,不用进行封装。
为了达到++iterator的功能是移动到下一个节点处,我们需要重载++操作符,因为对于内置类型才能进行重载操作符,所以我们要将Node*封装成为一个自定类型,进而对++操作符进行重载。
先大概看一下对于操作符 * 和 ++ 的重载,后面会详细讲解:
reference operator*() const { return node->data; }self& operator++()
{ node = node->_next;return *this;
}
所以迭代器的框架可以定义为:
struct __list_iterator
{// 成员变量typedef list_node<T> node;typedef __list_iterator<T> self; // 因为__list_iterator名字较长,可以在内部重命名一下node* _node;
};
这个自定义类型也需要有一个构造函数:
__list_iterator(node* n):_node(n)
{}
-
迭代器中对操作符的重载
然后再__list_iterator自定义类型内部对各种操作符进行重载:
// 解引用操作符:返回的是node指向的值,要求可以被修改,所以返回值要使用引用
Ref& operator*()
{return _node->_data;
}// 前置++
self& operator++()
{_node = _node->_next;return *this;
}// 后置++
self operator++(int)
{self ret(*this);_node = _node->_next;return ret;
}// 前置--
self& operator--()
{_node = _node->_prev;return *this;
}// 后置--
self operator--(int)
{self ret(*this);_node = _node->_prev;return ret;
}bool operator!=(const self& s)
{ return _node != s._node;
}bool operator==(const self& s)
{return _node == s._node;
}
-
const迭代器的设计
使用场景:
void func(const list<int>& lt)
{list<int>::iterator it = lt.begin();while (it != lt.end()){(*it) *= 2;cout << *it << " ";++it;}cout << endl;
}
会出现以下错误:
原因是因为,lt是一个const的对象,则会用一个const的对象调用begin和end,而实现的begin()和end()都是普通对象才能够调用的,所以,我们还要重载一个const对象使用的begin和end函数。
iterator begin() const
{return iterator(_head->_next);
}
但是,如果只是使用const修饰begin()和end(),会出现这样的问题:我们使用const对象作为参数,主要是不想更改对象的内容的,但是现在,这段代码还是可以改变该对象的内容的。
这是因为,这个begin()返回的迭代器(_head->_next)是一个普通迭代器,并没有使用const进行修饰,所以可以修改迭代器指向的内容。
分析:
需要注意的是,虽然begin()使用const进行修饰,也能创建出一个普通的迭代器(非const):const修饰的是 list<int>* this 即 const *this,所以const修饰的是this指针指向的_head,所以_head的常性的,同时_head也是一个指针,用const修饰代表着_head的指向不能再改变了,但是_head指向的内容是可以改变的,即_head->_next是可以改变的,非const修饰的,所以由_head->_next创建出来的迭代器也是非const修饰的。
所以还要让这两个函数的返回值设置成一个const迭代器,要求不能通过这个const迭代器修改其指向的值。
错误结构:
不能直接在普通迭代器前面加上一个const成为一个const迭代器,这种写法是保证迭代器本身不能被修改,我们想要实现的效果是迭代器指向的内容不能被修改
注意:这里的typedef进行重命名时不是进行简单的替换,typedef const iterator const_iterator;并不等于 typedef const node* const_iterator,而应该是 typedef node* const const_iterator
所以const修饰的是node的指针,并不是修饰node指针指向的内容。例如:
int main()
{int a = 2;int c = 1;typedef int* PA;const PA b = &a;*b += 2;printf("%d", a);return 0;
}
我们发现可以通过b修改 a 的值,从a = 2,变成a = 4。并且:
int main()
{int a = 2;int c = 1;typedef int* PA;const PA b = &a;b = &c;return 0;
}
b的指向不能被改变。
所以这样进行定义是不行的:const修饰的是b指针,而不是b指向的内容。
正确结构
我们可以对按照普通迭代器一样,再创建一个const类型的迭代器。
const迭代器和普通迭代器的之间的实现,对于其他操作符的实现不用改变(如++、==),需要改变的就是 * 操作符的重载,const迭代器要求迭代器指向的值不能被修改,因此只需要在实现 * 操作符的重载时,将返回值设置成为常量,这样就完成了。
template<class T>
struct __list_const_iterator
{// 成员变量typedef list_node<T> node;typedef __list_const_iterator<T> self;node* _node;// 函数、运算符__list_const_iterator(node* n):_node(n){}// 解引用操作符:返回的是node指向的值,要求不可以被修改,所以返回值要使用常量引用const T& operator*(){return _node->_data;}// 前置++self& operator++(){_node = _node->_next;return *this;}// 后置++self operator++(int){self ret(*this);_node = _node->_next;return ret;}// 前置--self& operator--(){_node = _node->_prev;return *this;}// 后置--self operator--(int){self ret(*this);_node = _node->_prev;return ret;}bool operator!=(const self& s){return _node != s._node;}bool operator==(const self& s){return _node == s._node;}
};
但是,我们发现,这两个迭代器只有解引用操作符的返回值类型不同,其余的函数和变量都相同,代码比较冗余。
我们可以再使用一个模板参数进行改进,当我们在创建一个迭代器时可以将迭代器的类型作为一个参数进行创建。
template<class T, class Ref>
struct __list_iterator
{// 成员变量typedef list_node<T> node;typedef __list_iterator<T, Ref> self;node* _node;// 函数、运算符__list_iterator(node* n):_node(n){}// 解引用操作符:返回的是node指向的值,要求可以被修改,所以返回值要使用引用Ref operator*(){return _node->_data;}};typedef __list_iterator<T, T&> iterator;
typedef __list_iterator<T, const T&> const_iterator;
这样就可以使用一个模板完成两个类才能完成的工作。
const对象调用const类型的begin(),begin()函数返回一个const类型的迭代器(注意:const类型的迭代器不是const对象,其本身是可以修改的,是其指向的内容不能被修改),这样设计就能完成前面的使用场景了。
-
-> 操作符的重载
使用场景:
struct AA
{int _a1;int _a2;AA(int a1, int a2):_a1(a1), _a2(a2){}
}
<<操作符右操作数为AA类型的一个对象(lt指向list第一个对象也就是AA,所以lt就是AA类型的一个指针),由于没有对<<进行重载,所以不能进行打印。
所以解决办法要么我们对<<操作符进行重载;
要么用这种方法进行使用,但用起来不方便,在C语言中,我们可以通过->操作符访问一个自定义类型指针的成员变量,所以我们也可以对->操作符进行重载。
T* operator->()
{return &_node->data;
}
可以这样使用:
使用的时候可以这样使用,但是通过分析好像有点不妥:
我们发现 it.operator->() 得到的只是AA*,并没有得到_a1,如果想要得到_a1,还要在此基础上再加一个->才能得到_a1,即 (it.operator->())->_a1,it->->_a1.
但事实上我们可以只用一个->就可以访问到_a1,其实是编译器进行了优化,省略了一个箭头。
同时为保证重载的operator->() 也有const的版本,也会像上面,那样再添加一个模板参数,用来实例化operator->() 不同的迭代器。
template<class T, class Ref, class Ptr>
struct __list_iterator
{// 成员变量typedef list_node<T> node;typedef __list_iterator<T, Ref, Ptr> self;node* _node;Ptr operator->(){return &_node->data;}
};typedef __list_iterator<T, T&, T*> iterator;
typedef __list_iterator<T, const T&, const T*> const_iterator;
2、list相关函数实现
1)Modifiers
Ⅰ. insert
这里的insert的操作就是在一个带头双向循环链表中插入一个节点:
void insert(iterator pos, const T& x)
{node* cur = pos._node;node* prev = cur->_prev;node* new_node = new node(x);prev->_next = new_node;new_node->_prev = prev;new_node->_next = cur;cur->_prev = new_node;
}
同时也要注意迭代器失效的问题,这里需要设置一个返回值,用来更新外部迭代器的位置。
iterator insert(iterator pos, const T& x)
{node* cur = pos._node;node* prev = cur->_prev;node* new_node = new node(x);prev->_next = new_node;new_node->_prev = prev;new_node->_next = cur;cur->_prev = new_node;return new_node;
}
与之逻辑相配的函数是push_back()和push_front(),因此这两个函数的实现可以对insert进行复用:
void push_back(const T& x)
{insert(end(), x);
}void push_front(const T& x)
{insert(begin(), x);
}
Ⅱ. erase
iterator erase(iterator pos)
{assert(pos != end());node* prev = pos._node->_prev;node* next = pos._node->_next;prev->_next = next;next->_prev = prev;delete pos._node;return iterator(next);
}
注意这里的迭代器失效问题,会导致野指针的问题,所以同样设置一个返回值。
与之逻辑相配的函数是pop_back()和pop_front(),因此这两个函数的实现可以对erase进行复用:
void pop_back()
{erase(--end());
}void pop_front()
{erase(begin());
}
2)默认成员函数
Ⅰ. destructor
在模拟实现析构函数前,先介绍一下clear()函数。
- clear()
Removes all elements from the list container (which are destroyed), and leaving the container with a size of 0.--移除所有元素,不包括头节点。
这里为了防止迭代器失效的问题,可以使用erase的返回值(返回删除当前节点的下一个)。
// 不清除头节点
void clear()
{iterator it = begin();while (it != end()){it = erase(it);}
}
也可以使用下面这种写法。
// 不清除头节点
void clear()
{iterator it = begin();while (it != end()){erase(it++);}
}
注意这里并不是通过 it 进行删除的,而是使用 it++ 返回的一个拷贝值进行删除。
- 析构函数
可以对clear()函数进行复用,并删除头节点即可。
~list()
{clear();delete _head;_head = nullptr;
}
Ⅱ. constructor
- 构造函数
// 构造函数
list()
{_head = new node;_head->_next = _head;_head->_prev = _head;
}
这里手动创建了一个头节点。
当然也可以使用迭代器进行构造一个对象,这里可以复用push_back()函数,但是在使用push_back之前,需要有一个头节点。因为后面也要使用到初始化list,让list有一个头节点,所以可以将这个步骤写成一个函数empty_init():
void empty_init()
{_head = new node;_head->_next = _head;_head->_prev = _head;
}
// 迭代器区间的初始化
template<class Iterator>
list(Iterator first, Iterator last)
{// 使用push_back之前需要有头节点empty_init();while (first != last){push_back(*first);++first;}
}
- 拷贝构造函数
第一种方法:根据右边的节点,一个一个创建出节点值相同的list
void empty_init()
{_head = new node;_head->_next = _head;_head->_prev = _head;
}// 拷贝构造函数(传统写法)
list(const list<T>& lt)
{empty_init();for (auto e : lt){push_back(e);}
}
第二种:使用迭代器区间的构造函数构造一个对象,再交换两者的指针
void empty_init()
{_head = new node;_head->_next = _head;_head->_prev = _head;
}void swap(list<T>& temp)
{std::swap(_head, temp._head);
}
// 拷贝构造函数(使用swap交换头指针)
list(const list<T>& lt)
{empty_init();// 使用迭代器区间构建一个对象list<T> temp(lt.begin(), lt.end());// 交换指针swap(temp);
}
- operator=
第一种,传统写法:
// 传统写法
list<T>& operator=(const list<T>& lt)
{if (this != <){clear(); // 删除被赋值list中的值for (auto& e : lt) // 插入值{push_back(e);}}return *this;
}
第二种写法:
list<T>& operator=(list<T> lt)
{swap(lt);return *this;
}
注意:不能将对象的引用作为参数。要与被拷贝对象的一个拷贝进行交换(传值调用了拷贝构造函数),如果使用引用会导致右边被赋值的对象被改变。
3)iterator
typedef __list_iterator<T, T&, T*> iterator;
typedef __list_iterator<T, const T&, const T*> const_iterator;
Ⅰ. begin()
这里的细节在前面已经讲过了。
iterator begin()
{return iterator(_head->_next);
}const_iterator begin() const
{return const_iterator(_head->_next);
}
Ⅱ. end()
iterator end()
{return iterator(_head);
}const_iterator end() const
{return const_iterator(_head);
}
3、反向迭代器
1)反向迭代器的介绍
反向迭代器是一种迭代器,它可以在容器中从后向前遍历元素。与正向迭代器不同的是,反向迭代器的++操作符是让迭代器指向前一个元素,--操作符是让迭代器指向后一个元素。反向迭代器通常用于需要从后往前遍历容器的算法和场景中。
反向迭代器和正向迭代器在使用上有相似之处,它们都可以通过解引用运算符来访问当前迭代器指向的元素,也可以通过比较运算符来判断迭代器之间的大小关系。但是,它们之间也存在一些区别和联系。
区别:
-
步进方向不同:正向迭代器的步进方向是从前向后,而反向迭代器的步进方向是从后向前。
-
支持的操作不同:由于反向迭代器的步进方向不同,因此它们不支持正向迭代器的某些操作,例如递增和递减操作等。
-
消耗的空间不同:反向迭代器需要保存一个正向迭代器来实现其功能,因此它们通常会消耗更多的空间。
联系:
-
都是迭代器:反向迭代器和正向迭代器都是STL迭代器的一种,都具有迭代器的基本特性。
-
具有相同的接口:反向迭代器和正向迭代器都具有相同的解引用和比较运算符等接口。
-
可以互相转换:由于反向迭代器是基于正向迭代器实现的,因此它们可以通过std::reverse_iterator构造函数进行互相转换。
2)反向迭代器的模拟实现
反向迭代器的++就是正向迭代器的-,反向迭代器的一就是正向迭代器的++,因此反向迭
代器的实现可以借助正向迭代器。
如果直接在正向迭代器的基础上更改个别操作符的重载方法,虽然能够实现反向迭代器的功能,但是方法比较笨拙。
标准库中对反向迭代器是通过适配器适配正向迭代器来实现的。
所以反向迭代器的底层就是正向迭代器:
template<class Iterator> // 使用一个正向迭代器适配出一个反向迭代器
struct Reverse_Iterator
{Iterator cur; // 底层是一个正向迭代器// 构造函数Reverse_Iterator(Iterator it):cur(it){}
};
// 正向迭代器
typedef __list_iterator<T, T&, T*> iterator;
typedef __list_iterator<T, const T&, const T*> const_iterator;// 反向迭代器
typedef Reverse_Iterator<iterator, T&, T*> reverse_iterator;
typedef Reverse_Iterator<iterator, const T&, const T*> const_reverse_iterator;
有一些标准库中有以下规定(并不是所有的库都是这样实现的):
因此,在链表中rbegin()和rend()的模拟实现如下:
// 反向迭代器
reverse_iterator rbegin()
{return iterator(end());
}reverse_iterator rend()
{return iterator(begin());
}const_reverse_iterator rbegin() const
{return const_reverse_iterator(end());
}const_reverse_iterator rend() const
{return const_reverse_iterator(begin());
}
-
反向迭代器对操作符的重载
反向迭代器的++操作符是让迭代器指向前一个元素,所以反向迭代器对++操作符的重载可以复用正向迭代器的 --操作符,同时加上前面对正向迭代器关于const问题的处理,最终重载的结果为:
template<class Iterator, class Ref, class Ptr> // 使用一个正向迭代器适配出一个反向迭代器
struct Reverse_Iterator
{typedef Reverse_Iterator<Iterator, Ref, Ptr> Self;Iterator cur; // 底层是一个正向迭代器// 构造函数Reverse_Iterator(Iterator it):cur(it){}Self& operator++(){--cur; // 调用正向迭代器的--操作符return *this;}Self operator++(int){Iterator tmp = *this;--cur; // 调用正向迭代器的--操作符return tmp;}
};
--操作符是让迭代器指向后一个元素,所以反向迭代器对--操作符的重载可以复用正向迭代器的 ++操作符:
Self& operator--()
{++cur; // 调用正向迭代器的++操作符return *this;
}Self operator--(int)
{Iterator tmp = *this;++cur; // 调用正向迭代器的++操作符return tmp;
}
== 和 != 的重载
bool operator!=(const Self& it)
{return cur != it.cur; // 调用正向迭代器的!=操作符
}bool operator==(const Self& it)
{return cur == it.cur; // 调用正向迭代器的==操作符
}
* 和 -> 的重载
因为我们选择让 实现的反向迭代器和正向迭代器的begin和end具有一定的对称性 ,所以这里对 * 操作符的重载并不能直接对正向迭代器的 * 操作符进行复用。
因为 rbegin 指向 end 的位置,直接使用正向迭代器的 * 操作符会出现错误,所以反向迭代器对 * 的重载应该返回 rbegin 前一个的元素:
Ref operator*()
{Iterator tmp = cur;--tmp;return *tmp; // 调用正向迭代器的解引用操作符
}
在前面正向迭代器部分,我们知道->操作符返回的是迭代器指向元素的指针,所以:
Ptr operator->()
{return &(operator*());
}
首先,让我们来解读 operator*() 函数的实现:
- 在函数内部创建一个临时的迭代器对象 temp,并将其初始化为当前迭代器对象 _it。
- 对 temp 进行递减操作符 -- 的操作,将其指向前一个元素。
- 返回 temp 所指向的元素对象 *temp。
这段代码的作用是返回当前迭代器所指向的元素。通过创建一个临时的迭代器对象并将其指向前一个元素,然后返回该元素,实现了迭代器的解引用操作。
接下来,我们来看 operator->() 函数的实现:
- 在函数内部调用 operator*() 函数,获取当前迭代器所指向的元素。
- 使用取地址符 & 取得该元素的指针,并返回该指针。
这段代码的作用是返回一个指向当前迭代器所指向元素的指针。通过调用 operator*() 函数获取元素的引用,然后使用取地址符 & 获取该元素的指针,实现了迭代器的箭头操作符。
注意:
因为这里的迭代器使用了一个模板,所以不仅可以实现list的反向迭代器,也可以通过其他容器的正向迭代器适配出它的反向迭代器。
今天的分享就到这里了,如果,你感觉这篇博客对你有帮助的话,就点个赞吧!感谢感谢……