C++ - Introduction to list and simulation implementation of list

list introduction

 list is a sequential container that supports insertion and deletion at any position within a constant range, and this container can be iterated back and forth. We can understand the list as the structure of a two-way circular linked list.

Compared with other structured containers, the efficiency of list insertion and function at any position is much higher; and the disadvantage of list is also obvious, when it randomly accesses the data in the container, it can only start linearly from a known position Search, this search is time-consuming compared to other containers; and in terms of storage, because each node is stored separately, more space will be opened to store the relationship between each node. Consumption is also higher.

 use of lists

 Constructor

Constructor ( (constructor)) Interface Description
list (size_type n, const value_type& val = value_type()) The constructed list contains n elements whose value is val
list() Construct an empty list
list (const list& x) copy constructor
list (InputIterator first, InputIterator last) Constructs a list with elements in the range [first, last)

The constructor of list is similar to string and vector. For details, please refer to the following blog:

C++ string class iterator range for_c++string iterator_chihiro1122's blog-CSDN blog

 iterator

 The iterator of the list is no longer  simply implemented using raw pointers  like string and vector, but is packaged with classes and objects, so that pointers cannot implement ++ and other operations in the list, and are implemented with operator overloading functions (For the implementation of specific iterators, please refer to the simulation implementation of list).

function declaration Interface Description
begin +
end
Returns an iterator to the first element + returns an iterator to the next position of the last element
rbegin +
rend

Return the reverse_iterator of the first element, which is the end position, and return the reverse_iterator of the next position of the last element , which is the begin position

 Note :

  • Because the storage structure of the list is different from that of the string, the storage structure of the string is a continuous interval, so the iterator of the string class supports operations like str.begin() + 5; but because the list It is a discontinuous space, and the cost of the operator "+" is higher than that of string, so the function operator+ is not implemented in the iterator of the list! !
  • For the use of iterators, the form of "<" cannot be used to judge the range of iterators! ! Because the storage space of each node of the list is not continuous, if you directly use "<" to compare, the address size of the pointer storage will be compared, which will cause a big problem. The use of iterators is generally like this:
list<int> L( 10 , 1 );
list::iterator it = L.begin();

while(it != L.end())
{
    cout << *it << " ";
    ++it;
}
cout << endl;

 Other basic operations

 In STL, these functions are basically used in the same way. You can refer to the blog that introduced string and vector before:

C++ string class iterator range for_c++string iterator_chihiro1122's blog-CSDN blog

C++ string class-2_chihiro1122's blog-CSDN blog

C++ - First knowledge of vector class iterator invalidation - chihiro1122's blog - CSDN blog

list capacity

function declaration Interface Description
empty Check whether the list is empty, return true, otherwise return false
size Returns the number of valid nodes in the list

list element access

function declaration Interface Description
front Returns a reference to the value in the first node of the list
back Returns a reference to the value in the last node of the list

 list modifiers

function declaration Interface Description
push_front Insert an element with value val before the first element of the list
pop_front delete the first element in the list
push_back Insert an element with value val at the end of the list
pop_back delete the last element in the list
insert Insert an element with value val in list position
erase Delete the element at list position
swap Swap the elements of two lists
clear Clear the valid elements in the list

Iterators in list are invalidated

 The insert () function in the list has no iterator invalidation, because the iterator of the insert () function that appeared in the vector before is invalid, because the vector is a continuous space that needs to be expanded, and the expansion will cause the iterator to fail ; But every time data is inserted into the list, a new space needs to be created, and it will not affect the existing elements in the list.

However, for deleted functions, such as the erase() function, there is still the problem of storing iterator invalidation:

int array[] = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 0 };
list<int> l(array, array+sizeof(array)/sizeof(array[0]));
auto it = l.begin();
while (it != l.end())
{
// erase()函数执行后,it所指向的节点已被删除,因此it无效,在下一次使用it时,必须先给
其赋值
l.erase(it);
++it;

As in the above example, it is an external iterator failure problem. When the space pointed to by it is deleted, the space pointed to by it is also released, so the overloaded operator function ++it in the while statement cannot find the next up.

amendment;

while (it != l.end())
{
l.erase(it++); // it = l.erase(it);
}

 Iterator understanding in STL

 Let's first look at the different iterator types of the following three functions:

 

 The above has three iterator types:

  •  InputIterator: One-way iterator, only ++ operation can be used.
  •  BidirectionalIterator: Bidirectional iterator, you can use ++ / -- two operations.
  • RandomAccessIterator: Random iterator, can use ++ / -- / + / - four operations.

 Different container types have requirements for the use of iterators:

 For example, a single-linked list can only use one-way iterators, and neither two-way nor random can be used. Then, for functions implemented by two-way and random iterators in the library, single-linked lists cannot be used either.

But these three iterators are upward compatible, that is to say, random is a special case of two-way, so the function of two-way iterator can be used by using the container of random iterator; similarly, two-way is a special case of one-way , two-way, one-way can be used.

 The mock implementation of list

 General framework

#pragma once

namespace My_List
{
	template<class T>
	struct List_Node  // 结点指针结构体
	{
·············
	};

    template<class T>
	struct List_iterator // 非const 迭代器
	{
··········
    };

    template<class T>
	class List         // List 类
	{
············
        private:
		Node* _head;

	};
}

 Node structure definition 

 In the official List source code, the node definition of the List container is defined in a structure under the same namespace. Because the structure has been upgraded to a class in C++, the constructor can also be defined in the structure.

Therefore, because there are numerical fields and pointer fields in the node, we implement this structure as a function to construct the node, and the effect is the same, but when using it, use new to open space and definition:

	template<class T>
	struct List_Node
	{
		List_Node<T>* _next;
		List_Node<T>* _prev;
		T _val;

		// List_Node 的构造函数
		List_Node(const T& val = T())
			:_next(nullptr),
			_prev(nullptr),
			_val(val)
		{}
	};

Of course, for the reusability of the numerical field, templates are used to template the type of the numerical field.

 constructor and destructor

 parameterless constructor

		List()
		{
			_head = new Node;
			_head->_next = _head;
			_head->_prev = _head;
		}

Because the bottom layer of the List is a circular doubly-linked list with the head, there is only one head node for linking without nodes as above.

 destructor

		~List()
		{
			clear();
			delete _head;
			_head = nullptr;
		}

 The clear() function can be reused directly here.

CRUD

 push_back():

		void push_back(const T& x)
		{
			Node* tail = _head->_prev;
			Node* newNode = new Node(x);

			_head->_prev = newNode;
			tail->_next = newNode;

			newNode->_prev = tail;
			newNode->_next = _head;

            // 实现insert()函数之后
            // insert(end() , x);
		}

Open the space directly and modify the link relationship.

 insert() function:

The pos pointer passed in the insert() function here does not need to be checked for validity, because this is a linked list with a loop of the head node, and it is possible to delete before and after the head node. code:

		iterator insert(iterator pos, const T& x)
		{
			Node* cur = pos._Node;
			Node* prev = cur->_prev;
			Node* newNode = new Node(x);

			prev->_next = newNode;
			newNode->_next = cur:

			cur->_prev = newNode;
			newNode->_prev = prev;

			return newNode;
		}

Because after the element is inserted before the pos position, the pos iterator moves backwards. We think this is also an iterator failure, so we need to return the position of the newly inserted element to prevent the external iterator from becoming invalid. 

erase() function

 The erase() function also has the problem of iterator invalidation, so it needs to return a new iterator:

		iterator erase(iterator pos)
		{
			assert(pos != end());

			Node* cur = pos._Node;
			Node* prev = cur->_prev;
			Node* next = cur->_next;

			prev->_next = next;
			next->_prev = prev;

			delete[] cur; // 这里会有 pos 迭代器失效的问题,所要要返回新的迭代器

			return next;
		}

 push_front():

 Direct reuse of insert() function

		void push_front(const T& x)
		{
			insert(begin(), x);
		}

 pop_back() and pop_front(): 

		void pop_back()
		{
			erase(--end());
		}

		void pop_front()
		{
			erase(begin());
		}

 clear( )和 size():

		size_t size()
		{
			return _size;
		}

		void clear()
		{
			iterator it = begin();
			while (it != end())
			{
				it = erase(it);
			}
			_size = 0;
		}

 Assignment operator overloading function and swap() function

		void swap(List<T>& L)
		{
			std::swap(_head, L._head);
			std::swap(_head, L._size);
		}

		List<T>& operator(List<T> L)
		{
			swap(L);
			return *this;
		}

 The above principle refers to the article: (1 message) The simulation implementation of the C++-string class_chihiro1122's blog - the introduction of the assignment operator overloading function (under the title of comparison size) in the CSDN blog.

In fact, the type name of the template class in the above-mentioned assignment overload operator function does not need to be List<T>. We know that List<T> is the type name, and List is the class name. For the template class, the type name is not List, but List <T>, but if it is written in a class template, you can write the class name or the type name (the following writing methods are also available):

List& operator=(List L)

iterator (important)

 non-const iterator

 When introducing the List iterator above, it was also introduced that the iterator in the List is not a native pointer, but a custom type, so it is a bit difficult to define, but the advantage of this is that it is the same as an ordinary native pointer iterator. Direct ++ moves back, * dereferences to access elements, etc., because there are operator overloading functions in the custom type, so it can be realized.

 In fact, the iterator of the List is still a pointer in essence, but the pointer now points to a node space, which contains not only the data field, but also the pointer field, so the direct dereference cannot access the data field. At this time It is necessary to overload the " * " (dereference) operator, and the implementation is also very simple, just return the data field of the node directly:

		T& operator* ()
		{
			return _Node->_val;
		}

 Let's think again, which operators we will use when using iterators, as follows:
 

	while (it != L.end())
	{
		cout << *it << " ";
		++it; // 因为只重载了 前置的++
	}
	cout << endl;

 We need to overload the operations used above, then this iterator can be used normally:

		List_iterator<T>& operator++ ()
		{
			_Node = _Node->_next;
			return *this;
		}

		bool operator!= (const List_iterator<T>& L)
		{
			return _Node != L._Node;
		}

		bool operator== (const List_iterator<T>& L)
		{
			return _Node == L._Node;
		}

 The last is the construction of the entire iterator structure. As mentioned above, C++ upgrades the structure to a class, so the constructor can be used to construct the iterator object.

First of all, this iterator has only one member, which is a pointer to a certain node, so the constructor of this structure only needs to initialize this object, and only needs to pass in the value of this node when constructing A pointer will do:
 

		typedef List_Node<T> Node;
		Node* _Node;

		List_iterator(Node* node)
			:_Node(node)
		{}

 Then the entire non-const iterator is implemented, as follows:

	template<class T>
	struct List_iterator
	{
		typedef List_Node<T> Node;
		Node* _Node;

		List_iterator(Node* node)
			:_Node(node)
		{}

		T& operator* ()
		{
			return _Node->_val;
		}

		List_iterator<T>& operator++ ()
		{
			_Node = _Node->_next;
			return *this;
		}

		bool operator!= (const List_iterator<T>& L)
		{
			return _Node != L._Node;
		}

		bool operator== (const List_iterator<T>& L)
		{
			return _Node == L._Node;
		}
	};

 Then, two interfaces of begin() and end() also need to be given in the class, begin() points to the last one of the head node _head, and end points to _head:

		typedef List_iterator<T> iterator; // 一定要是公有的,不然不能访问
		iterator begin()
		{
			//return _head->_next; // 可以这样写,只有一个参数的构造函数发生 隐式类型的转换
		    // 上下两种都是一样的
			return iterator(_head->_next);
		}

		// 返回值应该是引用,要不然 != 函数会出错 传值返回返回的不是 _head 是 _head 的一个拷贝 
       // 临时对象具有常性  ······· 1
		iterator end()
		{
			return _head; // 同样发生隐式类型的转换
			// 上下两种都可以
			//return iterator(_head);
		}

 Need to pay attention to several issues: 

  •  The return value type mentioned in the above code comment should be a reference question (1), in fact, there is no need to do this. The above begin() and end() functions are used in the operator!= function and constructor, so you only need Just change the parameter of the operator!= function to const , and the above changes have been made.
  • My_List::List<int>::iterator it = L.begin(); This is not assignment, but copy construction, because L.begin() is an existing object assignment to another object needs to call the copy constructor , but the iterator does not implement the copy structure. The shallow copy implemented by the compiler here is no problem here, because what we need here is a shallow copy.
  • My_List::List<int>::iterator it = L.begin(); The two pointers here both point to an object, so why didn't the compiler report an error? This is because the iterator class does not implement a destructor, the compiler will call the default destructor to release the iterator pointer space, and the node space pointed to by the iterator does not need the iterator class to release, in List Released in the destructor of the class.

const iterator

 The const iterator here is aimed at the const object. If it is a const modified object, no ordinary iterator is used, because when the const iterator (pointer) of the const object is returned to the non-const iterator constructor, the const has become non-const, resulting in the amplification of permissions.

Therefore, it is still necessary to implement const iterators separately. The function of const iterators is similar to ordinary iterators, except that what is returned in the operator* function is not a non-const object, but a const object:

	template<class T>
	struct const_List_iterator
	{
		typedef List_Node<T> Node;
		Node* _Node;

		const_List_iterator(Node* node)
			:_Node(node)
		{}

		const T& operator* ()
		{
			return _Node->_val;
		}

		const_List_iterator<T>& operator++ ()
		{
			_Node = _Node->_next;
			return *this;
		}

		bool operator!= (const const_List_iterator<T>& L)
		{
			return _Node != L._Node;
		}

		bool operator== (const const_List_iterator<T>& L)
		{
			return _Node == L._Node;
		}
	};



		const_iterator begin() const
		{
			return _head->_next;
		}

		const_iterator end() const
		{
			return const_iterator(_head);
		}

Although this can achieve the effect of a const iterator, it is too redundant and not very good.

So at this time we have the use of multiple template parameters, as shown below, which are templates for ordinary iterator classes:
 

	template<class T, class Ref>
	struct List_iterator
	{

······················

At this point, the typedef dumb drum in the class is written like this:
 

		typedef List_iterator<T , T&> iterator; // 一定要是公有的,不然不能访问
		typedef List_iterator<T , const T&> const_iterator;

In this way, the const class and the non-const class can be distinguished in the same class, so the modification of the opeartor* function can be like this;

		Ref operator* ()
		{
			return _Node->_val;
		}

Similarly, different places in the class must be modified. The complete code is as follows:

template<class T, class Ref>
	struct List_iterator
	{
		typedef List_Node<T> Node;
		typedef List_iterator<T,Ref> selt;
		Node* _Node;

		List_iterator(Node* node)
			:_Node(node)
		{}

		Ref operator* ()
		{
			return _Node->_val;
		}

		selt& operator++ ()
		{
			_Node = _Node->_next;
			return *this;
		}

		selt operator++ (int)
		{
			_Node = _Node->_next;
			return *this;
		}

		bool operator!= (const selt& L)
		{
			return _Node != L._Node;
		}

		bool operator== (const selt& L)
		{
			return _Node == L._Node;
		}
	};
	template<class T>
	class List
	{
		typedef List_Node<T> Node;

	public:
		typedef List_iterator<T , T&> iterator; // 一定要是公有的,不然不能访问
		typedef List_iterator<T , const T&> const_iterator;

·········································

In this case, it seems that one class is written, but two classes are actually written, but the size of the code is saved, which is the benefit of the template.

In iterators, the " -> " operator is also used, so this operator also needs to be overloaded:

		T* operator-> ()
		{
			return &_Node->_val;
		}

However, when using it, the following scene is a bit strange, as shown below:

struct A
	{
		A(int a1 = 0, int a2 = 0)
			:_a1(a1)
			, _a2(a2)
		{}

		int _a1;
		int _a2;
	};

	void test_list2()
	{
		list<A> lt;
		lt.push_back(A(1, 1));
		lt.push_back(A(2, 2));
		lt.push_back(A(3, 3));
		lt.push_back(A(4, 4));

		list<A>::iterator it = lt.begin();
		while (it != lt.end())
		{
			//cout << (*it)._a1 << " " << (*it)._a2 << endl;
			cout << it->_a1 << " " << it->_a2 << endl;

			++it;
		}
		cout << endl;
	}

 The it usage in the above cout stream should actually be written like this:

it ->-> _a2

The above is the normal way of writing, but in the use of operator-> function, it is used directly as it -> _a2. This is because operator overloading requires readability, and the compiler performs special processing in this place, omitting a " -> ".

 If the above operator-> function is implemented in a const iterator, the return value should be const T*, so here, add a ptr to the parameter of the iterator class template to use :

Code for the final iterator:

	template<class T, class Ref , class ptr>
	struct List_iterator
	{
		typedef List_Node<T> Node;
		typedef List_iterator<T,Ref, ptr> selt;
		Node* _Node;

		List_iterator(Node* node)
			:_Node(node)
		{}

		Ref operator* ()
		{
			return _Node->_val;
		}

		selt& operator++ ()
		{
			_Node = _Node->_next;
			return *this;
		}

		selt operator++ (int)
		{
			_Node = _Node->_next;
			return *this;
		}

		selt& operator-- ()
		{
			_Node = _Node->_prev;
			return *this;
		}

		selt operator-- (int)
		{
			_Node = _Node->_prev;
			return *this;
		}

		bool operator!= (const selt& L)
		{
			return _Node != L._Node;
		}

		bool operator== (const selt& L)
		{
			return _Node == L._Node;
		}

		ptr operator-> ()
		{
			return &_Node->_val;
		}
	};

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;

·······················································

reverse iterator for list


From the previous example, we know that the ++ of the reverse iterator is the -- of the forward iterator, and the -- of the reverse iterator is the ++ of the forward iterator, so the realization of the reverse iterator can be realized by means of the forward iterator , that is: the reverse iterator can contain a forward iterator, and the interface of the forward iterator can be packaged.
 

template<class Iterator>
class ReverseListIterator
{
	// 注意:此处typename的作用是明确告诉编译器,Ref是Iterator类中的类型,而不是静态成员变量
	// 否则编译器编译时就不知道Ref是Iterator中的类型还是静态成员变量
	// 因为静态成员变量也是按照 类名::静态成员变量名 的方式访问的
public:
	typedef typename Iterator::Ref Ref;
	typedef typename Iterator::Ptr Ptr;
	typedef ReverseListIterator<Iterator> Self;
public:
	//
	// 构造
	ReverseListIterator(Iterator it) : _it(it) {}
	//
	// 具有指针类似行为
	Ref operator*() {
		Iterator temp(_it);
		--temp;
		return *temp;
	}
	Ptr operator->() { return &(operator*()); }
	//
	// 迭代器支持移动
	Self& operator++() {
        --_it;
        return *this;
    }
    Self operator++(int) {
    	Self temp(*this);
    	--_it;
    	return temp;
    }
        Self& operator--() {
    	++_it;
    	return *this;
    }
    Self operator--(int)
    {
	    Self temp(*this);
	    ++_it;
	    return temp;
    }
//
// 迭代器支持比较
bool operator!=(const Self& l)const { return _it != l._it; }
bool operator==(const Self& l)const { return _it != l._it; }
Iterator _it;
};

Comparison of list and vector

 list is a linked list, and vector is a sequential list. The structure of the two is different, resulting in different usage scenarios. The two are also typical manifestations of different characteristics of continuous space storage and chained space storage. The following table is a simple summary of the two Compare:

vector         list
underlying structure A dynamic sequence table is a continuous space Doubly linked circular list with head node
random access Support random access, the efficiency of accessing an element is O(1) Random access is not supported, and
the efficiency of accessing an element is O(N)
insert and delete Insertion and deletion at any position is inefficient, and elements need to be moved. The time complexity
is O(N). When inserting, it may need to increase capacity. Increase: open up new space
, copy elements, and release old space, resulting in lower efficiency
Insertion and deletion at any position are highly efficient, do not
need to move elements, and the time complexity is
O(1)
Space utilization The bottom layer is a continuous space, which is not easy to cause memory fragmentation,
high space utilization rate, and high cache utilization rate
The underlying nodes are dynamically opened, and small nodes are likely
to cause memory fragmentation, low space utilization, and
low cache utilization
iterator Original ecological pointer Encapsulate the original ecological pointer (node ​​pointer)
iterator invalidation When inserting elements, all iterators must be reassigned, because inserting
elements may cause re-expansion, causing the original iterator to become invalid.
When deleting, the current iterator needs to be reassigned or it will become invalid
Inserting an element will not invalidate the iterator.
When deleting an element, only the current
iterator will be invalidated, and other iterators will not be affected
scenes to be used Requires efficient storage, supports random access, and does not care about insertion and deletion efficiency Lots of insert and delete operations, don't care about random
access

Guess you like

Origin blog.csdn.net/chihiro1122/article/details/131899659