作者:小傅哥 博客:https://
? 沉淀、分享、成長,讓自己和他人都能有所收獲!??
? 目錄 一、前言 買房子最重要的是房屋格局!
如果買房子能接受地理位置、平米價格外,最重要的就是房屋格局。什么?丈母娘!你?????♂,出去! 房屋的格局其實對應的就是程序開發(fā)的根本,也就是數(shù)據(jù)結(jié)構(gòu)。有的土豪可以用錢換空間,房間格局更大,那沒錢的就只能選經(jīng)濟小空間節(jié)省錢。是不是很像不同的數(shù)據(jù)結(jié)構(gòu),直接影響著是空間換時間,還是時間換空間。那么,再細看房間,如;客廳沙發(fā)坐人像散列表、上衛(wèi)生間像進棧、廚房不大先進后出像隊列、晚上各回各屋子像進數(shù)組。所以你能在這個屋子生活的舒服,很大一部分取決于整個房間的布局。也同樣你能把程序?qū)懞?,很大的原因是因為?shù)據(jù)結(jié)構(gòu)定義的合理。
那么決定這程序開發(fā)基礎(chǔ)數(shù)據(jù)結(jié)構(gòu)有哪些呢?
程序開發(fā)中數(shù)據(jù)結(jié)構(gòu)可以分為這八類;數(shù)組
、鏈表
、棧
、隊列
、散列表
、樹
、堆
、圖
。其中,數(shù)組、鏈表、散列表、樹是程序開發(fā)直接或者間接用到的最多的。相關(guān)的對應實現(xiàn)類可以包括如下;
類型 實現(xiàn) 文章 數(shù)組 ArrayList ArrayList也這么多知識?一個指定位置插入就把謝飛機面暈了! 鏈表 LinkedList LinkedList插入速度比ArrayList快?你確定嗎? 樹 2-3樹、紅黑樹 看圖說話,講解2-3平衡樹「紅黑樹的前身」 紅黑樹操作原理,解析什么時候染色、怎么進行旋轉(zhuǎn)、與2-3樹有什么關(guān)聯(lián) 散列表 HashMap HashMap核心知識,擾動函數(shù)、負載因子、擴容鏈表拆分,深度學習 HashMap數(shù)據(jù)插入、查找、刪除、遍歷,源碼分析 棧 Stack 隊列 Queue、Deque
如上,除了棧和隊列外,小傅哥已經(jīng)編寫了非常細致的文章來介紹了其他數(shù)據(jù)結(jié)構(gòu)的核心知識和具體的實現(xiàn)應用。 接下來就把剩下的棧和隊列在本章介紹完,其實這部分知識并不難了,有了以上對數(shù)組和鏈表的理解,其他的數(shù)據(jù)結(jié)構(gòu)基本都從這兩方面擴展出來的。 本文涉及了較多的代碼和實踐驗證圖稿,歡迎關(guān)注公眾號:bugstack蟲洞棧
,回復下載得到一個鏈接打開后,找到ID:19??獲?。?/span>
二、面試題 謝飛機
,飛機你旁邊這是?
「答」 :啊,謝坦克,我弟弟。還沒畢業(yè),想來看看大公司面試官的容顏。
「問」 :飛機,上次把LinkedList
都看了吧,那我問你哈。LinkedList
可以當隊列用嗎?
「答」 :?。靠梢?,可以吧!
「問」 :那,數(shù)組能當隊列用嗎?不能?對列有啥特點嗎?
「答」 :隊列先進先出,嗯,嗯。
「問」 :還有嗎?了解延時隊列嗎?雙端隊列呢?
飛機拉著坦克的手出門了,還帶走了面試官送的一本《面經(jīng)手冊》,坦克對飛機說,基礎(chǔ)不牢,地動山搖,我要好好學習。
三、數(shù)據(jù)結(jié)構(gòu) 把我們已經(jīng)掌握了的數(shù)組和鏈表立起來,就是棧和隊列了!
如圖,這一章節(jié)的數(shù)據(jù)結(jié)構(gòu)的知識點并不難,只要已經(jīng)學習過數(shù)組和鏈表,那么對于掌握其他數(shù)據(jù)結(jié)構(gòu)就已經(jīng)有了基礎(chǔ),只不過對于數(shù)據(jù)的存放、讀取加了一些限定規(guī)則。尤其像鏈表這樣的數(shù)據(jù)結(jié)構(gòu),只操作頭尾的效率是非常高的。
四、源碼學習 1. 先說一個被拋棄Stack有時候不會反而不會犯錯誤!怕就怕在只知道一知半解。
拋棄的不是棧這種數(shù)據(jù)結(jié)構(gòu),而是Stack實現(xiàn)類,如果你還不了解就用到業(yè)務開發(fā)中,就很可能會影響系統(tǒng)性能。其實Stack這個棧已經(jīng)是不建議使用了,但是為什么不建議使用,我們可以通過使用和源碼分析了解下根本原因。
在學習之前先大概的了解下這樣的數(shù)據(jù)結(jié)構(gòu),它很像羽毛球的擺放,是一種后進先出隊列,如下;
1.1 功能使用@Test public void test_stack () { Stack<String> s = new Stack<String>(); s.push("aaa" ); s.push("bbb" ); s.push("ccc" ); System.out.println("獲取最后一個元素:" + s.peek()); System.out.println("獲取最后一個元素:" + s.lastElement()); System.out.println("獲取最先放置元素:" + s.firstElement()); System.out.println("彈出一個元素[LIFO]:" + s.pop()); System.out.println("彈出一個元素[LIFO]:" + s.pop()); System.out.println("彈出一個元素[LIFO]:" + s.pop()); }
例子是對Stack
棧的使用,如果不運行你能知道它的輸出結(jié)果嗎?
「測試結(jié)果:」
獲取最后一個元素:ccc 獲取最后一個元素:ccc 獲取最先放置元素:aaa 彈出一個元素[LIFO]:ccc 彈出一個元素[LIFO]:bbb 彈出一個元素[LIFO]:aaa Process finished with exit code 0
看到測試結(jié)果,與你想的答案是否一致?
peek,是偷看的意思,就是看一下,不會彈出元素。滿足后進先出的規(guī)則,它看的是最后放進去的元素ccc
。 lastElement、firstElement,字面意思的方法,獲取最后一個和獲取第一個元素。 pop,是隊列中彈出元素,彈出后也代表著要把屬于這個位置都元素清空,刪掉。 1.2 源碼分析我們說Stack
棧,這個實現(xiàn)類已經(jīng)不推薦使用了,需要從它的源碼上看。
/** * * <p>A more complete and consistent set of LIFO stack operations is * provided by the {@link Deque} interface and its implementations, which * should be used in preference to this class. For example: * <pre> {@code * Deque<Integer> stack = new ArrayDeque<Integer>();}</pre> * * @author Jonathan Payne * @since JDK1.0 */ public class Stack <E > extends Vector <E >
s.push("aaa" );public synchronized void addElement (E obj) { modCount++; ensureCapacityHelper(elementCount + 1 ); elementData[elementCount++] = obj; }
Stack
棧是在JDK1.0時代時,基于繼承Vector
,實現(xiàn)的。本身Vector
就是一個不推薦使用的類,主要在于它的一些操作方法鎖??(synchronized )的力度太粗,都是放到方法上。Stack
棧底層是使用Vector
數(shù)組實現(xiàn),在學習ArrayList
時候我們知道,數(shù)組結(jié)構(gòu)在元素添加和擅長需要通過System.arraycopy
,進行擴容操作。而本身棧的特點是首尾元素的操作,也不需要遍歷,使用數(shù)組結(jié)構(gòu)其實并不太理想。同時在這個方法的注釋上也明確標出來,推薦使用Deque<Integer> stack = new ArrayDeque<Integer>();
,雖然這也是數(shù)組結(jié)構(gòu),但是它沒有粗粒度的鎖,同時可以申請指定空間并且在擴容時操作時也要優(yōu)于Stack
。并且它還是一個雙端隊列,使用起來更靈活。 2. 雙端隊列ArrayDequeArrayDeque
是基于數(shù)組實現(xiàn)的可動態(tài)擴容的雙端隊列,也就是說你可以在隊列的頭和尾同時插入和彈出元素。當元素數(shù)量超過數(shù)組初始化長度時,則需要擴容和遷移數(shù)據(jù)。
「數(shù)據(jù)結(jié)構(gòu)和操作」 ,如下;
小傅哥 & 雙端隊列數(shù)據(jù)結(jié)構(gòu)操作從上圖我們可以了解到如下幾個知識點;
雙端隊列是基于數(shù)組實現(xiàn),所以擴容遷移數(shù)據(jù)操作。 push
,像結(jié)尾插入、offerLast
,向頭部插入,這樣兩端都滿足后進先出。整體來看,雙端隊列,就是一個環(huán)形。所以擴容后繼續(xù)插入元素也滿足后進先出。 2.1 功能使用@Test public void test_ArrayDeque () { Deque<String> deque = new ArrayDeque<String>(1 ); deque.push("a" ); deque.push("b" ); deque.push("c" ); deque.push("d" ); deque.offerLast("e" ); deque.offerLast("f" ); deque.offerLast("g" ); deque.offerLast("h" ); // 這時候擴容了 deque.push("i" ); deque.offerLast("j" ); System.out.println("數(shù)據(jù)出棧:" ); while (!deque.isEmpty()) { System.out.print(deque.pop() + " " ); } }
以上這部分代碼就是與上圖的展現(xiàn)是一致的,按照圖中的分析我們看下輸出結(jié)果,如下;
數(shù)據(jù)出棧: i d c b a e f g h j Process finished with exit code 0
i d c b a e f g h j
,正好滿足了我們的說的數(shù)據(jù)出棧順序。可以參考上圖再進行理解 2.2 源碼分析ArrayDeque
這種雙端隊列是基于數(shù)組實現(xiàn)的,所以源碼上從初始化到數(shù)據(jù)入棧擴容,都會有數(shù)組操作的痕跡。接下來我們就依次分析下。
2.2.1 初始化new ArrayDeque<String>(1);
,其實它的構(gòu)造函數(shù)初始化默認也提供了幾個方法,比如你可以指定大小以及提供默認元素。
private static int calculateSize (int numElements) { int initialCapacity = MIN_INITIAL_CAPACITY; // Find the best power of two to hold elements. // Tests "<=" because arrays aren't kept full. if (numElements >= initialCapacity) { initialCapacity = numElements; initialCapacity |= (initialCapacity >>> 1 ); initialCapacity |= (initialCapacity >>> 2 ); initialCapacity |= (initialCapacity >>> 4 ); initialCapacity |= (initialCapacity >>> 8 ); initialCapacity |= (initialCapacity >>> 16 ); initialCapacity++; if (initialCapacity < 0 ) // Too many elements, must back off initialCapacity >>>= 1 ;// Good luck allocating 2 ^ 30 element } return initialCapacity; }
在初始化的過程中,它需要找到你當前傳輸值最小的2的倍數(shù)的一個容量。這與HashMap的初始化過程相似。 2.2.2 數(shù)據(jù)入棧deque.push("a");
,ArrayDeque,提供了一個 push 方法,這個方法與deque.offerFirst(“a”)
,一致,因為它們的底層源碼是一樣的,如下;
「addFirst:」
public void addFirst (E e) { if (e == null ) throw new NullPointerException(); elements[head = (head - 1 ) & (elements.length - 1 )] = e; if (head == tail) doubleCapacity(); }
「addLast:」
public void addLast (E e) { if (e == null ) throw new NullPointerException(); elements[tail] = e; if ( (tail = (tail + 1 ) & (elements.length - 1 )) == head) doubleCapacity(); }
這部分入棧元素,其實就是給數(shù)組賦值,知識點如下;
在addFirst()
中,定位下標,head = (head - 1) & (elements.length - 1)
,因為我們的數(shù)組長度是2^n
的倍數(shù),所以 2^n - 1
就是一個全是1的二進制數(shù),可以用于與運算得出數(shù)組下標。 同樣addLast()
中,也使用了相同的方式定位下標,只不過它是從0開始,往上增加。 最后,當頭(head)與尾(tile),數(shù)組則需要兩倍擴容doubleCapacity
。 下標計算:head = (head - 1) & (elements.length - 1)
:
2.2.3 兩倍擴容,數(shù)據(jù)遷移private void doubleCapacity () { assert head == tail; int p = head; int n = elements.length; int r = n - p; // number of elements to the right of p int newCapacity = n << 1 ; if (newCapacity < 0 ) throw new IllegalStateException("Sorry, deque too big" ); Object[] a = new Object[newCapacity]; System.arraycopy(elements, p, a, 0 , r); System.arraycopy(elements, 0 , a, r, p); elements = a; head = 0 ; tail = n; }
其實以上這部分源碼,就是進行兩倍n << 1 擴容,同時把兩端數(shù)據(jù)遷移進新的數(shù)組,整個操作過程也與我們上圖對應。為了更好的理解,我們單獨把這部分代碼做一些測試。
「測試代碼:」
@Test public void test_arraycopy () { int head = 0 , tail = 0 ; Object[] elements = new Object[8 ]; elements[head = (head - 1 ) & (elements.length - 1 )] = "a" ; elements[head = (head - 1 ) & (elements.length - 1 )] = "b" ; elements[head = (head - 1 ) & (elements.length - 1 )] = "c" ; elements[head = (head - 1 ) & (elements.length - 1 )] = "d" ; elements[tail] = "e" ; tail = (tail + 1 ) & (elements.length - 1 ); elements[tail] = "f" ; tail = (tail + 1 ) & (elements.length - 1 ); elements[tail] = "g" ; tail = (tail + 1 ) & (elements.length - 1 ); elements[tail] = "h" ; tail = (tail + 1 ) & (elements.length - 1 ); System.out.println("head:" + head); System.out.println("tail:" + tail); int p = head; int n = elements.length; int r = n - p; // number of elements to the right of p // 輸出當前的元素 System.out.println(JSON.toJSONString(elements)); // head == tail 擴容 Object[] a = new Object[8 << 1 ]; System.arraycopy(elements, p, a, 0 , r); System.out.println(JSON.toJSONString(a)); System.arraycopy(elements, 0 , a, r, p); System.out.println(JSON.toJSONString(a)); elements = a; head = 0 ; tail = n; a[head = (head - 1 ) & (a.length - 1 )] = "i" ; System.out.println(JSON.toJSONString(a)); }
以上的測試過程主要模擬了8個長度的空間的數(shù)組,在進行雙端隊列操作時數(shù)組擴容,數(shù)據(jù)遷移操作,可以單獨運行,測試結(jié)果如下;
head:4 tail:4 ["e" ,"f" ,"g" ,"h" ,"d" ,"c" ,"b" ,"a" ] ["d" ,"c" ,"b" ,"a" ,null ,null ,null ,null ,null ,null ,null ,null ,null ,null ,null ,null ] ["d" ,"c" ,"b" ,"a" ,"e" ,"f" ,"g" ,"h" ,null ,null ,null ,null ,null ,null ,null ,null ] ["d" ,"c" ,"b" ,"a" ,"e" ,"f" ,"g" ,"h" ,"j" ,null ,null ,null ,null ,null ,null ,"i" ] Process finished with exit code 0
從測試結(jié)果可以看到;
第一次數(shù)據(jù)遷移,System.arraycopy(elements, p, a, 0, r);
,「d、c、b、a」 ,落入新數(shù)組。 第二次數(shù)據(jù)遷移,System.arraycopy(elements, 0, a, r, p);
,「e、f、g、h」 ,落入新數(shù)組。 最后再嘗試添加新的元素,i和j。每一次的輸出結(jié)果都可以看到整個雙端鏈路的變化。 3. 雙端隊列LinkedListLinkedlist
天生就可以支持雙端隊列,而且從頭尾取數(shù)據(jù)也是它時間復雜度O(1)的。同時數(shù)據(jù)的插入和刪除也不需要像數(shù)組隊列那樣拷貝數(shù)據(jù),雖然Linkedlist
有這些優(yōu)點,但不能說ArrayDeque
因為有數(shù)組復制性能比它低。
「Linkedlist,數(shù)據(jù)結(jié)構(gòu)」 :
3.1 功能使用@Test public void test_Deque_LinkedList () { Deque<String> deque = new LinkedList<>(); deque.push("a" ); deque.push("b" ); deque.push("c" ); deque.push("d" ); deque.offerLast("e" ); deque.offerLast("f" ); deque.offerLast("g" ); deque.offerLast("h" ); deque.push("i" ); deque.offerLast("j" ); System.out.println("數(shù)據(jù)出棧:" ); while (!deque.isEmpty()) { System.out.print(deque.pop() + " " ); } }
「測試結(jié)果」 :
數(shù)據(jù)出棧: i d c b a e f g h j Process finished with exit code 0
測試結(jié)果上看與使用ArrayDeque
是一樣的,功能上沒有差異。 3.2 源碼分析「壓?!?/strong>:deque.push("a");
、deque.offerFirst("a");
private void linkFirst (E e) { final Node<E> f = first; final Node<E> newNode = new Node<>(null , e, f); first = newNode; if (f == null ) last = newNode; else f.prev = newNode; size++; modCount++; }
「壓棧」 :deque.offerLast("e");
void linkLast (E e) { final Node<E> l = last; final Node<E> newNode = new Node<>(l, e, null ); last = newNode; if (l == null ) first = newNode; else l.next = newNode; size++; modCount++; }
linkFirst
、linkLast
,兩個方法分別是給鏈表的首尾節(jié)點插入元素,因為這是鏈表結(jié)構(gòu),所以也不存在擴容,只需要把雙向鏈路鏈接上即可。 4. 延時隊列DelayQueue你是否有時候需要把一些數(shù)據(jù)存起來,倒計時到某個時刻在使用?
在Java的隊列數(shù)據(jù)結(jié)構(gòu)中,還有一種隊列是延時隊列,可以通過設(shè)定存放時間,依次輪訓獲取。
4.1 功能使用「先寫一個Delayed的實現(xiàn)類」 :
public class TestDelayed implements Delayed { private String str; private long time; public TestDelayed (String str, long time, TimeUnit unit) { this .str = str; this .time = System.currentTimeMillis() + (time > 0 ? unit.toMillis(time) : 0 ); } @Override public long getDelay (TimeUnit unit) { return time - System.currentTimeMillis(); } @Override public int compareTo (Delayed o) { TestDelayed work = (TestDelayed) o; long diff = this .time - work.time; if (diff <= 0 ) { return -1 ; } else { return 1 ; } } public String getStr () { return str; } }
這個相當于延時隊列的一個固定模版方法,通過這種方式來控制延時。 「案例測試」 :
@Test public void test_DelayQueue () throws InterruptedException { DelayQueue<TestDelayed> delayQueue = new DelayQueue<TestDelayed>(); delayQueue.offer(new TestDelayed("aaa" , 5 , TimeUnit.SECONDS)); delayQueue.offer(new TestDelayed("ccc" , 1 , TimeUnit.SECONDS)); delayQueue.offer(new TestDelayed("bbb" , 3 , TimeUnit.SECONDS)); logger.info(((TestDelayed) delayQueue.take()).getStr()); logger.info(((TestDelayed) delayQueue.take()).getStr()); logger.info(((TestDelayed) delayQueue.take()).getStr()); }
「測試結(jié)果」 :
01 :44 :21.000 [main] INFO org.itstack.interview.test.ApiTest - ccc01 :44 :22.997 [main] INFO org.itstack.interview.test.ApiTest - bbb01 :44 :24.997 [main] INFO org.itstack.interview.test.ApiTest - aaa Process finished with exit code 0
在案例測試中我們分別設(shè)定不同的休眠時間,1、3、5,TimeUnit.SECONDS。 測試結(jié)果分別在21、22、24,輸出了我們要的隊列結(jié)果。 隊列中的元素不會因為存放的先后順序而導致輸出順序,它們是依賴于休眠時長決定。 4.2 源碼分析 4.2.1 元素入棧「入棧:」 :delayQueue.offer(new TestDelayed("aaa", 5, TimeUnit.SECONDS));
public boolean offer (E e) { if (e == null ) throw new NullPointerException(); modCount++; int i = size; if (i >= queue.length) grow(i + 1 ); size = i + 1 ; if (i == 0 ) queue[0 ] = e; else siftUp(i, e); return true ; }private void siftUpUsingComparator (int k, E x) { while (k > 0 ) { int parent = (k - 1 ) >>> 1 ; Object e = queue[parent]; if (comparator.compare(x, (E) e) >= 0 ) break ; queue[k] = e; k = parent; } queue[k] = x; }
關(guān)于數(shù)據(jù)存放還有 ReentrantLock
可重入鎖??,但暫時不是我們本章節(jié)數(shù)據(jù)結(jié)構(gòu)的重點,后面章節(jié)會介紹到。 DelayQueue
是基于數(shù)組實現(xiàn)的,所以可以動態(tài)擴容,另外它插入元素的順序并不影響最終的輸出順序。而元素的排序依賴于compareTo方法進行排序,也就是休眠的時間長短決定的。 同時只有實現(xiàn)了Delayed
接口,才能存放元素。 4.2.2 元素出棧「出?!?/strong>:delayQueue.take()
public E take () throws InterruptedException { final ReentrantLock lock = this .lock; lock.lockInterruptibly(); try { for (;;) { E first = q.peek(); if (first == null ) available.await(); else { long delay = first.getDelay(NANOSECONDS if (delay <= 0 ) return q.poll () ; first = null ; // don't retain ref while if (leader != null ) available.await(); else { Thread thisThread = Thread.currentT leader = thisThread; try { available.awaitNanos(delay); } finally { if (leader == thisThread) leader = null ; } } } } } finally { if (leader == null && q.peek() != null ) available.signal(); lock.unlock(); } }
這部分的代碼有點長,主要是元素的獲取。DelayQueue
是 Leader-Followr
模式的變種,消費者線程處于等待await時,總是等待最先休眠完成的元素。 這里會最小化的空等時間,提高線程利用率。數(shù)據(jù)結(jié)構(gòu)講完后,后面會有專門章節(jié)介紹 5. 還有哪些隊列? 5.1 隊列類結(jié)構(gòu)類型 實現(xiàn) 描述 Queue LinkedBlockingQueue 由鏈表結(jié)構(gòu)組成的有界阻塞隊列 Queue ArrayBlockingQueue 由數(shù)組結(jié)構(gòu)組成的有界阻塞隊列 Queue PriorityBlockingQueue 支持優(yōu)先級排序的無界阻塞隊列 Queue SynchronousQueue 不存儲元素的阻塞隊列 Queue LinkedTransferQueue 由鏈表結(jié)構(gòu)組成的無界阻塞隊列 Deque LinkedBlockingDeque 由鏈表結(jié)構(gòu)組成的雙向阻塞隊列 Deque ConcurrentLinkedDeque 由鏈表結(jié)構(gòu)組成的線程安全的雙向阻塞隊列
除了我們已經(jīng)講過的隊列以外,剩余的基本都是阻塞隊列,也就是上面這些。 在數(shù)據(jù)結(jié)構(gòu)方面基本沒有差異,只不過添加了相應的阻塞功能和鎖的機制。 5.2 使用案例public class DataQueueStack { private BlockingQueue<DataBean> dataQueue = null ; public DataQueueStack () { //實例化隊列 dataQueue = new LinkedBlockingQueue<DataBean>(100 ); } /** * 添加數(shù)據(jù)到隊列 * @param dataBean * @return */ public boolean doOfferData (DataBean dataBean) { try { return dataQueue.offer(dataBean, 2 , TimeUnit.SECONDS); } catch (InterruptedException e) { e.printStackTrace(); return false ; } } /** * 彈出隊列數(shù)據(jù) * @return */ public DataBean doPollData () { try { return dataQueue.poll(2 , TimeUnit.SECONDS); } catch (InterruptedException e) { e.printStackTrace(); return null ; } } /** * 獲得隊列數(shù)據(jù)個數(shù) * @return */ public int doGetQueueCount () { return dataQueue.size(); } }
這是一個LinkedBlockingQueue
隊列使用案例,一方面存儲數(shù)據(jù),一方面從隊列中獲取進行消費。 因為這是一個阻塞隊列,所以在獲取元素的時候,如果隊列為空,會進行阻塞。 LinkedBlockingQueue
是一個阻塞隊列,內(nèi)部由兩個ReentrantLock來實現(xiàn)出入隊列的線程安全,由各自的Condition對象的await和signal來實現(xiàn)等待和喚醒功能。五、總結(jié) 關(guān)于棧和隊列的數(shù)據(jù)結(jié)構(gòu)方面到這里就介紹完了,另外這里還有一些關(guān)于阻塞隊列鎖??的應用過程,到我們后面講鎖相關(guān)知識點,再重點介紹。 隊列結(jié)構(gòu)的設(shè)計非常適合某些需要LIFO
或者FIFO
的應用場景,同時在隊列的數(shù)據(jù)結(jié)構(gòu)中也有雙端、延時和組合的功能類,使用起來也非常方便。 數(shù)據(jù)結(jié)構(gòu)方面的知識到本章節(jié)算是告一段落,如果有優(yōu)秀的內(nèi)容,后面還會繼續(xù)補充。再下一章節(jié)小傅哥()準備給大家介紹,關(guān)于數(shù)據(jù)結(jié)構(gòu)中涉及的算法部分,這些主要來自于Collections
類的實現(xiàn)部分。 bugstack蟲洞棧
沉淀、分享、成長,讓自己和他人都能有所收獲!