Java集合类(链表,栈,队列实战)
本节将通过几个实例来深入理解链表,栈和队列的实际应用,包含以下内容:
-
括号匹配问题
-
Josephus问题
-
检查链表是否包含环
-
用两个栈实现队列
-
自定义阻塞式链表队列
- 括号匹配问题
括号匹配问题是指对于给定的一个字符串,检查里面的括号是否成对出现即是否匹配,成对的括号包括:(),【】,{}。
例如:字符串“()【】{}”里的括号是成对的,“a((【{c}】))b”也是成对的,但“(【)】”不是成对的。
要求写一程序,输入为字符串,输出为true或者false,表示括号匹配或者不匹配。
思路:对给定的字符串逐一检查,如果出现(,【,{,则将该字符压入一栈。如果出现)则从栈中弹出一字符,如果是(,则继续,否则返回false。同理,若出现}则检查弹出的字符是否是{,若出现】则检查弹出的字符是否是【。若不是任何的括号字符,则continue。遍历完所有的字符后,如果栈为空,那么原字符串括号匹配,否则不匹配。
该问题充分利用了栈的先进后出原理(FILO),源码如下,package com.my.study.algorithm.stackAndQueue; import java.util.EmptyStackException; import java.util.Stack; // This class demonstrates how to use stack to check if a string has matched brackets. public class BracketMatcher { public static void main(String[] args) { String str = "()"; System.out.println(str + " : " + checkBracket(str)); str = "()[]"; System.out.println(str + " : " + checkBracket(str)); str = "a(b)c[d]e{f}g"; System.out.println(str + " : " + checkBracket(str)); str = "(2+1)[5-8]{7*9}({55+98})([abc]def)g({sf}{ass})"; System.out.println(str + " : " + checkBracket(str)); str = "(()[]{})"; System.out.println(str + " : " + checkBracket(str)); str = "{(){}}"; System.out.println(str + " : " + checkBracket(str)); str = "()[{}]"; System.out.println(str + " : " + checkBracket(str)); str = "([)]{}"; System.out.println(str + " : " + checkBracket(str)); str = "(()[]{}"; System.out.println(str + " : " + checkBracket(str)); str = "()[]{}}"; System.out.println(str + " : " + checkBracket(str)); str = "()[]]{}"; System.out.println(str + " : " + checkBracket(str)); str = "()]{}"; System.out.println(str + " : " + checkBracket(str)); } /** * Check if string has matched brackets * * @param str * give string * @return true: string value has matched brackets, false: not have */ public static boolean checkBracket(String str) { if (str == null || str.isEmpty()) { return false; } Stack<Character> stack = new Stack<>(); try { for (char c : str.toCharArray()) { if (c == '(' || c == '[' || c == '{') { stack.push(c); continue; } if (c == ')') { if ('(' == stack.pop()) { continue; } else { break; } } if (c == ']') { if ('[' == stack.pop()) { continue; } else { break; } } if (c == '}') { if ('{' == stack.pop()) { continue; } else { break; } } } } catch (EmptyStackException e) { // This exception will occur when stack is empty but stack.pop is invoked. return false; } // Only when stack is empty, the given string has matched brackets. if (stack.isEmpty()) { return true; } return false; } }
输出:
() : true ()[] : true a(b)c[d]e{f}g : true (2+1)[5-8]{7*9}({55+98})([abc]def)g({sf}{ass}) : true (()[]{}) : true {(){}} : true ()[{}] : true ([)]{} : false (()[]{} : false ()[]{}} : false ()[]]{} : false ()]{} : false
-
Josephus问题
Josephus问题是下面的游戏:N个人编号从1到N,围坐成一个圆圈。从1号开始传递一个热土豆,。经过M次传递后拿着热土豆的人被清除离座,围坐的圆圈紧缩,由坐在被清除的人后面的人拿起热土豆继续进行游戏。最后剩下的人取胜。因此,如果M=1和N=5,则游戏人依序被清除,5号游戏人获胜。如果M=2和N=5,那么被清除的人的顺序是2,4,1,5,3号人获胜。
该问题有多种解法,可通过循环链表和队列的数据结构来实现,这里用队列实现。
思路:先将N个人依次入队,再逐一出队和重新入队并开始计数,每次计数到M的时候那个人就被踢出,并重新开始计数和出队入队,直到队列中只有一个人,此人获胜。
该问题利用了队列的先进先出原理(FIFO),源码如下:package com.my.study.algorithm.stackAndQueue; import java.util.Queue; import java.util.concurrent.ConcurrentLinkedQueue; /** * This class demonstrates how to use a queue to resolve Josephus cycle problem. * */ public class JosephusCycle { public static void main(String[] args) { josephus(5, 2); } private static void josephus(int persons, int k) { if (persons <= 0 || k <= 0) { throw new IllegalArgumentException("Parameters is incorrect: " + persons + ", k:" + k); } Queue<String> queue = new ConcurrentLinkedQueue<>(); // Initialize queue for (int i = 0; i < persons; i++) { queue.offer("Person_" + (i + 1)); } while (!queue.isEmpty()) { // Skip k -1 persons for (int i = 0; i < k - 1; i++) { queue.offer(queue.poll()); } // Check the number k person String name = queue.poll(); if (queue.isEmpty()) { System.out.println("Winner: " + name); } else { System.out.println("Eliminate: " + name); } } } }
输出:
Eliminate: Person_2 Eliminate: Person_4 Eliminate: Person_1 Eliminate: Person_5 Winner: Person_3
-
检查链表是否包含环
该问题检查给定的链表是否包含环,循环链表的last节点的next指向head节点,其包含环,是一种情况,第二种情况是last节点的next指向head的next.next...next,即部分包含环。
思路:给定两个指针p1和p2,初始情况都指向head节点,依次往后移动,p1每次移动一个单位,p2每次移动两个单位,如果在某个时间点p2与p1又重新指向了某一个相同的节点,那么原链表存在环,如果任何时候(遍历完链表所有节点)p2与p1都无法重新指向同一个节点,那么原链表不存在环。因为p2的移动速度比p1快,一般情况下p2都在p1前面,二者不可能重新相遇,除非存在环。
该问题考查了循环链表的特性,源码如下:package com.my.study.algorithm.stackAndQueue; /** * This class demonstrates how to check if a link contains a cycle. */ public class CheckLinkCycle { public static void main(String[] args) { MyLink<Object> objs = new MyLink<>(); System.out.println("Link size: " + objs.getSize()); System.out.println("Contains cycle? " + objs.isContainsCycle()); } private static class MyLink<E> { // Link size private int size; // Root node private Node<E> root; // Last node private Node<E> last; // Default link size private static final int DEFAULT_LINK_SIZE = 100; public MyLink() { this(DEFAULT_LINK_SIZE); } public MyLink(int size) { if (size <= 0) { size = DEFAULT_LINK_SIZE; } this.size = size; // Build link root = new Node<>(); Node<E> current = root; for (int i = 0; i < size - 1; i++) { current.next = new Node<>(); current = current.next; } last = current; // Case 1: last node's next points to root last.next = root; // Case 2: last node's next points to a middle node // last.next = root.next.next.next; } // Check if link contains a cycle, for example: // // * * * // * * // * * // * * * * // * // * // * // public boolean isContainsCycle() { Node<E> p1 = root; Node<E> p2 = root; for (int i = 0; i < size; i++) { if (p1.next == null || p2.next == null) { return false; } // p1 goes one step, but p2 goes two steps p1 = p1.next; p2 = p2.next; if (p2.next == null) { return false; } p2 = p2.next; // Cycle exists when p2 meets p1 if (p1 == p2) { return true; } } return false; } public int getSize() { return size; } private static class Node<E> { private E e; private Node<E> next; } } }
输出:
Link size: 100 Contains cycle? true
-
用两个栈实现队列
给定两个栈S1和S2,以及栈方法pop和push,要求实现一队列Queue的两个方法,offer,poll。
思路:栈的原理是FILO,队列的原理是FIFO,两个栈,一个用于入队,一个用于出队。当负责入队的那个栈满了,且出队的栈是空的,那么将入队的栈元素依次倒入出队的栈。如果出队的栈空了,且入队的栈不为空,那么将入队的栈元素依次倒入出队的栈。依次重复,即可用两个栈实现一个队列。注意,整个队列的容量(capacity)并不一定等于两个栈的容量和,因为可能出现入队的栈满了,但出队的栈不为空的情况。所以,队列的容量应该介于一个栈容量到两个栈容量之间。
该问题考查了栈和队列概念以及灵活运用,源码如下:package com.my.study.algorithm.stackAndQueue; import java.util.Stack; /** * This class demonstrates how to use two stacks to simulate a queue. */ public class TwoStacksQueue { public static void main(String[] args) { MyQueue<String> queue = new MyQueue<>(3); queue.offer("obj1"); queue.offer("obj2"); queue.offer("obj3"); queue.offer("obj4"); queue.offer("obj5"); queue.offer("obj6"); for (int i = 0; i < 6; i++) { String obj = queue.poll(); System.out.println(obj); } } static class MyQueue<E> { // Stack capacity private int stackCapacity; // s1 is used to offer elements private Stack<E> s1; // s2 is used to poll elements private Stack<E> s2; // Default stack size private static final int DEFAULT_STACK_SIZE = 5; public MyQueue() { this(DEFAULT_STACK_SIZE); } public MyQueue(int stackCapacity) { if (stackCapacity <= 0) { stackCapacity = DEFAULT_STACK_SIZE; } this.stackCapacity = stackCapacity; s1 = new Stack<E>(); s2 = new Stack<E>(); } public E poll() { if (s2.isEmpty()) { if (!s1.isEmpty()) { transfer(s1, s2); } else { throw new RuntimeException("Queue is empty, cannot poll element."); } } return s2.pop(); } public void offer(E e) { if (s1.size() >= stackCapacity) { if (s2.isEmpty()) { transfer(s1, s2); } else { throw new RuntimeException("Queue is full, cannot offer element."); } } s1.push(e); } public int size() { return s1.size() + s2.size(); } // Transfer all elements in stack "from" to stack "to" private void transfer(Stack<E> from, Stack<E> to) { to.clear(); while (!from.isEmpty()) { to.push(from.pop()); } from.clear(); } } }
输出:
obj1 obj2 obj3 obj4 obj5 obj6
-
自定义阻塞式链表队列
要求实现阻塞式链表队列,类似于JDK自带的类: LinkedBlockingQueue。
思路:阻塞式队列是生产者消费者模型的一个典型应用,一个或多个生产者负责生产产品,并将产品放入队列,一个或多个消费者负责消费产品,从队列中取出产品。如果队列满了,所有的生产者都将被阻塞,直到某个消费者消费了一个产品。如果队列为空,所有的消费者都将被阻塞,直到某个生产者生产了一个产品。为了防止并发问题,需要对临界资源(队列)加锁,每个生产者和消费者之间都互斥,为了实现阻塞,需要对生产者和消费者做同步处理。
该问题考查了并发环境下的同步与互斥,以及链表和队列的灵活运用,源码如下:package com.my.study.algorithm.stackAndQueue; import java.util.concurrent.locks.Condition; import java.util.concurrent.locks.ReentrantLock; /** * This class demonstrates the mechanism of LinkedBlockingQueue. * */ public class TestLinkedBlockingQueue { public static void main(String[] args) { MyLinkedBlockingQueue<String> queue = new MyLinkedBlockingQueue<String>(); new Thread(() -> { for (int i = 0; i < 20; i++) { String val = "str" + i; queue.offer(val); System.out.println("Offer:" + val); } }).start(); new Thread(() -> { for (int i = 0; i < 20; i++) { String str = queue.poll(); System.out.println("Poll:" + str); } }).start(); } private static class MyLinkedBlockingQueue<E> { // Queue head and last reference private Node<E> head; private Node<E> last; private int capacity; private int size; // ReentrantLock, used to synchronize offer and poll threads private ReentrantLock lock = new ReentrantLock(); // Condition notFull, means this queue should not be full, if queue is full, // then cannot offer element to queue, // notFull will be await, until a thread which has condition's lock permissions // invokes signalAll or signal method. private Condition notFull = lock.newCondition(); // The similar to notFull private Condition notEmpty = lock.newCondition(); private static final int DEFAULT_CAPACITY = 10; public MyLinkedBlockingQueue() { this(DEFAULT_CAPACITY); } public MyLinkedBlockingQueue(int capacity) { if (capacity <= 0) { capacity = DEFAULT_CAPACITY; } this.capacity = capacity; } /** * Insert element to queue, if queue if full, thread will be blocked until a * consumer thread invokes poll method. * * @param e * element */ public void offer(E element) { // Try to get lock permission lock.lock(); try { // Generally we need to use "while" instead of "if" here, but "if" is OK and // better than "while" here because we have two conditions to control consumer // and producer threads. if (size >= capacity) { try { notFull.await(); } catch (InterruptedException e) { throw new RuntimeException(e.getMessage()); } } // Initialize the first node, head and last should be null if (head == null) { head = new Node<E>(); last = head; } else { last.next = new Node<E>(); last = last.next; } last.element = element; size++; // Notify consumers notEmpty.signalAll(); } finally { // Need to unlock in finally block to make sure unlock successfully. lock.unlock(); } } /** * Retrieves and removes the head of this queue, if queue is empty, thread will * be blocked until a producer thread to invoke method offer. * * @return element */ public E poll() { lock.lock(); try { if (size <= 0) { try { notEmpty.await(); } catch (InterruptedException e) { throw new RuntimeException(e.getMessage()); } } E element = head.element; head = head.next; size--; // If there are only one element before polling, need to set last reference to // null. if (size == 0) { last = null; } // Notify producers notFull.signalAll(); return element; } finally { lock.unlock(); } } /** * Get queue size. * * @return queue size */ public int getSize() { return size; } private static class Node<E> { private E element; private Node<E> next; } } }
输出:
Poll:str0 Offer:str0 Offer:str1 Offer:str2 Offer:str3 Offer:str4 Offer:str5 Offer:str6 Poll:str1 Offer:str7 Poll:str2 Offer:str8 Poll:str3 Offer:str9 Poll:str4 Poll:str5 Poll:str6 Poll:str7 Poll:str8 Poll:str9 Poll:str10 Offer:str10 Offer:str11 Offer:str12 Offer:str13 Offer:str14 Offer:str15 Offer:str16 Offer:str17 Offer:str18 Offer:str19 Poll:str11 Poll:str12 Poll:str13 Poll:str14 Poll:str15 Poll:str16 Poll:str17 Poll:str18 Poll:str19