前言
這一篇筆記主要記錄總結了線性表
資料結構中的連結串列
概念,以及和陣列
的對比,陣列和連結串列都是計算機中最基本的資料結構,是構建其他高階複雜資料的結構的基礎。開發中應該根據具體的場景選擇最合適的資料結構和演算法。下一篇將會實現連結串列相關的演算法。
連結串列概念
上一篇陣列講了陣列是一種是一種線性表
資料結構,是用一組連續的記憶體空間
,來儲存一組具有相同資料型別
的資料。和陣列相同的是,連結串列
也是一種線性表
資料結構,不同的是連結串列是通過指標將一組零散的記憶體塊
串聯起來儲存資料,可以儲存不同資料型別
的資料。
連結串列的種類五花八門,主要有以下幾類:
單連結串列
每個記憶體塊被稱為連結串列的結點
,每個結點包含了資料域
和指標域
,資料域儲存資料,指標域的指標指向下一個結點的記憶體地址
,稱為後繼指標next
。
另外,通常把第一個結點稱作頭結點
,頭結點用來記錄連結串列的基地址
,有了頭結點就可以遍歷整個連結串列。最後一個結點稱作尾節點
,尾結點的指標指向NUll
。
迴圈連結串列
迴圈連結串列
是一種特殊的單連結串列
。和單連結串列唯一的區別是迴圈連結串列的尾指標不是指向NUll,而是指向頭結點
。像環一樣首尾相連,所以叫做迴圈連結串列
。
迴圈連結串列
的優點是在處理的資料具有環形結構特點時,特別適合使用,如經典的約瑟夫問題。
雙向連結串列
雙向連結串列
除了像單連結串列一樣結點指標域有後繼指標外,還有前驅指標prev
,指向上一個結點
。
缺點是需要額外開闢兩個空間來儲存後繼結點和前驅結點的地址,如果儲存同樣的資料,雙向連結串列要比單連結串列佔用更多的空間
。
優點是支援雙向遍歷
,可以在 O(1)的複雜度
下找到前驅結點,所以在某些情況下的插入、刪除等操作比單連結串列簡單高效。
如刪除指定指標指向的結點q,單連結串列需要先遍歷找到到這個結點的前驅結點,直到 p->nex = q, 時間複雜度是O(n),而雙向連結串列
的結點指標域中已經儲存了前驅結點的指標,不需要遍歷,時間複雜度是O(1)。同理,要在指定結點前面插入一個結點,使用雙向連結串列
也是隻要 O(1) 複雜度就能完成。
另外,對於有序連結串列,查詢一個資料時,可以記錄上次查詢的資料的位置p,後面的查詢的資料可以和 p 位置的資料比大小,決定是向前還是向後查詢,這樣平均查詢只需要找一半的資料。
雙向連結串列
裡面有個重要的設計思想就是空間換時間
思想,當需要很在意執行時間時,可以選擇空間複雜度較高而時間複雜度相對較低的演算法或者資料結構,相反,如果記憶體空間比較重要,如在微控制器上的程式,就要反過來使用時間換空間
思想。快取
的思想就是利用了空間換時間,讓經常使用到的資料儲存到到高速的記憶體中,大大提高了資料讀取的速度。
雙向迴圈連結串列
雙向迴圈連結串列
是雙向連結串列
和迴圈連結串列
的結合體,相比較雙向連結串列
,尾結點的後繼指標
指向了頭結點,頭結點的前驅指標
指向尾結點。
連結串列和陣列對比
可以看到,再演算法時間複雜度上,陣列和連結串列在隨機訪問
和插入刪除
的複雜度上正好相反
。
陣列是用一組連續的記憶體空間來儲存資料,優點是可以藉助 CPU 快取機制,預先讀取陣列中的資料,訪問效率更高,而連結串列在記憶體中不連續,所以對 CPU 快取不友好。
陣列的缺點是大小固定,要佔用整塊連續的記憶體空間,如果宣告的陣列過大,系統可能沒有足夠連續的空間給分配,就會導致記憶體不足,如果申請的陣列大小過小,出現不夠用,就需要重新申請一塊更大的連續記憶體空間,然後將之前的資料全部拷貝一份過來,這個過程很耗時,而連結串列天然就支援動態擴容
。
所以,如果程式碼對記憶體使用很苛刻,就使用用陣列,因為連結串列儲存相同的資料,需要更多的記憶體空間。在實際的開發中,需要根據不同情況選用最合適的資料結構和演算法。
LRU 快取淘汰演算法
最近最少使用策略 LRU
是一種常見的快取策略
。常用快取策略
的有下列幾種:
- 先進先出策略 FIFO(First In,Fitst Out)。
- 最少使用策略 LFU(Least Frequently Used)。
- 最近最少使用策略 LRU(Least Recently Used)。
連結串列實現
維護一個有序的單連結串列
,越靠近連結串列頭部的越是最近訪問的,也靠近尾結點的是越早之前訪問的。
- 當該資料存在快取的連結串列中,快取命中,遍歷得到資料對應到的結點,從原位置刪除,然後將該結點插入到頭結點位置。
- 沒有命中快取,需要將該資料加入到快取中,如果快取未滿,直接將該資料結點插入到連結串列的頭部,如果快取已滿,刪除連結串列尾結點,再將該資料結點插入到連結串列的頭結點位置。
陣列實現
維護一個有序的陣列,下標越小越是最近訪問的,下標越大越是越早之前訪問的。
- 當該資料存在陣列中,快取命中,從原位置刪除該元素,然後將該元素插入到陣列的首位置。
- 快取沒有命中,需要將資料存入到快取中。這個時候如果快取還沒滿,就直接將該資料插入到陣列的首位置。如果快取已經滿了,先刪除陣列最後一個元素,然後再將該資料插入到陣列的首位置。
課後問題
- 如何判斷一個字串是否是迴文字串的問題,我想你應該聽過,我們今天的思題目就是基於這個問題的改造版本。如果字串是通過單連結串列來儲存的,那該如何來判斷是一個迴文串呢?你有什麼好的解決思路呢?相應的時間空間複雜度又是多少呢?
解答:
- 使用快慢指標定位到中間結點,同時翻轉連結串列前半部分使其逆序;
- 如果連結串列長度是偶數,那麼快指標為空,此時慢指標指向的是下中位結點,如果是連結串列長度是奇數,快指標不為空,此時慢指標指向的正好是連結串列的中間結點,再將慢指標向前走一步。
- 遍歷連結串列比較連結串列兩端的結點,如果有一個結點不相等,則不是迴文串,遍歷結束,如果都相等則是迴文串。
- 時間複雜度遍歷一遍連結串列是O(n),空間複雜度沒有利用額外的空間是O(1)。
程式碼如下:
/*
* @lc app=leetcode.cn id=234 lang=c
*
* [234] 迴文連結串列
*
* https://leetcode-cn.com/problems/palindrome-linked-list/description/
*
* algorithms
* Easy (34.93%)
* Total Accepted: 22.3K
* Total Submissions: 61.2K
* Testcase Example: '[1,2]'
*
* 請判斷一個連結串列是否為迴文連結串列。
*
* 示例 1:
*
* 輸入: 1->2
* 輸出: false
*
* 示例 2:
*
* 輸入: 1->2->2->1
* 輸出: true
*
*
* 進階:
* 你能否用 O(n) 時間複雜度和 O(1) 空間複雜度解決此題?
*
*/
/**
* Definition for singly-linked list.
* struct ListNode {
* int val;
* struct ListNode *next;
* };
*/
bool isPalindrome(struct ListNode* head){
if(head == NULL || head->next == NULL) {
return true;
}
// 快慢指標獲取中間結點指標
// 快指標,每次走兩步
struct ListNode *fast = head;
// 慢指標,每次走一步
struct ListNode *slow = head;
struct ListNode *previous = NULL;
while (slow && fast && fast->next) {
fast = fast->next->next;
// 將前半部分連結串列翻轉
struct ListNode *temp = slow->next;
slow->next = previous;
previous = slow;
slow = temp;
}
// 連結串列長度是偶數時,fast 為空
if (fast) {
// 連結串列數是奇數,此時 slow 指向中間結點,provious 指向頭結點,slow指標需要向前走一步,
slow = slow->next;
}
while (slow) {
if (slow->val != previous->val) {
return false;
}
slow = slow->next;
previous = previous->next;
}
return true;
}
複製程式碼
擴充套件閱讀
分享個人技術學習記錄和跑步馬拉松訓練比賽、讀書筆記等內容,感興趣的朋友可以關注我的公眾號「青爭哥哥」。