【C++修仙录02】筑基篇:适配器

嗨~大家好,这里是春栀怡铃声的博客~

"做你害怕的事,然后发现,不过如此~"

目录

适配器模式

类模板实例化

[vector 与 list比对](#vector 与 list比对)

deque

原理图

deque的迭代器

内部迭代器的遍历

头插

尾插

优先级队列(priority_queue)

[核心入队操作:push 与 向上调整 (AdjustUp)C++](#核心入队操作:push 与 向上调整 (AdjustUp)C++)

[核心出队操作:pop 与 向下调整 (AdjustDown)](#核心出队操作:pop 与 向下调整 (AdjustDown))

[5. 其他辅助接口C++](#5. 其他辅助接口C++)


适配器模式

适配器的作用,简单说就是:把一个"已有的东西"的接口改一改,变成"你想要的样子"。在 C++ 里,stack、queue 这类就属于容器适配器。

以 stack 为例:

vector 本来支持很多操作:

  • push_back()

  • pop_back()

  • back()

  • 下标访问 \[\]

  • insert()

  • erase()

但"栈"这种数据结构只希望你用这些规则:

  • 只能从栈顶放入元素

  • 只能从栈顶删除元素

  • 只能访问栈顶元素

所以适配器做的事就是:

  • 底层仍然用 vector 存数据

  • 对外不暴露 vector 的全部功能

  • 只包装成栈需要的接口:push、pop、top、empty、size

也就是:

  • push() 本质调用 vector::push_back()

  • pop() 本质调用 vector::pop_back()

  • top() 本质调用 vector::back()

它的意义:

  1. 复用已有容器

不用自己重新写一套存储结构,直接拿 vector 来用。

  1. 限制接口

防止用户乱用,比如栈不应该让你随便访问中间元素。

  1. 突出数据结构语义

用户一看 stack 就知道这是"后进先出",而不是普通数组。

你可以把它理解成:

  • vector 是"原材料"

  • stack 适配器是"加工层"

  • 最后对外提供"栈"这种更符合需求的用法

总结:

适配器的本质就是:底层复用别人的实现,表面包装成另一种更合适的接口。

类模板实例化

类模板实例化时,按需实例化,使用那些成员就实例化那些,不会全部实例化

这个代码中并没有包 #include<deque> ,模板也没有报错

模板实例化(使用) 才会检查出错误

vector 与 list比对

vector 优点:支持随机访问、尾插尾删效率不错

物理空间连续,所以高速缓存利用率高

缺点:头部和中间位置 插入、删除麻烦

需要扩容,扩容会有代价(效率和空间浪费)

list 优点:任意位置插入、删除

按照需要申请空间,不需要扩容

缺点:不支持随机访问

deque

vector 和 list 缝合怪

原理图

中控数组中 存放着指向每一个buff数组的 指针,buff数组中存放数据

扩容发生在中控数组,扩容会生成新的中控数组,只需要拷贝指针

deque的迭代器

deque迭代器有4个指针

node:二级指针,指向中控数组

cur:在访问buff的第几个数组

first:指向buff的开始

last:指向buff的结束

cpp 复制代码
iterator it = begin();
while(it!= end())
{
   cout<<*it<<" ";
   ++it;
}

内部迭代器的遍历

operator++()

{

++cur;

if(cur == last)

{

set_node(node+1); //让当前node加1 到下一个node

cur=first;

}

return *this;

}

通过遍历迭代器进行打印时,第一步的cur ==last 这时需要node++ 来到第二步

让cur 变成新node 的first ,继续遍历

头插

node向左走

新开一个buff 数组,进行插入,

上一个的node-- 就是指向新buff数组的中控数组

注意!在新的buff数组插入是从后向前进行插入

尾插

node向右走

如果尾插时,空间还未达到last,直接插在last 前面

如果已经达到,就新开一个buff 数组,进行插入,

上一个的node++ 就是指向新buff数组的中控数组

vector 排序 与 deque 排序对比

总结:

1.deque 头插尾插效率高,优于vector list

2.下标随机访问效率还可以,比不过vector

3.在中间插入删除效率低

封装:不管底层,直接使用封装好的(菜上桌了,只管吃,不需要管怎么制作)

栈(保持先进后出)和队列(先进先出)要保持,使用迭代器反而不好实现,使用适配器更优

1.用deque 适配队列

cpp 复制代码
template<class T,class Container =deque<T>>

class queue
{
public:
	void pop()
	{
		_con.pop_front();
	}
	const T& front() const
	{
		return _con.front();
	}

	const T& back() const
	{
		return _con.back();
	}
	void push(const T& x)
	{
		_con.push_back(x);
	}
	size_t size()const
	{
		return _con.size();
	}
	bool empty()const
	{
		return _con.empty();
	}


private:
	Container _con;
};

2.用vector 适配栈

把vector 尾部当作栈顶

cpp 复制代码
template<class T,class Container=deque<T> >
class stack
{
public:
	void push(const T& x)
	{
		_con.push_back(x);
	}
	void pop()
	{
		_con.pop_back();
	}
	const T& top()
	{
		return _con.back(); //尾部是栈顶
	}
	size_t size()const
	{
		return _con.size();
	}
	bool empty()const
	{
		return _con.empty();
	}
private:
	Container _con;
};

优先级队列(priority_queue)

优先级队列 默认大的优先级高 底层是堆

仿函数

在排序算法中为什么使用仿函数

因为排序算法本身不知道你想按什么规则排。

它只负责"重排元素",至于:

  • 是从小到大还是从大到小

  • 是按年龄排还是按分数排

  • 分数相同再按名字排

这些规则都要你提供。仿函数就是一种"把比较规则封装起来"的方式。

最直接地说:

sort 需要一个"比较器",仿函数就是比较器的一种实现。

为什么不用算法自己判断

比如两个 Student:

cpp 复制代码
  struct Student {
      string name;
      int score;
  };

排序时算法看到两个学生对象,并不知道:

  • 比较 name

  • 还是比较 score

  • 还是先比 score,再比 name

所以你必须告诉它"谁应该排前面"。

仿函数的作用

仿函数本质上是"像函数一样使用的类对象"。

例如:

cpp 复制代码
template <class T>
class Less
{
public:
	bool operator()(const T& x, const T& y)
	{
		return x < y;
	}
};
cpp 复制代码
template <class T>
class Greater
{
public:
	bool operator()(const T& x, const T& y)
	{
		return x > y;
	}
};

优先级队列代码实现:

cpp 复制代码
template<class T ,class Container =vector<T>,class Compare = Less<T>>
class priority_queue
{
public:
	void AdjustUp(int child)
	{
		Compare com;
		int parent = (child - 1) / 2;
		while (child > 0)
		{
			if (com(_con[parent], _con[child]))
			{
				swap(_con[parent], _con[child]);
				child = parent;
				parent = (child - 1) / 2;
			}
			else
			{
				break;
			}
		}
	}
	void push(const T& x)
	{
		_con.push_back(x);

		AdjustUp(_con.size() - 1);
	}
	void AdjustDown(int parent)
	{
		size_t child = parent * 2 + 1;

		Compare com;
		while (child < _con.size())
		{
			if (child + 1 < _con.size() && com(_con[child], _con[child + 1]))
			{
				++child;
			}
			if (com(_con[parent], _con[child]))
			{
				swap(_con[child], _con[parent]);
				parent = child;
				child = parent * 2 + 1;
			}
			else
			{
				break;
			}
		}
	}

	void pop()
	{
		swap(_con[0], _con[_con.size() - 1]);
		_con.pop_back();
		AdjustDown(0);
	}

	const T& top()
	{
		return _con[0];
	}
	
	size_t size() const
	{
		return _con.size();
	}

	bool empty() const
	{
		return _con.empty();
	}

private:
	Container _con;
};
cpp 复制代码
template<class T ,class Container =vector<T>,class Compare = Less<T>>

这个类采用了泛型编程(模板),包含三个参数:

  • T : 存储在优先队列中的数据类型(如 int, string 等)。

  • Container : 底层用于存储数据的容器,默认使用 std::vector<T>。因为堆的操作需要频繁进行随机访问和在尾部增删元素,vector 是最合适的选择。

  • Compare : 仿函数(Functor),用于定义元素的优先级比较规则。代码中默认设为 Less<T>(通常表示 < 操作)。在默认情况下,这种实现会构造一个大顶堆(Max-Heap),即最大的元素放在堆顶。

cpp 复制代码
private:
	Container _con;

_con 是底层容器的实例。所有插入到优先队列中的元素都会实际存放在这个数组中。堆的父子节点关系通过数组的索引来计算:

  • 已知父节点索引 parent,其左子节点为 parent * 2 + 1,右子节点为 parent * 2 + 2

  • 已知子节点索引 child,其父节点为 (child - 1) / 2

核心入队操作:push 与 向上调整

cpp 复制代码
void push(const T& x) {
    _con.push_back(x);
    AdjustUp(_con.size() - 1);
}
  • push 逻辑 : 当有新元素入队时,首先将其放在底层数组的末尾(相当于插入到完全二叉树的最后一个叶子节点),然后调用 AdjustUp 对其进行"向上调整",以恢复堆的性质。
cpp 复制代码
void AdjustUp(int child) {
    Compare com;
    int parent = (child - 1) / 2; // 计算父节点索引
    while (child > 0) {
        // 如果 com(父节点, 子节点) 成立(例如 父 < 子)
        if (com(_con[parent], _con[child])) {
            swap(_con[parent], _con[child]); // 交换父子节点
            child = parent;                  // 子节点上移
            parent = (child - 1) / 2;        // 重新计算新的父节点
        } else {
            break; // 如果父节点已经大于等于子节点,说明堆已处于正确状态,停止调整
        }
    }
}
  • AdjustUp 逻辑 : 它不断将当前插入的节点与其父节点进行比较。如果父节点的值小于子节点(在 CompareLess 时),就将它们交换,直到该节点到达根部(child == 0)或不再大于其父节点为止。

核心出队操作:pop 与 向下调整

cpp 复制代码
void pop() {
    swap(_con[0], _con[_con.size() - 1]); // 交换堆顶(首)与堆底(尾)元素
    _con.pop_back();                      // 删除原堆顶元素
    AdjustDown(0);                        // 将新堆顶元素向下调整
}
  • pop 逻辑 : 优先队列出队时,只能弹出优先级最高(堆顶位置,即 _con[0])的元素。为了不在数组头部删除元素(导致全体元素前移,时间复杂度为 O(N)),这里巧妙地将首尾元素交换,然后删除末尾元素(时间复杂度 O(1)),最后将交换到堆顶的元素"向下调整"恢复堆的结构。
cpp 复制代码
void AdjustDown(int parent) {
    size_t child = parent * 2 + 1; // 默认先指向左孩子
    Compare com;
    while (child < _con.size()) {
        // 1. 找出左右孩子中优先级较高(较大)的一个
        if (child + 1 < _con.size() && com(_con[child], _con[child + 1])) {
            ++child; // 如果右孩子存在且大于左孩子,让 child 指向右孩子
        }
        // 2. 将较大的孩子与父节点比较
        if (com(_con[parent], _con[child])) { // 如果 父 < 最大的孩子
            swap(_con[child], _con[parent]);  // 交换它们
            parent = child;                   // 父节点下移
            child = parent * 2 + 1;           // 重新计算新的左孩子
        } else {
            break; // 如果父节点已经大于等于最大的孩子,停止调整
        }
    }
}
  • AdjustDown 逻辑 : 从指定的 parent 节点开始,每次比较它与它的两个子节点。先选出子节点中较大的一个,然后判断父节点是否小于这个较大的子节点。如果是,就交换它们,让父节点"下沉",一直循环直到它沉到合适的位置或者变成叶子节点。

5. 其他辅助接口

cpp 复制代码
const T& top() {
    return _con[0];
}
size_t size() const {
    return _con.size();
}
bool empty() const {
    return _con.empty();
}
  • top() : 返回堆顶元素的常量引用(即 _con[0]),也就是当前队列中优先级最高的元素。

  • size(): 返回队列中当前元素的个数。

  • empty(): 判断队列是否为空。

感谢花时间阅读这篇内容!

如果觉得有价值,欢迎点赞支持、收藏备用,或分享给同行。你的认可,是我持续输出高质量内容的最大动力。

我们下期再见喽!!!

相关推荐
LuminousCPP1 小时前
从零开始学 C++|系列开篇:从 C 到 C++ 的衔接之路
开发语言·c++·笔记
星恒随风1 小时前
C++ 类和对象入门(一):从 class、访问限定符到 this 指针
开发语言·c++·笔记·学习·状态模式
赵民勇1 小时前
如何查看一个二进制程序是否设置了rpath或runpath?
linux·c++
Brilliantwxx1 小时前
【C++】 哈希表 unordered_map 与 unordered_set(底层原理 + 线性哈希表代码实现)
开发语言·c++·散列表
ouliten2 小时前
C++笔记:C++20风格线程池
c++·笔记·c++20
weixin_467182282 小时前
Arduino进阶二|自定义类库保姆级教程(从零手写属于自己的传感器类库+完整源码)
c语言·c++·单片机·嵌入式硬件·arduino·c++面向对象·diy库文件
Highcharts.js2 小时前
通过CSS变量实现图表色彩与逻辑解耦、图表主题统一|Highcharts Palette 详解
c++·echarts·highcharts·可视化开发·palette·styledmode·图表样式
玖玥拾2 小时前
C/C++ 基础笔记(八)
c语言·c++
郝学胜_神的一滴2 小时前
Qt 高级开发 027: QTabWidget自定义样式表美化实战
c++·qt