可随机访问(<O(n))和动态增删的(=O(1))的队列结构

引言

最近的工作有个需求,做视频直播的评论列表。 本人从未有过这样的工作经验,分析一下觉得需求也有些不好搞:

  1. 评论不是实时返回的,是客户端轮询接口。(大概两秒一次的频率)
  2. 评论是动态添加上去的,所以数据结构需要尾插入。
  3. 评论不能无限展示,达到一定数目需要头部删除,维持总量基本不变。

乍一看头删除尾插入的模式,链表LinkedList很完美。

遇到的问题

实际使用时,因为评论内容使用RecyclerView展示,所以onBindViewHolder()时会需要随机读取数据结构中的数据。 所以链表的劣势就无可回避了,当评论数据量到达200+时,RecyclerView评论列表滑动会有肉眼可见的卡顿。

众所周知,链表是链式结构,我们阅读LinkedList源码也会发现:

public E get(int index) {
        checkElementIndex(index);
        return node(index).item;
    }

    
/**
 * Returns the (non-null) Node at the specified element index.
 */
Node<E> node(int index) {
    // assert isElementIndex(index);

    if (index < (size >> 1)) {
        Node<E> x = first;
        for (int i = 0; i < index; i++)
            x = x.next;
        return x;
    } else {
        Node<E> x = last;
        for (int i = size - 1; i > index; i--)
            x = x.prev;
        return x;
    }
}


复制代码

node(int)通过二分法从头或从末尾遍历获取当前下标对应的元素。最坏情况下,需要size/2次访问才能拿到我们指定的元素。 RecyclerView滑动时,一秒有十几次甚至几十次的下标访问,LinkedList的效率肯定不能满足要求。

不甚靠谱的解决方案

分析需求: 评论列表展示200条数据,当超过时,删除头部的评论以维持两百的数目。

问了一下需求SE,确认,并不要维持精准的200数目,所以做了如下调整:

数据达到targetCapacity(200)条时先不删除,等到超出粒度granularity(100)时,再一次性删除granularity条数据。

实现了如下CapacityKeepArrayList

GitHub代码

import java.util.ArrayList;
import java.util.LinkedList;

/**
 * 支持快速头删除维护的列表
 * 需要适当计算 capacity 和 granularity
 * @param <E>
 */
public class CapacityKeepArrayList<E> implements DynamicList<E> {


    private LinkedList<ArrayList<E>> parent;

    private ArrayList<E> child;

    /**
     * 列表的目标容量
     */
    private int targetCapacity = 200;

    /**
     * 动态删除头部时的粒度
     */
    private int granularity = 100;

    private int aimSize;

    private int size = 0;

    public int getGranularity() {
        return granularity;
    }

    public int getTargetCapacity() {
        return targetCapacity;
    }

    public CapacityKeepArrayList(int targetCapacity, int granularity) {
        this.targetCapacity = targetCapacity;
        this.granularity = granularity;
        aimSize = targetCapacity / granularity + 1;
        parent = new LinkedList<>();
        grow();
    }

    private void grow() {
        child = new ArrayList<>(granularity);
        parent.add(child);
    }

    /*
     * return 超标数量
     * 0 未超标
     */
    @Override
    public int add(E element) {
        child.add(element);
        size++;
        ensureGranularity();
        return ensureCapacity();
    }

    @Override
    public void forceAdd(E element) {
        child.add(element);
        size++;
        ensureGranularity();
    }

    /**
     * 确保粒度不超标
     */
    private void ensureGranularity() {
        if (child.size() == granularity) {
            grow();
        }
    }

    /**
     * 确保容量不超标,
     * return 超标数量
     * 0 未超标
     */
    private int ensureCapacity() {
        int amount = parent.size() -aimSize;
        if (amount < 1) {
            return 0;
        }
        for (int i = 0; i < amount; i++) {
            parent.removeFirst();
        }
        size -= amount * granularity;
        return amount * granularity;
    }

    @Override
    public E get(int position) {
        return parent.get(position / granularity).get(position % granularity);
    }

    @Override
    public int size() {
        return size;
    }
}

复制代码

根据需求,大概做出了这样的方案:

  1. 确定粒度后,每个粒度为一单位,使用ArrayList存储,保证当前粒度下,是能随机访问的。
  2. 然后用总容量除以粒度得到aimSize,维护一个长度为aimSize + 1的链表作为最终的数据结构,元素就是每一粒度的ArrayList

最终的数据结构,选择链表或者ArrayList其实都行。 因为实际需求是容量200,粒度100,所以只有3个元素,使用链表相比ArrayList在访问时几乎没有性能差距。链表只是为了方便头删除。

至于为啥会有forceAdd()不论容量是否超标都插入而不删除头部的方法,是因为当用户滑动评论,停留在某一条时,不能因为刷出新评论而把用户当前正在看的给删了。

此时,数据结构的容量不能维持在targetCapacity + granularity以内,可能会无限膨胀,这也是我为啥没有选择头尾循环队列来实现此需求。

存在的问题

  1. 用户在评论页面停留时,数据结构的容量可能会无限膨胀,那么aimSize有可能很大,性能又会下降。
  2. 如果外层不使用LinkedList,而是ArrayList,那么在容量膨胀时,读取性能不会下降,但是下次删除时,没法一次性删掉头部多处的很多数据,多次头删除一条数据,ArrayList性能差。
  3. 没有考虑线程安全的问题。

关于1.2问题,能想到的解决方案是,完全使用数组来实现两层结构,自己实现ArrayList没有的头部多个元素删除的功能。


工作经验太少,所以,想问题可能太简单或者太复杂了。 不知有啥好些的方案。期望有人讨论一下。

转载于:https://juejin.im/post/5cb700eef265da03a85ab84b

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
### 回答1: 可以使用双端队列来实现这个功能。双端队列是一种允许在两端进行插入和删除操作的特殊线性数据结构。因此,你可以在队列的头和尾进行插入和删除操作,时间复杂度均为 O(1)。此外,双端队列还允许随机访问,时间复杂度也是 O(1)。 你可以使用数组或链表来实现双端队列。使用数组实现的双端队列具有更快的随机访问速度,但是会有固定的空间限制,而使用链表实现的双端队列则没有空间限制,但是随机访问速度略慢。 ### 回答2: 对于实现头尾插入和随机访问的时间复杂度为O(1)的数据结构,有两种常见的选择:双向链表和哈希表。 双向链表是一种将每个节点连接到前一个和后一个节点的线性数据结构。在双向链表中,头节点和尾节点都是已知的,因此可以在O(1)时间复杂度内进行头部和尾部的插入操作。另外,双向链表在进行随机访问时,需要遍历链表来找到目标节点,因此时间复杂度为O(n)。所以,虽然双向链表满足头尾插入的O(1)时间复杂度要求,但对于随机访问的要求无法满足。 而哈希表是一种基于哈希函数的数据结构,可以通过键值对的方式存储和访问数据。在哈希表中,通过哈希函数将键转换为对应的哈希值,并将键值对存储在对应的哈希表中。哈希表的插入和访问操作都可以在O(1)时间复杂度内完成。但是,哈希表本身是无序的,所以涉及到随机访问时,可能需要通过遍历哈希表来找到目标数据,从而造成O(n)的时间复杂度。 综上所述,如果要同时满足头尾插入和随机访问的时间复杂度为O(1)的要求,我们可能需要结合并改进上述两个数据结构。例如,可以使用一个双向链表来保存数据,并在另一个哈希表中保存每个节点在链表中的位置,这样可以通过哈希表来实现O(1)的随机访问。通过这种方式,可以同时满足头尾插入和随机访问的O(1)时间复杂度要求。 ### 回答3: 要使得头尾插入和随机访问的时间复杂度都为O(1),可以选用双向链表作为数据结构。 双向链表拥有头节点和尾节点,并且每个节点都有指向前一个节点和后一个节点的指针。这样,无论是在头部插入、尾部插入还是随机访问某个节点,都可以在O(1)的时间内完成。 对于头部插入,只需将新节点插入到原头节点之前,然后更新头节点指针即可。 对于尾部插入,只需将新节点插入到原尾节点之后,然后更新尾节点指针即可。 对于随机访问,可以根据给定的索引从头节点(或者尾节点)开始,依次按照前驱节点(或者后继节点)的指针找到目标节点。 双向链表的基本操作时间复杂度如下: - 头部插入:O(1) - 尾部插入:O(1) - 随机访问:O(1) - 头部删除:O(1) - 尾部删除:O(1) - 指定节点删除:O(1) 因此,选择双向链表可以满足头尾插入和随机访问时间复杂度都为O(1)的需求。不过需要注意的是,双向链表的空间复杂度较高,因为每个节点需要额外的指针来指向前驱和后继节点。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值