【数据结构与算法】数组与链表
數(shù)組的定義和特性
數(shù)組(Array)是一種線性表數(shù)據(jù)結(jié)構(gòu)。它用一組連續(xù)的內(nèi)存空間,來(lái)存儲(chǔ)一組具有相同類型的數(shù)據(jù)。
-
線性表(Linear List):數(shù)組、鏈表、隊(duì)列、棧 非線性表:樹(shù) 圖
-
連續(xù)的內(nèi)存空間和相同類型的數(shù)據(jù)
-
性能 低效“插入”和“刪除”
-
警惕 數(shù)組越界
數(shù)組和鏈表的區(qū)別
“鏈表適合插入、刪除,時(shí)間復(fù)雜度 O(1);數(shù)組適合查找,數(shù)組支持隨機(jī)訪問(wèn),根據(jù)下標(biāo)隨機(jī)訪問(wèn)的時(shí)間復(fù)雜度為 O(1)”。
(數(shù)組是適合查找操作,但是查找的時(shí)間復(fù)雜度并不為 O(1)。即便是排好序的數(shù)組,你用二分查找,時(shí)間復(fù)雜度也是 O(logn))
數(shù)組缺點(diǎn)
1)若申請(qǐng)內(nèi)存空間很大,比如100M,但若內(nèi)存空間沒(méi)有100M的連續(xù)空間時(shí),則會(huì)申請(qǐng)失敗,盡管內(nèi)存可用空間超過(guò)100M。
2)大小固定,若存儲(chǔ)空間不足,需進(jìn)行擴(kuò)容,一旦擴(kuò)容就要進(jìn)行數(shù)據(jù)復(fù)制,而這時(shí)非常費(fèi)時(shí)的。
鏈表缺點(diǎn)
1)內(nèi)存空間消耗更大,因?yàn)樾枰~外的空間存儲(chǔ)指針信息。
2)對(duì)鏈表進(jìn)行頻繁的插入和刪除操作,會(huì)導(dǎo)致頻繁的內(nèi)存申請(qǐng)和釋放,容易造成內(nèi)存碎片,如果是Java語(yǔ)言,還可能會(huì)造成頻繁的GC(自動(dòng)垃圾回收器)操作。
如何選擇?
數(shù)組簡(jiǎn)單易用,在實(shí)現(xiàn)上使用連續(xù)的內(nèi)存空間,可以借助CPU的緩沖機(jī)制預(yù)讀數(shù)組中的數(shù)據(jù),所以訪問(wèn)效率更高,而鏈表在內(nèi)存中并不是連續(xù)存儲(chǔ),所以對(duì)CPU緩存不友好,沒(méi)辦法預(yù)讀。
如果代碼對(duì)內(nèi)存的使用非常苛刻,那數(shù)組就更適合。
容器能否完全替代數(shù)組?
Java 使用ArrayList
ArrayList 最大的優(yōu)勢(shì)就是可以將很多數(shù)組操作的細(xì)節(jié)封裝起來(lái)。比如前面提到的數(shù)組插入、刪除數(shù)據(jù)時(shí)需要搬移其他數(shù)據(jù)等。另外,它還有一個(gè)優(yōu)勢(shì),就是支持動(dòng)態(tài)擴(kuò)容(不足擴(kuò)容1.5倍)。
Java ArrayList 無(wú)法存儲(chǔ)基本類型,比如 int、long,需要封裝為 Integer、Long 類,而 Autoboxing、Unboxing 則有一定的性能消耗,所以如果特別關(guān)注性能,或者希望使用基本類型,就可以選用數(shù)組。
如果數(shù)據(jù)大小事先已知,并且對(duì)數(shù)據(jù)的操作非常簡(jiǎn)單,用不到 ArrayList 提供的大部分方法,也可以直接使用數(shù)組。
當(dāng)要表示多維數(shù)組時(shí),用數(shù)組往往會(huì)更加直觀。比如 Object[][] array;而用容器的話則需要這樣定義:ArrayList object array。
對(duì)于業(yè)務(wù)開(kāi)發(fā),直接使用容器就足夠了,省時(shí)省力。畢竟損耗一丟丟性能,完全不會(huì)影響到系統(tǒng)整體的性能。但如果你是做一些非常底層的開(kāi)發(fā),比如開(kāi)發(fā)網(wǎng)絡(luò)框架,性能的優(yōu)化需要做到極致,這個(gè)時(shí)候數(shù)組就會(huì)優(yōu)于容器,成為首選。
為什么很多編程語(yǔ)言中數(shù)組都從0開(kāi)始編號(hào)?
移角度理解a[0] 0為偏移量,如果從1計(jì)數(shù),會(huì)多出K-1。增加cpu負(fù)擔(dān)。
為什么循環(huán)要寫成for(int i = 0;i<3;i++) 而不是for(int i = 0 ;i<=2;i++)。第一個(gè)直接就可以算出3-0 = 3 有三個(gè)數(shù)據(jù),而后者 2-0+1個(gè)數(shù)據(jù),多出1個(gè)加法運(yùn)算,很惱火。
有一定的歷史原因 C語(yǔ)言設(shè)計(jì)者用0開(kāi)始計(jì)數(shù)數(shù)組下標(biāo) 其他高級(jí)語(yǔ)言紛紛效仿
鏈表的定義和特性
數(shù)組需要一塊連續(xù)的內(nèi)存空間來(lái)存儲(chǔ),對(duì)內(nèi)存的要求比較高。
鏈表并不需要一塊連續(xù)的內(nèi)存空間,它通過(guò)“指針”將一組零散的內(nèi)存塊串聯(lián)起來(lái)使用。
鏈表常見(jiàn)分類
單鏈表
1)每個(gè)節(jié)點(diǎn)只包含一個(gè)指針,即后繼指針。
2)單鏈表有兩個(gè)特殊的節(jié)點(diǎn),即首節(jié)點(diǎn)和尾節(jié)點(diǎn)。為什么特殊?用首節(jié)點(diǎn)地址表示整條鏈表,尾節(jié)點(diǎn)的后繼指針指向空地址null。
3)性能特點(diǎn):插入和刪除節(jié)點(diǎn)的時(shí)間復(fù)雜度為O(1),查找的時(shí)間復(fù)雜度為O(n)。
循環(huán)鏈表
1)除了尾節(jié)點(diǎn)的后繼指針指向首節(jié)點(diǎn)的地址外均與單鏈表一致。
2)適用于存儲(chǔ)有循環(huán)特點(diǎn)的數(shù)據(jù),比如約瑟夫問(wèn)題。
循環(huán)鏈表是一種特殊的單鏈表。和單鏈表相比,循環(huán)鏈表的優(yōu)點(diǎn)是從鏈尾到鏈頭比較方便。當(dāng)要處理的數(shù)據(jù)具有環(huán)型結(jié)構(gòu)特點(diǎn)時(shí),就特別適合采用循環(huán)鏈表。
雙向鏈表
1)節(jié)點(diǎn)除了存儲(chǔ)數(shù)據(jù)外,還有兩個(gè)指針?lè)謩e指向前一個(gè)節(jié)點(diǎn)地址(前驅(qū)指針prev)和下一個(gè)節(jié)點(diǎn)地址(后繼指針next)。
2)首節(jié)點(diǎn)的前驅(qū)指針prev和尾節(jié)點(diǎn)的后繼指針均指向空地址。
3)性能特點(diǎn):
3.1 和單鏈表相比,存儲(chǔ)相同的數(shù)據(jù),需要消耗更多的存儲(chǔ)空間。
3.2 插入、刪除操作比單鏈表效率更高O(1)級(jí)別。以刪除操作為例,刪除操作分為2種情況:給定數(shù)據(jù)值刪除對(duì)應(yīng)節(jié)點(diǎn)和給定節(jié)點(diǎn)地址刪除節(jié)點(diǎn)。對(duì)于前一種情況,單鏈表和雙向鏈表都需要從頭到尾進(jìn)行遍歷從而找到對(duì)應(yīng)節(jié)點(diǎn)進(jìn)行刪除,時(shí)間復(fù)雜度為O(n)。對(duì)于第二種情況,要進(jìn)行刪除操作必須找到前驅(qū)節(jié)點(diǎn),單鏈表需要從頭到尾進(jìn)行遍歷直到p->next = q,時(shí)間復(fù)雜度為O(n),而雙向鏈表可以直接找到前驅(qū)節(jié)點(diǎn),時(shí)間復(fù)雜度為O(1)。
3.3對(duì)于一個(gè)有序鏈表,雙向鏈表的按值查詢效率要比單鏈表高一些。因?yàn)槲覀兛梢杂涗浬洗尾檎业奈恢胮,每一次查詢時(shí),根據(jù)要查找的值與p的大小關(guān)系,決定是往前還是往后查找,所以平均只需要查找一半的數(shù)據(jù)。
雙向鏈表需要額外的兩個(gè)空間來(lái)存儲(chǔ)后繼結(jié)點(diǎn)和前驅(qū)結(jié)點(diǎn)的地址。所以,如果存儲(chǔ)同樣多的數(shù)據(jù),雙向鏈表要比單鏈表占用更多的內(nèi)存空間。
雙向循環(huán)鏈表
首節(jié)點(diǎn)的前驅(qū)指針指向尾節(jié)點(diǎn),尾節(jié)點(diǎn)的后繼指針指向首節(jié)點(diǎn)。
應(yīng)用
1. 如何分別用鏈表和數(shù)組實(shí)現(xiàn)LRU緩沖淘汰策略?
1)什么是緩存?
緩存是一種提高數(shù)據(jù)讀取性能的技術(shù),在硬件設(shè)計(jì)、軟件開(kāi)發(fā)中都有著非廣泛的應(yīng)用,比如常見(jiàn)的CPU緩存、數(shù)據(jù)庫(kù)緩存、瀏覽器緩存等等。
2)為什么使用緩存?即緩存的特點(diǎn)
緩存的大小是有限的,當(dāng)緩存被用滿時(shí),哪些數(shù)據(jù)應(yīng)該被清理出去,哪些數(shù)據(jù)應(yīng)該被保留?就需要用到緩存淘汰策略。
3)什么是緩存淘汰策略?
指的是當(dāng)緩存被用滿時(shí)清理數(shù)據(jù)的優(yōu)先順序。
4)有哪些緩存淘汰策略?
常見(jiàn)的3種包括先進(jìn)先出策略FIFO(First In,First Out)、最少使用策略LFU(Least Frenquently Used)、最近最少使用策略LRU(Least Recently Used)。
5)鏈表實(shí)現(xiàn)LRU緩存淘汰策略
當(dāng)訪問(wèn)的數(shù)據(jù)沒(méi)有存儲(chǔ)在緩存的鏈表中時(shí),直接將數(shù)據(jù)插入鏈表表頭,時(shí)間復(fù)雜度為O(1);
如果緩存被占滿,則從鏈表尾部的數(shù)據(jù)開(kāi)始清理,將新的數(shù)據(jù)結(jié)點(diǎn)插入鏈表的頭部,時(shí)間復(fù)雜度為O(1)。
當(dāng)訪問(wèn)的數(shù)據(jù)存在于存儲(chǔ)的鏈表中時(shí),將該數(shù)據(jù)對(duì)應(yīng)的節(jié)點(diǎn)刪除,插入到鏈表表頭,時(shí)間復(fù)雜度為O(n)。
6)數(shù)組實(shí)現(xiàn)LRU緩存淘汰策略
首位置保存最新訪問(wèn)數(shù)據(jù),末尾位置優(yōu)先清理
當(dāng)訪問(wèn)的數(shù)據(jù)未存在于緩存的數(shù)組中時(shí),直接將數(shù)據(jù)插入數(shù)組第一個(gè)元素位置,此時(shí)數(shù)組所有元素需要向后移動(dòng)1個(gè)位置,時(shí)間復(fù)雜度為O(n);
當(dāng)訪問(wèn)的數(shù)據(jù)存在于緩存的數(shù)組中時(shí),查找到數(shù)據(jù)并將其插入數(shù)組的第一個(gè)位置,此時(shí)亦需移動(dòng)數(shù)組元素,時(shí)間復(fù)雜度為O(n)。
緩存用滿時(shí),則清理掉末尾的數(shù)據(jù),時(shí)間復(fù)雜度為O(1)。
(優(yōu)化:清理的時(shí)候可以考慮一次性清理一定數(shù)量,從而降低清理次數(shù),提高性能。)
2.如何通過(guò)單鏈表實(shí)現(xiàn)“判斷某個(gè)字符串是否為水仙花字符串”?(比如 上海自來(lái)水來(lái)自海上)
1)前提:字符串以單個(gè)字符的形式存儲(chǔ)在單鏈表中。
2)遍歷鏈表,判斷字符個(gè)數(shù)是否為奇數(shù),若為偶數(shù),則不是。
3)將鏈表中的字符倒序存儲(chǔ)一份在另一個(gè)鏈表中。
4)同步遍歷2個(gè)鏈表,比較對(duì)應(yīng)的字符是否相等,若相等,則是水仙花字串,否則,不是。
寫鏈表代碼技巧
技巧一:理解指針或引用的含義
將某個(gè)變量賦值給指針,實(shí)際上就是將這個(gè)變量的地址賦值給指針,或者反過(guò)來(lái)說(shuō),指針中存儲(chǔ)了這個(gè)變量的內(nèi)存地址,指向了這個(gè)變量,通過(guò)指針就能找到這個(gè)變量。
技巧二:警惕指針丟失和內(nèi)存泄漏
插入結(jié)點(diǎn)時(shí),一定要注意操作的順序
刪除鏈表結(jié)點(diǎn)時(shí),也一定要記得手動(dòng)釋放內(nèi)存空間
技巧三:利用哨兵簡(jiǎn)化實(shí)現(xiàn)難度
哨兵最大的作用就是簡(jiǎn)化邊界條件的處理
針對(duì)鏈表的插入、刪除操作,需要對(duì)插入第一個(gè)結(jié)點(diǎn)和刪除最后一個(gè)結(jié)點(diǎn)的情況進(jìn)行特殊處理。
如果我們引入哨兵結(jié)點(diǎn),在任何時(shí)候,不管鏈表是不是空,head 指針都會(huì)一直指向這個(gè)哨兵結(jié)點(diǎn)。我們也把這種有哨兵結(jié)點(diǎn)的鏈表叫帶頭鏈表。相反,沒(méi)有哨兵結(jié)點(diǎn)的鏈表就叫作不帶頭鏈表。
技巧四:重點(diǎn)留意邊界條件處理
- 如果鏈表為空時(shí),代碼是否能正常工作?
- 如果鏈表只包含一個(gè)結(jié)點(diǎn)時(shí),代碼是否能正常工作?
- 如果鏈表只包含兩個(gè)結(jié)點(diǎn)時(shí),代碼是否能正常工作?
- 代碼邏輯在處理頭結(jié)點(diǎn)和尾結(jié)點(diǎn)的時(shí)候,是否能正常工作?
技巧五:舉例畫圖,輔助思考
舉例法和畫圖法
技巧六:多寫多練,沒(méi)有捷徑
熟練 寫鏈表代碼是最考驗(yàn)邏輯思維能力的
- 單鏈表反轉(zhuǎn)
- 鏈表中環(huán)的檢測(cè)
- 兩個(gè)有序的鏈表合并
- 刪除鏈表倒數(shù)第 n 個(gè)結(jié)點(diǎn)
- 求鏈表的中間結(jié)點(diǎn)
練習(xí)題LeetCode對(duì)應(yīng)編號(hào):206,141,21,19,876
設(shè)計(jì)思想
對(duì)于執(zhí)行較慢的程序,可以通過(guò)消耗更多的內(nèi)存(空間換時(shí)間)來(lái)進(jìn)行優(yōu)化;而消耗過(guò)多內(nèi)存的程序,可以通過(guò)消耗更多的時(shí)間(時(shí)間換空間)來(lái)降低內(nèi)存的消耗。
筆記整理來(lái)源: 王爭(zhēng) 數(shù)據(jù)結(jié)構(gòu)與算法之美
總結(jié)
以上是生活随笔為你收集整理的【数据结构与算法】数组与链表的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。
- 上一篇: velocityjs 动画库 比jque
- 下一篇: 752. Open the Lock