1 std::list 的排序

1.1 基础类型以及 std::string 类型的排序

std::list的排序可以通过调用其成员函数sort()来实现。sort()函数使用默认的比较操作符(<)对std::list中的元素进行排序。这意味着,如果元素类型定义了<操作符,std::list将使用它来比较元素。

例如,如果有一个std::list<int>,则可以这样排序它:

#include <list>  
#include <algorithm>  

int main() 
{
	std::list<int> myList = { 4, 2, 5, 1, 3 };
	myList.sort();

	// myList现在包含{1, 2, 3, 4, 5}  

	return 0;
}

如果 list 里面的元素类型没有定义<操作符,或者需要使用自定义的比较函数,则可以通过传递一个比较函数或对象给sort()函数来实现。这个比较函数或对象应该接受两个参数(即需要比较的两个元素)并返回一个布尔值,指示第一个参数是否应该在排序后出现在第二个参数之前。

例如有一个std::list<std::string>,并且需要按照字符串的长度来排序它,可以这样做:

#include <list>  
#include <algorithm>  
#include <string>  

bool compareByStringLength(const std::string& a, const std::string& b) {
	return a.size() < b.size();
}

int main() 
{
	std::list<std::string> myList = { "apple", "banana", "cherry", "date" };
	myList.sort(compareByStringLength);

	// myList现在按照字符串长度排序,可能包含{"date", "apple", "cherry", "banana"}  

	return 0;
}

上面代码中定义了一个名为 compareByStringLength 的比较函数,它比较两个字符串的长度。然后将这个函数作为参数传递给 sort() 函数,以按照字符串长度对 std::list 进行排序。

1.2 自定义类型的排序

如果需要对 std::list 中的自定义类型进行排序,则需要提供一个自定义的比较函数或比较对象,该函数或对象定义了如何比较两个自定义类型的对象。这个比较函数或对象应该接受两个参数并返回一个布尔值,用于确定第一个参数是否应该在排序后出现在第二个参数之前。

下面是一个例子,展示了如何对 std::list 中的自定义类型进行排序:

#include <list>  
#include <string>  
#include <iostream>  
#include <algorithm>  

// 自定义类型  
struct Person {
	std::string name;
	int age;

	// 构造函数  
	Person(const std::string& name, int age) : name(name), age(age) {}

	// 为了方便输出,重载<<操作符  
	friend std::ostream& operator<<(std::ostream& os, const Person& person) {
		os << "Name: " << person.name << ", Age: " << person.age;
		return os;
	}
};

// 自定义比较函数,用于排序Person对象  
bool comparePersonByAge(const Person& a, const Person& b) {
	return a.age < b.age; // 按年龄升序排序  
}

int main() 
{
	// 创建一个包含Person对象的std::list  
	std::list<Person> people = {
		{"Alice", 25},
		{"Bob", 20},
		{"Charlie", 30},
		{"David", 25}
	};

	// 使用自定义比较函数对people进行排序  
	people.sort(comparePersonByAge);

	// 输出排序后的结果  
	for (const auto& person : people) {
		std::cout << person << std::endl;
	}

	return 0;
}

上面代码的输出为:

Name: Bob, Age: 20
Name: Alice, Age: 25
Name: David, Age: 25
Name: Charlie, Age: 30

上面代码中定义了一个 Person 结构体,它包含 name 和 age 两个成员。然后创建了一个 comparePersonByAge 函数,它接受两个 Person 对象并比较它们的年龄。这个函数将用于 std::list 的 sort 成员函数来按照年龄对 Person 对象进行排序。

最后,在main函数中,创建了一个 std::list<Person>并初始化了几个 Person 对象。然后调用 sort 成员函数并传递 comparePersonByAge 函数作为比较对象,从而对 people 列表进行排序。

注意:如果需要按照降序排序,可以在comparePersonByAge函数中将比较操作从<改为>。此外,如果自定义类型定义了<操作符,也可以直接使用默认的排序而不需要提供自定义比较函数。

2 std::list 的主要应用场景

以下是一些std::list的主要应用场景:

(1)频繁插入和删除操作: std::list 允许在序列中的任意位置进行常数时间的插入和删除操作。这使得它非常适合需要频繁添加或移除元素的场景,如日志记录、任务队列、事件处理等。

(2)双向迭代: std::list 支持双向迭代,这意味着可以轻松地遍历列表,向前或向后移动。这种特性在处理需要前后关联的数据时非常有用,如文本编辑器中的撤销与重做操作。

(3)保持元素顺序: std::list 通过链表结构保持元素的插入顺序。这对于需要维护元素原始顺序的场景(如数据库查询结果、事件历史记录等)非常有用。

(4)动态数据结构: 当数据结构的大小经常变化时,std::list 是一个很好的选择。与基于数组的容器(如 std::vector)相比,std::list 在插入和删除元素时不需要移动大量数据,因此性能更高。

(5)内存管理: 在某些情况下,可能希望更好地控制内存使用。std::list 允许管理每个元素的内存分配,这在处理大量数据或需要精确控制内存使用的场景中非常有用。

(6)与线程安全容器结合使用: std::list 可以与其他线程安全容器(如 std::list<std::unique_ptr<T>>)结合使用,以在多线程环境中安全地管理资源。

2.1 std::list 应用于任务队列

std::list 在任务队列的应用场景中特别有用,特别是当需要频繁地在队列的任意位置插入和删除任务时。由于 std::list 的双向链表结构,它支持在常数时间内完成这些操作,这使得它成为处理动态任务队列的理想选择。

下面是一个简单的示例,展示了如何使用 std::list 来实现一个任务队列:

#include <iostream>  
#include <list>  
#include <thread>  
#include <chrono>  
#include <mutex>  
#include <condition_variable>  

// 任务类型定义  
struct Task {
	Task() {}
	Task(int id, std::function<void()> work) : id(id), work(work) {}

	int id;
	std::function<void()> work;
};

// 任务队列类  
class TaskQueue 
{
public:
	// 向队列中添加任务  
	void pushTask(Task task) {
		std::lock_guard<std::mutex> lock(mtx);
		tasks.push_back(task);
		condVar.notify_one(); // 通知等待的线程有新任务到来  
	}

	// 从队列中取出任务并执行  
	bool popTask(Task& task) {
		std::unique_lock<std::mutex> lock(mtx);
		condVar.wait(lock, [this] { return !tasks.empty(); }); // 等待直到有任务到来  
		task = tasks.front();
		tasks.pop_front(); // 移除并返回队列前端的任务  
		return true;
	}

	// 检查队列是否为空  
	bool isEmpty() {
		std::lock_guard<std::mutex> lock(mtx);
		return tasks.empty();
	}
private:
	std::list<Task> tasks;
	std::mutex mtx;
	std::condition_variable condVar;

};

// 模拟任务执行的函数  
void simulateWork(int taskId) {
	std::cout << "Executing task " << taskId << std::endl;
	std::this_thread::sleep_for(std::chrono::seconds(1)); // 模拟耗时任务  
}

int main() 
{
	TaskQueue taskQueue;

	// 启动生产者线程,向队列中添加任务  
	std::thread producer([&taskQueue]() {
		for (int i = 0; i < 10; ++i) {
			taskQueue.pushTask(Task(i, std::bind(simulateWork, i)));
			std::this_thread::sleep_for(std::chrono::milliseconds(500)); // 模拟任务生成间隔  
		}
	});

	// 启动消费者线程,从队列中取出并执行任务  
	std::thread consumer([&taskQueue]() {
		Task task;
		while (taskQueue.popTask(task)) {
			task.work(); // 执行任务  
		}
	});

	// 等待生产者和消费者线程完成  
	producer.join();
	consumer.join();

	return 0;
}

上面代码的输出为:

Executing task 0
Executing task 1
Executing task 2
Executing task 3
Executing task 4
Executing task 5
Executing task 6
Executing task 7
Executing task 8
Executing task 9

上面代码中定义了一个 Task 结构体来表示任务,它包含一个标识符和一个工作函数。TaskQueue 类提供了 pushTask 方法来向队列中添加任务,以及 popTask 方法来从队列中取出并执行任务。TaskQueue 还使用了一个互斥锁和一个条件变量来确保线程安全,并允许消费者在任务可用时被唤醒。

simulateWork 函数模拟了任务的执行过程,它只是打印任务 ID 并休眠一秒钟来模拟耗时任务。在 main 函数中,创建了一个生产者线程来生成任务,并将其添加到任务队列中,同时还创建了一个消费者线程来从队列中取出任务并执行。

2.2 std::list 应用于文本编辑器中的撤销与重做操作

在文本编辑器的撤销与重做操作中,使用 std::list 的双向迭代特性可以使得在向前和向后遍历编辑历史时都非常高效。下面是一个示例,该示例演示了如何使用 std::list 来管理编辑历史,并支持撤销和重做操作,同时体现了双向迭代在处理这种需要前后关联的数据时的优势。

#include <iostream>  
#include <list>  
#include <string>  

// 文本编辑器类  
class TextEditor 
{
public:
	// 构造函数  
	TextEditor() : currentPos(history.end()) {}

	// 输入文本  
	void inputText(const std::string& text) {
		// 添加新文本到历史记录  
		history.push_back(text);
		// 更新当前位置迭代器  
		currentPos = std::prev(history.end());
	}

	// 撤销操作  
	void undo() {
		if (currentPos != history.begin()) {
			// 如果不是第一次编辑,则向前移动迭代器  
			--currentPos;
		}
	}

	// 重做操作  
	void redo() {
		if (std::next(currentPos) != history.end()) {
			// 如果还有后续的历史记录,则向后移动迭代器  
			++currentPos;
		}
	}

	// 获取当前文本  
	std::string getCurrentText() const {
		if (currentPos != history.end()) {
			return *currentPos;
		}
		return "";
	}

	// 显示编辑历史  
	void showHistory() const {
		for (const auto& text : history) {
			std::cout << text << std::endl;
		}
	}

private:
	std::list<std::string> history; // 存储编辑历史的列表  
	std::list<std::string>::iterator currentPos; // 当前编辑位置的迭代器  
};

int main() {
	TextEditor editor;

	// 用户输入一些文本  
	editor.inputText("Initial text.");
	editor.inputText("Edited text.");
	editor.inputText("Edited again.");

	// 显示编辑历史  
	editor.showHistory();

	// 执行撤销操作  
	editor.undo();
	std::cout << "After undo: " << editor.getCurrentText() << std::endl;

	// 执行重做操作  
	editor.redo();
	std::cout << "After redo: " << editor.getCurrentText() << std::endl;

	// 再次显示编辑历史来验证状态  
	editor.showHistory();

	return 0;
}

上面代码的输出为:

Initial text.
Edited text.
Edited again.
After undo: Edited text.
After redo: Edited again.
Initial text.
Edited text.
Edited again.

在上面代码中,TextEditor 类使用 std::list 来维护编辑历史。每次调用 inputText 方法时,它都会清除当前位置之后的所有历史记录,并将新文本添加到历史列表的末尾。撤销操作通过向前移动迭代器并删除当前元素来实现,而重做操作则通过向后移动迭代器来实现。由于 std::list 支持双向迭代,因此这些操作都是常数时间复杂度的,非常高效。

3 std::list 的扩展与自定义

在大多数情况下,不需要对 std::list 做扩展与自定义,因为它已经提供了非常完整的功能集。然而,如果需要更高级的功能或定制行为,则可以考虑以下几种方法:

(1)继承自 std::list:
可以通过继承 std::list 并添加自定义成员函数来扩展其功能。然而,这通常不是推荐的做法,因为继承标准库组件可能导致未定义的行为或意外的副作用。标准库组件通常设计为不可继承的,并且它们的内部实现可能会在不同版本的编译器或标准库中发生变化。

(2)使用适配器模式:
适配器模式允许将一个类的接口转换为另一个类的接口,而不需要修改原始类。可以创建一个适配器类,它封装了一个 std::list 实例,并提供了自定义的接口。这样,可以在不修改 std::list 本身的情况下添加新功能。

(3)自定义迭代器:
std::list 允许使用自定义迭代器。开发人员可以创建自己的迭代器类,该类提供对 std::list 元素的访问,并在迭代过程中添加自定义逻辑。然后,可以将这些自定义迭代器传递给 std::list 的成员函数,以在遍历元素时应用自定义行为。

(4)使用代理类:
代理类是一个设计模式,它允许一个类代表另一个类的功能,并在调用功能时添加额外的逻辑。你可以创建一个代理类,它封装了一个 std::list 实例,并提供与 std::list 相同的接口。在代理类的实现中,可以在调用 std::list 的方法之前或之后添加自定义逻辑:

(5)自定义分配器:
std::list 使用分配器来管理内存。可以通过提供一个自定义的分配器来定制 std::list 的内存分配行为。自定义分配器可以允许你控制内存的分配策略,例如使用内存池、共享内存或其他高级内存管理技术。

4 实现一个简单的 std::list 容器

如下是一个一个简化的 std::list 容器的实现,仅包含基本的构造函数、析构函数、插入和遍历功能:

#include <iostream>  

template <typename T>
class MyList 
{
public:
	struct Node {
		T data;
		Node* next;
		Node* prev;

		Node(const T& value) : data(value), next(nullptr), prev(nullptr) {}
	};

	MyList() : head(nullptr), tail(nullptr) {}

	~MyList() {
		clear();
	}

	void push_back(const T& value) {
		Node* newNode = new Node(value);
		if (head == nullptr) {
			head = newNode;
			tail = newNode;
		}
		else {
			tail->next = newNode;
			newNode->prev = tail;
			tail = newNode;
		}
	}

	void push_front(const T& value) {
		Node* newNode = new Node(value);
		if (head == nullptr) {
			head = newNode;
			tail = newNode;
		}
		else {
			newNode->next = head;
			head->prev = newNode;
			head = newNode;
		}
	}

	void clear() {
		Node* current = head;
		while (current != nullptr) {
			Node* next = current->next;
			delete current;
			current = next;
		}
		head = nullptr;
		tail = nullptr;
	}

	void display() const {
		Node* current = head;
		while (current != nullptr) {
			std::cout << current->data << " ";
			current = current->next;
		}
		std::cout << std::endl;
	}

private:
	Node* head;
	Node* tail;
};

int main() 
{
	MyList<int> myList;

	myList.push_back(10);
	myList.push_back(20);
	myList.push_front(5);

	myList.display();

	myList.clear();

	return 0;
}

上面代码的输出为:

5 10 20

这个简化的 MyList 类包含了一个内部的Node结构,用于存储数据以及指向下一个和上一个节点的指针。MyList 类提供了 push_back 和 push_front 方法用于在列表的末尾和开头插入元素,clear 方法用于删除所有元素,以及 display 方法用于遍历并打印列表中的所有元素。

03-04 06:45