2.5.3. 有环链表入口问题
同样看下面这段代码,完成需求:
//测试类 public class Test { public static void main(String[] args) throws Exception { Node<String> first = new Node<String>("aa", null); Node<String> second = new Node<String>("bb", null); Node<String> third = new Node<String>("cc", null); Node<String> fourth = new Node<String>("dd", null); Node<String> fifth = new Node<String>("ee", null); Node<String> six = new Node<String>("ff", null); Node<String> seven = new Node<String>("gg", null); //完成结点之间的指向 first.next = second; second.next = third; third.next = fourth; fourth.next = fifth; fifth.next = six; six.next = seven; //产生环 seven.next = third; //查找环的入口结点 Node<String> entrance = getEntrance(first); System.out.println("first链表中环的入口结点元素为:"+entrance.item); } /** * 查找有环链表中环的入口结点 * @param first 链表首结点 * @return 环的入口结点 */ public static Node getEntrance(Node<String> first) { return null; } //结点类 private static class Node<T> { //存储数据 T item; //下一个结点 Node next; public Node(T item, Node next) { this.item = item; this.next = next; } } }
需求:
请完善Test类中的getEntrance方法,查找有环链表中环的入口结点。
当快慢指针相遇时,我们可以判断到链表中有环,这时重新设定一个新指针指向链表的起点,且步长与慢指针一样为1,则慢指针与“新”指针相遇的地方就是环的入口。证明这一结论牵涉到数论的知识,这里略,只讲实现。
代码:
/** * 查找有环链表中环的入口结点 * @param first 链表首结点 * @return 环的入口结点 */ public static Node getEntrance(Node<String> first) { Node<String> slow = first; Node<String> fast = first; Node<String> temp = null; while(fast!=null && fast.next!=null){ fast = fast.next.next; slow=slow.next; if (fast.equals(slow)){ temp = first; continue; } if (temp!=null){ temp=temp.next; if (temp.equals(slow)){ return temp; } } } return null; }
2.6.循环链表
循环链表,顾名思义,链表整体要形成一个圆环状。在单向链表中,最后一个节点的指针为null,不指向任何结点,因为没有下一个元素了。要实现循环链表,我们只需要让单向链表的最后一个节点的指针指向头结点即可。
循环链表的构建:
public class Test { public static void main(String[] args) throws Exception { //构建结点 Node<Integer> first = new Node<Integer>(1, null); Node<Integer> second = new Node<Integer>(2, null); Node<Integer> third = new Node<Integer>(3, null); Node<Integer> fourth = new Node<Integer>(4, null); Node<Integer> fifth = new Node<Integer>(5, null); Node<Integer> six = new Node<Integer>(6, null); Node<Integer> seven = new Node<Integer>(7, null); //构建单链表 first.next = second; second.next = third; third.next = fourth; fourth.next = fifth; fifth.next = six; six.next = seven; //构建循环链表,让最后一个结点指向第一个结点 seven.next = first; } }
2.7.约瑟夫问题
问题描述:
传说有这样一个故事,在罗马人占领乔塔帕特后,39 个犹太人与约瑟夫及他的朋友躲到一个洞中,39个犹太人决定宁愿死也不要被敌人抓到,于是决定了一个自杀方式,41个人排成一个圆圈,第一个人从1开始报数,依次往后,如果有人报数到3,那么这个人就必须自杀,然后再由他的下一个人重新从1开始报数,直到所有人都自杀身亡为止。然而约瑟夫和他的朋友并不想遵从。于是,约瑟夫要他的朋友先假装遵从,他将朋友与自己安排在第16个与第31个位置,从而逃过了这场死亡游戏 。
问题转换:
41个人坐一圈,第一个人编号为1,第二个人编号为2,第n个人编号为n。
1.编号为1的人开始从1报数,依次向后,报数为3的那个人退出圈;
2.自退出那个人开始的下一个人再次从1开始报数,以此类推;
3.求出最后退出的那个人的编号。
图示:
解题思路:
1.构建含有41个结点的单向循环链表,分别存储1~41的值,分别代表这41个人;
2.使用计数器count,记录当前报数的值;
3.遍历链表,每循环一次,count++;
4.判断count的值,如果是3,则从链表中删除这个结点并打印结点的值,把count重置为0;
代码:
public class Test { public static void main(String[] args) throws Exception { //1.构建循环链表 Node<Integer> first = null; //记录前一个结点 Node<Integer> pre = null; for (int i = 1; i <= 41; i++) { //第一个元素 if (i==1){ first = new Node(i,null); pre = first; continue; } Node<Integer> node = new Node<>(i,null); pre.next = node; pre = node; if (i==41){ //构建循环链表,让最后一个结点指向第一个结点 pre.next=first; } } //2.使用count,记录当前的报数值 int count=0; //3.遍历链表,每循环一次,count++ Node<Integer> n = first; Node<Integer> before = null; while(n!=n.next){ //4.判断count的值,如果是3,则从链表中删除这个结点并打印结点的值,把count重置为0; count++; if (count==3){ //删除当前结点 before.next = n.next; System.out.print(n.item+","); count=0; n = n.next; }else{ before=n; n = n.next; } } /*打印剩余的最后那个人*/ System.out.println(n.item); } }
三、栈
3.1.栈概述
3.1.1.生活中的栈
存储货物或供旅客住宿的地方,可引申为仓库、中转站 。例如我们现在生活中的酒店,在古时候叫客栈,是供旅客休息的地方,旅客可以进客栈休息,休息完毕后就离开客栈。
3.1.2.计算机中的栈
我们把生活中的栈的概念引入到计算机中,就是供数据休息的地方,它是一种数据结构,数据既可以进入到栈中,又可以从栈中出去。
栈是一种基于先进后出(FILO)的数据结构,是一种只能在一端进行插入和删除操作的特殊线性表。它按照先进后出的原则存储数据,先进入的数据被压入栈底,最后的数据在栈顶,需要读数据的时候从栈顶开始弹出数据(最后一个数据被第一个读出来)。
我们称数据进入到栈的动作为压栈,数据从栈中出去的动作为弹栈。
3.2.栈的实现
3.2.1 栈API设计
3.2.2.栈代码实现
// 栈代码 import java.util.Iterator; public class Stack<T> implements Iterable<T>{ //记录首结点 private Node head; //栈中元素的个数 private int N; public Stack() { head = new Node(null,null); N=0; } //判断当前栈中元素个数是否为0 public boolean isEmpty(){ return N==0; } //把t元素压入栈 public void push(T t){ Node oldNext = head.next; Node node = new Node(t, oldNext); head.next = node; //个数+1 N++; } //弹出栈顶元素 public T pop(){ Node oldNext = head.next; if (oldNext==null){ return null; } //删除首个元素 head.next = head.next.next; //个数-1 N--; return oldNext.item; } //获取栈中元素的个数 public int size(){ return N; } @Override public Iterator<T> iterator() { return new SIterator(); } private class SIterator implements Iterator<T>{ private Node n = head; @Override public boolean hasNext() { return n.next!=null; } @Override public T next() { Node node = n.next; n = n.next; return node.item; } } private class Node{ public T item; public Node next; public Node(T item, Node next) { this.item = item; this.next = next; } } } //测试代码 public class Test { public static void main(String[] args) throws Exception { Stack<String> stack = new Stack<>(); stack.push("a"); stack.push("b"); stack.push("c"); stack.push("d"); for (String str : stack) { System.out.print(str+" "); } System.out.println("-----------------------------"); String result = stack.pop(); System.out.println("弹出了元素:"+result); System.out.println(stack.size()); } }
3.3.案例
3.3.1 括号匹配问题
问题描述:
给定一个字符串,里边可能包含"()"小括号和其他字符,请编写程序检查该字符串的中的小括号是否成对出现。
例如:
“(上海)(长安)”:正确匹配
“上海((长安))”:正确匹配
“上海(长安(北京)(深圳)南京)”:正确匹配
“上海(长安))”:错误匹配
“((上海)长安”:错误匹配
示例代码:
public class BracketsMatch { public static void main(String[] args) { String str = "(上海(长安)())"; boolean match = isMatch(str); System.out.println(str+"中的括号是否匹配:"+match); } /** * 判断str中的括号是否匹配 * @param str 括号组成的字符串 * @return 如果匹配,返回true,如果不匹配,返回false */ public static boolean isMatch(String str){ return false; } }
请完善 isMath方法。
分析:
1.创建一个栈用来存储左括号
2.从左往右遍历字符串,拿到每一个字符
3.判断该字符是不是左括号,如果是,放入栈中存储
4.判断该字符是不是右括号,如果不是,继续下一次循环
5.如果该字符是右括号,则从栈中弹出一个元素t;
6.判断元素t是否为null,如果不是,则证明有对应的左括号,如果不是,则证明没有对应的左括号
7.循环结束后,判断栈中还有没有剩余的左括号,如果有,则不匹配,如果没有,则匹配
代码实现:
public class BracketsMatch { public static void main(String[] args) { String str = "(fdafds(fafds)())"; boolean match = isMatch(str); System.out.println(str + "中的括号是否匹配:" + match); } /** * 判断str中的括号是否匹配 * * @param str 括号组成的字符串 * @return 如果匹配,返回true,如果不匹配,返回false */ public static boolean isMatch(String str) { //1.创建一个栈用来存储左括号 Stack<String> chars = new Stack<>(); //2.从左往右遍历字符串,拿到每一个字符 for (int i = 0; i < str.length(); i++) { String currChar = str.charAt(i) + ""; //3.判断该字符是不是左括号,如果是,放入栈中存储 if (currChar.equals("(")) { chars.push(currChar); } else if (currChar.equals(")")) {//4.判断该字符是不是右括号,如果不是,继续下一次循环 //5.如果该字符是右括号,则从栈中弹出一个元素t; String t = chars.pop(); //6.判断元素t是否为null,如果不是,则证明有对应的左括号,如果不是,则证明没有对应的左括号 if (t == null) { return false; } } } //7.循环结束后,判断栈中还有没有剩余的左括号,如果有,则不匹配,如果没有,则匹配 if (chars.size() == 0) { return true; } else { return false; } } }
3.3.2.逆波兰表达式求值问题
逆波兰表达式求值问题是我们计算机中经常遇到的一类问题,要研究明白这个问题,首先我们得搞清楚什么是逆波兰表达式?要搞清楚逆波兰表达式,我们得从中缀表达式说起。
中缀表达式:
中缀表达式就是我们平常生活中使用的表达式,例如:1+3*2,2-(1+3)等等,中缀表达式的特点是:二元运算符总是置于两个操作数中间。
中缀表达式是人们最喜欢的表达式方式,因为简单,易懂。但是对于计算机来说就不是这样了,因为中缀表达式的运算顺序不具有规律性。不同的运算符具有不同的优先级,如果计算机执行中缀表达式,需要解析表达式语义,做大量的优先级相关操作。
逆波兰表达式(后缀表达式):
逆波兰表达式是波兰逻辑学家J・卢卡西维兹(J・ Lukasewicz)于1929年首先提出的一种表达式的表示方法,后缀表达式的特点:运算符总是放在跟它相关的操作数之后。
需求:
给定一个只包含加减乘除四种运算的逆波兰表达式的数组表示方式,求出该逆波兰表达式的结果。
public class ReversePolishNotation { public static void main(String[] args) { //中缀表达式3*(17-15)+18/6的逆波兰表达式如下 String[] notation = {"3", "17", "15", "-", "*","18", "6","/","+"}; int result = caculate(notation); System.out.println("逆波兰表达式的结果为:"+result); } /** * @param notaion 逆波兰表达式的数组表示方式 * @return 逆波兰表达式的计算结果 */ public static int caculate(String[] notaion){ return -1; } }
完善caculate方法,计算出逆波兰表达式的结果。
分析:
1.创建一个栈对象oprands存储操作数
2.从左往右遍历逆波兰表达式,得到每一个字符串
3.判断该字符串是不是运算符,如果不是,把该该操作数压入oprands栈中
4.如果是运算符,则从oprands栈中弹出两个操作数o1,o2
5.使用该运算符计算o1和o2,得到结果result
6.把该结果压入oprands栈中
7.遍历结束后,拿出栈中最终的结果返回
代码实现:
public class ReversePolishNotation { public static void main(String[] args) { //中缀表达式3*(17-15)+18/6的逆波兰表达式如下 String[] notation = {"3", "17", "15", "-", "*", "18", "6", "/", "+"}; int result = caculate(notation); System.out.println("逆波兰表达式的结果为:" + result); } /** * @param notaion 逆波兰表达式的数组表示方式 * @return 逆波兰表达式的计算结果 */ public static int caculate(String[] notaion) { //1.创建一个栈对象oprands存储操作数 Stack<Integer> oprands = new Stack<>(); //2.从左往右遍历逆波兰表达式,得到每一个字符串 for (int i = 0; i < notaion.length; i++) { String curr = notaion[i]; //3.判断该字符串是不是运算符,如果不是,把该该操作数压入oprands栈中 Integer o1; Integer o2; Integer result; switch (curr) { case "+": //4.如果是运算符,则从oprands栈中弹出两个操作数o1,o2 o1 = oprands.pop(); o2 = oprands.pop(); //5.使用该运算符计算o1和o2,得到结果result result = o2 + o1; //6.把该结果压入oprands栈中 oprands.push(result); break; case "-": //4.如果是运算符,则从oprands栈中弹出两个操作数o1,o2 o1 = oprands.pop(); o2 = oprands.pop(); //5.使用该运算符计算o1和o2,得到结果result result = o2 - o1; //6.把该结果压入oprands栈中 oprands.push(result); break; case "*": //4.如果是运算符,则从oprands栈中弹出两个操作数o1,o2 o1 = oprands.pop(); o2 = oprands.pop(); //5.使用该运算符计算o1和o2,得到结果result result = o2 * o1; //6.把该结果压入oprands栈中 oprands.push(result); break; case "/": //4.如果是运算符,则从oprands栈中弹出两个操作数o1,o2 o1 = oprands.pop(); o2 = oprands.pop(); //5.使用该运算符计算o1和o2,得到结果result result = o2 / o1; //6.把该结果压入oprands栈中 oprands.push(result); break; default: oprands.push(Integer.parseInt(curr)); break; } } //7.遍历结束后,拿出栈中最终的结果返回 Integer result = oprands.pop(); return result; } }
四、队列
队列是一种基于先进先出(FIFO)的数据结构,是一种只能在一端进行插入,在另一端进行删除操作的特殊线性表,它按照先进先出的原则存储数据,先进入的数据,在读取数据时先读被读出来。
4.1.队列的API设计
4.2.队列的实现
// 队列代码 import java.util.Iterator; public class Queue<T> implements Iterable<T>{ //记录首结点 private Node head; //记录最后一个结点 private Node last; //记录队列中元素的个数 private int N; public Queue() { head = new Node(null,null); last=null; N=0; } //判断队列是否为空 public boolean isEmpty(){ return N==0; } //返回队列中元素的个数 public int size(){ return N; } //向队列中插入元素t public void enqueue(T t){ if (last==null){ last = new Node(t,null); head.next=last; }else{ Node oldLast = last; last = new Node(t,null); oldLast.next=last; } //个数+1 N++; } //从队列中拿出一个元素 public T dequeue(){ if (isEmpty()){ return null; } Node oldFirst = head.next; head.next = oldFirst.next; N--; if (isEmpty()){ last=null; } return oldFirst.item; } @Override public Iterator<T> iterator() { return new QIterator(); } private class QIterator implements Iterator<T>{ private Node n = head; @Override public boolean hasNext() { return n.next!=null; } @Override public T next() { Node node = n.next; n = n.next; return node.item; } } private class Node{ public T item; public Node next; public Node(T item, Node next) { this.item = item; this.next = next; } } } //测试代码 public class Test { public static void main(String[] args) throws Exception { Queue<String> queue = new Queue<>(); queue.enqueue("a"); queue.enqueue("b"); queue.enqueue("c"); queue.enqueue("d"); for (String str : queue) { System.out.print(str+" "); } System.out.println("-----------------------------"); String result = queue.dequeue(); System.out.println("出列了元素:"+result); System.out.println(queue.size()); } }