我暗恋她,就像对待队列一样。然而,我却无法像队列一样运作:尽管我早早地出现在她的生活中,却没有得到她的关注和回应。就像队列中的元素一样,我的位置固定,却始终无法领先于其他人。
——————————————————————————————————————————
背包,队列
可以先看他们的API:都含有一个无参构造函数,添加单个元素的方法,测试集合是否为空的方法和返回集合大小的方法。而Stack和Queue有能删除集合的特定元素的方法。
它们具有泛型和迭代的特性。
泛型的使用,可以用它们存储任意类型的数据。
如:Stack , 用Item替换任意数据类型
Stack<String> stack = new Stack<String>();
而类型参数必须被实例化为引用类型。而java具有自动装箱和自动拆箱的机制,可以在引用类型和对应的原始数据类型之间转换。
Stack<Integer> stack = new Stack<Integer>();
stack.push(17); //自动装箱 (int 转换为 Integer)
int i = stack.pop(); //自动拆箱 (Integer 转换为 int)
如果集合可迭代,可以使用如foreach循环来逐个访问集合中的元素,无需手动管理索引或遍历位置,这样可以简化代码实现。
Queue<String> queue = new LinkedList<>();
// 使用 foreach 循环来遍历 Queue
for (String element : queue) {
System.out.println(element);
}
背包
特点:
- 不支持删除元素和遍历操作
- 收集元素并迭代遍历所有收集到的元素
- 迭代的顺序不确定(无序)且与用例无关
- 主要用于统计计算,去重,求平均值或者样本标准差之类的场景
就理解成一个背包即可,里面各种球,一次放进去一个球,或者一次从里面找自己想要的具有某种特点的球。
Bag<Double> numbers = new Bag<Double>();
while (!StdIn.isEpty())
numbers.add(StdIn.readDouble());
int N = number.size(); // 背包的元素数量
double sum = 0.0
for (double x : number)
sum += x;
double mean = sum/N;
sum = 0.0;
for(double x : number)
sum += (x - mean)*(x- mean);
double std = Math.sqrt(sum/(N-1));
队列(queue)
先进先出队列是一种基于先进先出(FIFO) 策略的集合类型。
使用场景很常见,尤其是排队,打印机等待打印或者是计算机某些软件等待处理的任务。
因为是服务性的策略,所以主打一个公平,先到先得,等的越久越先服务。
Queue<Integer> queue = new Queue<Integer>();
// Enqueue 操作 可以通过add()或offer()方法实现
queue.add("A");
queue.add("B");
queue.offer("C");
// dequeue:从队列的头部移除并返回一个元素,可以使用remove()或poll()方法来实现
String dequeuedElement = queue.remove();
System.out.println("Dequeued element: " + dequeuedElement);
队列是一个有序列表,可以用数组或是链表来实现。
数组模拟队列思路
- 队列本身是有序列表,若使用数组的结构来存储队列的数据,则队列数组的声明如下图, 其中 maxSize 是该队 列的最大容量。
- 因为队列的输出、输入是分别从前后端来处理,因此需要两个变量 front 及 rear 分别记录队列前后端的下标, front 会随着数据输出而改变,而 rear 则是随着数据输入而改变,如图所示:
准备工作思路:
-
将front和rear初始化为-1 , 为了表示队列为空的状态 (当队列中添加第一个元素时,front和rear都会更新为0,表示队列中有一个元素)
-
当 front == rear 则代表队列为空
- 疑问:如果队列中只有一个元素,头尾指针都指向这个元素,那这样也不是空的,所以分析出来,头指针指向的是队列头的前一个位置;尾指针指向最后一个元素。
- 解决方法:也出现插入数据的时候,front指针总是指向队列的第一个元素,尾指针向后移动一位,元素添加到尾指针的位置。
- 当 rear == maxSize - 1 则代表队列为满
class ArrayQueue {
private int maxSize; // 表示数组的最大容量
private int front; // 队列头
private int rear; // 队列尾
private int[] arr; // 该数据用于存放数据, 模拟队列
// 创建队列的构造器
public ArrayQueue(int arrMaxSize) {
maxSize = arrMaxSize;
arr = new int[maxSize];
front = -1; // 指向队列头部,分析出 front 是指向队列头的前一个位置.
rear = -1; // 指向队列尾,指向队列尾的数据(即就是队列最后一个数据)
}
// 判断队列是否满 (队列尾指向数组的最大索引)
public boolean isFull() {
return rear == maxSize - 1;
}
// 判断队列是否为空 (初始时,两指针都为-1,指向同一个位置)
public boolean isEmpty() {
return rear == front;
}
存入数据思路分析:
1.添加元素时,判断队列是不是满的。若不是,则头指针不动,用于元素出队列,将尾指针后移:rear+1
2.再将数据存入 rear 所指的数组元素中
// 添加数据到队列
public void addQueue(int n) {
// 判断队列是否满
if (isFull()) {
System.out.println("队列满,不能加入数据~");
return;
}
rear++; // 让 rear 后移
arr[rear] = n;
}
出队列(获取队列的数据)代码:
public int getQueue() {
// 判断队列是否空
if (isEmpty()) {
// 通过抛出异常
throw new RuntimeException("队列空,不能取数据");
}
front++; // front 后移 满足先进先出原则。
return arr[front];
}
显示队列中的所有数据 代码:
public void showQueue() {
// 遍历
if (isEmpty()) {
System.out.println("队列空的,没有数据~~");
return;
}
for (int i = 0; i < arr.length; i++) {
System.out.printf("arr[%d]=%d\n", i, arr[i]);
}
}
显示队列的头数据 代码:
public int headQueue() {
if (isEmpty()) {
throw new RuntimeException("队列空的,没有数据~~");
}
return arr[front + 1];
}
}
对于这个代码所模拟的队列进行问题分析及方案优化:
- 问题:目前数组使用一次就不能用,没有出队列的功能,则没有达到复用的效果。且如果队列尾部的空间已满,而队列头部仍有空间,此时无法再添加元素
- 优化方案:将这个数组使用算法,改进成一个环形的队列 通过取模(%)变成循环的
数组模拟环形队列
对前面的数组模拟队列进行优化,实现可以复用。成为是一个环形的循环队列。(通过取模的方式来实现即可)
分析说明:
-
因为是环形结构,满了会回头。所以当尾索引的下一个为头索引时,表示队列满了。因此将队列容量空出一个作为约定。通过这个约定可以得出, (rear + 1) % maxSize == front,代表队列满了。
-
rear == front 则代表队列为空
具体思路如下:
- 将front指向队列的第一个元素,即front=0 (不这样的话,这样如果队列满的时候,尾指针无法与头指针相逢,形成不了环状结构。)
问题:这个时候假如有一个元素,那头尾指针又指向一个位置了。所以我们把尾指针指向最后一个元素的后一位。
-
将rear指向队列最后一个元素的后一个位置,即rear=0。(这里需要注意,rear指向的位置实际上是不存储数据的,只是为了区分队列空和满的情况,因此需要浪费一个位置)
而因为你是指向后一位的,这里当你定义了1个长度为5的队列,已经有了4个元素。这个时候头指针指着第一个元素,尾指针值向第4个元素后面的位置。这时候你再加一个元素的话,那尾指针就指回了第一个位置,这样子就出现了rear == front。那你就没办法确定到底是队列满了还是队列空了。 因此采用(rear + 1) % maxSize == front就表示队列满了,此时rear指向的位置实际上是不存储数据的。
在网上找到的其他思路:
新增一个容量 capacity 字段,并进行维护,当 capacity=size 表示队列已满。
维护一个标记,或者当 头指针=尾指针 时同步判断当前位置是否有元素来判断当前是队空还是队满。
现在使用的方法会浪费一个空间;而新增容量字段需要维护,而标记的方法队列空和队列满的时候需要判断是那种情况,影响性能。一般使用浪费一个空间的方法,用空间换时间。
-
在判断队列是否满时,尾指针的下一个位置会等于头指针的位置。所以使用**(rear + 1) % maxSize == front**条件,当rear和front相邻且都指向有数据的位置时,队列为满。
public boolean isFull() { return (rear + 1) % maxSize == front; }
-
在判断队列是否为空时,使用rear == front的条件,当队列中没有元素时,front和rear都指向同一个位置。
public boolean isEmpty() { return rear == front; }
-
计算队列中有效数据元素的个数时,使用**(rear + maxSize - front) % maxSize**的公式,由于队列是循环的,所以队列的尾部可能在数组的起始位置之前。
计算出队列尾指针
rear
到队列头指针front
的距离,并进行模运算,得到实际的元素个数。
添加数据到队列
public void addQueue(int n) {
// 判断队列是否满
if (isFull()) {
System.out.println("队列满,不能加入数据~");
return;
}
//直接将数据加入
arr[rear] = n;
//将 rear 后移, 这里必须考虑取模!
rear = (rear + 1) % maxSize;
}
获取队列的数据, 出队列
public int getQueue() {
// 判断队列是否空
if (isEmpty()) {
// 通过抛出异常
throw new RuntimeException("队列空,不能取数据");
}
// 这里需要分析出 front 是指向队列的第一个元素
// 1. 先把 front 对应的值保留到一个临时变量
// 2. 然后将 front 后移, 考虑取模!
// 3. 将临时保存的变量返回
int value = arr[front];
front = (front + 1) % maxSize;
return value;
}
显示队列的所有数据
public void showQueue() {
// 遍历
if (isEmpty()) {
System.out.println("队列空的,没有数据~~");
return;
}
for (int i = front; i < front + size() ; i++) {
// 使用 i % maxSize 的目的是确保索引值在循环队列的有效范围内
//假设队列的最大大小为 5,且队列起始位置front是2,不取模的话,那就超出了队列范围,应该回到队列的开头,则需要取模。
System.out.printf("arr[%d]=%d\n", i % maxSize, arr[i % maxSize])
}
}
// 求出当前队列有效数据的个数
public int size() {
// rear = 2
// front = 1
// maxSize = 3
return (rear + maxSize - front) % maxSize;
}
显示队列的头数据
public int headQueue() {
// 判断
if (isEmpty()) {
throw new RuntimeException("队列空的,没有数据~~");
}
return arr[front];
}
}
————————————————————————————————————
那这样子是不是就有机会了