Loading...
墨滴

高性能架构探索

2021/07/14  阅读:37  主题:默认主题

蓄水池抽样

问题

1、给定一个数据流,数据流长度N很大,且N直到处理完所有数据之前都不可知,请问如何在只遍历一遍数据(O(N))的情况下,能够随机选取出m个不重复的数据

2、在不知道文件行数的情况下,如何在只遍历一遍文件的情况下,随机选取出m行

分析

看到此种问题,我们的第一想法是,把数据流中的数据保存起来,然后通过把数据流中的数据存储起来,然后进行随机获取,我们以leetcode中的某个题目为例,代码如下:

class Solution {
public:
    /** @param head The linked list's head.
        Note that the head is guaranteed to be not null, so it contains at least one node. */
    Solution(ListNode* head) {
        auto h = head;
        while (h) {
            v.push_back(h->val);
            h = h->next;
        }
    }
    
    int getRandom() {
        return v[random() % v.size()];
    }
    private:
      std::vector<int> v;
};

此解法基本上满足了随机获取链表节点值的要求,但是如果再加一个条件,即链表很长,乃至内存中存储不了,此时应该怎么做呢?

解决

这就涉及到蓄水池算法。

蓄水池抽样

蓄水池抽样是一系列随机算法,用于在不替换的情况下,从一个未知大小n的总体中选择一个简单的随机样本(k个项目),只需对这些项目进行一次遍历。总体n的大小对于算法来说是未知的,并且通常对于所有n个项来说都太大而无法放入主内存。随着时间的推移,总体将显示给算法,并且算法不能回顾以前的项目。在任何时候,算法的当前状态必须允许提取一个简单的随机样本,而不替换迄今为止看到的部分总体的大小k。

算法思路大致如下:

  • 如果接收的数据量小于m,则依次放入蓄水池。
  • 当接收到第i个数据时,i >= m,在[0, i]范围内取以随机数d,若d的落在[0, m-1]范围内,则用接收到的第i个数据替换蓄水池中的第d个数据。
  • 重复上一个步骤

证明

为了证明这个解是完全有效的,我们必须证明0<=i<n的任何项流[i]在最终储层[]中的概率是k/n。让我们把证据分为两种情况,因为前k项的处理方式不同。

情况1:对于最后n-k个流项,即,对于流[i],其中k<=i<n

对于每一个这样的流项流[i],我们从0到i选取一个随机索引,如果选取的索引是前k个索引之一,我们将选取索引处的元素替换为流[i]

为了简化证明,让我们先考虑最后一个项目。最后一个项目在最终库中的概率=为最后一个项目选取前k个索引之一的概率=k/n(从大小为n的列表中选取k个项目之一的概率)

现在让我们考虑第二个最后一个项目。最后第二项在最终储层中的概率[]=[在流[n-2]的迭代中选取前k个索引之一的概率]X[在流[n-1]的迭代中选取的索引与在流[n-2]中选取的索引不同的概率]=[k/(n-1)]*[(n-1)/n]=k/n。

类似地,我们可以从流[n-1 ]到流[k]中考虑所有流项的其他项,并推广证明。

情况2:对于前k个流项,即,对于流[i],其中0<=i<k

第一k个项目最初被复制到库[],并且可以在稍后的流[k]到流[n]的迭代中被移除。

来自流[0..k-1]的项目在最终数组中的概率=当项目流[k]、流[k+1]、….时项目未被拾取的概率…。考虑流[n-1]=[k/(k+1)]x[(k+1)/(k+2)]x[(k+2)/(k+3)]x…x[(n-1)/n]=k/n

实现

仍然以leetcode中此题为例,随机获取一个链表中的一个节点值,注意,此处k = 1:

class Solution {
public:
    Solution(ListNode* head) {
        this->head = head;
    }
    
    int getRandom() {
        ListNode* phead = this->head;
        int val = phead->val;
        int count = 1;
        while (phead){
            if (rand() % count++ == 0)
                val = phead->val;
            phead = phead->next;
        }
        return val;
    }
    ListNode* head;
};

那么 如果要获取链表中的k(k != 1)个值,时候,又该怎么实现呢? 此时,需要遍历链表的前k个节点,将前k个节点的值存储在数组中,然后从第k + 1个节点开始遍历链表,从中获取值,代码如下:

class Solution {
public:
    Solution(ListNode* head) {
        this->head = head;
    }
    
    std::vector<int> getRandom(int count) {
        ListNode* phead = this->head;
        int i = 0;
        int k = count;
        std::vector<int> res;
        while (phead && k--) {
          res.push_back(phead->val);
        }
  
        k = count;
        while (phead) {
          int rd = rand() % k++;
          if (rd < k) {
            res[rd] = phead->val;         
          }
  
          phead = phead->next;
        }
        return res;
    }
    ListNode* head;
};

高性能架构探索

2021/07/14  阅读:37  主题:默认主题

作者介绍

高性能架构探索