C++初阶—list类

第一章:list的介绍及使用

1.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来说这可能是一个重要的因素

1.2 list的使用

list中的接口比较多,此处类似,只需要掌握如何正确的使用,然后再去深入研究背后的原理,已达到可扩展的能力。以下为list中一些常见的重要接口。

1.2.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 |

1.2.2 list iterator的使用

此处,大家可暂时将迭代器理解成一个指针,该指针指向list中的某个节点。

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

【注意】

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

1.2.3 list capacity

|-------|------------------------------|
| 函数声明 | 接口说明 |
| empty | 检测list是否为空,是返回true,否则返回false |
| size | 返回list中有效节点的个数 |

1.2.4 list element access

|-------|--------------------|
| 函数声明 | 接口说明 |
| front | 返回list的第一个节点中值的引用 |
| back | 返回list的最后一个节点中值的引用 |

1.2.5 list modifiers

|------------|------------------------------|
| 函数声明 | 接口说明 |
| 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中的有效元素 |

1.2.6 list的迭代器失效

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

Use of List.cpp

cpp 复制代码
#define _CRT_SECURE_NO_WARNINGS
#include <iostream>
#include <vector>
#include <list>
#include <algorithm>

using namespace std;

void test_list1() {
	list<int> lt;
	lt.push_back(1);
	lt.push_back(2);
	lt.push_back(3);
	lt.push_back(4);
	lt.push_back(5);

	list<int>::iterator it = lt.begin();
	while (it != lt.end()) { //遍历
		cout << *it << " ";
		++it;
	}
	cout << endl;

	for (auto e : lt) //范围for
		cout << e << " ";
	cout << endl;
}

void test_list2() {
	list<int> lt;
	lt.push_back(1);
	lt.push_back(2);
	lt.push_back(3);
	lt.push_back(4);
	lt.push_back(5);
	
	lt.reverse();//逆置
	for (auto e : lt)
		cout << e << " ";//5 4 3 2 1
	cout << endl;

	//sort(lt.begin(), lt.end());//报错
	(从访问使用的角度划分)迭代器功能上分类:正向/反向 及 正向/反向 + const
	(从容器底层实现的角度划分)迭代器性质上分类:
	单向 ++		    单链表/哈希表
	双向 ++/--		双向链表/红黑树(map和set)
	随机 ++/--/+/-  vector/string/deque
	算法库的sort要求传随机迭代器,而list的是双向迭代器。所以list有自己的sort
	
	//less<int> less_lt;//升序
	//lt.sort(less_lt);
	lt.sort();//sort默认升序
	for (auto e : lt)
		cout << e << " ";
	cout << endl;
	//降序
	//greater<int> gt;
	//lt.sort(gt);
	lt.sort(greater<int>());//实际中这种传匿名对象的方式更常用
	//数据量小可以用这个sort,数据量大效率不高。	
}

void test_list3() { //验证list模板中sort的效率
	vecor排序比list快,数据量越大,两者差距越大
	//srand((int)time(0));
	//const int N = 1000000;

	//vector<int> v;
	//v.reserve(N);
	//list<int> lt1;

	//for (int i = 0; i < N; ++i) {
	//	auto e = rand();
	//	lt1.push_back(e);
	//	v.push_back(e);
	//}

	 排序
	//int begin1 = clock();
	//sort(v.begin(), v.end());
	//int end1 = clock();
	//
	//int begin2 = clock();
	//lt1.sort();
	//int end2 = clock();

	//printf("vector sort:%d\n", end1 - begin1);
	//printf("list sort:%d\n", end2 - begin2);

	//比较:lt1直接排序,lt2数据拷贝到vector在排序
	srand((int)time(0));
	const int N = 1000000;

	list<int> lt1;
	list<int> lt2;

	for (int i = 0; i < N; ++i) {
		auto e = rand();
		lt1.push_back(e);
		lt2.push_back(e);
	}

	// 排序
	int begin1 = clock();
	vector<int> v(lt2.begin(), lt2.end());//将lt2的数据拷贝到vector再排序
	sort(v.begin(), v.end());
	lt2.assign(v.begin(), v.end());//将vector的数据再拷贝回lt2
	int end1 = clock();

	int begin2 = clock();
	lt1.sort();
	int end2 = clock();

	printf("list2 sort:%d\n", end1 - begin1);
	printf("list1 sort:%d\n", end2 - begin2);
}

void test_list4() {
	//merge() 归并两个链表,取小的尾插。前提是两个链表有序

	//去重演示,需要保证有序
	list<int> lt;
	lt.push_back(1);
	lt.push_back(2);
	lt.push_back(3);
	lt.push_back(4);
	lt.push_back(3);
	lt.push_back(3);
	lt.push_back(3);
	lt.push_back(5);
	lt.push_back(5);
	lt.push_back(3);
	for (auto e : lt)
		cout << e << " ";//1 2 3 4 3 3 3 5 5 3
	cout << endl;

	lt.sort();
	lt.unique();
	for (auto e : lt)
		cout << e << " ";//1 2 3 4 5
	cout << endl;

	lt.remove(3);//删除全部3
	//lt.remove(30);//删除的数据不存在,什么都不做
	for (auto e : lt)
		cout << e << " ";//1 2 4 5
	cout << endl;
}

void test_list5() {
	list<int> lt1, lt2;

	for (int i = 1; i <= 4; ++i)
		lt1.push_back(i);// lt1: 1 2 3 4

	for (int i = 1; i <= 3; ++i)
		lt2.push_back(i * 10);// lt2: 10 20 30

	list<int>::iterator it = lt1.begin();
	++it;//it指向2

	//lt1.splice(it, lt2);//1 10 20 30 2 3 4  //将整个lt2转移到lt1 第二节点的前面
	lt1:1 10 20 30 2 3 4   lt2为空,it依旧指向2

	//lt1.splice(it, lt2, ++lt2.begin());//1 20 2 3 4  //将lt2的第二节点转移到lt1的第二节点前面
	lt1.splice(it, lt2, ++lt2.begin(), lt2.end());//1 20 30 2 3 4  //将lt2从第二节点至末尾转移到lt1的第二节点前面
	for (auto e : lt1)
		cout << e << " ";
	cout << endl;
}

int main() {
	//test_list1();
	test_list2();
	//test_list3();
	//test_list4();
	//test_list5();

	return 0;
}

第二章:list的模拟实现

2.1 模拟实现list

List Class.h

cpp 复制代码
#pragma once

namespace bit {
	//封装目的: 封装成 list_node 结构体是为了将链表节点与具体的操作逻辑分离,保持代码的清晰性和模块化。
	//在链表实现中多次使用这个结构体来管理节点,而不仅仅是在 list 类里,因此单独封装是合理的做法。
	template<class T>
	//(如果使用class封装就会变成私有,后期可能需要友元)
	struct list_node { //节点结构体。
		T _data;
		list_node<T>* _next;
		list_node<T>* _prev;

		list_node(const T& x = T())//如果T是自定义类型,则x会是该类型的默认构造对象,取决于该类型的默认构造函数如何定义。
			:_data(x), _next(nullptr), _prev(nullptr) {
		}
	};


	vector中原生指针符合迭代器需要的场景。
	但list不行,对list指针解引用得到的是一个节点结构体,而不是该节点储存的数据;且单纯的++也不支持这种操作
	所以需要对list的迭代器进行封装+运算符重载(即封装节点指针,可以参考日期类的++)
	将迭代器封装成一个独立的类,使得链表的遍历更加简洁和灵活。
	迭代器是 STL 容器设计中的重要概念,单独封装可以提高代码的复用性和可维护性。
	//template<class T>
	//struct __list_iterator { //迭代器
	//	typedef list_node<T> Node;
	//	typedef __list_iterator<T> self;
	//	Node* _node;

	//	__list_iterator(Node* node)
	//		:_node(node) {
	//	}

	//	//迭代器不需要析构函数,因为不需要释放资源(即节点)
	//	//迭代器的赋值和拷贝构造需不需要深拷贝?
	//	//不需要。迭代器通常只是一个轻量级的指针包装类,它指向的资源归容器管理,迭代器不负责资源的分配或释放。
	//	//深拷贝的必要性:深拷贝(Deep Copy)通常用于动态分配资源的类,如 new 申请的内存。
	//	//目的是避免多个对象共享同一块动态内存,导致释放时的重复释放或悬空指针问题。

	//	self& operator++() { //前置++
	//		_node = _node->_next;
	//		return *this;
	//	}
	//	self& operator--() { //前置--
	//		_node = _node->_prev;
	//		return *this;
	//	}

	//	self operator++(int) { //后置++
	//		self tmp(*this);
	//		_node = _node->_next;
	//		return tmp;
	//	}
	//	self operator--(int) { //后置--
	//		self tmp(*this);
	//		_node = _node->_prev;
	//		return tmp;
	//	}

	//	T& operator*() { return _node->_data; }
	//	T* operator->() { return &_node->_data; }//提供给自定义类型解引用。自定义类型解引用是 指针->形式,所以返回指针
	//	bool operator!= (const self& s) { return _node != s._node; }
	//	bool operator== (const self& s) { return _node == s._node; }
	//};

	const迭代器
	非const迭代器是可读可写,const迭代器是只读
	const T*(指向的内容不能修改)、T* const(指针本身不能修改,即不能指向其他内容)
	const迭代器依然要实现++等(即迭代器指针要改变指向),所以模拟的是第一种
	const迭代器不是非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* node)
	//		:_node(node) {
	//	}

	//	//self& operator++() const该写法不对,这里的const修饰指的是this指向的成员不能修改,
	//	//那么_node就不能被重新赋值,无法向下一节点移动
	//	self& operator++() { //前置++
	//		_node = _node->_next;
	//		return *this;
	//	}
	//	self& operator--() { //前置--
	//		_node = _node->_prev;
	//		return *this;
	//	}

	//	self operator++(int) { //后置++
	//		self tmp(*this);
	//		_node = _node->_next;
	//		return tmp;
	//	}
	//	self operator--(int) { //后置--
	//		self tmp(*this);
	//		_node = _node->_prev;
	//		return tmp;
	//	}

	//	//const迭代器不能修改指向的内容,但本身可以移动。所以不能改为const T& operator*() const { return _node->_data; }
	//	const T& operator*() { return _node->_data; }
	//	const T* operator->() { return &_node->_data; }
	//  //迭代器通过上面两个运算符重载修改指向内容,所以const迭代器要对他们返回值进行const修饰防止修改
	//	bool operator!= (const self& s) { return _node != s._node; }
	//	bool operator== (const self& s) { return _node == s._node; }
	//};
	//非const迭代器和const迭代器仅仅只是operator*()和operator->()返回值不同,
	//又因为同一个类模板,实例化参数不同,就是完全不同的类型。所以两个迭代器通过模板参数列表可以合并为一个。
	
	//将非const和const迭代器合并为一个类模板
	//vector中原生指针符合迭代器需要的场景。
	//但list不行,对list指针解引用得到的是一个节点结构体,而不是该节点储存的数据
	//所以需要对list的迭代器进行封装+运算符重载(即封装节点指针,可以参考日期类的
	//将迭代器封装成一个独立的类,使得链表的遍历更加简洁和灵活。
	//迭代器是 STL 容器设计中的重要概念,单独封装可以提高代码的复用性和可维护性。
	template<class T, class Ref, class Ptr>//迭代器是模拟指针的行为
	struct __list_iterator { //迭代器
		typedef list_node<T> Node;
		typedef __list_iterator<T, Ref, Ptr> self;
		Node* _node;

		//迭代器可以理解为节点指针
		__list_iterator(Node* node)//单参数构造函数支持隐式类型转换,也就是说节点指针也可以隐式类型转换为迭代器
			:_node(node) {
		}

		//迭代器不需要析构函数,因为不需要释放资源(即节点)
		//迭代器的赋值和拷贝构造需不需要深拷贝?
		//不需要。迭代器通常只是一个轻量级的指针包装类,它指向的资源归容器管理,迭代器不负责资源的分配或释放。
		//深拷贝的必要性:深拷贝(Deep Copy)通常用于动态分配资源的类,如 new 申请的内存。
		//目的是避免多个对象共享同一块动态内存,导致释放时的重复释放或悬空指针问题。

		self& operator++() { //前置++
			_node = _node->_next;
			return *this;
		}
		self& operator--() { //前置--
			_node = _node->_prev;
			return *this;
		}

		self operator++(int) { //后置++
			self tmp(*this);
			_node = _node->_next;
			return tmp;
		}
		self operator--(int) { //后置--
			self tmp(*this);
			_node = _node->_prev;
			return tmp;
		}

		Ref operator*() { return _node->_data; }
		Ptr operator->() { return &_node->_data; }//提供给自定义类型解引用。自定义类型解引用是 指针->形式,所以返回指针
		//非const和const迭代器中只有上面这两个运算符重载的返回值不同
		bool operator!= (const self& s) { return _node != s._node; }
		bool operator== (const self& s) { return _node == s._node; }
	};



	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;
		//上方并没有实例化迭代器,而是根据list<T>模板的不同类型创建了适用于该类型的迭代器类型别名。这些别名只是便于使用而已。
		//对迭代器模板的重命名:因为迭代器模板内部因为需要同时兼容非const和const迭代器,所以参数表达更宽泛。
		//而list类模板内部知道需要使用哪几种迭代器,所以对类模板的参数进行了进一步的限定表达。
		//typedef __list_const_iterator<T> const_iterator;//非const和const迭代器合并为一个类模板,所以不需要这个

		//const_iterator begin() const { return const_iterator(_head->_next); }
		const_iterator begin() const { return _head->_next; }//支持隐式类型转换
		const_iterator end() const { return _head; }//支持隐式类型转换

		iterator begin() {
			//在return iterator(_head->_next); 这一行,_head->_next是list_node<T>*类型的指针,代表链表中第一个有效节点。
			//iterator(_head->_next) 会通过 __list_iterator 构造函数将这个指针传递给 _node 成员。
			//然后返回一个 iterator 对象,该对象持有一个指向节点的指针。
			//return iterator(_head->_next);
			return _head->_next;
		}

		iterator end() { return _head; }

		void empty_init() {
			_head = new Node;
			_head->_next = _head;
			_head->_prev = _head;
			_size = 0;
		}

		list() { empty_init(); } //构造函数本质是创建一个哨兵位节点

		//lt2(lt1)
		list(const list<T>& lt) { //lt是const对象,需要const迭代器
			empty_init();//创建(lt2的)哨兵位
			for (auto e : lt)//遍历lt1每个节点赋值给e
				push_back(e);//lt2尾插每个e节点
		}

		传统写法 lt3 = lt1 思路:释放lt3,将lt1的节点尾插到lt3
		//list<T>& operator=(const list<T>& lt) {
		//	if (this != &lt) {
		//		clear();
		//		for (auto e : lt)
		//			push_back(e);
		//	}
		//	return *this;
		//}
		//现代写法
		void swap(list<T>& lt) {
			std::swap(_head, lt._head);
			std::swap(_size, lt._size);
		}
		list<T>& operator=(list<T> lt) {
			swap(lt);
			return *this;
		}

		~list() {
			clear();
			delete _head;//清除哨兵位
			_head = nullptr;
		}

		void clear() {
			iterator it = begin();//begin指向哨兵位的下一节点,即有效数据的头节点
			while (it != end())
				it = erase(it);
		}

		//void push_back(const T& x) {
		//	Node* newnode = new Node(x);
		//	Node* tail = _head->_prev;
		//	tail->_next = newnode;
		//	newnode->_prev = tail;
		//	newnode->_next = _head;
		//	_head->_prev = newnode;
		//}
		//优化版本 - 复用insert
		void push_back(const T& x) { insert(end(), x); }//end指向哨兵位,在哨兵位之前插入
		void push_front(const T& x) { insert(begin(), x); }
		void pop_back() { erase(--end()); }//哨兵位前一节点就是尾结点
		void pop_front() { erase(begin()); }

		//list中insert之后迭代器理论上不会失效。因为不涉及扩容,且迭代器依旧指向原先位置。
		//但库里面有返回值,所以和库里保持一致。
		//pos是迭代器对象,现在需要使用的是节点指针,所以需要通过迭代器对象来访它自己的成员变量来获取节点指针。
		iterator insert(iterator pos, const T& x) { 
			Node* cur = pos._node;
			Node* newnode = new Node(x);
			Node* prev = cur->_prev;
			//prev newnode cur
			prev->_next = newnode;
			newnode->_prev = prev;
			newnode->_next = cur;
			cur->_prev = newnode;
			++_size;
			return iterator(newnode);
		}

		iterator erase(iterator pos) { //erase后迭代器失效,因为迭代器指向的节点被释放了,所以返回下一个节点的位置
			Node* cur = pos._node;
			Node* prev = cur->_prev;
			Node* next = cur->_next;
			delete cur;
			prev->_next = next;
			next->_prev = prev;
			--_size;
			return iterator(next);
		}

		size_t size() { return _size; }

	private:
		Node* _head;
		size_t _size;//增加该成员避免每次获取size都要遍历一遍链表
	};



	void test_list1() {
		list<int> lt;
		lt.push_back(1);
		lt.push_back(2);
		lt.push_back(3);
		lt.push_back(4);
		lt.push_back(5);

		//封装屏蔽底层差异和实现细节
		//提供统一的访问修改方式
		list<int>::iterator it = lt.begin();
		while (it != lt.end()) {
			cout << *it << " ";
			++it;
		}
		cout << endl;

		for (auto e : lt)
			cout << e << " ";
		cout << endl;
	}

	void test_list2() {
		list<int> lt;
		lt.push_back(1);
		lt.push_back(2);
		lt.push_back(3);
		lt.push_back(4);
		lt.push_back(5);
		for (auto e : lt)
			cout << e << " ";
		cout << endl;

		list<int> lt1(lt);
		for (auto e : lt1)
			cout << e << " ";
		cout << endl;

		list<int> lt2;
		lt2.push_back(10);
		lt2.push_back(20);
		lt2.push_back(30);
		lt2.push_back(40);
		lt2.push_back(50);
		lt1 = lt2;
		for (auto e : lt1)
			cout << e << " ";
		cout << endl;

		for (auto e : lt2)
			cout << e << " ";
		cout << endl;
	}

	struct AA {
		AA(int a1 = 0, int a2 = 0)
			:_a1(a1), _a2(a2) {
		}
		int _a1;
		int _a2;
	};
	void test_list3() {
		list<AA> lt;
		lt.push_back(AA(1, 1));
		lt.push_back(AA(2, 2));
		lt.push_back(AA(3, 3));
		list<AA>::iterator it = lt.begin();
		while (it != lt.end()) {
			//cout << *it << " ";//报错。
			//该*it(AA)是自定义对象,不支持流插入
			//第一种方式让AA支持流插入,但这种方式怎么分隔两个值有歧义,并且提供这种方式的流插入就不能变了(写死了)

			//cout << (*it)._a1 << " " << (*it)._a2 << endl;//_a1和_a2是公有的,可以直接访问
			cout << it->_a1 << " " << it->_a2 << endl;//迭代器模拟的是指针的行为,所以自定义类型指针解引用就是->
			cout << it.operator->()->_a1 << " " << it.operator->()->_a2 << endl;
			//根据上面显示调用可知,本应该是it->->_a1。第一个箭头是运算符重载,返回一个指针,第二个箭头才是解引用。
			//但是这种写法可读性不好,所以编译器特殊处理,省略了一个箭头
			++it;
		}
		cout << endl;
	}

	该版本只能打印list<int>
	//void print_list(const list<int>& lt) {
	//	list<int>::const_iterator it = lt.begin();
	//	while (it != lt.end()) {
	//		cout << *it << " ";
	//		++it;
	//	}
	//	cout << endl;

	//	for (auto e : lt)
	//		cout << e << " ";
	//	cout << endl;
	//}
	该版本只能打印list
	template<class T>
	//template<typename T>
	//void print_list(const list<T>& lt) {
	//	//下方list<T>还没有实例化(list<int>才是实例化)
	//	//编译器不确定list<T>::const_iterator是静态成员变量还是内嵌类型
	//	//所以加typename告诉编译器这是一个类型,等类模板(list<T>)实例化再去类里面取
	//	typename list<T>::const_iterator it = lt.begin();
	//	while (it != lt.end()) {
	//		cout << *it << " ";
	//		++it;
	//	}
	//	cout << endl;
	//}
	该版本只能打印list
	//template<typename T>
	//void print_container(const list<T>& lt) {
	//	//下方list<T>还没有实例化(list<int>才是实例化)
	//	//编译器不确定list<T>::const_iterator是静态成员变量还是内嵌类型
	//	//所以加typename告诉编译器这是一个类型,等类模板(list<T>)实例化再去类里面取
	//	typename list<T>::const_iterator it = lt.begin();
	//	while (it != lt.end()) {
	//		cout << *it << " ";
	//		++it;
	//	}
	//	cout << endl;
	//}
	template<typename Container> //Container是一个自定义模板参数
	void print_container(const Container& con) {
		typename Container::const_iterator it = con.begin();//Container可以是list<T>、vector<T>
		while (it != con.end()) {
			cout << *it << " ";
			++it;
		}
		cout << endl;
	}
	void test_list4() {
		list<int> lt;
		lt.push_back(1);
		lt.push_back(2);
		lt.push_back(3);
		lt.push_back(4);
		lt.push_back(5);
		print_container(lt);

		//vector的push_back涉及深拷贝问题,因为vector需要扩容,
		//当vector中元素拷贝到新空间时,vector中元素指向的内容也需要拷贝。但list不存在扩容问题。
		list<string> lt1;
		lt1.push_back("1111111111111");
		lt1.push_back("1111111111111");
		lt1.push_back("1111111111111");
		lt1.push_back("1111111111111");
		lt1.push_back("1111111111111");
		print_container(lt1);

		vector<string> v;
		v.push_back("2222222222222");
		v.push_back("2222222222222");
		v.push_back("2222222222222");
		v.push_back("2222222222222");
		print_container(v);
	}
}

Test.cpp

cpp 复制代码
#define _CRT_SECURE_NO_WARNIN
#include <iostream>
#include <string>
#include <vector>

using namespace std;
#include "List Class.h"

int main() {
	//bit::test_list1();
	//bit::test_list2();
	//bit::test_list3();
	bit::test_list4();
	return 0;
}

第三章:list与vector的对比

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

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

作业

1. 以下程序输出结果为( )

cpp 复制代码
int main() {
	int ar[] = { 0,1, 2, 3, 4,  5, 6, 7, 8, 9 };
	int n = sizeof(ar) / sizeof(int);
	list<int> mylist(ar, ar + n);
	list<int>::iterator pos = find(mylist.begin(), mylist.end(), 5);

	reverse(mylist.begin(), pos);
	reverse(pos, mylist.end());

	list<int>::const_reverse_iterator crit = mylist.crbegin();
	while (crit != mylist.crend()) {
		cout << *crit << " ";
		++crit;
	}
	cout << endl;
}

A.4 3 2 1 0 5 6 7 8 9

B.0 1 2 3 4 9 8 7 6 5

C.5 6 7 8 9 0 1 2 3 4

D.5 6 7 8 9 4 3 2 1 0

答案:C

分析 : list<int>::iterator pos = find(mylist.begin(), mylist.end(), 5); //找到5的位置

reverse(mylist.begin(), pos);//逆置0 1 2 3 4 为 4 3 2 1 0

reverse(pos, mylist.end()); //逆置5 6 7 8 9 为 9 8 7 6 5

逆置完成之后其数据为:4 3 2 1 0 9 8 7 6 5

list<int>::const_reverse_iterator crit = mylist.crbegin(); //反向迭代器进行反向访问

while (crit != mylist.crend()) {}

所以答案为:5 6 7 8 9 0 1 2 3 4

2. 以下代码实现了从表中删除重复项的功能,请选择其中空白行应填入的正确代码( )

cpp 复制代码
template<typename T>
void removeDuplicates(list<T>& aList) {
	T curValue;
	list<T>::iterator cur, p;
	cur = aList.begin();

	while (cur != aList.end()) {
		curValue = *cur;
		//空白行 1
		while (p != aList.end()) {
			if (*p == curValue)	
				//空白行 2			
			else			
				p++;			
		}
	}
}

A. p=cur+1;aList.erase(p++);

B.p=++cur; p == cur ? cur = p = aList.erase(p) : p = aList.erase(p);

C.p=cur+1;aList.erase(p);

D.p=++cur;aList.erase(p);

答案:B

分析:迭代p需要迭代不重复节点的下一节点,重要的是cur迭代器需要往下迭代,因此cur需要往前移动,二答案A C的cur都不会改变,空白行2是当需要找到重复值时进行节点删除,当删除时当前迭代器会失效,因此需要将迭代器p往后迭代,所以答案为 B

3. 对于list有迭代器it, 当erase(it)后,说法错误的是( )

A.当前迭代器it失效

B.it前面的迭代器仍然有效

C.it后面的迭代器失效

D.it后面的迭代器仍然有效

答案:C

分析:删除节点后,只有指向当前节点的迭代器失效了,其前后的迭代器仍然有效,因为底层为不连续空间,只有被删除的节点才会失效, 所以答案为 C

4. 下面程序的输出结果正确的是( )

cpp 复制代码
int main() {
	int array[] = { 1, 2, 3, 4, 0, 5, 6, 7, 8, 9 };
	int n = sizeof(array) / sizeof(int);

	list<int> mylist(array, array + n);
	auto it = mylist.begin();

	while (it != mylist.end()) {
		if (*it != 0)
			cout << *it << " ";
		else
			it = mylist.erase(it);
		++it;
	}
	return 0;
}

A.1 2 3 4 5 6 7 8 9

B. 1 2 3 4 6 7 8 9

C.程序运行崩溃

D.1 2 3 4 0 5 6 7 8 9

答案:B

分析:程序在使用迭代器取值时,如果不等于0就进行打印,为0时不打印并删除当前节点,所以答案为 B

5. 下面有关vector和list的区别,描述正确的是( )

A.两者在尾部插入的效率一样高

B.两者在头部插入的效率一样高

C.两者都提供了push_back和push_front方法

D.两者都提供了迭代器,且迭代器都支持随机访问

答案:A

A.vector在尾部插入数据不需要移动数据,list为双向循环链表也很容易找到尾部,因此两者在尾部插入数据效率相同

B.vector头部插入效率极其低,需要移动大量数据

C.vector由于在头部插入数据效率很低,所以没有提供push_front方法

D.list不支持随机访问

6. 下面有关vector和list的区别,描述错误的是( )

A.vector拥有一段连续的内存空间,因此支持随机存取,如果需要高效的随机读取,应该使用vector

B.list拥有一段不连续的内存空间,如果需要大量的插入和删除,应该使用list

C.vector<int>::iterator支持"+"、"+="、"<"等操作符

D.list<int>::iterator则不支持"+"、"+="、"<"等操作符运算,但是支持了[]运算符

答案:D

A.如果想大量随机读取数据操作,vector是首选的容器

B.如果想大量的插入和删除数据,list效率较高,是首选

C.由于vector底层是连续空间,其迭代器就是相应类型的指针,所以支持对应的操作

D.list迭代器不支持[]运算符

相关推荐
阳光_你好4 分钟前
解决用git bash终端 tail -f 命令查看日志中文乱码问题
开发语言·git·bash
nlog3n5 分钟前
Java 原型模式 详解
java·开发语言·原型模式
能来帮帮蒟蒻吗1 小时前
Go语言学习(15)结构体标签与反射机制
开发语言·笔记·学习·golang
陈皮话梅糖@3 小时前
使用 Provider 和 GetX 实现 Flutter 局部刷新的几个示例
开发语言·javascript·flutter
hvinsion4 小时前
基于PyQt5的自动化任务管理软件:高效、智能的任务调度与执行管理
开发语言·python·自动化·自动化任务管理
Aphelios3804 小时前
Java全栈面试宝典:线程机制与Spring IOC容器深度解析
java·开发语言·jvm·学习·rbac
qq_529835355 小时前
装饰器模式:如何用Java打扮一个对象?
java·开发语言·装饰器模式
日暮南城故里5 小时前
Java学习------源码解析之StringBuilder
java·开发语言·学习·源码
Vitalia5 小时前
从零开始学Rust:枚举(enum)与模式匹配核心机制
开发语言·后端·rust
双叶8366 小时前
(C语言)虚数运算(结构体教程)(指针解法)(C语言教程)
c语言·开发语言·数据结构·c++·算法·microsoft