前言:題圖無關,隻是好看,接下來就來複習一下棧和隊列的相關知識
前序文章:
- 資料結構與算法(1)——數組與連結清單(https://www.jianshu.com/p/7b93b3570875)
棧
什麼是棧
棧是一種用于存儲資料的簡單資料結構(與連結清單類似)。資料入棧的次序是棧的關鍵。可以把一桶桶裝的薯片看作是一個棧的例子,當薯片做好之後,它們會依次被添加到桶裡,每一片都會是目前的最上面一片,而每次我們取的時候也是取的最上面的那一片,規定你不能破壞桶也不能把底部捅穿,是以第一個放入桶的薯片隻能最後一個從桶裡取出;
定義:棧(Stack)是一個有序線性表,隻能在表的一端(稱為棧頂,top)執行插入和删除操作。最後插入的元素将第一個被删除,是以棧也稱為後進先出(Last In First Out,LIFO)或先進後出(First In Last Out)線性表;
兩個改變棧的操作都有專用名稱。一個稱為入棧(push),表示在棧中插入一個元素;另一個稱為出棧(pop),表示從棧中删除一個元素。試圖對一個空棧執行棧操作稱為下溢(underflow);試圖對一個滿棧執行棧操作稱為溢出(overflow)。通常,溢出和下溢均認為是異常;
棧的應用
- 無處不在的Undo操作(撤銷);
- 程式調用的系統棧;
- 括号/符号比對;
- 等等等等....
棧抽象資料類型
下面給出棧抽象資料類型中的操作,為了簡單起見,假設資料類型為整型;
棧的主要操作
-
:将data(資料)插入棧;void push(int data)
-
:删除并傳回最後一個插入棧的元素;int pop()
棧的輔助操作
-
:傳回最後一個插入棧的元素,但不删除;int top()
-
:傳回存儲在棧中元素的個數;int size()
-
:判斷棧中是否有元素;int isEmpty()
-
:判斷棧中是否存滿元素;int isStackFull()
動态數組簡單實作棧結構
我們結合之前建立的Array類,我們能夠很好的建立屬于我們自己的動态數組實作的棧結構,對于使用者來說,我們隻需要完成我們的相關操作,并且知道我能夠不斷地往裡添加元素而不出錯就行了,是以我們先來定義一個通用的棧接口:
public interface Stack<E> {
int getSize();
boolean isEmepty();
void push(E e);
E pop();
E top();
}
然後我們往之前的動态數組中添加兩個使用者友好的方法:
public E getLast() {
return get(size - 1);
}
public E getFirst() {
return get(0);
}
然後實作自己的動态數組為底層的棧結構就輕松多了:
public class ArrayStack<E> implements Stack<E> {
Array<E> array;
public ArrayStack(int capacity) {
array = new Array<>(capacity);
}
public ArrayStack() {
array = new Array<>();
}
@Override
public int getSize() {
return array.getSize();
}
@Override
public boolean isEmepty() {
return array.isEmpty();
}
public int getCapacity() {
return array.getCapacity();
}
@Override
public void push(E e) {
array.addLast(e);
}
@Override
public E pop() {
return array.removeLast();
}
@Override
public E top() {
return array.getLast();
}
@Override
public String toString() {
StringBuilder res = new StringBuilder();
res.append("Stack:");
res.append("[");
for (int i = 0; i < array.getSize(); i++) {
res.append(array.get(i));
if (i != array.getSize() - 1) {
res.append(",");
}
}
res.append("]");
return res.toString();
}
}
簡單複雜度分析
從代碼中可以看出,幾乎所有的時間複雜度都為O(1)級别,比較特别的是
push()
和
pop()
操作可能涉及到底層數組的擴容或縮容的操作,是以是均攤下來的複雜度;
隊列
什麼是隊列
隊列是一種用于存儲資料的資料結構(與連結清單和棧類似),資料到達的次序是隊列的關鍵;在日常生活中隊列是指從序列的開始按照順序等待服務的一隊人或物;
定義:隊列是一種隻能在一端插入(隊尾),在另一端删除(隊首)的有序線性表。隊列中第一個插入的元素也是第一個被删除的元素,是以隊列是一種先進先出(FIFO,First In First Out)或後進後出(LiLO,Last In Last Out)線性表;
與棧類似,兩個改變隊列的操作各有專用名稱;在隊列中插入一個元素,稱為入隊(EnQueue),從隊列中删除一個元素,稱為出隊(DeQueue);試圖對一個空隊列執行出隊操作稱為下溢(underflow),試圖對一個滿隊列執行入隊操作稱為溢出(overflow);通常認為溢出和下溢是異常。
隊列的一些應用舉例
- 作業系統根據(具有相同優先級的)任務到達的順序排程任務(例如列印隊列);
- 模拟現實世界中的隊列,如售票櫃台前的隊伍,或者任何需要先來先服務的場景;
- 多道程式設計;
- 異步資料傳輸(檔案輸入輸出、管道、套接字);
- 等等等等...
動态數組簡單實作隊列結構
我們仍然定義一個Queue接口來說明我們隊列中常用的一些方法:
public interface Queue<E> {
int getSize();
boolean isEmpty();
void enqueue(E e);
E dequeue();
E getFront();
}
借由我們之前自己實作的動态數組,那麼我們的隊列就很簡單了:
public class ArrayQueue<E> implements Queue<E> {
private Array<E> array;
public ArrayQueue(int capacity){
array = new Array<>(capacity);
}
public ArrayQueue(){
array = new Array<>();
}
@Override
public int getSize(){
return array.getSize();
}
@Override
public boolean isEmpty(){
return array.isEmpty();
}
public int getCapacity(){
return array.getCapacity();
}
@Override
public void enqueue(E e){
array.addLast(e);
}
@Override
public E dequeue(){
return array.removeFirst();
}
@Override
public E getFront(){
return array.getFirst();
}
@Override
public String toString(){
StringBuilder res = new StringBuilder();
res.append("Queue: ");
res.append("front [");
for(int i = 0 ; i < array.getSize() ; i ++){
res.append(array.get(i));
if(i != array.getSize() - 1)
res.append(", ");
}
res.append("] tail");
return res.toString();
}
}
簡單的複雜度分析
-
:O(1)(均攤)void enquque(E)
-
:O(n)E dequeue()
-
:O(1)E front()
-
int getSize()
-
boolean isEmpty()
實作自己的循環隊列
循環隊列的實作其實就是維護了一個front和一個tail分别指向頭和尾,然後需要特别注意的呢是判定隊滿和隊空的條件:
- 隊空:
,這沒啥好說的;front == tail
- 隊滿:
,這裡其實是有意浪費了一個空間,不然就判定不了到底是隊空還是隊滿了,因為條件都一樣...tail + 1 == front
public class LoopQueue<E> implements Queue<E> {
private E[] data;
private int front, tail;
private int size;
public LoopQueue(int capacity){
data = (E[])new Object[capacity + 1];
front = 0;
tail = 0;
size = 0;
}
public LoopQueue(){
this(10);
}
public int getCapacity(){
return data.length - 1;
}
@Override
public boolean isEmpty(){
return front == tail;
}
@Override
public int getSize(){
return size;
}
@Override
public void enqueue(E e){
if((tail + 1) % data.length == front)
resize(getCapacity() * 2);
data[tail] = e;
tail = (tail + 1) % data.length;
size ++;
}
@Override
public E dequeue(){
if(isEmpty())
throw new IllegalArgumentException("Cannot dequeue from an empty queue.");
E ret = data[front];
data[front] = null;
front = (front + 1) % data.length;
size --;
if(size == getCapacity() / 4 && getCapacity() / 2 != 0)
resize(getCapacity() / 2);
return ret;
}
@Override
public E getFront(){
if(isEmpty())
throw new IllegalArgumentException("Queue is empty.");
return data[front];
}
private void resize(int newCapacity){
E[] newData = (E[])new Object[newCapacity + 1];
for(int i = 0 ; i < size ; i ++)
newData[i] = data[(i + front) % data.length];
data = newData;
front = 0;
tail = size;
}
@Override
public String toString(){
StringBuilder res = new StringBuilder();
res.append(String.format("Queue: size = %d , capacity = %d\n", size, getCapacity()));
res.append("front [");
for(int i = front ; i != tail ; i = (i + 1) % data.length){
res.append(data[i]);
if((i + 1) % data.length != tail)
res.append(", ");
}
res.append("] tail");
return res.toString();
}
}
-
void enquque(E)
-
E dequeue()
-
E front()
-
int getSize()
-
boolean isEmpty()
簡單數組隊列和循環隊列的簡單比較
我們來簡單對比一下兩個隊列的性能吧,這裡直接上代碼:
// 測試使用q運作opCount個enqueueu和dequeue操作所需要的時間,機關:秒
private static double testQueue(Queue<Integer> q, int opCount){
long startTime = System.nanoTime();
Random random = new Random();
for(int i = 0 ; i < opCount ; i ++)
q.enqueue(random.nextInt(Integer.MAX_VALUE));
for(int i = 0 ; i < opCount ; i ++)
q.dequeue();
long endTime = System.nanoTime();
return (endTime - startTime) / 1000000000.0;
}
public static void main(String[] args) {
int opCount = 100000;
ArrayQueue<Integer> arrayQueue = new ArrayQueue<>();
double time1 = testQueue(arrayQueue, opCount);
System.out.println("ArrayQueue, time: " + time1 + " s");
LoopQueue<Integer> loopQueue = new LoopQueue<>();
double time2 = testQueue(loopQueue, opCount);
System.out.println("LoopQueue, time: " + time2 + " s");
}
我這裡的測試結果是這樣的,大家也就可見一斑啦:
其實ArrayQueue慢主要是因為出棧時每次都需要把整個結構往前挪一下
LeetCode 相關題目整理
20.有效的括号
我的答案:(10ms)
public boolean isValid(String s) {
// 正确性判斷
if (null == s || s.length() == 1) {
return false;
}
Stack<Character> stack = new Stack<>();
// 周遊輸入的字元
for (int i = 0; i < s.length(); i++) {
char c = s.charAt(i);
// 如果為左括号則push進棧
if (c == '(' || c == '[' || c == '{') {
stack.push(c);
} else {
if (stack.isEmpty()) {
return false;
}
char topChar = stack.pop();
if (c == ')' && topChar != '(') {
return false;
}
if (c == ']' && topChar != '[') {
return false;
}
if (c == '}' && topChar != '{') {
return false;
}
}
}
// 最後棧為空才能傳回true
return stack.isEmpty();
}
參考答案:(8ms)
public boolean isValid(String s) {
// 正确性判斷
if (0 == s.length()) {
return true;
}
if (s.length() % 2 == 1) {
return false;
}
Stack<Character> stack = new Stack();
char[] cs = s.toCharArray();
for (int i = 0; i < cs.length; i++) {
if (cs[i] == '(' || cs[i] == '[' || cs[i] == '{') {
stack.push(cs[i]);
} else {
if (stack.isEmpty()) {
return false;
}
char c = stack.pop();
if ((cs[i] == ')' && c == '(') || (cs[i] == '}' && c == '{') || (cs[i] == ']' && c == '[')) {
} else {
return false;
}
}
}
return stack.isEmpty();
}
155. 最小棧(劍指Offer面試題30)
參考答案(107ms)
class MinStack {
// 資料棧,用于存放插入的資料
private Stack<Integer> dataStack;
// 最小數位置棧,存放資料棧中最小的數的位置
private Stack<Integer> minStack;
/**
* initialize your data structure here.
*/
public MinStack() {
this.dataStack = new Stack<>();
this.minStack = new Stack<>();
}
/**
* 元素入棧
*
* @param x 入棧的元素
*/
public void push(int x) {
dataStack.push(x);
// 如果最小棧是空的,隻要将元素入棧
if (minStack.isEmpty()) {
minStack.push(x);
}
// 如果最小棧中有資料
else {
minStack.push(Math.min(x, minStack.peek()));
}
}
/**
* 出棧方法
*/
public void pop() {
// 如果棧已經為空,則傳回(LeetCode不能抛異常...)
if (dataStack.isEmpty()) {
return;
}
// 如果有資料,最小數位置棧和資料棧必定是有相同的元素個數,
// 兩個棧同時出棧
minStack.pop();
dataStack.pop();
}
/**
* 傳回棧頂元素
*
* @return 棧頂元素
*/
public int top() {
return dataStack.peek();
}
/**
* 擷取棧中的最小元素
*
* @return 棧中的最小元素
*/
public int getMin() {
// 如果最小數公位置棧已經為空(資料棧中已經沒有資料了),則抛出異常
if (minStack.isEmpty()) {
return 0;
}
// 擷取資料占中的最小元素,并且傳回結果
return minStack.peek();
}
}
改進答案:
上面求解方法的主要問題在于,每次push操作時,minStack也執行了一次push操作(新元素或目前的最小元素),也就是說,重複執行了最小值的入棧操作,是以現在我們來修改算法降低空間複雜度。仍然需要設定一個minStack,但是隻有當從dataStack中出棧的元素等于minStack棧頂元素時,才對minStack執行出棧的操作;也隻有當dataStack入棧的元素小于或等于目前最小值時,才對minStack執行入棧操作,下面就簡單寫一下了主要看一下出棧和入棧實作的邏輯就好了:
class MinStack {
private Stack<Integer> dataStack;
private Stack<Integer> minStack;
public MinStack() {
this.dataStack = new Stack<>();
this.minStack = new Stack<>();
}
public void push(int x) {
dataStack.push(x);
if (minStack.isEmpty() || minStack.peek() >= (Integer) x) {
minStack.push(x);
}
}
public void pop() {
if (dataStack.isEmpty()) {
return;
}
Integer minTop = minStack.peek();
Integer dataTop = dataStack.peek();
if (minTop.intValue() == dataTop.intValue()) {
minStack.pop();
}
dataStack.pop();
}
public int top() {
return dataStack.peek();
}
public int getMin() {
return minStack.peek();
}
}
225. 用隊列實作棧
我的答案:(118ms)
class MyStack {
private Queue<Integer> queue1;
private Queue<Integer> queue2;
/**
* Initialize your data structure here.
*/
public MyStack() {
queue1 = new LinkedList<>();
queue2 = new LinkedList<>();
}
/**
* Push element x onto stack.
*/
public void push(int x) {
if (queue1.isEmpty()) {
queue2.offer(x);
} else {
queue1.offer(x);
}
}
/**
* Removes the element on top of the stack and returns that element.
*/
public int pop() {
int size;
if (!queue1.isEmpty()) {
size = queue1.size();
for (int i = 0; i < size - 1; i++) {
queue2.offer(queue1.poll());
}
return queue1.poll();
} else {
size = queue2.size();
for (int i = 0; i < size - 1; i++) {
queue1.offer(queue2.poll());
}
return queue2.poll();
}
}
/**
* Get the top element.
*/
public int top() {
int size;
if (!queue1.isEmpty()) {
size = queue1.size();
for (int i = 0; i < size - 1; i++) {
queue2.offer(queue1.poll());
}
int result = queue1.peek();
queue2.offer(queue1.poll());
return result;
} else {
size = queue2.size();
for (int i = 0; i < size - 1; i++) {
queue1.offer(queue2.poll());
}
int result = queue2.peek();
queue1.offer(queue2.poll());
return result;
}
}
/**
* Returns whether the stack is empty.
*/
public boolean empty() {
return queue1.isEmpty() && queue2.isEmpty();
}
}
參考答案:(121ms)
class MyStack {
Queue<Integer> q;
/**
* Initialize your data structure here.
*/
public MyStack() {
this.q = new LinkedList<Integer>();
}
/**
* Push element x onto stack.
*/
public void push(int x) {
q.add(x);
}
/**
* Removes the element on top of the stack and returns that element.
*/
public int pop() {
int size = q.size();
for (int i = 0; i < size - 1; i++) {
q.add(q.remove());
}
return q.remove();
}
/**
* Get the top element.
*/
public int top() {
int size = q.size();
for (int i = 0; i < size - 1; i++) {
q.add(q.remove());
}
int ret = q.remove();
q.add(ret);
return ret;
}
/**
* Returns whether the stack is empty.
*/
public boolean empty() {
return q.isEmpty();
}
}
确實寫得簡潔啊,這樣一來我就使用一個隊列和兩個隊列都掌握啦,開心~
232.用棧實作隊列(劍指Offer面試題9)
參考答案:(72ms)
class MyQueue {
Stack<Integer> pushstack;
Stack<Integer> popstack;
/**
* Initialize your data structure here.
*/
public MyQueue() {
this.pushstack = new Stack();
this.popstack = new Stack();
}
/**
* Push element x to the back of queue.
*/
public void push(int x) {
pushstack.push(x);
}
/**
* Removes the element from in front of queue and returns that element.
*/
public int pop() {
if (popstack.isEmpty()) {
while (!pushstack.isEmpty()) {
popstack.push(pushstack.pop());
}
}
return popstack.pop();
}
/**
* Get the front element.
*/
public int peek() {
if (popstack.isEmpty()) {
while (!pushstack.isEmpty()) {
popstack.push(pushstack.pop());
}
}
return popstack.peek();
}
/**
* Returns whether the queue is empty.
*/
public boolean empty() {
return pushstack.isEmpty() && popstack.isEmpty();
}
}
其他題目整理
劍指Offer面試題31:棧的壓入、彈出序列
題目:輸入兩個整數序列,第一個序清單示棧的壓入順序,請判斷第二個序列是否為該棧的彈出順序。假設壓入棧的所有數字均不相等。例如,序列{1,2,3,4,5}是某棧的壓棧序列,序列{4,5,3,2,1}是該壓棧序列對應的一個彈出序列,但{4,3,5,1,2}就不可能是該壓棧序列的彈出序列。
參考答案:(原文連結:https://blog.csdn.net/derrantcm/article/details/46691083)
public class Test22 {
/**
* 輸入兩個整數序列,第一個序清單示棧的壓入順序,請判斷二個序列是否為該棧的彈出順序。
* 假設壓入棧的所有數字均不相等。例如序列1 、2、3 、4、5 是某棧壓棧序列,
* 序列4、5、3、2、1是該壓棧序列對應的一個彈出序列,
* 但4、3、5、1、2就不可能是該壓棋序列的彈出序列。
* 【與書本的的方法不同】
*
* @param push 入棧序列
* @param pop 出棧序列
* @return true:出棧序列是入棧序列的一個彈出順序
*/
public static boolean isPopOrder(int[] push, int[] pop) {
// 輸入校驗,參數不能為空,并且兩個數組中必須有數字,并且兩個數組中的數字個數相同
// 否則傳回false
if (push == null || pop == null || pop.length == 0 || push.length == 0 || push.length != pop.length) {
return false;
}
// 經過上面的參數校驗,兩個數組中一定有資料,且資料數目相等
// 用于存放入棧時的資料
Stack<Integer> stack = new Stack<>();
// 用于記錄入棧數組元素的處理位置
int pushIndex = 0;
// 用于記錄出棧數組元素的處理位置
int popIndex = 0;
// 如果還有出棧元素要處理
while (popIndex < pop.length) {
// 入棧元素還未全部入棧的條件下,如果棧為空,或者棧頂的元素不與目前處理的相等,則一直進行棧操作,
// 直到入棧元素全部入棧或者找到了一個與當出棧元素相等的元素
while (pushIndex < push.length && (stack.isEmpty() || stack.peek() != pop[popIndex])) {
// 入棧數組中的元素入棧
stack.push(push[pushIndex]);
// 指向下一個要處理的入棧元素
pushIndex++;
}
// 如果在上一步的入棧過程中找到了與出棧的元素相等的元素
if (stack.peek() == pop[popIndex]) {
// 将元素出棧
stack.pop();
// 處理下一個出棧元素
popIndex++;
}
// 如果沒有找到與出棧元素相等的元素,說明這個出棧順序是不合法的
// 就傳回false
else {
return false;
}
}
// 下面的語句總是成立的
// return stack.isEmpty();
// 為什麼可以直接傳回true:對上面的外層while進行分析可知道,對每一個入棧的元素,
// 在stack棧中,通過一些入棧操作,總可以在棧頂上找到與入棧元素值相同的元素,
// 這就說明了這個出棧的順序是入棧順序的一個彈出隊列,這也可以解釋為什麼stack.isEmpty()
// 總是傳回true,所有的入棧元素都可以進棧,并且可以被比對到,之後就彈出,最後棧中就無元素。
return true;
}
/**
* 輸入兩個整數序列,第一個序清單示棧的壓入順序,請判斷二個序列是否為該棧的彈出順序。
* 【按書本上的思路進行求解,兩者相差不大】
*
* @param push 入棧序列
* @param pop 出棧序列
* @return true:出棧序列是入棧序列的一個彈出順序
*/
public static boolean isPopOrder2(int[] push, int[] pop) {
// 用于記錄判斷出棧順序是不是入棧順的一個出棧序列,預設false
boolean isPossible = false;
// 當入棧和出棧數組者都不為空,并且都有資料,并且資料個數都相等
if (push != null && pop != null && push.length > 0 && push.length == pop.length) {
// 用于存放入棧時的資料
Stack<Integer> stack = new Stack<>();
// 記錄下一個要處理的入棧元素的位置
int nextPush = 0;
// 記錄下一個要處理的出棧元素的位置
int nextPop = 0;
// 如果出棧元素沒有處理完就繼續進行處理
while (nextPop < pop.length) {
// 如果棧為空或者棧頂的元素與目前處理的出棧元素不相同,一直進行操作
while (stack.isEmpty() || stack.peek() != pop[nextPop]) {
// 如果入棧的元素已經全部入棧了,就退出内層循環
if (nextPush >= push.length) {
break;
}
// 執行到此處說明還有入棧元素可以入棧
// 即将元素入棧
stack.push(push[nextPush]);
// 指向下一個要處理的入棧元素的位置
nextPush++;
}
// 執行到此處有兩種情況:
// 第一種:在棧頂上找到了一個與入棧元素相等的元素
// 第二種:在棧頂上沒有找到一個與入棧元素相等的元素,而且輸入棧的元素已經全部入棧了
// 對于第二種情況就說彈出棧的順序是不符合要求的,退出外層循環
if (stack.peek() != pop[nextPop]) {
break;
}
// 對應到第一種情況:需要要棧的棧頂元素彈出
stack.pop();
// 指向下一個要處理的出棧元素的位置
nextPop++;
}
// 執行到此處有兩種情況
// 第一種:外層while循環的在第一種情況下退出,
// 第二種:所有的出棧元素都被正确比對
// 對于出現的第一種情況其stack.isEmpty()必不為空,原因為分析如下:
// 所有的入棧元素一定會入棧,但是隻有比對的情況下才會出棧,
// 比對的次數最多與入棧元素個數元素相同(兩個數組的長度相等),如果有不比對的元素,
// 必然會使出棧的次數比入棧的次數少,這樣棧中至少會有一個元素
// 對于第二種情況其stack.isEmpty()一定為空
// 是以書本上的nextPop == pop.length(pNextPop-pPop==nLength)是多餘的
if (stack.isEmpty()) {
isPossible = true;
}
}
return isPossible;
}
public static void main(String[] args) {
int[] push = {1, 2, 3, 4, 5};
int[] pop1 = {4, 5, 3, 2, 1};
int[] pop2 = {3, 5, 4, 2, 1};
int[] pop3 = {4, 3, 5, 1, 2};
int[] pop4 = {3, 5, 4, 1, 2};
System.out.println("true: " + isPopOrder(push, pop1));
System.out.println("true: " + isPopOrder(push, pop2));
System.out.println("false: " + isPopOrder(push, pop3));
System.out.println("false: " + isPopOrder(push, pop4));
int[] push5 = {1};
int[] pop5 = {2};
System.out.println("false: " + isPopOrder(push5, pop5));
int[] push6 = {1};
int[] pop6 = {1};
System.out.println("true: " + isPopOrder(push6, pop6));
System.out.println("false: " + isPopOrder(null, null));
// 測試方法2
System.out.println();
System.out.println("true: " + isPopOrder2(push, pop1));
System.out.println("true: " + isPopOrder2(push, pop2));
System.out.println("false: " + isPopOrder2(push, pop3));
System.out.println("false: " + isPopOrder2(push, pop4));
System.out.println("false: " + isPopOrder2(push5, pop5));
System.out.println("true: " + isPopOrder2(push6, pop6));
System.out.println("false: " + isPopOrder2(null, null));
}
}
簡單總結
棧和隊列的應用遠不止上面學習到的那些,實作方式也有很多種,現在也隻是暫時學到這裡,通過刷LeetCode也加深了我對于這兩種資料結構的認識,不過自己還需要去熟悉了解一下計算機系統關于棧的應用這方面的知識,因為棧這種結構本身就很适合用來儲存CPU現場之類的工作,還是抓緊時間吧,過兩天還考試,這兩天就先複習啦...
歡迎轉載,轉載請注明出處!
簡書ID:@我沒有三顆心髒
github:wmyskxz
歡迎關注公衆微信号:wmyskxz_javaweb
分享自己的Java Web學習之路以及各種Java學習資料