索引优先队列

本文介绍了一种在内存有限条件下统计广告投放展示与点击次数的算法。通过使用词典存储和时间队列的方式,实现了对大量广告日志的有效处理。该方法能够确保即使在高并发环境下也能准确记录广告的展示及点击情况。

一次广告投放会话(session)中,服务器会分别打印推送日志(push)、展示日志(show)(如果广告成功展示)和点击日志(click)(如果广告被点击),一次广告会话由唯一id标记,一个广告可能被点击多次,也可能不被点击。由于广告服务器的并发处理,同一id对应的推送日志,展示日志,点击日志可能不会按照原本的顺序出现(原本顺序指先推送后展示最后点击)。设计算法在内存有限的情况下,统计每一次广告投放的展示与点击次数。约定:展示日志和点击日志的时间差不超过10分钟,广告推送后20分钟后产生的点击视为无效不计入点击次数。

考虑到需要对推送日志、展示日志、和点击日志按id进行匹配,可以采用词典存储,由于日志很多,不能都存入词典,可以采用时间队列将推送时间已超过当前时间20分钟的日志从词典中剔除,并输出展示与点击次数。

具体可以采用词典{id:(push, show, click)}和有限队列<push.time, id>来实现。

Code

class IndexedPriorityQueue:

  def __init__(self):

    self.container = {}

    self.queue = []

 

  def top(self):

    if len(self.queue) <= 0:

      raise IndexError("Queue is EmptyNow!")

    key_index = self.queue[0][1]

    return self.container[key_index]

 

  def top_key(self):

    if len(self.queue) <= 0:

      raise IndexError("Queue is EmptyNow!")

    return self.queue[0]

 

  def pop(self):

    if len(self.queue) <= 0:

      raise IndexError("Queue is EmptyNow!")

    _, index_key = heapq.heappop(self.queue)

    e = self.container[index_key]

    del self.container[index_key]

    return e

 

  def push(self, e, index_key, queue_key):

    if index_key in self.container:

      raise IndexError("Duplicated keyserror!")

    heapq.heappush(self.queue, (queue_key,index_key))

        self.container[index_key] = e

### 3.1 索引优先队列的实现思路 索引优先队列是一种特殊的优先队列,它允许通过索引来访问和操作队列中的元素。与传统的优先队列不同,索引优先队列不仅支持插入和删除操作,还支持更新操作,这使得它在某些应用场景中非常有用,例如图算法中的最短路径问题。 #### 数据结构设计 索引优先队列的设计需要维护三个主要数组: - `pq[]`:存储堆中的元素索引。 - `qp[]`:反向映射,存储每个索引在堆中的位置。 - `keys[]`:存储每个索引对应的优先级值。 为了实现索引优先队列,首先需要定义这些数组并初始化它们。堆的根节点从索引 `1` 开始,这样可以方便地进行堆操作。 ```java public class IndexMinPQ<Key extends Comparable<Key>> { private int maxN; // 最大容量 private int n; // 当前元素数量 private int[] pq; // 堆数组 private int[] qp; // 反向映射数组 private Key[] keys; // 优先级值数组 public IndexMinPQ(int maxN) { this.maxN = maxN; n = 0; pq = new int[maxN + 1]; qp = new int[maxN]; keys = (Key[]) new Comparable[maxN]; for (int i = 0; i < maxN; i++) { qp[i] = -1; // 初始化为-1,表示该索引不在队列中 } } } ``` #### 插入元素 插入元素时,需要将元素的索引和优先级值分别存储到 `pq` 和 `keys` 数组中,并更新反向映射数组 `qp`。插入操作后,需要通过上浮操作来维护堆的性质。 ```java public void insert(int i, Key key) { if (contains(i)) { throw new IllegalArgumentException("索引 " + i + " 已经存在于队列中"); } n++; qp[i] = n; pq[n] = i; keys[i] = key; swim(n); } ``` #### 删除最小元素 删除最小元素的操作实际上是删除堆顶元素。这个过程包括将堆顶元素与最后一个元素交换,然后调整堆的结构以维持堆的性质。 ```java public int delMin() { if (n == 0) { throw new NoSuchElementException("队列为空"); } int minIndex = pq[1]; exch(1, n); n--; sink(1); qp[minIndex] = -1; // 标记为不在队列中 return minIndex; } ``` #### 更新元素 更新元素的优先级时,需要找到该元素在堆中的位置,并更新其优先级值,然后通过上浮或下沉操作来恢复堆的性质。 ```java public void decreaseKey(int i, Key key) { if (!contains(i)) { throw new NoSuchElementException("索引 " + i + " 不在队列中"); } if (keys[i].compareTo(key) <= 0) { throw new IllegalArgumentException("新键不小于当前键"); } keys[i] = key; swim(qp[i]); } ``` #### 辅助方法 - **上浮操作**:将指定位置的元素向上移动,直到满足堆的性质。 - **下沉操作**:将指定位置的元素向下移动,直到满足堆的性质。 - **交换操作**:交换堆中两个位置的元素。 - **检查索引是否存在**:检查指定的索引是否存在于队列中。 ```java private void swim(int k) { while (k > 1 && greater(k / 2, k)) { exch(k, k / 2); k = k / 2; } } private void sink(int k) { while (2 * k <= n) { int j = 2 * k; if (j < n && greater(j, j + 1)) { j++; } if (!greater(k, j)) { break; } exch(k, j); k = j; } } private boolean greater(int i, int j) { return keys[pq[i]].compareTo(keys[pq[j]]) > 0; } private void exch(int i, int j) { int swap = pq[i]; pq[i] = pq[j]; pq[j] = swap; qp[pq[i]] = i; qp[pq[j]] = j; } public boolean contains(int i) { return qp[i] != -1; } ``` 通过这些方法,可以实现一个完整的索引优先队列。这种方法不仅提高了操作的效率,还使得程序更加简洁和易于理解。索引优先队列在处理需要频繁更新优先级的应用场景中非常有用,例如任务调度和图算法中的最短路径问题。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值