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