[ Java学习 ] 实验 银行业务模拟
题目:
说明:
/* 虽然写了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空指针异常问题