模拟实现优先队列和反向迭代器的个人总结

优先队列

之前的容器传的都是迭代器,优先队列为什么传的是索引?

可以使用索引的前提就是容器支持随机访问(如vector),而优先队列有这个性质

如果使用迭代器,代码肯定会复杂很多,毕竟每次都要调用

void heapAdjustByIter(vector<int>& heap, vector<int>::iterator current, int heapSize)

迭代器本身是一个对象,构造、复制、解引用都有微小时间和空间的开销

为什么优先队列模板会有三个参数

template<class T,class Container=vector<T>, class Compare=less<T>>

第二个是选择优先队列存储的容器类型,这里只能使用支持随机访问的容器,第三个是选择比较用的容器,用于控制构建大堆/小堆

我本来想着传过去的容器(Container)里面本来就有存储类型了,干嘛还要传第一个T?其实容器的存储类型对于优先队列来说 本来就是个盲盒,既然选择要接收这个容器,就必须明确容器的存储类型,所以才用第一个参数(类型)来写第二个参数

可以确保模板参数发挥设想的作用吗?

实际上不能,模板参数就是用于接收不确定的类型,所以不用想可能会接收到什么,支不支持完成什么功能,必要的检查加上即可,剩下的就看调用者传什么过来了

priority_queue(const Inputiterator p1,const Inputiterator p2) {//不知道要干啥了
    assert(p1 < p2);//是谁都有这种比大小的运算符重载吗?//不是,这里不检查
    for (auto i = p1; i != p2;i++) {
        _arr.push_back(input[i]);
    }
    for (int i = (input.size() - 1 - 1) / 2; i >= 0; i--) {//for循环不能用size_t会爆
        Down_Adjust(i);//用索引而不是迭代器
    }
}

建堆的过程中为什么要保证左右子树都是堆?

首先这是最高效的方式,只需要n/2次向下调整

其次如果换成其它办法(比方对堆顶重复往下调整)是完全没办法保证堆本身的性质的

为什么Pop后选择向下调整而不是左右子树补位?

虽然这样可以让画出来的“树状图”确实还像个堆,但是数组早就被破坏了。如图如果选择7补位,需要进行判断。而且假如空缺的是3,5这些位置,补位会导致2,6,7构成的子树完全混乱

为什么push选择向上调整而不是像pop一样先交换然后向下调整?

向上必然是正确的(因为只有一条路可走),而向下每次有两条路径,有可能走错路


仿函数

一般形式

先定义一个类类型,然后写一个它的括号重载函数,用起来的时候就跟函数一样,所以叫仿函数

template<class T>
class less {
public:
	bool operator()(const T& p1,const T& p2){
		return p1 < p2;
	}
};

为什么调用仿函数的时候不会调用它的构造函数?两个不是长得一样吗?

首先,语法上就不同:

// 语法1:类型名 对象名(参数); → 构造函数
MyFunctor obj(5);   // 调用构造函数 MyFunctor(int)

// 语法2:对象名(参数); → operator()
obj(10);            // 调用 operator()(int)

其次,构造函数只能使用一次(对象被定义的时候)后续不能再显式调用构造函数

特殊情况:临时仿函数对象如果要调用仿函数就要写成下面的的形式:

MyFunctor(5)(10);

为什么不直接用仿函数的构造函数来触发而选择用重载运算符?比如下面这样

template<class T>
class less {
public:
    less(T p1){
    p1++;}
};

有三个原因:

  1. 构造函数不能有返回值,而仿函数通常需要返回值。如果上面代码真要比大小的话,肯定要返回一个Bool的

  2. 构造函数只能通过创建新对象来调用,会导致每次“调用”都会创建一个新对象,开销大

  3. 如前文所述,构造函数不能被显式调用,而仿函数可以。

能否用除了仿函数以外的方式,实现优先队列指定建大堆/小堆呢?

既然是“指定”,构建优先队列的时侯必然传递了一些参数,这样就会有多种替代方式,这里只列

传函数指针这一种方式

bool minHeapCompare(int a, int b) {
    return a > b;  // 小顶堆

bool maxHeapCompare(int a, int b) {
    return a < b;  // 大顶堆
}

int main() {
    // 将函数指针类型作为模板参数
    bool (*minHeapFuncPtr)(int, int) = minHeapCompare;
    priority_queue<int, vector<int>, bool(*)(int, int)> 
        minHeap(minHeapFuncPtr);
    
    bool (*maxHeapFuncPtr)(int, int) = maxHeapCompare;
    priority_queue<int, vector<int>, bool(*)(int, int)> 
        maxHeap(maxHeapFuncPtr);}

然而,仿函数仍然是最佳选择,原因如下

1. 性能最优:内联优化好
2. 可携带状态:可以存储比较所需的额外信息
3. 类型安全:编译时类型检查
4. 可复用:容易在不同地方复用

什么时候需要自己写仿函数?

比如比较List指针的时候,由于指针没有前后之分,所以必须自己控制实现

反向迭代器

template<class Iterator,class Ref,class Ptr>
class Reiterator {
public:
	typedef Reiterator<Iterator, Ref, Ptr>self;
	Iterator _it;
};

template<class T,class Ref,class Ptr>
struct List_Iterator {
	node* _it;
};

//看看在容器中是怎么调用的?以List为例
template<class T>
class List {
public:
	typedef List_Iterator<T,T&,T*> iterator;
	typedef List_Iterator<const T,const T&,const T*> const_iterator;
	typedef Reiterator<iterator, T&, T*> reiterator;
	typedef Reiterator<const_iterator, const T&, const T*> const_reiterator;//为啥要我自己传给他?不然它不知道啊,又回到了那个问题//一定不要重名
	typedef List_node<T> node;

	iterator begin() {
		return iterator(_start->_next);
	}

	iterator end() {
		return iterator(_start);//左闭右开
	}

	reiterator rebegin() {
		return reiterator(end());
	}

	reiterator rend() {
		return reiterator(begin());
	}
};

跟容器适配器有点像,reiterator直接把Iterator拿去复用了。“reiterator”这个名字本身只是为了给iterator做配套的服务而已.

值得注意的是,设计上rebegin()是iterator的end(),rend是原函数的begin();那我总不能真去访问end吧?所以解引用重载就往前挪了一位

Ref operator*() {
	Iterator temp = _it;
	return *(--temp);//这里也可以显式调用operator()
}

Ptr operator->() {
	return &(operator*());
}

为啥要在容器中将正向迭代器传给反向迭代器?在反向迭代器里面定义iterator不就好了?

这其实就和之前设计const和非const迭代器一样的意思,假如我在迭代器内部这样写:

template<class T,class Ref,class Ptr>
class Reiterator {
public:
    typefdef List_Iterator<T,T&, T*> iterator;
    typefdef List_Iterator<T,const T&,const T*> const_iterator;
	typedef Reiterator<Iterator, Ref, Ptr>self;

    const_iterator _const_it;
    iterator _it;
	//那我后面每个iterator有关的函数都得注明用const还是非const的
    //要么就写两个Reiterator,那我还是在list里面直接定义吧
};

为什么前置++可以返回引用而后置++就不行?

self& operator++() {
	_it++;
	return *this;
}

self operator++(int) {
	 self temp=*this;
    //这里使用默认的拷贝构造函数,不需要另外写一个
    //因为会自动调用iterator的拷贝构造函数
	_it--;
	return temp;
    //此时temp已经被销毁,不能拿去引用,必须构造
}

其它各种小错误

非const引用不能绑定到字面值、临时对象或需要类型转换的对象,而const引用可以

范围for是不能使用反向迭代器

 一旦声明了任何构造函数,编译器就不再自动生成默认构造函数,如果没写后面会报错的

swap是浅拷贝吗?不是的:

namespace std {
    template<typename T>
    void swap(T& a, T& b) {
        T temp = a;  // 调用拷贝构造函数
        a = b;       // 调用拷贝赋值运算符
        b = temp;    // 调用拷贝赋值运算符
    }
}//这样的实现方式决定了swap既可以浅拷贝又可以深拷贝

 for循环不能用size_t,会爆,如果是i++还好,要是i--的话,i如果小于0就会变成随机值

for (size_t i = n-1; i >= 0; --i) {
    // 当i为0时,执行循环体后,--i导致i变为size_t的最大值,循环条件i>=0始终成立,导致无限循环。
}

尽量不要和库里面的函数同名,虽然有命名空间,但要是用了using namespace就可能冲突了

调用模板类的时候千万记得加参数,嫌麻烦就typedef一下,把自己坑了好多次了

如果是编译错误就用注释代码的方式,能快速定位到错误区域,在较大片的区域里面还可以用二分法的原则去找,挺推荐的

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值