一、棧
1.1 棧的實現
棧(Stack
)是限制僅在表的一端進行插入和刪除運算的線性表。java
沒有棧這樣的數據結構,如果想利用先進后出(FILO
)這樣的數據結構,就必須自己實現。要實現Stack
,至少應該包括:
-
pop()
出棧操作,彈出棧頂元素。 -
push(E e)
入棧操作 -
peek()
查看棧頂元素 -
isEmpty()
棧為空
另外,實現一個棧,還應該考慮到幾個問題:
- 棧的初始大小以及棧滿以后如何新增棧空間
- 對棧進行更新時需要進行同步
(轉自:https://segmentfault.com/a/1190000002516799
)
有三種實現:
實現一:數組實現
package cn.list;
import java.util.EmptyStackException;
public class MyArrayStack {
private int[] array;// 用數組實現
private int top; // 棧頂指針
private final static int size = 100;
public MyArrayStack() {
array = new int[size];
top = -1; // 棧空的時候
}
// 壓棧
public void push(int element) {
if (top == size - 1) {
resize();//這里進行擴容
} else
array[++top] = element;
}
// 彈棧
public int pop() {
if (top == -1) {
throw new EmptyStackException();
}
return array[top--];
}
// 判斷是否為空
public boolean isEmpty() {
return top == -1;
}
// 返回棧頂元素
public Integer peek() {
if (top == -1) {
throw new EmptyStackException();
}
return array[top];
}
//返回棧中元素個數
public int size(){
return top + 1 ;
}
private void resize(){
assert size == array.length;//表示當條件成立時程序立即中止并返回一個錯誤消息
int[] newArr = new int[size * 2 + 1];
System.arraycopy(array, 0, newArr, 0, size);
array = newArr;
}
}
實現二:容器實現
package cn.list;
import java.util.ArrayList;
import java.util.EmptyStackException;
import java.util.List;
public class MyArrayListStack<T> implements Stack<T> {
private List<T> list; // 用容器實現
MyArrayListStack() {
list = new ArrayList<T>();
}
// 彈棧
public T pop() {
if (this.isEmpty() == true) {
throw new EmptyStackException();
}
return list.remove(list.size() - 1);
}
// 壓棧
public void push(T element) {
list.add(element);
}
// 判斷是否為空
public boolean isEmpty() {
return list.size() == 0;
}
// 返回棧頂元素
public T peek() {
if (this.isEmpty() == true) {
throw new EmptyStackException();
}
return list.get(list.size() - 1);
}
public int size(){
return list.size();
}
}
interface Stack<T> {
public T pop();
public void push(T element);
public boolean isEmpty();
public T peek();
public int size();
}
實現三:鏈表實現
package cn.list;
import java.util.LinkedList;
//自己實現棧,使用LinkedList實現
public class MyLinkedListStack<T> {
private LinkedList<T> list = new LinkedList<T>();
public MyLinkedListStack() {
}
public void clear() {
list.clear();
}
public boolean isEmpty() {
return list.isEmpty();
}
public T peek() {
if (isEmpty()) {
throw new java.util.EmptyStackException();
}
return list.getLast();
}
public T pop() {
if (isEmpty()) {
throw new java.util.EmptyStackException();
}
return list.removeLast();
}
public void push(T element) {
list.addLast(element);
}
public int size(){
return list.size();
}
}
1.2 棧的應用
(轉自:http://blog.csdn.net/hengjie2009/article/details/8478863
)
1.2.1 平衡符號
問題描述: 在編寫代碼并且編譯時,難免會因為少寫了一個')'
和被編譯器報錯。也就是說,編譯器會去匹配括號是否匹配。當你輸入了一個'('
,很自然編譯器回去檢查你是否有另一個')'
符號與之匹配。如果所有的括號都能夠成對出現,那么編譯器是能夠通過的。否則編譯器會報錯。例如字符序列“(a+b)”
是匹配的, 而字符序列"(a+b]"
則不是。
算法描述如下: 創建一個空棧,讀取字符序列直到結尾。如果字符是開放符號'(''[''{'
,將其入棧;如果是一個封閉符號')'']''}'
,則當棧為空時報錯。否則,將棧頂元素彈出。如果彈出的符號不是對應的開放符號,則報錯。當字符序列結束,判斷棧是否為空,為空則報錯
package cn.list;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileInputStream;
import java.io.InputStreamReader;
import java.util.Stack;
//棧的應用:平衡符號
public class BalanceSigned {
public static boolean isBalanceChar() {
Stack<Character> stack = new Stack<Character>();
String path = "D:/a.txt";
File file = new File(path);
try {
FileInputStream fis = new FileInputStream(file);
InputStreamReader isr = new InputStreamReader(fis);
BufferedReader br = new BufferedReader(isr);
String line = "";
while ((line = br.readLine()) != null) {
for (int i = 0; i < line.length(); i++) {
switch (line.charAt(i)) {
case '[':
stack.push('[');
break;
case '(':
stack.push('(');
break;
case '{':
stack.push('{');
break;
case '/':
if (i < line.length() - 1 && line.charAt(i + 1) == '*') {
stack.push('/');
stack.push('*');
}
break;
case ']':
if (stack.size() == 0 || stack.pop() != '[') {
System.out.print("illigal character" + "[]");
return false;
}
break;
case ')':
if (stack.size() == 0 || stack.pop() != '(') {
System.out.print("illigal character" + "()");
return false;
}
break;
case '}':
if (stack.size() == 0 || stack.pop() != '{') {
System.out.print("illigal character" + "{}");
return false;
}
break;
case '*':
if ((i < line.length() - 1 && line.charAt(i + 1) == '/')
&& (stack.size() < 2 || (stack.pop() != '*' || stack
.pop() != '/'))) {
System.out.print("illigal character" + "");
return false;
}
break;
default:
break;
}
}
}
if (stack.size() != 0) {
System.out.print("error");
return false;
}
} catch (Exception e) {
e.printStackTrace();
}
return true;
}
public static void main(String[] args) {
boolean flag = isBalanceChar();
if (flag) {
System.out.println("是平衡符號");
} else {
System.out.println("不是平衡符號");
}
}
}
1.2.2 中綴表達式轉后綴表達式
package cn.list;
/**
* 工具類:
* 1、中綴表達式轉化為后綴表達式
* 2、給出一個算術表達式(中綴表達式),直接得到計算結果
*/
import java.util.Stack;
import java.util.regex.Pattern;
//中綴表達式到后綴表達式的轉換
public class Infix2Suffix {
private Infix2Suffix() {}
// 方法:給出一個算術表達式(中綴表達式),得到計算結果。 例如 (5+8+10)*1,返回23
public static double stringToArithmetic(String string) {
return suffixToArithmetic(infixToSuffix(string));
}
/**
* 中綴表達式轉后綴表達式 只處理了+,-,*,/和括號,沒有處理負號及其它運算符,也沒對前綴表達式驗證。*/
// 方法:中綴表達式轉成后綴表達式
public static String infixToSuffix(String infix) {
Stack<Character> stack = new Stack<Character>();
String suffix = "";
int length = infix.length();
for (int i = 0; i < length; i++) {
Character temp;
char c = infix.charAt(i);
switch (c) {
// 忽略空格
case ' ':
break;
// 碰到'(',push到棧
case '(':
stack.push(c);
break;
// 碰到'+''-',將棧中所有運算符彈出,送到輸出隊列中
case '+':
case '-':
while (stack.size() != 0) {
temp = stack.pop();
if (temp == '(') {
stack.push('(');
break;
}
suffix += " " + temp;
}
stack.push(c);
suffix += " ";
break;
// 碰到'*''/',將棧中所有乘除運算符彈出,送到輸出隊列中
case '*':
case '/':
while (stack.size() != 0) {
temp = stack.pop();
if (temp == '(' || temp == '+' || temp == '-') {
stack.push(temp);
break;
} else {
suffix += " " + temp;
}
}
stack.push(c);
suffix += " ";
break;
// 碰到右括號,將靠近棧頂的第一個左括號上面的運算符全部依次彈出,送至輸出隊列后,再丟棄左括號
case ')':
while (stack.size() != 0) {
temp = stack.pop();
if (temp == '(')
break;
else
suffix += " " + temp;
}
// suffix += " ";
break;
// 如果是數字,直接送至輸出序列
default:
suffix += c;
}
}
// 如果棧不為空,把剩余的運算符依次彈出,送至輸出序列。
while (stack.size() != 0) {
suffix += " " + stack.pop();
}
return suffix;
}
// 方法:通過后綴表達式求出算術結果
public static double suffixToArithmetic(String postfix) {
Pattern pattern = Pattern.compile("\\d+||(\\d+\\.\\d+)"); // 使用正則表達式匹配數字
String strings[] = postfix.split(" "); // 將字符串轉化為字符串數組
for (int i = 0; i < strings.length; i++){
strings[i].trim(); // 去掉字符串首尾的空格
}
Stack<Double> stack = new Stack<Double>();
for (int i = 0; i < strings.length; i++) {
if (strings[i].equals(""))
continue;
// 如果是數字,則進棧
if ((pattern.matcher(strings[i])).matches()) {
stack.push(Double.parseDouble(strings[i]));
} else {
// 如果是運算符,彈出運算數,計算結果。
double y = stack.pop();
double x = stack.pop();
stack.push(caculate(x, y, strings[i])); // 將運算結果重新壓入棧。
}
}
return stack.pop(); // 彈出棧頂元素就是運算最終結果。
}
private static double caculate(double x, double y, String simble) {
if (simble.trim().equals("+"))
return x + y;
if (simble.trim().equals("-"))
return x - y;
if (simble.trim().equals("*"))
return x * y;
if (simble.trim().equals("/"))
return x / y;
return 0;
}
}
說明:中綴表達式如a+b*c+(d*e+f)*g
會轉換成abc*+de*f+g*+
。
二、隊列
(摘自:http://www.cnblogs.com/smyhvae/p/4793339.html
)
隊列和棧的實現其實差不多,但是要注意一個問題,隊列是從尾部添加(入隊),從頭部刪除(出隊),有兩種實現方式(數組實現和鏈表實現),對于數組實現有“假溢出”的問題,即經過一些列的出隊和入隊,導致隊列頭尾標記都指向了數組的尾部,此時雖然數組未滿,但是卻不能再插入數據了,于是我們需要實現循環,即當尾部標記指向數組的尾部且數組未滿的情況下,讓尾部標記在下一次插入的時候指向數組的頭位置。
這里我們給出兩種實現方式:
方式一:數組實現
package cn.list;
//循環順序隊列,數組實現
public class MyArrayQueue implements Queue {
static final int defaultSize = 10; // 默認隊列的長度
int front; // 隊頭
int rear; // 隊尾
int count; // 統計元素個數的計數器
int maxSize; // 隊的最大長度
Object[] queue; // 隊列
public MyArrayQueue() {
init(defaultSize);
}
public MyArrayQueue(int size) {
init(size);
}
public void init(int size) {
maxSize = size;
front = rear = 0;
count = 0;
queue = new Object[size];
}
public void append(Object obj) throws Exception {
if (count > 0 && front == rear) {
throw new Exception("隊列已滿!");
}
queue[rear] = obj;
//當隊列滿時讓尾部標記重新指向數組開始位置,達到循環的目的
rear = (rear + 1) % maxSize;
count++;
}
public Object delete() throws Exception {
if (isEmpty()) {
throw new Exception("隊列為空!");
}
Object obj = queue[front];
//開始標記在到達數組最后一個位置時也需要循環
front = (front + 1) % maxSize;
count--;
return obj;
}
//取得隊列頭
public Object getFront() throws Exception {
if (!isEmpty()) {
return queue[front];
} else {
return null;
}
}
public boolean isEmpty() {
return count == 0;
}
}
// 隊列接口
interface Queue {
// 入隊
public void append(Object obj) throws Exception;
// 出隊
public Object delete() throws Exception;
// 獲得隊頭元素
public Object getFront() throws Exception;
// 判斷對列是否為空
public boolean isEmpty();
}
方式二:鏈表實現
package cn.list;
//實現鏈式隊列
public class MyListQueue implements Queue {
Node front; //隊頭
Node rear; //隊尾
int count; //計數器
public MyListQueue() {
init();
}
public void init() {
front = rear = null;
count = 0;
}
public void append(Object obj) throws Exception {
Node node = new Node(obj, null);
//如果當前隊列不為空。
if (rear != null) {
rear.next = node; //隊尾結點指向新結點
}
rear = node; //設置隊尾結點為新結點
//說明要插入的結點是隊列的第一個結點
//此時頭尾指向同一個節點
if (front == null) {
front = node;
}
count++;
}
public Object delete() throws Exception {
if (isEmpty()) {
new Exception("隊列已空!");
}
Node node = front;
front = front.next;
count--;
return node.getElement();
}
public Object getFront() throws Exception {
if (!isEmpty()) {
return front.getElement();
} else {
return null;
}
}
public boolean isEmpty() {
return count == 0;
}
}
//結點類
class Node {
Object element; //數據域
Node next; //指針域
//頭結點的構造方法
public Node(Node nextval) {
this.next = nextval;
}
//非頭結點的構造方法
public Node(Object obj, Node nextval) {
this.element = obj;
this.next = nextval;
}
//獲得當前結點的后繼結點
public Node getNext() {
return this.next;
}
//獲得當前的數據域的值
public Object getElement() {
return this.element;
}
//設置當前結點的指針域
public void setNext(Node nextval) {
this.next = nextval;
}
//設置當前結點的數據域
public void setElement(Object obj) {
this.element = obj;
}
public String toString() {
return this.element.toString();
}
}
三、棧和隊列的應用
(摘自:http://www.cnblogs.com/smyhvae/p/4795984.html
)
3.1 應用一:判斷回文
隊列有一個很常見的應用就是判斷字符串是否是回文字符串(從頭看和從尾看是一樣的,如soros
),這可以將此字符串分別加入到棧和隊列中,然后分別出棧和出隊進行比較,如果都是一樣的則表示是回文字符串。
3.2 棧實現隊列
package cn.list;
import java.util.Stack;
//使用兩個棧實現一個隊列
//說的通俗一點,現在把數據1、2、3分別入棧一,然后從棧一中出來(3、2、1),放到棧二中,
//那么,從棧二中出來的數據(1、2、3)就符合隊列的規律了,即負負得正。
public class MyStackQueue {
private Stack<Integer> stack1 = new Stack<Integer>();// 執行入隊操作的棧
private Stack<Integer> stack2 = new Stack<Integer>();// 執行出隊操作的棧
// 方法:給隊列增加一個入隊的操作
public void push(int data) {
stack1.push(data);
}
// 方法:給隊列正價一個出隊的操作
public int pop() throws Exception {
// stack1中的數據放到stack2之前,先要保證stack2里面是空的(要么一開始就是空的,
//要么是stack2中的數據出完了),不然出隊的順序會亂的,這一點很容易忘
if (stack2.empty()) {
while (!stack1.empty()) {
// 把stack1中的數據出棧,放到stack2中【核心代碼】
stack2.push(stack1.pop());
}
}
// stack2為空時,有兩種可能:1、一開始,兩個棧的數據都是空的;2、stack2中的數據出完了
if (stack2.empty()) {
throw new Exception("隊列為空");
}
//如果stack2不為空,則直接出棧
return stack2.pop();
}
}
3.3 隊列實現棧
package cn.list;
import java.util.ArrayDeque;
import java.util.Queue;
//使用隊列實現棧
public class MyQueueStack {
Queue<Integer> queue1 = new ArrayDeque<Integer>();
Queue<Integer> queue2 = new ArrayDeque<Integer>();
// 方法:入棧操作
public void push(int data) {
queue1.add(data);
}
// 方法:出棧操作
/*將1、2、3依次入隊列一, 然后最上面的3留在隊列一,
* 將下面的1、2入隊列二,將3出隊列一,此時隊列一空了,然后把隊列二中的所有數據入隊列一;
* 將最上面的2留在隊列一,將下面的3入隊列二。。。依次循環。
* */
public int pop() throws Exception {
int data;
if (queue1.size() == 0) {
throw new Exception("棧為空");
}
while (queue1.size() != 0) {
if (queue1.size() == 1) {
data = queue1.poll();
while (queue2.size() != 0) { // 把queue2中的全部數據放到隊列一中
queue1.add(queue2.poll());
}
return data;
}
queue2.add(queue1.poll());
}
throw new Exception("棧為空");// 不知道這一行的代碼是什么意思
}
}
3.4 設計含最小函數min()的棧
(摘自:http://blog.csdn.net/sgbfblog/article/details/7752878
)
要求min、push、pop
的時間復雜度都是O(1)
分析:
很剛開始很容易想到一個方法,那就是額外建立一個最小堆保存所有元素,這樣每次獲取最小元素只需要O(1)
的時間。但是這樣的話,PUSH
和POP
操作就需要O(lgn)
的時間了(假定棧中元素個數為n
),不符合題目的要求。可以使用一個輔助棧。
解法:
使用一個輔助棧來保存最小元素,這個解法簡單不失優雅。設該輔助棧名字為minStack
,其棧頂元素為當前棧中的最小元素。這意味著要獲取當前棧中最小元素,只需要返回minStack
的棧頂元素即可。每次執行push
操作,檢查push
的元素是否小于或等于minStack
棧頂元素。如果是,則也push
該元素到minStack
中。當執行pop
操作的時候,檢查pop
的元素是否與當前最小值相等。如果相同,則需要將改元素從minStack
中pop
出去。
實例:
假定有元素3, 2, 5, 4, 2, 1依次入棧,則原始棧中元素為(1), 輔助棧中元素為(2)
(1) | (2) |
---|---|
1 | null |
2 | null |
4 | 1 |
5 | 2 |
2 | 2 |
3 | 3 |
這樣,第1次pop
時,1從兩個棧都pop
出去;第2次pop
時,2從兩個棧都pop
出去;第3次pop
,元素4從原始棧pop
出去,輔助棧不用pop
;第4次pop
,元素5從原始棧pop
出去,輔助棧不需pop
;第5次pop
,元素2從兩個棧pop
出去;第6次pop
,元素3從兩個棧都pop
出去。我們可以發現,每次push
或者pop
后,輔助棧的棧頂元素總是當前棧的最小元素。
package cn.list;
import java.util.Stack;
public class GetMinStack {
private Stack<Integer> stack = new Stack<Integer>();
//輔助棧:棧頂永遠保存stack中當前的最小的元素
private Stack<Integer> minStack = new Stack<Integer>();
public void push(int data) {
stack.push(data); //直接往棧中添加數據
//在輔助棧中需要做判斷
if (minStack.size() == 0 || data <= minStack.peek()) {
minStack.push(data);
} else {
minStack.add(minStack.peek()); //【核心代碼】peek方法返回的是棧頂的元素
}
}
public int pop() throws Exception {
if (stack.size() == 0) {
throw new Exception("棧中為空");
}
int data = stack.pop();
minStack.pop(); //核心代碼
return data;
}
public int min() throws Exception {
if (minStack.size() == 0) {
throw new Exception("棧中空了");
}
return minStack.peek();
}
}
3.5 判斷棧的push和pop序列是否一致
已知一組數據1、2、3、4、5依次進棧,那么它的出棧方式有很多種,請判斷一下給出的出棧方式是否是正確的?
例如:
數據:
1、2、3、4、5
出棧1:
5、4、3、2、1(正確)
出棧2:
4、5、3、2、1(正確)
出棧3:
4、3、5、1、2(錯誤)
package cn.list;
import java.util.Stack;
/*
* 判斷棧的push和pop序列是否一致
* 通俗一點講:已知一組數據1、2、3、4、5依次進棧,
* 那么它的出棧方式有很多種,請判斷一下給出的出棧方式是否是正確的?
*
* 例如:
數據:
1、2、3、4、5
出棧1:
5、4、3、2、1(正確)
出棧2:
4、5、3、2、1(正確)
出棧3:
4、3、5、1、2(錯誤)
* */
public class StackTest {
//方法:data1數組的順序表示入棧的順序。現在判斷data2的這種出棧順序是否正確
public static boolean sequenseIsPop(int[] data1, int[] data2) {
Stack<Integer> stack = new Stack<Integer>(); //這里需要用到輔助棧
for (int i = 0, j = 0; i < data1.length; i++) {
stack.push(data1[i]);
while (stack.size() > 0 && stack.peek() == data2[j]) {
stack.pop();
j++;
}
}
return stack.size() == 0;
}
public static void main(String[] args) {
Stack<Integer> stack = new Stack<Integer>();
int[] data1 = {1, 2, 3, 4, 5};
int[] data2 = {4, 5, 3, 2, 1};
int[] data3 = {4, 5, 2, 3, 1};
System.out.println(sequenseIsPop(data1, data2));
System.out.println(sequenseIsPop(data1, data3));
}
}
說明:下面我們看此題的原理:
我們以前面的序列4、5、3、2、1為例。第一個希望被
pop
出來的數字是4,因此4需要先push
到棧里面。由于push
的順序已經由push
序列確定了,也就是在把4push
進棧之前,數字1,2,3都需要push
到棧里面。此時棧里的包含4個數字,分別是1,2,3,4,其中4位于棧頂。把4pop
出棧后,剩下三個數字1,2,3。接下來希望被pop
的是5,由于仍然不是棧頂數字,我們接著在push
序列中4以后的數字中尋找。找到數字5后再一次push
進棧,這個時候5就是位于棧頂,可以被pop
出來。接下來希望被pop
的三個數字是3,2,1。每次操作前都位于棧頂,直接pop
即可。再來看序列4、3、5、1、2。
pop
數字4的情況和前面一樣。把4pop
出來之后,3位于棧頂,直接pop
。接下來希望pop
的數字是5,由于5不是棧頂數字,我們到push
序列中沒有被push
進棧的數字中去搜索該數字,幸運的時候能夠找到5,于是把5push
進入棧。此時pop
5之后,棧內包含兩個數字1、2,其中2位于棧頂。這個時候希望pop
的數字是1,由于不是棧頂數字,我們需要到push
序列中還沒有被push
進棧的數字中去搜索該數字。但此時push
序列中所有數字都已被push
進入棧,因此該序列不可能是一個pop
序列。也就是說,如果我們希望
pop
的數字正好是棧頂數字,直接pop
出棧即可;如果希望pop
的數字目前不在棧頂,我們就到push
序列中還沒有被push
到棧里的數字中去搜索這個數字,并把在它之前的所有數字都push
進棧。如果所有的數字都被push
進棧仍然沒有找到這個數字,表明該序列不可能是一個pop
序列。
通過此原理我們可以手工進行判斷。