1. 數(shù)組
數(shù)組(Array) 是一種很常見(jiàn)的數(shù)據(jù)結(jié)構(gòu)。它由相同類型的元素(element)組成,并且是使用一塊連續(xù)的內(nèi)存來(lái)存儲(chǔ)。
我們直接可以利用元素的索引(index)可以計(jì)算出該元素對(duì)應(yīng)的存儲(chǔ)地址。
數(shù)組的特點(diǎn)是:提供隨機(jī)訪問(wèn) 并且容量有限。
假如數(shù)組的長(zhǎng)度為 n。
訪問(wèn):O(1)//訪問(wèn)特定位置的元素
插入:O(n )//最壞的情況發(fā)生在插入發(fā)生在數(shù)組的首部并需要移動(dòng)所有元素時(shí)
刪除:O(n)//最壞的情況發(fā)生在刪除數(shù)組的開頭發(fā)生并需要移動(dòng)第一元素后面所有的元素時(shí)
2. 鏈表
2.1. 鏈表簡(jiǎn)介
鏈表(LinkedList) 雖然是一種線性表,但是并不會(huì)按線性的順序存儲(chǔ)數(shù)據(jù),使用的不是連續(xù)的內(nèi)存空間來(lái)存儲(chǔ)數(shù)據(jù)。
鏈表的插入和刪除操作的復(fù)雜度為 O(1) ,只需要知道目標(biāo)位置元素的上一個(gè)元素即可。但是,在查找一個(gè)節(jié)點(diǎn)或者訪問(wèn)特定位置的節(jié)點(diǎn)的時(shí)候復(fù)雜度為 O(n) 。
使用鏈表結(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)。
2.2. 鏈表分類
常見(jiàn)鏈表分類:
- 單鏈表
- 雙向鏈表
- 循環(huán)鏈表
- 雙向循環(huán)鏈表
假如鏈表中有n個(gè)元素。
訪問(wèn):O(n)//訪問(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. 應(yīng)用場(chǎng)景
- 如果需要支持隨機(jī)訪問(wèn)的話,鏈表沒(méi)辦法做到。如
- 果需要存儲(chǔ)的數(shù)據(jù)元素的個(gè)數(shù)不確定,并且需要經(jīng)常添加和刪除數(shù)據(jù)的話,使用鏈表比較合適。
- 如果需要存儲(chǔ)的數(shù)據(jù)元素的個(gè)數(shù)確定,并且不需要經(jīng)常添加和刪除數(shù)據(jù)的話,使用數(shù)組比較合適。
2.4. 數(shù)組 vs 鏈表
- 數(shù)據(jù)支持隨機(jī)訪問(wèn),而鏈表不支持。
- 數(shù)組使用的是連續(xù)內(nèi)存空間對(duì) CPU 的緩存機(jī)制友好,鏈表則相反。
- 數(shù)據(jù)的大小固定,而鏈表則天然支持動(dòng)態(tài)擴(kuò)容。如果聲明的數(shù)組過(guò)小,需要另外申請(qǐng)一個(gè)更大的內(nèi)存空間存放數(shù)組元素,然后將原數(shù)組拷貝進(jìn)去,這個(gè)操作是比較耗時(shí)的!
3. 棧
3.1. 棧簡(jiǎn)介
棧 (stack)只允許在有序的線性數(shù)據(jù)集合的一端(稱為棧頂 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)的棧叫作 順序棧 ,用鏈表實(shí)現(xiàn)的棧叫作 鏈?zhǔn)綏?/strong> 。
假設(shè)堆棧中有n個(gè)元素。
訪問(wèn):O(n)//最壞情況
插入刪除:O(1)//頂端插入和刪除元素
3.2. 棧的常見(jiàn)應(yīng)用常見(jiàn)應(yīng)用場(chǎng)景
當(dāng)我們我們要處理的數(shù)據(jù)只涉及在一端插入和刪除數(shù)據(jù),并且滿足 后進(jìn)先出(LIFO, Last In First Out) 的特性時(shí),我們就可以使用棧這個(gè)數(shù)據(jù)結(jié)構(gòu)。
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è)只包括
'('
,')'
,'{'
,'}'
,'['
,']'
的字符串,判斷該字符串是否有效。有效字符串需滿足:
- 左括號(hào)必須用相同類型的右括號(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) 特性。
3.3. 棧的實(shí)現(xiàn)
棧既可以通過(guò)數(shù)組實(shí)現(xiàn),也可以通過(guò)鏈表來(lái)實(shí)現(xiàn)。不管基于數(shù)組還是鏈表,入棧、出棧的時(shí)間復(fù)雜度都為 O(1)。
下面我們使用數(shù)組來(lái)實(shí)現(xiàn)一個(gè)棧,并且這個(gè)棧具有push()
、pop()
(返回棧頂元素并出棧)、peek()
(返回棧頂元素不出棧)、isEmpty()
、size()
這些基本的方法。
提示:每次入棧之前先判斷棧的容量是否夠用,如果不夠用就用
Arrays.copyOf()
進(jìn)行擴(kuò)容;
public class MyStack {
private int[] storage;//存放棧中元素的數(shù)組
private int capacity;//棧的容量
private int count;//棧中元素?cái)?shù)量
private static final int GROW_FACTOR = 2;
//不帶初始容量的構(gòu)造方法。默認(rèn)容量為8
public MyStack() {
this.capacity = 8;
this.storage=new int[8];
this.count = 0;
}
//帶初始容量的構(gòu)造方法
public MyStack(int initialCapacity) {
if (initialCapacity < 1)
throw new IllegalArgumentException("Capacity too small.");
this.capacity = initialCapacity;
this.storage = new int[initialCapacity];
this.count = 0;
}
//入棧
public void push(int value) {
if (count == capacity) {
ensureCapacity();
}
storage[count++] = value;
}
//確保容量大小
private void ensureCapacity() {
int newCapacity = capacity * GROW_FACTOR;
storage = Arrays.copyOf(storage, newCapacity);
capacity = newCapacity;
}
//返回棧頂元素并出棧
private int pop() {
if (count == 0)
throw new IllegalArgumentException("Stack is empty.");
count--;
return storage[count];
}
//返回棧頂元素不出棧
private int peek() {
if (count == 0){
throw new IllegalArgumentException("Stack is empty.");
}else {
return storage[count-1];
}
}
//判斷棧是否為空
private boolean isEmpty() {
return count == 0;
}
//返回棧中元素的個(gè)數(shù)
private int size() {
return count;
}
}
驗(yàn)證
MyStack myStack = new MyStack(3);
myStack.push(1);
myStack.push(2);
myStack.push(3);
myStack.push(4);
myStack.push(5);
myStack.push(6);
myStack.push(7);
myStack.push(8);
System.out.println(myStack.peek());//8
System.out.println(myStack.size());//8
for (int i = 0; i < 8; i++) {
System.out.println(myStack.pop());
}
System.out.println(myStack.isEmpty());//true
myStack.pop();//報(bào)錯(cuò):java.lang.IllegalArgumentException: Stack is empty.
4. 隊(duì)列
4.1. 隊(duì)列簡(jiǎn)介
隊(duì)列 是 先進(jìn)先出( FIFO,F(xiàn)irst In, First Out) 的線性表。在具體應(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ì)列的操作方式和堆棧類似,唯一的區(qū)別在于隊(duì)列只允許新數(shù)據(jù)在后端進(jìn)行添加。
假設(shè)隊(duì)列中有n個(gè)元素。
訪問(wèn):O(n)//最壞情況
插入刪除:O(1)//后端插入前端刪除元素
4.2. 隊(duì)列分類
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ì)列?!狥rom 《大話數(shù)據(jù)結(jié)構(gòu)》
4.2.2. 循環(huán)隊(duì)列
循環(huán)隊(duì)列可以解決順序隊(duì)列的假溢出和越界問(wèn)題。解決辦法就是:從頭開始,這樣也就會(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ì)列中則不一樣,也可能為滿,如上圖所示。解決辦法有兩種:
- 可以設(shè)置一個(gè)標(biāo)志變量
flag
,當(dāng)front==rear
并且flag=0
的時(shí)候隊(duì)列為空,當(dāng)front==rear
并且flag=1
的時(shí)候隊(duì)列為滿。 - 隊(duì)列為空的時(shí)候就是
front==rear
,隊(duì)列滿的時(shí)候,我們保證數(shù)組還有一個(gè)空閑的位置,rear 就指向這個(gè)空閑位置,如下圖所示,那么現(xiàn)在判斷隊(duì)列是否為滿的條件就是:(rear+1) % QueueSize= front
。
文章來(lái)源:http://www.zghlxwxcb.cn/news/detail-667448.html
4.3. 常見(jiàn)應(yīng)用場(chǎng)景
當(dāng)我們需要按照一定順序來(lái)處理數(shù)據(jù)的時(shí)候可以考慮使用隊(duì)列這個(gè)數(shù)據(jù)結(jié)構(gòu)。文章來(lái)源地址http://www.zghlxwxcb.cn/news/detail-667448.html
- 阻塞隊(duì)列: 阻塞隊(duì)列可以看成在隊(duì)列基礎(chǔ)上加了阻塞操作的隊(duì)列。當(dāng)隊(duì)列為空的時(shí)候,出隊(duì)操作阻塞,當(dāng)隊(duì)列滿的時(shí)候,入隊(duì)操作阻塞。使用阻塞隊(duì)列我們可以很容易實(shí)現(xiàn)“生產(chǎn)者 - 消費(fèi)者“模型。
-
線程池中的請(qǐng)求/任務(wù)隊(duì)列: 線程池中沒(méi)有空閑線程時(shí),新的任務(wù)請(qǐng)求線程資源時(shí),線程池該如何處理呢?答案是將這些請(qǐng)求放在隊(duì)列中,當(dāng)有空閑線程的時(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ì)列滿的話后面再有任務(wù)/請(qǐng)求就會(huì)拒絕,在 Java 中的體現(xiàn)就是會(huì)拋出java.util.concurrent.RejectedExecutionException
異常。 - Linux 內(nèi)核進(jìn)程隊(duì)列(按優(yōu)先級(jí)排隊(duì))
- 現(xiàn)實(shí)生活中的派對(duì),播放器上的播放列表;
- 消息隊(duì)列
到了這里,關(guān)于數(shù)據(jù)結(jié)構(gòu)——線性數(shù)據(jù)結(jié)構(gòu)(數(shù)組,鏈表,棧,隊(duì)列)的文章就介紹完了。如果您還想了解更多內(nèi)容,請(qǐng)?jiān)谟疑辖撬阉鱐OY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!