一 .數(shù)組
數(shù)組(Array) 是一種很常見(jiàn)的數(shù)據(jù)結(jié)構(gòu)。它是由相同類(lèi)型的元素(element)的集合所組成,并且被分配一塊連續(xù)的內(nèi)存來(lái)存儲(chǔ)(與鏈表對(duì)比),利用元素的索引(index)可以計(jì)算出該元素對(duì)應(yīng)的存儲(chǔ)地址。
它的特點(diǎn)是提供隨機(jī)訪(fǎng)問(wèn)并且容量有限。
假如數(shù)組的長(zhǎng)度為 n
訪(fǎng)問(wèn):O(1)//訪(fǎng)問(wèn)特定位置的元素
插入:O(n )//最壞的情況發(fā)生在插入發(fā)生在數(shù)組的首部并需要移動(dòng)所有元素時(shí)
刪除:O(n)//最壞的情況發(fā)生在刪除數(shù)組的開(kāi)頭發(fā)生并需要移動(dòng)第一元素后面所有的元素時(shí)
二 .鏈表
2.1 鏈表簡(jiǎn)介
鏈表(LinkedList) 雖然是一種線(xiàn)性表,但是并不會(huì)按線(xiàn)性的順序存儲(chǔ)數(shù)據(jù),而是在每一個(gè)節(jié)點(diǎn)里存到下一個(gè)節(jié)點(diǎn)的指針(Pointer)。由于不必須按順序存儲(chǔ),鏈表在插入和刪除的時(shí)候可以達(dá)到 O(1) 的復(fù)雜度,比另一種線(xiàn)性表順序表快得多,但是查找一個(gè)節(jié)點(diǎn)或者訪(fǎng)問(wèn)特定編號(hào)的節(jié)點(diǎn)則需要 O(n) 的時(shí)間,而順序表相應(yīng)的時(shí)間復(fù)雜度分別是O(logn) 和O(1)。
使用鏈表結(jié)構(gòu)可以克服數(shù)組需要預(yù)先知道數(shù)據(jù)大小的缺點(diǎn),鏈表結(jié)構(gòu)可以充分利用計(jì)算機(jī)內(nèi)存空間,實(shí)現(xiàn)靈活的內(nèi)存動(dòng)態(tài)管理。但鏈表不會(huì)節(jié)省空間,相比于數(shù)組會(huì)占用更多的空間,因?yàn)殒湵碇忻總€(gè)節(jié)點(diǎn)存放的還有指向其他節(jié)點(diǎn)的指針。鏈表不具有數(shù)組隨機(jī)讀取的優(yōu)點(diǎn),但是插入刪除元素的時(shí)間復(fù)雜度為O(1)
2.2 鏈表分類(lèi)
常見(jiàn)鏈表分類(lèi):
單鏈表
雙向鏈表
循環(huán)鏈表
雙向循環(huán)鏈表
假如鏈表中有n個(gè)元素
訪(fǎng)問(wèn):O(n)//訪(fǎng)問(wèn)特定位置的元素插入
刪除:O(1)//必須要要知道插入元素的位置
2.2.1 單鏈表
單鏈表 單向鏈表只有一個(gè)方向,結(jié)點(diǎn)只有一個(gè)后繼指針 next 指向后面的節(jié)點(diǎn)。因此,鏈表這種數(shù)據(jù)結(jié)構(gòu)通常在物理內(nèi)存上是不連續(xù)的。我們習(xí)慣性地把第一個(gè)結(jié)點(diǎn)叫作頭結(jié)點(diǎn),鏈表通常有一個(gè)不保存任何值的 head 節(jié)點(diǎn)(頭結(jié)點(diǎn)),通過(guò)頭結(jié)點(diǎn)我們可以遍歷整個(gè)鏈表。尾結(jié)點(diǎn)通常指向null。
2.2.2 循環(huán)鏈表
循環(huán)鏈表 其實(shí)是一種特殊的單鏈表,和單鏈表不同的是循環(huán)鏈表的尾結(jié)點(diǎn)不是指向null,而是指向鏈表的頭結(jié)點(diǎn)。
2.2.3 雙向鏈表
雙向鏈表 包含兩個(gè)指針,一個(gè)prev指向前一個(gè)節(jié)點(diǎn),一個(gè)next指向后一個(gè)節(jié)點(diǎn)。
2.2.4 雙向循環(huán)鏈表
雙向循環(huán)鏈表 最后一個(gè)節(jié)點(diǎn)的 next 指向head,而 head 的prev指向最后一個(gè)節(jié)點(diǎn),構(gòu)成一個(gè)環(huán)。
2.3 數(shù)組vs鏈表
數(shù)組使用的是連續(xù)內(nèi)存空間對(duì)CPU的緩存機(jī)制友好,鏈表則相反。
數(shù)組的大小固定,聲明之后就要占用所需的連續(xù)內(nèi)存空間。如果聲明的數(shù)組過(guò)小的話(huà),需要再申請(qǐng)一個(gè)更大的內(nèi)存空間,然后將原數(shù)組拷貝進(jìn)去。數(shù)組多的情況下,這將是非常耗時(shí)的。鏈表則天然支持動(dòng)態(tài)擴(kuò)容。
三 棧
3.1 棧簡(jiǎn)介
棧 (stack)只允許在有序的線(xiàn)性數(shù)據(jù)集合的一端(稱(chēng)為棧頂 top)進(jìn)行加入數(shù)據(jù)(push)和移除數(shù)據(jù)(pop)。因而按照 后進(jìn)先出(LIFO, Last In First Out) 的原理運(yùn)作。在棧中,push 和 pop 的操作都發(fā)生在棧頂。 棧常用一維數(shù)組或鏈表來(lái)實(shí)現(xiàn),用數(shù)組實(shí)現(xiàn)的隊(duì)列叫作 順序棧 ,用鏈表實(shí)現(xiàn)的隊(duì)列叫作 鏈?zhǔn)綏?/strong> 。
假設(shè)堆棧中有n個(gè)元素
訪(fǎng)問(wèn):O(n)//最壞情況 插入
刪除:O(1)//頂端插入和刪除元素
3.2 棧的常見(jiàn)應(yīng)用常見(jiàn)應(yīng)用場(chǎng)景
3.2.1 實(shí)現(xiàn)瀏覽器的回退和前進(jìn)功能
我們只需要使用兩個(gè)棧(Stack1和Stack2)和就能實(shí)現(xiàn)這個(gè)功能。比如你按順序查看了 1,2,3,4 這四個(gè)頁(yè)面,我們依次把 1,2,3,4 這四個(gè)頁(yè)面壓入 Stack1 中。當(dāng)你想回頭看2這個(gè)頁(yè)面的時(shí)候,你點(diǎn)擊回退按鈕,我們依次把4,3這兩個(gè)頁(yè)面從Stack1 彈出,然后壓入 Stack2 中。假如你又想回到頁(yè)面3,你點(diǎn)擊前進(jìn)按鈕,我們將3頁(yè)面從Stack2 彈出,然后壓入到 Stack1 中。示例圖如下:
3.2.2 檢查符號(hào)是否成對(duì)出現(xiàn)
給定一個(gè)只包括
'('
,')'
,'{'
,'}'
,'['
,']'
的字符串,判斷該字符串是否有效。有效字符串需滿(mǎn)足:
- 左括號(hào)必須用相同類(lèi)型的右括號(hào)閉合。
- 左括號(hào)必須以正確的順序閉合。
比如 "()"、"()[]{}"、"{[]}" 都是有效字符串,而 "(]" 、"([)]" 則不是。
這個(gè)問(wèn)題實(shí)際是Leetcode的一道題目,我們可以利用棧 Stack
來(lái)解決這個(gè)問(wèn)題。
首先我們將括號(hào)間的對(duì)應(yīng)規(guī)則存放在
Map
中,這一點(diǎn)應(yīng)該毋容置疑;創(chuàng)建一個(gè)棧。遍歷字符串,如果字符是左括號(hào)就直接加入
stack
中,否則將stack
的棧頂元素與這個(gè)括號(hào)做比較,如果不相等就直接返回false。遍歷結(jié)束,如果stack
為空,返回true
。
public boolean isValid(String s){
// 括號(hào)之間的對(duì)應(yīng)規(guī)則
HashMap<Character, Character> mappings = new HashMap<Character, Character>();
mappings.put(')', '(');
mappings.put('}', '{');
mappings.put(']', '[');
Stack<Character> stack = new Stack<Character>();
char[] chars = s.toCharArray();
for (int i = 0; i < chars.length; i++) {
if (mappings.containsKey(chars[i])) {
char topElement = stack.empty() ? '#' : stack.pop();
if (topElement != mappings.get(chars[i])) {
return false;
}
} else {
stack.push(chars[i]);
}
}
return stack.isEmpty();
}
3.2.3 反轉(zhuǎn)字符串
將字符串中的每個(gè)字符先入棧再出棧就可以了。
3.2.4 維護(hù)函數(shù)調(diào)用
最后一個(gè)被調(diào)用的函數(shù)必須先完成執(zhí)行,符合棧的 后進(jìn)先出(LIFO, Last In First Out)特性。
四. 隊(duì)列
4.1 隊(duì)列簡(jiǎn)介
隊(duì)列 是 先進(jìn)先出( FIFO,F(xiàn)irst In, First Out) 的線(xiàn)性表。在具體應(yīng)用中通常用鏈表或者數(shù)組來(lái)實(shí)現(xiàn),用數(shù)組實(shí)現(xiàn)的隊(duì)列叫作 順序隊(duì)列 ,用鏈表實(shí)現(xiàn)的隊(duì)列叫作 鏈?zhǔn)疥?duì)列 。隊(duì)列只允許在后端(rear)進(jìn)行插入操作也就是 入隊(duì) enqueue,在前端(front)進(jìn)行刪除操作也就是出隊(duì) dequeue
隊(duì)列的操作方式和堆棧類(lèi)似,唯一的區(qū)別在于隊(duì)列只允許新數(shù)據(jù)在后端進(jìn)行添加。
假設(shè)隊(duì)列中有n個(gè)元素
訪(fǎng)問(wèn):O(n)//最壞情況插入
刪除:O(1)//后端插入前端刪除元素
4.2 隊(duì)列分類(lèi)
4.2.1 單隊(duì)列
單隊(duì)列就是常見(jiàn)的隊(duì)列, 每次添加元素時(shí),都是添加到隊(duì)尾。單隊(duì)列又分為 順序隊(duì)列(數(shù)組實(shí)現(xiàn)) 和 鏈?zhǔn)疥?duì)列(鏈表實(shí)現(xiàn))。
順序隊(duì)列存在“假溢出”的問(wèn)題也就是明明有位置卻不能添加的情況。
假設(shè)下圖是一個(gè)順序隊(duì)列,我們將前兩個(gè)元素1,2 出隊(duì),并入隊(duì)兩個(gè)元素7,8。當(dāng)進(jìn)行入隊(duì)、出隊(duì)操作的時(shí)候,front和 rear 都會(huì)持續(xù)往后移動(dòng),當(dāng) rear 移動(dòng)到最后的時(shí)候,我們無(wú)法再往隊(duì)列中添加數(shù)據(jù),即使數(shù)組中還有空余空間,這種現(xiàn)象就是 ”假溢出“ 。除了假溢出問(wèn)題之外,如下圖所示,當(dāng)添加元素8的時(shí)候,rear 指針移動(dòng)到數(shù)組之外(越界)。
為了避免當(dāng)只有一個(gè)元素的時(shí)候,隊(duì)頭和隊(duì)尾重合使處理變得麻煩,所以引入兩個(gè)指針,front 指針指向?qū)︻^元素,rear 指針指向隊(duì)列最后一個(gè)元素的下一個(gè)位置,這樣當(dāng) front 等于 rear 時(shí),此隊(duì)列不是還剩一個(gè)元素,而是空隊(duì)列。——From 《大話(huà)數(shù)據(jù)結(jié)構(gòu)》
4.2.2 循環(huán)隊(duì)列
循環(huán)隊(duì)列可以解決順序隊(duì)列的假溢出和越界問(wèn)題。解決辦法就是:從頭開(kāi)始,這樣也就會(huì)形成頭尾相接的循環(huán),這也就是循環(huán)隊(duì)列名字的由來(lái)。
還是用上面的圖,我們將 rear 指針指向數(shù)組下標(biāo)為 0 的位置就不會(huì)有越界問(wèn)題了。當(dāng)我們?cè)傧蜿?duì)列中添加元素的時(shí)候, rear 向后移動(dòng)。
順序隊(duì)列中,我們說(shuō) front==rear
的時(shí)候隊(duì)列為空,循環(huán)隊(duì)列中則不一樣,也可能為滿(mǎn),如上圖所示。解決辦法有兩種:
可以設(shè)置一個(gè)標(biāo)志變量
flag
,當(dāng)front==rear
并且flag=0
的時(shí)候隊(duì)列為空,當(dāng)front==rear
并且flag=1
的時(shí)候隊(duì)列為滿(mǎn)。隊(duì)列為空的時(shí)候就是
front==rear
,隊(duì)列滿(mǎn)的時(shí)候,我們保證數(shù)組還有一個(gè)空閑的位置,rear 就指向這個(gè)空閑位置,如下圖所示,那么現(xiàn)在判斷隊(duì)列是否為滿(mǎn)的條件就是:(rear+1) % QueueSize= front
。
常見(jiàn)應(yīng)用場(chǎng)景
阻塞隊(duì)列: 阻塞隊(duì)列可以看成在隊(duì)列基礎(chǔ)上加了阻塞操作的隊(duì)列。當(dāng)隊(duì)列為空的時(shí)候,出隊(duì)操作阻塞,當(dāng)隊(duì)列滿(mǎn)的時(shí)候,入隊(duì)操作阻塞。使用阻塞隊(duì)列我們可以很容易實(shí)現(xiàn)“生產(chǎn)者 - 消費(fèi)者“模型。
線(xiàn)程池中的請(qǐng)求/任務(wù)隊(duì)列: 線(xiàn)程池中沒(méi)有空閑線(xiàn)程時(shí),新的任務(wù)請(qǐng)求線(xiàn)程資源時(shí),線(xiàn)程池該如何處理呢?答案是將這些請(qǐng)求放在隊(duì)列中,當(dāng)有空閑線(xiàn)程的時(shí)候,會(huì)循環(huán)中反復(fù)從隊(duì)列中獲取任務(wù)來(lái)執(zhí)行。隊(duì)列分為無(wú)界隊(duì)列(基于鏈表)和有界隊(duì)列(基于數(shù)組)。無(wú)界隊(duì)列的特點(diǎn)就是可以一直入列,除非系統(tǒng)資源耗盡,比如 :
FixedThreadPool
使用無(wú)界隊(duì)列LinkedBlockingQueue
。但是有界隊(duì)列就不一樣了,當(dāng)隊(duì)列滿(mǎn)的話(huà)后面再有任務(wù)/請(qǐng)求就會(huì)拒絕,在 Java 中的體現(xiàn)就是會(huì)拋出java.util.concurrent.RejectedExecutionException
異常。linux內(nèi)核進(jìn)程隊(duì)列(按優(yōu)先級(jí)排隊(duì))
實(shí)現(xiàn)生活中的派對(duì),播放器上的播放列表;
消息隊(duì)列
等等……
參考
https://www.geeksforgeeks.org/overview-of-data-structures-set-1-linear-data-structures/
《大話(huà)數(shù)據(jù)結(jié)構(gòu)》