队列的设计
基于内存的两个很有特点的基本数据思想:连续和非连续(引用,也就是地址)。
如果说连续这一特点创造出了数组,那么数组又进一步出现了栈,队列。
队列的特点是:FIFO,LILO 简单的说它的特点就是头是用来进的,尾是用来出的。队列也可以通过数组和链进行实现。对应一个是顺序队列,一个是链式队列。
队列的应用也非常广泛,特别是一些具有某些额外特性的队列,比如循环队列、阻塞队列、并发队列。它们在很多偏底层系统、框架、中间件的开发中,起着关键性的作用。比如高性能队列 Disruptor、Linux 环形缓存,都用到了循环并发队列;Java concurrent并发包利用 ArrayBlockingQueue 来实现公平锁等。
使用数组实现队列
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 | import java.util.Arrays; /** * description: ListQueue * date: 2020/12/7 13:34 * * @author: SmartCat * version: 1.0.0 */ public class ListQueue { static int capacity= 20 ; private int [] list = new int [capacity]; private int size = 0 ; private int head = 0 ; private int tail = 0 ; public ListQueue( int size){ list = new int [size]; } public ListQueue(){ } public boolean enqueue( int i){ if (tail == list.length){ return false ; } else { list[tail] = i; tail++; size++; return true ; } } public int dequeue(){ if (head==tail){ return 100000000 ; } else { int i = list[head]; list[head] = 0 ; head++; size--; return i; } } @Override public String toString(){ return Arrays.toString(list); } public static void main(String[] args) { ListQueue listQueue = new ListQueue(); for ( int i = 0 ; i < 10 ; i++) { listQueue.enqueue(i); } for ( int i = 0 ; i < 10 ; i++) { listQueue.dequeue(); } for ( int i = 0 ; i < 5 ; i++) { listQueue.enqueue(i); } System.out.println(listQueue.toString()); } } |
这边有个明显问题就是会造成数组的不连续。
数组的本身特点就是在虚拟内存上是一个连续的空间,如果出现了不连续的空间,那么应该进行维护,不然这个辛苦创建出的思想就被破坏了。
维护的话有两种方法,一种就是直接在本数组上进行迁移,另外一种就是用一个新的数组进行接收。
1 2 3 4 5 6 7 8 9 | public void adjustment(){ if (head!= 0 ){ int i = tail - head; for ( int i1 = 0 ; i1 < i; i1++) { list[i1] = list[i1+head]; list[i1+head] = 0 ; } } } |
这样的话不好的地方就是什么时候进行调整。调整的频率怎么进行控制。这边有两种思想:1.如果队列是一个自动扩容的队列,那么就可以在扩容的时候进行调整 2.如果是非自动扩容的队列那么就可以在尾下表达到数组长度的时候进行调整。
链表的实现也是一样的操纵。
但是如果每一次满的时候都需要进行调整, 这明显有点蠢,有没有更好的办法。
先思想下队列的特点,先进先出,后进后出,也就是说是一个天然的时序性。要么是不是游码一直变化就可以完成了持续性。那这边又有两种办法来实现了一种就是简单的环思想,另外一种就是直接点的,累加思想,头标从1,2,3,4....n,例如一个数组是10个大小,通过 头标%10来得知具体的数组上位置。这个逻辑就是基于 如果头标到了50,那么50之前的肯定不存在。而头和尾之间的防止重叠,可以通过数组的长度来控制,也就是说,如果进的多,出的少,为了不出现覆盖的情况,可以通过数组长度来进行判断。
如果尾变成了9,那么就会冲掉还没出的头。所以,如果头+数组长度=尾,那么就不可能进行添加。不过这个的缺点就是头和尾会越来越大。所以再简化一步就是环,环只需要将头标或者尾标进行重置就可以。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 | public class CircularQueue { // 数组:items,数组大小:n private String[] items; private int n = 0 ; // head 表示队头下标,tail 表示队尾下标 private int head = 0 ; private int tail = 0 ; // 申请一个大小为 capacity 的数组 public CircularQueue( int capacity) { items = new String[capacity]; n = capacity; } // 入队 public boolean enqueue(String item) { // 队列满了 if ((tail + 1 ) % n == head) return false ; items[tail] = item; <strong> tail = (tail + 1 ) % n;</strong> return true ; } // 出队 public String dequeue() { // 如果 head == tail 表示队列为空 if (head == tail) return null ; String ret = items[head]; <strong>head = (head + 1 ) % n;</strong> return ret; } } |
剩下的就是有关队列的线程安全问题了。
这边可以看出,队列中关键的是头标和尾标。然后是数组。所有对头标和尾标实现加锁操作,或者对这两个进行CAS 原子操作就可以完成实现。
整体队列不是特别难的一个思想,掌握了基本的思想,然后根据业务实现多线程or优先会更加考验程序员的功底。
【推荐】还在用 ECharts 开发大屏?试试这款永久免费的开源 BI 工具!
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步