[C++] STL understanding [container]

[C++] STL understanding [container]

1. Introduction of STL concept

For a long time, the software industry has been hoping to establish a reusable thing, and a method to create "reusable things", from functions, classes, function libraries, Class libraries and various components, from modular design to object oriented, are designed to improve reusability.

Reusability must be based on some kind of standard. But in many environments, even the most basic data structures and algorithms of software development fail to have a set of standards. A large number of programmers are forced to do a lot of repetitive work in order to complete the program code that their predecessors have completed but they do not own. This is not only a waste of human resources, but also a source of frustration and pain.

In order to establish a set of standards for data structures and algorithms, and reduce the coupling relationship between them, so as to improve their independence, flexibility, and interoperability (interoperability), STL was born.

STL (Standard Template Library, Standard Template Library) is a collective name for a series of software developed by Hewlett-Packard Labs. It's mostly found in C++ now, but the technique was around long before it was introduced to C++.

STL is broadly divided into: container (container) algorithm (algorithm) iterator (iterator).

There is a seamless connection between the container and the algorithm through an iterator. Almost all STL code uses template classes or template functions, which provides better code reuse opportunities than traditional libraries composed of functions and classes.

STL (Standard Template Library) standard template library, which belongs to STL in our c++ standard library accounts for more than 80%.

1.1 Six major components of STL

Since there is a good wish to establish a set of standards for data structures and algorithms, there must be tools to support the system. In STL, these tools are STL components——

Containers , algorithms , iterators , functors , adapters (adapters) , spatial configurators


Introduce respectively:

Container : Various data structures, such as vector, list, deque, set, map, etc., are used to store data. From the perspective of implementation, STL container is a class template, that is, a category template, which can be simply understood as templating, tools Simplify the learned data structures such as stacks and queues

Algorithms : Various commonly used algorithms, such as sort, find, copy, for_each. From the perspective of implementation, the STL algorithm is a function template, that is, a function template, which embodies the thinking mode of problem parameterization

Parameterization:
Make specific data into parameters to solve problems with different data.
Make specific data types into parameters to solve problems of different data types.

Iterator : It acts as the glue between the container and the algorithm. There are five types in total. From the perspective of implementation, the iterator is a kind of operator*, operator->, operator++, operator– and other pointer-related operations. class template. All STL containers come with their own iterators, and only the container designer knows how to traverse their elements. A native pointer is also an iterator

An iterator is a data type that can traverse the elements of a container. An iterator is a variable that acts as an intermediary between a container and the algorithms that manipulate it. C++ tends to use iterators rather than

Array subscript operation, because the standard library defines an iterator type for each standard container (such as vector, map, list, etc.), and only a few containers (such as vector) support array subscript operation to access content

device element. You can point to the address of the element you want to access the container through the iterator, and print out the element value through *x. This is very similar to the pointer we are familiar with.

Iterators and containers are inseparable and closely related. Different containers have different iterators, but their iterator functions are the same. If there is no iterator, because

Due to the different storage characteristics of different containers, you need multiple algorithms to implement the function of traversing containers, which is complex and inefficient. With iterators, the efficiency of traversing containers will be greatly improved.

Functor : Behaves like a function and can be used as some kind of strategy for an algorithm. From an implementation point of view, a functor is a class or class template that overloads operator()

adapter (adapter) : a thing used to decorate a container or a functor or iterator interface

Adapter (Adapter) mode: convert the interface of a class to another interface that the client wants. The Adapter pattern enables classes that would otherwise not work together due to incompatible interfaces to work together.

Space configurator : Responsible for space configuration and management. From an implementation point of view, the configurator is a class template that implements dynamic space configuration, space management, and space release

Since the operation objects of the entire STL are stored in the container, and the container needs to configure space to store data, so from the perspective of STL implementation, the introduction of the space configurator should be ranked first


Summarize the interaction relationship of the six major components of STL :

The container obtains the data storage space through the space configurator, the algorithm stores the content in the container through the iterator, the functor can assist the algorithm to complete different strategy changes, and the adapter can modify the functor

Due to the limited ability of the author, I can only introduce the superficial understanding at present, so I will not explain them in detail one by one.

1.2 Advantages of STL

  • STL is part of C++, it is built into your compiler without installing anything else.

  • An important feature of STL is the separation of data and operations. Data is managed by container classes and operations are defined by customizable algorithms. The iterator acts as the "glue" between the two so that the algorithm can interact with the container

  • Programmers don't need to think about the specific implementation process of STL, as long as they can use STL proficiently, it will be OK. This way they can focus on other aspects of program development.

  • STL has the advantages of high reusability, high performance, high portability, and cross-platform.

    • High reusability: Almost all codes in STL are implemented in the form of template classes and template functions, which provides better code reuse opportunities than traditional libraries composed of functions and classes.
    • High performance: For example, map can efficiently find a specified record from 100,000 records, because map is implemented using a variant of red-black tree.
    • High portability: For example, a module written in STL on project A can be directly transplanted to project B.

2. Introduction of container concept

2.1 Rough understanding of containers

In the previous study of data structures, it is not difficult for us to conclude:

In data storage, there is an object type that can hold other objects or pointers to other objects. This object type is called a container. Very simple, a container is an object that holds other objects. Of course, this is a simple understanding. This "object" also includes a series of methods for dealing with "other objects"

It can almost be said that any particular data structure is designed to implement a particular algorithm. The STL container is to realize some of the most widely used data structures.
Commonly used data structures: array (array), linked list (list), tree (tree), stack (stack), queue (queue), collection (set), mapping table (map), according to the arrangement characteristics of data in the container, These data are divided into two types: sequential containers and associative containers.

  • The sequence container emphasizes the ordering of values, and each element in the sequence container has a fixed position, unless the position is changed by a delete or insert operation. Vector container, Deque container, List container, etc.
  • Associative containers are non-linear tree structures, more precisely binary tree structures. There is no strict physical order relationship between the elements, that is to say, the elements in the container do not preserve the logical order of the elements when they are placed in the container. Another notable feature of the associative container is: select a value in the value as the keyword key, and this keyword acts as an index for the value, making it easy to find. Set/multiset container Map/multimap container

2.2 Container Classification

In C++, containers are class templates, roughly divided into sequence containers, adapter containers and associative containers

Sequential container (vector, string, deque, list)

Associative container (set (collection container)/multlist (multiple collection container)), (map (mapping container)/multimap (multiple mapping container))

Adapter container (stack (stack container)/queue (queue container)/priority_queue (priority queue container))

  • Sequential container: It is a linear table with sequential relationship between elements, and it is an orderable cluster of linear structure . Each element in a sequential container has a fixed position, unless the position is changed by deletion or insertion. The order in which elements are arranged in a sequential container has nothing to do with element values, but is determined by the order in which elements are added to the container. Sequential containers include: vector (vector), list (list), deque (queue) .
  • Associative containers: Associative containers are non-linear tree structures, more precisely binary tree structures. There is no strict physical order relationship between the elements, that is to say, the elements in the container do not preserve the logical order of the elements when they are placed in the container. But the associative container provides another function of sorting according to the characteristics of the elements, so that the iterator can obtain elements "sequentially" according to the characteristics of the elements. Elements are ordered collections, sorted in ascending order by default when inserted. Associative containers include: map (collection), set (mapping), multimap (multiple collections), multiset (multiple mapping) .
  • Container Adapter: Essentially, an adapter is a mechanism for making one different thing behave like another thing. Container adapters make an existing container type work in the same way as a different abstract type. An adapter is the interface of a container. It cannot store elements directly. Its mechanism for storing elements is to call another sequential container to implement it. That is, the adapter can be regarded as "it stores a container, and this container stores all elements". STL contains three kinds of adapters: stack stack, queue queue and priority queue priority_queue .

3. Common containers

3.1 string

3.1.1 Basic concept of string

C-style strings (null-terminated character arrays) are too complicated and difficult to grasp, and are not suitable for the development of large programs. Therefore, the C++ standard library defines a string class, which is defined in the header file.
Comparison of String and c-style strings:

  • Char is a pointer, and String is a class
    string that encapsulates char
    , and manages this string, which is a container of char* type.
  • String encapsulates many useful member methods
    to find, copy, delete, replace, insert
  • Do not consider memory release and out-of-bounds
    string to manage the memory allocated by char*. Every time a string is copied, the value is maintained by the string class, so there is no need to worry about copying out of bounds and value out of bounds.

3.1.2 string construction operation

The initialization of string objects is basically the same as the initialization of ordinary type variables, except that string is a class, and there are some characteristics of classes: initialization using constructors. As shown in the table below, Article 2 4 6 is an initialization method only available as a class

20210628120526746

It can also be initialized with:

string st1 = string("hello");
string st2(string(2,'b'));

3.1.3 string capacity operation

String-based capacity operations are as follows:

function name Function Description
size (emphasis) Returns the effective character length of the string
length Returns the effective character length of the string
capacity Return the total size of the space
empty (emphasis) If the detection string is released as an empty string, it returns true, otherwise it returns false
clear (emphasis) Clear valid characters
reserve (emphasis) reserve space for strings
resize (emphasis) Change the number of valid characters to n, and fill the extra space with the character c
void Test2()
{
	// 注意:string类对象支持直接用cin和cout进行输入和输出
	string s("hello, you!!!");
	cout << s.size() << endl;
	cout << s.length() << endl;
	cout << s.capacity() << endl;
	cout << s << endl;

	// 将s中的字符串清空,注意清空时只是将size清0,不改变底层空间的大小
	s.clear();
	cout << s.size() << endl;
	cout << s.capacity() << endl;

	// 将s中有效字符个数增加到10个,多出位置用'a'进行填充
	// “aaaaaaaaaa”
	s.resize(10, 'a');
	cout << s.size() << endl;
	cout << s.capacity() << endl;

	// 将s中有效字符个数增加到15个,多出位置用缺省值'\0'进行填充
	// "aaaaaaaaaa\0\0\0\0\0"
	// 注意此时s中有效字符个数已经增加到15个
	s.resize(15);
	cout << s.size() << endl;
	cout << s.capacity() << endl;
	cout << s << endl;

	// 将s中有效字符个数缩小到5个
	s.resize(5);
	cout << s.size() << endl;
	cout << s.capacity() << endl;
	cout << s << endl;
}

output:

image-20230408224348213

void Test3()
{
	string s;
	// 测试reserve是否会改变string中有效元素个数
	s.reserve(100);
	cout << s.size() << endl;
	cout << s.capacity() << endl;

	// 测试reserve参数小于string的底层空间大小时,是否会将空间缩小
	s.reserve(50);
	cout << s.size() << endl;
	cout << s.capacity() << endl;

    // 利用reserve提高插入数据的效率,避免增容带来的开销
}

output:

image-20230408224623915

3.1.4 string access traversal operation

function name Function Description
operator[] Returns the character at position pos, called by const string class object
begin + end begin gets an iterator for a character + end gets an iterator for the next position of the last character
rbegin + rend begin gets an iterator for a character + end gets an iterator for the next position of the last character
scope for C++11 supports a new traversal method for a more concise range for

First introduce the use of iterators

An iterator ( [iterator] ) is a data type that can iterate over the elements of a container. An iterator is a variable that acts as an intermediary between a container and the algorithms that manipulate it.

Compared with C language, C++ tends to use iterators rather than array subscript operations, because the standard library defines an iterator type for each standard container (such as vector, map, list, etc.), and only a few containers (such as vector) supports array subscript operations to access container elements. You can point to the address of the element you want to access the container through the iterator, and print out the element value through *x. This is very similar to the pointer we are familiar with.

Here we focus on access traversal operations, that is, to use iterators to traverse string class objects:

1) forward iterator
void Test4()
{
	string s1 = "hello,world";
	string::iterator it = s1.begin();
	while (it != s1.end())
	{
		cout << *it;
		++it;
	}

	cout << endl;
}

operation result:

image-20230409142920432

Note :

begin is an iterator that returns the first character of the string, and end returns an iterator pointing to the last character of the string.

end returns an iterator pointing to the next character of the character. Iterators in C++ are generally left-closed and right-open

2) reverse iterator
void Test5()
{
	string st1 = "hello,you!";

	string :: reverse_iterator it = st1.rbegin();
	while(it != st1.rend())
	{
		cout << *it << " ";
		++it;
	}

	cout << endl;
}

operation result:

image-20230409144249001

A reverse iterator outputs the opposite result of a forward iterator. In a reverse iterator, rbegin points to the last character of the string (i.e. the reverse beginning of the string). rend returns a reverse iterator pointing to the theoretical element preceding the first character of the string (considered to be the reverse end of the string). The difference between the forward iterator and the reverse iterator is that the ++ of the forward iterator goes to the tail, while the reverse iterator goes to the head.

3) Iterator read and write
void Test6()
{
	string s1 = "hello,world";
	string::iterator it = s1.begin();
	while (it != s1.end())
	{
	    *it='a';
		cout << *it;
		++it;
	}

	cout << endl;
}

operation result:

image-20230409144638061

The rest of the operations will not be illustrated one by one, and the following code will reflect it

3.1.5 string modification operation

function name Function Description
push back Insert the character c at the end of the string
append Append a string after a string
operator+= Append the string str after the string
c str Returns a C format string
find + npos Find the character c from the position of the string pos backwards, and return the position of the character in the string
rfind Find the character c from the position of the string pos forward, and return the position of the character in the string
substr Start at position pos in str, intercept n characters, and return them
1) string assignment operation
string& operator=(const char* s);//char*类型字符串 赋值给当前的字符串
string& operator=(const string &s);//把字符串s赋给当前的字符串
string& operator=(char c);//字符赋值给当前的字符串
string& assign(const char *s);//把字符串s赋给当前的字符串
string& assign(const char *s, int n);//把字符串s的前n个字符赋给当前的字符串
string& assign(const string &s);//把字符串s赋给当前字符串
string& assign(int n, char c);//用n个字符c赋给当前字符串
string& assign(const string &s, int start, int n);//将s从start开始n个字符赋值给字符串

Specific case:

#include<iostream>
using namespace std;
#include<string.h>

void test1()
{
    string st1 = "hello";
    string st2 = st1;

    cout << "st1 = " << st1 << endl;
    cout << "st2 = " << st2 << endl;

    string st3;
    st3.assign("hello");

    cout << "st3 = " << st3 << endl;

    string st4;
    st4.assign("hello c++", 5);

    cout << "st4 = " << st4 << endl;

    string st5;
    st5.assign(st4);

    cout << "st5 = " << st5 << endl;
}

int main()
{
    test1();

    system("pause");

    return 0;
}

operation result:

image-20230408163838973

To distinguish between initialization and assignment operations, the sequence relationship must first be clarified: assignment is to give an object value after initialization, for example:

string st1,string st2("hello");
st1 = st2;
2) string character access

There are two ways to access a single character in string:

  1. char &operator[](int n);//Get characters by [ ]
  2. char &at(int n);//Get character by at method

Example demonstration:

#include<iostream>
using namespace std;
#include<string.h>

void test1()
{
    string st1 = "hello,goodbye";

    cout << st1.size() << endl;

    //使用[]获取字符
    for(int i = 0;i < st1.size();i++)
    {
        cout << st1[i] << " ";
    }

    cout << endl;

    //使用at获取字符
    for(int j = 0;j < st1.size();j++)
    {
        cout << st1.at(j) <<" ";
    }

    cout << endl;

    st1[0] = 'H';

    st1.at(6) = 'G';

    cout << st1 << endl;

}

int main()
{
    test1();

    system("pause");

    return 0;
}

operation result:

image-20230408221303634

3) string splicing operation
void Test7()
{
	string str;
	str.push_back(' ');   // 在str后插入空格
	str.append("hello");  // 在str后追加一个字符"hello"
	str += 'd';           // 在str后追加一个字符'd'   
	str += "y";          // 在str后追加一个字符串"y"
	cout << str << endl;
	cout << str.c_str() << endl;   // 以C语言的方式打印字符串

	// 获取file的后缀
	string file("string.cpp");
	size_t pos = file.rfind('.');
	string suffix(file.substr(pos, file.size() - pos));
	cout << suffix << endl;

	// npos是string里面的一个静态成员变量
	// static const size_t npos = -1;

	// 取出url中的域名
	string url("http://www.cplusplus.com/reference/string/string/find/");
	cout << url << endl;
	size_t start = url.find("://");
	if (start == string::npos)
	{
		cout << "invalid url" << endl;
		return;
	}
	start += 3;
	size_t finish = url.find('/', start);
	string address = url.substr(start, finish - start);
	cout << address << endl;

	// 删除url的协议前缀
	pos = url.find("://");
	url.erase(0, pos + 3);
	cout << url << endl;
}

operation result:

image-20230409151117773

Notice:

  1. When appending characters at the end of the string, the implementation methods of s.push_back© / s.append(1, c) / s += 'c' are similar. In general, the += operation of the string class is used more, += Operations can concatenate not only single characters, but also strings.
    1. When operating on string, if you can roughly estimate how many characters to put, you can first reserve the space through reserve.

Understanding of npos :

When string::npos is used as a length parameter of a member function of string, it means "until the end of the string"

The string class defines npos as a value guaranteed to be greater than any valid subscript.

3.1.6 Use of string interface

function Function Description
operator+ Return by value, resulting in low efficiency of deep copy
operator>> Input operator overloading
operator<< output operator overloading
getline get a line of string
relational operators The size is relatively simple:

Operator overloading has already been mentioned, here is a brief introductiongetline

The function format of getline:getline(cin,string对象)

The function of getline is to read a whole line, and stop reading until it encounters a newline character, during which it can read blank characters such as spaces and tabs.

string s1;
getline(cin, s1);
cout << s1 << endl;

3.2 vector

3.2.1 Basic concept of vector

  1. A vector is a sequence container representing a variable-sized array.
  2. Just like arrays, vectors also use contiguous storage space to store elements. That means that the elements of the vector can be accessed using subscripts, which is as efficient as an array. But unlike an array, its size can be changed dynamically, and its size will be automatically handled by the container.
  3. Essentially, vector uses a dynamically allocated array to store its elements. When new elements are inserted, the array needs to be resized in order to increase storage space. It does this by allocating a new array and then moving all elements into this array. In terms of time, this is a relatively expensive task, because the vector does not resize each time a new element is added to the container.
  4. vector allocation space strategy: vector will allocate some additional space to accommodate possible growth, because the storage space is larger than the actual storage space required. Different libraries employ different strategies for weighing space usage and reallocation. But in any case, the reallocation should be of logarithmically growing interval size, so that inserting an element at the end is done in constant time complexity.
  5. Therefore, vector takes up more storage space, in order to gain the ability to manage storage space, and grow dynamically in an efficient way.
  6. Compared with other dynamic sequence containers (deque, list and forward_list), vector is more efficient when accessing elements, and adding and removing elements at the end is relatively efficient. For other deletion and insertion operations that are not at the end, it is less efficient. Better than list and forward_list unified iterators and references.

3.2.2 vector iterators

Vector maintains a linear space, so regardless of the type of elements, ordinary pointers can be used as vector iterators, because the operation behaviors required by vector iterators, such as operaroe*, operator->, operator++, operator–, operator+, operator -, operator+=, operator-=, normal pointers are born with.

Vector supports random access, and ordinary pointers have this ability. So vector provides random access iterators (Random Access Iterators)

Look at the following code:

Vector<int>::iterator a1;
Vector<Bus>::iterator a2;
//a1的类别是int*,a2的类型是Bus*

3.2.3 vector data structure

The data structure used by Vector is very simple, linear continuous space, it uses two iterators _Myfirst and _Mylast to point to the currently used range in the configured continuous space, and uses the iterator _Myend to point to the entire continuous memory end of space.

In order to reduce the speed cost of space configuration, the actual size of the vector configuration may be larger than the client's demand for possible future expansion. Here is the concept of capacity. The capacity of a vector is always greater than or equal to its size. Once the capacity is equal to the size, it is fully loaded. Next time there are new elements, the entire vector container will have to find another home.

The so-called dynamic increase in size is not a new space after the original space (because there is no guarantee that there is still configurable space after the original space), but a larger memory space, and then copy the original data to the new space and release the original space . Therefore, once any operation on the vector causes the space to be reconfigured, all iterators pointing to the original vector will become invalid. This is an easy mistake for programmers to make, so be careful.

  • 扩容The process needs to go through the following 3 steps: ——> Explain vectorthe reasons why the pointers, references, and iterators related to the container may become invalid after the container is expanded.
    1. Completely abandon the existing memory space and re-apply for a larger memory space;
    2. Move the data in the old memory space to the new memory space in the original order;
    3. Finally, the old memory space is freed.

3.2.4 Vector interface operation

1) vector construction

For this data structure, the constructor we use is as follows:

constructor declaration Interface Description
vector No parameter construction
vector(size_type n, const value_type& val = value_type()) Construct and initialize n vals
vector (const vector& x); (emphasis) copy construction
vector (InputIterator first, InputIterator last); Initialize construction using iterators

The test code is as follows:

int TestVector1()
{
    // constructors used in the same order as described above:
    vector<int> first;                                // empty vector of ints
    vector<int> second(4, 100);                       // four ints with value 100
    vector<int> third(second.begin(), second.end());  // iterating through second
    vector<int> fourth(third);                       // a copy of third

    // 下面涉及迭代器初始化的部分,我们学习完迭代器再来看这部分
    // the iterator constructor can also be used to construct from arrays:
    int myints[] = { 16,2,77,29 };
    vector<int> fifth(myints, myints + sizeof(myints) / sizeof(int));

    cout << "The contents of fifth are:";
    for (vector<int>::iterator it = fifth.begin(); it != fifth.end(); ++it)
        cout << ' ' << *it;
    cout << '\n';

    return 0;
}

The result of the operation is as follows:

image-20230412220107056
2) vector assignment
assign()

vector成员assign()负责分配新内容至vector中,代替现有内容并相应修改大小,本文介绍两种调用方法:

  1. Range用法:

    range是迭代器调用版本,新内容是由 firstlast 范围内的每个元素以相同的顺序构造的。使用的范围是 [first,last)

  2. Fill用法:

    用 n 个值为 val 的元素填充目的容器

说来也不是很复杂,以代码展示用法:

//Range
int TestVector2()
{
    vector<double> first{ 1.9, 2.9, 3.9, 4.9, 5.9 }; /*初始化源数组*/
    vector<double> second;                           /*声明空数组*/
    vector<int> third;
    vector<string> forth;
    
    vector<double>::iterator it;
    it = first.begin();

    second.assign(it, first.end());
    cout << "Size of second: " << int(second.size()) << '\n';
    for (int i = 0; i < second.size(); i++)
        cout << second[i] << " ";
    cout << endl;
    //结果:
    //Size of second: 5
    //1.9 2.9 3.9 4.9 5.9


    second.assign(it + 1, first.end() - 1);
    cout << "Size of second: " << int(second.size()) << '\n';
    for (int i = 0; i < second.size(); i++)
        cout << second[i] << " ";
    cout << endl;
    //Size of second: 3
    //2.9 3.9 4.9

    third.assign(it, first.end());
    cout << "Size of third: " << int(third.size()) << '\n';
    for (int i = 0; i < third.size(); i++)
        cout << third[i] << " ";
    cout << endl;
    //Size of third: 5
    //1 2 3 4 5

    int myints[] = {1776,7,4};
    third.assign (myints,myints+3);  /* assign with array*/
    cout << "Size of third: " << int(third.size()) << '\n';
    for (int i = 0; i < third.size(); i++)
        cout << third[i] << " ";
    cout << endl;
    //Size of third: 3
	//1776 7 4

    //third.assign (myints,myints+4); /*error usage,有结果但是行为错误*/
    //1776 7 4 787800
    // third = first; /*error usage*/
    // forth.assign(it,first.end()); /*error usage*/
    return 0;
}
//Fill
int TestVector3()
{
    vector<int> first(7);     /*fill版构造,无初值*/
    vector<int> second(7,1);  /*fill版构造,给定初值*/
    
    vector<int> third;
    third.assign(7,2);             /*fill版 assign */

    vector<int> forth;
    //forth.assign(7);           /*error usage, fill版assign必须给初值*/
    
    
    for (int i = 0; i < first.size(); i++)
        cout << first[i] << " ";
    cout << endl;

    for (int i = 0; i < second.size(); i++)
        cout << second[i] << " ";
    cout << endl;

    for (int i = 0; i < third.size(); i++)
        cout << third[i] << " ";
    cout << endl;
    
    //结果:
    //0 0 0 0 0 0 0
    //1 1 1 1 1 1 1
    //2 2 2 2 2 2 2
    return 0;
}
swap()
swap(vec);// 将vec与本身的元素互换

3)vector空间
容量空间 接口说明
size 获取数据个数
capacity 获取容量大小
empty 判断是否为空
resize 改变vector的size
reserve 改变vector的capacity
  • capacity的代码在vs和g++下分别运行会发现,vs下capacity是按1.5倍增长的,g++是按2倍增长的。 这个问题经常会考察,不要固化的认为,vector增容都是2倍,具体增长多少是根据具体的需求定义 的。vs是PJ版本STL,g++是SGI版本STL。
  • reserve只负责开辟空间,如果确定知道需要用多少空间,reserve可以缓解vector增容的代价缺陷问 题。
  • resize在开空间的同时还会进行初始化,影响size。

操作简介:

size();//返回容器中元素的个数
empty();//判断容器是否为空
resize(int num);//重新指定容器的长度为num,若容器变长,则以默认值填充新位置。如果容器变短,则末尾超出容器长度的元素被删除。
resize(int num, elem);//重新指定容器的长度为num,若容器变长,则以elem值填充新位置。如果容器变短,则末尾超出容器长>度的元素被删除。
capacity();//容器的容量
reserve(int len);//容器预留len个元素长度,预留位置不初始化,元素不可访问。

reserve用法:

void TestVector5() 
{
    vector<int> v;

    //预先开辟空间
    v.reserve(100000);

    int* pStart = NULL;
    int count = 0;
    for (int i = 0; i < 100000; i++) {
        v.push_back(i);
        if (pStart != &v[i]) {
            pStart = &v[i];
            count++;
        }
    }

    cout << "count:" << count << endl;
}

运行结果:

image-20230413201140079

resize用法:

void TestVector4()
{
    // reisze(size_t n, const T& data = T())
    // 将有效元素个数设置为n个,增多时,多的元素使用data进行填充
    // 注意:resize在增多元素个数时可能会扩容
        vector<int> v;

        for (int i = 1; i < 10; i++)
            v.push_back(i);

        v.resize(5);
        v.resize(8, 100);
        v.resize(12);

        cout << "v contains:";
        for (size_t i = 0; i < v.size(); i++)
            cout << ' ' << v[i];
        cout << '\n';
}

运行结果:

image-20230413195223723


4)vector增删查改
增删查改操作 接口说明
push_back 尾插
pop_back 尾删
find 查找
insert position前插入val
erase 删除position位置数据
swap 交换两vector数据空间
operator[] 像数组一样访问

用法:

insert(const_iterator pos, int count,ele);//迭代器指向位置pos插入count个元素ele.
push_back(ele); //尾部插入元素ele
pop_back();//删除最后一个元素
erase(const_iterator start, const_iterator end);//删除迭代器从start到end之间的元素
erase(const_iterator pos);//删除迭代器指向的元素
clear();//删除容器中所有元素

3.3 List

3.3.1 List基本概念

  1. list是可以在常数范围内在任意位置进行插入和删除的序列式容器,并且该容器可以前后双向迭代。
  2. list的底层是双向链表结构,双向链表中每个元素存储在互不相关的独立节点中,在节点中通过指针指向 其前一个元素和后一个元素。
  3. list与forward_list非常相似:最主要的不同在于forward_list是单链表,只能朝前迭代,已让其更简单高 效。
  4. 与其他的序列式容器相比(array,vector,deque),list通常在任意位置进行插入、移除元素的执行效率 更好。
  5. 与其他序列式容器相比,list和forward_list最大的缺陷是不支持任意位置的随机访问,比如:要访问list 的第6个元素,必须从已知的位置(比如头部或者尾部)迭代到该位置,在这段位置上迭代需要线性的时间 开销;list还需要一些额外的空间,以保存每个节点的相关联信息(对于存储类型较小元素的大list来说这 可能是一个重要的因素)

优缺点如下:

  • 采用动态存储分配,不会造成内存浪费和溢出
  • 链表执行插入和删除操作十分方便,修改指针即可,不需要移动大量元素
  • 链表灵活,但是空间和时间额外耗费较大

3.3.2 List迭代器

不像vector,list容器不能以普通指针为迭代器,因为其节点不能保证在同一块连续内存空间上。

List迭代器必须有能力指向list的节点,并有能力进行正确的递增、递减、取值、成员存取操作。所谓”list正确的递增,递减、取值、成员取用”是指,递增时指向下一个节点,递减时指向上一个节点,取值时取的是节点的数据值,成员取用时取的是节点的成员。

迭代器必须能够具备前移、后移的能力,所以list容器提供的是Bidirectional Iterators.

3.3.2.1 迭代器失效

可将迭代器暂时理解成类似于指针,迭代器失效即迭代器所指向的节点的无效,即该节点被删除了。因为list的底层结构为带头结点的双向循环链表,因此在list中进行插入时是不会导致list的迭代器失效的,只有在删除时才会失效,并且失效的只是指向被删除节点的迭代器,其他迭代器不会受到影响

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

3.3.3 List数据结构

list是个循环的双向链表

双向循环链表的循环方式是其尾结点的后继指针指向头结点(表头),而头结点的前置指针指向尾结点,达到双向循环的目的,这样不仅使得对链表尾部的操作更为简单,也减少了对NULL指针的引用。

3.3.4 List接口操作

1)list构造
constructor 接口说明
list (size_type n, const value_type& val = value_type()) 构造的list中包含n个值为val的元素
list() 构造空的list
list (const list& x) 拷贝构造函数
list (InputIterator first, InputIterator last) 用[first, last)区间中的元素构造list

代码演示:

void TestList1()
{
    list<int> l1;                         // 构造空的l1
    list<int> l2(4, 100);                 // l2中放4个值为100的元素
    list<int> l3(l2.begin(), l2.end());  // 用l2的[begin(), end())左闭右开的区间构造l3
    list<int> l4(l3);                    // 用l3拷贝构造l4

    // 以数组为迭代器区间构造l5
    int array[] = { 16,2,77,29 };
    list<int> l5(array, array + sizeof(array) / sizeof(int));

    // 列表格式初始化C++11
    list<int> l6{ 1,2,3,4,5 };

    // 用迭代器方式打印l5中的元素
    list<int>::iterator it = l5.begin();
    while (it != l5.end())
    {
        cout << *it << " ";
        ++it;
    }
    cout << endl;

    // C++11范围for的方式遍历
    for (auto& e : l5)
        cout << e << " ";

    cout << endl;
}

运行结果:

image-20230413204506140

2)list迭代器
函数声明 接口说明
begin + end 返回第一个元素的迭代器+返回最后一个元素下一个位置的迭代器
rbegin + rend 返回第一个元素的reverse_iterator,即end位置,返回最后一个元素下一个位置的 reverse_iterator,即begin位置

【注意】

  1. begin与end为正向迭代器,对迭代器执行++操作,迭代器向后移动
  2. rbegin(end)与rend(begin)为反向迭代器,对迭代器执行++操作,迭代器向前移动

代码演示:

list<int> l1{1,2,3,4,5};
//正向迭代器
list<int>::iterator it = l1.begin();
for(it;it != l1.end();it++)
{
    cout << *it << " ";
}
cout << endl;//结果:1 2 3 4 5

//反向迭代器
list<int>::reverse_iterator rit = l1.rbegin();
for (rit; rit != l1.rend(); rit++)
{
	cout << *rit << " ";
}
cout << endl;//结果:5 4 3 2 1
3)list大小操作
函数声明 接口说明
size 返回容器中元素的个数
empty 判断容器是否为空
resize() 重新指定容器长度
size();//返回容器中元素的个数
empty();//判断容器是否为空
resize(num);//重新指定容器的长度为num,
若容器变长,则以默认值填充新位置。
如果容器变短,则末尾超出容器长度的元素被删除。
resize(num, elem);//重新指定容器的长度为num,
若容器变长,则以elem值填充新位置。
如果容器变短,则末尾超出容器长度的元素被删除。
4)list增删存取
函数声明 接口说明
front 返回list的第一个节点中值的引用
back 返回list的最后一个节点中值的引用
push_front 在list首元素前插入值为val的元素
pop_front 删除list中第一个元素
push_back 在list尾部插入值为val的元素
pop_back 删除list中最后一个元素
insert 在list position 位置中插入值为val的元素
erase 删除list position位置的元素
swap 交换两个list中的元素
clear 清空list中的有效元素
front();//返回第一个元素。
back();//返回最后一个元素。
push_back(elem);//在容器尾部加入一个元素
pop_back();//删除容器中最后一个元素
push_front(elem);//在容器开头插入一个元素
pop_front();//从容器开头移除第一个元素
insert(pos,elem);//在pos位置插elem元素的拷贝,返回新数据的位置。
insert(pos,n,elem);//在pos位置插入n个elem数据,无返回值。
insert(pos,beg,end);//在pos位置插入[beg,end)区间的数据,无返回值。
clear();//移除容器的所有数据
erase(beg,end);//删除[beg,end)区间的数据,返回下一个数据的位置。
erase(pos);//删除pos位置的数据,返回下一个数据的位置。
remove(elem);//删除容器中所有与elem值匹配的元素。

3.4 vector/list 对比

vector与list都是STL中非常重要的序列式容器,由于两个容器的底层结构不同,导致其特性以及应用场景不 同,其主要不同如下:

vector list
底层结构 动态顺序表,一段连续空间 带头结点的双向循环链表
随机访问 支持随机访问,访问某个元素效率O(1) 不支持随机访问,访问某个元素 效率O(N)
插入/删除 任意位置插入和删除效率低,需要搬移元素,时间复杂 度为O(N),插入时有可能需要增容,增容:开辟新空 间,拷贝元素,释放旧空间,导致效率更低 任意位置插入和删除效率高,不 需要搬移元素,时间复杂度为 O(1)
空间利用率 底层为连续空间,不容易造成内存碎片,空间利用率 高,缓存利用率高 底层节点动态开辟,小节点容易造成内存碎片,空间利用率低,缓存利用率低
迭代器 原生态指针 对原生态指针(节点指针)进行封装
迭代器失效 在插入元素时,要给所有的迭代器重新赋值,因为插入 元素有可能会导致重新扩容,致使原来迭代器失效,删 除时,当前迭代器需要重新赋值否则会失效 插入元素不会导致迭代器失效,删除元素时,只会导致当前迭代器失效,其他迭代器不受影响
使用场景 需要高效存储,支持随机访问,不关心插入删除效率 大量删除及插入操作,不关心随机访问

3.5 stack

3.5.1 stack基本概念

stack是一种**先进后出(First In Last Out,FILO)**的数据结构,它只有一个出口,形式如图所示。

stack容器允许新增元素,移除元素,取得栈顶元素,但是除了最顶端外,没有任何其他方法可以存取stack的其他元素。换言之,stack不允许有遍历行为。

有元素推入栈的操作称为:push,将元素推出stack的操作称为pop

image-20230415155349674

3.5.2 stack无迭代器

Stack所有元素的进出都必须符合”先进后出”的条件,只有stack顶端的元素,才有机会被外界取用。Stack不提供遍历功能,也不提供迭代器。

3.5.3 stack数据结构

image-20230415155641791

限定仅在表尾进行插入和删除操作的线性表。这一端被称为栈顶,相对地,把另一端称为栈底。向一个栈插入新元素又称作进栈、入栈或压栈,它是把新元素放到栈顶元素的上面,使之成为新的栈顶元素;从一个栈删除元素又称作出栈或退栈,它是把栈顶元素删除掉,使其相邻的元素成为新的栈顶元素。

3.5.4 stack接口操作

函数说明 接口说明
stack() 构造空的栈
empty() 检测栈是否为空
size() 返回栈中元素个数
top() 返回栈顶元素引用
push() 将元素val压入栈中
pop() 将栈中尾部元素弹出

3.6 queue

3.6.1 queue基本概念

Queue是一种先进先出(First In First Out,FIFO)的数据结构,它有两个出口,queue容器允许从一端新增元素,从另一端移除元素。

image-20230415160152783

3.6.2 queue无迭代器

Queue所有元素的进出都必须符合”先进先出”的条件,只有queue的顶端元素,才有机会被外界取用。Queue不提供遍历功能,也不提供迭代器。

3.6.3 queue数据结构

队列是一种容器适配器,专门用于在FIFO上下文(先进先出)中操作,其中从容器一端插入元素,另一端 提取元素。

队列作为容器适配器实现,容器适配器即将特定容器类封装作为其底层容器类,queue提供一组特定的 成员函数来访问其元素。元素从队尾入队列,从队头出队列。

image-20230415160305854

3.6.4 queue接口操作

函数声明 接口说明
queue() 构造空的队列
empty() 检测队列是否为空,是返回true,否则返回false
size() 返回队列中有效元素的个数
front() 返回队头元素的引用
back() 返回队尾元素的引用
push() 在队尾将元素val入队列
pop() 将队头元素出队列

3.7 deque

deque(双端队列):是一种双开口的"连续"空间的数据结构,双开口的含义是:可以在头尾两端进行插入和删除操作,且时间复杂度为O(1),与vector比较,头插效率高,不需要搬移元素;与list比较,空间利用率比较高。

image-20230415165052607

3.7.1 deque基本概念

Deque容器是连续的空间,至少逻辑上看来如此,连续现行空间总是令我们联想到array和vector,array无法成长,vector虽可成长,却只能向尾端成长,而且其成长其实是一个假象,事实上

(1) 申请更大空间

(2)原数据复制新空间

(3)释放原空间

三步骤,如果不是vector每次配置新的空间时都留有余裕,其成长假象所带来的代价是非常昂贵的。

Deque是由一段一段的定量的连续空间构成。一旦有必要在deque前端或者尾端增加新的空间,便配置一段连续定量的空间,串接在deque的头端或者尾端。Deque最大的工作就是维护这些分段连续的内存空间的整体性的假象,并提供随机存取的接口,避开了重新配置空间,复制,释放的轮回,代价就是复杂的迭代器架构。

既然deque是分段连续内存空间,那么就必须有中央控制,维持整体连续的假象,数据结构的设计及迭代器的前进后退操作颇为繁琐。Deque代码的实现远比vector或list都多得多。

Deque采取一块所谓的map(注意,不是STL的map容器)作为主控,这里所谓的map是一小块连续的内存空间,其中每一个元素(此处成为一个结点)都是一个指针,指向另一段连续性内存空间,称作缓冲区。缓冲区才是deque的存储空间的主体。

3.7.2deque缺陷

与vector比较,deque的优势是:头部插入和删除时,不需要搬移元素,效率特别高,而且在扩容时,也不需要搬移大量的元素,因此其效率是必vector高的。

与list比较,其底层是连续空间,空间利用率比较高,不需要存储额外字段。

但是,deque有一个致命缺陷:不适合遍历,因为在遍历时,deque的迭代器要频繁的去检测其是否移动到某段小空间的边界,导致效率低下,而序列式场景中,可能需要经常遍历,因此在实际中,需要线性结构时,大多数情况下优先考虑vector和list,deque的应用并不多,而目前能看到的一个应用就是,STL用其作为stack和queue的底层数据结构。

为什么选择deque作为stack和queue的底层默认容器?

stack是一种后进先出的特殊线性数据结构,因此只要具有push_back()和pop_back()操作的线性结构,都可 以作为stack的底层容器,比如vector和list都可以;queue是先进先出的特殊线性数据结构,只要具有 push_back和pop_front操作的线性结构,都可以作为queue的底层容器,比如list。但是STL中对stack和 queue默认选择deque作为其底层容器,主要是因为:

  1. stack和queue不需要遍历(因此stack和queue没有迭代器),只需要在固定的一端或者两端进行操作。

  2. 在stack中元素增长时,deque比vector的效率高(扩容时不需要搬移大量数据);queue中的元素增长 时,deque不仅效率高,而且内存使用率高。

结合了deque的优点,而完美的避开了其缺陷。

3.7.3 deque接口操作

1)deque构造
deque<T> deqT;//默认构造形式
deque(beg, end);//构造函数将[beg, end)区间中的元素拷贝给本身。
deque(n, elem);//构造函数将n个elem拷贝给本身。
deque(const deque &deq);//拷贝构造函数。
2)deque赋值操作
assign(beg, end);//将[beg, end)区间中的数据拷贝赋值给本身。
assign(n, elem);//将n个elem拷贝赋值给本身。
deque& operator=(const deque &deq); //重载等号操作符 
swap(deq);// 将deq与本身的元素互换
3)deque大小操作
deque.size();//返回容器中元素的个数
deque.empty();//判断容器是否为空
deque.resize(num);//重新指定容器的长度为num,若容器变长,则以默认值填充新位置。如果容器变短,则末尾超出容器长度的元素被删除。
deque.resize(num, elem); //重新指定容器的长度为num,若容器变长,则以elem值填充新位置,如果容器变短,则末尾超出容器长度的元素被删除。
4)deque数据增删存取
//双端
push_back(elem);//在容器尾部添加一个数据
push_front(elem);//在容器头部插入一个数据
pop_back();//删除容器最后一个数据
pop_front();//删除容器第一个数据

at(idx);//返回索引idx所指的数据,如果idx越界,抛出out_of_range。
operator[];//返回索引idx所指的数据,如果idx越界,不抛出异常,直接出错。
front();//返回第一个数据。
back();//返回最后一个数据

insert(pos,elem);//在pos位置插入一个elem元素的拷贝,返回新数据的位置。
insert(pos,n,elem);//在pos位置插入n个elem数据,无返回值。
insert(pos,beg,end);//在pos位置插入[beg,end)区间的数据,无返回值。

clear();//移除容器的所有数据
erase(beg,end);//删除[beg,end)区间的数据,返回下一个数据的位置。
erase(pos);//删除pos位置的数据,返回下一个数据的位置。


4.容器使用场景

vector deque list set multiset map multimap
典型内存结构 单端数组 双端数组 双向链表 二叉树 二叉树 二叉树 二叉树
可随机存取 对key而言:不是
元素搜寻速度 非常慢 对key而言:快 对key而言:快
元素安插移除 尾端 头尾两端 任何位置 - - - -

vector的使用场景:比如软件历史操作记录的存储,我们经常要查看历史记录,比如上一次的记录,上上次的记录,但却不会去删除记录,因为记录是事实的描述。

Scenarios for using deque: For example, in a queuing ticketing system, deque can be used to store queuers, which supports quick removal at the head end and quick addition at the tail end. If vector is used, a large amount of data will be moved when the head end is removed, and the speed is slow.

Comparison between vector and deque:
1: vector.at() is more efficient than deque.at(). For example, vector.at(0) is fixed, but the starting position of deque is not fixed.
Two: If there are a lot of release operations, vector takes less time, which is related to the internal implementation of the two.
Three: deque supports fast insertion and fast removal of the head, which is the advantage of deque.

The usage scenario of list: such as the storage of bus passengers, passengers may get off at any time, and support the removal and insertion of frequent uncertain position elements.

Guess you like

Origin blog.csdn.net/qq_64893500/article/details/130172497