查看: 2214| 回复: 15
跳转到指定楼层
上一主题 下一主题
收起左侧

[数组] 请教一道算法题

全局:

注册一亩三分地论坛,查看更多干货!

您需要 登录 才可以下载或查看附件。没有帐号?注册账号

x
昨天面了一道题,感觉自己可能理解有问题,或者是没有问清楚,到现在也没有想清楚该怎么实现,请教各位大佬。

给你一个固定大小的memory buffer,比如4MB,让实现2个动态增长的Queue. 需要O(1) 实现 insert 和 read, 也就是写入和取出. 只能实现int array,要保证空间充分使用。
我当时给的solution。
1: 建int array,存取数据,需要时,然后动态扩容。面试官说这个空间利用率可能不高。
2: 建一个大的array,第一个queue从头存取,第二个queue从尾存取,分布用两个pointer来维护存存的位置,然后跑例子的时候,有bug,面试官也否定了这个方法。

不知道有没有其他的方法可以解决,或者我可能miss掉了一些重要的点。 谢谢。

评分

参与人数 1大米 +10 收起 理由
14417335 + 10

查看全部评分


上一篇:想问下大家都是怎么刷题的
下一篇:找一起做kaggle的队友
推荐
magicsets 2021-3-19 14:36:33 | 只看该作者
全局:
可以把4MB分成1024个4kB的内存块,然后以4kB内存块为单位进行动态分配与释放。

那么对于任一个Queue来说,它最“浪费”的情况就是首尾两个内存块为空,也就是8kB,只有 4MB的 0.2% —— 这种情况下我们可以认为内存是被高效利用的。

另一方面的考虑是动态内存分配的额外开销,注意到一个int类型是4 bytes,那么当我们每处理4kB / 4 bytes = 1024个元素的时候才会有一次内存分配(内存分配其实本来就是O(1)) 的额外开销,那么平摊后可以忽略不计 —— 也就是说,使用4kB内存块的方法性能应该是和一开始就开个足够大的数组不需要resize的性能没什么差别

参考代码:
  1. #include <cstdint>
  2. #include <iostream>
  3. #include <memory>
  4. #include <stack>
  5. #include <stdexcept>
  6. #include <utility>

  7. // CPU分支预测hint
  8. #define UNLIKELY(x) __builtin_expect(!!(x), 0)

  9. #define CHECK(cond) \
  10.   if (UNLIKELY(!(cond))) { throw std::runtime_error("CHECK failed: " #cond); }

  11. // 设置一个数据块为4kB
  12. static constexpr int64_t kBlockSize = 4096;

  13. // 用于管理内存池的抽象
  14. class Arena {
  15. public:
  16.   explicit Arena(int64_t size)
  17.       : memory_buffer_(std::make_unique<char[]>(size)) {
  18.     // size要求是4kB的整数倍
  19.     CHECK(size % kBlockSize == 0);
  20.     // 如果size是4MB的话,那么就有4MB / 4kB = 1024个内存块
  21.     // 初始化只有一次,并且时间可以忽略不计
  22.     // 更理想的是bitmap来记录可用块信息,那么只需要初始化1024 bits = 128 bytes
  23.     for (int64_t i = size / kBlockSize - 1; i >= 0; --i) {
  24.       available_blocks_.push(i);
  25.     }
  26.   }

  27.   // 分配一个数据块
  28.   template <typename BlockType>
  29.   std::unique_ptr<BlockType> Allocate() {
  30.     CHECK(!available_blocks_.empty());
  31.     const int64_t block_id = available_blocks_.top();
  32.     std::cout << "Allocate block " << block_id << "\n";
  33.     available_blocks_.pop();
  34.     return std::make_unique<BlockType>(
  35.         memory_buffer_.get() + block_id * kBlockSize, block_id, this);
  36.   }

  37.   // 回收一个数据块
  38.   void Release(int64_t block_id) {
  39.     std::cout << "Release block " << block_id << "\n";
  40.     available_blocks_.push(block_id);
  41.   }

  42. private:
  43.   // 总体的内存块 (例如4MB)
  44.   const std::unique_ptr<char[]> memory_buffer_;

  45.   // 理想的实现是用bitmap来维护可用内存块,不过代码量比较大
  46.   std::stack<int64_t> available_blocks_;
  47. };

  48. // 4kB数据块的封装
  49. class Block {
  50. public:
  51.   Block(void* block_data, int64_t block_id, Arena* arena)
  52.       : block_data_(block_data), block_id_(block_id), arena_(arena) {}

  53.   // RAII机制回收数据块
  54.   ~Block() { arena_->Release(block_id_); }

  55.   // 读取某个位置的数据
  56.   template <typename T>
  57.   T Get(int64_t index) const {
  58.     return static_cast<const T*>(block_data_)[index];
  59.   }

  60.   // 设置某个位置的数据
  61.   template <typename T>
  62.   void Set(int64_t index, T value) const {
  63.     static_cast<T*>(block_data_)[index] = value;
  64.   }

  65.   std::unique_ptr<Block>& next_block() { return next_block_; }

  66. private:
  67.   // 每个数据块需要额外32 bytes的内存指针 + block id + next pointer
  68.   // 这点开销比起4kB不到1%所以可以忽略不计
  69.   void* block_data_;
  70.   int64_t block_id_;
  71.   Arena* arena_;
  72.   std::unique_ptr<Block> next_block_;
  73. };

  74. // Queue的实现
  75. template <typename T>
  76. class Queue {
  77. public:
  78.   explicit Queue(Arena* arena)
  79.       : arena_(arena),
  80.         block_capacity_(kBlockSize / sizeof(T)) {
  81.     CHECK(block_capacity_ > 0);
  82.   }

  83.   // 往队列头添加一个新元素
  84.   void PushFront(T value) {
  85.     if (UNLIKELY(front_ == nullptr)) {
  86.       // Block链表为空
  87.       back_ = arena_->Allocate<Block>();
  88.       front_ = back_.get();
  89.     } else if (UNLIKELY(head_exclusive_ >= block_capacity_)) {
  90.       // front_已经满了
  91.       front_->next_block() = arena_->Allocate<Block>();
  92.       front_ = front_->next_block().get();
  93.       head_exclusive_ = 0;
  94.     }
  95.     ++size_;
  96.     front_->Set(head_exclusive_++, value);
  97.   }

  98.   // 从队列尾取出一个元素
  99.   T PopBack() {
  100.     if (UNLIKELY(back_.get() == front_)) {
  101.       // 链表里只有一个数据块的情况
  102.       CHECK(tail_inclusive_ < head_exclusive_);
  103.     } else if (UNLIKELY(tail_inclusive_ >= block_capacity_)) {
  104.       // back_没有剩余元素了,对其回收并移动到下一个数据块
  105.       back_ = std::move(back_->next_block());
  106.       tail_inclusive_ = 0;
  107.       CHECK(back_ != nullptr);
  108.     }
  109.     --size_;
  110.     return back_->Get<T>(tail_inclusive_++);
  111.   }

  112.   size_t size() const { return size_; }
  113.   bool IsEmpty() const { return size_ == 0; }

  114. private:
  115.   Arena* const arena_;
  116.   const int64_t block_capacity_;
  117.   int64_t size_ = 0;

  118.   // 这里back_是链表头,但是是队列尾
  119.   std::unique_ptr<Block> back_;
  120.   int64_t tail_inclusive_ = 0;

  121.   // front_是链表尾,但是是队列头
  122.   Block* front_ = nullptr;
  123.   int64_t head_exclusive_ = 0;
  124. };

  125. // 测试代码
  126. int main(int argc, char* argv[]) {
  127.   Arena arena(/*size=*/4 * 1024 * 1024);

  128.   Queue<int> a(&arena);
  129.   Queue<int64_t> b(&arena);

  130.   for (int i = 1; i <= 10000; ++i) {
  131.     a.PushFront(i);
  132.     b.PushFront(i);
  133.   }
  134.   while (!a.IsEmpty()) {
  135.     b.PushFront(a.PopBack() * 10);
  136.   }

  137.   int64_t b_sum = 0;
  138.   while (!b.IsEmpty()) {
  139.     b_sum += b.PopBack();
  140.   }
  141.   std::cout << "b sum = " << b_sum << "\n";

  142.   return 0;
  143. }
复制代码

评分

参与人数 2大米 +5 收起 理由
Andrew007 + 3 感谢大神
funmastermike + 2 给你点个赞!

查看全部评分

回复

使用道具 举报

推荐
twtypsj 2021-3-19 04:35:12 | 只看该作者
全局:
本帖最后由 twtypsj 于 2021-3-19 04:53 编辑
Andrew007 发表于 2021-3-19 04:24
非常感谢回复。有一个问题,就是这个array里面只能放Integer,不能有其他的信息,跟面试官确认了很多遍, ...

其实写Node的初衷就是为了方便访问,如果不能写Node的话,我觉得可以用index%2来区分是数据还是next指针,麻烦了一点但是还是可以正常跑的,我可是改一下代码再看看。

这里做了一个延伸,不限制queue的数量,从参数里指定。




  1. #include <iostream>
  2. #include <vector>
  3. using namespace std;


  4. struct MultiQueue
  5. {
  6.     int tail;
  7.     int capacity;
  8.     int queueCount;
  9.     vector<int> data;
  10.     vector<int> heads;
  11.     vector<int> tails;
  12.     MultiQueue(int count, int cap)
  13.     {
  14.         queueCount = count;
  15.         capacity = cap;
  16.         tail = 0;
  17.         data.resize(cap*2);
  18.         heads.assign(count, -1);
  19.         tails.assign(count, -1);
  20.     }

  21.     bool push(int index, int val)
  22.     {
  23.         if(index<0||index>=queueCount||tail*2>=capacity) return false;
  24.         data[tail*2] = val;
  25.         data[tail*2+1] = -1;
  26.         if(heads[index] == -1)
  27.             heads[index] = tail;
  28.         if(tails[index] != -1) data[tails[index]*2+1] = tail;
  29.         tails[index] = tail;
  30.         tail++;
  31.         return true;
  32.     }
  33.      
  34.     bool pop(int index, int& res)
  35.     {
  36.         if(index<0||index>=queueCount||heads[index]==-1||tails[index]==-1) return false;
  37.         res = data[heads[index]*2];
  38.         heads[index] = data[heads[index]*2+1];
  39.         return true;
  40.     }
  41.      
  42. };

  43. int main()
  44. {
  45.     int queueCount = 3;
  46.     MultiQueue q(queueCount, 1024);
  47.     q.push(0, 0);
  48.     q.push(1, 1);
  49.     q.push(1, 3);
  50.     q.push(0, 2);
  51.     q.push(0, 4);
  52.     q.push(2, 2);
  53.     for(int i=0;i<queueCount; ++i)
  54.     {
  55.         int val = -1;
  56.         while(q.pop(i, val))
  57.             cout << val << " ";
  58.         cout << endl;
  59.     }
  60.     return 0;
  61. }

复制代码


评分

参与人数 2大米 +4 收起 理由
blackrose + 1 赞一个
Andrew007 + 3 非常感谢,太牛了!

查看全部评分

回复

使用道具 举报

推荐
twtypsj 2021-3-19 03:56:10 | 只看该作者
全局:
不确定我是否理解了题意,大概是这样的思路
做一个数据结构,用来保存数据值和下一个数据的index
做一个数据结构,用来保存整个数组,其中记录heads 和tails,分别用来记录queue的起始index和结尾index。insert就保存数据到tails的位置,同时修改把tails的值。 read就读取heads位置的值,同时修改heads的值为next。
因为不清楚是不是要清除数据,所以这里没有考虑。

贴个代码吧,也不知道对不对,仅供讨论。
老规矩,求米:)


  1. #include <iostream>
  2. #include <vector>
  3. using namespace std;

  4. struct Node
  5. {
  6.     int val;
  7.     int next;
  8.     Node(int val, int next){this->val = val;this->next = next;}
  9.     Node(){}
  10.     Node& operator=(Node n)
  11.     {
  12.         this->val = n.val;
  13.         this->next = n.next;
  14.         return *this;
  15.     }
  16. };

  17. struct MultiQueue
  18. {
  19.     int tail;
  20.     int capacity;
  21.     int queueCount;
  22.     vector<Node> data;
  23.     vector<int> heads;
  24.     vector<int> tails;
  25.     MultiQueue(int count, int cap)
  26.     {
  27.         queueCount = count;
  28.         capacity = cap;
  29.         tail = 0;
  30.         data.resize(cap);
  31.         heads.assign(count, -1);
  32.         tails.assign(count, -1);
  33.     }
  34.    
  35.     bool push(int index, int val)
  36.     {
  37.         if(index<0||index>=queueCount||tail>=capacity) return false;
  38.         data[tail] = Node(val, -1);
  39.         if(heads[index] == -1)
  40.             heads[index] = tail;
  41.         if(tails[index] != -1) data[tails[index]].next = tail;
  42.         tails[index] = tail;
  43.         tail++;
  44.         return true;
  45.     }
  46.    
  47.     bool pop(int index, int& res)
  48.     {
  49.         if(index<0||index>=queueCount||heads[index]==-1||tails[index]==-1) return false;
  50.         res = data[heads[index]].val;
  51.         heads[index] = data[heads[index]].next;
  52.         return true;
  53.     }
  54.    
  55. };

  56. int main()
  57. {
  58.     int queueCount = 2;
  59.     MultiQueue q(2, 1024);
  60.     q.push(0, 0);
  61.     q.push(1, 1);
  62.     q.push(1, 3);
  63.     q.push(0, 2);
  64.     q.push(0, 4);
  65.     for(int i=0;i<queueCount; ++i)
  66.     {
  67.         int val = -1;
  68.         while(q.pop(i, val))
  69.             cout << val << " ";
  70.         cout << endl;
  71.     }
  72.     return 0;
  73. }
复制代码

评分

参与人数 2大米 +3 收起 理由
blackrose + 1 赞一个
Andrew007 + 2 给你点个赞!

查看全部评分

回复

使用道具 举报

🔗
maandma 2021-3-19 01:37:20 | 只看该作者
全局:
小菜鸡来围观答案,double linked list这种因为不算array所以不能考虑是不是?
回复

使用道具 举报

🔗
dorisH 2021-3-19 02:11:32 | 只看该作者
全局:
用所有memory做个循环数组?可以充分利用所有空间,然后用两个pointer表示当前queue的head和tail
回复

使用道具 举报

🔗
 楼主| Andrew007 2021-3-19 02:18:04 | 只看该作者
全局:
这个也是我第二个solution,但是面试官明显不接受这个,一堆challenge,尝试让我用其他方法。
回复

使用道具 举报

🔗
 楼主| Andrew007 2021-3-19 02:18:24 | 只看该作者
全局:
maandma 发表于 2021-3-19 01:37
小菜鸡来围观答案,double linked list这种因为不算array所以不能考虑是不是?

只能用 Int array
回复

使用道具 举报

🔗
 楼主| Andrew007 2021-3-19 02:18:40 | 只看该作者
全局:
dorisH 发表于 2021-3-19 02:11
用所有memory做个循环数组?可以充分利用所有空间,然后用两个pointer表示当前queue的head和tail

这个也是我第二个solution,但是面试官明显不接受这个,一堆challenge,尝试让我用其他方法。
回复

使用道具 举报

🔗
 楼主| Andrew007 2021-3-19 04:24:05 | 只看该作者
全局:
twtypsj 发表于 2021-3-19 03:56
不确定我是否理解了题意,大概是这样的思路
做一个数据结构,用来保存数据值和下一个数据的index
做一个 ...

非常感谢回复。有一个问题,就是这个array里面只能放Integer,不能有其他的信息,跟面试官确认了很多遍,所以感觉这个Node应该是不支持的。已加米。
回复

使用道具 举报

🔗
funmastermike 2021-3-19 04:37:31 | 只看该作者
全局:
circular queue?
回复

使用道具 举报

您需要登录后才可以回帖 登录 | 注册账号
隐私提醒:
  • ☑ 禁止发布广告,拉群,贴个人联系方式:找人请去🔗同学同事飞友,拉群请去🔗拉群结伴,广告请去🔗跳蚤市场,和 🔗租房广告|找室友
  • ☑ 论坛内容在发帖 30 分钟内可以编辑,过后则不能删帖。为防止被骚扰甚至人肉,不要公开留微信等联系方式,如有需求请以论坛私信方式发送。
  • ☑ 干货版块可免费使用 🔗超级匿名:面经(美国面经、中国面经、数科面经、PM面经),抖包袱(美国、中国)和录取汇报、定位选校版
  • ☑ 查阅全站 🔗各种匿名方法

本版积分规则

>
快速回复 返回顶部 返回列表