题目:
说明:
/*
虽然写了3种方法,但是在博客里,只对我自己最满意的法三做了分析
不过其他两个方法,都贴出了代码和注释,说明了我的思路,以及我的bug出在哪里,每种方法是做了什么改进,以及一些总结和分析
*/
/*法一:中转队列法(很不优)
* 1.法一是我最初的版本,没做任何的优化,除了把找了很久的bug改对以外,就完全是“硬怼”的方法,非常不推荐这个版本,下次一定不会这么写了
* (以前听到过一句话:第一眼能够想到的方法,往往时空上的效率都不理想...深以为然,这个方法就是这种类型)
* 2.不推荐是因为,用数组实现队列,其实可能会假溢出,虽然这题看来没什么影响,但养成这样的习惯,毕竟还是不好的
* 3.不推荐更因为,其实没必要再写一个栈Q3的,一旦数据规模变大,可以说是在时间、空间上都不优,甚至可以说,时间复杂度和空间复杂度都会极高,简称“双差”也不为过,这样的中转站的写法,一定要极力去避免
* 4.再说一下为什么会写bug,因为今天数据结构才刚学队列,学的不是很熟,就有些按照自己的感觉来写,写时也没有仔细翻书和查资料求证,导致写队列这个类时,出现了bug,这个bug找了我一个下午,询问了多个师兄和老师,最后发现...如果我数据结构学好一些,这是完全可以避免的...
* (我发现我对STL中的,list、queue、stack等等,我都只是会用,但是如果真正要我自己写代码,我其实是磕磕碰碰,写不出它们的类代码的,这非常不好,所以我打算把循环队列的代码也写一次,见法三)
* 就算我数据结构学的不是那么好,本来嘛,如果不是太过自信和依赖自己的记忆,如果翻翻书,也是完全不会错的,然而...就是这样一个bug,因为我太过自信,浪费了一个下午,所以我在深刻反省自己...T^T
*/
class Customer
{
private int num, type, money;
private int solve; //记录被处理的次序
public Customer(int n, int t, int m)
{
num = n; type = t; money = m;
}
public void setSolve(int n)
{
solve = n;
}
public void showInfo() //show information
{
System.out.println("我是第"+solve+"个被处理的,我的个人信息为:标号为"+num+";业务类型为:"+type+";金额为:"+money);
}
public int getMoney()
{
return money;
}
public int getType()
{
return type;
}
}
class Queue
{
private Customer[] Customers;
private int front, rear; //头尾指针
private int size, limit; //size是队列的真实大小,limit是数组的限定大小
//队头指针始终指向队列头元素
//队尾指针始终指向队列尾元素的下一个位置
public Queue(int n)
{
this.Customers = new Customer[n];
front = rear = size = 0;
limit = n;
}
public boolean isEmpty() //判空
{
return front >= rear;
//return size == 0;
}
public boolean isFull() //判满
{
return rear >= limit;
/*
* 这种判满方式可能会导致"假溢出",所以此时不一定是真的栈满,这是用普通数组实现列表的巨大劣势,也是循环队列的引入背景,务必注意!!!
* 如果要解决这个弊端,大致有三种方法:
* 1.用循环队列实现;(1是最经常采用的方法)
* 2.用链表实现队列(我觉得2不错,除了写法可能比较麻烦一些,但是链表尤其适合插入和删除)
* 3.队列的大小限制开大一些以避免(3其实不太好,因为不知道究竟需要开多大才足够,度很难把握,而且空间上也可能有较大的浪费)
*/
}
public void insert(Customer e) //尾插
{
if (isFull())
{
throw new RuntimeException("队满,无法尾插");
}
Customers[rear++] = e;//进队时,新元素按rear指针位置插入,然后队尾指针增一
size++;
}
public Customer getFront() //取队首
{
if (isEmpty())
{
throw new RuntimeException("队已空,无队首");
}
return Customers[front]; //头指针始终指向队列头元素
}
public Customer getRear() //取队尾
{
if (isEmpty())
{
throw new RuntimeException("队已空,无队尾");
}
return Customers[rear - 1]; //尾指针始终指向队列尾元素的下一个位置
}
public Customer remove() //删除队首
{
if (isEmpty())
{
throw new RuntimeException("队空,删除失败");
}
return Customers[front++]; //返回被删除的元素
}
}
class BankManage
{
static int Amount, Code, Order;
final int SIZE = 5;
Queue Q1 = new Queue(SIZE);
Queue Q2 = new Queue(SIZE);//2个队列
public void initBank() //初始化静态变量,新用户入队列
{
Amount = 1000; Code = Order = 1;
int info [][] = {{1, 700}, {1, 500}, {1, 200}, {2, 300},{2, 400}};
for (int i = 0; i < info.length; i++)
{
Customer c = new Customer(Code++, info[i][0], info[i][1]);
Q1.insert(c);
}
}
public boolean withdraw(Customer c) //取款
{
if (c.getMoney() > Amount)
return false;
Amount -= c.getMoney();
c.setSolve(Order++);
c.showInfo();
return true;
}
public void deposit(Customer c) //存款
{
Amount += c.getMoney();
c.setSolve(Order++);
c.showInfo();
}
public void solveQ2() //检查第二个队列所有客户,能满足的则满足,不满足的重新排到对尾
{
Queue Q3 = new Queue(SIZE); //Q3是一个临时队列,先将仍然不能取款的请求放入Q3,在Q2中所有取款判断完后(判断能否执行,能就取款,不能就插入队尾,但为了能够跳出循环,新建Q3作为中转队列),再将Q3中的所有取款操作,重新入队Q2
while (!Q2.isEmpty())
{
Customer tp = Q2.getFront();
boolean jud = withdraw(tp);
if (jud) Q2.remove();
else
{
Q3.insert(tp);
Q2.remove();
}
}
while (!Q3.isEmpty())
{
Q2.insert(Q3.getFront());
Q3.remove();
}
}
public void testBank()
{
while (!Q1.isEmpty())
{
Customer tp= Q1.getFront(); // tp(temp)
if (tp.getType() == 1) //取款类操作
{
boolean jud = withdraw(tp);
if (jud) Q1.remove();
else
{
Q2.insert(Q1.getFront());
Q1.remove();
}
}
else //存款类操作
{
deposit(tp);
Q1.remove();
solveQ2();
}
}
}
}
public class test
{
public static void main(String []args)
{
BankManage bank = new BankManage();
bank.initBank();
bank.testBank();
}
}
/* 法二 用Q2原来的大小作为循环次数
* 法二所做的改进,是将Q2的大小作为循环次数,省去了Q3的使用
* 法一的Q3,是作为一个临时队列,先将仍然不能取款的请求放入Q3,在Q2中所有取款判断完后(判断能否执行,能就取款,不能就插入队尾,但为了能够跳出循环,新建Q3作为中转队列),再将Q3中的所有取款操作,重新入队Q2
* 之前之所以定义Q3,是因为,如果用队列是否为空,来作为结束Q2循环的条件,可能会死循环
* 但是...我当时思路太闭塞了,没有想到,其实完全可以用Q2最初的大小作为循环次数的,这样就完全没必要定义Q3了
*/
class Customer
{
private int num, type, money;
private int solve; //记录被处理的次序
public Customer(int n, int t, int m)
{
num = n; type = t; money = m;
}
public void setSolve(int n)
{
solve = n;
}
public void showInfo() //show information
{
System.out.println("我是第"+solve+"个被处理的,我的个人信息为:标号为"+num+";业务类型为:"+type+";金额为:"+money);
}
public int getMoney()
{
return money;
}
public int getType()
{
return type;
}
}
class Queue
{
private Customer[] Customers;
private int front, rear; //头尾指针
private int size, limit; //size是队列的真实大小,limit是数组的限定大小
//队头指针始终指向队列头元素
//队尾指针始终指向队列尾元素的下一个位置
public Queue(int n)
{
this.Customers = new Customer[n];
front = rear = size = 0;
limit = n;
}
public boolean isEmpty() //判空
{
return front >= rear;
// return size == 0;
}
public boolean isFull() //判满
{
return rear >= limit;
/*
* 这种判满方式可能会导致"假溢出",所以此时不一定是真的栈满,这是用普通数组实现列表的巨大劣势,也是循环队列的引入背景,务必注意!!!
* 如果要解决这个弊端,大致有三种方法:
* 1.用循环队列实现;(1是最经常采用的方法)
* 2.用链表实现队列(我觉得2不错,除了写法可能比较麻烦一些,但是链表尤其适合插入和删除)
* 3.队列的大小限制开大一些以避免(3其实不太好,因为不知道究竟需要开多大才足够,度很难把握,而且空间上也可能有较大的浪费)
*/
}
public void insert(Customer e) //尾插
{
if (isFull())
{
throw new RuntimeException("队满,无法尾插");
}
Customers[rear++] = e;//进队时,新元素按rear指针位置插入,然后队尾指针增一
size++;
}
public Customer getFront() //取队首
{
return Customers[front]; //头指针始终指向队列头元素
}
public Customer getRear() //取队尾
{
return Customers[rear - 1]; //尾指针始终指向队列尾元素的下一个位置
}
public Customer remove() //删除队首
{
if (isEmpty())
{
throw new RuntimeException("队空,删除失败");
}
size--;
return Customers[front++]; //返回被删除的元素
}
public int getSize()
{
return size;
}
}
class BankManage
{
static int Amount, Code, Order;
final int SIZE = 5;
Queue Q1 = new Queue(SIZE);
Queue Q2 = new Queue(SIZE);//2个队列
public void initBank() //初始化静态变量,新用户入队列
{
Amount = 1000; Code = Order = 1;
int info [][] = {{1, 700}, {1, 500}, {1, 200}, {2, 300},{2, 400}};
for (int i = 0; i < info.length; i++)
{
Customer c = new Customer(Code++, info[i][0], info[i][1]);
Q1.insert(c);
}
}
public boolean withdraw(Customer c) //取款
{
if (c.getMoney() > Amount)
return false;
Amount -= c.getMoney();
c.setSolve(Order++);
c.showInfo();
return true;
}
public void deposit(Customer c) //存款
{
Amount += c.getMoney();
c.setSolve(Order++);
c.showInfo();
}
public void solveQ2() //检查第二个队列所有客户,能满足的则满足,不满足的重新排到对尾
{
int times = Q2.getSize(); //循环次数
for (int i = 0; i < times; i++)
{
if (Q2.isEmpty()) return;
Customer tp = Q2.getFront();
// if (tp == null) return;
boolean jud = withdraw(tp);
if (!jud) Q2.insert(tp);
Q2.remove();
}
}
public void testBank()
{
while (!Q1.isEmpty())
{
Customer tp= Q1.getFront(); // tp(temp)
// if (tp == null) return;
if (tp.getType() == 1) //取款类操作
{
boolean jud = withdraw(tp);
if (jud) Q1.remove();
else
{
Q2.insert(Q1.getFront());
Q1.remove();
}
}
else //存款类操作
{
deposit(tp);
Q1.remove();
if (!Q2.isEmpty()) solveQ2();
}
}
}
}
public class test
{
public static void main(String []args)
{
BankManage bank = new BankManage();
bank.initBank();
bank.testBank();
}
}
/* 法三:循环队列的实现
* 循环队列的判空判满操作:
* 注意!!!对循环队列而言,无法通过 front == rear 来判断队列是空还是满
* 一般解决这个问题有3种方法:
* 1. 设置一个布尔变量专门标记队列空满
* 2. 转换队满的标准,并不将所有位置都有元素看作是队满...而是,只要它只有一个位置没放元素,我们就把它看作是队满了;
* 换句话说,调整队满的判断标准
* 这种方法的实现,一般是少用一个元素的空间。约定入队前,测试尾指针在循环意义下 +1后,是否等于头指针,若相等则认为队满 (rear所指的单元始终为空,循环意义+1一般用取余实现)
* (法2最常用)
* 3. 使用一个计数器记录队列种元素的总数,即队列长度
*/
class Customer
{
private int num, type, money;
private int solve; //记录被处理的次序
public Customer(int n, int t, int m)
{
num = n; type = t; money = m;
}
public void setSolve(int n)
{
solve = n;
}
public void showInfo() //show information
{
System.out.println("我是第"+solve+"个被处理的,我的个人信息为:标号为"+num+";业务类型为:"+type+";金额为:"+money);
}
public int getMoney()
{
return money;
}
public int getType()
{
return type;
}
}
class Queue
{
private Customer[] Customers;
private int front, rear; //头尾指针
private int size, limit; //size是队列的真实大小,limit是数组的限定大小
//队头指针始终指向队列头元素
//队尾指针始终指向队列尾元素的下一个位置
public Queue(int n)
{
this.Customers = new Customer[n];
front = rear = size = 0;
limit = n;
}
public boolean isEmpty() //判空
{
return front == rear;
// return size == 0;
}
public boolean isFull() //判满
{
// return size == limit;
return (rear + 1) % limit == front; //我们把仅剩一个位置没放元素看作满,而不是所有元素都装了才叫满;此外,判满利用了“循环意义上+1”这一思想
}
public void insert(Customer e) //尾插
{
if (isFull())
{
// System.out.println("front == "+front+ " and rear == "+rear);
throw new RuntimeException("队满,无法尾插");
}
Customers[rear] = e;//进队时,新元素按rear指针位置插入,然后队尾指针增一
size++;
rear = (rear + 1) % limit;
}
public Customer getFront() //取队首
{
return Customers[front]; //头指针始终指向队列头元素
}
public Customer getRear() //取队尾
{
return Customers[rear - 1]; //尾指针始终指向队列尾元素的下一个位置
}
public Customer remove() //删除队首
{
if (isEmpty())
{
throw new RuntimeException("队空,删除失败");
}
size--;
return Customers[front++]; //返回被删除的元素
}
public int getSize()
{
return size;
}
}
class BankManage
{
static int Amount, Code, Order;
final int SIZE = 6; //一定要多设置一个,否则会报错: “Exception in thread "main" java.lang.RuntimeException: 队满,无法尾插”
//因为循环队列的队满并不是所有元素放满,而是恰好有一个位置没放元素,我们就已经当作是满了,这点尤其注意!!!
Queue Q1 = new Queue(SIZE);
Queue Q2 = new Queue(SIZE);//2个队列
public void initBank() //初始化静态变量,新用户入队列
{
Amount = 1000; Code = Order = 1;
int info [][] = {{1, 700}, {1, 500}, {1, 200}, {2, 300},{2, 400}};
for (int i = 0; i < info.length; i++)
{
Customer c = new Customer(Code++, info[i][0], info[i][1]);
Q1.insert(c);
}
}
public boolean withdraw(Customer c) //取款
{
if (c.getMoney() > Amount)
return false;
Amount -= c.getMoney();
c.setSolve(Order++);
c.showInfo();
return true;
}
public void deposit(Customer c) //存款
{
Amount += c.getMoney();
c.setSolve(Order++);
c.showInfo();
}
public void solveQ2() //检查第二个队列所有客户,能满足的则满足,不满足的重新排到对尾
{
int times = Q2.getSize(); //循环次数
for (int i = 0; i < times; i++)
{
if (Q2.isEmpty()) return;
Customer tp = Q2.getFront();
boolean jud = withdraw(tp);
if (!jud) Q2.insert(tp);
Q2.remove();
}
}
public void testBank()
{
while (!Q1.isEmpty())
{
Customer tp= Q1.getFront(); // tp(temp)
if (tp.getType() == 1) //取款类操作
{
boolean jud = withdraw(tp);
if (jud) Q1.remove();
else
{
Q2.insert(Q1.getFront());
Q1.remove();
}
}
else //存款类操作
{
deposit(tp);
Q1.remove();
if (!Q2.isEmpty()) solveQ2();
}
}
}
}
public class test
{
public static void main(String []args)
{
BankManage bank = new BankManage();
bank.initBank();
bank.testBank();
}
}
法三的一些详细说明:
Customer类
数据:
1. num、type、money是题目给的数据,分别是顾客标号、业务类型、业务金额;
2. solve是我自己加上的类型,记录该顾客是第几个被处理的,这样就能方便地在顾客类中,输出该顾客的所有信息了
方法:
1. 构造方法,设定num、type、money的初始值
2. setSolve()方法,设定顾客的solve值,也就是他是第几个被处理的客户
3. showInfo()方法,输出顾客的所有有关信息
4. getMoney()方法、getType()方法,都是起到获得类中的私有值的作用
Queue类
数据:
1. Customer类的数组Customers
2. front和rear头尾指针
3. size和limit,前者是队列实际大小,后者是Customers数组开辟的大小
方法:
1. 构造方法,为Customers开辟空间,并设定limit值,初始化front、rear、size为0
2. isEmpty()和isFull() 分别用于循环队列的判空和判满,遵循循环队列和判满判空原则
3. inset()方法用于尾插元素,使得元素入队
4. getFront()和getRear()方法,分别用于取队首元素,和取队尾元素
5. remove()用于删除队首元素,使其出队
6. getSize()用于获取队列元素(这个在BankManage类的solveQ2()函数,比较有用)
BankManage类
数据:
1. 静态变量Amount, Code, Order分别表示银行的剩余金额、顾客总数、已处理顾客数
2. 常量SIZE为队列大小的初值
3. Q1和Q2为题目提到的两个队列,排队队列和等待队列
方法:
1. initBank()方法,初始化题目给定的5个客户的有关信息
2. withdraw(Customer c)和deposit(Customer c)方法:分别完成取款和存款操作,存款无须判断,只要修改银行余额、设定处理序号,输出顾客信息即可;对于取款,要先判断是否有足够余额,没有则返回false,如果可以取,同理,修改银行余额、设定处理序号,输出顾客信息
3. solveQ2()方法,在每次有顾客存款后调用,用于判断第二个队列中是否有可处理的请求,如果有则处理,否则入队尾并删除。这里用到了 Q2的getSize()函数,用于获取Q2的元素个数,也就是说,只需要判断这么多次即可
4. testBank()完成所有的存钱取钱操作
test类
也就是我编写的测试类,建立一个BankManage类对象bank,完成bank的初始化,并调用它的测试函数,查看有关结果,即可完成有关数据的验证
做该题时查阅过的相关资料:
以下是在完成实验的过程中,曾经查阅过的资料,为了方便日后复习,已经都整理为超链接了,可直接点击
//因为最近才开始学队列,一开始不是很会写它的类代码(还因此出了bug,并找了一下午);后来痛定思痛,好好搜索学习了一下队列的有关知识,包括假溢出等等。
throws和thrownewRuntimeException和try-catch的区别
//因为在别人的队列实现代码中,看到了throw new Runtime Exception这种用法,于是自己搜索了解了下这个用法
StackOverflow: The static method…should be accessed in astatic way
//自己的代码出现这个报错以后,搜到的结果
java.lang.NullPointerException空指针异常问题