从C语言到C++_20(仿函数+优先级队列priority_queue的模拟实现+反向迭代器)(上)

简介: 从C语言到C++_20(仿函数+优先级队列priority_queue的模拟实现+反向迭代器)

1. priority_queue的模拟实现

默认情况下的priority_queue是大堆,我们先不考虑用仿函数去实现兼容大堆小堆排列问题,

我们先实现大堆,把基本的功能实现好,带着讲解完仿函数后再去进行优化实现。

优先级队列相较于普通的队列,其区别主要是在 push 和 pop 上

即需要在插入 / 删除数据的同时,增添调整的功能,其也是对适配器的封装,

push 和 pop 需要堆上调和下调操作,我们不可避免地需要实现堆上调和下调的算法。

我们这里暂时写死,设计成大堆的 adjust_up 和 adjust_down:

对堆调整算法不熟悉的可以看这里;

数据结构与算法⑪(第四章_中)堆的分步构建_GR C的博客-CSDN博客

1.1 未完全的priority_queue

入队时将数据插入后,从最后一个元素开始进行上调。

出队时采用堆的删除手法,将根元素(即队头)和最后一个元素进行交换,并调用尾删掉队头。

既然用了头尾交换法,我们就不得不对队列重新调整,所以从根位置进行下调,即可完成出队。

除了插入和删除,其它接口和queue差不多,PriorityQueue.h:

#pragma once
#include <iostream>
#include <vector> 
#include <algorithm>
#include <functional> // greater和less的头文件
using namespace std;
 
namespace rtx
{
  template<class T, class Container = vector<T>>
  class priority_queue
  {
  public:
    void push(const T& x)
    {
      _con.push_back(x);
      adjust_up(_con.size() - 1);
    }
 
    void adjust_up(size_t child)
    {
      size_t father = (child - 1) / 2;
      while (child > 0)
      {
        if (_con[father] < _con[child])
        {
          swap(_con[father], _con[child]);
          child = father;// 交换后向上走
          father = (child - 1) / 2;
        }
        else
        {
          break;
        }
      }
    }
 
    void pop()
    {
      std::swap(_con[0], _con[_con.size() - 1]);
      _con.pop_back();
 
      adjust_down(0);
    }
 
    void adjust_down(size_t father)
    {
      size_t child = father * 2 + 1;// 默认左孩子大
      while (child < _con.size())
      {
        if (child + 1 < _con.size() && _con[child] < _con[child + 1])//先考虑右孩子是否存在
        {
          child += 1;
        }
        if (_con[child] > _con[father])
        {
          swap(_con[child], _con[father]);
          father = child;
          child = father * 2 + 1;
        }
        else
        {
          break;
        }
      }
    }
 
    const T& top()
    {
      return _con[0];
    }
 
    bool empty()  const
    {
      return _con.empty();
    }
 
    size_t size() const
    {
      return _con.size();
    }
 
  private:
    Container _con;
  };
}

测试一下,Test.c:

#include "PriorityQueue.h"
 
void test_priority_queue()
{
  rtx::priority_queue<int> pq;
 
  pq.push(3);
  pq.push(1);
  pq.push(2);
  pq.push(5);
  pq.push(0);
  pq.push(8);
  pq.push(1);
 
  while (!pq.empty())
  {
    cout << pq.top() << " ";
    pq.pop();
  }
  cout << endl;
}
 
int main()
{
   test_priority_queue();
    
    return 0;
}


1.2 迭代器区间构造和无参构造

前面没写无参构造代码也跑出来了,先写下迭代器区间构造,和以前一样;

但这里不要直接push,push的时间是O(N*logN),建堆的时间是logN:

    template<class InputIterator>
    priority_queue(InputIterator first, InputIterator last)
      :_con(first,last) // 初始化列表初始化代替下面被注释的
    {
      //while (first != last)
      //{
      //  _con.push_back(*first);
      //  ++first;
      //}
      for (int i = (_con.size() - 1 - 1) / 2;i >= 0;--i)
      {
        adjust_down(i);
      }
    }

这样的话运行代码就发现前面的无参构造报错了:没有默认的构造函数使用。

所以我们还要写上无参构造,但实际里面什么都不用写:

    priority_queue()
    {}

测试一下,Test.c:

#include "PriorityQueue.h"
 
void test_priority_queue()
{
  rtx::priority_queue<int> pq;
 
  pq.push(3);
  pq.push(1);
  pq.push(2);
  pq.push(5);
  pq.push(0);
  pq.push(8);
  pq.push(1);
 
  while (!pq.empty())
  {
    cout << pq.top() << " ";
    pq.pop();
  }
  cout << endl;
 
  int arr[] = { 0,3,2,1,6,5,4,9,8,7 };
  rtx::priority_queue<int> heap(arr, arr + sizeof(arr) / sizeof(arr[0]));
  while (!heap.empty())
  {
    cout << heap.top() << " ";
    heap.pop();
  }
  cout << endl;
}
 
int main()
{
   test_priority_queue();
    
    return 0;
}


我们写死的最大问题是 —— 如果想按升序排列,就没办法排了。

我们这里写的用来排降序的大堆,如果想排升序我们需要写小堆,

C++ 有一种叫仿函数的东西,可以控制这些东西,下面先来介绍一下仿函数。

1.3 仿函数的介绍和使用

概念:使一个类的使用看上去像一个函数。可以像函数一样使用的对象,称为函数对象。

实现就是在类中重载 operator(),使得该类具备类似函数的行为,就是一个仿函数类了。

C语言优先级,() 圆括号使用形式为表达式 或 "作为函数形参列表的括号"

写一个最简单的仿函数:

#include<iostream>
using namespace std;
 
namespace rtx
{
  struct less
  {
    bool operator()(const int& left, const int& right) const
    {
      return left < right;
    }
  };
}
 
int main()
{
  int a = 10, b = 20;
 
  rtx::less lessCmp;
 
  cout << lessCmp(a, b) << endl;
 
  return 0;
}


仿函数(函数对象): 对象可以像调用函数一样去使用,

我们还可以使其能够支持泛型,我们加一个 template 模板,加一个 greater 仿函数:

#include<iostream>
using namespace std;
 
namespace rtx
{
  template<class T>
  struct less
  {
    bool operator()(const T& left, const T& right) const
    {
      return left < right;
    }
  };
 
  template<class T>
  struct greater
  {
    bool operator()(const T& left, const T& right) const
    {
      return left > right;
    }
  };
}
 
int main()
{
  int a = 10, b = 20;
 
  rtx::less<int> lessCmp;
  cout << lessCmp(a, b) << endl;
 
  rtx::greater<int> gtCmp;
  cout << gtCmp(a, b) << endl;
 
  return 0;
}

一个对象能像函数一样用,看上去很神奇,实际上调用的只是我们重载的 operator() 而已。

我们在C阶段不是学过函数指针么,用函数指针不行吗?

函数指针确实可以做到这些功能。但是在这里函数指针跟仿函数比,一点都方便。

在 C++ 里其实是非常不喜欢使用函数指针的,函数的指针在一些地方用起来非常复杂。

所以巨佬才发明了仿函数等,代替函数指针。

1.4 完整priority_queue代码:

我们现在传仿函数去比较那些数据(在模板再加一个参数),这样就不会写死了。

直接用库的less,PriorityQueue.h:

#pragma once
#include <iostream>
#include <vector> 
#include <algorithm>
#include <functional> // greater和less的头文件
using namespace std;
 
namespace rtx
{
  // Compare进行比较的仿函数 less->大堆
  // Compare进行比较的仿函数 greater->小堆
  template<class T, class Container = vector<T>,class Compare = less<T>>
  class priority_queue
  {
  public:
    priority_queue()
    {}
 
    template<class InputIterator>
    priority_queue(InputIterator first, InputIterator last)
      :_con(first,last) // 初始化列表初始化代替下面被注释的
    {
      //while (first != last)
      //{
      //  _con.push_back(*first);
      //  ++first;
      //}
      for (int i = (_con.size() - 1 - 1) / 2;i >= 0;--i)
      {
        adjust_down(i);
      }
    }
 
    void push(const T& x)
    {
      _con.push_back(x);
      adjust_up(_con.size() - 1);
    }
 
    void adjust_up(size_t child)
    {
      Compare cmp;
 
      size_t father = (child - 1) / 2;
      while (child > 0)
      {
        //if (_con[father] < _con[child])
        if (cmp(_con[father], _con[child]))//仿函数想函数一样使用
        {
          swap(_con[father], _con[child]);
          child = father;// 交换后向上走
          father = (child - 1) / 2;
        }
        else
        {
          break;
        }
      }
    }
 
    void pop()
    {
      std::swap(_con[0], _con[_con.size() - 1]);
      _con.pop_back();
 
      adjust_down(0);
    }
 
    void adjust_down(size_t father)
    {
      Compare cmp;
      size_t child = father * 2 + 1;// 默认左孩子大
      while (child < _con.size())
      {
        //if (child + 1 < _con.size() && _con[child] < _con[child + 1])
        if (child + 1 < _con.size() && cmp(_con[child], _con[child + 1]))//先考虑右孩子是否存在
        {
          child += 1;
        }
        //if (_con[child] > _con[father]) 这里写了大于,所以是不好的(下次一定)
        if (cmp(_con[father], _con[child]))
        {
          swap(_con[child], _con[father]);
          father = child;
          child = father * 2 + 1;
        }
        else
        {
          break;
        }
      }
    }
 
    const T& top()
    {
      return _con[0];
    }
 
    bool empty()  const
    {
      return _con.empty();
    }
 
    size_t size() const
    {
      return _con.size();
    }
 
  private:
    Container _con;
  };
}

测试:直接传greater 建个小堆看看:

#include "PriorityQueue.h"
 
void test_priority_queue()
{
  rtx::priority_queue<int,vector<int>, greater<int>> pq;
 
  pq.push(3);
  pq.push(1);
  pq.push(2);
  pq.push(5);
  pq.push(0);
  pq.push(8);
  pq.push(1);
 
  while (!pq.empty())
  {
    cout << pq.top() << " ";
    pq.pop();
  }
  cout << endl;
 
  int arr[] = { 0,3,2,1,6,5,4,9,8,7 };
  rtx::priority_queue<int, vector<int>, greater<int>> heap(arr, arr + sizeof(arr) / sizeof(arr[0]));
  while (!heap.empty())
  {
    cout << heap.top() << " ";
    heap.pop();
  }
  cout << endl;
}
 
int main()
{
   test_priority_queue();
    
    return 0;
}

值得一提的是我们这里传的是类型,因为是类模板,

sort里传的是对象(用匿名对象就好),是函数模板:

1.5 相关笔试选择题

1. STL中的priority_queue使用的底层数据结构是什么( )

A.queue

B.heap

C.deque

D.vector

2. 下列代码的运行结果是( )

int main()
{
    priority_queue<int> a;
    priority_queue<int, vector<int>, greater<int> > c;
    priority_queue<string> b;
    for (int i = 0; i < 5; i++)
    {
        a.push(i);
        c.push(i);
    }
    while (!a.empty())
    {
        cout << a.top() << ' ';
        a.pop();
    }
    cout << endl;
    while (!c.empty())
    {
        cout << c.top() << ' ';
        c.pop();
    }
    cout << endl;
    b.push("abc");
    b.push("abcd");
    b.push("cbd");
    while (!b.empty())
    {
        cout << b.top() << ' ';
        b.pop();
    }
    cout << endl;
    return 0;
}

A.4 3 2 1 0 0 1 2 3 4 cbd abcd abc


B.0 1 2 3 4 0 1 2 3 4 cbd abcd abc


C.4 3 2 1 0 4 3 2 1 0 abc abcd cbd


D.0 1 2 3 4 4 3 2 1 0 cbd abcd abc

3. 以下说法正确的是( )

A.deque的存储空间为连续空间

B.list迭代器支持随机访问

C.如果需要高效的随机存取,还要大量的首尾的插入删除则建议使用deque

D.vector容量满时,那么插入元素时只需增加当前元素个数的内存即可

4. 仿函数比起一般函数具有很多优点,以下描述错误的是( )

A.在同一时间里,由某个仿函数所代表的单一函数,可能有不同的状态

B.仿函数即使定义相同,也可能有不同的类型

C.仿函数通常比一般函数速度快

D.仿函数使程序代码变简单

5. 假设cont是一个Container 的示例,里面包含数个元素,那么当CONTAINER为:

1.vector 2.list 3.deque 会导致下面的代码片段崩溃的Container 类型是( )

int main()
{
  Container cont = { 1, 2, 3, 4, 5 };
  Container::iterator iter, tempIt;
  for (iter = cont.begin(); iter != cont.end();)
  {
    tempIt = iter;
    ++iter;
    cont.erase(tempIt);
  }
}

A.1, 2

B.2, 3

C.1, 3

D.1, 2, 3

答案:

1. D

分析:优先级队列priority_queue底层采用vector容器作为底层数据结构

2. A


分析:优先级队列默认情况为:大堆,这是解题的关键


 priority_queue a; //a是大堆


 priority_queue, greater > c; //c指定了比较规则,是小堆


 priority_queue b; //b是大堆


 因此分别建堆的过程是建立a大堆,c小堆,b大堆


 所以出队的顺序就按照其优先级大小出队


3. C


A.deque底层总体为不连续空间


B.不支持,因为底层是一个个的不连续节点


C.正确


D.一般会以容量的2倍扩充容量,这是为了减少扩容的次数,减少内存碎片


4. C


A.仿函数是模板函数,可以根据不同的类型代表不同的状态


B.仿函数是模板函数,可以有不同类型


C.仿函数是模板函数,其速度比一般函数要慢,故错误


D.仿函数在一定程度上使代码更通用,本质上简化了代码


5. C


分析:此题主要考察cont.erase(tmpit)删除数据之后,迭代器失效相关问题


本题重点要关注的是底层实现


vector、deque底层都是用了连续空间,所以虽然++iter迭代器了,但是erase(tempit)以后


底层是连续空间,删除会挪动数据,最终导致iter意义变了,已失效了。


而list,不是连续空间,删除以后tempIt虽然失效了,但是不影响iter。

从C语言到C++_20(仿函数+优先级队列priority_queue的模拟实现+反向迭代器)(下):https://developer.aliyun.com/article/1521893?spm=a2c6h.13148508.setting.22.712b4f0eDngT44

目录
相关文章
|
28天前
|
存储 算法 调度
【C++打怪之路Lv11】-- stack、queue和优先级队列
【C++打怪之路Lv11】-- stack、queue和优先级队列
29 1
|
1月前
|
C语言 C++
C 语言的关键字 static 和 C++ 的关键字 static 有什么区别
在C语言中,`static`关键字主要用于变量声明,使得该变量的作用域被限制在其被声明的函数内部,且在整个程序运行期间保留其值。而在C++中,除了继承了C的特性外,`static`还可以用于类成员,使该成员被所有类实例共享,同时在类外进行初始化。这使得C++中的`static`具有更广泛的应用场景,不仅限于控制变量的作用域和生存期。
48 10
|
1月前
|
C语言 C++
实现两个变量值的互换[C语言和C++的区别]
实现两个变量值的互换[C语言和C++的区别]
18 0
|
3月前
|
C++ 容器
【C/C++笔记】迭代器
【C/C++笔记】迭代器
25 1
|
2月前
|
编译器 C语言 C++
从C语言到C++
本文档详细介绍了C++相较于C语言的一些改进和新特性,包括类型检查、逻辑类型 `bool`、枚举类型、可赋值的表达式等。同时,文档还讲解了C++中的标准输入输出流 `cin` 和 `cout` 的使用方法及格式化输出技巧。此外,还介绍了函数重载、运算符重载、默认参数等高级特性,并探讨了引用的概念及其应用,包括常引用和引用的本质分析。以下是简要概述: 本文档适合有一定C语言基础的学习者深入了解C++的新特性及其应用。
|
4月前
|
程序员 编译器 C语言
云原生部署问题之C++中的nullptr相比C语言中的NULL优势如何解决
云原生部署问题之C++中的nullptr相比C语言中的NULL优势如何解决
53 10
|
3月前
|
存储 安全 程序员
【C/C++笔记】迭代器范围
【C/C++笔记】迭代器范围
65 0
|
4月前
|
存储 算法 Java
【C++】优先级队列priority_queue模拟实现&&仿函数
【C++】优先级队列priority_queue模拟实现&&仿函数
38 1
|
4月前
|
编译器 C语言 C++
C++从遗忘到入门问题之C++持从C语言的过渡问题如何解决
C++从遗忘到入门问题之C++持从C语言的过渡问题如何解决
|
4月前
|
C++ 容器
【C++】string类的使用①(迭代器接口begin,end,rbegin和rend)
迭代器接口是获取容器元素指针的成员函数。`begin()`返回首元素的正向迭代器,`end()`返回末元素之后的位置。`rbegin()`和`rend()`提供反向迭代器,分别指向尾元素和首元素之前。C++11增加了const版本以供只读访问。示例代码展示了如何使用这些迭代器遍历字符串。